@sechroom/cli 2026.6.22 → 2026.6.24
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 +258 -86
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
|
-
import { readFileSync as
|
|
4
|
+
import { readFileSync as readFileSync8 } from "fs";
|
|
5
5
|
import { Command } from "commander";
|
|
6
6
|
|
|
7
7
|
// src/auth.ts
|
|
@@ -11,14 +11,15 @@ import open from "open";
|
|
|
11
11
|
|
|
12
12
|
// src/config.ts
|
|
13
13
|
import { homedir } from "os";
|
|
14
|
-
import { join, dirname
|
|
14
|
+
import { join, dirname } from "path";
|
|
15
15
|
import { mkdirSync, readFileSync, writeFileSync, existsSync, rmSync } from "fs";
|
|
16
16
|
var CONFIG_DIR = join(homedir(), ".config", "sechroom");
|
|
17
17
|
var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
18
18
|
var TOKEN_FILE = join(CONFIG_DIR, "token.json");
|
|
19
19
|
var STATE_DIR_NAME = ".sechroom";
|
|
20
|
-
var
|
|
21
|
-
var
|
|
20
|
+
var BASELINE_CONFIG_NAME = ".sechroom.json";
|
|
21
|
+
var OVERRIDE_CONFIG_NAME = join(STATE_DIR_NAME, "config.json");
|
|
22
|
+
var BINDING_FIELDS = ["schemaVersion", "baseUrl", "tenant", "workspaceId", "defaultProjectId"];
|
|
22
23
|
var DEFAULT_BASE_URL = "https://app.sechroom.ai/api";
|
|
23
24
|
var LOCAL_CONFIG_SCHEMA_VERSION = 2;
|
|
24
25
|
function ensureDir() {
|
|
@@ -66,54 +67,55 @@ function clearPersisted() {
|
|
|
66
67
|
rmSync(CONFIG_FILE);
|
|
67
68
|
return CONFIG_FILE;
|
|
68
69
|
}
|
|
69
|
-
function
|
|
70
|
+
function readJsonConfig(path) {
|
|
71
|
+
try {
|
|
72
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
73
|
+
} catch {
|
|
74
|
+
return void 0;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function findConfigHome(start = process.cwd()) {
|
|
70
78
|
let dir = start;
|
|
71
79
|
for (; ; ) {
|
|
72
|
-
|
|
73
|
-
if (existsSync(candidate)) return candidate;
|
|
74
|
-
const legacy = join(dir, LEGACY_LOCAL_CONFIG_NAME);
|
|
75
|
-
if (existsSync(legacy)) return legacy;
|
|
80
|
+
if (existsSync(join(dir, BASELINE_CONFIG_NAME)) || existsSync(join(dir, OVERRIDE_CONFIG_NAME))) return dir;
|
|
76
81
|
const parent = dirname(dir);
|
|
77
82
|
if (parent === dir) return void 0;
|
|
78
83
|
dir = parent;
|
|
79
84
|
}
|
|
80
85
|
}
|
|
81
|
-
function localConfigWritePath(resolvedPath) {
|
|
82
|
-
const baseDir = resolvedPath ? dirname(resolvedPath) : process.cwd();
|
|
83
|
-
const dir = basename(baseDir) === STATE_DIR_NAME ? dirname(baseDir) : baseDir;
|
|
84
|
-
return join(dir, LOCAL_CONFIG_NAME);
|
|
85
|
-
}
|
|
86
86
|
function readLocalConfig() {
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
}
|
|
100
|
-
return {};
|
|
101
|
-
}
|
|
87
|
+
const home = findConfigHome();
|
|
88
|
+
if (!home) return {};
|
|
89
|
+
const baselinePath = join(home, BASELINE_CONFIG_NAME);
|
|
90
|
+
const overridePath = join(home, OVERRIDE_CONFIG_NAME);
|
|
91
|
+
const merged = { ...readJsonConfig(baselinePath) ?? {}, ...readJsonConfig(overridePath) ?? {} };
|
|
92
|
+
return {
|
|
93
|
+
schemaVersion: merged.schemaVersion,
|
|
94
|
+
baseUrl: merged.baseUrl,
|
|
95
|
+
tenant: merged.tenant,
|
|
96
|
+
workspaceId: merged.workspaceId,
|
|
97
|
+
defaultProjectId: merged.defaultProjectId,
|
|
98
|
+
path: existsSync(baselinePath) ? baselinePath : overridePath
|
|
99
|
+
};
|
|
102
100
|
}
|
|
103
101
|
function writeLocalConfig(patch) {
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
current = JSON.parse(readFileSync(readPath, "utf8"));
|
|
109
|
-
} catch {
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
const path = localConfigWritePath(readPath);
|
|
113
|
-
mkdirSync(dirname(path), { recursive: true, mode: 448 });
|
|
102
|
+
const home = findConfigHome() ?? process.cwd();
|
|
103
|
+
const baselinePath = join(home, BASELINE_CONFIG_NAME);
|
|
104
|
+
const overridePath = join(home, OVERRIDE_CONFIG_NAME);
|
|
105
|
+
const current = readJsonConfig(baselinePath) ?? {};
|
|
114
106
|
const next = { ...current, ...patch, schemaVersion: LOCAL_CONFIG_SCHEMA_VERSION };
|
|
115
|
-
writeFileSync(
|
|
116
|
-
|
|
107
|
+
writeFileSync(baselinePath, JSON.stringify(next, null, 2), { mode: 420 });
|
|
108
|
+
const override = readJsonConfig(overridePath);
|
|
109
|
+
if (override) {
|
|
110
|
+
for (const f of BINDING_FIELDS) delete override[f];
|
|
111
|
+
if (Object.keys(override).length === 0) rmSync(overridePath, { force: true });
|
|
112
|
+
else writeFileSync(overridePath, JSON.stringify(override, null, 2), { mode: 384 });
|
|
113
|
+
}
|
|
114
|
+
return baselinePath;
|
|
115
|
+
}
|
|
116
|
+
function committedBindingPath(dir) {
|
|
117
|
+
const p = join(dir, BASELINE_CONFIG_NAME);
|
|
118
|
+
return existsSync(p) ? p : void 0;
|
|
117
119
|
}
|
|
118
120
|
function resolveConfig(flags) {
|
|
119
121
|
const local = readLocalConfig();
|
|
@@ -373,8 +375,8 @@ async function promptYesNo(question) {
|
|
|
373
375
|
const { createInterface } = await import("readline");
|
|
374
376
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
375
377
|
try {
|
|
376
|
-
const answer = await new Promise((
|
|
377
|
-
rl.question(`${question} [y/N] `,
|
|
378
|
+
const answer = await new Promise((resolve2) => {
|
|
379
|
+
rl.question(`${question} [y/N] `, resolve2);
|
|
378
380
|
});
|
|
379
381
|
return /^y(es)?$/i.test(answer.trim());
|
|
380
382
|
} finally {
|
|
@@ -387,8 +389,8 @@ async function promptText(question, def) {
|
|
|
387
389
|
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
388
390
|
try {
|
|
389
391
|
const suffix = def ? ` [${def}]` : "";
|
|
390
|
-
const answer = await new Promise((
|
|
391
|
-
rl.question(`${question}${suffix} `,
|
|
392
|
+
const answer = await new Promise((resolve2) => {
|
|
393
|
+
rl.question(`${question}${suffix} `, resolve2);
|
|
392
394
|
});
|
|
393
395
|
const trimmed = answer.trim();
|
|
394
396
|
return trimmed.length > 0 ? trimmed : def ?? "";
|
|
@@ -414,8 +416,8 @@ async function promptSelect(question, choices, def) {
|
|
|
414
416
|
process.stderr.write(` ${marker} ${style.bold(String(i + 1))}. ${c.label}${hint}
|
|
415
417
|
`);
|
|
416
418
|
});
|
|
417
|
-
const answer = await new Promise((
|
|
418
|
-
rl.question(`Choose ${style.dim(`[${defIdx + 1}]`)} `,
|
|
419
|
+
const answer = await new Promise((resolve2) => {
|
|
420
|
+
rl.question(`Choose ${style.dim(`[${defIdx + 1}]`)} `, resolve2);
|
|
419
421
|
});
|
|
420
422
|
const trimmed = answer.trim();
|
|
421
423
|
if (!trimmed) return choices[defIdx].value;
|
|
@@ -447,8 +449,8 @@ async function promptMultiSelect(question, choices, preselected = []) {
|
|
|
447
449
|
process.stderr.write(` ${box} ${style.bold(String(i + 1))}. ${c.label}${hint}
|
|
448
450
|
`);
|
|
449
451
|
});
|
|
450
|
-
const answer = await new Promise((
|
|
451
|
-
rl.question(`Select ${style.dim("[Enter = \u25C9]")} `,
|
|
452
|
+
const answer = await new Promise((resolve2) => {
|
|
453
|
+
rl.question(`Select ${style.dim("[Enter = \u25C9]")} `, resolve2);
|
|
452
454
|
});
|
|
453
455
|
const trimmed = answer.trim().toLowerCase();
|
|
454
456
|
if (!trimmed) return preValues();
|
|
@@ -1652,6 +1654,15 @@ function ignoresSem(content) {
|
|
|
1652
1654
|
return t === STATE_DIR_NAME2 || t === STATE_DIR_IGNORE || t === `/${STATE_DIR_NAME2}` || t === `/${STATE_DIR_IGNORE}` || t === `**/${STATE_DIR_NAME2}` || t === `**/${STATE_DIR_IGNORE}`;
|
|
1653
1655
|
});
|
|
1654
1656
|
}
|
|
1657
|
+
function inGitRepo(startDir) {
|
|
1658
|
+
let dir = startDir;
|
|
1659
|
+
for (; ; ) {
|
|
1660
|
+
if (existsSync2(join2(dir, ".git"))) return true;
|
|
1661
|
+
const parent = dirname2(dir);
|
|
1662
|
+
if (parent === dir) return false;
|
|
1663
|
+
dir = parent;
|
|
1664
|
+
}
|
|
1665
|
+
}
|
|
1655
1666
|
function resolveGitignoreTarget(startDir) {
|
|
1656
1667
|
let dir = startDir;
|
|
1657
1668
|
for (; ; ) {
|
|
@@ -1667,6 +1678,7 @@ function resolveGitignoreTarget(startDir) {
|
|
|
1667
1678
|
function ensureSemIgnored(semPath) {
|
|
1668
1679
|
try {
|
|
1669
1680
|
const checkoutDir = dirname2(dirname2(semPath));
|
|
1681
|
+
if (!inGitRepo(checkoutDir)) return;
|
|
1670
1682
|
const target = resolveGitignoreTarget(checkoutDir);
|
|
1671
1683
|
if (target.exists) {
|
|
1672
1684
|
const content = readFileSync2(target.path, "utf8");
|
|
@@ -1963,7 +1975,7 @@ function mergeHooks(config2) {
|
|
|
1963
1975
|
}
|
|
1964
1976
|
return added;
|
|
1965
1977
|
}
|
|
1966
|
-
function
|
|
1978
|
+
function readJsonConfig2(path) {
|
|
1967
1979
|
if (!existsSync4(path)) return {};
|
|
1968
1980
|
const raw = readFileSync3(path, "utf8");
|
|
1969
1981
|
if (!raw.trim()) return {};
|
|
@@ -1971,7 +1983,7 @@ function readJsonConfig(path) {
|
|
|
1971
1983
|
}
|
|
1972
1984
|
function installHooksJson(path, dryRun) {
|
|
1973
1985
|
const existed = existsSync4(path) && readFileSync3(path, "utf8").trim().length > 0;
|
|
1974
|
-
const config2 =
|
|
1986
|
+
const config2 = readJsonConfig2(path);
|
|
1975
1987
|
const added = mergeHooks(config2);
|
|
1976
1988
|
if (added === 0 && existed) return { path, status: "current" };
|
|
1977
1989
|
if (!dryRun) {
|
|
@@ -3104,7 +3116,7 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
3104
3116
|
"Where should this tenant + base URL be saved?",
|
|
3105
3117
|
[
|
|
3106
3118
|
{ label: "Globally", value: "global", hint: "all projects on this machine" },
|
|
3107
|
-
{ label: "This directory", value: "local", hint: ".sechroom
|
|
3119
|
+
{ label: "This directory", value: "local", hint: ".sechroom.json \u2014 committed, project + subdirs" }
|
|
3108
3120
|
],
|
|
3109
3121
|
local.path ? "local" : "global"
|
|
3110
3122
|
) === "local";
|
|
@@ -3180,14 +3192,14 @@ async function chooseClients(clientFlag, yes, cwd) {
|
|
|
3180
3192
|
return picks.length > 0 ? picks : preselected;
|
|
3181
3193
|
}
|
|
3182
3194
|
function registerOnboard(program2) {
|
|
3183
|
-
program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--client <list>", `comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`).option("--local", "save tenant + base URL to a
|
|
3195
|
+
program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--client <list>", `comma-separated clients (${ALL_CLIENT_KEYS.join(", ")}) or 'all' (default: auto-detected)`).option("--local", "save the binding (tenant + base URL + workspace) to a committed .sechroom.json in this repo instead of the global config", false).option("--workspace <id>", "bind this directory to a workspace (skips the interactive workspace pick)").option("--cli-only", "configure the CLI only \u2014 don't wire any AI client (no MCP config, no agent files)", false).option("--no-mcp", "skip the MCP server config (.mcp.json etc.); still write the agent instruction files").option("--copy", "make a personal copy of the agent instructions you can edit (default: prompt on a TTY, else skip)").option("--dry-run", "walk through without writing files or changing the profile", false).option("--refresh", "re-fetch descriptors and refresh any out-of-date managed blocks (local edits preserved to .proposed)", false).option("--force", "rewrite every managed block, overwriting local edits inside the markers (content outside untouched)", false).option("--check", "report whether anything would change and exit (0 = all current, 1 = stale/drift/absent); writes nothing", false).option("-y, --yes", "non-interactive: accept defaults (system timezone, detected clients, global config, full wire)", false).addHelpText(
|
|
3184
3196
|
"after",
|
|
3185
3197
|
`
|
|
3186
3198
|
Examples:
|
|
3187
3199
|
$ sechroom onboard guided, interactive (asks where to save config + how to wire)
|
|
3188
3200
|
$ sechroom onboard --cli-only just the CLI \u2014 no .mcp.json, no agent files
|
|
3189
3201
|
$ sechroom onboard --no-mcp agent instructions only, skip MCP config
|
|
3190
|
-
$ sechroom onboard --local save tenant + base URL to ./.sechroom
|
|
3202
|
+
$ sechroom onboard --local save tenant + base URL to a committed ./.sechroom.json
|
|
3191
3203
|
$ sechroom onboard --workspace wsp_XX bind this directory to a workspace (no pick prompt)
|
|
3192
3204
|
$ sechroom onboard --refresh refresh out-of-date instruction blocks in place
|
|
3193
3205
|
$ sechroom onboard --check CI/pre-commit: nonzero exit if instructions are out of date
|
|
@@ -3352,15 +3364,174 @@ ${style.bold("Next:")} paste this into your AI agent to get going \u2014
|
|
|
3352
3364
|
);
|
|
3353
3365
|
}
|
|
3354
3366
|
|
|
3367
|
+
// src/commands/sweep.ts
|
|
3368
|
+
import { spawnSync } from "child_process";
|
|
3369
|
+
import { existsSync as existsSync6, readFileSync as readFileSync5 } from "fs";
|
|
3370
|
+
import { dirname as dirname6, isAbsolute, join as join6, resolve } from "path";
|
|
3371
|
+
var DEFAULT_MANIFEST = join6(".sechroom", "repos.json");
|
|
3372
|
+
function planEntry(entry, root) {
|
|
3373
|
+
const dir = isAbsolute(entry.path) ? entry.path : resolve(root, entry.path);
|
|
3374
|
+
if (!existsSync6(dir)) {
|
|
3375
|
+
return { entry, dir, disposition: "skip-missing", argv: [], reason: "directory does not exist" };
|
|
3376
|
+
}
|
|
3377
|
+
if (committedBindingPath(dir)) {
|
|
3378
|
+
return {
|
|
3379
|
+
entry,
|
|
3380
|
+
dir,
|
|
3381
|
+
disposition: "refresh",
|
|
3382
|
+
argv: ["onboard", "--refresh", "--yes"],
|
|
3383
|
+
reason: "bound (committed .sechroom.json) \u2014 refresh in place"
|
|
3384
|
+
};
|
|
3385
|
+
}
|
|
3386
|
+
if (entry.workspaceId) {
|
|
3387
|
+
return {
|
|
3388
|
+
entry,
|
|
3389
|
+
dir,
|
|
3390
|
+
disposition: "bind",
|
|
3391
|
+
argv: ["onboard", "--yes", "--local", "--workspace", entry.workspaceId],
|
|
3392
|
+
reason: `unbound \u2014 bind to ${entry.workspaceId} + commit .sechroom.json`
|
|
3393
|
+
};
|
|
3394
|
+
}
|
|
3395
|
+
return {
|
|
3396
|
+
entry,
|
|
3397
|
+
dir,
|
|
3398
|
+
disposition: "skip-unbound",
|
|
3399
|
+
argv: [],
|
|
3400
|
+
reason: "unbound + no workspaceId in the manifest \u2014 add one or run `sechroom onboard` there"
|
|
3401
|
+
};
|
|
3402
|
+
}
|
|
3403
|
+
function passthroughGlobals(g) {
|
|
3404
|
+
const out = [];
|
|
3405
|
+
if (g.baseUrl) out.push("--base-url", g.baseUrl);
|
|
3406
|
+
if (g.tenant) out.push("--tenant", g.tenant);
|
|
3407
|
+
return out;
|
|
3408
|
+
}
|
|
3409
|
+
var ICON = {
|
|
3410
|
+
refresh: "\u21BB",
|
|
3411
|
+
bind: "+",
|
|
3412
|
+
"skip-missing": "\u2013",
|
|
3413
|
+
"skip-unbound": "\u26A0"
|
|
3414
|
+
};
|
|
3415
|
+
function registerSweep(program2) {
|
|
3416
|
+
program2.command("sweep").description("Orchestration-root fan-out: run `sechroom onboard` in every repo listed in ./.sechroom/repos.json").option("--manifest <path>", "path to the repos manifest", DEFAULT_MANIFEST).option("--dry-run", "print the plan (per-repo disposition + the onboard command) without running anything", false).addHelpText(
|
|
3417
|
+
"after",
|
|
3418
|
+
`
|
|
3419
|
+
Manifest \u2014 ./.sechroom/repos.json (per-operator, gitignored, alongside lane.json):
|
|
3420
|
+
{
|
|
3421
|
+
"repos": [
|
|
3422
|
+
{ "path": "sechroom", "workspaceId": "wsp_XXXX" },
|
|
3423
|
+
{ "path": "../other-repo", "workspaceId": "wsp_YYYY" },
|
|
3424
|
+
{ "path": "already-bound" }
|
|
3425
|
+
]
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
Per repo (paths resolve relative to the manifest's root):
|
|
3429
|
+
${ICON.refresh} bound committed .sechroom.json present \u2192 onboard --refresh (manifest workspace ignored)
|
|
3430
|
+
${ICON.bind} unbound bind to the manifest workspaceId \u2192 onboard --local --workspace <id>
|
|
3431
|
+
${ICON["skip-missing"]} missing directory does not exist \u2192 skipped
|
|
3432
|
+
${ICON["skip-unbound"]} no workspace unbound + no workspaceId in manifest \u2192 skipped (add one, or onboard manually)
|
|
3433
|
+
|
|
3434
|
+
Examples:
|
|
3435
|
+
$ sechroom sweep --dry-run preview every repo's disposition, run nothing
|
|
3436
|
+
$ sechroom sweep onboard the whole tree from the root
|
|
3437
|
+
$ sechroom --tenant ocd sweep force a tenant for every child (else each resolves its own)`
|
|
3438
|
+
).action((opts, cmd) => {
|
|
3439
|
+
const g = cmd.optsWithGlobals();
|
|
3440
|
+
const json = Boolean(g.json);
|
|
3441
|
+
const dryRun = Boolean(opts.dryRun);
|
|
3442
|
+
const manifestPath = resolve(opts.manifest);
|
|
3443
|
+
if (!existsSync6(manifestPath)) {
|
|
3444
|
+
fail(`no manifest at ${manifestPath} \u2014 create ./.sechroom/repos.json listing the repos under this root (see \`sechroom sweep --help\`).`);
|
|
3445
|
+
}
|
|
3446
|
+
let manifest;
|
|
3447
|
+
try {
|
|
3448
|
+
manifest = JSON.parse(readFileSync5(manifestPath, "utf8"));
|
|
3449
|
+
} catch (err2) {
|
|
3450
|
+
fail(`couldn't parse ${manifestPath}: ${err2 instanceof Error ? err2.message : String(err2)}`);
|
|
3451
|
+
}
|
|
3452
|
+
const repos = Array.isArray(manifest.repos) ? manifest.repos.filter((r) => r && typeof r.path === "string") : [];
|
|
3453
|
+
if (repos.length === 0) {
|
|
3454
|
+
if (json) {
|
|
3455
|
+
process.stdout.write(JSON.stringify({ manifest: manifestPath, repos: [] }) + "\n");
|
|
3456
|
+
} else {
|
|
3457
|
+
process.stderr.write(`${warn("\u26A0")} ${manifestPath} lists no repos \u2014 nothing to do.
|
|
3458
|
+
`);
|
|
3459
|
+
}
|
|
3460
|
+
return;
|
|
3461
|
+
}
|
|
3462
|
+
const root = dirname6(dirname6(manifestPath));
|
|
3463
|
+
const globals = passthroughGlobals(g);
|
|
3464
|
+
const plans = repos.map((entry) => planEntry(entry, root));
|
|
3465
|
+
if (!json) {
|
|
3466
|
+
process.stderr.write(
|
|
3467
|
+
`${style.bold("sweep")} ${style.dim(`(${plans.length} repo${plans.length === 1 ? "" : "s"} from ${manifestPath})`)}
|
|
3468
|
+
`
|
|
3469
|
+
);
|
|
3470
|
+
}
|
|
3471
|
+
const results = [];
|
|
3472
|
+
for (const plan of plans) {
|
|
3473
|
+
const runs = plan.argv.length > 0;
|
|
3474
|
+
const argv = runs ? [...globals, ...plan.argv] : [];
|
|
3475
|
+
const skipped2 = !runs;
|
|
3476
|
+
if (!json) {
|
|
3477
|
+
const head = ` ${ICON[plan.disposition]} ${style.cyan(plan.entry.path)} ${style.dim(plan.reason)}`;
|
|
3478
|
+
process.stderr.write(head + "\n");
|
|
3479
|
+
if (runs && dryRun) process.stderr.write(` ${style.dim(`would run: sechroom ${argv.join(" ")}`)}
|
|
3480
|
+
`);
|
|
3481
|
+
}
|
|
3482
|
+
let exitCode = skipped2 ? null : 0;
|
|
3483
|
+
if (runs && !dryRun) {
|
|
3484
|
+
const res = spawnSync(process.execPath, [process.argv[1], ...argv], {
|
|
3485
|
+
cwd: plan.dir,
|
|
3486
|
+
stdio: json ? "ignore" : "inherit"
|
|
3487
|
+
});
|
|
3488
|
+
exitCode = res.status;
|
|
3489
|
+
if (!json) {
|
|
3490
|
+
process.stderr.write(
|
|
3491
|
+
exitCode === 0 ? ` ${ok("\u2713")} ${style.dim("onboard ok")}
|
|
3492
|
+
` : ` ${warn("\u2717")} ${style.dim(`onboard exited ${exitCode ?? "signal"}`)}
|
|
3493
|
+
`
|
|
3494
|
+
);
|
|
3495
|
+
}
|
|
3496
|
+
}
|
|
3497
|
+
results.push({
|
|
3498
|
+
path: plan.entry.path,
|
|
3499
|
+
dir: plan.dir,
|
|
3500
|
+
disposition: plan.disposition,
|
|
3501
|
+
ran: runs && !dryRun,
|
|
3502
|
+
exitCode,
|
|
3503
|
+
reason: plan.reason
|
|
3504
|
+
});
|
|
3505
|
+
}
|
|
3506
|
+
if (json) {
|
|
3507
|
+
process.stdout.write(JSON.stringify({ manifest: manifestPath, dryRun, repos: results }) + "\n");
|
|
3508
|
+
return;
|
|
3509
|
+
}
|
|
3510
|
+
const ran = results.filter((r) => r.ran);
|
|
3511
|
+
const failed = ran.filter((r) => r.exitCode !== 0);
|
|
3512
|
+
const skipped = results.filter((r) => r.disposition.startsWith("skip"));
|
|
3513
|
+
const wouldRun = results.filter((r) => !r.disposition.startsWith("skip"));
|
|
3514
|
+
const tally = (dryRun ? [wouldRun.length ? `${wouldRun.length} would onboard` : null, skipped.length ? `${skipped.length} would skip` : null] : [
|
|
3515
|
+
ran.length ? `${ran.length - failed.length}/${ran.length} onboarded` : null,
|
|
3516
|
+
skipped.length ? `${skipped.length} skipped` : null,
|
|
3517
|
+
failed.length ? `${failed.length} failed` : null
|
|
3518
|
+
]).filter(Boolean).join(", ");
|
|
3519
|
+
process.stderr.write(`
|
|
3520
|
+
${failed.length ? warn("\u26A0") : ok("\u2713")} ${tally || "nothing to do"}${dryRun ? style.dim(" (dry run)") : ""}
|
|
3521
|
+
`);
|
|
3522
|
+
if (failed.length) process.exit(1);
|
|
3523
|
+
});
|
|
3524
|
+
}
|
|
3525
|
+
|
|
3355
3526
|
// src/commands/skills.ts
|
|
3356
3527
|
import { homedir as homedir6 } from "os";
|
|
3357
|
-
import { join as
|
|
3358
|
-
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, rmSync as rmSync2, existsSync as
|
|
3528
|
+
import { join as join7 } from "path";
|
|
3529
|
+
import { mkdirSync as mkdirSync6, writeFileSync as writeFileSync6, rmSync as rmSync2, existsSync as existsSync7, readFileSync as readFileSync6 } from "fs";
|
|
3359
3530
|
var DEFAULT_SLUG = "operator-skills";
|
|
3360
3531
|
var ROLE_TAGS = ["sechroom:role:skill-template", "role:skill-template"];
|
|
3361
3532
|
var LOCK = ".sechroom-skills.json";
|
|
3362
3533
|
function skillsDir(global) {
|
|
3363
|
-
return global ?
|
|
3534
|
+
return global ? join7(homedir6(), ".claude", "skills") : join7(process.cwd(), ".claude", "skills");
|
|
3364
3535
|
}
|
|
3365
3536
|
function tagValue2(tags, prefix) {
|
|
3366
3537
|
return (tags ?? []).find((t) => t.startsWith(prefix))?.slice(prefix.length);
|
|
@@ -3438,13 +3609,13 @@ Examples:
|
|
|
3438
3609
|
const name = tagValue2(tags, "skill:");
|
|
3439
3610
|
if (!name) continue;
|
|
3440
3611
|
const body = m.text ?? m.Text ?? "";
|
|
3441
|
-
mkdirSync6(
|
|
3442
|
-
writeFileSync6(
|
|
3612
|
+
mkdirSync6(join7(dir, name), { recursive: true });
|
|
3613
|
+
writeFileSync6(join7(dir, name, "SKILL.md"), body.endsWith("\n") ? body : body + "\n");
|
|
3443
3614
|
written.push(name);
|
|
3444
3615
|
}
|
|
3445
3616
|
mkdirSync6(dir, { recursive: true });
|
|
3446
|
-
const lockPath =
|
|
3447
|
-
const lock =
|
|
3617
|
+
const lockPath = join7(dir, LOCK);
|
|
3618
|
+
const lock = existsSync7(lockPath) ? JSON.parse(readFileSync6(lockPath, "utf8")) : {};
|
|
3448
3619
|
lock[slug] = { surface: opts.surface, version, instance: wantInstance, skills: written.sort() };
|
|
3449
3620
|
writeFileSync6(lockPath, JSON.stringify(lock, null, 2) + "\n");
|
|
3450
3621
|
if (opts.json) return emit({ slug, version, instance: wantInstance, surface: opts.surface, dir, installed: written }, true);
|
|
@@ -3468,15 +3639,15 @@ Examples:
|
|
|
3468
3639
|
skills.command("clean [slug]").description(`Remove materialised skill files written by install (default ${DEFAULT_SLUG})`).option("--local", "clean ./.claude/skills instead of ~/.claude/skills").option("--json", "machine output").action(async (slugArg, opts) => {
|
|
3469
3640
|
const slug = slugArg || DEFAULT_SLUG;
|
|
3470
3641
|
const dir = skillsDir(!opts.local);
|
|
3471
|
-
const lockPath =
|
|
3472
|
-
if (!
|
|
3473
|
-
const lock = JSON.parse(
|
|
3642
|
+
const lockPath = join7(dir, LOCK);
|
|
3643
|
+
if (!existsSync7(lockPath)) fail(`No skills lockfile at ${lockPath}; nothing to clean.`);
|
|
3644
|
+
const lock = JSON.parse(readFileSync6(lockPath, "utf8"));
|
|
3474
3645
|
const entry = lock[slug];
|
|
3475
3646
|
if (!entry) fail(`No installed record for '${slug}' in ${lockPath}.`);
|
|
3476
3647
|
const removed = [];
|
|
3477
3648
|
for (const name of entry.skills) {
|
|
3478
|
-
const skillPath =
|
|
3479
|
-
if (
|
|
3649
|
+
const skillPath = join7(dir, name);
|
|
3650
|
+
if (existsSync7(skillPath)) {
|
|
3480
3651
|
rmSync2(skillPath, { recursive: true, force: true });
|
|
3481
3652
|
removed.push(name);
|
|
3482
3653
|
}
|
|
@@ -3572,21 +3743,21 @@ Examples:
|
|
|
3572
3743
|
|
|
3573
3744
|
// src/commands/reset.ts
|
|
3574
3745
|
import { homedir as homedir7 } from "os";
|
|
3575
|
-
import { join as
|
|
3576
|
-
import { existsSync as
|
|
3746
|
+
import { join as join8 } from "path";
|
|
3747
|
+
import { existsSync as existsSync8, readFileSync as readFileSync7, rmSync as rmSync3 } from "fs";
|
|
3577
3748
|
var SKILLS_LOCK = ".sechroom-skills.json";
|
|
3578
|
-
var localSkillsDir = () =>
|
|
3579
|
-
var globalSkillsDir = () =>
|
|
3749
|
+
var localSkillsDir = () => join8(process.cwd(), ".claude", "skills");
|
|
3750
|
+
var globalSkillsDir = () => join8(homedir7(), ".claude", "skills");
|
|
3580
3751
|
function removeMaterialisedSkills(dir) {
|
|
3581
3752
|
const removed = [];
|
|
3582
|
-
const lockPath =
|
|
3583
|
-
if (!
|
|
3753
|
+
const lockPath = join8(dir, SKILLS_LOCK);
|
|
3754
|
+
if (!existsSync8(lockPath)) return removed;
|
|
3584
3755
|
try {
|
|
3585
|
-
const lock = JSON.parse(
|
|
3756
|
+
const lock = JSON.parse(readFileSync7(lockPath, "utf8"));
|
|
3586
3757
|
for (const entry of Object.values(lock)) {
|
|
3587
3758
|
for (const name of entry.skills ?? []) {
|
|
3588
|
-
const p =
|
|
3589
|
-
if (
|
|
3759
|
+
const p = join8(dir, name);
|
|
3760
|
+
if (existsSync8(p)) {
|
|
3590
3761
|
rmSync3(p, { recursive: true, force: true });
|
|
3591
3762
|
removed.push(p);
|
|
3592
3763
|
}
|
|
@@ -3617,18 +3788,18 @@ function registerReset(program2) {
|
|
|
3617
3788
|
}
|
|
3618
3789
|
}
|
|
3619
3790
|
const removed = [];
|
|
3620
|
-
const stateDir =
|
|
3621
|
-
if (
|
|
3791
|
+
const stateDir = join8(process.cwd(), ".sechroom");
|
|
3792
|
+
if (existsSync8(stateDir)) {
|
|
3622
3793
|
rmSync3(stateDir, { recursive: true, force: true });
|
|
3623
3794
|
removed.push(stateDir);
|
|
3624
3795
|
}
|
|
3625
|
-
const legacyCfg =
|
|
3626
|
-
if (
|
|
3796
|
+
const legacyCfg = join8(process.cwd(), ".sechroom.json");
|
|
3797
|
+
if (existsSync8(legacyCfg)) {
|
|
3627
3798
|
rmSync3(legacyCfg, { force: true });
|
|
3628
3799
|
removed.push(legacyCfg);
|
|
3629
3800
|
}
|
|
3630
|
-
const legacySem =
|
|
3631
|
-
if (
|
|
3801
|
+
const legacySem = join8(process.cwd(), ".sem");
|
|
3802
|
+
if (existsSync8(legacySem)) {
|
|
3632
3803
|
rmSync3(legacySem, { force: true });
|
|
3633
3804
|
removed.push(legacySem);
|
|
3634
3805
|
}
|
|
@@ -3655,7 +3826,7 @@ function registerReset(program2) {
|
|
|
3655
3826
|
function resolveVersion() {
|
|
3656
3827
|
try {
|
|
3657
3828
|
const pkg = JSON.parse(
|
|
3658
|
-
|
|
3829
|
+
readFileSync8(new URL("../package.json", import.meta.url), "utf8")
|
|
3659
3830
|
);
|
|
3660
3831
|
return pkg.version ?? "0.0.0";
|
|
3661
3832
|
} catch {
|
|
@@ -3671,7 +3842,7 @@ Examples:
|
|
|
3671
3842
|
$ sechroom onboard guided first-run: configure, sign in, wire this project
|
|
3672
3843
|
$ sechroom login sign in via browser (OAuth + PKCE)
|
|
3673
3844
|
$ sechroom config set tenant ocd set your tenant (global)
|
|
3674
|
-
$ sechroom config set --local tenant cli-smoke pin tenant for this directory (.sechroom
|
|
3845
|
+
$ sechroom config set --local tenant cli-smoke pin tenant for this directory (committed .sechroom.json)
|
|
3675
3846
|
$ sechroom config show resolved config + which source won
|
|
3676
3847
|
|
|
3677
3848
|
$ sechroom memory create --text "a note" --title "Note" --tag idea
|
|
@@ -3683,7 +3854,7 @@ Examples:
|
|
|
3683
3854
|
$ sechroom --json memory search "auth" compact JSON for scripts and agents
|
|
3684
3855
|
$ SECHROOM_TOKEN=<bearer> sechroom --json memory get mem_XXXX headless
|
|
3685
3856
|
|
|
3686
|
-
Config precedence (high -> low): --flag > env (SECHROOM_*) > ./.sechroom
|
|
3857
|
+
Config precedence (high -> low): --flag > env (SECHROOM_*) > directory-local (committed ./.sechroom.json, shadowed per-field by the gitignored ./.sechroom/config.json override) > global > default.
|
|
3687
3858
|
Run 'sechroom <command> --help' for command-specific examples.`
|
|
3688
3859
|
);
|
|
3689
3860
|
program.hook("preAction", (_thisCmd, actionCmd) => {
|
|
@@ -3709,11 +3880,11 @@ config.addHelpText(
|
|
|
3709
3880
|
Examples:
|
|
3710
3881
|
$ sechroom config set baseUrl https://app.sechroom.ai/api prod (staging: https://staging.app.sechroom.ai/api)
|
|
3711
3882
|
$ sechroom config set tenant ocd
|
|
3712
|
-
$ sechroom config set --local tenant cli-smoke this dir + subdirs (.sechroom
|
|
3883
|
+
$ sechroom config set --local tenant cli-smoke this dir + subdirs (committed .sechroom.json)
|
|
3713
3884
|
$ sechroom config set clientId dyn-XXXX global-only escape hatch (no DCR endpoint)
|
|
3714
3885
|
$ sechroom config show --json`
|
|
3715
3886
|
);
|
|
3716
|
-
config.command("set <key> <value>").description("Set baseUrl | tenant | workspaceId | defaultProjectId | clientId (clientId is global-only)").option("--local", "Write
|
|
3887
|
+
config.command("set <key> <value>").description("Set baseUrl | tenant | workspaceId | defaultProjectId | clientId (clientId is global-only)").option("--local", "Write the committed directory-local .sechroom.json (nearest up the tree, else cwd) instead of the global config").action((key, value, opts) => {
|
|
3717
3888
|
if (opts.local) {
|
|
3718
3889
|
if (!["baseUrl", "tenant", "workspaceId", "defaultProjectId"].includes(key)) {
|
|
3719
3890
|
process.stderr.write(`--local supports only: baseUrl | tenant | workspaceId | defaultProjectId (clientId is global)
|
|
@@ -3772,6 +3943,7 @@ registerChat(program);
|
|
|
3772
3943
|
registerInit(program);
|
|
3773
3944
|
registerSetup(program);
|
|
3774
3945
|
registerOnboard(program);
|
|
3946
|
+
registerSweep(program);
|
|
3775
3947
|
registerSkills(program);
|
|
3776
3948
|
registerReset(program);
|
|
3777
3949
|
program.parseAsync().catch((err2) => {
|
package/package.json
CHANGED