@tablation/crew 0.77.0 → 0.78.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 (3) hide show
  1. package/README.md +1 -0
  2. package/dist/cli.js +191 -53
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -263,6 +263,7 @@ crew inbox [--member NAME] your tickets across every workspace
263
263
  crew connect WS[/PROJECT] resolve a workspace/project's ids into the state tree
264
264
  crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml, creating its Repos row if missing (--dry-run previews)
265
265
  crew repos list ROUTE the checkouts a route has, and the tracker Repos row each matches
266
+ crew repos check [PATH] validate a checkout's .crew.yaml (parse, release readiness, hook syntax) without attaching it (--route ROUTE, --strict, --json)
266
267
  crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--write route|repo [--replace] writes them, --clear route removes them, --recipes, --hook KEY=CMD, --agent, --json)
267
268
  crew install [--replace] write and load this platform's scheduler unit (macOS: clears a crew installed from another path)
268
269
  crew uninstall [--all] unload and remove it (--all: every crew launchd unit, whichever path it came from)
package/dist/cli.js CHANGED
@@ -9041,10 +9041,10 @@ function doctorSelftestLines(stored, toolchains) {
9041
9041
  }
9042
9042
 
9043
9043
  // src/cli.ts
9044
- import { resolve as resolve5, dirname as dirname20, join as join37, basename as basename11 } from "path";
9044
+ import { resolve as resolve5, dirname as dirname20, join as join37, basename as basename12 } from "path";
9045
9045
  import { fileURLToPath } from "url";
9046
9046
  import { hostname as hostname2, homedir as homedir10 } from "os";
9047
- import { spawnSync as spawnSync6 } from "child_process";
9047
+ import { spawnSync as spawnSync7 } from "child_process";
9048
9048
  import { parse as parseYaml2 } from "yaml";
9049
9049
 
9050
9050
  // src/argv.ts
@@ -14393,12 +14393,107 @@ async function showLogEntry(route, entryId, opts = {}) {
14393
14393
  };
14394
14394
  }
14395
14395
 
14396
+ // src/repos-check.ts
14397
+ import { spawnSync as spawnSync6 } from "child_process";
14398
+ import { readFileSync as readFileSync24 } from "fs";
14399
+ import { basename as basename8 } from "path";
14400
+ var POSIX_SHELL = /^(?:ba|z|da|k|a|mk)?sh$|^fish$/;
14401
+ function checkHookSyntax(script, shell) {
14402
+ const spec = resolveShell(shell);
14403
+ const name = basename8(spec.bin).replace(/\.exe$/i, "").toLowerCase();
14404
+ let run2;
14405
+ if (POSIX_SHELL.test(name)) {
14406
+ run2 = spawnSync6(spec.bin, ["-n"], { input: script, encoding: "utf8" });
14407
+ } else if (/^(pwsh|powershell)$/.test(name)) {
14408
+ const parse3 = "$e=$null;[void][System.Management.Automation.Language.Parser]::ParseInput($env:CREW_CHECK_SCRIPT,[ref]$null,[ref]$e);if($e){$e|ForEach-Object{[Console]::Error.WriteLine($_.Message)};exit 1}";
14409
+ run2 = spawnSync6(spec.bin, ["-NoProfile", "-NonInteractive", "-Command", parse3], {
14410
+ encoding: "utf8",
14411
+ env: { ...process.env, CREW_CHECK_SCRIPT: script }
14412
+ });
14413
+ } else {
14414
+ return { status: "unchecked", message: `no syntax check for ${spec.bin}` };
14415
+ }
14416
+ if (run2.error) return { status: "unchecked", message: `cannot run ${spec.bin}: ${run2.error.message}` };
14417
+ if (run2.status === 0) return { status: "ok" };
14418
+ const message = `${run2.stderr ?? ""}${run2.stdout ?? ""}`.trim() || `${spec.bin} exited ${run2.status}`;
14419
+ return { status: "error", message };
14420
+ }
14421
+ function renderSamples(cfg, dir) {
14422
+ const checkout = basename8(dir.replace(/[/\\]+$/, ""));
14423
+ const tagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", tag: "TABL-123", prefix: "tabl", lowercase: cfg.branch.lowercase };
14424
+ const untagged = { key: "ISSUE-123", title: "Fix the widget", role: "dev", lowercase: cfg.branch.lowercase };
14425
+ return [["with a project tag", tagged], ["without a project tag", untagged]].map(([label, ctx]) => {
14426
+ const branch = renderBranchName(effectiveBranchTemplate(cfg, ctx.prefix), ctx);
14427
+ const worktree = effectiveWorktreeDirName(cfg, dir, branch, "123", (t) => renderWorktreeName(t, { ...ctx, dir: checkout }));
14428
+ return { case: label, branch, worktree };
14429
+ });
14430
+ }
14431
+ function checkRepoSpec(dir, opts = {}) {
14432
+ const empty = { parsed: null, effective: null, readiness: [], hooks: [], shadowed: [], samples: [] };
14433
+ const file = findRepoConfig(dir);
14434
+ if (!file) {
14435
+ return { ...empty, file: null, error: `no .crew.yaml, .crew.yml or .crew.json in ${dir}`, exitCode: 1 };
14436
+ }
14437
+ let parsed;
14438
+ try {
14439
+ parsed = parseRepoConfig(readFileSync24(file, "utf8"), file);
14440
+ } catch (e) {
14441
+ if (!(e instanceof RepoConfigError)) throw e;
14442
+ return { ...empty, file, error: e.message, exitCode: 1 };
14443
+ }
14444
+ const effective = resolveRepoConfig(parsed, opts.ship, dir);
14445
+ const readiness = validateEffective(effective);
14446
+ const hooks = Object.keys(effective.hooks).map((name) => ({
14447
+ name,
14448
+ label: hookLabel(effective, name),
14449
+ syntax: checkHookSyntax(effective.hooks[name], effective.shell)
14450
+ }));
14451
+ const failed = hooks.some((h) => h.syntax.status === "error");
14452
+ return {
14453
+ file,
14454
+ parsed,
14455
+ effective,
14456
+ readiness,
14457
+ hooks,
14458
+ shadowed: effective.shadowed,
14459
+ samples: renderSamples(effective, dir),
14460
+ error: null,
14461
+ exitCode: failed || opts.strict && readiness.length > 0 ? 1 : 0
14462
+ };
14463
+ }
14464
+ function renderRepoCheck(r) {
14465
+ if (r.error) return `${r.file ?? "crew repos check"}: ${r.error}
14466
+ `;
14467
+ const e = r.effective;
14468
+ const lines = [
14469
+ `${r.file}`,
14470
+ ` platform ${e.platform}`,
14471
+ ` release.mode ${e.release.mode}`,
14472
+ ` release.versioning ${e.release.versioning}`,
14473
+ ` branch.base ${e.branch.base}`,
14474
+ ` branch.name ${e.branch.name}`,
14475
+ ` branch.push ${e.branch.push}`
14476
+ ];
14477
+ for (const s of r.samples) lines.push(` ${s.case}: branch ${s.branch}, worktree ../${s.worktree}`);
14478
+ lines.push(r.hooks.length ? " hooks" : " hooks none");
14479
+ for (const h of r.hooks) {
14480
+ const named = h.label !== e.hooks[h.name] ? ` (${h.label})` : "";
14481
+ const syntax = h.syntax.status === "ok" ? "syntax ok" : h.syntax.status === "unchecked" ? `syntax not checked: ${h.syntax.message}` : "syntax ERROR";
14482
+ lines.push(` ${h.name}${named}: ${syntax}`);
14483
+ if (h.syntax.status === "error") lines.push(...(h.syntax.message ?? "").split("\n").map((l) => ` ${l}`));
14484
+ }
14485
+ if (r.shadowed.length) lines.push(` this ship declares ${r.shadowed.join(", ")}, and the file overrides it \u2014 the file wins`);
14486
+ for (const p of r.readiness) lines.push(` - release readiness: ${p}`);
14487
+ return `${lines.join("\n")}
14488
+ `;
14489
+ }
14490
+
14396
14491
  // src/repos-hooks.ts
14397
14492
  import { isMap as isMap2, parseDocument as parseDocument3, YAMLMap } from "yaml";
14398
14493
 
14399
14494
  // src/repos-cmd.ts
14400
14495
  import { homedir as homedir9 } from "os";
14401
- import { basename as basename8, join as join28, resolve as resolve3 } from "path";
14496
+ import { basename as basename9, join as join28, resolve as resolve3 } from "path";
14402
14497
  import { isMap, isSeq, parseDocument as parseDocument2, stringify as stringify2 } from "yaml";
14403
14498
  var ReposError = class extends Error {
14404
14499
  };
@@ -14470,7 +14565,7 @@ function configuredRepos(text, route, base) {
14470
14565
  });
14471
14566
  }
14472
14567
  const dir = m.get("dir");
14473
- return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
14568
+ return dir ? [{ name: basename9(String(dir)), dir: expandDir(String(dir), base) }] : [];
14474
14569
  }
14475
14570
  function cloneBaseOf(text, route, base, isCheckout) {
14476
14571
  const m = routeMapOf(parseDocument2(text), route);
@@ -14559,7 +14654,7 @@ function planRepoAdd(i) {
14559
14654
  throw new ReposError(`${target} is already configured on route ${i.route}`);
14560
14655
  }
14561
14656
  const matched = matchRepoName(i.origin, i.resolved);
14562
- const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename8(target);
14657
+ const name = matched ?? i.name ?? normalizeRemote(i.origin)?.split("/").pop() ?? basename9(target);
14563
14658
  let warning;
14564
14659
  if (!matched) {
14565
14660
  const known = Object.values(i.resolved?.repoNames ?? {});
@@ -15075,7 +15170,7 @@ ${run2.err}` });
15075
15170
  }
15076
15171
 
15077
15172
  // src/sample-ticket.ts
15078
- import { basename as basename9, dirname as dirname17, join as join29 } from "path";
15173
+ import { basename as basename10, dirname as dirname17, join as join29 } from "path";
15079
15174
  var SAMPLE_REPO = "crew-sample";
15080
15175
  var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
15081
15176
  var SAMPLE_MARKER = "sample: true";
@@ -15191,8 +15286,8 @@ function schedulerInstalled(plan, exists, crontab) {
15191
15286
  }
15192
15287
 
15193
15288
  // src/worktree-sweep.ts
15194
- import { existsSync as existsSync27, readFileSync as readFileSync24, readdirSync as readdirSync8 } from "fs";
15195
- import { basename as basename10, dirname as dirname18, join as join30, resolve as resolve4 } from "path";
15289
+ import { existsSync as existsSync27, readFileSync as readFileSync25, readdirSync as readdirSync8 } from "fs";
15290
+ import { basename as basename11, dirname as dirname18, join as join30, resolve as resolve4 } from "path";
15196
15291
  function planWorktreeSweep(target, tickets, contract) {
15197
15292
  const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
15198
15293
  const verified = contract.statuses.verified;
@@ -15223,7 +15318,7 @@ function ticketForWorktree(w, byNumber, contract) {
15223
15318
  const t = num ? byNumber.get(num) : void 0;
15224
15319
  if (t && ticketStems(t).has(stem)) return t;
15225
15320
  }
15226
- const dir = basename10(w.path);
15321
+ const dir = basename11(w.path);
15227
15322
  const m = DIR_NUMBER.exec(dir);
15228
15323
  if (m && byNumber.has(m[1])) return byNumber.get(m[1]);
15229
15324
  if (!w.branch) {
@@ -15239,7 +15334,7 @@ function worktreeTicketNumbers(target) {
15239
15334
  const n = /-(\d+)$/.exec(BRANCH_STEM.exec(w.branch)?.[1] ?? "")?.[1];
15240
15335
  if (n) nums.add(n);
15241
15336
  }
15242
- const m = DIR_NUMBER_SUFFIXED.exec(basename10(w.path));
15337
+ const m = DIR_NUMBER_SUFFIXED.exec(basename11(w.path));
15243
15338
  if (m) nums.add(m[1]);
15244
15339
  }
15245
15340
  for (const d of siblingWorktreeDirs(target)) nums.add(d.number);
@@ -15247,7 +15342,7 @@ function worktreeTicketNumbers(target) {
15247
15342
  }
15248
15343
  function siblingWorktreeDirs(target) {
15249
15344
  const parent = dirname18(target.dir.replace(/[/\\]+$/, ""));
15250
- const stem = `${basename10(target.dir.replace(/[/\\]+$/, ""))}-`;
15345
+ const stem = `${basename11(target.dir.replace(/[/\\]+$/, ""))}-`;
15251
15346
  const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
15252
15347
  let names;
15253
15348
  try {
@@ -15272,7 +15367,7 @@ function orphanedWorktreeDirs(target, tickets, contract) {
15272
15367
  }
15273
15368
  function warnOrphanedWorktreeDirs(paths, log, held) {
15274
15369
  for (const p of paths) {
15275
- const number = DIR_NUMBER_SUFFIXED.exec(basename10(p))?.[1];
15370
+ const number = DIR_NUMBER_SUFFIXED.exec(basename11(p))?.[1];
15276
15371
  const when = held && number ? noteHeldWorktree(held.state, { repo: held.repo, path: p, ticket: number, status: "deployed", reason: "orphaned directory" }) : "first";
15277
15372
  if (when) {
15278
15373
  log.warn(`orphaned directory ${p}: its ticket is deployed but it is not a registered worktree; the sweep will not delete it \u2014 remove it by hand`, { step: "worktree" });
@@ -15362,7 +15457,7 @@ function isHandBackMerge(path, base) {
15362
15457
  if (!msgPath) return false;
15363
15458
  let first;
15364
15459
  try {
15365
- first = readFileSync24(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
15460
+ first = readFileSync25(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
15366
15461
  } catch {
15367
15462
  return false;
15368
15463
  }
@@ -15416,7 +15511,7 @@ function isTicketBranch(name, base, stems) {
15416
15511
  }
15417
15512
  function sweepCandidatesExist(cwd, base) {
15418
15513
  const linked = worktrees(cwd).slice(1);
15419
- if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename10(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
15514
+ if (linked.some((w) => DIR_NUMBER_SUFFIXED.test(basename11(w.path)) || w.branch && BRANCH_STEM.test(w.branch))) return true;
15420
15515
  if (siblingWorktreeDirs({ name: "", dir: cwd }).length) return true;
15421
15516
  return localBranches(cwd).some((b) => b !== base && /-\d+(?:-.*)?$/.test(b));
15422
15517
  }
@@ -15512,13 +15607,13 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
15512
15607
  }
15513
15608
 
15514
15609
  // src/cli.ts
15515
- import { realpathSync as realpathSync3, readFileSync as readFileSync30, existsSync as existsSync34, mkdirSync as mkdirSync22, writeFileSync as writeFileSync23, rmSync as rmSync15, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync10, readdirSync as readdirSync14 } from "fs";
15610
+ import { realpathSync as realpathSync3, readFileSync as readFileSync31, existsSync as existsSync34, mkdirSync as mkdirSync22, writeFileSync as writeFileSync23, rmSync as rmSync15, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync3, statSync as statSync10, readdirSync as readdirSync14 } from "fs";
15516
15611
  import { createInterface } from "readline/promises";
15517
15612
  import { dirname as dirOf, resolve as resolvePath } from "path";
15518
15613
 
15519
15614
  // src/ssh-keys.ts
15520
15615
  import { execFileSync as execFileSync6 } from "child_process";
15521
- import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync25, chmodSync } from "fs";
15616
+ import { existsSync as existsSync29, mkdirSync as mkdirSync18, readFileSync as readFileSync26, chmodSync } from "fs";
15522
15617
  import { dirname as dirname19, join as join32 } from "path";
15523
15618
  var SshKeyError = class extends Error {
15524
15619
  };
@@ -15552,7 +15647,7 @@ function ensureShipSshKeypair(stateDir) {
15552
15647
  if (!existsSync29(publicKeyPath)) {
15553
15648
  throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
15554
15649
  }
15555
- const publicKey = readFileSync25(publicKeyPath, "utf8").trim();
15650
+ const publicKey = readFileSync26(publicKeyPath, "utf8").trim();
15556
15651
  return { privateKeyPath, publicKeyPath, publicKey };
15557
15652
  }
15558
15653
  function sshKeygenAvailable() {
@@ -15769,7 +15864,7 @@ function syncAllPassengerCheckouts(cfg) {
15769
15864
 
15770
15865
  // src/tunnel.ts
15771
15866
  import { spawn as spawn5 } from "child_process";
15772
- import { existsSync as existsSync30, mkdirSync as mkdirSync19, readFileSync as readFileSync26, writeFileSync as writeFileSync20, unlinkSync as unlinkSync3, readdirSync as readdirSync10 } from "fs";
15867
+ import { existsSync as existsSync30, mkdirSync as mkdirSync19, readFileSync as readFileSync27, writeFileSync as writeFileSync20, unlinkSync as unlinkSync3, readdirSync as readdirSync10 } from "fs";
15773
15868
  import { randomInt } from "crypto";
15774
15869
  import { join as join33 } from "path";
15775
15870
  var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
@@ -15844,7 +15939,7 @@ function readPersistedTunnel(stateDir, workspaceId) {
15844
15939
  const path = tunnelStatePath(stateDir, workspaceId);
15845
15940
  if (!existsSync30(path)) return void 0;
15846
15941
  try {
15847
- return JSON.parse(readFileSync26(path, "utf8"));
15942
+ return JSON.parse(readFileSync27(path, "utf8"));
15848
15943
  } catch {
15849
15944
  return void 0;
15850
15945
  }
@@ -16071,7 +16166,7 @@ async function unregisterEndpoint(workspaceId, info, d) {
16071
16166
  import { createServer as createHttpServer } from "http";
16072
16167
  import { spawn as spawn6 } from "child_process";
16073
16168
  import { randomBytes } from "crypto";
16074
- import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync27, writeFileSync as writeFileSync21, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
16169
+ import { existsSync as existsSync31, mkdirSync as mkdirSync20, readFileSync as readFileSync28, writeFileSync as writeFileSync21, unlinkSync as unlinkSync4, readdirSync as readdirSync11 } from "fs";
16075
16170
  import { join as join34 } from "path";
16076
16171
 
16077
16172
  // src/secret-compare.ts
@@ -16167,7 +16262,7 @@ function readPersistedSyncDaemon(stateDir, workspaceId) {
16167
16262
  const path = syncDaemonStatePath(stateDir, workspaceId);
16168
16263
  if (!existsSync31(path)) return void 0;
16169
16264
  try {
16170
- return JSON.parse(readFileSync27(path, "utf8"));
16265
+ return JSON.parse(readFileSync28(path, "utf8"));
16171
16266
  } catch {
16172
16267
  return void 0;
16173
16268
  }
@@ -16284,11 +16379,11 @@ function describeCause(err) {
16284
16379
  }
16285
16380
 
16286
16381
  // src/daemon.ts
16287
- import { existsSync as existsSync33, mkdirSync as mkdirSync21, readFileSync as readFileSync29, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync22 } from "fs";
16382
+ import { existsSync as existsSync33, mkdirSync as mkdirSync21, readFileSync as readFileSync30, readdirSync as readdirSync13, rmSync as rmSync14, statSync as statSync9, writeFileSync as writeFileSync22 } from "fs";
16288
16383
  import { join as join36 } from "path";
16289
16384
 
16290
16385
  // src/interrupted.ts
16291
- import { existsSync as existsSync32, readdirSync as readdirSync12, readFileSync as readFileSync28 } from "fs";
16386
+ import { existsSync as existsSync32, readdirSync as readdirSync12, readFileSync as readFileSync29 } from "fs";
16292
16387
  import { join as join35 } from "path";
16293
16388
  var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
16294
16389
  function interruptedComment(b) {
@@ -16310,7 +16405,7 @@ function runFinished(streamPath) {
16310
16405
  const path = `${streamPath}.meta.json`;
16311
16406
  if (!existsSync32(path)) return false;
16312
16407
  try {
16313
- return JSON.parse(readFileSync28(path, "utf8")).running !== true;
16408
+ return JSON.parse(readFileSync29(path, "utf8")).running !== true;
16314
16409
  } catch {
16315
16410
  return true;
16316
16411
  }
@@ -16467,7 +16562,7 @@ function noteDaemonStarted(stateDir, now = Date.now()) {
16467
16562
  }
16468
16563
  function readDaemonStarted(stateDir) {
16469
16564
  try {
16470
- const v = JSON.parse(readFileSync29(daemonStampFor(stateDir), "utf8")).startedAt;
16565
+ const v = JSON.parse(readFileSync30(daemonStampFor(stateDir), "utf8")).startedAt;
16471
16566
  return typeof v === "number" ? v : null;
16472
16567
  } catch {
16473
16568
  return null;
@@ -17018,6 +17113,7 @@ function usage(problem, requested = false) {
17018
17113
  crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
17019
17114
  crew ship rename NAME [--json] rename this ship in crew.yaml and on every workspace's Ships row
17020
17115
  crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml (--dry-run previews)
17116
+ crew repos check [PATH] validate a checkout's .crew.yaml without attaching it (--route ROUTE, --strict, --json)
17021
17117
  crew repos clone [ROUTE] NAME clone a Repos row's remote into the route directory and attach it (--dir PATH, --dry-run, --json)
17022
17118
  crew repos hooks [ROUTE] NAME suggest test/build hooks for a checkout (--agent, --write route|repo, --recipes, --json)
17023
17119
  crew repos list ROUTE the checkouts a route has, with the tracker's Repos row each matches (--json: every Repos row too)
@@ -17116,6 +17212,22 @@ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
17116
17212
  "worktree"
17117
17213
  ]);
17118
17214
  if (!KNOWN_COMMANDS.has(command)) usage(`unknown command "${command}"`);
17215
+ function runReposCheck(dir, ship) {
17216
+ const report = checkRepoSpec(dir, { ship, strict: flag("strict") });
17217
+ if (flag("json")) {
17218
+ const { exitCode: _exit, ...rest } = report;
17219
+ process.stdout.write(`${JSON.stringify(rest)}
17220
+ `);
17221
+ } else {
17222
+ process.stdout.write(renderRepoCheck(report));
17223
+ }
17224
+ process.exitCode = report.exitCode;
17225
+ }
17226
+ var reposCheckBare = command === "repos" && positional[1] === "check" && !value("route") && positional.length < 4;
17227
+ if (reposCheckBare) {
17228
+ runReposCheck(resolvePath(positional[2] ?? "."));
17229
+ process.exit(process.exitCode ?? 0);
17230
+ }
17119
17231
  function findSyncRoute(c, name) {
17120
17232
  if (!name && c.routes.length > 1) {
17121
17233
  throw new ConfigError(`this ship has ${c.routes.length} routes \u2014 name one: ${c.routes.map((r) => r.route).join(", ")}`);
@@ -17502,7 +17614,7 @@ if (command === "passenger-sync-daemon") {
17502
17614
  `
17503
17615
  );
17504
17616
  if (!dryRun) {
17505
- spawnSync6("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
17617
+ spawnSync7("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
17506
17618
  try {
17507
17619
  unlinkSync5(staleTimer);
17508
17620
  } catch {
@@ -17542,7 +17654,7 @@ if (command === "passenger-sync-daemon") {
17542
17654
  async function doctorFix(o) {
17543
17655
  const routes = await Promise.all(routesInScope(cfg.routes, named).map((c) => gatherHealth({ route: c.route, enabled: c.enabled, hasRepos: reposOf(c).length > 0 }, () => routeProblems(c))));
17544
17656
  const unitExists = (p) => existsSync34(p);
17545
- const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
17657
+ const crontab = o.host === "linux" ? spawnSync7("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
17546
17658
  const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
17547
17659
  const plan = planFix({ routes, schedulerAbsent, host: o.host });
17548
17660
  for (const n of plan.notes) process.stdout.write(`fix: ${n}
@@ -17558,7 +17670,7 @@ if (command === "passenger-sync-daemon") {
17558
17670
  for (const a of plan.actions) {
17559
17671
  if (a.kind === "enable") {
17560
17672
  if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
17561
- writeFileSync23(cfg.configFile, planEnable(readFileSync30(cfg.configFile, "utf8"), a.route));
17673
+ writeFileSync23(cfg.configFile, planEnable(readFileSync31(cfg.configFile, "utf8"), a.route));
17562
17674
  enabledSound.add(a.route);
17563
17675
  changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
17564
17676
  } else {
@@ -18559,7 +18671,7 @@ ${changed2.map((c) => ` - ${c}
18559
18671
  }
18560
18672
  let failed = false;
18561
18673
  for (const cmd2 of commands) {
18562
- const res2 = spawnSync6(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
18674
+ const res2 = spawnSync7(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
18563
18675
  if (res2.stdout) process.stdout.write(res2.stdout);
18564
18676
  if (res2.stderr) process.stderr.write(res2.stderr);
18565
18677
  if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
@@ -18577,7 +18689,7 @@ ${changed2.map((c) => ` - ${c}
18577
18689
  `);
18578
18690
  break;
18579
18691
  }
18580
- const res = spawnSync6(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
18692
+ const res = spawnSync7(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
18581
18693
  if (res.stdout) process.stdout.write(res.stdout);
18582
18694
  if (res.stderr) process.stderr.write(res.stderr);
18583
18695
  if (daemonSub !== "status" && (res.error || res.status !== 0)) {
@@ -18828,7 +18940,7 @@ ${changed2.map((c) => ` - ${c}
18828
18940
  `);
18829
18941
  if (!dryRun) {
18830
18942
  const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
18831
- const raw = existsSync34(resolvedPath) ? JSON.parse(readFileSync30(resolvedPath, "utf8")) : {};
18943
+ const raw = existsSync34(resolvedPath) ? JSON.parse(readFileSync31(resolvedPath, "utf8")) : {};
18832
18944
  raw.agentSkills = result.agentSkills;
18833
18945
  mkdirSync22(dirOf(resolvedPath), { recursive: true });
18834
18946
  writeFileSync23(resolvedPath, `${JSON.stringify(raw, null, 2)}
@@ -18854,7 +18966,7 @@ ${changed2.map((c) => ` - ${c}
18854
18966
  throw e;
18855
18967
  }
18856
18968
  const oldName = cfg.ship.name;
18857
- writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync30(cfg.configFile, "utf8"), newName));
18969
+ writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync31(cfg.configFile, "utf8"), newName));
18858
18970
  cfg.ship.name = newName;
18859
18971
  let renameMachineId;
18860
18972
  try {
@@ -18897,11 +19009,37 @@ ${changed2.map((c) => ` - ${c}
18897
19009
  case "repos": {
18898
19010
  const sub = positional[1];
18899
19011
  const usage2 = () => {
18900
- process.stderr.write("crew repos add [workspace/project] [name] [path] [--name N] [--project P] [--dry-run] [--no-create]\ncrew repos clone [workspace/project] <name> [--dir PATH] [--dry-run] [--json]\ncrew repos list <workspace/project> [--json]\ncrew repos hooks [workspace/project] <name> [--suggest] [--agent] [--write route|repo [--replace]] [--clear route] [--recipes ID,ID] [--hook KEY=CMD ...] [--json]\n");
19012
+ process.stderr.write("crew repos add [workspace/project] [name] [path] [--name N] [--project P] [--dry-run] [--no-create]\ncrew repos clone [workspace/project] <name> [--dir PATH] [--dry-run] [--json]\ncrew repos list <workspace/project> [--json]\ncrew repos check [PATH | --route ROUTE [PATH] | ROUTE NAME] [--strict] [--json]\ncrew repos hooks [workspace/project] <name> [--suggest] [--agent] [--write route|repo [--replace]] [--clear route] [--recipes ID,ID] [--hook KEY=CMD ...] [--json]\n");
18901
19013
  process.exit(2);
18902
19014
  };
18903
- if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
18904
- const configText = readFileSync30(cfg.configFile, "utf8");
19015
+ if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks" && sub !== "check") usage2();
19016
+ const configText = readFileSync31(cfg.configFile, "utf8");
19017
+ if (sub === "check") {
19018
+ try {
19019
+ const routeArg = value("route") ?? positional[2];
19020
+ const found = cfg.routes.find((r) => r.route === routeArg);
19021
+ if (!found) throw new ReposError(`no route "${routeArg}" in the config (have: ${cfg.routes.map((r) => r.route).join(", ") || "none"})`);
19022
+ const named2 = value("route") ? void 0 : positional[3];
19023
+ const dir = named2 ? repoDirOf(configText, dirname20(cfg.configFile), found.route, named2) : resolvePath(positional[value("route") ? 2 : 3] ?? ".");
19024
+ const target = reposOf(found).find((t) => resolvePath(t.dir) === dir || t.name === named2);
19025
+ const o = target ? found.repoOverrides[target.name] : void 0;
19026
+ runReposCheck(dir, {
19027
+ hooks: { ...found.hooks, ...o?.hooks },
19028
+ labels: { ...found.labels, ...o?.labels },
19029
+ release: mergeRouteRelease(found.release, o?.release),
19030
+ branch: { ...found.branch, ...o?.branch },
19031
+ worktrees: { prefix: shipWorktreePrefixFor(found), nameOverride: o?.worktreeName }
19032
+ });
19033
+ } catch (e) {
19034
+ if (e instanceof ReposError) {
19035
+ process.stderr.write(`crew repos: ${e.message}
19036
+ `);
19037
+ process.exit(2);
19038
+ }
19039
+ throw e;
19040
+ }
19041
+ break;
19042
+ }
18905
19043
  if (sub === "hooks") {
18906
19044
  try {
18907
19045
  const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
@@ -18915,7 +19053,7 @@ ${changed2.map((c) => ` - ${c}
18915
19053
  }
18916
19054
  }, read: (r) => {
18917
19055
  try {
18918
- return readFileSync30(resolvePath(dir, r), "utf8");
19056
+ return readFileSync31(resolvePath(dir, r), "utf8");
18919
19057
  } catch {
18920
19058
  return void 0;
18921
19059
  }
@@ -19056,7 +19194,7 @@ ${changed2.map((c) => ` - ${c}
19056
19194
  let resolvedRemotes;
19057
19195
  if (existsSync34(resolvedFile)) {
19058
19196
  try {
19059
- resolvedRemotes = JSON.parse(readFileSync30(resolvedFile, "utf8"));
19197
+ resolvedRemotes = JSON.parse(readFileSync31(resolvedFile, "utf8"));
19060
19198
  } catch {
19061
19199
  }
19062
19200
  }
@@ -19077,7 +19215,7 @@ ${changed2.map((c) => ` - ${c}
19077
19215
  isCheckout,
19078
19216
  repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
19079
19217
  try {
19080
- return Object.keys(parseRepoConfig(readFileSync30(resolvePath(d, f), "utf8"), f).hooks).length > 0;
19218
+ return Object.keys(parseRepoConfig(readFileSync31(resolvePath(d, f), "utf8"), f).hooks).length > 0;
19081
19219
  } catch {
19082
19220
  return false;
19083
19221
  }
@@ -19186,7 +19324,7 @@ ${changed2.map((c) => ` - ${c}
19186
19324
  process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
19187
19325
  `);
19188
19326
  if (existsSync34(resolvedFile)) {
19189
- const raw = JSON.parse(readFileSync30(resolvedFile, "utf8"));
19327
+ const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
19190
19328
  raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
19191
19329
  raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
19192
19330
  writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
@@ -19544,7 +19682,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19544
19682
  process.exit(2);
19545
19683
  }
19546
19684
  if (found.shipNameAdopted && !dryRun) {
19547
- writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync30(cfg.configFile, "utf8"), found.shipNameAdopted));
19685
+ writeFileSync23(cfg.configFile, setShipNameInYaml(readFileSync31(cfg.configFile, "utf8"), found.shipNameAdopted));
19548
19686
  cfg.ship.name = found.shipNameAdopted;
19549
19687
  }
19550
19688
  if (found.shipRow) {
@@ -19590,7 +19728,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19590
19728
  const hadPreviousFile = existsSync34(resolvedPath);
19591
19729
  if (hadPreviousFile) {
19592
19730
  try {
19593
- previous = JSON.parse(readFileSync30(resolvedPath, "utf8"));
19731
+ previous = JSON.parse(readFileSync31(resolvedPath, "utf8"));
19594
19732
  } catch {
19595
19733
  }
19596
19734
  }
@@ -20475,7 +20613,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20475
20613
  };
20476
20614
  const droppedRoutes = [];
20477
20615
  try {
20478
- const rawCfg = parseYaml2(readFileSync30(cfg.configFile, "utf8"));
20616
+ const rawCfg = parseYaml2(readFileSync31(cfg.configFile, "utf8"));
20479
20617
  for (const r of rawCfg?.routes ?? []) {
20480
20618
  if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
20481
20619
  try {
@@ -20511,7 +20649,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20511
20649
  }
20512
20650
  const host = hostPlatform();
20513
20651
  const control = host === "windows" ? null : planDaemonControl(CREW_HOME, host, detectSystemd());
20514
- const unitStatus = control?.statusCommand ? spawnSync6(control.statusCommand[0], control.statusCommand.slice(1), { cwd: CREW_HOME, encoding: "utf8" }) : null;
20652
+ const unitStatus = control?.statusCommand ? spawnSync7(control.statusCommand[0], control.statusCommand.slice(1), { cwd: CREW_HOME, encoding: "utf8" }) : null;
20515
20653
  const unitRunning = unitStatus !== null && !unitStatus.error && unitStatus.status === 0;
20516
20654
  if (!unitRunning) {
20517
20655
  process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
@@ -20523,7 +20661,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20523
20661
  process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
20524
20662
  if (flag("now") && control?.stopCommand && control.startCommand) {
20525
20663
  for (const cmd of [control.stopCommand, control.startCommand]) {
20526
- const res = spawnSync6(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
20664
+ const res = spawnSync7(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
20527
20665
  if (cmd === control.startCommand && (res.error || res.status !== 0)) {
20528
20666
  process.stderr.write(`crew reload: ${cmd.join(" ")} exited ${res.status ?? "error"}
20529
20667
  `);
@@ -20569,7 +20707,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20569
20707
  }
20570
20708
  case "log": {
20571
20709
  try {
20572
- const text = readFileSync30(cfg.ship.logFile, "utf8").split("\n");
20710
+ const text = readFileSync31(cfg.ship.logFile, "utf8").split("\n");
20573
20711
  process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
20574
20712
  `);
20575
20713
  } catch {
@@ -20592,7 +20730,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20592
20730
  };
20593
20731
  if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
20594
20732
  const tracker = new Tracker(route, cfg.ship);
20595
- const configText = readFileSync30(cfg.configFile, "utf8");
20733
+ const configText = readFileSync31(cfg.configFile, "utf8");
20596
20734
  const cfgBase = dirname20(cfg.configFile);
20597
20735
  const configured = configuredRepos(configText, route.route, cfgBase);
20598
20736
  const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
@@ -20630,14 +20768,14 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20630
20768
  if (row) {
20631
20769
  await tracker.deleteRepoRow(row.id);
20632
20770
  if (existsSync34(resolvedFile)) {
20633
- const raw = JSON.parse(readFileSync30(resolvedFile, "utf8"));
20771
+ const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
20634
20772
  if (raw.repoNames) delete raw.repoNames[row.id];
20635
20773
  if (raw.repoRemotes) delete raw.repoRemotes[row.id];
20636
20774
  writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
20637
20775
  `);
20638
20776
  }
20639
20777
  }
20640
- if (existsSync34(join37(dir, ".git")) && basename11(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
20778
+ if (existsSync34(join37(dir, ".git")) && basename12(dir) === SAMPLE_REPO) rmSync15(dir, { recursive: true, force: true });
20641
20779
  say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
20642
20780
  break;
20643
20781
  }
@@ -20653,7 +20791,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20653
20791
  writeFileSync23(join37(dir, "README.md"), SAMPLE_README);
20654
20792
  writeFileSync23(join37(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
20655
20793
  const g = (...a) => {
20656
- const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
20794
+ const r = spawnSync7("git", a, { cwd: dir, encoding: "utf8" });
20657
20795
  if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
20658
20796
  };
20659
20797
  g("init", "-b", "main");
@@ -20670,7 +20808,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20670
20808
  });
20671
20809
  repoId = (await tracker.createRepoRow(payload)).id;
20672
20810
  if (existsSync34(resolvedFile)) {
20673
- const raw = JSON.parse(readFileSync30(resolvedFile, "utf8"));
20811
+ const raw = JSON.parse(readFileSync31(resolvedFile, "utf8"));
20674
20812
  raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
20675
20813
  raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
20676
20814
  writeFileSync23(resolvedFile, `${JSON.stringify(raw, null, 2)}
@@ -20901,7 +21039,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
20901
21039
  // `synthesis-tabl-123`. `worktreePrefix` above is kept for
20902
21040
  // config-provenance display; this is the field that actually
20903
21041
  // describes where a worktree lands by default.
20904
- worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename11(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
21042
+ worktreeDirPattern: r.config.worktrees.name ?? (r.config.provenance["worktrees.prefix"] !== "default" ? `${r.config.worktrees.prefix}<number>` : `${basename12(r.dir.replace(/[/\\]+$/, ""))}-<branch>`),
20905
21043
  worktreeParent: resolvePath(r.dir, "..")
20906
21044
  })),
20907
21045
  waiting: waiting ? { ticket: waiting.ticket, since: waiting.since, streak: fairness.streak() } : null
@@ -21068,7 +21206,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
21068
21206
  const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
21069
21207
  const abandons = unitAbandonsAgents(unitPlan, (p) => {
21070
21208
  try {
21071
- return readFileSync30(p, "utf8");
21209
+ return readFileSync31(p, "utf8");
21072
21210
  } catch {
21073
21211
  return null;
21074
21212
  }
@@ -21111,7 +21249,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
21111
21249
  );
21112
21250
  for (const r of await resolvedRepos(route)) {
21113
21251
  const { config } = r;
21114
- const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename11(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
21252
+ const worktreePattern = config.worktrees.name ?? (config.provenance["worktrees.prefix"] !== "default" ? `${config.worktrees.prefix}<number>` : `${basename12(r.dir.replace(/[/\\]+$/, ""))}-${effectiveBranchTemplate(config, void 0)}`);
21115
21253
  process.stdout.write(
21116
21254
  `${`repo ${r.name}:`.padEnd(19)}${r.dir}
21117
21255
  worktrees at ../${worktreePattern} (${config.provenance["worktrees.name"] ?? config.provenance["worktrees.prefix"] ?? "default"}), branch ${config.branch.name}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.77.0",
3
+ "version": "0.78.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",