@wirecat/cli-base 0.61.2 → 0.62.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +37 -0
- package/dist/commands/contracts.js +26 -23
- package/dist/commands/registry.d.ts +5 -0
- package/dist/commands/registry.js +14 -2
- package/docs/standards/architecture.md +2 -1
- package/docs/standards/ci.md +15 -11
- package/docs/standards/docs.md +8 -4
- package/docs/standards/new-cli.md +7 -3
- package/docs/standards/security.md +2 -1
- package/docs/standards/store.md +1 -1
- package/docs/standards/testing.md +8 -4
- package/docs/standards/toolchain.md +2 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,43 @@ Versions before 0.8.0 are in the [git tags](https://github.com/WireCatLabs/cli-b
|
|
|
8
8
|
Every entry says what changed as a caller sees it, why, and what to watch for — the rules are
|
|
9
9
|
[`docs/dev/CONVENTIONS.md`](docs/dev/CONVENTIONS.md#the-changelog).
|
|
10
10
|
|
|
11
|
+
## 0.62.0 — 26.10.2026
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`describeAlone(command, known)` describes a command as if it had no parent, reusing what `known` already holds.**
|
|
16
|
+
The shared-command check uses it to describe each command once per pass. Keep `known` for one pass only: a map kept
|
|
17
|
+
longer returns descriptions of commands that have since changed.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **The shared-command check describes each command once per pass, not once for itself and again for every group
|
|
22
|
+
above it.** It runs when a shared command is built and before each command runs, so every command of every CLI paid
|
|
23
|
+
for it: about 60% of tg's setup, even for `tg --version`. On tg's real program one check now takes about 5 ms
|
|
24
|
+
instead of 11, building the program about 35 ms instead of 46, and a warm `run(["--version"])` about 30 ms instead
|
|
25
|
+
of 47 ([measurement](https://github.com/WireCatLabs/cli-testing/blob/main/performance/cli/shared-contract-check/README.md)).
|
|
26
|
+
The check still covers every protected subcommand, with the same errors and overrides, and `sharedContracts` returns
|
|
27
|
+
the same contracts as before.
|
|
28
|
+
|
|
29
|
+
## 0.61.3 — 26.10.2026
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **Handbook corrections from the cli-template 0.6.0 audit.** [CI](docs/standards/ci.md): the shared `node-ci.yml`
|
|
34
|
+
runs a CLI's contract, leak, version, generate, test matrix, docs, package and command lint steps itself with
|
|
35
|
+
`cli: true`; the repository's `ci.yml` names a step it cannot run yet in `skip` and puts only extra checks in
|
|
36
|
+
`checks`. The page used to say the repository listed those steps in `checks`.
|
|
37
|
+
[Architecture](docs/standards/architecture.md): a command may fall back to `process.env` when the environment leaves
|
|
38
|
+
`env` unset, as in `environmentOf(this).env ?? process.env`; the two rules on the command environment no longer
|
|
39
|
+
disagree. [Testing](docs/standards/testing.md): tests that only read may share a profile; a test that writes config
|
|
40
|
+
or the store uses its own profile or undoes the write in a `finally` block. The upgrade check defines local state:
|
|
41
|
+
data the CLI's own code writes and reads back, not cli-core's config file and run records.
|
|
42
|
+
[Documents](docs/standards/docs.md): an empty `## Unreleased` is not kept, and a developer page anchors claims about
|
|
43
|
+
behaviour, measurements and findings, not instructions. [New CLI](docs/standards/new-cli.md): the copy tool under
|
|
44
|
+
`kit/` follows the rules for code and the repository but not those for a CLI's commands and runtime, and the
|
|
45
|
+
`CLIDEV_` prefix is a placeholder like `clidev`. The repository lists on the CI, Toolchain, Security and Documents
|
|
46
|
+
pages now include cli-template.
|
|
47
|
+
|
|
11
48
|
## 0.61.2 — 26.10.2026
|
|
12
49
|
|
|
13
50
|
### Fixed
|
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { CliError } from "../errors.js";
|
|
4
|
-
import {
|
|
4
|
+
import { describeAlone } from "./registry.js";
|
|
5
5
|
const protectedCommands = new WeakMap();
|
|
6
6
|
export const hasSharedContract = (command) => protectedCommands.has(command);
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const described = describeProgram(root)[0];
|
|
12
|
-
command.parent = parent;
|
|
13
|
-
if (!described)
|
|
7
|
+
const hidden = (command) => Boolean(command._hidden);
|
|
8
|
+
/** Shares arrays with the live command and with `known`: clone it before keeping it past the pass. */
|
|
9
|
+
const snapshot = (command, known = new Map(), children) => {
|
|
10
|
+
if (hidden(command))
|
|
14
11
|
throw new Error("a hidden command cannot own a shared contract");
|
|
15
|
-
const { commands: _, usage: __, path: ___, ...contract } =
|
|
16
|
-
return
|
|
12
|
+
const { commands: _, usage: __, path: ___, ...contract } = describeAlone(command, known);
|
|
13
|
+
return {
|
|
17
14
|
...contract,
|
|
18
15
|
aliases: [...command.aliases()],
|
|
19
16
|
children: (children ?? protectedCommands.get(command)?.originalChildren ?? command.commands)
|
|
20
|
-
.filter((child) => command.commands.includes(child) && !child
|
|
17
|
+
.filter((child) => command.commands.includes(child) && !hidden(child))
|
|
21
18
|
.map((child) => child.name()),
|
|
22
|
-
}
|
|
19
|
+
};
|
|
23
20
|
};
|
|
24
21
|
const same = isDeepStrictEqual;
|
|
25
22
|
const visit = (root, fn) => {
|
|
@@ -36,11 +33,12 @@ const pathOf = (command) => {
|
|
|
36
33
|
const differences = (before, after) => [...new Set([...Object.keys(before), ...Object.keys(after)])].filter((field) => !same(before[field], after[field]));
|
|
37
34
|
export const assertSharedContracts = (root) => {
|
|
38
35
|
const changed = [];
|
|
36
|
+
const known = new Map();
|
|
39
37
|
visit(root, (command) => {
|
|
40
38
|
const protectedContract = protectedCommands.get(command);
|
|
41
39
|
if (!protectedContract)
|
|
42
40
|
return;
|
|
43
|
-
const fields = differences(protectedContract.contract, snapshot(command));
|
|
41
|
+
const fields = differences(protectedContract.contract, snapshot(command, known));
|
|
44
42
|
if (fields.length)
|
|
45
43
|
changed.push({ command: pathOf(command).join(" "), fields });
|
|
46
44
|
});
|
|
@@ -55,9 +53,9 @@ export const overrideSharedContract = (command, reason, change) => {
|
|
|
55
53
|
if (!protectedContract)
|
|
56
54
|
throw new Error("only a core-owned command contract can be overridden");
|
|
57
55
|
assertSharedContracts(command);
|
|
58
|
-
const before = snapshot(command);
|
|
56
|
+
const before = structuredClone(snapshot(command));
|
|
59
57
|
change(command);
|
|
60
|
-
const after = snapshot(command);
|
|
58
|
+
const after = structuredClone(snapshot(command));
|
|
61
59
|
const fields = differences(before, after);
|
|
62
60
|
if (fields.length === 0)
|
|
63
61
|
throw new Error("a shared contract override must change its named command");
|
|
@@ -119,14 +117,18 @@ export const sharedCommand = (factory) => (...args) => {
|
|
|
119
117
|
if (!command.summary() && first.length > 80)
|
|
120
118
|
command.summary(`${first.slice(0, 77).replace(/\s+\S*$/, "")}…`);
|
|
121
119
|
});
|
|
120
|
+
const known = new Map();
|
|
122
121
|
visit(shared, (command) => {
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
122
|
+
if (hidden(command) || protectedCommands.has(command))
|
|
123
|
+
return;
|
|
124
|
+
// One object for both: `contract` is only ever replaced, and `reference` is cloned on the way out.
|
|
125
|
+
const contract = structuredClone(snapshot(command, known));
|
|
126
|
+
protectedCommands.set(command, {
|
|
127
|
+
contract,
|
|
128
|
+
reference: contract,
|
|
129
|
+
overrides: [],
|
|
130
|
+
originalChildren: [...command.commands],
|
|
131
|
+
});
|
|
130
132
|
});
|
|
131
133
|
shared.hook("preAction", () => {
|
|
132
134
|
let root = shared;
|
|
@@ -147,6 +149,7 @@ export const customizeSharedCommand = (reference, reason, build, expectedOverrid
|
|
|
147
149
|
const command = build();
|
|
148
150
|
assertSharedContracts(command);
|
|
149
151
|
const pending = [];
|
|
152
|
+
const known = new Map();
|
|
150
153
|
const edits = [];
|
|
151
154
|
const bindingPath = (own) => {
|
|
152
155
|
const path = [own.name()];
|
|
@@ -163,7 +166,7 @@ export const customizeSharedCommand = (reference, reason, build, expectedOverrid
|
|
|
163
166
|
return;
|
|
164
167
|
if (!protectedCommands.has(own)) {
|
|
165
168
|
const originalChildren = own.commands.filter((child) => inherited.reference.children.includes(child.name()));
|
|
166
|
-
const actual = snapshot(own, originalChildren);
|
|
169
|
+
const actual = structuredClone(snapshot(own, known, originalChildren));
|
|
167
170
|
const overrides = differences(inherited.contract, actual).map((field) => ({
|
|
168
171
|
command: bindingPath(own),
|
|
169
172
|
field,
|
|
@@ -119,3 +119,8 @@ export declare const flatten: (commands: readonly CommandInfo[]) => CommandInfo[
|
|
|
119
119
|
* in snake_case. Hand it to `stepOf` and `errorObjectOf` as `targets`.
|
|
120
120
|
*/
|
|
121
121
|
export declare const stepTargets: (root: Command) => ((command: string) => StepTarget | undefined);
|
|
122
|
+
/**
|
|
123
|
+
* The command as if it had no parent, so it inherits nothing, and its subcommands the same way. `known` holds what one
|
|
124
|
+
* pass has described already, so a whole tree costs one description per command; keep it no longer than that pass.
|
|
125
|
+
*/
|
|
126
|
+
export declare const describeAlone: (command: Command, known: Map<Command, CommandInfo>) => CommandInfo;
|
|
@@ -40,7 +40,19 @@ export const stepTargets = (root) => {
|
|
|
40
40
|
visit(child, [], root.options);
|
|
41
41
|
return (command) => found.get(command);
|
|
42
42
|
};
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The command as if it had no parent, so it inherits nothing, and its subcommands the same way. `known` holds what one
|
|
45
|
+
* pass has described already, so a whole tree costs one description per command; keep it no longer than that pass.
|
|
46
|
+
*/
|
|
47
|
+
export const describeAlone = (command, known) => {
|
|
48
|
+
let found = known.get(command);
|
|
49
|
+
if (!found) {
|
|
50
|
+
found = describe(command, "", [], { offMcp: false, noOffline: false }, known);
|
|
51
|
+
known.set(command, found);
|
|
52
|
+
}
|
|
53
|
+
return found;
|
|
54
|
+
};
|
|
55
|
+
const describe = (command, cli, parents, above, known) => {
|
|
44
56
|
const path = [...parents, command.name()];
|
|
45
57
|
const args = command.registeredArguments.map(describeArgument);
|
|
46
58
|
const options = describeOptions(command);
|
|
@@ -68,7 +80,7 @@ const describe = (command, cli, parents, above) => {
|
|
|
68
80
|
...(visible(command).length > 0 && hasAction(command) ? { runnable: true } : {}),
|
|
69
81
|
arguments: args,
|
|
70
82
|
options,
|
|
71
|
-
commands: visible(command).map((child) => describe(child, cli, path, here)),
|
|
83
|
+
commands: visible(command).map((child) => (known ? describeAlone(child, known) : describe(child, cli, path, here))),
|
|
72
84
|
};
|
|
73
85
|
};
|
|
74
86
|
// Commander has no public getter for the action either; without one, a group prints its help.
|
|
@@ -144,7 +144,8 @@ at start-up and the `serve` relay are in [Lifecycle](lifecycle.md); the store's
|
|
|
144
144
|
- **Why:** two registries of the same environment must both be filled on every run, and one is always forgotten.
|
|
145
145
|
- **Enforced by:** review only.
|
|
146
146
|
- **Example:** a CLI extends `BaseEnvironment`, hands it to cli-core's `provide(program, environment)`, and its
|
|
147
|
-
commands read it with `environmentOf<Environment>(command)
|
|
147
|
+
commands read it with `environmentOf<Environment>(command)`. `process.*` appears only as the default for a field
|
|
148
|
+
the environment leaves unset, as WC-ARCH-10 says: `environmentOf(this).env ?? process.env`.
|
|
148
149
|
- **Exceptions:** a CLI that does not use cli-core's command skeleton keeps one environment of its own.
|
|
149
150
|
- **Applies to:** CLIs built on cli-core's command skeleton. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
150
151
|
|
package/docs/standards/ci.md
CHANGED
|
@@ -11,7 +11,8 @@ Rule ids on this page are `WC-CI-<n>`. How to read a rule, how ids work and whic
|
|
|
11
11
|
[Security page](security.md).
|
|
12
12
|
|
|
13
13
|
"The ten repositories" on this page means the six CLIs (tg, max, memo, drive, zoom, wa), cli-base, cli-core,
|
|
14
|
-
cli-testing, cli-docs, plus community itself.
|
|
14
|
+
cli-testing, cli-docs, plus community itself. cli-template follows every rule here as a CLI does
|
|
15
|
+
([New CLI](new-cli.md#the-template-repository-itself)).
|
|
15
16
|
|
|
16
17
|
## Rules
|
|
17
18
|
|
|
@@ -51,7 +52,7 @@ cli-testing, cli-docs, plus community itself.
|
|
|
51
52
|
- **Why:** it is the first build-level answer on a pull request, so it must stay fast enough that nobody wants it gone.
|
|
52
53
|
- **Enforced by:** `standards.yml:standards` · `pnpm ci:quick`.
|
|
53
54
|
- **Example:** what each repository runs today:
|
|
54
|
-
- tg, max, memo and
|
|
55
|
+
- tg, max, memo, wa and cli-template: `pnpm typecheck && pnpm build && pnpm lint:commands`;
|
|
55
56
|
- drive: `pnpm typecheck && pnpm lint:commands`, which builds first;
|
|
56
57
|
- zoom, cli-base and cli-testing: `pnpm typecheck`;
|
|
57
58
|
- cli-core: `pnpm typecheck` and its command lint test;
|
|
@@ -95,8 +96,8 @@ cli-testing, cli-docs, plus community itself.
|
|
|
95
96
|
cli-base `macos.yml` for the path code. cli-docs and community run `security-pr.yml`, a new-commit secret scan;
|
|
96
97
|
cli-docs validates its site before each deployment instead, and community has no build. Third-party security apps
|
|
97
98
|
may post their own checks.
|
|
98
|
-
- **Applies to:** the CLIs, cli-base, cli-core and cli-testing. cli-base, whatsapp-cli, tg-cli, max-cli, cli-core
|
|
99
|
-
cli-memo, zoom-cli
|
|
99
|
+
- **Applies to:** the CLIs, cli-base, cli-core and cli-testing. cli-base, whatsapp-cli, tg-cli, max-cli, cli-core,
|
|
100
|
+
cli-memo, zoom-cli, drive-cli and cli-template have `pr.yml`; cli-testing does not have it yet. **Since:**
|
|
100
101
|
cli-base 0.41.0 (pull request validation since cli-base 0.45.0). **Rulings:** none.
|
|
101
102
|
|
|
102
103
|
### WC-CI-6 · Run the changelog guard on every pull request
|
|
@@ -115,9 +116,12 @@ cli-testing, cli-docs, plus community itself.
|
|
|
115
116
|
catches that merge skew the same hour, not at the next release.
|
|
116
117
|
- **Enforced by:** `main.yml:validation` · `node-ci.yml:check` · `node-ci.yml:bun` · `node-ci.yml:secrets`.
|
|
117
118
|
- **Example:** `main.yml` calls the repository's own `ci.yml`, which calls cli-base `node-ci.yml`. That runs
|
|
118
|
-
`pnpm lint`, `pnpm typecheck`, `pnpm test:coverage
|
|
119
|
-
contracts
|
|
120
|
-
`docs:check` and parity
|
|
119
|
+
`pnpm lint`, `pnpm typecheck`, `pnpm test:coverage` and `pnpm build`. With `cli: true` it then runs one named step
|
|
120
|
+
per CLI script: `contracts:check`, `contracts:upgrade`, `security:no-leak`, `version:check`, `generate:check`,
|
|
121
|
+
`test:matrix:check`, `docs:check`, `package:check`, `lint:commands`, and `parity:check` with `parity: true`. Then
|
|
122
|
+
the project's `checks` lines, a Bun job, and a gitleaks scan of the whole history. The shared workflow owns the CLI
|
|
123
|
+
steps: the repository's `ci.yml` names a step it cannot run yet in `skip`, with the reason beside it, and puts only
|
|
124
|
+
its extra checks in `checks` (tg and max: `pnpm check:agent-docs`).
|
|
121
125
|
- **Exceptions:** platform suites (Windows, macOS, install checks) are skipped here and run at release, monthly or on
|
|
122
126
|
request. Nothing in this run publishes or tags. cli-docs runs its full validation before each deployment and monthly
|
|
123
127
|
instead; community has no build to validate.
|
|
@@ -140,8 +144,8 @@ what CI runs, an audit reads the `uses: WireCatLabs/cli-base/.github/workflows/
|
|
|
140
144
|
- **Why:** one shared workflow keeps the full suite the same everywhere, and a pin keeps a cli-base change from
|
|
141
145
|
breaking ten repositories at once.
|
|
142
146
|
- **Enforced by:** `ci.yml:ci`.
|
|
143
|
-
- **Example:** `uses: WireCatLabs/cli-base/.github/workflows/node-ci.yml@<commit> # v0.31.0`, with
|
|
144
|
-
checks in the `checks` input and `bun-run` for the Bun job. A job that only one repository needs stays beside it in
|
|
147
|
+
- **Example:** `uses: WireCatLabs/cli-base/.github/workflows/node-ci.yml@<commit> # v0.31.0`, with `cli: true` for
|
|
148
|
+
the CLI steps (WC-CI-7), the project's extra checks in the `checks` input and `bun-run` for the Bun job. A job that only one repository needs stays beside it in
|
|
145
149
|
that repository's `ci.yml`.
|
|
146
150
|
- **Exceptions:** never pin to `main`. cli-base itself calls `./.github/workflows/node-ci.yml`. cli-docs has its own
|
|
147
151
|
full workflow for the site, and community has none.
|
|
@@ -182,8 +186,8 @@ what CI runs, an audit reads the `uses: WireCatLabs/cli-base/.github/workflows/
|
|
|
182
186
|
- **Why:** a red merge breaks `main` for every parallel branch, and an admin merge skips the only gate there is.
|
|
183
187
|
- **Enforced by:** `whatsapp-cli: main ruleset required checks` · `cli-core: main ruleset required checks` ·
|
|
184
188
|
`tg-cli: main ruleset required checks` · `max-cli: main ruleset required checks` ·
|
|
185
|
-
`cli-memo: main ruleset required checks` · `drive-cli: main ruleset required checks`, plus the main
|
|
186
|
-
cli-base
|
|
189
|
+
`cli-memo: main ruleset required checks` · `drive-cli: main ruleset required checks`, plus the main rulesets of
|
|
190
|
+
cli-base and cli-template. In each repository with pr.yml, the ruleset requires the pull request validation jobs besides the
|
|
187
191
|
standards job. cli-testing and cli-docs have no pr.yml, so their rulesets require only the standards job, and
|
|
188
192
|
review covers the rest. zoom-cli has pr.yml,
|
|
189
193
|
but its ruleset still requires only the standards job.
|
package/docs/standards/docs.md
CHANGED
|
@@ -27,8 +27,8 @@ Rule ids on this page are `WC-DOC-<n>`. How to read a rule, how ids work and whi
|
|
|
27
27
|
- **Enforced by:** `tg-cli: pnpm docs:check` · `max-cli: pnpm docs:check` · `whatsapp-cli: pnpm docs:check` ·
|
|
28
28
|
`zoom-cli: pnpm docs:check`.
|
|
29
29
|
- **Example:** write "`--limit` defaults to 50", not "~~20~~ 50 (correction 2026-10-01, NEED-12)".
|
|
30
|
-
- **Exceptions:** none. `cli-dev docs-check` checks this when the CLI's rules module sets `userPage`. tg, max, wa
|
|
31
|
-
and
|
|
30
|
+
- **Exceptions:** none. `cli-dev docs-check` checks this when the CLI's rules module sets `userPage`. tg, max, wa,
|
|
31
|
+
zoom and cli-template set it; memo, drive and cli-base's default check do not.
|
|
32
32
|
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
33
33
|
|
|
34
34
|
### WC-DOC-3 · Every document states current facts; rewrite a wrong sentence in place
|
|
@@ -110,7 +110,9 @@ Rule ids on this page are `WC-DOC-<n>`. How to read a rule, how ids work and whi
|
|
|
110
110
|
- **Enforced by:** review only.
|
|
111
111
|
- **Example:** cite `src/store/open.ts:42` for our code, a URL for anything outside, a commit for history. Write "ran
|
|
112
112
|
X, saw Y" for what you ran, and "the docs describe X" for what you only read. Say at the top what is not verified.
|
|
113
|
-
- **Exceptions:**
|
|
113
|
+
- **Exceptions:** an instruction or a rule ("run `pnpm test`", "one store per profile") needs no anchor; a claim about
|
|
114
|
+
how the code or a service behaves, a measurement and a finding do. A file path with no line number is enough when
|
|
115
|
+
the claim is about the whole file. User pages state behaviour and do not carry code anchors.
|
|
114
116
|
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
115
117
|
|
|
116
118
|
### WC-DOC-12 · A runnable command in a document has no placeholder in it
|
|
@@ -136,7 +138,9 @@ Rule ids on this page are `WC-DOC-<n>`. How to read a rule, how ids work and whi
|
|
|
136
138
|
- **Why:** the GitHub release and the release scripts read the changelog by its headings.
|
|
137
139
|
- **Enforced by:** `pnpm docs:check` · `changelog-guard.yml`.
|
|
138
140
|
- **Example:** `## Unreleased` at the top while work is merged; a release dates it as `## 0.41.0 — 17.10.2026`. Each
|
|
139
|
-
version heading and the pending heading appear once.
|
|
141
|
+
version heading and the pending heading appear once. The pending section exists only while it has entries: after a
|
|
142
|
+
release the changelog opens on the dated section, and the next entry adds `## Unreleased` back. Never keep an empty
|
|
143
|
+
one.
|
|
140
144
|
- **Exceptions:** max-cli names the pending section `## Не выпущено`.
|
|
141
145
|
- **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
142
146
|
|
|
@@ -114,8 +114,12 @@ upgrade, and no script or agent reads its answers.
|
|
|
114
114
|
| [WC-LAY-2](layout.md) | The skeleton keeps it in `src/bin/clidev.ts`. The package's `"bin"` names the copy tool, `kit/dist/main.js`. | `new` writes the CLI's own `"bin"` entry into the copy. |
|
|
115
115
|
| [WC-OUT-18](output.md#wc-out-18--exit-with-cli-bases-code-for-each-kind-of-failure), [WC-OUT-19](output.md#wc-out-19--report-a-wrong-command-line-as-validation_error-exit-2) | The copy tool is exempt: it exits 1 with a plain sentence. The skeleton follows them. | People run the copy tool by hand; nothing branches on its exit code or parses its errors (owner, ASK-349). |
|
|
116
116
|
| [WC-TOOL-4](toolchain.md#wc-tool-4--extend-the-shared-typescript-config-and-do-not-loosen-it), [WC-TEST-11](testing.md#wc-test-11--keep-a-coverage-floor-and-raise-it-never-lower-it) | Both programs follow them: `kit/tsconfig.json` extends the shared base, and `kit/src` has its own coverage floor. | The copy tool writes into a person's new repository, so it gets the same build-time and test checks (owner, ASK-349). |
|
|
117
|
-
| [WC-TEST-14](testing.md#wc-test-14--check-the-upgrade-from-the-previous-published-version) | Exempt: `ci.yml` skips `contracts:upgrade`. | The skeleton
|
|
118
|
-
| [WC-DOC-12](docs.md#wc-doc-12--a-runnable-command-in-a-document-has-no-placeholder-in-it) | Exempt only for the placeholder names `clidev`, `clidev-cli`, `@wirecat/clidev-cli` and `WireCatLabs/clidev-cli
|
|
117
|
+
| [WC-TEST-14](testing.md#wc-test-14--check-the-upgrade-from-the-previous-published-version) | Exempt: `ci.yml` skips `contracts:upgrade`. | The skeleton has no local state of its own: its config file and run records are written by cli-core. A CLI made from the template turns the step on once it has local state and a published version to upgrade from. |
|
|
118
|
+
| [WC-DOC-12](docs.md#wc-doc-12--a-runnable-command-in-a-document-has-no-placeholder-in-it) | Exempt only for the placeholder names `clidev`, `clidev-cli`, `@wirecat/clidev-cli` and `WireCatLabs/clidev-cli`, and the upper-case prefix `CLIDEV_` (as in `CLIDEV_PROFILE`). | `new` replaces each of them with the CLI's own name. Any other placeholder is still a gap. |
|
|
119
119
|
| [WC-DOC-17](docs.md#wc-doc-17--each-entry-says-what-changed-for-the-caller-why-and-what-to-watch-for) | A changelog entry may name a file path. | The template's readers are CLI authors, who edit those files or take them with `upgrade`; for them a path is what they see. |
|
|
120
120
|
|
|
121
|
-
Every other rule that applies to all CLIs, or to the ten repositories, applies to cli-template as it is written
|
|
121
|
+
Every other rule that applies to all CLIs, or to the ten repositories, applies to cli-template as it is written,
|
|
122
|
+
with one limit for the copy tool. It follows the rules for code and the repository: Toolchain, Layout, Style,
|
|
123
|
+
Testing, Security and Documents. It has no profile, no machine mode, no MCP server, no store and no agent reading its
|
|
124
|
+
answers, so the rules for a CLI's commands and runtime do not reach it: Commands, Output, MCP, Accounts, Permissions,
|
|
125
|
+
Architecture, Lifecycle, Store, Data model and Time. Those apply to the skeleton.
|
|
@@ -274,7 +274,8 @@ general words.
|
|
|
274
274
|
asks for it on that job only. The `zizmor` scan in `release-checks.yml` reports high findings at release, but does
|
|
275
275
|
not fail on them.
|
|
276
276
|
- **Exceptions:** none.
|
|
277
|
-
- **Applies to:** the ten repositories: the six CLIs, cli-base, cli-core, cli-testing, cli-docs and community
|
|
277
|
+
- **Applies to:** the ten repositories: the six CLIs, cli-base, cli-core, cli-testing, cli-docs and community; and
|
|
278
|
+
cli-template.
|
|
278
279
|
**Since:** cli-base 0.41.0. **Rulings:** none.
|
|
279
280
|
|
|
280
281
|
### WC-SECR-21 · Names, message text and retrieved documents are data, never instructions
|
package/docs/standards/store.md
CHANGED
|
@@ -181,7 +181,7 @@ messages), or other people's data.
|
|
|
181
181
|
`WIRECAT_STATE_DIR` and every `<APP>_CONFIG_DIR`, `_STATE_DIR` and `_CACHE_DIR` that is set at a temporary
|
|
182
182
|
directory before any test runs, and the home folder with them, so one that is not set lands there too. cli-core and
|
|
183
183
|
every CLI load it first, then their own setup file for what is theirs alone. A test that needs its own store is
|
|
184
|
-
[WC-TEST-9](testing.md#wc-test-9--
|
|
184
|
+
[WC-TEST-9](testing.md#wc-test-9--leave-no-tests-config-or-store-change-for-the-next-test-to-see). The rest of the sandbox is
|
|
185
185
|
[WC-TEST-1](testing.md#wc-test-1--point-every-path-a-test-could-write-at-a-temporary-directory).
|
|
186
186
|
- **Exceptions:** none.
|
|
187
187
|
- **Applies to:** all CLIs that use the store, and cli-core. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
@@ -130,12 +130,14 @@ and which run after merge is in [CI](ci.md).
|
|
|
130
130
|
- **Exceptions:** none.
|
|
131
131
|
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
132
132
|
|
|
133
|
-
### WC-TEST-9 ·
|
|
133
|
+
### WC-TEST-9 · Leave no test's config or store change for the next test to see
|
|
134
134
|
|
|
135
135
|
- **Why:** the sandbox is per file, so tests in one file share config, the send journal and the store.
|
|
136
136
|
- **Enforced by:** review only.
|
|
137
|
-
- **Example:**
|
|
138
|
-
store
|
|
137
|
+
- **Example:** tests that only read may share one profile name, such as a file-level constant. A test that writes
|
|
138
|
+
config or the store either uses a profile name no other test in the file uses, or undoes its write in a `finally`
|
|
139
|
+
block, so a failed assertion does not skip the undo. A test that needs an empty store makes a fresh directory and
|
|
140
|
+
passes `MESSAGING_STORE`.
|
|
139
141
|
- **Exceptions:** none.
|
|
140
142
|
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
141
143
|
|
|
@@ -190,7 +192,9 @@ and which run after merge is in [CI](ci.md).
|
|
|
190
192
|
- **Enforced by:** `node-ci.yml:check` · `<app>: pnpm contracts:upgrade`.
|
|
191
193
|
- **Example:** `pnpm contracts:upgrade` runs the previous published package and this build on the same synthetic
|
|
192
194
|
state and checks that the data survives. Each store migration also has its own upgrade test (WC-STORE-9).
|
|
193
|
-
- **Exceptions:** a CLI with no local state.
|
|
195
|
+
- **Exceptions:** a CLI with no local state, or with no published version yet. Local state is data the CLI's own
|
|
196
|
+
code writes and reads back in a later run, such as a store, a session or a cache. A config file and run records
|
|
197
|
+
that only cli-core writes do not count.
|
|
194
198
|
- **Applies to:** all CLIs with local state. wa has no upgrade contract yet. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
195
199
|
|
|
196
200
|
### WC-TEST-15 · A refactor keeps `commands --json` and the MCP tool list as they were
|
|
@@ -16,7 +16,8 @@ one source of the presets. Each repository holds a copy of them in `config/wirec
|
|
|
16
16
|
`adopt-standards.py`.
|
|
17
17
|
|
|
18
18
|
"The ten repositories" on this page means the six CLIs (tg, max, memo, drive, zoom, wa), cli-base, cli-core,
|
|
19
|
-
cli-testing, cli-docs, plus community itself.
|
|
19
|
+
cli-testing, cli-docs, plus community itself. cli-template follows every rule here as a CLI does
|
|
20
|
+
([New CLI](new-cli.md#the-template-repository-itself)).
|
|
20
21
|
|
|
21
22
|
## Rules
|
|
22
23
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-base",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.62.0",
|
|
4
4
|
"description": "The parts every command line tool needs: output modes, terminal rendering, error model, exit codes, credentials, clocks",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "Viacheslav Ptsarev",
|