plumbbob 0.8.13 → 0.8.17
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/.claude-plugin/plugin.json +1 -1
- package/dist/lib/agents.js +1 -1
- package/dist/lib/check.js +9 -6
- package/dist/lib/intent.js +20 -0
- package/dist/lib/orient.js +4 -3
- package/dist/verbs/checkpoint.js +30 -11
- package/dist/verbs/finish.js +24 -8
- package/node_modules/checkride/CHANGELOG.md +234 -0
- package/node_modules/checkride/README.md +134 -31
- package/node_modules/checkride/dist/adapters.d.ts +50 -1
- package/node_modules/checkride/dist/adapters.d.ts.map +1 -1
- package/node_modules/checkride/dist/adapters.js +123 -7
- package/node_modules/checkride/dist/adapters.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/hook.d.ts.map +1 -1
- package/node_modules/checkride/dist/agent-setup/hook.js +15 -1
- package/node_modules/checkride/dist/agent-setup/hook.js.map +1 -1
- package/node_modules/checkride/dist/agent-setup/index.d.ts +0 -1
- package/node_modules/checkride/dist/agent-setup/index.d.ts.map +1 -1
- package/node_modules/checkride/dist/agent-setup/index.js.map +1 -1
- package/node_modules/checkride/dist/baseline/fallow.d.ts +64 -0
- package/node_modules/checkride/dist/baseline/fallow.d.ts.map +1 -0
- package/node_modules/checkride/dist/baseline/fallow.js +256 -0
- package/node_modules/checkride/dist/baseline/fallow.js.map +1 -0
- package/node_modules/checkride/dist/baseline/fingerprint.d.ts +4 -6
- package/node_modules/checkride/dist/baseline/fingerprint.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/fingerprint.js +9 -4
- package/node_modules/checkride/dist/baseline/fingerprint.js.map +1 -1
- package/node_modules/checkride/dist/baseline/index.d.ts +4 -5
- package/node_modules/checkride/dist/baseline/index.d.ts.map +1 -1
- package/node_modules/checkride/dist/baseline/index.js +2 -2
- package/node_modules/checkride/dist/baseline/index.js.map +1 -1
- package/node_modules/checkride/dist/{baseline/command.d.ts → baseline-command.d.ts} +11 -9
- package/node_modules/checkride/dist/baseline-command.d.ts.map +1 -0
- package/node_modules/checkride/dist/{baseline/command.js → baseline-command.js} +9 -7
- package/node_modules/checkride/dist/baseline-command.js.map +1 -0
- package/node_modules/checkride/dist/cli.d.ts.map +1 -1
- package/node_modules/checkride/dist/cli.js +95 -8
- package/node_modules/checkride/dist/cli.js.map +1 -1
- package/node_modules/checkride/dist/config.d.ts +37 -12
- package/node_modules/checkride/dist/config.d.ts.map +1 -1
- package/node_modules/checkride/dist/config.js +268 -60
- package/node_modules/checkride/dist/config.js.map +1 -1
- package/node_modules/checkride/dist/digest/digest.d.ts +0 -6
- package/node_modules/checkride/dist/digest/digest.d.ts.map +1 -1
- package/node_modules/checkride/dist/digest/digest.js +3 -3
- package/node_modules/checkride/dist/digest/digest.js.map +1 -1
- package/node_modules/checkride/dist/digest/index.d.ts +1 -2
- package/node_modules/checkride/dist/digest/index.d.ts.map +1 -1
- package/node_modules/checkride/dist/digest/index.js +1 -1
- package/node_modules/checkride/dist/digest/index.js.map +1 -1
- package/node_modules/checkride/dist/doctor.d.ts +14 -1
- package/node_modules/checkride/dist/doctor.d.ts.map +1 -1
- package/node_modules/checkride/dist/doctor.js +124 -55
- package/node_modules/checkride/dist/doctor.js.map +1 -1
- package/node_modules/checkride/dist/init.d.ts +6 -0
- package/node_modules/checkride/dist/init.d.ts.map +1 -1
- package/node_modules/checkride/dist/init.js +347 -121
- package/node_modules/checkride/dist/init.js.map +1 -1
- package/node_modules/checkride/dist/links.d.ts.map +1 -1
- package/node_modules/checkride/dist/links.js +8 -9
- package/node_modules/checkride/dist/links.js.map +1 -1
- package/node_modules/checkride/dist/orchestrator.d.ts +56 -1
- package/node_modules/checkride/dist/orchestrator.d.ts.map +1 -1
- package/node_modules/checkride/dist/orchestrator.js +506 -138
- package/node_modules/checkride/dist/orchestrator.js.map +1 -1
- package/node_modules/checkride/dist/pack.d.ts +76 -0
- package/node_modules/checkride/dist/pack.d.ts.map +1 -0
- package/node_modules/checkride/dist/pack.js +217 -0
- package/node_modules/checkride/dist/pack.js.map +1 -0
- package/node_modules/checkride/dist/pm/translate.d.ts +18 -10
- package/node_modules/checkride/dist/pm/translate.d.ts.map +1 -1
- package/node_modules/checkride/dist/pm/translate.js +27 -13
- package/node_modules/checkride/dist/pm/translate.js.map +1 -1
- package/node_modules/checkride/dist/smoke.d.ts +86 -0
- package/node_modules/checkride/dist/smoke.d.ts.map +1 -0
- package/node_modules/checkride/dist/smoke.js +346 -0
- package/node_modules/checkride/dist/smoke.js.map +1 -0
- package/node_modules/checkride/dist/snippets.d.ts +215 -0
- package/node_modules/checkride/dist/snippets.d.ts.map +1 -0
- package/node_modules/checkride/dist/snippets.js +360 -0
- package/node_modules/checkride/dist/snippets.js.map +1 -0
- package/node_modules/checkride/package.json +4 -2
- package/node_modules/checkride/schema/checkride.config.schema.json +11 -6
- package/package.json +3 -2
- package/skills/pb-build/SKILL.md +7 -0
- package/skills/pb-park/SKILL.md +3 -2
- package/skills/pb-plan/SKILL.md +30 -9
- package/skills/pb-refine/SKILL.md +21 -6
- package/skills/pb-step/SKILL.md +10 -5
- package/skills/pb-verify/SKILL.md +12 -1
- package/templates/intent.md +55 -11
- package/node_modules/checkride/dist/baseline/command.d.ts.map +0 -1
- package/node_modules/checkride/dist/baseline/command.js.map +0 -1
|
@@ -12,14 +12,17 @@
|
|
|
12
12
|
* it afterward.
|
|
13
13
|
*/
|
|
14
14
|
import { existsSync, readFileSync } from 'node:fs';
|
|
15
|
-
import { mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
15
|
+
import { mkdir, readdir, readFile, writeFile } from 'node:fs/promises';
|
|
16
16
|
import { basename, dirname, join } from 'node:path';
|
|
17
17
|
import { fileURLToPath } from 'node:url';
|
|
18
18
|
import { ADAPTERS, SLOTS } from './adapters.js';
|
|
19
|
+
import { planSnippets, selectDocFiles } from './snippets.js';
|
|
19
20
|
import { CLAUDE_SETTINGS_FILE, writeStopHook } from './agent-setup/index.js';
|
|
20
|
-
import { BASELINE_FILE, isFingerprintable
|
|
21
|
+
import { BASELINE_FILE, isFingerprintable } from './baseline/index.js';
|
|
22
|
+
import { runBaseline } from './baseline-command.js';
|
|
21
23
|
import { configSchemaUrl, resolveChecks } from './config.js';
|
|
22
24
|
import { runChecks } from './orchestrator.js';
|
|
25
|
+
import { detectPackageManager } from './pm/index.js';
|
|
23
26
|
const NULL_OUT = { write: () => true };
|
|
24
27
|
/** Run the adopted checks once; a failing (non-skipped) check marks its slot. */
|
|
25
28
|
async function defaultProbe(slots, cwd) {
|
|
@@ -297,11 +300,10 @@ async function writeSharedStatic(w) {
|
|
|
297
300
|
['shared/gitignore', '.gitignore'],
|
|
298
301
|
['shared/npmrc', '.npmrc'],
|
|
299
302
|
];
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
303
|
+
// Independent writes to distinct paths — run them concurrently.
|
|
304
|
+
await Promise.all(map.map(([from, to]) => put(w, to, readTemplate(from))));
|
|
305
|
+
const rules = ['no-class.yml', 'no-default-export.yml', 'no-deep-sibling-import.yml', 'require-js-extension.yml'];
|
|
306
|
+
await Promise.all(rules.map((rule) => put(w, join('rules', rule), readTemplate(join('shared', 'rules', rule)))));
|
|
305
307
|
}
|
|
306
308
|
async function writePackage(w, dir, pkgName, value) {
|
|
307
309
|
await put(w, join(dir, 'package.json'), packageJsonFor(pkgName));
|
|
@@ -327,47 +329,97 @@ async function writeHook(w, hook, skipped) {
|
|
|
327
329
|
// New-project mode
|
|
328
330
|
// ----------------------------------------------------------------------------
|
|
329
331
|
const DEFAULT_ACTIVE_SLOTS = ['types', 'lint', 'struct', 'dead', 'test', 'docs', 'links', 'spell'];
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
const name = options.name ?? (basename(cwd) || 'app');
|
|
333
|
-
const scope = options.scope ?? null;
|
|
334
|
-
const license = options.license ?? 'MIT';
|
|
335
|
-
const author = options.author ?? null;
|
|
336
|
-
const fullName = scope ? `${scope}/${name}` : name;
|
|
337
|
-
const adapters = options.adapters ?? ADAPTERS;
|
|
338
|
-
const slots = options.slots ?? SLOTS;
|
|
339
|
-
const checkrideSpec = options.checkrideSpec ?? `^${productVersion()}`;
|
|
340
|
-
const w = { cwd, dryRun: options.dryRun ?? false, written: [] };
|
|
332
|
+
/** Emit every scaffold file for the shape into `w` (records paths; obeys dryRun). */
|
|
333
|
+
async function writeNewScaffold(w, s) {
|
|
341
334
|
await writeSharedStatic(w);
|
|
342
|
-
await put(w, 'tsconfig.json', render(readTemplate(join(shape, 'tsconfig.json')), { name }));
|
|
343
|
-
await put(w, 'fallow.toml', render(readTemplate(join(shape, 'fallow.toml')), { name }));
|
|
344
|
-
await put(w, 'pnpm-workspace.yaml', render(readTemplate(join(shape, 'pnpm-workspace.yaml')), { name }));
|
|
345
|
-
await put(w, 'cspell.json', cspellWithName(name, scope));
|
|
346
|
-
await put(w, 'package.json', rootPackageJson(fullName, license, author, collectDevDeps(adapters, slots, checkrideSpec), shape !== 'flat'));
|
|
347
|
-
await put(w, 'LICENSE', licenseText(license, author ?? fullName));
|
|
348
|
-
await put(w, 'README.md', readme(name));
|
|
335
|
+
await put(w, 'tsconfig.json', render(readTemplate(join(s.shape, 'tsconfig.json')), { name: s.name }));
|
|
336
|
+
await put(w, 'fallow.toml', render(readTemplate(join(s.shape, 'fallow.toml')), { name: s.name }));
|
|
337
|
+
await put(w, 'pnpm-workspace.yaml', render(readTemplate(join(s.shape, 'pnpm-workspace.yaml')), { name: s.name }));
|
|
338
|
+
await put(w, 'cspell.json', cspellWithName(s.name, s.scope));
|
|
339
|
+
await put(w, 'package.json', rootPackageJson(s.fullName, s.license, s.author, collectDevDeps(s.adapters, s.slots, s.checkrideSpec), s.shape !== 'flat'));
|
|
340
|
+
await put(w, 'LICENSE', licenseText(s.license, s.author ?? s.fullName));
|
|
341
|
+
await put(w, 'README.md', readme(s.name));
|
|
349
342
|
await put(w, 'AGENTS.md', agentsMd(DEFAULT_ACTIVE_SLOTS));
|
|
350
343
|
await put(w, 'CLAUDE.md', claudeMd());
|
|
351
|
-
if (shape === 'flat') {
|
|
352
|
-
await put(w, join('src', 'index.ts'), sourceModule(name));
|
|
353
|
-
await put(w, join('src', 'index.test.ts'), smokeTest(name));
|
|
344
|
+
if (s.shape === 'flat') {
|
|
345
|
+
await put(w, join('src', 'index.ts'), sourceModule(s.name));
|
|
346
|
+
await put(w, join('src', 'index.test.ts'), smokeTest(s.name));
|
|
354
347
|
}
|
|
355
|
-
else if (shape === 'monorepo') {
|
|
356
|
-
await writePackage(w, join('libs', 'core'), scope ? `${scope}/core` : 'core', 'core');
|
|
357
|
-
await writePackage(w, join('apps', name), fullName, name);
|
|
348
|
+
else if (s.shape === 'monorepo') {
|
|
349
|
+
await writePackage(w, join('libs', 'core'), s.scope ? `${s.scope}/core` : 'core', 'core');
|
|
350
|
+
await writePackage(w, join('apps', s.name), s.fullName, s.name);
|
|
358
351
|
}
|
|
359
352
|
else {
|
|
360
|
-
await put(w, join('src', 'index.ts'), sourceModule(name));
|
|
361
|
-
await put(w, join('src', 'index.test.ts'), smokeTest(name));
|
|
362
|
-
await writePackage(w, join('packages', 'core'), scope ? `${scope}/core` : 'core', 'core');
|
|
353
|
+
await put(w, join('src', 'index.ts'), sourceModule(s.name));
|
|
354
|
+
await put(w, join('src', 'index.test.ts'), smokeTest(s.name));
|
|
355
|
+
await writePackage(w, join('packages', 'core'), s.scope ? `${s.scope}/core` : 'core', 'core');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Plan the scaffold on a dry writer and return the paths that already exist on
|
|
360
|
+
* disk — the files `init` would clobber. The Stop hook is excluded: it merges
|
|
361
|
+
* into `.claude/settings.json` idempotently rather than overwriting.
|
|
362
|
+
*/
|
|
363
|
+
async function planCollisions(s, cwd) {
|
|
364
|
+
const plan = { cwd, dryRun: true, written: [] };
|
|
365
|
+
await writeNewScaffold(plan, s);
|
|
366
|
+
return plan.written.filter((rel) => existsSync(join(cwd, rel)));
|
|
367
|
+
}
|
|
368
|
+
/** Resolve the project name, scope, and derived scoped `fullName` for a new project. */
|
|
369
|
+
function resolveProjectName(options, cwd) {
|
|
370
|
+
const name = options.name ?? (basename(cwd) || 'app');
|
|
371
|
+
const scope = options.scope ?? null;
|
|
372
|
+
return { name, scope, fullName: scope ? `${scope}/${name}` : name };
|
|
373
|
+
}
|
|
374
|
+
/** Resolve the options for a new-project scaffold, applying every default. */
|
|
375
|
+
function resolveNewScaffold(options, cwd) {
|
|
376
|
+
const { name, scope, fullName } = resolveProjectName(options, cwd);
|
|
377
|
+
return {
|
|
378
|
+
shape: options.shape ?? 'flat',
|
|
379
|
+
name,
|
|
380
|
+
scope,
|
|
381
|
+
license: options.license ?? 'MIT',
|
|
382
|
+
author: options.author ?? null,
|
|
383
|
+
fullName,
|
|
384
|
+
adapters: options.adapters ?? ADAPTERS,
|
|
385
|
+
slots: options.slots ?? SLOTS,
|
|
386
|
+
// Exact pin, no caret: pre-1.0 minors are breaking (docs/contract.md pin policy).
|
|
387
|
+
checkrideSpec: options.checkrideSpec ?? productVersion(),
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
/**
|
|
391
|
+
* Overwrite protection (D4): unless `force`, refuse rather than clobber any file
|
|
392
|
+
* the scaffold would write. Planning on a dry writer first means a collision
|
|
393
|
+
* bails before a single real byte is written — the refusal writes nothing.
|
|
394
|
+
*/
|
|
395
|
+
async function assertNoCollisions(scaffold, cwd, force) {
|
|
396
|
+
if (force)
|
|
397
|
+
return;
|
|
398
|
+
const collisions = await planCollisions(scaffold, cwd);
|
|
399
|
+
if (collisions.length > 0) {
|
|
400
|
+
throw new Error(`init: refusing to overwrite ${collisions.length} existing file(s) (pass --force to overwrite):\n` +
|
|
401
|
+
collisions.map((c) => ` ${c}`).join('\n'));
|
|
363
402
|
}
|
|
403
|
+
}
|
|
404
|
+
/** Print the new-project summary and next steps. */
|
|
405
|
+
function reportNew(stdout, shape, w, cwd) {
|
|
406
|
+
stdout.write(`checkride init: generated a ${shape} project (${w.written.length} files)${w.dryRun ? ' [dry run]' : ''}.\n`);
|
|
407
|
+
// A fresh project has no lockfile/field yet, so this resolves to `pnpm`,
|
|
408
|
+
// matching the generated scripts.
|
|
409
|
+
const pm = detectPackageManager({ cwd });
|
|
410
|
+
stdout.write(` next: ${pm} install && ${pm} run check\n`);
|
|
411
|
+
}
|
|
412
|
+
async function initNew(options, cwd) {
|
|
413
|
+
const scaffold = resolveNewScaffold(options, cwd);
|
|
414
|
+
await assertNoCollisions(scaffold, cwd, options.force ?? false);
|
|
415
|
+
const w = { cwd, dryRun: options.dryRun ?? false, written: [] };
|
|
416
|
+
await writeNewScaffold(w, scaffold);
|
|
364
417
|
// Claude Code Stop hook (opt-out; step 12). A fresh project has no PM lockfile
|
|
365
418
|
// yet, so it resolves to the `pnpm` default — matching the generated scripts.
|
|
366
419
|
await writeHook(w, options.hook);
|
|
367
|
-
if (options.stdout)
|
|
368
|
-
options.stdout.
|
|
369
|
-
}
|
|
370
|
-
return { mode: 'new', shape, written: w.written, skipped: [], disabled: [], grandfathered: [], exitCode: 0 };
|
|
420
|
+
if (options.stdout)
|
|
421
|
+
reportNew(options.stdout, scaffold.shape, w, cwd);
|
|
422
|
+
return { mode: 'new', shape: scaffold.shape, written: w.written, skipped: [], disabled: [], grandfathered: [], exitCode: 0 };
|
|
371
423
|
}
|
|
372
424
|
// ----------------------------------------------------------------------------
|
|
373
425
|
// Existing-project mode
|
|
@@ -380,6 +432,133 @@ async function readIfExists(path) {
|
|
|
380
432
|
return null;
|
|
381
433
|
}
|
|
382
434
|
}
|
|
435
|
+
// ----------------------------------------------------------------------------
|
|
436
|
+
// Publish-ready bundle (step 9): the library path scaffolds the opt-in artifact
|
|
437
|
+
// checks into `checkride.config.json` so a buildable library orders itself out
|
|
438
|
+
// of the box (build → pack/smoke/attw/publint/snippets, D4).
|
|
439
|
+
// ----------------------------------------------------------------------------
|
|
440
|
+
/**
|
|
441
|
+
* The publish-ready bundle, in wave order. Each slot's value is the adapter
|
|
442
|
+
* `init` writes into `checks`; `build` and `snippets` are gated (see
|
|
443
|
+
* {@link planPublishBundle}). publint/attw/pack/smoke each resolve to the
|
|
444
|
+
* same-named adapter, so their config value is just the slot name.
|
|
445
|
+
*/
|
|
446
|
+
const PUBLISH_BUNDLE_SLOTS = ['build', 'publint', 'attw', 'pack', 'smoke', 'snippets'];
|
|
447
|
+
/** `--add` values that expand to the whole bundle. */
|
|
448
|
+
const PUBLISH_BUNDLE_ALIASES = new Set(['publish', 'bundle']);
|
|
449
|
+
/** Printed (and recorded in `skipped`) when snippets is requested but no tagged fence exists (Q12). */
|
|
450
|
+
const SNIPPETS_POINTER = 'snippets-dist: no tagged fences found — add a `<!-- snippet: check -->` marker above a ' +
|
|
451
|
+
'ts/typescript fence in README.md or docs/*.md to enable the doc-snippet typecheck';
|
|
452
|
+
/**
|
|
453
|
+
* Read package.json for the library path: its script names, and whether it is a
|
|
454
|
+
* publishable library (declares an `exports`/`main` entry and is not private).
|
|
455
|
+
*/
|
|
456
|
+
function readInitManifest(cwd) {
|
|
457
|
+
try {
|
|
458
|
+
const pkg = JSON.parse(readFileSync(join(cwd, 'package.json'), 'utf8'));
|
|
459
|
+
const hasEntry = pkg.exports !== undefined || (typeof pkg.main === 'string' && pkg.main.length > 0);
|
|
460
|
+
return { scripts: new Set(Object.keys(pkg.scripts ?? {})), isLibrary: hasEntry && pkg.private !== true };
|
|
461
|
+
}
|
|
462
|
+
catch {
|
|
463
|
+
return { scripts: new Set(), isLibrary: false };
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
/**
|
|
467
|
+
* True when README.md or a non-recursive docs/*.md carries at least one tagged
|
|
468
|
+
* snippet fence — the signal that gates scaffolding `snippets-dist` (Q12).
|
|
469
|
+
* Reuses step 7's pure discovery/plan primitives so the detection matches what
|
|
470
|
+
* the slot itself checks.
|
|
471
|
+
*/
|
|
472
|
+
async function hasTaggedSnippets(cwd) {
|
|
473
|
+
let docsEntries = [];
|
|
474
|
+
try {
|
|
475
|
+
docsEntries = await readdir(join(cwd, 'docs'));
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
// No docs/ directory — README.md alone can still carry a tagged fence.
|
|
479
|
+
}
|
|
480
|
+
const docs = [];
|
|
481
|
+
for (const relPath of selectDocFiles(docsEntries)) {
|
|
482
|
+
// oxlint-disable-next-line no-await-in-loop -- a handful of docs; sequential keeps it simple.
|
|
483
|
+
const text = await readIfExists(join(cwd, relPath));
|
|
484
|
+
if (text !== null)
|
|
485
|
+
docs.push({ relPath, text });
|
|
486
|
+
}
|
|
487
|
+
return planSnippets(docs).checked > 0;
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Fold the requested publish slots into config entries, gating `build` on a
|
|
491
|
+
* build script and `snippets` (→ `snippets-dist`) on a tagged fence (Q12) — the
|
|
492
|
+
* two slots that would otherwise stand down as a skip (`build`) or hard-error on
|
|
493
|
+
* zero snippets (`snippets`).
|
|
494
|
+
*/
|
|
495
|
+
function planPublishBundle(requested, hasBuildScript, taggedSnippets) {
|
|
496
|
+
const checks = {};
|
|
497
|
+
let snippetsPointer = null;
|
|
498
|
+
for (const slot of PUBLISH_BUNDLE_SLOTS) {
|
|
499
|
+
if (!requested.has(slot))
|
|
500
|
+
continue;
|
|
501
|
+
if (slot === 'build') {
|
|
502
|
+
if (hasBuildScript)
|
|
503
|
+
checks['build'] = 'build';
|
|
504
|
+
}
|
|
505
|
+
else if (slot === 'snippets') {
|
|
506
|
+
if (taggedSnippets)
|
|
507
|
+
checks['snippets'] = 'snippets-dist';
|
|
508
|
+
else
|
|
509
|
+
snippetsPointer = SNIPPETS_POINTER;
|
|
510
|
+
}
|
|
511
|
+
else {
|
|
512
|
+
checks[slot] = slot; // publint / attw / pack / smoke — adapter name == slot name.
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return { checks, snippetsPointer };
|
|
516
|
+
}
|
|
517
|
+
/**
|
|
518
|
+
* Partition the `--add` list and the library signal into (a) the publish slots
|
|
519
|
+
* to scaffold into `checks` and (b) the file-scaffold `--add` names left for
|
|
520
|
+
* {@link addConfigs}. The library path — a buildable, publishable package —
|
|
521
|
+
* requests the whole bundle; `--add` requests a named publish slot, or the
|
|
522
|
+
* whole bundle via the `publish`/`bundle` alias.
|
|
523
|
+
*/
|
|
524
|
+
function splitAdd(add, manifest) {
|
|
525
|
+
const publishRequested = new Set();
|
|
526
|
+
const fileAdds = [];
|
|
527
|
+
if (manifest.isLibrary && manifest.scripts.has('build')) {
|
|
528
|
+
for (const slot of PUBLISH_BUNDLE_SLOTS)
|
|
529
|
+
publishRequested.add(slot);
|
|
530
|
+
}
|
|
531
|
+
const bundleNames = new Set(PUBLISH_BUNDLE_SLOTS);
|
|
532
|
+
for (const name of add) {
|
|
533
|
+
if (PUBLISH_BUNDLE_ALIASES.has(name))
|
|
534
|
+
for (const slot of PUBLISH_BUNDLE_SLOTS)
|
|
535
|
+
publishRequested.add(slot);
|
|
536
|
+
else if (bundleNames.has(name))
|
|
537
|
+
publishRequested.add(name);
|
|
538
|
+
else
|
|
539
|
+
fileAdds.push(name);
|
|
540
|
+
}
|
|
541
|
+
return { publishRequested, fileAdds };
|
|
542
|
+
}
|
|
543
|
+
/**
|
|
544
|
+
* Write (or refresh) the AGENTS.md stanza for `slots`, idempotently: it writes
|
|
545
|
+
* only when the applied stanza differs from the current file, records the outcome
|
|
546
|
+
* on `w`/`skipped`, and honours dry-run. Shared by `initExisting` and
|
|
547
|
+
* `runAgentSetup` (both create-or-refresh the same stanza).
|
|
548
|
+
*/
|
|
549
|
+
async function writeAgentsStanza(w, cwd, slots, skipped) {
|
|
550
|
+
const agentsPath = join(cwd, 'AGENTS.md');
|
|
551
|
+
const existing = await readIfExists(agentsPath);
|
|
552
|
+
const nextAgents = applyStanza(existing ?? '', buildStanza(slots));
|
|
553
|
+
if (nextAgents !== existing) {
|
|
554
|
+
if (!w.dryRun)
|
|
555
|
+
await writeFile(agentsPath, nextAgents);
|
|
556
|
+
w.written.push(existing === null ? 'AGENTS.md' : 'AGENTS.md (refreshed stanza)');
|
|
557
|
+
}
|
|
558
|
+
else {
|
|
559
|
+
skipped.push('AGENTS.md (stanza unchanged)');
|
|
560
|
+
}
|
|
561
|
+
}
|
|
383
562
|
// Blessed-default config files `--add <slot>` scaffolds for an empty slot.
|
|
384
563
|
// Shape-specific slots (types, dead) use the flat variant — existing repos
|
|
385
564
|
// adopting checkride incrementally are overwhelmingly single-package.
|
|
@@ -395,6 +574,9 @@ const ADD_CONFIGS = {
|
|
|
395
574
|
['shared/rules/require-js-extension.yml', 'rules/require-js-extension.yml'],
|
|
396
575
|
],
|
|
397
576
|
dead: [['flat/fallow.toml', 'fallow.toml']],
|
|
577
|
+
// dupes/health are the same fallow tool + config as `dead`, just other slots.
|
|
578
|
+
dupes: [['flat/fallow.toml', 'fallow.toml']],
|
|
579
|
+
health: [['flat/fallow.toml', 'fallow.toml']],
|
|
398
580
|
test: [['shared/vitest.config.ts.template', 'vitest.config.ts']],
|
|
399
581
|
docs: [['shared/markdownlint-cli2.jsonc', '.markdownlint-cli2.jsonc']],
|
|
400
582
|
spell: [['shared/cspell.json', 'cspell.json']],
|
|
@@ -410,6 +592,7 @@ async function addConfigs(w, add, skipped) {
|
|
|
410
592
|
for (const [from, to] of files) {
|
|
411
593
|
if (existsSync(join(w.cwd, to)))
|
|
412
594
|
skipped.push(`${to} (exists)`);
|
|
595
|
+
// oxlint-disable-next-line no-await-in-loop -- one-shot `--add` scaffolding: writes are independent and perf-irrelevant, kept sequential for a deterministic progress list.
|
|
413
596
|
else
|
|
414
597
|
await put(w, to, readTemplate(from));
|
|
415
598
|
}
|
|
@@ -421,7 +604,17 @@ async function addCheckAlias(w, skipped) {
|
|
|
421
604
|
const raw = await readIfExists(pkgPath);
|
|
422
605
|
if (raw === null)
|
|
423
606
|
return;
|
|
424
|
-
|
|
607
|
+
let pkg;
|
|
608
|
+
try {
|
|
609
|
+
pkg = JSON.parse(raw);
|
|
610
|
+
}
|
|
611
|
+
catch (err) {
|
|
612
|
+
// Name the file so a malformed consumer package.json reads as
|
|
613
|
+
// `invalid package.json: <reason>`, not a bare SyntaxError stack
|
|
614
|
+
// (mirrors `invalidConfig` in `config.ts`).
|
|
615
|
+
const reason = err instanceof Error ? err.message : String(err);
|
|
616
|
+
throw new Error(`invalid package.json: ${reason}`, { cause: err });
|
|
617
|
+
}
|
|
425
618
|
if (pkg.scripts?.['check']) {
|
|
426
619
|
skipped.push('package.json (check script exists)');
|
|
427
620
|
return;
|
|
@@ -431,55 +624,57 @@ async function addCheckAlias(w, skipped) {
|
|
|
431
624
|
await writeFile(pkgPath, `${JSON.stringify(pkg, null, 2)}\n`);
|
|
432
625
|
w.written.push('package.json (added check script)');
|
|
433
626
|
}
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
const items = inventory({ cwd, slots, adapters });
|
|
443
|
-
const adopted = items.filter((i) => i.status === 'adopted');
|
|
444
|
-
// Run each adopted check once to see what currently fails.
|
|
445
|
-
const probe = options.probeFailures ?? defaultProbe;
|
|
446
|
-
const failing = new Set(await probe(adopted.map((i) => i.slot), cwd));
|
|
447
|
-
// Adoption path for the failures. `--baseline` (step 6 / c10) grandfathers a
|
|
448
|
-
// failing *fingerprintable* slot into checkride.baseline.json and keeps it
|
|
449
|
-
// enabled — the baseline masks its current debt on the next run, so it's the
|
|
450
|
-
// ratchet, not a disabled slot, that carries the cleanup. A failing slot with
|
|
451
|
-
// no extractor can't be grandfathered, so it still falls back to a `false`
|
|
452
|
-
// disable. Without `--baseline`, every failing slot is disabled (the original
|
|
453
|
-
// step-3 behavior): the first `pnpm check` is green-ish, re-enable as you fix.
|
|
454
|
-
const useBaseline = options.baseline ?? false;
|
|
627
|
+
/**
|
|
628
|
+
* Classify each failing adopted slot. `--baseline` (step 6 / c10) grandfathers a
|
|
629
|
+
* failing *fingerprintable* slot (kept enabled; the baseline masks its debt and
|
|
630
|
+
* the ratchet carries the cleanup); a failing slot with no extractor can't be
|
|
631
|
+
* grandfathered, so it still falls back to a `false` disable. Without
|
|
632
|
+
* `--baseline`, every failing slot is disabled (re-enable as you fix).
|
|
633
|
+
*/
|
|
634
|
+
function resolveAdoptionPlan(adopted, failing, useBaseline) {
|
|
455
635
|
const grandfathered = useBaseline
|
|
456
|
-
? adopted
|
|
636
|
+
? adopted
|
|
637
|
+
.filter((i) => failing.has(i.slot) && i.adapter !== null && isFingerprintable(i.adapter))
|
|
638
|
+
.map((i) => i.slot)
|
|
457
639
|
: [];
|
|
458
640
|
const grandfatheredSet = new Set(grandfathered);
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
await put(w, 'checkride.config.json', `${JSON.stringify(config, null, 2)}\n`);
|
|
478
|
-
}
|
|
479
|
-
else {
|
|
641
|
+
const disabled = adopted
|
|
642
|
+
.filter((i) => failing.has(i.slot) && !grandfatheredSet.has(i.slot))
|
|
643
|
+
.map((i) => i.slot);
|
|
644
|
+
return { grandfathered, disabled };
|
|
645
|
+
}
|
|
646
|
+
/** Capture a fresh baseline when any slot was grandfathered (a no-op on dry-run). */
|
|
647
|
+
async function captureBaselineIfNeeded(options, cwd, grandfathered, dryRun) {
|
|
648
|
+
if (grandfathered.length === 0 || dryRun)
|
|
649
|
+
return;
|
|
650
|
+
const capture = options.captureBaseline ?? ((at) => runBaseline({ cwd: at }).then(() => undefined));
|
|
651
|
+
await capture(cwd);
|
|
652
|
+
}
|
|
653
|
+
/**
|
|
654
|
+
* Write checkride.config.json for the adopted tools (disabled failures as
|
|
655
|
+
* `false`) plus the publish-ready bundle (step 9); never clobber an existing one.
|
|
656
|
+
*/
|
|
657
|
+
async function writeExistingConfig(w, cwd, adopted, disabledSet, bundleChecks, skipped) {
|
|
658
|
+
if (existsSync(join(cwd, 'checkride.config.json'))) {
|
|
480
659
|
skipped.push('checkride.config.json (exists)');
|
|
660
|
+
return;
|
|
481
661
|
}
|
|
482
|
-
|
|
662
|
+
const checks = {};
|
|
663
|
+
for (const i of adopted) {
|
|
664
|
+
if (disabledSet.has(i.slot))
|
|
665
|
+
checks[i.slot] = false;
|
|
666
|
+
else if (i.adapter)
|
|
667
|
+
checks[i.slot] = i.adapter;
|
|
668
|
+
}
|
|
669
|
+
// The opt-in publish bundle follows the adopted default slots; explicit
|
|
670
|
+
// entries opt these slots into the run (D4 — the library orders itself).
|
|
671
|
+
for (const [slot, adapter] of Object.entries(bundleChecks))
|
|
672
|
+
checks[slot] = adapter;
|
|
673
|
+
const config = { $schema: configSchemaUrl(productVersion()), checks };
|
|
674
|
+
await put(w, 'checkride.config.json', `${JSON.stringify(config, null, 2)}\n`);
|
|
675
|
+
}
|
|
676
|
+
/** Ensure `.check/` is gitignored: create the file, append to it, or note it's already there. */
|
|
677
|
+
async function ensureGitignore(w, cwd, skipped) {
|
|
483
678
|
const gitignorePath = join(cwd, '.gitignore');
|
|
484
679
|
const gi = await readIfExists(gitignorePath);
|
|
485
680
|
if (gi === null) {
|
|
@@ -493,39 +688,80 @@ async function initExisting(options, cwd) {
|
|
|
493
688
|
else {
|
|
494
689
|
skipped.push('.gitignore (.check/ already ignored)');
|
|
495
690
|
}
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
const nextAgents = applyStanza(existing ?? '', stanza);
|
|
503
|
-
if (nextAgents !== existing) {
|
|
504
|
-
if (!w.dryRun)
|
|
505
|
-
await writeFile(agentsPath, nextAgents);
|
|
506
|
-
w.written.push(existing === null ? 'AGENTS.md' : 'AGENTS.md (refreshed stanza)');
|
|
507
|
-
}
|
|
508
|
-
else {
|
|
509
|
-
skipped.push('AGENTS.md (stanza unchanged)');
|
|
691
|
+
}
|
|
692
|
+
/** Write the CLAUDE.md pointer if absent. */
|
|
693
|
+
async function writeClaudePointer(w, cwd, skipped) {
|
|
694
|
+
if (existsSync(join(cwd, 'CLAUDE.md'))) {
|
|
695
|
+
skipped.push('CLAUDE.md (exists)');
|
|
696
|
+
return;
|
|
510
697
|
}
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
698
|
+
await put(w, 'CLAUDE.md', claudeMd());
|
|
699
|
+
}
|
|
700
|
+
/** Print the existing-repo adoption summary (adopted count + grandfathered/disabled slots). */
|
|
701
|
+
function reportExisting(stdout, adopted, w, grandfathered, disabled) {
|
|
702
|
+
stdout.write(`checkride init: adopted ${adopted.length} slot(s); wrote ${w.written.length} file(s)${w.dryRun ? ' [dry run]' : ''}.\n`);
|
|
703
|
+
if (grandfathered.length > 0) {
|
|
704
|
+
stdout.write(` grandfathered failing slots into ${BASELINE_FILE}: ${grandfathered.join(', ')}\n`);
|
|
515
705
|
}
|
|
516
|
-
|
|
517
|
-
|
|
706
|
+
if (disabled.length > 0) {
|
|
707
|
+
stdout.write(` disabled failing slots (enable as you fix): ${disabled.join(', ')}\n`);
|
|
518
708
|
}
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* Plan the publish-ready bundle (step 9) for existing-repo init. Only meaningful
|
|
712
|
+
* when a config will actually be written: `writeExistingConfig` never clobbers an
|
|
713
|
+
* existing `checkride.config.json`, so an existing config means the bundle has
|
|
714
|
+
* nowhere to land. The bundle is scaffolded *enabled* — not probed/disabled like
|
|
715
|
+
* adopted tools — because it is an ordered pipeline (build → artifacts) the user
|
|
716
|
+
* opts into by naming it.
|
|
717
|
+
*/
|
|
718
|
+
async function planInitBundle(publishRequested, manifest, cwd) {
|
|
719
|
+
if (existsSync(join(cwd, 'checkride.config.json')))
|
|
720
|
+
return { checks: {}, snippetsPointer: null };
|
|
721
|
+
const wantsSnippets = publishRequested.has('snippets');
|
|
722
|
+
return planPublishBundle(publishRequested, manifest.scripts.has('build'), wantsSnippets ? await hasTaggedSnippets(cwd) : false);
|
|
723
|
+
}
|
|
724
|
+
/** Append the enabled publish-bundle slots (and any snippets pointer) to the summary. */
|
|
725
|
+
function reportInitBundle(stdout, bundle) {
|
|
726
|
+
const enabled = Object.keys(bundle.checks);
|
|
727
|
+
if (enabled.length > 0)
|
|
728
|
+
stdout.write(` enabled publish bundle: ${enabled.join(', ')}\n`);
|
|
729
|
+
if (bundle.snippetsPointer)
|
|
730
|
+
stdout.write(` ${bundle.snippetsPointer}\n`);
|
|
731
|
+
}
|
|
732
|
+
async function initExisting(options, cwd) {
|
|
733
|
+
const adapters = options.adapters ?? ADAPTERS;
|
|
734
|
+
const slots = options.slots ?? SLOTS;
|
|
735
|
+
const manifest = readInitManifest(cwd);
|
|
736
|
+
const w = { cwd, dryRun: options.dryRun ?? false, written: [] };
|
|
737
|
+
const skipped = [];
|
|
738
|
+
// Route --add: publish slots (and the library path) opt into the bundle; the
|
|
739
|
+
// rest scaffold blessed configs before inventory, so they're detected this run.
|
|
740
|
+
const { publishRequested, fileAdds } = splitAdd(options.add ?? [], manifest);
|
|
741
|
+
await addConfigs(w, fileAdds, skipped);
|
|
742
|
+
const items = inventory({ cwd, slots, adapters });
|
|
743
|
+
const adopted = items.filter((i) => i.status === 'adopted');
|
|
744
|
+
// Run each adopted check once to see what currently fails.
|
|
745
|
+
const probe = options.probeFailures ?? defaultProbe;
|
|
746
|
+
const failing = new Set(await probe(adopted.map((i) => i.slot), cwd));
|
|
747
|
+
const { grandfathered, disabled } = resolveAdoptionPlan(adopted, failing, options.baseline ?? false);
|
|
748
|
+
await captureBaselineIfNeeded(options, cwd, grandfathered, w.dryRun);
|
|
749
|
+
// Publish-ready bundle (step 9): planned only when a config will be written.
|
|
750
|
+
const bundle = await planInitBundle(publishRequested, manifest, cwd);
|
|
751
|
+
if (bundle.snippetsPointer)
|
|
752
|
+
skipped.push(bundle.snippetsPointer);
|
|
753
|
+
await writeExistingConfig(w, cwd, adopted, new Set(disabled), bundle.checks, skipped);
|
|
754
|
+
await ensureGitignore(w, cwd, skipped);
|
|
755
|
+
// package.json: add the `check: checkride` alias (decision 8) if missing.
|
|
756
|
+
await addCheckAlias(w, skipped);
|
|
757
|
+
// AGENTS.md stanza (create or refresh, idempotent).
|
|
758
|
+
await writeAgentsStanza(w, cwd, adopted.map((i) => i.slot), skipped);
|
|
759
|
+
await writeClaudePointer(w, cwd, skipped);
|
|
519
760
|
// Claude Code Stop hook (opt-out; step 12), using the repo's detected PM (b7).
|
|
520
761
|
await writeHook(w, options.hook, skipped);
|
|
521
762
|
if (options.stdout) {
|
|
522
|
-
options.stdout
|
|
523
|
-
|
|
524
|
-
options.stdout.write(` grandfathered failing slots into ${BASELINE_FILE}: ${grandfathered.join(', ')}\n`);
|
|
525
|
-
}
|
|
526
|
-
if (disabled.length > 0) {
|
|
527
|
-
options.stdout.write(` disabled failing slots (enable as you fix): ${disabled.join(', ')}\n`);
|
|
528
|
-
}
|
|
763
|
+
reportExisting(options.stdout, adopted, w, grandfathered, disabled);
|
|
764
|
+
reportInitBundle(options.stdout, bundle);
|
|
529
765
|
}
|
|
530
766
|
return { mode: 'existing', shape: null, written: w.written, skipped, disabled, grandfathered, exitCode: 0 };
|
|
531
767
|
}
|
|
@@ -558,17 +794,7 @@ export async function runAgentSetup(options) {
|
|
|
558
794
|
await addCheckAlias(w, skipped);
|
|
559
795
|
// AGENTS.md stanza for the currently-adopted slots (create or refresh).
|
|
560
796
|
const adopted = inventory({ cwd, slots, adapters }).filter((i) => i.status === 'adopted');
|
|
561
|
-
|
|
562
|
-
const existing = await readIfExists(agentsPath);
|
|
563
|
-
const nextAgents = applyStanza(existing ?? '', buildStanza(adopted.map((i) => i.slot)));
|
|
564
|
-
if (nextAgents !== existing) {
|
|
565
|
-
if (!w.dryRun)
|
|
566
|
-
await writeFile(agentsPath, nextAgents);
|
|
567
|
-
w.written.push(existing === null ? 'AGENTS.md' : 'AGENTS.md (refreshed stanza)');
|
|
568
|
-
}
|
|
569
|
-
else {
|
|
570
|
-
skipped.push('AGENTS.md (stanza unchanged)');
|
|
571
|
-
}
|
|
797
|
+
await writeAgentsStanza(w, cwd, adopted.map((i) => i.slot), skipped);
|
|
572
798
|
await writeHook(w, options.hook, skipped);
|
|
573
799
|
if (options.stdout) {
|
|
574
800
|
options.stdout.write(`checkride agent-setup: wrote ${w.written.length} file(s)${w.dryRun ? ' [dry run]' : ''}.\n`);
|