@sechroom/cli 2026.6.27 → 2026.6.28
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 +260 -44
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -16,12 +16,14 @@ 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
|
+
var ACCOUNTS_FILE = join(CONFIG_DIR, "accounts.json");
|
|
20
|
+
var DEFAULT_ACCOUNT = "default";
|
|
19
21
|
var STATE_DIR_NAME = ".sechroom";
|
|
20
22
|
var BASELINE_CONFIG_NAME = ".sechroom.json";
|
|
21
23
|
var OVERRIDE_CONFIG_NAME = join(STATE_DIR_NAME, "config.json");
|
|
22
|
-
var BINDING_FIELDS = ["schemaVersion", "baseUrl", "tenant", "workspaceId", "defaultProjectId"];
|
|
24
|
+
var BINDING_FIELDS = ["schemaVersion", "baseUrl", "tenant", "workspaceId", "defaultProjectId", "workspaces"];
|
|
23
25
|
var DEFAULT_BASE_URL = "https://app.sechroom.ai/api";
|
|
24
|
-
var LOCAL_CONFIG_SCHEMA_VERSION =
|
|
26
|
+
var LOCAL_CONFIG_SCHEMA_VERSION = 3;
|
|
25
27
|
function ensureDir() {
|
|
26
28
|
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
27
29
|
}
|
|
@@ -44,23 +46,87 @@ function writeDcrClientId(baseUrl, clientId) {
|
|
|
44
46
|
const p = readPersisted();
|
|
45
47
|
writePersisted({ clientIds: { ...p.clientIds ?? {}, [baseUrl]: clientId } });
|
|
46
48
|
}
|
|
47
|
-
function
|
|
48
|
-
const envTok = process.env.SECHROOM_TOKEN;
|
|
49
|
-
if (envTok) return { accessToken: envTok };
|
|
49
|
+
function readAccountsFile() {
|
|
50
50
|
try {
|
|
51
|
-
|
|
51
|
+
const parsed = JSON.parse(readFileSync(ACCOUNTS_FILE, "utf8"));
|
|
52
|
+
if (parsed && typeof parsed === "object" && parsed.accounts) return parsed;
|
|
52
53
|
} catch {
|
|
53
|
-
return null;
|
|
54
54
|
}
|
|
55
|
+
try {
|
|
56
|
+
const legacy = JSON.parse(readFileSync(TOKEN_FILE, "utf8"));
|
|
57
|
+
if (legacy?.accessToken) {
|
|
58
|
+
const migrated = {
|
|
59
|
+
accounts: {
|
|
60
|
+
[DEFAULT_ACCOUNT]: { ...legacy, baseUrl: readPersisted().baseUrl ?? DEFAULT_BASE_URL }
|
|
61
|
+
},
|
|
62
|
+
default: DEFAULT_ACCOUNT
|
|
63
|
+
};
|
|
64
|
+
writeAccountsFile(migrated);
|
|
65
|
+
rmSync(TOKEN_FILE, { force: true });
|
|
66
|
+
return migrated;
|
|
67
|
+
}
|
|
68
|
+
} catch {
|
|
69
|
+
}
|
|
70
|
+
return { accounts: {} };
|
|
55
71
|
}
|
|
56
|
-
function
|
|
72
|
+
function writeAccountsFile(file) {
|
|
57
73
|
ensureDir();
|
|
58
|
-
writeFileSync(
|
|
74
|
+
writeFileSync(ACCOUNTS_FILE, JSON.stringify(file, null, 2), { mode: 384 });
|
|
75
|
+
}
|
|
76
|
+
function readAccount(alias) {
|
|
77
|
+
return readAccountsFile().accounts[alias] ?? null;
|
|
78
|
+
}
|
|
79
|
+
function writeAccount(alias, entry) {
|
|
80
|
+
const file = readAccountsFile();
|
|
81
|
+
file.accounts[alias] = entry;
|
|
82
|
+
if (!file.default) file.default = alias;
|
|
83
|
+
writeAccountsFile(file);
|
|
84
|
+
}
|
|
85
|
+
function removeAccount(alias) {
|
|
86
|
+
const file = readAccountsFile();
|
|
87
|
+
if (!(alias in file.accounts)) return false;
|
|
88
|
+
delete file.accounts[alias];
|
|
89
|
+
if (file.default === alias) file.default = Object.keys(file.accounts)[0];
|
|
90
|
+
writeAccountsFile(file);
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
function listAccounts() {
|
|
94
|
+
const file = readAccountsFile();
|
|
95
|
+
return Object.entries(file.accounts).map(([alias, entry]) => ({
|
|
96
|
+
alias,
|
|
97
|
+
entry,
|
|
98
|
+
isDefault: file.default === alias
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
function setDefaultAccount(alias) {
|
|
102
|
+
const file = readAccountsFile();
|
|
103
|
+
if (!(alias in file.accounts)) throw new Error(`No account "${alias}". Run \`sechroom login --account ${alias}\` first.`);
|
|
104
|
+
file.default = alias;
|
|
105
|
+
writeAccountsFile(file);
|
|
106
|
+
}
|
|
107
|
+
function resolveAccountAlias(flagAccount) {
|
|
108
|
+
return flagAccount ?? process.env.SECHROOM_ACCOUNT ?? readLocalConfig().account ?? readAccountsFile().default ?? DEFAULT_ACCOUNT;
|
|
109
|
+
}
|
|
110
|
+
function writeLocalAccount(alias) {
|
|
111
|
+
const home = findConfigHome() ?? process.cwd();
|
|
112
|
+
const overridePath = join(home, OVERRIDE_CONFIG_NAME);
|
|
113
|
+
mkdirSync(dirname(overridePath), { recursive: true });
|
|
114
|
+
const current = readJsonConfig(overridePath) ?? {};
|
|
115
|
+
writeFileSync(
|
|
116
|
+
overridePath,
|
|
117
|
+
JSON.stringify({ ...current, account: alias }, null, 2),
|
|
118
|
+
{ mode: 384 }
|
|
119
|
+
);
|
|
120
|
+
return overridePath;
|
|
121
|
+
}
|
|
122
|
+
function readToken() {
|
|
123
|
+
const envTok = process.env.SECHROOM_TOKEN;
|
|
124
|
+
if (envTok) return { accessToken: envTok };
|
|
125
|
+
return readAccount(resolveAccountAlias());
|
|
59
126
|
}
|
|
60
127
|
function clearToken() {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
return TOKEN_FILE;
|
|
128
|
+
const alias = resolveAccountAlias();
|
|
129
|
+
return removeAccount(alias) ? `${ACCOUNTS_FILE} (account "${alias}")` : void 0;
|
|
64
130
|
}
|
|
65
131
|
function clearPersisted() {
|
|
66
132
|
if (!existsSync(CONFIG_FILE)) return void 0;
|
|
@@ -95,11 +161,14 @@ function readLocalConfig() {
|
|
|
95
161
|
tenant: merged.tenant,
|
|
96
162
|
workspaceId: merged.workspaceId,
|
|
97
163
|
defaultProjectId: merged.defaultProjectId,
|
|
98
|
-
|
|
164
|
+
workspaces: merged.workspaces,
|
|
165
|
+
account: merged.account,
|
|
166
|
+
path: existsSync(baselinePath) ? baselinePath : overridePath,
|
|
167
|
+
home
|
|
99
168
|
};
|
|
100
169
|
}
|
|
101
|
-
function writeLocalConfig(patch) {
|
|
102
|
-
const home = findConfigHome() ?? process.cwd();
|
|
170
|
+
function writeLocalConfig(patch, opts) {
|
|
171
|
+
const home = (opts?.here ? process.cwd() : findConfigHome()) ?? process.cwd();
|
|
103
172
|
const baselinePath = join(home, BASELINE_CONFIG_NAME);
|
|
104
173
|
const overridePath = join(home, OVERRIDE_CONFIG_NAME);
|
|
105
174
|
const current = readJsonConfig(baselinePath) ?? {};
|
|
@@ -117,6 +186,30 @@ function committedBindingPath(dir) {
|
|
|
117
186
|
const p = join(dir, BASELINE_CONFIG_NAME);
|
|
118
187
|
return existsSync(p) ? p : void 0;
|
|
119
188
|
}
|
|
189
|
+
function selectWorkspaceBinding(local, explicitName) {
|
|
190
|
+
const bindings = local.workspaces ?? [];
|
|
191
|
+
if (explicitName) {
|
|
192
|
+
const hit = bindings.find((b) => b.name === explicitName);
|
|
193
|
+
if (!hit)
|
|
194
|
+
throw new Error(
|
|
195
|
+
`No workspace binding named "${explicitName}" in ${local.home ?? "this repo"}'s .sechroom.json` + (bindings.length > 0 ? ` (have: ${bindings.map((b) => b.name).join(", ")})` : "") + ". Add one with `sechroom workspace bind`."
|
|
196
|
+
);
|
|
197
|
+
return hit;
|
|
198
|
+
}
|
|
199
|
+
if (!local.home || bindings.length === 0) return void 0;
|
|
200
|
+
const rel = process.cwd().startsWith(local.home) ? process.cwd().slice(local.home.length).replace(/^[/\\]/, "") : "";
|
|
201
|
+
const norm = (p) => p.replace(/\/\*\*$/, "").replace(/[/\\]+$/, "");
|
|
202
|
+
let best;
|
|
203
|
+
for (const b of bindings) {
|
|
204
|
+
for (const raw of b.paths ?? []) {
|
|
205
|
+
const prefix = norm(raw);
|
|
206
|
+
if (prefix.length === 0) continue;
|
|
207
|
+
const matches = rel === prefix || rel.startsWith(prefix + "/");
|
|
208
|
+
if (matches && (!best || prefix.length > best.len)) best = { binding: b, len: prefix.length };
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
return best?.binding;
|
|
212
|
+
}
|
|
120
213
|
function resolveConfig(flags) {
|
|
121
214
|
const local = readLocalConfig();
|
|
122
215
|
const persisted = readPersisted();
|
|
@@ -127,9 +220,11 @@ function resolveConfig(flags) {
|
|
|
127
220
|
"No tenant set. The Sechroom API rejects untenanted requests (HTTP 400). Pass --tenant <id>, set SECHROOM_TENANT, run `sechroom config set tenant <id>`, or `sechroom config set --local tenant <id>` for this directory."
|
|
128
221
|
);
|
|
129
222
|
}
|
|
130
|
-
const
|
|
131
|
-
const
|
|
132
|
-
|
|
223
|
+
const binding = selectWorkspaceBinding(local, flags.binding ?? process.env.SECHROOM_BINDING);
|
|
224
|
+
const workspaceId = process.env.SECHROOM_WORKSPACE ?? binding?.workspaceId ?? local.workspaceId ?? persisted.workspaceId ?? void 0;
|
|
225
|
+
const defaultProjectId = (binding ? binding.defaultProjectId : void 0) ?? local.defaultProjectId ?? persisted.defaultProjectId ?? void 0;
|
|
226
|
+
const account = resolveAccountAlias(flags.account);
|
|
227
|
+
return { baseUrl: baseUrl.replace(/\/$/, ""), tenant, account, workspaceId, defaultProjectId, clientId: persisted.clientId };
|
|
133
228
|
}
|
|
134
229
|
function describeConfig(flags) {
|
|
135
230
|
const local = readLocalConfig();
|
|
@@ -144,10 +239,17 @@ function describeConfig(flags) {
|
|
|
144
239
|
return { value: void 0, source: "unset" };
|
|
145
240
|
};
|
|
146
241
|
const baseUrl = pick(flags.baseUrl, process.env.SECHROOM_BASE_URL, local.baseUrl, g.baseUrl, DEFAULT_BASE_URL);
|
|
242
|
+
let binding;
|
|
243
|
+
try {
|
|
244
|
+
binding = selectWorkspaceBinding(local, flags.binding ?? process.env.SECHROOM_BINDING);
|
|
245
|
+
} catch {
|
|
246
|
+
binding = void 0;
|
|
247
|
+
}
|
|
248
|
+
const workspaceId = process.env.SECHROOM_WORKSPACE ? { value: process.env.SECHROOM_WORKSPACE, source: "env" } : binding ? { value: binding.workspaceId, source: `binding "${binding.name}"${localTag !== "local" ? ` (${local.path})` : ""}` } : pick(void 0, void 0, local.workspaceId, g.workspaceId);
|
|
147
249
|
return {
|
|
148
250
|
baseUrl: { value: baseUrl.value, source: baseUrl.source },
|
|
149
251
|
tenant: pick(flags.tenant, process.env.SECHROOM_TENANT, local.tenant, g.tenant),
|
|
150
|
-
workspaceId
|
|
252
|
+
workspaceId,
|
|
151
253
|
localPath: local.path
|
|
152
254
|
};
|
|
153
255
|
}
|
|
@@ -245,14 +347,25 @@ async function exchange(meta, clientId, code, verifier, redirectUri) {
|
|
|
245
347
|
})
|
|
246
348
|
});
|
|
247
349
|
if (!res.ok) throw new Error(`Token exchange failed (${res.status}): ${await res.text()}`);
|
|
248
|
-
|
|
350
|
+
return await res.json();
|
|
249
351
|
}
|
|
250
|
-
function
|
|
251
|
-
|
|
252
|
-
|
|
352
|
+
function decodeEmail(accessToken) {
|
|
353
|
+
try {
|
|
354
|
+
const payload = accessToken.split(".")[1];
|
|
355
|
+
if (!payload) return void 0;
|
|
356
|
+
const claims = JSON.parse(Buffer.from(payload, "base64url").toString("utf8"));
|
|
357
|
+
return typeof claims.email === "string" ? claims.email : void 0;
|
|
358
|
+
} catch {
|
|
359
|
+
return void 0;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
function persistTokenResponse(account, baseUrl, tok) {
|
|
363
|
+
writeAccount(account, {
|
|
364
|
+
baseUrl,
|
|
253
365
|
accessToken: tok.access_token,
|
|
254
366
|
refreshToken: tok.refresh_token,
|
|
255
|
-
expiresAt: tok.expires_in ? Date.now() + tok.expires_in * 1e3 : void 0
|
|
367
|
+
expiresAt: tok.expires_in ? Date.now() + tok.expires_in * 1e3 : void 0,
|
|
368
|
+
email: decodeEmail(tok.access_token)
|
|
256
369
|
});
|
|
257
370
|
}
|
|
258
371
|
async function login(cfg) {
|
|
@@ -276,14 +389,25 @@ ${authUrl}
|
|
|
276
389
|
`);
|
|
277
390
|
await open(authUrl.toString());
|
|
278
391
|
const code = await loopback.code;
|
|
279
|
-
await exchange(meta, clientId, code, verifier, loopback.redirectUri);
|
|
280
|
-
|
|
392
|
+
const tok = await exchange(meta, clientId, code, verifier, loopback.redirectUri);
|
|
393
|
+
persistTokenResponse(cfg.account, cfg.baseUrl, tok);
|
|
394
|
+
const email = decodeEmail(tok.access_token);
|
|
395
|
+
process.stderr.write(`Signed in${email ? ` as ${email}` : ""} (account "${cfg.account}"). Token cached.
|
|
396
|
+
`);
|
|
281
397
|
}
|
|
282
398
|
async function requireToken(cfg) {
|
|
283
399
|
if (process.env.SECHROOM_TOKEN) return process.env.SECHROOM_TOKEN;
|
|
284
|
-
const cached =
|
|
400
|
+
const cached = readAccount(cfg.account);
|
|
285
401
|
if (!cached?.accessToken) {
|
|
286
|
-
throw new Error(
|
|
402
|
+
throw new Error(
|
|
403
|
+
`Not signed in (account "${cfg.account}"). Run \`sechroom login --account ${cfg.account}\` (or set SECHROOM_TOKEN for headless use).`
|
|
404
|
+
);
|
|
405
|
+
}
|
|
406
|
+
if (cached.baseUrl !== cfg.baseUrl) {
|
|
407
|
+
const matching = listAccounts().filter((a) => a.entry.baseUrl === cfg.baseUrl).map((a) => a.alias);
|
|
408
|
+
throw new Error(
|
|
409
|
+
`Account "${cfg.account}" is signed in to ${cached.baseUrl}, but this call targets ${cfg.baseUrl}. ` + (matching.length > 0 ? `Use --account ${matching[0]} (signed in there)` + (matching.length > 1 ? ` \u2014 or one of: ${matching.join(", ")}` : "") + ", or " : "") + `run \`sechroom login --account ${cfg.account}\` against it.`
|
|
410
|
+
);
|
|
287
411
|
}
|
|
288
412
|
const nearExpiry = cached.expiresAt !== void 0 && Date.now() > cached.expiresAt - 6e4;
|
|
289
413
|
if (nearExpiry && cached.refreshToken) {
|
|
@@ -297,10 +421,10 @@ async function requireToken(cfg) {
|
|
|
297
421
|
})
|
|
298
422
|
});
|
|
299
423
|
if (res.ok) {
|
|
300
|
-
persistTokenResponse(await res.json());
|
|
301
|
-
return
|
|
424
|
+
persistTokenResponse(cfg.account, cfg.baseUrl, await res.json());
|
|
425
|
+
return readAccount(cfg.account).accessToken;
|
|
302
426
|
}
|
|
303
|
-
throw new Error(
|
|
427
|
+
throw new Error(`Session expired and refresh failed. Run \`sechroom login --account ${cfg.account}\` again.`);
|
|
304
428
|
}
|
|
305
429
|
return cached.accessToken;
|
|
306
430
|
}
|
|
@@ -545,6 +669,62 @@ function fail(error) {
|
|
|
545
669
|
process.exit(1);
|
|
546
670
|
}
|
|
547
671
|
|
|
672
|
+
// src/commands/accounts.ts
|
|
673
|
+
function registerAccounts(program2) {
|
|
674
|
+
const accounts = program2.command("accounts").description("Manage signed-in identities (multi-account)");
|
|
675
|
+
accounts.addHelpText(
|
|
676
|
+
"after",
|
|
677
|
+
`
|
|
678
|
+
Examples:
|
|
679
|
+
$ sechroom login --account work sign in a second identity under the "work" alias
|
|
680
|
+
$ sechroom accounts list see every signed-in identity + which is active here
|
|
681
|
+
$ sechroom accounts use work make "work" the machine default
|
|
682
|
+
$ sechroom accounts use work --local pin "work" for THIS directory only (gitignored)
|
|
683
|
+
$ sechroom logout --account work sign the "work" identity out`
|
|
684
|
+
);
|
|
685
|
+
accounts.command("list").description("List signed-in accounts (alias, email, server, expiry)").action((_opts, cmd) => {
|
|
686
|
+
const active2 = resolveAccountAlias(cmd.optsWithGlobals().account);
|
|
687
|
+
const rows = listAccounts().map(({ alias, entry, isDefault }) => ({
|
|
688
|
+
alias,
|
|
689
|
+
email: entry.email,
|
|
690
|
+
baseUrl: entry.baseUrl,
|
|
691
|
+
expiresAt: entry.expiresAt ? new Date(entry.expiresAt).toISOString() : void 0,
|
|
692
|
+
isDefault,
|
|
693
|
+
isActiveHere: alias === active2
|
|
694
|
+
}));
|
|
695
|
+
if (cmd.optsWithGlobals().json) return emit({ accounts: rows, active: active2 }, true);
|
|
696
|
+
if (rows.length === 0) {
|
|
697
|
+
console.log(style.dim("No accounts signed in. Run `sechroom login` (optionally --account <alias>)."));
|
|
698
|
+
return;
|
|
699
|
+
}
|
|
700
|
+
for (const r of rows) {
|
|
701
|
+
const markers = [r.isActiveHere ? style.green("active") : "", r.isDefault ? style.dim("default") : ""].filter(Boolean).join(" ");
|
|
702
|
+
console.log(
|
|
703
|
+
`${r.alias.padEnd(16)} ${(r.email ?? "\u2014").padEnd(28)} ${r.baseUrl} ${markers}`
|
|
704
|
+
);
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
accounts.command("use <alias>").description("Select the account used for calls (machine default, or --local for this directory)").option("--local", "pin for this directory only (gitignored .sechroom/config.json)").action((alias, opts, cmd) => {
|
|
708
|
+
const json = cmd.optsWithGlobals().json;
|
|
709
|
+
if (opts.local) {
|
|
710
|
+
const path = writeLocalAccount(alias);
|
|
711
|
+
if (json) return emit({ account: alias, scope: "local", path }, true);
|
|
712
|
+
console.log(style.green(`Account "${alias}" pinned for this directory (${path}).`));
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
setDefaultAccount(alias);
|
|
716
|
+
if (json) return emit({ account: alias, scope: "global" }, true);
|
|
717
|
+
console.log(style.green(`Account "${alias}" is now the machine default.`));
|
|
718
|
+
});
|
|
719
|
+
accounts.command("remove <alias>").description("Forget an account's cached token (local removal \u2014 does not revoke at the server)").action((alias, _opts, cmd) => {
|
|
720
|
+
const removed = removeAccount(alias);
|
|
721
|
+
if (cmd.optsWithGlobals().json) return emit({ removed: removed ? [alias] : [] }, true);
|
|
722
|
+
console.log(
|
|
723
|
+
removed ? style.green(`Account "${alias}" removed.`) : style.dim(`No account "${alias}".`)
|
|
724
|
+
);
|
|
725
|
+
});
|
|
726
|
+
}
|
|
727
|
+
|
|
548
728
|
// src/commands/memory.ts
|
|
549
729
|
function registerMemory(program2) {
|
|
550
730
|
const memory = program2.command("memory").description("Create, read, and search memories");
|
|
@@ -1022,6 +1202,26 @@ Examples:
|
|
|
1022
1202
|
// src/commands/workspace.ts
|
|
1023
1203
|
function registerWorkspace(program2) {
|
|
1024
1204
|
const workspace = program2.command("workspace").description("Create, browse, and manage workspaces");
|
|
1205
|
+
workspace.command("bind <workspaceId>").description("Bind a workspace to this repo (.sechroom.json). With --name it becomes a NAMED v3 binding (multi-workspace); --paths scopes it to subtrees.").option("--name <name>", "binding name (enables multi-workspace; selectable via --binding / path match)").option("--paths <csv>", "comma-separated directory prefixes (relative to the repo root) this binding covers, e.g. frontend,docs").option("--default-project <id>", "informational default project for this binding").option("--here", "write the binding file at THIS directory even when a parent already carries one").action((workspaceId, opts, cmd) => {
|
|
1206
|
+
const json = cmd.optsWithGlobals().json;
|
|
1207
|
+
if (!opts.name) {
|
|
1208
|
+
const path2 = writeLocalConfig({ workspaceId, ...opts.defaultProject ? { defaultProjectId: opts.defaultProject } : {} }, { here: Boolean(opts.here) });
|
|
1209
|
+
if (json) return emit({ workspaceId, path: path2 }, true);
|
|
1210
|
+
console.log(style.green(`Workspace ${workspaceId} bound (${path2}).`));
|
|
1211
|
+
return;
|
|
1212
|
+
}
|
|
1213
|
+
const local = readLocalConfig();
|
|
1214
|
+
const bindings = (local.workspaces ?? []).filter((b) => b.name !== opts.name);
|
|
1215
|
+
bindings.push({
|
|
1216
|
+
name: opts.name,
|
|
1217
|
+
workspaceId,
|
|
1218
|
+
...opts.defaultProject ? { defaultProjectId: opts.defaultProject } : {},
|
|
1219
|
+
...opts.paths ? { paths: String(opts.paths).split(",").map((p) => p.trim()).filter(Boolean) } : {}
|
|
1220
|
+
});
|
|
1221
|
+
const path = writeLocalConfig({ workspaces: bindings }, { here: Boolean(opts.here) });
|
|
1222
|
+
if (json) return emit({ binding: opts.name, workspaceId, paths: opts.paths ?? null, path }, true);
|
|
1223
|
+
console.log(style.green(`Binding "${opts.name}" \u2192 ${workspaceId} saved (${path}).`));
|
|
1224
|
+
});
|
|
1025
1225
|
workspace.addHelpText(
|
|
1026
1226
|
"after",
|
|
1027
1227
|
`
|
|
@@ -3179,7 +3379,7 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
3179
3379
|
const local = readLocalConfig();
|
|
3180
3380
|
let tenant = g.tenant ?? process.env.SECHROOM_TENANT ?? local.tenant ?? persisted.tenant ?? "";
|
|
3181
3381
|
if (!tenant) {
|
|
3182
|
-
const client = await makeClient({ baseUrl, tenant: "", clientId: persisted.clientId });
|
|
3382
|
+
const client = await makeClient({ baseUrl, tenant: "", account: resolveAccountAlias(), clientId: persisted.clientId });
|
|
3183
3383
|
const { data, error } = await client.GET("/auth/me/tenants", {});
|
|
3184
3384
|
if (error) {
|
|
3185
3385
|
fail(`Couldn't list your tenants: ${JSON.stringify(error)}. Pass --tenant <id> to skip this.`);
|
|
@@ -3214,7 +3414,7 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
3214
3414
|
}
|
|
3215
3415
|
}
|
|
3216
3416
|
const existingWorkspace = local.workspaceId ?? persisted.workspaceId ?? void 0;
|
|
3217
|
-
const wsClient = await makeClient({ baseUrl, tenant, clientId: persisted.clientId });
|
|
3417
|
+
const wsClient = await makeClient({ baseUrl, tenant, account: resolveAccountAlias(), clientId: persisted.clientId });
|
|
3218
3418
|
const workspaceId = await resolveWorkspaceBinding(wsClient, existingWorkspace, {
|
|
3219
3419
|
yes: opts.yes,
|
|
3220
3420
|
json: opts.json,
|
|
@@ -3236,7 +3436,7 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
3236
3436
|
if (opts.persist !== false) {
|
|
3237
3437
|
const patch = { baseUrl, tenant, ...workspaceId ? { workspaceId } : {} };
|
|
3238
3438
|
if (storeLocal) {
|
|
3239
|
-
const path = writeLocalConfig(patch);
|
|
3439
|
+
const path = writeLocalConfig(patch, { here: Boolean(opts.here) });
|
|
3240
3440
|
if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved to ${path} (directory-local)
|
|
3241
3441
|
`);
|
|
3242
3442
|
} else {
|
|
@@ -3249,7 +3449,7 @@ async function ensureTenant(baseUrl, g, opts) {
|
|
|
3249
3449
|
`);
|
|
3250
3450
|
}
|
|
3251
3451
|
}
|
|
3252
|
-
return { baseUrl, tenant, workspaceId, defaultProjectId, clientId: persisted.clientId };
|
|
3452
|
+
return { baseUrl, tenant, account: resolveAccountAlias(), workspaceId, defaultProjectId, clientId: persisted.clientId };
|
|
3253
3453
|
}
|
|
3254
3454
|
async function ensureAuth(cfg, yes) {
|
|
3255
3455
|
if (process.env.SECHROOM_TOKEN) return;
|
|
@@ -3406,7 +3606,7 @@ async function runRecurse(cfg, g, opts) {
|
|
|
3406
3606
|
summarizeFanout(results, { dryRun });
|
|
3407
3607
|
}
|
|
3408
3608
|
function registerOnboard(program2) {
|
|
3409
|
-
program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--recurse", "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", false).option("--lane <id>", "set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo").option("--design-lane <id>", "set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child").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(
|
|
3609
|
+
program2.command("onboard").description("Guided first-run setup: configure, sign in, set timezone, detect clients, and wire this project").option("--recurse", "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", false).option("--lane <id>", "set the code-lane (substrate source identity) explicitly instead of inferring it; with --recurse it's used for every child repo").option("--design-lane <id>", "set the design-lane explicitly (substrate-authoring identity); with --recurse applies to every child").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("--here", "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", 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(
|
|
3410
3610
|
"after",
|
|
3411
3611
|
`
|
|
3412
3612
|
Examples:
|
|
@@ -3432,13 +3632,13 @@ Examples:
|
|
|
3432
3632
|
if (opts.designLane) process.env.SECHROOM_DESIGN_LANE = opts.designLane;
|
|
3433
3633
|
if (opts.recurse) {
|
|
3434
3634
|
const baseUrl2 = resolveBaseUrl(g);
|
|
3435
|
-
await ensureAuth({ baseUrl: baseUrl2, tenant: "", clientId: readPersisted().clientId }, yes);
|
|
3635
|
+
await ensureAuth({ baseUrl: baseUrl2, tenant: "", account: resolveAccountAlias(), clientId: readPersisted().clientId }, yes);
|
|
3436
3636
|
const cfg2 = await ensureTenant(baseUrl2, g, { yes: true, json, persist: false });
|
|
3437
3637
|
await runRecurse(cfg2, g, { yes, dryRun, json, lane: opts.lane, designLane: opts.designLane });
|
|
3438
3638
|
return;
|
|
3439
3639
|
}
|
|
3440
3640
|
const baseUrl = resolveBaseUrl(g);
|
|
3441
|
-
await ensureAuth({ baseUrl, tenant: "", clientId: readPersisted().clientId }, yes);
|
|
3641
|
+
await ensureAuth({ baseUrl, tenant: "", account: resolveAccountAlias(), clientId: readPersisted().clientId }, yes);
|
|
3442
3642
|
const cfg = await ensureTenant(baseUrl, g, { yes, json, local: Boolean(opts.local), workspace: opts.workspace, persist: !check });
|
|
3443
3643
|
const tz = await ensureTimezone(cfg, { yes, dryRun: dryRun || check });
|
|
3444
3644
|
if (!json && tz.action !== "already-set") {
|
|
@@ -3935,9 +4135,23 @@ function removeMaterialisedSkills(dir) {
|
|
|
3935
4135
|
return removed;
|
|
3936
4136
|
}
|
|
3937
4137
|
function registerReset(program2) {
|
|
3938
|
-
program2.command("logout").description("Sign out \u2014 remove the cached (
|
|
4138
|
+
program2.command("logout").description("Sign out \u2014 remove the active account's cached token (--account <alias> for a specific one, --all for every account)").option("--account <alias>", "sign a specific account out").option("--all", "sign every account out").action((opts, cmd) => {
|
|
4139
|
+
const json = cmd.optsWithGlobals().json;
|
|
4140
|
+
if (opts.all) {
|
|
4141
|
+
const removed2 = listAccounts().map((a) => a.alias);
|
|
4142
|
+
for (const alias of removed2) removeAccount(alias);
|
|
4143
|
+
if (json) return emit({ removed: removed2 }, true);
|
|
4144
|
+
console.log(removed2.length > 0 ? style.green(`Signed out ${removed2.length} account(s): ${removed2.join(", ")}.`) : style.dim("Already signed out (no accounts)."));
|
|
4145
|
+
return;
|
|
4146
|
+
}
|
|
4147
|
+
if (opts.account) {
|
|
4148
|
+
const ok2 = removeAccount(opts.account);
|
|
4149
|
+
if (json) return emit({ removed: ok2 ? [opts.account] : [] }, true);
|
|
4150
|
+
console.log(ok2 ? style.green(`Signed out account "${opts.account}".`) : style.dim(`No account "${opts.account}".`));
|
|
4151
|
+
return;
|
|
4152
|
+
}
|
|
3939
4153
|
const removed = clearToken();
|
|
3940
|
-
if (
|
|
4154
|
+
if (json) return emit({ removed: removed ? [removed] : [] }, true);
|
|
3941
4155
|
console.log(
|
|
3942
4156
|
removed ? style.green("Signed out \u2014 auth token removed.") : style.dim("Already signed out (no token).")
|
|
3943
4157
|
);
|
|
@@ -3999,7 +4213,7 @@ function resolveVersion() {
|
|
|
3999
4213
|
}
|
|
4000
4214
|
}
|
|
4001
4215
|
var program = new Command();
|
|
4002
|
-
program.name("sechroom").description("Sechroom CLI \u2014 thin generated client over the Sechroom HTTP API. An agent/human surface alongside MCP.").version(resolveVersion()).option("--base-url <url>", "API base URL (overrides config / SECHROOM_BASE_URL)").option("--tenant <tenant>", "Tenant id (required by the API; overrides config / SECHROOM_TENANT)").option("--json", "Emit compact JSON (for scripts and agents)", false);
|
|
4216
|
+
program.name("sechroom").description("Sechroom CLI \u2014 thin generated client over the Sechroom HTTP API. An agent/human surface alongside MCP.").version(resolveVersion()).option("--base-url <url>", "API base URL (overrides config / SECHROOM_BASE_URL)").option("--tenant <tenant>", "Tenant id (required by the API; overrides config / SECHROOM_TENANT)").option("--binding <name>", "Named workspace binding from .sechroom.json `workspaces` (overrides path auto-selection / SECHROOM_BINDING)").option("--json", "Emit compact JSON (for scripts and agents)", false);
|
|
4003
4217
|
program.addHelpText(
|
|
4004
4218
|
"after",
|
|
4005
4219
|
`
|
|
@@ -4032,11 +4246,12 @@ Examples:
|
|
|
4032
4246
|
$ sechroom login sign in to the configured base URL + tenant
|
|
4033
4247
|
$ sechroom login --base-url https://staging.app.sechroom.ai/api
|
|
4034
4248
|
$ export SECHROOM_TOKEN=<bearer> headless: skip login entirely (CI / agents)`
|
|
4035
|
-
).action(async (
|
|
4249
|
+
).option("--account <alias>", "sign in under a named account alias (multi-account; default: the active alias)").action(async (opts, cmd) => {
|
|
4036
4250
|
const g = cmd.optsWithGlobals();
|
|
4037
4251
|
const persisted = readPersisted();
|
|
4038
4252
|
const baseUrl = g.baseUrl ?? process.env.SECHROOM_BASE_URL ?? persisted.baseUrl ?? "https://app.sechroom.ai/api";
|
|
4039
|
-
|
|
4253
|
+
const account = resolveAccountAlias(opts.account);
|
|
4254
|
+
await login({ baseUrl: baseUrl.replace(/\/$/, ""), tenant: g.tenant ?? "", account });
|
|
4040
4255
|
});
|
|
4041
4256
|
var config = program.command("config").description("Manage persisted CLI config");
|
|
4042
4257
|
config.addHelpText(
|
|
@@ -4072,7 +4287,7 @@ config.command("set <key> <value>").description("Set baseUrl | tenant | workspac
|
|
|
4072
4287
|
});
|
|
4073
4288
|
config.command("show").description("Print resolved config + sources (flag > env > local > global > default)").action((_opts, cmd) => {
|
|
4074
4289
|
const g = cmd.optsWithGlobals();
|
|
4075
|
-
const d = describeConfig({ baseUrl: g.baseUrl, tenant: g.tenant });
|
|
4290
|
+
const d = describeConfig({ baseUrl: g.baseUrl, tenant: g.tenant, binding: g.binding });
|
|
4076
4291
|
if (g.json) {
|
|
4077
4292
|
process.stdout.write(
|
|
4078
4293
|
JSON.stringify({
|
|
@@ -4093,6 +4308,7 @@ local: ${d.localPath ?? "(none)"} ${JSON.stringify(readLocalConfig())}
|
|
|
4093
4308
|
`
|
|
4094
4309
|
);
|
|
4095
4310
|
});
|
|
4311
|
+
registerAccounts(program);
|
|
4096
4312
|
registerMemory(program);
|
|
4097
4313
|
registerWorklog(program);
|
|
4098
4314
|
registerLookup(program);
|
package/package.json
CHANGED