@friedbotstudio/create-baseline 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.
Files changed (30) hide show
  1. package/README.md +10 -4
  2. package/bin/cli.js +197 -119
  3. package/obj/template/.claude/skills/changelog/SKILL.md +69 -0
  4. package/obj/template/.claude/skills/changelog/changelog.mjs +163 -0
  5. package/obj/template/.claude/skills/changelog/classifier.mjs +49 -0
  6. package/obj/template/.claude/skills/changelog/state-writer.mjs +19 -0
  7. package/obj/template/.claude/skills/changelog/tests/consent-expired_test.sh +126 -0
  8. package/obj/template/.claude/skills/changelog/tests/golden-path_test.sh +191 -0
  9. package/obj/template/.claude/skills/changelog/tests/idempotent-reentry_test.sh +121 -0
  10. package/obj/template/.claude/skills/changelog/tests/keepachangelog-unreleased-preserved_test.mjs +149 -0
  11. package/obj/template/.claude/skills/changelog/tests/non-git-shortcircuit_test.sh +98 -0
  12. package/obj/template/.claude/skills/changelog/tests/preview-only_test.sh +96 -0
  13. package/obj/template/.claude/skills/changelog/tests/run.sh +28 -0
  14. package/obj/template/.claude/skills/changelog/unreleased-writer.mjs +155 -0
  15. package/obj/template/.claude/skills/changelog/version-preview.mjs +124 -0
  16. package/obj/template/.claude/skills/commit/SKILL.md +1 -1
  17. package/obj/template/.claude/skills/harness/SKILL.md +3 -1
  18. package/obj/template/.claude/skills/triage/SKILL.md +6 -5
  19. package/obj/template/CLAUDE.md +2 -2
  20. package/obj/template/docs/init/seed.md +4 -4
  21. package/obj/template/manifest.json +21 -7
  22. package/package.json +5 -2
  23. package/src/CLAUDE.template.md +2 -2
  24. package/src/cli/merge.js +15 -10
  25. package/src/cli/tui/doctor.js +56 -0
  26. package/src/cli/tui/install.js +79 -0
  27. package/src/cli/tui/meta.js +30 -0
  28. package/src/cli/tui/tokens.js +38 -0
  29. package/src/cli/tui/upgrade.js +100 -0
  30. package/src/seed.template.md +4 -4
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "manifest_version": 2,
3
- "generated_at": "2026-05-17T19:24:12.190Z",
3
+ "generated_at": "2026-05-18T19:07:49.408Z",
4
4
  "files": {
5
5
  ".claude/agents/swarm-worker.md": "1735a220f268c9765cb22e0567b728803f2edd7776cbde51dd017a9f062ae41f",
6
6
  ".claude/bin/LICENSE": "a8dcf2775ab71a58c7d4cc935e3a8e9974e87bb7d6082ee25ef52f8140be8e07",
@@ -65,6 +65,19 @@
65
65
  ".claude/skills/audit-baseline/tests/preamble_check_test.sh": "73239378e51528c37f7843ff540ddfed2cd0ee1b8259589bc7cdaefd71424d95",
66
66
  ".claude/skills/brd/SKILL.md": "260e5d18396de21b6842ad0bc5fe74b0e124f3c2e2a4ac2867eeb543c7c2c0a3",
67
67
  ".claude/skills/brd/template.md": "be4095b4bed70fcebc821ad92b0217db4fee1afb9a341a9e704d2c1869ec6f43",
68
+ ".claude/skills/changelog/SKILL.md": "2a305b04be3fb44dd9766fae0344725600c2269276d5fe9496d068cebcc4969c",
69
+ ".claude/skills/changelog/changelog.mjs": "662ae8142ea55c70375e45d874f97b40bcb85b0d6e5378393be56aba1b254eee",
70
+ ".claude/skills/changelog/classifier.mjs": "e17cd4dacb48b5ebf3addd2f651e8e2465c10aa51958c11c9272faff32e379f2",
71
+ ".claude/skills/changelog/state-writer.mjs": "a773f14eb1d7d990f64eb54f70720451db279954ea5cbd025ca15f1a8d39449d",
72
+ ".claude/skills/changelog/tests/consent-expired_test.sh": "60351d32cb544af33a6929f898411e43d20ec258f620ebc08179754a74811cf1",
73
+ ".claude/skills/changelog/tests/golden-path_test.sh": "1ef7cdaae2dbeea25f7165b182e4b2cc80e69f3cc918089552145421fcc35d26",
74
+ ".claude/skills/changelog/tests/idempotent-reentry_test.sh": "af11b08744ab3743989f9ae8017b6f01a32373971f072c233ce28741783f5de0",
75
+ ".claude/skills/changelog/tests/keepachangelog-unreleased-preserved_test.mjs": "981e1831893449667b1460b13b9ef9275cc3bd23b22cd6a3cfa3d56d239ed0ad",
76
+ ".claude/skills/changelog/tests/non-git-shortcircuit_test.sh": "ec5760c8790a3cd5cb21e93bd1742a4f30115f0e4e4829a8f765fecb4ddba5f4",
77
+ ".claude/skills/changelog/tests/preview-only_test.sh": "b30a6c4b94a1cb87596af317f460c57c62ecde257dcd81447932564ce9e542d2",
78
+ ".claude/skills/changelog/tests/run.sh": "55ba1d59d67f3e43b07fae08a7059c4e04cfe963a09a2367528d509e748f3116",
79
+ ".claude/skills/changelog/unreleased-writer.mjs": "a03d9080317e2fc9c4895b7b8c68efc2ab90497a9265091f5981859cda3d6885",
80
+ ".claude/skills/changelog/version-preview.mjs": "3843093b08b5407012e53d601fa6d3d4d3ddc04ae4911d0e5ad015622585c67f",
68
81
  ".claude/skills/chore/SKILL.md": "3479c4ef4c7706928b54ed89678c3526c790e2aeb629674bd60e5a9e4b90d80d",
69
82
  ".claude/skills/claude-automation-recommender/LICENSE": "cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
70
83
  ".claude/skills/claude-automation-recommender/NOTICE": "2076093d5af61a10b585562e3b40fd76ce178c56e5eebde93efdc08b3c6bb81a",
@@ -75,7 +88,7 @@
75
88
  ".claude/skills/claude-automation-recommender/references/skills-reference.md": "b4b57d953526a07146d5757ca5332034b9b4b4eeba839e4bb08111398c94556f",
76
89
  ".claude/skills/claude-automation-recommender/references/subagent-templates.md": "7f9a107d8619ff96fea07d5a17da31b269cec0ee343e5a9ff643c0d6cb4ba944",
77
90
  ".claude/skills/code-structure/SKILL.md": "4fa79beb83f9171e973cd2373af12f007aeaf95d156edf3a8e7cf3e9c2c22ace",
78
- ".claude/skills/commit/SKILL.md": "27efe44a50096be2a00e6ee2e7e4d7b158b3f0e8ed3bf18d65d5f03c02d0f8d1",
91
+ ".claude/skills/commit/SKILL.md": "7f22653ae6832a5fc5631774f78d3feabe47c5b21c5925ae8a77e656bbac1b9c",
79
92
  ".claude/skills/copywriting/LICENSE": "3d015bf779e8f6f4b9366e0862c0d560aa60979bbe8d90426cede05528dc5390",
80
93
  ".claude/skills/copywriting/NOTICE": "ffd65b16660e4e9cbf42c5ff9bdf67b482815ebedfb515bedb65c6eb54930a15",
81
94
  ".claude/skills/copywriting/SKILL.md": "4631aae3a65204a1dac89fbe7606481b867b30ae3f64482d337c1e6bd5b4c902",
@@ -108,7 +121,7 @@
108
121
  ".claude/skills/google-analytics/references/reporting.md": "37cb7cfd1dd547e86a41664ffe13893ef138194a2288d1a2c43c9195e8ec609f",
109
122
  ".claude/skills/google-analytics/references/setup.md": "26743b72341a5658e73c1f070b924de0f11b1681af06164cb13c19f2047a1087",
110
123
  ".claude/skills/google-analytics/references/user-tracking.md": "5a945df2a575291c6b35d24830b497915c82a843a93ee133577f1deda93f6d75",
111
- ".claude/skills/harness/SKILL.md": "84c4498e51b9e1c098ddaa99e7b150ab015b8e5c341a2e57300dbafe812acf4d",
124
+ ".claude/skills/harness/SKILL.md": "af7cf374013d881d216462ccba353747e1727f670bd9b1c45b089ffde247d67a",
112
125
  ".claude/skills/humanizer/LICENSE": "5dd2bb7cb6b254edd93a87718efbfef8b81c8ef90e3f9180f06723683d7733a3",
113
126
  ".claude/skills/humanizer/NOTICE": "5bdb69381ab078e7fbb5fb910f10253baa6fe9dabb88143c6f613959e131cbb3",
114
127
  ".claude/skills/humanizer/SKILL.md": "398beef0cabe34df435972a13fb9ef21be74d942f93321b220a5dd393d554e40",
@@ -211,17 +224,18 @@
211
224
  ".claude/skills/technical-tutorials/references/audience-context.md": "7e00189e72d7a87a0cf59f8302aa4adad26222d3bbd14ddb498515f137a32775",
212
225
  ".claude/skills/technical-tutorials/references/audience-example.md": "610f559f691de5169f672e8f69b72fd44790e5e3f894cebb7e644c3331db419c",
213
226
  ".claude/skills/technical-tutorials/references/audience-template.md": "9e8da0e05544df3961d75fb3a7b1d6374b6a1c129ac2fc9862dc111b5a75e433",
214
- ".claude/skills/triage/SKILL.md": "9416c9de58e3dbf8c3bcac4d6e258c9a8e9c38083bd75a35937eaae16b82b0db",
227
+ ".claude/skills/triage/SKILL.md": "499f82a26d77c60af827dd89a7fcb3eae0cd903e34c70740c297bc260aeed38e",
215
228
  ".claude/skills/verify/SKILL.md": "fcddba67cf7f3623fc8f8ae142303b16b9db0c9fc1652bc920e16c56fe4c7864",
216
229
  ".mcp.json": "8ebb7966045486187bbdf9bac643e690c4fbc7a9a70a8345e3665ba72fa19b96",
217
- "CLAUDE.md": "27d7cd145d1ba2a1b9dcc006e0ff3faa1af6c1a92f7d935c4a7eafa87accefc4",
218
- "docs/init/seed.md": "437a05e448f639f5214edda0dc155d9e2f7b5e88b2866333e6639e75ff8822ab"
230
+ "CLAUDE.md": "5aff9bb3534792961b3e5c79a23f7ae3a859bb22efc3553c62847f555c61a08d",
231
+ "docs/init/seed.md": "e0d708ccf06ae1ae124e0a3ada3e77bfbdbb9f2899a548a1729c9270a12f3e98"
219
232
  },
220
233
  "owners": {
221
234
  "skills": {
222
235
  "archive": "baseline",
223
236
  "audit-baseline": "baseline",
224
237
  "brd": "baseline",
238
+ "changelog": "baseline",
225
239
  "chore": "baseline",
226
240
  "claude-automation-recommender": "baseline",
227
241
  "code-structure": "baseline",
@@ -257,5 +271,5 @@
257
271
  "verify": "baseline"
258
272
  }
259
273
  },
260
- "build_id": "gha-26000362681"
274
+ "build_id": "gha-26054481720"
261
275
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@friedbotstudio/create-baseline",
3
- "version": "0.3.0",
4
- "description": "Zero-dependency Node CLI scaffolder that materializes the Claude Code baseline (hooks, skills, commands, MCP servers, governance docs) into a target project. Run via `npx @friedbotstudio/create-baseline <target>`.",
3
+ "version": "0.4.0",
4
+ "description": "Node CLI scaffolder that materializes the Claude Code baseline (hooks, skills, commands, MCP servers, governance docs) into a target project, with branded interactive install / upgrade / doctor flows. Run via `npx @friedbotstudio/create-baseline <target>`.",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "create-baseline": "bin/cli.js"
@@ -43,6 +43,9 @@
43
43
  "email": "hello@friedbotstudio.com"
44
44
  },
45
45
  "homepage": "https://baseline.friedbotstudio.com",
46
+ "dependencies": {
47
+ "@clack/prompts": "1.4.0"
48
+ },
46
49
  "devDependencies": {
47
50
  "@11ty/eleventy": "3.1.5",
48
51
  "@semantic-release/changelog": "6.0.3",
@@ -40,7 +40,7 @@ On every new session, before any work, you SHALL:
40
40
 
41
41
  1. **Read** `.claude/project.json` and check the `configured` field.
42
42
  2. **If `configured: false`** — `/init-project` has not run. The repository is in a sanctioned operating state called **project-agnostic mode**: hooks are active but `test_runner` and `lint_runner` run in guide mode and nothing is tailored to the user's stack. You SHALL greet the user with this exact framing:
43
- > "This repo has the Claude Code baseline installed (22 hooks, 1 subagent, 36 skills). It's in **project-agnostic mode** — `test_runner` and `lint_runner` are in guide mode and nothing is tailored to your stack. Run **`/init-project`** to scout the codebase, run the recommender, and generate a config. Skip it if you want baseline-only behavior, but you'll miss stack-specific tailoring."
43
+ > "This repo has the Claude Code baseline installed (22 hooks, 1 subagent, 37 skills). It's in **project-agnostic mode** — `test_runner` and `lint_runner` are in guide mode and nothing is tailored to your stack. Run **`/init-project`** to scout the codebase, run the recommender, and generate a config. Skip it if you want baseline-only behavior, but you'll miss stack-specific tailoring."
44
44
  You SHALL then proceed with whatever the user asks. Project-agnostic mode is **allowed** — the user is not required to run `/init-project` to use the baseline. The `setup_guard` hook surfaces a one-shot reminder on Write/Edit/MultiEdit (rate-limited to 10 minutes); it does **not** block writes. Other guards (commit, env, spec-approval, verify-pass, track, swarm-boundary) remain hard regardless of `configured` state.
45
45
  3. **If `configured: true`** — read `docs/init/seed.md` §16 if present so you know what was added. Tell the user:
46
46
  > "Configured for `<stack>`. Run `/triage \"<request>\"` to start a workflow, or `/harness` for the full pipeline."
@@ -292,7 +292,7 @@ Cryptographic supply-chain attestation, signed lock files, and per-skill aggrega
292
292
  |---|---|
293
293
  | `.claude/hooks/` | 22 hook scripts (17 write/run-boundary + 4 lifecycle + 1 input-boundary). Bash + python3, no jq. |
294
294
  | `.claude/agents/` | 1 baseline subagent: `swarm-worker` (rendered from `src/agents/swarm-worker.template.md`) |
295
- | `.claude/skills/` | 36 skills: artifact (4) + phases (10) + workers (5) + spec helpers (4) + orchestration (3) + memory (1) + shared globals (7) + audit (1) + alt tracks (1) |
295
+ | `.claude/skills/` | 37 skills: artifact (4) + phases (11) + workers (5) + spec helpers (4) + orchestration (3) + memory (1) + shared globals (7) + audit (1) + alt tracks (1) |
296
296
  | `.claude/commands/` | 5 consent/bootstrap gates: `approve-spec`, `approve-swarm`, `grant-commit`, `grant-push`, `init-project` |
297
297
  | `.claude/memory/` | 7 canonical knowledge files + `_pending.md` (staging) + `_resume.md` (continuity snapshot) + `README.md` |
298
298
  | `.claude/project.json` | per-project config (test/lint cmd, TDD globs, destructive patterns, swarm config, additions). Populated by `/init-project`. |
package/src/cli/merge.js CHANGED
@@ -22,7 +22,8 @@ async function copyFile(src, dst) {
22
22
  await cp(src, dst, { force: true });
23
23
  }
24
24
 
25
- export async function threeWayMerge(templateDir, target, oldManifest, newManifest) {
25
+ export async function threeWayMerge(templateDir, target, oldManifest, newManifest, opts = {}) {
26
+ const { dryRun = false, onSkipCustomized = null } = opts;
26
27
  const actions = [];
27
28
  const oldFiles = oldManifest?.files ?? {};
28
29
  const newFiles = newManifest?.files ?? {};
@@ -36,7 +37,7 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
36
37
  if (await pathExists(tgtPath)) {
37
38
  actions.push({ kind: ACTION_KINDS.NEVER_TOUCH_PRESERVE, path: rel, reason: 'NEVER_TOUCH path present in target' });
38
39
  } else if (rel in newFiles) {
39
- await copyFile(tplPath, tgtPath);
40
+ if (!dryRun) await copyFile(tplPath, tgtPath);
40
41
  actions.push({ kind: ACTION_KINDS.NEVER_TOUCH_ADD, path: rel, reason: 'NEVER_TOUCH path absent; written from template' });
41
42
  }
42
43
  continue;
@@ -44,7 +45,7 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
44
45
 
45
46
  if (SPECIAL_MERGE.includes(rel)) {
46
47
  if (rel in newFiles && await pathExists(tplPath)) {
47
- await deepMergeMcpServers(tplPath, tgtPath);
48
+ if (!dryRun) await deepMergeMcpServers(tplPath, tgtPath);
48
49
  actions.push({ kind: ACTION_KINDS.SPECIAL_MERGE, path: rel, reason: 'additive deep-merge applied' });
49
50
  }
50
51
  continue;
@@ -56,7 +57,7 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
56
57
  const tgtHash = targetExists ? await hashFile(tgtPath) : null;
57
58
 
58
59
  if (!targetExists && newHash) {
59
- await copyFile(tplPath, tgtPath);
60
+ if (!dryRun) await copyFile(tplPath, tgtPath);
60
61
  actions.push({ kind: ACTION_KINDS.ADD, path: rel, reason: 'new in template; not present in target' });
61
62
  continue;
62
63
  }
@@ -67,13 +68,19 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
67
68
  }
68
69
 
69
70
  if (newHash && oldHash && tgtHash === oldHash) {
70
- await copyFile(tplPath, tgtPath);
71
+ if (!dryRun) await copyFile(tplPath, tgtPath);
71
72
  actions.push({ kind: ACTION_KINDS.OVERWRITE, path: rel, reason: 'target untouched since last install; updated' });
72
73
  continue;
73
74
  }
74
75
 
75
76
  if (newHash && tgtHash && tgtHash !== oldHash) {
76
- actions.push({ kind: ACTION_KINDS.SKIP_CUSTOMIZED, path: rel, reason: 'target customized since last install' });
77
+ const choice = onSkipCustomized ? await onSkipCustomized(rel) : 'keep-mine';
78
+ if (choice === 'take-theirs') {
79
+ if (!dryRun) await copyFile(tplPath, tgtPath);
80
+ actions.push({ kind: ACTION_KINDS.OVERWRITE, path: rel, reason: 'customized file; user chose take-theirs' });
81
+ } else {
82
+ actions.push({ kind: ACTION_KINDS.SKIP_CUSTOMIZED, path: rel, reason: 'target customized since last install' });
83
+ }
77
84
  continue;
78
85
  }
79
86
 
@@ -84,10 +91,8 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
84
91
  // prune. Otherwise the user accumulates stale baseline files forever.
85
92
  // - target customized (tgtHash != oldHash) → preserve to avoid
86
93
  // destroying user work; report drift via exit 3.
87
- // Pruning only runs when --merge already applies; there is no separate
88
- // flag (decision recorded in README).
89
94
  if (targetExists && tgtHash === oldHash) {
90
- await unlink(tgtPath);
95
+ if (!dryRun) await unlink(tgtPath);
91
96
  actions.push({ kind: ACTION_KINDS.PRUNE, path: rel, reason: 'removed from new template; target was untouched — deleted' });
92
97
  } else if (targetExists) {
93
98
  actions.push({ kind: ACTION_KINDS.PRUNE_SKIPPED_CUSTOMIZED, path: rel, reason: 'removed from new template; target customized — preserved' });
@@ -96,7 +101,7 @@ export async function threeWayMerge(templateDir, target, oldManifest, newManifes
96
101
  }
97
102
  }
98
103
 
99
- if (newManifest) {
104
+ if (newManifest && !dryRun) {
100
105
  await mkdir(join(target, '.claude'), { recursive: true });
101
106
  await saveManifest(join(target, '.claude/.baseline-manifest.json'), newManifest);
102
107
  }
@@ -0,0 +1,56 @@
1
+ // Domain — branded sectioned doctor report. Consumes the structured
2
+ // DoctorReport from src/cli/doctor.js (unchanged) and writes a colorized,
3
+ // sectioned rendering to stdout. The non-TTY plain path stays on doctor.js's
4
+ // formatReport — this renderer is only invoked when stdout is a TTY.
5
+
6
+ import { accent, muted, success, warn, error, accentLight } from './tokens.js';
7
+
8
+ function brandHeader(target, manifestInfo) {
9
+ const lines = [accent('Baseline doctor')];
10
+ if (target) lines.push(muted(`target: ${target}`));
11
+ if (manifestInfo) lines.push(muted(`manifest: ${manifestInfo}`));
12
+ return lines;
13
+ }
14
+
15
+ export function render(report) {
16
+ if (report.error) {
17
+ const headerLines = brandHeader(report.target);
18
+ process.stdout.write(headerLines.join('\n') + '\n\n');
19
+ process.stdout.write(`${error('doctor:')} ${report.error}\n`);
20
+ return;
21
+ }
22
+ const lines = brandHeader(report.target, `version ${report.manifestVersion}, installed ${report.generatedAt}`);
23
+ lines.push('');
24
+ lines.push(` ${success('matched')}: ${report.matched.length}`);
25
+ lines.push(` ${accentLight('customized')}: ${report.customized.length}`);
26
+ lines.push(` ${error('missing')}: ${report.missing.length}`);
27
+ lines.push(` ${warn('added')}: ${report.added.length}`);
28
+
29
+ if (report.missing.length > 0) {
30
+ lines.push('');
31
+ lines.push(error('Missing (deleted from disk; exit 1):'));
32
+ for (const p of report.missing) lines.push(` - ${p}`);
33
+ }
34
+ if (report.customized.length > 0) {
35
+ lines.push('');
36
+ const header = report.strict
37
+ ? accentLight('Customized (strict mode → exit 1):')
38
+ : accentLight('Customized (informational):');
39
+ lines.push(header);
40
+ if (Array.isArray(report.tampered) && report.tampered.length > 0) {
41
+ for (const entry of report.tampered) {
42
+ lines.push(` ${warn('TAMPERED')}: ${entry.path}`);
43
+ lines.push(` shipped=${muted(entry.shipped)} observed=${muted(entry.observed)}`);
44
+ }
45
+ } else {
46
+ for (const p of report.customized) lines.push(` - ${p}`);
47
+ }
48
+ }
49
+ if (report.added.length > 0) {
50
+ lines.push('');
51
+ lines.push(warn('Added under .claude/ since install (likely /init-project; informational):'));
52
+ for (const p of report.added) lines.push(` - ${p}`);
53
+ }
54
+
55
+ process.stdout.write(lines.join('\n') + '\n');
56
+ }
@@ -0,0 +1,79 @@
1
+ // Domain — branded install flow. Composes the pure-data install + plantuml
2
+ // foundations behind a clack-style presentation seam. The `prompts` parameter
3
+ // defaults to @clack/prompts but is injected in tests.
4
+
5
+ import * as clackModule from '@clack/prompts';
6
+ import { readFile } from 'node:fs/promises';
7
+ import { freshInstall, forceInstall } from '../install.js';
8
+ import { fetchPlantumlIfMissing, FETCH_OUTCOMES } from '../plantuml.js';
9
+
10
+ const SUCCESS = 0;
11
+ const ERR_INSTALL_FAILED = 1;
12
+ const ERR_PLANTUML_REQUIRED = 4;
13
+
14
+ export async function run({ target, opts = {}, prompts = clackModule } = {}) {
15
+ if (!target || typeof target !== 'string') {
16
+ throw new Error('tui.install.run requires a non-empty string target');
17
+ }
18
+ if (!opts.templateDir) {
19
+ throw new Error('tui.install.run requires opts.templateDir');
20
+ }
21
+
22
+ const version = await readPackageVersion();
23
+ prompts.intro(`create-baseline v${version}`);
24
+
25
+ const spinner = prompts.spinner();
26
+ spinner.start('Copying baseline files');
27
+
28
+ try {
29
+ await copyTemplate(target, opts);
30
+ } catch (err) {
31
+ spinner.error('Install failed');
32
+ prompts.outro(err.message);
33
+ return ERR_INSTALL_FAILED;
34
+ }
35
+
36
+ const plantumlExit = await fetchPlantumlBranded(target, opts, prompts, spinner);
37
+ if (plantumlExit !== SUCCESS) return plantumlExit;
38
+
39
+ spinner.stop('Baseline installed');
40
+ prompts.outro(`Installed at ${target}`);
41
+ return SUCCESS;
42
+ }
43
+
44
+ async function copyTemplate(target, opts) {
45
+ const installOpts = { withNpmrc: !!opts.withNpmrc };
46
+ if (opts.force) await forceInstall(opts.templateDir, target, installOpts);
47
+ else await freshInstall(opts.templateDir, target, installOpts);
48
+ }
49
+
50
+ async function fetchPlantumlBranded(target, opts, prompts, spinner) {
51
+ if (opts.noPlantuml) return SUCCESS;
52
+ spinner.message('Fetching PlantUML jar');
53
+ const result = await fetchPlantumlIfMissing(target, {
54
+ noPlantuml: opts.noPlantuml,
55
+ requirePlantuml: opts.requirePlantuml,
56
+ });
57
+ if (result.outcome === FETCH_OUTCOMES.ERRORED_REQUIRE_PLANTUML) {
58
+ spinner.error('PlantUML required but unavailable');
59
+ prompts.outro(result.reason);
60
+ return ERR_PLANTUML_REQUIRED;
61
+ }
62
+ if (
63
+ result.outcome === FETCH_OUTCOMES.WARNED_NETWORK_FAILURE ||
64
+ result.outcome === FETCH_OUTCOMES.WARNED_HASH_MISMATCH
65
+ ) {
66
+ prompts.log.warn(`PlantUML jar: ${result.reason} — install continued`);
67
+ }
68
+ return SUCCESS;
69
+ }
70
+
71
+ async function readPackageVersion() {
72
+ try {
73
+ const url = new URL('../../../package.json', import.meta.url);
74
+ const pkg = JSON.parse(await readFile(url, 'utf8'));
75
+ return pkg.version || '0.0.0';
76
+ } catch {
77
+ return '0.0.0';
78
+ }
79
+ }
@@ -0,0 +1,30 @@
1
+ // Domain — branded renderers for the meta commands (--help, --version).
2
+ // In a TTY, a brand banner frames the canonical body; in non-TTY the body is
3
+ // emitted unchanged so that piped consumers (`$(cli --version)`, `cli --help |
4
+ // grep ...`) keep working byte-clean.
5
+
6
+ import { accent, muted, rule } from './tokens.js';
7
+
8
+ export function renderHelp(helpText, version) {
9
+ if (!process.stdout.isTTY) {
10
+ process.stdout.write(helpText.endsWith('\n') ? helpText : helpText + '\n');
11
+ return;
12
+ }
13
+ const banner = [
14
+ '',
15
+ ` ${accent('Baseline CLI')} ${muted(`v${version}`)}`,
16
+ ` ${muted('@friedbotstudio/create-baseline')}`,
17
+ ` ${rule('─'.repeat(48))}`,
18
+ '',
19
+ ].join('\n');
20
+ process.stdout.write(banner + '\n');
21
+ process.stdout.write(helpText.endsWith('\n') ? helpText : helpText + '\n');
22
+ }
23
+
24
+ export function renderVersion(version) {
25
+ if (!process.stdout.isTTY) {
26
+ process.stdout.write(`${version}\n`);
27
+ return;
28
+ }
29
+ process.stdout.write(`${accent('baseline')} ${muted('v')}${version}\n`);
30
+ }
@@ -0,0 +1,38 @@
1
+ // Foundation — ANSI brand-color helpers for the TUI presentation layer.
2
+ // Translates Friedbot Studio's oklch tokens (site-src/assets/site.css :root) to
3
+ // 24-bit truecolor escape sequences. Respects NO_COLOR (https://no-color.org)
4
+ // and skips emission when stdout is not a TTY.
5
+
6
+ const NO_COLOR = process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '';
7
+
8
+ // oklch -> approximate sRGB hex used in the rendered docs site:
9
+ // --accent oklch(55.8% 0.187 41.5) ~ #c2410c (orange-700)
10
+ // --accent-light oklch(70.3% 0.187 41.5) ~ #ea6a25 (orange-500)
11
+ // --muted oklch(45% 0.026 257) ~ #6b7280
12
+ // --cli-success oklch(70% 0.15 145) ~ #4ade80
13
+ // --warn oklch(58% 0.13 60) ~ #d97706
14
+ // --mac-red oklch(70% 0.21 24) ~ #ef4444
15
+ // --rule oklch(89% 0.013 257) ~ #d1d5db
16
+ const RGB = {
17
+ accent: [194, 65, 12],
18
+ accentLight: [234, 106, 37],
19
+ muted: [107, 114, 128],
20
+ success: [74, 222, 128],
21
+ warn: [217, 119, 6],
22
+ error: [239, 68, 68],
23
+ rule: [209, 213, 219],
24
+ };
25
+
26
+ function paint(rgb, text) {
27
+ if (NO_COLOR || !process.stdout.isTTY) return text;
28
+ const [r, g, b] = rgb;
29
+ return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
30
+ }
31
+
32
+ export const accent = (text) => paint(RGB.accent, text);
33
+ export const accentLight = (text) => paint(RGB.accentLight, text);
34
+ export const muted = (text) => paint(RGB.muted, text);
35
+ export const success = (text) => paint(RGB.success, text);
36
+ export const warn = (text) => paint(RGB.warn, text);
37
+ export const error = (text) => paint(RGB.error, text);
38
+ export const rule = (text) => paint(RGB.rule, text);
@@ -0,0 +1,100 @@
1
+ // Domain — branded upgrade flow with interactive per-file conflict resolution.
2
+ // Plan/apply split:
3
+ // 1. dry-run threeWayMerge → enumerate SKIP_CUSTOMIZED conflicts
4
+ // 2. prompt the user once per conflict
5
+ // 3. on cancel/abort: bail before any write
6
+ // 4. on resolve: real threeWayMerge with onSkipCustomized backed by the Map
7
+
8
+ import * as clackModule from '@clack/prompts';
9
+ import { existsSync } from 'node:fs';
10
+ import { readdir } from 'node:fs/promises';
11
+ import { join, relative, sep } from 'node:path';
12
+ import { threeWayMerge, ACTION_KINDS } from '../merge.js';
13
+ import { loadManifest, buildManifestFromDir } from '../manifest.js';
14
+
15
+ const SUCCESS = 0;
16
+ const ERR_ABORT = 1;
17
+ const ERR_NO_MANIFEST = 2;
18
+ const ERR_DIVERGENCE = 3;
19
+
20
+ const CHOICE_OPTIONS = [
21
+ { value: 'keep-mine', label: 'Keep mine', hint: 'preserve target file as-is' },
22
+ { value: 'take-theirs', label: 'Take theirs', hint: 'overwrite with new baseline' },
23
+ { value: 'abort', label: 'Abort', hint: 'exit without changes' },
24
+ ];
25
+
26
+ export async function run({ target, opts = {}, prompts = clackModule } = {}) {
27
+ if (!target || typeof target !== 'string') {
28
+ throw new Error('tui.upgrade.run requires a non-empty string target');
29
+ }
30
+ if (!opts.templateDir) {
31
+ throw new Error('tui.upgrade.run requires opts.templateDir');
32
+ }
33
+
34
+ const manifestPath = join(target, '.claude/.baseline-manifest.json');
35
+ if (!existsSync(manifestPath)) {
36
+ prompts.log.error(`No baseline manifest at ${manifestPath}. Run a fresh install first.`);
37
+ return ERR_NO_MANIFEST;
38
+ }
39
+
40
+ prompts.intro('create-baseline upgrade');
41
+
42
+ const { oldManifest, newManifest } = await loadManifests(opts.templateDir, manifestPath);
43
+ const dryReport = await threeWayMerge(opts.templateDir, target, oldManifest, newManifest, { dryRun: true });
44
+ const conflicts = dryReport.actions.filter((a) => a.kind === ACTION_KINDS.SKIP_CUSTOMIZED);
45
+
46
+ const choices = new Map();
47
+ for (const conflict of conflicts) {
48
+ const choice = await prompts.select({
49
+ message: `${conflict.path} has been customized — choose:`,
50
+ options: CHOICE_OPTIONS,
51
+ });
52
+ if (prompts.isCancel(choice) || choice === 'abort') {
53
+ prompts.cancel('Upgrade aborted; tree unchanged.');
54
+ return ERR_ABORT;
55
+ }
56
+ choices.set(conflict.path, choice);
57
+ }
58
+
59
+ if (opts.dryRun) {
60
+ for (const action of dryReport.actions) {
61
+ prompts.log.info(`${action.kind.padEnd(24)} ${action.path}`);
62
+ }
63
+ prompts.outro('Dry run complete; no changes written.');
64
+ return SUCCESS;
65
+ }
66
+
67
+ const onSkipCustomized = (rel) => choices.get(rel) ?? 'keep-mine';
68
+ const finalReport = await threeWayMerge(opts.templateDir, target, oldManifest, newManifest, { onSkipCustomized });
69
+
70
+ const applied = finalReport.actions.filter((a) => isApplied(a.kind)).length;
71
+ const skipped = finalReport.actions.filter((a) => a.kind === ACTION_KINDS.SKIP_CUSTOMIZED).length;
72
+ prompts.outro(`Applied ${applied}; ${skipped} skipped.`);
73
+ return finalReport.exitCode === 3 ? ERR_DIVERGENCE : SUCCESS;
74
+ }
75
+
76
+ function isApplied(kind) {
77
+ return (
78
+ kind === ACTION_KINDS.ADD ||
79
+ kind === ACTION_KINDS.OVERWRITE ||
80
+ kind === ACTION_KINDS.PRUNE ||
81
+ kind === ACTION_KINDS.SPECIAL_MERGE ||
82
+ kind === ACTION_KINDS.NEVER_TOUCH_ADD
83
+ );
84
+ }
85
+
86
+ async function loadManifests(templateDir, manifestPath) {
87
+ const oldManifest = await loadManifest(manifestPath);
88
+ const tplFiles = await listShippedFiles(templateDir);
89
+ const newManifest = await buildManifestFromDir(templateDir, tplFiles);
90
+ return { oldManifest, newManifest };
91
+ }
92
+
93
+ async function listShippedFiles(root, base = root, acc = []) {
94
+ for (const entry of await readdir(root, { withFileTypes: true })) {
95
+ const full = join(root, entry.name);
96
+ if (entry.isDirectory()) await listShippedFiles(full, base, acc);
97
+ else if (entry.isFile()) acc.push(relative(base, full).split(sep).join('/'));
98
+ }
99
+ return acc;
100
+ }
@@ -11,7 +11,7 @@
11
11
 
12
12
  **Mandatory binding language.** Each numbered section (§) below specifies a binding requirement for the baseline. Implementations SHALL conform; `CLAUDE.md` Articles SHALL reference the corresponding §; project amendments (per `CLAUDE.md` Art. X) SHALL NOT contradict any § here.
13
13
 
14
- The baseline turns soft engineering rules (no unauthorized commits, no stubs, no mocks of internal code, no self-approved specs) into structural guarantees enforced by write-boundary hooks. Eleven workflow phases plus one stripped-down chore track (skips TDD; runs verify + archive mandatorily, simplify/integrate/document conditionally), seventeen write/run-boundary guards plus four lifecycle hooks plus one input-boundary hook (twenty-two hook scripts total — twenty `.sh` + two `.mjs` after the JS-port pilot), thirty-six skills, one subagent, and four consent gates. Decisions live in main context; the lone subagent (`swarm-worker`) executes pre-decided recipes in parallel worktrees during `/swarm-dispatch`. Every artifact is archived; every third-party API is looked up against live docs. Project memory accumulates across sessions in `.claude/memory/` — auto-extracted by a Stop hook, curated in main context via `/memory-flush`, self-healing via re-verification.
14
+ The baseline turns soft engineering rules (no unauthorized commits, no stubs, no mocks of internal code, no self-approved specs) into structural guarantees enforced by write-boundary hooks. Eleven workflow phases plus one stripped-down chore track (skips TDD; runs verify + archive mandatorily, simplify/integrate/document conditionally), seventeen write/run-boundary guards plus four lifecycle hooks plus one input-boundary hook (twenty-two hook scripts total — twenty `.sh` + two `.mjs` after the JS-port pilot), thirty-seven skills, one subagent, and four consent gates. Decisions live in main context; the lone subagent (`swarm-worker`) executes pre-decided recipes in parallel worktrees during `/swarm-dispatch`. Every artifact is archived; every third-party API is looked up against live docs. Project memory accumulates across sessions in `.claude/memory/` — auto-extracted by a Stop hook, curated in main context via `/memory-flush`, self-healing via re-verification.
15
15
 
16
16
  ---
17
17
 
@@ -110,7 +110,7 @@ Applies to every language. Mappings for TSX, Node, Python, Go, Rust ship inside
110
110
  │ │ └── lib/common.sh # shared helpers
111
111
  │ ├── agents/ # 1 subagent: swarm-worker (rendered from src/agents/swarm-worker.template.md)
112
112
  │ ├── commands/ # 5 consent/bootstrap gates (user-only — structurally)
113
- │ ├── skills/ # 36 skills: artifact (4) + phases (10) + workers (5) + spec helpers (4) + orchestration (3) + memory (1) + shared globals (7) + audit (1) + alt tracks (1)
113
+ │ ├── skills/ # 37 skills: artifact (4) + phases (11) + workers (5) + spec helpers (4) + orchestration (3) + memory (1) + shared globals (7) + audit (1) + alt tracks (1)
114
114
  │ ├── memory/ # project memory: 7 canonical files + _pending.md (gitignored body) + README.md
115
115
  │ └── state/ # runtime: workflow.json, approvals, swarm plans, verdicts, logs
116
116
  ├── src/ # pristine ship-time templates (overlay source for `npx @friedbotstudio/create-baseline`)
@@ -181,7 +181,7 @@ The baseline ships exactly one subagent. The architectural reason: subagents los
181
181
 
182
182
  **Automated re-rendering by `/init-project`.** Step 6.4 re-renders `swarm-worker.md` from the template, driven by the recommender's `additions.swarm_worker_skills`. The recommender does **not** propose new subagent types — only stack-skill additions for the existing worker. Specialization happens via skills loaded into the worker's context, not via parallel agent personas; new decision-making roles belong in skills, which run in main context.
183
183
 
184
- ### §4.3 Skills (36)
184
+ ### §4.3 Skills (37)
185
185
 
186
186
  Each at `.claude/skills/<name>/SKILL.md`, frontmatter `name` + `description`, plus optional `template.md` (artifact skills) or helper scripts.
187
187
 
@@ -516,7 +516,7 @@ Seed-level requirement: no stale workflow artifacts in the working tree after co
516
516
 
517
517
  **Step 4:** Write `src/agents/swarm-worker.template.md` (canonical-body store, per §4.2) — the only subagent template. Then render `.claude/agents/swarm-worker.md` from it with default tokens. The template carries four tokens — `{{NAME}}`, `{{DESCRIPTION}}`, `{{SKILLS}}`, `{{ROLE_LINE}}`. Default `SKILLS` is the YAML list block ` - scenario\n - implement` (the worker's two mandatory sub-skills). Render-parity holds at this stage. `/init-project` later re-renders the worker with stack-aware tokens when the recommender flags stack-specific skills to preload via `additions.swarm_worker_skills`.
518
518
 
519
- **Step 5:** Write `.claude/skills/` for the 36 skills (§4.3) — 28 workflow/worker/orchestration/memory/alt-track skills you author plus 7 shared globals plus 1 audit skill. The breakdown: artifact drafting (4) + workflow phases (10) + phase workers (5: `scenario`, `implement`, `verify`, `prose`, `design-ui`) + spec helpers (4: `spec-lint`, `spec-render`, `spec-diagram-review`, `spec-traceability-review`) + orchestration (3: `harness`, `swarm-plan`, `swarm-dispatch`) + memory (1: `memory-flush`) + shared globals (7: `claude-automation-recommender`, `code-structure`, `humanizer`, `documentation`, `technical-tutorials`, `copywriting`, `impeccable`) + drift defender (1: `audit-baseline`) + alternate tracks (1: `chore`). The vendored `claude-automation-recommender` (Apache 2.0, from `claude-code-setup`), the writing/quality globals, and the design global ship unchanged with their licenses intact. Artifact skills (intake, brd, spec, rca) each ship a `template.md`. Helper scripts: swarm-plan gets `validate.sh`, swarm-dispatch gets `swarm_merge.sh`, spec-render gets `render.sh`, spec-lint gets `lint.sh`, archive gets `archive.sh`, audit-baseline gets `audit.sh`. All helper scripts `chmod +x`.
519
+ **Step 5:** Write `.claude/skills/` for the 37 skills (§4.3) — 29 workflow/worker/orchestration/memory/alt-track skills you author (the +1 over 28 is the `changelog` Phase 11.5 skill) plus 7 shared globals plus 1 audit skill. The breakdown: artifact drafting (4) + workflow phases (10) + phase workers (5: `scenario`, `implement`, `verify`, `prose`, `design-ui`) + spec helpers (4: `spec-lint`, `spec-render`, `spec-diagram-review`, `spec-traceability-review`) + orchestration (3: `harness`, `swarm-plan`, `swarm-dispatch`) + memory (1: `memory-flush`) + shared globals (7: `claude-automation-recommender`, `code-structure`, `humanizer`, `documentation`, `technical-tutorials`, `copywriting`, `impeccable`) + drift defender (1: `audit-baseline`) + alternate tracks (1: `chore`). The vendored `claude-automation-recommender` (Apache 2.0, from `claude-code-setup`), the writing/quality globals, and the design global ship unchanged with their licenses intact. Artifact skills (intake, brd, spec, rca) each ship a `template.md`. Helper scripts: swarm-plan gets `validate.sh`, swarm-dispatch gets `swarm_merge.sh`, spec-render gets `render.sh`, spec-lint gets `lint.sh`, archive gets `archive.sh`, audit-baseline gets `audit.sh`. All helper scripts `chmod +x`.
520
520
 
521
521
  **Step 6:** Write `.claude/commands/*.md` for the 4 gates (§4.4). All carry `disable-model-invocation: true` as belt-and-braces; structural user-only is enforced by their directory.
522
522