@tablation/crew 0.7.1 → 0.9.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 +2 -2
- package/dist/cli.js +272 -68
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -213,8 +213,8 @@ crew inbox [--member NAME] your tickets across every workspace
|
|
|
213
213
|
crew connect WS[/PROJECT] resolve a workspace/project's ids into the state tree
|
|
214
214
|
crew repos add [ROUTE] [NAME] [PATH] attach a local checkout to a route in crew.yaml, creating its Repos row if missing (--dry-run previews)
|
|
215
215
|
crew repos list ROUTE the checkouts a route has, and the tracker Repos row each matches
|
|
216
|
-
crew install
|
|
217
|
-
crew uninstall
|
|
216
|
+
crew install [--replace] write and load this platform's scheduler unit (macOS: clears a crew installed from another path)
|
|
217
|
+
crew uninstall [--all] unload and remove it (--all: every crew launchd unit, whichever path it came from)
|
|
218
218
|
```
|
|
219
219
|
|
|
220
220
|
Every command that could change something takes `--dry-run`.
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { resolve as resolve4, dirname as dirname9, join as
|
|
4
|
+
import { resolve as resolve4, dirname as dirname9, join as join19, basename as basename7 } from "path";
|
|
5
5
|
import { fileURLToPath } from "url";
|
|
6
|
-
import { hostname as hostname2, homedir as
|
|
7
|
-
import { spawnSync as
|
|
6
|
+
import { hostname as hostname2, homedir as homedir5 } from "os";
|
|
7
|
+
import { spawnSync as spawnSync3 } from "child_process";
|
|
8
8
|
|
|
9
9
|
// src/config.ts
|
|
10
10
|
import { readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
@@ -1930,7 +1930,7 @@ function describeRelease(o, route) {
|
|
|
1930
1930
|
// src/install.ts
|
|
1931
1931
|
import { dirname as dirname3, join as join5, extname } from "path";
|
|
1932
1932
|
import { homedir as homedir2 } from "os";
|
|
1933
|
-
import { accessSync, constants, existsSync as existsSync5, mkdirSync as mkdirSync3, statSync as statSync2, writeFileSync as writeFileSync2, unlinkSync, realpathSync } from "fs";
|
|
1933
|
+
import { accessSync, constants, existsSync as existsSync5, mkdirSync as mkdirSync3, readdirSync, readFileSync as readFileSync5, statSync as statSync2, writeFileSync as writeFileSync2, unlinkSync, realpathSync } from "fs";
|
|
1934
1934
|
import { spawnSync } from "child_process";
|
|
1935
1935
|
import { createHash } from "crypto";
|
|
1936
1936
|
|
|
@@ -2164,7 +2164,8 @@ function planUninstall(ship, crewHome, hostShip, hasSystemd, job = "run") {
|
|
|
2164
2164
|
mechanism: "launchd",
|
|
2165
2165
|
unitPaths: [unitPath],
|
|
2166
2166
|
crontabLine: null,
|
|
2167
|
-
unloadCommands: [["launchctl", "unload", unitPath]]
|
|
2167
|
+
unloadCommands: [["launchctl", "unload", unitPath]],
|
|
2168
|
+
removeLabel: label
|
|
2168
2169
|
};
|
|
2169
2170
|
}
|
|
2170
2171
|
if (hostShip === "linux" && hasSystemd) {
|
|
@@ -2282,6 +2283,15 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
2282
2283
|
if (!ok) log.warn("unload command failed \u2014 it may not have been loaded; continuing to remove files");
|
|
2283
2284
|
}
|
|
2284
2285
|
}
|
|
2286
|
+
if (plan.removeLabel) {
|
|
2287
|
+
if (dryRun) {
|
|
2288
|
+
log.emit(`would run: launchctl remove ${plan.removeLabel}`);
|
|
2289
|
+
} else {
|
|
2290
|
+
if (spawnSync("launchctl", ["remove", plan.removeLabel], { encoding: "utf8" }).status === 0) {
|
|
2291
|
+
log.emit(`removed loaded job ${plan.removeLabel}`);
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2285
2295
|
for (const path of plan.unitPaths) {
|
|
2286
2296
|
if (!existsSync5(path)) continue;
|
|
2287
2297
|
if (dryRun) {
|
|
@@ -2300,6 +2310,91 @@ async function applyUninstall(plan, crewHome, dryRun, log) {
|
|
|
2300
2310
|
}
|
|
2301
2311
|
}
|
|
2302
2312
|
}
|
|
2313
|
+
var CREW_LAUNCHD_PREFIX = "com.tablation.crew";
|
|
2314
|
+
function launchAgentsDir() {
|
|
2315
|
+
return join5(homedir2(), "Library", "LaunchAgents");
|
|
2316
|
+
}
|
|
2317
|
+
function xmlUnescape(s) {
|
|
2318
|
+
return s.replace(/</g, "<").replace(/>/g, ">").replace(/&/g, "&");
|
|
2319
|
+
}
|
|
2320
|
+
function programFromPlist(text) {
|
|
2321
|
+
const block = /<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/.exec(text);
|
|
2322
|
+
if (!block) return null;
|
|
2323
|
+
const args = [...block[1].matchAll(/<string>([\s\S]*?)<\/string>/g)].map((m) => xmlUnescape(m[1]));
|
|
2324
|
+
return args.find((a) => a.endsWith("/bin/crew")) ?? args[0] ?? null;
|
|
2325
|
+
}
|
|
2326
|
+
function listLaunchdUnits(dir = launchAgentsDir()) {
|
|
2327
|
+
if (!existsSync5(dir)) return [];
|
|
2328
|
+
return readdirSync(dir).filter((f) => f.startsWith(CREW_LAUNCHD_PREFIX) && f.endsWith(".plist")).sort().map((f) => {
|
|
2329
|
+
const plistPath = join5(dir, f);
|
|
2330
|
+
let program = null;
|
|
2331
|
+
try {
|
|
2332
|
+
program = programFromPlist(readFileSync5(plistPath, "utf8"));
|
|
2333
|
+
} catch {
|
|
2334
|
+
}
|
|
2335
|
+
return {
|
|
2336
|
+
label: f.slice(0, -".plist".length),
|
|
2337
|
+
plistPath,
|
|
2338
|
+
program,
|
|
2339
|
+
programExists: program !== null && existsSync5(program)
|
|
2340
|
+
};
|
|
2341
|
+
});
|
|
2342
|
+
}
|
|
2343
|
+
function foreignLaunchdUnits(crewHome, dir) {
|
|
2344
|
+
const own = new Set(["run", "release", "passengers"].map((job) => labelFor(crewHome, job)));
|
|
2345
|
+
return listLaunchdUnits(dir).filter((u) => !own.has(u.label));
|
|
2346
|
+
}
|
|
2347
|
+
function removeLaunchdUnit(unit, dryRun, log) {
|
|
2348
|
+
if (dryRun) {
|
|
2349
|
+
log.emit(`would unload and remove ${unit.label}`);
|
|
2350
|
+
return;
|
|
2351
|
+
}
|
|
2352
|
+
if (existsSync5(unit.plistPath)) spawnSync("launchctl", ["unload", unit.plistPath], { encoding: "utf8" });
|
|
2353
|
+
spawnSync("launchctl", ["remove", unit.label], { encoding: "utf8" });
|
|
2354
|
+
if (existsSync5(unit.plistPath)) unlinkSync(unit.plistPath);
|
|
2355
|
+
log.emit(`removed ${unit.label} (${unit.plistPath})`);
|
|
2356
|
+
}
|
|
2357
|
+
function clearForeignLaunchdUnits(crewHome, replace, dryRun, log, dir) {
|
|
2358
|
+
const foreign = foreignLaunchdUnits(crewHome, dir);
|
|
2359
|
+
const blocking = foreign.filter((u) => u.program === null || u.programExists);
|
|
2360
|
+
if (blocking.length > 0 && !replace) {
|
|
2361
|
+
log.warn(
|
|
2362
|
+
`two crew installs target this machine: ${blocking.map((u) => `${u.label} runs ${u.program ?? "an unreadable ProgramArguments"}`).join("; ")}. Run \`crew uninstall\` from the other one, or pass --replace.`
|
|
2363
|
+
);
|
|
2364
|
+
return false;
|
|
2365
|
+
}
|
|
2366
|
+
for (const unit of foreign) {
|
|
2367
|
+
const why = unit.program === null ? "replacing a unit with no readable ProgramArguments" : unit.programExists ? "replacing a live other install" : "removing a stale unit whose program is gone";
|
|
2368
|
+
log.emit(`${why}: ${unit.label}`);
|
|
2369
|
+
removeLaunchdUnit(unit, dryRun, log);
|
|
2370
|
+
}
|
|
2371
|
+
return true;
|
|
2372
|
+
}
|
|
2373
|
+
function removeAllLaunchdUnits(dryRun, log, dir) {
|
|
2374
|
+
for (const unit of listLaunchdUnits(dir)) removeLaunchdUnit(unit, dryRun, log);
|
|
2375
|
+
}
|
|
2376
|
+
function parseLaunchctlList(text) {
|
|
2377
|
+
const jobs = /* @__PURE__ */ new Map();
|
|
2378
|
+
for (const line of text.split("\n")) {
|
|
2379
|
+
const cols = line.split(" ");
|
|
2380
|
+
if (cols.length !== 3 || cols[2] === "Label") continue;
|
|
2381
|
+
jobs.set(cols[2], { pid: cols[0], lastExit: cols[1] });
|
|
2382
|
+
}
|
|
2383
|
+
return jobs;
|
|
2384
|
+
}
|
|
2385
|
+
function describeForeignLaunchdUnits(crewHome, dir) {
|
|
2386
|
+
const foreign = foreignLaunchdUnits(crewHome, dir);
|
|
2387
|
+
if (foreign.length === 0) return [];
|
|
2388
|
+
const list = spawnSync("launchctl", ["list"], { encoding: "utf8" });
|
|
2389
|
+
const jobs = parseLaunchctlList(list.status === 0 ? list.stdout ?? "" : "");
|
|
2390
|
+
return foreign.map((u) => {
|
|
2391
|
+
const status2 = jobs.get(u.label);
|
|
2392
|
+
const loaded = status2 ? `loaded, last exit ${status2.lastExit}` : "not loaded";
|
|
2393
|
+
const program = u.program ? `${u.program} (${u.programExists ? "present" : "GONE"})` : "no readable ProgramArguments";
|
|
2394
|
+
const stale = status2 && !u.programExists ? " \u2014 stale: `crew install` removes it" : "";
|
|
2395
|
+
return ` ${u.label}: ${loaded}; runs ${program}${stale}`;
|
|
2396
|
+
});
|
|
2397
|
+
}
|
|
2303
2398
|
function readCrontab() {
|
|
2304
2399
|
const res = spawnSync("crontab", ["-l"], { encoding: "utf8" });
|
|
2305
2400
|
return res.status === 0 ? res.stdout ?? "" : "";
|
|
@@ -4511,7 +4606,7 @@ var alreadyReported = (ticket, g, comments) => comments.some((c) => c.ticket_id
|
|
|
4511
4606
|
|
|
4512
4607
|
// src/agent.ts
|
|
4513
4608
|
import { spawn as spawn2 } from "child_process";
|
|
4514
|
-
import { readFileSync as
|
|
4609
|
+
import { readFileSync as readFileSync6, existsSync as existsSync7, statSync as statSync4, mkdirSync as mkdirSync5, writeFileSync as writeFileSync4, createWriteStream } from "fs";
|
|
4515
4610
|
import { finished } from "stream/promises";
|
|
4516
4611
|
import { join as join7, dirname as dirname5 } from "path";
|
|
4517
4612
|
import { createHash as createHash2 } from "crypto";
|
|
@@ -4954,10 +5049,10 @@ function planAgentRun(o) {
|
|
|
4954
5049
|
let ageSeconds;
|
|
4955
5050
|
if (existsSync7(digestPath2)) {
|
|
4956
5051
|
ageSeconds = Math.floor((now() - statSync4(digestPath2).mtimeMs) / 1e3);
|
|
4957
|
-
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest =
|
|
5052
|
+
if (ageSeconds <= DIGEST_MAX_AGE_SECONDS) digest = readFileSync6(digestPath2, "utf8");
|
|
4958
5053
|
}
|
|
4959
|
-
const common =
|
|
4960
|
-
const brief =
|
|
5054
|
+
const common = readFileSync6(commonPath, "utf8");
|
|
5055
|
+
const brief = readFileSync6(briefPath, "utf8");
|
|
4961
5056
|
const effectiveCommon = o.divergedPrompt ? "" : common;
|
|
4962
5057
|
const effectiveBrief = o.divergedPrompt ?? brief;
|
|
4963
5058
|
const prompt = assemblePrompt({
|
|
@@ -5337,7 +5432,7 @@ Wrote ${o.configPath}.
|
|
|
5337
5432
|
}
|
|
5338
5433
|
|
|
5339
5434
|
// src/release-run.ts
|
|
5340
|
-
import { readFileSync as
|
|
5435
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync6, existsSync as existsSync9 } from "fs";
|
|
5341
5436
|
import { join as join9 } from "path";
|
|
5342
5437
|
|
|
5343
5438
|
// src/release.ts
|
|
@@ -5802,7 +5897,7 @@ async function bump(o, size) {
|
|
|
5802
5897
|
o.emit.error(`no ${o.repo.release.versionFiles[0]} to read a version from`);
|
|
5803
5898
|
return null;
|
|
5804
5899
|
}
|
|
5805
|
-
const current = JSON.parse(
|
|
5900
|
+
const current = JSON.parse(readFileSync7(first, "utf8")).version;
|
|
5806
5901
|
if (!current) {
|
|
5807
5902
|
o.emit.error(`no version field in ${o.repo.release.versionFiles[0]}`);
|
|
5808
5903
|
return null;
|
|
@@ -5815,7 +5910,7 @@ async function bump(o, size) {
|
|
|
5815
5910
|
return version;
|
|
5816
5911
|
}
|
|
5817
5912
|
for (const f of files) {
|
|
5818
|
-
const src =
|
|
5913
|
+
const src = readFileSync7(f, "utf8");
|
|
5819
5914
|
const out = src.replace(/("version"\s*:\s*")[^"]*(")/, `$1${version}$2`);
|
|
5820
5915
|
if (out === src) {
|
|
5821
5916
|
o.emit.error(`no version field in ${f}`);
|
|
@@ -5834,7 +5929,7 @@ function writeChangelog(o, version, entries) {
|
|
|
5834
5929
|
o.emit.emit(`would prepend ${entries.length} entr(ies) to ${file}`);
|
|
5835
5930
|
return;
|
|
5836
5931
|
}
|
|
5837
|
-
const existing = existsSync9(path) ?
|
|
5932
|
+
const existing = existsSync9(path) ? readFileSync7(path, "utf8") : "# Changelog\n";
|
|
5838
5933
|
writeFileSync6(path, insertChangelogSection(existing, section));
|
|
5839
5934
|
}
|
|
5840
5935
|
async function confirm(o, expected) {
|
|
@@ -6682,7 +6777,7 @@ function nextRoles(decision, ran) {
|
|
|
6682
6777
|
}
|
|
6683
6778
|
|
|
6684
6779
|
// src/agents.ts
|
|
6685
|
-
import { readFileSync as
|
|
6780
|
+
import { readFileSync as readFileSync8, existsSync as existsSync12 } from "fs";
|
|
6686
6781
|
import { join as join11 } from "path";
|
|
6687
6782
|
import { TablationClient as TablationClient2, StaleWriteError as StaleWriteError6, TablationApiError } from "@tablation/client";
|
|
6688
6783
|
var PERSONA_NAME = {
|
|
@@ -6699,10 +6794,10 @@ var AgentsSyncError = class extends Error {
|
|
|
6699
6794
|
function personaDefaultPrompt(promptsDir, role) {
|
|
6700
6795
|
const briefPath = join11(promptsDir, "personas", `lane-${role}.md`);
|
|
6701
6796
|
if (!existsSync12(briefPath)) throw new AgentsSyncError(`no brief at ${briefPath}`);
|
|
6702
|
-
if (role === "pair") return
|
|
6797
|
+
if (role === "pair") return readFileSync8(briefPath, "utf8");
|
|
6703
6798
|
const commonPath = join11(promptsDir, "personas", "common.md");
|
|
6704
6799
|
if (!existsSync12(commonPath)) throw new AgentsSyncError(`no shared policy at ${commonPath}`);
|
|
6705
|
-
return `${
|
|
6800
|
+
return `${readFileSync8(commonPath, "utf8")}${readFileSync8(briefPath, "utf8")}`;
|
|
6706
6801
|
}
|
|
6707
6802
|
async function resolveAgentId(route, role, opts) {
|
|
6708
6803
|
const cached = route.resolved?.agentPersonas?.[role]?.agentId;
|
|
@@ -7323,14 +7418,14 @@ import {
|
|
|
7323
7418
|
} from "@tablation/client";
|
|
7324
7419
|
|
|
7325
7420
|
// src/skills.ts
|
|
7326
|
-
import { readFileSync as
|
|
7421
|
+
import { readFileSync as readFileSync9, readdirSync as readdirSync2, existsSync as existsSync13 } from "fs";
|
|
7327
7422
|
import { join as join12 } from "path";
|
|
7328
7423
|
import { parse as parseYaml } from "yaml";
|
|
7329
7424
|
import { TablationClient as TablationClient3, StaleWriteError as StaleWriteError7, TablationApiError as TablationApiError2 } from "@tablation/client";
|
|
7330
7425
|
var SkillSyncError = class extends Error {
|
|
7331
7426
|
};
|
|
7332
7427
|
function parseSkillFile(path) {
|
|
7333
|
-
const text =
|
|
7428
|
+
const text = readFileSync9(path, "utf8");
|
|
7334
7429
|
const match = /^---\n([\s\S]*?)\n---\n([\s\S]*)$/.exec(text);
|
|
7335
7430
|
if (!match) throw new SkillSyncError(`${path}: missing YAML frontmatter (expected a leading --- block)`);
|
|
7336
7431
|
const frontmatter = match[1];
|
|
@@ -7345,7 +7440,7 @@ function parseSkillFile(path) {
|
|
|
7345
7440
|
function listLocalSkills(promptsDir) {
|
|
7346
7441
|
const dir = join12(promptsDir, "skills");
|
|
7347
7442
|
if (!existsSync13(dir)) return [];
|
|
7348
|
-
return
|
|
7443
|
+
return readdirSync2(dir).filter((f) => f.endsWith(".md")).map((f) => parseSkillFile(join12(dir, f)));
|
|
7349
7444
|
}
|
|
7350
7445
|
async function syncSkills(route, opts) {
|
|
7351
7446
|
if (!route.resolved) {
|
|
@@ -7668,6 +7763,72 @@ function resolveRepoProject(projects, opt, routeAreaId) {
|
|
|
7668
7763
|
);
|
|
7669
7764
|
}
|
|
7670
7765
|
|
|
7766
|
+
// src/claude-mcp.ts
|
|
7767
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
7768
|
+
import { readFileSync as readFileSync10 } from "fs";
|
|
7769
|
+
import { homedir as homedir4 } from "os";
|
|
7770
|
+
import { join as join13 } from "path";
|
|
7771
|
+
var MCP_SERVER_NAME = "tablation";
|
|
7772
|
+
var redact = (s, key) => key ? s.split(key).join("sk_***") : s;
|
|
7773
|
+
function mcpUrlFor(baseUrl) {
|
|
7774
|
+
return `${baseUrl.replace(/\/+$/, "")}/api/mcp`;
|
|
7775
|
+
}
|
|
7776
|
+
function runClaude(bin, args) {
|
|
7777
|
+
const r = spawnSync2(bin, args, { encoding: "utf8", timeout: 3e4 });
|
|
7778
|
+
const missing = r.error?.code === "ENOENT";
|
|
7779
|
+
return { status: r.status, missing, stderr: r.stderr ?? "" };
|
|
7780
|
+
}
|
|
7781
|
+
function userScopeEntry(home) {
|
|
7782
|
+
try {
|
|
7783
|
+
const j = JSON.parse(readFileSync10(join13(home, ".claude.json"), "utf8"));
|
|
7784
|
+
return j.mcpServers?.[MCP_SERVER_NAME];
|
|
7785
|
+
} catch {
|
|
7786
|
+
return void 0;
|
|
7787
|
+
}
|
|
7788
|
+
}
|
|
7789
|
+
function inspectClaudeMcp(o) {
|
|
7790
|
+
const got = runClaude(o.bin, ["mcp", "get", MCP_SERVER_NAME]);
|
|
7791
|
+
if (got.missing) return { state: "no-claude" };
|
|
7792
|
+
if (got.status !== 0) return { state: "missing" };
|
|
7793
|
+
const entry = userScopeEntry(o.home ?? homedir4());
|
|
7794
|
+
if (!entry) {
|
|
7795
|
+
return { state: "different", detail: `a "${MCP_SERVER_NAME}" server exists, but not in user scope (~/.claude.json) \u2014 a project or local registration` };
|
|
7796
|
+
}
|
|
7797
|
+
if (entry.url !== o.url) return { state: "different", detail: `it points at ${entry.url ?? "(no url)"}, not ${o.url}` };
|
|
7798
|
+
const auth = Object.entries(entry.headers ?? {}).find(([k]) => k.toLowerCase() === "authorization")?.[1];
|
|
7799
|
+
if (auth !== `Bearer ${o.key}`) return { state: "different", detail: `it is registered with a different key than this route's` };
|
|
7800
|
+
return { state: "registered", url: o.url };
|
|
7801
|
+
}
|
|
7802
|
+
function ensureClaudeMcp(o) {
|
|
7803
|
+
const manual = `claude mcp add --transport http ${MCP_SERVER_NAME} ${o.url} --header "Authorization: Bearer <key>" --scope user`;
|
|
7804
|
+
const st = inspectClaudeMcp(o);
|
|
7805
|
+
if (st.state === "no-claude") return `mcp: \`${o.bin}\` not found, so Claude Code was not configured. To do it by hand: ${manual}`;
|
|
7806
|
+
if (st.state === "registered") return `mcp: ${MCP_SERVER_NAME} already registered with Claude Code \u2192 ${st.url}`;
|
|
7807
|
+
if (st.state === "different" && !o.replace) {
|
|
7808
|
+
return `mcp: left Claude Code's "${MCP_SERVER_NAME}" server alone \u2014 ${st.detail}. Re-run with --mcp replace to overwrite it.`;
|
|
7809
|
+
}
|
|
7810
|
+
if (o.dryRun) return `(dry run) would register the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url}`;
|
|
7811
|
+
if (st.state === "different") {
|
|
7812
|
+
runClaude(o.bin, ["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]);
|
|
7813
|
+
}
|
|
7814
|
+
const add = runClaude(o.bin, [
|
|
7815
|
+
"mcp",
|
|
7816
|
+
"add",
|
|
7817
|
+
"--transport",
|
|
7818
|
+
"http",
|
|
7819
|
+
MCP_SERVER_NAME,
|
|
7820
|
+
o.url,
|
|
7821
|
+
"--header",
|
|
7822
|
+
`Authorization: Bearer ${o.key}`,
|
|
7823
|
+
"--scope",
|
|
7824
|
+
"user"
|
|
7825
|
+
]);
|
|
7826
|
+
if (add.status !== 0) {
|
|
7827
|
+
return `mcp: \`claude mcp add\` failed (${redact(add.stderr.trim(), o.key) || `exit ${add.status}`}). To do it by hand: ${manual}`;
|
|
7828
|
+
}
|
|
7829
|
+
return `mcp: registered the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url} (user scope, ~/.claude.json)`;
|
|
7830
|
+
}
|
|
7831
|
+
|
|
7671
7832
|
// src/doctor-fix.ts
|
|
7672
7833
|
import { parseDocument as parseDocument2 } from "yaml";
|
|
7673
7834
|
var sound = (r) => r.hasRepos && r.problems.length === 0;
|
|
@@ -7818,14 +7979,14 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
7818
7979
|
}
|
|
7819
7980
|
|
|
7820
7981
|
// src/stream-sweep.ts
|
|
7821
|
-
import { existsSync as existsSync15, readdirSync as
|
|
7822
|
-
import { join as
|
|
7982
|
+
import { existsSync as existsSync15, readdirSync as readdirSync3, rmSync as rmSync2, statSync as statSync6 } from "fs";
|
|
7983
|
+
import { join as join14 } from "path";
|
|
7823
7984
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
7824
7985
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
7825
|
-
const dir =
|
|
7986
|
+
const dir = join14(stateDir, "streams");
|
|
7826
7987
|
if (!existsSync15(dir)) return [];
|
|
7827
7988
|
const cutoff = now - retentionDays * DAY_MS;
|
|
7828
|
-
return
|
|
7989
|
+
return readdirSync3(dir).map((name) => join14(dir, name)).filter((path) => {
|
|
7829
7990
|
try {
|
|
7830
7991
|
return statSync6(path).mtimeMs < cutoff;
|
|
7831
7992
|
} catch {
|
|
@@ -7852,18 +8013,18 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
7852
8013
|
}
|
|
7853
8014
|
|
|
7854
8015
|
// src/cli.ts
|
|
7855
|
-
import { readFileSync as
|
|
8016
|
+
import { readFileSync as readFileSync14, existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, copyFileSync, renameSync } from "fs";
|
|
7856
8017
|
import { createInterface } from "readline/promises";
|
|
7857
8018
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
7858
8019
|
|
|
7859
8020
|
// src/ssh-keys.ts
|
|
7860
8021
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
7861
|
-
import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as
|
|
7862
|
-
import { dirname as dirname8, join as
|
|
8022
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync11, chmodSync } from "fs";
|
|
8023
|
+
import { dirname as dirname8, join as join15 } from "path";
|
|
7863
8024
|
var SshKeyError = class extends Error {
|
|
7864
8025
|
};
|
|
7865
8026
|
function shipSshKeyPathFor(stateDir) {
|
|
7866
|
-
return
|
|
8027
|
+
return join15(stateDir, "ssh", "id_ed25519");
|
|
7867
8028
|
}
|
|
7868
8029
|
function ensureShipSshKeypair(stateDir) {
|
|
7869
8030
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
@@ -7892,7 +8053,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
7892
8053
|
if (!existsSync16(publicKeyPath)) {
|
|
7893
8054
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
7894
8055
|
}
|
|
7895
|
-
const publicKey =
|
|
8056
|
+
const publicKey = readFileSync11(publicKeyPath, "utf8").trim();
|
|
7896
8057
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
7897
8058
|
}
|
|
7898
8059
|
function sshKeygenAvailable() {
|
|
@@ -8106,9 +8267,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
8106
8267
|
|
|
8107
8268
|
// src/tunnel.ts
|
|
8108
8269
|
import { spawn as spawn3 } from "child_process";
|
|
8109
|
-
import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as
|
|
8270
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync12, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, readdirSync as readdirSync4 } from "fs";
|
|
8110
8271
|
import { randomInt } from "crypto";
|
|
8111
|
-
import { join as
|
|
8272
|
+
import { join as join16 } from "path";
|
|
8112
8273
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
8113
8274
|
var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
8114
8275
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -8175,20 +8336,20 @@ function startTunnel(opts) {
|
|
|
8175
8336
|
};
|
|
8176
8337
|
}
|
|
8177
8338
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
8178
|
-
return
|
|
8339
|
+
return join16(stateDir, "passengers", `${workspaceId}.json`);
|
|
8179
8340
|
}
|
|
8180
8341
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
8181
8342
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
8182
8343
|
if (!existsSync17(path)) return void 0;
|
|
8183
8344
|
try {
|
|
8184
|
-
return JSON.parse(
|
|
8345
|
+
return JSON.parse(readFileSync12(path, "utf8"));
|
|
8185
8346
|
} catch {
|
|
8186
8347
|
return void 0;
|
|
8187
8348
|
}
|
|
8188
8349
|
}
|
|
8189
8350
|
function writePersistedTunnel(stateDir, state) {
|
|
8190
8351
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
8191
|
-
mkdirSync8(
|
|
8352
|
+
mkdirSync8(join16(stateDir, "passengers"), { recursive: true });
|
|
8192
8353
|
writeFileSync7(path, `${JSON.stringify(state, null, 2)}
|
|
8193
8354
|
`);
|
|
8194
8355
|
}
|
|
@@ -8286,9 +8447,9 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
8286
8447
|
}
|
|
8287
8448
|
}
|
|
8288
8449
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
8289
|
-
const dir =
|
|
8450
|
+
const dir = join16(stateDir, "passengers");
|
|
8290
8451
|
if (!existsSync17(dir)) return [];
|
|
8291
|
-
return
|
|
8452
|
+
return readdirSync4(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
8292
8453
|
}
|
|
8293
8454
|
|
|
8294
8455
|
// src/mcp-endpoint-registry.ts
|
|
@@ -8408,8 +8569,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
8408
8569
|
import { createServer as createHttpServer } from "http";
|
|
8409
8570
|
import { spawn as spawn4 } from "child_process";
|
|
8410
8571
|
import { randomBytes } from "crypto";
|
|
8411
|
-
import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as
|
|
8412
|
-
import { join as
|
|
8572
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync13, writeFileSync as writeFileSync8, unlinkSync as unlinkSync4, readdirSync as readdirSync5 } from "fs";
|
|
8573
|
+
import { join as join17 } from "path";
|
|
8413
8574
|
|
|
8414
8575
|
// src/secret-compare.ts
|
|
8415
8576
|
import { timingSafeEqual } from "crypto";
|
|
@@ -8498,20 +8659,20 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
8498
8659
|
return child;
|
|
8499
8660
|
};
|
|
8500
8661
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
8501
|
-
return
|
|
8662
|
+
return join17(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
8502
8663
|
}
|
|
8503
8664
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
8504
8665
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
8505
8666
|
if (!existsSync18(path)) return void 0;
|
|
8506
8667
|
try {
|
|
8507
|
-
return JSON.parse(
|
|
8668
|
+
return JSON.parse(readFileSync13(path, "utf8"));
|
|
8508
8669
|
} catch {
|
|
8509
8670
|
return void 0;
|
|
8510
8671
|
}
|
|
8511
8672
|
}
|
|
8512
8673
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
8513
8674
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
8514
|
-
mkdirSync9(
|
|
8675
|
+
mkdirSync9(join17(stateDir, "passengers"), { recursive: true });
|
|
8515
8676
|
writeFileSync8(path, `${JSON.stringify(state, null, 2)}
|
|
8516
8677
|
`);
|
|
8517
8678
|
}
|
|
@@ -8532,10 +8693,10 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
8532
8693
|
}
|
|
8533
8694
|
}
|
|
8534
8695
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
8535
|
-
const dir =
|
|
8696
|
+
const dir = join17(stateDir, "passengers");
|
|
8536
8697
|
if (!existsSync18(dir)) return [];
|
|
8537
8698
|
const suffix = "-sync.json";
|
|
8538
|
-
return
|
|
8699
|
+
return readdirSync5(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
8539
8700
|
}
|
|
8540
8701
|
function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
8541
8702
|
const alive = deps.isPidAlive ?? isPidAlive;
|
|
@@ -8590,8 +8751,8 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
8590
8751
|
}
|
|
8591
8752
|
|
|
8592
8753
|
// src/daemon.ts
|
|
8593
|
-
import { readdirSync as
|
|
8594
|
-
import { join as
|
|
8754
|
+
import { readdirSync as readdirSync6, statSync as statSync7 } from "fs";
|
|
8755
|
+
import { join as join18 } from "path";
|
|
8595
8756
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
8596
8757
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
8597
8758
|
var defaultSleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
@@ -8629,8 +8790,8 @@ function latestMtime(path) {
|
|
|
8629
8790
|
if (stat.isFile()) return stat.mtimeMs;
|
|
8630
8791
|
if (!stat.isDirectory()) return 0;
|
|
8631
8792
|
let latest = stat.mtimeMs;
|
|
8632
|
-
for (const entry of
|
|
8633
|
-
latest = Math.max(latest, latestMtime(
|
|
8793
|
+
for (const entry of readdirSync6(path, { withFileTypes: true })) {
|
|
8794
|
+
latest = Math.max(latest, latestMtime(join18(path, entry.name)));
|
|
8634
8795
|
}
|
|
8635
8796
|
return latest;
|
|
8636
8797
|
}
|
|
@@ -9006,7 +9167,7 @@ function usage() {
|
|
|
9006
9167
|
resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
|
|
9007
9168
|
crew log [route] tail the log
|
|
9008
9169
|
crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
|
|
9009
|
-
crew connect resolve a workspace's ids into a crew.yaml block
|
|
9170
|
+
crew connect resolve a workspace's ids into a crew.yaml block (--no-mcp skips registering the Tablation MCP server with Claude Code; --mcp replace overwrites a different one)
|
|
9010
9171
|
crew agents sync [route] [--force] push crew's personas AND skill files into the workspace Agents/Agent Skills tables (--force overwrites even a diverged persona)
|
|
9011
9172
|
crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook)
|
|
9012
9173
|
crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
|
|
@@ -9308,12 +9469,21 @@ if (command === "passenger-sync-daemon") {
|
|
|
9308
9469
|
const trimmed = typeof raw === "string" ? raw.trim() : "";
|
|
9309
9470
|
return trimmed ? `${trimmed.toLowerCase()}-` : void 0;
|
|
9310
9471
|
}
|
|
9311
|
-
async function installScheduler() {
|
|
9472
|
+
async function installScheduler(opts = {}) {
|
|
9312
9473
|
const host = hostPlatform();
|
|
9313
9474
|
if (host === "windows") {
|
|
9314
9475
|
process.stderr.write("crew install: no scheduler support yet for Windows (Task Scheduler is planned, not built)\n");
|
|
9315
9476
|
process.exit(2);
|
|
9316
9477
|
}
|
|
9478
|
+
if (host === "macos") {
|
|
9479
|
+
const ok = clearForeignLaunchdUnits(CREW_HOME, !!opts.replace, dryRun, {
|
|
9480
|
+
emit: (m) => process.stdout.write(`${m}
|
|
9481
|
+
`),
|
|
9482
|
+
warn: (m) => process.stderr.write(`crew install: ${m}
|
|
9483
|
+
`)
|
|
9484
|
+
});
|
|
9485
|
+
if (!ok) return false;
|
|
9486
|
+
}
|
|
9317
9487
|
const installPathProblem = cfg.routes.some((r) => r.enabled && r.hostPassengers) ? dockerPathProblem(cfg.ship) : void 0;
|
|
9318
9488
|
if (installPathProblem) {
|
|
9319
9489
|
process.stderr.write(`crew install: Host Passengers is on but ${installPathProblem}
|
|
@@ -9350,14 +9520,14 @@ if (command === "passenger-sync-daemon") {
|
|
|
9350
9520
|
}
|
|
9351
9521
|
if (host === "linux" && hasSystemd) {
|
|
9352
9522
|
const label = labelFor(CREW_HOME, "run");
|
|
9353
|
-
const staleTimer =
|
|
9523
|
+
const staleTimer = join19(homedir5(), ".config", "systemd", "user", `${label}.timer`);
|
|
9354
9524
|
if (existsSync19(staleTimer)) {
|
|
9355
9525
|
process.stdout.write(
|
|
9356
9526
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
9357
9527
|
`
|
|
9358
9528
|
);
|
|
9359
9529
|
if (!dryRun) {
|
|
9360
|
-
|
|
9530
|
+
spawnSync3("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
|
|
9361
9531
|
try {
|
|
9362
9532
|
unlinkSync5(staleTimer);
|
|
9363
9533
|
} catch {
|
|
@@ -9365,6 +9535,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
9365
9535
|
}
|
|
9366
9536
|
}
|
|
9367
9537
|
}
|
|
9538
|
+
return true;
|
|
9368
9539
|
}
|
|
9369
9540
|
async function routeProblems(c) {
|
|
9370
9541
|
const host = hostPlatform();
|
|
@@ -9395,7 +9566,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
9395
9566
|
async function doctorFix(o) {
|
|
9396
9567
|
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))));
|
|
9397
9568
|
const unitExists = (p) => existsSync19(p);
|
|
9398
|
-
const crontab = o.host === "linux" ?
|
|
9569
|
+
const crontab = o.host === "linux" ? spawnSync3("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
9399
9570
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
9400
9571
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
9401
9572
|
for (const n of plan.notes) process.stdout.write(`fix: ${n}
|
|
@@ -9411,14 +9582,13 @@ if (command === "passenger-sync-daemon") {
|
|
|
9411
9582
|
for (const a of plan.actions) {
|
|
9412
9583
|
if (a.kind === "enable") {
|
|
9413
9584
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
9414
|
-
writeFileSync9(cfg.configFile, planEnable(
|
|
9585
|
+
writeFileSync9(cfg.configFile, planEnable(readFileSync14(cfg.configFile, "utf8"), a.route));
|
|
9415
9586
|
enabledSound.add(a.route);
|
|
9416
9587
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
9417
9588
|
} else {
|
|
9418
9589
|
if (enabledSound.size === 0) continue;
|
|
9419
9590
|
if (prompter && !await prompter.confirm("No scheduler is installed for this ship \u2014 run `crew install` now?", false)) continue;
|
|
9420
|
-
await installScheduler();
|
|
9421
|
-
changed2.push("ran `crew install` (scheduler units written and loaded)");
|
|
9591
|
+
if (await installScheduler()) changed2.push("ran `crew install` (scheduler units written and loaded)");
|
|
9422
9592
|
}
|
|
9423
9593
|
}
|
|
9424
9594
|
} finally {
|
|
@@ -10121,7 +10291,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10121
10291
|
}
|
|
10122
10292
|
let failed = false;
|
|
10123
10293
|
for (const cmd2 of commands) {
|
|
10124
|
-
const res2 =
|
|
10294
|
+
const res2 = spawnSync3(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
10125
10295
|
if (res2.stdout) process.stdout.write(res2.stdout);
|
|
10126
10296
|
if (res2.stderr) process.stderr.write(res2.stderr);
|
|
10127
10297
|
if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
|
|
@@ -10139,7 +10309,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10139
10309
|
`);
|
|
10140
10310
|
break;
|
|
10141
10311
|
}
|
|
10142
|
-
const res =
|
|
10312
|
+
const res = spawnSync3(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
10143
10313
|
if (res.stdout) process.stdout.write(res.stdout);
|
|
10144
10314
|
if (res.stderr) process.stderr.write(res.stderr);
|
|
10145
10315
|
if (daemonSub !== "status" && (res.error || res.status !== 0)) {
|
|
@@ -10162,7 +10332,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10162
10332
|
process.exit(1);
|
|
10163
10333
|
}
|
|
10164
10334
|
requireArmed("run the daemon loop");
|
|
10165
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
10335
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join19(CREW_HOME, "src"), join19(CREW_HOME, "dist"), join19(CREW_HOME, "bin")];
|
|
10166
10336
|
const spawnBreaker = new SpawnBreaker();
|
|
10167
10337
|
const stopReason = await runDaemonLoop({
|
|
10168
10338
|
runPass: () => runOnePass({
|
|
@@ -10310,7 +10480,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10310
10480
|
`);
|
|
10311
10481
|
if (!dryRun) {
|
|
10312
10482
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
10313
|
-
const raw = existsSync19(resolvedPath) ? JSON.parse(
|
|
10483
|
+
const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync14(resolvedPath, "utf8")) : {};
|
|
10314
10484
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
10315
10485
|
raw.agentSkills = skillResult.agentSkills;
|
|
10316
10486
|
mkdirSync10(dirOf(resolvedPath), { recursive: true });
|
|
@@ -10359,7 +10529,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10359
10529
|
`);
|
|
10360
10530
|
if (!dryRun) {
|
|
10361
10531
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
10362
|
-
const raw = existsSync19(resolvedPath) ? JSON.parse(
|
|
10532
|
+
const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync14(resolvedPath, "utf8")) : {};
|
|
10363
10533
|
raw.agentSkills = result.agentSkills;
|
|
10364
10534
|
mkdirSync10(dirOf(resolvedPath), { recursive: true });
|
|
10365
10535
|
writeFileSync9(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -10375,14 +10545,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10375
10545
|
process.exit(2);
|
|
10376
10546
|
};
|
|
10377
10547
|
if (sub !== "add" && sub !== "list") usage2();
|
|
10378
|
-
const configText =
|
|
10548
|
+
const configText = readFileSync14(cfg.configFile, "utf8");
|
|
10379
10549
|
let routeName = positional[2];
|
|
10380
10550
|
let addPath = ".";
|
|
10381
10551
|
let addName;
|
|
10382
10552
|
if (sub === "list" && !routeName) usage2();
|
|
10383
10553
|
if (sub === "add") {
|
|
10384
10554
|
try {
|
|
10385
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync19(resolvePath(p.replace(/^~(?=\/|$)/,
|
|
10555
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync19(resolvePath(p.replace(/^~(?=\/|$)/, homedir5()))));
|
|
10386
10556
|
routeName = a.route;
|
|
10387
10557
|
addPath = a.path;
|
|
10388
10558
|
addName = a.name;
|
|
@@ -10399,7 +10569,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10399
10569
|
let resolvedRemotes;
|
|
10400
10570
|
if (existsSync19(resolvedFile)) {
|
|
10401
10571
|
try {
|
|
10402
|
-
resolvedRemotes = JSON.parse(
|
|
10572
|
+
resolvedRemotes = JSON.parse(readFileSync14(resolvedFile, "utf8"));
|
|
10403
10573
|
} catch {
|
|
10404
10574
|
}
|
|
10405
10575
|
}
|
|
@@ -10420,7 +10590,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10420
10590
|
}
|
|
10421
10591
|
break;
|
|
10422
10592
|
}
|
|
10423
|
-
const target = resolvePath(addPath.replace(/^~(?=\/|$)/,
|
|
10593
|
+
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir5()));
|
|
10424
10594
|
const top = existsSync19(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
10425
10595
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
10426
10596
|
const origin = originOf(top);
|
|
@@ -10470,7 +10640,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10470
10640
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
10471
10641
|
`);
|
|
10472
10642
|
if (existsSync19(resolvedFile)) {
|
|
10473
|
-
const raw = JSON.parse(
|
|
10643
|
+
const raw = JSON.parse(readFileSync14(resolvedFile, "utf8"));
|
|
10474
10644
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
10475
10645
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
10476
10646
|
writeFileSync9(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
@@ -10705,7 +10875,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
10705
10875
|
const hadPreviousFile = existsSync19(resolvedPath);
|
|
10706
10876
|
if (hadPreviousFile) {
|
|
10707
10877
|
try {
|
|
10708
|
-
previous = JSON.parse(
|
|
10878
|
+
previous = JSON.parse(readFileSync14(resolvedPath, "utf8"));
|
|
10709
10879
|
} catch {
|
|
10710
10880
|
}
|
|
10711
10881
|
}
|
|
@@ -10793,7 +10963,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
10793
10963
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
10794
10964
|
const wizard = firstRun;
|
|
10795
10965
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
10796
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
10966
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join19(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
10797
10967
|
const routeBlock = renderConnection(
|
|
10798
10968
|
found,
|
|
10799
10969
|
route2,
|
|
@@ -10813,6 +10983,11 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
10813
10983
|
`);
|
|
10814
10984
|
}
|
|
10815
10985
|
if (!wroteConfig) process.stdout.write(routeBlock);
|
|
10986
|
+
if (!flag("no-mcp") && apiKey) {
|
|
10987
|
+
const replace = value("mcp") === "replace";
|
|
10988
|
+
process.stderr.write(`${ensureClaudeMcp({ bin: cfg.ship.agent.bin, url: mcpUrlFor(baseUrl), key: apiKey, replace, dryRun })}
|
|
10989
|
+
`);
|
|
10990
|
+
}
|
|
10816
10991
|
if (found.provisioning.length) {
|
|
10817
10992
|
process.stderr.write(`
|
|
10818
10993
|
Provisioned for this machine (${ship.name}):
|
|
@@ -11203,7 +11378,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
11203
11378
|
break;
|
|
11204
11379
|
}
|
|
11205
11380
|
case "install": {
|
|
11206
|
-
await installScheduler();
|
|
11381
|
+
if (!await installScheduler({ replace: flag("replace") })) process.exit(2);
|
|
11207
11382
|
break;
|
|
11208
11383
|
}
|
|
11209
11384
|
case "uninstall": {
|
|
@@ -11220,6 +11395,16 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
11220
11395
|
emit: (m) => process.stdout.write(`${m}
|
|
11221
11396
|
`),
|
|
11222
11397
|
warn: (m) => process.stderr.write(`crew uninstall: ${m}
|
|
11398
|
+
`)
|
|
11399
|
+
});
|
|
11400
|
+
}
|
|
11401
|
+
if (flag("all") && host === "macos") {
|
|
11402
|
+
process.stdout.write(`uninstalling every crew launchd unit${dryRun ? " (dry run)" : ""}
|
|
11403
|
+
`);
|
|
11404
|
+
removeAllLaunchdUnits(dryRun, {
|
|
11405
|
+
emit: (m) => process.stdout.write(`${m}
|
|
11406
|
+
`),
|
|
11407
|
+
warn: (m) => process.stderr.write(`crew uninstall: ${m}
|
|
11223
11408
|
`)
|
|
11224
11409
|
});
|
|
11225
11410
|
}
|
|
@@ -11231,7 +11416,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
11231
11416
|
}
|
|
11232
11417
|
case "log": {
|
|
11233
11418
|
try {
|
|
11234
|
-
const text =
|
|
11419
|
+
const text = readFileSync14(cfg.ship.logFile, "utf8").split("\n");
|
|
11235
11420
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
11236
11421
|
`);
|
|
11237
11422
|
} catch {
|
|
@@ -11440,6 +11625,17 @@ git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler P
|
|
|
11440
11625
|
if (!agentBinPath || !gitPath) {
|
|
11441
11626
|
process.stdout.write(
|
|
11442
11627
|
` scheduler PATH is ${schedulerPath} \u2014 add the directory holding the missing binary to ship.extraPath in crew.yaml, then re-run \`crew doctor\`.
|
|
11628
|
+
`
|
|
11629
|
+
);
|
|
11630
|
+
}
|
|
11631
|
+
if (route.apiKey) {
|
|
11632
|
+
const mcpUrl = mcpUrlFor(route.baseUrl);
|
|
11633
|
+
const m = inspectClaudeMcp({ bin: cfg.ship.agent.bin, url: mcpUrl, key: route.apiKey });
|
|
11634
|
+
process.stdout.write(
|
|
11635
|
+
m.state === "registered" ? `mcp: tablation registered (user scope) \u2192 ${m.url}
|
|
11636
|
+
` : m.state === "no-claude" ? `mcp: claude not found \u2014 Claude Code was not checked
|
|
11637
|
+
` : m.state === "different" ? `mcp: tablation registered but not for this route \u2014 ${m.detail}
|
|
11638
|
+
` : `mcp: not registered \u2014 run crew connect
|
|
11443
11639
|
`
|
|
11444
11640
|
);
|
|
11445
11641
|
}
|
|
@@ -11475,6 +11671,14 @@ git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler P
|
|
|
11475
11671
|
process.stdout.write(`attention: ${i.kind} \u2014 ${i.message} (since ${i.since})
|
|
11476
11672
|
`);
|
|
11477
11673
|
}
|
|
11674
|
+
if (host === "macos") {
|
|
11675
|
+
const foreign = describeForeignLaunchdUnits(CREW_HOME);
|
|
11676
|
+
if (foreign.length > 0) {
|
|
11677
|
+
process.stdout.write(`foreign launchd units (not this checkout's \u2014 \`crew install\` or \`crew uninstall --all\` clears them):
|
|
11678
|
+
${foreign.join("\n")}
|
|
11679
|
+
`);
|
|
11680
|
+
}
|
|
11681
|
+
}
|
|
11478
11682
|
const passengerRoutes = cfg.routes.filter((r) => r.enabled && r.hostPassengers);
|
|
11479
11683
|
if (passengerRoutes.length === 0) {
|
|
11480
11684
|
process.stdout.write("host passengers: off (no enabled route sets hostPassengers: true)\n");
|
package/package.json
CHANGED