@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
|
@@ -0,0 +1,271 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DedicatedExecIdempotencyUnsupportedError = exports.DedicatedExecStartUnconfirmedError = exports.DedicatedExecContactLostError = exports.EXEC_REQUEST_ID_PATTERN = exports.EXEC_OBSERVE_WAIT_MS = void 0;
|
|
4
|
+
exports.newExecRequestId = newExecRequestId;
|
|
5
|
+
exports.isMissingExecutionsEndpoint = isMissingExecutionsEndpoint;
|
|
6
|
+
exports.runDedicatedExec = runDedicatedExec;
|
|
7
|
+
exports.describeTruncation = describeTruncation;
|
|
8
|
+
const node_crypto_1 = require("node:crypto");
|
|
9
|
+
const api_fetch_1 = require("./api-fetch");
|
|
10
|
+
const dedicated_machines_1 = require("./dedicated-machines");
|
|
11
|
+
/**
|
|
12
|
+
* `skrr machines dedicated exec`, as a start and a series of short observations
|
|
13
|
+
* (OSK-8705).
|
|
14
|
+
*
|
|
15
|
+
* The command used to be one request held open for as long as it ran. Behind
|
|
16
|
+
* CloudFront that request died at 60 s with an HTML 504 while the command kept
|
|
17
|
+
* running, and the page's advice — try again — ran it twice. Now no request
|
|
18
|
+
* lasts longer than one bounded observation, so no proxy timeout in front of
|
|
19
|
+
* the API decides whether the caller learns the exit code:
|
|
20
|
+
*
|
|
21
|
+
* 1. START under one idempotency key. A start whose answer never arrived (a
|
|
22
|
+
* dropped connection, a gateway page) is sent again with the SAME key, and
|
|
23
|
+
* the server returns the execution it already started — never a second run.
|
|
24
|
+
* 2. OBSERVE, letting the server hold each answer up to 20 s, until the
|
|
25
|
+
* command ends. A failed observation is only a failed read; it is retried.
|
|
26
|
+
* 3. PRINT exactly what the synchronous route printed: the same result, the
|
|
27
|
+
* same refusal body, the same exit code.
|
|
28
|
+
*
|
|
29
|
+
* Against a server that predates `executions`, it falls back to the synchronous
|
|
30
|
+
* route and says what that costs — but only on the FIRST start of a request id
|
|
31
|
+
* this invocation minted. A 404 without a product code proves the task that
|
|
32
|
+
* answered has no such route, not that no other task started the command; in a
|
|
33
|
+
* mixed fleet (a rolling deploy, a rollback) only the first attempt can rely on
|
|
34
|
+
* it. Every other case retries the same id or stops with the id to repeat.
|
|
35
|
+
*/
|
|
36
|
+
/** How long each observation lets the server hold the answer. */
|
|
37
|
+
exports.EXEC_OBSERVE_WAIT_MS = 20_000;
|
|
38
|
+
/** A request that has not answered by then is treated as lost, and retried. */
|
|
39
|
+
const REQUEST_DEADLINE_MS = exports.EXEC_OBSERVE_WAIT_MS + 25_000;
|
|
40
|
+
const START_ATTEMPTS = 4;
|
|
41
|
+
/**
|
|
42
|
+
* How long past the command's own timeout to keep observing. Longer than the
|
|
43
|
+
* server's own outcome deadline (timeout + 30 s response grace + 60 s dispatch
|
|
44
|
+
* allowance), so the server reports `unknown` before this gives up on it.
|
|
45
|
+
*/
|
|
46
|
+
const OBSERVE_SLACK_MS = 2 * 60 * 1000;
|
|
47
|
+
const MAX_BACKOFF_MS = 10_000;
|
|
48
|
+
/** A request id is the idempotency key; the server accepts exactly this shape. */
|
|
49
|
+
exports.EXEC_REQUEST_ID_PATTERN = /^[A-Za-z0-9._:/-]{1,200}$/;
|
|
50
|
+
function newExecRequestId() {
|
|
51
|
+
return `exec:${(0, node_crypto_1.randomUUID)()}`;
|
|
52
|
+
}
|
|
53
|
+
/** How long an observation keeps asking after the server says the execution is not there. */
|
|
54
|
+
const NOT_FOUND_TOLERANCE_MS = 60_000;
|
|
55
|
+
/** The CLI stopped hearing about a command it started. The command may still run. */
|
|
56
|
+
class DedicatedExecContactLostError extends Error {
|
|
57
|
+
requestId;
|
|
58
|
+
executionId;
|
|
59
|
+
code = 'DEDICATED_RUNTIME_EXECUTION_CONTACT_LOST';
|
|
60
|
+
constructor(requestId, executionId, cause) {
|
|
61
|
+
super(`Lost track of the command while it was running (execution ${executionId}). ` +
|
|
62
|
+
'It may still be running on the machine, or may have finished. To see how it ended ' +
|
|
63
|
+
`without running it again, repeat the same command with --request-id ${requestId} ` +
|
|
64
|
+
'(an outcome is kept for 5 minutes after it ends, and a minute after it is first read).' +
|
|
65
|
+
(cause instanceof Error && cause.message ? ` Last error: ${cause.message}` : ''));
|
|
66
|
+
this.requestId = requestId;
|
|
67
|
+
this.executionId = executionId;
|
|
68
|
+
this.name = 'DedicatedExecContactLostError';
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
exports.DedicatedExecContactLostError = DedicatedExecContactLostError;
|
|
72
|
+
/** No start request got an answer from the API, so whether the command started is unknown. */
|
|
73
|
+
class DedicatedExecStartUnconfirmedError extends Error {
|
|
74
|
+
requestId;
|
|
75
|
+
code = 'DEDICATED_RUNTIME_EXECUTION_START_UNCONFIRMED';
|
|
76
|
+
constructor(requestId, cause) {
|
|
77
|
+
super('Could not confirm whether the command started: no attempt got an answer from the API. ' +
|
|
78
|
+
'It may be running. To find out without running it a second time, repeat the same ' +
|
|
79
|
+
`command with --request-id ${requestId}.` +
|
|
80
|
+
(cause instanceof Error && cause.message ? ` Last error: ${cause.message}` : ''));
|
|
81
|
+
this.requestId = requestId;
|
|
82
|
+
this.name = 'DedicatedExecStartUnconfirmedError';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
exports.DedicatedExecStartUnconfirmedError = DedicatedExecStartUnconfirmedError;
|
|
86
|
+
/**
|
|
87
|
+
* The server has no `executions` route, but this invocation carries a request id
|
|
88
|
+
* it chose: falling back to the single-request route would ignore that id, and a
|
|
89
|
+
* run it already started elsewhere would happen twice.
|
|
90
|
+
*/
|
|
91
|
+
class DedicatedExecIdempotencyUnsupportedError extends Error {
|
|
92
|
+
requestId;
|
|
93
|
+
code = 'DEDICATED_RUNTIME_EXECUTION_REQUEST_ID_UNSUPPORTED';
|
|
94
|
+
constructor(requestId) {
|
|
95
|
+
super('This server answered as if it predates long-running exec, which is the only way --request-id ' +
|
|
96
|
+
'is honoured. The command was not run in a single request instead, because that could run ' +
|
|
97
|
+
`it a second time. During a deploy, retry shortly with --request-id ${requestId}; against ` +
|
|
98
|
+
'an older server, run it without --request-id.');
|
|
99
|
+
this.requestId = requestId;
|
|
100
|
+
this.name = 'DedicatedExecIdempotencyUnsupportedError';
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
exports.DedicatedExecIdempotencyUnsupportedError = DedicatedExecIdempotencyUnsupportedError;
|
|
104
|
+
const defaultIo = {
|
|
105
|
+
start: dedicated_machines_1.startDedicatedRuntimeExecution,
|
|
106
|
+
observe: dedicated_machines_1.observeDedicatedRuntimeExecution,
|
|
107
|
+
legacyExec: dedicated_machines_1.execDedicatedRuntime,
|
|
108
|
+
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
|
|
109
|
+
now: () => Date.now(),
|
|
110
|
+
notice: (message) => process.stderr.write(`${message}\n`),
|
|
111
|
+
signal: (ms) => typeof AbortSignal !== 'undefined' && typeof AbortSignal.timeout === 'function'
|
|
112
|
+
? AbortSignal.timeout(ms)
|
|
113
|
+
: undefined,
|
|
114
|
+
};
|
|
115
|
+
function productCode(err) {
|
|
116
|
+
try {
|
|
117
|
+
const parsed = JSON.parse(err.body || '');
|
|
118
|
+
return typeof parsed.code === 'string' && parsed.code ? parsed.code : undefined;
|
|
119
|
+
}
|
|
120
|
+
catch {
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
/** The server answered, but has no `executions` route: nothing was started. */
|
|
125
|
+
function isMissingExecutionsEndpoint(err) {
|
|
126
|
+
return err instanceof api_fetch_1.ApiFetchError && err.status === 404 && !productCode(err);
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* No answer from the API itself: the connection failed, the request timed out
|
|
130
|
+
* client-side, or something in front of the API answered instead (a gateway
|
|
131
|
+
* page carries no product code). Whether the request arrived is unknown.
|
|
132
|
+
*/
|
|
133
|
+
function isUnansweredRequest(err) {
|
|
134
|
+
if (err instanceof api_fetch_1.ApiFetchError) {
|
|
135
|
+
return [502, 503, 504].includes(err.status) && !productCode(err);
|
|
136
|
+
}
|
|
137
|
+
const e = err;
|
|
138
|
+
if (!e)
|
|
139
|
+
return false;
|
|
140
|
+
if (e.name === 'AbortError' || e.name === 'TimeoutError')
|
|
141
|
+
return true;
|
|
142
|
+
const code = e.code || e.cause?.code || '';
|
|
143
|
+
if (['ECONNRESET', 'ECONNREFUSED', 'ETIMEDOUT', 'EPIPE', 'ENOTFOUND', 'EAI_AGAIN'].includes(code)) {
|
|
144
|
+
return true;
|
|
145
|
+
}
|
|
146
|
+
return /fetch failed|socket hang up|network|terminated|other side closed/i.test(e.message || '');
|
|
147
|
+
}
|
|
148
|
+
function backoff(attempt) {
|
|
149
|
+
return Math.min(1000 * 2 ** Math.max(0, attempt - 1), MAX_BACKOFF_MS);
|
|
150
|
+
}
|
|
151
|
+
/** The refusal the synchronous route would have answered with, as its error. */
|
|
152
|
+
function executionFailure(execution) {
|
|
153
|
+
const { status, ...body } = execution.error || {};
|
|
154
|
+
const httpStatus = typeof status === 'number' ? status : 500;
|
|
155
|
+
const code = typeof body.code === 'string' ? body.code : undefined;
|
|
156
|
+
return new api_fetch_1.ApiFetchError(`HTTP ${httpStatus} — command ${execution.state} (execution ${execution.id})`, httpStatus, JSON.stringify(Object.keys(body).length ? body : { error: 'The command failed' }), code, 'POST');
|
|
157
|
+
}
|
|
158
|
+
async function runDedicatedExec(leaseId, input, options = {}, io = {}) {
|
|
159
|
+
const deps = { ...defaultIo, ...io };
|
|
160
|
+
const requestId = options.requestId || newExecRequestId();
|
|
161
|
+
const scope = options.workspaceId
|
|
162
|
+
? { workspaceId: options.workspaceId }
|
|
163
|
+
: {};
|
|
164
|
+
let execution;
|
|
165
|
+
for (let attempt = 1; !execution; attempt += 1) {
|
|
166
|
+
try {
|
|
167
|
+
const started = await deps.start(leaseId, input, {
|
|
168
|
+
...scope,
|
|
169
|
+
requestId,
|
|
170
|
+
signal: deps.signal(REQUEST_DEADLINE_MS),
|
|
171
|
+
});
|
|
172
|
+
execution = started.execution;
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
const missing = isMissingExecutionsEndpoint(err);
|
|
176
|
+
// Fall back only when nothing could have started: this is the first start
|
|
177
|
+
// for a request id this invocation minted. On a later attempt a codeless
|
|
178
|
+
// 404 is an OLDER task answering during a rolling deploy or a rollback,
|
|
179
|
+
// after an earlier attempt may have reached a newer one and started the
|
|
180
|
+
// command; a person's own --request-id may name a run a previous
|
|
181
|
+
// invocation started the same way. Either way `/exec` would run it twice.
|
|
182
|
+
if (missing && attempt === 1 && !options.requestId) {
|
|
183
|
+
deps.notice('This server predates long-running exec, so the command runs in a single request: ' +
|
|
184
|
+
'one that runs past about 60 seconds is cut off at the edge while it keeps running ' +
|
|
185
|
+
'on the machine.');
|
|
186
|
+
return deps.legacyExec(leaseId, input, scope);
|
|
187
|
+
}
|
|
188
|
+
// Safe only because the key is the same: the server answers a repeated
|
|
189
|
+
// start with the execution it already has.
|
|
190
|
+
if (!missing && !isUnansweredRequest(err))
|
|
191
|
+
throw err;
|
|
192
|
+
if (attempt >= START_ATTEMPTS) {
|
|
193
|
+
if (missing && attempt === 1)
|
|
194
|
+
throw new DedicatedExecIdempotencyUnsupportedError(requestId);
|
|
195
|
+
throw new DedicatedExecStartUnconfirmedError(requestId, err);
|
|
196
|
+
}
|
|
197
|
+
await deps.sleep(backoff(attempt));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
const timeoutMs = execution.timeoutMs || input.timeoutMs || dedicated_machines_1.DEDICATED_EXEC_DEFAULT_TIMEOUT_MS;
|
|
201
|
+
const giveUpAt = deps.now() + timeoutMs + OBSERVE_SLACK_MS;
|
|
202
|
+
let failures = 0;
|
|
203
|
+
let notFoundSince = null;
|
|
204
|
+
while (execution.state === 'running') {
|
|
205
|
+
try {
|
|
206
|
+
const observed = await deps.observe(leaseId, execution.id, {
|
|
207
|
+
...scope,
|
|
208
|
+
waitMs: exports.EXEC_OBSERVE_WAIT_MS,
|
|
209
|
+
signal: deps.signal(REQUEST_DEADLINE_MS),
|
|
210
|
+
});
|
|
211
|
+
execution = observed.execution;
|
|
212
|
+
failures = 0;
|
|
213
|
+
notFoundSince = null;
|
|
214
|
+
}
|
|
215
|
+
catch (err) {
|
|
216
|
+
const code = err instanceof api_fetch_1.ApiFetchError ? productCode(err) : undefined;
|
|
217
|
+
// A codeless 404 is an older task, during a deploy or a rollback, that has
|
|
218
|
+
// never heard of `executions`; the command this id started is unaffected.
|
|
219
|
+
const olderServer = isMissingExecutionsEndpoint(err);
|
|
220
|
+
// Not found can be a result read and released moments ago, or a store that
|
|
221
|
+
// lost it. Keep asking briefly, then stop: one that is really gone does not
|
|
222
|
+
// come back, and waiting out the whole timeout would only delay the hint.
|
|
223
|
+
const notFound = code === 'DEDICATED_RUNTIME_EXECUTION_NOT_FOUND';
|
|
224
|
+
const transient = olderServer ||
|
|
225
|
+
notFound ||
|
|
226
|
+
isUnansweredRequest(err) ||
|
|
227
|
+
(err instanceof api_fetch_1.ApiFetchError &&
|
|
228
|
+
(err.status === 429 || code === 'DEDICATED_RUNTIME_UNAVAILABLE'));
|
|
229
|
+
if (!transient)
|
|
230
|
+
throw err;
|
|
231
|
+
failures += 1;
|
|
232
|
+
const now = deps.now();
|
|
233
|
+
if (notFound)
|
|
234
|
+
notFoundSince ??= now;
|
|
235
|
+
else
|
|
236
|
+
notFoundSince = null;
|
|
237
|
+
const gaveUp = now >= giveUpAt ||
|
|
238
|
+
(notFoundSince !== null && now - notFoundSince >= NOT_FOUND_TOLERANCE_MS);
|
|
239
|
+
if (gaveUp)
|
|
240
|
+
throw new DedicatedExecContactLostError(requestId, execution.id, err);
|
|
241
|
+
await deps.sleep(backoff(failures));
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
if (execution.state === 'running' && deps.now() >= giveUpAt) {
|
|
245
|
+
// The server should have declared the outcome unknown by now.
|
|
246
|
+
throw new DedicatedExecContactLostError(requestId, execution.id, undefined);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
if (execution.state === 'completed' && execution.result) {
|
|
250
|
+
return execution.result;
|
|
251
|
+
}
|
|
252
|
+
throw executionFailure(execution);
|
|
253
|
+
}
|
|
254
|
+
function sizeOf(bytes) {
|
|
255
|
+
if (bytes >= 1024 * 1024)
|
|
256
|
+
return `${Math.round((bytes / (1024 * 1024)) * 10) / 10} MiB`;
|
|
257
|
+
if (bytes >= 1024)
|
|
258
|
+
return `${Math.round(bytes / 1024)} KiB`;
|
|
259
|
+
return `${bytes} B`;
|
|
260
|
+
}
|
|
261
|
+
/**
|
|
262
|
+
* What was cut. A started command's stored outcome keeps at most the last
|
|
263
|
+
* 256 KiB of what the machine returned (`outputBytes` is the whole); the
|
|
264
|
+
* single-request route caps at 1 MB from the start.
|
|
265
|
+
*/
|
|
266
|
+
function describeTruncation(result) {
|
|
267
|
+
if (typeof result.outputBytes === 'number') {
|
|
268
|
+
return `[output truncated: showing the last ${sizeOf(Buffer.byteLength(result.stdout))} of ${sizeOf(result.outputBytes)}]`;
|
|
269
|
+
}
|
|
270
|
+
return '[output truncated at 1 MB]';
|
|
271
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { BaseCommand } from '../base-command';
|
|
2
|
+
import { type DedicatedRuntimeResponse, type DedicatedRuntimeScope } from './dedicated-machines';
|
|
3
|
+
import { type DedicatedWaitGoal } from './dedicated-wait';
|
|
4
|
+
/**
|
|
5
|
+
* Lifecycle calls answer 202 with the lease already in its transitional state.
|
|
6
|
+
* `--wait` watches it to the END of the operation, which for snapshot and grow
|
|
7
|
+
* is a field, not a state (see `dedicated-wait.ts`).
|
|
8
|
+
*/
|
|
9
|
+
export declare const dedicatedWaitFlags: {
|
|
10
|
+
wait: import("@oclif/core/lib/interfaces").BooleanFlag<boolean>;
|
|
11
|
+
timeout: import("@oclif/core/lib/interfaces").OptionFlag<string | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
12
|
+
interval: import("@oclif/core/lib/interfaces").OptionFlag<number | undefined, import("@oclif/core/lib/interfaces").CustomOptions>;
|
|
13
|
+
};
|
|
14
|
+
/** The lease argument that means "the machine this command is running on". */
|
|
15
|
+
export declare const DEDICATED_SELF_LEASE = "self";
|
|
16
|
+
export declare const DEDICATED_LEASE_ARG_DESCRIPTION = "Dedicated Runtime lease id, or `self` when running on the machine itself";
|
|
17
|
+
export declare abstract class DedicatedLeaseCommand extends BaseCommand {
|
|
18
|
+
/**
|
|
19
|
+
* `--workspace` is a BaseCommand flag every command already accepts (it sets
|
|
20
|
+
* `X-Workspace-Id` for the whole invocation). It is also passed as the scope
|
|
21
|
+
* query, the way `create`, `exec` and `cp` always have, so the scope a request
|
|
22
|
+
* was made in is visible in the request itself.
|
|
23
|
+
*/
|
|
24
|
+
protected scopeFrom(flags: {
|
|
25
|
+
workspace?: string;
|
|
26
|
+
}): DedicatedRuntimeScope;
|
|
27
|
+
/**
|
|
28
|
+
* A lease argument as typed, or — for `self` — the lease of the machine this
|
|
29
|
+
* process runs on, so an agent working on a Dedicated Runtime can snapshot,
|
|
30
|
+
* inspect or stop its own machine without being told its id.
|
|
31
|
+
*/
|
|
32
|
+
protected resolveLeaseArg(lease: string): Promise<string>;
|
|
33
|
+
/**
|
|
34
|
+
* Open an interactive terminal on a lease's machine and run it to the end,
|
|
35
|
+
* exiting with the shell's own code.
|
|
36
|
+
*
|
|
37
|
+
* Every refusal is decided BEFORE the socket opens where it can be — a stopped
|
|
38
|
+
* machine, a daemon that is not connected, stdin that is not a terminal — so
|
|
39
|
+
* the caller gets a sentence that says what to do instead of a start that
|
|
40
|
+
* hangs and times out.
|
|
41
|
+
*/
|
|
42
|
+
protected openDedicatedTerminal(leaseId: string, flags: {
|
|
43
|
+
workspace?: string;
|
|
44
|
+
cwd?: string;
|
|
45
|
+
}, initialCommand?: string): Promise<void>;
|
|
46
|
+
/** Run an API call, turning a refusal into the server's own sentence. */
|
|
47
|
+
protected callDedicated<T>(fn: () => Promise<T>): Promise<T>;
|
|
48
|
+
/**
|
|
49
|
+
* Print the lease an action returned, or wait for its operation to finish
|
|
50
|
+
* first when `--wait` was passed.
|
|
51
|
+
*/
|
|
52
|
+
protected reportLeaseAction(response: DedicatedRuntimeResponse | undefined, flags: {
|
|
53
|
+
json?: boolean;
|
|
54
|
+
wait?: boolean;
|
|
55
|
+
timeout?: string;
|
|
56
|
+
interval?: number;
|
|
57
|
+
workspace?: string;
|
|
58
|
+
}, goal: DedicatedWaitGoal | null, verb: string): Promise<void>;
|
|
59
|
+
}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.DedicatedLeaseCommand = exports.DEDICATED_LEASE_ARG_DESCRIPTION = exports.DEDICATED_SELF_LEASE = exports.dedicatedWaitFlags = void 0;
|
|
4
|
+
const core_1 = require("@oclif/core");
|
|
5
|
+
const base_command_1 = require("../base-command");
|
|
6
|
+
const dedicated_machines_1 = require("./dedicated-machines");
|
|
7
|
+
const dedicated_terminal_1 = require("./dedicated-terminal");
|
|
8
|
+
const daemonBroker_1 = require("./daemonBroker");
|
|
9
|
+
const harnesses_1 = require("./harnesses");
|
|
10
|
+
const dedicated_wait_1 = require("./dedicated-wait");
|
|
11
|
+
const hosted_machines_1 = require("./hosted-machines");
|
|
12
|
+
/**
|
|
13
|
+
* Lifecycle calls answer 202 with the lease already in its transitional state.
|
|
14
|
+
* `--wait` watches it to the END of the operation, which for snapshot and grow
|
|
15
|
+
* is a field, not a state (see `dedicated-wait.ts`).
|
|
16
|
+
*/
|
|
17
|
+
exports.dedicatedWaitFlags = {
|
|
18
|
+
wait: core_1.Flags.boolean({
|
|
19
|
+
description: 'Wait until the operation has finished. Exits 124 if --timeout passes first; the operation keeps running.',
|
|
20
|
+
}),
|
|
21
|
+
timeout: core_1.Flags.string({
|
|
22
|
+
description: `How long --wait watches, such as 10m (default ${dedicated_wait_1.DEDICATED_WAIT_DEFAULT_TIMEOUT_MS / 60000}m)`,
|
|
23
|
+
dependsOn: ['wait'],
|
|
24
|
+
}),
|
|
25
|
+
interval: core_1.Flags.integer({
|
|
26
|
+
description: `Seconds between checks while waiting (default ${dedicated_wait_1.DEDICATED_WAIT_DEFAULT_INTERVAL_MS / 1000})`,
|
|
27
|
+
dependsOn: ['wait'],
|
|
28
|
+
min: 1,
|
|
29
|
+
}),
|
|
30
|
+
};
|
|
31
|
+
const TERMINAL_READY_STATES = new Set(['ready', 'active', 'idle']);
|
|
32
|
+
/** The lease argument that means "the machine this command is running on". */
|
|
33
|
+
exports.DEDICATED_SELF_LEASE = 'self';
|
|
34
|
+
exports.DEDICATED_LEASE_ARG_DESCRIPTION = 'Dedicated Runtime lease id, or `self` when running on the machine itself';
|
|
35
|
+
class DedicatedLeaseCommand extends base_command_1.BaseCommand {
|
|
36
|
+
/**
|
|
37
|
+
* `--workspace` is a BaseCommand flag every command already accepts (it sets
|
|
38
|
+
* `X-Workspace-Id` for the whole invocation). It is also passed as the scope
|
|
39
|
+
* query, the way `create`, `exec` and `cp` always have, so the scope a request
|
|
40
|
+
* was made in is visible in the request itself.
|
|
41
|
+
*/
|
|
42
|
+
scopeFrom(flags) {
|
|
43
|
+
return flags.workspace ? { workspaceId: flags.workspace } : {};
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* A lease argument as typed, or — for `self` — the lease of the machine this
|
|
47
|
+
* process runs on, so an agent working on a Dedicated Runtime can snapshot,
|
|
48
|
+
* inspect or stop its own machine without being told its id.
|
|
49
|
+
*/
|
|
50
|
+
async resolveLeaseArg(lease) {
|
|
51
|
+
if (lease !== exports.DEDICATED_SELF_LEASE)
|
|
52
|
+
return lease;
|
|
53
|
+
const descriptor = (0, daemonBroker_1.readBootstrap)(daemonBroker_1.DEDICATED_RUNTIME_CLI_HANDOFF_DESCRIPTOR);
|
|
54
|
+
if (!descriptor?.daemonId) {
|
|
55
|
+
this.error('`self` means the Dedicated Runtime this command runs on, and this is not one (or its daemon is not running). Pass a lease id.', { exit: 2 });
|
|
56
|
+
}
|
|
57
|
+
const listed = await this.callDedicated(() => (0, harnesses_1.listHarnesses)());
|
|
58
|
+
const resolved = (0, dedicated_machines_1.dedicatedLeaseIdForDaemon)(descriptor.daemonId, listed?.harnesses ?? []);
|
|
59
|
+
if (!resolved) {
|
|
60
|
+
this.error("This machine's daemon has not registered a lease yet, so `self` cannot be resolved. Pass a lease id, or try again once `skrr machines dedicated list` shows the machine ready.", { exit: 1 });
|
|
61
|
+
}
|
|
62
|
+
return resolved;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Open an interactive terminal on a lease's machine and run it to the end,
|
|
66
|
+
* exiting with the shell's own code.
|
|
67
|
+
*
|
|
68
|
+
* Every refusal is decided BEFORE the socket opens where it can be — a stopped
|
|
69
|
+
* machine, a daemon that is not connected, stdin that is not a terminal — so
|
|
70
|
+
* the caller gets a sentence that says what to do instead of a start that
|
|
71
|
+
* hangs and times out.
|
|
72
|
+
*/
|
|
73
|
+
async openDedicatedTerminal(leaseId, flags, initialCommand) {
|
|
74
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
75
|
+
this.error('An interactive terminal needs a terminal on both ends. For scripts use `skrr machines dedicated exec`; an agent on the machine uses its terminal_* tools.', { exit: 2 });
|
|
76
|
+
}
|
|
77
|
+
const response = await this.callDedicated(() => (0, dedicated_machines_1.getDedicatedRuntime)(leaseId, this.scopeFrom(flags)));
|
|
78
|
+
const lease = response?.lease;
|
|
79
|
+
if (!lease)
|
|
80
|
+
this.error('Dedicated Runtime lease not found.', { exit: 1 });
|
|
81
|
+
const state = (0, dedicated_machines_1.dedicatedLeaseState)(lease);
|
|
82
|
+
if (!TERMINAL_READY_STATES.has(state)) {
|
|
83
|
+
this.error(state === 'stopped'
|
|
84
|
+
? `${leaseId} is stopped. Start it first: \`${this.config.bin} machines dedicated start ${leaseId} --wait\`.`
|
|
85
|
+
: `${leaseId} is ${state}; a terminal opens once it is ready.`, { exit: 1 });
|
|
86
|
+
}
|
|
87
|
+
const listed = await this.callDedicated(() => (0, harnesses_1.listHarnesses)());
|
|
88
|
+
const harness = (0, dedicated_machines_1.dedicatedTerminalHarness)(lease, listed?.harnesses ?? []);
|
|
89
|
+
if (!harness?.daemonId) {
|
|
90
|
+
this.error(`${leaseId} is ${state}, but its daemon is not connected yet. Wait a moment, or run \`${this.config.bin} machines dedicated health-check ${leaseId}\`.`, { exit: 1 });
|
|
91
|
+
}
|
|
92
|
+
process.stderr.write(`Connected to ${(0, dedicated_machines_1.dedicatedLeaseDisplayName)(lease)} (${leaseId}) as skrr-workload. Ctrl-] closes the terminal.\n`);
|
|
93
|
+
let code;
|
|
94
|
+
try {
|
|
95
|
+
code = await (0, dedicated_terminal_1.runDedicatedTerminal)({
|
|
96
|
+
baseURL: this.cliConfig.baseURL,
|
|
97
|
+
token: this.resolvedCredential.token,
|
|
98
|
+
runtimeId: harness._id,
|
|
99
|
+
daemonId: harness.daemonId,
|
|
100
|
+
...(flags.cwd ? { cwd: flags.cwd } : {}),
|
|
101
|
+
...(initialCommand ? { initialCommand } : {}),
|
|
102
|
+
input: process.stdin,
|
|
103
|
+
output: process.stdout,
|
|
104
|
+
errorOutput: process.stderr,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
if (err instanceof dedicated_terminal_1.DedicatedTerminalError)
|
|
109
|
+
this.error(err.message, { exit: 1 });
|
|
110
|
+
throw err;
|
|
111
|
+
}
|
|
112
|
+
if (code !== 0)
|
|
113
|
+
this.exit(code);
|
|
114
|
+
}
|
|
115
|
+
/** Run an API call, turning a refusal into the server's own sentence. */
|
|
116
|
+
async callDedicated(fn) {
|
|
117
|
+
try {
|
|
118
|
+
return await fn();
|
|
119
|
+
}
|
|
120
|
+
catch (err) {
|
|
121
|
+
const message = (0, dedicated_machines_1.formatDedicatedRuntimeApiError)(err);
|
|
122
|
+
if (message)
|
|
123
|
+
this.error(message, { exit: 1, code: (0, dedicated_machines_1.dedicatedRuntimeApiErrorCode)(err) });
|
|
124
|
+
this.handleApiError(err);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Print the lease an action returned, or wait for its operation to finish
|
|
129
|
+
* first when `--wait` was passed.
|
|
130
|
+
*/
|
|
131
|
+
async reportLeaseAction(response, flags, goal, verb) {
|
|
132
|
+
let lease = response?.lease;
|
|
133
|
+
const id = (0, dedicated_machines_1.dedicatedLeaseId)(lease);
|
|
134
|
+
if (flags.wait && goal && id) {
|
|
135
|
+
let timeoutMs;
|
|
136
|
+
try {
|
|
137
|
+
timeoutMs = (0, hosted_machines_1.parseDurationMs)(flags.timeout, '--timeout');
|
|
138
|
+
}
|
|
139
|
+
catch (err) {
|
|
140
|
+
this.error(err.message, { exit: 2 });
|
|
141
|
+
}
|
|
142
|
+
if (!flags.json)
|
|
143
|
+
this.log(`${verb} ${id}: ${(0, dedicated_machines_1.dedicatedLeaseState)(lease)}`);
|
|
144
|
+
try {
|
|
145
|
+
lease = await (0, dedicated_wait_1.waitForDedicatedLease)(id, goal, {
|
|
146
|
+
...this.scopeFrom(flags),
|
|
147
|
+
initial: lease,
|
|
148
|
+
intervalMs: flags.interval ? flags.interval * 1000 : undefined,
|
|
149
|
+
timeoutMs,
|
|
150
|
+
// Progress is about the wait, not the result: stderr, so a caller
|
|
151
|
+
// reading `--json` from stdout gets exactly one document.
|
|
152
|
+
onProgress: (progress) => process.stderr.write(` … ${progress}\n`),
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
if (err instanceof dedicated_wait_1.DedicatedWaitTimeoutError) {
|
|
157
|
+
process.stderr.write(`${err.message}\n`);
|
|
158
|
+
this.exit(dedicated_wait_1.DEDICATED_WAIT_TIMEOUT_EXIT);
|
|
159
|
+
}
|
|
160
|
+
if (err instanceof dedicated_wait_1.DedicatedWaitFailedError) {
|
|
161
|
+
if (flags.json && err.lease)
|
|
162
|
+
this.log(JSON.stringify({ lease: err.lease }, null, 2));
|
|
163
|
+
this.error(err.message, { exit: 1 });
|
|
164
|
+
}
|
|
165
|
+
const message = (0, dedicated_machines_1.formatDedicatedRuntimeApiError)(err);
|
|
166
|
+
if (message)
|
|
167
|
+
this.error(message, { exit: 1, code: (0, dedicated_machines_1.dedicatedRuntimeApiErrorCode)(err) });
|
|
168
|
+
this.handleApiError(err);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
if (flags.json) {
|
|
172
|
+
this.log(JSON.stringify({ ...(response || {}), lease }, null, 2));
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.log(`Dedicated Runtime ${id || 'unknown'} ${(0, dedicated_machines_1.dedicatedLeaseState)(lease)}.`);
|
|
176
|
+
if (!flags.wait && goal && id) {
|
|
177
|
+
this.log(`The operation continues in the background. Watch it with \`${this.config.bin} machines dedicated show ${id}\`, or pass --wait.`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
exports.DedicatedLeaseCommand = DedicatedLeaseCommand;
|