dw-kit 1.1.0 → 1.2.1

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.
@@ -135,7 +135,7 @@ async function setupProject(projectDir, { projectName, depth, roles, language, a
135
135
 
136
136
  if (adapter === 'claude-cli') {
137
137
  copyClaudeFiles(projectDir);
138
- copyCLAUDEmd(projectDir);
138
+ createMinimalCLAUDEmd(projectDir, projectName);
139
139
  } else if (adapter === 'cursor') {
140
140
  copyCursorFiles(projectDir);
141
141
  copyGenericAdapter(projectDir);
@@ -191,34 +191,34 @@ function copyClaudeFiles(projectDir) {
191
191
  if (skipCount > 0) log(` ${skipCount} existing files preserved`);
192
192
  }
193
193
 
194
- function copyCLAUDEmd(projectDir) {
195
- const src = join(TOOLKIT_ROOT, 'CLAUDE.md');
194
+ function createMinimalCLAUDEmd(projectDir, projectName) {
196
195
  const dst = join(projectDir, 'CLAUDE.md');
197
196
 
198
197
  if (existsSync(dst)) {
199
- warn('CLAUDE.md already exists skipping (review manually if needed)');
198
+ // User already has their own CLAUDE.md — leave it completely alone
200
199
  return;
201
200
  }
202
201
 
203
- copyFile(src, dst);
202
+ const content = `# ${projectName}
203
+
204
+ > Add your project description here.
204
205
 
205
- const techStackSection = `
206
206
  ---
207
207
 
208
208
  ## Tech Stack
209
209
 
210
210
  <!-- Update with your project's actual stack -->
211
- - Framework: [e.g. NestJS / Django / Laravel / Next.js]
212
- - Database: [e.g. PostgreSQL / MySQL / MongoDB]
213
- - Testing: [e.g. Jest / Pytest / PHPUnit]
211
+ - Framework:
212
+ - Database:
213
+ - Testing:
214
214
 
215
215
  ## Project-Specific Rules
216
216
 
217
- <!-- Add project-specific rules -->
218
- - [Rule 1]
217
+ <!-- Add project-specific rules here -->
218
+ <!-- dw workflow rules are auto-loaded from .claude/rules/ -->
219
219
  `;
220
- appendFileSync(dst, techStackSection, 'utf-8');
221
- ok('CLAUDE.md (with Tech Stack template section)');
220
+ writeFileSync(dst, content, 'utf-8');
221
+ ok('CLAUDE.md (minimal project template — dw rules are in .claude/rules/)');
222
222
  }
223
223
 
224
224
  function copyCursorFiles(projectDir) {
@@ -276,7 +276,7 @@ function createRuntimeDirs(projectDir) {
276
276
 
277
277
  function updateGitignore(projectDir) {
278
278
  const gitignorePath = join(projectDir, '.gitignore');
279
- const entriesToAdd = ['CLAUDE.local.md', '.claude/settings.local.json'];
279
+ const entriesToAdd = ['CLAUDE.local.md', '.claude/settings.local.json', '.dw/config/dw.config.local.yml'];
280
280
 
281
281
  if (existsSync(gitignorePath)) {
282
282
  const content = readFileSync(gitignorePath, 'utf-8');
@@ -304,8 +304,8 @@ function printSummary({ projectName, depth, roles, language, adapter }) {
304
304
  console.log(` Files created:`);
305
305
  console.log(` .dw/ — core/, config/, adapters/, tasks, docs`);
306
306
  if (adapter === 'claude-cli') {
307
- console.log(` .claude/ — skills, agents, hooks, rules`);
308
- console.log(` CLAUDE.md`);
307
+ console.log(` .claude/ — skills, agents, hooks, rules/`);
308
+ console.log(` CLAUDE.md — minimal project template (dw rules in .claude/rules/)`);
309
309
  } else if (adapter === 'cursor') {
310
310
  console.log(` .cursor/rules/ — workflow rules for Cursor`);
311
311
  console.log(` AGENT.md — methodology reference`);
@@ -317,7 +317,7 @@ function printSummary({ projectName, depth, roles, language, adapter }) {
317
317
  console.log(` Next steps:`);
318
318
  console.log(` Run: claude (to open Claude Code in this directory in terminal)`);
319
319
  console.log(` Run: /dw-flow [task-name]`);
320
- console.log(` Suggested: Update Tech Stack in CLAUDE.md (optional, recommended)`);
320
+ console.log(` Suggested: Update Tech Stack + rules in CLAUDE.md`);
321
321
  } else if (adapter === 'cursor') {
322
322
  console.log(` Next steps:`);
323
323
  console.log(` 1. Open Cursor in this directory`);
@@ -1,262 +1,297 @@
1
- import { existsSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
2
- import { join, resolve } from 'node:path';
3
- import { fileURLToPath } from 'node:url';
4
- import { header, ok, warn, err, info, log, dry } from '../lib/ui.mjs';
5
- import { loadConfig, writeConfig, getToolkitVersions } from '../lib/config.mjs';
6
- import { diffDirs, copyDir, copyFile } from '../lib/copy.mjs';
7
-
8
- const TOOLKIT_ROOT = resolve(fileURLToPath(import.meta.url), '..', '..', '..');
9
-
10
- export async function upgradeCommand(opts) {
11
- const projectDir = process.cwd();
12
- const configPath = join(projectDir, '.dw', 'config', 'dw.config.yml');
13
-
14
- if (!existsSync(configPath)) {
15
- err('No .dw/config/dw.config.yml found. Run `dw init` first.');
16
- process.exit(1);
17
- }
18
-
19
- const projectConfig = loadConfig(configPath);
20
- const projectVersions = getToolkitVersions(projectConfig);
21
-
22
- const toolkitPkg = JSON.parse(readFileSync(join(TOOLKIT_ROOT, 'package.json'), 'utf-8'));
23
- const toolkitConfig = loadConfig(join(TOOLKIT_ROOT, '.dw', 'config', 'dw.config.yml'));
24
- const toolkitVersions = getToolkitVersions(toolkitConfig);
25
-
26
- header('dw-kit Upgrade');
27
- log(`Installed (package) : v${toolkitPkg.version}`);
28
- log(`Project core : ${projectVersions.core}`);
29
- log(`Toolkit core : ${toolkitVersions.core}`);
30
- if (opts.dryRun) log('Mode: DRY RUN (no changes)');
31
- console.log();
32
-
33
- if (opts.check) {
34
- if (projectVersions.core === toolkitVersions.core) {
35
- ok('Already up to date.');
36
- } else {
37
- log(`Update available: ${projectVersions.core} → ${toolkitVersions.core}`);
38
- }
39
- return;
40
- }
41
-
42
- const layer = opts.layer || 'all';
43
- let totalChanges = 0;
44
-
45
- if (layer === 'all' || layer === 'core') {
46
- totalChanges += upgradeCore(projectDir, opts);
47
- }
48
-
49
- if (layer === 'all' || layer === 'platform') {
50
- totalChanges += upgradePlatform(projectDir, opts);
51
- }
52
-
53
- if (layer === 'all' || layer === 'capability') {
54
- totalChanges += upgradeCapability(projectDir, opts);
55
- }
56
-
57
- upgradeScripts(projectDir, opts);
58
- upgradeConfigSchema(projectDir, opts);
59
-
60
- if (!opts.dryRun && totalChanges > 0) {
61
- updateVersionTracking(configPath, projectConfig, toolkitVersions);
62
- }
63
-
64
- console.log();
65
- header(opts.dryRun ? 'DRY RUN complete — no changes made' : `Upgrade complete (${totalChanges} files updated)`);
66
- if (opts.dryRun) log('Run without --dry-run to apply.');
67
- console.log();
68
- }
69
-
70
- function upgradeCore(projectDir, opts) {
71
- info('Layer 0: Methodology Core (.dw/core/)');
72
- const src = join(TOOLKIT_ROOT, '.dw', 'core');
73
- const dst = join(projectDir, '.dw', 'core');
74
-
75
- const diff = diffDirs(src, dst);
76
- reportDiff(diff);
77
-
78
- if (diff.added.length === 0 && diff.modified.length === 0) {
79
- ok('Core files are up to date');
80
- return 0;
81
- }
82
-
83
- const filesToUpdate = [...diff.added, ...diff.modified];
84
- for (const file of filesToUpdate) {
85
- if (opts.dryRun) {
86
- dry(`${diff.added.includes(file) ? 'add' : 'update'} .dw/core/${file}`);
87
- } else {
88
- copyFile(join(src, file), join(dst, file));
89
- ok(`.dw/core/${file}`);
90
- }
91
- }
92
- return filesToUpdate.length;
93
- }
94
-
95
- function upgradePlatform(projectDir, opts) {
96
- info('Layer 1: Platform Files (.claude/)');
97
- const src = join(TOOLKIT_ROOT, '.claude');
98
- const dst = join(projectDir, '.claude');
99
-
100
- if (!existsSync(dst)) {
101
- warn('.claude/ not found — skipping platform upgrade (run dw init first)');
102
- return 0;
103
- }
104
-
105
- const overridesDir = join(projectDir, '.dw', 'adapters', 'claude-cli', 'overrides');
106
- const diff = diffDirs(src, dst);
107
- reportDiff(diff);
108
-
109
- if (diff.added.length === 0 && diff.modified.length === 0) {
110
- ok('Platform files are up to date');
111
- return 0;
112
- }
113
-
114
- let count = 0;
115
- const filesToUpdate = [...diff.added, ...diff.modified];
116
- for (const file of filesToUpdate) {
117
- const overridePath = join(overridesDir, file);
118
- if (existsSync(overridePath)) {
119
- warn(`${file}: override exists → keeping your version`);
120
- continue;
121
- }
122
-
123
- if (opts.dryRun) {
124
- dry(`${diff.added.includes(file) ? 'add' : 'update'} .claude/${file}`);
125
- } else {
126
- copyFile(join(src, file), join(dst, file));
127
- ok(`.claude/${file}`);
128
- }
129
- count++;
130
- }
131
-
132
- copyExtensions(projectDir, opts);
133
- mergeSettingsJson(projectDir, opts);
134
-
135
- return count;
136
- }
137
-
138
- function copyExtensions(projectDir, opts) {
139
- const extDir = join(projectDir, '.dw', 'adapters', 'claude-cli', 'extensions');
140
- const skillsDir = join(projectDir, '.claude', 'skills');
141
-
142
- if (!existsSync(extDir)) return;
143
-
144
- let count = 0;
145
- try {
146
- const entries = readdirSync(extDir, { withFileTypes: true });
147
- for (const entry of entries) {
148
- if (!entry.isDirectory() || entry.name === '.gitkeep') continue;
149
- const src = join(extDir, entry.name);
150
- const dst = join(skillsDir, entry.name);
151
- if (opts.dryRun) {
152
- dry(`install extension: ${entry.name}`);
153
- } else {
154
- copyDir(src, dst, { overwrite: true });
155
- ok(`Extension '${entry.name}' installed`);
156
- }
157
- count++;
158
- }
159
- } catch { /* empty extensions dir */ }
160
- if (count === 0) log('No extensions found');
161
- }
162
-
163
- function mergeSettingsJson(projectDir, opts) {
164
- const toolkitSettings = join(TOOLKIT_ROOT, '.claude', 'settings.json');
165
- const projectSettings = join(projectDir, '.claude', 'settings.json');
166
-
167
- if (!existsSync(toolkitSettings) || !existsSync(projectSettings)) return;
168
-
169
- if (opts.dryRun) {
170
- dry('merge .claude/settings.json');
171
- return;
172
- }
173
-
174
- try {
175
- const template = JSON.parse(readFileSync(toolkitSettings, 'utf-8'));
176
- const current = JSON.parse(readFileSync(projectSettings, 'utf-8'));
177
- const merged = deepMerge(template, current);
178
- writeFileSync(projectSettings, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
179
- ok('settings.json: merged');
180
- } catch (e) {
181
- warn(`settings.json merge failed: ${e.message}`);
182
- }
183
- }
184
-
185
- function deepMerge(base, override) {
186
- const result = { ...base };
187
- for (const [key, val] of Object.entries(override)) {
188
- if (key in result && typeof result[key] === 'object' && !Array.isArray(result[key]) && typeof val === 'object' && !Array.isArray(val)) {
189
- result[key] = deepMerge(result[key], val);
190
- } else {
191
- result[key] = val;
192
- }
193
- }
194
- return result;
195
- }
196
-
197
- function upgradeCapability(projectDir, opts) {
198
- info('Layer 2: Capability Config');
199
- ok('Capability layer is config-driven — no file changes needed');
200
- log('Review claude: section in .dw/config/dw.config.yml for new options');
201
- return 0;
202
- }
203
-
204
- function upgradeScripts(projectDir, opts) {
205
- info('Scripts');
206
- const src = join(TOOLKIT_ROOT, 'scripts');
207
- const dst = join(projectDir, 'scripts');
208
-
209
- if (!existsSync(src)) return;
210
-
211
- const diff = diffDirs(src, dst);
212
- if (diff.added.length === 0 && diff.modified.length === 0) {
213
- ok('Scripts are up to date');
214
- return;
215
- }
216
-
217
- for (const file of [...diff.added, ...diff.modified]) {
218
- if (opts.dryRun) {
219
- dry(`update scripts/${file}`);
220
- } else {
221
- copyFile(join(src, file), join(dst, file));
222
- ok(`scripts/${file}`);
223
- }
224
- }
225
- }
226
-
227
- function upgradeConfigSchema(projectDir, opts) {
228
- const src = join(TOOLKIT_ROOT, '.dw', 'config', 'config.schema.json');
229
- const dst = join(projectDir, '.dw', 'config', 'config.schema.json');
230
-
231
- if (!existsSync(src)) return;
232
- if (existsSync(dst)) {
233
- const srcContent = readFileSync(src, 'utf-8');
234
- const dstContent = readFileSync(dst, 'utf-8');
235
- if (srcContent === dstContent) return;
236
- }
237
-
238
- if (opts.dryRun) {
239
- dry('update .dw/config/config.schema.json');
240
- } else {
241
- copyFile(src, dst);
242
- ok('.dw/config/config.schema.json updated');
243
- }
244
- }
245
-
246
- function updateVersionTracking(configPath, config, toolkitVersions) {
247
- const today = new Date().toISOString().split('T')[0];
248
- if (!config._toolkit) config._toolkit = {};
249
- config._toolkit.core_version = toolkitVersions.core;
250
- config._toolkit.platform_version = toolkitVersions.platform;
251
- config._toolkit.capability_version = toolkitVersions.capability;
252
- config._toolkit.last_upgrade = today;
253
-
254
- writeConfig(configPath, config);
255
- ok(`Version tracking updated: core=${toolkitVersions.core}, date=${today}`);
256
- }
257
-
258
- function reportDiff(diff) {
259
- if (diff.added.length > 0) log(` New files: ${diff.added.length}`);
260
- if (diff.modified.length > 0) log(` Modified: ${diff.modified.length}`);
261
- if (diff.unchanged.length > 0) log(` Unchanged: ${diff.unchanged.length}`);
262
- }
1
+ import { existsSync, readFileSync, writeFileSync, readdirSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { header, ok, warn, err, info, log, dry } from '../lib/ui.mjs';
5
+ import { loadConfig, writeConfig, getToolkitVersions } from '../lib/config.mjs';
6
+ import { diffDirs, copyDir, copyFile } from '../lib/copy.mjs';
7
+
8
+ const TOOLKIT_ROOT = resolve(fileURLToPath(import.meta.url), '..', '..', '..');
9
+
10
+ export async function upgradeCommand(opts) {
11
+ const projectDir = process.cwd();
12
+ const configPath = join(projectDir, '.dw', 'config', 'dw.config.yml');
13
+
14
+ if (!existsSync(configPath)) {
15
+ err('No .dw/config/dw.config.yml found. Run `dw init` first.');
16
+ process.exit(1);
17
+ }
18
+
19
+ const projectConfig = loadConfig(configPath);
20
+ const projectVersions = getToolkitVersions(projectConfig);
21
+
22
+ const toolkitPkg = JSON.parse(readFileSync(join(TOOLKIT_ROOT, 'package.json'), 'utf-8'));
23
+ const toolkitConfig = loadConfig(join(TOOLKIT_ROOT, '.dw', 'config', 'dw.config.yml'));
24
+ const toolkitVersions = getToolkitVersions(toolkitConfig);
25
+
26
+ header('dw-kit Upgrade');
27
+ log(`Installed (package) : v${toolkitPkg.version}`);
28
+ log(`Project core : ${projectVersions.core}`);
29
+ log(`Toolkit core : ${toolkitVersions.core}`);
30
+ if (opts.dryRun) log('Mode: DRY RUN (no changes)');
31
+ console.log();
32
+
33
+ if (opts.check) {
34
+ if (projectVersions.core === toolkitVersions.core) {
35
+ ok('Already up to date.');
36
+ } else {
37
+ log(`Update available: ${projectVersions.core} → ${toolkitVersions.core}`);
38
+ }
39
+ return;
40
+ }
41
+
42
+ const layer = opts.layer || 'all';
43
+ let totalChanges = 0;
44
+
45
+ if (layer === 'all' || layer === 'core') {
46
+ totalChanges += upgradeCore(projectDir, opts);
47
+ }
48
+
49
+ if (layer === 'all' || layer === 'platform') {
50
+ totalChanges += upgradePlatform(projectDir, opts);
51
+ upgradeGitattributes(projectDir, opts);
52
+ }
53
+
54
+ if (layer === 'all' || layer === 'capability') {
55
+ totalChanges += upgradeCapability(projectDir, opts);
56
+ }
57
+
58
+ upgradeScripts(projectDir, opts);
59
+ upgradeConfigSchema(projectDir, opts);
60
+
61
+ if (!opts.dryRun && totalChanges > 0) {
62
+ updateVersionTracking(configPath, projectConfig, toolkitVersions);
63
+ }
64
+
65
+ console.log();
66
+ header(opts.dryRun ? 'DRY RUN complete no changes made' : `Upgrade complete (${totalChanges} files updated)`);
67
+ if (opts.dryRun) log('Run without --dry-run to apply.');
68
+ console.log();
69
+ }
70
+
71
+ function upgradeCore(projectDir, opts) {
72
+ info('Layer 0: Methodology Core (.dw/core/)');
73
+ const src = join(TOOLKIT_ROOT, '.dw', 'core');
74
+ const dst = join(projectDir, '.dw', 'core');
75
+
76
+ const diff = diffDirs(src, dst);
77
+ reportDiff(diff);
78
+
79
+ if (diff.added.length === 0 && diff.modified.length === 0) {
80
+ ok('Core files are up to date');
81
+ return 0;
82
+ }
83
+
84
+ const filesToUpdate = [...diff.added, ...diff.modified];
85
+ for (const file of filesToUpdate) {
86
+ if (opts.dryRun) {
87
+ dry(`${diff.added.includes(file) ? 'add' : 'update'} .dw/core/${file}`);
88
+ } else {
89
+ copyFile(join(src, file), join(dst, file));
90
+ ok(`.dw/core/${file}`);
91
+ }
92
+ }
93
+ return filesToUpdate.length;
94
+ }
95
+
96
+ function upgradePlatform(projectDir, opts) {
97
+ info('Layer 1: Platform Files (.claude/)');
98
+ const src = join(TOOLKIT_ROOT, '.claude');
99
+ const dst = join(projectDir, '.claude');
100
+
101
+ if (!existsSync(dst)) {
102
+ warn('.claude/ not found — skipping platform upgrade (run dw init first)');
103
+ return 0;
104
+ }
105
+
106
+ const overridesDir = join(projectDir, '.dw', 'adapters', 'claude-cli', 'overrides');
107
+ const diff = diffDirs(src, dst);
108
+ reportDiff(diff);
109
+
110
+ if (diff.added.length === 0 && diff.modified.length === 0) {
111
+ ok('Platform files are up to date');
112
+ return 0;
113
+ }
114
+
115
+ let count = 0;
116
+ const filesToUpdate = [...diff.added, ...diff.modified];
117
+ for (const file of filesToUpdate) {
118
+ const overridePath = join(overridesDir, file);
119
+ if (existsSync(overridePath)) {
120
+ warn(`${file}: override exists → keeping your version`);
121
+ continue;
122
+ }
123
+
124
+ if (opts.dryRun) {
125
+ dry(`${diff.added.includes(file) ? 'add' : 'update'} .claude/${file}`);
126
+ } else {
127
+ copyFile(join(src, file), join(dst, file));
128
+ ok(`.claude/${file}`);
129
+ }
130
+ count++;
131
+ }
132
+
133
+ copyExtensions(projectDir, opts);
134
+ mergeSettingsJson(projectDir, opts);
135
+
136
+ return count;
137
+ }
138
+
139
+ function copyExtensions(projectDir, opts) {
140
+ const extDir = join(projectDir, '.dw', 'adapters', 'claude-cli', 'extensions');
141
+ const skillsDir = join(projectDir, '.claude', 'skills');
142
+
143
+ if (!existsSync(extDir)) return;
144
+
145
+ let count = 0;
146
+ try {
147
+ const entries = readdirSync(extDir, { withFileTypes: true });
148
+ for (const entry of entries) {
149
+ if (!entry.isDirectory() || entry.name === '.gitkeep') continue;
150
+ const src = join(extDir, entry.name);
151
+ const dst = join(skillsDir, entry.name);
152
+ if (opts.dryRun) {
153
+ dry(`install extension: ${entry.name}`);
154
+ } else {
155
+ copyDir(src, dst, { overwrite: true });
156
+ ok(`Extension '${entry.name}' installed`);
157
+ }
158
+ count++;
159
+ }
160
+ } catch { /* empty extensions dir */ }
161
+ if (count === 0) log('No extensions found');
162
+ }
163
+
164
+ function mergeSettingsJson(projectDir, opts) {
165
+ const toolkitSettings = join(TOOLKIT_ROOT, '.claude', 'settings.json');
166
+ const projectSettings = join(projectDir, '.claude', 'settings.json');
167
+
168
+ if (!existsSync(toolkitSettings) || !existsSync(projectSettings)) return;
169
+
170
+ if (opts.dryRun) {
171
+ dry('merge .claude/settings.json');
172
+ return;
173
+ }
174
+
175
+ try {
176
+ const template = JSON.parse(readFileSync(toolkitSettings, 'utf-8'));
177
+ const current = JSON.parse(readFileSync(projectSettings, 'utf-8'));
178
+ const merged = deepMerge(template, current);
179
+ writeFileSync(projectSettings, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
180
+ ok('settings.json: merged');
181
+ } catch (e) {
182
+ warn(`settings.json merge failed: ${e.message}`);
183
+ }
184
+ }
185
+
186
+ function deepMerge(base, override) {
187
+ const result = { ...base };
188
+ for (const [key, val] of Object.entries(override)) {
189
+ if (key in result && typeof result[key] === 'object' && !Array.isArray(result[key]) && typeof val === 'object' && !Array.isArray(val)) {
190
+ result[key] = deepMerge(result[key], val);
191
+ } else {
192
+ result[key] = val;
193
+ }
194
+ }
195
+ return result;
196
+ }
197
+
198
+ function upgradeGitattributes(projectDir, opts) {
199
+ info('Gitattributes');
200
+ const dst = join(projectDir, '.gitattributes');
201
+
202
+ // Entries dw-kit needs in the user's repo to survive git checkout on Windows
203
+ const requiredEntries = [
204
+ '.claude/hooks/*.sh text eol=lf',
205
+ '.claude/skills/**/*.sh text eol=lf',
206
+ ];
207
+
208
+ const existing = existsSync(dst)
209
+ ? readFileSync(dst, 'utf-8').replace(/\r\n/g, '\n')
210
+ : '';
211
+
212
+ const missing = requiredEntries.filter(e => !existing.includes(e));
213
+
214
+ if (missing.length === 0) {
215
+ ok('.gitattributes: dw-kit entries already present');
216
+ return;
217
+ }
218
+
219
+ if (opts.dryRun) {
220
+ missing.forEach(e => dry(`add to .gitattributes: ${e}`));
221
+ return;
222
+ }
223
+
224
+ // Append block (idempotent: only adds what's missing)
225
+ const separator = existing.length > 0 && !existing.endsWith('\n') ? '\n' : '';
226
+ const block = `${separator}\n# dw-kit: enforce LF for shell scripts (cross-platform safety)\n${missing.join('\n')}\n`;
227
+ writeFileSync(dst, existing + block, 'utf-8');
228
+ ok(`.gitattributes: added ${missing.length} dw-kit hook entr${missing.length > 1 ? 'ies' : 'y'}`);
229
+ log(' → Tip: run `git add --renormalize .claude/hooks/` to normalize any existing checked-out files');
230
+ }
231
+
232
+ function upgradeCapability(projectDir, opts) {
233
+ info('Layer 2: Capability Config');
234
+ ok('Capability layer is config-driven — no file changes needed');
235
+ log('Review claude: section in .dw/config/dw.config.yml for new options');
236
+ return 0;
237
+ }
238
+
239
+ function upgradeScripts(projectDir, opts) {
240
+ info('Scripts');
241
+ const src = join(TOOLKIT_ROOT, 'scripts');
242
+ const dst = join(projectDir, 'scripts');
243
+
244
+ if (!existsSync(src)) return;
245
+
246
+ const diff = diffDirs(src, dst);
247
+ if (diff.added.length === 0 && diff.modified.length === 0) {
248
+ ok('Scripts are up to date');
249
+ return;
250
+ }
251
+
252
+ for (const file of [...diff.added, ...diff.modified]) {
253
+ if (opts.dryRun) {
254
+ dry(`update scripts/${file}`);
255
+ } else {
256
+ copyFile(join(src, file), join(dst, file));
257
+ ok(`scripts/${file}`);
258
+ }
259
+ }
260
+ }
261
+
262
+ function upgradeConfigSchema(projectDir, opts) {
263
+ const src = join(TOOLKIT_ROOT, '.dw', 'config', 'config.schema.json');
264
+ const dst = join(projectDir, '.dw', 'config', 'config.schema.json');
265
+
266
+ if (!existsSync(src)) return;
267
+ if (existsSync(dst)) {
268
+ const srcContent = readFileSync(src, 'utf-8');
269
+ const dstContent = readFileSync(dst, 'utf-8');
270
+ if (srcContent === dstContent) return;
271
+ }
272
+
273
+ if (opts.dryRun) {
274
+ dry('update .dw/config/config.schema.json');
275
+ } else {
276
+ copyFile(src, dst);
277
+ ok('.dw/config/config.schema.json updated');
278
+ }
279
+ }
280
+
281
+ function updateVersionTracking(configPath, config, toolkitVersions) {
282
+ const today = new Date().toISOString().split('T')[0];
283
+ if (!config._toolkit) config._toolkit = {};
284
+ config._toolkit.core_version = toolkitVersions.core;
285
+ config._toolkit.platform_version = toolkitVersions.platform;
286
+ config._toolkit.capability_version = toolkitVersions.capability;
287
+ config._toolkit.last_upgrade = today;
288
+
289
+ writeConfig(configPath, config);
290
+ ok(`Version tracking updated: core=${toolkitVersions.core}, date=${today}`);
291
+ }
292
+
293
+ function reportDiff(diff) {
294
+ if (diff.added.length > 0) log(` New files: ${diff.added.length}`);
295
+ if (diff.modified.length > 0) log(` Modified: ${diff.modified.length}`);
296
+ if (diff.unchanged.length > 0) log(` Unchanged: ${diff.unchanged.length}`);
297
+ }