archgen-skill 0.0.7 → 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/README.md CHANGED
@@ -87,7 +87,7 @@ by git.
87
87
  | `npx archgen-skill init [dir]` | Prepare a project: canonical skill store at `.agents/skills/archgen`, Claude symlink adapter, AGENTS.md hub + CLAUDE.md bridge |
88
88
  | `npx archgen-skill install [--copy]` | Install into global harness skill dirs (symlinks by default, real copies with `--copy`) |
89
89
  | `npx archgen-skill uninstall` | Remove globally-installed copies (manifest-recorded, safe removal) |
90
- | `npx archgen-skill uninstall [dir]` | Remove a **project** install (same as `--project [dir]`): strips managed blocks, symlink, and store **only if unmodified** — `.archgen/` feature folders and all user content are preserved |
90
+ | `npx archgen-skill uninstall [dir]` | Remove a **project** install (same as `--project [dir]`): strips managed blocks, symlink, the store **only if unmodified**, and the dead `.archgen/.runtime/` extension state — `.archgen/` feature folders and all user content are preserved |
91
91
  | `npx archgen-skill doctor [dir] [--check]` | Verify and auto-repair an installation: store integrity, version stamp, Claude link, managed blocks, manifest (`--check` reports without fixing) |
92
92
  | `npx archgen-skill update [dir]` | Check npm for a newer `archgen-skill`, upgrade the global install if outdated, then re-init the project and run doctor against the new version |
93
93
  | `npx archgen-skill restore` | List backup snapshots; `--snapshot <ts>` restores one (current state is backed up first) |
@@ -135,15 +135,6 @@ Claude Code, OpenCode, Cursor, Codex, Gemini CLI, Antigravity — and any
135
135
  agentskills.io-compatible harness. Both global install and project-local `init`
136
136
  are supported for all of them.
137
137
 
138
- ## VS Code extension (optional)
139
-
140
- A visual layer that renders `.archgen/` as a live task DAG, your
141
- code-dependency graph, and rendered docs — with a ▶ button that hands any task to
142
- your agent. Install it from the VS Code Marketplace or from a `.vsix` on
143
- [GitHub Releases](https://github.com/akashmaurya99/archgen/releases). It never
144
- edits your plan, your tasks, or your code — only its own runtime state under
145
- `.archgen/.runtime/`, so uninstalling it loses nothing.
146
-
147
138
  ## Documentation
148
139
 
149
140
  | Doc | Contents |
package/bin/archgen.mjs CHANGED
@@ -57,6 +57,9 @@ Usage:
57
57
  backed up to .archgen/.backup/ (never destroyed)
58
58
  [--no-codegraph] skip the CodeGraph bootstrap
59
59
  [--no-mcp] skip wiring the archgen MCP server
60
+ [--location-project] wire the PROJECT-local harness configs too
61
+ (by default only the global ones are wired,
62
+ so a project carries no MCP configuration)
60
63
  npx archgen-skill doctor [dir] Health-check a project install: store integrity,
61
64
  version stamp, claude link, managed blocks,
62
65
  manifest. Repairs safe issues in place.
@@ -166,9 +169,17 @@ try {
166
169
  flag('--force');
167
170
  const noCodegraph = flag('--no-codegraph');
168
171
  const noMcp = flag('--no-mcp');
172
+ // The harness configs live in the user's home by default, so a project
173
+ // carries no MCP configuration of its own. `--location project` opts
174
+ // into wiring the project-local configs instead.
175
+ const location = flag('--location-project') ? 'project' : 'global';
169
176
  const dir = targetDir();
170
177
  assertNoUnknownFlags();
171
- const r = initProject(dir, PACKAGE_ROOT, { codegraph: !noCodegraph, mcp: !noMcp });
178
+ const r = initProject(dir, PACKAGE_ROOT, {
179
+ codegraph: !noCodegraph,
180
+ mcp: !noMcp,
181
+ location,
182
+ });
172
183
  console.log('archgen: project ready (single-store layout)');
173
184
  console.log(' + store ' + r.storePath);
174
185
  if (r.claudeLink === 'created') console.log(' + link .claude/skills/archgen -> ../../.agents/skills/archgen');
@@ -238,6 +249,7 @@ try {
238
249
  if (r.storeRemoved) console.log(' - .agents/skills/archgen (unmodified store)');
239
250
  if (r.storeKept) console.log(' = .agents/skills/archgen KEPT (customized)');
240
251
  if (r.manifestRemoved) console.log(' - .archgen/.install-manifest.json');
252
+ if (r.runtimeRemoved) console.log(' - .archgen/.runtime (legacy extension state)');
241
253
  for (const m of r.mcpRemoved ?? []) console.log(` - ${m} (archgen MCP entry)`);
242
254
  for (const w of r.warnings) console.log(' note: ' + w);
243
255
  console.log(' .archgen feature folders and backups are preserved.');
package/lib/bootstrap.js CHANGED
@@ -3,8 +3,8 @@
3
3
  //
4
4
  // The CLI used to shell out to the skill's scripts/codegraph.mjs. That script
5
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).
6
+ // server, so the CLI drives THAT instead. One implementation, two ways in
7
+ // (agent -> MCP stdio, CLI -> `archgen-mcp bootstrap`).
8
8
  //
9
9
  // The bootstrap does two things in one pass:
10
10
  // 1. wires the archgen MCP entry into every detected harness
@@ -15,26 +15,31 @@
15
15
  // the caller (ok:false + detail) and rendered as a warning by it — never
16
16
  // thrown, and never swallowed.
17
17
  import { bootstrapMcp, resolveMcpBinary } from './mcp-server.js';
18
+ import { ensureGlobalServer, serverVersion } from './global-server.js';
18
19
 
19
20
  /**
20
21
  * @param {{
21
22
  * projectDir: string,
23
+ * packageRoot?: string,
22
24
  * location?: 'project'|'global'|'both',
23
25
  * log?: (line: string) => void,
24
26
  * env?: Record<string, string|undefined>,
25
27
  * noCodegraph?: boolean,
26
28
  * noMcp?: boolean,
29
+ * global?: boolean,
27
30
  * }} opts
28
31
  * @returns {{ok: boolean, skipped?: string, detail?: string, report?: any}}
29
32
  */
30
33
  export function bootstrapServers(opts) {
31
34
  const {
32
35
  projectDir,
36
+ packageRoot = null,
33
37
  location = 'both',
34
38
  log = () => {},
35
39
  env = process.env,
36
40
  noCodegraph = false,
37
41
  noMcp = false,
42
+ global = true,
38
43
  } = opts;
39
44
 
40
45
  // ARCHGEN_CODEGRAPH=0 skips the bootstrap (no spawn, no network, no writes)
@@ -46,8 +51,25 @@ export function bootstrapServers(opts) {
46
51
  return { ok: true, skipped: '--no-mcp and --no-codegraph passed' };
47
52
  }
48
53
 
54
+ // ONE server for every harness. A global install gives every harness the
55
+ // same binary at a stable path, so a harness config can name an absolute
56
+ // executable that keeps working — instead of pointing into npm's npx
57
+ // CACHE, which `npm cache clean` removes out from under it.
58
+ //
59
+ // The server is refreshed to the version THIS CLI ships with: the CLI and
60
+ // the server are released together, so an older server is an older
61
+ // implementation of the very tools the CLI wires.
62
+ let serverNote = '';
63
+ if (global && !noMcp) {
64
+ const version = packageRoot ? serverVersion(packageRoot) : 'latest';
65
+ const installed = ensureGlobalServer({ env, version });
66
+ serverNote = installed.note;
67
+ if (installed.detail) serverNote += ` (${installed.detail})`;
68
+ }
69
+
49
70
  const resolved = resolveMcpBinary({ projectRoot: projectDir, env });
50
71
  log(` + mcp server: ${resolved.bin ?? 'npx -y archgen-mcp'} (${resolved.source})`);
72
+ if (serverNote) log(` + mcp server: ${serverNote}`);
51
73
 
52
74
  if (noMcp) {
53
75
  log(' ! archgen MCP wiring skipped (--no-mcp)');
@@ -57,7 +79,7 @@ export function bootstrapServers(opts) {
57
79
  }
58
80
 
59
81
  const r = bootstrapMcp({
60
- projectDir,
82
+ projectRoot: projectDir,
61
83
  location,
62
84
  approved: true,
63
85
  env,
package/lib/doctor.js CHANGED
@@ -363,9 +363,9 @@ export function doctorProject(projectDir, packageRoot, opts = {}) {
363
363
 
364
364
  // 7. Version-stamp inventory — every archgen skill tree visible from this
365
365
  // project/machine, one line each, for tools that probe
366
- // <skillRoot>/.archgen-version (the VS Code extension reads it to warn
367
- // "skill outdated"). Report-only: doctor repairs the project stamp in
368
- // section 2, never global installs.
366
+ // <skillRoot>/.archgen-version (the MCP server reads it to report the
367
+ // installed skill's version). Report-only: doctor repairs the project
368
+ // stamp in section 2, never global installs.
369
369
  const seen = new Set();
370
370
  const installs = [];
371
371
  const noteInstall = (p) => {
@@ -0,0 +1,154 @@
1
+ // global-server.js — ONE archgen MCP server for every harness, kept current.
2
+ //
3
+ // WHY A GLOBAL INSTALL.
4
+ //
5
+ // The harness configs name the server's executable by ABSOLUTE PATH, so the
6
+ // path has to keep working. The npx fallback points into npm's npx CACHE
7
+ // (~/.npm/_npx/<hash>/…), which `npm cache clean --force` removes — and the
8
+ // harness then fails to start a server that was wired minutes earlier.
9
+ //
10
+ // A global install (`npm install -g archgen-mcp`) puts the binary in the
11
+ // npm prefix's bin directory: one copy, shared by every harness, at a path
12
+ // that does not move when the registry publishes a new version.
13
+ //
14
+ // THE UPGRADE.
15
+ //
16
+ // The CLI and the server are released TOGETHER, so the CLI knows exactly
17
+ // which server version it was tested against. An installed server that is
18
+ // OLDER is refreshed to that version; one that is NEWER (a dev build, or a
19
+ // version published after this CLI) is left alone — downgrading it would
20
+ // break a working setup.
21
+ //
22
+ // The install is BEST EFFORT. A machine without a writable npm prefix, an
23
+ // offline machine, or a user who does not want a global package must still
24
+ // get a working skill install: the npx fallback covers all of them, and the
25
+ // OLD server keeps working until it is replaced.
26
+ import { spawnSync } from 'node:child_process';
27
+ import { readFileSync, realpathSync } from 'node:fs';
28
+ import { dirname, join } from 'node:path';
29
+ import { npmInvocation, whichOnPath } from './spawn-npm.js';
30
+ import { compareSemver } from './version.js';
31
+
32
+ export const MCP_PACKAGE = 'archgen-mcp';
33
+
34
+ /**
35
+ * The version of the server this CLI ships with.
36
+ *
37
+ * The CLI and the server are released together, so the installer can ask for
38
+ * exactly the version it was tested against instead of "whatever is latest".
39
+ *
40
+ * @param {string} packageRoot the CLI's own package root
41
+ * @returns {string}
42
+ */
43
+ export function serverVersion(packageRoot) {
44
+ try {
45
+ const pkg = JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8'));
46
+ return typeof pkg.version === 'string' ? pkg.version : 'latest';
47
+ } catch {
48
+ return 'latest';
49
+ }
50
+ }
51
+
52
+ /**
53
+ * The version of an INSTALLED server, read from its own package.
54
+ *
55
+ * The binary is a symlink into the global package's bin directory, so the
56
+ * package root is one level above the script's directory.
57
+ *
58
+ * @param {string} bin the resolved executable
59
+ * @returns {string|null} the version, or null when it cannot be read
60
+ */
61
+ export function installedServerVersion(bin) {
62
+ try {
63
+ const real = realpathSync(bin);
64
+ const pkg = JSON.parse(readFileSync(join(dirname(real), '..', 'package.json'), 'utf8'));
65
+ return typeof pkg.version === 'string' ? pkg.version : null;
66
+ } catch {
67
+ return null;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Make sure `archgen-mcp` is installed globally, at the right version.
73
+ *
74
+ * @param {{env?: Record<string, string|undefined>, version?: string}} opts
75
+ * @returns {{installed: boolean, bin: string|null, upgraded: boolean,
76
+ * note: string, detail?: string}}
77
+ */
78
+ export function ensureGlobalServer(opts = {}) {
79
+ const { env = process.env, version = 'latest' } = opts;
80
+
81
+ const onPath = whichOnPath(MCP_PACKAGE, env);
82
+ if (onPath) {
83
+ const installed = installedServerVersion(onPath);
84
+ if (installed === version) {
85
+ return {
86
+ installed: true,
87
+ bin: onPath,
88
+ upgraded: false,
89
+ note: `already installed at ${onPath} (v${installed})`,
90
+ };
91
+ }
92
+ if (installed && version !== 'latest' && compareSemver(installed, version) > 0) {
93
+ // A newer server is already there — a dev build, or a release published
94
+ // after this CLI. Downgrading it would break a working setup.
95
+ return {
96
+ installed: true,
97
+ bin: onPath,
98
+ upgraded: false,
99
+ note: `installed v${installed} is newer than this CLI's v${version} — left as it is`,
100
+ };
101
+ }
102
+ const refreshed = installGlobal(version, env);
103
+ if (refreshed.installed) {
104
+ return {
105
+ installed: true,
106
+ bin: refreshed.bin,
107
+ upgraded: true,
108
+ note: `upgraded the archgen MCP server to v${version} at ${refreshed.bin}`,
109
+ };
110
+ }
111
+ // The upgrade failed — the OLD server still works, so keep it and say so.
112
+ return {
113
+ installed: true,
114
+ bin: onPath,
115
+ upgraded: false,
116
+ note: `could not upgrade the archgen MCP server to v${version} — the installed v${installed} stays`,
117
+ detail: refreshed.detail,
118
+ };
119
+ }
120
+
121
+ const fresh = installGlobal(version, env);
122
+ return {
123
+ installed: fresh.installed,
124
+ bin: fresh.bin,
125
+ upgraded: false,
126
+ note: fresh.installed
127
+ ? `installed the archgen MCP server globally at ${fresh.bin}`
128
+ : `could not install the archgen MCP server globally — the npx fallback will be used`,
129
+ detail: fresh.detail,
130
+ };
131
+ }
132
+
133
+ /** `npm install -g archgen-mcp@<version>`. */
134
+ function installGlobal(version, env) {
135
+ const spec = version === 'latest' ? MCP_PACKAGE : `${MCP_PACKAGE}@${version}`;
136
+ const inv = npmInvocation(['install', '-g', spec], env);
137
+ const r = spawnSync(inv.command, inv.args, {
138
+ encoding: 'utf8',
139
+ timeout: 300000,
140
+ env,
141
+ shell: inv.shell,
142
+ });
143
+ if (r.status !== 0) {
144
+ const detail = (r.stderr || r.stdout || '').trim().split('\n').pop();
145
+ return { installed: false, bin: null, detail: detail || `npm exited ${r.status}` };
146
+ }
147
+ // npm succeeded — but the install only COUNTS if the binary is now
148
+ // findable. A prefix that is not on PATH, or a package that shipped no
149
+ // bin, would otherwise be reported as a success.
150
+ const bin = whichOnPath(MCP_PACKAGE, env);
151
+ return bin
152
+ ? { installed: true, bin, detail: undefined }
153
+ : { installed: false, bin: null, detail: 'npm reported success but the binary is not on PATH' };
154
+ }
package/lib/init.js CHANGED
@@ -205,10 +205,17 @@ export function initProject(projectDir, packageRoot, opts = {}) {
205
205
  // harness AND ensure CodeGraph (its CLI, its MCP entry, the project
206
206
  // index). Non-fatal by design: an offline machine still gets the skill
207
207
  // installed.
208
+ //
209
+ // The wiring is GLOBAL by default: the server is one binary for every
210
+ // harness, and the harness configs live in the user's home. A project
211
+ // that carries its own `.mcp.json`, `.codex/` and `.gemini/` is a
212
+ // project that has to be re-wired in every clone. `--location project`
213
+ // opts into the per-project wiring.
208
214
  if (codegraphAllowed || mcpAllowed) {
209
215
  const r = bootstrapServers({
210
216
  projectDir: root,
211
- location: 'both',
217
+ packageRoot: packageRoot,
218
+ location: opts.location ?? 'global',
212
219
  log,
213
220
  env: opts.env,
214
221
  noCodegraph: !codegraphAllowed,
package/lib/mcp-server.js CHANGED
@@ -11,8 +11,9 @@
11
11
  // never thrown — the same posture bootstrapCodegraph has always had.
12
12
  import { existsSync, readFileSync } from 'node:fs';
13
13
  import { spawnSync } from 'node:child_process';
14
+ import { whichOnPath } from './spawn-npm.js';
14
15
  import { homedir } from 'node:os';
15
- import { basename, dirname, join, resolve } from 'node:path';
16
+ import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
16
17
 
17
18
  export const MCP_PACKAGE = 'archgen-mcp';
18
19
 
@@ -52,25 +53,16 @@ export function resolveMcpBinary(opts = {}) {
52
53
 
53
54
  /**
54
55
  * Locate an executable on PATH (cross-platform, no shell).
55
- * @param {string} bin
56
- * @param {Record<string, string|undefined>} env
57
- * @returns {string|null}
56
+ *
57
+ * ONE implementation, in spawn-npm.js — this module used to carry its own
58
+ * copy, and the two had drifted: the copy here skipped a path ending in '/',
59
+ * the one there did not, and neither checked that the path was a FILE. Two
60
+ * implementations of one rule is how the rules start disagreeing.
61
+ *
62
+ * Imported for this module's own use AND re-exported, so the callers that
63
+ * already import it from here keep working.
58
64
  */
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
- }
65
+ export { whichOnPath };
74
66
 
75
67
  /**
76
68
  * Quote one argument for cmd.exe.
@@ -155,6 +147,33 @@ export function mcpInvocation(command, args, platform = process.platform, env =
155
147
  return { command: quoteWindowsArg(cmd), args: args.map(quoteWindowsArg), shell: true };
156
148
  }
157
149
 
150
+ /**
151
+ * The project root an MCP call is made against.
152
+ *
153
+ * It MUST be an ABSOLUTE path.
154
+ *
155
+ * The child process resolves a relative root against ITS OWN working
156
+ * directory, so a root that was missing — or a property-name mismatch
157
+ * between the caller and the callee — did not fail. It silently wrote the
158
+ * harness configs, the CodeGraph index and the managed blocks into a
159
+ * directory named "undefined", next to the real project.
160
+ *
161
+ * The guard turns that class of mistake into an immediate, named failure.
162
+ *
163
+ * @param {unknown} projectRoot
164
+ * @param {string} fn the caller's name, for the message
165
+ * @returns {string} the absolute project root
166
+ */
167
+ export function requireProjectRoot(projectRoot, fn) {
168
+ if (typeof projectRoot !== 'string' || projectRoot.trim() === '') {
169
+ throw new Error(`${fn}: projectRoot is required and must be an absolute path`);
170
+ }
171
+ if (!isAbsolute(projectRoot)) {
172
+ throw new Error(`${fn}: projectRoot must be an absolute path (got '${projectRoot}')`);
173
+ }
174
+ return resolve(projectRoot);
175
+ }
176
+
158
177
  /**
159
178
  * Run one `archgen-mcp` subcommand and return its JSON output.
160
179
  *
@@ -163,7 +182,8 @@ export function mcpInvocation(command, args, platform = process.platform, env =
163
182
  * @returns {{ok: boolean, code: number|null, data: any, stdout: string, stderr: string, detail?: string}}
164
183
  */
165
184
  export function runMcp(args, opts = {}) {
166
- const { projectRoot = process.cwd(), timeout = 600000, env = process.env } = opts;
185
+ const { timeout = 600000, env = process.env } = opts;
186
+ const projectRoot = requireProjectRoot(opts.projectRoot ?? process.cwd(), 'runMcp');
167
187
  const resolved = resolveMcpBinary({ projectRoot, env });
168
188
  const inv = mcpInvocation(resolved.command, [...resolved.args, ...args], process.platform, env);
169
189
 
@@ -198,6 +218,25 @@ export function runMcp(args, opts = {}) {
198
218
  };
199
219
  }
200
220
 
221
+ /**
222
+ * The arguments for the server's bootstrap call.
223
+ *
224
+ * Exported so the project root in them can be asserted directly: the bug
225
+ * this guards against was a property-name mismatch that put the literal
226
+ * string "undefined" in this exact position.
227
+ *
228
+ * @param {string} projectRoot
229
+ * @param {{location?: 'project'|'global'|'both', approved?: boolean}} [opts]
230
+ * @returns {string[]}
231
+ */
232
+ export function bootstrapArgs(projectRoot, opts = {}) {
233
+ const { location = 'both', approved = true } = opts;
234
+ const root = requireProjectRoot(projectRoot, 'bootstrapArgs');
235
+ const args = ['bootstrap', '--project', root, '--location', location, '--json'];
236
+ if (approved) args.push('--yes');
237
+ return args;
238
+ }
239
+
201
240
  /**
202
241
  * Wire the archgen MCP server into the detected harnesses and ensure CodeGraph.
203
242
  *
@@ -205,11 +244,10 @@ export function runMcp(args, opts = {}) {
205
244
  * @returns {{ok: boolean, skipped?: string, detail?: string, report?: any}}
206
245
  */
207
246
  export function bootstrapMcp(opts) {
208
- const { projectRoot, location = 'both', approved = true, env = process.env } = opts;
209
- const args = ['bootstrap', '--project', projectRoot, '--location', location, '--json'];
210
- if (approved) args.push('--yes');
247
+ const { location = 'both', approved = true, env = process.env } = opts;
248
+ const args = bootstrapArgs(opts.projectRoot, { location, approved });
211
249
 
212
- const r = runMcp(args, { projectRoot, env });
250
+ const r = runMcp(args, { projectRoot: opts.projectRoot, env });
213
251
  if (r.detail) {
214
252
  return { ok: false, detail: `cannot run ${MCP_PACKAGE}: ${r.detail}` };
215
253
  }
@@ -225,7 +263,8 @@ export function bootstrapMcp(opts) {
225
263
  * @param {{projectRoot: string, env?: Record<string, string|undefined>}} opts
226
264
  */
227
265
  export function mcpDoctor(opts) {
228
- const { projectRoot, env = process.env } = opts;
266
+ const { env = process.env } = opts;
267
+ const projectRoot = requireProjectRoot(opts.projectRoot, 'mcpDoctor');
229
268
  const r = runMcp(['doctor', '--project', projectRoot, '--json'], { projectRoot, env, timeout: 60000 });
230
269
  return { ok: r.ok, report: r.data, detail: r.detail ?? (r.data ? undefined : r.stderr.trim().split('\n')[0]) };
231
270
  }
@@ -235,7 +274,8 @@ export function mcpDoctor(opts) {
235
274
  * @param {{projectRoot: string, env?: Record<string, string|undefined>}} opts
236
275
  */
237
276
  export function uninstallMcp(opts) {
238
- const { projectRoot, env = process.env } = opts;
277
+ const { env = process.env } = opts;
278
+ const projectRoot = requireProjectRoot(opts.projectRoot, 'uninstallMcp');
239
279
  const r = runMcp(['uninstall-mcp', '--project', projectRoot, '--yes', '--json'], { projectRoot, env, timeout: 60000 });
240
280
  return { ok: r.ok, report: r.data, detail: r.detail };
241
281
  }
@@ -5,9 +5,9 @@
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
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
- // touched, nothing becomes required.
8
+ // preserves them verbatim), the verifier never sees them as data, and every
9
+ // other YAML consumer ignores them by the same contract. Additive-only: no
10
+ // existing key is touched, nothing becomes required.
11
11
  //
12
12
  // # schema_version: 1
13
13
  // # generator: archgen v1.2.3 (version token omitted when unknowable)
package/lib/spawn-npm.js CHANGED
@@ -37,12 +37,22 @@ export function npmInvocation(npmArgs, env = process.env) {
37
37
  return { command: isWin ? 'npm.cmd' : 'npm', args: npmArgs, shell: isWin };
38
38
  }
39
39
 
40
- /** Cross-platform `which` (PATHEXT on Windows). */
41
- export function whichOnPath(bin) {
40
+ /**
41
+ * Cross-platform `which` (PATHEXT on Windows).
42
+ *
43
+ * The env is a PARAMETER, not an assumption: a caller may be searching a
44
+ * modified PATH (a test, a sandboxed HOME), and reading process.env behind
45
+ * its back would answer a different question than the one asked.
46
+ *
47
+ * @param {string} bin
48
+ * @param {Record<string, string|undefined>} [env]
49
+ * @returns {string|null}
50
+ */
51
+ export function whichOnPath(bin, env = process.env) {
42
52
  const exts = process.platform === 'win32'
43
- ? (process.env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';')
53
+ ? (env.PATHEXT ?? '.COM;.EXE;.BAT;.CMD').split(';')
44
54
  : [''];
45
- const dirs = (process.env.PATH ?? '').split(process.platform === 'win32' ? ';' : ':').filter(Boolean);
55
+ const dirs = (env.PATH ?? '').split(process.platform === 'win32' ? ';' : ':').filter(Boolean);
46
56
  for (const d of dirs) {
47
57
  for (const ext of exts) {
48
58
  const p = join(d, bin + ext);
@@ -2,10 +2,11 @@
2
2
  //
3
3
  // Removes exactly what archgen owns in a project: the claude symlink, the
4
4
  // managed blocks, the canonical store (ONLY when unmodified vs the vendor
5
- // skill — a customized store is kept with a warning), and the install
6
- // manifest. `.archgen/<slug>` feature folders and `.archgen/.backup/` are
7
- // always preserved. Parents are pruned only when left completely empty, and
8
- // never past the guarded .claude/.agents levels.
5
+ // skill — a customized store is kept with a warning), the install
6
+ // manifest, and the legacy extension runtime directory. `.archgen/<slug>`
7
+ // feature folders and `.archgen/.backup/` are always preserved. Parents are
8
+ // pruned only when left completely empty, and never past the guarded
9
+ // .claude/.agents levels.
9
10
 
10
11
  import { existsSync, lstatSync, readlinkSync, rmSync, unlinkSync } from 'node:fs';
11
12
  import { dirname, join, resolve } from 'node:path';
@@ -36,6 +37,7 @@ function relJoin(rel) {
36
37
  * @param {string} packageRoot dir containing this CLI package (vendor source)
37
38
  * @returns {{root: string, linkRemoved: boolean, strippedBlocks: string[],
38
39
  * storeRemoved: boolean, storeKept: boolean, manifestRemoved: boolean,
40
+ * runtimeRemoved: boolean,
39
41
  * mcpRemoved: string[], warnings: string[]}}
40
42
  */
41
43
  export function uninstallProject(projectDir, packageRoot) {
@@ -122,5 +124,23 @@ export function uninstallProject(projectDir, packageRoot) {
122
124
  const manifestRemoved = rmIfExists(join(root, relJoin(MANIFEST_REL)));
123
125
  if (manifestRemoved) pruneEmptyParents(root, dirname(MANIFEST_REL));
124
126
 
125
- return { root, linkRemoved, strippedBlocks, storeRemoved, storeKept, manifestRemoved, mcpRemoved, warnings };
127
+ // 6. The legacy extension runtime directory. The VS Code extension wrote a
128
+ // heartbeat and per-command acknowledgements under `.archgen/.runtime/`;
129
+ // it is dead state now that the extension and its bridge are gone, and it
130
+ // is archgen-owned, so the uninstall removes it. Feature folders and
131
+ // backups beside it are untouched.
132
+ const runtimeRemoved = rmIfExists(join(root, '.archgen', '.runtime'));
133
+ if (runtimeRemoved) pruneEmptyParents(root, '.archgen/.runtime');
134
+
135
+ return {
136
+ root,
137
+ linkRemoved,
138
+ strippedBlocks,
139
+ storeRemoved,
140
+ storeKept,
141
+ manifestRemoved,
142
+ runtimeRemoved,
143
+ mcpRemoved,
144
+ warnings,
145
+ };
126
146
  }
@@ -1,7 +1,7 @@
1
1
  // version-stamp.js — the `.archgen-version` stamp contract.
2
2
  //
3
- // WHY: the VS Code extension reads `<skillRoot>/.archgen-version` to tell an
4
- // outdated skill copy from a current one and warns non-blockingly (missing or
3
+ // WHY: the MCP server reads `<skillRoot>/.archgen-version` to tell an
4
+ // outdated skill copy from a current one and reports it (missing or
5
5
  // unparseable content means "unknown (legacy)" — a pre-stamp install). The
6
6
  // CLI is the only writer: every operation that lays down skill files (init,
7
7
  // global install in symlink AND --copy mode, update via its init/doctor
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "archgen-skill",
3
- "version": "0.0.7",
3
+ "version": "0.1.0",
4
4
  "description": "Architecture generation & autonomous task execution for coding agents - conversational requirements in, dependency-ordered tasks out, executed wave-by-wave with verifier gates. Works with Claude Code, OpenCode, Cursor, Codex, Gemini CLI and any agentskills.io harness.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -38,10 +38,10 @@
38
38
  "bugs": {
39
39
  "url": "https://github.com/akashmaurya99/archgen/issues"
40
40
  },
41
- "//": "vendor/skills/archgen is a full mirror of ../../skill synced by scripts/sync-vendor.mjs (run automatically via prepublishOnly). The extension ships separately as a VS Code vsix.",
41
+ "//": "vendor/skills/archgen is a full mirror of ../../skill synced by scripts/sync-vendor.mjs (run automatically via prepublishOnly).",
42
42
  "scripts": {
43
43
  "sync:config": "node scripts/sync-config.mjs",
44
- "sync:check": "node scripts/sync-config.mjs --check",
44
+ "sync:check": "node scripts/sync-config.mjs --check && node scripts/sync-vendor.mjs --check",
45
45
  "sync": "node scripts/sync-config.mjs && node scripts/sync-vendor.mjs",
46
46
  "prepublishOnly": "node scripts/sync-config.mjs && node scripts/sync-vendor.mjs",
47
47
  "test": "ARCHGEN_CODEGRAPH=0 node --test test/*.test.mjs"