@tablation/crew 0.109.0 → 0.111.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 +112 -27
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1800,6 +1800,29 @@ function answerCredential(action, input, forges, stateDir, env = process.env) {
1800
1800
  password=${token}
1801
1801
  `;
1802
1802
  }
1803
+ var missesPath = (stateDir) => join9(stateDir, "forge-misses.json");
1804
+ function readForgeMisses(stateDir) {
1805
+ try {
1806
+ const raw = JSON.parse(readFileSync8(missesPath(stateDir), "utf8"));
1807
+ return Object.entries(raw).filter(([, t]) => typeof t === "string").map(([host, lastMissAt]) => ({ host, lastMissAt })).sort((a, b) => a.host.localeCompare(b.host));
1808
+ } catch {
1809
+ return [];
1810
+ }
1811
+ }
1812
+ function noteForgeMiss(stateDir, host, at) {
1813
+ try {
1814
+ const bare = bareHost(host);
1815
+ const cur = {};
1816
+ for (const m of readForgeMisses(stateDir)) cur[m.host] = m.lastMissAt;
1817
+ if (at === null) {
1818
+ if (!(bare in cur)) return;
1819
+ delete cur[bare];
1820
+ } else cur[bare] = at;
1821
+ mkdirSync5(stateDir, { recursive: true });
1822
+ writeFileSync6(missesPath(stateDir), JSON.stringify(cur), { mode: 384 });
1823
+ } catch {
1824
+ }
1825
+ }
1803
1826
  function gitCredentialEnv(o, base = process.env) {
1804
1827
  const start = Number.parseInt(base.GIT_CONFIG_COUNT ?? "0", 10) || 0;
1805
1828
  const helper = `!${o.crewCommand} credential`;
@@ -10767,7 +10790,7 @@ async function reconcileRuns(o) {
10767
10790
  }
10768
10791
 
10769
10792
  // src/live-runs.ts
10770
- import { existsSync as existsSync23, readdirSync as readdirSync10 } from "fs";
10793
+ import { existsSync as existsSync23, readdirSync as readdirSync10, statSync as statSync8 } from "fs";
10771
10794
  import { join as join25 } from "path";
10772
10795
  function liveRuns(stateDir, isAlive = runIsAlive) {
10773
10796
  const dir = join25(stateDir, "streams");
@@ -10783,6 +10806,23 @@ function liveRuns(stateDir, isAlive = runIsAlive) {
10783
10806
  }
10784
10807
  return out.sort((a, b) => a.spawnedAt.localeCompare(b.spawnedAt));
10785
10808
  }
10809
+ function runActivity(stateDir, isAlive = runIsAlive) {
10810
+ const activeRuns = liveRuns(stateDir, isAlive).length;
10811
+ const dir = join25(stateDir, "streams");
10812
+ let newest = 0;
10813
+ if (existsSync23(dir)) {
10814
+ for (const name of readdirSync10(dir)) {
10815
+ if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
10816
+ const streamPath = join25(dir, name);
10817
+ if (!runFinished(streamPath)) continue;
10818
+ try {
10819
+ newest = Math.max(newest, statSync8(`${streamPath}.meta.json`).mtimeMs);
10820
+ } catch {
10821
+ }
10822
+ }
10823
+ }
10824
+ return { activeRuns, lastRunEndedAt: newest ? new Date(newest).toISOString() : null };
10825
+ }
10786
10826
  function siblingRuns(runs, ownTicketRecordId) {
10787
10827
  return runs.filter((r) => !ownTicketRecordId || r.ticketRecordId !== ownTicketRecordId);
10788
10828
  }
@@ -12572,7 +12612,7 @@ async function signOutSessions(o) {
12572
12612
  }
12573
12613
 
12574
12614
  // src/reset.ts
12575
- import { existsSync as existsSync26, mkdirSync as mkdirSync17, readFileSync as readFileSync24, readdirSync as readdirSync12, rmSync as rmSync16, statSync as statSync8, writeFileSync as writeFileSync18 } from "fs";
12615
+ import { existsSync as existsSync26, mkdirSync as mkdirSync17, readFileSync as readFileSync24, readdirSync as readdirSync12, rmSync as rmSync16, statSync as statSync9, writeFileSync as writeFileSync18 } from "fs";
12576
12616
  import { dirname as dirname12, join as join29 } from "path";
12577
12617
  import { hostFromUrl as hostFromUrl3 } from "@tablation/client";
12578
12618
  function clonedCheckoutsFile(stateDir) {
@@ -12670,7 +12710,7 @@ function removeState(stateDir, dryRun) {
12670
12710
  const targets = [join29(stateDir, "resolved"), join29(stateDir, "keys"), clonedCheckoutsFile(stateDir), join29(stateDir, "connect-session.json"), join29(stateDir, "reload-requested"), join29(stateDir, "daemon.started")];
12671
12711
  try {
12672
12712
  for (const name of readdirSync12(stateDir)) {
12673
- if (name.startsWith(".") && statSync8(join29(stateDir, name)).isFile()) targets.push(join29(stateDir, name));
12713
+ if (name.startsWith(".") && statSync9(join29(stateDir, name)).isFile()) targets.push(join29(stateDir, name));
12674
12714
  }
12675
12715
  } catch {
12676
12716
  }
@@ -12798,6 +12838,7 @@ async function runPoolMember(deps) {
12798
12838
  }
12799
12839
  };
12800
12840
  let reported;
12841
+ let busy = false;
12801
12842
  const report = async (state, o = {}) => {
12802
12843
  const body = {
12803
12844
  state,
@@ -12807,6 +12848,13 @@ async function runPoolMember(deps) {
12807
12848
  reason: o.reason ?? null,
12808
12849
  crewVersion: deps.host.crewVersion
12809
12850
  };
12851
+ const act = state === "running" ? deps.activity?.() : void 0;
12852
+ if (act) {
12853
+ body.activeRuns = act.activeRuns;
12854
+ body.lastRunEndedAt = act.lastRunEndedAt;
12855
+ }
12856
+ if (act) busy = act.activeRuns > 0;
12857
+ if (state === "running" && deps.missingForgeHosts) body.missingForgeHosts = deps.missingForgeHosts();
12810
12858
  const r = await settle("status", () => deps.api.status(body));
12811
12859
  if (!r) return "stopped";
12812
12860
  if (r.status === 401) return "credential";
@@ -12902,7 +12950,25 @@ async function runPoolMember(deps) {
12902
12950
  if (!current) await deps.maybeSelfUpgrade?.().catch((e) => {
12903
12951
  deps.log(`self-upgrade: ${e.message}`);
12904
12952
  });
12905
- const ans = await settle("lease", () => deps.api.lease(platform2.pollWaitSeconds || DEFAULT_POLL_WAIT_SECONDS, current ?? void 0));
12953
+ let stopWatch;
12954
+ let flipping = false;
12955
+ if (current && deps.watch && deps.activity) {
12956
+ const held = current;
12957
+ stopWatch = deps.watch(() => {
12958
+ const now = deps.activity().activeRuns > 0;
12959
+ if (flipping || now === busy) return;
12960
+ flipping = true;
12961
+ void report("running", { leaseId: held }).catch(() => void 0).finally(() => {
12962
+ flipping = false;
12963
+ });
12964
+ });
12965
+ }
12966
+ let ans;
12967
+ try {
12968
+ ans = await settle("lease", () => deps.api.lease(platform2.pollWaitSeconds || DEFAULT_POLL_WAIT_SECONDS, current ?? void 0));
12969
+ } finally {
12970
+ stopWatch?.();
12971
+ }
12906
12972
  if (!ans) return POOL_EXIT.ok;
12907
12973
  if (ans.status === 401) {
12908
12974
  deps.log("the pool credential is unknown or revoked (401)");
@@ -12915,10 +12981,10 @@ async function runPoolMember(deps) {
12915
12981
  }
12916
12982
  if (ans.status === 204) {
12917
12983
  const url = current ? deps.relayUrl() : null;
12918
- if (current && url && url !== relayReported) {
12984
+ if (current && (url !== relayReported && url || deps.activity || deps.missingForgeHosts)) {
12919
12985
  const r2 = await report("running", { leaseId: current });
12920
12986
  if (r2 === "credential") return POOL_EXIT.credential;
12921
- relayReported = url;
12987
+ relayReported = url ?? relayReported;
12922
12988
  }
12923
12989
  continue;
12924
12990
  }
@@ -13992,6 +14058,20 @@ function realPoolMemberDeps(o) {
13992
14058
  }
13993
14059
  return null;
13994
14060
  },
14061
+ missingForgeHosts() {
14062
+ const cfg = loadShip(o.crewHome);
14063
+ return cfg ? readForgeMisses(cfg.ship.stateDir) : [];
14064
+ },
14065
+ activity() {
14066
+ const cfg = loadShip(o.crewHome);
14067
+ return cfg ? runActivity(cfg.ship.stateDir) : { activeRuns: 0, lastRunEndedAt: null };
14068
+ },
14069
+ watch(onTick) {
14070
+ const t = setInterval(onTick, 2e3);
14071
+ return () => {
14072
+ clearInterval(t);
14073
+ };
14074
+ },
13995
14075
  sleep: (ms3) => new Promise((r) => setTimeout(r, ms3)),
13996
14076
  log: o.log,
13997
14077
  stopping: o.stopping
@@ -17580,7 +17660,12 @@ function formatProjectsReport(report) {
17580
17660
  import { parseDocument as parseDocument4 } from "yaml";
17581
17661
  function runCredential(action, stdin, forges, stateDir, env = process.env) {
17582
17662
  try {
17583
- return { code: 0, out: answerCredential(action ?? "", parseCredentialInput(stdin), forges, stateDir, env), err: "" };
17663
+ const input = parseCredentialInput(stdin);
17664
+ const out = answerCredential(action ?? "", input, forges, stateDir, env);
17665
+ if (action === "get" && input.host && (!input.protocol || input.protocol === "https")) {
17666
+ noteForgeMiss(stateDir, input.host, out ? null : (/* @__PURE__ */ new Date()).toISOString());
17667
+ }
17668
+ return { code: 0, out, err: "" };
17584
17669
  } catch (e) {
17585
17670
  return { code: 0, out: "", err: `crew credential: ${e.message}
17586
17671
  ` };
@@ -17811,7 +17896,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
17811
17896
  }
17812
17897
 
17813
17898
  // src/watch.ts
17814
- import { watch as watchFile, existsSync as existsSync34, statSync as statSync9, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
17899
+ import { watch as watchFile, existsSync as existsSync34, statSync as statSync10, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
17815
17900
  var E = "\x1B[";
17816
17901
  var dim = (s) => `${E}2m${s}${E}0m`;
17817
17902
  var bold = (s) => `${E}1m${s}${E}0m`;
@@ -17913,7 +17998,7 @@ function renderHeaders(states, now = Date.now()) {
17913
17998
  }
17914
17999
  function readFrom(file, from) {
17915
18000
  if (!existsSync34(file)) return { events: [], offset: from };
17916
- const size = statSync9(file).size;
18001
+ const size = statSync10(file).size;
17917
18002
  if (size < from) return readFrom(file, 0);
17918
18003
  if (size === from) return { events: [], offset: from };
17919
18004
  const fd = openSync4(file, "r");
@@ -19913,7 +19998,7 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
19913
19998
  }
19914
19999
 
19915
20000
  // src/stream-sweep.ts
19916
- import { existsSync as existsSync38, readdirSync as readdirSync15, rmSync as rmSync18, statSync as statSync10 } from "fs";
20001
+ import { existsSync as existsSync38, readdirSync as readdirSync15, rmSync as rmSync18, statSync as statSync11 } from "fs";
19917
20002
  import { join as join40 } from "path";
19918
20003
  var DAY_MS2 = 24 * 60 * 60 * 1e3;
19919
20004
  function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
@@ -19922,7 +20007,7 @@ function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
19922
20007
  const cutoff = now - retentionDays * DAY_MS2;
19923
20008
  return readdirSync15(dir).map((name) => join40(dir, name)).filter((path) => {
19924
20009
  try {
19925
- return statSync10(path).mtimeMs < cutoff;
20010
+ return statSync11(path).mtimeMs < cutoff;
19926
20011
  } catch {
19927
20012
  return false;
19928
20013
  }
@@ -19950,7 +20035,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
19950
20035
  const cutoff = now - retentionDays * DAY_MS2;
19951
20036
  return readdirSync15(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join40(stateDir, name)).filter((path) => {
19952
20037
  try {
19953
- return statSync10(path).mtimeMs < cutoff;
20038
+ return statSync11(path).mtimeMs < cutoff;
19954
20039
  } catch {
19955
20040
  return false;
19956
20041
  }
@@ -19958,7 +20043,7 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
19958
20043
  }
19959
20044
 
19960
20045
  // src/cli.ts
19961
- import { realpathSync as realpathSync3, readFileSync as readFileSync40, existsSync as existsSync45, mkdirSync as mkdirSync32, writeFileSync as writeFileSync33, rmSync as rmSync23, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync6, statSync as statSync14, readdirSync as readdirSync20 } from "fs";
20046
+ import { realpathSync as realpathSync3, readFileSync as readFileSync40, existsSync as existsSync45, mkdirSync as mkdirSync32, writeFileSync as writeFileSync33, rmSync as rmSync23, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync6, statSync as statSync15, readdirSync as readdirSync20 } from "fs";
19962
20047
  import { createInterface } from "readline/promises";
19963
20048
 
19964
20049
  // src/periscope-cmd.ts
@@ -20386,7 +20471,7 @@ ${tail2(preview.output)}`);
20386
20471
 
20387
20472
  // src/periscope-idle.ts
20388
20473
  import { execFileSync as execFileSync10 } from "child_process";
20389
- import { statSync as statSync11 } from "fs";
20474
+ import { statSync as statSync12 } from "fs";
20390
20475
  import { join as join42 } from "path";
20391
20476
  var MAX_SCANNED_FILES = 2e4;
20392
20477
  function newestWorktreeMtime(root) {
@@ -20407,7 +20492,7 @@ function newestWorktreeMtime(root) {
20407
20492
  if (!rel) continue;
20408
20493
  if (++scanned > MAX_SCANNED_FILES) break;
20409
20494
  try {
20410
- const m = statSync11(join42(root, rel)).mtimeMs;
20495
+ const m = statSync12(join42(root, rel)).mtimeMs;
20411
20496
  if (newest === null || m > newest) newest = m;
20412
20497
  } catch {
20413
20498
  }
@@ -20935,7 +21020,7 @@ function describeCause(err3) {
20935
21020
  }
20936
21021
 
20937
21022
  // src/daemon.ts
20938
- import { existsSync as existsSync42, mkdirSync as mkdirSync30, readFileSync as readFileSync38, readdirSync as readdirSync17, rmSync as rmSync20, statSync as statSync12, writeFileSync as writeFileSync31 } from "fs";
21023
+ import { existsSync as existsSync42, mkdirSync as mkdirSync30, readFileSync as readFileSync38, readdirSync as readdirSync17, rmSync as rmSync20, statSync as statSync13, writeFileSync as writeFileSync31 } from "fs";
20939
21024
  import { join as join45 } from "path";
20940
21025
  import { randomBytes as randomBytes2 } from "crypto";
20941
21026
  var DEFAULT_FLOOR_MS = 5e3;
@@ -21016,7 +21101,7 @@ async function runDaemonLoop(o) {
21016
21101
  function latestMtime(path) {
21017
21102
  let stat2;
21018
21103
  try {
21019
- stat2 = statSync12(path);
21104
+ stat2 = statSync13(path);
21020
21105
  } catch {
21021
21106
  return 0;
21022
21107
  }
@@ -21040,7 +21125,7 @@ function reloadSentinelFor(stateDir) {
21040
21125
  }
21041
21126
  function readReloadRequestedAt(stateDir) {
21042
21127
  try {
21043
- return statSync12(reloadSentinelFor(stateDir)).mtimeMs;
21128
+ return statSync13(reloadSentinelFor(stateDir)).mtimeMs;
21044
21129
  } catch {
21045
21130
  return null;
21046
21131
  }
@@ -21065,7 +21150,7 @@ function readDaemonStarted(stateDir) {
21065
21150
  function configChangedAfter(files, startedAt) {
21066
21151
  for (const f of files) {
21067
21152
  try {
21068
- if (statSync12(f).mtimeMs > startedAt) return f;
21153
+ if (statSync13(f).mtimeMs > startedAt) return f;
21069
21154
  } catch {
21070
21155
  }
21071
21156
  }
@@ -21628,7 +21713,7 @@ import {
21628
21713
  readSync as readSync3,
21629
21714
  readdirSync as readdirSync18,
21630
21715
  rmSync as rmSync21,
21631
- statSync as statSync13,
21716
+ statSync as statSync14,
21632
21717
  watch
21633
21718
  } from "fs";
21634
21719
  import { createConnection } from "net";
@@ -21972,7 +22057,7 @@ function readRange(path, from, max) {
21972
22057
  }
21973
22058
  }
21974
22059
  function lastLines2(path, n) {
21975
- const st = statSync13(path);
22060
+ const st = statSync14(path);
21976
22061
  const cursorAt = (off) => encodeCursor({ dev: st.dev, ino: st.ino, off });
21977
22062
  if (n <= 0 || st.size === 0) return { lines: [], cursor: cursorAt(st.size), reset: false };
21978
22063
  let start = st.size;
@@ -22006,7 +22091,7 @@ function readTail(live, opts) {
22006
22091
  } else {
22007
22092
  const found = candidates(live, opts.rotated ?? false).find((p) => {
22008
22093
  try {
22009
- const s = statSync13(p);
22094
+ const s = statSync14(p);
22010
22095
  return s.ino === cur.ino && s.dev === cur.dev;
22011
22096
  } catch {
22012
22097
  return false;
@@ -22017,7 +22102,7 @@ function readTail(live, opts) {
22017
22102
  off = cur.off;
22018
22103
  } else reset = true;
22019
22104
  }
22020
- let size = statSync13(path).size;
22105
+ let size = statSync14(path).size;
22021
22106
  if (off > size) {
22022
22107
  off = 0;
22023
22108
  reset = true;
@@ -22025,12 +22110,12 @@ function readTail(live, opts) {
22025
22110
  if (off >= size && path !== live) {
22026
22111
  path = live;
22027
22112
  off = 0;
22028
- size = statSync13(path).size;
22113
+ size = statSync14(path).size;
22029
22114
  }
22030
22115
  const { text: text2 } = readRange(path, off, max);
22031
22116
  const cut = text2.lastIndexOf("\n");
22032
22117
  const whole = cut < 0 ? "" : text2.slice(0, cut + 1);
22033
- const st = statSync13(path);
22118
+ const st = statSync14(path);
22034
22119
  return {
22035
22120
  lines: whole.split("\n").filter((l) => l.length),
22036
22121
  cursor: encodeCursor({ dev: st.dev, ino: st.ino, off: off + Buffer.byteLength(whole) }),
@@ -28058,7 +28143,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
28058
28143
  }
28059
28144
  try {
28060
28145
  const evFile = eventFileFor(cfg.ship.stateDir);
28061
- const evSize = existsSync45(evFile) ? statSync14(evFile).size : 0;
28146
+ const evSize = existsSync45(evFile) ? statSync15(evFile).size : 0;
28062
28147
  process.stdout.write(
28063
28148
  evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
28064
28149
  ` : "events log: ok\n"
@@ -28092,7 +28177,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
28092
28177
  const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
28093
28178
  if (changed2) {
28094
28179
  process.stdout.write(
28095
- `daemon config: WARN ${changed2} changed at ${new Date(statSync14(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
28180
+ `daemon config: WARN ${changed2} changed at ${new Date(statSync15(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
28096
28181
  `
28097
28182
  );
28098
28183
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.109.0",
3
+ "version": "0.111.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",