airgate 0.1.5 → 0.1.6
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/README.md +18 -1
- package/dist/airgate.mjs +339 -39
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
⊠ airgate give your AI its own computer
|
|
5
5
|
```
|
|
6
6
|
|
|
7
|
-
A private cloud computer for Claude Code, Codex or any AI agent, ready in about two seconds
|
|
7
|
+
A private cloud computer for Claude Code, Codex or any AI agent, ready in about two seconds, and
|
|
8
|
+
crews of them that build, review and test one task together. It
|
|
8
9
|
can't touch your laptop, keeps working when you close it, and every step can be undone.
|
|
9
10
|
Always reachable, billed while working. [airgate.sh](https://airgate.sh)
|
|
10
11
|
|
|
@@ -28,6 +29,22 @@ airgate up # move the session you're in to a pod; airgate down brings the
|
|
|
28
29
|
airgate help # every command
|
|
29
30
|
```
|
|
30
31
|
|
|
32
|
+
## Crews
|
|
33
|
+
|
|
34
|
+
Several agents, each in its own pod, on one task: they build, review, test and argue it out, and
|
|
35
|
+
you get one patch back.
|
|
36
|
+
|
|
37
|
+
```sh
|
|
38
|
+
airgate crew contest "add sign-in with GitHub" # one builds, another reviews until it holds
|
|
39
|
+
airgate crew race "make search faster" # several try it, a judge picks the best
|
|
40
|
+
airgate crew council "pick a job queue" --build # plans, critiques, a decision, then the build
|
|
41
|
+
airgate crew squad "add CSV export" # testers write tests and hunt bugs
|
|
42
|
+
airgate crew split "add team billing" # parts in parallel, then merged
|
|
43
|
+
airgate crew down <id> # bring the result back here
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Guide: https://airgate.sh/docs/crews
|
|
47
|
+
|
|
31
48
|
## For your agent
|
|
32
49
|
|
|
33
50
|
Claude Code or Codex on your laptop can send work to pods on its own: one task in a fresh pod
|
package/dist/airgate.mjs
CHANGED
|
@@ -1984,7 +1984,7 @@ function borderOf(name, v) {
|
|
|
1984
1984
|
return { chars: chars3, title, color, pad: pad3 };
|
|
1985
1985
|
}
|
|
1986
1986
|
function box(s2, { x, y, cols: w, rows: h }, chars3, title, color) {
|
|
1987
|
-
const [tl, tr, bl, br, across2,
|
|
1987
|
+
const [tl, tr, bl, br, across2, down4] = Array.from(chars3, (ch) => ch.charCodeAt(0));
|
|
1988
1988
|
const set = (cx, cy, ch) => {
|
|
1989
1989
|
const i = s2.index(cx, cy);
|
|
1990
1990
|
if (i >= 0)
|
|
@@ -1993,7 +1993,7 @@ function box(s2, { x, y, cols: w, rows: h }, chars3, title, color) {
|
|
|
1993
1993
|
for (let c = 1; c < w - 1; c++)
|
|
1994
1994
|
set(x + c, y, across2), set(x + c, y + h - 1, across2);
|
|
1995
1995
|
for (let r = 1; r < h - 1; r++)
|
|
1996
|
-
set(x, y + r,
|
|
1996
|
+
set(x, y + r, down4), set(x + w - 1, y + r, down4);
|
|
1997
1997
|
set(x, y, tl), set(x + w - 1, y, tr), set(x, y + h - 1, bl), set(x + w - 1, y + h - 1, br);
|
|
1998
1998
|
if (title && w >= 7) {
|
|
1999
1999
|
const room = w - 6;
|
|
@@ -2984,10 +2984,10 @@ function outline(src, options) {
|
|
|
2984
2984
|
if (!line2(x, y))
|
|
2985
2985
|
continue;
|
|
2986
2986
|
const up2 = line2(x, y - 1) && (is(solid, x - 1, y) || is(solid, x - 1, y - 1) || is(solid, x + 1, y) || is(solid, x + 1, y - 1));
|
|
2987
|
-
const
|
|
2987
|
+
const down4 = line2(x, y + 1) && (is(solid, x - 1, y) || is(solid, x - 1, y + 1) || is(solid, x + 1, y) || is(solid, x + 1, y + 1));
|
|
2988
2988
|
const left = line2(x - 1, y) && (is(solid, x, y - 1) || is(solid, x - 1, y - 1) || is(solid, x, y + 1) || is(solid, x - 1, y + 1));
|
|
2989
2989
|
const right = line2(x + 1, y) && (is(solid, x, y - 1) || is(solid, x + 1, y - 1) || is(solid, x, y + 1) || is(solid, x + 1, y + 1));
|
|
2990
|
-
const bits = (up2 ? 1 : 0) | (
|
|
2990
|
+
const bits = (up2 ? 1 : 0) | (down4 ? 2 : 0) | (left ? 4 : 0) | (right ? 8 : 0);
|
|
2991
2991
|
if (!bits)
|
|
2992
2992
|
continue;
|
|
2993
2993
|
let c = ink;
|
|
@@ -3014,7 +3014,7 @@ function shadow(src, options) {
|
|
|
3014
3014
|
fail2(`shadow.solid takes true or false, not ${show(o.solid)}`);
|
|
3015
3015
|
const solid = o.solid ?? true;
|
|
3016
3016
|
return build2("shadow", src, o, ({ meta: m, inks }) => {
|
|
3017
|
-
const
|
|
3017
|
+
const tone2 = color ? [inks.add(color), inks.add(color)] : m.palette ? [inks.add(QUIET2.dark), inks.add(QUIET2.light)] : [NONE, NONE];
|
|
3018
3018
|
const cols = m.cols + Math.abs(dx), rows2 = m.rows + Math.abs(dy);
|
|
3019
3019
|
const x0 = dx < 0 ? -dx : 0, y0 = dy < 0 ? -dy : 0;
|
|
3020
3020
|
return {
|
|
@@ -3026,7 +3026,7 @@ function shadow(src, options) {
|
|
|
3026
3026
|
const ink = new Uint8Array(cols * rows2), out = new Uint8Array(cols * rows2).fill(1), queue = new Int32Array(cols * rows2);
|
|
3027
3027
|
const over2 = { mask: null };
|
|
3028
3028
|
return (t, s2, g, ctx) => {
|
|
3029
|
-
const c =
|
|
3029
|
+
const c = tone2[ctx.paper ? 1 : 0];
|
|
3030
3030
|
if (solid) {
|
|
3031
3031
|
ink.fill(0);
|
|
3032
3032
|
for (let y = 0, i = 0; y < g.rows; y++)
|
|
@@ -5757,9 +5757,9 @@ function colorOn(stream, env) {
|
|
|
5757
5757
|
}
|
|
5758
5758
|
var ANSI = /\u001b\[[0-9;]*m/g;
|
|
5759
5759
|
var visibleWidth = (s2) => [...s2.replace(ANSI, "")].length;
|
|
5760
|
-
function paint2(on,
|
|
5760
|
+
function paint2(on, tone2, s2) {
|
|
5761
5761
|
if (!on) return s2;
|
|
5762
|
-
const [a, b2] = CODES[
|
|
5762
|
+
const [a, b2] = CODES[tone2];
|
|
5763
5763
|
return `\x1B[${a}m${s2}\x1B[${b2}m`;
|
|
5764
5764
|
}
|
|
5765
5765
|
function formatTable(rows2, head, indent = 0) {
|
|
@@ -5776,16 +5776,16 @@ function paintArt(on, a) {
|
|
|
5776
5776
|
if (!on || !tones2) return line2.trimEnd();
|
|
5777
5777
|
let out = "";
|
|
5778
5778
|
let run2 = "";
|
|
5779
|
-
let
|
|
5779
|
+
let tone2;
|
|
5780
5780
|
const flush = () => {
|
|
5781
|
-
if (run2) out +=
|
|
5781
|
+
if (run2) out += tone2 ? paint2(true, tone2, run2) : run2;
|
|
5782
5782
|
run2 = "";
|
|
5783
5783
|
};
|
|
5784
5784
|
[...line2].forEach((ch, x) => {
|
|
5785
|
-
const t = ch === " " ?
|
|
5786
|
-
if (t !==
|
|
5785
|
+
const t = ch === " " ? tone2 : tones2[x];
|
|
5786
|
+
if (t !== tone2) {
|
|
5787
5787
|
flush();
|
|
5788
|
-
|
|
5788
|
+
tone2 = t;
|
|
5789
5789
|
}
|
|
5790
5790
|
run2 += ch;
|
|
5791
5791
|
});
|
|
@@ -5800,8 +5800,8 @@ function createUi(io) {
|
|
|
5800
5800
|
tty: Boolean(io.stdout.isTTY),
|
|
5801
5801
|
out: (line2 = "") => void io.stdout.write(line2 + "\n"),
|
|
5802
5802
|
err: (line2 = "") => void io.stderr.write(line2 + "\n"),
|
|
5803
|
-
c: (
|
|
5804
|
-
ce: (
|
|
5803
|
+
c: (tone2, s2) => paint2(outColor, tone2, s2),
|
|
5804
|
+
ce: (tone2, s2) => paint2(errColor, tone2, s2),
|
|
5805
5805
|
json: (v) => void io.stdout.write(JSON.stringify(v, null, 2) + "\n"),
|
|
5806
5806
|
table(rows2, opts2 = {}) {
|
|
5807
5807
|
const head = opts2.head ? opts2.head.map((h) => paint2(outColor, "dim", h)) : void 0;
|
|
@@ -7777,7 +7777,7 @@ var log = {
|
|
|
7777
7777
|
import { createInterface } from "node:readline";
|
|
7778
7778
|
|
|
7779
7779
|
// src/version.ts
|
|
7780
|
-
var VERSION = "0.1.
|
|
7780
|
+
var VERSION = "0.1.6";
|
|
7781
7781
|
|
|
7782
7782
|
// src/commands/updown.ts
|
|
7783
7783
|
import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
@@ -7829,7 +7829,7 @@ function gitFiles(dir) {
|
|
|
7829
7829
|
return out;
|
|
7830
7830
|
}
|
|
7831
7831
|
function runTar(cwd, args, input, limit = UPLOAD_LIMIT) {
|
|
7832
|
-
return new Promise((
|
|
7832
|
+
return new Promise((resolve8, reject) => {
|
|
7833
7833
|
const child = spawn("tar", args, { cwd, env: { ...process.env, COPYFILE_DISABLE: "1" }, stdio: ["pipe", "pipe", "pipe"] });
|
|
7834
7834
|
const chunks = [];
|
|
7835
7835
|
let size2 = 0;
|
|
@@ -7849,7 +7849,7 @@ function runTar(cwd, args, input, limit = UPLOAD_LIMIT) {
|
|
|
7849
7849
|
child.on("close", (code3) => {
|
|
7850
7850
|
if (over2) return reject(new TooLargeError(limit));
|
|
7851
7851
|
if (code3 !== 0) return reject(new Error(`tar failed: ${err.trim().split("\n").slice(-3).join(" ")}`));
|
|
7852
|
-
|
|
7852
|
+
resolve8(Buffer.concat(chunks));
|
|
7853
7853
|
});
|
|
7854
7854
|
child.stdin.on("error", () => void 0);
|
|
7855
7855
|
child.stdin.end(input ?? "");
|
|
@@ -8637,7 +8637,7 @@ var LineReader = class {
|
|
|
8637
8637
|
}
|
|
8638
8638
|
};
|
|
8639
8639
|
function ttyPrompt(stdin, out) {
|
|
8640
|
-
return (question, opts2 = {}) => new Promise((
|
|
8640
|
+
return (question, opts2 = {}) => new Promise((resolve8, reject) => {
|
|
8641
8641
|
out.write(question);
|
|
8642
8642
|
const decoder = new StringDecoder2("utf8");
|
|
8643
8643
|
let answer = "";
|
|
@@ -8650,7 +8650,7 @@ function ttyPrompt(stdin, out) {
|
|
|
8650
8650
|
stdin.pause();
|
|
8651
8651
|
out.write("\n");
|
|
8652
8652
|
if (err) reject(err);
|
|
8653
|
-
else
|
|
8653
|
+
else resolve8(answer);
|
|
8654
8654
|
};
|
|
8655
8655
|
const onData = (d) => {
|
|
8656
8656
|
for (const ch of decoder.write(d)) {
|
|
@@ -8677,16 +8677,16 @@ function ttyPrompt(stdin, out) {
|
|
|
8677
8677
|
}
|
|
8678
8678
|
function openInBrowser(url) {
|
|
8679
8679
|
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url]] : ["xdg-open", [url]];
|
|
8680
|
-
return new Promise((
|
|
8680
|
+
return new Promise((resolve8) => {
|
|
8681
8681
|
try {
|
|
8682
8682
|
const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
|
|
8683
|
-
child.on("error", () =>
|
|
8683
|
+
child.on("error", () => resolve8(false));
|
|
8684
8684
|
child.on("spawn", () => {
|
|
8685
8685
|
child.unref();
|
|
8686
|
-
|
|
8686
|
+
resolve8(true);
|
|
8687
8687
|
});
|
|
8688
8688
|
} catch {
|
|
8689
|
-
|
|
8689
|
+
resolve8(false);
|
|
8690
8690
|
}
|
|
8691
8691
|
});
|
|
8692
8692
|
}
|
|
@@ -9282,6 +9282,237 @@ When you are done, leave your changes in the files there (no need to commit or p
|
|
|
9282
9282
|
}
|
|
9283
9283
|
};
|
|
9284
9284
|
|
|
9285
|
+
// src/commands/crews.ts
|
|
9286
|
+
import { existsSync as existsSync6, readFileSync as readFileSync9, realpathSync as realpathSync3, rmSync as rmSync5, statSync as statSync4, writeFileSync as writeFileSync6 } from "node:fs";
|
|
9287
|
+
import { relative as relative3, resolve as resolve6 } from "node:path";
|
|
9288
|
+
var KINDS = ["race", "contest", "council", "squad", "split"];
|
|
9289
|
+
var ACTIONS = ["show", "say", "stop", "down", "rm", "list"];
|
|
9290
|
+
var AGENT_ALIAS = { claude: "claude-code", "claude-code": "claude-code", codex: "codex", opencode: "opencode", pi: "pi" };
|
|
9291
|
+
function membersFrom(values) {
|
|
9292
|
+
const items = values.flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
|
|
9293
|
+
if (!items.length) return void 0;
|
|
9294
|
+
return items.map((it) => {
|
|
9295
|
+
const m = /^([a-z][a-z0-9-]*)=([a-z-]+)(?::(.+))?$/.exec(it);
|
|
9296
|
+
if (!m) throw new UsageError(`--as ${it}: role=agent, e.g. reviewer=codex or worker-2=opencode:qwen/qwen3-coder`);
|
|
9297
|
+
const agent = AGENT_ALIAS[m[2]];
|
|
9298
|
+
if (!agent) throw new UsageError(`--as ${it}: the agent is one of claude, codex, opencode, pi`);
|
|
9299
|
+
return { role: m[1], agent, ...m[3] ? { model: m[3] } : {} };
|
|
9300
|
+
});
|
|
9301
|
+
}
|
|
9302
|
+
var tone = (ctx, state) => state === "done" ? ctx.ui.c("ok", state) : state === "failed" ? ctx.ui.c("bad", state) : state === "stopped" ? ctx.ui.c("dim", state) : ctx.ui.c("warn", state);
|
|
9303
|
+
function printRoom(ctx, messages) {
|
|
9304
|
+
for (const m of messages) {
|
|
9305
|
+
const who = m.author === "airgate" ? ctx.ui.c("dim", "airgate") : m.author === "you" ? ctx.ui.c("warn", "you") : m.author;
|
|
9306
|
+
const lines = m.text.split("\n");
|
|
9307
|
+
ctx.ui.out(`${who}: ${lines[0]}`);
|
|
9308
|
+
for (const l of lines.slice(1, 12)) ctx.ui.out(` ${l}`);
|
|
9309
|
+
if (lines.length > 12) ctx.ui.out(ctx.ui.c("dim", ` \u2026 (${lines.length - 12} more lines: airgate crew show)`));
|
|
9310
|
+
}
|
|
9311
|
+
}
|
|
9312
|
+
async function follow(ctx, id) {
|
|
9313
|
+
let since = 0;
|
|
9314
|
+
const controller = new AbortController();
|
|
9315
|
+
const stop = () => controller.abort();
|
|
9316
|
+
ctx.io.signals.on("SIGINT", stop);
|
|
9317
|
+
ctx.io.signal?.addEventListener("abort", stop);
|
|
9318
|
+
try {
|
|
9319
|
+
for (; ; ) {
|
|
9320
|
+
const r = await ctx.api.request("GET", `/v1/crews/${encodeURIComponent(id)}/messages?since=${since}`);
|
|
9321
|
+
printRoom(ctx, r.messages);
|
|
9322
|
+
if (r.messages.length) since = r.messages[r.messages.length - 1].id;
|
|
9323
|
+
if (["done", "failed", "stopped"].includes(r.state)) break;
|
|
9324
|
+
if (controller.signal.aborted) {
|
|
9325
|
+
ctx.ui.err(ctx.ui.ce("dim", `stopped following; the crew goes on. follow again: airgate crew show ${id} --follow \xB7 stop it: airgate crew stop ${id}`));
|
|
9326
|
+
break;
|
|
9327
|
+
}
|
|
9328
|
+
await new Promise((res) => setTimeout(res, Number(ctx.io.env.AIRGATE_CREW_POLL_MS) || 2e3));
|
|
9329
|
+
}
|
|
9330
|
+
} finally {
|
|
9331
|
+
ctx.io.signals.off("SIGINT", stop);
|
|
9332
|
+
ctx.io.signal?.removeEventListener("abort", stop);
|
|
9333
|
+
}
|
|
9334
|
+
return (await ctx.api.request("GET", `/v1/crews/${encodeURIComponent(id)}`)).crew;
|
|
9335
|
+
}
|
|
9336
|
+
function printResult(ctx, c) {
|
|
9337
|
+
if (c.state === "failed") return void ctx.ui.err(ctx.ui.ce("bad", `failed: ${c.error ?? "unknown"}. its pods are asleep with their work: airgate pods`));
|
|
9338
|
+
if (!c.result) return;
|
|
9339
|
+
ctx.ui.out();
|
|
9340
|
+
ctx.ui.out(`${tone(ctx, c.state)}: ${c.result.summary ?? ""}`);
|
|
9341
|
+
if (c.result.files?.length) ctx.ui.out(ctx.ui.c("dim", `${c.result.files.length} file(s): ${c.result.files.slice(0, 8).join(", ")}${c.result.files.length > 8 ? "\u2026" : ""}`));
|
|
9342
|
+
if (c.result.hasPatch) ctx.ui.out(`bring it back here: airgate crew down ${c.id}`);
|
|
9343
|
+
}
|
|
9344
|
+
async function down3(ctx, id) {
|
|
9345
|
+
const r = await ctx.api.request("GET", `/v1/crews/${encodeURIComponent(id)}/result`);
|
|
9346
|
+
const { blocks, outside } = selectProject(parsePatch(r.patch), r.dir);
|
|
9347
|
+
const refused = refusals(blocks);
|
|
9348
|
+
const patch = blocks.map((b2) => b2.text).join("");
|
|
9349
|
+
const files2 = orderFiles(blocks.map((b2) => b2.file));
|
|
9350
|
+
if (bool(ctx.args, "dry-run")) {
|
|
9351
|
+
if (ctx.json) return ctx.ui.json({ files: files2, outside, refused, patch });
|
|
9352
|
+
return void ctx.io.stdout.write(patch);
|
|
9353
|
+
}
|
|
9354
|
+
if (refused.length) throw new CliError(`refused: ${refused.map((x) => `${x.path} (${x.why})`).join(", ")}`, `nothing applied. look: airgate crew down ${id} --dry-run`);
|
|
9355
|
+
if (!blocks.length) return void ctx.ui.out("nothing to apply.");
|
|
9356
|
+
const top = gitTopLevel(ctx.io.cwd);
|
|
9357
|
+
if (!top) throw new CliError("crew down needs a git repo here", "git init && git add -A && git commit -m base");
|
|
9358
|
+
const rel = relative3(realpathSync3(top), realpathSync3(ctx.io.cwd));
|
|
9359
|
+
const args = ["--whitespace=nowarn", ...rel ? [`--directory=${rel}`] : [], "-"];
|
|
9360
|
+
const check = git(top, ["apply", "--check", ...args], patch);
|
|
9361
|
+
if (check.status !== 0) throw new CliError(`the patch doesn't apply here, nothing changed:
|
|
9362
|
+
${check.stderr.trim()}`, "commit or stash local edits to those files first");
|
|
9363
|
+
const risky = files2.filter((f) => f.risky).length;
|
|
9364
|
+
if (!ctx.json) for (const f of files2) ctx.ui.out(` ${f.risky ? ctx.ui.c("warn", "!") : " "} ${f.path}${f.risky ? ctx.ui.c("warn", ` ${f.reason ?? ""}`) : ""}`);
|
|
9365
|
+
if (!await confirm(ctx, `apply ${files2.length} file(s)${risky ? ` (${risky} risky)` : ""}?`)) throw new CliError("nothing applied");
|
|
9366
|
+
const applied = git(top, ["apply", ...args], patch);
|
|
9367
|
+
if (applied.status !== 0) throw new CliError(`git apply failed: ${applied.stderr.trim()}`);
|
|
9368
|
+
if (ctx.json) return ctx.ui.json({ applied: true, files: files2 });
|
|
9369
|
+
ctx.ui.out(`${ctx.ui.c("ok", "applied")} ${files2.length} file(s) from the crew. review: git status \xB7 git diff`);
|
|
9370
|
+
}
|
|
9371
|
+
var crew = {
|
|
9372
|
+
name: "crew",
|
|
9373
|
+
aliases: ["crews"],
|
|
9374
|
+
group: "pods",
|
|
9375
|
+
usage: '<race|contest|council|squad|split> "<task>" [dir] [--pod p | --from snapshot | --repo owner/name] [--as role=agent] \u2026 | show|say|stop|down|rm <id>',
|
|
9376
|
+
summary: "several agents, each in its own pod, on one task: race, contest, council, squad, split",
|
|
9377
|
+
example: 'airgate crew contest "add OAuth with GitHub"',
|
|
9378
|
+
notes: [
|
|
9379
|
+
"race: builders try the same task, a judge tests each and picks one. contest: a builder builds, a reviewer reviews and tries to break it, until it holds. council: members propose plans and critique each other, a chair decides (--build to build it). squad: a builder builds, testers write tests and hunt bugs, the builder fixes. split: a planner splits the work, workers build the parts, an integrator merges.",
|
|
9380
|
+
"starting point: this folder (sent up to a pod made for the crew, deleted after), --pod (an existing pod), --from (a snapshot) or --repo. members fork from it; each has its own pod.",
|
|
9381
|
+
"--as role=agent[:model] picks a member's agent: --as reviewer=codex --as builder-3=opencode:qwen/qwen3-coder (opencode with a model id uses airgate's built-in models).",
|
|
9382
|
+
"airgate runs the crew, so it keeps going with this laptop closed. this command follows the room until it ends (--detach to return at once). while it follows, this laptop signs the members' model calls if it has a sign-in (airgate models).",
|
|
9383
|
+
"the result is one patch: airgate crew down <id> applies it here like airgate down. --pr opens a pull request instead (it waits in your airlock; needs --repo or a pod with a repo)."
|
|
9384
|
+
],
|
|
9385
|
+
flags: {
|
|
9386
|
+
pod: { type: "string", value: "pod", help: "start from this pod (its workspace)" },
|
|
9387
|
+
from: { type: "string", value: "snapshot", help: "start from a saved snapshot (name or share code)" },
|
|
9388
|
+
repo: { type: "string", value: "owner/name", help: "start from a GitHub repo" },
|
|
9389
|
+
as: { type: "list", value: "role=agent", help: "a member's agent, e.g. reviewer=codex (repeatable)" },
|
|
9390
|
+
rounds: { type: "string", value: "n", help: "contest / squad rounds (1\u20135)" },
|
|
9391
|
+
minutes: { type: "string", value: "n", help: "time limit for the whole crew (default 60)" },
|
|
9392
|
+
build: { type: "boolean", help: "council: also build the decision" },
|
|
9393
|
+
pr: { type: "boolean", help: "open a pull request with the result (it waits in your airlock)" },
|
|
9394
|
+
delete: { type: "boolean", help: "delete member pods afterwards (default: they sleep)" },
|
|
9395
|
+
detach: { type: "boolean", short: "d", help: "start it and return; don't follow the room" },
|
|
9396
|
+
follow: { type: "boolean", short: "f", help: "show: keep following the room" },
|
|
9397
|
+
"dry-run": { type: "boolean", short: "n", help: "down: print the patch, change nothing" },
|
|
9398
|
+
yes: { type: "boolean", short: "y", help: "don't ask" }
|
|
9399
|
+
},
|
|
9400
|
+
async run(ctx) {
|
|
9401
|
+
const { ui: ui2 } = ctx;
|
|
9402
|
+
const [first, second, third, extra] = ctx.args.positionals;
|
|
9403
|
+
if (!first || first === "list") {
|
|
9404
|
+
const list2 = (await ctx.api.request("GET", "/v1/crews")).crews;
|
|
9405
|
+
if (ctx.json) return ui2.json(list2);
|
|
9406
|
+
if (!list2.length) return void ui2.out(ui2.c("dim", 'no crews yet. start one: airgate crew contest "<task>" (in a project folder)'));
|
|
9407
|
+
ui2.table(
|
|
9408
|
+
list2.map((c2) => [c2.id, c2.recipe, tone(ctx, c2.state), c2.task.length > 48 ? `${c2.task.slice(0, 47)}\u2026` : c2.task]),
|
|
9409
|
+
{ head: ["crew", "recipe", "", "task"] }
|
|
9410
|
+
);
|
|
9411
|
+
return;
|
|
9412
|
+
}
|
|
9413
|
+
if (ACTIONS.includes(first)) {
|
|
9414
|
+
if (!second) throw new UsageError(`airgate crew ${first} <id>`);
|
|
9415
|
+
const id = second;
|
|
9416
|
+
if (first === "show") {
|
|
9417
|
+
if (bool(ctx.args, "follow") && !ctx.json) return void printResult(ctx, await follow(ctx, id));
|
|
9418
|
+
const c2 = (await ctx.api.request("GET", `/v1/crews/${encodeURIComponent(id)}`)).crew;
|
|
9419
|
+
if (ctx.json) return ui2.json(c2);
|
|
9420
|
+
ui2.out(`${c2.id} \xB7 ${c2.recipe} \xB7 ${tone(ctx, c2.state)}${c2.step && !["done", "failed", "stopped"].includes(c2.state) ? ` \xB7 ${c2.step}` : ""}`);
|
|
9421
|
+
ui2.out(`task: ${c2.task}`);
|
|
9422
|
+
ui2.table(c2.members.map((m) => [m.role, AGENTS[m.agent]?.label ?? m.agent, m.pod ? `${m.pod.name} (${m.pod.state})` : ui2.c("dim", "not started")]), { head: ["member", "agent", "pod"] });
|
|
9423
|
+
ui2.out();
|
|
9424
|
+
printRoom(ctx, c2.messages ?? []);
|
|
9425
|
+
return void printResult(ctx, c2);
|
|
9426
|
+
}
|
|
9427
|
+
if (first === "say") {
|
|
9428
|
+
const text2 = [third, extra].filter(Boolean).join(" ").trim();
|
|
9429
|
+
if (!text2) throw new UsageError('airgate crew say <id> "what the crew should know"');
|
|
9430
|
+
await ctx.api.request("POST", `/v1/crews/${encodeURIComponent(id)}/messages`, { body: { text: text2 } });
|
|
9431
|
+
return ctx.json ? ui2.json({ ok: true }) : void ui2.out("said to the crew. members read the room between steps.");
|
|
9432
|
+
}
|
|
9433
|
+
if (first === "stop") {
|
|
9434
|
+
const c2 = (await ctx.api.request("POST", `/v1/crews/${encodeURIComponent(id)}/stop`)).crew;
|
|
9435
|
+
return ctx.json ? ui2.json(c2) : void ui2.out(`stopping ${c2.id} at its next step; its pods go to sleep with their work.`);
|
|
9436
|
+
}
|
|
9437
|
+
if (first === "down") return down3(ctx, id);
|
|
9438
|
+
if (first === "rm") {
|
|
9439
|
+
if (!await confirm(ctx, `delete crew ${id} and its member pods?`)) throw new CliError("nothing deleted");
|
|
9440
|
+
await ctx.api.request("DELETE", `/v1/crews/${encodeURIComponent(id)}`);
|
|
9441
|
+
return ctx.json ? ui2.json({ ok: true }) : void ui2.out(`deleted ${id}.`);
|
|
9442
|
+
}
|
|
9443
|
+
}
|
|
9444
|
+
if (!KINDS.includes(first)) throw new UsageError(`"${first}" isn't a recipe or an action`, `recipes: ${KINDS.join(", ")} \xB7 actions: ${ACTIONS.join(", ")}`);
|
|
9445
|
+
const kind = first;
|
|
9446
|
+
const task2 = (second ?? "").trim();
|
|
9447
|
+
if (!task2) throw new UsageError(`what should the crew do? airgate crew ${kind} "<task>"`);
|
|
9448
|
+
if (extra) throw new UsageError(`unexpected "${extra}" (put the task in quotes)`);
|
|
9449
|
+
const sources = ["pod", "from", "repo"].filter((k) => str(ctx.args, k));
|
|
9450
|
+
if (sources.length > 1) throw new UsageError("one starting point: --pod, --from or --repo");
|
|
9451
|
+
if (sources.length && third) throw new UsageError("a folder or --pod / --from / --repo, not both");
|
|
9452
|
+
let basePodId;
|
|
9453
|
+
let dir = "";
|
|
9454
|
+
let temporaryBase = false;
|
|
9455
|
+
let restoreState = null;
|
|
9456
|
+
if (str(ctx.args, "pod")) basePodId = (await resolvePod(ctx, str(ctx.args, "pod"))).id;
|
|
9457
|
+
else if (str(ctx.args, "from")) {
|
|
9458
|
+
const p = await ui2.spin("starting the base pod from the snapshot", async () => waitReady(ctx, await ctx.api.launchSnapshot(str(ctx.args, "from"), { name: `crew-base-${Date.now().toString(36).slice(-4)}` })));
|
|
9459
|
+
basePodId = p.id;
|
|
9460
|
+
temporaryBase = true;
|
|
9461
|
+
} else if (str(ctx.args, "repo")) {
|
|
9462
|
+
const repo = str(ctx.args, "repo");
|
|
9463
|
+
const look = await ctx.api.inspectRepo(repo).catch((e) => e instanceof ApiError && e.status === 404 ? null : Promise.reject(e));
|
|
9464
|
+
if (look && !look.reachable) throw new CliError(`airgate can't reach ${look.repo} (private, and not shared with Airgate)`, "connect GitHub: airgate github connect, or start from a local clone instead");
|
|
9465
|
+
const spec = { name: `crew-base-${Date.now().toString(36).slice(-4)}`, agent: "claude-code", size: "standard", repo: look?.repo ?? repo, masking: true, network: "allowlist", allow: DEFAULT_ALLOW, autoStopMinutes: null, autoCheckpoint: false };
|
|
9466
|
+
basePodId = (await ui2.spin(`starting the base pod on ${spec.repo}`, async () => waitReady(ctx, await ctx.api.launch(spec)))).id;
|
|
9467
|
+
temporaryBase = true;
|
|
9468
|
+
} else {
|
|
9469
|
+
const projectDir = resolve6(ctx.io.cwd, third ?? ".");
|
|
9470
|
+
if (!existsSync6(projectDir) || !statSync4(projectDir).isDirectory()) throw new CliError(`no directory ${projectDir}`);
|
|
9471
|
+
const sp = statePath(projectDir);
|
|
9472
|
+
const before = existsSync6(sp) ? readFileSync9(sp, "utf8") : null;
|
|
9473
|
+
restoreState = () => {
|
|
9474
|
+
try {
|
|
9475
|
+
if (before !== null) writeFileSync6(sp, before);
|
|
9476
|
+
else rmSync5(sp, { force: true });
|
|
9477
|
+
} catch {
|
|
9478
|
+
}
|
|
9479
|
+
};
|
|
9480
|
+
const sent = await ui2.spin(`sending ${podDirName(projectDir)} up for the crew`, runCommand(ctx, up, [projectDir, "--name", `crew-base-${Date.now().toString(36).slice(-4)}`, "--no-session", "--yes"], ctx.io.cwd));
|
|
9481
|
+
restoreState();
|
|
9482
|
+
basePodId = sent.pod.id;
|
|
9483
|
+
dir = sent.dir;
|
|
9484
|
+
temporaryBase = true;
|
|
9485
|
+
}
|
|
9486
|
+
const body = { kind, task: task2, basePodId, dir, temporaryBase, keep: bool(ctx.args, "delete") ? "delete" : "sleep", build: bool(ctx.args, "build"), pr: bool(ctx.args, "pr") };
|
|
9487
|
+
const members = membersFrom(ctx.args.flags.as ?? []);
|
|
9488
|
+
if (members) body.members = members;
|
|
9489
|
+
if (str(ctx.args, "rounds")) body.rounds = Number(str(ctx.args, "rounds"));
|
|
9490
|
+
if (str(ctx.args, "minutes")) body.maxMinutes = Number(str(ctx.args, "minutes"));
|
|
9491
|
+
let c;
|
|
9492
|
+
try {
|
|
9493
|
+
c = (await ctx.api.request("POST", "/v1/crews", { body })).crew;
|
|
9494
|
+
} catch (e) {
|
|
9495
|
+
if (temporaryBase) await ctx.api.destroy(basePodId).catch(() => void 0);
|
|
9496
|
+
throw e;
|
|
9497
|
+
}
|
|
9498
|
+
if (ctx.json || bool(ctx.args, "detach")) {
|
|
9499
|
+
if (ctx.json) return ui2.json(c);
|
|
9500
|
+
ui2.out(`${ui2.c("ok", "started")} ${c.id} (${c.recipe}): ${c.members.map((m) => `${m.role} ${AGENTS[m.agent]?.label ?? m.agent}`).join(" \xB7 ")}`);
|
|
9501
|
+
return void ui2.out(ui2.c("dim", `follow it: airgate crew show ${c.id} --follow \xB7 in the app: airgate.sh/app/crews`));
|
|
9502
|
+
}
|
|
9503
|
+
ui2.out(`${c.id} \xB7 ${c.recipe} \xB7 ${c.members.map((m) => `${m.role} ${AGENTS[m.agent]?.label ?? m.agent}`).join(" \xB7 ")}`);
|
|
9504
|
+
ui2.out(ui2.c("dim", `following the room (ctrl-c stops following; the crew goes on). in the app: airgate.sh/app/crews`));
|
|
9505
|
+
const relayer = relayIfSignedIn(ctx);
|
|
9506
|
+
try {
|
|
9507
|
+
const end = await follow(ctx, c.id);
|
|
9508
|
+
printResult(ctx, end);
|
|
9509
|
+
return end.state === "failed" ? 1 : 0;
|
|
9510
|
+
} finally {
|
|
9511
|
+
relayer?.stop();
|
|
9512
|
+
}
|
|
9513
|
+
}
|
|
9514
|
+
};
|
|
9515
|
+
|
|
9285
9516
|
// src/commands/mcp.ts
|
|
9286
9517
|
var PROTOCOL_VERSION = "2025-06-18";
|
|
9287
9518
|
var obj = (properties, required = []) => ({ type: "object", properties, required, additionalProperties: false });
|
|
@@ -9635,6 +9866,74 @@ var TOOLS = [
|
|
|
9635
9866
|
return { name: pod.spec.name, network: pod.spec.network, allow: pod.spec.allow ?? [], aiOff: Boolean(pod.spec.aiOff) };
|
|
9636
9867
|
}
|
|
9637
9868
|
},
|
|
9869
|
+
{
|
|
9870
|
+
name: "crew_start",
|
|
9871
|
+
description: "Start a crew: several agents, each in its own pod, on one task, run by airgate (it keeps going without you). kind: race (builders try the same task, a judge tests each and picks one), contest (a builder builds, a reviewer reviews and tries to break it, until it holds), council (members propose plans and critique each other, a chair decides; build: true also builds it), squad (a builder builds, testers write tests and hunt bugs, the builder fixes), split (a planner splits the work, workers build parts, an integrator merges). Starting point: the local project in dir (default), or pod, snapshot or repo. Returns at once with the crew id: follow with crew_status, bring the result back with crew_down. Tell the user it started and roughly what it costs (one pod per member while they work).",
|
|
9872
|
+
inputSchema: obj(
|
|
9873
|
+
{
|
|
9874
|
+
kind: s("race | contest | council | squad | split", { enum: ["race", "contest", "council", "squad", "split"] }),
|
|
9875
|
+
task: s("what the crew should do"),
|
|
9876
|
+
dir: s("the local project to start from (default: where the server started)"),
|
|
9877
|
+
pod: s("start from this existing pod instead"),
|
|
9878
|
+
snapshot: s("start from this saved snapshot instead"),
|
|
9879
|
+
repo: s("start from this GitHub repo instead (owner/name)"),
|
|
9880
|
+
members: { type: "array", items: { type: "string" }, description: 'role=agent[:model] overrides, e.g. ["reviewer=codex", "builder-3=opencode:qwen/qwen3-coder"]' },
|
|
9881
|
+
rounds: n("contest / squad rounds, 1\u20135"),
|
|
9882
|
+
maxMinutes: n("time limit for the whole crew (default 60)"),
|
|
9883
|
+
build: b("council: also build the decision"),
|
|
9884
|
+
pr: b("open a pull request with the result (it waits in the airlock)")
|
|
9885
|
+
},
|
|
9886
|
+
["kind", "task"]
|
|
9887
|
+
),
|
|
9888
|
+
async call(ctx, a) {
|
|
9889
|
+
const argv = [need(a, "kind"), need(a, "task"), "--detach"];
|
|
9890
|
+
const opt = (flag, v) => typeof v === "string" && v && argv.push(`--${flag}`, v);
|
|
9891
|
+
opt("pod", a.pod);
|
|
9892
|
+
opt("from", a.snapshot);
|
|
9893
|
+
opt("repo", a.repo);
|
|
9894
|
+
for (const m of Array.isArray(a.members) ? a.members : []) argv.push("--as", String(m));
|
|
9895
|
+
if (typeof a.rounds === "number") argv.push("--rounds", String(a.rounds));
|
|
9896
|
+
if (typeof a.maxMinutes === "number") argv.push("--minutes", String(a.maxMinutes));
|
|
9897
|
+
if (a.build === true) argv.push("--build");
|
|
9898
|
+
if (a.pr === true) argv.push("--pr");
|
|
9899
|
+
return runCommand(ctx, crew, argv, dirOf(ctx, a));
|
|
9900
|
+
}
|
|
9901
|
+
},
|
|
9902
|
+
{
|
|
9903
|
+
name: "crew_status",
|
|
9904
|
+
description: "A crew's state, members (and their pods), the room's messages after `since` (a message id), and the result once done.",
|
|
9905
|
+
inputSchema: obj({ crew: s("the crew id"), since: n("only messages after this id") }, ["crew"]),
|
|
9906
|
+
async call(ctx, a) {
|
|
9907
|
+
const id = encodeURIComponent(need(a, "crew"));
|
|
9908
|
+
const c = (await ctx.api.request("GET", `/v1/crews/${id}`)).crew;
|
|
9909
|
+
const since = typeof a.since === "number" ? a.since : 0;
|
|
9910
|
+
return { ...c, messages: (c.messages ?? []).filter((m) => m.id > since) };
|
|
9911
|
+
}
|
|
9912
|
+
},
|
|
9913
|
+
{
|
|
9914
|
+
name: "crew_say",
|
|
9915
|
+
description: "Post to a crew's room as the user (members read it between steps). Only what the user asked you to tell them.",
|
|
9916
|
+
inputSchema: obj({ crew: s("the crew id"), text: s("the message") }, ["crew", "text"]),
|
|
9917
|
+
async call(ctx, a) {
|
|
9918
|
+
return ctx.api.request("POST", `/v1/crews/${encodeURIComponent(need(a, "crew"))}/messages`, { body: { text: need(a, "text") } });
|
|
9919
|
+
}
|
|
9920
|
+
},
|
|
9921
|
+
{
|
|
9922
|
+
name: "crew_stop",
|
|
9923
|
+
description: "Stop a crew at its next step; its pods go to sleep with their work.",
|
|
9924
|
+
inputSchema: obj({ crew: s("the crew id") }, ["crew"]),
|
|
9925
|
+
async call(ctx, a) {
|
|
9926
|
+
return ctx.api.request("POST", `/v1/crews/${encodeURIComponent(need(a, "crew"))}/stop`);
|
|
9927
|
+
}
|
|
9928
|
+
},
|
|
9929
|
+
{
|
|
9930
|
+
name: "crew_down",
|
|
9931
|
+
description: "Bring a finished crew's result (one patch) into the local repo, with airgate down's checks (all or nothing; symlinks and paths outside refused). apply: false returns the patch without applying it.",
|
|
9932
|
+
inputSchema: obj({ crew: s("the crew id"), dir: s("the local project (default: where the server started)"), apply: b("false: just return the patch") }, ["crew"]),
|
|
9933
|
+
async call(ctx, a) {
|
|
9934
|
+
return runCommand(ctx, crew, ["down", need(a, "crew"), ...a.apply === false ? ["--dry-run"] : ["--yes"]], dirOf(ctx, a));
|
|
9935
|
+
}
|
|
9936
|
+
},
|
|
9638
9937
|
{
|
|
9639
9938
|
name: "pod_ports",
|
|
9640
9939
|
description: "What is listening in a pod (dev servers on ports 1024+) and its preview links.",
|
|
@@ -9678,7 +9977,7 @@ async function handle(ctx, msg) {
|
|
|
9678
9977
|
protocolVersion: asked,
|
|
9679
9978
|
capabilities: { tools: { listChanged: false } },
|
|
9680
9979
|
serverInfo: { name: "airgate", title: "Airgate", version: VERSION },
|
|
9681
|
-
instructions: "Airgate pods are private cloud computers for coding agents. pod_run does one task in a fresh pod and brings the result back; pod_up / pod_down move the local project and conversation there and back; task_start hands an existing pod a task (run_status / run_log follow it). Sleep pods you are done with; delete only pods you launched (or the user asked). Outbound actions wait in the airlock for the user: never approve on your own initiative."
|
|
9980
|
+
instructions: "Airgate pods are private cloud computers for coding agents; crews put several agents (each in its own pod) on one task: crew_start, crew_status, crew_down. pod_run does one task in a fresh pod and brings the result back; pod_up / pod_down move the local project and conversation there and back; task_start hands an existing pod a task (run_status / run_log follow it). Sleep pods you are done with; delete only pods you launched (or the user asked). Outbound actions wait in the airlock for the user: never approve on your own initiative."
|
|
9682
9981
|
});
|
|
9683
9982
|
}
|
|
9684
9983
|
case "ping":
|
|
@@ -9746,7 +10045,7 @@ var mcp = {
|
|
|
9746
10045
|
};
|
|
9747
10046
|
|
|
9748
10047
|
// src/commands/misc.ts
|
|
9749
|
-
import { mkdirSync as mkdirSync5, writeFileSync as
|
|
10048
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync7 } from "node:fs";
|
|
9750
10049
|
import { homedir as homedir3 } from "node:os";
|
|
9751
10050
|
import { join as join7 } from "node:path";
|
|
9752
10051
|
var PAGES = ["pods", "airlock", "usage", "settings", "mask", "chat", "notes", "data", "status"];
|
|
@@ -9827,7 +10126,7 @@ var skill = {
|
|
|
9827
10126
|
const base = bool(ctx.args, "project") ? join7(ctx.io.cwd, ".claude") : join7(ctx.io.env.HOME ?? homedir3(), ".claude");
|
|
9828
10127
|
const dir = join7(base, "skills", "airgate");
|
|
9829
10128
|
mkdirSync5(dir, { recursive: true });
|
|
9830
|
-
|
|
10129
|
+
writeFileSync7(join7(dir, "SKILL.md"), text2);
|
|
9831
10130
|
if (ctx.json) return ctx.ui.json({ installed: join7(dir, "SKILL.md") });
|
|
9832
10131
|
ctx.ui.out(`${ctx.ui.c("ok", "installed")} the airgate skill: ${join7(dir, "SKILL.md")}`);
|
|
9833
10132
|
ctx.ui.out(ctx.ui.c("dim", 'claude code picks it up in its next session. ask it to "move this to an airgate pod".'));
|
|
@@ -10441,8 +10740,8 @@ function modalKey(s2, m, key) {
|
|
|
10441
10740
|
}
|
|
10442
10741
|
}
|
|
10443
10742
|
}
|
|
10444
|
-
function toast(s2, text2,
|
|
10445
|
-
const t = { id: s2.nextToast, text: text2, tone, until: s2.now + TOAST_MS };
|
|
10743
|
+
function toast(s2, text2, tone2 = "ok") {
|
|
10744
|
+
const t = { id: s2.nextToast, text: text2, tone: tone2, until: s2.now + TOAST_MS };
|
|
10446
10745
|
return { ...s2, toasts: [...s2.toasts.slice(-2), t], nextToast: s2.nextToast + 1 };
|
|
10447
10746
|
}
|
|
10448
10747
|
function moveSel(s2, d) {
|
|
@@ -11427,25 +11726,25 @@ function startTui(d) {
|
|
|
11427
11726
|
`);
|
|
11428
11727
|
const { cols, rows: rows2 } = size2();
|
|
11429
11728
|
let note = `back from ${name}. the session keeps running.`;
|
|
11430
|
-
let
|
|
11729
|
+
let tone2 = "ok";
|
|
11431
11730
|
try {
|
|
11432
11731
|
const ws = d.websocket(d.api.terminalUrl(podId, cols, rows2));
|
|
11433
11732
|
const end = await attachSession({ stdin: d.stdin, stdout: d.stdout, ws, signals: d.signals });
|
|
11434
11733
|
if (end.reason === "exit") note = `${name}: session ended (exit ${end.code})`;
|
|
11435
11734
|
else if (end.reason === "error" || end.reason === "closed" && end.code !== 1e3) {
|
|
11436
11735
|
note = `${name}: terminal ${end.reason === "error" ? end.message : "closed"}`;
|
|
11437
|
-
|
|
11736
|
+
tone2 = "bad";
|
|
11438
11737
|
}
|
|
11439
11738
|
} catch (e) {
|
|
11440
11739
|
note = `couldn't attach: ${errText(e)}`;
|
|
11441
|
-
|
|
11740
|
+
tone2 = "bad";
|
|
11442
11741
|
}
|
|
11443
11742
|
if (finished) return;
|
|
11444
11743
|
suspended = false;
|
|
11445
11744
|
shown2 = [];
|
|
11446
11745
|
d.stdout.write(ENTER_ALT);
|
|
11447
11746
|
listenKeys(true);
|
|
11448
|
-
dispatch(
|
|
11747
|
+
dispatch(tone2 === "ok" ? { type: "done", text: note } : { type: "failed", text: note });
|
|
11449
11748
|
}
|
|
11450
11749
|
async function stream() {
|
|
11451
11750
|
for (let attempt = 0; !controller.signal.aborted; attempt++) {
|
|
@@ -11553,8 +11852,8 @@ var ui = {
|
|
|
11553
11852
|
};
|
|
11554
11853
|
|
|
11555
11854
|
// src/commands/workspace.ts
|
|
11556
|
-
import { writeFileSync as
|
|
11557
|
-
import { basename as basename3, resolve as
|
|
11855
|
+
import { writeFileSync as writeFileSync8, existsSync as existsSync7, statSync as statSync5 } from "node:fs";
|
|
11856
|
+
import { basename as basename3, resolve as resolve7 } from "node:path";
|
|
11558
11857
|
async function checkpointId(ctx, podId, ref) {
|
|
11559
11858
|
if (!ref || ref === "current") return void 0;
|
|
11560
11859
|
return resolveCheckpoint(await ctx.api.checkpoints(podId), ref).id;
|
|
@@ -11602,16 +11901,16 @@ var pull = {
|
|
|
11602
11901
|
const body = await ctx.ui.spin(`reading ${path}`, ctx.api.fileRaw(pod.id, path, at3));
|
|
11603
11902
|
const out = str(ctx.args, "out") ?? basename3(path);
|
|
11604
11903
|
if (out === "-") return void ctx.io.stdout.write(body);
|
|
11605
|
-
const dest =
|
|
11606
|
-
if (
|
|
11607
|
-
if (
|
|
11904
|
+
const dest = resolve7(ctx.io.cwd, out);
|
|
11905
|
+
if (existsSync7(dest) && !bool(ctx.args, "force")) {
|
|
11906
|
+
if (statSync5(dest).isDirectory()) return void save2(ctx, resolve7(dest, basename3(path)), body);
|
|
11608
11907
|
throw new CliError(`${out} exists here`, "pick another name with -o, or --force to overwrite");
|
|
11609
11908
|
}
|
|
11610
11909
|
save2(ctx, dest, body);
|
|
11611
11910
|
}
|
|
11612
11911
|
};
|
|
11613
11912
|
function save2(ctx, dest, body) {
|
|
11614
|
-
|
|
11913
|
+
writeFileSync8(dest, body);
|
|
11615
11914
|
if (ctx.json) return ctx.ui.json({ saved: dest, bytes: body.length });
|
|
11616
11915
|
ctx.ui.out(`${ctx.ui.c("ok", "saved")} ${dest} (${bytes(body.length)})`);
|
|
11617
11916
|
}
|
|
@@ -11773,6 +12072,7 @@ var COMMANDS = [
|
|
|
11773
12072
|
runTask,
|
|
11774
12073
|
models,
|
|
11775
12074
|
relay,
|
|
12075
|
+
crew,
|
|
11776
12076
|
files,
|
|
11777
12077
|
pull,
|
|
11778
12078
|
snapshot2,
|