@tablation/crew 0.7.1 → 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 +125 -43
- 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";
|
|
@@ -7668,6 +7668,72 @@ function resolveRepoProject(projects, opt, routeAreaId) {
|
|
|
7668
7668
|
);
|
|
7669
7669
|
}
|
|
7670
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
|
+
|
|
7671
7737
|
// src/doctor-fix.ts
|
|
7672
7738
|
import { parseDocument as parseDocument2 } from "yaml";
|
|
7673
7739
|
var sound = (r) => r.hasRepos && r.problems.length === 0;
|
|
@@ -7819,13 +7885,13 @@ function applyRemoteBranchCleanup(cwd, remote, branches2, dryRun, log) {
|
|
|
7819
7885
|
|
|
7820
7886
|
// src/stream-sweep.ts
|
|
7821
7887
|
import { existsSync as existsSync15, readdirSync as readdirSync2, rmSync as rmSync2, statSync as statSync6 } from "fs";
|
|
7822
|
-
import { join as
|
|
7888
|
+
import { join as join14 } from "path";
|
|
7823
7889
|
var DAY_MS = 24 * 60 * 60 * 1e3;
|
|
7824
7890
|
function planStreamSweep(stateDir, retentionDays, now = Date.now()) {
|
|
7825
|
-
const dir =
|
|
7891
|
+
const dir = join14(stateDir, "streams");
|
|
7826
7892
|
if (!existsSync15(dir)) return [];
|
|
7827
7893
|
const cutoff = now - retentionDays * DAY_MS;
|
|
7828
|
-
return readdirSync2(dir).map((name) =>
|
|
7894
|
+
return readdirSync2(dir).map((name) => join14(dir, name)).filter((path) => {
|
|
7829
7895
|
try {
|
|
7830
7896
|
return statSync6(path).mtimeMs < cutoff;
|
|
7831
7897
|
} catch {
|
|
@@ -7852,18 +7918,18 @@ function applyStreamSweep(paths, dryRun, log) {
|
|
|
7852
7918
|
}
|
|
7853
7919
|
|
|
7854
7920
|
// src/cli.ts
|
|
7855
|
-
import { readFileSync as
|
|
7921
|
+
import { readFileSync as readFileSync13, existsSync as existsSync19, mkdirSync as mkdirSync10, writeFileSync as writeFileSync9, unlinkSync as unlinkSync5, copyFileSync, renameSync } from "fs";
|
|
7856
7922
|
import { createInterface } from "readline/promises";
|
|
7857
7923
|
import { dirname as dirOf, resolve as resolvePath } from "path";
|
|
7858
7924
|
|
|
7859
7925
|
// src/ssh-keys.ts
|
|
7860
7926
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
7861
|
-
import { existsSync as existsSync16, mkdirSync as mkdirSync7, readFileSync as
|
|
7862
|
-
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";
|
|
7863
7929
|
var SshKeyError = class extends Error {
|
|
7864
7930
|
};
|
|
7865
7931
|
function shipSshKeyPathFor(stateDir) {
|
|
7866
|
-
return
|
|
7932
|
+
return join15(stateDir, "ssh", "id_ed25519");
|
|
7867
7933
|
}
|
|
7868
7934
|
function ensureShipSshKeypair(stateDir) {
|
|
7869
7935
|
const privateKeyPath = shipSshKeyPathFor(stateDir);
|
|
@@ -7892,7 +7958,7 @@ function ensureShipSshKeypair(stateDir) {
|
|
|
7892
7958
|
if (!existsSync16(publicKeyPath)) {
|
|
7893
7959
|
throw new SshKeyError(`${privateKeyPath} exists but ${publicKeyPath} does not \u2014 regenerate or restore it`);
|
|
7894
7960
|
}
|
|
7895
|
-
const publicKey =
|
|
7961
|
+
const publicKey = readFileSync10(publicKeyPath, "utf8").trim();
|
|
7896
7962
|
return { privateKeyPath, publicKeyPath, publicKey };
|
|
7897
7963
|
}
|
|
7898
7964
|
function sshKeygenAvailable() {
|
|
@@ -8106,9 +8172,9 @@ function syncAllPassengerCheckouts(cfg) {
|
|
|
8106
8172
|
|
|
8107
8173
|
// src/tunnel.ts
|
|
8108
8174
|
import { spawn as spawn3 } from "child_process";
|
|
8109
|
-
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";
|
|
8110
8176
|
import { randomInt } from "crypto";
|
|
8111
|
-
import { join as
|
|
8177
|
+
import { join as join16 } from "path";
|
|
8112
8178
|
var DEFAULT_SPAWN_OPTIONS = { stdio: "ignore", detached: true };
|
|
8113
8179
|
var defaultSpawn = (cmd, args) => spawn3(cmd, args, DEFAULT_SPAWN_OPTIONS);
|
|
8114
8180
|
var SLUG_ALPHABET = "abcdefghijklmnopqrstuvwxyz0123456789";
|
|
@@ -8175,20 +8241,20 @@ function startTunnel(opts) {
|
|
|
8175
8241
|
};
|
|
8176
8242
|
}
|
|
8177
8243
|
function tunnelStatePath(stateDir, workspaceId) {
|
|
8178
|
-
return
|
|
8244
|
+
return join16(stateDir, "passengers", `${workspaceId}.json`);
|
|
8179
8245
|
}
|
|
8180
8246
|
function readPersistedTunnel(stateDir, workspaceId) {
|
|
8181
8247
|
const path = tunnelStatePath(stateDir, workspaceId);
|
|
8182
8248
|
if (!existsSync17(path)) return void 0;
|
|
8183
8249
|
try {
|
|
8184
|
-
return JSON.parse(
|
|
8250
|
+
return JSON.parse(readFileSync11(path, "utf8"));
|
|
8185
8251
|
} catch {
|
|
8186
8252
|
return void 0;
|
|
8187
8253
|
}
|
|
8188
8254
|
}
|
|
8189
8255
|
function writePersistedTunnel(stateDir, state) {
|
|
8190
8256
|
const path = tunnelStatePath(stateDir, state.workspaceId);
|
|
8191
|
-
mkdirSync8(
|
|
8257
|
+
mkdirSync8(join16(stateDir, "passengers"), { recursive: true });
|
|
8192
8258
|
writeFileSync7(path, `${JSON.stringify(state, null, 2)}
|
|
8193
8259
|
`);
|
|
8194
8260
|
}
|
|
@@ -8286,7 +8352,7 @@ function syncPassengerTunnels(plans, relayHost, relayPort, privateKeyPath, state
|
|
|
8286
8352
|
}
|
|
8287
8353
|
}
|
|
8288
8354
|
function readAllPersistedWorkspaceIds(stateDir) {
|
|
8289
|
-
const dir =
|
|
8355
|
+
const dir = join16(stateDir, "passengers");
|
|
8290
8356
|
if (!existsSync17(dir)) return [];
|
|
8291
8357
|
return readdirSync3(dir).filter((f) => f.endsWith(".json") && !f.endsWith("-sync.json")).map((f) => f.slice(0, -".json".length));
|
|
8292
8358
|
}
|
|
@@ -8408,8 +8474,8 @@ async function unregisterEndpoint(workspaceId, info, d) {
|
|
|
8408
8474
|
import { createServer as createHttpServer } from "http";
|
|
8409
8475
|
import { spawn as spawn4 } from "child_process";
|
|
8410
8476
|
import { randomBytes } from "crypto";
|
|
8411
|
-
import { existsSync as existsSync18, mkdirSync as mkdirSync9, readFileSync as
|
|
8412
|
-
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";
|
|
8413
8479
|
|
|
8414
8480
|
// src/secret-compare.ts
|
|
8415
8481
|
import { timingSafeEqual } from "crypto";
|
|
@@ -8498,20 +8564,20 @@ var defaultSpawn2 = (cmd, args, env) => {
|
|
|
8498
8564
|
return child;
|
|
8499
8565
|
};
|
|
8500
8566
|
function syncDaemonStatePath(stateDir, workspaceId) {
|
|
8501
|
-
return
|
|
8567
|
+
return join17(stateDir, "passengers", `${workspaceId}-sync.json`);
|
|
8502
8568
|
}
|
|
8503
8569
|
function readPersistedSyncDaemon(stateDir, workspaceId) {
|
|
8504
8570
|
const path = syncDaemonStatePath(stateDir, workspaceId);
|
|
8505
8571
|
if (!existsSync18(path)) return void 0;
|
|
8506
8572
|
try {
|
|
8507
|
-
return JSON.parse(
|
|
8573
|
+
return JSON.parse(readFileSync12(path, "utf8"));
|
|
8508
8574
|
} catch {
|
|
8509
8575
|
return void 0;
|
|
8510
8576
|
}
|
|
8511
8577
|
}
|
|
8512
8578
|
function writePersistedSyncDaemon(stateDir, state) {
|
|
8513
8579
|
const path = syncDaemonStatePath(stateDir, state.workspaceId);
|
|
8514
|
-
mkdirSync9(
|
|
8580
|
+
mkdirSync9(join17(stateDir, "passengers"), { recursive: true });
|
|
8515
8581
|
writeFileSync8(path, `${JSON.stringify(state, null, 2)}
|
|
8516
8582
|
`);
|
|
8517
8583
|
}
|
|
@@ -8532,7 +8598,7 @@ function waitForExit(pid, alive, timeoutMs = 5e3) {
|
|
|
8532
8598
|
}
|
|
8533
8599
|
}
|
|
8534
8600
|
function readAllPersistedSyncWorkspaceIds(stateDir) {
|
|
8535
|
-
const dir =
|
|
8601
|
+
const dir = join17(stateDir, "passengers");
|
|
8536
8602
|
if (!existsSync18(dir)) return [];
|
|
8537
8603
|
const suffix = "-sync.json";
|
|
8538
8604
|
return readdirSync4(dir).filter((f) => f.endsWith(suffix)).map((f) => f.slice(0, -suffix.length));
|
|
@@ -8591,7 +8657,7 @@ function syncPassengerSyncDaemons(plans, cfg, stateDir, deps = {}) {
|
|
|
8591
8657
|
|
|
8592
8658
|
// src/daemon.ts
|
|
8593
8659
|
import { readdirSync as readdirSync5, statSync as statSync7 } from "fs";
|
|
8594
|
-
import { join as
|
|
8660
|
+
import { join as join18 } from "path";
|
|
8595
8661
|
var DEFAULT_FLOOR_MS = 5e3;
|
|
8596
8662
|
var DEFAULT_CEILING_MS = INTERVAL_SECONDS * 1e3;
|
|
8597
8663
|
var defaultSleep = (ms) => new Promise((resolve5) => setTimeout(resolve5, ms));
|
|
@@ -8630,7 +8696,7 @@ function latestMtime(path) {
|
|
|
8630
8696
|
if (!stat.isDirectory()) return 0;
|
|
8631
8697
|
let latest = stat.mtimeMs;
|
|
8632
8698
|
for (const entry of readdirSync5(path, { withFileTypes: true })) {
|
|
8633
|
-
latest = Math.max(latest, latestMtime(
|
|
8699
|
+
latest = Math.max(latest, latestMtime(join18(path, entry.name)));
|
|
8634
8700
|
}
|
|
8635
8701
|
return latest;
|
|
8636
8702
|
}
|
|
@@ -9006,7 +9072,7 @@ function usage() {
|
|
|
9006
9072
|
resume with no argument clears only the whole-crew pause; a role or release pause needs its own resume
|
|
9007
9073
|
crew log [route] tail the log
|
|
9008
9074
|
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
|
|
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)
|
|
9010
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)
|
|
9011
9077
|
crew agents prompt R [route] print one persona's current prompt (e.g. R=pair, for a SessionStart hook)
|
|
9012
9078
|
crew skills sync [route] push only crew's skill files (e.g. grill-me) into the workspace Agent Skills table
|
|
@@ -9350,14 +9416,14 @@ if (command === "passenger-sync-daemon") {
|
|
|
9350
9416
|
}
|
|
9351
9417
|
if (host === "linux" && hasSystemd) {
|
|
9352
9418
|
const label = labelFor(CREW_HOME, "run");
|
|
9353
|
-
const staleTimer =
|
|
9419
|
+
const staleTimer = join19(homedir5(), ".config", "systemd", "user", `${label}.timer`);
|
|
9354
9420
|
if (existsSync19(staleTimer)) {
|
|
9355
9421
|
process.stdout.write(
|
|
9356
9422
|
`removing stale run timer from before the persistent-service cutover: ${staleTimer}${dryRun ? " (dry run)" : ""}
|
|
9357
9423
|
`
|
|
9358
9424
|
);
|
|
9359
9425
|
if (!dryRun) {
|
|
9360
|
-
|
|
9426
|
+
spawnSync3("systemctl", ["--user", "disable", "--now", `${label}.timer`], { cwd: CREW_HOME });
|
|
9361
9427
|
try {
|
|
9362
9428
|
unlinkSync5(staleTimer);
|
|
9363
9429
|
} catch {
|
|
@@ -9395,7 +9461,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
9395
9461
|
async function doctorFix(o) {
|
|
9396
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))));
|
|
9397
9463
|
const unitExists = (p) => existsSync19(p);
|
|
9398
|
-
const crontab = o.host === "linux" ?
|
|
9464
|
+
const crontab = o.host === "linux" ? spawnSync3("crontab", ["-l"], { encoding: "utf8" }).stdout ?? "" : "";
|
|
9399
9465
|
const schedulerAbsent = o.host !== "windows" ? !schedulerInstalled(planInstall(cfg.ship, CREW_HOME, o.host, detectSystemd(), "run"), unitExists, crontab) : true;
|
|
9400
9466
|
const plan = planFix({ routes, schedulerAbsent, host: o.host });
|
|
9401
9467
|
for (const n of plan.notes) process.stdout.write(`fix: ${n}
|
|
@@ -9411,7 +9477,7 @@ if (command === "passenger-sync-daemon") {
|
|
|
9411
9477
|
for (const a of plan.actions) {
|
|
9412
9478
|
if (a.kind === "enable") {
|
|
9413
9479
|
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(
|
|
9480
|
+
writeFileSync9(cfg.configFile, planEnable(readFileSync13(cfg.configFile, "utf8"), a.route));
|
|
9415
9481
|
enabledSound.add(a.route);
|
|
9416
9482
|
changed2.push(`enabled route ${a.route} in ${cfg.configFile}`);
|
|
9417
9483
|
} else {
|
|
@@ -10121,7 +10187,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10121
10187
|
}
|
|
10122
10188
|
let failed = false;
|
|
10123
10189
|
for (const cmd2 of commands) {
|
|
10124
|
-
const res2 =
|
|
10190
|
+
const res2 = spawnSync3(cmd2[0], cmd2.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
10125
10191
|
if (res2.stdout) process.stdout.write(res2.stdout);
|
|
10126
10192
|
if (res2.stderr) process.stderr.write(res2.stderr);
|
|
10127
10193
|
if (cmd2 === control.startCommand && (res2.error || res2.status !== 0)) {
|
|
@@ -10139,7 +10205,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10139
10205
|
`);
|
|
10140
10206
|
break;
|
|
10141
10207
|
}
|
|
10142
|
-
const res =
|
|
10208
|
+
const res = spawnSync3(cmd[0], cmd.slice(1), { cwd: CREW_HOME, encoding: "utf8" });
|
|
10143
10209
|
if (res.stdout) process.stdout.write(res.stdout);
|
|
10144
10210
|
if (res.stderr) process.stderr.write(res.stderr);
|
|
10145
10211
|
if (daemonSub !== "status" && (res.error || res.status !== 0)) {
|
|
@@ -10162,7 +10228,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10162
10228
|
process.exit(1);
|
|
10163
10229
|
}
|
|
10164
10230
|
requireArmed("run the daemon loop");
|
|
10165
|
-
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")];
|
|
10166
10232
|
const spawnBreaker = new SpawnBreaker();
|
|
10167
10233
|
const stopReason = await runDaemonLoop({
|
|
10168
10234
|
runPass: () => runOnePass({
|
|
@@ -10310,7 +10376,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10310
10376
|
`);
|
|
10311
10377
|
if (!dryRun) {
|
|
10312
10378
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
10313
|
-
const raw = existsSync19(resolvedPath) ? JSON.parse(
|
|
10379
|
+
const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync13(resolvedPath, "utf8")) : {};
|
|
10314
10380
|
raw.agentPersonas = personaResult.agentPersonas;
|
|
10315
10381
|
raw.agentSkills = skillResult.agentSkills;
|
|
10316
10382
|
mkdirSync10(dirOf(resolvedPath), { recursive: true });
|
|
@@ -10359,7 +10425,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10359
10425
|
`);
|
|
10360
10426
|
if (!dryRun) {
|
|
10361
10427
|
const resolvedPath = resolvedPathFor(cfg.ship.stateDir, target.route);
|
|
10362
|
-
const raw = existsSync19(resolvedPath) ? JSON.parse(
|
|
10428
|
+
const raw = existsSync19(resolvedPath) ? JSON.parse(readFileSync13(resolvedPath, "utf8")) : {};
|
|
10363
10429
|
raw.agentSkills = result.agentSkills;
|
|
10364
10430
|
mkdirSync10(dirOf(resolvedPath), { recursive: true });
|
|
10365
10431
|
writeFileSync9(resolvedPath, `${JSON.stringify(raw, null, 2)}
|
|
@@ -10375,14 +10441,14 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10375
10441
|
process.exit(2);
|
|
10376
10442
|
};
|
|
10377
10443
|
if (sub !== "add" && sub !== "list") usage2();
|
|
10378
|
-
const configText =
|
|
10444
|
+
const configText = readFileSync13(cfg.configFile, "utf8");
|
|
10379
10445
|
let routeName = positional[2];
|
|
10380
10446
|
let addPath = ".";
|
|
10381
10447
|
let addName;
|
|
10382
10448
|
if (sub === "list" && !routeName) usage2();
|
|
10383
10449
|
if (sub === "add") {
|
|
10384
10450
|
try {
|
|
10385
|
-
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()))));
|
|
10386
10452
|
routeName = a.route;
|
|
10387
10453
|
addPath = a.path;
|
|
10388
10454
|
addName = a.name;
|
|
@@ -10399,7 +10465,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10399
10465
|
let resolvedRemotes;
|
|
10400
10466
|
if (existsSync19(resolvedFile)) {
|
|
10401
10467
|
try {
|
|
10402
|
-
resolvedRemotes = JSON.parse(
|
|
10468
|
+
resolvedRemotes = JSON.parse(readFileSync13(resolvedFile, "utf8"));
|
|
10403
10469
|
} catch {
|
|
10404
10470
|
}
|
|
10405
10471
|
}
|
|
@@ -10420,7 +10486,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10420
10486
|
}
|
|
10421
10487
|
break;
|
|
10422
10488
|
}
|
|
10423
|
-
const target = resolvePath(addPath.replace(/^~(?=\/|$)/,
|
|
10489
|
+
const target = resolvePath(addPath.replace(/^~(?=\/|$)/, homedir5()));
|
|
10424
10490
|
const top = existsSync19(target) ? gitOk(target, ["rev-parse", "--show-toplevel"]) : null;
|
|
10425
10491
|
if (!top) throw new ReposError(`${target} is not a git checkout`);
|
|
10426
10492
|
const origin = originOf(top);
|
|
@@ -10470,7 +10536,7 @@ ${changed2.map((c) => ` - ${c}
|
|
|
10470
10536
|
process.stdout.write(`created Repos row ${String(rowPayload.name)} (${String(rowPayload.remote)}) ${created.id}
|
|
10471
10537
|
`);
|
|
10472
10538
|
if (existsSync19(resolvedFile)) {
|
|
10473
|
-
const raw = JSON.parse(
|
|
10539
|
+
const raw = JSON.parse(readFileSync13(resolvedFile, "utf8"));
|
|
10474
10540
|
raw.repoNames = { ...raw.repoNames, [created.id]: String(rowPayload.name) };
|
|
10475
10541
|
raw.repoRemotes = { ...raw.repoRemotes, [created.id]: String(rowPayload.remote) };
|
|
10476
10542
|
writeFileSync9(resolvedFile, `${JSON.stringify(raw, null, 2)}
|
|
@@ -10705,7 +10771,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
10705
10771
|
const hadPreviousFile = existsSync19(resolvedPath);
|
|
10706
10772
|
if (hadPreviousFile) {
|
|
10707
10773
|
try {
|
|
10708
|
-
previous = JSON.parse(
|
|
10774
|
+
previous = JSON.parse(readFileSync13(resolvedPath, "utf8"));
|
|
10709
10775
|
} catch {
|
|
10710
10776
|
}
|
|
10711
10777
|
}
|
|
@@ -10793,7 +10859,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
10793
10859
|
const keyPersisted = keychainBacked || apiKeyFile !== void 0;
|
|
10794
10860
|
const wizard = firstRun;
|
|
10795
10861
|
const writeFirstRun = wizard !== void 0 && !dryRun && keyPersisted;
|
|
10796
|
-
const routeDir = value("dir") ?? (writeFirstRun ?
|
|
10862
|
+
const routeDir = value("dir") ?? (writeFirstRun ? join19(DEFAULT_REPOS_BASE_PATH, found.workspaceSlug) : void 0);
|
|
10797
10863
|
const routeBlock = renderConnection(
|
|
10798
10864
|
found,
|
|
10799
10865
|
route2,
|
|
@@ -10813,6 +10879,11 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
10813
10879
|
`);
|
|
10814
10880
|
}
|
|
10815
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
|
+
}
|
|
10816
10887
|
if (found.provisioning.length) {
|
|
10817
10888
|
process.stderr.write(`
|
|
10818
10889
|
Provisioned for this machine (${ship.name}):
|
|
@@ -11231,7 +11302,7 @@ Signed in as ${result.identity.email} \u2014 minted an API key in workspace "${r
|
|
|
11231
11302
|
}
|
|
11232
11303
|
case "log": {
|
|
11233
11304
|
try {
|
|
11234
|
-
const text =
|
|
11305
|
+
const text = readFileSync13(cfg.ship.logFile, "utf8").split("\n");
|
|
11235
11306
|
process.stdout.write(`${text.slice(-(Number(value("tail")) || 40)).join("\n")}
|
|
11236
11307
|
`);
|
|
11237
11308
|
} catch {
|
|
@@ -11440,6 +11511,17 @@ git: ${gitPath ? `OK (${gitPath})` : "NOT FOUND on the scheduler P
|
|
|
11440
11511
|
if (!agentBinPath || !gitPath) {
|
|
11441
11512
|
process.stdout.write(
|
|
11442
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
|
|
11443
11525
|
`
|
|
11444
11526
|
);
|
|
11445
11527
|
}
|
package/package.json
CHANGED