@zocomputer/cli 0.0.0-stage → 0.1.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/dist/http.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ import { SystemAPIError } from "@zocomputer/system-api";
2
+ import { CliError } from "./errors.ts";
3
+ export declare function systemApiFailure(response: Response, raw: string | Uint8Array, operation: string): SystemAPIError;
4
+ export declare class RequestFailure extends CliError {
5
+ readonly ambiguous: boolean;
6
+ constructor(ambiguous: boolean, code?: string, exitCode?: number);
7
+ }
8
+ export declare function requestJson(url: string, init?: RequestInit, fetcher?: typeof fetch, options?: {
9
+ timeoutMs?: number;
10
+ systemApi?: boolean;
11
+ }): Promise<unknown>;
package/dist/http.js ADDED
@@ -0,0 +1,124 @@
1
+ import { SystemAPIError } from "@zocomputer/system-api";
2
+ import { loadsJson } from "@zocomputer/system-api/json";
3
+ import { CliError, record, throwIfInterrupted } from "./errors.js";
4
+ export function systemApiFailure(response, raw, operation) {
5
+ let body;
6
+ try {
7
+ body = loadsJson(raw);
8
+ }
9
+ catch {
10
+ /* Non-JSON upstream failures carry no safe details. */
11
+ }
12
+ const error = record(body) && record(body.error) ? body.error : undefined;
13
+ const valid = error &&
14
+ typeof error.code === "string" &&
15
+ /^[a-z][a-z0-9_]*$/.test(error.code) &&
16
+ typeof error.message === "string";
17
+ const retryHeader = response.headers.get("retry-after")?.trim();
18
+ let retryAfterSeconds;
19
+ if (retryHeader) {
20
+ const seconds = /^\d+$/.test(retryHeader)
21
+ ? Number(retryHeader)
22
+ : Math.ceil((Date.parse(retryHeader) - Date.now()) / 1000);
23
+ if (!Number.isNaN(seconds))
24
+ retryAfterSeconds = Math.max(0, Math.min(seconds, 86_400));
25
+ }
26
+ const outcome = valid && record(error.details) ? error.details.outcome : undefined;
27
+ return new SystemAPIError({
28
+ code: valid
29
+ ? error.code
30
+ : response.status === 401
31
+ ? "unauthenticated"
32
+ : response.status === 403
33
+ ? "permission_denied"
34
+ : "invalid_response",
35
+ message: valid
36
+ ? error.message
37
+ : `${operation} failed with HTTP ${response.status}.`,
38
+ ...(record(body) && typeof body.request_id === "string"
39
+ ? { requestId: body.request_id }
40
+ : {}),
41
+ ...(valid ? { details: error.details } : {}),
42
+ ...(retryAfterSeconds !== undefined ? { retryAfterSeconds } : {}),
43
+ httpStatus: response.status,
44
+ outcome: outcome === "unknown" ? "unknown" : "not_started",
45
+ });
46
+ }
47
+ export class RequestFailure extends CliError {
48
+ ambiguous;
49
+ constructor(ambiguous, code = "transport_error", exitCode = 5) {
50
+ super(code === "invalid_grant"
51
+ ? "Your sign-in ended. Run zo login."
52
+ : "The authentication request failed. Try again or run zo login.", exitCode, code);
53
+ this.ambiguous = ambiguous;
54
+ }
55
+ }
56
+ export async function requestJson(url, init = {}, fetcher = fetch, options = {}) {
57
+ throwIfInterrupted(init.signal);
58
+ const timeout = AbortSignal.timeout(options.timeoutMs ?? 8_000);
59
+ const signal = init.signal
60
+ ? AbortSignal.any([init.signal, timeout])
61
+ : timeout;
62
+ let response;
63
+ try {
64
+ response = await fetcher(url, {
65
+ ...init,
66
+ redirect: "error",
67
+ signal,
68
+ });
69
+ }
70
+ catch {
71
+ throwIfInterrupted(init.signal);
72
+ throw new RequestFailure(true);
73
+ }
74
+ let raw;
75
+ try {
76
+ if (!response.body)
77
+ throw new RequestFailure(false, "invalid_response");
78
+ const reader = response.body.getReader();
79
+ const chunks = [];
80
+ let size = 0;
81
+ for (;;) {
82
+ const { value, done } = await reader.read();
83
+ if (done)
84
+ break;
85
+ size += value.length;
86
+ if (size > 1_048_576) {
87
+ // Cleanup failure cannot make a known oversized response retryable.
88
+ await reader.cancel().catch(() => { });
89
+ throw new RequestFailure(false, "invalid_response");
90
+ }
91
+ chunks.push(value);
92
+ }
93
+ raw = Buffer.concat(chunks).toString("utf8");
94
+ }
95
+ catch (error) {
96
+ throwIfInterrupted(init.signal);
97
+ if (error instanceof RequestFailure)
98
+ throw error;
99
+ throw new RequestFailure(true, "invalid_response");
100
+ }
101
+ throwIfInterrupted(init.signal);
102
+ if (!response.ok && options.systemApi)
103
+ throw systemApiFailure(response, raw, "Session discovery");
104
+ let body;
105
+ try {
106
+ body = JSON.parse(raw);
107
+ }
108
+ catch {
109
+ throw new RequestFailure(false, "invalid_response");
110
+ }
111
+ if (!response.ok) {
112
+ const code = record(body) &&
113
+ typeof body.error === "string" &&
114
+ /^[a-z_]+$/.test(body.error)
115
+ ? body.error
116
+ : "authentication_failed";
117
+ throw new RequestFailure(response.status >= 500 || response.status === 429, code, response.status === 401 || code === "invalid_grant"
118
+ ? 3
119
+ : response.status === 403
120
+ ? 4
121
+ : 5);
122
+ }
123
+ return body;
124
+ }
@@ -0,0 +1,14 @@
1
+ export interface LoginOptions {
2
+ noBrowser?: boolean;
3
+ timeoutMs?: number;
4
+ signal?: AbortSignal;
5
+ announce?: (url: string) => void;
6
+ openBrowser?: (url: string) => Promise<void>;
7
+ }
8
+ export interface LoginCode {
9
+ code: string;
10
+ verifier: string;
11
+ redirectUri: string;
12
+ }
13
+ export declare function openBrowser(url: string): Promise<void>;
14
+ export declare function loginCode(issuer: string, audience: string, clientId: string, options?: LoginOptions): Promise<LoginCode>;
package/dist/login.js ADDED
@@ -0,0 +1,102 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { createServer } from "node:http";
3
+ import { spawn } from "node:child_process";
4
+ import { CliError } from "./errors.js";
5
+ export async function openBrowser(url) {
6
+ await new Promise((resolve, reject) => {
7
+ const child = spawn(process.platform === "darwin" ? "open" : "xdg-open", [url], { stdio: "ignore", detached: true });
8
+ child.unref();
9
+ child.once("error", () => reject(new CliError("Could not open a browser. Retry with --no-browser.")));
10
+ child.once("exit", (status) => status === 0
11
+ ? resolve()
12
+ : reject(new CliError("Could not open a browser. Retry with --no-browser.")));
13
+ });
14
+ }
15
+ export async function loginCode(issuer, audience, clientId, options = {}) {
16
+ const state = randomBytes(32).toString("base64url");
17
+ const verifier = randomBytes(32).toString("base64url");
18
+ let complete = () => { };
19
+ let fail = () => { };
20
+ const received = new Promise((resolve, reject) => {
21
+ complete = resolve;
22
+ fail = reject;
23
+ });
24
+ // The callback may arrive while the browser opener is still running.
25
+ void received.catch(() => { });
26
+ let accepted = false;
27
+ const server = createServer((request, response) => {
28
+ let url;
29
+ try {
30
+ url = new URL(request.url ?? "/", "http://127.0.0.1");
31
+ }
32
+ catch {
33
+ response.writeHead(400).end("Invalid callback.");
34
+ return;
35
+ }
36
+ const params = url.searchParams;
37
+ const keys = [...params.keys()];
38
+ const valid = !accepted &&
39
+ request.method === "GET" &&
40
+ url.pathname === "/callback" &&
41
+ keys.length === new Set(keys).size &&
42
+ params.get("state") === state &&
43
+ params.get("iss") === issuer &&
44
+ params.has("code") !== params.has("error");
45
+ response.setHeader("Content-Type", "text/plain; charset=utf-8");
46
+ response.setHeader("Cache-Control", "no-store");
47
+ if (!valid) {
48
+ response.writeHead(400).end("Invalid callback.");
49
+ return;
50
+ }
51
+ accepted = true;
52
+ response.end("You may close this window and return to Zo.");
53
+ if (params.has("error"))
54
+ fail(new CliError("Authorization was denied. Run zo login to try again.", 4, "access_denied"));
55
+ else {
56
+ const code = params.get("code");
57
+ if (code)
58
+ complete(code);
59
+ else
60
+ fail(new CliError("Authorization returned an empty code.", 3, "invalid_response"));
61
+ }
62
+ });
63
+ server.requestTimeout = 5_000;
64
+ server.headersTimeout = 5_000;
65
+ await new Promise((resolve, reject) => {
66
+ server.once("error", reject);
67
+ server.listen(0, "127.0.0.1", resolve);
68
+ });
69
+ const address = server.address();
70
+ if (!address || typeof address === "string")
71
+ throw new CliError("Could not bind the login callback.");
72
+ const redirectUri = `http://127.0.0.1:${address.port}/callback`;
73
+ const authorize = new URL("/zo-system-api/oauth/authorize", issuer);
74
+ authorize.search = new URLSearchParams({
75
+ response_type: "code",
76
+ client_id: clientId,
77
+ redirect_uri: redirectUri,
78
+ resource: audience,
79
+ code_challenge: createHash("sha256").update(verifier).digest("base64url"),
80
+ code_challenge_method: "S256",
81
+ state,
82
+ }).toString();
83
+ const timer = setTimeout(() => fail(new CliError("Login timed out. Run zo login to try again.", 3, "login_timeout")), options.timeoutMs ?? 300_000);
84
+ const abort = () => fail(new CliError("Login interrupted.", 130, "interrupted"));
85
+ options.signal?.addEventListener("abort", abort, { once: true });
86
+ try {
87
+ if (options.signal?.aborted)
88
+ abort();
89
+ else {
90
+ options.announce?.(authorize.href);
91
+ if (!options.noBrowser)
92
+ void (options.openBrowser ?? openBrowser)(authorize.href).catch(fail);
93
+ }
94
+ return { code: await received, verifier, redirectUri };
95
+ }
96
+ finally {
97
+ clearTimeout(timer);
98
+ options.signal?.removeEventListener("abort", abort);
99
+ server.closeAllConnections();
100
+ await new Promise((resolve) => server.close(() => resolve()));
101
+ }
102
+ }
package/dist/main.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ import { Profiles } from "./profiles.ts";
3
+ import type { InvocationContext } from "./catalog.ts";
4
+ import type { OutputOptions } from "./output.ts";
5
+ import type { RpcOptions } from "./rpc.ts";
6
+ export { parseGlobals } from "./argv.ts";
7
+ export interface RunOptions extends OutputOptions {
8
+ profiles?: Profiles;
9
+ env?: NodeJS.ProcessEnv;
10
+ fetch?: InvocationContext["fetch"];
11
+ stdin?: RpcOptions["stdin"];
12
+ signal?: AbortSignal;
13
+ }
14
+ export declare function run(argv?: string[], runtime?: RunOptions): Promise<void>;
15
+ export declare function execute(argv?: string[], runtime?: RunOptions): Promise<number>;
package/dist/main.js ADDED
@@ -0,0 +1,306 @@
1
+ #!/usr/bin/env node
2
+ import { readFile, realpath } from "node:fs/promises";
3
+ import { createInterface } from "node:readline/promises";
4
+ import { pathToFileURL } from "node:url";
5
+ import { AuthClient } from "./auth.js";
6
+ import { commandOptions, parseGlobals } from "./argv.js";
7
+ import { authorizedWorkspace, contextStatus, useContext } from "./context.js";
8
+ import { CliError, record, safeText, throwIfInterrupted } from "./errors.js";
9
+ import { Profiles, selectedWorkspace } from "./profiles.js";
10
+ export { parseGlobals } from "./argv.js";
11
+ const help = `Zo public command line client
12
+
13
+ Usage: zo <command> [--profile <name>] [--workspace <id>] [--json]
14
+
15
+ login / logout Sign in through your browser or revoke the sign-in
16
+ auth status / token Inspect your identity or explicitly print an access token
17
+ context list/use/show Select an authorized workspace
18
+ actions list/describe Discover live action contracts
19
+ rpc <action> Invoke any callable action from JSON
20
+ <resource> <action> Invoke an installed built-in command
21
+ completion <shell> Print bash, zsh or fish completion (offline)
22
+ --version Show the installed CLI version
23
+
24
+ Run zo <command> --help for options; for example, zo machine list --help.
25
+ Use --profile to keep separate sign-ins. New profiles use production.
26
+ ZO_SYSTEM_API_ACCESS_TOKEN overrides saved credentials; ZO_WORKSPACE_ID selects a workspace.
27
+ `;
28
+ const localHelp = {
29
+ login: "Usage: zo login [--profile <name>] [--workspace <id>] [--no-browser]\nPrint the sign-in URL, open your browser, and wait for approval.\nNew profiles use production. Browser consent approves workspaces and identities.\n--no-browser skips opening the browser and still waits for the localhost callback.\nInternal testing: --environment staging explicitly binds a separate profile to staging.\n",
30
+ logout: "Usage: zo logout [--profile <name>] [--local-only]\nRevoke the saved sign-in and pending replaced sign-ins. --local-only removes local credentials without revoking server access.\n",
31
+ auth: "Usage: zo auth status | token [--headers]\nStatus shows the current identity; token explicitly prints an access token.\n",
32
+ "auth status": "Usage: zo auth status [--profile <name>] [--workspace <id>] [--json]\nShow the approving person, sign-in expiry and effective identity in the selected workspace.\n",
33
+ "auth token": "Usage: zo auth token [--profile <name>] [--headers]\nExplicitly print a short-lived access token; --headers prints Authorization JSON.\n",
34
+ context: "Usage: zo context list | use <workspace-id> | show\nWorkspace selection does not change permissions.\n",
35
+ "context list": "Usage: zo context list [--profile <name>] [--json]\nList currently authorized workspaces and effective identities.\n",
36
+ "context use": "Usage: zo context use <workspace-id> [--profile <name>]\nValidate access and save the workspace as this profile's default.\n",
37
+ "context show": "Usage: zo context show [--profile <name>] [--workspace <id>] [--json]\nShow and validate the selected workspace and identity.\n",
38
+ actions: "Usage: zo actions list | describe <action> [--version <integer>] [--catalog scoped|all]\nRead the live catalog. --catalog all includes definitions beyond current availability.\n",
39
+ "actions list": "Usage: zo actions list [--catalog scoped|all] [--profile <name>] [--workspace <id>] [--json]\nList callable contracts from the live catalog; --catalog all includes definitions.\n",
40
+ "actions describe": "Usage: zo actions describe <action> [--version <integer>] [--catalog scoped|all] [--json]\nShow live input, output and query schemas. Omit --version to inspect every published version.\n",
41
+ rpc: "Usage: zo rpc <action> [--input <json> | --input-file <path|->] [--version <integer>]\n --query <json> Declared list-query options; one page per call\n --idempotency-key <key> Envelope key where the action supports it\n --preview Print the request without invoking\n --envelope Print the complete success envelope\n --json Print exact result JSON\n --profile <name> --workspace <id>\nThe live catalog supplies the default version. RPC preserves supplied inputs. Invocations are never retried.\n",
42
+ completion: "Usage: zo completion bash|zsh|fish\nPrint completion from installed metadata without credentials or network requests.\n",
43
+ };
44
+ async function showHelp(args, stdout) {
45
+ if (!args.length) {
46
+ stdout.write(help);
47
+ return;
48
+ }
49
+ const path = args.slice(0, args.findIndex((arg) => arg.startsWith("--")) === -1
50
+ ? undefined
51
+ : args.findIndex((arg) => arg.startsWith("--")));
52
+ const key = path
53
+ .slice(0, ["auth", "context", "actions"].includes(path[0] ?? "") ? 2 : 1)
54
+ .join(" ");
55
+ if (Object.hasOwn(localHelp, key)) {
56
+ stdout.write(localHelp[key] ?? "");
57
+ return;
58
+ }
59
+ stdout.write((await import("./commands.js")).builtinHelp(path));
60
+ }
61
+ export async function run(argv = process.argv.slice(2), runtime = {}) {
62
+ const { args, options } = parseGlobals(argv);
63
+ const stdout = runtime.stdout ?? process.stdout;
64
+ const stderr = runtime.stderr ?? process.stderr;
65
+ const output = {
66
+ stdout,
67
+ stderr,
68
+ json: options.json,
69
+ envelope: options.envelope,
70
+ };
71
+ const env = runtime.env ?? process.env;
72
+ if (options.help || !args.length) {
73
+ await showHelp(args, stdout);
74
+ return;
75
+ }
76
+ if (args[0] === "--version") {
77
+ if (args.length !== 1)
78
+ throw new CliError("Usage: zo --version");
79
+ const manifest = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
80
+ if (!record(manifest) || typeof manifest.version !== "string")
81
+ throw new CliError("The installed package has no version.", 5);
82
+ stdout.write(`${manifest.version}\n`);
83
+ return;
84
+ }
85
+ if (args[0] === "completion") {
86
+ if (args.length !== 2 || !args[1])
87
+ throw new CliError(localHelp.completion ?? "Invalid completion command.");
88
+ stdout.write((await import("./commands.js")).completionScript(args[1]));
89
+ return;
90
+ }
91
+ const local = ["login", "logout", "auth", "context"].includes(args[0] ?? "");
92
+ if (options.envelope && local)
93
+ throw new CliError("--envelope is only supported for action invocation.");
94
+ const controller = new AbortController();
95
+ const interrupt = () => controller.abort();
96
+ process.once("SIGINT", interrupt);
97
+ const signal = runtime.signal
98
+ ? AbortSignal.any([controller.signal, runtime.signal])
99
+ : controller.signal;
100
+ try {
101
+ throwIfInterrupted(signal);
102
+ const profiles = runtime.profiles ?? new Profiles();
103
+ const { writeJson } = await import("./output.js");
104
+ const writeLocalJson = (value) => {
105
+ throwIfInterrupted(signal);
106
+ writeJson(value, output);
107
+ };
108
+ if (args[0] === "login") {
109
+ const parsed = commandOptions(args.slice(1), ["environment"], ["no-browser"]);
110
+ const requested = parsed.values.get("environment");
111
+ if (requested !== undefined &&
112
+ requested !== "production" &&
113
+ requested !== "staging")
114
+ throw new CliError("--environment must be production or staging.");
115
+ const profile = await profiles.create(options.profile, requested, signal);
116
+ const auth = new AuthClient(profile, profiles.directory, {
117
+ env,
118
+ fetch: runtime.fetch,
119
+ signal,
120
+ });
121
+ const login = await auth.login({
122
+ noBrowser: parsed.flags.has("no-browser"),
123
+ signal,
124
+ announce: (url) => {
125
+ const message = `Open this URL to sign in:\n${url}\n`;
126
+ const dim = stderr.isTTY &&
127
+ !options.json &&
128
+ env.NO_COLOR === undefined &&
129
+ env.TERM !== "dumb";
130
+ stderr.write(dim ? `\u001b[2m${message}\u001b[22m` : message);
131
+ },
132
+ });
133
+ stderr.write(`Sign-in saved for profile ${profile.name}.\n`);
134
+ if (login.cleanupPending)
135
+ stderr.write("A previous sign-in could not be revoked. Cleanup remains saved for the next login; the current sign-in is active. You can revoke the previous sign-in in Authorized apps.\n");
136
+ const session = await auth.session();
137
+ const desired = options.workspace ?? env.ZO_WORKSPACE_ID ?? profile.workspaceId;
138
+ let chosen;
139
+ if (options.workspace !== undefined)
140
+ chosen = authorizedWorkspace(session, options.workspace).workspace_id;
141
+ else if (desired !== undefined) {
142
+ chosen = session.workspaces.find((workspace) => workspace.workspace_id === desired)?.workspace_id;
143
+ if (!chosen)
144
+ stderr.write(env.ZO_WORKSPACE_ID !== undefined
145
+ ? "ZO_WORKSPACE_ID is not authorized by the new sign-in. Choose a workspace, then update or unset ZO_WORKSPACE_ID (or use --workspace) before invoking.\n"
146
+ : "The saved workspace is not authorized by the new sign-in. Choose a workspace explicitly; the previous selection is unchanged.\n");
147
+ }
148
+ else if (session.workspaces.length === 1)
149
+ chosen = session.workspaces[0]?.workspace_id;
150
+ if (!chosen &&
151
+ !runtime.stdin &&
152
+ process.stdin.isTTY &&
153
+ stderr.isTTY &&
154
+ session.workspaces.length > 0) {
155
+ session.workspaces.forEach((workspace, index) => stderr.write(`${index + 1}. ${safeText(workspace.name)} (${workspace.workspace_id})\n`));
156
+ const input = createInterface({
157
+ input: process.stdin,
158
+ output: process.stderr,
159
+ });
160
+ try {
161
+ const answer = await input.question("Choose a workspace number: ", {
162
+ signal,
163
+ });
164
+ if (/^[1-9][0-9]*$/.test(answer))
165
+ chosen = session.workspaces[Number(answer) - 1]?.workspace_id;
166
+ }
167
+ finally {
168
+ input.close();
169
+ }
170
+ }
171
+ if (!chosen) {
172
+ stderr.write("Select an authorized workspace with zo context use <workspace-id>.\n");
173
+ writeLocalJson({
174
+ profile: profile.name,
175
+ signed_in: true,
176
+ workspace_id: null,
177
+ selection_required: true,
178
+ });
179
+ return;
180
+ }
181
+ await profiles.select(profile, chosen, signal);
182
+ writeLocalJson({ profile: profile.name, workspace_id: chosen });
183
+ return;
184
+ }
185
+ const profile = await profiles.resolve(options.profile);
186
+ throwIfInterrupted(signal);
187
+ const auth = new AuthClient(profile, profiles.directory, {
188
+ env,
189
+ fetch: runtime.fetch,
190
+ signal,
191
+ });
192
+ if (args[0] === "logout") {
193
+ const parsed = commandOptions(args.slice(1), [], ["local-only"]);
194
+ const localOnly = parsed.flags.has("local-only");
195
+ await auth.logout(localOnly);
196
+ throwIfInterrupted(signal);
197
+ stderr.write(localOnly
198
+ ? "Local credentials removed. Server access was not revoked.\n"
199
+ : "Signed out; saved sign-ins revoked.\n");
200
+ return;
201
+ }
202
+ if (args[0] === "auth" && args[1] === "token") {
203
+ const parsed = commandOptions(args.slice(2), [], ["headers"]);
204
+ const token = await auth.accessToken();
205
+ throwIfInterrupted(signal);
206
+ if (parsed.flags.has("headers"))
207
+ writeLocalJson({ Authorization: `Bearer ${token}` });
208
+ else
209
+ stdout.write(`${token}\n`);
210
+ return;
211
+ }
212
+ if ((args[0] === "auth" && args[1] === "status") ||
213
+ (args[0] === "context" && args[1] === "show")) {
214
+ commandOptions(args.slice(2), []);
215
+ writeLocalJson(await contextStatus(profile, auth, options.workspace, env));
216
+ return;
217
+ }
218
+ if (args[0] === "context" && args[1] === "list") {
219
+ commandOptions(args.slice(2), []);
220
+ writeLocalJson((await auth.session()).workspaces);
221
+ return;
222
+ }
223
+ if (args[0] === "context" && args[1] === "use") {
224
+ if (args.length !== 3 || !args[2])
225
+ throw new CliError("Usage: zo context use <workspace-id>");
226
+ writeLocalJson(await useContext(profiles, profile, auth, args[2], signal));
227
+ return;
228
+ }
229
+ if (local)
230
+ throw new CliError("Unknown command. Run zo --help.");
231
+ const context = {
232
+ workspaceId: selectedWorkspace(profile, options.workspace, env),
233
+ apiOrigin: auth.binding.api,
234
+ accessToken: () => auth.accessToken(),
235
+ signal,
236
+ ...(runtime.fetch ? { fetch: runtime.fetch } : {}),
237
+ };
238
+ if (args[0] === "rpc") {
239
+ const action = args[1];
240
+ if (!action || action.startsWith("--"))
241
+ throw new CliError("Usage: zo rpc <action> [options]");
242
+ const parsed = commandOptions(args.slice(2), ["input", "input-file", "version", "query", "idempotency-key"], ["preview"]);
243
+ const { parseVersion } = await import("./catalog.js");
244
+ const version = parsed.values.get("version");
245
+ const result = await (await import("./rpc.js")).runRpc(context, {
246
+ action,
247
+ input: parsed.values.get("input"),
248
+ inputFile: parsed.values.get("input-file"),
249
+ query: parsed.values.get("query"),
250
+ version: version === undefined ? undefined : parseVersion(version),
251
+ idempotencyKey: parsed.values.get("idempotency-key"),
252
+ preview: parsed.flags.has("preview"),
253
+ stdin: runtime.stdin,
254
+ });
255
+ (await import("./output.js")).writeRpcResult(result, output);
256
+ return;
257
+ }
258
+ if (args[0] === "actions") {
259
+ const describe = args[1] === "describe";
260
+ if (args[1] !== "list" && !describe)
261
+ throw new CliError("Usage: zo actions list | describe <action>");
262
+ const action = describe ? args[2] : undefined;
263
+ if (describe && (!action || action.startsWith("--")))
264
+ throw new CliError("Usage: zo actions describe <action>");
265
+ if (options.envelope)
266
+ throw new CliError("--envelope is only supported for action invocation.");
267
+ const parsed = commandOptions(args.slice(describe ? 3 : 2), describe ? ["catalog", "version"] : ["catalog"]);
268
+ const mode = parsed.values.get("catalog") ?? "scoped";
269
+ if (mode !== "scoped" && mode !== "all")
270
+ throw new CliError("--catalog must be scoped or all.");
271
+ const { loadCatalog, listActions, describeAction, parseVersion } = await import("./catalog.js");
272
+ const version = parsed.values.get("version");
273
+ const parsedVersion = version === undefined ? undefined : parseVersion(version);
274
+ const { catalog } = await loadCatalog(context, { catalog: mode });
275
+ writeJson(action
276
+ ? describeAction(catalog, action, parsedVersion)
277
+ : listActions(catalog), output);
278
+ return;
279
+ }
280
+ const { matchBuiltinCommand, runBuiltin } = await import("./commands.js");
281
+ const command = matchBuiltinCommand(args);
282
+ if (!command)
283
+ throw new CliError("Unknown command. Run zo --help.");
284
+ const result = await runBuiltin(context, command.action, command.args, runtime.stdin);
285
+ (await import("./presentation.js")).writeBuiltinResult(command.action, result, { ...output, workspaceId: context.workspaceId });
286
+ }
287
+ finally {
288
+ process.removeListener("SIGINT", interrupt);
289
+ }
290
+ }
291
+ export async function execute(argv = process.argv.slice(2), runtime = {}) {
292
+ try {
293
+ await run(argv, runtime);
294
+ return 0;
295
+ }
296
+ catch (error) {
297
+ const { writeError } = await import("./output.js");
298
+ return writeError(error, {
299
+ ...runtime,
300
+ json: argv.some((arg) => arg === "--json"),
301
+ });
302
+ }
303
+ }
304
+ if (process.argv[1] &&
305
+ import.meta.url === pathToFileURL(await realpath(process.argv[1])).href)
306
+ process.exitCode = await execute();
@@ -0,0 +1,21 @@
1
+ import { type JsonObject } from "@zocomputer/system-api/json";
2
+ import type { RpcResult } from "./rpc.ts";
3
+ export interface OutputOptions {
4
+ json?: boolean;
5
+ envelope?: boolean;
6
+ stdout?: {
7
+ write(text: string): unknown;
8
+ isTTY?: boolean;
9
+ };
10
+ stderr?: {
11
+ write(text: string): unknown;
12
+ isTTY?: boolean;
13
+ };
14
+ }
15
+ export declare function writeJsonText(text: string, options?: OutputOptions): void;
16
+ export declare function writeJson(value: unknown, options?: OutputOptions): void;
17
+ export declare function writeRpcResult(result: RpcResult, options?: OutputOptions): void;
18
+ /** Mirror the public error detail fields in hostagent.system_api.errors. */
19
+ export declare function safeDetails(code: string, details: unknown): JsonObject;
20
+ export declare function exitCode(error: unknown): number;
21
+ export declare function writeError(error: unknown, options?: OutputOptions): number;