@skrr-ai/cli 0.1.28 → 0.1.30
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.
- package/dist/base-command.d.ts +15 -0
- package/dist/base-command.js +49 -0
- package/dist/commands/agents/chat.d.ts +2 -0
- package/dist/commands/agents/chat.js +40 -2
- package/dist/commands/balance/show.d.ts +2 -3
- package/dist/commands/balance/show.js +2 -3
- package/dist/commands/balance/usage.d.ts +3 -11
- package/dist/commands/balance/usage.js +19 -72
- package/dist/commands/code/index.d.ts +4 -0
- package/dist/commands/code/index.js +9 -0
- package/dist/commands/code/install.d.ts +10 -2
- package/dist/commands/code/install.js +38 -28
- package/dist/commands/harnesses/leases/show.js +7 -4
- package/dist/commands/inbox/index.d.ts +15 -0
- package/dist/commands/inbox/index.js +52 -20
- package/dist/commands/instructions/install.d.ts +12 -0
- package/dist/commands/instructions/install.js +59 -14
- package/dist/commands/login.js +6 -0
- package/dist/commands/machines/dedicated/attach.js +1 -1
- package/dist/commands/machines/dedicated/cp.d.ts +1 -0
- package/dist/commands/machines/dedicated/cp.js +63 -9
- package/dist/commands/machines/dedicated/create.d.ts +1 -1
- package/dist/commands/machines/dedicated/create.js +7 -2
- package/dist/commands/machines/dedicated/exec.d.ts +23 -1
- package/dist/commands/machines/dedicated/exec.js +67 -7
- package/dist/commands/machines/dedicated/index.js +2 -0
- package/dist/commands/machines/dedicated/restore.d.ts +6 -0
- package/dist/commands/machines/dedicated/restore.js +7 -1
- package/dist/commands/machines/dedicated/sign-in.d.ts +4 -3
- package/dist/commands/machines/dedicated/sign-in.js +4 -3
- package/dist/commands/machines/dedicated/terminal.js +1 -1
- package/dist/commands/machines/dedicated/update-image.d.ts +15 -0
- package/dist/commands/machines/dedicated/update-image.js +38 -0
- package/dist/lib/balance.d.ts +2 -2
- package/dist/lib/balance.js +6 -4
- package/dist/lib/daemon-target.d.ts +103 -0
- package/dist/lib/daemon-target.js +110 -0
- package/dist/lib/dedicated-copy.d.ts +92 -2
- package/dist/lib/dedicated-copy.js +223 -18
- package/dist/lib/dedicated-lease-command.d.ts +7 -1
- package/dist/lib/dedicated-lease-command.js +16 -3
- package/dist/lib/dedicated-machines.d.ts +130 -8
- package/dist/lib/dedicated-machines.js +274 -15
- package/dist/lib/dedicated-terminal.d.ts +5 -25
- package/dist/lib/dedicated-terminal.js +45 -73
- package/dist/lib/dedicated-wait.d.ts +10 -0
- package/dist/lib/dedicated-wait.js +52 -0
- package/dist/lib/device-code.d.ts +12 -1
- package/dist/lib/device-code.js +44 -9
- package/dist/lib/harnesses.d.ts +13 -0
- package/dist/lib/harnesses.js +24 -0
- package/dist/lib/login.js +8 -7
- package/dist/lib/sky-code-broker.d.ts +46 -5
- package/dist/lib/sky-code-broker.js +96 -26
- package/dist/lib/sky-code.d.ts +33 -0
- package/dist/lib/sky-code.js +45 -7
- package/dist/lib/task-instruction-offer.js +12 -0
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.d.ts +67 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/cjs/refresh.js +124 -12
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.d.ts +67 -1
- package/dist/node_modules/@skrr-ai/auth-core/dist/esm/refresh.js +123 -11
- package/dist/node_modules/@skrr-ai/auth-core/package.json +1 -1
- package/dist/node_modules/@skrr-ai/data-provider/index.js +3061 -2876
- package/dist/node_modules/@skrr-ai/data-provider/package.json +1 -1
- package/oclif.manifest.json +1429 -1327
- 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
|
|
20
|
-
const
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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
|
-
|
|
72
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
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 {};
|