@tablation/crew 0.72.1 → 0.72.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 (2) hide show
  1. package/dist/cli.js +60 -14
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -2587,7 +2587,9 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
2587
2587
  }
2588
2588
  }
2589
2589
  const hasRepos = Object.keys(repoDirs).length > 0;
2590
- const dir = c?.dir ? expand(String(c.dir), base) : hasRepos ? Object.values(repoDirs)[0] : missing.req(void 0, `${where}.dir (or ${where}.repos)`) ?? "";
2590
+ const reposBasePath = expand(String(c?.reposBasePath ?? raw.ship?.reposBasePath ?? DEFAULT_REPOS_BASE_PATH), base);
2591
+ const dirIsCloneBase = !c?.dir && !hasRepos && c?.repos && typeof c.repos === "object" && !Array.isArray(c.repos);
2592
+ const dir = c?.dir ? expand(String(c.dir), base) : hasRepos ? Object.values(repoDirs)[0] : dirIsCloneBase ? join7(reposBasePath, splitRoute(route)[0]) : missing.req(void 0, `${where}.dir (or ${where}.repos)`) ?? "";
2591
2593
  const weight = c?.weight === void 0 ? void 0 : Number(c.weight);
2592
2594
  if (weight !== void 0 && !(Number.isFinite(weight) && weight > 0)) {
2593
2595
  missing.add(`${where}.weight must be a positive number (got "${c?.weight}")`);
@@ -2597,8 +2599,9 @@ function parseOneRoute(c, where, file, base, stateDir, raw, missing, crewHome, t
2597
2599
  enabled: c?.enabled === true,
2598
2600
  area: c?.area,
2599
2601
  dir,
2602
+ ...dirIsCloneBase ? { dirIsCloneBase: true } : {},
2600
2603
  repos: repoDirs,
2601
- reposBasePath: expand(String(c?.reposBasePath ?? raw.ship?.reposBasePath ?? DEFAULT_REPOS_BASE_PATH), base),
2604
+ reposBasePath,
2602
2605
  repoOverrides,
2603
2606
  worktreePrefix: c?.worktreePrefix ? String(c.worktreePrefix) : void 0,
2604
2607
  ...toolchain !== void 0 ? { toolchain } : {},
@@ -2656,7 +2659,7 @@ function dirForRepo(r, repoId) {
2656
2659
  }
2657
2660
  function repoTargetFor(r, repoId) {
2658
2661
  const names = r.resolved?.repoNames;
2659
- if (!repoId || !names) return Object.keys(r.repos).length ? null : { name: basename3(r.dir), dir: r.dir };
2662
+ if (!repoId || !names) return Object.keys(r.repos).length || r.dirIsCloneBase ? null : { name: basename3(r.dir), dir: r.dir };
2660
2663
  const name = names[repoId];
2661
2664
  if (!name) return null;
2662
2665
  return reposOf(r).find((t) => t.name === name) ?? null;
@@ -2747,6 +2750,15 @@ function reposOf(r) {
2747
2750
  const named = Object.entries(r.repos);
2748
2751
  if (named.length > 0) return named.map(([name, dir]) => ({ name, dir }));
2749
2752
  const trackerNames = r.resolved?.repoNames ? Object.values(r.resolved.repoNames) : [];
2753
+ if (r.dirIsCloneBase) {
2754
+ const [workspace2] = splitRoute(r.route);
2755
+ const remotes2 = r.resolved?.repoRemotes ?? {};
2756
+ return Object.entries(r.resolved?.repoNames ?? {}).map(([id, name]) => ({
2757
+ name,
2758
+ dir: defaultRepoDir(r.reposBasePath, workspace2, name),
2759
+ remote: remotes2[id]
2760
+ }));
2761
+ }
2750
2762
  if (trackerNames.length <= 1) return [{ name: basename3(r.dir), dir: r.dir }];
2751
2763
  const remotes = r.resolved?.repoRemotes ?? {};
2752
2764
  const remoteByName = {};
@@ -8632,6 +8644,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
8632
8644
  "key",
8633
8645
  "level",
8634
8646
  "limit",
8647
+ "machine-id",
8635
8648
  "max-roles",
8636
8649
  "mcp",
8637
8650
  "member",
@@ -8644,6 +8657,7 @@ var VALUE_FLAGS = /* @__PURE__ */ new Set([
8644
8657
  "ship-name",
8645
8658
  "tail",
8646
8659
  "ticket",
8660
+ "toolchain",
8647
8661
  "workspace",
8648
8662
  "workspace-id",
8649
8663
  "write"
@@ -13399,7 +13413,7 @@ function renderConnection(d, route, dir, opts = {}) {
13399
13413
  return ` - route: ${route}
13400
13414
  enabled: false # arm it deliberately, once doctor is green
13401
13415
  ${opts.area ? ` area: "${opts.area}"
13402
- ` : ""} dir: "${dir}"
13416
+ ` : ""} ${dir === null ? "repos: {} # `crew repos clone` / `crew repos add` fill this in" : `dir: "${dir}"`}
13403
13417
  # worktreePrefix: omitted \u2014 each repo derives its own from its checkout's
13404
13418
  # directory name (ISSUE-400). Only set this if every repo this route
13405
13419
  # serves genuinely wants the SAME prefix, which is rare once an area has
@@ -13944,6 +13958,15 @@ function configuredRepos(text, route, base) {
13944
13958
  const dir = m.get("dir");
13945
13959
  return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
13946
13960
  }
13961
+ function cloneBaseOf(text, route, base, isCheckout) {
13962
+ const m = routeMapOf(parseDocument2(text), route);
13963
+ const repos = m.get("repos", true);
13964
+ if (isMap(repos) && repos.items.length > 0) return null;
13965
+ const dir = m.get("dir");
13966
+ if (!dir) return null;
13967
+ const abs = expandDir(String(dir), base);
13968
+ return isCheckout(abs) ? null : abs;
13969
+ }
13947
13970
  function routeSetsHooks(text, route, name) {
13948
13971
  const m = routeMapOf(parseDocument2(text), route);
13949
13972
  const repos = m.get("repos", true);
@@ -13954,7 +13977,8 @@ function routeSetsHooks(text, route, name) {
13954
13977
  return isMap(hooks) && hooks.items.length > 0;
13955
13978
  }
13956
13979
  function listRepoRows(i) {
13957
- const checkouts = configuredRepos(i.text, i.route, i.base);
13980
+ const cloneBase = i.isCheckout ? cloneBaseOf(i.text, i.route, i.base, i.isCheckout) : null;
13981
+ const checkouts = configuredRepos(i.text, i.route, i.base).filter((c) => c.dir !== cloneBase);
13958
13982
  const claimed = /* @__PURE__ */ new Set();
13959
13983
  const hooksSourceOf = (c) => i.exists(c.dir) && i.repoHasHooks(c.dir) ? "repo" : routeSetsHooks(i.text, i.route, c.name) ? "route" : "none";
13960
13984
  const originFor = (c, remote) => {
@@ -14004,8 +14028,19 @@ function planRepoAdd(i) {
14004
14028
  const doc = parseDocument2(i.text);
14005
14029
  if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
14006
14030
  const m = routeMapOf(doc, i.route);
14007
- const existing = configuredRepos(i.text, i.route, i.base);
14031
+ let existing = configuredRepos(i.text, i.route, i.base);
14008
14032
  const target = expandDir(i.path, i.base);
14033
+ let droppedDir;
14034
+ const placeholder = m.get("dir");
14035
+ const reposNow = m.get("repos", true);
14036
+ if (i.isCheckout && placeholder && !(isMap(reposNow) && reposNow.items.length > 0)) {
14037
+ const placeholderDir = expandDir(String(placeholder), i.base);
14038
+ if (placeholderDir !== target && !i.isCheckout(placeholderDir)) {
14039
+ droppedDir = placeholderDir;
14040
+ m.delete("dir");
14041
+ existing = [];
14042
+ }
14043
+ }
14009
14044
  if (existing.some((e) => e.dir === target)) {
14010
14045
  throw new ReposError(`${target} is already configured on route ${i.route}`);
14011
14046
  }
@@ -14031,11 +14066,14 @@ function planRepoAdd(i) {
14031
14066
  pair.key = doc.createNode("repos");
14032
14067
  pair.value = doc.createNode({ [oldName]: oldDir, [name]: target });
14033
14068
  mode = "repos";
14069
+ } else if (droppedDir) {
14070
+ m.set("repos", doc.createNode({ [name]: target }));
14071
+ mode = "repos";
14034
14072
  } else {
14035
14073
  m.set("dir", target);
14036
14074
  mode = "dir";
14037
14075
  }
14038
- return { text: String(doc), mode, name, block: renderBlock(m), warning, unmatched: !matched };
14076
+ return { text: String(doc), mode, name, block: renderBlock(m), warning, unmatched: !matched, droppedDir };
14039
14077
  }
14040
14078
  function planRepoRemove(text, base, route, path) {
14041
14079
  const doc = parseDocument2(text);
@@ -14125,7 +14163,7 @@ function planRepoClone(i) {
14125
14163
  const ship = doc.get("ship", true);
14126
14164
  const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
14127
14165
  const target = i.dir ? expandDir(i.dir, i.base) : join26(expandDir(basePath, i.base), i.route.split("/")[0], name);
14128
- const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name });
14166
+ const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name, isCheckout: i.isCheckout });
14129
14167
  return { ...add, remote, target };
14130
14168
  }
14131
14169
 
@@ -18416,6 +18454,7 @@ ${changed2.map((c) => ` - ${c}
18416
18454
  const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
18417
18455
  const text = configText;
18418
18456
  const base = dirname20(cfg.configFile);
18457
+ const isCheckout = (d) => existsSync34(`${d}/.git`);
18419
18458
  try {
18420
18459
  if (sub === "list") {
18421
18460
  if (flag("json")) {
@@ -18426,6 +18465,7 @@ ${changed2.map((c) => ` - ${c}
18426
18465
  resolved: resolvedRemotes,
18427
18466
  originOf,
18428
18467
  exists: (d) => existsSync34(d),
18468
+ isCheckout,
18429
18469
  repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
18430
18470
  try {
18431
18471
  return Object.keys(parseRepoConfig(readFileSync28(resolvePath(d, f), "utf8"), f).hooks).length > 0;
@@ -18434,11 +18474,12 @@ ${changed2.map((c) => ` - ${c}
18434
18474
  }
18435
18475
  })
18436
18476
  });
18437
- process.stdout.write(`${JSON.stringify({ route: routeName, repos: rows })}
18477
+ process.stdout.write(`${JSON.stringify({ route: routeName, repos: rows, cloneBase: cloneBaseOf(text, routeName, base, isCheckout) })}
18438
18478
  `);
18439
18479
  break;
18440
18480
  }
18441
- const repos = configuredRepos(text, routeName, base);
18481
+ const cloneBase = cloneBaseOf(text, routeName, base, isCheckout);
18482
+ const repos = configuredRepos(text, routeName, base).filter((r) => r.dir !== cloneBase);
18442
18483
  if (repos.length === 0) process.stdout.write(`route ${routeName} has no checkout configured \u2014 \`crew repos add ${routeName} <path>\`
18443
18484
  `);
18444
18485
  for (const r of repos) {
@@ -18455,7 +18496,7 @@ ${changed2.map((c) => ` - ${c}
18455
18496
  const say = (event, extra, human) => process.stdout.write(json ? `${JSON.stringify({ event, ...extra })}
18456
18497
  ` : `${human}
18457
18498
  `);
18458
- const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir") });
18499
+ const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir"), isCheckout });
18459
18500
  if (existsSync34(plan2.target) && (existsSync34(`${plan2.target}/.git`) || readdirSync14(plan2.target).length > 0)) {
18460
18501
  throw new ReposError(
18461
18502
  existsSync34(`${plan2.target}/.git`) ? `${plan2.target} is already a checkout \u2014 attach it with \`crew repos add ${routeName} ${plan2.name} ${plan2.target}\`` : `${plan2.target} exists and is not empty \u2014 pass --dir to clone somewhere else`
@@ -18477,6 +18518,8 @@ ${changed2.map((c) => ` - ${c}
18477
18518
  crew.yaml was not changed`);
18478
18519
  }
18479
18520
  writeFileSync22(cfg.configFile, plan2.text);
18521
+ if (plan2.droppedDir) process.stderr.write(`crew repos: dropped dir: ${plan2.droppedDir} from route ${routeName} \u2014 it is not a checkout (the clone base); checkouts now live under repos:
18522
+ `);
18480
18523
  say("attached", { route: routeName, name: plan2.name, dir: plan2.target, mode: plan2.mode }, `cloned and attached ${plan2.target} to ${routeName} as ${plan2.mode === "dir" ? "dir" : `repos.${plan2.name}`} in ${cfg.configFile}`);
18481
18524
  break;
18482
18525
  }
@@ -18492,8 +18535,11 @@ ${changed2.map((c) => ` - ${c}
18492
18535
  origin,
18493
18536
  resolved: resolvedRemotes,
18494
18537
  name: value("name") ?? addName,
18495
- nameForExisting: (d) => matchRepoName(originOf(d), resolvedRemotes)
18538
+ nameForExisting: (d) => matchRepoName(originOf(d), resolvedRemotes),
18539
+ isCheckout
18496
18540
  });
18541
+ if (plan.droppedDir) process.stderr.write(`crew repos: dropped dir: ${plan.droppedDir} from route ${routeName} \u2014 it is not a checkout (the clone base); checkouts now live under repos:
18542
+ `);
18497
18543
  let rowTracker;
18498
18544
  let rowPayload;
18499
18545
  if (plan.unmatched && origin && !flag("no-create")) {
@@ -19110,11 +19156,11 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
19110
19156
  const keyPersisted = keychainBacked || apiKeyFile !== void 0;
19111
19157
  const wizard = firstRun;
19112
19158
  const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
19113
- const routeDir = value("dir") ?? (writeFirstRun ? join35(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
19159
+ const routeDir = value("dir");
19114
19160
  const routeBlock = renderConnection(
19115
19161
  found,
19116
19162
  route2,
19117
- routeDir ?? "REPLACE \u2014 the local checkout this route works",
19163
+ routeDir ?? (writeFirstRun ? null : "REPLACE \u2014 the local checkout this route works"),
19118
19164
  {
19119
19165
  area: value("area"),
19120
19166
  apiKeyFile,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tablation/crew",
3
- "version": "0.72.1",
3
+ "version": "0.72.2",
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",