@zocomputer/cli 0.3.0 → 0.4.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/README.md +6 -6
- package/dist/commands.js +1 -0
- package/dist/generated/commands.js +14 -1
- package/dist/output.js +4 -2
- package/dist/rpc.js +1 -1
- package/dist/runner.js +15 -1
- 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
|
|
package/dist/commands.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Generated by scripts/generate-commands.mjs. Do not edit.
|
|
2
2
|
export const commandManifest = {
|
|
3
|
-
schemaDigest: "sha256:
|
|
3
|
+
schemaDigest: "sha256:f3723f93d9d519ebe2517e16cb3db50c99b38e60b9705187006ada3eaeb4e391",
|
|
4
4
|
commands: [
|
|
5
5
|
{
|
|
6
6
|
action: "zo.activity.list",
|
|
@@ -968,6 +968,19 @@ export const commandManifest = {
|
|
|
968
968
|
},
|
|
969
969
|
],
|
|
970
970
|
},
|
|
971
|
+
{
|
|
972
|
+
action: "zo.model.list",
|
|
973
|
+
path: ["model", "list"],
|
|
974
|
+
versions: [
|
|
975
|
+
{
|
|
976
|
+
version: 1,
|
|
977
|
+
description: "List the models this workspace can call, each with its ID, capabilities, accepted file types, reasoning levels, limits and current prices.",
|
|
978
|
+
inputJson: '{"additionalProperties":false,"properties":{"workspace_id":{"maxLength":200,"minLength":1,"title":"Workspace Id","type":"string"}},"required":["workspace_id"],"title":"ModelListInput","type":"object"}',
|
|
979
|
+
queryJson: '{"type":"object","additionalProperties":false,"properties":{"limit":{"type":"integer","minimum":1,"default":100,"description":"Rows per page, at most 200; a larger value is reduced to it."},"cursor":{"type":"string","minLength":1,"maxLength":5504,"description":"The previous page\'s next_cursor, sent with the same sort, filter and search."},"sort":{"type":"string","enum":["id.asc"],"default":"id.asc"},"filter":{"additionalProperties":false,"properties":{"capability":{"anyOf":[{"enum":["text","structured_output","embedding","rerank","image","speech","transcription","video"],"type":"string"},{"type":"null"}],"default":null,"description":"Only models with this capability.","title":"Capability"}},"type":"object","required":[],"default":{}}},"required":[]}',
|
|
980
|
+
resultKind: "inline",
|
|
981
|
+
},
|
|
982
|
+
],
|
|
983
|
+
},
|
|
971
984
|
{
|
|
972
985
|
action: "zo.oauth_app.create",
|
|
973
986
|
path: ["oauth-app", "create"],
|
package/dist/output.js
CHANGED
|
@@ -55,7 +55,7 @@ export function writeRpcResult(result, options = {}) {
|
|
|
55
55
|
}
|
|
56
56
|
const detailFields = {
|
|
57
57
|
invalid_envelope: ["reason"],
|
|
58
|
-
conflict: ["reason", "pointer"],
|
|
58
|
+
conflict: ["reason", "pointer", "route_ids", "total"],
|
|
59
59
|
quota_exceeded: ["reason"],
|
|
60
60
|
invalid_input: ["violations"],
|
|
61
61
|
invalid_query: ["violations"],
|
|
@@ -120,7 +120,9 @@ export function safeDetails(code, details) {
|
|
|
120
120
|
return integer === undefined ? [] : [integer];
|
|
121
121
|
});
|
|
122
122
|
}
|
|
123
|
-
else if (field === "function_ids" ||
|
|
123
|
+
else if (field === "function_ids" ||
|
|
124
|
+
field === "subscription_ids" ||
|
|
125
|
+
field === "route_ids")
|
|
124
126
|
projected[field] = value.filter((item) => boundedString(item));
|
|
125
127
|
}
|
|
126
128
|
}
|
package/dist/rpc.js
CHANGED
|
@@ -154,7 +154,7 @@ export async function runRpc(context, options, loadedCatalog) {
|
|
|
154
154
|
});
|
|
155
155
|
if (options.preview)
|
|
156
156
|
return { kind: "preview", workspaceId: context.workspaceId, requestJson };
|
|
157
|
-
//
|
|
157
|
+
// A call may run for 210 seconds; leave time to receive its result.
|
|
158
158
|
const timeout = AbortSignal.timeout(240_000);
|
|
159
159
|
const signal = context.signal
|
|
160
160
|
? AbortSignal.any([context.signal, timeout])
|
package/dist/runner.js
CHANGED
|
@@ -18,6 +18,7 @@ Usage: zo <command> [--profile <name>] [--workspace <id>] [--json]
|
|
|
18
18
|
rpc <action> Invoke any callable action from JSON
|
|
19
19
|
<resource> <action> Invoke an installed built-in command
|
|
20
20
|
completion <shell> Print bash, zsh or fish completion (offline)
|
|
21
|
+
update Install the latest standalone release
|
|
21
22
|
--version Show the installed CLI version
|
|
22
23
|
|
|
23
24
|
Run zo <command> --help for options; for example, zo machine list --help.
|
|
@@ -39,6 +40,7 @@ const localHelp = {
|
|
|
39
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",
|
|
40
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",
|
|
41
42
|
completion: "Usage: zo completion bash|zsh|fish\nPrint completion from installed metadata without credentials or network requests.\n",
|
|
43
|
+
update: "Usage: zo update [--json]\nDownload the latest standalone release, verify it and switch the zo command to it. Sign-ins are kept.\nDoes nothing when this version is already the latest. An npm installation upgrades with npm install --global @zocomputer/cli@latest.\n",
|
|
42
44
|
};
|
|
43
45
|
// Printed after login so a first-time caller, person or agent, knows the next
|
|
44
46
|
// command and that one sign-in serves the CLI, HTTP and MCP alike.
|
|
@@ -106,7 +108,7 @@ export async function run(argv = process.argv.slice(2), runtime = {}) {
|
|
|
106
108
|
stdout.write((await import("./commands.js")).completionScript(args[1]));
|
|
107
109
|
return;
|
|
108
110
|
}
|
|
109
|
-
const local = ["login", "logout", "auth", "context"].includes(args[0] ?? "");
|
|
111
|
+
const local = ["login", "logout", "auth", "context", "update"].includes(args[0] ?? "");
|
|
110
112
|
if (options.envelope && local)
|
|
111
113
|
throw new CliError("--envelope is only supported for action invocation.");
|
|
112
114
|
const controller = new AbortController();
|
|
@@ -123,6 +125,18 @@ export async function run(argv = process.argv.slice(2), runtime = {}) {
|
|
|
123
125
|
throwIfInterrupted(signal);
|
|
124
126
|
writeJson(value, output);
|
|
125
127
|
};
|
|
128
|
+
if (args[0] === "update") {
|
|
129
|
+
commandOptions(args.slice(1), []);
|
|
130
|
+
const { update } = await import("./update.js");
|
|
131
|
+
writeLocalJson(await update({
|
|
132
|
+
version: runtime.version,
|
|
133
|
+
env,
|
|
134
|
+
fetch: runtime.fetch,
|
|
135
|
+
signal,
|
|
136
|
+
stderr,
|
|
137
|
+
}));
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
126
140
|
if (args[0] === "login") {
|
|
127
141
|
const parsed = commandOptions(args.slice(1), ["environment"], ["device"]);
|
|
128
142
|
const requested = parsed.values.get("environment");
|
package/dist/update.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { FetchLike } from "@zocomputer/system-api";
|
|
2
|
+
export interface UpdateOptions {
|
|
3
|
+
/** The standalone executable's version. npm installations have none. */
|
|
4
|
+
version?: string;
|
|
5
|
+
execPath?: string;
|
|
6
|
+
env?: NodeJS.ProcessEnv;
|
|
7
|
+
fetch?: FetchLike;
|
|
8
|
+
signal?: AbortSignal;
|
|
9
|
+
stderr?: {
|
|
10
|
+
write(text: string): unknown;
|
|
11
|
+
};
|
|
12
|
+
/** Runs the release installer and resolves with its exit status. */
|
|
13
|
+
runInstaller?: (script: Buffer, args: string[]) => Promise<number>;
|
|
14
|
+
}
|
|
15
|
+
export interface UpdateResult {
|
|
16
|
+
/** The version the zo command runs after this update. */
|
|
17
|
+
version: string;
|
|
18
|
+
latest_version: string;
|
|
19
|
+
updated: boolean;
|
|
20
|
+
}
|
|
21
|
+
export declare function update(options?: UpdateOptions): Promise<UpdateResult>;
|
package/dist/update.js
ADDED
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { readFile, readlink, realpath } from "node:fs/promises";
|
|
4
|
+
import { homedir } from "node:os";
|
|
5
|
+
import { delimiter, dirname, isAbsolute, join } from "node:path";
|
|
6
|
+
import { CliError, record, safeText, throwIfInterrupted } from "./errors.js";
|
|
7
|
+
const releases = "https://github.com/zocomputer/zo-cli/releases";
|
|
8
|
+
const installCommand = "curl -fsSL https://zo.computer/install.sh | sh";
|
|
9
|
+
const versionPattern = /^(\d+)\.(\d+)\.(\d+)(-[A-Za-z0-9][A-Za-z0-9.-]*)?$/;
|
|
10
|
+
// The installer links <data home>/zo/cli/releases/<version>/<target>/zo.
|
|
11
|
+
const managedPath = /^(\/.*\/zo\/cli)\/releases\/[^/]+\/[^/]+\/zo$/;
|
|
12
|
+
function unreachable() {
|
|
13
|
+
return new CliError("Could not download the latest Zo CLI release. Check your connection and try again.", 5, "transport_error");
|
|
14
|
+
}
|
|
15
|
+
/** Follows redirects by hand so a release can never be fetched over plain HTTP. */
|
|
16
|
+
async function download(url, fetcher, signal) {
|
|
17
|
+
const timeout = AbortSignal.timeout(60_000);
|
|
18
|
+
const combined = signal ? AbortSignal.any([signal, timeout]) : timeout;
|
|
19
|
+
try {
|
|
20
|
+
let target = url;
|
|
21
|
+
for (let hop = 0; hop < 5; hop++) {
|
|
22
|
+
const response = await fetcher(target, {
|
|
23
|
+
redirect: "manual",
|
|
24
|
+
signal: combined,
|
|
25
|
+
});
|
|
26
|
+
const location = response.headers.get("location");
|
|
27
|
+
if (response.status >= 300 && response.status < 400 && location) {
|
|
28
|
+
await response.body?.cancel();
|
|
29
|
+
target = new URL(location, target).href;
|
|
30
|
+
if (!target.startsWith("https://"))
|
|
31
|
+
break;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (!response.ok || !response.body)
|
|
35
|
+
break;
|
|
36
|
+
// The manifest and installer are a few kilobytes; refuse anything else.
|
|
37
|
+
const reader = response.body.getReader();
|
|
38
|
+
const chunks = [];
|
|
39
|
+
for (let size = 0;;) {
|
|
40
|
+
const { value, done } = await reader.read();
|
|
41
|
+
if (done)
|
|
42
|
+
return Buffer.concat(chunks);
|
|
43
|
+
size += value.length;
|
|
44
|
+
if (size > 1_048_576) {
|
|
45
|
+
await reader.cancel().catch(() => { });
|
|
46
|
+
break;
|
|
47
|
+
}
|
|
48
|
+
chunks.push(value);
|
|
49
|
+
}
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
throwIfInterrupted(signal);
|
|
55
|
+
}
|
|
56
|
+
throw unreachable();
|
|
57
|
+
}
|
|
58
|
+
function isNewer(candidate, current) {
|
|
59
|
+
const next = versionPattern.exec(candidate);
|
|
60
|
+
const now = versionPattern.exec(current);
|
|
61
|
+
if (!next || !now)
|
|
62
|
+
return false;
|
|
63
|
+
for (const index of [1, 2, 3]) {
|
|
64
|
+
const difference = Number(next[index]) - Number(now[index]);
|
|
65
|
+
if (difference !== 0)
|
|
66
|
+
return difference > 0;
|
|
67
|
+
}
|
|
68
|
+
// 0.4.0 follows 0.4.0-rc.1; the latest release is never a prerelease.
|
|
69
|
+
return now[4] !== undefined && next[4] === undefined;
|
|
70
|
+
}
|
|
71
|
+
/** Finds the installer's link on PATH, else in the installer's default directory. */
|
|
72
|
+
async function managedCommand(executable, env) {
|
|
73
|
+
const directories = (env.PATH ?? "").split(delimiter);
|
|
74
|
+
directories.push(join(env.HOME ?? homedir(), ".local/bin"));
|
|
75
|
+
let shadowedBy;
|
|
76
|
+
for (const directory of directories) {
|
|
77
|
+
if (!isAbsolute(directory))
|
|
78
|
+
continue;
|
|
79
|
+
const command = join(directory, "zo");
|
|
80
|
+
const resolved = await realpath(command).catch(() => undefined);
|
|
81
|
+
if (resolved === undefined)
|
|
82
|
+
continue;
|
|
83
|
+
if (resolved !== executable) {
|
|
84
|
+
shadowedBy ??= command;
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
// The installer replaces only a link it wrote, so an alias that points at
|
|
88
|
+
// that link is passed over for the link itself.
|
|
89
|
+
const root = managedPath.exec(await readlink(command).catch(() => ""))?.[1];
|
|
90
|
+
if (root)
|
|
91
|
+
return { directory, root, shadowedBy };
|
|
92
|
+
}
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
function shell(env, signal, stderr) {
|
|
96
|
+
return (script, args) => new Promise((resolve, reject) => {
|
|
97
|
+
// The installer reads itself from stdin, as it does under `curl | sh`.
|
|
98
|
+
// It gets its own process group so an interrupt also stops its download;
|
|
99
|
+
// a shell alone would wait for curl before running its trap.
|
|
100
|
+
const child = spawn("/bin/sh", ["-s", "--", ...args], {
|
|
101
|
+
env,
|
|
102
|
+
detached: true,
|
|
103
|
+
stdio: ["pipe", "ignore", "pipe"],
|
|
104
|
+
});
|
|
105
|
+
const stop = () => {
|
|
106
|
+
try {
|
|
107
|
+
if (child.pid !== undefined)
|
|
108
|
+
process.kill(-child.pid, "SIGTERM");
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
/* The installer already exited. */
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
signal?.addEventListener("abort", stop, { once: true });
|
|
115
|
+
if (signal?.aborted)
|
|
116
|
+
stop();
|
|
117
|
+
child.on("error", reject);
|
|
118
|
+
child.on("close", (code) => {
|
|
119
|
+
signal?.removeEventListener("abort", stop);
|
|
120
|
+
resolve(code ?? 1);
|
|
121
|
+
});
|
|
122
|
+
child.stderr.on("data", (chunk) => stderr.write(String(chunk)));
|
|
123
|
+
// A shell that exits early closes the pipe; its status reports why.
|
|
124
|
+
child.stdin.on("error", () => { });
|
|
125
|
+
child.stdin.end(script);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
export async function update(options = {}) {
|
|
129
|
+
const env = options.env ?? process.env;
|
|
130
|
+
const stderr = options.stderr ?? process.stderr;
|
|
131
|
+
const current = options.version;
|
|
132
|
+
if (current === undefined)
|
|
133
|
+
throw new CliError("zo update replaces the standalone executable. This zo was installed with npm; run npm install --global @zocomputer/cli@latest instead.", 2, "unsupported_installation");
|
|
134
|
+
const unmanaged = (reason) => new CliError(`${reason} Install the latest release with: ${installCommand}`, 2, "unsupported_installation");
|
|
135
|
+
const executable = await realpath(options.execPath ?? process.execPath);
|
|
136
|
+
const command = await managedCommand(executable, env);
|
|
137
|
+
if (managedPath.test(executable) && !command)
|
|
138
|
+
throw unmanaged("No zo command on your PATH links to this executable.");
|
|
139
|
+
const marker = command
|
|
140
|
+
? await readFile(join(command.root, ".installer-v1"), "utf8").catch(() => "")
|
|
141
|
+
: "";
|
|
142
|
+
if (!command || marker.trim() !== "zo-cli-installer-v1")
|
|
143
|
+
throw unmanaged("This zo was not installed by the standalone installer, so it cannot update itself.");
|
|
144
|
+
const fetcher = options.fetch ?? fetch;
|
|
145
|
+
let manifest;
|
|
146
|
+
try {
|
|
147
|
+
manifest = JSON.parse((await download(`${releases}/latest/download/release.json`, fetcher, options.signal)).toString("utf8"));
|
|
148
|
+
}
|
|
149
|
+
catch (error) {
|
|
150
|
+
if (error instanceof CliError)
|
|
151
|
+
throw error;
|
|
152
|
+
}
|
|
153
|
+
const invalid = new CliError("The latest Zo CLI release could not be verified. Nothing was changed.", 5, "invalid_response");
|
|
154
|
+
if (!record(manifest) ||
|
|
155
|
+
manifest.schema_version !== 1 ||
|
|
156
|
+
typeof manifest.cli_version !== "string" ||
|
|
157
|
+
!versionPattern.test(manifest.cli_version) ||
|
|
158
|
+
typeof manifest.installer_sha256 !== "string")
|
|
159
|
+
throw invalid;
|
|
160
|
+
const latest = manifest.cli_version;
|
|
161
|
+
if (!isNewer(latest, current)) {
|
|
162
|
+
stderr.write(latest === current
|
|
163
|
+
? `zo ${current} is the latest version.\n`
|
|
164
|
+
: `zo ${current} is newer than the latest release, ${latest}. Nothing to update; to install ${latest}, run: ${installCommand}\n`);
|
|
165
|
+
return { version: current, latest_version: latest, updated: false };
|
|
166
|
+
}
|
|
167
|
+
// Each release publishes the installer pinned to its own version.
|
|
168
|
+
const installer = await download(`${releases}/download/v${latest}/install.sh`, fetcher, options.signal);
|
|
169
|
+
if (createHash("sha256").update(installer).digest("hex") !==
|
|
170
|
+
manifest.installer_sha256)
|
|
171
|
+
throw invalid;
|
|
172
|
+
stderr.write(`Updating zo ${current} to ${latest}…\n`);
|
|
173
|
+
// The installer derives its root from XDG_DATA_HOME, which may have changed
|
|
174
|
+
// since installation; the link records the root it must keep using.
|
|
175
|
+
const run = options.runInstaller ??
|
|
176
|
+
shell({ ...env, XDG_DATA_HOME: dirname(dirname(command.root)) }, options.signal, stderr);
|
|
177
|
+
let status;
|
|
178
|
+
try {
|
|
179
|
+
status = await run(installer, [
|
|
180
|
+
"--bin-dir",
|
|
181
|
+
command.directory,
|
|
182
|
+
"--no-modify-path",
|
|
183
|
+
]);
|
|
184
|
+
}
|
|
185
|
+
catch {
|
|
186
|
+
status = 1;
|
|
187
|
+
}
|
|
188
|
+
throwIfInterrupted(options.signal);
|
|
189
|
+
if (status !== 0)
|
|
190
|
+
throw new CliError(`The update did not finish. zo ${current} is still installed.`, 5, "update_failed");
|
|
191
|
+
stderr.write(`Updated zo to ${latest}. Sign-ins are unchanged.\n`);
|
|
192
|
+
if (command.shadowedBy)
|
|
193
|
+
stderr.write(`Another zo at ${safeText(command.shadowedBy)} comes first on your PATH and was not updated.\n`);
|
|
194
|
+
return { version: latest, latest_version: latest, updated: true };
|
|
195
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zocomputer/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Public Zo command line client",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"node": "^22.23.4 || ^24.22.1 || ^26.13.0"
|
|
15
15
|
},
|
|
16
16
|
"dependencies": {
|
|
17
|
-
"@zocomputer/system-api": "0.
|
|
17
|
+
"@zocomputer/system-api": "0.3.0"
|
|
18
18
|
},
|
|
19
19
|
"publishConfig": {
|
|
20
20
|
"access": "public",
|