dw-kit 1.2.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.
@@ -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
+ }