@ornncompute/cli 1.0.35 → 1.0.62

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 (31) hide show
  1. package/README.md +92 -47
  2. package/package.json +2 -2
  3. package/src/admin-capacity-reservations.mjs +65 -153
  4. package/src/admin-command-handlers.mjs +2 -0
  5. package/src/admin-credits.mjs +57 -0
  6. package/src/admin-node-lifecycle.mjs +48 -7
  7. package/src/admin-shell.mjs +113 -27
  8. package/src/catalog-shell.mjs +13 -21
  9. package/src/evidence-commands.mjs +30 -12
  10. package/src/kubernetes-commands.mjs +58 -16
  11. package/src/launch-mode-handlers.mjs +2 -2
  12. package/src/storage-commands.mjs +9 -1
  13. package/src/vm-commands.mjs +3 -3
  14. package/vendor/capabilities/capabilities/admin-capacity-reservations.js +55 -57
  15. package/vendor/capabilities/capabilities/admin-enrollment.js +3 -3
  16. package/vendor/capabilities/capabilities/admin.js +9 -7
  17. package/vendor/capabilities/capabilities/commerce-market.js +179 -104
  18. package/vendor/capabilities/capabilities/compute.js +4 -2
  19. package/vendor/capabilities/capabilities/credits.js +78 -0
  20. package/vendor/capabilities/capabilities/fleet.js +6 -5
  21. package/vendor/capabilities/capabilities/listings.js +58 -38
  22. package/vendor/capabilities/capabilities/market-shared.js +2 -0
  23. package/vendor/capabilities/capabilities/markets.js +254 -221
  24. package/vendor/capabilities/capabilities/native-listing.js +63 -0
  25. package/vendor/capabilities/capabilities/organization-invites.js +53 -0
  26. package/vendor/capabilities/capabilities/reservation-list.js +68 -0
  27. package/vendor/capabilities/capabilities/staff-shared.js +64 -0
  28. package/vendor/capabilities/capabilities/users.js +141 -31
  29. package/vendor/capabilities/cli-catalog.js +8 -2
  30. package/vendor/capabilities/cli-migrations.js +13 -10
  31. package/vendor/capabilities/software-catalog.js +52 -6
@@ -0,0 +1,63 @@
1
+ import { z } from "zod";
2
+ import { flag } from "./market-shared.js";
3
+ export const nativeTermFields = {
4
+ type: z.enum(["reserved", "spot", "on-demand"]).nullable().optional(),
5
+ minDurationHours: z.coerce.number().nonnegative(),
6
+ maxDurationHours: z.coerce.number().nonnegative(),
7
+ pricePerGpuHourMicroUsd: z.coerce.number().int().positive(),
8
+ downPaymentCents: z.coerce.number().int().nonnegative().nullable().optional(),
9
+ renewable: flag,
10
+ };
11
+ export const nativeTermInput = z.object(nativeTermFields).strict();
12
+ export const nativeTermsInput = z.preprocess((value) => {
13
+ if (typeof value !== "string")
14
+ return value;
15
+ try {
16
+ return JSON.parse(value);
17
+ }
18
+ catch {
19
+ return value;
20
+ }
21
+ }, z.array(nativeTermInput));
22
+ export const listingWriteControls = {
23
+ confirm: z.boolean().optional(),
24
+ idempotencyKey: z.string().min(1).max(200),
25
+ };
26
+ export const nativeListingCreateInput = z
27
+ .object({
28
+ organizationId: z.string().uuid().nullable().optional(),
29
+ kind: z.enum(["primary", "secondary"]).default("primary"),
30
+ gpuSlug: z.string().min(1),
31
+ gpusPerNode: z.coerce.number().int().positive(),
32
+ capacityNodeCount: z.coerce.number().int().positive(),
33
+ facilityId: z.string().uuid(),
34
+ provisioningType: z.enum(["bare-metal", "virtual-machine"]),
35
+ state: z.enum(["draft", "available"]),
36
+ availableAt: z.iso.datetime({ offset: true }).nullable().optional(),
37
+ availableUntil: z.iso.datetime({ offset: true }).nullable().optional(),
38
+ daysToRfs: z.coerce.number().int().nonnegative().optional(),
39
+ cpu: z.string().optional(),
40
+ ram: z.string().optional(),
41
+ storage: z.string().optional(),
42
+ fabricType: z.string().optional(),
43
+ internet: z.string().optional(),
44
+ terms: nativeTermsInput,
45
+ ...listingWriteControls,
46
+ })
47
+ .strict();
48
+ export const nativeListingUpdateFields = {
49
+ state: z.enum(["draft", "available", "delisted"]).optional(),
50
+ nodeCount: z.coerce.number().int().positive().optional(),
51
+ gpuSlug: z.string().min(1).optional(),
52
+ gpusPerNode: z.coerce.number().int().positive().optional(),
53
+ facilityId: z.string().uuid().optional(),
54
+ provisioningType: z.enum(["bare-metal", "virtual-machine"]).optional(),
55
+ availableAt: z.iso.datetime({ offset: true }).optional(),
56
+ availableUntil: z.iso.datetime({ offset: true }).nullable().optional(),
57
+ daysToRfs: z.coerce.number().int().nonnegative().optional(),
58
+ cpu: z.string().optional(),
59
+ ram: z.string().optional(),
60
+ storage: z.string().optional(),
61
+ fabricType: z.string().optional(),
62
+ internet: z.string().optional(),
63
+ };
@@ -0,0 +1,53 @@
1
+ import { z } from "zod";
2
+ import { defineCapability } from "../capability.js";
3
+ export const organizationInviteInput = z.object({
4
+ organization_id: z.uuid(),
5
+ email: z.string().trim().toLowerCase().pipe(z.email()),
6
+ });
7
+ export const organizationInviteCapability = defineCapability({
8
+ id: "organization.invite",
9
+ domain: "users",
10
+ roles: ["admin"],
11
+ description: "Email an invitation to an existing organization. Resolve its UUID with organization_list first; ask the operator to choose if matches are ambiguous. The recipient may already belong to another organization. Send directly when the admin requests the invitation.",
12
+ mutation: "immediate",
13
+ http: { method: "POST", path: "/v1/staff/invites" },
14
+ cli: {
15
+ command: ["admin", "organizations", "invite"],
16
+ vnext: {
17
+ audience: "admin",
18
+ category: "resource",
19
+ command: ["admin", "organizations", "invite"],
20
+ outcome: "Email an invitation to an existing organization.",
21
+ human: "ornn admin organizations invite --organization UUID --email EMAIL",
22
+ automation: "ornn admin organizations invite --organization UUID --email EMAIL --format json",
23
+ acceptedReferences: ["organization UUID from admin organizations list"],
24
+ related: ["admin organizations list"],
25
+ },
26
+ },
27
+ slack: {
28
+ parameters: {
29
+ type: "object",
30
+ properties: {
31
+ organization_id: {
32
+ type: "string",
33
+ format: "uuid",
34
+ description: "Exact organization UUID from organization_list.",
35
+ },
36
+ email: {
37
+ type: "string",
38
+ format: "email",
39
+ description: "Address to receive the invitation.",
40
+ },
41
+ },
42
+ required: ["organization_id", "email"],
43
+ },
44
+ },
45
+ input: organizationInviteInput,
46
+ execute: async (ctx, input) => {
47
+ const parsed = organizationInviteInput.parse(input);
48
+ return ctx.fetch("/v1/staff/invites", {
49
+ method: "POST",
50
+ body: { ...parsed, send_email: true },
51
+ });
52
+ },
53
+ });
@@ -0,0 +1,68 @@
1
+ import { z } from "zod";
2
+ import { defineCapability } from "../capability.js";
3
+ import { queryString } from "./staff-shared.js";
4
+ export const reservationListInput = z.object({
5
+ customerId: z.string().uuid().optional(),
6
+ listingId: z.string().uuid().optional(),
7
+ listingTermId: z.string().uuid().optional(),
8
+ status: z.enum(["confirmed", "active", "completed", "cancelled"]).optional(),
9
+ cursor: z.string().optional(),
10
+ limit: z.number().int().min(1).max(100).optional(),
11
+ });
12
+ // Staff on every surface: Slack and the CLI resolve a customer's reservations
13
+ // with this, then nodes.list reservation_id for that reservation's machines.
14
+ export const reservationListCapability = defineCapability({
15
+ id: "reservation.list",
16
+ domain: "reservations",
17
+ roles: ["admin"],
18
+ description: "Internal staff only: list Commerce reservations across organizations, optionally filtered by customer organization (customerId), listing, listing term, and derived status. Resolve a customer's organization with users.list or organization.list first; pass each reservationId to nodes.list reservation_id for its machines. Page with nextCursor as cursor.",
19
+ mutation: "read",
20
+ http: { method: "GET", path: "/commerce/reservations" },
21
+ cli: {
22
+ command: ["admin", "reservations", "list"],
23
+ vnext: {
24
+ audience: "admin",
25
+ category: "resource",
26
+ command: ["admin", "reservations", "list"],
27
+ outcome: "List reservations across organizations.",
28
+ human: "ornn admin reservations list [--customer UUID] [--listing UUID] [--listing-term UUID] [--status confirmed|active|completed|cancelled] [--limit 50] [--cursor CURSOR]",
29
+ automation: "ornn admin reservations list --customer UUID --limit 50 [--cursor CURSOR] --format json",
30
+ acceptedReferences: [],
31
+ },
32
+ },
33
+ slack: {
34
+ parameters: {
35
+ type: "object",
36
+ properties: {
37
+ customerId: {
38
+ type: "string",
39
+ description: "Optional customer organization UUID from users_list or organization_list.",
40
+ },
41
+ listingId: { type: "string", description: "Optional listing UUID." },
42
+ listingTermId: { type: "string", description: "Optional listing term UUID." },
43
+ status: {
44
+ type: "string",
45
+ enum: ["confirmed", "active", "completed", "cancelled"],
46
+ description: "Optional derived reservation status.",
47
+ },
48
+ cursor: {
49
+ type: "string",
50
+ description: "Optional. Pass the previous response's nextCursor to fetch the next page.",
51
+ },
52
+ limit: { type: "integer", description: "Optional page size (Slack returns at most 10)." },
53
+ },
54
+ },
55
+ },
56
+ input: reservationListInput,
57
+ execute: async (ctx, input) => {
58
+ const parsed = reservationListInput.parse(input ?? {});
59
+ return ctx.fetch(`/commerce/reservations${queryString({
60
+ customerId: parsed.customerId,
61
+ listingId: parsed.listingId,
62
+ status: parsed.status,
63
+ listingTermId: parsed.listingTermId,
64
+ cursor: parsed.cursor,
65
+ limit: parsed.limit,
66
+ })}`, { method: "GET" });
67
+ },
68
+ });
@@ -0,0 +1,64 @@
1
+ /** Helpers shared by staff capabilities. */
2
+ /**
3
+ * Build a request body from the fields that were actually supplied.
4
+ *
5
+ * Staff writes are almost all "PATCH the fields the caller named": `undefined`
6
+ * means leave alone, and `null` is a real value that clears the field. So the
7
+ * check is `!== undefined`, never truthiness.
8
+ */
9
+ export function mapBody(input, mapping) {
10
+ const body = {};
11
+ for (const [field, wire] of Object.entries(mapping)) {
12
+ if (input[field] !== undefined)
13
+ body[wire] = input[field];
14
+ }
15
+ return body;
16
+ }
17
+ /** Path segment escape. */
18
+ export function seg(value) {
19
+ return encodeURIComponent(value);
20
+ }
21
+ /**
22
+ * A transport failure's HTTP status, when it carries one.
23
+ *
24
+ * Capabilities cannot import a surface's error class (`apps/mcp`'s
25
+ * `WebApiError`, say), so read the status structurally.
26
+ */
27
+ export function statusOf(error) {
28
+ if (error && typeof error === "object" && "status" in error) {
29
+ const status = error.status;
30
+ if (typeof status === "number")
31
+ return status;
32
+ }
33
+ return undefined;
34
+ }
35
+ /**
36
+ * Read a resource's current optimistic-concurrency version.
37
+ *
38
+ * Commerce rejects a PATCH or lifecycle action whose `version` is absent or
39
+ * non-positive ("missing version" / "version must be positive"), but no staff
40
+ * caller has that number to hand. Read it back when it was not supplied, and
41
+ * only once confirmed so a preview stays a pure dry run.
42
+ */
43
+ export async function resolveVersion(ctx, path, provided) {
44
+ if (provided && provided > 0)
45
+ return provided;
46
+ if (!ctx.confirmed)
47
+ return 1;
48
+ try {
49
+ const current = (await ctx.fetch(path, { method: "GET" }));
50
+ if (typeof current?.version === "number" && current.version > 0)
51
+ return current.version;
52
+ }
53
+ catch {
54
+ // Fall through: send 1 and let commerce report the conflict, which names
55
+ // the expected version, rather than failing on a lookup the caller can't see.
56
+ }
57
+ return 1;
58
+ }
59
+ export function queryString(params) {
60
+ const parts = Object.entries(params)
61
+ .filter((entry) => entry[1] !== undefined && entry[1] !== "")
62
+ .map(([key, value]) => `${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`);
63
+ return parts.length > 0 ? `?${parts.join("&")}` : "";
64
+ }
@@ -1,49 +1,159 @@
1
1
  import { z } from "zod";
2
2
  import { defineCapability } from "../capability.js";
3
- function queryPath(path, params) {
4
- const parts = [];
5
- for (const [key, value] of Object.entries(params)) {
6
- if (value?.trim())
7
- parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(value.trim())}`);
3
+ const directoryLimit = z.number().int().min(1).max(100).default(25);
4
+ const directoryOffset = z.number().int().nonnegative().optional();
5
+ export const organizationListInput = z.object({
6
+ q: z.string().trim().optional().describe("Company name, contact/member name, or email."),
7
+ role: z.enum(["consumer", "supplier"]).optional(),
8
+ approved: z.boolean().optional(),
9
+ ids: z.array(z.uuid()).min(1).max(100).optional(),
10
+ limit: directoryLimit,
11
+ offset: directoryOffset,
12
+ });
13
+ export const usersListInput = z.object({
14
+ q: z.string().trim().optional().describe("Person name, email, or company name."),
15
+ organization_id: z.uuid().optional(),
16
+ limit: directoryLimit,
17
+ offset: directoryOffset,
18
+ });
19
+ function directoryPath(input) {
20
+ const params = [
21
+ `limit=${input.limit}`,
22
+ `offset=${input.offset}`,
23
+ "sort=name",
24
+ "direction=asc",
25
+ ];
26
+ if (input.q)
27
+ params.push(`q=${encodeURIComponent(input.q)}`);
28
+ if (input.role)
29
+ params.push(`role=${encodeURIComponent(input.role)}`);
30
+ if (input.approved !== undefined) {
31
+ params.push(`approved=${input.approved}`);
8
32
  }
9
- return parts.length ? `${path}?${parts.join("&")}` : path;
33
+ if (input.ids)
34
+ params.push(`ids=${encodeURIComponent(input.ids.join(","))}`);
35
+ return `/v1/organizations?${params.join("&")}`;
36
+ }
37
+ function directoryMembers(organization) {
38
+ return organization.members ?? [];
39
+ }
40
+ function organizationMatchesQuery(organization, query) {
41
+ return (organization.name.toLowerCase().includes(query) ||
42
+ organization.id.toLowerCase() === query ||
43
+ [
44
+ organization.company_profile?.company_name,
45
+ organization.company_profile?.contact_name,
46
+ organization.company_profile?.contact_email,
47
+ organization.company_profile?.work_email,
48
+ ].some((value) => value?.toLowerCase().includes(query)));
10
49
  }
11
- const usersQueryInput = z.object({
12
- name: z.string().optional(),
13
- email: z.string().optional(),
14
- organization: z.string().optional(),
15
- q: z.string().optional(),
16
- max: z.number().int().positive().max(1000).optional(),
17
- limit: z.number().int().positive().max(1000).optional(),
50
+ export const organizationListCapability = defineCapability({
51
+ id: "organization.list",
52
+ domain: "users",
53
+ roles: ["admin"],
54
+ description: "List organizations from the Authorization directory. Search by company name, contact/member name, or email; filter by role, approval, or UUIDs. Use the organization id as organizationId when recording downtime, and ask the user to choose if matches are ambiguous.",
55
+ mutation: "read",
56
+ http: { method: "GET", path: "/v1/organizations" },
57
+ slack: {
58
+ parameters: {
59
+ type: "object",
60
+ properties: {
61
+ q: {
62
+ type: "string",
63
+ description: "Company name, contact/member name, or email.",
64
+ },
65
+ role: { type: "string", enum: ["consumer", "supplier"] },
66
+ approved: { type: "boolean" },
67
+ ids: {
68
+ type: "array",
69
+ items: { type: "string", format: "uuid" },
70
+ minItems: 1,
71
+ maxItems: 100,
72
+ },
73
+ limit: { type: "integer", minimum: 1, maximum: 100, default: 25 },
74
+ offset: { type: "integer", minimum: 0 },
75
+ },
76
+ },
77
+ },
78
+ input: organizationListInput,
79
+ execute: async (ctx, input) => {
80
+ const parsed = organizationListInput.parse(input);
81
+ const limit = parsed.limit;
82
+ const offset = parsed.offset ?? 0;
83
+ const rows = (await ctx.fetch(directoryPath({ ...parsed, limit, offset }), { method: "GET" }));
84
+ return {
85
+ organizations: rows.map((organization) => ({
86
+ id: organization.id,
87
+ name: organization.name,
88
+ role: organization.role,
89
+ approved: organization.approved,
90
+ members: directoryMembers(organization).map((member) => ({
91
+ user_id: member.user_id,
92
+ email: member.email,
93
+ name: member.name ?? null,
94
+ })),
95
+ })),
96
+ ...(rows.length === limit ? { next_offset: offset + rows.length } : {}),
97
+ };
98
+ },
18
99
  });
19
- export const usersQueryCapability = defineCapability({
20
- id: "users.query",
100
+ export const usersListCapability = defineCapability({
101
+ id: "users.list",
21
102
  domain: "users",
22
103
  roles: ["admin"],
23
- description: "Admin-only directory search. Filter by name, email, or organization. Optional max (default 100, cap 1000). Returns matching users and organizations.",
104
+ description: "List users who belong to an organization, from the Authorization directory. Search by person name, email, or company name; use organization_id to restrict results to one organization.",
24
105
  mutation: "read",
25
- http: { method: "GET", path: "/internal/users" },
106
+ http: { method: "GET", path: "/v1/organizations" },
26
107
  slack: {
27
108
  parameters: {
28
109
  type: "object",
29
110
  properties: {
30
- name: { type: "string" },
31
- email: { type: "string" },
32
- organization: { type: "string" },
33
- max: { type: "integer", description: "Maximum rows to return (1-1000)." },
111
+ q: { type: "string", description: "Person name, email, or company name." },
112
+ organization_id: { type: "string", format: "uuid" },
113
+ limit: {
114
+ type: "integer",
115
+ minimum: 1,
116
+ maximum: 100,
117
+ default: 25,
118
+ description: "Maximum number of organizations to search.",
119
+ },
120
+ offset: { type: "integer", minimum: 0 },
34
121
  },
35
122
  },
36
123
  },
37
- input: usersQueryInput,
124
+ input: usersListInput,
38
125
  execute: async (ctx, input) => {
39
- const parsed = usersQueryInput.parse(input);
40
- const q = [parsed.q, parsed.name, parsed.email, parsed.organization].find((value) => value?.trim());
41
- return ctx.fetch(queryPath("/internal/users", {
42
- q,
43
- email: parsed.email,
44
- organization: parsed.organization,
45
- limit: String(parsed.max ?? parsed.limit ?? 100),
46
- }), { method: "GET" });
126
+ const parsed = usersListInput.parse(input);
127
+ const limit = parsed.limit;
128
+ const offset = parsed.offset ?? 0;
129
+ const rows = (await ctx.fetch(directoryPath({
130
+ limit,
131
+ offset,
132
+ q: parsed.q,
133
+ ids: parsed.organization_id ? [parsed.organization_id] : undefined,
134
+ }), { method: "GET" }));
135
+ const q = parsed.q?.toLowerCase();
136
+ const users = rows.flatMap((organization) => {
137
+ const members = directoryMembers(organization);
138
+ const matchesOrganization = q ? organizationMatchesQuery(organization, q) : false;
139
+ return members
140
+ .filter((member) => !q ||
141
+ matchesOrganization ||
142
+ (member.name ?? "").toLowerCase().includes(q) || member.email.toLowerCase().includes(q))
143
+ .map((member) => ({
144
+ user_id: member.user_id,
145
+ email: member.email,
146
+ name: member.name ?? null,
147
+ organization_id: organization.id,
148
+ organization_name: organization.name,
149
+ organization_role: organization.role,
150
+ approved: organization.approved,
151
+ }));
152
+ });
153
+ return {
154
+ users,
155
+ ...(rows.length === limit ? { next_offset: offset + rows.length } : {}),
156
+ };
47
157
  },
48
158
  });
49
- export const userCapabilities = [usersQueryCapability];
159
+ export const userCapabilities = [organizationListCapability, usersListCapability];
@@ -6,6 +6,7 @@ import { adminEnrollmentCapabilities, bindAdminEnrollmentCapabilities, } from ".
6
6
  import { adminNodeDiagnosticCapabilities } from "./capabilities/admin-node-diagnostics.js";
7
7
  import { adminNodeLifecycleCapabilities } from "./capabilities/admin-nodes-lifecycle.js";
8
8
  import { computeCapabilities } from "./capabilities/compute.js";
9
+ import { creditGrantCapability } from "./capabilities/credits.js";
9
10
  import { cliShellCapabilities } from "./capabilities/cli.js";
10
11
  import { clusterCliCapabilities, kubernetesCapabilities, slurmCapabilities, } from "./capabilities/clusters.js";
11
12
  import { externalBucketCapabilities } from "./capabilities/external-buckets.js";
@@ -16,7 +17,9 @@ import { mountCapabilities } from "./capabilities/mounts.js";
16
17
  import { identityCapabilities } from "./capabilities/identity.js";
17
18
  import { listingCapabilities } from "./capabilities/listings.js";
18
19
  import { observabilityCapabilities } from "./capabilities/observability.js";
20
+ import { organizationInviteCapability } from "./capabilities/organization-invites.js";
19
21
  import { networkCapabilities } from "./capabilities/networks.js";
22
+ import { reservationListCapability } from "./capabilities/reservation-list.js";
20
23
  import { volumeCapabilities } from "./capabilities/volumes.js";
21
24
  import { userCapabilities } from "./capabilities/users.js";
22
25
  function canonicalCliCatalog(capabilities) {
@@ -41,7 +44,7 @@ export const catalog = [
41
44
  ...bindAdminEnrollmentCapabilities(bindAdminCapabilities([
42
45
  ...cliShellCapabilities,
43
46
  ...identityCapabilities,
44
- ...listingCapabilities,
47
+ ...listingCapabilities.filter((capability) => capability.id !== "listings.create"),
45
48
  ...commerceMarketCapabilities,
46
49
  ...marketCapabilities,
47
50
  ...computeCapabilities,
@@ -60,8 +63,11 @@ export const catalog = [
60
63
  ...adminNodeLifecycleCapabilities,
61
64
  ])),
62
65
  ...adminCapabilities.filter((capability) => capability.id === "nodes.health"),
63
- ...adminCapacityReservationCapabilities.filter((capability) => capability.id !== "listings.create"),
66
+ ...adminCapacityReservationCapabilities,
67
+ reservationListCapability,
64
68
  ...adminNodeDiagnosticCapabilities,
65
69
  ...adminEnrollmentCapabilities,
70
+ organizationInviteCapability,
71
+ creditGrantCapability,
66
72
  ]),
67
73
  ];
@@ -9,21 +9,25 @@ export const LEGACY_CLI_MIGRATIONS = [
9
9
  { legacy: ["catalog", "search"], replacement: "ornn capacity-offers list" },
10
10
  { legacy: ["gpu-types"], replacement: "ornn capacity-offers list" },
11
11
  { legacy: ["availability", "list"], replacement: "ornn capacity-offers list" },
12
- { legacy: ["availability", "forecast"], replacement: "ornn schedules list" },
12
+ { legacy: ["availability", "forecast"], replacement: "ornn capacity-offers availability" },
13
13
  { legacy: ["availability", "show"], replacement: "ornn capacity-offers show" },
14
14
  { legacy: ["listing", "list"], replacement: "ornn capacity-offers list" },
15
- { legacy: ["listing", "forecast"], replacement: "ornn schedules list" },
15
+ { legacy: ["listing", "forecast"], replacement: "ornn capacity-offers availability" },
16
16
  { legacy: ["listing", "show"], replacement: "ornn capacity-offers show" },
17
17
  { legacy: ["listings", "list"], replacement: "ornn capacity-offers list" },
18
- { legacy: ["listings", "forecast"], replacement: "ornn schedules list" },
18
+ { legacy: ["listings", "forecast"], replacement: "ornn capacity-offers availability" },
19
19
  { legacy: ["listings", "show"], replacement: "ornn capacity-offers show" },
20
20
  { legacy: ["listings", "create"], replacement: "ornn admin capacity-offers create" },
21
21
  { legacy: ["buy"], replacement: "ornn offers post" },
22
- { legacy: ["bid", "create"], replacement: "ornn spot bid" },
23
- { legacy: ["bid", "list"], replacement: "ornn spot orders" },
24
- { legacy: ["bid", "show"], replacement: "ornn spot orders" },
25
- { legacy: ["bid", "update"], replacement: "ornn spot bid" },
26
- { legacy: ["bid", "withdraw"], replacement: "ornn spot cancel" },
22
+ { legacy: ["schedules", "list"], replacement: "ornn capacity-offers list --kind secondary" },
23
+ { legacy: ["schedules", "show"], replacement: "ornn capacity-offers availability" },
24
+ { legacy: ["schedules", "buy"], replacement: "ornn reservations buy" },
25
+ { legacy: ["schedules"], replacement: "ornn capacity-offers list" },
26
+ { legacy: ["bid", "create"], replacement: "ornn market bid" },
27
+ { legacy: ["bid", "list"], replacement: "ornn market orders" },
28
+ { legacy: ["bid", "show"], replacement: "ornn market orders" },
29
+ { legacy: ["bid", "update"], replacement: "ornn market bid" },
30
+ { legacy: ["bid", "withdraw"], replacement: "ornn market cancel" },
27
31
  {
28
32
  legacy: ["exchange", "create"],
29
33
  replacement: "ornn capacity-offers list (use the Ornn web app for commercial workflows)",
@@ -231,7 +235,6 @@ export const LEGACY_CLI_MIGRATIONS = [
231
235
  { legacy: ["tokens", "list"], replacement: "ornn admin enrollment-tokens list" },
232
236
  { legacy: ["tokens", "create"], replacement: "ornn admin enrollment-tokens create" },
233
237
  { legacy: ["tokens", "revoke"], replacement: "ornn admin enrollment-tokens revoke" },
234
- { legacy: ["users", "query"], replacement: "ornn admin users search" },
235
238
  { legacy: ["node", "enroll"], replacement: "ornn admin nodes enroll" },
236
239
  { legacy: ["node", "list"], replacement: "ornn admin nodes list" },
237
240
  { legacy: ["node", "health"], replacement: "ornn admin nodes health" },
@@ -247,7 +250,7 @@ export const LEGACY_CLI_MIGRATIONS = [
247
250
  { legacy: ["operators"], replacement: "ornn admin suppliers list" },
248
251
  { legacy: ["facilities"], replacement: "ornn admin facilities list" },
249
252
  { legacy: ["tokens"], replacement: "ornn admin enrollment-tokens list" },
250
- { legacy: ["users"], replacement: "ornn admin users search" },
253
+ { legacy: ["users"], replacement: "ornn admin users list" },
251
254
  { legacy: ["node"], replacement: "ornn admin nodes list" },
252
255
  { legacy: ["cache", "purge"], replacement: "ornn resources" },
253
256
  { legacy: ["api"], replacement: "ornn resources" },
@@ -1,6 +1,8 @@
1
1
  /** Live orchestrator software dictionary: `GET /v1/orchestrator/catalog/software`. */
2
2
  export const SOFTWARE_CATALOG_PATH = "/v1/orchestrator/catalog/software";
3
- export const SOFTWARE_KINDS = ["python", "package", "cuda", "nvidia_smi", "dcgmi"];
3
+ export const SOFTWARE_KINDS = ["python", "package", "cuda", "nvidia_driver", "dcgmi"];
4
+ /** Kinds a disk build installs per OS release; Python packages install anywhere. */
5
+ const RELEASE_KINDS = ["python", "cuda", "nvidia_driver", "dcgmi"];
4
6
  function stringMap(value) {
5
7
  if (!value || typeof value !== "object" || Array.isArray(value)) {
6
8
  return {};
@@ -13,22 +15,66 @@ function stringMap(value) {
13
15
  }
14
16
  return out;
15
17
  }
18
+ function stringList(value) {
19
+ return Array.isArray(value)
20
+ ? value.filter((entry) => typeof entry === "string")
21
+ : [];
22
+ }
23
+ function releaseSupport(value) {
24
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
25
+ return {};
26
+ }
27
+ const out = {};
28
+ for (const [release, kinds] of Object.entries(value)) {
29
+ if (!kinds || typeof kinds !== "object")
30
+ continue;
31
+ const row = kinds;
32
+ out[release] = Object.fromEntries(RELEASE_KINDS.map((kind) => [kind, stringList(row[kind])]));
33
+ }
34
+ return out;
35
+ }
16
36
  export function parseSoftwareCatalog(payload) {
17
37
  const row = payload && typeof payload === "object" ? payload : {};
18
- const python = Array.isArray(row.python)
19
- ? row.python.filter((value) => typeof value === "string")
20
- : [];
21
38
  return {
22
- python,
39
+ python: stringList(row.python),
23
40
  packages: stringMap(row.packages),
24
41
  cuda: stringMap(row.cuda),
25
- nvidia_smi: stringMap(row.nvidia_smi),
42
+ nvidia_driver: stringMap(row.nvidia_driver),
26
43
  dcgmi: stringMap(row.dcgmi),
44
+ os_releases: releaseSupport(row.os_releases),
27
45
  };
28
46
  }
29
47
  export async function loadSoftwareCatalog(fetchImpl) {
30
48
  return parseSoftwareCatalog(await fetchImpl(SOFTWARE_CATALOG_PATH, { method: "GET" }));
31
49
  }
50
+ /**
51
+ * The catalog a disk built on an image of `osRelease` can install: Python,
52
+ * CUDA, driver, and DCGM choices narrowed to that release (none when it is
53
+ * unset or unknown); packages unchanged.
54
+ */
55
+ export function softwareCatalogForOsRelease(catalog, osRelease) {
56
+ const supported = catalog.os_releases[osRelease ?? ""] ?? {};
57
+ const allows = (kind, slug) => supported[kind]?.includes(slug) ?? false;
58
+ const narrow = (kind) => Object.fromEntries(Object.entries(catalog[kind]).filter(([, slug]) => allows(kind, slug)));
59
+ return {
60
+ ...catalog,
61
+ python: catalog.python.filter((version) => allows("python", version)),
62
+ cuda: narrow("cuda"),
63
+ nvidia_driver: narrow("nvidia_driver"),
64
+ dcgmi: narrow("dcgmi"),
65
+ };
66
+ }
67
+ /** `stack` with every version `catalog` does not offer cleared. */
68
+ export function keepOfferedChoices(stack, catalog) {
69
+ const offered = (map, slug) => slug && Object.values(map).includes(slug) ? slug : "";
70
+ return {
71
+ ...stack,
72
+ python_version: catalog.python.includes(stack.python_version) ? stack.python_version : "",
73
+ cuda_version: offered(catalog.cuda, stack.cuda_version),
74
+ nvidia_driver_version: offered(catalog.nvidia_driver, stack.nvidia_driver_version),
75
+ dcgmi_version: offered(catalog.dcgmi, stack.dcgmi_version),
76
+ };
77
+ }
32
78
  export function displayForSoftwareSlug(catalog, kind, slug) {
33
79
  const map = catalog[kind];
34
80
  const hit = Object.entries(map).find(([, value]) => value === slug);