nitm-ai-dev-toolkit 5.1.1 → 5.1.3
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/CHANGELOG.md +42 -0
- package/README.md +91 -58
- package/bin/cli.js +175 -43
- package/bin/relevance.js +306 -0
- package/package.json +1 -1
- package/src/monorepo/.agents/skills/factory/SKILL.md +2 -1
- package/src/monorepo/.agents/skills/using-git-worktrees/SKILL.md +8 -11
- package/src/monorepo/.claude/prompt-snippets/bootstrap-patch.md +30 -14
- package/src/monorepo/.claude/prompt-snippets/bootstrap-upgrade.md +9 -4
- package/src/monorepo/.claude/prompt-snippets/bootstrap.md +50 -25
- package/src/monorepo/.claude/skills/README.md +7 -7
- package/src/monorepo/.cursor/skills/README.md +8 -8
- package/src/monorepo/.github/skills/README.md +4 -4
- package/src/monorepo/.opencode/commands/review-staged.md +4 -0
- package/src/monorepo/factory/README.md +2 -0
- package/src/repo/.agents/skills/factory/SKILL.md +2 -1
- package/src/repo/.agents/skills/using-git-worktrees/SKILL.md +8 -11
- package/src/repo/.claude/prompt-snippets/bootstrap-patch.md +30 -14
- package/src/repo/.claude/prompt-snippets/bootstrap-upgrade.md +9 -6
- package/src/repo/.claude/prompt-snippets/bootstrap.md +52 -29
- package/src/repo/.claude/skills/README.md +7 -7
- package/src/repo/.cursor/skills/README.md +8 -8
- package/src/repo/.github/skills/README.md +4 -4
- package/src/repo/.opencode/commands/review-staged.md +4 -0
- package/src/repo/factory/README.md +2 -0
package/bin/cli.js
CHANGED
|
@@ -13,6 +13,7 @@ const { execFileSync, spawnSync } = require('child_process');
|
|
|
13
13
|
const factory = require('./factory-watch.js');
|
|
14
14
|
const factoryArchive = require('./factory-archive.js');
|
|
15
15
|
const factoryPrune = require('./factory-prune.js');
|
|
16
|
+
const relevance = require('./relevance.js');
|
|
16
17
|
|
|
17
18
|
const PKG_ROOT = path.join(__dirname, '..');
|
|
18
19
|
const SRC_DIR = path.join(PKG_ROOT, 'src');
|
|
@@ -118,6 +119,10 @@ const KNOWN_HISTORICAL = {
|
|
|
118
119
|
// First release with --dry-run, --exclude and --only. `upgrade` refuses a dry run below it (older
|
|
119
120
|
// releases would write), and refuses a selection below it (older releases would ignore it).
|
|
120
121
|
const DRY_RUN_UPGRADE_MIN = '5.0.1';
|
|
122
|
+
// First release whose patch and install honour .nitm/retired.json and the relevance plan. Upgrade refuses
|
|
123
|
+
// older targets while that file has entries or a tracker override, because those targets would ignore it.
|
|
124
|
+
// Set to the version that ships this change; keep in step with the CHANGELOG [Unreleased] release.
|
|
125
|
+
const RELEVANCE_MIN = '5.2.0';
|
|
121
126
|
const VERSION_RE = /^\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?$/;
|
|
122
127
|
|
|
123
128
|
const C = {
|
|
@@ -306,30 +311,66 @@ function blockerOf(cwd, dest) {
|
|
|
306
311
|
return null;
|
|
307
312
|
}
|
|
308
313
|
|
|
309
|
-
// One entry per template file the command touches.
|
|
310
|
-
// add:
|
|
311
|
-
//
|
|
312
|
-
//
|
|
313
|
-
//
|
|
314
|
-
|
|
314
|
+
// One entry per template file the command touches. `action`:
|
|
315
|
+
// add: missing in the target and applies to this project (patch)
|
|
316
|
+
// update: patch refreshes a file the toolkit wrote and the user never changed (manifest hash matches)
|
|
317
|
+
// overwrite: install --force on an existing file
|
|
318
|
+
// skip: identical to the template, already current
|
|
319
|
+
// review: locally changed, or missing while relevance is undecided; kept and reported, never overwritten
|
|
320
|
+
// irrelevant: does not apply to this project (missing: not added; present: kept, reported)
|
|
321
|
+
// retired: listed in .nitm/retired.json; never added, updated or overwritten
|
|
322
|
+
// conflict: install without --force on an existing file, or a path blocked by a file/dir of another type
|
|
323
|
+
// `retired` and `facts` may be passed in; otherwise they are read from the project.
|
|
324
|
+
function planFiles(type, cwd, { mode, force, env, selection, retired, facts }) {
|
|
325
|
+
const retiredConfig = retired || relevance.readRetired(cwd);
|
|
326
|
+
const projectFacts = mode === 'patch' ? (facts || relevance.detectProject(cwd, retiredConfig)) : null;
|
|
327
|
+
const manifest = mode === 'patch' ? readManifest(cwd).files : {};
|
|
315
328
|
const rels = scopeFiles(templateFiles(type), env).filter((rel) => selectionKeeps(rel, selection));
|
|
316
329
|
return rels.map((rel) => {
|
|
317
330
|
const dest = installName(rel);
|
|
318
331
|
const from = path.join(SRC_DIR, type, rel);
|
|
319
332
|
const hash = fileHash(from);
|
|
333
|
+
const key = dest.split(path.sep).join('/');
|
|
334
|
+
const retirement = relevance.retiredFor(key, retiredConfig.entries);
|
|
335
|
+
if (retirement) {
|
|
336
|
+
return { rel: dest, from, hash, action: 'retired', reason: `${retirement.reason} (retired ${retirement.date})` };
|
|
337
|
+
}
|
|
320
338
|
const blocker = blockerOf(cwd, dest);
|
|
321
339
|
if (blocker) return { rel: dest, from, hash, action: 'conflict', reason: `blocked by ${blocker}` };
|
|
322
340
|
const to = path.join(cwd, dest);
|
|
323
|
-
if (
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
341
|
+
if (mode === 'install') {
|
|
342
|
+
if (!fs.existsSync(to)) return { rel: dest, from, hash, action: 'add', same: false };
|
|
343
|
+
const same = fileHash(to) === hash;
|
|
344
|
+
if (!force) return { rel: dest, from, hash, action: 'conflict', reason: 'exists', same };
|
|
345
|
+
return { rel: dest, from, hash, action: 'overwrite', same };
|
|
346
|
+
}
|
|
347
|
+
return patchEntry({ dest, from, hash, to, key, facts: projectFacts, manifest });
|
|
328
348
|
});
|
|
329
349
|
}
|
|
330
350
|
|
|
351
|
+
// Patch decision for one template file that is not retired and not blocked.
|
|
352
|
+
function patchEntry({ dest, from, hash, to, key, facts, manifest }) {
|
|
353
|
+
const relevant = relevance.relevanceOf(key, facts);
|
|
354
|
+
if (!fs.existsSync(to)) {
|
|
355
|
+
if (relevant.status === 'add') return { rel: dest, from, hash, action: 'add', same: false };
|
|
356
|
+
return { rel: dest, from, hash, action: relevant.status === 'review' ? 'review' : 'irrelevant', reason: relevant.reason };
|
|
357
|
+
}
|
|
358
|
+
const local = fileHash(to);
|
|
359
|
+
const same = local === hash;
|
|
360
|
+
if (relevant.status === 'irrelevant') {
|
|
361
|
+
return { rel: dest, from, hash, action: 'irrelevant', same, present: true, reason: relevant.reason };
|
|
362
|
+
}
|
|
363
|
+
if (same) return { rel: dest, from, hash, action: 'skip', same: true, reason: 'identical' };
|
|
364
|
+
if (relevant.status === 'add' && manifest[key] === local) {
|
|
365
|
+
return { rel: dest, from, hash, action: 'update', same: false, reason: 'unchanged since the toolkit wrote it' };
|
|
366
|
+
}
|
|
367
|
+
return { rel: dest, from, hash, action: 'review', same: false, reason: 'kept, differs from template' };
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const PLAN_ACTIONS = ['add', 'update', 'overwrite', 'skip', 'review', 'irrelevant', 'retired', 'conflict'];
|
|
371
|
+
|
|
331
372
|
function countActions(plan) {
|
|
332
|
-
const counts =
|
|
373
|
+
const counts = Object.fromEntries(PLAN_ACTIONS.map((a) => [a, 0]));
|
|
333
374
|
for (const e of plan) counts[e.action]++;
|
|
334
375
|
return counts;
|
|
335
376
|
}
|
|
@@ -337,7 +378,7 @@ function countActions(plan) {
|
|
|
337
378
|
function applyPlan(cwd, plan) {
|
|
338
379
|
let copied = 0;
|
|
339
380
|
for (const e of plan) {
|
|
340
|
-
if (
|
|
381
|
+
if (!['add', 'update', 'overwrite'].includes(e.action)) continue;
|
|
341
382
|
const to = path.join(cwd, e.rel);
|
|
342
383
|
try {
|
|
343
384
|
fs.mkdirSync(path.dirname(to), { recursive: true });
|
|
@@ -361,19 +402,46 @@ function conflicts(type, cwd, env) {
|
|
|
361
402
|
.map((e) => e.rel);
|
|
362
403
|
}
|
|
363
404
|
|
|
364
|
-
|
|
405
|
+
const PLAN_SECTIONS = [
|
|
406
|
+
['add', 'Would add'],
|
|
407
|
+
['update', 'Would update (unchanged since the toolkit wrote them; template changed)'],
|
|
408
|
+
['overwrite', 'Would overwrite'],
|
|
409
|
+
['review', 'Needs review (not written)'],
|
|
410
|
+
['irrelevant', 'Not relevant to this project (not written)'],
|
|
411
|
+
['retired', 'Retired (never re-added)'],
|
|
412
|
+
['conflict', 'Conflicts'],
|
|
413
|
+
];
|
|
414
|
+
|
|
415
|
+
// The plan is shown before any write: what is added, updated, held back and why.
|
|
416
|
+
function printPlan(plan, dryRun) {
|
|
365
417
|
const c = countActions(plan);
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
for (const [action, label] of
|
|
370
|
-
|
|
418
|
+
const summary = `${c.add} add, ${c.update} update, ${c.overwrite} overwrite, ${c.review} need review, `
|
|
419
|
+
+ `${c.irrelevant} not relevant, ${c.retired} retired, ${c.skip} already current, ${c.conflict} conflict.`;
|
|
420
|
+
info(dryRun ? `Dry run, nothing will be written: ${summary}` : `Plan: ${summary}`);
|
|
421
|
+
for (const [action, label] of PLAN_SECTIONS) {
|
|
422
|
+
// Present files that do not apply are kept silently here; doctor lists them as retirement candidates.
|
|
423
|
+
const list = plan.filter((e) => e.action === action && !(action === 'irrelevant' && e.present));
|
|
371
424
|
if (!list.length) continue;
|
|
372
425
|
console.log(` ${label} (${list.length}):`);
|
|
373
426
|
for (const e of list) console.log(` - ${e.rel}${e.reason ? ` (${e.reason})` : ''}`);
|
|
374
427
|
}
|
|
375
428
|
}
|
|
376
429
|
|
|
430
|
+
function printFacts(facts) {
|
|
431
|
+
const yn = (v) => (v ? 'yes' : 'no');
|
|
432
|
+
const tracker = facts.trackers === null
|
|
433
|
+
? `not detected (set "trackers" in ${relevance.RETIRED_FILE})`
|
|
434
|
+
: `${facts.trackers.join(', ') || 'none'} (${facts.trackerSource})`;
|
|
435
|
+
info(`Project facts: tracker ${tracker}; Playwright ${yn(facts.playwright)}; Prisma ${yn(facts.prisma)}; UI ${yn(facts.ui)}.`);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
// Harness wrappers that inline policy or diverge from their snippet. Reported only; nothing is moved.
|
|
439
|
+
function printWrappers(wrappers) {
|
|
440
|
+
if (!wrappers.length) return;
|
|
441
|
+
warn(`${wrappers.length} harness wrapper(s) inline policy or diverge from their snippet. Propose moving the content into the snippet and keeping the wrapper a thin link (nothing changed):`);
|
|
442
|
+
wrappers.forEach((w) => console.log(` - ${w.rel} (${w.status}: ${w.reason})`));
|
|
443
|
+
}
|
|
444
|
+
|
|
377
445
|
// ---- Handoff, version, manifest, selection (the files written besides templates) ----
|
|
378
446
|
|
|
379
447
|
function envHeader(type, env) {
|
|
@@ -498,7 +566,7 @@ function recordState(cwd, { type, env, plan, selection, explicit, dryRun }) {
|
|
|
498
566
|
const version = writeState(path.join(cwd, VERSION_FILE), versionText);
|
|
499
567
|
const files = { ...readManifest(cwd).files };
|
|
500
568
|
for (const e of plan) {
|
|
501
|
-
if (
|
|
569
|
+
if (['add', 'update', 'overwrite'].includes(e.action) || (e.action === 'skip' && e.same)) {
|
|
502
570
|
files[e.rel.split(path.sep).join('/')] = e.hash;
|
|
503
571
|
}
|
|
504
572
|
}
|
|
@@ -562,7 +630,7 @@ function cmdInstall(cwd, flags) {
|
|
|
562
630
|
const blocked = cf.filter((e) => e.reason !== 'exists');
|
|
563
631
|
|
|
564
632
|
if (flags.dryRun) {
|
|
565
|
-
printPlan(plan);
|
|
633
|
+
printPlan(plan, true);
|
|
566
634
|
printDryState(recordState(cwd, { type, env: flags.env, plan, selection, explicit, dryRun: true }));
|
|
567
635
|
if (cf.length) process.exitCode = 1;
|
|
568
636
|
return { copied: 0, skipped: 0, conflicts: cf.length, dryRun: true, planned: counts };
|
|
@@ -587,32 +655,41 @@ function cmdInstall(cwd, flags) {
|
|
|
587
655
|
return { copied, skipped: counts.skip, conflicts: 0, dryRun: false, planned: counts };
|
|
588
656
|
}
|
|
589
657
|
|
|
658
|
+
// Classifies every template file against the project, shows the plan, then writes only add and update
|
|
659
|
+
// entries. Existing files that differ are reported, never overwritten (not even with --force).
|
|
590
660
|
function cmdPatch(cwd, flags) {
|
|
591
661
|
const type = detectType(cwd, flags);
|
|
592
662
|
const { selection, explicit } = resolveSelection(cwd, flags);
|
|
593
|
-
|
|
594
|
-
const
|
|
663
|
+
const retired = relevance.readRetired(cwd);
|
|
664
|
+
const facts = relevance.detectProject(cwd, retired);
|
|
665
|
+
const plan = planFiles(type, cwd, { mode: 'patch', env: flags.env, selection, retired, facts });
|
|
595
666
|
const counts = countActions(plan);
|
|
596
667
|
const conflictEntries = plan.filter((e) => e.action === 'conflict');
|
|
597
668
|
const stale = findStale(cwd, type);
|
|
669
|
+
const wrappers = relevance.auditWrappers(cwd, plan);
|
|
598
670
|
|
|
599
671
|
if (flags.dryRun) {
|
|
600
|
-
|
|
672
|
+
printFacts(facts);
|
|
673
|
+
printPlan(plan, true);
|
|
674
|
+
printWrappers(wrappers);
|
|
601
675
|
printStale(stale);
|
|
602
676
|
printDryState(recordState(cwd, { type, env: flags.env, plan, selection, explicit, dryRun: true }));
|
|
603
677
|
if (conflictEntries.length) process.exitCode = 1;
|
|
604
|
-
return { copied: 0, skipped: counts.skip, conflicts: conflictEntries.length, stale, dryRun: true, planned: counts };
|
|
678
|
+
return { copied: 0, skipped: counts.skip, conflicts: conflictEntries.length, stale, wrappers, facts, dryRun: true, planned: counts };
|
|
605
679
|
}
|
|
680
|
+
printFacts(facts);
|
|
681
|
+
printPlan(plan, false);
|
|
606
682
|
const copied = applyPlan(cwd, plan);
|
|
607
|
-
ok(`
|
|
683
|
+
ok(`Patched: ${copied} file(s) written, ${counts.skip} already current, ${counts.review} kept for review.`);
|
|
608
684
|
if (conflictEntries.length) {
|
|
609
685
|
err(`${conflictEntries.length} path(s) could not be written (blocked by a file or directory of another type):`);
|
|
610
686
|
conflictEntries.forEach((e) => console.log(` - ${e.rel} (${e.reason})`));
|
|
611
687
|
process.exitCode = 1;
|
|
612
688
|
}
|
|
689
|
+
printWrappers(wrappers);
|
|
613
690
|
printStale(stale);
|
|
614
691
|
printHandoff(flags.env, recordState(cwd, { type, env: flags.env, plan, selection, explicit, dryRun: false }));
|
|
615
|
-
return { copied, skipped: counts.skip, conflicts: conflictEntries.length, stale, dryRun: false, planned: counts };
|
|
692
|
+
return { copied, skipped: counts.skip, conflicts: conflictEntries.length, stale, wrappers, facts, dryRun: false, planned: counts };
|
|
616
693
|
}
|
|
617
694
|
|
|
618
695
|
// Placeholders the agent fills per task or feature at runtime, so they are not bootstrap leftovers.
|
|
@@ -647,31 +724,68 @@ function scanPlaceholders(cwd, relFiles) {
|
|
|
647
724
|
return found;
|
|
648
725
|
}
|
|
649
726
|
|
|
727
|
+
// Read-only. Missing means a file that applies to the project and is absent. Retired paths, files that do
|
|
728
|
+
// not apply, and files whose relevance is still undecided are reported in their own sections.
|
|
650
729
|
function cmdDoctor(cwd, flags) {
|
|
651
730
|
const type = detectType(cwd, flags);
|
|
652
731
|
const { selection, explicit } = resolveSelection(cwd, flags);
|
|
653
|
-
const
|
|
654
|
-
|
|
655
|
-
|
|
732
|
+
const retired = relevance.readRetired(cwd);
|
|
733
|
+
const facts = relevance.detectProject(cwd, retired);
|
|
734
|
+
const plan = planFiles(type, cwd, { mode: 'patch', env: flags.env, selection, retired, facts });
|
|
735
|
+
const retiredFiles = [];
|
|
736
|
+
const notRelevant = [];
|
|
737
|
+
const undecided = [];
|
|
656
738
|
const missingFiles = [];
|
|
739
|
+
const files = [];
|
|
657
740
|
let present = 0;
|
|
658
|
-
for (const
|
|
659
|
-
if (
|
|
660
|
-
|
|
741
|
+
for (const e of plan) {
|
|
742
|
+
if (e.action === 'retired') {
|
|
743
|
+
retiredFiles.push({ rel: e.rel, reason: e.reason, present: fs.existsSync(path.join(cwd, e.rel)) });
|
|
744
|
+
continue;
|
|
745
|
+
}
|
|
746
|
+
files.push(e.rel);
|
|
747
|
+
if (fs.existsSync(path.join(cwd, e.rel))) {
|
|
748
|
+
present++;
|
|
749
|
+
if (e.action === 'irrelevant') notRelevant.push(e.rel);
|
|
750
|
+
} else if (e.action === 'irrelevant') {
|
|
751
|
+
continue;
|
|
752
|
+
} else if (e.action === 'review') {
|
|
753
|
+
undecided.push({ rel: e.rel, reason: e.reason });
|
|
754
|
+
} else {
|
|
755
|
+
missingFiles.push(e.rel);
|
|
756
|
+
}
|
|
661
757
|
}
|
|
662
758
|
const placeholders = scanPlaceholders(cwd, files);
|
|
663
759
|
const version = toolkitVersionStatus(cwd);
|
|
664
760
|
const stale = findStale(cwd, type);
|
|
665
761
|
const customized = customizedOwned(cwd);
|
|
666
|
-
const
|
|
667
|
-
|
|
762
|
+
const wrappers = relevance.auditWrappers(cwd, plan);
|
|
763
|
+
const result = { type, present, missingFiles, undecided, retired: retiredFiles, notRelevant, wrappers, facts,
|
|
764
|
+
placeholders, version, stale, customized, selection, selectionSource: explicit ? 'flags' : CONFIG_FILE };
|
|
668
765
|
|
|
669
766
|
console.log('');
|
|
670
|
-
|
|
671
|
-
|
|
767
|
+
printFacts(facts);
|
|
768
|
+
if (missingFiles.length === 0 && undecided.length === 0) ok(`All ${present} template files present.`);
|
|
769
|
+
if (missingFiles.length) {
|
|
672
770
|
warn(`${missingFiles.length} missing file(s):`);
|
|
673
771
|
missingFiles.forEach((f) => console.log(' - ' + f));
|
|
674
772
|
}
|
|
773
|
+
if (undecided.length) {
|
|
774
|
+
warn(`${undecided.length} file(s) not installed because relevance is undecided (run patch --dry-run for the reason):`);
|
|
775
|
+
undecided.forEach((u) => console.log(` - ${u.rel} (${u.reason})`));
|
|
776
|
+
}
|
|
777
|
+
if (retiredFiles.length) {
|
|
778
|
+
info(`${retiredFiles.length} retired path(s), never re-added (${relevance.RETIRED_FILE}):`);
|
|
779
|
+
retiredFiles.forEach((r) => console.log(` - ${r.rel} (${r.reason})${r.present ? ' [still on disk, kept]' : ''}`));
|
|
780
|
+
}
|
|
781
|
+
if (notRelevant.length) {
|
|
782
|
+
info(`${notRelevant.length} present file(s) do not apply to this project. Consider retiring them in ${relevance.RETIRED_FILE} (kept as is):`);
|
|
783
|
+
notRelevant.forEach((f) => console.log(' - ' + f));
|
|
784
|
+
}
|
|
785
|
+
if (wrappers.length) {
|
|
786
|
+
warn(`${wrappers.length} harness wrapper(s) inline policy or diverge from their snippet (nothing changed):`);
|
|
787
|
+
wrappers.forEach((w) => console.log(` - ${w.rel} (${w.status}: ${w.reason})`));
|
|
788
|
+
}
|
|
675
789
|
if (selection.exclude.length || selection.only.length) {
|
|
676
790
|
info(`Selection (${result.selectionSource}): exclude [${selection.exclude.join(', ')}], only [${selection.only.join(', ')}].`);
|
|
677
791
|
}
|
|
@@ -795,8 +909,15 @@ function cmdUpgrade(cwd, flags, deps = {}) {
|
|
|
795
909
|
const requested = parseUpgradeTarget(flags);
|
|
796
910
|
// Reads the effective selection first: a corrupt saved config or a combined --exclude/--only stops here.
|
|
797
911
|
const { selection, explicit } = resolveSelection(cwd, flags);
|
|
912
|
+
// A malformed retirement list stops here too, before the npm lookup and before any write.
|
|
913
|
+
const retired = relevance.readRetired(cwd);
|
|
798
914
|
const target = resolveUpgradeVersion(requested, deps.npmView || npmQuery);
|
|
799
915
|
info(`Resolved ${PKG_NAME}@${target} (installed toolkit: ${currentVersion()}).`);
|
|
916
|
+
const retirementActive = retired.entries.length > 0 || retired.trackers !== undefined;
|
|
917
|
+
if (retirementActive && compareVersions(target, RELEVANCE_MIN) < 0) {
|
|
918
|
+
throw new Error(`${PKG_NAME}@${target} predates retirement support and would re-add the paths and tracker `
|
|
919
|
+
+ `choices recorded in ${relevance.RETIRED_FILE}. Pin ${RELEVANCE_MIN} or later.`);
|
|
920
|
+
}
|
|
800
921
|
const selectionActive = selection.exclude.length > 0 || selection.only.length > 0;
|
|
801
922
|
if (flags.dryRun && compareVersions(target, DRY_RUN_UPGRADE_MIN) < 0) {
|
|
802
923
|
throw new Error(`--dry-run needs ${PKG_NAME} >= ${DRY_RUN_UPGRADE_MIN}; ${target} has no dry run and would write. Pin a newer version.`);
|
|
@@ -812,7 +933,7 @@ function cmdUpgrade(cwd, flags, deps = {}) {
|
|
|
812
933
|
if (writeState(path.join(cwd, CONFIG_FILE), text) === 'write') writeText(cwd, CONFIG_FILE, text);
|
|
813
934
|
}
|
|
814
935
|
if (flags.force) warn('--force: existing files will be overwritten with the latest templates. Back up your customisations first.');
|
|
815
|
-
else info('
|
|
936
|
+
else info('Applying the patch plan: adds files that apply and updates files you never changed; edited files are kept. Pass --force to overwrite them.');
|
|
816
937
|
info(`Running pinned ${PKG_NAME}@${target}${flags.dryRun ? ' (dry run, nothing written)' : ''}...`);
|
|
817
938
|
const run = deps.spawn || spawnSync;
|
|
818
939
|
const r = run('npx', upgradeArgs(flags, target), { cwd, stdio: 'inherit', timeout: 120000 });
|
|
@@ -939,8 +1060,11 @@ Usage:
|
|
|
939
1060
|
--dry-run lists adds, overwrites, skips and conflicts; writes nothing; exits 1 if there are conflicts.
|
|
940
1061
|
|
|
941
1062
|
npx nitm-ai-dev-toolkit patch [--monorepo|--repo] [--env <env>] [--exclude <groups>|--only <groups>] [--dry-run]
|
|
942
|
-
|
|
943
|
-
|
|
1063
|
+
Classify every template file against the project, print the plan, then write it: add
|
|
1064
|
+
files that apply, update files you never changed. Files that differ from the template are
|
|
1065
|
+
kept and listed for review; files that do not apply (other trackers, Playwright, Prisma,
|
|
1066
|
+
empty MCP configs) and retired paths (.nitm/retired.json) are never added. Harness wrappers
|
|
1067
|
+
that inline policy are reported, not changed. Then re-emit the bootstrap handoff.
|
|
944
1068
|
A given --exclude/--only is saved to .nitm/toolkit.json and used by later patch/doctor runs.
|
|
945
1069
|
|
|
946
1070
|
npx nitm-ai-dev-toolkit upgrade [version] [--env <env>] [--force] [--dry-run] [--exclude <groups>|--only <groups>]
|
|
@@ -951,9 +1075,10 @@ Usage:
|
|
|
951
1075
|
--dry-run runs the pinned version's dry run (needs ${DRY_RUN_UPGRADE_MIN} or newer).
|
|
952
1076
|
|
|
953
1077
|
npx nitm-ai-dev-toolkit doctor [--monorepo|--repo] [--env <env>]
|
|
954
|
-
Inspect the current install: missing files,
|
|
955
|
-
|
|
956
|
-
|
|
1078
|
+
Inspect the current install (read-only): missing files, retired paths (.nitm/retired.json,
|
|
1079
|
+
never counted as missing), files that do not apply to the project, files whose relevance is
|
|
1080
|
+
undecided, harness wrappers that inline policy, unresolved {{PLACEHOLDER}} values,
|
|
1081
|
+
.toolkit-version against the toolkit version, and stale toolkit files (reported, never deleted).
|
|
957
1082
|
|
|
958
1083
|
npx nitm-ai-dev-toolkit omo-slim-starter install
|
|
959
1084
|
Run \`npx nitm-opencode-starter install\` to scaffold the oh-my-opencode-slim
|
|
@@ -1074,6 +1199,7 @@ module.exports = {
|
|
|
1074
1199
|
resolveUpgradeVersion,
|
|
1075
1200
|
compareVersions,
|
|
1076
1201
|
DRY_RUN_UPGRADE_MIN,
|
|
1202
|
+
RELEVANCE_MIN,
|
|
1077
1203
|
cmdOmoStarter,
|
|
1078
1204
|
cmdFactory,
|
|
1079
1205
|
ENV_DIRS,
|
|
@@ -1084,4 +1210,10 @@ module.exports = {
|
|
|
1084
1210
|
HANDOFF_FILE,
|
|
1085
1211
|
CONFIG_FILE,
|
|
1086
1212
|
MANIFEST_FILE,
|
|
1213
|
+
RETIRED_FILE: relevance.RETIRED_FILE,
|
|
1214
|
+
readRetired: relevance.readRetired,
|
|
1215
|
+
detectProject: relevance.detectProject,
|
|
1216
|
+
relevanceOf: relevance.relevanceOf,
|
|
1217
|
+
auditWrappers: relevance.auditWrappers,
|
|
1218
|
+
countActions,
|
|
1087
1219
|
};
|