@ornncompute/cli 0.2.24 → 1.0.34

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.
Files changed (90) hide show
  1. package/README.md +351 -426
  2. package/package.json +12 -6
  3. package/src/admin-capacity-reservations.mjs +305 -0
  4. package/src/admin-command-handlers.mjs +57 -0
  5. package/src/admin-enrollment.mjs +681 -0
  6. package/src/admin-mutations.mjs +187 -0
  7. package/src/admin-node-diagnostics.mjs +412 -0
  8. package/src/admin-node-lifecycle.mjs +905 -0
  9. package/src/admin-shell.mjs +456 -0
  10. package/src/api-client.mjs +81 -28
  11. package/src/atomic-output.mjs +112 -0
  12. package/src/auth-store.mjs +182 -11
  13. package/src/capability-catalog.mjs +7 -0
  14. package/src/catalog-shell.mjs +1363 -0
  15. package/src/cli.mjs +123 -10266
  16. package/src/connect-mode-handlers.mjs +170 -0
  17. package/src/device-auth.mjs +179 -62
  18. package/src/evidence-commands.mjs +1354 -0
  19. package/src/external-bucket-commands.mjs +1374 -0
  20. package/src/gpu-catalog.mjs +21 -0
  21. package/src/identity-file.mjs +28 -0
  22. package/src/interaction-runtime.mjs +310 -0
  23. package/src/kubernetes-commands.mjs +2235 -0
  24. package/src/launch-mode-handlers.mjs +119 -0
  25. package/src/machine-reference.mjs +285 -0
  26. package/src/mount-commands.mjs +1148 -0
  27. package/src/session-commands.mjs +565 -0
  28. package/src/slurm-commands.mjs +274 -0
  29. package/src/startup.mjs +34 -11
  30. package/src/storage-commands.mjs +1164 -0
  31. package/src/update.mjs +172 -24
  32. package/src/vm-commands.mjs +4308 -0
  33. package/src/vpn-secret-output.mjs +150 -0
  34. package/src/wait-runtime.mjs +231 -0
  35. package/vendor/capabilities/capabilities/admin-capacity-reservations.js +96 -0
  36. package/vendor/capabilities/capabilities/admin-enrollment.js +321 -0
  37. package/vendor/capabilities/capabilities/admin-node-diagnostics.js +88 -0
  38. package/vendor/capabilities/capabilities/admin-nodes-lifecycle.js +235 -0
  39. package/vendor/capabilities/capabilities/admin.js +89 -0
  40. package/vendor/capabilities/capabilities/cli.js +156 -0
  41. package/vendor/capabilities/capabilities/clusters.js +150 -1
  42. package/vendor/capabilities/capabilities/commerce-market.js +330 -0
  43. package/vendor/capabilities/capabilities/compute.js +522 -0
  44. package/vendor/capabilities/capabilities/evidence.js +145 -0
  45. package/vendor/capabilities/capabilities/external-buckets.js +191 -0
  46. package/vendor/capabilities/capabilities/fleet.js +77 -25
  47. package/vendor/capabilities/capabilities/identity.js +114 -3
  48. package/vendor/capabilities/capabilities/listings.js +141 -144
  49. package/vendor/capabilities/capabilities/market-shared.js +44 -0
  50. package/vendor/capabilities/capabilities/markets.js +449 -0
  51. package/vendor/capabilities/capabilities/mounts.js +176 -0
  52. package/vendor/capabilities/capabilities/networks.js +1264 -0
  53. package/vendor/capabilities/capabilities/observability.js +38 -8
  54. package/vendor/capabilities/capabilities/tokens.js +32 -17
  55. package/vendor/capabilities/capabilities/volumes.js +178 -0
  56. package/vendor/capabilities/capability.js +21 -14
  57. package/vendor/capabilities/cli-catalog.js +67 -0
  58. package/vendor/capabilities/cli-migrations.js +261 -0
  59. package/vendor/capabilities/cli.js +7 -0
  60. package/vendor/capabilities/software-catalog.js +36 -0
  61. package/vendor/capabilities/spec-catalog.js +28 -5
  62. package/vendor/capabilities/supplier-directory.js +20 -0
  63. package/src/catalog-dispatch.mjs +0 -605
  64. package/src/color.mjs +0 -71
  65. package/vendor/capabilities/capabilities/clusters.d.ts +0 -2
  66. package/vendor/capabilities/capabilities/fleet.d.ts +0 -8
  67. package/vendor/capabilities/capabilities/identity.d.ts +0 -3
  68. package/vendor/capabilities/capabilities/listings.d.ts +0 -51
  69. package/vendor/capabilities/capabilities/observability.d.ts +0 -5
  70. package/vendor/capabilities/capabilities/staff-access.d.ts +0 -29
  71. package/vendor/capabilities/capabilities/staff-access.js +0 -278
  72. package/vendor/capabilities/capabilities/staff-commerce.d.ts +0 -160
  73. package/vendor/capabilities/capabilities/staff-commerce.js +0 -689
  74. package/vendor/capabilities/capabilities/staff-fleet.d.ts +0 -140
  75. package/vendor/capabilities/capabilities/staff-fleet.js +0 -538
  76. package/vendor/capabilities/capabilities/staff-gtm.d.ts +0 -142
  77. package/vendor/capabilities/capabilities/staff-gtm.js +0 -521
  78. package/vendor/capabilities/capabilities/staff-shared.d.ts +0 -47
  79. package/vendor/capabilities/capabilities/staff-shared.js +0 -98
  80. package/vendor/capabilities/capabilities/tokens.d.ts +0 -10
  81. package/vendor/capabilities/capabilities/users.d.ts +0 -2
  82. package/vendor/capabilities/capability.d.ts +0 -93
  83. package/vendor/capabilities/catalog.d.ts +0 -3
  84. package/vendor/capabilities/catalog.js +0 -26
  85. package/vendor/capabilities/index.d.ts +0 -16
  86. package/vendor/capabilities/index.js +0 -16
  87. package/vendor/capabilities/names.d.ts +0 -6
  88. package/vendor/capabilities/role.d.ts +0 -6
  89. package/vendor/capabilities/role.js +0 -17
  90. package/vendor/capabilities/spec-catalog.d.ts +0 -78
@@ -0,0 +1,565 @@
1
+ import { createInterface } from "node:readline/promises";
2
+
3
+ import { CliApiError, cliRequest, resolveApiBaseUrl } from "./api-client.mjs";
4
+ import {
5
+ bindAuthUser,
6
+ clearAuthSession,
7
+ getAuthConfigPath,
8
+ loadAuthSession,
9
+ saveAuthSession,
10
+ saveOrganizationHint,
11
+ } from "./auth-store.mjs";
12
+ import {
13
+ DeviceAuthError,
14
+ openBrowser,
15
+ resolveAuthBaseUrl,
16
+ startDeviceFlow,
17
+ waitForDeviceApproval,
18
+ } from "./device-auth.mjs";
19
+
20
+ const CANONICAL_ID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
21
+
22
+ export class SessionCommandError extends Error {
23
+ constructor(
24
+ message,
25
+ {
26
+ action = null,
27
+ code = "session_command_failed",
28
+ exitCode = 1,
29
+ outcomeClass = "failed",
30
+ retryable = false,
31
+ } = {}
32
+ ) {
33
+ super(message);
34
+ this.name = "SessionCommandError";
35
+ this.action = action;
36
+ this.code = code;
37
+ this.exitCode = exitCode;
38
+ this.outcomeClass = outcomeClass;
39
+ this.retryable = retryable;
40
+ }
41
+ }
42
+
43
+ function usage(message) {
44
+ throw new SessionCommandError(message, { code: "usage_error", exitCode: 2 });
45
+ }
46
+
47
+ function parseOptions(argv, { booleans = [], values = [], usageText }) {
48
+ const booleanNames = new Set(booleans);
49
+ const valueNames = new Set(values);
50
+ const parsed = {};
51
+ for (let index = 0; index < argv.length; index += 1) {
52
+ const argument = argv[index];
53
+ if (!argument.startsWith("--")) usage(usageText);
54
+ const equals = argument.indexOf("=");
55
+ const name = equals === -1 ? argument : argument.slice(0, equals);
56
+ const inlineValue = equals === -1 ? null : argument.slice(equals + 1);
57
+ if (booleanNames.has(name)) {
58
+ if (inlineValue !== null) usage(`${name} does not accept a value.\n${usageText}`);
59
+ parsed[name.slice(2)] = true;
60
+ continue;
61
+ }
62
+ if (!valueNames.has(name)) usage(`Unsupported option: ${name}.\n${usageText}`);
63
+ const value = inlineValue ?? argv[index + 1];
64
+ if (!value || (inlineValue === null && value.startsWith("--"))) {
65
+ usage(`Missing value for ${name}.\n${usageText}`);
66
+ }
67
+ if (inlineValue === null) index += 1;
68
+ if (parsed[name.slice(2)] !== undefined)
69
+ usage(`${name} may be supplied only once.\n${usageText}`);
70
+ parsed[name.slice(2)] = value;
71
+ }
72
+ return parsed;
73
+ }
74
+
75
+ function positiveNumberOption(value, name, usageText) {
76
+ if (value === undefined) return undefined;
77
+ const parsed = Number(value);
78
+ if (!Number.isFinite(parsed) || parsed <= 0) {
79
+ usage(`${name} must be a positive number.\n${usageText}`);
80
+ }
81
+ return parsed;
82
+ }
83
+
84
+ export function parseSessionCommandInput(capabilityId, argv) {
85
+ if (capabilityId === "identity.login") {
86
+ const usageText =
87
+ "Usage: ornn login [--organization <exact-name>] [--no-browser] [--timeout <seconds>]";
88
+ const parsed = parseOptions(argv, {
89
+ booleans: ["--no-browser"],
90
+ values: ["--auth-base", "--organization", "--poll-interval", "--timeout"],
91
+ usageText,
92
+ });
93
+ return {
94
+ authBase: parsed["auth-base"],
95
+ noBrowser: parsed["no-browser"],
96
+ organization: parsed.organization,
97
+ pollInterval: positiveNumberOption(parsed["poll-interval"], "--poll-interval", usageText),
98
+ timeout: positiveNumberOption(parsed.timeout, "--timeout", usageText),
99
+ };
100
+ }
101
+ if (capabilityId === "identity.context") {
102
+ return parseOptions(argv, {
103
+ values: ["--organization"],
104
+ usageText: "Usage: ornn context [--organization <exact-name-or-id>]",
105
+ });
106
+ }
107
+ if (capabilityId === "identity.logout") {
108
+ return parseOptions(argv, { usageText: "Usage: ornn logout" });
109
+ }
110
+ return {};
111
+ }
112
+
113
+ function organization(value) {
114
+ if (!value || typeof value !== "object") return null;
115
+ const id = typeof value.id === "string" ? value.id : "";
116
+ const name = typeof value.name === "string" ? value.name : "";
117
+ if (!id || !name) return null;
118
+ return { id, name, approved: value.approved === true };
119
+ }
120
+
121
+ function sessionContext(value) {
122
+ if (!value || typeof value !== "object" || !value.user || typeof value.user !== "object") {
123
+ throw new SessionCommandError("Ornn returned an invalid session context.", {
124
+ code: "invalid_session_context",
125
+ exitCode: 6,
126
+ retryable: true,
127
+ });
128
+ }
129
+ const organizations = Array.isArray(value.organizations)
130
+ ? value.organizations.map(organization).filter(Boolean)
131
+ : [];
132
+ const selectedOrganization = organization(
133
+ value.selectedOrganization ?? value.selected_organization
134
+ );
135
+ return {
136
+ user: {
137
+ id: typeof value.user.id === "string" ? value.user.id : null,
138
+ email: typeof value.user.email === "string" ? value.user.email : null,
139
+ },
140
+ selected_organization: selectedOrganization,
141
+ organizations,
142
+ };
143
+ }
144
+
145
+ async function readServerContext({ env, fetchImpl, session: suppliedSession }) {
146
+ const session = suppliedSession === undefined ? await loadAuthSession({ env }) : suppliedSession;
147
+ try {
148
+ return sessionContext(
149
+ await cliRequest({
150
+ endpoint: "/v1/cli/session",
151
+ env,
152
+ fetchImpl,
153
+ method: "GET",
154
+ ...(session ? { session } : {}),
155
+ })
156
+ );
157
+ } catch (error) {
158
+ if (
159
+ !(error instanceof CliApiError) ||
160
+ error.status !== 403 ||
161
+ !session?.accessToken ||
162
+ !session.selectedOrganizationId
163
+ ) {
164
+ throw error;
165
+ }
166
+ const unscopedSession = { ...session };
167
+ delete unscopedSession.selectedOrganizationId;
168
+ return sessionContext(
169
+ await cliRequest({
170
+ endpoint: "/v1/cli/session",
171
+ env,
172
+ fetchImpl,
173
+ method: "GET",
174
+ session: unscopedSession,
175
+ })
176
+ );
177
+ }
178
+ }
179
+
180
+ function selectionAction(machine) {
181
+ return {
182
+ type: "command",
183
+ command: machine
184
+ ? "ornn context --organization <canonical-id> --format json"
185
+ : "ornn context --organization <exact-name>",
186
+ description: machine
187
+ ? "Select one approved organization by canonical identifier."
188
+ : "Select one approved organization by its exact displayed name.",
189
+ };
190
+ }
191
+
192
+ function findApprovedOrganization(organizations, selector, machine) {
193
+ if (machine && !CANONICAL_ID.test(selector)) {
194
+ throw new SessionCommandError(
195
+ "Automation must select an organization by canonical identifier.",
196
+ {
197
+ action: selectionAction(true),
198
+ code: "organization_id_required",
199
+ exitCode: 2,
200
+ }
201
+ );
202
+ }
203
+ const matches = organizations.filter((candidate) =>
204
+ machine ? candidate.id === selector : candidate.id === selector || candidate.name === selector
205
+ );
206
+ if (matches.length === 0) {
207
+ throw new SessionCommandError("The requested organization is not a current membership.", {
208
+ action: selectionAction(machine),
209
+ code: "organization_not_available",
210
+ exitCode: 5,
211
+ });
212
+ }
213
+ if (matches.length > 1) {
214
+ throw new SessionCommandError(
215
+ "More than one organization has that name; select by canonical identifier.",
216
+ {
217
+ action: selectionAction(true),
218
+ code: "organization_ambiguous",
219
+ exitCode: 2,
220
+ }
221
+ );
222
+ }
223
+ if (!matches[0].approved) {
224
+ throw new SessionCommandError(`Organization ${matches[0].name} is not approved.`, {
225
+ code: "organization_not_approved",
226
+ exitCode: 5,
227
+ });
228
+ }
229
+ return matches[0];
230
+ }
231
+
232
+ async function promptForOrganization(organizations, { stdin, stdout }) {
233
+ stdout.write("Choose an approved organization by typing its exact name:\n");
234
+ for (const candidate of organizations) stdout.write(` ${candidate.name}\n`);
235
+ const readline = createInterface({ input: stdin, output: stdout, terminal: false });
236
+ try {
237
+ return await readline.question("Organization: ");
238
+ } finally {
239
+ readline.close();
240
+ }
241
+ }
242
+
243
+ function updateSnapshotOrganization(sessionSnapshot, selectedOrganizationId) {
244
+ if (!sessionSnapshot?.current) return;
245
+ const next = { ...sessionSnapshot.current };
246
+ if (selectedOrganizationId) next.selectedOrganizationId = selectedOrganizationId;
247
+ else delete next.selectedOrganizationId;
248
+ sessionSnapshot.current = next;
249
+ }
250
+
251
+ async function selectAndRevalidate(
252
+ selector,
253
+ context,
254
+ { env, fetchImpl, machine, sessionSnapshot }
255
+ ) {
256
+ const selected = findApprovedOrganization(context.organizations, selector, machine);
257
+ const expectedAccessToken = sessionSnapshot?.current?.accessToken;
258
+ await saveOrganizationHint(selected.id, { env, expectedAccessToken });
259
+ updateSnapshotOrganization(sessionSnapshot, selected.id);
260
+ const revalidated = await readServerContext({
261
+ env,
262
+ fetchImpl,
263
+ session: sessionSnapshot?.current,
264
+ });
265
+ if (revalidated.selected_organization?.id !== selected.id) {
266
+ await saveOrganizationHint(null, { env, expectedAccessToken });
267
+ updateSnapshotOrganization(sessionSnapshot, null);
268
+ throw new SessionCommandError(
269
+ "Organization membership changed before selection could be verified.",
270
+ {
271
+ action: selectionAction(machine),
272
+ code: "organization_membership_changed",
273
+ exitCode: 5,
274
+ }
275
+ );
276
+ }
277
+ return revalidated;
278
+ }
279
+
280
+ async function resolveContext(
281
+ input,
282
+ {
283
+ eligibleHuman,
284
+ env,
285
+ fetchImpl,
286
+ machine,
287
+ promptIfMultiple = false,
288
+ sessionSnapshot,
289
+ stdin,
290
+ stdout,
291
+ }
292
+ ) {
293
+ const localSession =
294
+ sessionSnapshot?.current === undefined
295
+ ? await loadAuthSession({ env })
296
+ : sessionSnapshot.current;
297
+ if (sessionSnapshot && sessionSnapshot.current === undefined) {
298
+ sessionSnapshot.current = localSession;
299
+ }
300
+ let context = await readServerContext({ env, fetchImpl, session: localSession });
301
+ await bindAuthUser(context.user, {
302
+ env,
303
+ expectedAccessToken: localSession?.accessToken,
304
+ });
305
+ if (sessionSnapshot?.current && context.user?.id) {
306
+ sessionSnapshot.current = {
307
+ ...sessionSnapshot.current,
308
+ user: { ...(sessionSnapshot.current.user ?? {}), ...context.user },
309
+ };
310
+ }
311
+ const localHint = localSession?.selectedOrganizationId;
312
+ const hintIsCurrent =
313
+ context.selected_organization && context.selected_organization.id === localHint;
314
+ if (localHint && !hintIsCurrent) {
315
+ await saveOrganizationHint(null, {
316
+ env,
317
+ expectedAccessToken: localSession?.accessToken,
318
+ });
319
+ updateSnapshotOrganization(sessionSnapshot, null);
320
+ }
321
+
322
+ const selectionRuntime = { env, fetchImpl, machine, sessionSnapshot };
323
+ if (input.organization) {
324
+ return {
325
+ context: await selectAndRevalidate(input.organization, context, selectionRuntime),
326
+ action: null,
327
+ exitCode: 0,
328
+ outcome: { class: "ready", phase: "complete" },
329
+ };
330
+ }
331
+
332
+ if (hintIsCurrent) {
333
+ return {
334
+ context,
335
+ action: null,
336
+ exitCode: 0,
337
+ outcome: { class: "ready", phase: "complete" },
338
+ };
339
+ }
340
+
341
+ const approved = context.organizations.filter((candidate) => candidate.approved);
342
+ if (approved.length === 1) {
343
+ context = await selectAndRevalidate(approved[0].id, context, {
344
+ ...selectionRuntime,
345
+ machine: true,
346
+ });
347
+ return {
348
+ context,
349
+ action: null,
350
+ exitCode: 0,
351
+ outcome: { class: "ready", phase: "complete" },
352
+ };
353
+ }
354
+ if (approved.length > 1 && promptIfMultiple && eligibleHuman) {
355
+ const answer = await promptForOrganization(approved, { stdin, stdout });
356
+ context = await selectAndRevalidate(answer, context, {
357
+ ...selectionRuntime,
358
+ machine: false,
359
+ });
360
+ return {
361
+ context,
362
+ action: null,
363
+ exitCode: 0,
364
+ outcome: { class: "ready", phase: "complete" },
365
+ };
366
+ }
367
+ if (approved.length > 1) {
368
+ return {
369
+ context,
370
+ action: selectionAction(machine),
371
+ exitCode: 3,
372
+ outcome: { class: "action_required", phase: "organization_selection" },
373
+ };
374
+ }
375
+ return {
376
+ context,
377
+ action: null,
378
+ exitCode: 0,
379
+ outcome: { class: "ready", phase: "complete" },
380
+ };
381
+ }
382
+
383
+ function loginAction(started) {
384
+ return {
385
+ type: "browser",
386
+ url: started.verificationUri,
387
+ code: started.userCode,
388
+ description: "Complete device authorization in a browser.",
389
+ };
390
+ }
391
+
392
+ function deviceFailure(error) {
393
+ const action = {
394
+ type: "command",
395
+ command: "ornn login",
396
+ description: "Start a new browser login.",
397
+ };
398
+ if (!(error instanceof DeviceAuthError)) {
399
+ return new SessionCommandError(error?.message ?? "Could not reach Ornn for browser login.", {
400
+ action,
401
+ code: "login_unavailable",
402
+ exitCode: 6,
403
+ retryable: true,
404
+ });
405
+ }
406
+ if (error.detail?.code === "interrupted") {
407
+ return new SessionCommandError("Local browser login waiting was interrupted.", {
408
+ action,
409
+ code: "login_interrupted",
410
+ exitCode: 130,
411
+ outcomeClass: "working",
412
+ retryable: true,
413
+ });
414
+ }
415
+ const expired = /expired|timed out/i.test(error.message);
416
+ const denied = /denied/i.test(error.message);
417
+ const message = expired ? "Device login expired or timed out." : error.message;
418
+ return new SessionCommandError(message, {
419
+ action,
420
+ code: expired ? "login_expired" : denied ? "login_denied" : "login_failed",
421
+ exitCode: expired || denied ? 3 : error.status >= 500 || error.status === undefined ? 6 : 1,
422
+ outcomeClass: expired || denied ? "action_required" : "failed",
423
+ retryable: expired || denied || error.status >= 500 || error.status === undefined,
424
+ });
425
+ }
426
+
427
+ async function login(input, runtime) {
428
+ const authBaseUrl = resolveAuthBaseUrl({ env: runtime.env, explicit: input.authBase });
429
+ const apiBaseUrl = resolveApiBaseUrl({ env: runtime.env, session: { authBaseUrl } });
430
+ if (runtime.eligibleHuman && !runtime.machine) {
431
+ runtime.stderr.write(`Starting Ornn browser login through ${apiBaseUrl}\n`);
432
+ }
433
+
434
+ let started;
435
+ try {
436
+ started = await startDeviceFlow({ apiBaseUrl, authBaseUrl, fetchImpl: runtime.fetchImpl });
437
+ } catch (error) {
438
+ throw deviceFailure(error);
439
+ }
440
+
441
+ if (!runtime.eligibleHuman || runtime.machine) {
442
+ return {
443
+ data: {
444
+ verification_uri: started.verificationUri,
445
+ user_code: started.userCode,
446
+ expires_in_seconds: started.expiresIn,
447
+ },
448
+ action: loginAction(started),
449
+ exitCode: 3,
450
+ outcome: { class: "action_required", phase: "browser_authorization" },
451
+ };
452
+ }
453
+
454
+ runtime.stderr.write(
455
+ `Open this URL to continue:\n${started.verificationUri}\nCode: ${started.userCode}\n`
456
+ );
457
+ if (!input.noBrowser) {
458
+ const opened = await (runtime.openBrowserImpl ?? openBrowser)(started.verificationUri);
459
+ if (!opened)
460
+ runtime.stderr.write(
461
+ "Could not open the browser automatically. Open the URL above manually.\n"
462
+ );
463
+ }
464
+ runtime.stderr.write("Waiting for browser approval");
465
+
466
+ let session;
467
+ try {
468
+ session = await waitForDeviceApproval({
469
+ apiBaseUrl,
470
+ authBaseUrl,
471
+ deviceCode: started.deviceCode,
472
+ expiresIn: started.expiresIn,
473
+ fetchImpl: runtime.fetchImpl,
474
+ now: runtime.now,
475
+ signal: runtime.signal,
476
+ sleepImpl: runtime.sleep,
477
+ interval: input.pollInterval ?? started.interval,
478
+ onPending: () => runtime.stderr.write("."),
479
+ timeoutSeconds: input.timeout,
480
+ });
481
+ } catch (error) {
482
+ runtime.stderr.write("\n");
483
+ throw deviceFailure(error);
484
+ }
485
+
486
+ await saveAuthSession(session, { env: runtime.env });
487
+ runtime.stderr.write(`\nSession saved to ${getAuthConfigPath(runtime.env)}\n`);
488
+ const resolved = await resolveContext(
489
+ { organization: input.organization },
490
+ { ...runtime, machine: false, promptIfMultiple: true }
491
+ );
492
+ return {
493
+ data: { ...resolved.context, session_saved: true },
494
+ action: resolved.action,
495
+ exitCode: resolved.exitCode,
496
+ outcome: resolved.outcome,
497
+ };
498
+ }
499
+
500
+ async function context(input, runtime) {
501
+ const resolved = await resolveContext(input, { ...runtime, promptIfMultiple: false });
502
+ return {
503
+ data: resolved.context,
504
+ action: resolved.action,
505
+ exitCode: resolved.exitCode,
506
+ outcome: resolved.outcome,
507
+ };
508
+ }
509
+
510
+ async function logout(runtime) {
511
+ let session = null;
512
+ let revocationError = null;
513
+ let attempted = false;
514
+ try {
515
+ session = await loadAuthSession({ env: runtime.env });
516
+ } catch (error) {
517
+ revocationError = error;
518
+ }
519
+ try {
520
+ if (!revocationError && session?.accessToken) {
521
+ attempted = true;
522
+ await cliRequest({
523
+ endpoint: "/v1/cli/session",
524
+ env: runtime.env,
525
+ fetchImpl: runtime.fetchImpl,
526
+ method: "DELETE",
527
+ raw: true,
528
+ session,
529
+ });
530
+ }
531
+ } catch (error) {
532
+ revocationError = error;
533
+ } finally {
534
+ await clearAuthSession({ env: runtime.env });
535
+ }
536
+
537
+ const data = {
538
+ local_session_cleared: true,
539
+ remote_revocation: revocationError ? "unconfirmed" : attempted ? "confirmed" : "not_applicable",
540
+ remote_session_revoked: attempted && !revocationError,
541
+ };
542
+ if (!revocationError) {
543
+ return {
544
+ data,
545
+ action: null,
546
+ exitCode: 0,
547
+ outcome: { class: "ended", phase: "logged_out" },
548
+ };
549
+ }
550
+ const message = `Local session cleared, but Ornn could not confirm remote revocation: ${revocationError.message}`;
551
+ return {
552
+ data,
553
+ action: null,
554
+ errors: [{ code: "remote_revocation_unconfirmed", message, retryable: false }],
555
+ exitCode: 4,
556
+ outcome: { class: "partial", phase: "remote_revocation_unconfirmed" },
557
+ };
558
+ }
559
+
560
+ export async function executeSessionCommand(command, input, runtime) {
561
+ if (command === "login") return login(input, runtime);
562
+ if (command === "context") return context(input, runtime);
563
+ if (command === "logout") return logout(runtime);
564
+ throw new CliApiError(`Unsupported session capability: ${command}`);
565
+ }