archgen-skill 0.0.6 → 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 +8 -10
- package/bin/archgen.mjs +135 -13
- package/lib/block.js +51 -10
- package/lib/bootstrap.js +33 -7
- package/lib/config.js +3 -2
- package/lib/doctor.js +159 -82
- package/lib/global-server.js +154 -0
- package/lib/init.js +53 -17
- package/lib/install.js +60 -7
- package/lib/mcp-server.js +149 -27
- package/lib/migrations/001-stamp-provenance.mjs +15 -8
- package/lib/migrations/index.mjs +19 -8
- package/lib/restore.js +52 -16
- package/lib/spawn-npm.js +63 -8
- package/lib/store.js +100 -23
- package/lib/uninstall-project.js +47 -9
- package/lib/version-stamp.js +2 -2
- package/lib/version.js +9 -1
- package/package.json +3 -3
- package/vendor/skills/archgen/SKILL.md +156 -375
- package/vendor/skills/archgen/archgen.config.json +1 -1
- package/vendor/skills/archgen/references/code-standards.md +28 -44
package/README.md
CHANGED
|
@@ -42,6 +42,13 @@ copies are backed up to `.archgen/.backup/<timestamp>/` before refresh, and lega
|
|
|
42
42
|
dual-copy layouts are migrated to symlinks automatically. Verify anytime with
|
|
43
43
|
`archgen-skill doctor`.
|
|
44
44
|
|
|
45
|
+
Every destructive step is behind a rail: the project root is asserted **before**
|
|
46
|
+
anything is written or deleted, a symlinked target is never written through, the
|
|
47
|
+
store swap is atomic (the old store is moved aside, not deleted, and restored if
|
|
48
|
+
the swap fails), and a restore puts back exactly the paths the snapshot recorded
|
|
49
|
+
— never a sibling skill. `archgen-skill uninstall <dir>` removes that project;
|
|
50
|
+
the global uninstall happens only when no project is named.
|
|
51
|
+
|
|
45
52
|
**Or install globally** into every detected harness:
|
|
46
53
|
|
|
47
54
|
```bash
|
|
@@ -80,7 +87,7 @@ by git.
|
|
|
80
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 |
|
|
81
88
|
| `npx archgen-skill install [--copy]` | Install into global harness skill dirs (symlinks by default, real copies with `--copy`) |
|
|
82
89
|
| `npx archgen-skill uninstall` | Remove globally-installed copies (manifest-recorded, safe removal) |
|
|
83
|
-
| `npx archgen-skill uninstall
|
|
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 |
|
|
84
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) |
|
|
85
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 |
|
|
86
93
|
| `npx archgen-skill restore` | List backup snapshots; `--snapshot <ts>` restores one (current state is backed up first) |
|
|
@@ -128,15 +135,6 @@ Claude Code, OpenCode, Cursor, Codex, Gemini CLI, Antigravity — and any
|
|
|
128
135
|
agentskills.io-compatible harness. Both global install and project-local `init`
|
|
129
136
|
are supported for all of them.
|
|
130
137
|
|
|
131
|
-
## VS Code extension (optional)
|
|
132
|
-
|
|
133
|
-
A visual layer that renders `.archgen/` as a live task DAG, your
|
|
134
|
-
code-dependency graph, and rendered docs — with a ▶ button that hands any task to
|
|
135
|
-
your agent. Install it from the VS Code Marketplace or from a `.vsix` on
|
|
136
|
-
[GitHub Releases](https://github.com/akashmaurya99/archgen/releases). It never
|
|
137
|
-
edits your plan, your tasks, or your code — only its own runtime state under
|
|
138
|
-
`.archgen/.runtime/`, so uninstalling it loses nothing.
|
|
139
|
-
|
|
140
138
|
## Documentation
|
|
141
139
|
|
|
142
140
|
| Doc | Contents |
|
package/bin/archgen.mjs
CHANGED
|
@@ -22,7 +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
|
+
import { npmInvocation, whichOnPath, globalBinPath } from '../lib/spawn-npm.js';
|
|
26
26
|
import { spawnSync } from 'node:child_process';
|
|
27
27
|
import { readFileSync } from 'node:fs';
|
|
28
28
|
|
|
@@ -35,7 +35,13 @@ function cliVersion() {
|
|
|
35
35
|
function runInitAndDoctor(dir) {
|
|
36
36
|
const r = initProject(dir, PACKAGE_ROOT);
|
|
37
37
|
console.log(' = store refreshed at ' + r.storePath);
|
|
38
|
-
doctorProject(dir, PACKAGE_ROOT, {});
|
|
38
|
+
const d = doctorProject(dir, PACKAGE_ROOT, {});
|
|
39
|
+
// A failed doctor must fail the update too: the project was NOT brought to
|
|
40
|
+
// the new version.
|
|
41
|
+
if (d.failures > 0) {
|
|
42
|
+
console.error(' ! doctor reported ' + d.failures + ' failure(s)');
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
}
|
|
39
45
|
}
|
|
40
46
|
|
|
41
47
|
const HELP = `archgen — architecture generation & autonomous task execution for coding agents
|
|
@@ -45,20 +51,29 @@ Usage:
|
|
|
45
51
|
store at .agents/skills/archgen, symlink
|
|
46
52
|
.claude/skills/archgen -> ../../.agents/skills/archgen,
|
|
47
53
|
and generate AGENTS.md / CLAUDE.md pointer blocks.
|
|
54
|
+
[--project <dir>] the project directory (same as the positional
|
|
55
|
+
form; give it once, not both)
|
|
48
56
|
[--force] proceed automatically; divergent copies are
|
|
49
57
|
backed up to .archgen/.backup/ (never destroyed)
|
|
50
58
|
[--no-codegraph] skip the CodeGraph bootstrap
|
|
51
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)
|
|
52
63
|
npx archgen-skill doctor [dir] Health-check a project install: store integrity,
|
|
53
64
|
version stamp, claude link, managed blocks,
|
|
54
65
|
manifest. Repairs safe issues in place.
|
|
66
|
+
[--project <dir>] the project directory (same as the positional form)
|
|
55
67
|
[--check] report only - change nothing
|
|
56
68
|
npx archgen-skill install Install into global harness skill dirs
|
|
57
69
|
[--copy] real copies instead of symlinks
|
|
58
70
|
[--uninstall] remove manifest-recorded entries
|
|
59
71
|
[--no-codegraph] skip the CodeGraph bootstrap
|
|
60
72
|
[--no-mcp] skip wiring the archgen MCP server
|
|
61
|
-
npx archgen-skill uninstall
|
|
73
|
+
npx archgen-skill uninstall [dir] Remove archgen from a project (the same
|
|
74
|
+
as the positional form); without a directory
|
|
75
|
+
the globally-installed copies are removed
|
|
76
|
+
(manifest-based)
|
|
62
77
|
--project [dir] remove archgen from a project: claude link,
|
|
63
78
|
managed blocks, canonical store (kept when
|
|
64
79
|
customized) and manifest; .archgen feature
|
|
@@ -89,8 +104,63 @@ function flag(name) {
|
|
|
89
104
|
return true;
|
|
90
105
|
}
|
|
91
106
|
|
|
92
|
-
|
|
93
|
-
|
|
107
|
+
/**
|
|
108
|
+
* Consume an option that takes a value: `--name <value>` or `--name=<value>`.
|
|
109
|
+
*
|
|
110
|
+
* Returns null when the option is absent, '' when it is present with no
|
|
111
|
+
* value ("use the default"), and the value otherwise.
|
|
112
|
+
*/
|
|
113
|
+
function takeValue(name) {
|
|
114
|
+
const eq = rest.findIndex((a) => a.startsWith(name + '='));
|
|
115
|
+
if (eq !== -1) {
|
|
116
|
+
const v = rest[eq].slice(name.length + 1);
|
|
117
|
+
rest.splice(eq, 1);
|
|
118
|
+
return v;
|
|
119
|
+
}
|
|
120
|
+
const i = rest.indexOf(name);
|
|
121
|
+
if (i === -1) return null;
|
|
122
|
+
rest.splice(i, 1);
|
|
123
|
+
const v = rest[i];
|
|
124
|
+
if (v === undefined || v.startsWith('-')) return '';
|
|
125
|
+
rest.splice(i, 1);
|
|
126
|
+
return v;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* The project directory for a command.
|
|
131
|
+
*
|
|
132
|
+
* Accepts a POSITIONAL dir (`init myproj`) and `--project <dir>` alike. The
|
|
133
|
+
* same flag name is used by `uninstall` and `restore`, and the MCP server's
|
|
134
|
+
* own commands take `--project` too — so a user who typed one form must
|
|
135
|
+
* never get a directory literally named "--project" created in their cwd.
|
|
136
|
+
*
|
|
137
|
+
* Extra positional arguments are REFUSED: silently ignoring the second one
|
|
138
|
+
* would install into a directory the user did not name.
|
|
139
|
+
*/
|
|
140
|
+
function targetDir() {
|
|
141
|
+
const explicit = takeValue('--project');
|
|
142
|
+
const positionals = rest.filter((a) => !a.startsWith('-'));
|
|
143
|
+
if (positionals.length > 1) {
|
|
144
|
+
throw new Error(`unexpected argument: ${positionals[1]} — give the project directory once`);
|
|
145
|
+
}
|
|
146
|
+
const positional = positionals[0];
|
|
147
|
+
const explicitDir = explicit === null ? null : (explicit === '' ? process.cwd() : resolve(explicit));
|
|
148
|
+
if (explicitDir !== null && positional !== undefined && explicitDir !== resolve(positional)) {
|
|
149
|
+
throw new Error('give the project directory once: either positionally or with --project');
|
|
150
|
+
}
|
|
151
|
+
return explicitDir ?? (positional !== undefined ? resolve(positional) : process.cwd());
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Refuse an option we do not understand.
|
|
156
|
+
*
|
|
157
|
+
* An unknown flag is a TYPO. It is never a directory name — creating a
|
|
158
|
+
* directory called "--no-mcp" because the flag was misspelled is exactly the
|
|
159
|
+
* kind of silent mess this check exists to prevent.
|
|
160
|
+
*/
|
|
161
|
+
function assertNoUnknownFlags() {
|
|
162
|
+
const unknown = rest.find((a) => a.startsWith('-'));
|
|
163
|
+
if (unknown) throw new Error(`unknown option: ${unknown}`);
|
|
94
164
|
}
|
|
95
165
|
|
|
96
166
|
try {
|
|
@@ -99,7 +169,17 @@ try {
|
|
|
99
169
|
flag('--force');
|
|
100
170
|
const noCodegraph = flag('--no-codegraph');
|
|
101
171
|
const noMcp = flag('--no-mcp');
|
|
102
|
-
|
|
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';
|
|
176
|
+
const dir = targetDir();
|
|
177
|
+
assertNoUnknownFlags();
|
|
178
|
+
const r = initProject(dir, PACKAGE_ROOT, {
|
|
179
|
+
codegraph: !noCodegraph,
|
|
180
|
+
mcp: !noMcp,
|
|
181
|
+
location,
|
|
182
|
+
});
|
|
103
183
|
console.log('archgen: project ready (single-store layout)');
|
|
104
184
|
console.log(' + store ' + r.storePath);
|
|
105
185
|
if (r.claudeLink === 'created') console.log(' + link .claude/skills/archgen -> ../../.agents/skills/archgen');
|
|
@@ -117,7 +197,9 @@ try {
|
|
|
117
197
|
}
|
|
118
198
|
case 'doctor': {
|
|
119
199
|
const check = flag('--check');
|
|
120
|
-
const
|
|
200
|
+
const dir = targetDir();
|
|
201
|
+
assertNoUnknownFlags();
|
|
202
|
+
const r = doctorProject(dir, PACKAGE_ROOT, { check });
|
|
121
203
|
console.log('archgen doctor — ' + r.root + (check ? ' (check mode: no changes made)' : ''));
|
|
122
204
|
for (const c of r.checks) console.log(` [${c.status.toLowerCase().padEnd(9)}] ${c.msg}`);
|
|
123
205
|
const t = r.tally;
|
|
@@ -129,7 +211,11 @@ try {
|
|
|
129
211
|
const copy = flag('--copy');
|
|
130
212
|
const noCodegraph = flag('--no-codegraph');
|
|
131
213
|
const noMcp = flag('--no-mcp');
|
|
132
|
-
|
|
214
|
+
const uninstallMode = flag('--uninstall');
|
|
215
|
+
assertNoUnknownFlags();
|
|
216
|
+
const stray = rest.find((a) => !a.startsWith('-'));
|
|
217
|
+
if (stray) throw new Error(`unexpected argument: ${stray} — install takes no directory`);
|
|
218
|
+
if (uninstallMode) { uninstall(); break; }
|
|
133
219
|
const r = installGlobal({ copy, codegraph: !noCodegraph, mcp: !noMcp });
|
|
134
220
|
console.log('archgen: global harness install (mode: ' + (copy ? 'copy' : 'symlink') + ')');
|
|
135
221
|
for (const [status, mode, path] of r.rows) console.log(` ${status.padEnd(7)} ${mode.padEnd(5)} ${path}`);
|
|
@@ -138,14 +224,32 @@ try {
|
|
|
138
224
|
break;
|
|
139
225
|
}
|
|
140
226
|
case 'uninstall': {
|
|
141
|
-
|
|
142
|
-
|
|
227
|
+
// `--project` may carry a directory (`uninstall --project myproj`) or
|
|
228
|
+
// stand alone (`uninstall --project` → this directory). A POSITIONAL
|
|
229
|
+
// directory (`uninstall myproj`) is a project uninstall as well: the
|
|
230
|
+
// GLOBAL uninstall happens only when no project was named at all.
|
|
231
|
+
const explicit = takeValue('--project');
|
|
232
|
+
const positionals = rest.filter((a) => !a.startsWith('-'));
|
|
233
|
+
assertNoUnknownFlags();
|
|
234
|
+
if (positionals.length > 1) {
|
|
235
|
+
throw new Error(`unexpected argument: ${positionals[1]} — give the project directory once`);
|
|
236
|
+
}
|
|
237
|
+
const positional = positionals[0];
|
|
238
|
+
const projectTarget = explicit !== null
|
|
239
|
+
? (explicit === '' ? process.cwd() : resolve(explicit))
|
|
240
|
+
: (positional !== undefined ? resolve(positional) : null);
|
|
241
|
+
if (explicit !== null && positional !== undefined && projectTarget !== resolve(positional)) {
|
|
242
|
+
throw new Error('give the project directory once: either positionally or with --project');
|
|
243
|
+
}
|
|
244
|
+
if (projectTarget !== null) {
|
|
245
|
+
const r = uninstallProject(projectTarget, PACKAGE_ROOT);
|
|
143
246
|
console.log('archgen: project uninstall — ' + r.root);
|
|
144
247
|
if (r.linkRemoved) console.log(' - .claude/skills/archgen (link)');
|
|
145
248
|
if (r.strippedBlocks.length) console.log(' - managed blocks stripped from ' + r.strippedBlocks.join(', '));
|
|
146
249
|
if (r.storeRemoved) console.log(' - .agents/skills/archgen (unmodified store)');
|
|
147
250
|
if (r.storeKept) console.log(' = .agents/skills/archgen KEPT (customized)');
|
|
148
251
|
if (r.manifestRemoved) console.log(' - .archgen/.install-manifest.json');
|
|
252
|
+
if (r.runtimeRemoved) console.log(' - .archgen/.runtime (legacy extension state)');
|
|
149
253
|
for (const m of r.mcpRemoved ?? []) console.log(` - ${m} (archgen MCP entry)`);
|
|
150
254
|
for (const w of r.warnings) console.log(' note: ' + w);
|
|
151
255
|
console.log(' .archgen feature folders and backups are preserved.');
|
|
@@ -166,7 +270,8 @@ try {
|
|
|
166
270
|
break;
|
|
167
271
|
}
|
|
168
272
|
case 'update': {
|
|
169
|
-
const dir =
|
|
273
|
+
const dir = targetDir();
|
|
274
|
+
assertNoUnknownFlags();
|
|
170
275
|
const current = cliVersion();
|
|
171
276
|
console.log('archgen update — running v' + current + ', checking npm registry…');
|
|
172
277
|
const latest = fetchLatestVersion('archgen-skill');
|
|
@@ -175,6 +280,14 @@ try {
|
|
|
175
280
|
break;
|
|
176
281
|
}
|
|
177
282
|
const cmp = compareSemver(latest, current);
|
|
283
|
+
if (cmp === null) {
|
|
284
|
+
// The registry answered with something that is not a semver (e.g. a
|
|
285
|
+
// "beta" dist-tag). It is NOT "already the latest version" — refuse
|
|
286
|
+
// instead, and never interpolate it into a command line.
|
|
287
|
+
console.error(' ! registry returned a non-semver version string; refusing: ' + JSON.stringify(latest));
|
|
288
|
+
process.exitCode = 1;
|
|
289
|
+
break;
|
|
290
|
+
}
|
|
178
291
|
if (cmp === 0) {
|
|
179
292
|
console.log(' = already on the latest published version (' + latest + ').');
|
|
180
293
|
console.log(' Refreshing this project against it anyway…');
|
|
@@ -204,7 +317,9 @@ try {
|
|
|
204
317
|
break;
|
|
205
318
|
}
|
|
206
319
|
// The running process is the OLD code; re-exec the freshly installed binary.
|
|
207
|
-
|
|
320
|
+
// The PATH search would find the FIRST match — often the OLD global
|
|
321
|
+
// install — so the binary npm actually wrote is preferred.
|
|
322
|
+
const freshBin = globalBinPath('archgen-skill') ?? whichOnPath('archgen-skill');
|
|
208
323
|
if (!freshBin) {
|
|
209
324
|
console.log('\nUpgraded to v' + latest + '. Now finish per project with:\n npx archgen-skill@latest init');
|
|
210
325
|
break;
|
|
@@ -218,7 +333,13 @@ try {
|
|
|
218
333
|
process.exitCode = upInit.status ?? 1;
|
|
219
334
|
break;
|
|
220
335
|
}
|
|
221
|
-
spawnSync(freshBin, ['doctor', dir], { stdio: 'inherit' });
|
|
336
|
+
const upDoctor = spawnSync(freshBin, ['doctor', dir], { stdio: 'inherit' });
|
|
337
|
+
if (upDoctor.status !== 0) {
|
|
338
|
+
// A failed doctor must fail the update too — the project was NOT
|
|
339
|
+
// brought to the new version.
|
|
340
|
+
console.error(' ! doctor failed during update (see message above)');
|
|
341
|
+
process.exitCode = upDoctor.status ?? 1;
|
|
342
|
+
}
|
|
222
343
|
break;
|
|
223
344
|
}
|
|
224
345
|
case '--version': case '-v': {
|
|
@@ -243,5 +364,6 @@ function uninstall() {
|
|
|
243
364
|
const r = uninstallGlobal();
|
|
244
365
|
if (r.noop) { console.log('archgen: nothing to uninstall (no manifest found).'); return; }
|
|
245
366
|
console.log(`archgen: removed ${r.removed} entr${r.removed === 1 ? 'y' : 'ies'}` + (r.failed ? `, ${r.failed} FAILED` : ''));
|
|
367
|
+
for (const b of r.backups ?? []) console.log(' ~ backup ' + b + ' (edited copy saved before removal)');
|
|
246
368
|
if (r.failed > 0) process.exitCode = 1;
|
|
247
369
|
}
|
package/lib/block.js
CHANGED
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
// place on the next init/doctor pass and detectBlockVersion() lets doctor
|
|
17
17
|
// report stale formats precisely.
|
|
18
18
|
|
|
19
|
-
import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
20
20
|
import { loadConfig } from './config.js';
|
|
21
21
|
import { assertNotSymlink } from './store.js';
|
|
22
22
|
|
|
@@ -171,11 +171,14 @@ export function upsertBlock(existing, block, eol = '\n') {
|
|
|
171
171
|
if (s === -1 || e === -1) {
|
|
172
172
|
throw new Error('found only one archgen marker - fix or remove it manually');
|
|
173
173
|
}
|
|
174
|
+
const prefix = trimmed.slice(0, s);
|
|
174
175
|
const tail = stripCompleteBlocks(trimmed.slice(e + END.length));
|
|
175
|
-
|
|
176
|
+
// An orphan marker ANYWHERE (before the first block as well as after it)
|
|
177
|
+
// would survive the rewrite and leave the file broken — refuse instead.
|
|
178
|
+
if (prefix.includes(START) || prefix.includes(END) || tail.includes(START) || tail.includes(END)) {
|
|
176
179
|
throw new Error('found only one archgen marker - fix or remove it manually');
|
|
177
180
|
}
|
|
178
|
-
return
|
|
181
|
+
return prefix + fresh + tail;
|
|
179
182
|
}
|
|
180
183
|
|
|
181
184
|
const startIdx = trimmed.indexOf(START);
|
|
@@ -209,13 +212,37 @@ function splitBom(raw) {
|
|
|
209
212
|
return { bom, body: bom ? raw.slice(1) : raw };
|
|
210
213
|
}
|
|
211
214
|
|
|
212
|
-
/**
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
215
|
+
/**
|
|
216
|
+
* Write via sibling temp file + rename so readers never observe partial content.
|
|
217
|
+
* @param {string} absPath target file
|
|
218
|
+
* @param {string} data contents
|
|
219
|
+
*/
|
|
220
|
+
export function atomicWrite(absPath, data) {
|
|
221
|
+
let tmp = absPath + '.tmp-' + process.pid + '-' + Date.now();
|
|
222
|
+
for (let attempt = 0; ; attempt++) {
|
|
223
|
+
try {
|
|
224
|
+
// 'wx' (O_CREAT|O_EXCL): if the tmp name already exists — e.g. an attacker
|
|
225
|
+
// planted a symlink there to redirect the write — fail instead of following.
|
|
226
|
+
writeFileSync(tmp, data, { flag: 'wx' });
|
|
227
|
+
break;
|
|
228
|
+
} catch (e) {
|
|
229
|
+
// A collision (two writes inside the same millisecond) is not a failure:
|
|
230
|
+
// retry with a fresh name instead of failing the whole write.
|
|
231
|
+
if (e && e.code === 'EEXIST' && attempt < 10) {
|
|
232
|
+
tmp = absPath + '.tmp-' + process.pid + '-' + Date.now() + '-' + (attempt + 1);
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
throw e;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
renameSync(tmp, absPath);
|
|
240
|
+
} catch (e) {
|
|
241
|
+
// Never leave the temp file behind: a failed rename must not litter the
|
|
242
|
+
// project with half-written siblings.
|
|
243
|
+
try { unlinkSync(tmp); } catch { /* original intact; leftover tmp is harmless */ }
|
|
244
|
+
throw e;
|
|
245
|
+
}
|
|
219
246
|
}
|
|
220
247
|
|
|
221
248
|
/**
|
|
@@ -250,6 +277,20 @@ export function importsAgents(absPath) {
|
|
|
250
277
|
return body.split(/\r?\n/).some((l) => l.trim() === '@AGENTS.md');
|
|
251
278
|
}
|
|
252
279
|
|
|
280
|
+
/**
|
|
281
|
+
* True when the file carries a COMPLETE managed block (both markers).
|
|
282
|
+
*
|
|
283
|
+
* A file with a hand-written `@AGENTS.md` import has none — that import is
|
|
284
|
+
* the user's, not ours, and must survive untouched. A file WITH a managed
|
|
285
|
+
* block is ours: its provenance stamp is refreshed on every init, exactly
|
|
286
|
+
* like the AGENTS.md block.
|
|
287
|
+
*/
|
|
288
|
+
export function hasManagedBlock(absPath) {
|
|
289
|
+
let raw = '';
|
|
290
|
+
try { raw = readFileSync(absPath, 'utf8'); } catch { return false; }
|
|
291
|
+
return raw.includes(START) && raw.includes(END);
|
|
292
|
+
}
|
|
293
|
+
|
|
253
294
|
/** Strip the managed block from a file on disk, preserving BOM. */
|
|
254
295
|
export function stripManagedFile(absPath) {
|
|
255
296
|
assertNotSymlink(absPath);
|
package/lib/bootstrap.js
CHANGED
|
@@ -3,37 +3,43 @@
|
|
|
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,
|
|
7
|
-
// (agent -> MCP stdio, CLI -> `archgen-mcp bootstrap
|
|
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
|
|
11
11
|
// 2. ensures CodeGraph: its CLI, its MCP entry, and the project index
|
|
12
12
|
//
|
|
13
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
|
|
14
|
+
// server must still leave a working skill install. Failures are REPORTED to
|
|
15
|
+
// the caller (ok:false + detail) and rendered as a warning by it — never
|
|
16
|
+
// thrown, and never swallowed.
|
|
16
17
|
import { bootstrapMcp, resolveMcpBinary } from './mcp-server.js';
|
|
18
|
+
import { ensureGlobalServer, serverVersion } from './global-server.js';
|
|
17
19
|
|
|
18
20
|
/**
|
|
19
21
|
* @param {{
|
|
20
22
|
* projectDir: string,
|
|
23
|
+
* packageRoot?: string,
|
|
21
24
|
* location?: 'project'|'global'|'both',
|
|
22
25
|
* log?: (line: string) => void,
|
|
23
26
|
* env?: Record<string, string|undefined>,
|
|
24
27
|
* noCodegraph?: boolean,
|
|
25
28
|
* noMcp?: boolean,
|
|
29
|
+
* global?: boolean,
|
|
26
30
|
* }} opts
|
|
27
31
|
* @returns {{ok: boolean, skipped?: string, detail?: string, report?: any}}
|
|
28
32
|
*/
|
|
29
33
|
export function bootstrapServers(opts) {
|
|
30
34
|
const {
|
|
31
35
|
projectDir,
|
|
36
|
+
packageRoot = null,
|
|
32
37
|
location = 'both',
|
|
33
38
|
log = () => {},
|
|
34
39
|
env = process.env,
|
|
35
40
|
noCodegraph = false,
|
|
36
41
|
noMcp = false,
|
|
42
|
+
global = true,
|
|
37
43
|
} = opts;
|
|
38
44
|
|
|
39
45
|
// ARCHGEN_CODEGRAPH=0 skips the bootstrap (no spawn, no network, no writes)
|
|
@@ -45,8 +51,25 @@ export function bootstrapServers(opts) {
|
|
|
45
51
|
return { ok: true, skipped: '--no-mcp and --no-codegraph passed' };
|
|
46
52
|
}
|
|
47
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
|
+
|
|
48
70
|
const resolved = resolveMcpBinary({ projectRoot: projectDir, env });
|
|
49
71
|
log(` + mcp server: ${resolved.bin ?? 'npx -y archgen-mcp'} (${resolved.source})`);
|
|
72
|
+
if (serverNote) log(` + mcp server: ${serverNote}`);
|
|
50
73
|
|
|
51
74
|
if (noMcp) {
|
|
52
75
|
log(' ! archgen MCP wiring skipped (--no-mcp)');
|
|
@@ -56,15 +79,18 @@ export function bootstrapServers(opts) {
|
|
|
56
79
|
}
|
|
57
80
|
|
|
58
81
|
const r = bootstrapMcp({
|
|
59
|
-
projectDir,
|
|
82
|
+
projectRoot: projectDir,
|
|
60
83
|
location,
|
|
61
84
|
approved: true,
|
|
62
85
|
env,
|
|
63
86
|
});
|
|
64
87
|
|
|
65
88
|
if (!r.report) {
|
|
89
|
+
// The bootstrap did NOT run: report it as a failure so the caller tells
|
|
90
|
+
// the user. The skill install itself still succeeded — this is a warning,
|
|
91
|
+
// not a fatal error — but it must never be swallowed silently.
|
|
66
92
|
return {
|
|
67
|
-
ok:
|
|
93
|
+
ok: false,
|
|
68
94
|
detail: `server bootstrap incomplete: ${r.detail ?? 'unknown error'}`,
|
|
69
95
|
};
|
|
70
96
|
}
|
|
@@ -96,7 +122,7 @@ export function bootstrapServers(opts) {
|
|
|
96
122
|
return {
|
|
97
123
|
ok: false,
|
|
98
124
|
report,
|
|
99
|
-
detail: 'one or more server bootstrap steps failed',
|
|
125
|
+
detail: r.detail ? `server bootstrap incomplete: ${r.detail}` : 'one or more server bootstrap steps failed',
|
|
100
126
|
};
|
|
101
127
|
}
|
|
102
128
|
return { ok: true, report };
|
package/lib/config.js
CHANGED
|
@@ -2,8 +2,9 @@
|
|
|
2
2
|
//
|
|
3
3
|
// WHY: the version, managed-block marker strings, and well-known filenames
|
|
4
4
|
// were hardcoded independently in block.js, store.js, install.js, install.sh,
|
|
5
|
-
// and SKILL.md frontmatter — and drifted (SKILL.md advertised
|
|
6
|
-
// CLI shipped
|
|
5
|
+
// and SKILL.md frontmatter — and drifted (SKILL.md advertised a version the
|
|
6
|
+
// CLI had never shipped). The canonical file lives at
|
|
7
|
+
// <repo-root>/archgen.config.json;
|
|
7
8
|
// a byte-identical derived copy ships inside the published npm package at
|
|
8
9
|
// vendor/skills/archgen/archgen.config.json (kept in sync by
|
|
9
10
|
// scripts/sync-config.mjs + scripts/sync-vendor.mjs).
|