@wirecat/cli-base 0.67.0 → 0.68.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 +32 -0
- package/dist/credentials.d.ts +11 -3
- package/dist/credentials.js +27 -15
- package/dist/index.d.ts +1 -1
- package/dist/standards/flags-how.js +7 -0
- package/docs/standards/README.md +1 -1
- package/docs/standards/releases.md +11 -12
- package/docs/standards/security.md +19 -7
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,38 @@ 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.68.0 — 31.10.2026
|
|
12
|
+
|
|
13
|
+
### Removed
|
|
14
|
+
|
|
15
|
+
- **WC-REL-10: a messenger CLI is released after the owner's yes in the conversation, with no signed report.**
|
|
16
|
+
`bin/release` in max-cli and whatsapp-cli no longer reads a release report or a `Signed off:` line. The agent asks
|
|
17
|
+
the owner before it runs `bin/release`; one yes covers one version.
|
|
18
|
+
|
|
19
|
+
### Changed — may break callers
|
|
20
|
+
|
|
21
|
+
- **`Credentials` in `auto` does the whole file fallback itself (WC-SECR-4).** A secret the file holds is read from the
|
|
22
|
+
file before the keyring, so a locked keyring is not tried for it, and a refresh `write` keeps it there. A sign-in
|
|
23
|
+
passes `write(account, secret, { signIn: true })`: it tries the keyring and, once the keyring takes it, removes the
|
|
24
|
+
file copy. The "could not reach the OS keyring" warning comes once per run (per `env` object), not once per
|
|
25
|
+
`Credentials`. Watch for: a file copy left next to a newer keyring entry by an older CLI now wins the read; sign in
|
|
26
|
+
again to move it back. CLIs can drop their own file-first read, "remove the file copy" step and `warnOnce`.
|
|
27
|
+
- **WC-SECR-4 is the one credential-storage standard.** The keyring by default; when it fails, the file without
|
|
28
|
+
asking, one warning on stderr, and the answer that saved the secret says where it went;
|
|
29
|
+
`--credential-storage file` on the sign-in command chooses the file and is discouraged; a secret in the file is
|
|
30
|
+
read from there until a sign-in reaches the keyring again. No CLI asks, none is keyring-only. `Credentials` already
|
|
31
|
+
falls back this way in `auto`; the rule now says every CLI must use it.
|
|
32
|
+
|
|
33
|
+
### Fixed
|
|
34
|
+
|
|
35
|
+
- **`Credentials.remove` with `storage: "keyring"` leaves the file alone.** It also deleted the account from the
|
|
36
|
+
credentials file, so a CLI that cleared the keyring copy after a file sign-in lost the sign-in it had just saved.
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **`--credential-storage <storage>` in the flag vocabulary** (role `mode`), so `cli-dev lint` accepts it on a sign-in
|
|
41
|
+
command without a per-CLI override.
|
|
42
|
+
|
|
11
43
|
## 0.67.0 — 31.10.2026
|
|
12
44
|
|
|
13
45
|
### Added
|
package/dist/credentials.d.ts
CHANGED
|
@@ -38,17 +38,25 @@ export declare const keyringService: (service: string, _configDir?: string, _iso
|
|
|
38
38
|
* shape `bin/keyring-dedupe` already finds and removes.
|
|
39
39
|
*/
|
|
40
40
|
export declare const movedRunService: (service: string, configDir: string) => string;
|
|
41
|
+
export interface WriteOptions {
|
|
42
|
+
/**
|
|
43
|
+
* A new sign-in rather than a refresh: it tries the keyring even when the file holds the account, and once the
|
|
44
|
+
* keyring takes it, removes the file copy (WC-SECR-4).
|
|
45
|
+
*/
|
|
46
|
+
signIn?: boolean;
|
|
47
|
+
}
|
|
41
48
|
/**
|
|
42
|
-
* Environment first, then the OS keyring, then a file.
|
|
49
|
+
* Environment first, then the OS keyring, then a file. In `auto`, a file that holds the account is read before the
|
|
50
|
+
* keyring and keeps its refreshes, so a locked keyring is not tried again for it; a sign-in moves it back.
|
|
43
51
|
*
|
|
44
52
|
* `auto` does not probe whether a keyring exists: it attempts the operation, and on failure warns
|
|
45
|
-
* once and falls through. A probe would touch the user's keychain for nothing and could still
|
|
53
|
+
* once per run and falls through. A probe would touch the user's keychain for nothing and could still
|
|
46
54
|
* succeed where the real operation fails.
|
|
47
55
|
*/
|
|
48
56
|
export declare class Credentials {
|
|
49
57
|
#private;
|
|
50
58
|
constructor(options: CredentialsOptions);
|
|
51
59
|
read(account: string): StoredCredential | undefined;
|
|
52
|
-
write(account: string, secret: string): CredentialSource;
|
|
60
|
+
write(account: string, secret: string, options?: WriteOptions): CredentialSource;
|
|
53
61
|
remove(account: string): CredentialSource[];
|
|
54
62
|
}
|
package/dist/credentials.js
CHANGED
|
@@ -15,11 +15,14 @@ export const keyringService = (service, _configDir, _isolated) => service;
|
|
|
15
15
|
* shape `bin/keyring-dedupe` already finds and removes.
|
|
16
16
|
*/
|
|
17
17
|
export const movedRunService = (service, configDir) => `${service}:${resolve(configDir)}`;
|
|
18
|
+
// One warning per run, not per instance: a run over several profiles builds one Credentials for each.
|
|
19
|
+
const warnedRuns = new WeakSet();
|
|
18
20
|
/**
|
|
19
|
-
* Environment first, then the OS keyring, then a file.
|
|
21
|
+
* Environment first, then the OS keyring, then a file. In `auto`, a file that holds the account is read before the
|
|
22
|
+
* keyring and keeps its refreshes, so a locked keyring is not tried again for it; a sign-in moves it back.
|
|
20
23
|
*
|
|
21
24
|
* `auto` does not probe whether a keyring exists: it attempts the operation, and on failure warns
|
|
22
|
-
* once and falls through. A probe would touch the user's keychain for nothing and could still
|
|
25
|
+
* once per run and falls through. A probe would touch the user's keychain for nothing and could still
|
|
23
26
|
* succeed where the real operation fails.
|
|
24
27
|
*/
|
|
25
28
|
export class Credentials {
|
|
@@ -33,7 +36,6 @@ export class Credentials {
|
|
|
33
36
|
#warn;
|
|
34
37
|
/** Set only for a moved run with the system keyring. */
|
|
35
38
|
#temporaryService;
|
|
36
|
-
#warned = false;
|
|
37
39
|
constructor(options) {
|
|
38
40
|
this.#configDir = options.configDir;
|
|
39
41
|
this.#storage = options.storage ?? "auto";
|
|
@@ -50,23 +52,29 @@ export class Credentials {
|
|
|
50
52
|
const fromEnv = this.#envVar ? this.#env[this.#envVar]?.trim() : undefined;
|
|
51
53
|
if (fromEnv)
|
|
52
54
|
return { secret: fromEnv, source: "environment" };
|
|
55
|
+
const fromFile = this.#storage === "keyring" ? undefined : this.#readFile()?.[account]?.secret;
|
|
56
|
+
if (fromFile)
|
|
57
|
+
return { secret: fromFile, source: "file" };
|
|
53
58
|
if (this.#storage !== "file") {
|
|
54
59
|
const temporary = this.#temporaryService;
|
|
55
60
|
const fromKeyring = this.#tryKeyring(() => (temporary && this.#keyring.get(temporary, account)) || this.#keyring.get(this.#service, account));
|
|
56
61
|
if (fromKeyring)
|
|
57
62
|
return { secret: fromKeyring, source: "keyring" };
|
|
58
63
|
}
|
|
59
|
-
|
|
60
|
-
return secret ? { secret, source: "file" } : undefined;
|
|
64
|
+
return undefined;
|
|
61
65
|
}
|
|
62
|
-
write(account, secret) {
|
|
63
|
-
|
|
66
|
+
write(account, secret, options = {}) {
|
|
67
|
+
const inFile = this.#storage === "auto" && this.#readFile()?.[account] !== undefined;
|
|
68
|
+
if (this.#storage === "keyring" || (this.#storage === "auto" && (options.signIn || !inFile))) {
|
|
64
69
|
const stored = this.#tryKeyring(() => {
|
|
65
70
|
this.#keyring.set(this.#temporaryService ?? this.#service, account, secret);
|
|
66
71
|
return true;
|
|
67
72
|
});
|
|
68
|
-
if (stored)
|
|
73
|
+
if (stored) {
|
|
74
|
+
if (inFile)
|
|
75
|
+
this.#removeFromFile(account);
|
|
69
76
|
return "keyring";
|
|
77
|
+
}
|
|
70
78
|
}
|
|
71
79
|
const store = this.#readFile();
|
|
72
80
|
if (!store) {
|
|
@@ -91,14 +99,18 @@ export class Credentials {
|
|
|
91
99
|
"moved folders never removes it");
|
|
92
100
|
}
|
|
93
101
|
}
|
|
94
|
-
|
|
95
|
-
if (store?.[account] !== undefined) {
|
|
96
|
-
delete store[account];
|
|
97
|
-
this.#writeFile(store);
|
|
102
|
+
if (this.#storage !== "keyring" && this.#removeFromFile(account))
|
|
98
103
|
removed.push("file");
|
|
99
|
-
}
|
|
100
104
|
return removed;
|
|
101
105
|
}
|
|
106
|
+
#removeFromFile(account) {
|
|
107
|
+
const store = this.#readFile();
|
|
108
|
+
if (store?.[account] === undefined)
|
|
109
|
+
return false;
|
|
110
|
+
delete store[account];
|
|
111
|
+
this.#writeFile(store);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
102
114
|
/** Only for the warning: a keyring that fails here must not fail a removal that already happened. */
|
|
103
115
|
#ownersEntryExists(account) {
|
|
104
116
|
try {
|
|
@@ -141,8 +153,8 @@ export class Credentials {
|
|
|
141
153
|
return operation();
|
|
142
154
|
}
|
|
143
155
|
catch (error) {
|
|
144
|
-
if (!this.#
|
|
145
|
-
this.#
|
|
156
|
+
if (!warnedRuns.has(this.#env)) {
|
|
157
|
+
warnedRuns.add(this.#env);
|
|
146
158
|
this.#warn(`could not reach the OS keyring — it may be locked, or no keyring service is running ` +
|
|
147
159
|
`(${error instanceof Error ? error.message : String(error)}); ` +
|
|
148
160
|
`using ${this.#path()} instead, readable only by you`);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { BATCH_LIMITS, type BatchContext, type BatchHandler, type BatchOperation, type BatchReport, type BatchResult, type PreparedBatchRequest, prepareBatch, projectBatchAnswers, readBatchInput, runBatch, } from "./batch.js";
|
|
2
2
|
export { loadConfigFile, saveConfigFile, writeSecurely } from "./config.js";
|
|
3
|
-
export type { CredentialSource, CredentialStorage, CredentialsOptions, StoredCredential } from "./credentials.js";
|
|
3
|
+
export type { CredentialSource, CredentialStorage, CredentialsOptions, StoredCredential, WriteOptions, } from "./credentials.js";
|
|
4
4
|
export { CREDENTIAL_STORAGE, Credentials, keyringService, movedRunService } from "./credentials.js";
|
|
5
5
|
export { createDeadline, type Deadline, type DeadlineOptions } from "./deadline.js";
|
|
6
6
|
export { CliError, type CliErrorDetails, type ErrorCode, errorCodes, isCliError } from "./errors.js";
|
|
@@ -375,6 +375,13 @@ export const HOW_FLAGS = [
|
|
|
375
375
|
type: "string",
|
|
376
376
|
meaning: "the browser program to open for sign-in",
|
|
377
377
|
},
|
|
378
|
+
{
|
|
379
|
+
name: "--credential-storage",
|
|
380
|
+
value: "<storage>",
|
|
381
|
+
role: "mode",
|
|
382
|
+
type: "choice",
|
|
383
|
+
meaning: "where a sign-in is kept: keyring, which falls back to a private file by itself, or file (WC-SECR-4)",
|
|
384
|
+
},
|
|
378
385
|
{ name: "--checkpoint", value: "<file>", role: "mode", type: "path", meaning: "keep resume progress in this file" },
|
|
379
386
|
{
|
|
380
387
|
name: "--expected-event-id",
|
package/docs/standards/README.md
CHANGED
|
@@ -42,7 +42,7 @@ the four steps that need a person. A new CLI is ready for its first release when
|
|
|
42
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
|
-
| [Releases](releases.md) | `REL` | 15 | `bin/release`, the release lock, versions,
|
|
45
|
+
| [Releases](releases.md) | `REL` | 15 | `bin/release`, the release lock, versions, the owner's yes, trusted publishing |
|
|
46
46
|
| [CI](ci.md) | `CI` | 12 | Hooks, the pull request budget, full validation, monthly runs |
|
|
47
47
|
| [Time](time.md) | `TIME` | 6 | UTC in the store and in answers, the zone at input and display edges, date-only filters, tests in another zone |
|
|
48
48
|
| [Style](style.md) | `STYLE` | 10 | Comments, TypeScript habits, commit messages |
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
## Scope
|
|
6
6
|
|
|
7
|
-
How a CLI is released: `bin/release`, the release lock, version numbers, the
|
|
7
|
+
How a CLI is released: `bin/release`, the release lock, version numbers, the owner's yes and trusted publishing.
|
|
8
8
|
|
|
9
9
|
Rule ids on this page are `WC-REL-<n>`. How to read a rule, how ids work and which rule wins are in the
|
|
10
10
|
[index](README.md). The changelog a release dates is on the [Documents](docs.md) page. The checks a release runs are
|
|
@@ -106,17 +106,16 @@ on the [CI](ci.md) page.
|
|
|
106
106
|
|
|
107
107
|
### WC-REL-9 · Merged into [WC-REL-8](#wc-rel-8--any-package-may-release-at-any-time)
|
|
108
108
|
|
|
109
|
-
### WC-REL-10 · A messenger CLI is released only
|
|
110
|
-
|
|
111
|
-
- **Why:**
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
- **
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
- **Exceptions:** none.
|
|
118
|
-
|
|
119
|
-
- **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
109
|
+
### WC-REL-10 · A messenger CLI is released only after the owner says yes in the conversation
|
|
110
|
+
|
|
111
|
+
- **Why:** a messenger CLI acts on the owner's own accounts, so the owner decides when a new version ships. A signed
|
|
112
|
+
release report added a step without adding a check: the owner removed it on 2026-10-31 ("remove the signing
|
|
113
|
+
requirements instead just ask").
|
|
114
|
+
- **Enforced by:** review only.
|
|
115
|
+
- **Example:** the agent finishes the release skill, then asks "release max 0.91.0?". It runs `bin/release` only after
|
|
116
|
+
the owner says yes for that version. One yes covers one version.
|
|
117
|
+
- **Exceptions:** none.
|
|
118
|
+
- **Applies to:** messenger CLIs. **Since:** cli-base 0.41.0. **Rulings:** owner, 2026-10-31.
|
|
120
119
|
|
|
121
120
|
### WC-REL-11 · Publish from GitHub Actions with npm trusted publishing
|
|
122
121
|
|
|
@@ -67,19 +67,31 @@ name. Any other top-level key is written as `[not allowed]`. A key that names co
|
|
|
67
67
|
### WC-SECR-4 · Secrets come from the environment, then the OS keyring, then a private file
|
|
68
68
|
|
|
69
69
|
cli-base's `Credentials` reads the environment variable first (for CI), then the OS keyring, then a file with mode
|
|
70
|
-
`0600
|
|
70
|
+
`0600` in the CLI's own folder. Storage is the keyring by default (`auto`): when the keyring fails (locked, not
|
|
71
|
+
running, no D-Bus), the CLI uses the file without asking, warns once on stderr, and the answer that saved the secret
|
|
72
|
+
says where it went (gcal: `credential_storage: "file"`). `--credential-storage file` on the sign-in command chooses
|
|
73
|
+
the file outright; help and docs discourage it. Once the file holds a secret, it is read from the file, so a locked
|
|
74
|
+
keyring is not tried again for it; the next sign-in that reaches the keyring moves it back and removes the file copy.
|
|
75
|
+
`Credentials` in `auto` does all of this: the CLI passes `{ signIn: true }` to `write` for a new sign-in and nothing
|
|
76
|
+
for a refresh.
|
|
77
|
+
No CLI asks which store to use, and none is keyring-only.
|
|
78
|
+
The keyring service is the CLI's app name, `appName` in its identity (`max-cli`, `gcal-cli`, `cli-memo`), not
|
|
71
79
|
the command word people type (`max`, `gcal`). A run with moved folders reads the same entry, writes a temporary one,
|
|
72
80
|
and never replaces or deletes the owner's
|
|
73
81
|
([WC-SECR-24](#wc-secr-24--a-run-with-moved-folders-shares-the-owners-keyring-entry-and-never-deletes-it)).
|
|
74
82
|
|
|
75
|
-
- **Why:** the keyring is the safest store on the computer
|
|
83
|
+
- **Why:** the keyring is the safest store on the computer, and a sign-in that fails on a server or in a container
|
|
84
|
+
because no keyring runs there is a broken CLI, not a safe one.
|
|
76
85
|
- **Enforced by:** `src/credentials.test.ts`.
|
|
77
86
|
- **Example:** a bot token is read from `MAX_BOT_TOKEN`, then the keyring entry `bot:<profile>`, then the file.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
-
|
|
87
|
+
`gcal personal session start` on a computer without a keyring signs in, warns once, and answers
|
|
88
|
+
`credential_storage: "file"`.
|
|
89
|
+
- **Exceptions:** a messenger library's own device store, which the library opens as a file and which cannot live in
|
|
90
|
+
the keyring (WhatsApp's `private.db`, tg's session file), is an owner-only file under
|
|
91
|
+
[WC-SECR-10](#wc-secr-10--private-files-are-written-atomically-with-private-modes) instead. A copy between two
|
|
92
|
+
profiles that must land in the keyring (cli-core's bot transfer) may use `storage: "keyring"`.
|
|
93
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** NEED-295; owner, 2026-10-31 (never ask, keyring
|
|
94
|
+
by default, fall back to a file on error, a discouraged flag for the file).
|
|
83
95
|
|
|
84
96
|
### WC-SECR-5 · A secret is never taken on the command line
|
|
85
97
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-base",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.68.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",
|