Skip to content

feat(cli): typed subcommand stubs (Phase 0 + skeleton) - #46

Open
codewizdave wants to merge 11 commits into
stagingfrom
feat/cli-skeleton
Open

codewizdave wants to merge 11 commits into
stagingfrom
feat/cli-skeleton

Conversation

@codewizdave

Copy link
Copy Markdown
Contributor

Summary

Three commits that land the CLI's design contract and its Phase 0 implementation, in order:

  1. docs/cli/*: the architecture documents (02-design.md, 03-architecture.md, 04-corpus.md, 05-testing.md, 06-roadmap.md, plus per-subcommand specs under commands/).
  2. apps/cli/* scaffold: @deessejs/package-cli workspace that builds, lints, type-checks, and exposes the bin.
  3. Typed subcommand stubs: every one of the six subcommands is wired with commander, parses its args, validates them, and exits 2 with a clear not implemented yet InternalError.

This PR is independent of any other open PR. It branches directly off origin/staging and contains everything needed for the CI to go green on its own.

Why a single PR

The user instruction was that documentation and implementation belong in the same PR. The doc in docs/cli/ is the contract that apps/cli/ must satisfy; splitting them across PRs lets one drift from the other.

What's added

docs/cli/
├── README.md              # vision, audience, success criteria, status
├── 02-design.md           # six subcommands, output conventions, .docs.md format
├── 03-architecture.md     # modules, types, data flow, argv, error model
├── 04-corpus.md           # options A/B/C, decision for B
├── 05-testing.md          # unit + integration + smoke, fixtures
├── 06-roadmap.md          # six phases with exit criteria
└── commands/
    ├── README.md          # per-command index
    ├── ls.md
    ├── cat.md
    ├── grep.md
    ├── find.md
    ├── path.md
    └── symbols.md

apps/cli/
├── package.json           # @deessejs/package-cli, bin, exports, scripts
├── tsconfig.json          # noEmit, NodeNext, types:[node]
├── tsconfig.build.json    # emits dist/, excludes tests
├── eslint.config.js       # flat config
├── vitest.config.ts       # globalSetup rebuilds before tests
├── .gitignore
├── bin/package-cli.mjs    # shebang + import('../dist/index.js')
├── scripts/postbuild.mjs  # chmod 755 on bin
├── src/
│   ├── index.ts           # shebang entry, delegates to cli.run
│   ├── cli.ts             # buildProgram(), run(argv)
│   ├── errors.ts          # ExitCode, UserError, InternalError, exitWithError
│   ├── output.ts          # writeRecords, writeWarning
│   ├── corpus.ts          # DocsFile, Corpus types; loadCorpus, resolveCorpusRoot stubs
│   ├── commands/
│   │   ├── shared.ts      # SubcommandRegist, CommandContext
│   │   ├── ls.ts
│   │   ├── cat.ts
│   │   ├── grep.ts
│   │   ├── find.ts
│   │   ├── path.ts
│   │   └── symbols.ts
│   └── index/
│       ├── build.ts       # buildIndex stub, reportDuplicateSymbol helper
│       └── search.ts      # SymbolMatch, TextMatch types; findSymbols/grepCorpus/listSymbols stubs
└── tests/
    ├── global-setup.ts    # tsc -p tsconfig.build.json before tests
    ├── commands.test.ts   # integration per subcommand (12 tests)
    ├── errors.test.ts     # exit codes and error format (5 tests)
    └── output.test.ts     # writeRecords, writeWarning (5 tests)
```

## Behaviour after this PR

```bash
$ package-cli --version
0.0.0

$ package-cli docs ls /
[error] ls is not implemented yet
exit 2

$ package-cli docs ls
error: missing required argument 'path'
exit 1

$ package-cli docs cat Buffer
[error] cat is not implemented yet
exit 2

$ package-cli docs symbols
[error] symbols is not implemented yet
exit 2

$ package-cli docs symbols extra
error: too many arguments for 'symbols'. Expected 0 arguments but got 1.
exit 1

$ package-cli --corpus /tmp/x docs symbols
[error] symbols is not implemented yet
exit 2
```

Exit codes follow the contract from `docs/cli/02-design.md#exit-codes` and `docs/cli/03-architecture.md#error-model`: 0 success, 1 user error (commander rejects invalid args), 2 internal error (subcommand stub throws `InternalError`).

## Verification

```
pnpm install
pnpm --filter @deessejs/package-cli type-check  # clean
pnpm --filter @deessejs/package-cli lint        # clean
pnpm --filter @deessejs/package-cli build       # builds dist/
pnpm --filter @deessejs/package-cli test:run    # 22 passed

vale --config .vale.ini docs/cli/               # 0 errors
prettier --check docs/cli/** apps/cli/**        # clean
```

## Subsequent phases

Per `docs/cli/06-roadmap.md`:

1. Phase 1: corpus loader (`loadCorpus()` in `src/corpus.ts`, frontmatter validation)
2. Phase 2: read commands (`ls`, `cat`, `path` go from stub to real behavior)
3. Phase 3: indexer and search (`find`, `grep`, `symbols` plus `src/index/{build,search}.ts`)
4. Phase 4: polish (--help, smoke test, changeset)
5. Phase 5: publish

Each phase will be its own PR off `staging`. The doc may grow alongside the implementation when reality forces a correction; a PR that diverges from `docs/cli/` lands **with** an update to the relevant section.

## Changeset

No changeset. Per `CONTRIBUTING.md`: a changeset is only added when a change should cut a release. This PR ships the `0.0.0` placeholder; the first real release lands with Phase 5.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Documents the design contract for the CLI before any code is written.
Read in order; this is the source of truth that the implementation
must satisfy.

- docs/cli/README.md: domain index and reading order.
- docs/cli/02-design.md: the six subcommands (ls, cat, grep, find,
  path, symbols), output formats, the .docs.md convention, the
  Source column in find, and naming consistency.
- docs/cli/03-architecture.md: module layout under apps/cli/, core
  types (DocsFile, Corpus, SymbolMatch, TextMatch, ExitCode, UserError,
  InternalError), data flow, argv parsing via commander, output
  contract, error model, performance expectations.
- docs/cli/04-corpus.md: three options (A: direct read of MDX, B: read
  .source/ build artefacts, C: shared indexer package) and the
  decision for B with rationale and cost.
- docs/cli/05-testing.md: three layers (unit, integration, smoke),
  fixture strategy, what is explicitly not tested, local loop.
- docs/cli/06-roadmap.md: six phases (scaffold, corpus loader, read
  commands, indexer and search, polish, publish) each with an exit
  criterion.
- docs/cli/commands/{ls,cat,grep,find,path,symbols,README}.md:
  per-subcommand specification (signature, output format, exit codes,
  edge cases, internal modules).

No code changes. No runtime behavior changes. No changeset required
(no release to cut). The CLI workspace itself does not yet exist;
this PR exists to align on design before scaffolding begins.
Phase 0 of the @deessejs/package-cli roadmap: a workspace that
builds, lints, type-checks, tests, and exposes a no-op stub binary.

Files:
- apps/cli/package.json: name @deessejs/package-cli, bin
  package-cli.mjs, scripts matching the turbo contract (build,
  test:run, type-check, lint, clean), ESM-only with exports map,
  files allowlist (bin + dist). Commander as runtime dependency.
- apps/cli/tsconfig.json: noEmit, ES2022 + NodeNext, strict, includes
  src/ and tests/, types: [node].
- apps/cli/tsconfig.build.json: emits dist/ from src/, excludes tests.
- apps/cli/eslint.config.js: flat config copied from packages/example,
  scoped to src/.
- apps/cli/vitest.config.ts: node environment, globals enabled,
  v8 coverage, globalSetup that rebuilds dist/ (tests spawn the bin
  which imports dist/index.js).
- apps/cli/.gitignore: dist, coverage, node_modules, tsbuildinfo.
- apps/cli/bin/package-cli.mjs: shebang, re-exports dist/index.js.
- apps/cli/scripts/postbuild.mjs: chmod 755 on the bin file so npm
  install doesn't drop the executable bit.
- apps/cli/src/index.ts: shebang entry, delegates to cli.run.
- apps/cli/tests/global-setup.ts: tsc -p tsconfig.build.json before
  the test files are loaded.
- pnpm-lock.yaml: new package + transitive deps.

Verified locally:
  pnpm --filter @deessejs/package-cli build -> builds dist/
  pnpm --filter @deessejs/package-cli lint -> clean
  pnpm --filter @deessejs/package-cli type-check -> clean
Wires every subcommand in apps/cli/src/commands/ as a typed stub that
parses its own args, validates them, and exits 2 with a clear 'not
implemented yet' InternalError. No corpus loading, no real behavior:
this lands the architecture described in docs/cli/03-architecture.md
and docs/cli/commands/<name>.md without claiming the Phase 1/2/3
implementations.

Modules introduced:

- src/errors.ts: ExitCode (0/1/2), UserError (exit 1),
  InternalError (exit 2), exitWithError() helper used by the
  top-level handler.
- src/output.ts: writeRecords() (stdout, one row per \n),
  writeWarning() (stderr, '[warn] <message>').
- src/corpus.ts: DocsFile and Corpus types, plus loadCorpus() and
  resolveCorpusRoot() stubs that throw InternalError.
- src/index/build.ts: buildIndex() stub, reportDuplicateSymbol()
  helper that emits the standard '[warn] duplicate symbol' format.
- src/index/search.ts: SymbolMatch, TextMatch types, plus
  findSymbols(), grepCorpus(), listSymbols() stubs.
- src/commands/shared.ts: SubcommandRegist type, CommandContext
  interface that threads the resolved corpus root into each
  subcommand.
- src/commands/<ls,cat,grep,find,path,symbols>.ts: each parses its
  args with commander, validates them (UserError on missing), and
  throws InternalError 'is not implemented yet'.
- src/cli.ts: buildProgram() and run(argv), the top-level handler.

Tests:

- tests/errors.test.ts: ExitCode values, UserError/InternalError
  exit code mapping, exitWithError() format and exit code for
  UserError, plain Error, and wrapped errors.
- tests/output.test.ts: writeRecords() row formatting, iterable
  acceptance, empty iterable, writeWarning() format and stdout
  separation.
- tests/commands.test.ts: integration via the published bin. Each
  subcommand is spawned once with valid args (exit 2 with
  'not implemented yet' stderr) and once with invalid args
  (commander rejects with status > 0). Also covers --version,
  --help, docs --help, and the global --corpus flag.

Verified locally:
  pnpm --filter @deessejs/package-cli type-check -> clean
  pnpm --filter @deessejs/package-cli lint        -> clean
  pnpm --filter @deessejs/package-cli test:run   -> 22 passed
  prettier --check apps/cli/** docs/cli/**       -> clean
  vale --config .vale.ini docs/cli/              -> 0 errors

This lands Phase 0's scaffold (commit 2 of 3) plus the typed
skeleton for all six subcommands. Phase 1 (corpus loader), Phase 2
(read commands), and Phase 3 (search) will follow on separate
branches; each one will land the corresponding implementation in
apps/cli/src/ without needing further doc changes unless reality
forces a contract correction.
@github-actions

github-actions Bot commented Sep 2, 2026

Copy link
Copy Markdown

Coverage Report for @deessejs/example coverage (packages/example)

Status Category Percentage Covered / Total
🟢 Lines 100% 1 / 1
🟢 Statements 100% 1 / 1
🟢 Functions 100% 1 / 1
🟢 Branches 100% 0 / 0
File CoverageNo changed files found.
Generated in workflow #83 for commit 9a852a7 by the Vitest Coverage Report Action

@pkg-pr-new

pkg-pr-new Bot commented Sep 2, 2026

Copy link
Copy Markdown

Open in StackBlitz

pnpm add https://pkg.pr.new/deessejs/package-template/@deessejs/example@46
pnpm add https://pkg.pr.new/deessejs/package-template/@deessejs/package-cli@46

commit: 9a852a7

claude and others added 8 commits September 2, 2026 11:30
The Publish Dry Run job inspects the tarball that would ship to npm.
Until this commit it only ran against @deessejs/example, so a broken
`files` allowlist or `exports` map on @deessejs/package-cli would
slip past CI and only fail at the real release.

Add a second `npm pack --dry-run` invocation for the CLI. Both
packages now go through the same pre-release gate.

Verified locally:
  pnpm --filter @deessejs/package-cli exec npm pack --dry-run
  -> 58 files, 10.4 kB tarball, contents match the
     bin/ + dist/ + package.json allowlist.
Until this commit pkg.pr.new published only @deessejs/example. Every
PR now also produces a preview tarball for the CLI, so contributors
can install the in-progress build with:

  npm i https://pkg.pr.new/<owner>/<repo>/@deessejs/package-cli@<sha>

The build step is split per workspace to keep the dependency order
explicit. pkg-pr-new publish is invoked once per package; that gives
each one its own URL on pkg.pr.new and its own install command on
the PR comment.
…shes

The previous attempt ran two `pkg-pr-new publish` invocations in the
same job. The first one succeeded; the second one failed with:

  {"url":"/check","statusCode":404,"statusMessage":"Not Found",
   "message":"There is no workflow defined for HxC6z2DNtL"}

pkg.pr.new ties each upload to a single workflow run, and a single
job run can only register one upload. Trying to register a second
one in the same job returns 404 on the /check endpoint.

Fix: split the workflow into two independent jobs, one per package.
Each job does its own install + build + publish. They run in
parallel and each one registers exactly one upload with pkg.pr.new.

Cost: ~30s of duplicate install + cache hit on both runners. Acceptable
for the gain of working preview releases for both packages.
The pkg.pr.new README is explicit:

  > Should be run one time for all the desired packages

  pnpm exec pkg-pr-new publish './packages/A' './packages/B'

Two separate `pkg-pr-new publish` invocations in the same workflow
run fail with:

  {"url":"/check","statusCode":404,"statusMessage":"Not Found",
   "message":"There is no workflow defined for ..."}

regardless of whether they're sequential steps in one job or two
parallel jobs. The service ties one upload token per workflow run
and rejects the second attempt.

Fix: collapse the two `publish` steps into one invocation that takes
both paths. pkg.pr.new generates two URLs (one per package) and posts
them as a single comment with both install commands.

This commit supersedes f8891f3 (sequential steps) and cef8102 (two
parallel jobs), both of which fail.
pkg.pr.new defaults to `pnpm add` in its install comment, which is
correct for libraries but wrong for binaries. `@deessejs/package-cli`
has a `bin` field in its package.json; the comment should advertise
`pnpm dlx` (or `npx`) so users don't have to install a binary
they only want to invoke.

The pkg.pr.new README documents `--bin` for this exact case:

  > For CLI applications you might want to show npx instead of npm i
  > for the preview command. This can be accomplished with the --bin flag

`--bin` is a global flag and cannot be scoped to a single path in a
multi-path invocation, so the workflow splits back into two jobs.
The previous multi-path commit (6a3443a) addressed a different 404
("no workflow defined") that came from a single job doing two
`publish` invocations. Two jobs in parallel is what pkg.pr.new
expects for two packages: one upload per workflow run per job.

If pkg.pr.new's /check endpoint rejects the second job with the
same 404, fall back to the multi-path single invocation and accept
`pnpm add` for the CLI. Tracked upstream if needed.
The pkg.pr.new README documents `--bin` for showing `npx` instead
of `npm i`, but until PR #524 (merged Jul 24, 2026) the flag was
global. With one CLI binary and one library in the same multi-path
invocation, a global `--bin` would render `npx` for both,
which is wrong for the library.

PR #524 added per-package scoping: `--bin pkg-a,pkg-b` marks only
the named packages as binaries. The server now picks `npx` or
`pnpm add` per package instead of per request.

Apply `--bin '@deessejs/package-cli'` so the package comment in
the PR shows:

  pnpm add .../@deessejs/example
  pnpm dlx .../@deessejs/package-cli

instead of two `pnpm add` lines.

Source: stackblitz-labs/pkg.pr.new issue #396, PR #524.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants