diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..7e65a9c --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,68 @@ +# File Organization + +Rules for where code goes in this repository. They describe the kind of code +each place holds, not the contents of specific files. Follow them when adding, +moving, or reviewing code. + +**Principle: keep logic at its call site. Move it to a shared location only +when sharing becomes necessary, never preemptively.** + +## Reviewing + +When reviewing a change, check every added or moved file against these rules, +and check that new code sits in the allowed layer for its location. Report +violations as findings even when the code works correctly. + +## `src/` + +Root files are cross-cutting concerns shared by many commands, one flat file +per concern. These files may know about Prismic and the CLI. + +## `src/commands/` + +One file per CLI command, flat. The filename is the full command path with +dashes: `repo-create.ts` handles `prismic repo create`. Parent commands are +routers only, with no logic. Leaf command files own the user experience: +options, output, and orchestration of everything else. Logic stays in the +command file unless another command needs it. + +## `src/lib/` + +Self-contained modules that know nothing about the application. A lib module +knows only its own subject and could in principle be extracted as a standalone +package. It does not know it is part of a CLI and does not reach into commands +or app state; callers pass in what it needs. + +### `src/lib/prismic/` + +Prismic-domain logic shared by multiple commands. Still application-unaware. + +### `src/lib/prismic/clients/` + +One file per backend service. Network calls only: build the request, validate +the response, return typed data. No orchestration, no business decisions, no +console output. + +## `src/adapters/` + +Framework-specific integrations. One file per framework, with a sibling +`*.templates.ts` file for generated file content. + +## `src/subprocesses/` + +Entry scripts for detached background work. Minimal: parse input, call one +function, exit quietly. + +## `test/` + +End-to-end tests organized by command, mirroring `src/commands/` filenames +one-to-one: `repo-create.test.ts` tests `repo-create.ts`. Never organized by +feature or concern. Non-test files are shared helpers; helpers never import +from `src/`. + +## `evals/` + +AI agent evaluations. One `*.eval.ts` file per agent capability, named as a +behavior phrase like `sync-models.eval.ts`. Organized by what an agent should +be able to do, not by command. Non-eval files are the shared harness; eval +files may reuse `test/` helpers. diff --git a/src/commands/docs-list.ts b/src/commands/docs-list.ts index c7909d4..2190c5c 100644 --- a/src/commands/docs-list.ts +++ b/src/commands/docs-list.ts @@ -1,3 +1,4 @@ +import { env } from "../env"; import { createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; import { getDocsIndex, getDocsPageIndex } from "../lib/prismic/clients/docs"; @@ -26,7 +27,7 @@ export default createCommand(config, async ({ positionals, values }) => { const { json } = values; if (path) { - const entry = await getDocsPageIndex(path); + const entry = await getDocsPageIndex(path, { host: env.PRISMIC_DOCS_HOST }); if (json) { console.info(stringify(entry)); @@ -41,7 +42,7 @@ export default createCommand(config, async ({ positionals, values }) => { const rows = entry.anchors.map((anchor) => [`${path}#${anchor.slug}`, anchor.excerpt]); console.info(formatTable(rows, { headers: ["PATH", "EXCERPT"] })); } else { - const pages = await getDocsIndex(); + const pages = await getDocsIndex({ host: env.PRISMIC_DOCS_HOST }); pages.sort((a, b) => a.path.localeCompare(b.path)); diff --git a/src/commands/docs-view.ts b/src/commands/docs-view.ts index 582ddaa..6bd2a78 100644 --- a/src/commands/docs-view.ts +++ b/src/commands/docs-view.ts @@ -1,5 +1,6 @@ import GithubSlugger from "github-slugger"; +import { env } from "../env"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { stringify } from "../lib/json"; import { getDocsPageContent } from "../lib/prismic/clients/docs"; @@ -30,7 +31,7 @@ export default createCommand(config, async ({ positionals, values }) => { const path = hashIndex >= 0 ? rawPath.slice(0, hashIndex) : rawPath; const anchor = hashIndex >= 0 ? rawPath.slice(hashIndex + 1) : undefined; - let markdown = await getDocsPageContent(path); + let markdown = await getDocsPageContent(path, { host: env.PRISMIC_DOCS_HOST }); if (anchor) { const section = extractSection(markdown, anchor); diff --git a/src/commands/pull.ts b/src/commands/pull.ts index d0aa8fd..7b287b4 100644 --- a/src/commands/pull.ts +++ b/src/commands/pull.ts @@ -4,7 +4,7 @@ import { CommandError, createCommand, type CommandConfig } from "../lib/command" import { diffArrays } from "../lib/diff"; import { getDirtyPaths, getGitRoot } from "../lib/git"; import { getCustomTypes, getSlices } from "../lib/prismic/clients/custom-types"; -import { completeOnboardingStepsSilently } from "../lib/prismic/clients/repository"; +import { completeOnboardingSteps } from "../lib/prismic/clients/repository"; import { canonicalizeCustomType, canonicalizeSlice } from "../lib/prismic/models"; import { isDescendant, relativePathname } from "../lib/url"; import { findProjectRoot, getRepositoryName } from "../project"; @@ -145,12 +145,12 @@ export default createCommand(config, async ({ values }) => { await adapter.generateTypes(); - await completeOnboardingStepsSilently({ + await completeOnboardingSteps({ repo: await getRepositoryName(), token, host, stepIds: ["connectPrismic"], - }); + }).catch(() => {}); const totalTypes = customTypeOps.insert.length + customTypeOps.update.length; const totalSlices = sliceOps.insert.length + sliceOps.update.length; diff --git a/src/commands/push.ts b/src/commands/push.ts index ea66f85..209a253 100644 --- a/src/commands/push.ts +++ b/src/commands/push.ts @@ -19,10 +19,7 @@ import { updateCustomType, updateSlice, } from "../lib/prismic/clients/custom-types"; -import { - completeOnboardingStepsSilently, - type OnboardingStep, -} from "../lib/prismic/clients/repository"; +import { completeOnboardingSteps, type OnboardingStep } from "../lib/prismic/clients/repository"; import { canonicalizeCustomType, canonicalizeSlice } from "../lib/prismic/models"; import { BadRequestError } from "../lib/request"; import { appendTrailingSlash, isDescendant, relativePathname } from "../lib/url"; @@ -172,12 +169,12 @@ export default createCommand(config, async ({ values }) => { onboardingSteps.push("createPageType"); } if (onboardingSteps.length > 0) { - await completeOnboardingStepsSilently({ + await completeOnboardingSteps({ repo: await getRepositoryName(), token, host, stepIds: onboardingSteps, - }); + }).catch(() => {}); } const totalTypes = customTypeOps.insert.length + customTypeOps.update.length; diff --git a/src/commands/repo-create.ts b/src/commands/repo-create.ts index a4b8709..dc4f083 100644 --- a/src/commands/repo-create.ts +++ b/src/commands/repo-create.ts @@ -4,7 +4,7 @@ import { detectAgent } from "../lib/ai"; import { CommandError, createCommand, type CommandConfig } from "../lib/command"; import { upsertLocale } from "../lib/prismic/clients/locale"; import { activateMCP } from "../lib/prismic/clients/mcp"; -import { completeOnboardingStepsSilently } from "../lib/prismic/clients/repository"; +import { completeOnboardingSteps } from "../lib/prismic/clients/repository"; import { checkIsDomainAvailable, createRepository } from "../lib/prismic/clients/wroom"; const MAX_DOMAIN_TRIES = 5; @@ -54,12 +54,12 @@ export async function createRepo(config: { // A new repository has no locale, so set the master locale to make it usable. await upsertLocale({ id: lang, isMaster: true }, { repo: domain, token, host }); - await completeOnboardingStepsSilently({ + await completeOnboardingSteps({ repo: domain, token, host, stepIds: ["createPrismicProject"], - }); + }).catch(() => {}); await activateMCP({ repo: domain, token, host }).catch(() => {}); diff --git a/src/commands/sync.ts b/src/commands/sync.ts index 20c79c4..f94e026 100644 --- a/src/commands/sync.ts +++ b/src/commands/sync.ts @@ -8,7 +8,7 @@ import { getErrorMessage } from "../error"; import { createCommand, type CommandConfig, CommandError } from "../lib/command"; import { diffArrays } from "../lib/diff"; import { getCustomTypes, getSlices } from "../lib/prismic/clients/custom-types"; -import { completeOnboardingStepsSilently } from "../lib/prismic/clients/repository"; +import { completeOnboardingSteps } from "../lib/prismic/clients/repository"; import { getRepositoryName } from "../project"; import { trackCommandStart, trackCommandEnd } from "../tracking"; @@ -121,12 +121,12 @@ export default createCommand(config, async ({ values }) => { lastHash = nextHash; if (isInitial) { - await completeOnboardingStepsSilently({ + await completeOnboardingSteps({ repo: await getRepositoryName(), token, host, stepIds: ["connectPrismic"], - }); + }).catch(() => {}); console.info("Initial sync complete."); } else { const timestamp = new Date().toLocaleTimeString(); diff --git a/src/lib/prismic/clients/docs.ts b/src/lib/prismic/clients/docs.ts index d2aa2f4..df11c0c 100644 --- a/src/lib/prismic/clients/docs.ts +++ b/src/lib/prismic/clients/docs.ts @@ -1,8 +1,13 @@ import * as z from "zod/mini"; -import { DEFAULT_PRISMIC_HOST, env } from "../../../env"; import { request } from "../../request"; +// Documentation is only published at prismic.io; the host option exists for +// overrides only. +const DEFAULT_DOCS_HOST = "prismic.io"; + +type DocsConfig = { host?: string }; + const DocsIndexEntrySchema = z.object({ path: z.string(), title: z.string(), @@ -23,16 +28,16 @@ const DocsPageSchema = z.object({ }); type DocsPage = z.infer; -export async function getDocsIndex(): Promise { - const url = new URL("api/index/", getDocsServiceUrl()); +export async function getDocsIndex(config?: DocsConfig): Promise { + const url = new URL("api/index/", getDocsServiceUrl(config?.host)); return request(url, { schema: z.array(DocsIndexEntrySchema), unknownErrorMessage: "Failed to fetch documentation index", }); } -export async function getDocsPageIndex(path: string): Promise { - const url = new URL(`api/index/${path}`, getDocsServiceUrl()); +export async function getDocsPageIndex(path: string, config?: DocsConfig): Promise { + const url = new URL(`api/index/${path}`, getDocsServiceUrl(config?.host)); return request(url, { schema: DocsPageSchema, notFoundMessage: `Documentation page not found: ${path}`, @@ -40,8 +45,8 @@ export async function getDocsPageIndex(path: string): Promise { }); } -export async function getDocsPageContent(path: string): Promise { - const url = new URL(path, getDocsServiceUrl()); +export async function getDocsPageContent(path: string, config?: DocsConfig): Promise { + const url = new URL(path, getDocsServiceUrl(config?.host)); return request(url, { headers: { Accept: "text/markdown" }, schema: z.string(), @@ -50,7 +55,6 @@ export async function getDocsPageContent(path: string): Promise { }); } -function getDocsServiceUrl(): URL { - const host = env.PRISMIC_DOCS_HOST ?? DEFAULT_PRISMIC_HOST; +function getDocsServiceUrl(host = DEFAULT_DOCS_HOST): URL { return new URL(`https://${host}/docs/`); } diff --git a/src/lib/prismic/clients/repository.ts b/src/lib/prismic/clients/repository.ts index fdfe1bf..3197b41 100644 --- a/src/lib/prismic/clients/repository.ts +++ b/src/lib/prismic/clients/repository.ts @@ -63,16 +63,6 @@ export async function completeOnboardingSteps( } } -export async function completeOnboardingStepsSilently( - config: OnboardingConfig & { stepIds: OnboardingStep[] }, -): Promise { - try { - await completeOnboardingSteps(config); - } catch { - // Ignore errors - } -} - function repositoryServiceRequest( url: URL, config: RepositoryConfig,