@tablation/crew 0.60.0 → 0.62.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.
- package/README.md +8 -0
- package/dist/cli.js +678 -348
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2332,6 +2332,7 @@ function parseResolved(raw, m, where) {
|
|
|
2332
2332
|
...typeof raw.releaseSeat === "string" && raw.releaseSeat ? { releaseSeat: raw.releaseSeat } : {},
|
|
2333
2333
|
...typeof raw.releaseAgentId === "string" && raw.releaseAgentId ? { releaseAgentId: raw.releaseAgentId } : {},
|
|
2334
2334
|
...typeof raw.role === "string" && raw.role ? { role: raw.role } : {},
|
|
2335
|
+
...Array.isArray(raw.capabilityMisses) && raw.capabilityMisses.length > 0 ? { capabilityMisses: raw.capabilityMisses.filter((x) => typeof x === "string") } : {},
|
|
2335
2336
|
...Object.keys(agentPersonas).length > 0 ? { agentPersonas } : {},
|
|
2336
2337
|
...Object.keys(agentSkills).length > 0 ? { agentSkills } : {}
|
|
2337
2338
|
};
|
|
@@ -5182,10 +5183,11 @@ function doctorSelftestLines(stored, toolchains) {
|
|
|
5182
5183
|
}
|
|
5183
5184
|
|
|
5184
5185
|
// src/cli.ts
|
|
5185
|
-
import { resolve as resolve5, dirname as
|
|
5186
|
+
import { resolve as resolve5, dirname as dirname17, join as join30, basename as basename10 } from "path";
|
|
5186
5187
|
import { fileURLToPath } from "url";
|
|
5187
5188
|
import { hostname as hostname2, homedir as homedir8 } from "os";
|
|
5188
5189
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
5190
|
+
import { parse as parseYaml2 } from "yaml";
|
|
5189
5191
|
|
|
5190
5192
|
// src/argv.ts
|
|
5191
5193
|
var VALUE_FLAGS = /* @__PURE__ */ new Set([
|
|
@@ -5760,154 +5762,105 @@ var State = class _State {
|
|
|
5760
5762
|
}
|
|
5761
5763
|
};
|
|
5762
5764
|
|
|
5763
|
-
// src/
|
|
5764
|
-
import {
|
|
5765
|
-
import {
|
|
5766
|
-
|
|
5767
|
-
|
|
5768
|
-
var DEFAULT_CONTRACT = {
|
|
5769
|
-
columns: {
|
|
5770
|
-
key: "issue_id",
|
|
5771
|
-
tag: "issue_tag",
|
|
5772
|
-
title: "title",
|
|
5773
|
-
status: "status",
|
|
5774
|
-
assignee: "assignee_id",
|
|
5775
|
-
priority: "priority",
|
|
5776
|
-
severity: "severity",
|
|
5777
|
-
slice: "project_id",
|
|
5778
|
-
repo: "repo_id",
|
|
5779
|
-
parent: "parent_id",
|
|
5780
|
-
blockedBy: "blocked_by",
|
|
5781
|
-
updatedAt: "updated_at",
|
|
5782
|
-
needsDesign: "needs_design",
|
|
5783
|
-
attachments: "attachments",
|
|
5784
|
-
workMode: "work_mode",
|
|
5785
|
-
reportType: "report_type",
|
|
5786
|
-
epic: "epic_id",
|
|
5787
|
-
author: "filed_by_id",
|
|
5788
|
-
ship: "ship_id",
|
|
5789
|
-
heldBy: "held_by_ship_id",
|
|
5790
|
-
resolutionNote: "resolution_note",
|
|
5791
|
-
verifiedSha: "verified_sha",
|
|
5792
|
-
prRef: "pr_ref",
|
|
5793
|
-
position: "position"
|
|
5794
|
-
},
|
|
5795
|
-
comments: {
|
|
5796
|
-
parent: "ticket_id",
|
|
5797
|
-
body: "body",
|
|
5798
|
-
author: "team_member_id",
|
|
5799
|
-
createdAt: "created_at",
|
|
5800
|
-
ship: "ship_id",
|
|
5801
|
-
publicReply: "is_public_reply",
|
|
5802
|
-
authorName: "author_name"
|
|
5803
|
-
},
|
|
5804
|
-
statuses: {
|
|
5805
|
-
open: ["new", "accepted", "blocked", "in_progress", "needs_info", "fixed", "qa", "verified"],
|
|
5806
|
-
resolved: [
|
|
5807
|
-
"verified",
|
|
5808
|
-
"closed_deployed",
|
|
5809
|
-
"closed_wont_fix",
|
|
5810
|
-
"closed_duplicate",
|
|
5811
|
-
"closed_completed",
|
|
5812
|
-
"closed_cant_reproduce"
|
|
5813
|
-
],
|
|
5814
|
-
approved: "accepted",
|
|
5815
|
-
parked: "blocked",
|
|
5816
|
-
building: "in_progress",
|
|
5817
|
-
handoff: "fixed",
|
|
5818
|
-
verifying: "qa",
|
|
5819
|
-
verified: "verified",
|
|
5820
|
-
deployed: "closed_deployed",
|
|
5821
|
-
needsHuman: "needs_info",
|
|
5822
|
-
reviewing: null,
|
|
5823
|
-
wontFix: "closed_wont_fix",
|
|
5824
|
-
draft: "draft"
|
|
5825
|
-
},
|
|
5826
|
-
epics: {
|
|
5827
|
-
statusColumn: "status",
|
|
5828
|
-
keyColumn: "epic_id",
|
|
5829
|
-
statuses: { planned: "planned", building: "in_progress", done: "done", cancelled: "cancelled" }
|
|
5830
|
-
},
|
|
5831
|
-
priorityOrder: ["p0", "p1", "p2", "p3"],
|
|
5832
|
-
severityOrder: ["s1", "s2", "s3", "s4"],
|
|
5833
|
-
unknownPriorityRank: 2,
|
|
5834
|
-
coordinatingValue: "coordinating",
|
|
5835
|
-
manualWorkMode: "manual",
|
|
5836
|
-
hybridWorkMode: "hybrid",
|
|
5837
|
-
repoExemptReportTypes: ["coordinating", "question", "investigation"]
|
|
5838
|
-
};
|
|
5839
|
-
function closedStatuses(c) {
|
|
5840
|
-
return c.statuses.resolved.filter((s) => s !== c.statuses.verified);
|
|
5765
|
+
// src/reset.ts
|
|
5766
|
+
import { existsSync as existsSync14, mkdirSync as mkdirSync7, readFileSync as readFileSync13, readdirSync as readdirSync4, rmSync as rmSync7, statSync as statSync5, writeFileSync as writeFileSync8 } from "fs";
|
|
5767
|
+
import { dirname as dirname6, join as join14 } from "path";
|
|
5768
|
+
function clonedCheckoutsFile(stateDir) {
|
|
5769
|
+
return join14(stateDir, "cloned-checkouts.json");
|
|
5841
5770
|
}
|
|
5842
|
-
function
|
|
5843
|
-
|
|
5844
|
-
|
|
5845
|
-
|
|
5846
|
-
|
|
5847
|
-
|
|
5848
|
-
epics: {
|
|
5849
|
-
...DEFAULT_CONTRACT.epics,
|
|
5850
|
-
...override.epics,
|
|
5851
|
-
statuses: { ...DEFAULT_CONTRACT.epics.statuses, ...override.epics?.statuses }
|
|
5852
|
-
},
|
|
5853
|
-
priorityOrder: override.priorityOrder ?? DEFAULT_CONTRACT.priorityOrder,
|
|
5854
|
-
severityOrder: override.severityOrder ?? DEFAULT_CONTRACT.severityOrder,
|
|
5855
|
-
unknownPriorityRank: override.unknownPriorityRank ?? DEFAULT_CONTRACT.unknownPriorityRank,
|
|
5856
|
-
coordinatingValue: override.coordinatingValue ?? DEFAULT_CONTRACT.coordinatingValue,
|
|
5857
|
-
manualWorkMode: override.manualWorkMode ?? DEFAULT_CONTRACT.manualWorkMode,
|
|
5858
|
-
hybridWorkMode: override.hybridWorkMode ?? DEFAULT_CONTRACT.hybridWorkMode,
|
|
5859
|
-
// A workspace overriding `coordinatingValue` alone (renaming the rollup
|
|
5860
|
-
// marker) still gets that renamed value exempted here by default,
|
|
5861
|
-
// without also having to repeat it — only an explicit
|
|
5862
|
-
// `repoExemptReportTypes` overrides this derived default.
|
|
5863
|
-
repoExemptReportTypes: override.repoExemptReportTypes ?? (override.coordinatingValue ? [override.coordinatingValue, "question", "investigation"] : DEFAULT_CONTRACT.repoExemptReportTypes)
|
|
5864
|
-
};
|
|
5865
|
-
}
|
|
5866
|
-
function validateContract(c) {
|
|
5867
|
-
const problems = [];
|
|
5868
|
-
const open = new Set(c.statuses.open);
|
|
5869
|
-
const resolved = new Set(c.statuses.resolved);
|
|
5870
|
-
for (const role of ["approved", "parked", "building", "handoff", "verifying", "needsHuman"]) {
|
|
5871
|
-
const v = c.statuses[role];
|
|
5872
|
-
if (!open.has(v)) problems.push(`statuses.${role} ("${v}") is not listed in statuses.open`);
|
|
5771
|
+
function readClonedCheckouts(stateDir) {
|
|
5772
|
+
try {
|
|
5773
|
+
const parsed = JSON.parse(readFileSync13(clonedCheckoutsFile(stateDir), "utf8"));
|
|
5774
|
+
return Array.isArray(parsed) ? parsed.filter((d) => typeof d === "string") : [];
|
|
5775
|
+
} catch {
|
|
5776
|
+
return [];
|
|
5873
5777
|
}
|
|
5874
|
-
|
|
5875
|
-
|
|
5778
|
+
}
|
|
5779
|
+
function recordClonedCheckout(stateDir, dir) {
|
|
5780
|
+
const known = readClonedCheckouts(stateDir);
|
|
5781
|
+
if (known.includes(dir)) return;
|
|
5782
|
+
const file = clonedCheckoutsFile(stateDir);
|
|
5783
|
+
mkdirSync7(dirname6(file), { recursive: true });
|
|
5784
|
+
writeFileSync8(file, `${JSON.stringify([...known, dir], null, 2)}
|
|
5785
|
+
`);
|
|
5786
|
+
}
|
|
5787
|
+
var mark = (dryRun) => dryRun ? "would-remove" : "removed";
|
|
5788
|
+
async function runReset(o, fx) {
|
|
5789
|
+
const steps = [];
|
|
5790
|
+
for (const line of await fx.uninstallUnits(o.dryRun)) steps.push({ step: "scheduler", target: line, action: mark(o.dryRun) });
|
|
5791
|
+
const urls = [...new Set(o.routes.map((r) => r.baseUrl))];
|
|
5792
|
+
for (const url of urls) {
|
|
5793
|
+
for (const line of fx.removePair(url, o.dryRun)) steps.push({ step: "pair", target: line, action: mark(o.dryRun) });
|
|
5876
5794
|
}
|
|
5877
|
-
|
|
5878
|
-
|
|
5795
|
+
for (const route of o.routes) {
|
|
5796
|
+
for (const r of await fx.revokeSeatKeys(route, o.dryRun)) {
|
|
5797
|
+
steps.push({ step: "seat-keys", target: `${route.route}: ${r.line}`, action: r.ok ? mark(o.dryRun) : "failed" });
|
|
5798
|
+
}
|
|
5879
5799
|
}
|
|
5880
|
-
|
|
5881
|
-
|
|
5882
|
-
|
|
5883
|
-
|
|
5800
|
+
for (const route of [...o.routes, ...o.droppedRoutes ?? []]) {
|
|
5801
|
+
try {
|
|
5802
|
+
const cred = await fx.credential(route);
|
|
5803
|
+
if (!cred.stored) {
|
|
5804
|
+
steps.push({ step: "credential", target: route.route, action: "absent", detail: "no workspace credential in the keychain" });
|
|
5805
|
+
continue;
|
|
5806
|
+
}
|
|
5807
|
+
if (!o.dryRun) await cred.clear();
|
|
5808
|
+
steps.push({ step: "credential", target: route.route, action: mark(o.dryRun), detail: "workspace credential in the keychain" });
|
|
5809
|
+
} catch (e) {
|
|
5810
|
+
steps.push({ step: "credential", target: route.route, action: "failed", detail: e.message });
|
|
5811
|
+
}
|
|
5884
5812
|
}
|
|
5885
|
-
if (
|
|
5886
|
-
|
|
5813
|
+
if (o.removeCheckouts) {
|
|
5814
|
+
for (const dir of readClonedCheckouts(o.stateDir)) steps.push(removeCheckout(dir, o.dryRun, fx));
|
|
5887
5815
|
}
|
|
5888
|
-
|
|
5889
|
-
|
|
5890
|
-
|
|
5891
|
-
);
|
|
5816
|
+
steps.push(...removeState(o.stateDir, o.dryRun));
|
|
5817
|
+
if (existsSync14(o.configFile)) {
|
|
5818
|
+
if (!o.dryRun) rmSync7(o.configFile, { force: true });
|
|
5819
|
+
steps.push({ step: "config", target: o.configFile, action: mark(o.dryRun) });
|
|
5820
|
+
} else {
|
|
5821
|
+
steps.push({ step: "config", target: o.configFile, action: "absent" });
|
|
5892
5822
|
}
|
|
5893
|
-
|
|
5894
|
-
|
|
5823
|
+
return steps;
|
|
5824
|
+
}
|
|
5825
|
+
function removeCheckout(dir, dryRun, fx) {
|
|
5826
|
+
if (!existsSync14(dir)) return { step: "checkout", target: dir, action: "absent", detail: "already gone" };
|
|
5827
|
+
const { dirty, linkedWorktrees } = fx.inspectCheckout(dir);
|
|
5828
|
+
if (dirty.length) {
|
|
5829
|
+
return { step: "checkout", target: dir, action: "kept", detail: `has uncommitted changes (${dirty.length} path${dirty.length === 1 ? "" : "s"}: ${dirty.slice(0, 5).join(", ")}${dirty.length > 5 ? ", \u2026" : ""})` };
|
|
5895
5830
|
}
|
|
5896
|
-
if (
|
|
5897
|
-
|
|
5831
|
+
if (linkedWorktrees.length) {
|
|
5832
|
+
return { step: "checkout", target: dir, action: "kept", detail: `has linked worktrees (${linkedWorktrees.join(", ")}); remove them first` };
|
|
5898
5833
|
}
|
|
5899
|
-
|
|
5900
|
-
|
|
5901
|
-
|
|
5834
|
+
if (!dryRun) rmSync7(dir, { recursive: true, force: true });
|
|
5835
|
+
return { step: "checkout", target: dir, action: mark(dryRun) };
|
|
5836
|
+
}
|
|
5837
|
+
function removeState(stateDir, dryRun) {
|
|
5838
|
+
const out = [];
|
|
5839
|
+
const targets = [join14(stateDir, "resolved"), join14(stateDir, "keys"), clonedCheckoutsFile(stateDir), join14(stateDir, "reload-requested"), join14(stateDir, "daemon.started")];
|
|
5840
|
+
try {
|
|
5841
|
+
for (const name of readdirSync4(stateDir)) {
|
|
5842
|
+
if (name.startsWith(".") && statSync5(join14(stateDir, name)).isFile()) targets.push(join14(stateDir, name));
|
|
5843
|
+
}
|
|
5844
|
+
} catch {
|
|
5902
5845
|
}
|
|
5903
|
-
|
|
5904
|
-
|
|
5905
|
-
|
|
5906
|
-
|
|
5846
|
+
for (const t of targets) {
|
|
5847
|
+
if (!existsSync14(t)) continue;
|
|
5848
|
+
if (!dryRun) rmSync7(t, { recursive: true, force: true });
|
|
5849
|
+
out.push({ step: "state", target: t, action: mark(dryRun) });
|
|
5907
5850
|
}
|
|
5908
|
-
|
|
5851
|
+
if (!out.length) out.push({ step: "state", target: stateDir, action: "absent", detail: "nothing to remove" });
|
|
5852
|
+
return out;
|
|
5853
|
+
}
|
|
5854
|
+
function describeResetStep(s) {
|
|
5855
|
+
const verb = { removed: "removed", "would-remove": "would remove", absent: "nothing to remove", kept: "KEPT", failed: "FAILED" }[s.action];
|
|
5856
|
+
return `${s.step}: ${verb}: ${s.target}${s.detail ? ` (${s.detail})` : ""}`;
|
|
5857
|
+
}
|
|
5858
|
+
function resetIncomplete(steps) {
|
|
5859
|
+
return steps.some((s) => s.action === "failed" || s.action === "kept");
|
|
5909
5860
|
}
|
|
5910
|
-
|
|
5861
|
+
|
|
5862
|
+
// src/crew-floor.ts
|
|
5863
|
+
import { TablationApiError as TablationApiError3 } from "@tablation/client";
|
|
5911
5864
|
|
|
5912
5865
|
// src/callout.ts
|
|
5913
5866
|
var calloutHeader = (kind, label) => `> [!${kind}]
|
|
@@ -6154,24 +6107,24 @@ function recoveredFailureHeadlines(o, route) {
|
|
|
6154
6107
|
}
|
|
6155
6108
|
|
|
6156
6109
|
// src/ship-attention.ts
|
|
6157
|
-
import { existsSync as
|
|
6110
|
+
import { existsSync as existsSync16, statfsSync } from "fs";
|
|
6158
6111
|
|
|
6159
6112
|
// src/git.ts
|
|
6160
6113
|
import { execFileSync as execFileSync2 } from "child_process";
|
|
6161
6114
|
import {
|
|
6162
|
-
existsSync as
|
|
6163
|
-
mkdirSync as
|
|
6115
|
+
existsSync as existsSync15,
|
|
6116
|
+
mkdirSync as mkdirSync8,
|
|
6164
6117
|
openSync as openSync3,
|
|
6165
6118
|
closeSync as closeSync3,
|
|
6166
6119
|
writeSync,
|
|
6167
|
-
statSync as
|
|
6120
|
+
statSync as statSync6,
|
|
6168
6121
|
unlinkSync as unlinkSync2,
|
|
6169
|
-
readFileSync as
|
|
6170
|
-
writeFileSync as
|
|
6171
|
-
readdirSync as
|
|
6172
|
-
rmSync as
|
|
6122
|
+
readFileSync as readFileSync14,
|
|
6123
|
+
writeFileSync as writeFileSync9,
|
|
6124
|
+
readdirSync as readdirSync5,
|
|
6125
|
+
rmSync as rmSync8
|
|
6173
6126
|
} from "fs";
|
|
6174
|
-
import { dirname as
|
|
6127
|
+
import { dirname as dirname7, basename as basename5, join as join15 } from "path";
|
|
6175
6128
|
var GitError = class extends Error {
|
|
6176
6129
|
};
|
|
6177
6130
|
function streamText(x) {
|
|
@@ -6537,8 +6490,8 @@ function recoverStrandedReleaseCheckouts(cwd) {
|
|
|
6537
6490
|
if (line.startsWith("worktree ")) path = line.slice("worktree ".length);
|
|
6538
6491
|
else if (line.startsWith("branch refs/heads/crew/release-") && path) {
|
|
6539
6492
|
gitOk(cwd, ["worktree", "remove", "--force", path]);
|
|
6540
|
-
const root =
|
|
6541
|
-
if (basename5(root).startsWith("crew-release-"))
|
|
6493
|
+
const root = dirname7(path);
|
|
6494
|
+
if (basename5(root).startsWith("crew-release-")) rmSync8(root, { recursive: true, force: true });
|
|
6542
6495
|
}
|
|
6543
6496
|
}
|
|
6544
6497
|
const stranded = (gitOk(cwd, ["for-each-ref", "--format=%(refname:short)", "refs/heads/crew/release-*"]) ?? "").split("\n").filter(Boolean);
|
|
@@ -6549,18 +6502,18 @@ function sweepStaleReleaseRoots(tmp, now = Date.now(), maxAgeMs = 60 * 60 * 1e3)
|
|
|
6549
6502
|
const removed = [];
|
|
6550
6503
|
let names;
|
|
6551
6504
|
try {
|
|
6552
|
-
names =
|
|
6505
|
+
names = readdirSync5(tmp);
|
|
6553
6506
|
} catch {
|
|
6554
6507
|
return removed;
|
|
6555
6508
|
}
|
|
6556
6509
|
for (const name of names) {
|
|
6557
6510
|
if (!name.startsWith("crew-release-")) continue;
|
|
6558
|
-
const root =
|
|
6511
|
+
const root = join15(tmp, name);
|
|
6559
6512
|
try {
|
|
6560
|
-
const st =
|
|
6513
|
+
const st = statSync6(root);
|
|
6561
6514
|
if (!st.isDirectory() || now - st.mtimeMs < maxAgeMs) continue;
|
|
6562
|
-
if (
|
|
6563
|
-
|
|
6515
|
+
if (existsSync15(join15(root, "checkout"))) continue;
|
|
6516
|
+
rmSync8(root, { recursive: true, force: true });
|
|
6564
6517
|
removed.push(name);
|
|
6565
6518
|
} catch {
|
|
6566
6519
|
}
|
|
@@ -6602,13 +6555,13 @@ function originUrlOf(dir, remoteName) {
|
|
|
6602
6555
|
}
|
|
6603
6556
|
}
|
|
6604
6557
|
function ensureRepoCheckout(dir, remote, opts = {}) {
|
|
6605
|
-
if (
|
|
6558
|
+
if (existsSync15(`${dir}/.git`)) return false;
|
|
6606
6559
|
if (!remote) {
|
|
6607
6560
|
throw new GitError(
|
|
6608
6561
|
`${dir} does not exist and this repo has no discovered remote to clone from \u2014 re-run \`crew connect\`, or check out the repository yourself and point \`repos:\` at it`
|
|
6609
6562
|
);
|
|
6610
6563
|
}
|
|
6611
|
-
|
|
6564
|
+
mkdirSync8(dirname7(dir), { recursive: true });
|
|
6612
6565
|
const url = cloneUrlFor(remote, opts);
|
|
6613
6566
|
try {
|
|
6614
6567
|
execFileSync2("git", ["clone", url, dir], { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -6617,8 +6570,13 @@ function ensureRepoCheckout(dir, remote, opts = {}) {
|
|
|
6617
6570
|
const why = [err.stderr, err.stdout].map(streamText).filter(Boolean).join(" / ");
|
|
6618
6571
|
throw new GitError(`git clone ${url} ${dir}: ${why || err.message.trim()}`);
|
|
6619
6572
|
}
|
|
6573
|
+
cloneRecorder?.(dir);
|
|
6620
6574
|
return true;
|
|
6621
6575
|
}
|
|
6576
|
+
var cloneRecorder;
|
|
6577
|
+
function setCloneRecorder(fn) {
|
|
6578
|
+
cloneRecorder = fn;
|
|
6579
|
+
}
|
|
6622
6580
|
var CHECKOUT_LOCK_STALE_MS = 5 * 60 * 1e3;
|
|
6623
6581
|
function checkoutLockPath(dir) {
|
|
6624
6582
|
return `${dir}.sync.lock`;
|
|
@@ -6628,7 +6586,7 @@ function sleepSync(ms2) {
|
|
|
6628
6586
|
}
|
|
6629
6587
|
function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
6630
6588
|
const lockPath = checkoutLockPath(dir);
|
|
6631
|
-
|
|
6589
|
+
mkdirSync8(dirname7(lockPath), { recursive: true });
|
|
6632
6590
|
const deadline = Date.now() + timeoutMs;
|
|
6633
6591
|
for (; ; ) {
|
|
6634
6592
|
try {
|
|
@@ -6646,7 +6604,7 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
|
6646
6604
|
const err = e;
|
|
6647
6605
|
if (err.code !== "EEXIST") throw err;
|
|
6648
6606
|
try {
|
|
6649
|
-
if (Date.now() -
|
|
6607
|
+
if (Date.now() - statSync6(lockPath).mtimeMs > CHECKOUT_LOCK_STALE_MS) {
|
|
6650
6608
|
try {
|
|
6651
6609
|
unlinkSync2(lockPath);
|
|
6652
6610
|
} catch {
|
|
@@ -6665,17 +6623,17 @@ function acquireCheckoutLock(dir, timeoutMs = 3e4) {
|
|
|
6665
6623
|
function syncPassengerCheckout(dir, githubRemote, gitRemoteName, base, clone = {}) {
|
|
6666
6624
|
const release = acquireCheckoutLock(dir);
|
|
6667
6625
|
try {
|
|
6668
|
-
if (!
|
|
6626
|
+
if (!existsSync15(`${dir}/.git`) && githubRemote) {
|
|
6669
6627
|
const url = cloneUrlFor(githubRemote, clone);
|
|
6670
6628
|
const marker = cloneFailureMarker(dir);
|
|
6671
|
-
if (
|
|
6672
|
-
return { action: "clone-failed", detail:
|
|
6629
|
+
if (existsSync15(marker) && readFileSync14(marker, "utf8").split("\n")[0] === url) {
|
|
6630
|
+
return { action: "clone-failed", detail: readFileSync14(marker, "utf8").split("\n").slice(1).join("\n"), repeat: true };
|
|
6673
6631
|
}
|
|
6674
6632
|
try {
|
|
6675
6633
|
ensureRepoCheckout(dir, githubRemote, clone);
|
|
6676
6634
|
} catch (e) {
|
|
6677
6635
|
if (!(e instanceof GitError)) throw e;
|
|
6678
|
-
|
|
6636
|
+
writeFileSync9(marker, `${url}
|
|
6679
6637
|
${e.message}`);
|
|
6680
6638
|
return { action: "clone-failed", detail: e.message, repeat: false };
|
|
6681
6639
|
}
|
|
@@ -6873,7 +6831,7 @@ function noteHeldWorktree(state, h, nowMs = Date.now()) {
|
|
|
6873
6831
|
function reconcileHeldWorktrees(state, repo, log, stillRefused) {
|
|
6874
6832
|
for (const i of state.shipAttention().previous()) {
|
|
6875
6833
|
if (i.kind !== "worktree_held" || !i.held || i.held.repo !== repo) continue;
|
|
6876
|
-
const gone = !
|
|
6834
|
+
const gone = !existsSync16(i.held.path);
|
|
6877
6835
|
if (!gone && !(stillRefused && !stillRefused.has(i.held.path))) continue;
|
|
6878
6836
|
clearShipAttention(state, i.key);
|
|
6879
6837
|
log.emit(
|
|
@@ -6951,6 +6909,223 @@ var SHELL_WORDS = /* @__PURE__ */ new Set([
|
|
|
6951
6909
|
"ok"
|
|
6952
6910
|
]);
|
|
6953
6911
|
|
|
6912
|
+
// src/crew-floor.ts
|
|
6913
|
+
var CREW_OUTDATED_KEY = "crew_outdated";
|
|
6914
|
+
function parseVersion(v) {
|
|
6915
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(v.trim());
|
|
6916
|
+
return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
|
|
6917
|
+
}
|
|
6918
|
+
function compareVersions(a, b) {
|
|
6919
|
+
const pa = parseVersion(a);
|
|
6920
|
+
const pb = parseVersion(b);
|
|
6921
|
+
if (!pa || !pb) return null;
|
|
6922
|
+
for (let i = 0; i < 3; i++) if (pa[i] !== pb[i]) return pa[i] - pb[i];
|
|
6923
|
+
return 0;
|
|
6924
|
+
}
|
|
6925
|
+
function belowFloor(have, floor) {
|
|
6926
|
+
if (!floor) return false;
|
|
6927
|
+
const c = compareVersions(have, floor);
|
|
6928
|
+
return c !== null && c < 0;
|
|
6929
|
+
}
|
|
6930
|
+
function outdatedItem(have, floor, nowIso = (/* @__PURE__ */ new Date()).toISOString()) {
|
|
6931
|
+
return {
|
|
6932
|
+
kind: "crew_outdated",
|
|
6933
|
+
key: CREW_OUTDATED_KEY,
|
|
6934
|
+
since: nowIso,
|
|
6935
|
+
message: `crew ${have} is below this workspace's minimum ${floor} \u2014 no ticket will be picked until it is updated (the Crew app updates it, or \`npm install -g @tablation/crew\`). Resumes automatically once the version satisfies the floor`
|
|
6936
|
+
};
|
|
6937
|
+
}
|
|
6938
|
+
function sentVersion(ship) {
|
|
6939
|
+
return /CrewAgent\/(\d+\.\d+\.\d+\S*)/.exec(ship.userAgent)?.[1] ?? null;
|
|
6940
|
+
}
|
|
6941
|
+
async function readCrewFloor(route, ship) {
|
|
6942
|
+
const r = route.resolved;
|
|
6943
|
+
if (!r?.projectId) return void 0;
|
|
6944
|
+
try {
|
|
6945
|
+
const res = await fetch(`${route.baseUrl}/api/projects/${encodeURIComponent(r.projectId)}?workspaceId=${r.workspaceId}`, {
|
|
6946
|
+
headers: { Authorization: `Bearer ${resolveApiKey(route)}`, "User-Agent": ship.userAgent }
|
|
6947
|
+
});
|
|
6948
|
+
if (!res.ok) return void 0;
|
|
6949
|
+
const body = await res.json();
|
|
6950
|
+
return body.crewMinVersion ?? null;
|
|
6951
|
+
} catch {
|
|
6952
|
+
return void 0;
|
|
6953
|
+
}
|
|
6954
|
+
}
|
|
6955
|
+
async function checkCrewFloor(o, deps = {}) {
|
|
6956
|
+
const have = deps.version ?? sentVersion(o.ship);
|
|
6957
|
+
if (!have) return null;
|
|
6958
|
+
const floor = await (deps.read ?? readCrewFloor)(o.route, o.ship);
|
|
6959
|
+
if (floor === void 0) return openOutdated(o.state);
|
|
6960
|
+
if (belowFloor(have, floor)) {
|
|
6961
|
+
const item = outdatedItem(have, floor);
|
|
6962
|
+
await raiseShipAttention(o, item);
|
|
6963
|
+
return item.message;
|
|
6964
|
+
}
|
|
6965
|
+
clearShipAttention(o.state, CREW_OUTDATED_KEY);
|
|
6966
|
+
return null;
|
|
6967
|
+
}
|
|
6968
|
+
function openOutdated(state) {
|
|
6969
|
+
return state.shipAttention().previous().find((i) => i.key === CREW_OUTDATED_KEY)?.message ?? null;
|
|
6970
|
+
}
|
|
6971
|
+
function isUpgradeRequired(err) {
|
|
6972
|
+
return err instanceof TablationApiError3 && err.status === 426;
|
|
6973
|
+
}
|
|
6974
|
+
async function raiseFromUpgradeRequired(o, err) {
|
|
6975
|
+
const have = sentVersion(o.ship) ?? "unknown";
|
|
6976
|
+
const floor = /(\d+\.\d+\.\d+)/.exec(err.message.replace(have, ""))?.[1] ?? "a newer version";
|
|
6977
|
+
await raiseShipAttention(o, outdatedItem(have, floor));
|
|
6978
|
+
}
|
|
6979
|
+
|
|
6980
|
+
// src/tracker.ts
|
|
6981
|
+
import { hostname } from "os";
|
|
6982
|
+
import { TablationClient as TablationClient3 } from "@tablation/client";
|
|
6983
|
+
|
|
6984
|
+
// src/contract.ts
|
|
6985
|
+
var DEFAULT_CONTRACT = {
|
|
6986
|
+
columns: {
|
|
6987
|
+
key: "issue_id",
|
|
6988
|
+
tag: "issue_tag",
|
|
6989
|
+
title: "title",
|
|
6990
|
+
status: "status",
|
|
6991
|
+
assignee: "assignee_id",
|
|
6992
|
+
priority: "priority",
|
|
6993
|
+
severity: "severity",
|
|
6994
|
+
slice: "project_id",
|
|
6995
|
+
repo: "repo_id",
|
|
6996
|
+
parent: "parent_id",
|
|
6997
|
+
blockedBy: "blocked_by",
|
|
6998
|
+
updatedAt: "updated_at",
|
|
6999
|
+
needsDesign: "needs_design",
|
|
7000
|
+
attachments: "attachments",
|
|
7001
|
+
workMode: "work_mode",
|
|
7002
|
+
reportType: "report_type",
|
|
7003
|
+
epic: "epic_id",
|
|
7004
|
+
author: "filed_by_id",
|
|
7005
|
+
ship: "ship_id",
|
|
7006
|
+
heldBy: "held_by_ship_id",
|
|
7007
|
+
resolutionNote: "resolution_note",
|
|
7008
|
+
verifiedSha: "verified_sha",
|
|
7009
|
+
prRef: "pr_ref",
|
|
7010
|
+
position: "position"
|
|
7011
|
+
},
|
|
7012
|
+
comments: {
|
|
7013
|
+
parent: "ticket_id",
|
|
7014
|
+
body: "body",
|
|
7015
|
+
author: "team_member_id",
|
|
7016
|
+
createdAt: "created_at",
|
|
7017
|
+
ship: "ship_id",
|
|
7018
|
+
publicReply: "is_public_reply",
|
|
7019
|
+
authorName: "author_name"
|
|
7020
|
+
},
|
|
7021
|
+
statuses: {
|
|
7022
|
+
open: ["new", "accepted", "blocked", "in_progress", "needs_info", "fixed", "qa", "verified"],
|
|
7023
|
+
resolved: [
|
|
7024
|
+
"verified",
|
|
7025
|
+
"closed_deployed",
|
|
7026
|
+
"closed_wont_fix",
|
|
7027
|
+
"closed_duplicate",
|
|
7028
|
+
"closed_completed",
|
|
7029
|
+
"closed_cant_reproduce"
|
|
7030
|
+
],
|
|
7031
|
+
approved: "accepted",
|
|
7032
|
+
parked: "blocked",
|
|
7033
|
+
building: "in_progress",
|
|
7034
|
+
handoff: "fixed",
|
|
7035
|
+
verifying: "qa",
|
|
7036
|
+
verified: "verified",
|
|
7037
|
+
deployed: "closed_deployed",
|
|
7038
|
+
needsHuman: "needs_info",
|
|
7039
|
+
reviewing: null,
|
|
7040
|
+
wontFix: "closed_wont_fix",
|
|
7041
|
+
draft: "draft"
|
|
7042
|
+
},
|
|
7043
|
+
epics: {
|
|
7044
|
+
statusColumn: "status",
|
|
7045
|
+
keyColumn: "epic_id",
|
|
7046
|
+
statuses: { planned: "planned", building: "in_progress", done: "done", cancelled: "cancelled" }
|
|
7047
|
+
},
|
|
7048
|
+
priorityOrder: ["p0", "p1", "p2", "p3"],
|
|
7049
|
+
severityOrder: ["s1", "s2", "s3", "s4"],
|
|
7050
|
+
unknownPriorityRank: 2,
|
|
7051
|
+
coordinatingValue: "coordinating",
|
|
7052
|
+
manualWorkMode: "manual",
|
|
7053
|
+
hybridWorkMode: "hybrid",
|
|
7054
|
+
repoExemptReportTypes: ["coordinating", "question", "investigation"]
|
|
7055
|
+
};
|
|
7056
|
+
function closedStatuses(c) {
|
|
7057
|
+
return c.statuses.resolved.filter((s) => s !== c.statuses.verified);
|
|
7058
|
+
}
|
|
7059
|
+
function resolveContract(override) {
|
|
7060
|
+
if (!override) return DEFAULT_CONTRACT;
|
|
7061
|
+
return {
|
|
7062
|
+
columns: { ...DEFAULT_CONTRACT.columns, ...override.columns },
|
|
7063
|
+
comments: { ...DEFAULT_CONTRACT.comments, ...override.comments },
|
|
7064
|
+
statuses: { ...DEFAULT_CONTRACT.statuses, ...override.statuses },
|
|
7065
|
+
epics: {
|
|
7066
|
+
...DEFAULT_CONTRACT.epics,
|
|
7067
|
+
...override.epics,
|
|
7068
|
+
statuses: { ...DEFAULT_CONTRACT.epics.statuses, ...override.epics?.statuses }
|
|
7069
|
+
},
|
|
7070
|
+
priorityOrder: override.priorityOrder ?? DEFAULT_CONTRACT.priorityOrder,
|
|
7071
|
+
severityOrder: override.severityOrder ?? DEFAULT_CONTRACT.severityOrder,
|
|
7072
|
+
unknownPriorityRank: override.unknownPriorityRank ?? DEFAULT_CONTRACT.unknownPriorityRank,
|
|
7073
|
+
coordinatingValue: override.coordinatingValue ?? DEFAULT_CONTRACT.coordinatingValue,
|
|
7074
|
+
manualWorkMode: override.manualWorkMode ?? DEFAULT_CONTRACT.manualWorkMode,
|
|
7075
|
+
hybridWorkMode: override.hybridWorkMode ?? DEFAULT_CONTRACT.hybridWorkMode,
|
|
7076
|
+
// A workspace overriding `coordinatingValue` alone (renaming the rollup
|
|
7077
|
+
// marker) still gets that renamed value exempted here by default,
|
|
7078
|
+
// without also having to repeat it — only an explicit
|
|
7079
|
+
// `repoExemptReportTypes` overrides this derived default.
|
|
7080
|
+
repoExemptReportTypes: override.repoExemptReportTypes ?? (override.coordinatingValue ? [override.coordinatingValue, "question", "investigation"] : DEFAULT_CONTRACT.repoExemptReportTypes)
|
|
7081
|
+
};
|
|
7082
|
+
}
|
|
7083
|
+
function validateContract(c) {
|
|
7084
|
+
const problems = [];
|
|
7085
|
+
const open = new Set(c.statuses.open);
|
|
7086
|
+
const resolved = new Set(c.statuses.resolved);
|
|
7087
|
+
for (const role of ["approved", "parked", "building", "handoff", "verifying", "needsHuman"]) {
|
|
7088
|
+
const v = c.statuses[role];
|
|
7089
|
+
if (!open.has(v)) problems.push(`statuses.${role} ("${v}") is not listed in statuses.open`);
|
|
7090
|
+
}
|
|
7091
|
+
if (c.statuses.reviewing && !open.has(c.statuses.reviewing)) {
|
|
7092
|
+
problems.push(`statuses.reviewing ("${c.statuses.reviewing}") is not listed in statuses.open`);
|
|
7093
|
+
}
|
|
7094
|
+
if (!resolved.has(c.statuses.deployed)) {
|
|
7095
|
+
problems.push(`statuses.deployed ("${c.statuses.deployed}") is not listed in statuses.resolved`);
|
|
7096
|
+
}
|
|
7097
|
+
if (resolved.has(c.statuses.handoff)) {
|
|
7098
|
+
problems.push(
|
|
7099
|
+
`statuses.handoff ("${c.statuses.handoff}") is listed as resolved \u2014 it means built-but-unchecked, so a ticket blocked by it is still blocked; calling it resolved would build on unverified work`
|
|
7100
|
+
);
|
|
7101
|
+
}
|
|
7102
|
+
if (!resolved.has(c.statuses.wontFix)) {
|
|
7103
|
+
problems.push(`statuses.wontFix ("${c.statuses.wontFix}") is not listed in statuses.resolved`);
|
|
7104
|
+
}
|
|
7105
|
+
if (c.statuses.reviewing && resolved.has(c.statuses.reviewing)) {
|
|
7106
|
+
problems.push(
|
|
7107
|
+
`statuses.reviewing ("${c.statuses.reviewing}") is listed as resolved \u2014 it is unmerged work under review, so a ticket blocked by it is still blocked`
|
|
7108
|
+
);
|
|
7109
|
+
}
|
|
7110
|
+
if (resolved.has(c.statuses.approved)) {
|
|
7111
|
+
problems.push(`statuses.approved ("${c.statuses.approved}") is listed as resolved`);
|
|
7112
|
+
}
|
|
7113
|
+
if (c.statuses.parked === c.statuses.approved) {
|
|
7114
|
+
problems.push("statuses.parked and statuses.approved are the same value \u2014 parking would be a no-op");
|
|
7115
|
+
}
|
|
7116
|
+
const epicValues = Object.values(c.epics.statuses);
|
|
7117
|
+
if (new Set(epicValues).size !== epicValues.length) {
|
|
7118
|
+
problems.push("epics.statuses names the same value twice \u2014 the epic sync could not tell those states apart");
|
|
7119
|
+
}
|
|
7120
|
+
if (c.priorityOrder.length === 0) problems.push("priorityOrder is empty");
|
|
7121
|
+
if (c.severityOrder.length === 0) problems.push("severityOrder is empty");
|
|
7122
|
+
for (const [name, order] of [["priorityOrder", c.priorityOrder], ["severityOrder", c.severityOrder]]) {
|
|
7123
|
+
if (new Set(order).size !== order.length) problems.push(`${name} contains duplicates`);
|
|
7124
|
+
}
|
|
7125
|
+
return problems;
|
|
7126
|
+
}
|
|
7127
|
+
var EVENT_LOG_MISSING = "the Issues table has no event log \u2014 crew writes its audit trail there (CREW-1574). In the app, open the Issues table's Data & lifecycle tab and turn on Event log, then rerun.";
|
|
7128
|
+
|
|
6954
7129
|
// src/work-mode.ts
|
|
6955
7130
|
function projectWorkModes(rows, contract) {
|
|
6956
7131
|
const out = /* @__PURE__ */ new Map();
|
|
@@ -8587,8 +8762,8 @@ function materializeBranchForTicket(dir, cfg, t, role, onProblem) {
|
|
|
8587
8762
|
}
|
|
8588
8763
|
|
|
8589
8764
|
// src/poll.ts
|
|
8590
|
-
import { writeFileSync as
|
|
8591
|
-
import { join as
|
|
8765
|
+
import { writeFileSync as writeFileSync10 } from "fs";
|
|
8766
|
+
import { join as join16 } from "path";
|
|
8592
8767
|
|
|
8593
8768
|
// src/stalled.ts
|
|
8594
8769
|
var STALLED_AFTER_CYCLES = 30;
|
|
@@ -8824,8 +8999,10 @@ async function decideCycle(o) {
|
|
|
8824
8999
|
emit.emit(`${what}: no longer missing from the scheduler PATH \u2014 stop cleared`, { step: "select" });
|
|
8825
9000
|
}
|
|
8826
9001
|
await checkRepoDisks({ state, emit, route, ship });
|
|
9002
|
+
const floorStop = await checkCrewFloor({ state, emit, route, ship });
|
|
9003
|
+
const itemStop = repoStopFor(openShipAttention(state), route);
|
|
8827
9004
|
const selectionInput = {
|
|
8828
|
-
repoStop:
|
|
9005
|
+
repoStop: floorStop ? () => floorStop : itemStop,
|
|
8829
9006
|
workModes,
|
|
8830
9007
|
tickets,
|
|
8831
9008
|
comments,
|
|
@@ -8877,7 +9054,7 @@ async function decideCycle(o) {
|
|
|
8877
9054
|
}
|
|
8878
9055
|
return decision;
|
|
8879
9056
|
}
|
|
8880
|
-
var digestPath = (stateDir, route, role) =>
|
|
9057
|
+
var digestPath = (stateDir, route, role) => join16(stateDir, `digest-${route}-${role}.md`);
|
|
8881
9058
|
function writeDigest(o, d, role, stateDir) {
|
|
8882
9059
|
const me = o.route.resolved?.seats[role];
|
|
8883
9060
|
if (!me) return false;
|
|
@@ -8941,7 +9118,7 @@ function writeDigest(o, d, role, stateDir) {
|
|
|
8941
9118
|
};
|
|
8942
9119
|
const text = role === "qa" ? qaDigest(input) : buildingDigest(input);
|
|
8943
9120
|
const path = digestPath(stateDir, routeSlug(o.route.route), role);
|
|
8944
|
-
|
|
9121
|
+
writeFileSync10(path, text);
|
|
8945
9122
|
o.emit.emit(`queue digest written for ${role} (${Buffer.byteLength(text)} bytes)`, {
|
|
8946
9123
|
data: { role, bytes: Buffer.byteLength(text) }
|
|
8947
9124
|
});
|
|
@@ -9318,13 +9495,13 @@ async function applyStrandedVerified(writer, s, contract, operatorId, memberId,
|
|
|
9318
9495
|
}
|
|
9319
9496
|
|
|
9320
9497
|
// src/run-worktree.ts
|
|
9321
|
-
import { existsSync as
|
|
9322
|
-
import { dirname as
|
|
9498
|
+
import { existsSync as existsSync18 } from "fs";
|
|
9499
|
+
import { dirname as dirname8, join as join18 } from "path";
|
|
9323
9500
|
|
|
9324
9501
|
// src/ports.ts
|
|
9325
9502
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
9326
|
-
import { existsSync as
|
|
9327
|
-
import { join as
|
|
9503
|
+
import { existsSync as existsSync17 } from "fs";
|
|
9504
|
+
import { join as join17 } from "path";
|
|
9328
9505
|
var run = (cmd, args) => {
|
|
9329
9506
|
try {
|
|
9330
9507
|
return execFileSync3(cmd, args, { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
@@ -9369,7 +9546,7 @@ function ticketForPort(port, s = DEFAULT_PORTS) {
|
|
|
9369
9546
|
}
|
|
9370
9547
|
function worktreeExists(parent, prefix, n) {
|
|
9371
9548
|
for (const name of [`${prefix}${n}`, `${prefix}${String(n).padStart(3, "0")}`]) {
|
|
9372
|
-
if (
|
|
9549
|
+
if (existsSync17(join17(parent, name, ".git"))) return true;
|
|
9373
9550
|
}
|
|
9374
9551
|
return false;
|
|
9375
9552
|
}
|
|
@@ -9445,7 +9622,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
9445
9622
|
const existing = worktreeForNumber(dir, num);
|
|
9446
9623
|
const located = locateBranchForTicket(dir, cfg, t, role);
|
|
9447
9624
|
const baseRef = hasRemote ? `${remote}/${base}` : base;
|
|
9448
|
-
if (existing && !existing.prunable &&
|
|
9625
|
+
if (existing && !existing.prunable && existsSync18(existing.path)) {
|
|
9449
9626
|
const branch = existing.branch;
|
|
9450
9627
|
if (status(existing.path).length > 0) {
|
|
9451
9628
|
return { action: opts.resumeInterrupted ? "resume-dirty" : "leave-dirty", path: existing.path, branch };
|
|
@@ -9461,7 +9638,7 @@ function planRunWorktree(dir, cfg, t, role, opts = {}) {
|
|
|
9461
9638
|
if (countCommits(existing.path, `${baseRef}..HEAD`) > 0) return { action: "leave-unpushed", path: existing.path, branch };
|
|
9462
9639
|
return { action: "recut", path: existing.path, branch, base: baseRef };
|
|
9463
9640
|
}
|
|
9464
|
-
const path =
|
|
9641
|
+
const path = join18(dirname8(dir), worktreeForTicket(dir, cfg, t, role));
|
|
9465
9642
|
if (located?.where === "remote" || located && hasRemote && remoteBranchExists(dir, remote, located.branch)) {
|
|
9466
9643
|
return { action: "cut-remote", path, branch: located.branch };
|
|
9467
9644
|
}
|
|
@@ -9510,7 +9687,7 @@ function worktreeHoldsUnpushed(path, remote) {
|
|
|
9510
9687
|
}
|
|
9511
9688
|
async function finishRunWorktree(dir, cfg, branch) {
|
|
9512
9689
|
const wt = worktrees(dir).find((w) => w.branch === branch && !w.prunable && w.path !== dir);
|
|
9513
|
-
if (!wt || !
|
|
9690
|
+
if (!wt || !existsSync18(wt.path)) return { kind: "none" };
|
|
9514
9691
|
if (cfg.hooks.handoff) return { kind: "kept-handoff", path: wt.path };
|
|
9515
9692
|
if (status(wt.path).length > 0) return { kind: "kept-dirty", path: wt.path };
|
|
9516
9693
|
if (remoteConfigured(dir, cfg.branch.remote) && worktreeHoldsUnpushed(wt.path, cfg.branch.remote)) {
|
|
@@ -9522,7 +9699,7 @@ async function finishRunWorktree(dir, cfg, branch) {
|
|
|
9522
9699
|
if (pids.length) await killGently(pids);
|
|
9523
9700
|
}
|
|
9524
9701
|
gitOk(dir, ["worktree", "remove", wt.path]);
|
|
9525
|
-
return
|
|
9702
|
+
return existsSync18(wt.path) ? { kind: "kept-dirty", path: wt.path } : { kind: "removed", path: wt.path };
|
|
9526
9703
|
}
|
|
9527
9704
|
|
|
9528
9705
|
// src/handoff-guard.ts
|
|
@@ -9783,13 +9960,13 @@ async function guardRunStrays(route, ship, tracker, role, workingId, since2, log
|
|
|
9783
9960
|
}
|
|
9784
9961
|
|
|
9785
9962
|
// src/connect-wizard.ts
|
|
9786
|
-
import { delimiter, dirname as
|
|
9787
|
-
import { accessSync as accessSync2, constants as constants2, existsSync as
|
|
9963
|
+
import { delimiter, dirname as dirname9, join as join19 } from "path";
|
|
9964
|
+
import { accessSync as accessSync2, constants as constants2, existsSync as existsSync19, mkdirSync as mkdirSync9, writeFileSync as writeFileSync11 } from "fs";
|
|
9788
9965
|
import { stringify } from "yaml";
|
|
9789
9966
|
var HOST_PASSENGERS_DOCS_URL = "https://crew.tablation.dev/host-passengers";
|
|
9790
9967
|
function shouldRunWizard(opts) {
|
|
9791
9968
|
if (!opts.isTTY) return false;
|
|
9792
|
-
const exists = opts.configExists ??
|
|
9969
|
+
const exists = opts.configExists ?? existsSync19;
|
|
9793
9970
|
return !configSearchPath(opts.crewHome).some((p) => exists(p));
|
|
9794
9971
|
}
|
|
9795
9972
|
function firstRunConfigPath() {
|
|
@@ -9806,7 +9983,7 @@ function defaultIsExecutable(p) {
|
|
|
9806
9983
|
function findOnPath2(name, pathEnv, isExecutable = defaultIsExecutable) {
|
|
9807
9984
|
for (const dir of pathEnv.split(delimiter)) {
|
|
9808
9985
|
if (!dir) continue;
|
|
9809
|
-
const candidate =
|
|
9986
|
+
const candidate = join19(dir, name);
|
|
9810
9987
|
if (isExecutable(candidate)) return candidate;
|
|
9811
9988
|
}
|
|
9812
9989
|
return void 0;
|
|
@@ -9832,9 +10009,9 @@ async function runWizard(p, env) {
|
|
|
9832
10009
|
if (entered && isExec(entered)) agentBin = entered;
|
|
9833
10010
|
else p.say(entered ? `${entered} is not an executable file.` : "A path is required.");
|
|
9834
10011
|
}
|
|
9835
|
-
const nodeDir =
|
|
10012
|
+
const nodeDir = dirname9(env.nodePath);
|
|
9836
10013
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
9837
|
-
const baseDirs = [nodeDir, pnpmPath ?
|
|
10014
|
+
const baseDirs = [nodeDir, pnpmPath ? dirname9(pnpmPath) : void 0];
|
|
9838
10015
|
p.say(
|
|
9839
10016
|
`
|
|
9840
10017
|
Host Passengers lets this machine run the agent sessions that other people's
|
|
@@ -9845,7 +10022,7 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
|
|
|
9845
10022
|
p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
|
|
9846
10023
|
}
|
|
9847
10024
|
const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
9848
|
-
const extraPath = deriveExtraPath([...baseDirs, dockerPath ?
|
|
10025
|
+
const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname9(dockerPath) : void 0]);
|
|
9849
10026
|
if (extraPath) {
|
|
9850
10027
|
p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
|
|
9851
10028
|
}
|
|
@@ -9867,9 +10044,9 @@ routes:
|
|
|
9867
10044
|
${routeBlock}`;
|
|
9868
10045
|
}
|
|
9869
10046
|
function writeNewConfig(path, text) {
|
|
9870
|
-
|
|
10047
|
+
mkdirSync9(dirname9(path), { recursive: true });
|
|
9871
10048
|
try {
|
|
9872
|
-
|
|
10049
|
+
writeFileSync11(path, text, { mode: 384, flag: "wx" });
|
|
9873
10050
|
return true;
|
|
9874
10051
|
} catch (e) {
|
|
9875
10052
|
if (e.code === "EEXIST") return false;
|
|
@@ -9902,7 +10079,7 @@ Wrote ${o.configPath}.
|
|
|
9902
10079
|
|
|
9903
10080
|
// src/connect-json.ts
|
|
9904
10081
|
import { accessSync as accessSync3, constants as constants3 } from "fs";
|
|
9905
|
-
import { dirname as
|
|
10082
|
+
import { dirname as dirname10 } from "path";
|
|
9906
10083
|
var CHOICE_EXIT_CODE = 3;
|
|
9907
10084
|
function createEmitter(json, write) {
|
|
9908
10085
|
return {
|
|
@@ -9950,7 +10127,7 @@ function flagWizardAnswers(f, env) {
|
|
|
9950
10127
|
if (f.agentBin && !isExec(f.agentBin)) return { error: `--agent-bin ${f.agentBin} is not an executable file` };
|
|
9951
10128
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
9952
10129
|
const dockerPath = f.hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
9953
|
-
const extraPath = f.extraPath ?? deriveExtraPath([
|
|
10130
|
+
const extraPath = f.extraPath ?? deriveExtraPath([dirname10(env.nodePath), pnpmPath ? dirname10(pnpmPath) : void 0, dockerPath ? dirname10(dockerPath) : void 0]);
|
|
9954
10131
|
return {
|
|
9955
10132
|
answers: {
|
|
9956
10133
|
name: f.shipName?.trim() || env.hostname,
|
|
@@ -9963,8 +10140,8 @@ function flagWizardAnswers(f, env) {
|
|
|
9963
10140
|
}
|
|
9964
10141
|
|
|
9965
10142
|
// src/release-run.ts
|
|
9966
|
-
import { readFileSync as
|
|
9967
|
-
import { dirname as
|
|
10143
|
+
import { readFileSync as readFileSync15, writeFileSync as writeFileSync12, existsSync as existsSync20, copyFileSync, mkdirSync as mkdirSync10, mkdtempSync as mkdtempSync2, rmSync as rmSync9 } from "fs";
|
|
10144
|
+
import { dirname as dirname11, join as join20 } from "path";
|
|
9968
10145
|
import { tmpdir as tmpdir3 } from "os";
|
|
9969
10146
|
|
|
9970
10147
|
// src/release.ts
|
|
@@ -10652,13 +10829,13 @@ async function bump(o, size) {
|
|
|
10652
10829
|
}
|
|
10653
10830
|
return printed;
|
|
10654
10831
|
}
|
|
10655
|
-
const files = o.repo.release.versionFiles.map((f) =>
|
|
10832
|
+
const files = o.repo.release.versionFiles.map((f) => join20(o.cwd, f));
|
|
10656
10833
|
const first = files[0];
|
|
10657
|
-
if (!
|
|
10834
|
+
if (!existsSync20(first)) {
|
|
10658
10835
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
10659
10836
|
return null;
|
|
10660
10837
|
}
|
|
10661
|
-
const current = JSON.parse(
|
|
10838
|
+
const current = JSON.parse(readFileSync15(first, "utf8")).version;
|
|
10662
10839
|
if (!current) {
|
|
10663
10840
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
10664
10841
|
return null;
|
|
@@ -10671,27 +10848,27 @@ async function bump(o, size) {
|
|
|
10671
10848
|
return version;
|
|
10672
10849
|
}
|
|
10673
10850
|
for (const f of files) {
|
|
10674
|
-
const src =
|
|
10851
|
+
const src = readFileSync15(f, "utf8");
|
|
10675
10852
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
10676
10853
|
if (out === src) {
|
|
10677
10854
|
o.emit.error(`no version field in ${f}`);
|
|
10678
10855
|
return null;
|
|
10679
10856
|
}
|
|
10680
|
-
|
|
10857
|
+
writeFileSync12(f, out);
|
|
10681
10858
|
}
|
|
10682
10859
|
return version;
|
|
10683
10860
|
}
|
|
10684
10861
|
function writeChangelog(o, version, entries) {
|
|
10685
10862
|
const file = o.repo.release.changelog;
|
|
10686
10863
|
if (!file || entries.length === 0) return;
|
|
10687
|
-
const path =
|
|
10864
|
+
const path = join20(o.cwd, file);
|
|
10688
10865
|
const section = renderChangelogSection(version, (/* @__PURE__ */ new Date()).toISOString().slice(0, 10), entries);
|
|
10689
10866
|
if (o.dryRun) {
|
|
10690
10867
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
10691
10868
|
return;
|
|
10692
10869
|
}
|
|
10693
|
-
const existing =
|
|
10694
|
-
|
|
10870
|
+
const existing = existsSync20(path) ? readFileSync15(path, "utf8") : "# Changelog\n";
|
|
10871
|
+
writeFileSync12(path, insertChangelogSection(existing, section));
|
|
10695
10872
|
}
|
|
10696
10873
|
async function confirm(o, expected) {
|
|
10697
10874
|
if (!o.repo.hooks.released) return void 0;
|
|
@@ -10774,7 +10951,7 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
10774
10951
|
run2(cwd, ["worktree", "remove", "--force", dir]);
|
|
10775
10952
|
} finally {
|
|
10776
10953
|
try {
|
|
10777
|
-
|
|
10954
|
+
rmSync9(root, { recursive: true, force: true });
|
|
10778
10955
|
} finally {
|
|
10779
10956
|
run2(cwd, ["worktree", "prune"]);
|
|
10780
10957
|
run2(cwd, ["branch", "-D", branch]);
|
|
@@ -10784,8 +10961,8 @@ function closeReleaseCheckout(cwd, root, dir, branch, run2 = gitOk) {
|
|
|
10784
10961
|
function openReleaseCheckout(o) {
|
|
10785
10962
|
const { remote, base } = o.repo.branch;
|
|
10786
10963
|
const branch = `crew/release-${Date.now()}`;
|
|
10787
|
-
const root = mkdtempSync2(
|
|
10788
|
-
const dir =
|
|
10964
|
+
const root = mkdtempSync2(join20(tmpdir3(), "crew-release-"));
|
|
10965
|
+
const dir = join20(root, "checkout");
|
|
10789
10966
|
o.emit.emit(`release checkout: ${dir}`, { step: "release" });
|
|
10790
10967
|
const close = () => closeReleaseCheckout(o.cwd, root, dir, branch);
|
|
10791
10968
|
try {
|
|
@@ -10795,10 +10972,10 @@ function openReleaseCheckout(o) {
|
|
|
10795
10972
|
throw e;
|
|
10796
10973
|
}
|
|
10797
10974
|
for (const f of o.repo.worktrees.copy) {
|
|
10798
|
-
const src =
|
|
10799
|
-
if (!
|
|
10800
|
-
|
|
10801
|
-
copyFileSync(src,
|
|
10975
|
+
const src = join20(o.cwd, f);
|
|
10976
|
+
if (!existsSync20(src)) continue;
|
|
10977
|
+
mkdirSync10(dirname11(join20(dir, f)), { recursive: true });
|
|
10978
|
+
copyFileSync(src, join20(dir, f));
|
|
10802
10979
|
}
|
|
10803
10980
|
return { dir, branch, close };
|
|
10804
10981
|
}
|
|
@@ -11466,7 +11643,7 @@ function missingReleaserWarning(repo, release, ships, now = Date.now()) {
|
|
|
11466
11643
|
}
|
|
11467
11644
|
|
|
11468
11645
|
// src/watch.ts
|
|
11469
|
-
import { watch as watchFile, existsSync as
|
|
11646
|
+
import { watch as watchFile, existsSync as existsSync21, statSync as statSync7, openSync as openSync4, readSync as readSync2, closeSync as closeSync4 } from "fs";
|
|
11470
11647
|
var E = "\x1B[";
|
|
11471
11648
|
var dim = (s) => `${E}2m${s}${E}0m`;
|
|
11472
11649
|
var bold = (s) => `${E}1m${s}${E}0m`;
|
|
@@ -11567,8 +11744,8 @@ function renderHeaders(states, now = Date.now()) {
|
|
|
11567
11744
|
return states.map((s) => renderHeader(s, now)).join("");
|
|
11568
11745
|
}
|
|
11569
11746
|
function readFrom(file, from) {
|
|
11570
|
-
if (!
|
|
11571
|
-
const size =
|
|
11747
|
+
if (!existsSync21(file)) return { events: [], offset: from };
|
|
11748
|
+
const size = statSync7(file).size;
|
|
11572
11749
|
if (size < from) return readFrom(file, 0);
|
|
11573
11750
|
if (size === from) return { events: [], offset: from };
|
|
11574
11751
|
const fd = openSync4(file, "r");
|
|
@@ -12518,9 +12695,54 @@ async function canMintAgentKeys(route, o = {}) {
|
|
|
12518
12695
|
}
|
|
12519
12696
|
}
|
|
12520
12697
|
|
|
12698
|
+
// src/capability-check.ts
|
|
12699
|
+
var WRITE_LEVELS = /* @__PURE__ */ new Set(["WRITE", "DELETE"]);
|
|
12700
|
+
async function canWriteTable(o, modelId) {
|
|
12701
|
+
try {
|
|
12702
|
+
const { access } = await get(o, `/data-models/${modelId}/access`);
|
|
12703
|
+
return access === void 0 ? void 0 : WRITE_LEVELS.has(access);
|
|
12704
|
+
} catch {
|
|
12705
|
+
return void 0;
|
|
12706
|
+
}
|
|
12707
|
+
}
|
|
12708
|
+
async function projectCapabilities(o, projectId) {
|
|
12709
|
+
if (!projectId) return void 0;
|
|
12710
|
+
try {
|
|
12711
|
+
return await get(o, `/projects/${projectId}/capabilities`);
|
|
12712
|
+
} catch {
|
|
12713
|
+
return void 0;
|
|
12714
|
+
}
|
|
12715
|
+
}
|
|
12716
|
+
async function checkCapabilities(o, found) {
|
|
12717
|
+
const admin = isAdminRole(found.role);
|
|
12718
|
+
const table2 = async (key, label, tableName2, modelId, askFor) => {
|
|
12719
|
+
if (!modelId) return { key, label, status: "skipped", note: `no ${tableName2} table in this project` };
|
|
12720
|
+
const ok = await canWriteTable(o, modelId);
|
|
12721
|
+
if (ok === void 0) return { key, label, status: "skipped", note: "could not read access" };
|
|
12722
|
+
return ok ? { key, label, status: "ok" } : { key, label, status: "missing", askFor };
|
|
12723
|
+
};
|
|
12724
|
+
const [crew, ships, repos, caps] = await Promise.all([
|
|
12725
|
+
table2("crew-table", "write the Crew table", "Crew", found.crewModelId ?? found.models.crew, "WRITE access on the Crew table"),
|
|
12726
|
+
table2("ships-table", "write the Ships table", "Ships", found.shipsModelId, "WRITE access on the Ships table"),
|
|
12727
|
+
table2("repos-table", "create Repos rows", "Repos", found.reposModelId, "WRITE access on the Repos table"),
|
|
12728
|
+
projectCapabilities(o, found.projectId)
|
|
12729
|
+
]);
|
|
12730
|
+
const mint = admin || caps?.MINT_AGENT_KEYS ? { key: "mint-agent-keys", label: "mint agent keys", status: "ok" } : caps ? { key: "mint-agent-keys", label: "mint agent keys", status: "missing", askFor: 'the "Mint agent keys" capability' } : { key: "mint-agent-keys", label: "mint agent keys", status: "skipped", note: "could not read capabilities" };
|
|
12731
|
+
const sync = admin ? { key: "sync-personas", label: "sync personas and skills", status: "ok" } : { key: "sync-personas", label: "sync personas and skills", status: "missing", askFor: "the workspace admin role (personas and skills are workspace-level tables)" };
|
|
12732
|
+
return [crew, ships, repos, mint, sync];
|
|
12733
|
+
}
|
|
12734
|
+
function missLine(i) {
|
|
12735
|
+
return `${i.label} \u2014 ask a workspace admin for ${i.askFor ?? "access"}`;
|
|
12736
|
+
}
|
|
12737
|
+
function describeCapability(i) {
|
|
12738
|
+
if (i.status === "ok") return `ok ${i.label}`;
|
|
12739
|
+
if (i.status === "skipped") return `skipped ${i.label} (${i.note ?? "not checked"})`;
|
|
12740
|
+
return `missing ${missLine(i)}`;
|
|
12741
|
+
}
|
|
12742
|
+
|
|
12521
12743
|
// src/agents-sync-run.ts
|
|
12522
|
-
import { existsSync as
|
|
12523
|
-
import { dirname as
|
|
12744
|
+
import { existsSync as existsSync22, mkdirSync as mkdirSync11, readFileSync as readFileSync16, writeFileSync as writeFileSync13 } from "fs";
|
|
12745
|
+
import { dirname as dirname12 } from "path";
|
|
12524
12746
|
async function runAgentsSync(target, o) {
|
|
12525
12747
|
const dryRun = o.dryRun ?? false;
|
|
12526
12748
|
const force = o.force ?? false;
|
|
@@ -12553,12 +12775,12 @@ async function runAgentsSync(target, o) {
|
|
|
12553
12775
|
for (const s of skillResult?.outcomes ?? []) o.say(describeSkillSyncOutcome(s));
|
|
12554
12776
|
if (!dryRun) {
|
|
12555
12777
|
const resolvedPath = resolvedPathFor(o.stateDir, target.route);
|
|
12556
|
-
const raw =
|
|
12778
|
+
const raw = existsSync22(resolvedPath) ? JSON.parse(readFileSync16(resolvedPath, "utf8")) : {};
|
|
12557
12779
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
12558
12780
|
if (skillResult) raw.agentSkills = skillResult.agentSkills;
|
|
12559
12781
|
if (personaResult.releaseAgentId) raw.releaseAgentId = personaResult.releaseAgentId;
|
|
12560
|
-
|
|
12561
|
-
|
|
12782
|
+
mkdirSync11(dirname12(resolvedPath), { recursive: true });
|
|
12783
|
+
writeFileSync13(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
12562
12784
|
`);
|
|
12563
12785
|
if (target.resolved) {
|
|
12564
12786
|
target.resolved.agentPersonas = personaResult.agentPersonas;
|
|
@@ -12665,7 +12887,7 @@ import { isMap as isMap2, parseDocument as parseDocument2, YAMLMap } from "yaml"
|
|
|
12665
12887
|
|
|
12666
12888
|
// src/repos-cmd.ts
|
|
12667
12889
|
import { homedir as homedir7 } from "os";
|
|
12668
|
-
import { basename as basename7, join as
|
|
12890
|
+
import { basename as basename7, join as join21, resolve as resolve3 } from "path";
|
|
12669
12891
|
import { isMap, isSeq, parseDocument, stringify as stringify2 } from "yaml";
|
|
12670
12892
|
var ReposError = class extends Error {
|
|
12671
12893
|
};
|
|
@@ -12863,7 +13085,7 @@ function planRepoClone(i) {
|
|
|
12863
13085
|
}
|
|
12864
13086
|
const ship = doc.get("ship", true);
|
|
12865
13087
|
const basePath = String(m.get("reposBasePath") ?? (isMap(ship) ? ship.get("reposBasePath") : void 0) ?? "~/Crew");
|
|
12866
|
-
const target = i.dir ? expandDir(i.dir, i.base) :
|
|
13088
|
+
const target = i.dir ? expandDir(i.dir, i.base) : join21(expandDir(basePath, i.base), i.route.split("/")[0], name);
|
|
12867
13089
|
const add = planRepoAdd({ text: i.text, base: i.base, route: i.route, path: target, origin: remote, resolved: i.resolved, name });
|
|
12868
13090
|
return { ...add, remote, target };
|
|
12869
13091
|
}
|
|
@@ -13006,7 +13228,7 @@ function existingRepoConfig(f) {
|
|
|
13006
13228
|
}
|
|
13007
13229
|
|
|
13008
13230
|
// src/sample-ticket.ts
|
|
13009
|
-
import { basename as basename8, dirname as
|
|
13231
|
+
import { basename as basename8, dirname as dirname14, join as join22 } from "path";
|
|
13010
13232
|
var SAMPLE_REPO = "crew-sample";
|
|
13011
13233
|
var SAMPLE_REMOTE = `local/${SAMPLE_REPO}`;
|
|
13012
13234
|
var SAMPLE_MARKER = "sample: true";
|
|
@@ -13029,8 +13251,8 @@ has something safe to work on: its test and build hooks always pass, it has no
|
|
|
13029
13251
|
remote, and \`crew sample-ticket --remove\` deletes it.
|
|
13030
13252
|
`;
|
|
13031
13253
|
function sampleDir(existing, reposBase, workspace) {
|
|
13032
|
-
if (existing.length > 0) return
|
|
13033
|
-
return
|
|
13254
|
+
if (existing.length > 0) return join22(dirname14(existing[0]), SAMPLE_REPO);
|
|
13255
|
+
return join22(reposBase, workspace, SAMPLE_REPO);
|
|
13034
13256
|
}
|
|
13035
13257
|
var SAMPLE_TITLE = "Sample: add a documented `greet` function with a test";
|
|
13036
13258
|
function sampleDescription() {
|
|
@@ -13111,8 +13333,8 @@ function schedulerInstalled(plan, exists, crontab) {
|
|
|
13111
13333
|
}
|
|
13112
13334
|
|
|
13113
13335
|
// src/worktree-sweep.ts
|
|
13114
|
-
import { existsSync as
|
|
13115
|
-
import { basename as basename9, dirname as
|
|
13336
|
+
import { existsSync as existsSync23, readFileSync as readFileSync17, readdirSync as readdirSync6 } from "fs";
|
|
13337
|
+
import { basename as basename9, dirname as dirname15, join as join23, resolve as resolve4 } from "path";
|
|
13116
13338
|
function planWorktreeSweep(target, tickets, contract) {
|
|
13117
13339
|
const terminal = new Set(contract.statuses.resolved.filter((s) => s !== contract.statuses.verified));
|
|
13118
13340
|
const verified = contract.statuses.verified;
|
|
@@ -13166,12 +13388,12 @@ function worktreeTicketNumbers(target) {
|
|
|
13166
13388
|
return [...nums];
|
|
13167
13389
|
}
|
|
13168
13390
|
function siblingWorktreeDirs(target) {
|
|
13169
|
-
const parent =
|
|
13391
|
+
const parent = dirname15(target.dir.replace(/[/\\]+$/, ""));
|
|
13170
13392
|
const stem = `${basename9(target.dir.replace(/[/\\]+$/, ""))}-`;
|
|
13171
13393
|
const registered = new Set(worktrees(target.dir).map((w) => resolve4(w.path)));
|
|
13172
13394
|
let names;
|
|
13173
13395
|
try {
|
|
13174
|
-
names =
|
|
13396
|
+
names = readdirSync6(parent);
|
|
13175
13397
|
} catch {
|
|
13176
13398
|
return [];
|
|
13177
13399
|
}
|
|
@@ -13179,7 +13401,7 @@ function siblingWorktreeDirs(target) {
|
|
|
13179
13401
|
for (const name of names) {
|
|
13180
13402
|
if (!name.startsWith(stem)) continue;
|
|
13181
13403
|
const m = DIR_NUMBER_SUFFIXED.exec(name);
|
|
13182
|
-
const path =
|
|
13404
|
+
const path = join23(parent, name);
|
|
13183
13405
|
if (m && !registered.has(resolve4(path))) out.push({ path, number: m[1] });
|
|
13184
13406
|
}
|
|
13185
13407
|
return out;
|
|
@@ -13214,7 +13436,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
13214
13436
|
let removed = 0;
|
|
13215
13437
|
let keptBranches = 0;
|
|
13216
13438
|
for (const a of actions) {
|
|
13217
|
-
if (!dryRun && base && !a.keepBranch &&
|
|
13439
|
+
if (!dryRun && base && !a.keepBranch && existsSync23(a.path) && isHandBackMerge(a.path, base)) {
|
|
13218
13440
|
if (gitOk(a.path, ["merge", "--abort"]) !== null) {
|
|
13219
13441
|
log.emit(
|
|
13220
13442
|
`abandoned the hand-back merge of ${base} in ${a.path}: ${a.ticket.issue_id} deployed without it`,
|
|
@@ -13222,7 +13444,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
13222
13444
|
);
|
|
13223
13445
|
}
|
|
13224
13446
|
}
|
|
13225
|
-
if (!dryRun &&
|
|
13447
|
+
if (!dryRun && existsSync23(a.path)) {
|
|
13226
13448
|
let dirty = false;
|
|
13227
13449
|
try {
|
|
13228
13450
|
dirty = status(a.path).length > 0;
|
|
@@ -13233,7 +13455,7 @@ async function applyWorktreeSweep(cwd, actions, dryRun, log, remote = "origin",
|
|
|
13233
13455
|
continue;
|
|
13234
13456
|
}
|
|
13235
13457
|
}
|
|
13236
|
-
if (!dryRun && a.requirePushed && a.branch &&
|
|
13458
|
+
if (!dryRun && a.requirePushed && a.branch && existsSync23(a.path) && remoteConfigured(cwd, remote) && worktreeHoldsUnpushed(a.path, remote)) {
|
|
13237
13459
|
refuse(a, "unpushed commits", `${a.branch} has commits that are not on ${remote}`);
|
|
13238
13460
|
continue;
|
|
13239
13461
|
}
|
|
@@ -13282,7 +13504,7 @@ function isHandBackMerge(path, base) {
|
|
|
13282
13504
|
if (!msgPath) return false;
|
|
13283
13505
|
let first;
|
|
13284
13506
|
try {
|
|
13285
|
-
first =
|
|
13507
|
+
first = readFileSync17(resolve4(path, msgPath), "utf8").split("\n")[0] ?? "";
|
|
13286
13508
|
} catch {
|
|
13287
13509
|
return false;
|
|
13288
13510
|
}
|
|
@@ -13387,16 +13609,16 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
13387
13609
|
}
|
|
13388
13610
|
|
|
13389
13611
|
// src/stream-sweep.ts
|
|
13390
|
-
import { existsSync as
|
|
13391
|
-
import { join as
|
|
13612
|
+
import { existsSync as existsSync24, readdirSync as readdirSync7, rmSync as rmSync10, statSync as statSync8 } from "fs";
|
|
13613
|
+
import { join as join24 } from "path";
|
|
13392
13614
|
var DAY_MS2 = 24 * 60 * 60 * 1e3;
|
|
13393
13615
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
13394
|
-
const dir =
|
|
13395
|
-
if (!
|
|
13616
|
+
const dir = join24(stateDir, "streams");
|
|
13617
|
+
if (!existsSync24(dir)) return [];
|
|
13396
13618
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
13397
|
-
return
|
|
13619
|
+
return readdirSync7(dir).map((name) => join24(dir, name)).filter((path) => {
|
|
13398
13620
|
try {
|
|
13399
|
-
return
|
|
13621
|
+
return statSync8(path).mtimeMs < cutoff;
|
|
13400
13622
|
} catch {
|
|
13401
13623
|
return false;
|
|
13402
13624
|
}
|
|
@@ -13410,7 +13632,7 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
13410
13632
|
continue;
|
|
13411
13633
|
}
|
|
13412
13634
|
try {
|
|
13413
|
-
|
|
13635
|
+
rmSync10(path, { force: true });
|
|
13414
13636
|
removed++;
|
|
13415
13637
|
log.emit(`removed stream artifact ${path}`, { step: "worktree" });
|
|
13416
13638
|
} catch (e) {
|
|
@@ -13420,11 +13642,11 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
13420
13642
|
return removed;
|
|
13421
13643
|
}
|
|
13422
13644
|
function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
13423
|
-
if (!
|
|
13645
|
+
if (!existsSync24(stateDir)) return [];
|
|
13424
13646
|
const cutoff = now - retentionDays * DAY_MS2;
|
|
13425
|
-
return
|
|
13647
|
+
return readdirSync7(stateDir).filter((name) => ROTATED_EVENTS_RE.test(name)).map((name) => join24(stateDir, name)).filter((path) => {
|
|
13426
13648
|
try {
|
|
13427
|
-
return
|
|
13649
|
+
return statSync8(path).mtimeMs < cutoff;
|
|
13428
13650
|
} catch {
|
|
13429
13651
|
return false;
|
|
13430
13652
|
}
|
|
@@ -13432,24 +13654,24 @@ function planEventsSweep(stateDir, retentionDays, now = Date.now()) {
|
|
|
13432
13654
|
}
|
|
13433
13655
|
|
|
13434
13656
|
// src/cli.ts
|
|
13435
|
-
import { readFileSync as
|
|
13657
|
+
import { realpathSync as realpathSync2, readFileSync as readFileSync23, existsSync as existsSync30, mkdirSync as mkdirSync16, writeFileSync as writeFileSync17, rmSync as rmSync12, unlinkSync as unlinkSync5, copyFileSync as copyFileSync2, renameSync as renameSync2, statSync as statSync10, readdirSync as readdirSync12 } from "fs";
|
|
13436
13658
|
import { createInterface } from "readline/promises";
|
|
13437
13659
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
13438
13660
|
|
|
13439
13661
|
// src/ssh-keys.ts
|
|
13440
13662
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
13441
|
-
import { existsSync as
|
|
13442
|
-
import { dirname as
|
|
13663
|
+
import { existsSync as existsSync25, mkdirSync as mkdirSync12, readFileSync as readFileSync18, chmodSync } from "fs";
|
|
13664
|
+
import { dirname as dirname16, join as join25 } from "path";
|
|
13443
13665
|
var SshKeyError = class extends Error {
|
|
13444
13666
|
};
|
|
13445
13667
|
function shipSshKeyPathFor(stateDir) {
|
|
13446
|
-
return
|
|
13668
|
+
return join25(stateDir, "ssh", "id_ed25519");
|
|
13447
13669
|
}
|
|
13448
13670
|
function ensureShipSshKeypair(stateDir) {
|
|
13449
13671
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
13450
13672
|
const publicKeyPath = `${privateKeyPath}.pub`;
|
|
13451
|
-
if (!
|
|
13452
|
-
|
|
13673
|
+
if (!existsSync25(privateKeyPath)) {
|
|
13674
|
+
mkdirSync12(dirname16(privateKeyPath), { recursive: true });
|
|
13453
13675
|
try {
|
|
13454
13676
|
execFileSync4("ssh-keygen", [
|
|
13455
13677
|
"-t",
|
|
@@ -13469,10 +13691,10 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
13469
13691
|
}
|
|
13470
13692
|
chmodSync(privateKeyPath, 384);
|
|
13471
13693
|
}
|
|
13472
|
-
if (!
|
|
13694
|
+
if (!existsSync25(publicKeyPath)) {
|
|
13473
13695
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
13474
13696
|
}
|
|
13475
|
-
const publicKey =
|
|
13697
|
+
const publicKey = readFileSync18(publicKeyPath, "utf8").trim();
|
|
13476
13698
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
13477
13699
|
}
|
|
13478
13700
|
function sshKeygenAvailable() {
|
|
@@ -13688,9 +13910,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
13688
13910
|
|
|
13689
13911
|
// src/tunnel.ts
|
|
13690
13912
|
import { spawn as spawn4 } from "child_process";
|
|
13691
|
-
import { existsSync as
|
|
13913
|
+
import { existsSync as existsSync26, mkdirSync as mkdirSync13, readFileSync as readFileSync19, writeFileSync as writeFileSync14, unlinkSync as unlinkSync3, readdirSync as readdirSync8 } from "fs";
|
|
13692
13914
|
import { randomInt } from "crypto";
|
|
13693
|
-
import { join as
|
|
13915
|
+
import { join as join26 } from "path";
|
|
13694
13916
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
13695
13917
|
var defaultSpawn = (cmd, args) => spawn4(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
13696
13918
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -13757,21 +13979,21 @@ function startTunnel(opts) {
|
|
|
13757
13979
|
};
|
|
13758
13980
|
}
|
|
13759
13981
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
13760
|
-
return
|
|
13982
|
+
return join26(stateDir, "passengers", `${workspaceId}.json`);
|
|
13761
13983
|
}
|
|
13762
13984
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
13763
13985
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
13764
|
-
if (!
|
|
13986
|
+
if (!existsSync26(path)) return void 0;
|
|
13765
13987
|
try {
|
|
13766
|
-
return JSON.parse(
|
|
13988
|
+
return JSON.parse(readFileSync19(path, "utf8"));
|
|
13767
13989
|
} catch {
|
|
13768
13990
|
return void 0;
|
|
13769
13991
|
}
|
|
13770
13992
|
}
|
|
13771
13993
|
function writePersistedTunnel(stateDir, state) {
|
|
13772
13994
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
13773
|
-
|
|
13774
|
-
|
|
13995
|
+
mkdirSync13(join26(stateDir, "passengers"), { recursive: true });
|
|
13996
|
+
writeFileSync14(path, `${JSON.stringify(state, null, 2)}
|
|
13775
13997
|
`);
|
|
13776
13998
|
}
|
|
13777
13999
|
function updatePersistedTunnel(stateDir, workspaceId, patch2) {
|
|
@@ -13868,9 +14090,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
13868
14090
|
}
|
|
13869
14091
|
}
|
|
13870
14092
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
13871
|
-
const dir =
|
|
13872
|
-
if (!
|
|
13873
|
-
return
|
|
14093
|
+
const dir = join26(stateDir, "passengers");
|
|
14094
|
+
if (!existsSync26(dir)) return [];
|
|
14095
|
+
return readdirSync8(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
13874
14096
|
}
|
|
13875
14097
|
|
|
13876
14098
|
// src/mcp-endpoint-registry.ts
|
|
@@ -13990,8 +14212,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
13990
14212
|
import { createServer as createHttpServer } from "http";
|
|
13991
14213
|
import { spawn as spawn5 } from "child_process";
|
|
13992
14214
|
import { randomBytes } from "crypto";
|
|
13993
|
-
import { existsSync as
|
|
13994
|
-
import { join as
|
|
14215
|
+
import { existsSync as existsSync27, mkdirSync as mkdirSync14, readFileSync as readFileSync20, writeFileSync as writeFileSync15, unlinkSync as unlinkSync4, readdirSync as readdirSync9 } from "fs";
|
|
14216
|
+
import { join as join27 } from "path";
|
|
13995
14217
|
|
|
13996
14218
|
// src/secret-compare.ts
|
|
13997
14219
|
import { timingSafeEqual } from "crypto";
|
|
@@ -14080,21 +14302,21 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
14080
14302
|
return child;
|
|
14081
14303
|
};
|
|
14082
14304
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
14083
|
-
return
|
|
14305
|
+
return join27(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
14084
14306
|
}
|
|
14085
14307
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
14086
14308
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
14087
|
-
if (!
|
|
14309
|
+
if (!existsSync27(path)) return void 0;
|
|
14088
14310
|
try {
|
|
14089
|
-
return JSON.parse(
|
|
14311
|
+
return JSON.parse(readFileSync20(path, "utf8"));
|
|
14090
14312
|
} catch {
|
|
14091
14313
|
return void 0;
|
|
14092
14314
|
}
|
|
14093
14315
|
}
|
|
14094
14316
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
14095
14317
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
14096
|
-
|
|
14097
|
-
|
|
14318
|
+
mkdirSync14(join27(stateDir, "passengers"), { recursive: true });
|
|
14319
|
+
writeFileSync15(path, `${JSON.stringify(state, null, 2)}
|
|
14098
14320
|
`);
|
|
14099
14321
|
}
|
|
14100
14322
|
function removePersistedSyncDaemon(stateDir, workspaceId) {
|
|
@@ -14114,10 +14336,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
14114
14336
|
}
|
|
14115
14337
|
}
|
|
14116
14338
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
14117
|
-
const dir =
|
|
14118
|
-
if (!
|
|
14339
|
+
const dir = join27(stateDir, "passengers");
|
|
14340
|
+
if (!existsSync27(dir)) return [];
|
|
14119
14341
|
const suffix = "-sync.json";
|
|
14120
|
-
return
|
|
14342
|
+
return readdirSync9(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
14121
14343
|
}
|
|
14122
14344
|
function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
14123
14345
|
const alive = deps.isPidAlive ?? isPidAlive;
|
|
@@ -14172,12 +14394,12 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
14172
14394
|
}
|
|
14173
14395
|
|
|
14174
14396
|
// src/daemon.ts
|
|
14175
|
-
import { existsSync as
|
|
14176
|
-
import { join as
|
|
14397
|
+
import { existsSync as existsSync29, mkdirSync as mkdirSync15, readFileSync as readFileSync22, readdirSync as readdirSync11, rmSync as rmSync11, statSync as statSync9, writeFileSync as writeFileSync16 } from "fs";
|
|
14398
|
+
import { join as join29 } from "path";
|
|
14177
14399
|
|
|
14178
14400
|
// src/interrupted.ts
|
|
14179
|
-
import { existsSync as
|
|
14180
|
-
import { join as
|
|
14401
|
+
import { existsSync as existsSync28, readdirSync as readdirSync10, readFileSync as readFileSync21 } from "fs";
|
|
14402
|
+
import { join as join28 } from "path";
|
|
14181
14403
|
var INTERRUPTED_MARKER = calloutHeader("NOTE", "Interrupted");
|
|
14182
14404
|
function interruptedComment(b) {
|
|
14183
14405
|
const tree = b.dirty === null ? "The worktree could not be checked." : b.dirty ? "The worktree has uncommitted changes \u2014 they are this seat's own, from that run." : "The worktree is clean.";
|
|
@@ -14196,21 +14418,21 @@ var pidAlive = (pid) => {
|
|
|
14196
14418
|
};
|
|
14197
14419
|
function runFinished(streamPath) {
|
|
14198
14420
|
const path = `${streamPath}.meta.json`;
|
|
14199
|
-
if (!
|
|
14421
|
+
if (!existsSync28(path)) return false;
|
|
14200
14422
|
try {
|
|
14201
|
-
return JSON.parse(
|
|
14423
|
+
return JSON.parse(readFileSync21(path, "utf8")).running !== true;
|
|
14202
14424
|
} catch {
|
|
14203
14425
|
return true;
|
|
14204
14426
|
}
|
|
14205
14427
|
}
|
|
14206
14428
|
function orphanedStreams(o) {
|
|
14207
|
-
const dir =
|
|
14208
|
-
if (!
|
|
14429
|
+
const dir = join28(o.stateDir, "streams");
|
|
14430
|
+
if (!existsSync28(dir)) return [];
|
|
14209
14431
|
const ownerAlive = o.isOwnerAlive ?? pidAlive;
|
|
14210
14432
|
const out = [];
|
|
14211
|
-
for (const name of
|
|
14433
|
+
for (const name of readdirSync10(dir)) {
|
|
14212
14434
|
if (!name.endsWith(".jsonl") || name.endsWith(".events.jsonl")) continue;
|
|
14213
|
-
const streamPath =
|
|
14435
|
+
const streamPath = join28(dir, name);
|
|
14214
14436
|
if (runFinished(streamPath)) continue;
|
|
14215
14437
|
const sidecar = readRunSidecar(streamPath);
|
|
14216
14438
|
if (!sidecar || sidecar.route !== o.routeLabel) continue;
|
|
@@ -14302,15 +14524,15 @@ async function runDaemonLoop(o) {
|
|
|
14302
14524
|
function latestMtime(path) {
|
|
14303
14525
|
let stat;
|
|
14304
14526
|
try {
|
|
14305
|
-
stat =
|
|
14527
|
+
stat = statSync9(path);
|
|
14306
14528
|
} catch {
|
|
14307
14529
|
return 0;
|
|
14308
14530
|
}
|
|
14309
14531
|
if (stat.isFile()) return stat.mtimeMs;
|
|
14310
14532
|
if (!stat.isDirectory()) return 0;
|
|
14311
14533
|
let latest = stat.mtimeMs;
|
|
14312
|
-
for (const entry of
|
|
14313
|
-
latest = Math.max(latest, latestMtime(
|
|
14534
|
+
for (const entry of readdirSync11(path, { withFileTypes: true })) {
|
|
14535
|
+
latest = Math.max(latest, latestMtime(join29(path, entry.name)));
|
|
14314
14536
|
}
|
|
14315
14537
|
return latest;
|
|
14316
14538
|
}
|
|
@@ -14319,23 +14541,23 @@ function computeTreeSignature(paths) {
|
|
|
14319
14541
|
}
|
|
14320
14542
|
function makeStaleChecker(paths, opts = {}) {
|
|
14321
14543
|
const initial = computeTreeSignature(paths);
|
|
14322
|
-
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 &&
|
|
14544
|
+
return () => computeTreeSignature(paths) !== initial || opts.reloadSentinel !== void 0 && existsSync29(opts.reloadSentinel);
|
|
14323
14545
|
}
|
|
14324
14546
|
function reloadSentinelFor(stateDir) {
|
|
14325
|
-
return
|
|
14547
|
+
return join29(stateDir, "reload-requested");
|
|
14326
14548
|
}
|
|
14327
14549
|
function daemonStampFor(stateDir) {
|
|
14328
|
-
return
|
|
14550
|
+
return join29(stateDir, "daemon.started");
|
|
14329
14551
|
}
|
|
14330
14552
|
function noteDaemonStarted(stateDir, now = Date.now()) {
|
|
14331
|
-
|
|
14332
|
-
|
|
14333
|
-
|
|
14553
|
+
mkdirSync15(stateDir, { recursive: true });
|
|
14554
|
+
rmSync11(reloadSentinelFor(stateDir), { force: true });
|
|
14555
|
+
writeFileSync16(daemonStampFor(stateDir), `${JSON.stringify({ pid: process.pid, startedAt: now })}
|
|
14334
14556
|
`);
|
|
14335
14557
|
}
|
|
14336
14558
|
function readDaemonStarted(stateDir) {
|
|
14337
14559
|
try {
|
|
14338
|
-
const v = JSON.parse(
|
|
14560
|
+
const v = JSON.parse(readFileSync22(daemonStampFor(stateDir), "utf8")).startedAt;
|
|
14339
14561
|
return typeof v === "number" ? v : null;
|
|
14340
14562
|
} catch {
|
|
14341
14563
|
return null;
|
|
@@ -14344,7 +14566,7 @@ function readDaemonStarted(stateDir) {
|
|
|
14344
14566
|
function configChangedAfter(files, startedAt) {
|
|
14345
14567
|
for (const f of files) {
|
|
14346
14568
|
try {
|
|
14347
|
-
if (
|
|
14569
|
+
if (statSync9(f).mtimeMs > startedAt) return f;
|
|
14348
14570
|
} catch {
|
|
14349
14571
|
}
|
|
14350
14572
|
}
|
|
@@ -14381,6 +14603,16 @@ var SpawnBreaker = class {
|
|
|
14381
14603
|
}
|
|
14382
14604
|
};
|
|
14383
14605
|
async function runOnePass(o) {
|
|
14606
|
+
try {
|
|
14607
|
+
return await runOnePassUnguarded(o);
|
|
14608
|
+
} catch (e) {
|
|
14609
|
+
if (!isUpgradeRequired(e)) throw e;
|
|
14610
|
+
const emit = o.newEmitter();
|
|
14611
|
+
await raiseFromUpgradeRequired({ state: o.state, emit, route: o.route, ship: o.ship }, e);
|
|
14612
|
+
return { pending: [], started: [], skipped: [] };
|
|
14613
|
+
}
|
|
14614
|
+
}
|
|
14615
|
+
async function runOnePassUnguarded(o) {
|
|
14384
14616
|
if (o.state.isPaused()) return { pending: [], started: [], skipped: [] };
|
|
14385
14617
|
await hydrateApiKeys([o.route], o.getSessionStore);
|
|
14386
14618
|
const emit = o.newEmitter();
|
|
@@ -14691,7 +14923,7 @@ async function buildEnvironment(route, ship, ticket) {
|
|
|
14691
14923
|
}
|
|
14692
14924
|
|
|
14693
14925
|
// src/cli.ts
|
|
14694
|
-
var CREW_HOME = isCompiledBinary(import.meta.url) ?
|
|
14926
|
+
var CREW_HOME = isCompiledBinary(import.meta.url) ? dirname17(process.execPath) : resolve5(dirname17(fileURLToPath(import.meta.url)), "..");
|
|
14695
14927
|
var RELEASE_LOCK_TTL_MS = 20 * 60 * 1e3;
|
|
14696
14928
|
var CYCLE_SECONDS2 = 120;
|
|
14697
14929
|
var STALE_CYCLES = 3;
|
|
@@ -14839,6 +15071,7 @@ function usage() {
|
|
|
14839
15071
|
crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
|
|
14840
15072
|
crew connect resolve a workspace's ids into a crew.yaml block, then (admin key) sync personas/skills and mint seat keys (--no-agents-sync skips that; --no-mcp skips registering the Tablation MCP server with Claude Code; --mcp replace overwrites a different one)
|
|
14841
15073
|
crew disconnect remove what connect installed for Pair (Claude MCP; Codex/Gemini instructions block and MCP entry per ship.pair)
|
|
15074
|
+
crew disconnect --reset [--remove-checkouts] [--dry-run] [--json] remove everything connect and install wrote on this machine: scheduler units, Pair/MCP entries, seat keys, keychain credential, resolved state, then crew.yaml (--remove-checkouts also deletes checkouts crew cloned, never one with local changes)
|
|
14842
15075
|
crew agents sync [route] [--force] [--revoke-seat-keys] push crew's personas AND skill files into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona or skill; never one marked Customize prompt)
|
|
14843
15076
|
crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook; --harness H adds which CLI it serves)
|
|
14844
15077
|
crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
|
|
@@ -15017,7 +15250,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15017
15250
|
firstRun = { answers, configPath };
|
|
15018
15251
|
cfg = loadConfig(CREW_HOME, configPath, { text: renderShipBlock(answers), allowNoRoutes: true });
|
|
15019
15252
|
} else {
|
|
15020
|
-
cfg = loadConfig(CREW_HOME, void 0, command === "repos" ? { allowNoRoutes: true } : {});
|
|
15253
|
+
cfg = loadConfig(CREW_HOME, void 0, command === "repos" || command === "disconnect" && flag("reset") ? { allowNoRoutes: true } : {});
|
|
15021
15254
|
}
|
|
15022
15255
|
} catch (e) {
|
|
15023
15256
|
if (e instanceof ConfigError) {
|
|
@@ -15048,6 +15281,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15048
15281
|
throw e;
|
|
15049
15282
|
}
|
|
15050
15283
|
const state = new State(cfg.ship.stateDir);
|
|
15284
|
+
setCloneRecorder((dir) => recordClonedCheckout(cfg.ship.stateDir, dir));
|
|
15051
15285
|
const dryRun = flag("dry-run");
|
|
15052
15286
|
const force = flag("force");
|
|
15053
15287
|
const EXCLUSIVE = { passengers: "passengers" };
|
|
@@ -15250,8 +15484,8 @@ if (command === "passenger-sync-daemon") {
|
|
|
15250
15484
|
}
|
|
15251
15485
|
if (host === "linux" && hasSystemd) {
|
|
15252
15486
|
const label = labelFor(CREW_HOME, "run");
|
|
15253
|
-
const staleTimer =
|
|
15254
|
-
if (
|
|
15487
|
+
const staleTimer = join30(homedir8(), ".config", "systemd", "user", `${label}.timer`);
|
|
15488
|
+
if (existsSync30(staleTimer)) {
|
|
15255
15489
|
process.stdout.write(
|
|
15256
15490
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
15257
15491
|
`
|
|
@@ -15277,7 +15511,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15277
15511
|
const tracker = new Tracker(c, cfg.ship);
|
|
15278
15512
|
problems.push(...validateContract(tracker.contract).map((p) => `contract: ${p}`));
|
|
15279
15513
|
for (const r of await resolvedRepos(c)) {
|
|
15280
|
-
if (!
|
|
15514
|
+
if (!existsSync30(r.dir)) problems.push(`repo ${r.name}: ${r.dir} is missing on disk`);
|
|
15281
15515
|
if (!satisfies(host, r.config.platform)) problems.push(`repo ${r.name}: requires ${r.config.platform}, this host is ${host}`);
|
|
15282
15516
|
problems.push(...validateEffective(r.config).map((p) => `repo ${r.name}: ${p}`));
|
|
15283
15517
|
}
|
|
@@ -15295,7 +15529,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15295
15529
|
}
|
|
15296
15530
|
async function doctorFix(o) {
|
|
15297
15531
|
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))));
|
|
15298
|
-
const unitExists = (p) =>
|
|
15532
|
+
const unitExists = (p) => existsSync30(p);
|
|
15299
15533
|
const crontab = o.host === "linux" ? spawnSync6("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
15300
15534
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
15301
15535
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
@@ -15312,7 +15546,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
15312
15546
|
for (const a of plan.actions) {
|
|
15313
15547
|
if (a.kind === "enable") {
|
|
15314
15548
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
15315
|
-
|
|
15549
|
+
writeFileSync17(cfg.configFile, planEnable(readFileSync23(cfg.configFile, "utf8"), a.route));
|
|
15316
15550
|
enabledSound.add(a.route);
|
|
15317
15551
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
15318
15552
|
} else {
|
|
@@ -16311,7 +16545,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16311
16545
|
process.exit(1);
|
|
16312
16546
|
}
|
|
16313
16547
|
requireArmed("run the daemon loop");
|
|
16314
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
16548
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join30(CREW_HOME, "src"), join30(CREW_HOME, "dist"), join30(CREW_HOME, "bin")];
|
|
16315
16549
|
const configPaths = daemonConfigPaths(cfg);
|
|
16316
16550
|
watchPaths.push(...configPaths);
|
|
16317
16551
|
noteDaemonStarted(cfg.ship.stateDir);
|
|
@@ -16512,10 +16746,10 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16512
16746
|
`);
|
|
16513
16747
|
if (!dryRun) {
|
|
16514
16748
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
16515
|
-
const raw =
|
|
16749
|
+
const raw = existsSync30(resolvedPath) ? JSON.parse(readFileSync23(resolvedPath, "utf8")) : {};
|
|
16516
16750
|
raw.agentSkills = result.agentSkills;
|
|
16517
|
-
|
|
16518
|
-
|
|
16751
|
+
mkdirSync16(dirOf(resolvedPath), { recursive: true });
|
|
16752
|
+
writeFileSync17(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
16519
16753
|
`);
|
|
16520
16754
|
}
|
|
16521
16755
|
if (result.outcomes.some((o) => o.action === "diverged")) process.exitCode = 1;
|
|
@@ -16528,15 +16762,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16528
16762
|
process.exit(2);
|
|
16529
16763
|
};
|
|
16530
16764
|
if (sub !== "add" && sub !== "list" && sub !== "clone" && sub !== "hooks") usage2();
|
|
16531
|
-
const configText =
|
|
16765
|
+
const configText = readFileSync23(cfg.configFile, "utf8");
|
|
16532
16766
|
if (sub === "hooks") {
|
|
16533
16767
|
try {
|
|
16534
16768
|
const a = parseHooksArgs(positional.slice(2), routeNamesOf(configText));
|
|
16535
|
-
const dir = repoDirOf(configText,
|
|
16536
|
-
if (!
|
|
16537
|
-
const files = { exists: (r) =>
|
|
16769
|
+
const dir = repoDirOf(configText, dirname17(cfg.configFile), a.route, a.name);
|
|
16770
|
+
if (!existsSync30(dir)) throw new ReposError(`${dir} does not exist on disk`);
|
|
16771
|
+
const files = { exists: (r) => existsSync30(resolvePath(dir, r)), read: (r) => {
|
|
16538
16772
|
try {
|
|
16539
|
-
return
|
|
16773
|
+
return readFileSync23(resolvePath(dir, r), "utf8");
|
|
16540
16774
|
} catch {
|
|
16541
16775
|
return void 0;
|
|
16542
16776
|
}
|
|
@@ -16548,13 +16782,13 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16548
16782
|
if (write && suggestion.source === null) throw new ReposError(`nothing to write: ${suggestion.notes[0]}`);
|
|
16549
16783
|
if (write === "route") {
|
|
16550
16784
|
const plan = planRouteHooks(configText, a.route, a.name, suggestion.hooks);
|
|
16551
|
-
|
|
16785
|
+
writeFileSync17(cfg.configFile, plan.text);
|
|
16552
16786
|
written = { target: "route", file: cfg.configFile, hooks: plan.written, kept: plan.kept };
|
|
16553
16787
|
} else if (write === "repo") {
|
|
16554
16788
|
const have = existingRepoConfig(files);
|
|
16555
16789
|
if (have) throw new ReposError(`${resolvePath(dir, have)} already exists; not overwriting it`);
|
|
16556
16790
|
const file = resolvePath(dir, ".crew.yaml");
|
|
16557
|
-
|
|
16791
|
+
writeFileSync17(file, starterRepoConfig(suggestion.hooks), { flag: "wx" });
|
|
16558
16792
|
written = { target: "repo", file, hooks: Object.keys(suggestion.hooks), kept: [] };
|
|
16559
16793
|
}
|
|
16560
16794
|
if (flag("json")) {
|
|
@@ -16591,7 +16825,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16591
16825
|
if (sub === "list" && !routeName) usage2();
|
|
16592
16826
|
if (sub === "add") {
|
|
16593
16827
|
try {
|
|
16594
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) =>
|
|
16828
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync30(resolvePath(p.replace(/^~(?=\/|$)/, homedir8()))));
|
|
16595
16829
|
routeName = a.route;
|
|
16596
16830
|
addPath = a.path;
|
|
16597
16831
|
addName = a.name;
|
|
@@ -16620,15 +16854,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16620
16854
|
}
|
|
16621
16855
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, routeName);
|
|
16622
16856
|
let resolvedRemotes;
|
|
16623
|
-
if (
|
|
16857
|
+
if (existsSync30(resolvedFile)) {
|
|
16624
16858
|
try {
|
|
16625
|
-
resolvedRemotes = JSON.parse(
|
|
16859
|
+
resolvedRemotes = JSON.parse(readFileSync23(resolvedFile, "utf8"));
|
|
16626
16860
|
} catch {
|
|
16627
16861
|
}
|
|
16628
16862
|
}
|
|
16629
16863
|
const originOf = (dir) => gitOk(dir, ["remote", "get-url", "origin"]) ?? void 0;
|
|
16630
16864
|
const text = configText;
|
|
16631
|
-
const base =
|
|
16865
|
+
const base = dirname17(cfg.configFile);
|
|
16632
16866
|
try {
|
|
16633
16867
|
if (sub === "list") {
|
|
16634
16868
|
const repos = configuredRepos(text, routeName, base);
|
|
@@ -16637,7 +16871,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16637
16871
|
for (const r of repos) {
|
|
16638
16872
|
const origin2 = originOf(r.dir);
|
|
16639
16873
|
const row = matchRepoName(origin2, resolvedRemotes);
|
|
16640
|
-
const note = !
|
|
16874
|
+
const note = !existsSync30(r.dir) ? "missing on disk" : row ? `Repos row: ${row}` : `no Repos row matches ${normalizeRemote(origin2) ?? "this checkout (no origin remote)"}`;
|
|
16641
16875
|
process.stdout.write(`${r.name} ${r.dir} ${note}
|
|
16642
16876
|
`);
|
|
16643
16877
|
}
|
|
@@ -16649,9 +16883,9 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16649
16883
|
` : `${human}
|
|
16650
16884
|
`);
|
|
16651
16885
|
const plan2 = planRepoClone({ text, base, route: routeName, name: addName, resolved: resolvedRemotes, dir: value("dir") });
|
|
16652
|
-
if (
|
|
16886
|
+
if (existsSync30(plan2.target) && (existsSync30(`${plan2.target}/.git`) || readdirSync12(plan2.target).length > 0)) {
|
|
16653
16887
|
throw new ReposError(
|
|
16654
|
-
|
|
16888
|
+
existsSync30(`${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`
|
|
16655
16889
|
);
|
|
16656
16890
|
}
|
|
16657
16891
|
if (dryRun) {
|
|
@@ -16669,12 +16903,12 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16669
16903
|
throw new ReposError(`${e.message}${authHint}
|
|
16670
16904
|
crew.yaml was not changed`);
|
|
16671
16905
|
}
|
|
16672
|
-
|
|
16906
|
+
writeFileSync17(cfg.configFile, plan2.text);
|
|
16673
16907
|
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}`);
|
|
16674
16908
|
break;
|
|
16675
16909
|
}
|
|
16676
16910
|
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir8()));
|
|
16677
|
-
const top =
|
|
16911
|
+
const top = existsSync30(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
16678
16912
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
16679
16913
|
const origin = originOf(top);
|
|
16680
16914
|
const plan = planRepoAdd({
|
|
@@ -16723,15 +16957,15 @@ ${changed2.map((c) => ` - ${c}
|
|
|
16723
16957
|
const created = await rowTracker.createRepoRow(rowPayload);
|
|
16724
16958
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
16725
16959
|
`);
|
|
16726
|
-
if (
|
|
16727
|
-
const raw = JSON.parse(
|
|
16960
|
+
if (existsSync30(resolvedFile)) {
|
|
16961
|
+
const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
|
|
16728
16962
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
16729
16963
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
16730
|
-
|
|
16964
|
+
writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
16731
16965
|
`);
|
|
16732
16966
|
}
|
|
16733
16967
|
}
|
|
16734
|
-
|
|
16968
|
+
writeFileSync17(cfg.configFile, plan.text);
|
|
16735
16969
|
process.stdout.write(`added ${top} to ${routeName} as ${plan.mode === "dir" ? "dir" : `repos.${plan.name}`} in ${cfg.configFile}
|
|
16736
16970
|
`);
|
|
16737
16971
|
} catch (e) {
|
|
@@ -17016,6 +17250,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17016
17250
|
}
|
|
17017
17251
|
const route2 = `${found.workspaceSlug}/${found.projectSlug ?? found.projectId}`;
|
|
17018
17252
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, route2);
|
|
17253
|
+
const capabilities = await checkCapabilities(authOpts, found);
|
|
17254
|
+
if (flag("json")) {
|
|
17255
|
+
process.stdout.write(`${JSON.stringify({ event: "capabilities", route: route2, items: capabilities })}
|
|
17256
|
+
`);
|
|
17257
|
+
} else {
|
|
17258
|
+
process.stderr.write("capabilities for this key:\n");
|
|
17259
|
+
for (const i of capabilities) process.stderr.write(` ${describeCapability(i)}
|
|
17260
|
+
`);
|
|
17261
|
+
}
|
|
17262
|
+
const capabilityMisses = capabilities.filter((i) => i.status === "missing").map(missLine);
|
|
17019
17263
|
if (found.locksModelId && !dryRun) {
|
|
17020
17264
|
const configured = cfg.routes.find((r) => r.route === route2);
|
|
17021
17265
|
const repoNames = [...Object.values(found.repoNames ?? {}), ...configured ? reposOf(configured).map((t) => t.name) : []].filter((n) => !!n);
|
|
@@ -17041,10 +17285,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17041
17285
|
}
|
|
17042
17286
|
}
|
|
17043
17287
|
let previous;
|
|
17044
|
-
const hadPreviousFile =
|
|
17288
|
+
const hadPreviousFile = existsSync30(resolvedPath);
|
|
17045
17289
|
if (hadPreviousFile) {
|
|
17046
17290
|
try {
|
|
17047
|
-
previous = JSON.parse(
|
|
17291
|
+
previous = JSON.parse(readFileSync23(resolvedPath, "utf8"));
|
|
17048
17292
|
} catch {
|
|
17049
17293
|
}
|
|
17050
17294
|
}
|
|
@@ -17134,6 +17378,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17134
17378
|
holds: found.holds.map((h) => ({ id: h.id, role: h.name })),
|
|
17135
17379
|
...found.releaseSeat ? { releaseSeat: found.releaseSeat } : {},
|
|
17136
17380
|
...found.role ?? previous?.role ? { role: found.role ?? previous?.role } : {},
|
|
17381
|
+
...capabilityMisses.length > 0 ? { capabilityMisses } : {},
|
|
17137
17382
|
...operator ? { operator } : {},
|
|
17138
17383
|
...contract ? { contract } : {},
|
|
17139
17384
|
...reviewedStatuses.length > 0 ? { reviewedStatuses } : {},
|
|
@@ -17152,10 +17397,10 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17152
17397
|
process.stderr.write(`(dry run) would write resolved ids to ${resolvedPath}
|
|
17153
17398
|
`);
|
|
17154
17399
|
} else {
|
|
17155
|
-
|
|
17400
|
+
mkdirSync16(dirname17(resolvedPath), { recursive: true });
|
|
17156
17401
|
if (hadPreviousFile) copyFileSync2(resolvedPath, `${resolvedPath}.bak`);
|
|
17157
17402
|
const tmpPath = `${resolvedPath}.tmp${process.pid}`;
|
|
17158
|
-
|
|
17403
|
+
writeFileSync17(tmpPath, `${JSON.stringify(resolved, null, 2)}
|
|
17159
17404
|
`);
|
|
17160
17405
|
renameSync2(tmpPath, resolvedPath);
|
|
17161
17406
|
process.stderr.write(`Resolved ids written to ${resolvedPath}
|
|
@@ -17190,8 +17435,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17190
17435
|
process.stderr.write(`(dry run) would write the minted API key to ${apiKeyFile}
|
|
17191
17436
|
`);
|
|
17192
17437
|
} else {
|
|
17193
|
-
|
|
17194
|
-
|
|
17438
|
+
mkdirSync16(dirname17(apiKeyFile), { recursive: true });
|
|
17439
|
+
writeFileSync17(apiKeyFile, `${apiKeyVar}=${apiKey}
|
|
17195
17440
|
`, { mode: 384 });
|
|
17196
17441
|
process.stderr.write(`API key written to ${apiKeyFile}
|
|
17197
17442
|
`);
|
|
@@ -17200,7 +17445,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17200
17445
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
17201
17446
|
const wizard = firstRun;
|
|
17202
17447
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
17203
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
17448
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join30(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
17204
17449
|
const routeBlock = renderConnection(
|
|
17205
17450
|
found,
|
|
17206
17451
|
route2,
|
|
@@ -17795,10 +18040,76 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17795
18040
|
case "disconnect": {
|
|
17796
18041
|
const url = mcpUrlFor(value("base-url") ?? cfg.routes[0]?.baseUrl ?? DEFAULT_BASE_URL);
|
|
17797
18042
|
const disconnectHarnesses = /* @__PURE__ */ new Set([...shipPair(cfg.ship), ...shipToolchains(cfg.ship).map((t) => t.harness).filter(isPairHarness)]);
|
|
17798
|
-
|
|
17799
|
-
|
|
18043
|
+
const removePairEntries = (mcpUrl, dry) => [...disconnectHarnesses].map((h) => pairInstallerFor(h).removePair({ url: mcpUrl, bin: cfg.ship.agent.bin, dryRun: dry }));
|
|
18044
|
+
if (!flag("reset")) {
|
|
18045
|
+
for (const line of removePairEntries(url, dryRun)) process.stderr.write(`${line}
|
|
17800
18046
|
`);
|
|
18047
|
+
break;
|
|
17801
18048
|
}
|
|
18049
|
+
const host = hostPlatform();
|
|
18050
|
+
let resetStore;
|
|
18051
|
+
try {
|
|
18052
|
+
resetStore = getSessionStore3();
|
|
18053
|
+
} catch {
|
|
18054
|
+
resetStore = void 0;
|
|
18055
|
+
}
|
|
18056
|
+
const effects = {
|
|
18057
|
+
async uninstallUnits(dry) {
|
|
18058
|
+
const lines = [];
|
|
18059
|
+
if (host === "windows") return lines;
|
|
18060
|
+
const log = { emit: (m) => {
|
|
18061
|
+
lines.push(m);
|
|
18062
|
+
}, warn: (m) => {
|
|
18063
|
+
lines.push(m);
|
|
18064
|
+
} };
|
|
18065
|
+
for (const job of ["run", "release", "passengers"]) {
|
|
18066
|
+
await applyUninstall(planUninstall(cfg.ship, CREW_HOME, host, detectSystemd(), job), CREW_HOME, dry, log);
|
|
18067
|
+
}
|
|
18068
|
+
return lines;
|
|
18069
|
+
},
|
|
18070
|
+
removePair: removePairEntries,
|
|
18071
|
+
async revokeSeatKeys(r, dry) {
|
|
18072
|
+
const outcomes = await revokeSeatKeys(r, { userAgent: cfg.ship.userAgent, dryRun: dry });
|
|
18073
|
+
return outcomes.map((o) => ({ line: describeRevokeOutcome(o), ok: o.action !== "failed" }));
|
|
18074
|
+
},
|
|
18075
|
+
async credential(r) {
|
|
18076
|
+
if (!resetStore) return { stored: false, clear: async () => {
|
|
18077
|
+
} };
|
|
18078
|
+
const store = resetStore;
|
|
18079
|
+
const h = hostFromUrl3(r.baseUrl);
|
|
18080
|
+
const [slug] = splitRoute(r.route);
|
|
18081
|
+
const session = await store.get(h, slug);
|
|
18082
|
+
return { stored: Boolean(session?.apiKey), clear: () => store.clear(h, slug) };
|
|
18083
|
+
},
|
|
18084
|
+
inspectCheckout(dir) {
|
|
18085
|
+
const dirty = status(dir).map((c) => c.path);
|
|
18086
|
+
const linkedWorktrees = worktrees(dir).map((w) => w.path).filter((p) => realpathSync2(p) !== realpathSync2(dir));
|
|
18087
|
+
return { dirty, linkedWorktrees };
|
|
18088
|
+
}
|
|
18089
|
+
};
|
|
18090
|
+
const droppedRoutes = [];
|
|
18091
|
+
try {
|
|
18092
|
+
const rawCfg = parseYaml2(readFileSync23(cfg.configFile, "utf8"));
|
|
18093
|
+
for (const r of rawCfg?.routes ?? []) {
|
|
18094
|
+
if (typeof r?.route !== "string" || cfg.routes.some((x) => x.route === r.route)) continue;
|
|
18095
|
+
try {
|
|
18096
|
+
splitRoute(r.route);
|
|
18097
|
+
} catch {
|
|
18098
|
+
continue;
|
|
18099
|
+
}
|
|
18100
|
+
droppedRoutes.push({ route: r.route, baseUrl: typeof r.baseUrl === "string" ? r.baseUrl : rawCfg?.ship?.baseUrl ?? DEFAULT_BASE_URL });
|
|
18101
|
+
}
|
|
18102
|
+
} catch {
|
|
18103
|
+
}
|
|
18104
|
+
const steps = await runReset(
|
|
18105
|
+
{ routes: cfg.routes, droppedRoutes, stateDir: cfg.ship.stateDir, configFile: cfg.configFile, dryRun, removeCheckouts: flag("remove-checkouts") },
|
|
18106
|
+
effects
|
|
18107
|
+
);
|
|
18108
|
+
if (flag("json")) process.stdout.write(`${JSON.stringify({ dryRun, ok: !resetIncomplete(steps), steps })}
|
|
18109
|
+
`);
|
|
18110
|
+
else for (const s of steps) process.stdout.write(`${describeResetStep(s)}
|
|
18111
|
+
`);
|
|
18112
|
+
if (resetIncomplete(steps)) process.exitCode = 1;
|
|
17802
18113
|
break;
|
|
17803
18114
|
}
|
|
17804
18115
|
case "install": {
|
|
@@ -17820,8 +18131,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17820
18131
|
process.stdout.write("crew reload: no daemon unit is running \u2014 nothing to reload (the next run reads crew.yaml fresh)\n");
|
|
17821
18132
|
break;
|
|
17822
18133
|
}
|
|
17823
|
-
|
|
17824
|
-
|
|
18134
|
+
mkdirSync16(cfg.ship.stateDir, { recursive: true });
|
|
18135
|
+
writeFileSync17(sentinel, `${(/* @__PURE__ */ new Date()).toISOString()}
|
|
17825
18136
|
`);
|
|
17826
18137
|
process.stdout.write("crew reload: requested \u2014 the daemon finishes its in-flight pass, then relaunches with the new config\n");
|
|
17827
18138
|
if (flag("now") && control?.stopCommand && control.startCommand) {
|
|
@@ -17872,7 +18183,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17872
18183
|
}
|
|
17873
18184
|
case "log": {
|
|
17874
18185
|
try {
|
|
17875
|
-
const text =
|
|
18186
|
+
const text = readFileSync23(cfg.ship.logFile, "utf8").split("\n");
|
|
17876
18187
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
17877
18188
|
`);
|
|
17878
18189
|
} catch {
|
|
@@ -17895,8 +18206,8 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17895
18206
|
};
|
|
17896
18207
|
if (!route.resolved) fail(`route ${route.route} is not connected \u2014 run \`crew connect\` first`);
|
|
17897
18208
|
const tracker = new Tracker(route, cfg.ship);
|
|
17898
|
-
const configText =
|
|
17899
|
-
const cfgBase =
|
|
18209
|
+
const configText = readFileSync23(cfg.configFile, "utf8");
|
|
18210
|
+
const cfgBase = dirname17(cfg.configFile);
|
|
17900
18211
|
const configured = configuredRepos(configText, route.route, cfgBase);
|
|
17901
18212
|
const dir = configured.find((r) => r.name === SAMPLE_REPO)?.dir ?? sampleDir(configured.map((r) => r.dir), route.reposBasePath, route.route.split("/")[0]);
|
|
17902
18213
|
const resolvedFile = resolvedPathFor(cfg.ship.stateDir, route.route);
|
|
@@ -17924,7 +18235,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17924
18235
|
}
|
|
17925
18236
|
if (configured.some((r) => r.dir === dir)) {
|
|
17926
18237
|
try {
|
|
17927
|
-
|
|
18238
|
+
writeFileSync17(cfg.configFile, planRepoRemove(configText, cfgBase, route.route, dir).text);
|
|
17928
18239
|
} catch (e) {
|
|
17929
18240
|
if (e instanceof ReposError) fail(e.message);
|
|
17930
18241
|
throw e;
|
|
@@ -17932,29 +18243,29 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17932
18243
|
}
|
|
17933
18244
|
if (row) {
|
|
17934
18245
|
await tracker.deleteRepoRow(row.id);
|
|
17935
|
-
if (
|
|
17936
|
-
const raw = JSON.parse(
|
|
18246
|
+
if (existsSync30(resolvedFile)) {
|
|
18247
|
+
const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
|
|
17937
18248
|
if (raw.repoNames) delete raw.repoNames[row.id];
|
|
17938
18249
|
if (raw.repoRemotes) delete raw.repoRemotes[row.id];
|
|
17939
|
-
|
|
18250
|
+
writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
17940
18251
|
`);
|
|
17941
18252
|
}
|
|
17942
18253
|
}
|
|
17943
|
-
if (
|
|
18254
|
+
if (existsSync30(join30(dir, ".git")) && basename10(dir) === SAMPLE_REPO) rmSync12(dir, { recursive: true, force: true });
|
|
17944
18255
|
say("removed", { dir, closed }, `removed the sample repo${closed.length ? `, closed ${closed.join(", ")}` : ""}`);
|
|
17945
18256
|
break;
|
|
17946
18257
|
}
|
|
17947
18258
|
const projectId = route.resolved.areaId ?? resolveRepoProject(await tracker.projectRows(), value("project"));
|
|
17948
|
-
const fresh = !
|
|
17949
|
-
if (
|
|
18259
|
+
const fresh = !existsSync30(join30(dir, ".git"));
|
|
18260
|
+
if (existsSync30(dir) && fresh && readdirSync12(dir).length > 0) fail(`${dir} exists and is not a git checkout \u2014 move it aside`);
|
|
17950
18261
|
if (dryRun) {
|
|
17951
18262
|
say("dry-run", { dir, newCheckout: fresh, repoRow: row?.id ?? null }, `would ${fresh ? "create" : "reuse"} ${dir}${row ? "" : ", create the Repos row"}, and file one ${tracker.contract.statuses.approved} ticket`);
|
|
17952
18263
|
break;
|
|
17953
18264
|
}
|
|
17954
18265
|
if (fresh) {
|
|
17955
|
-
|
|
17956
|
-
|
|
17957
|
-
|
|
18266
|
+
mkdirSync16(dir, { recursive: true });
|
|
18267
|
+
writeFileSync17(join30(dir, "README.md"), SAMPLE_README);
|
|
18268
|
+
writeFileSync17(join30(dir, ".crew.yaml"), SAMPLE_CREW_YAML);
|
|
17958
18269
|
const g = (...a) => {
|
|
17959
18270
|
const r = spawnSync6("git", a, { cwd: dir, encoding: "utf8" });
|
|
17960
18271
|
if (r.status !== 0) fail(`git ${a[0]} failed in ${dir}: ${(r.stderr || r.stdout).trim()}`);
|
|
@@ -17972,17 +18283,17 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
17972
18283
|
spec: { platform: "unix", releaseMode: "integrate", ciProvider: "none" }
|
|
17973
18284
|
});
|
|
17974
18285
|
repoId = (await tracker.createRepoRow(payload)).id;
|
|
17975
|
-
if (
|
|
17976
|
-
const raw = JSON.parse(
|
|
18286
|
+
if (existsSync30(resolvedFile)) {
|
|
18287
|
+
const raw = JSON.parse(readFileSync23(resolvedFile, "utf8"));
|
|
17977
18288
|
raw.repoNames = { ...raw.repoNames, [repoId]: SAMPLE_REPO };
|
|
17978
18289
|
raw.repoRemotes = { ...raw.repoRemotes, [repoId]: SAMPLE_REMOTE };
|
|
17979
|
-
|
|
18290
|
+
writeFileSync17(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
17980
18291
|
`);
|
|
17981
18292
|
}
|
|
17982
18293
|
}
|
|
17983
18294
|
if (!configured.some((r) => r.dir === dir)) {
|
|
17984
18295
|
try {
|
|
17985
|
-
|
|
18296
|
+
writeFileSync17(cfg.configFile, planRepoAdd({ text: configText, base: cfgBase, route: route.route, path: dir, name: SAMPLE_REPO }).text);
|
|
17986
18297
|
} catch (e) {
|
|
17987
18298
|
if (e instanceof ReposError) fail(e.message);
|
|
17988
18299
|
throw e;
|
|
@@ -18084,6 +18395,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18084
18395
|
const issueLabelById = new Map(engagedTickets.map((t) => [t.id, t.issue_id]));
|
|
18085
18396
|
const todo = await statusTracker.openTickets().then((open) => operatorTodo(open, statusTracker.contract, route.resolved?.operator ?? "")).catch(() => []);
|
|
18086
18397
|
const projects = projectModeLabels(await statusTracker.projectRowsWithModes().catch(() => []));
|
|
18398
|
+
const crewMinVersion = await readCrewFloor(route, cfg.ship) ?? null;
|
|
18087
18399
|
if (flag("json")) {
|
|
18088
18400
|
const repos = await resolvedRepos(route);
|
|
18089
18401
|
const seatIds = route.resolved?.seats ?? {};
|
|
@@ -18143,6 +18455,9 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
18143
18455
|
watermark: state.watermark(),
|
|
18144
18456
|
route: {
|
|
18145
18457
|
name: route.route,
|
|
18458
|
+
// Additive (CREW-2248): the running version and the workspace's floor. `version` stays 1.
|
|
18459
|
+
crewVersion: sentVersion(cfg.ship),
|
|
18460
|
+
crewMinVersion,
|
|
18146
18461
|
// Additive (CREW-1496): each project and its work mode. `version` stays 1.
|
|
18147
18462
|
projects: projects.map((p) => ({ id: p.id, name: p.name, workMode: p.mode })),
|
|
18148
18463
|
dir: route.dir,
|
|
@@ -18272,7 +18587,7 @@ watermark: ${state.watermark()}
|
|
|
18272
18587
|
}
|
|
18273
18588
|
for (const r of repos) {
|
|
18274
18589
|
const remote = r.config.branch.remote;
|
|
18275
|
-
if (
|
|
18590
|
+
if (existsSync30(r.dir)) {
|
|
18276
18591
|
pushAuth.push({
|
|
18277
18592
|
route: c.route,
|
|
18278
18593
|
repo: r.name,
|
|
@@ -18300,7 +18615,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
18300
18615
|
);
|
|
18301
18616
|
try {
|
|
18302
18617
|
const evFile = eventFileFor(cfg.ship.stateDir);
|
|
18303
|
-
const evSize =
|
|
18618
|
+
const evSize = existsSync30(evFile) ? statSync10(evFile).size : 0;
|
|
18304
18619
|
process.stdout.write(
|
|
18305
18620
|
evSize > cfg.ship.eventsRotateBytes * 4 ? `events log: WARN ${evFile} is ${Math.round(evSize / 1048576)} MB (> 4x eventsRotateBytes); run \`crew reap\` to rotate it
|
|
18306
18621
|
` : "events log: ok\n"
|
|
@@ -18311,7 +18626,7 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
18311
18626
|
const unitPlan = planInstall(cfg.ship, CREW_HOME, host, detectSystemd(), "run");
|
|
18312
18627
|
const abandons = unitAbandonsAgents(unitPlan, (p) => {
|
|
18313
18628
|
try {
|
|
18314
|
-
return
|
|
18629
|
+
return readFileSync23(p, "utf8");
|
|
18315
18630
|
} catch {
|
|
18316
18631
|
return null;
|
|
18317
18632
|
}
|
|
@@ -18327,12 +18642,25 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
18327
18642
|
const changed2 = configChangedAfter(daemonConfigPaths(cfg), daemonStartedAt);
|
|
18328
18643
|
if (changed2) {
|
|
18329
18644
|
process.stdout.write(
|
|
18330
|
-
`daemon config: WARN ${changed2} changed at ${new Date(
|
|
18645
|
+
`daemon config: WARN ${changed2} changed at ${new Date(statSync10(changed2).mtimeMs).toISOString()} but the daemon started at ${new Date(daemonStartedAt).toISOString()} \u2014 run \`crew reload\`
|
|
18331
18646
|
`
|
|
18332
18647
|
);
|
|
18333
18648
|
}
|
|
18334
18649
|
}
|
|
18335
18650
|
const tracker = new Tracker(route, cfg.ship);
|
|
18651
|
+
{
|
|
18652
|
+
const have = sentVersion(cfg.ship);
|
|
18653
|
+
const floor = await readCrewFloor(route, cfg.ship);
|
|
18654
|
+
if (have && floor) {
|
|
18655
|
+
process.stdout.write(
|
|
18656
|
+
`crew version: ${belowFloor(have, floor) ? "WARN " : ""}${have} (workspace requires ${floor})${belowFloor(have, floor) ? " \u2014 update crew (the Crew app, or `npm install -g @tablation/crew`)" : ""}
|
|
18657
|
+
`
|
|
18658
|
+
);
|
|
18659
|
+
} else if (have) {
|
|
18660
|
+
process.stdout.write(`crew version: ${have}${floor === null ? " (workspace sets no minimum)" : ""}
|
|
18661
|
+
`);
|
|
18662
|
+
}
|
|
18663
|
+
}
|
|
18336
18664
|
const contractProblems = validateContract(tracker.contract);
|
|
18337
18665
|
process.stdout.write(
|
|
18338
18666
|
contractProblems.length ? `contract: ${contractProblems.length} problem(s)
|
|
@@ -18387,6 +18715,8 @@ routes: ${cfg.routes.map((c) => c.route).join(", ")}
|
|
|
18387
18715
|
const keyRole = route.resolved?.role ?? await fetchKeyRole(route, cfg.ship.userAgent);
|
|
18388
18716
|
const roleRoute = route.resolved && keyRole ? { ...route, resolved: { ...route.resolved, role: keyRole } } : route;
|
|
18389
18717
|
process.stdout.write(`api key role: ${keyRole ?? "unknown"}
|
|
18718
|
+
`);
|
|
18719
|
+
for (const m of route.resolved?.capabilityMisses ?? []) process.stdout.write(`capability: MISSING ${m} (re-run \`crew connect\` once granted)
|
|
18390
18720
|
`);
|
|
18391
18721
|
const canMint = await canMintAgentKeys(roleRoute, { userAgent: cfg.ship.userAgent });
|
|
18392
18722
|
process.stdout.write(`can mint agent keys: ${canMint === void 0 ? "unknown" : canMint ? "yes" : "no"}
|