airgate 0.1.0 → 0.1.3
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 +16 -2
- package/dist/airgate.mjs +1281 -475
- package/package.json +1 -1
package/dist/airgate.mjs
CHANGED
|
@@ -229,7 +229,76 @@ function spaceName(taken = [], random = Math.random) {
|
|
|
229
229
|
if (!used.has(name)) return name;
|
|
230
230
|
}
|
|
231
231
|
const base = `${pick(FIRST, random)}-${pick(SECOND, random)}`;
|
|
232
|
-
for (let
|
|
232
|
+
for (let n2 = 2; ; n2++) if (!used.has(`${base}-${n2}`)) return `${base}-${n2}`;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ../sandbox/src/dotenv.ts
|
|
236
|
+
var SECRET_NAME_RE = /^[A-Z_][A-Z0-9_]{0,63}$/;
|
|
237
|
+
function reservedSecretName(name) {
|
|
238
|
+
return name.startsWith("AIRGATE_") || name.startsWith("LD_") || name.startsWith("DYLD_") || ["PATH", "HOME", "USER", "SHELL", "PWD", "BASH_ENV", "ENV", "PROMPT_COMMAND", "IFS", "TERM", "TMUX"].includes(name);
|
|
239
|
+
}
|
|
240
|
+
function suggestSecretName(raw) {
|
|
241
|
+
const s2 = raw.trim().toUpperCase().replace(/[^A-Z0-9_]/g, "_").replace(/^([0-9])/, "_$1").slice(0, 64);
|
|
242
|
+
return SECRET_NAME_RE.test(s2) && !reservedSecretName(s2) ? s2 : null;
|
|
243
|
+
}
|
|
244
|
+
var unescapeDouble = (s2) => s2.replace(/\\([nrt"\\$])/g, (_m, c) => ({ n: "\n", r: "\r", t: " " })[c] ?? c);
|
|
245
|
+
function parseDotenv(input) {
|
|
246
|
+
const lines = input.replace(/^\uFEFF/, "").replace(/\r\n?/g, "\n").split("\n");
|
|
247
|
+
const entries = [];
|
|
248
|
+
const unparsed = [];
|
|
249
|
+
for (let i = 0; i < lines.length; i++) {
|
|
250
|
+
const lineNo = i + 1;
|
|
251
|
+
const line2 = lines[i].trim();
|
|
252
|
+
if (!line2 || line2.startsWith("#")) continue;
|
|
253
|
+
const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.*)$/.exec(line2);
|
|
254
|
+
if (!m) {
|
|
255
|
+
unparsed.push(lineNo);
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
const key = m[1];
|
|
259
|
+
let rest = m[2];
|
|
260
|
+
let value;
|
|
261
|
+
const q = rest[0];
|
|
262
|
+
if (q === '"' || q === "'" || q === "`") {
|
|
263
|
+
let body = rest.slice(1);
|
|
264
|
+
let close = findClose(body, q);
|
|
265
|
+
while (close < 0 && i + 1 < lines.length) {
|
|
266
|
+
i++;
|
|
267
|
+
body += "\n" + lines[i];
|
|
268
|
+
close = findClose(body, q);
|
|
269
|
+
}
|
|
270
|
+
const raw = close < 0 ? body : body.slice(0, close);
|
|
271
|
+
value = q === '"' ? unescapeDouble(raw) : raw;
|
|
272
|
+
} else {
|
|
273
|
+
value = rest.replace(/\s+#.*$/, "").trim();
|
|
274
|
+
}
|
|
275
|
+
let name = key;
|
|
276
|
+
let note;
|
|
277
|
+
if (!SECRET_NAME_RE.test(key)) {
|
|
278
|
+
name = suggestSecretName(key);
|
|
279
|
+
note = name ? `saved as ${name}` : "not a usable name";
|
|
280
|
+
} else if (reservedSecretName(key)) {
|
|
281
|
+
name = null;
|
|
282
|
+
note = "reserved (the pod sets it)";
|
|
283
|
+
}
|
|
284
|
+
entries.push({ key, name, value, line: lineNo, ...note ? { note } : {} });
|
|
285
|
+
}
|
|
286
|
+
const values = {};
|
|
287
|
+
for (const e of entries) if (e.name) values[e.name] = e.value;
|
|
288
|
+
return { entries, values, unparsed };
|
|
289
|
+
}
|
|
290
|
+
function findClose(s2, q) {
|
|
291
|
+
for (let i = 0; i < s2.length; i++) {
|
|
292
|
+
if (q === '"' && s2[i] === "\\") {
|
|
293
|
+
i++;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
if (s2[i] === q) return i;
|
|
297
|
+
}
|
|
298
|
+
return -1;
|
|
299
|
+
}
|
|
300
|
+
function dotenvExample(names) {
|
|
301
|
+
return names.map((n2) => `${n2}=`).join("\n") + (names.length ? "\n" : "");
|
|
233
302
|
}
|
|
234
303
|
|
|
235
304
|
// src/api.ts
|
|
@@ -356,8 +425,8 @@ var Api = class {
|
|
|
356
425
|
pod(id) {
|
|
357
426
|
return this.request("GET", `/v1/pods/${enc(id)}`).then((r) => r.pod);
|
|
358
427
|
}
|
|
359
|
-
launch(spec, returnUrl) {
|
|
360
|
-
return this.request("POST", "/v1/pods", { body: { spec, returnUrl } }).then((r) => r.pod);
|
|
428
|
+
launch(spec, returnUrl, secrets2) {
|
|
429
|
+
return this.request("POST", "/v1/pods", { body: { spec, returnUrl, ...secrets2 && Object.keys(secrets2).length ? { secrets: secrets2 } : {} } }).then((r) => r.pod);
|
|
361
430
|
}
|
|
362
431
|
stop(id) {
|
|
363
432
|
return this.request("POST", `/v1/pods/${enc(id)}/stop`, { body: {} }).then((r) => r.pod);
|
|
@@ -436,18 +505,48 @@ var Api = class {
|
|
|
436
505
|
githubConnect(returnUrl) {
|
|
437
506
|
return this.request("POST", "/v1/github/connect", { body: { returnUrl } });
|
|
438
507
|
}
|
|
508
|
+
inspectRepo(repo) {
|
|
509
|
+
return this.request("GET", `/v1/github/inspect?repo=${encodeURIComponent(repo)}`);
|
|
510
|
+
}
|
|
439
511
|
githubRepos(fresh = false) {
|
|
440
512
|
return this.request("GET", `/v1/github/repos${fresh ? "?fresh=1" : ""}`).then((r) => r.repos);
|
|
441
513
|
}
|
|
442
514
|
githubDisconnect(id) {
|
|
443
515
|
return this.request("DELETE", `/v1/github/installations/${id}`);
|
|
444
516
|
}
|
|
517
|
+
/* model sign-ins: connected to airgate (opt-in), and whether a laptop relay is connected */
|
|
518
|
+
modelsAuth() {
|
|
519
|
+
return this.request("GET", "/v1/models/auth");
|
|
520
|
+
}
|
|
521
|
+
connectModel(vendor, value) {
|
|
522
|
+
return this.request("PUT", `/v1/models/auth/${enc(vendor)}`, { body: { value } });
|
|
523
|
+
}
|
|
524
|
+
disconnectModel(vendor) {
|
|
525
|
+
return this.request("DELETE", `/v1/models/auth/${enc(vendor)}`);
|
|
526
|
+
}
|
|
527
|
+
relayUrl() {
|
|
528
|
+
const q = new URLSearchParams({ token: this.tokenForUrl(), client: this.client });
|
|
529
|
+
return `${this.gateway.replace(/^http/, "ws")}/v1/relay?${q}`;
|
|
530
|
+
}
|
|
445
531
|
secrets() {
|
|
446
532
|
return this.request("GET", "/v1/secrets");
|
|
447
533
|
}
|
|
448
534
|
setSecret(name, value, podId) {
|
|
449
535
|
return this.request("PUT", `/v1/secrets/${enc(name)}`, { body: { value, ...podId ? { podId } : {} } });
|
|
450
536
|
}
|
|
537
|
+
previews(id) {
|
|
538
|
+
return this.request("GET", `/v1/pods/${enc(id)}/previews`);
|
|
539
|
+
}
|
|
540
|
+
ensurePreview(id, body) {
|
|
541
|
+
return this.request("POST", `/v1/pods/${enc(id)}/previews`, { body }).then((r) => r.preview);
|
|
542
|
+
}
|
|
543
|
+
openPreview(slug, path = "/") {
|
|
544
|
+
return this.request("POST", `/v1/previews/${enc(slug)}/open`, { body: { path } }).then((r) => r.url);
|
|
545
|
+
}
|
|
546
|
+
/** many at once (a .env file), saved together or not at all */
|
|
547
|
+
setSecrets(values, podId) {
|
|
548
|
+
return this.request("PUT", "/v1/secrets", { body: { values, ...podId ? { podId } : {} } });
|
|
549
|
+
}
|
|
451
550
|
deleteSecret(name, podId) {
|
|
452
551
|
return this.request("DELETE", `/v1/secrets/${enc(name)}${podId ? `?podId=${enc(podId)}` : ""}`);
|
|
453
552
|
}
|
|
@@ -1015,10 +1114,10 @@ function drawable(text2, font = "block") {
|
|
|
1015
1114
|
return [...String(text2)].filter((ch) => [...f.cased ? ch : ch.toUpperCase()].some((c) => Object.hasOwn(f.glyphs, c))).join("");
|
|
1016
1115
|
}
|
|
1017
1116
|
var shapes = (text2, f) => [...String(text2)].flatMap((ch) => [...f.cased ? ch : ch.toUpperCase()]).map((c) => Object.hasOwn(f.glyphs, c) ? f.glyphs[c] : void 0).filter((g) => Boolean(g)).map((g) => g.split("|"));
|
|
1018
|
-
function fade(stops,
|
|
1117
|
+
function fade(stops, n2) {
|
|
1019
1118
|
const rgb2 = stops.map((s2) => [1, 3, 5].map((i) => parseInt(s2.slice(i, i + 2), 16)));
|
|
1020
|
-
return Array.from({ length:
|
|
1021
|
-
const k =
|
|
1119
|
+
return Array.from({ length: n2 }, (_, i) => {
|
|
1120
|
+
const k = n2 > 1 ? i / (n2 - 1) * (rgb2.length - 1) : 0;
|
|
1022
1121
|
const a = Math.min(rgb2.length - 2, Math.floor(k)), f = k - a;
|
|
1023
1122
|
const c = rgb2.length === 1 ? rgb2[0] : rgb2[a].map((v, j) => Math.round(v + (rgb2[a + 1][j] - v) * f));
|
|
1024
1123
|
return `#${c.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
|
|
@@ -1446,14 +1545,14 @@ function merge(parts, extra = [], plain = false) {
|
|
|
1446
1545
|
};
|
|
1447
1546
|
const idx = lists.map((p) => (p ?? []).map(add));
|
|
1448
1547
|
const inked = lists.some((p) => !p) ? { light: add(INK2.light), dark: add(INK2.dark) } : null;
|
|
1449
|
-
const
|
|
1548
|
+
const n2 = all.length;
|
|
1450
1549
|
const values = all.map(rgb);
|
|
1451
1550
|
const into = all.map((_, i) => i);
|
|
1452
|
-
const alive = new Uint8Array(
|
|
1453
|
-
const near = new Int32Array(
|
|
1551
|
+
const alive = new Uint8Array(n2).fill(1);
|
|
1552
|
+
const near = new Int32Array(n2), gap = new Float64Array(n2);
|
|
1454
1553
|
const look = (i) => {
|
|
1455
1554
|
let best = Infinity, at4 = -1;
|
|
1456
|
-
for (let j = 0; j <
|
|
1555
|
+
for (let j = 0; j < n2; j++)
|
|
1457
1556
|
if (j !== i && alive[j]) {
|
|
1458
1557
|
const d = apart(values[i], values[j]);
|
|
1459
1558
|
if (d < best)
|
|
@@ -1462,19 +1561,19 @@ function merge(parts, extra = [], plain = false) {
|
|
|
1462
1561
|
near[i] = at4;
|
|
1463
1562
|
gap[i] = best;
|
|
1464
1563
|
};
|
|
1465
|
-
for (let i = 0; i <
|
|
1564
|
+
for (let i = 0; i < n2; i++)
|
|
1466
1565
|
look(i);
|
|
1467
|
-
for (let left =
|
|
1566
|
+
for (let left = n2; left > 64; left--) {
|
|
1468
1567
|
let a = -1;
|
|
1469
|
-
for (let i = 0; i <
|
|
1568
|
+
for (let i = 0; i < n2; i++)
|
|
1470
1569
|
if (alive[i] && (a < 0 || gap[i] < gap[a]))
|
|
1471
1570
|
a = i;
|
|
1472
1571
|
const keep = Math.min(a, near[a]), drop = Math.max(a, near[a]);
|
|
1473
1572
|
alive[drop] = 0;
|
|
1474
|
-
for (let i = 0; i <
|
|
1573
|
+
for (let i = 0; i < n2; i++)
|
|
1475
1574
|
if (into[i] === drop)
|
|
1476
1575
|
into[i] = keep;
|
|
1477
|
-
for (let i = 0; i <
|
|
1576
|
+
for (let i = 0; i < n2; i++)
|
|
1478
1577
|
if (alive[i] && near[i] === drop)
|
|
1479
1578
|
look(i);
|
|
1480
1579
|
}
|
|
@@ -1679,8 +1778,8 @@ function knockout(s2, g, x, y, h, mask2) {
|
|
|
1679
1778
|
}
|
|
1680
1779
|
}
|
|
1681
1780
|
var at = (v, t) => {
|
|
1682
|
-
const
|
|
1683
|
-
return finite(
|
|
1781
|
+
const n2 = typeof v === "function" ? v(t) : v;
|
|
1782
|
+
return finite(n2) ? Math.floor(n2) : 0;
|
|
1684
1783
|
};
|
|
1685
1784
|
var around = (p, size2, room) => {
|
|
1686
1785
|
const span = room + size2;
|
|
@@ -1870,19 +1969,19 @@ function crop(src, region) {
|
|
|
1870
1969
|
fail2(`crop's region, ${cols} by ${rows2} at ${x}, ${y}, misses ${p.meta.name}, which is ${p.cols} by ${p.rows}`);
|
|
1871
1970
|
return reshape("crop", p, x1 - x0, y1 - y0, (g, s2) => s2.paste(g, -x0, -y0));
|
|
1872
1971
|
}
|
|
1873
|
-
function pad(src,
|
|
1972
|
+
function pad(src, n2) {
|
|
1874
1973
|
const p = prepare("pad", src);
|
|
1875
1974
|
let sides;
|
|
1876
|
-
if (typeof
|
|
1877
|
-
sides = Array(4).fill(whole2(
|
|
1878
|
-
else if (Array.isArray(
|
|
1879
|
-
const [r, c] = [whole2(
|
|
1975
|
+
if (typeof n2 === "number")
|
|
1976
|
+
sides = Array(4).fill(whole2(n2, "pad", 0, 0));
|
|
1977
|
+
else if (Array.isArray(n2) && n2.length === 2) {
|
|
1978
|
+
const [r, c] = [whole2(n2[0], "pad's rows", 0, 0), whole2(n2[1], "pad's columns", 0, 0)];
|
|
1880
1979
|
sides = [r, c, r, c];
|
|
1881
|
-
} else if (
|
|
1882
|
-
const o =
|
|
1980
|
+
} else if (n2 !== null && typeof n2 === "object" && !Array.isArray(n2)) {
|
|
1981
|
+
const o = n2;
|
|
1883
1982
|
sides = ["top", "right", "bottom", "left"].map((k) => whole2(o[k], `pad's ${k}`, 0, 0));
|
|
1884
1983
|
} else
|
|
1885
|
-
return fail2(`pad takes a number, [rows, columns] or { top, right, bottom, left }, not ${shown(
|
|
1984
|
+
return fail2(`pad takes a number, [rows, columns] or { top, right, bottom, left }, not ${shown(n2)}`);
|
|
1886
1985
|
const [top, right, bottom, left] = sides;
|
|
1887
1986
|
return reshape("pad", p, p.cols + left + right, p.rows + top + bottom, (g, s2) => s2.paste(g, left, top));
|
|
1888
1987
|
}
|
|
@@ -2069,7 +2168,7 @@ function optionsOf2(o, fx, keys) {
|
|
|
2069
2168
|
fail2(`${fx}() has no option ${JSON.stringify(k)}${RENAMED[`${fx}.${k}`] ? "" : suggest(k, known)}: ${RENAMED[`${fx}.${k}`] ?? `it takes ${or2(known)}`}`);
|
|
2070
2169
|
return o;
|
|
2071
2170
|
}
|
|
2072
|
-
var mod = (a,
|
|
2171
|
+
var mod = (a, n2) => (a % n2 + n2) % n2;
|
|
2073
2172
|
var gcd2 = (a, b2) => b2 ? gcd2(b2, a % b2) : a;
|
|
2074
2173
|
function lcm(a, b2) {
|
|
2075
2174
|
const x = Math.round(a * 100), y = Math.round(b2 * 100);
|
|
@@ -2079,14 +2178,14 @@ function lcm(a, b2) {
|
|
|
2079
2178
|
return k * y <= 6e3 ? +(k * b2).toFixed(6) : void 0;
|
|
2080
2179
|
}
|
|
2081
2180
|
function quiet(t, busy, span) {
|
|
2082
|
-
const
|
|
2181
|
+
const n2 = 480;
|
|
2083
2182
|
let from = -1;
|
|
2084
|
-
for (let k = 0; k <=
|
|
2085
|
-
const u = t + span * k /
|
|
2183
|
+
for (let k = 0; k <= n2; k++) {
|
|
2184
|
+
const u = t + span * k / n2;
|
|
2086
2185
|
if (from < 0 && !busy(u))
|
|
2087
2186
|
from = u;
|
|
2088
2187
|
else if (from >= 0 && busy(u))
|
|
2089
|
-
return +((from + u) / 2 - span /
|
|
2188
|
+
return +((from + u) / 2 - span / n2 / 2).toFixed(3);
|
|
2090
2189
|
}
|
|
2091
2190
|
return from < 0 ? t : +((from + t + span) / 2).toFixed(3);
|
|
2092
2191
|
}
|
|
@@ -2152,7 +2251,7 @@ function mainInk(p, at3, options, map) {
|
|
|
2152
2251
|
const most = (paper) => {
|
|
2153
2252
|
const { text: text2, color } = snapshot(p, at3, { paper, options });
|
|
2154
2253
|
const count = /* @__PURE__ */ new Map();
|
|
2155
|
-
let best = 0,
|
|
2254
|
+
let best = 0, n2 = 0;
|
|
2156
2255
|
for (let k = 0, i = 0; k < text2.length; k++) {
|
|
2157
2256
|
const c = text2.charCodeAt(k);
|
|
2158
2257
|
if (c === 10)
|
|
@@ -2160,8 +2259,8 @@ function mainInk(p, at3, options, map) {
|
|
|
2160
2259
|
if (c !== 32 && color && i < color.length) {
|
|
2161
2260
|
const v = (count.get(color[i]) ?? 0) + 1;
|
|
2162
2261
|
count.set(color[i], v);
|
|
2163
|
-
if (v >
|
|
2164
|
-
|
|
2262
|
+
if (v > n2)
|
|
2263
|
+
n2 = v, best = color[i];
|
|
2165
2264
|
}
|
|
2166
2265
|
i++;
|
|
2167
2266
|
}
|
|
@@ -2380,14 +2479,14 @@ function typeIn(src, options) {
|
|
|
2380
2479
|
what: "typed in",
|
|
2381
2480
|
setup: () => (t, s2, g) => {
|
|
2382
2481
|
const local = cycle2 ? mod(t, cycle2) : t;
|
|
2383
|
-
const
|
|
2482
|
+
const n2 = Math.floor((local - start) * speed2 + 1e-6);
|
|
2384
2483
|
const all = g.chars.length;
|
|
2385
2484
|
let shown2 = 0, next = -1, last = -1;
|
|
2386
2485
|
for (let k = 0; k < all; k++) {
|
|
2387
2486
|
const i = seq ? seq[k] : k;
|
|
2388
2487
|
if (g.chars[i] === EMPTY)
|
|
2389
2488
|
continue;
|
|
2390
|
-
if (shown2 >=
|
|
2489
|
+
if (shown2 >= n2) {
|
|
2391
2490
|
next = i;
|
|
2392
2491
|
break;
|
|
2393
2492
|
}
|
|
@@ -2432,13 +2531,13 @@ function dissolve(src, options) {
|
|
|
2432
2531
|
const edge = o.edge === "" ? "" : chars2(o.edge, "dissolve.edge", ".:", 1);
|
|
2433
2532
|
const seed = num(o.seed, "dissolve.seed", 1, "whole");
|
|
2434
2533
|
return build2("dissolve", src, o, ({ meta: m }) => {
|
|
2435
|
-
const
|
|
2436
|
-
const v = new Float64Array(
|
|
2534
|
+
const n2 = m.cols * m.rows, aspect = m.cell ?? 2;
|
|
2535
|
+
const v = new Float64Array(n2);
|
|
2437
2536
|
for (let y = 0, i = 0; y < m.rows; y++)
|
|
2438
2537
|
for (let x = 0; x < m.cols; x++, i++)
|
|
2439
2538
|
v[i] = 0.8 * valueNoise(x * scale2, y * scale2 * aspect, seed) + 0.2 * hash(x, y, seed, 3);
|
|
2440
|
-
const at3 = new Float32Array(
|
|
2441
|
-
Int32Array.from({ length:
|
|
2539
|
+
const at3 = new Float32Array(n2);
|
|
2540
|
+
Int32Array.from({ length: n2 }, (_, i) => i).sort((a, b2) => v[a] - v[b2]).forEach((i, rank) => at3[i] = (rank + 0.5) / n2);
|
|
2442
2541
|
const band = edge ? 0.12 : 0;
|
|
2443
2542
|
const codes = [...edge].map((c) => c.charCodeAt(0)), blocks = auto ? [9617, 9618] : codes;
|
|
2444
2543
|
return {
|
|
@@ -2451,7 +2550,7 @@ function dissolve(src, options) {
|
|
|
2451
2550
|
if (k >= 1)
|
|
2452
2551
|
return copy(s2, g);
|
|
2453
2552
|
const front = k * (1 + band);
|
|
2454
|
-
for (let i = 0; i <
|
|
2553
|
+
for (let i = 0; i < n2; i++) {
|
|
2455
2554
|
const ch = g.chars[i];
|
|
2456
2555
|
if (ch === EMPTY)
|
|
2457
2556
|
continue;
|
|
@@ -2625,7 +2724,7 @@ function wave(src, options) {
|
|
|
2625
2724
|
const lines = rows2 ? h : w;
|
|
2626
2725
|
for (let k = 0; k < lines; k++) {
|
|
2627
2726
|
const off = room + Math.round(amplitude * Math.sin(TAU * (k / wavelength - t / period2)));
|
|
2628
|
-
for (let j = 0,
|
|
2727
|
+
for (let j = 0, n2 = rows2 ? w : h; j < n2; j++) {
|
|
2629
2728
|
const i = rows2 ? k * w + j : j * w + k;
|
|
2630
2729
|
if (g.chars[i] === EMPTY)
|
|
2631
2730
|
continue;
|
|
@@ -2637,8 +2736,8 @@ function wave(src, options) {
|
|
|
2637
2736
|
}
|
|
2638
2737
|
function hsl(h, s2, l) {
|
|
2639
2738
|
const a = s2 * Math.min(l, 1 - l);
|
|
2640
|
-
const f = (
|
|
2641
|
-
const k = (
|
|
2739
|
+
const f = (n2) => {
|
|
2740
|
+
const k = (n2 + h / 30) % 12;
|
|
2642
2741
|
return Math.round(255 * (l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1))));
|
|
2643
2742
|
};
|
|
2644
2743
|
return "#" + [f(0), f(8), f(4)].map((v) => v.toString(16).padStart(2, "0")).join("");
|
|
@@ -2647,7 +2746,7 @@ var hues = {
|
|
|
2647
2746
|
light: Array.from({ length: 12 }, (_, i) => hsl(i * 30, 0.8, 0.4)),
|
|
2648
2747
|
dark: Array.from({ length: 12 }, (_, i) => hsl(i * 30, 0.9, 0.66))
|
|
2649
2748
|
};
|
|
2650
|
-
var cycle = (stops,
|
|
2749
|
+
var cycle = (stops, n2) => stops.length === 1 ? Array(n2).fill(stops[0]) : gradient([...stops, stops[0]], n2 + 1).slice(0, n2);
|
|
2651
2750
|
function rainbowOf(fx, src, options, cyclesBy) {
|
|
2652
2751
|
const o = optionsOf2(options, fx, ["colors", "steps", "period", "cycles", "direction"]);
|
|
2653
2752
|
const steps = num(o.steps, `${fx}.steps`, 12, "steps");
|
|
@@ -2919,14 +3018,14 @@ var mix = (a, b2, k) => {
|
|
|
2919
3018
|
const x = rgb(a), y = rgb(b2);
|
|
2920
3019
|
return hex(x.map((v, i) => lerp(v, y[i], clamp(k))));
|
|
2921
3020
|
};
|
|
2922
|
-
function gradient(stops,
|
|
3021
|
+
function gradient(stops, n2) {
|
|
2923
3022
|
if (!Array.isArray(stops) || !stops.length)
|
|
2924
3023
|
fail2("a gradient takes one or more colours as #rrggbb");
|
|
2925
|
-
if (!Number.isInteger(
|
|
2926
|
-
fail2(`a gradient takes a whole number of colours of 1 or more, not ${String(
|
|
3024
|
+
if (!Number.isInteger(n2) || n2 < 1)
|
|
3025
|
+
fail2(`a gradient takes a whole number of colours of 1 or more, not ${String(n2)}`);
|
|
2927
3026
|
const c = stops.map(rgb);
|
|
2928
|
-
return Array.from({ length:
|
|
2929
|
-
const k =
|
|
3027
|
+
return Array.from({ length: n2 }, (_, i) => {
|
|
3028
|
+
const k = n2 > 1 ? i / (n2 - 1) * (c.length - 1) : 0;
|
|
2930
3029
|
const a = Math.min(c.length - 2, Math.floor(k));
|
|
2931
3030
|
return c.length === 1 ? hex(c[0]) : hex(c[a].map((v, j) => lerp(v, c[a + 1][j], k - a)));
|
|
2932
3031
|
});
|
|
@@ -3162,7 +3261,7 @@ function colorOf(v, what = "color") {
|
|
|
3162
3261
|
function didYouMean(word) {
|
|
3163
3262
|
const all = [.../* @__PURE__ */ new Set([...Object.keys(schemes), ...Object.keys(materialColors)])];
|
|
3164
3263
|
const near = nearWords(word, all);
|
|
3165
|
-
return near.length ? `did you mean ${near.map((
|
|
3264
|
+
return near.length ? `did you mean ${near.map((n2) => JSON.stringify(n2)).join(" or ")}?` : `a colour of your own is #rrggbb, and the names are ${and2(all)}`;
|
|
3166
3265
|
}
|
|
3167
3266
|
function lettersApart(a, b2) {
|
|
3168
3267
|
const d = Array.from({ length: b2.length + 1 }, (_, j) => j);
|
|
@@ -3230,14 +3329,14 @@ var SYNONYMS = {
|
|
|
3230
3329
|
function nearWords(word, words2) {
|
|
3231
3330
|
const w = word.toLowerCase().slice(0, 40);
|
|
3232
3331
|
const same = (SYNONYMS[w] ?? []).filter((s2) => words2.includes(s2) && s2 !== word);
|
|
3233
|
-
const close = words2.filter((
|
|
3332
|
+
const close = words2.filter((n2) => !same.includes(n2) && lettersApart(w, n2.toLowerCase()) <= (w.length <= 4 ? 1 : 2));
|
|
3234
3333
|
return [...same, ...close].slice(0, 3);
|
|
3235
3334
|
}
|
|
3236
3335
|
function suggest(word, words2) {
|
|
3237
3336
|
if (typeof word !== "string")
|
|
3238
3337
|
return "";
|
|
3239
3338
|
const near = nearWords(word, words2);
|
|
3240
|
-
return near.length ? ` (did you mean ${near.map((
|
|
3339
|
+
return near.length ? ` (did you mean ${near.map((n2) => JSON.stringify(n2)).join(" or ")}?)` : "";
|
|
3241
3340
|
}
|
|
3242
3341
|
function mergePalettes(parts) {
|
|
3243
3342
|
const palette = [];
|
|
@@ -3426,8 +3525,8 @@ var Surface = class _Surface {
|
|
|
3426
3525
|
const color = env.color;
|
|
3427
3526
|
if (color && this.palette) {
|
|
3428
3527
|
const ink = this.palette.ink(this.paper);
|
|
3429
|
-
const
|
|
3430
|
-
for (let i = 0; i <
|
|
3528
|
+
const n2 = Math.min(color.length, chars3.length);
|
|
3529
|
+
for (let i = 0; i < n2; i++)
|
|
3431
3530
|
color[i] = colors[i] === NONE ? ink : colors[i];
|
|
3432
3531
|
}
|
|
3433
3532
|
const line2 = this.#line;
|
|
@@ -3515,8 +3614,8 @@ var Chainable = class {
|
|
|
3515
3614
|
freeze(at3) {
|
|
3516
3615
|
return freeze(this, at3);
|
|
3517
3616
|
}
|
|
3518
|
-
pad(
|
|
3519
|
-
return pad(this,
|
|
3617
|
+
pad(n2) {
|
|
3618
|
+
return pad(this, n2);
|
|
3520
3619
|
}
|
|
3521
3620
|
border(o) {
|
|
3522
3621
|
return border(this, o);
|
|
@@ -3631,11 +3730,11 @@ function asPiece(src, name = "text") {
|
|
|
3631
3730
|
const meta3 = checkMeta({ name, category: "type", note: name, cols: grid2.cols, rows: grid2.rows, fps: 0, ...palette ? { palette: palette.colors } : {} });
|
|
3632
3731
|
const other = grid2.clone();
|
|
3633
3732
|
if (palette?.themed) {
|
|
3634
|
-
const
|
|
3733
|
+
const n2 = palette.size;
|
|
3635
3734
|
for (let i = 0; i < other.colors.length; i++) {
|
|
3636
3735
|
const c = other.colors[i];
|
|
3637
3736
|
if (c !== NONE)
|
|
3638
|
-
other.colors[i] = grid2.paper ? c <
|
|
3737
|
+
other.colors[i] = grid2.paper ? c < n2 ? c + n2 : c : c >= n2 ? c - n2 : c;
|
|
3639
3738
|
}
|
|
3640
3739
|
}
|
|
3641
3740
|
other.paper = !grid2.paper;
|
|
@@ -3651,7 +3750,7 @@ function asPiece(src, name = "text") {
|
|
|
3651
3750
|
lines.shift();
|
|
3652
3751
|
while (lines.length > 1 && !lines[lines.length - 1].trim())
|
|
3653
3752
|
lines.pop();
|
|
3654
|
-
const indent = lines.reduce((
|
|
3753
|
+
const indent = lines.reduce((n2, l) => l.trim() ? Math.min(n2, l.length - l.trimStart().length) : n2, Infinity);
|
|
3655
3754
|
if (Number.isFinite(indent))
|
|
3656
3755
|
lines = lines.map((l) => l.slice(indent));
|
|
3657
3756
|
}
|
|
@@ -3695,9 +3794,9 @@ function snapshot(src, t = 0, { paper = false, mono = false, options = {} } = {}
|
|
|
3695
3794
|
// ../../node_modules/ascii.rest/dist/kit/math.js
|
|
3696
3795
|
var said = (v) => typeof v === "string" ? JSON.stringify(v) : Array.isArray(v) ? `[${v.map(said).join(", ")}]` : String(v);
|
|
3697
3796
|
var finite2 = (v) => typeof v === "number" && Number.isFinite(v);
|
|
3698
|
-
function mod2(a,
|
|
3699
|
-
const r = a %
|
|
3700
|
-
return r < 0 ? r +
|
|
3797
|
+
function mod2(a, n2) {
|
|
3798
|
+
const r = a % n2;
|
|
3799
|
+
return r < 0 ? r + n2 < n2 ? r + n2 : 0 : r + 0;
|
|
3701
3800
|
}
|
|
3702
3801
|
var tables = [];
|
|
3703
3802
|
function table(seed) {
|
|
@@ -3862,7 +3961,7 @@ var H3 = 1 / 6;
|
|
|
3862
3961
|
var F4 = (Math.sqrt(5) - 1) / 4;
|
|
3863
3962
|
var H4 = (5 - Math.sqrt(5)) / 20;
|
|
3864
3963
|
var S4 = 27;
|
|
3865
|
-
var unit = (
|
|
3964
|
+
var unit = (n2) => n2 <= -1 ? 0 : n2 >= 1 ? 1 : 0.5 + 0.5 * n2;
|
|
3866
3965
|
var dot4 = (g, a, b2, c, d) => G4[g] * a + G4[g + 1] * b2 + G4[g + 2] * c + G4[g + 3] * d;
|
|
3867
3966
|
function simplex4(x, y, z, w, seed) {
|
|
3868
3967
|
const { perm } = table(seed);
|
|
@@ -3905,26 +4004,26 @@ function simplex4(x, y, z, w, seed) {
|
|
|
3905
4004
|
const x3 = x0 - i3 + 3 * H4, y3 = y0 - j3 + 3 * H4, z3 = z0 - l3 + 3 * H4, w3 = w0 - m3 + 3 * H4;
|
|
3906
4005
|
const x4 = x0 - 1 + 4 * H4, y4 = y0 - 1 + 4 * H4, z4 = z0 - 1 + 4 * H4, w4 = w0 - 1 + 4 * H4;
|
|
3907
4006
|
const ii = i & 255, jj = j & 255, ll = l & 255, mm = m & 255;
|
|
3908
|
-
let
|
|
4007
|
+
let n2 = 0, k;
|
|
3909
4008
|
if ((k = 0.6 - x0 * x0 - y0 * y0 - z0 * z0 - w0 * w0) > 0)
|
|
3910
|
-
k *= k,
|
|
4009
|
+
k *= k, n2 += k * k * dot4((perm[ii + perm[jj + perm[ll + perm[mm]]]] & 31) * 4, x0, y0, z0, w0);
|
|
3911
4010
|
if ((k = 0.6 - x1 * x1 - y1 * y1 - z1 * z1 - w1 * w1) > 0)
|
|
3912
|
-
k *= k,
|
|
4011
|
+
k *= k, n2 += k * k * dot4((perm[ii + i1 + perm[jj + j1 + perm[ll + l1 + perm[mm + m1]]]] & 31) * 4, x1, y1, z1, w1);
|
|
3913
4012
|
if ((k = 0.6 - x2 * x2 - y2 * y2 - z2 * z2 - w2 * w2) > 0)
|
|
3914
|
-
k *= k,
|
|
4013
|
+
k *= k, n2 += k * k * dot4((perm[ii + i2 + perm[jj + j2 + perm[ll + l2 + perm[mm + m2]]]] & 31) * 4, x2, y2, z2, w2);
|
|
3915
4014
|
if ((k = 0.6 - x3 * x3 - y3 * y3 - z3 * z3 - w3 * w3) > 0)
|
|
3916
|
-
k *= k,
|
|
4015
|
+
k *= k, n2 += k * k * dot4((perm[ii + i3 + perm[jj + j3 + perm[ll + l3 + perm[mm + m3]]]] & 31) * 4, x3, y3, z3, w3);
|
|
3917
4016
|
if ((k = 0.6 - x4 * x4 - y4 * y4 - z4 * z4 - w4 * w4) > 0)
|
|
3918
|
-
k *= k,
|
|
3919
|
-
return S4 *
|
|
4017
|
+
k *= k, n2 += k * k * dot4((perm[ii + 1 + perm[jj + 1 + perm[ll + 1 + perm[mm + 1]]]] & 31) * 4, x4, y4, z4, w4);
|
|
4018
|
+
return S4 * n2;
|
|
3920
4019
|
}
|
|
3921
4020
|
var OFF = Array.from({ length: 16 }, (_, i) => [i * 0.6180339887 % 1 * 97.3, i * 0.7548776662 % 1 * 89.1]);
|
|
3922
4021
|
var octN = 4;
|
|
3923
4022
|
var octLac = 2;
|
|
3924
4023
|
var octGain = 0.5;
|
|
3925
4024
|
var octSeed = 0;
|
|
3926
|
-
function octaves(o,
|
|
3927
|
-
octN = o?.octaves ??
|
|
4025
|
+
function octaves(o, n2 = 4) {
|
|
4026
|
+
octN = o?.octaves ?? n2;
|
|
3928
4027
|
octLac = o?.lacunarity ?? 2;
|
|
3929
4028
|
octGain = o?.gain ?? 0.5;
|
|
3930
4029
|
octSeed = o?.seed ?? 0;
|
|
@@ -3939,8 +4038,8 @@ function octaves(o, n = 4) {
|
|
|
3939
4038
|
}
|
|
3940
4039
|
var SPREAD = 1.45;
|
|
3941
4040
|
var WIDEN4 = 1.35;
|
|
3942
|
-
var crest = (
|
|
3943
|
-
const r = 1 - (
|
|
4041
|
+
var crest = (n2) => {
|
|
4042
|
+
const r = 1 - (n2 < 0 ? -n2 : n2);
|
|
3944
4043
|
return r < 0 ? 0 : r * r;
|
|
3945
4044
|
};
|
|
3946
4045
|
var weigh = (r) => r > 0.5 ? 1 : 2 * r;
|
|
@@ -3955,10 +4054,10 @@ function loopNoise(x, y, t, period2, o) {
|
|
|
3955
4054
|
const z = radius * Math.cos(a), w = radius * Math.sin(a);
|
|
3956
4055
|
let sum = 0, amp = 1, norm = 0, f = 1, weight = 1;
|
|
3957
4056
|
for (let i = 0; i < octN; i++) {
|
|
3958
|
-
let
|
|
4057
|
+
let n2 = simplex4(x * f + OFF[i][0], y * f + OFF[i][1], z * f, w * f, octSeed) * WIDEN4;
|
|
3959
4058
|
if (ridge)
|
|
3960
|
-
|
|
3961
|
-
sum += amp *
|
|
4059
|
+
n2 = crest(n2) * weight, weight = weigh(n2);
|
|
4060
|
+
sum += amp * n2;
|
|
3962
4061
|
norm += amp;
|
|
3963
4062
|
amp *= octGain;
|
|
3964
4063
|
f *= octLac;
|
|
@@ -3971,14 +4070,14 @@ var B3 = B1 + 1;
|
|
|
3971
4070
|
var E4 = TAU / 3;
|
|
3972
4071
|
var E5 = TAU / 4.5;
|
|
3973
4072
|
var bounce = (k) => {
|
|
3974
|
-
const
|
|
4073
|
+
const n2 = 7.5625, d = 2.75;
|
|
3975
4074
|
if (k < 1 / d)
|
|
3976
|
-
return
|
|
4075
|
+
return n2 * k * k;
|
|
3977
4076
|
if (k < 2 / d)
|
|
3978
|
-
return
|
|
4077
|
+
return n2 * (k -= 1.5 / d) * k + 0.75;
|
|
3979
4078
|
if (k < 2.5 / d)
|
|
3980
|
-
return
|
|
3981
|
-
return
|
|
4079
|
+
return n2 * (k -= 2.25 / d) * k + 0.9375;
|
|
4080
|
+
return n2 * (k -= 2.625 / d) * k + 0.984375;
|
|
3982
4081
|
};
|
|
3983
4082
|
var curves = {
|
|
3984
4083
|
linear: (k) => k,
|
|
@@ -4149,7 +4248,7 @@ function brush(ch, what) {
|
|
|
4149
4248
|
fail2(`${what} takes one character, or "" to clear, not ${show3(ch)}`);
|
|
4150
4249
|
return code(ch);
|
|
4151
4250
|
}
|
|
4152
|
-
var finite3 = (...v) => v.every((
|
|
4251
|
+
var finite3 = (...v) => v.every((n2) => typeof n2 === "number" && Number.isFinite(n2));
|
|
4153
4252
|
function show3(v, depth = 0) {
|
|
4154
4253
|
if (typeof v === "string")
|
|
4155
4254
|
return JSON.stringify(v.length > 40 ? v.slice(0, 40) + "\u2026" : v);
|
|
@@ -4159,7 +4258,7 @@ function show3(v, depth = 0) {
|
|
|
4159
4258
|
return String(v);
|
|
4160
4259
|
if (depth > 1)
|
|
4161
4260
|
return Array.isArray(v) ? "[...]" : "{...}";
|
|
4162
|
-
const more = (
|
|
4261
|
+
const more = (n2) => n2 > 6 ? ", ..." : "";
|
|
4163
4262
|
if (Array.isArray(v))
|
|
4164
4263
|
return `[${Array.from(v.slice(0, 6), (x) => show3(x, depth + 1)).join(", ")}${more(v.length)}]`;
|
|
4165
4264
|
const entries = Object.entries(v);
|
|
@@ -4219,12 +4318,12 @@ function trace(x0, y0, x1, y1, w, h) {
|
|
|
4219
4318
|
const dx = Math.abs(x1 - x0), dy = -Math.abs(y1 - y0), sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1;
|
|
4220
4319
|
if (XS.length < dx - dy + 1)
|
|
4221
4320
|
XS = new Int32Array(dx - dy + 1), YS = new Int32Array(dx - dy + 1);
|
|
4222
|
-
let err = dx + dy,
|
|
4321
|
+
let err = dx + dy, n2 = 0;
|
|
4223
4322
|
for (; ; ) {
|
|
4224
|
-
XS[
|
|
4225
|
-
YS[
|
|
4323
|
+
XS[n2] = x0;
|
|
4324
|
+
YS[n2++] = y0;
|
|
4226
4325
|
if (x0 === x1 && y0 === y1)
|
|
4227
|
-
return
|
|
4326
|
+
return n2;
|
|
4228
4327
|
const e2 = 2 * err;
|
|
4229
4328
|
if (e2 >= dy)
|
|
4230
4329
|
err += dy, x0 += sx;
|
|
@@ -4261,11 +4360,11 @@ function glyph(a, round, weight) {
|
|
|
4261
4360
|
return ROUND.get(a);
|
|
4262
4361
|
let c = GLYPH.get(a);
|
|
4263
4362
|
if (c === void 0) {
|
|
4264
|
-
let
|
|
4363
|
+
let n2 = 0;
|
|
4265
4364
|
for (let k = 0; k < 8; k += 2)
|
|
4266
4365
|
if (a >> k & 3)
|
|
4267
|
-
|
|
4268
|
-
c = GLYPH.get(
|
|
4366
|
+
n2 |= weight << k;
|
|
4367
|
+
c = GLYPH.get(n2);
|
|
4269
4368
|
}
|
|
4270
4369
|
return c ?? DOT;
|
|
4271
4370
|
}
|
|
@@ -4283,14 +4382,14 @@ function linked(s2, j, b2, ascii) {
|
|
|
4283
4382
|
const nx = x + (k === AR ? 1 : k === AL ? -1 : 0), ny = y + (k === AD ? 1 : k === AU ? -1 : 0);
|
|
4284
4383
|
if (nx < 0 || ny < 0 || nx >= s2.cols || ny >= s2.rows)
|
|
4285
4384
|
continue;
|
|
4286
|
-
const
|
|
4385
|
+
const n2 = s2.chars[ny * s2.cols + nx], nb = ascii ? asciiArms(n2) : ARMS.get(n2);
|
|
4287
4386
|
if (nb !== void 0 && nb >> (k ^ 4) & 3)
|
|
4288
4387
|
m |= b2 & 3 << k;
|
|
4289
4388
|
}
|
|
4290
4389
|
return m;
|
|
4291
4390
|
}
|
|
4292
|
-
function place3(s2, j, c, k,
|
|
4293
|
-
if (
|
|
4391
|
+
function place3(s2, j, c, k, join7, a = ARMS.get(c), ascii = false, round = false) {
|
|
4392
|
+
if (join7 && a !== void 0) {
|
|
4294
4393
|
const old = s2.chars[j], b2 = ascii ? asciiArms(old) : ARMS.get(old);
|
|
4295
4394
|
if (b2 !== void 0) {
|
|
4296
4395
|
const kept = linked(s2, j, b2, ascii);
|
|
@@ -4408,8 +4507,8 @@ function write(s2, x, y, str2, o, region) {
|
|
|
4408
4507
|
let lines = layout(t, bw, wrap2);
|
|
4409
4508
|
if (lines.length > bh)
|
|
4410
4509
|
lines = lines.slice(0, bh);
|
|
4411
|
-
const
|
|
4412
|
-
const top = valign === "top" ? fy : valign === "middle" ? fy + Math.floor((bh -
|
|
4510
|
+
const n2 = lines.length;
|
|
4511
|
+
const top = valign === "top" ? fy : valign === "middle" ? fy + Math.floor((bh - n2) / 2) : fy + bh - n2;
|
|
4413
4512
|
const lefts = lines.map((l) => align === "left" ? fx : align === "center" ? fx + Math.floor((bw - l.length) / 2) : fx + bw - l.length);
|
|
4414
4513
|
return writeLines(s2, lines, lefts, top, k, transparent);
|
|
4415
4514
|
}
|
|
@@ -4422,7 +4521,7 @@ function label(s2, x, y, str2, o) {
|
|
|
4422
4521
|
if (!finite3(x, y) || t === "")
|
|
4423
4522
|
return { x: finite3(x) ? Math.floor(x) : 0, y: finite3(y) ? Math.floor(y) : 0, cols: 0, rows: 0 };
|
|
4424
4523
|
const lines = t.split("\n");
|
|
4425
|
-
const start = (v,
|
|
4524
|
+
const start = (v, n2) => Math.floor(v - n2 / 2 + 0.5);
|
|
4426
4525
|
return writeLines(s2, lines, lines.map((l) => start(x, l.length)), start(y, lines.length), k, transparent);
|
|
4427
4526
|
}
|
|
4428
4527
|
function around2(on, cx, cy, r, turn) {
|
|
@@ -4456,7 +4555,7 @@ function rect(s2, x, y, w, h, o) {
|
|
|
4456
4555
|
if (title)
|
|
4457
4556
|
for (const ch of title)
|
|
4458
4557
|
code(ch);
|
|
4459
|
-
const
|
|
4558
|
+
const join7 = yes(o?.join, "join", true);
|
|
4460
4559
|
const k = paint(s2, color), fk = fillColor === void 0 ? k : paint(s2, fillColor);
|
|
4461
4560
|
if (!finite3(x, y, w, h))
|
|
4462
4561
|
return { x: 0, y: 0, cols: 0, rows: 0 };
|
|
@@ -4479,10 +4578,10 @@ function rect(s2, x, y, w, h, o) {
|
|
|
4479
4578
|
const put = (cx, cy, e, reach = 255) => {
|
|
4480
4579
|
const j = s2.index(cx, cy), a = ascii ? EDGE_ARMS[e] : ARMS.get(ed[e]);
|
|
4481
4580
|
if (j >= 0)
|
|
4482
|
-
place3(s2, j, ed[e], k,
|
|
4581
|
+
place3(s2, j, ed[e], k, join7, a === void 0 ? a : a & reach, ascii, round);
|
|
4483
4582
|
};
|
|
4484
|
-
const end = (i,
|
|
4485
|
-
const from = (o2, lo) => Math.max(lo, -o2), to = (o2, hi,
|
|
4583
|
+
const end = (i, n2, first, last) => n2 < 2 ? 255 : i === 0 ? 3 << first : i === n2 - 1 ? 3 << last : 255;
|
|
4584
|
+
const from = (o2, lo) => Math.max(lo, -o2), to = (o2, hi, n2) => Math.min(hi, n2 - o2);
|
|
4486
4585
|
if (h === 1)
|
|
4487
4586
|
for (let i = from(x, 0); i < to(x, w, s2.cols); i++)
|
|
4488
4587
|
put(x + i, y, 4, end(i, w, AR, AL));
|
|
@@ -4530,11 +4629,11 @@ function ellipseCells(w, h, cx, cy, rx, ry, inside, visit, aspect = 0, from = 0,
|
|
|
4530
4629
|
const j0 = Math.max(0, Math.floor(cy - ry) - 1), j1 = Math.min(h - 1, Math.ceil(cy + ry));
|
|
4531
4630
|
if (i0 > i1 || j0 > j1)
|
|
4532
4631
|
return;
|
|
4533
|
-
const bw = i1 - i0 + 1,
|
|
4534
|
-
if (EDGE.length <
|
|
4535
|
-
EDGE = new Uint8Array(
|
|
4632
|
+
const bw = i1 - i0 + 1, n2 = bw * (j1 - j0 + 1);
|
|
4633
|
+
if (EDGE.length < n2)
|
|
4634
|
+
EDGE = new Uint8Array(n2);
|
|
4536
4635
|
const edge = EDGE;
|
|
4537
|
-
edge.fill(0, 0,
|
|
4636
|
+
edge.fill(0, 0, n2);
|
|
4538
4637
|
const mark = (i2, j2, kind) => {
|
|
4539
4638
|
if (i2 >= i0 && i2 <= i1 && j2 >= j0 && j2 <= j1)
|
|
4540
4639
|
edge[(j2 - j0) * bw + i2 - i0] |= kind;
|
|
@@ -4558,7 +4657,7 @@ function ellipseCells(w, h, cx, cy, rx, ry, inside, visit, aspect = 0, from = 0,
|
|
|
4558
4657
|
mark(Math.floor(cx - xo), j2, steep ? 2 : 8);
|
|
4559
4658
|
mark(Math.ceil(cx + xo) - 1, j2, steep ? 2 : 8);
|
|
4560
4659
|
}
|
|
4561
|
-
for (let k = 0; k <
|
|
4660
|
+
for (let k = 0; k < n2; k++)
|
|
4562
4661
|
edge[k] = edge[k] & 3 | ((edge[k] & 12) === 12 ? 1 : 0);
|
|
4563
4662
|
for (let j2 = j0; j2 <= j1; j2++)
|
|
4564
4663
|
for (let i2 = i0; i2 <= i1; i2++) {
|
|
@@ -4615,11 +4714,11 @@ function shape(s2, cx, cy, rx, ry, o, what, from = 0, span = 1) {
|
|
|
4615
4714
|
const ch = char === "auto" ? "auto" : brush(char, `${/^[aeiou]/.test(what) ? "an" : "a"} ${what}'s char`);
|
|
4616
4715
|
const f = fill === void 0 || fill === false ? null : brush(fill, "fill");
|
|
4617
4716
|
const k = paint(s2, color), fk = fillColor === void 0 ? k : paint(s2, fillColor);
|
|
4618
|
-
const
|
|
4717
|
+
const join7 = yes(o?.join, "join", true);
|
|
4619
4718
|
ellipseCells(s2.cols, s2.rows, cx, cy, rx, ry, f !== null, (x, y, edge, slope) => {
|
|
4620
4719
|
const j = y * s2.cols + x;
|
|
4621
4720
|
if (edge)
|
|
4622
|
-
place3(s2, j, ch === "auto" ? slope : ch, k,
|
|
4721
|
+
place3(s2, j, ch === "auto" ? slope : ch, k, join7 && ch !== "auto");
|
|
4623
4722
|
else
|
|
4624
4723
|
s2.put(j, f, f === EMPTY ? NONE : fk);
|
|
4625
4724
|
}, ch === "auto" ? s2.aspect : 0, from, span);
|
|
@@ -4700,8 +4799,8 @@ var BrailleCanvas = class {
|
|
|
4700
4799
|
}
|
|
4701
4800
|
line(x0, y0, x1, y1, color) {
|
|
4702
4801
|
const k = this.#k(color);
|
|
4703
|
-
const
|
|
4704
|
-
for (let i = 0; i <
|
|
4802
|
+
const n2 = trace(x0, y0, x1, y1, this.width, this.height);
|
|
4803
|
+
for (let i = 0; i < n2; i++)
|
|
4705
4804
|
this.#dot(XS[i], YS[i], k);
|
|
4706
4805
|
}
|
|
4707
4806
|
ray(x, y, length, turn, color) {
|
|
@@ -4750,12 +4849,12 @@ var BrailleCanvas = class {
|
|
|
4750
4849
|
y1 ??= 1;
|
|
4751
4850
|
if (y0 === y1)
|
|
4752
4851
|
fail2(`plot's y0 and y1 take two different numbers, the values at the bottom and the top, not ${y0} and ${y1}`);
|
|
4753
|
-
const w = this.width, h = this.height,
|
|
4852
|
+
const w = this.width, h = this.height, n2 = list2 ? list2.length : w;
|
|
4754
4853
|
const top = fill ? new Int32Array(w).fill(h) : null;
|
|
4755
4854
|
let px = 0, py = NaN;
|
|
4756
|
-
for (let i = 0; i <
|
|
4855
|
+
for (let i = 0; i < n2; i++) {
|
|
4757
4856
|
const v = list2 ? list2[i] : f(x0 + (x1 - x0) * i / Math.max(1, w - 1));
|
|
4758
|
-
const x = list2 ?
|
|
4857
|
+
const x = list2 ? n2 > 1 ? Math.round(i * (w - 1) / (n2 - 1)) : 0 : i;
|
|
4759
4858
|
const y = typeof v === "number" && Number.isFinite(v) ? Math.round((y1 - v) / (y1 - y0) * (h - 1)) : NaN;
|
|
4760
4859
|
if (Number.isFinite(y)) {
|
|
4761
4860
|
const m = Number.isFinite(py) ? trace(px, py, x, y, w, h) : trace(x, y, x, y, w, h);
|
|
@@ -4854,7 +4953,7 @@ function tones(accent, more = []) {
|
|
|
4854
4953
|
var WIDGET = ["color", ...NAMING];
|
|
4855
4954
|
var naming2 = (o, name, note) => ({ name: o.name ?? name, note: (o.note ?? note).slice(0, 72) });
|
|
4856
4955
|
var framing = (p, framed) => framed ? Object.assign(p, { frames: true }) : p;
|
|
4857
|
-
function amountOf(what, v, min, max,
|
|
4956
|
+
function amountOf(what, v, min, max, share2) {
|
|
4858
4957
|
if (v === void 0)
|
|
4859
4958
|
return void 0;
|
|
4860
4959
|
if (typeof v === "function") {
|
|
@@ -4870,10 +4969,10 @@ function amountOf(what, v, min, max, share) {
|
|
|
4870
4969
|
return min + +m[1] / 100 * (max - min);
|
|
4871
4970
|
}
|
|
4872
4971
|
if (typeof v !== "number" || !Number.isFinite(v) || v < min || v > max) {
|
|
4873
|
-
const hint =
|
|
4972
|
+
const hint = share2 && typeof v === "number" && v > 1 && v <= 100 ? `: did you mean ${+(v / 100).toFixed(4)}, or "${v}%"?` : "";
|
|
4874
4973
|
fail2(`${what}'s value takes a number from ${min} to ${max}, a percentage such as "64%", or a function that reads one, not ${show2(v)}${hint}`);
|
|
4875
4974
|
}
|
|
4876
|
-
if (!
|
|
4975
|
+
if (!share2 && min === 0 && max === 100 && v > 0 && v < 1)
|
|
4877
4976
|
fail2(`${what}'s value is a reading from 0 to 100, and ${v} is less than 1: did you mean ${+(v * 100).toFixed(2)}, or "${+(v * 100).toFixed(2)}%"? Give it max: 1 for a reading from 0 to 1`);
|
|
4878
4977
|
return v;
|
|
4879
4978
|
}
|
|
@@ -4894,24 +4993,24 @@ function progressBar(o) {
|
|
|
4894
4993
|
const smooth = ease.inOutSine;
|
|
4895
4994
|
const loop = loopOf(fill + 1, true);
|
|
4896
4995
|
const cols = label2.length + width + (percent ? 5 : 0);
|
|
4897
|
-
const
|
|
4996
|
+
const share2 = (v) => (v - min) / (max - min);
|
|
4898
4997
|
const timing = live ? { fps: 4, clock: true } : value === void 0 ? { fps: 24, loop, still: fill } : { fps: 0 };
|
|
4899
|
-
const note = live ? "a progress bar, live" : value === void 0 ? "a progress bar filling" : `a progress bar at ${Math.round(
|
|
4998
|
+
const note = live ? "a progress bar, live" : value === void 0 ? "a progress bar filling" : `a progress bar at ${Math.round(share2(value) * 100)}%`;
|
|
4900
4999
|
return piece({ ...naming2(p, label2.trim() || "progress", note), category: "ui", cols, rows: 1, ...timing, palette: tones(themed2("progressBar's color", p.color)) }, (t, s2) => {
|
|
4901
|
-
const k = typeof value === "function" ?
|
|
5000
|
+
const k = typeof value === "function" ? share2(value()) : value !== void 0 ? share2(value) : smooth(Math.min(1, (t % loop + loop) % loop / fill));
|
|
4902
5001
|
const color = k >= 1 ? GOOD : ACCENT;
|
|
4903
5002
|
s2.write(0, 0, label2);
|
|
4904
5003
|
const x = label2.length;
|
|
4905
5004
|
if (style === "ascii") {
|
|
4906
|
-
const
|
|
5005
|
+
const n2 = Math.round(k * (width - 2));
|
|
4907
5006
|
s2.write(x, 0, "[", MUTED);
|
|
4908
5007
|
for (let i = 0; i < width - 2; i++)
|
|
4909
|
-
s2.set(x + 1 + i, 0, i <
|
|
5008
|
+
s2.set(x + 1 + i, 0, i < n2 ? "#" : "-", i < n2 ? color : MUTED);
|
|
4910
5009
|
s2.write(x + width - 1, 0, "]", MUTED);
|
|
4911
5010
|
} else if (style === "dots") {
|
|
4912
|
-
const
|
|
5011
|
+
const n2 = Math.round(k * width);
|
|
4913
5012
|
for (let i = 0; i < width; i++)
|
|
4914
|
-
s2.set(x + i, 0, i <
|
|
5013
|
+
s2.set(x + i, 0, i < n2 ? "\u25CF" : "\u25CB", i < n2 ? color : MUTED);
|
|
4915
5014
|
} else {
|
|
4916
5015
|
const eighths = Math.round(k * width * 8);
|
|
4917
5016
|
for (let i = 0; i < width; i++) {
|
|
@@ -4957,7 +5056,7 @@ function sparkline(data, o) {
|
|
|
4957
5056
|
[data, o] = [void 0, data];
|
|
4958
5057
|
}
|
|
4959
5058
|
const p = optionsOf3("sparkline()", o, ["label", "width", "height", "fill", ...WIDGET]);
|
|
4960
|
-
const numbers = (v) => Array.isArray(v) && v.length >= 2 && v.every((
|
|
5059
|
+
const numbers = (v) => Array.isArray(v) && v.length >= 2 && v.every((n2) => typeof n2 === "number" && Number.isFinite(n2));
|
|
4961
5060
|
const live = typeof data === "function" ? data : null;
|
|
4962
5061
|
if (data !== void 0 && !live && !numbers(data))
|
|
4963
5062
|
fail2(`sparkline() takes a list of two or more numbers, such as [3, 5, 2, 8], or a function that returns one, not ${show2(data)}`);
|
|
@@ -5111,7 +5210,7 @@ var meta2 = {
|
|
|
5111
5210
|
};
|
|
5112
5211
|
var QUAD = " \u2597\u2596\u2584\u259D\u2590\u259E\u259F\u2598\u259A\u258C\u2599\u2580\u259C\u259B\u2588";
|
|
5113
5212
|
var quad = (ul, ur, ll, lr) => QUAD[ul << 3 | ur << 2 | ll << 1 | lr];
|
|
5114
|
-
var ping = (
|
|
5213
|
+
var ping = (n2, i) => i % (2 * n2 - 2) < n2 ? i % (2 * n2 - 2) : 2 * n2 - 2 - i % (2 * n2 - 2);
|
|
5115
5214
|
var blank = (w, h) => Array.from({ length: h }, () => new Array(w).fill(" "));
|
|
5116
5215
|
var rows = (g) => g.map((r) => r.join(""));
|
|
5117
5216
|
var plot = (w, h, cells) => {
|
|
@@ -5143,8 +5242,8 @@ var SPINNERS = [
|
|
|
5143
5242
|
}],
|
|
5144
5243
|
["arc", 1, (i) => {
|
|
5145
5244
|
const ring2 = [[0, 0, "\u256D"], [1, 0, "\u2500"], [2, 0, "\u2500"], [3, 0, "\u2500"], [4, 0, "\u256E"], [4, 1, "\u2502"], [4, 2, "\u256F"], [3, 2, "\u2500"], [2, 2, "\u2500"], [1, 2, "\u2500"], [0, 2, "\u2570"], [0, 1, "\u2502"]];
|
|
5146
|
-
const
|
|
5147
|
-
return plot(5, 3, [...Array(
|
|
5245
|
+
const n2 = 3 + Math.round(ping(8, i) * 4 / 7);
|
|
5246
|
+
return plot(5, 3, [...Array(n2)].map((_, k) => ring2[(i + 12 - k) % 12]));
|
|
5148
5247
|
}],
|
|
5149
5248
|
["bounce", 1, (i) => ["[" + " ".repeat(ping(6, i)) + "o" + " ".repeat(5 - ping(6, i)) + "]"]],
|
|
5150
5249
|
["clock", 3, (i) => {
|
|
@@ -5176,7 +5275,7 @@ var SPINNERS = [
|
|
|
5176
5275
|
}]
|
|
5177
5276
|
];
|
|
5178
5277
|
function spinners2({ names = meta2.options.names, labels = meta2.options.labels, speed: speed2 = meta2.options.speed } = {}) {
|
|
5179
|
-
const chosen = names && names.length ? names.map((
|
|
5278
|
+
const chosen = names && names.length ? names.map((n2) => SPINNERS.find((s2) => s2[0] === n2)).filter(Boolean) : SPINNERS;
|
|
5180
5279
|
const all = chosen === SPINNERS;
|
|
5181
5280
|
const slot2 = 11, per = all ? 6 : Math.min(6, chosen.length);
|
|
5182
5281
|
const block = labels || all ? 5 : 3;
|
|
@@ -5184,8 +5283,8 @@ function spinners2({ names = meta2.options.names, labels = meta2.options.labels,
|
|
|
5184
5283
|
const height = all ? meta2.rows : Math.ceil(chosen.length / per) * (block + 1) + 1;
|
|
5185
5284
|
return (t) => {
|
|
5186
5285
|
const grid2 = Array.from({ length: height }, () => new Array(cols).fill(" "));
|
|
5187
|
-
chosen.forEach(([name, frames, draw2],
|
|
5188
|
-
const x0 =
|
|
5286
|
+
chosen.forEach(([name, frames, draw2], n2) => {
|
|
5287
|
+
const x0 = n2 % per * slot2, y0 = 1 + Math.floor(n2 / per) * (block + 1);
|
|
5189
5288
|
const art = draw2(Math.floor(t * meta2.fps * speed2 / frames + 1e-3));
|
|
5190
5289
|
const top = y0 + Math.floor((3 - art.length) / 2);
|
|
5191
5290
|
art.forEach((line2, j) => {
|
|
@@ -5293,8 +5392,8 @@ function cached(cache, key, make) {
|
|
|
5293
5392
|
return v;
|
|
5294
5393
|
}
|
|
5295
5394
|
var bars = /* @__PURE__ */ new Map();
|
|
5296
|
-
function bar(
|
|
5297
|
-
const value = Math.max(0, Math.min(1, Number.isFinite(
|
|
5395
|
+
function bar(share2, width, opts2 = {}) {
|
|
5396
|
+
const value = Math.max(0, Math.min(1, Number.isFinite(share2) ? share2 : 0));
|
|
5298
5397
|
const w = Math.max(4, Math.round(width));
|
|
5299
5398
|
const key = `${w}|${opts2.label ?? ""}|${opts2.percent !== false}|${value.toFixed(4)}`;
|
|
5300
5399
|
const b2 = cached(bars, key, () => {
|
|
@@ -5330,12 +5429,12 @@ function dial(value, max, opts2 = {}) {
|
|
|
5330
5429
|
return frameOf(g.piece, g.frame, 0);
|
|
5331
5430
|
}
|
|
5332
5431
|
var DIAL_SIZE = { cols: 25, rows: 12 };
|
|
5333
|
-
function divider(
|
|
5334
|
-
if (
|
|
5335
|
-
if (!label2) return "\u2500".repeat(
|
|
5432
|
+
function divider(n2, label2) {
|
|
5433
|
+
if (n2 <= 0) return "";
|
|
5434
|
+
if (!label2) return "\u2500".repeat(n2);
|
|
5336
5435
|
const l = ` ${label2} `;
|
|
5337
|
-
const side = Math.max(2, Math.floor((
|
|
5338
|
-
return [..."\u2500".repeat(side) + l + "\u2500".repeat(
|
|
5436
|
+
const side = Math.max(2, Math.floor((n2 - l.length) / 2));
|
|
5437
|
+
return [..."\u2500".repeat(side) + l + "\u2500".repeat(n2)].slice(0, n2).join("");
|
|
5339
5438
|
}
|
|
5340
5439
|
|
|
5341
5440
|
// src/args.ts
|
|
@@ -5526,9 +5625,9 @@ async function resolvePod(ctx, ref) {
|
|
|
5526
5625
|
}
|
|
5527
5626
|
function resolveCheckpoint(cps, ref) {
|
|
5528
5627
|
if (/^#\d+$/.test(ref)) {
|
|
5529
|
-
const
|
|
5628
|
+
const n2 = Number(ref.slice(1));
|
|
5530
5629
|
const sorted = [...cps].sort((a, b2) => b2.at - a.at);
|
|
5531
|
-
const cp = sorted[
|
|
5630
|
+
const cp = sorted[n2 - 1];
|
|
5532
5631
|
if (!cp) throw new CliError(`no checkpoint ${ref}`, "list them with: airgate checkpoints <pod>");
|
|
5533
5632
|
return cp;
|
|
5534
5633
|
}
|
|
@@ -5656,13 +5755,13 @@ function duration2(seconds) {
|
|
|
5656
5755
|
function hours(h) {
|
|
5657
5756
|
return `${Math.round(h * 100) / 100}h`;
|
|
5658
5757
|
}
|
|
5659
|
-
function usd(
|
|
5660
|
-
return `$${
|
|
5758
|
+
function usd(n2) {
|
|
5759
|
+
return `$${n2.toFixed(2)}`;
|
|
5661
5760
|
}
|
|
5662
|
-
function bytes(
|
|
5663
|
-
if (
|
|
5664
|
-
if (
|
|
5665
|
-
return `${(
|
|
5761
|
+
function bytes(n2) {
|
|
5762
|
+
if (n2 < 1024) return `${n2} B`;
|
|
5763
|
+
if (n2 < 1024 * 1024) return `${(n2 / 1024).toFixed(1)} KiB`;
|
|
5764
|
+
return `${(n2 / 1024 / 1024).toFixed(1)} MiB`;
|
|
5666
5765
|
}
|
|
5667
5766
|
function date(ts) {
|
|
5668
5767
|
if (!ts) return "-";
|
|
@@ -6132,8 +6231,8 @@ var US_PHONE_RE = /(?<![\w+.-])(?:1[-. ]?)?(?:\([2-9]\d{2}\)[ ]?|[2-9]\d{2}[-. ]
|
|
|
6132
6231
|
var INTL_PHONE_RE = /(?<![\w+])\+\d{1,3}(?:[ .-]?\(?\d{1,5}\)?){1,6}(?![\w-]|\.\d)/g;
|
|
6133
6232
|
var CUED_PHONE_RE = /\b(?:phone|tel|telephone|cell|mobile|fax|call|ph|whatsapp)\b\.?(?:[ \t]*(?:no\.?|number|#))?[ \t]*[:.]?[ \t]*(\+?\(?\d[\d \t().-]{5,}\d)/gi;
|
|
6134
6233
|
var phoneDigitsOk = (min) => (s2) => {
|
|
6135
|
-
const
|
|
6136
|
-
return
|
|
6234
|
+
const n2 = digitsOf(s2).length;
|
|
6235
|
+
return n2 >= min && n2 <= 15;
|
|
6137
6236
|
};
|
|
6138
6237
|
var SSN_RE = /(?<![\d-])(?!000|666|9\d\d)\d{3}-(?!00)\d{2}-(?!0000)\d{4}(?![\d-])/g;
|
|
6139
6238
|
var CUED_SSN_RE = /\b(?:SSN|social security(?:[ \t]+(?:number|no\.?))?)[ \t]*(?:#|no\.?|number)?[ \t]*[:.]?[ \t]*(\d{3}[ -]?\d{2}[ -]?\d{4})(?!\d)/gi;
|
|
@@ -6409,7 +6508,7 @@ var DETECTORS = [
|
|
|
6409
6508
|
];
|
|
6410
6509
|
|
|
6411
6510
|
// ../mask/src/index.ts
|
|
6412
|
-
var defaultPlaceholder = (kind,
|
|
6511
|
+
var defaultPlaceholder = (kind, n2) => `[${kind}_${n2}]`;
|
|
6413
6512
|
var escapeRe = (s2) => s2.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
|
|
6414
6513
|
function vaultKey(kind, value) {
|
|
6415
6514
|
switch (kind) {
|
|
@@ -6456,9 +6555,9 @@ function createMasker(options = {}) {
|
|
|
6456
6555
|
const key = vaultKey(kind, value);
|
|
6457
6556
|
const existing = byKey.get(key);
|
|
6458
6557
|
if (existing) return existing.placeholder;
|
|
6459
|
-
const
|
|
6460
|
-
counters.set(kind,
|
|
6461
|
-
const entry = { placeholder: makePlaceholder(kind,
|
|
6558
|
+
const n2 = (counters.get(kind) ?? 0) + 1;
|
|
6559
|
+
counters.set(kind, n2);
|
|
6560
|
+
const entry = { placeholder: makePlaceholder(kind, n2), kind, original: value };
|
|
6462
6561
|
byKey.set(key, entry);
|
|
6463
6562
|
byLoose.set(looseKey(coreOf(entry.placeholder)), entry);
|
|
6464
6563
|
unmaskRe = null;
|
|
@@ -6471,8 +6570,8 @@ function createMasker(options = {}) {
|
|
|
6471
6570
|
const entry = { placeholder: e.placeholder, kind: e.kind, original: e.original };
|
|
6472
6571
|
byKey.set(key, entry);
|
|
6473
6572
|
byLoose.set(looseKey(coreOf(entry.placeholder)), entry);
|
|
6474
|
-
const
|
|
6475
|
-
counters.set(e.kind, Math.max(counters.get(e.kind) ?? 0,
|
|
6573
|
+
const n2 = Number(/(\d+)\D*$/.exec(e.placeholder)?.[1] ?? 0);
|
|
6574
|
+
counters.set(e.kind, Math.max(counters.get(e.kind) ?? 0, n2));
|
|
6476
6575
|
}
|
|
6477
6576
|
unmaskRe = null;
|
|
6478
6577
|
}
|
|
@@ -6690,7 +6789,7 @@ var mask = {
|
|
|
6690
6789
|
if (r.spans.length) {
|
|
6691
6790
|
const kinds = /* @__PURE__ */ new Map();
|
|
6692
6791
|
for (const e of masker.entries()) kinds.set(e.kind, (kinds.get(e.kind) ?? 0) + 1);
|
|
6693
|
-
ctx.ui.err(ctx.ui.ce("dim", `masked ${r.spans.length} \xB7 ${[...kinds].map(([k,
|
|
6792
|
+
ctx.ui.err(ctx.ui.ce("dim", `masked ${r.spans.length} \xB7 ${[...kinds].map(([k, n2]) => `${k.toLowerCase()} ${n2}`).join(", ")}`));
|
|
6694
6793
|
} else ctx.ui.err(ctx.ui.ce("dim", "nothing to mask."));
|
|
6695
6794
|
}
|
|
6696
6795
|
};
|
|
@@ -6724,7 +6823,7 @@ function attachSession(o) {
|
|
|
6724
6823
|
return;
|
|
6725
6824
|
}
|
|
6726
6825
|
if (i > 0) send({ type: "input", data: s2.slice(0, i) });
|
|
6727
|
-
|
|
6826
|
+
finish2({ reason: "detached" });
|
|
6728
6827
|
};
|
|
6729
6828
|
const restore = () => {
|
|
6730
6829
|
if (restored) return;
|
|
@@ -6745,7 +6844,7 @@ function attachSession(o) {
|
|
|
6745
6844
|
} catch {
|
|
6746
6845
|
}
|
|
6747
6846
|
};
|
|
6748
|
-
function
|
|
6847
|
+
function finish2(end) {
|
|
6749
6848
|
restore();
|
|
6750
6849
|
try {
|
|
6751
6850
|
if (ws.readyState === 0 || ws.readyState === 1) ws.close(1e3, end.reason);
|
|
@@ -6754,7 +6853,7 @@ function attachSession(o) {
|
|
|
6754
6853
|
settle(end);
|
|
6755
6854
|
}
|
|
6756
6855
|
function onTerm() {
|
|
6757
|
-
|
|
6856
|
+
finish2({ reason: "closed", message: "signal" });
|
|
6758
6857
|
}
|
|
6759
6858
|
try {
|
|
6760
6859
|
stdin.setRawMode?.(true);
|
|
@@ -6782,14 +6881,14 @@ function attachSession(o) {
|
|
|
6782
6881
|
return;
|
|
6783
6882
|
}
|
|
6784
6883
|
if (msg.type === "output" && typeof msg.data === "string") stdout.write(msg.data);
|
|
6785
|
-
else if (msg.type === "exit")
|
|
6884
|
+
else if (msg.type === "exit") finish2({ reason: "exit", code: typeof msg.code === "number" ? msg.code : 0 });
|
|
6786
6885
|
else if (msg.type === "status" && msg.state === "waking") o.status?.("waking the pod\u2026");
|
|
6787
6886
|
};
|
|
6788
6887
|
ws.onerror = () => {
|
|
6789
|
-
if (!restored)
|
|
6888
|
+
if (!restored) finish2({ reason: "error", message: "connection error" });
|
|
6790
6889
|
};
|
|
6791
6890
|
ws.onclose = (ev) => {
|
|
6792
|
-
if (!restored)
|
|
6891
|
+
if (!restored) finish2({ reason: "closed", code: ev?.code, message: ev?.reason });
|
|
6793
6892
|
};
|
|
6794
6893
|
return done;
|
|
6795
6894
|
}
|
|
@@ -6934,81 +7033,81 @@ async function readAll2(stream) {
|
|
|
6934
7033
|
for await (const c of stream) s2 += c.toString();
|
|
6935
7034
|
return s2;
|
|
6936
7035
|
}
|
|
6937
|
-
async function readValue(ctx, name) {
|
|
7036
|
+
async function readValue(ctx, name, pipeHint = `printf %s "$VALUE" | airgate secrets set ${name}`) {
|
|
6938
7037
|
if (ctx.io.stdin.isTTY) {
|
|
6939
7038
|
const v2 = await ctx.io.prompt(`value for ${name} (hidden): `, { hidden: true });
|
|
6940
7039
|
if (!v2) throw new CliError("empty value, nothing saved");
|
|
6941
7040
|
return v2;
|
|
6942
7041
|
}
|
|
6943
7042
|
const v = (await readAll2(ctx.io.stdin)).replace(/\r?\n$/, "");
|
|
6944
|
-
if (!v) throw new CliError("no value on stdin",
|
|
7043
|
+
if (!v) throw new CliError("no value on stdin", pipeHint);
|
|
6945
7044
|
return v;
|
|
6946
7045
|
}
|
|
6947
|
-
function
|
|
6948
|
-
const
|
|
6949
|
-
|
|
6950
|
-
|
|
6951
|
-
|
|
6952
|
-
|
|
6953
|
-
|
|
6954
|
-
if (!m) continue;
|
|
6955
|
-
let v = m[2];
|
|
6956
|
-
if (v.startsWith('"')) {
|
|
6957
|
-
const end = v.lastIndexOf('"');
|
|
6958
|
-
v = v.slice(1, end > 0 ? end : void 0).replace(/\\(["\\n])/g, (_s, c) => c === "n" ? "\n" : c);
|
|
6959
|
-
} else if (v.startsWith("'")) {
|
|
6960
|
-
const end = v.lastIndexOf("'");
|
|
6961
|
-
v = v.slice(1, end > 0 ? end : void 0);
|
|
6962
|
-
} else v = v.replace(/\s+#.*$/, "").trim();
|
|
6963
|
-
if (NAME_RE.test(m[1]) && !m[1].startsWith("AIRGATE_")) values[m[1]] = v;
|
|
6964
|
-
else skipped.push(m[1]);
|
|
6965
|
-
}
|
|
6966
|
-
return { values, skipped };
|
|
7046
|
+
function parseDotenv2(text2) {
|
|
7047
|
+
const r = parseDotenv(text2);
|
|
7048
|
+
return {
|
|
7049
|
+
values: r.values,
|
|
7050
|
+
skipped: r.entries.filter((e) => !e.name).map((e) => e.key),
|
|
7051
|
+
renamed: r.entries.filter((e) => e.name && e.name !== e.key).map((e) => `${e.key} \u2192 ${e.name}`)
|
|
7052
|
+
};
|
|
6967
7053
|
}
|
|
6968
|
-
|
|
6969
|
-
const path = resolve3(ctx.io.cwd, file);
|
|
6970
|
-
let text2;
|
|
7054
|
+
function readDotenvFile(ctx, file, hint) {
|
|
6971
7055
|
try {
|
|
6972
|
-
|
|
7056
|
+
return readFileSync2(resolve3(ctx.io.cwd, file), "utf8");
|
|
6973
7057
|
} catch {
|
|
6974
|
-
throw new CliError(`can't read ${file}`,
|
|
7058
|
+
throw new CliError(`can't read ${file}`, hint);
|
|
6975
7059
|
}
|
|
6976
|
-
|
|
6977
|
-
|
|
6978
|
-
const
|
|
6979
|
-
|
|
6980
|
-
|
|
6981
|
-
|
|
6982
|
-
|
|
6983
|
-
|
|
6984
|
-
|
|
6985
|
-
|
|
6986
|
-
}
|
|
7060
|
+
}
|
|
7061
|
+
async function sendDotenv(ctx, file, podId) {
|
|
7062
|
+
const { values, skipped, renamed } = parseDotenv2(readDotenvFile(ctx, file, "airgate up --env path/to/.env"));
|
|
7063
|
+
if (!Object.keys(values).length) return { sent: [], skipped, renamed, failed: [] };
|
|
7064
|
+
try {
|
|
7065
|
+
const r = await ctx.api.setSecrets(values, podId);
|
|
7066
|
+
return { sent: [...r.created, ...r.updated], skipped, renamed, failed: [] };
|
|
7067
|
+
} catch (e) {
|
|
7068
|
+
if (e instanceof ApiError && e.status < 500) return { sent: [], skipped, renamed, failed: [e.message] };
|
|
7069
|
+
throw e;
|
|
6987
7070
|
}
|
|
6988
|
-
return { sent, skipped, failed };
|
|
6989
7071
|
}
|
|
6990
7072
|
var secrets = {
|
|
6991
7073
|
name: "secrets",
|
|
6992
7074
|
aliases: ["secret"],
|
|
6993
7075
|
group: "pods",
|
|
6994
|
-
usage: "[set NAME | rm NAME] [--pod p]",
|
|
7076
|
+
usage: "[set NAME | rm NAME | import FILE] [--pod p] [--dotenv]",
|
|
6995
7077
|
summary: "environment variables for pods, stored encrypted, never shown again",
|
|
6996
|
-
example: "airgate secrets
|
|
7078
|
+
example: "airgate secrets import .env --pod api",
|
|
6997
7079
|
notes: [
|
|
6998
7080
|
"set reads the value from a hidden prompt, or from stdin when piped (never from the command line).",
|
|
6999
7081
|
"account-wide unless --pod; a pod value overrides an account one. pods load them into ~/.airgate/secrets.env when they start or wake.",
|
|
7000
|
-
"every value of 6+ characters is masked before it reaches a model, even if the agent reads it."
|
|
7082
|
+
"every value of 6+ characters is masked before it reaches a model, even if the agent reads it.",
|
|
7083
|
+
"import FILE saves every KEY=value in a .env file at once (quotes, export, multiline values); ls --dotenv prints the names as a .env.example."
|
|
7001
7084
|
],
|
|
7002
7085
|
flags: {
|
|
7003
|
-
pod: { type: "string", value: "pod", help: "just this pod (default: every pod)" }
|
|
7086
|
+
pod: { type: "string", value: "pod", help: "just this pod (default: every pod)" },
|
|
7087
|
+
dotenv: { type: "boolean", help: "ls: print the names as a .env.example (no values)" }
|
|
7004
7088
|
},
|
|
7005
7089
|
async run(ctx) {
|
|
7006
7090
|
const [action, name, extra] = ctx.args.positionals;
|
|
7007
7091
|
if (extra !== void 0) throw new UsageError(`unexpected "${extra}"`, "values are read from a hidden prompt or stdin, never from arguments");
|
|
7008
7092
|
const podRef = str(ctx.args, "pod");
|
|
7009
7093
|
const pod = podRef ? await resolvePod(ctx, podRef) : void 0;
|
|
7094
|
+
if (action === "import") {
|
|
7095
|
+
if (!name) throw new UsageError("which file? airgate secrets import .env [--pod p]");
|
|
7096
|
+
const r = await ctx.ui.spin(`saving ${name}`, sendDotenv(ctx, name, pod?.id));
|
|
7097
|
+
if (ctx.json) return ctx.ui.json(r);
|
|
7098
|
+
if (r.failed.length) throw new CliError(`nothing saved: ${r.failed.join("; ")}`);
|
|
7099
|
+
if (!r.sent.length) throw new CliError(`no KEY=value lines in ${name}`);
|
|
7100
|
+
ctx.ui.out(`${ctx.ui.c("ok", "saved")} ${r.sent.length} for ${pod ? pod.spec.name : "every pod"}: ${r.sent.join(", ")}`);
|
|
7101
|
+
if (r.renamed.length) ctx.ui.out(ctx.ui.c("dim", `renamed: ${r.renamed.join(", ")}`));
|
|
7102
|
+
if (r.skipped.length) ctx.ui.err(ctx.ui.ce("warn", `skipped (reserved or unusable names): ${r.skipped.join(", ")}`));
|
|
7103
|
+
return;
|
|
7104
|
+
}
|
|
7010
7105
|
if (!action || action === "list" || action === "ls") {
|
|
7011
7106
|
const all = await ctx.api.secrets();
|
|
7107
|
+
if (bool(ctx.args, "dotenv")) {
|
|
7108
|
+
const names = [...all.account.map((s2) => s2.name), ...pod ? (all.pods[pod.id] ?? []).map((s2) => s2.name) : []];
|
|
7109
|
+
return void ctx.io.stdout.write(dotenvExample([...new Set(names)].sort()));
|
|
7110
|
+
}
|
|
7012
7111
|
if (ctx.json) return ctx.ui.json(pod ? { account: all.account, pod: all.pods[pod.id] ?? [] } : all);
|
|
7013
7112
|
const podNames2 = new Map((await ctx.api.pods()).map((p) => [p.id, p.spec.name]));
|
|
7014
7113
|
const rows2 = [];
|
|
@@ -7040,11 +7139,14 @@ var secrets = {
|
|
|
7040
7139
|
ctx.ui.out(`removed ${name} from ${pod ? pod.spec.name : "the account"}. ${r.note}.`);
|
|
7041
7140
|
return;
|
|
7042
7141
|
}
|
|
7043
|
-
throw new UsageError(`unknown action "${action}"`, "airgate secrets [set NAME | rm NAME] [--pod p]");
|
|
7142
|
+
throw new UsageError(`unknown action "${action}"`, "airgate secrets [set NAME | rm NAME | import FILE] [--pod p]");
|
|
7044
7143
|
}
|
|
7045
7144
|
};
|
|
7046
7145
|
|
|
7047
7146
|
// src/commands/pods.ts
|
|
7147
|
+
import { execFileSync } from "node:child_process";
|
|
7148
|
+
import { readFileSync as readFileSync3 } from "node:fs";
|
|
7149
|
+
import { resolve as resolvePath } from "node:path";
|
|
7048
7150
|
var AGENT_NAMES = Object.keys(AGENTS);
|
|
7049
7151
|
var SIZE_NAMES = Object.keys(SIZES);
|
|
7050
7152
|
var NETWORK_NAMES = ["allowlist", "open", "off"];
|
|
@@ -7060,9 +7162,9 @@ function cronProblem(cron) {
|
|
|
7060
7162
|
if (!cronValid(cron)) return `"${cron}" isn't a 5-field cron (min hour day month weekday, utc)`;
|
|
7061
7163
|
let t = cronNext(cron);
|
|
7062
7164
|
for (let i = 0; i < 12 && t !== null; i++) {
|
|
7063
|
-
const
|
|
7064
|
-
if (
|
|
7065
|
-
t =
|
|
7165
|
+
const n2 = cronNext(cron, t);
|
|
7166
|
+
if (n2 !== null && n2 - t < 15 * 6e4) return `"${cron}" runs more often than every 15 minutes`;
|
|
7167
|
+
t = n2;
|
|
7066
7168
|
}
|
|
7067
7169
|
return null;
|
|
7068
7170
|
}
|
|
@@ -7125,7 +7227,7 @@ var pods = {
|
|
|
7125
7227
|
var launch = {
|
|
7126
7228
|
name: "launch",
|
|
7127
7229
|
group: "pods",
|
|
7128
|
-
usage: "[--name n] [--template t] [--agent a] [--memory m] [--browser] [--repo owner/name] [--cron c --task t \u2026] [--model m] [--no-mask] [--no-undo] [--network n]",
|
|
7230
|
+
usage: "[--name n] [--template t] [--agent a] [--memory m] [--browser] [--repo owner/name|.] [--env .env] [--cron c --task t \u2026] [--model m] [--no-mask] [--no-undo] [--network n]",
|
|
7129
7231
|
summary: "start a new pod",
|
|
7130
7232
|
example: "airgate launch --name fix-nav --agent codex --repo acme/web",
|
|
7131
7233
|
notes: [
|
|
@@ -7143,7 +7245,8 @@ var launch = {
|
|
|
7143
7245
|
"builtin-model": { type: "boolean", help: "autopilot with opencode: use airgate's built-in model (no subscription)" },
|
|
7144
7246
|
model: { type: "string", value: "id", help: "built-in model: an openrouter id (default: the gateway's); see airgate templates" },
|
|
7145
7247
|
memory: { type: "string", value: "size", choices: Object.keys(SIZES), default: "standard", help: `standard 8 GB | large 16 GB (pro, ${addOnPrice(ADD_ONS.large.usdPerHour)})` },
|
|
7146
|
-
repo: { type: "string", value: "owner/name", help: "GitHub repo to clone (private ones: airgate github connect)" },
|
|
7248
|
+
repo: { type: "string", value: "owner/name", help: "GitHub repo to clone, a github link, or . for this folder's repo (private ones: airgate github connect)" },
|
|
7249
|
+
env: { type: "string", value: "file", help: "a .env file: its keys become this pod's secrets before it boots" },
|
|
7147
7250
|
"public-repo": { type: "boolean", help: "the repo is public: don't check it against your GitHub App" },
|
|
7148
7251
|
mask: { type: "boolean", default: true, help: "mask model traffic (--no-mask to turn off)" },
|
|
7149
7252
|
undo: { type: "boolean", default: true, help: "checkpoint after every agent step (--no-undo to turn off)" },
|
|
@@ -7156,16 +7259,48 @@ var launch = {
|
|
|
7156
7259
|
if (ctx.args.positionals.length) throw new UsageError(`unexpected "${ctx.args.positionals[0]}"`, "name the pod with --name");
|
|
7157
7260
|
const spec = specFromFlags(ctx);
|
|
7158
7261
|
if (spec.repo) await checkRepo(ctx, spec.repo);
|
|
7159
|
-
const
|
|
7262
|
+
const envFile = str(ctx.args, "env");
|
|
7263
|
+
let secrets2;
|
|
7264
|
+
if (envFile) {
|
|
7265
|
+
const { values, skipped, renamed } = parseDotenv2(readDotenv(ctx, envFile));
|
|
7266
|
+
secrets2 = values;
|
|
7267
|
+
if (renamed.length) ctx.ui.err(ctx.ui.ce("dim", `renamed: ${renamed.join(", ")}`));
|
|
7268
|
+
if (skipped.length) ctx.ui.err(ctx.ui.ce("warn", `skipped (reserved or unusable names): ${skipped.join(", ")}`));
|
|
7269
|
+
}
|
|
7270
|
+
const pod = await ctx.ui.spin(`launching ${spec.name}`, async () => waitReady(ctx, await ctx.api.launch(spec, void 0, secrets2)));
|
|
7271
|
+
if (secrets2 && Object.keys(secrets2).length && !ctx.json) ctx.ui.out(`secrets for ${pod.spec.name}: ${Object.keys(secrets2).join(", ")}`);
|
|
7160
7272
|
if (ctx.json) return ctx.ui.json(pod);
|
|
7161
7273
|
ctx.ui.out(readyLine(ctx, pod));
|
|
7162
7274
|
if ((pod.spec.template ?? "code") === "autopilot") {
|
|
7163
|
-
const
|
|
7275
|
+
const n2 = pod.spec.jobs?.length ?? 0;
|
|
7164
7276
|
if (!pod.spec.builtinModel) ctx.ui.out(`sign ${AGENTS[pod.spec.agent].label} in once: airgate attach ${pod.spec.name}`);
|
|
7165
|
-
ctx.ui.out(
|
|
7277
|
+
ctx.ui.out(n2 ? `${n2} job${n2 === 1 ? "" : "s"}: airgate jobs ${pod.spec.name}` : `add a job: airgate job add ${pod.spec.name} --cron "0 2 * * *" --task "\u2026"`);
|
|
7166
7278
|
} else ctx.ui.out(`next: airgate attach ${pod.spec.name}`);
|
|
7167
7279
|
}
|
|
7168
7280
|
};
|
|
7281
|
+
function repoArg(ctx, raw) {
|
|
7282
|
+
if (raw === void 0) return void 0;
|
|
7283
|
+
let input = raw;
|
|
7284
|
+
if (raw === ".") {
|
|
7285
|
+
try {
|
|
7286
|
+
input = execFileSync("git", ["remote", "get-url", "origin"], { cwd: ctx.io.cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
7287
|
+
} catch {
|
|
7288
|
+
throw new UsageError("--repo .: this folder has no git remote called origin");
|
|
7289
|
+
}
|
|
7290
|
+
}
|
|
7291
|
+
const s2 = input.trim().replace(/\.git$/, "").replace(/^git@github\.com:/, "").replace(/^(?:https?:\/\/)?(?:www\.)?github\.com\//, "");
|
|
7292
|
+
const [owner, name] = s2.split(/[/#?]/);
|
|
7293
|
+
const repo = owner && name ? `${owner}/${name}` : "";
|
|
7294
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo)) throw new UsageError(raw === "." ? `--repo .: origin isn't a github repo (${input})` : "--repo: owner/name or a github link");
|
|
7295
|
+
return repo;
|
|
7296
|
+
}
|
|
7297
|
+
function readDotenv(ctx, file) {
|
|
7298
|
+
try {
|
|
7299
|
+
return readFileSync3(resolvePath(ctx.io.cwd, file), "utf8");
|
|
7300
|
+
} catch {
|
|
7301
|
+
throw new CliError(`can't read ${file}`, "airgate launch --env path/to/.env");
|
|
7302
|
+
}
|
|
7303
|
+
}
|
|
7169
7304
|
function specFromFlags(ctx, defaults = {}) {
|
|
7170
7305
|
const a = ctx.args;
|
|
7171
7306
|
const network = str(a, "network") ?? "allowlist";
|
|
@@ -7176,8 +7311,7 @@ function specFromFlags(ctx, defaults = {}) {
|
|
|
7176
7311
|
autoStopMinutes = Number(autoStop);
|
|
7177
7312
|
if (!Number.isInteger(autoStopMinutes) || autoStopMinutes < 1) throw new UsageError("--auto-stop: whole idle minutes \u2265 1, or keep-awake");
|
|
7178
7313
|
}
|
|
7179
|
-
const repo = str(a, "repo");
|
|
7180
|
-
if (repo && !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo)) throw new UsageError("--repo: owner/name");
|
|
7314
|
+
const repo = repoArg(ctx, str(a, "repo"));
|
|
7181
7315
|
const template = str(a, "template") ?? "code";
|
|
7182
7316
|
const t = TEMPLATES[template];
|
|
7183
7317
|
const agent = str(a, "agent") ?? (defaults.agent && t.agents.includes(defaults.agent) ? defaults.agent : t.agents[0]);
|
|
@@ -7307,7 +7441,6 @@ var destroy = {
|
|
|
7307
7441
|
};
|
|
7308
7442
|
var exec = {
|
|
7309
7443
|
name: "exec",
|
|
7310
|
-
aliases: ["run"],
|
|
7311
7444
|
group: "pods",
|
|
7312
7445
|
usage: "<pod> -- <command\u2026>",
|
|
7313
7446
|
summary: "run a command in the pod (bash -c, in ~/workspace) and print its output",
|
|
@@ -7330,7 +7463,7 @@ var exec = {
|
|
|
7330
7463
|
};
|
|
7331
7464
|
|
|
7332
7465
|
// src/commands/jobs.ts
|
|
7333
|
-
var price = (
|
|
7466
|
+
var price = (n2) => `$${n2.toFixed(2)}/h`;
|
|
7334
7467
|
var templates = {
|
|
7335
7468
|
name: "templates",
|
|
7336
7469
|
group: "pods",
|
|
@@ -7528,199 +7661,16 @@ var log = {
|
|
|
7528
7661
|
// src/commands/mcp.ts
|
|
7529
7662
|
import { createInterface } from "node:readline";
|
|
7530
7663
|
|
|
7531
|
-
// src/io.ts
|
|
7532
|
-
import { spawn } from "node:child_process";
|
|
7533
|
-
import { EventEmitter } from "node:events";
|
|
7534
|
-
import { PassThrough } from "node:stream";
|
|
7535
|
-
import { StringDecoder as StringDecoder2 } from "node:string_decoder";
|
|
7536
|
-
var LineReader = class {
|
|
7537
|
-
buf = "";
|
|
7538
|
-
ended = false;
|
|
7539
|
-
waiters = [];
|
|
7540
|
-
stream;
|
|
7541
|
-
attached = false;
|
|
7542
|
-
decoder = new StringDecoder2("utf8");
|
|
7543
|
-
constructor(stream) {
|
|
7544
|
-
this.stream = stream;
|
|
7545
|
-
}
|
|
7546
|
-
attach() {
|
|
7547
|
-
if (this.attached) return;
|
|
7548
|
-
this.attached = true;
|
|
7549
|
-
this.stream.on("data", (d) => {
|
|
7550
|
-
this.buf += typeof d === "string" ? d : this.decoder.write(d);
|
|
7551
|
-
this.wake();
|
|
7552
|
-
});
|
|
7553
|
-
this.stream.on("end", () => {
|
|
7554
|
-
this.ended = true;
|
|
7555
|
-
this.wake();
|
|
7556
|
-
});
|
|
7557
|
-
this.stream.on("error", () => {
|
|
7558
|
-
this.ended = true;
|
|
7559
|
-
this.wake();
|
|
7560
|
-
});
|
|
7561
|
-
}
|
|
7562
|
-
wake() {
|
|
7563
|
-
for (const w of this.waiters.splice(0)) w();
|
|
7564
|
-
}
|
|
7565
|
-
async line() {
|
|
7566
|
-
this.attach();
|
|
7567
|
-
this.stream.resume();
|
|
7568
|
-
for (; ; ) {
|
|
7569
|
-
const i = this.buf.indexOf("\n");
|
|
7570
|
-
if (i !== -1) {
|
|
7571
|
-
const l = this.buf.slice(0, i).replace(/\r$/, "");
|
|
7572
|
-
this.buf = this.buf.slice(i + 1);
|
|
7573
|
-
if (!this.buf) this.stream.pause();
|
|
7574
|
-
return l;
|
|
7575
|
-
}
|
|
7576
|
-
if (this.ended) {
|
|
7577
|
-
const rest = this.buf;
|
|
7578
|
-
this.buf = "";
|
|
7579
|
-
return rest ? rest : null;
|
|
7580
|
-
}
|
|
7581
|
-
await new Promise((r) => this.waiters.push(r));
|
|
7582
|
-
}
|
|
7583
|
-
}
|
|
7584
|
-
};
|
|
7585
|
-
function ttyPrompt(stdin, out) {
|
|
7586
|
-
return (question, opts2 = {}) => new Promise((resolve5, reject) => {
|
|
7587
|
-
out.write(question);
|
|
7588
|
-
const decoder = new StringDecoder2("utf8");
|
|
7589
|
-
let answer = "";
|
|
7590
|
-
const wasRaw = Boolean(stdin.isRaw);
|
|
7591
|
-
stdin.setRawMode?.(true);
|
|
7592
|
-
stdin.resume();
|
|
7593
|
-
const done = (err) => {
|
|
7594
|
-
stdin.off("data", onData);
|
|
7595
|
-
stdin.setRawMode?.(wasRaw);
|
|
7596
|
-
stdin.pause();
|
|
7597
|
-
out.write("\n");
|
|
7598
|
-
if (err) reject(err);
|
|
7599
|
-
else resolve5(answer);
|
|
7600
|
-
};
|
|
7601
|
-
const onData = (d) => {
|
|
7602
|
-
for (const ch of decoder.write(d)) {
|
|
7603
|
-
if (ch === "\r" || ch === "\n") return done();
|
|
7604
|
-
if (ch === "") return done(Object.assign(new Error("cancelled"), { code: "cancelled" }));
|
|
7605
|
-
if (ch === "") {
|
|
7606
|
-
if (!answer) return done(Object.assign(new Error("cancelled"), { code: "cancelled" }));
|
|
7607
|
-
continue;
|
|
7608
|
-
}
|
|
7609
|
-
if (ch === "\x7F" || ch === "\b") {
|
|
7610
|
-
if (answer) {
|
|
7611
|
-
answer = [...answer].slice(0, -1).join("");
|
|
7612
|
-
if (!opts2.hidden) out.write("\b \b");
|
|
7613
|
-
}
|
|
7614
|
-
continue;
|
|
7615
|
-
}
|
|
7616
|
-
if (ch < " ") continue;
|
|
7617
|
-
answer += ch;
|
|
7618
|
-
if (!opts2.hidden) out.write(ch);
|
|
7619
|
-
}
|
|
7620
|
-
};
|
|
7621
|
-
stdin.on("data", onData);
|
|
7622
|
-
});
|
|
7623
|
-
}
|
|
7624
|
-
function openInBrowser(url) {
|
|
7625
|
-
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url]] : ["xdg-open", [url]];
|
|
7626
|
-
return new Promise((resolve5) => {
|
|
7627
|
-
try {
|
|
7628
|
-
const child = spawn(cmd, args, { stdio: "ignore", detached: true });
|
|
7629
|
-
child.on("error", () => resolve5(false));
|
|
7630
|
-
child.on("spawn", () => {
|
|
7631
|
-
child.unref();
|
|
7632
|
-
resolve5(true);
|
|
7633
|
-
});
|
|
7634
|
-
} catch {
|
|
7635
|
-
resolve5(false);
|
|
7636
|
-
}
|
|
7637
|
-
});
|
|
7638
|
-
}
|
|
7639
|
-
function processIo() {
|
|
7640
|
-
const stdin = process.stdin;
|
|
7641
|
-
const lines = new LineReader(stdin);
|
|
7642
|
-
const tty = ttyPrompt(stdin, process.stderr);
|
|
7643
|
-
return {
|
|
7644
|
-
stdin,
|
|
7645
|
-
stdout: process.stdout,
|
|
7646
|
-
stderr: process.stderr,
|
|
7647
|
-
env: process.env,
|
|
7648
|
-
// `npm run cli -- up` runs in packages/cli; INIT_CWD is where the user typed it
|
|
7649
|
-
cwd: process.env.AIRGATE_CWD || process.env.npm_lifecycle_event === "dev" && process.env.INIT_CWD || process.cwd(),
|
|
7650
|
-
async prompt(q, o) {
|
|
7651
|
-
if (stdin.isTTY) return tty(q, o);
|
|
7652
|
-
process.stderr.write(q);
|
|
7653
|
-
const l = await lines.line();
|
|
7654
|
-
if (l === null) throw Object.assign(new Error("no input (stdin closed)"), { code: "no_input" });
|
|
7655
|
-
process.stderr.write("\n");
|
|
7656
|
-
return l;
|
|
7657
|
-
},
|
|
7658
|
-
openUrl: openInBrowser,
|
|
7659
|
-
websocket: (url) => new WebSocket(url),
|
|
7660
|
-
signals: process
|
|
7661
|
-
};
|
|
7662
|
-
}
|
|
7663
|
-
function memoryIo(opts2 = {}) {
|
|
7664
|
-
const out = new PassThrough();
|
|
7665
|
-
const err = new PassThrough();
|
|
7666
|
-
let stdout = "";
|
|
7667
|
-
let stderr = "";
|
|
7668
|
-
out.on("data", (d) => stdout += d.toString());
|
|
7669
|
-
err.on("data", (d) => stderr += d.toString());
|
|
7670
|
-
const answers = [...opts2.answers ?? []];
|
|
7671
|
-
const opened = [];
|
|
7672
|
-
const prompts = [];
|
|
7673
|
-
const stdin = opts2.stdin ?? Object.assign(new PassThrough(), { isTTY: false });
|
|
7674
|
-
if (!opts2.stdin) stdin.end();
|
|
7675
|
-
const io = {
|
|
7676
|
-
stdin,
|
|
7677
|
-
stdout: out,
|
|
7678
|
-
stderr: err,
|
|
7679
|
-
env: opts2.env ?? {},
|
|
7680
|
-
cwd: opts2.cwd ?? process.cwd(),
|
|
7681
|
-
async prompt(q) {
|
|
7682
|
-
prompts.push(q);
|
|
7683
|
-
const a = answers.shift();
|
|
7684
|
-
if (a === void 0) throw Object.assign(new Error(`no answer queued for prompt: ${q}`), { code: "no_input" });
|
|
7685
|
-
return a;
|
|
7686
|
-
},
|
|
7687
|
-
async openUrl(u) {
|
|
7688
|
-
opened.push(u);
|
|
7689
|
-
return true;
|
|
7690
|
-
},
|
|
7691
|
-
websocket: () => {
|
|
7692
|
-
throw new Error("no websocket in memoryIo");
|
|
7693
|
-
},
|
|
7694
|
-
signals: new EventEmitter()
|
|
7695
|
-
};
|
|
7696
|
-
return {
|
|
7697
|
-
io,
|
|
7698
|
-
answers,
|
|
7699
|
-
opened,
|
|
7700
|
-
prompts,
|
|
7701
|
-
get stdout() {
|
|
7702
|
-
return stdout;
|
|
7703
|
-
},
|
|
7704
|
-
get stderr() {
|
|
7705
|
-
return stderr;
|
|
7706
|
-
},
|
|
7707
|
-
clear() {
|
|
7708
|
-
stdout = "";
|
|
7709
|
-
stderr = "";
|
|
7710
|
-
}
|
|
7711
|
-
};
|
|
7712
|
-
}
|
|
7713
|
-
|
|
7714
7664
|
// src/version.ts
|
|
7715
|
-
var VERSION = "0.1.
|
|
7665
|
+
var VERSION = "0.1.3";
|
|
7716
7666
|
|
|
7717
7667
|
// src/commands/updown.ts
|
|
7718
|
-
import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as
|
|
7668
|
+
import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
|
|
7719
7669
|
import { homedir as homedir2 } from "node:os";
|
|
7720
7670
|
import { basename, dirname as dirname2, isAbsolute, join as join4, relative as relative2, resolve as resolve4 } from "node:path";
|
|
7721
7671
|
|
|
7722
7672
|
// src/pack.ts
|
|
7723
|
-
import { spawn
|
|
7673
|
+
import { spawn, spawnSync } from "node:child_process";
|
|
7724
7674
|
import { existsSync, lstatSync } from "node:fs";
|
|
7725
7675
|
import { join as join2 } from "node:path";
|
|
7726
7676
|
var UPLOAD_LIMIT = 32 * 6 * 1024 * 1024;
|
|
@@ -7759,8 +7709,8 @@ function gitFiles(dir) {
|
|
|
7759
7709
|
return out;
|
|
7760
7710
|
}
|
|
7761
7711
|
function runTar(cwd, args, input, limit = UPLOAD_LIMIT) {
|
|
7762
|
-
return new Promise((
|
|
7763
|
-
const child =
|
|
7712
|
+
return new Promise((resolve6, reject) => {
|
|
7713
|
+
const child = spawn("tar", args, { cwd, env: { ...process.env, COPYFILE_DISABLE: "1" }, stdio: ["pipe", "pipe", "pipe"] });
|
|
7764
7714
|
const chunks = [];
|
|
7765
7715
|
let size2 = 0;
|
|
7766
7716
|
let err = "";
|
|
@@ -7779,7 +7729,7 @@ function runTar(cwd, args, input, limit = UPLOAD_LIMIT) {
|
|
|
7779
7729
|
child.on("close", (code3) => {
|
|
7780
7730
|
if (over2) return reject(new TooLargeError(limit));
|
|
7781
7731
|
if (code3 !== 0) return reject(new Error(`tar failed: ${err.trim().split("\n").slice(-3).join(" ")}`));
|
|
7782
|
-
|
|
7732
|
+
resolve6(Buffer.concat(chunks));
|
|
7783
7733
|
});
|
|
7784
7734
|
child.stdin.on("error", () => void 0);
|
|
7785
7735
|
child.stdin.end(input ?? "");
|
|
@@ -7815,7 +7765,7 @@ import { gunzipSync } from "node:zlib";
|
|
|
7815
7765
|
|
|
7816
7766
|
// src/sessions.ts
|
|
7817
7767
|
import { createHash, randomUUID } from "node:crypto";
|
|
7818
|
-
import { closeSync, existsSync as existsSync2, mkdirSync as mkdirSync2, mkdtempSync, openSync, readdirSync, readFileSync as
|
|
7768
|
+
import { closeSync, existsSync as existsSync2, mkdirSync as mkdirSync2, mkdtempSync, openSync, readdirSync, readFileSync as readFileSync4, readSync, realpathSync, renameSync as renameSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
|
|
7819
7769
|
import { tmpdir } from "node:os";
|
|
7820
7770
|
import { dirname, join as join3, relative } from "node:path";
|
|
7821
7771
|
var POD_HOME = "/home/sprite";
|
|
@@ -7843,15 +7793,15 @@ function findClaudeSession(home, projectDir, id, env = {}) {
|
|
|
7843
7793
|
const dir = join3(claudeHome(home, env), "projects", claudeKey(p));
|
|
7844
7794
|
let names = [];
|
|
7845
7795
|
try {
|
|
7846
|
-
names = readdirSync(dir).filter((
|
|
7796
|
+
names = readdirSync(dir).filter((n2) => n2.endsWith(".jsonl"));
|
|
7847
7797
|
} catch {
|
|
7848
7798
|
continue;
|
|
7849
7799
|
}
|
|
7850
|
-
for (const
|
|
7851
|
-
const sid =
|
|
7800
|
+
for (const n2 of names) {
|
|
7801
|
+
const sid = n2.slice(0, -".jsonl".length);
|
|
7852
7802
|
if (id && sid !== id) continue;
|
|
7853
7803
|
if (!ID_RE.test(sid)) continue;
|
|
7854
|
-
const file = join3(dir,
|
|
7804
|
+
const file = join3(dir, n2);
|
|
7855
7805
|
const side = join3(dir, sid);
|
|
7856
7806
|
best.push({ agent: "claude-code", id: sid, file, mtime: statSync(file).mtimeMs, ...existsSync2(side) && statSync(side).isDirectory() ? { sidecar: side } : {} });
|
|
7857
7807
|
}
|
|
@@ -7863,8 +7813,8 @@ function firstLine(file) {
|
|
|
7863
7813
|
const fd = openSync(file, "r");
|
|
7864
7814
|
try {
|
|
7865
7815
|
const buf = Buffer.alloc(1024 * 1024);
|
|
7866
|
-
const
|
|
7867
|
-
return buf.subarray(0,
|
|
7816
|
+
const n2 = readSync(fd, buf, 0, buf.length, 0);
|
|
7817
|
+
return buf.subarray(0, n2).toString("utf8").split("\n")[0];
|
|
7868
7818
|
} finally {
|
|
7869
7819
|
closeSync(fd);
|
|
7870
7820
|
}
|
|
@@ -7920,7 +7870,7 @@ async function packSession(s2, projectDir, podPath, limit = UPLOAD_LIMIT) {
|
|
|
7920
7870
|
const froms = spellings(projectDir);
|
|
7921
7871
|
const stage = mkdtempSync(join3(tmpdir(), "airgate-session-"));
|
|
7922
7872
|
try {
|
|
7923
|
-
const raw =
|
|
7873
|
+
const raw = readFileSync4(s2.file);
|
|
7924
7874
|
let podDir;
|
|
7925
7875
|
const names = [];
|
|
7926
7876
|
if (s2.agent === "claude-code") {
|
|
@@ -7936,7 +7886,7 @@ async function packSession(s2, projectDir, podPath, limit = UPLOAD_LIMIT) {
|
|
|
7936
7886
|
mkdirSync2(b2, { recursive: true });
|
|
7937
7887
|
copy2(a, b2);
|
|
7938
7888
|
} else if (x.isFile()) {
|
|
7939
|
-
const body =
|
|
7889
|
+
const body = readFileSync4(a);
|
|
7940
7890
|
writeFileSync2(b2, /\.(jsonl?|txt|md)$/.test(x.name) ? rewriteAll(body.toString("utf8"), froms, podPath) : body);
|
|
7941
7891
|
}
|
|
7942
7892
|
}
|
|
@@ -7968,7 +7918,7 @@ function bringBack(sent, podContent, now = /* @__PURE__ */ new Date()) {
|
|
|
7968
7918
|
let text2 = rewritePath(podContent.toString("utf8"), sent.podPath, sent.localPath);
|
|
7969
7919
|
const localId = sent.localId ?? sent.id;
|
|
7970
7920
|
if (localId !== sent.id) text2 = text2.split(sent.id).join(localId);
|
|
7971
|
-
const local = existsSync2(sent.file) ?
|
|
7921
|
+
const local = existsSync2(sent.file) ? readFileSync4(sent.file) : null;
|
|
7972
7922
|
if (local && local.toString("utf8") === text2) return { agent: sent.agent, id: localId, file: sent.file, outcome: "same", sha256: sha256(local) };
|
|
7973
7923
|
const atomic = (file2, body2) => {
|
|
7974
7924
|
mkdirSync2(dirname(file2), { recursive: true });
|
|
@@ -7985,7 +7935,7 @@ function bringBack(sent, podContent, now = /* @__PURE__ */ new Date()) {
|
|
|
7985
7935
|
let file;
|
|
7986
7936
|
if (sent.agent === "claude-code") file = join3(dirname(sent.file), `${id}.jsonl`);
|
|
7987
7937
|
else {
|
|
7988
|
-
const p = (
|
|
7938
|
+
const p = (n2) => String(n2).padStart(2, "0");
|
|
7989
7939
|
const root = join3(dirname(sent.file), "..", "..", "..");
|
|
7990
7940
|
const stamp = `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}T${p(now.getHours())}-${p(now.getMinutes())}-${p(now.getSeconds())}`;
|
|
7991
7941
|
file = join3(root, String(now.getFullYear()), p(now.getMonth() + 1), p(now.getDate()), `rollout-${stamp}-${id}.jsonl`);
|
|
@@ -8031,11 +7981,11 @@ function unquote(p) {
|
|
|
8031
7981
|
bytes2.push(...Buffer.from(c, "utf8"));
|
|
8032
7982
|
continue;
|
|
8033
7983
|
}
|
|
8034
|
-
const
|
|
8035
|
-
if (/[0-7]/.test(
|
|
7984
|
+
const n2 = p[++i];
|
|
7985
|
+
if (/[0-7]/.test(n2)) {
|
|
8036
7986
|
bytes2.push(parseInt(p.slice(i, i + 3), 8));
|
|
8037
7987
|
i += 2;
|
|
8038
|
-
} else bytes2.push(simple[
|
|
7988
|
+
} else bytes2.push(simple[n2] ?? n2.charCodeAt(0));
|
|
8039
7989
|
}
|
|
8040
7990
|
return Buffer.from(bytes2).toString("utf8");
|
|
8041
7991
|
}
|
|
@@ -8171,7 +8121,7 @@ function findState(from) {
|
|
|
8171
8121
|
const p = statePath(dir);
|
|
8172
8122
|
if (existsSync3(p)) {
|
|
8173
8123
|
try {
|
|
8174
|
-
const s2 = JSON.parse(
|
|
8124
|
+
const s2 = JSON.parse(readFileSync5(p, "utf8"));
|
|
8175
8125
|
if (s2 && typeof s2.podId === "string" && typeof s2.dir === "string") return { root: dir, state: s2 };
|
|
8176
8126
|
} catch {
|
|
8177
8127
|
}
|
|
@@ -8187,7 +8137,7 @@ function excludeStateDir(projectDir) {
|
|
|
8187
8137
|
if (r.status !== 0) return false;
|
|
8188
8138
|
const raw = r.stdout.trim();
|
|
8189
8139
|
const file = isAbsolute(raw) ? raw : join4(projectDir, raw);
|
|
8190
|
-
const current = existsSync3(file) ?
|
|
8140
|
+
const current = existsSync3(file) ? readFileSync5(file, "utf8") : "";
|
|
8191
8141
|
if (current.split(/\r?\n/).some((l) => l.trim() === ".airgate/" || l.trim() === "/.airgate/" || l.trim() === ".airgate")) return true;
|
|
8192
8142
|
mkdirSync3(dirname2(file), { recursive: true });
|
|
8193
8143
|
appendFileSync(file, `${current && !current.endsWith("\n") ? "\n" : ""}# airgate up/down state
|
|
@@ -8196,8 +8146,8 @@ function excludeStateDir(projectDir) {
|
|
|
8196
8146
|
return true;
|
|
8197
8147
|
}
|
|
8198
8148
|
function podDirName(projectDir) {
|
|
8199
|
-
const
|
|
8200
|
-
return
|
|
8149
|
+
const n2 = basename(resolve4(projectDir)).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[.-]+/, "");
|
|
8150
|
+
return n2 && n2 !== "." && n2 !== ".." ? n2.slice(0, 64) : "project";
|
|
8201
8151
|
}
|
|
8202
8152
|
var up = {
|
|
8203
8153
|
name: "up",
|
|
@@ -8507,30 +8457,654 @@ ${check.stderr.trim()}`, "commit or stash local edits to those files, or look wi
|
|
|
8507
8457
|
}
|
|
8508
8458
|
};
|
|
8509
8459
|
|
|
8510
|
-
// src/
|
|
8511
|
-
|
|
8512
|
-
|
|
8513
|
-
|
|
8514
|
-
|
|
8515
|
-
var
|
|
8516
|
-
|
|
8517
|
-
|
|
8518
|
-
|
|
8519
|
-
|
|
8520
|
-
|
|
8521
|
-
|
|
8522
|
-
|
|
8523
|
-
|
|
8524
|
-
|
|
8525
|
-
|
|
8526
|
-
|
|
8527
|
-
|
|
8528
|
-
|
|
8529
|
-
|
|
8530
|
-
|
|
8531
|
-
|
|
8532
|
-
|
|
8533
|
-
|
|
8460
|
+
// src/io.ts
|
|
8461
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
8462
|
+
import { EventEmitter } from "node:events";
|
|
8463
|
+
import { PassThrough } from "node:stream";
|
|
8464
|
+
import { StringDecoder as StringDecoder2 } from "node:string_decoder";
|
|
8465
|
+
var LineReader = class {
|
|
8466
|
+
buf = "";
|
|
8467
|
+
ended = false;
|
|
8468
|
+
waiters = [];
|
|
8469
|
+
stream;
|
|
8470
|
+
attached = false;
|
|
8471
|
+
decoder = new StringDecoder2("utf8");
|
|
8472
|
+
constructor(stream) {
|
|
8473
|
+
this.stream = stream;
|
|
8474
|
+
}
|
|
8475
|
+
attach() {
|
|
8476
|
+
if (this.attached) return;
|
|
8477
|
+
this.attached = true;
|
|
8478
|
+
this.stream.on("data", (d) => {
|
|
8479
|
+
this.buf += typeof d === "string" ? d : this.decoder.write(d);
|
|
8480
|
+
this.wake();
|
|
8481
|
+
});
|
|
8482
|
+
this.stream.on("end", () => {
|
|
8483
|
+
this.ended = true;
|
|
8484
|
+
this.wake();
|
|
8485
|
+
});
|
|
8486
|
+
this.stream.on("error", () => {
|
|
8487
|
+
this.ended = true;
|
|
8488
|
+
this.wake();
|
|
8489
|
+
});
|
|
8490
|
+
}
|
|
8491
|
+
wake() {
|
|
8492
|
+
for (const w of this.waiters.splice(0)) w();
|
|
8493
|
+
}
|
|
8494
|
+
async line() {
|
|
8495
|
+
this.attach();
|
|
8496
|
+
this.stream.resume();
|
|
8497
|
+
for (; ; ) {
|
|
8498
|
+
const i = this.buf.indexOf("\n");
|
|
8499
|
+
if (i !== -1) {
|
|
8500
|
+
const l = this.buf.slice(0, i).replace(/\r$/, "");
|
|
8501
|
+
this.buf = this.buf.slice(i + 1);
|
|
8502
|
+
if (!this.buf) this.stream.pause();
|
|
8503
|
+
return l;
|
|
8504
|
+
}
|
|
8505
|
+
if (this.ended) {
|
|
8506
|
+
const rest = this.buf;
|
|
8507
|
+
this.buf = "";
|
|
8508
|
+
return rest ? rest : null;
|
|
8509
|
+
}
|
|
8510
|
+
await new Promise((r) => this.waiters.push(r));
|
|
8511
|
+
}
|
|
8512
|
+
}
|
|
8513
|
+
};
|
|
8514
|
+
function ttyPrompt(stdin, out) {
|
|
8515
|
+
return (question, opts2 = {}) => new Promise((resolve6, reject) => {
|
|
8516
|
+
out.write(question);
|
|
8517
|
+
const decoder = new StringDecoder2("utf8");
|
|
8518
|
+
let answer = "";
|
|
8519
|
+
const wasRaw = Boolean(stdin.isRaw);
|
|
8520
|
+
stdin.setRawMode?.(true);
|
|
8521
|
+
stdin.resume();
|
|
8522
|
+
const done = (err) => {
|
|
8523
|
+
stdin.off("data", onData);
|
|
8524
|
+
stdin.setRawMode?.(wasRaw);
|
|
8525
|
+
stdin.pause();
|
|
8526
|
+
out.write("\n");
|
|
8527
|
+
if (err) reject(err);
|
|
8528
|
+
else resolve6(answer);
|
|
8529
|
+
};
|
|
8530
|
+
const onData = (d) => {
|
|
8531
|
+
for (const ch of decoder.write(d)) {
|
|
8532
|
+
if (ch === "\r" || ch === "\n") return done();
|
|
8533
|
+
if (ch === "") return done(Object.assign(new Error("cancelled"), { code: "cancelled" }));
|
|
8534
|
+
if (ch === "") {
|
|
8535
|
+
if (!answer) return done(Object.assign(new Error("cancelled"), { code: "cancelled" }));
|
|
8536
|
+
continue;
|
|
8537
|
+
}
|
|
8538
|
+
if (ch === "\x7F" || ch === "\b") {
|
|
8539
|
+
if (answer) {
|
|
8540
|
+
answer = [...answer].slice(0, -1).join("");
|
|
8541
|
+
if (!opts2.hidden) out.write("\b \b");
|
|
8542
|
+
}
|
|
8543
|
+
continue;
|
|
8544
|
+
}
|
|
8545
|
+
if (ch < " ") continue;
|
|
8546
|
+
answer += ch;
|
|
8547
|
+
if (!opts2.hidden) out.write(ch);
|
|
8548
|
+
}
|
|
8549
|
+
};
|
|
8550
|
+
stdin.on("data", onData);
|
|
8551
|
+
});
|
|
8552
|
+
}
|
|
8553
|
+
function openInBrowser(url) {
|
|
8554
|
+
const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url]] : ["xdg-open", [url]];
|
|
8555
|
+
return new Promise((resolve6) => {
|
|
8556
|
+
try {
|
|
8557
|
+
const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
|
|
8558
|
+
child.on("error", () => resolve6(false));
|
|
8559
|
+
child.on("spawn", () => {
|
|
8560
|
+
child.unref();
|
|
8561
|
+
resolve6(true);
|
|
8562
|
+
});
|
|
8563
|
+
} catch {
|
|
8564
|
+
resolve6(false);
|
|
8565
|
+
}
|
|
8566
|
+
});
|
|
8567
|
+
}
|
|
8568
|
+
function processIo() {
|
|
8569
|
+
const stdin = process.stdin;
|
|
8570
|
+
const lines = new LineReader(stdin);
|
|
8571
|
+
const tty = ttyPrompt(stdin, process.stderr);
|
|
8572
|
+
return {
|
|
8573
|
+
stdin,
|
|
8574
|
+
stdout: process.stdout,
|
|
8575
|
+
stderr: process.stderr,
|
|
8576
|
+
env: process.env,
|
|
8577
|
+
// `npm run cli -- up` runs in packages/cli; INIT_CWD is where the user typed it
|
|
8578
|
+
cwd: process.env.AIRGATE_CWD || process.env.npm_lifecycle_event === "dev" && process.env.INIT_CWD || process.cwd(),
|
|
8579
|
+
async prompt(q, o) {
|
|
8580
|
+
if (stdin.isTTY) return tty(q, o);
|
|
8581
|
+
process.stderr.write(q);
|
|
8582
|
+
const l = await lines.line();
|
|
8583
|
+
if (l === null) throw Object.assign(new Error("no input (stdin closed)"), { code: "no_input" });
|
|
8584
|
+
process.stderr.write("\n");
|
|
8585
|
+
return l;
|
|
8586
|
+
},
|
|
8587
|
+
openUrl: openInBrowser,
|
|
8588
|
+
websocket: (url) => new WebSocket(url),
|
|
8589
|
+
signals: process
|
|
8590
|
+
};
|
|
8591
|
+
}
|
|
8592
|
+
function memoryIo(opts2 = {}) {
|
|
8593
|
+
const out = new PassThrough();
|
|
8594
|
+
const err = new PassThrough();
|
|
8595
|
+
let stdout = "";
|
|
8596
|
+
let stderr = "";
|
|
8597
|
+
out.on("data", (d) => stdout += d.toString());
|
|
8598
|
+
err.on("data", (d) => stderr += d.toString());
|
|
8599
|
+
const answers = [...opts2.answers ?? []];
|
|
8600
|
+
const opened = [];
|
|
8601
|
+
const prompts = [];
|
|
8602
|
+
const stdin = opts2.stdin ?? Object.assign(new PassThrough(), { isTTY: false });
|
|
8603
|
+
if (!opts2.stdin) stdin.end();
|
|
8604
|
+
const io = {
|
|
8605
|
+
stdin,
|
|
8606
|
+
stdout: out,
|
|
8607
|
+
stderr: err,
|
|
8608
|
+
env: opts2.env ?? {},
|
|
8609
|
+
cwd: opts2.cwd ?? process.cwd(),
|
|
8610
|
+
async prompt(q) {
|
|
8611
|
+
prompts.push(q);
|
|
8612
|
+
const a = answers.shift();
|
|
8613
|
+
if (a === void 0) throw Object.assign(new Error(`no answer queued for prompt: ${q}`), { code: "no_input" });
|
|
8614
|
+
return a;
|
|
8615
|
+
},
|
|
8616
|
+
async openUrl(u) {
|
|
8617
|
+
opened.push(u);
|
|
8618
|
+
return true;
|
|
8619
|
+
},
|
|
8620
|
+
websocket: () => {
|
|
8621
|
+
throw new Error("no websocket in memoryIo");
|
|
8622
|
+
},
|
|
8623
|
+
signals: new EventEmitter()
|
|
8624
|
+
};
|
|
8625
|
+
return {
|
|
8626
|
+
io,
|
|
8627
|
+
answers,
|
|
8628
|
+
opened,
|
|
8629
|
+
prompts,
|
|
8630
|
+
get stdout() {
|
|
8631
|
+
return stdout;
|
|
8632
|
+
},
|
|
8633
|
+
get stderr() {
|
|
8634
|
+
return stderr;
|
|
8635
|
+
},
|
|
8636
|
+
clear() {
|
|
8637
|
+
stdout = "";
|
|
8638
|
+
stderr = "";
|
|
8639
|
+
}
|
|
8640
|
+
};
|
|
8641
|
+
}
|
|
8642
|
+
|
|
8643
|
+
// src/commands/sub.ts
|
|
8644
|
+
async function runCommand(ctx, cmd, argv, cwd) {
|
|
8645
|
+
const m = memoryIo({ env: ctx.io.env, cwd });
|
|
8646
|
+
const args = parseArgs([...argv, "--json"], cmd.flags ?? {});
|
|
8647
|
+
const sub = { ...makeCtx(m.io, createUi(m.io), args, ctx.fetch), api: ctx.api };
|
|
8648
|
+
await cmd.run(sub);
|
|
8649
|
+
const out = m.stdout.trim();
|
|
8650
|
+
try {
|
|
8651
|
+
return out ? JSON.parse(out) : { ok: true };
|
|
8652
|
+
} catch {
|
|
8653
|
+
return { output: out };
|
|
8654
|
+
}
|
|
8655
|
+
}
|
|
8656
|
+
|
|
8657
|
+
// src/commands/run.ts
|
|
8658
|
+
import { existsSync as existsSync5, readFileSync as readFileSync7, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync5 } from "node:fs";
|
|
8659
|
+
import { basename as basename2, resolve as resolve5 } from "node:path";
|
|
8660
|
+
import { randomBytes } from "node:crypto";
|
|
8661
|
+
|
|
8662
|
+
// src/relay.ts
|
|
8663
|
+
import { chmodSync as chmodSync2, existsSync as existsSync4, mkdirSync as mkdirSync4, readFileSync as readFileSync6, renameSync as renameSync3, writeFileSync as writeFileSync4 } from "node:fs";
|
|
8664
|
+
import { join as join5 } from "node:path";
|
|
8665
|
+
function vendorOf(name) {
|
|
8666
|
+
const n2 = (name ?? "").toLowerCase();
|
|
8667
|
+
if (n2 === "claude" || n2 === "anthropic" || n2 === "claude-code") return "anthropic";
|
|
8668
|
+
if (n2 === "openai" || n2 === "codex" || n2 === "chatgpt") return "openai";
|
|
8669
|
+
return null;
|
|
8670
|
+
}
|
|
8671
|
+
var vendorLabel = (v) => v === "anthropic" ? "Claude" : "OpenAI";
|
|
8672
|
+
var kindOf = (v, value) => v === "anthropic" && value.startsWith("sk-ant-oat") ? "oauth_token" : "api_key";
|
|
8673
|
+
var modelsPath = (env) => join5(configDir(env), "models.json");
|
|
8674
|
+
function readSaved(env) {
|
|
8675
|
+
try {
|
|
8676
|
+
return JSON.parse(readFileSync6(modelsPath(env), "utf8"));
|
|
8677
|
+
} catch {
|
|
8678
|
+
return {};
|
|
8679
|
+
}
|
|
8680
|
+
}
|
|
8681
|
+
function saveLocal(env, vendor, value) {
|
|
8682
|
+
const all = readSaved(env);
|
|
8683
|
+
if (value) all[vendor] = { kind: kindOf(vendor, value), value };
|
|
8684
|
+
else delete all[vendor];
|
|
8685
|
+
const dir = configDir(env);
|
|
8686
|
+
mkdirSync4(dir, { recursive: true, mode: 448 });
|
|
8687
|
+
const file = modelsPath(env);
|
|
8688
|
+
const tmp = `${file}.${process.pid}.tmp`;
|
|
8689
|
+
writeFileSync4(tmp, JSON.stringify(all, null, 2) + "\n", { mode: 384 });
|
|
8690
|
+
chmodSync2(tmp, 384);
|
|
8691
|
+
renameSync3(tmp, file);
|
|
8692
|
+
chmodSync2(file, 384);
|
|
8693
|
+
}
|
|
8694
|
+
function hasSavedFile(env) {
|
|
8695
|
+
return existsSync4(modelsPath(env));
|
|
8696
|
+
}
|
|
8697
|
+
function localAuth(env) {
|
|
8698
|
+
const saved = readSaved(env);
|
|
8699
|
+
const out = {};
|
|
8700
|
+
if (env.CLAUDE_CODE_OAUTH_TOKEN) out.anthropic = { kind: "oauth_token", value: env.CLAUDE_CODE_OAUTH_TOKEN, from: "CLAUDE_CODE_OAUTH_TOKEN" };
|
|
8701
|
+
else if (env.ANTHROPIC_API_KEY) out.anthropic = { kind: "api_key", value: env.ANTHROPIC_API_KEY, from: "ANTHROPIC_API_KEY" };
|
|
8702
|
+
else if (saved.anthropic) out.anthropic = { ...saved.anthropic, from: "this laptop" };
|
|
8703
|
+
if (env.OPENAI_API_KEY) out.openai = { kind: "api_key", value: env.OPENAI_API_KEY, from: "OPENAI_API_KEY" };
|
|
8704
|
+
else if (saved.openai) out.openai = { ...saved.openai, from: "this laptop" };
|
|
8705
|
+
return out;
|
|
8706
|
+
}
|
|
8707
|
+
function sign(vendor, auth, headers) {
|
|
8708
|
+
if (vendor === "openai") {
|
|
8709
|
+
headers.authorization = `Bearer ${auth.value}`;
|
|
8710
|
+
return;
|
|
8711
|
+
}
|
|
8712
|
+
if (auth.kind === "oauth_token") {
|
|
8713
|
+
delete headers["x-api-key"];
|
|
8714
|
+
headers.authorization = `Bearer ${auth.value}`;
|
|
8715
|
+
const beta = (headers["anthropic-beta"] ?? "").split(",").map((s2) => s2.trim()).filter(Boolean);
|
|
8716
|
+
if (!beta.includes("oauth-2025-04-20")) beta.push("oauth-2025-04-20");
|
|
8717
|
+
headers["anthropic-beta"] = beta.join(",");
|
|
8718
|
+
} else {
|
|
8719
|
+
delete headers.authorization;
|
|
8720
|
+
headers["x-api-key"] = auth.value;
|
|
8721
|
+
}
|
|
8722
|
+
}
|
|
8723
|
+
var UPSTREAM = { anthropic: "https://api.anthropic.com", openai: "https://api.openai.com" };
|
|
8724
|
+
function startRelay(o) {
|
|
8725
|
+
const vendors = Object.keys(o.auth).filter((v) => o.auth[v]);
|
|
8726
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
8727
|
+
let ws = null;
|
|
8728
|
+
let stopped = false;
|
|
8729
|
+
let signed = 0;
|
|
8730
|
+
let attempt = 0;
|
|
8731
|
+
let resolveDone;
|
|
8732
|
+
const done = new Promise((r) => resolveDone = r);
|
|
8733
|
+
const base = (v) => o.io.env[v === "anthropic" ? "AIRGATE_RELAY_ANTHROPIC_URL" : "AIRGATE_RELAY_OPENAI_URL"] || UPSTREAM[v];
|
|
8734
|
+
const handle2 = async (sock, m) => {
|
|
8735
|
+
const send = (f) => sock.readyState === 1 && sock.send(JSON.stringify(f));
|
|
8736
|
+
const auth = o.auth[m.vendor];
|
|
8737
|
+
if (!auth) return send({ t: "error", id: m.id, message: `this laptop has no ${vendorLabel(m.vendor)} sign-in` });
|
|
8738
|
+
const headers = { ...m.headers };
|
|
8739
|
+
sign(m.vendor, auth, headers);
|
|
8740
|
+
const ctl = new AbortController();
|
|
8741
|
+
inflight.set(m.id, ctl);
|
|
8742
|
+
try {
|
|
8743
|
+
const res = await o.fetch(`${base(m.vendor)}${m.path}`, {
|
|
8744
|
+
method: m.method,
|
|
8745
|
+
headers,
|
|
8746
|
+
body: m.body ? Buffer.from(m.body, "base64") : void 0,
|
|
8747
|
+
signal: ctl.signal
|
|
8748
|
+
});
|
|
8749
|
+
const out = {};
|
|
8750
|
+
res.headers.forEach((v, k) => {
|
|
8751
|
+
if (!["content-encoding", "content-length", "transfer-encoding", "connection", "keep-alive"].includes(k)) out[k] = v;
|
|
8752
|
+
});
|
|
8753
|
+
send({ t: "head", id: m.id, status: res.status, headers: out });
|
|
8754
|
+
signed++;
|
|
8755
|
+
if (res.body) for await (const chunk of res.body) send({ t: "data", id: m.id, b64: Buffer.from(chunk).toString("base64") });
|
|
8756
|
+
send({ t: "end", id: m.id });
|
|
8757
|
+
} catch (e) {
|
|
8758
|
+
if (!ctl.signal.aborted) send({ t: "error", id: m.id, message: e instanceof Error ? e.message : String(e) });
|
|
8759
|
+
} finally {
|
|
8760
|
+
inflight.delete(m.id);
|
|
8761
|
+
}
|
|
8762
|
+
};
|
|
8763
|
+
const connect = () => {
|
|
8764
|
+
if (stopped) return;
|
|
8765
|
+
const sock = o.io.websocket(o.url);
|
|
8766
|
+
ws = sock;
|
|
8767
|
+
sock.onopen = () => {
|
|
8768
|
+
attempt = 0;
|
|
8769
|
+
sock.send(JSON.stringify({ t: "hello", vendors, client: `airgate ${VERSION}` }));
|
|
8770
|
+
};
|
|
8771
|
+
sock.onmessage = (ev) => {
|
|
8772
|
+
let m;
|
|
8773
|
+
try {
|
|
8774
|
+
m = JSON.parse(String(ev.data));
|
|
8775
|
+
} catch {
|
|
8776
|
+
return;
|
|
8777
|
+
}
|
|
8778
|
+
if (m.t === "req" && typeof m.id === "string") void handle2(sock, m);
|
|
8779
|
+
else if (m.t === "cancel" && typeof m.id === "string") inflight.get(m.id)?.abort();
|
|
8780
|
+
};
|
|
8781
|
+
sock.onerror = () => void 0;
|
|
8782
|
+
sock.onclose = (ev) => {
|
|
8783
|
+
for (const c of inflight.values()) c.abort();
|
|
8784
|
+
inflight.clear();
|
|
8785
|
+
if (stopped) return resolveDone();
|
|
8786
|
+
if (ev.code === 4401 || ev.code === 1008) {
|
|
8787
|
+
o.say("the gateway refused this laptop (sign in again: airgate login)");
|
|
8788
|
+
stopped = true;
|
|
8789
|
+
return resolveDone();
|
|
8790
|
+
}
|
|
8791
|
+
attempt++;
|
|
8792
|
+
setTimeout(connect, Math.min(15e3, 500 * 2 ** Math.min(attempt, 5)));
|
|
8793
|
+
};
|
|
8794
|
+
};
|
|
8795
|
+
connect();
|
|
8796
|
+
return {
|
|
8797
|
+
vendors,
|
|
8798
|
+
done,
|
|
8799
|
+
count: () => signed,
|
|
8800
|
+
stop() {
|
|
8801
|
+
stopped = true;
|
|
8802
|
+
try {
|
|
8803
|
+
ws?.close(1e3, "done");
|
|
8804
|
+
} catch {
|
|
8805
|
+
}
|
|
8806
|
+
resolveDone();
|
|
8807
|
+
}
|
|
8808
|
+
};
|
|
8809
|
+
}
|
|
8810
|
+
|
|
8811
|
+
// src/commands/models.ts
|
|
8812
|
+
var HOW = {
|
|
8813
|
+
anthropic: "a Claude subscription token (run `claude setup-token` and paste what it prints) or an Anthropic API key",
|
|
8814
|
+
openai: "an OpenAI API key"
|
|
8815
|
+
};
|
|
8816
|
+
function relayIfSignedIn(ctx, say) {
|
|
8817
|
+
const auth = localAuth(ctx.io.env);
|
|
8818
|
+
if (!Object.keys(auth).length || !ctx.api.signedIn) return null;
|
|
8819
|
+
try {
|
|
8820
|
+
return startRelay({ io: ctx.io, url: ctx.api.relayUrl(), fetch: ctx.fetch, auth, say: say ?? (() => void 0) });
|
|
8821
|
+
} catch {
|
|
8822
|
+
return null;
|
|
8823
|
+
}
|
|
8824
|
+
}
|
|
8825
|
+
var models = {
|
|
8826
|
+
name: "models",
|
|
8827
|
+
group: "account",
|
|
8828
|
+
usage: "[connect|disconnect <claude|openai> [--laptop]]",
|
|
8829
|
+
summary: "how pods' agents sign in to their model: in the pod, through this laptop, or connected to airgate",
|
|
8830
|
+
example: "airgate models connect claude --laptop",
|
|
8831
|
+
notes: [
|
|
8832
|
+
"three ways, pick per your comfort: sign in inside the pod (nothing stored with airgate); this laptop signs (airgate relay: airgate holds nothing, works while the laptop is connected); or connect a sign-in to airgate (encrypted on the gateway, never sent to a pod).",
|
|
8833
|
+
"connect <claude|openai>: saves it on airgate. with --laptop: saves it only on this laptop (~/.config/airgate/models.json), for airgate relay.",
|
|
8834
|
+
"claude takes a subscription token (claude setup-token) or an API key; openai takes an API key. the value is read from a hidden prompt or stdin, never from the command line.",
|
|
8835
|
+
"this laptop also uses CLAUDE_CODE_OAUTH_TOKEN, ANTHROPIC_API_KEY or OPENAI_API_KEY from its environment."
|
|
8836
|
+
],
|
|
8837
|
+
flags: { laptop: { type: "boolean", help: "keep the sign-in on this laptop only (for airgate relay)" } },
|
|
8838
|
+
async run(ctx) {
|
|
8839
|
+
const { ui: ui2 } = ctx;
|
|
8840
|
+
const [action, who, extra] = ctx.args.positionals;
|
|
8841
|
+
if (extra) throw new UsageError(`unexpected "${extra}"`, "values are read from a hidden prompt or stdin, never from arguments");
|
|
8842
|
+
const laptop = bool(ctx.args, "laptop");
|
|
8843
|
+
if (action === "connect" || action === "disconnect") {
|
|
8844
|
+
const vendor = vendorOf(who);
|
|
8845
|
+
if (!vendor) throw new UsageError("which one? claude or openai", `airgate models ${action} claude`);
|
|
8846
|
+
if (action === "disconnect") {
|
|
8847
|
+
if (laptop) {
|
|
8848
|
+
saveLocal(ctx.io.env, vendor, null);
|
|
8849
|
+
if (ctx.json) return ui2.json({ ok: true, vendor, where: "laptop" });
|
|
8850
|
+
return void ui2.out(`${vendorLabel(vendor)}: removed from this laptop.`);
|
|
8851
|
+
}
|
|
8852
|
+
const r2 = await ctx.api.disconnectModel(vendor);
|
|
8853
|
+
if (ctx.json) return ui2.json({ ...r2, vendor, where: "airgate" });
|
|
8854
|
+
return void ui2.out(r2.ok ? `${vendorLabel(vendor)}: disconnected from airgate. airgate no longer holds it.` : `${vendorLabel(vendor)} wasn't connected to airgate.`);
|
|
8855
|
+
}
|
|
8856
|
+
if (!ctx.json && ctx.io.stdin.isTTY) ui2.err(ui2.ce("dim", `paste ${HOW[vendor]}.`));
|
|
8857
|
+
const value = (await readValue(ctx, vendor === "anthropic" ? "your Claude sign-in" : "your OpenAI key", `printf %s "$KEY" | airgate models connect ${who}${laptop ? " --laptop" : ""}`)).trim();
|
|
8858
|
+
if (value.length < 20 || /\s/.test(value)) throw new CliError("that doesn't look like a key or token", HOW[vendor]);
|
|
8859
|
+
if (laptop) {
|
|
8860
|
+
saveLocal(ctx.io.env, vendor, value);
|
|
8861
|
+
if (ctx.json) return ui2.json({ ok: true, vendor, kind: kindOf(vendor, value), where: "laptop" });
|
|
8862
|
+
ui2.out(`${ui2.c("ok", "saved")} on this laptop only (${kindOf(vendor, value) === "oauth_token" ? "subscription token" : "API key"}). airgate doesn't get it.`);
|
|
8863
|
+
return void ui2.out(ui2.c("dim", "pods use it while this laptop is connected: airgate relay (airgate run and airgate mcp do this on their own)."));
|
|
8864
|
+
}
|
|
8865
|
+
const r = await ctx.api.connectModel(vendor, value);
|
|
8866
|
+
if (ctx.json) return ui2.json({ ...r, where: "airgate" });
|
|
8867
|
+
ui2.out(`${ui2.c("ok", "connected")}: ${vendorLabel(vendor)} (${r.kind === "oauth_token" ? "subscription token" : "API key"}), kept encrypted on airgate and never sent to a pod.`);
|
|
8868
|
+
return void ui2.out(ui2.c("dim", `every pod's ${vendor === "anthropic" ? "Claude Code" : "Codex"} can use it now, unless signed in inside the pod. undo: airgate models disconnect ${who}`));
|
|
8869
|
+
}
|
|
8870
|
+
if (action) throw new UsageError(`unknown "${action}"`, "airgate models \xB7 airgate models connect claude [--laptop] \xB7 airgate models disconnect claude [--laptop]");
|
|
8871
|
+
const st = await ctx.api.modelsAuth();
|
|
8872
|
+
const local = localAuth(ctx.io.env);
|
|
8873
|
+
if (ctx.json) return ui2.json({ ...st, thisLaptop: Object.fromEntries(Object.entries(local).map(([v, a]) => [v, { kind: a.kind, from: a.from }])) });
|
|
8874
|
+
const rows2 = ["anthropic", "openai"].map((v) => {
|
|
8875
|
+
const c = st.connected.find((x) => x.vendor === v);
|
|
8876
|
+
const l = local[v];
|
|
8877
|
+
return [
|
|
8878
|
+
vendorLabel(v),
|
|
8879
|
+
c ? ui2.c("ok", `connected to airgate (${c.kind === "oauth_token" ? "subscription" : "API key"})`) : ui2.c("dim", "not on airgate"),
|
|
8880
|
+
l ? `this laptop: ${l.from}${st.laptop.connected && st.laptop.vendors.includes(v) ? ui2.c("ok", " \xB7 relaying") : ""}` : ui2.c("dim", "not on this laptop")
|
|
8881
|
+
];
|
|
8882
|
+
});
|
|
8883
|
+
ui2.table(rows2, { head: ["model", "airgate", "laptop"] });
|
|
8884
|
+
ui2.out();
|
|
8885
|
+
ui2.out(ui2.c("dim", "a pod uses, in order: a sign-in made inside it \xB7 a connected laptop (airgate relay) \xB7 one connected to airgate."));
|
|
8886
|
+
if (!st.connected.length && !Object.keys(local).length) ui2.out(ui2.c("dim", "set one up: airgate models connect claude --laptop (airgate holds nothing) or airgate models connect claude."));
|
|
8887
|
+
if (!hasSavedFile(ctx.io.env) && Object.keys(local).length) ui2.out(ui2.c("dim", "this laptop signs from its environment variables."));
|
|
8888
|
+
}
|
|
8889
|
+
};
|
|
8890
|
+
var relay = {
|
|
8891
|
+
name: "relay",
|
|
8892
|
+
group: "account",
|
|
8893
|
+
usage: "",
|
|
8894
|
+
summary: "sign your pods' model calls from this laptop while it runs: airgate never holds your sign-in",
|
|
8895
|
+
example: "airgate relay",
|
|
8896
|
+
notes: [
|
|
8897
|
+
"uses this laptop's Claude / OpenAI sign-in (see airgate models). only pods with no sign-in of their own use it, and only while this runs.",
|
|
8898
|
+
"airgate run and airgate mcp start it on their own when this laptop has a sign-in."
|
|
8899
|
+
],
|
|
8900
|
+
async run(ctx) {
|
|
8901
|
+
const { ui: ui2 } = ctx;
|
|
8902
|
+
const auth = localAuth(ctx.io.env);
|
|
8903
|
+
if (!Object.keys(auth).length) throw new CliError("no model sign-in on this laptop", "airgate models connect claude --laptop (or set CLAUDE_CODE_OAUTH_TOKEN / ANTHROPIC_API_KEY / OPENAI_API_KEY)");
|
|
8904
|
+
const r = startRelay({ io: ctx.io, url: ctx.api.relayUrl(), fetch: ctx.fetch, auth, say: (l) => ui2.err(ui2.ce("warn", l)) });
|
|
8905
|
+
const stop = () => r.stop();
|
|
8906
|
+
ctx.io.signals.on("SIGINT", stop);
|
|
8907
|
+
ctx.io.signal?.addEventListener("abort", stop);
|
|
8908
|
+
if (!ctx.json) {
|
|
8909
|
+
ui2.out(`relaying ${r.vendors.map(vendorLabel).join(" and ")} for your pods from this laptop. ${ui2.c("dim", "ctrl-c to stop.")}`);
|
|
8910
|
+
for (const v of r.vendors) ui2.out(ui2.c("dim", ` ${vendorLabel(v)}: ${auth[v].from}`));
|
|
8911
|
+
}
|
|
8912
|
+
try {
|
|
8913
|
+
await r.done;
|
|
8914
|
+
} finally {
|
|
8915
|
+
ctx.io.signals.off("SIGINT", stop);
|
|
8916
|
+
ctx.io.signal?.removeEventListener("abort", stop);
|
|
8917
|
+
}
|
|
8918
|
+
if (ctx.json) return ui2.json({ signed: r.count() });
|
|
8919
|
+
ui2.out(`stopped. ${r.count()} model call(s) signed.`);
|
|
8920
|
+
}
|
|
8921
|
+
};
|
|
8922
|
+
|
|
8923
|
+
// src/commands/run.ts
|
|
8924
|
+
var MODEL = {
|
|
8925
|
+
"claude-code": { vendor: "anthropic", secrets: ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY"] },
|
|
8926
|
+
codex: { vendor: "openai", secrets: ["OPENAI_API_KEY"] }
|
|
8927
|
+
};
|
|
8928
|
+
var shortId = () => randomBytes(2).toString("hex");
|
|
8929
|
+
var runName = (base) => `${base.slice(0, 52)}-run-${shortId()}`;
|
|
8930
|
+
async function signInWarning(ctx, agent, relaying) {
|
|
8931
|
+
const m = MODEL[agent];
|
|
8932
|
+
if (!m || relaying.includes(m.vendor)) return null;
|
|
8933
|
+
const st = await ctx.api.modelsAuth().catch(() => null);
|
|
8934
|
+
if (st?.connected.some((c) => c.vendor === m.vendor)) return null;
|
|
8935
|
+
const list2 = await ctx.api.secrets().catch(() => null);
|
|
8936
|
+
if (list2?.account.some((s2) => m.secrets.includes(s2.name))) return null;
|
|
8937
|
+
if (!st && !list2) return null;
|
|
8938
|
+
const who = m.vendor === "anthropic" ? "claude" : "openai";
|
|
8939
|
+
return `${AGENTS[agent].label} has no sign-in for a fresh pod. pick one: airgate models connect ${who} --laptop (this laptop signs; airgate holds nothing), or airgate models connect ${who} (kept on airgate)`;
|
|
8940
|
+
}
|
|
8941
|
+
async function finish(ctx, pod, keep) {
|
|
8942
|
+
if (keep === "destroy") await ctx.ui.spin(`deleting ${pod.spec.name}`, ctx.api.destroy(pod.id));
|
|
8943
|
+
else if (keep === "sleep") await ctx.ui.spin(`putting ${pod.spec.name} to sleep`, ctx.api.stop(pod.id)).catch(() => void 0);
|
|
8944
|
+
}
|
|
8945
|
+
var runTask = {
|
|
8946
|
+
name: "run",
|
|
8947
|
+
group: "up \xB7 down",
|
|
8948
|
+
usage: '"<task>" [dir] [--repo owner/name] [--agent a] [--env [.env]] [--keep] [--no-apply] [--pr] [--timeout min]',
|
|
8949
|
+
summary: "one task in a fresh pod: send the project, run it, bring the changes back, delete the pod",
|
|
8950
|
+
example: 'airgate run "get the failing tests passing"',
|
|
8951
|
+
notes: [
|
|
8952
|
+
"this folder (default): launches a pod, sends the project like `airgate up` (no conversation), hands the pod's agent the task, waits, applies the result here like `airgate down`, then deletes the pod.",
|
|
8953
|
+
"--no-apply prints the patch instead of applying it (the pod sleeps, so `airgate down --pod <it>` can still apply it); --keep keeps the pod (asleep).",
|
|
8954
|
+
"--repo owner/name (or a github link): runs on the repo itself instead (private repos need GitHub connected: airgate github connect). the work stays on a branch in the pod; --pr asks the agent to open a pull request, which waits in your airlock. the pod sleeps afterwards (free).",
|
|
8955
|
+
"if the task fails, the patch does not apply, or it runs past --timeout (default 60 minutes), the pod is kept so nothing is lost.",
|
|
8956
|
+
"the pod's agent needs a model sign-in: while run runs, this laptop signs for it if it has one (airgate models connect claude --laptop); or connect one to airgate (airgate models connect claude). see: airgate help models"
|
|
8957
|
+
],
|
|
8958
|
+
flags: {
|
|
8959
|
+
repo: { type: "string", value: "owner/name", help: "run on a GitHub repo instead of this folder" },
|
|
8960
|
+
agent: { type: "string", value: "agent", choices: Object.keys(AGENTS), help: "the pod's agent (default claude-code)" },
|
|
8961
|
+
memory: { type: "string", value: "size", choices: Object.keys(SIZES), help: "pod size (default standard)" },
|
|
8962
|
+
env: { type: "string", value: "file", implicit: ".env", help: "send a dotenv file's keys as the pod's secrets (default .env)" },
|
|
8963
|
+
keep: { type: "boolean", help: "keep the pod afterwards (asleep) instead of deleting it" },
|
|
8964
|
+
"no-apply": { type: "boolean", help: "print the patch, change nothing here (the pod is kept, asleep)" },
|
|
8965
|
+
pr: { type: "boolean", help: "with --repo: have the agent open a pull request (it waits in your airlock)" },
|
|
8966
|
+
timeout: { type: "string", value: "minutes", default: "60", help: "stop waiting after this long (the pod keeps working)" },
|
|
8967
|
+
yes: { type: "boolean", short: "y", help: "don't ask" }
|
|
8968
|
+
},
|
|
8969
|
+
async run(ctx) {
|
|
8970
|
+
const { ui: ui2 } = ctx;
|
|
8971
|
+
const [task2, dirArg, extra] = ctx.args.positionals;
|
|
8972
|
+
if (!task2?.trim()) throw new UsageError("what should the agent do?", 'airgate run "get the failing tests passing"');
|
|
8973
|
+
if (extra) throw new UsageError(`unexpected "${extra}" (put the task in quotes)`);
|
|
8974
|
+
if (task2.length > 3500) throw new UsageError("the task is 3500 characters at most");
|
|
8975
|
+
const agent = str(ctx.args, "agent") ?? "claude-code";
|
|
8976
|
+
const size2 = str(ctx.args, "memory") ?? "standard";
|
|
8977
|
+
const minutes = Number(str(ctx.args, "timeout") ?? "60");
|
|
8978
|
+
if (!Number.isFinite(minutes) || minutes <= 0) throw new UsageError("--timeout: minutes, e.g. 30");
|
|
8979
|
+
const repo = str(ctx.args, "repo");
|
|
8980
|
+
const warnings = [];
|
|
8981
|
+
const relayer = relayIfSignedIn(ctx);
|
|
8982
|
+
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)`));
|
|
8983
|
+
const warn = await signInWarning(ctx, agent, relayer?.vendors ?? []);
|
|
8984
|
+
if (warn) {
|
|
8985
|
+
warnings.push(warn);
|
|
8986
|
+
if (!ctx.json) ui2.err(ui2.ce("warn", warn));
|
|
8987
|
+
}
|
|
8988
|
+
try {
|
|
8989
|
+
return await go();
|
|
8990
|
+
} finally {
|
|
8991
|
+
relayer?.stop();
|
|
8992
|
+
}
|
|
8993
|
+
async function go() {
|
|
8994
|
+
const work = async (pod2, text3) => {
|
|
8995
|
+
let r2 = await ui2.spin(`handing ${pod2.spec.name} the task`, ctx.api.startTask(pod2.id, text3));
|
|
8996
|
+
r2 = await ui2.spin(`${pod2.spec.name} is working (${AGENTS[agent].label})`, () => waitRun(ctx, pod2.id, r2, minutes * 6e4));
|
|
8997
|
+
const log2 = await ctx.api.runLog(pod2.id, r2.id).catch(() => "");
|
|
8998
|
+
return { run: r2, log: log2, done: r2.endedAt !== void 0, ok: r2.endedAt !== void 0 && r2.exitCode === 0 };
|
|
8999
|
+
};
|
|
9000
|
+
if (repo) {
|
|
9001
|
+
if (dirArg) throw new UsageError("a folder or --repo, not both");
|
|
9002
|
+
const look = await ctx.api.inspectRepo(repo).catch((e) => {
|
|
9003
|
+
if (e instanceof ApiError && e.status === 404) return null;
|
|
9004
|
+
throw e;
|
|
9005
|
+
});
|
|
9006
|
+
if (look && !look.reachable) {
|
|
9007
|
+
const gh = await ctx.api.github().catch(() => null);
|
|
9008
|
+
if (gh && !gh.configured) throw new CliError(`${look.repo} is private, and private repos need the Airgate GitHub App, which isn't set up on this gateway yet`, 'run it from a local clone instead: cd into it, then airgate run "\u2026"');
|
|
9009
|
+
throw new CliError(`airgate can't reach ${look.repo} (private, and not shared with Airgate)`, "connect GitHub and install the app on it: airgate github connect");
|
|
9010
|
+
}
|
|
9011
|
+
const name2 = runName((look?.repo ?? repo).split("/").pop() ?? "repo");
|
|
9012
|
+
const spec = { name: name2, agent, size: size2, repo: look?.repo ?? repo, masking: true, network: "allowlist", allow: DEFAULT_ALLOW, autoStopMinutes: null, autoCheckpoint: true };
|
|
9013
|
+
const pod2 = await ui2.spin(`launching ${name2} on ${spec.repo}`, async () => waitReady(ctx, await ctx.api.launch(spec)));
|
|
9014
|
+
const envFile2 = str(ctx.args, "env");
|
|
9015
|
+
if (envFile2) await ui2.spin(`sending ${envFile2} as secrets`, sendDotenv(ctx, envFile2, pod2.id));
|
|
9016
|
+
const text3 = task2 + (bool(ctx.args, "pr") ? '\n\nWhen you are done: commit on a new branch and open a pull request with `airgate pr --title "\u2026"` (it waits for my approval).' : "\n\nWhen you are done: commit your work on a new branch here and say which branch.");
|
|
9017
|
+
const r2 = await work(pod2, text3);
|
|
9018
|
+
await finish(ctx, pod2, r2.done ? "sleep" : "running");
|
|
9019
|
+
if (ctx.json) return ui2.json({ pod: { id: pod2.id, name: name2 }, repo: spec.repo, run: r2.run, ok: r2.ok, log: r2.log, pod_state: r2.done ? "sleeping" : "running", warnings });
|
|
9020
|
+
ctx.io.stdout.write(r2.log.endsWith("\n") || !r2.log ? r2.log : r2.log + "\n");
|
|
9021
|
+
if (!r2.done) ui2.err(ui2.ce("warn", `still working after ${minutes} min; it carries on. follow it: airgate log ${name2}`));
|
|
9022
|
+
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" : ""}`);
|
|
9023
|
+
return r2.ok ? 0 : 1;
|
|
9024
|
+
}
|
|
9025
|
+
const projectDir = resolve5(ctx.io.cwd, dirArg ?? ".");
|
|
9026
|
+
if (!existsSync5(projectDir) || !statSync3(projectDir).isDirectory()) throw new CliError(`no directory ${projectDir}`);
|
|
9027
|
+
const noApply = bool(ctx.args, "no-apply");
|
|
9028
|
+
if (!noApply && !gitTopLevel(projectDir)) {
|
|
9029
|
+
throw new CliError(`${projectDir} isn't a git repo, so the result can't come back as a patch`, "git init && git add -A && git commit -m base, or use --no-apply");
|
|
9030
|
+
}
|
|
9031
|
+
const sp = statePath(projectDir);
|
|
9032
|
+
const before = existsSync5(sp) ? readFileSync7(sp, "utf8") : null;
|
|
9033
|
+
const restoreState = () => {
|
|
9034
|
+
try {
|
|
9035
|
+
if (before !== null) writeFileSync5(sp, before);
|
|
9036
|
+
else rmSync3(sp, { force: true });
|
|
9037
|
+
} catch {
|
|
9038
|
+
}
|
|
9039
|
+
};
|
|
9040
|
+
const name = runName(podDirName(projectDir));
|
|
9041
|
+
const upArgv = [projectDir, "--name", name, "--agent", agent, "--memory", size2, "--no-session", "--yes"];
|
|
9042
|
+
const envFile = str(ctx.args, "env");
|
|
9043
|
+
if (envFile) upArgv.push("--env", envFile);
|
|
9044
|
+
const sent = await ui2.spin(`sending ${basename2(projectDir)} to a fresh pod`, runCommand(ctx, up, upArgv, ctx.io.cwd));
|
|
9045
|
+
const pod = sent.pod;
|
|
9046
|
+
const text2 = `Work in ~/${sent.dir} (a project sent from my laptop). ${task2}
|
|
9047
|
+
|
|
9048
|
+
When you are done, leave your changes in the files there (no need to commit or push): they are brought back to my laptop as a patch.`;
|
|
9049
|
+
let r;
|
|
9050
|
+
try {
|
|
9051
|
+
r = await work(pod, text2);
|
|
9052
|
+
} catch (e) {
|
|
9053
|
+
await finish(ctx, pod, "sleep");
|
|
9054
|
+
throw e;
|
|
9055
|
+
}
|
|
9056
|
+
const keepNote = (why) => `${why}. the pod is kept (asleep): airgate attach ${name} \xB7 bring it back: airgate down --pod ${name} \xB7 delete it: airgate destroy ${name}`;
|
|
9057
|
+
const out = (o) => ui2.json({ pod: { id: pod.id, name }, run: r.run, ok: r.ok, log: r.log, warnings, ...o });
|
|
9058
|
+
if (!r.ok) {
|
|
9059
|
+
await finish(ctx, pod, r.done ? "sleep" : "running");
|
|
9060
|
+
if (ctx.json) return out({ applied: false, pod_state: r.done ? "sleeping" : "running" });
|
|
9061
|
+
ctx.io.stdout.write(r.log.endsWith("\n") || !r.log ? r.log : r.log + "\n");
|
|
9062
|
+
ui2.err(ui2.ce("bad", r.done ? keepNote(`the task failed (exit ${r.run.exitCode ?? "?"})`) : `still working after ${minutes} min; it carries on. follow it: airgate log ${name}, then: airgate down --pod ${name}`));
|
|
9063
|
+
return 1;
|
|
9064
|
+
}
|
|
9065
|
+
let result;
|
|
9066
|
+
try {
|
|
9067
|
+
result = await ui2.spin(`bringing the changes back from ${name}`, runCommand(ctx, down2, ["--pod", name, "--no-session", ...noApply ? ["--dry-run"] : ["--yes"]], projectDir));
|
|
9068
|
+
} catch (e) {
|
|
9069
|
+
await finish(ctx, pod, "sleep");
|
|
9070
|
+
const why = e instanceof Error ? e.message.split("\n")[0] : String(e);
|
|
9071
|
+
if (ctx.json) return out({ applied: false, error: why, pod_state: "sleeping" });
|
|
9072
|
+
ctx.io.stdout.write(r.log.endsWith("\n") || !r.log ? r.log : r.log + "\n");
|
|
9073
|
+
ui2.err(ui2.ce("bad", keepNote(`the task finished, but its changes didn't come back: ${why}`)));
|
|
9074
|
+
return 1;
|
|
9075
|
+
}
|
|
9076
|
+
const files = result.files ?? [];
|
|
9077
|
+
const keep = noApply || bool(ctx.args, "keep");
|
|
9078
|
+
await finish(ctx, pod, keep ? "sleep" : "destroy");
|
|
9079
|
+
if (!keep) restoreState();
|
|
9080
|
+
if (ctx.json) return out({ applied: !noApply && files.length > 0, files, ...noApply ? { patch: result.patch ?? "" } : {}, pod_state: keep ? "sleeping" : "deleted" });
|
|
9081
|
+
ctx.io.stdout.write(r.log.endsWith("\n") || !r.log ? r.log : r.log + "\n");
|
|
9082
|
+
if (noApply) {
|
|
9083
|
+
ctx.io.stdout.write(result.patch ?? "");
|
|
9084
|
+
ui2.err(`${files.length} file(s) changed, not applied. apply: airgate down --pod ${name} \xB7 delete the pod: airgate destroy ${name}`);
|
|
9085
|
+
return 0;
|
|
9086
|
+
}
|
|
9087
|
+
const risky = files.filter((f) => f.risky).length;
|
|
9088
|
+
ui2.out(
|
|
9089
|
+
`${ui2.c("ok", "done")}: ${files.length ? `${files.length} file(s) changed here${risky ? `, ${risky} risky` : ""}. review: git status \xB7 git diff` : "no changes came back"}` + (keep ? ` \xB7 the pod is kept (asleep): ${name}` : " \xB7 the pod is deleted")
|
|
9090
|
+
);
|
|
9091
|
+
return 0;
|
|
9092
|
+
}
|
|
9093
|
+
}
|
|
9094
|
+
};
|
|
9095
|
+
|
|
9096
|
+
// src/commands/mcp.ts
|
|
9097
|
+
var PROTOCOL_VERSION = "2025-06-18";
|
|
9098
|
+
var obj = (properties, required = []) => ({ type: "object", properties, required, additionalProperties: false });
|
|
9099
|
+
var s = (description, extra = {}) => ({ type: "string", description, ...extra });
|
|
9100
|
+
var b = (description) => ({ type: "boolean", description });
|
|
9101
|
+
var POD = s("pod name or id (a unique prefix is enough)");
|
|
9102
|
+
var need = (args, key) => {
|
|
9103
|
+
const v = args[key];
|
|
9104
|
+
if (typeof v !== "string" || !v.trim()) throw new UsageError(`${key} is required`);
|
|
9105
|
+
return v;
|
|
9106
|
+
};
|
|
9107
|
+
var jobSchema = obj(
|
|
8534
9108
|
{
|
|
8535
9109
|
id: s("keep an existing job: its id"),
|
|
8536
9110
|
name: s("a name to show"),
|
|
@@ -8540,6 +9114,9 @@ var jobSchema = obj(
|
|
|
8540
9114
|
},
|
|
8541
9115
|
["cron", "task"]
|
|
8542
9116
|
);
|
|
9117
|
+
var OWN = /* @__PURE__ */ new Map();
|
|
9118
|
+
var n = (description) => ({ type: "number", description });
|
|
9119
|
+
var dirOf = (ctx, a) => typeof a.dir === "string" && a.dir ? a.dir : ctx.io.cwd;
|
|
8543
9120
|
var TOOLS = [
|
|
8544
9121
|
{
|
|
8545
9122
|
name: "pods_list",
|
|
@@ -8589,6 +9166,7 @@ var TOOLS = [
|
|
|
8589
9166
|
}
|
|
8590
9167
|
const spec = specFromFlags({ ...ctx, args: parseArgs(argv, launch.flags ?? {}) });
|
|
8591
9168
|
const pod = await waitReady(ctx, await ctx.api.launch(spec));
|
|
9169
|
+
OWN.set(pod.id, "launch");
|
|
8592
9170
|
return { name: pod.spec.name, id: pod.id, state: pod.state, readyMs: pod.readyMs, spec: pod.spec };
|
|
8593
9171
|
}
|
|
8594
9172
|
},
|
|
@@ -8709,6 +9287,119 @@ var TOOLS = [
|
|
|
8709
9287
|
if (typeof a.pod === "string" && a.pod) argv.push("--pod", a.pod);
|
|
8710
9288
|
return runCommand(ctx, down2, argv, typeof a.dir === "string" && a.dir ? a.dir : ctx.io.cwd);
|
|
8711
9289
|
}
|
|
9290
|
+
},
|
|
9291
|
+
{
|
|
9292
|
+
name: "pod_run",
|
|
9293
|
+
description: "One task in a fresh pod, start to finish: sends the local project (like pod_up, without the conversation), the pod's agent does the task, the changes come back into the local repo as a patch (pod_down's checks), and the pod is deleted. With repo: runs on a GitHub repo instead (private ones need GitHub connected); the work stays on a branch (or a PR with pr, which waits in the airlock) and the pod sleeps. On failure the pod is kept, asleep. Can take many minutes: for long work prefer pod_up or pod_launch + task_start and check back with run_status.",
|
|
9294
|
+
inputSchema: obj(
|
|
9295
|
+
{
|
|
9296
|
+
task: s("what the agent should do"),
|
|
9297
|
+
dir: s("the local project directory (default: where the server started)"),
|
|
9298
|
+
repo: s("run on this GitHub repo (owner/name) instead of the local project"),
|
|
9299
|
+
agent: s("claude-code | codex | opencode | pi (default claude-code)"),
|
|
9300
|
+
apply: b("false: return the patch without applying it (the pod is kept, asleep)"),
|
|
9301
|
+
keep: b("keep the pod afterwards (asleep) instead of deleting it"),
|
|
9302
|
+
pr: b("with repo: open a pull request at the end (it waits in the airlock)"),
|
|
9303
|
+
env: s("a dotenv file whose keys become the pod's secrets, e.g. .env"),
|
|
9304
|
+
timeoutMinutes: n("stop waiting after this many minutes (default 60); the pod keeps working")
|
|
9305
|
+
},
|
|
9306
|
+
["task"]
|
|
9307
|
+
),
|
|
9308
|
+
async call(ctx, a) {
|
|
9309
|
+
const argv = [need(a, "task")];
|
|
9310
|
+
const opt = (flag, v) => typeof v === "string" && v && argv.push(`--${flag}`, v);
|
|
9311
|
+
opt("repo", a.repo);
|
|
9312
|
+
opt("agent", a.agent);
|
|
9313
|
+
opt("env", a.env);
|
|
9314
|
+
if (a.apply === false) argv.push("--no-apply");
|
|
9315
|
+
if (a.keep === true) argv.push("--keep");
|
|
9316
|
+
if (a.pr === true) argv.push("--pr");
|
|
9317
|
+
if (typeof a.timeoutMinutes === "number") argv.push("--timeout", String(a.timeoutMinutes));
|
|
9318
|
+
const out = await runCommand(ctx, runTask, argv, dirOf(ctx, a));
|
|
9319
|
+
if (out.pod?.id && out.pod_state !== "deleted") OWN.set(out.pod.id, a.repo ? "launch" : "project");
|
|
9320
|
+
return out;
|
|
9321
|
+
}
|
|
9322
|
+
},
|
|
9323
|
+
{
|
|
9324
|
+
name: "pod_up",
|
|
9325
|
+
description: "Move the local project into a pod: its files (git-tracked and untracked-but-not-ignored), and by default the latest Claude Code / Codex conversation here, which resumes in the pod. Launches a new pod named after the folder unless pod is given. Bring the work back later with pod_diff / pod_down.",
|
|
9326
|
+
inputSchema: obj({
|
|
9327
|
+
dir: s("the local project directory (default: where the server started)"),
|
|
9328
|
+
pod: s("send into this existing pod instead of launching one"),
|
|
9329
|
+
name: s("name for a new pod"),
|
|
9330
|
+
agent: s("agent for a new pod: claude-code | codex | opencode | pi"),
|
|
9331
|
+
env: s("a dotenv file whose keys become the pod's secrets, e.g. .env"),
|
|
9332
|
+
conversation: b("false: don't send the agent conversation")
|
|
9333
|
+
}),
|
|
9334
|
+
async call(ctx, a) {
|
|
9335
|
+
const argv = ["--yes"];
|
|
9336
|
+
const opt = (flag, v) => typeof v === "string" && v && argv.push(`--${flag}`, v);
|
|
9337
|
+
opt("pod", a.pod);
|
|
9338
|
+
opt("name", a.name);
|
|
9339
|
+
opt("agent", a.agent);
|
|
9340
|
+
opt("env", a.env);
|
|
9341
|
+
if (a.conversation === false) argv.push("--no-session");
|
|
9342
|
+
const out = await runCommand(ctx, up, argv, dirOf(ctx, a));
|
|
9343
|
+
if (!a.pod && out.pod?.id) OWN.set(out.pod.id, "project");
|
|
9344
|
+
return out;
|
|
9345
|
+
}
|
|
9346
|
+
},
|
|
9347
|
+
{
|
|
9348
|
+
name: "pod_sleep",
|
|
9349
|
+
description: "Put a pod to sleep: it stops, keeps its files and checkpoints, and costs nothing until woken. Do this when you are done with a pod you launched.",
|
|
9350
|
+
inputSchema: obj({ pod: POD }, ["pod"]),
|
|
9351
|
+
async call(ctx, a) {
|
|
9352
|
+
const p = await resolvePod(ctx, need(a, "pod"));
|
|
9353
|
+
return ctx.api.stop(p.id);
|
|
9354
|
+
}
|
|
9355
|
+
},
|
|
9356
|
+
{
|
|
9357
|
+
name: "pod_wake",
|
|
9358
|
+
description: "Wake a sleeping pod (seconds).",
|
|
9359
|
+
inputSchema: obj({ pod: POD }, ["pod"]),
|
|
9360
|
+
async call(ctx, a) {
|
|
9361
|
+
const p = await resolvePod(ctx, need(a, "pod"));
|
|
9362
|
+
return ctx.api.resume(p.id);
|
|
9363
|
+
}
|
|
9364
|
+
},
|
|
9365
|
+
{
|
|
9366
|
+
name: "pod_destroy",
|
|
9367
|
+
description: "Delete a pod for good (its files and checkpoints). Allowed on your own for pods you launched in this session (pod_launch, pod_up, pod_run) once their work is safe: a pod holding a local project refuses while it has changes not brought back (pod_down first, or discard_changes); for a pod_launch pod, make sure its work is pushed or not needed. Any other pod: only when the user explicitly asked to delete that pod; then set user_confirmed.",
|
|
9368
|
+
inputSchema: obj({ pod: POD, user_confirmed: b("the user explicitly asked to delete this pod"), discard_changes: b("delete even though it has changes not brought back") }, ["pod"]),
|
|
9369
|
+
async call(ctx, a) {
|
|
9370
|
+
const p = await resolvePod(ctx, need(a, "pod"));
|
|
9371
|
+
if (!OWN.has(p.id) && a.user_confirmed !== true) {
|
|
9372
|
+
throw new CliError(`${p.spec.name} wasn't launched by you in this session: ask the user before deleting it, then call again with user_confirmed`);
|
|
9373
|
+
}
|
|
9374
|
+
if (a.discard_changes !== true && (OWN.get(p.id) ?? "project") === "project") {
|
|
9375
|
+
const pending = await ctx.api.diff(p.id, "up").catch(() => null);
|
|
9376
|
+
if (pending && pending.files.length) throw new CliError(`${p.spec.name} has ${pending.files.length} changed file(s) not brought back: pod_down first, or set discard_changes if the user doesn't want them`);
|
|
9377
|
+
}
|
|
9378
|
+
await ctx.api.destroy(p.id);
|
|
9379
|
+
OWN.delete(p.id);
|
|
9380
|
+
return { deleted: p.spec.name, id: p.id };
|
|
9381
|
+
}
|
|
9382
|
+
},
|
|
9383
|
+
{
|
|
9384
|
+
name: "pod_ports",
|
|
9385
|
+
description: "What is listening in a pod (dev servers on ports 1024+) and its preview links.",
|
|
9386
|
+
inputSchema: obj({ pod: POD }, ["pod"]),
|
|
9387
|
+
async call(ctx, a) {
|
|
9388
|
+
const p = await resolvePod(ctx, need(a, "pod"));
|
|
9389
|
+
return ctx.api.previews(p.id);
|
|
9390
|
+
}
|
|
9391
|
+
},
|
|
9392
|
+
{
|
|
9393
|
+
name: "pod_preview",
|
|
9394
|
+
description: "A link to a port in a pod, for the user to open: private (signed in as them) by default; public makes a link anyone can open (tell the user it is public).",
|
|
9395
|
+
inputSchema: obj({ pod: POD, port: n("the port, 1024+"), public: b("a public link anyone can open") }, ["pod", "port"]),
|
|
9396
|
+
async call(ctx, a) {
|
|
9397
|
+
const p = await resolvePod(ctx, need(a, "pod"));
|
|
9398
|
+
const port = Number(a.port);
|
|
9399
|
+
if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new UsageError("port: 1024\u201365535");
|
|
9400
|
+
const pv = await ctx.api.ensurePreview(p.id, { port, public: a.public === true });
|
|
9401
|
+
return pv.public ? { url: pv.url, public: true } : { url: await ctx.api.openPreview(pv.slug, "/"), public: false, note: "a private link: it signs the user in; give it to them, not to anyone else" };
|
|
9402
|
+
}
|
|
8712
9403
|
}
|
|
8713
9404
|
];
|
|
8714
9405
|
function errorText(e) {
|
|
@@ -8732,7 +9423,7 @@ async function handle(ctx, msg) {
|
|
|
8732
9423
|
protocolVersion: asked,
|
|
8733
9424
|
capabilities: { tools: { listChanged: false } },
|
|
8734
9425
|
serverInfo: { name: "airgate", title: "Airgate", version: VERSION },
|
|
8735
|
-
instructions: "Airgate pods are private cloud computers for coding agents.
|
|
9426
|
+
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."
|
|
8736
9427
|
});
|
|
8737
9428
|
}
|
|
8738
9429
|
case "ping":
|
|
@@ -8774,6 +9465,10 @@ var mcp = {
|
|
|
8774
9465
|
const rl = createInterface({ input: ctx.io.stdin, crlfDelay: Infinity, terminal: false });
|
|
8775
9466
|
const pending = /* @__PURE__ */ new Set();
|
|
8776
9467
|
if (!ctx.api.signedIn) ctx.io.stderr.write("airgate mcp: not signed in; tools will say so. run: airgate login\n");
|
|
9468
|
+
const relayer = relayIfSignedIn(ctx, (l) => ctx.io.stderr.write(`airgate mcp: ${l}
|
|
9469
|
+
`));
|
|
9470
|
+
if (relayer) ctx.io.stderr.write(`airgate mcp: signing pods' ${relayer.vendors.join(" / ")} calls from this laptop while this runs
|
|
9471
|
+
`);
|
|
8777
9472
|
for await (const line2 of rl) {
|
|
8778
9473
|
if (!line2.trim()) continue;
|
|
8779
9474
|
let msg;
|
|
@@ -8791,10 +9486,14 @@ var mcp = {
|
|
|
8791
9486
|
void job2.finally(() => pending.delete(job2));
|
|
8792
9487
|
}
|
|
8793
9488
|
await Promise.all(pending);
|
|
9489
|
+
relayer?.stop();
|
|
8794
9490
|
}
|
|
8795
9491
|
};
|
|
8796
9492
|
|
|
8797
9493
|
// src/commands/misc.ts
|
|
9494
|
+
import { mkdirSync as mkdirSync5, writeFileSync as writeFileSync6 } from "node:fs";
|
|
9495
|
+
import { homedir as homedir3 } from "node:os";
|
|
9496
|
+
import { join as join6 } from "node:path";
|
|
8798
9497
|
var PAGES = ["pods", "airlock", "usage", "settings", "mask", "chat", "notes", "data", "status"];
|
|
8799
9498
|
var open = {
|
|
8800
9499
|
name: "open",
|
|
@@ -8853,6 +9552,106 @@ var version = {
|
|
|
8853
9552
|
ctx.ui.out(`airgate ${VERSION}`);
|
|
8854
9553
|
}
|
|
8855
9554
|
};
|
|
9555
|
+
var skill = {
|
|
9556
|
+
name: "skill",
|
|
9557
|
+
group: "other",
|
|
9558
|
+
usage: "[install [--project] | show]",
|
|
9559
|
+
summary: "teach Claude Code airgate: install the airgate skill (~/.claude/skills, or this project with --project)",
|
|
9560
|
+
example: "airgate skill install",
|
|
9561
|
+
public: true,
|
|
9562
|
+
flags: { project: { type: "boolean", help: "install into ./.claude/skills (shared with the project) instead of ~/.claude/skills" } },
|
|
9563
|
+
async run(ctx) {
|
|
9564
|
+
const action = ctx.args.positionals[0] ?? "show";
|
|
9565
|
+
const url = `${ctx.cfg.app}/skills/airgate/SKILL.md`;
|
|
9566
|
+
const res = await ctx.ui.spin("fetching the airgate skill", ctx.fetch(url));
|
|
9567
|
+
if (!res.ok) throw new CliError(`couldn't fetch ${url} (${res.status})`);
|
|
9568
|
+
const text2 = await res.text();
|
|
9569
|
+
if (!text2.startsWith("---")) throw new CliError(`${url} isn't a skill file`);
|
|
9570
|
+
if (action === "show") return void ctx.io.stdout.write(text2);
|
|
9571
|
+
if (action !== "install") throw new UsageError(`unknown action "${action}"`, "airgate skill install [--project] | airgate skill show");
|
|
9572
|
+
const base = bool(ctx.args, "project") ? join6(ctx.io.cwd, ".claude") : join6(ctx.io.env.HOME ?? homedir3(), ".claude");
|
|
9573
|
+
const dir = join6(base, "skills", "airgate");
|
|
9574
|
+
mkdirSync5(dir, { recursive: true });
|
|
9575
|
+
writeFileSync6(join6(dir, "SKILL.md"), text2);
|
|
9576
|
+
if (ctx.json) return ctx.ui.json({ installed: join6(dir, "SKILL.md") });
|
|
9577
|
+
ctx.ui.out(`${ctx.ui.c("ok", "installed")} the airgate skill: ${join6(dir, "SKILL.md")}`);
|
|
9578
|
+
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".'));
|
|
9579
|
+
}
|
|
9580
|
+
};
|
|
9581
|
+
|
|
9582
|
+
// src/commands/previews.ts
|
|
9583
|
+
var portArg = (v) => {
|
|
9584
|
+
const n2 = Number(v);
|
|
9585
|
+
if (!v || !Number.isInteger(n2) || n2 < 1 || n2 > 65535) throw new UsageError("a port, like 3000");
|
|
9586
|
+
return n2;
|
|
9587
|
+
};
|
|
9588
|
+
async function podAndPort(ctx) {
|
|
9589
|
+
const [ref, port] = ctx.args.positionals;
|
|
9590
|
+
if (!ref) throw new UsageError("which pod?");
|
|
9591
|
+
return { pod: await resolvePod(ctx, ref), port: portArg(port) };
|
|
9592
|
+
}
|
|
9593
|
+
var ports = {
|
|
9594
|
+
name: "ports",
|
|
9595
|
+
aliases: ["previews"],
|
|
9596
|
+
group: "pods",
|
|
9597
|
+
usage: "<pod>",
|
|
9598
|
+
summary: "what's listening in a pod, and its preview links",
|
|
9599
|
+
example: "airgate ports web",
|
|
9600
|
+
async run(ctx) {
|
|
9601
|
+
const ref = ctx.args.positionals[0];
|
|
9602
|
+
if (!ref) throw new UsageError("which pod? airgate ports <pod>");
|
|
9603
|
+
const pod = await resolvePod(ctx, ref);
|
|
9604
|
+
const r = await ctx.api.previews(pod.id);
|
|
9605
|
+
if (ctx.json) return ctx.ui.json(r);
|
|
9606
|
+
if (!r.enabled) return void ctx.ui.out("previews are not set up on this gateway.");
|
|
9607
|
+
const byPort = new Map(r.previews.map((p) => [p.port, p]));
|
|
9608
|
+
const all = [.../* @__PURE__ */ new Set([...r.ports, ...r.previews.map((p) => p.port)])].sort((a, b2) => a - b2);
|
|
9609
|
+
if (!all.length) return void ctx.ui.out(`nothing listening in ${pod.spec.name} yet. start a dev server (any port 1024+), then: airgate preview ${pod.spec.name} <port>`);
|
|
9610
|
+
ctx.ui.table(
|
|
9611
|
+
all.map((port) => {
|
|
9612
|
+
const p = byPort.get(port);
|
|
9613
|
+
return [`:${port}`, r.ports.includes(port) ? ctx.ui.c("ok", "listening") : ctx.ui.c("dim", "not listening"), p ? p.public ? ctx.ui.c("warn", "public") : "private" : ctx.ui.c("dim", "-"), p?.public ? p.url : ""];
|
|
9614
|
+
}),
|
|
9615
|
+
{ head: ["port", "", "preview", "link"] }
|
|
9616
|
+
);
|
|
9617
|
+
ctx.ui.out(ctx.ui.c("dim", `open one: airgate preview ${pod.spec.name} <port> \xB7 share it: airgate share ${pod.spec.name} <port>`));
|
|
9618
|
+
}
|
|
9619
|
+
};
|
|
9620
|
+
var preview = {
|
|
9621
|
+
name: "preview",
|
|
9622
|
+
group: "pods",
|
|
9623
|
+
usage: "<pod> <port> [/path] [--no-open]",
|
|
9624
|
+
summary: "open a port of a pod in your browser (private to you)",
|
|
9625
|
+
example: "airgate preview web 3000",
|
|
9626
|
+
flags: { "no-open": { type: "boolean", help: "print the link instead (it signs you in, once, within a minute)" } },
|
|
9627
|
+
async run(ctx) {
|
|
9628
|
+
const { pod, port } = await podAndPort(ctx);
|
|
9629
|
+
const path = ctx.args.positionals[2] ?? "/";
|
|
9630
|
+
const p = await ctx.api.ensurePreview(pod.id, { port });
|
|
9631
|
+
const url = await ctx.api.openPreview(p.slug, path.startsWith("/") ? path : `/${path}`);
|
|
9632
|
+
if (ctx.json) return ctx.ui.json({ preview: p, open: url });
|
|
9633
|
+
await openOrPrint(ctx, url, `${pod.spec.name}:${port}`);
|
|
9634
|
+
}
|
|
9635
|
+
};
|
|
9636
|
+
var share = {
|
|
9637
|
+
name: "share",
|
|
9638
|
+
group: "pods",
|
|
9639
|
+
usage: "<pod> <port> [--off]",
|
|
9640
|
+
summary: "a public link to a port of a pod (anyone with it can open it); --off makes it private again",
|
|
9641
|
+
example: "airgate share web 3000",
|
|
9642
|
+
flags: { off: { type: "boolean", help: "stop sharing: private to you again" } },
|
|
9643
|
+
async run(ctx) {
|
|
9644
|
+
const { pod, port } = await podAndPort(ctx);
|
|
9645
|
+
const off = bool(ctx.args, "off");
|
|
9646
|
+
const p = await ctx.api.ensurePreview(pod.id, { port, public: !off });
|
|
9647
|
+
if (ctx.json) return ctx.ui.json(p);
|
|
9648
|
+
if (off) ctx.ui.out(`${pod.spec.name}:${port} is private again: only you can open it (airgate preview ${pod.spec.name} ${port}).`);
|
|
9649
|
+
else {
|
|
9650
|
+
ctx.ui.out(p.url);
|
|
9651
|
+
ctx.ui.err(ctx.ui.ce("warn", `public: anyone with this link reaches ${pod.spec.name}:${port}. stop with: airgate share ${pod.spec.name} ${port} --off`));
|
|
9652
|
+
}
|
|
9653
|
+
}
|
|
9654
|
+
};
|
|
8856
9655
|
|
|
8857
9656
|
// src/commands/undo.ts
|
|
8858
9657
|
var newestFirst = (cps) => [...cps].sort((a, b2) => b2.at - a.at);
|
|
@@ -9033,15 +9832,15 @@ var Canvas = class {
|
|
|
9033
9832
|
/** write text at (x, y), clipped to maxWidth and the canvas; returns columns written */
|
|
9034
9833
|
text(x, y, s2, st = {}, maxWidth = Infinity) {
|
|
9035
9834
|
if (y < 0 || y >= this.rows) return 0;
|
|
9036
|
-
let
|
|
9835
|
+
let n2 = 0;
|
|
9037
9836
|
for (const ch of s2.replace(/[\u0000-\u001f\u007f]/g, " ")) {
|
|
9038
|
-
if (
|
|
9039
|
-
const cx = x +
|
|
9837
|
+
if (n2 >= maxWidth) break;
|
|
9838
|
+
const cx = x + n2;
|
|
9040
9839
|
if (cx >= this.cols) break;
|
|
9041
9840
|
if (cx >= 0) this.cells[y][cx] = { ch, st };
|
|
9042
|
-
|
|
9841
|
+
n2++;
|
|
9043
9842
|
}
|
|
9044
|
-
return
|
|
9843
|
+
return n2;
|
|
9045
9844
|
}
|
|
9046
9845
|
fill(x, y, w, h, st = {}) {
|
|
9047
9846
|
for (let r = y; r < y + h; r++) for (let c = x; c < x + w; c++) if (r >= 0 && r < this.rows && c >= 0 && c < this.cols) this.cells[r][c] = { ch: " ", st };
|
|
@@ -9246,7 +10045,7 @@ function formKey(s2, form, key) {
|
|
|
9246
10045
|
}
|
|
9247
10046
|
return { state: { ...s2, modal: { kind: "form", form: next } }, effects: [] };
|
|
9248
10047
|
};
|
|
9249
|
-
const cycle2 = (
|
|
10048
|
+
const cycle2 = (n2, len, d2) => (n2 + d2 + len) % len;
|
|
9250
10049
|
switch (key) {
|
|
9251
10050
|
case "tab":
|
|
9252
10051
|
case "down":
|
|
@@ -9338,10 +10137,10 @@ function modalKey(s2, m, key) {
|
|
|
9338
10137
|
return none;
|
|
9339
10138
|
}
|
|
9340
10139
|
case "timeline": {
|
|
9341
|
-
const
|
|
10140
|
+
const n2 = m.checkpoints?.length ?? 0;
|
|
9342
10141
|
if (key === "esc" || key === "q" || key === "u") return close;
|
|
9343
|
-
if ((key === "down" || key === "j") &&
|
|
9344
|
-
if ((key === "up" || key === "k") &&
|
|
10142
|
+
if ((key === "down" || key === "j") && n2) return { state: { ...s2, modal: { ...m, sel: Math.min(n2 - 1, m.sel + 1) } }, effects: [] };
|
|
10143
|
+
if ((key === "up" || key === "k") && n2) return { state: { ...s2, modal: { ...m, sel: Math.max(0, m.sel - 1) } }, effects: [] };
|
|
9345
10144
|
const cp = m.checkpoints?.[m.sel];
|
|
9346
10145
|
if (key === "enter" && cp) {
|
|
9347
10146
|
return {
|
|
@@ -9649,16 +10448,16 @@ function draw(s2, cols, rows2) {
|
|
|
9649
10448
|
}
|
|
9650
10449
|
function header(c, s2) {
|
|
9651
10450
|
let x = c.text(0, 0, " \u22A0 airgate", { bold: true }) + 2;
|
|
9652
|
-
const
|
|
10451
|
+
const n2 = pendingApprovals(s2).length;
|
|
9653
10452
|
const tabs = [
|
|
9654
10453
|
["pods", "pods"],
|
|
9655
|
-
["airlock",
|
|
10454
|
+
["airlock", n2 ? `airlock [${n2}]` : "airlock"],
|
|
9656
10455
|
["usage", "usage"]
|
|
9657
10456
|
];
|
|
9658
10457
|
for (const [id, label2] of tabs) {
|
|
9659
10458
|
if (x + label2.length + 2 > c.cols) break;
|
|
9660
10459
|
const active = s2.screen === id;
|
|
9661
|
-
x += c.text(x, 0, ` ${label2} `, { inverse: active, bold: active, fg: id === "airlock" &&
|
|
10460
|
+
x += c.text(x, 0, ` ${label2} `, { inverse: active, bold: active, fg: id === "airlock" && n2 && !active ? "warn" : void 0 });
|
|
9662
10461
|
x += 1;
|
|
9663
10462
|
}
|
|
9664
10463
|
const right = s2.connected ? "\u25CF live" : s2.loaded ? "\u25CB offline" : "\u25CB connecting";
|
|
@@ -9704,9 +10503,9 @@ function splash(c, s2) {
|
|
|
9704
10503
|
}
|
|
9705
10504
|
function statusLine(c, s2) {
|
|
9706
10505
|
const y = c.rows - 1;
|
|
9707
|
-
const
|
|
10506
|
+
const n2 = pendingApprovals(s2).length;
|
|
9708
10507
|
const parts = [];
|
|
9709
|
-
if (
|
|
10508
|
+
if (n2) parts.push(`airlock [${n2}]`);
|
|
9710
10509
|
if (s2.me) parts.push(s2.me.plan === "none" ? "no plan" : s2.me.plan, `${hrs(s2.me.usedHours)} this month`);
|
|
9711
10510
|
const right = parts.join(" \xB7 ");
|
|
9712
10511
|
let left;
|
|
@@ -9725,8 +10524,8 @@ function statusLine(c, s2) {
|
|
|
9725
10524
|
c.text(0, y, " " + clip(left, Math.max(0, c.cols - rw - 2)), { fg: leftFg });
|
|
9726
10525
|
if (right && c.cols - rw > 10) {
|
|
9727
10526
|
const x = c.cols - rw;
|
|
9728
|
-
if (
|
|
9729
|
-
const badge = `airlock [${
|
|
10527
|
+
if (n2) {
|
|
10528
|
+
const badge = `airlock [${n2}]`;
|
|
9730
10529
|
c.text(x, y, badge, { fg: "warn", bold: true });
|
|
9731
10530
|
c.text(x + badge.length, y, right.slice(badge.length), { fg: "dim" });
|
|
9732
10531
|
} else c.text(x, y, right, { fg: "dim" });
|
|
@@ -10029,11 +10828,11 @@ function modal(c, s2, m) {
|
|
|
10029
10828
|
return;
|
|
10030
10829
|
}
|
|
10031
10830
|
case "timeline": {
|
|
10032
|
-
const
|
|
10033
|
-
const b2 = centered(c, 64, Math.max(6, Math.min(
|
|
10831
|
+
const n2 = m.checkpoints?.length ?? 0;
|
|
10832
|
+
const b2 = centered(c, 64, Math.max(6, Math.min(n2, 14) + 4));
|
|
10034
10833
|
c.box(b2.x, b2.y, b2.w, b2.h, { double: true, title: `undo \xB7 ${m.name}`, clear: true });
|
|
10035
10834
|
if (!m.checkpoints) c.text(b2.x + 2, b2.y + 1, "loading\u2026", { fg: "dim" });
|
|
10036
|
-
else if (!
|
|
10835
|
+
else if (!n2) c.text(b2.x + 2, b2.y + 1, "no checkpoints yet. c takes one.", { fg: "dim" }, b2.w - 4);
|
|
10037
10836
|
const rowsAvail = b2.h - 3;
|
|
10038
10837
|
const start = Math.max(0, m.sel - rowsAvail + 1);
|
|
10039
10838
|
(m.checkpoints ?? []).slice(start, start + rowsAvail).forEach((cp, i) => {
|
|
@@ -10080,7 +10879,7 @@ function formModal(c, s2, f) {
|
|
|
10080
10879
|
const t = TEMPLATES[formTemplate(f)];
|
|
10081
10880
|
const builtin = formBuiltin(f);
|
|
10082
10881
|
const plan2 = s2.me?.plan === "pro" ? "pro" : "payg";
|
|
10083
|
-
const
|
|
10882
|
+
const models2 = s2.catalog?.models ?? [];
|
|
10084
10883
|
fields.forEach((id, i) => {
|
|
10085
10884
|
const y = b2.y + 1 + i;
|
|
10086
10885
|
if (y >= b2.y + b2.h - 3) return;
|
|
@@ -10118,7 +10917,7 @@ function formModal(c, s2, f) {
|
|
|
10118
10917
|
v = `[${f.builtin ? "x" : " "}] built-in model, no subscription`;
|
|
10119
10918
|
break;
|
|
10120
10919
|
case "model":
|
|
10121
|
-
v = `\u2039 ${
|
|
10920
|
+
v = `\u2039 ${models2[f.model]?.id ?? `default${s2.catalog?.defaultModel ? ` (${s2.catalog.defaultModel})` : ""}`} \u203A`;
|
|
10122
10921
|
break;
|
|
10123
10922
|
case "schedule": {
|
|
10124
10923
|
const p = SCHEDULE_LIST[f.schedule];
|
|
@@ -10236,7 +11035,7 @@ function startTui(d) {
|
|
|
10236
11035
|
scheduleDraw();
|
|
10237
11036
|
}
|
|
10238
11037
|
};
|
|
10239
|
-
const onSignal = () =>
|
|
11038
|
+
const onSignal = () => finish2(130);
|
|
10240
11039
|
const onCrash = (e) => crash(e);
|
|
10241
11040
|
function listenKeys(on) {
|
|
10242
11041
|
if (on) {
|
|
@@ -10256,7 +11055,7 @@ function startTui(d) {
|
|
|
10256
11055
|
d.stdin.pause();
|
|
10257
11056
|
d.stdout.write(LEAVE_ALT);
|
|
10258
11057
|
}
|
|
10259
|
-
function
|
|
11058
|
+
function finish2(code3, message) {
|
|
10260
11059
|
if (finished) return;
|
|
10261
11060
|
finished = true;
|
|
10262
11061
|
controller.abort();
|
|
@@ -10271,7 +11070,7 @@ function startTui(d) {
|
|
|
10271
11070
|
resolveDone(code3);
|
|
10272
11071
|
}
|
|
10273
11072
|
function crash(e) {
|
|
10274
|
-
|
|
11073
|
+
finish2(1, `airgate ui crashed: ${e instanceof Error ? e.message : String(e)}`);
|
|
10275
11074
|
}
|
|
10276
11075
|
const errText = (e) => {
|
|
10277
11076
|
if (e instanceof ApiError) {
|
|
@@ -10300,7 +11099,7 @@ function startTui(d) {
|
|
|
10300
11099
|
try {
|
|
10301
11100
|
switch (e.type) {
|
|
10302
11101
|
case "quit":
|
|
10303
|
-
return
|
|
11102
|
+
return finish2(0);
|
|
10304
11103
|
case "attach":
|
|
10305
11104
|
return await handoff(e.podId, e.name);
|
|
10306
11105
|
case "loadUsage": {
|
|
@@ -10538,6 +11337,9 @@ var COMMANDS = [
|
|
|
10538
11337
|
task,
|
|
10539
11338
|
runs,
|
|
10540
11339
|
log,
|
|
11340
|
+
ports,
|
|
11341
|
+
preview,
|
|
11342
|
+
share,
|
|
10541
11343
|
checkpoints,
|
|
10542
11344
|
checkpoint,
|
|
10543
11345
|
rewind,
|
|
@@ -10548,6 +11350,9 @@ var COMMANDS = [
|
|
|
10548
11350
|
mask,
|
|
10549
11351
|
up,
|
|
10550
11352
|
down2,
|
|
11353
|
+
runTask,
|
|
11354
|
+
models,
|
|
11355
|
+
relay,
|
|
10551
11356
|
usage,
|
|
10552
11357
|
plan,
|
|
10553
11358
|
upgrade,
|
|
@@ -10556,6 +11361,7 @@ var COMMANDS = [
|
|
|
10556
11361
|
ui,
|
|
10557
11362
|
mcp,
|
|
10558
11363
|
open,
|
|
11364
|
+
skill,
|
|
10559
11365
|
status,
|
|
10560
11366
|
help,
|
|
10561
11367
|
version
|
|
@@ -10598,7 +11404,7 @@ function printCommandHelp(ui2, c) {
|
|
|
10598
11404
|
ui2.out(`usage: airgate ${c.name}${c.usage ? " " + c.usage : ""}`);
|
|
10599
11405
|
ui2.out();
|
|
10600
11406
|
ui2.out(c.summary + ".");
|
|
10601
|
-
for (const
|
|
11407
|
+
for (const n2 of c.notes ?? []) ui2.out(n2);
|
|
10602
11408
|
const flags = flagLines({ ...c.flags ?? {}, json: GLOBAL_FLAGS.json });
|
|
10603
11409
|
ui2.out();
|
|
10604
11410
|
for (const l of formatTable(flags, void 0, 2)) ui2.out(l);
|
|
@@ -10733,7 +11539,7 @@ async function run(argv, io, opts2 = {}) {
|
|
|
10733
11539
|
}
|
|
10734
11540
|
}
|
|
10735
11541
|
function suggest2(name) {
|
|
10736
|
-
const near = COMMANDS.filter((c) => [c.name, ...c.aliases ?? []].some((
|
|
11542
|
+
const near = COMMANDS.filter((c) => [c.name, ...c.aliases ?? []].some((n2) => n2.startsWith(name.slice(0, 2)))).map((c) => c.name);
|
|
10737
11543
|
return near.length ? `did you mean: ${near.slice(0, 3).join(", ")}? (airgate help)` : "see: airgate help";
|
|
10738
11544
|
}
|
|
10739
11545
|
|