@tablation/crew 0.72.0 → 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.
- package/dist/cli.js +95 -34
- 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
|
|
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
|
|
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"
|
|
@@ -8798,40 +8812,47 @@ function readPointers(stateDir) {
|
|
|
8798
8812
|
try {
|
|
8799
8813
|
const parsed = JSON.parse(readFileSync16(lastSessionFile(stateDir), "utf8"));
|
|
8800
8814
|
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return {};
|
|
8801
|
-
|
|
8815
|
+
const out = {};
|
|
8816
|
+
for (const [host, v] of Object.entries(parsed)) {
|
|
8817
|
+
const slugs = (Array.isArray(v) ? v : [v]).filter((x) => typeof x === "string" && x !== "");
|
|
8818
|
+
if (slugs.length) out[host] = slugs;
|
|
8819
|
+
}
|
|
8820
|
+
return out;
|
|
8802
8821
|
} catch {
|
|
8803
8822
|
return {};
|
|
8804
8823
|
}
|
|
8805
8824
|
}
|
|
8825
|
+
function writePointers(stateDir, pointers) {
|
|
8826
|
+
const file = lastSessionFile(stateDir);
|
|
8827
|
+
mkdirSync10(dirname8(file), { recursive: true });
|
|
8828
|
+
writeFileSync11(file, `${JSON.stringify(pointers, null, 2)}
|
|
8829
|
+
`, { mode: 384 });
|
|
8830
|
+
}
|
|
8806
8831
|
function rememberSession(stateDir, host, workspaceSlug) {
|
|
8807
8832
|
try {
|
|
8808
|
-
const
|
|
8809
|
-
|
|
8810
|
-
writeFileSync11(file, `${JSON.stringify({ ...readPointers(stateDir), [host]: workspaceSlug }, null, 2)}
|
|
8811
|
-
`, { mode: 384 });
|
|
8833
|
+
const all = readPointers(stateDir);
|
|
8834
|
+
writePointers(stateDir, { ...all, [host]: [...(all[host] ?? []).filter((w) => w !== workspaceSlug), workspaceSlug] });
|
|
8812
8835
|
} catch {
|
|
8813
8836
|
}
|
|
8814
8837
|
}
|
|
8815
|
-
async function
|
|
8816
|
-
const
|
|
8817
|
-
|
|
8818
|
-
|
|
8819
|
-
|
|
8820
|
-
|
|
8821
|
-
|
|
8822
|
-
|
|
8823
|
-
} catch {
|
|
8824
|
-
return void 0;
|
|
8838
|
+
async function resumableSessions(o) {
|
|
8839
|
+
const out = [];
|
|
8840
|
+
for (const workspace of readPointers(o.stateDir)[o.host] ?? []) {
|
|
8841
|
+
try {
|
|
8842
|
+
const stored = await o.store.get(o.host, workspace);
|
|
8843
|
+
if (stored?.apiKey && await o.verify(stored.apiKey)) out.push({ workspace, apiKey: stored.apiKey });
|
|
8844
|
+
} catch {
|
|
8845
|
+
}
|
|
8825
8846
|
}
|
|
8847
|
+
return out;
|
|
8826
8848
|
}
|
|
8827
8849
|
function rememberedSessions(stateDir) {
|
|
8828
|
-
return Object.entries(readPointers(stateDir)).
|
|
8850
|
+
return Object.entries(readPointers(stateDir)).flatMap(([host, ws]) => ws.map((workspace) => ({ host, workspace })));
|
|
8829
8851
|
}
|
|
8830
8852
|
function forgetSession(stateDir, host) {
|
|
8831
8853
|
try {
|
|
8832
8854
|
const { [host]: _gone, ...rest } = readPointers(stateDir);
|
|
8833
|
-
if (Object.keys(rest).length)
|
|
8834
|
-
`, { mode: 384 });
|
|
8855
|
+
if (Object.keys(rest).length) writePointers(stateDir, rest);
|
|
8835
8856
|
else rmSync10(lastSessionFile(stateDir), { force: true });
|
|
8836
8857
|
} catch {
|
|
8837
8858
|
}
|
|
@@ -13392,7 +13413,7 @@ function renderConnection(d, route, dir, opts = {}) {
|
|
|
13392
13413
|
return ` - route: ${route}
|
|
13393
13414
|
enabled: false # arm it deliberately, once doctor is green
|
|
13394
13415
|
${opts.area ? ` area: "${opts.area}"
|
|
13395
|
-
` : ""} dir: "${dir}"
|
|
13416
|
+
` : ""} ${dir === null ? "repos: {} # `crew repos clone` / `crew repos add` fill this in" : `dir: "${dir}"`}
|
|
13396
13417
|
# worktreePrefix: omitted \u2014 each repo derives its own from its checkout's
|
|
13397
13418
|
# directory name (ISSUE-400). Only set this if every repo this route
|
|
13398
13419
|
# serves genuinely wants the SAME prefix, which is rare once an area has
|
|
@@ -13937,6 +13958,15 @@ function configuredRepos(text, route, base) {
|
|
|
13937
13958
|
const dir = m.get("dir");
|
|
13938
13959
|
return dir ? [{ name: basename8(String(dir)), dir: expandDir(String(dir), base) }] : [];
|
|
13939
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
|
+
}
|
|
13940
13970
|
function routeSetsHooks(text, route, name) {
|
|
13941
13971
|
const m = routeMapOf(parseDocument2(text), route);
|
|
13942
13972
|
const repos = m.get("repos", true);
|
|
@@ -13947,7 +13977,8 @@ function routeSetsHooks(text, route, name) {
|
|
|
13947
13977
|
return isMap(hooks) && hooks.items.length > 0;
|
|
13948
13978
|
}
|
|
13949
13979
|
function listRepoRows(i) {
|
|
13950
|
-
const
|
|
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);
|
|
13951
13982
|
const claimed = /* @__PURE__ */ new Set();
|
|
13952
13983
|
const hooksSourceOf = (c) => i.exists(c.dir) && i.repoHasHooks(c.dir) ? "repo" : routeSetsHooks(i.text, i.route, c.name) ? "route" : "none";
|
|
13953
13984
|
const originFor = (c, remote) => {
|
|
@@ -13997,8 +14028,19 @@ function planRepoAdd(i) {
|
|
|
13997
14028
|
const doc = parseDocument2(i.text);
|
|
13998
14029
|
if (doc.errors.length > 0) throw new ReposError(`config is not valid YAML \u2014 ${doc.errors[0].message}`);
|
|
13999
14030
|
const m = routeMapOf(doc, i.route);
|
|
14000
|
-
|
|
14031
|
+
let existing = configuredRepos(i.text, i.route, i.base);
|
|
14001
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
|
+
}
|
|
14002
14044
|
if (existing.some((e) => e.dir === target)) {
|
|
14003
14045
|
throw new ReposError(`${target} is already configured on route ${i.route}`);
|
|
14004
14046
|
}
|
|
@@ -14024,11 +14066,14 @@ function planRepoAdd(i) {
|
|
|
14024
14066
|
pair.key = doc.createNode("repos");
|
|
14025
14067
|
pair.value = doc.createNode({ [oldName]: oldDir, [name]: target });
|
|
14026
14068
|
mode = "repos";
|
|
14069
|
+
} else if (droppedDir) {
|
|
14070
|
+
m.set("repos", doc.createNode({ [name]: target }));
|
|
14071
|
+
mode = "repos";
|
|
14027
14072
|
} else {
|
|
14028
14073
|
m.set("dir", target);
|
|
14029
14074
|
mode = "dir";
|
|
14030
14075
|
}
|
|
14031
|
-
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 };
|
|
14032
14077
|
}
|
|
14033
14078
|
function planRepoRemove(text, base, route, path) {
|
|
14034
14079
|
const doc = parseDocument2(text);
|
|
@@ -14118,7 +14163,7 @@ function planRepoClone(i) {
|
|
|
14118
14163
|
const ship = doc.get("ship", true);
|
|
14119
14164
|
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
14120
14165
|
const target = i.dir ? expandDir(i.dir, i.base) : join26(expandDir(basePath, i.base), i.route.split("/")[0], name);
|
|
14121
|
-
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 });
|
|
14122
14167
|
return { ...add, remote, target };
|
|
14123
14168
|
}
|
|
14124
14169
|
|
|
@@ -18409,6 +18454,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18409
18454
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
18410
18455
|
const text = configText;
|
|
18411
18456
|
const base = dirname20(cfg.configFile);
|
|
18457
|
+
const isCheckout = (d) => existsSync34(`${d}/.git`);
|
|
18412
18458
|
try {
|
|
18413
18459
|
if (sub === "list") {
|
|
18414
18460
|
if (flag("json")) {
|
|
@@ -18419,6 +18465,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18419
18465
|
resolved: resolvedRemotes,
|
|
18420
18466
|
originOf,
|
|
18421
18467
|
exists: (d) => existsSync34(d),
|
|
18468
|
+
isCheckout,
|
|
18422
18469
|
repoHasHooks: (d) => REPO_CONFIG_FILENAMES.some((f) => {
|
|
18423
18470
|
try {
|
|
18424
18471
|
return Object.keys(parseRepoConfig(readFileSync28(resolvePath(d, f), "utf8"), f).hooks).length > 0;
|
|
@@ -18427,11 +18474,12 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18427
18474
|
}
|
|
18428
18475
|
})
|
|
18429
18476
|
});
|
|
18430
|
-
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) })}
|
|
18431
18478
|
`);
|
|
18432
18479
|
break;
|
|
18433
18480
|
}
|
|
18434
|
-
const
|
|
18481
|
+
const cloneBase = cloneBaseOf(text, routeName, base, isCheckout);
|
|
18482
|
+
const repos = configuredRepos(text, routeName, base).filter((r) => r.dir !== cloneBase);
|
|
18435
18483
|
if (repos.length === 0) process.stdout.write(`route ${routeName} has no checkout configured \u2014 \`crew repos add ${routeName} <path>\`
|
|
18436
18484
|
`);
|
|
18437
18485
|
for (const r of repos) {
|
|
@@ -18448,7 +18496,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18448
18496
|
const say = (event, extra, human) => process.stdout.write(json ? `${JSON.stringify({ event, ...extra })}
|
|
18449
18497
|
` : `${human}
|
|
18450
18498
|
`);
|
|
18451
|
-
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 });
|
|
18452
18500
|
if (existsSync34(plan2.target) && (existsSync34(`${plan2.target}/.git`) || readdirSync14(plan2.target).length > 0)) {
|
|
18453
18501
|
throw new ReposError(
|
|
18454
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`
|
|
@@ -18470,6 +18518,8 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18470
18518
|
crew.yaml was not changed`);
|
|
18471
18519
|
}
|
|
18472
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
|
+
`);
|
|
18473
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}`);
|
|
18474
18524
|
break;
|
|
18475
18525
|
}
|
|
@@ -18485,8 +18535,11 @@ ${changed2.map((c) => ` - ${c}
|
|
|
18485
18535
|
origin,
|
|
18486
18536
|
resolved: resolvedRemotes,
|
|
18487
18537
|
name: value("name") ?? addName,
|
|
18488
|
-
nameForExisting: (d) => matchRepoName(originOf(d), resolvedRemotes)
|
|
18538
|
+
nameForExisting: (d) => matchRepoName(originOf(d), resolvedRemotes),
|
|
18539
|
+
isCheckout
|
|
18489
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
|
+
`);
|
|
18490
18543
|
let rowTracker;
|
|
18491
18544
|
let rowPayload;
|
|
18492
18545
|
if (plan.unmatched && origin && !flag("no-create")) {
|
|
@@ -18686,8 +18739,8 @@ ${reconstruction.prompt}
|
|
|
18686
18739
|
}
|
|
18687
18740
|
}
|
|
18688
18741
|
let signInSource = apiKey ? "key" : "device";
|
|
18689
|
-
if (!apiKey && !workspace &&
|
|
18690
|
-
const
|
|
18742
|
+
if (!apiKey && !workspace && store) {
|
|
18743
|
+
const resumable = await resumableSessions({
|
|
18691
18744
|
store,
|
|
18692
18745
|
stateDir: cfg.ship.stateDir,
|
|
18693
18746
|
host: hostFromUrl4(baseUrl),
|
|
@@ -18700,6 +18753,14 @@ ${reconstruction.prompt}
|
|
|
18700
18753
|
}
|
|
18701
18754
|
}
|
|
18702
18755
|
});
|
|
18756
|
+
if (resumable.length > 1 && connectEvents.json) {
|
|
18757
|
+
stopForChoice(
|
|
18758
|
+
"workspace",
|
|
18759
|
+
`${resumable.length} workspaces have a remembered sign-in on ${hostFromUrl4(baseUrl)} \u2014 which one?`,
|
|
18760
|
+
resumable.map((r) => ({ value: r.workspace, label: r.workspace }))
|
|
18761
|
+
);
|
|
18762
|
+
}
|
|
18763
|
+
const resumed = resumable.length === 1 ? resumable[0] : void 0;
|
|
18703
18764
|
if (resumed) {
|
|
18704
18765
|
apiKey = resumed.apiKey;
|
|
18705
18766
|
workspace = resumed.workspace;
|
|
@@ -19095,11 +19156,11 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
19095
19156
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
19096
19157
|
const wizard = firstRun;
|
|
19097
19158
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
19098
|
-
const routeDir = value("dir")
|
|
19159
|
+
const routeDir = value("dir");
|
|
19099
19160
|
const routeBlock = renderConnection(
|
|
19100
19161
|
found,
|
|
19101
19162
|
route2,
|
|
19102
|
-
routeDir ?? "REPLACE \u2014 the local checkout this route works",
|
|
19163
|
+
routeDir ?? (writeFirstRun ? null : "REPLACE \u2014 the local checkout this route works"),
|
|
19103
19164
|
{
|
|
19104
19165
|
area: value("area"),
|
|
19105
19166
|
apiKeyFile,
|
package/package.json
CHANGED