@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 +12 -0
- package/dist/credentials.d.ts +3 -0
- package/dist/credentials.js +13 -3
- package/dist/dev/land.d.ts +4 -3
- package/dist/dev/land.js +4 -3
- package/dist/dev/main.js +4 -0
- package/dist/standards/audit.js +1 -1
- package/docs/standards/README.md +1 -1
- package/docs/standards/ci.md +1 -0
- package/docs/standards/layout.md +3 -2
- package/docs/standards/security.md +17 -0
- package/package.json +1 -1
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
|
package/dist/credentials.d.ts
CHANGED
|
@@ -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. */
|
package/dist/credentials.js
CHANGED
|
@@ -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
|
-
|
|
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;
|
package/dist/dev/land.d.ts
CHANGED
|
@@ -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
|
|
43
|
-
* last release does not have its tag, and comparing against an older one would
|
|
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
|
|
128
|
-
* last release does not have its tag, and comparing against an older one would
|
|
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
|
-
|
|
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;
|
package/dist/standards/audit.js
CHANGED
|
@@ -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
|
},
|
package/docs/standards/README.md
CHANGED
|
@@ -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` |
|
|
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 |
|
package/docs/standards/ci.md
CHANGED
|
@@ -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 |
|
package/docs/standards/layout.md
CHANGED
|
@@ -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
|
|
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.
|
|
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",
|