archgen-skill 0.0.5 → 0.0.6

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.
Files changed (36) hide show
  1. package/README.md +12 -6
  2. package/bin/archgen.mjs +22 -5
  3. package/lib/block.js +1 -1
  4. package/lib/bootstrap.js +103 -0
  5. package/lib/doctor.js +45 -2
  6. package/lib/init.js +32 -3
  7. package/lib/install.js +20 -1
  8. package/lib/mcp-server.js +159 -0
  9. package/lib/migrations/001-stamp-provenance.mjs +3 -3
  10. package/lib/spawn-npm.js +54 -0
  11. package/lib/store.js +75 -5
  12. package/lib/uninstall-project.js +16 -3
  13. package/lib/version.js +11 -3
  14. package/package.json +3 -3
  15. package/vendor/skills/archgen/SKILL.md +363 -213
  16. package/vendor/skills/archgen/archgen.config.json +1 -1
  17. package/vendor/skills/archgen/references/code-standards.md +2 -2
  18. package/vendor/skills/archgen/assets/mcp-template.json +0 -42
  19. package/vendor/skills/archgen/assets/skill-registry.json +0 -24
  20. package/vendor/skills/archgen/references/artifact-templates.md +0 -406
  21. package/vendor/skills/archgen/references/interview.md +0 -275
  22. package/vendor/skills/archgen/references/mcp-and-skills.md +0 -117
  23. package/vendor/skills/archgen/references/orchestration.md +0 -292
  24. package/vendor/skills/archgen/references/platforms.md +0 -138
  25. package/vendor/skills/archgen/scripts/doc-index.mjs +0 -457
  26. package/vendor/skills/archgen/scripts/impact.mjs +0 -77
  27. package/vendor/skills/archgen/scripts/lib/graph.mjs +0 -222
  28. package/vendor/skills/archgen/scripts/lib/plan-coverage.mjs +0 -32
  29. package/vendor/skills/archgen/scripts/lib/scope.mjs +0 -65
  30. package/vendor/skills/archgen/scripts/lib/yaml.mjs +0 -492
  31. package/vendor/skills/archgen/scripts/next-tasks.mjs +0 -55
  32. package/vendor/skills/archgen/scripts/plan-graph.mjs +0 -329
  33. package/vendor/skills/archgen/scripts/set-status.mjs +0 -82
  34. package/vendor/skills/archgen/scripts/update-agents.mjs +0 -213
  35. package/vendor/skills/archgen/scripts/validate.mjs +0 -87
  36. package/vendor/skills/archgen/scripts/verify-plan.mjs +0 -70
package/README.md CHANGED
@@ -6,7 +6,7 @@
6
6
 
7
7
  [![CI](https://github.com/akashmaurya99/archgen/actions/workflows/ci.yml/badge.svg)](https://github.com/akashmaurya99/archgen/actions/workflows/ci.yml)
8
8
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://github.com/akashmaurya99/archgen/blob/main/LICENSE)
9
- [![Node](https://img.shields.io/badge/node-%3E%3D18-brightgreen)](https://nodejs.org)
9
+ [![Node](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
10
10
 
11
11
  Describe what you want to build. archgen interviews you (or surveys your existing
12
12
  codebase), produces an architecture contract plus a dependency-ordered task graph,
@@ -49,8 +49,13 @@ npx archgen-skill install # symlinks; --copy for real copies
49
49
  ```
50
50
 
51
51
  Prefer shell? Clone the repo and run `./install.sh` (same behavior, plus
52
- `--init`, `--project`, `--uninstall`). Requires Node.js ≥ 18. The skill itself
53
- has zero npm dependencies.
52
+ `--init`, `--project`, `--uninstall`). Requires Node.js ≥ 20 — the official MCP
53
+ SDK v2 floor.
54
+
55
+ **What `init` actually does:** installs the skill (guidance only — no code),
56
+ wires the `archgen-mcp` MCP server into every detected harness, and ensures
57
+ CodeGraph (its CLI, its MCP entry, and the project index). The CLI itself is
58
+ zero-dependency; the MCP server it installs depends on the official MCP SDK.
54
59
 
55
60
  ## Quick start
56
61
 
@@ -125,11 +130,12 @@ are supported for all of them.
125
130
 
126
131
  ## VS Code extension (optional)
127
132
 
128
- A read-only visual layer that renders `.archgen/` as a live task DAG, your
133
+ A visual layer that renders `.archgen/` as a live task DAG, your
129
134
  code-dependency graph, and rendered docs — with a ▶ button that hands any task to
130
135
  your agent. Install it from the VS Code Marketplace or from a `.vsix` on
131
136
  [GitHub Releases](https://github.com/akashmaurya99/archgen/releases). It never
132
- edits anything: uninstalling it loses nothing.
137
+ edits your plan, your tasks, or your code — only its own runtime state under
138
+ `.archgen/.runtime/`, so uninstalling it loses nothing.
133
139
 
134
140
  ## Documentation
135
141
 
@@ -145,7 +151,7 @@ See [CONTRIBUTING.md](https://github.com/akashmaurya99/archgen/blob/main/CONTRIB
145
151
  Skill suite and CLI tests run with zero dependencies:
146
152
 
147
153
  ```sh
148
- node --test skill/scripts/test/*.test.mjs # skill suite
154
+ node --test packages/mcp/test/**/*.test.mjs # MCP server suite
149
155
  cd packages/cli && npm test # CLI suite
150
156
  ```
151
157
 
package/bin/archgen.mjs CHANGED
@@ -22,6 +22,7 @@ import { uninstallProject } from '../lib/uninstall-project.js';
22
22
  import { restoreMain } from '../lib/restore.js';
23
23
  import { migrateMain } from '../lib/migrate.js';
24
24
  import { compareSemver, fetchLatestVersion } from '../lib/version.js';
25
+ import { npmInvocation, whichOnPath } from '../lib/spawn-npm.js';
25
26
  import { spawnSync } from 'node:child_process';
26
27
  import { readFileSync } from 'node:fs';
27
28
 
@@ -46,6 +47,8 @@ Usage:
46
47
  and generate AGENTS.md / CLAUDE.md pointer blocks.
47
48
  [--force] proceed automatically; divergent copies are
48
49
  backed up to .archgen/.backup/ (never destroyed)
50
+ [--no-codegraph] skip the CodeGraph bootstrap
51
+ [--no-mcp] skip wiring the archgen MCP server
49
52
  npx archgen-skill doctor [dir] Health-check a project install: store integrity,
50
53
  version stamp, claude link, managed blocks,
51
54
  manifest. Repairs safe issues in place.
@@ -53,6 +56,8 @@ Usage:
53
56
  npx archgen-skill install Install into global harness skill dirs
54
57
  [--copy] real copies instead of symlinks
55
58
  [--uninstall] remove manifest-recorded entries
59
+ [--no-codegraph] skip the CodeGraph bootstrap
60
+ [--no-mcp] skip wiring the archgen MCP server
56
61
  npx archgen-skill uninstall Remove globally-installed copies (manifest-based)
57
62
  --project [dir] remove archgen from a project: claude link,
58
63
  managed blocks, canonical store (kept when
@@ -92,7 +97,9 @@ try {
92
97
  switch (cmd) {
93
98
  case 'init': {
94
99
  flag('--force');
95
- const r = initProject(positionalDir(), PACKAGE_ROOT);
100
+ const noCodegraph = flag('--no-codegraph');
101
+ const noMcp = flag('--no-mcp');
102
+ const r = initProject(positionalDir(), PACKAGE_ROOT, { codegraph: !noCodegraph, mcp: !noMcp });
96
103
  console.log('archgen: project ready (single-store layout)');
97
104
  console.log(' + store ' + r.storePath);
98
105
  if (r.claudeLink === 'created') console.log(' + link .claude/skills/archgen -> ../../.agents/skills/archgen');
@@ -120,8 +127,10 @@ try {
120
127
  }
121
128
  case 'install': {
122
129
  const copy = flag('--copy');
130
+ const noCodegraph = flag('--no-codegraph');
131
+ const noMcp = flag('--no-mcp');
123
132
  if (flag('--uninstall')) { uninstall(); break; }
124
- const r = installGlobal({ copy });
133
+ const r = installGlobal({ copy, codegraph: !noCodegraph, mcp: !noMcp });
125
134
  console.log('archgen: global harness install (mode: ' + (copy ? 'copy' : 'symlink') + ')');
126
135
  for (const [status, mode, path] of r.rows) console.log(` ${status.padEnd(7)} ${mode.padEnd(5)} ${path}`);
127
136
  console.log('\nPer-project setup (recommended): npx archgen-skill init');
@@ -137,6 +146,7 @@ try {
137
146
  if (r.storeRemoved) console.log(' - .agents/skills/archgen (unmodified store)');
138
147
  if (r.storeKept) console.log(' = .agents/skills/archgen KEPT (customized)');
139
148
  if (r.manifestRemoved) console.log(' - .archgen/.install-manifest.json');
149
+ for (const m of r.mcpRemoved ?? []) console.log(` - ${m} (archgen MCP entry)`);
140
150
  for (const w of r.warnings) console.log(' note: ' + w);
141
151
  console.log(' .archgen feature folders and backups are preserved.');
142
152
  } else {
@@ -177,17 +187,24 @@ try {
177
187
  runInitAndDoctor(dir);
178
188
  break;
179
189
  }
190
+ if (!/^\d+\.\d+\.\d+[-+]?[0-9A-Za-z.-]*$/.test(latest)) {
191
+ // Registry returned something that is not a semver tag — never
192
+ // interpolate it into a command line.
193
+ console.error(' ! registry returned a non-semver version string; refusing: ' + JSON.stringify(latest));
194
+ process.exitCode = 1;
195
+ break;
196
+ }
180
197
  console.log(' ↑ newer version available: ' + latest);
181
198
  console.log(' upgrading global install: npm install -g archgen-skill@' + latest);
182
- const up = spawnSync('npm', ['install', '-g', 'archgen-skill@' + latest], { stdio: 'inherit' });
199
+ const inv = npmInvocation(['install', '-g', 'archgen-skill@' + latest]);
200
+ const up = spawnSync(inv.command, inv.args, { stdio: 'inherit', shell: inv.shell });
183
201
  if (up.status !== 0) {
184
202
  console.error(' ! global upgrade failed — run it manually, then re-run: archgen-skill update');
185
203
  process.exitCode = 1;
186
204
  break;
187
205
  }
188
206
  // The running process is the OLD code; re-exec the freshly installed binary.
189
- const bin = spawnSync('command', ['-v', 'archgen-skill'], { encoding: 'utf8', shell: true });
190
- const freshBin = (bin.stdout || '').trim();
207
+ const freshBin = whichOnPath('archgen-skill');
191
208
  if (!freshBin) {
192
209
  console.log('\nUpgraded to v' + latest + '. Now finish per project with:\n npx archgen-skill@latest init');
193
210
  break;
package/lib/block.js CHANGED
@@ -98,7 +98,7 @@ export function renderBlock(skillRelPath = '.agents/skills/archgen', eol = '\n')
98
98
  '',
99
99
  'Rules of the road:',
100
100
  '- Generated artifacts live ONLY under `.archgen/<slug>/`',
101
- '- Never hand-edit task statuses - use `scripts/set-status.mjs` (comment-safe)',
101
+ '- Never hand-edit task statuses - call the `archgen_tasks` MCP tool (comment-safe)',
102
102
  '- Two gates before any execution: verifier approval, then human approval',
103
103
  '',
104
104
  '## Features registry',
@@ -0,0 +1,103 @@
1
+ // bootstrap.js — the install-time server bootstrap for `archgen init` /
2
+ // `archgen install`.
3
+ //
4
+ // The CLI used to shell out to the skill's scripts/codegraph.mjs. That script
5
+ // no longer exists: ALL deterministic capability now lives in the archgen MCP
6
+ // server, so the CLI drives THAT instead. One implementation, three ways in
7
+ // (agent -> MCP stdio, CLI -> `archgen-mcp bootstrap`, extension -> exec).
8
+ //
9
+ // The bootstrap does two things in one pass:
10
+ // 1. wires the archgen MCP entry into every detected harness
11
+ // 2. ensures CodeGraph: its CLI, its MCP entry, and the project index
12
+ //
13
+ // NEVER FATAL: no npm, no network, a read-only config, or a missing MCP
14
+ // server must still leave a working skill install. Failures are REPORTED,
15
+ // never thrown.
16
+ import { bootstrapMcp, resolveMcpBinary } from './mcp-server.js';
17
+
18
+ /**
19
+ * @param {{
20
+ * projectDir: string,
21
+ * location?: 'project'|'global'|'both',
22
+ * log?: (line: string) => void,
23
+ * env?: Record<string, string|undefined>,
24
+ * noCodegraph?: boolean,
25
+ * noMcp?: boolean,
26
+ * }} opts
27
+ * @returns {{ok: boolean, skipped?: string, detail?: string, report?: any}}
28
+ */
29
+ export function bootstrapServers(opts) {
30
+ const {
31
+ projectDir,
32
+ location = 'both',
33
+ log = () => {},
34
+ env = process.env,
35
+ noCodegraph = false,
36
+ noMcp = false,
37
+ } = opts;
38
+
39
+ // ARCHGEN_CODEGRAPH=0 skips the bootstrap (no spawn, no network, no writes)
40
+ // — without it the test suite would wire the developer's real MCP configs.
41
+ if (env.ARCHGEN_CODEGRAPH === '0' || env.ARCHGEN_CODEGRAPH === 'off') {
42
+ return { ok: true, skipped: 'ARCHGEN_CODEGRAPH=0 (bootstrap disabled)' };
43
+ }
44
+ if (noMcp && noCodegraph) {
45
+ return { ok: true, skipped: '--no-mcp and --no-codegraph passed' };
46
+ }
47
+
48
+ const resolved = resolveMcpBinary({ projectRoot: projectDir, env });
49
+ log(` + mcp server: ${resolved.bin ?? 'npx -y archgen-mcp'} (${resolved.source})`);
50
+
51
+ if (noMcp) {
52
+ log(' ! archgen MCP wiring skipped (--no-mcp)');
53
+ }
54
+ if (noCodegraph) {
55
+ log(' ! CodeGraph bootstrap skipped (--no-codegraph)');
56
+ }
57
+
58
+ const r = bootstrapMcp({
59
+ projectDir,
60
+ location,
61
+ approved: true,
62
+ env,
63
+ });
64
+
65
+ if (!r.report) {
66
+ return {
67
+ ok: true, // never fatal — the skill install itself succeeded
68
+ detail: `server bootstrap incomplete: ${r.detail ?? 'unknown error'}`,
69
+ };
70
+ }
71
+
72
+ const report = r.report;
73
+ const mcp = report.mcp ?? {};
74
+ const codegraph = report.codegraph ?? {};
75
+
76
+ for (const w of mcp.wired ?? []) {
77
+ log(` + harness: ${w.harness} → ${w.path} [${w.key}] ${w.status}${w.detail ? ' — ' + w.detail : ''}`);
78
+ }
79
+ if (codegraph.cli) {
80
+ log(
81
+ ` + codegraph ${codegraph.cli.version ?? 'unknown'}${codegraph.cli.action !== 'none' ? ' (' + codegraph.cli.action + ')' : ''}${codegraph.cli.latest ? ' · latest available ' + codegraph.cli.latest : ''}`,
82
+ );
83
+ }
84
+ if (codegraph.telemetry) {
85
+ log(` + telemetry: ${codegraph.telemetry.state ?? 'unknown'}${codegraph.telemetry.action !== 'none' ? ' → ' + codegraph.telemetry.action : ''}`);
86
+ }
87
+ for (const w of codegraph.wiring?.native ?? []) {
88
+ log(` + harness: ${w.targets} (${w.location}) → ${w.status}${w.detail ? ' — ' + w.detail : ''}`);
89
+ }
90
+ if (codegraph.index) {
91
+ log(` + index: ${codegraph.index.action ?? 'none'}${codegraph.index.files != null ? ' (' + codegraph.index.files + ' files)' : ''}`);
92
+ }
93
+ log(' + realtime: the MCP server watches files — the index stays live, no re-indexing needed');
94
+
95
+ if (r.ok === false || codegraph.ok === false) {
96
+ return {
97
+ ok: false,
98
+ report,
99
+ detail: 'one or more server bootstrap steps failed',
100
+ };
101
+ }
102
+ return { ok: true, report };
103
+ }
package/lib/doctor.js CHANGED
@@ -22,9 +22,10 @@ import {
22
22
  upsertManagedFile,
23
23
  } from './block.js';
24
24
  import { resolveSkillSource } from './init.js';
25
- import { CLAUDE_LINK_REL, DEV_ONLY_RELPATHS, MANIFEST_REL, STORE_REL, VERSION_FILE, cliVersion, hashDir, loadManifest, pruneDevOnly, saveManifest } from './store.js';
25
+ import { CLAUDE_LINK_REL, DEV_ONLY_RELPATHS, MANIFEST_REL, STORE_REL, VERSION_FILE, cliVersion, findObsolete, hashDir, loadManifest, pruneDevOnly, pruneObsolete, saveManifest } from './store.js';
26
26
  import { compareSemver, parseSemver } from './version.js';
27
27
  import { readStamp, writeStamp } from './version-stamp.js';
28
+ import { mcpDoctor } from './mcp-server.js';
28
29
 
29
30
  function lstatSafe(p) {
30
31
  try { return lstatSync(p); } catch { return null; }
@@ -83,7 +84,21 @@ export function doctorProject(projectDir, packageRoot, opts = {}) {
83
84
  if (!existsSync(join(storeAbs, 'SKILL.md'))) {
84
85
  add('FAIL', 'store .agents/skills/archgen missing or incomplete - run `archgen init`');
85
86
  } else {
86
- const missingCore = ['scripts', 'references'].filter((d) => !existsSync(join(storeAbs, d)));
87
+ // MIGRATION: the skill no longer ships code. An install carried over from
88
+ // the pre-MCP layout still has a scripts/ tree — dead weight that would
89
+ // otherwise read as "diverges from packaged skill (customized?)".
90
+ // Pruned BEFORE the divergence check so an upgrade is not misreported.
91
+ const packaged = (() => {
92
+ try { return resolveSkillSource(packageRoot); } catch { return null; }
93
+ })();
94
+ if (packaged) {
95
+ // --check reports WITHOUT mutating; repair mode prunes.
96
+ const obsolete = checkOnly ? findObsolete(storeAbs, packaged) : pruneObsolete(storeAbs, packaged);
97
+ for (const rel of obsolete) {
98
+ add(checkOnly ? 'WOULD-FIX' : 'FIXED', `store carried the obsolete ${rel}/ tree (pre-MCP layout) -> ${checkOnly ? 'would prune' : 'pruned'}`);
99
+ }
100
+ }
101
+ const missingCore = ['references'].filter((d) => !existsSync(join(storeAbs, d)));
87
102
  if (missingCore.length > 0) {
88
103
  // Deferred so the managed-block FAIL rows below stay FIRST: existing
89
104
  // consumers read checks.find(FAIL) expecting the marker verdict.
@@ -296,6 +311,34 @@ export function doctorProject(projectDir, packageRoot, opts = {}) {
296
311
  add(v ? 'OK' : 'WARN', `skill ${p}: ${slot}`);
297
312
  }
298
313
 
314
+ // 8. MCP server wiring — the archgen MCP server is what actually gives the
315
+ // agent its tools. A skill with no MCP entry is a skill the agent cannot
316
+ // use, so this is a first-class check, not a footnote.
317
+ //
318
+ // READ-ONLY and NEVER FATAL: doctor must work on a machine with no npm, no
319
+ // network, and no MCP server installed. The MCP server is asked for its own
320
+ // report; if it cannot run, that is a WARN with the remediation command.
321
+ const mcp = mcpDoctor({ projectRoot: root, env: { ...process.env, HOME: homeDir } });
322
+ if (!mcp.report) {
323
+ add('WARN', `archgen MCP server not reachable${mcp.detail ? ` — ${mcp.detail}` : ''} — install with: npx archgen-mcp bootstrap --project . --yes`);
324
+ } else {
325
+ const rep = mcp.report;
326
+ add(
327
+ rep.skill ? 'OK' : 'WARN',
328
+ rep.skill
329
+ ? `archgen MCP server v${rep.server} ready (skill ${rep.skill.version ?? 'unknown'})`
330
+ : 'archgen MCP server ready, but no skill was found',
331
+ );
332
+ const wired = (rep.mcpTargets ?? []).filter((t) => t.exists).length;
333
+ if (wired === 0) {
334
+ add('WARN', 'archgen MCP is not wired into any harness config — run: npx archgen-mcp bootstrap --project . --yes');
335
+ } else {
336
+ add('OK', `archgen MCP wiring present in ${wired} harness config(s)`);
337
+ }
338
+ if (rep.codegraph) add('OK', `codegraph ${rep.codegraph.version}`);
339
+ else add('WARN', 'codegraph not installed — run: npx archgen-mcp bootstrap --project . --yes');
340
+ }
341
+
299
342
  const tally = { OK: 0, FIXED: 0, 'WOULD-FIX': 0, WARN: 0, FAIL: 0 };
300
343
  for (const c of checks) tally[c.status]++;
301
344
  return { root, checks, tally, failures: opFailures };
package/lib/init.js CHANGED
@@ -17,12 +17,13 @@
17
17
  // to a symlink; a customized one is left untouched with a warning;
18
18
  // - installs are atomic: built in a temp sibling dir, then renamed.
19
19
 
20
- import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, renameSync, rmSync, symlinkSync, unlinkSync } from 'node:fs';
20
+ import { cpSync, existsSync, lstatSync, mkdirSync, readlinkSync, renameSync, rmSync, symlinkSync, unlinkSync } from 'node:fs';
21
21
  import { randomBytes } from 'node:crypto';
22
22
  import { dirname, join, resolve } from 'node:path';
23
23
  import { importsAgents, renderBlock, START, END, upsertManagedFile } from './block.js';
24
24
  import { CLAUDE_LINK_REL, STORE_REL, VERSION_FILE, appendEntry, assertNotSymlink, assertWithinRoot, cliVersion, hashDir, moveToBackup, pruneDevOnly } from './store.js';
25
25
  import { writeStamp } from './version-stamp.js';
26
+ import { bootstrapServers } from './bootstrap.js';
26
27
 
27
28
  const CONTEXT_FILES = ['AGENTS.md', 'CLAUDE.md'];
28
29
 
@@ -99,8 +100,11 @@ function installClaudeAdapter(root, source, warnings) {
99
100
  * claude adapter, and write context pointers.
100
101
  * @param {string} projectDir target repo root
101
102
  * @param {string} packageRoot dir containing this CLI package
102
- * @param {{force?: boolean}} opts --force proceeds automatically; divergent
103
- * copies are always auto-backed-up (never refused, never destroyed)
103
+ * @param {{force?: boolean, codegraph?: boolean, mcp?: boolean, log?: (s: string) => void, env?: NodeJS.ProcessEnv}} opts
104
+ * --force proceeds automatically; divergent copies are always auto-backed-up
105
+ * (never refused, never destroyed). opts.codegraph === false opts OUT of the
106
+ * automatic CodeGraph bootstrap (install + MCP wiring + index build);
107
+ * opts.mcp === false opts OUT of the archgen MCP wiring.
104
108
  * @returns {{storePath: string, claudeLink: 'created'|'existing'|'migrated'|'skipped'|'kept-divergent',
105
109
  * backups: string[], contextFiles: string[], createdContextFiles: string[], warnings: string[]}}
106
110
  */
@@ -110,6 +114,9 @@ export function initProject(projectDir, packageRoot, opts = {}) {
110
114
  // both plain and --force runs take the same safe path.
111
115
  const force = opts.force === true;
112
116
  void force;
117
+ const codegraphAllowed = opts.codegraph !== false;
118
+ const mcpAllowed = opts.mcp !== false;
119
+ const log = opts.log ?? console.log;
113
120
 
114
121
  const root = resolve(projectDir);
115
122
  const source = resolveSkillSource(packageRoot);
@@ -165,5 +172,27 @@ export function initProject(projectDir, packageRoot, opts = {}) {
165
172
  appendEntry(root, 'block', name);
166
173
  }
167
174
 
175
+ // 4. Server bootstrap — wire the archgen MCP server into every detected
176
+ // harness AND ensure CodeGraph (its CLI, its MCP entry, the project
177
+ // index). Non-fatal by design: an offline machine still gets the skill
178
+ // installed.
179
+ if (codegraphAllowed || mcpAllowed) {
180
+ const r = bootstrapServers({
181
+ projectDir: root,
182
+ location: 'both',
183
+ log,
184
+ env: opts.env,
185
+ noCodegraph: !codegraphAllowed,
186
+ noMcp: !mcpAllowed,
187
+ });
188
+ if (!r.ok) {
189
+ warnings.push(`server bootstrap incomplete${r.detail ? ': ' + r.detail : ''} — install later with: npx archgen-mcp bootstrap --project . --yes`);
190
+ } else if (r.skipped) {
191
+ warnings.push(`server bootstrap skipped (${r.skipped})`);
192
+ }
193
+ } else {
194
+ warnings.push('server bootstrap skipped (--no-codegraph --no-mcp)');
195
+ }
196
+
168
197
  return { storePath: storeAbs, claudeLink, backups, contextFiles, createdContextFiles, warnings };
169
198
  }
package/lib/install.js CHANGED
@@ -10,6 +10,7 @@ import { resolveSkillSource } from './init.js';
10
10
  import { assertNotSymlink, assertWithinRoot, cliVersion, hashDir, moveToBackupInto, pruneDevOnly, refuseSymlinkMessage } from './store.js';
11
11
  import { writeStamp } from './version-stamp.js';
12
12
  import { loadConfig } from './config.js';
13
+ import { bootstrapServers } from './bootstrap.js';
13
14
 
14
15
  // Module-anchored default: works from the npm package (cli/lib -> cli/vendor)
15
16
  // AND from a monorepo checkout (packages/cli/lib -> ../../skill) without depending on cwd.
@@ -58,7 +59,7 @@ function nextBackupTs(tdir) {
58
59
  }
59
60
 
60
61
  /**
61
- * @param {{copy?: boolean, home?: string, packageRoot?: string}} opts
62
+ * @param {{copy?: boolean, home?: string, packageRoot?: string, codegraph?: boolean, mcp?: boolean, log?: (s: string) => void, env?: NodeJS.ProcessEnv}} opts
62
63
  * @returns {{rows: Array<[string, string, string]>, failures: number, manifest: string,
63
64
  * backups: string[]}} rows are [STATUS, MODE, PATH]; `backups` lists
64
65
  * absolute vault locations written by this run.
@@ -165,6 +166,24 @@ export function installGlobal(opts = {}) {
165
166
  rows.push(['FAILED', '-', dest + ' (' + e.message + ')']);
166
167
  }
167
168
  }
169
+ // Server bootstrap — GLOBAL wiring only (project-local configs belong to
170
+ // `init`). Wires the archgen MCP server and CodeGraph into every DETECTED
171
+ // global harness. Non-fatal: an offline machine still gets the skill
172
+ // installed.
173
+ if (opts.codegraph !== false || opts.mcp !== false) {
174
+ const r = bootstrapServers({
175
+ projectDir: process.cwd(),
176
+ location: 'global',
177
+ log: opts.log ?? console.log,
178
+ env: opts.env,
179
+ noCodegraph: opts.codegraph === false,
180
+ noMcp: opts.mcp === false,
181
+ });
182
+ if (!r.ok) {
183
+ rows.push(['WARN', '-', 'server bootstrap incomplete — run: npx archgen-mcp bootstrap --location global --yes']);
184
+ }
185
+ }
186
+
168
187
  return { rows, failures, manifest, backups };
169
188
  }
170
189
 
@@ -0,0 +1,159 @@
1
+ // mcp-server.js — locate and drive the `archgen-mcp` server from the CLI.
2
+ //
3
+ // WHY THE CLI DELEGATES: archgen's deterministic capability now lives in ONE
4
+ // place — the MCP server. The CLI used to shell out to the skill's
5
+ // scripts/codegraph.mjs; it now shells out to `archgen-mcp bootstrap`, which
6
+ // runs the same code the agent's tools run. There is no second implementation
7
+ // of harness wiring, CodeGraph bootstrap, or doctor logic.
8
+ //
9
+ // NEVER FATAL: no npm, no network, a read-only config, or a missing MCP
10
+ // server must still leave a working skill install. Failures are REPORTED,
11
+ // never thrown — the same posture bootstrapCodegraph has always had.
12
+ import { existsSync } from 'node:fs';
13
+ import { spawnSync } from 'node:child_process';
14
+ import { homedir } from 'node:os';
15
+ import { join, resolve } from 'node:path';
16
+
17
+ export const MCP_PACKAGE = 'archgen-mcp';
18
+
19
+ /**
20
+ * Locate the archgen-mcp binary.
21
+ *
22
+ * Resolution order (first hit wins):
23
+ * 1. $ARCHGEN_MCP_BIN — explicit override
24
+ * 2. <project>/node_modules/.bin/archgen-mcp
25
+ * 3. `archgen-mcp` on PATH — a global npm install
26
+ * 4. `npx -y archgen-mcp` — network fallback (npx caches it)
27
+ *
28
+ * @param {{projectRoot?: string, home?: string, env?: Record<string, string|undefined>}} opts
29
+ * @returns {{bin: string|null, source: 'override'|'local'|'path'|'npx', command: string, args: string[]}}
30
+ */
31
+ export function resolveMcpBinary(opts = {}) {
32
+ const { projectRoot = process.cwd(), home = homedir(), env = process.env } = opts;
33
+
34
+ const override = env.ARCHGEN_MCP_BIN;
35
+ if (override && existsSync(override)) {
36
+ return { bin: override, source: 'override', command: override, args: [] };
37
+ }
38
+
39
+ const local = join(
40
+ resolve(projectRoot),
41
+ 'node_modules',
42
+ '.bin',
43
+ process.platform === 'win32' ? 'archgen-mcp.cmd' : 'archgen-mcp',
44
+ );
45
+ if (existsSync(local)) return { bin: local, source: 'local', command: local, args: [] };
46
+
47
+ const onPath = whichOnPath('archgen-mcp', env);
48
+ if (onPath) return { bin: onPath, source: 'path', command: onPath, args: [] };
49
+
50
+ return { bin: null, source: 'npx', command: 'npx', args: ['-y', MCP_PACKAGE] };
51
+ }
52
+
53
+ /**
54
+ * Locate an executable on PATH (cross-platform, no shell).
55
+ * @param {string} bin
56
+ * @param {Record<string, string|undefined>} env
57
+ * @returns {string|null}
58
+ */
59
+ export function whichOnPath(bin, env = process.env) {
60
+ const exts = process.platform === 'win32' ? (env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';') : [''];
61
+ const dirs = (env.PATH ?? '').split(process.platform === 'win32' ? ';' : ':').filter(Boolean);
62
+ for (const d of dirs) {
63
+ for (const ext of exts) {
64
+ const p = join(d, bin + ext);
65
+ try {
66
+ if (existsSync(p) && !p.endsWith('/')) return p;
67
+ } catch {
68
+ /* not here */
69
+ }
70
+ }
71
+ }
72
+ return null;
73
+ }
74
+
75
+ /**
76
+ * Run one `archgen-mcp` subcommand and return its JSON output.
77
+ *
78
+ * @param {string[]} args the subcommand + flags
79
+ * @param {{projectRoot?: string, timeout?: number, env?: Record<string, string|undefined>}} opts
80
+ * @returns {{ok: boolean, code: number|null, data: any, stdout: string, stderr: string, detail?: string}}
81
+ */
82
+ export function runMcp(args, opts = {}) {
83
+ const { projectRoot = process.cwd(), timeout = 600000, env = process.env } = opts;
84
+ const resolved = resolveMcpBinary({ projectRoot, env });
85
+
86
+ let r;
87
+ try {
88
+ r = spawnSync(resolved.command, [...resolved.args, ...args], {
89
+ encoding: 'utf8',
90
+ timeout,
91
+ env,
92
+ cwd: projectRoot,
93
+ shell: false,
94
+ // On Windows the npx shim is a .cmd file, which needs a shell.
95
+ ...(process.platform === 'win32' && /\.(cmd|bat)$/i.test(resolved.command) ? { shell: true } : {}),
96
+ });
97
+ } catch (e) {
98
+ return { ok: false, code: null, data: null, stdout: '', stderr: '', detail: String(e?.message ?? e) };
99
+ }
100
+ if (r.error) {
101
+ return { ok: false, code: null, data: null, stdout: '', stderr: '', detail: String(r.error.message ?? r.error) };
102
+ }
103
+ const stdout = r.stdout ?? '';
104
+ let data = null;
105
+ try {
106
+ data = JSON.parse(stdout);
107
+ } catch {
108
+ /* not JSON — the caller renders the raw output */
109
+ }
110
+ return {
111
+ ok: r.status === 0,
112
+ code: r.status ?? null,
113
+ data,
114
+ stdout,
115
+ stderr: r.stderr ?? '',
116
+ };
117
+ }
118
+
119
+ /**
120
+ * Wire the archgen MCP server into the detected harnesses and ensure CodeGraph.
121
+ *
122
+ * @param {{projectRoot: string, location?: 'project'|'global'|'both', approved?: boolean, env?: Record<string, string|undefined>}} opts
123
+ * @returns {{ok: boolean, skipped?: string, detail?: string, report?: any}}
124
+ */
125
+ export function bootstrapMcp(opts) {
126
+ const { projectRoot, location = 'both', approved = true, env = process.env } = opts;
127
+ const args = ['bootstrap', '--project', projectRoot, '--location', location, '--json'];
128
+ if (approved) args.push('--yes');
129
+
130
+ const r = runMcp(args, { projectRoot, env });
131
+ if (r.detail) {
132
+ return { ok: false, detail: `cannot run ${MCP_PACKAGE}: ${r.detail}` };
133
+ }
134
+ if (r.data === null) {
135
+ const firstLine = (r.stderr || r.stdout || '').trim().split('\n')[0];
136
+ return { ok: false, detail: firstLine || `${MCP_PACKAGE} bootstrap produced no output` };
137
+ }
138
+ return { ok: r.ok, report: r.data, detail: r.ok ? undefined : r.stderr.trim().split('\n')[0] };
139
+ }
140
+
141
+ /**
142
+ * Health-check the archgen MCP install.
143
+ * @param {{projectRoot: string, env?: Record<string, string|undefined>}} opts
144
+ */
145
+ export function mcpDoctor(opts) {
146
+ const { projectRoot, env = process.env } = opts;
147
+ const r = runMcp(['doctor', '--project', projectRoot, '--json'], { projectRoot, env, timeout: 60000 });
148
+ return { ok: r.ok, report: r.data, detail: r.detail ?? (r.data ? undefined : r.stderr.trim().split('\n')[0]) };
149
+ }
150
+
151
+ /**
152
+ * Remove the archgen MCP entries.
153
+ * @param {{projectRoot: string, env?: Record<string, string|undefined>}} opts
154
+ */
155
+ export function uninstallMcp(opts) {
156
+ const { projectRoot, env = process.env } = opts;
157
+ const r = runMcp(['uninstall-mcp', '--project', projectRoot, '--yes', '--json'], { projectRoot, env, timeout: 60000 });
158
+ return { ok: r.ok, report: r.data, detail: r.detail };
159
+ }
@@ -4,9 +4,9 @@
4
4
  //
5
5
  // What it writes: three YAML COMMENT lines at the very top of tasks.yaml and
6
6
  // architecture.yaml. Comments are inert to every consumer by contract — the
7
- // skill's zero-dep parser records them positionally (set-status.mjs preserves
8
- // them verbatim), verify-plan.mjs never sees them as data, and the VS Code
9
- // extension port behaves identically. Additive-only: no existing key is
7
+ // MCP core's parser records them positionally (`archgen_tasks{set_status}`
8
+ // preserves them verbatim), the verifier never sees them as data, and the VS
9
+ // Code extension port behaves identically. Additive-only: no existing key is
10
10
  // touched, nothing becomes required.
11
11
  //
12
12
  // # schema_version: 1
@@ -0,0 +1,54 @@
1
+ // spawn-npm.js — run npm on every platform without a shell.
2
+ // An unshelled spawnSync("npm", …) fails with EINVAL on Windows since the
3
+ // CVE-2024-27980 hardening, so npm's JS entry point is invoked with the
4
+ // current node binary instead.
5
+ import { existsSync } from 'node:fs';
6
+ import { dirname, join } from 'node:path';
7
+
8
+ /** npm-cli.js path: `npm_execpath` first, then node-relative layouts. */
9
+ export function npmCliPath() {
10
+ const fromEnv = process.env.npm_execpath;
11
+ if (typeof fromEnv === 'string' && fromEnv.endsWith('.js') && existsSync(fromEnv)) {
12
+ return fromEnv;
13
+ }
14
+ const dir = dirname(process.execPath);
15
+ const candidates =
16
+ process.platform === 'win32'
17
+ ? [join(dir, 'node_modules', 'npm', 'bin', 'npm-cli.js')] // Windows: node.exe sits beside node_modules/
18
+ : [join(dir, '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js')]; // POSIX: bin/../lib/...
19
+ for (const c of candidates) if (existsSync(c)) return c;
20
+ // Homebrew cellar layout: /opt/homebrew/{bin,lib}/...
21
+ if (process.platform !== 'win32') {
22
+ const brew = join(dir, '..', '..', 'lib', 'node_modules', 'npm', 'bin', 'npm-cli.js');
23
+ if (existsSync(brew)) return brew;
24
+ }
25
+ return null;
26
+ }
27
+
28
+ /**
29
+ * Spawn npm with { command, args, shell }. Args MUST be registry-verified
30
+ * constants (the shell fallback on Windows interpolates them).
31
+ */
32
+ export function npmInvocation(npmArgs) {
33
+ const cli = npmCliPath();
34
+ if (cli) return { command: process.execPath, args: [cli, ...npmArgs], shell: false };
35
+ const isWin = process.platform === 'win32';
36
+ return { command: isWin ? 'npm.cmd' : 'npm', args: npmArgs, shell: isWin };
37
+ }
38
+
39
+ /** Cross-platform `which` (PATHEXT on Windows). */
40
+ export function whichOnPath(bin) {
41
+ const exts = process.platform === 'win32'
42
+ ? (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';')
43
+ : [''];
44
+ const dirs = (process.env.PATH ?? '').split(process.platform === 'win32' ? ';' : ':').filter(Boolean);
45
+ for (const d of dirs) {
46
+ for (const ext of exts) {
47
+ const p = join(d, bin + ext);
48
+ try {
49
+ if (existsSync(p)) return p;
50
+ } catch { /* next */ }
51
+ }
52
+ }
53
+ return null;
54
+ }