@wirecat/cli-base 0.57.1 → 0.59.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 CHANGED
@@ -8,6 +8,45 @@ 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.59.0 — 26.10.2026
12
+
13
+ ### Added
14
+
15
+ - **`skill install --for hermes` gives Hermes Agent the skill, and `skill status` reports what Hermes reads.** It writes
16
+ the `~/.agents/skills` copy, then adds that folder to Hermes's `skills.external_dirs` through Hermes's own
17
+ `hermes config set`, keeping every folder already listed. When Hermes is not on PATH or refuses, it copies the file
18
+ into `$HERMES_HOME/skills/<app>/` and says why; with neither `hermes` on PATH nor a Hermes home folder it fails
19
+ with `not_found` and writes nothing. `--for all` stays Claude Code and `~/.agents/skills`: it is the
20
+ default, and a bare `skill install` must not change another program's settings. `skill status --json` gains
21
+ `hermes: { path, installed, state, via }` beside `targets`, which keeps its two entries. It runs
22
+ `hermes config get` only when `HERMES_HOME` (else `~/.hermes`) exists, so a test with a temporary `HOME` starts no
23
+ Hermes unless its environment sets `HERMES_HOME`.
24
+
25
+ ## 0.58.0 — 25.10.2026
26
+
27
+ ### Changed — may break callers
28
+
29
+ - **The release lock file is now `wirecat-release-<package>.lock`, not `leemour-release-<package>.lock`**
30
+ (`WC-REL-3`, [Releases](docs/standards/releases.md)). The shared standards no longer carry one person's account
31
+ name. It is still one lock per npm package. A repository picks the new name up at its next standards sync from
32
+ community. During the rollout two names never guard the same package: each package is released from one repository,
33
+ and every `bin/release` checks out a fresh `origin/main` before it takes the lock, so all checkouts of a repository
34
+ use one name. The one exception: a release that already holds the old lock when that repository's sync merges, and
35
+ a second one started after it. Do not merge the sync while that repository is releasing. An old
36
+ `leemour-release-*.lock` file left in `$XDG_RUNTIME_DIR` is harmless.
37
+ - **The shared spelling list no longer allows `leemour`**, in `config/wirecat/cspell.json` and in the
38
+ `@wirecat/cli-base/cspell` preset. No repository spells the name in a checked page outside code or a link target.
39
+ A page that does now fails `docs:spell`; put the name in backticks or in the repository's own `words`.
40
+
41
+ ### Fixed
42
+
43
+ - **A `cli-dev lint` override's `owner` error names a neutral example, `@octocat`**, not the maintainer's handle.
44
+ - **The handbook no longer exempts cli-template from the README rule and the private vulnerability report rule**
45
+ ([New CLI](docs/standards/new-cli.md#the-template-repository-itself)). Since cli-template 0.1.7 its README has
46
+ "Why this template" and "Main features", and `new` writes the CLI its own README. Its `SECURITY.md` and
47
+ `docs/security.md` link cli-template, where a report about the template belongs, and `new` rewrites both links to
48
+ the CLI's repository. Both rules now apply to the template as written.
49
+
11
50
  ## 0.57.1 — 25.10.2026
12
51
 
13
52
  ### Added
@@ -16,7 +16,6 @@
16
16
  "frontmatter",
17
17
  "gitleaks",
18
18
  "kwallet",
19
- "leemour",
20
19
  "libc",
21
20
  "libsecret",
22
21
  "LUKS",
@@ -25,7 +25,6 @@
25
25
  "JSONL",
26
26
  "keyring",
27
27
  "kwallet",
28
- "leemour",
29
28
  "libc",
30
29
  "libsecret",
31
30
  "Lucene",
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "source": "https://github.com/WireCatLabs/community",
3
- "revision": "2963cda0f28eda3f731fe5edbf935d017d51ddf3",
3
+ "revision": "c846abf63807a478277fc61e6989742a81e7c21c",
4
4
  "files": {
5
5
  "biome.base.json": "f65d5754b03a4b7a3de08706e80b7b7a3f44df3218d7a573271195c2a3b3bfa0",
6
- "cspell.json": "8241122ed4d461976a838099b265ef8f2689a066608257b6fdc250d9b49b58aa",
6
+ "cspell.json": "fb47520d0b8847fe3928adfd88d2a28ad9283faaa58a3cea9cf9a4e534fb423e",
7
7
  "lefthook.yml": "2f65a2e3103006d43f6d6f0a07ab333c132286157763222a59a5a22676acd391",
8
8
  "markdownlint.json": "5ed7d6980054c539f3f3c0e47a090f1a725b15be2c486a769a836bc6cd48ad2d",
9
9
  "no-deep-wirecat-import.grit": "b9cb69b9ae125604c951cd924941ab089af32f9fa91f072e34531ca861ed125d",
@@ -13,7 +13,7 @@
13
13
  "no-sqlite-driver.grit": "19e1f510a88ddd9df44d6db43c1c9b603c3e5bbec03a3f65305b72eac7f09389",
14
14
  "no-unbounded-spread.grit": "f4178e5b74212fef58fec8916b6783d9f4f192ac618aa4ced041c4167bfbf81b",
15
15
  "release-lock-info.mjs": "4abab1f01ceee7d4c709222e923c9a6fc634bcf7b8b64d745a9ac495993310e0",
16
- "release-lock.sh": "bef364092c4ab595f3b9b4130e23570c8306223228cdfa3e9384230f3b7f4a49",
16
+ "release-lock.sh": "6c0b35a2ca0e7c53f6a5c23dcfe6edb62d565c9e402d4eab3071a6f7c4d57dbb",
17
17
  "rumdl.toml": "048b929176f13234324f5825d1e811397f1491b9cb6684a1953e268e2c2ed863",
18
18
  "check.mjs": "34b266411eaab7707030915f645308e86784b1574b21fa44ca974861a90c9ce1"
19
19
  },
@@ -17,7 +17,7 @@ wirecat_release_lock() {
17
17
  echo "release locking needs a package name in ./package.json" >&2
18
18
  return 1
19
19
  }
20
- _wirecat_release_lock="${XDG_RUNTIME_DIR:-/tmp}/leemour-release-$_wirecat_release_key.lock"
20
+ _wirecat_release_lock="${XDG_RUNTIME_DIR:-/tmp}/wirecat-release-$_wirecat_release_key.lock"
21
21
  exec 9>>"$_wirecat_release_lock"
22
22
  if flock -E 75 -w "$_wirecat_release_wait" 9; then
23
23
  printf '%s pid %s since %s\n' "$(basename "$PWD")" "$$" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$_wirecat_release_lock.who"
@@ -35,6 +35,53 @@ export interface SkillTargetStatus {
35
35
  }
36
36
  /** Each target's installed copy against this CLI's version; reads only, and an unreadable file is missing. */
37
37
  export declare const skillStatus: (app: SkillApp, env?: Env) => SkillTargetStatus[];
38
+ export interface ProgramResult {
39
+ status: number | null;
40
+ error?: Error;
41
+ stdout: string;
42
+ }
43
+ /** Runs a program with no shell, found and started with the `env` given, never `process.env`. */
44
+ export type RunProgram = (file: string, args: readonly string[], env: Env) => ProgramResult;
45
+ /** How Hermes reads the skill: the `~/.agents/skills` folder listed in its `skills.external_dirs`, or a copy of its own. */
46
+ export type HermesRoute = "external_dirs" | "copy";
47
+ /** `HERMES_HOME`, else the folder Hermes itself defaults to. */
48
+ export declare const hermesHome: (env: Env, platform?: NodeJS.Platform) => string;
49
+ export declare const hermesCopyPath: (appName: string, env: Env) => string;
50
+ export interface HermesInstall {
51
+ via: HermesRoute;
52
+ /** The file Hermes reads. */
53
+ path: string;
54
+ /** Why the copy was written instead, when it was. */
55
+ reason?: string;
56
+ /** An earlier copy this CLI wrote, taken out because Hermes refuses a skill name it finds twice. */
57
+ removed?: string;
58
+ }
59
+ /**
60
+ * Writes the skill to `~/.agents/skills`, then has Hermes read that folder by adding it to `skills.external_dirs`
61
+ * with Hermes's own `config set`, keeping every folder already listed. When Hermes is not on PATH, cannot be asked,
62
+ * or does not keep the folder, the skill is copied into `$HERMES_HOME/skills` instead — but only when that home exists:
63
+ * with neither the program nor its home, Hermes is not installed, and nothing is written.
64
+ */
65
+ export declare const installHermesSkill: (app: SkillApp, skill: URL, { env, run }?: {
66
+ env?: Env;
67
+ run?: RunProgram;
68
+ }) => {
69
+ written: string[];
70
+ hermes: HermesInstall;
71
+ };
72
+ export interface HermesSkillStatus extends Omit<SkillTargetStatus, "target"> {
73
+ /** How Hermes reads the copy reported; `null` when it reads none. */
74
+ via: HermesRoute | null;
75
+ }
76
+ /**
77
+ * The copy Hermes reads: its own `skills/<app>` first, as Hermes scans it first, else the `~/.agents/skills` one when
78
+ * `skills.external_dirs` lists that folder. Asks Hermes through `config get` when its home exists, so a host calls it
79
+ * only when asked.
80
+ */
81
+ export declare const hermesSkillStatus: (app: SkillApp, { env, run }?: {
82
+ env?: Env;
83
+ run?: RunProgram;
84
+ }) => HermesSkillStatus;
38
85
  /** What the command writes to and where home is, for this invocation — the host's own output resolution. */
39
86
  export type SkillEnvironment = (command: Command) => {
40
87
  renderer: Renderer;
@@ -5,12 +5,14 @@
5
5
  * Nothing here prints by itself. The command writes through the renderer and streams the host hands
6
6
  * it, and the hint is a string the host puts on stderr — never stdout, which an agent parses.
7
7
  */
8
- import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
8
+ import { existsSync, mkdirSync, readdirSync, readFileSync, rmdirSync, rmSync, writeFileSync } from "node:fs";
9
9
  import { homedir } from "node:os";
10
- import { dirname, join } from "node:path";
10
+ import { dirname, join, resolve } from "node:path";
11
11
  import { Command, Option } from "commander";
12
+ import crossSpawn from "cross-spawn";
12
13
  import { annotate } from "../commands/index.js";
13
14
  import { CliError } from "../errors.js";
15
+ import { executableEnvironment, executableOnPath } from "../executable.js";
14
16
  import { CHECK_EVERY_MS, isNewer, readNoticeState, writeUpdateState } from "../update/index.js";
15
17
  export const SKILL_TARGETS = ["claude", "agents"];
16
18
  const homeOf = (env) => env.HOME || env.USERPROFILE || homedir();
@@ -91,18 +93,133 @@ export const installSkill = (app, skill, { targets = SKILL_TARGETS, env = proces
91
93
  /** Each target's installed copy against this CLI's version; reads only, and an unreadable file is missing. */
92
94
  export const skillStatus = (app, env = process.env) => SKILL_TARGETS.map((target) => {
93
95
  const path = skillPath(target, app.appName, env);
96
+ return { target, ...(copyStatus(app, path) ?? { path, installed: null, state: "missing" }) };
97
+ });
98
+ const copyStatus = (app, path) => {
94
99
  let content;
95
100
  try {
96
101
  content = readFileSync(path, "utf8");
97
102
  }
98
103
  catch {
99
- return { target, path, installed: null, state: "missing" };
104
+ return undefined;
100
105
  }
101
106
  const installed = skillVersion(content);
102
107
  if (installed === undefined)
103
- return { target, path, installed: null, state: "unversioned" };
104
- return { target, path, installed, state: isNewer(app.version, installed) ? "stale" : "current" };
105
- });
108
+ return { path, installed: null, state: "unversioned" };
109
+ return { path, installed, state: isNewer(app.version, installed) ? "stale" : "current" };
110
+ };
111
+ const runProgram = (file, args, env) => {
112
+ const result = crossSpawn.sync(executableOnPath(file, env), [...args], {
113
+ encoding: "utf8",
114
+ timeout: 30_000,
115
+ maxBuffer: 256_000,
116
+ env: executableEnvironment(env),
117
+ });
118
+ return { status: result.status, ...(result.error ? { error: result.error } : {}), stdout: result.stdout ?? "" };
119
+ };
120
+ /** `HERMES_HOME`, else the folder Hermes itself defaults to. */
121
+ export const hermesHome = (env, platform = process.platform) => {
122
+ const set = env.HERMES_HOME?.trim();
123
+ if (set)
124
+ return set;
125
+ if (platform === "win32")
126
+ return join(env.LOCALAPPDATA || join(homeOf(env), "AppData", "Local"), "hermes");
127
+ return join(homeOf(env), ".hermes");
128
+ };
129
+ export const hermesCopyPath = (appName, env) => join(hermesHome(env), "skills", appName, "SKILL.md");
130
+ const EXTERNAL_DIRS = "skills.external_dirs";
131
+ /**
132
+ * The folders Hermes lists in `skills.external_dirs`, as its own `config get` answers, or `undefined` when Hermes
133
+ * cannot be asked or the setting is neither a list nor one folder — a value this code must never overwrite.
134
+ */
135
+ const externalDirs = (env, run) => {
136
+ const answer = run("hermes", ["config", "get", EXTERNAL_DIRS, "--json"], env);
137
+ if (answer.error || answer.status !== 0)
138
+ return undefined;
139
+ let value;
140
+ try {
141
+ value = JSON.parse(answer.stdout);
142
+ }
143
+ catch {
144
+ return undefined;
145
+ }
146
+ if (value === null)
147
+ return [];
148
+ if (typeof value === "string")
149
+ return [value];
150
+ return Array.isArray(value) && value.every((entry) => typeof entry === "string") ? value : undefined;
151
+ };
152
+ /** Hermes expands `~` and environment variables in each entry before it resolves the path. */
153
+ const expanded = (entry, env) => resolve(entry
154
+ .replace(/^~(?=$|[\\/])/, homeOf(env))
155
+ .replace(/\$\{(\w+)\}|\$(\w+)/g, (whole, braced, bare) => env[braced ?? bare ?? ""] ?? whole));
156
+ const lists = (dirs, folder, env) => dirs.some((entry) => expanded(entry, env) === resolve(folder));
157
+ /** Our own earlier copy, alone in its folder: anything else there was put by someone else and stays. */
158
+ const removeOwnCopy = (appName, env) => {
159
+ const path = hermesCopyPath(appName, env);
160
+ try {
161
+ if (readdirSync(dirname(path)).join() !== "SKILL.md")
162
+ return undefined;
163
+ if (skillName(readFileSync(path, "utf8")) !== appName)
164
+ return undefined;
165
+ rmSync(path);
166
+ rmdirSync(dirname(path));
167
+ return path;
168
+ }
169
+ catch {
170
+ return undefined;
171
+ }
172
+ };
173
+ /** Has Hermes list `folder` in `skills.external_dirs`, keeping the folders there; why not, when it does not. */
174
+ const readFolder = (folder, env, run) => {
175
+ const dirs = externalDirs(env, run);
176
+ if (dirs === undefined)
177
+ return "Hermes could not be asked for skills.external_dirs";
178
+ if (lists(dirs, folder, env))
179
+ return undefined;
180
+ const set = run("hermes", ["config", "set", EXTERNAL_DIRS, JSON.stringify([...dirs, folder])], env);
181
+ if (set.error || set.status !== 0)
182
+ return "hermes config set failed";
183
+ const after = externalDirs(env, run);
184
+ return after !== undefined && lists(after, folder, env) ? undefined : "Hermes did not keep the folder";
185
+ };
186
+ /**
187
+ * Writes the skill to `~/.agents/skills`, then has Hermes read that folder by adding it to `skills.external_dirs`
188
+ * with Hermes's own `config set`, keeping every folder already listed. When Hermes is not on PATH, cannot be asked,
189
+ * or does not keep the folder, the skill is copied into `$HERMES_HOME/skills` instead — but only when that home exists:
190
+ * with neither the program nor its home, Hermes is not installed, and nothing is written.
191
+ */
192
+ export const installHermesSkill = (app, skill, { env = process.env, run = runProgram } = {}) => {
193
+ if (!existsSync(hermesHome(env)) && run("hermes", ["--version"], env).error)
194
+ throw new CliError("not_found", "Hermes is not installed here; install Hermes first, or set HERMES_HOME");
195
+ const [shared] = installSkill(app, skill, { targets: ["agents"], env });
196
+ const reason = readFolder(dirname(dirname(shared)), env, run);
197
+ if (reason === undefined) {
198
+ const removed = removeOwnCopy(app.appName, env);
199
+ return { written: [shared], hermes: { via: "external_dirs", path: shared, ...(removed ? { removed } : {}) } };
200
+ }
201
+ const copy = hermesCopyPath(app.appName, env);
202
+ mkdirSync(dirname(copy), { recursive: true });
203
+ writeFileSync(copy, readFileSync(shared));
204
+ return { written: [shared, copy], hermes: { via: "copy", path: copy, reason } };
205
+ };
206
+ /**
207
+ * The copy Hermes reads: its own `skills/<app>` first, as Hermes scans it first, else the `~/.agents/skills` one when
208
+ * `skills.external_dirs` lists that folder. Asks Hermes through `config get` when its home exists, so a host calls it
209
+ * only when asked.
210
+ */
211
+ export const hermesSkillStatus = (app, { env = process.env, run = runProgram } = {}) => {
212
+ const own = copyStatus(app, hermesCopyPath(app.appName, env));
213
+ if (own)
214
+ return { ...own, via: "copy" };
215
+ const shared = skillPath("agents", app.appName, env);
216
+ // Asked with no home of its own, Hermes creates one: a status that only reads never starts it there.
217
+ const dirs = existsSync(hermesHome(env)) ? externalDirs(env, run) : undefined;
218
+ const read = dirs !== undefined && lists(dirs, dirname(dirname(shared)), env) ? copyStatus(app, shared) : undefined;
219
+ if (read)
220
+ return { ...read, via: "external_dirs" };
221
+ return { path: shared, installed: null, state: "missing", via: null };
222
+ };
106
223
  /**
107
224
  * `more`: skills a library ships for every CLI built on it, by name — `skill show <name>` prints one with
108
225
  * `{{command}}` turned into this CLI's command. They are printed, not installed: two CLIs would install
@@ -159,10 +276,22 @@ export const skillCommand = (app, skill, environment, more = {}) => {
159
276
  });
160
277
  annotate(command.command("install"), { effects: { remote: "none", local: "write" } })
161
278
  .summary("install the tool's instructions for AI agents")
162
- .description(`write SKILL.md to ~/.claude/skills/${app.appName}/ (Claude Code) and ~/.agents/skills/${app.appName}/ (Codex, Gemini CLI)`)
163
- .addOption(new Option("--for <agents>", "which agents to install for").choices(["claude", "agents", "all"]).default("all"))
279
+ .description(`write SKILL.md to ~/.claude/skills/${app.appName}/ (Claude Code) and ~/.agents/skills/${app.appName}/ (Codex, Gemini CLI, OpenClaw); --for hermes also has Hermes read ~/.agents/skills, which changes Hermes's own settings, so all leaves it out`)
280
+ .addOption(new Option("--for <agents>", "which agents to install for")
281
+ .choices(["claude", "agents", "hermes", "all"])
282
+ .default("all"))
164
283
  .action(function ({ for: chosen }) {
165
284
  const { renderer, env } = environment(this);
285
+ if (chosen === "hermes") {
286
+ const { written, hermes } = installHermesSkill(app, skill, env ? { env } : {});
287
+ renderer.result({ name: app.appName, version: app.version, written, hermes });
288
+ renderer.success(hermes.via === "external_dirs"
289
+ ? `${app.appName} ${app.version} skill installed: ${hermes.path}; Hermes reads ${dirname(dirname(hermes.path))} (skills.external_dirs)`
290
+ : `${app.appName} ${app.version} skill installed: ${hermes.path}, a copy for Hermes (${hermes.reason})`);
291
+ if (hermes.removed)
292
+ renderer.note(`removed the earlier copy Hermes would also have found: ${hermes.removed}`);
293
+ return;
294
+ }
166
295
  const targets = chosen === "all" ? SKILL_TARGETS : [chosen];
167
296
  const written = installSkill(app, skill, { targets, ...(env ? { env } : {}) });
168
297
  renderer.result({ name: app.appName, version: app.version, written });
@@ -173,11 +302,14 @@ export const skillCommand = (app, skill, environment, more = {}) => {
173
302
  examples: [`${app.command} skill status`, `${app.command} skill status --json`],
174
303
  })
175
304
  .summary("whether the installed instructions for AI agents match this version")
176
- .description(`for each place \`${app.command} skill install\` writes to: the path, the version installed there, and whether it is current, stale, missing or unversioned`)
305
+ .description(`for each place \`${app.command} skill install\` writes to, and for the copy Hermes reads: the path, the version installed there, and whether it is current, stale, missing or unversioned`)
177
306
  .action(function () {
178
307
  const { renderer, env } = environment(this);
308
+ // Hermes is its own key, not a third target: it is no folder `skill install` writes, and a caller that
309
+ // expects the two targets keeps working.
179
310
  const targets = skillStatus(app, env ?? process.env);
180
- renderer.result({ name: app.appName, version: app.version, targets });
311
+ const hermes = hermesSkillStatus(app, env ? { env } : {});
312
+ renderer.result({ name: app.appName, version: app.version, targets, hermes });
181
313
  });
182
314
  return command;
183
315
  };
@@ -19,8 +19,8 @@ export declare const overridesSchema: v.ArraySchema<v.StrictObjectSchema<{
19
19
  readonly rule: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a lower-case word, words joined by -">]>;
20
20
  readonly target: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
21
21
  readonly reason: v.SchemaWithPipe<readonly [v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, v.MinLengthAction<string, 10, "say why, in a sentence">]>;
22
- /** A GitHub handle or a team, `@leemour`. */
23
- readonly owner: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a GitHub handle or team, such as @leemour">]>;
22
+ /** A GitHub handle or a team, `@octocat`. */
23
+ readonly owner: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a GitHub handle or team, such as @octocat">]>;
24
24
  readonly review_after: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoDateAction<string, "a date, YYYY-MM-DD">]>;
25
25
  }, undefined>, undefined>;
26
26
  export type Override = v.InferOutput<typeof overridesSchema>[number];
@@ -124,8 +124,8 @@ export const overridesSchema = v.array(v.strictObject({
124
124
  rule: word,
125
125
  target: text,
126
126
  reason: v.pipe(text, v.minLength(10, "say why, in a sentence")),
127
- /** A GitHub handle or a team, `@leemour`. */
128
- owner: v.pipe(v.string(), v.regex(/^@[\w.-]+(\/[\w.-]+)?$/, "a GitHub handle or team, such as @leemour")),
127
+ /** A GitHub handle or a team, `@octocat`. */
128
+ owner: v.pipe(v.string(), v.regex(/^@[\w.-]+(\/[\w.-]+)?$/, "a GitHub handle or team, such as @octocat")),
129
129
  review_after: v.pipe(v.string(), v.isoDate("a date, YYYY-MM-DD")),
130
130
  }));
131
131
  /** What the schemas cannot say, so they stay convertible to JSON Schema: no entry twice. */
@@ -117,7 +117,5 @@ upgrade, and no script or agent reads its answers.
117
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 is never published under its own name, so there is no earlier package to upgrade from. A CLI made from the template turns the step on after its first release. |
118
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`. | `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
- | [WC-DOC-7](docs.md#wc-doc-7--readmemd-is-the-full-introduction-to-the-tool) | The README may leave out "why this tool" and "the main features". It keeps every other section. | It is the seed of the CLI's README: only the CLI's author can say why the tool exists and what it does best. |
121
- | [WC-SECR-18](security.md#wc-secr-18--report-a-vulnerability-privately) | `SECURITY.md` and `docs/security.md` link the placeholder repository, `WireCatLabs/clidev-cli`. The address and the rest of the page follow the rule. | `new` rewrites the link to the CLI's own repository; a link to cli-template would not be rewritten. |
122
120
 
123
121
  Every other rule that applies to all CLIs, or to the ten repositories, applies to cli-template as it is written.
@@ -45,9 +45,9 @@ on the [CI](ci.md) page.
45
45
  ```
46
46
 
47
47
  The helper comes from the community presets (`adopt-standards.py --only release-lock`). It holds
48
- `${XDG_RUNTIME_DIR:-/tmp}/leemour-release-<key>.lock` with `flock` on file descriptor 9 until the release ends.
48
+ `${XDG_RUNTIME_DIR:-/tmp}/wirecat-release-<key>.lock` with `flock` on file descriptor 9 until the release ends.
49
49
  `<key>` is the `name` in `package.json`, made safe for a file name: `@wirecat/cli-core` locks
50
- `leemour-release-wirecat-cli-core.lock`. The key is the package, not the folder, so every checkout and worktree
50
+ `wirecat-release-wirecat-cli-core.lock`. The key is the package, not the folder, so every checkout and worktree
51
51
  of one package shares the lock. A `package.json` without a name refuses the release.
52
52
  - **Exceptions:** none.
53
53
  - **Applies to:** all CLIs and shared libraries. **Since:** cli-base 0.41.0. **Rulings:** NEED-854, ASK-336.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-base",
3
- "version": "0.57.1",
3
+ "version": "0.59.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",