scripts/bootstrap_adr.js
scripts/bootstrap_adr.jsBrowse 11 files
2,350 tokens
9,379 bytes
Token encoding: o200k_base
Snapshot 8b92ba9
← Back to SKILL.md
1#!/usr/bin/env node2/**3 * Bootstrap ADRs in a repo:4 * - create ADR directory5 * - create adr/README.md (index) using a template6 * - create first ADR: "Adopt architecture decision records"7 */8 9const fs = require('node:fs');10const path = require('node:path');11 12function die(msg) {13 process.stderr.write(`${msg}\n`);14 process.exit(1);15}16 17function parseArgs(argv) {18 const out = {19 repoRoot: '.',20 dir: 'adr',21 forceIndex: false,22 indexFile: null,23 firstTitle: 'Adopt architecture decision records',24 firstStatus: 'accepted',25 deciders: '',26 technicalStory: '',27 strategy: 'date',28 json: false,29 };30 31 for (let i = 2; i < argv.length; i++) {32 const a = argv[i];33 const next = () => {34 if (i + 1 >= argv.length) die(`Missing value for ${a}`);35 return argv[++i];36 };37 38 if (a === '--repo-root') out.repoRoot = next();39 else if (a === '--dir') out.dir = next();40 else if (a === '--force-index') out.forceIndex = true;41 else if (a === '--index-file') out.indexFile = next();42 else if (a === '--first-title') out.firstTitle = next();43 else if (a === '--first-status') out.firstStatus = next();44 else if (a === '--deciders') out.deciders = next();45 else if (a === '--technical-story') out.technicalStory = next();46 else if (a === '--strategy') out.strategy = next();47 else if (a === '--json') out.json = true;48 else if (a === '--help' || a === '-h') {49 process.stdout.write(50 [51 'Usage: node bootstrap_adr.js [options]',52 '',53 'Options:',54 ' --repo-root <path> Repo root (default: .)',55 ' --dir <path> ADR directory (default: adr)',56 ' --index-file <path> Override index file path (relative to repo root unless absolute)',57 ' --force-index Overwrite index file if it exists',58 ' --first-title <text> Title for initial ADR',59 ' --first-status <text> Status for initial ADR (default: accepted)',60 ' --strategy date|slug|auto Filename strategy for initial ADR (default: date)',61 ' --json Output machine-readable JSON (default: off)',62 '',63 ].join('\n'),64 );65 process.exit(0);66 } else {67 die(`Unknown arg: ${a}`);68 }69 }70 71 if (!['auto', 'date', 'slug'].includes(out.strategy))72 die(`Invalid --strategy: ${out.strategy}`);73 return out;74}75 76function loadReadmeTemplate() {77 const skillRoot = path.resolve(__dirname, '..');78 const templatePath = path.join(79 skillRoot,80 'assets',81 'templates',82 'adr-readme.md',83 );84 if (!fs.existsSync(templatePath))85 die(`README template not found: ${templatePath}`);86 return fs.readFileSync(templatePath, 'utf8');87}88 89function writeIndex(indexFile, adrDirName, { force }) {90 if (fs.existsSync(indexFile) && !force) return;91 const content = loadReadmeTemplate().replaceAll('{ADR_DIR}', adrDirName);92 fs.mkdirSync(path.dirname(indexFile), { recursive: true });93 fs.writeFileSync(indexFile, `${content.trimEnd()}\n`, 'utf8');94}95 96function slugify(text) {97 const t = String(text || '')98 .trim()99 .toLowerCase();100 const noQuotes = t.replace(/['"`]/g, '');101 const dashed = noQuotes.replace(/[^a-z0-9]+/g, '-').replace(/-{2,}/g, '-');102 const trimmed = dashed.replace(/^-+/, '').replace(/-+$/, '');103 return trimmed || 'decision';104}105 106function toPosix(p) {107 return p.split(path.sep).join('/');108}109 110function generateFirstAdr({ title, status, date, deciders, adrDir }) {111 const deciderLine = deciders112 ? String(deciders)113 .split(',')114 .map(s => s.trim())115 .filter(Boolean)116 .join(', ')117 : '';118 119 return `---120status: ${status}121date: ${date}122decision-makers: ${deciderLine}123---124 125# ${title}126 127## Context and Problem Statement128 129Architecture decisions in this project are made implicitly — through code, conversations, and tribal knowledge. When a new contributor (human or AI agent) joins the codebase, there is no record of *why* things are built the way they are. This makes it hard to:130 131- Understand whether a pattern is intentional or accidental132- Know if a past decision still applies or has been superseded133- Avoid relitigating decisions that were already carefully considered134 135We need a lightweight, version-controlled way to capture decisions where the code lives.136 137## Decision138 139Adopt Architecture Decision Records (ADRs) using the MADR 4.0 format, stored in \`${adrDir}/\`.140 141Conventions:142- One ADR per file, named \`YYYY-MM-DD-title-with-dashes.md\`143- New ADRs start as \`proposed\`, move to \`accepted\` or \`rejected\`144- Superseded ADRs link to their replacement145- ADRs are written to be self-contained — a coding agent should be able to read one and implement the decision without further context146 147## Consequences148 149* Good, because decisions are discoverable and version-controlled alongside the code150* Good, because new contributors (human or agent) can understand the "why" behind architecture choices151* Good, because the team builds a shared decision log that prevents relitigating settled questions152* Bad, because writing ADRs takes time — though a good ADR saves more time than it costs153* Neutral, because ADRs require periodic review to mark outdated decisions as deprecated or superseded154 155## Alternatives Considered156 157* No formal records: Continue making decisions in conversations and code comments. Rejected because context is lost and decisions get relitigated.158* Wiki or Notion pages: Capture decisions outside the repo. Rejected because they drift out of sync with the code and are not version-controlled.159* Lightweight RFCs: More heavyweight process with formal review cycles. Rejected as overkill for most decisions — ADRs can scale up to RFC-level detail when needed.160 161## More Information162 163* MADR: <https://adr.github.io/madr/>164* Michael Nygard, "Documenting Architecture Decisions": <https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions>`;165}166 167function updateIndexFile(indexFile, { relLink, title, status, date }) {168 if (!fs.existsSync(indexFile)) return;169 let content = fs.readFileSync(indexFile, 'utf8');170 if (content.includes(relLink)) return;171 172 const entryLine = `- [${title}](${relLink}) (${status}, ${date})`;173 174 // Append after "## ADRs" heading if found, otherwise append at end175 const normalized = content.replace(/\r\n/g, '\n');176 const lines = normalized.split('\n');177 const headingIdx = lines.findIndex(l => /^##\s+ADRs\s*$/i.test(l));178 179 if (headingIdx !== -1) {180 // Insert after the heading (and any blank line after it)181 let insertAt = headingIdx + 1;182 while (insertAt < lines.length && lines[insertAt].trim() === '') insertAt++;183 lines.splice(insertAt, 0, entryLine);184 } else {185 lines.push(entryLine);186 }187 188 fs.writeFileSync(indexFile, lines.join('\n'), 'utf8');189}190 191function main() {192 const args = parseArgs(process.argv);193 194 const repoRoot = path.resolve(process.cwd(), args.repoRoot);195 if (!fs.existsSync(repoRoot)) die(`Repo root does not exist: ${repoRoot}`);196 197 const adrDir = path.resolve(repoRoot, args.dir);198 fs.mkdirSync(adrDir, { recursive: true });199 200 const indexFile = args.indexFile201 ? path.isAbsolute(args.indexFile)202 ? args.indexFile203 : path.resolve(repoRoot, args.indexFile)204 : path.join(adrDir, 'README.md');205 206 const indexExistedBefore = fs.existsSync(indexFile);207 writeIndex(indexFile, args.dir, { force: args.forceIndex });208 const indexWritten =209 fs.existsSync(indexFile) && (!indexExistedBefore || args.forceIndex);210 211 // Create the first ADR as a filled-out decision (not a blank template).212 const relIndex = path.isAbsolute(indexFile)213 ? path.relative(repoRoot, indexFile)214 : indexFile;215 const today = new Date().toISOString().slice(0, 10);216 217 const firstAdrContent = generateFirstAdr({218 title: args.firstTitle,219 status: args.firstStatus,220 date: today,221 deciders: args.deciders,222 adrDir: args.dir,223 });224 225 // Determine filename using same logic as new_adr.js226 const strategy = args.strategy === 'auto' ? 'date' : args.strategy;227 let firstAdrFilename;228 if (strategy === 'date') {229 firstAdrFilename = `${today}-${slugify(args.firstTitle)}.md`;230 } else {231 firstAdrFilename = `${slugify(args.firstTitle)}.md`;232 }233 const firstAdrPath = path.join(adrDir, firstAdrFilename);234 fs.writeFileSync(firstAdrPath, `${firstAdrContent.trimEnd()}\n`, 'utf8');235 236 // Update index237 const relLink = toPosix(path.relative(path.dirname(indexFile), firstAdrPath));238 updateIndexFile(indexFile, {239 relLink,240 title: args.firstTitle,241 status: args.firstStatus,242 date: today,243 });244 245 if (args.json) {246 const payload = {247 repoRoot,248 adrDir,249 adrDirRelPath: toPosix(path.relative(repoRoot, adrDir)),250 indexPath: indexFile,251 indexRelPath: toPosix(relIndex),252 indexExistedBefore,253 indexWritten,254 firstAdr: {255 createdAdrPath: firstAdrPath,256 createdAdrRelPath: toPosix(path.relative(repoRoot, firstAdrPath)),257 title: args.firstTitle,258 status: args.firstStatus,259 strategy,260 date: today,261 },262 date: today,263 };264 process.stdout.write(`${JSON.stringify(payload)}\n`);265 return;266 }267 268 process.stdout.write(`${firstAdrPath}\n`);269 process.stdout.write(`Bootstrapped ADRs at ${adrDir} (${today})\n`);270 process.stdout.write(`Index: ${indexFile}\n`);271}272 273main();274 Referenced from SKILL.md
SKILL.mdView in source ↗
Source excerpt starting at line 286.SKILL.mdView in source ↗286- `scripts/set_adr_status.js` — update an ADR status in-place (YAML front matter or inline). Use `--json` for machine output.287- `scripts/bootstrap_adr.js` — create ADR dir, `README.md`, and initial "Adopt ADRs" decision.
Source excerpt starting at line 299.299- `assets/templates/adr-madr.md` — MADR 4.0 template for decisions with multiple options and structured tradeoffs.300- `assets/templates/adr-readme.md` — default ADR index scaffold used by `scripts/bootstrap_adr.js`.