dw-kit 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/hooks/post-write.sh +64 -58
- package/.claude/hooks/pre-commit-gate.sh +96 -90
- package/.claude/hooks/privacy-block.sh +99 -94
- package/.claude/hooks/progress-ping.sh +53 -47
- package/.claude/hooks/safety-guard.sh +60 -54
- package/.claude/hooks/scout-block.sh +88 -82
- package/.claude/hooks/session-init.sh +91 -74
- package/.claude/hooks/stop-check.sh +88 -36
- package/.claude/hooks/telemetry-log.sh +34 -0
- package/.claude/rules/code-style.md +37 -37
- package/.claude/rules/commit-standards.md +37 -37
- package/.claude/rules/dw.md +136 -0
- package/.claude/settings.json +120 -99
- package/.claude/skills/dw-arch-review/SKILL.md +119 -119
- package/.claude/skills/dw-archive/SKILL.md +81 -81
- package/.claude/skills/dw-commit/SKILL.md +81 -81
- package/.claude/skills/dw-config-init/SKILL.md +91 -91
- package/.claude/skills/dw-config-validate/SKILL.md +75 -75
- package/.claude/skills/dw-dashboard/SKILL.md +209 -209
- package/.claude/skills/dw-debug/SKILL.md +97 -97
- package/.claude/skills/dw-decision/SKILL.md +116 -0
- package/.claude/skills/dw-docs-update/SKILL.md +125 -125
- package/.claude/skills/dw-estimate/SKILL.md +90 -90
- package/.claude/skills/dw-execute/SKILL.md +98 -98
- package/.claude/skills/dw-flow/SKILL.md +274 -274
- package/.claude/skills/dw-handoff/SKILL.md +81 -81
- package/.claude/skills/dw-kit-report/SKILL.md +152 -152
- package/.claude/skills/dw-log-work/SKILL.md +69 -69
- package/.claude/skills/dw-onboard/SKILL.md +201 -201
- package/.claude/skills/dw-plan/SKILL.md +125 -125
- package/.claude/skills/dw-prompt/SKILL.md +62 -62
- package/.claude/skills/dw-requirements/SKILL.md +98 -98
- package/.claude/skills/dw-research/SKILL.md +114 -114
- package/.claude/skills/dw-retroactive/SKILL.md +311 -311
- package/.claude/skills/dw-review/SKILL.md +66 -66
- package/.claude/skills/dw-rollback/SKILL.md +90 -90
- package/.claude/skills/dw-sprint-review/SKILL.md +99 -99
- package/.claude/skills/dw-task-init/SKILL.md +59 -59
- package/.claude/skills/dw-test-plan/SKILL.md +113 -113
- package/.claude/skills/dw-thinking/SKILL.md +70 -70
- package/.claude/skills/dw-upgrade/SKILL.md +72 -72
- package/.dw/config/dw.config.yml +82 -82
- package/.dw/core/PILLARS.md +122 -0
- package/.dw/core/templates/v2/spec.md +68 -0
- package/.dw/core/templates/v2/tracking.md +62 -0
- package/.dw/core/v14-evaluation-protocol.md +118 -0
- package/CLAUDE.md +42 -39
- package/MIGRATION-v1.3.md +201 -0
- package/README.md +43 -6
- package/package.json +86 -84
- package/src/cli.mjs +45 -9
- package/src/commands/dashboard.mjs +116 -0
- package/src/commands/doctor.mjs +165 -149
- package/src/commands/init.mjs +339 -332
- package/src/commands/metrics.mjs +165 -0
- package/src/commands/upgrade.mjs +297 -262
- package/src/lib/active-index.mjs +87 -0
- package/src/lib/copy.mjs +118 -110
- package/src/lib/cut-analysis.mjs +161 -0
- package/src/lib/telemetry.mjs +80 -0
- package/.claude/rules/dw-core.md +0 -100
- package/.claude/rules/dw-skills.md +0 -53
- package/.claude/rules/workflow-rules.md +0 -77
package/src/commands/upgrade.mjs
CHANGED
|
@@ -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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
const
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
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
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { readdirSync, readFileSync, existsSync, writeFileSync, statSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
|
|
4
|
+
const TASKS_DIR = '.dw/tasks';
|
|
5
|
+
const ACTIVE_FILE = join(TASKS_DIR, 'ACTIVE.md');
|
|
6
|
+
const EXCLUDE = new Set(['archive', 'ACTIVE.md']);
|
|
7
|
+
|
|
8
|
+
function parseFrontmatter(content) {
|
|
9
|
+
const match = content.match(/^---\n([\s\S]*?)\n---/);
|
|
10
|
+
if (!match) return {};
|
|
11
|
+
const lines = match[1].split('\n');
|
|
12
|
+
const fm = {};
|
|
13
|
+
for (const line of lines) {
|
|
14
|
+
const kv = line.match(/^(\w+):\s*(.+)$/);
|
|
15
|
+
if (kv) fm[kv[1]] = kv[2].trim();
|
|
16
|
+
}
|
|
17
|
+
return fm;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readTaskStatus(taskDir) {
|
|
21
|
+
const fullPath = join(TASKS_DIR, taskDir);
|
|
22
|
+
if (!statSync(fullPath).isDirectory()) return null;
|
|
23
|
+
|
|
24
|
+
const trackingV2 = join(fullPath, 'tracking.md');
|
|
25
|
+
if (existsSync(trackingV2)) {
|
|
26
|
+
const fm = parseFrontmatter(readFileSync(trackingV2, 'utf8'));
|
|
27
|
+
return {
|
|
28
|
+
name: taskDir,
|
|
29
|
+
status: fm.status || 'unknown',
|
|
30
|
+
lastUpdated: fm.last_updated || fm.started || '—',
|
|
31
|
+
blockers: fm.blockers || 'none',
|
|
32
|
+
format: 'v2',
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const progressV1 = join(fullPath, `${taskDir}-progress.md`);
|
|
37
|
+
if (existsSync(progressV1)) {
|
|
38
|
+
const content = readFileSync(progressV1, 'utf8');
|
|
39
|
+
const statusMatch = content.match(/Trạng thái:\s*([^\n]+)/);
|
|
40
|
+
return {
|
|
41
|
+
name: taskDir,
|
|
42
|
+
status: statusMatch ? statusMatch[1].trim() : 'unknown',
|
|
43
|
+
lastUpdated: '—',
|
|
44
|
+
blockers: 'none',
|
|
45
|
+
format: 'v1',
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { name: taskDir, status: 'no-tracking', lastUpdated: '—', blockers: '—', format: 'unknown' };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function generateActiveIndex(rootDir = process.cwd()) {
|
|
53
|
+
const tasksPath = join(rootDir, TASKS_DIR);
|
|
54
|
+
if (!existsSync(tasksPath)) return '';
|
|
55
|
+
|
|
56
|
+
const entries = readdirSync(tasksPath).filter((e) => !EXCLUDE.has(e));
|
|
57
|
+
const tasks = entries.map(readTaskStatus).filter(Boolean);
|
|
58
|
+
|
|
59
|
+
const today = new Date().toISOString().slice(0, 10);
|
|
60
|
+
const lines = [
|
|
61
|
+
'# ACTIVE Tasks',
|
|
62
|
+
'',
|
|
63
|
+
`Auto-generated ${today}. Run \`dw active\` to refresh.`,
|
|
64
|
+
'',
|
|
65
|
+
'Format: `{task-name} · {status} · {last-updated} · {blockers}`',
|
|
66
|
+
'',
|
|
67
|
+
'## Current',
|
|
68
|
+
'',
|
|
69
|
+
...tasks.map(
|
|
70
|
+
(t) => `- \`${t.name}\` · ${t.status} · ${t.lastUpdated} · ${t.blockers}`
|
|
71
|
+
),
|
|
72
|
+
'',
|
|
73
|
+
'## Archive',
|
|
74
|
+
'',
|
|
75
|
+
`Completed tasks: \`.dw/tasks/archive/\``,
|
|
76
|
+
'',
|
|
77
|
+
];
|
|
78
|
+
|
|
79
|
+
return lines.join('\n');
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function writeActiveIndex(rootDir = process.cwd()) {
|
|
83
|
+
const content = generateActiveIndex(rootDir);
|
|
84
|
+
const target = join(rootDir, ACTIVE_FILE);
|
|
85
|
+
writeFileSync(target, content, 'utf8');
|
|
86
|
+
return target;
|
|
87
|
+
}
|