@tablation/crew 0.7.0 → 0.8.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/dist/cli.js +160 -47
- package/package.json +1 -1
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";
|
|
@@ -1990,6 +1990,13 @@ function findOnPath(bin, searchPath) {
|
|
|
1990
1990
|
}
|
|
1991
1991
|
return void 0;
|
|
1992
1992
|
}
|
|
1993
|
+
var COMMON_DOCKER_DIRS = ["/usr/local/bin", "/opt/homebrew/bin", join5(homedir2(), ".docker", "bin")].join(":");
|
|
1994
|
+
function dockerPathProblem(ship, shellPath = process.env.PATH ?? "") {
|
|
1995
|
+
const schedulerPath = pathFor(ship);
|
|
1996
|
+
if (findOnPath("docker", schedulerPath)) return void 0;
|
|
1997
|
+
const inShell = findOnPath("docker", shellPath);
|
|
1998
|
+
return inShell ? `\`docker\` is not on the scheduler PATH (${schedulerPath}) but is at ${inShell} in your shell; add ${dirname3(inShell)} to ship.extraPath in crew.yaml and run \`crew install\`` : `\`docker\` is not on the scheduler PATH (${schedulerPath}) or your shell PATH; install Docker, then add its directory to ship.extraPath in crew.yaml and run \`crew install\``;
|
|
1999
|
+
}
|
|
1993
2000
|
function xmlEscape(s) {
|
|
1994
2001
|
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
1995
2002
|
}
|
|
@@ -5263,8 +5270,7 @@ async function runWizard(p, env) {
|
|
|
5263
5270
|
}
|
|
5264
5271
|
const nodeDir = dirname6(env.nodePath);
|
|
5265
5272
|
const pnpmPath = findOnPath2("pnpm", env.pathEnv, isExec);
|
|
5266
|
-
const
|
|
5267
|
-
if (extraPath) p.say(`Hooks will get ${extraPath} on their PATH (where node and pnpm live).`);
|
|
5273
|
+
const baseDirs = [nodeDir, pnpmPath ? dirname6(pnpmPath) : void 0];
|
|
5268
5274
|
p.say(
|
|
5269
5275
|
`
|
|
5270
5276
|
Host Passengers lets this machine run the agent sessions that other people's
|
|
@@ -5274,6 +5280,11 @@ Tablation workspaces ask for, in a container. More: ${HOST_PASSENGERS_DOCS_URL}`
|
|
|
5274
5280
|
if (hostPassengers && !env.dockerAvailable) {
|
|
5275
5281
|
p.say("Docker is not available here, so nothing will be hosted until it is. Your answer is recorded anyway.");
|
|
5276
5282
|
}
|
|
5283
|
+
const dockerPath = hostPassengers ? findOnPath2("docker", env.pathEnv, isExec) : void 0;
|
|
5284
|
+
const extraPath = deriveExtraPath([...baseDirs, dockerPath ? dirname6(dockerPath) : void 0]);
|
|
5285
|
+
if (extraPath) {
|
|
5286
|
+
p.say(`Hooks will get ${extraPath} on their PATH (where node, pnpm${dockerPath ? " and docker" : ""} live).`);
|
|
5287
|
+
}
|
|
5277
5288
|
return { name, platform: env.platform, agentBin, extraPath, hostPassengers };
|
|
5278
5289
|
}
|
|
5279
5290
|
function renderShipBlock(a) {
|
|
@@ -7657,6 +7668,72 @@ function resolveRepoProject(projects, opt, routeAreaId) {
|
|
|
7657
7668
|
);
|
|
7658
7669
|
}
|
|
7659
7670
|
|
|
7671
|
+
// src/claude-mcp.ts
|
|
7672
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
7673
|
+
import { readFileSync as readFileSync9 } from "fs";
|
|
7674
|
+
import { homedir as homedir4 } from "os";
|
|
7675
|
+
import { join as join13 } from "path";
|
|
7676
|
+
var MCP_SERVER_NAME = "tablation";
|
|
7677
|
+
var redact = (s, key) => key ? s.split(key).join("sk_***") : s;
|
|
7678
|
+
function mcpUrlFor(baseUrl) {
|
|
7679
|
+
return `${baseUrl.replace(/\/+$/, "")}/api/mcp`;
|
|
7680
|
+
}
|
|
7681
|
+
function runClaude(bin, args) {
|
|
7682
|
+
const r = spawnSync2(bin, args, { encoding: "utf8", timeout: 3e4 });
|
|
7683
|
+
const missing = r.error?.code === "ENOENT";
|
|
7684
|
+
return { status: r.status, missing, stderr: r.stderr ?? "" };
|
|
7685
|
+
}
|
|
7686
|
+
function userScopeEntry(home) {
|
|
7687
|
+
try {
|
|
7688
|
+
const j = JSON.parse(readFileSync9(join13(home, ".claude.json"), "utf8"));
|
|
7689
|
+
return j.mcpServers?.[MCP_SERVER_NAME];
|
|
7690
|
+
} catch {
|
|
7691
|
+
return void 0;
|
|
7692
|
+
}
|
|
7693
|
+
}
|
|
7694
|
+
function inspectClaudeMcp(o) {
|
|
7695
|
+
const got = runClaude(o.bin, ["mcp", "get", MCP_SERVER_NAME]);
|
|
7696
|
+
if (got.missing) return { state: "no-claude" };
|
|
7697
|
+
if (got.status !== 0) return { state: "missing" };
|
|
7698
|
+
const entry = userScopeEntry(o.home ?? homedir4());
|
|
7699
|
+
if (!entry) {
|
|
7700
|
+
return { state: "different", detail: `a "${MCP_SERVER_NAME}" server exists, but not in user scope (~/.claude.json) \u2014 a project or local registration` };
|
|
7701
|
+
}
|
|
7702
|
+
if (entry.url !== o.url) return { state: "different", detail: `it points at ${entry.url ?? "(no url)"}, not ${o.url}` };
|
|
7703
|
+
const auth = Object.entries(entry.headers ?? {}).find(([k]) => k.toLowerCase() === "authorization")?.[1];
|
|
7704
|
+
if (auth !== `Bearer ${o.key}`) return { state: "different", detail: `it is registered with a different key than this route's` };
|
|
7705
|
+
return { state: "registered", url: o.url };
|
|
7706
|
+
}
|
|
7707
|
+
function ensureClaudeMcp(o) {
|
|
7708
|
+
const manual = `claude mcp add --transport http ${MCP_SERVER_NAME} ${o.url} --header "Authorization: Bearer <key>" --scope user`;
|
|
7709
|
+
const st = inspectClaudeMcp(o);
|
|
7710
|
+
if (st.state === "no-claude") return `mcp: \`${o.bin}\` not found, so Claude Code was not configured. To do it by hand: ${manual}`;
|
|
7711
|
+
if (st.state === "registered") return `mcp: ${MCP_SERVER_NAME} already registered with Claude Code \u2192 ${st.url}`;
|
|
7712
|
+
if (st.state === "different" && !o.replace) {
|
|
7713
|
+
return `mcp: left Claude Code's "${MCP_SERVER_NAME}" server alone \u2014 ${st.detail}. Re-run with --mcp replace to overwrite it.`;
|
|
7714
|
+
}
|
|
7715
|
+
if (o.dryRun) return `(dry run) would register the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url}`;
|
|
7716
|
+
if (st.state === "different") {
|
|
7717
|
+
runClaude(o.bin, ["mcp", "remove", MCP_SERVER_NAME, "--scope", "user"]);
|
|
7718
|
+
}
|
|
7719
|
+
const add = runClaude(o.bin, [
|
|
7720
|
+
"mcp",
|
|
7721
|
+
"add",
|
|
7722
|
+
"--transport",
|
|
7723
|
+
"http",
|
|
7724
|
+
MCP_SERVER_NAME,
|
|
7725
|
+
o.url,
|
|
7726
|
+
"--header",
|
|
7727
|
+
`Authorization: Bearer ${o.key}`,
|
|
7728
|
+
"--scope",
|
|
7729
|
+
"user"
|
|
7730
|
+
]);
|
|
7731
|
+
if (add.status !== 0) {
|
|
7732
|
+
return `mcp: \`claude mcp add\` failed (${redact(add.stderr.trim(), o.key) || `exit ${add.status}`}). To do it by hand: ${manual}`;
|
|
7733
|
+
}
|
|
7734
|
+
return `mcp: registered the ${MCP_SERVER_NAME} MCP server with Claude Code \u2192 ${o.url} (user scope, ~/.claude.json)`;
|
|
7735
|
+
}
|
|
7736
|
+
|
|
7660
7737
|
// src/doctor-fix.ts
|
|
7661
7738
|
import { parseDocument as parseDocument2 } from "yaml";
|
|
7662
7739
|
var sound = (r) => r.hasRepos && r.problems.length === 0;
|
|
@@ -7808,13 +7885,13 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
7808
7885
|
|
|
7809
7886
|
// src/stream-sweep.ts
|
|
7810
7887
|
import { existsSync as existsSync15, readdirSync as readdirSync2, rmSync as rmSync2, statSync as statSync6 } from "fs";
|
|
7811
|
-
import { join as
|
|
7888
|
+
import { join as join14 } from "path";
|
|
7812
7889
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
7813
7890
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
7814
|
-
const dir =
|
|
7891
|
+
const dir = join14(stateDir, "streams");
|
|
7815
7892
|
if (!existsSync15(dir)) return [];
|
|
7816
7893
|
const cutoff = now - retentionDays * DAY_MS;
|
|
7817
|
-
return readdirSync2(dir).map((name) =>
|
|
7894
|
+
return readdirSync2(dir).map((name) => join14(dir, name)).filter((path) => {
|
|
7818
7895
|
try {
|
|
7819
7896
|
return statSync6(path).mtimeMs < cutoff;
|
|
7820
7897
|
} catch {
|
|
@@ -7841,18 +7918,18 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
7841
7918
|
}
|
|
7842
7919
|
|
|
7843
7920
|
// src/cli.ts
|
|
7844
|
-
import { readFileSync as
|
|
7921
|
+
import { readFileSync as readFileSync13, existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, copyFileSync, renameSync } from "fs";
|
|
7845
7922
|
import { createInterface } from "readline/promises";
|
|
7846
7923
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
7847
7924
|
|
|
7848
7925
|
// src/ssh-keys.ts
|
|
7849
7926
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
7850
|
-
import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as
|
|
7851
|
-
import { dirname as dirname8, join as
|
|
7927
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as readFileSync10, chmodSync } from "fs";
|
|
7928
|
+
import { dirname as dirname8, join as join15 } from "path";
|
|
7852
7929
|
var SshKeyError = class extends Error {
|
|
7853
7930
|
};
|
|
7854
7931
|
function shipSshKeyPathFor(stateDir) {
|
|
7855
|
-
return
|
|
7932
|
+
return join15(stateDir, "ssh", "id_ed25519");
|
|
7856
7933
|
}
|
|
7857
7934
|
function ensureShipSshKeypair(stateDir) {
|
|
7858
7935
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
@@ -7881,7 +7958,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
7881
7958
|
if (!existsSync16(publicKeyPath)) {
|
|
7882
7959
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
7883
7960
|
}
|
|
7884
|
-
const publicKey =
|
|
7961
|
+
const publicKey = readFileSync10(publicKeyPath, "utf8").trim();
|
|
7885
7962
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
7886
7963
|
}
|
|
7887
7964
|
function sshKeygenAvailable() {
|
|
@@ -8095,9 +8172,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
8095
8172
|
|
|
8096
8173
|
// src/tunnel.ts
|
|
8097
8174
|
import { spawn as spawn3 } from "child_process";
|
|
8098
|
-
import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as
|
|
8175
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync8, readFileSync as readFileSync11, writeFileSync as writeFileSync7, unlinkSync as unlinkSync3, readdirSync as readdirSync3 } from "fs";
|
|
8099
8176
|
import { randomInt } from "crypto";
|
|
8100
|
-
import { join as
|
|
8177
|
+
import { join as join16 } from "path";
|
|
8101
8178
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
8102
8179
|
var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
8103
8180
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -8164,20 +8241,20 @@ function startTunnel(opts) {
|
|
|
8164
8241
|
};
|
|
8165
8242
|
}
|
|
8166
8243
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
8167
|
-
return
|
|
8244
|
+
return join16(stateDir, "passengers", `${workspaceId}.json`);
|
|
8168
8245
|
}
|
|
8169
8246
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
8170
8247
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
8171
8248
|
if (!existsSync17(path)) return void 0;
|
|
8172
8249
|
try {
|
|
8173
|
-
return JSON.parse(
|
|
8250
|
+
return JSON.parse(readFileSync11(path, "utf8"));
|
|
8174
8251
|
} catch {
|
|
8175
8252
|
return void 0;
|
|
8176
8253
|
}
|
|
8177
8254
|
}
|
|
8178
8255
|
function writePersistedTunnel(stateDir, state) {
|
|
8179
8256
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
8180
|
-
mkdirSync8(
|
|
8257
|
+
mkdirSync8(join16(stateDir, "passengers"), { recursive: true });
|
|
8181
8258
|
writeFileSync7(path, `${JSON.stringify(state, null, 2)}
|
|
8182
8259
|
`);
|
|
8183
8260
|
}
|
|
@@ -8275,7 +8352,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
8275
8352
|
}
|
|
8276
8353
|
}
|
|
8277
8354
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
8278
|
-
const dir =
|
|
8355
|
+
const dir = join16(stateDir, "passengers");
|
|
8279
8356
|
if (!existsSync17(dir)) return [];
|
|
8280
8357
|
return readdirSync3(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
8281
8358
|
}
|
|
@@ -8397,8 +8474,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
8397
8474
|
import { createServer as createHttpServer } from "http";
|
|
8398
8475
|
import { spawn as spawn4 } from "child_process";
|
|
8399
8476
|
import { randomBytes } from "crypto";
|
|
8400
|
-
import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as
|
|
8401
|
-
import { join as
|
|
8477
|
+
import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as readFileSync12, writeFileSync as writeFileSync8, unlinkSync as unlinkSync4, readdirSync as readdirSync4 } from "fs";
|
|
8478
|
+
import { join as join17 } from "path";
|
|
8402
8479
|
|
|
8403
8480
|
// src/secret-compare.ts
|
|
8404
8481
|
import { timingSafeEqual } from "crypto";
|
|
@@ -8487,20 +8564,20 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
8487
8564
|
return child;
|
|
8488
8565
|
};
|
|
8489
8566
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
8490
|
-
return
|
|
8567
|
+
return join17(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
8491
8568
|
}
|
|
8492
8569
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
8493
8570
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
8494
8571
|
if (!existsSync18(path)) return void 0;
|
|
8495
8572
|
try {
|
|
8496
|
-
return JSON.parse(
|
|
8573
|
+
return JSON.parse(readFileSync12(path, "utf8"));
|
|
8497
8574
|
} catch {
|
|
8498
8575
|
return void 0;
|
|
8499
8576
|
}
|
|
8500
8577
|
}
|
|
8501
8578
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
8502
8579
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
8503
|
-
mkdirSync9(
|
|
8580
|
+
mkdirSync9(join17(stateDir, "passengers"), { recursive: true });
|
|
8504
8581
|
writeFileSync8(path, `${JSON.stringify(state, null, 2)}
|
|
8505
8582
|
`);
|
|
8506
8583
|
}
|
|
@@ -8521,7 +8598,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
8521
8598
|
}
|
|
8522
8599
|
}
|
|
8523
8600
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
8524
|
-
const dir =
|
|
8601
|
+
const dir = join17(stateDir, "passengers");
|
|
8525
8602
|
if (!existsSync18(dir)) return [];
|
|
8526
8603
|
const suffix = "-sync.json";
|
|
8527
8604
|
return readdirSync4(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
@@ -8580,7 +8657,7 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
8580
8657
|
|
|
8581
8658
|
// src/daemon.ts
|
|
8582
8659
|
import { readdirSync as readdirSync5, statSync as statSync7 } from "fs";
|
|
8583
|
-
import { join as
|
|
8660
|
+
import { join as join18 } from "path";
|
|
8584
8661
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
8585
8662
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
8586
8663
|
var defaultSleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
@@ -8619,7 +8696,7 @@ function latestMtime(path) {
|
|
|
8619
8696
|
if (!stat.isDirectory()) return 0;
|
|
8620
8697
|
let latest = stat.mtimeMs;
|
|
8621
8698
|
for (const entry of readdirSync5(path, { withFileTypes: true })) {
|
|
8622
|
-
latest = Math.max(latest, latestMtime(
|
|
8699
|
+
latest = Math.max(latest, latestMtime(join18(path, entry.name)));
|
|
8623
8700
|
}
|
|
8624
8701
|
return latest;
|
|
8625
8702
|
}
|
|
@@ -8995,7 +9072,7 @@ function usage() {
|
|
|
8995
9072
|
resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
|
|
8996
9073
|
crew log [route] tail the log
|
|
8997
9074
|
crew inbox [--member NAME] your tickets across every workspace (or a colleague's)
|
|
8998
|
-
crew connect resolve a workspace's ids into a crew.yaml block
|
|
9075
|
+
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)
|
|
8999
9076
|
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)
|
|
9000
9077
|
crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook)
|
|
9001
9078
|
crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
|
|
@@ -9176,8 +9253,9 @@ if (command === "passenger-sync-daemon") {
|
|
|
9176
9253
|
const passengerRoutes = cfg.routes.filter((r) => r.enabled && r.hostPassengers);
|
|
9177
9254
|
if (passengerRoutes.length === 0) return;
|
|
9178
9255
|
if (!dockerAvailable()) {
|
|
9256
|
+
const pathProblem = dockerPathProblem(cfg.ship, COMMON_DOCKER_DIRS);
|
|
9179
9257
|
emit.warn(
|
|
9180
|
-
|
|
9258
|
+
`Host Passengers is on for at least one route but ${pathProblem ?? "Docker is not available"} \u2014 skipping container sync this cycle`,
|
|
9181
9259
|
{ step: "passengers" }
|
|
9182
9260
|
);
|
|
9183
9261
|
return;
|
|
@@ -9302,7 +9380,12 @@ if (command === "passenger-sync-daemon") {
|
|
|
9302
9380
|
process.stderr.write("crew install: no scheduler support yet for Windows (Task Scheduler is planned, not built)\n");
|
|
9303
9381
|
process.exit(2);
|
|
9304
9382
|
}
|
|
9305
|
-
|
|
9383
|
+
const installPathProblem = cfg.routes.some((r) => r.enabled && r.hostPassengers) ? dockerPathProblem(cfg.ship) : void 0;
|
|
9384
|
+
if (installPathProblem) {
|
|
9385
|
+
process.stderr.write(`crew install: Host Passengers is on but ${installPathProblem}
|
|
9386
|
+
`);
|
|
9387
|
+
process.exitCode = 1;
|
|
9388
|
+
} else if (!dryRun && cfg.routes.some((r) => r.enabled && r.hostPassengers) && dockerAvailable()) {
|
|
9306
9389
|
const image = passengerImageRef(cfg);
|
|
9307
9390
|
if (imagePresent(image)) {
|
|
9308
9391
|
process.stdout.write(`Host Passengers image ${image} is already present
|
|
@@ -9333,14 +9416,14 @@ if (command === "passenger-sync-daemon") {
|
|
|
9333
9416
|
}
|
|
9334
9417
|
if (host === "linux" && hasSystemd) {
|
|
9335
9418
|
const label = labelFor(CREW_HOME, "run");
|
|
9336
|
-
const staleTimer =
|
|
9419
|
+
const staleTimer = join19(homedir5(), ".config", "systemd", "user", `${label}.timer`);
|
|
9337
9420
|
if (existsSync19(staleTimer)) {
|
|
9338
9421
|
process.stdout.write(
|
|
9339
9422
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
9340
9423
|
`
|
|
9341
9424
|
);
|
|
9342
9425
|
if (!dryRun) {
|
|
9343
|
-
|
|
9426
|
+
spawnSync3("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
|
|
9344
9427
|
try {
|
|
9345
9428
|
unlinkSync5(staleTimer);
|
|
9346
9429
|
} catch {
|
|
@@ -9378,7 +9461,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
9378
9461
|
async function doctorFix(o) {
|
|
9379
9462
|
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))));
|
|
9380
9463
|
const unitExists = (p) => existsSync19(p);
|
|
9381
|
-
const crontab = o.host === "linux" ?
|
|
9464
|
+
const crontab = o.host === "linux" ? spawnSync3("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
9382
9465
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
9383
9466
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
9384
9467
|
for (const n of plan.notes) process.stdout.write(`fix: ${n}
|
|
@@ -9394,7 +9477,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
9394
9477
|
for (const a of plan.actions) {
|
|
9395
9478
|
if (a.kind === "enable") {
|
|
9396
9479
|
if (prompter && !await prompter.confirm(`Route ${a.route} passes every check \u2014 set enabled: true in ${cfg.configFile}?`, false)) continue;
|
|
9397
|
-
writeFileSync9(cfg.configFile, planEnable(
|
|
9480
|
+
writeFileSync9(cfg.configFile, planEnable(readFileSync13(cfg.configFile, "utf8"), a.route));
|
|
9398
9481
|
enabledSound.add(a.route);
|
|
9399
9482
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
9400
9483
|
} else {
|
|
@@ -10104,7 +10187,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10104
10187
|
}
|
|
10105
10188
|
let failed = false;
|
|
10106
10189
|
for (const cmd2 of commands) {
|
|
10107
|
-
const res2 =
|
|
10190
|
+
const res2 = spawnSync3(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
10108
10191
|
if (res2.stdout) process.stdout.write(res2.stdout);
|
|
10109
10192
|
if (res2.stderr) process.stderr.write(res2.stderr);
|
|
10110
10193
|
if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
|
|
@@ -10122,7 +10205,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10122
10205
|
`);
|
|
10123
10206
|
break;
|
|
10124
10207
|
}
|
|
10125
|
-
const res =
|
|
10208
|
+
const res = spawnSync3(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
10126
10209
|
if (res.stdout) process.stdout.write(res.stdout);
|
|
10127
10210
|
if (res.stderr) process.stderr.write(res.stderr);
|
|
10128
10211
|
if (daemonSub !== "status" && (res.error || res.status !== 0)) {
|
|
@@ -10145,7 +10228,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10145
10228
|
process.exit(1);
|
|
10146
10229
|
}
|
|
10147
10230
|
requireArmed("run the daemon loop");
|
|
10148
|
-
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [
|
|
10231
|
+
const watchPaths = isCompiledBinary(import.meta.url) ? [process.execPath] : [join19(CREW_HOME, "src"), join19(CREW_HOME, "dist"), join19(CREW_HOME, "bin")];
|
|
10149
10232
|
const spawnBreaker = new SpawnBreaker();
|
|
10150
10233
|
const stopReason = await runDaemonLoop({
|
|
10151
10234
|
runPass: () => runOnePass({
|
|
@@ -10293,7 +10376,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10293
10376
|
`);
|
|
10294
10377
|
if (!dryRun) {
|
|
10295
10378
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
10296
|
-
const raw = existsSync19(resolvedPath) ? JSON.parse(
|
|
10379
|
+
const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync13(resolvedPath, "utf8")) : {};
|
|
10297
10380
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
10298
10381
|
raw.agentSkills = skillResult.agentSkills;
|
|
10299
10382
|
mkdirSync10(dirOf(resolvedPath), { recursive: true });
|
|
@@ -10342,7 +10425,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10342
10425
|
`);
|
|
10343
10426
|
if (!dryRun) {
|
|
10344
10427
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
10345
|
-
const raw = existsSync19(resolvedPath) ? JSON.parse(
|
|
10428
|
+
const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync13(resolvedPath, "utf8")) : {};
|
|
10346
10429
|
raw.agentSkills = result.agentSkills;
|
|
10347
10430
|
mkdirSync10(dirOf(resolvedPath), { recursive: true });
|
|
10348
10431
|
writeFileSync9(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -10358,14 +10441,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10358
10441
|
process.exit(2);
|
|
10359
10442
|
};
|
|
10360
10443
|
if (sub !== "add" && sub !== "list") usage2();
|
|
10361
|
-
const configText =
|
|
10444
|
+
const configText = readFileSync13(cfg.configFile, "utf8");
|
|
10362
10445
|
let routeName = positional[2];
|
|
10363
10446
|
let addPath = ".";
|
|
10364
10447
|
let addName;
|
|
10365
10448
|
if (sub === "list" && !routeName) usage2();
|
|
10366
10449
|
if (sub === "add") {
|
|
10367
10450
|
try {
|
|
10368
|
-
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync19(resolvePath(p.replace(/^~(?=\/|$)/,
|
|
10451
|
+
const a = parseAddArgs(positional.slice(2), routeNamesOf(configText), (p) => existsSync19(resolvePath(p.replace(/^~(?=\/|$)/, homedir5()))));
|
|
10369
10452
|
routeName = a.route;
|
|
10370
10453
|
addPath = a.path;
|
|
10371
10454
|
addName = a.name;
|
|
@@ -10382,7 +10465,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10382
10465
|
let resolvedRemotes;
|
|
10383
10466
|
if (existsSync19(resolvedFile)) {
|
|
10384
10467
|
try {
|
|
10385
|
-
resolvedRemotes = JSON.parse(
|
|
10468
|
+
resolvedRemotes = JSON.parse(readFileSync13(resolvedFile, "utf8"));
|
|
10386
10469
|
} catch {
|
|
10387
10470
|
}
|
|
10388
10471
|
}
|
|
@@ -10403,7 +10486,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10403
10486
|
}
|
|
10404
10487
|
break;
|
|
10405
10488
|
}
|
|
10406
|
-
const target = resolvePath(addPath.replace(/^~(?=\/|$)/,
|
|
10489
|
+
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir5()));
|
|
10407
10490
|
const top = existsSync19(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
10408
10491
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
10409
10492
|
const origin = originOf(top);
|
|
@@ -10453,7 +10536,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10453
10536
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
10454
10537
|
`);
|
|
10455
10538
|
if (existsSync19(resolvedFile)) {
|
|
10456
|
-
const raw = JSON.parse(
|
|
10539
|
+
const raw = JSON.parse(readFileSync13(resolvedFile, "utf8"));
|
|
10457
10540
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
10458
10541
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
10459
10542
|
writeFileSync9(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
@@ -10688,7 +10771,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
10688
10771
|
const hadPreviousFile = existsSync19(resolvedPath);
|
|
10689
10772
|
if (hadPreviousFile) {
|
|
10690
10773
|
try {
|
|
10691
|
-
previous = JSON.parse(
|
|
10774
|
+
previous = JSON.parse(readFileSync13(resolvedPath, "utf8"));
|
|
10692
10775
|
} catch {
|
|
10693
10776
|
}
|
|
10694
10777
|
}
|
|
@@ -10776,7 +10859,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
10776
10859
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
10777
10860
|
const wizard = firstRun;
|
|
10778
10861
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
10779
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
10862
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join19(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
10780
10863
|
const routeBlock = renderConnection(
|
|
10781
10864
|
found,
|
|
10782
10865
|
route2,
|
|
@@ -10796,6 +10879,11 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
10796
10879
|
`);
|
|
10797
10880
|
}
|
|
10798
10881
|
if (!wroteConfig) process.stdout.write(routeBlock);
|
|
10882
|
+
if (!flag("no-mcp") && apiKey) {
|
|
10883
|
+
const replace = value("mcp") === "replace";
|
|
10884
|
+
process.stderr.write(`${ensureClaudeMcp({ bin: cfg.ship.agent.bin, url: mcpUrlFor(baseUrl), key: apiKey, replace, dryRun })}
|
|
10885
|
+
`);
|
|
10886
|
+
}
|
|
10799
10887
|
if (found.provisioning.length) {
|
|
10800
10888
|
process.stderr.write(`
|
|
10801
10889
|
Provisioned for this machine (${ship.name}):
|
|
@@ -11214,7 +11302,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
11214
11302
|
}
|
|
11215
11303
|
case "log": {
|
|
11216
11304
|
try {
|
|
11217
|
-
const text =
|
|
11305
|
+
const text = readFileSync13(cfg.ship.logFile, "utf8").split("\n");
|
|
11218
11306
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
11219
11307
|
`);
|
|
11220
11308
|
} catch {
|
|
@@ -11423,6 +11511,17 @@ git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler P
|
|
|
11423
11511
|
if (!agentBinPath || !gitPath) {
|
|
11424
11512
|
process.stdout.write(
|
|
11425
11513
|
` scheduler PATH is ${schedulerPath} \u2014 add the directory holding the missing binary to ship.extraPath in crew.yaml, then re-run \`crew doctor\`.
|
|
11514
|
+
`
|
|
11515
|
+
);
|
|
11516
|
+
}
|
|
11517
|
+
if (route.apiKey) {
|
|
11518
|
+
const mcpUrl = mcpUrlFor(route.baseUrl);
|
|
11519
|
+
const m = inspectClaudeMcp({ bin: cfg.ship.agent.bin, url: mcpUrl, key: route.apiKey });
|
|
11520
|
+
process.stdout.write(
|
|
11521
|
+
m.state === "registered" ? `mcp: tablation registered (user scope) \u2192 ${m.url}
|
|
11522
|
+
` : m.state === "no-claude" ? `mcp: claude not found \u2014 Claude Code was not checked
|
|
11523
|
+
` : m.state === "different" ? `mcp: tablation registered but not for this route \u2014 ${m.detail}
|
|
11524
|
+
` : `mcp: not registered \u2014 run crew connect
|
|
11426
11525
|
`
|
|
11427
11526
|
);
|
|
11428
11527
|
}
|
|
@@ -11463,6 +11562,20 @@ git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler P
|
|
|
11463
11562
|
process.stdout.write("host passengers: off (no enabled route sets hostPassengers: true)\n");
|
|
11464
11563
|
} else {
|
|
11465
11564
|
const dockerOk = dockerAvailable();
|
|
11565
|
+
const dockerPathIssue = dockerPathProblem(cfg.ship);
|
|
11566
|
+
const dockerKey = "docker_missing";
|
|
11567
|
+
if (dockerPathIssue) {
|
|
11568
|
+
process.stdout.write(`docker on scheduler PATH: NOT FOUND \u2014 ${dockerPathIssue}
|
|
11569
|
+
`);
|
|
11570
|
+
await raiseShipAttention({ state, emit, route: passengerRoutes[0], ship: cfg.ship }, {
|
|
11571
|
+
kind: "docker_missing",
|
|
11572
|
+
key: dockerKey,
|
|
11573
|
+
message: `Host Passengers is on but ${dockerPathIssue}`,
|
|
11574
|
+
since: (/* @__PURE__ */ new Date()).toISOString()
|
|
11575
|
+
});
|
|
11576
|
+
} else {
|
|
11577
|
+
clearShipAttention(state, dockerKey);
|
|
11578
|
+
}
|
|
11466
11579
|
const keygenOk = sshKeygenAvailable();
|
|
11467
11580
|
const image = passengerImageRef(cfg);
|
|
11468
11581
|
const imageOk = dockerOk && imagePresent(image);
|
package/package.json
CHANGED