@pasajero_0/agent-stack 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/dist/index.js ADDED
@@ -0,0 +1,683 @@
1
+ // src/cli.ts
2
+ import { Command } from "commander";
3
+
4
+ // src/commands/init.ts
5
+ import { checkbox, input, confirm } from "@inquirer/prompts";
6
+ import chalk2 from "chalk";
7
+
8
+ // src/detect/project.ts
9
+ import { existsSync } from "node:fs";
10
+ import { readFile } from "node:fs/promises";
11
+ import { join } from "node:path";
12
+ async function detectProject(dir) {
13
+ const hasPackageJson = existsSync(join(dir, "package.json"));
14
+ const hasSrcDir = existsSync(join(dir, "src"));
15
+ const hasGitRepo = existsSync(join(dir, ".git"));
16
+ const isExistingProject = hasPackageJson || hasSrcDir || hasGitRepo;
17
+ let projectName;
18
+ if (hasPackageJson) {
19
+ try {
20
+ const raw = await readFile(join(dir, "package.json"), "utf-8");
21
+ const pkg = JSON.parse(raw);
22
+ projectName = pkg.name;
23
+ } catch {
24
+ }
25
+ }
26
+ return {
27
+ isExistingProject,
28
+ hasPackageJson,
29
+ hasSrcDir,
30
+ hasGitRepo,
31
+ projectName,
32
+ scenario: isExistingProject ? "existing" : "empty"
33
+ };
34
+ }
35
+
36
+ // src/claude/provider.ts
37
+ import { homedir } from "node:os";
38
+ import { existsSync as existsSync2 } from "node:fs";
39
+ import { join as join2 } from "node:path";
40
+
41
+ // src/utils/shell.ts
42
+ import { execa } from "execa";
43
+ async function commandExists(cmd) {
44
+ try {
45
+ await execa("which", [cmd]);
46
+ return true;
47
+ } catch {
48
+ return false;
49
+ }
50
+ }
51
+ async function run(cmd, args, options) {
52
+ const result = await execa(cmd, args, {
53
+ cwd: options?.cwd,
54
+ env: options?.env,
55
+ reject: false
56
+ });
57
+ return {
58
+ stdout: result.stdout,
59
+ stderr: result.stderr,
60
+ exitCode: result.exitCode ?? 1
61
+ };
62
+ }
63
+ async function runSilent(cmd, args) {
64
+ const result = await execa(cmd, args, { reject: false });
65
+ return { stdout: result.stdout, exitCode: result.exitCode ?? 1 };
66
+ }
67
+
68
+ // src/utils/logger.ts
69
+ import chalk from "chalk";
70
+ import ora from "ora";
71
+ var log = {
72
+ info: (msg) => console.log(chalk.blue("\u2139"), msg),
73
+ success: (msg) => console.log(chalk.green("\u2714"), msg),
74
+ warn: (msg) => console.log(chalk.yellow("\u26A0"), msg),
75
+ error: (msg) => console.error(chalk.red("\u2716"), msg),
76
+ step: (msg) => console.log(chalk.cyan("\u2192"), msg),
77
+ dim: (msg) => console.log(chalk.dim(msg))
78
+ };
79
+ function spinner(text) {
80
+ return ora({ text, color: "cyan" });
81
+ }
82
+ function banner() {
83
+ console.log();
84
+ console.log(chalk.bold.cyan(" agent-stack"));
85
+ console.log(chalk.dim(" AI coding environment configurator"));
86
+ console.log();
87
+ }
88
+
89
+ // src/claude/provider.ts
90
+ var PROVIDER_ID = "claude-code";
91
+ var PROVIDER_NAME = "Claude Code";
92
+ async function detect() {
93
+ const home = homedir();
94
+ const configPaths = [join2(home, ".claude"), join2(home, ".config", "claude")];
95
+ const installed = await commandExists("claude");
96
+ let version;
97
+ if (installed) {
98
+ const result = await runSilent("claude", ["--version"]);
99
+ if (result.exitCode === 0) version = result.stdout.trim();
100
+ }
101
+ return {
102
+ name: PROVIDER_ID,
103
+ displayName: PROVIDER_NAME,
104
+ installed,
105
+ version,
106
+ configPaths: configPaths.filter((p) => existsSync2(p))
107
+ };
108
+ }
109
+ async function install() {
110
+ log.step("Installing Claude Code via npm...");
111
+ const result = await run("npm", ["install", "-g", "@anthropic-ai/claude-code"]);
112
+ if (result.exitCode !== 0) {
113
+ throw new Error(`Failed to install Claude Code: ${result.stderr}`);
114
+ }
115
+ log.success("Claude Code installed successfully");
116
+ }
117
+ async function configureMcp(servers, envValues) {
118
+ for (const server of servers) {
119
+ const args = ["mcp", "add", server.name, "--transport", server.transport];
120
+ if (server.command) args.push("--", server.command);
121
+ if (server.args) args.push(...server.args);
122
+ const env = {};
123
+ if (server.env) {
124
+ for (const key of Object.keys(server.env)) {
125
+ if (envValues[key]) env[key] = envValues[key];
126
+ }
127
+ }
128
+ const envArgs = [];
129
+ for (const [key, value] of Object.entries(env)) {
130
+ envArgs.push("-e", `${key}=${value}`);
131
+ }
132
+ const result = await run("claude", [...args, ...envArgs]);
133
+ if (result.exitCode !== 0) {
134
+ log.warn(`Failed to add MCP server "${server.name}": ${result.stderr}`);
135
+ } else {
136
+ log.success(`MCP server "${server.displayName}" configured`);
137
+ }
138
+ }
139
+ }
140
+ async function listMcp() {
141
+ const result = await runSilent("claude", ["mcp", "list"]);
142
+ if (result.exitCode !== 0) return [];
143
+ const lines = result.stdout.trim().split("\n").filter(Boolean);
144
+ return lines.map((line) => {
145
+ const parts = line.split(/\s+/);
146
+ return { name: parts[0] ?? line, command: parts[1] ?? "", args: parts.slice(2) };
147
+ });
148
+ }
149
+
150
+ // src/mcp/catalog.ts
151
+ import { readFileSync } from "node:fs";
152
+ import { join as join3, dirname } from "node:path";
153
+ import { fileURLToPath } from "node:url";
154
+ function getPackageRoot() {
155
+ const thisDir = dirname(fileURLToPath(import.meta.url));
156
+ const candidates = [
157
+ join3(thisDir, "..", "mcp", "catalog.json"),
158
+ join3(thisDir, "..", "..", "mcp", "catalog.json")
159
+ ];
160
+ for (const candidate of candidates) {
161
+ try {
162
+ return readFileSync(candidate, "utf-8");
163
+ } catch {
164
+ continue;
165
+ }
166
+ }
167
+ throw new Error("Could not find mcp/catalog.json");
168
+ }
169
+ var catalogData = JSON.parse(getPackageRoot());
170
+ var MCP_CATALOG = catalogData;
171
+
172
+ // src/mcp/installer.ts
173
+ async function installMcpServers(servers, envValues) {
174
+ const s = spinner(`Configuring MCP servers for ${PROVIDER_NAME}...`);
175
+ s.start();
176
+ try {
177
+ await configureMcp(servers, envValues);
178
+ s.succeed(`MCP servers configured for ${PROVIDER_NAME}`);
179
+ } catch (err) {
180
+ s.fail(`Failed to configure MCP for ${PROVIDER_NAME}`);
181
+ throw err;
182
+ }
183
+ }
184
+
185
+ // src/claude/harness.ts
186
+ import { readFileSync as readFileSync2, readdirSync, statSync, existsSync as existsSync4 } from "node:fs";
187
+ import { writeFile, mkdir, chmod } from "node:fs/promises";
188
+ import { join as join5, dirname as dirname2, relative, basename } from "node:path";
189
+ import { fileURLToPath as fileURLToPath2 } from "node:url";
190
+
191
+ // src/detect/harness-params.ts
192
+ import { existsSync as existsSync3 } from "node:fs";
193
+ import { readFile as readFile2 } from "node:fs/promises";
194
+ import { join as join4 } from "node:path";
195
+ var PM_BY_LOCKFILE = [
196
+ { lock: "pnpm-lock.yaml", pm: "pnpm" },
197
+ { lock: "yarn.lock", pm: "yarn" },
198
+ { lock: "bun.lockb", pm: "bun" },
199
+ { lock: "package-lock.json", pm: "npm" }
200
+ ];
201
+ var ALL_PMS = ["npm", "yarn", "pnpm", "bun"];
202
+ function detectPackageManager(dir) {
203
+ for (const { lock, pm } of PM_BY_LOCKFILE) {
204
+ if (existsSync3(join4(dir, lock))) return { packageManager: pm, lockfile: lock };
205
+ }
206
+ return { packageManager: "npm", lockfile: "package-lock.json" };
207
+ }
208
+ function forbiddenPmsFor(pm) {
209
+ return ALL_PMS.filter((p) => p !== pm).join("|");
210
+ }
211
+ function forgeFromRemote(remoteUrl) {
212
+ return /gitlab/i.test(remoteUrl) ? "glab" : "gh";
213
+ }
214
+ function generatedGlobsFor(dir) {
215
+ return existsSync3(join4(dir, "tsconfig.json")) ? "*/dist/*|dist/*|*.d.ts|*.map" : "*/dist/*|dist/*|*/build/*";
216
+ }
217
+ async function detectGit(dir) {
218
+ let mainBranch = "main";
219
+ let forgeCli = "gh";
220
+ const head = await run("git", ["symbolic-ref", "refs/remotes/origin/HEAD"], { cwd: dir });
221
+ const m = head.exitCode === 0 ? head.stdout.trim().match(/refs\/remotes\/origin\/(.+)$/) : null;
222
+ if (m) {
223
+ mainBranch = m[1];
224
+ } else {
225
+ const cur = await run("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: dir });
226
+ const branch = cur.stdout.trim();
227
+ if (cur.exitCode === 0 && branch && branch !== "HEAD") mainBranch = branch;
228
+ }
229
+ const remote = await run("git", ["remote", "get-url", "origin"], { cwd: dir });
230
+ if (remote.exitCode === 0 && remote.stdout) forgeCli = forgeFromRemote(remote.stdout);
231
+ return { mainBranch, forgeCli };
232
+ }
233
+ async function detectCommands(dir, pm) {
234
+ const cmds = [];
235
+ try {
236
+ const pkg = JSON.parse(await readFile2(join4(dir, "package.json"), "utf-8"));
237
+ const scripts = pkg.scripts ?? {};
238
+ for (const name of ["typecheck", "test", "build"]) {
239
+ if (scripts[name]) cmds.push(`${pm} ${name}`);
240
+ }
241
+ } catch {
242
+ }
243
+ return cmds;
244
+ }
245
+ async function detectHarnessParams(dir) {
246
+ const { packageManager, lockfile } = detectPackageManager(dir);
247
+ const { mainBranch, forgeCli } = await detectGit(dir);
248
+ const commands = await detectCommands(dir, packageManager);
249
+ return {
250
+ packageManager,
251
+ forbiddenPms: forbiddenPmsFor(packageManager),
252
+ lockfile,
253
+ generatedGlobs: generatedGlobsFor(dir),
254
+ docsDir: "docs/",
255
+ mainBranch,
256
+ forgeCli,
257
+ commands
258
+ };
259
+ }
260
+
261
+ // src/claude/harness.ts
262
+ var HOOKS_BLOCK = {
263
+ PreToolUse: [
264
+ {
265
+ matcher: "Bash",
266
+ hooks: [{ type: "command", command: "$CLAUDE_PROJECT_DIR/.claude/hooks/guard-bash.sh" }]
267
+ },
268
+ {
269
+ matcher: "Edit|Write|MultiEdit",
270
+ hooks: [{ type: "command", command: "$CLAUDE_PROJECT_DIR/.claude/hooks/guard-file.sh" }]
271
+ }
272
+ ],
273
+ SessionStart: [
274
+ {
275
+ hooks: [
276
+ { type: "command", command: "$CLAUDE_PROJECT_DIR/.claude/hooks/session-start-context.sh" }
277
+ ]
278
+ }
279
+ ],
280
+ Stop: [
281
+ {
282
+ hooks: [{ type: "command", command: "$CLAUDE_PROJECT_DIR/.claude/hooks/stop-flag-reflect.sh" }]
283
+ }
284
+ ],
285
+ PostToolUse: [
286
+ {
287
+ matcher: "Edit|Write|MultiEdit",
288
+ hooks: [
289
+ { type: "command", command: "$CLAUDE_PROJECT_DIR/.claude/hooks/post-edit-lint.sh" },
290
+ { type: "command", command: "$CLAUDE_PROJECT_DIR/.claude/hooks/post-edit-verify.sh" }
291
+ ]
292
+ }
293
+ ]
294
+ };
295
+ function templatesRoot() {
296
+ const thisDir = dirname2(fileURLToPath2(import.meta.url));
297
+ const candidates = [
298
+ join5(thisDir, "..", "templates", "claude"),
299
+ // dist/index.js -> ../templates/claude
300
+ join5(thisDir, "..", "..", "templates", "claude")
301
+ // src/claude/ -> ../../templates/claude
302
+ ];
303
+ for (const c of candidates) {
304
+ if (existsSync4(c)) return c;
305
+ }
306
+ throw new Error("Could not find templates/claude");
307
+ }
308
+ function toolVersion() {
309
+ const thisDir = dirname2(fileURLToPath2(import.meta.url));
310
+ for (const c of [join5(thisDir, "..", "package.json"), join5(thisDir, "..", "..", "package.json")]) {
311
+ try {
312
+ return JSON.parse(readFileSync2(c, "utf-8")).version ?? "0.0.0";
313
+ } catch {
314
+ continue;
315
+ }
316
+ }
317
+ return "0.0.0";
318
+ }
319
+ function substitute(text, p) {
320
+ return text.replaceAll("{{PM}}", p.packageManager).replaceAll("{{FORBIDDEN_PM}}", p.forbiddenPms).replaceAll("{{GEN_GLOBS}}", p.generatedGlobs).replaceAll("{{LOCKFILE}}", p.lockfile).replaceAll("{{DOCS_DIR}}", p.docsDir).replaceAll("{{MAIN_BRANCH}}", p.mainBranch);
321
+ }
322
+ function walk(dir) {
323
+ const out = [];
324
+ for (const name of readdirSync(dir)) {
325
+ const full = join5(dir, name);
326
+ if (statSync(full).isDirectory()) out.push(...walk(full));
327
+ else out.push(full);
328
+ }
329
+ return out;
330
+ }
331
+ function buildSettings(p) {
332
+ const allow = [
333
+ ...p.commands.map((c) => `Bash(${c}:*)`),
334
+ "Bash(git status:*)",
335
+ "Bash(git diff:*)",
336
+ "Bash(git log:*)",
337
+ "Bash(git show:*)",
338
+ `Bash(${p.forgeCli} api:*)`,
339
+ `Bash(${p.forgeCli} pr view:*)`,
340
+ `Bash(${p.forgeCli} pr diff:*)`,
341
+ "WebSearch"
342
+ ];
343
+ const settings = {
344
+ $schema: "https://json.schemastore.org/claude-code-settings.json",
345
+ permissions: { allow },
346
+ hooks: HOOKS_BLOCK
347
+ };
348
+ return JSON.stringify(settings, null, 2) + "\n";
349
+ }
350
+ function buildSettingsLocal(p) {
351
+ const settings = {
352
+ permissions: {
353
+ allow: ["WebSearch", `Bash(${p.forgeCli} api:*)`],
354
+ deny: [
355
+ "Read(./**/dist/**)",
356
+ "Read(./**/build/**)",
357
+ "Read(./**/coverage/**)",
358
+ "Read(./**/*.d.ts)",
359
+ "Read(./**/*.map)",
360
+ "Read(./**/*.gen.*)",
361
+ `Read(./**/${p.lockfile})`,
362
+ "Read(./**/node_modules/**)"
363
+ ]
364
+ }
365
+ };
366
+ return JSON.stringify(settings, null, 2) + "\n";
367
+ }
368
+ function buildClaudeMd(p, root) {
369
+ const normative = readFileSync2(join5(root, "_normative.md"), "utf-8").replace(
370
+ /^<!--[\s\S]*?-->\s*/,
371
+ ""
372
+ );
373
+ const cmds = p.commands.length ? `- Commands: ${p.commands.map((c) => `\`${c}\``).join(", ")}.
374
+ ` : "";
375
+ const body = `# Project guide
376
+
377
+ > Generated by agent-stack. Edit freely to describe your project.
378
+
379
+ ## Tooling
380
+
381
+ - Package manager: **${p.packageManager}** only \u2014 other package managers are blocked by the guard-bash hook.
382
+ - Default branch: \`${p.mainBranch}\`. \`git push\` is manual-only.
383
+ - Never hand-edit generated files: ${p.generatedGlobs.split("|").join(", ")}, ${p.lockfile}.
384
+ ${cmds}
385
+ ## Subagent delegation
386
+
387
+ - **task-analyzer** at the start of a non-trivial task.
388
+ - **code-reviewer** after writing or modifying code.
389
+ - **test-writer** for unit tests.
390
+ - **pattern-scout** to (re)generate \`.claude/rules/\` after structural changes.
391
+
392
+ `;
393
+ return body + normative;
394
+ }
395
+ function buildCodemap() {
396
+ return `# CODEMAP
397
+
398
+ > Generated by agent-stack \u2014 replace with a real map of where things live (apps, packages, entry points).
399
+
400
+ | Path | What |
401
+ | --- | --- |
402
+ | \`src/\` | source |
403
+ | \`tests/\` | tests |
404
+
405
+ ## Harness (not committed)
406
+
407
+ \`.claude/\`, \`CLAUDE.md\`, \`CODEMAP.md\` are git-excluded (\`.git/info/exclude\`) \u2014 personal harness, not project history.
408
+ `;
409
+ }
410
+ function buildHarnessFiles(params) {
411
+ const root = templatesRoot();
412
+ const files = [];
413
+ for (const abs of walk(root)) {
414
+ if (basename(abs).startsWith("_")) continue;
415
+ const rel = relative(root, abs);
416
+ const content = substitute(readFileSync2(abs, "utf-8"), params);
417
+ files.push({ path: join5(".claude", rel), content, action: "create" });
418
+ }
419
+ files.push({ path: ".claude/settings.json", content: buildSettings(params), action: "create" });
420
+ files.push({
421
+ path: ".claude/settings.local.json",
422
+ content: buildSettingsLocal(params),
423
+ action: "create"
424
+ });
425
+ files.push({ path: "CLAUDE.md", content: buildClaudeMd(params, root), action: "create" });
426
+ files.push({ path: "CODEMAP.md", content: buildCodemap(), action: "create" });
427
+ files.push({
428
+ path: ".claude/.harness.json",
429
+ content: JSON.stringify({ templateVersion: toolVersion(), params }, null, 2) + "\n",
430
+ action: "create"
431
+ });
432
+ return files;
433
+ }
434
+ async function writeHarnessFiles(projectDir, files) {
435
+ for (const f of files) {
436
+ const full = join5(projectDir, f.path);
437
+ await mkdir(dirname2(full), { recursive: true });
438
+ await writeFile(full, f.content, "utf-8");
439
+ if (f.path.startsWith(".claude/hooks/") && f.path.endsWith(".sh")) {
440
+ await chmod(full, 493);
441
+ }
442
+ log.success(f.path);
443
+ }
444
+ }
445
+ async function emitHarness(projectDir) {
446
+ const params = await detectHarnessParams(projectDir);
447
+ const files = buildHarnessFiles(params);
448
+ await writeHarnessFiles(projectDir, files);
449
+ return files;
450
+ }
451
+
452
+ // src/commands/init.ts
453
+ async function initCommand() {
454
+ banner();
455
+ log.step("Detecting environment...");
456
+ const project = await detectProject(process.cwd());
457
+ if (project.scenario === "existing") {
458
+ const name = project.projectName ? chalk2.cyan(project.projectName) : chalk2.dim("unnamed");
459
+ log.info(`Existing project: ${name}`);
460
+ } else {
461
+ log.info("Empty directory \u2014 fresh setup");
462
+ }
463
+ console.log();
464
+ const info = await detect();
465
+ const status = info.installed ? chalk2.green(" \u2714 " + info.displayName) : chalk2.dim(" \u2716 " + info.displayName);
466
+ const version = info.version ? chalk2.dim(` (${info.version})`) : "";
467
+ console.log(`${status}${version}`);
468
+ console.log();
469
+ if (!info.installed) {
470
+ log.warn("Claude Code is not installed.");
471
+ const doInstall = await confirm({ message: "Install Claude Code now?", default: true });
472
+ if (doInstall) {
473
+ await install();
474
+ } else {
475
+ log.error("Claude Code is required. Exiting.");
476
+ process.exit(1);
477
+ }
478
+ }
479
+ console.log();
480
+ log.step("Generating the Claude harness...");
481
+ await emitHarness(process.cwd());
482
+ const { mcpServers, envValues } = await collectMcpConfig();
483
+ if (mcpServers.length > 0) {
484
+ console.log();
485
+ await installMcpServers(mcpServers, envValues);
486
+ }
487
+ console.log();
488
+ console.log(chalk2.bold.green(" Setup complete!"));
489
+ console.log();
490
+ log.dim(" Next steps:");
491
+ log.dim(" \u2022 Exclude the harness from git: add .claude/, CLAUDE.md, CODEMAP.md to .git/info/exclude");
492
+ log.dim(" \u2022 Generate rules: open `claude` and run the pattern-scout subagent (.claude/rules/ is empty by design)");
493
+ log.dim(" \u2022 Smoke-test the guards: a `git push` or foreign package-manager command should block");
494
+ console.log();
495
+ }
496
+ async function collectMcpConfig() {
497
+ const selectedNames = await checkbox({
498
+ message: "Select MCP servers to install:",
499
+ choices: MCP_CATALOG.map((s) => ({
500
+ name: `${s.displayName} \u2014 ${s.description}`,
501
+ value: s.name,
502
+ checked: !s.optional
503
+ }))
504
+ });
505
+ const mcpServers = MCP_CATALOG.filter((s) => selectedNames.includes(s.name));
506
+ const envValues = {};
507
+ for (const server of mcpServers) {
508
+ if (server.envPrompts) {
509
+ for (const [key, prompt] of Object.entries(server.envPrompts)) {
510
+ const value = await input({ message: prompt + chalk2.dim(" (enter to skip)") });
511
+ envValues[key] = value || "<your-token-here>";
512
+ }
513
+ }
514
+ }
515
+ return { mcpServers, envValues };
516
+ }
517
+
518
+ // src/commands/detect.ts
519
+ import chalk3 from "chalk";
520
+ async function detectCommand() {
521
+ banner();
522
+ const project = await detectProject(process.cwd());
523
+ if (project.scenario === "existing") {
524
+ const name = project.projectName ? chalk3.cyan(project.projectName) : chalk3.dim("unnamed");
525
+ log.info(`Existing project detected: ${name}`);
526
+ } else {
527
+ log.info("Empty directory \u2014 no existing project detected");
528
+ }
529
+ console.log();
530
+ log.step("Detecting Claude Code...");
531
+ console.log();
532
+ const info = await detect();
533
+ const status = info.installed ? chalk3.green("\u2714 installed") : chalk3.dim("\u2716 not found");
534
+ const version = info.version ? chalk3.dim(` (${info.version})`) : "";
535
+ console.log(` ${info.displayName.padEnd(20)} ${status}${version}`);
536
+ console.log();
537
+ if (info.installed) {
538
+ log.success("Claude Code detected.");
539
+ } else {
540
+ log.warn("Claude Code not detected.");
541
+ log.info("Run 'agent-stack init' to install and configure it.");
542
+ }
543
+ }
544
+
545
+ // src/commands/mcp.ts
546
+ import chalk4 from "chalk";
547
+ import { checkbox as checkbox2, input as input2 } from "@inquirer/prompts";
548
+ async function mcpCommand(action) {
549
+ banner();
550
+ const info = await detect();
551
+ if (!info.installed) {
552
+ log.error("Claude Code not detected. Run 'agent-stack init' first.");
553
+ process.exit(1);
554
+ }
555
+ if (action === "list") {
556
+ console.log();
557
+ log.step(`${info.displayName} MCP servers:`);
558
+ const servers2 = await listMcp();
559
+ if (servers2.length === 0) {
560
+ log.dim(" No MCP servers configured");
561
+ } else {
562
+ for (const s of servers2) {
563
+ console.log(` ${chalk4.cyan(s.name.padEnd(25))} ${chalk4.dim(s.command)} ${s.args.join(" ")}`);
564
+ }
565
+ }
566
+ console.log();
567
+ return;
568
+ }
569
+ const selectedServers = await checkbox2({
570
+ message: "Select MCP servers to install:",
571
+ choices: MCP_CATALOG.map((s) => ({
572
+ name: `${s.displayName} \u2014 ${s.description}`,
573
+ value: s.name,
574
+ checked: !s.optional
575
+ }))
576
+ });
577
+ const servers = MCP_CATALOG.filter((s) => selectedServers.includes(s.name));
578
+ const envValues = {};
579
+ for (const server of servers) {
580
+ if (server.envPrompts) {
581
+ for (const [key, prompt] of Object.entries(server.envPrompts)) {
582
+ const value = await input2({ message: prompt });
583
+ envValues[key] = value;
584
+ }
585
+ }
586
+ }
587
+ await installMcpServers(servers, envValues);
588
+ console.log();
589
+ log.success("MCP servers configured successfully!");
590
+ }
591
+
592
+ // src/commands/generate.ts
593
+ import { existsSync as existsSync5 } from "node:fs";
594
+ import { join as join6 } from "node:path";
595
+ async function generateCommand(options = {}) {
596
+ banner();
597
+ const projectDir = process.cwd();
598
+ if (existsSync5(join6(projectDir, ".claude")) && !options.force) {
599
+ log.error(".claude/ already exists.");
600
+ log.info("Run `agent-stack update` to refresh the harness (preserves rules/ and tmp/),");
601
+ log.info("or pass --force to overwrite from scratch.");
602
+ process.exit(1);
603
+ }
604
+ log.step("Detecting project and generating the Claude harness...");
605
+ const files = await emitHarness(projectDir);
606
+ console.log();
607
+ log.success(`Generated ${files.length} harness file(s).`);
608
+ log.dim(" Next steps:");
609
+ log.dim(
610
+ " \u2022 Exclude the harness from git: add .claude/, CLAUDE.md, CODEMAP.md to .git/info/exclude"
611
+ );
612
+ log.dim(
613
+ " \u2022 Generate rules: open `claude` and run the pattern-scout subagent (.claude/rules/ is empty by design)"
614
+ );
615
+ log.dim(" \u2022 Smoke-test the guards: a `git push` or foreign package-manager command should block");
616
+ }
617
+
618
+ // src/commands/update.ts
619
+ import { existsSync as existsSync6, readFileSync as readFileSync3 } from "node:fs";
620
+ import { join as join7 } from "node:path";
621
+ async function updateCommand(options = {}) {
622
+ banner();
623
+ const projectDir = process.cwd();
624
+ if (!existsSync6(join7(projectDir, ".claude"))) {
625
+ log.error("No .claude/ found. Run `agent-stack generate` to deploy the harness first.");
626
+ process.exit(1);
627
+ }
628
+ const params = await detectHarnessParams(projectDir);
629
+ const files = buildHarnessFiles(params);
630
+ const created = [];
631
+ const changed = [];
632
+ let unchanged = 0;
633
+ for (const f of files) {
634
+ const full = join7(projectDir, f.path);
635
+ if (!existsSync6(full)) created.push(f.path);
636
+ else if (readFileSync3(full, "utf-8") !== f.content) changed.push(f.path);
637
+ else unchanged++;
638
+ }
639
+ if (options.dryRun) {
640
+ log.step("Harness update preview (dry run):");
641
+ created.forEach((p) => console.log(` + ${p}`));
642
+ changed.forEach((p) => console.log(` ~ ${p}`));
643
+ console.log();
644
+ log.dim(` ${unchanged} unchanged. rules/ and tmp/ are preserved (not emitter-owned).`);
645
+ return;
646
+ }
647
+ log.step("Updating the Claude harness...");
648
+ await writeHarnessFiles(projectDir, files);
649
+ console.log();
650
+ log.success(
651
+ `Harness updated: ${created.length} added, ${changed.length} changed, ${unchanged} unchanged.`
652
+ );
653
+ log.dim(" .claude/rules/ and .claude/tmp/ were left untouched.");
654
+ }
655
+
656
+ // src/commands/sync.ts
657
+ import { existsSync as existsSync7 } from "node:fs";
658
+ import { join as join8 } from "node:path";
659
+ async function syncCommand() {
660
+ await detectCommand();
661
+ console.log();
662
+ if (existsSync7(join8(process.cwd(), ".claude"))) {
663
+ await updateCommand();
664
+ } else {
665
+ await generateCommand();
666
+ }
667
+ }
668
+
669
+ // src/cli.ts
670
+ var program = new Command();
671
+ program.name("agent-stack").description("CLI that deploys a Claude Code harness and manages MCP servers").version("0.1.0");
672
+ program.command("init").description("Setup wizard: detect Claude Code, deploy the harness, install MCP servers").action(initCommand);
673
+ program.command("detect").description("Detect the project and whether Claude Code is installed").action(detectCommand);
674
+ var mcp = program.command("mcp").description("Manage MCP servers");
675
+ mcp.command("install").description("Install and configure MCP servers for detected providers").action(() => mcpCommand("install"));
676
+ mcp.command("list").description("List configured MCP servers").action(() => mcpCommand("list"));
677
+ program.command("generate").description("Deploy the Claude harness into the current repo (fresh)").option("-f, --force", "Overwrite an existing .claude/ harness").action(generateCommand);
678
+ program.command("update").description("Update an existing harness to the current version (preserves rules/ and tmp/)").option("--dry-run", "Show what would change without writing").action(updateCommand);
679
+ program.command("sync").description("Detect Claude Code and generate or update the harness").action(syncCommand);
680
+
681
+ // src/index.ts
682
+ program.parse();
683
+ //# sourceMappingURL=index.js.map