agent-docs-bootstrap 0.3.0 → 0.4.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/CHANGELOG.md +8 -0
- package/README.md +10 -6
- package/install.mjs +60 -19
- package/package.json +1 -1
- package/skills/docs-check/scripts/reader-run.mjs +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,14 @@ Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), versions follo
|
|
|
4
4
|
|
|
5
5
|
## [Unreleased]
|
|
6
6
|
|
|
7
|
+
## [0.4.0] - 2026-10-27
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
- `.agent-docs-bootstrap` lines are `<path> <content hash>`. A path is replaced or removed only if its content still matches what the kit wrote; lines without a hash, from 0.3.0 and earlier, are checked against the hashes of everything those versions installed.
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- An update or `--uninstall` removed a listed path whatever was in it, so a skill the user had replaced with their own version under the same name, such as `grilling` after 0.2.0, was deleted. A changed path is now left in place as the user's, with a warning, and drops out of the marker. Subagent files a 0.3.0 or earlier install rendered with `--scout-model` or `--reader-model` do not match the known hashes and are left in place too: delete them and run the installer again.
|
|
14
|
+
|
|
7
15
|
## [0.3.0] - 2026-10-27
|
|
8
16
|
|
|
9
17
|
### Changed
|
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ You need Node.js 18+ and a git repository.
|
|
|
17
17
|
1. From your project root, install the skills and subagents:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
npx agent-docs-bootstrap@0.
|
|
20
|
+
npx agent-docs-bootstrap@0.4.0 --target claude
|
|
21
21
|
```
|
|
22
22
|
|
|
23
23
|
Replace `claude` with `codex`, `opencode`, a list such as `claude,codex`, or `all`.
|
|
@@ -103,19 +103,19 @@ All options run the same installer from your project root. Pin the version: the
|
|
|
103
103
|
**From npm** (recommended):
|
|
104
104
|
|
|
105
105
|
```bash
|
|
106
|
-
npx agent-docs-bootstrap@0.
|
|
106
|
+
npx agent-docs-bootstrap@0.4.0 --target claude
|
|
107
107
|
```
|
|
108
108
|
|
|
109
109
|
**From a GitHub release tag**, when the npm registry is blocked or unavailable:
|
|
110
110
|
|
|
111
111
|
```bash
|
|
112
|
-
npx github:TraneDev/agent-docs-bootstrap#v0.
|
|
112
|
+
npx github:TraneDev/agent-docs-bootstrap#v0.4.0 --target claude
|
|
113
113
|
```
|
|
114
114
|
|
|
115
115
|
**From a clone**, to read or change the kit first:
|
|
116
116
|
|
|
117
117
|
```bash
|
|
118
|
-
git clone --branch v0.
|
|
118
|
+
git clone --branch v0.4.0 https://github.com/TraneDev/agent-docs-bootstrap.git ~/agent-docs-bootstrap
|
|
119
119
|
node ~/agent-docs-bootstrap/install.mjs --target claude
|
|
120
120
|
```
|
|
121
121
|
|
|
@@ -131,7 +131,11 @@ Files are copied into the project, not linked, so they get committed with it.
|
|
|
131
131
|
|
|
132
132
|
Two skills are installed, `docs-bootstrap` and `docs-check`. The guides by other authors that `docs-bootstrap` relies on travel inside it, in `docs-bootstrap/vendor/`, and are not registered as skills, so a skill of yours with the same name, such as `grilling`, never clashes with them.
|
|
133
133
|
|
|
134
|
-
`.agent-docs-bootstrap` lists every installed path
|
|
134
|
+
`.agent-docs-bootstrap` lists every installed path with a hash of what was written. The installer replaces or removes a path only if it is listed there and its content still matches, so it never touches files it did not create or that you changed:
|
|
135
|
+
|
|
136
|
+
- a path that exists but is not listed is skipped with a warning;
|
|
137
|
+
- a listed path whose content you changed, for example a skill you replaced with your own version, is left in place as yours, with a warning, and drops out of the marker;
|
|
138
|
+
- a marker line not shaped like a path the kit installs (for example `src` or `../x`, added by a bad merge) is ignored with a warning.
|
|
135
139
|
|
|
136
140
|
### Other agents
|
|
137
141
|
|
|
@@ -144,7 +148,7 @@ What changes without the installer's targets:
|
|
|
144
148
|
|
|
145
149
|
### Update or remove
|
|
146
150
|
|
|
147
|
-
- **Update:** run the installer again with the new version and commit the result. Paths the previous version installed and this one no longer ships are removed.
|
|
151
|
+
- **Update:** run the installer again with the new version and commit the result. Paths the previous version installed and this one no longer ships are removed. Do not install an older version over a newer one: an older installer does not read the newer marker and treats the kit's files as yours. Run `--uninstall` with the newer version first.
|
|
148
152
|
- **Remove:** run it with `--uninstall`. Everything listed in `.agent-docs-bootstrap` is deleted, along with the marker itself.
|
|
149
153
|
|
|
150
154
|
<details>
|
package/install.mjs
CHANGED
|
@@ -10,15 +10,17 @@
|
|
|
10
10
|
// claude skills → .claude/skills/ agents → .claude/agents/*.md
|
|
11
11
|
// codex skills → .agents/skills/ agents → .codex/agents/*.toml
|
|
12
12
|
// opencode skills → .agents/skills/ agents → .opencode/agents/*.md commands → .opencode/commands/*.md
|
|
13
|
-
// Installed paths are listed in .agent-docs-bootstrap
|
|
14
|
-
// has the shape of a path the kit writes (<layout folder>/<kit name>)
|
|
15
|
-
//
|
|
13
|
+
// Installed paths are listed in .agent-docs-bootstrap with a hash of what was written. A path is replaced or removed
|
|
14
|
+
// only if it is listed, has the shape of a path the kit writes (<layout folder>/<kit name>), and its content still
|
|
15
|
+
// matches that hash; a changed path is left in place as the user's. On an update, listed paths this version no
|
|
16
|
+
// longer ships are removed under the same rule.
|
|
16
17
|
// No network access, no changes to harness settings, no admin rights.
|
|
17
18
|
|
|
18
19
|
import fs from 'node:fs';
|
|
19
20
|
import path from 'node:path';
|
|
20
21
|
import os from 'node:os';
|
|
21
22
|
import { fileURLToPath } from 'node:url';
|
|
23
|
+
import { createHash } from 'node:crypto';
|
|
22
24
|
|
|
23
25
|
const KIT = path.dirname(fileURLToPath(import.meta.url));
|
|
24
26
|
const TARGETS = ['claude', 'codex', 'opencode'];
|
|
@@ -54,9 +56,26 @@ const layoutDirs = new Set(Object.values(LAYOUT).flatMap(L => Object.values(L)).
|
|
|
54
56
|
// The marker is committed, so a bad merge or PR can add any line to it. Only a line shaped like a path the kit
|
|
55
57
|
// writes, <layout folder>/<shipped name>[.md|.toml], is trusted; nothing else is ever replaced or removed.
|
|
56
58
|
const isOwnedPath = (r) => layoutDirs.has(path.normalize(path.dirname(r))) && SHIPPED.has(path.basename(r).replace(/\.(md|toml)$/, ''));
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
const
|
|
59
|
+
// Marker lines are `<path> <hash>`; versions up to 0.3.0 wrote the path alone, so their content is checked
|
|
60
|
+
// against everything those versions installed (LEGACY).
|
|
61
|
+
const old = new Map();
|
|
62
|
+
for (const l of read(MARKER).split('\n').map(l => l.trim()).filter(l => l && !l.startsWith('version='))) {
|
|
63
|
+
const [r, h] = l.split(/\s+/);
|
|
64
|
+
if (isOwnedPath(r)) old.set(r, h);
|
|
65
|
+
else warn(`ignored '${r}' in .agent-docs-bootstrap: not a path this kit installs`);
|
|
66
|
+
}
|
|
67
|
+
// Hashes of every path versions 0.1.0–0.3.0 installed (all targets, no model flags). Fixed: never edit.
|
|
68
|
+
const LEGACY = new Set([
|
|
69
|
+
'00ca80d7077c0e4b', '122ae594f7eb38bd', '185ec7d02d9cf70b', '1f5a0fe496f38dd6', '3798ad63dd954bac',
|
|
70
|
+
'4c36074d70c5a88c', '562d1e8f3a090ffd', '698cadefe39cb46f', '6acf8481d4dd6a1a', '72bcff6d0e5476ba',
|
|
71
|
+
'790d7e6ae5034b3e', '79d9da94af685633', '7cd7eeac11324b51', '855778c0459cea17', '8c5adb3f63300a43',
|
|
72
|
+
'900cf27ab683e5d1', '96288a3e60126264', '9e0e5c30da22c059', 'b45162299d9aa78b', 'c5d47d8811aa61ee',
|
|
73
|
+
'c87450584ed42c8b', 'cf2f8a40c91f3c45', 'dac09810ceb351f7', 'dc418221c7363caa', 'f741ba5711364c55',
|
|
74
|
+
'fceff4c3b3159c80', 'fd975138dcda6b38', 'fe412d3c3c312f7a',
|
|
75
|
+
]);
|
|
76
|
+
// The path is the kit's and nobody changed it since it was installed.
|
|
77
|
+
const isUnchanged = (r) => { const h = contentHash(abs(r)); return old.get(r) ? h === old.get(r) : LEGACY.has(h); };
|
|
78
|
+
const changedNote = (r) => `${r} changed since it was installed, so it is left in place as yours`;
|
|
60
79
|
if (a.uninstall) { uninstall(); process.exit(0); }
|
|
61
80
|
|
|
62
81
|
if (!a.target) usage(1, 'Missing --target.');
|
|
@@ -69,16 +88,17 @@ if (!GLOBAL) {
|
|
|
69
88
|
die(`No .git here (${BASE}) — is this the project root? Run from the root or add --yes.`);
|
|
70
89
|
}
|
|
71
90
|
|
|
72
|
-
const installed = new
|
|
73
|
-
const
|
|
91
|
+
const installed = new Map();
|
|
92
|
+
const released = new Set();
|
|
74
93
|
function writeOwned(r, fn) {
|
|
75
|
-
if (installed.has(r)) return false;
|
|
94
|
+
if (installed.has(r) || released.has(r)) return false;
|
|
76
95
|
const dst = abs(r);
|
|
77
|
-
if (exists(dst) && !
|
|
96
|
+
if (exists(dst) && !old.has(r)) { warn(`skipped ${r}: exists and was not installed by this kit`); return false; }
|
|
97
|
+
if (exists(dst) && !isUnchanged(r)) { warn(`skipped ${changedNote(r)}; delete it and run again to get this version`); released.add(r); return false; }
|
|
78
98
|
fs.rmSync(dst, { recursive: true, force: true });
|
|
79
99
|
fs.mkdirSync(path.dirname(dst), { recursive: true });
|
|
80
100
|
fn(dst);
|
|
81
|
-
installed.
|
|
101
|
+
installed.set(r, contentHash(dst));
|
|
82
102
|
return true;
|
|
83
103
|
}
|
|
84
104
|
|
|
@@ -107,12 +127,15 @@ for (const t of targets) {
|
|
|
107
127
|
|
|
108
128
|
// Paths an earlier version installed into these targets' folders and this version no longer ships.
|
|
109
129
|
const ownDirs = new Set(targets.flatMap(t => Object.values(LAYOUT[t])));
|
|
110
|
-
for (const r of
|
|
111
|
-
if (
|
|
130
|
+
for (const r of old.keys()) {
|
|
131
|
+
if (installed.has(r) || released.has(r) || !ownDirs.has(path.dirname(r)) || !exists(abs(r))) continue;
|
|
132
|
+
if (isUnchanged(r)) { fs.rmSync(abs(r), { recursive: true, force: true }); log(`removed ${r} (no longer shipped)`); }
|
|
133
|
+
else { warn(`not removed: ${changedNote(r)}`); released.add(r); }
|
|
134
|
+
}
|
|
112
135
|
|
|
113
|
-
// Keep entries of earlier installs (e.g. another target) that are still on disk.
|
|
114
|
-
const keep =
|
|
115
|
-
fs.writeFileSync(MARKER, [`version=${VERSION}`, ...keep, ...installed].join('\n') + '\n');
|
|
136
|
+
// Keep entries of earlier installs (e.g. another target) that are still on disk; a changed path is no longer listed.
|
|
137
|
+
const keep = [...old].filter(([r]) => !installed.has(r) && !released.has(r) && exists(abs(r)));
|
|
138
|
+
fs.writeFileSync(MARKER, [`version=${VERSION}`, ...[...keep, ...installed].map(([r, h]) => h ? `${r} ${h}` : r)].join('\n') + '\n');
|
|
116
139
|
|
|
117
140
|
console.log(`\nDone: agent-docs-bootstrap ${VERSION}, ${targets.join(', ')}${GLOBAL ? ' (global)' : ''}.`);
|
|
118
141
|
if (!GLOBAL) log(`commit the installed folders and .agent-docs-bootstrap so the whole team gets the same version`);
|
|
@@ -166,11 +189,15 @@ function skillDirs() {
|
|
|
166
189
|
return out;
|
|
167
190
|
}
|
|
168
191
|
function uninstall() {
|
|
169
|
-
if (!
|
|
192
|
+
if (!old.size) die(`Nothing to remove: no ${MARKER}.`);
|
|
170
193
|
console.log(`Removing agent-docs-bootstrap from ${BASE}`);
|
|
171
|
-
for (const r of
|
|
194
|
+
for (const r of old.keys()) {
|
|
195
|
+
if (!exists(abs(r))) continue;
|
|
196
|
+
if (isUnchanged(r)) { fs.rmSync(abs(r), { recursive: true, force: true }); log(`removed ${r}`); }
|
|
197
|
+
else warn(`not removed: ${changedNote(r)}`);
|
|
198
|
+
}
|
|
172
199
|
fs.rmSync(MARKER, { force: true });
|
|
173
|
-
const dirs = new Set(
|
|
200
|
+
const dirs = new Set([...old.keys()].flatMap(r => [path.dirname(r), path.dirname(path.dirname(r))]));
|
|
174
201
|
for (const d of [...dirs].sort((x, y) => y.length - x.length)) try { fs.rmdirSync(abs(d)); } catch {}
|
|
175
202
|
}
|
|
176
203
|
function parseArgs(argv) {
|
|
@@ -192,6 +219,20 @@ function usage(code, msg) {
|
|
|
192
219
|
console.log(src.slice(0, src.findIndex(l => !l.startsWith('//'))).map(l => l.replace(/^\/\/ ?/, '')).join('\n'));
|
|
193
220
|
process.exit(code);
|
|
194
221
|
}
|
|
222
|
+
// Content hash of an installed file or folder: relative names and text with line endings normalised, so a
|
|
223
|
+
// checkout with CRLF line endings still matches.
|
|
224
|
+
function contentHash(p) {
|
|
225
|
+
const h = createHash('sha256');
|
|
226
|
+
const files = fs.lstatSync(p).isDirectory() ? listFiles(p).sort() : [''];
|
|
227
|
+
for (const f of files) h.update(`${f}\0${read(path.join(p, f)).replace(/\r\n/g, '\n')}\0`);
|
|
228
|
+
return h.digest('hex').slice(0, 16);
|
|
229
|
+
}
|
|
230
|
+
function listFiles(dir, base = dir) {
|
|
231
|
+
return fs.readdirSync(dir, { withFileTypes: true }).flatMap(e => {
|
|
232
|
+
const p = path.join(dir, e.name);
|
|
233
|
+
return e.isDirectory() ? listFiles(p, base) : [path.relative(base, p).split(path.sep).join('/')];
|
|
234
|
+
});
|
|
235
|
+
}
|
|
195
236
|
function read(p) { try { return fs.readFileSync(p, 'utf8'); } catch { return ''; } }
|
|
196
237
|
function log(s) { console.log(` ${s}`); }
|
|
197
238
|
function warn(s) { console.error(`WARNING: ${s}`); }
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agent-docs-bootstrap",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "Agent skills that build reference documentation for a codebase (glossary, ADRs, architecture, how-tos, AGENTS.md) through interviews with the team, with an optional check by a fresh agent. Works with any agent that runs skills; the installer sets up Claude Code, Codex and OpenCode.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": { "type": "git", "url": "git+https://github.com/TraneDev/agent-docs-bootstrap.git" },
|
|
@@ -38,7 +38,7 @@ const win = process.platform === 'win32';
|
|
|
38
38
|
// Documents the kit builds; kit-installed paths and its run files never reach the reader. The marker is committed
|
|
39
39
|
// and could hold any line, so only lines shaped like an installed path count (a line `docs` would hide every document).
|
|
40
40
|
const KIT_DIRS = ['.claude/skills', '.claude/agents', '.agents/skills', '.codex/agents', '.opencode/agents', '.opencode/commands'];
|
|
41
|
-
const kitPaths = read(path.join(ROOT, '.agent-docs-bootstrap'), '', true).split('\n').map(l => l.trim())
|
|
41
|
+
const kitPaths = read(path.join(ROOT, '.agent-docs-bootstrap'), '', true).split('\n').map(l => l.trim().split(/\s+/)[0])
|
|
42
42
|
.filter(l => KIT_DIRS.includes(path.posix.dirname(l)) && /^[a-z0-9-]+(\.md|\.toml)?$/.test(path.posix.basename(l)));
|
|
43
43
|
const isKit = (p) => p === '.agent-docs-bootstrap' || p.startsWith('docs/agents/') || kitPaths.some(k => p === k || p.startsWith(k + '/'));
|
|
44
44
|
// README and other files stay as they were in the parent commit, like the code: they are the project as it was,
|