archgen-skill 0.0.1 → 0.0.3
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 +156 -0
- package/bin/archgen.mjs +124 -13
- package/lib/block.js +109 -5
- package/lib/doctor.js +186 -0
- package/lib/init.js +129 -27
- package/lib/store.js +146 -0
- package/lib/uninstall-project.js +92 -0
- package/lib/version.js +23 -0
- package/package.json +5 -3
- package/vendor/skills/archgen/SKILL.md +14 -3
- package/vendor/skills/archgen/references/platforms.md +17 -0
- package/vendor/skills/archgen/scripts/update-agents.mjs +202 -0
package/lib/doctor.js
ADDED
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// doctor.js — `archgen doctor [dir] [--check]` health-check + safe auto-repair.
|
|
2
|
+
//
|
|
3
|
+
// Verifies a project install: canonical store integrity, version stamp,
|
|
4
|
+
// claude link resolution, managed blocks (exactly once each), and manifest
|
|
5
|
+
// entry resolution. Safe issues are repaired in place unless --check is given
|
|
6
|
+
// (report-only). Exit non-zero only for unrepairable failures.
|
|
7
|
+
|
|
8
|
+
import { existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { dirname, join, resolve } from 'node:path';
|
|
10
|
+
import {
|
|
11
|
+
END,
|
|
12
|
+
FEATURES_END,
|
|
13
|
+
FEATURES_START,
|
|
14
|
+
START,
|
|
15
|
+
renderBlock,
|
|
16
|
+
upsertFeaturesRegistry,
|
|
17
|
+
upsertManagedFile,
|
|
18
|
+
} from './block.js';
|
|
19
|
+
import { CLAUDE_LINK_REL, MANIFEST_REL, STORE_REL, VERSION_FILE, cliVersion, loadManifest, saveManifest } from './store.js';
|
|
20
|
+
|
|
21
|
+
function lstatSafe(p) {
|
|
22
|
+
try { return lstatSync(p); } catch { return null; }
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function countOccurrences(haystack, needle) {
|
|
26
|
+
let n = 0;
|
|
27
|
+
for (let i = haystack.indexOf(needle); i !== -1; i = haystack.indexOf(needle, i + needle.length)) n++;
|
|
28
|
+
return n;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function relJoin(rel) {
|
|
32
|
+
return join(...rel.split('/'));
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @param {string} projectDir project root to inspect
|
|
37
|
+
* @param {string} packageRoot dir containing this CLI package
|
|
38
|
+
* @param {{check?: boolean}} opts check=true → report-only, no mutations
|
|
39
|
+
* @returns {{root: string, checks: {status: 'OK'|'FIXED'|'WOULD-FIX'|'WARN'|'FAIL', msg: string}[],
|
|
40
|
+
* tally: Record<string, number>, failures: number}}
|
|
41
|
+
*/
|
|
42
|
+
export function doctorProject(projectDir, packageRoot, opts = {}) {
|
|
43
|
+
const root = resolve(projectDir);
|
|
44
|
+
const checkOnly = opts.check === true;
|
|
45
|
+
const version = cliVersion(packageRoot);
|
|
46
|
+
const checks = [];
|
|
47
|
+
const add = (status, msg) => checks.push({ status, msg });
|
|
48
|
+
|
|
49
|
+
const storeAbs = join(root, relJoin(STORE_REL));
|
|
50
|
+
const claudeAbs = join(root, relJoin(CLAUDE_LINK_REL));
|
|
51
|
+
const agentsAbs = join(root, 'AGENTS.md');
|
|
52
|
+
const claudeMdAbs = join(root, 'CLAUDE.md');
|
|
53
|
+
|
|
54
|
+
// 1. Canonical store validity.
|
|
55
|
+
if (existsSync(join(storeAbs, 'SKILL.md'))) {
|
|
56
|
+
add('OK', 'store .agents/skills/archgen (SKILL.md present)');
|
|
57
|
+
} else {
|
|
58
|
+
add('FAIL', 'store .agents/skills/archgen missing or incomplete - run `archgen init`');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. Version stamp currency.
|
|
62
|
+
const stampAbs = join(storeAbs, VERSION_FILE);
|
|
63
|
+
let stamp = null;
|
|
64
|
+
try { stamp = readFileSync(stampAbs, 'utf8').trim(); } catch { /* absent */ }
|
|
65
|
+
if (!existsSync(storeAbs)) {
|
|
66
|
+
// already reported above; skip stamp handling without touching anything
|
|
67
|
+
} else if (stamp === version) {
|
|
68
|
+
add('OK', `version stamp ${version} (current)`);
|
|
69
|
+
} else if (checkOnly) {
|
|
70
|
+
add('WOULD-FIX', `version stamp ${stamp || 'missing'} is stale -> ${version}`);
|
|
71
|
+
} else {
|
|
72
|
+
mkdirSync(dirname(stampAbs), { recursive: true });
|
|
73
|
+
writeFileSync(stampAbs, version + '\n');
|
|
74
|
+
add('FIXED', `version stamp updated ${stamp || 'missing'} -> ${version}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// 3. Claude link resolution.
|
|
78
|
+
const repairLink = () => {
|
|
79
|
+
if (checkOnly) {
|
|
80
|
+
add('WOULD-FIX', 'claude link missing/dangling -> ../../.agents/skills/archgen');
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
try {
|
|
84
|
+
try { unlinkSync(claudeAbs); } catch { /* absent */ }
|
|
85
|
+
mkdirSync(dirname(claudeAbs), { recursive: true });
|
|
86
|
+
symlinkSync('../../.agents/skills/archgen', claudeAbs);
|
|
87
|
+
add('FIXED', 'claude link recreated -> ../../.agents/skills/archgen');
|
|
88
|
+
} catch (e) {
|
|
89
|
+
add('FAIL', 'cannot recreate claude link: ' + e.message);
|
|
90
|
+
}
|
|
91
|
+
};
|
|
92
|
+
const st = lstatSafe(claudeAbs);
|
|
93
|
+
if (st && st.isSymbolicLink()) {
|
|
94
|
+
const target = resolve(dirname(claudeAbs), readlinkSync(claudeAbs));
|
|
95
|
+
if (target === storeAbs && existsSync(claudeAbs)) add('OK', 'claude link resolves to the canonical store');
|
|
96
|
+
else if (existsSync(claudeAbs)) add('WARN', 'claude link is foreign (points elsewhere) - left untouched');
|
|
97
|
+
else repairLink();
|
|
98
|
+
} else if (st && st.isDirectory()) {
|
|
99
|
+
add('WARN', '.claude/skills/archgen is a real directory (not managed) - inspect manually');
|
|
100
|
+
} else if (st) {
|
|
101
|
+
add('WARN', '.claude/skills/archgen is a regular file - left untouched');
|
|
102
|
+
} else {
|
|
103
|
+
repairLink();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// 4. AGENTS.md — managed block exactly once, features registry inside.
|
|
107
|
+
if (!existsSync(agentsAbs)) {
|
|
108
|
+
if (checkOnly) add('WOULD-FIX', 'AGENTS.md missing -> created with managed block');
|
|
109
|
+
else {
|
|
110
|
+
writeFileSync(agentsAbs, renderBlock(STORE_REL) + '\n');
|
|
111
|
+
add('FIXED', 'AGENTS.md created with managed block');
|
|
112
|
+
}
|
|
113
|
+
} else {
|
|
114
|
+
const raw = readFileSync(agentsAbs, 'utf8');
|
|
115
|
+
const starts = countOccurrences(raw, START);
|
|
116
|
+
const ends = countOccurrences(raw, END);
|
|
117
|
+
if (starts === 0) {
|
|
118
|
+
if (checkOnly) add('WOULD-FIX', 'AGENTS.md has no archgen block -> added');
|
|
119
|
+
else {
|
|
120
|
+
upsertManagedFile(agentsAbs, renderBlock(STORE_REL).split('\n'));
|
|
121
|
+
add('FIXED', 'AGENTS.md managed block added');
|
|
122
|
+
}
|
|
123
|
+
} else if (starts > 1 || ends > 1) {
|
|
124
|
+
add('FAIL', `AGENTS.md has ${starts} start / ${ends} end markers - fix manually`);
|
|
125
|
+
} else if (ends === 0 || raw.indexOf(START) > raw.indexOf(END)) {
|
|
126
|
+
add('FAIL', 'AGENTS.md has broken archgen markers - fix manually');
|
|
127
|
+
} else {
|
|
128
|
+
const fsCount = countOccurrences(raw, FEATURES_START);
|
|
129
|
+
const feCount = countOccurrences(raw, FEATURES_END);
|
|
130
|
+
if (fsCount === 1 && feCount === 1) add('OK', 'AGENTS.md block + features registry present');
|
|
131
|
+
else if (fsCount === 0 && feCount === 0) {
|
|
132
|
+
if (checkOnly) add('WOULD-FIX', 'AGENTS.md missing features registry -> inserted');
|
|
133
|
+
else {
|
|
134
|
+
upsertFeaturesRegistry(agentsAbs, []);
|
|
135
|
+
add('FIXED', 'AGENTS.md features registry inserted');
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
add('FAIL', 'AGENTS.md has broken archgen:features markers - fix manually');
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 5. CLAUDE.md — @AGENTS.md bridge present exactly once.
|
|
144
|
+
if (!existsSync(claudeMdAbs)) {
|
|
145
|
+
if (checkOnly) add('WOULD-FIX', 'CLAUDE.md missing -> created with @AGENTS.md bridge');
|
|
146
|
+
else {
|
|
147
|
+
upsertManagedFile(claudeMdAbs, [START, '@AGENTS.md', END]);
|
|
148
|
+
add('FIXED', 'CLAUDE.md created with @AGENTS.md bridge');
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
const raw = readFileSync(claudeMdAbs, 'utf8');
|
|
152
|
+
const body = raw.charCodeAt(0) === 0xfeff ? raw.slice(1) : raw;
|
|
153
|
+
const imports = body.split(/\r?\n/).some((l) => l.trim() === '@AGENTS.md');
|
|
154
|
+
const starts = countOccurrences(raw, START);
|
|
155
|
+
const ends = countOccurrences(raw, END);
|
|
156
|
+
if (imports && starts <= 1 && ends <= 1) add('OK', 'CLAUDE.md bridges to AGENTS.md');
|
|
157
|
+
else if (starts > 1 || ends > 1) add('FAIL', 'CLAUDE.md has multiple archgen blocks - fix manually');
|
|
158
|
+
else if (checkOnly) add('WOULD-FIX', 'CLAUDE.md missing @AGENTS.md bridge -> written');
|
|
159
|
+
else {
|
|
160
|
+
upsertManagedFile(claudeMdAbs, [START, '@AGENTS.md', END]);
|
|
161
|
+
add('FIXED', 'CLAUDE.md @AGENTS.md bridge written');
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 6. Manifest entries resolve.
|
|
166
|
+
const manifest = loadManifest(root);
|
|
167
|
+
if (!manifest) {
|
|
168
|
+
if (existsSync(storeAbs)) add('WARN', `no install manifest at ${MANIFEST_REL} (re-run init to regenerate)`);
|
|
169
|
+
else add('OK', 'no manifest (nothing installed)');
|
|
170
|
+
} else {
|
|
171
|
+
const resolves = (e) => e && typeof e.path === 'string' && existsSync(join(root, ...e.path.split('/')));
|
|
172
|
+
const stale = manifest.entries.filter((e) => !resolves(e));
|
|
173
|
+
if (stale.length === 0) {
|
|
174
|
+
add('OK', `manifest: all ${manifest.entries.length} entr${manifest.entries.length === 1 ? 'y' : 'ies'} resolve`);
|
|
175
|
+
} else if (checkOnly) {
|
|
176
|
+
add('WOULD-FIX', `manifest: ${stale.length} stale entr${stale.length === 1 ? 'y' : 'ies'} -> pruned`);
|
|
177
|
+
} else {
|
|
178
|
+
saveManifest(root, { ...manifest, entries: manifest.entries.filter(resolves) });
|
|
179
|
+
add('FIXED', `manifest: pruned ${stale.length} stale entr${stale.length === 1 ? 'y' : 'ies'}`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const tally = { OK: 0, FIXED: 0, 'WOULD-FIX': 0, WARN: 0, FAIL: 0 };
|
|
184
|
+
for (const c of checks) tally[c.status]++;
|
|
185
|
+
return { root, checks, tally, failures: tally.FAIL };
|
|
186
|
+
}
|
package/lib/init.js
CHANGED
|
@@ -1,13 +1,27 @@
|
|
|
1
|
-
// init.js —
|
|
1
|
+
// init.js — single-store project installer + AGENTS.md/CLAUDE.md context files.
|
|
2
2
|
//
|
|
3
|
-
// `archgen init` makes a repository SELF-CONTAINED
|
|
4
|
-
// .agents/skills/archgen (committed to git,
|
|
5
|
-
// .claude/skills/archgen
|
|
6
|
-
//
|
|
3
|
+
// `archgen init` makes a repository SELF-CONTAINED with ONE real copy of the
|
|
4
|
+
// skill: the canonical store at .agents/skills/archgen (committed to git,
|
|
5
|
+
// team-shared). .claude/skills/archgen becomes a RELATIVE symlink to the
|
|
6
|
+
// store — skipped gracefully where symlinks are unavailable, never a second
|
|
7
|
+
// copy. AGENTS.md carries a managed pointer block with an embedded features
|
|
8
|
+
// registry; CLAUDE.md carries a one-line `@AGENTS.md` bridge. Every created
|
|
9
|
+
// artifact is recorded in .archgen/.install-manifest.json.
|
|
10
|
+
//
|
|
11
|
+
// Verified-surgical safety semantics:
|
|
12
|
+
// - existing paths are fingerprinted (stable-order sha256 over relpaths +
|
|
13
|
+
// contents) BEFORE any mutation;
|
|
14
|
+
// - an identical store is refreshed in place; a divergent one is moved to
|
|
15
|
+
// .archgen/.backup/<timestamp>/ FIRST, then reinstalled fresh;
|
|
16
|
+
// - a real-directory claude copy identical to the vendor skill is migrated
|
|
17
|
+
// to a symlink; a customized one is left untouched with a warning;
|
|
18
|
+
// - installs are atomic: built in a temp sibling dir, then renamed.
|
|
7
19
|
|
|
8
|
-
import { cpSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
20
|
+
import { cpSync, existsSync, lstatSync, mkdirSync, readFileSync, readlinkSync, renameSync, rmSync, symlinkSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { randomBytes } from 'node:crypto';
|
|
22
|
+
import { dirname, join, resolve } from 'node:path';
|
|
23
|
+
import { importsAgents, renderBlock, START, END, upsertManagedFile } from './block.js';
|
|
24
|
+
import { CLAUDE_LINK_REL, STORE_REL, VERSION_FILE, appendEntry, cliVersion, hashDir, moveToBackup } from './store.js';
|
|
11
25
|
|
|
12
26
|
const CONTEXT_FILES = ['AGENTS.md', 'CLAUDE.md'];
|
|
13
27
|
|
|
@@ -21,36 +35,124 @@ export function resolveSkillSource(packageRoot) {
|
|
|
21
35
|
throw new Error('bundled archgen skill not found (expected vendor/skills/archgen)');
|
|
22
36
|
}
|
|
23
37
|
|
|
38
|
+
function lstatSafe(p) {
|
|
39
|
+
try { return lstatSync(p); } catch { return null; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function relJoin(rel) {
|
|
43
|
+
return join(...rel.split('/'));
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Create the claude adapter: `<project>/.claude/skills/archgen` as a RELATIVE
|
|
48
|
+
* symlink into the canonical store. Never copies. Migration/keep semantics:
|
|
49
|
+
* - ours already -> 'existing'
|
|
50
|
+
* - real dir identical to vendor -> replaced with symlink ('migrated')
|
|
51
|
+
* - real dir modified / foreign live symlink / stray file -> hands off + warn
|
|
52
|
+
* - dangling symlink -> recreated
|
|
53
|
+
* - symlink creation fails (privileges/FS) -> skipped + warning
|
|
54
|
+
*/
|
|
55
|
+
function installClaudeAdapter(root, source, warnings) {
|
|
56
|
+
const claudeAbs = join(root, relJoin(CLAUDE_LINK_REL));
|
|
57
|
+
const storeAbs = join(root, relJoin(STORE_REL));
|
|
58
|
+
const st = lstatSafe(claudeAbs);
|
|
59
|
+
let migrating = false;
|
|
60
|
+
|
|
61
|
+
if (st && st.isSymbolicLink()) {
|
|
62
|
+
const resolvedTarget = resolve(dirname(claudeAbs), readlinkSync(claudeAbs));
|
|
63
|
+
if (resolvedTarget === storeAbs && existsSync(claudeAbs)) return 'existing';
|
|
64
|
+
if (existsSync(claudeAbs)) {
|
|
65
|
+
warnings.push('.claude/skills/archgen is a foreign symlink - left untouched');
|
|
66
|
+
return 'kept-divergent';
|
|
67
|
+
}
|
|
68
|
+
unlinkSync(claudeAbs); // dangling: recreate ours below
|
|
69
|
+
} else if (st && st.isDirectory()) {
|
|
70
|
+
if (hashDir(claudeAbs) === hashDir(source)) {
|
|
71
|
+
rmSync(claudeAbs, { recursive: true }); // legacy dual copy -> migrate
|
|
72
|
+
migrating = true;
|
|
73
|
+
} else {
|
|
74
|
+
warnings.push('user-customized claude copy kept at .claude/skills/archgen (not managed)');
|
|
75
|
+
return 'kept-divergent';
|
|
76
|
+
}
|
|
77
|
+
} else if (st) {
|
|
78
|
+
warnings.push('.claude/skills/archgen exists and is not a directory - left untouched');
|
|
79
|
+
return 'kept-divergent';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
try {
|
|
83
|
+
mkdirSync(dirname(claudeAbs), { recursive: true });
|
|
84
|
+
symlinkSync('../../.agents/skills/archgen', claudeAbs);
|
|
85
|
+
appendEntry(root, 'link', CLAUDE_LINK_REL);
|
|
86
|
+
return migrating ? 'migrated' : 'created';
|
|
87
|
+
} catch (e) {
|
|
88
|
+
warnings.push('claude adapter skipped: symlink unavailable (' + e.message + ')');
|
|
89
|
+
return 'skipped';
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
24
93
|
/**
|
|
25
|
-
* Initialize a project:
|
|
94
|
+
* Initialize a project: install the single canonical skill store, wire the
|
|
95
|
+
* claude adapter, and write context pointers.
|
|
26
96
|
* @param {string} projectDir target repo root
|
|
27
97
|
* @param {string} packageRoot dir containing this CLI package
|
|
28
|
-
* @
|
|
98
|
+
* @param {{force?: boolean}} opts --force proceeds automatically; divergent
|
|
99
|
+
* copies are always auto-backed-up (never refused, never destroyed)
|
|
100
|
+
* @returns {{storePath: string, claudeLink: 'created'|'existing'|'migrated'|'skipped'|'kept-divergent',
|
|
101
|
+
* backups: string[], contextFiles: string[], createdContextFiles: string[], warnings: string[]}}
|
|
29
102
|
*/
|
|
30
|
-
export function initProject(projectDir, packageRoot) {
|
|
103
|
+
export function initProject(projectDir, packageRoot, opts = {}) {
|
|
104
|
+
// --force is accepted for explicitness: this installer never refuses and
|
|
105
|
+
// never destroys data — divergent copies are backed up automatically, so
|
|
106
|
+
// both plain and --force runs take the same safe path.
|
|
107
|
+
const force = opts.force === true;
|
|
108
|
+
void force;
|
|
109
|
+
|
|
31
110
|
const root = resolve(projectDir);
|
|
32
111
|
const source = resolveSkillSource(packageRoot);
|
|
112
|
+
const version = cliVersion(packageRoot);
|
|
113
|
+
const warnings = [];
|
|
114
|
+
const backups = [];
|
|
115
|
+
|
|
116
|
+
const storeAbs = join(root, relJoin(STORE_REL));
|
|
117
|
+
|
|
118
|
+
// 1. Canonical store — fingerprint before mutating (ownership check).
|
|
119
|
+
const prev = lstatSafe(storeAbs);
|
|
120
|
+
if (prev) {
|
|
121
|
+
const identical = prev.isDirectory() && !prev.isSymbolicLink()
|
|
122
|
+
&& hashDir(storeAbs, { ignore: [VERSION_FILE] }) === hashDir(source);
|
|
123
|
+
if (!identical) backups.push(moveToBackup(root, STORE_REL));
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Atomic install: build in a temp sibling dir, then rename into place.
|
|
127
|
+
const skillsParent = dirname(storeAbs);
|
|
128
|
+
mkdirSync(skillsParent, { recursive: true });
|
|
129
|
+
const tmp = join(skillsParent, '.archgen-tmp-' + randomBytes(6).toString('hex'));
|
|
130
|
+
cpSync(source, tmp, { recursive: true });
|
|
131
|
+
writeFileSync(join(tmp, VERSION_FILE), version + '\n');
|
|
132
|
+
rmSync(storeAbs, { recursive: true, force: true });
|
|
133
|
+
renameSync(tmp, storeAbs);
|
|
134
|
+
appendEntry(root, 'dir', STORE_REL);
|
|
33
135
|
|
|
34
|
-
//
|
|
35
|
-
const
|
|
36
|
-
const claudeSkills = join(root, '.claude', 'skills', 'archgen');
|
|
37
|
-
mkdirSync(agentsSkills, { recursive: true });
|
|
38
|
-
cpSync(source, agentsSkills, { recursive: true });
|
|
39
|
-
mkdirSync(join(root, '.claude', 'skills'), { recursive: true });
|
|
40
|
-
cpSync(agentsSkills, claudeSkills, { recursive: true });
|
|
136
|
+
// 2. Claude adapter — relative symlink, never a second copy.
|
|
137
|
+
const claudeLink = installClaudeAdapter(root, source, warnings);
|
|
41
138
|
|
|
42
|
-
//
|
|
43
|
-
const block = renderBlock('.agents/skills/archgen');
|
|
139
|
+
// 3. Context files — managed blocks, user content preserved.
|
|
44
140
|
const contextFiles = [];
|
|
45
|
-
const
|
|
141
|
+
const createdContextFiles = [];
|
|
46
142
|
for (const name of CONTEXT_FILES) {
|
|
47
|
-
const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
143
|
+
const abs = join(root, name);
|
|
144
|
+
if (name === 'CLAUDE.md' && importsAgents(abs)) {
|
|
145
|
+
warnings.push('CLAUDE.md already imports @AGENTS.md - skipped (already-imports)');
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
const existed = upsertManagedFile(
|
|
149
|
+
abs,
|
|
150
|
+
name === 'AGENTS.md' ? renderBlock(STORE_REL).split('\n') : [START, '@AGENTS.md', END],
|
|
151
|
+
);
|
|
51
152
|
contextFiles.push(name);
|
|
52
|
-
if (!existed)
|
|
153
|
+
if (!existed) createdContextFiles.push(name);
|
|
154
|
+
appendEntry(root, 'block', name);
|
|
53
155
|
}
|
|
54
156
|
|
|
55
|
-
return {
|
|
157
|
+
return { storePath: storeAbs, claudeLink, backups, contextFiles, createdContextFiles, warnings };
|
|
56
158
|
}
|
package/lib/store.js
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
// store.js — shared primitives for the single-store project layout.
|
|
2
|
+
//
|
|
3
|
+
// Layout (all paths relative to the project root):
|
|
4
|
+
// .agents/skills/archgen canonical store (the ONLY real copy)
|
|
5
|
+
// .agents/skills/archgen/.archgen-version CLI version stamp (rewritten every init)
|
|
6
|
+
// .claude/skills/archgen RELATIVE symlink -> ../../.agents/skills/archgen
|
|
7
|
+
// .archgen/.install-manifest.json manifest of everything archgen created
|
|
8
|
+
// .archgen/.backup/<timestamp>/ divergent copies moved aside before replace
|
|
9
|
+
//
|
|
10
|
+
// Safety rails:
|
|
11
|
+
// - hashDir() fingerprints a tree (stable-order walk of relpaths + contents,
|
|
12
|
+
// sha256) so mutations only ever happen after verifying ownership/divergence;
|
|
13
|
+
// - GUARDED_RELPATHS (.claude, .agents, .claude/skills, .agents/skills) are
|
|
14
|
+
// never deleted: parent pruning stops before touching those levels.
|
|
15
|
+
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
17
|
+
import {
|
|
18
|
+
cpSync,
|
|
19
|
+
existsSync,
|
|
20
|
+
lstatSync,
|
|
21
|
+
mkdirSync,
|
|
22
|
+
readFileSync,
|
|
23
|
+
readdirSync,
|
|
24
|
+
readlinkSync,
|
|
25
|
+
renameSync,
|
|
26
|
+
rmSync,
|
|
27
|
+
writeFileSync,
|
|
28
|
+
} from 'node:fs';
|
|
29
|
+
import { dirname, join } from 'node:path';
|
|
30
|
+
|
|
31
|
+
export const STORE_REL = '.agents/skills/archgen';
|
|
32
|
+
export const CLAUDE_LINK_REL = '.claude/skills/archgen';
|
|
33
|
+
export const MANIFEST_REL = '.archgen/.install-manifest.json';
|
|
34
|
+
export const BACKUP_ROOT_REL = '.archgen/.backup';
|
|
35
|
+
export const VERSION_FILE = '.archgen-version';
|
|
36
|
+
|
|
37
|
+
const GUARDED_RELPATHS = new Set(['.claude', '.agents', '.claude/skills', '.agents/skills']);
|
|
38
|
+
|
|
39
|
+
/** Version string of this CLI package (used for the in-store stamp). */
|
|
40
|
+
export function cliVersion(packageRoot) {
|
|
41
|
+
return JSON.parse(readFileSync(join(packageRoot, 'package.json'), 'utf8')).version;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function byName(a, b) {
|
|
45
|
+
return a < b ? -1 : a > b ? 1 : 0;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Stable-order directory fingerprint: sha256 over sorted relpaths plus file
|
|
50
|
+
* contents (symlinks hashed via their targets). Deterministic across machines.
|
|
51
|
+
* @param {string} dir absolute directory to hash
|
|
52
|
+
* @param {{ignore?: string[]}} opts relpaths to skip (e.g. our version stamp)
|
|
53
|
+
*/
|
|
54
|
+
export function hashDir(dir, opts = {}) {
|
|
55
|
+
const ignore = opts.ignore ?? [];
|
|
56
|
+
const hash = createHash('sha256');
|
|
57
|
+
(function walk(abs, rel) {
|
|
58
|
+
const entries = readdirSync(abs, { withFileTypes: true }).sort((a, b) => byName(a.name, b.name));
|
|
59
|
+
for (const ent of entries) {
|
|
60
|
+
const r = rel ? rel + '/' + ent.name : ent.name;
|
|
61
|
+
if (ignore.includes(r)) continue;
|
|
62
|
+
const p = join(abs, ent.name);
|
|
63
|
+
if (ent.isSymbolicLink()) hash.update('l ' + r + ' -> ' + readlinkSync(p) + '\n');
|
|
64
|
+
else if (ent.isDirectory()) { hash.update('d ' + r + '\n'); walk(p, r); }
|
|
65
|
+
else { hash.update('f ' + r + '\0'); hash.update(readFileSync(p)); hash.update('\n'); }
|
|
66
|
+
}
|
|
67
|
+
})(dir, '');
|
|
68
|
+
return hash.digest('hex');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Load `.archgen/.install-manifest.json`; null when absent/corrupt. */
|
|
72
|
+
export function loadManifest(root) {
|
|
73
|
+
const p = join(root, ...MANIFEST_REL.split('/'));
|
|
74
|
+
if (!existsSync(p)) return null;
|
|
75
|
+
try {
|
|
76
|
+
const m = JSON.parse(readFileSync(p, 'utf8'));
|
|
77
|
+
return m && typeof m === 'object' ? m : null;
|
|
78
|
+
} catch {
|
|
79
|
+
return null;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function saveManifest(root, manifest) {
|
|
84
|
+
const p = join(root, ...MANIFEST_REL.split('/'));
|
|
85
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
86
|
+
writeFileSync(p, JSON.stringify(manifest, null, 2) + '\n');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Record a created dir/link/block; dedupes on kind+path. */
|
|
90
|
+
export function appendEntry(root, kind, relPath) {
|
|
91
|
+
const now = new Date().toISOString();
|
|
92
|
+
const m = loadManifest(root) ?? { version: 1, createdAt: now, entries: [] };
|
|
93
|
+
if (!Array.isArray(m.entries)) m.entries = [];
|
|
94
|
+
if (m.entries.some((e) => e && e.kind === kind && e.path === relPath)) return;
|
|
95
|
+
m.version = m.version ?? 1;
|
|
96
|
+
m.createdAt = m.createdAt ?? now;
|
|
97
|
+
m.entries.push({ kind, path: relPath, createdAt: now });
|
|
98
|
+
saveManifest(root, m);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Move an existing project-relative path into `.archgen/.backup/<ts>/<rel>`.
|
|
103
|
+
* Preserves the relative layout so backups restore unambiguously.
|
|
104
|
+
* @returns {string} backup location relative to the project root
|
|
105
|
+
*/
|
|
106
|
+
export function moveToBackup(root, relPath) {
|
|
107
|
+
const ts = new Date().toISOString().replace(/:/g, '-');
|
|
108
|
+
const destRel = BACKUP_ROOT_REL + '/' + ts + '/' + relPath;
|
|
109
|
+
const destAbs = join(root, ...destRel.split('/'));
|
|
110
|
+
mkdirSync(dirname(destAbs), { recursive: true });
|
|
111
|
+
const srcAbs = join(root, ...relPath.split('/'));
|
|
112
|
+
try {
|
|
113
|
+
renameSync(srcAbs, destAbs);
|
|
114
|
+
} catch {
|
|
115
|
+
cpSync(srcAbs, destAbs, { recursive: true });
|
|
116
|
+
rmSync(srcAbs, { recursive: true, force: true });
|
|
117
|
+
}
|
|
118
|
+
return destRel;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function rmIfExists(absPath) {
|
|
122
|
+
let st = null;
|
|
123
|
+
try { st = lstatSync(absPath); } catch { /* absent */ }
|
|
124
|
+
if (!st) return false;
|
|
125
|
+
rmSync(absPath, { force: true, recursive: st.isDirectory() && !st.isSymbolicLink() });
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Walk UPWARD from relPath removing directories only while they are left
|
|
131
|
+
* completely empty by our removals. Stops at guarded levels and the root —
|
|
132
|
+
* `.claude`, `.agents`, `.claude/skills`, `.agents/skills` are never deleted.
|
|
133
|
+
*/
|
|
134
|
+
export function pruneEmptyParents(root, relPath) {
|
|
135
|
+
let cur = relPath;
|
|
136
|
+
while (cur && cur !== '.') {
|
|
137
|
+
if (GUARDED_RELPATHS.has(cur)) break;
|
|
138
|
+
const abs = join(root, ...cur.split('/'));
|
|
139
|
+
let empty = false;
|
|
140
|
+
try { empty = readdirSync(abs).length === 0; } catch { break; }
|
|
141
|
+
if (!empty) break;
|
|
142
|
+
rmSync(abs, { recursive: true, force: true });
|
|
143
|
+
const parent = dirname(cur);
|
|
144
|
+
cur = parent === '.' ? '' : parent;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// uninstall-project.js — `archgen uninstall --project [dir]` removal with rails.
|
|
2
|
+
//
|
|
3
|
+
// Removes exactly what archgen owns in a project: the claude symlink, the
|
|
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.
|
|
9
|
+
|
|
10
|
+
import { existsSync, lstatSync, readlinkSync, rmSync, unlinkSync } from 'node:fs';
|
|
11
|
+
import { dirname, join, resolve } from 'node:path';
|
|
12
|
+
import { stripManagedFile } from './block.js';
|
|
13
|
+
import {
|
|
14
|
+
CLAUDE_LINK_REL,
|
|
15
|
+
MANIFEST_REL,
|
|
16
|
+
STORE_REL,
|
|
17
|
+
VERSION_FILE,
|
|
18
|
+
hashDir,
|
|
19
|
+
pruneEmptyParents,
|
|
20
|
+
rmIfExists,
|
|
21
|
+
} from './store.js';
|
|
22
|
+
import { resolveSkillSource } from './init.js';
|
|
23
|
+
|
|
24
|
+
function lstatSafe(p) {
|
|
25
|
+
try { return lstatSync(p); } catch { return null; }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function relJoin(rel) {
|
|
29
|
+
return join(...rel.split('/'));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* @param {string} projectDir project root to clean
|
|
34
|
+
* @param {string} packageRoot dir containing this CLI package (vendor source)
|
|
35
|
+
* @returns {{root: string, linkRemoved: boolean, strippedBlocks: string[],
|
|
36
|
+
* storeRemoved: boolean, storeKept: boolean, manifestRemoved: boolean, warnings: string[]}}
|
|
37
|
+
*/
|
|
38
|
+
export function uninstallProject(projectDir, packageRoot) {
|
|
39
|
+
const root = resolve(projectDir);
|
|
40
|
+
const warnings = [];
|
|
41
|
+
const strippedBlocks = [];
|
|
42
|
+
|
|
43
|
+
const storeAbs = join(root, relJoin(STORE_REL));
|
|
44
|
+
const claudeAbs = join(root, relJoin(CLAUDE_LINK_REL));
|
|
45
|
+
|
|
46
|
+
// 1. Claude adapter — remove only our own symlink (dangling included).
|
|
47
|
+
let linkRemoved = false;
|
|
48
|
+
const lst = lstatSafe(claudeAbs);
|
|
49
|
+
if (lst && lst.isSymbolicLink()) {
|
|
50
|
+
const target = resolve(dirname(claudeAbs), readlinkSync(claudeAbs));
|
|
51
|
+
if (target === storeAbs || !existsSync(claudeAbs)) {
|
|
52
|
+
unlinkSync(claudeAbs); // removes the link itself, never its target
|
|
53
|
+
linkRemoved = true;
|
|
54
|
+
} else {
|
|
55
|
+
warnings.push('foreign symlink at .claude/skills/archgen - left untouched');
|
|
56
|
+
}
|
|
57
|
+
} else if (lst && lst.isDirectory()) {
|
|
58
|
+
warnings.push('.claude/skills/archgen is a real directory - left untouched (remove manually if unwanted)');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 2. Managed blocks — strip markers, keep all user content.
|
|
62
|
+
for (const name of ['AGENTS.md', 'CLAUDE.md']) {
|
|
63
|
+
if (stripManagedFile(join(root, name)).hadBlock) strippedBlocks.push(name);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// 3. Canonical store — remove ONLY if unmodified vs vendor.
|
|
67
|
+
// The version stamp is our metadata, so it is ignored when comparing.
|
|
68
|
+
let storeRemoved = false;
|
|
69
|
+
let storeKept = false;
|
|
70
|
+
if (lstatSafe(storeAbs)) {
|
|
71
|
+
const st = lstatSafe(storeAbs);
|
|
72
|
+
const mine = st.isDirectory() && !st.isSymbolicLink()
|
|
73
|
+
? hashDir(storeAbs, { ignore: [VERSION_FILE] })
|
|
74
|
+
: null;
|
|
75
|
+
let vendor = null;
|
|
76
|
+
try { vendor = hashDir(resolveSkillSource(packageRoot)); } catch { vendor = null; }
|
|
77
|
+
if (vendor !== null && mine === vendor) {
|
|
78
|
+
rmSync(storeAbs, { recursive: true, force: true });
|
|
79
|
+
storeRemoved = true;
|
|
80
|
+
} else {
|
|
81
|
+
storeKept = true;
|
|
82
|
+
warnings.push('canonical store kept at .agents/skills/archgen: modified vs vendor (or unverifiable) - review manually');
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 4. Manifest file — then prune .archgen only if nothing else remains
|
|
87
|
+
// (feature folders / backups keep it alive).
|
|
88
|
+
const manifestRemoved = rmIfExists(join(root, relJoin(MANIFEST_REL)));
|
|
89
|
+
if (manifestRemoved) pruneEmptyParents(root, dirname(MANIFEST_REL));
|
|
90
|
+
|
|
91
|
+
return { root, linkRemoved, strippedBlocks, storeRemoved, storeKept, manifestRemoved, warnings };
|
|
92
|
+
}
|
package/lib/version.js
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// version.js — semver compare + npm registry lookup (zero deps).
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
|
|
4
|
+
export function parseSemver(v) {
|
|
5
|
+
const m = /^v?(\d+)\.(\d+)\.(\d+)/.exec(String(v ?? '').trim());
|
|
6
|
+
return m ? { major: +m[1], minor: +m[2], patch: +m[3] } : null;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function compareSemver(a, b) {
|
|
10
|
+
const x = parseSemver(a);
|
|
11
|
+
const y = parseSemver(b);
|
|
12
|
+
if (!x || !y) return 0;
|
|
13
|
+
for (const k of ['major', 'minor', 'patch']) {
|
|
14
|
+
if (x[k] !== y[k]) return x[k] < y[k] ? -1 : 1;
|
|
15
|
+
}
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function fetchLatestVersion(pkg) {
|
|
20
|
+
const r = spawnSync('npm', ['view', pkg, 'version'], { encoding: 'utf8', timeout: 15000 });
|
|
21
|
+
if (r.status !== 0 || !r.stdout) return null;
|
|
22
|
+
return r.stdout.trim().split(/\r?\n/).pop() || null;
|
|
23
|
+
}
|
package/package.json
CHANGED
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "archgen-skill",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
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",
|
|
7
|
-
"bin": { "archgen": "bin/archgen.mjs" },
|
|
7
|
+
"bin": { "archgen-skill": "bin/archgen.mjs" },
|
|
8
8
|
"files": ["bin/", "lib/", "vendor/", "README.md"],
|
|
9
9
|
"engines": { "node": ">=18" },
|
|
10
|
-
"keywords": ["ai", "agents", "
|
|
10
|
+
"keywords": ["ai", "agents", "claude", "codex", "cursor", "opencode", "gemini", "architecture", "skills", "orchestration", "task-graph", "planning"],
|
|
11
11
|
"repository": { "type": "git", "url": "git+https://github.com/akashmaurya99/archgen.git" },
|
|
12
|
+
"homepage": "https://github.com/akashmaurya99/archgen#readme",
|
|
13
|
+
"bugs": { "url": "https://github.com/akashmaurya99/archgen/issues" },
|
|
12
14
|
"//": "vendor/skills/archgen is synced from ../skills/archgen by scripts/sync-vendor.mjs (run automatically via prepublishOnly). The extension ships separately as a VS Code vsix.",
|
|
13
15
|
"scripts": {
|
|
14
16
|
"sync": "node scripts/sync-vendor.mjs",
|