archgen-skill 0.0.5 → 0.0.7
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 +20 -7
- package/bin/archgen.mjs +142 -15
- package/lib/block.js +52 -11
- package/lib/bootstrap.js +107 -0
- package/lib/config.js +3 -2
- package/lib/doctor.js +197 -77
- package/lib/init.js +77 -19
- package/lib/install.js +79 -7
- package/lib/mcp-server.js +241 -0
- 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 +99 -0
- package/lib/store.js +175 -28
- package/lib/uninstall-project.js +38 -7
- package/lib/version.js +20 -4
- package/package.json +3 -3
- package/vendor/skills/archgen/SKILL.md +363 -213
- package/vendor/skills/archgen/archgen.config.json +1 -1
- package/vendor/skills/archgen/references/code-standards.md +2 -2
- package/vendor/skills/archgen/assets/mcp-template.json +0 -42
- package/vendor/skills/archgen/assets/skill-registry.json +0 -24
- package/vendor/skills/archgen/references/artifact-templates.md +0 -406
- package/vendor/skills/archgen/references/interview.md +0 -275
- package/vendor/skills/archgen/references/mcp-and-skills.md +0 -117
- package/vendor/skills/archgen/references/orchestration.md +0 -292
- package/vendor/skills/archgen/references/platforms.md +0 -138
- package/vendor/skills/archgen/scripts/doc-index.mjs +0 -457
- package/vendor/skills/archgen/scripts/impact.mjs +0 -77
- package/vendor/skills/archgen/scripts/lib/graph.mjs +0 -222
- package/vendor/skills/archgen/scripts/lib/plan-coverage.mjs +0 -32
- package/vendor/skills/archgen/scripts/lib/scope.mjs +0 -65
- package/vendor/skills/archgen/scripts/lib/yaml.mjs +0 -492
- package/vendor/skills/archgen/scripts/next-tasks.mjs +0 -55
- package/vendor/skills/archgen/scripts/plan-graph.mjs +0 -329
- package/vendor/skills/archgen/scripts/set-status.mjs +0 -82
- package/vendor/skills/archgen/scripts/update-agents.mjs +0 -213
- package/vendor/skills/archgen/scripts/validate.mjs +0 -87
- package/vendor/skills/archgen/scripts/verify-plan.mjs +0 -70
package/lib/store.js
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
rmSync,
|
|
32
32
|
writeFileSync,
|
|
33
33
|
} from 'node:fs';
|
|
34
|
-
import { dirname, join, sep } from 'node:path';
|
|
34
|
+
import { basename, dirname, join, resolve, sep } from 'node:path';
|
|
35
35
|
import { loadConfig } from './config.js';
|
|
36
36
|
|
|
37
37
|
export const STORE_REL = '.agents/skills/archgen';
|
|
@@ -44,6 +44,18 @@ export const MANIFEST_REL = '.archgen/' + CONFIG_FILES.projectManifest;
|
|
|
44
44
|
export const BACKUP_ROOT_REL = '.archgen/' + CONFIG_FILES.backupDir;
|
|
45
45
|
export const VERSION_FILE = CONFIG_FILES.stamp;
|
|
46
46
|
|
|
47
|
+
/**
|
|
48
|
+
* Name of the sidecar that records WHICH paths a backup snapshot holds.
|
|
49
|
+
*
|
|
50
|
+
* A snapshot's directory layout alone does not say what was backed up: the
|
|
51
|
+
* tree `<ts>/.agents/skills/archgen` and the tree `<ts>/archgen` are the same
|
|
52
|
+
* shape at different depths, and restoring the snapshot's top-level directory
|
|
53
|
+
* would replace a whole `.agents/` tree — every sibling skill with it. The
|
|
54
|
+
* record names the exact paths, so a restore puts back exactly those and
|
|
55
|
+
* leaves siblings alone.
|
|
56
|
+
*/
|
|
57
|
+
export const BACKUP_PATHS_FILE = '.archgen-backup-paths.json';
|
|
58
|
+
|
|
47
59
|
const GUARDED_RELPATHS = new Set(['.claude', '.agents', '.claude/skills', '.agents/skills']);
|
|
48
60
|
|
|
49
61
|
/**
|
|
@@ -59,6 +71,55 @@ const GUARDED_RELPATHS = new Set(['.claude', '.agents', '.claude/skills', '.agen
|
|
|
59
71
|
*/
|
|
60
72
|
export const DEV_ONLY_RELPATHS = ['scripts/test'];
|
|
61
73
|
|
|
74
|
+
/**
|
|
75
|
+
* Subtrees that the CURRENT packaged skill no longer ships.
|
|
76
|
+
*
|
|
77
|
+
* The skill used to ship a `scripts/` tree (11 scripts + a lib/ folder). It
|
|
78
|
+
* does any more — every capability moved into the archgen MCP server. An
|
|
79
|
+
* existing install still carries that tree, and it is dead weight: nothing
|
|
80
|
+
* reads it, and it makes the store hash as DIVERGED from the packaged skill.
|
|
81
|
+
*
|
|
82
|
+
* Pruning it is a MIGRATION, not a repair: the tree is ours (it shipped in a
|
|
83
|
+
* previous version), so removing it is safe. It is never removed from a store
|
|
84
|
+
* whose packaged skill still ships the same path — the check is always
|
|
85
|
+
* "packaged skill has no such directory".
|
|
86
|
+
*/
|
|
87
|
+
export const OBSOLETE_RELPATHS = ['scripts'];
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The subtrees the packaged skill no longer ships that are still present in
|
|
91
|
+
* an installed tree (report-only — nothing is touched).
|
|
92
|
+
*
|
|
93
|
+
* @param {string} absRoot the installed skill tree
|
|
94
|
+
* @param {string} packagedRoot the packaged (vendor) skill tree
|
|
95
|
+
* @returns {string[]} the relpaths that are obsolete
|
|
96
|
+
*/
|
|
97
|
+
export function findObsolete(absRoot, packagedRoot) {
|
|
98
|
+
const found = [];
|
|
99
|
+
for (const rel of OBSOLETE_RELPATHS) {
|
|
100
|
+
if (existsSync(join(packagedRoot, ...rel.split('/')))) continue; // still shipped — not obsolete
|
|
101
|
+
if (!existsSync(join(absRoot, ...rel.split('/')))) continue;
|
|
102
|
+
found.push(rel);
|
|
103
|
+
}
|
|
104
|
+
return found;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Remove the subtrees the packaged skill no longer ships.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} absRoot the installed skill tree
|
|
111
|
+
* @param {string} packagedRoot the packaged (vendor) skill tree
|
|
112
|
+
* @returns {string[]} the relpaths actually removed
|
|
113
|
+
*/
|
|
114
|
+
export function pruneObsolete(absRoot, packagedRoot) {
|
|
115
|
+
const removed = [];
|
|
116
|
+
for (const rel of findObsolete(absRoot, packagedRoot)) {
|
|
117
|
+
rmSync(join(absRoot, ...rel.split('/')), { recursive: true, force: true });
|
|
118
|
+
removed.push(rel);
|
|
119
|
+
}
|
|
120
|
+
return removed;
|
|
121
|
+
}
|
|
122
|
+
|
|
62
123
|
/** True when relpath `r` is (or lives inside) a dev-only excluded subtree. */
|
|
63
124
|
function isDevOnlyRelPath(r) {
|
|
64
125
|
for (const prefix of DEV_ONLY_RELPATHS) {
|
|
@@ -84,30 +145,58 @@ export function assertNotSymlink(absPath) {
|
|
|
84
145
|
}
|
|
85
146
|
|
|
86
147
|
/**
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
148
|
+
* Resolve `p` to the real location of its nearest existing ancestor, keeping
|
|
149
|
+
* the not-yet-existing tail as lexical segments.
|
|
150
|
+
*
|
|
151
|
+
* `realpathSync` cannot answer for a path that does not exist yet, but the
|
|
152
|
+
* guard must still work before anything is created (init's very first run) —
|
|
153
|
+
* so the tail is appended to the resolved ancestor instead of being dropped.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} p absolute path
|
|
156
|
+
* @returns {string|null} resolved path, or null when nothing could be resolved
|
|
92
157
|
*/
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
let probe = absPath;
|
|
158
|
+
function realpathWithTail(p) {
|
|
159
|
+
const tail = [];
|
|
160
|
+
let probe = resolve(p);
|
|
97
161
|
for (;;) {
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
162
|
+
try {
|
|
163
|
+
const real = realpathSync(probe);
|
|
164
|
+
return tail.length ? join(real, ...tail) : real;
|
|
165
|
+
} catch (e) {
|
|
166
|
+
if (!e || e.code !== 'ENOENT') return null;
|
|
167
|
+
const parent = dirname(probe);
|
|
168
|
+
if (parent === probe) return null;
|
|
169
|
+
tail.unshift(basename(probe));
|
|
170
|
+
probe = parent;
|
|
107
171
|
}
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Realpath-escape guard: refuse when the real location of `absPath` lands
|
|
177
|
+
* outside `rootAbs`. Catches `.agents -> /elsewhere` style redirects that a
|
|
178
|
+
* final-component lstat cannot see.
|
|
179
|
+
*
|
|
180
|
+
* The LEXICAL check always applies — it is the only guard that can work when
|
|
181
|
+
* the root does not exist yet (init calls this before it created anything),
|
|
182
|
+
* and a lexical escape (`../../etc/passwd`) is an escape under any root.
|
|
183
|
+
*
|
|
184
|
+
* @param {string} rootAbs project/base root
|
|
185
|
+
* @param {string} absPath path that must stay inside the root
|
|
186
|
+
*/
|
|
187
|
+
export function assertWithinRoot(rootAbs, absPath) {
|
|
188
|
+
const rootLex = resolve(rootAbs);
|
|
189
|
+
const pathLex = resolve(absPath);
|
|
190
|
+
if (pathLex !== rootLex && !pathLex.startsWith(rootLex + sep)) {
|
|
191
|
+
throw new Error(refuseSymlinkMessage(absPath));
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
const rootReal = realpathWithTail(rootLex);
|
|
195
|
+
const pathReal = realpathWithTail(pathLex);
|
|
196
|
+
// Unresolvable (no existing ancestor at all): the lexical check stands.
|
|
197
|
+
if (!rootReal || !pathReal) return;
|
|
198
|
+
if (pathReal !== rootReal && !pathReal.startsWith(rootReal + sep)) {
|
|
199
|
+
throw new Error(refuseSymlinkMessage(absPath));
|
|
111
200
|
}
|
|
112
201
|
}
|
|
113
202
|
|
|
@@ -136,17 +225,38 @@ function byName(a, b) {
|
|
|
136
225
|
export function hashDir(dir, opts = {}) {
|
|
137
226
|
const ignore = new Set([VERSION_FILE, ...(opts.ignore ?? [])]);
|
|
138
227
|
const hash = createHash('sha256');
|
|
139
|
-
|
|
228
|
+
// A directory contributes its marker ONLY when its subtree contributes
|
|
229
|
+
// something. Without this, a directory holding nothing but excluded
|
|
230
|
+
// dev-only content hashed a 'd <rel>' marker on one side and nothing on the
|
|
231
|
+
// other — the two trees then read as divergent even though they are
|
|
232
|
+
// identical once the exclusions are applied.
|
|
233
|
+
//
|
|
234
|
+
// File contents are hashed as BYTES, never as strings: a lossy string
|
|
235
|
+
// round-trip would change the digest for any non-UTF-8 file.
|
|
236
|
+
const walk = (abs, rel) => {
|
|
140
237
|
const entries = readdirSync(abs, { withFileTypes: true }).sort((a, b) => byName(a.name, b.name));
|
|
238
|
+
const chunks = [];
|
|
141
239
|
for (const ent of entries) {
|
|
142
240
|
const r = rel ? rel + '/' + ent.name : ent.name;
|
|
143
241
|
if (ignore.has(r) || isDevOnlyRelPath(r)) continue;
|
|
144
242
|
const p = join(abs, ent.name);
|
|
145
|
-
if (ent.isSymbolicLink())
|
|
146
|
-
|
|
147
|
-
else
|
|
243
|
+
if (ent.isSymbolicLink()) {
|
|
244
|
+
chunks.push(Buffer.from('l ' + r + ' -> ' + readlinkSync(p) + '\n'));
|
|
245
|
+
} else if (ent.isDirectory()) {
|
|
246
|
+
const sub = walk(p, r);
|
|
247
|
+
if (sub.length > 0) {
|
|
248
|
+
chunks.push(Buffer.from('d ' + r + '\n'));
|
|
249
|
+
chunks.push(...sub);
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
chunks.push(Buffer.from('f ' + r + '\0'));
|
|
253
|
+
chunks.push(readFileSync(p));
|
|
254
|
+
chunks.push(Buffer.from('\n'));
|
|
255
|
+
}
|
|
148
256
|
}
|
|
149
|
-
|
|
257
|
+
return chunks;
|
|
258
|
+
};
|
|
259
|
+
for (const chunk of walk(dir, '')) hash.update(chunk);
|
|
150
260
|
return hash.digest('hex');
|
|
151
261
|
}
|
|
152
262
|
|
|
@@ -178,8 +288,11 @@ export function loadManifest(root) {
|
|
|
178
288
|
|
|
179
289
|
export function saveManifest(root, manifest) {
|
|
180
290
|
const p = join(root, ...MANIFEST_REL.split('/'));
|
|
181
|
-
|
|
291
|
+
// Guard BEFORE creating the parent: a symlinked `.archgen` would otherwise
|
|
292
|
+
// redirect the whole manifest write outside the project.
|
|
182
293
|
assertNotSymlink(p);
|
|
294
|
+
assertWithinRoot(root, p);
|
|
295
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
183
296
|
writeFileSync(p, JSON.stringify(manifest, null, 2) + '\n');
|
|
184
297
|
}
|
|
185
298
|
|
|
@@ -195,12 +308,45 @@ export function appendEntry(root, kind, relPath) {
|
|
|
195
308
|
saveManifest(root, m);
|
|
196
309
|
}
|
|
197
310
|
|
|
311
|
+
/**
|
|
312
|
+
* Read the path record of a backup snapshot.
|
|
313
|
+
* @param {string} snapshotDir absolute path of the snapshot directory
|
|
314
|
+
* @returns {string[]|null} the recorded relative paths, or null when the
|
|
315
|
+
* snapshot carries no record (written by an older CLI)
|
|
316
|
+
*/
|
|
317
|
+
export function readBackupPaths(snapshotDir) {
|
|
318
|
+
let raw;
|
|
319
|
+
try {
|
|
320
|
+
raw = JSON.parse(readFileSync(join(snapshotDir, BACKUP_PATHS_FILE), 'utf8'));
|
|
321
|
+
} catch {
|
|
322
|
+
return null;
|
|
323
|
+
}
|
|
324
|
+
if (!raw || !Array.isArray(raw.paths)) return null;
|
|
325
|
+
return raw.paths.filter((p) => typeof p === 'string' && p !== '');
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Add `relPaths` to a snapshot's path record (creating it when absent). */
|
|
329
|
+
function recordBackupPaths(snapshotDir, relPaths) {
|
|
330
|
+
const existing = readBackupPaths(snapshotDir) ?? [];
|
|
331
|
+
const merged = [...existing];
|
|
332
|
+
for (const p of relPaths) if (!merged.includes(p)) merged.push(p);
|
|
333
|
+
writeFileSync(
|
|
334
|
+
join(snapshotDir, BACKUP_PATHS_FILE),
|
|
335
|
+
JSON.stringify({ version: 1, paths: merged }, null, 2) + '\n',
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
198
339
|
/**
|
|
199
340
|
* Move an existing path into `<root>/<backupRootRel>/<ts>/<relPath>`, preserving
|
|
200
341
|
* the relative layout so backups restore unambiguously. Generalized core shared
|
|
201
342
|
* by the project vault (.archgen/.backup) and the global vault
|
|
202
343
|
* (<skills-dir>/.archgen-backups); `ts` can be injected so one operation lands
|
|
203
344
|
* every entry under a single timestamp.
|
|
345
|
+
*
|
|
346
|
+
* The exact relative path is RECORDED in the snapshot: a restore must put back
|
|
347
|
+
* that path, not the snapshot's top-level directory (which can hold sibling
|
|
348
|
+
* skills the snapshot never covered).
|
|
349
|
+
*
|
|
204
350
|
* @returns {string} backup location relative to `root`
|
|
205
351
|
*/
|
|
206
352
|
export function moveToBackupInto(root, relPath, backupRootRel, ts = new Date().toISOString().replace(/:/g, '-')) {
|
|
@@ -220,6 +366,7 @@ export function moveToBackupInto(root, relPath, backupRootRel, ts = new Date().t
|
|
|
220
366
|
cpSync(srcAbs, destAbs, { recursive: true });
|
|
221
367
|
rmSync(srcAbs, { recursive: true, force: true });
|
|
222
368
|
}
|
|
369
|
+
recordBackupPaths(join(root, ...backupRootRel.split('/'), ts), [relPath]);
|
|
223
370
|
return destRel;
|
|
224
371
|
}
|
|
225
372
|
|
package/lib/uninstall-project.js
CHANGED
|
@@ -15,11 +15,13 @@ import {
|
|
|
15
15
|
MANIFEST_REL,
|
|
16
16
|
STORE_REL,
|
|
17
17
|
VERSION_FILE,
|
|
18
|
+
assertWithinRoot,
|
|
18
19
|
hashDir,
|
|
19
20
|
pruneEmptyParents,
|
|
20
21
|
rmIfExists,
|
|
21
22
|
} from './store.js';
|
|
22
23
|
import { resolveSkillSource } from './init.js';
|
|
24
|
+
import { uninstallMcp } from './mcp-server.js';
|
|
23
25
|
|
|
24
26
|
function lstatSafe(p) {
|
|
25
27
|
try { return lstatSync(p); } catch { return null; }
|
|
@@ -33,7 +35,8 @@ function relJoin(rel) {
|
|
|
33
35
|
* @param {string} projectDir project root to clean
|
|
34
36
|
* @param {string} packageRoot dir containing this CLI package (vendor source)
|
|
35
37
|
* @returns {{root: string, linkRemoved: boolean, strippedBlocks: string[],
|
|
36
|
-
* storeRemoved: boolean, storeKept: boolean, manifestRemoved: boolean,
|
|
38
|
+
* storeRemoved: boolean, storeKept: boolean, manifestRemoved: boolean,
|
|
39
|
+
* mcpRemoved: string[], warnings: string[]}}
|
|
37
40
|
*/
|
|
38
41
|
export function uninstallProject(projectDir, packageRoot) {
|
|
39
42
|
const root = resolve(projectDir);
|
|
@@ -58,9 +61,18 @@ export function uninstallProject(projectDir, packageRoot) {
|
|
|
58
61
|
warnings.push('.claude/skills/archgen is a real directory - left untouched (remove manually if unwanted)');
|
|
59
62
|
}
|
|
60
63
|
|
|
61
|
-
// 2. Managed blocks — strip markers, keep all user content.
|
|
64
|
+
// 2. Managed blocks — strip markers, keep all user content. A symlinked
|
|
65
|
+
// managed file is REFUSED (never written through) and skipped, so the
|
|
66
|
+
// rest of the uninstall still runs and the project is left in a
|
|
67
|
+
// coherent state instead of half-uninstalled.
|
|
62
68
|
for (const name of ['AGENTS.md', 'CLAUDE.md']) {
|
|
63
|
-
|
|
69
|
+
const abs = join(root, name);
|
|
70
|
+
const fileSt = lstatSafe(abs);
|
|
71
|
+
if (fileSt && fileSt.isSymbolicLink()) {
|
|
72
|
+
warnings.push(`${name} is a symlink - left untouched (refusing to write through it)`);
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
if (stripManagedFile(abs).hadBlock) strippedBlocks.push(name);
|
|
64
76
|
}
|
|
65
77
|
|
|
66
78
|
// 3. Canonical store — remove ONLY if unmodified vs vendor.
|
|
@@ -78,18 +90,37 @@ export function uninstallProject(projectDir, packageRoot) {
|
|
|
78
90
|
let vendor = null;
|
|
79
91
|
try { vendor = hashDir(resolveSkillSource(packageRoot)); } catch { vendor = null; }
|
|
80
92
|
if (vendor !== null && mine === vendor) {
|
|
81
|
-
|
|
82
|
-
|
|
93
|
+
// Containment FIRST: a symlinked `.agents` would make this delete a
|
|
94
|
+
// tree OUTSIDE the project, so the guard runs before the removal.
|
|
95
|
+
try {
|
|
96
|
+
assertWithinRoot(root, storeAbs);
|
|
97
|
+
rmSync(storeAbs, { recursive: true, force: true });
|
|
98
|
+
storeRemoved = true;
|
|
99
|
+
} catch (e) {
|
|
100
|
+
storeKept = true;
|
|
101
|
+
warnings.push('canonical store kept at .agents/skills/archgen: ' + e.message);
|
|
102
|
+
}
|
|
83
103
|
} else {
|
|
84
104
|
storeKept = true;
|
|
85
105
|
warnings.push('canonical store kept at .agents/skills/archgen: modified vs vendor (or unverifiable) - review manually');
|
|
86
106
|
}
|
|
87
107
|
}
|
|
88
108
|
|
|
89
|
-
// 4.
|
|
109
|
+
// 4. MCP server entries — remove ONLY the archgen entries this install
|
|
110
|
+
// wrote (the MCP server keeps a manifest of exactly what it created, so
|
|
111
|
+
// unrelated servers in the same config are never touched).
|
|
112
|
+
let mcpRemoved = [];
|
|
113
|
+
const mcp = uninstallMcp({ projectRoot: root });
|
|
114
|
+
if (mcp.report?.removed?.length) {
|
|
115
|
+
mcpRemoved = mcp.report.removed;
|
|
116
|
+
} else if (mcp.detail) {
|
|
117
|
+
warnings.push(`archgen MCP entries not removed: ${mcp.detail}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 5. Manifest file — then prune .archgen only if nothing else remains
|
|
90
121
|
// (feature folders / backups keep it alive).
|
|
91
122
|
const manifestRemoved = rmIfExists(join(root, relJoin(MANIFEST_REL)));
|
|
92
123
|
if (manifestRemoved) pruneEmptyParents(root, dirname(MANIFEST_REL));
|
|
93
124
|
|
|
94
|
-
return { root, linkRemoved, strippedBlocks, storeRemoved, storeKept, manifestRemoved, warnings };
|
|
125
|
+
return { root, linkRemoved, strippedBlocks, storeRemoved, storeKept, manifestRemoved, mcpRemoved, warnings };
|
|
95
126
|
}
|
package/lib/version.js
CHANGED
|
@@ -1,15 +1,24 @@
|
|
|
1
1
|
// version.js — semver compare + npm registry lookup (zero deps).
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { npmInvocation } from './spawn-npm.js';
|
|
3
4
|
|
|
4
5
|
export function parseSemver(v) {
|
|
5
6
|
const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(String(v ?? '').trim());
|
|
6
7
|
return m ? { major: +m[1], minor: +m[2], patch: +m[3] } : null;
|
|
7
8
|
}
|
|
8
9
|
|
|
10
|
+
/**
|
|
11
|
+
* Compare two semver strings.
|
|
12
|
+
*
|
|
13
|
+
* @returns {number|null} -1 / 0 / 1, or null when either side is not a
|
|
14
|
+
* semver. An unparseable version is NOT "equal" to anything: reporting 0
|
|
15
|
+
* would make a registry tag like "beta" read as "already up to date" and
|
|
16
|
+
* silently skip the caller's refusal path.
|
|
17
|
+
*/
|
|
9
18
|
export function compareSemver(a, b) {
|
|
10
19
|
const x = parseSemver(a);
|
|
11
20
|
const y = parseSemver(b);
|
|
12
|
-
if (!x || !y) return
|
|
21
|
+
if (!x || !y) return null;
|
|
13
22
|
for (const k of ['major', 'minor', 'patch']) {
|
|
14
23
|
if (x[k] !== y[k]) return x[k] < y[k] ? -1 : 1;
|
|
15
24
|
}
|
|
@@ -17,7 +26,14 @@ export function compareSemver(a, b) {
|
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
export function fetchLatestVersion(pkg) {
|
|
20
|
-
|
|
21
|
-
if (
|
|
22
|
-
|
|
29
|
+
// Registry-verified constant in every caller; refuse anything else outright.
|
|
30
|
+
if (!/^[a-z0-9@][a-z0-9._/-]*$/i.test(String(pkg ?? ''))) return null;
|
|
31
|
+
try {
|
|
32
|
+
const inv = npmInvocation(['view', String(pkg), 'version']);
|
|
33
|
+
const r = spawnSync(inv.command, inv.args, { encoding: 'utf8', timeout: 15000, shell: inv.shell });
|
|
34
|
+
if (r.status !== 0 || !r.stdout) return null;
|
|
35
|
+
return r.stdout.trim().split(/\r?\n/).pop() || null;
|
|
36
|
+
} catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
23
39
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archgen-skill",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.7",
|
|
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",
|
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
"README.md"
|
|
15
15
|
],
|
|
16
16
|
"engines": {
|
|
17
|
-
"node": ">=
|
|
17
|
+
"node": ">=20"
|
|
18
18
|
},
|
|
19
19
|
"keywords": [
|
|
20
20
|
"ai",
|
|
@@ -44,6 +44,6 @@
|
|
|
44
44
|
"sync:check": "node scripts/sync-config.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
|
-
"test": "node --test test/*.test.mjs"
|
|
47
|
+
"test": "ARCHGEN_CODEGRAPH=0 node --test test/*.test.mjs"
|
|
48
48
|
}
|
|
49
49
|
}
|