@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.
- 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 +2930 -2828
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.DEDICATED_FILE_MAX_BYTES = exports.DEDICATED_FILE_CHUNK_BYTES = exports.DEDICATED_EXEC_DEFAULT_TIMEOUT_MS = exports.DEDICATED_EXEC_MAX_TIMEOUT_MS = exports.DEDICATED_MACHINE_PRODUCT = exports.DEDICATED_LEASE_ACTIONS = void 0;
|
|
4
|
+
exports.describeDedicatedImageStanding = describeDedicatedImageStanding;
|
|
4
5
|
exports.dedicatedRuntimeCreateInput = dedicatedRuntimeCreateInput;
|
|
5
6
|
exports.isDedicatedRepositoryRef = isDedicatedRepositoryRef;
|
|
6
7
|
exports.createDedicatedRuntime = createDedicatedRuntime;
|
|
@@ -25,23 +26,59 @@ exports.formatUsdCents = formatUsdCents;
|
|
|
25
26
|
exports.dedicatedSpendSummary = dedicatedSpendSummary;
|
|
26
27
|
exports.describeDedicatedDeletion = describeDedicatedDeletion;
|
|
27
28
|
exports.describeDedicatedLease = describeDedicatedLease;
|
|
29
|
+
exports.formatDedicatedDeadline = formatDedicatedDeadline;
|
|
30
|
+
exports.dedicatedDeletionLines = dedicatedDeletionLines;
|
|
28
31
|
exports.dedicatedLeaseHarnesses = dedicatedLeaseHarnesses;
|
|
29
32
|
exports.dedicatedLeaseIdForDaemon = dedicatedLeaseIdForDaemon;
|
|
30
33
|
exports.dedicatedTerminalHarness = dedicatedTerminalHarness;
|
|
31
34
|
exports.selectDedicatedAttachHarness = selectDedicatedAttachHarness;
|
|
32
35
|
exports.dedicatedCatalogFromProducts = dedicatedCatalogFromProducts;
|
|
33
36
|
exports.dedicatedRuntimeApiErrorCode = dedicatedRuntimeApiErrorCode;
|
|
37
|
+
exports.dedicatedRuntimeApiErrorDetails = dedicatedRuntimeApiErrorDetails;
|
|
34
38
|
exports.formatDedicatedRuntimeApiError = formatDedicatedRuntimeApiError;
|
|
35
39
|
const api_fetch_1 = require("./api-fetch");
|
|
36
40
|
const edge_error_page_1 = require("./edge-error-page");
|
|
41
|
+
const harnesses_1 = require("./harnesses");
|
|
37
42
|
exports.DEDICATED_LEASE_ACTIONS = [
|
|
38
43
|
'start',
|
|
39
44
|
'stop',
|
|
40
45
|
'restart',
|
|
46
|
+
'update-image',
|
|
41
47
|
'snapshot',
|
|
42
48
|
'health-check',
|
|
43
49
|
];
|
|
44
50
|
exports.DEDICATED_MACHINE_PRODUCT = 'dedicated-machine';
|
|
51
|
+
/** One line saying where a machine's image stands and what the owner can do. */
|
|
52
|
+
function describeDedicatedImageStanding(image, leaseId) {
|
|
53
|
+
const last = image.lastUpdate;
|
|
54
|
+
if (last?.status === 'applying') {
|
|
55
|
+
return `moving onto the current image${last.startedAt ? ` since ${last.startedAt}` : ''}; files are kept`;
|
|
56
|
+
}
|
|
57
|
+
if (last?.status === 'rolling_back') {
|
|
58
|
+
return 'the current image did not come back healthy; returning to the previous image, files kept';
|
|
59
|
+
}
|
|
60
|
+
const parts = [];
|
|
61
|
+
if (image.updateAvailable) {
|
|
62
|
+
parts.push(`update available${image.updateBy ? `, required by ${image.updateBy}` : ''} — \`skrr machines dedicated update-image ${leaseId || '<lease-id>'}\``);
|
|
63
|
+
}
|
|
64
|
+
else if (last?.status === 'rolled_back') {
|
|
65
|
+
parts.push('on the previous image: the current one did not come back healthy here');
|
|
66
|
+
}
|
|
67
|
+
else if (last?.status === 'failed') {
|
|
68
|
+
parts.push('the last image update did not complete on either image; files are kept');
|
|
69
|
+
}
|
|
70
|
+
else {
|
|
71
|
+
parts.push('current');
|
|
72
|
+
}
|
|
73
|
+
if (last?.status === 'succeeded' && last.settledAt)
|
|
74
|
+
parts.push(`updated ${last.settledAt}`);
|
|
75
|
+
if (last?.status === 'rolled_back' && last.settledAt) {
|
|
76
|
+
parts.push(`rolled back ${last.settledAt}`);
|
|
77
|
+
}
|
|
78
|
+
if (last?.status === 'failed' && last.settledAt)
|
|
79
|
+
parts.push(`update failed ${last.settledAt}`);
|
|
80
|
+
return parts.join(' · ');
|
|
81
|
+
}
|
|
45
82
|
/**
|
|
46
83
|
* Flags in, request body out — and the point of it being a function is the
|
|
47
84
|
* OMISSION.
|
|
@@ -61,7 +98,9 @@ function dedicatedRuntimeCreateInput(input) {
|
|
|
61
98
|
requestId: input.requestId,
|
|
62
99
|
displayName: input.displayName,
|
|
63
100
|
sizePreset: input.sizePreset,
|
|
64
|
-
|
|
101
|
+
...(input.monthlySpendingLimitCents !== undefined
|
|
102
|
+
? { monthlySpendingLimitCents: input.monthlySpendingLimitCents }
|
|
103
|
+
: {}),
|
|
65
104
|
...(input.regionClass ? { regionClass: input.regionClass } : {}),
|
|
66
105
|
...(input.storageGb !== undefined ? { storageGb: input.storageGb } : {}),
|
|
67
106
|
...(input.retentionClass ? { retentionClass: input.retentionClass } : {}),
|
|
@@ -226,8 +265,8 @@ async function observeDedicatedRuntimeExecution(leaseId, executionId, options) {
|
|
|
226
265
|
/** Mirrors the daemon's bounds (daemon/src/tools/file-transfer.ts). */
|
|
227
266
|
exports.DEDICATED_FILE_CHUNK_BYTES = 512 * 1024;
|
|
228
267
|
exports.DEDICATED_FILE_MAX_BYTES = 64 * 1024 * 1024;
|
|
229
|
-
async function readDedicatedRuntimeFileChunk(leaseId, input, scope = {}) {
|
|
230
|
-
return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/files/read`, scope), { method: 'POST', body: input });
|
|
268
|
+
async function readDedicatedRuntimeFileChunk(leaseId, input, scope = {}, signal) {
|
|
269
|
+
return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/files/read`, scope), { method: 'POST', body: input, ...(signal ? { signal } : {}) });
|
|
231
270
|
}
|
|
232
271
|
async function writeDedicatedRuntimeFileChunk(leaseId, input, scope = {}) {
|
|
233
272
|
return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/files/write`, scope), { method: 'POST', body: input });
|
|
@@ -308,6 +347,14 @@ function describeDedicatedDeletion(deletion, leaseId) {
|
|
|
308
347
|
if (deletion.status === 'purging') {
|
|
309
348
|
return 'recovery window closed; every copy is being deleted';
|
|
310
349
|
}
|
|
350
|
+
if (deletion.status === 'withheld') {
|
|
351
|
+
return deletion.restorable
|
|
352
|
+
? `deletion stopped after the machine was shut down; files kept — \`skrr machines dedicated restore ${leaseId || '<lease-id>'}\``
|
|
353
|
+
: 'deletion stopped after the machine was shut down; files kept';
|
|
354
|
+
}
|
|
355
|
+
if (deletion.restorable && deletion.supersededByLeaseId) {
|
|
356
|
+
return `previous copy kept until ${deletion.recoverableUntil} (restored into ${deletion.supersededByLeaseId}); restorable until then — \`skrr machines dedicated restore ${leaseId || '<lease-id>'}\``;
|
|
357
|
+
}
|
|
311
358
|
if (deletion.restorable) {
|
|
312
359
|
return `restorable until ${deletion.recoverableUntil} — \`skrr machines dedicated restore ${leaseId || '<lease-id>'}\``;
|
|
313
360
|
}
|
|
@@ -348,6 +395,9 @@ function describeDedicatedLease(lease, harnesses = []) {
|
|
|
348
395
|
row('last backup', storage.lastBackupAt);
|
|
349
396
|
const image = [resources.imageVersion || lease.daemon?.imageVersion].filter(Boolean).join('');
|
|
350
397
|
row('image', image);
|
|
398
|
+
if (lease.health?.image) {
|
|
399
|
+
row('image update', describeDedicatedImageStanding(lease.health.image, dedicatedLeaseId(lease)));
|
|
400
|
+
}
|
|
351
401
|
if (lease.daemon?.version || lease.daemon?.lastHeartbeatAt) {
|
|
352
402
|
row('daemon', [
|
|
353
403
|
lease.daemon.version ? `skrrd ${lease.daemon.version}` : '',
|
|
@@ -358,19 +408,18 @@ function describeDedicatedLease(lease, harnesses = []) {
|
|
|
358
408
|
}
|
|
359
409
|
const health = lease.health;
|
|
360
410
|
if (health && typeof health.assignable === 'boolean') {
|
|
361
|
-
|
|
362
|
-
? '
|
|
363
|
-
:
|
|
411
|
+
const why = health.assignable
|
|
412
|
+
? ''
|
|
413
|
+
: (0, harnesses_1.describeHealthReasonSentences)(health.reasons, dedicatedLeaseState(lease));
|
|
414
|
+
row('health', health.assignable ? 'can take work' : `not assignable${why ? `: ${why}` : ''}`);
|
|
364
415
|
}
|
|
365
416
|
row('spend', lease.billing ? dedicatedSpendSummary(lease) : undefined);
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
.filter(Boolean)
|
|
373
|
-
.join(' · '));
|
|
417
|
+
const deletionPlan = dedicatedDeletionLines(lease);
|
|
418
|
+
if (deletionPlan.length > 0) {
|
|
419
|
+
row('deletion', deletionPlan[0]);
|
|
420
|
+
for (const line of deletionPlan.slice(1)) {
|
|
421
|
+
lines.push(` ${''.padEnd(13)}${line}`);
|
|
422
|
+
}
|
|
374
423
|
}
|
|
375
424
|
const deletion = storage.deletion;
|
|
376
425
|
if (deletion?.status) {
|
|
@@ -394,6 +443,178 @@ function describeDedicatedLease(lease, harnesses = []) {
|
|
|
394
443
|
row('support ref', lease.supportReference);
|
|
395
444
|
return lines;
|
|
396
445
|
}
|
|
446
|
+
/** "2026-09-30 14:00 UTC (in 17 days)" — a deadline readable without converting it. */
|
|
447
|
+
function formatDedicatedDeadline(value, now = new Date()) {
|
|
448
|
+
const at = value ? new Date(value) : null;
|
|
449
|
+
if (!at || Number.isNaN(at.getTime()))
|
|
450
|
+
return 'unknown';
|
|
451
|
+
const stamp = `${at.toISOString().slice(0, 16).replace('T', ' ')} UTC`;
|
|
452
|
+
const days = Math.round((at.getTime() - now.getTime()) / 86_400_000);
|
|
453
|
+
const relative = days === 0
|
|
454
|
+
? 'today'
|
|
455
|
+
: days > 0
|
|
456
|
+
? `in ${days} day${days === 1 ? '' : 's'}`
|
|
457
|
+
: `${-days} day${days === -1 ? '' : 's'} ago`;
|
|
458
|
+
return `${stamp} (${relative})`;
|
|
459
|
+
}
|
|
460
|
+
/** Why, by the remedy when the plan names one; by the reason for an older plan. */
|
|
461
|
+
const DELETION_REASON_BY_REMEDY = {
|
|
462
|
+
pay_invoice: 'an invoice for its usage is unpaid',
|
|
463
|
+
subscribe: 'the plan it needs has ended',
|
|
464
|
+
};
|
|
465
|
+
const DELETION_REASON_TEXT = {
|
|
466
|
+
plan_inactive: DELETION_REASON_BY_REMEDY.subscribe,
|
|
467
|
+
subscription_canceled: 'subscription cancelled',
|
|
468
|
+
payment_failed: 'payment failed and retries finished',
|
|
469
|
+
unknown: 'billing account not in good standing',
|
|
470
|
+
};
|
|
471
|
+
const LEGACY_DELETION_REMEDY_TEXT = {
|
|
472
|
+
subscription_canceled: 'unless you subscribe again',
|
|
473
|
+
payment_failed: 'unless the outstanding invoice is paid',
|
|
474
|
+
unknown: 'unless billing is restored',
|
|
475
|
+
};
|
|
476
|
+
/** An invoice's amount in its own currency, from Stripe's minor units. */
|
|
477
|
+
function formatInvoiceAmount(amountDue, currency) {
|
|
478
|
+
const code = String(currency || '').toUpperCase();
|
|
479
|
+
if (!/^[A-Z]{3}$/.test(code) || !Number.isSafeInteger(amountDue))
|
|
480
|
+
return null;
|
|
481
|
+
try {
|
|
482
|
+
const format = new Intl.NumberFormat('en-US', { style: 'currency', currency: code });
|
|
483
|
+
return format.format(amountDue / 10 ** (format.resolvedOptions().maximumFractionDigits ?? 2));
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Exactly the remedy that clears the cause, as the tail of "workspace deleted
|
|
491
|
+
* after …", and the pages it is carried out at: each unpaid invoice's (for an
|
|
492
|
+
* owner who pays it), or Billing in the app to subscribe again.
|
|
493
|
+
*/
|
|
494
|
+
function dedicatedDeletionRemedy(plan, lease) {
|
|
495
|
+
const remedy = plan.remedy;
|
|
496
|
+
if (remedy?.kind === 'pay_invoice') {
|
|
497
|
+
if (!remedy.paidByOwner || remedy.invoices.length === 0) {
|
|
498
|
+
return {
|
|
499
|
+
clause: "unless the unpaid invoice is paid (it was sent to your workspace's billing contact)",
|
|
500
|
+
lines: [],
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
return {
|
|
504
|
+
clause: `unless the unpaid invoice${remedy.invoices.length === 1 ? ' is' : 's are'} paid`,
|
|
505
|
+
lines: [
|
|
506
|
+
...remedy.invoices.map((invoice) => {
|
|
507
|
+
const amount = formatInvoiceAmount(invoice.amountDue, invoice.currency);
|
|
508
|
+
return `pay: ${amount ? `${amount} ` : ''}${invoice.hostedInvoiceUrl}`;
|
|
509
|
+
}),
|
|
510
|
+
// Read again from Stripe with the plan, like the machine's page; a link in
|
|
511
|
+
// an email can expire.
|
|
512
|
+
"a payment link in an email can expire; this one, and the machine's page on the web, stay current",
|
|
513
|
+
],
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
if (remedy?.kind === 'subscribe') {
|
|
517
|
+
return {
|
|
518
|
+
clause: lease.workspaceId
|
|
519
|
+
? "unless the workspace subscribes again (the workspace's Settings → Billing in the app)"
|
|
520
|
+
: 'unless you subscribe again (Billing in the app)',
|
|
521
|
+
lines: [],
|
|
522
|
+
};
|
|
523
|
+
}
|
|
524
|
+
return {
|
|
525
|
+
clause: LEGACY_DELETION_REMEDY_TEXT[plan.reason || 'unknown'] || LEGACY_DELETION_REMEDY_TEXT.unknown,
|
|
526
|
+
lines: [],
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
const DELETION_BLOCKER_TEXT = {
|
|
530
|
+
billing_status_unavailable: 'billing status could not be read',
|
|
531
|
+
paused: 'deletions are paused by an operator',
|
|
532
|
+
hold: 'a hold is open on this workspace',
|
|
533
|
+
other_operation_running: 'another operation on this machine is running',
|
|
534
|
+
invoice_added: 'another invoice became unpaid; a notice naming it is being sent',
|
|
535
|
+
not_stopped: 'the machine is not stopped',
|
|
536
|
+
notice_not_delivered: 'no deletion notice has reached your email yet',
|
|
537
|
+
final_notice_not_delivered: 'the final notice has not reached your email yet',
|
|
538
|
+
final_notice_resent: 'the final notice is being sent again',
|
|
539
|
+
notice_undeliverable: 'the deletion notice could not be delivered to your email',
|
|
540
|
+
email_unverified: 'your email address is not verified',
|
|
541
|
+
email_changed: 'your email address changed; the notice is being sent to the new one',
|
|
542
|
+
cause_changed: 'the billing state changed; a new notice is being sent',
|
|
543
|
+
billing_contact_unavailable: "the billing account's contact could not be read",
|
|
544
|
+
changed_concurrently: 'the lease changed while deleting',
|
|
545
|
+
};
|
|
546
|
+
const NOTICE_TEXT = {
|
|
547
|
+
compute_stopped: 'stopped',
|
|
548
|
+
deletion_notice: 'deletion notice',
|
|
549
|
+
early_reminder: '30-day reminder',
|
|
550
|
+
reminder: '7-day reminder',
|
|
551
|
+
final_notice: '1-day notice',
|
|
552
|
+
deletion_cancelled: 'cancellation',
|
|
553
|
+
archive_reminder: '7-day reminder',
|
|
554
|
+
archive_final_reminder: '1-day reminder',
|
|
555
|
+
};
|
|
556
|
+
/**
|
|
557
|
+
* The deletion plan as lines for `show`: what will happen and when first, then
|
|
558
|
+
* why, which notices reached the owner, and what is holding it back.
|
|
559
|
+
*/
|
|
560
|
+
function dedicatedDeletionLines(lease, now = new Date()) {
|
|
561
|
+
// ONE source: the server's plan. `storage.deleteAfter` and `unpaidSince` are
|
|
562
|
+
// its compatibility fields, and a stopped lease's stored date is a deadline only
|
|
563
|
+
// once a delivered notice started it — so they are not read here.
|
|
564
|
+
const plan = lease.storage?.deletionPlan;
|
|
565
|
+
if (!plan)
|
|
566
|
+
return [];
|
|
567
|
+
const lines = [];
|
|
568
|
+
const remedy = dedicatedDeletionRemedy(plan, lease);
|
|
569
|
+
let remedyApplies = false;
|
|
570
|
+
if (plan.kind === 'retention_period') {
|
|
571
|
+
lines.push(`workspace deleted after ${formatDedicatedDeadline(plan.deleteAfter, now)}`);
|
|
572
|
+
}
|
|
573
|
+
else if (plan.status === 'cancelled') {
|
|
574
|
+
const kept = plan.workspaceWithheld
|
|
575
|
+
? ' — the machine had been shut down; files kept, restore them into a new machine'
|
|
576
|
+
: ' — workspace kept';
|
|
577
|
+
lines.push(`deletion cancelled ${formatDedicatedDeadline(plan.cancelledAt, now)}${plan.cancelReason === 'billing_resolved' ? ` — billing in good standing${kept}` : plan.workspaceWithheld ? kept : ''}`);
|
|
578
|
+
}
|
|
579
|
+
else if (plan.status === 'deleting') {
|
|
580
|
+
// Until its files are about to be deleted, clearing the cause still stops it.
|
|
581
|
+
remedyApplies = plan.cancellable === true;
|
|
582
|
+
lines.push(remedyApplies
|
|
583
|
+
? `workspace is being deleted — not too late: it stops, with every file kept, ${remedy.clause.replace(/^unless /, 'if ')}`
|
|
584
|
+
: 'workspace is being deleted');
|
|
585
|
+
}
|
|
586
|
+
else if (plan.status === 'scheduled') {
|
|
587
|
+
remedyApplies = true;
|
|
588
|
+
lines.push(`workspace deleted after ${formatDedicatedDeadline(plan.deleteAfter, now)} ${remedy.clause}`);
|
|
589
|
+
}
|
|
590
|
+
else {
|
|
591
|
+
remedyApplies = true;
|
|
592
|
+
lines.push(`files kept until at least ${formatDedicatedDeadline(plan.retentionEndsAt, now)}; no deletion date until a deletion notice reaches your email, then at least ${plan.noticeFloorDays || 30} days; no deletion at all ${remedy.clause.replace(/^unless /, 'if ')}`);
|
|
593
|
+
}
|
|
594
|
+
if (remedyApplies)
|
|
595
|
+
lines.push(...remedy.lines);
|
|
596
|
+
if (plan.kind === 'unpaid' && plan.status !== 'cancelled') {
|
|
597
|
+
const unpaid = plan.remedy?.kind === 'pay_invoice';
|
|
598
|
+
const why = [
|
|
599
|
+
plan.entitlementEndedAt
|
|
600
|
+
? `${unpaid ? 'unpaid since' : 'billing ended'} ${formatDedicatedDeadline(plan.entitlementEndedAt, now)}`
|
|
601
|
+
: '',
|
|
602
|
+
(plan.remedy && DELETION_REASON_BY_REMEDY[plan.remedy.kind]) ||
|
|
603
|
+
(plan.reason ? DELETION_REASON_TEXT[plan.reason] || plan.reason : ''),
|
|
604
|
+
].filter(Boolean);
|
|
605
|
+
if (why.length > 0)
|
|
606
|
+
lines.push(why.join(' · '));
|
|
607
|
+
}
|
|
608
|
+
const delivered = (plan.notices || [])
|
|
609
|
+
.filter((notice) => notice.deliveredAt)
|
|
610
|
+
.map((notice) => `${NOTICE_TEXT[notice.notice] || notice.notice} ${formatDedicatedDeadline(notice.deliveredAt, now)} (${(notice.channels || []).join(', ')})`);
|
|
611
|
+
if (delivered.length > 0)
|
|
612
|
+
lines.push(`notices: ${delivered.join(' · ')}`);
|
|
613
|
+
if (plan.blockedBy) {
|
|
614
|
+
lines.push(`held back: ${DELETION_BLOCKER_TEXT[plan.blockedBy] || plan.blockedBy}`);
|
|
615
|
+
}
|
|
616
|
+
return lines;
|
|
617
|
+
}
|
|
397
618
|
/**
|
|
398
619
|
* Every harness row a Dedicated Runtime lease's daemon registered, freshest
|
|
399
620
|
* first.
|
|
@@ -609,6 +830,43 @@ function dedicatedRuntimeApiErrorCode(err) {
|
|
|
609
830
|
}
|
|
610
831
|
return err.status ? `HTTP_${err.status}` : undefined;
|
|
611
832
|
}
|
|
833
|
+
/**
|
|
834
|
+
* The public `details` object a refusal carried — `state`, `action` and the
|
|
835
|
+
* health `reasons` of a failed health check — for the `--json` envelope.
|
|
836
|
+
*
|
|
837
|
+
* The server builds it from an allow-list of safe values (reason codes, lease
|
|
838
|
+
* states), so it is passed through whole rather than re-filtered here, where a
|
|
839
|
+
* second list would only drift from the first.
|
|
840
|
+
*/
|
|
841
|
+
function dedicatedRuntimeApiErrorDetails(err) {
|
|
842
|
+
if (!(err instanceof api_fetch_1.ApiFetchError) || !err.body) {
|
|
843
|
+
return undefined;
|
|
844
|
+
}
|
|
845
|
+
try {
|
|
846
|
+
const parsed = JSON.parse(err.body);
|
|
847
|
+
const details = parsed?.details;
|
|
848
|
+
return details && typeof details === 'object' && !Array.isArray(details)
|
|
849
|
+
? details
|
|
850
|
+
: undefined;
|
|
851
|
+
}
|
|
852
|
+
catch {
|
|
853
|
+
return undefined;
|
|
854
|
+
}
|
|
855
|
+
}
|
|
856
|
+
/**
|
|
857
|
+
* The health reasons a refusal named, deduplicated, as the sentence's tail.
|
|
858
|
+
*
|
|
859
|
+
* A failed `health-check` exists to say WHY a machine cannot take work, and the
|
|
860
|
+
* sentence alone never did: it printed "did not pass health checks." and kept
|
|
861
|
+
* `image_version_missing` to itself (OSK-8687). It then printed the codes, which
|
|
862
|
+
* say nothing to the person running it (OSK-8768); `--json` keeps them in
|
|
863
|
+
* `error.details.reasons`.
|
|
864
|
+
*/
|
|
865
|
+
function describeRefusalReasons(err) {
|
|
866
|
+
const details = dedicatedRuntimeApiErrorDetails(err);
|
|
867
|
+
const sentences = (0, harnesses_1.describeHealthReasonSentences)(details?.reasons, details?.state);
|
|
868
|
+
return sentences ? ` Reasons: ${sentences}` : '';
|
|
869
|
+
}
|
|
612
870
|
function formatDedicatedRuntimeApiError(err) {
|
|
613
871
|
if (!(err instanceof api_fetch_1.ApiFetchError) || !err.body) {
|
|
614
872
|
return null;
|
|
@@ -632,7 +890,8 @@ function formatDedicatedRuntimeApiError(err) {
|
|
|
632
890
|
}
|
|
633
891
|
try {
|
|
634
892
|
const parsed = JSON.parse(err.body);
|
|
635
|
-
|
|
893
|
+
const sentence = parsed.message || parsed.error || parsed.code;
|
|
894
|
+
return sentence ? `${sentence}${describeRefusalReasons(err)}` : null;
|
|
636
895
|
}
|
|
637
896
|
catch {
|
|
638
897
|
return err.body.trim() || null;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { io, type Socket } from 'socket.io-client';
|
|
2
|
+
import { type PendingShellCommandStatus } from '@skrr-ai/data-provider';
|
|
2
3
|
/**
|
|
3
4
|
* A real pseudo-terminal on a Dedicated Runtime, from a local terminal.
|
|
4
5
|
*
|
|
@@ -17,13 +18,6 @@ import { io, type Socket } from 'socket.io-client';
|
|
|
17
18
|
export declare const TERMINAL_DETACH_BYTE = 29;
|
|
18
19
|
/** How long the machine has to answer a start before the CLI gives up. */
|
|
19
20
|
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
21
|
export interface TerminalOutputFrame {
|
|
28
22
|
sessionId?: string;
|
|
29
23
|
daemonId?: string;
|
|
@@ -44,25 +38,11 @@ export interface TerminalStartAck {
|
|
|
44
38
|
/** Bytes of one output frame, without splitting a UTF-8 or ANSI sequence. */
|
|
45
39
|
export declare function decodeTerminalFrame(frame: TerminalOutputFrame): Buffer;
|
|
46
40
|
/**
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
*
|
|
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.
|
|
41
|
+
* What the terminal tells the user about an initial command that has not been
|
|
42
|
+
* typed. The web terminal shows the same two states with a button; here the
|
|
43
|
+
* user types it themselves.
|
|
52
44
|
*/
|
|
53
|
-
export declare function
|
|
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
|
-
};
|
|
45
|
+
export declare function describePendingCommand(status: PendingShellCommandStatus, command: string): string | null;
|
|
66
46
|
export declare class DedicatedTerminalError extends Error {
|
|
67
47
|
readonly code: string;
|
|
68
48
|
constructor(message: string, code: string);
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.DedicatedTerminalError = exports.
|
|
3
|
+
exports.DedicatedTerminalError = exports.TERMINAL_START_TIMEOUT_MS = exports.TERMINAL_DETACH_BYTE = void 0;
|
|
4
4
|
exports.decodeTerminalFrame = decodeTerminalFrame;
|
|
5
|
-
exports.
|
|
5
|
+
exports.describePendingCommand = describePendingCommand;
|
|
6
6
|
exports.describeTerminalStartRefusal = describeTerminalStartRefusal;
|
|
7
7
|
exports.runDedicatedTerminal = runDedicatedTerminal;
|
|
8
8
|
const node_crypto_1 = require("node:crypto");
|
|
@@ -27,13 +27,6 @@ const agentic_stream_1 = require("./agentic-stream");
|
|
|
27
27
|
exports.TERMINAL_DETACH_BYTE = 0x1d;
|
|
28
28
|
/** How long the machine has to answer a start before the CLI gives up. */
|
|
29
29
|
exports.TERMINAL_START_TIMEOUT_MS = 30_000;
|
|
30
|
-
/**
|
|
31
|
-
* When to type an initial command: once the shell has printed something and
|
|
32
|
-
* then gone quiet, or after a fallback — the same heuristic the web terminal
|
|
33
|
-
* uses for its Sign in menu, because both are waiting on the same login bash.
|
|
34
|
-
*/
|
|
35
|
-
exports.TERMINAL_READY_QUIET_MS = 300;
|
|
36
|
-
exports.TERMINAL_READY_FALLBACK_MS = 2_500;
|
|
37
30
|
/** Bytes of one output frame, without splitting a UTF-8 or ANSI sequence. */
|
|
38
31
|
function decodeTerminalFrame(frame) {
|
|
39
32
|
const encoded = frame.dataBase64 ?? (frame.encoding === 'base64' ? frame.data : undefined);
|
|
@@ -43,54 +36,17 @@ function decodeTerminalFrame(frame) {
|
|
|
43
36
|
return Buffer.from(typeof frame.data === 'string' ? frame.data : '', 'utf8');
|
|
44
37
|
}
|
|
45
38
|
/**
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
49
|
-
* without a PTY. `onOutput` re-arms the quiet timer; the fallback fires
|
|
50
|
-
* regardless, because a shell with a silent prompt must not wait forever.
|
|
39
|
+
* What the terminal tells the user about an initial command that has not been
|
|
40
|
+
* typed. The web terminal shows the same two states with a button; here the
|
|
41
|
+
* user types it themselves.
|
|
51
42
|
*/
|
|
52
|
-
function
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
let fallbackTimer = null;
|
|
60
|
-
const type = () => {
|
|
61
|
-
if (typed)
|
|
62
|
-
return;
|
|
63
|
-
typed = true;
|
|
64
|
-
if (quietTimer)
|
|
65
|
-
clearTimer(quietTimer);
|
|
66
|
-
if (fallbackTimer)
|
|
67
|
-
clearTimer(fallbackTimer);
|
|
68
|
-
options.send(`${options.command}\r`);
|
|
69
|
-
};
|
|
70
|
-
return {
|
|
71
|
-
start() {
|
|
72
|
-
if (typed || fallbackTimer)
|
|
73
|
-
return;
|
|
74
|
-
fallbackTimer = setTimer(type, fallbackMs);
|
|
75
|
-
},
|
|
76
|
-
onOutput() {
|
|
77
|
-
if (typed)
|
|
78
|
-
return;
|
|
79
|
-
if (quietTimer)
|
|
80
|
-
clearTimer(quietTimer);
|
|
81
|
-
quietTimer = setTimer(type, quietMs);
|
|
82
|
-
},
|
|
83
|
-
cancel() {
|
|
84
|
-
typed = true;
|
|
85
|
-
if (quietTimer)
|
|
86
|
-
clearTimer(quietTimer);
|
|
87
|
-
if (fallbackTimer)
|
|
88
|
-
clearTimer(fallbackTimer);
|
|
89
|
-
},
|
|
90
|
-
get typed() {
|
|
91
|
-
return typed;
|
|
92
|
-
},
|
|
93
|
-
};
|
|
43
|
+
function describePendingCommand(status, command) {
|
|
44
|
+
if (status === 'overdue') {
|
|
45
|
+
return `[waiting for the shell prompt to type: ${command}; type anything and it is left to you]`;
|
|
46
|
+
}
|
|
47
|
+
if (status === 'yielded')
|
|
48
|
+
return `[not typing ${command}: you typed first]`;
|
|
49
|
+
return null;
|
|
94
50
|
}
|
|
95
51
|
class DedicatedTerminalError extends Error {
|
|
96
52
|
code;
|
|
@@ -143,10 +99,19 @@ function runDedicatedTerminal(options) {
|
|
|
143
99
|
// different process tree mid-command. Say it ended instead.
|
|
144
100
|
reconnection: false,
|
|
145
101
|
});
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
102
|
+
// Typed only once the shell's line editor says it is reading a command line
|
|
103
|
+
// (`createPendingShellCommand`, the rule the web terminal reads too). A
|
|
104
|
+
// login profile that sleeps or asks a question is never typed into.
|
|
105
|
+
const { initialCommand } = options;
|
|
106
|
+
const pending = initialCommand
|
|
107
|
+
? (0, data_provider_1.createPendingShellCommand)({
|
|
108
|
+
command: initialCommand,
|
|
149
109
|
send: (data) => socket.emit(events.input, { sessionId, data }),
|
|
110
|
+
onStatusChange: (status) => {
|
|
111
|
+
const notice = describePendingCommand(status, initialCommand);
|
|
112
|
+
if (notice && !finished)
|
|
113
|
+
errorOutput.write(`\r\n${notice}\r\n`);
|
|
114
|
+
},
|
|
150
115
|
})
|
|
151
116
|
: null;
|
|
152
117
|
const size = () => ({
|
|
@@ -162,26 +127,32 @@ function runDedicatedTerminal(options) {
|
|
|
162
127
|
const detachAt = buffer.indexOf(exports.TERMINAL_DETACH_BYTE);
|
|
163
128
|
if (detachAt >= 0) {
|
|
164
129
|
if (detachAt > 0) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
});
|
|
130
|
+
const typed = buffer.subarray(0, detachAt);
|
|
131
|
+
pending?.observeUserInput(typed);
|
|
132
|
+
socket.emit(events.input, { sessionId, data: typed.toString('utf8') });
|
|
169
133
|
}
|
|
170
134
|
errorOutput.write('\r\n[closed the terminal]\r\n');
|
|
171
135
|
finish(0, true);
|
|
172
136
|
return;
|
|
173
137
|
}
|
|
138
|
+
pending?.observeUserInput(buffer);
|
|
174
139
|
socket.emit(events.input, { sessionId, data: buffer.toString('utf8') });
|
|
175
140
|
};
|
|
176
|
-
|
|
141
|
+
// A terminal that closes (SIGHUP) or a supervisor that stops us (SIGTERM):
|
|
142
|
+
// put the TTY back, stop the remote shell, and exit 128 + the signal's
|
|
143
|
+
// number. Unreachable until OSK-8708 — auth-core's lock handler exited
|
|
144
|
+
// first and left the local TTY in raw mode — and it said 130, which is
|
|
145
|
+
// SIGINT's status, for both.
|
|
146
|
+
const onSigterm = () => finish(143, true);
|
|
147
|
+
const onSighup = () => finish(129, true);
|
|
177
148
|
const startTimer = setTimeout(() => {
|
|
178
149
|
fail(new DedicatedTerminalError('The machine did not answer the terminal start in time. Its daemon may be busy or offline.', 'TERMINAL_START_TIMEOUT'));
|
|
179
150
|
}, exports.TERMINAL_START_TIMEOUT_MS);
|
|
180
151
|
function restoreTty() {
|
|
181
152
|
input.off('data', onInput);
|
|
182
153
|
output.off('resize', onResize);
|
|
183
|
-
process.off('SIGTERM',
|
|
184
|
-
process.off('SIGHUP',
|
|
154
|
+
process.off('SIGTERM', onSigterm);
|
|
155
|
+
process.off('SIGHUP', onSighup);
|
|
185
156
|
if (rawMode && typeof input.setRawMode === 'function') {
|
|
186
157
|
input.setRawMode(false);
|
|
187
158
|
rawMode = false;
|
|
@@ -193,7 +164,7 @@ function runDedicatedTerminal(options) {
|
|
|
193
164
|
return;
|
|
194
165
|
finished = true;
|
|
195
166
|
clearTimeout(startTimer);
|
|
196
|
-
|
|
167
|
+
pending?.close();
|
|
197
168
|
restoreTty();
|
|
198
169
|
if (stopRemote && started)
|
|
199
170
|
socket.emit(events.stop, { sessionId });
|
|
@@ -205,7 +176,7 @@ function runDedicatedTerminal(options) {
|
|
|
205
176
|
return;
|
|
206
177
|
finished = true;
|
|
207
178
|
clearTimeout(startTimer);
|
|
208
|
-
|
|
179
|
+
pending?.close();
|
|
209
180
|
restoreTty();
|
|
210
181
|
socket.disconnect();
|
|
211
182
|
reject(error);
|
|
@@ -239,9 +210,9 @@ function runDedicatedTerminal(options) {
|
|
|
239
210
|
input.resume();
|
|
240
211
|
input.on('data', onInput);
|
|
241
212
|
output.on('resize', onResize);
|
|
242
|
-
process.on('SIGTERM',
|
|
243
|
-
process.on('SIGHUP',
|
|
244
|
-
|
|
213
|
+
process.on('SIGTERM', onSigterm);
|
|
214
|
+
process.on('SIGHUP', onSighup);
|
|
215
|
+
pending?.beginSession();
|
|
245
216
|
});
|
|
246
217
|
socket.on(events.output, (frame) => {
|
|
247
218
|
if (!frame || frame.sessionId !== sessionId || finished)
|
|
@@ -258,8 +229,9 @@ function runDedicatedTerminal(options) {
|
|
|
258
229
|
errorOutput.write(`\r\n[${frame.type}] ${text}\r\n`);
|
|
259
230
|
return;
|
|
260
231
|
}
|
|
261
|
-
|
|
262
|
-
|
|
232
|
+
const bytes = decodeTerminalFrame(frame);
|
|
233
|
+
output.write(bytes);
|
|
234
|
+
pending?.observeOutput(bytes);
|
|
263
235
|
});
|
|
264
236
|
socket.on('disconnect', () => {
|
|
265
237
|
if (finished)
|
|
@@ -20,6 +20,9 @@ export type DedicatedWaitGoal = {
|
|
|
20
20
|
} | {
|
|
21
21
|
kind: 'grown';
|
|
22
22
|
storageGb: number;
|
|
23
|
+
} | {
|
|
24
|
+
kind: 'image_updated';
|
|
25
|
+
previousUpdateStartedAt: string | null;
|
|
23
26
|
};
|
|
24
27
|
export type DedicatedWaitVerdict = {
|
|
25
28
|
status: 'done';
|
|
@@ -37,6 +40,13 @@ export type DedicatedWaitVerdict = {
|
|
|
37
40
|
* reading that cannot turn into the goal without someone acting again; anything
|
|
38
41
|
* that might still settle is `pending`, and the caller's timeout decides how long
|
|
39
42
|
* that is worth watching.
|
|
43
|
+
*
|
|
44
|
+
* Reaching the goal is not the end of the wait while the lease's operation is
|
|
45
|
+
* still in progress: `--wait` promises the operation has FINISHED, and the
|
|
46
|
+
* server refuses the next one until it has. A create read `ready` the moment its
|
|
47
|
+
* guest registered and returned, and the restart that followed was refused for
|
|
48
|
+
* minutes (OSK-8775). A server that does not report the operation is judged on
|
|
49
|
+
* the goal alone, as before.
|
|
40
50
|
*/
|
|
41
51
|
export declare function evaluateDedicatedWait(goal: DedicatedWaitGoal, lease: DedicatedLease | undefined | null): DedicatedWaitVerdict;
|
|
42
52
|
/**
|