@arker-ai/sdk 0.5.2 → 0.6.3

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,10 @@ 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");
29
+ var import_node_module = require("module");
28
30
  var import_node_os = require("os");
29
31
  var import_node_path = require("path");
30
- var readline = __toESM(require("readline/promises"), 1);
31
32
  var import_node_process = require("process");
32
33
 
33
34
  // src/index.ts
@@ -153,6 +154,12 @@ var Arker = class {
153
154
  );
154
155
  }
155
156
  const sourceOrgId = src.sourceOrgId ?? (src.sourceVmName !== void 0 && GOLDEN_NAMES.has(src.sourceVmName) ? ARKER_ORG_ID : void 0);
157
+ const legacy = src;
158
+ const resources = src.resources ?? (legacy.vcpu_count != null || legacy.memory_mib != null || legacy.disk_mib != null ? {
159
+ vcpu: legacy.vcpu_count ?? null,
160
+ memory_mib: legacy.memory_mib ?? null,
161
+ disk_mib: legacy.disk_mib ?? null
162
+ } : null);
156
163
  const body = {
157
164
  source_vm_id: src.sourceVmId ?? null,
158
165
  source_vm_name: src.sourceVmName ?? null,
@@ -160,13 +167,10 @@ var Arker = class {
160
167
  name: src.name ?? null,
161
168
  public: src.public ?? null,
162
169
  network: src.network ?? null,
170
+ egress: src.egress ?? null,
163
171
  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
172
  durable: src.durable ?? null,
169
- tunnel: src.tunnel ?? null
173
+ resources
170
174
  };
171
175
  const useBurst = sourceOrgId === ARKER_ORG_ID && src.sourceVmName !== void 0 && isBurstRef(src.sourceVmName);
172
176
  const baseUrl = useBurst && this.burstBaseUrl ? this.burstBaseUrl : this.baseUrl;
@@ -313,6 +317,10 @@ var Arker = class {
313
317
  return retryDelay(this.retry, attempt);
314
318
  }
315
319
  /** @internal */
320
+ _authHeaders() {
321
+ return { authorization: `Bearer ${this.apiKey}` };
322
+ }
323
+ /** @internal */
316
324
  _baseUrlFor(ref) {
317
325
  if (isBurstRef(ref) && this.burstBaseUrl) return this.burstBaseUrl;
318
326
  return this.baseUrl;
@@ -347,7 +355,6 @@ var VM = class _VM {
347
355
  root_source_vm_name;
348
356
  worker_id;
349
357
  sessions;
350
- tunnels;
351
358
  constructor(client, vmId, baseUrl = client._baseUrlFor(vmId), data) {
352
359
  this._client = client;
353
360
  this.id = vmId;
@@ -470,8 +477,25 @@ var VM = class _VM {
470
477
  }
471
478
  throw new ArkerError(lastError?.code ?? "internal", lastError?.message ?? "write failed", 200);
472
479
  }
480
+ /**
481
+ * Update this VM's resource allocation and/or network settings via
482
+ * `PATCH /v1/vms/{id}`. Returns the updated `Vm`.
483
+ *
484
+ * Accepts either a `PatchVmRequest` (`{ resources, network }`) or, for
485
+ * convenience, flat resource fields (`{ vcpu, memory_mib, disk_mib }`)
486
+ * which are folded into `resources`.
487
+ */
473
488
  async resize(request) {
474
- return this._client._request("POST", `${vmPath(this.id)}/resize`, request, this.baseUrl);
489
+ const r = request;
490
+ 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 } : {
491
+ resources: {
492
+ vcpu: r.vcpu ?? null,
493
+ memory_mib: r.memory_mib ?? null,
494
+ disk_mib: r.disk_mib ?? null
495
+ },
496
+ network: r.network ?? null
497
+ };
498
+ return this._client._request("PATCH", vmPath(this.id), body, this.baseUrl);
475
499
  }
476
500
  async delete() {
477
501
  return this._client._request("DELETE", vmPath(this.id), void 0, this.baseUrl);
@@ -527,22 +551,30 @@ var VM = class _VM {
527
551
  async deleteSession(sessionId) {
528
552
  return this._client._request("DELETE", `${vmPath(this.id)}/sessions/${pathSegment(sessionId)}`, void 0, this.baseUrl);
529
553
  }
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);
554
+ async connectPty(options = {}) {
555
+ const sessionId = options.sessionId ?? sessionIdFrom(await this.createSession());
556
+ const useTicket = options.useTicket ?? !isNodeRuntime();
557
+ const params = {
558
+ cols: options.cols,
559
+ rows: options.rows,
560
+ command: options.command,
561
+ persist: options.persist,
562
+ cancel_ttl_secs: options.cancelTtlSecs && options.cancelTtlSecs > 0 ? Math.floor(options.cancelTtlSecs) : void 0
563
+ };
564
+ let ticket;
565
+ if (useTicket) {
566
+ const response = await this._client._request(
567
+ "POST",
568
+ `${vmPath(this.id)}/sessions/${pathSegment(sessionId)}/pty-ticket`,
569
+ {},
570
+ this.baseUrl
571
+ );
572
+ ticket = response.ticket;
573
+ }
574
+ const url = buildPtyWebSocketUrl(this.baseUrl, this.id, sessionId, { ...params, ticket });
575
+ const factory = options.webSocketFactory ?? (useTicket ? browserPtyWebSocketFactory : nodePtyWebSocketFactory);
576
+ const socket = await factory(url, useTicket ? {} : { headers: this._client._authHeaders() });
577
+ return new PtyConnectionImpl(sessionId, socket);
546
578
  }
547
579
  };
548
580
  function buildQuery(path, params) {
@@ -554,6 +586,176 @@ function buildQuery(path, params) {
554
586
  const qs = usp.toString();
555
587
  return qs ? `${path}?${qs}` : path;
556
588
  }
589
+ function buildPtyWebSocketUrl(baseUrl, vmId, sessionId, params) {
590
+ const url = new URL(`${normalizeBaseUrl(baseUrl)}${vmPath(vmId)}/sessions/${pathSegment(sessionId)}/pty`);
591
+ if (url.protocol === "https:") url.protocol = "wss:";
592
+ else if (url.protocol === "http:") url.protocol = "ws:";
593
+ else throw new Error(`unsupported PTY WebSocket protocol: ${url.protocol}`);
594
+ for (const [key, value] of Object.entries(params)) {
595
+ if (value === void 0 || value === null) continue;
596
+ url.searchParams.set(key, String(value));
597
+ }
598
+ return url.toString();
599
+ }
600
+ function sessionIdFrom(session) {
601
+ const id = session.session_id ?? session.id;
602
+ if (!id) throw new ArkerError("internal", "createSession response missing session_id", 200);
603
+ return id;
604
+ }
605
+ function isNodeRuntime() {
606
+ return typeof process !== "undefined" && Boolean(process.versions?.node);
607
+ }
608
+ async function nodePtyWebSocketFactory(url, init) {
609
+ const ws = await import("ws");
610
+ return new ws.default(url, { headers: init.headers });
611
+ }
612
+ function browserPtyWebSocketFactory(url) {
613
+ if (typeof globalThis.WebSocket !== "function") {
614
+ throw new Error("WebSocket is not available in this runtime");
615
+ }
616
+ return new globalThis.WebSocket(url);
617
+ }
618
+ var PtyConnectionImpl = class {
619
+ constructor(sessionId, socket) {
620
+ this.sessionId = sessionId;
621
+ this.socket = socket;
622
+ try {
623
+ socket.binaryType = "arraybuffer";
624
+ } catch {
625
+ }
626
+ this.ready = waitForSocketOpen(socket);
627
+ addSocketListener(socket, "message", (event) => {
628
+ const data = messageData(event);
629
+ if (data !== void 0) this.emitData(bytesFromMessageData(data));
630
+ });
631
+ addSocketListener(socket, "close", (event) => this.emitClose(closeEvent(event)));
632
+ addSocketListener(socket, "error", (event) => this.emitError(event));
633
+ }
634
+ sessionId;
635
+ socket;
636
+ ready;
637
+ dataListeners = /* @__PURE__ */ new Set();
638
+ closeListeners = /* @__PURE__ */ new Set();
639
+ errorListeners = /* @__PURE__ */ new Set();
640
+ onData(listener) {
641
+ this.dataListeners.add(listener);
642
+ return () => this.dataListeners.delete(listener);
643
+ }
644
+ onClose(listener) {
645
+ this.closeListeners.add(listener);
646
+ return () => this.closeListeners.delete(listener);
647
+ }
648
+ onError(listener) {
649
+ this.errorListeners.add(listener);
650
+ return () => this.errorListeners.delete(listener);
651
+ }
652
+ send(data) {
653
+ this.socket.send(ptyInputBytes(data));
654
+ }
655
+ resize(cols, rows) {
656
+ this.socket.send(JSON.stringify({ type: "resize", cols: clampPtyDimension(cols), rows: clampPtyDimension(rows) }));
657
+ }
658
+ kill() {
659
+ this.socket.send(JSON.stringify({ type: "kill" }));
660
+ }
661
+ close(code, reason) {
662
+ this.socket.close(code, reason);
663
+ }
664
+ emitData(data) {
665
+ for (const listener of this.dataListeners) listener(data);
666
+ }
667
+ emitClose(event) {
668
+ for (const listener of this.closeListeners) listener(event);
669
+ }
670
+ emitError(error) {
671
+ for (const listener of this.errorListeners) listener(error);
672
+ }
673
+ };
674
+ function waitForSocketOpen(socket) {
675
+ if (socket.readyState === 1) return Promise.resolve();
676
+ return new Promise((resolve, reject) => {
677
+ let removeOpen;
678
+ let removeError;
679
+ let removeClose;
680
+ const cleanup = () => {
681
+ removeOpen?.();
682
+ removeError?.();
683
+ removeClose?.();
684
+ };
685
+ removeOpen = addSocketListener(socket, "open", () => {
686
+ cleanup();
687
+ resolve();
688
+ });
689
+ removeError = addSocketListener(socket, "error", (event) => {
690
+ cleanup();
691
+ reject(event instanceof Error ? event : new Error("PTY WebSocket failed to open"));
692
+ });
693
+ removeClose = addSocketListener(socket, "close", (event) => {
694
+ cleanup();
695
+ const ev = closeEvent(event);
696
+ reject(new Error(`PTY WebSocket closed before opening${ev.code ? ` (${ev.code})` : ""}`));
697
+ });
698
+ });
699
+ }
700
+ function addSocketListener(socket, type, listener) {
701
+ if (socket.addEventListener) {
702
+ socket.addEventListener(type, listener);
703
+ return () => socket.removeEventListener?.(type, listener);
704
+ }
705
+ if (socket.on) {
706
+ const nodeListener = (...args) => {
707
+ if (type === "message") listener({ data: args[0] });
708
+ else if (type === "close") listener({ code: args[0], reason: args[1] });
709
+ else listener(args[0]);
710
+ };
711
+ socket.on(type, nodeListener);
712
+ return () => socket.off?.(type, nodeListener);
713
+ }
714
+ return () => {
715
+ };
716
+ }
717
+ function messageData(event) {
718
+ if (event && typeof event === "object" && "data" in event) {
719
+ return event.data;
720
+ }
721
+ return void 0;
722
+ }
723
+ function closeEvent(event) {
724
+ if (!event || typeof event !== "object") return {};
725
+ const raw = event;
726
+ return {
727
+ code: typeof raw.code === "number" ? raw.code : void 0,
728
+ reason: typeof raw.reason === "string" ? raw.reason : void 0
729
+ };
730
+ }
731
+ function bytesFromMessageData(data) {
732
+ if (typeof data === "string") return new TextEncoder().encode(data);
733
+ if (data instanceof Uint8Array) return data;
734
+ if (data instanceof ArrayBuffer) return new Uint8Array(data);
735
+ if (ArrayBuffer.isView(data)) {
736
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
737
+ }
738
+ if (Array.isArray(data)) {
739
+ const chunks = data.map(bytesFromMessageData);
740
+ const total = chunks.reduce((sum, chunk) => sum + chunk.length, 0);
741
+ const out2 = new Uint8Array(total);
742
+ let offset = 0;
743
+ for (const chunk of chunks) {
744
+ out2.set(chunk, offset);
745
+ offset += chunk.length;
746
+ }
747
+ return out2;
748
+ }
749
+ return new Uint8Array();
750
+ }
751
+ function ptyInputBytes(data) {
752
+ if (typeof data === "string") return new TextEncoder().encode(data);
753
+ return data;
754
+ }
755
+ function clampPtyDimension(value) {
756
+ if (!Number.isFinite(value)) return 1;
757
+ return Math.max(1, Math.min(1e3, Math.trunc(value)));
758
+ }
557
759
  function normalizeBaseUrl(baseUrl) {
558
760
  const trimmed = baseUrl.trim().replace(/\/+$/, "");
559
761
  if (!trimmed) throw new Error("baseUrl must not be empty");
@@ -636,7 +838,10 @@ function parseRunResponse(payload) {
636
838
  stderr: decodeBytes(stderr, stderrEncoding),
637
839
  stderrEncoding,
638
840
  exitCode: numberField(body.exit_code, "run response.exit_code"),
639
- failReason: typeof body.fail_reason === "string" ? body.fail_reason : null
841
+ failReason: typeof body.fail_reason === "string" ? body.fail_reason : null,
842
+ memoryRequestedMib: optionalNumberOrNull(body.memory_requested_mib),
843
+ memoryAchievedMib: optionalNumberOrNull(body.memory_achieved_mib),
844
+ memoryPartial: typeof body.memory_partial === "boolean" ? body.memory_partial : void 0
640
845
  };
641
846
  }
642
847
  if (typeof body.run_id === "string") {
@@ -707,6 +912,10 @@ function numberField(value, context) {
707
912
  if (typeof value !== "number") throw new ArkerError("internal", `${context} must be a number`, 200);
708
913
  return value;
709
914
  }
915
+ function optionalNumberOrNull(value) {
916
+ if (value === null || typeof value === "number") return value;
917
+ return void 0;
918
+ }
710
919
  function assertWriteComplete(result, context) {
711
920
  if (result.complete && result.written) return;
712
921
  throw new ArkerError("internal", `${context} did not complete`, 200);
@@ -755,6 +964,14 @@ function bufferConstructor() {
755
964
  }
756
965
 
757
966
  // src/cli.ts
967
+ var import_meta = {};
968
+ var VERSION = (() => {
969
+ try {
970
+ return (0, import_node_module.createRequire)(import_meta.url)("../package.json").version;
971
+ } catch {
972
+ return "unknown";
973
+ }
974
+ })();
758
975
  function parseArgs(argv) {
759
976
  const positional = [];
760
977
  const flags = {};
@@ -782,27 +999,30 @@ function parseArgs(argv) {
782
999
  return { positional, flags };
783
1000
  }
784
1001
  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 {};
1002
+ for (const name of ["config.json", "config"]) {
1003
+ const path = (0, import_node_path.join)((0, import_node_os.homedir)(), ".arker", name);
1004
+ if (!(0, import_node_fs.existsSync)(path)) continue;
1005
+ try {
1006
+ return JSON.parse((0, import_node_fs.readFileSync)(path, "utf8"));
1007
+ } catch {
1008
+ return {};
1009
+ }
791
1010
  }
1011
+ return {};
792
1012
  }
1013
+ var DEFAULT_REGION = "us-west-2";
793
1014
  function clientFromArgs(args) {
794
1015
  const file = readFileConfig();
1016
+ const explicitBaseUrl = args.flags["base-url"] ?? process.env.ARKER_BASE_URL;
1017
+ const explicitRegion = args.flags.region ?? process.env.ARKER_REGION;
795
1018
  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;
1019
+ const baseUrl = explicitBaseUrl ?? (explicitRegion ? void 0 : file.baseUrl);
797
1020
  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;
1021
+ const region = explicitRegion ?? file.region ?? (baseUrl ? void 0 : DEFAULT_REGION);
799
1022
  const provider = args.flags.provider ?? process.env.ARKER_PROVIDER;
800
1023
  if (!apiKey) {
801
1024
  die("Missing API key. Set ARKER_API_KEY or pass --api-key.");
802
1025
  }
803
- if (!baseUrl && !region) {
804
- die("Missing region. Set ARKER_REGION or pass --region (e.g. us-west-2). --provider (aws|aws-burst) defaults to aws.");
805
- }
806
1026
  return new Arker({ apiKey, baseUrl, region, provider, controlBaseUrl });
807
1027
  }
808
1028
  function out(value) {
@@ -825,7 +1045,8 @@ function fmtVm(vm) {
825
1045
  const region = vm.region ?? "?";
826
1046
  const name = vm.name ?? "\u2014";
827
1047
  const state = vm.state ?? "?";
828
- return `${vm.vm_id ?? vm.id} ${provider}-${region} ${state} ${name}`;
1048
+ const id = vm.vm_id ?? vm.id;
1049
+ return `${id} ${provider}-${region} ${state} ${name}`;
829
1050
  }
830
1051
  async function cmdVms(args, client) {
831
1052
  const sub = args.positional[0];
@@ -870,6 +1091,10 @@ async function cmdVms(args, client) {
870
1091
  await cmdRun({ ...args, positional: rest }, client);
871
1092
  return;
872
1093
  }
1094
+ case "resize": {
1095
+ await cmdResize({ ...args, positional: rest }, client);
1096
+ return;
1097
+ }
873
1098
  default:
874
1099
  die(`unknown vms subcommand: ${sub}`);
875
1100
  }
@@ -888,14 +1113,22 @@ async function cmdFork(args, client) {
888
1113
  sourceVmName = refPositional;
889
1114
  }
890
1115
  if (!sourceVmId && !sourceVmName) {
891
- die("usage: arker fork <vm_name> | --source-vm-id <id> | --source-vm-name <name> [--source-org-id <org>]");
892
- }
1116
+ 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]");
1117
+ }
1118
+ const vcpu = numFlag(args, "vcpu");
1119
+ const memoryMib = numFlag(args, "memory-mib");
1120
+ const diskMib = numFlag(args, "disk-mib");
1121
+ const hasResources = vcpu !== void 0 || memoryMib !== void 0 || diskMib !== void 0;
1122
+ const resources = hasResources ? { vcpu: vcpu ?? null, memory_mib: memoryMib ?? null, disk_mib: diskMib ?? null } : void 0;
1123
+ const disk = boolFlag(args, "no-disk") ? false : void 0;
893
1124
  const computer = await client.fork({
894
1125
  sourceVmId,
895
1126
  sourceVmName,
896
1127
  sourceOrgId,
897
1128
  name,
898
- public: publicFlag
1129
+ public: publicFlag,
1130
+ ...resources ? { resources } : {},
1131
+ ...disk !== void 0 ? { disk } : {}
899
1132
  });
900
1133
  out({ vm_id: computer.id });
901
1134
  }
@@ -903,13 +1136,24 @@ async function cmdRun(args, client) {
903
1136
  const vmId = args.positional[0] ?? die("usage: arker run <vm_id> <command...>");
904
1137
  const command = args.positional.slice(1).join(" ");
905
1138
  if (!command) die("missing command to run");
1139
+ const sessionIdx = numFlag(args, "session-idx");
906
1140
  const result = await client.vm(vmId).run(command, {
907
1141
  background: boolFlag(args, "background"),
908
1142
  timeout: numFlag(args, "timeout"),
909
1143
  acquire: args.flags.acquire,
910
- release: args.flags.release
1144
+ release: args.flags.release,
1145
+ session_id: args.flags["session-id"],
1146
+ ...sessionIdx !== void 0 ? { session_idx: sessionIdx } : {}
911
1147
  });
1148
+ if (args.flags.json) {
1149
+ out(runResultForJson(result));
1150
+ if (result.type === "completed") process.exitCode = result.exitCode === 0 ? 0 : result.exitCode;
1151
+ return;
1152
+ }
912
1153
  if (result.type === "completed") {
1154
+ if (result.memoryPartial) {
1155
+ err(`Memory target partially applied: requested ${formatMib(result.memoryRequestedMib)}, achieved ${formatMib(result.memoryAchievedMib)}.`);
1156
+ }
913
1157
  process.stdout.write(new TextDecoder().decode(result.stdout));
914
1158
  if (result.stderr.length) process.stderr.write(new TextDecoder().decode(result.stderr));
915
1159
  process.exitCode = result.exitCode === 0 ? 0 : result.exitCode;
@@ -917,6 +1161,30 @@ async function cmdRun(args, client) {
917
1161
  }
918
1162
  out({ run_id: result.runId, state: result.state });
919
1163
  }
1164
+ function formatMib(value) {
1165
+ return typeof value === "number" ? `${value} MiB` : "unknown";
1166
+ }
1167
+ function runResultForJson(result) {
1168
+ switch (result.type) {
1169
+ case "completed":
1170
+ return {
1171
+ type: result.type,
1172
+ runId: result.runId,
1173
+ state: result.state,
1174
+ stdout: new TextDecoder().decode(result.stdout),
1175
+ stdoutEncoding: result.stdoutEncoding,
1176
+ stderr: new TextDecoder().decode(result.stderr),
1177
+ stderrEncoding: result.stderrEncoding,
1178
+ exitCode: result.exitCode,
1179
+ failReason: result.failReason,
1180
+ memoryRequestedMib: result.memoryRequestedMib,
1181
+ memoryAchievedMib: result.memoryAchievedMib,
1182
+ memoryPartial: result.memoryPartial
1183
+ };
1184
+ case "background":
1185
+ return result;
1186
+ }
1187
+ }
920
1188
  async function cmdRuns(args, client) {
921
1189
  const sub = args.positional[0];
922
1190
  const rest = args.positional.slice(1);
@@ -1058,51 +1326,24 @@ async function cmdSync(args, client) {
1058
1326
  }
1059
1327
  import_node_process.stdout.write(await client.vm(vm).sync(path));
1060
1328
  }
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;
1080
- }
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;
1329
+ async function cmdResize(args, client) {
1330
+ const vm = args.positional[0];
1331
+ if (!vm) die("usage: arker resize <vm_id> [--memory-mib N] [--vcpu N] [--disk-mib N]");
1332
+ const memoryMib = numFlag(args, "memory-mib");
1333
+ const vcpu = numFlag(args, "vcpu");
1334
+ const diskMib = numFlag(args, "disk-mib");
1335
+ if (memoryMib === void 0 && vcpu === void 0 && diskMib === void 0) {
1336
+ die("resize: pass at least one of --memory-mib, --vcpu, --disk-mib");
1337
+ }
1338
+ const updated = await client.vm(vm).resize({
1339
+ resources: {
1340
+ vcpu: vcpu ?? null,
1341
+ memory_mib: memoryMib ?? null,
1342
+ disk_mib: diskMib ?? null
1094
1343
  }
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
- }
1344
+ });
1345
+ if (args.flags.json) return out(updated);
1346
+ out(fmtVm(updated));
1106
1347
  }
1107
1348
  async function cmdFilesystems(args, client) {
1108
1349
  const sub = args.positional[0];
@@ -1148,6 +1389,10 @@ async function cmdFilesystems(args, client) {
1148
1389
  async function cmdShell(args, client) {
1149
1390
  let computer;
1150
1391
  const vmIdArg = args.flags["vm-id"] ?? args.positional[0];
1392
+ const explicitSessionId = args.flags["session-id"];
1393
+ if (!vmIdArg && explicitSessionId) {
1394
+ die("usage: arker shell <vm_id> --session-id <session_id>");
1395
+ }
1151
1396
  if (vmIdArg) {
1152
1397
  computer = await client.vm(vmIdArg).refresh();
1153
1398
  } else {
@@ -1156,100 +1401,114 @@ async function cmdShell(args, client) {
1156
1401
  sourceVmName,
1157
1402
  sourceOrgId: ARKER_ORG_ID
1158
1403
  });
1404
+ err(`forked ${computer.id}`);
1159
1405
  }
1160
- const header = computer;
1161
- const session = await computer.createSession({
1162
- cwd: args.flags.cwd
1406
+ let sessionId = explicitSessionId;
1407
+ if (!sessionId) {
1408
+ const session = await computer.createSession({
1409
+ cwd: args.flags.cwd
1410
+ });
1411
+ sessionId = session.session_id ?? session.id;
1412
+ if (!sessionId) die("createSession response missing session_id");
1413
+ }
1414
+ const persist = args.flags["no-persist"] === true ? false : boolFlag(args, "persist");
1415
+ const colsFlag = numFlag(args, "cols");
1416
+ const rowsFlag = numFlag(args, "rows");
1417
+ const cols = colsFlag ?? import_node_process.stdout.columns ?? 80;
1418
+ const rows = rowsFlag ?? import_node_process.stdout.rows ?? 24;
1419
+ const cancelTtlSecs = numFlag(args, "cancel-ttl");
1420
+ const pty = await computer.connectPty({
1421
+ sessionId,
1422
+ cols,
1423
+ rows,
1424
+ command: args.flags.command,
1425
+ persist,
1426
+ cancelTtlSecs
1163
1427
  });
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;
1428
+ err(`connected ${computer.id} session ${sessionId}`);
1429
+ const exitCode = await bridgePty(pty, {
1430
+ fallbackCols: cols,
1431
+ fallbackRows: rows,
1432
+ autoResize: colsFlag === void 0 && rowsFlag === void 0 && Boolean(import_node_process.stdout.isTTY)
1433
+ });
1434
+ if (exitCode !== 0) process.exit(exitCode);
1435
+ }
1436
+ function bridgePty(pty, options) {
1437
+ return new Promise((resolve) => {
1438
+ let settled = false;
1439
+ let rawEnabled = false;
1440
+ const wasRaw = Boolean(import_node_process.stdin.isTTY && import_node_process.stdin.isRaw);
1441
+ const restoreTerminal = () => {
1442
+ if (rawEnabled && import_node_process.stdin.isTTY && typeof import_node_process.stdin.setRawMode === "function") {
1443
+ import_node_process.stdin.setRawMode(wasRaw);
1193
1444
  }
1194
- process.stdout.write("^C\n");
1195
- rl.write(null, { ctrl: true, name: "u" });
1196
- rl.prompt();
1445
+ rawEnabled = false;
1446
+ };
1447
+ const finish = (code) => {
1448
+ if (settled) return;
1449
+ settled = true;
1450
+ restoreTerminal();
1451
+ import_node_process.stdin.off("data", onInput);
1452
+ import_node_process.stdin.off("end", onInputEnd);
1453
+ process.off("SIGWINCH", onResize);
1454
+ process.off("SIGINT", onSigint);
1455
+ process.off("SIGTERM", onSigterm);
1456
+ process.off("SIGHUP", onSighup);
1457
+ process.off("exit", restoreTerminal);
1458
+ offData();
1459
+ offClose();
1460
+ offError();
1461
+ resolve(code);
1462
+ };
1463
+ const onInput = (chunk) => {
1464
+ pty.send(chunk);
1465
+ };
1466
+ const onInputEnd = () => {
1467
+ pty.close();
1468
+ };
1469
+ const onResize = () => {
1470
+ pty.resize(import_node_process.stdout.columns ?? options.fallbackCols, import_node_process.stdout.rows ?? options.fallbackRows);
1471
+ };
1472
+ const onSigint = () => {
1473
+ pty.send(new Uint8Array([3]));
1474
+ };
1475
+ const onSigterm = () => {
1476
+ pty.close();
1477
+ finish(143);
1478
+ };
1479
+ const onSighup = () => {
1480
+ pty.close();
1481
+ finish(129);
1482
+ };
1483
+ const offData = pty.onData((data) => {
1484
+ import_node_process.stdout.write(data);
1197
1485
  });
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}`);
1486
+ const offClose = pty.onClose(() => finish(0));
1487
+ const offError = pty.onError((error) => {
1488
+ const message = error instanceof Error ? error.message : String(error);
1489
+ err(`pty error: ${message}`);
1490
+ });
1491
+ process.once("exit", restoreTerminal);
1492
+ pty.ready.then(() => {
1493
+ if (settled) return;
1494
+ if (import_node_process.stdin.isTTY && typeof import_node_process.stdin.setRawMode === "function") {
1495
+ import_node_process.stdin.setRawMode(true);
1496
+ rawEnabled = true;
1216
1497
  }
1217
- rl.prompt();
1218
- }
1219
- rl.close();
1220
- } finally {
1221
- await computer.deleteSession(sessionId).catch(() => {
1498
+ import_node_process.stdin.resume();
1499
+ import_node_process.stdin.on("data", onInput);
1500
+ import_node_process.stdin.on("end", onInputEnd);
1501
+ if (options.autoResize) process.on("SIGWINCH", onResize);
1502
+ process.on("SIGINT", onSigint);
1503
+ process.on("SIGTERM", onSigterm);
1504
+ process.on("SIGHUP", onSighup);
1505
+ if (options.autoResize) onResize();
1506
+ }).catch((error) => {
1507
+ const message = error instanceof Error ? error.message : String(error);
1508
+ err(`pty failed to open: ${message}`);
1509
+ finish(1);
1222
1510
  });
1223
- }
1224
- if (exitCode !== 0) process.exit(exitCode);
1225
- }
1226
- async function runShellLine(computer, sessionId, cmd, timeout) {
1227
- 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 };
1242
- }
1243
- }
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";
1248
- }
1249
- if ((msg.includes("not_found") || msg.includes("not found")) && msg.includes(vmId.toLowerCase())) {
1250
- return "fatal";
1251
- }
1252
- return "recoverable";
1511
+ });
1253
1512
  }
1254
1513
  function numFlag(args, name) {
1255
1514
  const v = args.flags[name];
@@ -1263,10 +1522,6 @@ function boolFlag(args, name) {
1263
1522
  if (v === "false" || v === "0") return false;
1264
1523
  return true;
1265
1524
  }
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
1525
  async function readAllStdin() {
1271
1526
  const chunks = [];
1272
1527
  for await (const chunk of import_node_process.stdin) chunks.push(chunk);
@@ -1275,7 +1530,7 @@ async function readAllStdin() {
1275
1530
  function usage() {
1276
1531
  out(
1277
1532
  [
1278
- "arker \u2014 VM control plane CLI",
1533
+ `arker v${VERSION}`,
1279
1534
  "",
1280
1535
  "Usage:",
1281
1536
  " arker <command> [args]",
@@ -1287,26 +1542,46 @@ function usage() {
1287
1542
  " arker fork --source-vm-id <id> fork by global id",
1288
1543
  " arker fork --source-vm-name <n> --source-org-id <org>",
1289
1544
  " 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)",
1545
+ " arker fork <vm> [--vcpu N] [--memory-mib N] [--disk-mib N] [--no-disk]",
1546
+ " fork with resource/network overrides",
1547
+ " arker run <vm> <command> [--session-id <id>] [--session-idx N] run a command",
1548
+ " arker resize <vm> [--memory-mib N] [--vcpu N] [--disk-mib N] resize a VM (PATCH)",
1549
+ " arker shell [vm_id] native PTY shell (forks ubuntu-full if no vm)",
1292
1550
  "",
1293
1551
  "Resources:",
1294
1552
  " arker vms <ls|get|rm|fork|run> ...",
1295
1553
  " arker runs <ls|get|rm> <vm_id> ...",
1296
1554
  " arker sessions <ls|get|create|rm> <vm_id> ...",
1297
1555
  " arker syncs <ls|create|rm> <vm_id> ...",
1298
- " arker tunnels <ls|get|rm> <vm_id> ...",
1299
1556
  " arker filesystems <ls|create|get|rm> ... (alias: fs)",
1300
1557
  "",
1301
1558
  "Flags:",
1302
1559
  " --api-key <key> (or env ARKER_API_KEY)",
1303
1560
  " --region <region> (or env ARKER_REGION; e.g. us-west-2)",
1304
- " --provider <aws|aws-burst> (or env ARKER_PROVIDER; default aws)",
1561
+ " --provider <aws> (or env ARKER_PROVIDER; default aws)",
1305
1562
  " --base-url <url> override compute URL (env ARKER_BASE_URL)",
1306
1563
  " --control-base-url <url> override CF Worker URL (env ARKER_CONTROL_BASE_URL)",
1307
1564
  " --json emit JSON instead of tabular output",
1308
1565
  "",
1309
- `Arker org id: ${ARKER_ORG_ID}`
1566
+ "Fork flags:",
1567
+ " --vcpu <n> vCPU count for the new VM (capped by source max_vcpus)",
1568
+ " --memory-mib <n> memory (MiB) for the new VM",
1569
+ " --disk-mib <n> disk size (MiB) for the new VM",
1570
+ " --no-disk fork a memory-backed (nodisk) VM",
1571
+ "",
1572
+ "Run flags:",
1573
+ " --session-id <ulid> run in a specific existing session",
1574
+ " --session-idx <n> run in the session at this index (default 0)",
1575
+ " --background return a run id instead of blocking",
1576
+ " --timeout <secs> per-run timeout",
1577
+ " --acquire <list> warm resources before the run (cpu,memory,disk)",
1578
+ " --release <list> release resources after the run (cpu,memory,disk)",
1579
+ "",
1580
+ "Shell flags:",
1581
+ " --session-id <id> reconnect to an existing PTY session",
1582
+ " --command <path> shell executable path (default: /bin/bash)",
1583
+ " --cols <n> --rows <n> initial terminal size",
1584
+ " --no-persist close the remote PTY process on disconnect"
1310
1585
  ].join("\n")
1311
1586
  );
1312
1587
  process.exit(2);
@@ -1336,6 +1611,10 @@ async function main() {
1336
1611
  return await cmdSyncs(args, client);
1337
1612
  case "shell":
1338
1613
  return await cmdShell(args, client);
1614
+ // SSH is descoped/unsupported and hidden from the interface. The
1615
+ // implementation below (cmdSsh / cmdSshKeys) is kept intact; re-add
1616
+ // the `ssh` / `ssh-keys` cases here and their help entries to expose
1617
+ // it once the server-side SSH path is supported.
1339
1618
  // Resources.
1340
1619
  case "vms":
1341
1620
  return await cmdVms(args, client);
@@ -1343,8 +1622,8 @@ async function main() {
1343
1622
  return await cmdRuns(args, client);
1344
1623
  case "sessions":
1345
1624
  return await cmdSessions(args, client);
1346
- case "tunnels":
1347
- return await cmdTunnels(args, client);
1625
+ case "resize":
1626
+ return await cmdResize(args, client);
1348
1627
  case "filesystems":
1349
1628
  case "fs":
1350
1629
  return await cmdFilesystems(args, client);