@sechroom/cli 2026.6.12 → 2026.6.14
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 +408 -63
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -18,6 +18,7 @@ var CONFIG_FILE = join(CONFIG_DIR, "config.json");
|
|
|
18
18
|
var TOKEN_FILE = join(CONFIG_DIR, "token.json");
|
|
19
19
|
var LOCAL_CONFIG_NAME = ".sechroom.json";
|
|
20
20
|
var DEFAULT_BASE_URL = "https://app.sechroom.ai/api";
|
|
21
|
+
var LOCAL_CONFIG_SCHEMA_VERSION = 2;
|
|
21
22
|
function ensureDir() {
|
|
22
23
|
if (!existsSync(CONFIG_DIR)) mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
23
24
|
}
|
|
@@ -78,7 +79,14 @@ function readLocalConfig() {
|
|
|
78
79
|
if (!path) return {};
|
|
79
80
|
try {
|
|
80
81
|
const c = JSON.parse(readFileSync(path, "utf8"));
|
|
81
|
-
return {
|
|
82
|
+
return {
|
|
83
|
+
schemaVersion: c.schemaVersion,
|
|
84
|
+
baseUrl: c.baseUrl,
|
|
85
|
+
tenant: c.tenant,
|
|
86
|
+
workspaceId: c.workspaceId,
|
|
87
|
+
defaultProjectId: c.defaultProjectId,
|
|
88
|
+
path
|
|
89
|
+
};
|
|
82
90
|
} catch {
|
|
83
91
|
return {};
|
|
84
92
|
}
|
|
@@ -90,7 +98,8 @@ function writeLocalConfig(patch) {
|
|
|
90
98
|
current = JSON.parse(readFileSync(path, "utf8"));
|
|
91
99
|
} catch {
|
|
92
100
|
}
|
|
93
|
-
|
|
101
|
+
const next = { ...current, ...patch, schemaVersion: LOCAL_CONFIG_SCHEMA_VERSION };
|
|
102
|
+
writeFileSync(path, JSON.stringify(next, null, 2), { mode: 384 });
|
|
94
103
|
return path;
|
|
95
104
|
}
|
|
96
105
|
function resolveConfig(flags) {
|
|
@@ -103,7 +112,9 @@ function resolveConfig(flags) {
|
|
|
103
112
|
"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."
|
|
104
113
|
);
|
|
105
114
|
}
|
|
106
|
-
|
|
115
|
+
const workspaceId = process.env.SECHROOM_WORKSPACE ?? local.workspaceId ?? persisted.workspaceId ?? void 0;
|
|
116
|
+
const defaultProjectId = local.defaultProjectId ?? persisted.defaultProjectId ?? void 0;
|
|
117
|
+
return { baseUrl: baseUrl.replace(/\/$/, ""), tenant, workspaceId, defaultProjectId, clientId: persisted.clientId };
|
|
107
118
|
}
|
|
108
119
|
function describeConfig(flags) {
|
|
109
120
|
const local = readLocalConfig();
|
|
@@ -121,6 +132,7 @@ function describeConfig(flags) {
|
|
|
121
132
|
return {
|
|
122
133
|
baseUrl: { value: baseUrl.value, source: baseUrl.source },
|
|
123
134
|
tenant: pick(flags.tenant, process.env.SECHROOM_TENANT, local.tenant, g.tenant),
|
|
135
|
+
workspaceId: pick(void 0, process.env.SECHROOM_WORKSPACE, local.workspaceId, g.workspaceId),
|
|
124
136
|
localPath: local.path
|
|
125
137
|
};
|
|
126
138
|
}
|
|
@@ -302,6 +314,7 @@ var style = {
|
|
|
302
314
|
cyan: wrap(36, 39)
|
|
303
315
|
};
|
|
304
316
|
var ok = (s) => style.green(s);
|
|
317
|
+
var warn = (s) => style.yellow(s);
|
|
305
318
|
var err = (s) => style.red(s);
|
|
306
319
|
function active() {
|
|
307
320
|
return !quiet && Boolean(process.stderr.isTTY);
|
|
@@ -1761,6 +1774,7 @@ Examples:
|
|
|
1761
1774
|
}
|
|
1762
1775
|
|
|
1763
1776
|
// src/setup/apply.ts
|
|
1777
|
+
import { createHash as createHash2 } from "crypto";
|
|
1764
1778
|
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
|
|
1765
1779
|
import { dirname as dirname2 } from "path";
|
|
1766
1780
|
|
|
@@ -1770,11 +1784,17 @@ var SectionType = {
|
|
|
1770
1784
|
McpConfigToml: "mcp-config-toml",
|
|
1771
1785
|
InstructionFile: "instruction-file",
|
|
1772
1786
|
ProjectConfig: "project-config",
|
|
1773
|
-
Verify: "verify"
|
|
1787
|
+
Verify: "verify",
|
|
1788
|
+
/** SBC-999 — workspace-pinned conventions, emitted only when the request
|
|
1789
|
+
* carried a workspaceId and that workspace has agent-setup-bundle memories. */
|
|
1790
|
+
WorkspaceConventions: "workspace-conventions"
|
|
1774
1791
|
};
|
|
1775
1792
|
async function fetchSetup(cfg) {
|
|
1776
1793
|
const client = await makeClient(cfg);
|
|
1777
|
-
const { data, error } = await client.GET(
|
|
1794
|
+
const { data, error } = await client.GET(
|
|
1795
|
+
"/operator-surface/setup",
|
|
1796
|
+
cfg.workspaceId ? { params: { query: { workspaceId: cfg.workspaceId } } } : {}
|
|
1797
|
+
);
|
|
1778
1798
|
if (error) throw new Error(`GET /operator-surface/setup failed: ${JSON.stringify(error)}`);
|
|
1779
1799
|
return data;
|
|
1780
1800
|
}
|
|
@@ -1806,7 +1826,10 @@ async function fetchMemoryFields(cfg, id) {
|
|
|
1806
1826
|
const client = await makeClient(cfg);
|
|
1807
1827
|
const { data } = await client.GET("/memories/{memoryId}", { params: { path: { memoryId: id } } });
|
|
1808
1828
|
const env = data;
|
|
1809
|
-
|
|
1829
|
+
const m = env?.item ?? env;
|
|
1830
|
+
if (!m) return null;
|
|
1831
|
+
const version = typeof m.currentVersion === "string" ? Number(m.currentVersion) : m.currentVersion;
|
|
1832
|
+
return { text: m.text, title: m.title, tags: m.tags, version: Number.isFinite(version) ? version : void 0 };
|
|
1810
1833
|
}
|
|
1811
1834
|
async function resolveInstruction(cfg, section, personalWorkspaceId) {
|
|
1812
1835
|
const client = await makeClient(cfg);
|
|
@@ -1830,14 +1853,28 @@ async function resolveInstruction(cfg, section, personalWorkspaceId) {
|
|
|
1830
1853
|
if (ovrHits.length > 0) {
|
|
1831
1854
|
const override = await fetchMemoryFields(cfg, ovrHits[0].id);
|
|
1832
1855
|
if (typeof override?.text === "string" && override.text.length > 0) {
|
|
1833
|
-
return { title: override.title ?? template.title ?? artifact.title, body: override.text, source: "override", templateId, templateTags };
|
|
1856
|
+
return { title: override.title ?? template.title ?? artifact.title, body: override.text, source: "override", templateId, templateTags, sourceRef: `${ovrHits[0].id}@v${override.version ?? 1}` };
|
|
1834
1857
|
}
|
|
1835
1858
|
}
|
|
1836
1859
|
}
|
|
1837
|
-
return { title: template.title ?? artifact.title, body: template.text, source: "template", templateId, templateTags };
|
|
1860
|
+
return { title: template.title ?? artifact.title, body: template.text, source: "template", templateId, templateTags, sourceRef: `${templateId}@v${template.version ?? 1}` };
|
|
1838
1861
|
}
|
|
1839
1862
|
return null;
|
|
1840
1863
|
}
|
|
1864
|
+
async function resolveWorkspaceConventions(cfg, section) {
|
|
1865
|
+
const parts = [];
|
|
1866
|
+
const refs = [];
|
|
1867
|
+
for (const artifact of section.artifacts) {
|
|
1868
|
+
if (parseTagArtifactId(artifact.id)) continue;
|
|
1869
|
+
const mem = await fetchMemoryFields(cfg, artifact.id);
|
|
1870
|
+
if (typeof mem?.text === "string" && mem.text.trim().length > 0) {
|
|
1871
|
+
parts.push(mem.text.trim());
|
|
1872
|
+
refs.push(`${artifact.id}@v${mem.version ?? 1}`);
|
|
1873
|
+
}
|
|
1874
|
+
}
|
|
1875
|
+
if (parts.length === 0) return null;
|
|
1876
|
+
return { body: parts.join("\n\n---\n\n"), refs };
|
|
1877
|
+
}
|
|
1841
1878
|
async function createOverride(cfg, template, personalWorkspaceId) {
|
|
1842
1879
|
const client = await makeClient(cfg);
|
|
1843
1880
|
const overrideTags = template.templateTags.filter(
|
|
@@ -1861,8 +1898,57 @@ async function createOverride(cfg, template, personalWorkspaceId) {
|
|
|
1861
1898
|
}
|
|
1862
1899
|
|
|
1863
1900
|
// src/setup/apply.ts
|
|
1864
|
-
var
|
|
1865
|
-
var
|
|
1901
|
+
var MARKER_BEGIN = "<!-- @sechroom/cli:begin";
|
|
1902
|
+
var MARKER_END = "<!-- @sechroom/cli:end";
|
|
1903
|
+
function normalizeBody(s) {
|
|
1904
|
+
return s.replace(/\r\n/g, "\n").trim();
|
|
1905
|
+
}
|
|
1906
|
+
function bodySha256(body) {
|
|
1907
|
+
return createHash2("sha256").update(normalizeBody(body), "utf8").digest("hex");
|
|
1908
|
+
}
|
|
1909
|
+
function renderBlock(write) {
|
|
1910
|
+
const body = normalizeBody(write.body);
|
|
1911
|
+
const attrs = [`block=${write.block}`];
|
|
1912
|
+
if (write.source) attrs.push(`source=${write.source}`);
|
|
1913
|
+
attrs.push(`sha256=${bodySha256(body)}`);
|
|
1914
|
+
return `${MARKER_BEGIN} ${attrs.join(" ")} -->
|
|
1915
|
+
${body}
|
|
1916
|
+
${MARKER_END} block=${write.block} -->
|
|
1917
|
+
`;
|
|
1918
|
+
}
|
|
1919
|
+
function keyedBlockRe(block) {
|
|
1920
|
+
const b = escapeRe(block);
|
|
1921
|
+
return new RegExp(
|
|
1922
|
+
`${escapeRe(MARKER_BEGIN)}[^\\n]*?\\bblock=${b}\\b[^\\n]*?-->\\n[\\s\\S]*?${escapeRe(MARKER_END)}[^\\n]*?\\bblock=${b}\\b[^\\n]*?-->\\n?`
|
|
1923
|
+
);
|
|
1924
|
+
}
|
|
1925
|
+
function legacyBlockRe() {
|
|
1926
|
+
return new RegExp(
|
|
1927
|
+
`${escapeRe(MARKER_BEGIN)}(?:(?!block=)[^\\n])*?-->\\n[\\s\\S]*?${escapeRe(MARKER_END)}(?:(?!block=)[^\\n])*?-->\\n?`
|
|
1928
|
+
);
|
|
1929
|
+
}
|
|
1930
|
+
function parseAttrs(beginLine) {
|
|
1931
|
+
const attrs = {};
|
|
1932
|
+
for (const m of beginLine.matchAll(/(\w+)=(\S+)/g)) attrs[m[1]] = m[2];
|
|
1933
|
+
return attrs;
|
|
1934
|
+
}
|
|
1935
|
+
function innerBody(segment) {
|
|
1936
|
+
const firstNl = segment.indexOf("\n");
|
|
1937
|
+
const endIdx = segment.lastIndexOf(MARKER_END);
|
|
1938
|
+
return segment.slice(firstNl + 1, endIdx).replace(/\n$/, "");
|
|
1939
|
+
}
|
|
1940
|
+
function parseManagedBlock(content, block) {
|
|
1941
|
+
const keyed = content.match(keyedBlockRe(block));
|
|
1942
|
+
if (keyed) {
|
|
1943
|
+
const attrs = parseAttrs(keyed[0].slice(0, keyed[0].indexOf("\n")));
|
|
1944
|
+
return { block, source: attrs.source ?? null, sha256: attrs.sha256 ?? null, body: innerBody(keyed[0]) };
|
|
1945
|
+
}
|
|
1946
|
+
if (block === "role-template") {
|
|
1947
|
+
const legacy = content.match(legacyBlockRe());
|
|
1948
|
+
if (legacy) return { block, source: null, sha256: null, body: innerBody(legacy[0]) };
|
|
1949
|
+
}
|
|
1950
|
+
return null;
|
|
1951
|
+
}
|
|
1866
1952
|
function ensureDir2(path) {
|
|
1867
1953
|
mkdirSync2(dirname2(path), { recursive: true });
|
|
1868
1954
|
}
|
|
@@ -1901,32 +1987,74 @@ function mergeCodexToml(path, snippet, dryRun) {
|
|
|
1901
1987
|
writeFileSync2(path, next, { mode: 384 });
|
|
1902
1988
|
return { kind: "mcp", path, status: existed ? "merged" : "created" };
|
|
1903
1989
|
}
|
|
1904
|
-
function writeInstructionBlock(path,
|
|
1905
|
-
const block = `${BLOCK_BEGIN}
|
|
1906
|
-
${body.trim()}
|
|
1907
|
-
${BLOCK_END}
|
|
1908
|
-
`;
|
|
1990
|
+
function writeInstructionBlock(path, write, dryRun) {
|
|
1909
1991
|
const existed = existsSync2(path);
|
|
1910
|
-
const
|
|
1911
|
-
let next;
|
|
1912
|
-
const re = new RegExp(`${escapeRe(BLOCK_BEGIN)}[\\s\\S]*?${escapeRe(BLOCK_END)}\\n?`);
|
|
1913
|
-
if (re.test(current)) {
|
|
1914
|
-
next = current.replace(re, block);
|
|
1915
|
-
} else {
|
|
1916
|
-
next = current.trim().length > 0 ? `${current.trimEnd()}
|
|
1917
|
-
|
|
1918
|
-
${block}` : block;
|
|
1919
|
-
}
|
|
1992
|
+
const next = computeBlockFile(readOr(path, ""), write);
|
|
1920
1993
|
if (dryRun) return { kind: "instruction", path, status: "dry-run" };
|
|
1921
1994
|
ensureDir2(path);
|
|
1922
1995
|
writeFileSync2(path, next);
|
|
1923
1996
|
return { kind: "instruction", path, status: existed ? "merged" : "created" };
|
|
1924
1997
|
}
|
|
1998
|
+
function computeBlockFile(current, write) {
|
|
1999
|
+
const rendered = renderBlock(write);
|
|
2000
|
+
const keyed = keyedBlockRe(write.block);
|
|
2001
|
+
if (keyed.test(current)) return current.replace(keyed, rendered);
|
|
2002
|
+
if (write.block === "role-template" && legacyBlockRe().test(current)) {
|
|
2003
|
+
return current.replace(legacyBlockRe(), rendered);
|
|
2004
|
+
}
|
|
2005
|
+
return current.trim().length > 0 ? `${current.trimEnd()}
|
|
2006
|
+
|
|
2007
|
+
${rendered}` : rendered;
|
|
2008
|
+
}
|
|
2009
|
+
function evaluateBlock(content, block, serverBody) {
|
|
2010
|
+
const onDisk = parseManagedBlock(content, block);
|
|
2011
|
+
if (!onDisk) return "absent";
|
|
2012
|
+
const actual = bodySha256(onDisk.body);
|
|
2013
|
+
if (onDisk.sha256 && actual !== onDisk.sha256) return "drift";
|
|
2014
|
+
return actual === bodySha256(serverBody) ? "current" : "stale";
|
|
2015
|
+
}
|
|
2016
|
+
function applyBlock(path, write, mode, dryRun) {
|
|
2017
|
+
const current = readOr(path, "");
|
|
2018
|
+
const state = evaluateBlock(current, write.block, write.body);
|
|
2019
|
+
if (mode === "check") {
|
|
2020
|
+
return {
|
|
2021
|
+
kind: "instruction",
|
|
2022
|
+
path,
|
|
2023
|
+
status: state === "current" ? "current" : "skipped",
|
|
2024
|
+
eval: state,
|
|
2025
|
+
note: state === "current" ? void 0 : `would ${state === "absent" ? "write" : "refresh"} (${state})`
|
|
2026
|
+
};
|
|
2027
|
+
}
|
|
2028
|
+
if (state === "current") {
|
|
2029
|
+
return { kind: "instruction", path, status: "current", eval: "current" };
|
|
2030
|
+
}
|
|
2031
|
+
if (state === "drift" && mode !== "force") {
|
|
2032
|
+
const proposedPath = `${path}.proposed`;
|
|
2033
|
+
const next = computeBlockFile(current, write);
|
|
2034
|
+
if (!dryRun) {
|
|
2035
|
+
ensureDir2(proposedPath);
|
|
2036
|
+
writeFileSync2(proposedPath, next);
|
|
2037
|
+
}
|
|
2038
|
+
return {
|
|
2039
|
+
kind: "instruction",
|
|
2040
|
+
path,
|
|
2041
|
+
status: "skipped",
|
|
2042
|
+
eval: "drift",
|
|
2043
|
+
proposedPath,
|
|
2044
|
+
note: `local edits \u2014 wrote ${proposedPath} (original left untouched)`
|
|
2045
|
+
};
|
|
2046
|
+
}
|
|
2047
|
+
const action = writeInstructionBlock(path, write, dryRun);
|
|
2048
|
+
const note = state === "stale" ? "refreshed" : state === "drift" ? "overwrote local edits" : void 0;
|
|
2049
|
+
return { ...action, eval: state, note: note ?? action.note };
|
|
2050
|
+
}
|
|
1925
2051
|
function escapeRe(s) {
|
|
1926
2052
|
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1927
2053
|
}
|
|
1928
2054
|
async function applyClient(cfg, setup, target, opts) {
|
|
1929
2055
|
const actions = [];
|
|
2056
|
+
const mode = opts.mode ?? "apply";
|
|
2057
|
+
const dryRun = opts.dryRun || mode === "check";
|
|
1930
2058
|
if (opts.mcp && target.mcp) {
|
|
1931
2059
|
const surface = findSurface(setup, target.mcp.surfaceKey);
|
|
1932
2060
|
const section = findSection(surface, target.mcp.sectionType);
|
|
@@ -1935,7 +2063,7 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
1935
2063
|
actions.push({ kind: "mcp", path: target.mcp.path, status: "skipped", note: `no ${target.mcp.sectionType} section on surface '${target.mcp.surfaceKey}'` });
|
|
1936
2064
|
} else {
|
|
1937
2065
|
actions.push(
|
|
1938
|
-
target.mcp.format === "toml" ? mergeCodexToml(target.mcp.path, snippet,
|
|
2066
|
+
target.mcp.format === "toml" ? mergeCodexToml(target.mcp.path, snippet, dryRun) : mergeMcpJson(target.mcp.path, snippet, dryRun)
|
|
1939
2067
|
);
|
|
1940
2068
|
}
|
|
1941
2069
|
}
|
|
@@ -1949,8 +2077,26 @@ async function applyClient(cfg, setup, target, opts) {
|
|
|
1949
2077
|
if (!resolved) {
|
|
1950
2078
|
actions.push({ kind: "instruction", path: target.instruction.path, status: "skipped", note: "no role template found in this tenant \u2014 install the SEM Starter bundle, then re-run `sechroom setup agent-files`" });
|
|
1951
2079
|
} else {
|
|
1952
|
-
const action =
|
|
1953
|
-
|
|
2080
|
+
const action = applyBlock(
|
|
2081
|
+
target.instruction.path,
|
|
2082
|
+
{ block: "role-template", body: resolved.body, source: resolved.sourceRef },
|
|
2083
|
+
mode,
|
|
2084
|
+
opts.dryRun
|
|
2085
|
+
);
|
|
2086
|
+
actions.push(resolved.source === "override" && action.status !== "current" ? { ...action, note: action.note ?? "your personal copy" } : action);
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
const conventionsSection = findSection(surface, SectionType.WorkspaceConventions);
|
|
2090
|
+
if (conventionsSection) {
|
|
2091
|
+
const conventions = await resolveWorkspaceConventions(cfg, conventionsSection);
|
|
2092
|
+
if (conventions) {
|
|
2093
|
+
const action = applyBlock(
|
|
2094
|
+
target.instruction.path,
|
|
2095
|
+
{ block: "workspace-conventions", body: conventions.body, source: `workspace:${cfg.workspaceId ?? ""}` },
|
|
2096
|
+
mode,
|
|
2097
|
+
opts.dryRun
|
|
2098
|
+
);
|
|
2099
|
+
actions.push(action.status === "current" ? action : { ...action, note: action.note ?? `workspace conventions (${conventions.refs.length})` });
|
|
1954
2100
|
}
|
|
1955
2101
|
}
|
|
1956
2102
|
}
|
|
@@ -2284,20 +2430,169 @@ function systemTimezone() {
|
|
|
2284
2430
|
return "UTC";
|
|
2285
2431
|
}
|
|
2286
2432
|
}
|
|
2287
|
-
|
|
2433
|
+
function editDistance(a, b) {
|
|
2434
|
+
const m = a.length;
|
|
2435
|
+
const n = b.length;
|
|
2436
|
+
if (m === 0) return n;
|
|
2437
|
+
if (n === 0) return m;
|
|
2438
|
+
let prev = Array.from({ length: n + 1 }, (_, j) => j);
|
|
2439
|
+
for (let i = 1; i <= m; i++) {
|
|
2440
|
+
const curr = [i];
|
|
2441
|
+
for (let j = 1; j <= n; j++) {
|
|
2442
|
+
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
|
|
2443
|
+
curr[j] = Math.min(curr[j - 1] + 1, prev[j] + 1, prev[j - 1] + cost);
|
|
2444
|
+
}
|
|
2445
|
+
prev = curr;
|
|
2446
|
+
}
|
|
2447
|
+
return prev[n];
|
|
2448
|
+
}
|
|
2449
|
+
function namesCollide(a, b) {
|
|
2450
|
+
const x = a.trim().toLowerCase();
|
|
2451
|
+
const y = b.trim().toLowerCase();
|
|
2452
|
+
return x === y || editDistance(x, y) <= 1;
|
|
2453
|
+
}
|
|
2454
|
+
function workspacePath(ws, byId) {
|
|
2455
|
+
const parts = [];
|
|
2456
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2457
|
+
let cur = ws;
|
|
2458
|
+
while (cur && !seen.has(cur.id)) {
|
|
2459
|
+
seen.add(cur.id);
|
|
2460
|
+
parts.unshift(cur.name);
|
|
2461
|
+
cur = cur.parentId ? byId.get(cur.parentId) : void 0;
|
|
2462
|
+
}
|
|
2463
|
+
return parts.join(" / ");
|
|
2464
|
+
}
|
|
2465
|
+
function resolveBaseUrl(g) {
|
|
2288
2466
|
const persisted = readPersisted();
|
|
2289
2467
|
const local = readLocalConfig();
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
2468
|
+
const baseUrl = g.baseUrl ?? process.env.SECHROOM_BASE_URL ?? local.baseUrl ?? persisted.baseUrl ?? DEFAULT_BASE_URL2;
|
|
2469
|
+
return baseUrl.replace(/\/$/, "");
|
|
2470
|
+
}
|
|
2471
|
+
async function fetchWorkspaces(client) {
|
|
2472
|
+
const { data, error } = await client.GET("/workspaces", { params: { query: { includeArchived: false } } });
|
|
2473
|
+
if (error) throw new Error(`Couldn't list your workspaces: ${JSON.stringify(error)}`);
|
|
2474
|
+
const rows = data ?? [];
|
|
2475
|
+
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 }));
|
|
2476
|
+
}
|
|
2477
|
+
async function lookupWorkspace(client, id) {
|
|
2478
|
+
const { data, error } = await client.GET("/workspaces/{workspaceId}", { params: { path: { workspaceId: id } } });
|
|
2479
|
+
if (error) return null;
|
|
2480
|
+
const env = data;
|
|
2481
|
+
const w = env?.item ?? env;
|
|
2482
|
+
return w?.id ? { id: w.id, name: w.name ?? id, parentId: w.parentId ?? null } : null;
|
|
2483
|
+
}
|
|
2484
|
+
async function warnIfProjectStray(client, projectId, workspaceId, json) {
|
|
2485
|
+
const { data, error } = await client.GET("/projects/{projectId}", { params: { path: { projectId } } });
|
|
2486
|
+
if (error) return;
|
|
2487
|
+
const env = data;
|
|
2488
|
+
const owner = env?.item?.workspaceId;
|
|
2489
|
+
if (owner && owner !== workspaceId && !json) {
|
|
2490
|
+
process.stderr.write(
|
|
2491
|
+
`${warn("\u26A0")} defaultProjectId ${style.dim(projectId)} belongs to a different workspace (${style.dim(owner)}), not ${style.dim(workspaceId)} \u2014 leaving it as-is.
|
|
2492
|
+
`
|
|
2493
|
+
);
|
|
2294
2494
|
}
|
|
2295
|
-
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2495
|
+
}
|
|
2496
|
+
async function pickWorkspace(client) {
|
|
2497
|
+
const all = await withSpinner("Listing your workspaces", () => fetchWorkspaces(client));
|
|
2498
|
+
if (all.length === 0) {
|
|
2499
|
+
process.stderr.write(`no workspaces found \u2014 skipping workspace binding (you can set it later with \`sechroom config set --local workspaceId <id>\`)
|
|
2500
|
+
`);
|
|
2501
|
+
return void 0;
|
|
2502
|
+
}
|
|
2503
|
+
const byId = new Map(all.map((w) => [w.id, w]));
|
|
2504
|
+
let pool = all;
|
|
2505
|
+
if (all.length > 12) {
|
|
2506
|
+
const q = (await promptText(`Filter ${all.length} workspaces (substring, Enter to list all)?`, "")).trim().toLowerCase();
|
|
2507
|
+
if (q) {
|
|
2508
|
+
const hits = all.filter((w) => `${w.name} ${workspacePath(w, byId)}`.toLowerCase().includes(q));
|
|
2509
|
+
if (hits.length > 0) pool = hits;
|
|
2510
|
+
else process.stderr.write(`no match for "${q}" \u2014 listing all
|
|
2511
|
+
`);
|
|
2512
|
+
}
|
|
2513
|
+
}
|
|
2514
|
+
const SKIP = "__skip__";
|
|
2515
|
+
const choices = [
|
|
2516
|
+
...pool.slice().sort((a, b) => workspacePath(a, byId).localeCompare(workspacePath(b, byId))).map((w) => ({ label: workspacePath(w, byId), value: w.id, hint: w.id })),
|
|
2517
|
+
{ label: style.dim("skip \u2014 don't bind a workspace"), value: SKIP, hint: void 0 }
|
|
2518
|
+
];
|
|
2519
|
+
const chosen = await promptSelect("Bind this directory to a workspace:", choices, SKIP);
|
|
2520
|
+
if (chosen === SKIP) return void 0;
|
|
2521
|
+
const picked = byId.get(chosen);
|
|
2522
|
+
const collisions = all.filter((w) => w.id !== picked.id && namesCollide(w.name, picked.name));
|
|
2523
|
+
if (collisions.length > 0) {
|
|
2524
|
+
process.stderr.write(
|
|
2525
|
+
`${warn("\u26A0")} ${collisions.length} other workspace(s) have a similar name to ${style.cyan(workspacePath(picked, byId))}:
|
|
2526
|
+
` + collisions.map((w) => ` ${style.dim(workspacePath(w, byId))} ${style.dim(`(${w.id})`)}`).join("\n") + `
|
|
2527
|
+
You picked ${style.dim(picked.id)} \u2014 re-run \`sechroom config set --local workspaceId <id>\` if that's wrong.
|
|
2528
|
+
`
|
|
2299
2529
|
);
|
|
2300
2530
|
}
|
|
2531
|
+
return chosen;
|
|
2532
|
+
}
|
|
2533
|
+
async function resolveWorkspaceBinding(client, existing, opts) {
|
|
2534
|
+
if (opts.workspace) {
|
|
2535
|
+
const found = await lookupWorkspace(client, opts.workspace);
|
|
2536
|
+
if (!found && !opts.json) {
|
|
2537
|
+
process.stderr.write(
|
|
2538
|
+
`${warn("\u26A0")} workspace ${style.dim(opts.workspace)} not found (or you lack access) \u2014 binding it anyway.
|
|
2539
|
+
`
|
|
2540
|
+
);
|
|
2541
|
+
}
|
|
2542
|
+
return opts.workspace;
|
|
2543
|
+
}
|
|
2544
|
+
if (existing) return existing;
|
|
2545
|
+
if (!canPrompt() || opts.yes) return void 0;
|
|
2546
|
+
return pickWorkspace(client);
|
|
2547
|
+
}
|
|
2548
|
+
async function ensureTenant(baseUrl, g, opts) {
|
|
2549
|
+
const persisted = readPersisted();
|
|
2550
|
+
const local = readLocalConfig();
|
|
2551
|
+
let tenant = g.tenant ?? process.env.SECHROOM_TENANT ?? local.tenant ?? persisted.tenant ?? "";
|
|
2552
|
+
if (!tenant) {
|
|
2553
|
+
const client = await makeClient({ baseUrl, tenant: "", clientId: persisted.clientId });
|
|
2554
|
+
const { data, error } = await client.GET("/auth/me/tenants", {});
|
|
2555
|
+
if (error) {
|
|
2556
|
+
fail(`Couldn't list your tenants: ${JSON.stringify(error)}. Pass --tenant <id> to skip this.`);
|
|
2557
|
+
}
|
|
2558
|
+
const tenants = data?.tenants ?? [];
|
|
2559
|
+
if (tenants.length === 0) {
|
|
2560
|
+
fail(
|
|
2561
|
+
"You're signed in, but your account isn't a member of any tenant yet. Ask an admin to add you (or create one in the app), then re-run \u2014 or pass --tenant <id>."
|
|
2562
|
+
);
|
|
2563
|
+
} else if (tenants.length === 1) {
|
|
2564
|
+
tenant = tenants[0].key;
|
|
2565
|
+
if (!opts.json) {
|
|
2566
|
+
process.stderr.write(
|
|
2567
|
+
`${ok("\u2713")} using your tenant ${style.cyan(tenants[0].label)} ${style.dim(`(${tenant})`)}
|
|
2568
|
+
`
|
|
2569
|
+
);
|
|
2570
|
+
}
|
|
2571
|
+
} else if (canPrompt() && !opts.yes) {
|
|
2572
|
+
tenant = await promptSelect(
|
|
2573
|
+
"You belong to several tenants \u2014 pick one:",
|
|
2574
|
+
tenants.map((t) => ({ label: t.label, value: t.key, hint: t.key })),
|
|
2575
|
+
data?.defaultTenantKey ?? tenants[0].key
|
|
2576
|
+
);
|
|
2577
|
+
} else {
|
|
2578
|
+
tenant = data?.defaultTenantKey ?? tenants[0].key;
|
|
2579
|
+
if (!opts.json) {
|
|
2580
|
+
process.stderr.write(
|
|
2581
|
+
`using tenant ${tenant} (${tenants.length} available \u2014 pass --tenant to choose another)
|
|
2582
|
+
`
|
|
2583
|
+
);
|
|
2584
|
+
}
|
|
2585
|
+
}
|
|
2586
|
+
}
|
|
2587
|
+
const existingWorkspace = local.workspaceId ?? persisted.workspaceId ?? void 0;
|
|
2588
|
+
const wsClient = await makeClient({ baseUrl, tenant, clientId: persisted.clientId });
|
|
2589
|
+
const workspaceId = await resolveWorkspaceBinding(wsClient, existingWorkspace, {
|
|
2590
|
+
yes: opts.yes,
|
|
2591
|
+
json: opts.json,
|
|
2592
|
+
workspace: opts.workspace
|
|
2593
|
+
});
|
|
2594
|
+
const defaultProjectId = local.defaultProjectId ?? persisted.defaultProjectId ?? void 0;
|
|
2595
|
+
if (defaultProjectId && workspaceId) await warnIfProjectStray(wsClient, defaultProjectId, workspaceId, opts.json);
|
|
2301
2596
|
let storeLocal = Boolean(opts.local);
|
|
2302
2597
|
if (!opts.local && canPrompt() && !opts.yes) {
|
|
2303
2598
|
storeLocal = await promptSelect(
|
|
@@ -2309,16 +2604,23 @@ async function ensureConfig(g, opts) {
|
|
|
2309
2604
|
local.path ? "local" : "global"
|
|
2310
2605
|
) === "local";
|
|
2311
2606
|
}
|
|
2312
|
-
if (
|
|
2313
|
-
const
|
|
2314
|
-
if (
|
|
2607
|
+
if (opts.persist !== false) {
|
|
2608
|
+
const patch = { baseUrl, tenant, ...workspaceId ? { workspaceId } : {} };
|
|
2609
|
+
if (storeLocal) {
|
|
2610
|
+
const path = writeLocalConfig(patch);
|
|
2611
|
+
if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved to ${path} (directory-local)
|
|
2315
2612
|
`);
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2613
|
+
} else {
|
|
2614
|
+
writePersisted(patch);
|
|
2615
|
+
if (!opts.json) process.stderr.write(`${ok("\u2713")} config saved globally (~/.config/sechroom/config.json)
|
|
2616
|
+
`);
|
|
2617
|
+
}
|
|
2618
|
+
if (workspaceId && !existingWorkspace && !opts.json) {
|
|
2619
|
+
process.stderr.write(`${ok("\u2713")} bound to workspace ${style.dim(workspaceId)}
|
|
2319
2620
|
`);
|
|
2621
|
+
}
|
|
2320
2622
|
}
|
|
2321
|
-
return { baseUrl, tenant, clientId: persisted.clientId };
|
|
2623
|
+
return { baseUrl, tenant, workspaceId, defaultProjectId, clientId: persisted.clientId };
|
|
2322
2624
|
}
|
|
2323
2625
|
async function ensureAuth(cfg, yes) {
|
|
2324
2626
|
if (process.env.SECHROOM_TOKEN) return;
|
|
@@ -2373,7 +2675,7 @@ async function chooseClients(clientFlag, yes, cwd) {
|
|
|
2373
2675
|
return picks.length > 0 ? picks : preselected;
|
|
2374
2676
|
}
|
|
2375
2677
|
function registerOnboard(program2) {
|
|
2376
|
-
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 directory-local .sechroom.json instead of the global config", false).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("-y, --yes", "non-interactive: accept defaults (system timezone, detected clients, global config, full wire)", false).addHelpText(
|
|
2678
|
+
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 directory-local .sechroom.json 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(
|
|
2377
2679
|
"after",
|
|
2378
2680
|
`
|
|
2379
2681
|
Examples:
|
|
@@ -2381,16 +2683,22 @@ Examples:
|
|
|
2381
2683
|
$ sechroom onboard --cli-only just the CLI \u2014 no .mcp.json, no agent files
|
|
2382
2684
|
$ sechroom onboard --no-mcp agent instructions only, skip MCP config
|
|
2383
2685
|
$ sechroom onboard --local save tenant + base URL to ./.sechroom.json
|
|
2686
|
+
$ sechroom onboard --workspace wsp_XX bind this directory to a workspace (no pick prompt)
|
|
2687
|
+
$ sechroom onboard --refresh refresh out-of-date instruction blocks in place
|
|
2688
|
+
$ sechroom onboard --check CI/pre-commit: nonzero exit if instructions are out of date
|
|
2384
2689
|
$ sechroom onboard --yes non-interactive: defaults + global config + full wire
|
|
2385
2690
|
$ sechroom onboard --client all --dry-run preview wiring every client, write nothing`
|
|
2386
2691
|
).action(async (opts, cmd) => {
|
|
2387
2692
|
const g = cmd.optsWithGlobals();
|
|
2388
2693
|
const json = Boolean(g.json);
|
|
2389
|
-
const yes = Boolean(opts.yes);
|
|
2390
2694
|
const dryRun = Boolean(opts.dryRun);
|
|
2391
|
-
const
|
|
2392
|
-
|
|
2393
|
-
const
|
|
2695
|
+
const mode = opts.check ? "check" : opts.force ? "force" : "apply";
|
|
2696
|
+
const check = mode === "check";
|
|
2697
|
+
const yes = Boolean(opts.yes) || check;
|
|
2698
|
+
const baseUrl = resolveBaseUrl(g);
|
|
2699
|
+
await ensureAuth({ baseUrl, tenant: "", clientId: readPersisted().clientId }, yes);
|
|
2700
|
+
const cfg = await ensureTenant(baseUrl, g, { yes, json, local: Boolean(opts.local), workspace: opts.workspace, persist: !check });
|
|
2701
|
+
const tz = await ensureTimezone(cfg, { yes, dryRun: dryRun || check });
|
|
2394
2702
|
if (!json && tz.action !== "already-set") {
|
|
2395
2703
|
const line = tz.action === "set" ? `${ok("\u2713")} timezone set to ${tz.timezone}
|
|
2396
2704
|
` : tz.action === "dry-run" ? `(dry run \u2014 would set timezone to ${tz.timezone})
|
|
@@ -2401,7 +2709,7 @@ Examples:
|
|
|
2401
2709
|
const wire = await chooseWire(opts, yes);
|
|
2402
2710
|
if (wire === "cli-only") {
|
|
2403
2711
|
if (json) {
|
|
2404
|
-
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, timezone: tz, wire, clients: [] }, true);
|
|
2712
|
+
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, clients: [] }, true);
|
|
2405
2713
|
return;
|
|
2406
2714
|
}
|
|
2407
2715
|
process.stdout.write(
|
|
@@ -2416,7 +2724,7 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
2416
2724
|
const setup = await withSpinner("Fetching setup descriptors", () => fetchSetup(cfg));
|
|
2417
2725
|
const targets = clientTargets(process.cwd());
|
|
2418
2726
|
const personalWorkspaceId = await getPersonalWorkspaceId(cfg);
|
|
2419
|
-
if (!dryRun) {
|
|
2727
|
+
if (!dryRun && !check) {
|
|
2420
2728
|
await maybeOfferCopies(cfg, setup, targets, keys, personalWorkspaceId, copyChoice(opts));
|
|
2421
2729
|
}
|
|
2422
2730
|
const writeMcp = wire === "full";
|
|
@@ -2427,20 +2735,56 @@ Try: ${style.cyan('sechroom memory search "..."')} or ${style.cyan("sechroom -
|
|
|
2427
2735
|
dryRun,
|
|
2428
2736
|
mcp: writeMcp,
|
|
2429
2737
|
agentFiles: true,
|
|
2430
|
-
personalWorkspaceId
|
|
2738
|
+
personalWorkspaceId,
|
|
2739
|
+
mode
|
|
2431
2740
|
});
|
|
2432
2741
|
result.push({ client: key, actions });
|
|
2433
|
-
if (!json) printActions(target, actions);
|
|
2742
|
+
if (!json && !check) printActions(target, actions);
|
|
2743
|
+
}
|
|
2744
|
+
const evalCounts = { current: 0, stale: 0, drift: 0, absent: 0 };
|
|
2745
|
+
for (const { actions } of result) for (const a of actions) if (a.eval) evalCounts[a.eval]++;
|
|
2746
|
+
const wouldChange = evalCounts.stale + evalCounts.drift + evalCounts.absent;
|
|
2747
|
+
if (check) {
|
|
2748
|
+
if (json) {
|
|
2749
|
+
emit({ check: true, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, eval: evalCounts, wouldChange, clients: result }, true);
|
|
2750
|
+
} else if (wouldChange === 0) {
|
|
2751
|
+
process.stdout.write(`${ok("\u2713")} all instruction blocks are up to date.
|
|
2752
|
+
`);
|
|
2753
|
+
} else {
|
|
2754
|
+
const bits = [];
|
|
2755
|
+
if (evalCounts.stale) bits.push(`${evalCounts.stale} out of date`);
|
|
2756
|
+
if (evalCounts.drift) bits.push(`${evalCounts.drift} with local edits`);
|
|
2757
|
+
if (evalCounts.absent) bits.push(`${evalCounts.absent} not yet written`);
|
|
2758
|
+
process.stderr.write(
|
|
2759
|
+
`${warn("\u26A0")} ${wouldChange} instruction block(s) would change: ${bits.join(", ")}. Run ${style.cyan("sechroom onboard --refresh")}.
|
|
2760
|
+
`
|
|
2761
|
+
);
|
|
2762
|
+
}
|
|
2763
|
+
process.exit(wouldChange === 0 ? 0 : 1);
|
|
2434
2764
|
}
|
|
2435
2765
|
if (!json && !dryRun) {
|
|
2436
2766
|
await maybeOfferSkills(cfg, personalWorkspaceId, { yes, dryRun, surface: "claude-code" });
|
|
2437
2767
|
}
|
|
2438
2768
|
if (json) {
|
|
2439
|
-
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, timezone: tz, wire, clients: result }, true);
|
|
2769
|
+
emit({ dryRun, baseUrl: cfg.baseUrl, tenant: cfg.tenant, workspaceId: cfg.workspaceId ?? null, timezone: tz, wire, eval: evalCounts, clients: result }, true);
|
|
2440
2770
|
return;
|
|
2441
2771
|
}
|
|
2772
|
+
if (!dryRun && evalCounts.stale) {
|
|
2773
|
+
process.stderr.write(`${style.cyan("\u21BB")} refreshed ${evalCounts.stale} section(s) the server had moved
|
|
2774
|
+
`);
|
|
2775
|
+
}
|
|
2776
|
+
if (!dryRun && evalCounts.drift) {
|
|
2777
|
+
process.stderr.write(
|
|
2778
|
+
mode === "force" ? `${warn("\u26A0")} overwrote ${evalCounts.drift} section(s) that had local edits (--force)
|
|
2779
|
+
` : `${warn("\u26A0")} ${evalCounts.drift} section(s) have local edits \u2014 wrote a .proposed file alongside (original untouched). Review + merge, or re-run with ${style.cyan("--force")}.
|
|
2780
|
+
`
|
|
2781
|
+
);
|
|
2782
|
+
}
|
|
2783
|
+
const wroteSomething = result.some(({ actions }) => actions.some((a) => a.status === "created" || a.status === "merged"));
|
|
2442
2784
|
process.stdout.write(
|
|
2443
|
-
dryRun ? "\n(dry run \u2014 nothing written)\n" :
|
|
2785
|
+
dryRun ? "\n(dry run \u2014 nothing written)\n" : !wroteSomething ? `
|
|
2786
|
+
${style.bold("Done.")} Everything's already up to date.
|
|
2787
|
+
` : writeMcp ? `
|
|
2444
2788
|
${style.bold("Done.")} Restart your AI client (or reload MCP) to pick up the new config.
|
|
2445
2789
|
` : `
|
|
2446
2790
|
${style.bold("Done.")} Agent instructions written (no MCP config).
|
|
@@ -2821,10 +3165,10 @@ Examples:
|
|
|
2821
3165
|
$ sechroom config set clientId dyn-XXXX global-only escape hatch (no DCR endpoint)
|
|
2822
3166
|
$ sechroom config show --json`
|
|
2823
3167
|
);
|
|
2824
|
-
config.command("set <key> <value>").description("Set baseUrl | tenant | clientId (clientId is global-only)").option("--local", "Write to the directory-local .sechroom.json (nearest up the tree, else cwd) instead of the global config").action((key, value, opts) => {
|
|
3168
|
+
config.command("set <key> <value>").description("Set baseUrl | tenant | workspaceId | defaultProjectId | clientId (clientId is global-only)").option("--local", "Write to the directory-local .sechroom.json (nearest up the tree, else cwd) instead of the global config").action((key, value, opts) => {
|
|
2825
3169
|
if (opts.local) {
|
|
2826
|
-
if (!["baseUrl", "tenant"].includes(key)) {
|
|
2827
|
-
process.stderr.write(`--local supports only: baseUrl | tenant (clientId is global)
|
|
3170
|
+
if (!["baseUrl", "tenant", "workspaceId", "defaultProjectId"].includes(key)) {
|
|
3171
|
+
process.stderr.write(`--local supports only: baseUrl | tenant | workspaceId | defaultProjectId (clientId is global)
|
|
2828
3172
|
`);
|
|
2829
3173
|
process.exit(1);
|
|
2830
3174
|
}
|
|
@@ -2833,8 +3177,8 @@ config.command("set <key> <value>").description("Set baseUrl | tenant | clientId
|
|
|
2833
3177
|
`);
|
|
2834
3178
|
return;
|
|
2835
3179
|
}
|
|
2836
|
-
if (!["baseUrl", "tenant", "clientId"].includes(key)) {
|
|
2837
|
-
process.stderr.write(`unknown key: ${key} (expected baseUrl | tenant | clientId)
|
|
3180
|
+
if (!["baseUrl", "tenant", "clientId", "workspaceId", "defaultProjectId"].includes(key)) {
|
|
3181
|
+
process.stderr.write(`unknown key: ${key} (expected baseUrl | tenant | workspaceId | defaultProjectId | clientId)
|
|
2838
3182
|
`);
|
|
2839
3183
|
process.exit(1);
|
|
2840
3184
|
}
|
|
@@ -2848,7 +3192,7 @@ config.command("show").description("Print resolved config + sources (flag > env
|
|
|
2848
3192
|
if (g.json) {
|
|
2849
3193
|
process.stdout.write(
|
|
2850
3194
|
JSON.stringify({
|
|
2851
|
-
resolved: { baseUrl: d.baseUrl, tenant: d.tenant },
|
|
3195
|
+
resolved: { baseUrl: d.baseUrl, tenant: d.tenant, workspaceId: d.workspaceId },
|
|
2852
3196
|
global: readPersisted(),
|
|
2853
3197
|
local: readLocalConfig()
|
|
2854
3198
|
}) + "\n"
|
|
@@ -2856,8 +3200,9 @@ config.command("show").description("Print resolved config + sources (flag > env
|
|
|
2856
3200
|
return;
|
|
2857
3201
|
}
|
|
2858
3202
|
process.stdout.write(
|
|
2859
|
-
`baseUrl:
|
|
2860
|
-
tenant:
|
|
3203
|
+
`baseUrl: ${d.baseUrl.value} [${d.baseUrl.source}]
|
|
3204
|
+
tenant: ${d.tenant.value ?? "(unset)"} [${d.tenant.source}]
|
|
3205
|
+
workspaceId: ${d.workspaceId.value ?? "(unset)"} [${d.workspaceId.source}]
|
|
2861
3206
|
|
|
2862
3207
|
global: ${JSON.stringify(readPersisted())}
|
|
2863
3208
|
local: ${d.localPath ?? "(none)"} ${JSON.stringify(readLocalConfig())}
|
package/package.json
CHANGED