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.
- package/dist/airgate.mjs +333 -48
- 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,
|
|
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,
|
|
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
|
|
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 (
|
|
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
|
-
|
|
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.
|
|
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((
|
|
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
|
-
|
|
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
|
|
7815
|
-
if (!
|
|
7816
|
-
const archive2 = await runTar(dir, ["--null", "-T", "-", "-czf", "-"],
|
|
7817
|
-
return { archive: archive2, files:
|
|
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
|
|
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))
|
|
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
|
|
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(
|
|
8165
|
-
return [...
|
|
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
|
|
8420
|
-
const width = Math.max(...
|
|
8421
|
-
for (const f of
|
|
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
|
|
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 =
|
|
8484
|
-
const head = `${
|
|
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 ${
|
|
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 ${
|
|
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((
|
|
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
|
|
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((
|
|
8680
|
+
return new Promise((resolve7) => {
|
|
8632
8681
|
try {
|
|
8633
8682
|
const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
|
|
8634
|
-
child.on("error", () =>
|
|
8683
|
+
child.on("error", () => resolve7(false));
|
|
8635
8684
|
child.on("spawn", () => {
|
|
8636
8685
|
child.unref();
|
|
8637
|
-
|
|
8686
|
+
resolve7(true);
|
|
8638
8687
|
});
|
|
8639
8688
|
} catch {
|
|
8640
|
-
|
|
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
|
|
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 &&
|
|
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(`${
|
|
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 =
|
|
9276
|
+
const risky = files2.filter((f) => f.risky).length;
|
|
9228
9277
|
ui2.out(
|
|
9229
|
-
`${ui2.c("ok", "done")}: ${
|
|
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
|
|
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,
|
|
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 ${
|
|
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
|
|
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 (
|
|
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,
|