dw-kit 1.0.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.
Files changed (90) hide show
  1. package/.claude/agents/executor.md +80 -0
  2. package/.claude/agents/planner.md +100 -0
  3. package/.claude/agents/quality-checker.md +86 -0
  4. package/.claude/agents/researcher.md +93 -0
  5. package/.claude/agents/reviewer.md +126 -0
  6. package/.claude/hooks/post-write.sh +62 -0
  7. package/.claude/hooks/pre-commit-gate.sh +90 -0
  8. package/.claude/hooks/progress-ping.sh +47 -0
  9. package/.claude/hooks/safety-guard.sh +54 -0
  10. package/.claude/rules/code-style.md +37 -0
  11. package/.claude/rules/commit-standards.md +37 -0
  12. package/.claude/rules/workflow-rules.md +62 -0
  13. package/.claude/settings.json +71 -0
  14. package/.claude/settings.local.json +12 -0
  15. package/.claude/skills/dw-arch-review/SKILL.md +119 -0
  16. package/.claude/skills/dw-archive/SKILL.md +81 -0
  17. package/.claude/skills/dw-commit/SKILL.md +81 -0
  18. package/.claude/skills/dw-config-init/SKILL.md +91 -0
  19. package/.claude/skills/dw-config-validate/SKILL.md +75 -0
  20. package/.claude/skills/dw-dashboard/SKILL.md +209 -0
  21. package/.claude/skills/dw-debug/SKILL.md +97 -0
  22. package/.claude/skills/dw-docs-update/SKILL.md +125 -0
  23. package/.claude/skills/dw-estimate/SKILL.md +90 -0
  24. package/.claude/skills/dw-execute/SKILL.md +98 -0
  25. package/.claude/skills/dw-flow/SKILL.md +274 -0
  26. package/.claude/skills/dw-handoff/SKILL.md +81 -0
  27. package/.claude/skills/dw-log-work/SKILL.md +69 -0
  28. package/.claude/skills/dw-plan/SKILL.md +125 -0
  29. package/.claude/skills/dw-plan/template-plan.md +47 -0
  30. package/.claude/skills/dw-requirements/SKILL.md +98 -0
  31. package/.claude/skills/dw-research/SKILL.md +98 -0
  32. package/.claude/skills/dw-research/template-research.md +51 -0
  33. package/.claude/skills/dw-review/SKILL.md +66 -0
  34. package/.claude/skills/dw-review/checklist.md +88 -0
  35. package/.claude/skills/dw-rollback/SKILL.md +90 -0
  36. package/.claude/skills/dw-sprint-review/SKILL.md +99 -0
  37. package/.claude/skills/dw-task-init/SKILL.md +59 -0
  38. package/.claude/skills/dw-test-plan/SKILL.md +113 -0
  39. package/.claude/skills/dw-thinking/SKILL.md +70 -0
  40. package/.claude/skills/dw-thinking/THINKING.md +91 -0
  41. package/.claude/skills/dw-upgrade/SKILL.md +82 -0
  42. package/.claude/templates/en/task-context.md +73 -0
  43. package/.claude/templates/en/task-plan.md +79 -0
  44. package/.claude/templates/en/task-progress.md +65 -0
  45. package/.claude/templates/pr-template.md +56 -0
  46. package/.claude/templates/task-context.md +73 -0
  47. package/.claude/templates/task-plan.md +79 -0
  48. package/.claude/templates/task-progress.md +65 -0
  49. package/.dw/adapters/claude-cli/extensions/.gitkeep +0 -0
  50. package/.dw/adapters/claude-cli/extensions/README.md +36 -0
  51. package/.dw/adapters/claude-cli/generated/README.md +23 -0
  52. package/.dw/adapters/claude-cli/generated/agents/.gitkeep +0 -0
  53. package/.dw/adapters/claude-cli/generated/skills/.gitkeep +0 -0
  54. package/.dw/adapters/claude-cli/overrides/README.md +35 -0
  55. package/.dw/adapters/claude-cli/overrides/agents/.gitkeep +0 -0
  56. package/.dw/adapters/claude-cli/overrides/skills/.gitkeep +0 -0
  57. package/.dw/adapters/generic/AGENT.md +169 -0
  58. package/.dw/adapters/generic/README.md +21 -0
  59. package/.dw/config/config.schema.json +121 -0
  60. package/.dw/config/dw.config.yml +82 -0
  61. package/.dw/config/presets/enterprise.yml +52 -0
  62. package/.dw/config/presets/small-team.yml +39 -0
  63. package/.dw/config/presets/solo-quick.yml +37 -0
  64. package/.dw/core/QUALITY.md +220 -0
  65. package/.dw/core/ROLES.md +257 -0
  66. package/.dw/core/THINKING.md +126 -0
  67. package/.dw/core/WORKFLOW.md +450 -0
  68. package/.dw/core/templates/vi/task-context.md +92 -0
  69. package/.dw/core/templates/vi/task-plan.md +93 -0
  70. package/.dw/core/templates/vi/task-progress.md +56 -0
  71. package/CLAUDE.md +98 -0
  72. package/LICENSE +21 -0
  73. package/README.md +183 -0
  74. package/bin/dw.mjs +28 -0
  75. package/package.json +52 -0
  76. package/scripts/e2e-local-check.sh +76 -0
  77. package/scripts/migrate-v03-to-v1.sh +243 -0
  78. package/scripts/upgrade.sh +246 -0
  79. package/setup.sh +382 -0
  80. package/src/cli.mjs +68 -0
  81. package/src/commands/doctor.mjs +149 -0
  82. package/src/commands/init.mjs +332 -0
  83. package/src/commands/migrate.mjs +215 -0
  84. package/src/commands/upgrade.mjs +262 -0
  85. package/src/commands/validate.mjs +102 -0
  86. package/src/lib/config.mjs +75 -0
  87. package/src/lib/copy.mjs +110 -0
  88. package/src/lib/platform.mjs +39 -0
  89. package/src/lib/ui.mjs +66 -0
  90. package/src/smoke-test.mjs +315 -0
@@ -0,0 +1,332 @@
1
+ import { existsSync, readFileSync, appendFileSync, writeFileSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { banner, ok, warn, info, log, ask, choose } from '../lib/ui.mjs';
5
+ import { buildConfig, writeConfig } from '../lib/config.mjs';
6
+ import { copyDir, copyFile, ensureDir } from '../lib/copy.mjs';
7
+ import { detectPlatform, platformLabel } from '../lib/platform.mjs';
8
+
9
+ const TOOLKIT_ROOT = resolve(fileURLToPath(import.meta.url), '..', '..', '..');
10
+
11
+ const PRESETS = {
12
+ 'solo-quick': { depth: 'quick', roles: ['dev'], tracking: false },
13
+ 'small-team': { depth: 'standard', roles: ['dev', 'techlead'], tracking: true },
14
+ 'enterprise': { depth: 'thorough', roles: ['dev', 'techlead', 'ba', 'qc', 'pm'], tracking: true },
15
+ };
16
+ const VALID_DEPTHS = ['quick', 'standard', 'thorough'];
17
+ const VALID_LANGUAGES = ['vi', 'en'];
18
+
19
+ export async function initCommand(opts) {
20
+ const projectDir = process.cwd();
21
+
22
+ if (existsSync(join(projectDir, '.dw', 'config', 'dw.config.yml'))) {
23
+ warn('dw-kit is already initialized in this project (.dw/config/dw.config.yml exists).');
24
+ const answer = await ask('Reinitialize? This will overwrite config. (y/N)', 'N');
25
+ if (answer.toLowerCase() !== 'y') {
26
+ log('Aborted.');
27
+ return;
28
+ }
29
+ }
30
+
31
+ let projectName, depth, roles, language;
32
+
33
+ if (opts.preset) {
34
+ const preset = PRESETS[opts.preset];
35
+ if (!preset) {
36
+ warn(`Unknown preset: ${opts.preset}. Available: ${Object.keys(PRESETS).join(', ')}`);
37
+ process.exit(1);
38
+ }
39
+ banner(`Setup Wizard v1.0 — preset: ${opts.preset}`);
40
+ projectName = process.env.DW_NAME || guessProjectName(projectDir);
41
+ depth = preset.depth;
42
+ roles = preset.roles;
43
+ language = process.env.DW_LANG || 'vi';
44
+ log(`Using preset: ${opts.preset}`);
45
+ log(` Project: ${projectName}, Depth: ${depth}, Roles: ${roles.join(', ')}, Lang: ${language}`);
46
+ } else if (opts.silent) {
47
+ banner('Setup Wizard v1.0 — silent mode');
48
+ projectName = process.env.DW_NAME || guessProjectName(projectDir);
49
+ depth = process.env.DW_DEPTH || 'standard';
50
+ const parsedRoles = parseRolesEnv(process.env.DW_ROLES || '');
51
+ language = process.env.DW_LANG || 'vi';
52
+ if (!VALID_DEPTHS.includes(depth)) {
53
+ warn(`Invalid DW_DEPTH="${depth}". Allowed: ${VALID_DEPTHS.join(', ')}`);
54
+ process.exit(1);
55
+ }
56
+ if (!VALID_LANGUAGES.includes(language)) {
57
+ warn(`Invalid DW_LANG="${language}". Allowed: ${VALID_LANGUAGES.join(', ')}`);
58
+ process.exit(1);
59
+ }
60
+ roles = normalizeRolesForDepth(parsedRoles, depth);
61
+ log(`Silent mode — reading from environment variables`);
62
+ log(` Project: ${projectName}, Depth: ${depth}, Roles: ${roles.join(', ')}, Lang: ${language}`);
63
+ } else {
64
+ banner('Setup Wizard v1.0');
65
+ projectName = await askProjectName(projectDir);
66
+ depth = await askDepth();
67
+ roles = defaultRolesForDepth(depth);
68
+ language = await askLanguage();
69
+ log(` Roles auto-selected by depth: ${roles.join(', ')}`);
70
+ }
71
+
72
+ console.log();
73
+ info('Detecting platform...');
74
+ const adapter = opts.adapter || detectPlatform(projectDir);
75
+ ok(`Platform: ${platformLabel(adapter)}`);
76
+
77
+ info('Setting up project...');
78
+ await setupProject(projectDir, { projectName, depth, roles, language, adapter });
79
+
80
+ printSummary({ projectName, depth, roles, language, adapter });
81
+ }
82
+
83
+ async function askProjectName(projectDir) {
84
+ const guess = guessProjectName(projectDir);
85
+ return ask('[Project] Project name?', guess);
86
+ }
87
+
88
+ async function askDepth() {
89
+ return choose('[Depth] Default workflow depth:', [
90
+ { value: 'quick', label: 'Solo dev, hotfix, familiar code — minimal ceremony' },
91
+ { value: 'standard', label: 'Team, feature — full 6-phase workflow' },
92
+ { value: 'thorough', label: 'Enterprise — full workflow + arch-review + test-plan' },
93
+ ], 'standard');
94
+ }
95
+
96
+ async function askLanguage() {
97
+ return choose('[Lang] Documentation language:', [
98
+ { value: 'vi', label: 'Tiếng Việt' },
99
+ { value: 'en', label: 'English' },
100
+ ], 'vi');
101
+ }
102
+
103
+ function guessProjectName(dir) {
104
+ const base = dir.split(/[/\\]/).pop();
105
+ return base || 'my-project';
106
+ }
107
+
108
+ function parseRolesEnv(rolesStr) {
109
+ if (!rolesStr) return [];
110
+ const valid = ['dev', 'techlead', 'ba', 'qc', 'pm'];
111
+ const parsed = rolesStr.split(',').map(r => r.trim().toLowerCase()).filter(r => valid.includes(r));
112
+ return [...new Set(parsed)];
113
+ }
114
+
115
+ function defaultRolesForDepth(depth) {
116
+ if (depth === 'quick') return ['dev'];
117
+ if (depth === 'thorough') return ['dev', 'techlead', 'ba', 'qc', 'pm'];
118
+ return ['dev', 'techlead'];
119
+ }
120
+
121
+ function normalizeRolesForDepth(parsedRoles, depth) {
122
+ const required = defaultRolesForDepth(depth);
123
+ const merged = [...new Set([...parsedRoles, ...required])];
124
+ const missing = required.filter((r) => !parsedRoles.includes(r));
125
+ if (parsedRoles.length > 0 && missing.length > 0) {
126
+ warn(`DW_ROLES missing required roles for depth "${depth}". Auto-added: ${missing.join(', ')}`);
127
+ }
128
+ return merged;
129
+ }
130
+
131
+ async function setupProject(projectDir, { projectName, depth, roles, language, adapter }) {
132
+ copyCoreDocs(projectDir);
133
+ copyConfig(projectDir, { projectName, depth, roles, language });
134
+ copyAdapterStructure(projectDir);
135
+
136
+ if (adapter === 'claude-cli') {
137
+ copyClaudeFiles(projectDir);
138
+ copyCLAUDEmd(projectDir);
139
+ } else if (adapter === 'cursor') {
140
+ copyCursorFiles(projectDir);
141
+ copyGenericAdapter(projectDir);
142
+ } else if (adapter === 'generic') {
143
+ copyGenericAdapter(projectDir);
144
+ }
145
+
146
+ createRuntimeDirs(projectDir);
147
+ updateGitignore(projectDir);
148
+ }
149
+
150
+ function copyCoreDocs(projectDir) {
151
+ const src = join(TOOLKIT_ROOT, '.dw', 'core');
152
+ const dst = join(projectDir, '.dw', 'core');
153
+ copyDir(src, dst, { overwrite: true });
154
+ ok('.dw/core/ (WORKFLOW.md, THINKING.md, QUALITY.md, ROLES.md, templates/)');
155
+ }
156
+
157
+ function copyConfig(projectDir, { projectName, depth, roles, language }) {
158
+ const configDir = join(projectDir, '.dw', 'config');
159
+ ensureDir(configDir);
160
+
161
+ const config = buildConfig({ projectName, language, depth, roles });
162
+ writeConfig(join(configDir, 'dw.config.yml'), config);
163
+
164
+ copyFile(
165
+ join(TOOLKIT_ROOT, '.dw', 'config', 'config.schema.json'),
166
+ join(configDir, 'config.schema.json'),
167
+ );
168
+
169
+ const presetsDir = join(configDir, 'presets');
170
+ ensureDir(presetsDir);
171
+ copyDir(join(TOOLKIT_ROOT, '.dw', 'config', 'presets'), presetsDir, { overwrite: true });
172
+
173
+ ok('.dw/config/dw.config.yml + schema + presets');
174
+ }
175
+
176
+ function copyAdapterStructure(projectDir) {
177
+ const adaptersDir = join(projectDir, '.dw', 'adapters');
178
+ copyDir(join(TOOLKIT_ROOT, '.dw', 'adapters'), adaptersDir);
179
+ ok('.dw/adapters/ (claude-cli, generic)');
180
+ }
181
+
182
+ function copyClaudeFiles(projectDir) {
183
+ const src = join(TOOLKIT_ROOT, '.claude');
184
+ const dst = join(projectDir, '.claude');
185
+ const results = copyDir(src, dst, { overwrite: true });
186
+
187
+ const skipCount = results.filter(r => r.action === 'skip').length;
188
+ const copyCount = results.filter(r => r.action === 'copy').length;
189
+
190
+ ok(`.claude/ (${copyCount} files — skills, agents, hooks, rules, templates)`);
191
+ if (skipCount > 0) log(` ${skipCount} existing files preserved`);
192
+ }
193
+
194
+ function copyCLAUDEmd(projectDir) {
195
+ const src = join(TOOLKIT_ROOT, 'CLAUDE.md');
196
+ const dst = join(projectDir, 'CLAUDE.md');
197
+
198
+ if (existsSync(dst)) {
199
+ warn('CLAUDE.md already exists — skipping (review manually if needed)');
200
+ return;
201
+ }
202
+
203
+ copyFile(src, dst);
204
+
205
+ const techStackSection = `
206
+ ---
207
+
208
+ ## Tech Stack
209
+
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]
214
+
215
+ ## Project-Specific Rules
216
+
217
+ <!-- Add project-specific rules -->
218
+ - [Rule 1]
219
+ `;
220
+ appendFileSync(dst, techStackSection, 'utf-8');
221
+ ok('CLAUDE.md (with Tech Stack template section)');
222
+ }
223
+
224
+ function copyCursorFiles(projectDir) {
225
+ const rulesDir = join(projectDir, '.cursor', 'rules');
226
+ ensureDir(rulesDir);
227
+
228
+ const ruleContent = `# dw-kit Workflow Rules
229
+ # Auto-generated by dw init --adapter cursor
230
+ # Reference: .dw/core/WORKFLOW.md for full methodology
231
+
232
+ ## Workflow
233
+ Follow the 6-phase workflow: Initialize → Understand → Plan → Execute → Verify → Close.
234
+ Read @.dw/core/WORKFLOW.md for detailed phase instructions.
235
+
236
+ ## Quality
237
+ - Apply TDD: write test → implement → refactor
238
+ - Read @.dw/core/QUALITY.md for the 4-layer quality strategy
239
+ - Run test/lint commands from .dw/config/dw.config.yml before committing
240
+
241
+ ## Thinking
242
+ When planning or debugging, apply the thinking framework from @.dw/core/THINKING.md:
243
+ - Critical Thinking: question assumptions
244
+ - Systems Thinking: trace dependencies and side effects
245
+ - First Principles: break down to fundamentals
246
+
247
+ ## Roles
248
+ Check @.dw/core/ROLES.md for role definitions and decision authority.
249
+
250
+ ## Task Tracking
251
+ - Task docs go in .dw/tasks/[task-name]/
252
+ - Use templates from .dw/core/templates/ for context, plan, and progress docs
253
+ `;
254
+
255
+ writeFileSync(join(rulesDir, 'dw-workflow.mdc'), ruleContent, 'utf-8');
256
+ ok('.cursor/rules/dw-workflow.mdc (Cursor rules from core methodology)');
257
+ }
258
+
259
+ function copyGenericAdapter(projectDir) {
260
+ const src = join(TOOLKIT_ROOT, '.dw', 'adapters', 'generic', 'AGENT.md');
261
+ const dst = join(projectDir, 'AGENT.md');
262
+ if (!existsSync(dst)) {
263
+ copyFile(src, dst);
264
+ ok('AGENT.md (generic adapter for Cursor/Windsurf/Copilot)');
265
+ } else {
266
+ warn('AGENT.md already exists — skipping');
267
+ }
268
+ }
269
+
270
+ function createRuntimeDirs(projectDir) {
271
+ for (const dir of ['.dw/tasks', '.dw/docs', '.dw/metrics', '.dw/reports']) {
272
+ ensureDir(join(projectDir, dir));
273
+ }
274
+ ok('.dw/ (tasks, docs, metrics, reports)');
275
+ }
276
+
277
+ function updateGitignore(projectDir) {
278
+ const gitignorePath = join(projectDir, '.gitignore');
279
+ const entriesToAdd = ['.dw/metrics/', '.dw/reports/', 'CLAUDE.local.md'];
280
+
281
+ if (existsSync(gitignorePath)) {
282
+ const content = readFileSync(gitignorePath, 'utf-8');
283
+ const missing = entriesToAdd.filter(e => !content.includes(e));
284
+ if (missing.length > 0) {
285
+ appendFileSync(gitignorePath, `\n# dw-kit\n${missing.join('\n')}\n`, 'utf-8');
286
+ ok('.gitignore updated');
287
+ }
288
+ } else {
289
+ writeFileSync(gitignorePath, `# dw-kit\n${entriesToAdd.join('\n')}\n`, 'utf-8');
290
+ ok('.gitignore created');
291
+ }
292
+ }
293
+
294
+ function printSummary({ projectName, depth, roles, language, adapter }) {
295
+ console.log();
296
+ console.log(` ✅ Setup complete!`);
297
+ console.log();
298
+ console.log(` Project : ${projectName}`);
299
+ console.log(` Depth : ${depth}`);
300
+ console.log(` Roles : ${roles.join(', ')}`);
301
+ console.log(` Language : ${language}`);
302
+ console.log(` Platform : ${platformLabel(adapter)}`);
303
+ console.log();
304
+ console.log(` Files created:`);
305
+ console.log(` .dw/ — core/, config/, adapters/, tasks, docs`);
306
+ if (adapter === 'claude-cli') {
307
+ console.log(` .claude/ — skills, agents, hooks, rules`);
308
+ console.log(` CLAUDE.md`);
309
+ } else if (adapter === 'cursor') {
310
+ console.log(` .cursor/rules/ — workflow rules for Cursor`);
311
+ console.log(` AGENT.md — methodology reference`);
312
+ } else {
313
+ console.log(` AGENT.md — methodology reference`);
314
+ }
315
+ console.log();
316
+ if (adapter === 'claude-cli') {
317
+ console.log(` Next steps:`);
318
+ console.log(` Run: claude (to open Claude Code in this directory in terminal)`);
319
+ console.log(` Run: /dw-flow [task-name]`);
320
+ console.log(` Suggested: Update Tech Stack in CLAUDE.md (optional, recommended)`);
321
+ } else if (adapter === 'cursor') {
322
+ console.log(` Next steps:`);
323
+ console.log(` 1. Open Cursor in this directory`);
324
+ console.log(` 2. Rules auto-loaded from .cursor/rules/`);
325
+ console.log(` 3. Reference AGENT.md + .dw/core/WORKFLOW.md for guidance`);
326
+ } else {
327
+ console.log(` Next steps:`);
328
+ console.log(` 1. Open your AI coding tool in this directory`);
329
+ console.log(` 2. Reference AGENT.md for workflow guidance`);
330
+ }
331
+ console.log();
332
+ }
@@ -0,0 +1,215 @@
1
+ import { existsSync, readFileSync, readdirSync, mkdirSync, copyFileSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { execSync } from 'node:child_process';
4
+ import { header, ok, warn, err, info, log, dry, ask } from '../lib/ui.mjs';
5
+ import { loadConfig, buildConfig, writeConfig } from '../lib/config.mjs';
6
+
7
+ const DEPTH_MAP = { '1': 'quick', '2': 'standard', '3': 'thorough' };
8
+ const VALID_ROLES = ['dev', 'techlead', 'ba', 'qc', 'pm'];
9
+
10
+ export async function migrateCommand(opts) {
11
+ const projectDir = process.cwd();
12
+ const dryRun = opts.dryRun || false;
13
+
14
+ header('dw-kit v0.3 → v1 Migration');
15
+ if (dryRun) log('Mode: DRY RUN');
16
+
17
+ const oldConfigPath = findOldConfig(projectDir);
18
+ if (!oldConfigPath) {
19
+ ok('No v0.3 config found (dv-workflow.config.yml). Already on v1 or fresh install.');
20
+ log('If migrating manually, run `dw init` instead.');
21
+ return;
22
+ }
23
+
24
+ info(`Step 1: Parse old config (${oldConfigPath})`);
25
+ const oldConfig = loadConfig(oldConfigPath);
26
+ if (!oldConfig) {
27
+ err('Failed to parse old config. Manual migration needed.');
28
+ process.exit(1);
29
+ }
30
+ ok('Old config parsed');
31
+
32
+ const extracted = extractOldValues(oldConfig);
33
+ log(` Project: ${extracted.name}`);
34
+ log(` Level: ${extracted.level} → depth: ${extracted.depth}`);
35
+ log(` Roles: ${extracted.roles.join(', ')}`);
36
+ log(` Language: ${extracted.language}`);
37
+
38
+ info('Step 2: Detect customized skills');
39
+ const customized = detectCustomizedSkills(projectDir);
40
+ if (customized.length > 0) {
41
+ for (const skill of customized) {
42
+ if (dryRun) {
43
+ dry(`preserve ${skill} → .dw/adapters/claude-cli/overrides/skills/${skill}/`);
44
+ } else {
45
+ preserveSkillOverride(projectDir, skill);
46
+ ok(`Preserved: ${skill} → overrides/`);
47
+ }
48
+ }
49
+ } else {
50
+ ok('No customized skills detected');
51
+ }
52
+
53
+ info('Step 3: Create v1 config');
54
+ const newConfigDir = join(projectDir, '.dw', 'config');
55
+ const newConfigPath = join(newConfigDir, 'dw.config.yml');
56
+
57
+ if (existsSync(newConfigPath)) {
58
+ warn('.dw/config/dw.config.yml already exists — skipping config generation');
59
+ warn('Review and update manually if needed.');
60
+ } else {
61
+ const newConfig = buildConfig({
62
+ projectName: extracted.name,
63
+ language: extracted.language,
64
+ depth: extracted.depth,
65
+ roles: extracted.roles,
66
+ });
67
+
68
+ if (extracted.testCommand) newConfig.quality.test_command = extracted.testCommand;
69
+ if (extracted.lintCommand) newConfig.quality.lint_command = extracted.lintCommand;
70
+ newConfig._toolkit.migrated_from = 'v0.3';
71
+
72
+ if (dryRun) {
73
+ dry('Would create .dw/config/dw.config.yml');
74
+ } else {
75
+ mkdirSync(newConfigDir, { recursive: true });
76
+ writeConfig(newConfigPath, newConfig);
77
+ ok(`.dw/config/dw.config.yml created (depth: ${extracted.depth})`);
78
+ }
79
+ }
80
+
81
+ info('Step 4: Backup old config');
82
+ if (dryRun) {
83
+ dry(`Would backup ${oldConfigPath} → ${oldConfigPath}.bak`);
84
+ } else {
85
+ const bakPath = `${oldConfigPath}.bak`;
86
+ if (!existsSync(bakPath)) {
87
+ copyFileSync(oldConfigPath, bakPath);
88
+ ok(`Backed up: ${oldConfigPath} → ${bakPath}`);
89
+ } else {
90
+ warn('Backup already exists — skipping');
91
+ }
92
+ }
93
+
94
+ info('Step 5: Check CI/CD references');
95
+ const ciFiles = checkCIReferences(projectDir);
96
+ if (ciFiles.length > 0) {
97
+ for (const file of ciFiles) {
98
+ warn(`Found reference in: ${file} — update to .dw/config/dw.config.yml`);
99
+ }
100
+ } else {
101
+ ok('No CI/CD references found');
102
+ }
103
+
104
+ console.log();
105
+ header(dryRun ? 'DRY RUN complete — no changes made' : 'Migration complete!');
106
+
107
+ if (!dryRun) {
108
+ console.log();
109
+ log('Next steps:');
110
+ log(' 1. Review .dw/config/dw.config.yml');
111
+ log(' 2. Run: dw init --adapter claude-cli (to regenerate .claude/)');
112
+ log(' 3. Check .dw/adapters/claude-cli/overrides/ for preserved customizations');
113
+ if (ciFiles.length > 0) {
114
+ log(' 4. Update CI/CD files (see warnings above)');
115
+ }
116
+ } else {
117
+ log('Run without --dry-run to apply migration.');
118
+ }
119
+ console.log();
120
+ }
121
+
122
+ function findOldConfig(projectDir) {
123
+ const candidates = [
124
+ join(projectDir, 'dv-workflow.config.yml'),
125
+ join(projectDir, 'dw.config.yml'),
126
+ ];
127
+ return candidates.find(p => existsSync(p)) || null;
128
+ }
129
+
130
+ function extractOldValues(config) {
131
+ const name = config.project?.name || config.name || 'my-project';
132
+ const language = config.project?.language || config.language || 'vi';
133
+ const level = String(config.level || config.workflow?.level || '2');
134
+ const depth = DEPTH_MAP[level] || 'standard';
135
+
136
+ let roles = config.team?.roles || [];
137
+ if (!Array.isArray(roles)) roles = ['dev'];
138
+ roles = roles.filter(r => VALID_ROLES.includes(r));
139
+ if (!roles.includes('dev')) roles.unshift('dev');
140
+
141
+ const testCommand = config.quality?.test_command || config.test_command || '';
142
+ const lintCommand = config.quality?.lint_command || config.lint_command || '';
143
+
144
+ return { name, language, level, depth, roles, testCommand, lintCommand };
145
+ }
146
+
147
+ function detectCustomizedSkills(projectDir) {
148
+ const skillsDir = join(projectDir, '.claude', 'skills');
149
+ if (!existsSync(skillsDir)) return [];
150
+
151
+ const customized = [];
152
+ try {
153
+ const isGitRepo = existsSync(join(projectDir, '.git'));
154
+ if (!isGitRepo) return [];
155
+
156
+ const skills = readdirSync(skillsDir, { withFileTypes: true })
157
+ .filter(d => d.isDirectory())
158
+ .map(d => d.name);
159
+
160
+ for (const skill of skills) {
161
+ const skillFile = join('.claude', 'skills', skill, 'SKILL.md');
162
+ try {
163
+ execSync(`git diff --quiet HEAD -- "${skillFile}"`, {
164
+ cwd: projectDir,
165
+ stdio: 'pipe',
166
+ });
167
+ } catch {
168
+ customized.push(skill);
169
+ }
170
+ }
171
+ } catch { /* not a git repo or other issue */ }
172
+
173
+ return customized;
174
+ }
175
+
176
+ function preserveSkillOverride(projectDir, skillName) {
177
+ const srcFile = join(projectDir, '.claude', 'skills', skillName, 'SKILL.md');
178
+ const dstDir = join(projectDir, '.dw', 'adapters', 'claude-cli', 'overrides', 'skills', skillName);
179
+ const dstFile = join(dstDir, 'SKILL.md');
180
+
181
+ if (!existsSync(srcFile)) return;
182
+ mkdirSync(dstDir, { recursive: true });
183
+ copyFileSync(srcFile, dstFile);
184
+ }
185
+
186
+ function checkCIReferences(projectDir) {
187
+ const found = [];
188
+
189
+ const directChecks = ['.gitlab-ci.yml', 'Makefile', '.circleci/config.yml'];
190
+ for (const file of directChecks) {
191
+ const fullPath = join(projectDir, file);
192
+ if (existsSync(fullPath)) {
193
+ const content = readFileSync(fullPath, 'utf-8');
194
+ if (/dv-workflow\.config\.yml|dv-workflow-kit/.test(content)) {
195
+ found.push(file);
196
+ }
197
+ }
198
+ }
199
+
200
+ const workflowsDir = join(projectDir, '.github', 'workflows');
201
+ if (existsSync(workflowsDir)) {
202
+ try {
203
+ const files = readdirSync(workflowsDir).filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
204
+ for (const file of files) {
205
+ const fullPath = join(workflowsDir, file);
206
+ const content = readFileSync(fullPath, 'utf-8');
207
+ if (/dv-workflow\.config\.yml|dv-workflow-kit/.test(content)) {
208
+ found.push(join('.github', 'workflows', file));
209
+ }
210
+ }
211
+ } catch { /* dir read failed */ }
212
+ }
213
+
214
+ return found;
215
+ }