airgate 0.1.0 → 0.1.2

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.
Files changed (3) hide show
  1. package/README.md +14 -2
  2. package/dist/airgate.mjs +985 -475
  3. 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 n = 2; ; n++) if (!used.has(`${base}-${n}`)) return `${base}-${n}`;
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,6 +505,9 @@ 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
  }
@@ -448,6 +520,19 @@ var Api = class {
448
520
  setSecret(name, value, podId) {
449
521
  return this.request("PUT", `/v1/secrets/${enc(name)}`, { body: { value, ...podId ? { podId } : {} } });
450
522
  }
523
+ previews(id) {
524
+ return this.request("GET", `/v1/pods/${enc(id)}/previews`);
525
+ }
526
+ ensurePreview(id, body) {
527
+ return this.request("POST", `/v1/pods/${enc(id)}/previews`, { body }).then((r) => r.preview);
528
+ }
529
+ openPreview(slug, path = "/") {
530
+ return this.request("POST", `/v1/previews/${enc(slug)}/open`, { body: { path } }).then((r) => r.url);
531
+ }
532
+ /** many at once (a .env file), saved together or not at all */
533
+ setSecrets(values, podId) {
534
+ return this.request("PUT", "/v1/secrets", { body: { values, ...podId ? { podId } : {} } });
535
+ }
451
536
  deleteSecret(name, podId) {
452
537
  return this.request("DELETE", `/v1/secrets/${enc(name)}${podId ? `?podId=${enc(podId)}` : ""}`);
453
538
  }
@@ -1015,10 +1100,10 @@ function drawable(text2, font = "block") {
1015
1100
  return [...String(text2)].filter((ch) => [...f.cased ? ch : ch.toUpperCase()].some((c) => Object.hasOwn(f.glyphs, c))).join("");
1016
1101
  }
1017
1102
  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, n) {
1103
+ function fade(stops, n2) {
1019
1104
  const rgb2 = stops.map((s2) => [1, 3, 5].map((i) => parseInt(s2.slice(i, i + 2), 16)));
1020
- return Array.from({ length: n }, (_, i) => {
1021
- const k = n > 1 ? i / (n - 1) * (rgb2.length - 1) : 0;
1105
+ return Array.from({ length: n2 }, (_, i) => {
1106
+ const k = n2 > 1 ? i / (n2 - 1) * (rgb2.length - 1) : 0;
1022
1107
  const a = Math.min(rgb2.length - 2, Math.floor(k)), f = k - a;
1023
1108
  const c = rgb2.length === 1 ? rgb2[0] : rgb2[a].map((v, j) => Math.round(v + (rgb2[a + 1][j] - v) * f));
1024
1109
  return `#${c.map((v) => v.toString(16).padStart(2, "0")).join("")}`;
@@ -1446,14 +1531,14 @@ function merge(parts, extra = [], plain = false) {
1446
1531
  };
1447
1532
  const idx = lists.map((p) => (p ?? []).map(add));
1448
1533
  const inked = lists.some((p) => !p) ? { light: add(INK2.light), dark: add(INK2.dark) } : null;
1449
- const n = all.length;
1534
+ const n2 = all.length;
1450
1535
  const values = all.map(rgb);
1451
1536
  const into = all.map((_, i) => i);
1452
- const alive = new Uint8Array(n).fill(1);
1453
- const near = new Int32Array(n), gap = new Float64Array(n);
1537
+ const alive = new Uint8Array(n2).fill(1);
1538
+ const near = new Int32Array(n2), gap = new Float64Array(n2);
1454
1539
  const look = (i) => {
1455
1540
  let best = Infinity, at4 = -1;
1456
- for (let j = 0; j < n; j++)
1541
+ for (let j = 0; j < n2; j++)
1457
1542
  if (j !== i && alive[j]) {
1458
1543
  const d = apart(values[i], values[j]);
1459
1544
  if (d < best)
@@ -1462,19 +1547,19 @@ function merge(parts, extra = [], plain = false) {
1462
1547
  near[i] = at4;
1463
1548
  gap[i] = best;
1464
1549
  };
1465
- for (let i = 0; i < n; i++)
1550
+ for (let i = 0; i < n2; i++)
1466
1551
  look(i);
1467
- for (let left = n; left > 64; left--) {
1552
+ for (let left = n2; left > 64; left--) {
1468
1553
  let a = -1;
1469
- for (let i = 0; i < n; i++)
1554
+ for (let i = 0; i < n2; i++)
1470
1555
  if (alive[i] && (a < 0 || gap[i] < gap[a]))
1471
1556
  a = i;
1472
1557
  const keep = Math.min(a, near[a]), drop = Math.max(a, near[a]);
1473
1558
  alive[drop] = 0;
1474
- for (let i = 0; i < n; i++)
1559
+ for (let i = 0; i < n2; i++)
1475
1560
  if (into[i] === drop)
1476
1561
  into[i] = keep;
1477
- for (let i = 0; i < n; i++)
1562
+ for (let i = 0; i < n2; i++)
1478
1563
  if (alive[i] && near[i] === drop)
1479
1564
  look(i);
1480
1565
  }
@@ -1679,8 +1764,8 @@ function knockout(s2, g, x, y, h, mask2) {
1679
1764
  }
1680
1765
  }
1681
1766
  var at = (v, t) => {
1682
- const n = typeof v === "function" ? v(t) : v;
1683
- return finite(n) ? Math.floor(n) : 0;
1767
+ const n2 = typeof v === "function" ? v(t) : v;
1768
+ return finite(n2) ? Math.floor(n2) : 0;
1684
1769
  };
1685
1770
  var around = (p, size2, room) => {
1686
1771
  const span = room + size2;
@@ -1870,19 +1955,19 @@ function crop(src, region) {
1870
1955
  fail2(`crop's region, ${cols} by ${rows2} at ${x}, ${y}, misses ${p.meta.name}, which is ${p.cols} by ${p.rows}`);
1871
1956
  return reshape("crop", p, x1 - x0, y1 - y0, (g, s2) => s2.paste(g, -x0, -y0));
1872
1957
  }
1873
- function pad(src, n) {
1958
+ function pad(src, n2) {
1874
1959
  const p = prepare("pad", src);
1875
1960
  let sides;
1876
- if (typeof n === "number")
1877
- sides = Array(4).fill(whole2(n, "pad", 0, 0));
1878
- else if (Array.isArray(n) && n.length === 2) {
1879
- const [r, c] = [whole2(n[0], "pad's rows", 0, 0), whole2(n[1], "pad's columns", 0, 0)];
1961
+ if (typeof n2 === "number")
1962
+ sides = Array(4).fill(whole2(n2, "pad", 0, 0));
1963
+ else if (Array.isArray(n2) && n2.length === 2) {
1964
+ const [r, c] = [whole2(n2[0], "pad's rows", 0, 0), whole2(n2[1], "pad's columns", 0, 0)];
1880
1965
  sides = [r, c, r, c];
1881
- } else if (n !== null && typeof n === "object" && !Array.isArray(n)) {
1882
- const o = n;
1966
+ } else if (n2 !== null && typeof n2 === "object" && !Array.isArray(n2)) {
1967
+ const o = n2;
1883
1968
  sides = ["top", "right", "bottom", "left"].map((k) => whole2(o[k], `pad's ${k}`, 0, 0));
1884
1969
  } else
1885
- return fail2(`pad takes a number, [rows, columns] or { top, right, bottom, left }, not ${shown(n)}`);
1970
+ return fail2(`pad takes a number, [rows, columns] or { top, right, bottom, left }, not ${shown(n2)}`);
1886
1971
  const [top, right, bottom, left] = sides;
1887
1972
  return reshape("pad", p, p.cols + left + right, p.rows + top + bottom, (g, s2) => s2.paste(g, left, top));
1888
1973
  }
@@ -2069,7 +2154,7 @@ function optionsOf2(o, fx, keys) {
2069
2154
  fail2(`${fx}() has no option ${JSON.stringify(k)}${RENAMED[`${fx}.${k}`] ? "" : suggest(k, known)}: ${RENAMED[`${fx}.${k}`] ?? `it takes ${or2(known)}`}`);
2070
2155
  return o;
2071
2156
  }
2072
- var mod = (a, n) => (a % n + n) % n;
2157
+ var mod = (a, n2) => (a % n2 + n2) % n2;
2073
2158
  var gcd2 = (a, b2) => b2 ? gcd2(b2, a % b2) : a;
2074
2159
  function lcm(a, b2) {
2075
2160
  const x = Math.round(a * 100), y = Math.round(b2 * 100);
@@ -2079,14 +2164,14 @@ function lcm(a, b2) {
2079
2164
  return k * y <= 6e3 ? +(k * b2).toFixed(6) : void 0;
2080
2165
  }
2081
2166
  function quiet(t, busy, span) {
2082
- const n = 480;
2167
+ const n2 = 480;
2083
2168
  let from = -1;
2084
- for (let k = 0; k <= n; k++) {
2085
- const u = t + span * k / n;
2169
+ for (let k = 0; k <= n2; k++) {
2170
+ const u = t + span * k / n2;
2086
2171
  if (from < 0 && !busy(u))
2087
2172
  from = u;
2088
2173
  else if (from >= 0 && busy(u))
2089
- return +((from + u) / 2 - span / n / 2).toFixed(3);
2174
+ return +((from + u) / 2 - span / n2 / 2).toFixed(3);
2090
2175
  }
2091
2176
  return from < 0 ? t : +((from + t + span) / 2).toFixed(3);
2092
2177
  }
@@ -2152,7 +2237,7 @@ function mainInk(p, at3, options, map) {
2152
2237
  const most = (paper) => {
2153
2238
  const { text: text2, color } = snapshot(p, at3, { paper, options });
2154
2239
  const count = /* @__PURE__ */ new Map();
2155
- let best = 0, n = 0;
2240
+ let best = 0, n2 = 0;
2156
2241
  for (let k = 0, i = 0; k < text2.length; k++) {
2157
2242
  const c = text2.charCodeAt(k);
2158
2243
  if (c === 10)
@@ -2160,8 +2245,8 @@ function mainInk(p, at3, options, map) {
2160
2245
  if (c !== 32 && color && i < color.length) {
2161
2246
  const v = (count.get(color[i]) ?? 0) + 1;
2162
2247
  count.set(color[i], v);
2163
- if (v > n)
2164
- n = v, best = color[i];
2248
+ if (v > n2)
2249
+ n2 = v, best = color[i];
2165
2250
  }
2166
2251
  i++;
2167
2252
  }
@@ -2380,14 +2465,14 @@ function typeIn(src, options) {
2380
2465
  what: "typed in",
2381
2466
  setup: () => (t, s2, g) => {
2382
2467
  const local = cycle2 ? mod(t, cycle2) : t;
2383
- const n = Math.floor((local - start) * speed2 + 1e-6);
2468
+ const n2 = Math.floor((local - start) * speed2 + 1e-6);
2384
2469
  const all = g.chars.length;
2385
2470
  let shown2 = 0, next = -1, last = -1;
2386
2471
  for (let k = 0; k < all; k++) {
2387
2472
  const i = seq ? seq[k] : k;
2388
2473
  if (g.chars[i] === EMPTY)
2389
2474
  continue;
2390
- if (shown2 >= n) {
2475
+ if (shown2 >= n2) {
2391
2476
  next = i;
2392
2477
  break;
2393
2478
  }
@@ -2432,13 +2517,13 @@ function dissolve(src, options) {
2432
2517
  const edge = o.edge === "" ? "" : chars2(o.edge, "dissolve.edge", ".:", 1);
2433
2518
  const seed = num(o.seed, "dissolve.seed", 1, "whole");
2434
2519
  return build2("dissolve", src, o, ({ meta: m }) => {
2435
- const n = m.cols * m.rows, aspect = m.cell ?? 2;
2436
- const v = new Float64Array(n);
2520
+ const n2 = m.cols * m.rows, aspect = m.cell ?? 2;
2521
+ const v = new Float64Array(n2);
2437
2522
  for (let y = 0, i = 0; y < m.rows; y++)
2438
2523
  for (let x = 0; x < m.cols; x++, i++)
2439
2524
  v[i] = 0.8 * valueNoise(x * scale2, y * scale2 * aspect, seed) + 0.2 * hash(x, y, seed, 3);
2440
- const at3 = new Float32Array(n);
2441
- Int32Array.from({ length: n }, (_, i) => i).sort((a, b2) => v[a] - v[b2]).forEach((i, rank) => at3[i] = (rank + 0.5) / n);
2525
+ const at3 = new Float32Array(n2);
2526
+ Int32Array.from({ length: n2 }, (_, i) => i).sort((a, b2) => v[a] - v[b2]).forEach((i, rank) => at3[i] = (rank + 0.5) / n2);
2442
2527
  const band = edge ? 0.12 : 0;
2443
2528
  const codes = [...edge].map((c) => c.charCodeAt(0)), blocks = auto ? [9617, 9618] : codes;
2444
2529
  return {
@@ -2451,7 +2536,7 @@ function dissolve(src, options) {
2451
2536
  if (k >= 1)
2452
2537
  return copy(s2, g);
2453
2538
  const front = k * (1 + band);
2454
- for (let i = 0; i < n; i++) {
2539
+ for (let i = 0; i < n2; i++) {
2455
2540
  const ch = g.chars[i];
2456
2541
  if (ch === EMPTY)
2457
2542
  continue;
@@ -2625,7 +2710,7 @@ function wave(src, options) {
2625
2710
  const lines = rows2 ? h : w;
2626
2711
  for (let k = 0; k < lines; k++) {
2627
2712
  const off = room + Math.round(amplitude * Math.sin(TAU * (k / wavelength - t / period2)));
2628
- for (let j = 0, n = rows2 ? w : h; j < n; j++) {
2713
+ for (let j = 0, n2 = rows2 ? w : h; j < n2; j++) {
2629
2714
  const i = rows2 ? k * w + j : j * w + k;
2630
2715
  if (g.chars[i] === EMPTY)
2631
2716
  continue;
@@ -2637,8 +2722,8 @@ function wave(src, options) {
2637
2722
  }
2638
2723
  function hsl(h, s2, l) {
2639
2724
  const a = s2 * Math.min(l, 1 - l);
2640
- const f = (n) => {
2641
- const k = (n + h / 30) % 12;
2725
+ const f = (n2) => {
2726
+ const k = (n2 + h / 30) % 12;
2642
2727
  return Math.round(255 * (l - a * Math.max(-1, Math.min(k - 3, 9 - k, 1))));
2643
2728
  };
2644
2729
  return "#" + [f(0), f(8), f(4)].map((v) => v.toString(16).padStart(2, "0")).join("");
@@ -2647,7 +2732,7 @@ var hues = {
2647
2732
  light: Array.from({ length: 12 }, (_, i) => hsl(i * 30, 0.8, 0.4)),
2648
2733
  dark: Array.from({ length: 12 }, (_, i) => hsl(i * 30, 0.9, 0.66))
2649
2734
  };
2650
- var cycle = (stops, n) => stops.length === 1 ? Array(n).fill(stops[0]) : gradient([...stops, stops[0]], n + 1).slice(0, n);
2735
+ var cycle = (stops, n2) => stops.length === 1 ? Array(n2).fill(stops[0]) : gradient([...stops, stops[0]], n2 + 1).slice(0, n2);
2651
2736
  function rainbowOf(fx, src, options, cyclesBy) {
2652
2737
  const o = optionsOf2(options, fx, ["colors", "steps", "period", "cycles", "direction"]);
2653
2738
  const steps = num(o.steps, `${fx}.steps`, 12, "steps");
@@ -2919,14 +3004,14 @@ var mix = (a, b2, k) => {
2919
3004
  const x = rgb(a), y = rgb(b2);
2920
3005
  return hex(x.map((v, i) => lerp(v, y[i], clamp(k))));
2921
3006
  };
2922
- function gradient(stops, n) {
3007
+ function gradient(stops, n2) {
2923
3008
  if (!Array.isArray(stops) || !stops.length)
2924
3009
  fail2("a gradient takes one or more colours as #rrggbb");
2925
- if (!Number.isInteger(n) || n < 1)
2926
- fail2(`a gradient takes a whole number of colours of 1 or more, not ${String(n)}`);
3010
+ if (!Number.isInteger(n2) || n2 < 1)
3011
+ fail2(`a gradient takes a whole number of colours of 1 or more, not ${String(n2)}`);
2927
3012
  const c = stops.map(rgb);
2928
- return Array.from({ length: n }, (_, i) => {
2929
- const k = n > 1 ? i / (n - 1) * (c.length - 1) : 0;
3013
+ return Array.from({ length: n2 }, (_, i) => {
3014
+ const k = n2 > 1 ? i / (n2 - 1) * (c.length - 1) : 0;
2930
3015
  const a = Math.min(c.length - 2, Math.floor(k));
2931
3016
  return c.length === 1 ? hex(c[0]) : hex(c[a].map((v, j) => lerp(v, c[a + 1][j], k - a)));
2932
3017
  });
@@ -3162,7 +3247,7 @@ function colorOf(v, what = "color") {
3162
3247
  function didYouMean(word) {
3163
3248
  const all = [.../* @__PURE__ */ new Set([...Object.keys(schemes), ...Object.keys(materialColors)])];
3164
3249
  const near = nearWords(word, all);
3165
- return near.length ? `did you mean ${near.map((n) => JSON.stringify(n)).join(" or ")}?` : `a colour of your own is #rrggbb, and the names are ${and2(all)}`;
3250
+ 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
3251
  }
3167
3252
  function lettersApart(a, b2) {
3168
3253
  const d = Array.from({ length: b2.length + 1 }, (_, j) => j);
@@ -3230,14 +3315,14 @@ var SYNONYMS = {
3230
3315
  function nearWords(word, words2) {
3231
3316
  const w = word.toLowerCase().slice(0, 40);
3232
3317
  const same = (SYNONYMS[w] ?? []).filter((s2) => words2.includes(s2) && s2 !== word);
3233
- const close = words2.filter((n) => !same.includes(n) && lettersApart(w, n.toLowerCase()) <= (w.length <= 4 ? 1 : 2));
3318
+ const close = words2.filter((n2) => !same.includes(n2) && lettersApart(w, n2.toLowerCase()) <= (w.length <= 4 ? 1 : 2));
3234
3319
  return [...same, ...close].slice(0, 3);
3235
3320
  }
3236
3321
  function suggest(word, words2) {
3237
3322
  if (typeof word !== "string")
3238
3323
  return "";
3239
3324
  const near = nearWords(word, words2);
3240
- return near.length ? ` (did you mean ${near.map((n) => JSON.stringify(n)).join(" or ")}?)` : "";
3325
+ return near.length ? ` (did you mean ${near.map((n2) => JSON.stringify(n2)).join(" or ")}?)` : "";
3241
3326
  }
3242
3327
  function mergePalettes(parts) {
3243
3328
  const palette = [];
@@ -3426,8 +3511,8 @@ var Surface = class _Surface {
3426
3511
  const color = env.color;
3427
3512
  if (color && this.palette) {
3428
3513
  const ink = this.palette.ink(this.paper);
3429
- const n = Math.min(color.length, chars3.length);
3430
- for (let i = 0; i < n; i++)
3514
+ const n2 = Math.min(color.length, chars3.length);
3515
+ for (let i = 0; i < n2; i++)
3431
3516
  color[i] = colors[i] === NONE ? ink : colors[i];
3432
3517
  }
3433
3518
  const line2 = this.#line;
@@ -3515,8 +3600,8 @@ var Chainable = class {
3515
3600
  freeze(at3) {
3516
3601
  return freeze(this, at3);
3517
3602
  }
3518
- pad(n) {
3519
- return pad(this, n);
3603
+ pad(n2) {
3604
+ return pad(this, n2);
3520
3605
  }
3521
3606
  border(o) {
3522
3607
  return border(this, o);
@@ -3631,11 +3716,11 @@ function asPiece(src, name = "text") {
3631
3716
  const meta3 = checkMeta({ name, category: "type", note: name, cols: grid2.cols, rows: grid2.rows, fps: 0, ...palette ? { palette: palette.colors } : {} });
3632
3717
  const other = grid2.clone();
3633
3718
  if (palette?.themed) {
3634
- const n = palette.size;
3719
+ const n2 = palette.size;
3635
3720
  for (let i = 0; i < other.colors.length; i++) {
3636
3721
  const c = other.colors[i];
3637
3722
  if (c !== NONE)
3638
- other.colors[i] = grid2.paper ? c < n ? c + n : c : c >= n ? c - n : c;
3723
+ other.colors[i] = grid2.paper ? c < n2 ? c + n2 : c : c >= n2 ? c - n2 : c;
3639
3724
  }
3640
3725
  }
3641
3726
  other.paper = !grid2.paper;
@@ -3651,7 +3736,7 @@ function asPiece(src, name = "text") {
3651
3736
  lines.shift();
3652
3737
  while (lines.length > 1 && !lines[lines.length - 1].trim())
3653
3738
  lines.pop();
3654
- const indent = lines.reduce((n, l) => l.trim() ? Math.min(n, l.length - l.trimStart().length) : n, Infinity);
3739
+ const indent = lines.reduce((n2, l) => l.trim() ? Math.min(n2, l.length - l.trimStart().length) : n2, Infinity);
3655
3740
  if (Number.isFinite(indent))
3656
3741
  lines = lines.map((l) => l.slice(indent));
3657
3742
  }
@@ -3695,9 +3780,9 @@ function snapshot(src, t = 0, { paper = false, mono = false, options = {} } = {}
3695
3780
  // ../../node_modules/ascii.rest/dist/kit/math.js
3696
3781
  var said = (v) => typeof v === "string" ? JSON.stringify(v) : Array.isArray(v) ? `[${v.map(said).join(", ")}]` : String(v);
3697
3782
  var finite2 = (v) => typeof v === "number" && Number.isFinite(v);
3698
- function mod2(a, n) {
3699
- const r = a % n;
3700
- return r < 0 ? r + n < n ? r + n : 0 : r + 0;
3783
+ function mod2(a, n2) {
3784
+ const r = a % n2;
3785
+ return r < 0 ? r + n2 < n2 ? r + n2 : 0 : r + 0;
3701
3786
  }
3702
3787
  var tables = [];
3703
3788
  function table(seed) {
@@ -3862,7 +3947,7 @@ var H3 = 1 / 6;
3862
3947
  var F4 = (Math.sqrt(5) - 1) / 4;
3863
3948
  var H4 = (5 - Math.sqrt(5)) / 20;
3864
3949
  var S4 = 27;
3865
- var unit = (n) => n <= -1 ? 0 : n >= 1 ? 1 : 0.5 + 0.5 * n;
3950
+ var unit = (n2) => n2 <= -1 ? 0 : n2 >= 1 ? 1 : 0.5 + 0.5 * n2;
3866
3951
  var dot4 = (g, a, b2, c, d) => G4[g] * a + G4[g + 1] * b2 + G4[g + 2] * c + G4[g + 3] * d;
3867
3952
  function simplex4(x, y, z, w, seed) {
3868
3953
  const { perm } = table(seed);
@@ -3905,26 +3990,26 @@ function simplex4(x, y, z, w, seed) {
3905
3990
  const x3 = x0 - i3 + 3 * H4, y3 = y0 - j3 + 3 * H4, z3 = z0 - l3 + 3 * H4, w3 = w0 - m3 + 3 * H4;
3906
3991
  const x4 = x0 - 1 + 4 * H4, y4 = y0 - 1 + 4 * H4, z4 = z0 - 1 + 4 * H4, w4 = w0 - 1 + 4 * H4;
3907
3992
  const ii = i & 255, jj = j & 255, ll = l & 255, mm = m & 255;
3908
- let n = 0, k;
3993
+ let n2 = 0, k;
3909
3994
  if ((k = 0.6 - x0 * x0 - y0 * y0 - z0 * z0 - w0 * w0) > 0)
3910
- k *= k, n += k * k * dot4((perm[ii + perm[jj + perm[ll + perm[mm]]]] & 31) * 4, x0, y0, z0, w0);
3995
+ k *= k, n2 += k * k * dot4((perm[ii + perm[jj + perm[ll + perm[mm]]]] & 31) * 4, x0, y0, z0, w0);
3911
3996
  if ((k = 0.6 - x1 * x1 - y1 * y1 - z1 * z1 - w1 * w1) > 0)
3912
- k *= k, n += k * k * dot4((perm[ii + i1 + perm[jj + j1 + perm[ll + l1 + perm[mm + m1]]]] & 31) * 4, x1, y1, z1, w1);
3997
+ 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
3998
  if ((k = 0.6 - x2 * x2 - y2 * y2 - z2 * z2 - w2 * w2) > 0)
3914
- k *= k, n += k * k * dot4((perm[ii + i2 + perm[jj + j2 + perm[ll + l2 + perm[mm + m2]]]] & 31) * 4, x2, y2, z2, w2);
3999
+ 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
4000
  if ((k = 0.6 - x3 * x3 - y3 * y3 - z3 * z3 - w3 * w3) > 0)
3916
- k *= k, n += k * k * dot4((perm[ii + i3 + perm[jj + j3 + perm[ll + l3 + perm[mm + m3]]]] & 31) * 4, x3, y3, z3, w3);
4001
+ 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
4002
  if ((k = 0.6 - x4 * x4 - y4 * y4 - z4 * z4 - w4 * w4) > 0)
3918
- k *= k, n += k * k * dot4((perm[ii + 1 + perm[jj + 1 + perm[ll + 1 + perm[mm + 1]]]] & 31) * 4, x4, y4, z4, w4);
3919
- return S4 * n;
4003
+ k *= k, n2 += k * k * dot4((perm[ii + 1 + perm[jj + 1 + perm[ll + 1 + perm[mm + 1]]]] & 31) * 4, x4, y4, z4, w4);
4004
+ return S4 * n2;
3920
4005
  }
3921
4006
  var OFF = Array.from({ length: 16 }, (_, i) => [i * 0.6180339887 % 1 * 97.3, i * 0.7548776662 % 1 * 89.1]);
3922
4007
  var octN = 4;
3923
4008
  var octLac = 2;
3924
4009
  var octGain = 0.5;
3925
4010
  var octSeed = 0;
3926
- function octaves(o, n = 4) {
3927
- octN = o?.octaves ?? n;
4011
+ function octaves(o, n2 = 4) {
4012
+ octN = o?.octaves ?? n2;
3928
4013
  octLac = o?.lacunarity ?? 2;
3929
4014
  octGain = o?.gain ?? 0.5;
3930
4015
  octSeed = o?.seed ?? 0;
@@ -3939,8 +4024,8 @@ function octaves(o, n = 4) {
3939
4024
  }
3940
4025
  var SPREAD = 1.45;
3941
4026
  var WIDEN4 = 1.35;
3942
- var crest = (n) => {
3943
- const r = 1 - (n < 0 ? -n : n);
4027
+ var crest = (n2) => {
4028
+ const r = 1 - (n2 < 0 ? -n2 : n2);
3944
4029
  return r < 0 ? 0 : r * r;
3945
4030
  };
3946
4031
  var weigh = (r) => r > 0.5 ? 1 : 2 * r;
@@ -3955,10 +4040,10 @@ function loopNoise(x, y, t, period2, o) {
3955
4040
  const z = radius * Math.cos(a), w = radius * Math.sin(a);
3956
4041
  let sum = 0, amp = 1, norm = 0, f = 1, weight = 1;
3957
4042
  for (let i = 0; i < octN; i++) {
3958
- let n = simplex4(x * f + OFF[i][0], y * f + OFF[i][1], z * f, w * f, octSeed) * WIDEN4;
4043
+ let n2 = simplex4(x * f + OFF[i][0], y * f + OFF[i][1], z * f, w * f, octSeed) * WIDEN4;
3959
4044
  if (ridge)
3960
- n = crest(n) * weight, weight = weigh(n);
3961
- sum += amp * n;
4045
+ n2 = crest(n2) * weight, weight = weigh(n2);
4046
+ sum += amp * n2;
3962
4047
  norm += amp;
3963
4048
  amp *= octGain;
3964
4049
  f *= octLac;
@@ -3971,14 +4056,14 @@ var B3 = B1 + 1;
3971
4056
  var E4 = TAU / 3;
3972
4057
  var E5 = TAU / 4.5;
3973
4058
  var bounce = (k) => {
3974
- const n = 7.5625, d = 2.75;
4059
+ const n2 = 7.5625, d = 2.75;
3975
4060
  if (k < 1 / d)
3976
- return n * k * k;
4061
+ return n2 * k * k;
3977
4062
  if (k < 2 / d)
3978
- return n * (k -= 1.5 / d) * k + 0.75;
4063
+ return n2 * (k -= 1.5 / d) * k + 0.75;
3979
4064
  if (k < 2.5 / d)
3980
- return n * (k -= 2.25 / d) * k + 0.9375;
3981
- return n * (k -= 2.625 / d) * k + 0.984375;
4065
+ return n2 * (k -= 2.25 / d) * k + 0.9375;
4066
+ return n2 * (k -= 2.625 / d) * k + 0.984375;
3982
4067
  };
3983
4068
  var curves = {
3984
4069
  linear: (k) => k,
@@ -4149,7 +4234,7 @@ function brush(ch, what) {
4149
4234
  fail2(`${what} takes one character, or "" to clear, not ${show3(ch)}`);
4150
4235
  return code(ch);
4151
4236
  }
4152
- var finite3 = (...v) => v.every((n) => typeof n === "number" && Number.isFinite(n));
4237
+ var finite3 = (...v) => v.every((n2) => typeof n2 === "number" && Number.isFinite(n2));
4153
4238
  function show3(v, depth = 0) {
4154
4239
  if (typeof v === "string")
4155
4240
  return JSON.stringify(v.length > 40 ? v.slice(0, 40) + "\u2026" : v);
@@ -4159,7 +4244,7 @@ function show3(v, depth = 0) {
4159
4244
  return String(v);
4160
4245
  if (depth > 1)
4161
4246
  return Array.isArray(v) ? "[...]" : "{...}";
4162
- const more = (n) => n > 6 ? ", ..." : "";
4247
+ const more = (n2) => n2 > 6 ? ", ..." : "";
4163
4248
  if (Array.isArray(v))
4164
4249
  return `[${Array.from(v.slice(0, 6), (x) => show3(x, depth + 1)).join(", ")}${more(v.length)}]`;
4165
4250
  const entries = Object.entries(v);
@@ -4219,12 +4304,12 @@ function trace(x0, y0, x1, y1, w, h) {
4219
4304
  const dx = Math.abs(x1 - x0), dy = -Math.abs(y1 - y0), sx = x0 < x1 ? 1 : -1, sy = y0 < y1 ? 1 : -1;
4220
4305
  if (XS.length < dx - dy + 1)
4221
4306
  XS = new Int32Array(dx - dy + 1), YS = new Int32Array(dx - dy + 1);
4222
- let err = dx + dy, n = 0;
4307
+ let err = dx + dy, n2 = 0;
4223
4308
  for (; ; ) {
4224
- XS[n] = x0;
4225
- YS[n++] = y0;
4309
+ XS[n2] = x0;
4310
+ YS[n2++] = y0;
4226
4311
  if (x0 === x1 && y0 === y1)
4227
- return n;
4312
+ return n2;
4228
4313
  const e2 = 2 * err;
4229
4314
  if (e2 >= dy)
4230
4315
  err += dy, x0 += sx;
@@ -4261,11 +4346,11 @@ function glyph(a, round, weight) {
4261
4346
  return ROUND.get(a);
4262
4347
  let c = GLYPH.get(a);
4263
4348
  if (c === void 0) {
4264
- let n = 0;
4349
+ let n2 = 0;
4265
4350
  for (let k = 0; k < 8; k += 2)
4266
4351
  if (a >> k & 3)
4267
- n |= weight << k;
4268
- c = GLYPH.get(n);
4352
+ n2 |= weight << k;
4353
+ c = GLYPH.get(n2);
4269
4354
  }
4270
4355
  return c ?? DOT;
4271
4356
  }
@@ -4283,14 +4368,14 @@ function linked(s2, j, b2, ascii) {
4283
4368
  const nx = x + (k === AR ? 1 : k === AL ? -1 : 0), ny = y + (k === AD ? 1 : k === AU ? -1 : 0);
4284
4369
  if (nx < 0 || ny < 0 || nx >= s2.cols || ny >= s2.rows)
4285
4370
  continue;
4286
- const n = s2.chars[ny * s2.cols + nx], nb = ascii ? asciiArms(n) : ARMS.get(n);
4371
+ const n2 = s2.chars[ny * s2.cols + nx], nb = ascii ? asciiArms(n2) : ARMS.get(n2);
4287
4372
  if (nb !== void 0 && nb >> (k ^ 4) & 3)
4288
4373
  m |= b2 & 3 << k;
4289
4374
  }
4290
4375
  return m;
4291
4376
  }
4292
- function place3(s2, j, c, k, join5, a = ARMS.get(c), ascii = false, round = false) {
4293
- if (join5 && a !== void 0) {
4377
+ function place3(s2, j, c, k, join6, a = ARMS.get(c), ascii = false, round = false) {
4378
+ if (join6 && a !== void 0) {
4294
4379
  const old = s2.chars[j], b2 = ascii ? asciiArms(old) : ARMS.get(old);
4295
4380
  if (b2 !== void 0) {
4296
4381
  const kept = linked(s2, j, b2, ascii);
@@ -4408,8 +4493,8 @@ function write(s2, x, y, str2, o, region) {
4408
4493
  let lines = layout(t, bw, wrap2);
4409
4494
  if (lines.length > bh)
4410
4495
  lines = lines.slice(0, bh);
4411
- const n = lines.length;
4412
- const top = valign === "top" ? fy : valign === "middle" ? fy + Math.floor((bh - n) / 2) : fy + bh - n;
4496
+ const n2 = lines.length;
4497
+ const top = valign === "top" ? fy : valign === "middle" ? fy + Math.floor((bh - n2) / 2) : fy + bh - n2;
4413
4498
  const lefts = lines.map((l) => align === "left" ? fx : align === "center" ? fx + Math.floor((bw - l.length) / 2) : fx + bw - l.length);
4414
4499
  return writeLines(s2, lines, lefts, top, k, transparent);
4415
4500
  }
@@ -4422,7 +4507,7 @@ function label(s2, x, y, str2, o) {
4422
4507
  if (!finite3(x, y) || t === "")
4423
4508
  return { x: finite3(x) ? Math.floor(x) : 0, y: finite3(y) ? Math.floor(y) : 0, cols: 0, rows: 0 };
4424
4509
  const lines = t.split("\n");
4425
- const start = (v, n) => Math.floor(v - n / 2 + 0.5);
4510
+ const start = (v, n2) => Math.floor(v - n2 / 2 + 0.5);
4426
4511
  return writeLines(s2, lines, lines.map((l) => start(x, l.length)), start(y, lines.length), k, transparent);
4427
4512
  }
4428
4513
  function around2(on, cx, cy, r, turn) {
@@ -4456,7 +4541,7 @@ function rect(s2, x, y, w, h, o) {
4456
4541
  if (title)
4457
4542
  for (const ch of title)
4458
4543
  code(ch);
4459
- const join5 = yes(o?.join, "join", true);
4544
+ const join6 = yes(o?.join, "join", true);
4460
4545
  const k = paint(s2, color), fk = fillColor === void 0 ? k : paint(s2, fillColor);
4461
4546
  if (!finite3(x, y, w, h))
4462
4547
  return { x: 0, y: 0, cols: 0, rows: 0 };
@@ -4479,10 +4564,10 @@ function rect(s2, x, y, w, h, o) {
4479
4564
  const put = (cx, cy, e, reach = 255) => {
4480
4565
  const j = s2.index(cx, cy), a = ascii ? EDGE_ARMS[e] : ARMS.get(ed[e]);
4481
4566
  if (j >= 0)
4482
- place3(s2, j, ed[e], k, join5, a === void 0 ? a : a & reach, ascii, round);
4567
+ place3(s2, j, ed[e], k, join6, a === void 0 ? a : a & reach, ascii, round);
4483
4568
  };
4484
- const end = (i, n, first, last) => n < 2 ? 255 : i === 0 ? 3 << first : i === n - 1 ? 3 << last : 255;
4485
- const from = (o2, lo) => Math.max(lo, -o2), to = (o2, hi, n) => Math.min(hi, n - o2);
4569
+ const end = (i, n2, first, last) => n2 < 2 ? 255 : i === 0 ? 3 << first : i === n2 - 1 ? 3 << last : 255;
4570
+ const from = (o2, lo) => Math.max(lo, -o2), to = (o2, hi, n2) => Math.min(hi, n2 - o2);
4486
4571
  if (h === 1)
4487
4572
  for (let i = from(x, 0); i < to(x, w, s2.cols); i++)
4488
4573
  put(x + i, y, 4, end(i, w, AR, AL));
@@ -4530,11 +4615,11 @@ function ellipseCells(w, h, cx, cy, rx, ry, inside, visit, aspect = 0, from = 0,
4530
4615
  const j0 = Math.max(0, Math.floor(cy - ry) - 1), j1 = Math.min(h - 1, Math.ceil(cy + ry));
4531
4616
  if (i0 > i1 || j0 > j1)
4532
4617
  return;
4533
- const bw = i1 - i0 + 1, n = bw * (j1 - j0 + 1);
4534
- if (EDGE.length < n)
4535
- EDGE = new Uint8Array(n);
4618
+ const bw = i1 - i0 + 1, n2 = bw * (j1 - j0 + 1);
4619
+ if (EDGE.length < n2)
4620
+ EDGE = new Uint8Array(n2);
4536
4621
  const edge = EDGE;
4537
- edge.fill(0, 0, n);
4622
+ edge.fill(0, 0, n2);
4538
4623
  const mark = (i2, j2, kind) => {
4539
4624
  if (i2 >= i0 && i2 <= i1 && j2 >= j0 && j2 <= j1)
4540
4625
  edge[(j2 - j0) * bw + i2 - i0] |= kind;
@@ -4558,7 +4643,7 @@ function ellipseCells(w, h, cx, cy, rx, ry, inside, visit, aspect = 0, from = 0,
4558
4643
  mark(Math.floor(cx - xo), j2, steep ? 2 : 8);
4559
4644
  mark(Math.ceil(cx + xo) - 1, j2, steep ? 2 : 8);
4560
4645
  }
4561
- for (let k = 0; k < n; k++)
4646
+ for (let k = 0; k < n2; k++)
4562
4647
  edge[k] = edge[k] & 3 | ((edge[k] & 12) === 12 ? 1 : 0);
4563
4648
  for (let j2 = j0; j2 <= j1; j2++)
4564
4649
  for (let i2 = i0; i2 <= i1; i2++) {
@@ -4615,11 +4700,11 @@ function shape(s2, cx, cy, rx, ry, o, what, from = 0, span = 1) {
4615
4700
  const ch = char === "auto" ? "auto" : brush(char, `${/^[aeiou]/.test(what) ? "an" : "a"} ${what}'s char`);
4616
4701
  const f = fill === void 0 || fill === false ? null : brush(fill, "fill");
4617
4702
  const k = paint(s2, color), fk = fillColor === void 0 ? k : paint(s2, fillColor);
4618
- const join5 = yes(o?.join, "join", true);
4703
+ const join6 = yes(o?.join, "join", true);
4619
4704
  ellipseCells(s2.cols, s2.rows, cx, cy, rx, ry, f !== null, (x, y, edge, slope) => {
4620
4705
  const j = y * s2.cols + x;
4621
4706
  if (edge)
4622
- place3(s2, j, ch === "auto" ? slope : ch, k, join5 && ch !== "auto");
4707
+ place3(s2, j, ch === "auto" ? slope : ch, k, join6 && ch !== "auto");
4623
4708
  else
4624
4709
  s2.put(j, f, f === EMPTY ? NONE : fk);
4625
4710
  }, ch === "auto" ? s2.aspect : 0, from, span);
@@ -4700,8 +4785,8 @@ var BrailleCanvas = class {
4700
4785
  }
4701
4786
  line(x0, y0, x1, y1, color) {
4702
4787
  const k = this.#k(color);
4703
- const n = trace(x0, y0, x1, y1, this.width, this.height);
4704
- for (let i = 0; i < n; i++)
4788
+ const n2 = trace(x0, y0, x1, y1, this.width, this.height);
4789
+ for (let i = 0; i < n2; i++)
4705
4790
  this.#dot(XS[i], YS[i], k);
4706
4791
  }
4707
4792
  ray(x, y, length, turn, color) {
@@ -4750,12 +4835,12 @@ var BrailleCanvas = class {
4750
4835
  y1 ??= 1;
4751
4836
  if (y0 === y1)
4752
4837
  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, n = list2 ? list2.length : w;
4838
+ const w = this.width, h = this.height, n2 = list2 ? list2.length : w;
4754
4839
  const top = fill ? new Int32Array(w).fill(h) : null;
4755
4840
  let px = 0, py = NaN;
4756
- for (let i = 0; i < n; i++) {
4841
+ for (let i = 0; i < n2; i++) {
4757
4842
  const v = list2 ? list2[i] : f(x0 + (x1 - x0) * i / Math.max(1, w - 1));
4758
- const x = list2 ? n > 1 ? Math.round(i * (w - 1) / (n - 1)) : 0 : i;
4843
+ const x = list2 ? n2 > 1 ? Math.round(i * (w - 1) / (n2 - 1)) : 0 : i;
4759
4844
  const y = typeof v === "number" && Number.isFinite(v) ? Math.round((y1 - v) / (y1 - y0) * (h - 1)) : NaN;
4760
4845
  if (Number.isFinite(y)) {
4761
4846
  const m = Number.isFinite(py) ? trace(px, py, x, y, w, h) : trace(x, y, x, y, w, h);
@@ -4854,7 +4939,7 @@ function tones(accent, more = []) {
4854
4939
  var WIDGET = ["color", ...NAMING];
4855
4940
  var naming2 = (o, name, note) => ({ name: o.name ?? name, note: (o.note ?? note).slice(0, 72) });
4856
4941
  var framing = (p, framed) => framed ? Object.assign(p, { frames: true }) : p;
4857
- function amountOf(what, v, min, max, share) {
4942
+ function amountOf(what, v, min, max, share2) {
4858
4943
  if (v === void 0)
4859
4944
  return void 0;
4860
4945
  if (typeof v === "function") {
@@ -4870,10 +4955,10 @@ function amountOf(what, v, min, max, share) {
4870
4955
  return min + +m[1] / 100 * (max - min);
4871
4956
  }
4872
4957
  if (typeof v !== "number" || !Number.isFinite(v) || v < min || v > max) {
4873
- const hint = share && typeof v === "number" && v > 1 && v <= 100 ? `: did you mean ${+(v / 100).toFixed(4)}, or "${v}%"?` : "";
4958
+ const hint = share2 && typeof v === "number" && v > 1 && v <= 100 ? `: did you mean ${+(v / 100).toFixed(4)}, or "${v}%"?` : "";
4874
4959
  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
4960
  }
4876
- if (!share && min === 0 && max === 100 && v > 0 && v < 1)
4961
+ if (!share2 && min === 0 && max === 100 && v > 0 && v < 1)
4877
4962
  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
4963
  return v;
4879
4964
  }
@@ -4894,24 +4979,24 @@ function progressBar(o) {
4894
4979
  const smooth = ease.inOutSine;
4895
4980
  const loop = loopOf(fill + 1, true);
4896
4981
  const cols = label2.length + width + (percent ? 5 : 0);
4897
- const share = (v) => (v - min) / (max - min);
4982
+ const share2 = (v) => (v - min) / (max - min);
4898
4983
  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(share(value) * 100)}%`;
4984
+ const note = live ? "a progress bar, live" : value === void 0 ? "a progress bar filling" : `a progress bar at ${Math.round(share2(value) * 100)}%`;
4900
4985
  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" ? share(value()) : value !== void 0 ? share(value) : smooth(Math.min(1, (t % loop + loop) % loop / fill));
4986
+ const k = typeof value === "function" ? share2(value()) : value !== void 0 ? share2(value) : smooth(Math.min(1, (t % loop + loop) % loop / fill));
4902
4987
  const color = k >= 1 ? GOOD : ACCENT;
4903
4988
  s2.write(0, 0, label2);
4904
4989
  const x = label2.length;
4905
4990
  if (style === "ascii") {
4906
- const n = Math.round(k * (width - 2));
4991
+ const n2 = Math.round(k * (width - 2));
4907
4992
  s2.write(x, 0, "[", MUTED);
4908
4993
  for (let i = 0; i < width - 2; i++)
4909
- s2.set(x + 1 + i, 0, i < n ? "#" : "-", i < n ? color : MUTED);
4994
+ s2.set(x + 1 + i, 0, i < n2 ? "#" : "-", i < n2 ? color : MUTED);
4910
4995
  s2.write(x + width - 1, 0, "]", MUTED);
4911
4996
  } else if (style === "dots") {
4912
- const n = Math.round(k * width);
4997
+ const n2 = Math.round(k * width);
4913
4998
  for (let i = 0; i < width; i++)
4914
- s2.set(x + i, 0, i < n ? "\u25CF" : "\u25CB", i < n ? color : MUTED);
4999
+ s2.set(x + i, 0, i < n2 ? "\u25CF" : "\u25CB", i < n2 ? color : MUTED);
4915
5000
  } else {
4916
5001
  const eighths = Math.round(k * width * 8);
4917
5002
  for (let i = 0; i < width; i++) {
@@ -4957,7 +5042,7 @@ function sparkline(data, o) {
4957
5042
  [data, o] = [void 0, data];
4958
5043
  }
4959
5044
  const p = optionsOf3("sparkline()", o, ["label", "width", "height", "fill", ...WIDGET]);
4960
- const numbers = (v) => Array.isArray(v) && v.length >= 2 && v.every((n) => typeof n === "number" && Number.isFinite(n));
5045
+ const numbers = (v) => Array.isArray(v) && v.length >= 2 && v.every((n2) => typeof n2 === "number" && Number.isFinite(n2));
4961
5046
  const live = typeof data === "function" ? data : null;
4962
5047
  if (data !== void 0 && !live && !numbers(data))
4963
5048
  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 +5196,7 @@ var meta2 = {
5111
5196
  };
5112
5197
  var QUAD = " \u2597\u2596\u2584\u259D\u2590\u259E\u259F\u2598\u259A\u258C\u2599\u2580\u259C\u259B\u2588";
5113
5198
  var quad = (ul, ur, ll, lr) => QUAD[ul << 3 | ur << 2 | ll << 1 | lr];
5114
- var ping = (n, i) => i % (2 * n - 2) < n ? i % (2 * n - 2) : 2 * n - 2 - i % (2 * n - 2);
5199
+ var ping = (n2, i) => i % (2 * n2 - 2) < n2 ? i % (2 * n2 - 2) : 2 * n2 - 2 - i % (2 * n2 - 2);
5115
5200
  var blank = (w, h) => Array.from({ length: h }, () => new Array(w).fill(" "));
5116
5201
  var rows = (g) => g.map((r) => r.join(""));
5117
5202
  var plot = (w, h, cells) => {
@@ -5143,8 +5228,8 @@ var SPINNERS = [
5143
5228
  }],
5144
5229
  ["arc", 1, (i) => {
5145
5230
  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 n = 3 + Math.round(ping(8, i) * 4 / 7);
5147
- return plot(5, 3, [...Array(n)].map((_, k) => ring2[(i + 12 - k) % 12]));
5231
+ const n2 = 3 + Math.round(ping(8, i) * 4 / 7);
5232
+ return plot(5, 3, [...Array(n2)].map((_, k) => ring2[(i + 12 - k) % 12]));
5148
5233
  }],
5149
5234
  ["bounce", 1, (i) => ["[" + " ".repeat(ping(6, i)) + "o" + " ".repeat(5 - ping(6, i)) + "]"]],
5150
5235
  ["clock", 3, (i) => {
@@ -5176,7 +5261,7 @@ var SPINNERS = [
5176
5261
  }]
5177
5262
  ];
5178
5263
  function spinners2({ names = meta2.options.names, labels = meta2.options.labels, speed: speed2 = meta2.options.speed } = {}) {
5179
- const chosen = names && names.length ? names.map((n) => SPINNERS.find((s2) => s2[0] === n)).filter(Boolean) : SPINNERS;
5264
+ const chosen = names && names.length ? names.map((n2) => SPINNERS.find((s2) => s2[0] === n2)).filter(Boolean) : SPINNERS;
5180
5265
  const all = chosen === SPINNERS;
5181
5266
  const slot2 = 11, per = all ? 6 : Math.min(6, chosen.length);
5182
5267
  const block = labels || all ? 5 : 3;
@@ -5184,8 +5269,8 @@ function spinners2({ names = meta2.options.names, labels = meta2.options.labels,
5184
5269
  const height = all ? meta2.rows : Math.ceil(chosen.length / per) * (block + 1) + 1;
5185
5270
  return (t) => {
5186
5271
  const grid2 = Array.from({ length: height }, () => new Array(cols).fill(" "));
5187
- chosen.forEach(([name, frames, draw2], n) => {
5188
- const x0 = n % per * slot2, y0 = 1 + Math.floor(n / per) * (block + 1);
5272
+ chosen.forEach(([name, frames, draw2], n2) => {
5273
+ const x0 = n2 % per * slot2, y0 = 1 + Math.floor(n2 / per) * (block + 1);
5189
5274
  const art = draw2(Math.floor(t * meta2.fps * speed2 / frames + 1e-3));
5190
5275
  const top = y0 + Math.floor((3 - art.length) / 2);
5191
5276
  art.forEach((line2, j) => {
@@ -5293,8 +5378,8 @@ function cached(cache, key, make) {
5293
5378
  return v;
5294
5379
  }
5295
5380
  var bars = /* @__PURE__ */ new Map();
5296
- function bar(share, width, opts2 = {}) {
5297
- const value = Math.max(0, Math.min(1, Number.isFinite(share) ? share : 0));
5381
+ function bar(share2, width, opts2 = {}) {
5382
+ const value = Math.max(0, Math.min(1, Number.isFinite(share2) ? share2 : 0));
5298
5383
  const w = Math.max(4, Math.round(width));
5299
5384
  const key = `${w}|${opts2.label ?? ""}|${opts2.percent !== false}|${value.toFixed(4)}`;
5300
5385
  const b2 = cached(bars, key, () => {
@@ -5330,12 +5415,12 @@ function dial(value, max, opts2 = {}) {
5330
5415
  return frameOf(g.piece, g.frame, 0);
5331
5416
  }
5332
5417
  var DIAL_SIZE = { cols: 25, rows: 12 };
5333
- function divider(n, label2) {
5334
- if (n <= 0) return "";
5335
- if (!label2) return "\u2500".repeat(n);
5418
+ function divider(n2, label2) {
5419
+ if (n2 <= 0) return "";
5420
+ if (!label2) return "\u2500".repeat(n2);
5336
5421
  const l = ` ${label2} `;
5337
- const side = Math.max(2, Math.floor((n - l.length) / 2));
5338
- return [..."\u2500".repeat(side) + l + "\u2500".repeat(n)].slice(0, n).join("");
5422
+ const side = Math.max(2, Math.floor((n2 - l.length) / 2));
5423
+ return [..."\u2500".repeat(side) + l + "\u2500".repeat(n2)].slice(0, n2).join("");
5339
5424
  }
5340
5425
 
5341
5426
  // src/args.ts
@@ -5526,9 +5611,9 @@ async function resolvePod(ctx, ref) {
5526
5611
  }
5527
5612
  function resolveCheckpoint(cps, ref) {
5528
5613
  if (/^#\d+$/.test(ref)) {
5529
- const n = Number(ref.slice(1));
5614
+ const n2 = Number(ref.slice(1));
5530
5615
  const sorted = [...cps].sort((a, b2) => b2.at - a.at);
5531
- const cp = sorted[n - 1];
5616
+ const cp = sorted[n2 - 1];
5532
5617
  if (!cp) throw new CliError(`no checkpoint ${ref}`, "list them with: airgate checkpoints <pod>");
5533
5618
  return cp;
5534
5619
  }
@@ -5656,13 +5741,13 @@ function duration2(seconds) {
5656
5741
  function hours(h) {
5657
5742
  return `${Math.round(h * 100) / 100}h`;
5658
5743
  }
5659
- function usd(n) {
5660
- return `$${n.toFixed(2)}`;
5744
+ function usd(n2) {
5745
+ return `$${n2.toFixed(2)}`;
5661
5746
  }
5662
- function bytes(n) {
5663
- if (n < 1024) return `${n} B`;
5664
- if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KiB`;
5665
- return `${(n / 1024 / 1024).toFixed(1)} MiB`;
5747
+ function bytes(n2) {
5748
+ if (n2 < 1024) return `${n2} B`;
5749
+ if (n2 < 1024 * 1024) return `${(n2 / 1024).toFixed(1)} KiB`;
5750
+ return `${(n2 / 1024 / 1024).toFixed(1)} MiB`;
5666
5751
  }
5667
5752
  function date(ts) {
5668
5753
  if (!ts) return "-";
@@ -6132,8 +6217,8 @@ var US_PHONE_RE = /(?<![\w+.-])(?:1[-. ]?)?(?:\([2-9]\d{2}\)[ ]?|[2-9]\d{2}[-. ]
6132
6217
  var INTL_PHONE_RE = /(?<![\w+])\+\d{1,3}(?:[ .-]?\(?\d{1,5}\)?){1,6}(?![\w-]|\.\d)/g;
6133
6218
  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
6219
  var phoneDigitsOk = (min) => (s2) => {
6135
- const n = digitsOf(s2).length;
6136
- return n >= min && n <= 15;
6220
+ const n2 = digitsOf(s2).length;
6221
+ return n2 >= min && n2 <= 15;
6137
6222
  };
6138
6223
  var SSN_RE = /(?<![\d-])(?!000|666|9\d\d)\d{3}-(?!00)\d{2}-(?!0000)\d{4}(?![\d-])/g;
6139
6224
  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 +6494,7 @@ var DETECTORS = [
6409
6494
  ];
6410
6495
 
6411
6496
  // ../mask/src/index.ts
6412
- var defaultPlaceholder = (kind, n) => `[${kind}_${n}]`;
6497
+ var defaultPlaceholder = (kind, n2) => `[${kind}_${n2}]`;
6413
6498
  var escapeRe = (s2) => s2.replace(/[.*+?^${}()|[\]\\/]/g, "\\$&");
6414
6499
  function vaultKey(kind, value) {
6415
6500
  switch (kind) {
@@ -6456,9 +6541,9 @@ function createMasker(options = {}) {
6456
6541
  const key = vaultKey(kind, value);
6457
6542
  const existing = byKey.get(key);
6458
6543
  if (existing) return existing.placeholder;
6459
- const n = (counters.get(kind) ?? 0) + 1;
6460
- counters.set(kind, n);
6461
- const entry = { placeholder: makePlaceholder(kind, n), kind, original: value };
6544
+ const n2 = (counters.get(kind) ?? 0) + 1;
6545
+ counters.set(kind, n2);
6546
+ const entry = { placeholder: makePlaceholder(kind, n2), kind, original: value };
6462
6547
  byKey.set(key, entry);
6463
6548
  byLoose.set(looseKey(coreOf(entry.placeholder)), entry);
6464
6549
  unmaskRe = null;
@@ -6471,8 +6556,8 @@ function createMasker(options = {}) {
6471
6556
  const entry = { placeholder: e.placeholder, kind: e.kind, original: e.original };
6472
6557
  byKey.set(key, entry);
6473
6558
  byLoose.set(looseKey(coreOf(entry.placeholder)), entry);
6474
- const n = Number(/(\d+)\D*$/.exec(e.placeholder)?.[1] ?? 0);
6475
- counters.set(e.kind, Math.max(counters.get(e.kind) ?? 0, n));
6559
+ const n2 = Number(/(\d+)\D*$/.exec(e.placeholder)?.[1] ?? 0);
6560
+ counters.set(e.kind, Math.max(counters.get(e.kind) ?? 0, n2));
6476
6561
  }
6477
6562
  unmaskRe = null;
6478
6563
  }
@@ -6690,7 +6775,7 @@ var mask = {
6690
6775
  if (r.spans.length) {
6691
6776
  const kinds = /* @__PURE__ */ new Map();
6692
6777
  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, n]) => `${k.toLowerCase()} ${n}`).join(", ")}`));
6778
+ ctx.ui.err(ctx.ui.ce("dim", `masked ${r.spans.length} \xB7 ${[...kinds].map(([k, n2]) => `${k.toLowerCase()} ${n2}`).join(", ")}`));
6694
6779
  } else ctx.ui.err(ctx.ui.ce("dim", "nothing to mask."));
6695
6780
  }
6696
6781
  };
@@ -6724,7 +6809,7 @@ function attachSession(o) {
6724
6809
  return;
6725
6810
  }
6726
6811
  if (i > 0) send({ type: "input", data: s2.slice(0, i) });
6727
- finish({ reason: "detached" });
6812
+ finish2({ reason: "detached" });
6728
6813
  };
6729
6814
  const restore = () => {
6730
6815
  if (restored) return;
@@ -6745,7 +6830,7 @@ function attachSession(o) {
6745
6830
  } catch {
6746
6831
  }
6747
6832
  };
6748
- function finish(end) {
6833
+ function finish2(end) {
6749
6834
  restore();
6750
6835
  try {
6751
6836
  if (ws.readyState === 0 || ws.readyState === 1) ws.close(1e3, end.reason);
@@ -6754,7 +6839,7 @@ function attachSession(o) {
6754
6839
  settle(end);
6755
6840
  }
6756
6841
  function onTerm() {
6757
- finish({ reason: "closed", message: "signal" });
6842
+ finish2({ reason: "closed", message: "signal" });
6758
6843
  }
6759
6844
  try {
6760
6845
  stdin.setRawMode?.(true);
@@ -6782,14 +6867,14 @@ function attachSession(o) {
6782
6867
  return;
6783
6868
  }
6784
6869
  if (msg.type === "output" && typeof msg.data === "string") stdout.write(msg.data);
6785
- else if (msg.type === "exit") finish({ reason: "exit", code: typeof msg.code === "number" ? msg.code : 0 });
6870
+ else if (msg.type === "exit") finish2({ reason: "exit", code: typeof msg.code === "number" ? msg.code : 0 });
6786
6871
  else if (msg.type === "status" && msg.state === "waking") o.status?.("waking the pod\u2026");
6787
6872
  };
6788
6873
  ws.onerror = () => {
6789
- if (!restored) finish({ reason: "error", message: "connection error" });
6874
+ if (!restored) finish2({ reason: "error", message: "connection error" });
6790
6875
  };
6791
6876
  ws.onclose = (ev) => {
6792
- if (!restored) finish({ reason: "closed", code: ev?.code, message: ev?.reason });
6877
+ if (!restored) finish2({ reason: "closed", code: ev?.code, message: ev?.reason });
6793
6878
  };
6794
6879
  return done;
6795
6880
  }
@@ -6944,71 +7029,71 @@ async function readValue(ctx, name) {
6944
7029
  if (!v) throw new CliError("no value on stdin", `printf %s "$VALUE" | airgate secrets set ${name}`);
6945
7030
  return v;
6946
7031
  }
6947
- function parseDotenv(text2) {
6948
- const values = {};
6949
- const skipped = [];
6950
- for (const raw of text2.split(/\r?\n/)) {
6951
- const line2 = raw.trim();
6952
- if (!line2 || line2.startsWith("#")) continue;
6953
- const m = /^(?:export\s+)?([A-Za-z_][A-Za-z0-9_.-]*)\s*=\s*(.*)$/.exec(line2);
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 };
7032
+ function parseDotenv2(text2) {
7033
+ const r = parseDotenv(text2);
7034
+ return {
7035
+ values: r.values,
7036
+ skipped: r.entries.filter((e) => !e.name).map((e) => e.key),
7037
+ renamed: r.entries.filter((e) => e.name && e.name !== e.key).map((e) => `${e.key} \u2192 ${e.name}`)
7038
+ };
6967
7039
  }
6968
- async function sendDotenv(ctx, file, podId) {
6969
- const path = resolve3(ctx.io.cwd, file);
6970
- let text2;
7040
+ function readDotenvFile(ctx, file, hint) {
6971
7041
  try {
6972
- text2 = readFileSync2(path, "utf8");
7042
+ return readFileSync2(resolve3(ctx.io.cwd, file), "utf8");
6973
7043
  } catch {
6974
- throw new CliError(`can't read ${file}`, "airgate up --env path/to/.env");
7044
+ throw new CliError(`can't read ${file}`, hint);
6975
7045
  }
6976
- const { values, skipped } = parseDotenv(text2);
6977
- const sent = [];
6978
- const failed = [];
6979
- for (const [k, v] of Object.entries(values)) {
6980
- try {
6981
- await ctx.api.setSecret(k, v, podId);
6982
- sent.push(k);
6983
- } catch (e) {
6984
- if (e instanceof ApiError && e.status < 500) failed.push(`${k} (${e.message})`);
6985
- else throw e;
6986
- }
7046
+ }
7047
+ async function sendDotenv(ctx, file, podId) {
7048
+ const { values, skipped, renamed } = parseDotenv2(readDotenvFile(ctx, file, "airgate up --env path/to/.env"));
7049
+ if (!Object.keys(values).length) return { sent: [], skipped, renamed, failed: [] };
7050
+ try {
7051
+ const r = await ctx.api.setSecrets(values, podId);
7052
+ return { sent: [...r.created, ...r.updated], skipped, renamed, failed: [] };
7053
+ } catch (e) {
7054
+ if (e instanceof ApiError && e.status < 500) return { sent: [], skipped, renamed, failed: [e.message] };
7055
+ throw e;
6987
7056
  }
6988
- return { sent, skipped, failed };
6989
7057
  }
6990
7058
  var secrets = {
6991
7059
  name: "secrets",
6992
7060
  aliases: ["secret"],
6993
7061
  group: "pods",
6994
- usage: "[set NAME | rm NAME] [--pod p]",
7062
+ usage: "[set NAME | rm NAME | import FILE] [--pod p] [--dotenv]",
6995
7063
  summary: "environment variables for pods, stored encrypted, never shown again",
6996
- example: "airgate secrets set STRIPE_KEY --pod api",
7064
+ example: "airgate secrets import .env --pod api",
6997
7065
  notes: [
6998
7066
  "set reads the value from a hidden prompt, or from stdin when piped (never from the command line).",
6999
7067
  "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."
7068
+ "every value of 6+ characters is masked before it reaches a model, even if the agent reads it.",
7069
+ "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
7070
  ],
7002
7071
  flags: {
7003
- pod: { type: "string", value: "pod", help: "just this pod (default: every pod)" }
7072
+ pod: { type: "string", value: "pod", help: "just this pod (default: every pod)" },
7073
+ dotenv: { type: "boolean", help: "ls: print the names as a .env.example (no values)" }
7004
7074
  },
7005
7075
  async run(ctx) {
7006
7076
  const [action, name, extra] = ctx.args.positionals;
7007
7077
  if (extra !== void 0) throw new UsageError(`unexpected "${extra}"`, "values are read from a hidden prompt or stdin, never from arguments");
7008
7078
  const podRef = str(ctx.args, "pod");
7009
7079
  const pod = podRef ? await resolvePod(ctx, podRef) : void 0;
7080
+ if (action === "import") {
7081
+ if (!name) throw new UsageError("which file? airgate secrets import .env [--pod p]");
7082
+ const r = await ctx.ui.spin(`saving ${name}`, sendDotenv(ctx, name, pod?.id));
7083
+ if (ctx.json) return ctx.ui.json(r);
7084
+ if (r.failed.length) throw new CliError(`nothing saved: ${r.failed.join("; ")}`);
7085
+ if (!r.sent.length) throw new CliError(`no KEY=value lines in ${name}`);
7086
+ ctx.ui.out(`${ctx.ui.c("ok", "saved")} ${r.sent.length} for ${pod ? pod.spec.name : "every pod"}: ${r.sent.join(", ")}`);
7087
+ if (r.renamed.length) ctx.ui.out(ctx.ui.c("dim", `renamed: ${r.renamed.join(", ")}`));
7088
+ if (r.skipped.length) ctx.ui.err(ctx.ui.ce("warn", `skipped (reserved or unusable names): ${r.skipped.join(", ")}`));
7089
+ return;
7090
+ }
7010
7091
  if (!action || action === "list" || action === "ls") {
7011
7092
  const all = await ctx.api.secrets();
7093
+ if (bool(ctx.args, "dotenv")) {
7094
+ const names = [...all.account.map((s2) => s2.name), ...pod ? (all.pods[pod.id] ?? []).map((s2) => s2.name) : []];
7095
+ return void ctx.io.stdout.write(dotenvExample([...new Set(names)].sort()));
7096
+ }
7012
7097
  if (ctx.json) return ctx.ui.json(pod ? { account: all.account, pod: all.pods[pod.id] ?? [] } : all);
7013
7098
  const podNames2 = new Map((await ctx.api.pods()).map((p) => [p.id, p.spec.name]));
7014
7099
  const rows2 = [];
@@ -7040,11 +7125,14 @@ var secrets = {
7040
7125
  ctx.ui.out(`removed ${name} from ${pod ? pod.spec.name : "the account"}. ${r.note}.`);
7041
7126
  return;
7042
7127
  }
7043
- throw new UsageError(`unknown action "${action}"`, "airgate secrets [set NAME | rm NAME] [--pod p]");
7128
+ throw new UsageError(`unknown action "${action}"`, "airgate secrets [set NAME | rm NAME | import FILE] [--pod p]");
7044
7129
  }
7045
7130
  };
7046
7131
 
7047
7132
  // src/commands/pods.ts
7133
+ import { execFileSync } from "node:child_process";
7134
+ import { readFileSync as readFileSync3 } from "node:fs";
7135
+ import { resolve as resolvePath } from "node:path";
7048
7136
  var AGENT_NAMES = Object.keys(AGENTS);
7049
7137
  var SIZE_NAMES = Object.keys(SIZES);
7050
7138
  var NETWORK_NAMES = ["allowlist", "open", "off"];
@@ -7060,9 +7148,9 @@ function cronProblem(cron) {
7060
7148
  if (!cronValid(cron)) return `"${cron}" isn't a 5-field cron (min hour day month weekday, utc)`;
7061
7149
  let t = cronNext(cron);
7062
7150
  for (let i = 0; i < 12 && t !== null; i++) {
7063
- const n = cronNext(cron, t);
7064
- if (n !== null && n - t < 15 * 6e4) return `"${cron}" runs more often than every 15 minutes`;
7065
- t = n;
7151
+ const n2 = cronNext(cron, t);
7152
+ if (n2 !== null && n2 - t < 15 * 6e4) return `"${cron}" runs more often than every 15 minutes`;
7153
+ t = n2;
7066
7154
  }
7067
7155
  return null;
7068
7156
  }
@@ -7125,7 +7213,7 @@ var pods = {
7125
7213
  var launch = {
7126
7214
  name: "launch",
7127
7215
  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]",
7216
+ 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
7217
  summary: "start a new pod",
7130
7218
  example: "airgate launch --name fix-nav --agent codex --repo acme/web",
7131
7219
  notes: [
@@ -7143,7 +7231,8 @@ var launch = {
7143
7231
  "builtin-model": { type: "boolean", help: "autopilot with opencode: use airgate's built-in model (no subscription)" },
7144
7232
  model: { type: "string", value: "id", help: "built-in model: an openrouter id (default: the gateway's); see airgate templates" },
7145
7233
  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)" },
7234
+ 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)" },
7235
+ env: { type: "string", value: "file", help: "a .env file: its keys become this pod's secrets before it boots" },
7147
7236
  "public-repo": { type: "boolean", help: "the repo is public: don't check it against your GitHub App" },
7148
7237
  mask: { type: "boolean", default: true, help: "mask model traffic (--no-mask to turn off)" },
7149
7238
  undo: { type: "boolean", default: true, help: "checkpoint after every agent step (--no-undo to turn off)" },
@@ -7156,16 +7245,48 @@ var launch = {
7156
7245
  if (ctx.args.positionals.length) throw new UsageError(`unexpected "${ctx.args.positionals[0]}"`, "name the pod with --name");
7157
7246
  const spec = specFromFlags(ctx);
7158
7247
  if (spec.repo) await checkRepo(ctx, spec.repo);
7159
- const pod = await ctx.ui.spin(`launching ${spec.name}`, async () => waitReady(ctx, await ctx.api.launch(spec)));
7248
+ const envFile = str(ctx.args, "env");
7249
+ let secrets2;
7250
+ if (envFile) {
7251
+ const { values, skipped, renamed } = parseDotenv2(readDotenv(ctx, envFile));
7252
+ secrets2 = values;
7253
+ if (renamed.length) ctx.ui.err(ctx.ui.ce("dim", `renamed: ${renamed.join(", ")}`));
7254
+ if (skipped.length) ctx.ui.err(ctx.ui.ce("warn", `skipped (reserved or unusable names): ${skipped.join(", ")}`));
7255
+ }
7256
+ const pod = await ctx.ui.spin(`launching ${spec.name}`, async () => waitReady(ctx, await ctx.api.launch(spec, void 0, secrets2)));
7257
+ if (secrets2 && Object.keys(secrets2).length && !ctx.json) ctx.ui.out(`secrets for ${pod.spec.name}: ${Object.keys(secrets2).join(", ")}`);
7160
7258
  if (ctx.json) return ctx.ui.json(pod);
7161
7259
  ctx.ui.out(readyLine(ctx, pod));
7162
7260
  if ((pod.spec.template ?? "code") === "autopilot") {
7163
- const n = pod.spec.jobs?.length ?? 0;
7261
+ const n2 = pod.spec.jobs?.length ?? 0;
7164
7262
  if (!pod.spec.builtinModel) ctx.ui.out(`sign ${AGENTS[pod.spec.agent].label} in once: airgate attach ${pod.spec.name}`);
7165
- ctx.ui.out(n ? `${n} job${n === 1 ? "" : "s"}: airgate jobs ${pod.spec.name}` : `add a job: airgate job add ${pod.spec.name} --cron "0 2 * * *" --task "\u2026"`);
7263
+ 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
7264
  } else ctx.ui.out(`next: airgate attach ${pod.spec.name}`);
7167
7265
  }
7168
7266
  };
7267
+ function repoArg(ctx, raw) {
7268
+ if (raw === void 0) return void 0;
7269
+ let input = raw;
7270
+ if (raw === ".") {
7271
+ try {
7272
+ input = execFileSync("git", ["remote", "get-url", "origin"], { cwd: ctx.io.cwd, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
7273
+ } catch {
7274
+ throw new UsageError("--repo .: this folder has no git remote called origin");
7275
+ }
7276
+ }
7277
+ const s2 = input.trim().replace(/\.git$/, "").replace(/^git@github\.com:/, "").replace(/^(?:https?:\/\/)?(?:www\.)?github\.com\//, "");
7278
+ const [owner, name] = s2.split(/[/#?]/);
7279
+ const repo = owner && name ? `${owner}/${name}` : "";
7280
+ 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");
7281
+ return repo;
7282
+ }
7283
+ function readDotenv(ctx, file) {
7284
+ try {
7285
+ return readFileSync3(resolvePath(ctx.io.cwd, file), "utf8");
7286
+ } catch {
7287
+ throw new CliError(`can't read ${file}`, "airgate launch --env path/to/.env");
7288
+ }
7289
+ }
7169
7290
  function specFromFlags(ctx, defaults = {}) {
7170
7291
  const a = ctx.args;
7171
7292
  const network = str(a, "network") ?? "allowlist";
@@ -7176,8 +7297,7 @@ function specFromFlags(ctx, defaults = {}) {
7176
7297
  autoStopMinutes = Number(autoStop);
7177
7298
  if (!Number.isInteger(autoStopMinutes) || autoStopMinutes < 1) throw new UsageError("--auto-stop: whole idle minutes \u2265 1, or keep-awake");
7178
7299
  }
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");
7300
+ const repo = repoArg(ctx, str(a, "repo"));
7181
7301
  const template = str(a, "template") ?? "code";
7182
7302
  const t = TEMPLATES[template];
7183
7303
  const agent = str(a, "agent") ?? (defaults.agent && t.agents.includes(defaults.agent) ? defaults.agent : t.agents[0]);
@@ -7307,7 +7427,6 @@ var destroy = {
7307
7427
  };
7308
7428
  var exec = {
7309
7429
  name: "exec",
7310
- aliases: ["run"],
7311
7430
  group: "pods",
7312
7431
  usage: "<pod> -- <command\u2026>",
7313
7432
  summary: "run a command in the pod (bash -c, in ~/workspace) and print its output",
@@ -7330,7 +7449,7 @@ var exec = {
7330
7449
  };
7331
7450
 
7332
7451
  // src/commands/jobs.ts
7333
- var price = (n) => `$${n.toFixed(2)}/h`;
7452
+ var price = (n2) => `$${n2.toFixed(2)}/h`;
7334
7453
  var templates = {
7335
7454
  name: "templates",
7336
7455
  group: "pods",
@@ -7528,199 +7647,16 @@ var log = {
7528
7647
  // src/commands/mcp.ts
7529
7648
  import { createInterface } from "node:readline";
7530
7649
 
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
7650
  // src/version.ts
7715
- var VERSION = "0.1.0";
7651
+ var VERSION = "0.1.2";
7716
7652
 
7717
7653
  // src/commands/updown.ts
7718
- import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync4, realpathSync as realpathSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
7654
+ import { appendFileSync, existsSync as existsSync3, mkdirSync as mkdirSync3, readFileSync as readFileSync5, realpathSync as realpathSync2, statSync as statSync2, writeFileSync as writeFileSync3 } from "node:fs";
7719
7655
  import { homedir as homedir2 } from "node:os";
7720
7656
  import { basename, dirname as dirname2, isAbsolute, join as join4, relative as relative2, resolve as resolve4 } from "node:path";
7721
7657
 
7722
7658
  // src/pack.ts
7723
- import { spawn as spawn2, spawnSync } from "node:child_process";
7659
+ import { spawn, spawnSync } from "node:child_process";
7724
7660
  import { existsSync, lstatSync } from "node:fs";
7725
7661
  import { join as join2 } from "node:path";
7726
7662
  var UPLOAD_LIMIT = 32 * 6 * 1024 * 1024;
@@ -7759,8 +7695,8 @@ function gitFiles(dir) {
7759
7695
  return out;
7760
7696
  }
7761
7697
  function runTar(cwd, args, input, limit = UPLOAD_LIMIT) {
7762
- return new Promise((resolve5, reject) => {
7763
- const child = spawn2("tar", args, { cwd, env: { ...process.env, COPYFILE_DISABLE: "1" }, stdio: ["pipe", "pipe", "pipe"] });
7698
+ return new Promise((resolve6, reject) => {
7699
+ const child = spawn("tar", args, { cwd, env: { ...process.env, COPYFILE_DISABLE: "1" }, stdio: ["pipe", "pipe", "pipe"] });
7764
7700
  const chunks = [];
7765
7701
  let size2 = 0;
7766
7702
  let err = "";
@@ -7779,7 +7715,7 @@ function runTar(cwd, args, input, limit = UPLOAD_LIMIT) {
7779
7715
  child.on("close", (code3) => {
7780
7716
  if (over2) return reject(new TooLargeError(limit));
7781
7717
  if (code3 !== 0) return reject(new Error(`tar failed: ${err.trim().split("\n").slice(-3).join(" ")}`));
7782
- resolve5(Buffer.concat(chunks));
7718
+ resolve6(Buffer.concat(chunks));
7783
7719
  });
7784
7720
  child.stdin.on("error", () => void 0);
7785
7721
  child.stdin.end(input ?? "");
@@ -7815,7 +7751,7 @@ import { gunzipSync } from "node:zlib";
7815
7751
 
7816
7752
  // src/sessions.ts
7817
7753
  import { createHash, randomUUID } from "node:crypto";
7818
- import { closeSync, existsSync as existsSync2, mkdirSync as mkdirSync2, mkdtempSync, openSync, readdirSync, readFileSync as readFileSync3, readSync, realpathSync, renameSync as renameSync2, rmSync as rmSync2, statSync, writeFileSync as writeFileSync2 } from "node:fs";
7754
+ 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
7755
  import { tmpdir } from "node:os";
7820
7756
  import { dirname, join as join3, relative } from "node:path";
7821
7757
  var POD_HOME = "/home/sprite";
@@ -7843,15 +7779,15 @@ function findClaudeSession(home, projectDir, id, env = {}) {
7843
7779
  const dir = join3(claudeHome(home, env), "projects", claudeKey(p));
7844
7780
  let names = [];
7845
7781
  try {
7846
- names = readdirSync(dir).filter((n) => n.endsWith(".jsonl"));
7782
+ names = readdirSync(dir).filter((n2) => n2.endsWith(".jsonl"));
7847
7783
  } catch {
7848
7784
  continue;
7849
7785
  }
7850
- for (const n of names) {
7851
- const sid = n.slice(0, -".jsonl".length);
7786
+ for (const n2 of names) {
7787
+ const sid = n2.slice(0, -".jsonl".length);
7852
7788
  if (id && sid !== id) continue;
7853
7789
  if (!ID_RE.test(sid)) continue;
7854
- const file = join3(dir, n);
7790
+ const file = join3(dir, n2);
7855
7791
  const side = join3(dir, sid);
7856
7792
  best.push({ agent: "claude-code", id: sid, file, mtime: statSync(file).mtimeMs, ...existsSync2(side) && statSync(side).isDirectory() ? { sidecar: side } : {} });
7857
7793
  }
@@ -7863,8 +7799,8 @@ function firstLine(file) {
7863
7799
  const fd = openSync(file, "r");
7864
7800
  try {
7865
7801
  const buf = Buffer.alloc(1024 * 1024);
7866
- const n = readSync(fd, buf, 0, buf.length, 0);
7867
- return buf.subarray(0, n).toString("utf8").split("\n")[0];
7802
+ const n2 = readSync(fd, buf, 0, buf.length, 0);
7803
+ return buf.subarray(0, n2).toString("utf8").split("\n")[0];
7868
7804
  } finally {
7869
7805
  closeSync(fd);
7870
7806
  }
@@ -7920,7 +7856,7 @@ async function packSession(s2, projectDir, podPath, limit = UPLOAD_LIMIT) {
7920
7856
  const froms = spellings(projectDir);
7921
7857
  const stage = mkdtempSync(join3(tmpdir(), "airgate-session-"));
7922
7858
  try {
7923
- const raw = readFileSync3(s2.file);
7859
+ const raw = readFileSync4(s2.file);
7924
7860
  let podDir;
7925
7861
  const names = [];
7926
7862
  if (s2.agent === "claude-code") {
@@ -7936,7 +7872,7 @@ async function packSession(s2, projectDir, podPath, limit = UPLOAD_LIMIT) {
7936
7872
  mkdirSync2(b2, { recursive: true });
7937
7873
  copy2(a, b2);
7938
7874
  } else if (x.isFile()) {
7939
- const body = readFileSync3(a);
7875
+ const body = readFileSync4(a);
7940
7876
  writeFileSync2(b2, /\.(jsonl?|txt|md)$/.test(x.name) ? rewriteAll(body.toString("utf8"), froms, podPath) : body);
7941
7877
  }
7942
7878
  }
@@ -7968,7 +7904,7 @@ function bringBack(sent, podContent, now = /* @__PURE__ */ new Date()) {
7968
7904
  let text2 = rewritePath(podContent.toString("utf8"), sent.podPath, sent.localPath);
7969
7905
  const localId = sent.localId ?? sent.id;
7970
7906
  if (localId !== sent.id) text2 = text2.split(sent.id).join(localId);
7971
- const local = existsSync2(sent.file) ? readFileSync3(sent.file) : null;
7907
+ const local = existsSync2(sent.file) ? readFileSync4(sent.file) : null;
7972
7908
  if (local && local.toString("utf8") === text2) return { agent: sent.agent, id: localId, file: sent.file, outcome: "same", sha256: sha256(local) };
7973
7909
  const atomic = (file2, body2) => {
7974
7910
  mkdirSync2(dirname(file2), { recursive: true });
@@ -7985,7 +7921,7 @@ function bringBack(sent, podContent, now = /* @__PURE__ */ new Date()) {
7985
7921
  let file;
7986
7922
  if (sent.agent === "claude-code") file = join3(dirname(sent.file), `${id}.jsonl`);
7987
7923
  else {
7988
- const p = (n) => String(n).padStart(2, "0");
7924
+ const p = (n2) => String(n2).padStart(2, "0");
7989
7925
  const root = join3(dirname(sent.file), "..", "..", "..");
7990
7926
  const stamp = `${now.getFullYear()}-${p(now.getMonth() + 1)}-${p(now.getDate())}T${p(now.getHours())}-${p(now.getMinutes())}-${p(now.getSeconds())}`;
7991
7927
  file = join3(root, String(now.getFullYear()), p(now.getMonth() + 1), p(now.getDate()), `rollout-${stamp}-${id}.jsonl`);
@@ -8031,11 +7967,11 @@ function unquote(p) {
8031
7967
  bytes2.push(...Buffer.from(c, "utf8"));
8032
7968
  continue;
8033
7969
  }
8034
- const n = p[++i];
8035
- if (/[0-7]/.test(n)) {
7970
+ const n2 = p[++i];
7971
+ if (/[0-7]/.test(n2)) {
8036
7972
  bytes2.push(parseInt(p.slice(i, i + 3), 8));
8037
7973
  i += 2;
8038
- } else bytes2.push(simple[n] ?? n.charCodeAt(0));
7974
+ } else bytes2.push(simple[n2] ?? n2.charCodeAt(0));
8039
7975
  }
8040
7976
  return Buffer.from(bytes2).toString("utf8");
8041
7977
  }
@@ -8171,7 +8107,7 @@ function findState(from) {
8171
8107
  const p = statePath(dir);
8172
8108
  if (existsSync3(p)) {
8173
8109
  try {
8174
- const s2 = JSON.parse(readFileSync4(p, "utf8"));
8110
+ const s2 = JSON.parse(readFileSync5(p, "utf8"));
8175
8111
  if (s2 && typeof s2.podId === "string" && typeof s2.dir === "string") return { root: dir, state: s2 };
8176
8112
  } catch {
8177
8113
  }
@@ -8187,7 +8123,7 @@ function excludeStateDir(projectDir) {
8187
8123
  if (r.status !== 0) return false;
8188
8124
  const raw = r.stdout.trim();
8189
8125
  const file = isAbsolute(raw) ? raw : join4(projectDir, raw);
8190
- const current = existsSync3(file) ? readFileSync4(file, "utf8") : "";
8126
+ const current = existsSync3(file) ? readFileSync5(file, "utf8") : "";
8191
8127
  if (current.split(/\r?\n/).some((l) => l.trim() === ".airgate/" || l.trim() === "/.airgate/" || l.trim() === ".airgate")) return true;
8192
8128
  mkdirSync3(dirname2(file), { recursive: true });
8193
8129
  appendFileSync(file, `${current && !current.endsWith("\n") ? "\n" : ""}# airgate up/down state
@@ -8196,8 +8132,8 @@ function excludeStateDir(projectDir) {
8196
8132
  return true;
8197
8133
  }
8198
8134
  function podDirName(projectDir) {
8199
- const n = basename(resolve4(projectDir)).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[.-]+/, "");
8200
- return n && n !== "." && n !== ".." ? n.slice(0, 64) : "project";
8135
+ const n2 = basename(resolve4(projectDir)).replace(/[^A-Za-z0-9._-]+/g, "-").replace(/^[.-]+/, "");
8136
+ return n2 && n2 !== "." && n2 !== ".." ? n2.slice(0, 64) : "project";
8201
8137
  }
8202
8138
  var up = {
8203
8139
  name: "up",
@@ -8507,39 +8443,391 @@ ${check.stderr.trim()}`, "commit or stash local edits to those files, or look wi
8507
8443
  }
8508
8444
  };
8509
8445
 
8510
- // src/commands/mcp.ts
8511
- var PROTOCOL_VERSION = "2025-06-18";
8512
- var obj = (properties, required = []) => ({ type: "object", properties, required, additionalProperties: false });
8513
- var s = (description, extra = {}) => ({ type: "string", description, ...extra });
8514
- var b = (description) => ({ type: "boolean", description });
8515
- var POD = s("pod name or id (a unique prefix is enough)");
8516
- var need = (args, key) => {
8517
- const v = args[key];
8518
- if (typeof v !== "string" || !v.trim()) throw new UsageError(`${key} is required`);
8519
- return v;
8520
- };
8521
- async function runCommand(ctx, cmd, argv, cwd) {
8522
- const m = memoryIo({ env: ctx.io.env, cwd });
8523
- const args = parseArgs([...argv, "--json"], cmd.flags ?? {});
8524
- const sub = { ...makeCtx(m.io, createUi(m.io), args, ctx.fetch), api: ctx.api };
8525
- await cmd.run(sub);
8526
- const out = m.stdout.trim();
8527
- try {
8528
- return out ? JSON.parse(out) : { ok: true };
8529
- } catch {
8530
- return { output: out };
8531
- }
8532
- }
8533
- var jobSchema = obj(
8534
- {
8535
- id: s("keep an existing job: its id"),
8536
- name: s("a name to show"),
8537
- cron: s('5-field cron in UTC, at most every 15 minutes, e.g. "0 2 * * *"'),
8446
+ // src/io.ts
8447
+ import { spawn as spawn2 } from "node:child_process";
8448
+ import { EventEmitter } from "node:events";
8449
+ import { PassThrough } from "node:stream";
8450
+ import { StringDecoder as StringDecoder2 } from "node:string_decoder";
8451
+ var LineReader = class {
8452
+ buf = "";
8453
+ ended = false;
8454
+ waiters = [];
8455
+ stream;
8456
+ attached = false;
8457
+ decoder = new StringDecoder2("utf8");
8458
+ constructor(stream) {
8459
+ this.stream = stream;
8460
+ }
8461
+ attach() {
8462
+ if (this.attached) return;
8463
+ this.attached = true;
8464
+ this.stream.on("data", (d) => {
8465
+ this.buf += typeof d === "string" ? d : this.decoder.write(d);
8466
+ this.wake();
8467
+ });
8468
+ this.stream.on("end", () => {
8469
+ this.ended = true;
8470
+ this.wake();
8471
+ });
8472
+ this.stream.on("error", () => {
8473
+ this.ended = true;
8474
+ this.wake();
8475
+ });
8476
+ }
8477
+ wake() {
8478
+ for (const w of this.waiters.splice(0)) w();
8479
+ }
8480
+ async line() {
8481
+ this.attach();
8482
+ this.stream.resume();
8483
+ for (; ; ) {
8484
+ const i = this.buf.indexOf("\n");
8485
+ if (i !== -1) {
8486
+ const l = this.buf.slice(0, i).replace(/\r$/, "");
8487
+ this.buf = this.buf.slice(i + 1);
8488
+ if (!this.buf) this.stream.pause();
8489
+ return l;
8490
+ }
8491
+ if (this.ended) {
8492
+ const rest = this.buf;
8493
+ this.buf = "";
8494
+ return rest ? rest : null;
8495
+ }
8496
+ await new Promise((r) => this.waiters.push(r));
8497
+ }
8498
+ }
8499
+ };
8500
+ function ttyPrompt(stdin, out) {
8501
+ return (question, opts2 = {}) => new Promise((resolve6, reject) => {
8502
+ out.write(question);
8503
+ const decoder = new StringDecoder2("utf8");
8504
+ let answer = "";
8505
+ const wasRaw = Boolean(stdin.isRaw);
8506
+ stdin.setRawMode?.(true);
8507
+ stdin.resume();
8508
+ const done = (err) => {
8509
+ stdin.off("data", onData);
8510
+ stdin.setRawMode?.(wasRaw);
8511
+ stdin.pause();
8512
+ out.write("\n");
8513
+ if (err) reject(err);
8514
+ else resolve6(answer);
8515
+ };
8516
+ const onData = (d) => {
8517
+ for (const ch of decoder.write(d)) {
8518
+ if (ch === "\r" || ch === "\n") return done();
8519
+ if (ch === "") return done(Object.assign(new Error("cancelled"), { code: "cancelled" }));
8520
+ if (ch === "") {
8521
+ if (!answer) return done(Object.assign(new Error("cancelled"), { code: "cancelled" }));
8522
+ continue;
8523
+ }
8524
+ if (ch === "\x7F" || ch === "\b") {
8525
+ if (answer) {
8526
+ answer = [...answer].slice(0, -1).join("");
8527
+ if (!opts2.hidden) out.write("\b \b");
8528
+ }
8529
+ continue;
8530
+ }
8531
+ if (ch < " ") continue;
8532
+ answer += ch;
8533
+ if (!opts2.hidden) out.write(ch);
8534
+ }
8535
+ };
8536
+ stdin.on("data", onData);
8537
+ });
8538
+ }
8539
+ function openInBrowser(url) {
8540
+ const [cmd, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["cmd", ["/c", "start", '""', url]] : ["xdg-open", [url]];
8541
+ return new Promise((resolve6) => {
8542
+ try {
8543
+ const child = spawn2(cmd, args, { stdio: "ignore", detached: true });
8544
+ child.on("error", () => resolve6(false));
8545
+ child.on("spawn", () => {
8546
+ child.unref();
8547
+ resolve6(true);
8548
+ });
8549
+ } catch {
8550
+ resolve6(false);
8551
+ }
8552
+ });
8553
+ }
8554
+ function processIo() {
8555
+ const stdin = process.stdin;
8556
+ const lines = new LineReader(stdin);
8557
+ const tty = ttyPrompt(stdin, process.stderr);
8558
+ return {
8559
+ stdin,
8560
+ stdout: process.stdout,
8561
+ stderr: process.stderr,
8562
+ env: process.env,
8563
+ // `npm run cli -- up` runs in packages/cli; INIT_CWD is where the user typed it
8564
+ cwd: process.env.AIRGATE_CWD || process.env.npm_lifecycle_event === "dev" && process.env.INIT_CWD || process.cwd(),
8565
+ async prompt(q, o) {
8566
+ if (stdin.isTTY) return tty(q, o);
8567
+ process.stderr.write(q);
8568
+ const l = await lines.line();
8569
+ if (l === null) throw Object.assign(new Error("no input (stdin closed)"), { code: "no_input" });
8570
+ process.stderr.write("\n");
8571
+ return l;
8572
+ },
8573
+ openUrl: openInBrowser,
8574
+ websocket: (url) => new WebSocket(url),
8575
+ signals: process
8576
+ };
8577
+ }
8578
+ function memoryIo(opts2 = {}) {
8579
+ const out = new PassThrough();
8580
+ const err = new PassThrough();
8581
+ let stdout = "";
8582
+ let stderr = "";
8583
+ out.on("data", (d) => stdout += d.toString());
8584
+ err.on("data", (d) => stderr += d.toString());
8585
+ const answers = [...opts2.answers ?? []];
8586
+ const opened = [];
8587
+ const prompts = [];
8588
+ const stdin = opts2.stdin ?? Object.assign(new PassThrough(), { isTTY: false });
8589
+ if (!opts2.stdin) stdin.end();
8590
+ const io = {
8591
+ stdin,
8592
+ stdout: out,
8593
+ stderr: err,
8594
+ env: opts2.env ?? {},
8595
+ cwd: opts2.cwd ?? process.cwd(),
8596
+ async prompt(q) {
8597
+ prompts.push(q);
8598
+ const a = answers.shift();
8599
+ if (a === void 0) throw Object.assign(new Error(`no answer queued for prompt: ${q}`), { code: "no_input" });
8600
+ return a;
8601
+ },
8602
+ async openUrl(u) {
8603
+ opened.push(u);
8604
+ return true;
8605
+ },
8606
+ websocket: () => {
8607
+ throw new Error("no websocket in memoryIo");
8608
+ },
8609
+ signals: new EventEmitter()
8610
+ };
8611
+ return {
8612
+ io,
8613
+ answers,
8614
+ opened,
8615
+ prompts,
8616
+ get stdout() {
8617
+ return stdout;
8618
+ },
8619
+ get stderr() {
8620
+ return stderr;
8621
+ },
8622
+ clear() {
8623
+ stdout = "";
8624
+ stderr = "";
8625
+ }
8626
+ };
8627
+ }
8628
+
8629
+ // src/commands/sub.ts
8630
+ async function runCommand(ctx, cmd, argv, cwd) {
8631
+ const m = memoryIo({ env: ctx.io.env, cwd });
8632
+ const args = parseArgs([...argv, "--json"], cmd.flags ?? {});
8633
+ const sub = { ...makeCtx(m.io, createUi(m.io), args, ctx.fetch), api: ctx.api };
8634
+ await cmd.run(sub);
8635
+ const out = m.stdout.trim();
8636
+ try {
8637
+ return out ? JSON.parse(out) : { ok: true };
8638
+ } catch {
8639
+ return { output: out };
8640
+ }
8641
+ }
8642
+
8643
+ // src/commands/run.ts
8644
+ import { existsSync as existsSync4, readFileSync as readFileSync6, rmSync as rmSync3, statSync as statSync3, writeFileSync as writeFileSync4 } from "node:fs";
8645
+ import { basename as basename2, resolve as resolve5 } from "node:path";
8646
+ import { randomBytes } from "node:crypto";
8647
+ var SIGN_IN = {
8648
+ "claude-code": { names: ["CLAUDE_CODE_OAUTH_TOKEN", "ANTHROPIC_API_KEY"], fix: "claude setup-token (on this laptop), then: airgate secrets set CLAUDE_CODE_OAUTH_TOKEN" },
8649
+ codex: { names: ["OPENAI_API_KEY"], fix: "airgate secrets set OPENAI_API_KEY (or sign in to codex once in a pod you keep)" }
8650
+ };
8651
+ var shortId = () => randomBytes(2).toString("hex");
8652
+ var runName = (base) => `${base.slice(0, 52)}-run-${shortId()}`;
8653
+ async function signInWarning(ctx, agent) {
8654
+ const need2 = SIGN_IN[agent];
8655
+ if (!need2) return null;
8656
+ const list2 = await ctx.api.secrets().catch(() => null);
8657
+ if (!list2) return null;
8658
+ if (list2.account.some((s2) => need2.names.includes(s2.name))) return null;
8659
+ return `no ${need2.names.join(" or ")} secret: ${AGENTS[agent].label} may not be signed in inside the pod. fix once for every pod: ${need2.fix}`;
8660
+ }
8661
+ async function finish(ctx, pod, keep) {
8662
+ if (keep === "destroy") await ctx.ui.spin(`deleting ${pod.spec.name}`, ctx.api.destroy(pod.id));
8663
+ else if (keep === "sleep") await ctx.ui.spin(`putting ${pod.spec.name} to sleep`, ctx.api.stop(pod.id)).catch(() => void 0);
8664
+ }
8665
+ var runTask = {
8666
+ name: "run",
8667
+ group: "up \xB7 down",
8668
+ usage: '"<task>" [dir] [--repo owner/name] [--agent a] [--env [.env]] [--keep] [--no-apply] [--pr] [--timeout min]',
8669
+ summary: "one task in a fresh pod: send the project, run it, bring the changes back, delete the pod",
8670
+ example: 'airgate run "get the failing tests passing"',
8671
+ notes: [
8672
+ "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.",
8673
+ "--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).",
8674
+ "--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).",
8675
+ "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.",
8676
+ "the agent needs a sign-in that works in any pod: for Claude Code run `claude setup-token`, then `airgate secrets set CLAUDE_CODE_OAUTH_TOKEN`."
8677
+ ],
8678
+ flags: {
8679
+ repo: { type: "string", value: "owner/name", help: "run on a GitHub repo instead of this folder" },
8680
+ agent: { type: "string", value: "agent", choices: Object.keys(AGENTS), help: "the pod's agent (default claude-code)" },
8681
+ memory: { type: "string", value: "size", choices: Object.keys(SIZES), help: "pod size (default standard)" },
8682
+ env: { type: "string", value: "file", implicit: ".env", help: "send a dotenv file's keys as the pod's secrets (default .env)" },
8683
+ keep: { type: "boolean", help: "keep the pod afterwards (asleep) instead of deleting it" },
8684
+ "no-apply": { type: "boolean", help: "print the patch, change nothing here (the pod is kept, asleep)" },
8685
+ pr: { type: "boolean", help: "with --repo: have the agent open a pull request (it waits in your airlock)" },
8686
+ timeout: { type: "string", value: "minutes", default: "60", help: "stop waiting after this long (the pod keeps working)" },
8687
+ yes: { type: "boolean", short: "y", help: "don't ask" }
8688
+ },
8689
+ async run(ctx) {
8690
+ const { ui: ui2 } = ctx;
8691
+ const [task2, dirArg, extra] = ctx.args.positionals;
8692
+ if (!task2?.trim()) throw new UsageError("what should the agent do?", 'airgate run "get the failing tests passing"');
8693
+ if (extra) throw new UsageError(`unexpected "${extra}" (put the task in quotes)`);
8694
+ if (task2.length > 3500) throw new UsageError("the task is 3500 characters at most");
8695
+ const agent = str(ctx.args, "agent") ?? "claude-code";
8696
+ const size2 = str(ctx.args, "memory") ?? "standard";
8697
+ const minutes = Number(str(ctx.args, "timeout") ?? "60");
8698
+ if (!Number.isFinite(minutes) || minutes <= 0) throw new UsageError("--timeout: minutes, e.g. 30");
8699
+ const repo = str(ctx.args, "repo");
8700
+ const warnings = [];
8701
+ const warn = await signInWarning(ctx, agent);
8702
+ if (warn) {
8703
+ warnings.push(warn);
8704
+ if (!ctx.json) ui2.err(ui2.ce("warn", warn));
8705
+ }
8706
+ const work = async (pod2, text3) => {
8707
+ let r2 = await ui2.spin(`handing ${pod2.spec.name} the task`, ctx.api.startTask(pod2.id, text3));
8708
+ r2 = await ui2.spin(`${pod2.spec.name} is working (${AGENTS[agent].label})`, () => waitRun(ctx, pod2.id, r2, minutes * 6e4));
8709
+ const log2 = await ctx.api.runLog(pod2.id, r2.id).catch(() => "");
8710
+ return { run: r2, log: log2, done: r2.endedAt !== void 0, ok: r2.endedAt !== void 0 && r2.exitCode === 0 };
8711
+ };
8712
+ if (repo) {
8713
+ if (dirArg) throw new UsageError("a folder or --repo, not both");
8714
+ const look = await ctx.api.inspectRepo(repo).catch((e) => {
8715
+ if (e instanceof ApiError && e.status === 404) return null;
8716
+ throw e;
8717
+ });
8718
+ if (look && !look.reachable) {
8719
+ const gh = await ctx.api.github().catch(() => null);
8720
+ 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"');
8721
+ 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");
8722
+ }
8723
+ const name2 = runName((look?.repo ?? repo).split("/").pop() ?? "repo");
8724
+ const spec = { name: name2, agent, size: size2, repo: look?.repo ?? repo, masking: true, network: "allowlist", allow: DEFAULT_ALLOW, autoStopMinutes: null, autoCheckpoint: true };
8725
+ const pod2 = await ui2.spin(`launching ${name2} on ${spec.repo}`, async () => waitReady(ctx, await ctx.api.launch(spec)));
8726
+ const envFile2 = str(ctx.args, "env");
8727
+ if (envFile2) await ui2.spin(`sending ${envFile2} as secrets`, sendDotenv(ctx, envFile2, pod2.id));
8728
+ 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.");
8729
+ const r2 = await work(pod2, text3);
8730
+ await finish(ctx, pod2, r2.done ? "sleep" : "running");
8731
+ 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 });
8732
+ ctx.io.stdout.write(r2.log.endsWith("\n") || !r2.log ? r2.log : r2.log + "\n");
8733
+ if (!r2.done) ui2.err(ui2.ce("warn", `still working after ${minutes} min; it carries on. follow it: airgate log ${name2}`));
8734
+ 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" : ""}`);
8735
+ return r2.ok ? 0 : 1;
8736
+ }
8737
+ const projectDir = resolve5(ctx.io.cwd, dirArg ?? ".");
8738
+ if (!existsSync4(projectDir) || !statSync3(projectDir).isDirectory()) throw new CliError(`no directory ${projectDir}`);
8739
+ const noApply = bool(ctx.args, "no-apply");
8740
+ if (!noApply && !gitTopLevel(projectDir)) {
8741
+ 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");
8742
+ }
8743
+ const sp = statePath(projectDir);
8744
+ const before = existsSync4(sp) ? readFileSync6(sp, "utf8") : null;
8745
+ const restoreState = () => {
8746
+ try {
8747
+ if (before !== null) writeFileSync4(sp, before);
8748
+ else rmSync3(sp, { force: true });
8749
+ } catch {
8750
+ }
8751
+ };
8752
+ const name = runName(podDirName(projectDir));
8753
+ const upArgv = [projectDir, "--name", name, "--agent", agent, "--memory", size2, "--no-session", "--yes"];
8754
+ const envFile = str(ctx.args, "env");
8755
+ if (envFile) upArgv.push("--env", envFile);
8756
+ const sent = await ui2.spin(`sending ${basename2(projectDir)} to a fresh pod`, runCommand(ctx, up, upArgv, ctx.io.cwd));
8757
+ const pod = sent.pod;
8758
+ const text2 = `Work in ~/${sent.dir} (a project sent from my laptop). ${task2}
8759
+
8760
+ 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.`;
8761
+ let r;
8762
+ try {
8763
+ r = await work(pod, text2);
8764
+ } catch (e) {
8765
+ await finish(ctx, pod, "sleep");
8766
+ throw e;
8767
+ }
8768
+ 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}`;
8769
+ const out = (o) => ui2.json({ pod: { id: pod.id, name }, run: r.run, ok: r.ok, log: r.log, warnings, ...o });
8770
+ if (!r.ok) {
8771
+ await finish(ctx, pod, r.done ? "sleep" : "running");
8772
+ if (ctx.json) return out({ applied: false, pod_state: r.done ? "sleeping" : "running" });
8773
+ ctx.io.stdout.write(r.log.endsWith("\n") || !r.log ? r.log : r.log + "\n");
8774
+ 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}`));
8775
+ return 1;
8776
+ }
8777
+ let result;
8778
+ try {
8779
+ result = await ui2.spin(`bringing the changes back from ${name}`, runCommand(ctx, down2, ["--pod", name, "--no-session", ...noApply ? ["--dry-run"] : ["--yes"]], projectDir));
8780
+ } catch (e) {
8781
+ await finish(ctx, pod, "sleep");
8782
+ const why = e instanceof Error ? e.message.split("\n")[0] : String(e);
8783
+ if (ctx.json) return out({ applied: false, error: why, pod_state: "sleeping" });
8784
+ ctx.io.stdout.write(r.log.endsWith("\n") || !r.log ? r.log : r.log + "\n");
8785
+ ui2.err(ui2.ce("bad", keepNote(`the task finished, but its changes didn't come back: ${why}`)));
8786
+ return 1;
8787
+ }
8788
+ const files = result.files ?? [];
8789
+ const keep = noApply || bool(ctx.args, "keep");
8790
+ await finish(ctx, pod, keep ? "sleep" : "destroy");
8791
+ if (!keep) restoreState();
8792
+ if (ctx.json) return out({ applied: !noApply && files.length > 0, files, ...noApply ? { patch: result.patch ?? "" } : {}, pod_state: keep ? "sleeping" : "deleted" });
8793
+ ctx.io.stdout.write(r.log.endsWith("\n") || !r.log ? r.log : r.log + "\n");
8794
+ if (noApply) {
8795
+ ctx.io.stdout.write(result.patch ?? "");
8796
+ ui2.err(`${files.length} file(s) changed, not applied. apply: airgate down --pod ${name} \xB7 delete the pod: airgate destroy ${name}`);
8797
+ return 0;
8798
+ }
8799
+ const risky = files.filter((f) => f.risky).length;
8800
+ ui2.out(
8801
+ `${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")
8802
+ );
8803
+ return 0;
8804
+ }
8805
+ };
8806
+
8807
+ // src/commands/mcp.ts
8808
+ var PROTOCOL_VERSION = "2025-06-18";
8809
+ var obj = (properties, required = []) => ({ type: "object", properties, required, additionalProperties: false });
8810
+ var s = (description, extra = {}) => ({ type: "string", description, ...extra });
8811
+ var b = (description) => ({ type: "boolean", description });
8812
+ var POD = s("pod name or id (a unique prefix is enough)");
8813
+ var need = (args, key) => {
8814
+ const v = args[key];
8815
+ if (typeof v !== "string" || !v.trim()) throw new UsageError(`${key} is required`);
8816
+ return v;
8817
+ };
8818
+ var jobSchema = obj(
8819
+ {
8820
+ id: s("keep an existing job: its id"),
8821
+ name: s("a name to show"),
8822
+ cron: s('5-field cron in UTC, at most every 15 minutes, e.g. "0 2 * * *"'),
8538
8823
  task: s("what the agent should do each time (\u2264 4000 chars)"),
8539
8824
  enabled: b("false pauses it")
8540
8825
  },
8541
8826
  ["cron", "task"]
8542
8827
  );
8828
+ var OWN = /* @__PURE__ */ new Map();
8829
+ var n = (description) => ({ type: "number", description });
8830
+ var dirOf = (ctx, a) => typeof a.dir === "string" && a.dir ? a.dir : ctx.io.cwd;
8543
8831
  var TOOLS = [
8544
8832
  {
8545
8833
  name: "pods_list",
@@ -8589,6 +8877,7 @@ var TOOLS = [
8589
8877
  }
8590
8878
  const spec = specFromFlags({ ...ctx, args: parseArgs(argv, launch.flags ?? {}) });
8591
8879
  const pod = await waitReady(ctx, await ctx.api.launch(spec));
8880
+ OWN.set(pod.id, "launch");
8592
8881
  return { name: pod.spec.name, id: pod.id, state: pod.state, readyMs: pod.readyMs, spec: pod.spec };
8593
8882
  }
8594
8883
  },
@@ -8709,6 +8998,119 @@ var TOOLS = [
8709
8998
  if (typeof a.pod === "string" && a.pod) argv.push("--pod", a.pod);
8710
8999
  return runCommand(ctx, down2, argv, typeof a.dir === "string" && a.dir ? a.dir : ctx.io.cwd);
8711
9000
  }
9001
+ },
9002
+ {
9003
+ name: "pod_run",
9004
+ 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.",
9005
+ inputSchema: obj(
9006
+ {
9007
+ task: s("what the agent should do"),
9008
+ dir: s("the local project directory (default: where the server started)"),
9009
+ repo: s("run on this GitHub repo (owner/name) instead of the local project"),
9010
+ agent: s("claude-code | codex | opencode | pi (default claude-code)"),
9011
+ apply: b("false: return the patch without applying it (the pod is kept, asleep)"),
9012
+ keep: b("keep the pod afterwards (asleep) instead of deleting it"),
9013
+ pr: b("with repo: open a pull request at the end (it waits in the airlock)"),
9014
+ env: s("a dotenv file whose keys become the pod's secrets, e.g. .env"),
9015
+ timeoutMinutes: n("stop waiting after this many minutes (default 60); the pod keeps working")
9016
+ },
9017
+ ["task"]
9018
+ ),
9019
+ async call(ctx, a) {
9020
+ const argv = [need(a, "task")];
9021
+ const opt = (flag, v) => typeof v === "string" && v && argv.push(`--${flag}`, v);
9022
+ opt("repo", a.repo);
9023
+ opt("agent", a.agent);
9024
+ opt("env", a.env);
9025
+ if (a.apply === false) argv.push("--no-apply");
9026
+ if (a.keep === true) argv.push("--keep");
9027
+ if (a.pr === true) argv.push("--pr");
9028
+ if (typeof a.timeoutMinutes === "number") argv.push("--timeout", String(a.timeoutMinutes));
9029
+ const out = await runCommand(ctx, runTask, argv, dirOf(ctx, a));
9030
+ if (out.pod?.id && out.pod_state !== "deleted") OWN.set(out.pod.id, a.repo ? "launch" : "project");
9031
+ return out;
9032
+ }
9033
+ },
9034
+ {
9035
+ name: "pod_up",
9036
+ 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.",
9037
+ inputSchema: obj({
9038
+ dir: s("the local project directory (default: where the server started)"),
9039
+ pod: s("send into this existing pod instead of launching one"),
9040
+ name: s("name for a new pod"),
9041
+ agent: s("agent for a new pod: claude-code | codex | opencode | pi"),
9042
+ env: s("a dotenv file whose keys become the pod's secrets, e.g. .env"),
9043
+ conversation: b("false: don't send the agent conversation")
9044
+ }),
9045
+ async call(ctx, a) {
9046
+ const argv = ["--yes"];
9047
+ const opt = (flag, v) => typeof v === "string" && v && argv.push(`--${flag}`, v);
9048
+ opt("pod", a.pod);
9049
+ opt("name", a.name);
9050
+ opt("agent", a.agent);
9051
+ opt("env", a.env);
9052
+ if (a.conversation === false) argv.push("--no-session");
9053
+ const out = await runCommand(ctx, up, argv, dirOf(ctx, a));
9054
+ if (!a.pod && out.pod?.id) OWN.set(out.pod.id, "project");
9055
+ return out;
9056
+ }
9057
+ },
9058
+ {
9059
+ name: "pod_sleep",
9060
+ 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.",
9061
+ inputSchema: obj({ pod: POD }, ["pod"]),
9062
+ async call(ctx, a) {
9063
+ const p = await resolvePod(ctx, need(a, "pod"));
9064
+ return ctx.api.stop(p.id);
9065
+ }
9066
+ },
9067
+ {
9068
+ name: "pod_wake",
9069
+ description: "Wake a sleeping pod (seconds).",
9070
+ inputSchema: obj({ pod: POD }, ["pod"]),
9071
+ async call(ctx, a) {
9072
+ const p = await resolvePod(ctx, need(a, "pod"));
9073
+ return ctx.api.resume(p.id);
9074
+ }
9075
+ },
9076
+ {
9077
+ name: "pod_destroy",
9078
+ 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.",
9079
+ 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"]),
9080
+ async call(ctx, a) {
9081
+ const p = await resolvePod(ctx, need(a, "pod"));
9082
+ if (!OWN.has(p.id) && a.user_confirmed !== true) {
9083
+ 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`);
9084
+ }
9085
+ if (a.discard_changes !== true && (OWN.get(p.id) ?? "project") === "project") {
9086
+ const pending = await ctx.api.diff(p.id, "up").catch(() => null);
9087
+ 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`);
9088
+ }
9089
+ await ctx.api.destroy(p.id);
9090
+ OWN.delete(p.id);
9091
+ return { deleted: p.spec.name, id: p.id };
9092
+ }
9093
+ },
9094
+ {
9095
+ name: "pod_ports",
9096
+ description: "What is listening in a pod (dev servers on ports 1024+) and its preview links.",
9097
+ inputSchema: obj({ pod: POD }, ["pod"]),
9098
+ async call(ctx, a) {
9099
+ const p = await resolvePod(ctx, need(a, "pod"));
9100
+ return ctx.api.previews(p.id);
9101
+ }
9102
+ },
9103
+ {
9104
+ name: "pod_preview",
9105
+ 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).",
9106
+ inputSchema: obj({ pod: POD, port: n("the port, 1024+"), public: b("a public link anyone can open") }, ["pod", "port"]),
9107
+ async call(ctx, a) {
9108
+ const p = await resolvePod(ctx, need(a, "pod"));
9109
+ const port = Number(a.port);
9110
+ if (!Number.isInteger(port) || port < 1024 || port > 65535) throw new UsageError("port: 1024\u201365535");
9111
+ const pv = await ctx.api.ensurePreview(p.id, { port, public: a.public === true });
9112
+ 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" };
9113
+ }
8712
9114
  }
8713
9115
  ];
8714
9116
  function errorText(e) {
@@ -8732,7 +9134,7 @@ async function handle(ctx, msg) {
8732
9134
  protocolVersion: asked,
8733
9135
  capabilities: { tools: { listChanged: false } },
8734
9136
  serverInfo: { name: "airgate", title: "Airgate", version: VERSION },
8735
- instructions: "Airgate pods are private cloud computers for coding agents. Use pods_list first; task_start hands a pod a task and run_status/run_log follow it. Outbound actions wait in the airlock for the user: never approve on your own initiative."
9137
+ 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
9138
  });
8737
9139
  }
8738
9140
  case "ping":
@@ -8795,6 +9197,9 @@ var mcp = {
8795
9197
  };
8796
9198
 
8797
9199
  // src/commands/misc.ts
9200
+ import { mkdirSync as mkdirSync4, writeFileSync as writeFileSync5 } from "node:fs";
9201
+ import { homedir as homedir3 } from "node:os";
9202
+ import { join as join5 } from "node:path";
8798
9203
  var PAGES = ["pods", "airlock", "usage", "settings", "mask", "chat", "notes", "data", "status"];
8799
9204
  var open = {
8800
9205
  name: "open",
@@ -8853,6 +9258,106 @@ var version = {
8853
9258
  ctx.ui.out(`airgate ${VERSION}`);
8854
9259
  }
8855
9260
  };
9261
+ var skill = {
9262
+ name: "skill",
9263
+ group: "other",
9264
+ usage: "[install [--project] | show]",
9265
+ summary: "teach Claude Code airgate: install the airgate skill (~/.claude/skills, or this project with --project)",
9266
+ example: "airgate skill install",
9267
+ public: true,
9268
+ flags: { project: { type: "boolean", help: "install into ./.claude/skills (shared with the project) instead of ~/.claude/skills" } },
9269
+ async run(ctx) {
9270
+ const action = ctx.args.positionals[0] ?? "show";
9271
+ const url = `${ctx.cfg.app}/skills/airgate/SKILL.md`;
9272
+ const res = await ctx.ui.spin("fetching the airgate skill", ctx.fetch(url));
9273
+ if (!res.ok) throw new CliError(`couldn't fetch ${url} (${res.status})`);
9274
+ const text2 = await res.text();
9275
+ if (!text2.startsWith("---")) throw new CliError(`${url} isn't a skill file`);
9276
+ if (action === "show") return void ctx.io.stdout.write(text2);
9277
+ if (action !== "install") throw new UsageError(`unknown action "${action}"`, "airgate skill install [--project] | airgate skill show");
9278
+ const base = bool(ctx.args, "project") ? join5(ctx.io.cwd, ".claude") : join5(ctx.io.env.HOME ?? homedir3(), ".claude");
9279
+ const dir = join5(base, "skills", "airgate");
9280
+ mkdirSync4(dir, { recursive: true });
9281
+ writeFileSync5(join5(dir, "SKILL.md"), text2);
9282
+ if (ctx.json) return ctx.ui.json({ installed: join5(dir, "SKILL.md") });
9283
+ ctx.ui.out(`${ctx.ui.c("ok", "installed")} the airgate skill: ${join5(dir, "SKILL.md")}`);
9284
+ 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".'));
9285
+ }
9286
+ };
9287
+
9288
+ // src/commands/previews.ts
9289
+ var portArg = (v) => {
9290
+ const n2 = Number(v);
9291
+ if (!v || !Number.isInteger(n2) || n2 < 1 || n2 > 65535) throw new UsageError("a port, like 3000");
9292
+ return n2;
9293
+ };
9294
+ async function podAndPort(ctx) {
9295
+ const [ref, port] = ctx.args.positionals;
9296
+ if (!ref) throw new UsageError("which pod?");
9297
+ return { pod: await resolvePod(ctx, ref), port: portArg(port) };
9298
+ }
9299
+ var ports = {
9300
+ name: "ports",
9301
+ aliases: ["previews"],
9302
+ group: "pods",
9303
+ usage: "<pod>",
9304
+ summary: "what's listening in a pod, and its preview links",
9305
+ example: "airgate ports web",
9306
+ async run(ctx) {
9307
+ const ref = ctx.args.positionals[0];
9308
+ if (!ref) throw new UsageError("which pod? airgate ports <pod>");
9309
+ const pod = await resolvePod(ctx, ref);
9310
+ const r = await ctx.api.previews(pod.id);
9311
+ if (ctx.json) return ctx.ui.json(r);
9312
+ if (!r.enabled) return void ctx.ui.out("previews are not set up on this gateway.");
9313
+ const byPort = new Map(r.previews.map((p) => [p.port, p]));
9314
+ const all = [.../* @__PURE__ */ new Set([...r.ports, ...r.previews.map((p) => p.port)])].sort((a, b2) => a - b2);
9315
+ 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>`);
9316
+ ctx.ui.table(
9317
+ all.map((port) => {
9318
+ const p = byPort.get(port);
9319
+ 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 : ""];
9320
+ }),
9321
+ { head: ["port", "", "preview", "link"] }
9322
+ );
9323
+ ctx.ui.out(ctx.ui.c("dim", `open one: airgate preview ${pod.spec.name} <port> \xB7 share it: airgate share ${pod.spec.name} <port>`));
9324
+ }
9325
+ };
9326
+ var preview = {
9327
+ name: "preview",
9328
+ group: "pods",
9329
+ usage: "<pod> <port> [/path] [--no-open]",
9330
+ summary: "open a port of a pod in your browser (private to you)",
9331
+ example: "airgate preview web 3000",
9332
+ flags: { "no-open": { type: "boolean", help: "print the link instead (it signs you in, once, within a minute)" } },
9333
+ async run(ctx) {
9334
+ const { pod, port } = await podAndPort(ctx);
9335
+ const path = ctx.args.positionals[2] ?? "/";
9336
+ const p = await ctx.api.ensurePreview(pod.id, { port });
9337
+ const url = await ctx.api.openPreview(p.slug, path.startsWith("/") ? path : `/${path}`);
9338
+ if (ctx.json) return ctx.ui.json({ preview: p, open: url });
9339
+ await openOrPrint(ctx, url, `${pod.spec.name}:${port}`);
9340
+ }
9341
+ };
9342
+ var share = {
9343
+ name: "share",
9344
+ group: "pods",
9345
+ usage: "<pod> <port> [--off]",
9346
+ summary: "a public link to a port of a pod (anyone with it can open it); --off makes it private again",
9347
+ example: "airgate share web 3000",
9348
+ flags: { off: { type: "boolean", help: "stop sharing: private to you again" } },
9349
+ async run(ctx) {
9350
+ const { pod, port } = await podAndPort(ctx);
9351
+ const off = bool(ctx.args, "off");
9352
+ const p = await ctx.api.ensurePreview(pod.id, { port, public: !off });
9353
+ if (ctx.json) return ctx.ui.json(p);
9354
+ if (off) ctx.ui.out(`${pod.spec.name}:${port} is private again: only you can open it (airgate preview ${pod.spec.name} ${port}).`);
9355
+ else {
9356
+ ctx.ui.out(p.url);
9357
+ 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`));
9358
+ }
9359
+ }
9360
+ };
8856
9361
 
8857
9362
  // src/commands/undo.ts
8858
9363
  var newestFirst = (cps) => [...cps].sort((a, b2) => b2.at - a.at);
@@ -9033,15 +9538,15 @@ var Canvas = class {
9033
9538
  /** write text at (x, y), clipped to maxWidth and the canvas; returns columns written */
9034
9539
  text(x, y, s2, st = {}, maxWidth = Infinity) {
9035
9540
  if (y < 0 || y >= this.rows) return 0;
9036
- let n = 0;
9541
+ let n2 = 0;
9037
9542
  for (const ch of s2.replace(/[\u0000-\u001f\u007f]/g, " ")) {
9038
- if (n >= maxWidth) break;
9039
- const cx = x + n;
9543
+ if (n2 >= maxWidth) break;
9544
+ const cx = x + n2;
9040
9545
  if (cx >= this.cols) break;
9041
9546
  if (cx >= 0) this.cells[y][cx] = { ch, st };
9042
- n++;
9547
+ n2++;
9043
9548
  }
9044
- return n;
9549
+ return n2;
9045
9550
  }
9046
9551
  fill(x, y, w, h, st = {}) {
9047
9552
  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 +9751,7 @@ function formKey(s2, form, key) {
9246
9751
  }
9247
9752
  return { state: { ...s2, modal: { kind: "form", form: next } }, effects: [] };
9248
9753
  };
9249
- const cycle2 = (n, len, d2) => (n + d2 + len) % len;
9754
+ const cycle2 = (n2, len, d2) => (n2 + d2 + len) % len;
9250
9755
  switch (key) {
9251
9756
  case "tab":
9252
9757
  case "down":
@@ -9338,10 +9843,10 @@ function modalKey(s2, m, key) {
9338
9843
  return none;
9339
9844
  }
9340
9845
  case "timeline": {
9341
- const n = m.checkpoints?.length ?? 0;
9846
+ const n2 = m.checkpoints?.length ?? 0;
9342
9847
  if (key === "esc" || key === "q" || key === "u") return close;
9343
- if ((key === "down" || key === "j") && n) return { state: { ...s2, modal: { ...m, sel: Math.min(n - 1, m.sel + 1) } }, effects: [] };
9344
- if ((key === "up" || key === "k") && n) return { state: { ...s2, modal: { ...m, sel: Math.max(0, m.sel - 1) } }, effects: [] };
9848
+ if ((key === "down" || key === "j") && n2) return { state: { ...s2, modal: { ...m, sel: Math.min(n2 - 1, m.sel + 1) } }, effects: [] };
9849
+ if ((key === "up" || key === "k") && n2) return { state: { ...s2, modal: { ...m, sel: Math.max(0, m.sel - 1) } }, effects: [] };
9345
9850
  const cp = m.checkpoints?.[m.sel];
9346
9851
  if (key === "enter" && cp) {
9347
9852
  return {
@@ -9649,16 +10154,16 @@ function draw(s2, cols, rows2) {
9649
10154
  }
9650
10155
  function header(c, s2) {
9651
10156
  let x = c.text(0, 0, " \u22A0 airgate", { bold: true }) + 2;
9652
- const n = pendingApprovals(s2).length;
10157
+ const n2 = pendingApprovals(s2).length;
9653
10158
  const tabs = [
9654
10159
  ["pods", "pods"],
9655
- ["airlock", n ? `airlock [${n}]` : "airlock"],
10160
+ ["airlock", n2 ? `airlock [${n2}]` : "airlock"],
9656
10161
  ["usage", "usage"]
9657
10162
  ];
9658
10163
  for (const [id, label2] of tabs) {
9659
10164
  if (x + label2.length + 2 > c.cols) break;
9660
10165
  const active = s2.screen === id;
9661
- x += c.text(x, 0, ` ${label2} `, { inverse: active, bold: active, fg: id === "airlock" && n && !active ? "warn" : void 0 });
10166
+ x += c.text(x, 0, ` ${label2} `, { inverse: active, bold: active, fg: id === "airlock" && n2 && !active ? "warn" : void 0 });
9662
10167
  x += 1;
9663
10168
  }
9664
10169
  const right = s2.connected ? "\u25CF live" : s2.loaded ? "\u25CB offline" : "\u25CB connecting";
@@ -9704,9 +10209,9 @@ function splash(c, s2) {
9704
10209
  }
9705
10210
  function statusLine(c, s2) {
9706
10211
  const y = c.rows - 1;
9707
- const n = pendingApprovals(s2).length;
10212
+ const n2 = pendingApprovals(s2).length;
9708
10213
  const parts = [];
9709
- if (n) parts.push(`airlock [${n}]`);
10214
+ if (n2) parts.push(`airlock [${n2}]`);
9710
10215
  if (s2.me) parts.push(s2.me.plan === "none" ? "no plan" : s2.me.plan, `${hrs(s2.me.usedHours)} this month`);
9711
10216
  const right = parts.join(" \xB7 ");
9712
10217
  let left;
@@ -9725,8 +10230,8 @@ function statusLine(c, s2) {
9725
10230
  c.text(0, y, " " + clip(left, Math.max(0, c.cols - rw - 2)), { fg: leftFg });
9726
10231
  if (right && c.cols - rw > 10) {
9727
10232
  const x = c.cols - rw;
9728
- if (n) {
9729
- const badge = `airlock [${n}]`;
10233
+ if (n2) {
10234
+ const badge = `airlock [${n2}]`;
9730
10235
  c.text(x, y, badge, { fg: "warn", bold: true });
9731
10236
  c.text(x + badge.length, y, right.slice(badge.length), { fg: "dim" });
9732
10237
  } else c.text(x, y, right, { fg: "dim" });
@@ -10029,11 +10534,11 @@ function modal(c, s2, m) {
10029
10534
  return;
10030
10535
  }
10031
10536
  case "timeline": {
10032
- const n = m.checkpoints?.length ?? 0;
10033
- const b2 = centered(c, 64, Math.max(6, Math.min(n, 14) + 4));
10537
+ const n2 = m.checkpoints?.length ?? 0;
10538
+ const b2 = centered(c, 64, Math.max(6, Math.min(n2, 14) + 4));
10034
10539
  c.box(b2.x, b2.y, b2.w, b2.h, { double: true, title: `undo \xB7 ${m.name}`, clear: true });
10035
10540
  if (!m.checkpoints) c.text(b2.x + 2, b2.y + 1, "loading\u2026", { fg: "dim" });
10036
- else if (!n) c.text(b2.x + 2, b2.y + 1, "no checkpoints yet. c takes one.", { fg: "dim" }, b2.w - 4);
10541
+ else if (!n2) c.text(b2.x + 2, b2.y + 1, "no checkpoints yet. c takes one.", { fg: "dim" }, b2.w - 4);
10037
10542
  const rowsAvail = b2.h - 3;
10038
10543
  const start = Math.max(0, m.sel - rowsAvail + 1);
10039
10544
  (m.checkpoints ?? []).slice(start, start + rowsAvail).forEach((cp, i) => {
@@ -10236,7 +10741,7 @@ function startTui(d) {
10236
10741
  scheduleDraw();
10237
10742
  }
10238
10743
  };
10239
- const onSignal = () => finish(130);
10744
+ const onSignal = () => finish2(130);
10240
10745
  const onCrash = (e) => crash(e);
10241
10746
  function listenKeys(on) {
10242
10747
  if (on) {
@@ -10256,7 +10761,7 @@ function startTui(d) {
10256
10761
  d.stdin.pause();
10257
10762
  d.stdout.write(LEAVE_ALT);
10258
10763
  }
10259
- function finish(code3, message) {
10764
+ function finish2(code3, message) {
10260
10765
  if (finished) return;
10261
10766
  finished = true;
10262
10767
  controller.abort();
@@ -10271,7 +10776,7 @@ function startTui(d) {
10271
10776
  resolveDone(code3);
10272
10777
  }
10273
10778
  function crash(e) {
10274
- finish(1, `airgate ui crashed: ${e instanceof Error ? e.message : String(e)}`);
10779
+ finish2(1, `airgate ui crashed: ${e instanceof Error ? e.message : String(e)}`);
10275
10780
  }
10276
10781
  const errText = (e) => {
10277
10782
  if (e instanceof ApiError) {
@@ -10300,7 +10805,7 @@ function startTui(d) {
10300
10805
  try {
10301
10806
  switch (e.type) {
10302
10807
  case "quit":
10303
- return finish(0);
10808
+ return finish2(0);
10304
10809
  case "attach":
10305
10810
  return await handoff(e.podId, e.name);
10306
10811
  case "loadUsage": {
@@ -10538,6 +11043,9 @@ var COMMANDS = [
10538
11043
  task,
10539
11044
  runs,
10540
11045
  log,
11046
+ ports,
11047
+ preview,
11048
+ share,
10541
11049
  checkpoints,
10542
11050
  checkpoint,
10543
11051
  rewind,
@@ -10548,6 +11056,7 @@ var COMMANDS = [
10548
11056
  mask,
10549
11057
  up,
10550
11058
  down2,
11059
+ runTask,
10551
11060
  usage,
10552
11061
  plan,
10553
11062
  upgrade,
@@ -10556,6 +11065,7 @@ var COMMANDS = [
10556
11065
  ui,
10557
11066
  mcp,
10558
11067
  open,
11068
+ skill,
10559
11069
  status,
10560
11070
  help,
10561
11071
  version
@@ -10598,7 +11108,7 @@ function printCommandHelp(ui2, c) {
10598
11108
  ui2.out(`usage: airgate ${c.name}${c.usage ? " " + c.usage : ""}`);
10599
11109
  ui2.out();
10600
11110
  ui2.out(c.summary + ".");
10601
- for (const n of c.notes ?? []) ui2.out(n);
11111
+ for (const n2 of c.notes ?? []) ui2.out(n2);
10602
11112
  const flags = flagLines({ ...c.flags ?? {}, json: GLOBAL_FLAGS.json });
10603
11113
  ui2.out();
10604
11114
  for (const l of formatTable(flags, void 0, 2)) ui2.out(l);
@@ -10733,7 +11243,7 @@ async function run(argv, io, opts2 = {}) {
10733
11243
  }
10734
11244
  }
10735
11245
  function suggest2(name) {
10736
- const near = COMMANDS.filter((c) => [c.name, ...c.aliases ?? []].some((n) => n.startsWith(name.slice(0, 2)))).map((c) => c.name);
11246
+ const near = COMMANDS.filter((c) => [c.name, ...c.aliases ?? []].some((n2) => n2.startsWith(name.slice(0, 2)))).map((c) => c.name);
10737
11247
  return near.length ? `did you mean: ${near.slice(0, 3).join(", ")}? (airgate help)` : "see: airgate help";
10738
11248
  }
10739
11249