airgate 0.1.4 → 0.1.5

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.
Files changed (2) hide show
  1. package/dist/airgate.mjs +333 -48
  2. package/package.json +1 -1
package/dist/airgate.mjs CHANGED
@@ -431,6 +431,7 @@ var Api = class {
431
431
  if (e?.name === "AbortError") throw e;
432
432
  throw new NetworkError(this.gateway, e);
433
433
  }
434
+ if (opts2.bytes && res.ok) return Buffer.from(await res.arrayBuffer());
434
435
  const text2 = await res.text();
435
436
  if (opts2.text && res.ok) return text2;
436
437
  let data = {};
@@ -530,11 +531,11 @@ var Api = class {
530
531
  checkpoint(id, label2) {
531
532
  return this.request("POST", `/v1/pods/${enc(id)}/checkpoints`, { body: { label: label2 } }).then((r) => r.pod);
532
533
  }
533
- restore(id, checkpointId) {
534
- return this.request("POST", `/v1/pods/${enc(id)}/restore`, { body: { checkpointId } }).then((r) => r.pod);
534
+ restore(id, checkpointId2) {
535
+ return this.request("POST", `/v1/pods/${enc(id)}/restore`, { body: { checkpointId: checkpointId2 } }).then((r) => r.pod);
535
536
  }
536
- fork(id, checkpointId, overrides) {
537
- return this.request("POST", `/v1/pods/${enc(id)}/fork`, { body: { checkpointId, overrides } }).then((r) => r.pod);
537
+ fork(id, checkpointId2, overrides) {
538
+ return this.request("POST", `/v1/pods/${enc(id)}/fork`, { body: { checkpointId: checkpointId2, overrides } }).then((r) => r.pod);
538
539
  }
539
540
  diff(id, base) {
540
541
  return this.request("GET", `/v1/pods/${enc(id)}/diff?base=${encodeURIComponent(base)}`);
@@ -580,6 +581,38 @@ var Api = class {
580
581
  githubDisconnect(id) {
581
582
  return this.request("DELETE", `/v1/github/installations/${id}`);
582
583
  }
584
+ /* a workspace's files, live or at a checkpoint */
585
+ files(id, path = "", checkpoint2) {
586
+ const q = new URLSearchParams({ path, ...checkpoint2 ? { checkpoint: checkpoint2 } : {} });
587
+ return this.request("GET", `/v1/pods/${enc(id)}/files?${q}`);
588
+ }
589
+ fileRaw(id, path, checkpoint2) {
590
+ const q = new URLSearchParams({ path, ...checkpoint2 ? { checkpoint: checkpoint2 } : {} });
591
+ return this.request("GET", `/v1/pods/${enc(id)}/files/raw?${q}`, { bytes: true });
592
+ }
593
+ /** after launch: the network and the AI switch */
594
+ updatePod(id, patch) {
595
+ return this.request("PATCH", `/v1/pods/${enc(id)}`, { body: patch }).then((r) => r.pod);
596
+ }
597
+ /* saved snapshots */
598
+ snapshots() {
599
+ return this.request("GET", "/v1/snapshots").then((r) => r.snapshots);
600
+ }
601
+ saveSnapshot(id, name, checkpointId2) {
602
+ return this.request("POST", `/v1/pods/${enc(id)}/snapshots`, { body: { name, ...checkpointId2 ? { checkpointId: checkpointId2 } : {} } }).then((r) => r.snapshot);
603
+ }
604
+ sharedSnapshot(code3) {
605
+ return this.request("GET", `/v1/snapshots/shared/${enc(code3)}`).then((r) => r.snapshot);
606
+ }
607
+ launchSnapshot(ref, overrides = {}) {
608
+ return this.request("POST", `/v1/snapshots/${enc(ref)}/launch`, { body: { overrides } }).then((r) => r.pod);
609
+ }
610
+ shareSnapshot(ref, on) {
611
+ return this.request(on ? "POST" : "DELETE", `/v1/snapshots/${enc(ref)}/share`).then((r) => r.snapshot);
612
+ }
613
+ deleteSnapshot(ref) {
614
+ return this.request("DELETE", `/v1/snapshots/${enc(ref)}`);
615
+ }
583
616
  /* model sign-ins: connected to airgate (opt-in), and whether a laptop relay is connected */
584
617
  modelsAuth() {
585
618
  return this.request("GET", "/v1/models/auth");
@@ -7301,6 +7334,7 @@ var launch = {
7301
7334
  'autopilot jobs: --cron "0 2 * * *" --task "update deps, run tests" (repeat both for more; --job-name names them). more later: airgate job add'
7302
7335
  ],
7303
7336
  flags: {
7337
+ from: { type: "string", value: "snapshot", help: "start from a saved snapshot: its name, or a share code (airgate snapshots)" },
7304
7338
  name: { type: "string", value: "name" },
7305
7339
  template: { type: "string", value: "template", choices: TEMPLATE_IDS, help: `${TEMPLATE_IDS.join(" | ")} (default code)` },
7306
7340
  agent: { type: "string", value: "agent", choices: Object.keys(AGENTS), help: `${AGENT_NAMES.join(" | ")} (default: the template's first)` },
@@ -7323,6 +7357,18 @@ var launch = {
7323
7357
  },
7324
7358
  async run(ctx) {
7325
7359
  if (ctx.args.positionals.length) throw new UsageError(`unexpected "${ctx.args.positionals[0]}"`, "name the pod with --name");
7360
+ const from = str(ctx.args, "from");
7361
+ if (from) {
7362
+ const o = {};
7363
+ if (str(ctx.args, "name")) o.name = str(ctx.args, "name");
7364
+ if (str(ctx.args, "agent")) o.agent = str(ctx.args, "agent");
7365
+ if (ctx.args.flags.memory !== "standard") o.size = ctx.args.flags.memory;
7366
+ if (ctx.args.flags.browser === true) o.browser = true;
7367
+ const pod2 = await ctx.ui.spin(`starting a pod from ${from}`, async () => waitReady(ctx, await ctx.api.launchSnapshot(from, o)));
7368
+ if (ctx.json) return ctx.ui.json(pod2);
7369
+ ctx.ui.out(readyLine(ctx, pod2));
7370
+ return void ctx.ui.out(`next: airgate attach ${pod2.spec.name}`);
7371
+ }
7326
7372
  const spec = specFromFlags(ctx);
7327
7373
  if (spec.repo) await checkRepo(ctx, spec.repo);
7328
7374
  const envFile = str(ctx.args, "env");
@@ -7369,7 +7415,7 @@ function readDotenv(ctx, file) {
7369
7415
  }
7370
7416
  function specFromFlags(ctx, defaults = {}) {
7371
7417
  const a = ctx.args;
7372
- const network = str(a, "network") ?? "allowlist";
7418
+ const network2 = str(a, "network") ?? "allowlist";
7373
7419
  const autoStop = str(a, "auto-stop");
7374
7420
  let autoStopMinutes = null;
7375
7421
  if (autoStop === "keep-awake" || autoStop === "never") autoStopMinutes = 0;
@@ -7408,12 +7454,12 @@ function specFromFlags(ctx, defaults = {}) {
7408
7454
  agent,
7409
7455
  size: str(a, "memory") ?? "standard",
7410
7456
  masking: a.flags.mask !== false,
7411
- network,
7457
+ network: network2,
7412
7458
  autoStopMinutes,
7413
7459
  autoCheckpoint: a.flags.undo !== false
7414
7460
  };
7415
7461
  if (repo) spec.repo = repo;
7416
- if (network === "allowlist") spec.allow = [.../* @__PURE__ */ new Set([...DEFAULT_ALLOW, ...list(a, "allow").flatMap((h) => h.split(",")).map((h) => h.trim()).filter(Boolean)])];
7462
+ if (network2 === "allowlist") spec.allow = [.../* @__PURE__ */ new Set([...DEFAULT_ALLOW, ...list(a, "allow").flatMap((h) => h.split(",")).map((h) => h.trim()).filter(Boolean)])];
7417
7463
  const terms = list(a, "mask-term");
7418
7464
  if (terms.length) spec.maskTerms = terms;
7419
7465
  if (template !== "code") spec.template = template;
@@ -7497,7 +7543,10 @@ var destroy = {
7497
7543
  flags: { yes: { type: "boolean", short: "y", help: "don't ask" } },
7498
7544
  async run(ctx) {
7499
7545
  const p = await resolvePod(ctx, ctx.args.positionals[0]);
7500
- if (!await confirm(ctx, `destroy ${p.spec.name} (${p.id})? its files and checkpoints are gone for good.`)) {
7546
+ const pending = await ctx.api.diff(p.id, "up").catch(() => null);
7547
+ const unpulled = pending?.files.length ?? 0;
7548
+ if (unpulled && !ctx.json) ctx.ui.err(ctx.ui.ce("warn", `${p.spec.name} has ${unpulled} changed file(s) not brought back. keep them first: airgate down --pod ${p.spec.name} (or save it: airgate snapshot ${p.spec.name} <name>)`));
7549
+ if (!await confirm(ctx, `destroy ${p.spec.name} (${p.id})? its files and checkpoints are gone for good${unpulled ? `, including ${unpulled} changed file(s)` : ""}.`)) {
7501
7550
  throw new CliError("not destroyed");
7502
7551
  }
7503
7552
  await ctx.ui.spin(`destroying ${p.spec.name}`, ctx.api.destroy(p.id));
@@ -7728,7 +7777,7 @@ var log = {
7728
7777
  import { createInterface } from "node:readline";
7729
7778
 
7730
7779
  // src/version.ts
7731
- var VERSION = "0.1.4";
7780
+ var VERSION = "0.1.5";
7732
7781
 
7733
7782
  // src/commands/updown.ts
7734
7783
  import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
@@ -7780,7 +7829,7 @@ function gitFiles(dir) {
7780
7829
  return out;
7781
7830
  }
7782
7831
  function runTar(cwd, args, input, limit = UPLOAD_LIMIT) {
7783
- return new Promise((resolve6, reject) => {
7832
+ return new Promise((resolve7, reject) => {
7784
7833
  const child = spawn("tar", args, { cwd, env: { ...process.env, COPYFILE_DISABLE: "1" }, stdio: ["pipe", "pipe", "pipe"] });
7785
7834
  const chunks = [];
7786
7835
  let size2 = 0;
@@ -7800,7 +7849,7 @@ function runTar(cwd, args, input, limit = UPLOAD_LIMIT) {
7800
7849
  child.on("close", (code3) => {
7801
7850
  if (over2) return reject(new TooLargeError(limit));
7802
7851
  if (code3 !== 0) return reject(new Error(`tar failed: ${err.trim().split("\n").slice(-3).join(" ")}`));
7803
- resolve6(Buffer.concat(chunks));
7852
+ resolve7(Buffer.concat(chunks));
7804
7853
  });
7805
7854
  child.stdin.on("error", () => void 0);
7806
7855
  child.stdin.end(input ?? "");
@@ -7811,10 +7860,10 @@ async function packProject(dir, limit = UPLOAD_LIMIT) {
7811
7860
  const tracked = new Set(git(dir, ["ls-files", "-c", "-z"]).stdout.split("\0"));
7812
7861
  const all = gitFiles(dir).filter((f) => !f.startsWith(".airgate/"));
7813
7862
  const skipped2 = all.filter((f) => (isEnvFile(f) || f.split("/").pop() === ".envrc") && !tracked.has(f));
7814
- const files = all.filter((f) => !skipped2.includes(f));
7815
- if (!files.length) throw new Error("no files to send (git sees nothing tracked or untracked here)");
7816
- const archive2 = await runTar(dir, ["--null", "-T", "-", "-czf", "-"], files.join("\0") + "\0", limit);
7817
- return { archive: archive2, files: files.length, git: true, skipped: skipped2 };
7863
+ const files2 = all.filter((f) => !skipped2.includes(f));
7864
+ if (!files2.length) throw new Error("no files to send (git sees nothing tracked or untracked here)");
7865
+ const archive2 = await runTar(dir, ["--null", "-T", "-", "-czf", "-"], files2.join("\0") + "\0", limit);
7866
+ return { archive: archive2, files: files2.length, git: true, skipped: skipped2 };
7818
7867
  }
7819
7868
  const skipped = readdirSync(dir).filter((f) => isEnvFile(f) || f === ".envrc");
7820
7869
  const archive = await runTar(dir, ["--exclude=node_modules", "--exclude=.git", "--exclude=.airgate", ...ENV_EXCLUDES, "-czf", "-", "."], null, limit);
@@ -7895,7 +7944,7 @@ function firstLine(file) {
7895
7944
  }
7896
7945
  }
7897
7946
  function codexRollouts(root, limit = 3e3) {
7898
- const files = [];
7947
+ const files2 = [];
7899
7948
  const walk = (d, depth) => {
7900
7949
  let list2 = [];
7901
7950
  try {
@@ -7906,11 +7955,11 @@ function codexRollouts(root, limit = 3e3) {
7906
7955
  for (const x of list2) {
7907
7956
  const full = join3(d, x.name);
7908
7957
  if (x.isDirectory() && depth < 4) walk(full, depth + 1);
7909
- else if (x.isFile() && /^rollout-.*\.jsonl$/.test(x.name)) files.push({ f: full, m: statSync(full).mtimeMs });
7958
+ else if (x.isFile() && /^rollout-.*\.jsonl$/.test(x.name)) files2.push({ f: full, m: statSync(full).mtimeMs });
7910
7959
  }
7911
7960
  };
7912
7961
  walk(join3(root, "sessions"), 0);
7913
- return files.sort((a, b2) => b2.m - a.m).slice(0, limit).map((x) => x.f);
7962
+ return files2.sort((a, b2) => b2.m - a.m).slice(0, limit).map((x) => x.f);
7914
7963
  }
7915
7964
  var codexId = (file) => /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/.exec(file)?.[1] ?? null;
7916
7965
  function findCodexSession(home, projectDir, id, env = {}) {
@@ -8161,8 +8210,8 @@ function rewritePaths(text2, pre) {
8161
8210
  }
8162
8211
  return lines.join("\n");
8163
8212
  }
8164
- function orderFiles(files) {
8165
- return [...files.filter((f) => f.risky), ...files.filter((f) => !f.risky)];
8213
+ function orderFiles(files2) {
8214
+ return [...files2.filter((f) => f.risky), ...files2.filter((f) => !f.risky)];
8166
8215
  }
8167
8216
  function refusals(blocks) {
8168
8217
  const out = [];
@@ -8416,9 +8465,9 @@ function printSessions(ctx, list2) {
8416
8465
  function printFiles(ctx, blocks, toErr) {
8417
8466
  const line2 = toErr ? ctx.ui.err : ctx.ui.out;
8418
8467
  const c = toErr ? ctx.ui.ce : ctx.ui.c;
8419
- const files = orderFiles(blocks.map((b2) => b2.file));
8420
- const width = Math.max(...files.map((f) => f.path.length + (f.from ? f.from.length + 4 : 0)));
8421
- for (const f of files) {
8468
+ const files2 = orderFiles(blocks.map((b2) => b2.file));
8469
+ const width = Math.max(...files2.map((f) => f.path.length + (f.from ? f.from.length + 4 : 0)));
8470
+ for (const f of files2) {
8422
8471
  const name = f.from ? `${f.from} \u2192 ${f.path}` : f.path;
8423
8472
  const mark = f.risky ? c("warn", "!") : " ";
8424
8473
  line2(` ${mark} ${STATUS_MARK[f.status]} ${f.risky ? name.padEnd(width) + " " + c("warn", f.reason ?? "") : name}`.trimEnd());
@@ -8469,9 +8518,9 @@ var down2 = {
8469
8518
  const { blocks, outside } = selectProject(parsePatch(d.patch), prefix);
8470
8519
  const refused = refusals(blocks);
8471
8520
  const patch = blocks.map((b2) => b2.text).join("");
8472
- const files = orderFiles(blocks.map((b2) => b2.file));
8521
+ const files2 = orderFiles(blocks.map((b2) => b2.file));
8473
8522
  if (ctx.json && (dry || !blocks.length)) {
8474
- return ui2.json({ pod: pod.id, base, files, outside, refused, patch, applied: false });
8523
+ return ui2.json({ pod: pod.id, base, files: files2, outside, refused, patch, applied: false });
8475
8524
  }
8476
8525
  const takeSessions = !dry && !bool(ctx.args, "no-session") && found?.state.podId === pod.id;
8477
8526
  if (!blocks.length) {
@@ -8480,8 +8529,8 @@ var down2 = {
8480
8529
  if (takeSessions && found) await downSessions(ctx, found.root, found.state, pod);
8481
8530
  return;
8482
8531
  }
8483
- const risky = files.filter((f) => f.risky).length;
8484
- const head = `${files.length} file(s) from ${pod.spec.name}${risky ? `, ${risky} risky` : ""}:`;
8532
+ const risky = files2.filter((f) => f.risky).length;
8533
+ const head = `${files2.length} file(s) from ${pod.spec.name}${risky ? `, ${risky} risky` : ""}:`;
8485
8534
  if (dry) {
8486
8535
  ui2.err(head);
8487
8536
  printFiles(ctx, blocks, true);
@@ -8516,7 +8565,7 @@ var down2 = {
8516
8565
  throw new CliError(`the patch doesn't apply here, nothing changed:
8517
8566
  ${check.stderr.trim()}`, "commit or stash local edits to those files, or look with: airgate down --dry-run");
8518
8567
  }
8519
- if (!await confirm(ctx, `apply ${files.length} file(s)${risky ? ` (${risky} risky)` : ""}?`)) throw new CliError("nothing applied");
8568
+ if (!await confirm(ctx, `apply ${files2.length} file(s)${risky ? ` (${risky} risky)` : ""}?`)) throw new CliError("nothing applied");
8520
8569
  const applied = git(top, ["apply", ...applyArgs], patch);
8521
8570
  if (applied.status !== 0) throw new CliError(`git apply failed: ${applied.stderr.trim()}`);
8522
8571
  let acked = true;
@@ -8526,8 +8575,8 @@ ${check.stderr.trim()}`, "commit or stash local edits to those files, or look wi
8526
8575
  acked = false;
8527
8576
  }
8528
8577
  const convo = takeSessions && found ? await downSessions(ctx, found.root, found.state, pod, true) : [];
8529
- if (ctx.json) return ui2.json({ pod: pod.id, base, files, outside, refused, applied: true, acked, sessions: convo });
8530
- ui2.out(`${ui2.c("ok", "down")}: applied ${files.length} file(s). review: git status \xB7 git diff`);
8578
+ if (ctx.json) return ui2.json({ pod: pod.id, base, files: files2, outside, refused, applied: true, acked, sessions: convo });
8579
+ ui2.out(`${ui2.c("ok", "down")}: applied ${files2.length} file(s). review: git status \xB7 git diff`);
8531
8580
  if (!acked) ui2.err(ui2.ce("warn", "couldn't tell the pod what was taken; the next down will show these again"));
8532
8581
  printSessions(ctx, convo);
8533
8582
  }
@@ -8588,7 +8637,7 @@ var LineReader = class {
8588
8637
  }
8589
8638
  };
8590
8639
  function ttyPrompt(stdin, out) {
8591
- return (question, opts2 = {}) => new Promise((resolve6, reject) => {
8640
+ return (question, opts2 = {}) => new Promise((resolve7, reject) => {
8592
8641
  out.write(question);
8593
8642
  const decoder = new StringDecoder2("utf8");
8594
8643
  let answer = "";
@@ -8601,7 +8650,7 @@ function ttyPrompt(stdin, out) {
8601
8650
  stdin.pause();
8602
8651
  out.write("\n");
8603
8652
  if (err) reject(err);
8604
- else resolve6(answer);
8653
+ else resolve7(answer);
8605
8654
  };
8606
8655
  const onData = (d) => {
8607
8656
  for (const ch of decoder.write(d)) {
@@ -8628,16 +8677,16 @@ function ttyPrompt(stdin, out) {
8628
8677
  }
8629
8678
  function openInBrowser(url) {
8630
8679
  const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url]] : ["xdg-open", [url]];
8631
- return new Promise((resolve6) => {
8680
+ return new Promise((resolve7) => {
8632
8681
  try {
8633
8682
  const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
8634
- child.on("error", () => resolve6(false));
8683
+ child.on("error", () => resolve7(false));
8635
8684
  child.on("spawn", () => {
8636
8685
  child.unref();
8637
- resolve6(true);
8686
+ resolve7(true);
8638
8687
  });
8639
8688
  } catch {
8640
- resolve6(false);
8689
+ resolve7(false);
8641
8690
  }
8642
8691
  });
8643
8692
  }
@@ -9213,20 +9262,20 @@ When you are done, leave your changes in the files there (no need to commit or p
9213
9262
  ui2.err(ui2.ce("bad", keepNote(`the task finished, but its changes didn't come back: ${why}`)));
9214
9263
  return 1;
9215
9264
  }
9216
- const files = result.files ?? [];
9265
+ const files2 = result.files ?? [];
9217
9266
  const keep = noApply || bool(ctx.args, "keep");
9218
9267
  await finish(ctx, pod, keep ? "sleep" : "destroy");
9219
9268
  if (!keep) restoreState();
9220
- if (ctx.json) return out({ applied: !noApply && files.length > 0, files, ...noApply ? { patch: result.patch ?? "" } : {}, pod_state: keep ? "sleeping" : "deleted" });
9269
+ if (ctx.json) return out({ applied: !noApply && files2.length > 0, files: files2, ...noApply ? { patch: result.patch ?? "" } : {}, pod_state: keep ? "sleeping" : "deleted" });
9221
9270
  ctx.io.stdout.write(r.log.endsWith("\n") || !r.log ? r.log : r.log + "\n");
9222
9271
  if (noApply) {
9223
9272
  ctx.io.stdout.write(result.patch ?? "");
9224
- ui2.err(`${files.length} file(s) changed, not applied. apply: airgate down --pod ${name} \xB7 delete the pod: airgate destroy ${name}`);
9273
+ ui2.err(`${files2.length} file(s) changed, not applied. apply: airgate down --pod ${name} \xB7 delete the pod: airgate destroy ${name}`);
9225
9274
  return 0;
9226
9275
  }
9227
- const risky = files.filter((f) => f.risky).length;
9276
+ const risky = files2.filter((f) => f.risky).length;
9228
9277
  ui2.out(
9229
- `${ui2.c("ok", "done")}: ${files.length ? `${files.length} file(s) changed here${risky ? `, ${risky} risky` : ""}. review: git status \xB7 git diff` : "no changes came back"}` + (keep ? ` \xB7 the pod is kept (asleep): ${name}` : " \xB7 the pod is deleted")
9278
+ `${ui2.c("ok", "done")}: ${files2.length ? `${files2.length} file(s) changed here${risky ? `, ${risky} risky` : ""}. review: git status \xB7 git diff` : "no changes came back"}` + (keep ? ` \xB7 the pod is kept (asleep): ${name}` : " \xB7 the pod is deleted")
9230
9279
  );
9231
9280
  return 0;
9232
9281
  }
@@ -9287,9 +9336,19 @@ var TOOLS = [
9287
9336
  model: s("built-in model: an OpenRouter id, e.g. qwen/qwen3-coder"),
9288
9337
  repo: s("GitHub repo to clone, owner/name"),
9289
9338
  browser: b("add Chromium + Playwright the agent can drive"),
9290
- jobs: { type: "array", items: jobSchema, description: "autopilot: scheduled jobs" }
9339
+ jobs: { type: "array", items: jobSchema, description: "autopilot: scheduled jobs" },
9340
+ fromSnapshot: s("start from a saved snapshot (its name, or a share code): its workspace and settings")
9291
9341
  }),
9292
9342
  async call(ctx, a) {
9343
+ if (typeof a.fromSnapshot === "string" && a.fromSnapshot) {
9344
+ const o = {};
9345
+ if (typeof a.name === "string" && a.name) o.name = a.name;
9346
+ if (typeof a.agent === "string" && a.agent) o.agent = a.agent;
9347
+ if (a.browser === true) o.browser = true;
9348
+ const pod2 = await waitReady(ctx, await ctx.api.launchSnapshot(a.fromSnapshot, o));
9349
+ OWN.set(pod2.id, "launch");
9350
+ return { name: pod2.spec.name, id: pod2.id, state: pod2.state, spec: pod2.spec };
9351
+ }
9293
9352
  const argv = [];
9294
9353
  const opt = (flag, v) => typeof v === "string" && v && argv.push(`--${flag}`, v);
9295
9354
  opt("name", a.name);
@@ -9520,6 +9579,62 @@ var TOOLS = [
9520
9579
  return { deleted: p.spec.name, id: p.id };
9521
9580
  }
9522
9581
  },
9582
+ {
9583
+ name: "pod_files",
9584
+ description: "List a folder of a pod's workspace (path relative to ~/workspace; '' for the top), live or at a checkpoint. Looking at a checkpoint never rewinds the pod.",
9585
+ inputSchema: obj({ pod: POD, path: s("folder inside the workspace (default: the top)"), checkpoint: s("a checkpoint id or label (default: the live workspace)") }, ["pod"]),
9586
+ async call(ctx, a) {
9587
+ const p = await resolvePod(ctx, need(a, "pod"));
9588
+ const cp = typeof a.checkpoint === "string" && a.checkpoint ? resolveCheckpoint(await ctx.api.checkpoints(p.id), a.checkpoint).id : void 0;
9589
+ return ctx.api.files(p.id, typeof a.path === "string" ? a.path : "", cp);
9590
+ }
9591
+ },
9592
+ {
9593
+ name: "pod_file_read",
9594
+ description: "Read one file of a pod's workspace, live or at a checkpoint (e.g. to compare with how it was before). Text up to 256 KB comes back as text; anything else as base64.",
9595
+ inputSchema: obj({ pod: POD, path: s("file inside the workspace"), checkpoint: s("a checkpoint id or label (default: live)") }, ["pod", "path"]),
9596
+ async call(ctx, a) {
9597
+ const p = await resolvePod(ctx, need(a, "pod"));
9598
+ const cp = typeof a.checkpoint === "string" && a.checkpoint ? resolveCheckpoint(await ctx.api.checkpoints(p.id), a.checkpoint).id : void 0;
9599
+ const body = await ctx.api.fileRaw(p.id, need(a, "path"), cp);
9600
+ const text2 = body.toString("utf8");
9601
+ if (body.length <= 256 * 1024 && !text2.includes("\uFFFD") && !body.includes(0)) return { path: a.path, bytes: body.length, text: text2 };
9602
+ return { path: a.path, bytes: body.length, base64: body.length <= 2 * 1024 * 1024 ? body.toString("base64") : void 0, note: body.length > 2 * 1024 * 1024 ? "too big to return here: airgate pull" : void 0 };
9603
+ }
9604
+ },
9605
+ {
9606
+ name: "snapshot_save",
9607
+ description: "Save a pod's workspace (live, or at a checkpoint) as a named snapshot new pods can start from (pod_launch with fromSnapshot). Only files: never secrets, conversations or the masking vault.",
9608
+ inputSchema: obj({ pod: POD, name: s("a name for it"), checkpoint: s("save it as it was at this checkpoint (default: live)") }, ["pod", "name"]),
9609
+ async call(ctx, a) {
9610
+ const p = await resolvePod(ctx, need(a, "pod"));
9611
+ const cp = typeof a.checkpoint === "string" && a.checkpoint ? resolveCheckpoint(await ctx.api.checkpoints(p.id), a.checkpoint).id : void 0;
9612
+ return ctx.api.saveSnapshot(p.id, need(a, "name"), cp);
9613
+ }
9614
+ },
9615
+ {
9616
+ name: "snapshots_list",
9617
+ description: "Your saved snapshots (name, state, which pod they came from, whether shared).",
9618
+ inputSchema: obj({}),
9619
+ async call(ctx) {
9620
+ return ctx.api.snapshots();
9621
+ }
9622
+ },
9623
+ {
9624
+ name: "pod_settings",
9625
+ description: "Change a pod's network after launch (open | allowlist | off, extra allowlist hosts) or switch its AI off/on (aiOff: no model calls at all). Applies immediately. Only when the user asked for it.",
9626
+ inputSchema: obj({ pod: POD, network: s("open | allowlist | off", { enum: ["open", "allowlist", "off"] }), allow: { type: "array", items: { type: "string" }, description: "extra allowlist hosts (replaces the list)" }, aiOff: b("true switches the pod's AI off; false back on") }, ["pod"]),
9627
+ async call(ctx, a) {
9628
+ const p = await resolvePod(ctx, need(a, "pod"));
9629
+ const patch = {};
9630
+ if (typeof a.network === "string") patch.network = a.network;
9631
+ if (Array.isArray(a.allow)) patch.allow = a.allow.map(String);
9632
+ if (typeof a.aiOff === "boolean") patch.aiOff = a.aiOff;
9633
+ if (!Object.keys(patch).length) throw new UsageError("nothing to change: network, allow or aiOff");
9634
+ const pod = await ctx.api.updatePod(p.id, patch);
9635
+ return { name: pod.spec.name, network: pod.spec.network, allow: pod.spec.allow ?? [], aiOff: Boolean(pod.spec.aiOff) };
9636
+ }
9637
+ },
9523
9638
  {
9524
9639
  name: "pod_ports",
9525
9640
  description: "What is listening in a pod (dev servers on ports 1024+) and its preview links.",
@@ -9867,7 +9982,7 @@ var race = {
9867
9982
  async run(ctx) {
9868
9983
  const [ref, cpRef = "current"] = ctx.args.positionals;
9869
9984
  const p = await resolvePod(ctx, ref);
9870
- const checkpointId = cpRef === "current" ? "current" : resolveCheckpoint(await ctx.api.checkpoints(p.id), cpRef).id;
9985
+ const checkpointId2 = cpRef === "current" ? "current" : resolveCheckpoint(await ctx.api.checkpoints(p.id), cpRef).id;
9871
9986
  const overrides = {};
9872
9987
  const agent = str(ctx.args, "agent");
9873
9988
  if (agent) overrides.agent = agent;
@@ -9875,7 +9990,7 @@ var race = {
9875
9990
  if (name) overrides.name = name;
9876
9991
  let pod;
9877
9992
  try {
9878
- pod = await ctx.ui.spin(`forking ${p.spec.name}`, async () => waitReady(ctx, await ctx.api.fork(p.id, checkpointId, overrides)));
9993
+ pod = await ctx.ui.spin(`forking ${p.spec.name}`, async () => waitReady(ctx, await ctx.api.fork(p.id, checkpointId2, overrides)));
9879
9994
  } catch (e) {
9880
9995
  if (e instanceof ApiError && e.code === "checkpoint_not_mounted") {
9881
9996
  throw new CliError("that checkpoint can't be read on the provider right now", `race from the live workspace: airgate race ${p.spec.name} current (or rewind first)`);
@@ -9884,7 +9999,7 @@ var race = {
9884
9999
  }
9885
10000
  if (ctx.json) return ctx.ui.json(pod);
9886
10001
  ctx.ui.out(readyLine(ctx, pod));
9887
- ctx.ui.out(`racing ${p.spec.name} from ${checkpointId === "current" ? "its current state" : `"${cpRef}"`}. compare: airgate down --pod ${pod.spec.name} --dry-run`);
10002
+ ctx.ui.out(`racing ${p.spec.name} from ${checkpointId2 === "current" ? "its current state" : `"${cpRef}"`}. compare: airgate down --pod ${pod.spec.name} --dry-run`);
9888
10003
  }
9889
10004
  };
9890
10005
 
@@ -10148,19 +10263,19 @@ function validateForm(f) {
10148
10263
  return e;
10149
10264
  }
10150
10265
  function formSpec(f, catalog = null) {
10151
- const network = NETWORK_LIST[f.network];
10266
+ const network2 = NETWORK_LIST[f.network];
10152
10267
  const template = formTemplate(f);
10153
10268
  const spec = {
10154
10269
  name: f.name.trim(),
10155
10270
  agent: AGENT_LIST[f.agent],
10156
10271
  size: SIZE_LIST[f.memory],
10157
10272
  masking: f.mask,
10158
- network,
10273
+ network: network2,
10159
10274
  autoStopMinutes: null,
10160
10275
  autoCheckpoint: f.undo
10161
10276
  };
10162
10277
  if (f.repo.trim()) spec.repo = f.repo.trim();
10163
- if (network === "allowlist") spec.allow = DEFAULT_ALLOW2;
10278
+ if (network2 === "allowlist") spec.allow = DEFAULT_ALLOW2;
10164
10279
  if (template !== "code") spec.template = template;
10165
10280
  if (f.browser) spec.browser = true;
10166
10281
  if (template === "autopilot") spec.jobs = [{ cron: formCron(f), task: f.task.trim(), enabled: true }];
@@ -11437,6 +11552,171 @@ var ui = {
11437
11552
  }
11438
11553
  };
11439
11554
 
11555
+ // src/commands/workspace.ts
11556
+ import { writeFileSync as writeFileSync7, existsSync as existsSync6, statSync as statSync4 } from "node:fs";
11557
+ import { basename as basename3, resolve as resolve6 } from "node:path";
11558
+ async function checkpointId(ctx, podId, ref) {
11559
+ if (!ref || ref === "current") return void 0;
11560
+ return resolveCheckpoint(await ctx.api.checkpoints(podId), ref).id;
11561
+ }
11562
+ var files = {
11563
+ name: "files",
11564
+ aliases: ["ls-files"],
11565
+ group: "pods",
11566
+ usage: "<pod> [path] [--at <checkpoint>]",
11567
+ summary: "what's in a pod's workspace, live or at a checkpoint (nothing is rewound)",
11568
+ example: 'airgate files web src --at "#2"',
11569
+ notes: ["--at takes a checkpoint id, its label, or #1 for the newest (airgate checkpoints <pod>). get a file with: airgate pull <pod> <path>"],
11570
+ flags: { at: { type: "string", value: "checkpoint", help: "look at a checkpoint instead of the live workspace" } },
11571
+ async run(ctx) {
11572
+ const [ref, path = ""] = ctx.args.positionals;
11573
+ if (!ref) throw new UsageError("which pod? airgate files <pod> [path]");
11574
+ const pod = await resolvePod(ctx, ref);
11575
+ const at3 = await checkpointId(ctx, pod.id, str(ctx.args, "at"));
11576
+ const r = await ctx.api.files(pod.id, path, at3);
11577
+ if (ctx.json) return ctx.ui.json(r);
11578
+ if (!r.entries.length) return void ctx.ui.out(ctx.ui.c("dim", `${r.path || "~/workspace"} is empty`));
11579
+ ctx.ui.table(
11580
+ r.entries.map((e) => [e.type === "dir" ? `${e.name}/` : e.name, e.type === "dir" ? "" : ctx.ui.c("dim", bytes(e.size))]),
11581
+ { head: [`${pod.spec.name}:~/workspace/${r.path}${at3 ? ` @ ${str(ctx.args, "at")}` : ""}`, ""] }
11582
+ );
11583
+ }
11584
+ };
11585
+ var pull = {
11586
+ name: "pull",
11587
+ group: "pods",
11588
+ usage: "<pod> <path> [--at <checkpoint>] [-o <file>]",
11589
+ summary: "copy one file from a pod (live or at a checkpoint) to this laptop",
11590
+ example: 'airgate pull web src/app.ts --at "before the refactor"',
11591
+ notes: ["up to 25 MB. without -o it lands in this folder under its own name (never over an existing file unless --force). -o - prints it."],
11592
+ flags: {
11593
+ at: { type: "string", value: "checkpoint", help: "from a checkpoint instead of the live workspace" },
11594
+ out: { type: "string", short: "o", value: "file", help: "where to save it (- prints it)" },
11595
+ force: { type: "boolean", help: "overwrite an existing file" }
11596
+ },
11597
+ async run(ctx) {
11598
+ const [ref, path] = ctx.args.positionals;
11599
+ if (!ref || !path) throw new UsageError("airgate pull <pod> <path>");
11600
+ const pod = await resolvePod(ctx, ref);
11601
+ const at3 = await checkpointId(ctx, pod.id, str(ctx.args, "at"));
11602
+ const body = await ctx.ui.spin(`reading ${path}`, ctx.api.fileRaw(pod.id, path, at3));
11603
+ const out = str(ctx.args, "out") ?? basename3(path);
11604
+ if (out === "-") return void ctx.io.stdout.write(body);
11605
+ const dest = resolve6(ctx.io.cwd, out);
11606
+ if (existsSync6(dest) && !bool(ctx.args, "force")) {
11607
+ if (statSync4(dest).isDirectory()) return void save2(ctx, resolve6(dest, basename3(path)), body);
11608
+ throw new CliError(`${out} exists here`, "pick another name with -o, or --force to overwrite");
11609
+ }
11610
+ save2(ctx, dest, body);
11611
+ }
11612
+ };
11613
+ function save2(ctx, dest, body) {
11614
+ writeFileSync7(dest, body);
11615
+ if (ctx.json) return ctx.ui.json({ saved: dest, bytes: body.length });
11616
+ ctx.ui.out(`${ctx.ui.c("ok", "saved")} ${dest} (${bytes(body.length)})`);
11617
+ }
11618
+ var ai = {
11619
+ name: "ai",
11620
+ group: "pods",
11621
+ usage: "<pod> on|off",
11622
+ summary: "switch a pod's AI off (no model calls at all) or back on",
11623
+ example: "airgate ai web off",
11624
+ notes: ["off: airgate refuses the pod's model calls, and on allowlist or off networks the model hosts are closed too (an open network can still reach them directly)."],
11625
+ async run(ctx) {
11626
+ const [ref, state] = ctx.args.positionals;
11627
+ if (!ref || state !== "on" && state !== "off") throw new UsageError("airgate ai <pod> on|off");
11628
+ const pod = await resolvePod(ctx, ref);
11629
+ const p = await ctx.api.updatePod(pod.id, { aiOff: state === "off" });
11630
+ if (ctx.json) return ctx.ui.json(p);
11631
+ ctx.ui.out(state === "off" ? `${ctx.ui.c("warn", "AI off")} for ${p.spec.name}: no model calls.${p.spec.network === "open" ? ctx.ui.c("dim", " (its network is open, so it could still reach a model directly: airgate network " + p.spec.name + " allowlist)") : ""}` : `${ctx.ui.c("ok", "AI on")} for ${p.spec.name}.`);
11632
+ }
11633
+ };
11634
+ var network = {
11635
+ name: "network",
11636
+ group: "pods",
11637
+ usage: "<pod> [open|allowlist|off] [--allow host,host] [--add host] [--remove host]",
11638
+ summary: "see or change what a pod can reach on the internet, now",
11639
+ example: "airgate network web allowlist --add api.stripe.com",
11640
+ notes: [
11641
+ "allowlist: common package registries and GitHub, plus your hosts; off: only the agent's model and airgate; open: anything.",
11642
+ "the agent's own model and sign-in hosts stay reachable unless its AI is off."
11643
+ ],
11644
+ flags: {
11645
+ allow: { type: "string", value: "hosts", help: "replace the extra hosts (comma separated)" },
11646
+ add: { type: "string", value: "host", help: "add hosts (comma separated)" },
11647
+ remove: { type: "string", value: "host", help: "remove hosts (comma separated)" }
11648
+ },
11649
+ async run(ctx) {
11650
+ const [ref, mode] = ctx.args.positionals;
11651
+ if (!ref) throw new UsageError("airgate network <pod> [open|allowlist|off]");
11652
+ if (mode && !["open", "allowlist", "off"].includes(mode)) throw new UsageError("open, allowlist or off");
11653
+ const pod = await resolvePod(ctx, ref);
11654
+ const list2 = (v) => (v ?? "").split(",").map((h) => h.trim().toLowerCase()).filter(Boolean);
11655
+ let allow = pod.spec.allow ?? [];
11656
+ if (str(ctx.args, "allow") !== void 0) allow = list2(str(ctx.args, "allow"));
11657
+ allow = [.../* @__PURE__ */ new Set([...allow, ...list2(str(ctx.args, "add"))])].filter((h) => !list2(str(ctx.args, "remove")).includes(h));
11658
+ const changed = mode || ["allow", "add", "remove"].some((k) => str(ctx.args, k) !== void 0);
11659
+ const p = changed ? await ctx.api.updatePod(pod.id, { ...mode ? { network: mode } : {}, allow }) : pod;
11660
+ if (ctx.json) return ctx.ui.json({ network: p.spec.network, allow: p.spec.allow ?? [], aiOff: Boolean(p.spec.aiOff) });
11661
+ ctx.ui.out(`${p.spec.name}: ${p.spec.network}${p.spec.network === "allowlist" && (p.spec.allow ?? []).length ? ` + ${(p.spec.allow ?? []).join(", ")}` : ""}${p.spec.aiOff ? ctx.ui.c("warn", " \xB7 AI off") : ""}${changed ? ctx.ui.c("dim", " (applied now)") : ""}`);
11662
+ }
11663
+ };
11664
+ var snapshot2 = {
11665
+ name: "snapshot",
11666
+ aliases: ["snapshots"],
11667
+ group: "undo \xB7 race",
11668
+ usage: "[<pod> <name> [--at <checkpoint>] | share|unshare|rm <name> | show <code>]",
11669
+ summary: "save a pod as a snapshot new pods start from; share it by code",
11670
+ example: 'airgate snapshot web "clean base"',
11671
+ notes: [
11672
+ 'a snapshot is a frozen copy of the workspace (live, or at a checkpoint): it stays when the pod is deleted. start a pod from it: airgate launch --from "<name>" (or a share code).',
11673
+ "only the workspace is copied: never the pod's secrets, its agent's conversations or its masking vault. share gives a code anyone signed in to airgate can launch from; unshare stops it.",
11674
+ "with no arguments: your snapshots."
11675
+ ],
11676
+ flags: { at: { type: "string", value: "checkpoint", help: "save the workspace as it was at a checkpoint" }, yes: { type: "boolean", short: "y", help: "don't ask" } },
11677
+ async run(ctx) {
11678
+ const { ui: ui2 } = ctx;
11679
+ const [a, b2, extra] = ctx.args.positionals;
11680
+ if (extra) throw new UsageError(`unexpected "${extra}" (put names with spaces in quotes)`);
11681
+ if (!a) {
11682
+ const list2 = await ctx.api.snapshots();
11683
+ if (ctx.json) return ui2.json(list2);
11684
+ if (!list2.length) return void ui2.out(ui2.c("dim", "no snapshots yet. save one: airgate snapshot <pod> <name>"));
11685
+ ui2.table(
11686
+ list2.map((s3) => [s3.name, s3.state === "ready" ? ui2.c("ok", "ready") : s3.state === "failed" ? ui2.c("bad", "failed") : ui2.c("warn", "saving"), `from ${s3.from.podName}`, s3.shareCode ? ui2.c("warn", `shared: ${s3.shareCode}`) : ui2.c("dim", "private")]),
11687
+ { head: ["snapshot", "", "", ""] }
11688
+ );
11689
+ return void ui2.out(ui2.c("dim", 'start a pod from one: airgate launch --from "<name>"'));
11690
+ }
11691
+ if (a === "share" || a === "unshare") {
11692
+ if (!b2) throw new UsageError(`airgate snapshot ${a} <name>`);
11693
+ const s3 = await ctx.api.shareSnapshot(b2, a === "share");
11694
+ if (ctx.json) return ui2.json(s3);
11695
+ if (a === "unshare") return void ui2.out(`${s3.name}: private again; its old code no longer works.`);
11696
+ ui2.out(`${s3.name}: share code ${s3.shareCode ?? ""}`);
11697
+ return void ui2.out(ui2.c("dim", `anyone signed in to airgate can start a pod from it: airgate launch --from ${s3.shareCode}. it shares only the workspace files.`));
11698
+ }
11699
+ if (a === "rm" || a === "delete") {
11700
+ if (!b2) throw new UsageError("airgate snapshot rm <name>");
11701
+ if (!await confirm(ctx, `delete the snapshot "${b2}"? pods already started from it keep their files.`)) throw new CliError("nothing deleted");
11702
+ await ctx.api.deleteSnapshot(b2);
11703
+ return ctx.json ? ui2.json({ ok: true }) : void ui2.out(`deleted ${b2}.`);
11704
+ }
11705
+ if (a === "show") {
11706
+ if (!b2) throw new UsageError("airgate snapshot show <code>");
11707
+ const s3 = await ctx.api.sharedSnapshot(b2);
11708
+ if (ctx.json) return ui2.json(s3);
11709
+ return void ui2.out(`${s3.name} \xB7 ${s3.agent} \xB7 ${s3.template}${s3.state !== "ready" ? ` \xB7 ${s3.state}` : ""}. start a pod from it: airgate launch --from ${b2}`);
11710
+ }
11711
+ if (!b2) throw new UsageError("airgate snapshot <pod> <name> (or: airgate snapshots)");
11712
+ const pod = await resolvePod(ctx, a);
11713
+ const at3 = await checkpointId(ctx, pod.id, str(ctx.args, "at"));
11714
+ const s2 = await ui2.spin(`saving ${pod.spec.name} as "${b2}"`, ctx.api.saveSnapshot(pod.id, b2, at3));
11715
+ if (ctx.json) return ui2.json(s2);
11716
+ ui2.out(`${ui2.c("ok", "saved")} "${s2.name}" from ${pod.spec.name}${at3 ? ` at ${str(ctx.args, "at")}` : ""}. ${ui2.c("dim", `start a pod from it: airgate launch --from "${s2.name}" \xB7 share it: airgate snapshot share "${s2.name}"`)}`);
11717
+ }
11718
+ };
11719
+
11440
11720
  // src/cli.ts
11441
11721
  var help = {
11442
11722
  name: "help",
@@ -11493,6 +11773,11 @@ var COMMANDS = [
11493
11773
  runTask,
11494
11774
  models,
11495
11775
  relay,
11776
+ files,
11777
+ pull,
11778
+ snapshot2,
11779
+ ai,
11780
+ network,
11496
11781
  usage,
11497
11782
  plan,
11498
11783
  upgrade,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "airgate",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Give your AI its own computer: the airgate command line.",
5
5
  "type": "module",
6
6
  "bin": {