airgate 0.1.5 → 0.1.7
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 +38 -1
- package/dist/airgate.mjs +1067 -115
- package/package.json +1 -1
package/dist/airgate.mjs
CHANGED
|
@@ -95,6 +95,26 @@ var PLANS = {
|
|
|
95
95
|
concurrentPods: 5,
|
|
96
96
|
blurb: "for agents that work every day.",
|
|
97
97
|
features: ["160 active hours included every month", "5 pods at once, 50 GB of storage", "lower rate after that", "priority support"]
|
|
98
|
+
},
|
|
99
|
+
team: {
|
|
100
|
+
id: "team",
|
|
101
|
+
name: "team",
|
|
102
|
+
monthlyUsd: 30,
|
|
103
|
+
includedHours: 160,
|
|
104
|
+
hourlyUsd: 0.12,
|
|
105
|
+
storageGb: 50,
|
|
106
|
+
storageUsdPerGb: 0.2,
|
|
107
|
+
concurrentPods: 5,
|
|
108
|
+
perSeat: true,
|
|
109
|
+
blurb: "for teams that share pods, environments and what their agents learned.",
|
|
110
|
+
features: [
|
|
111
|
+
"everything in pro, per seat",
|
|
112
|
+
"160 hours per seat, pooled across the team",
|
|
113
|
+
"shared pods, snapshots, secrets and sign-ins",
|
|
114
|
+
"a team library: never pay twice for the same work",
|
|
115
|
+
"crews from GitHub, airlock approvers, audit log",
|
|
116
|
+
"join by work email"
|
|
117
|
+
]
|
|
98
118
|
}
|
|
99
119
|
};
|
|
100
120
|
function estimate(plan2, hours2) {
|
|
@@ -273,15 +293,15 @@ function parseDotenv(input) {
|
|
|
273
293
|
value = rest.replace(/\s+#.*$/, "").trim();
|
|
274
294
|
}
|
|
275
295
|
let name = key;
|
|
276
|
-
let
|
|
296
|
+
let note2;
|
|
277
297
|
if (!SECRET_NAME_RE.test(key)) {
|
|
278
298
|
name = suggestSecretName(key);
|
|
279
|
-
|
|
299
|
+
note2 = name ? `saved as ${name}` : "not a usable name";
|
|
280
300
|
} else if (reservedSecretName(key)) {
|
|
281
301
|
name = null;
|
|
282
|
-
|
|
302
|
+
note2 = "reserved (the pod sets it)";
|
|
283
303
|
}
|
|
284
|
-
entries.push({ key, name, value, line: lineNo, ...
|
|
304
|
+
entries.push({ key, name, value, line: lineNo, ...note2 ? { note: note2 } : {} });
|
|
285
305
|
}
|
|
286
306
|
const values = {};
|
|
287
307
|
for (const e of entries) if (e.name) values[e.name] = e.value;
|
|
@@ -489,6 +509,10 @@ var Api = class {
|
|
|
489
509
|
pods() {
|
|
490
510
|
return this.request("GET", "/v1/pods").then((r) => r.pods);
|
|
491
511
|
}
|
|
512
|
+
/** your pods, and the ones teammates shared with your workspace */
|
|
513
|
+
podsWithTeam() {
|
|
514
|
+
return this.request("GET", "/v1/pods").then((r) => ({ pods: r.pods, team: r.team ?? [] }));
|
|
515
|
+
}
|
|
492
516
|
pod(id) {
|
|
493
517
|
return this.request("GET", `/v1/pods/${enc(id)}`).then((r) => r.pod);
|
|
494
518
|
}
|
|
@@ -672,6 +696,107 @@ var Api = class {
|
|
|
672
696
|
portal(returnUrl) {
|
|
673
697
|
return this.request("POST", "/v1/billing/portal", { body: { returnUrl } });
|
|
674
698
|
}
|
|
699
|
+
/* --- teams ------------------------------------------------------------------------- */
|
|
700
|
+
team() {
|
|
701
|
+
return this.request("GET", "/v1/workspace");
|
|
702
|
+
}
|
|
703
|
+
createTeam(name) {
|
|
704
|
+
return this.request("POST", "/v1/workspace", { body: { name } });
|
|
705
|
+
}
|
|
706
|
+
updateTeam(patch) {
|
|
707
|
+
return this.request("PATCH", "/v1/workspace", { body: patch });
|
|
708
|
+
}
|
|
709
|
+
deleteTeam() {
|
|
710
|
+
return this.request("DELETE", "/v1/workspace");
|
|
711
|
+
}
|
|
712
|
+
claimDomain(domain, autoJoin) {
|
|
713
|
+
return this.request("PUT", "/v1/workspace/domain", { body: { domain, ...autoJoin === void 0 ? {} : { autoJoin } } });
|
|
714
|
+
}
|
|
715
|
+
setMember(id, patch) {
|
|
716
|
+
return this.request("PATCH", `/v1/workspace/members/${enc(id)}`, { body: patch });
|
|
717
|
+
}
|
|
718
|
+
removeMember(id) {
|
|
719
|
+
return this.request("DELETE", `/v1/workspace/members/${enc(id)}`);
|
|
720
|
+
}
|
|
721
|
+
leaveTeam() {
|
|
722
|
+
return this.request("POST", "/v1/workspace/leave", { body: {} });
|
|
723
|
+
}
|
|
724
|
+
teamInvites() {
|
|
725
|
+
return this.request("GET", "/v1/workspace/invites").then((r) => r.invites);
|
|
726
|
+
}
|
|
727
|
+
invite(email, role) {
|
|
728
|
+
return this.request("POST", "/v1/workspace/invites", { body: { ...email ? { email } : {}, role } });
|
|
729
|
+
}
|
|
730
|
+
revokeInvite(code3) {
|
|
731
|
+
return this.request("DELETE", `/v1/workspace/invites/${enc(code3)}`);
|
|
732
|
+
}
|
|
733
|
+
peekInvite(code3) {
|
|
734
|
+
return this.request("GET", `/v1/workspace/join/${enc(code3)}`);
|
|
735
|
+
}
|
|
736
|
+
joinTeam(code3) {
|
|
737
|
+
return this.request("POST", "/v1/workspace/join", { body: { code: code3 } });
|
|
738
|
+
}
|
|
739
|
+
requestJoin() {
|
|
740
|
+
return this.request("POST", "/v1/workspace/requests", { body: {} });
|
|
741
|
+
}
|
|
742
|
+
decideRequest(id, accept) {
|
|
743
|
+
return this.request("POST", `/v1/workspace/requests/${enc(id)}/${accept ? "accept" : "decline"}`, { body: {} });
|
|
744
|
+
}
|
|
745
|
+
audit(before) {
|
|
746
|
+
return this.request("GET", `/v1/workspace/audit${before ? `?before=${before}` : ""}`).then((r) => r.entries);
|
|
747
|
+
}
|
|
748
|
+
auditCsv() {
|
|
749
|
+
return this.request("GET", "/v1/workspace/audit?format=csv", { text: true });
|
|
750
|
+
}
|
|
751
|
+
teamBilling() {
|
|
752
|
+
return this.request("GET", "/v1/workspace/billing");
|
|
753
|
+
}
|
|
754
|
+
teamCheckout(seats, returnUrl) {
|
|
755
|
+
return this.request("POST", "/v1/workspace/billing/checkout", { body: { seats, returnUrl } });
|
|
756
|
+
}
|
|
757
|
+
setSeats(seats) {
|
|
758
|
+
return this.request("POST", "/v1/workspace/billing/seats", { body: { seats } });
|
|
759
|
+
}
|
|
760
|
+
teamSecrets() {
|
|
761
|
+
return this.request("GET", "/v1/workspace/secrets").then((r) => r.secrets);
|
|
762
|
+
}
|
|
763
|
+
setTeamSecrets(values) {
|
|
764
|
+
return this.request("PUT", "/v1/workspace/secrets", { body: { values } });
|
|
765
|
+
}
|
|
766
|
+
deleteTeamSecret(name) {
|
|
767
|
+
return this.request("DELETE", `/v1/workspace/secrets/${enc(name)}`);
|
|
768
|
+
}
|
|
769
|
+
sharePod(id, shared) {
|
|
770
|
+
return this.request("PUT", `/v1/pods/${enc(id)}/share`, { body: { shared } }).then((r) => r.pod);
|
|
771
|
+
}
|
|
772
|
+
handover(id, to) {
|
|
773
|
+
return this.request("POST", `/v1/pods/${enc(id)}/handover`, { body: { to } });
|
|
774
|
+
}
|
|
775
|
+
library(q = "") {
|
|
776
|
+
return this.request("GET", `/v1/library${q.trim() ? `?q=${encodeURIComponent(q.trim())}` : ""}`).then((r) => r.entries);
|
|
777
|
+
}
|
|
778
|
+
/** the same work already done in your workspace (empty outside one) */
|
|
779
|
+
similar(task2) {
|
|
780
|
+
return this.request("POST", "/v1/library/similar", { body: { task: task2 } }).then((r) => r.similar);
|
|
781
|
+
}
|
|
782
|
+
libraryPatch(id) {
|
|
783
|
+
return this.request("GET", `/v1/library/${enc(id)}/patch`);
|
|
784
|
+
}
|
|
785
|
+
note(text2) {
|
|
786
|
+
return this.request("POST", "/v1/library/notes", { body: { text: text2 } });
|
|
787
|
+
}
|
|
788
|
+
triggers() {
|
|
789
|
+
return this.request("GET", "/v1/github/triggers").then((r) => r.triggers);
|
|
790
|
+
}
|
|
791
|
+
addTrigger(body) {
|
|
792
|
+
return this.request("POST", "/v1/github/triggers", { body }).then((r) => r.trigger);
|
|
793
|
+
}
|
|
794
|
+
removeTrigger(id) {
|
|
795
|
+
return this.request("DELETE", `/v1/github/triggers/${enc(id)}`);
|
|
796
|
+
}
|
|
797
|
+
sendVerification() {
|
|
798
|
+
return this.request("POST", "/v1/auth/verify/send", { body: {} });
|
|
799
|
+
}
|
|
675
800
|
terminalUrl(id, cols, rows2) {
|
|
676
801
|
const q = new URLSearchParams({ token: this.tokenForUrl(), cols: String(cols), rows: String(rows2), client: this.client });
|
|
677
802
|
return `${this.gateway.replace(/^http/, "ws")}/v1/pods/${enc(id)}/terminal?${q}`;
|
|
@@ -1956,9 +2081,9 @@ function stack(fn, all, given = {}) {
|
|
|
1956
2081
|
});
|
|
1957
2082
|
}
|
|
1958
2083
|
function over(top, bottom, o = {}) {
|
|
1959
|
-
const { name, note, ...opts2 } = object(o, "over()", [...LAYER.filter((k) => k !== "src"), "name", "note"]);
|
|
2084
|
+
const { name, note: note2, ...opts2 } = object(o, "over()", [...LAYER.filter((k) => k !== "src"), "name", "note"]);
|
|
1960
2085
|
const upper = isClip(top) ? { ...opts2, ...top } : { ...opts2, src: top };
|
|
1961
|
-
return stack("over", [layerOf("over", bottom, "top-left"), layerOf("over", upper, "center")], naming("over", { name, note }));
|
|
2086
|
+
return stack("over", [layerOf("over", bottom, "top-left"), layerOf("over", upper, "center")], naming("over", { name, note: note2 }));
|
|
1962
2087
|
}
|
|
1963
2088
|
var BOXES = { single: "\u250C\u2510\u2514\u2518\u2500\u2502", double: "\u2554\u2557\u255A\u255D\u2550\u2551", rounded: "\u256D\u256E\u2570\u256F\u2500\u2502", heavy: "\u250F\u2513\u2517\u251B\u2501\u2503", ascii: "++++-|" };
|
|
1964
2089
|
function borderOf(name, v) {
|
|
@@ -1984,7 +2109,7 @@ function borderOf(name, v) {
|
|
|
1984
2109
|
return { chars: chars3, title, color, pad: pad3 };
|
|
1985
2110
|
}
|
|
1986
2111
|
function box(s2, { x, y, cols: w, rows: h }, chars3, title, color) {
|
|
1987
|
-
const [tl, tr, bl, br, across2,
|
|
2112
|
+
const [tl, tr, bl, br, across2, down4] = Array.from(chars3, (ch) => ch.charCodeAt(0));
|
|
1988
2113
|
const set = (cx, cy, ch) => {
|
|
1989
2114
|
const i = s2.index(cx, cy);
|
|
1990
2115
|
if (i >= 0)
|
|
@@ -1993,7 +2118,7 @@ function box(s2, { x, y, cols: w, rows: h }, chars3, title, color) {
|
|
|
1993
2118
|
for (let c = 1; c < w - 1; c++)
|
|
1994
2119
|
set(x + c, y, across2), set(x + c, y + h - 1, across2);
|
|
1995
2120
|
for (let r = 1; r < h - 1; r++)
|
|
1996
|
-
set(x, y + r,
|
|
2121
|
+
set(x, y + r, down4), set(x + w - 1, y + r, down4);
|
|
1997
2122
|
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
2123
|
if (title && w >= 7) {
|
|
1999
2124
|
const room = w - 6;
|
|
@@ -2382,11 +2507,11 @@ function build2(fx, src, o, make, keep = true) {
|
|
|
2382
2507
|
const own = periodOf(p, options ?? {});
|
|
2383
2508
|
const loop = !e.moves ? own : e.period ? lcm(own ?? e.period, e.period) : void 0;
|
|
2384
2509
|
const name = o.name === void 0 ? m.name : typeof o.name === "string" ? o.name : fail2(`${fx}.name takes a string, not ${show(o.name)}`);
|
|
2385
|
-
const
|
|
2510
|
+
const note2 = o.note ?? `${name}, ${e.what}`.slice(0, 72);
|
|
2386
2511
|
const still = e.still ?? m.still;
|
|
2387
2512
|
const out = piece({
|
|
2388
2513
|
name,
|
|
2389
|
-
note,
|
|
2514
|
+
note: note2,
|
|
2390
2515
|
category: m.category,
|
|
2391
2516
|
cols,
|
|
2392
2517
|
rows: rows2,
|
|
@@ -2984,10 +3109,10 @@ function outline(src, options) {
|
|
|
2984
3109
|
if (!line2(x, y))
|
|
2985
3110
|
continue;
|
|
2986
3111
|
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
|
|
3112
|
+
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
3113
|
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
3114
|
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) | (
|
|
3115
|
+
const bits = (up2 ? 1 : 0) | (down4 ? 2 : 0) | (left ? 4 : 0) | (right ? 8 : 0);
|
|
2991
3116
|
if (!bits)
|
|
2992
3117
|
continue;
|
|
2993
3118
|
let c = ink;
|
|
@@ -3014,7 +3139,7 @@ function shadow(src, options) {
|
|
|
3014
3139
|
fail2(`shadow.solid takes true or false, not ${show(o.solid)}`);
|
|
3015
3140
|
const solid = o.solid ?? true;
|
|
3016
3141
|
return build2("shadow", src, o, ({ meta: m, inks }) => {
|
|
3017
|
-
const
|
|
3142
|
+
const tone2 = color ? [inks.add(color), inks.add(color)] : m.palette ? [inks.add(QUIET2.dark), inks.add(QUIET2.light)] : [NONE, NONE];
|
|
3018
3143
|
const cols = m.cols + Math.abs(dx), rows2 = m.rows + Math.abs(dy);
|
|
3019
3144
|
const x0 = dx < 0 ? -dx : 0, y0 = dy < 0 ? -dy : 0;
|
|
3020
3145
|
return {
|
|
@@ -3026,7 +3151,7 @@ function shadow(src, options) {
|
|
|
3026
3151
|
const ink = new Uint8Array(cols * rows2), out = new Uint8Array(cols * rows2).fill(1), queue = new Int32Array(cols * rows2);
|
|
3027
3152
|
const over2 = { mask: null };
|
|
3028
3153
|
return (t, s2, g, ctx) => {
|
|
3029
|
-
const c =
|
|
3154
|
+
const c = tone2[ctx.paper ? 1 : 0];
|
|
3030
3155
|
if (solid) {
|
|
3031
3156
|
ink.fill(0);
|
|
3032
3157
|
for (let y = 0, i = 0; y < g.rows; y++)
|
|
@@ -3689,11 +3814,11 @@ function checkMeta(m) {
|
|
|
3689
3814
|
return m;
|
|
3690
3815
|
}
|
|
3691
3816
|
var Chainable = class {
|
|
3692
|
-
named(name,
|
|
3693
|
-
return named(this, name,
|
|
3817
|
+
named(name, note2) {
|
|
3818
|
+
return named(this, name, note2 === void 0 ? {} : { note: note2 });
|
|
3694
3819
|
}
|
|
3695
|
-
note(
|
|
3696
|
-
return named(this, this.meta.name, { note });
|
|
3820
|
+
note(note2) {
|
|
3821
|
+
return named(this, this.meta.name, { note: note2 });
|
|
3697
3822
|
}
|
|
3698
3823
|
over(bottom, o) {
|
|
3699
3824
|
return over(this, bottom, o);
|
|
@@ -5050,7 +5175,7 @@ function tones(accent, more = []) {
|
|
|
5050
5175
|
return { light: list2.map((c) => c.light), dark: list2.map((c) => c.dark) };
|
|
5051
5176
|
}
|
|
5052
5177
|
var WIDGET = ["color", ...NAMING];
|
|
5053
|
-
var naming2 = (o, name,
|
|
5178
|
+
var naming2 = (o, name, note2) => ({ name: o.name ?? name, note: (o.note ?? note2).slice(0, 72) });
|
|
5054
5179
|
var framing = (p, framed) => framed ? Object.assign(p, { frames: true }) : p;
|
|
5055
5180
|
function amountOf(what, v, min, max, share2) {
|
|
5056
5181
|
if (v === void 0)
|
|
@@ -5094,8 +5219,8 @@ function progressBar(o) {
|
|
|
5094
5219
|
const cols = label2.length + width + (percent ? 5 : 0);
|
|
5095
5220
|
const share2 = (v) => (v - min) / (max - min);
|
|
5096
5221
|
const timing = live ? { fps: 4, clock: true } : value === void 0 ? { fps: 24, loop, still: fill } : { fps: 0 };
|
|
5097
|
-
const
|
|
5098
|
-
return piece({ ...naming2(p, label2.trim() || "progress",
|
|
5222
|
+
const note2 = live ? "a progress bar, live" : value === void 0 ? "a progress bar filling" : `a progress bar at ${Math.round(share2(value) * 100)}%`;
|
|
5223
|
+
return piece({ ...naming2(p, label2.trim() || "progress", note2), category: "ui", cols, rows: 1, ...timing, palette: tones(themed2("progressBar's color", p.color)) }, (t, s2) => {
|
|
5099
5224
|
const k = typeof value === "function" ? share2(value()) : value !== void 0 ? share2(value) : smooth(Math.min(1, (t % loop + loop) % loop / fill));
|
|
5100
5225
|
const color = k >= 1 ? GOOD : ACCENT;
|
|
5101
5226
|
s2.write(0, 0, label2);
|
|
@@ -5134,8 +5259,8 @@ function gauge(o) {
|
|
|
5134
5259
|
const loop = 12;
|
|
5135
5260
|
const wander = (t) => 0.15 + 0.8 * loopNoise(0.3, 0.7, t, loop, { radius: 0.9 });
|
|
5136
5261
|
const timing = typeof value === "function" ? { fps: 4, clock: true } : value === void 0 ? { fps: 24, loop } : { fps: 0 };
|
|
5137
|
-
const
|
|
5138
|
-
const made = piece({ ...naming2(p, title || "gauge",
|
|
5262
|
+
const note2 = typeof value === "function" ? "a gauge, live" : value === void 0 ? "a gauge, its reading rising and falling" : `a gauge reading ${+value.toFixed(2)}${unit2}`;
|
|
5263
|
+
const made = piece({ ...naming2(p, title || "gauge", note2), category: "data", cols: 25, rows: 12, ...timing, palette: tones(own) }, (t, s2) => {
|
|
5139
5264
|
const k = typeof value === "function" ? (value() - min) / (max - min) : value === void 0 ? wander(t) : (value - min) / (max - min);
|
|
5140
5265
|
const color = own ? ACCENT : k > 0.85 ? BAD2 : k > 0.6 ? WARN : GOOD;
|
|
5141
5266
|
if (framed)
|
|
@@ -5174,8 +5299,8 @@ function sparkline(data, o) {
|
|
|
5174
5299
|
const last = list2 ? String(+list2[list2.length - 1].toFixed(2)) : live ? "100000" : "100";
|
|
5175
5300
|
const cols = words2.length + width + 1 + last.length;
|
|
5176
5301
|
const timing = live ? { fps: 4, clock: true } : { fps: 24, loop, ...list2 ? { still: 1.5 } : {} };
|
|
5177
|
-
const
|
|
5178
|
-
return piece({ ...naming2(p, label2 || "sparkline",
|
|
5302
|
+
const note2 = live ? "a sparkline of live data" : list2 ? "a sparkline drawing itself in" : "a sparkline of a wandering reading";
|
|
5303
|
+
return piece({ ...naming2(p, label2 || "sparkline", note2), category: "data", cols, rows: height, ...timing, palette: tones(themed2("sparkline's color", p.color)) }, (t, s2) => {
|
|
5179
5304
|
const u = (t % loop + loop) % loop;
|
|
5180
5305
|
textAt(s2, words2, "left");
|
|
5181
5306
|
const area2 = { x: words2.length, y: 0, cols: width, rows: height };
|
|
@@ -5717,10 +5842,19 @@ function matchOne(items, ref, keys, what, label2) {
|
|
|
5717
5842
|
if (pre.length > 1) throw new CliError(`"${ref}" matches ${pre.length} ${what}s`, `be more specific: ${pre.map(label2).join(", ")}`);
|
|
5718
5843
|
throw new CliError(`no ${what} "${ref}"`, what === "pod" ? "list them with: airgate pods" : void 0);
|
|
5719
5844
|
}
|
|
5720
|
-
async function resolvePod(ctx, ref) {
|
|
5845
|
+
async function resolvePod(ctx, ref, opts2 = {}) {
|
|
5721
5846
|
if (!ref) throw new UsageError("which pod? give its name or id");
|
|
5722
|
-
|
|
5723
|
-
|
|
5847
|
+
if (!opts2.team) {
|
|
5848
|
+
const pods3 = await ctx.api.pods();
|
|
5849
|
+
return matchOne(pods3, ref, (p) => [p.id, p.spec.name], "pod", (p) => `${p.spec.name} (${p.id})`);
|
|
5850
|
+
}
|
|
5851
|
+
const { pods: pods2, team: team2 } = await ctx.api.podsWithTeam();
|
|
5852
|
+
try {
|
|
5853
|
+
return matchOne(pods2, ref, (p) => [p.id, p.spec.name], "pod", (p) => `${p.spec.name} (${p.id})`);
|
|
5854
|
+
} catch (e) {
|
|
5855
|
+
if (!team2.length || !(e instanceof CliError) || !e.message.startsWith("no pod")) throw e;
|
|
5856
|
+
return matchOne(team2, ref, (p) => [p.id, p.spec.name], "pod", (p) => `${p.spec.name} (${p.id}, ${p.owner?.email ?? "a teammate"}'s)`);
|
|
5857
|
+
}
|
|
5724
5858
|
}
|
|
5725
5859
|
function resolveCheckpoint(cps, ref) {
|
|
5726
5860
|
if (/^#\d+$/.test(ref)) {
|
|
@@ -5757,9 +5891,9 @@ function colorOn(stream, env) {
|
|
|
5757
5891
|
}
|
|
5758
5892
|
var ANSI = /\u001b\[[0-9;]*m/g;
|
|
5759
5893
|
var visibleWidth = (s2) => [...s2.replace(ANSI, "")].length;
|
|
5760
|
-
function paint2(on,
|
|
5894
|
+
function paint2(on, tone2, s2) {
|
|
5761
5895
|
if (!on) return s2;
|
|
5762
|
-
const [a, b2] = CODES[
|
|
5896
|
+
const [a, b2] = CODES[tone2];
|
|
5763
5897
|
return `\x1B[${a}m${s2}\x1B[${b2}m`;
|
|
5764
5898
|
}
|
|
5765
5899
|
function formatTable(rows2, head, indent = 0) {
|
|
@@ -5776,16 +5910,16 @@ function paintArt(on, a) {
|
|
|
5776
5910
|
if (!on || !tones2) return line2.trimEnd();
|
|
5777
5911
|
let out = "";
|
|
5778
5912
|
let run2 = "";
|
|
5779
|
-
let
|
|
5913
|
+
let tone2;
|
|
5780
5914
|
const flush = () => {
|
|
5781
|
-
if (run2) out +=
|
|
5915
|
+
if (run2) out += tone2 ? paint2(true, tone2, run2) : run2;
|
|
5782
5916
|
run2 = "";
|
|
5783
5917
|
};
|
|
5784
5918
|
[...line2].forEach((ch, x) => {
|
|
5785
|
-
const t = ch === " " ?
|
|
5786
|
-
if (t !==
|
|
5919
|
+
const t = ch === " " ? tone2 : tones2[x];
|
|
5920
|
+
if (t !== tone2) {
|
|
5787
5921
|
flush();
|
|
5788
|
-
|
|
5922
|
+
tone2 = t;
|
|
5789
5923
|
}
|
|
5790
5924
|
run2 += ch;
|
|
5791
5925
|
});
|
|
@@ -5800,8 +5934,8 @@ function createUi(io) {
|
|
|
5800
5934
|
tty: Boolean(io.stdout.isTTY),
|
|
5801
5935
|
out: (line2 = "") => void io.stdout.write(line2 + "\n"),
|
|
5802
5936
|
err: (line2 = "") => void io.stderr.write(line2 + "\n"),
|
|
5803
|
-
c: (
|
|
5804
|
-
ce: (
|
|
5937
|
+
c: (tone2, s2) => paint2(outColor, tone2, s2),
|
|
5938
|
+
ce: (tone2, s2) => paint2(errColor, tone2, s2),
|
|
5805
5939
|
json: (v) => void io.stdout.write(JSON.stringify(v, null, 2) + "\n"),
|
|
5806
5940
|
table(rows2, opts2 = {}) {
|
|
5807
5941
|
const head = opts2.head ? opts2.head.map((h) => paint2(outColor, "dim", h)) : void 0;
|
|
@@ -6755,8 +6889,8 @@ function approvalRows(ctx, items, names) {
|
|
|
6755
6889
|
return items.map((a) => [
|
|
6756
6890
|
ctx.ui.c(a.state === "pending" ? "warn" : a.state === "denied" || a.state === "failed" ? "bad" : "ok", a.state),
|
|
6757
6891
|
a.kind,
|
|
6758
|
-
names.get(a.podId) ?? a.podId,
|
|
6759
|
-
a.summary,
|
|
6892
|
+
names.get(a.podId) ?? (a.requestedBy ? `${a.requestedBy}'s pod` : a.podId),
|
|
6893
|
+
a.summary + (a.requestedBy ? ctx.ui.c("dim", ` \xB7 from ${a.requestedBy}`) : "") + (a.needsApprover && a.state === "pending" && !a.requestedBy ? ctx.ui.c("warn", " \xB7 waits for an approver") : ""),
|
|
6760
6894
|
ago(a.createdAt),
|
|
6761
6895
|
ctx.ui.c("dim", a.id)
|
|
6762
6896
|
]);
|
|
@@ -7003,7 +7137,7 @@ var attach = {
|
|
|
7003
7137
|
if (!io.stdin.isTTY || !io.stdout.isTTY) {
|
|
7004
7138
|
throw new CliError("attach needs an interactive terminal", "for scripts use: airgate exec <pod> -- <command\u2026>", 2);
|
|
7005
7139
|
}
|
|
7006
|
-
const p = await resolvePod(ctx, ctx.args.positionals[0]);
|
|
7140
|
+
const p = await resolvePod(ctx, ctx.args.positionals[0], { team: true });
|
|
7007
7141
|
const url = ctx.api.terminalUrl(p.id, io.stdout.columns ?? 80, io.stdout.rows ?? 24);
|
|
7008
7142
|
ui2.err(ui2.ce("dim", `attached to ${p.spec.name}. ctrl-] to detach.`));
|
|
7009
7143
|
let ws;
|
|
@@ -7242,6 +7376,427 @@ var secrets = {
|
|
|
7242
7376
|
}
|
|
7243
7377
|
};
|
|
7244
7378
|
|
|
7379
|
+
// src/commands/team.ts
|
|
7380
|
+
import { spawnSync } from "node:child_process";
|
|
7381
|
+
import { rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
7382
|
+
import { resolve as resolve4 } from "node:path";
|
|
7383
|
+
async function checkSimilar(ctx, task2) {
|
|
7384
|
+
const similar = await ctx.api.similar(task2).catch(() => []);
|
|
7385
|
+
if (!similar.length || ctx.json) return { go: true, similar };
|
|
7386
|
+
const { ui: ui2 } = ctx;
|
|
7387
|
+
for (const e of similar) {
|
|
7388
|
+
ui2.err(`${ui2.ce("warn", "a teammate already did this")}: ${e.title} ${ui2.ce("dim", `by ${e.author}, ${ago(e.at)}${e.cost?.tokens ? ` \xB7 ${e.cost.tokens} tokens` : ""}`)}`);
|
|
7389
|
+
ui2.err(ui2.ce("dim", ` reuse it: airgate recall --patch ${e.id}${e.summary ? ` \xB7 ${e.summary.split("\n")[0].slice(0, 100)}` : ""}`));
|
|
7390
|
+
}
|
|
7391
|
+
if (ctx.args.flags.yes === true || !ctx.io.stdin.isTTY) return { go: true, similar };
|
|
7392
|
+
const a = await ctx.io.prompt("start anyway? [y/N] ").catch(() => "y");
|
|
7393
|
+
return { go: /^y(es)?$/i.test(a.trim()), similar };
|
|
7394
|
+
}
|
|
7395
|
+
function notStarted(similar) {
|
|
7396
|
+
throw new CliError("not started", similar[0] ? `reuse it: airgate recall --patch ${similar[0].id} --apply` : void 0);
|
|
7397
|
+
}
|
|
7398
|
+
async function mine(ctx) {
|
|
7399
|
+
const v = await ctx.api.team();
|
|
7400
|
+
if (!v.workspace) throw new CliError("you're not in a team workspace", 'start one: airgate team create "<name>" \xB7 or join: airgate team join <code>');
|
|
7401
|
+
return v;
|
|
7402
|
+
}
|
|
7403
|
+
function member(v, ref) {
|
|
7404
|
+
if (!ref) throw new UsageError("which member? their email");
|
|
7405
|
+
return matchOne(v.members ?? [], ref.toLowerCase(), (m) => [m.id, m.email], "member", (m) => m.email);
|
|
7406
|
+
}
|
|
7407
|
+
var onOff = (v, what) => {
|
|
7408
|
+
if (v === "on" || v === "yes" || v === "true") return true;
|
|
7409
|
+
if (v === "off" || v === "no" || v === "false") return false;
|
|
7410
|
+
throw new UsageError(`${what}: on or off`);
|
|
7411
|
+
};
|
|
7412
|
+
var csvList = (v) => v === "any" || v === "all" ? null : v.split(",").map((x) => x.trim()).filter(Boolean);
|
|
7413
|
+
var numOrNone = (v, what) => {
|
|
7414
|
+
if (v === "none" || v === "off") return null;
|
|
7415
|
+
const n2 = Number(v);
|
|
7416
|
+
if (!Number.isFinite(n2) || n2 < 0) throw new UsageError(`${what}: a number, or none`);
|
|
7417
|
+
return n2;
|
|
7418
|
+
};
|
|
7419
|
+
function printTeam(ctx, v) {
|
|
7420
|
+
const { ui: ui2 } = ctx;
|
|
7421
|
+
const ws = v.workspace;
|
|
7422
|
+
ui2.out(`${ui2.c("ok", ws.name)} ${ui2.c("dim", `\xB7 you're ${v.me?.role ?? "member"}${v.me?.approver ? " and an approver" : ""}${ws.domain ? ` \xB7 ${ws.domain}${ws.autoJoin ? " (joins by work email)" : ""}` : ""}`)}`);
|
|
7423
|
+
ui2.table(
|
|
7424
|
+
(v.members ?? []).map((m) => [m.email, m.role === "member" ? ui2.c("dim", "member") : m.role, m.approver && m.role === "member" ? "approver" : "", ui2.c("dim", `joined ${ago(m.joinedAt)}`)]),
|
|
7425
|
+
{ indent: 2 }
|
|
7426
|
+
);
|
|
7427
|
+
if (v.requests?.length) {
|
|
7428
|
+
ui2.out();
|
|
7429
|
+
ui2.out(`${v.requests.length} asking to join:`);
|
|
7430
|
+
ui2.table(v.requests.map((r) => [r.email, ui2.c("dim", ago(r.createdAt)), ui2.c("dim", `airgate team accept ${r.email}`)]), { indent: 2 });
|
|
7431
|
+
}
|
|
7432
|
+
}
|
|
7433
|
+
var team = {
|
|
7434
|
+
name: "team",
|
|
7435
|
+
aliases: ["workspace"],
|
|
7436
|
+
group: "account",
|
|
7437
|
+
usage: "[create|invite|invites|join|leave|members|role|approver|remove|accept|decline|domain|rules|audit|billing|upgrade|seats|secrets|secret|triggers|trigger|delete] \u2026",
|
|
7438
|
+
summary: "your team workspace: members, invites, rules, audit log, billing, team secrets, GitHub triggers",
|
|
7439
|
+
example: "airgate team invite sam@acme.dev",
|
|
7440
|
+
notes: [
|
|
7441
|
+
"airgate team the workspace, your role, its members",
|
|
7442
|
+
'airgate team create "<name>" start one (you own it)',
|
|
7443
|
+
"airgate team invite [email] [--admin] an invite (emailed when you give an address); no email: a link anyone can use",
|
|
7444
|
+
"airgate team invites \xB7 revoke <code> open invites",
|
|
7445
|
+
"airgate team join <code|link> join with an invite (your pods and snapshots come with you)",
|
|
7446
|
+
"airgate team request ask to join the workspace at your email's domain",
|
|
7447
|
+
"airgate team accept|decline <email> answer someone asking to join",
|
|
7448
|
+
"airgate team leave leave (your pods stay yours; shared ones stop being shared)",
|
|
7449
|
+
"airgate team role <email> admin|member|owner \xB7 approver <email> on|off \xB7 remove <email>",
|
|
7450
|
+
"airgate team domain <domain> [--no-auto-join] | --release people with a confirmed email there join on their own",
|
|
7451
|
+
"airgate team rules [--approvers on|off] [--kinds git-push,deploy] [--agents a,b|any] [--networks allowlist,off|any] [--max-pods n|none] [--cap usd|none] [--auto-seats on|off] [--hints on|off]",
|
|
7452
|
+
"airgate team audit [--csv] who did what (admins)",
|
|
7453
|
+
"airgate team billing \xB7 upgrade [--seats n] \xB7 seats <n> the team plan: seats and its pooled hours",
|
|
7454
|
+
"airgate team secrets \xB7 secret set NAME (value from a hidden prompt or stdin) \xB7 secret rm NAME reach every member's pods",
|
|
7455
|
+
"airgate team triggers \xB7 trigger add <owner/repo> pr|issue [--label l] [--recipe r] \xB7 trigger rm <id> crews started by GitHub"
|
|
7456
|
+
],
|
|
7457
|
+
flags: {
|
|
7458
|
+
admin: { type: "boolean", help: "invite: as an admin" },
|
|
7459
|
+
"auto-join": { type: "boolean", default: true, help: "domain: people at it join on their own (--no-auto-join: they ask)" },
|
|
7460
|
+
release: { type: "boolean", help: "domain: let the domain go" },
|
|
7461
|
+
approvers: { type: "string", value: "on|off", help: "rules: an approver (not the requester) decides airlock requests" },
|
|
7462
|
+
kinds: { type: "string", value: "kinds", help: "rules: which requests need an approver (git-push,pull-request,deploy,email,http)" },
|
|
7463
|
+
agents: { type: "string", value: "list|any", help: "rules: agents members may launch" },
|
|
7464
|
+
networks: { type: "string", value: "list|any", help: "rules: networks members may use (open,allowlist,off)" },
|
|
7465
|
+
"max-pods": { type: "string", value: "n|none", help: "rules: awake pods per member" },
|
|
7466
|
+
cap: { type: "string", value: "usd|none", help: "rules: monthly spend cap per member" },
|
|
7467
|
+
"auto-seats": { type: "string", value: "on|off", help: "rules: buy a seat when someone joins past the count" },
|
|
7468
|
+
hints: { type: "string", value: "on|off", help: "rules: offer the same work from the library before new work starts" },
|
|
7469
|
+
csv: { type: "boolean", help: "audit: as CSV" },
|
|
7470
|
+
seats: { type: "string", value: "n", help: "upgrade: how many seats (at least everyone in it)" },
|
|
7471
|
+
label: { type: "string", value: "label", help: "trigger: the issue label that starts a crew (default airgate)" },
|
|
7472
|
+
recipe: { type: "string", value: "recipe", help: "trigger: race, contest, council, squad or split" },
|
|
7473
|
+
yes: { type: "boolean", short: "y", help: "don't ask" },
|
|
7474
|
+
"no-open": { type: "boolean", help: "print the link instead of opening the browser" }
|
|
7475
|
+
},
|
|
7476
|
+
async run(ctx) {
|
|
7477
|
+
const { ui: ui2 } = ctx;
|
|
7478
|
+
const [action, a1, a2] = ctx.args.positionals;
|
|
7479
|
+
if (!action) {
|
|
7480
|
+
const v = await ctx.api.team();
|
|
7481
|
+
if (ctx.json) return ui2.json(v);
|
|
7482
|
+
if (!v.workspace) {
|
|
7483
|
+
ui2.out(ui2.c("dim", "you're not in a team workspace."));
|
|
7484
|
+
for (const i of v.invites) ui2.out(`invited to ${ui2.c("ok", i.workspace.name)} as ${i.role}: airgate team join ${i.code}`);
|
|
7485
|
+
if (v.suggestion) ui2.out(`${v.suggestion.name} uses your email's domain (${v.suggestion.domain}): ${v.suggestion.requested ? "you asked to join; an admin decides" : "airgate team request"}`);
|
|
7486
|
+
return void ui2.out(ui2.c("dim", 'start one: airgate team create "<name>"'));
|
|
7487
|
+
}
|
|
7488
|
+
return printTeam(ctx, v);
|
|
7489
|
+
}
|
|
7490
|
+
switch (action) {
|
|
7491
|
+
case "create": {
|
|
7492
|
+
if (!a1) throw new UsageError('airgate team create "<name>"');
|
|
7493
|
+
const v = await ctx.api.createTeam(a1);
|
|
7494
|
+
if (ctx.json) return ui2.json(v);
|
|
7495
|
+
ui2.out(`${ui2.c("ok", "created")} ${v.workspace.name}. you own it.`);
|
|
7496
|
+
return void ui2.out(ui2.c("dim", "next: airgate team invite <email> \xB7 airgate team domain <your domain> \xB7 airgate team upgrade"));
|
|
7497
|
+
}
|
|
7498
|
+
case "invite": {
|
|
7499
|
+
const inv = await ctx.api.invite(a1 ?? null, bool(ctx.args, "admin") ? "admin" : "member");
|
|
7500
|
+
if (ctx.json) return ui2.json(inv);
|
|
7501
|
+
ui2.out(inv.url);
|
|
7502
|
+
return void ui2.err(ui2.ce("dim", inv.email ? `emailed to ${inv.email} (as ${inv.role}); works for 14 days` : `anyone with this link can join as ${inv.role} for 14 days. revoke: airgate team revoke ${inv.code}`));
|
|
7503
|
+
}
|
|
7504
|
+
case "invites": {
|
|
7505
|
+
const list2 = await ctx.api.teamInvites();
|
|
7506
|
+
if (ctx.json) return ui2.json(list2);
|
|
7507
|
+
if (!list2.length) return void ui2.out(ui2.c("dim", "no open invites. make one: airgate team invite [email]"));
|
|
7508
|
+
return void ui2.table(list2.map((i) => [i.email ?? ui2.c("dim", "link"), i.role, ui2.c("dim", `expires ${new Date(i.expiresAt).toISOString().slice(0, 10)}`), ui2.c("dim", i.code)]), { head: ["for", "role", "", "code"] });
|
|
7509
|
+
}
|
|
7510
|
+
case "revoke": {
|
|
7511
|
+
if (!a1) throw new UsageError("airgate team revoke <code>");
|
|
7512
|
+
await ctx.api.revokeInvite(a1);
|
|
7513
|
+
return ctx.json ? ui2.json({ ok: true }) : void ui2.out("revoked.");
|
|
7514
|
+
}
|
|
7515
|
+
case "join": {
|
|
7516
|
+
if (!a1) throw new UsageError("airgate team join <code or link>");
|
|
7517
|
+
const code3 = /code=([A-Za-z0-9_-]+)/.exec(a1)?.[1] ?? a1.trim();
|
|
7518
|
+
const peek = await ctx.api.peekInvite(code3);
|
|
7519
|
+
if (!ctx.json && !await confirm(ctx, `join ${peek.workspace.name} (${peek.members} people) as ${peek.role}? your pods and snapshots stay yours, your usage bills to the team`)) throw new CliError("not joined");
|
|
7520
|
+
const v = await ctx.api.joinTeam(code3);
|
|
7521
|
+
if (ctx.json) return ui2.json(v);
|
|
7522
|
+
ui2.out(`${ui2.c("ok", "joined")} ${v.workspace.name}.`);
|
|
7523
|
+
return void ui2.out(ui2.c("dim", "teammates' shared pods: airgate pods \xB7 the team library: airgate recall"));
|
|
7524
|
+
}
|
|
7525
|
+
case "request": {
|
|
7526
|
+
const r = await ctx.api.requestJoin();
|
|
7527
|
+
return ctx.json ? ui2.json(r) : void ui2.out("asked. an admin of the workspace at your domain decides.");
|
|
7528
|
+
}
|
|
7529
|
+
case "accept":
|
|
7530
|
+
case "decline": {
|
|
7531
|
+
const v = await mine(ctx);
|
|
7532
|
+
const req = matchOne(v.requests ?? [], (a1 ?? "").toLowerCase(), (r) => [r.id, r.email], "request", (r) => r.email);
|
|
7533
|
+
const out = await ctx.api.decideRequest(req.id, action === "accept");
|
|
7534
|
+
return ctx.json ? ui2.json(out) : void ui2.out(`${action === "accept" ? "added" : "declined"} ${req.email}.`);
|
|
7535
|
+
}
|
|
7536
|
+
case "leave": {
|
|
7537
|
+
const v = await mine(ctx);
|
|
7538
|
+
if (!await confirm(ctx, `leave ${v.workspace.name}? your pods stay yours; shared ones stop being shared`)) throw new CliError("still in it");
|
|
7539
|
+
await ctx.api.leaveTeam();
|
|
7540
|
+
return ctx.json ? ui2.json({ ok: true }) : void ui2.out(`left ${v.workspace.name}.`);
|
|
7541
|
+
}
|
|
7542
|
+
case "delete": {
|
|
7543
|
+
const v = await mine(ctx);
|
|
7544
|
+
if (!await confirm(ctx, `delete the ${v.workspace.name} workspace? everyone in it becomes a personal account again (their pods stay theirs). cancel the team plan in billing too`)) throw new CliError("nothing deleted");
|
|
7545
|
+
await ctx.api.deleteTeam();
|
|
7546
|
+
return ctx.json ? ui2.json({ ok: true }) : void ui2.out("deleted. cancel the plan: airgate billing");
|
|
7547
|
+
}
|
|
7548
|
+
case "members": {
|
|
7549
|
+
const v = await mine(ctx);
|
|
7550
|
+
return ctx.json ? ui2.json(v.members) : printTeam(ctx, v);
|
|
7551
|
+
}
|
|
7552
|
+
case "role": {
|
|
7553
|
+
const v = await mine(ctx);
|
|
7554
|
+
const m = member(v, a1);
|
|
7555
|
+
if (a2 !== "admin" && a2 !== "member" && a2 !== "owner") throw new UsageError("airgate team role <email> admin|member|owner");
|
|
7556
|
+
if (a2 === "owner" && !await confirm(ctx, `make ${m.email} the owner? you stay an admin`)) throw new CliError("nothing changed");
|
|
7557
|
+
const out = await ctx.api.setMember(m.id, { role: a2 });
|
|
7558
|
+
return ctx.json ? ui2.json(out) : void ui2.out(`${m.email} is ${a2} now.`);
|
|
7559
|
+
}
|
|
7560
|
+
case "approver": {
|
|
7561
|
+
const v = await mine(ctx);
|
|
7562
|
+
const m = member(v, a1);
|
|
7563
|
+
const on = onOff(a2, "approver");
|
|
7564
|
+
const out = await ctx.api.setMember(m.id, { approver: on });
|
|
7565
|
+
return ctx.json ? ui2.json(out) : void ui2.out(`${m.email} ${on ? "decides" : "no longer decides"} teammates' airlock requests.`);
|
|
7566
|
+
}
|
|
7567
|
+
case "remove": {
|
|
7568
|
+
const v = await mine(ctx);
|
|
7569
|
+
const m = member(v, a1);
|
|
7570
|
+
if (!await confirm(ctx, `remove ${m.email} from ${v.workspace.name}? their pods stay theirs`)) throw new CliError("nothing changed");
|
|
7571
|
+
const out = await ctx.api.removeMember(m.id);
|
|
7572
|
+
return ctx.json ? ui2.json(out) : void ui2.out(`removed ${m.email}.`);
|
|
7573
|
+
}
|
|
7574
|
+
case "domain": {
|
|
7575
|
+
if (bool(ctx.args, "release")) {
|
|
7576
|
+
const out2 = await ctx.api.claimDomain(null);
|
|
7577
|
+
return ctx.json ? ui2.json(out2) : void ui2.out("the domain is free again; nobody joins by email.");
|
|
7578
|
+
}
|
|
7579
|
+
if (!a1) throw new UsageError("airgate team domain <domain> [--no-auto-join]");
|
|
7580
|
+
const out = await ctx.api.claimDomain(a1, bool(ctx.args, "auto-join"));
|
|
7581
|
+
if (ctx.json) return ui2.json(out);
|
|
7582
|
+
return void ui2.out(
|
|
7583
|
+
out.workspace?.autoJoin ? `${out.workspace.domain}: people with a confirmed email there join ${out.workspace.name} on their own.` : `${out.workspace?.domain}: people there can ask to join (airgate team request); you decide.`
|
|
7584
|
+
);
|
|
7585
|
+
}
|
|
7586
|
+
case "rules": {
|
|
7587
|
+
const patch = {};
|
|
7588
|
+
const f = (k) => str(ctx.args, k);
|
|
7589
|
+
if (f("approvers") !== void 0) patch.approvals = onOff(f("approvers"), "--approvers") ? "approvers" : "self";
|
|
7590
|
+
if (f("kinds") !== void 0) patch.approvalKinds = csvList(f("kinds")) ?? ["git-push", "pull-request", "deploy", "email", "http"];
|
|
7591
|
+
if (f("agents") !== void 0) patch.agents = csvList(f("agents"));
|
|
7592
|
+
if (f("networks") !== void 0) patch.networks = csvList(f("networks"));
|
|
7593
|
+
if (f("max-pods") !== void 0) patch.maxPodsPerMember = numOrNone(f("max-pods"), "--max-pods");
|
|
7594
|
+
if (f("cap") !== void 0) patch.capUsdPerMember = numOrNone(f("cap"), "--cap");
|
|
7595
|
+
if (f("auto-seats") !== void 0) patch.autoSeats = onOff(f("auto-seats"), "--auto-seats");
|
|
7596
|
+
if (f("hints") !== void 0) patch.libraryHints = onOff(f("hints"), "--hints");
|
|
7597
|
+
const v = Object.keys(patch).length ? await ctx.api.updateTeam({ rules: patch }) : await mine(ctx);
|
|
7598
|
+
const r = v.workspace.rules;
|
|
7599
|
+
if (ctx.json) return ui2.json(r);
|
|
7600
|
+
return void ui2.table(
|
|
7601
|
+
[
|
|
7602
|
+
["airlock", r.approvals === "approvers" ? `an approver decides ${r.approvalKinds.join(", ")}` : "each member decides their own"],
|
|
7603
|
+
["agents", r.agents ? r.agents.join(", ") : "any"],
|
|
7604
|
+
["networks", r.networks ? r.networks.join(", ") : "any"],
|
|
7605
|
+
["pods awake", r.maxPodsPerMember == null ? "the plan's limit" : `${r.maxPodsPerMember} per member`],
|
|
7606
|
+
["spend cap", r.capUsdPerMember == null ? "none" : `$${r.capUsdPerMember} per member a month`],
|
|
7607
|
+
["seats", r.autoSeats ? "bought as people join" : "an admin adds them"],
|
|
7608
|
+
["library hints", r.libraryHints ? "on: the same work is offered before it starts" : "off"]
|
|
7609
|
+
].map(([k, val]) => [ui2.c("dim", k), val])
|
|
7610
|
+
);
|
|
7611
|
+
}
|
|
7612
|
+
case "audit": {
|
|
7613
|
+
if (bool(ctx.args, "csv")) return void ctx.io.stdout.write(await ctx.api.auditCsv());
|
|
7614
|
+
const list2 = await ctx.api.audit();
|
|
7615
|
+
if (ctx.json) return ui2.json(list2);
|
|
7616
|
+
if (!list2.length) return void ui2.out(ui2.c("dim", "nothing yet."));
|
|
7617
|
+
return void ui2.table(list2.slice(0, 50).map((e) => [ui2.c("dim", new Date(e.at).toISOString().slice(0, 16).replace("T", " ")), e.actor, e.action, e.target ?? ""]), { head: ["when (utc)", "who", "what", "on"] });
|
|
7618
|
+
}
|
|
7619
|
+
case "billing": {
|
|
7620
|
+
const b2 = await ctx.api.teamBilling();
|
|
7621
|
+
if (ctx.json) return ui2.json(b2);
|
|
7622
|
+
if (b2.plan !== "team") {
|
|
7623
|
+
ui2.out(`no team plan yet: everyone is on their own plan. ${ui2.c("dim", `team: $${b2.price.perSeatUsd} a seat a month, ${b2.price.hoursPerSeat} h per seat pooled, then $${b2.price.hourlyUsd.toFixed(2)}/h`)}`);
|
|
7624
|
+
return void ui2.out(ui2.c("dim", "start it (the owner): airgate team upgrade --seats <n>"));
|
|
7625
|
+
}
|
|
7626
|
+
return void ui2.table(
|
|
7627
|
+
[
|
|
7628
|
+
["plan", `team \xB7 ${b2.status ?? "active"}`],
|
|
7629
|
+
["seats", `${b2.seats.used} used of ${b2.seats.paid ?? b2.seats.used} paid`],
|
|
7630
|
+
["pooled hours", `${(b2.seats.paid ?? b2.seats.used) * b2.price.hoursPerSeat} h this month`],
|
|
7631
|
+
["renews", b2.periodEnd ? new Date(b2.periodEnd).toISOString().slice(0, 10) : "?"],
|
|
7632
|
+
["paid by", b2.payer?.you ? "you" : b2.payer?.email ?? "the owner"]
|
|
7633
|
+
].map(([k, val]) => [ui2.c("dim", k), val])
|
|
7634
|
+
);
|
|
7635
|
+
}
|
|
7636
|
+
case "upgrade": {
|
|
7637
|
+
const v = await mine(ctx);
|
|
7638
|
+
const seats = Math.max((v.members ?? []).length, Number(str(ctx.args, "seats") ?? a1 ?? (v.members ?? []).length) || 1);
|
|
7639
|
+
const { url } = await ctx.api.teamCheckout(seats, "https://airgate.sh/app/team");
|
|
7640
|
+
return openOrPrint(ctx, url, `the team plan checkout (${seats} seats)`);
|
|
7641
|
+
}
|
|
7642
|
+
case "seats": {
|
|
7643
|
+
const n2 = Number(a1);
|
|
7644
|
+
if (!Number.isInteger(n2) || n2 < 1) throw new UsageError("airgate team seats <n>");
|
|
7645
|
+
const r = await ctx.api.setSeats(n2);
|
|
7646
|
+
return ctx.json ? ui2.json(r) : void ui2.out(`${r.seats.paid} seats from now on (prorated). ${r.seats.used} in use.`);
|
|
7647
|
+
}
|
|
7648
|
+
case "secrets": {
|
|
7649
|
+
const list2 = await ctx.api.teamSecrets();
|
|
7650
|
+
if (ctx.json) return ui2.json(list2);
|
|
7651
|
+
if (!list2.length) return void ui2.out(ui2.c("dim", "no team secrets. add one (admins): airgate team secret set NAME"));
|
|
7652
|
+
ui2.table(list2.map((x) => [x.name, ui2.c("dim", `updated ${ago(x.updatedAt)}`)]));
|
|
7653
|
+
return void ui2.out(ui2.c("dim", "every member's pods get these (their own secrets win on a clash); values never come back out"));
|
|
7654
|
+
}
|
|
7655
|
+
case "secret": {
|
|
7656
|
+
if (a1 === "set") {
|
|
7657
|
+
if (!a2) throw new UsageError("airgate team secret set NAME (the value comes from a hidden prompt or stdin)");
|
|
7658
|
+
const value = await readValue(ctx, a2, `printf %s "$VALUE" | airgate team secret set ${a2}`);
|
|
7659
|
+
const r = await ctx.api.setTeamSecrets({ [a2]: value });
|
|
7660
|
+
return ctx.json ? ui2.json(r) : void ui2.out(`${r.created.length ? "added" : "updated"} ${a2} for the team. pods pick it up on their next wake.`);
|
|
7661
|
+
}
|
|
7662
|
+
if (a1 === "rm" || a1 === "remove" || a1 === "delete") {
|
|
7663
|
+
if (!a2) throw new UsageError("airgate team secret rm NAME");
|
|
7664
|
+
await ctx.api.deleteTeamSecret(a2);
|
|
7665
|
+
return ctx.json ? ui2.json({ ok: true }) : void ui2.out(`removed ${a2} from the team.`);
|
|
7666
|
+
}
|
|
7667
|
+
throw new UsageError("airgate team secret set NAME | rm NAME");
|
|
7668
|
+
}
|
|
7669
|
+
case "triggers": {
|
|
7670
|
+
const list2 = await ctx.api.triggers();
|
|
7671
|
+
if (ctx.json) return ui2.json(list2);
|
|
7672
|
+
if (!list2.length) return void ui2.out(ui2.c("dim", "no triggers. add one: airgate team trigger add <owner/repo> pr|issue [--recipe squad]"));
|
|
7673
|
+
return void ui2.table(list2.map((t) => [t.repo, t.event === "pr_opened" ? "a pull request opens" : `an issue gets "${t.label}"`, `${t.recipe} crew`, ui2.c("dim", t.id)]), { head: ["repo", "when", "starts", "id"] });
|
|
7674
|
+
}
|
|
7675
|
+
case "trigger": {
|
|
7676
|
+
if (a1 === "add") {
|
|
7677
|
+
const [, , repo, when] = ctx.args.positionals;
|
|
7678
|
+
if (!repo || when !== "pr" && when !== "issue") throw new UsageError("airgate team trigger add <owner/repo> pr|issue [--label l] [--recipe r]");
|
|
7679
|
+
const t = await ctx.api.addTrigger({ repo, event: when === "pr" ? "pr_opened" : "issue_labeled", ...str(ctx.args, "label") ? { label: str(ctx.args, "label") } : {}, ...str(ctx.args, "recipe") ? { recipe: str(ctx.args, "recipe") } : {} });
|
|
7680
|
+
if (ctx.json) return ui2.json(t);
|
|
7681
|
+
ui2.out(`${ui2.c("ok", "added")}: ${t.event === "pr_opened" ? "each pull request opened on" : `each issue labelled "${t.label}" on`} ${t.repo} starts a ${t.recipe} crew.`);
|
|
7682
|
+
return void ui2.out(ui2.c("dim", "it runs as you; what it pushes or opens waits in the airlock. the GitHub App must be on the repo: airgate github"));
|
|
7683
|
+
}
|
|
7684
|
+
if (a1 === "rm" || a1 === "remove") {
|
|
7685
|
+
if (!a2) throw new UsageError("airgate team trigger rm <id>");
|
|
7686
|
+
await ctx.api.removeTrigger(a2);
|
|
7687
|
+
return ctx.json ? ui2.json({ ok: true }) : void ui2.out("removed.");
|
|
7688
|
+
}
|
|
7689
|
+
throw new UsageError("airgate team trigger add \u2026 | rm <id>");
|
|
7690
|
+
}
|
|
7691
|
+
default:
|
|
7692
|
+
throw new UsageError(`unknown "${action}"`, "see: airgate help team");
|
|
7693
|
+
}
|
|
7694
|
+
}
|
|
7695
|
+
};
|
|
7696
|
+
var handover = {
|
|
7697
|
+
name: "handover",
|
|
7698
|
+
group: "pods",
|
|
7699
|
+
usage: "<pod> <email>",
|
|
7700
|
+
summary: "give a pod to a teammate, with its checkpoints, conversation and pod secrets",
|
|
7701
|
+
example: "airgate handover web sam@acme.dev",
|
|
7702
|
+
flags: { yes: { type: "boolean", short: "y", help: "don't ask" } },
|
|
7703
|
+
async run(ctx) {
|
|
7704
|
+
const [ref, to] = ctx.args.positionals;
|
|
7705
|
+
if (!ref || !to) throw new UsageError("airgate handover <pod> <email>");
|
|
7706
|
+
const p = await resolvePod(ctx, ref, { team: true });
|
|
7707
|
+
if (!await confirm(ctx, `give ${p.spec.name} to ${to}? it becomes theirs`)) throw new CliError("nothing changed");
|
|
7708
|
+
const r = await ctx.api.handover(p.id, to);
|
|
7709
|
+
return ctx.json ? ctx.ui.json(r) : void ctx.ui.out(`${p.spec.name} is ${r.to.email}'s now.`);
|
|
7710
|
+
}
|
|
7711
|
+
};
|
|
7712
|
+
var recall = {
|
|
7713
|
+
name: "recall",
|
|
7714
|
+
aliases: ["library"],
|
|
7715
|
+
group: "pods",
|
|
7716
|
+
usage: '["query"] | --patch <id> [--apply]',
|
|
7717
|
+
summary: "your team's library: work teammates already did (reuse it instead of paying again) and their notes",
|
|
7718
|
+
example: 'airgate recall "rate limiting"',
|
|
7719
|
+
notes: [
|
|
7720
|
+
"every finished task, run and crew in a team workspace lands here, with what it cost.",
|
|
7721
|
+
"--patch <id> prints that work as a patch; --apply puts it into this folder with git apply (after asking)."
|
|
7722
|
+
],
|
|
7723
|
+
flags: {
|
|
7724
|
+
patch: { type: "string", value: "id", help: "print an entry's work as a patch" },
|
|
7725
|
+
apply: { type: "boolean", help: "with --patch: apply it here (git apply)" },
|
|
7726
|
+
yes: { type: "boolean", short: "y", help: "don't ask" }
|
|
7727
|
+
},
|
|
7728
|
+
async run(ctx) {
|
|
7729
|
+
const { ui: ui2 } = ctx;
|
|
7730
|
+
const id = str(ctx.args, "patch");
|
|
7731
|
+
if (id) {
|
|
7732
|
+
const { patch, entry } = await ctx.api.libraryPatch(id);
|
|
7733
|
+
if (!bool(ctx.args, "apply")) {
|
|
7734
|
+
if (ctx.json) return ui2.json({ patch, entry });
|
|
7735
|
+
return void ctx.io.stdout.write(patch);
|
|
7736
|
+
}
|
|
7737
|
+
if (!patch.trim()) throw new CliError("that work has no changes to apply");
|
|
7738
|
+
const file = resolve4(ctx.io.cwd, `.airgate-recall-${id}.patch`);
|
|
7739
|
+
writeFileSync2(file, patch);
|
|
7740
|
+
try {
|
|
7741
|
+
const check = spawnSync("git", ["apply", "--check", file], { cwd: ctx.io.cwd, encoding: "utf8" });
|
|
7742
|
+
if (check.status !== 0) throw new CliError("it doesn't apply cleanly here", (check.stderr || "").trim().split("\n")[0] || "look at it first: airgate recall --patch " + id);
|
|
7743
|
+
const files2 = (patch.match(/^\+\+\+ b\/(.+)$/gm) ?? []).map((l) => l.slice(6));
|
|
7744
|
+
if (!await confirm(ctx, `apply "${entry.title}" by ${entry.author} here (${files2.length} file${files2.length === 1 ? "" : "s"})?`)) throw new CliError("nothing applied");
|
|
7745
|
+
const r = spawnSync("git", ["apply", file], { cwd: ctx.io.cwd, encoding: "utf8" });
|
|
7746
|
+
if (r.status !== 0) throw new CliError("git apply failed", (r.stderr || "").trim());
|
|
7747
|
+
if (ctx.json) return ui2.json({ applied: true, files: files2 });
|
|
7748
|
+
return void ui2.out(`${ui2.c("ok", "applied")} ${files2.length} file${files2.length === 1 ? "" : "s"}: ${files2.slice(0, 8).join(", ")}${files2.length > 8 ? ", \u2026" : ""}`);
|
|
7749
|
+
} finally {
|
|
7750
|
+
rmSync2(file, { force: true });
|
|
7751
|
+
}
|
|
7752
|
+
}
|
|
7753
|
+
const q = ctx.args.positionals.join(" ");
|
|
7754
|
+
let entries;
|
|
7755
|
+
try {
|
|
7756
|
+
entries = await ctx.api.library(q);
|
|
7757
|
+
} catch (e) {
|
|
7758
|
+
if (e instanceof ApiError && e.code === "no_workspace") throw new CliError("you're not in a team workspace", "the library is for teams: airgate team");
|
|
7759
|
+
throw e;
|
|
7760
|
+
}
|
|
7761
|
+
if (ctx.json) return ui2.json(entries);
|
|
7762
|
+
if (!entries.length) return void ui2.out(ui2.c("dim", q ? `nothing in the library matches "${q}"` : "the library is empty: finished team work lands here"));
|
|
7763
|
+
for (const e of entries) {
|
|
7764
|
+
const cost = e.cost?.tokens ? ` \xB7 ${e.cost.tokens} tokens` : e.cost?.activeSeconds ? ` \xB7 ${Math.round(e.cost.activeSeconds / 60)} min` : "";
|
|
7765
|
+
ui2.out(`${ui2.c("ok", e.title)} ${ui2.c("dim", `${e.kind} by ${e.author}, ${ago(e.at)}${cost} \xB7 ${e.id}`)}`);
|
|
7766
|
+
if (e.summary) ui2.out(` ${e.summary.split("\n")[0].slice(0, 160)}`);
|
|
7767
|
+
if (e.files?.length) ui2.out(ui2.c("dim", ` ${e.files.slice(0, 6).join(", ")}${e.files.length > 6 ? ", \u2026" : ""}`));
|
|
7768
|
+
}
|
|
7769
|
+
ui2.out(ui2.c("dim", "reuse one: airgate recall --patch <id> --apply"));
|
|
7770
|
+
}
|
|
7771
|
+
};
|
|
7772
|
+
var note = {
|
|
7773
|
+
name: "note",
|
|
7774
|
+
group: "pods",
|
|
7775
|
+
usage: '"<what the team should know>"',
|
|
7776
|
+
summary: "save a note to your team library; every new pod in the team reads it",
|
|
7777
|
+
example: 'airgate note "tests need DATABASE_URL=sqlite::memory:"',
|
|
7778
|
+
async run(ctx) {
|
|
7779
|
+
const text2 = ctx.args.positionals.join(" ").trim();
|
|
7780
|
+
if (!text2) throw new UsageError('airgate note "<what the team should know>"');
|
|
7781
|
+
const r = await ctx.api.note(text2);
|
|
7782
|
+
return ctx.json ? ctx.ui.json(r) : void ctx.ui.out("saved for the team.");
|
|
7783
|
+
}
|
|
7784
|
+
};
|
|
7785
|
+
var verify = {
|
|
7786
|
+
name: "verify",
|
|
7787
|
+
group: "account",
|
|
7788
|
+
usage: "",
|
|
7789
|
+
summary: "send the link that confirms your email again (it also adds you to your team at that domain)",
|
|
7790
|
+
example: "airgate verify",
|
|
7791
|
+
async run(ctx) {
|
|
7792
|
+
const me = await ctx.api.me();
|
|
7793
|
+
if (me.user.emailVerified) return ctx.json ? ctx.ui.json({ sent: false, verified: true }) : void ctx.ui.out(`${me.user.email} is confirmed already.`);
|
|
7794
|
+
const r = await ctx.api.sendVerification();
|
|
7795
|
+
if (ctx.json) return ctx.ui.json(r);
|
|
7796
|
+
ctx.ui.out(r.sent ? `sent to ${me.user.email}. open the link in it.` : "this gateway can't send email; ask whoever runs it");
|
|
7797
|
+
}
|
|
7798
|
+
};
|
|
7799
|
+
|
|
7245
7800
|
// src/commands/pods.ts
|
|
7246
7801
|
import { execFileSync } from "node:child_process";
|
|
7247
7802
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
@@ -7308,19 +7863,35 @@ var pods = {
|
|
|
7308
7863
|
aliases: ["ls"],
|
|
7309
7864
|
group: "pods",
|
|
7310
7865
|
usage: "",
|
|
7311
|
-
summary: "list your pods",
|
|
7866
|
+
summary: "list your pods, and the ones teammates shared with your team",
|
|
7312
7867
|
example: "airgate ls --json",
|
|
7868
|
+
notes: ["--json: your pods as a list (pods teammates shared are in airgate ls --team --json)."],
|
|
7869
|
+
flags: { team: { type: "boolean", help: "with --json: { pods, team } including teammates' shared pods" } },
|
|
7313
7870
|
async run(ctx) {
|
|
7314
|
-
const all = await ctx.api.
|
|
7315
|
-
if (ctx.json) return ctx.ui.json(all);
|
|
7316
|
-
if (!all.length) {
|
|
7871
|
+
const { pods: all, team: team2 } = await ctx.api.podsWithTeam();
|
|
7872
|
+
if (ctx.json) return ctx.ui.json(ctx.args.flags.team === true ? { pods: all, team: team2 } : all);
|
|
7873
|
+
if (!all.length && !team2.length) {
|
|
7317
7874
|
ctx.ui.out("no pods yet. launch one: airgate launch");
|
|
7318
7875
|
return;
|
|
7319
7876
|
}
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7877
|
+
if (all.length) {
|
|
7878
|
+
ctx.ui.table(
|
|
7879
|
+
all.map((p) => {
|
|
7880
|
+
const row = podRow(ctx, p);
|
|
7881
|
+
if (p.shared) row[0] += ctx.ui.c("dim", " (team)");
|
|
7882
|
+
return row;
|
|
7883
|
+
}),
|
|
7884
|
+
{ head: ["name", "id", "state", "agent", "size", "template", "active", "created"] }
|
|
7885
|
+
);
|
|
7886
|
+
} else ctx.ui.out(ctx.ui.c("dim", "no pods of your own yet. launch one: airgate launch"));
|
|
7887
|
+
if (team2.length) {
|
|
7888
|
+
ctx.ui.out();
|
|
7889
|
+
ctx.ui.out(ctx.ui.c("dim", "shared by teammates (open: airgate attach <name> \xB7 copy: airgate race <name>):"));
|
|
7890
|
+
ctx.ui.table(
|
|
7891
|
+
team2.map((p) => [ctx.ui.c("ok", p.spec.name), ctx.ui.c("dim", p.id), p.state, p.owner?.email ?? "", p.viewers?.length ? ctx.ui.c("warn", `${p.viewers.length} in it`) : ""]),
|
|
7892
|
+
{ indent: 2 }
|
|
7893
|
+
);
|
|
7894
|
+
}
|
|
7324
7895
|
}
|
|
7325
7896
|
};
|
|
7326
7897
|
var launch = {
|
|
@@ -7476,7 +8047,7 @@ var info = {
|
|
|
7476
8047
|
summary: "details of one pod",
|
|
7477
8048
|
example: "airgate info fix-nav",
|
|
7478
8049
|
async run(ctx) {
|
|
7479
|
-
const p = await resolvePod(ctx, ctx.args.positionals[0]);
|
|
8050
|
+
const p = await resolvePod(ctx, ctx.args.positionals[0], { team: true });
|
|
7480
8051
|
if (ctx.json) return ctx.ui.json(p);
|
|
7481
8052
|
const s2 = p.spec;
|
|
7482
8053
|
const rows2 = [
|
|
@@ -7496,6 +8067,9 @@ var info = {
|
|
|
7496
8067
|
for (const [i, j] of (s2.jobs ?? []).entries()) {
|
|
7497
8068
|
rows2.push([i ? "" : "jobs", `${j.enabled === false ? ctx.ui.c("dim", "paused ") : ""}${j.name ? j.name + " \xB7 " : ""}${j.cron} \xB7 ${j.task.length > 60 ? j.task.slice(0, 59) + "\u2026" : j.task}`]);
|
|
7498
8069
|
}
|
|
8070
|
+
if (p.owner) rows2.push(["owner", `${p.owner.email} (shared with your team)`]);
|
|
8071
|
+
else if (p.shared) rows2.push(["team", "shared with your workspace"]);
|
|
8072
|
+
if (p.viewers?.length) rows2.push(["in it now", p.viewers.map((v) => v.email).join(", ")]);
|
|
7499
8073
|
if (p.forkOf) rows2.push(["race of", `${p.forkOf.podId} @ ${p.forkOf.checkpointId}`]);
|
|
7500
8074
|
rows2.push(["checkpoints", String(p.snapshots.length)]);
|
|
7501
8075
|
rows2.push(["active", duration2(p.seconds)]);
|
|
@@ -7695,13 +8269,15 @@ var task = {
|
|
|
7695
8269
|
usage: '<pod> "<task>" [--wait]',
|
|
7696
8270
|
summary: "hand the pod's agent a task now; it runs headless and logs like a job",
|
|
7697
8271
|
example: 'airgate task fix-nav "make the nav sticky on mobile, run the tests"',
|
|
7698
|
-
flags: { wait: { type: "boolean", short: "w", help: "wait for it to finish and print the log" } },
|
|
8272
|
+
flags: { wait: { type: "boolean", short: "w", help: "wait for it to finish and print the log" }, yes: { type: "boolean", short: "y", help: "don't ask" } },
|
|
7699
8273
|
async run(ctx) {
|
|
7700
8274
|
const [ref, ...words2] = ctx.args.positionals;
|
|
7701
8275
|
const text2 = (ctx.args.dashdash ? ctx.args.rest : words2).join(" ").trim();
|
|
7702
8276
|
if (!text2) throw new UsageError("what should the agent do?", 'airgate task <pod> "<task>"');
|
|
7703
8277
|
if (text2.length > 4e3) throw new UsageError("the task is 4000 characters at most");
|
|
7704
8278
|
const p = await resolvePod(ctx, ref);
|
|
8279
|
+
const same = await checkSimilar(ctx, text2);
|
|
8280
|
+
if (!same.go) notStarted(same.similar);
|
|
7705
8281
|
let run2 = await ctx.ui.spin(`handing ${p.spec.name} the task`, ctx.api.startTask(p.id, text2));
|
|
7706
8282
|
if (ctx.args.flags.wait === true) {
|
|
7707
8283
|
run2 = await ctx.ui.spin(`${p.spec.name} is working`, () => waitRun(ctx, p.id, run2));
|
|
@@ -7777,15 +8353,15 @@ var log = {
|
|
|
7777
8353
|
import { createInterface } from "node:readline";
|
|
7778
8354
|
|
|
7779
8355
|
// src/version.ts
|
|
7780
|
-
var VERSION = "0.1.
|
|
8356
|
+
var VERSION = "0.1.7";
|
|
7781
8357
|
|
|
7782
8358
|
// src/commands/updown.ts
|
|
7783
|
-
import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync2, writeFileSync as
|
|
8359
|
+
import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
7784
8360
|
import { homedir as homedir2 } from "node:os";
|
|
7785
|
-
import { basename, dirname as dirname2, isAbsolute, join as join4, relative as relative2, resolve as
|
|
8361
|
+
import { basename, dirname as dirname2, isAbsolute, join as join4, relative as relative2, resolve as resolve5 } from "node:path";
|
|
7786
8362
|
|
|
7787
8363
|
// src/pack.ts
|
|
7788
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
8364
|
+
import { spawn, spawnSync as spawnSync2 } from "node:child_process";
|
|
7789
8365
|
import { existsSync, lstatSync, readdirSync } from "node:fs";
|
|
7790
8366
|
import { join as join2 } from "node:path";
|
|
7791
8367
|
var UPLOAD_LIMIT = 32 * 6 * 1024 * 1024;
|
|
@@ -7802,7 +8378,7 @@ function isEnvFile(path) {
|
|
|
7802
8378
|
}
|
|
7803
8379
|
var ENV_EXCLUDES = [".env", ".env.local", ".env.*.local", ".env.development", ".env.production", ".env.test", ".env.staging", ".envrc"].map((p) => `--exclude=${p}`);
|
|
7804
8380
|
function git(cwd, args, input) {
|
|
7805
|
-
const r =
|
|
8381
|
+
const r = spawnSync2("git", args, { cwd, input, maxBuffer: 256 * 1024 * 1024 });
|
|
7806
8382
|
return { status: r.status ?? (r.error ? 127 : 1), stdout: r.stdout?.toString("utf8") ?? "", stderr: r.stderr?.toString("utf8") ?? "", error: r.error };
|
|
7807
8383
|
}
|
|
7808
8384
|
function isGitRepo(dir) {
|
|
@@ -7829,7 +8405,7 @@ function gitFiles(dir) {
|
|
|
7829
8405
|
return out;
|
|
7830
8406
|
}
|
|
7831
8407
|
function runTar(cwd, args, input, limit = UPLOAD_LIMIT) {
|
|
7832
|
-
return new Promise((
|
|
8408
|
+
return new Promise((resolve9, reject) => {
|
|
7833
8409
|
const child = spawn("tar", args, { cwd, env: { ...process.env, COPYFILE_DISABLE: "1" }, stdio: ["pipe", "pipe", "pipe"] });
|
|
7834
8410
|
const chunks = [];
|
|
7835
8411
|
let size2 = 0;
|
|
@@ -7849,7 +8425,7 @@ function runTar(cwd, args, input, limit = UPLOAD_LIMIT) {
|
|
|
7849
8425
|
child.on("close", (code3) => {
|
|
7850
8426
|
if (over2) return reject(new TooLargeError(limit));
|
|
7851
8427
|
if (code3 !== 0) return reject(new Error(`tar failed: ${err.trim().split("\n").slice(-3).join(" ")}`));
|
|
7852
|
-
|
|
8428
|
+
resolve9(Buffer.concat(chunks));
|
|
7853
8429
|
});
|
|
7854
8430
|
child.stdin.on("error", () => void 0);
|
|
7855
8431
|
child.stdin.end(input ?? "");
|
|
@@ -7889,7 +8465,7 @@ import { gunzipSync } from "node:zlib";
|
|
|
7889
8465
|
|
|
7890
8466
|
// src/sessions.ts
|
|
7891
8467
|
import { createHash, randomUUID } from "node:crypto";
|
|
7892
|
-
import { closeSync, existsSync as existsSync2, mkdirSync as mkdirSync2, mkdtempSync, openSync, readdirSync as readdirSync2, readFileSync as readFileSync4, readSync, realpathSync, renameSync as renameSync2, rmSync as
|
|
8468
|
+
import { closeSync, existsSync as existsSync2, mkdirSync as mkdirSync2, mkdtempSync, openSync, readdirSync as readdirSync2, readFileSync as readFileSync4, readSync, realpathSync, renameSync as renameSync2, rmSync as rmSync3, statSync, writeFileSync as writeFileSync3 } from "node:fs";
|
|
7893
8469
|
import { tmpdir } from "node:os";
|
|
7894
8470
|
import { dirname, join as join3, relative } from "node:path";
|
|
7895
8471
|
var POD_HOME = "/home/sprite";
|
|
@@ -7999,7 +8575,7 @@ async function packSession(s2, projectDir, podPath, limit = UPLOAD_LIMIT) {
|
|
|
7999
8575
|
const names = [];
|
|
8000
8576
|
if (s2.agent === "claude-code") {
|
|
8001
8577
|
podDir = `.claude/projects/${claudeKey(podPath)}`;
|
|
8002
|
-
|
|
8578
|
+
writeFileSync3(join3(stage, `${s2.id}.jsonl`), rewriteAll(raw.toString("utf8"), froms, podPath));
|
|
8003
8579
|
names.push(`${s2.id}.jsonl`);
|
|
8004
8580
|
if (s2.sidecar) {
|
|
8005
8581
|
const copy2 = (src, dst) => {
|
|
@@ -8011,7 +8587,7 @@ async function packSession(s2, projectDir, podPath, limit = UPLOAD_LIMIT) {
|
|
|
8011
8587
|
copy2(a, b2);
|
|
8012
8588
|
} else if (x.isFile()) {
|
|
8013
8589
|
const body = readFileSync4(a);
|
|
8014
|
-
|
|
8590
|
+
writeFileSync3(b2, /\.(jsonl?|txt|md)$/.test(x.name) ? rewriteAll(body.toString("utf8"), froms, podPath) : body);
|
|
8015
8591
|
}
|
|
8016
8592
|
}
|
|
8017
8593
|
};
|
|
@@ -8023,7 +8599,7 @@ async function packSession(s2, projectDir, podPath, limit = UPLOAD_LIMIT) {
|
|
|
8023
8599
|
const rel = relative(join3(dirname(s2.file), "..", "..", ".."), s2.file);
|
|
8024
8600
|
podDir = `.codex/sessions/${dirname(rel)}`;
|
|
8025
8601
|
const name = rel.split("/").pop();
|
|
8026
|
-
|
|
8602
|
+
writeFileSync3(join3(stage, name), rewriteAll(raw.toString("utf8"), froms, podPath));
|
|
8027
8603
|
names.push(name);
|
|
8028
8604
|
}
|
|
8029
8605
|
const archive = await runTar(stage, ["-czf", "-", ...names], null, limit);
|
|
@@ -8034,7 +8610,7 @@ async function packSession(s2, projectDir, podPath, limit = UPLOAD_LIMIT) {
|
|
|
8034
8610
|
sent: { agent: s2.agent, id: s2.id, file: s2.file, sha256: sha256(raw), localPath: projectDir, podPath }
|
|
8035
8611
|
};
|
|
8036
8612
|
} finally {
|
|
8037
|
-
|
|
8613
|
+
rmSync3(stage, { recursive: true, force: true });
|
|
8038
8614
|
}
|
|
8039
8615
|
}
|
|
8040
8616
|
var resumeCommand = (agent, id) => agent === "claude-code" ? `claude --resume ${id}` : `codex resume ${id}`;
|
|
@@ -8047,7 +8623,7 @@ function bringBack(sent, podContent, now = /* @__PURE__ */ new Date()) {
|
|
|
8047
8623
|
const atomic = (file2, body2) => {
|
|
8048
8624
|
mkdirSync2(dirname(file2), { recursive: true });
|
|
8049
8625
|
const tmp = `${file2}.airgate-${process.pid}`;
|
|
8050
|
-
|
|
8626
|
+
writeFileSync3(tmp, body2, { mode: 384 });
|
|
8051
8627
|
renameSync2(tmp, file2);
|
|
8052
8628
|
};
|
|
8053
8629
|
if (!local || sha256(local) === sent.sha256) {
|
|
@@ -8237,10 +8813,10 @@ var STATE_DIR = ".airgate";
|
|
|
8237
8813
|
var statePath = (projectDir) => join4(projectDir, STATE_DIR, "state.json");
|
|
8238
8814
|
function writeState(projectDir, s2) {
|
|
8239
8815
|
mkdirSync3(join4(projectDir, STATE_DIR), { recursive: true });
|
|
8240
|
-
|
|
8816
|
+
writeFileSync4(statePath(projectDir), JSON.stringify(s2, null, 2) + "\n");
|
|
8241
8817
|
}
|
|
8242
8818
|
function findState(from) {
|
|
8243
|
-
let dir =
|
|
8819
|
+
let dir = resolve5(from);
|
|
8244
8820
|
for (; ; ) {
|
|
8245
8821
|
const p = statePath(dir);
|
|
8246
8822
|
if (existsSync3(p)) {
|
|
@@ -8270,7 +8846,7 @@ function excludeStateDir(projectDir) {
|
|
|
8270
8846
|
return true;
|
|
8271
8847
|
}
|
|
8272
8848
|
function podDirName(projectDir) {
|
|
8273
|
-
const n2 = basename(
|
|
8849
|
+
const n2 = basename(resolve5(projectDir)).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[.-]+/, "");
|
|
8274
8850
|
return n2 && n2 !== "." && n2 !== ".." ? n2.slice(0, 64) : "project";
|
|
8275
8851
|
}
|
|
8276
8852
|
var up = {
|
|
@@ -8304,7 +8880,7 @@ var up = {
|
|
|
8304
8880
|
const noSession = bool(ctx.args, "no-session");
|
|
8305
8881
|
const sessionId = str(ctx.args, "session");
|
|
8306
8882
|
if (noSession && sessionId) throw new UsageError("--session and --no-session together?");
|
|
8307
|
-
const projectDir =
|
|
8883
|
+
const projectDir = resolve5(ctx.io.cwd, ctx.args.positionals[0] ?? ".");
|
|
8308
8884
|
if (!existsSync3(projectDir) || !statSync2(projectDir).isDirectory()) throw new CliError(`no directory ${projectDir}`);
|
|
8309
8885
|
const name = podDirName(projectDir);
|
|
8310
8886
|
const podDir = `workspace/${name}`;
|
|
@@ -8637,7 +9213,7 @@ var LineReader = class {
|
|
|
8637
9213
|
}
|
|
8638
9214
|
};
|
|
8639
9215
|
function ttyPrompt(stdin, out) {
|
|
8640
|
-
return (question, opts2 = {}) => new Promise((
|
|
9216
|
+
return (question, opts2 = {}) => new Promise((resolve9, reject) => {
|
|
8641
9217
|
out.write(question);
|
|
8642
9218
|
const decoder = new StringDecoder2("utf8");
|
|
8643
9219
|
let answer = "";
|
|
@@ -8650,7 +9226,7 @@ function ttyPrompt(stdin, out) {
|
|
|
8650
9226
|
stdin.pause();
|
|
8651
9227
|
out.write("\n");
|
|
8652
9228
|
if (err) reject(err);
|
|
8653
|
-
else
|
|
9229
|
+
else resolve9(answer);
|
|
8654
9230
|
};
|
|
8655
9231
|
const onData = (d) => {
|
|
8656
9232
|
for (const ch of decoder.write(d)) {
|
|
@@ -8677,16 +9253,16 @@ function ttyPrompt(stdin, out) {
|
|
|
8677
9253
|
}
|
|
8678
9254
|
function openInBrowser(url) {
|
|
8679
9255
|
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url]] : ["xdg-open", [url]];
|
|
8680
|
-
return new Promise((
|
|
9256
|
+
return new Promise((resolve9) => {
|
|
8681
9257
|
try {
|
|
8682
9258
|
const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
|
|
8683
|
-
child.on("error", () =>
|
|
9259
|
+
child.on("error", () => resolve9(false));
|
|
8684
9260
|
child.on("spawn", () => {
|
|
8685
9261
|
child.unref();
|
|
8686
|
-
|
|
9262
|
+
resolve9(true);
|
|
8687
9263
|
});
|
|
8688
9264
|
} catch {
|
|
8689
|
-
|
|
9265
|
+
resolve9(false);
|
|
8690
9266
|
}
|
|
8691
9267
|
});
|
|
8692
9268
|
}
|
|
@@ -8780,12 +9356,12 @@ async function runCommand(ctx, cmd, argv, cwd) {
|
|
|
8780
9356
|
}
|
|
8781
9357
|
|
|
8782
9358
|
// src/commands/run.ts
|
|
8783
|
-
import { existsSync as existsSync5, readFileSync as readFileSync8, rmSync as
|
|
8784
|
-
import { basename as basename2, resolve as
|
|
9359
|
+
import { existsSync as existsSync5, readFileSync as readFileSync8, rmSync as rmSync5, statSync as statSync3, writeFileSync as writeFileSync6 } from "node:fs";
|
|
9360
|
+
import { basename as basename2, resolve as resolve6 } from "node:path";
|
|
8785
9361
|
import { randomBytes } from "node:crypto";
|
|
8786
9362
|
|
|
8787
9363
|
// src/relay.ts
|
|
8788
|
-
import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync as renameSync3, writeFileSync as
|
|
9364
|
+
import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
8789
9365
|
import { join as join5 } from "node:path";
|
|
8790
9366
|
function vendorOf(name) {
|
|
8791
9367
|
const n2 = (name ?? "").toLowerCase();
|
|
@@ -8811,7 +9387,7 @@ function saveLocal(env, vendor, value, kind = value ? kindOf(vendor, value) : "a
|
|
|
8811
9387
|
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
8812
9388
|
const file = modelsPath(env);
|
|
8813
9389
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
8814
|
-
|
|
9390
|
+
writeFileSync5(tmp, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
|
|
8815
9391
|
chmodSync2(tmp, 384);
|
|
8816
9392
|
renameSync3(tmp, file);
|
|
8817
9393
|
chmodSync2(file, 384);
|
|
@@ -8946,8 +9522,8 @@ function startRelay(o) {
|
|
|
8946
9522
|
}
|
|
8947
9523
|
|
|
8948
9524
|
// src/commands/models.ts
|
|
8949
|
-
import { spawnSync as
|
|
8950
|
-
import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync7, rmSync as
|
|
9525
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
9526
|
+
import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync7, rmSync as rmSync4 } from "node:fs";
|
|
8951
9527
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
8952
9528
|
import { join as join6 } from "node:path";
|
|
8953
9529
|
var HOW = {
|
|
@@ -8970,14 +9546,14 @@ async function connectChatgpt(ctx, laptop) {
|
|
|
8970
9546
|
if (!ctx.io.stdin.isTTY) {
|
|
8971
9547
|
text2 = await readValue(ctx, "a Codex auth.json", "cat auth.json | airgate models connect chatgpt");
|
|
8972
9548
|
} else {
|
|
8973
|
-
if (
|
|
9549
|
+
if (spawnSync3("codex", ["--version"], { stdio: "ignore" }).status !== 0) {
|
|
8974
9550
|
throw new CliError("Codex is not installed on this laptop", "npm i -g @openai/codex, then: airgate models connect chatgpt");
|
|
8975
9551
|
}
|
|
8976
9552
|
const home = mkdtempSync2(join6(tmpdir2(), "airgate-codex-"));
|
|
8977
9553
|
try {
|
|
8978
9554
|
ui2.err(ui2.ce("dim", "signing in to ChatGPT for airgate: a separate Codex login, so the one on this laptop stays as it is."));
|
|
8979
9555
|
const args = ["login", ...bool(ctx.args, "device") ? ["--device-auth"] : [], "-c", 'cli_auth_credentials_store="file"'];
|
|
8980
|
-
const r2 =
|
|
9556
|
+
const r2 = spawnSync3("codex", args, { stdio: "inherit", env: { ...process.env, ...ctx.io.env, CODEX_HOME: home } });
|
|
8981
9557
|
if (r2.status !== 0) throw new CliError("the ChatGPT sign-in did not finish", "try again, or with --device");
|
|
8982
9558
|
try {
|
|
8983
9559
|
text2 = readFileSync7(join6(home, "auth.json"), "utf8");
|
|
@@ -8985,7 +9561,7 @@ async function connectChatgpt(ctx, laptop) {
|
|
|
8985
9561
|
throw new CliError("Codex didn't write a login file", "update Codex (npm i -g @openai/codex) and try again");
|
|
8986
9562
|
}
|
|
8987
9563
|
} finally {
|
|
8988
|
-
|
|
9564
|
+
rmSync4(home, { recursive: true, force: true });
|
|
8989
9565
|
}
|
|
8990
9566
|
}
|
|
8991
9567
|
let tokens2;
|
|
@@ -9166,6 +9742,8 @@ var runTask = {
|
|
|
9166
9742
|
const minutes = Number(str(ctx.args, "timeout") ?? "60");
|
|
9167
9743
|
if (!Number.isFinite(minutes) || minutes <= 0) throw new UsageError("--timeout: minutes, e.g. 30");
|
|
9168
9744
|
const repo = str(ctx.args, "repo");
|
|
9745
|
+
const same = await checkSimilar(ctx, task2);
|
|
9746
|
+
if (!same.go) notStarted(same.similar);
|
|
9169
9747
|
const warnings = [];
|
|
9170
9748
|
const relayer = relayIfSignedIn(ctx);
|
|
9171
9749
|
if (relayer && !ctx.json) ui2.err(ui2.ce("dim", `this laptop signs the pod's ${relayer.vendors.map(vendorLabel).join(" / ")} calls while it runs (airgate holds nothing)`));
|
|
@@ -9211,7 +9789,7 @@ var runTask = {
|
|
|
9211
9789
|
ui2.out(`${r2.ok ? ui2.c("ok", "done") : ui2.c("bad", "failed")} on ${name2}${r2.done ? " (asleep now, free)" : ""}. look: airgate attach ${name2}${bool(ctx.args, "pr") ? " \xB7 the pull request: airgate airlock" : ""}`);
|
|
9212
9790
|
return r2.ok ? 0 : 1;
|
|
9213
9791
|
}
|
|
9214
|
-
const projectDir =
|
|
9792
|
+
const projectDir = resolve6(ctx.io.cwd, dirArg ?? ".");
|
|
9215
9793
|
if (!existsSync5(projectDir) || !statSync3(projectDir).isDirectory()) throw new CliError(`no directory ${projectDir}`);
|
|
9216
9794
|
const noApply = bool(ctx.args, "no-apply");
|
|
9217
9795
|
if (!noApply && !gitTopLevel(projectDir)) {
|
|
@@ -9221,8 +9799,8 @@ var runTask = {
|
|
|
9221
9799
|
const before = existsSync5(sp) ? readFileSync8(sp, "utf8") : null;
|
|
9222
9800
|
const restoreState = () => {
|
|
9223
9801
|
try {
|
|
9224
|
-
if (before !== null)
|
|
9225
|
-
else
|
|
9802
|
+
if (before !== null) writeFileSync6(sp, before);
|
|
9803
|
+
else rmSync5(sp, { force: true });
|
|
9226
9804
|
} catch {
|
|
9227
9805
|
}
|
|
9228
9806
|
};
|
|
@@ -9282,6 +9860,239 @@ When you are done, leave your changes in the files there (no need to commit or p
|
|
|
9282
9860
|
}
|
|
9283
9861
|
};
|
|
9284
9862
|
|
|
9863
|
+
// src/commands/crews.ts
|
|
9864
|
+
import { existsSync as existsSync6, readFileSync as readFileSync9, realpathSync as realpathSync3, rmSync as rmSync6, statSync as statSync4, writeFileSync as writeFileSync7 } from "node:fs";
|
|
9865
|
+
import { relative as relative3, resolve as resolve7 } from "node:path";
|
|
9866
|
+
var KINDS = ["race", "contest", "council", "squad", "split"];
|
|
9867
|
+
var ACTIONS = ["show", "say", "stop", "down", "rm", "list"];
|
|
9868
|
+
var AGENT_ALIAS = { claude: "claude-code", "claude-code": "claude-code", codex: "codex", opencode: "opencode", pi: "pi" };
|
|
9869
|
+
function membersFrom(values) {
|
|
9870
|
+
const items = values.flatMap((v) => v.split(",")).map((v) => v.trim()).filter(Boolean);
|
|
9871
|
+
if (!items.length) return void 0;
|
|
9872
|
+
return items.map((it) => {
|
|
9873
|
+
const m = /^([a-z][a-z0-9-]*)=([a-z-]+)(?::(.+))?$/.exec(it);
|
|
9874
|
+
if (!m) throw new UsageError(`--as ${it}: role=agent, e.g. reviewer=codex or worker-2=opencode:qwen/qwen3-coder`);
|
|
9875
|
+
const agent = AGENT_ALIAS[m[2]];
|
|
9876
|
+
if (!agent) throw new UsageError(`--as ${it}: the agent is one of claude, codex, opencode, pi`);
|
|
9877
|
+
return { role: m[1], agent, ...m[3] ? { model: m[3] } : {} };
|
|
9878
|
+
});
|
|
9879
|
+
}
|
|
9880
|
+
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);
|
|
9881
|
+
function printRoom(ctx, messages) {
|
|
9882
|
+
for (const m of messages) {
|
|
9883
|
+
const who = m.author === "airgate" ? ctx.ui.c("dim", "airgate") : m.author === "you" ? ctx.ui.c("warn", "you") : m.author;
|
|
9884
|
+
const lines = m.text.split("\n");
|
|
9885
|
+
ctx.ui.out(`${who}: ${lines[0]}`);
|
|
9886
|
+
for (const l of lines.slice(1, 12)) ctx.ui.out(` ${l}`);
|
|
9887
|
+
if (lines.length > 12) ctx.ui.out(ctx.ui.c("dim", ` \u2026 (${lines.length - 12} more lines: airgate crew show)`));
|
|
9888
|
+
}
|
|
9889
|
+
}
|
|
9890
|
+
async function follow(ctx, id) {
|
|
9891
|
+
let since = 0;
|
|
9892
|
+
const controller = new AbortController();
|
|
9893
|
+
const stop = () => controller.abort();
|
|
9894
|
+
ctx.io.signals.on("SIGINT", stop);
|
|
9895
|
+
ctx.io.signal?.addEventListener("abort", stop);
|
|
9896
|
+
try {
|
|
9897
|
+
for (; ; ) {
|
|
9898
|
+
const r = await ctx.api.request("GET", `/v1/crews/${encodeURIComponent(id)}/messages?since=${since}`);
|
|
9899
|
+
printRoom(ctx, r.messages);
|
|
9900
|
+
if (r.messages.length) since = r.messages[r.messages.length - 1].id;
|
|
9901
|
+
if (["done", "failed", "stopped"].includes(r.state)) break;
|
|
9902
|
+
if (controller.signal.aborted) {
|
|
9903
|
+
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}`));
|
|
9904
|
+
break;
|
|
9905
|
+
}
|
|
9906
|
+
await new Promise((res) => setTimeout(res, Number(ctx.io.env.AIRGATE_CREW_POLL_MS) || 2e3));
|
|
9907
|
+
}
|
|
9908
|
+
} finally {
|
|
9909
|
+
ctx.io.signals.off("SIGINT", stop);
|
|
9910
|
+
ctx.io.signal?.removeEventListener("abort", stop);
|
|
9911
|
+
}
|
|
9912
|
+
return (await ctx.api.request("GET", `/v1/crews/${encodeURIComponent(id)}`)).crew;
|
|
9913
|
+
}
|
|
9914
|
+
function printResult(ctx, c) {
|
|
9915
|
+
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`));
|
|
9916
|
+
if (!c.result) return;
|
|
9917
|
+
ctx.ui.out();
|
|
9918
|
+
ctx.ui.out(`${tone(ctx, c.state)}: ${c.result.summary ?? ""}`);
|
|
9919
|
+
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" : ""}`));
|
|
9920
|
+
if (c.result.hasPatch) ctx.ui.out(`bring it back here: airgate crew down ${c.id}`);
|
|
9921
|
+
}
|
|
9922
|
+
async function down3(ctx, id) {
|
|
9923
|
+
const r = await ctx.api.request("GET", `/v1/crews/${encodeURIComponent(id)}/result`);
|
|
9924
|
+
const { blocks, outside } = selectProject(parsePatch(r.patch), r.dir);
|
|
9925
|
+
const refused = refusals(blocks);
|
|
9926
|
+
const patch = blocks.map((b2) => b2.text).join("");
|
|
9927
|
+
const files2 = orderFiles(blocks.map((b2) => b2.file));
|
|
9928
|
+
if (bool(ctx.args, "dry-run")) {
|
|
9929
|
+
if (ctx.json) return ctx.ui.json({ files: files2, outside, refused, patch });
|
|
9930
|
+
return void ctx.io.stdout.write(patch);
|
|
9931
|
+
}
|
|
9932
|
+
if (refused.length) throw new CliError(`refused: ${refused.map((x) => `${x.path} (${x.why})`).join(", ")}`, `nothing applied. look: airgate crew down ${id} --dry-run`);
|
|
9933
|
+
if (!blocks.length) return void ctx.ui.out("nothing to apply.");
|
|
9934
|
+
const top = gitTopLevel(ctx.io.cwd);
|
|
9935
|
+
if (!top) throw new CliError("crew down needs a git repo here", "git init && git add -A && git commit -m base");
|
|
9936
|
+
const rel = relative3(realpathSync3(top), realpathSync3(ctx.io.cwd));
|
|
9937
|
+
const args = ["--whitespace=nowarn", ...rel ? [`--directory=${rel}`] : [], "-"];
|
|
9938
|
+
const check = git(top, ["apply", "--check", ...args], patch);
|
|
9939
|
+
if (check.status !== 0) throw new CliError(`the patch doesn't apply here, nothing changed:
|
|
9940
|
+
${check.stderr.trim()}`, "commit or stash local edits to those files first");
|
|
9941
|
+
const risky = files2.filter((f) => f.risky).length;
|
|
9942
|
+
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 ?? ""}`) : ""}`);
|
|
9943
|
+
if (!await confirm(ctx, `apply ${files2.length} file(s)${risky ? ` (${risky} risky)` : ""}?`)) throw new CliError("nothing applied");
|
|
9944
|
+
const applied = git(top, ["apply", ...args], patch);
|
|
9945
|
+
if (applied.status !== 0) throw new CliError(`git apply failed: ${applied.stderr.trim()}`);
|
|
9946
|
+
if (ctx.json) return ctx.ui.json({ applied: true, files: files2 });
|
|
9947
|
+
ctx.ui.out(`${ctx.ui.c("ok", "applied")} ${files2.length} file(s) from the crew. review: git status \xB7 git diff`);
|
|
9948
|
+
}
|
|
9949
|
+
var crew = {
|
|
9950
|
+
name: "crew",
|
|
9951
|
+
aliases: ["crews"],
|
|
9952
|
+
group: "pods",
|
|
9953
|
+
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>',
|
|
9954
|
+
summary: "several agents, each in its own pod, on one task: race, contest, council, squad, split",
|
|
9955
|
+
example: 'airgate crew contest "add OAuth with GitHub"',
|
|
9956
|
+
notes: [
|
|
9957
|
+
"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.",
|
|
9958
|
+
"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.",
|
|
9959
|
+
"--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).",
|
|
9960
|
+
"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).",
|
|
9961
|
+
"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)."
|
|
9962
|
+
],
|
|
9963
|
+
flags: {
|
|
9964
|
+
pod: { type: "string", value: "pod", help: "start from this pod (its workspace)" },
|
|
9965
|
+
from: { type: "string", value: "snapshot", help: "start from a saved snapshot (name or share code)" },
|
|
9966
|
+
repo: { type: "string", value: "owner/name", help: "start from a GitHub repo" },
|
|
9967
|
+
as: { type: "list", value: "role=agent", help: "a member's agent, e.g. reviewer=codex (repeatable)" },
|
|
9968
|
+
rounds: { type: "string", value: "n", help: "contest / squad rounds (1\u20135)" },
|
|
9969
|
+
minutes: { type: "string", value: "n", help: "time limit for the whole crew (default 60)" },
|
|
9970
|
+
build: { type: "boolean", help: "council: also build the decision" },
|
|
9971
|
+
pr: { type: "boolean", help: "open a pull request with the result (it waits in your airlock)" },
|
|
9972
|
+
delete: { type: "boolean", help: "delete member pods afterwards (default: they sleep)" },
|
|
9973
|
+
detach: { type: "boolean", short: "d", help: "start it and return; don't follow the room" },
|
|
9974
|
+
follow: { type: "boolean", short: "f", help: "show: keep following the room" },
|
|
9975
|
+
"dry-run": { type: "boolean", short: "n", help: "down: print the patch, change nothing" },
|
|
9976
|
+
yes: { type: "boolean", short: "y", help: "don't ask" }
|
|
9977
|
+
},
|
|
9978
|
+
async run(ctx) {
|
|
9979
|
+
const { ui: ui2 } = ctx;
|
|
9980
|
+
const [first, second, third, extra] = ctx.args.positionals;
|
|
9981
|
+
if (!first || first === "list") {
|
|
9982
|
+
const list2 = (await ctx.api.request("GET", "/v1/crews")).crews;
|
|
9983
|
+
if (ctx.json) return ui2.json(list2);
|
|
9984
|
+
if (!list2.length) return void ui2.out(ui2.c("dim", 'no crews yet. start one: airgate crew contest "<task>" (in a project folder)'));
|
|
9985
|
+
ui2.table(
|
|
9986
|
+
list2.map((c2) => [c2.id, c2.recipe, tone(ctx, c2.state), c2.task.length > 48 ? `${c2.task.slice(0, 47)}\u2026` : c2.task]),
|
|
9987
|
+
{ head: ["crew", "recipe", "", "task"] }
|
|
9988
|
+
);
|
|
9989
|
+
return;
|
|
9990
|
+
}
|
|
9991
|
+
if (ACTIONS.includes(first)) {
|
|
9992
|
+
if (!second) throw new UsageError(`airgate crew ${first} <id>`);
|
|
9993
|
+
const id = second;
|
|
9994
|
+
if (first === "show") {
|
|
9995
|
+
if (bool(ctx.args, "follow") && !ctx.json) return void printResult(ctx, await follow(ctx, id));
|
|
9996
|
+
const c2 = (await ctx.api.request("GET", `/v1/crews/${encodeURIComponent(id)}`)).crew;
|
|
9997
|
+
if (ctx.json) return ui2.json(c2);
|
|
9998
|
+
ui2.out(`${c2.id} \xB7 ${c2.recipe} \xB7 ${tone(ctx, c2.state)}${c2.step && !["done", "failed", "stopped"].includes(c2.state) ? ` \xB7 ${c2.step}` : ""}`);
|
|
9999
|
+
ui2.out(`task: ${c2.task}`);
|
|
10000
|
+
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"] });
|
|
10001
|
+
ui2.out();
|
|
10002
|
+
printRoom(ctx, c2.messages ?? []);
|
|
10003
|
+
return void printResult(ctx, c2);
|
|
10004
|
+
}
|
|
10005
|
+
if (first === "say") {
|
|
10006
|
+
const text2 = [third, extra].filter(Boolean).join(" ").trim();
|
|
10007
|
+
if (!text2) throw new UsageError('airgate crew say <id> "what the crew should know"');
|
|
10008
|
+
await ctx.api.request("POST", `/v1/crews/${encodeURIComponent(id)}/messages`, { body: { text: text2 } });
|
|
10009
|
+
return ctx.json ? ui2.json({ ok: true }) : void ui2.out("said to the crew. members read the room between steps.");
|
|
10010
|
+
}
|
|
10011
|
+
if (first === "stop") {
|
|
10012
|
+
const c2 = (await ctx.api.request("POST", `/v1/crews/${encodeURIComponent(id)}/stop`)).crew;
|
|
10013
|
+
return ctx.json ? ui2.json(c2) : void ui2.out(`stopping ${c2.id} at its next step; its pods go to sleep with their work.`);
|
|
10014
|
+
}
|
|
10015
|
+
if (first === "down") return down3(ctx, id);
|
|
10016
|
+
if (first === "rm") {
|
|
10017
|
+
if (!await confirm(ctx, `delete crew ${id} and its member pods?`)) throw new CliError("nothing deleted");
|
|
10018
|
+
await ctx.api.request("DELETE", `/v1/crews/${encodeURIComponent(id)}`);
|
|
10019
|
+
return ctx.json ? ui2.json({ ok: true }) : void ui2.out(`deleted ${id}.`);
|
|
10020
|
+
}
|
|
10021
|
+
}
|
|
10022
|
+
if (!KINDS.includes(first)) throw new UsageError(`"${first}" isn't a recipe or an action`, `recipes: ${KINDS.join(", ")} \xB7 actions: ${ACTIONS.join(", ")}`);
|
|
10023
|
+
const kind = first;
|
|
10024
|
+
const task2 = (second ?? "").trim();
|
|
10025
|
+
if (!task2) throw new UsageError(`what should the crew do? airgate crew ${kind} "<task>"`);
|
|
10026
|
+
if (extra) throw new UsageError(`unexpected "${extra}" (put the task in quotes)`);
|
|
10027
|
+
const same = await checkSimilar(ctx, task2);
|
|
10028
|
+
if (!same.go) notStarted(same.similar);
|
|
10029
|
+
const sources = ["pod", "from", "repo"].filter((k) => str(ctx.args, k));
|
|
10030
|
+
if (sources.length > 1) throw new UsageError("one starting point: --pod, --from or --repo");
|
|
10031
|
+
if (sources.length && third) throw new UsageError("a folder or --pod / --from / --repo, not both");
|
|
10032
|
+
let basePodId;
|
|
10033
|
+
let dir = "";
|
|
10034
|
+
let temporaryBase = false;
|
|
10035
|
+
let restoreState = null;
|
|
10036
|
+
if (str(ctx.args, "pod")) basePodId = (await resolvePod(ctx, str(ctx.args, "pod"))).id;
|
|
10037
|
+
else if (str(ctx.args, "from")) {
|
|
10038
|
+
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)}` })));
|
|
10039
|
+
basePodId = p.id;
|
|
10040
|
+
temporaryBase = true;
|
|
10041
|
+
} else if (str(ctx.args, "repo")) {
|
|
10042
|
+
const repo = str(ctx.args, "repo");
|
|
10043
|
+
const look = await ctx.api.inspectRepo(repo).catch((e) => e instanceof ApiError && e.status === 404 ? null : Promise.reject(e));
|
|
10044
|
+
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");
|
|
10045
|
+
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 };
|
|
10046
|
+
basePodId = (await ui2.spin(`starting the base pod on ${spec.repo}`, async () => waitReady(ctx, await ctx.api.launch(spec)))).id;
|
|
10047
|
+
temporaryBase = true;
|
|
10048
|
+
} else {
|
|
10049
|
+
const projectDir = resolve7(ctx.io.cwd, third ?? ".");
|
|
10050
|
+
if (!existsSync6(projectDir) || !statSync4(projectDir).isDirectory()) throw new CliError(`no directory ${projectDir}`);
|
|
10051
|
+
const sp = statePath(projectDir);
|
|
10052
|
+
const before = existsSync6(sp) ? readFileSync9(sp, "utf8") : null;
|
|
10053
|
+
restoreState = () => {
|
|
10054
|
+
try {
|
|
10055
|
+
if (before !== null) writeFileSync7(sp, before);
|
|
10056
|
+
else rmSync6(sp, { force: true });
|
|
10057
|
+
} catch {
|
|
10058
|
+
}
|
|
10059
|
+
};
|
|
10060
|
+
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));
|
|
10061
|
+
restoreState();
|
|
10062
|
+
basePodId = sent.pod.id;
|
|
10063
|
+
dir = sent.dir;
|
|
10064
|
+
temporaryBase = true;
|
|
10065
|
+
}
|
|
10066
|
+
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") };
|
|
10067
|
+
const members = membersFrom(ctx.args.flags.as ?? []);
|
|
10068
|
+
if (members) body.members = members;
|
|
10069
|
+
if (str(ctx.args, "rounds")) body.rounds = Number(str(ctx.args, "rounds"));
|
|
10070
|
+
if (str(ctx.args, "minutes")) body.maxMinutes = Number(str(ctx.args, "minutes"));
|
|
10071
|
+
let c;
|
|
10072
|
+
try {
|
|
10073
|
+
c = (await ctx.api.request("POST", "/v1/crews", { body })).crew;
|
|
10074
|
+
} catch (e) {
|
|
10075
|
+
if (temporaryBase) await ctx.api.destroy(basePodId).catch(() => void 0);
|
|
10076
|
+
throw e;
|
|
10077
|
+
}
|
|
10078
|
+
if (ctx.json || bool(ctx.args, "detach")) {
|
|
10079
|
+
if (ctx.json) return ui2.json(c);
|
|
10080
|
+
ui2.out(`${ui2.c("ok", "started")} ${c.id} (${c.recipe}): ${c.members.map((m) => `${m.role} ${AGENTS[m.agent]?.label ?? m.agent}`).join(" \xB7 ")}`);
|
|
10081
|
+
return void ui2.out(ui2.c("dim", `follow it: airgate crew show ${c.id} --follow \xB7 in the app: airgate.sh/app/crews`));
|
|
10082
|
+
}
|
|
10083
|
+
ui2.out(`${c.id} \xB7 ${c.recipe} \xB7 ${c.members.map((m) => `${m.role} ${AGENTS[m.agent]?.label ?? m.agent}`).join(" \xB7 ")}`);
|
|
10084
|
+
ui2.out(ui2.c("dim", `following the room (ctrl-c stops following; the crew goes on). in the app: airgate.sh/app/crews`));
|
|
10085
|
+
const relayer = relayIfSignedIn(ctx);
|
|
10086
|
+
try {
|
|
10087
|
+
const end = await follow(ctx, c.id);
|
|
10088
|
+
printResult(ctx, end);
|
|
10089
|
+
return end.state === "failed" ? 1 : 0;
|
|
10090
|
+
} finally {
|
|
10091
|
+
relayer?.stop();
|
|
10092
|
+
}
|
|
10093
|
+
}
|
|
10094
|
+
};
|
|
10095
|
+
|
|
9285
10096
|
// src/commands/mcp.ts
|
|
9286
10097
|
var PROTOCOL_VERSION = "2025-06-18";
|
|
9287
10098
|
var obj = (properties, required = []) => ({ type: "object", properties, required, additionalProperties: false });
|
|
@@ -9305,6 +10116,7 @@ var jobSchema = obj(
|
|
|
9305
10116
|
);
|
|
9306
10117
|
var OWN = /* @__PURE__ */ new Map();
|
|
9307
10118
|
var n = (description) => ({ type: "number", description });
|
|
10119
|
+
var sameWork = async (ctx, task2) => (await ctx.api.similar(task2).catch(() => [])).map((e) => ({ id: e.id, title: e.title, by: e.author, summary: e.summary, reuse: `team_patch id=${e.id}` }));
|
|
9308
10120
|
var dirOf = (ctx, a) => typeof a.dir === "string" && a.dir ? a.dir : ctx.io.cwd;
|
|
9309
10121
|
var TOOLS = [
|
|
9310
10122
|
{
|
|
@@ -9514,9 +10326,10 @@ var TOOLS = [
|
|
|
9514
10326
|
if (a.keep === true) argv.push("--keep");
|
|
9515
10327
|
if (a.pr === true) argv.push("--pr");
|
|
9516
10328
|
if (typeof a.timeoutMinutes === "number") argv.push("--timeout", String(a.timeoutMinutes));
|
|
10329
|
+
const similar = await sameWork(ctx, String(a.task));
|
|
9517
10330
|
const out = await runCommand(ctx, runTask, argv, dirOf(ctx, a));
|
|
9518
10331
|
if (out.pod?.id && out.pod_state !== "deleted") OWN.set(out.pod.id, a.repo ? "launch" : "project");
|
|
9519
|
-
return out;
|
|
10332
|
+
return similar.length ? { ...out, similar } : out;
|
|
9520
10333
|
}
|
|
9521
10334
|
},
|
|
9522
10335
|
{
|
|
@@ -9635,6 +10448,120 @@ var TOOLS = [
|
|
|
9635
10448
|
return { name: pod.spec.name, network: pod.spec.network, allow: pod.spec.allow ?? [], aiOff: Boolean(pod.spec.aiOff) };
|
|
9636
10449
|
}
|
|
9637
10450
|
},
|
|
10451
|
+
{
|
|
10452
|
+
name: "crew_start",
|
|
10453
|
+
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).",
|
|
10454
|
+
inputSchema: obj(
|
|
10455
|
+
{
|
|
10456
|
+
kind: s("race | contest | council | squad | split", { enum: ["race", "contest", "council", "squad", "split"] }),
|
|
10457
|
+
task: s("what the crew should do"),
|
|
10458
|
+
dir: s("the local project to start from (default: where the server started)"),
|
|
10459
|
+
pod: s("start from this existing pod instead"),
|
|
10460
|
+
snapshot: s("start from this saved snapshot instead"),
|
|
10461
|
+
repo: s("start from this GitHub repo instead (owner/name)"),
|
|
10462
|
+
members: { type: "array", items: { type: "string" }, description: 'role=agent[:model] overrides, e.g. ["reviewer=codex", "builder-3=opencode:qwen/qwen3-coder"]' },
|
|
10463
|
+
rounds: n("contest / squad rounds, 1\u20135"),
|
|
10464
|
+
maxMinutes: n("time limit for the whole crew (default 60)"),
|
|
10465
|
+
build: b("council: also build the decision"),
|
|
10466
|
+
pr: b("open a pull request with the result (it waits in the airlock)")
|
|
10467
|
+
},
|
|
10468
|
+
["kind", "task"]
|
|
10469
|
+
),
|
|
10470
|
+
async call(ctx, a) {
|
|
10471
|
+
const argv = [need(a, "kind"), need(a, "task"), "--detach"];
|
|
10472
|
+
const opt = (flag, v) => typeof v === "string" && v && argv.push(`--${flag}`, v);
|
|
10473
|
+
opt("pod", a.pod);
|
|
10474
|
+
opt("from", a.snapshot);
|
|
10475
|
+
opt("repo", a.repo);
|
|
10476
|
+
for (const m of Array.isArray(a.members) ? a.members : []) argv.push("--as", String(m));
|
|
10477
|
+
if (typeof a.rounds === "number") argv.push("--rounds", String(a.rounds));
|
|
10478
|
+
if (typeof a.maxMinutes === "number") argv.push("--minutes", String(a.maxMinutes));
|
|
10479
|
+
if (a.build === true) argv.push("--build");
|
|
10480
|
+
if (a.pr === true) argv.push("--pr");
|
|
10481
|
+
const similar = await sameWork(ctx, String(a.task));
|
|
10482
|
+
const out = await runCommand(ctx, crew, argv, dirOf(ctx, a));
|
|
10483
|
+
return similar.length ? { ...out, similar } : out;
|
|
10484
|
+
}
|
|
10485
|
+
},
|
|
10486
|
+
{
|
|
10487
|
+
name: "team_recall",
|
|
10488
|
+
description: "Search the user's team library: work teammates already did (finished tasks, runs and crews, with a summary, the files and what it cost) and notes they wrote. Before a big task, search for it: reusing a teammate's result (team_patch) saves paying for the same work twice. Empty query: the newest entries. Only for people in a team workspace.",
|
|
10489
|
+
inputSchema: obj({ query: s("a few words about the work") }),
|
|
10490
|
+
async call(ctx, a) {
|
|
10491
|
+
return (await ctx.api.library(typeof a.query === "string" ? a.query : "")).map((e) => ({ id: e.id, kind: e.kind, title: e.title, summary: e.summary, files: e.files, by: e.author, at: new Date(e.at).toISOString(), cost: e.cost }));
|
|
10492
|
+
}
|
|
10493
|
+
},
|
|
10494
|
+
{
|
|
10495
|
+
name: "team_patch",
|
|
10496
|
+
description: "A team library entry's work as a unified diff (a crew's result, or a pod's changes). Apply it in the local repo with git apply after telling the user what it is and who made it.",
|
|
10497
|
+
inputSchema: obj({ id: s("the entry id from team_recall") }, ["id"]),
|
|
10498
|
+
async call(ctx, a) {
|
|
10499
|
+
const r = await ctx.api.libraryPatch(need(a, "id"));
|
|
10500
|
+
return { title: r.entry.title, by: r.entry.author, patch: r.patch };
|
|
10501
|
+
}
|
|
10502
|
+
},
|
|
10503
|
+
{
|
|
10504
|
+
name: "team_note",
|
|
10505
|
+
description: "Save a short note to the team library for every teammate and every new pod in the team (how to run the tests, a gotcha, where things live). Only durable facts, never secrets.",
|
|
10506
|
+
inputSchema: obj({ text: s("the note, one or two sentences") }, ["text"]),
|
|
10507
|
+
async call(ctx, a) {
|
|
10508
|
+
return ctx.api.note(need(a, "text"));
|
|
10509
|
+
}
|
|
10510
|
+
},
|
|
10511
|
+
{
|
|
10512
|
+
name: "pod_share",
|
|
10513
|
+
description: "Share one of the user's pods with their team workspace (teammates can open its terminal, files and checkpoints and fork it; never delete it), or stop sharing. Only when the user asks for it.",
|
|
10514
|
+
inputSchema: obj({ pod: POD, shared: b("true shares it, false makes it private again") }, ["pod", "shared"]),
|
|
10515
|
+
async call(ctx, a) {
|
|
10516
|
+
const p = await resolvePod(ctx, need(a, "pod"));
|
|
10517
|
+
const pod = await ctx.api.sharePod(p.id, a.shared !== false);
|
|
10518
|
+
return { pod: pod.spec.name, shared: Boolean(pod.shared) };
|
|
10519
|
+
}
|
|
10520
|
+
},
|
|
10521
|
+
{
|
|
10522
|
+
name: "pod_handover",
|
|
10523
|
+
description: "Give one of the user's pods to a teammate (it becomes theirs, with its checkpoints and conversation). Only when the user explicitly asks for it, naming the teammate.",
|
|
10524
|
+
inputSchema: obj({ pod: POD, email: s("the teammate's email") }, ["pod", "email"]),
|
|
10525
|
+
async call(ctx, a) {
|
|
10526
|
+
const p = await resolvePod(ctx, need(a, "pod"));
|
|
10527
|
+
return ctx.api.handover(p.id, need(a, "email"));
|
|
10528
|
+
}
|
|
10529
|
+
},
|
|
10530
|
+
{
|
|
10531
|
+
name: "crew_status",
|
|
10532
|
+
description: "A crew's state, members (and their pods), the room's messages after `since` (a message id), and the result once done.",
|
|
10533
|
+
inputSchema: obj({ crew: s("the crew id"), since: n("only messages after this id") }, ["crew"]),
|
|
10534
|
+
async call(ctx, a) {
|
|
10535
|
+
const id = encodeURIComponent(need(a, "crew"));
|
|
10536
|
+
const c = (await ctx.api.request("GET", `/v1/crews/${id}`)).crew;
|
|
10537
|
+
const since = typeof a.since === "number" ? a.since : 0;
|
|
10538
|
+
return { ...c, messages: (c.messages ?? []).filter((m) => m.id > since) };
|
|
10539
|
+
}
|
|
10540
|
+
},
|
|
10541
|
+
{
|
|
10542
|
+
name: "crew_say",
|
|
10543
|
+
description: "Post to a crew's room as the user (members read it between steps). Only what the user asked you to tell them.",
|
|
10544
|
+
inputSchema: obj({ crew: s("the crew id"), text: s("the message") }, ["crew", "text"]),
|
|
10545
|
+
async call(ctx, a) {
|
|
10546
|
+
return ctx.api.request("POST", `/v1/crews/${encodeURIComponent(need(a, "crew"))}/messages`, { body: { text: need(a, "text") } });
|
|
10547
|
+
}
|
|
10548
|
+
},
|
|
10549
|
+
{
|
|
10550
|
+
name: "crew_stop",
|
|
10551
|
+
description: "Stop a crew at its next step; its pods go to sleep with their work.",
|
|
10552
|
+
inputSchema: obj({ crew: s("the crew id") }, ["crew"]),
|
|
10553
|
+
async call(ctx, a) {
|
|
10554
|
+
return ctx.api.request("POST", `/v1/crews/${encodeURIComponent(need(a, "crew"))}/stop`);
|
|
10555
|
+
}
|
|
10556
|
+
},
|
|
10557
|
+
{
|
|
10558
|
+
name: "crew_down",
|
|
10559
|
+
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.",
|
|
10560
|
+
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"]),
|
|
10561
|
+
async call(ctx, a) {
|
|
10562
|
+
return runCommand(ctx, crew, ["down", need(a, "crew"), ...a.apply === false ? ["--dry-run"] : ["--yes"]], dirOf(ctx, a));
|
|
10563
|
+
}
|
|
10564
|
+
},
|
|
9638
10565
|
{
|
|
9639
10566
|
name: "pod_ports",
|
|
9640
10567
|
description: "What is listening in a pod (dev servers on ports 1024+) and its preview links.",
|
|
@@ -9678,7 +10605,7 @@ async function handle(ctx, msg) {
|
|
|
9678
10605
|
protocolVersion: asked,
|
|
9679
10606
|
capabilities: { tools: { listChanged: false } },
|
|
9680
10607
|
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."
|
|
10608
|
+
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. In a team workspace: search team_recall before big work and reuse a teammate's result with team_patch instead of paying for it again; save durable learnings with team_note; never touch teammates' pods beyond reading them."
|
|
9682
10609
|
});
|
|
9683
10610
|
}
|
|
9684
10611
|
case "ping":
|
|
@@ -9746,7 +10673,7 @@ var mcp = {
|
|
|
9746
10673
|
};
|
|
9747
10674
|
|
|
9748
10675
|
// src/commands/misc.ts
|
|
9749
|
-
import { mkdirSync as mkdirSync5, writeFileSync as
|
|
10676
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
9750
10677
|
import { homedir as homedir3 } from "node:os";
|
|
9751
10678
|
import { join as join7 } from "node:path";
|
|
9752
10679
|
var PAGES = ["pods", "airlock", "usage", "settings", "mask", "chat", "notes", "data", "status"];
|
|
@@ -9780,15 +10707,19 @@ var status = {
|
|
|
9780
10707
|
const h = await ctx.ui.spin("checking the gateway", ctx.api.health());
|
|
9781
10708
|
const ms = Date.now() - t0;
|
|
9782
10709
|
let signedIn = null;
|
|
10710
|
+
let verified = null;
|
|
9783
10711
|
if (ctx.api.signedIn) {
|
|
9784
|
-
|
|
10712
|
+
const m = await ctx.api.me().catch(() => null);
|
|
10713
|
+
signedIn = m?.user.email ?? null;
|
|
10714
|
+
const v = m?.user?.emailVerified;
|
|
10715
|
+
verified = typeof v === "boolean" ? v : null;
|
|
9785
10716
|
}
|
|
9786
|
-
if (ctx.json) return ctx.ui.json({ gateway: ctx.cfg.gateway, ...h, latencyMs: ms, signedIn, version: VERSION });
|
|
10717
|
+
if (ctx.json) return ctx.ui.json({ gateway: ctx.cfg.gateway, ...h, latencyMs: ms, signedIn, emailVerified: verified, version: VERSION });
|
|
9787
10718
|
const rows2 = [
|
|
9788
10719
|
["gateway", `${ctx.cfg.gateway} ${h.ok ? ctx.ui.c("ok", "ok") : ctx.ui.c("bad", "down")} ${ctx.ui.c("dim", `${ms}ms`)}`],
|
|
9789
10720
|
["provider", h.provider === "simulator" ? ctx.ui.c("warn", "simulator") : h.provider],
|
|
9790
10721
|
["billing", h.billing ? "on" : "off"],
|
|
9791
|
-
["signed in", signedIn ?? (ctx.api.signedIn ? ctx.ui.c("bad", "token rejected \xB7 airgate login") : ctx.ui.c("warn", "no \xB7 airgate login"))],
|
|
10722
|
+
["signed in", (signedIn ?? (ctx.api.signedIn ? ctx.ui.c("bad", "token rejected \xB7 airgate login") : ctx.ui.c("warn", "no \xB7 airgate login"))) + (signedIn && verified === false ? ctx.ui.c("warn", " \xB7 email not confirmed (airgate verify)") : "")],
|
|
9792
10723
|
["config", configPath(ctx.io.env)]
|
|
9793
10724
|
];
|
|
9794
10725
|
ctx.ui.table(rows2.map(([k, v]) => [ctx.ui.c("dim", k), v]));
|
|
@@ -9827,7 +10758,7 @@ var skill = {
|
|
|
9827
10758
|
const base = bool(ctx.args, "project") ? join7(ctx.io.cwd, ".claude") : join7(ctx.io.env.HOME ?? homedir3(), ".claude");
|
|
9828
10759
|
const dir = join7(base, "skills", "airgate");
|
|
9829
10760
|
mkdirSync5(dir, { recursive: true });
|
|
9830
|
-
|
|
10761
|
+
writeFileSync8(join7(dir, "SKILL.md"), text2);
|
|
9831
10762
|
if (ctx.json) return ctx.ui.json({ installed: join7(dir, "SKILL.md") });
|
|
9832
10763
|
ctx.ui.out(`${ctx.ui.c("ok", "installed")} the airgate skill: ${join7(dir, "SKILL.md")}`);
|
|
9833
10764
|
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".'));
|
|
@@ -9891,11 +10822,22 @@ var preview = {
|
|
|
9891
10822
|
var share = {
|
|
9892
10823
|
name: "share",
|
|
9893
10824
|
group: "pods",
|
|
9894
|
-
usage: "<pod>
|
|
9895
|
-
summary: "a
|
|
9896
|
-
example: "airgate share web
|
|
10825
|
+
usage: "<pod> [port] [--off]",
|
|
10826
|
+
summary: "share a pod with your team (teammates open, watch and fork it), or a port publicly by link; --off undoes it",
|
|
10827
|
+
example: "airgate share web",
|
|
10828
|
+
notes: [
|
|
10829
|
+
"airgate share <pod> your team workspace sees it: its terminal, files, checkpoints; they can fork it, never delete it",
|
|
10830
|
+
"airgate share <pod> <port> a public link to that port: anyone with the link can open it"
|
|
10831
|
+
],
|
|
9897
10832
|
flags: { off: { type: "boolean", help: "stop sharing: private to you again" } },
|
|
9898
10833
|
async run(ctx) {
|
|
10834
|
+
if (ctx.args.positionals.length === 1) {
|
|
10835
|
+
const p2 = await resolvePod(ctx, ctx.args.positionals[0]);
|
|
10836
|
+
const off2 = bool(ctx.args, "off");
|
|
10837
|
+
const out = await ctx.api.sharePod(p2.id, !off2);
|
|
10838
|
+
if (ctx.json) return ctx.ui.json(out);
|
|
10839
|
+
return void ctx.ui.out(off2 ? `${p2.spec.name} is private again.` : `${p2.spec.name} is shared with your team: they see it in airgate pods and can attach (you all share one terminal).`);
|
|
10840
|
+
}
|
|
9899
10841
|
const { pod, port } = await podAndPort(ctx);
|
|
9900
10842
|
const off = bool(ctx.args, "off");
|
|
9901
10843
|
const p = await ctx.api.ensurePreview(pod.id, { port, public: !off });
|
|
@@ -9918,7 +10860,7 @@ var checkpoints = {
|
|
|
9918
10860
|
summary: "the pod's undo timeline, newest first",
|
|
9919
10861
|
example: "airgate checkpoints fix-nav",
|
|
9920
10862
|
async run(ctx) {
|
|
9921
|
-
const p = await resolvePod(ctx, ctx.args.positionals[0]);
|
|
10863
|
+
const p = await resolvePod(ctx, ctx.args.positionals[0], { team: true });
|
|
9922
10864
|
const cps = newestFirst(await ctx.api.checkpoints(p.id));
|
|
9923
10865
|
if (ctx.json) return ctx.ui.json(cps);
|
|
9924
10866
|
if (!cps.length) {
|
|
@@ -9981,7 +10923,7 @@ var race = {
|
|
|
9981
10923
|
},
|
|
9982
10924
|
async run(ctx) {
|
|
9983
10925
|
const [ref, cpRef = "current"] = ctx.args.positionals;
|
|
9984
|
-
const p = await resolvePod(ctx, ref);
|
|
10926
|
+
const p = await resolvePod(ctx, ref, { team: true });
|
|
9985
10927
|
const checkpointId2 = cpRef === "current" ? "current" : resolveCheckpoint(await ctx.api.checkpoints(p.id), cpRef).id;
|
|
9986
10928
|
const overrides = {};
|
|
9987
10929
|
const agent = str(ctx.args, "agent");
|
|
@@ -10441,8 +11383,8 @@ function modalKey(s2, m, key) {
|
|
|
10441
11383
|
}
|
|
10442
11384
|
}
|
|
10443
11385
|
}
|
|
10444
|
-
function toast(s2, text2,
|
|
10445
|
-
const t = { id: s2.nextToast, text: text2, tone, until: s2.now + TOAST_MS };
|
|
11386
|
+
function toast(s2, text2, tone2 = "ok") {
|
|
11387
|
+
const t = { id: s2.nextToast, text: text2, tone: tone2, until: s2.now + TOAST_MS };
|
|
10446
11388
|
return { ...s2, toasts: [...s2.toasts.slice(-2), t], nextToast: s2.nextToast + 1 };
|
|
10447
11389
|
}
|
|
10448
11390
|
function moveSel(s2, d) {
|
|
@@ -11426,26 +12368,26 @@ function startTui(d) {
|
|
|
11426
12368
|
d.stdout.write(LEAVE_ALT + `\x1B[2m attached to ${name}. ctrl-] comes back to airgate.\x1B[0m\r
|
|
11427
12369
|
`);
|
|
11428
12370
|
const { cols, rows: rows2 } = size2();
|
|
11429
|
-
let
|
|
11430
|
-
let
|
|
12371
|
+
let note2 = `back from ${name}. the session keeps running.`;
|
|
12372
|
+
let tone2 = "ok";
|
|
11431
12373
|
try {
|
|
11432
12374
|
const ws = d.websocket(d.api.terminalUrl(podId, cols, rows2));
|
|
11433
12375
|
const end = await attachSession({ stdin: d.stdin, stdout: d.stdout, ws, signals: d.signals });
|
|
11434
|
-
if (end.reason === "exit")
|
|
12376
|
+
if (end.reason === "exit") note2 = `${name}: session ended (exit ${end.code})`;
|
|
11435
12377
|
else if (end.reason === "error" || end.reason === "closed" && end.code !== 1e3) {
|
|
11436
|
-
|
|
11437
|
-
|
|
12378
|
+
note2 = `${name}: terminal ${end.reason === "error" ? end.message : "closed"}`;
|
|
12379
|
+
tone2 = "bad";
|
|
11438
12380
|
}
|
|
11439
12381
|
} catch (e) {
|
|
11440
|
-
|
|
11441
|
-
|
|
12382
|
+
note2 = `couldn't attach: ${errText(e)}`;
|
|
12383
|
+
tone2 = "bad";
|
|
11442
12384
|
}
|
|
11443
12385
|
if (finished) return;
|
|
11444
12386
|
suspended = false;
|
|
11445
12387
|
shown2 = [];
|
|
11446
12388
|
d.stdout.write(ENTER_ALT);
|
|
11447
12389
|
listenKeys(true);
|
|
11448
|
-
dispatch(
|
|
12390
|
+
dispatch(tone2 === "ok" ? { type: "done", text: note2 } : { type: "failed", text: note2 });
|
|
11449
12391
|
}
|
|
11450
12392
|
async function stream() {
|
|
11451
12393
|
for (let attempt = 0; !controller.signal.aborted; attempt++) {
|
|
@@ -11553,8 +12495,8 @@ var ui = {
|
|
|
11553
12495
|
};
|
|
11554
12496
|
|
|
11555
12497
|
// src/commands/workspace.ts
|
|
11556
|
-
import { writeFileSync as
|
|
11557
|
-
import { basename as basename3, resolve as
|
|
12498
|
+
import { writeFileSync as writeFileSync9, existsSync as existsSync7, statSync as statSync5 } from "node:fs";
|
|
12499
|
+
import { basename as basename3, resolve as resolve8 } from "node:path";
|
|
11558
12500
|
async function checkpointId(ctx, podId, ref) {
|
|
11559
12501
|
if (!ref || ref === "current") return void 0;
|
|
11560
12502
|
return resolveCheckpoint(await ctx.api.checkpoints(podId), ref).id;
|
|
@@ -11571,7 +12513,7 @@ var files = {
|
|
|
11571
12513
|
async run(ctx) {
|
|
11572
12514
|
const [ref, path = ""] = ctx.args.positionals;
|
|
11573
12515
|
if (!ref) throw new UsageError("which pod? airgate files <pod> [path]");
|
|
11574
|
-
const pod = await resolvePod(ctx, ref);
|
|
12516
|
+
const pod = await resolvePod(ctx, ref, { team: true });
|
|
11575
12517
|
const at3 = await checkpointId(ctx, pod.id, str(ctx.args, "at"));
|
|
11576
12518
|
const r = await ctx.api.files(pod.id, path, at3);
|
|
11577
12519
|
if (ctx.json) return ctx.ui.json(r);
|
|
@@ -11597,21 +12539,21 @@ var pull = {
|
|
|
11597
12539
|
async run(ctx) {
|
|
11598
12540
|
const [ref, path] = ctx.args.positionals;
|
|
11599
12541
|
if (!ref || !path) throw new UsageError("airgate pull <pod> <path>");
|
|
11600
|
-
const pod = await resolvePod(ctx, ref);
|
|
12542
|
+
const pod = await resolvePod(ctx, ref, { team: true });
|
|
11601
12543
|
const at3 = await checkpointId(ctx, pod.id, str(ctx.args, "at"));
|
|
11602
12544
|
const body = await ctx.ui.spin(`reading ${path}`, ctx.api.fileRaw(pod.id, path, at3));
|
|
11603
12545
|
const out = str(ctx.args, "out") ?? basename3(path);
|
|
11604
12546
|
if (out === "-") return void ctx.io.stdout.write(body);
|
|
11605
|
-
const dest =
|
|
11606
|
-
if (
|
|
11607
|
-
if (
|
|
12547
|
+
const dest = resolve8(ctx.io.cwd, out);
|
|
12548
|
+
if (existsSync7(dest) && !bool(ctx.args, "force")) {
|
|
12549
|
+
if (statSync5(dest).isDirectory()) return void save2(ctx, resolve8(dest, basename3(path)), body);
|
|
11608
12550
|
throw new CliError(`${out} exists here`, "pick another name with -o, or --force to overwrite");
|
|
11609
12551
|
}
|
|
11610
12552
|
save2(ctx, dest, body);
|
|
11611
12553
|
}
|
|
11612
12554
|
};
|
|
11613
12555
|
function save2(ctx, dest, body) {
|
|
11614
|
-
|
|
12556
|
+
writeFileSync9(dest, body);
|
|
11615
12557
|
if (ctx.json) return ctx.ui.json({ saved: dest, bytes: body.length });
|
|
11616
12558
|
ctx.ui.out(`${ctx.ui.c("ok", "saved")} ${dest} (${bytes(body.length)})`);
|
|
11617
12559
|
}
|
|
@@ -11741,6 +12683,8 @@ var COMMANDS = [
|
|
|
11741
12683
|
forgot,
|
|
11742
12684
|
reset,
|
|
11743
12685
|
deleteAccount,
|
|
12686
|
+
verify,
|
|
12687
|
+
team,
|
|
11744
12688
|
github,
|
|
11745
12689
|
pods,
|
|
11746
12690
|
launch,
|
|
@@ -11773,6 +12717,10 @@ var COMMANDS = [
|
|
|
11773
12717
|
runTask,
|
|
11774
12718
|
models,
|
|
11775
12719
|
relay,
|
|
12720
|
+
crew,
|
|
12721
|
+
handover,
|
|
12722
|
+
recall,
|
|
12723
|
+
note,
|
|
11776
12724
|
files,
|
|
11777
12725
|
pull,
|
|
11778
12726
|
snapshot2,
|
|
@@ -11872,6 +12820,10 @@ function report(ui2, err, cmd, env) {
|
|
|
11872
12820
|
line2(err.message.charAt(0).toLowerCase() + err.message.slice(1), "see what this gateway offers: airgate templates");
|
|
11873
12821
|
return 1;
|
|
11874
12822
|
}
|
|
12823
|
+
if (err.code === "team_plan_required" || err.code === "no_seat") {
|
|
12824
|
+
line2(err.message.charAt(0).toLowerCase() + err.message.slice(1), err.code === "no_seat" ? "add one: airgate team seats <n>" : "the owner starts it: airgate team upgrade --seats <n>");
|
|
12825
|
+
return 1;
|
|
12826
|
+
}
|
|
11875
12827
|
if (err.status === 402 || err.code === "billing_required") {
|
|
11876
12828
|
line2(`${err.message}`);
|
|
11877
12829
|
const p = PLANS;
|