@tablation/crew 0.83.0 → 0.84.0

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 (2) hide show
  1. package/dist/cli.js +61 -5
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -34,6 +34,29 @@ function doctorBaseReport(r, base, health) {
34
34
  }
35
35
  }
36
36
 
37
+ // src/periscopes.ts
38
+ import { lookup } from "dns/promises";
39
+ function periscopeRoutes(cfg) {
40
+ return cfg.routes.filter((r) => r.enabled && r.hostPeriscopes).map((r) => r.route);
41
+ }
42
+ function periscopeSummary(cfg) {
43
+ const routes = periscopeRoutes(cfg);
44
+ const limits = `max ${cfg.ship.maxPeriscopes}, idle ${cfg.ship.periscopeIdleMinutes} min`;
45
+ if (routes.length === 0) return `off (no enabled route sets hostPeriscopes: true; ${limits})`;
46
+ return `ON for ${routes.join(", ")} (host ${cfg.ship.previewHost ?? "NOT SET (ship.previewHost)"}, ${limits})`;
47
+ }
48
+ async function previewHostProblem(cfg, resolve6 = (h) => lookup(h)) {
49
+ if (periscopeRoutes(cfg).length === 0) return void 0;
50
+ const host = cfg.ship.previewHost;
51
+ if (!host) return "ship.previewHost is not set \u2014 a route hosts periscopes, so Passengers have no host to be sent to";
52
+ try {
53
+ await resolve6(host);
54
+ } catch {
55
+ return `ship.previewHost "${host}" does not resolve from this machine`;
56
+ }
57
+ return void 0;
58
+ }
59
+
37
60
  // src/config.ts
38
61
  import { readFileSync as readFileSync8, existsSync as existsSync7 } from "fs";
39
62
  import { resolve, dirname as dirname3, isAbsolute, join as join9, basename as basename3 } from "path";
@@ -2034,6 +2057,8 @@ var ROLE_LABEL = {
2034
2057
  };
2035
2058
  var ConfigError = class extends Error {
2036
2059
  };
2060
+ var DEFAULT_MAX_PERISCOPES = 3;
2061
+ var DEFAULT_PERISCOPE_IDLE_MINUTES = 120;
2037
2062
  function parseCloneScheme(v) {
2038
2063
  if (v === void 0 || v === null || v === "") return void 0;
2039
2064
  if (v === "ssh" || v === "https") return v;
@@ -2065,7 +2090,10 @@ var SHIP_KEYS = /* @__PURE__ */ new Set([
2065
2090
  "relayHttpPort",
2066
2091
  "relayPublicDomain",
2067
2092
  "passengerImage",
2068
- "cloneScheme"
2093
+ "cloneScheme",
2094
+ "previewHost",
2095
+ "maxPeriscopes",
2096
+ "periscopeIdleMinutes"
2069
2097
  ]);
2070
2098
  var ROUTE_KEYS = /* @__PURE__ */ new Set([
2071
2099
  "route",
@@ -2088,6 +2116,7 @@ var ROUTE_KEYS = /* @__PURE__ */ new Set([
2088
2116
  "resolved",
2089
2117
  "promptSet",
2090
2118
  "hostPassengers",
2119
+ "hostPeriscopes",
2091
2120
  "toolchain"
2092
2121
  ]);
2093
2122
  function parseWorktreeName(v, where, file) {
@@ -2399,6 +2428,14 @@ Create the first of those:
2399
2428
  if (!Number.isInteger(streamRetentionDays) || streamRetentionDays < 1) {
2400
2429
  missing.add(`ship.streamRetentionDays must be a positive integer (got "${shipRaw.streamRetentionDays}")`);
2401
2430
  }
2431
+ const maxPeriscopes = shipRaw.maxPeriscopes === void 0 ? DEFAULT_MAX_PERISCOPES : Number(shipRaw.maxPeriscopes);
2432
+ if (!Number.isInteger(maxPeriscopes) || maxPeriscopes < 1) {
2433
+ missing.add(`ship.maxPeriscopes must be a positive integer (got "${shipRaw.maxPeriscopes}")`);
2434
+ }
2435
+ const periscopeIdleMinutes = shipRaw.periscopeIdleMinutes === void 0 ? DEFAULT_PERISCOPE_IDLE_MINUTES : Number(shipRaw.periscopeIdleMinutes);
2436
+ if (!Number.isInteger(periscopeIdleMinutes) || periscopeIdleMinutes < 1) {
2437
+ missing.add(`ship.periscopeIdleMinutes must be a positive integer (got "${shipRaw.periscopeIdleMinutes}")`);
2438
+ }
2402
2439
  const eventsRotateBytes = shipRaw.eventsRotateBytes === void 0 ? DEFAULT_EVENTS_ROTATE_BYTES : Number(shipRaw.eventsRotateBytes);
2403
2440
  if (!Number.isInteger(eventsRotateBytes) || eventsRotateBytes < 1) {
2404
2441
  missing.add(`ship.eventsRotateBytes must be a positive integer (got "${shipRaw.eventsRotateBytes}")`);
@@ -2468,7 +2505,10 @@ Create the first of those:
2468
2505
  relayHttpPort: shipRaw.relayHttpPort !== void 0 ? Number(shipRaw.relayHttpPort) : 443,
2469
2506
  relayPublicDomain: shipRaw.relayPublicDomain ? String(shipRaw.relayPublicDomain) : void 0,
2470
2507
  passengerImage: shipRaw.passengerImage ? String(shipRaw.passengerImage) : void 0,
2471
- cloneScheme: parseCloneScheme(shipRaw.cloneScheme)
2508
+ cloneScheme: parseCloneScheme(shipRaw.cloneScheme),
2509
+ previewHost: shipRaw.previewHost ? String(shipRaw.previewHost).trim() || void 0 : void 0,
2510
+ maxPeriscopes,
2511
+ periscopeIdleMinutes
2472
2512
  },
2473
2513
  routes,
2474
2514
  crewHome,
@@ -2569,6 +2609,9 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
2569
2609
  missing.add(`${where}.toolchain "${toolchain}" is not defined in ship.toolchains (have: ${[...toolchainNames].join(", ")})`);
2570
2610
  }
2571
2611
  const route = missing.req(c?.route, `${where}.route`);
2612
+ if (c?.hostPeriscopes === true && c?.hostPassengers !== true) {
2613
+ missing.add(`${where} (route "${route}") sets hostPeriscopes: true without hostPassengers: true \u2014 a Passenger must be able to board before they can open a periscope`);
2614
+ }
2572
2615
  const routeParts = route ? route.split("/") : [];
2573
2616
  const routeWellFormed = routeParts.length === 2 && routeParts.every(Boolean);
2574
2617
  if (route && !routeWellFormed) {
@@ -2643,7 +2686,8 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
2643
2686
  },
2644
2687
  release: parseReleaseOverride(c?.release, where, file),
2645
2688
  branch: parseBranchOverride(c?.branch, where, file),
2646
- hostPassengers: c?.hostPassengers === true
2689
+ hostPassengers: c?.hostPassengers === true,
2690
+ hostPeriscopes: c?.hostPeriscopes === true
2647
2691
  };
2648
2692
  const resolvedFile = routeWellFormed ? readResolvedFile(stateDir, route) : void 0;
2649
2693
  result.resolved = parseResolved(resolvedFile ?? c?.resolved, missing, where);
@@ -7762,7 +7806,7 @@ function skillMarkerNames(text) {
7762
7806
  }
7763
7807
  return names;
7764
7808
  }
7765
- function expandSkillMarkers(text, lookup, opts = {}) {
7809
+ function expandSkillMarkers(text, lookup2, opts = {}) {
7766
7810
  const maxDepth = opts.maxDepth ?? MAX_INCLUDE_DEPTH;
7767
7811
  const maxBytes = opts.maxBytes ?? MAX_INCLUDED_BYTES;
7768
7812
  const warnings = [];
@@ -7778,7 +7822,7 @@ function expandSkillMarkers(text, lookup, opts = {}) {
7778
7822
  warnings.push(`skill include "${name}" left literal: nested deeper than ${maxDepth} levels`);
7779
7823
  return literal;
7780
7824
  }
7781
- const body = lookup(name);
7825
+ const body = lookup2(name);
7782
7826
  if (body === void 0) throw new SkillIncludeError(`no skill named "${name}" for the include ${literal}`);
7783
7827
  const trimmed = body.replace(/^\n+/, "").replace(/\n+$/, "");
7784
7828
  const bytes = Buffer.byteLength(trimmed, "utf8");
@@ -20385,6 +20429,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20385
20429
  platform: cfg.ship.platform,
20386
20430
  machineId,
20387
20431
  hostPassengers: firstRun?.answers.hostPassengers ?? existingRoute?.hostPassengers === true,
20432
+ hostPeriscopes: existingRoute?.hostPeriscopes === true,
20388
20433
  sshPublicKey,
20389
20434
  lastSyncedName: (r) => readSyncedName(resolvedPathFor(cfg.ship.stateDir, r))
20390
20435
  };
@@ -21848,6 +21893,7 @@ ${owed.map((l) => ` ${l}
21848
21893
  `ship: ${cfg.ship.name} (${cfg.ship.platform})
21849
21894
  route: ${route.route} -> ${route.dir}
21850
21895
  enabled: ${route.enabled}
21896
+ periscopes: ${periscopeSummary(cfg)}
21851
21897
  crew: ${state.isPaused() ? "paused" : "active"}
21852
21898
  release: ${state.releaseSkipReason() ? "paused" : "active"}
21853
21899
  watermark: ${state.watermark()}
@@ -21965,6 +22011,8 @@ watermark: ${state.watermark()}
21965
22011
  }
21966
22012
  }
21967
22013
  for (const message of roleCapWarnings(cfg.ship)) problems.push({ severity: "note", kind: "role_cap_above_ship_cap", message });
22014
+ const previewProblem2 = await previewHostProblem(cfg);
22015
+ if (previewProblem2) problems.push({ severity: "blocker", kind: "preview_host", message: previewProblem2 });
21968
22016
  problems.push(...attentionProblems(openShipAttention(state), problems));
21969
22017
  if (problems.some((p) => p.severity === "blocker")) process.exitCode = 1;
21970
22018
  process.stdout.write(`${JSON.stringify({ offline: flag("offline"), pushAuth, tools: toolRows, problems, passed }, null, 2)}
@@ -21978,6 +22026,14 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
21978
22026
  );
21979
22027
  for (const w of roleCapWarnings(cfg.ship)) process.stdout.write(`role concurrency: WARN ${w}
21980
22028
  `);
22029
+ process.stdout.write(`periscopes: ${periscopeSummary(cfg)}
22030
+ `);
22031
+ const previewProblem = await previewHostProblem(cfg);
22032
+ if (previewProblem) {
22033
+ process.stdout.write(` BLOCKER ${previewProblem}
22034
+ `);
22035
+ process.exitCode = 1;
22036
+ }
21981
22037
  try {
21982
22038
  const evFile = eventFileFor(cfg.ship.stateDir);
21983
22039
  const evSize = existsSync35(evFile) ? statSync10(evFile).size : 0;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.83.0",
3
+ "version": "0.84.0",
4
4
  "description": "A standing team of headless agents that picks work off a Tablation board, does it on a machine you control, and reports back.",
5
5
  "type": "module",
6
6
  "license": "MIT",