@sechroom/cli 2026.8.1 → 2026.8.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1347 -229
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -4366,9 +4366,19 @@ import { dirname as dirname5, join as join7 } from "path";
|
|
|
4366
4366
|
function claudeDesktopConfigPath(home) {
|
|
4367
4367
|
switch (process.platform) {
|
|
4368
4368
|
case "darwin":
|
|
4369
|
-
return join7(
|
|
4369
|
+
return join7(
|
|
4370
|
+
home,
|
|
4371
|
+
"Library",
|
|
4372
|
+
"Application Support",
|
|
4373
|
+
"Claude",
|
|
4374
|
+
"claude_desktop_config.json"
|
|
4375
|
+
);
|
|
4370
4376
|
case "win32":
|
|
4371
|
-
return join7(
|
|
4377
|
+
return join7(
|
|
4378
|
+
process.env.APPDATA ?? join7(home, "AppData", "Roaming"),
|
|
4379
|
+
"Claude",
|
|
4380
|
+
"claude_desktop_config.json"
|
|
4381
|
+
);
|
|
4372
4382
|
default:
|
|
4373
4383
|
return join7(home, ".config", "Claude", "claude_desktop_config.json");
|
|
4374
4384
|
}
|
|
@@ -4376,30 +4386,53 @@ function claudeDesktopConfigPath(home) {
|
|
|
4376
4386
|
function clientTargets(cwd, opts = {}) {
|
|
4377
4387
|
const home = homedir3();
|
|
4378
4388
|
const claudeDir = opts.claudeDir ?? join7(home, ".claude");
|
|
4379
|
-
const codexHome = opts.codexHome
|
|
4389
|
+
const codexHome = opts.codexHome === void 0 ? join7(home, ".codex") : opts.codexHome;
|
|
4380
4390
|
return {
|
|
4381
4391
|
"claude-code": {
|
|
4382
4392
|
key: "claude-code",
|
|
4383
4393
|
label: "Claude Code",
|
|
4384
|
-
mcp: {
|
|
4394
|
+
mcp: {
|
|
4395
|
+
surfaceKey: "claude-code",
|
|
4396
|
+
sectionType: SectionType.McpConfig,
|
|
4397
|
+
path: join7(cwd, ".mcp.json"),
|
|
4398
|
+
format: "json"
|
|
4399
|
+
},
|
|
4385
4400
|
instruction: { surfaceKey: "claude-code", path: join7(cwd, "CLAUDE.md") }
|
|
4386
4401
|
},
|
|
4387
4402
|
"claude-desktop": {
|
|
4388
4403
|
key: "claude-desktop",
|
|
4389
4404
|
label: "Claude Desktop",
|
|
4390
|
-
mcp: {
|
|
4391
|
-
|
|
4405
|
+
mcp: {
|
|
4406
|
+
surfaceKey: "claude-desktop",
|
|
4407
|
+
sectionType: SectionType.McpConfig,
|
|
4408
|
+
path: claudeDesktopConfigPath(home),
|
|
4409
|
+
format: "json"
|
|
4410
|
+
},
|
|
4411
|
+
instruction: {
|
|
4412
|
+
surfaceKey: "claude-desktop",
|
|
4413
|
+
path: join7(claudeDir, "CLAUDE.md")
|
|
4414
|
+
}
|
|
4392
4415
|
},
|
|
4393
4416
|
codex: {
|
|
4394
4417
|
key: "codex",
|
|
4395
4418
|
label: "Codex CLI",
|
|
4396
|
-
mcp:
|
|
4419
|
+
mcp: codexHome ? {
|
|
4420
|
+
surfaceKey: "chatgpt",
|
|
4421
|
+
sectionType: SectionType.McpConfigToml,
|
|
4422
|
+
path: join7(codexHome, "config.toml"),
|
|
4423
|
+
format: "toml"
|
|
4424
|
+
} : null,
|
|
4397
4425
|
instruction: { surfaceKey: "chatgpt", path: join7(cwd, "AGENTS.md") }
|
|
4398
4426
|
},
|
|
4399
4427
|
cursor: {
|
|
4400
4428
|
key: "cursor",
|
|
4401
4429
|
label: "Cursor",
|
|
4402
|
-
mcp: {
|
|
4430
|
+
mcp: {
|
|
4431
|
+
surfaceKey: "claude-code",
|
|
4432
|
+
sectionType: SectionType.McpConfig,
|
|
4433
|
+
path: join7(cwd, ".cursor", "mcp.json"),
|
|
4434
|
+
format: "json"
|
|
4435
|
+
},
|
|
4403
4436
|
instruction: { surfaceKey: "chatgpt", path: join7(cwd, "AGENTS.md") }
|
|
4404
4437
|
},
|
|
4405
4438
|
antigravity: {
|
|
@@ -4411,20 +4444,34 @@ function clientTargets(cwd, opts = {}) {
|
|
|
4411
4444
|
// `type` — comes from the `antigravity` server surface, so we don't
|
|
4412
4445
|
// hardcode it here. Instructions go in the project `AGENTS.md`
|
|
4413
4446
|
// (cross-tool, shared with Codex/Cursor).
|
|
4414
|
-
mcp: {
|
|
4447
|
+
mcp: {
|
|
4448
|
+
surfaceKey: "antigravity",
|
|
4449
|
+
sectionType: SectionType.McpConfig,
|
|
4450
|
+
path: join7(home, ".gemini", "config", "mcp_config.json"),
|
|
4451
|
+
format: "json"
|
|
4452
|
+
},
|
|
4415
4453
|
instruction: { surfaceKey: "antigravity", path: join7(cwd, "AGENTS.md") }
|
|
4416
4454
|
}
|
|
4417
4455
|
};
|
|
4418
4456
|
}
|
|
4419
|
-
var ALL_CLIENT_KEYS = [
|
|
4457
|
+
var ALL_CLIENT_KEYS = [
|
|
4458
|
+
"claude-code",
|
|
4459
|
+
"claude-desktop",
|
|
4460
|
+
"codex",
|
|
4461
|
+
"cursor",
|
|
4462
|
+
"antigravity"
|
|
4463
|
+
];
|
|
4420
4464
|
var DEFAULT_CLIENT_KEY = "claude-code";
|
|
4421
4465
|
function detectInstalledClients(cwd) {
|
|
4422
4466
|
const home = homedir3();
|
|
4423
4467
|
const detected = [];
|
|
4424
|
-
if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir)))
|
|
4425
|
-
|
|
4468
|
+
if (resolveClaudeTargets({}).some((t) => existsSync5(t.dir)))
|
|
4469
|
+
detected.push("claude-code");
|
|
4470
|
+
if (existsSync5(dirname5(claudeDesktopConfigPath(home))))
|
|
4471
|
+
detected.push("claude-desktop");
|
|
4426
4472
|
if (resolveCodexHomes({}).some((d) => existsSync5(d))) detected.push("codex");
|
|
4427
|
-
if (existsSync5(join7(home, ".cursor")) || existsSync5(join7(cwd, ".cursor")))
|
|
4473
|
+
if (existsSync5(join7(home, ".cursor")) || existsSync5(join7(cwd, ".cursor")))
|
|
4474
|
+
detected.push("cursor");
|
|
4428
4475
|
if (existsSync5(join7(home, ".gemini"))) detected.push("antigravity");
|
|
4429
4476
|
return detected;
|
|
4430
4477
|
}
|
|
@@ -8975,7 +9022,7 @@ function herdrRequest(socketPath, method, params, timeoutMs = 1e4) {
|
|
|
8975
9022
|
});
|
|
8976
9023
|
}
|
|
8977
9024
|
function herdrStream(socketPath, subscriptions, onEvent, options = {}) {
|
|
8978
|
-
const { signal, onWarning, subscribeTimeoutMs = 1e4 } = options;
|
|
9025
|
+
const { signal, onWarning, onReady, subscribeTimeoutMs = 1e4 } = options;
|
|
8979
9026
|
return new Promise((resolvePromise, rejectPromise) => {
|
|
8980
9027
|
if (signal?.aborted) {
|
|
8981
9028
|
resolvePromise();
|
|
@@ -9017,6 +9064,7 @@ function herdrStream(socketPath, subscriptions, onEvent, options = {}) {
|
|
|
9017
9064
|
if (!acked) {
|
|
9018
9065
|
acked = true;
|
|
9019
9066
|
clearTimeout(ackTimer);
|
|
9067
|
+
onReady?.();
|
|
9020
9068
|
}
|
|
9021
9069
|
const classified = classifyStreamLine(line, id);
|
|
9022
9070
|
if (classified.kind === "ack") continue;
|
|
@@ -9191,9 +9239,12 @@ function indexAgents(agents) {
|
|
|
9191
9239
|
return new Map(agents.map((agent) => [agent.pane_id, agent]));
|
|
9192
9240
|
}
|
|
9193
9241
|
function buildWatchSubscriptions(agents) {
|
|
9194
|
-
return agents.
|
|
9195
|
-
|
|
9196
|
-
|
|
9242
|
+
return buildPaneSubscriptions(agents.map((agent) => agent.pane_id));
|
|
9243
|
+
}
|
|
9244
|
+
function buildPaneSubscriptions(paneIds) {
|
|
9245
|
+
return [...new Set(paneIds)].flatMap((pane_id) => [
|
|
9246
|
+
{ type: "pane.agent_status_changed", pane_id },
|
|
9247
|
+
{ type: "pane.exited", pane_id }
|
|
9197
9248
|
]);
|
|
9198
9249
|
}
|
|
9199
9250
|
function normalizeWatchEvent(raw) {
|
|
@@ -9385,6 +9436,7 @@ async function runWait(ports, targets, options) {
|
|
|
9385
9436
|
const remaining = new Set(targetIds);
|
|
9386
9437
|
let seed = /* @__PURE__ */ new Map();
|
|
9387
9438
|
let attempt = 0;
|
|
9439
|
+
const subscriptions = buildPaneSubscriptions(targetIds);
|
|
9388
9440
|
const settle = (agent, paneId, status) => {
|
|
9389
9441
|
if (!remaining.has(paneId)) return;
|
|
9390
9442
|
const record = {
|
|
@@ -9403,67 +9455,124 @@ async function runWait(ports, targets, options) {
|
|
|
9403
9455
|
try {
|
|
9404
9456
|
while (!controller.signal.aborted) {
|
|
9405
9457
|
try {
|
|
9406
|
-
const listed = await raceAbort(
|
|
9407
|
-
ports.request("agent.list", {})
|
|
9408
|
-
);
|
|
9409
|
-
if (listed === aborted) break;
|
|
9410
|
-
const current = listed.agents ?? [];
|
|
9411
|
-
const fresh = indexAgents(current);
|
|
9412
|
-
for (const paneId of [...remaining]) {
|
|
9413
|
-
const currentAgent = fresh.get(paneId);
|
|
9414
|
-
if (currentAgent === void 0) {
|
|
9415
|
-
settle(seed.get(paneId), paneId, "exited");
|
|
9416
|
-
continue;
|
|
9417
|
-
}
|
|
9418
|
-
const status = currentAgent.agent_status?.trim().toLowerCase();
|
|
9419
|
-
if (status !== void 0 && statuses.has(status)) {
|
|
9420
|
-
settle(currentAgent, paneId, currentAgent.agent_status ?? status);
|
|
9421
|
-
}
|
|
9422
|
-
}
|
|
9423
|
-
seed = fresh;
|
|
9424
|
-
if (completed || remaining.size === 0) return;
|
|
9425
|
-
const subscriptions = buildWatchSubscriptions(
|
|
9426
|
-
current.filter((agent) => remaining.has(agent.pane_id))
|
|
9427
|
-
);
|
|
9428
9458
|
let carriedTraffic = false;
|
|
9429
9459
|
let relistNeeded = false;
|
|
9460
|
+
let reconciled = false;
|
|
9461
|
+
const bufferedEvents = [];
|
|
9430
9462
|
const startedAt = now();
|
|
9431
9463
|
const cycle = new AbortController();
|
|
9432
9464
|
const stopCycle = () => cycle.abort();
|
|
9433
9465
|
controller.signal.addEventListener("abort", stopCycle, { once: true });
|
|
9466
|
+
let streamReady = false;
|
|
9467
|
+
let streamFailed = false;
|
|
9468
|
+
let streamFailure;
|
|
9469
|
+
let resolveReady;
|
|
9470
|
+
let rejectReady;
|
|
9471
|
+
const ready = new Promise((resolve9, reject) => {
|
|
9472
|
+
resolveReady = resolve9;
|
|
9473
|
+
rejectReady = reject;
|
|
9474
|
+
});
|
|
9475
|
+
const handleEvent = (event) => {
|
|
9476
|
+
if (!remaining.has(event.paneId)) return;
|
|
9477
|
+
const known = seed.get(event.paneId);
|
|
9478
|
+
if (event.type === "pane.exited") {
|
|
9479
|
+
settle(known, event.paneId, "exited");
|
|
9480
|
+
return;
|
|
9481
|
+
}
|
|
9482
|
+
const status = event.to?.trim().toLowerCase();
|
|
9483
|
+
if (known && event.to) known.agent_status = event.to;
|
|
9484
|
+
if (status !== void 0 && statuses.has(status)) {
|
|
9485
|
+
settle(known, event.paneId, event.to ?? status);
|
|
9486
|
+
}
|
|
9487
|
+
};
|
|
9488
|
+
let streamFinished;
|
|
9434
9489
|
try {
|
|
9435
|
-
const
|
|
9436
|
-
|
|
9437
|
-
|
|
9438
|
-
|
|
9439
|
-
|
|
9440
|
-
|
|
9441
|
-
|
|
9442
|
-
|
|
9443
|
-
|
|
9444
|
-
|
|
9445
|
-
|
|
9446
|
-
|
|
9447
|
-
|
|
9448
|
-
|
|
9449
|
-
|
|
9450
|
-
|
|
9451
|
-
|
|
9452
|
-
|
|
9453
|
-
|
|
9454
|
-
|
|
9455
|
-
|
|
9456
|
-
|
|
9490
|
+
const stream = ports.stream(
|
|
9491
|
+
subscriptions,
|
|
9492
|
+
(raw) => {
|
|
9493
|
+
carriedTraffic = true;
|
|
9494
|
+
if (!streamReady) {
|
|
9495
|
+
streamReady = true;
|
|
9496
|
+
resolveReady();
|
|
9497
|
+
}
|
|
9498
|
+
const event = normalizeWatchEvent(raw);
|
|
9499
|
+
if (event === null || needsRelistFallback(event)) {
|
|
9500
|
+
relistNeeded = true;
|
|
9501
|
+
cycle.abort();
|
|
9502
|
+
return;
|
|
9503
|
+
}
|
|
9504
|
+
if (!reconciled) {
|
|
9505
|
+
bufferedEvents.push(event);
|
|
9506
|
+
return;
|
|
9507
|
+
}
|
|
9508
|
+
handleEvent(event);
|
|
9509
|
+
},
|
|
9510
|
+
{
|
|
9511
|
+
signal: cycle.signal,
|
|
9512
|
+
onReady: () => {
|
|
9513
|
+
streamReady = true;
|
|
9514
|
+
resolveReady();
|
|
9457
9515
|
},
|
|
9458
|
-
{
|
|
9459
|
-
|
|
9460
|
-
|
|
9516
|
+
onWarning: (message) => warn2(`herdr wait: ${message}`)
|
|
9517
|
+
}
|
|
9518
|
+
);
|
|
9519
|
+
streamFinished = stream.then(
|
|
9520
|
+
() => {
|
|
9521
|
+
if (!streamReady) {
|
|
9522
|
+
rejectReady(
|
|
9523
|
+
new Error("Herdr subscription closed before it was ready")
|
|
9524
|
+
);
|
|
9461
9525
|
}
|
|
9462
|
-
|
|
9526
|
+
},
|
|
9527
|
+
(error) => {
|
|
9528
|
+
streamFailed = true;
|
|
9529
|
+
streamFailure = error;
|
|
9530
|
+
if (!streamReady) rejectReady(error);
|
|
9531
|
+
}
|
|
9532
|
+
);
|
|
9533
|
+
} catch (error) {
|
|
9534
|
+
streamFailed = true;
|
|
9535
|
+
streamFailure = error;
|
|
9536
|
+
rejectReady(error);
|
|
9537
|
+
streamFinished = Promise.resolve();
|
|
9538
|
+
}
|
|
9539
|
+
try {
|
|
9540
|
+
const readyResult = await raceAbort(ready);
|
|
9541
|
+
if (readyResult === aborted) break;
|
|
9542
|
+
const listed = await raceAbort(
|
|
9543
|
+
ports.request("agent.list", {})
|
|
9463
9544
|
);
|
|
9545
|
+
if (listed === aborted) break;
|
|
9546
|
+
const current = listed.agents ?? [];
|
|
9547
|
+
const fresh = indexAgents(current);
|
|
9548
|
+
for (const paneId of [...remaining]) {
|
|
9549
|
+
const currentAgent = fresh.get(paneId);
|
|
9550
|
+
if (currentAgent === void 0) {
|
|
9551
|
+
settle(seed.get(paneId), paneId, "exited");
|
|
9552
|
+
}
|
|
9553
|
+
}
|
|
9554
|
+
seed = fresh;
|
|
9555
|
+
reconciled = true;
|
|
9556
|
+
for (const paneId of [...remaining]) {
|
|
9557
|
+
const currentAgent = fresh.get(paneId);
|
|
9558
|
+
if (currentAgent === void 0) continue;
|
|
9559
|
+
for (const event of bufferedEvents) {
|
|
9560
|
+
if (event.paneId === paneId) handleEvent(event);
|
|
9561
|
+
}
|
|
9562
|
+
if (!remaining.has(paneId)) continue;
|
|
9563
|
+
const status = currentAgent.agent_status?.trim().toLowerCase();
|
|
9564
|
+
if (status !== void 0 && statuses.has(status)) {
|
|
9565
|
+
settle(currentAgent, paneId, currentAgent.agent_status ?? status);
|
|
9566
|
+
}
|
|
9567
|
+
}
|
|
9568
|
+
if (completed || remaining.size === 0) return;
|
|
9569
|
+
if (relistNeeded) continue;
|
|
9570
|
+
const streamed = await raceAbort(streamFinished);
|
|
9464
9571
|
if (streamed === aborted) break;
|
|
9572
|
+
if (streamFailed) throw streamFailure;
|
|
9465
9573
|
} finally {
|
|
9466
9574
|
controller.signal.removeEventListener("abort", stopCycle);
|
|
9575
|
+
cycle.abort();
|
|
9467
9576
|
}
|
|
9468
9577
|
if (completed) return;
|
|
9469
9578
|
if (relistNeeded) continue;
|
|
@@ -10055,6 +10164,41 @@ function resolveCreateBody(textOpt, fileOpt) {
|
|
|
10055
10164
|
);
|
|
10056
10165
|
return { text: text2, defaultTitle };
|
|
10057
10166
|
}
|
|
10167
|
+
async function fetchAllMemoryRelationships(cfg, memoryId) {
|
|
10168
|
+
const pages = [];
|
|
10169
|
+
let page = 1;
|
|
10170
|
+
while (true) {
|
|
10171
|
+
const current = await runApi(
|
|
10172
|
+
`Fetching relationships (page ${page})`,
|
|
10173
|
+
async () => {
|
|
10174
|
+
const client = await makeClient(cfg);
|
|
10175
|
+
return client.GET("/memories/{memoryId}/relationships", {
|
|
10176
|
+
params: { path: { memoryId }, query: { page } }
|
|
10177
|
+
});
|
|
10178
|
+
}
|
|
10179
|
+
);
|
|
10180
|
+
pages.push(current);
|
|
10181
|
+
const hasNextPage = current.hasNextPage ?? (current.pageCount !== void 0 ? current.page < current.pageCount : current.page * current.pageSize < current.count);
|
|
10182
|
+
if (!hasNextPage) break;
|
|
10183
|
+
page = current.page + 1;
|
|
10184
|
+
}
|
|
10185
|
+
const first = pages[0];
|
|
10186
|
+
if (!first || pages.length === 1) return first;
|
|
10187
|
+
const items = pages.flatMap((current) => current.items);
|
|
10188
|
+
return {
|
|
10189
|
+
...first,
|
|
10190
|
+
items,
|
|
10191
|
+
page: 1,
|
|
10192
|
+
pageSize: items.length,
|
|
10193
|
+
pageCount: 1,
|
|
10194
|
+
hasPreviousPage: false,
|
|
10195
|
+
hasNextPage: false,
|
|
10196
|
+
isFirstPage: true,
|
|
10197
|
+
isLastPage: true,
|
|
10198
|
+
firstItemOnPage: 1,
|
|
10199
|
+
lastItemOnPage: items.length
|
|
10200
|
+
};
|
|
10201
|
+
}
|
|
10058
10202
|
function registerMemory(program2) {
|
|
10059
10203
|
const memory = program2.command("memory").description("Create, read, and search memories");
|
|
10060
10204
|
memory.addHelpText(
|
|
@@ -10309,7 +10453,16 @@ ${plan.rows.map(
|
|
|
10309
10453
|
false
|
|
10310
10454
|
);
|
|
10311
10455
|
});
|
|
10312
|
-
memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").
|
|
10456
|
+
const get = memory.command("get <memoryId>").description("Fetch a memory by id (GET /memories/{memoryId})").option(
|
|
10457
|
+
"--with-relationships",
|
|
10458
|
+
"Include the memory's relationships in the response",
|
|
10459
|
+
false
|
|
10460
|
+
).addHelpText(
|
|
10461
|
+
"after",
|
|
10462
|
+
`
|
|
10463
|
+
Note: CLI memory get omits relationships by default; MCP get_memory includes them. Use --with-relationships to read all relationship pages, or relationship list <memoryId> to inspect edges.`
|
|
10464
|
+
);
|
|
10465
|
+
get.action(async (memoryId, opts, cmd) => {
|
|
10313
10466
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
10314
10467
|
const data = await runApi("Fetching memory", async () => {
|
|
10315
10468
|
const client = await makeClient(cfg);
|
|
@@ -10317,6 +10470,11 @@ ${plan.rows.map(
|
|
|
10317
10470
|
params: { path: { memoryId } }
|
|
10318
10471
|
});
|
|
10319
10472
|
});
|
|
10473
|
+
if (opts.withRelationships) {
|
|
10474
|
+
const relationships = await fetchAllMemoryRelationships(cfg, memoryId);
|
|
10475
|
+
emit({ ...data, relationships }, cmd.optsWithGlobals().json);
|
|
10476
|
+
return;
|
|
10477
|
+
}
|
|
10320
10478
|
emit(data, cmd.optsWithGlobals().json);
|
|
10321
10479
|
});
|
|
10322
10480
|
memory.command("search <query>").description(
|
|
@@ -10664,8 +10822,18 @@ ${plan.rows.map(
|
|
|
10664
10822
|
}
|
|
10665
10823
|
|
|
10666
10824
|
// src/setup/apply.ts
|
|
10667
|
-
import { createHash as createHash5 } from "crypto";
|
|
10668
|
-
import {
|
|
10825
|
+
import { createHash as createHash5, randomUUID as randomUUID3 } from "crypto";
|
|
10826
|
+
import {
|
|
10827
|
+
chmodSync as chmodSync2,
|
|
10828
|
+
copyFileSync,
|
|
10829
|
+
existsSync as existsSync13,
|
|
10830
|
+
mkdirSync as mkdirSync16,
|
|
10831
|
+
readFileSync as readFileSync16,
|
|
10832
|
+
renameSync as renameSync4,
|
|
10833
|
+
rmSync as rmSync7,
|
|
10834
|
+
statSync as statSync5,
|
|
10835
|
+
writeFileSync as writeFileSync15
|
|
10836
|
+
} from "fs";
|
|
10669
10837
|
import { dirname as dirname15 } from "path";
|
|
10670
10838
|
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
10671
10839
|
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
@@ -10711,11 +10879,17 @@ function parseManagedBlock(content, block) {
|
|
|
10711
10879
|
const keyed = content.match(keyedBlockRe(block));
|
|
10712
10880
|
if (keyed) {
|
|
10713
10881
|
const attrs = parseAttrs(keyed[0].slice(0, keyed[0].indexOf("\n")));
|
|
10714
|
-
return {
|
|
10882
|
+
return {
|
|
10883
|
+
block,
|
|
10884
|
+
source: attrs.source ?? null,
|
|
10885
|
+
sha256: attrs.sha256 ?? null,
|
|
10886
|
+
body: innerBody(keyed[0])
|
|
10887
|
+
};
|
|
10715
10888
|
}
|
|
10716
10889
|
if (block === "role-template") {
|
|
10717
10890
|
const legacy = content.match(legacyBlockRe());
|
|
10718
|
-
if (legacy)
|
|
10891
|
+
if (legacy)
|
|
10892
|
+
return { block, source: null, sha256: null, body: innerBody(legacy[0]) };
|
|
10719
10893
|
}
|
|
10720
10894
|
return null;
|
|
10721
10895
|
}
|
|
@@ -10737,33 +10911,446 @@ function mergeMcpJson(path, snippet, dryRun) {
|
|
|
10737
10911
|
try {
|
|
10738
10912
|
current = JSON.parse(readFileSync16(path, "utf8"));
|
|
10739
10913
|
} catch {
|
|
10740
|
-
return {
|
|
10914
|
+
return {
|
|
10915
|
+
kind: "mcp",
|
|
10916
|
+
path,
|
|
10917
|
+
status: "skipped",
|
|
10918
|
+
note: "existing file isn't valid JSON \u2014 left untouched"
|
|
10919
|
+
};
|
|
10741
10920
|
}
|
|
10742
10921
|
}
|
|
10743
|
-
current.mcpServers = {
|
|
10922
|
+
current.mcpServers = {
|
|
10923
|
+
...current.mcpServers ?? {},
|
|
10924
|
+
...incoming.mcpServers ?? {}
|
|
10925
|
+
};
|
|
10744
10926
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
10745
10927
|
ensureDir2(path);
|
|
10746
10928
|
writeFileSync15(path, JSON.stringify(current, null, 2) + "\n", { mode: 384 });
|
|
10747
10929
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
10748
10930
|
}
|
|
10749
|
-
|
|
10931
|
+
var CODEX_MCP_TABLE = "mcp_servers.sechroom";
|
|
10932
|
+
function decodeTomlBasicString(value) {
|
|
10933
|
+
let decoded = "";
|
|
10934
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
10935
|
+
const char = value[i];
|
|
10936
|
+
if (char !== "\\") {
|
|
10937
|
+
decoded += char;
|
|
10938
|
+
continue;
|
|
10939
|
+
}
|
|
10940
|
+
const escaped = value[++i];
|
|
10941
|
+
if (escaped === void 0) throw new Error("unterminated TOML escape");
|
|
10942
|
+
const simple = {
|
|
10943
|
+
b: "\b",
|
|
10944
|
+
f: "\f",
|
|
10945
|
+
n: "\n",
|
|
10946
|
+
r: "\r",
|
|
10947
|
+
t: " ",
|
|
10948
|
+
'"': '"',
|
|
10949
|
+
"\\": "\\"
|
|
10950
|
+
};
|
|
10951
|
+
if (escaped in simple) {
|
|
10952
|
+
decoded += simple[escaped];
|
|
10953
|
+
continue;
|
|
10954
|
+
}
|
|
10955
|
+
const digits = escaped === "u" ? 4 : escaped === "U" ? 8 : 0;
|
|
10956
|
+
if (digits === 0) throw new Error(`invalid TOML escape '\\\\${escaped}'`);
|
|
10957
|
+
const code = value.slice(i + 1, i + 1 + digits);
|
|
10958
|
+
if (!new RegExp(`^[0-9a-fA-F]{${digits}}$`).test(code))
|
|
10959
|
+
throw new Error(`invalid TOML Unicode escape '\\\\${escaped}${code}'`);
|
|
10960
|
+
decoded += String.fromCodePoint(Number.parseInt(code, 16));
|
|
10961
|
+
i += digits;
|
|
10962
|
+
}
|
|
10963
|
+
return decoded;
|
|
10964
|
+
}
|
|
10965
|
+
function normalizeTomlKey(raw) {
|
|
10966
|
+
const segments = [];
|
|
10967
|
+
let i = 0;
|
|
10968
|
+
while (i < raw.length) {
|
|
10969
|
+
while (/\s/.test(raw[i] ?? "")) i += 1;
|
|
10970
|
+
if (i >= raw.length) break;
|
|
10971
|
+
const quote = raw[i];
|
|
10972
|
+
let segment;
|
|
10973
|
+
if (quote === '"' || quote === "'") {
|
|
10974
|
+
i += 1;
|
|
10975
|
+
const start = i;
|
|
10976
|
+
let escaped = false;
|
|
10977
|
+
while (i < raw.length) {
|
|
10978
|
+
const char = raw[i];
|
|
10979
|
+
if (quote === '"' && escaped) {
|
|
10980
|
+
escaped = false;
|
|
10981
|
+
i += 1;
|
|
10982
|
+
continue;
|
|
10983
|
+
}
|
|
10984
|
+
if (quote === '"' && char === "\\") {
|
|
10985
|
+
escaped = true;
|
|
10986
|
+
i += 1;
|
|
10987
|
+
continue;
|
|
10988
|
+
}
|
|
10989
|
+
if (char === quote) break;
|
|
10990
|
+
i += 1;
|
|
10991
|
+
}
|
|
10992
|
+
if (raw[i] !== quote) throw new Error(`unterminated TOML key '${raw}'`);
|
|
10993
|
+
const encoded = raw.slice(start, i);
|
|
10994
|
+
segment = quote === '"' ? decodeTomlBasicString(encoded) : encoded;
|
|
10995
|
+
i += 1;
|
|
10996
|
+
} else {
|
|
10997
|
+
const start = i;
|
|
10998
|
+
while (i < raw.length && /[A-Za-z0-9_-]/.test(raw[i])) i += 1;
|
|
10999
|
+
segment = raw.slice(start, i);
|
|
11000
|
+
if (!segment) throw new Error(`invalid TOML key '${raw.trim()}'`);
|
|
11001
|
+
}
|
|
11002
|
+
segments.push(segment);
|
|
11003
|
+
while (/\s/.test(raw[i] ?? "")) i += 1;
|
|
11004
|
+
if (i >= raw.length) break;
|
|
11005
|
+
if (raw[i] !== ".") throw new Error(`invalid TOML key '${raw.trim()}'`);
|
|
11006
|
+
i += 1;
|
|
11007
|
+
}
|
|
11008
|
+
if (segments.length === 0) throw new Error("empty TOML key");
|
|
11009
|
+
return segments;
|
|
11010
|
+
}
|
|
11011
|
+
function sameTomlKey(left, right) {
|
|
11012
|
+
return left.length === right.length && left.every((segment, index) => segment === right[index]);
|
|
11013
|
+
}
|
|
11014
|
+
function isTomlKeyPrefix(prefix, candidate) {
|
|
11015
|
+
return prefix.length < candidate.length && prefix.every((segment, index) => segment === candidate[index]);
|
|
11016
|
+
}
|
|
11017
|
+
function tomlKeyLabel(key) {
|
|
11018
|
+
return key.join(".");
|
|
11019
|
+
}
|
|
11020
|
+
function stripTomlComment(line) {
|
|
11021
|
+
let quote = null;
|
|
11022
|
+
let escaped = false;
|
|
11023
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
11024
|
+
const rest = line.slice(i);
|
|
11025
|
+
const char = line[i];
|
|
11026
|
+
if (quote === "multiline-basic") {
|
|
11027
|
+
if (rest.startsWith('"""') && !escaped) {
|
|
11028
|
+
escaped = false;
|
|
11029
|
+
quote = null;
|
|
11030
|
+
i += 2;
|
|
11031
|
+
continue;
|
|
11032
|
+
}
|
|
11033
|
+
if (escaped) escaped = false;
|
|
11034
|
+
else if (char === "\\") escaped = true;
|
|
11035
|
+
continue;
|
|
11036
|
+
}
|
|
11037
|
+
if (quote === "multiline-literal") {
|
|
11038
|
+
if (rest.startsWith("'''")) {
|
|
11039
|
+
i += 2;
|
|
11040
|
+
quote = null;
|
|
11041
|
+
}
|
|
11042
|
+
continue;
|
|
11043
|
+
}
|
|
11044
|
+
if (quote === "basic") {
|
|
11045
|
+
if (escaped) escaped = false;
|
|
11046
|
+
else if (char === "\\") escaped = true;
|
|
11047
|
+
else if (char === '"') quote = null;
|
|
11048
|
+
continue;
|
|
11049
|
+
}
|
|
11050
|
+
if (quote === "literal") {
|
|
11051
|
+
if (char === "'") quote = null;
|
|
11052
|
+
continue;
|
|
11053
|
+
}
|
|
11054
|
+
if (rest.startsWith('"""')) {
|
|
11055
|
+
quote = "multiline-basic";
|
|
11056
|
+
i += 2;
|
|
11057
|
+
} else if (rest.startsWith("'''")) {
|
|
11058
|
+
quote = "multiline-literal";
|
|
11059
|
+
i += 2;
|
|
11060
|
+
} else if (char === '"') quote = "basic";
|
|
11061
|
+
else if (char === "'") quote = "literal";
|
|
11062
|
+
else if (char === "#") return line.slice(0, i);
|
|
11063
|
+
}
|
|
11064
|
+
return line;
|
|
11065
|
+
}
|
|
11066
|
+
function scanTomlValue(value, state) {
|
|
11067
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
11068
|
+
const rest = value.slice(i);
|
|
11069
|
+
const char = value[i];
|
|
11070
|
+
if (state.quote === "multiline-basic") {
|
|
11071
|
+
if (rest.startsWith('"""') && !state.escaped) {
|
|
11072
|
+
state.quote = null;
|
|
11073
|
+
i += 2;
|
|
11074
|
+
continue;
|
|
11075
|
+
}
|
|
11076
|
+
if (state.escaped) state.escaped = false;
|
|
11077
|
+
else if (char === "\\") state.escaped = true;
|
|
11078
|
+
continue;
|
|
11079
|
+
}
|
|
11080
|
+
if (state.quote === "multiline-literal") {
|
|
11081
|
+
if (rest.startsWith("'''")) {
|
|
11082
|
+
state.quote = null;
|
|
11083
|
+
i += 2;
|
|
11084
|
+
}
|
|
11085
|
+
continue;
|
|
11086
|
+
}
|
|
11087
|
+
if (state.quote === "basic") {
|
|
11088
|
+
if (state.escaped) state.escaped = false;
|
|
11089
|
+
else if (char === "\\") state.escaped = true;
|
|
11090
|
+
else if (char === '"') state.quote = null;
|
|
11091
|
+
continue;
|
|
11092
|
+
}
|
|
11093
|
+
if (state.quote === "literal") {
|
|
11094
|
+
if (char === "'") state.quote = null;
|
|
11095
|
+
continue;
|
|
11096
|
+
}
|
|
11097
|
+
if (char === "#") break;
|
|
11098
|
+
if (rest.startsWith('"""')) {
|
|
11099
|
+
state.quote = "multiline-basic";
|
|
11100
|
+
i += 2;
|
|
11101
|
+
} else if (rest.startsWith("'''")) {
|
|
11102
|
+
state.quote = "multiline-literal";
|
|
11103
|
+
i += 2;
|
|
11104
|
+
} else if (char === '"') state.quote = "basic";
|
|
11105
|
+
else if (char === "'") state.quote = "literal";
|
|
11106
|
+
else if (char === "[" || char === "{") state.containers.push(char);
|
|
11107
|
+
else if (char === "]" || char === "}") {
|
|
11108
|
+
const expected = char === "]" ? "[" : "{";
|
|
11109
|
+
if (state.containers.pop() !== expected)
|
|
11110
|
+
throw new Error(`unbalanced TOML delimiter '${char}'`);
|
|
11111
|
+
}
|
|
11112
|
+
}
|
|
11113
|
+
}
|
|
11114
|
+
function parseTomlHeader(rawLine) {
|
|
11115
|
+
const line = stripTomlComment(rawLine).trim();
|
|
11116
|
+
const array = line.startsWith("[[") && line.endsWith("]]");
|
|
11117
|
+
const table = !array && line.startsWith("[") && line.endsWith("]");
|
|
11118
|
+
if (!array && !table) return null;
|
|
11119
|
+
const prefix = array ? 2 : 1;
|
|
11120
|
+
const suffix = array ? 2 : 1;
|
|
11121
|
+
const rawName = line.slice(prefix, line.length - suffix).trim();
|
|
11122
|
+
if (!rawName) throw new Error("empty TOML table name");
|
|
11123
|
+
return { name: normalizeTomlKey(rawName), array };
|
|
11124
|
+
}
|
|
11125
|
+
function findTomlEquals(line) {
|
|
11126
|
+
let quote = null;
|
|
11127
|
+
let escaped = false;
|
|
11128
|
+
for (let i = 0; i < line.length; i += 1) {
|
|
11129
|
+
const char = line[i];
|
|
11130
|
+
if (quote === "basic") {
|
|
11131
|
+
if (escaped) escaped = false;
|
|
11132
|
+
else if (char === "\\") escaped = true;
|
|
11133
|
+
else if (char === '"') quote = null;
|
|
11134
|
+
continue;
|
|
11135
|
+
}
|
|
11136
|
+
if (quote === "literal") {
|
|
11137
|
+
if (char === "'") quote = null;
|
|
11138
|
+
continue;
|
|
11139
|
+
}
|
|
11140
|
+
if (char === '"') quote = "basic";
|
|
11141
|
+
else if (char === "'") quote = "literal";
|
|
11142
|
+
else if (char === "=") return i;
|
|
11143
|
+
}
|
|
11144
|
+
return -1;
|
|
11145
|
+
}
|
|
11146
|
+
function arrayTableContext(name, arrays) {
|
|
11147
|
+
let best = null;
|
|
11148
|
+
for (let i = arrays.length - 1; i >= 0; i -= 1) {
|
|
11149
|
+
const candidate = arrays[i];
|
|
11150
|
+
if (isTomlKeyPrefix(candidate.key, name)) {
|
|
11151
|
+
if (!best || candidate.key.length > best.key.length) best = candidate;
|
|
11152
|
+
}
|
|
11153
|
+
}
|
|
11154
|
+
return best;
|
|
11155
|
+
}
|
|
11156
|
+
function tomlTableIdentity(key, context) {
|
|
11157
|
+
return `${context?.identity ?? "root"}:${JSON.stringify(key)}`;
|
|
11158
|
+
}
|
|
11159
|
+
function isTomlScalar(value) {
|
|
11160
|
+
const decimal = "[+-]?(?:0|[1-9](?:_?[0-9])*)(?:\\.[0-9](?:_?[0-9])*)?(?:[eE][+-]?[0-9](?:_?[0-9])*)?";
|
|
11161
|
+
const number = new RegExp(
|
|
11162
|
+
`^(?:${decimal}|[+-]?0x[0-9A-Fa-f](?:_?[0-9A-Fa-f])*|[+-]?0o[0-7](?:_?[0-7])*|[+-]?0b[01](?:_?[01])*|[+-]?(?:inf|nan))$`
|
|
11163
|
+
);
|
|
11164
|
+
const datetime = /^(?:[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]+)?|[0-9]{4}-[0-9]{2}-[0-9]{2}(?:(?:[Tt ]|[Tt])[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]+)?(?:Z|[+-][0-9]{2}:[0-9]{2})?)?)$/;
|
|
11165
|
+
return number.test(value) || datetime.test(value) || /^(?:true|false)$/.test(value);
|
|
11166
|
+
}
|
|
11167
|
+
function validateTomlValueShape(value) {
|
|
11168
|
+
const token = stripTomlComment(value).trim();
|
|
11169
|
+
if (!token) throw new Error("empty TOML value");
|
|
11170
|
+
if (token.startsWith('"') || token.startsWith("'")) {
|
|
11171
|
+
const quote = token[0];
|
|
11172
|
+
if (quote && token.endsWith(quote)) return;
|
|
11173
|
+
}
|
|
11174
|
+
if (token.startsWith("[") && token.endsWith("]")) return;
|
|
11175
|
+
if (token.startsWith("{") && token.endsWith("}")) return;
|
|
11176
|
+
if (isTomlScalar(token)) return;
|
|
11177
|
+
throw new Error(`invalid TOML value '${token}'`);
|
|
11178
|
+
}
|
|
11179
|
+
function validateToml(content) {
|
|
11180
|
+
const tables = /* @__PURE__ */ new Set();
|
|
11181
|
+
const keys = /* @__PURE__ */ new Set();
|
|
11182
|
+
const arrayInstances = [];
|
|
11183
|
+
let currentTable = { identity: "root" };
|
|
11184
|
+
let valueState = {
|
|
11185
|
+
containers: [],
|
|
11186
|
+
quote: null,
|
|
11187
|
+
escaped: false
|
|
11188
|
+
};
|
|
11189
|
+
for (const rawLine of content.replace(/\r\n/g, "\n").split("\n")) {
|
|
11190
|
+
if (valueState.quote || valueState.containers.length > 0) {
|
|
11191
|
+
scanTomlValue(rawLine, valueState);
|
|
11192
|
+
continue;
|
|
11193
|
+
}
|
|
11194
|
+
const line = stripTomlComment(rawLine).trim();
|
|
11195
|
+
if (!line) continue;
|
|
11196
|
+
const header = parseTomlHeader(rawLine);
|
|
11197
|
+
if (header) {
|
|
11198
|
+
if (header.array) {
|
|
11199
|
+
const context = arrayTableContext(header.name, arrayInstances);
|
|
11200
|
+
const parentIdentity = context?.identity ?? null;
|
|
11201
|
+
const previous = [...arrayInstances].reverse().find(
|
|
11202
|
+
(candidate) => sameTomlKey(candidate.key, header.name) && candidate.parentIdentity === parentIdentity
|
|
11203
|
+
);
|
|
11204
|
+
const count = (previous?.count ?? 0) + 1;
|
|
11205
|
+
const instance = {
|
|
11206
|
+
key: header.name,
|
|
11207
|
+
parentIdentity,
|
|
11208
|
+
identity: `${parentIdentity ?? "root"}:${JSON.stringify(header.name)}#${count}`,
|
|
11209
|
+
count
|
|
11210
|
+
};
|
|
11211
|
+
arrayInstances.push(instance);
|
|
11212
|
+
currentTable = { identity: instance.identity };
|
|
11213
|
+
} else {
|
|
11214
|
+
const context = arrayTableContext(header.name, arrayInstances);
|
|
11215
|
+
currentTable = {
|
|
11216
|
+
identity: tomlTableIdentity(header.name, context)
|
|
11217
|
+
};
|
|
11218
|
+
if (tables.has(currentTable.identity))
|
|
11219
|
+
throw new Error(
|
|
11220
|
+
`duplicate TOML table '${tomlKeyLabel(header.name)}'`
|
|
11221
|
+
);
|
|
11222
|
+
tables.add(currentTable.identity);
|
|
11223
|
+
}
|
|
11224
|
+
continue;
|
|
11225
|
+
}
|
|
11226
|
+
const equals = findTomlEquals(line);
|
|
11227
|
+
if (equals <= 0) throw new Error(`invalid TOML line '${rawLine}'`);
|
|
11228
|
+
const key = normalizeTomlKey(line.slice(0, equals).trim());
|
|
11229
|
+
const value = line.slice(equals + 1).trim();
|
|
11230
|
+
if (!value || value.startsWith("#"))
|
|
11231
|
+
throw new Error(`empty TOML value for '${tomlKeyLabel(key)}'`);
|
|
11232
|
+
const fullKey = `${currentTable.identity}\0${JSON.stringify(key)}`;
|
|
11233
|
+
if (keys.has(fullKey))
|
|
11234
|
+
throw new Error(`duplicate TOML key '${tomlKeyLabel(key)}'`);
|
|
11235
|
+
keys.add(fullKey);
|
|
11236
|
+
scanTomlValue(line.slice(equals + 1), valueState);
|
|
11237
|
+
if (valueState.quote === "basic" || valueState.quote === "literal")
|
|
11238
|
+
throw new Error("unterminated TOML string");
|
|
11239
|
+
if (!valueState.quote && valueState.containers.length === 0)
|
|
11240
|
+
validateTomlValueShape(value);
|
|
11241
|
+
}
|
|
11242
|
+
if (valueState.quote) throw new Error("unterminated TOML string");
|
|
11243
|
+
if (valueState.containers.length > 0)
|
|
11244
|
+
throw new Error("unbalanced TOML value");
|
|
11245
|
+
}
|
|
11246
|
+
function isTomlTableHeader(line) {
|
|
11247
|
+
try {
|
|
11248
|
+
return parseTomlHeader(line) !== null;
|
|
11249
|
+
} catch {
|
|
11250
|
+
return false;
|
|
11251
|
+
}
|
|
11252
|
+
}
|
|
11253
|
+
function replaceTomlTable(content, tableName, replacement) {
|
|
11254
|
+
const target = normalizeTomlKey(tableName);
|
|
11255
|
+
const lines = content.replace(/\r\n/g, "\n").split("\n");
|
|
11256
|
+
const replacementLines = replacement.trim().split("\n");
|
|
11257
|
+
const kept = [];
|
|
11258
|
+
let i = 0;
|
|
11259
|
+
while (i < lines.length) {
|
|
11260
|
+
const header = parseTomlHeader(lines[i]);
|
|
11261
|
+
if (header && (sameTomlKey(header.name, target) || isTomlKeyPrefix(target, header.name))) {
|
|
11262
|
+
i += 1;
|
|
11263
|
+
while (i < lines.length && !isTomlTableHeader(lines[i])) i += 1;
|
|
11264
|
+
continue;
|
|
11265
|
+
}
|
|
11266
|
+
kept.push(lines[i]);
|
|
11267
|
+
i += 1;
|
|
11268
|
+
}
|
|
11269
|
+
const base = kept.join("\n").trim();
|
|
11270
|
+
return `${base.length > 0 ? `${base}
|
|
11271
|
+
|
|
11272
|
+
` : ""}${replacementLines.join("\n")}
|
|
11273
|
+
`;
|
|
11274
|
+
}
|
|
11275
|
+
var defaultTomlFileOperations = {
|
|
11276
|
+
copyFileSync,
|
|
11277
|
+
renameSync: renameSync4
|
|
11278
|
+
};
|
|
11279
|
+
function writeTomlAtomic(path, content, fileOperations = {}) {
|
|
11280
|
+
ensureDir2(path);
|
|
11281
|
+
const operations = { ...defaultTomlFileOperations, ...fileOperations };
|
|
11282
|
+
const temporary = `${path}.sechroom-${process.pid}-${randomUUID3()}.tmp`;
|
|
11283
|
+
const backup = `${path}.bak`;
|
|
11284
|
+
const backupTemporary = `${backup}.sechroom-${process.pid}-${randomUUID3()}.tmp`;
|
|
11285
|
+
const mode = existsSync13(path) ? statSync5(path).mode & 4095 : 384;
|
|
11286
|
+
try {
|
|
11287
|
+
writeFileSync15(temporary, content, { mode });
|
|
11288
|
+
chmodSync2(temporary, mode);
|
|
11289
|
+
validateToml(readFileSync16(temporary, "utf8"));
|
|
11290
|
+
if (existsSync13(path) && !existsSync13(backup)) {
|
|
11291
|
+
operations.copyFileSync(path, backupTemporary);
|
|
11292
|
+
chmodSync2(backupTemporary, mode);
|
|
11293
|
+
validateToml(readFileSync16(backupTemporary, "utf8"));
|
|
11294
|
+
operations.renameSync(backupTemporary, backup);
|
|
11295
|
+
}
|
|
11296
|
+
operations.renameSync(temporary, path);
|
|
11297
|
+
} finally {
|
|
11298
|
+
rmSync7(temporary, { force: true });
|
|
11299
|
+
rmSync7(backupTemporary, { force: true });
|
|
11300
|
+
}
|
|
11301
|
+
}
|
|
11302
|
+
function mergeCodexToml(path, snippet, dryRun, fileOperations = {}) {
|
|
10750
11303
|
const existed = existsSync13(path);
|
|
10751
|
-
|
|
10752
|
-
|
|
10753
|
-
|
|
10754
|
-
|
|
11304
|
+
const body = readOr(path, "");
|
|
11305
|
+
try {
|
|
11306
|
+
validateToml(body);
|
|
11307
|
+
} catch (error) {
|
|
11308
|
+
return {
|
|
11309
|
+
kind: "mcp",
|
|
11310
|
+
path,
|
|
11311
|
+
status: "skipped",
|
|
11312
|
+
note: `existing TOML is invalid \u2014 left untouched (${error.message})`
|
|
11313
|
+
};
|
|
11314
|
+
}
|
|
11315
|
+
const next = replaceTomlTable(body, CODEX_MCP_TABLE, snippet);
|
|
11316
|
+
try {
|
|
11317
|
+
validateToml(next);
|
|
11318
|
+
} catch (error) {
|
|
11319
|
+
return {
|
|
11320
|
+
kind: "mcp",
|
|
11321
|
+
path,
|
|
11322
|
+
status: "skipped",
|
|
11323
|
+
note: `generated TOML is invalid \u2014 left untouched (${error.message})`
|
|
11324
|
+
};
|
|
11325
|
+
}
|
|
10755
11326
|
if (dryRun) return { kind: "mcp", path, status: "dry-run" };
|
|
10756
|
-
|
|
10757
|
-
|
|
11327
|
+
if (next === body)
|
|
11328
|
+
return { kind: "mcp", path, status: existed ? "current" : "created" };
|
|
11329
|
+
try {
|
|
11330
|
+
writeTomlAtomic(path, next, fileOperations);
|
|
11331
|
+
} catch (error) {
|
|
11332
|
+
return {
|
|
11333
|
+
kind: "mcp",
|
|
11334
|
+
path,
|
|
11335
|
+
status: "skipped",
|
|
11336
|
+
note: `could not replace TOML \u2014 left untouched (${error.message})`
|
|
11337
|
+
};
|
|
11338
|
+
}
|
|
10758
11339
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
10759
11340
|
}
|
|
10760
11341
|
function writeInstructionBlock(path, write, dryRun) {
|
|
10761
11342
|
const existed = existsSync13(path);
|
|
10762
11343
|
const next = computeBlockFile(readOr(path, ""), write);
|
|
10763
|
-
if (dryRun)
|
|
11344
|
+
if (dryRun)
|
|
11345
|
+
return { kind: "instruction", path, status: "dry-run", block: write.block };
|
|
10764
11346
|
ensureDir2(path);
|
|
10765
11347
|
writeFileSync15(path, next);
|
|
10766
|
-
return {
|
|
11348
|
+
return {
|
|
11349
|
+
kind: "instruction",
|
|
11350
|
+
path,
|
|
11351
|
+
status: existed ? "merged" : "created",
|
|
11352
|
+
block: write.block
|
|
11353
|
+
};
|
|
10767
11354
|
}
|
|
10768
11355
|
function computeBlockFile(current, write) {
|
|
10769
11356
|
const rendered = renderBlock(write);
|
|
@@ -10812,7 +11399,13 @@ function applyBlock(path, write, mode, dryRun) {
|
|
|
10812
11399
|
};
|
|
10813
11400
|
}
|
|
10814
11401
|
if (state === "current") {
|
|
10815
|
-
return {
|
|
11402
|
+
return {
|
|
11403
|
+
kind: "instruction",
|
|
11404
|
+
path,
|
|
11405
|
+
status: "current",
|
|
11406
|
+
eval: "current",
|
|
11407
|
+
block: write.block
|
|
11408
|
+
};
|
|
10816
11409
|
}
|
|
10817
11410
|
if (state === "drift" && mode !== "force") {
|
|
10818
11411
|
const proposedPath = `${path}.proposed`;
|
|
@@ -10847,7 +11440,12 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
10847
11440
|
const section = findSection(surface, target.mcp.sectionType);
|
|
10848
11441
|
const snippet = sectionSnippet(section);
|
|
10849
11442
|
if (!snippet) {
|
|
10850
|
-
actions.push({
|
|
11443
|
+
actions.push({
|
|
11444
|
+
kind: "mcp",
|
|
11445
|
+
path: target.mcp.path,
|
|
11446
|
+
status: "skipped",
|
|
11447
|
+
note: `no ${target.mcp.sectionType} section on surface '${target.mcp.surfaceKey}'`
|
|
11448
|
+
});
|
|
10851
11449
|
} else {
|
|
10852
11450
|
actions.push(
|
|
10853
11451
|
target.mcp.format === "toml" ? mergeCodexToml(target.mcp.path, snippet, dryRun) : mergeMcpJson(target.mcp.path, snippet, dryRun)
|
|
@@ -10858,25 +11456,44 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
10858
11456
|
const surface = findSurface(setup, target.instruction.surfaceKey);
|
|
10859
11457
|
const section = findSection(surface, SectionType.InstructionFile);
|
|
10860
11458
|
if (!section) {
|
|
10861
|
-
actions.push({
|
|
11459
|
+
actions.push({
|
|
11460
|
+
kind: "instruction",
|
|
11461
|
+
path: target.instruction.path,
|
|
11462
|
+
status: "skipped",
|
|
11463
|
+
note: `no instruction-file section on surface '${target.instruction.surfaceKey}'`
|
|
11464
|
+
});
|
|
10862
11465
|
} else {
|
|
10863
11466
|
const resolved = await withSpinner(
|
|
10864
11467
|
`Resolving ${target.label} agent instructions`,
|
|
10865
11468
|
() => resolveInstruction(cfg, section, opts.personalWorkspaceId)
|
|
10866
11469
|
);
|
|
10867
11470
|
if (!resolved) {
|
|
10868
|
-
actions.push({
|
|
11471
|
+
actions.push({
|
|
11472
|
+
kind: "instruction",
|
|
11473
|
+
path: target.instruction.path,
|
|
11474
|
+
status: "skipped",
|
|
11475
|
+
note: "no role template found in this tenant \u2014 install the SEM Starter bundle, then re-run `sechroom setup agent-files`"
|
|
11476
|
+
});
|
|
10869
11477
|
} else {
|
|
10870
11478
|
const action = applyBlock(
|
|
10871
11479
|
target.instruction.path,
|
|
10872
|
-
{
|
|
11480
|
+
{
|
|
11481
|
+
block: "role-template",
|
|
11482
|
+
body: resolved.body,
|
|
11483
|
+
source: resolved.sourceRef
|
|
11484
|
+
},
|
|
10873
11485
|
mode,
|
|
10874
11486
|
opts.dryRun
|
|
10875
11487
|
);
|
|
10876
|
-
actions.push(
|
|
11488
|
+
actions.push(
|
|
11489
|
+
resolved.source === "override" && action.status !== "current" ? { ...action, note: action.note ?? "your personal copy" } : action
|
|
11490
|
+
);
|
|
10877
11491
|
}
|
|
10878
11492
|
}
|
|
10879
|
-
const conventionsSection = findSection(
|
|
11493
|
+
const conventionsSection = findSection(
|
|
11494
|
+
surface,
|
|
11495
|
+
SectionType.WorkspaceConventions
|
|
11496
|
+
);
|
|
10880
11497
|
if (conventionsSection) {
|
|
10881
11498
|
const conventions = await withSpinner(
|
|
10882
11499
|
`Composing ${target.label} workspace conventions`,
|
|
@@ -10885,11 +11502,20 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
10885
11502
|
if (conventions) {
|
|
10886
11503
|
const action = applyBlock(
|
|
10887
11504
|
target.instruction.path,
|
|
10888
|
-
{
|
|
11505
|
+
{
|
|
11506
|
+
block: "workspace-conventions",
|
|
11507
|
+
body: conventions.body,
|
|
11508
|
+
source: `workspace:${cfg.workspaceId ?? ""}`
|
|
11509
|
+
},
|
|
10889
11510
|
mode,
|
|
10890
11511
|
opts.dryRun
|
|
10891
11512
|
);
|
|
10892
|
-
actions.push(
|
|
11513
|
+
actions.push(
|
|
11514
|
+
action.status === "current" ? action : {
|
|
11515
|
+
...action,
|
|
11516
|
+
note: action.note ?? `workspace conventions (${conventions.refs.length})`
|
|
11517
|
+
}
|
|
11518
|
+
);
|
|
10893
11519
|
}
|
|
10894
11520
|
}
|
|
10895
11521
|
}
|
|
@@ -11107,7 +11733,12 @@ function buildConventionDraft(title, rawKind, rawBody) {
|
|
|
11107
11733
|
|
|
11108
11734
|
${body}
|
|
11109
11735
|
`,
|
|
11110
|
-
tags: [
|
|
11736
|
+
tags: [
|
|
11737
|
+
"agent-setup-bundle",
|
|
11738
|
+
"scope:sechroom",
|
|
11739
|
+
`kind:${kind}`,
|
|
11740
|
+
"archetype:document"
|
|
11741
|
+
]
|
|
11111
11742
|
};
|
|
11112
11743
|
}
|
|
11113
11744
|
function copyChoice(opts) {
|
|
@@ -11120,9 +11751,16 @@ async function maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId,
|
|
|
11120
11751
|
const instr = targets[key]?.instruction;
|
|
11121
11752
|
if (!instr || seen.has(instr.surfaceKey)) continue;
|
|
11122
11753
|
seen.add(instr.surfaceKey);
|
|
11123
|
-
const section = findSection(
|
|
11754
|
+
const section = findSection(
|
|
11755
|
+
findSurface(setup, instr.surfaceKey),
|
|
11756
|
+
SectionType.InstructionFile
|
|
11757
|
+
);
|
|
11124
11758
|
if (!section) continue;
|
|
11125
|
-
const resolved = await resolveInstruction(
|
|
11759
|
+
const resolved = await resolveInstruction(
|
|
11760
|
+
cfg,
|
|
11761
|
+
section,
|
|
11762
|
+
personalWorkspaceId
|
|
11763
|
+
);
|
|
11126
11764
|
if (!resolved || resolved.source === "override") continue;
|
|
11127
11765
|
let make = choice === "yes";
|
|
11128
11766
|
if (choice === "ask") {
|
|
@@ -11137,8 +11775,10 @@ version, the shared template stays clean, and you can discard back anytime.
|
|
|
11137
11775
|
}
|
|
11138
11776
|
if (make) {
|
|
11139
11777
|
await createOverride(cfg, resolved, personalWorkspaceId);
|
|
11140
|
-
process.stderr.write(
|
|
11141
|
-
|
|
11778
|
+
process.stderr.write(
|
|
11779
|
+
`\u2713 personal copy created for ${instr.surfaceKey} \u2014 edit it on the Agent setup page or via the API.
|
|
11780
|
+
`
|
|
11781
|
+
);
|
|
11142
11782
|
}
|
|
11143
11783
|
}
|
|
11144
11784
|
}
|
|
@@ -11148,7 +11788,9 @@ function resolveClientKeys(raw) {
|
|
|
11148
11788
|
if (tokens.includes("all")) return [...ALL_CLIENT_KEYS];
|
|
11149
11789
|
for (const k of tokens) {
|
|
11150
11790
|
if (!targets[k]) {
|
|
11151
|
-
fail(
|
|
11791
|
+
fail(
|
|
11792
|
+
`unknown client '${k}'. Known: ${ALL_CLIENT_KEYS.join(", ")}, or 'all'.`
|
|
11793
|
+
);
|
|
11152
11794
|
}
|
|
11153
11795
|
}
|
|
11154
11796
|
return [...new Set(tokens)];
|
|
@@ -11159,8 +11801,10 @@ ${client.label} (${client.key}):
|
|
|
11159
11801
|
`);
|
|
11160
11802
|
for (const a of actions) {
|
|
11161
11803
|
const tag = a.status === "skipped" ? "skip" : a.status;
|
|
11162
|
-
process.stdout.write(
|
|
11163
|
-
`
|
|
11804
|
+
process.stdout.write(
|
|
11805
|
+
` [${tag}] ${a.kind}: ${a.path}${a.note ? ` \u2014 ${a.note}` : ""}
|
|
11806
|
+
`
|
|
11807
|
+
);
|
|
11164
11808
|
}
|
|
11165
11809
|
}
|
|
11166
11810
|
function resolveEvalMode(opts) {
|
|
@@ -11234,8 +11878,10 @@ function summarizeEval(result, mode, json, dryRun) {
|
|
|
11234
11878
|
const { eval: counts } = buildCheckReport(result);
|
|
11235
11879
|
if (json) return;
|
|
11236
11880
|
if (!dryRun && counts.stale) {
|
|
11237
|
-
process.stderr.write(
|
|
11238
|
-
|
|
11881
|
+
process.stderr.write(
|
|
11882
|
+
`\u21BB refreshed ${counts.stale} section(s) the server had moved
|
|
11883
|
+
`
|
|
11884
|
+
);
|
|
11239
11885
|
}
|
|
11240
11886
|
if (!dryRun && counts.drift) {
|
|
11241
11887
|
process.stderr.write(
|
|
@@ -11266,7 +11912,39 @@ async function resolveNamespaceChoice(cfg, flag) {
|
|
|
11266
11912
|
return picked === GLOBAL_NAMESPACE ? null : picked;
|
|
11267
11913
|
}
|
|
11268
11914
|
function registerInit(program2) {
|
|
11269
|
-
program2.command("init").description(
|
|
11915
|
+
program2.command("init").description(
|
|
11916
|
+
"Wire this project for sechroom: write MCP config + agent instruction files from the server's setup descriptors"
|
|
11917
|
+
).option(
|
|
11918
|
+
"--client <list...>",
|
|
11919
|
+
`clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all'`,
|
|
11920
|
+
DEFAULT_CLIENT_KEY
|
|
11921
|
+
).option(
|
|
11922
|
+
"--scope <scope>",
|
|
11923
|
+
"install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default global",
|
|
11924
|
+
"global"
|
|
11925
|
+
).option("--dry-run", "print what would be written without writing", false).option("--mcp-only", "only write MCP config (skip agent files)", false).option(
|
|
11926
|
+
"--agent-files-only",
|
|
11927
|
+
"only write agent instruction files (skip MCP config)",
|
|
11928
|
+
false
|
|
11929
|
+
).option(
|
|
11930
|
+
"--copy",
|
|
11931
|
+
"make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)"
|
|
11932
|
+
).option(
|
|
11933
|
+
"--namespace <slug>",
|
|
11934
|
+
"MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)"
|
|
11935
|
+
).option(
|
|
11936
|
+
"--refresh",
|
|
11937
|
+
"refresh out-of-date agent-file blocks in place (local edits preserved to .proposed)",
|
|
11938
|
+
false
|
|
11939
|
+
).option(
|
|
11940
|
+
"--force",
|
|
11941
|
+
"rewrite agent-file managed blocks, overwriting local edits inside the markers",
|
|
11942
|
+
false
|
|
11943
|
+
).option(
|
|
11944
|
+
"--check",
|
|
11945
|
+
"report whether agent files would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
|
|
11946
|
+
false
|
|
11947
|
+
).addHelpText(
|
|
11270
11948
|
"after",
|
|
11271
11949
|
`
|
|
11272
11950
|
Examples:
|
|
@@ -11281,7 +11959,9 @@ Examples:
|
|
|
11281
11959
|
const mode = resolveEvalMode(opts);
|
|
11282
11960
|
const check = mode === "check";
|
|
11283
11961
|
if (check && opts.mcpOnly) {
|
|
11284
|
-
fail(
|
|
11962
|
+
fail(
|
|
11963
|
+
"--check inspects agent files and cannot be combined with --mcp-only."
|
|
11964
|
+
);
|
|
11285
11965
|
}
|
|
11286
11966
|
const namespaceSlug = await resolveNamespaceChoice(cfg, opts.namespace);
|
|
11287
11967
|
const setup = await withSpinner(
|
|
@@ -11295,14 +11975,28 @@ Examples:
|
|
|
11295
11975
|
} catch (err2) {
|
|
11296
11976
|
return fail(err2.message);
|
|
11297
11977
|
}
|
|
11298
|
-
const claudeTargets = resolveClaudeTargets({
|
|
11978
|
+
const claudeTargets = resolveClaudeTargets({
|
|
11979
|
+
override: g.claudeConfigDir,
|
|
11980
|
+
scope,
|
|
11981
|
+
cwd: process.cwd()
|
|
11982
|
+
});
|
|
11299
11983
|
const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
|
|
11300
|
-
const targets = clientTargets(process.cwd(), {
|
|
11984
|
+
const targets = clientTargets(process.cwd(), {
|
|
11985
|
+
claudeDir: claudeTargets[0]?.dir,
|
|
11986
|
+
codexHome: codexHomes[0] ?? null
|
|
11987
|
+
});
|
|
11301
11988
|
const keys = resolveClientKeys(opts.client);
|
|
11302
11989
|
const json = g.json;
|
|
11303
11990
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
11304
11991
|
if (!opts.dryRun && !opts.mcpOnly && !check) {
|
|
11305
|
-
await maybeOfferCopies(
|
|
11992
|
+
await maybeOfferCopies(
|
|
11993
|
+
cfg,
|
|
11994
|
+
setup,
|
|
11995
|
+
targets,
|
|
11996
|
+
keys,
|
|
11997
|
+
personalWorkspaceId,
|
|
11998
|
+
copyChoice(opts)
|
|
11999
|
+
);
|
|
11306
12000
|
}
|
|
11307
12001
|
const result = [];
|
|
11308
12002
|
for (const key of keys) {
|
|
@@ -11320,18 +12014,33 @@ Examples:
|
|
|
11320
12014
|
summarizeEval(result, mode, Boolean(json), Boolean(opts.dryRun));
|
|
11321
12015
|
if (!json && !opts.dryRun && !opts.mcpOnly && !check) {
|
|
11322
12016
|
for (const t of claudeTargets) {
|
|
11323
|
-
await maybeOfferSkills(cfg, personalWorkspaceId, {
|
|
12017
|
+
await maybeOfferSkills(cfg, personalWorkspaceId, {
|
|
12018
|
+
yes: false,
|
|
12019
|
+
dryRun: Boolean(opts.dryRun),
|
|
12020
|
+
surface: "claude-code",
|
|
12021
|
+
configDir: t.dir
|
|
12022
|
+
});
|
|
11324
12023
|
}
|
|
11325
12024
|
}
|
|
11326
12025
|
if (!json && !opts.dryRun && !opts.mcpOnly && !check) {
|
|
11327
|
-
await maybeOfferHooks({
|
|
12026
|
+
await maybeOfferHooks({
|
|
12027
|
+
yes: false,
|
|
12028
|
+
dryRun: Boolean(opts.dryRun),
|
|
12029
|
+
cwd: process.cwd(),
|
|
12030
|
+
scope,
|
|
12031
|
+
claudeConfigDir: g.claudeConfigDir,
|
|
12032
|
+
codexHome: g.codexHome
|
|
12033
|
+
});
|
|
11328
12034
|
}
|
|
11329
12035
|
if (json) {
|
|
11330
12036
|
emit({ dryRun: Boolean(opts.dryRun), clients: result }, true);
|
|
11331
12037
|
return;
|
|
11332
12038
|
}
|
|
11333
12039
|
const first = targets[keys[0]];
|
|
11334
|
-
const surface = findSurface(
|
|
12040
|
+
const surface = findSurface(
|
|
12041
|
+
setup,
|
|
12042
|
+
first.mcp?.surfaceKey ?? first.instruction?.surfaceKey ?? ""
|
|
12043
|
+
);
|
|
11335
12044
|
const verify = findSection(surface, SectionType.Verify);
|
|
11336
12045
|
if (verify?.description) {
|
|
11337
12046
|
process.stdout.write(`
|
|
@@ -11345,13 +12054,64 @@ Next \u2014 verify: ${verify.description}
|
|
|
11345
12054
|
}
|
|
11346
12055
|
function registerSetup(program2, deps = {}) {
|
|
11347
12056
|
const setup = program2.command("setup").description("Granular onboarding steps (init runs these together)");
|
|
11348
|
-
setup.command("mcp <clients...>").description(
|
|
11349
|
-
|
|
11350
|
-
|
|
11351
|
-
|
|
11352
|
-
|
|
12057
|
+
setup.command("mcp <clients...>").description(
|
|
12058
|
+
`Write only the MCP config for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`
|
|
12059
|
+
).option("--dry-run", "print what would be written without writing", false).option(
|
|
12060
|
+
"--namespace <slug>",
|
|
12061
|
+
"MCP namespace for the connection URL (interactive picker if omitted on a TTY; defaults to tenant-global)"
|
|
12062
|
+
).addHelpText(
|
|
12063
|
+
"after",
|
|
12064
|
+
"\nExamples:\n $ sechroom setup mcp codex\n $ sechroom setup mcp claude-code codex\n $ sechroom setup mcp all"
|
|
12065
|
+
).action(async (clients, opts, cmd) => {
|
|
12066
|
+
await runClients(clients, cmd, {
|
|
12067
|
+
dryRun: Boolean(opts.dryRun),
|
|
12068
|
+
mcp: true,
|
|
12069
|
+
agentFiles: false,
|
|
12070
|
+
namespace: opts.namespace
|
|
12071
|
+
});
|
|
12072
|
+
});
|
|
12073
|
+
setup.command("agent-files <clients...>").description(
|
|
12074
|
+
`Write only the agent instruction file(s) for one or more clients (${ALL_CLIENT_KEYS.join(", ")}, or 'all')`
|
|
12075
|
+
).option("--dry-run", "print what would be written without writing", false).option(
|
|
12076
|
+
"--copy",
|
|
12077
|
+
"make a personal copy you can edit (default: prompt on a TTY, else skip)"
|
|
12078
|
+
).option(
|
|
12079
|
+
"--refresh",
|
|
12080
|
+
"refresh out-of-date blocks in place (local edits preserved to .proposed)",
|
|
12081
|
+
false
|
|
12082
|
+
).option(
|
|
12083
|
+
"--force",
|
|
12084
|
+
"rewrite managed blocks, overwriting local edits inside the markers",
|
|
12085
|
+
false
|
|
12086
|
+
).option(
|
|
12087
|
+
"--check",
|
|
12088
|
+
"report whether anything would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
|
|
12089
|
+
false
|
|
12090
|
+
).addHelpText(
|
|
12091
|
+
"after",
|
|
12092
|
+
"\nExamples:\n $ sechroom setup agent-files claude-code CLAUDE.md\n $ sechroom setup agent-files claude-code codex CLAUDE.md + AGENTS.md in one run\n $ sechroom setup agent-files all --check CI gate: nonzero exit if out of date\n $ sechroom setup agent-files claude-code --force overwrite local edits in the managed block"
|
|
12093
|
+
).action(async (clients, opts, cmd) => {
|
|
12094
|
+
await runClients(clients, cmd, {
|
|
12095
|
+
dryRun: Boolean(opts.dryRun),
|
|
12096
|
+
mcp: false,
|
|
12097
|
+
agentFiles: true,
|
|
12098
|
+
copy: opts.copy,
|
|
12099
|
+
mode: resolveEvalMode(opts)
|
|
12100
|
+
});
|
|
11353
12101
|
});
|
|
11354
|
-
setup.command("new-convention <title...>").description(
|
|
12102
|
+
setup.command("new-convention <title...>").description(
|
|
12103
|
+
"Scaffold a workspace-conventions section: author a correctly-tagged memo (header as first body line) + regen the agent files"
|
|
12104
|
+
).option(
|
|
12105
|
+
"--kind <kind>",
|
|
12106
|
+
"reference | standard (orders the section; reference first)",
|
|
12107
|
+
"reference"
|
|
12108
|
+
).option(
|
|
12109
|
+
"--workspace <id>",
|
|
12110
|
+
"workspace to author in (default: the bound workspace)"
|
|
12111
|
+
).option(
|
|
12112
|
+
"--body <markdown>",
|
|
12113
|
+
"section body (default: a TODO scaffold to edit later)"
|
|
12114
|
+
).option("--no-regen", "skip the agent-files regen after authoring").option("--dry-run", "print what would be authored; write nothing", false).addHelpText(
|
|
11355
12115
|
"after",
|
|
11356
12116
|
`
|
|
11357
12117
|
The memo carries the two conventions a workspace-conventions section needs (FR-sechroom-236):
|
|
@@ -11367,10 +12127,15 @@ Examples:
|
|
|
11367
12127
|
const cfg = resolveConfig(cmd.optsWithGlobals());
|
|
11368
12128
|
const json = Boolean(cmd.optsWithGlobals().json);
|
|
11369
12129
|
const title = titleParts.join(" ").trim();
|
|
11370
|
-
if (!title)
|
|
12130
|
+
if (!title)
|
|
12131
|
+
fail(
|
|
12132
|
+
'a section title is required, e.g. `sechroom setup new-convention "Deploy runbook"`.'
|
|
12133
|
+
);
|
|
11371
12134
|
const workspaceId = opts.workspace ?? cfg.workspaceId;
|
|
11372
12135
|
if (!workspaceId)
|
|
11373
|
-
fail(
|
|
12136
|
+
fail(
|
|
12137
|
+
"no workspace \u2014 pass --workspace <id> or bind one (`sechroom config set --local workspaceId <id>`)."
|
|
12138
|
+
);
|
|
11374
12139
|
const draft = buildConventionDraft(title, opts.kind, opts.body);
|
|
11375
12140
|
if (opts.dryRun) {
|
|
11376
12141
|
emit(
|
|
@@ -11411,7 +12176,10 @@ Examples:
|
|
|
11411
12176
|
}
|
|
11412
12177
|
if (opts.regen === false) {
|
|
11413
12178
|
if (json) emit({ id: data.id, workspaceId, regen: false }, true);
|
|
11414
|
-
else
|
|
12179
|
+
else
|
|
12180
|
+
process.stdout.write(
|
|
12181
|
+
"Skipped regen (--no-regen). Run `sechroom setup agent-files all` to apply.\n"
|
|
12182
|
+
);
|
|
11415
12183
|
return;
|
|
11416
12184
|
}
|
|
11417
12185
|
const regenerate = deps.regenerateConvention ?? runClients;
|
|
@@ -11440,7 +12208,14 @@ async function runClients(clients, cmd, opts) {
|
|
|
11440
12208
|
);
|
|
11441
12209
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
11442
12210
|
if (opts.agentFiles && !opts.dryRun && !check) {
|
|
11443
|
-
await maybeOfferCopies(
|
|
12211
|
+
await maybeOfferCopies(
|
|
12212
|
+
cfg,
|
|
12213
|
+
setupData,
|
|
12214
|
+
targets,
|
|
12215
|
+
keys,
|
|
12216
|
+
personalWorkspaceId,
|
|
12217
|
+
copyChoice(opts)
|
|
12218
|
+
);
|
|
11444
12219
|
}
|
|
11445
12220
|
const json = g.json;
|
|
11446
12221
|
const result = [];
|
|
@@ -11461,7 +12236,9 @@ async function runClients(clients, cmd, opts) {
|
|
|
11461
12236
|
emit({ dryRun: opts.dryRun, clients: result }, true);
|
|
11462
12237
|
return;
|
|
11463
12238
|
}
|
|
11464
|
-
process.stdout.write(
|
|
12239
|
+
process.stdout.write(
|
|
12240
|
+
opts.dryRun ? "\n(dry run \u2014 nothing written)\n" : "\nDone.\n"
|
|
12241
|
+
);
|
|
11465
12242
|
}
|
|
11466
12243
|
|
|
11467
12244
|
// src/commands/namespace.ts
|
|
@@ -11537,7 +12314,7 @@ import { basename as basename5, join as join21 } from "path";
|
|
|
11537
12314
|
|
|
11538
12315
|
// src/commands/fanout.ts
|
|
11539
12316
|
import { spawnSync } from "child_process";
|
|
11540
|
-
import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync4, statSync as
|
|
12317
|
+
import { existsSync as existsSync14, readFileSync as readFileSync17, readdirSync as readdirSync4, statSync as statSync6 } from "fs";
|
|
11541
12318
|
import { isAbsolute as isAbsolute3, join as join20, resolve as resolve7 } from "path";
|
|
11542
12319
|
var ICON = {
|
|
11543
12320
|
refresh: "\u21BB",
|
|
@@ -11560,7 +12337,7 @@ function discoverChildren(root) {
|
|
|
11560
12337
|
if (name.startsWith(".") || name === "node_modules") continue;
|
|
11561
12338
|
const dir = join20(root, name);
|
|
11562
12339
|
try {
|
|
11563
|
-
if (!
|
|
12340
|
+
if (!statSync6(dir).isDirectory()) continue;
|
|
11564
12341
|
} catch {
|
|
11565
12342
|
continue;
|
|
11566
12343
|
}
|
|
@@ -11686,20 +12463,27 @@ function resolveBaseUrl(g) {
|
|
|
11686
12463
|
return baseUrl.replace(/\/$/, "");
|
|
11687
12464
|
}
|
|
11688
12465
|
async function fetchWorkspaces(client) {
|
|
11689
|
-
const { data, error } = await client.GET("/workspaces", {
|
|
11690
|
-
|
|
12466
|
+
const { data, error } = await client.GET("/workspaces", {
|
|
12467
|
+
params: { query: { includeArchived: false } }
|
|
12468
|
+
});
|
|
12469
|
+
if (error)
|
|
12470
|
+
throw new Error(`Couldn't list your workspaces: ${JSON.stringify(error)}`);
|
|
11691
12471
|
const rows = data ?? [];
|
|
11692
12472
|
return rows.map((r) => r.item ?? r).filter((w) => Boolean(w?.id && w?.name)).map((w) => ({ id: w.id, name: w.name, parentId: w.parentId ?? null }));
|
|
11693
12473
|
}
|
|
11694
12474
|
async function lookupWorkspace(client, id) {
|
|
11695
|
-
const { data, error } = await client.GET("/workspaces/{workspaceId}", {
|
|
12475
|
+
const { data, error } = await client.GET("/workspaces/{workspaceId}", {
|
|
12476
|
+
params: { path: { workspaceId: id } }
|
|
12477
|
+
});
|
|
11696
12478
|
if (error) return null;
|
|
11697
12479
|
const env = data;
|
|
11698
12480
|
const w = env?.item ?? env;
|
|
11699
12481
|
return w?.id ? { id: w.id, name: w.name ?? id, parentId: w.parentId ?? null } : null;
|
|
11700
12482
|
}
|
|
11701
12483
|
async function warnIfProjectStray(client, projectId, workspaceId, json) {
|
|
11702
|
-
const { data, error } = await client.GET("/projects/{projectId}", {
|
|
12484
|
+
const { data, error } = await client.GET("/projects/{projectId}", {
|
|
12485
|
+
params: { path: { projectId } }
|
|
12486
|
+
});
|
|
11703
12487
|
if (error) return;
|
|
11704
12488
|
const env = data;
|
|
11705
12489
|
const owner = env?.item?.workspaceId;
|
|
@@ -11743,10 +12527,15 @@ function personalSubtreeIds(personalId, all) {
|
|
|
11743
12527
|
async function pickWorkspace(client, opts = {}) {
|
|
11744
12528
|
const promptLabel = opts.promptLabel ?? "Bind this directory to a workspace:";
|
|
11745
12529
|
const dirName = opts.dirName ?? basename5(process.cwd());
|
|
11746
|
-
const all = await withSpinner(
|
|
12530
|
+
const all = await withSpinner(
|
|
12531
|
+
"Listing your workspaces",
|
|
12532
|
+
() => fetchWorkspaces(client)
|
|
12533
|
+
);
|
|
11747
12534
|
if (all.length === 0) {
|
|
11748
|
-
process.stderr.write(
|
|
11749
|
-
`)
|
|
12535
|
+
process.stderr.write(
|
|
12536
|
+
`no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
|
|
12537
|
+
`
|
|
12538
|
+
);
|
|
11750
12539
|
return void 0;
|
|
11751
12540
|
}
|
|
11752
12541
|
const byId = new Map(all.map((w) => [w.id, w]));
|
|
@@ -11761,19 +12550,35 @@ async function pickWorkspace(client, opts = {}) {
|
|
|
11761
12550
|
const matched = candidates.filter(isMatch).sort(byPath);
|
|
11762
12551
|
const rest = candidates.filter((w) => !isMatch(w)).sort(byPath);
|
|
11763
12552
|
const choices = [
|
|
11764
|
-
...matched.map((w) => ({
|
|
11765
|
-
|
|
11766
|
-
|
|
12553
|
+
...matched.map((w) => ({
|
|
12554
|
+
label: workspacePath(w, byId),
|
|
12555
|
+
value: w.id,
|
|
12556
|
+
hint: style.dim(`matches "${dirName}"`)
|
|
12557
|
+
})),
|
|
12558
|
+
...rest.map((w) => ({
|
|
12559
|
+
label: workspacePath(w, byId),
|
|
12560
|
+
value: w.id,
|
|
12561
|
+
hint: w.id
|
|
12562
|
+
})),
|
|
12563
|
+
{
|
|
12564
|
+
label: style.dim("skip \u2014 don't bind a workspace"),
|
|
12565
|
+
value: SKIP,
|
|
12566
|
+
hint: void 0
|
|
12567
|
+
}
|
|
11767
12568
|
];
|
|
11768
12569
|
const defaultValue = matched.length === 1 ? matched[0].id : SKIP;
|
|
11769
12570
|
const chosen = candidates.length > 12 ? await promptAutocomplete(promptLabel, choices, defaultValue) : await promptSelect(promptLabel, choices, defaultValue);
|
|
11770
12571
|
if (chosen === SKIP) return void 0;
|
|
11771
12572
|
const picked = byId.get(chosen);
|
|
11772
|
-
const collisions = all.filter(
|
|
12573
|
+
const collisions = all.filter(
|
|
12574
|
+
(w) => w.id !== picked.id && namesCollide(w.name, picked.name)
|
|
12575
|
+
);
|
|
11773
12576
|
if (collisions.length > 0) {
|
|
11774
12577
|
process.stderr.write(
|
|
11775
12578
|
`${warn("\u26A0")} ${collisions.length} other workspace(s) have a similar name to ${style.cyan(workspacePath(picked, byId))}:
|
|
11776
|
-
` + collisions.map(
|
|
12579
|
+
` + collisions.map(
|
|
12580
|
+
(w) => ` ${style.dim(workspacePath(w, byId))} ${style.dim(`(${w.id})`)}`
|
|
12581
|
+
).join("\n") + `
|
|
11777
12582
|
You picked ${style.dim(picked.id)} \u2014 re-run \`sechroom config set --local workspaceId <id>\` if that's wrong.
|
|
11778
12583
|
`
|
|
11779
12584
|
);
|
|
@@ -11800,10 +12605,17 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
11800
12605
|
const local = readLocalConfig();
|
|
11801
12606
|
let tenant = g.tenant ?? process.env.SECHROOM_TENANT ?? local.tenant ?? persisted.tenant ?? "";
|
|
11802
12607
|
if (!tenant) {
|
|
11803
|
-
const client = await makeClient({
|
|
12608
|
+
const client = await makeClient({
|
|
12609
|
+
baseUrl,
|
|
12610
|
+
tenant: "",
|
|
12611
|
+
account: resolveAccountAlias(),
|
|
12612
|
+
clientId: persisted.clientId
|
|
12613
|
+
});
|
|
11804
12614
|
const { data, error } = await client.GET("/auth/me/tenants", {});
|
|
11805
12615
|
if (error) {
|
|
11806
|
-
fail(
|
|
12616
|
+
fail(
|
|
12617
|
+
`Couldn't list your tenants: ${JSON.stringify(error)}. Pass --tenant <id> to skip this.`
|
|
12618
|
+
);
|
|
11807
12619
|
}
|
|
11808
12620
|
const tenants = data?.tenants ?? [];
|
|
11809
12621
|
if (tenants.length === 0) {
|
|
@@ -11835,21 +12647,44 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
11835
12647
|
}
|
|
11836
12648
|
}
|
|
11837
12649
|
const existingWorkspace = local.workspaceId ?? persisted.workspaceId ?? void 0;
|
|
11838
|
-
const wsClient = await makeClient({
|
|
11839
|
-
|
|
11840
|
-
|
|
11841
|
-
|
|
11842
|
-
|
|
12650
|
+
const wsClient = await makeClient({
|
|
12651
|
+
baseUrl,
|
|
12652
|
+
tenant,
|
|
12653
|
+
account: resolveAccountAlias(),
|
|
12654
|
+
clientId: persisted.clientId
|
|
11843
12655
|
});
|
|
12656
|
+
const workspaceId = await resolveWorkspaceBinding(
|
|
12657
|
+
wsClient,
|
|
12658
|
+
existingWorkspace,
|
|
12659
|
+
{
|
|
12660
|
+
yes: opts.yes,
|
|
12661
|
+
json: opts.json,
|
|
12662
|
+
workspace: opts.workspace
|
|
12663
|
+
}
|
|
12664
|
+
);
|
|
11844
12665
|
const defaultProjectId = local.defaultProjectId ?? persisted.defaultProjectId ?? void 0;
|
|
11845
|
-
if (defaultProjectId && workspaceId)
|
|
12666
|
+
if (defaultProjectId && workspaceId)
|
|
12667
|
+
await warnIfProjectStray(
|
|
12668
|
+
wsClient,
|
|
12669
|
+
defaultProjectId,
|
|
12670
|
+
workspaceId,
|
|
12671
|
+
opts.json
|
|
12672
|
+
);
|
|
11846
12673
|
let storeLocal = Boolean(opts.local);
|
|
11847
12674
|
if (!opts.local && canPrompt() && !opts.yes) {
|
|
11848
12675
|
storeLocal = await promptSelect(
|
|
11849
12676
|
"Where should this tenant + base URL be saved?",
|
|
11850
12677
|
[
|
|
11851
|
-
{
|
|
11852
|
-
|
|
12678
|
+
{
|
|
12679
|
+
label: "Globally",
|
|
12680
|
+
value: "global",
|
|
12681
|
+
hint: "all projects on this machine"
|
|
12682
|
+
},
|
|
12683
|
+
{
|
|
12684
|
+
label: "This directory",
|
|
12685
|
+
value: "local",
|
|
12686
|
+
hint: ".sechroom.json \u2014 committed, project + subdirs"
|
|
12687
|
+
}
|
|
11853
12688
|
],
|
|
11854
12689
|
local.path ? "local" : "global"
|
|
11855
12690
|
) === "local";
|
|
@@ -11858,19 +12693,34 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
11858
12693
|
const patch = { baseUrl, tenant, ...workspaceId ? { workspaceId } : {} };
|
|
11859
12694
|
if (storeLocal) {
|
|
11860
12695
|
const path = writeLocalConfig(patch, { here: Boolean(opts.here) });
|
|
11861
|
-
if (!opts.json)
|
|
11862
|
-
|
|
12696
|
+
if (!opts.json)
|
|
12697
|
+
process.stderr.write(
|
|
12698
|
+
`${ok("\u2713")} config saved to ${path} (directory-local)
|
|
12699
|
+
`
|
|
12700
|
+
);
|
|
11863
12701
|
} else {
|
|
11864
12702
|
writePersisted(patch);
|
|
11865
|
-
if (!opts.json)
|
|
11866
|
-
|
|
12703
|
+
if (!opts.json)
|
|
12704
|
+
process.stderr.write(
|
|
12705
|
+
`${ok("\u2713")} config saved globally (~/.config/sechroom/config.json)
|
|
12706
|
+
`
|
|
12707
|
+
);
|
|
11867
12708
|
}
|
|
11868
12709
|
if (workspaceId && !existingWorkspace && !opts.json) {
|
|
11869
|
-
process.stderr.write(
|
|
11870
|
-
|
|
12710
|
+
process.stderr.write(
|
|
12711
|
+
`${ok("\u2713")} bound to workspace ${style.dim(workspaceId)}
|
|
12712
|
+
`
|
|
12713
|
+
);
|
|
11871
12714
|
}
|
|
11872
12715
|
}
|
|
11873
|
-
return {
|
|
12716
|
+
return {
|
|
12717
|
+
baseUrl,
|
|
12718
|
+
tenant,
|
|
12719
|
+
account: resolveAccountAlias(),
|
|
12720
|
+
workspaceId,
|
|
12721
|
+
defaultProjectId,
|
|
12722
|
+
clientId: persisted.clientId
|
|
12723
|
+
};
|
|
11874
12724
|
}
|
|
11875
12725
|
async function ensureAuth(cfg, yes) {
|
|
11876
12726
|
if (process.env.SECHROOM_TOKEN) return;
|
|
@@ -11878,17 +12728,27 @@ async function ensureAuth(cfg, yes) {
|
|
|
11878
12728
|
const usable = Boolean(cached?.accessToken) && (cached.expiresAt === void 0 || Date.now() < cached.expiresAt - 6e4 || Boolean(cached.refreshToken));
|
|
11879
12729
|
if (usable) return;
|
|
11880
12730
|
if (!canPrompt() || yes) {
|
|
11881
|
-
fail(
|
|
12731
|
+
fail(
|
|
12732
|
+
"Not signed in. Run `sechroom login` first, or set SECHROOM_TOKEN for headless use."
|
|
12733
|
+
);
|
|
11882
12734
|
}
|
|
11883
|
-
process.stderr.write(
|
|
12735
|
+
process.stderr.write(
|
|
12736
|
+
"\nNot signed in \u2014 opening the browser to authenticate.\n"
|
|
12737
|
+
);
|
|
11884
12738
|
await login(cfg);
|
|
11885
12739
|
}
|
|
11886
12740
|
async function ensureTimezone(cfg, opts) {
|
|
11887
12741
|
const client = await makeClient(cfg);
|
|
11888
12742
|
const { data, error } = await client.GET("/me/profile", {});
|
|
11889
|
-
if (error)
|
|
12743
|
+
if (error)
|
|
12744
|
+
return {
|
|
12745
|
+
timezone: null,
|
|
12746
|
+
action: "skipped",
|
|
12747
|
+
note: "could not read profile"
|
|
12748
|
+
};
|
|
11890
12749
|
const current = data?.effectiveTimezone;
|
|
11891
|
-
if (current && current.trim().length > 0)
|
|
12750
|
+
if (current && current.trim().length > 0)
|
|
12751
|
+
return { timezone: current, action: "already-set" };
|
|
11892
12752
|
const system = systemTimezone();
|
|
11893
12753
|
let tz = system;
|
|
11894
12754
|
if (canPrompt() && !opts.yes) {
|
|
@@ -11900,12 +12760,18 @@ async function ensureTimezone(cfg, opts) {
|
|
|
11900
12760
|
note: "no timezone set \u2014 re-run interactively or pass --yes to adopt the system timezone"
|
|
11901
12761
|
};
|
|
11902
12762
|
}
|
|
11903
|
-
if (!tz)
|
|
12763
|
+
if (!tz)
|
|
12764
|
+
return { timezone: null, action: "skipped", note: "no timezone provided" };
|
|
11904
12765
|
if (opts.dryRun) return { timezone: tz, action: "dry-run" };
|
|
11905
12766
|
const { error: putErr } = await client.PUT("/me/profile", {
|
|
11906
12767
|
body: { displayName: null, photoUrl: null, bio: null, timezone: tz }
|
|
11907
12768
|
});
|
|
11908
|
-
if (putErr)
|
|
12769
|
+
if (putErr)
|
|
12770
|
+
return {
|
|
12771
|
+
timezone: tz,
|
|
12772
|
+
action: "skipped",
|
|
12773
|
+
note: `update failed: ${JSON.stringify(putErr)}`
|
|
12774
|
+
};
|
|
11909
12775
|
return { timezone: tz, action: "set" };
|
|
11910
12776
|
}
|
|
11911
12777
|
async function chooseClients(clientFlag, yes, cwd) {
|
|
@@ -11930,7 +12796,11 @@ async function chooseScope(scopeFlag, yes) {
|
|
|
11930
12796
|
return promptSelect(
|
|
11931
12797
|
"Install skills, agents, and hooks globally or just for this project?",
|
|
11932
12798
|
[
|
|
11933
|
-
{
|
|
12799
|
+
{
|
|
12800
|
+
label: "Globally",
|
|
12801
|
+
value: "global",
|
|
12802
|
+
hint: "~/.claude (or CLAUDE_CONFIG_DIR) \u2014 all projects"
|
|
12803
|
+
},
|
|
11934
12804
|
{ label: "This project", value: "project", hint: "<repo>/.claude" }
|
|
11935
12805
|
],
|
|
11936
12806
|
"global"
|
|
@@ -11939,7 +12809,13 @@ async function chooseScope(scopeFlag, yes) {
|
|
|
11939
12809
|
async function planRecurseChild(entry, root, client, opts) {
|
|
11940
12810
|
const dir = resolveChildDir(entry.path, root);
|
|
11941
12811
|
if (!existsSync15(dir)) {
|
|
11942
|
-
return {
|
|
12812
|
+
return {
|
|
12813
|
+
label: entry.path,
|
|
12814
|
+
dir,
|
|
12815
|
+
disposition: "skip-missing",
|
|
12816
|
+
argv: [],
|
|
12817
|
+
reason: "directory does not exist"
|
|
12818
|
+
};
|
|
11943
12819
|
}
|
|
11944
12820
|
if (existsSync15(join21(dir, ".sechroom.json"))) {
|
|
11945
12821
|
return {
|
|
@@ -11960,20 +12836,40 @@ async function planRecurseChild(entry, root, client, opts) {
|
|
|
11960
12836
|
};
|
|
11961
12837
|
}
|
|
11962
12838
|
if (opts.dryRun) {
|
|
11963
|
-
return {
|
|
12839
|
+
return {
|
|
12840
|
+
label: entry.path,
|
|
12841
|
+
dir,
|
|
12842
|
+
disposition: "bind",
|
|
12843
|
+
argv: ["onboard", "--yes", "--local", "--workspace", "<prompt>"],
|
|
12844
|
+
reason: "unbound \u2014 would prompt for a workspace"
|
|
12845
|
+
};
|
|
11964
12846
|
}
|
|
11965
12847
|
if (opts.yes || !canPrompt()) {
|
|
11966
|
-
return {
|
|
12848
|
+
return {
|
|
12849
|
+
label: entry.path,
|
|
12850
|
+
dir,
|
|
12851
|
+
disposition: "skip-unbound",
|
|
12852
|
+
argv: [],
|
|
12853
|
+
reason: "unbound + no workspace (run interactively, or add it to ./.sechroom/repos.json)"
|
|
12854
|
+
};
|
|
11967
12855
|
}
|
|
11968
|
-
process.stderr.write(
|
|
12856
|
+
process.stderr.write(
|
|
12857
|
+
`
|
|
11969
12858
|
${style.bold(entry.path)} ${style.dim("is not bound yet.")}
|
|
11970
|
-
`
|
|
12859
|
+
`
|
|
12860
|
+
);
|
|
11971
12861
|
const ws = await pickWorkspace(client, {
|
|
11972
12862
|
promptLabel: `Bind ${style.cyan(entry.path)} to a workspace:`,
|
|
11973
12863
|
dirName: basename5(entry.path)
|
|
11974
12864
|
});
|
|
11975
12865
|
if (!ws) {
|
|
11976
|
-
return {
|
|
12866
|
+
return {
|
|
12867
|
+
label: entry.path,
|
|
12868
|
+
dir,
|
|
12869
|
+
disposition: "skip-unbound",
|
|
12870
|
+
argv: [],
|
|
12871
|
+
reason: "unbound \u2014 no workspace chosen (skipped)"
|
|
12872
|
+
};
|
|
11977
12873
|
}
|
|
11978
12874
|
return {
|
|
11979
12875
|
label: entry.path,
|
|
@@ -11988,20 +12884,34 @@ async function resolveFanoutLane(cfg, opts) {
|
|
|
11988
12884
|
let design = opts.designLane ?? process.env.SECHROOM_DESIGN_LANE;
|
|
11989
12885
|
if (!code || !design) {
|
|
11990
12886
|
const clients = detectInstalledClients(process.cwd());
|
|
11991
|
-
const inferred = await inferLanes(
|
|
12887
|
+
const inferred = await inferLanes(
|
|
12888
|
+
cfg,
|
|
12889
|
+
clients.length ? clients : void 0
|
|
12890
|
+
);
|
|
11992
12891
|
code = code ?? inferred.code;
|
|
11993
12892
|
design = design ?? inferred.design;
|
|
11994
12893
|
}
|
|
11995
12894
|
if (!opts.lane && !opts.yes && !opts.dryRun && canPrompt() && (code || design)) {
|
|
11996
|
-
process.stderr.write(
|
|
12895
|
+
process.stderr.write(
|
|
12896
|
+
`
|
|
11997
12897
|
This fan-out will pin the same lane in every repo:
|
|
11998
|
-
`
|
|
11999
|
-
|
|
12000
|
-
|
|
12001
|
-
|
|
12002
|
-
`)
|
|
12898
|
+
`
|
|
12899
|
+
);
|
|
12900
|
+
if (code)
|
|
12901
|
+
process.stderr.write(
|
|
12902
|
+
` ${style.dim("code-lane")} = ${style.cyan(code)}
|
|
12903
|
+
`
|
|
12904
|
+
);
|
|
12905
|
+
if (design)
|
|
12906
|
+
process.stderr.write(
|
|
12907
|
+
` ${style.dim("design-lane")} = ${style.cyan(design)}
|
|
12908
|
+
`
|
|
12909
|
+
);
|
|
12003
12910
|
if (!await promptYesNo("Use this lane for all repos?")) {
|
|
12004
|
-
code = await promptText(
|
|
12911
|
+
code = await promptText(
|
|
12912
|
+
"Code-lane id (blank = let each repo infer)?",
|
|
12913
|
+
code ?? ""
|
|
12914
|
+
) || void 0;
|
|
12005
12915
|
design = await promptText("Design-lane id (blank = skip)?", design ?? "") || void 0;
|
|
12006
12916
|
}
|
|
12007
12917
|
}
|
|
@@ -12019,30 +12929,112 @@ async function runRecurse(cfg, g, opts) {
|
|
|
12019
12929
|
const entries = fromManifest ?? discoverChildren(root).map((path) => ({ path }));
|
|
12020
12930
|
const sourceLabel = fromManifest ? `manifest ${manifestPath}` : `auto-discovered under ${root}`;
|
|
12021
12931
|
if (entries.length === 0) {
|
|
12022
|
-
if (json)
|
|
12023
|
-
|
|
12024
|
-
|
|
12932
|
+
if (json)
|
|
12933
|
+
process.stdout.write(
|
|
12934
|
+
JSON.stringify({ recurse: true, root, repos: [] }) + "\n"
|
|
12935
|
+
);
|
|
12936
|
+
else
|
|
12937
|
+
process.stderr.write(
|
|
12938
|
+
`${warn("\u26A0")} no child repos found ${fromManifest ? `in ${manifestPath}` : `under ${root}`} \u2014 nothing to do.
|
|
12939
|
+
`
|
|
12940
|
+
);
|
|
12025
12941
|
return;
|
|
12026
12942
|
}
|
|
12027
12943
|
if (!json) {
|
|
12028
|
-
process.stderr.write(
|
|
12029
|
-
`)
|
|
12944
|
+
process.stderr.write(
|
|
12945
|
+
`${style.bold("onboard --recurse")} ${style.dim(`(${entries.length} repo${entries.length === 1 ? "" : "s"} from ${sourceLabel})`)}
|
|
12946
|
+
`
|
|
12947
|
+
);
|
|
12030
12948
|
}
|
|
12031
|
-
const lane = await resolveFanoutLane(cfg, {
|
|
12032
|
-
|
|
12033
|
-
|
|
12949
|
+
const lane = await resolveFanoutLane(cfg, {
|
|
12950
|
+
lane: opts.lane,
|
|
12951
|
+
designLane: opts.designLane,
|
|
12952
|
+
yes,
|
|
12953
|
+
dryRun
|
|
12954
|
+
});
|
|
12955
|
+
if (!json && lane.code)
|
|
12956
|
+
process.stderr.write(
|
|
12957
|
+
`${ok("\u2713")} lane ${style.cyan(lane.code)}${lane.design ? ` ${style.dim(`/ ${lane.design}`)}` : ""} for every repo
|
|
12958
|
+
`
|
|
12959
|
+
);
|
|
12034
12960
|
const client = await makeClient(cfg);
|
|
12035
12961
|
const plans = [];
|
|
12036
|
-
for (const entry of entries)
|
|
12037
|
-
|
|
12962
|
+
for (const entry of entries)
|
|
12963
|
+
plans.push(await planRecurseChild(entry, root, client, { yes, dryRun }));
|
|
12964
|
+
const results = runChildren(plans, {
|
|
12965
|
+
globals: passthroughGlobals(g),
|
|
12966
|
+
dryRun,
|
|
12967
|
+
json
|
|
12968
|
+
});
|
|
12038
12969
|
if (json) {
|
|
12039
|
-
process.stdout.write(
|
|
12970
|
+
process.stdout.write(
|
|
12971
|
+
JSON.stringify({ recurse: true, root, dryRun, repos: results }) + "\n"
|
|
12972
|
+
);
|
|
12040
12973
|
return;
|
|
12041
12974
|
}
|
|
12042
12975
|
summarizeFanout(results, { dryRun });
|
|
12043
12976
|
}
|
|
12044
12977
|
function registerOnboard(program2) {
|
|
12045
|
-
program2.command("onboard").description(
|
|
12978
|
+
program2.command("onboard").description(
|
|
12979
|
+
"Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project"
|
|
12980
|
+
).option(
|
|
12981
|
+
"--recurse",
|
|
12982
|
+
"orchestration-root mode: onboard every child repo under this dir (auto-discovered, or from ./.sechroom/repos.json) \u2014 refreshes bound repos, prompts a workspace per new one",
|
|
12983
|
+
false
|
|
12984
|
+
).option(
|
|
12985
|
+
"--lane <id>",
|
|
12986
|
+
"set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo"
|
|
12987
|
+
).option(
|
|
12988
|
+
"--design-lane <id>",
|
|
12989
|
+
"set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child"
|
|
12990
|
+
).option(
|
|
12991
|
+
"--client <list...>",
|
|
12992
|
+
`clients to wire \u2014 space- or comma-separated (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`
|
|
12993
|
+
).option(
|
|
12994
|
+
"--scope <scope>",
|
|
12995
|
+
"install skills/agents/hooks 'global' (config dir / CLAUDE_CONFIG_DIR) or 'project' (<cwd>/.claude) \u2014 default: prompt, else global"
|
|
12996
|
+
).option(
|
|
12997
|
+
"--local",
|
|
12998
|
+
"save the binding (tenant + base URL + workspace) to a committed .sechroom.json in this repo instead of the global config",
|
|
12999
|
+
false
|
|
13000
|
+
).option(
|
|
13001
|
+
"--here",
|
|
13002
|
+
"with --local: write the binding at THIS directory even when a parent already carries one \u2014 binds a subtree (e.g. a monorepo's frontend/) to its own workspace",
|
|
13003
|
+
false
|
|
13004
|
+
).option(
|
|
13005
|
+
"--workspace <id>",
|
|
13006
|
+
"bind this directory to a workspace (skips the interactive workspace pick)"
|
|
13007
|
+
).option(
|
|
13008
|
+
"--cli-only",
|
|
13009
|
+
"configure the CLI only \u2014 don't wire any AI client (no MCP config, no agent files)",
|
|
13010
|
+
false
|
|
13011
|
+
).option(
|
|
13012
|
+
"--no-mcp",
|
|
13013
|
+
"skip the MCP server config (.mcp.json etc.); still write the agent instruction files"
|
|
13014
|
+
).option(
|
|
13015
|
+
"--copy",
|
|
13016
|
+
"make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)"
|
|
13017
|
+
).option(
|
|
13018
|
+
"--dry-run",
|
|
13019
|
+
"walk through without writing files or changing the profile",
|
|
13020
|
+
false
|
|
13021
|
+
).option(
|
|
13022
|
+
"--refresh",
|
|
13023
|
+
"re-fetch descriptors and refresh any out-of-date managed blocks (local edits preserved to .proposed)",
|
|
13024
|
+
false
|
|
13025
|
+
).option(
|
|
13026
|
+
"--force",
|
|
13027
|
+
"rewrite every managed block, overwriting local edits inside the markers (content outside untouched)",
|
|
13028
|
+
false
|
|
13029
|
+
).option(
|
|
13030
|
+
"--check",
|
|
13031
|
+
"report whether anything would change and exit (0 = current/stub, 1 = stale/drift/absent); writes nothing",
|
|
13032
|
+
false
|
|
13033
|
+
).option(
|
|
13034
|
+
"-y, --yes",
|
|
13035
|
+
"non-interactive: accept defaults (system timezone, detected clients, global config, full wire)",
|
|
13036
|
+
false
|
|
13037
|
+
).addHelpText(
|
|
12046
13038
|
"after",
|
|
12047
13039
|
`
|
|
12048
13040
|
Examples:
|
|
@@ -12065,20 +13057,56 @@ Examples:
|
|
|
12065
13057
|
const check = mode === "check";
|
|
12066
13058
|
const yes = Boolean(opts.yes) || check;
|
|
12067
13059
|
if (check && (opts.recurse || opts.cliOnly)) {
|
|
12068
|
-
fail(
|
|
13060
|
+
fail(
|
|
13061
|
+
"--check inspects this project's agent files and cannot be combined with --recurse or --cli-only."
|
|
13062
|
+
);
|
|
12069
13063
|
}
|
|
12070
13064
|
if (opts.lane) process.env.SECHROOM_CODE_LANE = opts.lane;
|
|
12071
13065
|
if (opts.designLane) process.env.SECHROOM_DESIGN_LANE = opts.designLane;
|
|
12072
13066
|
if (opts.recurse) {
|
|
12073
13067
|
const baseUrl2 = resolveBaseUrl(g);
|
|
12074
|
-
await ensureAuth(
|
|
12075
|
-
|
|
12076
|
-
|
|
13068
|
+
await ensureAuth(
|
|
13069
|
+
{
|
|
13070
|
+
baseUrl: baseUrl2,
|
|
13071
|
+
tenant: "",
|
|
13072
|
+
account: resolveAccountAlias(),
|
|
13073
|
+
clientId: readPersisted().clientId
|
|
13074
|
+
},
|
|
13075
|
+
yes
|
|
13076
|
+
);
|
|
13077
|
+
const cfg2 = await ensureTenant(baseUrl2, g, {
|
|
13078
|
+
yes: true,
|
|
13079
|
+
json,
|
|
13080
|
+
persist: false
|
|
13081
|
+
});
|
|
13082
|
+
await runRecurse(cfg2, g, {
|
|
13083
|
+
yes,
|
|
13084
|
+
dryRun,
|
|
13085
|
+
json,
|
|
13086
|
+
lane: opts.lane,
|
|
13087
|
+
designLane: opts.designLane
|
|
13088
|
+
});
|
|
12077
13089
|
return;
|
|
12078
13090
|
}
|
|
12079
13091
|
const baseUrl = resolveBaseUrl(g);
|
|
12080
|
-
await ensureAuth(
|
|
12081
|
-
|
|
13092
|
+
await ensureAuth(
|
|
13093
|
+
{
|
|
13094
|
+
baseUrl,
|
|
13095
|
+
tenant: "",
|
|
13096
|
+
account: resolveAccountAlias(),
|
|
13097
|
+
clientId: readPersisted().clientId
|
|
13098
|
+
},
|
|
13099
|
+
yes
|
|
13100
|
+
);
|
|
13101
|
+
const scope = await chooseScope(opts.scope, yes);
|
|
13102
|
+
const cfg = await ensureTenant(baseUrl, g, {
|
|
13103
|
+
yes,
|
|
13104
|
+
json,
|
|
13105
|
+
local: Boolean(opts.local) || scope === "project",
|
|
13106
|
+
here: scope === "project" ? true : Boolean(opts.here),
|
|
13107
|
+
workspace: opts.workspace,
|
|
13108
|
+
persist: !check
|
|
13109
|
+
});
|
|
12082
13110
|
const tz = await ensureTimezone(cfg, { yes, dryRun: dryRun || check });
|
|
12083
13111
|
if (!json && tz.action !== "already-set") {
|
|
12084
13112
|
const line = tz.action === "set" ? `${ok("\u2713")} timezone set to ${tz.timezone}
|
|
@@ -12088,21 +13116,48 @@ Examples:
|
|
|
12088
13116
|
process.stderr.write(line);
|
|
12089
13117
|
}
|
|
12090
13118
|
const wire = await chooseWire(opts, yes);
|
|
12091
|
-
const
|
|
12092
|
-
|
|
13119
|
+
const claudeTargets = resolveClaudeTargets({
|
|
13120
|
+
override: g.claudeConfigDir,
|
|
13121
|
+
scope,
|
|
13122
|
+
cwd: process.cwd()
|
|
13123
|
+
});
|
|
12093
13124
|
const codexHomes = resolveCodexHomes({ override: g.codexHome, scope });
|
|
12094
13125
|
if (scope === "project" && g.claudeConfigDir && !json) {
|
|
12095
|
-
process.stderr.write(
|
|
12096
|
-
|
|
13126
|
+
process.stderr.write(
|
|
13127
|
+
`${style.dim("(--claude-config-dir is ignored for --scope project \u2014 project files are repo-relative)")}
|
|
13128
|
+
`
|
|
13129
|
+
);
|
|
12097
13130
|
}
|
|
12098
13131
|
if (wire === "cli-only") {
|
|
12099
13132
|
if (json) {
|
|
12100
|
-
emit(
|
|
13133
|
+
emit(
|
|
13134
|
+
{
|
|
13135
|
+
dryRun,
|
|
13136
|
+
baseUrl: cfg.baseUrl,
|
|
13137
|
+
tenant: cfg.tenant,
|
|
13138
|
+
workspaceId: cfg.workspaceId ?? null,
|
|
13139
|
+
timezone: tz,
|
|
13140
|
+
wire,
|
|
13141
|
+
clients: []
|
|
13142
|
+
},
|
|
13143
|
+
true
|
|
13144
|
+
);
|
|
12101
13145
|
return;
|
|
12102
13146
|
}
|
|
12103
13147
|
if (!dryRun) {
|
|
12104
|
-
await ensureLanePin(cfg, {
|
|
12105
|
-
|
|
13148
|
+
await ensureLanePin(cfg, {
|
|
13149
|
+
yes,
|
|
13150
|
+
dryRun,
|
|
13151
|
+
clients: detectInstalledClients(process.cwd())
|
|
13152
|
+
});
|
|
13153
|
+
await maybeOfferHooks({
|
|
13154
|
+
yes,
|
|
13155
|
+
dryRun,
|
|
13156
|
+
cwd: process.cwd(),
|
|
13157
|
+
scope,
|
|
13158
|
+
claudeConfigDir: g.claudeConfigDir,
|
|
13159
|
+
codexHome: g.codexHome
|
|
13160
|
+
});
|
|
12106
13161
|
}
|
|
12107
13162
|
process.stdout.write(
|
|
12108
13163
|
`
|
|
@@ -12113,12 +13168,36 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
12113
13168
|
await printStarterPrompt("cli");
|
|
12114
13169
|
return;
|
|
12115
13170
|
}
|
|
12116
|
-
const
|
|
12117
|
-
|
|
12118
|
-
|
|
13171
|
+
const requestedKeys = await chooseClients(
|
|
13172
|
+
opts.client,
|
|
13173
|
+
yes,
|
|
13174
|
+
process.cwd()
|
|
13175
|
+
);
|
|
13176
|
+
const keys = scope === "project" ? requestedKeys.filter((key) => key !== "codex") : requestedKeys;
|
|
13177
|
+
if (scope === "project" && requestedKeys.includes("codex") && !json) {
|
|
13178
|
+
process.stderr.write(
|
|
13179
|
+
`${style.dim("Codex has no project scope \u2014 skipped (use --scope global for Codex).")}
|
|
13180
|
+
`
|
|
13181
|
+
);
|
|
13182
|
+
}
|
|
13183
|
+
const setup = await withSpinner(
|
|
13184
|
+
"Fetching setup descriptors",
|
|
13185
|
+
() => fetchSetup(cfg)
|
|
13186
|
+
);
|
|
13187
|
+
const targets = clientTargets(process.cwd(), {
|
|
13188
|
+
claudeDir: claudeTargets[0]?.dir,
|
|
13189
|
+
codexHome: codexHomes[0] ?? null
|
|
13190
|
+
});
|
|
12119
13191
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
12120
13192
|
if (!dryRun && !check) {
|
|
12121
|
-
await maybeOfferCopies(
|
|
13193
|
+
await maybeOfferCopies(
|
|
13194
|
+
cfg,
|
|
13195
|
+
setup,
|
|
13196
|
+
targets,
|
|
13197
|
+
keys,
|
|
13198
|
+
personalWorkspaceId,
|
|
13199
|
+
copyChoice(opts)
|
|
13200
|
+
);
|
|
12122
13201
|
}
|
|
12123
13202
|
const writeMcp = wire === "full";
|
|
12124
13203
|
const result = [];
|
|
@@ -12135,16 +13214,11 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
12135
13214
|
if (!json && !check) printActions(target, actions);
|
|
12136
13215
|
}
|
|
12137
13216
|
if (check) {
|
|
12138
|
-
reportCheckAndExit(
|
|
12139
|
-
|
|
12140
|
-
|
|
12141
|
-
|
|
12142
|
-
|
|
12143
|
-
baseUrl: cfg.baseUrl,
|
|
12144
|
-
tenant: cfg.tenant,
|
|
12145
|
-
workspaceId: cfg.workspaceId ?? null
|
|
12146
|
-
}
|
|
12147
|
-
);
|
|
13217
|
+
reportCheckAndExit(result, json, "sechroom onboard --refresh", {
|
|
13218
|
+
baseUrl: cfg.baseUrl,
|
|
13219
|
+
tenant: cfg.tenant,
|
|
13220
|
+
workspaceId: cfg.workspaceId ?? null
|
|
13221
|
+
});
|
|
12148
13222
|
}
|
|
12149
13223
|
const evalCounts = buildCheckReport(result).eval;
|
|
12150
13224
|
if (!json && !dryRun) {
|
|
@@ -12152,19 +13226,45 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
12152
13226
|
}
|
|
12153
13227
|
if (!json && !dryRun) {
|
|
12154
13228
|
for (const t of claudeTargets) {
|
|
12155
|
-
await maybeOfferSkills(cfg, personalWorkspaceId, {
|
|
13229
|
+
await maybeOfferSkills(cfg, personalWorkspaceId, {
|
|
13230
|
+
yes,
|
|
13231
|
+
dryRun,
|
|
13232
|
+
surface: "claude-code",
|
|
13233
|
+
configDir: t.dir
|
|
13234
|
+
});
|
|
12156
13235
|
}
|
|
12157
13236
|
}
|
|
12158
13237
|
if (!json && !dryRun) {
|
|
12159
|
-
await maybeOfferHooks({
|
|
13238
|
+
await maybeOfferHooks({
|
|
13239
|
+
yes,
|
|
13240
|
+
dryRun,
|
|
13241
|
+
cwd: process.cwd(),
|
|
13242
|
+
scope,
|
|
13243
|
+
claudeConfigDir: g.claudeConfigDir,
|
|
13244
|
+
codexHome: g.codexHome
|
|
13245
|
+
});
|
|
12160
13246
|
}
|
|
12161
13247
|
if (json) {
|
|
12162
|
-
emit(
|
|
13248
|
+
emit(
|
|
13249
|
+
{
|
|
13250
|
+
dryRun,
|
|
13251
|
+
baseUrl: cfg.baseUrl,
|
|
13252
|
+
tenant: cfg.tenant,
|
|
13253
|
+
workspaceId: cfg.workspaceId ?? null,
|
|
13254
|
+
timezone: tz,
|
|
13255
|
+
wire,
|
|
13256
|
+
eval: evalCounts,
|
|
13257
|
+
clients: result
|
|
13258
|
+
},
|
|
13259
|
+
true
|
|
13260
|
+
);
|
|
12163
13261
|
return;
|
|
12164
13262
|
}
|
|
12165
13263
|
if (!dryRun && evalCounts.stale) {
|
|
12166
|
-
process.stderr.write(
|
|
12167
|
-
|
|
13264
|
+
process.stderr.write(
|
|
13265
|
+
`${style.cyan("\u21BB")} refreshed ${evalCounts.stale} section(s) the server had moved
|
|
13266
|
+
`
|
|
13267
|
+
);
|
|
12168
13268
|
}
|
|
12169
13269
|
if (!dryRun && evalCounts.drift) {
|
|
12170
13270
|
process.stderr.write(
|
|
@@ -12173,7 +13273,9 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
12173
13273
|
`
|
|
12174
13274
|
);
|
|
12175
13275
|
}
|
|
12176
|
-
const wroteSomething = result.some(
|
|
13276
|
+
const wroteSomething = result.some(
|
|
13277
|
+
({ actions }) => actions.some((a) => a.status === "created" || a.status === "merged")
|
|
13278
|
+
);
|
|
12177
13279
|
process.stdout.write(
|
|
12178
13280
|
dryRun ? "\n(dry run \u2014 nothing written)\n" : !wroteSomething ? `
|
|
12179
13281
|
${style.bold("Done.")} Everything's already up to date.
|
|
@@ -12192,9 +13294,21 @@ async function chooseWire(opts, yes) {
|
|
|
12192
13294
|
return promptSelect(
|
|
12193
13295
|
"How should I set up Sechroom in this project?",
|
|
12194
13296
|
[
|
|
12195
|
-
{
|
|
12196
|
-
|
|
12197
|
-
|
|
13297
|
+
{
|
|
13298
|
+
label: "Wire my AI client",
|
|
13299
|
+
value: "full",
|
|
13300
|
+
hint: "MCP server (.mcp.json) + agent instructions"
|
|
13301
|
+
},
|
|
13302
|
+
{
|
|
13303
|
+
label: "Agent instructions only",
|
|
13304
|
+
value: "agent-only",
|
|
13305
|
+
hint: "skip MCP config"
|
|
13306
|
+
},
|
|
13307
|
+
{
|
|
13308
|
+
label: "CLI only",
|
|
13309
|
+
value: "cli-only",
|
|
13310
|
+
hint: "don't write any AI-client files"
|
|
13311
|
+
}
|
|
12198
13312
|
],
|
|
12199
13313
|
"full"
|
|
12200
13314
|
);
|
|
@@ -12216,7 +13330,9 @@ ${rule}
|
|
|
12216
13330
|
}
|
|
12217
13331
|
async function printStarterPrompt(mode, cfg) {
|
|
12218
13332
|
if (mode === "cli") {
|
|
12219
|
-
printNextStepBlock("Next \u2014 pick up where you left off:", [
|
|
13333
|
+
printNextStepBlock("Next \u2014 pick up where you left off:", [
|
|
13334
|
+
style.cyan("sechroom continuity resume-me")
|
|
13335
|
+
]);
|
|
12220
13336
|
return;
|
|
12221
13337
|
}
|
|
12222
13338
|
let primary = FALLBACK_AGENT_PROMPT;
|
|
@@ -12228,7 +13344,9 @@ async function printStarterPrompt(mode, cfg) {
|
|
|
12228
13344
|
} catch {
|
|
12229
13345
|
}
|
|
12230
13346
|
}
|
|
12231
|
-
printNextStepBlock("Next \u2014 paste this into your AI agent to get going:", [
|
|
13347
|
+
printNextStepBlock("Next \u2014 paste this into your AI agent to get going:", [
|
|
13348
|
+
style.cyan(`"${primary}"`)
|
|
13349
|
+
]);
|
|
12232
13350
|
}
|
|
12233
13351
|
|
|
12234
13352
|
// src/commands/project.ts
|
|
@@ -12543,7 +13661,7 @@ Examples:
|
|
|
12543
13661
|
// src/commands/reset.ts
|
|
12544
13662
|
import { homedir as homedir6 } from "os";
|
|
12545
13663
|
import { join as join22 } from "path";
|
|
12546
|
-
import { existsSync as existsSync16, readFileSync as readFileSync18, rmSync as
|
|
13664
|
+
import { existsSync as existsSync16, readFileSync as readFileSync18, rmSync as rmSync8 } from "fs";
|
|
12547
13665
|
var SKILLS_LOCK2 = ".sechroom-skills.json";
|
|
12548
13666
|
var localSkillsDir = () => join22(process.cwd(), ".claude", "skills");
|
|
12549
13667
|
var globalSkillsDir = () => join22(homedir6(), ".claude", "skills");
|
|
@@ -12559,14 +13677,14 @@ function removeMaterialisedSkills(dir) {
|
|
|
12559
13677
|
for (const name of entry.skills ?? []) {
|
|
12560
13678
|
const p = join22(dir, name);
|
|
12561
13679
|
if (existsSync16(p)) {
|
|
12562
|
-
|
|
13680
|
+
rmSync8(p, { recursive: true, force: true });
|
|
12563
13681
|
removed.push(p);
|
|
12564
13682
|
}
|
|
12565
13683
|
}
|
|
12566
13684
|
}
|
|
12567
13685
|
} catch {
|
|
12568
13686
|
}
|
|
12569
|
-
|
|
13687
|
+
rmSync8(lockPath, { force: true });
|
|
12570
13688
|
removed.push(lockPath);
|
|
12571
13689
|
return removed;
|
|
12572
13690
|
}
|
|
@@ -12605,17 +13723,17 @@ function registerReset(program2) {
|
|
|
12605
13723
|
const removed = [];
|
|
12606
13724
|
const stateDir = join22(process.cwd(), ".sechroom");
|
|
12607
13725
|
if (existsSync16(stateDir)) {
|
|
12608
|
-
|
|
13726
|
+
rmSync8(stateDir, { recursive: true, force: true });
|
|
12609
13727
|
removed.push(stateDir);
|
|
12610
13728
|
}
|
|
12611
13729
|
const legacyCfg = join22(process.cwd(), ".sechroom.json");
|
|
12612
13730
|
if (existsSync16(legacyCfg)) {
|
|
12613
|
-
|
|
13731
|
+
rmSync8(legacyCfg, { force: true });
|
|
12614
13732
|
removed.push(legacyCfg);
|
|
12615
13733
|
}
|
|
12616
13734
|
const legacySem = join22(process.cwd(), ".sem");
|
|
12617
13735
|
if (existsSync16(legacySem)) {
|
|
12618
|
-
|
|
13736
|
+
rmSync8(legacySem, { force: true });
|
|
12619
13737
|
removed.push(legacySem);
|
|
12620
13738
|
}
|
|
12621
13739
|
removed.push(...removeMaterialisedSkills(localSkillsDir()));
|
|
@@ -12640,7 +13758,7 @@ function registerReset(program2) {
|
|
|
12640
13758
|
}
|
|
12641
13759
|
|
|
12642
13760
|
// src/commands/skills.ts
|
|
12643
|
-
import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as
|
|
13761
|
+
import { existsSync as existsSync17, mkdirSync as mkdirSync18, statSync as statSync7, writeFileSync as writeFileSync17 } from "fs";
|
|
12644
13762
|
import { join as join23 } from "path";
|
|
12645
13763
|
function filenameFromDisposition(header) {
|
|
12646
13764
|
if (!header) return void 0;
|
|
@@ -12650,7 +13768,7 @@ function filenameFromDisposition(header) {
|
|
|
12650
13768
|
function resolveOutputPath(output, serverFilename) {
|
|
12651
13769
|
const filename = serverFilename || "skills.zip";
|
|
12652
13770
|
if (!output) return join23(process.cwd(), filename);
|
|
12653
|
-
const looksLikeDir = output.endsWith("/") || existsSync17(output) &&
|
|
13771
|
+
const looksLikeDir = output.endsWith("/") || existsSync17(output) && statSync7(output).isDirectory();
|
|
12654
13772
|
if (looksLikeDir) {
|
|
12655
13773
|
mkdirSync18(output, { recursive: true });
|
|
12656
13774
|
return join23(output, filename);
|