@wirecat/cli-base 0.65.0 → 0.65.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.
package/CHANGELOG.md CHANGED
@@ -8,6 +8,18 @@ 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.65.1 — 29.10.2026
12
+
13
+ ### Fixed
14
+
15
+ - **`Credentials` with `isolated` no longer writes the system keyring** ([WC-SECR-24](docs/standards/security.md), new:
16
+ a run with moved folders never writes the owner's keyring). A per-folder keyring service (`<app>:<folder>`) was
17
+ leaving one copy of a sign-in per temp folder. A secret goes to the private file in the moved config folder
18
+ instead; one saved in the keyring before is still read, so an existing per-folder sign-in keeps working until its
19
+ next save moves it to the file. A test passing its own keyring (`memoryKeyring()`) is unchanged.
20
+ - **`cli-dev changelog restore` no longer fails a package's first release.** With no `v<x.y.z>` tag yet it says there
21
+ is nothing to restore and exits 0; `bin/release` stopped at "no v<x.y.z> tag to compare the changelog with".
22
+
11
23
  ## 0.65.0 — 27.10.2026
12
24
 
13
25
  ### Added
@@ -19,6 +19,9 @@ export interface CredentialsOptions {
19
19
  * not — it overwrites the real secret for that account. In `brazecli` that destroyed two working
20
20
  * keys on 2026-09-14, and a keyring entry cannot be read back out. Passing `true` here scopes the
21
21
  * service name to the directory, making the isolation people already assume they have real.
22
+ *
23
+ * With the system keyring it also keeps new secrets out of it: they go to the file in the moved directory, so a test
24
+ * or worktree run leaves nothing in the owner's keyring (WC-SECR-24). A secret saved there before is still read.
22
25
  */
23
26
  isolated?: boolean;
24
27
  /** Read before the keyring, for CI. */
@@ -21,6 +21,7 @@ export class Credentials {
21
21
  #envVar;
22
22
  #fileName;
23
23
  #warn;
24
+ #fileFirst;
24
25
  #warned = false;
25
26
  constructor(options) {
26
27
  this.#configDir = options.configDir;
@@ -31,21 +32,30 @@ export class Credentials {
31
32
  this.#fileName = options.fileName ?? "credentials.json";
32
33
  this.#warn = options.warn ?? ((message) => process.stderr.write(`${message}\n`));
33
34
  this.#service = keyringService(options.service, options.configDir, options.isolated);
35
+ this.#fileFirst = options.isolated === true && this.#keyring === systemKeyring;
34
36
  }
35
37
  read(account) {
36
38
  const fromEnv = this.#envVar ? this.#env[this.#envVar]?.trim() : undefined;
37
39
  if (fromEnv)
38
40
  return { secret: fromEnv, source: "environment" };
41
+ const fromFile = () => {
42
+ const secret = this.#readFile()?.[account]?.secret;
43
+ return secret ? { secret, source: "file" } : undefined;
44
+ };
45
+ if (this.#fileFirst) {
46
+ const found = fromFile();
47
+ if (found)
48
+ return found;
49
+ }
39
50
  if (this.#storage !== "file") {
40
51
  const fromKeyring = this.#tryKeyring(() => this.#keyring.get(this.#service, account));
41
52
  if (fromKeyring)
42
53
  return { secret: fromKeyring, source: "keyring" };
43
54
  }
44
- const fromFile = this.#readFile()?.[account]?.secret;
45
- return fromFile ? { secret: fromFile, source: "file" } : undefined;
55
+ return this.#fileFirst ? undefined : fromFile();
46
56
  }
47
57
  write(account, secret) {
48
- if (this.#storage !== "file") {
58
+ if (this.#storage !== "file" && !this.#fileFirst) {
49
59
  const stored = this.#tryKeyring(() => {
50
60
  this.#keyring.set(this.#service, account, secret);
51
61
  return true;
@@ -39,10 +39,11 @@ export declare const landChange: ({ file, branch, change: changeOf, done, shell,
39
39
  log: (text: string) => void;
40
40
  }) => string | undefined;
41
41
  /**
42
- * The newest `v<x.y.z>` tag in this commit's history and the changelog at it. Tags are fetched first: a clone made before the
43
- * last release does not have its tag, and comparing against an older one would hide the newest drift.
42
+ * The newest `v<x.y.z>` tag in this commit's history and the changelog at it, or `undefined` before the first release. Tags
43
+ * are fetched first: a clone made before the last release does not have its tag, and comparing against an older one would
44
+ * hide the newest drift.
44
45
  */
45
46
  export declare const newestTag: (shell: Shell, path: string) => {
46
47
  version: string;
47
48
  text: string;
48
- };
49
+ } | undefined;
package/dist/dev/land.js CHANGED
@@ -124,8 +124,9 @@ export const landChange = ({ file, branch, change: changeOf, done, shell, log, }
124
124
  throw new Error(`main moved under ${branch} twice — run bin/release again`);
125
125
  };
126
126
  /**
127
- * The newest `v<x.y.z>` tag in this commit's history and the changelog at it. Tags are fetched first: a clone made before the
128
- * last release does not have its tag, and comparing against an older one would hide the newest drift.
127
+ * The newest `v<x.y.z>` tag in this commit's history and the changelog at it, or `undefined` before the first release. Tags
128
+ * are fetched first: a clone made before the last release does not have its tag, and comparing against an older one would
129
+ * hide the newest drift.
129
130
  */
130
131
  export const newestTag = (shell, path) => {
131
132
  const fetched = shell.run("git", ["fetch", "-q", "--tags", "origin"]);
@@ -141,7 +142,7 @@ export const newestTag = (shell, path) => {
141
142
  return (y?.[0] ?? 0) - (x?.[0] ?? 0) || (y?.[1] ?? 0) - (x?.[1] ?? 0) || (y?.[2] ?? 0) - (x?.[2] ?? 0);
142
143
  });
143
144
  if (!version)
144
- throw new Error("no v<x.y.z> tag to compare the changelog with");
145
+ return undefined;
145
146
  const shown = shell.run("git", ["show", `v${version}:${path}`]);
146
147
  if (!shown.ok)
147
148
  throw new Error(`git show v${version}:${path} failed: ${shown.out}`);
package/dist/dev/main.js CHANGED
@@ -133,6 +133,10 @@ const changelog = (args, io) => {
133
133
  if (action === "restore") {
134
134
  const shell = processShell(io.cwd);
135
135
  const tag = newestTag(shell, relative(io.cwd, file));
136
+ if (!tag) {
137
+ io.err("no v<x.y.z> tag yet: a first release has nothing to restore");
138
+ return 0;
139
+ }
136
140
  if (!taggedSection(tag.text, tag.version)) {
137
141
  io.err(`v${tag.version}'s changelog has no section of its own; nothing to restore`);
138
142
  return 0;
@@ -64,7 +64,7 @@ export const PARITY_GROUPS = defineParityGroups([
64
64
  { name: "messengers", members: ["tg", "max"], covers: "tree", differences: [] },
65
65
  {
66
66
  name: "all",
67
- members: ["tg", "max", "memo", "drive", "zm", "wa"],
67
+ members: ["tg", "max", "memo", "drive", "zm", "wa", "mail"],
68
68
  covers: ["commands", "doctor", "skill", "mcp", "upgrade", "search", "notes", "meetings"],
69
69
  differences: [],
70
70
  },
@@ -39,7 +39,7 @@ the four steps that need a person. A new CLI is ready for its first release when
39
39
  | [Store](store.md) | `STORE` | 14 | Store areas, account scope, migrations, test isolation |
40
40
  | [Data model](data-model.md) | `DATA` | 9 | Core entities, entities and source records, `external_id`, field names, links and duplicates, files, calendar entries |
41
41
  | [Testing](testing.md) | `TEST` | 17 | Sandbox, no network, no real waits, leak tests, coverage, contracts, test matrix, parity |
42
- | [Security](security.md) | `SECR` | 22 | Secrets, logs, keyring, argv, error text, untrusted text, scanners, workflow hardening, reporting, OAuth sign-in, meeting secrets |
42
+ | [Security](security.md) | `SECR` | 23 | Secrets, logs, keyring, argv, error text, untrusted text, scanners, workflow hardening, reporting, OAuth sign-in, meeting secrets, keyring in moved-folder runs |
43
43
  | [Documents](docs.md) | `DOC` | 20 | User pages and developer pages, generated pages, current facts, changelog |
44
44
  | [Comparisons](comparisons.md) | `CMP` | 11 | Which tools a comparison page lists, rows as user tasks, agents and safety, where others are better, proof of marks |
45
45
  | [Releases](releases.md) | `REL` | 15 | `bin/release`, the release lock, versions, release reports, trusted publishing |
@@ -172,6 +172,7 @@ its prompt in cli-template says so from cli-template 0.8.0. A CLI on an older te
172
172
  | 02:27 | gcal-cli |
173
173
  | 02:31 | cli-memo |
174
174
  | 02:38 | cli-core |
175
+ | 02:41 | mail-cli |
175
176
  | 02:45 | drive-cli |
176
177
  | 02:52 | cli-testing |
177
178
  | 02:59 | max-cli |
@@ -131,7 +131,7 @@ bundle halved `--help` and every other short command
131
131
  - **Why:** one shared build keeps what tg learned in one place: lazy imports stay lazy, files and packages are found
132
132
  where they were installed, and a running server survives an in-place upgrade.
133
133
  - **Enforced by:** `<app>: pnpm build` · `<app>: pnpm smoke:node` · `<app>: pnpm smoke:bun`.
134
- - **Example:** `"build": "tsc --build --force && cli-dev bundle --bin dist/bin/tg.js --external '@mtcute/*' --external undici"`,
134
+ - **Example:** `"build": "tsc --build --force && cli-dev bundle --bin dist/bin/tg.js --external \"@mtcute/*\" --external undici"`,
135
135
  with `esbuild` as a devDependency in cli-base's peer range. `dist/bundle/` holds the bundle, split so every lazy
136
136
  `import()` stays a chunk of its own; `dist/bin/tg.js` stays the path `"bin"` names and becomes a loader. What it
137
137
  does and checks:
@@ -145,7 +145,8 @@ bundle halved `--help` and every other short command
145
145
  - A dynamic import of CommonJS (`yaml`) keeps its named exports. Names are kept (`keepNames`), so `commands --json`
146
146
  and errors read as before.
147
147
  - Native modules, Bun's SQLite and the packages cli-core loads by name stay outside; `--external` adds the CLI's
148
- own (`*` matches anything).
148
+ own (`*` matches anything). Quote a pattern with double quotes: pnpm runs scripts through `cmd.exe` on Windows,
149
+ which keeps single quotes, so `'@mtcute/*'` would match nothing there.
149
150
  - The build fails when a bundled module would look for a file or package from the chunk's place, or when the loader
150
151
  names a chunk that was not built. A third-party file read and found harmless is named with `--harmless`.
151
152
  - The CLI's smoke test runs the built bin under Node and Bun and checks that the store opens and the document
@@ -328,6 +328,23 @@ store and runs no search. A CLI declares its provider and may replace any step.
328
328
  - **Applies to:** cli-core and every CLI that stores or shows meetings or calendar entries. **Since:** cli-base 0.57.1
329
329
  (output and agent answers: cli-base 0.65.0). **Rulings:** none (Wave 3 requirements 3.4).
330
330
 
331
+ ### WC-SECR-24 · A run with moved folders never writes the owner's system keyring
332
+
333
+ - **Why:** with its folders moved (`<PREFIX>_CONFIG_DIR`, `<PREFIX>_STATE_DIR`, a throwaway `HOME` or `XDG_*`),
334
+ `Credentials` keys the keyring by `<app>:<config folder>` (`keyringService`, `isolated`). Each test run, agent check
335
+ or worktree that signs in then leaves its own copy of a sign-in in the owner's keyring, and nothing removes it.
336
+ - **Enforced by:** `src/credentials.test.ts`.
337
+ - **Example:** a test passes `memoryKeyring()`. An agent checking a built CLI signs in with file storage inside its
338
+ temp folder (`session start --credential-storage file`), or does not sign in. A run against the owner's real data
339
+ keeps the default folders, so it reads the owner's existing entry (`<app>` / `oauth:<provider>:<profile>`) instead
340
+ of making a new one. A throwaway `HOME` does not hide the keyring, which is reached over D-Bus: a run that must not
341
+ see it also runs with `DBUS_SESSION_BUS_ADDRESS` removed. With `isolated` and the system keyring, `Credentials`
342
+ saves new secrets to the private file in the moved config folder and only reads the keyring, for a secret saved
343
+ there before.
344
+ - **Exceptions:** none.
345
+ - **Applies to:** every CLI, its tests and every agent that runs a CLI. **Since:** the next cli-base release. **Rulings:** owner,
346
+ 2026-10-29 (sign-ins copied per work folder should have reused the existing entry).
347
+
331
348
  ## Sources
332
349
 
333
350
  These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wirecat/cli-base",
3
- "version": "0.65.0",
3
+ "version": "0.65.1",
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",