@arker-ai/sdk 0.6.2 → 0.7.0

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.
@@ -56,6 +56,7 @@ var Arker = class {
56
56
  provider;
57
57
  apiKey;
58
58
  fetchImpl;
59
+ http2;
59
60
  retry;
60
61
  constructor(opts = {}) {
61
62
  const apiKey = opts.apiKey ?? env("ARKER_API_KEY") ?? env("AUTH_KEY");
@@ -77,6 +78,7 @@ var Arker = class {
77
78
  this.region = region ? normalizeRegion(region) : void 0;
78
79
  this.provider = effectiveProvider;
79
80
  this.fetchImpl = opts.fetch ?? globalThis.fetch;
81
+ this.http2 = opts.fetch === void 0;
80
82
  this.retry = normalizeRetry(opts.retry);
81
83
  if (!this.fetchImpl) throw new Error("fetch is required in this runtime");
82
84
  }
@@ -137,7 +139,10 @@ var Arker = class {
137
139
  egress: src.egress ?? null,
138
140
  disk: src.disk ?? true,
139
141
  durable: src.durable ?? null,
140
- resources
142
+ resources,
143
+ // ARK-125: omit to inherit the source's policy; pass a doc to override
144
+ // (an empty `{ policies: [] }` clears to allow-all, NOT inherit).
145
+ policies: src.policies ?? null
141
146
  };
142
147
  const useBurst = sourceOrgId === ARKER_ORG_ID && src.sourceVmName !== void 0 && isBurstRef(src.sourceVmName);
143
148
  const baseUrl = useBurst && this.burstBaseUrl ? this.burstBaseUrl : this.baseUrl;
@@ -232,30 +237,29 @@ var Arker = class {
232
237
  if (value !== void 0) headers[key] = value;
233
238
  }
234
239
  }
235
- const init = { method, headers };
240
+ let requestBody;
236
241
  if (body !== void 0) {
237
242
  headers["content-type"] = "application/json";
238
- init.body = JSON.stringify(withoutUndefined(body));
243
+ requestBody = JSON.stringify(withoutUndefined(body));
239
244
  }
240
245
  let lastStatus = 0;
241
246
  let lastText = "";
242
247
  let lastError;
243
248
  for (let attempt = 0; attempt < this.retry.attempts; attempt++) {
244
249
  try {
245
- const response = await this.fetchImpl(url, init);
246
- const text = await response.text();
250
+ const { status, ok, text } = await sendRequest(url, { method, headers, body: requestBody }, this.fetchImpl, this.http2);
247
251
  const payload = parseJson(text);
248
252
  const parsedError = extractError(payload);
249
- lastStatus = response.status;
253
+ lastStatus = status;
250
254
  lastText = text;
251
255
  lastError = parsedError;
252
- if (isRetryable(response.status, parsedError) && attempt < this.retry.attempts - 1) {
256
+ if (isRetryable(status, parsedError) && attempt < this.retry.attempts - 1) {
253
257
  await sleep(retryDelay(this.retry, attempt));
254
258
  continue;
255
259
  }
256
- if (parsedError) throw new ArkerError(parsedError.code, parsedError.message, response.status);
257
- if (!response.ok) {
258
- throw new ArkerError("internal", lastText.slice(0, 300) || `HTTP ${response.status}`, response.status);
260
+ if (parsedError) throw new ArkerError(parsedError.code, parsedError.message, status);
261
+ if (!ok) {
262
+ throw new ArkerError("internal", lastText.slice(0, 300) || `HTTP ${status}`, status);
259
263
  }
260
264
  return payload;
261
265
  } catch (error) {
@@ -467,6 +471,32 @@ var VM = class _VM {
467
471
  async delete() {
468
472
  return this._client._request("DELETE", vmPath(this.id), void 0, this.baseUrl);
469
473
  }
474
+ // ── ARK-125 egress policy ────────────────────────────────────────
475
+ /**
476
+ * Read this VM's outbound egress policy document (ARK-125). Returns an
477
+ * empty doc (`{}`) when no policy is set.
478
+ */
479
+ async getPolicies() {
480
+ return this._client._request("GET", `${vmPath(this.id)}/policies`, void 0, this.baseUrl);
481
+ }
482
+ /**
483
+ * Replace this VM's outbound egress policy with `doc` — an ordered,
484
+ * first-match-wins rule list. An empty doc (`{}` or `{ policies: [] }`)
485
+ * clears the policy to allow-all. Returns the stored policy plus the
486
+ * domains it escalates to MITM and any degrade warnings.
487
+ *
488
+ * await vm.setPolicies({
489
+ * policies: [
490
+ * { type: "network.outbound",
491
+ * match: { domains: ["github.com"], ports: [443] },
492
+ * action: "allow" },
493
+ * { type: "network.outbound", action: "deny" },
494
+ * ],
495
+ * });
496
+ */
497
+ async setPolicies(doc) {
498
+ return this._client._request("PUT", `${vmPath(this.id)}/policies`, doc, this.baseUrl);
499
+ }
470
500
  // ── Syncs: bindings of a filesystem into this VM at a path ────────
471
501
  async listSyncs(opts = {}) {
472
502
  return this._client._request("GET", buildQuery(`${vmPath(this.id)}/syncs`, {
@@ -518,6 +548,20 @@ var VM = class _VM {
518
548
  async deleteSession(sessionId) {
519
549
  return this._client._request("DELETE", `${vmPath(this.id)}/sessions/${pathSegment(sessionId)}`, void 0, this.baseUrl);
520
550
  }
551
+ /**
552
+ * Update a session via `PATCH /v1/vms/{id}/sessions/{sid}`: resize its PTY
553
+ * (`cols`/`rows`) and/or set the idle `timeoutSecs`. Works whether or not a
554
+ * PTY is currently attached — the REST equivalent of {@link PtyConnection.resize}
555
+ * (which sends an in-band control frame on the live WebSocket).
556
+ */
557
+ async updateSession(sessionId, update) {
558
+ return this._client._request(
559
+ "PATCH",
560
+ `${vmPath(this.id)}/sessions/${pathSegment(sessionId)}`,
561
+ { cols: update.cols, rows: update.rows, timeout_secs: update.timeoutSecs },
562
+ this.baseUrl
563
+ );
564
+ }
521
565
  async connectPty(options = {}) {
522
566
  const sessionId = options.sessionId ?? sessionIdFrom(await this.createSession());
523
567
  const useTicket = options.useTicket ?? !isNodeRuntime();
@@ -929,6 +973,84 @@ function base64ToBytes(text) {
929
973
  function bufferConstructor() {
930
974
  return globalThis.Buffer;
931
975
  }
976
+ var http2Module;
977
+ function loadHttp2() {
978
+ return http2Module ??= (async () => {
979
+ const proc = globalThis.process;
980
+ if (!proc?.versions?.node) return null;
981
+ try {
982
+ return await import(
983
+ /* webpackIgnore: true */
984
+ /* @vite-ignore */
985
+ "http2"
986
+ );
987
+ } catch {
988
+ return null;
989
+ }
990
+ })();
991
+ }
992
+ var HTTP2_REQUEST_TIMEOUT_MS = 12e4;
993
+ var Http2Connection = class {
994
+ confirmed = false;
995
+ streams = 0;
996
+ session;
997
+ constructor(http2, origin) {
998
+ this.session = http2.connect(origin);
999
+ this.session.on("error", () => {
1000
+ });
1001
+ }
1002
+ get closed() {
1003
+ return this.session.closed || this.session.destroyed;
1004
+ }
1005
+ request(method, path, headers, body) {
1006
+ if (this.streams === 0) this.session.ref();
1007
+ this.streams++;
1008
+ return new Promise((resolve, reject) => {
1009
+ const stream = this.session.request({ ...headers, ":method": method, ":path": path });
1010
+ let status = 0;
1011
+ let text = "";
1012
+ stream.setEncoding("utf8");
1013
+ stream.setTimeout(HTTP2_REQUEST_TIMEOUT_MS, () => stream.destroy(new Error("HTTP/2 request timed out")));
1014
+ stream.on("response", (responseHeaders) => {
1015
+ this.confirmed = true;
1016
+ status = Number(responseHeaders[":status"]) || 0;
1017
+ });
1018
+ stream.on("data", (chunk) => {
1019
+ text += chunk;
1020
+ });
1021
+ stream.on("end", () => resolve({ status, ok: status >= 200 && status < 300, text }));
1022
+ stream.on("error", reject);
1023
+ stream.end(body);
1024
+ }).finally(() => {
1025
+ if (--this.streams === 0) this.session.unref();
1026
+ });
1027
+ }
1028
+ };
1029
+ var http2Connections = /* @__PURE__ */ new Map();
1030
+ async function sendRequest(url, init, fetchImpl, http2Enabled) {
1031
+ if (http2Enabled) {
1032
+ const http2 = await loadHttp2();
1033
+ if (http2) {
1034
+ const { origin, pathname, search } = new URL(url);
1035
+ const cached = http2Connections.get(origin);
1036
+ if (cached !== null) {
1037
+ let connection = cached;
1038
+ if (!connection || connection.closed) {
1039
+ connection = new Http2Connection(http2, origin);
1040
+ http2Connections.set(origin, connection);
1041
+ }
1042
+ try {
1043
+ return await connection.request(init.method, `${pathname}${search}`, init.headers, init.body);
1044
+ } catch (error) {
1045
+ if (connection.confirmed) throw error;
1046
+ http2Connections.set(origin, null);
1047
+ }
1048
+ }
1049
+ }
1050
+ }
1051
+ const response = await fetchImpl(url, init);
1052
+ return { status: response.status, ok: response.ok, text: await response.text() };
1053
+ }
932
1054
 
933
1055
  export {
934
1056
  CHUNK_SIZE,
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  Arker,
3
3
  ArkerError
4
- } from "./chunk-7BHPVQNG.js";
4
+ } from "./chunk-4NHUAVXK.js";
5
5
 
6
6
  // src/compat/common.ts
7
7
  function shouldGuardNested(value) {
package/dist/cli.cjs CHANGED
@@ -26,6 +26,7 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  // src/cli.ts
27
27
  var import_node_fs = require("fs");
28
28
  var import_node_child_process = require("child_process");
29
+ var import_node_module = require("module");
29
30
  var import_node_os = require("os");
30
31
  var import_node_path = require("path");
31
32
  var import_node_process = require("process");
@@ -88,6 +89,7 @@ var Arker = class {
88
89
  provider;
89
90
  apiKey;
90
91
  fetchImpl;
92
+ http2;
91
93
  retry;
92
94
  constructor(opts = {}) {
93
95
  const apiKey = opts.apiKey ?? env("ARKER_API_KEY") ?? env("AUTH_KEY");
@@ -109,6 +111,7 @@ var Arker = class {
109
111
  this.region = region ? normalizeRegion(region) : void 0;
110
112
  this.provider = effectiveProvider;
111
113
  this.fetchImpl = opts.fetch ?? globalThis.fetch;
114
+ this.http2 = opts.fetch === void 0;
112
115
  this.retry = normalizeRetry(opts.retry);
113
116
  if (!this.fetchImpl) throw new Error("fetch is required in this runtime");
114
117
  }
@@ -169,7 +172,10 @@ var Arker = class {
169
172
  egress: src.egress ?? null,
170
173
  disk: src.disk ?? true,
171
174
  durable: src.durable ?? null,
172
- resources
175
+ resources,
176
+ // ARK-125: omit to inherit the source's policy; pass a doc to override
177
+ // (an empty `{ policies: [] }` clears to allow-all, NOT inherit).
178
+ policies: src.policies ?? null
173
179
  };
174
180
  const useBurst = sourceOrgId === ARKER_ORG_ID && src.sourceVmName !== void 0 && isBurstRef(src.sourceVmName);
175
181
  const baseUrl = useBurst && this.burstBaseUrl ? this.burstBaseUrl : this.baseUrl;
@@ -264,30 +270,29 @@ var Arker = class {
264
270
  if (value !== void 0) headers[key] = value;
265
271
  }
266
272
  }
267
- const init = { method, headers };
273
+ let requestBody;
268
274
  if (body !== void 0) {
269
275
  headers["content-type"] = "application/json";
270
- init.body = JSON.stringify(withoutUndefined(body));
276
+ requestBody = JSON.stringify(withoutUndefined(body));
271
277
  }
272
278
  let lastStatus = 0;
273
279
  let lastText = "";
274
280
  let lastError;
275
281
  for (let attempt = 0; attempt < this.retry.attempts; attempt++) {
276
282
  try {
277
- const response = await this.fetchImpl(url, init);
278
- const text = await response.text();
283
+ const { status, ok, text } = await sendRequest(url, { method, headers, body: requestBody }, this.fetchImpl, this.http2);
279
284
  const payload = parseJson(text);
280
285
  const parsedError = extractError(payload);
281
- lastStatus = response.status;
286
+ lastStatus = status;
282
287
  lastText = text;
283
288
  lastError = parsedError;
284
- if (isRetryable(response.status, parsedError) && attempt < this.retry.attempts - 1) {
289
+ if (isRetryable(status, parsedError) && attempt < this.retry.attempts - 1) {
285
290
  await sleep(retryDelay(this.retry, attempt));
286
291
  continue;
287
292
  }
288
- if (parsedError) throw new ArkerError(parsedError.code, parsedError.message, response.status);
289
- if (!response.ok) {
290
- throw new ArkerError("internal", lastText.slice(0, 300) || `HTTP ${response.status}`, response.status);
293
+ if (parsedError) throw new ArkerError(parsedError.code, parsedError.message, status);
294
+ if (!ok) {
295
+ throw new ArkerError("internal", lastText.slice(0, 300) || `HTTP ${status}`, status);
291
296
  }
292
297
  return payload;
293
298
  } catch (error) {
@@ -499,6 +504,32 @@ var VM = class _VM {
499
504
  async delete() {
500
505
  return this._client._request("DELETE", vmPath(this.id), void 0, this.baseUrl);
501
506
  }
507
+ // ── ARK-125 egress policy ────────────────────────────────────────
508
+ /**
509
+ * Read this VM's outbound egress policy document (ARK-125). Returns an
510
+ * empty doc (`{}`) when no policy is set.
511
+ */
512
+ async getPolicies() {
513
+ return this._client._request("GET", `${vmPath(this.id)}/policies`, void 0, this.baseUrl);
514
+ }
515
+ /**
516
+ * Replace this VM's outbound egress policy with `doc` — an ordered,
517
+ * first-match-wins rule list. An empty doc (`{}` or `{ policies: [] }`)
518
+ * clears the policy to allow-all. Returns the stored policy plus the
519
+ * domains it escalates to MITM and any degrade warnings.
520
+ *
521
+ * await vm.setPolicies({
522
+ * policies: [
523
+ * { type: "network.outbound",
524
+ * match: { domains: ["github.com"], ports: [443] },
525
+ * action: "allow" },
526
+ * { type: "network.outbound", action: "deny" },
527
+ * ],
528
+ * });
529
+ */
530
+ async setPolicies(doc) {
531
+ return this._client._request("PUT", `${vmPath(this.id)}/policies`, doc, this.baseUrl);
532
+ }
502
533
  // ── Syncs: bindings of a filesystem into this VM at a path ────────
503
534
  async listSyncs(opts = {}) {
504
535
  return this._client._request("GET", buildQuery(`${vmPath(this.id)}/syncs`, {
@@ -550,6 +581,20 @@ var VM = class _VM {
550
581
  async deleteSession(sessionId) {
551
582
  return this._client._request("DELETE", `${vmPath(this.id)}/sessions/${pathSegment(sessionId)}`, void 0, this.baseUrl);
552
583
  }
584
+ /**
585
+ * Update a session via `PATCH /v1/vms/{id}/sessions/{sid}`: resize its PTY
586
+ * (`cols`/`rows`) and/or set the idle `timeoutSecs`. Works whether or not a
587
+ * PTY is currently attached — the REST equivalent of {@link PtyConnection.resize}
588
+ * (which sends an in-band control frame on the live WebSocket).
589
+ */
590
+ async updateSession(sessionId, update) {
591
+ return this._client._request(
592
+ "PATCH",
593
+ `${vmPath(this.id)}/sessions/${pathSegment(sessionId)}`,
594
+ { cols: update.cols, rows: update.rows, timeout_secs: update.timeoutSecs },
595
+ this.baseUrl
596
+ );
597
+ }
553
598
  async connectPty(options = {}) {
554
599
  const sessionId = options.sessionId ?? sessionIdFrom(await this.createSession());
555
600
  const useTicket = options.useTicket ?? !isNodeRuntime();
@@ -961,8 +1006,94 @@ function base64ToBytes(text) {
961
1006
  function bufferConstructor() {
962
1007
  return globalThis.Buffer;
963
1008
  }
1009
+ var http2Module;
1010
+ function loadHttp2() {
1011
+ return http2Module ??= (async () => {
1012
+ const proc = globalThis.process;
1013
+ if (!proc?.versions?.node) return null;
1014
+ try {
1015
+ return await import(
1016
+ /* webpackIgnore: true */
1017
+ /* @vite-ignore */
1018
+ "http2"
1019
+ );
1020
+ } catch {
1021
+ return null;
1022
+ }
1023
+ })();
1024
+ }
1025
+ var HTTP2_REQUEST_TIMEOUT_MS = 12e4;
1026
+ var Http2Connection = class {
1027
+ confirmed = false;
1028
+ streams = 0;
1029
+ session;
1030
+ constructor(http2, origin) {
1031
+ this.session = http2.connect(origin);
1032
+ this.session.on("error", () => {
1033
+ });
1034
+ }
1035
+ get closed() {
1036
+ return this.session.closed || this.session.destroyed;
1037
+ }
1038
+ request(method, path, headers, body) {
1039
+ if (this.streams === 0) this.session.ref();
1040
+ this.streams++;
1041
+ return new Promise((resolve, reject) => {
1042
+ const stream = this.session.request({ ...headers, ":method": method, ":path": path });
1043
+ let status = 0;
1044
+ let text = "";
1045
+ stream.setEncoding("utf8");
1046
+ stream.setTimeout(HTTP2_REQUEST_TIMEOUT_MS, () => stream.destroy(new Error("HTTP/2 request timed out")));
1047
+ stream.on("response", (responseHeaders) => {
1048
+ this.confirmed = true;
1049
+ status = Number(responseHeaders[":status"]) || 0;
1050
+ });
1051
+ stream.on("data", (chunk) => {
1052
+ text += chunk;
1053
+ });
1054
+ stream.on("end", () => resolve({ status, ok: status >= 200 && status < 300, text }));
1055
+ stream.on("error", reject);
1056
+ stream.end(body);
1057
+ }).finally(() => {
1058
+ if (--this.streams === 0) this.session.unref();
1059
+ });
1060
+ }
1061
+ };
1062
+ var http2Connections = /* @__PURE__ */ new Map();
1063
+ async function sendRequest(url, init, fetchImpl, http2Enabled) {
1064
+ if (http2Enabled) {
1065
+ const http2 = await loadHttp2();
1066
+ if (http2) {
1067
+ const { origin, pathname, search } = new URL(url);
1068
+ const cached = http2Connections.get(origin);
1069
+ if (cached !== null) {
1070
+ let connection = cached;
1071
+ if (!connection || connection.closed) {
1072
+ connection = new Http2Connection(http2, origin);
1073
+ http2Connections.set(origin, connection);
1074
+ }
1075
+ try {
1076
+ return await connection.request(init.method, `${pathname}${search}`, init.headers, init.body);
1077
+ } catch (error) {
1078
+ if (connection.confirmed) throw error;
1079
+ http2Connections.set(origin, null);
1080
+ }
1081
+ }
1082
+ }
1083
+ }
1084
+ const response = await fetchImpl(url, init);
1085
+ return { status: response.status, ok: response.ok, text: await response.text() };
1086
+ }
964
1087
 
965
1088
  // src/cli.ts
1089
+ var import_meta = {};
1090
+ var VERSION = (() => {
1091
+ try {
1092
+ return (0, import_node_module.createRequire)(import_meta.url)("../package.json").version;
1093
+ } catch {
1094
+ return "unknown";
1095
+ }
1096
+ })();
966
1097
  function parseArgs(argv) {
967
1098
  const positional = [];
968
1099
  const flags = {};
@@ -1001,6 +1132,7 @@ function readFileConfig() {
1001
1132
  }
1002
1133
  return {};
1003
1134
  }
1135
+ var DEFAULT_REGION = "us-west-2";
1004
1136
  function clientFromArgs(args) {
1005
1137
  const file = readFileConfig();
1006
1138
  const explicitBaseUrl = args.flags["base-url"] ?? process.env.ARKER_BASE_URL;
@@ -1008,14 +1140,11 @@ function clientFromArgs(args) {
1008
1140
  const apiKey = args.flags["api-key"] ?? process.env.ARKER_API_KEY ?? file.apiKey;
1009
1141
  const baseUrl = explicitBaseUrl ?? (explicitRegion ? void 0 : file.baseUrl);
1010
1142
  const controlBaseUrl = args.flags["control-base-url"] ?? process.env.ARKER_CONTROL_BASE_URL;
1011
- const region = explicitRegion ?? file.region;
1143
+ const region = explicitRegion ?? file.region ?? (baseUrl ? void 0 : DEFAULT_REGION);
1012
1144
  const provider = args.flags.provider ?? process.env.ARKER_PROVIDER;
1013
1145
  if (!apiKey) {
1014
1146
  die("Missing API key. Set ARKER_API_KEY or pass --api-key.");
1015
1147
  }
1016
- if (!baseUrl && !region) {
1017
- die("Missing region. Set ARKER_REGION or pass --region (e.g. us-west-2). --provider (aws|aws-burst) defaults to aws.");
1018
- }
1019
1148
  return new Arker({ apiKey, baseUrl, region, provider, controlBaseUrl });
1020
1149
  }
1021
1150
  function out(value) {
@@ -1503,134 +1632,6 @@ function bridgePty(pty, options) {
1503
1632
  });
1504
1633
  });
1505
1634
  }
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");
1527
- }
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
- );
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`;
1544
- try {
1545
- return new URL(client.baseUrl).hostname;
1546
- } catch {
1547
- die("could not determine SSH host; pass --host <hostname>");
1548
- }
1549
- }
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
- }
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> ...");
1632
- }
1633
- }
1634
1635
  function numFlag(args, name) {
1635
1636
  const v = args.flags[name];
1636
1637
  if (typeof v === "string") return Number(v);
@@ -1651,7 +1652,7 @@ async function readAllStdin() {
1651
1652
  function usage() {
1652
1653
  out(
1653
1654
  [
1654
- "arker \u2014 VM control plane CLI",
1655
+ `arker v${VERSION}`,
1655
1656
  "",
1656
1657
  "Usage:",
1657
1658
  " arker <command> [args]",
@@ -1668,8 +1669,6 @@ function usage() {
1668
1669
  " arker run <vm> <command> [--session-id <id>] [--session-idx N] run a command",
1669
1670
  " arker resize <vm> [--memory-mib N] [--vcpu N] [--disk-mib N] resize a VM (PATCH)",
1670
1671
  " 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",
1673
1672
  "",
1674
1673
  "Resources:",
1675
1674
  " arker vms <ls|get|rm|fork|run> ...",
@@ -1677,12 +1676,11 @@ function usage() {
1677
1676
  " arker sessions <ls|get|create|rm> <vm_id> ...",
1678
1677
  " arker syncs <ls|create|rm> <vm_id> ...",
1679
1678
  " arker filesystems <ls|create|get|rm> ... (alias: fs)",
1680
- " arker ssh-keys <ls|add|rm> ... manage account SSH keys",
1681
1679
  "",
1682
1680
  "Flags:",
1683
1681
  " --api-key <key> (or env ARKER_API_KEY)",
1684
1682
  " --region <region> (or env ARKER_REGION; e.g. us-west-2)",
1685
- " --provider <aws|aws-burst> (or env ARKER_PROVIDER; default aws)",
1683
+ " --provider <aws> (or env ARKER_PROVIDER; default aws)",
1686
1684
  " --base-url <url> override compute URL (env ARKER_BASE_URL)",
1687
1685
  " --control-base-url <url> override CF Worker URL (env ARKER_CONTROL_BASE_URL)",
1688
1686
  " --json emit JSON instead of tabular output",
@@ -1705,18 +1703,7 @@ function usage() {
1705
1703
  " --session-id <id> reconnect to an existing PTY session",
1706
1704
  " --command <path> shell executable path (default: /bin/bash)",
1707
1705
  " --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
- "",
1719
- `Arker org id: ${ARKER_ORG_ID}`
1706
+ " --no-persist close the remote PTY process on disconnect"
1720
1707
  ].join("\n")
1721
1708
  );
1722
1709
  process.exit(2);
@@ -1746,11 +1733,10 @@ async function main() {
1746
1733
  return await cmdSyncs(args, client);
1747
1734
  case "shell":
1748
1735
  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);
1736
+ // SSH is descoped/unsupported and hidden from the interface. The
1737
+ // implementation below (cmdSsh / cmdSshKeys) is kept intact; re-add
1738
+ // the `ssh` / `ssh-keys` cases here and their help entries to expose
1739
+ // it once the server-side SSH path is supported.
1754
1740
  // Resources.
1755
1741
  case "vms":
1756
1742
  return await cmdVms(args, client);