@skrr-ai/cli 0.1.25 → 0.1.26

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 (70) hide show
  1. package/dist/base-command.js +4 -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 +75 -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/destroy.d.ts +6 -2
  14. package/dist/commands/machines/dedicated/destroy.js +41 -24
  15. package/dist/commands/machines/dedicated/exec.d.ts +28 -0
  16. package/dist/commands/machines/dedicated/exec.js +105 -0
  17. package/dist/commands/machines/dedicated/grow.d.ts +6 -2
  18. package/dist/commands/machines/dedicated/grow.js +14 -19
  19. package/dist/commands/machines/dedicated/health-check.d.ts +2 -2
  20. package/dist/commands/machines/dedicated/health-check.js +16 -14
  21. package/dist/commands/machines/dedicated/incident-close.d.ts +3 -2
  22. package/dist/commands/machines/dedicated/incident-close.js +11 -14
  23. package/dist/commands/machines/dedicated/index.js +37 -16
  24. package/dist/commands/machines/dedicated/list.d.ts +2 -2
  25. package/dist/commands/machines/dedicated/list.js +28 -17
  26. package/dist/commands/machines/dedicated/restart.d.ts +5 -2
  27. package/dist/commands/machines/dedicated/restart.js +8 -19
  28. package/dist/commands/machines/dedicated/show.d.ts +2 -2
  29. package/dist/commands/machines/dedicated/show.js +28 -30
  30. package/dist/commands/machines/dedicated/sign-in.d.ts +19 -0
  31. package/dist/commands/machines/dedicated/sign-in.js +49 -0
  32. package/dist/commands/machines/dedicated/snapshot.d.ts +5 -2
  33. package/dist/commands/machines/dedicated/snapshot.js +11 -19
  34. package/dist/commands/machines/dedicated/spending-limit.d.ts +22 -0
  35. package/dist/commands/machines/dedicated/spending-limit.js +50 -0
  36. package/dist/commands/machines/dedicated/start.d.ts +5 -2
  37. package/dist/commands/machines/dedicated/start.js +8 -19
  38. package/dist/commands/machines/dedicated/stop.d.ts +5 -2
  39. package/dist/commands/machines/dedicated/stop.js +8 -19
  40. package/dist/commands/machines/dedicated/terminal.d.ts +22 -0
  41. package/dist/commands/machines/dedicated/terminal.js +42 -0
  42. package/dist/commands/tasks/list.js +27 -0
  43. package/dist/lib/daemonBroker.d.ts +8 -0
  44. package/dist/lib/daemonBroker.js +2 -2
  45. package/dist/lib/dedicated-copy.d.ts +22 -0
  46. package/dist/lib/dedicated-copy.js +115 -0
  47. package/dist/lib/dedicated-lease-command.d.ts +50 -0
  48. package/dist/lib/dedicated-lease-command.js +158 -0
  49. package/dist/lib/dedicated-machines.d.ts +248 -12
  50. package/dist/lib/dedicated-machines.js +385 -25
  51. package/dist/lib/dedicated-terminal.d.ts +89 -0
  52. package/dist/lib/dedicated-terminal.js +275 -0
  53. package/dist/lib/dedicated-wait.d.ts +72 -0
  54. package/dist/lib/dedicated-wait.js +195 -0
  55. package/dist/lib/harnesses.d.ts +6 -0
  56. package/dist/lib/node-adapter.js +1 -1
  57. package/dist/lib/sky-code.d.ts +30 -1
  58. package/dist/lib/sky-code.js +44 -2
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.d.ts +1 -1
  60. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/index.js +2 -1
  61. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.d.ts +10 -0
  62. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/spawnEnv.js +17 -1
  63. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.d.ts +1 -1
  64. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/index.js +1 -1
  65. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.d.ts +10 -0
  66. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/spawnEnv.js +16 -0
  67. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  68. package/dist/node_modules/@skrr-ai/data-provider/index.js +2613 -2608
  69. package/oclif.manifest.json +23016 -22295
  70. package/package.json +1 -1
@@ -1,20 +1,33 @@
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_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.setDedicatedRuntimeSpendingLimit = setDedicatedRuntimeSpendingLimit;
10
12
  exports.closeDedicatedRuntimeIncidentHold = closeDedicatedRuntimeIncidentHold;
11
13
  exports.destroyDedicatedRuntime = destroyDedicatedRuntime;
14
+ exports.execDedicatedRuntime = execDedicatedRuntime;
15
+ exports.readDedicatedRuntimeFileChunk = readDedicatedRuntimeFileChunk;
16
+ exports.writeDedicatedRuntimeFileChunk = writeDedicatedRuntimeFileChunk;
17
+ exports.parseDedicatedCopyEndpoints = parseDedicatedCopyEndpoints;
12
18
  exports.dedicatedLeaseId = dedicatedLeaseId;
13
19
  exports.dedicatedLeaseState = dedicatedLeaseState;
14
20
  exports.dedicatedLeaseDisplayName = dedicatedLeaseDisplayName;
21
+ exports.formatUsdCents = formatUsdCents;
22
+ exports.dedicatedSpendSummary = dedicatedSpendSummary;
23
+ exports.describeDedicatedLease = describeDedicatedLease;
24
+ exports.dedicatedLeaseHarnesses = dedicatedLeaseHarnesses;
25
+ exports.dedicatedTerminalHarness = dedicatedTerminalHarness;
26
+ exports.selectDedicatedAttachHarness = selectDedicatedAttachHarness;
27
+ exports.dedicatedCatalogFromProducts = dedicatedCatalogFromProducts;
28
+ exports.dedicatedRuntimeApiErrorCode = dedicatedRuntimeApiErrorCode;
15
29
  exports.formatDedicatedRuntimeApiError = formatDedicatedRuntimeApiError;
16
30
  const api_fetch_1 = require("./api-fetch");
17
- const harnesses_1 = require("./harnesses");
18
31
  exports.DEDICATED_LEASE_ACTIONS = [
19
32
  'start',
20
33
  'stop',
@@ -22,6 +35,7 @@ exports.DEDICATED_LEASE_ACTIONS = [
22
35
  'snapshot',
23
36
  'health-check',
24
37
  ];
38
+ exports.DEDICATED_MACHINE_PRODUCT = 'dedicated-machine';
25
39
  /**
26
40
  * Flags in, request body out — and the point of it being a function is the
27
41
  * OMISSION.
@@ -36,6 +50,7 @@ exports.DEDICATED_LEASE_ACTIONS = [
36
50
  * means the same bug was invisible in them.
37
51
  */
38
52
  function dedicatedRuntimeCreateInput(input) {
53
+ const repositoryRef = input.repositoryRef?.trim();
39
54
  return {
40
55
  requestId: input.requestId,
41
56
  displayName: input.displayName,
@@ -47,45 +62,74 @@ function dedicatedRuntimeCreateInput(input) {
47
62
  ...(input.backupProfile
48
63
  ? { backupProfile: input.backupProfile }
49
64
  : {}),
65
+ ...(repositoryRef ? { repositoryRef } : {}),
50
66
  ...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
51
67
  };
52
68
  }
69
+ /**
70
+ * The shapes the server's repository parser accepts: `owner/repo`,
71
+ * `github.com/owner/repo`, or an https URL to it, optionally ending in `.git`.
72
+ *
73
+ * This is a typo catcher, not the authority. The server re-parses the value and
74
+ * decides whether the owner's GitHub App installation covers it; checking the
75
+ * shape here only turns "create failed" into "that is not a repository" before a
76
+ * billable request is sent.
77
+ */
78
+ function isDedicatedRepositoryRef(value) {
79
+ return /^(?:https?:\/\/)?(?:github\.com\/)?[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?(?:\.git)?$/i.test(value.trim());
80
+ }
81
+ // Takes the finished path rather than composing one from a suffix, so the route
82
+ // each call reaches is a literal at the call site. `cli/src/__tests__/helpers/
83
+ // api-surface.ts` reads coverage out of the source, and a path assembled inside a
84
+ // builder is invisible to it: exec, files/read and files/write were all reported
85
+ // unreachable by domain-api-parity while being perfectly reachable, which is the
86
+ // reading that trains people to allowlist things that are fine. Same shape as
87
+ // `withQuery` on the `request.*` side.
88
+ function withDedicatedScope(path, scope) {
89
+ if (!scope.workspaceId)
90
+ return path;
91
+ const separator = path.includes('?') ? '&' : '?';
92
+ return `${path}${separator}workspaceId=${encodeURIComponent(scope.workspaceId)}`;
93
+ }
53
94
  async function createDedicatedRuntime(input) {
54
95
  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
- });
96
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope('/api/machines/dedicated', { workspaceId }), { method: 'POST', body, headers: { 'Idempotency-Key': requestId } });
67
97
  }
68
98
  async function listDedicatedRuntimes(options = {}) {
69
- return (0, harnesses_1.listHarnessLeases)({
70
- ...options,
71
- machineProduct: 'dedicated-machine',
72
- });
99
+ const query = new URLSearchParams();
100
+ if (options.limit)
101
+ query.set('limit', String(options.limit));
102
+ if (options.state)
103
+ query.set('state', options.state);
104
+ query.set('machineProduct', exports.DEDICATED_MACHINE_PRODUCT);
105
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/leases?${query.toString()}`, options));
73
106
  }
74
- async function getDedicatedRuntime(leaseId) {
75
- return (0, harnesses_1.getHarnessLease)(leaseId);
107
+ async function getDedicatedRuntime(leaseId, scope = {}) {
108
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/leases/${encodeURIComponent(leaseId)}`, scope));
76
109
  }
77
- async function performDedicatedLeaseAction(leaseId, action) {
78
- return (0, harnesses_1.performHarnessLeaseAction)(leaseId, action);
110
+ async function performDedicatedLeaseAction(leaseId, action, scope = {}) {
111
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/leases/${encodeURIComponent(leaseId)}/actions/${encodeURIComponent(action)}`, scope), { method: 'POST' });
79
112
  }
80
113
  async function growDedicatedRuntime(leaseId, storageGb, options = {}) {
81
- return (0, api_fetch_1.apiFetch)(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/storage`, {
114
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/storage`, options), {
82
115
  method: 'POST',
83
116
  body: { storageGb },
84
117
  ...(options.requestId ? { headers: { 'Idempotency-Key': options.requestId } } : {}),
85
118
  });
86
119
  }
120
+ /**
121
+ * Change the monthly spending cap of one lease.
122
+ *
123
+ * The route accepts `spendingLimitCents` and nothing else: who pays is decided
124
+ * at create and there is no API that moves it. A cap below what the month has
125
+ * already spent (or is projected to) is refused rather than stopping the machine
126
+ * on the next reconciler pass.
127
+ */
128
+ async function setDedicatedRuntimeSpendingLimit(leaseId, spendingLimitCents, scope = {}) {
129
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/leases/${encodeURIComponent(leaseId)}/billing`, scope), { method: 'PATCH', body: { spendingLimitCents } });
130
+ }
87
131
  async function closeDedicatedRuntimeIncidentHold(leaseId, options = {}) {
88
- return (0, api_fetch_1.apiFetch)(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/incident-hold/close`, {
132
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/incident-hold/close`, options), {
89
133
  method: 'POST',
90
134
  body: {},
91
135
  ...(options.requestId ? { headers: { 'Idempotency-Key': options.requestId } } : {}),
@@ -95,7 +139,57 @@ async function destroyDedicatedRuntime(leaseId, options = {}) {
95
139
  const query = options.retentionIntent
96
140
  ? `?retentionIntent=${encodeURIComponent(options.retentionIntent)}`
97
141
  : '';
98
- return (0, api_fetch_1.apiFetch)(`/api/machines/leases/${encodeURIComponent(leaseId)}${query}`, { method: 'DELETE' });
142
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/leases/${encodeURIComponent(leaseId)}${query}`, options), { method: 'DELETE' });
143
+ }
144
+ /** The server's ceiling for one exec; documented beside the route. */
145
+ exports.DEDICATED_EXEC_MAX_TIMEOUT_MS = 10 * 60 * 1000;
146
+ /**
147
+ * Run one command on the machine as the workload user.
148
+ *
149
+ * Not idempotent and not retried: `apiFetch` gives each call a fresh
150
+ * idempotency key, which is the rule for an unfenced write — running the same
151
+ * command twice is two intents even when the text is identical.
152
+ */
153
+ async function execDedicatedRuntime(leaseId, input, scope = {}) {
154
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/exec`, scope), {
155
+ method: 'POST',
156
+ body: {
157
+ command: input.command,
158
+ ...(input.cwd ? { cwd: input.cwd } : {}),
159
+ ...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
160
+ },
161
+ });
162
+ }
163
+ /** Mirrors the daemon's bounds (daemon/src/tools/file-transfer.ts). */
164
+ exports.DEDICATED_FILE_CHUNK_BYTES = 512 * 1024;
165
+ exports.DEDICATED_FILE_MAX_BYTES = 64 * 1024 * 1024;
166
+ async function readDedicatedRuntimeFileChunk(leaseId, input, scope = {}) {
167
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/files/read`, scope), { method: 'POST', body: input });
168
+ }
169
+ async function writeDedicatedRuntimeFileChunk(leaseId, input, scope = {}) {
170
+ return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/files/write`, scope), { method: 'POST', body: input });
171
+ }
172
+ const REMOTE_ENDPOINT = /^(rtl_[A-Za-z0-9_-]{1,80}):(.+)$/;
173
+ /**
174
+ * `cp <src> <dst>` where exactly one side is `<lease-id>:<path>`.
175
+ *
176
+ * The remote side is recognised by the lease id prefix rather than by "has a
177
+ * colon", so a Windows path (`C:\\build\\out.zip`) or a local file whose name
178
+ * contains a colon is never mistaken for a machine.
179
+ */
180
+ function parseDedicatedCopyEndpoints(source, destination) {
181
+ const src = REMOTE_ENDPOINT.exec(source);
182
+ const dst = REMOTE_ENDPOINT.exec(destination);
183
+ if (src && dst) {
184
+ throw new Error('Copy between two machines is not supported; one side must be a local path.');
185
+ }
186
+ if (!src && !dst) {
187
+ throw new Error('One side must name the machine, as <lease-id>:<path>.');
188
+ }
189
+ if (src) {
190
+ return { direction: 'download', leaseId: src[1], remotePath: src[2], localPath: destination };
191
+ }
192
+ return { direction: 'upload', localPath: source, leaseId: dst[1], remotePath: dst[2] };
99
193
  }
100
194
  function dedicatedLeaseId(lease) {
101
195
  return String(lease?.id ?? '');
@@ -106,6 +200,224 @@ function dedicatedLeaseState(lease) {
106
200
  function dedicatedLeaseDisplayName(lease) {
107
201
  return lease?.displayName ? String(lease.displayName) : '-';
108
202
  }
203
+ /** `$12.34` — cents are what the API speaks; dollars are what a person reads. */
204
+ function formatUsdCents(cents) {
205
+ if (typeof cents !== 'number' || !Number.isFinite(cents))
206
+ return '-';
207
+ const sign = cents < 0 ? '-' : '';
208
+ return `${sign}$${(Math.abs(cents) / 100).toFixed(2)}`;
209
+ }
210
+ /** `this month $1.23 of $500.00 cap`, or why that sentence cannot be written. */
211
+ function dedicatedSpendSummary(lease) {
212
+ const billing = lease.billing;
213
+ if (!billing)
214
+ return '-';
215
+ const cap = billing.monthlySpendLimitCents ?? billing.spendingLimitCents;
216
+ if (billing.spendStatusUnavailable) {
217
+ return `spend unavailable right now · cap ${formatUsdCents(cap)}`;
218
+ }
219
+ const spent = billing.currentMonthSpendCents;
220
+ const projected = billing.projectedCurrentMonthSpendCents;
221
+ const parts = [`${formatUsdCents(spent)} this month of ${formatUsdCents(cap)} cap`];
222
+ if (typeof projected === 'number' && projected !== spent) {
223
+ parts.push(`projected ${formatUsdCents(projected)}`);
224
+ }
225
+ if (billing.spendingLimitExceeded) {
226
+ parts.push('OVER CAP — compute is stopped, files are kept');
227
+ }
228
+ return parts.join(' · ');
229
+ }
230
+ /**
231
+ * The human rendering of `machines dedicated show`, as lines.
232
+ *
233
+ * Pure so it can be pinned without a server. Every field is optional because a
234
+ * lease that is still `requested` has no resources, daemon or spend yet, and a
235
+ * missing value is omitted rather than printed as a column of dashes.
236
+ */
237
+ function describeDedicatedLease(lease, harnesses = []) {
238
+ const lines = [dedicatedLeaseDisplayName(lease)];
239
+ const row = (label, value) => {
240
+ if (value === undefined || value === null || value === '')
241
+ return;
242
+ lines.push(` ${label.padEnd(13)}${value}`);
243
+ };
244
+ row('id', dedicatedLeaseId(lease));
245
+ row('state', lease.stateReason
246
+ ? `${dedicatedLeaseState(lease)} (${lease.stateReason})`
247
+ : dedicatedLeaseState(lease));
248
+ const resources = lease.resources || {};
249
+ const size = [
250
+ resources.sizePreset,
251
+ resources.cpu ? `${resources.cpu} vCPU` : '',
252
+ resources.memoryGb ? `${resources.memoryGb} GiB RAM` : '',
253
+ resources.regionClass,
254
+ ]
255
+ .filter(Boolean)
256
+ .join(' · ');
257
+ row('size', size);
258
+ const storage = lease.storage || {};
259
+ const disk = [
260
+ resources.storageGb ? `${resources.storageGb} GiB` : '',
261
+ storage.retentionClass ? `retention ${storage.retentionClass}` : '',
262
+ storage.backupProfile ? `backup ${storage.backupProfile}` : '',
263
+ ]
264
+ .filter(Boolean)
265
+ .join(' · ');
266
+ row('workspace', disk);
267
+ row('last backup', storage.lastBackupAt);
268
+ const image = [resources.imageVersion || lease.daemon?.imageVersion].filter(Boolean).join('');
269
+ row('image', image);
270
+ if (lease.daemon?.version || lease.daemon?.lastHeartbeatAt) {
271
+ row('daemon', [
272
+ lease.daemon.version ? `skrrd ${lease.daemon.version}` : '',
273
+ lease.daemon.lastHeartbeatAt ? `heartbeat ${lease.daemon.lastHeartbeatAt}` : '',
274
+ ]
275
+ .filter(Boolean)
276
+ .join(' · '));
277
+ }
278
+ const health = lease.health;
279
+ if (health && typeof health.assignable === 'boolean') {
280
+ row('health', health.assignable
281
+ ? 'can take work'
282
+ : `not assignable${health.reasons?.length ? `: ${[...new Set(health.reasons)].join(', ')}` : ''}`);
283
+ }
284
+ row('spend', lease.billing ? dedicatedSpendSummary(lease) : undefined);
285
+ if (storage.deleteAfter || storage.unpaidSince) {
286
+ row('deletion', [
287
+ storage.unpaidSince ? `unpaid since ${storage.unpaidSince}` : '',
288
+ storage.deletionNoticeAt ? `notice ${storage.deletionNoticeAt}` : '',
289
+ storage.deleteAfter ? `workspace deleted after ${storage.deleteAfter}` : '',
290
+ ]
291
+ .filter(Boolean)
292
+ .join(' · '));
293
+ }
294
+ const hold = lease.recovery?.incidentHold;
295
+ if (hold?.status === 'open') {
296
+ row('incident', `hold open since ${hold.heldAt || '?'} — the failed source volume is billed until \`incident-close\``);
297
+ }
298
+ const engines = dedicatedLeaseHarnesses(lease, harnesses);
299
+ if (engines.length > 0) {
300
+ row('engines', engines.map((harness) => `${harness.provider} (${harness.status || 'unknown'})`).join(', '));
301
+ }
302
+ // What the PRODUCT supports, not what this state allows: an archived lease
303
+ // still lists `start`. Labelled so nobody scripts against it as the latter.
304
+ const capabilities = Object.entries(lease.lifecycleCapabilities || {})
305
+ .filter(([, allowed]) => allowed)
306
+ .map(([name]) => name);
307
+ row('supports', capabilities.join(', '));
308
+ row('support ref', lease.supportReference);
309
+ return lines;
310
+ }
311
+ /**
312
+ * Every harness row a Dedicated Runtime lease's daemon registered, freshest
313
+ * first.
314
+ *
315
+ * A lease payload names no daemon (provider and daemon ids are deliberately
316
+ * absent), so the join goes the other way: each row a guest daemon registers
317
+ * carries `metadata.machineLeaseId` and `metadata.machineProduct`. One daemon
318
+ * writes one row PER ENGINE it announces (claude, codex, sky-code, or `oversky`
319
+ * when it announces none), all sharing the daemon id.
320
+ */
321
+ function dedicatedLeaseHarnesses(lease, harnesses) {
322
+ const seenAt = (harness) => Date.parse(harness.lastSeenAt || '') || 0;
323
+ return harnesses
324
+ .filter((harness) => {
325
+ const metadata = harness.metadata || {};
326
+ return (metadata.machineProduct === exports.DEDICATED_MACHINE_PRODUCT &&
327
+ metadata.machineLeaseId === lease.id);
328
+ })
329
+ .sort((a, b) => seenAt(b) - seenAt(a));
330
+ }
331
+ /**
332
+ * The row an interactive terminal opens on: online, local, with a daemon.
333
+ *
334
+ * Any engine row reaches the same shell — they share the daemon — so the
335
+ * freshest one wins. The terminal relay's own access check refuses a row that
336
+ * is not local or has no daemon, so offering one here would only move the
337
+ * refusal later.
338
+ */
339
+ function dedicatedTerminalHarness(lease, harnesses) {
340
+ return (dedicatedLeaseHarnesses(lease, harnesses).find((harness) => harness.status === 'online' &&
341
+ Boolean(harness.daemonId) &&
342
+ (harness.harnessLocation === undefined || harness.harnessLocation === 'local')) || null);
343
+ }
344
+ /**
345
+ * The harness row an agent should be attached to, for `attach`.
346
+ *
347
+ * Choosing a row also chooses the ENGINE the agent's turns run under, so an
348
+ * ambiguous choice is refused with the options named rather than guessed. A
349
+ * row need not be online to be attached — a stopped machine keeps its rows —
350
+ * but the error says so when it is not.
351
+ */
352
+ function selectDedicatedAttachHarness(lease, harnesses, engine) {
353
+ const rows = dedicatedLeaseHarnesses(lease, harnesses);
354
+ if (rows.length === 0) {
355
+ return {
356
+ error: `No harness is registered for ${lease.id} yet. A machine's daemon registers one when it ` +
357
+ 'first connects; start the machine, wait for it to be ready, and try again.',
358
+ };
359
+ }
360
+ const providers = [...new Set(rows.map((row) => row.provider))];
361
+ if (engine) {
362
+ const match = rows.find((row) => row.provider === engine);
363
+ if (!match) {
364
+ return {
365
+ error: `${lease.id} has no "${engine}" engine. Registered: ${providers.join(', ')}.`,
366
+ };
367
+ }
368
+ return { harness: match };
369
+ }
370
+ if (providers.length > 1) {
371
+ return {
372
+ error: `${lease.id} runs more than one engine (${providers.join(', ')}); the one you attach is ` +
373
+ 'the one the agent works under. Pass --engine <name>.',
374
+ };
375
+ }
376
+ return { harness: rows[0] };
377
+ }
378
+ /**
379
+ * The Dedicated Runtime row of `GET /api/machines/products`, reduced to what a
380
+ * caller needs to choose `create` flags.
381
+ *
382
+ * `--size-preset` has no client-side list of values on purpose — presets are
383
+ * gated by server flags — so this is where a caller learns which ones exist.
384
+ */
385
+ function dedicatedCatalogFromProducts(payload) {
386
+ const products = Array.isArray(payload)
387
+ ? payload
388
+ : (payload?.products ?? []);
389
+ const product = products.find((row) => row?.machineProduct === exports.DEDICATED_MACHINE_PRODUCT || row?.id === exports.DEDICATED_MACHINE_PRODUCT);
390
+ if (!product)
391
+ return null;
392
+ const configuration = (product.configuration || {});
393
+ const billing = (product.billing || {});
394
+ const presentation = (billing.presentation || {});
395
+ const sizeCatalog = Array.isArray(configuration.sizePresetCatalog)
396
+ ? configuration.sizePresetCatalog
397
+ : (Array.isArray(configuration.sizePresets) ? configuration.sizePresets : []).map((id) => ({ id }));
398
+ const availability = typeof product.availability === 'string'
399
+ ? product.availability
400
+ : String(product.availability?.status || 'unknown');
401
+ return {
402
+ availability,
403
+ ...(typeof product.disabledReason === 'string' && product.disabledReason
404
+ ? { disabledReason: product.disabledReason }
405
+ : {}),
406
+ sizes: sizeCatalog,
407
+ regionClasses: configuration.regionClasses || [],
408
+ ...(configuration.storage
409
+ ? { storage: configuration.storage }
410
+ : {}),
411
+ retentionClasses: configuration.retentionClasses || [],
412
+ backupProfiles: configuration.backupProfiles || [],
413
+ defaultMonthlySpendingLimitCents: billing.defaultMonthlySpendingLimitCents,
414
+ maxUserMonthlySpendingLimitCents: billing.maxUserMonthlySpendingLimitCents,
415
+ priceLines: Array.isArray(presentation.priceLines)
416
+ ? presentation.priceLines
417
+ : [],
418
+ ...(typeof presentation.headline === 'string' ? { headline: presentation.headline } : {}),
419
+ };
420
+ }
109
421
  /**
110
422
  * Dedicated Runtime commands are unusually likely to meet a server that has
111
423
  * never heard of them.
@@ -140,6 +452,50 @@ function describeMissingEndpoint(err) {
140
452
  'the deployment — `skrr context` names the server it is talking to, and `skrr machines ' +
141
453
  'dedicated list` will still work if the feature is deployed at all.');
142
454
  }
455
+ /**
456
+ * A lease that is not found is usually a SCOPE question, not a typo.
457
+ *
458
+ * Every lease lookup is `(owner, workspace)`: with a workspace selected the
459
+ * server searches only that workspace's leases, and without one only personal
460
+ * leases. So `not found` for an id the caller just created most often means the
461
+ * active workspace differs from the one the machine was created in.
462
+ */
463
+ function describeLeaseNotFound(err) {
464
+ if (err.status !== 404)
465
+ return null;
466
+ try {
467
+ const parsed = JSON.parse(err.body || '');
468
+ if (parsed.code || parsed.error !== 'Machine lease not found')
469
+ return null;
470
+ }
471
+ catch {
472
+ return null;
473
+ }
474
+ return ('Machine lease not found. Leases are looked up within one scope: your personal machines, or ' +
475
+ 'the workspace you name. If you created it with --workspace, pass the same --workspace here; ' +
476
+ '`skrr machines dedicated list` shows what this scope can see.');
477
+ }
478
+ /**
479
+ * The product refusal code the server sent (`DEDICATED_RUNTIME_NOT_ASSIGNABLE`,
480
+ * …), for `this.error(message, { code })`. Without it a `--json` caller got the
481
+ * generic `CLI_ERROR` and could only branch on prose (OSK-8685). Falls back to
482
+ * `HTTP_<status>`, the same code `handleApiError` uses for a body with none.
483
+ */
484
+ function dedicatedRuntimeApiErrorCode(err) {
485
+ if (!(err instanceof api_fetch_1.ApiFetchError)) {
486
+ return undefined;
487
+ }
488
+ try {
489
+ const parsed = JSON.parse(err.body || '');
490
+ if (typeof parsed.code === 'string' && parsed.code) {
491
+ return parsed.code;
492
+ }
493
+ }
494
+ catch {
495
+ // Unparseable body: no product code.
496
+ }
497
+ return err.status ? `HTTP_${err.status}` : undefined;
498
+ }
143
499
  function formatDedicatedRuntimeApiError(err) {
144
500
  if (!(err instanceof api_fetch_1.ApiFetchError) || !err.body) {
145
501
  return null;
@@ -147,6 +503,10 @@ function formatDedicatedRuntimeApiError(err) {
147
503
  if (err.status === 401) {
148
504
  return null;
149
505
  }
506
+ const notFound = describeLeaseNotFound(err);
507
+ if (notFound) {
508
+ return notFound;
509
+ }
150
510
  const missingEndpoint = describeMissingEndpoint(err);
151
511
  if (missingEndpoint) {
152
512
  return missingEndpoint;
@@ -0,0 +1,89 @@
1
+ import { io, type Socket } from 'socket.io-client';
2
+ /**
3
+ * A real pseudo-terminal on a Dedicated Runtime, from a local terminal.
4
+ *
5
+ * This is the same lane the web lease card's "Open terminal" drives — the
6
+ * human terminal vocabulary (`HARNESS_TERMINAL_EVENTS`) relayed by `EngineTerminal/handlers.js` to the
7
+ * daemon's `TerminalSessionManager`, which runs an interactive login bash as
8
+ * `skrr-workload` through the PTY relay. Nothing here is a second way in: the
9
+ * relay admits a Dedicated Runtime terminal only when agent dispatch to that
10
+ * lease would be admitted (current generation, running, healthy volume).
11
+ *
12
+ * It is deliberately NOT a loop over `exec`. `exec` is one command with no TTY;
13
+ * a shell built from it would look like a shell and behave like none — no job
14
+ * control, no prompts, no state between lines. This is the honest version.
15
+ */
16
+ /** Ctrl-] — telnet's escape. Ctrl-C and Ctrl-D belong to the remote shell. */
17
+ export declare const TERMINAL_DETACH_BYTE = 29;
18
+ /** How long the machine has to answer a start before the CLI gives up. */
19
+ export declare const TERMINAL_START_TIMEOUT_MS = 30000;
20
+ /**
21
+ * When to type an initial command: once the shell has printed something and
22
+ * then gone quiet, or after a fallback — the same heuristic the web terminal
23
+ * uses for its Sign in menu, because both are waiting on the same login bash.
24
+ */
25
+ export declare const TERMINAL_READY_QUIET_MS = 300;
26
+ export declare const TERMINAL_READY_FALLBACK_MS = 2500;
27
+ export interface TerminalOutputFrame {
28
+ sessionId?: string;
29
+ daemonId?: string;
30
+ type?: 'stdout' | 'stderr' | 'exit' | 'error' | 'info';
31
+ data?: string;
32
+ dataBase64?: string;
33
+ encoding?: 'utf8' | 'base64';
34
+ exitCode?: number | null;
35
+ signal?: string | null;
36
+ }
37
+ export interface TerminalStartAck {
38
+ sessionId?: string;
39
+ daemonId?: string;
40
+ ok?: boolean;
41
+ error?: string;
42
+ code?: string;
43
+ }
44
+ /** Bytes of one output frame, without splitting a UTF-8 or ANSI sequence. */
45
+ export declare function decodeTerminalFrame(frame: TerminalOutputFrame): Buffer;
46
+ /**
47
+ * Types `command` once, when the shell looks ready.
48
+ *
49
+ * Returned as an object with injectable timers so the readiness rule is tested
50
+ * without a PTY. `onOutput` re-arms the quiet timer; the fallback fires
51
+ * regardless, because a shell with a silent prompt must not wait forever.
52
+ */
53
+ export declare function createInitialCommandTyper(options: {
54
+ command: string;
55
+ send: (data: string) => void;
56
+ quietMs?: number;
57
+ fallbackMs?: number;
58
+ setTimer?: (fn: () => void, ms: number) => unknown;
59
+ clearTimer?: (handle: unknown) => void;
60
+ }): {
61
+ start(): void;
62
+ onOutput(): void;
63
+ cancel(): void;
64
+ readonly typed: boolean;
65
+ };
66
+ export declare class DedicatedTerminalError extends Error {
67
+ readonly code: string;
68
+ constructor(message: string, code: string);
69
+ }
70
+ /** A start refusal in words that say what to do, keyed by the relay's codes. */
71
+ export declare function describeTerminalStartRefusal(ack: TerminalStartAck): string;
72
+ export interface DedicatedTerminalOptions {
73
+ baseURL: string;
74
+ token: string;
75
+ runtimeId: string;
76
+ daemonId: string;
77
+ cwd?: string;
78
+ initialCommand?: string;
79
+ input: NodeJS.ReadStream;
80
+ output: NodeJS.WriteStream;
81
+ errorOutput: NodeJS.WritableStream;
82
+ /** Injected for tests. */
83
+ connect?: (url: string, opts: Parameters<typeof io>[1]) => Socket;
84
+ }
85
+ /**
86
+ * Run one interactive session to completion. Resolves with the exit code the
87
+ * CLI should exit with: the shell's own, 0 for a detach, 1 for a lost session.
88
+ */
89
+ export declare function runDedicatedTerminal(options: DedicatedTerminalOptions): Promise<number>;