@zocomputer/cli 0.2.1 → 0.3.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/dist/main.js CHANGED
@@ -1,313 +1,8 @@
1
1
  #!/usr/bin/env node
2
- import { readFile, realpath } from "node:fs/promises";
3
- import { createInterface } from "node:readline/promises";
2
+ import { realpath } from "node:fs/promises";
4
3
  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 a sign-in URL and code, open your browser, and wait for approval.\nNew profiles use production. Browser consent approves workspaces and identities.\n--no-browser prints the URL without opening a browser; approve it on any device.\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
- function identity(session, workspace) {
45
- const { type, id } = workspace.subject;
46
- if (type === "user" &&
47
- session.approving_subject?.type === "user" &&
48
- session.approving_subject.id === id)
49
- return "as you";
50
- if (type === "service_account")
51
- return `as service account ${safeText(id)}`;
52
- return `as ${type} ${safeText(id)}`;
53
- }
54
- async function showHelp(args, stdout) {
55
- if (!args.length) {
56
- stdout.write(help);
57
- return;
58
- }
59
- const path = args.slice(0, args.findIndex((arg) => arg.startsWith("--")) === -1
60
- ? undefined
61
- : args.findIndex((arg) => arg.startsWith("--")));
62
- const key = path
63
- .slice(0, ["auth", "context", "actions"].includes(path[0] ?? "") ? 2 : 1)
64
- .join(" ");
65
- if (Object.hasOwn(localHelp, key)) {
66
- stdout.write(localHelp[key] ?? "");
67
- return;
68
- }
69
- stdout.write((await import("./commands.js")).builtinHelp(path));
70
- }
71
- export async function run(argv = process.argv.slice(2), runtime = {}) {
72
- const { args, options } = parseGlobals(argv);
73
- const stdout = runtime.stdout ?? process.stdout;
74
- const stderr = runtime.stderr ?? process.stderr;
75
- const output = {
76
- stdout,
77
- stderr,
78
- json: options.json,
79
- envelope: options.envelope,
80
- };
81
- const env = runtime.env ?? process.env;
82
- if (options.help || !args.length) {
83
- await showHelp(args, stdout);
84
- return;
85
- }
86
- if (args[0] === "--version") {
87
- if (args.length !== 1)
88
- throw new CliError("Usage: zo --version");
89
- const manifest = JSON.parse(await readFile(new URL("../package.json", import.meta.url), "utf8"));
90
- if (!record(manifest) || typeof manifest.version !== "string")
91
- throw new CliError("The installed package has no version.", 5);
92
- stdout.write(`${manifest.version}\n`);
93
- return;
94
- }
95
- if (args[0] === "completion") {
96
- if (args.length !== 2 || !args[1])
97
- throw new CliError(localHelp.completion ?? "Invalid completion command.");
98
- stdout.write((await import("./commands.js")).completionScript(args[1]));
99
- return;
100
- }
101
- const local = ["login", "logout", "auth", "context"].includes(args[0] ?? "");
102
- if (options.envelope && local)
103
- throw new CliError("--envelope is only supported for action invocation.");
104
- const controller = new AbortController();
105
- const interrupt = () => controller.abort();
106
- process.once("SIGINT", interrupt);
107
- const signal = runtime.signal
108
- ? AbortSignal.any([controller.signal, runtime.signal])
109
- : controller.signal;
110
- try {
111
- throwIfInterrupted(signal);
112
- const profiles = runtime.profiles ?? new Profiles();
113
- const { writeJson } = await import("./output.js");
114
- const writeLocalJson = (value) => {
115
- throwIfInterrupted(signal);
116
- writeJson(value, output);
117
- };
118
- if (args[0] === "login") {
119
- const parsed = commandOptions(args.slice(1), ["environment"], ["no-browser"]);
120
- const requested = parsed.values.get("environment");
121
- if (requested !== undefined &&
122
- requested !== "production" &&
123
- requested !== "staging")
124
- throw new CliError("--environment must be production or staging.");
125
- const profile = await profiles.create(options.profile, requested, signal);
126
- const auth = new AuthClient(profile, profiles.directory, {
127
- env,
128
- fetch: runtime.fetch,
129
- signal,
130
- });
131
- const login = await auth.login({
132
- noBrowser: parsed.flags.has("no-browser"),
133
- signal,
134
- announce: ({ url, userCode, browser }) => stderr.write(`${browser
135
- ? "Opening your browser to sign in. If it doesn't open, visit:"
136
- : "To sign in, visit this URL on any device:"}\n\n ${url}\n\nCode: ${userCode}. Approve only if your browser shows the same code.\nWaiting for approval… (Ctrl-C to cancel)\n`),
137
- });
138
- stderr.write(`\nApproved in the browser. Sign-in saved for profile ${profile.name}.\n`);
139
- if (login.cleanupPending)
140
- 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");
141
- const session = await auth.session();
142
- // Always list the approved workspaces, so a sign-in someone else
143
- // approved shows up as workspaces or identities you don't recognize.
144
- session.workspaces.forEach((workspace, index) => stderr.write(`${index + 1}. ${safeText(workspace.name)} (${workspace.workspace_id}), ${identity(session, workspace)}\n`));
145
- const desired = options.workspace ?? env.ZO_WORKSPACE_ID ?? profile.workspaceId;
146
- let chosen;
147
- if (options.workspace !== undefined)
148
- chosen = authorizedWorkspace(session, options.workspace).workspace_id;
149
- else if (desired !== undefined) {
150
- chosen = session.workspaces.find((workspace) => workspace.workspace_id === desired)?.workspace_id;
151
- if (!chosen)
152
- stderr.write(env.ZO_WORKSPACE_ID !== undefined
153
- ? "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"
154
- : "The saved workspace is not authorized by the new sign-in. Choose a workspace explicitly; the previous selection is unchanged.\n");
155
- }
156
- else if (session.workspaces.length === 1)
157
- chosen = session.workspaces[0]?.workspace_id;
158
- if (!chosen &&
159
- !runtime.stdin &&
160
- process.stdin.isTTY &&
161
- stderr.isTTY &&
162
- session.workspaces.length > 0) {
163
- const input = createInterface({
164
- input: process.stdin,
165
- output: process.stderr,
166
- });
167
- try {
168
- const answer = await input.question("Choose a workspace number: ", {
169
- signal,
170
- });
171
- if (/^[1-9][0-9]*$/.test(answer))
172
- chosen = session.workspaces[Number(answer) - 1]?.workspace_id;
173
- }
174
- finally {
175
- input.close();
176
- }
177
- }
178
- if (!chosen) {
179
- stderr.write("Select an authorized workspace with zo context use <workspace-id>.\n");
180
- writeLocalJson({
181
- profile: profile.name,
182
- signed_in: true,
183
- workspace_id: null,
184
- selection_required: true,
185
- });
186
- return;
187
- }
188
- await profiles.select(profile, chosen, signal);
189
- writeLocalJson({ profile: profile.name, workspace_id: chosen });
190
- return;
191
- }
192
- const profile = await profiles.resolve(options.profile);
193
- throwIfInterrupted(signal);
194
- const auth = new AuthClient(profile, profiles.directory, {
195
- env,
196
- fetch: runtime.fetch,
197
- signal,
198
- });
199
- if (args[0] === "logout") {
200
- const parsed = commandOptions(args.slice(1), [], ["local-only"]);
201
- const localOnly = parsed.flags.has("local-only");
202
- await auth.logout(localOnly);
203
- throwIfInterrupted(signal);
204
- stderr.write(localOnly
205
- ? "Local credentials removed. Server access was not revoked.\n"
206
- : "Signed out; saved sign-ins revoked.\n");
207
- return;
208
- }
209
- if (args[0] === "auth" && args[1] === "token") {
210
- const parsed = commandOptions(args.slice(2), [], ["headers"]);
211
- const token = await auth.accessToken();
212
- throwIfInterrupted(signal);
213
- if (parsed.flags.has("headers"))
214
- writeLocalJson({ Authorization: `Bearer ${token}` });
215
- else
216
- stdout.write(`${token}\n`);
217
- return;
218
- }
219
- if ((args[0] === "auth" && args[1] === "status") ||
220
- (args[0] === "context" && args[1] === "show")) {
221
- commandOptions(args.slice(2), []);
222
- writeLocalJson(await contextStatus(profile, auth, options.workspace, env));
223
- return;
224
- }
225
- if (args[0] === "context" && args[1] === "list") {
226
- commandOptions(args.slice(2), []);
227
- writeLocalJson((await auth.session()).workspaces);
228
- return;
229
- }
230
- if (args[0] === "context" && args[1] === "use") {
231
- if (args.length !== 3 || !args[2])
232
- throw new CliError("Usage: zo context use <workspace-id>");
233
- writeLocalJson(await useContext(profiles, profile, auth, args[2], signal));
234
- return;
235
- }
236
- if (local)
237
- throw new CliError("Unknown command. Run zo --help.");
238
- const context = {
239
- workspaceId: selectedWorkspace(profile, options.workspace, env),
240
- apiOrigin: auth.binding.api,
241
- accessToken: () => auth.accessToken(),
242
- signal,
243
- ...(runtime.fetch ? { fetch: runtime.fetch } : {}),
244
- };
245
- if (args[0] === "rpc") {
246
- const action = args[1];
247
- if (!action || action.startsWith("--"))
248
- throw new CliError("Usage: zo rpc <action> [options]");
249
- const parsed = commandOptions(args.slice(2), ["input", "input-file", "version", "query", "idempotency-key"], ["preview"]);
250
- const { parseVersion } = await import("./catalog.js");
251
- const version = parsed.values.get("version");
252
- const result = await (await import("./rpc.js")).runRpc(context, {
253
- action,
254
- input: parsed.values.get("input"),
255
- inputFile: parsed.values.get("input-file"),
256
- query: parsed.values.get("query"),
257
- version: version === undefined ? undefined : parseVersion(version),
258
- idempotencyKey: parsed.values.get("idempotency-key"),
259
- preview: parsed.flags.has("preview"),
260
- stdin: runtime.stdin,
261
- });
262
- (await import("./output.js")).writeRpcResult(result, output);
263
- return;
264
- }
265
- if (args[0] === "actions") {
266
- const describe = args[1] === "describe";
267
- if (args[1] !== "list" && !describe)
268
- throw new CliError("Usage: zo actions list | describe <action>");
269
- const action = describe ? args[2] : undefined;
270
- if (describe && (!action || action.startsWith("--")))
271
- throw new CliError("Usage: zo actions describe <action>");
272
- if (options.envelope)
273
- throw new CliError("--envelope is only supported for action invocation.");
274
- const parsed = commandOptions(args.slice(describe ? 3 : 2), describe ? ["catalog", "version"] : ["catalog"]);
275
- const mode = parsed.values.get("catalog") ?? "scoped";
276
- if (mode !== "scoped" && mode !== "all")
277
- throw new CliError("--catalog must be scoped or all.");
278
- const { loadCatalog, listActions, describeAction, parseVersion } = await import("./catalog.js");
279
- const version = parsed.values.get("version");
280
- const parsedVersion = version === undefined ? undefined : parseVersion(version);
281
- const { catalog } = await loadCatalog(context, { catalog: mode });
282
- writeJson(action
283
- ? describeAction(catalog, action, parsedVersion)
284
- : listActions(catalog), output);
285
- return;
286
- }
287
- const { matchBuiltinCommand, runBuiltin } = await import("./commands.js");
288
- const command = matchBuiltinCommand(args);
289
- if (!command)
290
- throw new CliError("Unknown command. Run zo --help.");
291
- const result = await runBuiltin(context, command.action, command.args, runtime.stdin);
292
- (await import("./presentation.js")).writeBuiltinResult(command.action, result, { ...output, workspaceId: context.workspaceId });
293
- }
294
- finally {
295
- process.removeListener("SIGINT", interrupt);
296
- }
297
- }
298
- export async function execute(argv = process.argv.slice(2), runtime = {}) {
299
- try {
300
- await run(argv, runtime);
301
- return 0;
302
- }
303
- catch (error) {
304
- const { writeError } = await import("./output.js");
305
- return writeError(error, {
306
- ...runtime,
307
- json: argv.some((arg) => arg === "--json"),
308
- });
309
- }
310
- }
4
+ import { execute } from "./runner.js";
5
+ export * from "./runner.js";
311
6
  if (process.argv[1] &&
312
7
  import.meta.url === pathToFileURL(await realpath(process.argv[1])).href)
313
8
  process.exitCode = await execute();
@@ -1,15 +1,20 @@
1
+ /**
2
+ * The Zo CLI's OAuth client metadata document, served from www as its own
3
+ * client ID for both environments; its redirects are the loopback callbacks.
4
+ */
5
+ export declare const CLIENT_ID = "https://www.zo.computer/oauth/zo-cli-client-metadata.json";
1
6
  export declare const environments: {
2
7
  readonly production: {
3
8
  readonly issuer: "https://auth.zo.computer";
4
9
  readonly api: "https://api.zo.computer";
5
10
  readonly audience: "https://api.zo.computer/zo-system-api";
6
- readonly clientId: "zo-public-cli";
11
+ readonly clientId: "https://www.zo.computer/oauth/zo-cli-client-metadata.json";
7
12
  };
8
13
  readonly staging: {
9
14
  readonly issuer: "https://auth.zostaging.computer";
10
15
  readonly api: "https://api.zostaging.computer";
11
16
  readonly audience: "https://api.zostaging.computer/zo-system-api";
12
- readonly clientId: "zo-public-cli";
17
+ readonly clientId: "https://www.zo.computer/oauth/zo-cli-client-metadata.json";
13
18
  };
14
19
  };
15
20
  export type Environment = keyof typeof environments;
package/dist/profiles.js CHANGED
@@ -1,18 +1,23 @@
1
1
  import { join } from "node:path";
2
2
  import { CliError, record, throwIfInterrupted } from "./errors.js";
3
3
  import { configDirectory, privateDirectory, readPrivate, withLock, writePrivate, } from "./storage.js";
4
+ /**
5
+ * The Zo CLI's OAuth client metadata document, served from www as its own
6
+ * client ID for both environments; its redirects are the loopback callbacks.
7
+ */
8
+ export const CLIENT_ID = "https://www.zo.computer/oauth/zo-cli-client-metadata.json";
4
9
  export const environments = {
5
10
  production: {
6
11
  issuer: "https://auth.zo.computer",
7
12
  api: "https://api.zo.computer",
8
13
  audience: "https://api.zo.computer/zo-system-api",
9
- clientId: "zo-public-cli",
14
+ clientId: CLIENT_ID,
10
15
  },
11
16
  staging: {
12
17
  issuer: "https://auth.zostaging.computer",
13
18
  api: "https://api.zostaging.computer",
14
19
  audience: "https://api.zostaging.computer/zo-system-api",
15
- clientId: "zo-public-cli",
20
+ clientId: CLIENT_ID,
16
21
  },
17
22
  };
18
23
  export function profileName(name) {
@@ -0,0 +1,16 @@
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
+ version?: string;
9
+ profiles?: Profiles;
10
+ env?: NodeJS.ProcessEnv;
11
+ fetch?: InvocationContext["fetch"];
12
+ stdin?: RpcOptions["stdin"];
13
+ signal?: AbortSignal;
14
+ }
15
+ export declare function run(argv?: string[], runtime?: RunOptions): Promise<void>;
16
+ export declare function execute(argv?: string[], runtime?: RunOptions): Promise<number>;