@arker-ai/sdk 0.5.2 → 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,13 +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
171
  durable: src.durable ?? null,
169
- tunnel: src.tunnel ?? null
172
+ resources
170
173
  };
171
174
  const useBurst = sourceOrgId === ARKER_ORG_ID && src.sourceVmName !== void 0 && isBurstRef(src.sourceVmName);
172
175
  const baseUrl = useBurst && this.burstBaseUrl ? this.burstBaseUrl : this.baseUrl;
@@ -313,6 +316,10 @@ var Arker = class {
313
316
  return retryDelay(this.retry, attempt);
314
317
  }
315
318
  /** @internal */
319
+ _authHeaders() {
320
+ return { authorization: `Bearer ${this.apiKey}` };
321
+ }
322
+ /** @internal */
316
323
  _baseUrlFor(ref) {
317
324
  if (isBurstRef(ref) && this.burstBaseUrl) return this.burstBaseUrl;
318
325
  return this.baseUrl;
@@ -347,7 +354,6 @@ var VM = class _VM {
347
354
  root_source_vm_name;
348
355
  worker_id;
349
356
  sessions;
350
- tunnels;
351
357
  constructor(client, vmId, baseUrl = client._baseUrlFor(vmId), data) {
352
358
  this._client = client;
353
359
  this.id = vmId;
@@ -470,8 +476,25 @@ var VM = class _VM {
470
476
  }
471
477
  throw new ArkerError(lastError?.code ?? "internal", lastError?.message ?? "write failed", 200);
472
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
+ */
473
487
  async resize(request) {
474
- 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);
475
498
  }
476
499
  async delete() {
477
500
  return this._client._request("DELETE", vmPath(this.id), void 0, this.baseUrl);
@@ -527,22 +550,30 @@ var VM = class _VM {
527
550
  async deleteSession(sessionId) {
528
551
  return this._client._request("DELETE", `${vmPath(this.id)}/sessions/${pathSegment(sessionId)}`, void 0, this.baseUrl);
529
552
  }
530
- // ── Tunnels: VM-scoped, addressed by recoverable tunnel key ───────
531
- async listTunnels(opts = {}) {
532
- return this._client._request("GET", buildQuery(`${vmPath(this.id)}/tunnels`, {
533
- cursor: opts.cursor,
534
- limit: opts.limit,
535
- state: opts.state
536
- }), void 0, this.baseUrl);
537
- }
538
- async createTunnel(request = {}) {
539
- return this._client._request("POST", `${vmPath(this.id)}/tunnels`, request, this.baseUrl);
540
- }
541
- async getTunnel(key) {
542
- return this._client._request("GET", `${vmPath(this.id)}/tunnels/${pathSegment(key)}`, void 0, this.baseUrl);
543
- }
544
- async deleteTunnel(key) {
545
- return this._client._request("DELETE", `${vmPath(this.id)}/tunnels/${pathSegment(key)}`, 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);
546
577
  }
547
578
  };
548
579
  function buildQuery(path, params) {
@@ -554,6 +585,176 @@ function buildQuery(path, params) {
554
585
  const qs = usp.toString();
555
586
  return qs ? `${path}?${qs}` : path;
556
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
+ }
557
758
  function normalizeBaseUrl(baseUrl) {
558
759
  const trimmed = baseUrl.trim().replace(/\/+$/, "");
559
760
  if (!trimmed) throw new Error("baseUrl must not be empty");
@@ -636,7 +837,10 @@ function parseRunResponse(payload) {
636
837
  stderr: decodeBytes(stderr, stderrEncoding),
637
838
  stderrEncoding,
638
839
  exitCode: numberField(body.exit_code, "run response.exit_code"),
639
- 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
640
844
  };
641
845
  }
642
846
  if (typeof body.run_id === "string") {
@@ -707,6 +911,10 @@ function numberField(value, context) {
707
911
  if (typeof value !== "number") throw new ArkerError("internal", `${context} must be a number`, 200);
708
912
  return value;
709
913
  }
914
+ function optionalNumberOrNull(value) {
915
+ if (value === null || typeof value === "number") return value;
916
+ return void 0;
917
+ }
710
918
  function assertWriteComplete(result, context) {
711
919
  if (result.complete && result.written) return;
712
920
  throw new ArkerError("internal", `${context} did not complete`, 200);
@@ -782,20 +990,25 @@ function parseArgs(argv) {
782
990
  return { positional, flags };
783
991
  }
784
992
  function readFileConfig() {
785
- const path = (0, import_node_path.join)((0, import_node_os.homedir)(), ".arker", "config.json");
786
- if (!(0, import_node_fs.existsSync)(path)) return {};
787
- try {
788
- return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
789
- } catch {
790
- 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
+ }
791
1001
  }
1002
+ return {};
792
1003
  }
793
1004
  function clientFromArgs(args) {
794
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;
795
1008
  const apiKey = args.flags["api-key"] ?? process.env.ARKER_API_KEY ?? file.apiKey;
796
- const baseUrl = args.flags["base-url"] ?? process.env.ARKER_BASE_URL ?? file.baseUrl;
1009
+ const baseUrl = explicitBaseUrl ?? (explicitRegion ? void 0 : file.baseUrl);
797
1010
  const controlBaseUrl = args.flags["control-base-url"] ?? process.env.ARKER_CONTROL_BASE_URL;
798
- const region = args.flags.region ?? process.env.ARKER_REGION ?? file.region;
1011
+ const region = explicitRegion ?? file.region;
799
1012
  const provider = args.flags.provider ?? process.env.ARKER_PROVIDER;
800
1013
  if (!apiKey) {
801
1014
  die("Missing API key. Set ARKER_API_KEY or pass --api-key.");
@@ -825,7 +1038,8 @@ function fmtVm(vm) {
825
1038
  const region = vm.region ?? "?";
826
1039
  const name = vm.name ?? "\u2014";
827
1040
  const state = vm.state ?? "?";
828
- 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}`;
829
1043
  }
830
1044
  async function cmdVms(args, client) {
831
1045
  const sub = args.positional[0];
@@ -870,6 +1084,10 @@ async function cmdVms(args, client) {
870
1084
  await cmdRun({ ...args, positional: rest }, client);
871
1085
  return;
872
1086
  }
1087
+ case "resize": {
1088
+ await cmdResize({ ...args, positional: rest }, client);
1089
+ return;
1090
+ }
873
1091
  default:
874
1092
  die(`unknown vms subcommand: ${sub}`);
875
1093
  }
@@ -888,14 +1106,22 @@ async function cmdFork(args, client) {
888
1106
  sourceVmName = refPositional;
889
1107
  }
890
1108
  if (!sourceVmId && !sourceVmName) {
891
- die("usage: arker fork <vm_name> | --source-vm-id <id> | --source-vm-name <name> [--source-org-id <org>]");
892
- }
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;
893
1117
  const computer = await client.fork({
894
1118
  sourceVmId,
895
1119
  sourceVmName,
896
1120
  sourceOrgId,
897
1121
  name,
898
- public: publicFlag
1122
+ public: publicFlag,
1123
+ ...resources ? { resources } : {},
1124
+ ...disk !== void 0 ? { disk } : {}
899
1125
  });
900
1126
  out({ vm_id: computer.id });
901
1127
  }
@@ -903,13 +1129,24 @@ async function cmdRun(args, client) {
903
1129
  const vmId = args.positional[0] ?? die("usage: arker run <vm_id> <command...>");
904
1130
  const command = args.positional.slice(1).join(" ");
905
1131
  if (!command) die("missing command to run");
1132
+ const sessionIdx = numFlag(args, "session-idx");
906
1133
  const result = await client.vm(vmId).run(command, {
907
1134
  background: boolFlag(args, "background"),
908
1135
  timeout: numFlag(args, "timeout"),
909
1136
  acquire: args.flags.acquire,
910
- release: args.flags.release
1137
+ release: args.flags.release,
1138
+ session_id: args.flags["session-id"],
1139
+ ...sessionIdx !== void 0 ? { session_idx: sessionIdx } : {}
911
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
+ }
912
1146
  if (result.type === "completed") {
1147
+ if (result.memoryPartial) {
1148
+ err(`Memory target partially applied: requested ${formatMib(result.memoryRequestedMib)}, achieved ${formatMib(result.memoryAchievedMib)}.`);
1149
+ }
913
1150
  process.stdout.write(new TextDecoder().decode(result.stdout));
914
1151
  if (result.stderr.length) process.stderr.write(new TextDecoder().decode(result.stderr));
915
1152
  process.exitCode = result.exitCode === 0 ? 0 : result.exitCode;
@@ -917,6 +1154,30 @@ async function cmdRun(args, client) {
917
1154
  }
918
1155
  out({ run_id: result.runId, state: result.state });
919
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
+ }
1180
+ }
920
1181
  async function cmdRuns(args, client) {
921
1182
  const sub = args.positional[0];
922
1183
  const rest = args.positional.slice(1);
@@ -1058,51 +1319,24 @@ async function cmdSync(args, client) {
1058
1319
  }
1059
1320
  import_node_process.stdout.write(await client.vm(vm).sync(path));
1060
1321
  }
1061
- async function cmdTunnels(args, client) {
1062
- const sub = args.positional[0];
1063
- const rest = args.positional.slice(1);
1064
- const vm = rest[0];
1065
- switch (sub) {
1066
- case "ls":
1067
- case "list": {
1068
- if (!vm) die("usage: arker tunnels ls <vm_id>");
1069
- const res = await client.vm(vm).listTunnels({
1070
- state: args.flags.state,
1071
- cursor: args.flags.cursor,
1072
- limit: numFlag(args, "limit")
1073
- });
1074
- if (args.flags.json) return out(res);
1075
- for (const t of res.tunnels) {
1076
- out(`${t.tunnel_key ?? "-"} ${t.port} ${t.state} ${t.protocol} ${t.url ?? "-"}`);
1077
- }
1078
- if (res.next_cursor) out(`# next_cursor=${res.next_cursor}`);
1079
- 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
1080
1336
  }
1081
- case "create": {
1082
- if (!vm) die("usage: arker tunnels create <vm_id> [--ports 80,8080] [--auth-mode open|authenticated]");
1083
- const tunnel = await client.vm(vm).createTunnel({
1084
- ports: parsePorts(args.flags.ports),
1085
- auth_mode: args.flags["auth-mode"]
1086
- });
1087
- return out(tunnel);
1088
- }
1089
- case "get": {
1090
- if (!vm) die("usage: arker tunnels get <vm_id> <key>");
1091
- const key = rest[1] ?? die("missing key");
1092
- out(await client.vm(vm).getTunnel(key));
1093
- return;
1094
- }
1095
- case "rm":
1096
- case "delete": {
1097
- if (!vm) die("usage: arker tunnels rm <vm_id> <key>");
1098
- const key = rest[1] ?? die("missing key");
1099
- const r = await client.vm(vm).deleteTunnel(key);
1100
- out(r.deleted ? `deleted tunnel ${key}` : "delete failed");
1101
- return;
1102
- }
1103
- default:
1104
- die(`usage: arker tunnels <ls|create|get|rm> ...`);
1105
- }
1337
+ });
1338
+ if (args.flags.json) return out(updated);
1339
+ out(fmtVm(updated));
1106
1340
  }
1107
1341
  async function cmdFilesystems(args, client) {
1108
1342
  const sub = args.positional[0];
@@ -1148,6 +1382,10 @@ async function cmdFilesystems(args, client) {
1148
1382
  async function cmdShell(args, client) {
1149
1383
  let computer;
1150
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
+ }
1151
1389
  if (vmIdArg) {
1152
1390
  computer = await client.vm(vmIdArg).refresh();
1153
1391
  } else {
@@ -1156,100 +1394,242 @@ async function cmdShell(args, client) {
1156
1394
  sourceVmName,
1157
1395
  sourceOrgId: ARKER_ORG_ID
1158
1396
  });
1397
+ err(`forked ${computer.id}`);
1159
1398
  }
1160
- const header = computer;
1161
- const session = await computer.createSession({
1162
- 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
1163
1420
  });
1164
- const sessionId = session.session_id;
1165
- const timeout = numFlag(args, "timeout");
1166
- const preload = args.positional.slice(vmIdArg ? 1 : 0).join(" ").trim();
1167
- const exitAfter = args.flags.exit === true;
1168
- import_node_process.stdout.write(JSON.stringify(header, null, 2) + "\n");
1169
- let inFlight = false;
1170
- let exitCode = 0;
1171
- try {
1172
- if (preload && preload !== "exit") {
1173
- const step = await runShellLine(computer, sessionId, preload, timeout);
1174
- if (step.kind === "fatal") {
1175
- err(`shell ended: ${step.message}`);
1176
- return;
1177
- }
1178
- if (step.kind === "recoverable") {
1179
- err(`error: ${step.message}`);
1180
- exitCode = 1;
1181
- } else {
1182
- exitCode = step.exitCode;
1183
- }
1184
- }
1185
- if (exitAfter || preload === "exit") {
1186
- process.exit(exitCode);
1187
- }
1188
- const rl = readline.createInterface({ input: import_node_process.stdin, output: import_node_process.stdout, prompt: "> " });
1189
- rl.on("SIGINT", () => {
1190
- if (inFlight) {
1191
- process.stderr.write("^C\n");
1192
- 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);
1193
1437
  }
1194
- process.stdout.write("^C\n");
1195
- rl.write(null, { ctrl: true, name: "u" });
1196
- 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);
1197
1478
  });
1198
- rl.prompt();
1199
- for await (const line of rl) {
1200
- const cmd = line.trim();
1201
- if (!cmd) {
1202
- rl.prompt();
1203
- continue;
1204
- }
1205
- if (cmd === "exit" || cmd === "quit") break;
1206
- inFlight = true;
1207
- const step = await runShellLine(computer, sessionId, cmd, timeout);
1208
- inFlight = false;
1209
- if (step.kind === "fatal") {
1210
- err(`shell ended: ${step.message}`);
1211
- exitCode = 1;
1212
- break;
1213
- }
1214
- if (step.kind === "recoverable") {
1215
- 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;
1216
1490
  }
1217
- rl.prompt();
1218
- }
1219
- rl.close();
1220
- } finally {
1221
- 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);
1222
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");
1223
1527
  }
1224
- 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 };
1225
1531
  }
1226
- async function runShellLine(computer, sessionId, cmd, timeout) {
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
+ );
1539
+ }
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`;
1227
1544
  try {
1228
- const result = await computer.run(cmd, { session_id: sessionId, timeout });
1229
- if (result.type === "completed") {
1230
- if (result.stdout.length) process.stdout.write(new TextDecoder().decode(result.stdout));
1231
- if (result.stderr.length) process.stderr.write(new TextDecoder().decode(result.stderr));
1232
- const tail = result.stderr.length > 0 ? result.stderr[result.stderr.length - 1] : result.stdout.length > 0 ? result.stdout[result.stdout.length - 1] : void 0;
1233
- if (tail !== void 0 && tail !== 10) process.stdout.write("\n");
1234
- return { kind: "ok", exitCode: result.exitCode };
1235
- }
1236
- out({ run_id: result.runId });
1237
- return { kind: "ok", exitCode: 0 };
1238
- } catch (e) {
1239
- const message = e instanceof Error ? e.message : String(e);
1240
- const kind = classifyShellError(message, computer.id);
1241
- return { kind, message };
1545
+ return new URL(client.baseUrl).hostname;
1546
+ } catch {
1547
+ die("could not determine SSH host; pass --host <hostname>");
1242
1548
  }
1243
1549
  }
1244
- function classifyShellError(message, vmId) {
1245
- const msg = message.toLowerCase();
1246
- if (msg.includes("unauthor") || msg.includes("forbidden") || msg.includes("invalid api key")) {
1247
- 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
+ );
1248
1556
  }
1249
- if ((msg.includes("not_found") || msg.includes("not found")) && msg.includes(vmId.toLowerCase())) {
1250
- return "fatal";
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
+ }
1574
+ }
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> ...");
1251
1632
  }
1252
- return "recoverable";
1253
1633
  }
1254
1634
  function numFlag(args, name) {
1255
1635
  const v = args.flags[name];
@@ -1263,10 +1643,6 @@ function boolFlag(args, name) {
1263
1643
  if (v === "false" || v === "0") return false;
1264
1644
  return true;
1265
1645
  }
1266
- function parsePorts(value) {
1267
- if (typeof value !== "string" || value.trim() === "") return void 0;
1268
- return value.split(",").map((part) => Number(part.trim())).filter((port) => Number.isFinite(port));
1269
- }
1270
1646
  async function readAllStdin() {
1271
1647
  const chunks = [];
1272
1648
  for await (const chunk of import_node_process.stdin) chunks.push(chunk);
@@ -1287,16 +1663,21 @@ function usage() {
1287
1663
  " arker fork --source-vm-id <id> fork by global id",
1288
1664
  " arker fork --source-vm-name <n> --source-org-id <org>",
1289
1665
  " fork by name in another org",
1290
- " arker run <vm> <command> run a command",
1291
- " 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",
1292
1673
  "",
1293
1674
  "Resources:",
1294
1675
  " arker vms <ls|get|rm|fork|run> ...",
1295
1676
  " arker runs <ls|get|rm> <vm_id> ...",
1296
1677
  " arker sessions <ls|get|create|rm> <vm_id> ...",
1297
1678
  " arker syncs <ls|create|rm> <vm_id> ...",
1298
- " arker tunnels <ls|get|rm> <vm_id> ...",
1299
1679
  " arker filesystems <ls|create|get|rm> ... (alias: fs)",
1680
+ " arker ssh-keys <ls|add|rm> ... manage account SSH keys",
1300
1681
  "",
1301
1682
  "Flags:",
1302
1683
  " --api-key <key> (or env ARKER_API_KEY)",
@@ -1306,6 +1687,35 @@ function usage() {
1306
1687
  " --control-base-url <url> override CF Worker URL (env ARKER_CONTROL_BASE_URL)",
1307
1688
  " --json emit JSON instead of tabular output",
1308
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
+ "",
1309
1719
  `Arker org id: ${ARKER_ORG_ID}`
1310
1720
  ].join("\n")
1311
1721
  );
@@ -1336,6 +1746,11 @@ async function main() {
1336
1746
  return await cmdSyncs(args, client);
1337
1747
  case "shell":
1338
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);
1339
1754
  // Resources.
1340
1755
  case "vms":
1341
1756
  return await cmdVms(args, client);
@@ -1343,8 +1758,8 @@ async function main() {
1343
1758
  return await cmdRuns(args, client);
1344
1759
  case "sessions":
1345
1760
  return await cmdSessions(args, client);
1346
- case "tunnels":
1347
- return await cmdTunnels(args, client);
1761
+ case "resize":
1762
+ return await cmdResize(args, client);
1348
1763
  case "filesystems":
1349
1764
  case "fs":
1350
1765
  return await cmdFilesystems(args, client);