@friedbotstudio/create-baseline 0.4.0 → 0.5.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/bin/cli.js +64 -17
- package/obj/template/{manifest.json → .claude/manifest.json} +5 -5
- package/obj/template/.claude/skills/audit-baseline/audit.sh +16 -9
- package/obj/template/CLAUDE.md +3 -3
- package/obj/template/docs/init/seed.md +2 -2
- package/package.json +1 -1
- package/src/CLAUDE.template.md +3 -3
- package/src/cli/install.js +7 -9
- package/src/cli/tui/install.js +3 -1
- package/src/cli/tui/meta.js +51 -18
- package/src/cli/tui/splash.js +111 -0
- package/src/cli/tui/tokens.js +15 -8
- package/src/cli/tui/upgrade.js +21 -2
- package/src/seed.template.md +2 -2
package/bin/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import { existsSync } from 'node:fs';
|
|
|
7
7
|
|
|
8
8
|
import * as io from '../src/cli/io.js';
|
|
9
9
|
import { scanSentinels } from '../src/cli/conflict.js';
|
|
10
|
-
import { freshInstall, forceInstall } from '../src/cli/install.js';
|
|
10
|
+
import { freshInstall, forceInstall, COPY_EXCLUDE } from '../src/cli/install.js';
|
|
11
11
|
import { threeWayMerge } from '../src/cli/merge.js';
|
|
12
12
|
import { loadManifest, buildManifestFromDir } from '../src/cli/manifest.js';
|
|
13
13
|
import { fetchPlantumlIfMissing, FETCH_OUTCOMES } from '../src/cli/plantuml.js';
|
|
@@ -100,7 +100,13 @@ function getTemplateDir() {
|
|
|
100
100
|
async function listShippedFiles(templateDir) {
|
|
101
101
|
const out = [];
|
|
102
102
|
await walkFiles(templateDir, templateDir, out);
|
|
103
|
-
|
|
103
|
+
// COPY_EXCLUDE was used to keep the legacy `manifest.json` at the template
|
|
104
|
+
// root from being copied to the consumer target root. The manifest now
|
|
105
|
+
// ships at `.claude/manifest.json` so no path-level exclusion is needed;
|
|
106
|
+
// COPY_EXCLUDE is currently empty but the filter stays so future entries
|
|
107
|
+
// (e.g., dev-only artifacts that accidentally leak into obj/template) can
|
|
108
|
+
// be added in one place. See src/cli/install.js → COPY_EXCLUDE.
|
|
109
|
+
return out.filter((p) => !COPY_EXCLUDE.includes(p));
|
|
104
110
|
}
|
|
105
111
|
|
|
106
112
|
async function walkFiles(dir, base, acc) {
|
|
@@ -111,6 +117,34 @@ async function walkFiles(dir, base, acc) {
|
|
|
111
117
|
}
|
|
112
118
|
}
|
|
113
119
|
|
|
120
|
+
// Renders a usage error and the canonical HELP_TEXT through the branded TUI
|
|
121
|
+
// when stderr is a TTY, falling back to plain `Error: <msg>` + help body
|
|
122
|
+
// otherwise. Every non-success exit from `main()` flows through here so the
|
|
123
|
+
// user always sees usage guidance alongside the failure.
|
|
124
|
+
async function usageError(msg) {
|
|
125
|
+
const version = await readPackageVersion();
|
|
126
|
+
const meta = await import('../src/cli/tui/meta.js');
|
|
127
|
+
meta.renderUsageError(msg, HELP_TEXT, version);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Translates Node's parseArgs error noise into a short, branded message.
|
|
131
|
+
// Node emits e.g. `Unknown option '--upgrade'. To specify a positional ...`
|
|
132
|
+
// which leaks library implementation detail; we collapse it and, where we
|
|
133
|
+
// can identify the user's likely intent (typing `--upgrade` for the
|
|
134
|
+
// `upgrade` subcommand), we hint at the correct shape.
|
|
135
|
+
function friendlyParseArgsMessage(rawMessage) {
|
|
136
|
+
const firstLine = (rawMessage || '').split('\n')[0];
|
|
137
|
+
if (/Unknown option ['"]?--upgrade['"]?/.test(firstLine)) {
|
|
138
|
+
return 'Did you mean `create-baseline upgrade <target>`? `upgrade` is a subcommand, not a flag.';
|
|
139
|
+
}
|
|
140
|
+
if (/Unknown option ['"]?--doctor['"]?/.test(firstLine)) {
|
|
141
|
+
return 'Did you mean `create-baseline doctor <target>`? `doctor` is a subcommand, not a flag.';
|
|
142
|
+
}
|
|
143
|
+
const unknown = /Unknown option ['"]?([^'"]+?)['"]?(?:\.|$)/.exec(firstLine);
|
|
144
|
+
if (unknown) return `Unknown option '${unknown[1]}'.`;
|
|
145
|
+
return firstLine;
|
|
146
|
+
}
|
|
147
|
+
|
|
114
148
|
async function dispatchInstall(target, values, templateDir) {
|
|
115
149
|
const dryRun = !!values['dry-run'];
|
|
116
150
|
if (process.stdout.isTTY && !values.force && !dryRun) {
|
|
@@ -136,13 +170,13 @@ async function runPlainInstall(target, values, templateDir) {
|
|
|
136
170
|
const dryRun = !!values['dry-run'];
|
|
137
171
|
if (values.force) {
|
|
138
172
|
if (!process.stdin.isTTY) {
|
|
139
|
-
|
|
173
|
+
await usageError('--force requires an interactive TTY for the confirmation prompt');
|
|
140
174
|
return 2;
|
|
141
175
|
}
|
|
142
176
|
if (!dryRun) {
|
|
143
177
|
const answer = await io.ask("type 'overwrite' to proceed: ");
|
|
144
178
|
if (answer.toLowerCase() !== 'overwrite') {
|
|
145
|
-
|
|
179
|
+
await usageError('confirmation declined');
|
|
146
180
|
return 1;
|
|
147
181
|
}
|
|
148
182
|
}
|
|
@@ -157,7 +191,7 @@ async function runPlainInstall(target, values, templateDir) {
|
|
|
157
191
|
else await freshInstall(templateDir, target, { withNpmrc: !!values['with-npmrc'] });
|
|
158
192
|
}
|
|
159
193
|
} catch (err) {
|
|
160
|
-
|
|
194
|
+
await usageError(`install failed: ${err.message}`);
|
|
161
195
|
return 1;
|
|
162
196
|
}
|
|
163
197
|
|
|
@@ -181,7 +215,7 @@ async function fetchPlantumlPlain(target, values) {
|
|
|
181
215
|
return 0;
|
|
182
216
|
}
|
|
183
217
|
if (result.outcome === FETCH_OUTCOMES.ERRORED_REQUIRE_PLANTUML) {
|
|
184
|
-
|
|
218
|
+
await usageError(`--require-plantuml: ${result.reason}`);
|
|
185
219
|
return 4;
|
|
186
220
|
}
|
|
187
221
|
return 0;
|
|
@@ -190,7 +224,7 @@ async function fetchPlantumlPlain(target, values) {
|
|
|
190
224
|
async function dispatchUpgrade(target, values, templateDir) {
|
|
191
225
|
const manifestPath = join(target, '.claude/.baseline-manifest.json');
|
|
192
226
|
if (!existsSync(manifestPath)) {
|
|
193
|
-
|
|
227
|
+
await usageError(`No baseline manifest at ${manifestPath}. Run a fresh install first.`);
|
|
194
228
|
return 2;
|
|
195
229
|
}
|
|
196
230
|
if (process.stdout.isTTY) {
|
|
@@ -245,10 +279,10 @@ async function main(argv) {
|
|
|
245
279
|
});
|
|
246
280
|
} catch (err) {
|
|
247
281
|
if (/--merge/.test(err.message)) {
|
|
248
|
-
|
|
282
|
+
await usageError('--merge has been removed; use `create-baseline upgrade <target>` instead.');
|
|
249
283
|
return 2;
|
|
250
284
|
}
|
|
251
|
-
|
|
285
|
+
await usageError(friendlyParseArgsMessage(err.message));
|
|
252
286
|
return 2;
|
|
253
287
|
}
|
|
254
288
|
|
|
@@ -285,23 +319,34 @@ async function main(argv) {
|
|
|
285
319
|
try {
|
|
286
320
|
templateDir = getTemplateDir();
|
|
287
321
|
} catch (err) {
|
|
288
|
-
|
|
322
|
+
await usageError(err.message);
|
|
289
323
|
return 2;
|
|
290
324
|
}
|
|
291
325
|
return await dispatchUpgrade(target, values, templateDir);
|
|
292
326
|
}
|
|
293
327
|
|
|
294
328
|
if (values['no-plantuml'] && values['require-plantuml']) {
|
|
295
|
-
|
|
329
|
+
await usageError('--no-plantuml and --require-plantuml are mutually exclusive');
|
|
296
330
|
return 2;
|
|
297
331
|
}
|
|
298
332
|
if (positionals.length === 0) {
|
|
299
|
-
|
|
300
|
-
|
|
333
|
+
// TTY landing: render the branded splash (skills.sh-style marquee) so the
|
|
334
|
+
// empty invocation reads as "here's what this tool does" instead of an
|
|
335
|
+
// angry error. Non-TTY keeps the strict error+help+exit-2 contract so
|
|
336
|
+
// scripts and CI still detect the missing argument.
|
|
337
|
+
if (process.stdout.isTTY) {
|
|
338
|
+
const splash = await import('../src/cli/tui/splash.js');
|
|
339
|
+
process.stdout.write(splash.renderSplash({
|
|
340
|
+
tryLine: 'npx @friedbotstudio/create-baseline ./my-project',
|
|
341
|
+
discoverUrl: 'https://baseline.friedbotstudio.com/',
|
|
342
|
+
}));
|
|
343
|
+
return 0;
|
|
344
|
+
}
|
|
345
|
+
await usageError('missing required <target> argument');
|
|
301
346
|
return 2;
|
|
302
347
|
}
|
|
303
348
|
if (positionals.length > 1) {
|
|
304
|
-
|
|
349
|
+
await usageError(`unexpected positional arguments: ${positionals.slice(1).join(', ')}`);
|
|
305
350
|
return 2;
|
|
306
351
|
}
|
|
307
352
|
|
|
@@ -310,15 +355,17 @@ async function main(argv) {
|
|
|
310
355
|
try {
|
|
311
356
|
templateDir = getTemplateDir();
|
|
312
357
|
} catch (err) {
|
|
313
|
-
|
|
358
|
+
await usageError(err.message);
|
|
314
359
|
return 2;
|
|
315
360
|
}
|
|
316
361
|
|
|
317
362
|
const sentinels = await scanSentinels(target);
|
|
318
363
|
const hasConflict = sentinels.length > 0;
|
|
319
364
|
if (hasConflict && !values.force) {
|
|
320
|
-
|
|
321
|
-
|
|
365
|
+
await usageError(
|
|
366
|
+
`existing baseline detected at ${target}: ${sentinels.join(', ')}. ` +
|
|
367
|
+
'Pass --force to overwrite or use `create-baseline upgrade <target>` to three-way merge.'
|
|
368
|
+
);
|
|
322
369
|
return 1;
|
|
323
370
|
}
|
|
324
371
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest_version": 2,
|
|
3
|
-
"generated_at": "2026-05-
|
|
3
|
+
"generated_at": "2026-05-20T09:13:44.485Z",
|
|
4
4
|
"files": {
|
|
5
5
|
".claude/agents/swarm-worker.md": "1735a220f268c9765cb22e0567b728803f2edd7776cbde51dd017a9f062ae41f",
|
|
6
6
|
".claude/bin/LICENSE": "a8dcf2775ab71a58c7d4cc935e3a8e9974e87bb7d6082ee25ef52f8140be8e07",
|
|
@@ -56,7 +56,7 @@
|
|
|
56
56
|
".claude/skills/archive/SKILL.md": "5174e8b72ab1e830912c231052d1e535023d58f0808434fe1d4abce305b80318",
|
|
57
57
|
".claude/skills/archive/archive.sh": "29f5b3c8870d0665ce624c6d1b5770f691f1d19f82a201767d5685fc3b0a0b58",
|
|
58
58
|
".claude/skills/audit-baseline/SKILL.md": "86e9955a99ade89074571320b2b9b0780bc033d201968f62edcca35d9e25f7cb",
|
|
59
|
-
".claude/skills/audit-baseline/audit.sh": "
|
|
59
|
+
".claude/skills/audit-baseline/audit.sh": "bd95803bc27cc398564e6df1b89521ead94f3cff458b50497e78fefcced2958e",
|
|
60
60
|
".claude/skills/audit-baseline/tests/fixtures/_pending_opener_only.md": "9c64d7ef0b3be48b3e87acd89f9ac638db956f2c7cc5adf569d9d97d16f18163",
|
|
61
61
|
".claude/skills/audit-baseline/tests/fixtures/preamble_full_empty_body.md": "b757223c9b4954882b7f4ac828de7d83a105f74ba0cdf0951fbbe19c07ae892d",
|
|
62
62
|
".claude/skills/audit-baseline/tests/fixtures/preamble_full_with_entries.md": "9fcd049b006a7474d182a04420df6b3af08e3b6777a2ec5b4031d661ba2f4c82",
|
|
@@ -227,8 +227,8 @@
|
|
|
227
227
|
".claude/skills/triage/SKILL.md": "499f82a26d77c60af827dd89a7fcb3eae0cd903e34c70740c297bc260aeed38e",
|
|
228
228
|
".claude/skills/verify/SKILL.md": "fcddba67cf7f3623fc8f8ae142303b16b9db0c9fc1652bc920e16c56fe4c7864",
|
|
229
229
|
".mcp.json": "8ebb7966045486187bbdf9bac643e690c4fbc7a9a70a8345e3665ba72fa19b96",
|
|
230
|
-
"CLAUDE.md": "
|
|
231
|
-
"docs/init/seed.md": "
|
|
230
|
+
"CLAUDE.md": "1d87311fa1b177944afe69f060b0ddd31359e5391445034d10dc51ced1503ada",
|
|
231
|
+
"docs/init/seed.md": "63f3a0c09895f2040f7fdc188cb0e759bd6a47d615759ec7c83671798504a71d"
|
|
232
232
|
},
|
|
233
233
|
"owners": {
|
|
234
234
|
"skills": {
|
|
@@ -271,5 +271,5 @@
|
|
|
271
271
|
"verify": "baseline"
|
|
272
272
|
}
|
|
273
273
|
},
|
|
274
|
-
"build_id": "gha-
|
|
274
|
+
"build_id": "gha-26153025904"
|
|
275
275
|
}
|
|
@@ -49,18 +49,25 @@ EXPECTED_AGENTS = {
|
|
|
49
49
|
# from main context; they never make decisions.
|
|
50
50
|
"swarm-worker",
|
|
51
51
|
}
|
|
52
|
-
# Skill provenance comes from the shipped manifest
|
|
52
|
+
# Skill provenance comes from the shipped manifest. Two possible locations,
|
|
53
|
+
# tried in order:
|
|
54
|
+
# 1. <root>/.claude/manifest.json — present in consumer projects after the
|
|
55
|
+
# CLI installs the baseline (the recursive cp puts it there directly).
|
|
56
|
+
# 2. <root>/obj/template/.claude/manifest.json — present in the baseline
|
|
57
|
+
# dev repo, where `npm run build` writes the manifest before publishing.
|
|
53
58
|
# The build (scripts/build-manifest.mjs) reads owner: frontmatter from every
|
|
54
59
|
# .claude/skills/<slug>/SKILL.md and emits the canonical baseline-skill set as
|
|
55
60
|
# manifest.owners.skills. See CLAUDE.md Article XI and seed.md §17.
|
|
56
61
|
def load_manifest():
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
for rel in (".claude/manifest.json", "obj/template/.claude/manifest.json"):
|
|
63
|
+
path = root / rel
|
|
64
|
+
if not path.exists():
|
|
65
|
+
continue
|
|
66
|
+
try:
|
|
67
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
68
|
+
except Exception:
|
|
69
|
+
return None
|
|
70
|
+
return None
|
|
64
71
|
|
|
65
72
|
def read_skill_owner(slug):
|
|
66
73
|
p = root / f".claude/skills/{slug}/SKILL.md"
|
|
@@ -245,7 +252,7 @@ def check_skill_ownership():
|
|
|
245
252
|
# Manifest-driven baseline-skill presence + per-file hash check.
|
|
246
253
|
manifest = load_manifest()
|
|
247
254
|
if manifest is None:
|
|
248
|
-
add("skill ownership: manifest", "WARN", "obj/template/manifest.json missing — run npm run build")
|
|
255
|
+
add("skill ownership: manifest", "WARN", ".claude/manifest.json (or obj/template/.claude/manifest.json) missing — run npm run build")
|
|
249
256
|
return
|
|
250
257
|
owners_skills = (manifest.get("owners") or {}).get("skills", {}) or {}
|
|
251
258
|
files_map = manifest.get("files") or {}
|
package/obj/template/CLAUDE.md
CHANGED
|
@@ -272,13 +272,13 @@ The vendored `impeccable` skill stays untouched (Article IX). `design-ui` is the
|
|
|
272
272
|
|
|
273
273
|
## Article XI — Skill provenance and the baseline manifest
|
|
274
274
|
|
|
275
|
-
A skill at `.claude/skills/<slug>/SKILL.md` is **baseline-owned** iff its YAML frontmatter declares `owner: baseline`. Every other skill on disk — those without an `owner:` field, or those declaring `owner: user` — is user/third-party and out-of-scope of baseline audit checks. Absence-of-`owner` is the deliberate default so a project with pre-existing skills can install the baseline without annotating any of its own files. The build script `scripts/build-manifest.mjs` reads each `owner:` value and emits the canonical baseline-skill set into `obj/template/manifest.json` under `owners.skills` (a JSON object mapping slug → `"baseline"`). The
|
|
275
|
+
A skill at `.claude/skills/<slug>/SKILL.md` is **baseline-owned** iff its YAML frontmatter declares `owner: baseline`. Every other skill on disk — those without an `owner:` field, or those declaring `owner: user` — is user/third-party and out-of-scope of baseline audit checks. Absence-of-`owner` is the deliberate default so a project with pre-existing skills can install the baseline without annotating any of its own files. The build script `scripts/build-manifest.mjs` reads each `owner:` value and emits the canonical baseline-skill set into the shipped manifest at `obj/template/.claude/manifest.json` under `owners.skills` (a JSON object mapping slug → `"baseline"`). The recursive install copies the manifest straight to `<target>/.claude/manifest.json` (same path inside the `.claude/` subtree, no special-case). The CLI separately writes `<target>/.claude/.baseline-manifest.json` post-install as a runtime sha256 table of the target's actual on-disk contents (used by `doctor` and `upgrade`). The audit at `.claude/skills/audit-baseline/audit.sh` consumes `manifest.owners.skills` from the shipped `.claude/manifest.json` as the canonical baseline-skill enumeration — the previous hard-coded `EXPECTED_SKILLS` set is removed.
|
|
276
276
|
|
|
277
277
|
You SHALL:
|
|
278
278
|
|
|
279
279
|
1. **Declare baseline ownership only.** A SKILL.md that ships in the baseline SHALL declare `owner: baseline` in its frontmatter directly after `name:`. Authoring a user/third-party skill does NOT require any `owner:` annotation — absence is the default. Explicit `owner: user` is permitted but never required. The only frontmatter-related FAIL the audit emits is `invalid owner=<value>` (a present-but-malformed `owner:` field, e.g. typo). Missing-`owner:` is silently skipped.
|
|
280
|
-
2. **Trust the manifest.** The shipped `obj/template/manifest.json` (
|
|
281
|
-
3. **Re-derive on drift.** The audit re-derives sha256 hashes from `manifest.files` for every path under `.claude/skills/<slug>/` whose slug appears in `owners.skills`, and compares against on-disk content. Mismatches surface as `hash mismatch at <path>`. A baseline-listed slug missing from disk surfaces as `baseline skill missing`. These are hard FAIL — drift detection has no opt-out.
|
|
280
|
+
2. **Trust the manifest.** The shipped manifest at `obj/template/.claude/manifest.json` (delivered to `<target>/.claude/manifest.json` by the recursive install copy) is the canonical record of baseline-owned skills and their content hashes. The runtime `<target>/.claude/.baseline-manifest.json` written by the CLI post-install is a separate file that captures the target's actual on-disk hashes for `doctor`/`upgrade` — do not conflate the two. You SHALL NOT maintain a separate hard-coded list of baseline-skill slugs anywhere in the codebase.
|
|
281
|
+
3. **Re-derive on drift.** The audit reads the manifest from `<root>/.claude/manifest.json` (consumer projects) with a fallback to `<root>/obj/template/.claude/manifest.json` (the baseline dev repo). It re-derives sha256 hashes from `manifest.files` for every path under `.claude/skills/<slug>/` whose slug appears in `owners.skills`, and compares against on-disk content. Mismatches surface as `hash mismatch at <path>`. A baseline-listed slug missing from disk surfaces as `baseline skill missing`. These are hard FAIL — drift detection has no opt-out.
|
|
282
282
|
4. **Preserve constitutional citation.** This Article XI SHALL remain in CLAUDE.md AND in `src/CLAUDE.template.md` (byte-equal mirror). The genesis §17 in `docs/init/seed.md` SHALL remain present, with `src/seed.template.md` mirroring it. The audit verifies both citations and reports `CLAUDE.md missing Article XI citation` or `seed.md missing §17 citation` on absence.
|
|
283
283
|
5. **Out-of-scope skills don't break the audit.** Any skill on disk that doesn't declare `owner: baseline` is out-of-scope: excluded from the baseline count, the names-match check, and the hash-drift check. Installing the baseline into a project that already has its own skills is zero-friction — no per-file annotation required. Maintenance of those skills is the user's responsibility.
|
|
284
284
|
|
|
@@ -591,9 +591,9 @@ Until `/init-project` runs, this section stays empty. Once populated, every fiel
|
|
|
591
591
|
|
|
592
592
|
## §17 — Skill provenance and the baseline manifest
|
|
593
593
|
|
|
594
|
-
A skill at `.claude/skills/<slug>/SKILL.md` is **baseline-owned** iff its YAML frontmatter declares `owner: baseline`. Baseline-owned skills are those that ship with the baseline; every other skill on disk — those without an `owner:` field, or those declaring `owner: user` — is user/third-party and out-of-scope of baseline audit checks. Absence-of-`owner` is the deliberate default so a project that already has its own skills can install the baseline without annotating any of those files. The build script `scripts/build-manifest.mjs` reads each `owner:` value at release time and emits the canonical baseline-skill set into `obj/template/manifest.json` under `owners.skills` (a JSON object mapping slug → `"baseline"`). The
|
|
594
|
+
A skill at `.claude/skills/<slug>/SKILL.md` is **baseline-owned** iff its YAML frontmatter declares `owner: baseline`. Baseline-owned skills are those that ship with the baseline; every other skill on disk — those without an `owner:` field, or those declaring `owner: user` — is user/third-party and out-of-scope of baseline audit checks. Absence-of-`owner` is the deliberate default so a project that already has its own skills can install the baseline without annotating any of those files. The build script `scripts/build-manifest.mjs` reads each `owner:` value at release time and emits the canonical baseline-skill set into the shipped manifest at `obj/template/.claude/manifest.json` under `owners.skills` (a JSON object mapping slug → `"baseline"`). The recursive install copies the manifest into the consumer target at `<target>/.claude/manifest.json` (same in-tree path, no special-case). The CLI separately writes `<target>/.claude/.baseline-manifest.json` post-install on `freshInstall`/`forceInstall`/`merge` — that file is the runtime snapshot of the target's actual on-disk hashes, consumed by `doctor` and `upgrade`. The two files coexist by design: the shipped manifest is frozen at release time and carries `owners.skills`; the runtime manifest is generated at install time and is hash-only.
|
|
595
595
|
|
|
596
|
-
The audit at `.claude/skills/audit-baseline/audit.sh` consumes `manifest.owners.skills` as the canonical baseline-skill enumeration (replacing the previous hard-coded `EXPECTED_SKILLS` set). For every baseline-owned skill, the audit re-derives sha256 hashes from `manifest.files` and compares against on-disk content; a mismatch is reported as `hash mismatch at <path>` against the named slug. A baseline skill present in the manifest but absent from disk is reported as `baseline skill missing`. A SKILL.md whose `owner:` field is present but carries an invalid value (anything other than `baseline` or `user`) is reported as `invalid owner=<value>`. SKILL.md files without an `owner:` field are treated as user/third-party and silently skipped — they are excluded from the baseline count, the names-match check, and the hash-drift check, so installing the baseline into a project that already has its own skills never breaks the audit.
|
|
596
|
+
The audit at `.claude/skills/audit-baseline/audit.sh` consumes `manifest.owners.skills` as the canonical baseline-skill enumeration (replacing the previous hard-coded `EXPECTED_SKILLS` set). It reads the manifest from `<root>/.claude/manifest.json` first (consumer projects) and falls back to `<root>/obj/template/.claude/manifest.json` (the baseline dev repo where `npm run build` writes the manifest). For every baseline-owned skill, the audit re-derives sha256 hashes from `manifest.files` and compares against on-disk content; a mismatch is reported as `hash mismatch at <path>` against the named slug. A baseline skill present in the manifest but absent from disk is reported as `baseline skill missing`. A SKILL.md whose `owner:` field is present but carries an invalid value (anything other than `baseline` or `user`) is reported as `invalid owner=<value>`. SKILL.md files without an `owner:` field are treated as user/third-party and silently skipped — they are excluded from the baseline count, the names-match check, and the hash-drift check, so installing the baseline into a project that already has its own skills never breaks the audit.
|
|
597
597
|
|
|
598
598
|
The audit also verifies constitutional citation: CLAUDE.md SHALL contain the literal string "Article XI" and a reference to the manifest, and `docs/init/seed.md` SHALL contain "§17" and a manifest reference. Missing citations trigger FAIL with `CLAUDE.md missing Article XI citation` or `seed.md missing §17 citation`.
|
|
599
599
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@friedbotstudio/create-baseline",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
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": {
|
package/src/CLAUDE.template.md
CHANGED
|
@@ -272,13 +272,13 @@ The vendored `impeccable` skill stays untouched (Article IX). `design-ui` is the
|
|
|
272
272
|
|
|
273
273
|
## Article XI — Skill provenance and the baseline manifest
|
|
274
274
|
|
|
275
|
-
A skill at `.claude/skills/<slug>/SKILL.md` is **baseline-owned** iff its YAML frontmatter declares `owner: baseline`. Every other skill on disk — those without an `owner:` field, or those declaring `owner: user` — is user/third-party and out-of-scope of baseline audit checks. Absence-of-`owner` is the deliberate default so a project with pre-existing skills can install the baseline without annotating any of its own files. The build script `scripts/build-manifest.mjs` reads each `owner:` value and emits the canonical baseline-skill set into `obj/template/manifest.json` under `owners.skills` (a JSON object mapping slug → `"baseline"`). The
|
|
275
|
+
A skill at `.claude/skills/<slug>/SKILL.md` is **baseline-owned** iff its YAML frontmatter declares `owner: baseline`. Every other skill on disk — those without an `owner:` field, or those declaring `owner: user` — is user/third-party and out-of-scope of baseline audit checks. Absence-of-`owner` is the deliberate default so a project with pre-existing skills can install the baseline without annotating any of its own files. The build script `scripts/build-manifest.mjs` reads each `owner:` value and emits the canonical baseline-skill set into the shipped manifest at `obj/template/.claude/manifest.json` under `owners.skills` (a JSON object mapping slug → `"baseline"`). The recursive install copies the manifest straight to `<target>/.claude/manifest.json` (same path inside the `.claude/` subtree, no special-case). The CLI separately writes `<target>/.claude/.baseline-manifest.json` post-install as a runtime sha256 table of the target's actual on-disk contents (used by `doctor` and `upgrade`). The audit at `.claude/skills/audit-baseline/audit.sh` consumes `manifest.owners.skills` from the shipped `.claude/manifest.json` as the canonical baseline-skill enumeration — the previous hard-coded `EXPECTED_SKILLS` set is removed.
|
|
276
276
|
|
|
277
277
|
You SHALL:
|
|
278
278
|
|
|
279
279
|
1. **Declare baseline ownership only.** A SKILL.md that ships in the baseline SHALL declare `owner: baseline` in its frontmatter directly after `name:`. Authoring a user/third-party skill does NOT require any `owner:` annotation — absence is the default. Explicit `owner: user` is permitted but never required. The only frontmatter-related FAIL the audit emits is `invalid owner=<value>` (a present-but-malformed `owner:` field, e.g. typo). Missing-`owner:` is silently skipped.
|
|
280
|
-
2. **Trust the manifest.** The shipped `obj/template/manifest.json` (
|
|
281
|
-
3. **Re-derive on drift.** The audit re-derives sha256 hashes from `manifest.files` for every path under `.claude/skills/<slug>/` whose slug appears in `owners.skills`, and compares against on-disk content. Mismatches surface as `hash mismatch at <path>`. A baseline-listed slug missing from disk surfaces as `baseline skill missing`. These are hard FAIL — drift detection has no opt-out.
|
|
280
|
+
2. **Trust the manifest.** The shipped manifest at `obj/template/.claude/manifest.json` (delivered to `<target>/.claude/manifest.json` by the recursive install copy) is the canonical record of baseline-owned skills and their content hashes. The runtime `<target>/.claude/.baseline-manifest.json` written by the CLI post-install is a separate file that captures the target's actual on-disk hashes for `doctor`/`upgrade` — do not conflate the two. You SHALL NOT maintain a separate hard-coded list of baseline-skill slugs anywhere in the codebase.
|
|
281
|
+
3. **Re-derive on drift.** The audit reads the manifest from `<root>/.claude/manifest.json` (consumer projects) with a fallback to `<root>/obj/template/.claude/manifest.json` (the baseline dev repo). It re-derives sha256 hashes from `manifest.files` for every path under `.claude/skills/<slug>/` whose slug appears in `owners.skills`, and compares against on-disk content. Mismatches surface as `hash mismatch at <path>`. A baseline-listed slug missing from disk surfaces as `baseline skill missing`. These are hard FAIL — drift detection has no opt-out.
|
|
282
282
|
4. **Preserve constitutional citation.** This Article XI SHALL remain in CLAUDE.md AND in `src/CLAUDE.template.md` (byte-equal mirror). The genesis §17 in `docs/init/seed.md` SHALL remain present, with `src/seed.template.md` mirroring it. The audit verifies both citations and reports `CLAUDE.md missing Article XI citation` or `seed.md missing §17 citation` on absence.
|
|
283
283
|
5. **Out-of-scope skills don't break the audit.** Any skill on disk that doesn't declare `owner: baseline` is out-of-scope: excluded from the baseline count, the names-match check, and the hash-drift check. Installing the baseline into a project that already has its own skills is zero-friction — no per-file annotation required. Maintenance of those skills is the user's responsibility.
|
|
284
284
|
|
package/src/cli/install.js
CHANGED
|
@@ -12,15 +12,13 @@ const NPMRC_TEMPLATE_PATH = join(PACKAGE_ROOT, 'src/.npmrc.template');
|
|
|
12
12
|
|
|
13
13
|
export const NEVER_TOUCH = Object.freeze(['.claude/project.json']);
|
|
14
14
|
export const SPECIAL_MERGE = Object.freeze(['.mcp.json']);
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
// `
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
// see what shipped, but exclude it from the fresh/force install copy.
|
|
23
|
-
export const COPY_EXCLUDE = Object.freeze(['manifest.json']);
|
|
15
|
+
// The shipped manifest now lives at `.claude/manifest.json` (inside the
|
|
16
|
+
// template's .claude/ subtree), so the recursive cp drops it at the correct
|
|
17
|
+
// consumer path without any special-case filtering. The consumer-side audit
|
|
18
|
+
// (`.claude/skills/audit-baseline/audit.sh`) reads it from there for
|
|
19
|
+
// hash-drift detection. COPY_EXCLUDE stays as a list (currently empty) so
|
|
20
|
+
// future never-copy artifacts can be added without API churn at the callers.
|
|
21
|
+
export const COPY_EXCLUDE = Object.freeze([]);
|
|
24
22
|
|
|
25
23
|
async function listFiles(root, base = root, acc = []) {
|
|
26
24
|
for (const entry of await readdir(root, { withFileTypes: true })) {
|
package/src/cli/tui/install.js
CHANGED
|
@@ -6,6 +6,7 @@ import * as clackModule from '@clack/prompts';
|
|
|
6
6
|
import { readFile } from 'node:fs/promises';
|
|
7
7
|
import { freshInstall, forceInstall } from '../install.js';
|
|
8
8
|
import { fetchPlantumlIfMissing, FETCH_OUTCOMES } from '../plantuml.js';
|
|
9
|
+
import { renderBrandStrip } from './splash.js';
|
|
9
10
|
|
|
10
11
|
const SUCCESS = 0;
|
|
11
12
|
const ERR_INSTALL_FAILED = 1;
|
|
@@ -20,7 +21,8 @@ export async function run({ target, opts = {}, prompts = clackModule } = {}) {
|
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
const version = await readPackageVersion();
|
|
23
|
-
|
|
24
|
+
process.stdout.write(renderBrandStrip({ version, subtitle: 'install' }));
|
|
25
|
+
prompts.intro('create-baseline');
|
|
24
26
|
|
|
25
27
|
const spinner = prompts.spinner();
|
|
26
28
|
spinner.start('Copying baseline files');
|
package/src/cli/tui/meta.js
CHANGED
|
@@ -1,24 +1,30 @@
|
|
|
1
|
-
// Domain — branded renderers for the meta commands (--help, --version)
|
|
2
|
-
// In a TTY,
|
|
3
|
-
//
|
|
4
|
-
//
|
|
1
|
+
// Domain — branded renderers for the meta commands (--help, --version) and
|
|
2
|
+
// for usage-class errors. In a TTY, the splash marquee (wordmark + brand
|
|
3
|
+
// strip from splash.js) frames the canonical body; in non-TTY the body is
|
|
4
|
+
// emitted unchanged so that piped consumers (`$(cli --version)`,
|
|
5
|
+
// `cli --help | grep ...`) keep working byte-clean.
|
|
5
6
|
|
|
6
|
-
import { accent, muted, rule } from './tokens.js';
|
|
7
|
+
import { accent, muted, rule, error as errorPaint } from './tokens.js';
|
|
8
|
+
import {
|
|
9
|
+
renderSplash,
|
|
10
|
+
renderBrandStrip,
|
|
11
|
+
renderVersionMarquee,
|
|
12
|
+
wordmarkFits,
|
|
13
|
+
} from './splash.js';
|
|
7
14
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
15
|
+
const DISCOVER_URL = 'https://baseline.friedbotstudio.com/';
|
|
16
|
+
|
|
17
|
+
export function renderHelp(helpText, _version) {
|
|
18
|
+
const body = helpText.endsWith('\n') ? helpText : helpText + '\n';
|
|
19
|
+
if (!process.stdout.isTTY || !wordmarkFits()) {
|
|
20
|
+
process.stdout.write(body);
|
|
11
21
|
return;
|
|
12
22
|
}
|
|
13
|
-
|
|
14
|
-
'',
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
'',
|
|
19
|
-
].join('\n');
|
|
20
|
-
process.stdout.write(banner + '\n');
|
|
21
|
-
process.stdout.write(helpText.endsWith('\n') ? helpText : helpText + '\n');
|
|
23
|
+
process.stdout.write(renderSplash({
|
|
24
|
+
tryLine: 'npx @friedbotstudio/create-baseline ./my-project',
|
|
25
|
+
discoverUrl: DISCOVER_URL,
|
|
26
|
+
}));
|
|
27
|
+
process.stdout.write(body);
|
|
22
28
|
}
|
|
23
29
|
|
|
24
30
|
export function renderVersion(version) {
|
|
@@ -26,5 +32,32 @@ export function renderVersion(version) {
|
|
|
26
32
|
process.stdout.write(`${version}\n`);
|
|
27
33
|
return;
|
|
28
34
|
}
|
|
29
|
-
|
|
35
|
+
if (wordmarkFits()) {
|
|
36
|
+
process.stdout.write(renderVersionMarquee(version));
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
process.stdout.write(renderBrandStrip({ version }));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Usage errors always print to stderr (so a `cli ... 2>/dev/null` pipeline
|
|
43
|
+
// can still consume stdout cleanly). In a TTY we wrap the message and the
|
|
44
|
+
// HELP_TEXT body in the same brand banner used by --help, with the error
|
|
45
|
+
// label painted in --mac-red. In non-TTY we emit a plain `Error: <msg>`
|
|
46
|
+
// line followed by the canonical help body — same body, no ANSI.
|
|
47
|
+
export function renderUsageError(msg, helpText, version) {
|
|
48
|
+
const body = helpText.endsWith('\n') ? helpText : helpText + '\n';
|
|
49
|
+
if (!process.stderr.isTTY) {
|
|
50
|
+
process.stderr.write(`Error: ${msg}\n`);
|
|
51
|
+
process.stderr.write(body);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const banner = [
|
|
55
|
+
'',
|
|
56
|
+
` ${errorPaint('Error')} ${msg}`,
|
|
57
|
+
` ${muted(`@friedbotstudio/create-baseline v${version}`)}`,
|
|
58
|
+
` ${rule('─'.repeat(48))}`,
|
|
59
|
+
'',
|
|
60
|
+
].join('\n');
|
|
61
|
+
process.stderr.write(banner + '\n');
|
|
62
|
+
process.stderr.write(body);
|
|
30
63
|
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// Domain — branded splash surfaces for the CLI. Renders a chunky pixel-art
|
|
2
|
+
// "BASELINE" wordmark in three bands of FBS orange (bevel: shadow / mid /
|
|
3
|
+
// highlight / mid / shadow) so the marquee surfaces (--help, --version,
|
|
4
|
+
// no-arg landing) share a single visual identity. Slimmer brand strip is
|
|
5
|
+
// reused by install / upgrade intros and inside the usage-error renderer.
|
|
6
|
+
//
|
|
7
|
+
// All renderers degrade cleanly when stdout is not a TTY or NO_COLOR is set
|
|
8
|
+
// (paintRGB short-circuits to plain text). When the terminal is narrower
|
|
9
|
+
// than the wordmark, callers should fall through to the plain banner via
|
|
10
|
+
// `wordmarkFits(width)` instead of letting the glyphs wrap.
|
|
11
|
+
|
|
12
|
+
import { paintRGB, PALETTE, accent, muted } from './tokens.js';
|
|
13
|
+
|
|
14
|
+
// ANSI-Shadow style block-letter wordmark for "BASELINE". 5 rows × ~60 cols.
|
|
15
|
+
// Kept as raw strings (not paint-wrapped) so renderWordmark can apply a
|
|
16
|
+
// per-row shade. Trailing spaces matter — they're part of the glyph shape.
|
|
17
|
+
const WORDMARK = [
|
|
18
|
+
'██████ █████ ███████ ███████ ██ ██ ███ ██ ███████',
|
|
19
|
+
'██ ██ ██ ██ ██ ██ ██ ██ ████ ██ ██ ',
|
|
20
|
+
'██████ ███████ ███████ █████ ██ ██ ██ ██ ██ █████ ',
|
|
21
|
+
'██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ',
|
|
22
|
+
'██████ ██ ██ ███████ ███████ ███████ ██ ██ ████ ███████',
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
// Outline trace — mirrors the bottom row of the wordmark using the upper
|
|
26
|
+
// one-eighth block (▔) so it visually kisses the base of every letter,
|
|
27
|
+
// producing the subtle "letters are sitting on a baseline" shadow from
|
|
28
|
+
// the skills.sh reference. Painted in accentShadow so it reads as a
|
|
29
|
+
// trace, not a fifth band of the letter body.
|
|
30
|
+
const WORDMARK_OUTLINE = WORDMARK[4].replace(/█/g, '▔');
|
|
31
|
+
|
|
32
|
+
// Bevel banding: dim → mid → bright → mid → dim. Produces the chiseled
|
|
33
|
+
// pixel-art look of the skills.sh reference (substituting FBS oranges for
|
|
34
|
+
// the reference's grayscale palette).
|
|
35
|
+
const SHADES = [
|
|
36
|
+
PALETTE.accentShadow,
|
|
37
|
+
PALETTE.accent,
|
|
38
|
+
PALETTE.accentLight,
|
|
39
|
+
PALETTE.accent,
|
|
40
|
+
PALETTE.accentShadow,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const WORDMARK_WIDTH = Math.max(...WORDMARK.map((row) => row.length));
|
|
44
|
+
|
|
45
|
+
export const SPLASH_COMMANDS = Object.freeze([
|
|
46
|
+
['$ npx @friedbotstudio/create-baseline <target>', 'Install the baseline'],
|
|
47
|
+
['$ npx @friedbotstudio/create-baseline upgrade', 'Three-way merge upgrade'],
|
|
48
|
+
['$ npx @friedbotstudio/create-baseline doctor', 'Drift report'],
|
|
49
|
+
]);
|
|
50
|
+
|
|
51
|
+
// `process.stdout.columns` is 0 (not undefined) under `script(1)` and some
|
|
52
|
+
// CI ptys; treat any falsy value as "unknown, assume wide enough" so the
|
|
53
|
+
// marquee renders rather than silently degrading to the plain banner.
|
|
54
|
+
export function wordmarkFits(columns) {
|
|
55
|
+
const cols = columns ?? process.stdout.columns;
|
|
56
|
+
if (!cols) return true;
|
|
57
|
+
return cols >= WORDMARK_WIDTH;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function renderWordmark() {
|
|
61
|
+
const bands = WORDMARK.map((row, i) => paintRGB(SHADES[i], row));
|
|
62
|
+
bands.push(paintRGB(PALETTE.accentShadow, WORDMARK_OUTLINE));
|
|
63
|
+
return bands.join('\n');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Full marquee splash. Used by --help in TTY and the no-arg landing. The
|
|
67
|
+
// version is intentionally NOT rendered here — `--version` already surfaces
|
|
68
|
+
// it via renderVersionMarquee, and embedding it in the splash would force
|
|
69
|
+
// docs-site screenshots to re-render every release.
|
|
70
|
+
export function renderSplash({ tagline, tryLine, discoverUrl } = {}) {
|
|
71
|
+
const lines = [
|
|
72
|
+
'',
|
|
73
|
+
`${muted('▲')} ${muted('~/')} ${muted('npx @friedbotstudio/create-baseline@latest')}`,
|
|
74
|
+
'',
|
|
75
|
+
renderWordmark(),
|
|
76
|
+
'',
|
|
77
|
+
muted(tagline ?? 'The Claude Code baseline — hooks, skills, MCP, governance.'),
|
|
78
|
+
'',
|
|
79
|
+
];
|
|
80
|
+
for (const [cmd, desc] of SPLASH_COMMANDS) {
|
|
81
|
+
const left = ` ${cmd}`;
|
|
82
|
+
lines.push(`${left.padEnd(54)}${muted(desc)}`);
|
|
83
|
+
}
|
|
84
|
+
if (tryLine) {
|
|
85
|
+
lines.push('');
|
|
86
|
+
lines.push(`${muted('try:')} ${tryLine}`);
|
|
87
|
+
}
|
|
88
|
+
if (discoverUrl) {
|
|
89
|
+
lines.push('');
|
|
90
|
+
lines.push(`${muted('Discover more at')} ${discoverUrl}`);
|
|
91
|
+
}
|
|
92
|
+
lines.push('');
|
|
93
|
+
lines.push(`${muted('▲')} ${muted('~/')}`);
|
|
94
|
+
lines.push('');
|
|
95
|
+
return lines.join('\n');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Slim two-line brand strip. Used by install / upgrade intros, --version,
|
|
99
|
+
// and the top of the usage-error renderer. Cheap and width-safe (~32 cols).
|
|
100
|
+
export function renderBrandStrip({ version, subtitle } = {}) {
|
|
101
|
+
const left = `${accent('▲ BASELINE')}`;
|
|
102
|
+
const right = version ? ` ${muted(`v${version}`)}` : '';
|
|
103
|
+
const sub = subtitle ? `\n ${muted(subtitle)}` : '';
|
|
104
|
+
return ['', `${left}${right}${sub}`, ''].join('\n');
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --version flourish: the wordmark + a version line. Wider than the strip;
|
|
108
|
+
// callers should fall back to renderBrandStrip when the terminal is narrow.
|
|
109
|
+
export function renderVersionMarquee(version) {
|
|
110
|
+
return ['', renderWordmark(), '', ` ${muted(`v${version}`)}`, ''].join('\n');
|
|
111
|
+
}
|
package/src/cli/tui/tokens.js
CHANGED
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
const NO_COLOR = process.env.NO_COLOR !== undefined && process.env.NO_COLOR !== '';
|
|
7
7
|
|
|
8
8
|
// oklch -> approximate sRGB hex used in the rendered docs site:
|
|
9
|
+
// --accent-shadow oklch(35% 0.15 41.5) ~ #7a2907 (dark band of the wordmark bevel)
|
|
9
10
|
// --accent oklch(55.8% 0.187 41.5) ~ #c2410c (orange-700)
|
|
10
11
|
// --accent-light oklch(70.3% 0.187 41.5) ~ #ea6a25 (orange-500)
|
|
11
12
|
// --muted oklch(45% 0.026 257) ~ #6b7280
|
|
@@ -14,6 +15,7 @@ const NO_COLOR = process.env.NO_COLOR !== undefined && process.env.NO_COLOR !==
|
|
|
14
15
|
// --mac-red oklch(70% 0.21 24) ~ #ef4444
|
|
15
16
|
// --rule oklch(89% 0.013 257) ~ #d1d5db
|
|
16
17
|
const RGB = {
|
|
18
|
+
accentShadow: [122, 41, 7],
|
|
17
19
|
accent: [194, 65, 12],
|
|
18
20
|
accentLight: [234, 106, 37],
|
|
19
21
|
muted: [107, 114, 128],
|
|
@@ -23,16 +25,21 @@ const RGB = {
|
|
|
23
25
|
rule: [209, 213, 219],
|
|
24
26
|
};
|
|
25
27
|
|
|
26
|
-
|
|
28
|
+
// Exposed for the splash wordmark, which paints individual rows in their own
|
|
29
|
+
// shade. All other UI tokens funnel through the named helpers below.
|
|
30
|
+
export function paintRGB(rgb, text) {
|
|
27
31
|
if (NO_COLOR || !process.stdout.isTTY) return text;
|
|
28
32
|
const [r, g, b] = rgb;
|
|
29
33
|
return `\x1b[38;2;${r};${g};${b}m${text}\x1b[0m`;
|
|
30
34
|
}
|
|
31
35
|
|
|
32
|
-
export const
|
|
33
|
-
|
|
34
|
-
export const
|
|
35
|
-
export const
|
|
36
|
-
export const
|
|
37
|
-
export const
|
|
38
|
-
export const
|
|
36
|
+
export const PALETTE = Object.freeze(RGB);
|
|
37
|
+
|
|
38
|
+
export const accentShadow = (text) => paintRGB(RGB.accentShadow, text);
|
|
39
|
+
export const accent = (text) => paintRGB(RGB.accent, text);
|
|
40
|
+
export const accentLight = (text) => paintRGB(RGB.accentLight, text);
|
|
41
|
+
export const muted = (text) => paintRGB(RGB.muted, text);
|
|
42
|
+
export const success = (text) => paintRGB(RGB.success, text);
|
|
43
|
+
export const warn = (text) => paintRGB(RGB.warn, text);
|
|
44
|
+
export const error = (text) => paintRGB(RGB.error, text);
|
|
45
|
+
export const rule = (text) => paintRGB(RGB.rule, text);
|
package/src/cli/tui/upgrade.js
CHANGED
|
@@ -7,10 +7,12 @@
|
|
|
7
7
|
|
|
8
8
|
import * as clackModule from '@clack/prompts';
|
|
9
9
|
import { existsSync } from 'node:fs';
|
|
10
|
-
import { readdir } from 'node:fs/promises';
|
|
10
|
+
import { readdir, readFile } from 'node:fs/promises';
|
|
11
11
|
import { join, relative, sep } from 'node:path';
|
|
12
12
|
import { threeWayMerge, ACTION_KINDS } from '../merge.js';
|
|
13
13
|
import { loadManifest, buildManifestFromDir } from '../manifest.js';
|
|
14
|
+
import { COPY_EXCLUDE } from '../install.js';
|
|
15
|
+
import { renderBrandStrip } from './splash.js';
|
|
14
16
|
|
|
15
17
|
const SUCCESS = 0;
|
|
16
18
|
const ERR_ABORT = 1;
|
|
@@ -37,6 +39,8 @@ export async function run({ target, opts = {}, prompts = clackModule } = {}) {
|
|
|
37
39
|
return ERR_NO_MANIFEST;
|
|
38
40
|
}
|
|
39
41
|
|
|
42
|
+
const version = await readPackageVersion();
|
|
43
|
+
process.stdout.write(renderBrandStrip({ version, subtitle: 'upgrade' }));
|
|
40
44
|
prompts.intro('create-baseline upgrade');
|
|
41
45
|
|
|
42
46
|
const { oldManifest, newManifest } = await loadManifests(opts.templateDir, manifestPath);
|
|
@@ -90,11 +94,26 @@ async function loadManifests(templateDir, manifestPath) {
|
|
|
90
94
|
return { oldManifest, newManifest };
|
|
91
95
|
}
|
|
92
96
|
|
|
97
|
+
async function readPackageVersion() {
|
|
98
|
+
try {
|
|
99
|
+
const url = new URL('../../../package.json', import.meta.url);
|
|
100
|
+
const pkg = JSON.parse(await readFile(url, 'utf8'));
|
|
101
|
+
return pkg.version || '0.0.0';
|
|
102
|
+
} catch {
|
|
103
|
+
return '0.0.0';
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
93
107
|
async function listShippedFiles(root, base = root, acc = []) {
|
|
94
108
|
for (const entry of await readdir(root, { withFileTypes: true })) {
|
|
95
109
|
const full = join(root, entry.name);
|
|
96
110
|
if (entry.isDirectory()) await listShippedFiles(full, base, acc);
|
|
97
111
|
else if (entry.isFile()) acc.push(relative(base, full).split(sep).join('/'));
|
|
98
112
|
}
|
|
99
|
-
|
|
113
|
+
// COPY_EXCLUDE (single source of truth in install.js) now lists no paths —
|
|
114
|
+
// the shipped manifest moved into `.claude/manifest.json` so the recursive
|
|
115
|
+
// walk picks it up at the same path the consumer expects. The filter stays
|
|
116
|
+
// for forward-compat; if a future path needs to be kept out of the merge,
|
|
117
|
+
// add it to install.js → COPY_EXCLUDE in one place.
|
|
118
|
+
return acc.filter((p) => !COPY_EXCLUDE.includes(p));
|
|
100
119
|
}
|
package/src/seed.template.md
CHANGED
|
@@ -591,9 +591,9 @@ Until `/init-project` runs, this section stays empty. Once populated, every fiel
|
|
|
591
591
|
|
|
592
592
|
## §17 — Skill provenance and the baseline manifest
|
|
593
593
|
|
|
594
|
-
A skill at `.claude/skills/<slug>/SKILL.md` is **baseline-owned** iff its YAML frontmatter declares `owner: baseline`. Baseline-owned skills are those that ship with the baseline; every other skill on disk — those without an `owner:` field, or those declaring `owner: user` — is user/third-party and out-of-scope of baseline audit checks. Absence-of-`owner` is the deliberate default so a project that already has its own skills can install the baseline without annotating any of those files. The build script `scripts/build-manifest.mjs` reads each `owner:` value at release time and emits the canonical baseline-skill set into `obj/template/manifest.json` under `owners.skills` (a JSON object mapping slug → `"baseline"`). The
|
|
594
|
+
A skill at `.claude/skills/<slug>/SKILL.md` is **baseline-owned** iff its YAML frontmatter declares `owner: baseline`. Baseline-owned skills are those that ship with the baseline; every other skill on disk — those without an `owner:` field, or those declaring `owner: user` — is user/third-party and out-of-scope of baseline audit checks. Absence-of-`owner` is the deliberate default so a project that already has its own skills can install the baseline without annotating any of those files. The build script `scripts/build-manifest.mjs` reads each `owner:` value at release time and emits the canonical baseline-skill set into the shipped manifest at `obj/template/.claude/manifest.json` under `owners.skills` (a JSON object mapping slug → `"baseline"`). The recursive install copies the manifest into the consumer target at `<target>/.claude/manifest.json` (same in-tree path, no special-case). The CLI separately writes `<target>/.claude/.baseline-manifest.json` post-install on `freshInstall`/`forceInstall`/`merge` — that file is the runtime snapshot of the target's actual on-disk hashes, consumed by `doctor` and `upgrade`. The two files coexist by design: the shipped manifest is frozen at release time and carries `owners.skills`; the runtime manifest is generated at install time and is hash-only.
|
|
595
595
|
|
|
596
|
-
The audit at `.claude/skills/audit-baseline/audit.sh` consumes `manifest.owners.skills` as the canonical baseline-skill enumeration (replacing the previous hard-coded `EXPECTED_SKILLS` set). For every baseline-owned skill, the audit re-derives sha256 hashes from `manifest.files` and compares against on-disk content; a mismatch is reported as `hash mismatch at <path>` against the named slug. A baseline skill present in the manifest but absent from disk is reported as `baseline skill missing`. A SKILL.md whose `owner:` field is present but carries an invalid value (anything other than `baseline` or `user`) is reported as `invalid owner=<value>`. SKILL.md files without an `owner:` field are treated as user/third-party and silently skipped — they are excluded from the baseline count, the names-match check, and the hash-drift check, so installing the baseline into a project that already has its own skills never breaks the audit.
|
|
596
|
+
The audit at `.claude/skills/audit-baseline/audit.sh` consumes `manifest.owners.skills` as the canonical baseline-skill enumeration (replacing the previous hard-coded `EXPECTED_SKILLS` set). It reads the manifest from `<root>/.claude/manifest.json` first (consumer projects) and falls back to `<root>/obj/template/.claude/manifest.json` (the baseline dev repo where `npm run build` writes the manifest). For every baseline-owned skill, the audit re-derives sha256 hashes from `manifest.files` and compares against on-disk content; a mismatch is reported as `hash mismatch at <path>` against the named slug. A baseline skill present in the manifest but absent from disk is reported as `baseline skill missing`. A SKILL.md whose `owner:` field is present but carries an invalid value (anything other than `baseline` or `user`) is reported as `invalid owner=<value>`. SKILL.md files without an `owner:` field are treated as user/third-party and silently skipped — they are excluded from the baseline count, the names-match check, and the hash-drift check, so installing the baseline into a project that already has its own skills never breaks the audit.
|
|
597
597
|
|
|
598
598
|
The audit also verifies constitutional citation: CLAUDE.md SHALL contain the literal string "Article XI" and a reference to the manifest, and `docs/init/seed.md` SHALL contain "§17" and a manifest reference. Missing citations trigger FAIL with `CLAUDE.md missing Article XI citation` or `seed.md missing §17 citation`.
|
|
599
599
|
|