@wirecat/cli-core 0.0.0 → 0.18.1

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.
Files changed (173) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +344 -0
  3. package/config/biome.base.json +56 -0
  4. package/config/cspell.json +57 -0
  5. package/config/lefthook.yml +34 -0
  6. package/config/rumdl.toml +9 -0
  7. package/config/tsconfig.base.json +21 -0
  8. package/config/tsconfig.scripts.json +13 -0
  9. package/config/tsconfig.test.json +11 -0
  10. package/config/vale/CliDocs/Filler.yml +9 -0
  11. package/config/vale/CliDocs/SentenceLength.yml +6 -0
  12. package/config/vale/Tg/Tone.yml +7 -0
  13. package/dist/codegen/index.d.ts +12 -0
  14. package/dist/codegen/index.d.ts.map +1 -0
  15. package/dist/codegen/index.js +11 -0
  16. package/dist/codegen/index.js.map +1 -0
  17. package/dist/codegen/manifest.d.ts +47 -0
  18. package/dist/codegen/manifest.d.ts.map +1 -0
  19. package/dist/codegen/manifest.js +73 -0
  20. package/dist/codegen/manifest.js.map +1 -0
  21. package/dist/codegen/model.d.ts +146 -0
  22. package/dist/codegen/model.d.ts.map +1 -0
  23. package/dist/codegen/model.js +7 -0
  24. package/dist/codegen/model.js.map +1 -0
  25. package/dist/codegen/names.d.ts +4 -0
  26. package/dist/codegen/names.d.ts.map +1 -0
  27. package/dist/codegen/names.js +46 -0
  28. package/dist/codegen/names.js.map +1 -0
  29. package/dist/codegen/pipeline.d.ts +24 -0
  30. package/dist/codegen/pipeline.d.ts.map +1 -0
  31. package/dist/codegen/pipeline.js +193 -0
  32. package/dist/codegen/pipeline.js.map +1 -0
  33. package/dist/codegen/runtime.d.ts +24 -0
  34. package/dist/codegen/runtime.d.ts.map +1 -0
  35. package/dist/codegen/runtime.js +38 -0
  36. package/dist/codegen/runtime.js.map +1 -0
  37. package/dist/codegen/tree.d.ts +29 -0
  38. package/dist/codegen/tree.d.ts.map +1 -0
  39. package/dist/codegen/tree.js +87 -0
  40. package/dist/codegen/tree.js.map +1 -0
  41. package/dist/codegen/typescript.d.ts +6 -0
  42. package/dist/codegen/typescript.d.ts.map +1 -0
  43. package/dist/codegen/typescript.js +78 -0
  44. package/dist/codegen/typescript.js.map +1 -0
  45. package/dist/codegen/valibot.d.ts +10 -0
  46. package/dist/codegen/valibot.d.ts.map +1 -0
  47. package/dist/codegen/valibot.js +132 -0
  48. package/dist/codegen/valibot.js.map +1 -0
  49. package/dist/commands/index.d.ts +76 -0
  50. package/dist/commands/index.d.ts.map +1 -0
  51. package/dist/commands/index.js +62 -0
  52. package/dist/commands/index.js.map +1 -0
  53. package/dist/commands/page.d.ts +80 -0
  54. package/dist/commands/page.d.ts.map +1 -0
  55. package/dist/commands/page.js +100 -0
  56. package/dist/commands/page.js.map +1 -0
  57. package/dist/completion/index.d.ts +37 -0
  58. package/dist/completion/index.d.ts.map +1 -0
  59. package/dist/completion/index.js +87 -0
  60. package/dist/completion/index.js.map +1 -0
  61. package/dist/config.d.ts +20 -0
  62. package/dist/config.d.ts.map +1 -0
  63. package/dist/config.js +61 -0
  64. package/dist/config.js.map +1 -0
  65. package/dist/credentials.d.ts +48 -0
  66. package/dist/credentials.d.ts.map +1 -0
  67. package/dist/credentials.js +120 -0
  68. package/dist/credentials.js.map +1 -0
  69. package/dist/dev/bin.d.ts +3 -0
  70. package/dist/dev/bin.d.ts.map +1 -0
  71. package/dist/dev/bin.js +8 -0
  72. package/dist/dev/bin.js.map +1 -0
  73. package/dist/dev/main.d.ts +7 -0
  74. package/dist/dev/main.d.ts.map +1 -0
  75. package/dist/dev/main.js +149 -0
  76. package/dist/dev/main.js.map +1 -0
  77. package/dist/dev/next-version.d.ts +11 -0
  78. package/dist/dev/next-version.d.ts.map +1 -0
  79. package/dist/dev/next-version.js +33 -0
  80. package/dist/dev/next-version.js.map +1 -0
  81. package/dist/dev/slow-tests.d.ts +14 -0
  82. package/dist/dev/slow-tests.d.ts.map +1 -0
  83. package/dist/dev/slow-tests.js +17 -0
  84. package/dist/dev/slow-tests.js.map +1 -0
  85. package/dist/dev/test-matrix.d.ts +35 -0
  86. package/dist/dev/test-matrix.d.ts.map +1 -0
  87. package/dist/dev/test-matrix.js +93 -0
  88. package/dist/dev/test-matrix.js.map +1 -0
  89. package/dist/errors.d.ts +34 -0
  90. package/dist/errors.d.ts.map +1 -0
  91. package/dist/errors.js +36 -0
  92. package/dist/errors.js.map +1 -0
  93. package/dist/exit-codes.d.ts +11 -0
  94. package/dist/exit-codes.d.ts.map +1 -0
  95. package/dist/exit-codes.js +25 -0
  96. package/dist/exit-codes.js.map +1 -0
  97. package/dist/http/index.d.ts +20 -0
  98. package/dist/http/index.d.ts.map +1 -0
  99. package/dist/http/index.js +46 -0
  100. package/dist/http/index.js.map +1 -0
  101. package/dist/index.d.ts +16 -0
  102. package/dist/index.d.ts.map +1 -0
  103. package/dist/index.js +15 -0
  104. package/dist/index.js.map +1 -0
  105. package/dist/keyring.d.ts +27 -0
  106. package/dist/keyring.d.ts.map +1 -0
  107. package/dist/keyring.js +45 -0
  108. package/dist/keyring.js.map +1 -0
  109. package/dist/logger.d.ts +12 -0
  110. package/dist/logger.d.ts.map +1 -0
  111. package/dist/logger.js +7 -0
  112. package/dist/logger.js.map +1 -0
  113. package/dist/logging.d.ts +31 -0
  114. package/dist/logging.d.ts.map +1 -0
  115. package/dist/logging.js +58 -0
  116. package/dist/logging.js.map +1 -0
  117. package/dist/mcp/index.d.ts +32 -0
  118. package/dist/mcp/index.d.ts.map +1 -0
  119. package/dist/mcp/index.js +105 -0
  120. package/dist/mcp/index.js.map +1 -0
  121. package/dist/paths.d.ts +28 -0
  122. package/dist/paths.d.ts.map +1 -0
  123. package/dist/paths.js +23 -0
  124. package/dist/paths.js.map +1 -0
  125. package/dist/pretty.d.ts +12 -0
  126. package/dist/pretty.d.ts.map +1 -0
  127. package/dist/pretty.js +83 -0
  128. package/dist/pretty.js.map +1 -0
  129. package/dist/release/index.d.ts +38 -0
  130. package/dist/release/index.d.ts.map +1 -0
  131. package/dist/release/index.js +91 -0
  132. package/dist/release/index.js.map +1 -0
  133. package/dist/release/markdown.d.ts +36 -0
  134. package/dist/release/markdown.d.ts.map +1 -0
  135. package/dist/release/markdown.js +153 -0
  136. package/dist/release/markdown.js.map +1 -0
  137. package/dist/release/structure.d.ts +16 -0
  138. package/dist/release/structure.d.ts.map +1 -0
  139. package/dist/release/structure.js +74 -0
  140. package/dist/release/structure.js.map +1 -0
  141. package/dist/renderer.d.ts +34 -0
  142. package/dist/renderer.d.ts.map +1 -0
  143. package/dist/renderer.js +55 -0
  144. package/dist/renderer.js.map +1 -0
  145. package/dist/retry.d.ts +27 -0
  146. package/dist/retry.d.ts.map +1 -0
  147. package/dist/retry.js +26 -0
  148. package/dist/retry.js.map +1 -0
  149. package/dist/sanitize.d.ts +25 -0
  150. package/dist/sanitize.d.ts.map +1 -0
  151. package/dist/sanitize.js +52 -0
  152. package/dist/sanitize.js.map +1 -0
  153. package/dist/skill/index.d.ts +78 -0
  154. package/dist/skill/index.d.ts.map +1 -0
  155. package/dist/skill/index.js +190 -0
  156. package/dist/skill/index.js.map +1 -0
  157. package/dist/streams.d.ts +24 -0
  158. package/dist/streams.d.ts.map +1 -0
  159. package/dist/streams.js +26 -0
  160. package/dist/streams.js.map +1 -0
  161. package/dist/testing/index.d.ts +24 -0
  162. package/dist/testing/index.d.ts.map +1 -0
  163. package/dist/testing/index.js +29 -0
  164. package/dist/testing/index.js.map +1 -0
  165. package/dist/time.d.ts +21 -0
  166. package/dist/time.d.ts.map +1 -0
  167. package/dist/time.js +19 -0
  168. package/dist/time.js.map +1 -0
  169. package/dist/update/index.d.ts +80 -0
  170. package/dist/update/index.d.ts.map +1 -0
  171. package/dist/update/index.js +159 -0
  172. package/dist/update/index.js.map +1 -0
  173. package/package.json +116 -2
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Viacheslav Ptsarev
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,344 @@
1
+ # @wirecat/cli-core
2
+
3
+ The parts every command line tool needs and nobody enjoys writing twice: the two output streams,
4
+ a renderer for people and for machines, a closed error model with stable exit codes, the OS
5
+ keyring behind a testable seam, and injectable clocks.
6
+
7
+ Extracted from [`brazecli`](https://github.com/leemour/brazecli), where each piece earned its
8
+ shape, and shared with [`max-cli`](https://github.com/WireCatLabs/max-cli).
9
+
10
+ Published on npm and used by `max-cli`, `cli-messaging`, `tg-cli` and `brazecli`. What changed in
11
+ each version is in [`CHANGELOG.md`](CHANGELOG.md); how the package is put together and released is in
12
+ [`docs/dev/`](docs/dev/ARCHITECTURE.md).
13
+
14
+ ## The rule this package exists to keep
15
+
16
+ **In a machine mode, stdout carries data and nothing else.** No spinner, no `✓`, no warning, no
17
+ ANSI. Diagnostics go to stderr, in every mode including the pretty one. That is the contract a
18
+ script or an agent depends on, and `captureStreams` exists so a test can prove nothing leaked
19
+ across.
20
+
21
+ ```ts
22
+ import { captureStreams, createRenderer } from "@wirecat/cli-core"
23
+
24
+ const streams = captureStreams()
25
+ createRenderer({ format: "json", color: false, streams }).result({ chats: 2 })
26
+
27
+ streams.stdout // ['{"chats":2}']
28
+ streams.stderr // []
29
+ ```
30
+
31
+ ## What is in it
32
+
33
+ | | |
34
+ |---|---|
35
+ | `streams` | the stdout/stderr split, and the capture used to test it |
36
+ | `renderer` · `pretty` | `pretty` / `json` / `jsonl`; tables for lists, labelled lines for objects; `quiet` keeps only failures on stderr |
37
+ | `errors` · `exit-codes` | 14 closed error codes, one exit number each, so a script can branch on `$?` |
38
+ | `keyring` | `KeyringStore` with the system, memory and deliberately-broken implementations |
39
+ | `time` | monotonic and wall clocks, and the one sleep that both timeouts and backoff use |
40
+ | `logger` | the four-method interface a host adapts Pino to — this package logs nothing itself |
41
+ | `paths` | `env-paths` for config, state and cache, each overridable by environment variable |
42
+ | `config` | JSON config loading that names the bad field, and an atomic write that locks the directory down |
43
+ | `credentials` | environment → keyring → file, warning once and falling through when the keyring refuses |
44
+ | `logging` | a Pino adapter writing JSON lines with secrets redacted by field name; a file that cannot be written is reported to `onError`, never thrown |
45
+ | `retry` | full-jitter backoff, and the distinction between "no answer came" and "safe to repeat" |
46
+ | `/testing` | `captureStreams`, `memoryKeyring`, `brokenKeyring`, `fakeClock` |
47
+ | `/commands` | the command registry: `describeProgram`, `annotate`, `flatten` — see below |
48
+ | `/completion` | shell completion over the registry: `suggest`, `formatSuggestions` — see below |
49
+ | `/update` | keeping an install current: `installerOf`, `updateCommand`, `latestVersion`, `mayNotify`, `updateNotice`, `runUpdate` — see below |
50
+ | `/codegen` | build time: an official API description → types, Valibot schemas, an operation manifest and a coverage page — see below |
51
+ | `/release` | release time: `releaseCheck` and the checks it runs — changelog shape, links, package contents, version in step — see below |
52
+ | `/skill` | the CLI's SKILL.md for coding agents: `skillCommand` (`show`, `install`), `skillHint`, `skillResource` — see below |
53
+ | `/mcp` | local stdio MCP setup for Codex and Claude Code, plus a read-only handshake and tool-list probe |
54
+
55
+ **Nothing in the root export is HTTP.** Status classification, `Retry-After` parsing and the fetch
56
+ seam live in `@wirecat/cli-core/http`, so a CLI that speaks a socket never depends on a stack it
57
+ does not call:
58
+
59
+ ```ts
60
+ import { providerWaitMs, statusToCode } from "@wirecat/cli-core/http"
61
+ ```
62
+
63
+ **The command registry is `@wirecat/cli-core/commands`** — the whole command tree as data, like
64
+ `rails routes`, for an agent to read instead of `--help` and for generated documentation. It walks
65
+ the live [Commander](https://github.com/tj/commander.js) tree, so a command built in a loop from a
66
+ catalog appears exactly like a handwritten one; `annotate` adds what the tree cannot say. Commander
67
+ is a type here, not a runtime dependency — an optional peer, 15 or newer.
68
+
69
+ ```ts
70
+ import { annotate, describeProgram } from "@wirecat/cli-core/commands"
71
+
72
+ annotate(program.command("send"), { mutates: true, examples: ["max messages send 42 hi"] })
73
+ annotate(generated, { origin: "generated", operationId: "campaigns.list" })
74
+
75
+ describeProgram(program) // [{ path: ["send"], usage, origin, mutates, options: [...], commands: [...] }]
76
+ ```
77
+
78
+ Each option says whether it `takesValue` and, separately, whether it is `mandatory` — not
79
+ Commander's `required`, which means "takes a value when given" — plus its choices, default,
80
+ environment variable, the options it `conflicts` with and the values it `implies`.
81
+
82
+ **Shell completion is `@wirecat/cli-core/completion`**, built on the registry. `suggest` takes the
83
+ words typed so far and answers what may come next: a command, an action, an option, one of its
84
+ values, or an argument's values from a source the CLI hands in — a local cache, never the network,
85
+ because a shell calls it on every Tab. `formatSuggestions` writes the answer in the protocol of the
86
+ shell scripts [`@bomb.sh/tab`](https://github.com/bombshell-dev/tab) generates, so the CLI prints
87
+ those scripts with tab and answers `<cli> complete -- <words>` with this; cli-core itself does not
88
+ depend on tab.
89
+
90
+ ```ts
91
+ import { formatSuggestions, suggest } from "@wirecat/cli-core/completion"
92
+
93
+ const words = argv.slice(argv.indexOf("--") + 1)
94
+ streams.data(formatSuggestions(suggest({ commands, globalOptions, words, sources: { arguments: { chat: chatNames } } })))
95
+ ```
96
+
97
+ **Two traps worth knowing before you use the credential store.** The OS keyring is global: an entry
98
+ is addressed by service and account and knows nothing about which config directory asked for it, so
99
+ a throwaway config directory silently overwrites the real secret unless you pass `isolated: true`.
100
+ And a secret should never be handed to a logger in the first place — redaction by field name is the
101
+ second line of defence, not the first.
102
+
103
+ **Everything the environment knows can be passed in**: the clock, the sleep, the keyring, `env`,
104
+ `fetch` and the streams. The real ones are only defaults — `Credentials` without a `keyring` uses the
105
+ system keychain, `resolvePaths` without `env` reads `process.env`. Passing them is what makes a
106
+ timeout test finish instantly and a keyring test incapable of reaching a real keychain.
107
+
108
+ **Generating an API catalog is `@wirecat/cli-core/codegen`**, a build-time tool. It reads a
109
+ format-neutral `ApiModel` — operations with a transport binding (`http` method and path, or `rpc`
110
+ name), an `effect` (`read`, `write`, `destructive`) and how far the source can be trusted
111
+ (`contract`, `example`, `inferred`, `override`) — and writes committed files. It parses no file
112
+ format: the adapter from OpenAPI, Postman or anything else lives in the CLI that has that source,
113
+ so this adds no dependency. Generation stops, naming the problem, when two operations collide, an
114
+ operation is not classified as a read or a write, an override matches nothing, a reference points
115
+ nowhere or a construct cannot be expressed; it never drops an operation or falls back to `any`.
116
+
117
+ ```ts
118
+ import { generate, manifestGenerator, typesGenerator, valibotGenerator, writeArtifacts } from "@wirecat/cli-core/codegen"
119
+
120
+ const artifacts = generate(model, [typesGenerator({ path }), valibotGenerator({ path, typesImport }), manifestGenerator({ path })], {
121
+ overrides: { answerOnCallback: { effect: "write", reason: "a POST that edits a message" } },
122
+ banner: ["Source: spec/bot/schema.yaml", "Run: pnpm bot:generate"],
123
+ })
124
+ const stale = writeArtifacts(artifacts, { root, check: process.argv.includes("--check") })
125
+ ```
126
+
127
+ The generated schemas import `@wirecat/cli-core/codegen/runtime` at run time — a few number
128
+ helpers, not the generator. Numbers are expected from a lossless JSON parser (`lossless-json`): a
129
+ 64-bit integer comes out as its exact decimal string, and any other integer that does not fit a JS
130
+ number fails instead of rounding. Objects are loose, so a field the API added later passes through.
131
+ Unions retain every alternative, including recursive references and Boolean literals.
132
+ `definitionsGenerator` emits the same schema nodes for runtime input traversal; RPC parameters can be in the JSON body.
133
+ Enums and discriminated unions are **strict**: a value or subtype the snapshot does not know fails,
134
+ so decode a live stream (updates, webhooks) only where that is what you want.
135
+
136
+ **The schemas decode; they do not encode.** Their output carries an `int64` as a string, so sending
137
+ it back would put a string where the API expects a number. Validate a request with the schema, then
138
+ send the original lossless value.
139
+
140
+ **Keeping an install current is `@wirecat/cli-core/update`.** `installerOf(realpath(script))` says
141
+ which package manager put the CLI there — pnpm, npm or bun, measured on real installs — and
142
+ `updateCommand` gives the argv that updates it; a checkout or `npx` gets none. `latestVersion` asks
143
+ npm with a timeout and answers `undefined` on any failure. `mayNotify` says whether a person is at a
144
+ terminal to be told: never in JSON, a pipe, `--quiet`, `CI` or with `NO_UPDATE_NOTIFIER`.
145
+ `updateNotice` is the whole daily line: it stays silent when `update` or `complete` is anywhere on
146
+ the command line, asks npm at most once a day through a state file the CLI names, and returns the
147
+ sentence or `undefined` — it never throws. `runUpdate` starts the package manager with its output on
148
+ stderr (`spawnPlan` is the Windows `.cmd` rule). Nothing here updates or prints by itself — a CLI
149
+ holding somebody's credentials must not change itself unasked.
150
+
151
+ **A CLI's guide for coding agents is `@wirecat/cli-core/skill`.** `skillCommand(app, skillUrl,
152
+ environment)` is `<cli> skill show` — SKILL.md on stdout, even into a pipe, or `{ name, content }`
153
+ with `--json` — and `<cli> skill install [--for claude|agents|all]`, which writes it to
154
+ `~/.claude/skills/<appName>/` and `~/.agents/skills/<appName>/` with the CLI's version as
155
+ `metadata.version` in the frontmatter, refusing a file whose `name` is not `<appName>`. `environment` is the host's own renderer, streams and `env` for that invocation.
156
+ `skillHint` is one line, or `undefined`, for an agent (`AI_AGENT` or `CLAUDECODE` set) with no copy
157
+ installed or an older one, at most once a day, through the same state file as `updateNotice`; the
158
+ host prints it on stderr. `skillResource` is what an MCP server registers to serve the file as
159
+ `<command>://skill`, plus a line for its `instructions` — the SDK stays the host's.
160
+
161
+ **The release checks are `@wirecat/cli-core/release`** — everything about a release that a program
162
+ can decide. A check is `{ name, run }`, and `run` returns the problems it found, one line each; an
163
+ empty list is a pass. `releaseCheck(checks, { version, log })` runs them all, even after a failure,
164
+ prints `ok` or `FAIL` per check and returns how many failed — the caller exits. The checks every CLI
165
+ shares: `command(root, "pnpm", "lint")` for anything that passes by exiting 0, `notOnNpm`,
166
+ `packContents` against a list of what may ship, `changelogProblems` for the fixed headings and the
167
+ dated top section, `docsProblems` for dead links and anchors and the rules of the pages a user reads,
168
+ and `versionScript` — the whole of a `version:check` / `version:sync` script. Each CLI passes its
169
+ own headings, id prefixes and pages; the checks that belong to one messenger stay in that CLI.
170
+
171
+ ```ts
172
+ import { command, notOnNpm, packageVersion, releaseCheck } from "@wirecat/cli-core/release"
173
+
174
+ const version = packageVersion(root)
175
+ const failed = releaseCheck(
176
+ [
177
+ { name: "version not on npm", run: notOnNpm(root, "@wirecat/tg-cli", version) },
178
+ { name: "lint", run: command(root, "pnpm", "lint") },
179
+ ],
180
+ { version, log: console.log },
181
+ )
182
+ process.exit(failed === 0 ? 0 : 1)
183
+ ```
184
+
185
+ ## Where secrets actually go, per platform
186
+
187
+ The keyring is real on every desktop and absent on most servers, so the fallback is not an edge
188
+ case — it is the normal path in CI and containers. Checked against `@napi-rs/keyring` 2.1.0 on
189
+ 2026-09-19.
190
+
191
+ | Platform | Backing store | Needs installing |
192
+ |---|---|---|
193
+ | macOS | Keychain, via the Security framework | nothing — part of the OS |
194
+ | Windows | Credential Manager | nothing — part of the OS |
195
+ | Linux desktop (GNOME, KDE) | Secret Service over D-Bus — `gnome-keyring`, `kwallet` | nothing on a normal desktop; the keyring must be **unlocked** |
196
+ | Linux headless, container, WSL, CI | usually **nothing** — no session bus, no secrets daemon | falls back to a `0600` file, with one warning on stderr |
197
+ | FreeBSD | Secret Service, same as Linux | same |
198
+
199
+ **No compiler is involved.** The package ships prebuilt binaries for twelve platform triples —
200
+ macOS arm64/x64, Windows x64/ia32/arm64, Linux x64 and arm64 in both glibc and musl, armv7,
201
+ riscv64, FreeBSD x64 — so there is no Rust toolchain and no node-gyp on any mainstream target.
202
+ The Linux binary links only against libc: it speaks D-Bus itself rather than through libsecret,
203
+ so *libsecret is not a requirement* — a running Secret Service provider is.
204
+
205
+ Two consequences worth designing for rather than discovering:
206
+
207
+ - **`auto` is the right default and `file` must stay available.** `credentialStorage: "file"` skips
208
+ the keyring entirely, which is what a container wants and what a locked keyring makes necessary.
209
+ - **A locked Linux keyring can block on a prompt** rather than failing. That is the one case the
210
+ fallback does not rescue, and a CLI that hangs looks broken rather than locked.
211
+
212
+ ## Both runtimes
213
+
214
+ Node 22+ and Bun, and the Bun half is executed rather than assumed:
215
+
216
+ ```sh
217
+ pnpm test # vitest, Node
218
+ pnpm smoke:bun # the same exports, actually run under Bun
219
+ pnpm lint
220
+ pnpm typecheck
221
+ pnpm build
222
+ ```
223
+
224
+ The rest of the checks, and what each is for, are in [`docs/dev/TESTING.md`](docs/dev/TESTING.md).
225
+
226
+ ## Shared tooling
227
+
228
+ The configuration, development scripts and CI that every CLI on cli-core used to copy. A repository
229
+ takes each piece on its own.
230
+
231
+ **TypeScript.** Compiler options only; paths and file lists stay in the repository, because `tsc`
232
+ resolves them against the file that declares them.
233
+
234
+ ```jsonc
235
+ // tsconfig.json
236
+ {
237
+ "extends": "@wirecat/cli-core/tsconfig.base.json",
238
+ "compilerOptions": { "rootDir": "src", "outDir": "dist" },
239
+ "include": ["src/**/*.ts"],
240
+ "exclude": ["src/**/*.test.ts"]
241
+ }
242
+ // tsconfig.test.json — checks every file under src/, tests included, without emitting
243
+ { "extends": ["./tsconfig.json", "@wirecat/cli-core/tsconfig.test.json"] }
244
+ // tsconfig.scripts.json — checks scripts/
245
+ { "extends": ["./tsconfig.json", "@wirecat/cli-core/tsconfig.scripts.json"] }
246
+ ```
247
+
248
+ **Biome.** Formatter, linter and the rule that lets `scripts/` print. Your `files.includes` and
249
+ your own `overrides` (import boundaries) stay in your file; Biome adds them to the shared ones.
250
+ Write the specifier without `.json`: Biome reads anything ending in `.json` as a relative path.
251
+
252
+ ```json
253
+ { "extends": ["@wirecat/cli-core/biome"], "files": { "includes": ["**", "!**/dist", "!**/node_modules", "!pnpm-lock.yaml"] } }
254
+ ```
255
+
256
+ **lefthook.** Biome and gitleaks on staged files before a commit, `pnpm typecheck` before a push.
257
+ An extended file **overrides** your `lefthook.yml`; to change a shared hook, use `lefthook-local.yml`.
258
+
259
+ ```yaml
260
+ extends:
261
+ - node_modules/@wirecat/cli-core/config/lefthook.yml
262
+ ```
263
+
264
+ **`cli-dev`.** Installed with the package; `pnpm exec cli-dev help` lists the commands.
265
+
266
+ | Instead of | Run |
267
+ |---|---|
268
+ | `scripts/slow-tests.ts` | `cli-dev slow-tests [report] [count]` |
269
+ | `scripts/next-version.mjs` | `cli-dev next-version <wanted> <latest> <versions-json>` |
270
+ | `scripts/version.ts` | `cli-dev version [--sync] [--file src/version.ts]` |
271
+ | `scripts/docs-check.ts` | `cli-dev docs-check --rules scripts/release/checks.ts`, or `--ids CLI,MAX` with the English defaults |
272
+ | `scripts/test-matrix.ts` | `cli-dev test-matrix --program dist/program.js --untested scripts/test-matrix-untested.ts --name max [--check]` |
273
+
274
+ `--rules` names a module exporting `CHANGELOG` and `docsRules(root)`; `--program` one exporting
275
+ `createProgram`; `--untested` one exporting `UNTESTED`. A TypeScript module loads on Node 24 as it is.
276
+
277
+ **Docs.** Spelling, Markdown form and prose rules for the pages a user reads. The tools are your
278
+ own dev dependencies (`cspell`, `@cspell/dict-ru_ru`, `@cspell/dict-en-gb`, `rumdl`); the settings
279
+ and the shared word list are here. Code blocks and inline code are not spell-checked: commands and
280
+ options are checked against the program by the parity check.
281
+
282
+ ```jsonc
283
+ // cspell.json — the dictionaries resolve from your node_modules, so you import them yourself
284
+ {
285
+ "version": "0.2",
286
+ "import": ["@wirecat/cli-core/cspell", "@cspell/dict-en-gb/cspell-ext.json", "@cspell/dict-ru_ru/cspell-ext.json"],
287
+ "ignorePaths": ["docs/commands.md", "docs/dev/**", "CHANGELOG.md"],
288
+ "words": []
289
+ }
290
+ ```
291
+
292
+ ```sh
293
+ cspell --no-progress README.md "docs/*.md"
294
+ rumdl check --config node_modules/@wirecat/cli-core/config/rumdl.toml README.md docs/*.md
295
+ vale README.md docs/*.md # .vale.ini: StylesPath = node_modules/@wirecat/cli-core/config/vale
296
+ ```
297
+
298
+ `cli-dev docs-check --pages` adds the structure check: `docs/` against `docs/meta.json`, the
299
+ sidebar of [the docs portal](https://github.com/WireCatLabs/cli-docs/blob/main/docs/STRUCTURE.md).
300
+ Vale's styles are `CliDocs` (filler words, long sentences) and `Tg` (tg's tone: a Telegram client,
301
+ never "unofficial"); its rules warn rather than fail until they are tuned.
302
+
303
+ **CI.** Install, lint, typecheck, test with coverage, build, your extra checks, Bun, and a gitleaks
304
+ scan of the whole history. Jobs only your repository needs stay beside it in your `ci.yml`.
305
+
306
+ ```yaml
307
+ jobs:
308
+ ci:
309
+ uses: WireCatLabs/cli-core/.github/workflows/node-ci.yml@v<version>
310
+ with:
311
+ checks: |
312
+ pnpm docs:check
313
+ bun-run: pnpm build && pnpm smoke:bun # the default is pnpm smoke:bun
314
+ ```
315
+
316
+ Inputs: `node-version` (`"24"`), `checks` (shell lines run after the build), `bun` (`true`),
317
+ `bun-run`. The workflow is in the repository, not the package: pin a tag or a commit, not `main`.
318
+
319
+ ## Releasing
320
+
321
+ Raise `version` in `package.json` and date the `## Unreleased` section of
322
+ [`CHANGELOG.md`](CHANGELOG.md) as that version, through a pull request; merge it, then on `main`:
323
+
324
+ ```sh
325
+ bin/release
326
+ ```
327
+
328
+ It refuses a dirty tree, a branch other than `main` and a `main` that is not pushed. When npm already
329
+ has the version, or a higher one, it commits the next free version to `main` — the next minor for
330
+ `x.y.0`, the next patch otherwise — renames the changelog heading to match, and publishes that. Then it starts
331
+ [`release.yml`](.github/workflows/release.yml) and follows it. The workflow runs every check,
332
+ publishes through npm's [trusted publishing](https://docs.npmjs.com/trusted-publishers/) — no npm
333
+ token in GitHub, and npm attaches provenance — and tags `v<version>` only once npm shows the new version.
334
+
335
+ npm trusts the workflow **by file name**: the package's Trusted Publisher settings on npmjs.com name
336
+ `WireCatLabs` / `cli-core` / `release.yml`. Rename the file and publishing stops until they are updated.
337
+
338
+ `bin/release --local` is the fallback: it runs the same checks here and publishes with the npm token
339
+ from the keyring (`secret-tool`, service `npm`, account `leemour`) without printing it. The token
340
+ must be allowed to write `@wirecat/cli-core`, not only `@wirecat/max-cli`.
341
+
342
+ ## Licence
343
+
344
+ MIT.
@@ -0,0 +1,56 @@
1
+ {
2
+ "vcs": {
3
+ "enabled": true,
4
+ "clientKind": "git",
5
+ "useIgnoreFile": true
6
+ },
7
+ "formatter": {
8
+ "enabled": true,
9
+ "indentStyle": "space",
10
+ "indentWidth": 2,
11
+ "lineWidth": 120
12
+ },
13
+ "javascript": {
14
+ "formatter": {
15
+ "quoteStyle": "double",
16
+ "semicolons": "asNeeded",
17
+ "trailingCommas": "all",
18
+ "arrowParentheses": "always"
19
+ }
20
+ },
21
+ "linter": {
22
+ "enabled": true,
23
+ "rules": {
24
+ "preset": "recommended",
25
+ "style": {
26
+ "useConst": "error",
27
+ "noNonNullAssertion": "error"
28
+ },
29
+ "correctness": {
30
+ "noUnusedImports": "error",
31
+ "noUnusedVariables": "error"
32
+ },
33
+ "suspicious": {
34
+ "noExplicitAny": "error",
35
+ "noConsole": {
36
+ "level": "error",
37
+ "options": {
38
+ "allow": ["error"]
39
+ }
40
+ }
41
+ }
42
+ }
43
+ },
44
+ "overrides": [
45
+ {
46
+ "includes": ["scripts/**"],
47
+ "linter": {
48
+ "rules": {
49
+ "suspicious": {
50
+ "noConsole": "off"
51
+ }
52
+ }
53
+ }
54
+ }
55
+ ]
56
+ }
@@ -0,0 +1,57 @@
1
+ {
2
+ "version": "0.2",
3
+ "language": "en,en-GB,ru",
4
+ "ignoreRegExpList": [
5
+ "/^\\s*(```|~~~)[\\s\\S]*?^\\s*\\1/gm",
6
+ "/`[^`\\n]*`/g",
7
+ "/<!--[\\s\\S]*?-->/g",
8
+ "/\\]\\([^)]*\\)/g"
9
+ ],
10
+ "words": [
11
+ "armv",
12
+ "brazecli",
13
+ "bunx",
14
+ "claude",
15
+ "codegen",
16
+ "frontmatter",
17
+ "gitleaks",
18
+ "kwallet",
19
+ "leemour",
20
+ "libc",
21
+ "libsecret",
22
+ "LUKS",
23
+ "mtcute",
24
+ "npmjs",
25
+ "pioh",
26
+ "riscv",
27
+ "tailnet",
28
+ "tailscale",
29
+ "tgcli",
30
+ "unpushed",
31
+ "volta",
32
+ "wirecat",
33
+ "автодополнение",
34
+ "бана",
35
+ "вебхук",
36
+ "вебхуки",
37
+ "капчу",
38
+ "контрибьюторы",
39
+ "листателя",
40
+ "Минцифры",
41
+ "опкод",
42
+ "отсканируете",
43
+ "переголосовать",
44
+ "пулл",
45
+ "реквесты",
46
+ "рендерер",
47
+ "спиннера",
48
+ "схлопнет",
49
+ "схлопывает",
50
+ "токенам",
51
+ "токене",
52
+ "токеном",
53
+ "токену",
54
+ "хэша",
55
+ "хэшем"
56
+ ]
57
+ }
@@ -0,0 +1,34 @@
1
+ # Git hooks. Installed by the `prepare` npm script, which pnpm runs on every install — necessary
2
+ # because pnpm blocks dependencies' build scripts by default, so lefthook's own postinstall never
3
+ # fires and the hooks would otherwise silently never land.
4
+ #
5
+ # Install by hand: pnpm exec lefthook install
6
+ # Skip for one commit: LEFTHOOK=0 git commit …
7
+
8
+ pre-commit:
9
+ parallel: true
10
+ commands:
11
+ biome:
12
+ glob: "*.{ts,tsx,js,mjs,cjs,json,jsonc}"
13
+ # Staged files only. The whole-repo run is the CI gate; running it per-commit is how people
14
+ # end up reaching for --no-verify, which is worse than no hook.
15
+ run: pnpm exec biome check --no-errors-on-unmatched --files-ignore-unknown=true {staged_files}
16
+
17
+ gitleaks:
18
+ # Staged content only, and it warns rather than blocks when gitleaks is not installed: a
19
+ # fresh clone must still be able to commit. CI is the gate that cannot be skipped.
20
+ run: |
21
+ if command -v gitleaks >/dev/null 2>&1; then
22
+ gitleaks git --staged --no-banner --redact .
23
+ else
24
+ echo "gitleaks is not installed — staged changes were NOT scanned for secrets."
25
+ echo " install: https://github.com/gitleaks/gitleaks#installing (CI scans regardless)"
26
+ fi
27
+
28
+ pre-push:
29
+ parallel: true
30
+ commands:
31
+ typecheck:
32
+ # A messaging CLI's parity check builds into the same dist/ as typecheck, so it runs here, after
33
+ # it, rather than as a command of its own beside it. A project without the script skips it.
34
+ run: pnpm typecheck && pnpm -s run --if-present parity:check
@@ -0,0 +1,9 @@
1
+ # MD013 (line length): prose is wrapped by hand at 100–120 and tables cannot wrap.
2
+ # MD041 (first line a heading): READMEs open with the logo; docs-check --pages checks the pages.
3
+ # MD057 (relative links): docs-check follows every link and anchor already.
4
+ [global]
5
+ disable = ["MD013", "MD041", "MD057"]
6
+ exclude = ["docs/commands.md", "node_modules", "dist", "docs_ai"]
7
+
8
+ [MD033]
9
+ allowed-elements = ["p", "img", "a", "br", "details", "summary", "sup", "sub", "kbd"]
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2023",
4
+ "lib": ["ES2023"],
5
+ "module": "NodeNext",
6
+ "moduleResolution": "NodeNext",
7
+ "types": ["node"],
8
+ "strict": true,
9
+ "noUncheckedIndexedAccess": true,
10
+ "noImplicitOverride": true,
11
+ "noFallthroughCasesInSwitch": true,
12
+ "verbatimModuleSyntax": true,
13
+ "isolatedModules": true,
14
+ "esModuleInterop": true,
15
+ "skipLibCheck": true,
16
+ "declaration": true,
17
+ "declarationMap": true,
18
+ "sourceMap": true,
19
+ "composite": true
20
+ }
21
+ }
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": false,
4
+ "declaration": false,
5
+ "declarationMap": false,
6
+ "emitDeclarationOnly": false,
7
+ "noEmit": true,
8
+ "rootDir": "${configDir}",
9
+ "allowImportingTsExtensions": true
10
+ },
11
+ "include": ["${configDir}/scripts/**/*.ts"],
12
+ "exclude": []
13
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": false,
4
+ "declaration": false,
5
+ "declarationMap": false,
6
+ "emitDeclarationOnly": false,
7
+ "noEmit": true
8
+ },
9
+ "include": ["${configDir}/src/**/*.ts"],
10
+ "exclude": []
11
+ }
@@ -0,0 +1,9 @@
1
+ extends: existence
2
+ message: "'%s' tells the reader nothing; say what happens instead."
3
+ level: warning
4
+ ignorecase: true
5
+ nonword: true
6
+ # \p{L}, not \w or \b: those match no Cyrillic, and the rule would pass Russian text silently.
7
+ tokens:
8
+ - '(?<!\p{L})(?:simply|just|easily|obviously|of course|basically|really|very)(?!\p{L})'
9
+ - '(?<!\p{L})(?:просто|легко|очевидно|конечно|всего лишь|на самом деле|очень)(?!\p{L})'
@@ -0,0 +1,6 @@
1
+ extends: occurrence
2
+ message: "A sentence of more than 35 words; split it, one idea per sentence."
3
+ level: suggestion
4
+ scope: sentence
5
+ max: 35
6
+ token: '\p{L}+'
@@ -0,0 +1,7 @@
1
+ extends: existence
2
+ message: "'%s': tg is a Telegram client; state its limits as features, not as a risk."
3
+ level: error
4
+ ignorecase: true
5
+ nonword: true
6
+ tokens:
7
+ - '(?<!\p{L})(?:unofficial|at your own risk|banned|bans?|get blocked)(?!\p{L})'
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Turns an official API description into committed code: types, Valibot schemas, an operation
3
+ * manifest and a coverage page. Build-time only, and format-neutral: it parses no file format and
4
+ * adds no dependency. The adapter from OpenAPI, Postman or anything else lives in the consumer.
5
+ */
6
+ export { type CoverageOptions, coverageGenerator, definitionsGenerator, type ManifestOperation, manifestGenerator, } from "./manifest.js";
7
+ export type { ApiBody, ApiModel, ApiOperation, ApiParameter, ApiSchema, Binding, CodeGenerator, Confidence, Discriminator, Effect, GeneratedArtifact, HttpBinding, HttpMethod, Override, RpcBinding, SchemaNode, SourceInfo, SourceReference, } from "./model.js";
8
+ export { identifier, kebab } from "./names.js";
9
+ export { applyOverrides, CodegenError, type GenerateOptions, generate, validateModel, type WriteOptions, writeArtifacts, } from "./pipeline.js";
10
+ export { typesGenerator } from "./typescript.js";
11
+ export { type ValibotOptions, valibotGenerator } from "./valibot.js";
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/codegen/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EACL,KAAK,eAAe,EACpB,iBAAiB,EACjB,oBAAoB,EACpB,KAAK,iBAAiB,EACtB,iBAAiB,GAClB,MAAM,eAAe,CAAA;AACtB,YAAY,EACV,OAAO,EACP,QAAQ,EACR,YAAY,EACZ,YAAY,EACZ,SAAS,EACT,OAAO,EACP,aAAa,EACb,UAAU,EACV,aAAa,EACb,MAAM,EACN,iBAAiB,EACjB,WAAW,EACX,UAAU,EACV,QAAQ,EACR,UAAU,EACV,UAAU,EACV,UAAU,EACV,eAAe,GAChB,MAAM,YAAY,CAAA;AACnB,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EACL,cAAc,EACd,YAAY,EACZ,KAAK,eAAe,EACpB,QAAQ,EACR,aAAa,EACb,KAAK,YAAY,EACjB,cAAc,GACf,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,EAAE,KAAK,cAAc,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAA"}
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Turns an official API description into committed code: types, Valibot schemas, an operation
3
+ * manifest and a coverage page. Build-time only, and format-neutral: it parses no file format and
4
+ * adds no dependency. The adapter from OpenAPI, Postman or anything else lives in the consumer.
5
+ */
6
+ export { coverageGenerator, definitionsGenerator, manifestGenerator, } from "./manifest.js";
7
+ export { identifier, kebab } from "./names.js";
8
+ export { applyOverrides, CodegenError, generate, validateModel, writeArtifacts, } from "./pipeline.js";
9
+ export { typesGenerator } from "./typescript.js";
10
+ export { valibotGenerator } from "./valibot.js";
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/codegen/index.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,EAEL,iBAAiB,EACjB,oBAAoB,EAEpB,iBAAiB,GAClB,MAAM,eAAe,CAAA;AAqBtB,OAAO,EAAE,UAAU,EAAE,KAAK,EAAE,MAAM,YAAY,CAAA;AAC9C,OAAO,EACL,cAAc,EACd,YAAY,EAEZ,QAAQ,EACR,aAAa,EAEb,cAAc,GACf,MAAM,eAAe,CAAA;AACtB,OAAO,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAA;AAChD,OAAO,EAAuB,gBAAgB,EAAE,MAAM,cAAc,CAAA"}