airgate 0.1.6 → 0.1.8
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 +20 -0
- package/dist/airgate.mjs +789 -102
- 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,110 @@ 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
|
+
}
|
|
800
|
+
reportBug(text2, expected, context) {
|
|
801
|
+
return this.request("POST", "/v1/bugs", { body: { text: text2, ...expected ? { expected } : {}, ...context ? { context } : {} } });
|
|
802
|
+
}
|
|
675
803
|
terminalUrl(id, cols, rows2) {
|
|
676
804
|
const q = new URLSearchParams({ token: this.tokenForUrl(), cols: String(cols), rows: String(rows2), client: this.client });
|
|
677
805
|
return `${this.gateway.replace(/^http/, "ws")}/v1/pods/${enc(id)}/terminal?${q}`;
|
|
@@ -1956,9 +2084,9 @@ function stack(fn, all, given = {}) {
|
|
|
1956
2084
|
});
|
|
1957
2085
|
}
|
|
1958
2086
|
function over(top, bottom, o = {}) {
|
|
1959
|
-
const { name, note, ...opts2 } = object(o, "over()", [...LAYER.filter((k) => k !== "src"), "name", "note"]);
|
|
2087
|
+
const { name, note: note2, ...opts2 } = object(o, "over()", [...LAYER.filter((k) => k !== "src"), "name", "note"]);
|
|
1960
2088
|
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 }));
|
|
2089
|
+
return stack("over", [layerOf("over", bottom, "top-left"), layerOf("over", upper, "center")], naming("over", { name, note: note2 }));
|
|
1962
2090
|
}
|
|
1963
2091
|
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
2092
|
function borderOf(name, v) {
|
|
@@ -2382,11 +2510,11 @@ function build2(fx, src, o, make, keep = true) {
|
|
|
2382
2510
|
const own = periodOf(p, options ?? {});
|
|
2383
2511
|
const loop = !e.moves ? own : e.period ? lcm(own ?? e.period, e.period) : void 0;
|
|
2384
2512
|
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
|
|
2513
|
+
const note2 = o.note ?? `${name}, ${e.what}`.slice(0, 72);
|
|
2386
2514
|
const still = e.still ?? m.still;
|
|
2387
2515
|
const out = piece({
|
|
2388
2516
|
name,
|
|
2389
|
-
note,
|
|
2517
|
+
note: note2,
|
|
2390
2518
|
category: m.category,
|
|
2391
2519
|
cols,
|
|
2392
2520
|
rows: rows2,
|
|
@@ -3689,11 +3817,11 @@ function checkMeta(m) {
|
|
|
3689
3817
|
return m;
|
|
3690
3818
|
}
|
|
3691
3819
|
var Chainable = class {
|
|
3692
|
-
named(name,
|
|
3693
|
-
return named(this, name,
|
|
3820
|
+
named(name, note2) {
|
|
3821
|
+
return named(this, name, note2 === void 0 ? {} : { note: note2 });
|
|
3694
3822
|
}
|
|
3695
|
-
note(
|
|
3696
|
-
return named(this, this.meta.name, { note });
|
|
3823
|
+
note(note2) {
|
|
3824
|
+
return named(this, this.meta.name, { note: note2 });
|
|
3697
3825
|
}
|
|
3698
3826
|
over(bottom, o) {
|
|
3699
3827
|
return over(this, bottom, o);
|
|
@@ -5050,7 +5178,7 @@ function tones(accent, more = []) {
|
|
|
5050
5178
|
return { light: list2.map((c) => c.light), dark: list2.map((c) => c.dark) };
|
|
5051
5179
|
}
|
|
5052
5180
|
var WIDGET = ["color", ...NAMING];
|
|
5053
|
-
var naming2 = (o, name,
|
|
5181
|
+
var naming2 = (o, name, note2) => ({ name: o.name ?? name, note: (o.note ?? note2).slice(0, 72) });
|
|
5054
5182
|
var framing = (p, framed) => framed ? Object.assign(p, { frames: true }) : p;
|
|
5055
5183
|
function amountOf(what, v, min, max, share2) {
|
|
5056
5184
|
if (v === void 0)
|
|
@@ -5094,8 +5222,8 @@ function progressBar(o) {
|
|
|
5094
5222
|
const cols = label2.length + width + (percent ? 5 : 0);
|
|
5095
5223
|
const share2 = (v) => (v - min) / (max - min);
|
|
5096
5224
|
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",
|
|
5225
|
+
const note2 = live ? "a progress bar, live" : value === void 0 ? "a progress bar filling" : `a progress bar at ${Math.round(share2(value) * 100)}%`;
|
|
5226
|
+
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
5227
|
const k = typeof value === "function" ? share2(value()) : value !== void 0 ? share2(value) : smooth(Math.min(1, (t % loop + loop) % loop / fill));
|
|
5100
5228
|
const color = k >= 1 ? GOOD : ACCENT;
|
|
5101
5229
|
s2.write(0, 0, label2);
|
|
@@ -5134,8 +5262,8 @@ function gauge(o) {
|
|
|
5134
5262
|
const loop = 12;
|
|
5135
5263
|
const wander = (t) => 0.15 + 0.8 * loopNoise(0.3, 0.7, t, loop, { radius: 0.9 });
|
|
5136
5264
|
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",
|
|
5265
|
+
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}`;
|
|
5266
|
+
const made = piece({ ...naming2(p, title || "gauge", note2), category: "data", cols: 25, rows: 12, ...timing, palette: tones(own) }, (t, s2) => {
|
|
5139
5267
|
const k = typeof value === "function" ? (value() - min) / (max - min) : value === void 0 ? wander(t) : (value - min) / (max - min);
|
|
5140
5268
|
const color = own ? ACCENT : k > 0.85 ? BAD2 : k > 0.6 ? WARN : GOOD;
|
|
5141
5269
|
if (framed)
|
|
@@ -5174,8 +5302,8 @@ function sparkline(data, o) {
|
|
|
5174
5302
|
const last = list2 ? String(+list2[list2.length - 1].toFixed(2)) : live ? "100000" : "100";
|
|
5175
5303
|
const cols = words2.length + width + 1 + last.length;
|
|
5176
5304
|
const timing = live ? { fps: 4, clock: true } : { fps: 24, loop, ...list2 ? { still: 1.5 } : {} };
|
|
5177
|
-
const
|
|
5178
|
-
return piece({ ...naming2(p, label2 || "sparkline",
|
|
5305
|
+
const note2 = live ? "a sparkline of live data" : list2 ? "a sparkline drawing itself in" : "a sparkline of a wandering reading";
|
|
5306
|
+
return piece({ ...naming2(p, label2 || "sparkline", note2), category: "data", cols, rows: height, ...timing, palette: tones(themed2("sparkline's color", p.color)) }, (t, s2) => {
|
|
5179
5307
|
const u = (t % loop + loop) % loop;
|
|
5180
5308
|
textAt(s2, words2, "left");
|
|
5181
5309
|
const area2 = { x: words2.length, y: 0, cols: width, rows: height };
|
|
@@ -5717,10 +5845,19 @@ function matchOne(items, ref, keys, what, label2) {
|
|
|
5717
5845
|
if (pre.length > 1) throw new CliError(`"${ref}" matches ${pre.length} ${what}s`, `be more specific: ${pre.map(label2).join(", ")}`);
|
|
5718
5846
|
throw new CliError(`no ${what} "${ref}"`, what === "pod" ? "list them with: airgate pods" : void 0);
|
|
5719
5847
|
}
|
|
5720
|
-
async function resolvePod(ctx, ref) {
|
|
5848
|
+
async function resolvePod(ctx, ref, opts2 = {}) {
|
|
5721
5849
|
if (!ref) throw new UsageError("which pod? give its name or id");
|
|
5722
|
-
|
|
5723
|
-
|
|
5850
|
+
if (!opts2.team) {
|
|
5851
|
+
const pods3 = await ctx.api.pods();
|
|
5852
|
+
return matchOne(pods3, ref, (p) => [p.id, p.spec.name], "pod", (p) => `${p.spec.name} (${p.id})`);
|
|
5853
|
+
}
|
|
5854
|
+
const { pods: pods2, team: team2 } = await ctx.api.podsWithTeam();
|
|
5855
|
+
try {
|
|
5856
|
+
return matchOne(pods2, ref, (p) => [p.id, p.spec.name], "pod", (p) => `${p.spec.name} (${p.id})`);
|
|
5857
|
+
} catch (e) {
|
|
5858
|
+
if (!team2.length || !(e instanceof CliError) || !e.message.startsWith("no pod")) throw e;
|
|
5859
|
+
return matchOne(team2, ref, (p) => [p.id, p.spec.name], "pod", (p) => `${p.spec.name} (${p.id}, ${p.owner?.email ?? "a teammate"}'s)`);
|
|
5860
|
+
}
|
|
5724
5861
|
}
|
|
5725
5862
|
function resolveCheckpoint(cps, ref) {
|
|
5726
5863
|
if (/^#\d+$/.test(ref)) {
|
|
@@ -6755,8 +6892,8 @@ function approvalRows(ctx, items, names) {
|
|
|
6755
6892
|
return items.map((a) => [
|
|
6756
6893
|
ctx.ui.c(a.state === "pending" ? "warn" : a.state === "denied" || a.state === "failed" ? "bad" : "ok", a.state),
|
|
6757
6894
|
a.kind,
|
|
6758
|
-
names.get(a.podId) ?? a.podId,
|
|
6759
|
-
a.summary,
|
|
6895
|
+
names.get(a.podId) ?? (a.requestedBy ? `${a.requestedBy}'s pod` : a.podId),
|
|
6896
|
+
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
6897
|
ago(a.createdAt),
|
|
6761
6898
|
ctx.ui.c("dim", a.id)
|
|
6762
6899
|
]);
|
|
@@ -7003,7 +7140,7 @@ var attach = {
|
|
|
7003
7140
|
if (!io.stdin.isTTY || !io.stdout.isTTY) {
|
|
7004
7141
|
throw new CliError("attach needs an interactive terminal", "for scripts use: airgate exec <pod> -- <command\u2026>", 2);
|
|
7005
7142
|
}
|
|
7006
|
-
const p = await resolvePod(ctx, ctx.args.positionals[0]);
|
|
7143
|
+
const p = await resolvePod(ctx, ctx.args.positionals[0], { team: true });
|
|
7007
7144
|
const url = ctx.api.terminalUrl(p.id, io.stdout.columns ?? 80, io.stdout.rows ?? 24);
|
|
7008
7145
|
ui2.err(ui2.ce("dim", `attached to ${p.spec.name}. ctrl-] to detach.`));
|
|
7009
7146
|
let ws;
|
|
@@ -7242,6 +7379,461 @@ var secrets = {
|
|
|
7242
7379
|
}
|
|
7243
7380
|
};
|
|
7244
7381
|
|
|
7382
|
+
// src/commands/team.ts
|
|
7383
|
+
import { spawnSync } from "node:child_process";
|
|
7384
|
+
import { rmSync as rmSync2, writeFileSync as writeFileSync2 } from "node:fs";
|
|
7385
|
+
import { resolve as resolve4 } from "node:path";
|
|
7386
|
+
|
|
7387
|
+
// src/version.ts
|
|
7388
|
+
var VERSION = "0.1.8";
|
|
7389
|
+
|
|
7390
|
+
// src/commands/team.ts
|
|
7391
|
+
async function checkSimilar(ctx, task2) {
|
|
7392
|
+
const similar = await ctx.api.similar(task2).catch(() => []);
|
|
7393
|
+
if (!similar.length || ctx.json) return { go: true, similar };
|
|
7394
|
+
const { ui: ui2 } = ctx;
|
|
7395
|
+
for (const e of similar) {
|
|
7396
|
+
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` : ""}`)}`);
|
|
7397
|
+
ui2.err(ui2.ce("dim", ` reuse it: airgate recall --patch ${e.id}${e.summary ? ` \xB7 ${e.summary.split("\n")[0].slice(0, 100)}` : ""}`));
|
|
7398
|
+
}
|
|
7399
|
+
if (ctx.args.flags.yes === true || !ctx.io.stdin.isTTY) return { go: true, similar };
|
|
7400
|
+
const a = await ctx.io.prompt("start anyway? [y/N] ").catch(() => "y");
|
|
7401
|
+
return { go: /^y(es)?$/i.test(a.trim()), similar };
|
|
7402
|
+
}
|
|
7403
|
+
function notStarted(similar) {
|
|
7404
|
+
throw new CliError("not started", similar[0] ? `reuse it: airgate recall --patch ${similar[0].id} --apply` : void 0);
|
|
7405
|
+
}
|
|
7406
|
+
async function mine(ctx) {
|
|
7407
|
+
const v = await ctx.api.team();
|
|
7408
|
+
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>');
|
|
7409
|
+
return v;
|
|
7410
|
+
}
|
|
7411
|
+
function member(v, ref) {
|
|
7412
|
+
if (!ref) throw new UsageError("which member? their email");
|
|
7413
|
+
return matchOne(v.members ?? [], ref.toLowerCase(), (m) => [m.id, m.email], "member", (m) => m.email);
|
|
7414
|
+
}
|
|
7415
|
+
var onOff = (v, what) => {
|
|
7416
|
+
if (v === "on" || v === "yes" || v === "true") return true;
|
|
7417
|
+
if (v === "off" || v === "no" || v === "false") return false;
|
|
7418
|
+
throw new UsageError(`${what}: on or off`);
|
|
7419
|
+
};
|
|
7420
|
+
var csvList = (v) => v === "any" || v === "all" ? null : v.split(",").map((x) => x.trim()).filter(Boolean);
|
|
7421
|
+
var numOrNone = (v, what) => {
|
|
7422
|
+
if (v === "none" || v === "off") return null;
|
|
7423
|
+
const n2 = Number(v);
|
|
7424
|
+
if (!Number.isFinite(n2) || n2 < 0) throw new UsageError(`${what}: a number, or none`);
|
|
7425
|
+
return n2;
|
|
7426
|
+
};
|
|
7427
|
+
function printTeam(ctx, v) {
|
|
7428
|
+
const { ui: ui2 } = ctx;
|
|
7429
|
+
const ws = v.workspace;
|
|
7430
|
+
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)" : ""}` : ""}`)}`);
|
|
7431
|
+
ui2.table(
|
|
7432
|
+
(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)}`)]),
|
|
7433
|
+
{ indent: 2 }
|
|
7434
|
+
);
|
|
7435
|
+
if (v.requests?.length) {
|
|
7436
|
+
ui2.out();
|
|
7437
|
+
ui2.out(`${v.requests.length} asking to join:`);
|
|
7438
|
+
ui2.table(v.requests.map((r) => [r.email, ui2.c("dim", ago(r.createdAt)), ui2.c("dim", `airgate team accept ${r.email}`)]), { indent: 2 });
|
|
7439
|
+
}
|
|
7440
|
+
}
|
|
7441
|
+
var team = {
|
|
7442
|
+
name: "team",
|
|
7443
|
+
aliases: ["workspace"],
|
|
7444
|
+
group: "account",
|
|
7445
|
+
usage: "[create|invite|invites|join|leave|members|role|approver|remove|accept|decline|domain|rules|audit|billing|upgrade|seats|secrets|secret|triggers|trigger|delete] \u2026",
|
|
7446
|
+
summary: "your team workspace: members, invites, rules, audit log, billing, team secrets, GitHub triggers",
|
|
7447
|
+
example: "airgate team invite sam@acme.dev",
|
|
7448
|
+
notes: [
|
|
7449
|
+
"airgate team the workspace, your role, its members",
|
|
7450
|
+
'airgate team create "<name>" start one (you own it)',
|
|
7451
|
+
"airgate team invite [email] [--admin] an invite (emailed when you give an address); no email: a link anyone can use",
|
|
7452
|
+
"airgate team invites \xB7 revoke <code> open invites",
|
|
7453
|
+
"airgate team join <code|link> join with an invite (your pods and snapshots come with you)",
|
|
7454
|
+
"airgate team request ask to join the workspace at your email's domain",
|
|
7455
|
+
"airgate team accept|decline <email> answer someone asking to join",
|
|
7456
|
+
"airgate team leave leave (your pods stay yours; shared ones stop being shared)",
|
|
7457
|
+
"airgate team role <email> admin|member|owner \xB7 approver <email> on|off \xB7 remove <email>",
|
|
7458
|
+
"airgate team domain <domain> [--no-auto-join] | --release people with a confirmed email there join on their own",
|
|
7459
|
+
"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]",
|
|
7460
|
+
"airgate team audit [--csv] who did what (admins)",
|
|
7461
|
+
"airgate team billing \xB7 upgrade [--seats n] \xB7 seats <n> the team plan: seats and its pooled hours",
|
|
7462
|
+
"airgate team secrets \xB7 secret set NAME (value from a hidden prompt or stdin) \xB7 secret rm NAME reach every member's pods",
|
|
7463
|
+
"airgate team triggers \xB7 trigger add <owner/repo> pr|issue [--label l] [--recipe r] \xB7 trigger rm <id> crews started by GitHub"
|
|
7464
|
+
],
|
|
7465
|
+
flags: {
|
|
7466
|
+
admin: { type: "boolean", help: "invite: as an admin" },
|
|
7467
|
+
"auto-join": { type: "boolean", default: true, help: "domain: people at it join on their own (--no-auto-join: they ask)" },
|
|
7468
|
+
release: { type: "boolean", help: "domain: let the domain go" },
|
|
7469
|
+
approvers: { type: "string", value: "on|off", help: "rules: an approver (not the requester) decides airlock requests" },
|
|
7470
|
+
kinds: { type: "string", value: "kinds", help: "rules: which requests need an approver (git-push,pull-request,deploy,email,http)" },
|
|
7471
|
+
agents: { type: "string", value: "list|any", help: "rules: agents members may launch" },
|
|
7472
|
+
networks: { type: "string", value: "list|any", help: "rules: networks members may use (open,allowlist,off)" },
|
|
7473
|
+
"max-pods": { type: "string", value: "n|none", help: "rules: awake pods per member" },
|
|
7474
|
+
cap: { type: "string", value: "usd|none", help: "rules: monthly spend cap per member" },
|
|
7475
|
+
"auto-seats": { type: "string", value: "on|off", help: "rules: buy a seat when someone joins past the count" },
|
|
7476
|
+
hints: { type: "string", value: "on|off", help: "rules: offer the same work from the library before new work starts" },
|
|
7477
|
+
csv: { type: "boolean", help: "audit: as CSV" },
|
|
7478
|
+
seats: { type: "string", value: "n", help: "upgrade: how many seats (at least everyone in it)" },
|
|
7479
|
+
label: { type: "string", value: "label", help: "trigger: the issue label that starts a crew (default airgate)" },
|
|
7480
|
+
recipe: { type: "string", value: "recipe", help: "trigger: race, contest, council, squad or split" },
|
|
7481
|
+
yes: { type: "boolean", short: "y", help: "don't ask" },
|
|
7482
|
+
"no-open": { type: "boolean", help: "print the link instead of opening the browser" }
|
|
7483
|
+
},
|
|
7484
|
+
async run(ctx) {
|
|
7485
|
+
const { ui: ui2 } = ctx;
|
|
7486
|
+
const [action, a1, a2] = ctx.args.positionals;
|
|
7487
|
+
if (!action) {
|
|
7488
|
+
const v = await ctx.api.team();
|
|
7489
|
+
if (ctx.json) return ui2.json(v);
|
|
7490
|
+
if (!v.workspace) {
|
|
7491
|
+
ui2.out(ui2.c("dim", "you're not in a team workspace."));
|
|
7492
|
+
for (const i of v.invites) ui2.out(`invited to ${ui2.c("ok", i.workspace.name)} as ${i.role}: airgate team join ${i.code}`);
|
|
7493
|
+
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"}`);
|
|
7494
|
+
return void ui2.out(ui2.c("dim", 'start one: airgate team create "<name>"'));
|
|
7495
|
+
}
|
|
7496
|
+
return printTeam(ctx, v);
|
|
7497
|
+
}
|
|
7498
|
+
switch (action) {
|
|
7499
|
+
case "create": {
|
|
7500
|
+
if (!a1) throw new UsageError('airgate team create "<name>"');
|
|
7501
|
+
const v = await ctx.api.createTeam(a1);
|
|
7502
|
+
if (ctx.json) return ui2.json(v);
|
|
7503
|
+
ui2.out(`${ui2.c("ok", "created")} ${v.workspace.name}. you own it.`);
|
|
7504
|
+
return void ui2.out(ui2.c("dim", "next: airgate team invite <email> \xB7 airgate team domain <your domain> \xB7 airgate team upgrade"));
|
|
7505
|
+
}
|
|
7506
|
+
case "invite": {
|
|
7507
|
+
const inv = await ctx.api.invite(a1 ?? null, bool(ctx.args, "admin") ? "admin" : "member");
|
|
7508
|
+
if (ctx.json) return ui2.json(inv);
|
|
7509
|
+
ui2.out(inv.url);
|
|
7510
|
+
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}`));
|
|
7511
|
+
}
|
|
7512
|
+
case "invites": {
|
|
7513
|
+
const list2 = await ctx.api.teamInvites();
|
|
7514
|
+
if (ctx.json) return ui2.json(list2);
|
|
7515
|
+
if (!list2.length) return void ui2.out(ui2.c("dim", "no open invites. make one: airgate team invite [email]"));
|
|
7516
|
+
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"] });
|
|
7517
|
+
}
|
|
7518
|
+
case "revoke": {
|
|
7519
|
+
if (!a1) throw new UsageError("airgate team revoke <code>");
|
|
7520
|
+
await ctx.api.revokeInvite(a1);
|
|
7521
|
+
return ctx.json ? ui2.json({ ok: true }) : void ui2.out("revoked.");
|
|
7522
|
+
}
|
|
7523
|
+
case "join": {
|
|
7524
|
+
if (!a1) throw new UsageError("airgate team join <code or link>");
|
|
7525
|
+
const code3 = /code=([A-Za-z0-9_-]+)/.exec(a1)?.[1] ?? a1.trim();
|
|
7526
|
+
const peek = await ctx.api.peekInvite(code3);
|
|
7527
|
+
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");
|
|
7528
|
+
const v = await ctx.api.joinTeam(code3);
|
|
7529
|
+
if (ctx.json) return ui2.json(v);
|
|
7530
|
+
ui2.out(`${ui2.c("ok", "joined")} ${v.workspace.name}.`);
|
|
7531
|
+
return void ui2.out(ui2.c("dim", "teammates' shared pods: airgate pods \xB7 the team library: airgate recall"));
|
|
7532
|
+
}
|
|
7533
|
+
case "request": {
|
|
7534
|
+
const r = await ctx.api.requestJoin();
|
|
7535
|
+
return ctx.json ? ui2.json(r) : void ui2.out("asked. an admin of the workspace at your domain decides.");
|
|
7536
|
+
}
|
|
7537
|
+
case "accept":
|
|
7538
|
+
case "decline": {
|
|
7539
|
+
const v = await mine(ctx);
|
|
7540
|
+
const req = matchOne(v.requests ?? [], (a1 ?? "").toLowerCase(), (r) => [r.id, r.email], "request", (r) => r.email);
|
|
7541
|
+
const out = await ctx.api.decideRequest(req.id, action === "accept");
|
|
7542
|
+
return ctx.json ? ui2.json(out) : void ui2.out(`${action === "accept" ? "added" : "declined"} ${req.email}.`);
|
|
7543
|
+
}
|
|
7544
|
+
case "leave": {
|
|
7545
|
+
const v = await mine(ctx);
|
|
7546
|
+
if (!await confirm(ctx, `leave ${v.workspace.name}? your pods stay yours; shared ones stop being shared`)) throw new CliError("still in it");
|
|
7547
|
+
await ctx.api.leaveTeam();
|
|
7548
|
+
return ctx.json ? ui2.json({ ok: true }) : void ui2.out(`left ${v.workspace.name}.`);
|
|
7549
|
+
}
|
|
7550
|
+
case "delete": {
|
|
7551
|
+
const v = await mine(ctx);
|
|
7552
|
+
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");
|
|
7553
|
+
await ctx.api.deleteTeam();
|
|
7554
|
+
return ctx.json ? ui2.json({ ok: true }) : void ui2.out("deleted. cancel the plan: airgate billing");
|
|
7555
|
+
}
|
|
7556
|
+
case "members": {
|
|
7557
|
+
const v = await mine(ctx);
|
|
7558
|
+
return ctx.json ? ui2.json(v.members) : printTeam(ctx, v);
|
|
7559
|
+
}
|
|
7560
|
+
case "role": {
|
|
7561
|
+
const v = await mine(ctx);
|
|
7562
|
+
const m = member(v, a1);
|
|
7563
|
+
if (a2 !== "admin" && a2 !== "member" && a2 !== "owner") throw new UsageError("airgate team role <email> admin|member|owner");
|
|
7564
|
+
if (a2 === "owner" && !await confirm(ctx, `make ${m.email} the owner? you stay an admin`)) throw new CliError("nothing changed");
|
|
7565
|
+
const out = await ctx.api.setMember(m.id, { role: a2 });
|
|
7566
|
+
return ctx.json ? ui2.json(out) : void ui2.out(`${m.email} is ${a2} now.`);
|
|
7567
|
+
}
|
|
7568
|
+
case "approver": {
|
|
7569
|
+
const v = await mine(ctx);
|
|
7570
|
+
const m = member(v, a1);
|
|
7571
|
+
const on = onOff(a2, "approver");
|
|
7572
|
+
const out = await ctx.api.setMember(m.id, { approver: on });
|
|
7573
|
+
return ctx.json ? ui2.json(out) : void ui2.out(`${m.email} ${on ? "decides" : "no longer decides"} teammates' airlock requests.`);
|
|
7574
|
+
}
|
|
7575
|
+
case "remove": {
|
|
7576
|
+
const v = await mine(ctx);
|
|
7577
|
+
const m = member(v, a1);
|
|
7578
|
+
if (!await confirm(ctx, `remove ${m.email} from ${v.workspace.name}? their pods stay theirs`)) throw new CliError("nothing changed");
|
|
7579
|
+
const out = await ctx.api.removeMember(m.id);
|
|
7580
|
+
return ctx.json ? ui2.json(out) : void ui2.out(`removed ${m.email}.`);
|
|
7581
|
+
}
|
|
7582
|
+
case "domain": {
|
|
7583
|
+
if (bool(ctx.args, "release")) {
|
|
7584
|
+
const out2 = await ctx.api.claimDomain(null);
|
|
7585
|
+
return ctx.json ? ui2.json(out2) : void ui2.out("the domain is free again; nobody joins by email.");
|
|
7586
|
+
}
|
|
7587
|
+
if (!a1) throw new UsageError("airgate team domain <domain> [--no-auto-join]");
|
|
7588
|
+
const out = await ctx.api.claimDomain(a1, bool(ctx.args, "auto-join"));
|
|
7589
|
+
if (ctx.json) return ui2.json(out);
|
|
7590
|
+
return void ui2.out(
|
|
7591
|
+
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.`
|
|
7592
|
+
);
|
|
7593
|
+
}
|
|
7594
|
+
case "rules": {
|
|
7595
|
+
const patch = {};
|
|
7596
|
+
const f = (k) => str(ctx.args, k);
|
|
7597
|
+
if (f("approvers") !== void 0) patch.approvals = onOff(f("approvers"), "--approvers") ? "approvers" : "self";
|
|
7598
|
+
if (f("kinds") !== void 0) patch.approvalKinds = csvList(f("kinds")) ?? ["git-push", "pull-request", "deploy", "email", "http"];
|
|
7599
|
+
if (f("agents") !== void 0) patch.agents = csvList(f("agents"));
|
|
7600
|
+
if (f("networks") !== void 0) patch.networks = csvList(f("networks"));
|
|
7601
|
+
if (f("max-pods") !== void 0) patch.maxPodsPerMember = numOrNone(f("max-pods"), "--max-pods");
|
|
7602
|
+
if (f("cap") !== void 0) patch.capUsdPerMember = numOrNone(f("cap"), "--cap");
|
|
7603
|
+
if (f("auto-seats") !== void 0) patch.autoSeats = onOff(f("auto-seats"), "--auto-seats");
|
|
7604
|
+
if (f("hints") !== void 0) patch.libraryHints = onOff(f("hints"), "--hints");
|
|
7605
|
+
const v = Object.keys(patch).length ? await ctx.api.updateTeam({ rules: patch }) : await mine(ctx);
|
|
7606
|
+
const r = v.workspace.rules;
|
|
7607
|
+
if (ctx.json) return ui2.json(r);
|
|
7608
|
+
return void ui2.table(
|
|
7609
|
+
[
|
|
7610
|
+
["airlock", r.approvals === "approvers" ? `an approver decides ${r.approvalKinds.join(", ")}` : "each member decides their own"],
|
|
7611
|
+
["agents", r.agents ? r.agents.join(", ") : "any"],
|
|
7612
|
+
["networks", r.networks ? r.networks.join(", ") : "any"],
|
|
7613
|
+
["pods awake", r.maxPodsPerMember == null ? "the plan's limit" : `${r.maxPodsPerMember} per member`],
|
|
7614
|
+
["spend cap", r.capUsdPerMember == null ? "none" : `$${r.capUsdPerMember} per member a month`],
|
|
7615
|
+
["seats", r.autoSeats ? "bought as people join" : "an admin adds them"],
|
|
7616
|
+
["library hints", r.libraryHints ? "on: the same work is offered before it starts" : "off"]
|
|
7617
|
+
].map(([k, val]) => [ui2.c("dim", k), val])
|
|
7618
|
+
);
|
|
7619
|
+
}
|
|
7620
|
+
case "audit": {
|
|
7621
|
+
if (bool(ctx.args, "csv")) return void ctx.io.stdout.write(await ctx.api.auditCsv());
|
|
7622
|
+
const list2 = await ctx.api.audit();
|
|
7623
|
+
if (ctx.json) return ui2.json(list2);
|
|
7624
|
+
if (!list2.length) return void ui2.out(ui2.c("dim", "nothing yet."));
|
|
7625
|
+
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"] });
|
|
7626
|
+
}
|
|
7627
|
+
case "billing": {
|
|
7628
|
+
const b2 = await ctx.api.teamBilling();
|
|
7629
|
+
if (ctx.json) return ui2.json(b2);
|
|
7630
|
+
if (b2.plan !== "team") {
|
|
7631
|
+
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`)}`);
|
|
7632
|
+
return void ui2.out(ui2.c("dim", "start it (the owner): airgate team upgrade --seats <n>"));
|
|
7633
|
+
}
|
|
7634
|
+
return void ui2.table(
|
|
7635
|
+
[
|
|
7636
|
+
["plan", `team \xB7 ${b2.status ?? "active"}`],
|
|
7637
|
+
["seats", `${b2.seats.used} used of ${b2.seats.paid ?? b2.seats.used} paid`],
|
|
7638
|
+
["pooled hours", `${(b2.seats.paid ?? b2.seats.used) * b2.price.hoursPerSeat} h this month`],
|
|
7639
|
+
["renews", b2.periodEnd ? new Date(b2.periodEnd).toISOString().slice(0, 10) : "?"],
|
|
7640
|
+
["paid by", b2.payer?.you ? "you" : b2.payer?.email ?? "the owner"]
|
|
7641
|
+
].map(([k, val]) => [ui2.c("dim", k), val])
|
|
7642
|
+
);
|
|
7643
|
+
}
|
|
7644
|
+
case "upgrade": {
|
|
7645
|
+
const v = await mine(ctx);
|
|
7646
|
+
const seats = Math.max((v.members ?? []).length, Number(str(ctx.args, "seats") ?? a1 ?? (v.members ?? []).length) || 1);
|
|
7647
|
+
const { url } = await ctx.api.teamCheckout(seats, "https://airgate.sh/app/team");
|
|
7648
|
+
return openOrPrint(ctx, url, `the team plan checkout (${seats} seats)`);
|
|
7649
|
+
}
|
|
7650
|
+
case "seats": {
|
|
7651
|
+
const n2 = Number(a1);
|
|
7652
|
+
if (!Number.isInteger(n2) || n2 < 1) throw new UsageError("airgate team seats <n>");
|
|
7653
|
+
const r = await ctx.api.setSeats(n2);
|
|
7654
|
+
return ctx.json ? ui2.json(r) : void ui2.out(`${r.seats.paid} seats from now on (prorated). ${r.seats.used} in use.`);
|
|
7655
|
+
}
|
|
7656
|
+
case "secrets": {
|
|
7657
|
+
const list2 = await ctx.api.teamSecrets();
|
|
7658
|
+
if (ctx.json) return ui2.json(list2);
|
|
7659
|
+
if (!list2.length) return void ui2.out(ui2.c("dim", "no team secrets. add one (admins): airgate team secret set NAME"));
|
|
7660
|
+
ui2.table(list2.map((x) => [x.name, ui2.c("dim", `updated ${ago(x.updatedAt)}`)]));
|
|
7661
|
+
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"));
|
|
7662
|
+
}
|
|
7663
|
+
case "secret": {
|
|
7664
|
+
if (a1 === "set") {
|
|
7665
|
+
if (!a2) throw new UsageError("airgate team secret set NAME (the value comes from a hidden prompt or stdin)");
|
|
7666
|
+
const value = await readValue(ctx, a2, `printf %s "$VALUE" | airgate team secret set ${a2}`);
|
|
7667
|
+
const r = await ctx.api.setTeamSecrets({ [a2]: value });
|
|
7668
|
+
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.`);
|
|
7669
|
+
}
|
|
7670
|
+
if (a1 === "rm" || a1 === "remove" || a1 === "delete") {
|
|
7671
|
+
if (!a2) throw new UsageError("airgate team secret rm NAME");
|
|
7672
|
+
await ctx.api.deleteTeamSecret(a2);
|
|
7673
|
+
return ctx.json ? ui2.json({ ok: true }) : void ui2.out(`removed ${a2} from the team.`);
|
|
7674
|
+
}
|
|
7675
|
+
throw new UsageError("airgate team secret set NAME | rm NAME");
|
|
7676
|
+
}
|
|
7677
|
+
case "triggers": {
|
|
7678
|
+
const list2 = await ctx.api.triggers();
|
|
7679
|
+
if (ctx.json) return ui2.json(list2);
|
|
7680
|
+
if (!list2.length) return void ui2.out(ui2.c("dim", "no triggers. add one: airgate team trigger add <owner/repo> pr|issue [--recipe squad]"));
|
|
7681
|
+
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"] });
|
|
7682
|
+
}
|
|
7683
|
+
case "trigger": {
|
|
7684
|
+
if (a1 === "add") {
|
|
7685
|
+
const [, , repo, when] = ctx.args.positionals;
|
|
7686
|
+
if (!repo || when !== "pr" && when !== "issue") throw new UsageError("airgate team trigger add <owner/repo> pr|issue [--label l] [--recipe r]");
|
|
7687
|
+
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") } : {} });
|
|
7688
|
+
if (ctx.json) return ui2.json(t);
|
|
7689
|
+
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.`);
|
|
7690
|
+
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"));
|
|
7691
|
+
}
|
|
7692
|
+
if (a1 === "rm" || a1 === "remove") {
|
|
7693
|
+
if (!a2) throw new UsageError("airgate team trigger rm <id>");
|
|
7694
|
+
await ctx.api.removeTrigger(a2);
|
|
7695
|
+
return ctx.json ? ui2.json({ ok: true }) : void ui2.out("removed.");
|
|
7696
|
+
}
|
|
7697
|
+
throw new UsageError("airgate team trigger add \u2026 | rm <id>");
|
|
7698
|
+
}
|
|
7699
|
+
default:
|
|
7700
|
+
throw new UsageError(`unknown "${action}"`, "see: airgate help team");
|
|
7701
|
+
}
|
|
7702
|
+
}
|
|
7703
|
+
};
|
|
7704
|
+
var handover = {
|
|
7705
|
+
name: "handover",
|
|
7706
|
+
group: "pods",
|
|
7707
|
+
usage: "<pod> <email>",
|
|
7708
|
+
summary: "give a pod to a teammate, with its checkpoints, conversation and pod secrets",
|
|
7709
|
+
example: "airgate handover web sam@acme.dev",
|
|
7710
|
+
flags: { yes: { type: "boolean", short: "y", help: "don't ask" } },
|
|
7711
|
+
async run(ctx) {
|
|
7712
|
+
const [ref, to] = ctx.args.positionals;
|
|
7713
|
+
if (!ref || !to) throw new UsageError("airgate handover <pod> <email>");
|
|
7714
|
+
const p = await resolvePod(ctx, ref, { team: true });
|
|
7715
|
+
if (!await confirm(ctx, `give ${p.spec.name} to ${to}? it becomes theirs`)) throw new CliError("nothing changed");
|
|
7716
|
+
const r = await ctx.api.handover(p.id, to);
|
|
7717
|
+
return ctx.json ? ctx.ui.json(r) : void ctx.ui.out(`${p.spec.name} is ${r.to.email}'s now.`);
|
|
7718
|
+
}
|
|
7719
|
+
};
|
|
7720
|
+
var recall = {
|
|
7721
|
+
name: "recall",
|
|
7722
|
+
aliases: ["library"],
|
|
7723
|
+
group: "pods",
|
|
7724
|
+
usage: '["query"] | --patch <id> [--apply]',
|
|
7725
|
+
summary: "your team's library: work teammates already did (reuse it instead of paying again) and their notes",
|
|
7726
|
+
example: 'airgate recall "rate limiting"',
|
|
7727
|
+
notes: [
|
|
7728
|
+
"every finished task, run and crew in a team workspace lands here, with what it cost.",
|
|
7729
|
+
"--patch <id> prints that work as a patch; --apply puts it into this folder with git apply (after asking)."
|
|
7730
|
+
],
|
|
7731
|
+
flags: {
|
|
7732
|
+
patch: { type: "string", value: "id", help: "print an entry's work as a patch" },
|
|
7733
|
+
apply: { type: "boolean", help: "with --patch: apply it here (git apply)" },
|
|
7734
|
+
yes: { type: "boolean", short: "y", help: "don't ask" }
|
|
7735
|
+
},
|
|
7736
|
+
async run(ctx) {
|
|
7737
|
+
const { ui: ui2 } = ctx;
|
|
7738
|
+
const id = str(ctx.args, "patch");
|
|
7739
|
+
if (id) {
|
|
7740
|
+
const { patch, entry } = await ctx.api.libraryPatch(id);
|
|
7741
|
+
if (!bool(ctx.args, "apply")) {
|
|
7742
|
+
if (ctx.json) return ui2.json({ patch, entry });
|
|
7743
|
+
return void ctx.io.stdout.write(patch);
|
|
7744
|
+
}
|
|
7745
|
+
if (!patch.trim()) throw new CliError("that work has no changes to apply");
|
|
7746
|
+
const file = resolve4(ctx.io.cwd, `.airgate-recall-${id}.patch`);
|
|
7747
|
+
writeFileSync2(file, patch);
|
|
7748
|
+
try {
|
|
7749
|
+
const check = spawnSync("git", ["apply", "--check", file], { cwd: ctx.io.cwd, encoding: "utf8" });
|
|
7750
|
+
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);
|
|
7751
|
+
const files2 = (patch.match(/^\+\+\+ b\/(.+)$/gm) ?? []).map((l) => l.slice(6));
|
|
7752
|
+
if (!await confirm(ctx, `apply "${entry.title}" by ${entry.author} here (${files2.length} file${files2.length === 1 ? "" : "s"})?`)) throw new CliError("nothing applied");
|
|
7753
|
+
const r = spawnSync("git", ["apply", file], { cwd: ctx.io.cwd, encoding: "utf8" });
|
|
7754
|
+
if (r.status !== 0) throw new CliError("git apply failed", (r.stderr || "").trim());
|
|
7755
|
+
if (ctx.json) return ui2.json({ applied: true, files: files2 });
|
|
7756
|
+
return void ui2.out(`${ui2.c("ok", "applied")} ${files2.length} file${files2.length === 1 ? "" : "s"}: ${files2.slice(0, 8).join(", ")}${files2.length > 8 ? ", \u2026" : ""}`);
|
|
7757
|
+
} finally {
|
|
7758
|
+
rmSync2(file, { force: true });
|
|
7759
|
+
}
|
|
7760
|
+
}
|
|
7761
|
+
const q = ctx.args.positionals.join(" ");
|
|
7762
|
+
let entries;
|
|
7763
|
+
try {
|
|
7764
|
+
entries = await ctx.api.library(q);
|
|
7765
|
+
} catch (e) {
|
|
7766
|
+
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");
|
|
7767
|
+
throw e;
|
|
7768
|
+
}
|
|
7769
|
+
if (ctx.json) return ui2.json(entries);
|
|
7770
|
+
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"));
|
|
7771
|
+
for (const e of entries) {
|
|
7772
|
+
const cost = e.cost?.tokens ? ` \xB7 ${e.cost.tokens} tokens` : e.cost?.activeSeconds ? ` \xB7 ${Math.round(e.cost.activeSeconds / 60)} min` : "";
|
|
7773
|
+
ui2.out(`${ui2.c("ok", e.title)} ${ui2.c("dim", `${e.kind} by ${e.author}, ${ago(e.at)}${cost} \xB7 ${e.id}`)}`);
|
|
7774
|
+
if (e.summary) ui2.out(` ${e.summary.split("\n")[0].slice(0, 160)}`);
|
|
7775
|
+
if (e.files?.length) ui2.out(ui2.c("dim", ` ${e.files.slice(0, 6).join(", ")}${e.files.length > 6 ? ", \u2026" : ""}`));
|
|
7776
|
+
}
|
|
7777
|
+
ui2.out(ui2.c("dim", "reuse one: airgate recall --patch <id> --apply"));
|
|
7778
|
+
}
|
|
7779
|
+
};
|
|
7780
|
+
var note = {
|
|
7781
|
+
name: "note",
|
|
7782
|
+
group: "pods",
|
|
7783
|
+
usage: '"<what the team should know>"',
|
|
7784
|
+
summary: "save a note to your team library; every new pod in the team reads it",
|
|
7785
|
+
example: 'airgate note "tests need DATABASE_URL=sqlite::memory:"',
|
|
7786
|
+
async run(ctx) {
|
|
7787
|
+
const text2 = ctx.args.positionals.join(" ").trim();
|
|
7788
|
+
if (!text2) throw new UsageError('airgate note "<what the team should know>"');
|
|
7789
|
+
const r = await ctx.api.note(text2);
|
|
7790
|
+
return ctx.json ? ctx.ui.json(r) : void ctx.ui.out("saved for the team.");
|
|
7791
|
+
}
|
|
7792
|
+
};
|
|
7793
|
+
var verify = {
|
|
7794
|
+
name: "verify",
|
|
7795
|
+
group: "account",
|
|
7796
|
+
usage: "",
|
|
7797
|
+
summary: "send the link that confirms your email again (it also adds you to your team at that domain)",
|
|
7798
|
+
example: "airgate verify",
|
|
7799
|
+
async run(ctx) {
|
|
7800
|
+
const me = await ctx.api.me();
|
|
7801
|
+
if (me.user.emailVerified) return ctx.json ? ctx.ui.json({ sent: false, verified: true }) : void ctx.ui.out(`${me.user.email} is confirmed already.`);
|
|
7802
|
+
const r = await ctx.api.sendVerification();
|
|
7803
|
+
if (ctx.json) return ctx.ui.json(r);
|
|
7804
|
+
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");
|
|
7805
|
+
}
|
|
7806
|
+
};
|
|
7807
|
+
var bug = {
|
|
7808
|
+
name: "bug",
|
|
7809
|
+
aliases: ["report"],
|
|
7810
|
+
group: "account",
|
|
7811
|
+
usage: '"<what happened>" [--expected "<what you expected>"]',
|
|
7812
|
+
summary: "report a bug: a person reads every report, and you get an email when it changes",
|
|
7813
|
+
example: 'airgate bug "airgate down said the patch was empty after a crew"',
|
|
7814
|
+
notes: [
|
|
7815
|
+
"it sends your words plus the cli version, your OS and node version, and the folder name (never its contents). --no-context sends only your words.",
|
|
7816
|
+
"or pipe a longer report in: airgate bug < report.txt"
|
|
7817
|
+
],
|
|
7818
|
+
flags: {
|
|
7819
|
+
expected: { type: "string", value: "text", help: "what you expected instead" },
|
|
7820
|
+
"no-context": { type: "boolean", help: "don't send the cli version, OS and folder name" }
|
|
7821
|
+
},
|
|
7822
|
+
async run(ctx) {
|
|
7823
|
+
let text2 = ctx.args.positionals.join(" ").trim();
|
|
7824
|
+
if (!text2 && !ctx.io.stdin.isTTY) {
|
|
7825
|
+
const chunks = [];
|
|
7826
|
+
for await (const c of ctx.io.stdin) chunks.push(Buffer.from(c));
|
|
7827
|
+
text2 = Buffer.concat(chunks).toString("utf8").trim();
|
|
7828
|
+
}
|
|
7829
|
+
if (text2.length < 5) throw new UsageError('airgate bug "<what happened>"', 'a sentence is enough; add --expected "<what you expected>"');
|
|
7830
|
+
const context = bool(ctx.args, "no-context") ? void 0 : { cli: VERSION, os: `${process.platform} ${process.arch}`, node: process.version, folder: ctx.io.cwd.split("/").pop() ?? "" };
|
|
7831
|
+
const r = await ctx.api.reportBug(text2.slice(0, 8e3), str(ctx.args, "expected"), context);
|
|
7832
|
+
if (ctx.json) return ctx.ui.json(r);
|
|
7833
|
+
ctx.ui.out(`${ctx.ui.c("ok", "reported")} ${r.bug.id}. a person reads it; you get an email when it changes.`);
|
|
7834
|
+
}
|
|
7835
|
+
};
|
|
7836
|
+
|
|
7245
7837
|
// src/commands/pods.ts
|
|
7246
7838
|
import { execFileSync } from "node:child_process";
|
|
7247
7839
|
import { readFileSync as readFileSync3 } from "node:fs";
|
|
@@ -7308,19 +7900,35 @@ var pods = {
|
|
|
7308
7900
|
aliases: ["ls"],
|
|
7309
7901
|
group: "pods",
|
|
7310
7902
|
usage: "",
|
|
7311
|
-
summary: "list your pods",
|
|
7903
|
+
summary: "list your pods, and the ones teammates shared with your team",
|
|
7312
7904
|
example: "airgate ls --json",
|
|
7905
|
+
notes: ["--json: your pods as a list (pods teammates shared are in airgate ls --team --json)."],
|
|
7906
|
+
flags: { team: { type: "boolean", help: "with --json: { pods, team } including teammates' shared pods" } },
|
|
7313
7907
|
async run(ctx) {
|
|
7314
|
-
const all = await ctx.api.
|
|
7315
|
-
if (ctx.json) return ctx.ui.json(all);
|
|
7316
|
-
if (!all.length) {
|
|
7908
|
+
const { pods: all, team: team2 } = await ctx.api.podsWithTeam();
|
|
7909
|
+
if (ctx.json) return ctx.ui.json(ctx.args.flags.team === true ? { pods: all, team: team2 } : all);
|
|
7910
|
+
if (!all.length && !team2.length) {
|
|
7317
7911
|
ctx.ui.out("no pods yet. launch one: airgate launch");
|
|
7318
7912
|
return;
|
|
7319
7913
|
}
|
|
7320
|
-
|
|
7321
|
-
|
|
7322
|
-
|
|
7323
|
-
|
|
7914
|
+
if (all.length) {
|
|
7915
|
+
ctx.ui.table(
|
|
7916
|
+
all.map((p) => {
|
|
7917
|
+
const row = podRow(ctx, p);
|
|
7918
|
+
if (p.shared) row[0] += ctx.ui.c("dim", " (team)");
|
|
7919
|
+
return row;
|
|
7920
|
+
}),
|
|
7921
|
+
{ head: ["name", "id", "state", "agent", "size", "template", "active", "created"] }
|
|
7922
|
+
);
|
|
7923
|
+
} else ctx.ui.out(ctx.ui.c("dim", "no pods of your own yet. launch one: airgate launch"));
|
|
7924
|
+
if (team2.length) {
|
|
7925
|
+
ctx.ui.out();
|
|
7926
|
+
ctx.ui.out(ctx.ui.c("dim", "shared by teammates (open: airgate attach <name> \xB7 copy: airgate race <name>):"));
|
|
7927
|
+
ctx.ui.table(
|
|
7928
|
+
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`) : ""]),
|
|
7929
|
+
{ indent: 2 }
|
|
7930
|
+
);
|
|
7931
|
+
}
|
|
7324
7932
|
}
|
|
7325
7933
|
};
|
|
7326
7934
|
var launch = {
|
|
@@ -7476,7 +8084,7 @@ var info = {
|
|
|
7476
8084
|
summary: "details of one pod",
|
|
7477
8085
|
example: "airgate info fix-nav",
|
|
7478
8086
|
async run(ctx) {
|
|
7479
|
-
const p = await resolvePod(ctx, ctx.args.positionals[0]);
|
|
8087
|
+
const p = await resolvePod(ctx, ctx.args.positionals[0], { team: true });
|
|
7480
8088
|
if (ctx.json) return ctx.ui.json(p);
|
|
7481
8089
|
const s2 = p.spec;
|
|
7482
8090
|
const rows2 = [
|
|
@@ -7496,6 +8104,9 @@ var info = {
|
|
|
7496
8104
|
for (const [i, j] of (s2.jobs ?? []).entries()) {
|
|
7497
8105
|
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
8106
|
}
|
|
8107
|
+
if (p.owner) rows2.push(["owner", `${p.owner.email} (shared with your team)`]);
|
|
8108
|
+
else if (p.shared) rows2.push(["team", "shared with your workspace"]);
|
|
8109
|
+
if (p.viewers?.length) rows2.push(["in it now", p.viewers.map((v) => v.email).join(", ")]);
|
|
7499
8110
|
if (p.forkOf) rows2.push(["race of", `${p.forkOf.podId} @ ${p.forkOf.checkpointId}`]);
|
|
7500
8111
|
rows2.push(["checkpoints", String(p.snapshots.length)]);
|
|
7501
8112
|
rows2.push(["active", duration2(p.seconds)]);
|
|
@@ -7695,13 +8306,15 @@ var task = {
|
|
|
7695
8306
|
usage: '<pod> "<task>" [--wait]',
|
|
7696
8307
|
summary: "hand the pod's agent a task now; it runs headless and logs like a job",
|
|
7697
8308
|
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" } },
|
|
8309
|
+
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
8310
|
async run(ctx) {
|
|
7700
8311
|
const [ref, ...words2] = ctx.args.positionals;
|
|
7701
8312
|
const text2 = (ctx.args.dashdash ? ctx.args.rest : words2).join(" ").trim();
|
|
7702
8313
|
if (!text2) throw new UsageError("what should the agent do?", 'airgate task <pod> "<task>"');
|
|
7703
8314
|
if (text2.length > 4e3) throw new UsageError("the task is 4000 characters at most");
|
|
7704
8315
|
const p = await resolvePod(ctx, ref);
|
|
8316
|
+
const same = await checkSimilar(ctx, text2);
|
|
8317
|
+
if (!same.go) notStarted(same.similar);
|
|
7705
8318
|
let run2 = await ctx.ui.spin(`handing ${p.spec.name} the task`, ctx.api.startTask(p.id, text2));
|
|
7706
8319
|
if (ctx.args.flags.wait === true) {
|
|
7707
8320
|
run2 = await ctx.ui.spin(`${p.spec.name} is working`, () => waitRun(ctx, p.id, run2));
|
|
@@ -7776,16 +8389,13 @@ var log = {
|
|
|
7776
8389
|
// src/commands/mcp.ts
|
|
7777
8390
|
import { createInterface } from "node:readline";
|
|
7778
8391
|
|
|
7779
|
-
// src/version.ts
|
|
7780
|
-
var VERSION = "0.1.6";
|
|
7781
|
-
|
|
7782
8392
|
// src/commands/updown.ts
|
|
7783
|
-
import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync2, writeFileSync as
|
|
8393
|
+
import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync2, writeFileSync as writeFileSync4 } from "node:fs";
|
|
7784
8394
|
import { homedir as homedir2 } from "node:os";
|
|
7785
|
-
import { basename, dirname as dirname2, isAbsolute, join as join4, relative as relative2, resolve as
|
|
8395
|
+
import { basename, dirname as dirname2, isAbsolute, join as join4, relative as relative2, resolve as resolve5 } from "node:path";
|
|
7786
8396
|
|
|
7787
8397
|
// src/pack.ts
|
|
7788
|
-
import { spawn, spawnSync } from "node:child_process";
|
|
8398
|
+
import { spawn, spawnSync as spawnSync2 } from "node:child_process";
|
|
7789
8399
|
import { existsSync, lstatSync, readdirSync } from "node:fs";
|
|
7790
8400
|
import { join as join2 } from "node:path";
|
|
7791
8401
|
var UPLOAD_LIMIT = 32 * 6 * 1024 * 1024;
|
|
@@ -7802,7 +8412,7 @@ function isEnvFile(path) {
|
|
|
7802
8412
|
}
|
|
7803
8413
|
var ENV_EXCLUDES = [".env", ".env.local", ".env.*.local", ".env.development", ".env.production", ".env.test", ".env.staging", ".envrc"].map((p) => `--exclude=${p}`);
|
|
7804
8414
|
function git(cwd, args, input) {
|
|
7805
|
-
const r =
|
|
8415
|
+
const r = spawnSync2("git", args, { cwd, input, maxBuffer: 256 * 1024 * 1024 });
|
|
7806
8416
|
return { status: r.status ?? (r.error ? 127 : 1), stdout: r.stdout?.toString("utf8") ?? "", stderr: r.stderr?.toString("utf8") ?? "", error: r.error };
|
|
7807
8417
|
}
|
|
7808
8418
|
function isGitRepo(dir) {
|
|
@@ -7829,7 +8439,7 @@ function gitFiles(dir) {
|
|
|
7829
8439
|
return out;
|
|
7830
8440
|
}
|
|
7831
8441
|
function runTar(cwd, args, input, limit = UPLOAD_LIMIT) {
|
|
7832
|
-
return new Promise((
|
|
8442
|
+
return new Promise((resolve9, reject) => {
|
|
7833
8443
|
const child = spawn("tar", args, { cwd, env: { ...process.env, COPYFILE_DISABLE: "1" }, stdio: ["pipe", "pipe", "pipe"] });
|
|
7834
8444
|
const chunks = [];
|
|
7835
8445
|
let size2 = 0;
|
|
@@ -7849,7 +8459,7 @@ function runTar(cwd, args, input, limit = UPLOAD_LIMIT) {
|
|
|
7849
8459
|
child.on("close", (code3) => {
|
|
7850
8460
|
if (over2) return reject(new TooLargeError(limit));
|
|
7851
8461
|
if (code3 !== 0) return reject(new Error(`tar failed: ${err.trim().split("\n").slice(-3).join(" ")}`));
|
|
7852
|
-
|
|
8462
|
+
resolve9(Buffer.concat(chunks));
|
|
7853
8463
|
});
|
|
7854
8464
|
child.stdin.on("error", () => void 0);
|
|
7855
8465
|
child.stdin.end(input ?? "");
|
|
@@ -7889,7 +8499,7 @@ import { gunzipSync } from "node:zlib";
|
|
|
7889
8499
|
|
|
7890
8500
|
// src/sessions.ts
|
|
7891
8501
|
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
|
|
8502
|
+
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
8503
|
import { tmpdir } from "node:os";
|
|
7894
8504
|
import { dirname, join as join3, relative } from "node:path";
|
|
7895
8505
|
var POD_HOME = "/home/sprite";
|
|
@@ -7999,7 +8609,7 @@ async function packSession(s2, projectDir, podPath, limit = UPLOAD_LIMIT) {
|
|
|
7999
8609
|
const names = [];
|
|
8000
8610
|
if (s2.agent === "claude-code") {
|
|
8001
8611
|
podDir = `.claude/projects/${claudeKey(podPath)}`;
|
|
8002
|
-
|
|
8612
|
+
writeFileSync3(join3(stage, `${s2.id}.jsonl`), rewriteAll(raw.toString("utf8"), froms, podPath));
|
|
8003
8613
|
names.push(`${s2.id}.jsonl`);
|
|
8004
8614
|
if (s2.sidecar) {
|
|
8005
8615
|
const copy2 = (src, dst) => {
|
|
@@ -8011,7 +8621,7 @@ async function packSession(s2, projectDir, podPath, limit = UPLOAD_LIMIT) {
|
|
|
8011
8621
|
copy2(a, b2);
|
|
8012
8622
|
} else if (x.isFile()) {
|
|
8013
8623
|
const body = readFileSync4(a);
|
|
8014
|
-
|
|
8624
|
+
writeFileSync3(b2, /\.(jsonl?|txt|md)$/.test(x.name) ? rewriteAll(body.toString("utf8"), froms, podPath) : body);
|
|
8015
8625
|
}
|
|
8016
8626
|
}
|
|
8017
8627
|
};
|
|
@@ -8023,7 +8633,7 @@ async function packSession(s2, projectDir, podPath, limit = UPLOAD_LIMIT) {
|
|
|
8023
8633
|
const rel = relative(join3(dirname(s2.file), "..", "..", ".."), s2.file);
|
|
8024
8634
|
podDir = `.codex/sessions/${dirname(rel)}`;
|
|
8025
8635
|
const name = rel.split("/").pop();
|
|
8026
|
-
|
|
8636
|
+
writeFileSync3(join3(stage, name), rewriteAll(raw.toString("utf8"), froms, podPath));
|
|
8027
8637
|
names.push(name);
|
|
8028
8638
|
}
|
|
8029
8639
|
const archive = await runTar(stage, ["-czf", "-", ...names], null, limit);
|
|
@@ -8034,7 +8644,7 @@ async function packSession(s2, projectDir, podPath, limit = UPLOAD_LIMIT) {
|
|
|
8034
8644
|
sent: { agent: s2.agent, id: s2.id, file: s2.file, sha256: sha256(raw), localPath: projectDir, podPath }
|
|
8035
8645
|
};
|
|
8036
8646
|
} finally {
|
|
8037
|
-
|
|
8647
|
+
rmSync3(stage, { recursive: true, force: true });
|
|
8038
8648
|
}
|
|
8039
8649
|
}
|
|
8040
8650
|
var resumeCommand = (agent, id) => agent === "claude-code" ? `claude --resume ${id}` : `codex resume ${id}`;
|
|
@@ -8047,7 +8657,7 @@ function bringBack(sent, podContent, now = /* @__PURE__ */ new Date()) {
|
|
|
8047
8657
|
const atomic = (file2, body2) => {
|
|
8048
8658
|
mkdirSync2(dirname(file2), { recursive: true });
|
|
8049
8659
|
const tmp = `${file2}.airgate-${process.pid}`;
|
|
8050
|
-
|
|
8660
|
+
writeFileSync3(tmp, body2, { mode: 384 });
|
|
8051
8661
|
renameSync2(tmp, file2);
|
|
8052
8662
|
};
|
|
8053
8663
|
if (!local || sha256(local) === sent.sha256) {
|
|
@@ -8237,10 +8847,10 @@ var STATE_DIR = ".airgate";
|
|
|
8237
8847
|
var statePath = (projectDir) => join4(projectDir, STATE_DIR, "state.json");
|
|
8238
8848
|
function writeState(projectDir, s2) {
|
|
8239
8849
|
mkdirSync3(join4(projectDir, STATE_DIR), { recursive: true });
|
|
8240
|
-
|
|
8850
|
+
writeFileSync4(statePath(projectDir), JSON.stringify(s2, null, 2) + "\n");
|
|
8241
8851
|
}
|
|
8242
8852
|
function findState(from) {
|
|
8243
|
-
let dir =
|
|
8853
|
+
let dir = resolve5(from);
|
|
8244
8854
|
for (; ; ) {
|
|
8245
8855
|
const p = statePath(dir);
|
|
8246
8856
|
if (existsSync3(p)) {
|
|
@@ -8270,7 +8880,7 @@ function excludeStateDir(projectDir) {
|
|
|
8270
8880
|
return true;
|
|
8271
8881
|
}
|
|
8272
8882
|
function podDirName(projectDir) {
|
|
8273
|
-
const n2 = basename(
|
|
8883
|
+
const n2 = basename(resolve5(projectDir)).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[.-]+/, "");
|
|
8274
8884
|
return n2 && n2 !== "." && n2 !== ".." ? n2.slice(0, 64) : "project";
|
|
8275
8885
|
}
|
|
8276
8886
|
var up = {
|
|
@@ -8304,7 +8914,7 @@ var up = {
|
|
|
8304
8914
|
const noSession = bool(ctx.args, "no-session");
|
|
8305
8915
|
const sessionId = str(ctx.args, "session");
|
|
8306
8916
|
if (noSession && sessionId) throw new UsageError("--session and --no-session together?");
|
|
8307
|
-
const projectDir =
|
|
8917
|
+
const projectDir = resolve5(ctx.io.cwd, ctx.args.positionals[0] ?? ".");
|
|
8308
8918
|
if (!existsSync3(projectDir) || !statSync2(projectDir).isDirectory()) throw new CliError(`no directory ${projectDir}`);
|
|
8309
8919
|
const name = podDirName(projectDir);
|
|
8310
8920
|
const podDir = `workspace/${name}`;
|
|
@@ -8637,7 +9247,7 @@ var LineReader = class {
|
|
|
8637
9247
|
}
|
|
8638
9248
|
};
|
|
8639
9249
|
function ttyPrompt(stdin, out) {
|
|
8640
|
-
return (question, opts2 = {}) => new Promise((
|
|
9250
|
+
return (question, opts2 = {}) => new Promise((resolve9, reject) => {
|
|
8641
9251
|
out.write(question);
|
|
8642
9252
|
const decoder = new StringDecoder2("utf8");
|
|
8643
9253
|
let answer = "";
|
|
@@ -8650,7 +9260,7 @@ function ttyPrompt(stdin, out) {
|
|
|
8650
9260
|
stdin.pause();
|
|
8651
9261
|
out.write("\n");
|
|
8652
9262
|
if (err) reject(err);
|
|
8653
|
-
else
|
|
9263
|
+
else resolve9(answer);
|
|
8654
9264
|
};
|
|
8655
9265
|
const onData = (d) => {
|
|
8656
9266
|
for (const ch of decoder.write(d)) {
|
|
@@ -8677,16 +9287,16 @@ function ttyPrompt(stdin, out) {
|
|
|
8677
9287
|
}
|
|
8678
9288
|
function openInBrowser(url) {
|
|
8679
9289
|
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url]] : ["xdg-open", [url]];
|
|
8680
|
-
return new Promise((
|
|
9290
|
+
return new Promise((resolve9) => {
|
|
8681
9291
|
try {
|
|
8682
9292
|
const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
|
|
8683
|
-
child.on("error", () =>
|
|
9293
|
+
child.on("error", () => resolve9(false));
|
|
8684
9294
|
child.on("spawn", () => {
|
|
8685
9295
|
child.unref();
|
|
8686
|
-
|
|
9296
|
+
resolve9(true);
|
|
8687
9297
|
});
|
|
8688
9298
|
} catch {
|
|
8689
|
-
|
|
9299
|
+
resolve9(false);
|
|
8690
9300
|
}
|
|
8691
9301
|
});
|
|
8692
9302
|
}
|
|
@@ -8780,12 +9390,12 @@ async function runCommand(ctx, cmd, argv, cwd) {
|
|
|
8780
9390
|
}
|
|
8781
9391
|
|
|
8782
9392
|
// src/commands/run.ts
|
|
8783
|
-
import { existsSync as existsSync5, readFileSync as readFileSync8, rmSync as
|
|
8784
|
-
import { basename as basename2, resolve as
|
|
9393
|
+
import { existsSync as existsSync5, readFileSync as readFileSync8, rmSync as rmSync5, statSync as statSync3, writeFileSync as writeFileSync6 } from "node:fs";
|
|
9394
|
+
import { basename as basename2, resolve as resolve6 } from "node:path";
|
|
8785
9395
|
import { randomBytes } from "node:crypto";
|
|
8786
9396
|
|
|
8787
9397
|
// src/relay.ts
|
|
8788
|
-
import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync as renameSync3, writeFileSync as
|
|
9398
|
+
import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync as renameSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
8789
9399
|
import { join as join5 } from "node:path";
|
|
8790
9400
|
function vendorOf(name) {
|
|
8791
9401
|
const n2 = (name ?? "").toLowerCase();
|
|
@@ -8811,7 +9421,7 @@ function saveLocal(env, vendor, value, kind = value ? kindOf(vendor, value) : "a
|
|
|
8811
9421
|
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
8812
9422
|
const file = modelsPath(env);
|
|
8813
9423
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
8814
|
-
|
|
9424
|
+
writeFileSync5(tmp, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
|
|
8815
9425
|
chmodSync2(tmp, 384);
|
|
8816
9426
|
renameSync3(tmp, file);
|
|
8817
9427
|
chmodSync2(file, 384);
|
|
@@ -8946,8 +9556,8 @@ function startRelay(o) {
|
|
|
8946
9556
|
}
|
|
8947
9557
|
|
|
8948
9558
|
// src/commands/models.ts
|
|
8949
|
-
import { spawnSync as
|
|
8950
|
-
import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync7, rmSync as
|
|
9559
|
+
import { spawnSync as spawnSync3 } from "node:child_process";
|
|
9560
|
+
import { mkdtempSync as mkdtempSync2, readFileSync as readFileSync7, rmSync as rmSync4 } from "node:fs";
|
|
8951
9561
|
import { tmpdir as tmpdir2 } from "node:os";
|
|
8952
9562
|
import { join as join6 } from "node:path";
|
|
8953
9563
|
var HOW = {
|
|
@@ -8970,14 +9580,14 @@ async function connectChatgpt(ctx, laptop) {
|
|
|
8970
9580
|
if (!ctx.io.stdin.isTTY) {
|
|
8971
9581
|
text2 = await readValue(ctx, "a Codex auth.json", "cat auth.json | airgate models connect chatgpt");
|
|
8972
9582
|
} else {
|
|
8973
|
-
if (
|
|
9583
|
+
if (spawnSync3("codex", ["--version"], { stdio: "ignore" }).status !== 0) {
|
|
8974
9584
|
throw new CliError("Codex is not installed on this laptop", "npm i -g @openai/codex, then: airgate models connect chatgpt");
|
|
8975
9585
|
}
|
|
8976
9586
|
const home = mkdtempSync2(join6(tmpdir2(), "airgate-codex-"));
|
|
8977
9587
|
try {
|
|
8978
9588
|
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
9589
|
const args = ["login", ...bool(ctx.args, "device") ? ["--device-auth"] : [], "-c", 'cli_auth_credentials_store="file"'];
|
|
8980
|
-
const r2 =
|
|
9590
|
+
const r2 = spawnSync3("codex", args, { stdio: "inherit", env: { ...process.env, ...ctx.io.env, CODEX_HOME: home } });
|
|
8981
9591
|
if (r2.status !== 0) throw new CliError("the ChatGPT sign-in did not finish", "try again, or with --device");
|
|
8982
9592
|
try {
|
|
8983
9593
|
text2 = readFileSync7(join6(home, "auth.json"), "utf8");
|
|
@@ -8985,7 +9595,7 @@ async function connectChatgpt(ctx, laptop) {
|
|
|
8985
9595
|
throw new CliError("Codex didn't write a login file", "update Codex (npm i -g @openai/codex) and try again");
|
|
8986
9596
|
}
|
|
8987
9597
|
} finally {
|
|
8988
|
-
|
|
9598
|
+
rmSync4(home, { recursive: true, force: true });
|
|
8989
9599
|
}
|
|
8990
9600
|
}
|
|
8991
9601
|
let tokens2;
|
|
@@ -9166,6 +9776,8 @@ var runTask = {
|
|
|
9166
9776
|
const minutes = Number(str(ctx.args, "timeout") ?? "60");
|
|
9167
9777
|
if (!Number.isFinite(minutes) || minutes <= 0) throw new UsageError("--timeout: minutes, e.g. 30");
|
|
9168
9778
|
const repo = str(ctx.args, "repo");
|
|
9779
|
+
const same = await checkSimilar(ctx, task2);
|
|
9780
|
+
if (!same.go) notStarted(same.similar);
|
|
9169
9781
|
const warnings = [];
|
|
9170
9782
|
const relayer = relayIfSignedIn(ctx);
|
|
9171
9783
|
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 +9823,7 @@ var runTask = {
|
|
|
9211
9823
|
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
9824
|
return r2.ok ? 0 : 1;
|
|
9213
9825
|
}
|
|
9214
|
-
const projectDir =
|
|
9826
|
+
const projectDir = resolve6(ctx.io.cwd, dirArg ?? ".");
|
|
9215
9827
|
if (!existsSync5(projectDir) || !statSync3(projectDir).isDirectory()) throw new CliError(`no directory ${projectDir}`);
|
|
9216
9828
|
const noApply = bool(ctx.args, "no-apply");
|
|
9217
9829
|
if (!noApply && !gitTopLevel(projectDir)) {
|
|
@@ -9221,8 +9833,8 @@ var runTask = {
|
|
|
9221
9833
|
const before = existsSync5(sp) ? readFileSync8(sp, "utf8") : null;
|
|
9222
9834
|
const restoreState = () => {
|
|
9223
9835
|
try {
|
|
9224
|
-
if (before !== null)
|
|
9225
|
-
else
|
|
9836
|
+
if (before !== null) writeFileSync6(sp, before);
|
|
9837
|
+
else rmSync5(sp, { force: true });
|
|
9226
9838
|
} catch {
|
|
9227
9839
|
}
|
|
9228
9840
|
};
|
|
@@ -9283,8 +9895,8 @@ When you are done, leave your changes in the files there (no need to commit or p
|
|
|
9283
9895
|
};
|
|
9284
9896
|
|
|
9285
9897
|
// src/commands/crews.ts
|
|
9286
|
-
import { existsSync as existsSync6, readFileSync as readFileSync9, realpathSync as realpathSync3, rmSync as
|
|
9287
|
-
import { relative as relative3, resolve as
|
|
9898
|
+
import { existsSync as existsSync6, readFileSync as readFileSync9, realpathSync as realpathSync3, rmSync as rmSync6, statSync as statSync4, writeFileSync as writeFileSync7 } from "node:fs";
|
|
9899
|
+
import { relative as relative3, resolve as resolve7 } from "node:path";
|
|
9288
9900
|
var KINDS = ["race", "contest", "council", "squad", "split"];
|
|
9289
9901
|
var ACTIONS = ["show", "say", "stop", "down", "rm", "list"];
|
|
9290
9902
|
var AGENT_ALIAS = { claude: "claude-code", "claude-code": "claude-code", codex: "codex", opencode: "opencode", pi: "pi" };
|
|
@@ -9446,6 +10058,8 @@ var crew = {
|
|
|
9446
10058
|
const task2 = (second ?? "").trim();
|
|
9447
10059
|
if (!task2) throw new UsageError(`what should the crew do? airgate crew ${kind} "<task>"`);
|
|
9448
10060
|
if (extra) throw new UsageError(`unexpected "${extra}" (put the task in quotes)`);
|
|
10061
|
+
const same = await checkSimilar(ctx, task2);
|
|
10062
|
+
if (!same.go) notStarted(same.similar);
|
|
9449
10063
|
const sources = ["pod", "from", "repo"].filter((k) => str(ctx.args, k));
|
|
9450
10064
|
if (sources.length > 1) throw new UsageError("one starting point: --pod, --from or --repo");
|
|
9451
10065
|
if (sources.length && third) throw new UsageError("a folder or --pod / --from / --repo, not both");
|
|
@@ -9466,14 +10080,14 @@ var crew = {
|
|
|
9466
10080
|
basePodId = (await ui2.spin(`starting the base pod on ${spec.repo}`, async () => waitReady(ctx, await ctx.api.launch(spec)))).id;
|
|
9467
10081
|
temporaryBase = true;
|
|
9468
10082
|
} else {
|
|
9469
|
-
const projectDir =
|
|
10083
|
+
const projectDir = resolve7(ctx.io.cwd, third ?? ".");
|
|
9470
10084
|
if (!existsSync6(projectDir) || !statSync4(projectDir).isDirectory()) throw new CliError(`no directory ${projectDir}`);
|
|
9471
10085
|
const sp = statePath(projectDir);
|
|
9472
10086
|
const before = existsSync6(sp) ? readFileSync9(sp, "utf8") : null;
|
|
9473
10087
|
restoreState = () => {
|
|
9474
10088
|
try {
|
|
9475
|
-
if (before !== null)
|
|
9476
|
-
else
|
|
10089
|
+
if (before !== null) writeFileSync7(sp, before);
|
|
10090
|
+
else rmSync6(sp, { force: true });
|
|
9477
10091
|
} catch {
|
|
9478
10092
|
}
|
|
9479
10093
|
};
|
|
@@ -9536,6 +10150,7 @@ var jobSchema = obj(
|
|
|
9536
10150
|
);
|
|
9537
10151
|
var OWN = /* @__PURE__ */ new Map();
|
|
9538
10152
|
var n = (description) => ({ type: "number", description });
|
|
10153
|
+
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}` }));
|
|
9539
10154
|
var dirOf = (ctx, a) => typeof a.dir === "string" && a.dir ? a.dir : ctx.io.cwd;
|
|
9540
10155
|
var TOOLS = [
|
|
9541
10156
|
{
|
|
@@ -9745,9 +10360,10 @@ var TOOLS = [
|
|
|
9745
10360
|
if (a.keep === true) argv.push("--keep");
|
|
9746
10361
|
if (a.pr === true) argv.push("--pr");
|
|
9747
10362
|
if (typeof a.timeoutMinutes === "number") argv.push("--timeout", String(a.timeoutMinutes));
|
|
10363
|
+
const similar = await sameWork(ctx, String(a.task));
|
|
9748
10364
|
const out = await runCommand(ctx, runTask, argv, dirOf(ctx, a));
|
|
9749
10365
|
if (out.pod?.id && out.pod_state !== "deleted") OWN.set(out.pod.id, a.repo ? "launch" : "project");
|
|
9750
|
-
return out;
|
|
10366
|
+
return similar.length ? { ...out, similar } : out;
|
|
9751
10367
|
}
|
|
9752
10368
|
},
|
|
9753
10369
|
{
|
|
@@ -9896,7 +10512,53 @@ var TOOLS = [
|
|
|
9896
10512
|
if (typeof a.maxMinutes === "number") argv.push("--minutes", String(a.maxMinutes));
|
|
9897
10513
|
if (a.build === true) argv.push("--build");
|
|
9898
10514
|
if (a.pr === true) argv.push("--pr");
|
|
9899
|
-
|
|
10515
|
+
const similar = await sameWork(ctx, String(a.task));
|
|
10516
|
+
const out = await runCommand(ctx, crew, argv, dirOf(ctx, a));
|
|
10517
|
+
return similar.length ? { ...out, similar } : out;
|
|
10518
|
+
}
|
|
10519
|
+
},
|
|
10520
|
+
{
|
|
10521
|
+
name: "team_recall",
|
|
10522
|
+
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.",
|
|
10523
|
+
inputSchema: obj({ query: s("a few words about the work") }),
|
|
10524
|
+
async call(ctx, a) {
|
|
10525
|
+
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 }));
|
|
10526
|
+
}
|
|
10527
|
+
},
|
|
10528
|
+
{
|
|
10529
|
+
name: "team_patch",
|
|
10530
|
+
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.",
|
|
10531
|
+
inputSchema: obj({ id: s("the entry id from team_recall") }, ["id"]),
|
|
10532
|
+
async call(ctx, a) {
|
|
10533
|
+
const r = await ctx.api.libraryPatch(need(a, "id"));
|
|
10534
|
+
return { title: r.entry.title, by: r.entry.author, patch: r.patch };
|
|
10535
|
+
}
|
|
10536
|
+
},
|
|
10537
|
+
{
|
|
10538
|
+
name: "team_note",
|
|
10539
|
+
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.",
|
|
10540
|
+
inputSchema: obj({ text: s("the note, one or two sentences") }, ["text"]),
|
|
10541
|
+
async call(ctx, a) {
|
|
10542
|
+
return ctx.api.note(need(a, "text"));
|
|
10543
|
+
}
|
|
10544
|
+
},
|
|
10545
|
+
{
|
|
10546
|
+
name: "pod_share",
|
|
10547
|
+
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.",
|
|
10548
|
+
inputSchema: obj({ pod: POD, shared: b("true shares it, false makes it private again") }, ["pod", "shared"]),
|
|
10549
|
+
async call(ctx, a) {
|
|
10550
|
+
const p = await resolvePod(ctx, need(a, "pod"));
|
|
10551
|
+
const pod = await ctx.api.sharePod(p.id, a.shared !== false);
|
|
10552
|
+
return { pod: pod.spec.name, shared: Boolean(pod.shared) };
|
|
10553
|
+
}
|
|
10554
|
+
},
|
|
10555
|
+
{
|
|
10556
|
+
name: "pod_handover",
|
|
10557
|
+
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.",
|
|
10558
|
+
inputSchema: obj({ pod: POD, email: s("the teammate's email") }, ["pod", "email"]),
|
|
10559
|
+
async call(ctx, a) {
|
|
10560
|
+
const p = await resolvePod(ctx, need(a, "pod"));
|
|
10561
|
+
return ctx.api.handover(p.id, need(a, "email"));
|
|
9900
10562
|
}
|
|
9901
10563
|
},
|
|
9902
10564
|
{
|
|
@@ -9977,7 +10639,7 @@ async function handle(ctx, msg) {
|
|
|
9977
10639
|
protocolVersion: asked,
|
|
9978
10640
|
capabilities: { tools: { listChanged: false } },
|
|
9979
10641
|
serverInfo: { name: "airgate", title: "Airgate", version: VERSION },
|
|
9980
|
-
instructions: "Airgate pods are private cloud computers for coding agents; crews put several agents (each in its own pod) on one task: crew_start, crew_status, crew_down. pod_run does one task in a fresh pod and brings the result back; pod_up / pod_down move the local project and conversation there and back; task_start hands an existing pod a task (run_status / run_log follow it). Sleep pods you are done with; delete only pods you launched (or the user asked). Outbound actions wait in the airlock for the user: never approve on your own initiative."
|
|
10642
|
+
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."
|
|
9981
10643
|
});
|
|
9982
10644
|
}
|
|
9983
10645
|
case "ping":
|
|
@@ -10045,7 +10707,7 @@ var mcp = {
|
|
|
10045
10707
|
};
|
|
10046
10708
|
|
|
10047
10709
|
// src/commands/misc.ts
|
|
10048
|
-
import { mkdirSync as mkdirSync5, writeFileSync as
|
|
10710
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync8 } from "node:fs";
|
|
10049
10711
|
import { homedir as homedir3 } from "node:os";
|
|
10050
10712
|
import { join as join7 } from "node:path";
|
|
10051
10713
|
var PAGES = ["pods", "airlock", "usage", "settings", "mask", "chat", "notes", "data", "status"];
|
|
@@ -10079,15 +10741,19 @@ var status = {
|
|
|
10079
10741
|
const h = await ctx.ui.spin("checking the gateway", ctx.api.health());
|
|
10080
10742
|
const ms = Date.now() - t0;
|
|
10081
10743
|
let signedIn = null;
|
|
10744
|
+
let verified = null;
|
|
10082
10745
|
if (ctx.api.signedIn) {
|
|
10083
|
-
|
|
10746
|
+
const m = await ctx.api.me().catch(() => null);
|
|
10747
|
+
signedIn = m?.user.email ?? null;
|
|
10748
|
+
const v = m?.user?.emailVerified;
|
|
10749
|
+
verified = typeof v === "boolean" ? v : null;
|
|
10084
10750
|
}
|
|
10085
|
-
if (ctx.json) return ctx.ui.json({ gateway: ctx.cfg.gateway, ...h, latencyMs: ms, signedIn, version: VERSION });
|
|
10751
|
+
if (ctx.json) return ctx.ui.json({ gateway: ctx.cfg.gateway, ...h, latencyMs: ms, signedIn, emailVerified: verified, version: VERSION });
|
|
10086
10752
|
const rows2 = [
|
|
10087
10753
|
["gateway", `${ctx.cfg.gateway} ${h.ok ? ctx.ui.c("ok", "ok") : ctx.ui.c("bad", "down")} ${ctx.ui.c("dim", `${ms}ms`)}`],
|
|
10088
10754
|
["provider", h.provider === "simulator" ? ctx.ui.c("warn", "simulator") : h.provider],
|
|
10089
10755
|
["billing", h.billing ? "on" : "off"],
|
|
10090
|
-
["signed in", signedIn ?? (ctx.api.signedIn ? ctx.ui.c("bad", "token rejected \xB7 airgate login") : ctx.ui.c("warn", "no \xB7 airgate login"))],
|
|
10756
|
+
["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)") : "")],
|
|
10091
10757
|
["config", configPath(ctx.io.env)]
|
|
10092
10758
|
];
|
|
10093
10759
|
ctx.ui.table(rows2.map(([k, v]) => [ctx.ui.c("dim", k), v]));
|
|
@@ -10126,7 +10792,7 @@ var skill = {
|
|
|
10126
10792
|
const base = bool(ctx.args, "project") ? join7(ctx.io.cwd, ".claude") : join7(ctx.io.env.HOME ?? homedir3(), ".claude");
|
|
10127
10793
|
const dir = join7(base, "skills", "airgate");
|
|
10128
10794
|
mkdirSync5(dir, { recursive: true });
|
|
10129
|
-
|
|
10795
|
+
writeFileSync8(join7(dir, "SKILL.md"), text2);
|
|
10130
10796
|
if (ctx.json) return ctx.ui.json({ installed: join7(dir, "SKILL.md") });
|
|
10131
10797
|
ctx.ui.out(`${ctx.ui.c("ok", "installed")} the airgate skill: ${join7(dir, "SKILL.md")}`);
|
|
10132
10798
|
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".'));
|
|
@@ -10190,11 +10856,22 @@ var preview = {
|
|
|
10190
10856
|
var share = {
|
|
10191
10857
|
name: "share",
|
|
10192
10858
|
group: "pods",
|
|
10193
|
-
usage: "<pod>
|
|
10194
|
-
summary: "a
|
|
10195
|
-
example: "airgate share web
|
|
10859
|
+
usage: "<pod> [port] [--off]",
|
|
10860
|
+
summary: "share a pod with your team (teammates open, watch and fork it), or a port publicly by link; --off undoes it",
|
|
10861
|
+
example: "airgate share web",
|
|
10862
|
+
notes: [
|
|
10863
|
+
"airgate share <pod> your team workspace sees it: its terminal, files, checkpoints; they can fork it, never delete it",
|
|
10864
|
+
"airgate share <pod> <port> a public link to that port: anyone with the link can open it"
|
|
10865
|
+
],
|
|
10196
10866
|
flags: { off: { type: "boolean", help: "stop sharing: private to you again" } },
|
|
10197
10867
|
async run(ctx) {
|
|
10868
|
+
if (ctx.args.positionals.length === 1) {
|
|
10869
|
+
const p2 = await resolvePod(ctx, ctx.args.positionals[0]);
|
|
10870
|
+
const off2 = bool(ctx.args, "off");
|
|
10871
|
+
const out = await ctx.api.sharePod(p2.id, !off2);
|
|
10872
|
+
if (ctx.json) return ctx.ui.json(out);
|
|
10873
|
+
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).`);
|
|
10874
|
+
}
|
|
10198
10875
|
const { pod, port } = await podAndPort(ctx);
|
|
10199
10876
|
const off = bool(ctx.args, "off");
|
|
10200
10877
|
const p = await ctx.api.ensurePreview(pod.id, { port, public: !off });
|
|
@@ -10217,7 +10894,7 @@ var checkpoints = {
|
|
|
10217
10894
|
summary: "the pod's undo timeline, newest first",
|
|
10218
10895
|
example: "airgate checkpoints fix-nav",
|
|
10219
10896
|
async run(ctx) {
|
|
10220
|
-
const p = await resolvePod(ctx, ctx.args.positionals[0]);
|
|
10897
|
+
const p = await resolvePod(ctx, ctx.args.positionals[0], { team: true });
|
|
10221
10898
|
const cps = newestFirst(await ctx.api.checkpoints(p.id));
|
|
10222
10899
|
if (ctx.json) return ctx.ui.json(cps);
|
|
10223
10900
|
if (!cps.length) {
|
|
@@ -10280,7 +10957,7 @@ var race = {
|
|
|
10280
10957
|
},
|
|
10281
10958
|
async run(ctx) {
|
|
10282
10959
|
const [ref, cpRef = "current"] = ctx.args.positionals;
|
|
10283
|
-
const p = await resolvePod(ctx, ref);
|
|
10960
|
+
const p = await resolvePod(ctx, ref, { team: true });
|
|
10284
10961
|
const checkpointId2 = cpRef === "current" ? "current" : resolveCheckpoint(await ctx.api.checkpoints(p.id), cpRef).id;
|
|
10285
10962
|
const overrides = {};
|
|
10286
10963
|
const agent = str(ctx.args, "agent");
|
|
@@ -11725,18 +12402,18 @@ function startTui(d) {
|
|
|
11725
12402
|
d.stdout.write(LEAVE_ALT + `\x1B[2m attached to ${name}. ctrl-] comes back to airgate.\x1B[0m\r
|
|
11726
12403
|
`);
|
|
11727
12404
|
const { cols, rows: rows2 } = size2();
|
|
11728
|
-
let
|
|
12405
|
+
let note2 = `back from ${name}. the session keeps running.`;
|
|
11729
12406
|
let tone2 = "ok";
|
|
11730
12407
|
try {
|
|
11731
12408
|
const ws = d.websocket(d.api.terminalUrl(podId, cols, rows2));
|
|
11732
12409
|
const end = await attachSession({ stdin: d.stdin, stdout: d.stdout, ws, signals: d.signals });
|
|
11733
|
-
if (end.reason === "exit")
|
|
12410
|
+
if (end.reason === "exit") note2 = `${name}: session ended (exit ${end.code})`;
|
|
11734
12411
|
else if (end.reason === "error" || end.reason === "closed" && end.code !== 1e3) {
|
|
11735
|
-
|
|
12412
|
+
note2 = `${name}: terminal ${end.reason === "error" ? end.message : "closed"}`;
|
|
11736
12413
|
tone2 = "bad";
|
|
11737
12414
|
}
|
|
11738
12415
|
} catch (e) {
|
|
11739
|
-
|
|
12416
|
+
note2 = `couldn't attach: ${errText(e)}`;
|
|
11740
12417
|
tone2 = "bad";
|
|
11741
12418
|
}
|
|
11742
12419
|
if (finished) return;
|
|
@@ -11744,7 +12421,7 @@ function startTui(d) {
|
|
|
11744
12421
|
shown2 = [];
|
|
11745
12422
|
d.stdout.write(ENTER_ALT);
|
|
11746
12423
|
listenKeys(true);
|
|
11747
|
-
dispatch(tone2 === "ok" ? { type: "done", text:
|
|
12424
|
+
dispatch(tone2 === "ok" ? { type: "done", text: note2 } : { type: "failed", text: note2 });
|
|
11748
12425
|
}
|
|
11749
12426
|
async function stream() {
|
|
11750
12427
|
for (let attempt = 0; !controller.signal.aborted; attempt++) {
|
|
@@ -11852,8 +12529,8 @@ var ui = {
|
|
|
11852
12529
|
};
|
|
11853
12530
|
|
|
11854
12531
|
// src/commands/workspace.ts
|
|
11855
|
-
import { writeFileSync as
|
|
11856
|
-
import { basename as basename3, resolve as
|
|
12532
|
+
import { writeFileSync as writeFileSync9, existsSync as existsSync7, statSync as statSync5 } from "node:fs";
|
|
12533
|
+
import { basename as basename3, resolve as resolve8 } from "node:path";
|
|
11857
12534
|
async function checkpointId(ctx, podId, ref) {
|
|
11858
12535
|
if (!ref || ref === "current") return void 0;
|
|
11859
12536
|
return resolveCheckpoint(await ctx.api.checkpoints(podId), ref).id;
|
|
@@ -11870,7 +12547,7 @@ var files = {
|
|
|
11870
12547
|
async run(ctx) {
|
|
11871
12548
|
const [ref, path = ""] = ctx.args.positionals;
|
|
11872
12549
|
if (!ref) throw new UsageError("which pod? airgate files <pod> [path]");
|
|
11873
|
-
const pod = await resolvePod(ctx, ref);
|
|
12550
|
+
const pod = await resolvePod(ctx, ref, { team: true });
|
|
11874
12551
|
const at3 = await checkpointId(ctx, pod.id, str(ctx.args, "at"));
|
|
11875
12552
|
const r = await ctx.api.files(pod.id, path, at3);
|
|
11876
12553
|
if (ctx.json) return ctx.ui.json(r);
|
|
@@ -11896,21 +12573,21 @@ var pull = {
|
|
|
11896
12573
|
async run(ctx) {
|
|
11897
12574
|
const [ref, path] = ctx.args.positionals;
|
|
11898
12575
|
if (!ref || !path) throw new UsageError("airgate pull <pod> <path>");
|
|
11899
|
-
const pod = await resolvePod(ctx, ref);
|
|
12576
|
+
const pod = await resolvePod(ctx, ref, { team: true });
|
|
11900
12577
|
const at3 = await checkpointId(ctx, pod.id, str(ctx.args, "at"));
|
|
11901
12578
|
const body = await ctx.ui.spin(`reading ${path}`, ctx.api.fileRaw(pod.id, path, at3));
|
|
11902
12579
|
const out = str(ctx.args, "out") ?? basename3(path);
|
|
11903
12580
|
if (out === "-") return void ctx.io.stdout.write(body);
|
|
11904
|
-
const dest =
|
|
12581
|
+
const dest = resolve8(ctx.io.cwd, out);
|
|
11905
12582
|
if (existsSync7(dest) && !bool(ctx.args, "force")) {
|
|
11906
|
-
if (statSync5(dest).isDirectory()) return void save2(ctx,
|
|
12583
|
+
if (statSync5(dest).isDirectory()) return void save2(ctx, resolve8(dest, basename3(path)), body);
|
|
11907
12584
|
throw new CliError(`${out} exists here`, "pick another name with -o, or --force to overwrite");
|
|
11908
12585
|
}
|
|
11909
12586
|
save2(ctx, dest, body);
|
|
11910
12587
|
}
|
|
11911
12588
|
};
|
|
11912
12589
|
function save2(ctx, dest, body) {
|
|
11913
|
-
|
|
12590
|
+
writeFileSync9(dest, body);
|
|
11914
12591
|
if (ctx.json) return ctx.ui.json({ saved: dest, bytes: body.length });
|
|
11915
12592
|
ctx.ui.out(`${ctx.ui.c("ok", "saved")} ${dest} (${bytes(body.length)})`);
|
|
11916
12593
|
}
|
|
@@ -12040,6 +12717,9 @@ var COMMANDS = [
|
|
|
12040
12717
|
forgot,
|
|
12041
12718
|
reset,
|
|
12042
12719
|
deleteAccount,
|
|
12720
|
+
verify,
|
|
12721
|
+
bug,
|
|
12722
|
+
team,
|
|
12043
12723
|
github,
|
|
12044
12724
|
pods,
|
|
12045
12725
|
launch,
|
|
@@ -12073,6 +12753,9 @@ var COMMANDS = [
|
|
|
12073
12753
|
models,
|
|
12074
12754
|
relay,
|
|
12075
12755
|
crew,
|
|
12756
|
+
handover,
|
|
12757
|
+
recall,
|
|
12758
|
+
note,
|
|
12076
12759
|
files,
|
|
12077
12760
|
pull,
|
|
12078
12761
|
snapshot2,
|
|
@@ -12172,6 +12855,10 @@ function report(ui2, err, cmd, env) {
|
|
|
12172
12855
|
line2(err.message.charAt(0).toLowerCase() + err.message.slice(1), "see what this gateway offers: airgate templates");
|
|
12173
12856
|
return 1;
|
|
12174
12857
|
}
|
|
12858
|
+
if (err.code === "team_plan_required" || err.code === "no_seat") {
|
|
12859
|
+
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>");
|
|
12860
|
+
return 1;
|
|
12861
|
+
}
|
|
12175
12862
|
if (err.status === 402 || err.code === "billing_required") {
|
|
12176
12863
|
line2(`${err.message}`);
|
|
12177
12864
|
const p = PLANS;
|