@wirecat/cli-base 0.75.1 → 0.76.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 +73 -0
- package/constants.json +1 -0
- package/dist/constants.d.ts +12 -0
- package/dist/constants.js +63 -0
- package/dist/dev/main.js +43 -25
- package/dist/dev/wcat-lint.d.ts +3 -1
- package/dist/dev/wcat-lint.js +2 -0
- package/dist/errors.d.ts +4 -1
- package/dist/errors.js +4 -0
- package/dist/exit-codes.js +1 -0
- package/dist/failure.d.ts +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/query/parser.js +11 -0
- package/dist/query/registry.d.ts +1 -1
- package/dist/release/markdown.js +5 -0
- package/dist/standards/errors.d.ts +2 -2
- package/dist/standards/errors.js +6 -0
- package/dist/standards/lint.d.ts +9 -0
- package/dist/standards/lint.js +5 -3
- package/dist/standards/output.d.ts +5 -5
- package/dist/testing/sandbox.d.ts +2 -0
- package/dist/testing/sandbox.js +14 -1
- package/dist/testing/setup.js +7 -2
- package/docs/standards/README.md +3 -3
- package/docs/standards/architecture.md +15 -1
- package/docs/standards/commands.md +47 -3
- package/docs/standards/docs.md +17 -1
- package/docs/standards/lifecycle.md +6 -5
- package/docs/standards/mcp.md +3 -2
- package/docs/standards/output.md +4 -2
- package/docs/standards/permissions.md +3 -4
- package/docs/standards/releases.md +1 -1
- package/docs/standards/time.md +2 -4
- package/docs/standards/toolchain.md +5 -3
- package/package.json +2 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,79 @@ 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.76.0 — 04.11.2026
|
|
12
|
+
|
|
13
|
+
### Changed — may break callers
|
|
14
|
+
|
|
15
|
+
- **A new error code, `conflict`, exit 15: a write refused because what it would change moved on since the caller
|
|
16
|
+
read it** (a stale `--revision`). Until now such a refusal had to borrow `configuration_error` (exit 3), which says
|
|
17
|
+
the setup is wrong and leaves an agent to read `details.reason` to tell them apart. `errorCodes` now has 15 codes,
|
|
18
|
+
`EXIT_CODES.conflict` is 15, the fallback reason is `write.conflict`, and `retry.safe` is always false: read the
|
|
19
|
+
record again and decide on the new copy. A CLI keeps its own reason (`task.stale`) and details. What to watch for:
|
|
20
|
+
anything that lists the codes itself, or maps every code or exit number, must add this one. Every CLI's
|
|
21
|
+
`contracts/mcp-tools.json` names the codes and is regenerated at its bump; braze-cli keeps its own copy of the list;
|
|
22
|
+
the desktop app branches on codes for sync failures. cli-core moves `task.stale` (and later the other `*.stale`
|
|
23
|
+
reasons) to this code after a release.
|
|
24
|
+
|
|
25
|
+
### Added
|
|
26
|
+
|
|
27
|
+
- **`loadConstants(layers, schema)`: one way to ship values inside the packages.** Each layer (cli-base, cli-core,
|
|
28
|
+
the tool) keeps a `constants.json` at its package root; `loadConstants([BASE_CONSTANTS, core, tool], schema)` merges
|
|
29
|
+
them in that order (objects key by key, a later layer winning; arrays and scalars replaced), treats a missing file as
|
|
30
|
+
empty, validates against the caller's schema built on `baseConstantsSchema`, names the dotted path and file of a bad
|
|
31
|
+
value, and freezes the result. cli-base's own layer is `{}`. Rule
|
|
32
|
+
[WC-ARCH-23](docs/standards/architecture.md#wc-arch-23--a-value-that-ships-with-the-package-lives-in-the-layers-constantsjson):
|
|
33
|
+
such a value is never a scattered constant or a file CI writes from a secret. cli-core builds the OAuth apps on it.
|
|
34
|
+
- **A query value takes a ref unquoted: `about:project:1` and `about:chat:max/1/2` are one value.** Every WireCat
|
|
35
|
+
CLI prints refs this way, and a list refused them unless the caller quoted them. A field value made of a lower-case
|
|
36
|
+
word, a colon and more text with no space now reads as one term, so `url:https://example.com` works unquoted too.
|
|
37
|
+
`about:project` alone and `project:1` as a field and value read as before. A CLI gets it by updating cli-base.
|
|
38
|
+
- **Rule WC-CMD-31: accept every ref any WireCat CLI prints, and never ask the caller to translate it**
|
|
39
|
+
([WC-CMD-31](docs/standards/commands.md#wc-cmd-31--accept-every-ref-any-wirecat-cli-prints-and-never-ask-the-caller-to-translate-it)).
|
|
40
|
+
Arguments, options and query values take a stored thing's ref as printed; a ref for another account or provider
|
|
41
|
+
fails with an error that names the right one.
|
|
42
|
+
- **Rule WC-CMD-30: every setting is in the config file with its default, and `config show` lists them all**
|
|
43
|
+
([WC-CMD-30](docs/standards/commands.md#wc-cmd-30--write-every-setting-into-the-config-file-with-its-default-and-list-them-all-in-config-show)).
|
|
44
|
+
A CLI declares each scalar setting with its default and a description through cli-core's `DeclaredSetting`. It
|
|
45
|
+
fills the missing ones whenever it writes the file anyway, and on `config migrate`. A plain read never rewrites the
|
|
46
|
+
file. The owner then finds and edits every setting in the file without the docs. memo follows it first. The known
|
|
47
|
+
gaps now name what cli-core's shared settings cover (tg, max, wa, mail, gcal after their cli-core bump) and what is
|
|
48
|
+
left: those CLIs' own settings, drive's own `config` (no `migrate`, writes do not fill) and zm.
|
|
49
|
+
|
|
50
|
+
## 0.75.2 — 03.11.2026
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- **Rule WC-CMD-30: every setting is in the config file with its default, and `config show` lists them all**
|
|
55
|
+
([WC-CMD-30](docs/standards/commands.md#wc-cmd-30--write-every-setting-into-the-config-file-with-its-default-and-list-them-all-in-config-show)).
|
|
56
|
+
A CLI declares each scalar setting with its default and a description through cli-core's `DeclaredSetting`. It
|
|
57
|
+
fills the missing ones whenever it writes the file anyway, and on `config migrate`. A plain read never rewrites the
|
|
58
|
+
file. The owner then finds and edits every setting in the file without the docs. memo follows it first; the
|
|
59
|
+
messenger CLIs and zm are listed as known gaps.
|
|
60
|
+
|
|
61
|
+
### Fixed
|
|
62
|
+
|
|
63
|
+
- **A test that reaches the real keyring now fails, even when it falls back to a file.** The shared test setup's
|
|
64
|
+
keyring stand-in threw, but `Credentials` in `auto` mode catches that, warns and saves to a file, so the test
|
|
65
|
+
passed with only a warning on the console. The stand-in now records the reach, and the setup fails the test after it
|
|
66
|
+
with the stack of the call. Pass `memoryKeyring()` to fix such a test. mail-cli's and drive-cli's suites pass
|
|
67
|
+
under it.
|
|
68
|
+
- **One bad entry in `lint/overrides.json` no longer turns off all the others.** `cli-dev lint` and `cli-dev audit`
|
|
69
|
+
dropped the whole file when one entry did not fit its shape, so every violation the good entries covered came back
|
|
70
|
+
as new, and the cause was one line among them. Now the bad entry fails the run as `entry <n> (<rule>) does not apply
|
|
71
|
+
— <why>`, and the other entries still apply.
|
|
72
|
+
- **The `wcat-bin-name` and `wcat-describe` warnings can be overridden.** An entry for them was refused as "no lint
|
|
73
|
+
rule" and the warning stayed. Now an override with its reason silences the warning, counts against the budget, and
|
|
74
|
+
fails as stale once the warning is gone. A CLI that keeps its short command name says why this way.
|
|
75
|
+
- **`docs:check` refuses an entry under no `###` heading in the top changelog section.** It passed one, so a release
|
|
76
|
+
could ship notes with an entry outside the five headings
|
|
77
|
+
([the changelog rule](docs/standards/docs.md#wc-doc-15--changelog-entries-go-under-five-fixed-headings-each-at-most-once-per-section)).
|
|
78
|
+
Older sections are not checked: they are frozen, and several CLIs have such entries in them.
|
|
79
|
+
- **The handbook's lines about gcal match gcal again:** `store status` (not `status`), a calendar sync's 15-minute
|
|
80
|
+
terminal deadline, all-day events matched by local date, `mcp serve --profiles` built, gcal's own Node 22 job and
|
|
81
|
+
Windows/macOS runs, its fixed package list, its generated `api` among the generated-command exceptions, and its
|
|
82
|
+
`api` writes no longer listed as all `ask`.
|
|
83
|
+
|
|
11
84
|
## 0.75.1 — 02.11.2026
|
|
12
85
|
|
|
13
86
|
### Fixed
|
package/constants.json
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import * as v from "valibot";
|
|
2
|
+
/** cli-base's own layer, the first one every program passes to `loadConstants`. */
|
|
3
|
+
export declare const BASE_CONSTANTS: import("url").URL;
|
|
4
|
+
/** What cli-base's layer holds; a layer above extends it with `v.object({ ...baseConstantsSchema.entries, … })`. */
|
|
5
|
+
export declare const baseConstantsSchema: v.ObjectSchema<{}, undefined>;
|
|
6
|
+
/**
|
|
7
|
+
* Values that ship inside the packages — never the owner's settings. Each layer is a JSON file, base first; a file
|
|
8
|
+
* that does not exist is an empty layer, so a tool without constants of its own passes its URL anyway. Plain objects
|
|
9
|
+
* merge key by key and a later layer wins; an array or a scalar replaces what was below it. The result is checked
|
|
10
|
+
* against the caller's schema and frozen.
|
|
11
|
+
*/
|
|
12
|
+
export declare const loadConstants: <TSchema extends v.GenericSchema>(layers: readonly URL[], schema: TSchema) => v.InferOutput<TSchema>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { fileURLToPath } from "node:url";
|
|
2
|
+
import * as v from "valibot";
|
|
3
|
+
import { loadConfigFile } from "./config.js";
|
|
4
|
+
/** cli-base's own layer, the first one every program passes to `loadConstants`. */
|
|
5
|
+
export const BASE_CONSTANTS = new URL("../constants.json", import.meta.url);
|
|
6
|
+
/** What cli-base's layer holds; a layer above extends it with `v.object({ ...baseConstantsSchema.entries, … })`. */
|
|
7
|
+
export const baseConstantsSchema = v.object({});
|
|
8
|
+
const LayerSchema = v.record(v.string(), v.unknown());
|
|
9
|
+
const isPlainObject = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
10
|
+
const merge = (into, from, file, setBy, prefix = "") => {
|
|
11
|
+
const out = { ...into };
|
|
12
|
+
for (const [key, value] of Object.entries(from)) {
|
|
13
|
+
const path = prefix ? `${prefix}.${key}` : key;
|
|
14
|
+
const current = out[key];
|
|
15
|
+
if (isPlainObject(current) && isPlainObject(value)) {
|
|
16
|
+
out[key] = merge(current, value, file, setBy, path);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
out[key] = value;
|
|
20
|
+
setBy.set(path, file);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return out;
|
|
24
|
+
};
|
|
25
|
+
const deepFreeze = (value) => {
|
|
26
|
+
if (typeof value === "object" && value !== null && !Object.isFrozen(value)) {
|
|
27
|
+
for (const child of Object.values(value))
|
|
28
|
+
deepFreeze(child);
|
|
29
|
+
Object.freeze(value);
|
|
30
|
+
}
|
|
31
|
+
return value;
|
|
32
|
+
};
|
|
33
|
+
const fileOf = (path, setBy, files) => {
|
|
34
|
+
for (let at = path.split("."); at.length > 0; at = at.slice(0, -1)) {
|
|
35
|
+
const file = setBy.get(at.join("."));
|
|
36
|
+
if (file)
|
|
37
|
+
return `set in ${file}`;
|
|
38
|
+
}
|
|
39
|
+
return `not set in ${files.join(", ")}`;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Values that ship inside the packages — never the owner's settings. Each layer is a JSON file, base first; a file
|
|
43
|
+
* that does not exist is an empty layer, so a tool without constants of its own passes its URL anyway. Plain objects
|
|
44
|
+
* merge key by key and a later layer wins; an array or a scalar replaces what was below it. The result is checked
|
|
45
|
+
* against the caller's schema and frozen.
|
|
46
|
+
*/
|
|
47
|
+
export const loadConstants = (layers, schema) => {
|
|
48
|
+
const files = layers.map((layer) => fileURLToPath(layer));
|
|
49
|
+
const setBy = new Map();
|
|
50
|
+
let merged = {};
|
|
51
|
+
for (const file of files) {
|
|
52
|
+
merged = merge(merged, loadConfigFile(file, LayerSchema, () => ({})), file, setBy);
|
|
53
|
+
}
|
|
54
|
+
const result = v.safeParse(schema, merged);
|
|
55
|
+
if (!result.success) {
|
|
56
|
+
const problems = result.issues.map((issue) => {
|
|
57
|
+
const path = v.getDotPath(issue) ?? "(root)";
|
|
58
|
+
return `${path}: ${issue.message} (${fileOf(path, setBy, files)})`;
|
|
59
|
+
});
|
|
60
|
+
throw new Error(`the packaged constants are not valid:\n ${problems.join("\n ")}`);
|
|
61
|
+
}
|
|
62
|
+
return deepFreeze(result.output);
|
|
63
|
+
};
|
package/dist/dev/main.js
CHANGED
|
@@ -15,7 +15,7 @@ import { taggedSection } from "../release/fold.js";
|
|
|
15
15
|
import { changelogProblems, docsProblems, JOURNAL_IDS, markdownFiles, packageVersion, structureProblems, versionScript, } from "../release/index.js";
|
|
16
16
|
import { AUDIT_BLOCKS, defineParityGroups, PARITY_GROUPS, REQUIRED_ROOTS } from "../standards/audit.js";
|
|
17
17
|
import { defineReasons } from "../standards/errors.js";
|
|
18
|
-
import { baselineProblems, baselineSchema, LINT_RULES, OVERRIDE_BUDGET, OVERRIDE_RULE_REVIEW,
|
|
18
|
+
import { baselineProblems, baselineSchema, LINT_RULES, OVERRIDE_BUDGET, OVERRIDE_RULE_REVIEW, overrideSchema, overridesProblems, rulesForReview, } from "../standards/lint.js";
|
|
19
19
|
import { issuesOf, RegistryError } from "../standards/registry.js";
|
|
20
20
|
import { VOCABULARY } from "../standards/vocabulary.js";
|
|
21
21
|
import { auditClis, scorecardTable, snakeKeys } from "./audit.js";
|
|
@@ -25,7 +25,7 @@ import { baselineOf, judge, lintCommands, RUN_LOG_COMMANDS, walk } from "./lint.
|
|
|
25
25
|
import { nextVersion } from "./next-version.js";
|
|
26
26
|
import { slowTests } from "./slow-tests.js";
|
|
27
27
|
import { matrixPage, matrixRows, parseArgvLog } from "./test-matrix.js";
|
|
28
|
-
import { binNameWarnings, describeWarnings } from "./wcat-lint.js";
|
|
28
|
+
import { binNameWarnings, describeWarnings, WCAT_RULES } from "./wcat-lint.js";
|
|
29
29
|
const USAGE = `usage: cli-dev <command> [options]
|
|
30
30
|
|
|
31
31
|
slow-tests [report] [count] the slowest tests and files (report: coverage/tests.json, count: 20)
|
|
@@ -314,6 +314,30 @@ const readBaseline = (dir, label, problems) => {
|
|
|
314
314
|
}
|
|
315
315
|
return baseline;
|
|
316
316
|
};
|
|
317
|
+
/**
|
|
318
|
+
* Each entry on its own: one bad entry is named and ignored, and the others still apply. Dropping the whole file hid
|
|
319
|
+
* the bad entry behind every violation the good ones had covered.
|
|
320
|
+
*/
|
|
321
|
+
const readOverrides = (path, label, today, problems) => {
|
|
322
|
+
if (!existsSync(path))
|
|
323
|
+
return [];
|
|
324
|
+
const raw = readJson(path);
|
|
325
|
+
if (!Array.isArray(raw)) {
|
|
326
|
+
problems.push(`${label}: a JSON array of overrides, so none of it applies`);
|
|
327
|
+
return [];
|
|
328
|
+
}
|
|
329
|
+
const overrides = raw.flatMap((entry, index) => {
|
|
330
|
+
const read = v.safeParse(overrideSchema, entry);
|
|
331
|
+
if (read.success)
|
|
332
|
+
return [read.output];
|
|
333
|
+
const rule = typeof entry === "object" && entry !== null && "rule" in entry ? ` (${String(entry.rule)})` : "";
|
|
334
|
+
problems.push(`${label}: entry ${index}${rule} does not apply — ${issuesOf(read.issues).join("; ")}`);
|
|
335
|
+
return [];
|
|
336
|
+
});
|
|
337
|
+
for (const issue of overridesProblems(overrides, today))
|
|
338
|
+
problems.push(`${label}: ${issue}`);
|
|
339
|
+
return overrides;
|
|
340
|
+
};
|
|
317
341
|
const lint = (args, io) => {
|
|
318
342
|
try {
|
|
319
343
|
return lintRun(args, io);
|
|
@@ -364,15 +388,9 @@ const lintRun = (args, io) => {
|
|
|
364
388
|
return 2;
|
|
365
389
|
}
|
|
366
390
|
const problems = [];
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const read = v.safeParse(overridesSchema, readJson(overridesPath));
|
|
371
|
-
const found = read.success ? overridesProblems(read.output, today(io)) : issuesOf(read.issues);
|
|
372
|
-
if (read.success)
|
|
373
|
-
overrides = read.output;
|
|
374
|
-
problems.push(...found.map((issue) => `${values.overrides}: ${issue}`));
|
|
375
|
-
}
|
|
391
|
+
const overrides = readOverrides(join(io.cwd, values.overrides), values.overrides, today(io), problems);
|
|
392
|
+
const isWcat = (entry) => WCAT_RULES.includes(entry.rule);
|
|
393
|
+
const ruleOverrides = overrides.filter((entry) => !isWcat(entry));
|
|
376
394
|
const baselineDir = join(io.cwd, values.baseline);
|
|
377
395
|
const baseline = readBaseline(baselineDir, values.baseline, problems);
|
|
378
396
|
const schemas = values.schemas === undefined ? undefined : readJson(join(io.cwd, values.schemas));
|
|
@@ -390,7 +408,7 @@ const lintRun = (args, io) => {
|
|
|
390
408
|
const severity = new Map(LINT_RULES.map((rule) => [rule.id, rule.severity]));
|
|
391
409
|
if (values["write-baseline"]) {
|
|
392
410
|
mkdirSync(baselineDir, { recursive: true });
|
|
393
|
-
for (const [rule, targets] of Object.entries(baselineOf(violations,
|
|
411
|
+
for (const [rule, targets] of Object.entries(baselineOf(violations, ruleOverrides))) {
|
|
394
412
|
const path = join(baselineDir, `${rule}.json`);
|
|
395
413
|
if (targets.length > 0)
|
|
396
414
|
writeFileSync(path, `${JSON.stringify(targets, null, 2)}\n`);
|
|
@@ -399,7 +417,7 @@ const lintRun = (args, io) => {
|
|
|
399
417
|
baseline[rule] = targets;
|
|
400
418
|
}
|
|
401
419
|
}
|
|
402
|
-
const verdict = judge(violations, baseline,
|
|
420
|
+
const verdict = judge(violations, baseline, ruleOverrides, budget);
|
|
403
421
|
for (const id of verdict.unknownRules)
|
|
404
422
|
problems.push(`no lint rule "${id}" (named in ${values.baseline} or ${values.overrides})`);
|
|
405
423
|
for (const found of verdict.fresh)
|
|
@@ -408,11 +426,19 @@ const lintRun = (args, io) => {
|
|
|
408
426
|
problems.push(entry.in === "baseline"
|
|
409
427
|
? `stale baseline entry, fixed or overridden — remove it: ${values.baseline}/${entry.rule}.json "${entry.target}"`
|
|
410
428
|
: `stale override, no such violation — remove it: ${entry.rule} "${entry.target}"`);
|
|
411
|
-
|
|
429
|
+
const warnings = wcatWarnings(io.cwd, values.cli);
|
|
430
|
+
const warned = new Set(warnings.map((warning) => `${warning.rule} ${warning.target}`));
|
|
431
|
+
const wcatOverridden = new Set(overrides.filter(isWcat).map((entry) => `${entry.rule} ${entry.target}`));
|
|
432
|
+
// wcat-describe is checked only with --cli, so without it an override of it is neither used nor stale.
|
|
433
|
+
for (const entry of overrides.filter(isWcat))
|
|
434
|
+
if (!warned.has(`${entry.rule} ${entry.target}`) && (values.cli || entry.rule !== "wcat-describe"))
|
|
435
|
+
problems.push(`stale override, no such warning — remove it: ${entry.rule} "${entry.target}"`);
|
|
436
|
+
if (overrides.length > budget)
|
|
412
437
|
problems.push(`${overrides.length} overrides; the budget is ${budget}`);
|
|
413
438
|
io.err(`lint: ${violations.length} violations — ${verdict.baselined} in the baseline, ${verdict.overridden} overridden (budget ${budget}), ${verdict.fresh.length} new`);
|
|
414
|
-
for (const warning of
|
|
415
|
-
|
|
439
|
+
for (const warning of warnings)
|
|
440
|
+
if (!wcatOverridden.has(`${warning.rule} ${warning.target}`))
|
|
441
|
+
io.err(`warning ${warning.rule} ${warning.target}: ${warning.message}`);
|
|
416
442
|
return report(io, problems, "lint: ok");
|
|
417
443
|
};
|
|
418
444
|
const wcatWarnings = (cwd, cli) => {
|
|
@@ -527,15 +553,7 @@ const auditRun = (args, io) => {
|
|
|
527
553
|
}
|
|
528
554
|
const dir = lintDirs.get(cli);
|
|
529
555
|
if (dir) {
|
|
530
|
-
const
|
|
531
|
-
let overrides = [];
|
|
532
|
-
if (existsSync(overridesPath)) {
|
|
533
|
-
const read = v.safeParse(overridesSchema, readJson(overridesPath));
|
|
534
|
-
const found = read.success ? overridesProblems(read.output, today(io)) : issuesOf(read.issues);
|
|
535
|
-
if (read.success)
|
|
536
|
-
overrides = read.output;
|
|
537
|
-
problems.push(...found.map((issue) => `${dir}/overrides.json: ${issue}`));
|
|
538
|
-
}
|
|
556
|
+
const overrides = readOverrides(join(at(dir), "overrides.json"), `${dir}/overrides.json`, today(io), problems);
|
|
539
557
|
input.lint = {
|
|
540
558
|
baseline: readBaseline(join(at(dir), "baseline"), `${dir}/baseline`, problems),
|
|
541
559
|
overrides,
|
package/dist/dev/wcat-lint.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
/** Warnings, not lint rules: an entry in `lint/overrides.json` silences one, with its reason, like any override. */
|
|
2
|
+
export declare const WCAT_RULES: readonly ["wcat-bin-name", "wcat-describe"];
|
|
1
3
|
export interface WcatWarning {
|
|
2
|
-
rule:
|
|
4
|
+
rule: (typeof WCAT_RULES)[number];
|
|
3
5
|
target: string;
|
|
4
6
|
message: string;
|
|
5
7
|
}
|
package/dist/dev/wcat-lint.js
CHANGED
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import { parseDescribe } from "../wcat/describe.js";
|
|
6
6
|
import { DESCRIBE_FLAG, PROGRAM_PREFIX, RESERVED_NAMES, TOOL_NAME } from "../wcat/names.js";
|
|
7
|
+
/** Warnings, not lint rules: an entry in `lint/overrides.json` silences one, with its reason, like any override. */
|
|
8
|
+
export const WCAT_RULES = ["wcat-bin-name", "wcat-describe"];
|
|
7
9
|
const WIRECAT_SCOPE = "@wirecat/";
|
|
8
10
|
/** The commands a `package.json` installs: its `"bin"` keys, or the package's own name for a `"bin"` string. */
|
|
9
11
|
const binNames = (pkg) => {
|
package/dist/errors.d.ts
CHANGED
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
* Every failure a CLI built on this package, and anything consuming its output, can see. The list
|
|
3
3
|
* is closed on purpose: an agent branches on `code`, and a new code is a contract change.
|
|
4
4
|
*
|
|
5
|
+
* `conflict` is a write refused because what it would change moved on since the caller read it (a stale revision):
|
|
6
|
+
* nothing is wrong with the input or the setup, and the same write on a fresh copy may work.
|
|
7
|
+
*
|
|
5
8
|
* "provider" is whatever the CLI talks to — an HTTP API, a socket, a messenger. The word is
|
|
6
9
|
* deliberately not "server": a provider that answered with a refusal is not the same as one that
|
|
7
10
|
* never answered.
|
|
8
11
|
*/
|
|
9
|
-
export declare const errorCodes: readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled"];
|
|
12
|
+
export declare const errorCodes: readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "conflict", "cancelled"];
|
|
10
13
|
export type ErrorCode = (typeof errorCodes)[number];
|
|
11
14
|
/**
|
|
12
15
|
* Anything a caller may attach. Everything is optional because these are diagnostics, not a
|
package/dist/errors.js
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* Every failure a CLI built on this package, and anything consuming its output, can see. The list
|
|
3
3
|
* is closed on purpose: an agent branches on `code`, and a new code is a contract change.
|
|
4
4
|
*
|
|
5
|
+
* `conflict` is a write refused because what it would change moved on since the caller read it (a stale revision):
|
|
6
|
+
* nothing is wrong with the input or the setup, and the same write on a fresh copy may work.
|
|
7
|
+
*
|
|
5
8
|
* "provider" is whatever the CLI talks to — an HTTP API, a socket, a messenger. The word is
|
|
6
9
|
* deliberately not "server": a provider that answered with a refusal is not the same as one that
|
|
7
10
|
* never answered.
|
|
@@ -20,6 +23,7 @@ export const errorCodes = [
|
|
|
20
23
|
"provider_unavailable",
|
|
21
24
|
"invalid_response",
|
|
22
25
|
"outcome_unknown",
|
|
26
|
+
"conflict",
|
|
23
27
|
"cancelled",
|
|
24
28
|
];
|
|
25
29
|
// Symbol.for, so two copies of this package — one CLI's and one dependency's, during a pin bump —
|
package/dist/exit-codes.js
CHANGED
package/dist/failure.d.ts
CHANGED
|
@@ -58,7 +58,7 @@ export declare const mcpErrorResult: (object: ErrorObject) => {
|
|
|
58
58
|
}[];
|
|
59
59
|
structuredContent: {
|
|
60
60
|
error: {
|
|
61
|
-
code: "authentication_error" | "cancelled" | "configuration_error" | "confirmation_required" | "internal_error" | "invalid_response" | "network_error" | "not_found" | "outcome_unknown" | "permission_error" | "provider_error" | "provider_unavailable" | "rate_limited" | "timeout" | "validation_error";
|
|
61
|
+
code: "authentication_error" | "cancelled" | "configuration_error" | "confirmation_required" | "conflict" | "internal_error" | "invalid_response" | "network_error" | "not_found" | "outcome_unknown" | "permission_error" | "provider_error" | "provider_unavailable" | "rate_limited" | "timeout" | "validation_error";
|
|
62
62
|
reason: string;
|
|
63
63
|
message: string;
|
|
64
64
|
param?: string | undefined;
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +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 { BASE_CONSTANTS, baseConstantsSchema, loadConstants } from "./constants.js";
|
|
3
4
|
export type { CredentialSource, CredentialStorage, CredentialsOptions, StoredCredential, WriteOptions, } from "./credentials.js";
|
|
4
5
|
export { CREDENTIAL_STORAGE, Credentials, keyringService, movedRunService } from "./credentials.js";
|
|
5
6
|
export { createDeadline, type Deadline, type DeadlineOptions } from "./deadline.js";
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { BATCH_LIMITS, prepareBatch, projectBatchAnswers, readBatchInput, runBatch, } from "./batch.js";
|
|
2
2
|
export { loadConfigFile, saveConfigFile, writeSecurely } from "./config.js";
|
|
3
|
+
export { BASE_CONSTANTS, baseConstantsSchema, loadConstants } from "./constants.js";
|
|
3
4
|
export { CREDENTIAL_STORAGE, Credentials, keyringService, movedRunService } from "./credentials.js";
|
|
4
5
|
export { createDeadline } from "./deadline.js";
|
|
5
6
|
export { CliError, errorCodes, isCliError } from "./errors.js";
|
package/dist/query/parser.js
CHANGED
|
@@ -115,6 +115,17 @@ const tokensOf = (text, fail = queryError) => {
|
|
|
115
115
|
break;
|
|
116
116
|
at++;
|
|
117
117
|
}
|
|
118
|
+
// A ref after a field (`about:project:1`, `about:chat:max/1/2`) is one value: every WireCat CLI prints refs
|
|
119
|
+
// unquoted, and Lucene would read the second colon as a stray field.
|
|
120
|
+
const [field, colon] = tokens.slice(-2);
|
|
121
|
+
if (field?.kind === "term" &&
|
|
122
|
+
colon?.kind === ":" &&
|
|
123
|
+
colon.span.end === start &&
|
|
124
|
+
text[at] === ":" &&
|
|
125
|
+
/^[a-z][a-z_-]*$/u.test(text.slice(start, at)) &&
|
|
126
|
+
/^:[^\s()"]/u.test(text.slice(at)))
|
|
127
|
+
while (at < text.length && !whitespace(text[at] ?? "") && !["(", ")", '"'].includes(text[at] ?? ""))
|
|
128
|
+
at++;
|
|
118
129
|
const raw = text.slice(start, at);
|
|
119
130
|
kind =
|
|
120
131
|
{ AND: "AND", OR: "OR", NOT: "NOT", "&&": "AND", "||": "OR" }[raw] ??
|
package/dist/query/registry.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare const defineQuery: (registry: QueryRegistry) => QueryRegistry;
|
|
|
15
15
|
/** The query reasons every CLI shares; a CLI adds its own (`query.invalid-tag`) with `defineReasons`. */
|
|
16
16
|
export declare const QUERY_REASONS: {
|
|
17
17
|
reason: string;
|
|
18
|
-
code: "authentication_error" | "cancelled" | "configuration_error" | "confirmation_required" | "internal_error" | "invalid_response" | "network_error" | "not_found" | "outcome_unknown" | "permission_error" | "provider_error" | "provider_unavailable" | "rate_limited" | "timeout" | "validation_error";
|
|
18
|
+
code: "authentication_error" | "cancelled" | "configuration_error" | "confirmation_required" | "conflict" | "internal_error" | "invalid_response" | "network_error" | "not_found" | "outcome_unknown" | "permission_error" | "provider_error" | "provider_unavailable" | "rate_limited" | "timeout" | "validation_error";
|
|
19
19
|
message: string;
|
|
20
20
|
details: string[];
|
|
21
21
|
step?: string | undefined;
|
package/dist/release/markdown.js
CHANGED
|
@@ -27,6 +27,7 @@ export const changelogProblems = (text, { version, release, headings, unreleased
|
|
|
27
27
|
let section;
|
|
28
28
|
let subheadings = new Set();
|
|
29
29
|
let first = true;
|
|
30
|
+
let top = false;
|
|
30
31
|
let legacy = false;
|
|
31
32
|
if (legacyHeadingsThrough !== undefined && !/^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/.test(legacyHeadingsThrough))
|
|
32
33
|
problems.push(`legacyHeadingsThrough: "${legacyHeadingsThrough}" is not a version`);
|
|
@@ -35,6 +36,7 @@ export const changelogProblems = (text, { version, release, headings, unreleased
|
|
|
35
36
|
if (line.startsWith("## ")) {
|
|
36
37
|
section = line.slice(3);
|
|
37
38
|
subheadings = new Set();
|
|
39
|
+
top = first;
|
|
38
40
|
legacy = false;
|
|
39
41
|
if (section === unreleased) {
|
|
40
42
|
if (release)
|
|
@@ -66,6 +68,9 @@ export const changelogProblems = (text, { version, release, headings, unreleased
|
|
|
66
68
|
problems.push(`${where}: "${heading}" twice in "${section}"`);
|
|
67
69
|
subheadings.add(heading);
|
|
68
70
|
}
|
|
71
|
+
// Only the top section: released ones are frozen, and older ones in several CLIs still have such entries.
|
|
72
|
+
if (top && !legacy && subheadings.size === 0 && /^[-*] /.test(line))
|
|
73
|
+
problems.push(`${where}: an entry under no "###" heading in "${section}" — put it under one of: ${headings.join(", ")}`);
|
|
69
74
|
for (const found of line.match(id) ?? [])
|
|
70
75
|
problems.push(`${where}: internal id ${found} — say what changed instead`);
|
|
71
76
|
});
|
|
@@ -8,7 +8,7 @@ export declare const CRASH_REASON = "command.crashed";
|
|
|
8
8
|
/** The one error object, on stderr in machine mode and as MCP's `isError` content alike. */
|
|
9
9
|
export declare const errorObjectSchema: v.StrictObjectSchema<{
|
|
10
10
|
readonly error: v.StrictObjectSchema<{
|
|
11
|
-
readonly code: v.PicklistSchema<["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled", "internal_error"], undefined>;
|
|
11
|
+
readonly code: v.PicklistSchema<["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "conflict", "cancelled", "internal_error"], undefined>;
|
|
12
12
|
readonly reason: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>;
|
|
13
13
|
readonly message: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
|
|
14
14
|
/** The option or argument at fault. */
|
|
@@ -55,7 +55,7 @@ export declare const errorObjectSchema: v.StrictObjectSchema<{
|
|
|
55
55
|
export type ErrorObject = v.InferOutput<typeof errorObjectSchema>;
|
|
56
56
|
declare const reasonSchema: v.StrictObjectSchema<{
|
|
57
57
|
readonly reason: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>;
|
|
58
|
-
readonly code: v.PicklistSchema<["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled", "internal_error"], undefined>;
|
|
58
|
+
readonly code: v.PicklistSchema<["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "conflict", "cancelled", "internal_error"], undefined>;
|
|
59
59
|
/** `{name}` placeholders, each one of `details`. */
|
|
60
60
|
readonly message: v.SchemaWithPipe<readonly [v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, v.MaxLengthAction<string, 120, undefined>]>;
|
|
61
61
|
readonly details: v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a snake_case key">]>, undefined>;
|
package/dist/standards/errors.js
CHANGED
|
@@ -97,6 +97,12 @@ export const BASE_REASONS = defineReasons([
|
|
|
97
97
|
details: [],
|
|
98
98
|
},
|
|
99
99
|
{ reason: "outcome.unknown", code: "outcome_unknown", message: "it is not known whether it worked", details: [] },
|
|
100
|
+
{
|
|
101
|
+
reason: "write.conflict",
|
|
102
|
+
code: "conflict",
|
|
103
|
+
message: "it changed since it was read; read it again",
|
|
104
|
+
details: [],
|
|
105
|
+
},
|
|
100
106
|
{ reason: "command.cancelled", code: "cancelled", message: "the command was cancelled", details: [] },
|
|
101
107
|
{ reason: CRASH_REASON, code: CRASH_CODE, message: "the command failed; this is a bug", details: [] },
|
|
102
108
|
]);
|
package/dist/standards/lint.d.ts
CHANGED
|
@@ -14,6 +14,15 @@ export declare const defineLintRules: (input: unknown) => LintRule[];
|
|
|
14
14
|
export declare const LINT_RULES: LintRule[];
|
|
15
15
|
/** `lint/baseline/<rule-id>.json`: today's violations of one rule, by target. It may only shrink. */
|
|
16
16
|
export declare const baselineSchema: v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>;
|
|
17
|
+
/** One `lint/overrides.json` entry: the exception, its reason, who keeps it and when to look again. */
|
|
18
|
+
export declare const overrideSchema: v.StrictObjectSchema<{
|
|
19
|
+
readonly rule: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a lower-case word, words joined by -">]>;
|
|
20
|
+
readonly target: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
|
|
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, `@octocat`. */
|
|
23
|
+
readonly owner: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a GitHub handle or team, such as @octocat">]>;
|
|
24
|
+
readonly review_after: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.IsoDateAction<string, "a date, YYYY-MM-DD">]>;
|
|
25
|
+
}, undefined>;
|
|
17
26
|
/** `lint/overrides.json`: exceptions, each with its reason, who keeps it and when to look again; counted against a budget. */
|
|
18
27
|
export declare const overridesSchema: v.ArraySchema<v.StrictObjectSchema<{
|
|
19
28
|
readonly rule: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "a lower-case word, words joined by -">]>;
|
package/dist/standards/lint.js
CHANGED
|
@@ -119,15 +119,17 @@ export const LINT_RULES = defineLintRules([
|
|
|
119
119
|
]);
|
|
120
120
|
/** `lint/baseline/<rule-id>.json`: today's violations of one rule, by target. It may only shrink. */
|
|
121
121
|
export const baselineSchema = v.array(text);
|
|
122
|
-
/** `lint/overrides.json
|
|
123
|
-
export const
|
|
122
|
+
/** One `lint/overrides.json` entry: the exception, its reason, who keeps it and when to look again. */
|
|
123
|
+
export const overrideSchema = 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
127
|
/** A GitHub handle or a team, `@octocat`. */
|
|
128
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
|
+
/** `lint/overrides.json`: exceptions, each with its reason, who keeps it and when to look again; counted against a budget. */
|
|
132
|
+
export const overridesSchema = v.array(overrideSchema);
|
|
131
133
|
/** What the schemas cannot say, so they stay convertible to JSON Schema: no entry twice. */
|
|
132
134
|
export const baselineProblems = (targets) => duplicates(targets).map((target) => `"${target}" is listed twice`);
|
|
133
135
|
/** No entry twice, and none past its `review_after` on `today` (YYYY-MM-DD, the caller's clock). */
|
|
@@ -65,7 +65,7 @@ export declare const pageSchema: v.VariantSchema<"mode", [v.StrictObjectSchema<{
|
|
|
65
65
|
export type Total = v.InferOutput<typeof totalSchema>;
|
|
66
66
|
/** A source that failed or was skipped: the answer holds what the others gave. */
|
|
67
67
|
export declare const issueSchema: v.StrictObjectSchema<{
|
|
68
|
-
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled"], undefined>;
|
|
68
|
+
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "conflict", "cancelled"], undefined>;
|
|
69
69
|
readonly reason: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>, undefined>;
|
|
70
70
|
/** The account, provider or source that is missing from the answer. */
|
|
71
71
|
readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
|
|
@@ -80,7 +80,7 @@ export declare const metaSchema: v.StrictObjectSchema<{
|
|
|
80
80
|
readonly accounts: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>, undefined>;
|
|
81
81
|
readonly complete: v.BooleanSchema<undefined>;
|
|
82
82
|
readonly issues: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
83
|
-
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled"], undefined>;
|
|
83
|
+
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "conflict", "cancelled"], undefined>;
|
|
84
84
|
readonly reason: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>, undefined>;
|
|
85
85
|
/** The account, provider or source that is missing from the answer. */
|
|
86
86
|
readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
|
|
@@ -172,7 +172,7 @@ export declare const listEnvelopeSchema: <T extends v.GenericSchema>(item: T) =>
|
|
|
172
172
|
readonly accounts: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>, undefined>;
|
|
173
173
|
readonly complete: v.BooleanSchema<undefined>;
|
|
174
174
|
readonly issues: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
175
|
-
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled"], undefined>;
|
|
175
|
+
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "conflict", "cancelled"], undefined>;
|
|
176
176
|
readonly reason: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>, undefined>;
|
|
177
177
|
/** The account, provider or source that is missing from the answer. */
|
|
178
178
|
readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
|
|
@@ -237,7 +237,7 @@ export declare const itemEnvelopeSchema: <T extends v.GenericSchema>(item: T) =>
|
|
|
237
237
|
readonly accounts: v.OptionalSchema<v.ArraySchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>, undefined>, undefined>;
|
|
238
238
|
readonly complete: v.BooleanSchema<undefined>;
|
|
239
239
|
readonly issues: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
240
|
-
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled"], undefined>;
|
|
240
|
+
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "conflict", "cancelled"], undefined>;
|
|
241
241
|
readonly reason: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>, undefined>;
|
|
242
242
|
/** The account, provider or source that is missing from the answer. */
|
|
243
243
|
readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
|
|
@@ -280,7 +280,7 @@ export declare const jsonlEndSchema: v.StrictObjectSchema<{
|
|
|
280
280
|
readonly type: v.LiteralSchema<"end", undefined>;
|
|
281
281
|
readonly complete: v.BooleanSchema<undefined>;
|
|
282
282
|
readonly issues: v.OptionalSchema<v.ArraySchema<v.StrictObjectSchema<{
|
|
283
|
-
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "cancelled"], undefined>;
|
|
283
|
+
readonly code: v.PicklistSchema<readonly ["validation_error", "configuration_error", "authentication_error", "permission_error", "not_found", "confirmation_required", "rate_limited", "timeout", "network_error", "provider_error", "provider_unavailable", "invalid_response", "outcome_unknown", "conflict", "cancelled"], undefined>;
|
|
284
284
|
readonly reason: v.OptionalSchema<v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "dotted lower-case words, such as `page.next`">]>, undefined>;
|
|
285
285
|
/** The account, provider or source that is missing from the answer. */
|
|
286
286
|
readonly source: v.SchemaWithPipe<readonly [v.StringSchema<undefined>, v.RegexAction<string, "must not be empty">]>;
|
|
@@ -15,6 +15,8 @@ export interface Sandbox {
|
|
|
15
15
|
export declare const enterSandbox: (env?: NodeJS.ProcessEnv, platform?: NodeJS.Platform) => Sandbox;
|
|
16
16
|
export declare const removed: (path: string, maxRetries: number, rm?: typeof rmSync) => boolean;
|
|
17
17
|
export declare const REAL_KEYRING_REACHED = "a test reached the real OS keyring \u2014 pass memoryKeyring() instead";
|
|
18
|
+
/** The stack of the first reach of the keyring stand-in since the last call, and forgets it. */
|
|
19
|
+
export declare const takeKeyringReach: () => string | undefined;
|
|
18
20
|
/**
|
|
19
21
|
* `systemKeyring` loads `@napi-rs/keyring` through `createRequire`, which `vi.mock` does not
|
|
20
22
|
* intercept, so the stand-in goes into the CommonJS require cache. Resolved from here (the copy
|
package/dist/testing/sandbox.js
CHANGED
|
@@ -99,11 +99,24 @@ export const removed = (path, maxRetries, rm = rmSync) => {
|
|
|
99
99
|
}
|
|
100
100
|
};
|
|
101
101
|
export const REAL_KEYRING_REACHED = "a test reached the real OS keyring — pass memoryKeyring() instead";
|
|
102
|
+
let reached;
|
|
103
|
+
/**
|
|
104
|
+
* Its throw alone is not enough: `Credentials` in `auto` mode catches it, warns and falls back to a file, so a test
|
|
105
|
+
* that reached the keyring still passed. The first reach is kept for `takeKeyringReach`.
|
|
106
|
+
*/
|
|
102
107
|
class ThrowingEntry {
|
|
103
108
|
constructor() {
|
|
104
|
-
|
|
109
|
+
const error = new Error(REAL_KEYRING_REACHED);
|
|
110
|
+
reached ??= error.stack ?? error.message;
|
|
111
|
+
throw error;
|
|
105
112
|
}
|
|
106
113
|
}
|
|
114
|
+
/** The stack of the first reach of the keyring stand-in since the last call, and forgets it. */
|
|
115
|
+
export const takeKeyringReach = () => {
|
|
116
|
+
const stack = reached;
|
|
117
|
+
reached = undefined;
|
|
118
|
+
return stack;
|
|
119
|
+
};
|
|
107
120
|
/**
|
|
108
121
|
* `systemKeyring` loads `@napi-rs/keyring` through `createRequire`, which `vi.mock` does not
|
|
109
122
|
* intercept, so the stand-in goes into the CommonJS require cache. Resolved from here (the copy
|
package/dist/testing/setup.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
import { afterAll } from "vitest";
|
|
1
|
+
import { afterAll, afterEach } from "vitest";
|
|
2
2
|
import { installNetworkGuard } from "./network.js";
|
|
3
|
-
import { enterSandbox, installThrowingKeyring } from "./sandbox.js";
|
|
3
|
+
import { enterSandbox, installThrowingKeyring, takeKeyringReach } from "./sandbox.js";
|
|
4
4
|
/**
|
|
5
5
|
* One vitest `setupFiles` entry for every WireCat CLI: `setupFiles: ["@wirecat/cli-base/testing/setup"]`.
|
|
6
6
|
* Each test file gets its own sandbox directory, removed when the file is done; the real keyring and
|
|
@@ -9,4 +9,9 @@ import { enterSandbox, installThrowingKeyring } from "./sandbox.js";
|
|
|
9
9
|
const sandbox = enterSandbox();
|
|
10
10
|
installThrowingKeyring();
|
|
11
11
|
installNetworkGuard();
|
|
12
|
+
afterEach(() => {
|
|
13
|
+
const stack = takeKeyringReach();
|
|
14
|
+
if (stack)
|
|
15
|
+
throw new Error(`the test passed only because the keyring fell back to a file:\n${stack}`);
|
|
16
|
+
});
|
|
12
17
|
afterAll(() => sandbox.restore());
|
package/docs/standards/README.md
CHANGED
|
@@ -29,18 +29,18 @@ the four steps that need a person. A new CLI is ready for its first release when
|
|
|
29
29
|
|---|---|---:|---|
|
|
30
30
|
| [Toolchain](toolchain.md) | `TOOL` | 20 | TypeScript, Biome, grit plugins, size caps, import cycles, spelling, Markdown, Go |
|
|
31
31
|
| [Layout](layout.md) | `LAY` | 10 | Repository layout, entry points, exports, file size, one job per file, splitting a unit, the bundled bin |
|
|
32
|
-
| [Commands](commands.md) | `CMD` |
|
|
32
|
+
| [Commands](commands.md) | `CMD` | 31 | Paths, vocabulary, flags, arguments, effects, lifetime, query, help, external references, joining `wcat`, settings in the config file, refs |
|
|
33
33
|
| [Output](output.md) | `OUT` | 24 | Data on stdout only, envelopes, errors, exit codes, partial answers, projection |
|
|
34
34
|
| [MCP](mcp.md) | `MCP` | 13 | The MCP tools, how they come from commands, what each permission shows |
|
|
35
35
|
| [Accounts](accounts.md) | `ACCT` | 12 | The explicit profile, reads across accounts, scope flags, linked identities, sign-in commands |
|
|
36
36
|
| [Permissions](permissions.md) | `PERM` | 22 | Levels, keys, defaults, hard limits, the write guard and its journal |
|
|
37
|
-
| [Architecture](architecture.md) | `ARCH` |
|
|
37
|
+
| [Architecture](architecture.md) | `ARCH` | 19 | Layers, dependency direction, vendor types, what goes in which package, the command tree built once |
|
|
38
38
|
| [Lifecycle](lifecycle.md) | `LIFE` | 15 | Opening late, who closes, deadlines, long-running commands, the `serve` relay, resumable syncs and their request budget, the run log |
|
|
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
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
|
-
| [Documents](docs.md) | `DOC` |
|
|
43
|
+
| [Documents](docs.md) | `DOC` | 21 | User pages and developer pages, generated pages, current facts, changelog, repository page |
|
|
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, the owner's yes, trusted publishing |
|
|
46
46
|
| [CI](ci.md) | `CI` | 12 | Hooks, the pull request budget, full validation, monthly runs |
|
|
@@ -86,7 +86,7 @@ at start-up and the `serve` relay are in [Lifecycle](lifecycle.md); the store's
|
|
|
86
86
|
- **Example:** topics and polls are port groups and shared commands in cli-core. A messenger that lacks a capability
|
|
87
87
|
refuses the command; it does not silently do less. A bot is another messenger with fewer capabilities, and tg and
|
|
88
88
|
max share the bot code. Calendars have the same shape: cli-core's `addCalendarCommands` builds `calendars`,
|
|
89
|
-
`sync`, `status`, `events` and `search` over the `CalendarSource` and `CalendarWriter` ports, and mounts
|
|
89
|
+
`sync`, `store status`, `events` and `search` over the `CalendarSource` and `CalendarWriter` ports, and mounts
|
|
90
90
|
`calendars busy` or the event writes only when the provider has them.
|
|
91
91
|
- **Exceptions:** a feature stays in one CLI only when the other providers cannot have it, such as MAX's socket server,
|
|
92
92
|
Telegram's app registration, or gcal's generated `api` commands.
|
|
@@ -249,6 +249,20 @@ options, its hooks) lives in a per-call context that the action receives, never
|
|
|
249
249
|
[CORE-41](https://github.com/WireCatLabs/cli-core/blob/main/docs/dev/BACKLOG_LATER.md). New code does not add to it.
|
|
250
250
|
- **Applies to:** cli-core's command skeleton and every CLI built on it. **Since:** cli-base 0.65.3. **Rulings:** none.
|
|
251
251
|
|
|
252
|
+
### WC-ARCH-23 · A value that ships with the package lives in the layer's constants.json
|
|
253
|
+
|
|
254
|
+
A value that ships inside a package and is the same for every owner — an OAuth app's client id, a fixed URL — lives
|
|
255
|
+
in that layer's `constants.json` and is read through `loadConstants` (cli-base, then cli-core, then the tool). It is
|
|
256
|
+
never a constant scattered through the source, and never a file CI writes from a secret.
|
|
257
|
+
|
|
258
|
+
- **Why:** a CI-written file goes missing when the secret does, and the release then cannot sign in (gcal 0.19.0);
|
|
259
|
+
scattered constants are copied between CLIs and drift apart.
|
|
260
|
+
- **Enforced by:** review only.
|
|
261
|
+
- **Example:** cli-core's `constants.json` holds the WireCat Google desktop app's `google.app.client_id`; drive and
|
|
262
|
+
gcal read it through cli-core instead of each keeping a copy. The owner's own choices stay in `config.json`.
|
|
263
|
+
- **Exceptions:** none.
|
|
264
|
+
- **Applies to:** cli-base, cli-core and every CLI. **Since:** cli-base 0.76.0. **Rulings:** none.
|
|
265
|
+
|
|
252
266
|
## Sources
|
|
253
267
|
|
|
254
268
|
These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
|
|
@@ -127,7 +127,7 @@ catalogue and parity manifest stay in cli-core
|
|
|
127
127
|
before the create, so a retry finds it at the provider. A CLI whose provider takes such an id may offer both. gcal
|
|
128
128
|
offers only `--operation-id` (from gcal 0.9.0): its `operation_id` is also the event's id at Google, so one value
|
|
129
129
|
serves both.
|
|
130
|
-
- **Exceptions:** generated commands (`bot api`) are exempt from the naming rules.
|
|
130
|
+
- **Exceptions:** generated commands (`bot api`, and gcal's and drive's `api`) are exempt from the naming rules.
|
|
131
131
|
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
132
132
|
|
|
133
133
|
### WC-CMD-11 · Name an option in plain words, with the kind of value in the name
|
|
@@ -291,7 +291,7 @@ store or files without reading the service first (`remote: "none"`, `local: "wri
|
|
|
291
291
|
- **Enforced by:** `command-example-count` · `faulty-help-example-parameters`.
|
|
292
292
|
- **Example:** `annotate(list, { examples: ["max chats list --limit 20"] })`. Each example starts with the tool, runs
|
|
293
293
|
its own command, and gives only arguments and options that command takes.
|
|
294
|
-
- **Exceptions:** generated commands (`bot api`) need no examples.
|
|
294
|
+
- **Exceptions:** generated commands (`bot api`, and gcal's and drive's `api`) need no examples.
|
|
295
295
|
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
296
296
|
|
|
297
297
|
### WC-CMD-25 · Give a shared command and a shared option the same sentence in every CLI of its group
|
|
@@ -380,10 +380,54 @@ The references, each for its own part:
|
|
|
380
380
|
`help`, `version`, `shortcuts`, `doctor`, `update`, `upgrade`, `app`), and, with `--cli`, when the built CLI does not
|
|
381
381
|
answer `--wcat-describe`.
|
|
382
382
|
- **Exceptions:** a CLI keeps its short `"bin"` name beside `wcat-<name>` until callers have moved to the new name; the
|
|
383
|
-
warning stays until then.
|
|
383
|
+
warning stays until then. A CLI that keeps the short name for good says why in `lint/overrides.json` (`"rule":
|
|
384
|
+
"wcat-bin-name"`, the target as the warning prints it), which silences the warning and counts against the budget.
|
|
384
385
|
- **Applies to:** all CLIs, and third-party CLIs that join `wcat`. **Since:** cli-base 0.66.0. **Rulings:** NEED-149,
|
|
385
386
|
NEED-153, NEED-154.
|
|
386
387
|
|
|
388
|
+
## Settings
|
|
389
|
+
|
|
390
|
+
### WC-CMD-30 · Write every setting into the config file with its default, and list them all in `config show`
|
|
391
|
+
|
|
392
|
+
A CLI declares each scalar setting once, with its default and one line that says what it does, through cli-core's
|
|
393
|
+
`DeclaredSetting`. `config show` lists every declared setting with its value, its default and whether the file sets
|
|
394
|
+
it. Whenever the CLI writes its config file anyway, and on an explicit `config migrate`, it adds each missing setting
|
|
395
|
+
at its default with `withSettingDefaults`. It keeps the owner's values, their order and unknown keys. It may create
|
|
396
|
+
the top-level `defaults` section, which needs no field of its own, so every key shows; it creates no other section. A
|
|
397
|
+
plain read never rewrites the file.
|
|
398
|
+
|
|
399
|
+
- **Why:** the owner finds and edits every setting in the file, without reading the docs to learn that it exists.
|
|
400
|
+
- **Enforced by:** review only.
|
|
401
|
+
- **Example:** `memo config migrate` adds `"agent_decisions": false` to a file that lacks it; `memo config show` lists
|
|
402
|
+
it as `false (default)`.
|
|
403
|
+
- **Exceptions:** lists and sections that need fields no default can give (accounts, folders, permissions) get no
|
|
404
|
+
placeholder. A secret is never declared, since `config show` prints every value. A setting whose absence means
|
|
405
|
+
something no value says (no timeout, the terminal decides colour) is not declared either. Known gaps, as of
|
|
406
|
+
2026-11-03: cli-core's shared settings declare every shared scalar setting and give `config migrate`, so tg, max,
|
|
407
|
+
wa, mail and gcal follow once they take the cli-core release after 0.561.0; their own settings (max's `serve` and
|
|
408
|
+
`transcribe_model`, for example) are not declared yet. drive's first-run file holds them all, but drive's own
|
|
409
|
+
`config` has no `migrate` and its writes do not fill the file. zm declares nothing yet.
|
|
410
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.76.0. **Rulings:** none.
|
|
411
|
+
|
|
412
|
+
## References
|
|
413
|
+
|
|
414
|
+
### WC-CMD-31 · Accept every ref any WireCat CLI prints, and never ask the caller to translate it
|
|
415
|
+
|
|
416
|
+
A command argument, an option and a query value that take a stored thing accept its ref exactly as any WireCat CLI
|
|
417
|
+
prints it ([WC-OUT-13](output.md#wc-out-13--write-times-durations-references-and-empty-values-one-way)), unquoted.
|
|
418
|
+
The CLI that owns the thing opens it; a ref for another account or provider fails with an error that names the
|
|
419
|
+
right one, never `not_found`.
|
|
420
|
+
|
|
421
|
+
- **Why:** agents copy refs from one CLI's output into the next command. Each ref that fails costs a retry, a guess
|
|
422
|
+
or a correction from the owner: memo printed `chat:max/…`, and max read it as a chat title.
|
|
423
|
+
- **Enforced by:** `src/query/parser.test.ts` · `cli-core: src/domain/references.test.ts`.
|
|
424
|
+
- **Example:** `memo relationships list about:project:1` and `about:chat:max/1/2` read the ref as one value;
|
|
425
|
+
`max work messages list chat:max/<account>/<id>` answers as the bare id does.
|
|
426
|
+
- **Exceptions:** none. Known gaps, as of 2026-11-03: the query value works from cli-base 0.75.3 in each CLI that
|
|
427
|
+
takes it; chat and message arguments in tg, max and wa, and `document:N` in drive's `knowledge` commands, follow
|
|
428
|
+
cli-core's reference slices.
|
|
429
|
+
- **Applies to:** all CLIs. **Since:** cli-base 0.75.3. **Rulings:** none.
|
|
430
|
+
|
|
387
431
|
## Sources
|
|
388
432
|
|
|
389
433
|
These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
|
package/docs/standards/docs.md
CHANGED
|
@@ -153,7 +153,8 @@ Rule ids on this page are `WC-DOC-<n>`. How to read a rule, how ids work and whi
|
|
|
153
153
|
- **Enforced by:** `pnpm docs:check` · `changelog-guard.yml`.
|
|
154
154
|
- **Example:** `### Added`, `### Changed — may break callers`, `### Fixed`, `### Security`, `### Removed`. max-cli
|
|
155
155
|
uses this fixed Russian rendering: `Что нового`, `Изменено — может сломать скрипты`, `Исправлено`, `Безопасность`,
|
|
156
|
-
`Удалено`.
|
|
156
|
+
`Удалено`. Every entry sits under one of them: `docs:check` refuses an entry under no heading in the top
|
|
157
|
+
section.
|
|
157
158
|
- **Exceptions:** a released section keeps the headings it was released with, because it is never edited
|
|
158
159
|
([WC-DOC-20](#wc-doc-20--a-released-section-is-frozen-and-a-new-entry-goes-under-an-existing-heading)). A CLI that
|
|
159
160
|
moves to this set puts its last version with the old headings in its changelog rules as
|
|
@@ -216,6 +217,21 @@ Rule ids on this page are `WC-DOC-<n>`. How to read a rule, how ids work and whi
|
|
|
216
217
|
Sections that already differed from their tag on 25.10.2026 stay as they are, and nothing reports them.
|
|
217
218
|
- **Applies to:** every repository that calls the changelog guard. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
218
219
|
|
|
220
|
+
### WC-DOC-21 · A public repository has a banner, a link preview, a homepage and topics
|
|
221
|
+
|
|
222
|
+
- **Why:** the GitHub page and a shared link are often the first thing a person sees. A tool without them looks
|
|
223
|
+
unfinished next to the others, and one without topics is not found in GitHub search.
|
|
224
|
+
- **Enforced by:** review only.
|
|
225
|
+
- **Example:** `docs/images/social-preview.png` is 1280×640 in the shared style: the command name in the
|
|
226
|
+
cyan-violet-magenta gradient, a one-line tagline and the "part of WireCat · wirecat.dev" footer. `README.md` opens
|
|
227
|
+
with it through an absolute `raw.githubusercontent.com` URL, so it also renders on npmjs.com, with the tagline as
|
|
228
|
+
its alt text. The same file is the repository's social preview (Settings → Social preview; GitHub has no API for
|
|
229
|
+
it). The homepage is the tool's wirecat.dev docs page, and the topics name the service, `cli`, `ai-agents`, `mcp`
|
|
230
|
+
where it serves MCP, and `wirecat`.
|
|
231
|
+
- **Exceptions:** private repositories, and repositories that only support the tools (`.github`, `community`,
|
|
232
|
+
`cli-docs`, `cli-testing`).
|
|
233
|
+
- **Applies to:** every public WireCat repository. **Since:** cli-base 0.75.3. **Rulings:** none.
|
|
234
|
+
|
|
219
235
|
## Sources
|
|
220
236
|
|
|
221
237
|
These documents explain the rules on this page in more detail. Where one of them disagrees with this page, this page
|
|
@@ -82,6 +82,7 @@ and [WC-SECR-2](security.md#wc-secr-2--a-log-keeps-only-known-attribute-keys-and
|
|
|
82
82
|
|---|---|---|
|
|
83
83
|
| one-shot | 30 s | 30 s |
|
|
84
84
|
| `cooperative` ([WC-LIFE-10](#wc-life-10--mark-a-long-command-cooperative-when-it-should-stop-at-its-own-pace-and-keep-its-work)) | 5 min | 45 s |
|
|
85
|
+
| `cooperative` that also declares `partial` (a resumable sync) | 15 min | 45 s |
|
|
85
86
|
|
|
86
87
|
`--timeout 2m` or the `<PREFIX>_TIMEOUT` variable sets the deadline, higher or lower. Past the deadline a one-shot
|
|
87
88
|
command fails with `timeout` (exit 9).
|
|
@@ -93,10 +94,10 @@ command fails with `timeout` (exit 9).
|
|
|
93
94
|
a cut call. Claude Code waits longer ([its MCP page](https://code.claude.com/docs/en/mcp)), but a CLI does not know
|
|
94
95
|
its client, so it takes the shortest.
|
|
95
96
|
- **Enforced by:** `cli-core: src/cli/deadline.test.ts` · `cli-core: src/cli/program.test.ts`.
|
|
96
|
-
- **Example:** from cli-core 0.
|
|
97
|
-
MCP stops at 45 seconds with what it saved. cli-core's `DEFAULT_COMMAND_MS`, `DEFAULT_COOPERATIVE_MS
|
|
98
|
-
`DEFAULT_AGENT_COOPERATIVE_MS` (`src/cli/execution.ts`) hold the
|
|
99
|
-
the command.
|
|
97
|
+
- **Example:** from cli-core 0.473.0, a calendar `sync` in a terminal may run 15 minutes; the same command called over
|
|
98
|
+
MCP stops at 45 seconds with what it saved. cli-core's `DEFAULT_COMMAND_MS`, `DEFAULT_COOPERATIVE_MS`,
|
|
99
|
+
`DEFAULT_SYNC_MS` and `DEFAULT_AGENT_COOPERATIVE_MS` (`src/cli/execution.ts`) hold the four values, and `run()` picks
|
|
100
|
+
one when it starts the command.
|
|
100
101
|
- **Exceptions:** a `persistent` command has no deadline and ignores `--timeout`. An `interactive` command has none
|
|
101
102
|
while a person is at the terminal; run headless, it keeps the deadline. `--help` and `--version` need none. A
|
|
102
103
|
background fetch started with `<PREFIX>_BACKFILL_JOB` is bounded only by an explicit `--timeout`.
|
|
@@ -274,7 +275,7 @@ status command answers how far it is
|
|
|
274
275
|
- **Enforced by:** `cli-core: src/background/budget.test.ts` · `cli-core: src/calendar/testing/sync-contract.ts`.
|
|
275
276
|
- **Example:** `gcal personal sync` over MCP has 45 s: margin 6.75 s, so with gcal's 1.5 s about 25 requests. It
|
|
276
277
|
answers `state: "partial"`, `stopped: "requests"`, `progress: { calendars, calendarsDone, requests, saved }`, and
|
|
277
|
-
`next` with `gcal personal sync`; `gcal personal status` shows each calendar's progress. cli-core builds the budget
|
|
278
|
+
`next` with `gcal personal sync`; `gcal personal store status` shows each calendar's progress. cli-core builds the budget
|
|
278
279
|
with `runBudgetOf(command, maxPages)` from `@wirecat/cli-core/cli` over `runBudget` in
|
|
279
280
|
`@wirecat/cli-core/background`.
|
|
280
281
|
- **Exceptions:** known gaps, as of 2026-10-27: only the calendar sync spends a budget (see "Who follows"). The
|
package/docs/standards/mcp.md
CHANGED
|
@@ -16,8 +16,9 @@ Rule ids on this page are `WC-MCP-<n>`. How to read a rule, how ids work and whi
|
|
|
16
16
|
### Who follows these rules today
|
|
17
17
|
|
|
18
18
|
tg, max and wa serve cli-core's personal server (`<cli> <profile> mcp serve`) and bot server (`<cli> <profile> bot mcp serve`).
|
|
19
|
-
`mcp serve --profiles`
|
|
20
|
-
|
|
19
|
+
cli-core serves `mcp serve --profiles`
|
|
20
|
+
([WC-MCP-9](#wc-mcp-9--a-server-serves-the-profiles-it-is-told-with-one-connection-for-each)) from 0.470.0, for the
|
|
21
|
+
messengers and for the CLIs below that use its shared server.
|
|
21
22
|
zoom serves the three shared tools over its own commands from zoom-cli 0.35.0 (`zm <profile> mcp serve`, cli-core's
|
|
22
23
|
`toolsFromCommands` and `runCommandForAgent`), and gcal does the same (`gcal <profile> mcp serve`). memo and drive have their own servers: memo has hand-written
|
|
23
24
|
tools, and drive builds one tool per command from its command tree and skips commands through its own list. Those two are known
|
package/docs/standards/output.md
CHANGED
|
@@ -216,7 +216,8 @@ whole or partial.
|
|
|
216
216
|
- **Enforced by:** `src/exit-codes.test.ts`.
|
|
217
217
|
- **Example:** 0 ok · 1 internal error · 2 validation error · 3 configuration error · 4 authentication error ·
|
|
218
218
|
5 permission error · 6 not found · 7 confirmation required · 8 rate limited · 9 timeout · 10 network error ·
|
|
219
|
-
11 provider error · 12 provider unavailable · 13 invalid response · 14 outcome unknown ·
|
|
219
|
+
11 provider error · 12 provider unavailable · 13 invalid response · 14 outcome unknown · 15 conflict ·
|
|
220
|
+
130 cancelled.
|
|
220
221
|
- **Exceptions:** on cli-core's `run()`, a one-shot command stopped by SIGTERM exits 143. A `persistent` command
|
|
221
222
|
stopped by Ctrl-C or SIGTERM exits 0. A closed pipe exits 0. See the [lifecycle](lifecycle.md) page.
|
|
222
223
|
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
@@ -237,7 +238,8 @@ whole or partial.
|
|
|
237
238
|
- **Example:** every error has `retry: { safe, after_ms? }`. A transient failure (`timeout`, `rate_limited`,
|
|
238
239
|
`network_error`, `provider_unavailable`) is safe for a command that writes nothing on the service, or for a write
|
|
239
240
|
that never left (`sent: false`). A sync that only writes the local store is safe to repeat. `outcome_unknown` is
|
|
240
|
-
never safe: repeat that write only with its `--operation-id`.
|
|
241
|
+
never safe: repeat that write only with its `--operation-id`. `conflict` (the record changed since the caller read
|
|
242
|
+
it, as a `--revision` check finds) is never safe either: read the record again and decide on the new copy.
|
|
241
243
|
- **Exceptions:** cli-base's `retryOf`, used on its own, also counts a local write as a write, so it is stricter
|
|
242
244
|
than cli-core's runner.
|
|
243
245
|
- **Applies to:** all CLIs. **Since:** cli-base 0.41.0. **Rulings:** none.
|
|
@@ -358,10 +358,9 @@ A messenger's keys come from its resources (`messages`, `chats`). Any other CLI
|
|
|
358
358
|
`ask` runs without a question, and each reaches people other than the owner. gcal adds its own keys for its generated
|
|
359
359
|
`api <method>` commands: sharing a calendar is `ask` (NEED-1857), and emptying the main calendar
|
|
360
360
|
(`api.calendar.calendars.clear`) is `deny`.
|
|
361
|
-
- **Exceptions:** known gaps, as of 2026-
|
|
362
|
-
-
|
|
363
|
-
|
|
364
|
-
memo uses cli-core's permissions for the shared commands only. None of them declares `commandPermissions` yet.
|
|
361
|
+
- **Exceptions:** known gaps, as of 2026-11-03: drive keeps its own policy and `--read-only` flag, zoom reads its own
|
|
362
|
+
`remote-meetings` key with `levelFor`, and memo uses cli-core's permissions for the shared commands only. None of
|
|
363
|
+
them declares `commandPermissions` yet.
|
|
365
364
|
- **Applies to:** every CLI that writes and is not a messenger. **Since:** cli-base 0.64.0 (the levels and the shared
|
|
366
365
|
keys on every path: cli-base 0.65.0). **Rulings:** NEED-1857, NEED-1883.
|
|
367
366
|
|
|
@@ -199,7 +199,7 @@ fixtures, `scripts/`, `.github/`, `bin/release`, `config/`, `lint/`, `contracts/
|
|
|
199
199
|
`scripts/smoke.ts` does, checks only that npm packs what `files` says, and cannot fail on a wrong `files` list.
|
|
200
200
|
`npm pack --dry-run` in gcal-cli packed 256 files on 2026-10-27, `src/`, `scripts/`, `.github/`, `config/`,
|
|
201
201
|
`contracts/`, `lint/` and `template.json` among them, because its `files` list named them; its check could not see
|
|
202
|
-
it.
|
|
202
|
+
it. gcal's check now uses a fixed list of its own (`scripts/smoke.ts`). The check this rule needs
|
|
203
203
|
refuses the forbidden paths above whatever `files` says: cli-template's `scripts/smoke.ts` does since
|
|
204
204
|
[cli-template#85](https://github.com/WireCatLabs/cli-template/pull/85). `scripts/` is a seed that `upgrade` never
|
|
205
205
|
changes, so a CLI made before it copies the check by hand.
|
package/docs/standards/time.md
CHANGED
|
@@ -80,10 +80,8 @@ local midnight. The answer gives the window it used, in UTC.
|
|
|
80
80
|
not included), matches by comparing local dates, never UTC moments: it is in the window when its dates overlap the
|
|
81
81
|
local dates the window covers. So in `Europe/Madrid` a filter for 1 September returns the all-day event of
|
|
82
82
|
1 September and not the one of 31 August, whatever zone the event's calendar is in.
|
|
83
|
-
- **Exceptions:** none.
|
|
84
|
-
(`src/store/sqlite/calendar.ts
|
|
85
|
-
all-day events too, and gcal stores an all-day event's start as midnight in its calendar's zone. The two agree
|
|
86
|
-
when the calendar's zone is the zone in use, and miss by a day at the edges when it is not.
|
|
83
|
+
- **Exceptions:** none. cli-core's event filter matches an all-day event by its local dates
|
|
84
|
+
(`src/store/sqlite/calendar-events.ts`), and `events list` answers the window in `meta.window`.
|
|
87
85
|
- **Applies to:** all CLIs. **Since:** cli-base 0.48.0. **Rulings:** none.
|
|
88
86
|
|
|
89
87
|
### WC-TIME-6 · Run the tests in a zone that is not UTC, and prove stored values do not depend on it
|
|
@@ -90,11 +90,13 @@ plus community itself. cli-template follows every rule here as a CLI does
|
|
|
90
90
|
`scripts/smoke.ts` runs the built exports with plain assertions, and the Bun job runs it
|
|
91
91
|
(`pnpm build && pnpm smoke:bun`). A new entry point gets a line there. Node 22 runs in the Windows install matrix of
|
|
92
92
|
tg, max and wa, in zoom's full validation, in cli-core's old-Node job and in cli-template's `node-22` job (Node
|
|
93
|
-
22.16.0, from cli-template 0.11.0), which a CLI made from the template gets with `upgrade
|
|
93
|
+
22.16.0, from cli-template 0.11.0), which a CLI made from the template gets with `upgrade`; gcal has its own
|
|
94
|
+
`node-22` job.
|
|
94
95
|
- **Exceptions:** cli-docs and community publish no runtime package. tg needs `^22.19.0`. memo, drive, cli-base and
|
|
95
|
-
the CLIs made from a template before 0.11.0 do not run Node 22 in any workflow yet
|
|
96
|
+
the CLIs made from a template before 0.11.0 do not run Node 22 in any workflow yet.
|
|
96
97
|
cli-base's own `engines` is still `>=22`. The unit suite runs on Windows and macOS for tg, max and drive
|
|
97
|
-
(`platforms: true`); zoom and memo still fail tests there,
|
|
98
|
+
(`platforms: true`), and for gcal at release, monthly and on a manual run; zoom and memo still fail tests there,
|
|
99
|
+
so theirs is off.
|
|
98
100
|
- **Applies to:** the CLIs, cli-core and cli-base. **Since:** cli-base 0.41.0. **Rulings:** NEED-11.
|
|
99
101
|
|
|
100
102
|
### WC-TOOL-7 · Let Biome format the code, and do not format by hand
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-base",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.76.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",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"files": [
|
|
21
21
|
"dist",
|
|
22
22
|
"config",
|
|
23
|
+
"constants.json",
|
|
23
24
|
"docs/standards",
|
|
24
25
|
"README.md",
|
|
25
26
|
"CHANGELOG.md",
|