@skrr-ai/cli 0.1.28 → 0.1.29

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 (66) hide show
  1. package/dist/base-command.d.ts +15 -0
  2. package/dist/base-command.js +49 -0
  3. package/dist/commands/agents/chat.d.ts +2 -0
  4. package/dist/commands/agents/chat.js +40 -2
  5. package/dist/commands/balance/show.d.ts +2 -3
  6. package/dist/commands/balance/show.js +2 -3
  7. package/dist/commands/balance/usage.d.ts +3 -11
  8. package/dist/commands/balance/usage.js +19 -72
  9. package/dist/commands/code/index.d.ts +4 -0
  10. package/dist/commands/code/index.js +9 -0
  11. package/dist/commands/code/install.d.ts +10 -2
  12. package/dist/commands/code/install.js +38 -28
  13. package/dist/commands/harnesses/leases/show.js +7 -4
  14. package/dist/commands/inbox/index.d.ts +15 -0
  15. package/dist/commands/inbox/index.js +52 -20
  16. package/dist/commands/instructions/install.d.ts +12 -0
  17. package/dist/commands/instructions/install.js +59 -14
  18. package/dist/commands/login.js +6 -0
  19. package/dist/commands/machines/dedicated/attach.js +1 -1
  20. package/dist/commands/machines/dedicated/cp.d.ts +1 -0
  21. package/dist/commands/machines/dedicated/cp.js +63 -9
  22. package/dist/commands/machines/dedicated/create.d.ts +1 -1
  23. package/dist/commands/machines/dedicated/create.js +7 -2
  24. package/dist/commands/machines/dedicated/exec.d.ts +23 -1
  25. package/dist/commands/machines/dedicated/exec.js +67 -7
  26. package/dist/commands/machines/dedicated/index.js +2 -0
  27. package/dist/commands/machines/dedicated/restore.d.ts +6 -0
  28. package/dist/commands/machines/dedicated/restore.js +7 -1
  29. package/dist/commands/machines/dedicated/sign-in.d.ts +4 -3
  30. package/dist/commands/machines/dedicated/sign-in.js +4 -3
  31. package/dist/commands/machines/dedicated/terminal.js +1 -1
  32. package/dist/commands/machines/dedicated/update-image.d.ts +15 -0
  33. package/dist/commands/machines/dedicated/update-image.js +38 -0
  34. package/dist/lib/balance.d.ts +2 -2
  35. package/dist/lib/balance.js +6 -4
  36. package/dist/lib/daemon-target.d.ts +103 -0
  37. package/dist/lib/daemon-target.js +110 -0
  38. package/dist/lib/dedicated-copy.d.ts +92 -2
  39. package/dist/lib/dedicated-copy.js +223 -18
  40. package/dist/lib/dedicated-lease-command.d.ts +7 -1
  41. package/dist/lib/dedicated-lease-command.js +16 -3
  42. package/dist/lib/dedicated-machines.d.ts +130 -8
  43. package/dist/lib/dedicated-machines.js +274 -15
  44. package/dist/lib/dedicated-terminal.d.ts +5 -25
  45. package/dist/lib/dedicated-terminal.js +45 -73
  46. package/dist/lib/dedicated-wait.d.ts +10 -0
  47. package/dist/lib/dedicated-wait.js +52 -0
  48. package/dist/lib/device-code.d.ts +12 -1
  49. package/dist/lib/device-code.js +44 -9
  50. package/dist/lib/harnesses.d.ts +13 -0
  51. package/dist/lib/harnesses.js +24 -0
  52. package/dist/lib/login.js +8 -7
  53. package/dist/lib/sky-code-broker.d.ts +46 -5
  54. package/dist/lib/sky-code-broker.js +96 -26
  55. package/dist/lib/sky-code.d.ts +33 -0
  56. package/dist/lib/sky-code.js +45 -7
  57. package/dist/lib/task-instruction-offer.js +12 -0
  58. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +67 -1
  59. package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +124 -12
  60. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +67 -1
  61. package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +123 -11
  62. package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
  63. package/dist/node_modules/@skrr-ai/data-provider/index.js +3061 -2876
  64. package/dist/node_modules/@skrr-ai/data-provider/package.json +1 -1
  65. package/oclif.manifest.json +2930 -2828
  66. package/package.json +1 -1
@@ -1,39 +1,216 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DedicatedCopyCancelledError = exports.DedicatedCopyError = void 0;
4
+ exports.listenForCopyInterrupt = listenForCopyInterrupt;
5
+ exports.createCopyProgress = createCopyProgress;
6
+ exports.isContinuousIntegration = isContinuousIntegration;
7
+ exports.namesRemoteDirectory = namesRemoteDirectory;
8
+ exports.intoRemoteDirectory = intoRemoteDirectory;
3
9
  exports.uploadToDedicatedRuntime = uploadToDedicatedRuntime;
4
10
  exports.downloadFromDedicatedRuntime = downloadFromDedicatedRuntime;
5
11
  const node_crypto_1 = require("node:crypto");
12
+ const node_fs_1 = require("node:fs");
6
13
  const promises_1 = require("node:fs/promises");
7
14
  const node_path_1 = require("node:path");
8
15
  const dedicated_machines_1 = require("./dedicated-machines");
16
+ /**
17
+ * A refusal the CLI makes itself, with the code the server uses for the same
18
+ * cause. A file over the limit, a file that changed and a file that is missing
19
+ * each read one way and carry one code whichever side found it; the server's
20
+ * sentences are in `DedicatedRuntimeMachineAccess.mapFileTransferError`
21
+ * (OSK-8714). Before, these local refusals had no code at all.
22
+ */
23
+ class DedicatedCopyError extends Error {
24
+ code;
25
+ constructor(code, message) {
26
+ super(message);
27
+ this.code = code;
28
+ this.name = 'DedicatedCopyError';
29
+ }
30
+ }
31
+ exports.DedicatedCopyError = DedicatedCopyError;
32
+ const changedWhileCopying = (path, where) => new DedicatedCopyError('DEDICATED_RUNTIME_FILE_CHANGED', `${path} changed ${where} while it was being copied, so nothing was written. Copy it again once it has stopped changing.`);
9
33
  const defaultIo = {
10
34
  readChunk: dedicated_machines_1.readDedicatedRuntimeFileChunk,
11
35
  writeChunk: dedicated_machines_1.writeDedicatedRuntimeFileChunk,
12
36
  chunkBytes: dedicated_machines_1.DEDICATED_FILE_CHUNK_BYTES,
13
37
  maxBytes: dedicated_machines_1.DEDICATED_FILE_MAX_BYTES,
38
+ onProcessExit: (fn) => {
39
+ process.on('exit', fn);
40
+ return () => process.removeListener('exit', fn);
41
+ },
14
42
  };
43
+ // SIGHUP too: a closed terminal cancels a copy like Ctrl-C. With no listener of
44
+ // the command's own for it, auth-core's handler lets the process die by the
45
+ // signal, and a download's exit handler never runs to remove its temporary file.
46
+ const SIGNAL_NUMBERS = { SIGHUP: 1, SIGINT: 2, SIGTERM: 15 };
47
+ /** A copy stopped by SIGINT, SIGTERM or SIGHUP. Exits 128 + the signal's number, as a shell would. */
48
+ class DedicatedCopyCancelledError extends Error {
49
+ signalName;
50
+ code = 'DEDICATED_RUNTIME_COPY_CANCELLED';
51
+ exitCode;
52
+ constructor(signalName) {
53
+ super(`Copy cancelled by ${signalName}.`);
54
+ this.signalName = signalName;
55
+ this.name = 'DedicatedCopyCancelledError';
56
+ this.exitCode = 128 + SIGNAL_NUMBERS[signalName];
57
+ }
58
+ }
59
+ exports.DedicatedCopyCancelledError = DedicatedCopyCancelledError;
60
+ function throwIfCancelled(signal) {
61
+ if (signal?.aborted) {
62
+ throw signal.reason instanceof Error ? signal.reason : new Error('Copy cancelled');
63
+ }
64
+ }
65
+ /**
66
+ * Listen for SIGINT, SIGTERM and SIGHUP for the length of one copy.
67
+ *
68
+ * An ordinary command-level listener, nothing more. The lock-release handler
69
+ * every command inherits defers to it (`installLockExitHandlers`,
70
+ * packages/auth-core/src/refresh.ts): the first signal reaches this listener,
71
+ * which aborts the copy so the loop removes its partial and rejects; a second
72
+ * signal, or a cleanup that hangs past auth-core's fallback, exits the process,
73
+ * and a download's exit handler still removes its temporary file then.
74
+ */
75
+ function listenForCopyInterrupt({ proc = process, notice = (message) => process.stderr.write(`${message}\n`), } = {}) {
76
+ const controller = new AbortController();
77
+ let interruptedBy = null;
78
+ const listeners = Object.keys(SIGNAL_NUMBERS).map((name) => {
79
+ const listener = () => {
80
+ if (interruptedBy)
81
+ return;
82
+ interruptedBy = name;
83
+ // After SIGHUP the terminal that would show the notice is gone.
84
+ if (name !== 'SIGHUP') {
85
+ notice(`\nCancelling the copy and removing its partial file… (${name === 'SIGINT' ? 'Ctrl-C' : name} again to stop now)`);
86
+ }
87
+ controller.abort(new DedicatedCopyCancelledError(name));
88
+ };
89
+ proc.on(name, listener);
90
+ return [name, listener];
91
+ });
92
+ return {
93
+ signal: controller.signal,
94
+ interruptedBy: () => interruptedBy,
95
+ dispose: () => {
96
+ for (const [name, listener] of listeners)
97
+ proc.removeListener(name, listener);
98
+ },
99
+ };
100
+ }
15
101
  function mib(bytes) {
16
102
  return `${Math.round((bytes / (1024 * 1024)) * 10) / 10} MiB`;
17
103
  }
104
+ /**
105
+ * One line of progress for a terminal, redrawn in place: a 60 MiB copy takes
106
+ * about forty seconds, and printed nothing until it ended (OSK-8714). The
107
+ * caller decides whether there is a terminal to draw on.
108
+ */
109
+ function createCopyProgress({ label, write, now = Date.now, }) {
110
+ const startedAt = now();
111
+ let drawn = false;
112
+ return {
113
+ update(copied, total) {
114
+ const seconds = Math.max(now() - startedAt, 1) / 1000;
115
+ const percent = total > 0 ? Math.floor((copied / total) * 100) : 100;
116
+ write(`\r\x1b[2K${label} ${mib(copied)} of ${mib(total)} (${percent}%), ${mib(copied / seconds)}/s`);
117
+ drawn = true;
118
+ },
119
+ clear() {
120
+ if (drawn)
121
+ write('\r\x1b[2K');
122
+ drawn = false;
123
+ },
124
+ };
125
+ }
126
+ /** Whether a CI system is running this. Progress is for a person watching. */
127
+ function isContinuousIntegration(env = process.env) {
128
+ const ci = env.CI?.trim().toLowerCase();
129
+ return Boolean(ci) && ci !== '0' && ci !== 'false';
130
+ }
131
+ /** `dir/`, `dir/.` or `.`: a machine path that names a directory to copy into. */
132
+ function namesRemoteDirectory(remotePath) {
133
+ return /(^|\/)\.?$/.test(remotePath);
134
+ }
135
+ /** `name` inside the machine directory `directory`, however it was spelled. */
136
+ function intoRemoteDirectory(directory, name) {
137
+ const parent = directory.replace(/\/\.?$/, '').replace(/^\.$/, '');
138
+ return parent ? `${parent}/${name}` : name;
139
+ }
140
+ const PATH_IS_DIRECTORY = 'DEDICATED_RUNTIME_PATH_IS_DIRECTORY';
141
+ /** The machine refused the destination as a directory before anything was staged. */
142
+ class DestinationIsDirectory extends Error {
143
+ refusal;
144
+ constructor(refusal) {
145
+ super('The destination on the machine is a directory');
146
+ this.refusal = refusal;
147
+ }
148
+ }
149
+ /**
150
+ * Upload one file. A destination that is a directory — spelled `dir/`, or an
151
+ * existing directory named without the slash — receives the file under its own
152
+ * name, as `cp` and `scp` do and as a download into a local directory already
153
+ * did. Before, `cp tiny.txt <lease>:repos/scout-a/` sent the whole file and then
154
+ * failed the machine's final rename with EISDIR (OSK-8713).
155
+ *
156
+ * `dir/` is resolved here, without asking. A directory named without the slash
157
+ * is found out by the machine, which refuses it on the first chunk before
158
+ * anything is staged; the copy then goes into it, once.
159
+ */
18
160
  async function uploadToDedicatedRuntime(localPath, leaseId, remotePath, scope = {}, io = {}) {
19
- const { writeChunk, chunkBytes, maxBytes } = { ...defaultIo, ...io };
20
- const info = await (0, promises_1.stat)(localPath);
161
+ const intoDirectory = (directory) => intoRemoteDirectory(directory, (0, node_path_1.basename)(localPath));
162
+ const attempt = async (target) => {
163
+ try {
164
+ return await uploadFile(localPath, leaseId, target, scope, io);
165
+ }
166
+ catch (err) {
167
+ throw err instanceof DestinationIsDirectory ? err.refusal : err;
168
+ }
169
+ };
170
+ if (namesRemoteDirectory(remotePath)) {
171
+ return attempt(intoDirectory(remotePath));
172
+ }
173
+ try {
174
+ return await uploadFile(localPath, leaseId, remotePath, scope, io);
175
+ }
176
+ catch (err) {
177
+ if (err instanceof DestinationIsDirectory)
178
+ return attempt(intoDirectory(remotePath));
179
+ throw err;
180
+ }
181
+ }
182
+ async function uploadFile(localPath, leaseId, remotePath, scope, io) {
183
+ const { writeChunk, chunkBytes, maxBytes, signal, onProgress } = { ...defaultIo, ...io };
184
+ throwIfCancelled(signal);
185
+ const info = await (0, promises_1.stat)(localPath).catch((err) => {
186
+ // The raw `ENOENT: no such file or directory, stat './x'` said the same
187
+ // thing in the platform's words rather than ours.
188
+ if (err?.code === 'ENOENT' || err?.code === 'ENOTDIR') {
189
+ throw new DedicatedCopyError('DEDICATED_RUNTIME_FILE_NOT_FOUND', `${localPath} does not exist here.`);
190
+ }
191
+ throw err;
192
+ });
21
193
  if (!info.isFile()) {
22
- throw new Error(`${localPath} is not a regular file`);
194
+ throw new DedicatedCopyError('DEDICATED_RUNTIME_PATH_NOT_A_FILE', `${localPath} is not a regular file; cp copies one file.`);
23
195
  }
24
196
  if (info.size > maxBytes) {
25
- throw new Error(`${localPath} is ${mib(info.size)}; copy is limited to ${mib(maxBytes)}. Push it through Git or object storage instead.`);
197
+ throw new DedicatedCopyError('DEDICATED_RUNTIME_FILE_TOO_LARGE', `${localPath} is ${mib(info.size)}; copy is limited to ${mib(maxBytes)}. Push it through Git or object storage instead.`);
26
198
  }
27
199
  const transferId = `cp-${Date.now().toString(36)}-${(0, node_crypto_1.randomBytes)(6).toString('hex')}`;
28
200
  const handle = await (0, promises_1.open)(localPath, 'r');
201
+ let offset = 0;
29
202
  try {
30
203
  const buffer = Buffer.alloc(chunkBytes);
31
- let offset = 0;
32
204
  for (;;) {
205
+ // Checked between chunks, never by abandoning one in flight: the machine
206
+ // still applies a chunk whose request was abandoned, and while it does,
207
+ // the abort below is refused as a write to a file another request holds.
208
+ // Waiting out one chunk is what lets the abort remove the staged file.
209
+ throwIfCancelled(signal);
33
210
  const { bytesRead } = await handle.read(buffer, 0, buffer.byteLength, offset);
34
211
  const final = offset + bytesRead >= info.size;
35
212
  if (!final && bytesRead === 0) {
36
- throw new Error(`${localPath} shrank while it was being copied`);
213
+ throw changedWhileCopying(localPath, 'here');
37
214
  }
38
215
  const result = await writeChunk(leaseId, {
39
216
  path: remotePath,
@@ -45,14 +222,22 @@ async function uploadToDedicatedRuntime(localPath, leaseId, remotePath, scope =
45
222
  ...(final ? { final: true, totalSize: info.size } : {}),
46
223
  }, scope);
47
224
  offset += bytesRead;
225
+ onProgress?.(offset, info.size);
48
226
  if (final) {
49
227
  return { bytes: offset, destination: `${leaseId}:${result.path || remotePath}` };
50
228
  }
51
229
  }
52
230
  }
53
231
  catch (err) {
232
+ if (offset === 0 && err?.code === PATH_IS_DIRECTORY) {
233
+ // Refused before the first chunk was staged: there is nothing to abort.
234
+ throw new DestinationIsDirectory(err);
235
+ }
54
236
  // Best effort: remove the staged partial so it does not sit beside the
55
- // destination. The destination itself was never touched.
237
+ // destination. The destination itself was never touched. An abort that
238
+ // never arrives — the process killed, the network gone — is covered by the
239
+ // machine, which removes a stale staging file on the next upload into the
240
+ // same directory (daemon/src/tools/file-transfer.ts).
56
241
  await writeChunk(leaseId, { path: remotePath, transferId, offset: 0, abort: true }, scope).catch(() => undefined);
57
242
  throw err;
58
243
  }
@@ -61,44 +246,61 @@ async function uploadToDedicatedRuntime(localPath, leaseId, remotePath, scope =
61
246
  }
62
247
  }
63
248
  async function downloadFromDedicatedRuntime(leaseId, remotePath, localPath, scope = {}, io = {}) {
64
- const { readChunk, chunkBytes } = { ...defaultIo, ...io };
249
+ const { readChunk, chunkBytes, onProcessExit, signal, onProgress } = { ...defaultIo, ...io };
250
+ throwIfCancelled(signal);
65
251
  let target = localPath;
66
- try {
67
- if ((await (0, promises_1.stat)(localPath)).isDirectory()) {
68
- target = (0, node_path_1.join)(localPath, (0, node_path_1.basename)(remotePath));
69
- }
252
+ if (localPath.endsWith('/') || localPath.endsWith(node_path_1.sep)) {
253
+ // Spelled as a directory: into it, made if missing. It used to become a
254
+ // FILE named after the directory when the directory did not exist yet.
255
+ target = (0, node_path_1.join)(localPath, (0, node_path_1.basename)(remotePath));
70
256
  }
71
- catch {
72
- // Does not exist yet: it names the file.
257
+ else {
258
+ try {
259
+ if ((await (0, promises_1.stat)(localPath)).isDirectory()) {
260
+ target = (0, node_path_1.join)(localPath, (0, node_path_1.basename)(remotePath));
261
+ }
262
+ }
263
+ catch {
264
+ // Does not exist yet: it names the file.
265
+ }
73
266
  }
74
267
  await (0, promises_1.mkdir)((0, node_path_1.dirname)(target), { recursive: true });
75
268
  const temporary = (0, node_path_1.join)((0, node_path_1.dirname)(target), `.${(0, node_path_1.basename)(target)}.skrr-download-${(0, node_crypto_1.randomBytes)(6).toString('hex')}`);
76
269
  const handle = await (0, promises_1.open)(temporary, 'wx');
270
+ // For an exit nothing waits for — a second Ctrl-C, auth-core's fallback, a
271
+ // caller that passed no signal. Synchronous, because an exit handler cannot
272
+ // await; harmless after the rename, when the path is gone.
273
+ const forgetExitCleanup = onProcessExit(() => (0, node_fs_1.rmSync)(temporary, { force: true }));
77
274
  let closed = false;
78
275
  let offset = 0;
79
276
  let size = -1;
80
277
  let mtimeMs = -1;
81
278
  try {
82
279
  for (;;) {
83
- const chunk = await readChunk(leaseId, { path: remotePath, offset, length: chunkBytes }, scope);
280
+ throwIfCancelled(signal);
281
+ // Nothing on the machine needs undoing, so an in-flight read is abandoned
282
+ // at once rather than waited out.
283
+ const chunk = await readChunk(leaseId, { path: remotePath, offset, length: chunkBytes }, scope, signal);
84
284
  if (size === -1) {
85
285
  size = chunk.size;
86
286
  mtimeMs = chunk.mtimeMs;
87
287
  }
88
288
  else if (chunk.size !== size || chunk.mtimeMs !== mtimeMs) {
89
289
  // Chunks from two versions of a file splice into one that never existed.
90
- throw new Error(`${remotePath} changed on the machine while it was being copied`);
290
+ throw changedWhileCopying(remotePath, 'on the machine');
91
291
  }
92
292
  const bytes = Buffer.from(chunk.dataBase64, 'base64');
93
293
  if (bytes.byteLength !== chunk.bytesRead) {
94
- throw new Error('A chunk arrived damaged; nothing was written to the destination');
294
+ throw new DedicatedCopyError('DEDICATED_RUNTIME_FILE_TRANSFER_FAILED', 'A chunk arrived damaged; nothing was written to the destination.');
95
295
  }
96
296
  await handle.write(bytes, 0, bytes.byteLength, offset);
97
297
  offset += bytes.byteLength;
298
+ onProgress?.(offset, size);
98
299
  if (chunk.eof)
99
300
  break;
100
301
  if (bytes.byteLength === 0) {
101
- throw new Error(`${remotePath} ended before its reported size`);
302
+ // It was shorter than it said when the first chunk was read.
303
+ throw changedWhileCopying(remotePath, 'on the machine');
102
304
  }
103
305
  }
104
306
  await handle.close();
@@ -112,4 +314,7 @@ async function downloadFromDedicatedRuntime(leaseId, remotePath, localPath, scop
112
314
  await (0, promises_1.unlink)(temporary).catch(() => undefined);
113
315
  throw err;
114
316
  }
317
+ finally {
318
+ forgetExitCleanup();
319
+ }
115
320
  }
@@ -43,7 +43,13 @@ export declare abstract class DedicatedLeaseCommand extends BaseCommand {
43
43
  workspace?: string;
44
44
  cwd?: string;
45
45
  }, initialCommand?: string): Promise<void>;
46
- /** Run an API call, turning a refusal into the server's own sentence. */
46
+ /**
47
+ * Run an API call, turning a refusal into the server's own sentence.
48
+ *
49
+ * The refusal's public `details` ride along in the `--json` envelope: a script
50
+ * that meets a failed health check can branch on `details.reasons` rather than
51
+ * parse the sentence, which `this.error` alone had no way to carry (OSK-8687).
52
+ */
47
53
  protected callDedicated<T>(fn: () => Promise<T>): Promise<T>;
48
54
  /**
49
55
  * Print the lease an action returned, or wait for its operation to finish
@@ -112,15 +112,28 @@ class DedicatedLeaseCommand extends base_command_1.BaseCommand {
112
112
  if (code !== 0)
113
113
  this.exit(code);
114
114
  }
115
- /** Run an API call, turning a refusal into the server's own sentence. */
115
+ /**
116
+ * Run an API call, turning a refusal into the server's own sentence.
117
+ *
118
+ * The refusal's public `details` ride along in the `--json` envelope: a script
119
+ * that meets a failed health check can branch on `details.reasons` rather than
120
+ * parse the sentence, which `this.error` alone had no way to carry (OSK-8687).
121
+ */
116
122
  async callDedicated(fn) {
117
123
  try {
118
124
  return await fn();
119
125
  }
120
126
  catch (err) {
121
127
  const message = (0, dedicated_machines_1.formatDedicatedRuntimeApiError)(err);
122
- if (message)
123
- this.error(message, { exit: 1, code: (0, dedicated_machines_1.dedicatedRuntimeApiErrorCode)(err) });
128
+ if (message) {
129
+ this.failWithCliError({
130
+ message,
131
+ code: (0, dedicated_machines_1.dedicatedRuntimeApiErrorCode)(err) ?? 'CLI_ERROR',
132
+ exit: 1,
133
+ retryable: false,
134
+ details: (0, dedicated_machines_1.dedicatedRuntimeApiErrorDetails)(err),
135
+ });
136
+ }
124
137
  this.handleApiError(err);
125
138
  }
126
139
  }
@@ -1,5 +1,5 @@
1
1
  import type { Harness, HarnessLease } from './harnesses';
2
- export type DedicatedLeaseAction = 'start' | 'stop' | 'restart' | 'snapshot' | 'health-check';
2
+ export type DedicatedLeaseAction = 'start' | 'stop' | 'restart' | 'update-image' | 'snapshot' | 'health-check';
3
3
  export declare const DEDICATED_LEASE_ACTIONS: DedicatedLeaseAction[];
4
4
  export declare const DEDICATED_MACHINE_PRODUCT = "dedicated-machine";
5
5
  export interface DedicatedRuntimeScope {
@@ -13,7 +13,16 @@ export interface DedicatedRuntimeScope {
13
13
  * compile while the columns stayed empty. Provider ids (instance, volume, AMI)
14
14
  * are deliberately absent from the payload and therefore from this type.
15
15
  */
16
- export interface DedicatedLease extends Omit<HarnessLease, 'storage'> {
16
+ export interface DedicatedLease extends Omit<HarnessLease, 'storage' | 'health'> {
17
+ /** Canonical public scope; the persisted MachineLease field is `teamId`. */
18
+ workspaceId?: string | null;
19
+ health?: {
20
+ assignable?: boolean;
21
+ reasons?: string[];
22
+ advisories?: string[];
23
+ /** Where the machine's root image stands (D11). Absent when it cannot be judged. */
24
+ image?: DedicatedImageStanding;
25
+ } & Record<string, unknown>;
17
26
  supportReference?: string;
18
27
  lifecycleCapabilities?: Record<string, boolean>;
19
28
  executionCapabilities?: Record<string, boolean>;
@@ -41,10 +50,14 @@ export interface DedicatedLease extends Omit<HarnessLease, 'storage'> {
41
50
  lastRestoreTestAt?: string;
42
51
  lastRestoreTestStatus?: string;
43
52
  unpaidSince?: string;
53
+ /** When the deletion notice REACHED the owner (the clock's start), not when one was recorded. */
44
54
  deletionNoticeAt?: string;
55
+ /** The deletion deadline in force: an archive deadline, or an unpaid one a delivered notice started. */
45
56
  deleteAfter?: string;
46
57
  /** A workspace deletion and its recovery window (D12). */
47
58
  deletion?: DedicatedLeaseDeletion;
59
+ /** Why and when this workspace may be deleted, before it is (D7). */
60
+ deletionPlan?: DedicatedDeletionPlan;
48
61
  } & Record<string, unknown>;
49
62
  recovery?: {
50
63
  incidentHold?: {
@@ -56,19 +69,108 @@ export interface DedicatedLease extends Omit<HarnessLease, 'storage'> {
56
69
  /** Set on a machine created by restoring a deleted one. */
57
70
  restoredFromLeaseId?: string;
58
71
  };
72
+ /**
73
+ * The lease's current (or last) operation, and whether it has finished. A
74
+ * state can be reached before the operation that reached it is done: a create
75
+ * is `ready` the moment its guest registers. Absent from servers that predate
76
+ * it, and for a lease that never had an operation.
77
+ */
78
+ operation?: DedicatedLeaseOperation;
79
+ }
80
+ export interface DedicatedLeaseOperation {
81
+ /** `provision` (create), `start`, `stop`, `restart`, `update-image`, `snapshot`, `grow`, `destroy`; null when the server does not name it. */
82
+ action?: string | null;
83
+ /** `cancelled`: an unpaid purge undone before its destroy was issued. Every status but `in_progress` is settled. */
84
+ status?: 'in_progress' | 'succeeded' | 'failed' | 'cancelled';
85
+ startedAt?: string | null;
86
+ completedAt?: string | null;
87
+ }
88
+ /**
89
+ * A Dedicated Runtime's root image, as the server judges it (D11). The platform
90
+ * owns the image: a machine on an older one moves onto the current image at its
91
+ * next start from stopped, its next restart, or when its owner asks, and must
92
+ * have moved by `updateBy`. A move that does not come back healthy returns the
93
+ * machine to the previous image. Files under the workspace survive both.
94
+ */
95
+ export interface DedicatedImageStanding {
96
+ /** A newer image exists that has not already failed on this machine. */
97
+ updateAvailable?: boolean;
98
+ /** When the older image stops being supported; null when there is no deadline. */
99
+ updateBy?: string | null;
100
+ lastUpdate?: {
101
+ status?: 'applying' | 'succeeded' | 'rolling_back' | 'rolled_back' | 'failed';
102
+ startedAt?: string | null;
103
+ settledAt?: string | null;
104
+ } | null;
59
105
  }
106
+ /** One line saying where a machine's image stands and what the owner can do. */
107
+ export declare function describeDedicatedImageStanding(image: DedicatedImageStanding, leaseId?: string): string;
60
108
  /**
61
109
  * A deleted workspace: restorable into a new machine until `recoverableUntil`,
62
- * then every copy is deleted and `completedAt` records it.
110
+ * then every copy is deleted and `completedAt` records it. `withheld`: an unpaid
111
+ * deletion stopped after its machine was shut down and before its files were
112
+ * deleted — kept in the archived lease, with no window, restorable.
63
113
  */
64
114
  export interface DedicatedLeaseDeletion {
65
- status?: 'recoverable' | 'purging' | 'completed';
115
+ status?: 'recoverable' | 'purging' | 'completed' | 'withheld';
66
116
  requestedAt?: string | null;
67
117
  recoverableUntil?: string | null;
68
118
  restorable?: boolean;
69
119
  restoredToLeaseId?: string | null;
120
+ /** This lease is the previous copy of a workspace restored into that machine. */
121
+ supersededByLeaseId?: string | null;
70
122
  completedAt?: string | null;
71
123
  }
124
+ /**
125
+ * `storage.deletionPlan`: why and when this workspace may be deleted, which
126
+ * notices reached the owner on which channels, and what is holding a deletion
127
+ * back. Brand-free and provider-free, like the rest of the payload.
128
+ */
129
+ /**
130
+ * What clears an unpaid deletion's cause, and so cancels it: paying the runtime's
131
+ * unpaid invoices (their pages only for an owner who pays them), or subscribing
132
+ * again to the plan the machine needs.
133
+ */
134
+ export type DedicatedDeletionRemedy = {
135
+ kind: 'subscribe';
136
+ } | {
137
+ kind: 'pay_invoice';
138
+ paidByOwner: boolean;
139
+ invoices: Array<{
140
+ hostedInvoiceUrl: string;
141
+ amountDue: number | null;
142
+ currency: string | null;
143
+ }>;
144
+ };
145
+ export interface DedicatedDeletionPlan {
146
+ kind: 'unpaid' | 'retention_period';
147
+ status: 'notice_pending' | 'scheduled' | 'deleting' | 'cancelled';
148
+ /** Chosen by the remedy: `payment_failed` an invoice to pay, `plan_inactive` a plan to subscribe to. */
149
+ reason?: 'subscription_canceled' | 'payment_failed' | 'plan_inactive' | 'unknown';
150
+ remedy?: DedicatedDeletionRemedy;
151
+ entitlementEndedAt?: string | null;
152
+ retentionEndsAt?: string | null;
153
+ unpaidSince?: string | null;
154
+ deletionNoticeDeliveredAt?: string | null;
155
+ deleteAfter?: string | null;
156
+ retentionDays?: number | null;
157
+ noticeFloorDays?: number | null;
158
+ notices?: Array<{
159
+ notice: string;
160
+ round?: number;
161
+ deliveredAt?: string | null;
162
+ channels?: string[];
163
+ }>;
164
+ blockedBy?: string;
165
+ blockedSince?: string | null;
166
+ /** While `deleting`: whether restoring billing still cancels it, and from when it may start. */
167
+ cancellable?: boolean;
168
+ deletionStartsAt?: string | null;
169
+ cancelledAt?: string | null;
170
+ cancelReason?: string;
171
+ /** Cancelled after the machine was shut down: the files are kept in the archived lease. */
172
+ workspaceWithheld?: boolean;
173
+ }
72
174
  export interface CreateDedicatedRuntimeInput extends DedicatedRuntimeScope {
73
175
  requestId: string;
74
176
  displayName: string;
@@ -77,7 +179,7 @@ export interface CreateDedicatedRuntimeInput extends DedicatedRuntimeScope {
77
179
  storageGb?: number;
78
180
  retentionClass?: string;
79
181
  backupProfile?: 'crash-consistent' | 'application-consistent';
80
- monthlySpendingLimitCents: number;
182
+ monthlySpendingLimitCents?: number;
81
183
  repositoryRef?: string;
82
184
  }
83
185
  export interface DedicatedRuntimeResponse {
@@ -100,7 +202,7 @@ export declare function dedicatedRuntimeCreateInput(input: {
100
202
  requestId: string;
101
203
  displayName: string;
102
204
  sizePreset: string;
103
- monthlySpendingLimitCents: number;
205
+ monthlySpendingLimitCents?: number;
104
206
  regionClass?: string;
105
207
  storageGb?: number;
106
208
  retentionClass?: string;
@@ -162,7 +264,11 @@ export interface DedicatedRuntimeExecInput {
162
264
  timeoutMs?: number;
163
265
  }
164
266
  export interface DedicatedRuntimeExecResult {
165
- /** stdout and stderr, merged in the order the machine produced them, capped at 1 MB. */
267
+ /**
268
+ * stdout and stderr together, capped at 1 MB. Each stream keeps its order, but
269
+ * they are read as two pipes, so their lines can interleave differently from
270
+ * the order the command wrote them.
271
+ */
166
272
  stdout: string;
167
273
  /** null when the command was killed at its deadline. */
168
274
  exitCode: number | null;
@@ -252,7 +358,7 @@ export declare function readDedicatedRuntimeFileChunk(leaseId: string, input: {
252
358
  path: string;
253
359
  offset: number;
254
360
  length: number;
255
- }, scope?: DedicatedRuntimeScope): Promise<DedicatedFileChunk>;
361
+ }, scope?: DedicatedRuntimeScope, signal?: AbortSignal): Promise<DedicatedFileChunk>;
256
362
  export declare function writeDedicatedRuntimeFileChunk(leaseId: string, input: {
257
363
  path: string;
258
364
  transferId: string;
@@ -298,6 +404,13 @@ export declare function dedicatedSpendSummary(lease: DedicatedLease): string;
298
404
  /** One line saying what a deletion still allows. */
299
405
  export declare function describeDedicatedDeletion(deletion: DedicatedLeaseDeletion, leaseId?: string): string;
300
406
  export declare function describeDedicatedLease(lease: DedicatedLease, harnesses?: readonly Harness[]): string[];
407
+ /** "2026-09-30 14:00 UTC (in 17 days)" — a deadline readable without converting it. */
408
+ export declare function formatDedicatedDeadline(value: string | null | undefined, now?: Date): string;
409
+ /**
410
+ * The deletion plan as lines for `show`: what will happen and when first, then
411
+ * why, which notices reached the owner, and what is holding it back.
412
+ */
413
+ export declare function dedicatedDeletionLines(lease: DedicatedLease, now?: Date): string[];
301
414
  /**
302
415
  * Every harness row a Dedicated Runtime lease's daemon registered, freshest
303
416
  * first.
@@ -387,5 +500,14 @@ export declare function dedicatedCatalogFromProducts(payload: unknown): Dedicate
387
500
  * `HTTP_<status>`, the same code `handleApiError` uses for a body with none.
388
501
  */
389
502
  export declare function dedicatedRuntimeApiErrorCode(err: unknown): string | undefined;
503
+ /**
504
+ * The public `details` object a refusal carried — `state`, `action` and the
505
+ * health `reasons` of a failed health check — for the `--json` envelope.
506
+ *
507
+ * The server builds it from an allow-list of safe values (reason codes, lease
508
+ * states), so it is passed through whole rather than re-filtered here, where a
509
+ * second list would only drift from the first.
510
+ */
511
+ export declare function dedicatedRuntimeApiErrorDetails(err: unknown): Record<string, unknown> | undefined;
390
512
  export declare function formatDedicatedRuntimeApiError(err: unknown): string | null;
391
513
  export {};