@cmnwlth/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/dist/index.d.ts +442 -0
- package/dist/index.js +1081 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1081 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/index.ts
|
|
4
|
+
import { spawnSync as spawnSync2 } from "child_process";
|
|
5
|
+
import path6 from "path";
|
|
6
|
+
import { parseArgs } from "util";
|
|
7
|
+
|
|
8
|
+
// src/commands.ts
|
|
9
|
+
import { spawn } from "child_process";
|
|
10
|
+
import { createRequire } from "module";
|
|
11
|
+
import path2 from "path";
|
|
12
|
+
import { FEATURE_FLAGS, loadBrainConfig, resolveBrainDir, setFeature } from "@cmnwlth/core";
|
|
13
|
+
import { gatherCandidates } from "@cmnwlth/seed";
|
|
14
|
+
|
|
15
|
+
// src/discover.ts
|
|
16
|
+
import { promises as fs } from "fs";
|
|
17
|
+
import path from "path";
|
|
18
|
+
async function findGitRepos(baseDir, opts = {}) {
|
|
19
|
+
const maxDepth = opts.maxDepth ?? 2;
|
|
20
|
+
const root = path.resolve(baseDir);
|
|
21
|
+
const found = /* @__PURE__ */ new Set();
|
|
22
|
+
const visit = async (dir, depth) => {
|
|
23
|
+
let entries;
|
|
24
|
+
try {
|
|
25
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
26
|
+
} catch {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (entries.some((e) => e.name === ".git")) {
|
|
30
|
+
found.add(dir);
|
|
31
|
+
}
|
|
32
|
+
if (depth >= maxDepth) return;
|
|
33
|
+
for (const entry of entries) {
|
|
34
|
+
if (!entry.isDirectory() && !entry.isSymbolicLink()) continue;
|
|
35
|
+
if (entry.name === "node_modules") continue;
|
|
36
|
+
if (entry.name.startsWith(".")) continue;
|
|
37
|
+
const child = path.join(dir, entry.name);
|
|
38
|
+
try {
|
|
39
|
+
const stat = await fs.stat(child);
|
|
40
|
+
if (!stat.isDirectory()) continue;
|
|
41
|
+
} catch {
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
await visit(child, depth + 1);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
await visit(root, 0);
|
|
48
|
+
return [...found].sort();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// src/commands.ts
|
|
52
|
+
var require2 = createRequire(import.meta.url);
|
|
53
|
+
function resolvePkgBin(pkg) {
|
|
54
|
+
const pkgJsonPath = require2.resolve(`${pkg}/package.json`);
|
|
55
|
+
const pkgJson = require2(`${pkg}/package.json`);
|
|
56
|
+
const rel = typeof pkgJson.bin === "string" ? pkgJson.bin : Object.values(pkgJson.bin ?? {})[0];
|
|
57
|
+
if (!rel) throw new Error(`${pkg} exposes no bin`);
|
|
58
|
+
return path2.join(path2.dirname(pkgJsonPath), rel);
|
|
59
|
+
}
|
|
60
|
+
function runBin(bin, args, input) {
|
|
61
|
+
return new Promise((resolve) => {
|
|
62
|
+
const child = spawn("node", [bin, ...args], {
|
|
63
|
+
stdio: [input === void 0 ? "inherit" : "pipe", "inherit", "inherit"]
|
|
64
|
+
});
|
|
65
|
+
child.on("error", () => resolve(1));
|
|
66
|
+
child.on("close", (code) => resolve(code ?? 0));
|
|
67
|
+
if (input !== void 0 && child.stdin) {
|
|
68
|
+
child.stdin.on("error", () => {
|
|
69
|
+
});
|
|
70
|
+
child.stdin.write(input, () => child.stdin.end());
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async function brainOrError() {
|
|
75
|
+
const env = process.env.COMMONWEALTH_BRAIN_DIR;
|
|
76
|
+
const brain = env && env.length > 0 ? path2.resolve(env) : await resolveBrainDir(process.cwd());
|
|
77
|
+
if (!brain) {
|
|
78
|
+
process.stderr.write(
|
|
79
|
+
`No Commonwealth brain is configured for ${process.cwd()}. Run \`commonwealth init\` here, or add a prefix \u2192 brain mapping to ~/.commonwealth/registry.json.
|
|
80
|
+
`
|
|
81
|
+
);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return brain;
|
|
85
|
+
}
|
|
86
|
+
async function cmdConfig(rest) {
|
|
87
|
+
const brain = await brainOrError();
|
|
88
|
+
if (!brain) return 1;
|
|
89
|
+
const [sub, key, value] = rest;
|
|
90
|
+
const config = await loadBrainConfig(brain);
|
|
91
|
+
const flagNames = FEATURE_FLAGS.map((f) => f.name);
|
|
92
|
+
if (sub === void 0 || sub === "list") {
|
|
93
|
+
process.stdout.write(
|
|
94
|
+
`brain: ${brain}
|
|
95
|
+
name: ${config.name}
|
|
96
|
+
remotes: ${config.remotes.join(", ") || "(none)"}
|
|
97
|
+
features:
|
|
98
|
+
`
|
|
99
|
+
);
|
|
100
|
+
for (const flag of FEATURE_FLAGS) {
|
|
101
|
+
const val = config.features[flag.name] ?? flag.default;
|
|
102
|
+
process.stdout.write(` ${flag.name} = ${val} \u2014 ${flag.description}
|
|
103
|
+
`);
|
|
104
|
+
}
|
|
105
|
+
return 0;
|
|
106
|
+
}
|
|
107
|
+
if (sub === "get") {
|
|
108
|
+
if (!key) {
|
|
109
|
+
process.stderr.write("usage: commonwealth config get <key>\n");
|
|
110
|
+
return 2;
|
|
111
|
+
}
|
|
112
|
+
if (key === "name") process.stdout.write(`${config.name}
|
|
113
|
+
`);
|
|
114
|
+
else if (flagNames.includes(key)) process.stdout.write(`${config.features[key] ?? false}
|
|
115
|
+
`);
|
|
116
|
+
else {
|
|
117
|
+
process.stderr.write(`Unknown config key "${key}". Known: name, ${flagNames.join(", ")}
|
|
118
|
+
`);
|
|
119
|
+
return 2;
|
|
120
|
+
}
|
|
121
|
+
return 0;
|
|
122
|
+
}
|
|
123
|
+
if (sub === "set") {
|
|
124
|
+
if (!key || value === void 0) {
|
|
125
|
+
process.stderr.write("usage: commonwealth config set <key> <value>\n");
|
|
126
|
+
return 2;
|
|
127
|
+
}
|
|
128
|
+
if (!flagNames.includes(key)) {
|
|
129
|
+
process.stderr.write(`Unknown feature flag "${key}". Known: ${flagNames.join(", ")}
|
|
130
|
+
`);
|
|
131
|
+
return 2;
|
|
132
|
+
}
|
|
133
|
+
const on = value === "true" || value === "1" || value === "on" || value === "yes";
|
|
134
|
+
await setFeature(brain, key, on);
|
|
135
|
+
process.stderr.write(`[commonwealth] set ${key} = ${on} for ${brain}
|
|
136
|
+
`);
|
|
137
|
+
return 0;
|
|
138
|
+
}
|
|
139
|
+
process.stderr.write("usage: commonwealth config <list | get <key> | set <key> <value>>\n");
|
|
140
|
+
return 2;
|
|
141
|
+
}
|
|
142
|
+
async function cmdReseed(rest) {
|
|
143
|
+
const brain = await brainOrError();
|
|
144
|
+
if (!brain) return 1;
|
|
145
|
+
const all = rest.includes("--all");
|
|
146
|
+
const explicit = rest.filter((a) => !a.startsWith("-")).map((a) => path2.resolve(a));
|
|
147
|
+
const repos = explicit.length > 0 ? explicit : all ? await findGitRepos(process.cwd()) : [path2.resolve(process.cwd())];
|
|
148
|
+
if (repos.length === 0) {
|
|
149
|
+
process.stderr.write("No repositories to reseed.\n");
|
|
150
|
+
return 0;
|
|
151
|
+
}
|
|
152
|
+
const curateBin = resolvePkgBin("@cmnwlth/curate");
|
|
153
|
+
let total = 0;
|
|
154
|
+
for (const repo of repos) {
|
|
155
|
+
const { candidates } = await gatherCandidates(repo);
|
|
156
|
+
if (candidates.length === 0) {
|
|
157
|
+
process.stderr.write(`[commonwealth] ${repo}: no candidates.
|
|
158
|
+
`);
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
process.stderr.write(`[commonwealth] ${repo}: mined ${candidates.length} candidate(s)\u2026
|
|
162
|
+
`);
|
|
163
|
+
const code = await runBin(
|
|
164
|
+
curateBin,
|
|
165
|
+
["capture", "--dir", brain, "--cwd", repo, "--force"],
|
|
166
|
+
JSON.stringify(candidates)
|
|
167
|
+
);
|
|
168
|
+
if (code === 0) total += candidates.length;
|
|
169
|
+
}
|
|
170
|
+
process.stderr.write(
|
|
171
|
+
`[commonwealth] reseed done: ${total} candidate(s) captured into ${brain}.
|
|
172
|
+
`
|
|
173
|
+
);
|
|
174
|
+
return 0;
|
|
175
|
+
}
|
|
176
|
+
function delegateCurate(args) {
|
|
177
|
+
return runBin(resolvePkgBin("@cmnwlth/curate"), args);
|
|
178
|
+
}
|
|
179
|
+
function delegateSync(args) {
|
|
180
|
+
return runBin(resolvePkgBin("@cmnwlth/sync"), args);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// src/deps.ts
|
|
184
|
+
import { spawn as spawn2, spawnSync } from "child_process";
|
|
185
|
+
import { existsSync as existsSync2, promises as fs2 } from "fs";
|
|
186
|
+
import { createInterface } from "readline";
|
|
187
|
+
import { createRequire as createRequire2 } from "module";
|
|
188
|
+
import os2 from "os";
|
|
189
|
+
import path4 from "path";
|
|
190
|
+
import { fileURLToPath } from "url";
|
|
191
|
+
import * as core from "@cmnwlth/core";
|
|
192
|
+
import { gatherCandidates as gatherCandidates2 } from "@cmnwlth/seed";
|
|
193
|
+
|
|
194
|
+
// src/init.ts
|
|
195
|
+
import { createHash } from "crypto";
|
|
196
|
+
import { existsSync } from "fs";
|
|
197
|
+
import os from "os";
|
|
198
|
+
import path3 from "path";
|
|
199
|
+
function findRepoRoot(startDir) {
|
|
200
|
+
let dir = path3.resolve(startDir);
|
|
201
|
+
for (; ; ) {
|
|
202
|
+
if (existsSync(path3.join(dir, ".git"))) return dir;
|
|
203
|
+
const parent = path3.dirname(dir);
|
|
204
|
+
if (parent === dir) return path3.resolve(startDir);
|
|
205
|
+
dir = parent;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
function defaultBrainDir(repoRoot) {
|
|
209
|
+
const resolved = path3.resolve(repoRoot);
|
|
210
|
+
const hash = createHash("sha256").update(resolved).digest("hex").slice(0, 8);
|
|
211
|
+
return path3.join(os.homedir(), ".commonwealth", "brains", `${path3.basename(resolved)}-${hash}`);
|
|
212
|
+
}
|
|
213
|
+
async function runInit(cwd, opts, deps) {
|
|
214
|
+
const projectDir = path3.resolve(cwd);
|
|
215
|
+
const repoRoot = findRepoRoot(cwd);
|
|
216
|
+
const existing = await deps.resolveBrain(cwd);
|
|
217
|
+
if (existing !== null && !opts.reseed && !opts.brain) {
|
|
218
|
+
await deps.registerBrain(projectDir, existing);
|
|
219
|
+
deps.log(`Joined existing brain at ${existing}. Run the sync daemon to pull.`);
|
|
220
|
+
return { mode: "join", brainDir: existing, staged: 0, gathered: 0 };
|
|
221
|
+
}
|
|
222
|
+
const brainDir = opts.brain ?? existing ?? defaultBrainDir(projectDir);
|
|
223
|
+
await deps.createBrain(brainDir, path3.basename(brainDir));
|
|
224
|
+
await deps.registerBrain(projectDir, brainDir);
|
|
225
|
+
if (opts.seed === false) {
|
|
226
|
+
deps.log(`Seeding skipped. Brain created at ${brainDir}.`);
|
|
227
|
+
return { mode: "skipped", brainDir, staged: 0, gathered: 0 };
|
|
228
|
+
}
|
|
229
|
+
const { candidates, bySource } = await deps.gather(repoRoot);
|
|
230
|
+
deps.log(
|
|
231
|
+
`Found ${candidates.length} candidates \u2014 adr:${bySource.adr} git:${bySource.git} config:${bySource.config}`
|
|
232
|
+
);
|
|
233
|
+
if (candidates.length > 0 && !opts.yes) {
|
|
234
|
+
const ok = await deps.confirm(
|
|
235
|
+
`Seed ${candidates.length} candidates into the brain's review queue?`
|
|
236
|
+
);
|
|
237
|
+
if (!ok) {
|
|
238
|
+
deps.log(`Skipped seeding. Brain created at ${brainDir}.`);
|
|
239
|
+
return { mode: "skipped", brainDir, staged: 0, gathered: candidates.length };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const { captured } = candidates.length ? await deps.stage(brainDir, candidates) : { captured: 0 };
|
|
243
|
+
deps.log(
|
|
244
|
+
`Your brain has ${captured} notes pending review. Run \`commonwealth-curate list\` to approve, then start a Claude session here and ask it something your team already knows.`
|
|
245
|
+
);
|
|
246
|
+
return { mode: "new", brainDir, staged: captured, gathered: candidates.length };
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/deps.ts
|
|
250
|
+
function resolveCurateEntry(override) {
|
|
251
|
+
if (override) return override;
|
|
252
|
+
const fromEnv = process.env.COMMONWEALTH_CURATE_BIN;
|
|
253
|
+
if (fromEnv) return fromEnv;
|
|
254
|
+
const require3 = createRequire2(import.meta.url);
|
|
255
|
+
const pkgJsonPath = require3.resolve("@cmnwlth/curate/package.json");
|
|
256
|
+
const pkg = require3("@cmnwlth/curate/package.json");
|
|
257
|
+
const rel = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.["commonwealth-curate"];
|
|
258
|
+
if (!rel) throw new Error("@cmnwlth/curate exposes no `commonwealth-curate` bin");
|
|
259
|
+
return path4.resolve(path4.dirname(pkgJsonPath), rel);
|
|
260
|
+
}
|
|
261
|
+
function makeStage(curateEntry, log) {
|
|
262
|
+
return (brainDir, candidates) => new Promise((resolve) => {
|
|
263
|
+
const child = spawn2("node", [curateEntry, "capture", "--dir", brainDir, "--force"], {
|
|
264
|
+
stdio: ["pipe", "pipe", "inherit"]
|
|
265
|
+
});
|
|
266
|
+
let out = "";
|
|
267
|
+
child.stdout.setEncoding("utf8");
|
|
268
|
+
child.stdout.on("data", (chunk) => {
|
|
269
|
+
out += chunk;
|
|
270
|
+
});
|
|
271
|
+
child.on("error", (err) => {
|
|
272
|
+
log(`Staging failed to spawn commonwealth-curate: ${err.message}`);
|
|
273
|
+
resolve({ captured: 0 });
|
|
274
|
+
});
|
|
275
|
+
child.on("close", (code) => {
|
|
276
|
+
if (code !== 0) {
|
|
277
|
+
log(`commonwealth-curate exited with code ${code ?? "null"}; staged nothing.`);
|
|
278
|
+
resolve({ captured: 0 });
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
const captured = out.split("\n").filter((line) => line.trim().length > 0).length;
|
|
282
|
+
resolve({ captured });
|
|
283
|
+
});
|
|
284
|
+
child.stdin.on("error", () => {
|
|
285
|
+
});
|
|
286
|
+
child.stdin.end(JSON.stringify(candidates));
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
function promptConfirm(message) {
|
|
290
|
+
return new Promise((resolve) => {
|
|
291
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
292
|
+
rl.question(`${message} [y/N] `, (answer) => {
|
|
293
|
+
rl.close();
|
|
294
|
+
resolve(/^y(es)?$/i.test(answer.trim()));
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
function defaultInitDeps(opts = {}) {
|
|
299
|
+
const log = (m) => {
|
|
300
|
+
process.stderr.write(m + "\n");
|
|
301
|
+
};
|
|
302
|
+
let curateEntry = null;
|
|
303
|
+
let curateError = null;
|
|
304
|
+
try {
|
|
305
|
+
curateEntry = resolveCurateEntry(opts.curateEntry);
|
|
306
|
+
} catch (err) {
|
|
307
|
+
curateError = err.message;
|
|
308
|
+
}
|
|
309
|
+
const stage = curateEntry ? makeStage(curateEntry, log) : async () => {
|
|
310
|
+
log(`Cannot stage: ${curateError ?? "commonwealth-curate not found"}.`);
|
|
311
|
+
return { captured: 0 };
|
|
312
|
+
};
|
|
313
|
+
return {
|
|
314
|
+
gather: (repoDir) => gatherCandidates2(repoDir),
|
|
315
|
+
resolveBrain: (cwd) => core.resolveBrainDir(cwd),
|
|
316
|
+
createBrain: (dir, name) => core.initBrain(dir, { name }),
|
|
317
|
+
registerBrain: async (repoDir, brainDir) => {
|
|
318
|
+
await core.addRegistryMapping(repoDir, brainDir);
|
|
319
|
+
await core.linkBrain(path4.basename(brainDir), brainDir);
|
|
320
|
+
},
|
|
321
|
+
stage,
|
|
322
|
+
confirm: opts.assumeYes ? async () => true : promptConfirm,
|
|
323
|
+
log
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
var REQUIRED_DIST_PACKAGES = ["core", "mcp", "sync", "curate", "seed", "cli"];
|
|
327
|
+
function hasExecutable(name) {
|
|
328
|
+
const probe = process.platform === "win32" ? "where" : "which";
|
|
329
|
+
const res = spawnSync(probe, [name], { stdio: "ignore" });
|
|
330
|
+
return res.status === 0;
|
|
331
|
+
}
|
|
332
|
+
function hasClaudeEntry(args, name) {
|
|
333
|
+
const json = spawnSync("claude", [...args, "--json"], { encoding: "utf8" });
|
|
334
|
+
if (json.status === 0 && json.stdout) {
|
|
335
|
+
try {
|
|
336
|
+
const parsed = JSON.parse(json.stdout);
|
|
337
|
+
const rows = Array.isArray(parsed) ? parsed : Array.isArray(parsed.plugins) ? parsed.plugins : [];
|
|
338
|
+
return rows.some((row) => {
|
|
339
|
+
if (typeof row !== "object" || row === null) return false;
|
|
340
|
+
const r = row;
|
|
341
|
+
return r.id === name || r.name === name || r.plugin === name || typeof r.id === "string" && r.id.split("@")[0] === name;
|
|
342
|
+
});
|
|
343
|
+
} catch {
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
const text = spawnSync("claude", args, { encoding: "utf8" });
|
|
347
|
+
const out = `${text.stdout ?? ""}${text.stderr ?? ""}`;
|
|
348
|
+
return text.status === 0 && new RegExp(`(^|\\s)${name}(\\s|@|$)`, "m").test(out);
|
|
349
|
+
}
|
|
350
|
+
function commonwealthRoot() {
|
|
351
|
+
let dir = path4.dirname(fileURLToPath(import.meta.url));
|
|
352
|
+
for (; ; ) {
|
|
353
|
+
if (existsSync2(path4.join(dir, "pnpm-workspace.yaml"))) return dir;
|
|
354
|
+
const parent = path4.dirname(dir);
|
|
355
|
+
if (parent === dir) return null;
|
|
356
|
+
dir = parent;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function defaultOnboardDeps(opts = {}) {
|
|
360
|
+
const log = (m) => {
|
|
361
|
+
process.stderr.write(m + "\n");
|
|
362
|
+
};
|
|
363
|
+
const repoRoot = opts.repoRoot ?? commonwealthRoot() ?? findRepoRoot(process.cwd());
|
|
364
|
+
const isWorkspace = existsSync2(path4.join(repoRoot, "pnpm-workspace.yaml"));
|
|
365
|
+
const distEntry = (pkg) => path4.join(repoRoot, "packages", pkg, "dist", "index.js");
|
|
366
|
+
const ensureBuilt = async () => {
|
|
367
|
+
if (!isWorkspace) {
|
|
368
|
+
return { built: false, skipped: "not a workspace checkout; assuming an installed build" };
|
|
369
|
+
}
|
|
370
|
+
const missing = REQUIRED_DIST_PACKAGES.filter((pkg) => !existsSync2(distEntry(pkg)));
|
|
371
|
+
if (missing.length === 0) return { built: false };
|
|
372
|
+
if (!hasExecutable("pnpm")) {
|
|
373
|
+
return {
|
|
374
|
+
built: false,
|
|
375
|
+
skipped: `pnpm not found; cannot build (missing: ${missing.join(", ")})`
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
const build = spawnSync("pnpm", ["-r", "build"], { cwd: repoRoot, stdio: "inherit" });
|
|
379
|
+
if (build.status !== 0) {
|
|
380
|
+
const reason = build.error ? build.error.message : build.signal ? `signal ${build.signal}` : `exit code ${build.status}`;
|
|
381
|
+
return { built: false, skipped: `pnpm -r build failed (${reason})` };
|
|
382
|
+
}
|
|
383
|
+
const bundle = path4.join(repoRoot, "packages", "plugin", "scripts", "bundle.mjs");
|
|
384
|
+
if (existsSync2(bundle)) {
|
|
385
|
+
const res = spawnSync("node", [bundle], { cwd: repoRoot, stdio: "inherit" });
|
|
386
|
+
if (res.status !== 0)
|
|
387
|
+
log(`Plugin bundle exited with code ${res.status ?? "null"} (ignored).`);
|
|
388
|
+
}
|
|
389
|
+
return { built: true };
|
|
390
|
+
};
|
|
391
|
+
const init = (cwd, initOpts) => runInit(
|
|
392
|
+
cwd,
|
|
393
|
+
initOpts,
|
|
394
|
+
defaultInitDeps({ assumeYes: initOpts.yes, curateEntry: opts.curateEntry })
|
|
395
|
+
);
|
|
396
|
+
const configureScope = async (repoRoot2) => {
|
|
397
|
+
let curateEntry;
|
|
398
|
+
try {
|
|
399
|
+
curateEntry = resolveCurateEntry(opts.curateEntry);
|
|
400
|
+
} catch (err) {
|
|
401
|
+
return { added: false, skipped: `commonwealth-curate not found: ${err.message}` };
|
|
402
|
+
}
|
|
403
|
+
const res = spawnSync("node", [curateEntry, "scope", "allow", repoRoot2], { stdio: "ignore" });
|
|
404
|
+
if (res.error || res.status !== 0) {
|
|
405
|
+
const reason = res.error ? res.error.message : `exit code ${res.status ?? "null"}`;
|
|
406
|
+
return { added: false, skipped: `scope allow failed (${reason})` };
|
|
407
|
+
}
|
|
408
|
+
return { added: true };
|
|
409
|
+
};
|
|
410
|
+
const setAutoAdr = async (brainDir, on) => {
|
|
411
|
+
let curateEntry;
|
|
412
|
+
try {
|
|
413
|
+
curateEntry = resolveCurateEntry(opts.curateEntry);
|
|
414
|
+
} catch (err) {
|
|
415
|
+
return { set: false, skipped: `commonwealth-curate not found: ${err.message}` };
|
|
416
|
+
}
|
|
417
|
+
const verb = on ? "enable" : "disable";
|
|
418
|
+
const res = spawnSync("node", [curateEntry, "feature", verb, "autoAdr", "--dir", brainDir], {
|
|
419
|
+
stdio: "ignore"
|
|
420
|
+
});
|
|
421
|
+
if (res.error || res.status !== 0) {
|
|
422
|
+
const reason = res.error ? res.error.message : `exit code ${res.status ?? "null"}`;
|
|
423
|
+
return { set: false, skipped: `feature ${verb} autoAdr failed (${reason})` };
|
|
424
|
+
}
|
|
425
|
+
return { set: true };
|
|
426
|
+
};
|
|
427
|
+
const setRemote = async (brainDir, url) => {
|
|
428
|
+
const existing = spawnSync("git", ["-C", brainDir, "remote", "get-url", "origin"], {
|
|
429
|
+
stdio: "ignore"
|
|
430
|
+
});
|
|
431
|
+
if (existing.status === 0) return { set: false, skipped: "origin exists" };
|
|
432
|
+
const add = spawnSync("git", ["-C", brainDir, "remote", "add", "origin", url], {
|
|
433
|
+
stdio: "ignore"
|
|
434
|
+
});
|
|
435
|
+
if (add.error || add.status !== 0) {
|
|
436
|
+
const reason = add.error ? add.error.message : `exit code ${add.status ?? "null"}`;
|
|
437
|
+
return { set: false, skipped: `git remote add failed (${reason})` };
|
|
438
|
+
}
|
|
439
|
+
return { set: true };
|
|
440
|
+
};
|
|
441
|
+
const installPlugin = async () => {
|
|
442
|
+
if (!hasExecutable("claude")) {
|
|
443
|
+
return { installed: false, skipped: "claude CLI not found" };
|
|
444
|
+
}
|
|
445
|
+
const bundle = path4.join(repoRoot, "packages", "plugin", "scripts", "bundle.mjs");
|
|
446
|
+
if (existsSync2(bundle)) {
|
|
447
|
+
const res = spawnSync("node", [bundle], { cwd: repoRoot, stdio: "inherit" });
|
|
448
|
+
if (res.status !== 0)
|
|
449
|
+
log(`Plugin bundle exited with code ${res.status ?? "null"} (ignored).`);
|
|
450
|
+
}
|
|
451
|
+
if (!hasClaudeEntry(["plugin", "marketplace", "list"], "commonwealth")) {
|
|
452
|
+
const add = spawnSync("claude", ["plugin", "marketplace", "add", repoRoot], {
|
|
453
|
+
stdio: "inherit"
|
|
454
|
+
});
|
|
455
|
+
if (add.error || add.status !== 0) {
|
|
456
|
+
return {
|
|
457
|
+
installed: false,
|
|
458
|
+
skipped: `claude plugin marketplace add failed (code ${add.status ?? "null"})`
|
|
459
|
+
};
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (!hasClaudeEntry(["plugin", "list"], "commonwealth")) {
|
|
463
|
+
const install = spawnSync("claude", ["plugin", "install", "commonwealth@cmnwlth"], {
|
|
464
|
+
stdio: "inherit"
|
|
465
|
+
});
|
|
466
|
+
if (install.error || install.status !== 0) {
|
|
467
|
+
return {
|
|
468
|
+
installed: false,
|
|
469
|
+
skipped: `claude plugin install failed (code ${install.status ?? "null"})`
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
const staleGet = spawnSync("claude", ["mcp", "get", "commonwealth"], { stdio: "ignore" });
|
|
474
|
+
if (staleGet.status === 0) {
|
|
475
|
+
spawnSync("claude", ["mcp", "remove", "commonwealth", "-s", "local"], { stdio: "ignore" });
|
|
476
|
+
}
|
|
477
|
+
return { installed: true };
|
|
478
|
+
};
|
|
479
|
+
const startDaemon = async (brainDir) => {
|
|
480
|
+
const syncEntry = distEntry("sync");
|
|
481
|
+
if (!existsSync2(syncEntry)) {
|
|
482
|
+
return { started: false, skipped: "sync daemon not built" };
|
|
483
|
+
}
|
|
484
|
+
const status = spawnSync("node", [syncEntry, "status", "--dir", brainDir], {
|
|
485
|
+
encoding: "utf8"
|
|
486
|
+
});
|
|
487
|
+
const output = `${status.stdout ?? ""}${status.stderr ?? ""}`;
|
|
488
|
+
if (/\brunning on\b/.test(output) && !/\bnot running on\b/.test(output)) {
|
|
489
|
+
return { started: false, alreadyRunning: true };
|
|
490
|
+
}
|
|
491
|
+
try {
|
|
492
|
+
const child = spawn2("node", [syncEntry, "start", "--dir", brainDir], {
|
|
493
|
+
detached: true,
|
|
494
|
+
stdio: "ignore"
|
|
495
|
+
});
|
|
496
|
+
child.unref();
|
|
497
|
+
return { started: true };
|
|
498
|
+
} catch (err) {
|
|
499
|
+
return { started: false, skipped: `failed to start daemon: ${err.message}` };
|
|
500
|
+
}
|
|
501
|
+
};
|
|
502
|
+
const registerBrain = async (folder, brainDir) => {
|
|
503
|
+
try {
|
|
504
|
+
const map = await core.addRegistryMapping(folder, brainDir);
|
|
505
|
+
const link = await core.linkBrain(path4.basename(brainDir), brainDir);
|
|
506
|
+
return { mapped: map.added || map.updated, linked: link.linked, skipped: link.skipped };
|
|
507
|
+
} catch (err) {
|
|
508
|
+
return { mapped: false, linked: false, skipped: err.message };
|
|
509
|
+
}
|
|
510
|
+
};
|
|
511
|
+
const seedFrom = async (brainDir, repoDir) => {
|
|
512
|
+
let curateEntry;
|
|
513
|
+
try {
|
|
514
|
+
curateEntry = resolveCurateEntry(opts.curateEntry);
|
|
515
|
+
} catch (err) {
|
|
516
|
+
return { staged: 0, skipped: `commonwealth-curate not found: ${err.message}` };
|
|
517
|
+
}
|
|
518
|
+
const seedEntry = distEntry("seed");
|
|
519
|
+
if (!existsSync2(seedEntry)) return { staged: 0, skipped: "seed CLI not built" };
|
|
520
|
+
return new Promise((resolve) => {
|
|
521
|
+
const gather = spawn2("node", [seedEntry, "gather", "--repo", repoDir], {
|
|
522
|
+
stdio: ["ignore", "pipe", "inherit"]
|
|
523
|
+
});
|
|
524
|
+
const capture = spawn2("node", [curateEntry, "capture", "--dir", brainDir, "--force"], {
|
|
525
|
+
stdio: ["pipe", "pipe", "inherit"]
|
|
526
|
+
});
|
|
527
|
+
let settled = false;
|
|
528
|
+
const done = (result) => {
|
|
529
|
+
if (settled) return;
|
|
530
|
+
settled = true;
|
|
531
|
+
resolve(result);
|
|
532
|
+
};
|
|
533
|
+
gather.on(
|
|
534
|
+
"error",
|
|
535
|
+
(err) => done({ staged: 0, skipped: `seed spawn failed (${err.message})` })
|
|
536
|
+
);
|
|
537
|
+
capture.on(
|
|
538
|
+
"error",
|
|
539
|
+
(err) => done({ staged: 0, skipped: `capture spawn failed (${err.message})` })
|
|
540
|
+
);
|
|
541
|
+
gather.stdout.pipe(capture.stdin);
|
|
542
|
+
let out = "";
|
|
543
|
+
capture.stdout.setEncoding("utf8");
|
|
544
|
+
capture.stdout.on("data", (chunk) => {
|
|
545
|
+
out += chunk;
|
|
546
|
+
});
|
|
547
|
+
capture.on("close", (code) => {
|
|
548
|
+
if (code !== 0) {
|
|
549
|
+
done({ staged: 0, skipped: `capture exited with code ${code ?? "null"}` });
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
const staged = out.split("\n").filter((line) => line.trim().length > 0).length;
|
|
553
|
+
done({ staged });
|
|
554
|
+
});
|
|
555
|
+
});
|
|
556
|
+
};
|
|
557
|
+
const ensureUserConfig = async () => {
|
|
558
|
+
const configPath = process.env.COMMONWEALTH_CONFIG ?? path4.join(os2.homedir(), ".commonwealth", "config.json");
|
|
559
|
+
if (!existsSync2(configPath)) {
|
|
560
|
+
try {
|
|
561
|
+
await fs2.mkdir(path4.dirname(configPath), { recursive: true });
|
|
562
|
+
await fs2.writeFile(
|
|
563
|
+
configPath,
|
|
564
|
+
`${JSON.stringify({ allow: [], deny: [] }, null, 2)}
|
|
565
|
+
`,
|
|
566
|
+
"utf8"
|
|
567
|
+
);
|
|
568
|
+
} catch (err) {
|
|
569
|
+
log(`Could not create scope config at ${configPath}: ${err.message}`);
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return { path: configPath };
|
|
573
|
+
};
|
|
574
|
+
return {
|
|
575
|
+
ensureBuilt,
|
|
576
|
+
init,
|
|
577
|
+
configureScope,
|
|
578
|
+
registerBrain,
|
|
579
|
+
seedFrom,
|
|
580
|
+
ensureUserConfig,
|
|
581
|
+
setAutoAdr,
|
|
582
|
+
setRemote,
|
|
583
|
+
installPlugin,
|
|
584
|
+
startDaemon,
|
|
585
|
+
confirm: promptConfirm,
|
|
586
|
+
log
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
// src/onboard.ts
|
|
591
|
+
import path5 from "path";
|
|
592
|
+
async function runOnboard(cwd, opts, deps) {
|
|
593
|
+
const doBuild = opts.build !== false;
|
|
594
|
+
const doSeed = opts.seed !== false;
|
|
595
|
+
const doScope = opts.scope !== false;
|
|
596
|
+
const doAutoAdr = opts.autoAdr === true;
|
|
597
|
+
const doRemote = typeof opts.remote === "string" && opts.remote.trim().length > 0;
|
|
598
|
+
const doPlugin = opts.plugin !== false && opts.mcp !== false;
|
|
599
|
+
const doDaemon = opts.daemon !== false;
|
|
600
|
+
const syncFolders = opts.syncFolders ?? [path5.resolve(cwd)];
|
|
601
|
+
const seedRepos = opts.seedRepos ?? (doSeed ? syncFolders : []);
|
|
602
|
+
const brainLabel = opts.brain ?? "the project's default brain dir";
|
|
603
|
+
const plan = [
|
|
604
|
+
doBuild ? "build the workspace if needed" : null,
|
|
605
|
+
`create brain at ${brainLabel}`,
|
|
606
|
+
doScope ? `sync ${syncFolders.length} folder(s) into the brain` : null,
|
|
607
|
+
seedRepos.length > 0 ? `seed from ${seedRepos.length} repo(s)` : null,
|
|
608
|
+
doAutoAdr ? "enable auto-ADR" : null,
|
|
609
|
+
doRemote ? `set brain remote to ${opts.remote}` : null,
|
|
610
|
+
doPlugin ? "install the Commonwealth plugin (global MCP + session hooks)" : null,
|
|
611
|
+
doDaemon ? "start the sync daemon" : null,
|
|
612
|
+
"ensure the per-user scope config exists"
|
|
613
|
+
].filter((step) => step !== null);
|
|
614
|
+
deps.log(`Will: ${plan.join(", ")}.`);
|
|
615
|
+
if (!opts.yes) {
|
|
616
|
+
const ok = await deps.confirm("Proceed with the plan above?");
|
|
617
|
+
if (!ok) {
|
|
618
|
+
deps.log("Aborted. Nothing was changed.");
|
|
619
|
+
return {
|
|
620
|
+
brainDir: opts.brain ?? "",
|
|
621
|
+
mode: "skipped",
|
|
622
|
+
built: false,
|
|
623
|
+
staged: 0,
|
|
624
|
+
scopedFolders: 0,
|
|
625
|
+
mappedFolders: 0,
|
|
626
|
+
seededRepos: 0,
|
|
627
|
+
scopeConfigPath: "",
|
|
628
|
+
scope: "skipped",
|
|
629
|
+
autoAdr: "skipped",
|
|
630
|
+
remote: "skipped",
|
|
631
|
+
plugin: "skipped",
|
|
632
|
+
daemon: "skipped"
|
|
633
|
+
};
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
let built = false;
|
|
637
|
+
if (doBuild) {
|
|
638
|
+
const res = await deps.ensureBuilt();
|
|
639
|
+
built = res.built;
|
|
640
|
+
if (res.skipped) deps.log(`Build: ${res.skipped}`);
|
|
641
|
+
else deps.log(built ? "Build: built workspace." : "Build: dist up to date.");
|
|
642
|
+
}
|
|
643
|
+
const initResult = await deps.init(cwd, {
|
|
644
|
+
brain: opts.brain,
|
|
645
|
+
yes: opts.yes,
|
|
646
|
+
reseed: opts.reseed,
|
|
647
|
+
seed: false
|
|
648
|
+
});
|
|
649
|
+
const brainDir = initResult.brainDir;
|
|
650
|
+
let scopedFolders = 0;
|
|
651
|
+
let mappedFolders = 0;
|
|
652
|
+
let scope = "skipped";
|
|
653
|
+
if (doScope) {
|
|
654
|
+
for (const folder of syncFolders) {
|
|
655
|
+
const scopeRes = await deps.configureScope(folder);
|
|
656
|
+
if (scopeRes.skipped) {
|
|
657
|
+
deps.log(`WARNING: scope step skipped for ${folder}: ${scopeRes.skipped}`);
|
|
658
|
+
} else if (scopeRes.added) {
|
|
659
|
+
scopedFolders += 1;
|
|
660
|
+
deps.log(`Scope: added ${folder}`);
|
|
661
|
+
} else {
|
|
662
|
+
deps.log(`Scope: ${folder} already allowed`);
|
|
663
|
+
}
|
|
664
|
+
const regRes = await deps.registerBrain(folder, brainDir);
|
|
665
|
+
if (regRes.mapped) mappedFolders += 1;
|
|
666
|
+
deps.log(`registered ${folder} -> ${brainDir} (symlink brains/${path5.basename(brainDir)})`);
|
|
667
|
+
if (regRes.skipped) {
|
|
668
|
+
deps.log(`WARNING: brain symlink skipped for ${folder}: ${regRes.skipped}`);
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
scope = scopedFolders > 0 ? `added ${scopedFolders}` : "none added";
|
|
672
|
+
}
|
|
673
|
+
let staged = 0;
|
|
674
|
+
let seededRepos = 0;
|
|
675
|
+
if (seedRepos.length > 0) {
|
|
676
|
+
for (const repo of seedRepos) {
|
|
677
|
+
const seedRes = await deps.seedFrom(brainDir, repo);
|
|
678
|
+
if (seedRes.skipped) {
|
|
679
|
+
deps.log(`WARNING: seed step skipped for ${repo}: ${seedRes.skipped}`);
|
|
680
|
+
} else {
|
|
681
|
+
staged += seedRes.staged;
|
|
682
|
+
seededRepos += 1;
|
|
683
|
+
deps.log(`Seed: staged ${seedRes.staged} from ${repo}`);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
let autoAdr = "skipped";
|
|
688
|
+
if (doAutoAdr) {
|
|
689
|
+
const res = await deps.setAutoAdr(brainDir, true);
|
|
690
|
+
autoAdr = res.skipped ?? (res.set ? "enabled" : "not enabled");
|
|
691
|
+
deps.log(`Auto-ADR: ${autoAdr}`);
|
|
692
|
+
}
|
|
693
|
+
let remote = "skipped";
|
|
694
|
+
if (doRemote) {
|
|
695
|
+
const res = await deps.setRemote(brainDir, opts.remote);
|
|
696
|
+
remote = res.skipped ?? (res.set ? "set" : "not set");
|
|
697
|
+
deps.log(`Remote: ${remote}`);
|
|
698
|
+
}
|
|
699
|
+
let plugin = "skipped";
|
|
700
|
+
if (doPlugin) {
|
|
701
|
+
const res = await deps.installPlugin();
|
|
702
|
+
if (res.skipped) {
|
|
703
|
+
deps.log(`WARNING: plugin step skipped: ${res.skipped}`);
|
|
704
|
+
plugin = res.skipped;
|
|
705
|
+
} else {
|
|
706
|
+
plugin = res.installed ? "installed" : "not installed";
|
|
707
|
+
deps.log(`Plugin: ${plugin}`);
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
let daemon = "skipped";
|
|
711
|
+
if (doDaemon) {
|
|
712
|
+
const res = await deps.startDaemon(brainDir);
|
|
713
|
+
if (res.skipped) {
|
|
714
|
+
deps.log(`WARNING: daemon step skipped: ${res.skipped}`);
|
|
715
|
+
daemon = res.skipped;
|
|
716
|
+
} else if (res.alreadyRunning) {
|
|
717
|
+
daemon = "already running";
|
|
718
|
+
deps.log(`Daemon: ${daemon}`);
|
|
719
|
+
} else {
|
|
720
|
+
daemon = res.started ? "started" : "not started";
|
|
721
|
+
deps.log(`Daemon: ${daemon}`);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
const { path: scopeConfigPath } = await deps.ensureUserConfig();
|
|
725
|
+
deps.log(
|
|
726
|
+
`Done. mode=${initResult.mode} brain=${brainDir} staged=${staged} scopedFolders=${scopedFolders} mappedFolders=${mappedFolders} seededRepos=${seededRepos} scope=${scope} autoAdr=${autoAdr} remote=${remote} plugin=${plugin} daemon=${daemon}. Scope config: ${scopeConfigPath}. Open a Claude session here and ask it something your team knows.`
|
|
727
|
+
);
|
|
728
|
+
return {
|
|
729
|
+
brainDir,
|
|
730
|
+
mode: initResult.mode,
|
|
731
|
+
built,
|
|
732
|
+
staged,
|
|
733
|
+
scopedFolders,
|
|
734
|
+
mappedFolders,
|
|
735
|
+
seededRepos,
|
|
736
|
+
scopeConfigPath,
|
|
737
|
+
scope,
|
|
738
|
+
autoAdr,
|
|
739
|
+
remote,
|
|
740
|
+
plugin,
|
|
741
|
+
daemon
|
|
742
|
+
};
|
|
743
|
+
}
|
|
744
|
+
function defaultWizardDeps() {
|
|
745
|
+
return { scan: (baseDir) => findGitRepos(baseDir) };
|
|
746
|
+
}
|
|
747
|
+
async function runWizard(defaults, prompter, deps = defaultWizardDeps()) {
|
|
748
|
+
const brain = await prompter.text("Brain directory", defaults.brain);
|
|
749
|
+
const scanDefault = path5.dirname(defaults.projectDir);
|
|
750
|
+
const scanDir = await prompter.text("Scan which directory for projects?", scanDefault);
|
|
751
|
+
const repos = await deps.scan(scanDir);
|
|
752
|
+
let syncFolders;
|
|
753
|
+
let seedRepos;
|
|
754
|
+
if (repos.length > 0) {
|
|
755
|
+
const items = repos.map((r) => ({ label: r, value: r }));
|
|
756
|
+
const allTrue = repos.map(() => true);
|
|
757
|
+
syncFolders = await prompter.select("Folders to SYNC into this brain", items, allTrue);
|
|
758
|
+
const seedDefault = repos.map((r) => syncFolders.includes(r));
|
|
759
|
+
seedRepos = await prompter.select("Repos to SEED from now", items, seedDefault);
|
|
760
|
+
} else {
|
|
761
|
+
syncFolders = [defaults.projectDir];
|
|
762
|
+
seedRepos = [defaults.projectDir];
|
|
763
|
+
}
|
|
764
|
+
const plugin = await prompter.confirm(
|
|
765
|
+
"Install the Commonwealth plugin (global MCP + session hooks)?",
|
|
766
|
+
defaults.plugin
|
|
767
|
+
);
|
|
768
|
+
const daemon = await prompter.confirm("Start the sync daemon?", defaults.daemon);
|
|
769
|
+
const autoAdr = await prompter.confirm("Enable auto-ADR?", defaults.autoAdr);
|
|
770
|
+
const remote = await prompter.text("Brain git remote (blank to skip)", "");
|
|
771
|
+
const opts = {
|
|
772
|
+
brain: brain.trim() === "" ? void 0 : brain,
|
|
773
|
+
yes: true,
|
|
774
|
+
seed: seedRepos.length > 0,
|
|
775
|
+
plugin,
|
|
776
|
+
daemon,
|
|
777
|
+
scope: true,
|
|
778
|
+
autoAdr,
|
|
779
|
+
remote: remote.trim() === "" ? void 0 : remote,
|
|
780
|
+
syncFolders,
|
|
781
|
+
seedRepos
|
|
782
|
+
};
|
|
783
|
+
const ok = await prompter.confirm("Proceed?", true);
|
|
784
|
+
if (!ok) return { proceed: false, opts: null };
|
|
785
|
+
return { proceed: true, opts };
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
// src/prompt.ts
|
|
789
|
+
import { createInterface as createInterface2 } from "readline/promises";
|
|
790
|
+
function isInteractive() {
|
|
791
|
+
return Boolean(process.stdin.isTTY && process.stdout.isTTY);
|
|
792
|
+
}
|
|
793
|
+
function parseConfirm(input, def) {
|
|
794
|
+
const v = input.trim().toLowerCase();
|
|
795
|
+
if (v === "") return def;
|
|
796
|
+
if (v === "y" || v === "yes") return true;
|
|
797
|
+
if (v === "n" || v === "no") return false;
|
|
798
|
+
return def;
|
|
799
|
+
}
|
|
800
|
+
function parseText(input, def) {
|
|
801
|
+
const v = input.trim();
|
|
802
|
+
return v === "" ? def : v;
|
|
803
|
+
}
|
|
804
|
+
function parseSelection(input, values, defaultSelected) {
|
|
805
|
+
const v = input.trim().toLowerCase();
|
|
806
|
+
if (v === "") return values.filter((_, i) => defaultSelected[i]);
|
|
807
|
+
if (v === "all") return [...values];
|
|
808
|
+
if (v === "none") return [];
|
|
809
|
+
const picked = /* @__PURE__ */ new Set();
|
|
810
|
+
for (const token of v.split(/[\s,]+/).filter((t) => t.length > 0)) {
|
|
811
|
+
const n = Number.parseInt(token, 10);
|
|
812
|
+
if (!Number.isInteger(n)) continue;
|
|
813
|
+
const idx = n - 1;
|
|
814
|
+
if (idx >= 0 && idx < values.length) picked.add(idx);
|
|
815
|
+
}
|
|
816
|
+
return values.filter((_, i) => picked.has(i));
|
|
817
|
+
}
|
|
818
|
+
function confirmHint(def) {
|
|
819
|
+
return def ? "[Y/n]" : "[y/N]";
|
|
820
|
+
}
|
|
821
|
+
function createReadlinePrompter() {
|
|
822
|
+
const rl = createInterface2({ input: process.stdin, output: process.stdout });
|
|
823
|
+
let closed = false;
|
|
824
|
+
return {
|
|
825
|
+
async text(message, def) {
|
|
826
|
+
const answer = await rl.question(`${message} [${def}]: `);
|
|
827
|
+
return parseText(answer, def);
|
|
828
|
+
},
|
|
829
|
+
async confirm(message, def) {
|
|
830
|
+
const answer = await rl.question(`${message} ${confirmHint(def)}: `);
|
|
831
|
+
return parseConfirm(answer, def);
|
|
832
|
+
},
|
|
833
|
+
async select(message, items, defaultSelected) {
|
|
834
|
+
const lines = items.map(
|
|
835
|
+
(item, i) => ` ${i + 1}. [${defaultSelected[i] ? "x" : " "}] ${item.label}`
|
|
836
|
+
);
|
|
837
|
+
const prompt = `${message}
|
|
838
|
+
${lines.join("\n")}
|
|
839
|
+
Select (all/none/1,3 \u2014 Enter keeps defaults): `;
|
|
840
|
+
const answer = await rl.question(prompt);
|
|
841
|
+
return parseSelection(
|
|
842
|
+
answer,
|
|
843
|
+
items.map((item) => item.value),
|
|
844
|
+
defaultSelected
|
|
845
|
+
);
|
|
846
|
+
},
|
|
847
|
+
close() {
|
|
848
|
+
if (closed) return;
|
|
849
|
+
closed = true;
|
|
850
|
+
rl.close();
|
|
851
|
+
}
|
|
852
|
+
};
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
// src/index.ts
|
|
856
|
+
function printUsage() {
|
|
857
|
+
process.stderr.write(
|
|
858
|
+
[
|
|
859
|
+
"commonwealth \u2014 git-backed markdown team-brain",
|
|
860
|
+
"",
|
|
861
|
+
"Usage:",
|
|
862
|
+
" commonwealth init [flags] onboard: build, create/join brain, plugin, daemon",
|
|
863
|
+
" commonwealth reseed [<repo>...] [--all] mine repo(s) into the mapped brain and capture",
|
|
864
|
+
" commonwealth config <list | get <k> | set <k> <v>> read/set the brain's shared config",
|
|
865
|
+
" commonwealth status review queue + sync-daemon state",
|
|
866
|
+
" commonwealth health freshness/trust rollup for the brain",
|
|
867
|
+
" commonwealth consolidate [--dry-run] supersede near-duplicate canon notes",
|
|
868
|
+
" commonwealth sync <start | stop | once> control/run the sync daemon",
|
|
869
|
+
" commonwealth pending list notes awaiting review",
|
|
870
|
+
" commonwealth promote <id...> | --all approve staged notes into canon",
|
|
871
|
+
" commonwealth reject <id...> discard staged notes",
|
|
872
|
+
" commonwealth scope <show | allow <p> | deny <p> | check> per-user capture scope",
|
|
873
|
+
" commonwealth recall <query> search the brain",
|
|
874
|
+
"",
|
|
875
|
+
"All commands resolve the brain from the registry for the current directory \u2014 no --dir needed.",
|
|
876
|
+
"",
|
|
877
|
+
"init flags: [--brain <dir>] [--yes] [--reseed] [--auto-adr] [--remote <url>]",
|
|
878
|
+
" [--sync <dir,dir,...>] [--seed-repo <dir,dir,...>]",
|
|
879
|
+
" [--no-scope] [--no-seed] [--no-plugin] [--no-daemon] [--no-build]",
|
|
880
|
+
"",
|
|
881
|
+
"`init` is a single idempotent command: it builds the workspace (if needed), creates or",
|
|
882
|
+
"joins the brain, syncs one or more folders into it (allowlist + a global-registry mapping",
|
|
883
|
+
"with a ~/.commonwealth/brains/<name> symlink), seeds it from one or more repos, installs",
|
|
884
|
+
"the Commonwealth plugin (global MCP + session hooks), and starts the sync daemon. Run in a",
|
|
885
|
+
"terminal without --yes for an interactive wizard that scans for and lets you multi-select",
|
|
886
|
+
"folders/repos; with --yes (or non-interactively) it uses defaults + flags and never prompts.",
|
|
887
|
+
"",
|
|
888
|
+
"Options:",
|
|
889
|
+
" --brain <dir> Create/use the brain at <dir> (default: ~/.commonwealth/brains/<project>)",
|
|
890
|
+
" --yes Run non-interactively; skip the wizard and all prompts",
|
|
891
|
+
" --reseed Re-seed even if this project already resolves to a brain",
|
|
892
|
+
" --auto-adr Enable auto-ADR capture for the brain",
|
|
893
|
+
" --remote <url> Add <url> as the brain's git origin remote",
|
|
894
|
+
" --sync <dir,dir,...> Folders to sync into the brain (default: this repo)",
|
|
895
|
+
" --seed-repo <dir,...> Repos to seed from now (default: the --sync folders)",
|
|
896
|
+
" --no-scope Skip adding folders to the capture allowlist",
|
|
897
|
+
" --no-seed Create the brain but skip gathering/staging seed candidates",
|
|
898
|
+
" --no-plugin Skip installing the Commonwealth plugin (alias: --no-mcp)",
|
|
899
|
+
" --no-daemon Skip starting the sync daemon",
|
|
900
|
+
" --no-build Skip the workspace build even if dist artifacts are missing",
|
|
901
|
+
""
|
|
902
|
+
].join("\n")
|
|
903
|
+
);
|
|
904
|
+
}
|
|
905
|
+
async function run(argv) {
|
|
906
|
+
const [command, ...rest] = argv;
|
|
907
|
+
if (command === void 0 || command === "--help" || command === "-h") {
|
|
908
|
+
printUsage();
|
|
909
|
+
return command === void 0 ? 2 : 0;
|
|
910
|
+
}
|
|
911
|
+
switch (command) {
|
|
912
|
+
case "init":
|
|
913
|
+
return cmdInit(rest);
|
|
914
|
+
case "reseed":
|
|
915
|
+
return cmdReseed(rest);
|
|
916
|
+
case "config":
|
|
917
|
+
return cmdConfig(rest);
|
|
918
|
+
case "status": {
|
|
919
|
+
const queue = await delegateCurate(["list"]);
|
|
920
|
+
const daemon = await delegateSync(["status"]);
|
|
921
|
+
return queue || daemon;
|
|
922
|
+
}
|
|
923
|
+
case "sync": {
|
|
924
|
+
const sub = rest[0] === "once" ? "sync" : rest[0] ?? "status";
|
|
925
|
+
return delegateSync([sub, ...rest.slice(1)]);
|
|
926
|
+
}
|
|
927
|
+
case "health":
|
|
928
|
+
return delegateCurate(["health"]);
|
|
929
|
+
case "consolidate":
|
|
930
|
+
return delegateCurate(["consolidate", ...rest]);
|
|
931
|
+
case "pending":
|
|
932
|
+
return delegateCurate(["list"]);
|
|
933
|
+
case "promote":
|
|
934
|
+
return delegateCurate(rest.includes("--all") ? ["approve-all"] : ["approve", ...rest]);
|
|
935
|
+
case "reject":
|
|
936
|
+
return delegateCurate(["reject", ...rest]);
|
|
937
|
+
case "scope":
|
|
938
|
+
return delegateCurate(["scope", ...rest]);
|
|
939
|
+
case "recall":
|
|
940
|
+
return delegateCurate(["context", "--cwd", process.cwd(), "--query", rest.join(" ")]);
|
|
941
|
+
default:
|
|
942
|
+
process.stderr.write(`Unknown command: ${command}
|
|
943
|
+
`);
|
|
944
|
+
printUsage();
|
|
945
|
+
return 2;
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
async function cmdInit(rest) {
|
|
949
|
+
if (rest.includes("--help") || rest.includes("-h")) {
|
|
950
|
+
printUsage();
|
|
951
|
+
return 0;
|
|
952
|
+
}
|
|
953
|
+
const negations = [
|
|
954
|
+
"--no-scope",
|
|
955
|
+
"--no-seed",
|
|
956
|
+
"--no-plugin",
|
|
957
|
+
"--no-mcp",
|
|
958
|
+
"--no-daemon",
|
|
959
|
+
"--no-build"
|
|
960
|
+
];
|
|
961
|
+
const scope = !rest.includes("--no-scope");
|
|
962
|
+
const seed = !rest.includes("--no-seed");
|
|
963
|
+
const plugin = !rest.includes("--no-plugin") && !rest.includes("--no-mcp");
|
|
964
|
+
const daemon = !rest.includes("--no-daemon");
|
|
965
|
+
const build = !rest.includes("--no-build");
|
|
966
|
+
const positional = rest.filter((a) => !negations.includes(a));
|
|
967
|
+
let values;
|
|
968
|
+
try {
|
|
969
|
+
({ values } = parseArgs({
|
|
970
|
+
args: positional,
|
|
971
|
+
options: {
|
|
972
|
+
brain: { type: "string" },
|
|
973
|
+
yes: { type: "boolean", default: false },
|
|
974
|
+
reseed: { type: "boolean", default: false },
|
|
975
|
+
"auto-adr": { type: "boolean", default: false },
|
|
976
|
+
remote: { type: "string" },
|
|
977
|
+
sync: { type: "string" },
|
|
978
|
+
"seed-repo": { type: "string" }
|
|
979
|
+
},
|
|
980
|
+
allowPositionals: false
|
|
981
|
+
}));
|
|
982
|
+
} catch (err) {
|
|
983
|
+
process.stderr.write(`${err.message}
|
|
984
|
+
`);
|
|
985
|
+
printUsage();
|
|
986
|
+
return 2;
|
|
987
|
+
}
|
|
988
|
+
const cwd = process.cwd();
|
|
989
|
+
const projectDir = path6.resolve(cwd);
|
|
990
|
+
const interactive = isInteractive();
|
|
991
|
+
if (!interactive && !values.yes) {
|
|
992
|
+
process.stderr.write("Non-interactive: re-run in a terminal or pass --yes.\n");
|
|
993
|
+
return 0;
|
|
994
|
+
}
|
|
995
|
+
const claudePresent = hasExecutable2("claude");
|
|
996
|
+
const deps = defaultOnboardDeps({ curateEntry: process.env.COMMONWEALTH_CURATE_BIN });
|
|
997
|
+
const splitDirs = (raw) => {
|
|
998
|
+
if (raw === void 0) return void 0;
|
|
999
|
+
const dirs = raw.split(",").map((d) => d.trim()).filter((d) => d.length > 0);
|
|
1000
|
+
return dirs.length > 0 ? dirs : void 0;
|
|
1001
|
+
};
|
|
1002
|
+
let opts;
|
|
1003
|
+
let prompter = null;
|
|
1004
|
+
try {
|
|
1005
|
+
if (interactive && !values.yes) {
|
|
1006
|
+
const defaults = {
|
|
1007
|
+
brain: defaultBrainDir(projectDir),
|
|
1008
|
+
projectDir,
|
|
1009
|
+
scope: true,
|
|
1010
|
+
seed: true,
|
|
1011
|
+
plugin: claudePresent,
|
|
1012
|
+
daemon: true,
|
|
1013
|
+
autoAdr: false
|
|
1014
|
+
};
|
|
1015
|
+
prompter = createReadlinePrompter();
|
|
1016
|
+
const outcome = await runWizard(defaults, prompter);
|
|
1017
|
+
if (!outcome.proceed) {
|
|
1018
|
+
process.stderr.write("Aborted.\n");
|
|
1019
|
+
return 0;
|
|
1020
|
+
}
|
|
1021
|
+
opts = outcome.opts;
|
|
1022
|
+
} else {
|
|
1023
|
+
const syncFolders = splitDirs(values.sync) ?? [projectDir];
|
|
1024
|
+
const seedRepos = splitDirs(values["seed-repo"]) ?? (seed ? syncFolders : []);
|
|
1025
|
+
opts = {
|
|
1026
|
+
brain: values.brain,
|
|
1027
|
+
yes: true,
|
|
1028
|
+
reseed: values.reseed,
|
|
1029
|
+
seed,
|
|
1030
|
+
plugin,
|
|
1031
|
+
daemon,
|
|
1032
|
+
build,
|
|
1033
|
+
scope,
|
|
1034
|
+
autoAdr: values["auto-adr"],
|
|
1035
|
+
remote: values.remote,
|
|
1036
|
+
syncFolders,
|
|
1037
|
+
seedRepos
|
|
1038
|
+
};
|
|
1039
|
+
}
|
|
1040
|
+
const result = await runOnboard(cwd, opts, deps);
|
|
1041
|
+
process.stderr.write(
|
|
1042
|
+
`init: mode=${result.mode} brain=${result.brainDir} built=${result.built} staged=${result.staged} scopedFolders=${result.scopedFolders} mappedFolders=${result.mappedFolders} seededRepos=${result.seededRepos} scope=${result.scope} autoAdr=${result.autoAdr} remote=${result.remote} plugin=${result.plugin} daemon=${result.daemon} scopeConfig=${result.scopeConfigPath}
|
|
1043
|
+
`
|
|
1044
|
+
);
|
|
1045
|
+
return 0;
|
|
1046
|
+
} finally {
|
|
1047
|
+
prompter?.close();
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
function hasExecutable2(name) {
|
|
1051
|
+
const probe = process.platform === "win32" ? "where" : "which";
|
|
1052
|
+
const res = spawnSync2(probe, [name], { stdio: "ignore" });
|
|
1053
|
+
return res.status === 0;
|
|
1054
|
+
}
|
|
1055
|
+
var isEntrypoint = process.argv[1] !== void 0 && import.meta.url === new URL(`file://${process.argv[1]}`).href;
|
|
1056
|
+
if (isEntrypoint) {
|
|
1057
|
+
run(process.argv.slice(2)).then((code) => {
|
|
1058
|
+
process.exitCode = code;
|
|
1059
|
+
}).catch((err) => {
|
|
1060
|
+
process.stderr.write(`${err.stack ?? String(err)}
|
|
1061
|
+
`);
|
|
1062
|
+
process.exitCode = 1;
|
|
1063
|
+
});
|
|
1064
|
+
}
|
|
1065
|
+
export {
|
|
1066
|
+
createReadlinePrompter,
|
|
1067
|
+
defaultBrainDir,
|
|
1068
|
+
defaultInitDeps,
|
|
1069
|
+
defaultOnboardDeps,
|
|
1070
|
+
findGitRepos,
|
|
1071
|
+
findRepoRoot,
|
|
1072
|
+
isInteractive,
|
|
1073
|
+
parseConfirm,
|
|
1074
|
+
parseSelection,
|
|
1075
|
+
parseText,
|
|
1076
|
+
run,
|
|
1077
|
+
runInit,
|
|
1078
|
+
runOnboard,
|
|
1079
|
+
runWizard
|
|
1080
|
+
};
|
|
1081
|
+
//# sourceMappingURL=index.js.map
|