@friedbotstudio/create-baseline 0.4.0 → 0.6.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 +1 -1
- package/bin/cli.js +97 -23
- package/obj/template/.claude/manifest.json +961 -0
- package/obj/template/.claude/skills/audit-baseline/audit.sh +20 -10
- package/obj/template/.claude/skills/upgrade-project/SKILL.md +121 -0
- package/obj/template/CLAUDE.md +9 -8
- package/obj/template/docs/init/seed.md +6 -6
- package/package.json +1 -1
- package/src/CLAUDE.template.md +9 -8
- package/src/cli/diff-render.js +54 -0
- package/src/cli/install.js +40 -11
- package/src/cli/manifest.js +7 -3
- package/src/cli/merge.js +80 -13
- 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 +138 -22
- package/src/cli/upgrade-tiers.js +234 -0
- package/src/seed.template.md +6 -6
- package/obj/template/manifest.json +0 -275
package/README.md
CHANGED
|
@@ -41,7 +41,7 @@ A discipline layer for Claude Code. Hooks at every tool boundary, a workflow tha
|
|
|
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, **38 skills** organised into ten 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
|
|
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';
|
|
@@ -18,7 +18,7 @@ const PKG_ROOT = resolve(__dirname, '..');
|
|
|
18
18
|
|
|
19
19
|
const HELP_TEXT = `Usage:
|
|
20
20
|
create-baseline <target> [options] install the baseline
|
|
21
|
-
create-baseline upgrade [target] three-
|
|
21
|
+
create-baseline upgrade [target] three-tier merge (mechanical / semantic / binary-prompt)
|
|
22
22
|
create-baseline doctor [target] report drift in an installed target
|
|
23
23
|
|
|
24
24
|
Materializes the Claude Code baseline (.claude/, CLAUDE.md, .mcp.json,
|
|
@@ -31,10 +31,15 @@ Install modes:
|
|
|
31
31
|
|
|
32
32
|
Upgrade:
|
|
33
33
|
Replaces the prior --merge flag. Reads <target>/.claude/.baseline-manifest.json
|
|
34
|
-
and runs a three-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
34
|
+
and runs a three-tier merge against the shipped template:
|
|
35
|
+
- tier 1 (binary prompt): customized files prompt "Keep your version / Use
|
|
36
|
+
new baseline / Show diff" in TTY mode (exit 3 on any skipped).
|
|
37
|
+
- tier 2 (mechanical): files routed through git merge-file --diff3 with
|
|
38
|
+
BASE recovered from .claude/.baseline-prior/ cache or npm fallback;
|
|
39
|
+
clean merges land silently, conflicts surface with markers (exit 4).
|
|
40
|
+
- tier 3 (semantic): staged at .claude/state/upgrade/<ts>/ for the
|
|
41
|
+
/upgrade-project Claude Code skill to reconcile (exit 5).
|
|
42
|
+
Prunes baseline files removed upstream that the user hadn't touched.
|
|
38
43
|
--dry-run Print intended actions without writing.
|
|
39
44
|
|
|
40
45
|
Doctor:
|
|
@@ -100,7 +105,13 @@ function getTemplateDir() {
|
|
|
100
105
|
async function listShippedFiles(templateDir) {
|
|
101
106
|
const out = [];
|
|
102
107
|
await walkFiles(templateDir, templateDir, out);
|
|
103
|
-
|
|
108
|
+
// COPY_EXCLUDE was used to keep the legacy `manifest.json` at the template
|
|
109
|
+
// root from being copied to the consumer target root. The manifest now
|
|
110
|
+
// ships at `.claude/manifest.json` so no path-level exclusion is needed;
|
|
111
|
+
// COPY_EXCLUDE is currently empty but the filter stays so future entries
|
|
112
|
+
// (e.g., dev-only artifacts that accidentally leak into obj/template) can
|
|
113
|
+
// be added in one place. See src/cli/install.js → COPY_EXCLUDE.
|
|
114
|
+
return out.filter((p) => !COPY_EXCLUDE.includes(p));
|
|
104
115
|
}
|
|
105
116
|
|
|
106
117
|
async function walkFiles(dir, base, acc) {
|
|
@@ -111,6 +122,34 @@ async function walkFiles(dir, base, acc) {
|
|
|
111
122
|
}
|
|
112
123
|
}
|
|
113
124
|
|
|
125
|
+
// Renders a usage error and the canonical HELP_TEXT through the branded TUI
|
|
126
|
+
// when stderr is a TTY, falling back to plain `Error: <msg>` + help body
|
|
127
|
+
// otherwise. Every non-success exit from `main()` flows through here so the
|
|
128
|
+
// user always sees usage guidance alongside the failure.
|
|
129
|
+
async function usageError(msg) {
|
|
130
|
+
const version = await readPackageVersion();
|
|
131
|
+
const meta = await import('../src/cli/tui/meta.js');
|
|
132
|
+
meta.renderUsageError(msg, HELP_TEXT, version);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Translates Node's parseArgs error noise into a short, branded message.
|
|
136
|
+
// Node emits e.g. `Unknown option '--upgrade'. To specify a positional ...`
|
|
137
|
+
// which leaks library implementation detail; we collapse it and, where we
|
|
138
|
+
// can identify the user's likely intent (typing `--upgrade` for the
|
|
139
|
+
// `upgrade` subcommand), we hint at the correct shape.
|
|
140
|
+
function friendlyParseArgsMessage(rawMessage) {
|
|
141
|
+
const firstLine = (rawMessage || '').split('\n')[0];
|
|
142
|
+
if (/Unknown option ['"]?--upgrade['"]?/.test(firstLine)) {
|
|
143
|
+
return 'Did you mean `create-baseline upgrade <target>`? `upgrade` is a subcommand, not a flag.';
|
|
144
|
+
}
|
|
145
|
+
if (/Unknown option ['"]?--doctor['"]?/.test(firstLine)) {
|
|
146
|
+
return 'Did you mean `create-baseline doctor <target>`? `doctor` is a subcommand, not a flag.';
|
|
147
|
+
}
|
|
148
|
+
const unknown = /Unknown option ['"]?([^'"]+?)['"]?(?:\.|$)/.exec(firstLine);
|
|
149
|
+
if (unknown) return `Unknown option '${unknown[1]}'.`;
|
|
150
|
+
return firstLine;
|
|
151
|
+
}
|
|
152
|
+
|
|
114
153
|
async function dispatchInstall(target, values, templateDir) {
|
|
115
154
|
const dryRun = !!values['dry-run'];
|
|
116
155
|
if (process.stdout.isTTY && !values.force && !dryRun) {
|
|
@@ -136,13 +175,13 @@ async function runPlainInstall(target, values, templateDir) {
|
|
|
136
175
|
const dryRun = !!values['dry-run'];
|
|
137
176
|
if (values.force) {
|
|
138
177
|
if (!process.stdin.isTTY) {
|
|
139
|
-
|
|
178
|
+
await usageError('--force requires an interactive TTY for the confirmation prompt');
|
|
140
179
|
return 2;
|
|
141
180
|
}
|
|
142
181
|
if (!dryRun) {
|
|
143
182
|
const answer = await io.ask("type 'overwrite' to proceed: ");
|
|
144
183
|
if (answer.toLowerCase() !== 'overwrite') {
|
|
145
|
-
|
|
184
|
+
await usageError('confirmation declined');
|
|
146
185
|
return 1;
|
|
147
186
|
}
|
|
148
187
|
}
|
|
@@ -157,7 +196,7 @@ async function runPlainInstall(target, values, templateDir) {
|
|
|
157
196
|
else await freshInstall(templateDir, target, { withNpmrc: !!values['with-npmrc'] });
|
|
158
197
|
}
|
|
159
198
|
} catch (err) {
|
|
160
|
-
|
|
199
|
+
await usageError(`install failed: ${err.message}`);
|
|
161
200
|
return 1;
|
|
162
201
|
}
|
|
163
202
|
|
|
@@ -181,7 +220,7 @@ async function fetchPlantumlPlain(target, values) {
|
|
|
181
220
|
return 0;
|
|
182
221
|
}
|
|
183
222
|
if (result.outcome === FETCH_OUTCOMES.ERRORED_REQUIRE_PLANTUML) {
|
|
184
|
-
|
|
223
|
+
await usageError(`--require-plantuml: ${result.reason}`);
|
|
185
224
|
return 4;
|
|
186
225
|
}
|
|
187
226
|
return 0;
|
|
@@ -190,9 +229,16 @@ async function fetchPlantumlPlain(target, values) {
|
|
|
190
229
|
async function dispatchUpgrade(target, values, templateDir) {
|
|
191
230
|
const manifestPath = join(target, '.claude/.baseline-manifest.json');
|
|
192
231
|
if (!existsSync(manifestPath)) {
|
|
193
|
-
|
|
232
|
+
await usageError(`No baseline manifest at ${manifestPath}. Run a fresh install first.`);
|
|
194
233
|
return 2;
|
|
195
234
|
}
|
|
235
|
+
const { findPendingStage } = await import('../src/cli/upgrade-tiers.js');
|
|
236
|
+
const pending = await findPendingStage(target);
|
|
237
|
+
if (pending) {
|
|
238
|
+
const fileLines = pending.files.map((f) => ` - ${f}`).join('\n');
|
|
239
|
+
io.log(`Pending semantic-merge stage at ${pending.stage_ts}.\n${pending.files.length} file(s) awaiting reconciliation:\n${fileLines}\nOpen Claude Code and run /upgrade-project to reconcile.`);
|
|
240
|
+
return 5;
|
|
241
|
+
}
|
|
196
242
|
if (process.stdout.isTTY) {
|
|
197
243
|
const tui = await import('../src/cli/tui/upgrade.js');
|
|
198
244
|
return tui.run({
|
|
@@ -207,17 +253,32 @@ async function runPlainUpgrade(target, values, templateDir, manifestPath) {
|
|
|
207
253
|
const oldManifest = await loadManifest(manifestPath);
|
|
208
254
|
const tplFiles = await listShippedFiles(templateDir);
|
|
209
255
|
const newManifest = await buildManifestFromDir(templateDir, tplFiles);
|
|
256
|
+
await overlayShippedTiers(templateDir, newManifest);
|
|
210
257
|
if (values['dry-run']) {
|
|
211
258
|
io.log(`Would upgrade ${tplFiles.length} files into ${target}`);
|
|
212
259
|
return 0;
|
|
213
260
|
}
|
|
214
261
|
const report = await threeWayMerge(templateDir, target, oldManifest, newManifest);
|
|
215
262
|
for (const action of report.actions) {
|
|
216
|
-
io.log(`${action.kind.padEnd(
|
|
263
|
+
io.log(`${action.kind.padEnd(28)} ${action.path}`);
|
|
217
264
|
}
|
|
218
265
|
return report.exitCode;
|
|
219
266
|
}
|
|
220
267
|
|
|
268
|
+
async function overlayShippedTiers(templateDir, newManifest) {
|
|
269
|
+
const shippedPath = join(templateDir, '.claude/manifest.json');
|
|
270
|
+
if (!existsSync(shippedPath)) return;
|
|
271
|
+
const { readFile: rf } = await import('node:fs/promises');
|
|
272
|
+
const shipped = JSON.parse(await rf(shippedPath, 'utf8'));
|
|
273
|
+
if (!shipped?.files) return;
|
|
274
|
+
for (const rel of Object.keys(newManifest.files)) {
|
|
275
|
+
const shippedEntry = shipped.files[rel];
|
|
276
|
+
if (shippedEntry && typeof shippedEntry === 'object' && typeof shippedEntry.tier === 'string') {
|
|
277
|
+
newManifest.files[rel] = { sha256: newManifest.files[rel], tier: shippedEntry.tier };
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
221
282
|
async function dispatchDoctor(positionals, values) {
|
|
222
283
|
const target = resolve(positionals[1] ?? '.');
|
|
223
284
|
const report = await runDoctor(target, { strict: !!values.strict });
|
|
@@ -245,10 +306,10 @@ async function main(argv) {
|
|
|
245
306
|
});
|
|
246
307
|
} catch (err) {
|
|
247
308
|
if (/--merge/.test(err.message)) {
|
|
248
|
-
|
|
309
|
+
await usageError('--merge has been removed; use `create-baseline upgrade <target>` instead.');
|
|
249
310
|
return 2;
|
|
250
311
|
}
|
|
251
|
-
|
|
312
|
+
await usageError(friendlyParseArgsMessage(err.message));
|
|
252
313
|
return 2;
|
|
253
314
|
}
|
|
254
315
|
|
|
@@ -285,23 +346,34 @@ async function main(argv) {
|
|
|
285
346
|
try {
|
|
286
347
|
templateDir = getTemplateDir();
|
|
287
348
|
} catch (err) {
|
|
288
|
-
|
|
349
|
+
await usageError(err.message);
|
|
289
350
|
return 2;
|
|
290
351
|
}
|
|
291
352
|
return await dispatchUpgrade(target, values, templateDir);
|
|
292
353
|
}
|
|
293
354
|
|
|
294
355
|
if (values['no-plantuml'] && values['require-plantuml']) {
|
|
295
|
-
|
|
356
|
+
await usageError('--no-plantuml and --require-plantuml are mutually exclusive');
|
|
296
357
|
return 2;
|
|
297
358
|
}
|
|
298
359
|
if (positionals.length === 0) {
|
|
299
|
-
|
|
300
|
-
|
|
360
|
+
// TTY landing: render the branded splash (skills.sh-style marquee) so the
|
|
361
|
+
// empty invocation reads as "here's what this tool does" instead of an
|
|
362
|
+
// angry error. Non-TTY keeps the strict error+help+exit-2 contract so
|
|
363
|
+
// scripts and CI still detect the missing argument.
|
|
364
|
+
if (process.stdout.isTTY) {
|
|
365
|
+
const splash = await import('../src/cli/tui/splash.js');
|
|
366
|
+
process.stdout.write(splash.renderSplash({
|
|
367
|
+
tryLine: 'npx @friedbotstudio/create-baseline ./my-project',
|
|
368
|
+
discoverUrl: 'https://baseline.friedbotstudio.com/',
|
|
369
|
+
}));
|
|
370
|
+
return 0;
|
|
371
|
+
}
|
|
372
|
+
await usageError('missing required <target> argument');
|
|
301
373
|
return 2;
|
|
302
374
|
}
|
|
303
375
|
if (positionals.length > 1) {
|
|
304
|
-
|
|
376
|
+
await usageError(`unexpected positional arguments: ${positionals.slice(1).join(', ')}`);
|
|
305
377
|
return 2;
|
|
306
378
|
}
|
|
307
379
|
|
|
@@ -310,15 +382,17 @@ async function main(argv) {
|
|
|
310
382
|
try {
|
|
311
383
|
templateDir = getTemplateDir();
|
|
312
384
|
} catch (err) {
|
|
313
|
-
|
|
385
|
+
await usageError(err.message);
|
|
314
386
|
return 2;
|
|
315
387
|
}
|
|
316
388
|
|
|
317
389
|
const sentinels = await scanSentinels(target);
|
|
318
390
|
const hasConflict = sentinels.length > 0;
|
|
319
391
|
if (hasConflict && !values.force) {
|
|
320
|
-
|
|
321
|
-
|
|
392
|
+
await usageError(
|
|
393
|
+
`existing baseline detected at ${target}: ${sentinels.join(', ')}. ` +
|
|
394
|
+
'Pass --force to overwrite or use `create-baseline upgrade <target>` to three-way merge.'
|
|
395
|
+
);
|
|
322
396
|
return 1;
|
|
323
397
|
}
|
|
324
398
|
|