@wirecat/cli-base 0.65.4 → 0.66.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 +43 -0
- package/README.md +4 -0
- package/dist/credentials.d.ts +10 -6
- package/dist/credentials.js +23 -14
- package/dist/dev/main.d.ts +5 -0
- package/dist/dev/main.js +31 -16
- package/dist/dev/wcat-lint.d.ts +15 -0
- package/dist/dev/wcat-lint.js +42 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/installer.d.ts +7 -0
- package/dist/installer.js +17 -0
- package/dist/standards/errors.js +6 -0
- package/dist/update/index.d.ts +2 -7
- package/dist/update/index.js +1 -17
- package/dist/wcat/describe.d.ts +22 -0
- package/dist/wcat/describe.js +31 -0
- package/dist/wcat/dispatch.d.ts +43 -0
- package/dist/wcat/dispatch.js +184 -0
- package/dist/wcat/index.d.ts +16 -0
- package/dist/wcat/index.js +16 -0
- package/dist/wcat/launcher.d.ts +40 -0
- package/dist/wcat/launcher.js +109 -0
- package/dist/wcat/names.d.ts +16 -0
- package/dist/wcat/names.js +27 -0
- package/dist/wcat/path.d.ts +15 -0
- package/dist/wcat/path.js +83 -0
- package/dist/wcat/probe.d.ts +11 -0
- package/dist/wcat/probe.js +58 -0
- package/dist/wcat/shim.d.ts +45 -0
- package/dist/wcat/shim.js +104 -0
- package/dist/wcat/shortcuts.d.ts +31 -0
- package/dist/wcat/shortcuts.js +105 -0
- package/dist/wcat/start.d.ts +38 -0
- package/dist/wcat/start.js +45 -0
- package/docs/standards/README.md +1 -1
- package/docs/standards/commands.md +20 -1
- package/docs/standards/new-cli.md +26 -0
- package/docs/standards/security.md +29 -15
- package/package.json +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,49 @@ 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.66.0 — 31.10.2026
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- **`wcat`, one command that runs every tool: `wcat tg …` runs `wcat-tg …`** (`@wirecat/cli-base/wcat`). Another
|
|
16
|
+
program named `tg` or `drive` can sit earlier on PATH, and an agent or a script then runs it without any sign; a
|
|
17
|
+
`wcat-<name>` name is the tool's alone. Any `wcat-<name>` program on PATH joins, ours or not. `wcat --help` lists
|
|
18
|
+
what is installed with each tool's description, the WireCat tools that are not, with their install commands, and
|
|
19
|
+
warns when two programs share a name; `--json` prints the list. Built-in names (`help`, `version`, `shortcuts`,
|
|
20
|
+
`doctor`, `update`, `upgrade`, `app`) always belong to `wcat`. An unknown tool is `not_found` with the new reason
|
|
21
|
+
`tool.not-installed`. Nothing changes for a CLI until it calls `wcatStart`.
|
|
22
|
+
- **`wcatStart(tool)` from `@wirecat/cli-base/wcat/start`, the first call in a CLI's bin.** It answers
|
|
23
|
+
`--wcat-describe` with the tool's name, version, description and docs page, runs `wcat` when the `wcat` launcher
|
|
24
|
+
starts the tool, and on any other start makes sure the `wcat` launcher in the tool's global bin folder works. The
|
|
25
|
+
launcher is a small marked script that starts an installed `wcat-<tool>` by name (a `.cmd` and `.ps1` pair on
|
|
26
|
+
Windows), never a link into a package folder, which pnpm renames on every upgrade. A file named `wcat` that
|
|
27
|
+
cli-base did not write is never touched. When the launcher works, the check costs a few `stat` calls; it loads
|
|
28
|
+
only Node built-ins. Call it before `startCli`, and set `process.exitCode` to the `code` it returns.
|
|
29
|
+
- **`wcat shortcuts`, and `listShortcuts`, `addShortcuts` and `removeShortcuts` for a program.** A tool's short name,
|
|
30
|
+
such as `tg`, is made only when no program on PATH has it, and only shortcuts cli-base made are removed. A short
|
|
31
|
+
name the tool's package installs itself shows as `package` and is left to the package manager.
|
|
32
|
+
- **`RESERVED_NAMES` and a `cli-dev lint` warning for joining `wcat`** ([WC-CMD-29](docs/standards/commands.md#wc-cmd-29--answer---wcat-describe-and-take-no-name-wirecat-reserves)).
|
|
33
|
+
The lint warns when a `"bin"` key does not start with `wcat-`, when a package outside `@wirecat/` takes a reserved
|
|
34
|
+
name, and, with `--cli`, when the built CLI does not answer `--wcat-describe`. The warnings do not fail the run:
|
|
35
|
+
every CLI still installs its short name today.
|
|
36
|
+
|
|
37
|
+
## 0.65.5 — 31.10.2026
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- **`movedRunService(app, configDir)` names a moved run's temporary keyring entry**, so a test or an agent can delete
|
|
42
|
+
it by name.
|
|
43
|
+
|
|
44
|
+
### Security
|
|
45
|
+
|
|
46
|
+
- **A moved run no longer replaces the owner's keyring entry.** `session start` in a worktree or with
|
|
47
|
+
`HOME=/tmp/x` overwrote the owner's real sign-in. With the system keyring, `Credentials` in a moved run (`isolated`,
|
|
48
|
+
or a moved `HOME` or XDG folder) now writes a temporary entry, `<app>:<absolute config folder>`; reads it first and
|
|
49
|
+
the owner's `<app>` entry when it has none; and `remove` deletes only the temporary entry, so `session end` cleans up.
|
|
50
|
+
A token refreshed in a moved run is saved in the temporary entry too, so a run that never ends its session leaves
|
|
51
|
+
one behind ([WC-SECR-24](docs/standards/security.md#wc-secr-24--a-run-with-moved-folders-shares-the-owners-keyring-entry-and-never-deletes-it)).
|
|
52
|
+
A run with the default folders, and a test with `memoryKeyring()`, are unchanged.
|
|
53
|
+
|
|
11
54
|
## 0.65.4 — 31.10.2026
|
|
12
55
|
|
|
13
56
|
### Fixed
|
package/README.md
CHANGED
|
@@ -187,6 +187,10 @@ operation and never falls back to `any`. 64-bit integers stay exact.
|
|
|
187
187
|
- **Update checks that never update unasked.** Which package manager installed the CLI, the command that updates it,
|
|
188
188
|
and a daily notice only when a person is at a terminal. A tool holding someone's credentials does not change itself.
|
|
189
189
|
- **Release checks** every CLI shares: changelog shape, dead links, package contents, versions in step.
|
|
190
|
+
- **`wcat`, one command for every tool.** `wcat tg …` runs `wcat-tg …`, like `git foo` runs `git-foo`, and any
|
|
191
|
+
`wcat-<name>` program on PATH joins. `wcat --help` lists what is installed with each tool's description;
|
|
192
|
+
`wcat shortcuts add` gives a tool its short name only where no other program has it. Each tool keeps the `wcat`
|
|
193
|
+
launcher working on start, at the cost of a few `stat` calls.
|
|
190
194
|
|
|
191
195
|
## Engineering
|
|
192
196
|
|
package/dist/credentials.d.ts
CHANGED
|
@@ -14,10 +14,9 @@ export interface CredentialsOptions {
|
|
|
14
14
|
/**
|
|
15
15
|
* **True when the run's folders are not the owner's own** — pass `isMovedRun(…)`.
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* moved even when this is false.
|
|
17
|
+
* With the system keyring the run reads the owner's entry (`service`) but never replaces or deletes it: `write`
|
|
18
|
+
* goes to a temporary entry, `movedRunService(service, configDir)`, which `read` prefers and `remove` deletes
|
|
19
|
+
* (WC-SECR-24). A moved `HOME` or XDG folder in `env` counts as moved even when this is false.
|
|
21
20
|
*/
|
|
22
21
|
isolated?: boolean;
|
|
23
22
|
/** Read before the keyring, for CI. */
|
|
@@ -30,10 +29,15 @@ export interface CredentialsOptions {
|
|
|
30
29
|
warn?: (message: string) => void;
|
|
31
30
|
}
|
|
32
31
|
/**
|
|
33
|
-
* The
|
|
34
|
-
*
|
|
32
|
+
* The app's shared keyring service, which a moved run reads too. The other two arguments are ignored and stay only so
|
|
33
|
+
* that callers written against 0.65.0 still compile.
|
|
35
34
|
*/
|
|
36
35
|
export declare const keyringService: (service: string, _configDir?: string, _isolated?: boolean) => string;
|
|
36
|
+
/**
|
|
37
|
+
* The temporary keyring service a moved run writes instead of the owner's entry: `<app>:<absolute config folder>`, the
|
|
38
|
+
* shape `bin/keyring-dedupe` already finds and removes.
|
|
39
|
+
*/
|
|
40
|
+
export declare const movedRunService: (service: string, configDir: string) => string;
|
|
37
41
|
/**
|
|
38
42
|
* Environment first, then the OS keyring, then a file.
|
|
39
43
|
*
|
package/dist/credentials.js
CHANGED
|
@@ -1,15 +1,20 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import { join } from "node:path";
|
|
2
|
+
import { join, resolve } from "node:path";
|
|
3
3
|
import { writeSecurely } from "./config.js";
|
|
4
4
|
import { CliError } from "./errors.js";
|
|
5
5
|
import { systemKeyring } from "./keyring.js";
|
|
6
6
|
import { homeIsMoved } from "./paths.js";
|
|
7
7
|
export const CREDENTIAL_STORAGE = ["auto", "keyring", "file"];
|
|
8
8
|
/**
|
|
9
|
-
* The
|
|
10
|
-
*
|
|
9
|
+
* The app's shared keyring service, which a moved run reads too. The other two arguments are ignored and stay only so
|
|
10
|
+
* that callers written against 0.65.0 still compile.
|
|
11
11
|
*/
|
|
12
12
|
export const keyringService = (service, _configDir, _isolated) => service;
|
|
13
|
+
/**
|
|
14
|
+
* The temporary keyring service a moved run writes instead of the owner's entry: `<app>:<absolute config folder>`, the
|
|
15
|
+
* shape `bin/keyring-dedupe` already finds and removes.
|
|
16
|
+
*/
|
|
17
|
+
export const movedRunService = (service, configDir) => `${service}:${resolve(configDir)}`;
|
|
13
18
|
/**
|
|
14
19
|
* Environment first, then the OS keyring, then a file.
|
|
15
20
|
*
|
|
@@ -26,7 +31,8 @@ export class Credentials {
|
|
|
26
31
|
#envVar;
|
|
27
32
|
#fileName;
|
|
28
33
|
#warn;
|
|
29
|
-
|
|
34
|
+
/** Set only for a moved run with the system keyring. */
|
|
35
|
+
#temporaryService;
|
|
30
36
|
#warned = false;
|
|
31
37
|
constructor(options) {
|
|
32
38
|
this.#configDir = options.configDir;
|
|
@@ -37,15 +43,16 @@ export class Credentials {
|
|
|
37
43
|
this.#fileName = options.fileName ?? "credentials.json";
|
|
38
44
|
this.#warn = options.warn ?? ((message) => process.stderr.write(`${message}\n`));
|
|
39
45
|
this.#service = options.service;
|
|
40
|
-
this.#
|
|
41
|
-
|
|
46
|
+
const moved = this.#keyring === systemKeyring && (options.isolated === true || homeIsMoved({ env: this.#env }));
|
|
47
|
+
this.#temporaryService = moved ? movedRunService(options.service, options.configDir) : undefined;
|
|
42
48
|
}
|
|
43
49
|
read(account) {
|
|
44
50
|
const fromEnv = this.#envVar ? this.#env[this.#envVar]?.trim() : undefined;
|
|
45
51
|
if (fromEnv)
|
|
46
52
|
return { secret: fromEnv, source: "environment" };
|
|
47
53
|
if (this.#storage !== "file") {
|
|
48
|
-
const
|
|
54
|
+
const temporary = this.#temporaryService;
|
|
55
|
+
const fromKeyring = this.#tryKeyring(() => (temporary && this.#keyring.get(temporary, account)) || this.#keyring.get(this.#service, account));
|
|
49
56
|
if (fromKeyring)
|
|
50
57
|
return { secret: fromKeyring, source: "keyring" };
|
|
51
58
|
}
|
|
@@ -55,7 +62,7 @@ export class Credentials {
|
|
|
55
62
|
write(account, secret) {
|
|
56
63
|
if (this.#storage !== "file") {
|
|
57
64
|
const stored = this.#tryKeyring(() => {
|
|
58
|
-
this.#keyring.set(this.#service, account, secret);
|
|
65
|
+
this.#keyring.set(this.#temporaryService ?? this.#service, account, secret);
|
|
59
66
|
return true;
|
|
60
67
|
});
|
|
61
68
|
if (stored)
|
|
@@ -72,12 +79,14 @@ export class Credentials {
|
|
|
72
79
|
}
|
|
73
80
|
remove(account) {
|
|
74
81
|
const removed = [];
|
|
75
|
-
if (this.#storage !== "file"
|
|
76
|
-
this.#
|
|
77
|
-
"
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
82
|
+
if (this.#storage !== "file") {
|
|
83
|
+
if (this.#tryKeyring(() => this.#keyring.delete(this.#temporaryService ?? this.#service, account))) {
|
|
84
|
+
removed.push("keyring");
|
|
85
|
+
}
|
|
86
|
+
if (this.#temporaryService) {
|
|
87
|
+
this.#warn(`kept ${account} in the OS keyring: it is the owner's sign-in, shared with every run, and a run with moved ` +
|
|
88
|
+
"folders never removes it");
|
|
89
|
+
}
|
|
81
90
|
}
|
|
82
91
|
const store = this.#readFile();
|
|
83
92
|
if (store?.[account] !== undefined) {
|
package/dist/dev/main.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import spawn from "cross-spawn";
|
|
1
2
|
import { type RunLogProbe } from "./lint.js";
|
|
2
3
|
export interface DevIo {
|
|
3
4
|
cwd: string;
|
|
@@ -12,4 +13,8 @@ export interface DevIo {
|
|
|
12
13
|
* `XDG_STATE_HOME` would point it at the owner's real runs.
|
|
13
14
|
*/
|
|
14
15
|
export declare const probeRunLog: (cwd: string, bin: string, environment?: NodeJS.ProcessEnv) => RunLogProbe;
|
|
16
|
+
type HomeCall = (args: string[]) => ReturnType<typeof spawn.sync>;
|
|
17
|
+
/** Runs the built CLI in a new empty home with only PATH from this environment, and removes the home after. */
|
|
18
|
+
export declare const inEmptyHome: <T>(cwd: string, bin: string, environment: NodeJS.ProcessEnv, body: (call: HomeCall) => T) => T;
|
|
15
19
|
export declare const main: (argv: readonly string[], io: DevIo) => Promise<number>;
|
|
20
|
+
export {};
|
package/dist/dev/main.js
CHANGED
|
@@ -25,6 +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
29
|
const USAGE = `usage: cli-dev <command> [options]
|
|
29
30
|
|
|
30
31
|
slow-tests [report] [count] the slowest tests and files (report: coverage/tests.json, count: 20)
|
|
@@ -257,7 +258,25 @@ const PROBE_FLAG = "--cli-dev-run-log-probe";
|
|
|
257
258
|
* kept. The CLI sees a new empty home and nothing else from this environment: a stray `*_STATE_DIR` or
|
|
258
259
|
* `XDG_STATE_HOME` would point it at the owner's real runs.
|
|
259
260
|
*/
|
|
260
|
-
export const probeRunLog = (cwd, bin, environment = process.env) => {
|
|
261
|
+
export const probeRunLog = (cwd, bin, environment = process.env) => inEmptyHome(cwd, bin, environment, (call) => {
|
|
262
|
+
const failed = call([PROBE_FLAG]);
|
|
263
|
+
if (failed.status === 0)
|
|
264
|
+
return { kept: false, detail: `\`${PROBE_FLAG}\` did not fail, so nothing could be kept` };
|
|
265
|
+
const listed = call(["runs", "list", "--json"]);
|
|
266
|
+
if (listed.status !== 0)
|
|
267
|
+
return { kept: false, detail: `\`runs list --json\` exited ${listed.status ?? listed.error?.message}` };
|
|
268
|
+
let items;
|
|
269
|
+
try {
|
|
270
|
+
items = JSON.parse(String(listed.stdout)).items;
|
|
271
|
+
}
|
|
272
|
+
catch {
|
|
273
|
+
return { kept: false, detail: "`runs list --json` did not print JSON" };
|
|
274
|
+
}
|
|
275
|
+
const kept = Array.isArray(items) && items.some((item) => item?.status === "failed");
|
|
276
|
+
return kept ? { kept } : { kept, detail: `\`runs list --json\` shows no failed run after \`${PROBE_FLAG}\`` };
|
|
277
|
+
});
|
|
278
|
+
/** Runs the built CLI in a new empty home with only PATH from this environment, and removes the home after. */
|
|
279
|
+
export const inEmptyHome = (cwd, bin, environment, body) => {
|
|
261
280
|
const home = mkdtempSync(join(tmpdir(), "cli-dev-run-log-"));
|
|
262
281
|
const env = {
|
|
263
282
|
PATH: environment.PATH ?? "",
|
|
@@ -277,21 +296,7 @@ export const probeRunLog = (cwd, bin, environment = process.env) => {
|
|
|
277
296
|
maxBuffer: 16 * 1024 * 1024,
|
|
278
297
|
});
|
|
279
298
|
try {
|
|
280
|
-
|
|
281
|
-
if (failed.status === 0)
|
|
282
|
-
return { kept: false, detail: `\`${PROBE_FLAG}\` did not fail, so nothing could be kept` };
|
|
283
|
-
const listed = call(["runs", "list", "--json"]);
|
|
284
|
-
if (listed.status !== 0)
|
|
285
|
-
return { kept: false, detail: `\`runs list --json\` exited ${listed.status ?? listed.error?.message}` };
|
|
286
|
-
let items;
|
|
287
|
-
try {
|
|
288
|
-
items = JSON.parse(String(listed.stdout)).items;
|
|
289
|
-
}
|
|
290
|
-
catch {
|
|
291
|
-
return { kept: false, detail: "`runs list --json` did not print JSON" };
|
|
292
|
-
}
|
|
293
|
-
const kept = Array.isArray(items) && items.some((item) => item?.status === "failed");
|
|
294
|
-
return kept ? { kept } : { kept, detail: `\`runs list --json\` shows no failed run after \`${PROBE_FLAG}\`` };
|
|
299
|
+
return body(call);
|
|
295
300
|
}
|
|
296
301
|
finally {
|
|
297
302
|
rmSync(home, { recursive: true, force: true });
|
|
@@ -406,8 +411,18 @@ const lintRun = (args, io) => {
|
|
|
406
411
|
if (verdict.overBudget)
|
|
407
412
|
problems.push(`${overrides.length} overrides; the budget is ${budget}`);
|
|
408
413
|
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 wcatWarnings(io.cwd, values.cli))
|
|
415
|
+
io.err(`warning ${warning.rule} ${warning.target}: ${warning.message}`);
|
|
409
416
|
return report(io, problems, "lint: ok");
|
|
410
417
|
};
|
|
418
|
+
const wcatWarnings = (cwd, cli) => {
|
|
419
|
+
const pkgPath = join(cwd, "package.json");
|
|
420
|
+
const pkg = existsSync(pkgPath) ? readJson(pkgPath) : {};
|
|
421
|
+
return [
|
|
422
|
+
...binNameWarnings(pkg),
|
|
423
|
+
...(cli ? describeWarnings(inEmptyHome(cwd, cli, process.env, (call) => call(["--wcat-describe"]))) : []),
|
|
424
|
+
];
|
|
425
|
+
};
|
|
411
426
|
const byCli = (pairs, flag) => new Map((pairs ?? []).map((pair) => {
|
|
412
427
|
const [cli, path] = pair.split(/=(.*)/s);
|
|
413
428
|
if (!cli || !path)
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
export interface WcatWarning {
|
|
2
|
+
rule: "wcat-bin-name" | "wcat-describe";
|
|
3
|
+
target: string;
|
|
4
|
+
message: string;
|
|
5
|
+
}
|
|
6
|
+
/** Each command is `wcat-<name>`, and a package outside `@wirecat/` takes no reserved name. */
|
|
7
|
+
export declare const binNameWarnings: (pkg: {
|
|
8
|
+
name?: unknown;
|
|
9
|
+
bin?: unknown;
|
|
10
|
+
}) => WcatWarning[];
|
|
11
|
+
/** The built CLI answers `--wcat-describe` with its JSON description. */
|
|
12
|
+
export declare const describeWarnings: (answer: {
|
|
13
|
+
status: number | null;
|
|
14
|
+
stdout: unknown;
|
|
15
|
+
}) => WcatWarning[];
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cli-dev lint`'s checks for joining `wcat` (WC-CMD-29). They warn and never fail the run: every CLI still installs
|
|
3
|
+
* its short name until it moves to `wcat-<name>`, and the warning becomes a failure once they have.
|
|
4
|
+
*/
|
|
5
|
+
import { parseDescribe } from "../wcat/describe.js";
|
|
6
|
+
import { DESCRIBE_FLAG, PROGRAM_PREFIX, RESERVED_NAMES, TOOL_NAME } from "../wcat/names.js";
|
|
7
|
+
const WIRECAT_SCOPE = "@wirecat/";
|
|
8
|
+
/** The commands a `package.json` installs: its `"bin"` keys, or the package's own name for a `"bin"` string. */
|
|
9
|
+
const binNames = (pkg) => {
|
|
10
|
+
if (typeof pkg.bin === "string")
|
|
11
|
+
return typeof pkg.name === "string" ? [pkg.name.replace(/^@[^/]+\//, "")] : [];
|
|
12
|
+
return typeof pkg.bin === "object" && pkg.bin !== null ? Object.keys(pkg.bin) : [];
|
|
13
|
+
};
|
|
14
|
+
/** Each command is `wcat-<name>`, and a package outside `@wirecat/` takes no reserved name. */
|
|
15
|
+
export const binNameWarnings = (pkg) => binNames(pkg).flatMap((command) => {
|
|
16
|
+
const target = `package.json "bin" ${command}`;
|
|
17
|
+
if (!command.startsWith(PROGRAM_PREFIX))
|
|
18
|
+
return [
|
|
19
|
+
{
|
|
20
|
+
rule: "wcat-bin-name",
|
|
21
|
+
target,
|
|
22
|
+
message: `install the command as ${PROGRAM_PREFIX}${command}, so no other program on PATH has its name`,
|
|
23
|
+
},
|
|
24
|
+
];
|
|
25
|
+
const name = command.slice(PROGRAM_PREFIX.length);
|
|
26
|
+
if (!TOOL_NAME.test(name))
|
|
27
|
+
return [{ rule: "wcat-bin-name", target, message: "the name after wcat- is lower-case letters, digits and -" }];
|
|
28
|
+
const ours = typeof pkg.name === "string" && pkg.name.startsWith(WIRECAT_SCOPE);
|
|
29
|
+
if (!ours && RESERVED_NAMES.includes(name))
|
|
30
|
+
return [{ rule: "wcat-bin-name", target, message: `${name} is reserved for WireCat's own tools or wcat itself` }];
|
|
31
|
+
return [];
|
|
32
|
+
});
|
|
33
|
+
/** The built CLI answers `--wcat-describe` with its JSON description. */
|
|
34
|
+
export const describeWarnings = (answer) => answer.status === 0 && parseDescribe(String(answer.stdout))
|
|
35
|
+
? []
|
|
36
|
+
: [
|
|
37
|
+
{
|
|
38
|
+
rule: "wcat-describe",
|
|
39
|
+
target: DESCRIBE_FLAG,
|
|
40
|
+
message: `the CLI does not answer ${DESCRIBE_FLAG}: call wcatStart from @wirecat/cli-base/wcat/start first in its bin`,
|
|
41
|
+
},
|
|
42
|
+
];
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
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
3
|
export type { CredentialSource, CredentialStorage, CredentialsOptions, StoredCredential } from "./credentials.js";
|
|
4
|
-
export { CREDENTIAL_STORAGE, Credentials, keyringService } from "./credentials.js";
|
|
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";
|
|
7
7
|
export { EXIT_CODES, exitCodeFor, GENERIC_FAILURE } from "./exit-codes.js";
|
package/dist/index.js
CHANGED
|
@@ -1,6 +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 { CREDENTIAL_STORAGE, Credentials, keyringService } from "./credentials.js";
|
|
3
|
+
export { CREDENTIAL_STORAGE, Credentials, keyringService, movedRunService } from "./credentials.js";
|
|
4
4
|
export { createDeadline } from "./deadline.js";
|
|
5
5
|
export { CliError, errorCodes, isCliError } from "./errors.js";
|
|
6
6
|
export { EXIT_CODES, exitCodeFor, GENERIC_FAILURE } from "./exit-codes.js";
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export type Installer = "pnpm" | "npm" | "bun" | "npx" | "checkout" | "unknown";
|
|
2
|
+
/**
|
|
3
|
+
* Read from where the running file really lives — `realpath` of the script, not `which`: the
|
|
4
|
+
* command on `PATH` is a shim or a link, and only its target says who installed it. Paths measured
|
|
5
|
+
* on 2026-09-24 by installing into throwaway prefixes.
|
|
6
|
+
*/
|
|
7
|
+
export declare const installerOf: (scriptPath: string) => Installer;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read from where the running file really lives — `realpath` of the script, not `which`: the
|
|
3
|
+
* command on `PATH` is a shim or a link, and only its target says who installed it. Paths measured
|
|
4
|
+
* on 2026-09-24 by installing into throwaway prefixes.
|
|
5
|
+
*/
|
|
6
|
+
export const installerOf = (scriptPath) => {
|
|
7
|
+
const path = scriptPath.replaceAll("\\", "/");
|
|
8
|
+
if (path.includes("/_npx/"))
|
|
9
|
+
return "npx";
|
|
10
|
+
if (path.includes("/install/global/node_modules/"))
|
|
11
|
+
return "bun";
|
|
12
|
+
if (/\/\.?pnpm\//.test(path))
|
|
13
|
+
return "pnpm";
|
|
14
|
+
if (path.includes("/node_modules/"))
|
|
15
|
+
return "npm";
|
|
16
|
+
return path.includes("/dist/") ? "checkout" : "unknown";
|
|
17
|
+
};
|
package/dist/standards/errors.js
CHANGED
|
@@ -61,6 +61,12 @@ export const BASE_REASONS = defineReasons([
|
|
|
61
61
|
{ reason: "auth.failed", code: "authentication_error", message: "the sign-in did not work", details: [] },
|
|
62
62
|
{ reason: "access.denied", code: "permission_error", message: "this account may not do that", details: [] },
|
|
63
63
|
{ reason: "item.not-found", code: "not_found", message: "nothing found by that name", details: [] },
|
|
64
|
+
{
|
|
65
|
+
reason: "tool.not-installed",
|
|
66
|
+
code: "not_found",
|
|
67
|
+
message: "no wcat-<name> program by that name is on PATH",
|
|
68
|
+
details: ["tool", "installed"],
|
|
69
|
+
},
|
|
64
70
|
{
|
|
65
71
|
reason: "confirmation.required",
|
|
66
72
|
code: "confirmation_required",
|
package/dist/update/index.d.ts
CHANGED
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
export { executableEnvironment, executableOnPath } from "../executable.js";
|
|
2
2
|
import * as v from "valibot";
|
|
3
3
|
import type { FetchLike } from "../http/index.js";
|
|
4
|
-
export type Installer
|
|
5
|
-
|
|
6
|
-
* Read from where the running file really lives — `realpath` of the script, not `which`: the
|
|
7
|
-
* command on `PATH` is a shim or a link, and only its target says who installed it. Paths measured
|
|
8
|
-
* on 2026-09-24 by installing into throwaway prefixes.
|
|
9
|
-
*/
|
|
10
|
-
export declare const installerOf: (scriptPath: string) => Installer;
|
|
4
|
+
export { type Installer, installerOf } from "../installer.js";
|
|
5
|
+
import type { Installer } from "../installer.js";
|
|
11
6
|
/** The argv that updates a global install, or `undefined` where there is nothing to run. */
|
|
12
7
|
export declare const updateCommand: (installer: Installer, packageName: string) => string[] | undefined;
|
|
13
8
|
/** Plain `x.y.z` only: a pre-release is never offered, so it never counts as newer. */
|
package/dist/update/index.js
CHANGED
|
@@ -12,23 +12,7 @@ import { executableEnvironment, executableOnPath } from "../executable.js";
|
|
|
12
12
|
export { executableEnvironment, executableOnPath } from "../executable.js";
|
|
13
13
|
import * as v from "valibot";
|
|
14
14
|
import { loadConfigFile, saveConfigFile } from "../config.js";
|
|
15
|
-
|
|
16
|
-
* Read from where the running file really lives — `realpath` of the script, not `which`: the
|
|
17
|
-
* command on `PATH` is a shim or a link, and only its target says who installed it. Paths measured
|
|
18
|
-
* on 2026-09-24 by installing into throwaway prefixes.
|
|
19
|
-
*/
|
|
20
|
-
export const installerOf = (scriptPath) => {
|
|
21
|
-
const path = scriptPath.replaceAll("\\", "/");
|
|
22
|
-
if (path.includes("/_npx/"))
|
|
23
|
-
return "npx";
|
|
24
|
-
if (path.includes("/install/global/node_modules/"))
|
|
25
|
-
return "bun";
|
|
26
|
-
if (/\/\.?pnpm\//.test(path))
|
|
27
|
-
return "pnpm";
|
|
28
|
-
if (path.includes("/node_modules/"))
|
|
29
|
-
return "npm";
|
|
30
|
-
return path.includes("/dist/") ? "checkout" : "unknown";
|
|
31
|
-
};
|
|
15
|
+
export { installerOf } from "../installer.js";
|
|
32
16
|
/** The argv that updates a global install, or `undefined` where there is nothing to run. */
|
|
33
17
|
export const updateCommand = (installer, packageName) => {
|
|
34
18
|
const target = `${packageName}@latest`;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/** What a tool says about itself to `wcat`, from `wcat-<name> --wcat-describe` or the tool's own metadata. */
|
|
2
|
+
export interface WcatTool {
|
|
3
|
+
/** The name in `wcat <name>`, and the short command: `tg`. */
|
|
4
|
+
command: string;
|
|
5
|
+
version: string;
|
|
6
|
+
/** One line: what the tool reads or does. */
|
|
7
|
+
description: string;
|
|
8
|
+
/** The tool's documentation page. */
|
|
9
|
+
docs?: string;
|
|
10
|
+
}
|
|
11
|
+
/** Format of the `--wcat-describe` answer; a later format adds keys and keeps these. */
|
|
12
|
+
export declare const DESCRIBE_FORMAT = 1;
|
|
13
|
+
export interface DescribeAnswer {
|
|
14
|
+
wcat_describe: number;
|
|
15
|
+
name: string;
|
|
16
|
+
version: string;
|
|
17
|
+
description: string;
|
|
18
|
+
docs_url?: string;
|
|
19
|
+
}
|
|
20
|
+
export declare const describeAnswer: (tool: WcatTool) => DescribeAnswer;
|
|
21
|
+
/** A `--wcat-describe` answer, or `undefined` for anything else: the program is listed either way. */
|
|
22
|
+
export declare const parseDescribe: (text: string) => Omit<WcatTool, "command"> | undefined;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { singleLine } from "../sanitize.js";
|
|
2
|
+
/** Format of the `--wcat-describe` answer; a later format adds keys and keeps these. */
|
|
3
|
+
export const DESCRIBE_FORMAT = 1;
|
|
4
|
+
export const describeAnswer = (tool) => ({
|
|
5
|
+
wcat_describe: DESCRIBE_FORMAT,
|
|
6
|
+
name: tool.command,
|
|
7
|
+
version: tool.version,
|
|
8
|
+
description: tool.description,
|
|
9
|
+
...(tool.docs ? { docs_url: tool.docs } : {}),
|
|
10
|
+
});
|
|
11
|
+
const MAX_TEXT = 200;
|
|
12
|
+
const clean = (value) => typeof value === "string" && value.trim() !== "" ? singleLine(value.trim()).slice(0, MAX_TEXT) : undefined;
|
|
13
|
+
/** A `--wcat-describe` answer, or `undefined` for anything else: the program is listed either way. */
|
|
14
|
+
export const parseDescribe = (text) => {
|
|
15
|
+
let answer;
|
|
16
|
+
try {
|
|
17
|
+
answer = JSON.parse(text);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return undefined;
|
|
21
|
+
}
|
|
22
|
+
if (typeof answer !== "object" || answer === null || !("wcat_describe" in answer))
|
|
23
|
+
return undefined;
|
|
24
|
+
const { version, description, docs_url: docs } = answer;
|
|
25
|
+
const url = clean(docs);
|
|
26
|
+
return {
|
|
27
|
+
version: clean(version) ?? "",
|
|
28
|
+
description: clean(description) ?? "",
|
|
29
|
+
...(url && /^https?:\/\//.test(url) ? { docs: url } : {}),
|
|
30
|
+
};
|
|
31
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import type { WcatTool } from "./describe.js";
|
|
2
|
+
import { type SearchEnv } from "./path.js";
|
|
3
|
+
interface Output {
|
|
4
|
+
write: (text: string) => unknown;
|
|
5
|
+
isTTY?: boolean;
|
|
6
|
+
}
|
|
7
|
+
export interface WcatContext extends SearchEnv {
|
|
8
|
+
stdout: Output;
|
|
9
|
+
stderr: Output;
|
|
10
|
+
/** The tool running the dispatcher: described from its metadata, without starting it again. */
|
|
11
|
+
self?: WcatTool;
|
|
12
|
+
/** How long one `--wcat-describe` may take; 2 s when left out. */
|
|
13
|
+
describeTimeoutMs?: number;
|
|
14
|
+
/** Where SIGTERM and SIGHUP arrive, to pass on to the tool; `process` when left out. */
|
|
15
|
+
signals?: Pick<NodeJS.Process, "on" | "off">;
|
|
16
|
+
}
|
|
17
|
+
export interface ListedTool {
|
|
18
|
+
name: string;
|
|
19
|
+
installed: boolean;
|
|
20
|
+
/** The program `wcat <name>` starts. */
|
|
21
|
+
path?: string;
|
|
22
|
+
version?: string;
|
|
23
|
+
description?: string;
|
|
24
|
+
docs_url?: string;
|
|
25
|
+
/** Other programs with the same name, later on PATH: `wcat` never runs them. */
|
|
26
|
+
shadowed?: string[];
|
|
27
|
+
package?: string;
|
|
28
|
+
install?: string;
|
|
29
|
+
}
|
|
30
|
+
/** Every `wcat-<name>` on PATH with what it says about itself, then WireCat's own tools that are not installed. */
|
|
31
|
+
export declare const listTools: (context: WcatContext) => Promise<ListedTool[]>;
|
|
32
|
+
/**
|
|
33
|
+
* Starts `wcat-<name>` with the rest of the words, the same streams and environment, and answers its exit code.
|
|
34
|
+
* SIGTERM and SIGHUP are passed on. Ctrl-C reaches the tool from the terminal by itself, so SIGINT is only kept from
|
|
35
|
+
* ending `wcat` before the tool does.
|
|
36
|
+
*/
|
|
37
|
+
export declare const runTool: (path: string, args: readonly string[], context: WcatContext) => Promise<number>;
|
|
38
|
+
/**
|
|
39
|
+
* `wcat` itself: the words after `wcat`. Built-in commands come first, so a `wcat-<name>` program can never take
|
|
40
|
+
* one over; anything else names a tool, and the rest of the line is that tool's.
|
|
41
|
+
*/
|
|
42
|
+
export declare const runWcat: (args: readonly string[], context: WcatContext) => Promise<number>;
|
|
43
|
+
export {};
|