Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 3 additions & 2 deletions src/commands/docs-list.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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));
Expand All @@ -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));

Expand Down
3 changes: 2 additions & 1 deletion src/commands/docs-view.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions src/commands/pull.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down
9 changes: 3 additions & 6 deletions src/commands/push.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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;
Expand Down
6 changes: 3 additions & 3 deletions src/commands/repo-create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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(() => {});

Expand Down
6 changes: 3 additions & 3 deletions src/commands/sync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand Down Expand Up @@ -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();
Expand Down
22 changes: 13 additions & 9 deletions src/lib/prismic/clients/docs.ts
Original file line number Diff line number Diff line change
@@ -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(),
Expand All @@ -23,25 +28,25 @@ const DocsPageSchema = z.object({
});
type DocsPage = z.infer<typeof DocsPageSchema>;

export async function getDocsIndex(): Promise<DocsIndexEntry[]> {
const url = new URL("api/index/", getDocsServiceUrl());
export async function getDocsIndex(config?: DocsConfig): Promise<DocsIndexEntry[]> {
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<DocsPage> {
const url = new URL(`api/index/${path}`, getDocsServiceUrl());
export async function getDocsPageIndex(path: string, config?: DocsConfig): Promise<DocsPage> {
const url = new URL(`api/index/${path}`, getDocsServiceUrl(config?.host));
return request(url, {
schema: DocsPageSchema,
notFoundMessage: `Documentation page not found: ${path}`,
unknownErrorMessage: "Failed to fetch documentation index",
});
}

export async function getDocsPageContent(path: string): Promise<string> {
const url = new URL(path, getDocsServiceUrl());
export async function getDocsPageContent(path: string, config?: DocsConfig): Promise<string> {
const url = new URL(path, getDocsServiceUrl(config?.host));
return request(url, {
headers: { Accept: "text/markdown" },
schema: z.string(),
Expand All @@ -50,7 +55,6 @@ export async function getDocsPageContent(path: string): Promise<string> {
});
}

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/`);
}
10 changes: 0 additions & 10 deletions src/lib/prismic/clients/repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -63,16 +63,6 @@ export async function completeOnboardingSteps(
}
}

export async function completeOnboardingStepsSilently(
config: OnboardingConfig & { stepIds: OnboardingStep[] },
): Promise<void> {
try {
await completeOnboardingSteps(config);
} catch {
// Ignore errors
}
}

function repositoryServiceRequest<T>(
url: URL,
config: RepositoryConfig,
Expand Down
Loading