@friedbotstudio/create-baseline 0.3.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/README.md +10 -4
- package/bin/cli.js +252 -127
- package/obj/template/{manifest.json → .claude/manifest.json} +22 -8
- package/obj/template/.claude/skills/audit-baseline/audit.sh +16 -9
- package/obj/template/.claude/skills/changelog/SKILL.md +69 -0
- package/obj/template/.claude/skills/changelog/changelog.mjs +163 -0
- package/obj/template/.claude/skills/changelog/classifier.mjs +49 -0
- package/obj/template/.claude/skills/changelog/state-writer.mjs +19 -0
- package/obj/template/.claude/skills/changelog/tests/consent-expired_test.sh +126 -0
- package/obj/template/.claude/skills/changelog/tests/golden-path_test.sh +191 -0
- package/obj/template/.claude/skills/changelog/tests/idempotent-reentry_test.sh +121 -0
- package/obj/template/.claude/skills/changelog/tests/keepachangelog-unreleased-preserved_test.mjs +149 -0
- package/obj/template/.claude/skills/changelog/tests/non-git-shortcircuit_test.sh +98 -0
- package/obj/template/.claude/skills/changelog/tests/preview-only_test.sh +96 -0
- package/obj/template/.claude/skills/changelog/tests/run.sh +28 -0
- package/obj/template/.claude/skills/changelog/unreleased-writer.mjs +155 -0
- package/obj/template/.claude/skills/changelog/version-preview.mjs +124 -0
- package/obj/template/.claude/skills/commit/SKILL.md +1 -1
- package/obj/template/.claude/skills/harness/SKILL.md +3 -1
- package/obj/template/.claude/skills/triage/SKILL.md +6 -5
- package/obj/template/CLAUDE.md +5 -5
- package/obj/template/docs/init/seed.md +6 -6
- package/package.json +5 -2
- package/src/CLAUDE.template.md +5 -5
- package/src/cli/install.js +7 -9
- package/src/cli/merge.js +15 -10
- package/src/cli/tui/doctor.js +56 -0
- package/src/cli/tui/install.js +81 -0
- package/src/cli/tui/meta.js +63 -0
- package/src/cli/tui/splash.js +111 -0
- package/src/cli/tui/tokens.js +45 -0
- package/src/cli/tui/upgrade.js +119 -0
- package/src/seed.template.md +6 -6
package/README.md
CHANGED
|
@@ -37,11 +37,11 @@ A discipline layer for Claude Code. Hooks at every tool boundary, a workflow tha
|
|
|
37
37
|
> [!IMPORTANT]
|
|
38
38
|
> **Install in one line:** `npx @friedbotstudio/create-baseline ./your-project`
|
|
39
39
|
>
|
|
40
|
-
> The CLI fetches the published package, runs the install, and leaves your project with `.claude/`, `CLAUDE.md`, `docs/init/seed.md`, and `.mcp.json`. Re-run with
|
|
40
|
+
> The CLI fetches the published package, runs the install, and leaves your project with `.claude/`, `CLAUDE.md`, `docs/init/seed.md`, and `.mcp.json`. Re-run with the `upgrade` subcommand to bring an existing install forward (interactive in a TTY, batch-mode in CI). Add `--dry-run` to preview, and run `doctor` to report drift (pass `--json` for machine output).
|
|
41
41
|
|
|
42
42
|
## What this is
|
|
43
43
|
|
|
44
|
-
The Claude Code Baseline is a repository overlay shipped via `npx @friedbotstudio/create-baseline ./target`. It installs **22 hooks** at Claude's tool boundaries, **
|
|
44
|
+
The Claude Code Baseline is a repository overlay shipped via `npx @friedbotstudio/create-baseline ./target`. It installs **22 hooks** at Claude's tool boundaries, **37 skills** organised into nine categories, **1 subagent** for parallel work in isolated worktrees, an **11-phase workflow** from intake to commit, and **3 user-typed consent gates** that Claude cannot forge.
|
|
45
45
|
|
|
46
46
|
Every soft engineering rule a team usually repeats every session — *don't push, don't `--amend`, don't self-approve specs, don't skip phases, don't mock internal modules* — becomes a structural guarantee because the hooks run **outside Claude's tool boundary**. Claude cannot disable a hook with a flag, cannot write a consent marker, cannot reorder the phases without an explicit exception that triage records on disk.
|
|
47
47
|
|
|
@@ -94,12 +94,14 @@ npx @friedbotstudio/create-baseline ./your-project
|
|
|
94
94
|
# Force-overwrite an existing install (interactive — type 'overwrite')
|
|
95
95
|
npx @friedbotstudio/create-baseline ./your-project --overwrite
|
|
96
96
|
|
|
97
|
-
#
|
|
97
|
+
# Upgrade an existing install against a newer baseline version.
|
|
98
|
+
# In a TTY, each customised file becomes a keep-mine / take-theirs / abort
|
|
99
|
+
# prompt. In CI / piped stdout, reproduces the prior --merge behaviour:
|
|
98
100
|
# - adds new baseline files
|
|
99
101
|
# - refreshes baseline files the user has not touched
|
|
100
102
|
# - preserves user-customised files (exit 3 if any)
|
|
101
103
|
# - removes baseline files the upstream removed (only if untouched locally)
|
|
102
|
-
npx @friedbotstudio/create-baseline ./your-project
|
|
104
|
+
npx @friedbotstudio/create-baseline upgrade ./your-project
|
|
103
105
|
|
|
104
106
|
# Preview without writing anything
|
|
105
107
|
npx @friedbotstudio/create-baseline ./your-project --dry-run
|
|
@@ -124,6 +126,10 @@ npx @friedbotstudio/create-baseline doctor ./your-project
|
|
|
124
126
|
# Strict mode — print TAMPERED: shipped vs observed sha256 for every
|
|
125
127
|
# customised file and exit 1 on any drift.
|
|
126
128
|
npx @friedbotstudio/create-baseline doctor ./your-project --strict
|
|
129
|
+
|
|
130
|
+
# JSON mode — emit the structured report on stdout for CI parsers.
|
|
131
|
+
# Same exit codes; honours --strict.
|
|
132
|
+
npx @friedbotstudio/create-baseline doctor ./your-project --json
|
|
127
133
|
```
|
|
128
134
|
|
|
129
135
|
## Quickstart
|
package/bin/cli.js
CHANGED
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { dirname, join, resolve } from 'node:path';
|
|
5
|
-
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import { readFile, readdir } from 'node:fs/promises';
|
|
6
6
|
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';
|
|
@@ -17,18 +17,24 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
17
17
|
const PKG_ROOT = resolve(__dirname, '..');
|
|
18
18
|
|
|
19
19
|
const HELP_TEXT = `Usage:
|
|
20
|
-
create-baseline <target> [options] install
|
|
20
|
+
create-baseline <target> [options] install the baseline
|
|
21
|
+
create-baseline upgrade [target] three-way merge against an installed baseline
|
|
21
22
|
create-baseline doctor [target] report drift in an installed target
|
|
22
23
|
|
|
23
24
|
Materializes the Claude Code baseline (.claude/, CLAUDE.md, .mcp.json,
|
|
24
25
|
docs/init/seed.md, plus vendored LICENSE/NOTICE) into <target>.
|
|
25
26
|
|
|
26
|
-
|
|
27
|
+
Install modes:
|
|
27
28
|
(default) Fresh install. Refuses if any sentinel path already present.
|
|
28
29
|
--force Overwrite unconditionally (requires typing 'overwrite' in TTY).
|
|
29
|
-
--
|
|
30
|
-
|
|
31
|
-
|
|
30
|
+
--dry-run Print intended actions without writing.
|
|
31
|
+
|
|
32
|
+
Upgrade:
|
|
33
|
+
Replaces the prior --merge flag. Reads <target>/.claude/.baseline-manifest.json
|
|
34
|
+
and runs a three-way merge against the shipped template. Prunes baseline files
|
|
35
|
+
removed upstream that the user hadn't touched; customized-stale files are
|
|
36
|
+
preserved (exit 3) — or interactively resolved when stdout is a TTY (keep
|
|
37
|
+
mine / take theirs / abort).
|
|
32
38
|
--dry-run Print intended actions without writing.
|
|
33
39
|
|
|
34
40
|
Doctor:
|
|
@@ -38,6 +44,8 @@ Doctor:
|
|
|
38
44
|
--strict Promote customized to exit 1 and prefix tampered paths
|
|
39
45
|
with "TAMPERED:" with shipped vs observed sha256. Detects
|
|
40
46
|
post-install supply-chain tampering of the baseline tree.
|
|
47
|
+
--json Emit the structured report as JSON to stdout instead of
|
|
48
|
+
the text renderer. Honours --strict; same exit codes.
|
|
41
49
|
|
|
42
50
|
PlantUML jar (~19 MB, fetched at install time from upstream):
|
|
43
51
|
--no-plantuml Skip the jar download entirely.
|
|
@@ -54,12 +62,24 @@ Misc:
|
|
|
54
62
|
|
|
55
63
|
Exit codes:
|
|
56
64
|
0 success / clean doctor
|
|
57
|
-
1 user abort, conflict-without-force
|
|
58
|
-
2 argv error, non-TTY where TTY required,
|
|
59
|
-
3
|
|
65
|
+
1 user abort, conflict-without-force, doctor reports missing files, or upgrade aborted
|
|
66
|
+
2 argv error, non-TTY where TTY required, doctor finds no manifest, or --merge passed
|
|
67
|
+
3 upgrade had skipped customizations (or stale-customized prunes)
|
|
60
68
|
4 --require-plantuml fetch failure
|
|
61
69
|
`;
|
|
62
70
|
|
|
71
|
+
const OPTIONS = {
|
|
72
|
+
help: { type: 'boolean', short: 'h' },
|
|
73
|
+
version: { type: 'boolean' },
|
|
74
|
+
force: { type: 'boolean' },
|
|
75
|
+
'dry-run': { type: 'boolean' },
|
|
76
|
+
'no-plantuml': { type: 'boolean' },
|
|
77
|
+
'require-plantuml': { type: 'boolean' },
|
|
78
|
+
'with-npmrc': { type: 'boolean' },
|
|
79
|
+
strict: { type: 'boolean' },
|
|
80
|
+
json: { type: 'boolean' },
|
|
81
|
+
};
|
|
82
|
+
|
|
63
83
|
async function readPackageVersion() {
|
|
64
84
|
try {
|
|
65
85
|
const pkg = JSON.parse(await readFile(join(PKG_ROOT, 'package.json'), 'utf8'));
|
|
@@ -77,16 +97,175 @@ function getTemplateDir() {
|
|
|
77
97
|
throw new Error(`Template directory not found at ${candidate}. Run \`npm run build\` (or rely on prepack).`);
|
|
78
98
|
}
|
|
79
99
|
|
|
80
|
-
function listShippedFiles(templateDir) {
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
100
|
+
async function listShippedFiles(templateDir) {
|
|
101
|
+
const out = [];
|
|
102
|
+
await walkFiles(templateDir, templateDir, out);
|
|
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));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function walkFiles(dir, base, acc) {
|
|
113
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
114
|
+
const full = join(dir, entry.name);
|
|
115
|
+
if (entry.isDirectory()) await walkFiles(full, base, acc);
|
|
116
|
+
else if (entry.isFile()) acc.push(full.slice(base.length + 1).split('/').join('/'));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
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
|
+
|
|
148
|
+
async function dispatchInstall(target, values, templateDir) {
|
|
149
|
+
const dryRun = !!values['dry-run'];
|
|
150
|
+
if (process.stdout.isTTY && !values.force && !dryRun) {
|
|
151
|
+
return await runBrandedInstall(target, values, templateDir);
|
|
152
|
+
}
|
|
153
|
+
return await runPlainInstall(target, values, templateDir);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function runBrandedInstall(target, values, templateDir) {
|
|
157
|
+
const tui = await import('../src/cli/tui/install.js');
|
|
158
|
+
return tui.run({
|
|
159
|
+
target,
|
|
160
|
+
opts: {
|
|
161
|
+
templateDir,
|
|
162
|
+
noPlantuml: !!values['no-plantuml'],
|
|
163
|
+
requirePlantuml: !!values['require-plantuml'],
|
|
164
|
+
withNpmrc: !!values['with-npmrc'],
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function runPlainInstall(target, values, templateDir) {
|
|
170
|
+
const dryRun = !!values['dry-run'];
|
|
171
|
+
if (values.force) {
|
|
172
|
+
if (!process.stdin.isTTY) {
|
|
173
|
+
await usageError('--force requires an interactive TTY for the confirmation prompt');
|
|
174
|
+
return 2;
|
|
87
175
|
}
|
|
88
|
-
|
|
89
|
-
|
|
176
|
+
if (!dryRun) {
|
|
177
|
+
const answer = await io.ask("type 'overwrite' to proceed: ");
|
|
178
|
+
if (answer.toLowerCase() !== 'overwrite') {
|
|
179
|
+
await usageError('confirmation declined');
|
|
180
|
+
return 1;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
try {
|
|
186
|
+
if (values.force) {
|
|
187
|
+
if (dryRun) io.log(`Would force-install into ${target}`);
|
|
188
|
+
else await forceInstall(templateDir, target, { withNpmrc: !!values['with-npmrc'] });
|
|
189
|
+
} else {
|
|
190
|
+
if (dryRun) io.log(`Would fresh-install into ${target}`);
|
|
191
|
+
else await freshInstall(templateDir, target, { withNpmrc: !!values['with-npmrc'] });
|
|
192
|
+
}
|
|
193
|
+
} catch (err) {
|
|
194
|
+
await usageError(`install failed: ${err.message}`);
|
|
195
|
+
return 1;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (!dryRun) {
|
|
199
|
+
const plantumlExit = await fetchPlantumlPlain(target, values);
|
|
200
|
+
if (plantumlExit !== 0) return plantumlExit;
|
|
201
|
+
io.log(`Installed manifest version 1 to ${target}.`);
|
|
202
|
+
io.log(`Pin via "@friedbotstudio/create-baseline@<exact-version>" in your bootstrap docs.`);
|
|
203
|
+
}
|
|
204
|
+
return 0;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function fetchPlantumlPlain(target, values) {
|
|
208
|
+
const result = await fetchPlantumlIfMissing(target, {
|
|
209
|
+
noPlantuml: values['no-plantuml'],
|
|
210
|
+
requirePlantuml: values['require-plantuml'],
|
|
211
|
+
});
|
|
212
|
+
if (result.outcome === FETCH_OUTCOMES.WARNED_NETWORK_FAILURE
|
|
213
|
+
|| result.outcome === FETCH_OUTCOMES.WARNED_HASH_MISMATCH) {
|
|
214
|
+
io.warn(`PlantUML jar fetch failed (${result.reason}); install continued. Retry with --require-plantuml or set system plantuml on PATH.`);
|
|
215
|
+
return 0;
|
|
216
|
+
}
|
|
217
|
+
if (result.outcome === FETCH_OUTCOMES.ERRORED_REQUIRE_PLANTUML) {
|
|
218
|
+
await usageError(`--require-plantuml: ${result.reason}`);
|
|
219
|
+
return 4;
|
|
220
|
+
}
|
|
221
|
+
return 0;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
async function dispatchUpgrade(target, values, templateDir) {
|
|
225
|
+
const manifestPath = join(target, '.claude/.baseline-manifest.json');
|
|
226
|
+
if (!existsSync(manifestPath)) {
|
|
227
|
+
await usageError(`No baseline manifest at ${manifestPath}. Run a fresh install first.`);
|
|
228
|
+
return 2;
|
|
229
|
+
}
|
|
230
|
+
if (process.stdout.isTTY) {
|
|
231
|
+
const tui = await import('../src/cli/tui/upgrade.js');
|
|
232
|
+
return tui.run({
|
|
233
|
+
target,
|
|
234
|
+
opts: { templateDir, dryRun: !!values['dry-run'] },
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return await runPlainUpgrade(target, values, templateDir, manifestPath);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function runPlainUpgrade(target, values, templateDir, manifestPath) {
|
|
241
|
+
const oldManifest = await loadManifest(manifestPath);
|
|
242
|
+
const tplFiles = await listShippedFiles(templateDir);
|
|
243
|
+
const newManifest = await buildManifestFromDir(templateDir, tplFiles);
|
|
244
|
+
if (values['dry-run']) {
|
|
245
|
+
io.log(`Would upgrade ${tplFiles.length} files into ${target}`);
|
|
246
|
+
return 0;
|
|
247
|
+
}
|
|
248
|
+
const report = await threeWayMerge(templateDir, target, oldManifest, newManifest);
|
|
249
|
+
for (const action of report.actions) {
|
|
250
|
+
io.log(`${action.kind.padEnd(24)} ${action.path}`);
|
|
251
|
+
}
|
|
252
|
+
return report.exitCode;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function dispatchDoctor(positionals, values) {
|
|
256
|
+
const target = resolve(positionals[1] ?? '.');
|
|
257
|
+
const report = await runDoctor(target, { strict: !!values.strict });
|
|
258
|
+
if (values.json) {
|
|
259
|
+
io.log(JSON.stringify(report));
|
|
260
|
+
return report.exitCode;
|
|
261
|
+
}
|
|
262
|
+
if (process.stdout.isTTY) {
|
|
263
|
+
const tui = await import('../src/cli/tui/doctor.js');
|
|
264
|
+
tui.render(report);
|
|
265
|
+
} else {
|
|
266
|
+
process.stdout.write(formatReport(report));
|
|
267
|
+
}
|
|
268
|
+
return report.exitCode;
|
|
90
269
|
}
|
|
91
270
|
|
|
92
271
|
async function main(argv) {
|
|
@@ -94,157 +273,103 @@ async function main(argv) {
|
|
|
94
273
|
try {
|
|
95
274
|
parsed = parseArgs({
|
|
96
275
|
args: argv.slice(2),
|
|
97
|
-
options:
|
|
98
|
-
help: { type: 'boolean', short: 'h' },
|
|
99
|
-
version: { type: 'boolean' },
|
|
100
|
-
force: { type: 'boolean' },
|
|
101
|
-
merge: { type: 'boolean' },
|
|
102
|
-
'dry-run': { type: 'boolean' },
|
|
103
|
-
'no-plantuml': { type: 'boolean' },
|
|
104
|
-
'require-plantuml': { type: 'boolean' },
|
|
105
|
-
'with-npmrc': { type: 'boolean' },
|
|
106
|
-
strict: { type: 'boolean' },
|
|
107
|
-
},
|
|
276
|
+
options: OPTIONS,
|
|
108
277
|
strict: true,
|
|
109
278
|
allowPositionals: true,
|
|
110
279
|
});
|
|
111
280
|
} catch (err) {
|
|
112
|
-
|
|
281
|
+
if (/--merge/.test(err.message)) {
|
|
282
|
+
await usageError('--merge has been removed; use `create-baseline upgrade <target>` instead.');
|
|
283
|
+
return 2;
|
|
284
|
+
}
|
|
285
|
+
await usageError(friendlyParseArgsMessage(err.message));
|
|
113
286
|
return 2;
|
|
114
287
|
}
|
|
115
288
|
|
|
116
289
|
const { values, positionals } = parsed;
|
|
117
290
|
|
|
118
291
|
if (values.help) {
|
|
119
|
-
|
|
292
|
+
const version = await readPackageVersion();
|
|
293
|
+
if (process.stdout.isTTY) {
|
|
294
|
+
const meta = await import('../src/cli/tui/meta.js');
|
|
295
|
+
meta.renderHelp(HELP_TEXT, version);
|
|
296
|
+
} else {
|
|
297
|
+
io.log(HELP_TEXT);
|
|
298
|
+
}
|
|
120
299
|
return 0;
|
|
121
300
|
}
|
|
122
301
|
if (values.version) {
|
|
123
|
-
|
|
302
|
+
const version = await readPackageVersion();
|
|
303
|
+
if (process.stdout.isTTY) {
|
|
304
|
+
const meta = await import('../src/cli/tui/meta.js');
|
|
305
|
+
meta.renderVersion(version);
|
|
306
|
+
} else {
|
|
307
|
+
io.log(version);
|
|
308
|
+
}
|
|
124
309
|
return 0;
|
|
125
310
|
}
|
|
126
311
|
|
|
127
|
-
// `doctor` subcommand: read-only drift check against an installed target's manifest.
|
|
128
312
|
if (positionals[0] === 'doctor') {
|
|
129
|
-
|
|
130
|
-
const report = await runDoctor(target, { strict: !!values.strict });
|
|
131
|
-
io.log(formatReport(report));
|
|
132
|
-
return report.exitCode;
|
|
313
|
+
return await dispatchDoctor(positionals, values);
|
|
133
314
|
}
|
|
134
315
|
|
|
135
|
-
if (
|
|
136
|
-
|
|
137
|
-
|
|
316
|
+
if (positionals[0] === 'upgrade') {
|
|
317
|
+
const target = resolve(positionals[1] ?? '.');
|
|
318
|
+
let templateDir;
|
|
319
|
+
try {
|
|
320
|
+
templateDir = getTemplateDir();
|
|
321
|
+
} catch (err) {
|
|
322
|
+
await usageError(err.message);
|
|
323
|
+
return 2;
|
|
324
|
+
}
|
|
325
|
+
return await dispatchUpgrade(target, values, templateDir);
|
|
138
326
|
}
|
|
327
|
+
|
|
139
328
|
if (values['no-plantuml'] && values['require-plantuml']) {
|
|
140
|
-
|
|
329
|
+
await usageError('--no-plantuml and --require-plantuml are mutually exclusive');
|
|
141
330
|
return 2;
|
|
142
331
|
}
|
|
143
332
|
if (positionals.length === 0) {
|
|
144
|
-
|
|
145
|
-
|
|
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');
|
|
146
346
|
return 2;
|
|
147
347
|
}
|
|
148
348
|
if (positionals.length > 1) {
|
|
149
|
-
|
|
349
|
+
await usageError(`unexpected positional arguments: ${positionals.slice(1).join(', ')}`);
|
|
150
350
|
return 2;
|
|
151
351
|
}
|
|
152
352
|
|
|
153
353
|
const target = resolve(positionals[0]);
|
|
154
|
-
const dryRun = !!values['dry-run'];
|
|
155
|
-
|
|
156
354
|
let templateDir;
|
|
157
355
|
try {
|
|
158
356
|
templateDir = getTemplateDir();
|
|
159
357
|
} catch (err) {
|
|
160
|
-
|
|
358
|
+
await usageError(err.message);
|
|
161
359
|
return 2;
|
|
162
360
|
}
|
|
163
361
|
|
|
164
362
|
const sentinels = await scanSentinels(target);
|
|
165
363
|
const hasConflict = sentinels.length > 0;
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
364
|
+
if (hasConflict && !values.force) {
|
|
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
|
+
);
|
|
170
369
|
return 1;
|
|
171
370
|
}
|
|
172
371
|
|
|
173
|
-
|
|
174
|
-
if (!process.stdin.isTTY) {
|
|
175
|
-
io.error('--force requires an interactive TTY for the confirmation prompt');
|
|
176
|
-
return 2;
|
|
177
|
-
}
|
|
178
|
-
if (!dryRun) {
|
|
179
|
-
const answer = await io.ask("type 'overwrite' to proceed: ");
|
|
180
|
-
if (answer.toLowerCase() !== 'overwrite') {
|
|
181
|
-
io.error('confirmation declined');
|
|
182
|
-
return 1;
|
|
183
|
-
}
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (values.merge) {
|
|
188
|
-
if (!process.stdin.isTTY && !dryRun) {
|
|
189
|
-
io.error('--merge requires an interactive TTY for the confirmation prompt');
|
|
190
|
-
return 2;
|
|
191
|
-
}
|
|
192
|
-
if (!dryRun) {
|
|
193
|
-
const answer = await io.ask("type 'merge' to proceed: ");
|
|
194
|
-
if (answer.toLowerCase() !== 'merge') {
|
|
195
|
-
io.error('confirmation declined');
|
|
196
|
-
return 1;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
let exitCode = 0;
|
|
202
|
-
try {
|
|
203
|
-
if (values.merge) {
|
|
204
|
-
const oldManifest = await loadManifest(join(target, '.claude/.baseline-manifest.json'));
|
|
205
|
-
const tplFiles = listShippedFiles(templateDir);
|
|
206
|
-
const newManifest = await buildManifestFromDir(templateDir, tplFiles);
|
|
207
|
-
if (dryRun) {
|
|
208
|
-
io.log(`Would merge ${tplFiles.length} files into ${target}`);
|
|
209
|
-
} else {
|
|
210
|
-
const report = await threeWayMerge(templateDir, target, oldManifest, newManifest);
|
|
211
|
-
for (const a of report.actions) {
|
|
212
|
-
io.log(`${a.kind.padEnd(24)} ${a.path}`);
|
|
213
|
-
}
|
|
214
|
-
exitCode = report.exitCode;
|
|
215
|
-
}
|
|
216
|
-
} else if (values.force) {
|
|
217
|
-
if (dryRun) io.log(`Would force-install into ${target}`);
|
|
218
|
-
else await forceInstall(templateDir, target, { withNpmrc: !!values['with-npmrc'] });
|
|
219
|
-
} else {
|
|
220
|
-
if (dryRun) io.log(`Would fresh-install into ${target}`);
|
|
221
|
-
else await freshInstall(templateDir, target, { withNpmrc: !!values['with-npmrc'] });
|
|
222
|
-
}
|
|
223
|
-
} catch (err) {
|
|
224
|
-
io.error(`install failed: ${err.message}`);
|
|
225
|
-
return 1;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
if (!dryRun) {
|
|
229
|
-
const fetchResult = await fetchPlantumlIfMissing(target, {
|
|
230
|
-
noPlantuml: values['no-plantuml'],
|
|
231
|
-
requirePlantuml: values['require-plantuml'],
|
|
232
|
-
});
|
|
233
|
-
if (fetchResult.outcome === FETCH_OUTCOMES.WARNED_NETWORK_FAILURE
|
|
234
|
-
|| fetchResult.outcome === FETCH_OUTCOMES.WARNED_HASH_MISMATCH) {
|
|
235
|
-
io.warn(`PlantUML jar fetch failed (${fetchResult.reason}); install continued. Retry with --require-plantuml or set system plantuml on PATH.`);
|
|
236
|
-
} else if (fetchResult.outcome === FETCH_OUTCOMES.ERRORED_REQUIRE_PLANTUML) {
|
|
237
|
-
io.error(`--require-plantuml: ${fetchResult.reason}`);
|
|
238
|
-
return 4;
|
|
239
|
-
}
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
if (!dryRun && exitCode === 0) {
|
|
243
|
-
io.log(`Installed manifest version 1 to ${target}.`);
|
|
244
|
-
io.log(`Pin via "@friedbotstudio/create-baseline@<exact-version>" in your bootstrap docs.`);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
return exitCode;
|
|
372
|
+
return await dispatchInstall(target, values, templateDir);
|
|
248
373
|
}
|
|
249
374
|
|
|
250
375
|
main(process.argv).then((code) => { process.exit(code); }).catch((err) => {
|
|
@@ -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",
|
|
@@ -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": "
|
|
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": "
|
|
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": "
|
|
227
|
+
".claude/skills/triage/SKILL.md": "499f82a26d77c60af827dd89a7fcb3eae0cd903e34c70740c297bc260aeed38e",
|
|
215
228
|
".claude/skills/verify/SKILL.md": "fcddba67cf7f3623fc8f8ae142303b16b9db0c9fc1652bc920e16c56fe4c7864",
|
|
216
229
|
".mcp.json": "8ebb7966045486187bbdf9bac643e690c4fbc7a9a70a8345e3665ba72fa19b96",
|
|
217
|
-
"CLAUDE.md": "
|
|
218
|
-
"docs/init/seed.md": "
|
|
230
|
+
"CLAUDE.md": "1d87311fa1b177944afe69f060b0ddd31359e5391445034d10dc51ced1503ada",
|
|
231
|
+
"docs/init/seed.md": "63f3a0c09895f2040f7fdc188cb0e759bd6a47d615759ec7c83671798504a71d"
|
|
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-
|
|
274
|
+
"build_id": "gha-26153025904"
|
|
261
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 {}
|