@skrr-ai/cli 0.1.77 → 0.1.79
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/commands/machines/dedicated/cp.js +2 -0
- package/dist/lib/dedicated-copy.js +14 -2
- package/dist/lib/dedicated-lease-command.js +1 -0
- package/dist/lib/dedicated-machines.d.ts +2 -0
- package/dist/lib/dedicated-ssh.d.ts +1 -0
- package/dist/lib/dedicated-ssh.js +15 -2
- package/dist/lib/dedicated-terminal.d.ts +3 -0
- package/dist/lib/dedicated-terminal.js +71 -15
- package/oclif.manifest.json +37543 -37543
- package/package.json +1 -1
|
@@ -26,6 +26,8 @@ class DedicatedMachinesCp extends base_command_1.BaseCommand {
|
|
|
26
26
|
'machine is created.',
|
|
27
27
|
'As with cp and scp, a destination that ends in / or is an existing directory receives the file ' +
|
|
28
28
|
'under its source name.',
|
|
29
|
+
'Files marked executable keep owner execute permission on the copy. Other permissions use ' +
|
|
30
|
+
'destination defaults; source special permission bits are not copied.',
|
|
29
31
|
'Neither side is left half-written: the file is staged beside its destination and renamed into ' +
|
|
30
32
|
'place at the end. A file that changes while it is copied is refused, not spliced. Ctrl-C removes ' +
|
|
31
33
|
'the partial copy and exits 130 (143 for SIGTERM, 129 when the terminal closes). Progress is ' +
|
|
@@ -220,7 +220,7 @@ async function uploadFile(localPath, leaseId, remotePath, scope, io) {
|
|
|
220
220
|
dataBase64: buffer.subarray(0, bytesRead).toString('base64'),
|
|
221
221
|
// A file that GREW is caught here: the machine refuses to commit
|
|
222
222
|
// when what it staged is not the size declared at the start.
|
|
223
|
-
...(final ? { final: true, totalSize: info.size } : {}),
|
|
223
|
+
...(final ? { final: true, totalSize: info.size, executable: (info.mode & 0o111) !== 0 } : {}),
|
|
224
224
|
}, scope), signal);
|
|
225
225
|
offset += bytesRead;
|
|
226
226
|
onProgress?.(offset, info.size);
|
|
@@ -276,6 +276,7 @@ async function downloadFromDedicatedRuntime(leaseId, remotePath, localPath, scop
|
|
|
276
276
|
let offset = 0;
|
|
277
277
|
let size = -1;
|
|
278
278
|
let mtimeMs = -1;
|
|
279
|
+
let executable;
|
|
279
280
|
try {
|
|
280
281
|
for (;;) {
|
|
281
282
|
throwIfCancelled(signal);
|
|
@@ -285,11 +286,17 @@ async function downloadFromDedicatedRuntime(leaseId, remotePath, localPath, scop
|
|
|
285
286
|
if (size === -1) {
|
|
286
287
|
size = chunk.size;
|
|
287
288
|
mtimeMs = chunk.mtimeMs;
|
|
289
|
+
executable = chunk.executable;
|
|
288
290
|
}
|
|
289
|
-
else if (chunk.size !== size || chunk.mtimeMs !== mtimeMs
|
|
291
|
+
else if (chunk.size !== size || chunk.mtimeMs !== mtimeMs ||
|
|
292
|
+
(typeof chunk.executable === 'boolean' && typeof executable === 'boolean' &&
|
|
293
|
+
chunk.executable !== executable)) {
|
|
290
294
|
// Chunks from two versions of a file splice into one that never existed.
|
|
291
295
|
throw changedWhileCopying(remotePath, 'on the machine');
|
|
292
296
|
}
|
|
297
|
+
// Older API replicas omit this additive metadata during a rollout.
|
|
298
|
+
if (typeof chunk.executable === 'boolean')
|
|
299
|
+
executable = chunk.executable;
|
|
293
300
|
const bytes = Buffer.from(chunk.dataBase64, 'base64');
|
|
294
301
|
if (bytes.byteLength !== chunk.bytesRead) {
|
|
295
302
|
throw new DedicatedCopyError('DEDICATED_RUNTIME_FILE_TRANSFER_FAILED', 'A chunk arrived damaged; nothing was written to the destination.');
|
|
@@ -304,6 +311,11 @@ async function downloadFromDedicatedRuntime(leaseId, remotePath, localPath, scop
|
|
|
304
311
|
throw changedWhileCopying(remotePath, 'on the machine');
|
|
305
312
|
}
|
|
306
313
|
}
|
|
314
|
+
// Set only the owner execute bit on the pinned staging inode. Other
|
|
315
|
+
// permissions keep destination defaults; source special bits never travel.
|
|
316
|
+
if (executable === true) {
|
|
317
|
+
await handle.chmod(((await handle.stat()).mode & 0o777) | 0o100);
|
|
318
|
+
}
|
|
307
319
|
await handle.close();
|
|
308
320
|
closed = true;
|
|
309
321
|
await (0, promises_1.rename)(temporary, target);
|
|
@@ -129,6 +129,7 @@ class DedicatedLeaseCommand extends base_command_1.BaseCommand {
|
|
|
129
129
|
...(flags.cwd ? { cwd: flags.cwd } : {}),
|
|
130
130
|
...(initialCommand ? { initialCommand } : {}),
|
|
131
131
|
persistent,
|
|
132
|
+
resumeExistingSupported: capabilities.includes('persistent_terminal_resume_v1'),
|
|
132
133
|
...(sessionName ? { sessionName } : {}),
|
|
133
134
|
leaseId,
|
|
134
135
|
input: process.stdin,
|
|
@@ -466,6 +466,7 @@ export interface DedicatedFileChunk {
|
|
|
466
466
|
path: string;
|
|
467
467
|
size: number;
|
|
468
468
|
mtimeMs: number;
|
|
469
|
+
executable?: boolean;
|
|
469
470
|
offset: number;
|
|
470
471
|
bytesRead: number;
|
|
471
472
|
dataBase64: string;
|
|
@@ -491,6 +492,7 @@ export declare function writeDedicatedRuntimeFileChunk(leaseId: string, input: {
|
|
|
491
492
|
final?: boolean;
|
|
492
493
|
abort?: boolean;
|
|
493
494
|
totalSize?: number;
|
|
495
|
+
executable?: boolean;
|
|
494
496
|
}, scope?: DedicatedRuntimeScope): Promise<DedicatedFileWriteResult>;
|
|
495
497
|
export type DedicatedCopyPlan = {
|
|
496
498
|
direction: 'upload';
|
|
@@ -343,6 +343,7 @@ function runSshProxy(options) {
|
|
|
343
343
|
channelId,
|
|
344
344
|
keyType: identity.keyType,
|
|
345
345
|
keyBlob: identity.keyBlob,
|
|
346
|
+
receiverAckV1: true,
|
|
346
347
|
});
|
|
347
348
|
});
|
|
348
349
|
socket.on('dedicated:ssh:start:ack', (ack) => {
|
|
@@ -366,11 +367,23 @@ function runSshProxy(options) {
|
|
|
366
367
|
input.resume();
|
|
367
368
|
input.on('data', onInput);
|
|
368
369
|
});
|
|
369
|
-
socket.on('dedicated:ssh:data', (frame) => {
|
|
370
|
+
socket.on('dedicated:ssh:data', (frame, receipt) => {
|
|
370
371
|
if (!frame || frame.channelId !== channelId || finished)
|
|
371
372
|
return;
|
|
372
373
|
if (typeof frame.data === 'string' && frame.data.length > 0) {
|
|
373
|
-
|
|
374
|
+
const bytes = Buffer.from(frame.data, 'base64');
|
|
375
|
+
if (typeof receipt === 'function') {
|
|
376
|
+
// A frame can precede start-ack (sshd's banner). The owned channel
|
|
377
|
+
// already advertised support; confirm only when stdout flushed.
|
|
378
|
+
output.write(bytes, (error) => {
|
|
379
|
+
receipt({ channelId, ok: !error && !finished });
|
|
380
|
+
if (error)
|
|
381
|
+
finish(1, 'Could not write SSH output.');
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
output.write(bytes);
|
|
386
|
+
}
|
|
374
387
|
}
|
|
375
388
|
});
|
|
376
389
|
socket.on('dedicated:ssh:closed', (frame) => {
|
|
@@ -27,6 +27,7 @@ export interface TerminalOutputFrame {
|
|
|
27
27
|
encoding?: 'utf8' | 'base64';
|
|
28
28
|
exitCode?: number | null;
|
|
29
29
|
signal?: string | null;
|
|
30
|
+
code?: string;
|
|
30
31
|
}
|
|
31
32
|
export interface TerminalStartAck {
|
|
32
33
|
sessionId?: string;
|
|
@@ -66,6 +67,8 @@ export interface DedicatedTerminalOptions {
|
|
|
66
67
|
* capability before asking.
|
|
67
68
|
*/
|
|
68
69
|
persistent?: boolean;
|
|
70
|
+
/** Capability-gated recovery can attach only an existing named shell. */
|
|
71
|
+
resumeExistingSupported?: boolean;
|
|
69
72
|
/**
|
|
70
73
|
* The session suffix (`skrr-term-<name>`). The same name re-attaches the
|
|
71
74
|
* same session; omitted under `persistent`, the machine names the session
|
|
@@ -67,6 +67,9 @@ function describeTerminalStartRefusal(ack) {
|
|
|
67
67
|
if (code === 'DEDICATED_RUNTIME_GENERATION_STALE') {
|
|
68
68
|
return 'The machine was replaced since its daemon registered. Wait a minute and try again.';
|
|
69
69
|
}
|
|
70
|
+
if (code === 'persistent-session-missing') {
|
|
71
|
+
return 'The original terminal no longer exists. Start a new terminal explicitly; no replacement shell was opened.';
|
|
72
|
+
}
|
|
70
73
|
if (code === 'not_found' || code === 'offline') {
|
|
71
74
|
return 'The machine’s daemon is not connected. Wait for it to come online and try again.';
|
|
72
75
|
}
|
|
@@ -94,6 +97,9 @@ function runDedicatedTerminal(options) {
|
|
|
94
97
|
let sessionReady = false;
|
|
95
98
|
let reattaching = false;
|
|
96
99
|
let persistentName = options.sessionName;
|
|
100
|
+
let retryTimer;
|
|
101
|
+
let recoveryTimer;
|
|
102
|
+
let connected = false;
|
|
97
103
|
const socket = connect((0, agentic_stream_1.toWebSocketOrigin)(options.baseURL), {
|
|
98
104
|
path: '/ws/agentic',
|
|
99
105
|
auth: {
|
|
@@ -170,6 +176,10 @@ function runDedicatedTerminal(options) {
|
|
|
170
176
|
const onSigterm = () => finish(143, true);
|
|
171
177
|
const onSighup = () => finish(129, true);
|
|
172
178
|
const onStartTimeout = () => {
|
|
179
|
+
if (reattaching) {
|
|
180
|
+
scheduleRetry();
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
173
183
|
fail(new DedicatedTerminalError('The machine did not answer the terminal start in time. Its daemon may be busy or offline.', 'TERMINAL_START_TIMEOUT'));
|
|
174
184
|
};
|
|
175
185
|
let startTimer = setTimeout(onStartTimeout, exports.TERMINAL_START_TIMEOUT_MS);
|
|
@@ -190,6 +200,8 @@ function runDedicatedTerminal(options) {
|
|
|
190
200
|
return;
|
|
191
201
|
finished = true;
|
|
192
202
|
clearTimeout(startTimer);
|
|
203
|
+
clearTimeout(retryTimer);
|
|
204
|
+
clearTimeout(recoveryTimer);
|
|
193
205
|
pending?.close();
|
|
194
206
|
restoreTty();
|
|
195
207
|
detachReconnectFailed();
|
|
@@ -203,6 +215,8 @@ function runDedicatedTerminal(options) {
|
|
|
203
215
|
return;
|
|
204
216
|
finished = true;
|
|
205
217
|
clearTimeout(startTimer);
|
|
218
|
+
clearTimeout(retryTimer);
|
|
219
|
+
clearTimeout(recoveryTimer);
|
|
206
220
|
pending?.close();
|
|
207
221
|
restoreTty();
|
|
208
222
|
detachReconnectFailed();
|
|
@@ -212,15 +226,19 @@ function runDedicatedTerminal(options) {
|
|
|
212
226
|
socket.on('connect_error', (err) => {
|
|
213
227
|
// A failed retry while reattaching is not fatal — the attempt countdown
|
|
214
228
|
// keeps running and `reconnect_failed` ends it if they all miss.
|
|
215
|
-
if (persistent && reattaching)
|
|
229
|
+
if (persistent && reattaching && !err.data?.code)
|
|
216
230
|
return;
|
|
217
231
|
fail(new DedicatedTerminalError(`Could not connect to skrr: ${err.message}`, err.data?.code || 'TERMINAL_CONNECT_FAILED'));
|
|
218
232
|
});
|
|
219
|
-
|
|
220
|
-
if (finished)
|
|
233
|
+
function sendStart() {
|
|
234
|
+
if (finished || !connected)
|
|
221
235
|
return;
|
|
236
|
+
// Clear the previous relay entry too; never replay bytes sent before loss.
|
|
237
|
+
if (sessionId)
|
|
238
|
+
socket.emit(events.stop, { sessionId });
|
|
222
239
|
sessionId = (0, node_crypto_1.randomUUID)();
|
|
223
240
|
clearTimeout(startTimer);
|
|
241
|
+
clearTimeout(retryTimer);
|
|
224
242
|
startTimer = setTimeout(onStartTimeout, exports.TERMINAL_START_TIMEOUT_MS);
|
|
225
243
|
startTimer.unref();
|
|
226
244
|
socket.emit(events.start, {
|
|
@@ -228,13 +246,44 @@ function runDedicatedTerminal(options) {
|
|
|
228
246
|
sessionId,
|
|
229
247
|
...(options.cwd ? { cwd: options.cwd } : {}),
|
|
230
248
|
...(persistent ? { persistent: true } : {}),
|
|
231
|
-
|
|
232
|
-
// machine named itself comes back in the ack, and a re-attach must
|
|
233
|
-
// name it — `tmux -A` without `-s` would open a fresh session instead.
|
|
249
|
+
...(reattaching ? { resumeExisting: true } : {}),
|
|
234
250
|
...(persistentName ? { sessionName: persistentName } : {}),
|
|
235
251
|
...(options.leaseId ? { leaseId: options.leaseId } : {}),
|
|
236
252
|
...size(),
|
|
237
253
|
});
|
|
254
|
+
}
|
|
255
|
+
function scheduleRetry() {
|
|
256
|
+
clearTimeout(startTimer);
|
|
257
|
+
clearTimeout(retryTimer);
|
|
258
|
+
if (finished || !reattaching)
|
|
259
|
+
return;
|
|
260
|
+
retryTimer = setTimeout(sendStart, 2_000);
|
|
261
|
+
retryTimer.unref();
|
|
262
|
+
}
|
|
263
|
+
function recover() {
|
|
264
|
+
if (finished || reattaching)
|
|
265
|
+
return;
|
|
266
|
+
started = false;
|
|
267
|
+
pending?.close(); // The sign-in command is never typed a second time.
|
|
268
|
+
clearTimeout(startTimer);
|
|
269
|
+
if (!persistent || !persistentName || !options.resumeExistingSupported) {
|
|
270
|
+
errorOutput.write(`\r\n[terminal attachment lost; rerun the same command to reattach${persistentName ? ` to '${persistentName}'` : ''}]\r\n`);
|
|
271
|
+
finish(1, false);
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
reattaching = true;
|
|
275
|
+
errorOutput.write(`\r\n[lost the terminal attachment — reattaching to session '${persistentName}'…]\r\n`);
|
|
276
|
+
recoveryTimer = setTimeout(() => {
|
|
277
|
+
errorOutput.write(`\r\n[could not reattach within 60 seconds; rerun the same command for session '${persistentName}']\r\n`);
|
|
278
|
+
finish(1, false);
|
|
279
|
+
}, 60_000);
|
|
280
|
+
recoveryTimer.unref();
|
|
281
|
+
if (connected)
|
|
282
|
+
sendStart();
|
|
283
|
+
}
|
|
284
|
+
socket.on('connect', () => {
|
|
285
|
+
connected = true;
|
|
286
|
+
sendStart();
|
|
238
287
|
});
|
|
239
288
|
socket.on(events.startAck, (ack) => {
|
|
240
289
|
if (!ack || ack.sessionId !== sessionId || started || finished)
|
|
@@ -242,6 +291,12 @@ function runDedicatedTerminal(options) {
|
|
|
242
291
|
if (ack.daemonId && ack.daemonId !== options.daemonId)
|
|
243
292
|
return;
|
|
244
293
|
if (!ack.ok) {
|
|
294
|
+
// Only daemon availability refusals are retryable. Authorization,
|
|
295
|
+
// generation fencing and a missing original shell end recovery.
|
|
296
|
+
if (reattaching && (ack.code === 'offline' || ack.code === 'not_found')) {
|
|
297
|
+
scheduleRetry();
|
|
298
|
+
return;
|
|
299
|
+
}
|
|
245
300
|
fail(new DedicatedTerminalError(describeTerminalStartRefusal(ack), ack.code || 'TERMINAL_START_REFUSED'));
|
|
246
301
|
return;
|
|
247
302
|
}
|
|
@@ -249,6 +304,8 @@ function runDedicatedTerminal(options) {
|
|
|
249
304
|
started = true;
|
|
250
305
|
reattaching = false;
|
|
251
306
|
clearTimeout(startTimer);
|
|
307
|
+
clearTimeout(retryTimer);
|
|
308
|
+
clearTimeout(recoveryTimer);
|
|
252
309
|
if (typeof ack.persistentName === 'string' && ack.persistentName) {
|
|
253
310
|
persistentName = ack.persistentName;
|
|
254
311
|
}
|
|
@@ -278,8 +335,13 @@ function runDedicatedTerminal(options) {
|
|
|
278
335
|
return;
|
|
279
336
|
if (frame.daemonId && frame.daemonId !== options.daemonId)
|
|
280
337
|
return;
|
|
338
|
+
if (frame.code === 'daemon-disconnected' ||
|
|
339
|
+
(frame.type === 'error' && (frame.code === 'not-found' || frame.code === 'closed'))) {
|
|
340
|
+
recover();
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
281
343
|
if (frame.type === 'exit') {
|
|
282
|
-
finish(typeof frame.exitCode === 'number' ? frame.exitCode :
|
|
344
|
+
finish(typeof frame.exitCode === 'number' ? frame.exitCode : 1, false);
|
|
283
345
|
return;
|
|
284
346
|
}
|
|
285
347
|
if (frame.type === 'error' || frame.type === 'info') {
|
|
@@ -318,17 +380,11 @@ function runDedicatedTerminal(options) {
|
|
|
318
380
|
}
|
|
319
381
|
}
|
|
320
382
|
socket.on('disconnect', () => {
|
|
383
|
+
connected = false;
|
|
321
384
|
if (finished)
|
|
322
385
|
return;
|
|
323
386
|
if (persistent) {
|
|
324
|
-
|
|
325
|
-
// detaches. socket.io retries and the next start re-attaches by name.
|
|
326
|
-
started = false;
|
|
327
|
-
if (!reattaching) {
|
|
328
|
-
reattaching = true;
|
|
329
|
-
const named = persistentName ? ` '${persistentName}'` : '';
|
|
330
|
-
errorOutput.write(`\r\n[lost the connection to skrr — reattaching to session${named}…]\r\n`);
|
|
331
|
-
}
|
|
387
|
+
recover();
|
|
332
388
|
return;
|
|
333
389
|
}
|
|
334
390
|
if (!started) {
|