Skip to content

feat(cli): self-documenting CLI — colocated typed docs for every command, API function, and schema - #4714

Merged
josephfarina merged 16 commits into
mainfrom
docs/cli
Aug 5, 2026
Merged

josephfarina merged 16 commits into
mainfrom
docs/cli

Conversation

@josephfarina

Copy link
Copy Markdown
Contributor

Summary

Makes the CLI self-documenting: a typed, colocated .doc.mjs sits next to every documentable surface, validated so it cannot drift from the code.

  • Doc-types (new, on @astryxdesign/cli/authoring): FunctionDoc, CommandDoc, SchemaDoc, EnumDoc, each with a sealed zod parser wired into parseDoc. Generalizes the shared 'function' doc so hooks and CLI/API functions are one kind.
  • ~57 colocated docs: a FunctionDoc for every @astryxdesign/cli/api export, a CommandDoc for every command/subcommand (referencing its function via fn), a SchemaDoc for config/integration/codemod, the response envelope, and the doc-types themselves, and EnumDocs for the error-code and response-type vocabularies.
  • defineCommand converter (CommandDoc → Commander) so --help and the manifest can be sourced from the docs.
  • Two CI-gated harnesses: a drift harness (every CommandDoc's fn/args/options resolve to a real function and match the manifest; the EnumDocs equal ERROR_CODES / the manifest response-type set exactly) and a golden harness (the whole observable CLI surface — help, manifest, command output, error paths, exit codes — is byte-stable).
  • CONTRIBUTING.md: a new "Working on the astryx CLI" section.

Additive only: the CLI's observable behavior is unchanged (golden --check 0-diff throughout). Every .doc.mjs is type-checked in CI via typecheck:strict (checkJs over api/, clients/, foundation/, authoring/).

Test plan

  • pnpm -F @astryxdesign/cli test (parser + drift + golden suites)
  • pnpm -F @astryxdesign/cli golden:check -> 0 diffs
  • pnpm -F @astryxdesign/cli typecheck:strict (the new .doc.mjs type-check)
  • pnpm -F @astryxdesign/cli typecheck:authoring
  • node packages/cli/test/drift/docs-drift.mjs -> 0 drift

Follow-ups (not in this PR)

  • Surface the docs in astryx docs (namespaced discovery + rendering) and on the doc site.
  • Migrate command registrations to defineCommand so --help is sourced from the docs.
  • Generate the README command/error/response tables from the manifest + EnumDocs.

Made with Cursor

@vercel

vercel Bot commented Aug 4, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
astryx Ready Ready Preview Aug 5, 2026 6:27pm

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Aug 4, 2026
@github-actions

github-actions Bot commented Aug 4, 2026 •

Copy link
Copy Markdown
Contributor

PR Analysis Report

📚 Storybook Preview

View Storybook for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

🧪 Sandbox Preview

View Sandbox for this PR
GitHub Pages may take up to a minute to hydrate after deploy.

No new or modified components detected.

Bundle Size Summary

No component packages changed.

Accessibility Audit

Status: No accessibility violations detected.


Generated by PR Enrichment workflow | Storybook | Sandbox | View full report

josephfarina and others added 12 commits August 5, 2026 10:00
…-types

Adds the typed, colocated doc vocabulary the CLI docs will be authored against,
and generalizes the shared `type: 'function'` doc so hooks and CLI/API functions
are one kind:

- FunctionDoc — a function (hook or @astryxdesign/cli/api export); `returns` now
  also accepts nameless {type,data} envelope entries (relaxed, back-compat).
- SchemaDoc — an authored object shape (config, integration, codemod, the
  doc-types, the response envelope) with recursive field docs.
- CommandDoc — a CLI command; references its FunctionDoc via `fn` (feeds the
  later defineCommand -> --help converter).
- EnumDoc — a closed vocabulary (error codes, response-type discriminants).

Each has a sealed zod parser wired into parseDoc's discriminant switch and
exported from @astryxdesign/cli/authoring. Purely additive: existing docs still
validate (typecheck:authoring green) and the CLI surface is unchanged
(golden --check: 0 diffs).

Co-authored-by: Cursor <cursoragent@cursor.com>
Adds a colocated FunctionDoc (`<fn>.doc.mjs`, kind: 'api') for every public
@astryxdesign/cli/api export — component, docs, hook, template, blog, discover,
swizzle, theme{Build,Add,List}, listThemes, layout{Expand,Check,Grammar}, search,
build, upgrade, init, doctor, validateIntegration, summarizeIssues.

Each is grounded in its function's source of truth: params from the Options
typedef, returns as one {type,data} envelope entry per response discriminant, and
throws enumerated from the real ERROR_CODES throw sites. A colocated
api-docs-parse test validates every doc through parseDoc (21 docs). Additive only
— the CLI surface is unchanged (golden --check: 0 diffs).

Co-authored-by: Cursor <cursoragent@cursor.com>
Colocated docs for the rest of the CLI surface:
- CommandDocs (clients/cli/commands/*.doc.mjs) — one per command/subcommand,
  each referencing its FunctionDoc via `fn`, with flags mapped to params.
- SchemaDocs — the authored objects: config, integration, codemod, the response
  envelope, and the doc-types themselves (component/hook/function/reference/
  template/schema/command/enum) so authors can finally see each shape.
- EnumDocs — the error-codes and response-types vocabularies.

Adds a drift harness (packages/cli/test/drift): every colocated doc must parse;
each CommandDoc's fn + arg/option params must resolve to a real FunctionDoc; the
command name must exist in the manifest; and the two EnumDocs must equal
ERROR_CODES / the manifest response-type set exactly. 57 docs checked, 0 drift.
Additive only — golden --check: 0 diffs; typecheck:authoring green.

Co-authored-by: Cursor <cursoragent@cursor.com>
defineCommand (clients/cli/lib) turns a typed CommandDoc (+ the FunctionDoc it
wraps, for inherited param descriptions) into a configured Commander command —
the single converter that lets --help + the manifest be sourced from the same
colocated docs that feed `astryx docs` and the docsite. Unit-tested against
search's doc.

Extends the drift harness so every CommandDoc must mirror the live CLI exactly:
args (name/required/variadic), option flags, and subcommands all match the
manifest (57 docs, 0 drift). Registrations are not switched over yet, so the CLI
surface is unchanged (golden --check: 0 diffs); flipping each command to
defineCommand — which regenerates that command's help golden — is the follow-on.

Co-authored-by: Cursor <cursoragent@cursor.com>
Documents the CLI's four-layer architecture (clients/api/authoring/foundation),
the colocated self-documenting .doc.mjs system (FunctionDoc/CommandDoc/SchemaDoc/
EnumDoc/ReferenceDoc) and its drift harness, how to add a command, and how to
test the CLI (drift + golden harnesses, typecheck:authoring).

Co-authored-by: Cursor <cursoragent@cursor.com>
typecheck:strict (checkJs over the whole CLI, including every .doc.mjs) now
covers the converter: guard the command token against the string|undefined from
Array.pop(), and cast the action to Commander's expected signature.

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
The command, error-code, and response-type tables in packages/cli/README.md are
now generated between markers from the sources of truth (astryx manifest + the
error-codes and response-types EnumDocs), formatted through the repo prettier
config, with a `readme:check` vitest gate.

Fixes the drift: the command table now lists blog/build/layout/
validate-integration; the error-code table drops the non-existent
ERR_TEMPLATE_CONFIG/ERR_TEMPLATE_GET and adds the missing real codes
(ERR_UNKNOWN_THEME, ERR_CODEMOD_FAILED, ERR_FETCH_FAILED, ERR_LAYOUT_PARSE/INVALID,
ERR_AMBIGUOUS_*, ...); the response-type table covers all 40 discriminants.

Co-authored-by: Cursor <cursoragent@cursor.com>
…neCommand

Migrates the search registration to defineCommand, which builds the Commander
command (description, args, options) from search.doc.mjs — the first command with
--help sourced from the docs. Refines defineCommand to preserve the exact
--help/manifest surface: `choices` and `examples` stay doc metadata (surfaced by
`astryx docs` and the doc site, not injected into help), and an arg gets a
description only when the doc sets one explicitly.

Behavior-preserving: golden --check 0 diffs, drift 0, typecheck:strict 0.
Co-authored-by: Cursor <cursoragent@cursor.com>
Migrates every remaining command registration — component, docs, hook, template,
blog, discover, swizzle, build, doctor, upgrade, init, validate-integration, and
the theme + layout command groups — to defineCommand, so each command's --help
and the introspected manifest are built from its colocated CommandDoc.

Actions are preserved verbatim; doc summaries and option descriptions were
aligned to the exact current strings, which surfaced (and fixed) real doc drift:
stray trailing periods on summaries and param-only options that would have
inherited FunctionDoc text. Widens CommandOptionDoc.default to
string | boolean | string[] for upgrade's repeatable/boolean flag defaults.

Behavior-preserving across the whole CLI surface: golden --check 84 invocations
0 diffs, typecheck:strict 0, typecheck:authoring 0, drift 0.

Co-authored-by: Cursor <cursoragent@cursor.com>
A field with no `type` hit Zod's default invalid_type message ("expected string,
received undefined") because the `.min(1, 'field type is required')` message only
covers empty strings, not undefined. Adds the Zod v4 `{error}` message so a
missing type reports "field type is required" for doc authors.

Co-authored-by: Cursor <cursoragent@cursor.com>
The golden verification harness stays a local-only tool for proving CLI-surface
changes are behavior-preserving; it is not committed. Remove its commands from
CONTRIBUTING and the changeset, pointing contributors at the committed drift
suite and the README generator (readme / readme:check) instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
The published ./api type-surface check (packing the CLI fires prepack ->
sync:api-types) failed with TS7016: authoring/index.d.ts re-exports
parseFunction/parseSchema/parseCommand/parseEnum from their .mjs parsers, which
shipped no declaration files, so a strict consumer resolved them as implicit
any. Adds a parse.d.mts beside each new parser, matching the existing
component/hook/reference/template parsers.

Verified with the CI script itself: node .github/scripts/cli-api-types-verify.mjs

Co-authored-by: Cursor <cursoragent@cursor.com>
…ture check

CONTRIBUTING describes the CLI's layering and file conventions; most of them are
mechanical, so check them instead of reviewing them.

ESLint (no custom rules needed — no-restricted-imports / no-restricted-syntax,
kept disjoint per layer because flat config overrides rather than merges a
same-named rule):
  - authoring/ imports no other layer; api/ never imports clients/
  - zod stays sealed behind the authoring/ parsers
  - commands register via defineCommand, so --help and the manifest keep coming
    from the colocated CommandDoc

scripts/check-cli-structure.mjs covers what per-file linting cannot — which files
exist next to each other. Wired into check:repo, so it runs in `pnpm lint` and
the pre-commit hook:
  - every doc-type ships type.ts + parse.mjs + parse.d.mts + <kind>.doc.mjs, and
    re-exports its parser. This is exactly the invariant whose violation shipped
    a broken ./api type surface and only failed at pack time in CI (TS7016).
  - every api/<name>/ ships typedefs, a FunctionDoc, and a test

Closes the one gap that check surfaced: api/json's three published consumer
helpers (parseResponse, isError, assertResponse) had no FunctionDocs. All 16 api
folders now conform, with no exemptions.

Every rule is already clean repo-wide, so they land as errors to prevent
regressions rather than to flag a backlog.

Co-authored-by: Cursor <cursoragent@cursor.com>
… script

Two pieces of drift found in review, both in code this PR added:

- authoring/doctypes/parse.d.mts declared only 4 of the 8 doc kinds parseDoc
  dispatches to. That declaration shadows the implementation for published
  consumers, so narrowing a parsed SchemaDoc/CommandDoc/EnumDoc read as a
  no-overlap comparison and `fields`/`members` were inaccessible.
- CONTRIBUTING told contributors to run `pnpm -F @astryxdesign/cli test`, but the
  CLI package defined no test script, so the documented command silently did
  nothing. Adds test + test:watch, scoped to packages/cli.

check:cli-structure only asserted that parse.d.mts existed, not that it was
complete — so it could not have caught the first bug. It now also asserts the
parseDoc union covers every doc-type folder (verified: reintroducing the stale
union fails the check with one error per missing kind).

Also hardens two gaps found while reviewing that script:
- it reported "0 checked — structure is intact" if a checked directory were ever
  moved; a check that passes when its subject vanishes is worse than none
- the README generator surfaced a CLI that failed to boot as "Unexpected end of
  JSON input" rather than the real exit code and stderr

Co-authored-by: Cursor <cursoragent@cursor.com>
github-actions Bot added a commit that referenced this pull request Aug 5, 2026
github-actions Bot added a commit that referenced this pull request Aug 5, 2026
Audit of the human-facing artifacts turned up three inaccuracies:

- CONTRIBUTING's "Adding a command" steps were out of order. defineCommand builds
  the command *from* its CommandDoc, so the doc has to exist before the handler;
  the steps still said register-then-document. They also never mentioned
  defineCommand or the test that check:cli-structure now requires.
- The three api/json FunctionDocs overstated their types. The published surface is
  `parseResponse(raw): any` and `isError(result): boolean` — isError is not a
  TypeScript type predicate and does not narrow, so describing it as one was
  wrong. They now state the real signatures and point at the *Response types for
  typed access.
- The changeset omitted both user-visible outcomes: --help is now generated from
  the CommandDocs, and the README reference tables are generated (which fixed
  documented error codes that do not exist and four missing commands).

Co-authored-by: Cursor <cursoragent@cursor.com>
@josephfarina
josephfarina merged commit 2d2dba1 into main Aug 5, 2026
19 checks passed
github-actions Bot added a commit that referenced this pull request Aug 5, 2026
josephfarina added a commit that referenced this pull request Aug 5, 2026
… bottom layer (#4736)

Two follow-ups to #4714.

Generate the published ./authoring type declarations from JSDoc instead of
hand-writing them, the same way ./api already worked. The 13 hand-maintained
.d.mts files are gone. A hand-written declaration shadows the JSDoc beside it,
so it could disagree with the implementation and still compile — and both ways
that can go wrong had already shipped (a missing declaration resolving as `any`,
and a stale parseDoc union that dropped three doc kinds). Generation makes both
impossible: tsc rejects a `@returns` that disagrees with the code before a
declaration can be emitted. Also fixes parseFunction, which published HookDoc
under the general name.

Make foundation/ a true bottom layer: it no longer imports api/, and ESLint now
enforces that direction alongside the existing authoring/ and api/ rules. The
template adapter and the integration contribution validators moved into
foundation, where their callers already were; neither had any api dependency.
The Project <-> adapter module cycle still exists, but no longer spans layers.

Behavior-preserving: the CLI's observable surface is byte-identical across 84
invocations.
@github-actions
github-actions Bot deleted the docs/cli branch August 6, 2026 06:58

This branch was successfully deployed

1 active deployment
Preview — 05fa365e Deployed Aug 5, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Meta Open Source bot.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant