@dzhechkov/skills-feature-adr 1.3.8 → 1.3.10
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 +13 -12
- package/docs/team-onboarding.md +4 -4
- package/package.json +3 -4
- package/src/cli.js +13 -11
- package/src/commands/doctor.js +23 -3
- package/src/commands/init.js +126 -50
- package/src/commands/remove.js +92 -7
- package/src/commands/update.js +86 -10
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
An 11-step, complexity-routed pipeline that makes an AI coding agent build a feature the way a
|
|
6
6
|
disciplined engineering team does: **spec first, code last.** Every phase emits a durable,
|
|
7
|
-
versioned, human-approved **specification artifact** (`00_…`–`
|
|
7
|
+
versioned, human-approved **specification artifact** (`00_…`–`09_…`), and code is *generated from
|
|
8
8
|
the frozen spec* — not reverse-documented after the fact. Scales from a 3-file config change to a
|
|
9
9
|
cross-cutting 30+ file refactor via a Complexity Router (S/M/L/XL). Integrates 15 skills from
|
|
10
10
|
[agentic-qe](https://github.com/proffesor-for-testing/agentic-qe) for quality engineering. Part of
|
|
@@ -14,7 +14,7 @@ the [Keysarium](https://www.npmjs.com/package/@dzhechkov/keysarium) ecosystem.
|
|
|
14
14
|
> onboarding doc for free, every decision captured as an ADR, and machine-checked back against the
|
|
15
15
|
> code by the QE phase. See **[Spec-Driven Development](#spec-driven-development-sdd)** below.
|
|
16
16
|
>
|
|
17
|
-
> 👉 **Onboarding a team? Start here:** [**Feature ADR — Team Onboarding**](https://
|
|
17
|
+
> 👉 **Onboarding a team? Start here:** [**Feature ADR — Team Onboarding**](https://github.com/djd1m/dz-skill-bundles/blob/main/docs/feature-adr-team-onboarding.md) — a single read-top-to-bottom playbook (install → first feature → SDD mechanics → model routing incl. Fable → self-learning layers → team PR workflow → FAQ). It also ships **inside this package** at `node_modules/@dzhechkov/skills-feature-adr/docs/team-onboarding.md` ([raw copy on unpkg](https://unpkg.com/@dzhechkov/skills-feature-adr/docs/team-onboarding.md)), so every install has it locally.
|
|
18
18
|
|
|
19
19
|
---
|
|
20
20
|
|
|
@@ -76,13 +76,13 @@ npx @dzhechkov/skills-feature-adr doctor # Health check
|
|
|
76
76
|
## Feature ADR Pipeline
|
|
77
77
|
|
|
78
78
|
```
|
|
79
|
-
Step 0 Step 1 Step 2 Step 3
|
|
80
|
-
COMPLEXITY → REQUIREMENTS → RESEARCH → ADR → DDD
|
|
81
|
-
ROUTER (L/XL only) (M+ only)
|
|
79
|
+
Step 0 Step 1 Step 2 Step 3 Step 3.5 Step 4
|
|
80
|
+
COMPLEXITY → REQUIREMENTS → RESEARCH → ADR → QCSD SWARM → DDD
|
|
81
|
+
ROUTER (L/XL only) (M+ only) (M+ only) (L/XL only)
|
|
82
82
|
|
|
83
|
-
Step 5 Step 6 Step 7 Step 8
|
|
84
|
-
ARCHITECTURE → IMPLEMENTATION → CODE → QE
|
|
85
|
-
(M+ only) PLAN
|
|
83
|
+
Step 5 Step 6 Step 7 Step 8 Step 9
|
|
84
|
+
ARCHITECTURE → IMPLEMENTATION → CODE → QE → FLEET QE
|
|
85
|
+
(M+ only) PLAN (L/XL only)
|
|
86
86
|
```
|
|
87
87
|
|
|
88
88
|
### Usage in Claude Code
|
|
@@ -197,7 +197,7 @@ gate — not discovered in production.
|
|
|
197
197
|
| Tier | Scope | Active Steps | Time Budget |
|
|
198
198
|
|------|-------|-------------|-------------|
|
|
199
199
|
| **S** | 1-3 files, 1 domain | 0→1→6→7→8 | ~15 min |
|
|
200
|
-
| **M** | 4-10 files, 1-2 domains | 0→1→3→5→6→7→8 | ~45 min |
|
|
200
|
+
| **M** | 4-10 files, 1-2 domains | 0→1→3→3.5→5→6→7→8 | ~45 min |
|
|
201
201
|
| **L** | 11-30 files, 2-4 domains | Full pipeline with parallelism | ~2 hours |
|
|
202
202
|
| **XL** | 30+ files, cross-cutting | Full DAG + multi-agent swarm | ~4+ hours |
|
|
203
203
|
|
|
@@ -461,9 +461,10 @@ npx @dzhechkov/skills-feature-adr init # Feature development
|
|
|
461
461
|
|
|
462
462
|
## Links
|
|
463
463
|
|
|
464
|
-
- **Team Onboarding playbook (public):** [
|
|
465
|
-
- **GitHub:** [https://github.com/
|
|
466
|
-
- **Issues:** [https://github.com/
|
|
464
|
+
- **Team Onboarding playbook (public, rendered):** [dz-skill-bundles/docs/feature-adr-team-onboarding.md](https://github.com/djd1m/dz-skill-bundles/blob/main/docs/feature-adr-team-onboarding.md) — also shipped in this package at `docs/team-onboarding.md` ([raw on unpkg](https://unpkg.com/@dzhechkov/skills-feature-adr/docs/team-onboarding.md))
|
|
465
|
+
- **GitHub (public docs & bundles):** [https://github.com/djd1m/dz-skill-bundles](https://github.com/djd1m/dz-skill-bundles)
|
|
466
|
+
- **Issues:** [https://github.com/djd1m/dz-skill-bundles/issues](https://github.com/djd1m/dz-skill-bundles/issues)
|
|
467
|
+
- *Note: the source monorepo (`djd1m/dz-harness-hub`) is private; the links above are the public entry points.*
|
|
467
468
|
- **npm:** [https://www.npmjs.com/package/@dzhechkov/skills-feature-adr](https://www.npmjs.com/package/@dzhechkov/skills-feature-adr)
|
|
468
469
|
- **Keysarium:** [https://www.npmjs.com/package/@dzhechkov/keysarium](https://www.npmjs.com/package/@dzhechkov/keysarium)
|
|
469
470
|
- **BTO:** [https://www.npmjs.com/package/@dzhechkov/skills-bto](https://www.npmjs.com/package/@dzhechkov/skills-bto)
|
package/docs/team-onboarding.md
CHANGED
|
@@ -6,7 +6,7 @@ This is the practical guide for a dev team adopting `@dzhechkov/skills-feature-a
|
|
|
6
6
|
from zero to your first spec-driven feature, then explains the mechanics, the model routing, the
|
|
7
7
|
self-learning options, and the team conventions.
|
|
8
8
|
|
|
9
|
-
- 📦 npm: <https://www.npmjs.com/package/@dzhechkov/skills-feature-adr>
|
|
9
|
+
- 📦 npm: <https://www.npmjs.com/package/@dzhechkov/skills-feature-adr>
|
|
10
10
|
- 📄 Full reference README: same npm page (this doc is the *team playbook*, not a reference dump)
|
|
11
11
|
- 🧭 Deep design: `packages/@dzhechkov/skills-meta/feature-adr/SKILL.md` + its `references/`
|
|
12
12
|
|
|
@@ -15,7 +15,7 @@ self-learning options, and the team conventions.
|
|
|
15
15
|
## TL;DR — what & why
|
|
16
16
|
|
|
17
17
|
An AI agent, left alone, jumps straight to code. Feature ADR forces the opposite: **spec first,
|
|
18
|
-
code last.** Each phase emits a durable, human-approved specification artifact (`00_…`–`
|
|
18
|
+
code last.** Each phase emits a durable, human-approved specification artifact (`00_…`–`09_…`), and
|
|
19
19
|
code is *generated from the frozen spec* — then verified back against it.
|
|
20
20
|
|
|
21
21
|
**Why your team should care:**
|
|
@@ -133,7 +133,7 @@ features/<feature-slug>/
|
|
|
133
133
|
| Tier | Scope | Active steps | Budget |
|
|
134
134
|
|------|-------|--------------|--------|
|
|
135
135
|
| **S** | 1–3 files, 1 domain | 0→1→6→7→8 (no ADR/DDD/architecture) | ~15 min |
|
|
136
|
-
| **M** | 4–10 files | 0→1→3→5→6→7→8 | ~45 min |
|
|
136
|
+
| **M** | 4–10 files | 0→1→3→3.5→5→6→7→8 | ~45 min |
|
|
137
137
|
| **L** | 11–30 files | Full pipeline with parallelism | ~2 h |
|
|
138
138
|
| **XL** | 30+ files, cross-cutting | Full DAG + multi-agent swarm | ~4 h+ |
|
|
139
139
|
|
|
@@ -230,7 +230,7 @@ uses agentdb, and even there it's optional (lexical recall works without it).
|
|
|
230
230
|
fixed for the run — restart if scope changes drastically mid-pipeline.
|
|
231
231
|
- **"`--full-qe` did nothing extra."** `agentic-qe` isn't installed → it silently falls back to
|
|
232
232
|
Reference mode with a warning. Install it (`npm i -g agentic-qe && aqe init --auto`).
|
|
233
|
-
- **"Do I have to use all
|
|
233
|
+
- **"Do I have to use all 11 steps?"** No — the Complexity Router picks the active subset per tier.
|
|
234
234
|
- **"Where's the learning stored / how do I reset it?"** Layer A: delete `.keysarium/memory/`.
|
|
235
235
|
B: `.agentic-qe/`. C: `.dz/`. They're independent.
|
|
236
236
|
- **"Can I change which model a step uses?"** Yes — the `Model` column is a default. See §6.
|
package/package.json
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/skills-feature-adr",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.10",
|
|
4
4
|
"description": "Adaptive Feature Development skill pack for Claude Code — 11-step pipeline with Complexity Router (S/M/L/XL), ADR-driven architecture, 15 agentic-qe skills, multi-agent fleet QE. Supports --full-qe, --full-qe-extended, --with-learning, and --knowledge-extractor modes.",
|
|
5
|
-
"main": "src/cli.js",
|
|
6
5
|
"bin": {
|
|
7
6
|
"skills-feature-adr": "./bin/cli.js"
|
|
8
7
|
},
|
|
@@ -43,9 +42,9 @@
|
|
|
43
42
|
"url": "https://github.com/djd1m/dz-harness-hub.git",
|
|
44
43
|
"directory": "packages/@dzhechkov/skills-feature-adr"
|
|
45
44
|
},
|
|
46
|
-
"homepage": "https://github.com/djd1m/dz-
|
|
45
|
+
"homepage": "https://github.com/djd1m/dz-skill-bundles/blob/main/docs/feature-adr-team-onboarding.md",
|
|
47
46
|
"bugs": {
|
|
48
|
-
"url": "https://github.com/
|
|
47
|
+
"url": "https://github.com/djd1m/dz-skill-bundles/issues"
|
|
49
48
|
},
|
|
50
49
|
"peerDependencies": {
|
|
51
50
|
"@dzhechkov/keysarium-core": "^1.0.0"
|
package/src/cli.js
CHANGED
|
@@ -138,24 +138,19 @@ function main() {
|
|
|
138
138
|
|
|
139
139
|
switch (resolvedCommand) {
|
|
140
140
|
case 'init':
|
|
141
|
-
require('./commands/init')(flags);
|
|
142
|
-
break;
|
|
141
|
+
return require('./commands/init')(flags);
|
|
143
142
|
|
|
144
143
|
case 'update':
|
|
145
|
-
require('./commands/update')(flags);
|
|
146
|
-
break;
|
|
144
|
+
return require('./commands/update')(flags);
|
|
147
145
|
|
|
148
146
|
case 'remove':
|
|
149
|
-
require('./commands/remove')(flags);
|
|
150
|
-
break;
|
|
147
|
+
return require('./commands/remove')(flags);
|
|
151
148
|
|
|
152
149
|
case 'list':
|
|
153
|
-
require('./commands/list')(flags);
|
|
154
|
-
break;
|
|
150
|
+
return require('./commands/list')(flags);
|
|
155
151
|
|
|
156
152
|
case 'doctor':
|
|
157
|
-
require('./commands/doctor')(flags);
|
|
158
|
-
break;
|
|
153
|
+
return require('./commands/doctor')(flags);
|
|
159
154
|
|
|
160
155
|
default:
|
|
161
156
|
error(`Unknown command: "${resolvedCommand}"`);
|
|
@@ -166,4 +161,11 @@ function main() {
|
|
|
166
161
|
}
|
|
167
162
|
}
|
|
168
163
|
|
|
169
|
-
|
|
164
|
+
// Command handlers may be async — catch rejections so they never surface as
|
|
165
|
+
// unhandled promise rejections (finding: un-awaited run()).
|
|
166
|
+
Promise.resolve()
|
|
167
|
+
.then(() => main())
|
|
168
|
+
.catch((err) => {
|
|
169
|
+
console.error('✗ ' + (err && err.message ? err.message : err));
|
|
170
|
+
process.exit(1);
|
|
171
|
+
});
|
package/src/commands/doctor.js
CHANGED
|
@@ -35,15 +35,33 @@ function check(name, fn) {
|
|
|
35
35
|
function checkFilesExist(manifest, targetDir) {
|
|
36
36
|
return check('Files exist', () => {
|
|
37
37
|
const files = manifest.files || [];
|
|
38
|
+
const resolvedTarget = path.resolve(targetDir);
|
|
38
39
|
const missing = [];
|
|
40
|
+
const suspicious = [];
|
|
39
41
|
|
|
40
42
|
for (const relPath of files) {
|
|
41
|
-
const absPath = path.
|
|
43
|
+
const absPath = path.resolve(resolvedTarget, relPath);
|
|
44
|
+
|
|
45
|
+
// Manifest entries must stay inside the project — anything that
|
|
46
|
+
// resolves outside is suspicious (possible tampering), not checked.
|
|
47
|
+
if (absPath !== resolvedTarget && !absPath.startsWith(resolvedTarget + path.sep)) {
|
|
48
|
+
suspicious.push(relPath);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
42
52
|
if (!fileExists(absPath)) {
|
|
43
53
|
missing.push(relPath);
|
|
44
54
|
}
|
|
45
55
|
}
|
|
46
56
|
|
|
57
|
+
if (suspicious.length > 0) {
|
|
58
|
+
return {
|
|
59
|
+
passed: false,
|
|
60
|
+
detail: `${suspicious.length} suspicious manifest entr${suspicious.length === 1 ? 'y' : 'ies'} outside the project (possible tampering): ${suspicious.slice(0, 5).join(', ')}${suspicious.length > 5 ? '...' : ''}`,
|
|
61
|
+
fix: `Inspect ${cyan(MANIFEST_FILE)} and remove entries pointing outside the project, or reinstall with ${cyan('@dzhechkov/skills-feature-adr init --force')}.`,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
if (missing.length === 0) {
|
|
48
66
|
return {
|
|
49
67
|
passed: true,
|
|
@@ -112,7 +130,7 @@ function checkSkillComplete(targetDir) {
|
|
|
112
130
|
}
|
|
113
131
|
|
|
114
132
|
function checkModulesComplete(targetDir) {
|
|
115
|
-
return check('Pipeline modules (
|
|
133
|
+
return check('Pipeline modules (11)', () => {
|
|
116
134
|
const modulesDir = path.join(targetDir, '.claude', 'skills', 'feature-adr', 'modules');
|
|
117
135
|
|
|
118
136
|
if (!fileExists(modulesDir)) {
|
|
@@ -128,11 +146,13 @@ function checkModulesComplete(targetDir) {
|
|
|
128
146
|
'01-requirements.md',
|
|
129
147
|
'02-research.md',
|
|
130
148
|
'03-adr.md',
|
|
149
|
+
'03.5-ideation-swarm.md',
|
|
131
150
|
'04-ddd.md',
|
|
132
151
|
'05-architecture.md',
|
|
133
152
|
'06-implementation-plan.md',
|
|
134
153
|
'07-code.md',
|
|
135
154
|
'08-qe.md',
|
|
155
|
+
'09-fleet-qe.md',
|
|
136
156
|
];
|
|
137
157
|
|
|
138
158
|
const missing = [];
|
|
@@ -145,7 +165,7 @@ function checkModulesComplete(targetDir) {
|
|
|
145
165
|
if (missing.length === 0) {
|
|
146
166
|
return {
|
|
147
167
|
passed: true,
|
|
148
|
-
detail: `All
|
|
168
|
+
detail: `All 11 pipeline modules present`,
|
|
149
169
|
};
|
|
150
170
|
}
|
|
151
171
|
|
package/src/commands/init.js
CHANGED
|
@@ -5,7 +5,7 @@ const path = require('path');
|
|
|
5
5
|
const {
|
|
6
6
|
green, yellow, cyan, bold, dim,
|
|
7
7
|
info, success, warn, error: logError, step,
|
|
8
|
-
|
|
8
|
+
fileExists, readJSON,
|
|
9
9
|
ensureDir, getRelativePaths, getRelativePathsFiltered,
|
|
10
10
|
createManifest, writeManifest, getTemplatesDir,
|
|
11
11
|
COMPONENTS, OPTIONAL_COMPONENTS, MANIFEST_FILE, getComponentFilter,
|
|
@@ -27,7 +27,9 @@ function showKeysariumIntegration(keysariumManifest) {
|
|
|
27
27
|
console.log('');
|
|
28
28
|
console.log(cyan(' \u250C\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510'));
|
|
29
29
|
console.log(cyan(' \u2502') + bold(' @dzhechkov/keysarium detected!') + ' ' + cyan('\u2502'));
|
|
30
|
-
|
|
30
|
+
const v = keysariumManifest.version;
|
|
31
|
+
const kv = typeof v === 'string' && v.length ? v : 'unknown';
|
|
32
|
+
console.log(cyan(' \u2502') + ` Version: ${dim(kv)}` + ' '.repeat(Math.max(0, 39 - kv.length)) + cyan('\u2502'));
|
|
31
33
|
console.log(cyan(' \u2502') + ' Feature ADR integrates with existing Keysarium. ' + cyan('\u2502'));
|
|
32
34
|
console.log(cyan(' \u2502') + ' Shared: .claude/commands, rules, skills ' + cyan('\u2502'));
|
|
33
35
|
console.log(cyan(' \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518'));
|
|
@@ -38,34 +40,67 @@ function showKeysariumIntegration(keysariumManifest) {
|
|
|
38
40
|
// Helpers
|
|
39
41
|
// ---------------------------------------------------------------------------
|
|
40
42
|
|
|
41
|
-
|
|
43
|
+
// Copies a component file-by-file with per-file overwrite protection:
|
|
44
|
+
// - destination missing -> write, record in `written`
|
|
45
|
+
// - destination exists + --force -> overwrite, record in `written`
|
|
46
|
+
// - destination exists, no --force -> do NOT write, record in `preserved`
|
|
47
|
+
// In dry-run mode nothing is written, but the same written/preserved
|
|
48
|
+
// classification is produced.
|
|
49
|
+
// Returns { missing, fileCount } where `missing` means the template source
|
|
50
|
+
// was absent on disk and `fileCount` is how many files the template provides.
|
|
51
|
+
function installComponent(key, comp, templatesDir, targetDir, opts) {
|
|
52
|
+
const { force, dryRun, written, preserved } = opts;
|
|
42
53
|
const src = path.join(templatesDir, comp.src);
|
|
43
|
-
const
|
|
54
|
+
const destRoot = path.join(targetDir, comp.src);
|
|
44
55
|
|
|
45
56
|
if (!fileExists(src)) {
|
|
46
57
|
warn(`Template source not found: ${comp.src} \u2014 skipping.`);
|
|
47
|
-
return
|
|
58
|
+
return { missing: true, fileCount: 0 };
|
|
48
59
|
}
|
|
49
60
|
|
|
50
|
-
//
|
|
61
|
+
// Build the file list from the TEMPLATE source, never a scan of the destination — otherwise
|
|
62
|
+
// user-created files inside a component dir get adopted into the manifest and a later
|
|
63
|
+
// `remove` deletes them (finding #8).
|
|
64
|
+
let entries;
|
|
51
65
|
if (comp.isFile) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
66
|
+
entries = [{ srcFile: src, destFile: destRoot, rel: comp.src }];
|
|
67
|
+
} else {
|
|
68
|
+
const filterFn = getComponentFilter(comp);
|
|
69
|
+
const rels = filterFn ? getRelativePathsFiltered(src, filterFn) : getRelativePaths(src);
|
|
70
|
+
entries = rels.map((rel) => ({
|
|
71
|
+
srcFile: path.join(src, rel),
|
|
72
|
+
destFile: path.join(destRoot, rel),
|
|
73
|
+
rel: path.join(comp.src, rel),
|
|
74
|
+
}));
|
|
55
75
|
}
|
|
56
76
|
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
77
|
+
for (const entry of entries) {
|
|
78
|
+
if (fileExists(entry.destFile) && !force) {
|
|
79
|
+
preserved.push(entry.rel);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (!dryRun) {
|
|
83
|
+
ensureDir(path.dirname(entry.destFile));
|
|
84
|
+
fs.copyFileSync(entry.srcFile, entry.destFile);
|
|
85
|
+
}
|
|
86
|
+
written.push(entry.rel);
|
|
65
87
|
}
|
|
66
88
|
|
|
67
|
-
|
|
68
|
-
|
|
89
|
+
return { missing: false, fileCount: entries.length };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Print the block of pre-existing files that were (or would be) preserved
|
|
93
|
+
function printPreservedBlock(preserved, dryRun) {
|
|
94
|
+
console.log('');
|
|
95
|
+
const verb = dryRun ? 'would be preserved' : 'preserved';
|
|
96
|
+
warn(`${preserved.length} pre-existing file(s) ${verb} (not overwritten, not tracked by the manifest) — use --force to overwrite:`);
|
|
97
|
+
const MAX_SHOWN = 10;
|
|
98
|
+
for (const rel of preserved.slice(0, MAX_SHOWN)) {
|
|
99
|
+
console.log(` ${yellow('⚠')} ${rel}`);
|
|
100
|
+
}
|
|
101
|
+
if (preserved.length > MAX_SHOWN) {
|
|
102
|
+
console.log(dim(` …and ${preserved.length - MAX_SHOWN} more`));
|
|
103
|
+
}
|
|
69
104
|
}
|
|
70
105
|
|
|
71
106
|
// Resolve which optional component keys to install based on flags
|
|
@@ -151,40 +186,81 @@ async function run(options) {
|
|
|
151
186
|
|
|
152
187
|
console.log('');
|
|
153
188
|
|
|
154
|
-
|
|
155
|
-
warn('Dry run \u2014 no files were written.');
|
|
156
|
-
process.exit(0);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// ── e) Install core components ─────────────────────────────────────────
|
|
189
|
+
// ── e) Install components (classify-only when --dry-run) ────────────────
|
|
160
190
|
const totalComponents = componentKeys.length + optionalKeys.length;
|
|
161
|
-
const installedFiles = [];
|
|
191
|
+
const installedFiles = []; // files written (or would-write in dry-run)
|
|
192
|
+
const preservedFiles = []; // pre-existing files NOT overwritten (no --force)
|
|
193
|
+
const completedKeys = []; // component keys processed so far (for partial manifest)
|
|
194
|
+
const installedOptionalKeys = [];
|
|
162
195
|
let stepNum = 0;
|
|
196
|
+
const installVerb = dryRun ? 'Checking' : 'Installing';
|
|
197
|
+
|
|
198
|
+
try {
|
|
199
|
+
for (const key of componentKeys) {
|
|
200
|
+
stepNum++;
|
|
201
|
+
const comp = COMPONENTS[key];
|
|
202
|
+
step(stepNum, totalComponents, `${installVerb} ${comp.label}...`);
|
|
203
|
+
|
|
204
|
+
installComponent(key, comp, templatesDir, targetDir, {
|
|
205
|
+
force, dryRun, written: installedFiles, preserved: preservedFiles,
|
|
206
|
+
});
|
|
207
|
+
completedKeys.push(key);
|
|
208
|
+
}
|
|
163
209
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
210
|
+
// ── e2) Install optional components — track which actually installed ──
|
|
211
|
+
// Components whose template source is missing report missing=true;
|
|
212
|
+
// we must NOT record them in the manifest, otherwise `update` reports
|
|
213
|
+
// "Unknown component" on subsequent runs.
|
|
214
|
+
for (const key of optionalKeys) {
|
|
215
|
+
stepNum++;
|
|
216
|
+
const comp = OPTIONAL_COMPONENTS[key];
|
|
217
|
+
step(stepNum, totalComponents, `${installVerb} ${comp.label}...`);
|
|
218
|
+
|
|
219
|
+
const res = installComponent(key, comp, templatesDir, targetDir, {
|
|
220
|
+
force, dryRun, written: installedFiles, preserved: preservedFiles,
|
|
221
|
+
});
|
|
222
|
+
if (!res.missing && res.fileCount > 0) {
|
|
223
|
+
installedOptionalKeys.push(key);
|
|
224
|
+
completedKeys.push(key);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
} catch (err) {
|
|
228
|
+
// ── Non-atomic install recovery: record what WAS written so far ───────
|
|
229
|
+
console.log('');
|
|
230
|
+
logError(`Install failed: ${err && err.message ? err.message : err}`);
|
|
231
|
+
if (!dryRun) {
|
|
232
|
+
try {
|
|
233
|
+
const failPkg = readJSON(path.resolve(__dirname, '../../package.json'));
|
|
234
|
+
const partialManifest = createManifest(
|
|
235
|
+
failPkg ? failPkg.version : '0.0.0',
|
|
236
|
+
completedKeys,
|
|
237
|
+
installedFiles.sort()
|
|
238
|
+
);
|
|
239
|
+
partialManifest.partial = true;
|
|
240
|
+
writeManifest(targetDir, partialManifest);
|
|
241
|
+
} catch (writeErr) {
|
|
242
|
+
logError(`Could not write partial manifest: ${writeErr && writeErr.message ? writeErr.message : writeErr}`);
|
|
243
|
+
}
|
|
244
|
+
info(`Partial install recorded in ${MANIFEST_FILE} — run \`npx @dzhechkov/skills-feature-adr remove\` to clean up, or fix the cause and re-run init.`);
|
|
245
|
+
}
|
|
246
|
+
process.exit(1);
|
|
171
247
|
}
|
|
172
248
|
|
|
173
|
-
// ──
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
stepNum++;
|
|
180
|
-
const comp = OPTIONAL_COMPONENTS[key];
|
|
181
|
-
step(stepNum, totalComponents, `Installing ${comp.label}...`);
|
|
182
|
-
|
|
183
|
-
const files = installComponent(key, comp, templatesDir, targetDir);
|
|
184
|
-
if (files.length > 0) {
|
|
185
|
-
installedFiles.push(...files);
|
|
186
|
-
installedOptionalKeys.push(key);
|
|
249
|
+
// ── e3) Dry-run report: same would-write / would-preserve classification ─
|
|
250
|
+
if (dryRun) {
|
|
251
|
+
console.log('');
|
|
252
|
+
info(`Dry run: ${installedFiles.length} file(s) would be written, ${preservedFiles.length} pre-existing file(s) would be preserved.`);
|
|
253
|
+
if (preservedFiles.length > 0) {
|
|
254
|
+
printPreservedBlock(preservedFiles, true);
|
|
187
255
|
}
|
|
256
|
+
console.log('');
|
|
257
|
+
warn('Dry run — no files were written.');
|
|
258
|
+
process.exit(0);
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (preservedFiles.length > 0) {
|
|
262
|
+
printPreservedBlock(preservedFiles, false);
|
|
263
|
+
console.log('');
|
|
188
264
|
}
|
|
189
265
|
|
|
190
266
|
// ── f) Write manifest ──────────────────────────────────────────────────
|
|
@@ -238,9 +314,9 @@ async function run(options) {
|
|
|
238
314
|
|
|
239
315
|
console.log(bold('Complexity Tiers:'));
|
|
240
316
|
console.log(` ${green('S')} ${dim('\u2014 1-3 files, ~15 min (Steps: 0\u21921\u21926\u21927\u21928)')}`);
|
|
241
|
-
console.log(` ${yellow('M')} ${dim('\u2014 4-10 files, ~45 min (Steps: 0\u21921\u21923\u21925\u21926\u21927\u21928)')}`);
|
|
242
|
-
console.log(` ${cyan('L')} ${dim('\u2014 11-30 files, ~2h (
|
|
243
|
-
console.log(` ${bold('XL')} ${dim('\u2014 30+ files, ~4h+ (Full DAG +
|
|
317
|
+
console.log(` ${yellow('M')} ${dim('\u2014 4-10 files, ~45 min (Steps: 0\u21921\u21923\u21923.5\u21925\u21926\u21927\u21928)')}`);
|
|
318
|
+
console.log(` ${cyan('L')} ${dim('\u2014 11-30 files, ~2h (Steps: 0\u21921\u21922\u21923\u21923.5\u21924\u21925\u21926\u21927\u21928\u21929)')}`);
|
|
319
|
+
console.log(` ${bold('XL')} ${dim('\u2014 30+ files, ~4h+ (Full DAG with parallelism + fleet QE)')}`);
|
|
244
320
|
console.log('');
|
|
245
321
|
|
|
246
322
|
if (!withLearning && !knowledgeExtractor && !keysariumDetected) {
|
package/src/commands/remove.js
CHANGED
|
@@ -43,8 +43,11 @@ function removeFile(filePath) {
|
|
|
43
43
|
|
|
44
44
|
function removeEmptyDirs(dirPath, stopDir) {
|
|
45
45
|
try {
|
|
46
|
-
|
|
47
|
-
|
|
46
|
+
const root = path.resolve(stopDir);
|
|
47
|
+
let current = path.resolve(dirPath);
|
|
48
|
+
|
|
49
|
+
// Containment-based loop: never stat/rmdir/climb outside the project root
|
|
50
|
+
while (current !== root && current.startsWith(root + path.sep)) {
|
|
48
51
|
if (!fileExists(current)) {
|
|
49
52
|
current = path.dirname(current);
|
|
50
53
|
continue;
|
|
@@ -66,12 +69,57 @@ function removeEmptyDirs(dirPath, stopDir) {
|
|
|
66
69
|
}
|
|
67
70
|
}
|
|
68
71
|
|
|
72
|
+
// Directories shared with @dzhechkov/keysarium — never delete their files
|
|
73
|
+
// when keysarium is installed in the same project.
|
|
74
|
+
const KEYSARIUM_SHARED_DIRS = [
|
|
75
|
+
['.claude', 'skills', 'explore'],
|
|
76
|
+
['.claude', 'skills', 'problem-solver-enhanced'],
|
|
77
|
+
['.claude', 'skills', 'frontend-design'],
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
function isKeysariumShared(relParts) {
|
|
81
|
+
return KEYSARIUM_SHARED_DIRS.some(
|
|
82
|
+
(dir) => relParts.length > dir.length && dir.every((seg, i) => relParts[i] === seg)
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Classify a manifest entry:
|
|
87
|
+
// 'outside' — resolves outside the project root (possible tampering)
|
|
88
|
+
// 'shared' — lives in a keysarium-shared directory (preserve)
|
|
89
|
+
// 'ok' — safe to remove
|
|
90
|
+
function classifyManifestEntry(relPath, resolvedTarget, keysariumDetected) {
|
|
91
|
+
const absPath = path.resolve(resolvedTarget, relPath);
|
|
92
|
+
|
|
93
|
+
if (absPath !== resolvedTarget && !absPath.startsWith(resolvedTarget + path.sep)) {
|
|
94
|
+
return { absPath, kind: 'outside' };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (keysariumDetected) {
|
|
98
|
+
const relParts = path.relative(resolvedTarget, absPath).split(path.sep);
|
|
99
|
+
if (isKeysariumShared(relParts)) {
|
|
100
|
+
return { absPath, kind: 'shared' };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
return { absPath, kind: 'ok' };
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function printOutsideWarning(outsideEntries) {
|
|
108
|
+
console.log(
|
|
109
|
+
` ${yellow('⚠')} ${outsideEntries.length} manifest entr${outsideEntries.length === 1 ? 'y' : 'ies'} outside the project ${outsideEntries.length === 1 ? 'was' : 'were'} skipped (possible tampering):`
|
|
110
|
+
);
|
|
111
|
+
for (const p of outsideEntries) {
|
|
112
|
+
console.log(` ${dim(p)}`);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
69
116
|
// ---------------------------------------------------------------------------
|
|
70
117
|
// Main command
|
|
71
118
|
// ---------------------------------------------------------------------------
|
|
72
119
|
|
|
73
120
|
async function run(options) {
|
|
74
121
|
const { force, dryRun, targetDir } = options;
|
|
122
|
+
const resolvedTarget = path.resolve(targetDir);
|
|
75
123
|
const manifestPath = path.join(targetDir, MANIFEST_FILE);
|
|
76
124
|
|
|
77
125
|
// ── a) Read manifest ──────────────────────────────────────────────────
|
|
@@ -105,20 +153,35 @@ async function run(options) {
|
|
|
105
153
|
console.log('');
|
|
106
154
|
|
|
107
155
|
const keysariumPath = path.join(targetDir, '.keysarium.json');
|
|
108
|
-
|
|
156
|
+
const keysariumDetected = fileExists(keysariumPath);
|
|
157
|
+
if (keysariumDetected) {
|
|
109
158
|
info('@dzhechkov/keysarium detected \u2014 shared directories will be preserved.');
|
|
110
159
|
}
|
|
111
160
|
|
|
112
161
|
if (dryRun) {
|
|
113
162
|
console.log(bold('Files to be removed:'));
|
|
163
|
+
const outsideDry = [];
|
|
114
164
|
for (const relPath of files) {
|
|
115
|
-
const absPath =
|
|
165
|
+
const { absPath, kind } = classifyManifestEntry(relPath, resolvedTarget, keysariumDetected);
|
|
166
|
+
if (kind === 'outside') {
|
|
167
|
+
outsideDry.push(relPath);
|
|
168
|
+
console.log(` ${yellow('- OUT')} ${relPath}`);
|
|
169
|
+
continue;
|
|
170
|
+
}
|
|
171
|
+
if (kind === 'shared') {
|
|
172
|
+
console.log(` ${dim('- KEEP')} ${relPath} ${dim('(shared with Keysarium)')}`);
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
116
175
|
const exists = fileExists(absPath);
|
|
117
176
|
const marker = exists ? red('- DEL') : dim('- N/A');
|
|
118
177
|
console.log(` ${marker} ${relPath}`);
|
|
119
178
|
}
|
|
120
179
|
console.log(` ${red('- DEL')} ${MANIFEST_FILE}`);
|
|
121
180
|
console.log('');
|
|
181
|
+
if (outsideDry.length > 0) {
|
|
182
|
+
printOutsideWarning(outsideDry);
|
|
183
|
+
console.log('');
|
|
184
|
+
}
|
|
122
185
|
warn('Dry run \u2014 no files were removed.');
|
|
123
186
|
process.exit(0);
|
|
124
187
|
}
|
|
@@ -138,11 +201,25 @@ async function run(options) {
|
|
|
138
201
|
// ── d) Remove files ───────────────────────────────────────────────────
|
|
139
202
|
let removedCount = 0;
|
|
140
203
|
let skippedCount = 0;
|
|
204
|
+
let preservedCount = 0;
|
|
205
|
+
const outsideEntries = [];
|
|
141
206
|
const dirsToCheck = new Set();
|
|
142
207
|
|
|
143
208
|
for (let i = 0; i < files.length; i++) {
|
|
144
209
|
const relPath = files[i];
|
|
145
|
-
const absPath =
|
|
210
|
+
const { absPath, kind } = classifyManifestEntry(relPath, resolvedTarget, keysariumDetected);
|
|
211
|
+
|
|
212
|
+
if (kind === 'outside') {
|
|
213
|
+
outsideEntries.push(relPath);
|
|
214
|
+
step(i + 1, files.length, `Skipping ${relPath} (outside project)`);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (kind === 'shared') {
|
|
219
|
+
preservedCount++;
|
|
220
|
+
step(i + 1, files.length, `Preserving ${relPath} (shared with Keysarium)`);
|
|
221
|
+
continue;
|
|
222
|
+
}
|
|
146
223
|
|
|
147
224
|
step(i + 1, files.length, `Removing ${relPath}`);
|
|
148
225
|
|
|
@@ -154,10 +231,12 @@ async function run(options) {
|
|
|
154
231
|
}
|
|
155
232
|
}
|
|
156
233
|
|
|
157
|
-
// Remove empty directories (bottom-up), only feature-adr-exclusive ones
|
|
234
|
+
// Remove empty directories (bottom-up), only feature-adr-exclusive ones.
|
|
235
|
+
// dirsToCheck contains only parents of files actually deleted, so preserved
|
|
236
|
+
// (keysarium-shared) directories are never pruned.
|
|
158
237
|
const sortedDirs = Array.from(dirsToCheck).sort((a, b) => b.length - a.length);
|
|
159
238
|
for (const dir of sortedDirs) {
|
|
160
|
-
removeEmptyDirs(dir,
|
|
239
|
+
removeEmptyDirs(dir, resolvedTarget);
|
|
161
240
|
}
|
|
162
241
|
|
|
163
242
|
// ── e) Remove manifest ────────────────────────────────────────────────
|
|
@@ -168,9 +247,15 @@ async function run(options) {
|
|
|
168
247
|
console.log('');
|
|
169
248
|
success(bold('Feature ADR skill pack removal complete!'));
|
|
170
249
|
console.log(` ${green('\u2713')} ${removedCount} file(s) removed`);
|
|
250
|
+
if (preservedCount > 0) {
|
|
251
|
+
console.log(` ${green('\u2713')} preserved (shared with Keysarium): ${preservedCount} file(s)`);
|
|
252
|
+
}
|
|
171
253
|
if (skippedCount > 0) {
|
|
172
254
|
console.log(` ${dim('-')} ${skippedCount} file(s) already missing (skipped)`);
|
|
173
255
|
}
|
|
256
|
+
if (outsideEntries.length > 0) {
|
|
257
|
+
printOutsideWarning(outsideEntries);
|
|
258
|
+
}
|
|
174
259
|
console.log('');
|
|
175
260
|
info(`To reinstall, run: ${cyan('@dzhechkov/skills-feature-adr init')}`);
|
|
176
261
|
console.log('');
|
package/src/commands/update.js
CHANGED
|
@@ -16,7 +16,7 @@ const {
|
|
|
16
16
|
// ---------------------------------------------------------------------------
|
|
17
17
|
|
|
18
18
|
async function run(options) {
|
|
19
|
-
const { dryRun, targetDir } = options;
|
|
19
|
+
const { dryRun, force, targetDir } = options;
|
|
20
20
|
const manifestPath = path.join(targetDir, MANIFEST_FILE);
|
|
21
21
|
|
|
22
22
|
// ── a) Read existing manifest ─────────────────────────────────────────
|
|
@@ -53,6 +53,10 @@ async function run(options) {
|
|
|
53
53
|
let totalModified = 0;
|
|
54
54
|
let totalUnchanged = 0;
|
|
55
55
|
const filesToCopy = [];
|
|
56
|
+
// Files the user has locally modified. These are markdown prompt files users
|
|
57
|
+
// are expected to tune, so by default we KEEP them (never clobber). Only
|
|
58
|
+
// --force overwrites them, and then only after writing a .bak sibling.
|
|
59
|
+
const keptModified = [];
|
|
56
60
|
|
|
57
61
|
for (const key of installedKeys) {
|
|
58
62
|
// Optional components (--with-learning / --knowledge-extractor) live in
|
|
@@ -73,6 +77,28 @@ async function run(options) {
|
|
|
73
77
|
continue;
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
// Single-file components (--with-learning / --knowledge-extractor extras like
|
|
81
|
+
// lib/memory-protocol.md): diffFiles/getRelativePaths readdir a DIRECTORY and
|
|
82
|
+
// cannot handle them \u2014 without this branch, a deleted learning file is never
|
|
83
|
+
// detected ("Everything is up to date!" while doctor flags it missing).
|
|
84
|
+
if (comp.isFile) {
|
|
85
|
+
if (!fileExists(destBase)) {
|
|
86
|
+
filesToCopy.push({ src: srcBase, dest: destBase, status: 'added', relPath: comp.src });
|
|
87
|
+
totalAdded++;
|
|
88
|
+
} else if (!fs.readFileSync(srcBase).equals(fs.readFileSync(destBase))) {
|
|
89
|
+
const entry = { src: srcBase, dest: destBase, status: 'modified', relPath: comp.src };
|
|
90
|
+
if (force) {
|
|
91
|
+
filesToCopy.push(entry);
|
|
92
|
+
totalModified++;
|
|
93
|
+
} else {
|
|
94
|
+
keptModified.push(entry);
|
|
95
|
+
}
|
|
96
|
+
} else {
|
|
97
|
+
totalUnchanged++;
|
|
98
|
+
}
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
|
|
76
102
|
const filterFn = getComponentFilter(comp);
|
|
77
103
|
const diff = diffFiles(srcBase, destBase, filterFn);
|
|
78
104
|
|
|
@@ -87,13 +113,18 @@ async function run(options) {
|
|
|
87
113
|
}
|
|
88
114
|
|
|
89
115
|
for (const rel of diff.modified) {
|
|
90
|
-
|
|
116
|
+
const entry = {
|
|
91
117
|
src: path.join(srcBase, rel),
|
|
92
118
|
dest: path.join(destBase, rel),
|
|
93
119
|
status: 'modified',
|
|
94
120
|
relPath: path.join(comp.src, rel),
|
|
95
|
-
}
|
|
96
|
-
|
|
121
|
+
};
|
|
122
|
+
if (force) {
|
|
123
|
+
filesToCopy.push(entry);
|
|
124
|
+
totalModified++;
|
|
125
|
+
} else {
|
|
126
|
+
keptModified.push(entry);
|
|
127
|
+
}
|
|
97
128
|
}
|
|
98
129
|
|
|
99
130
|
totalUnchanged += diff.unchanged.length;
|
|
@@ -103,11 +134,26 @@ async function run(options) {
|
|
|
103
134
|
info(bold('Update summary:'));
|
|
104
135
|
console.log(` ${green('+')} ${totalAdded} file(s) to add`);
|
|
105
136
|
console.log(` ${yellow('~')} ${totalModified} file(s) to update`);
|
|
137
|
+
if (keptModified.length > 0) {
|
|
138
|
+
console.log(` ${yellow('\u26a0')} ${keptModified.length} file(s) locally modified, kept`);
|
|
139
|
+
}
|
|
106
140
|
console.log(` ${dim('=')} ${totalUnchanged} file(s) unchanged`);
|
|
107
141
|
console.log('');
|
|
108
142
|
|
|
143
|
+
if (keptModified.length > 0) {
|
|
144
|
+
console.log(yellow('\u26a0') + ' ' + bold('locally modified, kept (use --force to overwrite):'));
|
|
145
|
+
for (const f of keptModified) {
|
|
146
|
+
console.log(` ${yellow('~ KEEP')} ${f.relPath}`);
|
|
147
|
+
}
|
|
148
|
+
console.log('');
|
|
149
|
+
}
|
|
150
|
+
|
|
109
151
|
if (totalAdded === 0 && totalModified === 0) {
|
|
110
|
-
|
|
152
|
+
if (keptModified.length > 0) {
|
|
153
|
+
success('No files to update \u2014 locally modified file(s) kept.');
|
|
154
|
+
} else {
|
|
155
|
+
success('Everything is up to date!');
|
|
156
|
+
}
|
|
111
157
|
process.exit(0);
|
|
112
158
|
}
|
|
113
159
|
|
|
@@ -115,7 +161,8 @@ async function run(options) {
|
|
|
115
161
|
console.log(bold('Files to be changed:'));
|
|
116
162
|
for (const f of filesToCopy) {
|
|
117
163
|
const marker = f.status === 'added' ? green('+ ADD') : yellow('~ MOD');
|
|
118
|
-
|
|
164
|
+
const note = f.status === 'modified' ? dim(' (will back up to .bak)') : '';
|
|
165
|
+
console.log(` ${marker} ${f.relPath}${note}`);
|
|
119
166
|
}
|
|
120
167
|
console.log('');
|
|
121
168
|
warn('Dry run \u2014 no files were written.');
|
|
@@ -129,11 +176,18 @@ async function run(options) {
|
|
|
129
176
|
step(i + 1, filesToCopy.length, `${label} ${f.relPath}`);
|
|
130
177
|
|
|
131
178
|
ensureDir(path.dirname(f.dest));
|
|
179
|
+
// --force on a locally modified file: preserve the user's version as a
|
|
180
|
+
// sibling .bak before overwriting.
|
|
181
|
+
if (f.status === 'modified' && fileExists(f.dest)) {
|
|
182
|
+
fs.copyFileSync(f.dest, `${f.dest}.bak`);
|
|
183
|
+
console.log(` ${dim(`backed up to ${f.relPath}.bak`)}`);
|
|
184
|
+
}
|
|
132
185
|
fs.copyFileSync(f.src, f.dest);
|
|
133
186
|
}
|
|
134
187
|
|
|
135
188
|
// ── e) Update manifest ────────────────────────────────────────────────
|
|
136
189
|
const allFiles = [];
|
|
190
|
+
const prevFiles = Array.isArray(manifest.files) ? manifest.files : [];
|
|
137
191
|
for (const key of installedKeys) {
|
|
138
192
|
const comp = COMPONENTS[key] || OPTIONAL_COMPONENTS[key];
|
|
139
193
|
if (!comp) continue;
|
|
@@ -142,12 +196,31 @@ async function run(options) {
|
|
|
142
196
|
const destPath = path.join(targetDir, comp.src);
|
|
143
197
|
const filterFn = getComponentFilter(comp);
|
|
144
198
|
|
|
145
|
-
// Record from the TEMPLATE source
|
|
146
|
-
|
|
199
|
+
// Record from the TEMPLATE source only — NEVER scan destPath, or user-owned
|
|
200
|
+
// files get adopted into manifest.files (and a later remove would delete
|
|
201
|
+
// them). If the template source is missing, keep the previous manifest
|
|
202
|
+
// entries for this component verbatim.
|
|
203
|
+
if (!fileExists(srcPath)) {
|
|
204
|
+
const kept = prevFiles.filter(
|
|
205
|
+
(rel) => rel === comp.src || rel.startsWith(comp.src + path.sep)
|
|
206
|
+
);
|
|
207
|
+
allFiles.push(...kept);
|
|
208
|
+
warn(`component '${key}' not found in current templates — manifest entries kept as-is`);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Single-file components: record the file itself. getRelativePaths() would
|
|
213
|
+
// readdir it — without this branch the 3 learning files silently drop out of
|
|
214
|
+
// the manifest on every rebuild, and a later `remove` strands them.
|
|
215
|
+
if (comp.isFile) {
|
|
216
|
+
if (fileExists(destPath)) allFiles.push(comp.src);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
|
|
147
220
|
if (fileExists(destPath)) {
|
|
148
221
|
const paths = filterFn
|
|
149
|
-
? getRelativePathsFiltered(
|
|
150
|
-
: getRelativePaths(
|
|
222
|
+
? getRelativePathsFiltered(srcPath, filterFn)
|
|
223
|
+
: getRelativePaths(srcPath);
|
|
151
224
|
allFiles.push(...paths.map((rel) => path.join(comp.src, rel)));
|
|
152
225
|
}
|
|
153
226
|
}
|
|
@@ -164,6 +237,9 @@ async function run(options) {
|
|
|
164
237
|
success(bold('Update complete!'));
|
|
165
238
|
console.log(` ${green('+')} ${totalAdded} file(s) added`);
|
|
166
239
|
console.log(` ${yellow('~')} ${totalModified} file(s) updated`);
|
|
240
|
+
if (keptModified.length > 0) {
|
|
241
|
+
console.log(` ${yellow('⚠')} ${keptModified.length} file(s) locally modified, kept`);
|
|
242
|
+
}
|
|
167
243
|
console.log(` ${dim('=')} ${totalUnchanged} file(s) unchanged`);
|
|
168
244
|
console.log('');
|
|
169
245
|
|