@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/README.md CHANGED
@@ -1,3 +1,171 @@
1
- # Temporary Holding Version
1
+ # Zo CLI
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The public Zo CLI signs in through your browser and selects a workspace for
4
+ System API commands. It supports the maintained Node 22, 24 and 26 LTS lines on macOS and Linux;
5
+ see [runtime requirements](../README.md#set-up).
6
+
7
+ ## Install or upgrade
8
+
9
+ After publication, install the versioned package:
10
+
11
+ ```sh
12
+ npm install --global @zocomputer/cli@0.1.1
13
+ zo --version
14
+ zo login
15
+ ```
16
+
17
+ Use the same install command with the desired version to upgrade. The CLI
18
+ installs its matching public SDK dependency. It performs no update check when
19
+ you run a command.
20
+
21
+ To test unpublished artifacts, install both tarballs together:
22
+
23
+ ```sh
24
+ npm install --global ./zocomputer-system-api-0.1.1.tgz ./zocomputer-cli-0.1.1.tgz
25
+ zo --version
26
+ ```
27
+
28
+ ## Develop locally
29
+
30
+ Install the independent client workspace, then build the CLI:
31
+
32
+ ```sh
33
+ cd clients
34
+ nvm use
35
+ npm ci
36
+ npm run build
37
+ ```
38
+
39
+ The CLI build compiles the SDK first. npm links the two packages inside this
40
+ workspace; application pnpm settings and dependencies are not used.
41
+
42
+ From this package directory:
43
+
44
+ ```sh
45
+ node dist/main.js --help
46
+ node dist/main.js login
47
+ node dist/main.js context list
48
+ node dist/main.js context use wsp_example
49
+ node dist/main.js auth status
50
+ ```
51
+
52
+ Use `node dist/main.js` while working in this repository. The repository's
53
+ internal operator CLI is `./zocli/zo` from the repository root. Run `type -a zo`
54
+ to diagnose a PATH collision; the two clients store separate sign-ins.
55
+
56
+ ## Accounts and workspaces
57
+
58
+ Use named profiles to keep separate sign-ins. New profiles use production:
59
+
60
+ ```sh
61
+ node dist/main.js login --profile work
62
+ node dist/main.js auth status --profile work
63
+ ```
64
+
65
+ A missing `environment` in a saved profile also means production. Internal
66
+ testing can explicitly create a staging profile with
67
+ `login --profile staging --environment staging`. An existing profile keeps its
68
+ environment when you sign in again; changing it requires a different profile.
69
+
70
+ Workspace selection uses `--workspace`, then
71
+ `ZO_WORKSPACE_ID`, then the profile's saved default. An unavailable selected
72
+ workspace produces an error; the CLI never switches identities or workspaces
73
+ to make a command succeed.
74
+
75
+ Login selects a sole authorized workspace automatically. With several
76
+ workspaces, a terminal prompts for a selection; scripts must supply
77
+ `--workspace` or subsequently run `context use`. Status shows the approving
78
+ person separately from the effective subject in the selected workspace.
79
+ `context list`, `context use` and status refresh that view from the server.
80
+
81
+ `login` prints the authorization URL on its own line on stderr, opens your
82
+ browser, and waits for sign-in to finish. You can copy the URL into another
83
+ browser window. The message is dim on interactive terminals and plain when
84
+ redirected, with `--json`, or when `NO_COLOR` is set.
85
+
86
+ `login --no-browser` prints the same URL and waits without opening a browser.
87
+ It is not a device-code login. The browser must reach the CLI's localhost callback.
88
+
89
+ ## Tokens and logout
90
+
91
+ `auth token` explicitly prints a short-lived access token. Add `--headers` to
92
+ print an Authorization JSON object for an MCP client. Status never prints a
93
+ token. `ZO_SYSTEM_API_ACCESS_TOKEN` supplies an explicit automation token for
94
+ the selected profile's fixed origins; the CLI never saves or refreshes it,
95
+ including when the server refuses it.
96
+
97
+ `logout` revokes the saved sign-in and pending replaced sign-ins. If revocation
98
+ fails, local credentials remain so you can retry. `logout --local-only`
99
+ removes local credentials without revoking server access. Unset a bearer
100
+ override before logging in or out of a saved profile.
101
+
102
+ If a credential file is malformed, revoke its login in **Authorized apps**,
103
+ then run `logout --local-only` for that profile before signing in again.
104
+ Ordinary login and logout preserve the unreadable file so they do not silently
105
+ discard pending revocations.
106
+
107
+ ## Local storage
108
+
109
+ Configuration lives in `${XDG_CONFIG_HOME:-$HOME/.config}/zo/cli/`:
110
+
111
+ - `config.json` stores versioned profiles and workspace selections.
112
+ - `credentials/<profile>.json` stores the profile binding, access and refresh
113
+ tokens, refresh state, and pending revocations.
114
+ - `credentials/<profile>.lock/` contains temporary process claims.
115
+
116
+ Directories use mode 0700; files use 0600. The CLI checks ownership and refuses
117
+ symlinks and writable parent directories. Credential writes sync a private
118
+ file, replace the previous file atomically, then sync the directory. These
119
+ permissions protect against other OS users, not programs running as you.
120
+
121
+ Every credential change uses a cross-process bakery lock. Each process
122
+ publishes a unique choosing claim, then an ordered ticket. It waits for lower
123
+ tickets before entering the credential operation. Waits stop after 30 seconds;
124
+ a live owner is never evicted based on elapsed time. Claims from dead PIDs are
125
+ removed. A reused PID can delay recovery until the unrelated process exits;
126
+ it cannot admit two credential writers. This uses native Node APIs on both
127
+ supported systems without depending on Python or native lock extensions.
128
+
129
+ Refresh starts only with 60 seconds or less remaining on the access token.
130
+ Before sending, the CLI durably marks the outcome uncertain. If syncing that
131
+ marker fails after replacement, it attempts to restore the original ready
132
+ credential and reports the storage error; no exchange was sent. A lost response
133
+ may be retried once under the lock within the server's 30-second recovery
134
+ window. Saving the received successor clears the marker. A crash, failure to
135
+ replace the credential file, or unresolved second request leaves the marker
136
+ in place and requires login;
137
+ later processes cannot unknowingly replay that refresh token.
138
+
139
+ Replacing a login saves the new token and the old token's pending revocation
140
+ in one atomic credential write before contacting the revocation endpoint.
141
+ Failed cleanup remains in that journal and is retried by logout or the next
142
+ replacement login. Replacement login reports success with a warning when old
143
+ sign-in cleanup fails; the newly saved credential remains usable. Revoke the
144
+ previous sign-in in Authorized apps if needed. Logout revokes the current
145
+ sign-in too.
146
+
147
+ ## Checks
148
+
149
+ ```sh
150
+ npm run build
151
+ npm exec -- vitest run test/auth.test.ts test/credentials.test.ts test/profiles.test.ts test/context.test.ts
152
+ npm run lint
153
+ npm run typecheck
154
+ npm run test:packed
155
+ ```
156
+
157
+ The packed check builds both public packages and checks their contents. It
158
+ installs their tarballs outside the monorepo and runs the installed binary's
159
+ help, completion, version, RPC, preview and generated commands. A local fake
160
+ API checks that large integers and exact decimals survive invocation. No live
161
+ account or token is needed.
162
+
163
+ To retain release archives for inspection or installation:
164
+
165
+ ```sh
166
+ npm run pack:release -- --out /tmp/zo-cli-release
167
+ ```
168
+
169
+ The command checks generated-command drift and writes both versioned tarballs.
170
+ It rejects source files, missing compiled entry points, internal dependencies
171
+ and unresolved workspace dependencies. It does not publish either package.
package/dist/argv.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ export interface GlobalOptions {
2
+ profile?: string;
3
+ workspace?: string;
4
+ json: boolean;
5
+ envelope: boolean;
6
+ help: boolean;
7
+ }
8
+ export declare function parseGlobals(argv: readonly string[]): {
9
+ args: string[];
10
+ options: GlobalOptions;
11
+ };
12
+ export declare function commandOptions(args: readonly string[], values: readonly string[], flags?: readonly string[]): {
13
+ values: Map<string, string>;
14
+ flags: Set<string>;
15
+ };
package/dist/argv.js ADDED
@@ -0,0 +1,83 @@
1
+ import { CliError } from "./errors.js";
2
+ const switches = new Set(["json", "envelope", "help", "preview"]);
3
+ export function parseGlobals(argv) {
4
+ const options = { json: false, envelope: false, help: false };
5
+ const seen = new Set();
6
+ const args = [];
7
+ let command = false;
8
+ for (let index = 0; index < argv.length; index++) {
9
+ const arg = argv[index];
10
+ if (arg === undefined)
11
+ continue;
12
+ if (!arg.startsWith("--")) {
13
+ args.push(arg);
14
+ command = true;
15
+ continue;
16
+ }
17
+ const equal = arg.indexOf("=");
18
+ const name = arg.slice(2, equal === -1 ? undefined : equal);
19
+ if (!/^[a-z][a-z0-9-]*$/.test(name))
20
+ throw new CliError("Invalid option syntax.");
21
+ const localSwitch = (name === "no-browser" && args[0] === "login") ||
22
+ (name === "local-only" && args[0] === "logout") ||
23
+ (name === "headers" && args[0] === "auth" && args[1] === "token");
24
+ const flag = switches.has(name) || localSwitch || (name === "version" && !command);
25
+ if (flag && equal !== -1)
26
+ throw new CliError(`--${name} does not take a value.`);
27
+ const value = flag
28
+ ? undefined
29
+ : equal === -1
30
+ ? argv[++index]
31
+ : arg.slice(equal + 1);
32
+ if (!flag &&
33
+ (value === undefined || (equal === -1 && value.startsWith("--"))))
34
+ throw new CliError(`--${name} requires a value. Use --name=value for values beginning with --.`);
35
+ if (["profile", "workspace", "json", "envelope", "help"].includes(name)) {
36
+ if (seen.has(name))
37
+ throw new CliError(`Repeated --${name}.`);
38
+ seen.add(name);
39
+ if (name === "profile" || name === "workspace") {
40
+ if (!value)
41
+ throw new CliError(`--${name} requires a nonempty value.`);
42
+ options[name] = value;
43
+ }
44
+ else if (name === "json" || name === "envelope" || name === "help")
45
+ options[name] = true;
46
+ }
47
+ else {
48
+ args.push(arg);
49
+ if (!flag && equal === -1 && value !== undefined)
50
+ args.push(value);
51
+ }
52
+ }
53
+ return { args, options };
54
+ }
55
+ export function commandOptions(args, values, flags = []) {
56
+ const parsed = {
57
+ values: new Map(),
58
+ flags: new Set(),
59
+ };
60
+ for (let index = 0; index < args.length; index++) {
61
+ const arg = args[index];
62
+ if (!arg?.startsWith("--"))
63
+ throw new CliError("Unexpected argument. Run this command with --help.");
64
+ const equal = arg.indexOf("=");
65
+ const name = arg.slice(2, equal === -1 ? undefined : equal);
66
+ if (parsed.values.has(name) || parsed.flags.has(name))
67
+ throw new CliError(`Repeated --${name}.`);
68
+ if (flags.includes(name)) {
69
+ if (equal !== -1)
70
+ throw new CliError(`--${name} does not take a value.`);
71
+ parsed.flags.add(name);
72
+ }
73
+ else if (values.includes(name)) {
74
+ const value = equal === -1 ? args[++index] : arg.slice(equal + 1);
75
+ if (value === undefined || (equal === -1 && value.startsWith("--")))
76
+ throw new CliError(`--${name} requires a value.`);
77
+ parsed.values.set(name, value);
78
+ }
79
+ else
80
+ throw new CliError(`Unknown option --${name}. Run this command with --help.`);
81
+ }
82
+ return parsed;
83
+ }
package/dist/auth.d.ts ADDED
@@ -0,0 +1,39 @@
1
+ import { type LoginOptions } from "./login.ts";
2
+ import { type Profile } from "./profiles.ts";
3
+ import { type Session } from "./session.ts";
4
+ interface Binding {
5
+ issuer: string;
6
+ api: string;
7
+ audience: string;
8
+ clientId: string;
9
+ }
10
+ export interface AuthOptions {
11
+ fetch?: typeof fetch;
12
+ environment?: Binding;
13
+ env?: NodeJS.ProcessEnv;
14
+ now?: () => number;
15
+ signal?: AbortSignal;
16
+ }
17
+ export declare class AuthClient {
18
+ readonly profile: Profile;
19
+ readonly directory: string;
20
+ readonly binding: Binding;
21
+ readonly path: string;
22
+ private readonly fetcher;
23
+ private readonly env;
24
+ private readonly now;
25
+ private readonly signal?;
26
+ constructor(profile: Profile, directory: string, options?: AuthOptions);
27
+ private locked;
28
+ private read;
29
+ private tokenRequest;
30
+ accessToken(): Promise<string>;
31
+ session(): Promise<Session>;
32
+ private revoke;
33
+ private cleanup;
34
+ login(options?: LoginOptions): Promise<{
35
+ cleanupPending: boolean;
36
+ }>;
37
+ logout(localOnly?: boolean): Promise<void>;
38
+ }
39
+ export {};
package/dist/auth.js ADDED
@@ -0,0 +1,290 @@
1
+ import { join } from "node:path";
2
+ import { CliError, record, throwIfInterrupted } from "./errors.js";
3
+ import { requestJson, RequestFailure } from "./http.js";
4
+ import { loginCode } from "./login.js";
5
+ import { environments, profileName } from "./profiles.js";
6
+ import { parseSession } from "./session.js";
7
+ import { privateDirectory, PrivateWriteDurabilityError, readPrivate, removePrivate, withLock, writePrivate, } from "./storage.js";
8
+ function tokens(raw, now) {
9
+ if (!record(raw) ||
10
+ raw.error !== undefined ||
11
+ typeof raw.access_token !== "string" ||
12
+ !raw.access_token ||
13
+ typeof raw.refresh_token !== "string" ||
14
+ !raw.refresh_token ||
15
+ typeof raw.token_type !== "string" ||
16
+ raw.token_type.toLowerCase() !== "bearer" ||
17
+ typeof raw.expires_in !== "number" ||
18
+ !Number.isSafeInteger(raw.expires_in) ||
19
+ raw.expires_in <= 0 ||
20
+ raw.expires_in > 300)
21
+ throw new RequestFailure(false, "invalid_token_response");
22
+ return {
23
+ accessToken: raw.access_token,
24
+ refreshToken: raw.refresh_token,
25
+ expiresAt: now + raw.expires_in * 1000,
26
+ };
27
+ }
28
+ function invalidCredential() {
29
+ return new CliError("The saved credential cannot be read safely. Revoke Zo CLI in Authorized apps, then run zo logout --local-only for this profile before signing in again. Local-only logout does not revoke server access.", 3, "invalid_credential");
30
+ }
31
+ export class AuthClient {
32
+ profile;
33
+ directory;
34
+ binding;
35
+ path;
36
+ fetcher;
37
+ env;
38
+ now;
39
+ signal;
40
+ constructor(profile, directory, options = {}) {
41
+ this.profile = profile;
42
+ this.directory = directory;
43
+ this.binding = options.environment ?? environments[profile.environment];
44
+ this.path = join(directory, "credentials", `${profileName(profile.name)}.json`);
45
+ this.fetcher = options.fetch ?? fetch;
46
+ this.env = options.env ?? process.env;
47
+ this.now = options.now ?? Date.now;
48
+ this.signal = options.signal;
49
+ }
50
+ async locked(work, signal = this.signal) {
51
+ throwIfInterrupted(signal);
52
+ await privateDirectory(this.directory);
53
+ await privateDirectory(join(this.directory, "credentials"));
54
+ return withLock(join(this.directory, "credentials", `${this.profile.name}.lock`), work, undefined, signal);
55
+ }
56
+ async read() {
57
+ let raw;
58
+ try {
59
+ raw = await readPrivate(this.path);
60
+ }
61
+ catch (error) {
62
+ if (error instanceof CliError && error.code === "invalid_local_data")
63
+ throw invalidCredential();
64
+ throw error;
65
+ }
66
+ if (raw === undefined)
67
+ return undefined;
68
+ if (!record(raw) ||
69
+ raw.version !== 1 ||
70
+ !record(raw.binding) ||
71
+ !Object.entries(this.binding).every(([key, value]) => record(raw.binding) && raw.binding[key] === value) ||
72
+ !Array.isArray(raw.pendingRevocations) ||
73
+ !raw.pendingRevocations.every((entry) => typeof entry === "string" && entry.length > 0) ||
74
+ (raw.refreshState !== "ready" && raw.refreshState !== "uncertain"))
75
+ throw invalidCredential();
76
+ let active;
77
+ if (raw.active !== undefined) {
78
+ if (!record(raw.active) ||
79
+ typeof raw.active.accessToken !== "string" ||
80
+ !raw.active.accessToken ||
81
+ typeof raw.active.refreshToken !== "string" ||
82
+ !raw.active.refreshToken ||
83
+ typeof raw.active.expiresAt !== "number" ||
84
+ !Number.isFinite(raw.active.expiresAt))
85
+ throw invalidCredential();
86
+ active = {
87
+ accessToken: raw.active.accessToken,
88
+ refreshToken: raw.active.refreshToken,
89
+ expiresAt: raw.active.expiresAt,
90
+ };
91
+ }
92
+ return {
93
+ version: 1,
94
+ binding: this.binding,
95
+ ...(active ? { active } : {}),
96
+ pendingRevocations: raw.pendingRevocations,
97
+ refreshState: raw.refreshState,
98
+ };
99
+ }
100
+ async tokenRequest(form, signal = this.signal) {
101
+ const issuedAt = this.now();
102
+ return tokens(await requestJson(`${this.binding.issuer}/zo-system-api/oauth/token`, {
103
+ method: "POST",
104
+ signal,
105
+ body: new URLSearchParams({
106
+ ...form,
107
+ client_id: this.binding.clientId,
108
+ resource: this.binding.audience,
109
+ }),
110
+ }, this.fetcher), issuedAt);
111
+ }
112
+ async accessToken() {
113
+ throwIfInterrupted(this.signal);
114
+ const override = this.env.ZO_SYSTEM_API_ACCESS_TOKEN;
115
+ if (override !== undefined) {
116
+ if (!override || /\s/.test(override))
117
+ throw new CliError("ZO_SYSTEM_API_ACCESS_TOKEN is empty or invalid.", 3, "invalid_token");
118
+ return override;
119
+ }
120
+ return this.locked(async () => {
121
+ const credential = await this.read();
122
+ throwIfInterrupted(this.signal);
123
+ if (!credential?.active)
124
+ throw new CliError("Sign in with zo login.", 3, "login_required");
125
+ if (credential.refreshState !== "ready")
126
+ throw new CliError("The previous refresh outcome is unknown. Run zo login; automatic refresh is stopped.", 3, "refresh_uncertain");
127
+ if (credential.active.expiresAt - this.now() > 60_000)
128
+ return credential.active.accessToken;
129
+ credential.refreshState = "uncertain";
130
+ try {
131
+ await writePrivate(this.path, credential);
132
+ }
133
+ catch (error) {
134
+ if (error instanceof PrivateWriteDurabilityError) {
135
+ // No exchange was sent, so the visible journal can safely be undone.
136
+ credential.refreshState = "ready";
137
+ await writePrivate(this.path, credential);
138
+ }
139
+ throw error;
140
+ }
141
+ const started = performance.now();
142
+ const startedAt = this.now();
143
+ const form = {
144
+ grant_type: "refresh_token",
145
+ refresh_token: credential.active.refreshToken,
146
+ };
147
+ let successor;
148
+ try {
149
+ successor = await this.tokenRequest(form);
150
+ }
151
+ catch (error) {
152
+ throwIfInterrupted(this.signal);
153
+ // Two eight-second requests fit the issuer's thirty-second recovery
154
+ // window. Wall time also catches suspension of the monotonic clock.
155
+ const elapsed = this.now() - startedAt;
156
+ if (!(error instanceof RequestFailure) ||
157
+ !error.ambiguous ||
158
+ performance.now() - started >= 20_000 ||
159
+ elapsed < 0 ||
160
+ elapsed >= 20_000)
161
+ throw error;
162
+ successor = await this.tokenRequest(form);
163
+ }
164
+ credential.active = successor;
165
+ credential.refreshState = "ready";
166
+ await writePrivate(this.path, credential);
167
+ throwIfInterrupted(this.signal);
168
+ return successor.accessToken;
169
+ });
170
+ }
171
+ async session() {
172
+ const token = await this.accessToken();
173
+ return parseSession(await requestJson(`${this.binding.api}/zo-system-api/v1/session`, { headers: { Authorization: `Bearer ${token}` }, signal: this.signal }, this.fetcher, { systemApi: true, timeoutMs: 30_000 }));
174
+ }
175
+ async revoke(token, signal = this.signal) {
176
+ throwIfInterrupted(signal);
177
+ const timeout = AbortSignal.timeout(8_000);
178
+ let response;
179
+ try {
180
+ response = await this.fetcher(`${this.binding.issuer}/zo-system-api/oauth/revoke`, {
181
+ method: "POST",
182
+ body: new URLSearchParams({
183
+ client_id: this.binding.clientId,
184
+ token,
185
+ token_type_hint: "refresh_token",
186
+ }),
187
+ redirect: "error",
188
+ signal: signal ? AbortSignal.any([signal, timeout]) : timeout,
189
+ });
190
+ }
191
+ catch {
192
+ throwIfInterrupted(signal);
193
+ throw new CliError("Revocation failed. Local credentials were retained; retry zo logout.", 5, "revocation_failed");
194
+ }
195
+ await response.body?.cancel();
196
+ throwIfInterrupted(signal);
197
+ if (!response.ok)
198
+ throw new CliError("Revocation failed. Local credentials were retained; retry zo logout.", 5, "revocation_failed");
199
+ }
200
+ async cleanup(credential, signal = this.signal) {
201
+ while (credential.pendingRevocations.length) {
202
+ const token = credential.pendingRevocations[0];
203
+ if (token === undefined)
204
+ break;
205
+ await this.revoke(token, signal);
206
+ credential.pendingRevocations.shift();
207
+ await writePrivate(this.path, credential);
208
+ throwIfInterrupted(signal);
209
+ }
210
+ }
211
+ async login(options = {}) {
212
+ const signal = this.signal && options.signal
213
+ ? AbortSignal.any([this.signal, options.signal])
214
+ : (this.signal ?? options.signal);
215
+ throwIfInterrupted(signal);
216
+ if (this.env.ZO_SYSTEM_API_ACCESS_TOKEN !== undefined)
217
+ throw new CliError("Unset ZO_SYSTEM_API_ACCESS_TOKEN before browser login.");
218
+ return this.locked(async () => {
219
+ const previous = await this.read();
220
+ throwIfInterrupted(signal);
221
+ const code = await loginCode(this.binding.issuer, this.binding.audience, this.binding.clientId, { ...options, signal });
222
+ const active = await this.tokenRequest({
223
+ grant_type: "authorization_code",
224
+ code: code.code,
225
+ code_verifier: code.verifier,
226
+ redirect_uri: code.redirectUri,
227
+ }, signal);
228
+ const credential = {
229
+ version: 1,
230
+ binding: this.binding,
231
+ active,
232
+ refreshState: "ready",
233
+ pendingRevocations: [
234
+ ...(previous?.pendingRevocations ?? []),
235
+ ...(previous?.active ? [previous.active.refreshToken] : []),
236
+ ],
237
+ };
238
+ try {
239
+ await writePrivate(this.path, credential);
240
+ }
241
+ catch (error) {
242
+ if (error instanceof PrivateWriteDurabilityError)
243
+ throw error;
244
+ try {
245
+ // Saving failed; cleanup must still run after an interruption.
246
+ await this.revoke(active.refreshToken, null);
247
+ }
248
+ catch {
249
+ throw new CliError("Could not save or revoke the new sign-in. Revoke Zo CLI in Authorized apps before retrying login.", 5, "unsaved_sign_in");
250
+ }
251
+ throw error;
252
+ }
253
+ throwIfInterrupted(signal);
254
+ try {
255
+ await this.cleanup(credential, signal);
256
+ }
257
+ catch {
258
+ throwIfInterrupted(signal);
259
+ // The new sign-in is durable. Cleanup failure must not send the user
260
+ // to logout, which would revoke that successfully saved replacement.
261
+ return { cleanupPending: true };
262
+ }
263
+ return { cleanupPending: false };
264
+ }, signal);
265
+ }
266
+ async logout(localOnly = false) {
267
+ throwIfInterrupted(this.signal);
268
+ if (this.env.ZO_SYSTEM_API_ACCESS_TOKEN !== undefined)
269
+ throw new CliError("Unset ZO_SYSTEM_API_ACCESS_TOKEN before logging out a saved profile.");
270
+ await this.locked(async () => {
271
+ if (localOnly) {
272
+ await removePrivate(this.path);
273
+ throwIfInterrupted(this.signal);
274
+ return;
275
+ }
276
+ const credential = await this.read();
277
+ throwIfInterrupted(this.signal);
278
+ if (!credential)
279
+ return;
280
+ if (credential.active) {
281
+ await this.revoke(credential.active.refreshToken);
282
+ delete credential.active;
283
+ await writePrivate(this.path, credential);
284
+ }
285
+ await this.cleanup(credential);
286
+ await removePrivate(this.path);
287
+ throwIfInterrupted(this.signal);
288
+ });
289
+ }
290
+ }
@@ -0,0 +1,23 @@
1
+ import { type FetchLike, type RuntimeCatalog } from "@zocomputer/system-api";
2
+ import { type NormalizedCatalog, type OperationVersion } from "@zocomputer/system-api/catalog";
3
+ import { type JsonObject, type JsonValue } from "@zocomputer/system-api/json";
4
+ export interface InvocationContext {
5
+ workspaceId: string;
6
+ apiOrigin: string;
7
+ accessToken: () => Promise<string>;
8
+ fetch?: FetchLike;
9
+ signal?: AbortSignal;
10
+ }
11
+ export interface LoadedCatalog {
12
+ authenticatedContext: InvocationContext;
13
+ catalog: NormalizedCatalog;
14
+ runtimeCatalog: RuntimeCatalog;
15
+ }
16
+ export declare function apiBaseUrl(context: InvocationContext): string;
17
+ export declare function loadCatalog(context: InvocationContext, options?: {
18
+ catalog?: "scoped" | "all";
19
+ }): Promise<LoadedCatalog>;
20
+ export declare function parseVersion(value: string): number;
21
+ export declare function resolveAction(catalog: NormalizedCatalog, action: string, explicitVersion?: number): OperationVersion;
22
+ export declare function listActions(catalog: NormalizedCatalog): JsonValue;
23
+ export declare function describeAction(catalog: NormalizedCatalog, action: string, version?: number): JsonObject;