@wirecat/cli-base 0.40.3 → 0.41.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.
@@ -0,0 +1,119 @@
1
+ # Style
2
+
3
+ ## Scope
4
+
5
+ How code and history read: comments say why and only where needed, names, TypeScript habits, and commit messages.
6
+
7
+ Rule ids on this page are `WC-STYLE-<n>`. How to read a rule, how ids work and which rule wins are in the
8
+ [index](README.md). Formatting and the lint rules are on the [Toolchain page](toolchain.md); Biome decides
9
+ formatting, so this page does not repeat it.
10
+
11
+ ## Rules
12
+
13
+ ### WC-STYLE-1 · Write a comment only where the code cannot say it, and say why
14
+
15
+ - **Why:** a comment costs a reader's attention and goes stale silently, so it must carry what the code cannot.
16
+ - **Enforced by:** review only.
17
+ - **Example:** a comment records a decision, warns about a trap, or names a constraint that is not visible in the
18
+ file: `// A static import would load the whole program before ensureSqlite could swap it.`
19
+ - **Exceptions:** none. When in doubt, leave it out; the commit message, the pull request and the tests carry the
20
+ story.
21
+ - **Applies to:** all repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
22
+
23
+ ### WC-STYLE-2 · Do not restate the code, narrate a change or leave old code in comments
24
+
25
+ - **Why:** these comments repeat what the code or git already says, and they turn false at the next edit.
26
+ - **Enforced by:** review only.
27
+ - **Example:** no `// increment the counter` above `count++`, no `// fixed the timeout bug`, no section banners, no
28
+ commented-out alternatives. A test gets a clear name, not a paragraph above it.
29
+ - **Exceptions:** existing comments in a file you only pass through stay; this rule is for new comments.
30
+ - **Applies to:** all repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
31
+
32
+ ### WC-STYLE-3 · Name files in kebab case, and name a test after what it tests
33
+
34
+ - **Why:** one naming habit lets a reader and a glob find a module and its test without searching.
35
+ - **Enforced by:** review only.
36
+ - **Example:** `src/format-markdown.ts` and `src/format-markdown.test.ts`. A test that covers a behaviour across
37
+ modules is named after that behaviour, such as `src/every-failure.test.ts`.
38
+ - **Exceptions:** none.
39
+ - **Applies to:** all repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
40
+
41
+ ### WC-STYLE-4 · Write functions in the small, and a class only where identity matters
42
+
43
+ - **Why:** a function that takes its inputs is easy to test and to reuse; a class earns its place by holding state
44
+ that must live together.
45
+ - **Enforced by:** review only.
46
+ - **Example:** a client that holds a connection and a session is a class. Parsing, mapping and formatting are plain
47
+ functions.
48
+ - **Exceptions:** none.
49
+ - **Applies to:** all repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
50
+
51
+ ### WC-STYLE-5 · Narrow a type instead of casting it with `as`
52
+
53
+ - **Why:** a cast tells the compiler to stop checking, so a renamed field fails at run time instead of at build time.
54
+ - **Enforced by:** review only.
55
+ - **Example:** narrow with a type guard, `instanceof` or a shape check. A test fixture uses an honest type, such as
56
+ `Omit<T, "x"> & { x: string | null }`, not `as unknown as T`.
57
+ - **Exceptions:** `as const` is allowed. A cast that cannot be avoided, because a library is typed too loosely, keeps
58
+ a comment on that line that says why. Existing casts are not yet removed; the rule holds for new code.
59
+ - **Applies to:** all repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
60
+
61
+ ### WC-STYLE-6 · In a `catch`, narrow with `instanceof` and wrap anything else
62
+
63
+ - **Why:** a caught value can be anything, and the error model needs a code and a cause to show the right message.
64
+ - **Enforced by:** review only.
65
+ - **Example:** `e instanceof CliError ? e : new CliError("provider_error", String(e), { cause: e })`. The error codes
66
+ and reasons are on the [Output page](output.md).
67
+ - **Exceptions:** none.
68
+ - **Applies to:** all repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
69
+
70
+ ### WC-STYLE-7 · Annotate exports and signatures, and let the rest infer
71
+
72
+ - **Why:** a redundant annotation is noise, and a hand-written `x is T` is not checked against its body.
73
+ - **Enforced by:** review only.
74
+ - **Example:** `export function count(items: readonly Item[]): number` with `const total = items.length` inside. In
75
+ `.filter()`, let the callback narrow: `xs.filter((x) => x !== null)`. Write `x is T` only for a named, reused guard
76
+ or a check the compiler cannot follow.
77
+ - **Exceptions:** none.
78
+ - **Applies to:** all repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
79
+
80
+ ### WC-STYLE-8 · Inline a type used once, and keep a type beside the code that owns it
81
+
82
+ - **Why:** a name for a one-off type sends the reader to another place for nothing, and a shared types file collects
83
+ types nobody owns.
84
+ - **Enforced by:** review only.
85
+ - **Example:** name a type when it is exported or used in three or more places. Keep it in the module that owns it. A
86
+ shared types file holds only a contract between several modules.
87
+ - **Exceptions:** none.
88
+ - **Applies to:** all repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
89
+
90
+ ### WC-STYLE-9 · Spread an optional field in, and build an object in one expression
91
+
92
+ - **Why:** an object built once has one type that the compiler checks; a key added later widens it or fails to
93
+ compile.
94
+ - **Enforced by:** review only.
95
+ - **Example:** `{ id, ...(signal === undefined ? {} : { signal }) }`, never `{ id, signal: undefined }`. Use
96
+ `=== undefined` when a falsy value is valid. A small helper that returns the optional part is fine.
97
+ - **Exceptions:** none. The shared TypeScript config leaves `exactOptionalPropertyTypes` off, so the compiler does not
98
+ force this.
99
+ - **Applies to:** all repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
100
+
101
+ ### WC-STYLE-10 · Write commit messages and pull request titles as conventional commits
102
+
103
+ - **Why:** the type and scope tell a reader what changed before they open it, and `!` tells the changelog guard that a
104
+ caller may break.
105
+ - **Enforced by:** review only.
106
+ - **Example:** `feat(standards): add the verb scan`, `docs(standards): toolchain rules`,
107
+ `feat!: remove the --take alias`. The body says why. A title marked breaking needs a changelog line; the
108
+ changelog rules are on the [Documents page](docs.md).
109
+ - **Exceptions:** none.
110
+ - **Applies to:** all repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
111
+
112
+ ## Sources
113
+
114
+ These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
115
+ holds.
116
+
117
+ - [cli-base `CONVENTIONS.md`, "Code"](https://github.com/WireCatLabs/cli-base/blob/main/docs/dev/CONVENTIONS.md#code)
118
+ - [tg-cli TypeScript standards](https://github.com/WireCatLabs/tg-cli/tree/main/agent-os/standards/typescript): no casts,
119
+ inference and optional fields
@@ -0,0 +1,222 @@
1
+ # Testing
2
+
3
+ ## Scope
4
+
5
+ How a CLI is tested: the sandbox, no network and no real waits in tests, what kind of test to write, coverage
6
+ floors, offline contracts, the test matrix, parity between CLIs, and the live checks that are not tests.
7
+
8
+ Rule ids on this page are `WC-TEST-<n>`. How to read a rule, how ids work and which rule wins are in the
9
+ [index](README.md).
10
+
11
+ Related pages: tests and the shared store are in [Store](store.md); secrets in logs are in [Security](security.md);
12
+ typechecking the tests and the Bun smoke test are in [Toolchain](toolchain.md); which checks run on a pull request
13
+ and which run after merge is in [CI](ci.md).
14
+
15
+ ### Words used here
16
+
17
+ - **Sandbox:** a vitest `setupFiles` entry that runs before every test file and moves every path a test could write
18
+ into a temporary directory.
19
+ - **Offline contract:** a check that runs the built CLI from outside, with no account and no network, and compares
20
+ its help, its `commands --json` and its MCP tool list with reviewed snapshots. The runner is `cli-contract` from
21
+ `@wirecat/cli-testing`.
22
+ - **Test matrix:** the page that lists every command and option of a CLI, with the test that drove it or the reason
23
+ why no offline test can.
24
+ - **Full validation:** the CI run on each merge to `main`, on a release and in the monthly run. It is not the pull
25
+ request run.
26
+
27
+ ## Rules
28
+
29
+ ### WC-TEST-1 · Point every path a test could write at a temporary directory
30
+
31
+ - **Why:** one test that falls back to a default path writes into the owner's config, state, cache or store.
32
+ - **Enforced by:** review only.
33
+ - **Example:** `src/testing/sandbox.ts` is a vitest `setupFiles` entry, so it holds for every test file, including
34
+ the next one. It sets the CLI's config, state and cache directories, the store variables
35
+ ([WC-STORE-14](store.md#wc-store-14--tests-never-open-the-owners-store)) and `TMPDIR`, `TMP` and `TEMP` to a
36
+ directory of its own. It closes resources first, then removes that directory and restores the variables when the
37
+ file is done.
38
+ - **Exceptions:** keep coverage reports and debug traces outside the sandbox, with bounded retention. Never clean
39
+ another session's scratch directory as test teardown.
40
+ - **Applies to:** all CLIs and the shared packages. drive has no sandbox yet, and zoom's sandbox moves only the
41
+ temporary directory. **Since:** cli-base 0.41.0. **Rulings:** none.
42
+
43
+ ### WC-TEST-2 · No test reaches the real keyring
44
+
45
+ - **Why:** the owner's tokens live in the real keychain, and one test that forgets an option would write there.
46
+ - **Enforced by:** `src/keyring.test.ts`.
47
+ - **Example:** every test passes `memoryKeyring()` from `@wirecat/cli-base/testing`. cli-base's own sandbox also
48
+ replaces `@napi-rs/keyring` with a module that throws, so a forgotten option fails the test instead of reaching the
49
+ keychain, and `src/keyring.test.ts` asserts that `systemKeyring` throws under it.
50
+ - **Exceptions:** none. The CLIs' sandboxes do not replace `@napi-rs/keyring` yet; tg and max only scope the keyring
51
+ entry through environment variables.
52
+ - **Applies to:** all CLIs and the shared packages. **Since:** cli-base 0.41.0. **Rulings:** none.
53
+
54
+ ### WC-TEST-3 · No test contacts a provider or the network
55
+
56
+ - **Why:** a test that reaches the network is slow, flaky, and can act on a real account.
57
+ - **Enforced by:** `max-cli: src/testing/unscripted.ts`.
58
+ - **Example:** max-cli's `src/testing/mock-max.ts` scripts the service: an answer per request, a refusal, or silence
59
+ to test a timeout. A request with no scripted answer fails the test that sent it, through a second `setupFiles`
60
+ entry. tg's `scripted({...})` adapter does the same job.
61
+ - **Exceptions:** none. No CLI has a global guard that refuses sockets and `fetch` in tests yet.
62
+ - **Applies to:** all CLIs and the shared packages. **Since:** cli-base 0.41.0. **Rulings:** none.
63
+
64
+ ### WC-TEST-4 · Give a command that must not connect an adapter that throws
65
+
66
+ - **Why:** a command that swallows an adapter error would otherwise pass while it still connects.
67
+ - **Enforced by:** review only.
68
+ - **Example:** an `--offline` test passes an adapter that throws "--offline must never open Telegram". When the
69
+ adapter is shared across a file, it is a `vi.fn`, and `afterAll` checks `expect(refuse).not.toHaveBeenCalled()`.
70
+ The test leaves the provider credentials out of `env` to show that none are needed.
71
+ - **Exceptions:** none.
72
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
73
+
74
+ ### WC-TEST-5 · No test waits for real time
75
+
76
+ - **Why:** a test that sleeps a real second crosses the time limit on a slow CI runner and fails at random.
77
+ - **Enforced by:** review only.
78
+ - **Example:** waits go through an injected `SleepLike`, and `fakeClock()` from `@wirecat/cli-base/testing` records
79
+ them without waiting. A test harness passes `sleep: async () => {}`. `pnpm test:slow` lists the 20 slowest tests
80
+ and the 10 slowest files.
81
+ - **Exceptions:** none. A new wait in the code gets a seam, never a longer test timeout.
82
+ - **Applies to:** all CLIs and the shared packages. **Since:** cli-base 0.41.0. **Rulings:** dev-process:4.
83
+
84
+ ### WC-TEST-6 · Test mostly through the command line, with `run()`
85
+
86
+ - **Why:** a test that drives `run()` checks what a person or an agent types, from parsing to output.
87
+ - **Enforced by:** review only.
88
+ - **Example:** most tests call `run(["chats", "list", "--json"], environment)` with fake effects and check stdout,
89
+ stderr and the exit code. Unit tests cover pure logic with many cases. Adapter contract cases (cli-core
90
+ `./testing`) cover implementations that must behave the same.
91
+ - **Exceptions:** none.
92
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
93
+
94
+ ### WC-TEST-7 · Write the test that pins a promise someone could break
95
+
96
+ - **Why:** a test that restates the code passes when the code is wrong; a test of a promise fails.
97
+ - **Enforced by:** review only.
98
+ - **Example:** reading history marks nothing read; a retried send reuses its id; a send with no answer is
99
+ `outcome_unknown`, never failed and never sent; an ambiguous chat name is refused, not guessed.
100
+ - **Exceptions:** none.
101
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
102
+
103
+ ### WC-TEST-8 · Prove that content does not leak, with a marker and visibly fake data
104
+
105
+ - **Why:** "the output has no message text" only means something when the text was there to leak; a convention alone
106
+ proves nothing.
107
+ - **Enforced by:** review only.
108
+ - **Example:** test data is visibly fake: `synthetic text ${id}`, the phone number `+71234567890`, the device id
109
+ `00000000-0000-0000-0000-000000000001`, `your-token-here`. Content that must stay hidden carries a unique marker,
110
+ and the test asserts that the marker is absent from output, errors and run events, while the id is present:
111
+ `expect(err).not.toContain(token)`. The positive check stops the test from passing by recording nothing.
112
+ - **Exceptions:** none.
113
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
114
+
115
+ ### WC-TEST-9 · Give each test its own profile or its own store
116
+
117
+ - **Why:** the sandbox is per file, so tests in one file share config, the send journal and the store.
118
+ - **Enforced by:** review only.
119
+ - **Example:** each test passes its own profile name first, or a file-level constant. A test that needs an empty
120
+ store makes a fresh directory and passes `MESSAGING_STORE`.
121
+ - **Exceptions:** none.
122
+ - **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
123
+
124
+ ### WC-TEST-10 · A skip is not a pass: report the suite that ran and its counts
125
+
126
+ - **Why:** a suite that ran nothing, or skipped what mattered, still exits 0.
127
+ - **Enforced by:** review only.
128
+ - **Example:** when you report work, name the suite that ran and give its counts: passed, failed and skipped. A
129
+ skipped test is counted as skipped, never as passed. That the tests are typechecked at all is
130
+ [WC-TOOL-5](toolchain.md#wc-tool-5--typecheck-every-typescript-file-source-tests-and-scripts).
131
+ - **Exceptions:** none.
132
+ - **Applies to:** all CLIs and the shared packages. **Since:** cli-base 0.41.0. **Rulings:** none.
133
+
134
+ ### WC-TEST-11 · Keep a coverage floor, and raise it; never lower it
135
+
136
+ - **Why:** a floor stops coverage from falling one pull request at a time.
137
+ - **Enforced by:** `pnpm test:coverage` · `node-ci.yml:check`.
138
+ - **Example:** `vitest.config.ts` holds thresholds for lines, statements, functions and branches over `src/`, a little
139
+ under what the suite reaches, and `perFile: { lines: 50 }`. A change that falls below gets a test, not a lower
140
+ number. Each excluded file has its reason written beside it.
141
+ - **Exceptions:** generated code, types-only files, the entry point, and code that only runs under Bun or as a
142
+ separate process, each with its reason.
143
+ - **Applies to:** all CLIs and the shared packages. memo has no thresholds yet; drive and zoom have no per-file
144
+ floor. **Since:** cli-base 0.41.0. **Rulings:** dev-process:4.
145
+
146
+ ### WC-TEST-12 · Every command and option has a test, or a written reason
147
+
148
+ - **Why:** a flag that no test passes can break without anyone noticing.
149
+ - **Enforced by:** `tg-cli: pnpm test:matrix` · `max-cli: pnpm test:matrix` · `whatsapp-cli: pnpm test:matrix` ·
150
+ `zoom-cli: pnpm test:matrix:check`.
151
+ - **Example:** under vitest, `run()` appends each parsed command path and option name (never a value) to
152
+ `coverage/argv.jsonl`. `pnpm test:matrix` turns that into `docs/dev/test-matrix.md` and fails on a command or
153
+ option with neither a test nor a reason. A reason names the live scenario that covers it. A test that calls a
154
+ function directly does not count.
155
+ - **Exceptions:** none.
156
+ - **Applies to:** all CLIs. tg, max, zoom and wa have the matrix; memo and drive do not yet. **Since:** cli-base
157
+ 0.41.0. **Rulings:** NEED-359.
158
+
159
+ ### WC-TEST-13 · Keep offline contracts for help, discovery, errors and the MCP tool list
160
+
161
+ - **Why:** agents depend on `commands --json` and the MCP tool list; a change to them must be seen and reviewed.
162
+ - **Enforced by:** `tg-cli: pnpm contracts:check` · `max-cli: pnpm contracts:check` ·
163
+ `whatsapp-cli: pnpm contracts:check` · `drive-cli: pnpm contracts:check` · `zoom-cli: pnpm contracts:check`.
164
+ - **Example:** `pnpm contracts:check` runs `cli-contract contracts/plan.json` against the built CLI. It checks the
165
+ help snapshot, that discovery is one JSON value on stdout with empty stderr, that a bad option gives the documented
166
+ exit code and one JSON error on stderr, and the MCP tool names, schemas and annotations. A change is accepted with
167
+ `pnpm exec cli-contract contracts/plan.json --update` and a reviewed diff.
168
+ - **Exceptions:** the contracts run in full validation, not on pull requests.
169
+ - **Applies to:** all CLIs. memo has no contracts yet. **Since:** cli-base 0.41.0. **Rulings:** none.
170
+
171
+ ### WC-TEST-14 · Check the upgrade from the previous published version
172
+
173
+ - **Why:** a release that cannot read what the previous one wrote loses the owner's data.
174
+ - **Enforced by:** `tg-cli: pnpm contracts:upgrade` · `max-cli: pnpm contracts:upgrade` ·
175
+ `drive-cli: pnpm contracts:upgrade` · `zoom-cli: pnpm contracts:upgrade`.
176
+ - **Example:** `pnpm contracts:upgrade` runs the previous published package and this build on the same synthetic
177
+ state and checks that the data survives. Each store migration also has its own upgrade test (WC-STORE-9).
178
+ - **Exceptions:** a CLI with no local state.
179
+ - **Applies to:** all CLIs with local state. memo and wa have no upgrade contract yet. **Since:** cli-base 0.41.0. **Rulings:** none.
180
+
181
+ ### WC-TEST-15 · A refactor keeps `commands --json` and the MCP tool list as they were
182
+
183
+ - **Why:** a change that is meant to move code must not change what agents see.
184
+ - **Enforced by:** review only.
185
+ - **Example:** capture `commands --json` and the MCP tool list from `main` before the first edit, and compare them
186
+ after the last. The offline contract snapshots (WC-TEST-13) do the same job in full validation.
187
+ - **Exceptions:** a refactor that changes behaviour on purpose says so in its pull request.
188
+ - **Applies to:** all CLIs and cli-core. **Since:** cli-base 0.41.0. **Rulings:** none.
189
+
190
+ ### WC-TEST-16 · A messenger CLI matches its column of the parity manifest
191
+
192
+ - **Why:** tg, max and wa promise the same commands and options where their messengers allow it.
193
+ - **Enforced by:** `<app>: pnpm parity:check` · `cli-core: parity.yml` · `cli-core: src/parity/manifest.test.ts`.
194
+ - **Example:** `pnpm parity:check` compares the CLI's `commands --json` with its column of `parity.json` from the
195
+ cli-core version it installed. cli-core's `parity.yml` builds tg, max and wa from `main` and runs the same check in
196
+ full validation.
197
+ - **Exceptions:** a row marked `planned` passes whether the command exists or not.
198
+ - **Applies to:** messenger CLIs (tg, max, wa). **Since:** cli-base 0.41.0. **Rulings:** none.
199
+
200
+ ### WC-TEST-17 · Merged into [WC-TOOL-6](toolchain.md#wc-tool-6--support-node-22-and-later-and-bun-and-run-the-built-exports-under-bun)
201
+
202
+ ### WC-TEST-18 · Check the real service by hand, never in the test suite
203
+
204
+ - **Why:** a check against a real account needs a live login and can send messages to people.
205
+ - **Enforced by:** review only.
206
+ - **Example:** live scenarios run by hand, by the owner or with the owner's yes, on test profiles and test chats
207
+ only. The cast lives in an ignored file, no profile is assumed, and the record keeps the shape of each answer,
208
+ never its content. When a scenario is run again, the sentence it supports is updated.
209
+ - **Exceptions:** none.
210
+ - **Applies to:** all CLIs that talk to a real account. **Since:** cli-base 0.41.0. **Rulings:** none.
211
+
212
+ ## Sources
213
+
214
+ These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
215
+ holds.
216
+
217
+ - [cli-base `TESTING.md`](https://github.com/WireCatLabs/cli-base/blob/main/docs/dev/TESTING.md)
218
+ - [cli-core `TESTING.md`](https://github.com/WireCatLabs/cli-core/blob/main/docs/dev/TESTING.md)
219
+ - [max-cli `TESTING.md`](https://github.com/WireCatLabs/max-cli/blob/main/docs/dev/TESTING.md)
220
+ - [cli-testing contracts guide](https://github.com/WireCatLabs/cli-testing/blob/main/docs/ci/CONTRACTS.md) and
221
+ [upgrade guide](https://github.com/WireCatLabs/cli-testing/blob/main/docs/ci/UPGRADES.md)
222
+ - [cli-testing CI docs](https://github.com/WireCatLabs/cli-testing/tree/main/docs/ci)
@@ -0,0 +1,232 @@
1
+ # Toolchain
2
+
3
+ ## Scope
4
+
5
+ How a repository is built and checked before review: the shared presets and `standards:check`, strict TypeScript,
6
+ the shared Biome preset and its grit plugins, the file size cap, import cycles, layer boundaries as lint, the command
7
+ lint, spelling and Markdown lint.
8
+
9
+ Rule ids on this page are `WC-TOOL-<n>`. How to read a rule, how ids work and which rule wins are in the
10
+ [index](README.md). What runs on which trigger is on the [CI page](ci.md).
11
+
12
+ The [community standards](https://github.com/WireCatLabs/community/blob/main/standards/README.md) repository is the
13
+ one source of the presets. Each repository holds a copy of them in `config/wirecat/`, installed by
14
+ `adopt-standards.py`.
15
+
16
+ "The ten repositories" on this page means the six CLIs (tg, max, memo, drive, zoom, wa), cli-base, cli-core,
17
+ cli-testing, cli-docs, plus community itself.
18
+
19
+ ## Rules
20
+
21
+ ### WC-TOOL-1 · Install the shared presets with `adopt-standards.py`, and never edit the copies
22
+
23
+ - **Why:** one reviewed set of presets keeps ten repositories alike; an edited copy drifts without anyone seeing it.
24
+ - **Enforced by:** `pnpm standards:check` · `standards.yml:standards`.
25
+ - **Example:** `python3 scripts/adopt-standards.py /path/to/project` from a community checkout, then commit
26
+ `config/wirecat/` and the lockfile. `standards:check` compares each file in `config/wirecat/` with the hash in
27
+ `config/wirecat/manifest.json`.
28
+ - **Exceptions:** none. A general improvement goes to community and comes back through a new adoption. A
29
+ project-only setting goes in the root config (WC-TOOL-2).
30
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
31
+
32
+ ### WC-TOOL-2 · Make each root config extend its copy in `config/wirecat/`
33
+
34
+ - **Why:** a root config that does not extend the preset runs none of the shared rules, and nothing else would say so.
35
+ - **Enforced by:** `pnpm standards:check` · `standards.yml:standards`.
36
+ - **Example:** `biome.json` has `"extends": ["./config/wirecat/biome.base.json"]`. `cspell.json` imports
37
+ `./config/wirecat/cspell.json`. `.markdownlint-cli2.jsonc` extends `./config/wirecat/markdownlint.json`.
38
+ `lefthook.yml` extends `config/wirecat/lefthook.yml`.
39
+ - **Exceptions:** do not extend `@wirecat/cli-base/biome` or `@wirecat/cli-base/lefthook.yml`. No repository uses
40
+ them, and `standards:check` accepts only the paths above.
41
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
42
+
43
+ ### WC-TOOL-3 · Pin the tool versions that `toolchain.json` names
44
+
45
+ - **Why:** two Biome versions give two verdicts on the same file.
46
+ - **Enforced by:** `pnpm standards:check` · `standards.yml:standards`.
47
+ - **Example:** `package.json` has `"packageManager": "pnpm@11.20.0"` and exact `devDependencies` for
48
+ `@biomejs/biome`, `cspell` and its three dictionaries, `lefthook` and `markdownlint-cli2`, as in the manifest.
49
+ - **Exceptions:** none.
50
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
51
+
52
+ ### WC-TOOL-4 · Extend the shared TypeScript config, and do not loosen it
53
+
54
+ - **Why:** strict options catch at build time what would otherwise fail on a user's machine.
55
+ - **Enforced by:** review only.
56
+ - **Example:** `tsconfig.json` has `"extends": "@wirecat/cli-base/tsconfig.base.json"` and adds only `rootDir`,
57
+ `outDir`, `include` and `exclude`. The base turns on `strict`, `noUncheckedIndexedAccess`, `noImplicitOverride`,
58
+ `noFallthroughCasesInSwitch`, `verbatimModuleSyntax` and `isolatedModules`.
59
+ - **Exceptions:** cli-docs is a Next.js site with its own config. cli-testing reaches the same file through
60
+ `./node_modules/@wirecat/cli-base/config/tsconfig.base.json`. A repository may add stricter options.
61
+ - **Applies to:** the CLIs, cli-core, cli-base and cli-testing. **Since:** cli-base 0.41.0. **Rulings:** none.
62
+
63
+ ### WC-TOOL-5 · Typecheck every TypeScript file: source, tests and scripts
64
+
65
+ - **Why:** a folder that no `tsconfig` covers breaks without a sign, and a test that compares against a constant that
66
+ does not exist passes for any input while CI stays green.
67
+ - **Enforced by:** `pnpm typecheck` · `pnpm ci:quick` · `node-ci.yml:check`.
68
+ - **Example:** `tsconfig.test.json` extends `@wirecat/cli-base/tsconfig.test.json`, covers all of `src/`, tests
69
+ included, checks them with `noEmit` and carries `"exclude": []`. Without that line it inherits the build's
70
+ `exclude`, checks no test file and still passes. `tsconfig.scripts.json` extends
71
+ `@wirecat/cli-base/tsconfig.scripts.json` and covers `scripts/`. The `typecheck` script runs all three projects.
72
+ - **Exceptions:** a new folder of TypeScript goes into one of these projects in the same pull request. memo has no
73
+ `tsconfig.scripts.json` yet.
74
+ - **Applies to:** the CLIs, cli-core, cli-base and cli-testing. **Since:** cli-base 0.41.0. **Rulings:** none.
75
+
76
+ ### WC-TOOL-6 · Support Node 22 and later, and Bun, and run the built exports under Bun
77
+
78
+ - **Why:** users install the CLIs with either runtime, and a static import of one runtime's module fails to load on
79
+ the other.
80
+ - **Enforced by:** `pnpm smoke:bun` · `node-ci.yml:bun`.
81
+ - **Example:** `"engines": { "node": ">=22" }` in `package.json`. Full validation runs on Node 24. `bun test` cannot
82
+ run the vitest suite, so `scripts/smoke.ts` runs the built exports with plain assertions, and the Bun job runs it
83
+ (`pnpm build && pnpm smoke:bun`). A new entry point gets a line there. Node 22 runs in the Windows install matrix of
84
+ tg, max and wa, in zoom's full validation and in cli-core's old-Node job.
85
+ - **Exceptions:** cli-docs and community publish no runtime package. memo, drive and cli-base do not run Node 22 in
86
+ any workflow yet. memo has no Bun smoke test yet.
87
+ - **Applies to:** the CLIs, cli-core and cli-base. **Since:** cli-base 0.41.0. **Rulings:** NEED-11.
88
+
89
+ ### WC-TOOL-7 · Let Biome format the code, and do not format by hand
90
+
91
+ - **Why:** one formatter ends every argument about layout, and review reads only the change.
92
+ - **Enforced by:** `pnpm lint` · `standards.yml:standards`.
93
+ - **Example:** the shared preset sets 2 spaces, double quotes, semicolons only where needed, trailing commas, arrow
94
+ parentheses always and 120 columns. Run `pnpm lint:fix` to apply them.
95
+ - **Exceptions:** none.
96
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
97
+
98
+ ### WC-TOOL-8 · Keep the shared Biome rules at error
99
+
100
+ - **Why:** each of these rules catches a real class of bug, and a warning gets ignored.
101
+ - **Enforced by:** `pnpm lint` · `standards.yml:standards`.
102
+ - **Example:** the preset turns on Biome's recommended rules and adds, as errors: useConst, noNonNullAssertion,
103
+ noUnusedImports, noUnusedVariables and noExplicitAny.
104
+ - **Exceptions:** a repository may add rules or raise a level. It may not turn a shared rule off (index, "Which rule
105
+ wins").
106
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
107
+
108
+ ### WC-TOOL-9 · Write to the console only with `console.error`, except in `scripts/`
109
+
110
+ - **Why:** stdout carries data only, so a stray `console.log` breaks every caller that parses it.
111
+ - **Enforced by:** `pnpm lint` · `standards.yml:standards`.
112
+ - **Example:** the preset sets noConsole to error and allows `error`. Command output goes through the renderer and the
113
+ streams of cli-base.
114
+ - **Exceptions:** files under `scripts/` may print, because they are development tools. A shared package prints
115
+ nothing at all, not even with `console.error`
116
+ ([WC-ARCH-19](architecture.md#wc-arch-19--shared-packages-print-nothing-and-log-nothing-on-their-own)).
117
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
118
+
119
+ ### WC-TOOL-10 · Keep the secret lint on, and suppress a finding only with its reason
120
+
121
+ - **Why:** a token caught at commit time never reaches the history; gitleaks is the second net.
122
+ - **Enforced by:** `pnpm lint` · `standards.yml:standards`.
123
+ - **Example:** noSecrets runs at error with an entropy threshold of 50. A reviewed site reads
124
+ `// biome-ignore lint/security/noSecrets: Crockford base32 alphabet, not a key`.
125
+ - **Exceptions:** the rule is off in test files, `test/`, `tests/`, `__tests__/`, `fixtures/` and `generated/`,
126
+ because those hold fake credentials or cannot keep a suppression comment.
127
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
128
+
129
+ ### WC-TOOL-11 · Keep the three grit plugins that block unsafe calls
130
+
131
+ - **Why:** a shell or an evaluated string built from input is how a command line tool runs an attacker's code.
132
+ - **Enforced by:** `pnpm lint` · `standards.yml:standards`.
133
+ - **Example:** these plugins report at error. no-shell-true reports `shell: true` in `spawn`, `exec`, `execFile` or
134
+ `fork` and their `Sync` forms. no-exec-interpolation reports `exec` or `execSync` with a template literal or a `+`
135
+ concatenation. no-new-function reports `new Function(...)` and `Function(...)`.
136
+ - **Exceptions:** a reviewed site may keep a `biome-ignore` comment with the reason, for example a Windows `.cmd` shim
137
+ with fixed arguments. The plugins read syntax, not data flow, so review and CodeQL at release cover the rest.
138
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
139
+
140
+ ### WC-TOOL-12 · Do not spread a list of user data into call arguments
141
+
142
+ - **Why:** V8 throws at about 125,000 arguments, so `push(...items)` fails on a large mailbox or chat.
143
+ - **Enforced by:** `pnpm lint` · `standards.yml:standards`.
144
+ - **Example:** write `for (const item of items) out.push(item)`, not `out.push(...items)`. The no-unbounded-spread
145
+ plugin reports `.push(...x)`, `Math.max(...x)` and `Math.min(...x)` as a warning.
146
+ - **Exceptions:** a spread over a list that is small by construction may stay. The plugin skips tests, `testing/`,
147
+ `scripts/`, `bench/` and `performance/`.
148
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
149
+
150
+ ### WC-TOOL-13 · Cap every file at 1,000 lines with Biome
151
+
152
+ - **Why:** a file past 1,000 lines always holds more than one job; the cap stops it before review has to.
153
+ - **Enforced by:** `<app>: biome noExcessiveLinesPerFile` · `cli-core: biome noExcessiveLinesPerFile`.
154
+ - **Example:** `biome.json` sets the style rule noExcessiveLinesPerFile to error with `"maxLines": 1000`, and turns it
155
+ off for `**/*.test.ts`, `test/**` and `**/generated/**`. Below the cap, the review signals at 300 and 500 lines are
156
+ in [WC-LAY-7](layout.md#wc-lay-7--treat-300-and-500-lines-as-review-signals).
157
+ - **Exceptions:** a file that is one cohesive unit may get its own override, listed by path in `biome.json` (tg
158
+ `src/telegram/adapter.ts`; four files in cli-core). A smaller cap is allowed: zoom uses 500. cli-base, cli-testing,
159
+ cli-docs and community have no cap yet.
160
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
161
+
162
+ ### WC-TOOL-14 · Turn on the import cycle rule
163
+
164
+ - **Why:** a cycle makes load order decide which value a module sees, and the failure shows only at run time.
165
+ - **Enforced by:** `<app>: biome noImportCycles` · `cli-core: biome noImportCycles`.
166
+ - **Example:** `biome.json` sets the suspicious rule noImportCycles to error.
167
+ - **Exceptions:** cli-base, cli-testing, cli-docs and community do not have it yet.
168
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
169
+
170
+ ### WC-TOOL-15 · Declare layer boundaries as restricted imports in `biome.json`
171
+
172
+ - **Why:** a boundary that only a document states is crossed in the first busy week.
173
+ - **Enforced by:** `<app>: biome noRestrictedImports` · `cli-testing: biome noRestrictedImports`.
174
+ - **Example:** an override with `"includes": ["src/commands/**"]` sets the style rule noRestrictedImports with the
175
+ folders that layer may not import, and a `message` that says why, such as "commands speak the domain model; only
176
+ `src/client.ts` may reach the protocol". Which layers exist and which way they depend is on the
177
+ [Architecture page](architecture.md).
178
+ - **Exceptions:** cli-base and cli-docs have no layer overrides. cli-core has them, but its last override replaces
179
+ the earlier ones today, so they catch nothing until it is fixed.
180
+ - **Applies to:** the CLIs, cli-core and cli-testing. **Since:** cli-base 0.41.0. **Rulings:** none.
181
+
182
+ ### WC-TOOL-16 · Run the command lint on the built CLI, and let its baseline only shrink
183
+
184
+ - **Why:** the command tree is what users and agents see; the lint holds it to the shared vocabulary on every pull
185
+ request.
186
+ - **Enforced by:** `<app>: pnpm lint:commands`.
187
+ - **Example:** `"lint:commands": "cli-dev lint --cli dist/bin/max.js"`, run by `ci:quick` after the build. Known
188
+ violations live in `lint/baseline/<rule-id>.json`; an entry with no violation left fails the run. A permanent
189
+ exception goes in `lint/overrides.json` with `rule`, `target`, `reason`, `owner` and `review_after`. More than 10
190
+ overrides fail the run, and so does one past its date. The rules themselves are on the
191
+ [Commands page](commands.md).
192
+ - **Exceptions:** zoom runs `lint:commands` in full validation only. cli-core runs its own command lint test in
193
+ `ci:quick`. cli-base, cli-testing, cli-docs and community have no command tree.
194
+ - **Applies to:** the CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
195
+
196
+ ### WC-TOOL-17 · Spell-check prose with the shared `cspell` config
197
+
198
+ - **Why:** a typo in a page or a help text is the first thing a reader sees.
199
+ - **Enforced by:** `pnpm docs:spell`.
200
+ - **Example:** `cspell.json` imports `./config/wirecat/cspell.json` (English, British English, Russian, Spanish and the
201
+ WireCat word list) and adds only the project's own words. Code blocks and inline code are not checked.
202
+ - **Exceptions:** a general word goes into the community word list, not into each project. In tg, max, wa, zoom and
203
+ cli-testing, `cspell` also runs inside `docs:check` in full validation.
204
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
205
+
206
+ ### WC-TOOL-18 · Lint Markdown with `markdownlint-cli2` and the shared preset
207
+
208
+ - **Why:** a code block with no language or an empty link breaks the docs site and agent readers alike.
209
+ - **Enforced by:** `pnpm docs:lint` · `standards.yml:standards`.
210
+ - **Example:** the preset turns on only: no tabs (MD010), at most two blank lines in a row (MD012), a language on
211
+ every fenced code block (MD040) and no empty links (MD042). `.markdownlint-cli2.jsonc` lists the files to check.
212
+ - **Exceptions:** a repository may turn on more rules in its root config.
213
+ - **Applies to:** the ten repositories. **Since:** cli-base 0.41.0. **Rulings:** none.
214
+
215
+ ### WC-TOOL-19 · Check user pages with `rumdl` and the shared `rumdl.toml`
216
+
217
+ - **Why:** user pages feed the docs site, so their Markdown form is checked more closely than developer notes.
218
+ - **Enforced by:** `tg-cli: pnpm docs:check` · `max-cli: pnpm docs:check` · `whatsapp-cli: pnpm docs:check` ·
219
+ `zoom-cli: pnpm docs:check` · `cli-testing: pnpm docs:check`.
220
+ - **Example:** `rumdl check --config config/wirecat/rumdl.toml README.md docs/*.md`, inside `docs:check`. The preset
221
+ turns off line length (MD013), first-line heading (MD041) and relative links (MD057), because other checks cover
222
+ them.
223
+ - **Exceptions:** memo, drive, cli-base and cli-core do not run `rumdl` yet.
224
+ - **Applies to:** the CLIs and cli-testing. **Since:** cli-base 0.41.0. **Rulings:** none.
225
+
226
+ ## Sources
227
+
228
+ These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
229
+ holds.
230
+
231
+ - [community standards](https://github.com/WireCatLabs/community/blob/main/standards/README.md)
232
+ - [cli-base `TOOLING.md`](https://github.com/WireCatLabs/cli-base/blob/main/docs/TOOLING.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-base",
3
- "version": "0.40.3",
3
+ "version": "0.41.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",
@@ -16,6 +16,7 @@
16
16
  "files": [
17
17
  "dist",
18
18
  "config",
19
+ "docs/standards",
19
20
  "README.md",
20
21
  "LICENSE",
21
22
  "THIRD_PARTY_NOTICES"
@@ -88,7 +89,7 @@
88
89
  "typecheck": "tsc --build --force --emitDeclarationOnly && tsc -p tsconfig.test.json && tsc -p tsconfig.scripts.json",
89
90
  "lint": "biome check .",
90
91
  "lint:fix": "biome check --write .",
91
- "docs:check": "pnpm build && node dist/dev/bin.js docs-check --ids CLI,MAX,OPS,CORE,SPEC,DOC",
92
+ "docs:check": "pnpm build && node dist/dev/bin.js docs-check --ids CLI,MAX,OPS,CORE,SPEC,DOC && node scripts/standards-ids.ts",
92
93
  "test": "vitest run",
93
94
  "test:watch": "vitest",
94
95
  "test:coverage": "vitest run --coverage",
@@ -99,7 +100,7 @@
99
100
  "prepublishOnly": "pnpm build",
100
101
  "standards:check": "node config/wirecat/check.mjs",
101
102
  "docs:lint": "markdownlint-cli2",
102
- "docs:spell": "cspell lint --config cspell.json --no-progress README.md",
103
+ "docs:spell": "cspell lint --config cspell.json --no-progress README.md \"docs/standards/**/*.md\"",
103
104
  "ci:quick": "pnpm typecheck"
104
105
  },
105
106
  "dependencies": {