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