Skip to content

feat(cli): render human output through a shared formatter kit - #4686

Open
josephfarina wants to merge 15 commits into
mainfrom
feat/cli-formatters
Open

feat(cli): render human output through a shared formatter kit#4686
josephfarina wants to merge 15 commits into
mainfrom
feat/cli-formatters

Conversation

@josephfarina

Copy link
Copy Markdown
Contributor

Summary

  • --json stays the source of truth; the human (non---json) output is now a consistent, greppable plain-ASCII projection of that same JSON, rendered through a new packages/cli/clients/cli/formatters kit.
  • The kit is small and JSON-oriented: emit (the single stdout sink), record/records (a JSON object/array -> aligned key: value), plus title, section (with optional subtitle), text, list, table, code, markdown. Output is deterministic — no color, no TTY detection, no width wrapping — so it's identical piped or not.
  • Type-safe by construction: renderers return an opaque Block and emit(...) accepts only Blocks (a @ts-expect-error guard proves emit('string') won't compile under checkJs/strict).
  • Migrated every command's human branch onto emit + renderers: search, build, component, docs, hook, template, discover, doctor, swizzle, layout, theme, validate-integration, blog. upgrade/init already stream progress via the shared logger and are unchanged.
  • Enforced the funnel: extended @astryx/no-raw-console-cli to ban humanLog/humanWarn in command files, so emit is the only stdout path (errors/warnings stay on stderr via cliError). humanLog remains internal to the logger + formatters.

Notes / follow-ups

  • --json envelopes are byte-for-byte unchanged.
  • Unicode glyphs in CLI-authored output (, , ✓/⚠/✗) were replaced with ASCII (->, -, [ok]/[warn]/[fail]); em-dashes that come from data (component/template descriptions) pass through verbatim.
  • A @astryxdesign/cli patch changeset still needs to be added (left out to avoid guessing the author handle).
  • Separate follow-up: trim/rerank api/build candidate selection (it surfaces several low-relevance blocks), and optionally migrate the manifest/postinstall banner in clients/cli/index.mjs.

Test plan

  • pnpm exec vitest run packages/cli/clients — 895 tests pass
  • pnpm -F @astryxdesign/cli typecheck:strict — clean (only the pre-existing theme build --check type errors remain)
  • pnpm exec eslint packages/cli/{clients,api,foundation} — clean; new humanLog/humanWarn ban verified with a probe
  • Eyeball a few commands piped vs. TTY (identical), e.g. astryx search button, astryx build "user settings page", astryx doctor

Made with Cursor

JSON stays the source of truth; the plain-text output is now a consistent,
greppable projection of it. Adds clients/cli/formatters with an opaque Block
type and a small renderer set — emit (the single stdout sink), title, section,
text, list, record/records (JSON object -> aligned key: value), table, code,
markdown. Output is deterministic plain ASCII (no color, no TTY, no width
detection); errors/warnings stay on stderr via cliError.

Migrates every command's human branch onto emit + renderers (search, build,
component, docs, hook, template, discover, doctor, swizzle, layout, theme,
validate-integration, blog). Enforces the funnel by extending
@astryx/no-raw-console-cli to ban humanLog/humanWarn in command files (emit is
now the only path); humanLog stays internal to the logger and formatters.

JSON envelopes are unchanged. CLI test suite (895 tests) passes.

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

vercel Bot commented Aug 3, 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 3, 2026 11:57pm

Request Review

@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Meta Open Source bot. label Aug 3, 2026
…order)

Replace the status-like "Building \"<q>\"" header with a titled "Build kit for
\"<q>\"" plus a short legend: what the output is, how to use it (run START, then
pull from the sections; each item has a `command:`), and the exact order of the
sections that follow (computed from the ones actually present) so it reads
clearly and parses predictably.

Co-authored-by: Cursor <cursoragent@cursor.com>
josephfarina and others added 2 commits August 3, 2026 12:29
Rename the build kit's "START" section to "RECOMMENDED START" and explain why:
the closest page template is called out as the recommendation ("this template
appears closest ... we recommend starting here"), with an explicit fallback
("otherwise browse PAGE TEMPLATES, then BLOCKS and DOMAIN COMPONENTS"). Keeps the
top legend in sync.

Co-authored-by: Cursor <cursoragent@cursor.com>
The recommended scaffold command hardcoded `./src/App.tsx`, which wrongly assumes
the consumer's project layout. Use a `<path>` placeholder instead and explain in
the note that it's the file (or folder) to write the template to. The
non-direct-match case now points at the printed `--skeleton` layout as the
reference rather than implying a scaffold path.

Co-authored-by: Cursor <cursoragent@cursor.com>
Human output is meant to be plain ASCII, but data (e.g. template display names
like "Avatar — Group") and prose carried em/en dashes, curly quotes, and
ellipses. The prose/data renderers (title, section, text, list, record, table)
now normalize these to ASCII (— -> -, curly quotes -> straight, … -> ...); the
verbatim renderers (code, markdown) and --json are untouched. Table cells are
normalized before width calc so alignment stays correct.

Co-authored-by: Cursor <cursoragent@cursor.com>
Commander wraps each help description to a column width (80 when captured
non-TTY), splitting long descriptions like --json across several indented lines.
Override Help.wrap to a no-op via configureHelp so every option/command renders
on a single line — deterministic and width-independent, matching the rest of the
CLI. Set before subcommands register so they inherit it (copyInheritedSettings).

Co-authored-by: Cursor <cursoragent@cursor.com>
josephfarina and others added 2 commits August 3, 2026 14:09
The names view interleaved headerless standalone components with "(group)"
family sections and ragged blank lines, which read as choppy/weird. Render a
single sorted table of Component + Import instead (the import column already
conveys families); package-qualify external/colliding names. Also drop the
"(group)" suffix from the --detail compact headers.

Co-authored-by: Cursor <cursoragent@cursor.com>
The manifest summary used a nested 2/4-space indent (command on one line,
description indented under it) plus a raw em-dash, which was hard to grep and
inconsistent. Render it through the formatter as a single Command | JSON |
Description table instead (one row per command, ASCII-normalized).

Co-authored-by: Cursor <cursoragent@cursor.com>
Add an "Output format" section to `astryx --help` describing the plain-text
contract so agents can parse/grep it reliably: records are blocks of aligned
"key: value" lines separated by a single blank line (RFC-822 / recfile style),
sections are a header line + optional subtitle, and --json is the stable
structured surface. Formalizes the convention rather than adding per-item
divider lines (a blank line is the standard record separator).

Co-authored-by: Cursor <cursoragent@cursor.com>
Consolidate the formatter kit to one small, formalized set and document it in
--help so every current and future command conforms and agents can rely on it:

- Data is always records (key: value blocks, blank-line separated) — dropped
  `table`; `component --list` and `manifest` are now records too.
- Merged `title` into `section` (a heading with no subtitle) and `markdown` into
  `code` (one verbatim block: source, skeleton, or doc).
- Final vocabulary: section, record/records, list, text, code, emit.
- `astryx --help` now documents the block types (Record/Section/List/Text/Code),
  the blank-line record separator, how to grep a field, and that --json is the
  structured surface.

Net -75 lines. All CLI tests pass; tsc strict + lint clean.

Co-authored-by: Cursor <cursoragent@cursor.com>
Render the "Output format" help through the formatter itself — a section heading,
one record per block type (block/shape), and a text footer — so the spec is
written in the very format it documents (records separated by blank lines).

Co-authored-by: Cursor <cursoragent@cursor.com>
@astryxdesign/cli patch — human output now renders through the documented
formatter kit; --json unchanged.

Co-authored-by: Cursor <cursoragent@cursor.com>
The blog detail body was rendered via text(), which ASCII-normalizes content
(em/en dashes, curly quotes, ellipses) — silently altering the article. Use
code() so the post body is emitted byte-for-byte, and render the feed URL as a
record. Caught by Bugbot.

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

# Conflicts:
#	packages/cli/clients/cli/commands/build.mjs
#	packages/cli/clients/cli/commands/layout.mjs
#	packages/cli/clients/cli/commands/search.mjs
Post-merge, main's new `--verbose` regression test asserted the old combined
`match:` line. The formatter renders ranking detail as separate `score:` /
`reason:` record fields (mirroring --json), so assert those instead.

Co-authored-by: Cursor <cursoragent@cursor.com>
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