@arker-ai/sdk 0.2.1 → 0.5.1

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/cli.cjs ADDED
@@ -0,0 +1,1316 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __create = Object.create;
4
+ var __defProp = Object.defineProperty;
5
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
6
+ var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
8
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
18
+ // If the importer is in node compatibility mode or this is not an ESM
19
+ // file that has been converted to a CommonJS file using a Babel-
20
+ // compatible transform (i.e. "__esModule" has not been set), then set
21
+ // "default" to the CommonJS "module.exports" for node compatibility.
22
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
23
+ mod
24
+ ));
25
+
26
+ // src/cli.ts
27
+ var import_node_fs = require("fs");
28
+ var import_node_os = require("os");
29
+ var import_node_path = require("path");
30
+ var readline = __toESM(require("readline/promises"), 1);
31
+ var import_node_process = require("process");
32
+
33
+ // src/index.ts
34
+ var CHUNK_SIZE = 4 * 1024 * 1024;
35
+ var ARKER_ORG_ID = "ArkerHQ";
36
+ var GOLDEN_NAMES = /* @__PURE__ */ new Set([
37
+ "arkuntu",
38
+ "ubuntu",
39
+ "ubuntu-small",
40
+ "ubuntu-nodisk",
41
+ "ubuntu-nonet-nodisk",
42
+ "ubuntu-full",
43
+ "ubuntu-full-32",
44
+ "ubuntu-py-repl",
45
+ "ubuntu-js-repl",
46
+ "ubuntu-docker",
47
+ "ubuntu-chromium",
48
+ "ubuntu-servo",
49
+ "ubuntu-servo-js-repl",
50
+ "ubuntu-chromium-js-repl"
51
+ ]);
52
+ var DEFAULT_RETRY_ATTEMPTS = 4;
53
+ var DEFAULT_RETRY_BASE_DELAY_MS = 200;
54
+ var DEFAULT_RETRY_MAX_DELAY_MS = 2e3;
55
+ var DEFAULT_RETRY_JITTER_MS = 50;
56
+ var PRESIGNED_PUT_TIMEOUT_MS = 6e5;
57
+ var RETRYABLE_HTTP = /* @__PURE__ */ new Set([429, 502, 503, 504]);
58
+ var RETRYABLE_CODES = /* @__PURE__ */ new Set(["routing_unavailable", "unavailable", "temporarily_unavailable"]);
59
+ var TRANSIENT_HINTS = ["503", "Service Unavailable", "throttle", "SlowDown", "ThrottlingException"];
60
+ var ULID_ALPHABET = "0123456789ABCDEFGHJKMNPQRSTVWXYZ";
61
+ var DEFAULT_REGION_ENV = "ARKER_REGION";
62
+ var DEFAULT_PROVIDER_ENV = "ARKER_PROVIDER";
63
+ var DEFAULT_PROVIDER = "aws";
64
+ var DEFAULT_CONTROL_BASE_URL = "https://arker.ai/api";
65
+ var BURST_SOURCE_REFS = /* @__PURE__ */ new Set(["arkuntu"]);
66
+ var BURST_VM_ID = /^[0-9A-HJKMNP-TV-Z]{26}_[A-Za-z0-9]+$/;
67
+ var ArkerError = class extends Error {
68
+ code;
69
+ status;
70
+ constructor(code, message, status) {
71
+ super(`${code}: ${message}`);
72
+ this.name = "ArkerError";
73
+ this.code = code;
74
+ this.status = status;
75
+ }
76
+ };
77
+ var Arker = class {
78
+ /** Compute base URL for `provider` + `region` — used for fork/run/
79
+ * per-VM ops. SDK calls go straight to this host, skipping the CF
80
+ * Worker control plane. */
81
+ baseUrl;
82
+ /** Compute base URL for the burst provider in this region. */
83
+ burstBaseUrl;
84
+ /** CF Worker control-plane URL — used for cross-cutting admin calls
85
+ * like list-VMs and filesystems. */
86
+ controlBaseUrl;
87
+ region;
88
+ provider;
89
+ apiKey;
90
+ fetchImpl;
91
+ retry;
92
+ constructor(opts = {}) {
93
+ const apiKey = opts.apiKey ?? env("ARKER_API_KEY") ?? env("AUTH_KEY");
94
+ const explicitBaseUrl = opts.baseUrl ?? env("ARKER_BASE_URL");
95
+ const rawRegion = opts.region ?? (explicitBaseUrl ? void 0 : env(DEFAULT_REGION_ENV));
96
+ const rawProvider = opts.provider ?? env(DEFAULT_PROVIDER_ENV) ?? DEFAULT_PROVIDER;
97
+ const provider = parseProvider(rawProvider);
98
+ const { region, providerFromRegion } = splitRegion(rawRegion);
99
+ const effectiveProvider = providerFromRegion ?? provider;
100
+ const baseUrl = explicitBaseUrl ?? (region ? computeBaseUrl(effectiveProvider, region) : void 0);
101
+ const burstBaseUrl = opts.burstBaseUrl ?? env("ARKER_BURST_BASE_URL") ?? (region ? computeBaseUrl("aws-burst", region) : void 0);
102
+ const controlBaseUrl = opts.controlBaseUrl ?? env("ARKER_CONTROL_BASE_URL") ?? DEFAULT_CONTROL_BASE_URL;
103
+ if (!apiKey) throw new Error("apiKey is required; pass apiKey or set ARKER_API_KEY");
104
+ if (!baseUrl) throw new Error("region or baseUrl is required; pass region, baseUrl, ARKER_REGION, or ARKER_BASE_URL");
105
+ this.apiKey = apiKey;
106
+ this.baseUrl = normalizeBaseUrl(baseUrl);
107
+ this.burstBaseUrl = burstBaseUrl ? normalizeBaseUrl(burstBaseUrl) : void 0;
108
+ this.controlBaseUrl = normalizeBaseUrl(controlBaseUrl);
109
+ this.region = region ? normalizeRegion(region) : void 0;
110
+ this.provider = effectiveProvider;
111
+ this.fetchImpl = opts.fetch ?? globalThis.fetch;
112
+ this.retry = normalizeRetry(opts.retry);
113
+ if (!this.fetchImpl) throw new Error("fetch is required in this runtime");
114
+ }
115
+ /**
116
+ * Address an existing VM. Doesn't make any network calls; returns a
117
+ * lightweight handle.
118
+ */
119
+ vm(vmId) {
120
+ return new VM(this, vmId, this._baseUrlFor(vmId));
121
+ }
122
+ /**
123
+ * Create a new VM by forking from a source.
124
+ *
125
+ * fork("ubuntu-full") // public golden by name
126
+ * fork("base") // a VM by name in your org
127
+ * fork(vm) // an existing VM (uses its id)
128
+ * fork({ sourceVmId: "vm_abc..." })
129
+ * fork({ sourceVmName: "base", sourceOrgId: "org_..." })
130
+ *
131
+ * The source can be a name string, a `VM` handle, or a `ForkSource`
132
+ * object. `sourceOrgId` defaults to the Arker org when `sourceVmName`
133
+ * is a known public golden, otherwise to your own org; an explicit
134
+ * value always wins, and it's irrelevant when forking by id. Forking a
135
+ * VM in another org requires that VM to be `public: true`. The new VM's
136
+ * name (in your org) is passed as `name`. When the source is a name or
137
+ * `VM`, extra fork options go in the second `opts` argument.
138
+ */
139
+ async fork(source, opts = {}) {
140
+ const src = typeof source === "string" ? { sourceVmName: source, ...opts } : source instanceof VM ? { sourceVmId: source.id, ...opts } : source;
141
+ if (!src.sourceVmId && !src.sourceVmName) {
142
+ throw new ArkerError(
143
+ "bad_request",
144
+ "fork requires a source (a name, a VM, sourceVmName, or sourceVmId)",
145
+ 400
146
+ );
147
+ }
148
+ if (src.sourceVmId && src.sourceVmName) {
149
+ throw new ArkerError(
150
+ "bad_request",
151
+ "fork: pass only one of sourceVmId or sourceVmName",
152
+ 400
153
+ );
154
+ }
155
+ const sourceOrgId = src.sourceOrgId ?? (src.sourceVmName !== void 0 && GOLDEN_NAMES.has(src.sourceVmName) ? ARKER_ORG_ID : void 0);
156
+ const body = {
157
+ source_vm_id: src.sourceVmId ?? null,
158
+ source_vm_name: src.sourceVmName ?? null,
159
+ source_org_id: sourceOrgId ?? null,
160
+ name: src.name ?? null,
161
+ public: src.public ?? null,
162
+ network: src.network ?? null,
163
+ disk: src.disk ?? true,
164
+ vcpu_count: src.vcpu_count ?? null,
165
+ memory_mib: src.memory_mib ?? null,
166
+ max_memory_mib: src.max_memory_mib ?? null,
167
+ disk_mib: src.disk_mib ?? null,
168
+ durable: src.durable ?? null
169
+ };
170
+ const useBurst = sourceOrgId === ARKER_ORG_ID && src.sourceVmName !== void 0 && isBurstRef(src.sourceVmName);
171
+ const baseUrl = useBurst && this.burstBaseUrl ? this.burstBaseUrl : this.baseUrl;
172
+ const vm = await this._request("POST", "/v1/fork", body, baseUrl);
173
+ const vmId = vm.vm_id ?? vm.id ?? "";
174
+ return new VM(this, vmId, baseUrl, vm);
175
+ }
176
+ /**
177
+ * List VMs visible to the authenticated caller. **Admin call** —
178
+ * goes through the control plane (`controlBaseUrl`) so it can
179
+ * aggregate across providers and regions. Pass `?provider=` /
180
+ * `?region=` to narrow.
181
+ */
182
+ async listVms(opts = {}) {
183
+ const resp = await this._request("GET", buildQuery("/v1/vms", {
184
+ cursor: opts.cursor,
185
+ limit: opts.limit,
186
+ region: opts.region,
187
+ provider: opts.provider,
188
+ state: opts.state,
189
+ source_org_id: opts.sourceOrgId,
190
+ started_after: opts.startedAfter,
191
+ started_before: opts.startedBefore
192
+ }), void 0, this.controlBaseUrl);
193
+ const vms = (resp.vms ?? []).map((v) => {
194
+ const id = v.vm_id ?? v.id ?? "";
195
+ return new VM(this, id, this._baseUrlFor(id), v);
196
+ });
197
+ return { vms, nextCursor: resp.next_cursor ?? null };
198
+ }
199
+ /** Compute call — goes direct to the backend hosting this VM (no
200
+ * control-plane hop). Returns a fully-populated VM handle. */
201
+ async getVm(vmId) {
202
+ const data = await this._request("GET", vmPath(vmId), void 0, this._baseUrlFor(vmId));
203
+ return new VM(this, vmId, this._baseUrlFor(vmId), data);
204
+ }
205
+ // ── Filesystems (region-scoped, served by arkerd directly) ──────────
206
+ // Route to the regional endpoint (baseUrl), not the control plane: the
207
+ // control-plane path (arker.ai → api_proxy_bash) does not route
208
+ // /v1/filesystems, while the regional NLB → arkerd serves the full CRUD.
209
+ async listFilesystems(opts = {}) {
210
+ return this._request("GET", buildQuery("/v1/filesystems", {
211
+ cursor: opts.cursor,
212
+ limit: opts.limit,
213
+ name_prefix: opts.namePrefix
214
+ }), void 0, this.baseUrl);
215
+ }
216
+ async createFilesystem(request) {
217
+ return this._request("POST", "/v1/filesystems", { name: request.name }, this.baseUrl);
218
+ }
219
+ async getFilesystem(filesystemId) {
220
+ return this._request("GET", `/v1/filesystems/${pathSegment(filesystemId)}`, void 0, this.baseUrl);
221
+ }
222
+ async deleteFilesystem(filesystemId) {
223
+ return this._request("DELETE", `/v1/filesystems/${pathSegment(filesystemId)}`, void 0, this.baseUrl);
224
+ }
225
+ /** @internal */
226
+ async _request(method, path, body, baseUrl = this.baseUrl, extraHeaders) {
227
+ const url = `${baseUrl}${path}`;
228
+ const headers = {
229
+ authorization: `Bearer ${this.apiKey}`
230
+ };
231
+ if (extraHeaders) {
232
+ for (const [key, value] of Object.entries(extraHeaders)) {
233
+ if (value !== void 0) headers[key] = value;
234
+ }
235
+ }
236
+ const init = { method, headers };
237
+ if (body !== void 0) {
238
+ headers["content-type"] = "application/json";
239
+ init.body = JSON.stringify(withoutUndefined(body));
240
+ }
241
+ let lastStatus = 0;
242
+ let lastText = "";
243
+ let lastError;
244
+ for (let attempt = 0; attempt < this.retry.attempts; attempt++) {
245
+ try {
246
+ const response = await this.fetchImpl(url, init);
247
+ const text = await response.text();
248
+ const payload = parseJson(text);
249
+ const parsedError = extractError(payload);
250
+ lastStatus = response.status;
251
+ lastText = text;
252
+ lastError = parsedError;
253
+ if (isRetryable(response.status, parsedError) && attempt < this.retry.attempts - 1) {
254
+ await sleep(retryDelay(this.retry, attempt));
255
+ continue;
256
+ }
257
+ if (parsedError) throw new ArkerError(parsedError.code, parsedError.message, response.status);
258
+ if (!response.ok) {
259
+ throw new ArkerError("internal", lastText.slice(0, 300) || `HTTP ${response.status}`, response.status);
260
+ }
261
+ return payload;
262
+ } catch (error) {
263
+ if (error instanceof ArkerError) throw error;
264
+ if (attempt < this.retry.attempts - 1) {
265
+ await sleep(retryDelay(this.retry, attempt));
266
+ continue;
267
+ }
268
+ const message = error instanceof Error ? error.message : String(error);
269
+ throw new ArkerError("network_error", message, 0);
270
+ }
271
+ }
272
+ if (lastError) throw new ArkerError(lastError.code, lastError.message, lastStatus);
273
+ throw new ArkerError("internal", lastText.slice(0, 300) || `HTTP ${lastStatus}`, lastStatus);
274
+ }
275
+ /** @internal */
276
+ async _fetch(input2, init) {
277
+ return this.fetchImpl(input2, init);
278
+ }
279
+ /** @internal */
280
+ _retryAttempts() {
281
+ return this.retry.attempts;
282
+ }
283
+ /** @internal */
284
+ _retryDelay(attempt) {
285
+ return retryDelay(this.retry, attempt);
286
+ }
287
+ /** @internal */
288
+ _baseUrlFor(ref) {
289
+ if (isBurstRef(ref) && this.burstBaseUrl) return this.burstBaseUrl;
290
+ return this.baseUrl;
291
+ }
292
+ };
293
+ var VM = class _VM {
294
+ id;
295
+ baseUrl;
296
+ /** @internal */
297
+ _client;
298
+ // ── Data fields ──────────────────────────────────────────────────
299
+ // Populated from fork/get/list/refresh; `undefined` on a bare handle
300
+ // from `arker.vm(id)` until you call `refresh()`. Names mirror the
301
+ // contract (`Vm`).
302
+ vm_id;
303
+ name;
304
+ state;
305
+ owner_org_id;
306
+ created_at;
307
+ public;
308
+ region;
309
+ provider;
310
+ vcpu_count;
311
+ memory_mib;
312
+ disk_mib;
313
+ started_at;
314
+ root_source_vm_id;
315
+ root_source_vm_name;
316
+ worker_id;
317
+ sessions;
318
+ tunnels;
319
+ constructor(client, vmId, baseUrl = client._baseUrlFor(vmId), data) {
320
+ this._client = client;
321
+ this.id = vmId;
322
+ this.baseUrl = baseUrl;
323
+ if (data) Object.assign(this, data);
324
+ Object.defineProperty(this, "_client", { enumerable: false });
325
+ }
326
+ /** Re-fetch this VM and return a fresh, fully-populated handle. */
327
+ async refresh() {
328
+ const data = await this._client._request("GET", vmPath(this.id), void 0, this.baseUrl);
329
+ return new _VM(this._client, this.id, this.baseUrl, data);
330
+ }
331
+ /**
332
+ * @deprecated Use `Arker.fork({ sourceVmId: this.id, ... })`.
333
+ * Kept for back-compat with older user code that called `.fork()` on
334
+ * a VM instance.
335
+ */
336
+ async fork(request = {}) {
337
+ const merged = {
338
+ ...request,
339
+ source_vm_id: request.source_vm_id ?? this.id,
340
+ disk: request.disk ?? true
341
+ };
342
+ const vm = await this._client._request("POST", "/v1/fork", merged, this.baseUrl);
343
+ const vmId = vm.vm_id ?? vm.id ?? "";
344
+ return new _VM(this._client, vmId, this.baseUrl, vm);
345
+ }
346
+ async run(command, options = {}) {
347
+ const { idempotencyKey, ...body } = options;
348
+ const headers = idempotencyKey ? { "Idempotency-Key": idempotencyKey } : void 0;
349
+ const response = await this._client._request(
350
+ "POST",
351
+ `${vmPath(this.id)}/runs`,
352
+ { ...body, command },
353
+ this.baseUrl,
354
+ headers
355
+ );
356
+ return parseRunResponse(response);
357
+ }
358
+ async sync(path, data) {
359
+ if (data === void 0) return this.syncRead(path);
360
+ const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
361
+ if (bytes.length <= CHUNK_SIZE) await this.syncWriteInline(path, bytes);
362
+ else await this.syncWritePresigned(path, bytes);
363
+ }
364
+ async syncRead(path) {
365
+ const response = await this._client._request(
366
+ "POST",
367
+ `${vmPath(this.id)}/sync`,
368
+ { op: "read", path },
369
+ this.baseUrl
370
+ );
371
+ if ("content" in response) return decodeBytes(response.content, response.encoding);
372
+ const signed = await this._client._fetch(response.presigned_url);
373
+ if (!signed.ok) throw new ArkerError("internal", `signed GET failed: ${signed.status}`, signed.status);
374
+ return new Uint8Array(await signed.arrayBuffer());
375
+ }
376
+ async syncWriteInline(path, data) {
377
+ const result = await this.sendOneWrite({
378
+ path,
379
+ size: data.length,
380
+ upload_id: ulid(),
381
+ content: bytesToBase64(data),
382
+ start: 0,
383
+ end: data.length
384
+ });
385
+ assertWriteComplete(result, "inline write");
386
+ }
387
+ async syncWritePresigned(path, data) {
388
+ const request = await this.sendOneWrite({ path, size: data.length, presigned: true });
389
+ if (!("presigned_url" in request) || !request.presigned_url || !request.upload_id) {
390
+ throw new ArkerError("internal", "write response missing presigned upload fields", 200);
391
+ }
392
+ await this.putPresigned(request.presigned_url, data);
393
+ const commit = await this.sendOneWrite({ path, size: data.length, upload_id: request.upload_id });
394
+ assertWriteComplete(commit, "presigned write commit");
395
+ }
396
+ async putPresigned(url, data) {
397
+ const attempts = this._client._retryAttempts();
398
+ for (let attempt = 0; attempt < attempts; attempt++) {
399
+ const controller = new AbortController();
400
+ const timeout = setTimeout(() => controller.abort(), PRESIGNED_PUT_TIMEOUT_MS);
401
+ try {
402
+ const response = await this._client._fetch(url, {
403
+ method: "PUT",
404
+ body: data,
405
+ signal: controller.signal
406
+ });
407
+ clearTimeout(timeout);
408
+ if (response.ok) return;
409
+ if (!RETRYABLE_HTTP.has(response.status) || attempt === attempts - 1) {
410
+ throw new ArkerError("internal", `upload PUT failed: ${response.status}`, response.status);
411
+ }
412
+ } catch (error) {
413
+ clearTimeout(timeout);
414
+ if (error instanceof ArkerError) throw error;
415
+ if (attempt === attempts - 1) {
416
+ const message = error instanceof Error ? error.message : String(error);
417
+ throw new ArkerError("network_error", `upload PUT failed: ${message}`, 0);
418
+ }
419
+ }
420
+ await sleep(this._client._retryDelay(attempt));
421
+ }
422
+ }
423
+ async sendOneWrite(entry) {
424
+ let lastError;
425
+ const attempts = this._client._retryAttempts();
426
+ for (let attempt = 0; attempt < attempts; attempt++) {
427
+ const response = await this._client._request("POST", `${vmPath(this.id)}/sync`, {
428
+ op: "write",
429
+ writes: [entry]
430
+ }, this.baseUrl);
431
+ const result = response.results[0];
432
+ if (!result) throw new ArkerError("internal", "write response missing results[0]", 200);
433
+ const error = result.error ?? void 0;
434
+ if (!error) return result;
435
+ lastError = error;
436
+ if (!isRetryable(200, { code: error.code, message: error.message }) || attempt === attempts - 1) break;
437
+ await sleep(this._client._retryDelay(attempt));
438
+ }
439
+ throw new ArkerError(lastError?.code ?? "internal", lastError?.message ?? "write failed", 200);
440
+ }
441
+ async resize(request) {
442
+ return this._client._request("POST", `${vmPath(this.id)}/resize`, request, this.baseUrl);
443
+ }
444
+ async delete() {
445
+ return this._client._request("DELETE", vmPath(this.id), void 0, this.baseUrl);
446
+ }
447
+ // ── Syncs: bindings of a filesystem into this VM at a path ────────
448
+ async listSyncs(opts = {}) {
449
+ return this._client._request("GET", buildQuery(`${vmPath(this.id)}/syncs`, {
450
+ cursor: opts.cursor,
451
+ limit: opts.limit,
452
+ filesystem_id: opts.filesystemId
453
+ }), void 0, this.baseUrl);
454
+ }
455
+ async createSync(request) {
456
+ return this._client._request("POST", `${vmPath(this.id)}/syncs`, {
457
+ filesystem_id: request.filesystemId,
458
+ path: request.path
459
+ }, this.baseUrl);
460
+ }
461
+ async deleteSync(syncId) {
462
+ return this._client._request("DELETE", `${vmPath(this.id)}/syncs/${pathSegment(syncId)}`, void 0, this.baseUrl);
463
+ }
464
+ // ── Runs ──────────────────────────────────────────────────────────
465
+ async listRuns(opts = {}) {
466
+ return this._client._request("GET", buildQuery(`${vmPath(this.id)}/runs`, {
467
+ cursor: opts.cursor,
468
+ limit: opts.limit,
469
+ state: opts.state,
470
+ started_after: opts.startedAfter,
471
+ started_before: opts.startedBefore,
472
+ completed_after: opts.completedAfter
473
+ }), void 0, this.baseUrl);
474
+ }
475
+ async getRun(runId) {
476
+ return this._client._request("GET", `${vmPath(this.id)}/runs/${pathSegment(runId)}`, void 0, this.baseUrl);
477
+ }
478
+ async cancelRun(runId) {
479
+ return this._client._request("DELETE", `${vmPath(this.id)}/runs/${pathSegment(runId)}`, void 0, this.baseUrl);
480
+ }
481
+ // ── Sessions ──────────────────────────────────────────────────────
482
+ async listSessions(opts = {}) {
483
+ return this._client._request("GET", buildQuery(`${vmPath(this.id)}/sessions`, {
484
+ cursor: opts.cursor,
485
+ limit: opts.limit,
486
+ state: opts.state
487
+ }), void 0, this.baseUrl);
488
+ }
489
+ async createSession(request = {}) {
490
+ return this._client._request("POST", `${vmPath(this.id)}/sessions`, request, this.baseUrl);
491
+ }
492
+ async getSession(sessionId) {
493
+ return this._client._request("GET", `${vmPath(this.id)}/sessions/${pathSegment(sessionId)}`, void 0, this.baseUrl);
494
+ }
495
+ async deleteSession(sessionId) {
496
+ return this._client._request("DELETE", `${vmPath(this.id)}/sessions/${pathSegment(sessionId)}`, void 0, this.baseUrl);
497
+ }
498
+ // ── Tunnels: opened as a side effect of fork/run, addressed by port ─
499
+ async listTunnels(opts = {}) {
500
+ return this._client._request("GET", buildQuery(`${vmPath(this.id)}/tunnels`, {
501
+ cursor: opts.cursor,
502
+ limit: opts.limit,
503
+ state: opts.state
504
+ }), void 0, this.baseUrl);
505
+ }
506
+ async getTunnel(port) {
507
+ return this._client._request("GET", `${vmPath(this.id)}/tunnels/${port}`, void 0, this.baseUrl);
508
+ }
509
+ async deleteTunnel(port) {
510
+ return this._client._request("DELETE", `${vmPath(this.id)}/tunnels/${port}`, void 0, this.baseUrl);
511
+ }
512
+ };
513
+ function buildQuery(path, params) {
514
+ const usp = new URLSearchParams();
515
+ for (const [key, value] of Object.entries(params)) {
516
+ if (value === void 0 || value === null) continue;
517
+ usp.append(key, String(value));
518
+ }
519
+ const qs = usp.toString();
520
+ return qs ? `${path}?${qs}` : path;
521
+ }
522
+ function normalizeBaseUrl(baseUrl) {
523
+ const trimmed = baseUrl.trim().replace(/\/+$/, "");
524
+ if (!trimmed) throw new Error("baseUrl must not be empty");
525
+ return trimmed;
526
+ }
527
+ function normalizeRegion(region) {
528
+ const trimmed = region.trim().toLowerCase();
529
+ if (!trimmed) throw new Error("region must not be empty");
530
+ return trimmed;
531
+ }
532
+ function computeBaseUrl(provider, region) {
533
+ const normalized = normalizeRegion(region);
534
+ return `https://${provider}-${normalized}.arker.ai/api`;
535
+ }
536
+ function parseProvider(value) {
537
+ if (!value) return DEFAULT_PROVIDER;
538
+ const trimmed = value.trim().toLowerCase();
539
+ if (trimmed === "aws-burst" || trimmed === "burst") return "aws-burst";
540
+ return "aws";
541
+ }
542
+ function splitRegion(value) {
543
+ if (!value) return {};
544
+ const normalized = value.trim().toLowerCase();
545
+ if (normalized.startsWith("aws-burst-")) {
546
+ return { region: normalized.slice("aws-burst-".length), providerFromRegion: "aws-burst" };
547
+ }
548
+ if (normalized.startsWith("aws-")) {
549
+ return { region: normalized.slice("aws-".length), providerFromRegion: "aws" };
550
+ }
551
+ return { region: normalized };
552
+ }
553
+ function isBurstRef(ref) {
554
+ const trimmed = ref.trim();
555
+ return BURST_SOURCE_REFS.has(trimmed.toLowerCase()) || BURST_VM_ID.test(trimmed);
556
+ }
557
+ function normalizeRetry(retry) {
558
+ if (retry === false) {
559
+ return { attempts: 1, baseDelayMs: 0, maxDelayMs: 0, jitterMs: 0 };
560
+ }
561
+ return {
562
+ attempts: Math.max(1, Math.floor(retry?.attempts ?? DEFAULT_RETRY_ATTEMPTS)),
563
+ baseDelayMs: Math.max(0, retry?.baseDelayMs ?? DEFAULT_RETRY_BASE_DELAY_MS),
564
+ maxDelayMs: Math.max(0, retry?.maxDelayMs ?? DEFAULT_RETRY_MAX_DELAY_MS),
565
+ jitterMs: Math.max(0, retry?.jitterMs ?? DEFAULT_RETRY_JITTER_MS)
566
+ };
567
+ }
568
+ function env(name) {
569
+ const value = globalThis.process?.env?.[name];
570
+ const trimmed = value?.trim();
571
+ return trimmed ? trimmed : void 0;
572
+ }
573
+ function vmPath(vmId) {
574
+ return `/v1/vms/${pathSegment(vmId)}`;
575
+ }
576
+ function pathSegment(value) {
577
+ return encodeURIComponent(value);
578
+ }
579
+ function withoutUndefined(value) {
580
+ if (Array.isArray(value)) return value.map(withoutUndefined);
581
+ if (!value || typeof value !== "object") return value;
582
+ const output2 = {};
583
+ for (const [key, entry] of Object.entries(value)) {
584
+ if (entry !== void 0) output2[key] = withoutUndefined(entry);
585
+ }
586
+ return output2;
587
+ }
588
+ function parseRunResponse(payload) {
589
+ const body = objectPayload(payload, "run response");
590
+ if (typeof body.stdout === "string") {
591
+ const stdout = stringValue(body.stdout, "run response.stdout");
592
+ const stdoutEncoding = stringField(body.stdout_encoding, "run response.stdout_encoding");
593
+ const stderr = stringValue(body.stderr, "run response.stderr");
594
+ const stderrEncoding = stringField(body.stderr_encoding, "run response.stderr_encoding");
595
+ return {
596
+ type: "completed",
597
+ runId: typeof body.run_id === "string" ? body.run_id : void 0,
598
+ state: typeof body.state === "string" ? body.state : "completed",
599
+ stdout: decodeBytes(stdout, stdoutEncoding),
600
+ stdoutEncoding,
601
+ stderr: decodeBytes(stderr, stderrEncoding),
602
+ stderrEncoding,
603
+ exitCode: numberField(body.exit_code, "run response.exit_code"),
604
+ failReason: typeof body.fail_reason === "string" ? body.fail_reason : null
605
+ };
606
+ }
607
+ if (typeof body.run_id === "string") {
608
+ return {
609
+ type: "background",
610
+ runId: body.run_id,
611
+ state: typeof body.state === "string" ? body.state : "running",
612
+ tunnels: Array.isArray(body.tunnels) ? body.tunnels : []
613
+ };
614
+ }
615
+ throw new ArkerError("internal", "unrecognized run response shape", 200);
616
+ }
617
+ function parseJson(text) {
618
+ if (!text) return {};
619
+ try {
620
+ return JSON.parse(text);
621
+ } catch {
622
+ return void 0;
623
+ }
624
+ }
625
+ function extractError(payload) {
626
+ if (!isObject(payload)) return void 0;
627
+ if (typeof payload.code === "string" && typeof payload.message === "string") {
628
+ return { code: payload.code, message: payload.message };
629
+ }
630
+ if (isObject(payload.error)) {
631
+ return {
632
+ code: typeof payload.error.code === "string" ? payload.error.code : "internal",
633
+ message: typeof payload.error.message === "string" ? payload.error.message : ""
634
+ };
635
+ }
636
+ return void 0;
637
+ }
638
+ function isRetryable(status, error) {
639
+ if (RETRYABLE_HTTP.has(status)) return true;
640
+ if (!error) return false;
641
+ if (RETRYABLE_CODES.has(error.code)) return true;
642
+ if (error.code !== "internal") return false;
643
+ return TRANSIENT_HINTS.some((hint) => error.message.includes(hint));
644
+ }
645
+ function retryDelay(retry, attempt) {
646
+ const base = Math.min(retry.maxDelayMs, retry.baseDelayMs * 2 ** attempt);
647
+ return base + jitter(retry.jitterMs);
648
+ }
649
+ function jitter(maxMs) {
650
+ return Math.floor(Math.random() * (maxMs + 1));
651
+ }
652
+ async function sleep(ms) {
653
+ await new Promise((resolve) => setTimeout(resolve, ms));
654
+ }
655
+ function objectPayload(value, context) {
656
+ if (!isObject(value)) throw new ArkerError("internal", `${context} must be an object`, 200);
657
+ return value;
658
+ }
659
+ function isObject(value) {
660
+ return value !== null && typeof value === "object" && !Array.isArray(value);
661
+ }
662
+ function stringField(value, context) {
663
+ if (typeof value !== "string" || value.length === 0) {
664
+ throw new ArkerError("internal", `${context} must be a non-empty string`, 200);
665
+ }
666
+ return value;
667
+ }
668
+ function stringValue(value, context) {
669
+ if (typeof value !== "string") throw new ArkerError("internal", `${context} must be a string`, 200);
670
+ return value;
671
+ }
672
+ function numberField(value, context) {
673
+ if (typeof value !== "number") throw new ArkerError("internal", `${context} must be a number`, 200);
674
+ return value;
675
+ }
676
+ function assertWriteComplete(result, context) {
677
+ if (result.complete && result.written) return;
678
+ throw new ArkerError("internal", `${context} did not complete`, 200);
679
+ }
680
+ function ulid() {
681
+ const crypto = globalThis.crypto;
682
+ if (!crypto?.getRandomValues) throw new Error("crypto.getRandomValues is required in this runtime");
683
+ const time = BigInt(Date.now()) & (1n << 48n) - 1n;
684
+ const rand = new Uint8Array(10);
685
+ crypto.getRandomValues(rand);
686
+ let raw = time << 80n | rand.reduce((acc, byte) => acc << 8n | BigInt(byte), 0n);
687
+ const out2 = [];
688
+ for (let i = 0; i < 26; i++) {
689
+ out2.push(ULID_ALPHABET[Number(raw & 31n)]);
690
+ raw >>= 5n;
691
+ }
692
+ return out2.reverse().join("");
693
+ }
694
+ function decodeBytes(text, encoding) {
695
+ if (encoding === "base64") return base64ToBytes(text);
696
+ return new TextEncoder().encode(text);
697
+ }
698
+ function bytesToBase64(data) {
699
+ const buffer = bufferConstructor();
700
+ if (buffer) return buffer.from(data).toString("base64");
701
+ let binary = "";
702
+ for (let offset = 0; offset < data.length; offset += 32768) {
703
+ const chunk = data.subarray(offset, offset + 32768);
704
+ binary += String.fromCharCode(...chunk);
705
+ }
706
+ return btoa(binary);
707
+ }
708
+ function base64ToBytes(text) {
709
+ const buffer = bufferConstructor();
710
+ if (buffer) {
711
+ const decoded = buffer.from(text, "base64");
712
+ return new Uint8Array(decoded.buffer, decoded.byteOffset, decoded.byteLength);
713
+ }
714
+ const binary = atob(text);
715
+ const out2 = new Uint8Array(binary.length);
716
+ for (let i = 0; i < binary.length; i++) out2[i] = binary.charCodeAt(i);
717
+ return out2;
718
+ }
719
+ function bufferConstructor() {
720
+ return globalThis.Buffer;
721
+ }
722
+
723
+ // src/cli.ts
724
+ function parseArgs(argv) {
725
+ const positional = [];
726
+ const flags = {};
727
+ for (let i = 0; i < argv.length; i++) {
728
+ const arg = argv[i];
729
+ if (arg.startsWith("--")) {
730
+ const eq = arg.indexOf("=");
731
+ if (eq !== -1) {
732
+ flags[arg.slice(2, eq)] = arg.slice(eq + 1);
733
+ } else {
734
+ const next = argv[i + 1];
735
+ if (next !== void 0 && !next.startsWith("--")) {
736
+ flags[arg.slice(2)] = next;
737
+ i++;
738
+ } else {
739
+ flags[arg.slice(2)] = true;
740
+ }
741
+ }
742
+ } else if (arg.startsWith("-") && arg.length > 1) {
743
+ flags[arg.slice(1)] = true;
744
+ } else {
745
+ positional.push(arg);
746
+ }
747
+ }
748
+ return { positional, flags };
749
+ }
750
+ function readFileConfig() {
751
+ const path = (0, import_node_path.join)((0, import_node_os.homedir)(), ".arker", "config.json");
752
+ if (!(0, import_node_fs.existsSync)(path)) return {};
753
+ try {
754
+ return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
755
+ } catch {
756
+ return {};
757
+ }
758
+ }
759
+ function clientFromArgs(args) {
760
+ const file = readFileConfig();
761
+ const apiKey = args.flags["api-key"] ?? process.env.ARKER_API_KEY ?? file.apiKey;
762
+ const baseUrl = args.flags["base-url"] ?? process.env.ARKER_BASE_URL ?? file.baseUrl;
763
+ const controlBaseUrl = args.flags["control-base-url"] ?? process.env.ARKER_CONTROL_BASE_URL;
764
+ const region = args.flags.region ?? process.env.ARKER_REGION ?? file.region;
765
+ const provider = args.flags.provider ?? process.env.ARKER_PROVIDER;
766
+ if (!apiKey) {
767
+ die("Missing API key. Set ARKER_API_KEY or pass --api-key.");
768
+ }
769
+ if (!baseUrl && !region) {
770
+ die("Missing region. Set ARKER_REGION or pass --region (e.g. us-west-2). --provider (aws|aws-burst) defaults to aws.");
771
+ }
772
+ return new Arker({ apiKey, baseUrl, region, provider, controlBaseUrl });
773
+ }
774
+ function out(value) {
775
+ if (typeof value === "string") {
776
+ import_node_process.stdout.write(value + "\n");
777
+ } else {
778
+ import_node_process.stdout.write(JSON.stringify(value, null, 2) + "\n");
779
+ }
780
+ }
781
+ function err(msg) {
782
+ process.stderr.write(`arker: ${msg}
783
+ `);
784
+ }
785
+ function die(msg) {
786
+ err(msg);
787
+ process.exit(1);
788
+ }
789
+ function fmtVm(vm) {
790
+ const provider = vm.provider ?? "?";
791
+ const region = vm.region ?? "?";
792
+ const name = vm.name ?? "\u2014";
793
+ const state = vm.state ?? "?";
794
+ return `${vm.vm_id ?? vm.id} ${provider}-${region} ${state} ${name}`;
795
+ }
796
+ async function cmdVms(args, client) {
797
+ const sub = args.positional[0];
798
+ const rest = args.positional.slice(1);
799
+ switch (sub) {
800
+ case void 0:
801
+ case "ls":
802
+ case "list": {
803
+ const res = await client.listVms({
804
+ provider: args.flags.provider,
805
+ region: args.flags.region,
806
+ state: args.flags.state,
807
+ cursor: args.flags.cursor,
808
+ limit: numFlag(args, "limit")
809
+ });
810
+ if (args.flags.json) return out({ vms: res.vms, next_cursor: res.nextCursor });
811
+ for (const vm of res.vms) out(fmtVm(vm));
812
+ if (res.nextCursor) out(`# next_cursor=${res.nextCursor}`);
813
+ return;
814
+ }
815
+ case "get": {
816
+ const id = rest[0] ?? die("usage: arker vms get <vm_id>");
817
+ out(await client.getVm(id));
818
+ return;
819
+ }
820
+ case "rm":
821
+ case "delete": {
822
+ const id = rest[0] ?? die("usage: arker vms rm <vm_id>");
823
+ const r = await client.vm(id).delete();
824
+ if (r.deleted) out(`deleted ${id}`);
825
+ else {
826
+ err("delete failed");
827
+ process.exitCode = 1;
828
+ }
829
+ return;
830
+ }
831
+ case "fork": {
832
+ await cmdFork({ ...args, positional: rest }, client);
833
+ return;
834
+ }
835
+ case "run": {
836
+ await cmdRun({ ...args, positional: rest }, client);
837
+ return;
838
+ }
839
+ default:
840
+ die(`unknown vms subcommand: ${sub}`);
841
+ }
842
+ }
843
+ async function cmdFork(args, client) {
844
+ const refPositional = args.positional[0];
845
+ const srcVmIdFlag = args.flags["source-vm-id"];
846
+ const srcVmNameFlag = args.flags["source-vm-name"];
847
+ const srcOrgIdFlag = args.flags["source-org-id"];
848
+ const name = args.flags.name;
849
+ const publicFlag = boolFlag(args, "public");
850
+ let sourceVmId = srcVmIdFlag;
851
+ let sourceVmName = srcVmNameFlag;
852
+ let sourceOrgId = srcOrgIdFlag;
853
+ if (!sourceVmId && !sourceVmName && refPositional) {
854
+ sourceVmName = refPositional;
855
+ }
856
+ if (!sourceVmId && !sourceVmName) {
857
+ die("usage: arker fork <vm_name> | --source-vm-id <id> | --source-vm-name <name> [--source-org-id <org>]");
858
+ }
859
+ const computer = await client.fork({
860
+ sourceVmId,
861
+ sourceVmName,
862
+ sourceOrgId,
863
+ name,
864
+ public: publicFlag
865
+ });
866
+ out({ vm_id: computer.id });
867
+ }
868
+ async function cmdRun(args, client) {
869
+ const vmId = args.positional[0] ?? die("usage: arker run <vm_id> <command...>");
870
+ const command = args.positional.slice(1).join(" ");
871
+ if (!command) die("missing command to run");
872
+ const result = await client.vm(vmId).run(command, {
873
+ background: boolFlag(args, "background"),
874
+ timeout: numFlag(args, "timeout"),
875
+ acquire: args.flags.acquire,
876
+ release: args.flags.release
877
+ });
878
+ if (result.type === "completed") {
879
+ process.stdout.write(new TextDecoder().decode(result.stdout));
880
+ if (result.stderr.length) process.stderr.write(new TextDecoder().decode(result.stderr));
881
+ process.exitCode = result.exitCode === 0 ? 0 : result.exitCode;
882
+ return;
883
+ }
884
+ out({ run_id: result.runId, tunnels: result.tunnels });
885
+ }
886
+ async function cmdRuns(args, client) {
887
+ const sub = args.positional[0];
888
+ const rest = args.positional.slice(1);
889
+ switch (sub) {
890
+ case "ls":
891
+ case "list": {
892
+ const vm = rest[0] ?? die("usage: arker runs ls <vm_id>");
893
+ const res = await client.vm(vm).listRuns({
894
+ state: args.flags.state,
895
+ cursor: args.flags.cursor,
896
+ limit: numFlag(args, "limit")
897
+ });
898
+ if (args.flags.json) return out(res);
899
+ for (const r of res.runs) {
900
+ out(`${r.run_id} ${r.state} ${r.exit_code ?? "-"} ${r.command ?? ""}`);
901
+ }
902
+ if (res.next_cursor) out(`# next_cursor=${res.next_cursor}`);
903
+ return;
904
+ }
905
+ case "get": {
906
+ const [vm, runId] = rest;
907
+ if (!vm || !runId) die("usage: arker runs get <vm_id> <run_id>");
908
+ out(await client.vm(vm).getRun(runId));
909
+ return;
910
+ }
911
+ case "rm":
912
+ case "cancel": {
913
+ const [vm, runId] = rest;
914
+ if (!vm || !runId) die("usage: arker runs rm <vm_id> <run_id>");
915
+ const r = await client.vm(vm).cancelRun(runId);
916
+ out(r.cancelled ? `cancelled ${runId}` : "cancel failed");
917
+ return;
918
+ }
919
+ default:
920
+ die(`usage: arker runs <ls|get|rm> ...`);
921
+ }
922
+ }
923
+ async function cmdSessions(args, client) {
924
+ const sub = args.positional[0];
925
+ const rest = args.positional.slice(1);
926
+ const vm = rest[0];
927
+ switch (sub) {
928
+ case "ls":
929
+ case "list": {
930
+ if (!vm) die("usage: arker sessions ls <vm_id>");
931
+ const res = await client.vm(vm).listSessions({
932
+ state: args.flags.state,
933
+ cursor: args.flags.cursor,
934
+ limit: numFlag(args, "limit")
935
+ });
936
+ if (args.flags.json) return out(res);
937
+ for (const s of res.sessions) {
938
+ out(`${s.session_id} ${s.state} ${s.cwd}`);
939
+ }
940
+ if (res.next_cursor) out(`# next_cursor=${res.next_cursor}`);
941
+ return;
942
+ }
943
+ case "get": {
944
+ if (!vm) die("usage: arker sessions get <vm_id> <session_id>");
945
+ const sid = rest[1] ?? die("missing session_id");
946
+ out(await client.vm(vm).getSession(sid));
947
+ return;
948
+ }
949
+ case "create": {
950
+ if (!vm) die("usage: arker sessions create <vm_id>");
951
+ out(await client.vm(vm).createSession({ cwd: args.flags.cwd }));
952
+ return;
953
+ }
954
+ case "rm":
955
+ case "delete": {
956
+ if (!vm) die("usage: arker sessions rm <vm_id> <session_id>");
957
+ const sid = rest[1] ?? die("missing session_id");
958
+ const r = await client.vm(vm).deleteSession(sid);
959
+ out(r.deleted ? `deleted ${sid}` : "delete failed");
960
+ return;
961
+ }
962
+ default:
963
+ die(`usage: arker sessions <ls|get|create|rm> ...`);
964
+ }
965
+ }
966
+ async function cmdSyncs(args, client) {
967
+ const sub = args.positional[0];
968
+ const rest = args.positional.slice(1);
969
+ const vm = rest[0];
970
+ switch (sub) {
971
+ case "ls":
972
+ case "list": {
973
+ if (!vm) die("usage: arker syncs ls <vm_id>");
974
+ const res = await client.vm(vm).listSyncs({
975
+ cursor: args.flags.cursor,
976
+ limit: numFlag(args, "limit"),
977
+ filesystemId: args.flags["filesystem-id"]
978
+ });
979
+ if (args.flags.json) return out(res);
980
+ for (const s of res.syncs) {
981
+ out(`${s.sync_id} ${s.filesystem_id} ${s.path}`);
982
+ }
983
+ if (res.next_cursor) out(`# next_cursor=${res.next_cursor}`);
984
+ return;
985
+ }
986
+ case "create": {
987
+ if (!vm) die("usage: arker syncs create <vm_id> --filesystem-id <fs> [--path /mnt]");
988
+ const filesystemId = args.flags["filesystem-id"];
989
+ if (!filesystemId) die("missing --filesystem-id");
990
+ out(await client.vm(vm).createSync({
991
+ filesystemId,
992
+ path: args.flags.path
993
+ }));
994
+ return;
995
+ }
996
+ case "rm":
997
+ case "delete": {
998
+ if (!vm) die("usage: arker syncs rm <vm_id> <sync_id>");
999
+ const sid = rest[1] ?? die("missing sync_id");
1000
+ const r = await client.vm(vm).deleteSync(sid);
1001
+ out(r.deleted ? `deleted ${sid}` : "delete failed");
1002
+ return;
1003
+ }
1004
+ default:
1005
+ die(`usage: arker syncs <ls|create|rm> ... (read/write files with: arker sync)`);
1006
+ }
1007
+ }
1008
+ async function cmdSync(args, client) {
1009
+ const vm = args.positional[0] ?? die("usage: arker sync <vm_id> <path> [data] (omit data to read; or pipe stdin to write)");
1010
+ const path = args.positional[1] ?? die("missing path");
1011
+ const inline = args.positional[2];
1012
+ if (inline !== void 0) {
1013
+ await client.vm(vm).sync(path, inline);
1014
+ out(`wrote ${Buffer.byteLength(inline)} bytes to ${path}`);
1015
+ return;
1016
+ }
1017
+ if (!process.stdin.isTTY) {
1018
+ const buf = await readAllStdin();
1019
+ if (buf.length > 0) {
1020
+ await client.vm(vm).sync(path, buf);
1021
+ out(`wrote ${buf.length} bytes to ${path}`);
1022
+ return;
1023
+ }
1024
+ }
1025
+ import_node_process.stdout.write(await client.vm(vm).sync(path));
1026
+ }
1027
+ async function cmdTunnels(args, client) {
1028
+ const sub = args.positional[0];
1029
+ const rest = args.positional.slice(1);
1030
+ const vm = rest[0];
1031
+ switch (sub) {
1032
+ case "ls":
1033
+ case "list": {
1034
+ if (!vm) die("usage: arker tunnels ls <vm_id>");
1035
+ const res = await client.vm(vm).listTunnels({
1036
+ state: args.flags.state,
1037
+ cursor: args.flags.cursor,
1038
+ limit: numFlag(args, "limit")
1039
+ });
1040
+ if (args.flags.json) return out(res);
1041
+ for (const t of res.tunnels) {
1042
+ out(`${t.port} ${t.state} ${t.protocol} ${t.url ?? "-"}`);
1043
+ }
1044
+ if (res.next_cursor) out(`# next_cursor=${res.next_cursor}`);
1045
+ return;
1046
+ }
1047
+ case "get": {
1048
+ if (!vm) die("usage: arker tunnels get <vm_id> <port>");
1049
+ const port = Number(rest[1] ?? die("missing port"));
1050
+ out(await client.vm(vm).getTunnel(port));
1051
+ return;
1052
+ }
1053
+ case "rm":
1054
+ case "delete": {
1055
+ if (!vm) die("usage: arker tunnels rm <vm_id> <port>");
1056
+ const port = Number(rest[1] ?? die("missing port"));
1057
+ const r = await client.vm(vm).deleteTunnel(port);
1058
+ out(r.deleted ? `deleted tunnel ${port}` : "delete failed");
1059
+ return;
1060
+ }
1061
+ default:
1062
+ die(`usage: arker tunnels <ls|get|rm> ...`);
1063
+ }
1064
+ }
1065
+ async function cmdFilesystems(args, client) {
1066
+ const sub = args.positional[0];
1067
+ const rest = args.positional.slice(1);
1068
+ switch (sub) {
1069
+ case void 0:
1070
+ case "ls":
1071
+ case "list": {
1072
+ const res = await client.listFilesystems({
1073
+ cursor: args.flags.cursor,
1074
+ limit: numFlag(args, "limit"),
1075
+ namePrefix: args.flags["name-prefix"]
1076
+ });
1077
+ if (args.flags.json) return out(res);
1078
+ for (const f of res.filesystems) {
1079
+ out(`${f.filesystem_id} ${f.name} ${f.size_bytes ?? "-"}`);
1080
+ }
1081
+ if (res.next_cursor) out(`# next_cursor=${res.next_cursor}`);
1082
+ return;
1083
+ }
1084
+ case "create": {
1085
+ const name = args.flags.name ?? rest[0];
1086
+ if (!name) die("usage: arker fs create --name <name> (or: arker fs create <name>)");
1087
+ out(await client.createFilesystem({ name }));
1088
+ return;
1089
+ }
1090
+ case "get": {
1091
+ const id = rest[0] ?? die("usage: arker fs get <filesystem_id>");
1092
+ out(await client.getFilesystem(id));
1093
+ return;
1094
+ }
1095
+ case "rm":
1096
+ case "delete": {
1097
+ const id = rest[0] ?? die("usage: arker fs rm <filesystem_id>");
1098
+ const r = await client.deleteFilesystem(id);
1099
+ out(r.deleted ? `deleted ${id}` : "delete failed");
1100
+ return;
1101
+ }
1102
+ default:
1103
+ die(`usage: arker fs <ls|create|get|rm> ...`);
1104
+ }
1105
+ }
1106
+ async function cmdShell(args, client) {
1107
+ let computer;
1108
+ const vmIdArg = args.flags["vm-id"] ?? args.positional[0];
1109
+ if (vmIdArg) {
1110
+ computer = await client.vm(vmIdArg).refresh();
1111
+ } else {
1112
+ const sourceVmName = args.flags["source-vm-name"] ?? "ubuntu-full";
1113
+ computer = await client.fork({
1114
+ sourceVmName,
1115
+ sourceOrgId: ARKER_ORG_ID
1116
+ });
1117
+ }
1118
+ const header = computer;
1119
+ const session = await computer.createSession({
1120
+ cwd: args.flags.cwd
1121
+ });
1122
+ const sessionId = session.session_id;
1123
+ const timeout = numFlag(args, "timeout");
1124
+ const preload = args.positional.slice(vmIdArg ? 1 : 0).join(" ").trim();
1125
+ const exitAfter = args.flags.exit === true;
1126
+ import_node_process.stdout.write(JSON.stringify(header, null, 2) + "\n");
1127
+ let inFlight = false;
1128
+ let exitCode = 0;
1129
+ try {
1130
+ if (preload && preload !== "exit") {
1131
+ const step = await runShellLine(computer, sessionId, preload, timeout);
1132
+ if (step.kind === "fatal") {
1133
+ err(`shell ended: ${step.message}`);
1134
+ return;
1135
+ }
1136
+ if (step.kind === "recoverable") {
1137
+ err(`error: ${step.message}`);
1138
+ exitCode = 1;
1139
+ } else {
1140
+ exitCode = step.exitCode;
1141
+ }
1142
+ }
1143
+ if (exitAfter || preload === "exit") {
1144
+ process.exit(exitCode);
1145
+ }
1146
+ const rl = readline.createInterface({ input: import_node_process.stdin, output: import_node_process.stdout, prompt: "> " });
1147
+ rl.on("SIGINT", () => {
1148
+ if (inFlight) {
1149
+ process.stderr.write("^C\n");
1150
+ return;
1151
+ }
1152
+ process.stdout.write("^C\n");
1153
+ rl.write(null, { ctrl: true, name: "u" });
1154
+ rl.prompt();
1155
+ });
1156
+ rl.prompt();
1157
+ for await (const line of rl) {
1158
+ const cmd = line.trim();
1159
+ if (!cmd) {
1160
+ rl.prompt();
1161
+ continue;
1162
+ }
1163
+ if (cmd === "exit" || cmd === "quit") break;
1164
+ inFlight = true;
1165
+ const step = await runShellLine(computer, sessionId, cmd, timeout);
1166
+ inFlight = false;
1167
+ if (step.kind === "fatal") {
1168
+ err(`shell ended: ${step.message}`);
1169
+ exitCode = 1;
1170
+ break;
1171
+ }
1172
+ if (step.kind === "recoverable") {
1173
+ err(`error: ${step.message}`);
1174
+ }
1175
+ rl.prompt();
1176
+ }
1177
+ rl.close();
1178
+ } finally {
1179
+ await computer.deleteSession(sessionId).catch(() => {
1180
+ });
1181
+ }
1182
+ if (exitCode !== 0) process.exit(exitCode);
1183
+ }
1184
+ async function runShellLine(computer, sessionId, cmd, timeout) {
1185
+ try {
1186
+ const result = await computer.run(cmd, { session_id: sessionId, timeout });
1187
+ if (result.type === "completed") {
1188
+ if (result.stdout.length) process.stdout.write(new TextDecoder().decode(result.stdout));
1189
+ if (result.stderr.length) process.stderr.write(new TextDecoder().decode(result.stderr));
1190
+ const tail = result.stderr.length > 0 ? result.stderr[result.stderr.length - 1] : result.stdout.length > 0 ? result.stdout[result.stdout.length - 1] : void 0;
1191
+ if (tail !== void 0 && tail !== 10) process.stdout.write("\n");
1192
+ return { kind: "ok", exitCode: result.exitCode };
1193
+ }
1194
+ out({ run_id: result.runId });
1195
+ return { kind: "ok", exitCode: 0 };
1196
+ } catch (e) {
1197
+ const message = e instanceof Error ? e.message : String(e);
1198
+ const kind = classifyShellError(message, computer.id);
1199
+ return { kind, message };
1200
+ }
1201
+ }
1202
+ function classifyShellError(message, vmId) {
1203
+ const msg = message.toLowerCase();
1204
+ if (msg.includes("unauthor") || msg.includes("forbidden") || msg.includes("invalid api key")) {
1205
+ return "fatal";
1206
+ }
1207
+ if ((msg.includes("not_found") || msg.includes("not found")) && msg.includes(vmId.toLowerCase())) {
1208
+ return "fatal";
1209
+ }
1210
+ return "recoverable";
1211
+ }
1212
+ function numFlag(args, name) {
1213
+ const v = args.flags[name];
1214
+ if (typeof v === "string") return Number(v);
1215
+ return void 0;
1216
+ }
1217
+ function boolFlag(args, name) {
1218
+ const v = args.flags[name];
1219
+ if (v === void 0) return void 0;
1220
+ if (typeof v === "boolean") return v;
1221
+ if (v === "false" || v === "0") return false;
1222
+ return true;
1223
+ }
1224
+ async function readAllStdin() {
1225
+ const chunks = [];
1226
+ for await (const chunk of import_node_process.stdin) chunks.push(chunk);
1227
+ return new Uint8Array(Buffer.concat(chunks));
1228
+ }
1229
+ function usage() {
1230
+ out(
1231
+ [
1232
+ "arker \u2014 VM control plane CLI",
1233
+ "",
1234
+ "Usage:",
1235
+ " arker <command> [args]",
1236
+ "",
1237
+ "Shortcuts:",
1238
+ " arker ls list VMs",
1239
+ " arker rm <vm> delete VM",
1240
+ " arker fork <vm_name> fork the public golden (in Arker org)",
1241
+ " arker fork --source-vm-id <id> fork by global id",
1242
+ " arker fork --source-vm-name <n> --source-org-id <org>",
1243
+ " fork by name in another org",
1244
+ " arker run <vm> <command> run a command",
1245
+ " arker shell [vm_id] interactive shell (forks arkuntu if no vm)",
1246
+ "",
1247
+ "Resources:",
1248
+ " arker vms <ls|get|rm|fork|run> ...",
1249
+ " arker runs <ls|get|rm> <vm_id> ...",
1250
+ " arker sessions <ls|get|create|rm> <vm_id> ...",
1251
+ " arker syncs <ls|create|rm> <vm_id> ...",
1252
+ " arker tunnels <ls|get|rm> <vm_id> ...",
1253
+ " arker filesystems <ls|create|get|rm> ... (alias: fs)",
1254
+ "",
1255
+ "Flags:",
1256
+ " --api-key <key> (or env ARKER_API_KEY)",
1257
+ " --region <region> (or env ARKER_REGION; e.g. us-west-2)",
1258
+ " --provider <aws|aws-burst> (or env ARKER_PROVIDER; default aws)",
1259
+ " --base-url <url> override compute URL (env ARKER_BASE_URL)",
1260
+ " --control-base-url <url> override CF Worker URL (env ARKER_CONTROL_BASE_URL)",
1261
+ " --json emit JSON instead of tabular output",
1262
+ "",
1263
+ `Arker org id: ${ARKER_ORG_ID}`
1264
+ ].join("\n")
1265
+ );
1266
+ process.exit(2);
1267
+ }
1268
+ async function main() {
1269
+ const argv = process.argv.slice(2);
1270
+ if (argv.length === 0 || argv[0] === "-h" || argv[0] === "--help") usage();
1271
+ const cmd = argv[0];
1272
+ const args = parseArgs(argv.slice(1));
1273
+ const client = clientFromArgs(args);
1274
+ try {
1275
+ switch (cmd) {
1276
+ // Shortcuts.
1277
+ case "ls":
1278
+ case "list":
1279
+ return await cmdVms({ ...args, positional: ["ls", ...args.positional] }, client);
1280
+ case "rm":
1281
+ case "delete":
1282
+ return await cmdVms({ ...args, positional: ["rm", ...args.positional] }, client);
1283
+ case "fork":
1284
+ return await cmdFork(args, client);
1285
+ case "run":
1286
+ return await cmdRun(args, client);
1287
+ case "sync":
1288
+ return await cmdSync(args, client);
1289
+ case "syncs":
1290
+ return await cmdSyncs(args, client);
1291
+ case "shell":
1292
+ return await cmdShell(args, client);
1293
+ // Resources.
1294
+ case "vms":
1295
+ return await cmdVms(args, client);
1296
+ case "runs":
1297
+ return await cmdRuns(args, client);
1298
+ case "sessions":
1299
+ return await cmdSessions(args, client);
1300
+ case "tunnels":
1301
+ return await cmdTunnels(args, client);
1302
+ case "filesystems":
1303
+ case "fs":
1304
+ return await cmdFilesystems(args, client);
1305
+ default:
1306
+ die(`unknown command: ${cmd}. Run 'arker --help'.`);
1307
+ }
1308
+ } catch (e) {
1309
+ if (e instanceof ArkerError) {
1310
+ err(`${e.code}: ${e.message}`);
1311
+ process.exit(1);
1312
+ }
1313
+ throw e;
1314
+ }
1315
+ }
1316
+ void main();