@wirecat/cli-base 0.61.3 → 0.62.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 +18 -0
- package/dist/commands/contracts.js +26 -23
- package/dist/commands/registry.d.ts +5 -0
- package/dist/commands/registry.js +14 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,24 @@ 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.62.0 — 26.10.2026
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`describeAlone(command, known)` describes a command as if it had no parent, reusing what `known` already holds.**
|
|
16
|
+
The shared-command check uses it to describe each command once per pass. Keep `known` for one pass only: a map kept
|
|
17
|
+
longer returns descriptions of commands that have since changed.
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **The shared-command check describes each command once per pass, not once for itself and again for every group
|
|
22
|
+
above it.** It runs when a shared command is built and before each command runs, so every command of every CLI paid
|
|
23
|
+
for it: about 60% of tg's setup, even for `tg --version`. On tg's real program one check now takes about 5 ms
|
|
24
|
+
instead of 11, building the program about 35 ms instead of 46, and a warm `run(["--version"])` about 30 ms instead
|
|
25
|
+
of 47 ([measurement](https://github.com/WireCatLabs/cli-testing/blob/main/performance/cli/shared-contract-check/README.md)).
|
|
26
|
+
The check still covers every protected subcommand, with the same errors and overrides, and `sharedContracts` returns
|
|
27
|
+
the same contracts as before.
|
|
28
|
+
|
|
11
29
|
## 0.61.3 — 26.10.2026
|
|
12
30
|
|
|
13
31
|
### Fixed
|
|
@@ -1,25 +1,22 @@
|
|
|
1
1
|
import { isDeepStrictEqual } from "node:util";
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { CliError } from "../errors.js";
|
|
4
|
-
import {
|
|
4
|
+
import { describeAlone } from "./registry.js";
|
|
5
5
|
const protectedCommands = new WeakMap();
|
|
6
6
|
export const hasSharedContract = (command) => protectedCommands.has(command);
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
const described = describeProgram(root)[0];
|
|
12
|
-
command.parent = parent;
|
|
13
|
-
if (!described)
|
|
7
|
+
const hidden = (command) => Boolean(command._hidden);
|
|
8
|
+
/** Shares arrays with the live command and with `known`: clone it before keeping it past the pass. */
|
|
9
|
+
const snapshot = (command, known = new Map(), children) => {
|
|
10
|
+
if (hidden(command))
|
|
14
11
|
throw new Error("a hidden command cannot own a shared contract");
|
|
15
|
-
const { commands: _, usage: __, path: ___, ...contract } =
|
|
16
|
-
return
|
|
12
|
+
const { commands: _, usage: __, path: ___, ...contract } = describeAlone(command, known);
|
|
13
|
+
return {
|
|
17
14
|
...contract,
|
|
18
15
|
aliases: [...command.aliases()],
|
|
19
16
|
children: (children ?? protectedCommands.get(command)?.originalChildren ?? command.commands)
|
|
20
|
-
.filter((child) => command.commands.includes(child) && !child
|
|
17
|
+
.filter((child) => command.commands.includes(child) && !hidden(child))
|
|
21
18
|
.map((child) => child.name()),
|
|
22
|
-
}
|
|
19
|
+
};
|
|
23
20
|
};
|
|
24
21
|
const same = isDeepStrictEqual;
|
|
25
22
|
const visit = (root, fn) => {
|
|
@@ -36,11 +33,12 @@ const pathOf = (command) => {
|
|
|
36
33
|
const differences = (before, after) => [...new Set([...Object.keys(before), ...Object.keys(after)])].filter((field) => !same(before[field], after[field]));
|
|
37
34
|
export const assertSharedContracts = (root) => {
|
|
38
35
|
const changed = [];
|
|
36
|
+
const known = new Map();
|
|
39
37
|
visit(root, (command) => {
|
|
40
38
|
const protectedContract = protectedCommands.get(command);
|
|
41
39
|
if (!protectedContract)
|
|
42
40
|
return;
|
|
43
|
-
const fields = differences(protectedContract.contract, snapshot(command));
|
|
41
|
+
const fields = differences(protectedContract.contract, snapshot(command, known));
|
|
44
42
|
if (fields.length)
|
|
45
43
|
changed.push({ command: pathOf(command).join(" "), fields });
|
|
46
44
|
});
|
|
@@ -55,9 +53,9 @@ export const overrideSharedContract = (command, reason, change) => {
|
|
|
55
53
|
if (!protectedContract)
|
|
56
54
|
throw new Error("only a core-owned command contract can be overridden");
|
|
57
55
|
assertSharedContracts(command);
|
|
58
|
-
const before = snapshot(command);
|
|
56
|
+
const before = structuredClone(snapshot(command));
|
|
59
57
|
change(command);
|
|
60
|
-
const after = snapshot(command);
|
|
58
|
+
const after = structuredClone(snapshot(command));
|
|
61
59
|
const fields = differences(before, after);
|
|
62
60
|
if (fields.length === 0)
|
|
63
61
|
throw new Error("a shared contract override must change its named command");
|
|
@@ -119,14 +117,18 @@ export const sharedCommand = (factory) => (...args) => {
|
|
|
119
117
|
if (!command.summary() && first.length > 80)
|
|
120
118
|
command.summary(`${first.slice(0, 77).replace(/\s+\S*$/, "")}…`);
|
|
121
119
|
});
|
|
120
|
+
const known = new Map();
|
|
122
121
|
visit(shared, (command) => {
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
122
|
+
if (hidden(command) || protectedCommands.has(command))
|
|
123
|
+
return;
|
|
124
|
+
// One object for both: `contract` is only ever replaced, and `reference` is cloned on the way out.
|
|
125
|
+
const contract = structuredClone(snapshot(command, known));
|
|
126
|
+
protectedCommands.set(command, {
|
|
127
|
+
contract,
|
|
128
|
+
reference: contract,
|
|
129
|
+
overrides: [],
|
|
130
|
+
originalChildren: [...command.commands],
|
|
131
|
+
});
|
|
130
132
|
});
|
|
131
133
|
shared.hook("preAction", () => {
|
|
132
134
|
let root = shared;
|
|
@@ -147,6 +149,7 @@ export const customizeSharedCommand = (reference, reason, build, expectedOverrid
|
|
|
147
149
|
const command = build();
|
|
148
150
|
assertSharedContracts(command);
|
|
149
151
|
const pending = [];
|
|
152
|
+
const known = new Map();
|
|
150
153
|
const edits = [];
|
|
151
154
|
const bindingPath = (own) => {
|
|
152
155
|
const path = [own.name()];
|
|
@@ -163,7 +166,7 @@ export const customizeSharedCommand = (reference, reason, build, expectedOverrid
|
|
|
163
166
|
return;
|
|
164
167
|
if (!protectedCommands.has(own)) {
|
|
165
168
|
const originalChildren = own.commands.filter((child) => inherited.reference.children.includes(child.name()));
|
|
166
|
-
const actual = snapshot(own, originalChildren);
|
|
169
|
+
const actual = structuredClone(snapshot(own, known, originalChildren));
|
|
167
170
|
const overrides = differences(inherited.contract, actual).map((field) => ({
|
|
168
171
|
command: bindingPath(own),
|
|
169
172
|
field,
|
|
@@ -119,3 +119,8 @@ export declare const flatten: (commands: readonly CommandInfo[]) => CommandInfo[
|
|
|
119
119
|
* in snake_case. Hand it to `stepOf` and `errorObjectOf` as `targets`.
|
|
120
120
|
*/
|
|
121
121
|
export declare const stepTargets: (root: Command) => ((command: string) => StepTarget | undefined);
|
|
122
|
+
/**
|
|
123
|
+
* The command as if it had no parent, so it inherits nothing, and its subcommands the same way. `known` holds what one
|
|
124
|
+
* pass has described already, so a whole tree costs one description per command; keep it no longer than that pass.
|
|
125
|
+
*/
|
|
126
|
+
export declare const describeAlone: (command: Command, known: Map<Command, CommandInfo>) => CommandInfo;
|
|
@@ -40,7 +40,19 @@ export const stepTargets = (root) => {
|
|
|
40
40
|
visit(child, [], root.options);
|
|
41
41
|
return (command) => found.get(command);
|
|
42
42
|
};
|
|
43
|
-
|
|
43
|
+
/**
|
|
44
|
+
* The command as if it had no parent, so it inherits nothing, and its subcommands the same way. `known` holds what one
|
|
45
|
+
* pass has described already, so a whole tree costs one description per command; keep it no longer than that pass.
|
|
46
|
+
*/
|
|
47
|
+
export const describeAlone = (command, known) => {
|
|
48
|
+
let found = known.get(command);
|
|
49
|
+
if (!found) {
|
|
50
|
+
found = describe(command, "", [], { offMcp: false, noOffline: false }, known);
|
|
51
|
+
known.set(command, found);
|
|
52
|
+
}
|
|
53
|
+
return found;
|
|
54
|
+
};
|
|
55
|
+
const describe = (command, cli, parents, above, known) => {
|
|
44
56
|
const path = [...parents, command.name()];
|
|
45
57
|
const args = command.registeredArguments.map(describeArgument);
|
|
46
58
|
const options = describeOptions(command);
|
|
@@ -68,7 +80,7 @@ const describe = (command, cli, parents, above) => {
|
|
|
68
80
|
...(visible(command).length > 0 && hasAction(command) ? { runnable: true } : {}),
|
|
69
81
|
arguments: args,
|
|
70
82
|
options,
|
|
71
|
-
commands: visible(command).map((child) => describe(child, cli, path, here)),
|
|
83
|
+
commands: visible(command).map((child) => (known ? describeAlone(child, known) : describe(child, cli, path, here))),
|
|
72
84
|
};
|
|
73
85
|
};
|
|
74
86
|
// Commander has no public getter for the action either; without one, a group prints its help.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@wirecat/cli-base",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.62.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",
|