@zocomputer/cli 0.3.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -6
- package/dist/catalog-cache.d.ts +7 -0
- package/dist/catalog-cache.js +63 -0
- package/dist/catalog.d.ts +2 -0
- package/dist/catalog.js +57 -4
- package/dist/commands.d.ts +14 -0
- package/dist/commands.js +83 -14
- package/dist/generated/commands.js +38 -13
- package/dist/help.d.ts +7 -0
- package/dist/help.js +43 -0
- package/dist/output.d.ts +7 -9
- package/dist/output.js +32 -4
- package/dist/presentation.d.ts +5 -1
- package/dist/presentation.js +91 -42
- package/dist/render.d.ts +26 -0
- package/dist/render.js +145 -0
- package/dist/rpc.js +1 -1
- package/dist/runner.d.ts +0 -1
- package/dist/runner.js +118 -51
- package/dist/storage.d.ts +1 -1
- package/dist/storage.js +2 -2
- package/dist/style.d.ts +40 -0
- package/dist/style.js +74 -0
- package/dist/suggest.d.ts +5 -0
- package/dist/suggest.js +42 -0
- package/dist/update.d.ts +21 -0
- package/dist/update.js +195 -0
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -16,9 +16,9 @@ curl -fsSL https://zo.computer/install.sh | sh
|
|
|
16
16
|
Then open a new terminal and run `zo --version`. It includes Node, verifies the archive, and keeps versions under
|
|
17
17
|
`${XDG_DATA_HOME:-$HOME/.local/share}/zo/cli/` with a managed `~/.local/bin/zo`
|
|
18
18
|
symlink, and adds `~/.local/bin` to your shell's PATH configuration unless it's
|
|
19
|
-
already on PATH.
|
|
20
|
-
control pinned installs, command placement
|
|
21
|
-
are preserved. The [CLI guide](../../src/hostagent/system_api/docs/guides/cli.md)
|
|
19
|
+
already on PATH. Run `zo update` to upgrade, or re-run the installer; `--version`,
|
|
20
|
+
`--bin-dir`, and `--no-modify-path` control pinned installs, command placement
|
|
21
|
+
and shell setup. Existing sign-ins are preserved. The [CLI guide](../../src/hostagent/system_api/docs/guides/cli.md)
|
|
22
22
|
covers supported systems, PATH collisions, rollback and removal.
|
|
23
23
|
|
|
24
24
|
Install the latest published version from npm:
|
|
@@ -29,9 +29,9 @@ zo --version
|
|
|
29
29
|
zo login
|
|
30
30
|
```
|
|
31
31
|
|
|
32
|
-
Run the same install command again to upgrade
|
|
33
|
-
|
|
34
|
-
you run a command.
|
|
32
|
+
Run the same install command again to upgrade; `zo update` refuses an npm
|
|
33
|
+
installation and names this command. The CLI installs its matching public SDK
|
|
34
|
+
dependency. It performs no update check when you run a command.
|
|
35
35
|
|
|
36
36
|
To try the CLI without installing it globally:
|
|
37
37
|
|
|
@@ -127,6 +127,43 @@ Each poll waits up to 20 seconds. After a timeout or network error the CLI
|
|
|
127
127
|
retries once at once, inside the issuer's 30-second redemption window, then
|
|
128
128
|
resumes polling every `interval` seconds; `slow_down` and a 429 add 5 seconds.
|
|
129
129
|
|
|
130
|
+
## Output for terminals and scripts
|
|
131
|
+
|
|
132
|
+
`zo actions list`, `zo actions describe`, and commands with schema-derived
|
|
133
|
+
flags revalidate a private disk copy of the workspace catalog on each call.
|
|
134
|
+
The server returns `304` when its `ETag` still matches; the CLI then uses the
|
|
135
|
+
saved copy. A permission error is never served from disk.
|
|
136
|
+
|
|
137
|
+
Results are data. When stdout is piped or redirected, or you pass `--json` or
|
|
138
|
+
`--envelope`, the CLI prints the JSON result for scripts and agents to parse,
|
|
139
|
+
with no color or tables. `rpc`, `--preview`, `actions describe` and
|
|
140
|
+
`auth token --headers` print JSON on a terminal too.
|
|
141
|
+
|
|
142
|
+
When stdout is a terminal, a list command prints a table, any other action
|
|
143
|
+
prints aligned keys and values, and `login`, `context` and `auth status` print
|
|
144
|
+
text. A table shows a few columns of each row and points to `--json` for the
|
|
145
|
+
rest. A key/value view shows the whole result, since the command may not be
|
|
146
|
+
safe to run twice; a result it can't lay out, such as a query's rows, prints
|
|
147
|
+
as JSON.
|
|
148
|
+
|
|
149
|
+
Help, status lines and error wording are prose, read by people and agents
|
|
150
|
+
alike. Help is the same text everywhere: `zo` and `zo --help` print the
|
|
151
|
+
commands in groups with every resource name, and a terminal only adds bold.
|
|
152
|
+
`login` prints the same status lines everywhere too; a terminal adds color, a
|
|
153
|
+
success mark and a spinner, and anything else gets one `Waiting for approval…`
|
|
154
|
+
line. A mistyped command names the nearest installed one.
|
|
155
|
+
|
|
156
|
+
A script can rely on the exit status, the error code and the shape of the
|
|
157
|
+
JSON, and should not match the wording of a message. When stderr is not a
|
|
158
|
+
terminal a failure begins `<code>: <message>`, so the code is there to match.
|
|
159
|
+
On a terminal it reads `Error: <message> (<code>)` with any next step on the
|
|
160
|
+
following line, Ctrl-C prints `Cancelled.`, and `login` shows a spinner while
|
|
161
|
+
it waits for approval. `--json` makes a failure a JSON object either way.
|
|
162
|
+
|
|
163
|
+
Color needs a terminal and an unset or empty `NO_COLOR`. Names and other text
|
|
164
|
+
from the server are escaped before printing, so they cannot send control
|
|
165
|
+
sequences to your terminal.
|
|
166
|
+
|
|
130
167
|
## Tokens and logout
|
|
131
168
|
|
|
132
169
|
`auth token` explicitly prints a short-lived access token. Add `--headers` to
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export interface CachedCatalogExport {
|
|
2
|
+
etag: string;
|
|
3
|
+
catalogDigest: string;
|
|
4
|
+
body: Uint8Array;
|
|
5
|
+
}
|
|
6
|
+
export declare function readCatalogCache(url: URL, directory?: string): Promise<CachedCatalogExport | undefined>;
|
|
7
|
+
export declare function writeCatalogCache(url: URL, entry: CachedCatalogExport, directory?: string): Promise<void>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { configDirectory, privateDirectory, readPrivate, writePrivate, } from "./storage.js";
|
|
4
|
+
const MAX_CACHE_FILE_BYTES = 6 * 1024 * 1024;
|
|
5
|
+
const MAX_CATALOG_BYTES = 4 * 1024 * 1024;
|
|
6
|
+
const STRONG_ETAG = /^"[0-9a-f]{64}"$/;
|
|
7
|
+
const CATALOG_DIGEST = /^sha256:[0-9a-f]{64}$/;
|
|
8
|
+
function cachePath(url, directory) {
|
|
9
|
+
const key = createHash("sha256").update(url.href).digest("hex");
|
|
10
|
+
return join(directory, `${key}.json`);
|
|
11
|
+
}
|
|
12
|
+
function bodyEtag(body) {
|
|
13
|
+
return `"${createHash("sha256").update(body).digest("hex")}"`;
|
|
14
|
+
}
|
|
15
|
+
export async function readCatalogCache(url, directory = join(configDirectory(), "catalog-cache")) {
|
|
16
|
+
try {
|
|
17
|
+
const path = cachePath(url, directory);
|
|
18
|
+
await privateDirectory(directory);
|
|
19
|
+
const value = await readPrivate(path, MAX_CACHE_FILE_BYTES);
|
|
20
|
+
if (value === null ||
|
|
21
|
+
typeof value !== "object" ||
|
|
22
|
+
Array.isArray(value) ||
|
|
23
|
+
!Object.hasOwn(value, "body") ||
|
|
24
|
+
!Object.hasOwn(value, "etag") ||
|
|
25
|
+
!Object.hasOwn(value, "catalogDigest"))
|
|
26
|
+
return undefined;
|
|
27
|
+
const entry = value;
|
|
28
|
+
if (entry.version !== 1 ||
|
|
29
|
+
typeof entry.body !== "string" ||
|
|
30
|
+
typeof entry.etag !== "string" ||
|
|
31
|
+
typeof entry.catalogDigest !== "string" ||
|
|
32
|
+
!STRONG_ETAG.test(entry.etag) ||
|
|
33
|
+
!CATALOG_DIGEST.test(entry.catalogDigest) ||
|
|
34
|
+
entry.body.length > MAX_CACHE_FILE_BYTES)
|
|
35
|
+
return undefined;
|
|
36
|
+
const body = Buffer.from(entry.body, "base64");
|
|
37
|
+
if (body.length > MAX_CATALOG_BYTES || bodyEtag(body) !== entry.etag)
|
|
38
|
+
return undefined;
|
|
39
|
+
return { body, etag: entry.etag, catalogDigest: entry.catalogDigest };
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
// A damaged or unavailable optional cache must not block live discovery.
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
export async function writeCatalogCache(url, entry, directory = join(configDirectory(), "catalog-cache")) {
|
|
47
|
+
if (!STRONG_ETAG.test(entry.etag) ||
|
|
48
|
+
!CATALOG_DIGEST.test(entry.catalogDigest) ||
|
|
49
|
+
entry.body.length > MAX_CATALOG_BYTES ||
|
|
50
|
+
bodyEtag(entry.body) !== entry.etag)
|
|
51
|
+
return;
|
|
52
|
+
try {
|
|
53
|
+
await writePrivate(cachePath(url, directory), {
|
|
54
|
+
version: 1,
|
|
55
|
+
etag: entry.etag,
|
|
56
|
+
catalogDigest: entry.catalogDigest,
|
|
57
|
+
body: Buffer.from(entry.body).toString("base64"),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
// The disk copy is an optimization, never the source of authorization.
|
|
62
|
+
}
|
|
63
|
+
}
|
package/dist/catalog.d.ts
CHANGED
|
@@ -7,6 +7,8 @@ export interface InvocationContext {
|
|
|
7
7
|
accessToken: () => Promise<string>;
|
|
8
8
|
fetch?: FetchLike;
|
|
9
9
|
signal?: AbortSignal;
|
|
10
|
+
/** Test-only override; production uses the private CLI configuration directory. */
|
|
11
|
+
catalogCacheDirectory?: string;
|
|
10
12
|
}
|
|
11
13
|
export interface LoadedCatalog {
|
|
12
14
|
authenticatedContext: InvocationContext;
|
package/dist/catalog.js
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
1
2
|
import { defineRuntimeCatalog, SystemAPIError, } from "@zocomputer/system-api";
|
|
2
3
|
import { loadNormalizedCatalog, } from "@zocomputer/system-api/catalog";
|
|
3
|
-
import { canonicalNumberFromToken, JsonNumber, } from "@zocomputer/system-api/json";
|
|
4
|
+
import { canonicalNumberFromToken, canonicalJsonBytes, JsonNumber, loadsJson, } from "@zocomputer/system-api/json";
|
|
4
5
|
import { CliError } from "./errors.js";
|
|
6
|
+
import { readCatalogCache, writeCatalogCache } from "./catalog-cache.js";
|
|
5
7
|
import { systemApiFailure } from "./http.js";
|
|
6
8
|
const MAX_CATALOG_BYTES = 4 * 1024 * 1024;
|
|
7
9
|
export function apiBaseUrl(context) {
|
|
@@ -45,6 +47,24 @@ async function readCatalog(response) {
|
|
|
45
47
|
}
|
|
46
48
|
return bytes;
|
|
47
49
|
}
|
|
50
|
+
function verifiedDigest(bytes) {
|
|
51
|
+
const tree = loadsJson(bytes, { maxNodes: null, maxDepth: 40 });
|
|
52
|
+
if (tree === null ||
|
|
53
|
+
typeof tree !== "object" ||
|
|
54
|
+
Array.isArray(tree) ||
|
|
55
|
+
tree instanceof JsonNumber)
|
|
56
|
+
throw invalidCatalog("The service returned an invalid catalog export.");
|
|
57
|
+
const digest = tree.catalog_digest;
|
|
58
|
+
if (typeof digest !== "string" || !/^sha256:[0-9a-f]{64}$/.test(digest))
|
|
59
|
+
throw invalidCatalog("The service returned an invalid catalog digest.");
|
|
60
|
+
const body = { ...tree };
|
|
61
|
+
delete body.catalog_digest;
|
|
62
|
+
delete body.view_revision;
|
|
63
|
+
const computed = `sha256:${createHash("sha256").update(canonicalJsonBytes(body)).digest("hex")}`;
|
|
64
|
+
if (computed !== digest)
|
|
65
|
+
throw invalidCatalog("The service returned an invalid catalog digest.");
|
|
66
|
+
return digest;
|
|
67
|
+
}
|
|
48
68
|
export async function loadCatalog(context, options = {}) {
|
|
49
69
|
if (!/^wsp_[A-Za-z0-9_-]+$/.test(context.workspaceId))
|
|
50
70
|
throw new CliError("Select a workspace with zo context use <workspace-id> or --workspace.");
|
|
@@ -65,16 +85,27 @@ export async function loadCatalog(context, options = {}) {
|
|
|
65
85
|
const signal = context.signal
|
|
66
86
|
? AbortSignal.any([context.signal, timeout])
|
|
67
87
|
: timeout;
|
|
88
|
+
const useCache = context.fetch === undefined || context.catalogCacheDirectory !== undefined;
|
|
89
|
+
const cached = useCache
|
|
90
|
+
? await readCatalogCache(url, context.catalogCacheDirectory)
|
|
91
|
+
: undefined;
|
|
68
92
|
let response;
|
|
69
93
|
let bytes;
|
|
70
94
|
try {
|
|
71
95
|
response = await (context.fetch ?? fetch)(url, {
|
|
72
|
-
headers: {
|
|
96
|
+
headers: {
|
|
97
|
+
authorization: `Bearer ${token}`,
|
|
98
|
+
accept: "application/json",
|
|
99
|
+
...(cached ? { "If-None-Match": cached.etag } : {}),
|
|
100
|
+
},
|
|
73
101
|
redirect: "error",
|
|
74
102
|
cache: "no-store",
|
|
75
103
|
signal,
|
|
76
104
|
});
|
|
77
|
-
bytes =
|
|
105
|
+
bytes =
|
|
106
|
+
response.status === 304
|
|
107
|
+
? (cached?.body ?? new Uint8Array())
|
|
108
|
+
: await readCatalog(response);
|
|
78
109
|
}
|
|
79
110
|
catch (error) {
|
|
80
111
|
if (signal.aborted)
|
|
@@ -95,8 +126,28 @@ export async function loadCatalog(context, options = {}) {
|
|
|
95
126
|
outcome: "not_started",
|
|
96
127
|
});
|
|
97
128
|
}
|
|
98
|
-
if (
|
|
129
|
+
if (response.status === 304 && !cached)
|
|
130
|
+
throw invalidCatalog("The service returned 304 without a saved catalog.");
|
|
131
|
+
if (!response.ok && response.status !== 304)
|
|
99
132
|
throw systemApiFailure(response, bytes, "Catalog discovery");
|
|
133
|
+
const etag = response.headers.get("etag");
|
|
134
|
+
if (response.status === 304 && etag !== cached?.etag)
|
|
135
|
+
throw invalidCatalog("The catalog validator changed without a response body.");
|
|
136
|
+
if (response.status === 200 &&
|
|
137
|
+
etag !== null &&
|
|
138
|
+
etag !== `"${createHash("sha256").update(bytes).digest("hex")}"`)
|
|
139
|
+
throw invalidCatalog("The service returned an invalid catalog validator.");
|
|
140
|
+
let digest;
|
|
141
|
+
if (etag !== null || response.status === 304) {
|
|
142
|
+
try {
|
|
143
|
+
digest = verifiedDigest(bytes);
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
throw invalidCatalog("The service returned an invalid catalog digest.");
|
|
147
|
+
}
|
|
148
|
+
if (response.status === 304 && digest !== cached?.catalogDigest)
|
|
149
|
+
throw invalidCatalog("The saved catalog digest changed.");
|
|
150
|
+
}
|
|
100
151
|
let catalog;
|
|
101
152
|
try {
|
|
102
153
|
catalog = loadNormalizedCatalog(bytes).catalog;
|
|
@@ -108,6 +159,8 @@ export async function loadCatalog(context, options = {}) {
|
|
|
108
159
|
catalog.selection.catalog !== mode) {
|
|
109
160
|
throw invalidCatalog("The catalog selection does not match the requested workspace and catalog.");
|
|
110
161
|
}
|
|
162
|
+
if (response.status === 200 && etag && digest)
|
|
163
|
+
await writeCatalogCache(url, { etag, catalogDigest: digest, body: bytes }, context.catalogCacheDirectory);
|
|
111
164
|
return {
|
|
112
165
|
authenticatedContext: { ...context, accessToken: async () => token },
|
|
113
166
|
catalog,
|
package/dist/commands.d.ts
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { type InvocationContext } from "./catalog.ts";
|
|
2
|
+
import { CliError } from "./errors.ts";
|
|
2
3
|
import { type RpcResult, type RpcOptions } from "./rpc.ts";
|
|
4
|
+
/** The first word of every built-in command, in catalog order. */
|
|
5
|
+
export declare function resourceNames(): string[];
|
|
6
|
+
/**
|
|
7
|
+
* The error for arguments that name no command. It never repeats what was
|
|
8
|
+
* typed: it names the installed words that did match, and the nearest
|
|
9
|
+
* installed word to the first one that did not.
|
|
10
|
+
*/
|
|
11
|
+
export declare function unknownCommand(args: readonly string[], kind?: string): CliError;
|
|
12
|
+
/**
|
|
13
|
+
* The fields a list action sorts by, from installed metadata. Undefined for
|
|
14
|
+
* an action that declares no list query, which is how a list is recognized.
|
|
15
|
+
*/
|
|
16
|
+
export declare function listSortFields(action: string): string[] | undefined;
|
|
3
17
|
export declare function matchBuiltinCommand(args: readonly string[]): {
|
|
4
18
|
action: string;
|
|
5
19
|
args: string[];
|
package/dist/commands.js
CHANGED
|
@@ -4,6 +4,84 @@ import { CliError, safeText } from "./errors.js";
|
|
|
4
4
|
import { encodeCommandFlags, inputFlags, parseCommandFlags, reservedOptions, resolvedSchema, scalarType, schemaObject, } from "./flags.js";
|
|
5
5
|
import { commandManifest } from "./generated/commands.js";
|
|
6
6
|
import { inputJson, runRpc } from "./rpc.js";
|
|
7
|
+
import { closest } from "./suggest.js";
|
|
8
|
+
/** Commands the runner implements itself, each with its subcommands. */
|
|
9
|
+
const localCommands = {
|
|
10
|
+
login: [],
|
|
11
|
+
logout: [],
|
|
12
|
+
auth: ["status", "token"],
|
|
13
|
+
context: ["list", "use", "show"],
|
|
14
|
+
actions: ["list", "describe"],
|
|
15
|
+
rpc: [],
|
|
16
|
+
completion: [],
|
|
17
|
+
update: [],
|
|
18
|
+
};
|
|
19
|
+
/** The first word of every built-in command, in catalog order. */
|
|
20
|
+
export function resourceNames() {
|
|
21
|
+
return [
|
|
22
|
+
...new Set(commandManifest.commands.flatMap((command) => command.path.slice(0, 1))),
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The error for arguments that name no command. It never repeats what was
|
|
27
|
+
* typed: it names the installed words that did match, and the nearest
|
|
28
|
+
* installed word to the first one that did not.
|
|
29
|
+
*/
|
|
30
|
+
export function unknownCommand(args, kind = "command") {
|
|
31
|
+
let paths = [
|
|
32
|
+
...Object.entries(localCommands).flatMap(([name, subcommands]) => subcommands.length
|
|
33
|
+
? subcommands.map((subcommand) => [name, subcommand])
|
|
34
|
+
: [[name]]),
|
|
35
|
+
...commandManifest.commands.map((command) => command.path),
|
|
36
|
+
];
|
|
37
|
+
let depth = 0;
|
|
38
|
+
for (; depth < args.length; depth++) {
|
|
39
|
+
const matching = paths.filter((path) => path[depth] === args[depth]);
|
|
40
|
+
if (!matching.length)
|
|
41
|
+
break;
|
|
42
|
+
paths = matching;
|
|
43
|
+
}
|
|
44
|
+
const next = [
|
|
45
|
+
...new Set(paths.flatMap((path) => {
|
|
46
|
+
const word = path[depth];
|
|
47
|
+
return word === undefined ? [] : [word];
|
|
48
|
+
})),
|
|
49
|
+
];
|
|
50
|
+
const known = paths[0]?.slice(0, depth) ?? [];
|
|
51
|
+
const prefix = ["zo", ...known].join(" ");
|
|
52
|
+
const help = `Run ${prefix} --help.`;
|
|
53
|
+
const typed = args[depth];
|
|
54
|
+
if (depth > 0 &&
|
|
55
|
+
next.length &&
|
|
56
|
+
(typed === undefined || typed.startsWith("--")))
|
|
57
|
+
return new CliError(`${prefix} needs a command. ${help}`);
|
|
58
|
+
const guess = typed === undefined || typed.startsWith("--")
|
|
59
|
+
? undefined
|
|
60
|
+
: closest(typed, next);
|
|
61
|
+
return new CliError(`Unknown ${depth ? `${known.join(" ")} command` : kind}.${guess ? ` Did you mean ${prefix} ${guess}?` : ""} ${help}`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* The fields a list action sorts by, from installed metadata. Undefined for
|
|
65
|
+
* an action that declares no list query, which is how a list is recognized.
|
|
66
|
+
*/
|
|
67
|
+
export function listSortFields(action) {
|
|
68
|
+
const queries = (commandManifest.commands.find((command) => command.action === action)
|
|
69
|
+
?.versions ?? []).flatMap((version) => "queryJson" in version && typeof version.queryJson === "string"
|
|
70
|
+
? [version.queryJson]
|
|
71
|
+
: []);
|
|
72
|
+
if (!queries.length)
|
|
73
|
+
return undefined;
|
|
74
|
+
const fields = new Set();
|
|
75
|
+
for (const text of queries) {
|
|
76
|
+
const query = loadsJson(text);
|
|
77
|
+
const sort = schemaObject(resolvedSchema(query)?.properties)?.sort;
|
|
78
|
+
const choices = sort === undefined ? undefined : resolvedSchema(sort, query)?.enum;
|
|
79
|
+
for (const choice of Array.isArray(choices) ? choices : [])
|
|
80
|
+
if (typeof choice === "string")
|
|
81
|
+
fields.add(choice.replace(/\.(?:asc|desc)$/, ""));
|
|
82
|
+
}
|
|
83
|
+
return [...fields];
|
|
84
|
+
}
|
|
7
85
|
export function matchBuiltinCommand(args) {
|
|
8
86
|
const command = commandManifest.commands.find((command) => command.path.every((segment, index) => args[index] === segment));
|
|
9
87
|
return command
|
|
@@ -47,7 +125,7 @@ Async actions return their actual accepted/run/operation state; there is no univ
|
|
|
47
125
|
export function builtinHelp(path = []) {
|
|
48
126
|
const commands = commandManifest.commands.filter((command) => path.every((segment, index) => command.path[index] === segment));
|
|
49
127
|
if (!commands.length)
|
|
50
|
-
throw
|
|
128
|
+
throw unknownCommand(path, "built-in command");
|
|
51
129
|
const command = commands.find((command) => command.path.length === path.length);
|
|
52
130
|
if (!command)
|
|
53
131
|
return `Built-in commands (installed metadata; availability is checked live)\n\n${commands.map((item) => ` zo ${item.path.join(" ")}`).join("\n")}\n\nRun a command with --help for its schema-derived flags.\n`;
|
|
@@ -90,19 +168,10 @@ export function builtinHelp(path = []) {
|
|
|
90
168
|
export function completionScript(shell) {
|
|
91
169
|
const words = [
|
|
92
170
|
...new Set([
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
"actions",
|
|
98
|
-
"rpc",
|
|
99
|
-
"completion",
|
|
100
|
-
"status",
|
|
101
|
-
"token",
|
|
102
|
-
"list",
|
|
103
|
-
"describe",
|
|
104
|
-
"use",
|
|
105
|
-
"show",
|
|
171
|
+
...Object.entries(localCommands).flatMap(([name, subcommands]) => [
|
|
172
|
+
name,
|
|
173
|
+
...subcommands,
|
|
174
|
+
]),
|
|
106
175
|
...[...reservedOptions].map((option) => `--${option}`),
|
|
107
176
|
...commandManifest.commands.flatMap((command) => command.path),
|
|
108
177
|
...commandManifest.commands.flatMap((command) => command.versions.flatMap((version) => inputFlags(loadsJson(version.inputJson)).map((flag) => `--${flag.name}`))),
|