@skrr-ai/cli 0.1.25 → 0.1.27

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 (80) hide show
  1. package/dist/base-command.js +29 -5
  2. package/dist/commands/code/install.js +13 -0
  3. package/dist/commands/harnesses/leases/billing.d.ts +9 -6
  4. package/dist/commands/harnesses/leases/billing.js +19 -24
  5. package/dist/commands/machines/dedicated/attach.d.ts +28 -0
  6. package/dist/commands/machines/dedicated/attach.js +76 -0
  7. package/dist/commands/machines/dedicated/catalog.d.ts +17 -0
  8. package/dist/commands/machines/dedicated/catalog.js +75 -0
  9. package/dist/commands/machines/dedicated/cp.d.ts +22 -0
  10. package/dist/commands/machines/dedicated/cp.js +76 -0
  11. package/dist/commands/machines/dedicated/create.d.ts +6 -2
  12. package/dist/commands/machines/dedicated/create.js +29 -20
  13. package/dist/commands/machines/dedicated/current.d.ts +19 -0
  14. package/dist/commands/machines/dedicated/current.js +49 -0
  15. package/dist/commands/machines/dedicated/destroy.d.ts +6 -2
  16. package/dist/commands/machines/dedicated/destroy.js +43 -25
  17. package/dist/commands/machines/dedicated/exec.d.ts +32 -0
  18. package/dist/commands/machines/dedicated/exec.js +122 -0
  19. package/dist/commands/machines/dedicated/grow.d.ts +6 -2
  20. package/dist/commands/machines/dedicated/grow.js +16 -20
  21. package/dist/commands/machines/dedicated/health-check.d.ts +2 -2
  22. package/dist/commands/machines/dedicated/health-check.js +18 -15
  23. package/dist/commands/machines/dedicated/incident-close.d.ts +3 -2
  24. package/dist/commands/machines/dedicated/incident-close.js +13 -15
  25. package/dist/commands/machines/dedicated/index.js +39 -16
  26. package/dist/commands/machines/dedicated/list.d.ts +2 -2
  27. package/dist/commands/machines/dedicated/list.js +28 -17
  28. package/dist/commands/machines/dedicated/restart.d.ts +5 -2
  29. package/dist/commands/machines/dedicated/restart.js +10 -20
  30. package/dist/commands/machines/dedicated/restore.d.ts +26 -0
  31. package/dist/commands/machines/dedicated/restore.js +64 -0
  32. package/dist/commands/machines/dedicated/show.d.ts +2 -2
  33. package/dist/commands/machines/dedicated/show.js +30 -31
  34. package/dist/commands/machines/dedicated/sign-in.d.ts +19 -0
  35. package/dist/commands/machines/dedicated/sign-in.js +49 -0
  36. package/dist/commands/machines/dedicated/snapshot.d.ts +5 -2
  37. package/dist/commands/machines/dedicated/snapshot.js +13 -20
  38. package/dist/commands/machines/dedicated/spending-limit.d.ts +22 -0
  39. package/dist/commands/machines/dedicated/spending-limit.js +51 -0
  40. package/dist/commands/machines/dedicated/start.d.ts +5 -2
  41. package/dist/commands/machines/dedicated/start.js +10 -20
  42. package/dist/commands/machines/dedicated/stop.d.ts +5 -2
  43. package/dist/commands/machines/dedicated/stop.js +10 -20
  44. package/dist/commands/machines/dedicated/terminal.d.ts +22 -0
  45. package/dist/commands/machines/dedicated/terminal.js +42 -0
  46. package/dist/commands/tasks/list.js +27 -0
  47. package/dist/lib/daemonBroker.d.ts +8 -0
  48. package/dist/lib/daemonBroker.js +2 -2
  49. package/dist/lib/dedicated-copy.d.ts +22 -0
  50. package/dist/lib/dedicated-copy.js +115 -0
  51. package/dist/lib/dedicated-exec.d.ts +80 -0
  52. package/dist/lib/dedicated-exec.js +271 -0
  53. package/dist/lib/dedicated-lease-command.d.ts +59 -0
  54. package/dist/lib/dedicated-lease-command.js +181 -0
  55. package/dist/lib/dedicated-machines.d.ts +337 -12
  56. package/dist/lib/dedicated-machines.js +502 -25
  57. package/dist/lib/dedicated-terminal.d.ts +89 -0
  58. package/dist/lib/dedicated-terminal.js +275 -0
  59. package/dist/lib/dedicated-wait.d.ts +72 -0
  60. package/dist/lib/dedicated-wait.js +195 -0
  61. package/dist/lib/edge-error-page.d.ts +49 -0
  62. package/dist/lib/edge-error-page.js +120 -0
  63. package/dist/lib/harnesses.d.ts +15 -1
  64. package/dist/lib/harnesses.js +20 -2
  65. package/dist/lib/hosted-machines.js +7 -0
  66. package/dist/lib/node-adapter.js +1 -1
  67. package/dist/lib/sky-code.d.ts +30 -1
  68. package/dist/lib/sky-code.js +44 -2
  69. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
  70. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +2 -1
  71. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +10 -0
  72. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +17 -1
  73. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
  74. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
  75. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +10 -0
  76. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +16 -0
  77. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  78. package/dist/node_modules/@skrr-ai/data-provider/index.js +2625 -2608
  79. package/oclif.manifest.json +27851 -26936
  80. package/package.json +1 -1
@@ -1,20 +1,39 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.DEDICATED_LEASE_ACTIONS = void 0;
3
+ exports.DEDICATED_FILE_MAX_BYTES = exports.DEDICATED_FILE_CHUNK_BYTES = exports.DEDICATED_EXEC_DEFAULT_TIMEOUT_MS = exports.DEDICATED_EXEC_MAX_TIMEOUT_MS = exports.DEDICATED_MACHINE_PRODUCT = exports.DEDICATED_LEASE_ACTIONS = void 0;
4
4
  exports.dedicatedRuntimeCreateInput = dedicatedRuntimeCreateInput;
5
+ exports.isDedicatedRepositoryRef = isDedicatedRepositoryRef;
5
6
  exports.createDedicatedRuntime = createDedicatedRuntime;
6
7
  exports.listDedicatedRuntimes = listDedicatedRuntimes;
7
8
  exports.getDedicatedRuntime = getDedicatedRuntime;
8
9
  exports.performDedicatedLeaseAction = performDedicatedLeaseAction;
9
10
  exports.growDedicatedRuntime = growDedicatedRuntime;
11
+ exports.restoreDeletedDedicatedRuntime = restoreDeletedDedicatedRuntime;
12
+ exports.setDedicatedRuntimeSpendingLimit = setDedicatedRuntimeSpendingLimit;
10
13
  exports.closeDedicatedRuntimeIncidentHold = closeDedicatedRuntimeIncidentHold;
11
14
  exports.destroyDedicatedRuntime = destroyDedicatedRuntime;
15
+ exports.execDedicatedRuntime = execDedicatedRuntime;
16
+ exports.startDedicatedRuntimeExecution = startDedicatedRuntimeExecution;
17
+ exports.observeDedicatedRuntimeExecution = observeDedicatedRuntimeExecution;
18
+ exports.readDedicatedRuntimeFileChunk = readDedicatedRuntimeFileChunk;
19
+ exports.writeDedicatedRuntimeFileChunk = writeDedicatedRuntimeFileChunk;
20
+ exports.parseDedicatedCopyEndpoints = parseDedicatedCopyEndpoints;
12
21
  exports.dedicatedLeaseId = dedicatedLeaseId;
13
22
  exports.dedicatedLeaseState = dedicatedLeaseState;
14
23
  exports.dedicatedLeaseDisplayName = dedicatedLeaseDisplayName;
24
+ exports.formatUsdCents = formatUsdCents;
25
+ exports.dedicatedSpendSummary = dedicatedSpendSummary;
26
+ exports.describeDedicatedDeletion = describeDedicatedDeletion;
27
+ exports.describeDedicatedLease = describeDedicatedLease;
28
+ exports.dedicatedLeaseHarnesses = dedicatedLeaseHarnesses;
29
+ exports.dedicatedLeaseIdForDaemon = dedicatedLeaseIdForDaemon;
30
+ exports.dedicatedTerminalHarness = dedicatedTerminalHarness;
31
+ exports.selectDedicatedAttachHarness = selectDedicatedAttachHarness;
32
+ exports.dedicatedCatalogFromProducts = dedicatedCatalogFromProducts;
33
+ exports.dedicatedRuntimeApiErrorCode = dedicatedRuntimeApiErrorCode;
15
34
  exports.formatDedicatedRuntimeApiError = formatDedicatedRuntimeApiError;
16
35
  const api_fetch_1 = require("./api-fetch");
17
- const harnesses_1 = require("./harnesses");
36
+ const edge_error_page_1 = require("./edge-error-page");
18
37
  exports.DEDICATED_LEASE_ACTIONS = [
19
38
  'start',
20
39
  'stop',
@@ -22,6 +41,7 @@ exports.DEDICATED_LEASE_ACTIONS = [
22
41
  'snapshot',
23
42
  'health-check',
24
43
  ];
44
+ exports.DEDICATED_MACHINE_PRODUCT = 'dedicated-machine';
25
45
  /**
26
46
  * Flags in, request body out — and the point of it being a function is the
27
47
  * OMISSION.
@@ -36,6 +56,7 @@ exports.DEDICATED_LEASE_ACTIONS = [
36
56
  * means the same bug was invisible in them.
37
57
  */
38
58
  function dedicatedRuntimeCreateInput(input) {
59
+ const repositoryRef = input.repositoryRef?.trim();
39
60
  return {
40
61
  requestId: input.requestId,
41
62
  displayName: input.displayName,
@@ -47,45 +68,102 @@ function dedicatedRuntimeCreateInput(input) {
47
68
  ...(input.backupProfile
48
69
  ? { backupProfile: input.backupProfile }
49
70
  : {}),
71
+ ...(repositoryRef ? { repositoryRef } : {}),
50
72
  ...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
51
73
  };
52
74
  }
75
+ /**
76
+ * The shapes the server's repository parser accepts: `owner/repo`,
77
+ * `github.com/owner/repo`, or an https URL to it, optionally ending in `.git`.
78
+ *
79
+ * This is a typo catcher, not the authority. The server re-parses the value and
80
+ * decides whether the owner's GitHub App installation covers it; checking the
81
+ * shape here only turns "create failed" into "that is not a repository" before a
82
+ * billable request is sent.
83
+ */
84
+ function isDedicatedRepositoryRef(value) {
85
+ return /^(?:https?:\/\/)?(?:github\.com\/)?[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?(?:\.git)?$/i.test(value.trim());
86
+ }
87
+ // Takes the finished path rather than composing one from a suffix, so the route
88
+ // each call reaches is a literal at the call site. `cli/src/__tests__/helpers/
89
+ // api-surface.ts` reads coverage out of the source, and a path assembled inside a
90
+ // builder is invisible to it: exec, files/read and files/write were all reported
91
+ // unreachable by domain-api-parity while being perfectly reachable, which is the
92
+ // reading that trains people to allowlist things that are fine. Same shape as
93
+ // `withQuery` on the `request.*` side.
94
+ //
95
+ // Query parameters therefore go in `query`, never into the literal: the coverage
96
+ // reader matches the literal against the route table, and a `?name=` inside it
97
+ // makes the endpoint read as unreachable.
98
+ function withDedicatedScope(path, scope, query = {}) {
99
+ const params = Object.entries(query);
100
+ if (scope.workspaceId)
101
+ params.push(['workspaceId', scope.workspaceId]);
102
+ if (params.length === 0)
103
+ return path;
104
+ const separator = path.includes('?') ? '&' : '?';
105
+ return `${path}${separator}${params
106
+ .map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`)
107
+ .join('&')}`;
108
+ }
53
109
  async function createDedicatedRuntime(input) {
54
110
  const { requestId, workspaceId, ...body } = input;
55
- if (workspaceId) {
56
- return (0, api_fetch_1.apiFetch)(`/api/machines/dedicated?workspaceId=${encodeURIComponent(workspaceId)}`, {
57
- method: 'POST',
58
- body,
59
- headers: { 'Idempotency-Key': requestId },
60
- });
61
- }
62
- return (0, api_fetch_1.apiFetch)('/api/machines/dedicated', {
63
- method: 'POST',
64
- body,
65
- headers: { 'Idempotency-Key': requestId },
66
- });
111
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope('/api/machines/dedicated', { workspaceId }), { method: 'POST', body, headers: { 'Idempotency-Key': requestId } });
67
112
  }
68
113
  async function listDedicatedRuntimes(options = {}) {
69
- return (0, harnesses_1.listHarnessLeases)({
70
- ...options,
71
- machineProduct: 'dedicated-machine',
72
- });
114
+ const query = new URLSearchParams();
115
+ if (options.limit)
116
+ query.set('limit', String(options.limit));
117
+ if (options.state)
118
+ query.set('state', options.state);
119
+ query.set('machineProduct', exports.DEDICATED_MACHINE_PRODUCT);
120
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/leases?${query.toString()}`, options));
73
121
  }
74
- async function getDedicatedRuntime(leaseId) {
75
- return (0, harnesses_1.getHarnessLease)(leaseId);
122
+ async function getDedicatedRuntime(leaseId, scope = {}) {
123
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/leases/${encodeURIComponent(leaseId)}`, scope));
76
124
  }
77
- async function performDedicatedLeaseAction(leaseId, action) {
78
- return (0, harnesses_1.performHarnessLeaseAction)(leaseId, action);
125
+ async function performDedicatedLeaseAction(leaseId, action, scope = {}) {
126
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/leases/${encodeURIComponent(leaseId)}/actions/${encodeURIComponent(action)}`, scope), { method: 'POST' });
79
127
  }
80
128
  async function growDedicatedRuntime(leaseId, storageGb, options = {}) {
81
- return (0, api_fetch_1.apiFetch)(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/storage`, {
129
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/storage`, options), {
82
130
  method: 'POST',
83
131
  body: { storageGb },
84
132
  ...(options.requestId ? { headers: { 'Idempotency-Key': options.requestId } } : {}),
85
133
  });
86
134
  }
135
+ /**
136
+ * Restore a deleted Dedicated Runtime into a NEW machine, from the final recovery
137
+ * point its deletion kept. Only inside the deletion's recovery window, and once
138
+ * per deletion; the response's `lease` is the new machine. The cap defaults to
139
+ * the deleted machine's.
140
+ */
141
+ async function restoreDeletedDedicatedRuntime(leaseId, options = {}) {
142
+ const body = {};
143
+ if (options.displayName)
144
+ body.displayName = options.displayName;
145
+ if (options.monthlySpendingLimitCents !== undefined) {
146
+ body.monthlySpendingLimitCents = options.monthlySpendingLimitCents;
147
+ }
148
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/restore`, options), {
149
+ method: 'POST',
150
+ body,
151
+ ...(options.requestId ? { headers: { 'Idempotency-Key': options.requestId } } : {}),
152
+ });
153
+ }
154
+ /**
155
+ * Change the monthly spending cap of one lease.
156
+ *
157
+ * The route accepts `spendingLimitCents` and nothing else: who pays is decided
158
+ * at create and there is no API that moves it. A cap below what the month has
159
+ * already spent (or is projected to) is refused rather than stopping the machine
160
+ * on the next reconciler pass.
161
+ */
162
+ async function setDedicatedRuntimeSpendingLimit(leaseId, spendingLimitCents, scope = {}) {
163
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/leases/${encodeURIComponent(leaseId)}/billing`, scope), { method: 'PATCH', body: { spendingLimitCents } });
164
+ }
87
165
  async function closeDedicatedRuntimeIncidentHold(leaseId, options = {}) {
88
- return (0, api_fetch_1.apiFetch)(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/incident-hold/close`, {
166
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/incident-hold/close`, options), {
89
167
  method: 'POST',
90
168
  body: {},
91
169
  ...(options.requestId ? { headers: { 'Idempotency-Key': options.requestId } } : {}),
@@ -95,7 +173,86 @@ async function destroyDedicatedRuntime(leaseId, options = {}) {
95
173
  const query = options.retentionIntent
96
174
  ? `?retentionIntent=${encodeURIComponent(options.retentionIntent)}`
97
175
  : '';
98
- return (0, api_fetch_1.apiFetch)(`/api/machines/leases/${encodeURIComponent(leaseId)}${query}`, { method: 'DELETE' });
176
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/leases/${encodeURIComponent(leaseId)}${query}`, options), { method: 'DELETE' });
177
+ }
178
+ /** The server's ceiling for one exec; documented beside the route. */
179
+ exports.DEDICATED_EXEC_MAX_TIMEOUT_MS = 10 * 60 * 1000;
180
+ /** The server's default when a command names no timeout. */
181
+ exports.DEDICATED_EXEC_DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
182
+ /**
183
+ * Run one command on the machine as the workload user, in ONE request.
184
+ *
185
+ * The synchronous route, kept for servers that predate `executions`. It holds
186
+ * the request open while the command runs, so behind CloudFront a command that
187
+ * runs past ~60 s is cut off at the edge while it keeps running (OSK-8705).
188
+ * `runDedicatedExec` (dedicated-exec.ts) starts and observes instead, and falls
189
+ * back to this only when the server has no `executions` route.
190
+ *
191
+ * Not idempotent and not retried: `apiFetch` gives each call a fresh
192
+ * idempotency key, which is the rule for an unfenced write — running the same
193
+ * command twice is two intents even when the text is identical.
194
+ */
195
+ async function execDedicatedRuntime(leaseId, input, scope = {}) {
196
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/exec`, scope), {
197
+ method: 'POST',
198
+ body: {
199
+ command: input.command,
200
+ ...(input.cwd ? { cwd: input.cwd } : {}),
201
+ ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
202
+ },
203
+ });
204
+ }
205
+ /**
206
+ * Start one command. `requestId` is the idempotency key: the same id, for the
207
+ * same command on the same lease, returns the execution it already started —
208
+ * so a retry after a lost response never runs the command twice.
209
+ */
210
+ async function startDedicatedRuntimeExecution(leaseId, input, options) {
211
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/executions`, options), {
212
+ method: 'POST',
213
+ body: {
214
+ command: input.command,
215
+ ...(input.cwd ? { cwd: input.cwd } : {}),
216
+ ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
217
+ },
218
+ headers: { 'X-Idempotency-Key': options.requestId },
219
+ ...(options.signal ? { signal: options.signal } : {}),
220
+ });
221
+ }
222
+ /** Read a started command, letting the server hold the answer up to `waitMs` for it to end. */
223
+ async function observeDedicatedRuntimeExecution(leaseId, executionId, options) {
224
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/executions/${encodeURIComponent(executionId)}`, options, { waitMs: String(Math.max(0, Math.floor(options.waitMs))) }), options.signal ? { signal: options.signal } : {});
225
+ }
226
+ /** Mirrors the daemon's bounds (daemon/src/tools/file-transfer.ts). */
227
+ exports.DEDICATED_FILE_CHUNK_BYTES = 512 * 1024;
228
+ exports.DEDICATED_FILE_MAX_BYTES = 64 * 1024 * 1024;
229
+ async function readDedicatedRuntimeFileChunk(leaseId, input, scope = {}) {
230
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/files/read`, scope), { method: 'POST', body: input });
231
+ }
232
+ async function writeDedicatedRuntimeFileChunk(leaseId, input, scope = {}) {
233
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/files/write`, scope), { method: 'POST', body: input });
234
+ }
235
+ const REMOTE_ENDPOINT = /^(rtl_[A-Za-z0-9_-]{1,80}):(.+)$/;
236
+ /**
237
+ * `cp <src> <dst>` where exactly one side is `<lease-id>:<path>`.
238
+ *
239
+ * The remote side is recognised by the lease id prefix rather than by "has a
240
+ * colon", so a Windows path (`C:\\build\\out.zip`) or a local file whose name
241
+ * contains a colon is never mistaken for a machine.
242
+ */
243
+ function parseDedicatedCopyEndpoints(source, destination) {
244
+ const src = REMOTE_ENDPOINT.exec(source);
245
+ const dst = REMOTE_ENDPOINT.exec(destination);
246
+ if (src && dst) {
247
+ throw new Error('Copy between two machines is not supported; one side must be a local path.');
248
+ }
249
+ if (!src && !dst) {
250
+ throw new Error('One side must name the machine, as <lease-id>:<path>.');
251
+ }
252
+ if (src) {
253
+ return { direction: 'download', leaseId: src[1], remotePath: src[2], localPath: destination };
254
+ }
255
+ return { direction: 'upload', localPath: source, leaseId: dst[1], remotePath: dst[2] };
99
256
  }
100
257
  function dedicatedLeaseId(lease) {
101
258
  return String(lease?.id ?? '');
@@ -106,6 +263,268 @@ function dedicatedLeaseState(lease) {
106
263
  function dedicatedLeaseDisplayName(lease) {
107
264
  return lease?.displayName ? String(lease.displayName) : '-';
108
265
  }
266
+ /** `$12.34` — cents are what the API speaks; dollars are what a person reads. */
267
+ function formatUsdCents(cents) {
268
+ if (typeof cents !== 'number' || !Number.isFinite(cents))
269
+ return '-';
270
+ const sign = cents < 0 ? '-' : '';
271
+ return `${sign}$${(Math.abs(cents) / 100).toFixed(2)}`;
272
+ }
273
+ /** `this month $1.23 of $500.00 cap`, or why that sentence cannot be written. */
274
+ function dedicatedSpendSummary(lease) {
275
+ const billing = lease.billing;
276
+ if (!billing)
277
+ return '-';
278
+ const cap = billing.monthlySpendLimitCents ?? billing.spendingLimitCents;
279
+ if (billing.spendStatusUnavailable) {
280
+ return `spend unavailable right now · cap ${formatUsdCents(cap)}`;
281
+ }
282
+ const spent = billing.currentMonthSpendCents;
283
+ const projected = billing.projectedCurrentMonthSpendCents;
284
+ const parts = [`${formatUsdCents(spent)} this month of ${formatUsdCents(cap)} cap`];
285
+ if (typeof projected === 'number' && projected !== spent) {
286
+ parts.push(`projected ${formatUsdCents(projected)}`);
287
+ }
288
+ if (billing.spendingLimitExceeded) {
289
+ parts.push('OVER CAP — compute is stopped, files are kept');
290
+ }
291
+ return parts.join(' · ');
292
+ }
293
+ /**
294
+ * The human rendering of `machines dedicated show`, as lines.
295
+ *
296
+ * Pure so it can be pinned without a server. Every field is optional because a
297
+ * lease that is still `requested` has no resources, daemon or spend yet, and a
298
+ * missing value is omitted rather than printed as a column of dashes.
299
+ */
300
+ /** One line saying what a deletion still allows. */
301
+ function describeDedicatedDeletion(deletion, leaseId) {
302
+ if (deletion.restoredToLeaseId) {
303
+ return `restored into ${deletion.restoredToLeaseId}`;
304
+ }
305
+ if (deletion.status === 'completed') {
306
+ return `permanently deleted${deletion.completedAt ? ` ${deletion.completedAt}` : ''}; no copy remains`;
307
+ }
308
+ if (deletion.status === 'purging') {
309
+ return 'recovery window closed; every copy is being deleted';
310
+ }
311
+ if (deletion.restorable) {
312
+ return `restorable until ${deletion.recoverableUntil} — \`skrr machines dedicated restore ${leaseId || '<lease-id>'}\``;
313
+ }
314
+ return 'nothing to restore; every copy is deleted when the window closes';
315
+ }
316
+ function describeDedicatedLease(lease, harnesses = []) {
317
+ const lines = [dedicatedLeaseDisplayName(lease)];
318
+ const row = (label, value) => {
319
+ if (value === undefined || value === null || value === '')
320
+ return;
321
+ lines.push(` ${label.padEnd(13)}${value}`);
322
+ };
323
+ row('id', dedicatedLeaseId(lease));
324
+ row('state', lease.stateReason
325
+ ? `${dedicatedLeaseState(lease)} (${lease.stateReason})`
326
+ : dedicatedLeaseState(lease));
327
+ const resources = lease.resources || {};
328
+ const size = [
329
+ resources.sizePreset,
330
+ resources.cpu ? `${resources.cpu} vCPU` : '',
331
+ resources.memoryGb ? `${resources.memoryGb} GiB RAM` : '',
332
+ resources.regionClass,
333
+ ]
334
+ .filter(Boolean)
335
+ .join(' · ');
336
+ row('size', size);
337
+ const storage = lease.storage || {};
338
+ const disk = [
339
+ resources.storageGb ? `${resources.storageGb} GiB` : '',
340
+ storage.retentionClass ? `retention ${storage.retentionClass}` : '',
341
+ storage.backupProfile ? `backup ${storage.backupProfile}` : '',
342
+ ]
343
+ .filter(Boolean)
344
+ .join(' · ');
345
+ row('workspace', disk);
346
+ row('last backup', storage.lastBackupAt);
347
+ const image = [resources.imageVersion || lease.daemon?.imageVersion].filter(Boolean).join('');
348
+ row('image', image);
349
+ if (lease.daemon?.version || lease.daemon?.lastHeartbeatAt) {
350
+ row('daemon', [
351
+ lease.daemon.version ? `skrrd ${lease.daemon.version}` : '',
352
+ lease.daemon.lastHeartbeatAt ? `heartbeat ${lease.daemon.lastHeartbeatAt}` : '',
353
+ ]
354
+ .filter(Boolean)
355
+ .join(' · '));
356
+ }
357
+ const health = lease.health;
358
+ if (health && typeof health.assignable === 'boolean') {
359
+ row('health', health.assignable
360
+ ? 'can take work'
361
+ : `not assignable${health.reasons?.length ? `: ${[...new Set(health.reasons)].join(', ')}` : ''}`);
362
+ }
363
+ row('spend', lease.billing ? dedicatedSpendSummary(lease) : undefined);
364
+ if (storage.deleteAfter || storage.unpaidSince) {
365
+ row('deletion', [
366
+ storage.unpaidSince ? `unpaid since ${storage.unpaidSince}` : '',
367
+ storage.deletionNoticeAt ? `notice ${storage.deletionNoticeAt}` : '',
368
+ storage.deleteAfter ? `workspace deleted after ${storage.deleteAfter}` : '',
369
+ ]
370
+ .filter(Boolean)
371
+ .join(' · '));
372
+ }
373
+ const deletion = storage.deletion;
374
+ if (deletion?.status) {
375
+ row('deleted', describeDedicatedDeletion(deletion, dedicatedLeaseId(lease)));
376
+ }
377
+ row('restored from', lease.recovery?.restoredFromLeaseId);
378
+ const hold = lease.recovery?.incidentHold;
379
+ if (hold?.status === 'open') {
380
+ row('incident', `hold open since ${hold.heldAt || '?'} — the failed source volume is billed until \`incident-close\``);
381
+ }
382
+ const engines = dedicatedLeaseHarnesses(lease, harnesses);
383
+ if (engines.length > 0) {
384
+ row('engines', engines.map((harness) => `${harness.provider} (${harness.status || 'unknown'})`).join(', '));
385
+ }
386
+ // What the PRODUCT supports, not what this state allows: an archived lease
387
+ // still lists `start`. Labelled so nobody scripts against it as the latter.
388
+ const capabilities = Object.entries(lease.lifecycleCapabilities || {})
389
+ .filter(([, allowed]) => allowed)
390
+ .map(([name]) => name);
391
+ row('supports', capabilities.join(', '));
392
+ row('support ref', lease.supportReference);
393
+ return lines;
394
+ }
395
+ /**
396
+ * Every harness row a Dedicated Runtime lease's daemon registered, freshest
397
+ * first.
398
+ *
399
+ * A lease payload names no daemon (provider and daemon ids are deliberately
400
+ * absent), so the join goes the other way: each row a guest daemon registers
401
+ * carries `metadata.machineLeaseId` and `metadata.machineProduct`. One daemon
402
+ * writes one row PER ENGINE it announces (claude, codex, sky-code, or `oversky`
403
+ * when it announces none), all sharing the daemon id.
404
+ */
405
+ function dedicatedLeaseHarnesses(lease, harnesses) {
406
+ const seenAt = (harness) => Date.parse(harness.lastSeenAt || '') || 0;
407
+ return harnesses
408
+ .filter((harness) => {
409
+ const metadata = harness.metadata || {};
410
+ return (metadata.machineProduct === exports.DEDICATED_MACHINE_PRODUCT &&
411
+ metadata.machineLeaseId === lease.id);
412
+ })
413
+ .sort((a, b) => seenAt(b) - seenAt(a));
414
+ }
415
+ /**
416
+ * The lease of the machine this process is running on, from the harness rows.
417
+ *
418
+ * Nothing in a workload process's environment names its lease — the daemon's
419
+ * environment (lease, generation, credentials) sits on the other side of the uid
420
+ * split by design. The CLI hand-off descriptor the daemon publishes to the
421
+ * workload group does carry the daemon's id, and every harness row that daemon
422
+ * registered carries `metadata.machineLeaseId`. Joining the two answers "which
423
+ * machine am I" with no new daemon surface.
424
+ */
425
+ function dedicatedLeaseIdForDaemon(daemonId, harnesses) {
426
+ if (!daemonId)
427
+ return null;
428
+ const leaseIds = new Set(harnesses
429
+ .filter((harness) => harness.daemonId === daemonId &&
430
+ harness.metadata?.machineProduct === exports.DEDICATED_MACHINE_PRODUCT &&
431
+ typeof harness.metadata?.machineLeaseId === 'string')
432
+ .map((harness) => String(harness.metadata?.machineLeaseId)));
433
+ // One daemon serves one lease. Two would mean rows from a replaced
434
+ // generation disagree, and guessing between them is how `self` would act on
435
+ // the wrong machine.
436
+ return leaseIds.size === 1 ? [...leaseIds][0] : null;
437
+ }
438
+ /**
439
+ * The row an interactive terminal opens on: online, local, with a daemon.
440
+ *
441
+ * Any engine row reaches the same shell — they share the daemon — so the
442
+ * freshest one wins. The terminal relay's own access check refuses a row that
443
+ * is not local or has no daemon, so offering one here would only move the
444
+ * refusal later.
445
+ */
446
+ function dedicatedTerminalHarness(lease, harnesses) {
447
+ return (dedicatedLeaseHarnesses(lease, harnesses).find((harness) => harness.status === 'online' &&
448
+ Boolean(harness.daemonId) &&
449
+ (harness.harnessLocation === undefined || harness.harnessLocation === 'local')) || null);
450
+ }
451
+ /**
452
+ * The harness row an agent should be attached to, for `attach`.
453
+ *
454
+ * Choosing a row also chooses the ENGINE the agent's turns run under, so an
455
+ * ambiguous choice is refused with the options named rather than guessed. A
456
+ * row need not be online to be attached — a stopped machine keeps its rows —
457
+ * but the error says so when it is not.
458
+ */
459
+ function selectDedicatedAttachHarness(lease, harnesses, engine) {
460
+ const rows = dedicatedLeaseHarnesses(lease, harnesses);
461
+ if (rows.length === 0) {
462
+ return {
463
+ error: `No harness is registered for ${lease.id} yet. A machine's daemon registers one when it ` +
464
+ 'first connects; start the machine, wait for it to be ready, and try again.',
465
+ };
466
+ }
467
+ const providers = [...new Set(rows.map((row) => row.provider))];
468
+ if (engine) {
469
+ const match = rows.find((row) => row.provider === engine);
470
+ if (!match) {
471
+ return {
472
+ error: `${lease.id} has no "${engine}" engine. Registered: ${providers.join(', ')}.`,
473
+ };
474
+ }
475
+ return { harness: match };
476
+ }
477
+ if (providers.length > 1) {
478
+ return {
479
+ error: `${lease.id} runs more than one engine (${providers.join(', ')}); the one you attach is ` +
480
+ 'the one the agent works under. Pass --engine <name>.',
481
+ };
482
+ }
483
+ return { harness: rows[0] };
484
+ }
485
+ /**
486
+ * The Dedicated Runtime row of `GET /api/machines/products`, reduced to what a
487
+ * caller needs to choose `create` flags.
488
+ *
489
+ * `--size-preset` has no client-side list of values on purpose — presets are
490
+ * gated by server flags — so this is where a caller learns which ones exist.
491
+ */
492
+ function dedicatedCatalogFromProducts(payload) {
493
+ const products = Array.isArray(payload)
494
+ ? payload
495
+ : (payload?.products ?? []);
496
+ const product = products.find((row) => row?.machineProduct === exports.DEDICATED_MACHINE_PRODUCT || row?.id === exports.DEDICATED_MACHINE_PRODUCT);
497
+ if (!product)
498
+ return null;
499
+ const configuration = (product.configuration || {});
500
+ const billing = (product.billing || {});
501
+ const presentation = (billing.presentation || {});
502
+ const sizeCatalog = Array.isArray(configuration.sizePresetCatalog)
503
+ ? configuration.sizePresetCatalog
504
+ : (Array.isArray(configuration.sizePresets) ? configuration.sizePresets : []).map((id) => ({ id }));
505
+ const availability = typeof product.availability === 'string'
506
+ ? product.availability
507
+ : String(product.availability?.status || 'unknown');
508
+ return {
509
+ availability,
510
+ ...(typeof product.disabledReason === 'string' && product.disabledReason
511
+ ? { disabledReason: product.disabledReason }
512
+ : {}),
513
+ sizes: sizeCatalog,
514
+ regionClasses: configuration.regionClasses || [],
515
+ ...(configuration.storage
516
+ ? { storage: configuration.storage }
517
+ : {}),
518
+ retentionClasses: configuration.retentionClasses || [],
519
+ backupProfiles: configuration.backupProfiles || [],
520
+ defaultMonthlySpendingLimitCents: billing.defaultMonthlySpendingLimitCents,
521
+ maxUserMonthlySpendingLimitCents: billing.maxUserMonthlySpendingLimitCents,
522
+ priceLines: Array.isArray(presentation.priceLines)
523
+ ? presentation.priceLines
524
+ : [],
525
+ ...(typeof presentation.headline === 'string' ? { headline: presentation.headline } : {}),
526
+ };
527
+ }
109
528
  /**
110
529
  * Dedicated Runtime commands are unusually likely to meet a server that has
111
530
  * never heard of them.
@@ -140,6 +559,54 @@ function describeMissingEndpoint(err) {
140
559
  'the deployment — `skrr context` names the server it is talking to, and `skrr machines ' +
141
560
  'dedicated list` will still work if the feature is deployed at all.');
142
561
  }
562
+ /**
563
+ * A lease that is not found is usually a SCOPE question, not a typo.
564
+ *
565
+ * Every lease lookup is `(owner, workspace)`: with a workspace selected the
566
+ * server searches only that workspace's leases, and without one only personal
567
+ * leases. So `not found` for an id the caller just created most often means the
568
+ * active workspace differs from the one the machine was created in.
569
+ */
570
+ function describeLeaseNotFound(err) {
571
+ if (err.status !== 404)
572
+ return null;
573
+ try {
574
+ const parsed = JSON.parse(err.body || '');
575
+ if (parsed.code || parsed.error !== 'Machine lease not found')
576
+ return null;
577
+ }
578
+ catch {
579
+ return null;
580
+ }
581
+ return ('Machine lease not found. Leases are looked up within one scope: your personal machines, or ' +
582
+ 'the workspace you name. If you created it with --workspace, pass the same --workspace here; ' +
583
+ '`skrr machines dedicated list` shows what this scope can see.');
584
+ }
585
+ /**
586
+ * The product refusal code the server sent (`DEDICATED_RUNTIME_NOT_ASSIGNABLE`,
587
+ * …), for `this.error(message, { code })`. Without it a `--json` caller got the
588
+ * generic `CLI_ERROR` and could only branch on prose (OSK-8685). Falls back to
589
+ * `HTTP_<status>`, the same code `handleApiError` uses for a body with none.
590
+ */
591
+ function dedicatedRuntimeApiErrorCode(err) {
592
+ if (!(err instanceof api_fetch_1.ApiFetchError)) {
593
+ return undefined;
594
+ }
595
+ const edge = (0, edge_error_page_1.describeEdgeErrorPage)({ status: err.status, body: err.body, method: err.method });
596
+ if (edge) {
597
+ return edge.code;
598
+ }
599
+ try {
600
+ const parsed = JSON.parse(err.body || '');
601
+ if (typeof parsed.code === 'string' && parsed.code) {
602
+ return parsed.code;
603
+ }
604
+ }
605
+ catch {
606
+ // Unparseable body: no product code.
607
+ }
608
+ return err.status ? `HTTP_${err.status}` : undefined;
609
+ }
143
610
  function formatDedicatedRuntimeApiError(err) {
144
611
  if (!(err instanceof api_fetch_1.ApiFetchError) || !err.body) {
145
612
  return null;
@@ -147,6 +614,16 @@ function formatDedicatedRuntimeApiError(err) {
147
614
  if (err.status === 401) {
148
615
  return null;
149
616
  }
617
+ // An HTML page is the edge answering in the API's place — a WAF block or a
618
+ // gateway timeout. Printing its markup told nobody anything (OSK-8700).
619
+ const edge = (0, edge_error_page_1.describeEdgeErrorPage)({ status: err.status, body: err.body, method: err.method });
620
+ if (edge) {
621
+ return edge.message;
622
+ }
623
+ const notFound = describeLeaseNotFound(err);
624
+ if (notFound) {
625
+ return notFound;
626
+ }
150
627
  const missingEndpoint = describeMissingEndpoint(err);
151
628
  if (missingEndpoint) {
152
629
  return missingEndpoint;