@arker-ai/sdk 0.5.1 → 0.6.2

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 CHANGED
@@ -25,9 +25,9 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
25
25
 
26
26
  // src/cli.ts
27
27
  var import_node_fs = require("fs");
28
+ var import_node_child_process = require("child_process");
28
29
  var import_node_os = require("os");
29
30
  var import_node_path = require("path");
30
- var readline = __toESM(require("readline/promises"), 1);
31
31
  var import_node_process = require("process");
32
32
 
33
33
  // src/index.ts
@@ -153,6 +153,12 @@ var Arker = class {
153
153
  );
154
154
  }
155
155
  const sourceOrgId = src.sourceOrgId ?? (src.sourceVmName !== void 0 && GOLDEN_NAMES.has(src.sourceVmName) ? ARKER_ORG_ID : void 0);
156
+ const legacy = src;
157
+ const resources = src.resources ?? (legacy.vcpu_count != null || legacy.memory_mib != null || legacy.disk_mib != null ? {
158
+ vcpu: legacy.vcpu_count ?? null,
159
+ memory_mib: legacy.memory_mib ?? null,
160
+ disk_mib: legacy.disk_mib ?? null
161
+ } : null);
156
162
  const body = {
157
163
  source_vm_id: src.sourceVmId ?? null,
158
164
  source_vm_name: src.sourceVmName ?? null,
@@ -160,12 +166,10 @@ var Arker = class {
160
166
  name: src.name ?? null,
161
167
  public: src.public ?? null,
162
168
  network: src.network ?? null,
169
+ egress: src.egress ?? null,
163
170
  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
171
+ durable: src.durable ?? null,
172
+ resources
169
173
  };
170
174
  const useBurst = sourceOrgId === ARKER_ORG_ID && src.sourceVmName !== void 0 && isBurstRef(src.sourceVmName);
171
175
  const baseUrl = useBurst && this.burstBaseUrl ? this.burstBaseUrl : this.baseUrl;
@@ -196,6 +200,33 @@ var Arker = class {
196
200
  });
197
201
  return { vms, nextCursor: resp.next_cursor ?? null };
198
202
  }
203
+ /**
204
+ * List run activity visible to the authenticated caller across VMs,
205
+ * providers, and regions. Admin call — routed through the control plane.
206
+ */
207
+ async listRuns(opts = {}) {
208
+ return this._request("GET", buildQuery("/v1/runs", {
209
+ since: opts.since,
210
+ until: opts.until,
211
+ vm: opts.vm,
212
+ vms: opts.vmIds && opts.vmIds.length > 0 ? opts.vmIds.join(",") : void 0,
213
+ region: opts.region,
214
+ provider: opts.provider,
215
+ source: opts.source,
216
+ search: opts.search,
217
+ limit: opts.limit,
218
+ offset: opts.offset,
219
+ lite: opts.lite === void 0 ? void 0 : opts.lite,
220
+ runtime: opts.runtime,
221
+ endpoint: opts.endpoint,
222
+ actions: opts.actions && opts.actions.length > 0 ? opts.actions.join(",") : void 0,
223
+ status: opts.status && opts.status.length > 0 ? opts.status.join(",") : void 0,
224
+ status_min: opts.statusMin,
225
+ status_max: opts.statusMax,
226
+ sort: opts.sort,
227
+ dir: opts.dir
228
+ }), void 0, this.controlBaseUrl);
229
+ }
199
230
  /** Compute call — goes direct to the backend hosting this VM (no
200
231
  * control-plane hop). Returns a fully-populated VM handle. */
201
232
  async getVm(vmId) {
@@ -285,6 +316,10 @@ var Arker = class {
285
316
  return retryDelay(this.retry, attempt);
286
317
  }
287
318
  /** @internal */
319
+ _authHeaders() {
320
+ return { authorization: `Bearer ${this.apiKey}` };
321
+ }
322
+ /** @internal */
288
323
  _baseUrlFor(ref) {
289
324
  if (isBurstRef(ref) && this.burstBaseUrl) return this.burstBaseUrl;
290
325
  return this.baseUrl;
@@ -310,12 +345,15 @@ var VM = class _VM {
310
345
  vcpu_count;
311
346
  memory_mib;
312
347
  disk_mib;
348
+ network;
349
+ max_vcpus;
350
+ max_memory_mib;
351
+ min_memory_mib;
313
352
  started_at;
314
353
  root_source_vm_id;
315
354
  root_source_vm_name;
316
355
  worker_id;
317
356
  sessions;
318
- tunnels;
319
357
  constructor(client, vmId, baseUrl = client._baseUrlFor(vmId), data) {
320
358
  this._client = client;
321
359
  this.id = vmId;
@@ -438,8 +476,25 @@ var VM = class _VM {
438
476
  }
439
477
  throw new ArkerError(lastError?.code ?? "internal", lastError?.message ?? "write failed", 200);
440
478
  }
479
+ /**
480
+ * Update this VM's resource allocation and/or network settings via
481
+ * `PATCH /v1/vms/{id}`. Returns the updated `Vm`.
482
+ *
483
+ * Accepts either a `PatchVmRequest` (`{ resources, network }`) or, for
484
+ * convenience, flat resource fields (`{ vcpu, memory_mib, disk_mib }`)
485
+ * which are folded into `resources`.
486
+ */
441
487
  async resize(request) {
442
- return this._client._request("POST", `${vmPath(this.id)}/resize`, request, this.baseUrl);
488
+ const r = request;
489
+ const body = r.resources !== void 0 || r.vcpu === void 0 && r.memory_mib === void 0 && r.disk_mib === void 0 ? { resources: r.resources ?? null, network: r.network ?? null } : {
490
+ resources: {
491
+ vcpu: r.vcpu ?? null,
492
+ memory_mib: r.memory_mib ?? null,
493
+ disk_mib: r.disk_mib ?? null
494
+ },
495
+ network: r.network ?? null
496
+ };
497
+ return this._client._request("PATCH", vmPath(this.id), body, this.baseUrl);
443
498
  }
444
499
  async delete() {
445
500
  return this._client._request("DELETE", vmPath(this.id), void 0, this.baseUrl);
@@ -495,19 +550,30 @@ var VM = class _VM {
495
550
  async deleteSession(sessionId) {
496
551
  return this._client._request("DELETE", `${vmPath(this.id)}/sessions/${pathSegment(sessionId)}`, void 0, this.baseUrl);
497
552
  }
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);
553
+ async connectPty(options = {}) {
554
+ const sessionId = options.sessionId ?? sessionIdFrom(await this.createSession());
555
+ const useTicket = options.useTicket ?? !isNodeRuntime();
556
+ const params = {
557
+ cols: options.cols,
558
+ rows: options.rows,
559
+ command: options.command,
560
+ persist: options.persist,
561
+ cancel_ttl_secs: options.cancelTtlSecs && options.cancelTtlSecs > 0 ? Math.floor(options.cancelTtlSecs) : void 0
562
+ };
563
+ let ticket;
564
+ if (useTicket) {
565
+ const response = await this._client._request(
566
+ "POST",
567
+ `${vmPath(this.id)}/sessions/${pathSegment(sessionId)}/pty-ticket`,
568
+ {},
569
+ this.baseUrl
570
+ );
571
+ ticket = response.ticket;
572
+ }
573
+ const url = buildPtyWebSocketUrl(this.baseUrl, this.id, sessionId, { ...params, ticket });
574
+ const factory = options.webSocketFactory ?? (useTicket ? browserPtyWebSocketFactory : nodePtyWebSocketFactory);
575
+ const socket = await factory(url, useTicket ? {} : { headers: this._client._authHeaders() });
576
+ return new PtyConnectionImpl(sessionId, socket);
511
577
  }
512
578
  };
513
579
  function buildQuery(path, params) {
@@ -519,6 +585,176 @@ function buildQuery(path, params) {
519
585
  const qs = usp.toString();
520
586
  return qs ? `${path}?${qs}` : path;
521
587
  }
588
+ function buildPtyWebSocketUrl(baseUrl, vmId, sessionId, params) {
589
+ const url = new URL(`${normalizeBaseUrl(baseUrl)}${vmPath(vmId)}/sessions/${pathSegment(sessionId)}/pty`);
590
+ if (url.protocol === "https:") url.protocol = "wss:";
591
+ else if (url.protocol === "http:") url.protocol = "ws:";
592
+ else throw new Error(`unsupported PTY WebSocket protocol: ${url.protocol}`);
593
+ for (const [key, value] of Object.entries(params)) {
594
+ if (value === void 0 || value === null) continue;
595
+ url.searchParams.set(key, String(value));
596
+ }
597
+ return url.toString();
598
+ }
599
+ function sessionIdFrom(session) {
600
+ const id = session.session_id ?? session.id;
601
+ if (!id) throw new ArkerError("internal", "createSession response missing session_id", 200);
602
+ return id;
603
+ }
604
+ function isNodeRuntime() {
605
+ return typeof process !== "undefined" && Boolean(process.versions?.node);
606
+ }
607
+ async function nodePtyWebSocketFactory(url, init) {
608
+ const ws = await import("ws");
609
+ return new ws.default(url, { headers: init.headers });
610
+ }
611
+ function browserPtyWebSocketFactory(url) {
612
+ if (typeof globalThis.WebSocket !== "function") {
613
+ throw new Error("WebSocket is not available in this runtime");
614
+ }
615
+ return new globalThis.WebSocket(url);
616
+ }
617
+ var PtyConnectionImpl = class {
618
+ constructor(sessionId, socket) {
619
+ this.sessionId = sessionId;
620
+ this.socket = socket;
621
+ try {
622
+ socket.binaryType = "arraybuffer";
623
+ } catch {
624
+ }
625
+ this.ready = waitForSocketOpen(socket);
626
+ addSocketListener(socket, "message", (event) => {
627
+ const data = messageData(event);
628
+ if (data !== void 0) this.emitData(bytesFromMessageData(data));
629
+ });
630
+ addSocketListener(socket, "close", (event) => this.emitClose(closeEvent(event)));
631
+ addSocketListener(socket, "error", (event) => this.emitError(event));
632
+ }
633
+ sessionId;
634
+ socket;
635
+ ready;
636
+ dataListeners = /* @__PURE__ */ new Set();
637
+ closeListeners = /* @__PURE__ */ new Set();
638
+ errorListeners = /* @__PURE__ */ new Set();
639
+ onData(listener) {
640
+ this.dataListeners.add(listener);
641
+ return () => this.dataListeners.delete(listener);
642
+ }
643
+ onClose(listener) {
644
+ this.closeListeners.add(listener);
645
+ return () => this.closeListeners.delete(listener);
646
+ }
647
+ onError(listener) {
648
+ this.errorListeners.add(listener);
649
+ return () => this.errorListeners.delete(listener);
650
+ }
651
+ send(data) {
652
+ this.socket.send(ptyInputBytes(data));
653
+ }
654
+ resize(cols, rows) {
655
+ this.socket.send(JSON.stringify({ type: "resize", cols: clampPtyDimension(cols), rows: clampPtyDimension(rows) }));
656
+ }
657
+ kill() {
658
+ this.socket.send(JSON.stringify({ type: "kill" }));
659
+ }
660
+ close(code, reason) {
661
+ this.socket.close(code, reason);
662
+ }
663
+ emitData(data) {
664
+ for (const listener of this.dataListeners) listener(data);
665
+ }
666
+ emitClose(event) {
667
+ for (const listener of this.closeListeners) listener(event);
668
+ }
669
+ emitError(error) {
670
+ for (const listener of this.errorListeners) listener(error);
671
+ }
672
+ };
673
+ function waitForSocketOpen(socket) {
674
+ if (socket.readyState === 1) return Promise.resolve();
675
+ return new Promise((resolve, reject) => {
676
+ let removeOpen;
677
+ let removeError;
678
+ let removeClose;
679
+ const cleanup = () => {
680
+ removeOpen?.();
681
+ removeError?.();
682
+ removeClose?.();
683
+ };
684
+ removeOpen = addSocketListener(socket, "open", () => {
685
+ cleanup();
686
+ resolve();
687
+ });
688
+ removeError = addSocketListener(socket, "error", (event) => {
689
+ cleanup();
690
+ reject(event instanceof Error ? event : new Error("PTY WebSocket failed to open"));
691
+ });
692
+ removeClose = addSocketListener(socket, "close", (event) => {
693
+ cleanup();
694
+ const ev = closeEvent(event);
695
+ reject(new Error(`PTY WebSocket closed before opening${ev.code ? ` (${ev.code})` : ""}`));
696
+ });
697
+ });
698
+ }
699
+ function addSocketListener(socket, type, listener) {
700
+ if (socket.addEventListener) {
701
+ socket.addEventListener(type, listener);
702
+ return () => socket.removeEventListener?.(type, listener);
703
+ }
704
+ if (socket.on) {
705
+ const nodeListener = (...args) => {
706
+ if (type === "message") listener({ data: args[0] });
707
+ else if (type === "close") listener({ code: args[0], reason: args[1] });
708
+ else listener(args[0]);
709
+ };
710
+ socket.on(type, nodeListener);
711
+ return () => socket.off?.(type, nodeListener);
712
+ }
713
+ return () => {
714
+ };
715
+ }
716
+ function messageData(event) {
717
+ if (event && typeof event === "object" && "data" in event) {
718
+ return event.data;
719
+ }
720
+ return void 0;
721
+ }
722
+ function closeEvent(event) {
723
+ if (!event || typeof event !== "object") return {};
724
+ const raw = event;
725
+ return {
726
+ code: typeof raw.code === "number" ? raw.code : void 0,
727
+ reason: typeof raw.reason === "string" ? raw.reason : void 0
728
+ };
729
+ }
730
+ function bytesFromMessageData(data) {
731
+ if (typeof data === "string") return new TextEncoder().encode(data);
732
+ if (data instanceof Uint8Array) return data;
733
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
734
+ if (ArrayBuffer.isView(data)) {
735
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
736
+ }
737
+ if (Array.isArray(data)) {
738
+ const chunks = data.map(bytesFromMessageData);
739
+ const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
740
+ const out2 = new Uint8Array(total);
741
+ let offset = 0;
742
+ for (const chunk of chunks) {
743
+ out2.set(chunk, offset);
744
+ offset += chunk.length;
745
+ }
746
+ return out2;
747
+ }
748
+ return new Uint8Array();
749
+ }
750
+ function ptyInputBytes(data) {
751
+ if (typeof data === "string") return new TextEncoder().encode(data);
752
+ return data;
753
+ }
754
+ function clampPtyDimension(value) {
755
+ if (!Number.isFinite(value)) return 1;
756
+ return Math.max(1, Math.min(1e3, Math.trunc(value)));
757
+ }
522
758
  function normalizeBaseUrl(baseUrl) {
523
759
  const trimmed = baseUrl.trim().replace(/\/+$/, "");
524
760
  if (!trimmed) throw new Error("baseUrl must not be empty");
@@ -601,15 +837,17 @@ function parseRunResponse(payload) {
601
837
  stderr: decodeBytes(stderr, stderrEncoding),
602
838
  stderrEncoding,
603
839
  exitCode: numberField(body.exit_code, "run response.exit_code"),
604
- failReason: typeof body.fail_reason === "string" ? body.fail_reason : null
840
+ failReason: typeof body.fail_reason === "string" ? body.fail_reason : null,
841
+ memoryRequestedMib: optionalNumberOrNull(body.memory_requested_mib),
842
+ memoryAchievedMib: optionalNumberOrNull(body.memory_achieved_mib),
843
+ memoryPartial: typeof body.memory_partial === "boolean" ? body.memory_partial : void 0
605
844
  };
606
845
  }
607
846
  if (typeof body.run_id === "string") {
608
847
  return {
609
848
  type: "background",
610
849
  runId: body.run_id,
611
- state: typeof body.state === "string" ? body.state : "running",
612
- tunnels: Array.isArray(body.tunnels) ? body.tunnels : []
850
+ state: typeof body.state === "string" ? body.state : "running"
613
851
  };
614
852
  }
615
853
  throw new ArkerError("internal", "unrecognized run response shape", 200);
@@ -673,6 +911,10 @@ function numberField(value, context) {
673
911
  if (typeof value !== "number") throw new ArkerError("internal", `${context} must be a number`, 200);
674
912
  return value;
675
913
  }
914
+ function optionalNumberOrNull(value) {
915
+ if (value === null || typeof value === "number") return value;
916
+ return void 0;
917
+ }
676
918
  function assertWriteComplete(result, context) {
677
919
  if (result.complete && result.written) return;
678
920
  throw new ArkerError("internal", `${context} did not complete`, 200);
@@ -748,20 +990,25 @@ function parseArgs(argv) {
748
990
  return { positional, flags };
749
991
  }
750
992
  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 {};
993
+ for (const name of ["config.json", "config"]) {
994
+ const path = (0, import_node_path.join)((0, import_node_os.homedir)(), ".arker", name);
995
+ if (!(0, import_node_fs.existsSync)(path)) continue;
996
+ try {
997
+ return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
998
+ } catch {
999
+ return {};
1000
+ }
757
1001
  }
1002
+ return {};
758
1003
  }
759
1004
  function clientFromArgs(args) {
760
1005
  const file = readFileConfig();
1006
+ const explicitBaseUrl = args.flags["base-url"] ?? process.env.ARKER_BASE_URL;
1007
+ const explicitRegion = args.flags.region ?? process.env.ARKER_REGION;
761
1008
  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;
1009
+ const baseUrl = explicitBaseUrl ?? (explicitRegion ? void 0 : file.baseUrl);
763
1010
  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;
1011
+ const region = explicitRegion ?? file.region;
765
1012
  const provider = args.flags.provider ?? process.env.ARKER_PROVIDER;
766
1013
  if (!apiKey) {
767
1014
  die("Missing API key. Set ARKER_API_KEY or pass --api-key.");
@@ -791,7 +1038,8 @@ function fmtVm(vm) {
791
1038
  const region = vm.region ?? "?";
792
1039
  const name = vm.name ?? "\u2014";
793
1040
  const state = vm.state ?? "?";
794
- return `${vm.vm_id ?? vm.id} ${provider}-${region} ${state} ${name}`;
1041
+ const id = vm.vm_id ?? vm.id;
1042
+ return `${id} ${provider}-${region} ${state} ${name}`;
795
1043
  }
796
1044
  async function cmdVms(args, client) {
797
1045
  const sub = args.positional[0];
@@ -836,6 +1084,10 @@ async function cmdVms(args, client) {
836
1084
  await cmdRun({ ...args, positional: rest }, client);
837
1085
  return;
838
1086
  }
1087
+ case "resize": {
1088
+ await cmdResize({ ...args, positional: rest }, client);
1089
+ return;
1090
+ }
839
1091
  default:
840
1092
  die(`unknown vms subcommand: ${sub}`);
841
1093
  }
@@ -854,14 +1106,22 @@ async function cmdFork(args, client) {
854
1106
  sourceVmName = refPositional;
855
1107
  }
856
1108
  if (!sourceVmId && !sourceVmName) {
857
- die("usage: arker fork <vm_name> | --source-vm-id <id> | --source-vm-name <name> [--source-org-id <org>]");
858
- }
1109
+ die("usage: arker fork <vm_name> | --source-vm-id <id> | --source-vm-name <name> [--source-org-id <org>]\n [--vcpu N] [--memory-mib N] [--disk-mib N] [--no-disk]");
1110
+ }
1111
+ const vcpu = numFlag(args, "vcpu");
1112
+ const memoryMib = numFlag(args, "memory-mib");
1113
+ const diskMib = numFlag(args, "disk-mib");
1114
+ const hasResources = vcpu !== void 0 || memoryMib !== void 0 || diskMib !== void 0;
1115
+ const resources = hasResources ? { vcpu: vcpu ?? null, memory_mib: memoryMib ?? null, disk_mib: diskMib ?? null } : void 0;
1116
+ const disk = boolFlag(args, "no-disk") ? false : void 0;
859
1117
  const computer = await client.fork({
860
1118
  sourceVmId,
861
1119
  sourceVmName,
862
1120
  sourceOrgId,
863
1121
  name,
864
- public: publicFlag
1122
+ public: publicFlag,
1123
+ ...resources ? { resources } : {},
1124
+ ...disk !== void 0 ? { disk } : {}
865
1125
  });
866
1126
  out({ vm_id: computer.id });
867
1127
  }
@@ -869,19 +1129,54 @@ async function cmdRun(args, client) {
869
1129
  const vmId = args.positional[0] ?? die("usage: arker run <vm_id> <command...>");
870
1130
  const command = args.positional.slice(1).join(" ");
871
1131
  if (!command) die("missing command to run");
1132
+ const sessionIdx = numFlag(args, "session-idx");
872
1133
  const result = await client.vm(vmId).run(command, {
873
1134
  background: boolFlag(args, "background"),
874
1135
  timeout: numFlag(args, "timeout"),
875
1136
  acquire: args.flags.acquire,
876
- release: args.flags.release
1137
+ release: args.flags.release,
1138
+ session_id: args.flags["session-id"],
1139
+ ...sessionIdx !== void 0 ? { session_idx: sessionIdx } : {}
877
1140
  });
1141
+ if (args.flags.json) {
1142
+ out(runResultForJson(result));
1143
+ if (result.type === "completed") process.exitCode = result.exitCode === 0 ? 0 : result.exitCode;
1144
+ return;
1145
+ }
878
1146
  if (result.type === "completed") {
1147
+ if (result.memoryPartial) {
1148
+ err(`Memory target partially applied: requested ${formatMib(result.memoryRequestedMib)}, achieved ${formatMib(result.memoryAchievedMib)}.`);
1149
+ }
879
1150
  process.stdout.write(new TextDecoder().decode(result.stdout));
880
1151
  if (result.stderr.length) process.stderr.write(new TextDecoder().decode(result.stderr));
881
1152
  process.exitCode = result.exitCode === 0 ? 0 : result.exitCode;
882
1153
  return;
883
1154
  }
884
- out({ run_id: result.runId, tunnels: result.tunnels });
1155
+ out({ run_id: result.runId, state: result.state });
1156
+ }
1157
+ function formatMib(value) {
1158
+ return typeof value === "number" ? `${value} MiB` : "unknown";
1159
+ }
1160
+ function runResultForJson(result) {
1161
+ switch (result.type) {
1162
+ case "completed":
1163
+ return {
1164
+ type: result.type,
1165
+ runId: result.runId,
1166
+ state: result.state,
1167
+ stdout: new TextDecoder().decode(result.stdout),
1168
+ stdoutEncoding: result.stdoutEncoding,
1169
+ stderr: new TextDecoder().decode(result.stderr),
1170
+ stderrEncoding: result.stderrEncoding,
1171
+ exitCode: result.exitCode,
1172
+ failReason: result.failReason,
1173
+ memoryRequestedMib: result.memoryRequestedMib,
1174
+ memoryAchievedMib: result.memoryAchievedMib,
1175
+ memoryPartial: result.memoryPartial
1176
+ };
1177
+ case "background":
1178
+ return result;
1179
+ }
885
1180
  }
886
1181
  async function cmdRuns(args, client) {
887
1182
  const sub = args.positional[0];
@@ -1024,43 +1319,24 @@ async function cmdSync(args, client) {
1024
1319
  }
1025
1320
  import_node_process.stdout.write(await client.vm(vm).sync(path));
1026
1321
  }
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;
1322
+ async function cmdResize(args, client) {
1323
+ const vm = args.positional[0];
1324
+ if (!vm) die("usage: arker resize <vm_id> [--memory-mib N] [--vcpu N] [--disk-mib N]");
1325
+ const memoryMib = numFlag(args, "memory-mib");
1326
+ const vcpu = numFlag(args, "vcpu");
1327
+ const diskMib = numFlag(args, "disk-mib");
1328
+ if (memoryMib === void 0 && vcpu === void 0 && diskMib === void 0) {
1329
+ die("resize: pass at least one of --memory-mib, --vcpu, --disk-mib");
1330
+ }
1331
+ const updated = await client.vm(vm).resize({
1332
+ resources: {
1333
+ vcpu: vcpu ?? null,
1334
+ memory_mib: memoryMib ?? null,
1335
+ disk_mib: diskMib ?? null
1060
1336
  }
1061
- default:
1062
- die(`usage: arker tunnels <ls|get|rm> ...`);
1063
- }
1337
+ });
1338
+ if (args.flags.json) return out(updated);
1339
+ out(fmtVm(updated));
1064
1340
  }
1065
1341
  async function cmdFilesystems(args, client) {
1066
1342
  const sub = args.positional[0];
@@ -1106,6 +1382,10 @@ async function cmdFilesystems(args, client) {
1106
1382
  async function cmdShell(args, client) {
1107
1383
  let computer;
1108
1384
  const vmIdArg = args.flags["vm-id"] ?? args.positional[0];
1385
+ const explicitSessionId = args.flags["session-id"];
1386
+ if (!vmIdArg && explicitSessionId) {
1387
+ die("usage: arker shell <vm_id> --session-id <session_id>");
1388
+ }
1109
1389
  if (vmIdArg) {
1110
1390
  computer = await client.vm(vmIdArg).refresh();
1111
1391
  } else {
@@ -1114,100 +1394,242 @@ async function cmdShell(args, client) {
1114
1394
  sourceVmName,
1115
1395
  sourceOrgId: ARKER_ORG_ID
1116
1396
  });
1397
+ err(`forked ${computer.id}`);
1117
1398
  }
1118
- const header = computer;
1119
- const session = await computer.createSession({
1120
- cwd: args.flags.cwd
1399
+ let sessionId = explicitSessionId;
1400
+ if (!sessionId) {
1401
+ const session = await computer.createSession({
1402
+ cwd: args.flags.cwd
1403
+ });
1404
+ sessionId = session.session_id ?? session.id;
1405
+ if (!sessionId) die("createSession response missing session_id");
1406
+ }
1407
+ const persist = args.flags["no-persist"] === true ? false : boolFlag(args, "persist");
1408
+ const colsFlag = numFlag(args, "cols");
1409
+ const rowsFlag = numFlag(args, "rows");
1410
+ const cols = colsFlag ?? import_node_process.stdout.columns ?? 80;
1411
+ const rows = rowsFlag ?? import_node_process.stdout.rows ?? 24;
1412
+ const cancelTtlSecs = numFlag(args, "cancel-ttl");
1413
+ const pty = await computer.connectPty({
1414
+ sessionId,
1415
+ cols,
1416
+ rows,
1417
+ command: args.flags.command,
1418
+ persist,
1419
+ cancelTtlSecs
1121
1420
  });
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;
1421
+ err(`connected ${computer.id} session ${sessionId}`);
1422
+ const exitCode = await bridgePty(pty, {
1423
+ fallbackCols: cols,
1424
+ fallbackRows: rows,
1425
+ autoResize: colsFlag === void 0 && rowsFlag === void 0 && Boolean(import_node_process.stdout.isTTY)
1426
+ });
1427
+ if (exitCode !== 0) process.exit(exitCode);
1428
+ }
1429
+ function bridgePty(pty, options) {
1430
+ return new Promise((resolve) => {
1431
+ let settled = false;
1432
+ let rawEnabled = false;
1433
+ const wasRaw = Boolean(import_node_process.stdin.isTTY && import_node_process.stdin.isRaw);
1434
+ const restoreTerminal = () => {
1435
+ if (rawEnabled && import_node_process.stdin.isTTY && typeof import_node_process.stdin.setRawMode === "function") {
1436
+ import_node_process.stdin.setRawMode(wasRaw);
1151
1437
  }
1152
- process.stdout.write("^C\n");
1153
- rl.write(null, { ctrl: true, name: "u" });
1154
- rl.prompt();
1438
+ rawEnabled = false;
1439
+ };
1440
+ const finish = (code) => {
1441
+ if (settled) return;
1442
+ settled = true;
1443
+ restoreTerminal();
1444
+ import_node_process.stdin.off("data", onInput);
1445
+ import_node_process.stdin.off("end", onInputEnd);
1446
+ process.off("SIGWINCH", onResize);
1447
+ process.off("SIGINT", onSigint);
1448
+ process.off("SIGTERM", onSigterm);
1449
+ process.off("SIGHUP", onSighup);
1450
+ process.off("exit", restoreTerminal);
1451
+ offData();
1452
+ offClose();
1453
+ offError();
1454
+ resolve(code);
1455
+ };
1456
+ const onInput = (chunk) => {
1457
+ pty.send(chunk);
1458
+ };
1459
+ const onInputEnd = () => {
1460
+ pty.close();
1461
+ };
1462
+ const onResize = () => {
1463
+ pty.resize(import_node_process.stdout.columns ?? options.fallbackCols, import_node_process.stdout.rows ?? options.fallbackRows);
1464
+ };
1465
+ const onSigint = () => {
1466
+ pty.send(new Uint8Array([3]));
1467
+ };
1468
+ const onSigterm = () => {
1469
+ pty.close();
1470
+ finish(143);
1471
+ };
1472
+ const onSighup = () => {
1473
+ pty.close();
1474
+ finish(129);
1475
+ };
1476
+ const offData = pty.onData((data) => {
1477
+ import_node_process.stdout.write(data);
1155
1478
  });
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}`);
1479
+ const offClose = pty.onClose(() => finish(0));
1480
+ const offError = pty.onError((error) => {
1481
+ const message = error instanceof Error ? error.message : String(error);
1482
+ err(`pty error: ${message}`);
1483
+ });
1484
+ process.once("exit", restoreTerminal);
1485
+ pty.ready.then(() => {
1486
+ if (settled) return;
1487
+ if (import_node_process.stdin.isTTY && typeof import_node_process.stdin.setRawMode === "function") {
1488
+ import_node_process.stdin.setRawMode(true);
1489
+ rawEnabled = true;
1174
1490
  }
1175
- rl.prompt();
1176
- }
1177
- rl.close();
1178
- } finally {
1179
- await computer.deleteSession(sessionId).catch(() => {
1491
+ import_node_process.stdin.resume();
1492
+ import_node_process.stdin.on("data", onInput);
1493
+ import_node_process.stdin.on("end", onInputEnd);
1494
+ if (options.autoResize) process.on("SIGWINCH", onResize);
1495
+ process.on("SIGINT", onSigint);
1496
+ process.on("SIGTERM", onSigterm);
1497
+ process.on("SIGHUP", onSighup);
1498
+ if (options.autoResize) onResize();
1499
+ }).catch((error) => {
1500
+ const message = error instanceof Error ? error.message : String(error);
1501
+ err(`pty failed to open: ${message}`);
1502
+ finish(1);
1180
1503
  });
1504
+ });
1505
+ }
1506
+ var SSH_PORT_DEFAULT = 22;
1507
+ function defaultIdentityBase() {
1508
+ return (0, import_node_path.join)((0, import_node_os.homedir)(), ".ssh", "id_ed25519");
1509
+ }
1510
+ function resolveLocalSshKey(args) {
1511
+ const explicit = args.flags.identity;
1512
+ const privateKeyPath = explicit ? explicit.replace(/\.pub$/, "") : defaultIdentityBase();
1513
+ const publicKeyPath = `${privateKeyPath}.pub`;
1514
+ if (!(0, import_node_fs.existsSync)(publicKeyPath)) {
1515
+ if (!boolFlag(args, "generate")) {
1516
+ die(
1517
+ `no SSH public key at ${publicKeyPath}. Pass --identity <path>, or --generate to create an ed25519 key pair.`
1518
+ );
1519
+ }
1520
+ err(`generating ed25519 key pair at ${privateKeyPath}`);
1521
+ const gen = (0, import_node_child_process.spawnSync)(
1522
+ "ssh-keygen",
1523
+ ["-t", "ed25519", "-N", "", "-f", privateKeyPath, "-C", "arker-cli"],
1524
+ { stdio: "inherit" }
1525
+ );
1526
+ if (gen.status !== 0) die("ssh-keygen failed to generate a key pair");
1181
1527
  }
1182
- if (exitCode !== 0) process.exit(exitCode);
1528
+ const publicKey = (0, import_node_fs.readFileSync)(publicKeyPath, "utf8").trim();
1529
+ if (!publicKey) die(`SSH public key at ${publicKeyPath} is empty`);
1530
+ return { publicKey, privateKeyPath, publicKeyPath };
1531
+ }
1532
+ async function registerAccountSshKey(client, publicKey, label) {
1533
+ return client._request(
1534
+ "POST",
1535
+ "/v1/account/ssh-keys",
1536
+ { public_key: publicKey, label: label ?? null },
1537
+ client.baseUrl
1538
+ );
1183
1539
  }
1184
- async function runShellLine(computer, sessionId, cmd, timeout) {
1540
+ function resolveSshHost(args, client) {
1541
+ const explicit = args.flags.host ?? process.env.ARKER_SSH_HOST;
1542
+ if (explicit) return explicit;
1543
+ if (client.region) return `aws-${client.region}.arker.ai`;
1185
1544
  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 };
1545
+ return new URL(client.baseUrl).hostname;
1546
+ } catch {
1547
+ die("could not determine SSH host; pass --host <hostname>");
1200
1548
  }
1201
1549
  }
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";
1550
+ async function cmdSsh(args, client) {
1551
+ const vmId = args.flags["vm-id"] ?? args.positional[0];
1552
+ if (!vmId) {
1553
+ die(
1554
+ "usage: arker ssh <vm_id> [--identity <path>] [--generate] [--host <h>] [--port <n>] [--connect|-c] [--skip-register]"
1555
+ );
1556
+ }
1557
+ const { publicKey, privateKeyPath } = resolveLocalSshKey(args);
1558
+ if (!boolFlag(args, "skip-register")) {
1559
+ const label = args.flags.label ?? `arker-cli ${(0, import_node_os.homedir)().split("/").pop() ?? ""}`.trim();
1560
+ try {
1561
+ const key = await registerAccountSshKey(client, publicKey, label);
1562
+ err(`registered SSH key ${key.fingerprint}${key.label ? ` (${key.label})` : ""}`);
1563
+ } catch (e) {
1564
+ if (e instanceof ArkerError && e.status === 409) {
1565
+ die(`this SSH key is already registered to a different account: ${e.message}`);
1566
+ }
1567
+ if (e instanceof ArkerError && e.status === 403) {
1568
+ die(
1569
+ "your API key lacks the developer role required to register SSH keys. Register the key in the console, then re-run with --skip-register."
1570
+ );
1571
+ }
1572
+ throw e;
1573
+ }
1206
1574
  }
1207
- if ((msg.includes("not_found") || msg.includes("not found")) && msg.includes(vmId.toLowerCase())) {
1208
- return "fatal";
1575
+ const host = resolveSshHost(args, client);
1576
+ const port = numFlag(args, "port") ?? SSH_PORT_DEFAULT;
1577
+ const sshArgs = [
1578
+ "-i",
1579
+ privateKeyPath,
1580
+ ...port !== 22 ? ["-p", String(port)] : [],
1581
+ `${vmId}@${host}`
1582
+ ];
1583
+ const command = `ssh ${sshArgs.join(" ")}`;
1584
+ if (boolFlag(args, "connect") || boolFlag(args, "c")) {
1585
+ err(`connecting: ${command}`);
1586
+ const r = (0, import_node_child_process.spawnSync)("ssh", sshArgs, { stdio: "inherit" });
1587
+ process.exit(r.status ?? 1);
1588
+ }
1589
+ out(command);
1590
+ }
1591
+ async function cmdSshKeys(args, client) {
1592
+ const sub = args.positional[0];
1593
+ const rest = args.positional.slice(1);
1594
+ switch (sub) {
1595
+ case void 0:
1596
+ case "ls":
1597
+ case "list": {
1598
+ const res = await client._request(
1599
+ "GET",
1600
+ "/v1/account/ssh-keys",
1601
+ void 0,
1602
+ client.baseUrl
1603
+ );
1604
+ if (args.flags.json) return out(res);
1605
+ for (const k of res.keys) {
1606
+ out(`${k.id} ${k.fingerprint} ${k.label ?? "\u2014"}`);
1607
+ }
1608
+ return;
1609
+ }
1610
+ case "add": {
1611
+ const { publicKey } = resolveLocalSshKey(args);
1612
+ const label = args.flags.label;
1613
+ const key = await registerAccountSshKey(client, publicKey, label);
1614
+ if (args.flags.json) return out(key);
1615
+ out(`${key.id} ${key.fingerprint} ${key.label ?? "\u2014"}`);
1616
+ return;
1617
+ }
1618
+ case "rm":
1619
+ case "delete": {
1620
+ const id = rest[0] ?? die("usage: arker ssh-keys rm <key_id>");
1621
+ const r = await client._request(
1622
+ "DELETE",
1623
+ `/v1/account/ssh-keys/${encodeURIComponent(id)}`,
1624
+ void 0,
1625
+ client.baseUrl
1626
+ );
1627
+ out(r.deleted ? `deleted ${id}` : "delete failed");
1628
+ return;
1629
+ }
1630
+ default:
1631
+ die("usage: arker ssh-keys <ls|add|rm> ...");
1209
1632
  }
1210
- return "recoverable";
1211
1633
  }
1212
1634
  function numFlag(args, name) {
1213
1635
  const v = args.flags[name];
@@ -1241,16 +1663,21 @@ function usage() {
1241
1663
  " arker fork --source-vm-id <id> fork by global id",
1242
1664
  " arker fork --source-vm-name <n> --source-org-id <org>",
1243
1665
  " 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)",
1666
+ " arker fork <vm> [--vcpu N] [--memory-mib N] [--disk-mib N] [--no-disk]",
1667
+ " fork with resource/network overrides",
1668
+ " arker run <vm> <command> [--session-id <id>] [--session-idx N] run a command",
1669
+ " arker resize <vm> [--memory-mib N] [--vcpu N] [--disk-mib N] resize a VM (PATCH)",
1670
+ " arker shell [vm_id] native PTY shell (forks ubuntu-full if no vm)",
1671
+ " arker ssh <vm_id> register your SSH key + print the ssh command",
1672
+ " arker ssh <vm_id> --connect register + drop straight into the ssh session",
1246
1673
  "",
1247
1674
  "Resources:",
1248
1675
  " arker vms <ls|get|rm|fork|run> ...",
1249
1676
  " arker runs <ls|get|rm> <vm_id> ...",
1250
1677
  " arker sessions <ls|get|create|rm> <vm_id> ...",
1251
1678
  " arker syncs <ls|create|rm> <vm_id> ...",
1252
- " arker tunnels <ls|get|rm> <vm_id> ...",
1253
1679
  " arker filesystems <ls|create|get|rm> ... (alias: fs)",
1680
+ " arker ssh-keys <ls|add|rm> ... manage account SSH keys",
1254
1681
  "",
1255
1682
  "Flags:",
1256
1683
  " --api-key <key> (or env ARKER_API_KEY)",
@@ -1260,6 +1687,35 @@ function usage() {
1260
1687
  " --control-base-url <url> override CF Worker URL (env ARKER_CONTROL_BASE_URL)",
1261
1688
  " --json emit JSON instead of tabular output",
1262
1689
  "",
1690
+ "Fork flags:",
1691
+ " --vcpu <n> vCPU count for the new VM (capped by source max_vcpus)",
1692
+ " --memory-mib <n> memory (MiB) for the new VM",
1693
+ " --disk-mib <n> disk size (MiB) for the new VM",
1694
+ " --no-disk fork a memory-backed (nodisk) VM",
1695
+ "",
1696
+ "Run flags:",
1697
+ " --session-id <ulid> run in a specific existing session",
1698
+ " --session-idx <n> run in the session at this index (default 0)",
1699
+ " --background return a run id instead of blocking",
1700
+ " --timeout <secs> per-run timeout",
1701
+ " --acquire <list> warm resources before the run (cpu,memory,disk)",
1702
+ " --release <list> release resources after the run (cpu,memory,disk)",
1703
+ "",
1704
+ "Shell flags:",
1705
+ " --session-id <id> reconnect to an existing PTY session",
1706
+ " --command <path> shell executable path (default: /bin/bash)",
1707
+ " --cols <n> --rows <n> initial terminal size",
1708
+ " --no-persist close the remote PTY process on disconnect",
1709
+ "",
1710
+ "SSH flags:",
1711
+ " --identity <path> local key (private or .pub); default ~/.ssh/id_ed25519",
1712
+ " --generate create an ed25519 key pair if none exists",
1713
+ " --host <hostname> SSH host (or env ARKER_SSH_HOST; default aws-<region>.arker.ai)",
1714
+ " --port <n> SSH port (default 22)",
1715
+ " --connect, -c exec ssh instead of just printing the command",
1716
+ " --skip-register don't register the key (assume already registered)",
1717
+ " --label <text> label for the registered key",
1718
+ "",
1263
1719
  `Arker org id: ${ARKER_ORG_ID}`
1264
1720
  ].join("\n")
1265
1721
  );
@@ -1290,6 +1746,11 @@ async function main() {
1290
1746
  return await cmdSyncs(args, client);
1291
1747
  case "shell":
1292
1748
  return await cmdShell(args, client);
1749
+ case "ssh":
1750
+ return await cmdSsh(args, client);
1751
+ case "ssh-keys":
1752
+ case "ssh_keys":
1753
+ return await cmdSshKeys(args, client);
1293
1754
  // Resources.
1294
1755
  case "vms":
1295
1756
  return await cmdVms(args, client);
@@ -1297,8 +1758,8 @@ async function main() {
1297
1758
  return await cmdRuns(args, client);
1298
1759
  case "sessions":
1299
1760
  return await cmdSessions(args, client);
1300
- case "tunnels":
1301
- return await cmdTunnels(args, client);
1761
+ case "resize":
1762
+ return await cmdResize(args, client);
1302
1763
  case "filesystems":
1303
1764
  case "fs":
1304
1765
  return await cmdFilesystems(args, client);