@skrr-ai/cli 0.1.25 → 0.1.27
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 +29 -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 +76 -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/current.d.ts +19 -0
- package/dist/commands/machines/dedicated/current.js +49 -0
- package/dist/commands/machines/dedicated/destroy.d.ts +6 -2
- package/dist/commands/machines/dedicated/destroy.js +43 -25
- package/dist/commands/machines/dedicated/exec.d.ts +32 -0
- package/dist/commands/machines/dedicated/exec.js +122 -0
- package/dist/commands/machines/dedicated/grow.d.ts +6 -2
- package/dist/commands/machines/dedicated/grow.js +16 -20
- package/dist/commands/machines/dedicated/health-check.d.ts +2 -2
- package/dist/commands/machines/dedicated/health-check.js +18 -15
- package/dist/commands/machines/dedicated/incident-close.d.ts +3 -2
- package/dist/commands/machines/dedicated/incident-close.js +13 -15
- package/dist/commands/machines/dedicated/index.js +39 -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 +10 -20
- package/dist/commands/machines/dedicated/restore.d.ts +26 -0
- package/dist/commands/machines/dedicated/restore.js +64 -0
- package/dist/commands/machines/dedicated/show.d.ts +2 -2
- package/dist/commands/machines/dedicated/show.js +30 -31
- 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 +13 -20
- package/dist/commands/machines/dedicated/spending-limit.d.ts +22 -0
- package/dist/commands/machines/dedicated/spending-limit.js +51 -0
- package/dist/commands/machines/dedicated/start.d.ts +5 -2
- package/dist/commands/machines/dedicated/start.js +10 -20
- package/dist/commands/machines/dedicated/stop.d.ts +5 -2
- package/dist/commands/machines/dedicated/stop.js +10 -20
- 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-exec.d.ts +80 -0
- package/dist/lib/dedicated-exec.js +271 -0
- package/dist/lib/dedicated-lease-command.d.ts +59 -0
- package/dist/lib/dedicated-lease-command.js +181 -0
- package/dist/lib/dedicated-machines.d.ts +337 -12
- package/dist/lib/dedicated-machines.js +502 -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/edge-error-page.d.ts +49 -0
- package/dist/lib/edge-error-page.js +120 -0
- package/dist/lib/harnesses.d.ts +15 -1
- package/dist/lib/harnesses.js +20 -2
- package/dist/lib/hosted-machines.js +7 -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 +2625 -2608
- package/oclif.manifest.json +27851 -26936
- package/package.json +1 -1
|
@@ -1,20 +1,39 @@
|
|
|
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_DEFAULT_TIMEOUT_MS = 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.restoreDeletedDedicatedRuntime = restoreDeletedDedicatedRuntime;
|
|
12
|
+
exports.setDedicatedRuntimeSpendingLimit = setDedicatedRuntimeSpendingLimit;
|
|
10
13
|
exports.closeDedicatedRuntimeIncidentHold = closeDedicatedRuntimeIncidentHold;
|
|
11
14
|
exports.destroyDedicatedRuntime = destroyDedicatedRuntime;
|
|
15
|
+
exports.execDedicatedRuntime = execDedicatedRuntime;
|
|
16
|
+
exports.startDedicatedRuntimeExecution = startDedicatedRuntimeExecution;
|
|
17
|
+
exports.observeDedicatedRuntimeExecution = observeDedicatedRuntimeExecution;
|
|
18
|
+
exports.readDedicatedRuntimeFileChunk = readDedicatedRuntimeFileChunk;
|
|
19
|
+
exports.writeDedicatedRuntimeFileChunk = writeDedicatedRuntimeFileChunk;
|
|
20
|
+
exports.parseDedicatedCopyEndpoints = parseDedicatedCopyEndpoints;
|
|
12
21
|
exports.dedicatedLeaseId = dedicatedLeaseId;
|
|
13
22
|
exports.dedicatedLeaseState = dedicatedLeaseState;
|
|
14
23
|
exports.dedicatedLeaseDisplayName = dedicatedLeaseDisplayName;
|
|
24
|
+
exports.formatUsdCents = formatUsdCents;
|
|
25
|
+
exports.dedicatedSpendSummary = dedicatedSpendSummary;
|
|
26
|
+
exports.describeDedicatedDeletion = describeDedicatedDeletion;
|
|
27
|
+
exports.describeDedicatedLease = describeDedicatedLease;
|
|
28
|
+
exports.dedicatedLeaseHarnesses = dedicatedLeaseHarnesses;
|
|
29
|
+
exports.dedicatedLeaseIdForDaemon = dedicatedLeaseIdForDaemon;
|
|
30
|
+
exports.dedicatedTerminalHarness = dedicatedTerminalHarness;
|
|
31
|
+
exports.selectDedicatedAttachHarness = selectDedicatedAttachHarness;
|
|
32
|
+
exports.dedicatedCatalogFromProducts = dedicatedCatalogFromProducts;
|
|
33
|
+
exports.dedicatedRuntimeApiErrorCode = dedicatedRuntimeApiErrorCode;
|
|
15
34
|
exports.formatDedicatedRuntimeApiError = formatDedicatedRuntimeApiError;
|
|
16
35
|
const api_fetch_1 = require("./api-fetch");
|
|
17
|
-
const
|
|
36
|
+
const edge_error_page_1 = require("./edge-error-page");
|
|
18
37
|
exports.DEDICATED_LEASE_ACTIONS = [
|
|
19
38
|
'start',
|
|
20
39
|
'stop',
|
|
@@ -22,6 +41,7 @@ exports.DEDICATED_LEASE_ACTIONS = [
|
|
|
22
41
|
'snapshot',
|
|
23
42
|
'health-check',
|
|
24
43
|
];
|
|
44
|
+
exports.DEDICATED_MACHINE_PRODUCT = 'dedicated-machine';
|
|
25
45
|
/**
|
|
26
46
|
* Flags in, request body out — and the point of it being a function is the
|
|
27
47
|
* OMISSION.
|
|
@@ -36,6 +56,7 @@ exports.DEDICATED_LEASE_ACTIONS = [
|
|
|
36
56
|
* means the same bug was invisible in them.
|
|
37
57
|
*/
|
|
38
58
|
function dedicatedRuntimeCreateInput(input) {
|
|
59
|
+
const repositoryRef = input.repositoryRef?.trim();
|
|
39
60
|
return {
|
|
40
61
|
requestId: input.requestId,
|
|
41
62
|
displayName: input.displayName,
|
|
@@ -47,45 +68,102 @@ function dedicatedRuntimeCreateInput(input) {
|
|
|
47
68
|
...(input.backupProfile
|
|
48
69
|
? { backupProfile: input.backupProfile }
|
|
49
70
|
: {}),
|
|
71
|
+
...(repositoryRef ? { repositoryRef } : {}),
|
|
50
72
|
...(input.workspaceId ? { workspaceId: input.workspaceId } : {}),
|
|
51
73
|
};
|
|
52
74
|
}
|
|
75
|
+
/**
|
|
76
|
+
* The shapes the server's repository parser accepts: `owner/repo`,
|
|
77
|
+
* `github.com/owner/repo`, or an https URL to it, optionally ending in `.git`.
|
|
78
|
+
*
|
|
79
|
+
* This is a typo catcher, not the authority. The server re-parses the value and
|
|
80
|
+
* decides whether the owner's GitHub App installation covers it; checking the
|
|
81
|
+
* shape here only turns "create failed" into "that is not a repository" before a
|
|
82
|
+
* billable request is sent.
|
|
83
|
+
*/
|
|
84
|
+
function isDedicatedRepositoryRef(value) {
|
|
85
|
+
return /^(?:https?:\/\/)?(?:github\.com\/)?[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?(?:\.git)?$/i.test(value.trim());
|
|
86
|
+
}
|
|
87
|
+
// Takes the finished path rather than composing one from a suffix, so the route
|
|
88
|
+
// each call reaches is a literal at the call site. `cli/src/__tests__/helpers/
|
|
89
|
+
// api-surface.ts` reads coverage out of the source, and a path assembled inside a
|
|
90
|
+
// builder is invisible to it: exec, files/read and files/write were all reported
|
|
91
|
+
// unreachable by domain-api-parity while being perfectly reachable, which is the
|
|
92
|
+
// reading that trains people to allowlist things that are fine. Same shape as
|
|
93
|
+
// `withQuery` on the `request.*` side.
|
|
94
|
+
//
|
|
95
|
+
// Query parameters therefore go in `query`, never into the literal: the coverage
|
|
96
|
+
// reader matches the literal against the route table, and a `?name=` inside it
|
|
97
|
+
// makes the endpoint read as unreachable.
|
|
98
|
+
function withDedicatedScope(path, scope, query = {}) {
|
|
99
|
+
const params = Object.entries(query);
|
|
100
|
+
if (scope.workspaceId)
|
|
101
|
+
params.push(['workspaceId', scope.workspaceId]);
|
|
102
|
+
if (params.length === 0)
|
|
103
|
+
return path;
|
|
104
|
+
const separator = path.includes('?') ? '&' : '?';
|
|
105
|
+
return `${path}${separator}${params
|
|
106
|
+
.map(([name, value]) => `${encodeURIComponent(name)}=${encodeURIComponent(value)}`)
|
|
107
|
+
.join('&')}`;
|
|
108
|
+
}
|
|
53
109
|
async function createDedicatedRuntime(input) {
|
|
54
110
|
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
|
-
});
|
|
111
|
+
return (0, api_fetch_1.apiFetch)(withDedicatedScope('/api/machines/dedicated', { workspaceId }), { method: 'POST', body, headers: { 'Idempotency-Key': requestId } });
|
|
67
112
|
}
|
|
68
113
|
async function listDedicatedRuntimes(options = {}) {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
114
|
+
const query = new URLSearchParams();
|
|
115
|
+
if (options.limit)
|
|
116
|
+
query.set('limit', String(options.limit));
|
|
117
|
+
if (options.state)
|
|
118
|
+
query.set('state', options.state);
|
|
119
|
+
query.set('machineProduct', exports.DEDICATED_MACHINE_PRODUCT);
|
|
120
|
+
return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/leases?${query.toString()}`, options));
|
|
73
121
|
}
|
|
74
|
-
async function getDedicatedRuntime(leaseId) {
|
|
75
|
-
return (0,
|
|
122
|
+
async function getDedicatedRuntime(leaseId, scope = {}) {
|
|
123
|
+
return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/leases/${encodeURIComponent(leaseId)}`, scope));
|
|
76
124
|
}
|
|
77
|
-
async function performDedicatedLeaseAction(leaseId, action) {
|
|
78
|
-
return (0,
|
|
125
|
+
async function performDedicatedLeaseAction(leaseId, action, scope = {}) {
|
|
126
|
+
return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/leases/${encodeURIComponent(leaseId)}/actions/${encodeURIComponent(action)}`, scope), { method: 'POST' });
|
|
79
127
|
}
|
|
80
128
|
async function growDedicatedRuntime(leaseId, storageGb, options = {}) {
|
|
81
|
-
return (0, api_fetch_1.apiFetch)(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/storage`, {
|
|
129
|
+
return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/storage`, options), {
|
|
82
130
|
method: 'POST',
|
|
83
131
|
body: { storageGb },
|
|
84
132
|
...(options.requestId ? { headers: { 'Idempotency-Key': options.requestId } } : {}),
|
|
85
133
|
});
|
|
86
134
|
}
|
|
135
|
+
/**
|
|
136
|
+
* Restore a deleted Dedicated Runtime into a NEW machine, from the final recovery
|
|
137
|
+
* point its deletion kept. Only inside the deletion's recovery window, and once
|
|
138
|
+
* per deletion; the response's `lease` is the new machine. The cap defaults to
|
|
139
|
+
* the deleted machine's.
|
|
140
|
+
*/
|
|
141
|
+
async function restoreDeletedDedicatedRuntime(leaseId, options = {}) {
|
|
142
|
+
const body = {};
|
|
143
|
+
if (options.displayName)
|
|
144
|
+
body.displayName = options.displayName;
|
|
145
|
+
if (options.monthlySpendingLimitCents !== undefined) {
|
|
146
|
+
body.monthlySpendingLimitCents = options.monthlySpendingLimitCents;
|
|
147
|
+
}
|
|
148
|
+
return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/restore`, options), {
|
|
149
|
+
method: 'POST',
|
|
150
|
+
body,
|
|
151
|
+
...(options.requestId ? { headers: { 'Idempotency-Key': options.requestId } } : {}),
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Change the monthly spending cap of one lease.
|
|
156
|
+
*
|
|
157
|
+
* The route accepts `spendingLimitCents` and nothing else: who pays is decided
|
|
158
|
+
* at create and there is no API that moves it. A cap below what the month has
|
|
159
|
+
* already spent (or is projected to) is refused rather than stopping the machine
|
|
160
|
+
* on the next reconciler pass.
|
|
161
|
+
*/
|
|
162
|
+
async function setDedicatedRuntimeSpendingLimit(leaseId, spendingLimitCents, scope = {}) {
|
|
163
|
+
return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/leases/${encodeURIComponent(leaseId)}/billing`, scope), { method: 'PATCH', body: { spendingLimitCents } });
|
|
164
|
+
}
|
|
87
165
|
async function closeDedicatedRuntimeIncidentHold(leaseId, options = {}) {
|
|
88
|
-
return (0, api_fetch_1.apiFetch)(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/incident-hold/close`, {
|
|
166
|
+
return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/incident-hold/close`, options), {
|
|
89
167
|
method: 'POST',
|
|
90
168
|
body: {},
|
|
91
169
|
...(options.requestId ? { headers: { 'Idempotency-Key': options.requestId } } : {}),
|
|
@@ -95,7 +173,86 @@ async function destroyDedicatedRuntime(leaseId, options = {}) {
|
|
|
95
173
|
const query = options.retentionIntent
|
|
96
174
|
? `?retentionIntent=${encodeURIComponent(options.retentionIntent)}`
|
|
97
175
|
: '';
|
|
98
|
-
return (0, api_fetch_1.apiFetch)(`/api/machines/leases/${encodeURIComponent(leaseId)}${query}`, { method: 'DELETE' });
|
|
176
|
+
return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/leases/${encodeURIComponent(leaseId)}${query}`, options), { method: 'DELETE' });
|
|
177
|
+
}
|
|
178
|
+
/** The server's ceiling for one exec; documented beside the route. */
|
|
179
|
+
exports.DEDICATED_EXEC_MAX_TIMEOUT_MS = 10 * 60 * 1000;
|
|
180
|
+
/** The server's default when a command names no timeout. */
|
|
181
|
+
exports.DEDICATED_EXEC_DEFAULT_TIMEOUT_MS = 2 * 60 * 1000;
|
|
182
|
+
/**
|
|
183
|
+
* Run one command on the machine as the workload user, in ONE request.
|
|
184
|
+
*
|
|
185
|
+
* The synchronous route, kept for servers that predate `executions`. It holds
|
|
186
|
+
* the request open while the command runs, so behind CloudFront a command that
|
|
187
|
+
* runs past ~60 s is cut off at the edge while it keeps running (OSK-8705).
|
|
188
|
+
* `runDedicatedExec` (dedicated-exec.ts) starts and observes instead, and falls
|
|
189
|
+
* back to this only when the server has no `executions` route.
|
|
190
|
+
*
|
|
191
|
+
* Not idempotent and not retried: `apiFetch` gives each call a fresh
|
|
192
|
+
* idempotency key, which is the rule for an unfenced write — running the same
|
|
193
|
+
* command twice is two intents even when the text is identical.
|
|
194
|
+
*/
|
|
195
|
+
async function execDedicatedRuntime(leaseId, input, scope = {}) {
|
|
196
|
+
return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/exec`, scope), {
|
|
197
|
+
method: 'POST',
|
|
198
|
+
body: {
|
|
199
|
+
command: input.command,
|
|
200
|
+
...(input.cwd ? { cwd: input.cwd } : {}),
|
|
201
|
+
...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
|
|
202
|
+
},
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
/**
|
|
206
|
+
* Start one command. `requestId` is the idempotency key: the same id, for the
|
|
207
|
+
* same command on the same lease, returns the execution it already started —
|
|
208
|
+
* so a retry after a lost response never runs the command twice.
|
|
209
|
+
*/
|
|
210
|
+
async function startDedicatedRuntimeExecution(leaseId, input, options) {
|
|
211
|
+
return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/executions`, options), {
|
|
212
|
+
method: 'POST',
|
|
213
|
+
body: {
|
|
214
|
+
command: input.command,
|
|
215
|
+
...(input.cwd ? { cwd: input.cwd } : {}),
|
|
216
|
+
...(input.timeoutMs !== undefined ? { timeoutMs: input.timeoutMs } : {}),
|
|
217
|
+
},
|
|
218
|
+
headers: { 'X-Idempotency-Key': options.requestId },
|
|
219
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
/** Read a started command, letting the server hold the answer up to `waitMs` for it to end. */
|
|
223
|
+
async function observeDedicatedRuntimeExecution(leaseId, executionId, options) {
|
|
224
|
+
return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/executions/${encodeURIComponent(executionId)}`, options, { waitMs: String(Math.max(0, Math.floor(options.waitMs))) }), options.signal ? { signal: options.signal } : {});
|
|
225
|
+
}
|
|
226
|
+
/** Mirrors the daemon's bounds (daemon/src/tools/file-transfer.ts). */
|
|
227
|
+
exports.DEDICATED_FILE_CHUNK_BYTES = 512 * 1024;
|
|
228
|
+
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 });
|
|
231
|
+
}
|
|
232
|
+
async function writeDedicatedRuntimeFileChunk(leaseId, input, scope = {}) {
|
|
233
|
+
return (0, api_fetch_1.apiFetch)(withDedicatedScope(`/api/machines/dedicated/${encodeURIComponent(leaseId)}/files/write`, scope), { method: 'POST', body: input });
|
|
234
|
+
}
|
|
235
|
+
const REMOTE_ENDPOINT = /^(rtl_[A-Za-z0-9_-]{1,80}):(.+)$/;
|
|
236
|
+
/**
|
|
237
|
+
* `cp <src> <dst>` where exactly one side is `<lease-id>:<path>`.
|
|
238
|
+
*
|
|
239
|
+
* The remote side is recognised by the lease id prefix rather than by "has a
|
|
240
|
+
* colon", so a Windows path (`C:\\build\\out.zip`) or a local file whose name
|
|
241
|
+
* contains a colon is never mistaken for a machine.
|
|
242
|
+
*/
|
|
243
|
+
function parseDedicatedCopyEndpoints(source, destination) {
|
|
244
|
+
const src = REMOTE_ENDPOINT.exec(source);
|
|
245
|
+
const dst = REMOTE_ENDPOINT.exec(destination);
|
|
246
|
+
if (src && dst) {
|
|
247
|
+
throw new Error('Copy between two machines is not supported; one side must be a local path.');
|
|
248
|
+
}
|
|
249
|
+
if (!src && !dst) {
|
|
250
|
+
throw new Error('One side must name the machine, as <lease-id>:<path>.');
|
|
251
|
+
}
|
|
252
|
+
if (src) {
|
|
253
|
+
return { direction: 'download', leaseId: src[1], remotePath: src[2], localPath: destination };
|
|
254
|
+
}
|
|
255
|
+
return { direction: 'upload', localPath: source, leaseId: dst[1], remotePath: dst[2] };
|
|
99
256
|
}
|
|
100
257
|
function dedicatedLeaseId(lease) {
|
|
101
258
|
return String(lease?.id ?? '');
|
|
@@ -106,6 +263,268 @@ function dedicatedLeaseState(lease) {
|
|
|
106
263
|
function dedicatedLeaseDisplayName(lease) {
|
|
107
264
|
return lease?.displayName ? String(lease.displayName) : '-';
|
|
108
265
|
}
|
|
266
|
+
/** `$12.34` — cents are what the API speaks; dollars are what a person reads. */
|
|
267
|
+
function formatUsdCents(cents) {
|
|
268
|
+
if (typeof cents !== 'number' || !Number.isFinite(cents))
|
|
269
|
+
return '-';
|
|
270
|
+
const sign = cents < 0 ? '-' : '';
|
|
271
|
+
return `${sign}$${(Math.abs(cents) / 100).toFixed(2)}`;
|
|
272
|
+
}
|
|
273
|
+
/** `this month $1.23 of $500.00 cap`, or why that sentence cannot be written. */
|
|
274
|
+
function dedicatedSpendSummary(lease) {
|
|
275
|
+
const billing = lease.billing;
|
|
276
|
+
if (!billing)
|
|
277
|
+
return '-';
|
|
278
|
+
const cap = billing.monthlySpendLimitCents ?? billing.spendingLimitCents;
|
|
279
|
+
if (billing.spendStatusUnavailable) {
|
|
280
|
+
return `spend unavailable right now · cap ${formatUsdCents(cap)}`;
|
|
281
|
+
}
|
|
282
|
+
const spent = billing.currentMonthSpendCents;
|
|
283
|
+
const projected = billing.projectedCurrentMonthSpendCents;
|
|
284
|
+
const parts = [`${formatUsdCents(spent)} this month of ${formatUsdCents(cap)} cap`];
|
|
285
|
+
if (typeof projected === 'number' && projected !== spent) {
|
|
286
|
+
parts.push(`projected ${formatUsdCents(projected)}`);
|
|
287
|
+
}
|
|
288
|
+
if (billing.spendingLimitExceeded) {
|
|
289
|
+
parts.push('OVER CAP — compute is stopped, files are kept');
|
|
290
|
+
}
|
|
291
|
+
return parts.join(' · ');
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* The human rendering of `machines dedicated show`, as lines.
|
|
295
|
+
*
|
|
296
|
+
* Pure so it can be pinned without a server. Every field is optional because a
|
|
297
|
+
* lease that is still `requested` has no resources, daemon or spend yet, and a
|
|
298
|
+
* missing value is omitted rather than printed as a column of dashes.
|
|
299
|
+
*/
|
|
300
|
+
/** One line saying what a deletion still allows. */
|
|
301
|
+
function describeDedicatedDeletion(deletion, leaseId) {
|
|
302
|
+
if (deletion.restoredToLeaseId) {
|
|
303
|
+
return `restored into ${deletion.restoredToLeaseId}`;
|
|
304
|
+
}
|
|
305
|
+
if (deletion.status === 'completed') {
|
|
306
|
+
return `permanently deleted${deletion.completedAt ? ` ${deletion.completedAt}` : ''}; no copy remains`;
|
|
307
|
+
}
|
|
308
|
+
if (deletion.status === 'purging') {
|
|
309
|
+
return 'recovery window closed; every copy is being deleted';
|
|
310
|
+
}
|
|
311
|
+
if (deletion.restorable) {
|
|
312
|
+
return `restorable until ${deletion.recoverableUntil} — \`skrr machines dedicated restore ${leaseId || '<lease-id>'}\``;
|
|
313
|
+
}
|
|
314
|
+
return 'nothing to restore; every copy is deleted when the window closes';
|
|
315
|
+
}
|
|
316
|
+
function describeDedicatedLease(lease, harnesses = []) {
|
|
317
|
+
const lines = [dedicatedLeaseDisplayName(lease)];
|
|
318
|
+
const row = (label, value) => {
|
|
319
|
+
if (value === undefined || value === null || value === '')
|
|
320
|
+
return;
|
|
321
|
+
lines.push(` ${label.padEnd(13)}${value}`);
|
|
322
|
+
};
|
|
323
|
+
row('id', dedicatedLeaseId(lease));
|
|
324
|
+
row('state', lease.stateReason
|
|
325
|
+
? `${dedicatedLeaseState(lease)} (${lease.stateReason})`
|
|
326
|
+
: dedicatedLeaseState(lease));
|
|
327
|
+
const resources = lease.resources || {};
|
|
328
|
+
const size = [
|
|
329
|
+
resources.sizePreset,
|
|
330
|
+
resources.cpu ? `${resources.cpu} vCPU` : '',
|
|
331
|
+
resources.memoryGb ? `${resources.memoryGb} GiB RAM` : '',
|
|
332
|
+
resources.regionClass,
|
|
333
|
+
]
|
|
334
|
+
.filter(Boolean)
|
|
335
|
+
.join(' · ');
|
|
336
|
+
row('size', size);
|
|
337
|
+
const storage = lease.storage || {};
|
|
338
|
+
const disk = [
|
|
339
|
+
resources.storageGb ? `${resources.storageGb} GiB` : '',
|
|
340
|
+
storage.retentionClass ? `retention ${storage.retentionClass}` : '',
|
|
341
|
+
storage.backupProfile ? `backup ${storage.backupProfile}` : '',
|
|
342
|
+
]
|
|
343
|
+
.filter(Boolean)
|
|
344
|
+
.join(' · ');
|
|
345
|
+
row('workspace', disk);
|
|
346
|
+
row('last backup', storage.lastBackupAt);
|
|
347
|
+
const image = [resources.imageVersion || lease.daemon?.imageVersion].filter(Boolean).join('');
|
|
348
|
+
row('image', image);
|
|
349
|
+
if (lease.daemon?.version || lease.daemon?.lastHeartbeatAt) {
|
|
350
|
+
row('daemon', [
|
|
351
|
+
lease.daemon.version ? `skrrd ${lease.daemon.version}` : '',
|
|
352
|
+
lease.daemon.lastHeartbeatAt ? `heartbeat ${lease.daemon.lastHeartbeatAt}` : '',
|
|
353
|
+
]
|
|
354
|
+
.filter(Boolean)
|
|
355
|
+
.join(' · '));
|
|
356
|
+
}
|
|
357
|
+
const health = lease.health;
|
|
358
|
+
if (health && typeof health.assignable === 'boolean') {
|
|
359
|
+
row('health', health.assignable
|
|
360
|
+
? 'can take work'
|
|
361
|
+
: `not assignable${health.reasons?.length ? `: ${[...new Set(health.reasons)].join(', ')}` : ''}`);
|
|
362
|
+
}
|
|
363
|
+
row('spend', lease.billing ? dedicatedSpendSummary(lease) : undefined);
|
|
364
|
+
if (storage.deleteAfter || storage.unpaidSince) {
|
|
365
|
+
row('deletion', [
|
|
366
|
+
storage.unpaidSince ? `unpaid since ${storage.unpaidSince}` : '',
|
|
367
|
+
storage.deletionNoticeAt ? `notice ${storage.deletionNoticeAt}` : '',
|
|
368
|
+
storage.deleteAfter ? `workspace deleted after ${storage.deleteAfter}` : '',
|
|
369
|
+
]
|
|
370
|
+
.filter(Boolean)
|
|
371
|
+
.join(' · '));
|
|
372
|
+
}
|
|
373
|
+
const deletion = storage.deletion;
|
|
374
|
+
if (deletion?.status) {
|
|
375
|
+
row('deleted', describeDedicatedDeletion(deletion, dedicatedLeaseId(lease)));
|
|
376
|
+
}
|
|
377
|
+
row('restored from', lease.recovery?.restoredFromLeaseId);
|
|
378
|
+
const hold = lease.recovery?.incidentHold;
|
|
379
|
+
if (hold?.status === 'open') {
|
|
380
|
+
row('incident', `hold open since ${hold.heldAt || '?'} — the failed source volume is billed until \`incident-close\``);
|
|
381
|
+
}
|
|
382
|
+
const engines = dedicatedLeaseHarnesses(lease, harnesses);
|
|
383
|
+
if (engines.length > 0) {
|
|
384
|
+
row('engines', engines.map((harness) => `${harness.provider} (${harness.status || 'unknown'})`).join(', '));
|
|
385
|
+
}
|
|
386
|
+
// What the PRODUCT supports, not what this state allows: an archived lease
|
|
387
|
+
// still lists `start`. Labelled so nobody scripts against it as the latter.
|
|
388
|
+
const capabilities = Object.entries(lease.lifecycleCapabilities || {})
|
|
389
|
+
.filter(([, allowed]) => allowed)
|
|
390
|
+
.map(([name]) => name);
|
|
391
|
+
row('supports', capabilities.join(', '));
|
|
392
|
+
row('support ref', lease.supportReference);
|
|
393
|
+
return lines;
|
|
394
|
+
}
|
|
395
|
+
/**
|
|
396
|
+
* Every harness row a Dedicated Runtime lease's daemon registered, freshest
|
|
397
|
+
* first.
|
|
398
|
+
*
|
|
399
|
+
* A lease payload names no daemon (provider and daemon ids are deliberately
|
|
400
|
+
* absent), so the join goes the other way: each row a guest daemon registers
|
|
401
|
+
* carries `metadata.machineLeaseId` and `metadata.machineProduct`. One daemon
|
|
402
|
+
* writes one row PER ENGINE it announces (claude, codex, sky-code, or `oversky`
|
|
403
|
+
* when it announces none), all sharing the daemon id.
|
|
404
|
+
*/
|
|
405
|
+
function dedicatedLeaseHarnesses(lease, harnesses) {
|
|
406
|
+
const seenAt = (harness) => Date.parse(harness.lastSeenAt || '') || 0;
|
|
407
|
+
return harnesses
|
|
408
|
+
.filter((harness) => {
|
|
409
|
+
const metadata = harness.metadata || {};
|
|
410
|
+
return (metadata.machineProduct === exports.DEDICATED_MACHINE_PRODUCT &&
|
|
411
|
+
metadata.machineLeaseId === lease.id);
|
|
412
|
+
})
|
|
413
|
+
.sort((a, b) => seenAt(b) - seenAt(a));
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* The lease of the machine this process is running on, from the harness rows.
|
|
417
|
+
*
|
|
418
|
+
* Nothing in a workload process's environment names its lease — the daemon's
|
|
419
|
+
* environment (lease, generation, credentials) sits on the other side of the uid
|
|
420
|
+
* split by design. The CLI hand-off descriptor the daemon publishes to the
|
|
421
|
+
* workload group does carry the daemon's id, and every harness row that daemon
|
|
422
|
+
* registered carries `metadata.machineLeaseId`. Joining the two answers "which
|
|
423
|
+
* machine am I" with no new daemon surface.
|
|
424
|
+
*/
|
|
425
|
+
function dedicatedLeaseIdForDaemon(daemonId, harnesses) {
|
|
426
|
+
if (!daemonId)
|
|
427
|
+
return null;
|
|
428
|
+
const leaseIds = new Set(harnesses
|
|
429
|
+
.filter((harness) => harness.daemonId === daemonId &&
|
|
430
|
+
harness.metadata?.machineProduct === exports.DEDICATED_MACHINE_PRODUCT &&
|
|
431
|
+
typeof harness.metadata?.machineLeaseId === 'string')
|
|
432
|
+
.map((harness) => String(harness.metadata?.machineLeaseId)));
|
|
433
|
+
// One daemon serves one lease. Two would mean rows from a replaced
|
|
434
|
+
// generation disagree, and guessing between them is how `self` would act on
|
|
435
|
+
// the wrong machine.
|
|
436
|
+
return leaseIds.size === 1 ? [...leaseIds][0] : null;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* The row an interactive terminal opens on: online, local, with a daemon.
|
|
440
|
+
*
|
|
441
|
+
* Any engine row reaches the same shell — they share the daemon — so the
|
|
442
|
+
* freshest one wins. The terminal relay's own access check refuses a row that
|
|
443
|
+
* is not local or has no daemon, so offering one here would only move the
|
|
444
|
+
* refusal later.
|
|
445
|
+
*/
|
|
446
|
+
function dedicatedTerminalHarness(lease, harnesses) {
|
|
447
|
+
return (dedicatedLeaseHarnesses(lease, harnesses).find((harness) => harness.status === 'online' &&
|
|
448
|
+
Boolean(harness.daemonId) &&
|
|
449
|
+
(harness.harnessLocation === undefined || harness.harnessLocation === 'local')) || null);
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* The harness row an agent should be attached to, for `attach`.
|
|
453
|
+
*
|
|
454
|
+
* Choosing a row also chooses the ENGINE the agent's turns run under, so an
|
|
455
|
+
* ambiguous choice is refused with the options named rather than guessed. A
|
|
456
|
+
* row need not be online to be attached — a stopped machine keeps its rows —
|
|
457
|
+
* but the error says so when it is not.
|
|
458
|
+
*/
|
|
459
|
+
function selectDedicatedAttachHarness(lease, harnesses, engine) {
|
|
460
|
+
const rows = dedicatedLeaseHarnesses(lease, harnesses);
|
|
461
|
+
if (rows.length === 0) {
|
|
462
|
+
return {
|
|
463
|
+
error: `No harness is registered for ${lease.id} yet. A machine's daemon registers one when it ` +
|
|
464
|
+
'first connects; start the machine, wait for it to be ready, and try again.',
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
const providers = [...new Set(rows.map((row) => row.provider))];
|
|
468
|
+
if (engine) {
|
|
469
|
+
const match = rows.find((row) => row.provider === engine);
|
|
470
|
+
if (!match) {
|
|
471
|
+
return {
|
|
472
|
+
error: `${lease.id} has no "${engine}" engine. Registered: ${providers.join(', ')}.`,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
return { harness: match };
|
|
476
|
+
}
|
|
477
|
+
if (providers.length > 1) {
|
|
478
|
+
return {
|
|
479
|
+
error: `${lease.id} runs more than one engine (${providers.join(', ')}); the one you attach is ` +
|
|
480
|
+
'the one the agent works under. Pass --engine <name>.',
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
return { harness: rows[0] };
|
|
484
|
+
}
|
|
485
|
+
/**
|
|
486
|
+
* The Dedicated Runtime row of `GET /api/machines/products`, reduced to what a
|
|
487
|
+
* caller needs to choose `create` flags.
|
|
488
|
+
*
|
|
489
|
+
* `--size-preset` has no client-side list of values on purpose — presets are
|
|
490
|
+
* gated by server flags — so this is where a caller learns which ones exist.
|
|
491
|
+
*/
|
|
492
|
+
function dedicatedCatalogFromProducts(payload) {
|
|
493
|
+
const products = Array.isArray(payload)
|
|
494
|
+
? payload
|
|
495
|
+
: (payload?.products ?? []);
|
|
496
|
+
const product = products.find((row) => row?.machineProduct === exports.DEDICATED_MACHINE_PRODUCT || row?.id === exports.DEDICATED_MACHINE_PRODUCT);
|
|
497
|
+
if (!product)
|
|
498
|
+
return null;
|
|
499
|
+
const configuration = (product.configuration || {});
|
|
500
|
+
const billing = (product.billing || {});
|
|
501
|
+
const presentation = (billing.presentation || {});
|
|
502
|
+
const sizeCatalog = Array.isArray(configuration.sizePresetCatalog)
|
|
503
|
+
? configuration.sizePresetCatalog
|
|
504
|
+
: (Array.isArray(configuration.sizePresets) ? configuration.sizePresets : []).map((id) => ({ id }));
|
|
505
|
+
const availability = typeof product.availability === 'string'
|
|
506
|
+
? product.availability
|
|
507
|
+
: String(product.availability?.status || 'unknown');
|
|
508
|
+
return {
|
|
509
|
+
availability,
|
|
510
|
+
...(typeof product.disabledReason === 'string' && product.disabledReason
|
|
511
|
+
? { disabledReason: product.disabledReason }
|
|
512
|
+
: {}),
|
|
513
|
+
sizes: sizeCatalog,
|
|
514
|
+
regionClasses: configuration.regionClasses || [],
|
|
515
|
+
...(configuration.storage
|
|
516
|
+
? { storage: configuration.storage }
|
|
517
|
+
: {}),
|
|
518
|
+
retentionClasses: configuration.retentionClasses || [],
|
|
519
|
+
backupProfiles: configuration.backupProfiles || [],
|
|
520
|
+
defaultMonthlySpendingLimitCents: billing.defaultMonthlySpendingLimitCents,
|
|
521
|
+
maxUserMonthlySpendingLimitCents: billing.maxUserMonthlySpendingLimitCents,
|
|
522
|
+
priceLines: Array.isArray(presentation.priceLines)
|
|
523
|
+
? presentation.priceLines
|
|
524
|
+
: [],
|
|
525
|
+
...(typeof presentation.headline === 'string' ? { headline: presentation.headline } : {}),
|
|
526
|
+
};
|
|
527
|
+
}
|
|
109
528
|
/**
|
|
110
529
|
* Dedicated Runtime commands are unusually likely to meet a server that has
|
|
111
530
|
* never heard of them.
|
|
@@ -140,6 +559,54 @@ function describeMissingEndpoint(err) {
|
|
|
140
559
|
'the deployment — `skrr context` names the server it is talking to, and `skrr machines ' +
|
|
141
560
|
'dedicated list` will still work if the feature is deployed at all.');
|
|
142
561
|
}
|
|
562
|
+
/**
|
|
563
|
+
* A lease that is not found is usually a SCOPE question, not a typo.
|
|
564
|
+
*
|
|
565
|
+
* Every lease lookup is `(owner, workspace)`: with a workspace selected the
|
|
566
|
+
* server searches only that workspace's leases, and without one only personal
|
|
567
|
+
* leases. So `not found` for an id the caller just created most often means the
|
|
568
|
+
* active workspace differs from the one the machine was created in.
|
|
569
|
+
*/
|
|
570
|
+
function describeLeaseNotFound(err) {
|
|
571
|
+
if (err.status !== 404)
|
|
572
|
+
return null;
|
|
573
|
+
try {
|
|
574
|
+
const parsed = JSON.parse(err.body || '');
|
|
575
|
+
if (parsed.code || parsed.error !== 'Machine lease not found')
|
|
576
|
+
return null;
|
|
577
|
+
}
|
|
578
|
+
catch {
|
|
579
|
+
return null;
|
|
580
|
+
}
|
|
581
|
+
return ('Machine lease not found. Leases are looked up within one scope: your personal machines, or ' +
|
|
582
|
+
'the workspace you name. If you created it with --workspace, pass the same --workspace here; ' +
|
|
583
|
+
'`skrr machines dedicated list` shows what this scope can see.');
|
|
584
|
+
}
|
|
585
|
+
/**
|
|
586
|
+
* The product refusal code the server sent (`DEDICATED_RUNTIME_NOT_ASSIGNABLE`,
|
|
587
|
+
* …), for `this.error(message, { code })`. Without it a `--json` caller got the
|
|
588
|
+
* generic `CLI_ERROR` and could only branch on prose (OSK-8685). Falls back to
|
|
589
|
+
* `HTTP_<status>`, the same code `handleApiError` uses for a body with none.
|
|
590
|
+
*/
|
|
591
|
+
function dedicatedRuntimeApiErrorCode(err) {
|
|
592
|
+
if (!(err instanceof api_fetch_1.ApiFetchError)) {
|
|
593
|
+
return undefined;
|
|
594
|
+
}
|
|
595
|
+
const edge = (0, edge_error_page_1.describeEdgeErrorPage)({ status: err.status, body: err.body, method: err.method });
|
|
596
|
+
if (edge) {
|
|
597
|
+
return edge.code;
|
|
598
|
+
}
|
|
599
|
+
try {
|
|
600
|
+
const parsed = JSON.parse(err.body || '');
|
|
601
|
+
if (typeof parsed.code === 'string' && parsed.code) {
|
|
602
|
+
return parsed.code;
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
catch {
|
|
606
|
+
// Unparseable body: no product code.
|
|
607
|
+
}
|
|
608
|
+
return err.status ? `HTTP_${err.status}` : undefined;
|
|
609
|
+
}
|
|
143
610
|
function formatDedicatedRuntimeApiError(err) {
|
|
144
611
|
if (!(err instanceof api_fetch_1.ApiFetchError) || !err.body) {
|
|
145
612
|
return null;
|
|
@@ -147,6 +614,16 @@ function formatDedicatedRuntimeApiError(err) {
|
|
|
147
614
|
if (err.status === 401) {
|
|
148
615
|
return null;
|
|
149
616
|
}
|
|
617
|
+
// An HTML page is the edge answering in the API's place — a WAF block or a
|
|
618
|
+
// gateway timeout. Printing its markup told nobody anything (OSK-8700).
|
|
619
|
+
const edge = (0, edge_error_page_1.describeEdgeErrorPage)({ status: err.status, body: err.body, method: err.method });
|
|
620
|
+
if (edge) {
|
|
621
|
+
return edge.message;
|
|
622
|
+
}
|
|
623
|
+
const notFound = describeLeaseNotFound(err);
|
|
624
|
+
if (notFound) {
|
|
625
|
+
return notFound;
|
|
626
|
+
}
|
|
150
627
|
const missingEndpoint = describeMissingEndpoint(err);
|
|
151
628
|
if (missingEndpoint) {
|
|
152
629
|
return missingEndpoint;
|