dw-kit 1.0.0 → 1.0.2
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 +1 -3
- package/.claude/rules/code-style.md +37 -37
- package/.claude/rules/commit-standards.md +37 -37
- package/.claude/settings.json +1 -1
- package/.claude/settings.local.json +2 -1
- package/.claude/skills/dw-prompt/SKILL.md +62 -0
- package/.claude/skills/dw-upgrade/SKILL.md +20 -30
- package/.dw/adapters/claude-cli/extensions/README.md +2 -2
- package/.dw/adapters/claude-cli/generated/README.md +3 -3
- package/.dw/adapters/claude-cli/overrides/README.md +4 -2
- package/CLAUDE.md +1 -0
- package/README.md +78 -122
- package/package.json +5 -5
- package/scripts/e2e-local-check.sh +1 -2
- package/src/__fixtures__/claude-cli-bug-snippet.js +15 -0
- package/src/cli.mjs +29 -5
- package/src/commands/claude-vn-fix.mjs +267 -0
- package/src/commands/prompt.mjs +125 -0
- package/src/lib/clipboard.mjs +24 -0
- package/src/lib/prompt-suggest.mjs +84 -0
- package/src/lib/update-checker.mjs +73 -0
- package/src/smoke-test.mjs +47 -11
- package/scripts/migrate-v03-to-v1.sh +0 -243
- package/scripts/upgrade.sh +0 -246
- package/setup.sh +0 -382
- package/src/commands/migrate.mjs +0 -215
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { load as yamlLoad } from 'js-yaml';
|
|
4
|
+
import { header, info, ok, warn, err, log } from '../lib/ui.mjs';
|
|
5
|
+
import { copyToClipboard } from '../lib/clipboard.mjs';
|
|
6
|
+
import { getSuggestions, isVague, expandTemplate } from '../lib/prompt-suggest.mjs';
|
|
7
|
+
|
|
8
|
+
export async function promptCommand(opts) {
|
|
9
|
+
header('dw-kit Prompt Builder');
|
|
10
|
+
|
|
11
|
+
const adapter = readAdapter();
|
|
12
|
+
|
|
13
|
+
// Non-interactive mode: --text <text>
|
|
14
|
+
if (opts.text !== undefined) {
|
|
15
|
+
if (!opts.text.trim()) {
|
|
16
|
+
err('--text cannot be empty.');
|
|
17
|
+
process.exit(1);
|
|
18
|
+
}
|
|
19
|
+
const result = await buildPrompt(opts.text, { interactive: false });
|
|
20
|
+
outputResult(result, adapter);
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Interactive mode
|
|
25
|
+
const result = await buildPrompt('', { interactive: true });
|
|
26
|
+
outputResult(result, adapter);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function buildPrompt(initialText, { interactive }) {
|
|
30
|
+
let description = initialText;
|
|
31
|
+
|
|
32
|
+
if (interactive) {
|
|
33
|
+
description = await runAutocompleteStep();
|
|
34
|
+
if (!description.trim()) {
|
|
35
|
+
err('No description provided.');
|
|
36
|
+
process.exit(1);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
let area = '';
|
|
41
|
+
let outcome = '';
|
|
42
|
+
|
|
43
|
+
if (isVague(description)) {
|
|
44
|
+
if (interactive) {
|
|
45
|
+
info('Description seems short — a couple of quick questions (Enter to skip):');
|
|
46
|
+
({ area, outcome } = await runWizardStep());
|
|
47
|
+
}
|
|
48
|
+
// In non-interactive mode: expand with just the description (no wizard data)
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return expandTemplate(description, { area, outcome });
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async function runAutocompleteStep() {
|
|
55
|
+
const { AutoComplete } = await import('enquirer');
|
|
56
|
+
const suggestions = getSuggestions(process.cwd());
|
|
57
|
+
|
|
58
|
+
const prompt = new AutoComplete({
|
|
59
|
+
name: 'description',
|
|
60
|
+
message: 'Describe your task:',
|
|
61
|
+
limit: 7,
|
|
62
|
+
choices: suggestions.length ? suggestions : ['(no suggestions — type your task)'],
|
|
63
|
+
suggest(typed, choices) {
|
|
64
|
+
const lower = typed.toLowerCase();
|
|
65
|
+
const filtered = choices.filter((c) => c.message.toLowerCase().includes(lower));
|
|
66
|
+
// Always offer the raw typed text as first selectable option
|
|
67
|
+
if (typed && !filtered.find((c) => c.message === typed)) {
|
|
68
|
+
return [{ message: typed, value: typed }, ...filtered];
|
|
69
|
+
}
|
|
70
|
+
return filtered.length ? filtered : [{ message: typed || '(empty)', value: typed }];
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
return prompt.run();
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function runWizardStep() {
|
|
78
|
+
const { Input } = await import('enquirer');
|
|
79
|
+
|
|
80
|
+
const area = await new Input({
|
|
81
|
+
name: 'area',
|
|
82
|
+
message: 'Which area/files? (e.g. auth middleware, src/login/)',
|
|
83
|
+
initial: '',
|
|
84
|
+
}).run().catch(() => '');
|
|
85
|
+
|
|
86
|
+
const outcome = await new Input({
|
|
87
|
+
name: 'outcome',
|
|
88
|
+
message: 'Expected outcome? (e.g. user redirected to /dashboard)',
|
|
89
|
+
initial: '',
|
|
90
|
+
}).run().catch(() => '');
|
|
91
|
+
|
|
92
|
+
return { area, outcome };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function outputResult(text, adapter) {
|
|
96
|
+
log('');
|
|
97
|
+
log('─── Result ───────────────────────────────────────────');
|
|
98
|
+
log(text);
|
|
99
|
+
log('──────────────────────────────────────────────────────');
|
|
100
|
+
log('');
|
|
101
|
+
|
|
102
|
+
const shouldCopy = adapter === 'claude-cli' || adapter === 'cursor';
|
|
103
|
+
if (shouldCopy) {
|
|
104
|
+
const copied = copyToClipboard(text);
|
|
105
|
+
if (copied) {
|
|
106
|
+
ok('Copied to clipboard. Paste into Claude CLI or your IDE.');
|
|
107
|
+
} else {
|
|
108
|
+
warn('Clipboard copy failed. Copy the text above manually.');
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
// generic adapter: just output to stdout
|
|
112
|
+
info('(generic adapter — copy the text above manually)');
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function readAdapter() {
|
|
117
|
+
const configPath = join(process.cwd(), '.dw', 'config', 'dw.config.yml');
|
|
118
|
+
if (!existsSync(configPath)) return 'claude-cli';
|
|
119
|
+
try {
|
|
120
|
+
const config = yamlLoad(readFileSync(configPath, 'utf-8'));
|
|
121
|
+
return config?.adapter || 'claude-cli';
|
|
122
|
+
} catch {
|
|
123
|
+
return 'claude-cli';
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { spawnSync } from 'node:child_process';
|
|
2
|
+
import { platform } from 'node:process';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Copy text to system clipboard.
|
|
6
|
+
* @returns {boolean} true if succeeded
|
|
7
|
+
*/
|
|
8
|
+
export function copyToClipboard(text) {
|
|
9
|
+
const candidates = platform === 'win32'
|
|
10
|
+
? [['clip']]
|
|
11
|
+
: platform === 'darwin'
|
|
12
|
+
? [['pbcopy']]
|
|
13
|
+
: [['wl-copy'], ['xclip', '-selection', 'clipboard'], ['xsel', '--clipboard', '--input']];
|
|
14
|
+
|
|
15
|
+
for (const [cmd, ...args] of candidates) {
|
|
16
|
+
try {
|
|
17
|
+
const result = spawnSync(cmd, args, { input: text, encoding: 'utf-8' });
|
|
18
|
+
if (result.status === 0) return true;
|
|
19
|
+
} catch {
|
|
20
|
+
// Try next candidate.
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { execSync } from 'node:child_process';
|
|
2
|
+
|
|
3
|
+
const TEMPLATE_SUGGESTIONS = [
|
|
4
|
+
'fix: ',
|
|
5
|
+
'fix authentication redirect after login',
|
|
6
|
+
'fix null pointer / undefined error in ',
|
|
7
|
+
'fix performance issue in ',
|
|
8
|
+
'feat: add ',
|
|
9
|
+
'feat: implement ',
|
|
10
|
+
'feat: support ',
|
|
11
|
+
'refactor: simplify ',
|
|
12
|
+
'refactor: extract ',
|
|
13
|
+
'refactor: rename ',
|
|
14
|
+
'perf: optimize ',
|
|
15
|
+
'perf: reduce load time of ',
|
|
16
|
+
'chore: update dependencies',
|
|
17
|
+
'docs: update README',
|
|
18
|
+
'test: add tests for ',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// Common verbs to detect whether a description has intent
|
|
22
|
+
const INTENT_VERBS = [
|
|
23
|
+
'fix', 'add', 'update', 'remove', 'delete', 'create', 'implement', 'refactor',
|
|
24
|
+
'rename', 'move', 'improve', 'optimize', 'support', 'extract', 'migrate',
|
|
25
|
+
'replace', 'upgrade', 'enable', 'disable', 'integrate', 'patch',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
function getTemplateSuggestions() {
|
|
29
|
+
return [...TEMPLATE_SUGGESTIONS];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function getGitSuggestions(cwd) {
|
|
33
|
+
try {
|
|
34
|
+
const out = execSync('git log --oneline -50 --no-merges', {
|
|
35
|
+
cwd,
|
|
36
|
+
encoding: 'utf-8',
|
|
37
|
+
timeout: 3000,
|
|
38
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
39
|
+
});
|
|
40
|
+
return out
|
|
41
|
+
.trim()
|
|
42
|
+
.split('\n')
|
|
43
|
+
.filter(Boolean)
|
|
44
|
+
.map((line) => line.replace(/^[a-f0-9]+ /, '').trim()) // strip hash
|
|
45
|
+
.filter((msg) => msg.length > 5)
|
|
46
|
+
.slice(0, 30);
|
|
47
|
+
} catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function getSuggestions(cwd = process.cwd()) {
|
|
53
|
+
const git = getGitSuggestions(cwd);
|
|
54
|
+
const templates = getTemplateSuggestions();
|
|
55
|
+
// git log first (most relevant to this repo), then templates
|
|
56
|
+
const merged = [...git, ...templates];
|
|
57
|
+
// dedupe
|
|
58
|
+
return [...new Set(merged)].slice(0, 20);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Returns true if the description is likely too vague to give Claude good context.
|
|
63
|
+
*/
|
|
64
|
+
export function isVague(text) {
|
|
65
|
+
const trimmed = (text || '').trim();
|
|
66
|
+
if (trimmed.length < 50) return true;
|
|
67
|
+
const lower = trimmed.toLowerCase();
|
|
68
|
+
const hasVerb = INTENT_VERBS.some((v) => lower.startsWith(v) || lower.includes(` ${v} `));
|
|
69
|
+
return !hasVerb;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Expand a short description into a structured prompt.
|
|
74
|
+
* @param {string} text - core task description
|
|
75
|
+
* @param {{ area?: string, outcome?: string }} extras
|
|
76
|
+
* @returns {string}
|
|
77
|
+
*/
|
|
78
|
+
export function expandTemplate(text, { area = '', outcome = '' } = {}) {
|
|
79
|
+
const base = text.trim();
|
|
80
|
+
const parts = [base];
|
|
81
|
+
if (area) parts.push(`Scope: ${area.trim()}.`);
|
|
82
|
+
if (outcome) parts.push(`Expected: ${outcome.trim()}.`);
|
|
83
|
+
return parts.join('\n');
|
|
84
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
|
|
5
|
+
const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
6
|
+
const REGISTRY_URL = 'https://registry.npmjs.org/dw-kit/latest';
|
|
7
|
+
const CACHE_DIR = join(homedir(), '.dw-kit');
|
|
8
|
+
const CACHE_FILE = join(CACHE_DIR, 'update-cache.json');
|
|
9
|
+
|
|
10
|
+
function parseSemver(v) {
|
|
11
|
+
const parts = String(v).replace(/^v/, '').split('.').map(Number);
|
|
12
|
+
return { major: parts[0] || 0, minor: parts[1] || 0, patch: parts[2] || 0 };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function isNewer(latest, current) {
|
|
16
|
+
const l = parseSemver(latest);
|
|
17
|
+
const c = parseSemver(current);
|
|
18
|
+
if (l.major !== c.major) return l.major > c.major;
|
|
19
|
+
if (l.minor !== c.minor) return l.minor > c.minor;
|
|
20
|
+
return l.patch > c.patch;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function readCache() {
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(readFileSync(CACHE_FILE, 'utf-8'));
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function writeCache(data) {
|
|
32
|
+
try {
|
|
33
|
+
mkdirSync(CACHE_DIR, { recursive: true });
|
|
34
|
+
writeFileSync(CACHE_FILE, JSON.stringify(data), 'utf-8');
|
|
35
|
+
} catch {
|
|
36
|
+
// ignore write errors (permission, disk full, etc.)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function fetchLatestVersion() {
|
|
41
|
+
const res = await fetch(REGISTRY_URL, { signal: AbortSignal.timeout(3000) });
|
|
42
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
43
|
+
const data = await res.json();
|
|
44
|
+
return data.version;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Returns latest version string if an update is available (from cache), null otherwise.
|
|
49
|
+
* Never throws, never makes network calls.
|
|
50
|
+
*/
|
|
51
|
+
export function getUpdateNotice(currentVersion) {
|
|
52
|
+
if (process.env.DW_NO_UPDATE_CHECK) return null;
|
|
53
|
+
const cache = readCache();
|
|
54
|
+
if (!cache?.latest) return null;
|
|
55
|
+
return isNewer(cache.latest, currentVersion) ? cache.latest : null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Fires off an async check against npm registry and updates the cache.
|
|
60
|
+
* Non-blocking — caller should NOT await this.
|
|
61
|
+
* Skips the check if cache is still fresh (< 24h).
|
|
62
|
+
*/
|
|
63
|
+
export function scheduleUpdateCheck(currentVersion) {
|
|
64
|
+
if (process.env.DW_NO_UPDATE_CHECK) return;
|
|
65
|
+
|
|
66
|
+
const cache = readCache();
|
|
67
|
+
const now = Date.now();
|
|
68
|
+
if (cache?.checkedAt && now - cache.checkedAt < CACHE_TTL_MS) return;
|
|
69
|
+
|
|
70
|
+
fetchLatestVersion()
|
|
71
|
+
.then((latest) => writeCache({ latest, checkedAt: now, current: currentVersion }))
|
|
72
|
+
.catch(() => {});
|
|
73
|
+
}
|
package/src/smoke-test.mjs
CHANGED
|
@@ -11,6 +11,7 @@ import { mkdirSync, rmSync, existsSync, readFileSync } from 'node:fs';
|
|
|
11
11
|
import { join, resolve } from 'node:path';
|
|
12
12
|
import { execSync } from 'node:child_process';
|
|
13
13
|
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import { copyFileSync } from 'node:fs';
|
|
14
15
|
|
|
15
16
|
const __dirname = resolve(fileURLToPath(import.meta.url), '..');
|
|
16
17
|
const DW_BIN = resolve(__dirname, '..', 'bin', 'dw.mjs');
|
|
@@ -72,7 +73,7 @@ test('--version returns semver', () => {
|
|
|
72
73
|
|
|
73
74
|
test('--help lists all commands', () => {
|
|
74
75
|
const out = dw('--help', TEMP_BASE);
|
|
75
|
-
for (const cmd of ['init', 'upgrade', 'validate', 'doctor', '
|
|
76
|
+
for (const cmd of ['init', 'upgrade', 'validate', 'doctor', 'prompt', 'claude-vn-fix']) {
|
|
76
77
|
assert(out.includes(cmd), `Missing command: ${cmd}`);
|
|
77
78
|
}
|
|
78
79
|
});
|
|
@@ -262,6 +263,51 @@ test('doctor reports issues on empty project', () => {
|
|
|
262
263
|
}
|
|
263
264
|
});
|
|
264
265
|
|
|
266
|
+
// ── Test: dw prompt ──────────────────────────────────────────────────────────
|
|
267
|
+
console.log();
|
|
268
|
+
console.log('▶ dw prompt');
|
|
269
|
+
|
|
270
|
+
test('prompt --text outputs structured result', () => {
|
|
271
|
+
const out = dw('prompt --text "fix login redirect after OAuth in auth middleware"', TEMP_BASE);
|
|
272
|
+
assert(out.includes('fix login redirect'), 'Should include description in output');
|
|
273
|
+
assert(!out.includes('Description seems short'), 'Long description should skip wizard');
|
|
274
|
+
});
|
|
275
|
+
|
|
276
|
+
test('prompt --text with short input expands without error', () => {
|
|
277
|
+
const out = dw('prompt --text "fix login"', TEMP_BASE);
|
|
278
|
+
assert(out.includes('fix login'), 'Should include description in output');
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
test('prompt --text empty string exits with error', () => {
|
|
282
|
+
try {
|
|
283
|
+
dw('prompt --text ""', TEMP_BASE);
|
|
284
|
+
assert(false, 'Should have thrown');
|
|
285
|
+
} catch (e) {
|
|
286
|
+
assert(e.status === 1, 'Should exit with code 1');
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
test('prompt --help shows options', () => {
|
|
291
|
+
const out = dw('prompt --help', TEMP_BASE);
|
|
292
|
+
assert(out.includes('--text'), 'Missing --text option');
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
// ── Test: dw claude-vn-fix (fixture patch) ───────────────────────────────────
|
|
296
|
+
console.log();
|
|
297
|
+
console.log('▶ dw claude-vn-fix');
|
|
298
|
+
|
|
299
|
+
test('claude-vn-fix patches known bug fixture', () => {
|
|
300
|
+
const dir = freshDir('claude-vn-fix-fixture');
|
|
301
|
+
const fixtureSrc = resolve(__dirname, '__fixtures__', 'claude-cli-bug-snippet.js');
|
|
302
|
+
const target = join(dir, 'cli.js');
|
|
303
|
+
copyFileSync(fixtureSrc, target);
|
|
304
|
+
|
|
305
|
+
const out = dw(`claude-vn-fix --path "${target}"`, dir);
|
|
306
|
+
assert(out.includes('Patch applied') || out.includes('Already patched'), 'Should apply patch');
|
|
307
|
+
const patched = readFileSync(target, 'utf-8');
|
|
308
|
+
assert(patched.includes('dw-kit Vietnamese IME fix'), 'Patch marker missing in output file');
|
|
309
|
+
});
|
|
310
|
+
|
|
265
311
|
// ── Test: dw upgrade ─────────────────────────────────────────────────────────
|
|
266
312
|
console.log();
|
|
267
313
|
console.log('▶ dw upgrade');
|
|
@@ -288,16 +334,6 @@ test('upgrade fails on project without config', () => {
|
|
|
288
334
|
}
|
|
289
335
|
});
|
|
290
336
|
|
|
291
|
-
// ── Test: dw migrate ─────────────────────────────────────────────────────────
|
|
292
|
-
console.log();
|
|
293
|
-
console.log('▶ dw migrate');
|
|
294
|
-
|
|
295
|
-
test('migrate on v1 project reports no v0.3 config', () => {
|
|
296
|
-
const dir = join(TEMP_BASE, 'init-solo');
|
|
297
|
-
const out = dw('migrate', dir);
|
|
298
|
-
assert(out.includes('Already on v1') || out.includes('No v0.3'), 'Should report no migration needed');
|
|
299
|
-
});
|
|
300
|
-
|
|
301
337
|
// ── Cleanup ──────────────────────────────────────────────────────────────────
|
|
302
338
|
rmSync(TEMP_BASE, { recursive: true });
|
|
303
339
|
|
|
@@ -1,243 +0,0 @@
|
|
|
1
|
-
#!/bin/bash
|
|
2
|
-
# scripts/migrate-v03-to-v1.sh
|
|
3
|
-
# ⚠ DEPRECATED: Prefer `dw migrate` from npm CLI.
|
|
4
|
-
# Migrate project từ dw-kit v0.3 sang v1
|
|
5
|
-
# Usage: bash scripts/migrate-v03-to-v1.sh [--dry-run]
|
|
6
|
-
|
|
7
|
-
set -euo pipefail
|
|
8
|
-
|
|
9
|
-
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
10
|
-
PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
|
|
11
|
-
|
|
12
|
-
DRY_RUN=false
|
|
13
|
-
[ "${1:-}" = "--dry-run" ] && DRY_RUN=true
|
|
14
|
-
|
|
15
|
-
log() { echo " $*"; }
|
|
16
|
-
info() { echo ""; echo "▶ $*"; }
|
|
17
|
-
warn() { echo " ⚠ $*"; }
|
|
18
|
-
ok() { echo " ✓ $*"; }
|
|
19
|
-
dry() { [ "$DRY_RUN" = true ] && echo " [dry] $*" || true; }
|
|
20
|
-
do_action() {
|
|
21
|
-
if [ "$DRY_RUN" = true ]; then echo " [dry] $*"; else eval "$*"; fi
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
OLD_CONFIG="$PROJECT_ROOT/.dw/config/dw.config.yml"
|
|
25
|
-
NEW_CONFIG_DIR="$PROJECT_ROOT/config"
|
|
26
|
-
NEW_CONFIG="$NEW_CONFIG_DIR/dw.config.yml"
|
|
27
|
-
|
|
28
|
-
echo ""
|
|
29
|
-
echo "══════════════════════════════════════════"
|
|
30
|
-
echo " dw-kit v0.3 → v1 Migration"
|
|
31
|
-
[ "$DRY_RUN" = true ] && echo " Mode: DRY RUN"
|
|
32
|
-
echo "══════════════════════════════════════════"
|
|
33
|
-
|
|
34
|
-
# ── Step 1: Validate v0.3 present ────────────────────────────────────────────
|
|
35
|
-
info "Step 1: Validate source config"
|
|
36
|
-
|
|
37
|
-
if [ ! -f "$OLD_CONFIG" ]; then
|
|
38
|
-
echo " No config/dw.config.yml found. Already on v1 or fresh install."
|
|
39
|
-
exit 0
|
|
40
|
-
fi
|
|
41
|
-
|
|
42
|
-
if [ -L "$OLD_CONFIG" ]; then
|
|
43
|
-
warn "config/dw.config.yml is already a symlink → migration may have already run."
|
|
44
|
-
read -r -p " Continue anyway? [y/N] " confirm
|
|
45
|
-
[[ "$confirm" =~ ^[Yy]$ ]] || exit 0
|
|
46
|
-
fi
|
|
47
|
-
|
|
48
|
-
ok "Found config/dw.config.yml"
|
|
49
|
-
|
|
50
|
-
# ── Step 2: Detect customized skills ─────────────────────────────────────────
|
|
51
|
-
info "Step 2: Detect customized skills"
|
|
52
|
-
|
|
53
|
-
OVERRIDES_DIR="$PROJECT_ROOT/.dw/adapters/claude-cli/overrides/skills"
|
|
54
|
-
SKILLS_DIR="$PROJECT_ROOT/.claude/skills"
|
|
55
|
-
customized_count=0
|
|
56
|
-
|
|
57
|
-
# Heuristic: compare với toolkit's own copy nếu có, hoặc check git
|
|
58
|
-
if command -v git &>/dev/null && git -C "$PROJECT_ROOT" rev-parse --git-dir &>/dev/null; then
|
|
59
|
-
for skill_dir in "$SKILLS_DIR"/*/; do
|
|
60
|
-
[ -d "$skill_dir" ] || continue
|
|
61
|
-
skill_name=$(basename "$skill_dir")
|
|
62
|
-
skill_file="$skill_dir/SKILL.md"
|
|
63
|
-
[ -f "$skill_file" ] || continue
|
|
64
|
-
|
|
65
|
-
# Check if file was modified from its committed version
|
|
66
|
-
if ! git -C "$PROJECT_ROOT" diff --quiet HEAD -- ".claude/skills/$skill_name/SKILL.md" 2>/dev/null; then
|
|
67
|
-
warn "Skill '$skill_name' has uncommitted changes → preserving to overrides/"
|
|
68
|
-
do_action "mkdir -p '$OVERRIDES_DIR/$skill_name'"
|
|
69
|
-
do_action "cp '$skill_file' '$OVERRIDES_DIR/$skill_name/SKILL.md'"
|
|
70
|
-
customized_count=$((customized_count + 1))
|
|
71
|
-
fi
|
|
72
|
-
done
|
|
73
|
-
else
|
|
74
|
-
warn "Not a git repo or git not available. Cannot auto-detect customizations."
|
|
75
|
-
warn "Manually copy customized skills to adapters/claude-cli/overrides/skills/ before upgrading."
|
|
76
|
-
fi
|
|
77
|
-
|
|
78
|
-
if [ $customized_count -gt 0 ]; then
|
|
79
|
-
ok "$customized_count customized skill(s) preserved to overrides/"
|
|
80
|
-
else
|
|
81
|
-
ok "No customized skills detected"
|
|
82
|
-
fi
|
|
83
|
-
|
|
84
|
-
# ── Step 3: Migrate config ────────────────────────────────────────────────────
|
|
85
|
-
info "Step 3: Migrate config (config/dw.config.yml → config/dw.config.yml)"
|
|
86
|
-
|
|
87
|
-
if [ -f "$NEW_CONFIG" ]; then
|
|
88
|
-
warn "config/dw.config.yml already exists. Skipping config migration."
|
|
89
|
-
warn "Review manually: $NEW_CONFIG"
|
|
90
|
-
else
|
|
91
|
-
if ! command -v python3 &>/dev/null; then
|
|
92
|
-
warn "python3 not found. Manual config migration needed."
|
|
93
|
-
warn "Map: level: 1 → default_depth: quick"
|
|
94
|
-
warn " level: 2 → default_depth: standard"
|
|
95
|
-
warn " level: 3 → default_depth: thorough"
|
|
96
|
-
else
|
|
97
|
-
if [ "$DRY_RUN" = false ]; then
|
|
98
|
-
mkdir -p "$NEW_CONFIG_DIR"
|
|
99
|
-
python3 - "$OLD_CONFIG" "$NEW_CONFIG" <<'PYEOF'
|
|
100
|
-
import sys, re
|
|
101
|
-
|
|
102
|
-
old_path = sys.argv[1]
|
|
103
|
-
new_path = sys.argv[2]
|
|
104
|
-
|
|
105
|
-
with open(old_path) as f:
|
|
106
|
-
old_content = f.read()
|
|
107
|
-
|
|
108
|
-
# Extract values from old config
|
|
109
|
-
def get_val(key, content, default=""):
|
|
110
|
-
m = re.search(rf'^\s*{re.escape(key)}:\s*([^\n#]+)', content, re.MULTILINE)
|
|
111
|
-
if m:
|
|
112
|
-
return m.group(1).strip().strip('"').strip("'")
|
|
113
|
-
return default
|
|
114
|
-
|
|
115
|
-
project_name = get_val("name", old_content, "my-project")
|
|
116
|
-
project_lang = get_val("language", old_content, "vi")
|
|
117
|
-
level_str = get_val("level", old_content, "2")
|
|
118
|
-
|
|
119
|
-
depth_map = {"1": "quick", "2": "standard", "3": "thorough"}
|
|
120
|
-
depth = depth_map.get(level_str, "standard")
|
|
121
|
-
|
|
122
|
-
# Extract roles
|
|
123
|
-
roles_match = re.findall(r'^\s+-\s+(dev|techlead|ba|qc|pm)\b', old_content, re.MULTILINE)
|
|
124
|
-
roles = roles_match if roles_match else ["dev"]
|
|
125
|
-
|
|
126
|
-
# Extract quality commands
|
|
127
|
-
test_cmd = get_val("test_command", old_content, "")
|
|
128
|
-
lint_cmd = get_val("lint_command", old_content, "")
|
|
129
|
-
|
|
130
|
-
# Extract paths
|
|
131
|
-
tasks_path = get_val("tasks", old_content, ".dw/tasks")
|
|
132
|
-
docs_path = get_val("docs", old_content, ".dw/docs")
|
|
133
|
-
|
|
134
|
-
# Build new config
|
|
135
|
-
roles_yaml = "\n".join(f" - {r}" for r in roles)
|
|
136
|
-
|
|
137
|
-
new_config = f"""# dw-kit Configuration
|
|
138
|
-
# Generated by migrate-v03-to-v1.sh
|
|
139
|
-
# Review and adjust as needed.
|
|
140
|
-
|
|
141
|
-
project:
|
|
142
|
-
name: "{project_name}"
|
|
143
|
-
language: "{project_lang}"
|
|
144
|
-
|
|
145
|
-
workflow:
|
|
146
|
-
default_depth: "{depth}" # quick | standard | thorough
|
|
147
|
-
# AI assesses per-task and may recommend different depth
|
|
148
|
-
|
|
149
|
-
team:
|
|
150
|
-
roles:
|
|
151
|
-
{roles_yaml}
|
|
152
|
-
|
|
153
|
-
quality:
|
|
154
|
-
test_command: "{test_cmd}" # empty = skip
|
|
155
|
-
lint_command: "{lint_cmd}" # empty = skip
|
|
156
|
-
block_on_fail: false
|
|
157
|
-
|
|
158
|
-
tracking:
|
|
159
|
-
estimation: false
|
|
160
|
-
log_work: false
|
|
161
|
-
estimation_unit: "hours"
|
|
162
|
-
|
|
163
|
-
paths:
|
|
164
|
-
tasks: "{tasks_path}"
|
|
165
|
-
docs: "{docs_path}"
|
|
166
|
-
|
|
167
|
-
# Claude-specific capabilities (Layer 2)
|
|
168
|
-
claude:
|
|
169
|
-
models:
|
|
170
|
-
plan: "" # empty = inherit from Claude Code settings
|
|
171
|
-
execute: ""
|
|
172
|
-
review: ""
|
|
173
|
-
structured_output: true
|
|
174
|
-
worktree_execution: false
|
|
175
|
-
mcp: []
|
|
176
|
-
|
|
177
|
-
# Toolkit version tracking (do not edit manually)
|
|
178
|
-
_toolkit:
|
|
179
|
-
core_version: "1.0"
|
|
180
|
-
platform_version: "1.0"
|
|
181
|
-
capability_version: "1.0"
|
|
182
|
-
migrated_from: "v0.3"
|
|
183
|
-
last_upgrade: "{__import__('datetime').date.today().isoformat()}"
|
|
184
|
-
"""
|
|
185
|
-
|
|
186
|
-
with open(new_path, 'w') as f:
|
|
187
|
-
f.write(new_config)
|
|
188
|
-
PYEOF
|
|
189
|
-
ok "config/dw.config.yml created (migrated from level: $level_str → depth: $(grep default_depth "$NEW_CONFIG" | head -1))"
|
|
190
|
-
else
|
|
191
|
-
dry "Would create config/dw.config.yml from config/dw.config.yml"
|
|
192
|
-
fi
|
|
193
|
-
fi
|
|
194
|
-
fi
|
|
195
|
-
|
|
196
|
-
# ── Step 4: Create backward-compat symlink ────────────────────────────────────
|
|
197
|
-
info "Step 4: Create backward-compat symlink"
|
|
198
|
-
|
|
199
|
-
if [ -f "$NEW_CONFIG" ] && [ ! -L "$OLD_CONFIG" ]; then
|
|
200
|
-
if [ "$DRY_RUN" = false ]; then
|
|
201
|
-
# Backup original
|
|
202
|
-
cp "$OLD_CONFIG" "${OLD_CONFIG}.bak"
|
|
203
|
-
ok "Backed up original to config/dw.config.yml.bak"
|
|
204
|
-
# Create symlink
|
|
205
|
-
rm "$OLD_CONFIG"
|
|
206
|
-
ln -s ".dw/.dw/config/dw.config.yml" "$OLD_CONFIG"
|
|
207
|
-
ok "Created symlink: config/dw.config.yml → config/dw.config.yml"
|
|
208
|
-
else
|
|
209
|
-
dry "Would create symlink: config/dw.config.yml → config/dw.config.yml"
|
|
210
|
-
fi
|
|
211
|
-
fi
|
|
212
|
-
|
|
213
|
-
# ── Step 5: Check CI/CD references ───────────────────────────────────────────
|
|
214
|
-
info "Step 5: Check CI/CD references"
|
|
215
|
-
|
|
216
|
-
found_refs=false
|
|
217
|
-
for ci_file in ".github/workflows/"*.yml ".gitlab-ci.yml" "Makefile" ".circleci/config.yml" "package.json"; do
|
|
218
|
-
full_path="$PROJECT_ROOT/$ci_file"
|
|
219
|
-
if [ -f "$full_path" ]; then
|
|
220
|
-
if grep -qE "dv-workflow\.config\.yml|dv-workflow-kit" "$full_path" 2>/dev/null; then
|
|
221
|
-
warn "Found reference in: $ci_file — update to config/dw.config.yml"
|
|
222
|
-
found_refs=true
|
|
223
|
-
fi
|
|
224
|
-
fi
|
|
225
|
-
done
|
|
226
|
-
[ "$found_refs" = false ] && ok "No CI/CD references found"
|
|
227
|
-
|
|
228
|
-
# ── Summary ───────────────────────────────────────────────────────────────────
|
|
229
|
-
echo ""
|
|
230
|
-
echo "══════════════════════════════════════════"
|
|
231
|
-
if [ "$DRY_RUN" = true ]; then
|
|
232
|
-
echo " DRY RUN complete. Review above, then run without --dry-run."
|
|
233
|
-
else
|
|
234
|
-
echo " Migration complete!"
|
|
235
|
-
echo ""
|
|
236
|
-
echo " Next steps:"
|
|
237
|
-
echo " 1. Review config/dw.config.yml — adjust team.roles, quality commands"
|
|
238
|
-
echo " 2. Run: bash setup.sh to regenerate .claude/ from new config"
|
|
239
|
-
echo " 3. Check adapters/claude-cli/overrides/ for preserved customizations"
|
|
240
|
-
[ "$found_refs" = true ] && echo " 4. ⚠ Update CI/CD files (see warnings above)"
|
|
241
|
-
fi
|
|
242
|
-
echo "══════════════════════════════════════════"
|
|
243
|
-
echo ""
|