@zocomputer/cli 0.0.0-stage → 0.1.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.
@@ -0,0 +1,193 @@
1
+ import { defineRuntimeCatalog, SystemAPIError, } from "@zocomputer/system-api";
2
+ import { loadNormalizedCatalog, } from "@zocomputer/system-api/catalog";
3
+ import { canonicalNumberFromToken, JsonNumber, } from "@zocomputer/system-api/json";
4
+ import { CliError } from "./errors.js";
5
+ import { systemApiFailure } from "./http.js";
6
+ const MAX_CATALOG_BYTES = 4 * 1024 * 1024;
7
+ export function apiBaseUrl(context) {
8
+ return new URL("/zo-system-api/v1", context.apiOrigin).href;
9
+ }
10
+ function invalidCatalog(message) {
11
+ return new SystemAPIError({
12
+ code: "invalid_response",
13
+ message,
14
+ outcome: "not_started",
15
+ });
16
+ }
17
+ async function readCatalog(response) {
18
+ const reader = response.body?.getReader();
19
+ if (!reader)
20
+ throw invalidCatalog("The catalog response is empty.");
21
+ const chunks = [];
22
+ let size = 0;
23
+ try {
24
+ for (;;) {
25
+ const { done, value } = await reader.read();
26
+ if (done)
27
+ break;
28
+ size += value.byteLength;
29
+ if (size > MAX_CATALOG_BYTES) {
30
+ // Cancellation is cleanup; its failure must not hide the size error.
31
+ await reader.cancel().catch(() => undefined);
32
+ throw invalidCatalog("The catalog response exceeds the supported size.");
33
+ }
34
+ chunks.push(value);
35
+ }
36
+ }
37
+ finally {
38
+ reader.releaseLock();
39
+ }
40
+ const bytes = new Uint8Array(size);
41
+ let offset = 0;
42
+ for (const chunk of chunks) {
43
+ bytes.set(chunk, offset);
44
+ offset += chunk.byteLength;
45
+ }
46
+ return bytes;
47
+ }
48
+ export async function loadCatalog(context, options = {}) {
49
+ if (!/^wsp_[A-Za-z0-9_-]+$/.test(context.workspaceId))
50
+ throw new CliError("Select a workspace with zo context use <workspace-id> or --workspace.");
51
+ if (context.signal?.aborted)
52
+ throw new SystemAPIError({
53
+ code: "interrupted",
54
+ message: "Interrupted.",
55
+ outcome: "not_started",
56
+ });
57
+ const mode = options.catalog ?? "scoped";
58
+ const url = new URL(`${apiBaseUrl(context)}/catalog/export`);
59
+ url.searchParams.set("workspace_id", context.workspaceId);
60
+ url.searchParams.set("catalog", mode);
61
+ const token = await context.accessToken();
62
+ if (!/^[\x21-\x7e]+$/.test(token))
63
+ throw new CliError("The access token is invalid. Replace ZO_SYSTEM_API_ACCESS_TOKEN if set, or unset it and run zo login.", 3, "unauthenticated");
64
+ const timeout = AbortSignal.timeout(30_000);
65
+ const signal = context.signal
66
+ ? AbortSignal.any([context.signal, timeout])
67
+ : timeout;
68
+ let response;
69
+ let bytes;
70
+ try {
71
+ response = await (context.fetch ?? fetch)(url, {
72
+ headers: { authorization: `Bearer ${token}`, accept: "application/json" },
73
+ redirect: "error",
74
+ cache: "no-store",
75
+ signal,
76
+ });
77
+ bytes = await readCatalog(response);
78
+ }
79
+ catch (error) {
80
+ if (signal.aborted)
81
+ throw new SystemAPIError({
82
+ code: context.signal?.aborted
83
+ ? "interrupted"
84
+ : "execution_deadline_exceeded",
85
+ message: context.signal?.aborted
86
+ ? "Interrupted."
87
+ : "The catalog request deadline expired.",
88
+ outcome: "not_started",
89
+ });
90
+ if (error instanceof SystemAPIError)
91
+ throw error;
92
+ throw new SystemAPIError({
93
+ code: "network",
94
+ message: "Could not read the workspace catalog.",
95
+ outcome: "not_started",
96
+ });
97
+ }
98
+ if (!response.ok)
99
+ throw systemApiFailure(response, bytes, "Catalog discovery");
100
+ let catalog;
101
+ try {
102
+ catalog = loadNormalizedCatalog(bytes).catalog;
103
+ }
104
+ catch {
105
+ throw invalidCatalog("The service returned an invalid catalog export.");
106
+ }
107
+ if (catalog.selection?.workspace !== context.workspaceId ||
108
+ catalog.selection.catalog !== mode) {
109
+ throw invalidCatalog("The catalog selection does not match the requested workspace and catalog.");
110
+ }
111
+ return {
112
+ authenticatedContext: { ...context, accessToken: async () => token },
113
+ catalog,
114
+ runtimeCatalog: defineRuntimeCatalog({
115
+ generatorVersion: "live",
116
+ actions: Object.fromEntries(catalog.operations.map((operation) => [
117
+ operation.action,
118
+ {
119
+ ...(operation.defaultVersion !== undefined
120
+ ? { defaultVersion: operation.defaultVersion }
121
+ : {}),
122
+ versions: Object.fromEntries(operation.versions.map((version) => [
123
+ version.version,
124
+ { digest: version.digest },
125
+ ])),
126
+ },
127
+ ])),
128
+ }),
129
+ };
130
+ }
131
+ export function parseVersion(value) {
132
+ if (!/^[1-9][0-9]*$/.test(value) || Number(value) > 2_147_483_647)
133
+ throw new CliError("--version must be a positive 32-bit integer.");
134
+ return Number(value);
135
+ }
136
+ function findAction(catalog, action) {
137
+ const entry = catalog.operations.find((entry) => entry.action === action);
138
+ if (!entry)
139
+ throw new CliError(`No callable contract for ${action} is visible in this catalog. Use zo actions list or zo actions describe ${action} --catalog all to inspect definitions.`, 2, "action_not_found");
140
+ return entry;
141
+ }
142
+ export function resolveAction(catalog, action, explicitVersion) {
143
+ const entry = findAction(catalog, action);
144
+ const version = explicitVersion ?? entry.defaultVersion;
145
+ if (version === undefined)
146
+ throw new CliError(`${action} has no default version. Choose --version from zo actions describe ${action}.`, 2, "version_required");
147
+ const contract = entry.versions.find((candidate) => candidate.version === version);
148
+ if (!contract)
149
+ throw new CliError(`${action} version ${version} is absent from this catalog. Available versions: ${entry.versions.map((item) => item.version).join(", ")}.`, 2, "unsupported_version");
150
+ return contract;
151
+ }
152
+ function number(value) {
153
+ return canonicalNumberFromToken(String(value));
154
+ }
155
+ function versionSummary(version) {
156
+ return {
157
+ version: number(version.version),
158
+ digest: version.digest,
159
+ ...(version.description !== undefined
160
+ ? { description: version.description }
161
+ : {}),
162
+ ...(version.availability !== undefined
163
+ ? { availability: version.availability }
164
+ : {}),
165
+ };
166
+ }
167
+ export function listActions(catalog) {
168
+ return catalog.operations.map((operation) => ({
169
+ action: operation.action,
170
+ default_version: operation.defaultVersion === undefined
171
+ ? null
172
+ : number(operation.defaultVersion),
173
+ versions: operation.versions.map(versionSummary),
174
+ }));
175
+ }
176
+ export function describeAction(catalog, action, version) {
177
+ const operation = findAction(catalog, action);
178
+ const versions = version === undefined
179
+ ? operation.versions
180
+ : [resolveAction(catalog, action, version)];
181
+ return {
182
+ action,
183
+ default_version: operation.defaultVersion === undefined
184
+ ? null
185
+ : number(operation.defaultVersion),
186
+ versions: versions.map((entry) => ({
187
+ ...versionSummary(entry),
188
+ input_schema: entry.input,
189
+ output_schema: entry.output,
190
+ ...(entry.query !== undefined ? { query_schema: entry.query } : {}),
191
+ })),
192
+ };
193
+ }
@@ -0,0 +1,9 @@
1
+ import { type InvocationContext } from "./catalog.ts";
2
+ import { type RpcResult, type RpcOptions } from "./rpc.ts";
3
+ export declare function matchBuiltinCommand(args: readonly string[]): {
4
+ action: string;
5
+ args: string[];
6
+ } | undefined;
7
+ export declare function runBuiltin(context: InvocationContext, action: string, args: readonly string[], stdin?: RpcOptions["stdin"]): Promise<RpcResult>;
8
+ export declare function builtinHelp(path?: readonly string[]): string;
9
+ export declare function completionScript(shell: string): string;
@@ -0,0 +1,119 @@
1
+ import { loadsJson, dumpsJson } from "@zocomputer/system-api/json";
2
+ import { loadCatalog, parseVersion, resolveAction, } from "./catalog.js";
3
+ import { CliError, safeText } from "./errors.js";
4
+ import { encodeCommandFlags, inputFlags, parseCommandFlags, reservedOptions, resolvedSchema, scalarType, schemaObject, } from "./flags.js";
5
+ import { commandManifest } from "./generated/commands.js";
6
+ import { inputJson, runRpc } from "./rpc.js";
7
+ export function matchBuiltinCommand(args) {
8
+ const command = commandManifest.commands.find((command) => command.path.every((segment, index) => args[index] === segment));
9
+ return command
10
+ ? { action: command.action, args: args.slice(command.path.length) }
11
+ : undefined;
12
+ }
13
+ export async function runBuiltin(context, action, args, stdin) {
14
+ if (!commandManifest.commands.some((command) => command.action === action))
15
+ throw new CliError("This action has no installed short command. Use zo rpc.");
16
+ const parsed = parseCommandFlags(args);
17
+ const versionText = parsed.values.get("version");
18
+ const version = versionText === undefined ? undefined : parseVersion(versionText);
19
+ if (parsed.values.has("input-file")) {
20
+ const input = await inputJson({
21
+ input: parsed.values.get("input"),
22
+ inputFile: parsed.values.get("input-file"),
23
+ stdin,
24
+ signal: context.signal,
25
+ });
26
+ parsed.values.delete("input-file");
27
+ parsed.values.set("input", input);
28
+ }
29
+ const loaded = await loadCatalog(context);
30
+ const contract = resolveAction(loaded.catalog, action, version);
31
+ const options = await encodeCommandFlags(parsed, contract, context.workspaceId, stdin, context.signal);
32
+ return runRpc(context, { action, version: contract.version, ...options }, loaded);
33
+ }
34
+ const invocationHelp = `
35
+ --input <json> | --input-file <path|-> Whole input; cannot mix with input flags
36
+ --version <integer> Select a live contract version
37
+ --preview Print request without invoking (not a server dry-run)
38
+ --json Complete result JSON
39
+ --envelope Complete success envelope
40
+ --workspace <id> --profile <name> Select request context
41
+
42
+ Boolean flags require true or false. Complex fields and explicit null use whole-input JSON.
43
+ Declared workspace_id defaults to the selected workspace; a conflict is refused.
44
+ Lists return one page; next_cursor is preserved. The CLI never retries an invocation.
45
+ Async actions return their actual accepted/run/operation state; there is no universal --wait.
46
+ `;
47
+ export function builtinHelp(path = []) {
48
+ const commands = commandManifest.commands.filter((command) => path.every((segment, index) => command.path[index] === segment));
49
+ if (!commands.length)
50
+ throw new CliError("Unknown built-in command. Run zo --help.");
51
+ const command = commands.find((command) => command.path.length === path.length);
52
+ if (!command)
53
+ 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`;
54
+ const lines = [
55
+ `Usage: zo ${command.path.join(" ")} [options]`,
56
+ "",
57
+ `Action: ${command.action}`,
58
+ "Installed metadata only. The live catalog chooses the default version.",
59
+ ];
60
+ for (const version of command.versions) {
61
+ const input = loadsJson(version.inputJson);
62
+ lines.push("", `Version ${version.version}: ${safeText(version.description)}`, "Input flags:");
63
+ const flags = inputFlags(input);
64
+ for (const flag of flags)
65
+ lines.push(` --${flag.name} <${flag.type}>${flag.required ? " (required)" : ""}${flag.field === "workspace_id" ? " (defaults to selected workspace)" : ""}${flag.choices ? `; choices: ${safeText(dumpsJson([...flag.choices]))}` : ""}${flag.description ? ` ${safeText(flag.description)}` : ""}`);
66
+ const properties = schemaObject(resolvedSchema(input)?.properties) ?? {};
67
+ const complex = Object.keys(properties).filter((field) => !flags.some((flag) => flag.field === field));
68
+ if (complex.length)
69
+ lines.push(` Whole-input JSON fields: ${safeText(complex.join(", "))}`);
70
+ if ("queryJson" in version && version.queryJson) {
71
+ const query = loadsJson(version.queryJson);
72
+ const properties = schemaObject(resolvedSchema(query)?.properties) ?? {};
73
+ lines.push("Query options:", " --query <json> (cannot mix with individual query options)");
74
+ for (const [name, schema] of Object.entries(properties)) {
75
+ if (["limit", "cursor", "sort", "search", "filter"].includes(name))
76
+ lines.push(` --${name} <${scalarType(schema, query) ?? "json"}>`);
77
+ }
78
+ }
79
+ }
80
+ if (command.action === "zo.function.invoke")
81
+ lines.push(" --idempotency-key <key> Envelope key; repeating an invocation is never automatic.");
82
+ const readCommand = commandManifest.commands.find((candidate) => candidate.path.slice(0, -1).join(" ") ===
83
+ command.path.slice(0, -1).join(" ") && candidate.path.at(-1) === "get");
84
+ if (readCommand && !["get", "list"].includes(command.path.at(-1) ?? ""))
85
+ lines.push(`Inspect current state with zo ${readCommand.path.join(" ")} --help.`);
86
+ if (command.versions.some((version) => version.resultKind === "run"))
87
+ lines.push(`Inspect runs with zo ${command.action.startsWith("zo.workflow.") ? "workflow" : "function"} get-run --help.`);
88
+ return `${lines.join("\n")}\n${invocationHelp}`;
89
+ }
90
+ export function completionScript(shell) {
91
+ const words = [
92
+ ...new Set([
93
+ "login",
94
+ "logout",
95
+ "auth",
96
+ "context",
97
+ "actions",
98
+ "rpc",
99
+ "completion",
100
+ "status",
101
+ "token",
102
+ "list",
103
+ "describe",
104
+ "use",
105
+ "show",
106
+ ...[...reservedOptions].map((option) => `--${option}`),
107
+ ...commandManifest.commands.flatMap((command) => command.path),
108
+ ...commandManifest.commands.flatMap((command) => command.versions.flatMap((version) => inputFlags(loadsJson(version.inputJson)).map((flag) => `--${flag.name}`))),
109
+ ]),
110
+ ].sort();
111
+ const list = words.join(" ");
112
+ if (shell === "bash")
113
+ return `_zo_complete() { local words='${list}'; COMPREPLY=( $(compgen -W "$words" -- "\${COMP_WORDS[COMP_CWORD]}") ); }\ncomplete -F _zo_complete zo\n`;
114
+ if (shell === "zsh")
115
+ return `#compdef zo\n_zo() { local -a words; words=(${list}); _describe 'zo command or option' words; }\ncompdef _zo zo\n`;
116
+ if (shell === "fish")
117
+ return `complete -c zo -f -a '${list}'\n`;
118
+ throw new CliError("Completion shell must be bash, zsh or fish.");
119
+ }
@@ -0,0 +1,15 @@
1
+ import { AuthClient } from "./auth.ts";
2
+ import { Profiles, type Profile } from "./profiles.ts";
3
+ import type { Session, Workspace } from "./session.ts";
4
+ export declare function authorizedWorkspace(session: Session, id: string): Workspace;
5
+ export declare function useContext(profiles: Profiles, profile: Profile, auth: AuthClient, id: string, signal?: AbortSignal): Promise<Workspace>;
6
+ export declare function contextStatus(profile: Profile, auth: AuthClient, explicit?: string, env?: NodeJS.ProcessEnv): Promise<{
7
+ profile: string;
8
+ environment: "production" | "staging";
9
+ issuer: string;
10
+ credential_kind: "sign_in" | "bearer";
11
+ approving_subject: import("./session.ts").Subject | null;
12
+ expires_at: string | null;
13
+ access_token_expires_at: string;
14
+ selected_workspace: Workspace | null;
15
+ }>;
@@ -0,0 +1,30 @@
1
+ import { CliError } from "./errors.js";
2
+ import { AuthClient } from "./auth.js";
3
+ import { Profiles, selectedWorkspace } from "./profiles.js";
4
+ export function authorizedWorkspace(session, id) {
5
+ const selected = session.workspaces.find((workspace) => workspace.workspace_id === id);
6
+ if (!selected)
7
+ throw new CliError("The selected workspace is not authorized. Run zo login to approve it or explicitly choose another workspace.", 4, "insufficient_scope");
8
+ return selected;
9
+ }
10
+ export async function useContext(profiles, profile, auth, id, signal) {
11
+ const selected = authorizedWorkspace(await auth.session(), id);
12
+ await profiles.select(profile, selected.workspace_id, signal);
13
+ return selected;
14
+ }
15
+ export async function contextStatus(profile, auth, explicit, env = process.env) {
16
+ const session = await auth.session();
17
+ const id = explicit ?? env.ZO_WORKSPACE_ID ?? profile.workspaceId;
18
+ return {
19
+ profile: profile.name,
20
+ environment: profile.environment,
21
+ issuer: auth.binding.issuer,
22
+ credential_kind: session.credential_kind,
23
+ approving_subject: session.approving_subject,
24
+ expires_at: session.expires_at,
25
+ access_token_expires_at: session.access_token_expires_at,
26
+ selected_workspace: id
27
+ ? authorizedWorkspace(session, selectedWorkspace(profile, explicit, env))
28
+ : null,
29
+ };
30
+ }
@@ -0,0 +1,9 @@
1
+ export declare class CliError extends Error {
2
+ readonly exitCode: number;
3
+ readonly code: string;
4
+ constructor(message: string, exitCode?: number, code?: string);
5
+ }
6
+ export declare function throwIfInterrupted(signal?: AbortSignal | null): void;
7
+ export declare function record(value: unknown): value is Record<string, unknown>;
8
+ export declare function errorCode(error: unknown): string | undefined;
9
+ export declare function safeText(text: string): string;
package/dist/errors.js ADDED
@@ -0,0 +1,25 @@
1
+ export class CliError extends Error {
2
+ exitCode;
3
+ code;
4
+ constructor(message, exitCode = 2, code = "invalid_input") {
5
+ super(message);
6
+ this.exitCode = exitCode;
7
+ this.code = code;
8
+ this.name = "CliError";
9
+ }
10
+ }
11
+ export function throwIfInterrupted(signal) {
12
+ if (signal?.aborted)
13
+ throw new CliError("Interrupted.", 130, "interrupted");
14
+ }
15
+ export function record(value) {
16
+ return typeof value === "object" && value !== null && !Array.isArray(value);
17
+ }
18
+ export function errorCode(error) {
19
+ return record(error) && typeof error.code === "string"
20
+ ? error.code
21
+ : undefined;
22
+ }
23
+ export function safeText(text) {
24
+ return text.replace(/[\u0000-\u001f\u007f-\u009f]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
25
+ }
@@ -0,0 +1,27 @@
1
+ import { type JsonObject, type JsonValue } from "@zocomputer/system-api/json";
2
+ import { type RpcOptions } from "./rpc.ts";
3
+ export declare const reservedOptions: Set<string>;
4
+ export type ScalarType = "string" | "boolean" | "integer" | "number";
5
+ export interface InputFlag {
6
+ field: string;
7
+ name: string;
8
+ type: ScalarType;
9
+ required: boolean;
10
+ description?: string;
11
+ choices?: readonly JsonValue[];
12
+ }
13
+ export declare function schemaObject(value: JsonValue | undefined): JsonObject | undefined;
14
+ export declare function resolvedSchema(schema: JsonValue, root?: JsonValue): JsonObject | undefined;
15
+ export declare function scalarType(schema: JsonValue, root?: JsonValue): ScalarType | undefined;
16
+ export declare function inputFlags(schema: JsonValue): InputFlag[];
17
+ export declare function scalarValue(text: string, type: ScalarType, label: string): JsonValue;
18
+ export interface ParsedFlags {
19
+ values: Map<string, string>;
20
+ preview: boolean;
21
+ envelope: boolean;
22
+ }
23
+ export declare function parseCommandFlags(args: readonly string[]): ParsedFlags;
24
+ export declare function encodeCommandFlags(parsed: ParsedFlags, contract: {
25
+ input: JsonValue;
26
+ query?: JsonValue;
27
+ }, workspaceId: string, stdin?: RpcOptions["stdin"], signal?: AbortSignal): Promise<Pick<RpcOptions, "input" | "query" | "idempotencyKey" | "preview">>;
package/dist/flags.js ADDED
@@ -0,0 +1,211 @@
1
+ import { canonicalNumberFromToken, dumpsJson, JsonError, JsonNumber, } from "@zocomputer/system-api/json";
2
+ import { CliError } from "./errors.js";
3
+ import { inputJson, parseJsonObject } from "./rpc.js";
4
+ export const reservedOptions = new Set([
5
+ "help",
6
+ "version",
7
+ "profile",
8
+ "workspace",
9
+ "json",
10
+ "envelope",
11
+ "preview",
12
+ "input",
13
+ "input-file",
14
+ "query",
15
+ "idempotency-key",
16
+ "limit",
17
+ "cursor",
18
+ "sort",
19
+ "search",
20
+ "filter",
21
+ ]);
22
+ const queryOptions = new Set(["limit", "cursor", "sort", "search", "filter"]);
23
+ export function schemaObject(value) {
24
+ return value !== null &&
25
+ typeof value === "object" &&
26
+ !Array.isArray(value) &&
27
+ !(value instanceof JsonNumber)
28
+ ? value
29
+ : undefined;
30
+ }
31
+ export function resolvedSchema(schema, root = schema) {
32
+ let object = schemaObject(schema);
33
+ const seen = new Set();
34
+ while (typeof object?.$ref === "string") {
35
+ const reference = object.$ref;
36
+ if (!reference.startsWith("#/") || seen.has(reference))
37
+ return undefined;
38
+ seen.add(reference);
39
+ let target = root;
40
+ for (const segment of reference.slice(2).split("/")) {
41
+ target =
42
+ schemaObject(target)?.[segment.replaceAll("~1", "/").replaceAll("~0", "~")];
43
+ }
44
+ const resolved = schemaObject(target);
45
+ if (!resolved)
46
+ return undefined;
47
+ const { $ref: _reference, ...siblings } = object;
48
+ object = { ...resolved, ...siblings };
49
+ }
50
+ return object;
51
+ }
52
+ export function scalarType(schema, root = schema) {
53
+ const object = resolvedSchema(schema, root);
54
+ if (!object || ["anyOf", "oneOf", "allOf"].some((key) => key in object))
55
+ return undefined;
56
+ const type = object.type;
57
+ return type === "string" ||
58
+ type === "boolean" ||
59
+ type === "integer" ||
60
+ type === "number"
61
+ ? type
62
+ : undefined;
63
+ }
64
+ export function inputFlags(schema) {
65
+ const object = resolvedSchema(schema);
66
+ const properties = schemaObject(object?.properties) ?? {};
67
+ const candidates = Object.entries(properties).flatMap(([field, property]) => {
68
+ const type = scalarType(property, schema);
69
+ if (!type || !/^[a-z][a-z0-9_-]*$/.test(field))
70
+ return [];
71
+ const base = field.replaceAll("_", "-");
72
+ const name = reservedOptions.has(base) ? `input-${base}` : base;
73
+ const details = resolvedSchema(property, schema);
74
+ return [
75
+ {
76
+ field,
77
+ name,
78
+ type,
79
+ required: Array.isArray(object?.required) && object.required.includes(field),
80
+ ...(typeof details?.description === "string"
81
+ ? { description: details.description }
82
+ : {}),
83
+ ...(Array.isArray(details?.enum) ? { choices: details.enum } : {}),
84
+ },
85
+ ];
86
+ });
87
+ // Both ambiguous spellings remain available through whole-input JSON.
88
+ return candidates.filter((flag) => !reservedOptions.has(flag.name) &&
89
+ candidates.filter((other) => other.name === flag.name).length === 1);
90
+ }
91
+ export function scalarValue(text, type, label) {
92
+ if (type === "string")
93
+ return text;
94
+ if (type === "boolean") {
95
+ if (text === "true")
96
+ return true;
97
+ if (text === "false")
98
+ return false;
99
+ throw new CliError(`${label} requires true or false.`);
100
+ }
101
+ if (!/^-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?$/.test(text))
102
+ throw new CliError(`${label} requires a JSON ${type}.`);
103
+ try {
104
+ const number = canonicalNumberFromToken(text);
105
+ if (type === "integer" && !number.isInteger)
106
+ throw new CliError(`${label} requires an integer.`);
107
+ return number;
108
+ }
109
+ catch (error) {
110
+ if (error instanceof JsonError)
111
+ throw new CliError(`${label} requires a supported exact ${type}.`);
112
+ throw error;
113
+ }
114
+ }
115
+ export function parseCommandFlags(args) {
116
+ const values = new Map();
117
+ let preview = false;
118
+ let envelope = false;
119
+ for (let index = 0; index < args.length; index++) {
120
+ const arg = args[index];
121
+ if (arg === undefined || !arg.startsWith("--"))
122
+ throw new CliError("Expected a named option. Run this command with --help.");
123
+ const equal = arg.indexOf("=");
124
+ const name = arg.slice(2, equal === -1 ? undefined : equal);
125
+ if (values.has(name))
126
+ throw new CliError("Repeated command option.");
127
+ if (name === "preview" || name === "envelope") {
128
+ if (equal !== -1)
129
+ throw new CliError(`--${name} does not take a value.`);
130
+ if (name === "preview")
131
+ preview = true;
132
+ else
133
+ envelope = true;
134
+ values.set(name, "");
135
+ continue;
136
+ }
137
+ const value = equal === -1 ? args[++index] : arg.slice(equal + 1);
138
+ if (value === undefined || (equal === -1 && value.startsWith("--")))
139
+ throw new CliError("Command option requires a value. Use --name=value for values beginning with --.");
140
+ values.set(name, value);
141
+ }
142
+ return { values, preview, envelope };
143
+ }
144
+ export async function encodeCommandFlags(parsed, contract, workspaceId, stdin, signal) {
145
+ const fields = inputFlags(contract.input);
146
+ const properties = schemaObject(resolvedSchema(contract.input)?.properties) ?? {};
147
+ const input = {};
148
+ const query = {};
149
+ let scalarInput = false;
150
+ for (const [name, text] of parsed.values) {
151
+ if ([
152
+ "input",
153
+ "input-file",
154
+ "query",
155
+ "idempotency-key",
156
+ "version",
157
+ "preview",
158
+ "envelope",
159
+ ].includes(name))
160
+ continue;
161
+ if (queryOptions.has(name)) {
162
+ const querySchema = contract.query === undefined
163
+ ? undefined
164
+ : resolvedSchema(contract.query);
165
+ const schema = schemaObject(querySchema?.properties)?.[name];
166
+ if (schema === undefined)
167
+ throw new CliError(`This action does not declare --${name}.`, 2, "invalid_query");
168
+ const type = scalarType(schema, contract.query);
169
+ if (type)
170
+ query[name] = scalarValue(text, type, `--${name}`);
171
+ else if (name === "filter")
172
+ query[name] = parseJsonObject(text, "Filter");
173
+ else
174
+ throw new CliError(`Use --query JSON for this action's ${name} option.`);
175
+ continue;
176
+ }
177
+ const flag = fields.find((flag) => flag.name === name);
178
+ if (!flag)
179
+ throw new CliError("Unknown input option for the selected action version. Inspect zo actions describe or use --input JSON.");
180
+ scalarInput = true;
181
+ input[flag.field] = scalarValue(text, flag.type, `--${flag.name}`);
182
+ }
183
+ const fullInput = parsed.values.has("input") || parsed.values.has("input-file");
184
+ if (fullInput && scalarInput)
185
+ throw new CliError("Use whole-input JSON or generated input flags, never both.");
186
+ const body = fullInput
187
+ ? parseJsonObject(await inputJson({
188
+ input: parsed.values.get("input"),
189
+ inputFile: parsed.values.get("input-file"),
190
+ stdin,
191
+ signal,
192
+ }))
193
+ : input;
194
+ if ("workspace_id" in properties) {
195
+ if (body.workspace_id !== undefined && body.workspace_id !== workspaceId)
196
+ throw new CliError("Input workspace_id conflicts with the selected workspace.");
197
+ body.workspace_id = workspaceId;
198
+ }
199
+ if (parsed.values.has("query") && Object.keys(query).length)
200
+ throw new CliError("Use --query or individual query options, never both.");
201
+ const queryJson = parsed.values.get("query") ??
202
+ (Object.keys(query).length ? dumpsJson(query) : undefined);
203
+ return {
204
+ input: dumpsJson(body),
205
+ ...(queryJson !== undefined ? { query: queryJson } : {}),
206
+ ...(parsed.values.has("idempotency-key")
207
+ ? { idempotencyKey: parsed.values.get("idempotency-key") }
208
+ : {}),
209
+ preview: parsed.preview,
210
+ };
211
+ }
@@ -0,0 +1,23 @@
1
+ export declare const commandManifest: {
2
+ schemaDigest: string;
3
+ commands: ({
4
+ action: string;
5
+ path: string[];
6
+ versions: {
7
+ version: number;
8
+ description: string;
9
+ inputJson: string;
10
+ queryJson: string;
11
+ resultKind: string;
12
+ }[];
13
+ } | {
14
+ action: string;
15
+ path: string[];
16
+ versions: {
17
+ version: number;
18
+ description: string;
19
+ inputJson: string;
20
+ resultKind: string;
21
+ }[];
22
+ })[];
23
+ };