|
8 | 8 | - Use only jest for writing test cases and refer existing unit test under the /src folder. |
9 | 9 | - Do not create code comments for any changes. |
10 | 10 |
|
| 11 | +**Integration tests.** `test/integration/` drives real code with only the network faked by `nock`. |
| 12 | +`projects-list-command.test.ts` runs whole commands through `@oclif/test`'s `runCommand`, which |
| 13 | +covers `init()`, the resolution chain, rendering and the `catch()` exit-code mapping in one pass. |
| 14 | +Two things make that reliable and both are load-bearing: |
| 15 | + |
| 16 | +- The `Config` is built from a root `Plugin` constructed with `ignoreManifest: true`. Without it, a |
| 17 | + generated `oclif.manifest.json` — `npm run prepack` writes one, and it is gitignored — makes oclif |
| 18 | + load the compiled `dist/commands` instead of `src`, so the suite would test stale compiled output |
| 19 | + and fail outright whenever `dist` is absent. |
| 20 | +- `console.log` is redirected straight to `process.stdout` so jest's console decoration stays out of |
| 21 | + the captured stdout, and `process.exitCode` is reset after each run because oclif sets it while |
| 22 | + handling a simulated CLI failure and would otherwise fail the whole jest run. |
| 23 | + |
| 24 | +The confirm gate is the one part of `LaunchCommand` `runCommand` cannot reach yet: no shipped |
| 25 | +command declares `yes: {}`. It stays covered by `src/base/launch-command.test.ts` until one does. |
| 26 | + |
| 27 | +## Adding a command (V2) |
| 28 | + |
| 29 | +Five touch points, in this order: |
| 30 | + |
| 31 | +1. **`src/flags/catalog.ts`** — only if the command introduces a flag no command uses yet. |
| 32 | + Transcribe it from the Commands Details page §"All flags" tables. Never set `required: true`. |
| 33 | + Catalog flag definition objects are shared by reference across every command that uses them — |
| 34 | + never mutate one in place. |
| 35 | +2. **`src/flags/resolution.ts`** — one entry per new flag, saying where its value may come from: |
| 36 | + `configPath`, `prompt`, `default`. `resolution` is typed `Record<FlagKey, ResolutionSpec>`, so a |
| 37 | + catalog key added without a matching resolution entry is a **compile error**, not something a |
| 38 | + test has to catch. |
| 39 | +3. **`src/api/<resource>.ts`** — only if the command calls an endpoint no command calls yet. |
| 40 | + These modules take a `RestApiClient` and return typed data. No `ux`, no prompts, no `process.exit`. |
| 41 | +4. **`src/api/index.ts`** — a new resource module must be registered here: a field on `ApiSurface` |
| 42 | + and its construction in `buildApi`. This is the one shared file every resource module edits, |
| 43 | + so expect to rebase on it. |
| 44 | +5. **`src/commands/launch/<resource>/<verb>.ts`** — the command itself: a `static inputs` |
| 45 | + declaration, `static flags = flagsFor(...)`, and a `run()` that calls the api and renders. The |
| 46 | + command's `flags` keys must equal its `inputs` keys — both shipped commands assert this by |
| 47 | + deriving `flags` from `inputs` via `flagsFor`, rather than declaring the two independently. |
| 48 | + |
| 49 | +A command that declares `--project` in `inputs` must also declare `--org`: `resolution.project.normalize` |
| 50 | +reads `resolved.org` to resolve the project uid, and catalog order only resolves `org` first because |
| 51 | +both shipped commands declare it. |
| 52 | + |
| 53 | +Everything else — parsing, resolution, prompting, name-to-uid normalisation, retries, auth |
| 54 | +headers, error mapping, exit codes, rendering — is inherited from `LaunchCommand`. If a new command |
| 55 | +needs a change in `src/base/`, `src/flags/` or `src/http/`, that is a signal worth raising rather |
| 56 | +than a routine edit. |
| 57 | + |
| 58 | +**Confirm gate.** A destructive command opts in by adding `yes: {}` to its `inputs` — `--yes` |
| 59 | +is deliberately not a global flag — and `await this.confirm('<question>')` at the top of `run()`. |
| 60 | +It returns silently when `--yes` was passed, prompts on a TTY, exits 2 when there is neither, and |
| 61 | +exits 3 when the user declines. Never assume a yes yourself. |
| 62 | + |
| 63 | +**Exit codes.** `src/config/constants.ts` owns them and `LaunchCommand.catch()` is the only place |
| 64 | +that maps an error to one: |
| 65 | + |
| 66 | +| Code | Constant | Meaning | |
| 67 | +|---|---|---| |
| 68 | +| 0 | `EXIT_OK` | the command did what it was asked to do | |
| 69 | +| 1 | `EXIT_RUNTIME` | a runtime failure — `LaunchApiError`, an unauthenticated session, anything oclif handles | |
| 70 | +| 2 | `EXIT_USAGE` | a usage error — `UsageError`, `MissingInputError`, a failing cross-flag rule | |
| 71 | +| 3 | `EXIT_CANCELLED` | the user declined a confirmation (`CancelledError`) | |
| 72 | + |
| 73 | +A declined confirmation is a deliberate "no", not a failure, so it does not share code 1 with an |
| 74 | +API 500 — a CI log has to be able to tell those apart. 130 would claim the process was killed by |
| 75 | +SIGINT, which is not what happened. |
| 76 | + |
| 77 | +**Cross-flag rules.** A rule that is pure flag-versus-flag and evaluable from argv alone belongs in |
| 78 | +oclif's native `exclusive` / `relationships` on the catalog entry, where it also shows in `--help`. |
| 79 | +A rule that must read a *resolved* value (one that config, a prompt or a default may have supplied) |
| 80 | +belongs in `src/flags/rules.ts` — `exactlyOneOf`, `dependsOnValue`, `requiresFrameworkIn` — declared |
| 81 | +as a `static rules = [...]` array on the command. `resolveInputs` evaluates them after resolution, |
| 82 | +and a failing rule is a usage error (exit 2). |
| 83 | + |
| 84 | +**Redaction.** Anything rendering an environment variable's value in a table or a detail block uses |
| 85 | +`src/output/redact.ts` (`REDACTED`, `redactedColumn`). Confirmation text and error text are not |
| 86 | +covered: nothing stops a future `variables:*` command from interpolating a value straight into |
| 87 | +`this.confirm(...)` or a thrown error's message. Building that guard needs a debug logger and an |
| 88 | +in-flight secret registry to redact against, neither of which exists yet — until one does, a command |
| 89 | +handling variable values must redact them itself before they reach `confirm()` or an error message. |
| 90 | + |
| 91 | +Required-ness is declared in `inputs`, never as an oclif `required: true` flag: oclif's parse-time |
| 92 | +enforcement would fire before config or a prompt has had a chance to supply the value, so |
| 93 | +required-ness is enforced after the resolution chain runs instead. A command must read |
| 94 | +`this.resolved`, never `this.flags` — reading `this.flags` bypasses the resolution chain |
| 95 | +(config file, prompt, default) entirely and returns only what was passed on argv. |
| 96 | + |
| 97 | +## Commits |
| 98 | + |
| 99 | +Use Conventional Commits — `feat(scope): subject`, `fix(scope): subject`, `test:`, `docs:`, |
| 100 | +`chore:`, `refactor:`. Do not prefix a commit subject with a ticket id; reference the ticket in |
| 101 | +the pull request instead. |
| 102 | + |
| 103 | +## What does not belong in this repository |
| 104 | + |
| 105 | +This repo holds the CLI and nothing else. Never commit AI tooling or process scaffolding here — |
| 106 | +agent prompts, per-epic or per-ticket instructions, workflow runbooks, planning or hand-off |
| 107 | +documents, or generated analysis. Those live in the developer workspace, outside this repo. |
| 108 | + |
| 109 | +`AGENTS.md` and `README.md` are the exception: repo-scoped guidance that a contributor reads to |
| 110 | +work on this codebase belongs here. A document written to drive an assistant through a ticket |
| 111 | +does not. |
0 commit comments