dw-kit 1.9.1 → 1.9.3
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/agents/planner.md +100 -100
- package/.claude/agents/quality-checker.md +86 -86
- package/.claude/agents/researcher.md +93 -93
- package/.claude/agents/reviewer.md +126 -126
- package/.claude/hooks/supply-chain-scan.sh +0 -0
- package/.claude/rules/code-style.md +37 -37
- package/.claude/settings.json +2 -28
- package/.claude/skills/dw-plan/template-plan.md +47 -47
- package/.claude/skills/dw-research/template-research.md +51 -51
- package/.claude/skills/dw-review/checklist.md +88 -88
- package/.claude/skills/dw-thinking/THINKING.md +91 -91
- package/.claude/templates/agent-report.md +35 -35
- package/.claude/templates/en/task-context.md +77 -73
- package/.claude/templates/en/task-plan.md +83 -79
- package/.claude/templates/en/task-progress.md +69 -65
- package/.claude/templates/pr-template.md +56 -56
- package/.claude/templates/task-context.md +77 -73
- package/.claude/templates/task-plan.md +83 -79
- package/.claude/templates/task-progress.md +69 -65
- package/.dw/adapters/claude-cli/extensions/README.md +36 -36
- package/.dw/adapters/claude-cli/generated/README.md +23 -23
- package/.dw/adapters/claude-cli/overrides/README.md +37 -37
- package/.dw/adapters/generic/README.md +21 -21
- package/.dw/config/presets/enterprise.yml +52 -52
- package/.dw/config/presets/small-team.yml +39 -39
- package/.dw/config/presets/solo-quick.yml +37 -37
- package/.dw/core/AGENTS.md +53 -53
- package/.dw/core/QUALITY.md +220 -220
- package/.dw/core/THINKING.md +126 -126
- package/.dw/core/WORKFLOW.md +17 -12
- package/.dw/core/templates/v2/spec.md +2 -0
- package/.dw/core/templates/v2/tracking.md +2 -0
- package/.dw/core/templates/vi/task-context.md +96 -92
- package/.dw/core/templates/vi/task-plan.md +97 -93
- package/.dw/core/templates/vi/task-progress.md +60 -56
- package/LICENSE +201 -201
- package/NOTICE +26 -26
- package/README.md +150 -121
- package/README.vi.md +230 -0
- package/bin/dw.mjs +28 -28
- package/package.json +4 -1
- package/src/commands/claude-vn-fix.mjs +267 -267
- package/src/commands/prompt.mjs +112 -112
- package/src/commands/validate.mjs +102 -102
- package/src/commands/voice.mjs +431 -2
- package/src/lib/board-data.mjs +23 -57
- package/src/lib/clipboard.mjs +24 -24
- package/src/lib/goal-driver.mjs +312 -0
- package/src/lib/goal-progress.mjs +193 -0
- package/src/lib/platform.mjs +39 -39
- package/src/lib/process-kill.mjs +77 -0
- package/src/lib/prompt-suggest.mjs +84 -84
- package/src/lib/task-md-utils.mjs +78 -0
- package/src/lib/ui.mjs +66 -66
- package/src/lib/update-checker.mjs +73 -73
- package/.dw/security/advisory-snapshot.json +0 -157
|
@@ -1,84 +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
|
-
}
|
|
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,78 @@
|
|
|
1
|
+
// task-md-utils.mjs — shared parsers for task.md v3 files.
|
|
2
|
+
//
|
|
3
|
+
// Used by board-data.mjs + goal-progress.mjs. Extracted into its own module
|
|
4
|
+
// to break the cycle that would otherwise form between those two
|
|
5
|
+
// (board-data attaches progress.computeGoalProgress, goal-progress reads the
|
|
6
|
+
// same parseSubtasks board-data uses).
|
|
7
|
+
//
|
|
8
|
+
// Per ADR-0001: zero-dep beyond js-yaml.
|
|
9
|
+
|
|
10
|
+
import { readFileSync, existsSync, readdirSync, statSync } from 'node:fs';
|
|
11
|
+
import { join } from 'node:path';
|
|
12
|
+
import yaml from 'js-yaml';
|
|
13
|
+
|
|
14
|
+
const TASKS_DIR = '.dw/tasks';
|
|
15
|
+
|
|
16
|
+
const STATUS_ICONS = {
|
|
17
|
+
'⬜': 'pending',
|
|
18
|
+
'🟡': 'in_progress',
|
|
19
|
+
'✅': 'done',
|
|
20
|
+
'🔴': 'blocked',
|
|
21
|
+
'⏸': 'paused',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** Extract frontmatter (between --- markers) from a markdown file. */
|
|
25
|
+
export function readFrontmatter(file) {
|
|
26
|
+
if (!existsSync(file)) return {};
|
|
27
|
+
let txt;
|
|
28
|
+
try { txt = readFileSync(file, 'utf8'); } catch { return {}; }
|
|
29
|
+
const m = txt.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
30
|
+
if (!m) return {};
|
|
31
|
+
try { return yaml.load(m[1]) || {}; } catch { return {}; }
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Parse Section 3 Subtask Tracker rows from a task.md.
|
|
36
|
+
* Returns rows shaped { st_id, title, status_bucket, status_icon, status_label, date, notes }.
|
|
37
|
+
*/
|
|
38
|
+
export function parseSubtasks(taskPath) {
|
|
39
|
+
if (!existsSync(taskPath)) return [];
|
|
40
|
+
let txt;
|
|
41
|
+
try { txt = readFileSync(taskPath, 'utf8'); } catch { return []; }
|
|
42
|
+
const sec = txt.match(/^## 3\.[^\n]*\n([\s\S]*?)(?=^## 4\.|$(?![\s\S]))/m);
|
|
43
|
+
if (!sec) return [];
|
|
44
|
+
const out = [];
|
|
45
|
+
for (const line of sec[1].split('\n')) {
|
|
46
|
+
// `u` flag is essential — 🟡 (U+1F7E1) and 🔴 (U+1F534) are surrogate
|
|
47
|
+
// pairs; the character class would otherwise match the low surrogate
|
|
48
|
+
// instead of the emoji, silently dropping in_progress / blocked rows.
|
|
49
|
+
const m = line.match(/^\|\s*(ST-[\w.-]+)\s*\|\s*(.+?)\s*\|\s*([⬜🟡✅🔴⏸])\s*([A-Za-z ]+)?\s*\|\s*([^|]*)\|\s*([^|]*)\|/u);
|
|
50
|
+
if (!m) continue;
|
|
51
|
+
const icon = m[3];
|
|
52
|
+
const statusBucket = STATUS_ICONS[icon] || 'unknown';
|
|
53
|
+
const statusLabel = (m[4] || '').trim();
|
|
54
|
+
out.push({
|
|
55
|
+
st_id: m[1].trim(),
|
|
56
|
+
title: m[2].replace(/`/g, '').trim(),
|
|
57
|
+
status_bucket: statusBucket,
|
|
58
|
+
status_icon: icon,
|
|
59
|
+
status_label: statusLabel,
|
|
60
|
+
date: m[5].trim(),
|
|
61
|
+
notes: m[6].trim().slice(0, 160),
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
return out;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** List task directories under `.dw/tasks/` (skipping archive + dotfiles). */
|
|
68
|
+
export function listTaskDirs(rootDir) {
|
|
69
|
+
const dir = join(rootDir, TASKS_DIR);
|
|
70
|
+
if (!existsSync(dir)) return [];
|
|
71
|
+
return readdirSync(dir)
|
|
72
|
+
.filter((entry) => {
|
|
73
|
+
if (entry.startsWith('.') || entry === 'archive') return false;
|
|
74
|
+
try { return statSync(join(dir, entry)).isDirectory(); } catch { return false; }
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export { TASKS_DIR, STATUS_ICONS };
|
package/src/lib/ui.mjs
CHANGED
|
@@ -1,66 +1,66 @@
|
|
|
1
|
-
import chalk from 'chalk';
|
|
2
|
-
import { createInterface } from 'node:readline/promises';
|
|
3
|
-
import { stdin, stdout } from 'node:process';
|
|
4
|
-
|
|
5
|
-
export const BANNER = `
|
|
6
|
-
██████╗ ██╗ ██╗ ██╗ ██╗██╗████████╗
|
|
7
|
-
██╔══██╗██║ ██║ ██║ ██╔╝██║╚══██╔══╝
|
|
8
|
-
██║ ██║██║ █╗ ██║ █████╔╝ ██║ ██║
|
|
9
|
-
██║ ██║██║███╗██║ ██╔═██╗ ██║ ██║
|
|
10
|
-
██████╔╝╚███╔███╔╝ ██║ ██╗██║ ██║
|
|
11
|
-
╚═════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝`;
|
|
12
|
-
|
|
13
|
-
export function banner(subtitle) {
|
|
14
|
-
console.log(chalk.cyan.bold(BANNER));
|
|
15
|
-
if (subtitle) console.log(chalk.cyan(` ${subtitle}`));
|
|
16
|
-
console.log();
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
export function header(text) {
|
|
20
|
-
console.log();
|
|
21
|
-
console.log(chalk.bold(`══════════════════════════════════════════`));
|
|
22
|
-
console.log(chalk.bold(` ${text}`));
|
|
23
|
-
console.log(chalk.bold(`══════════════════════════════════════════`));
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
export const log = (msg) => console.log(` ${msg}`);
|
|
27
|
-
export const ok = (msg) => console.log(chalk.green(` ✓ ${msg}`));
|
|
28
|
-
export const warn = (msg) => console.log(chalk.yellow(` ⚠ ${msg}`));
|
|
29
|
-
export const err = (msg) => console.log(chalk.red(` ✗ ${msg}`));
|
|
30
|
-
export const info = (msg) => { console.log(); console.log(chalk.cyan(`▶ ${msg}`)); };
|
|
31
|
-
export const dry = (msg) => console.log(chalk.dim(` [dry-run] ${msg}`));
|
|
32
|
-
|
|
33
|
-
export async function ask(question, defaultValue) {
|
|
34
|
-
const rl = createInterface({ input: stdin, output: stdout });
|
|
35
|
-
const suffix = defaultValue ? ` [${defaultValue}]` : '';
|
|
36
|
-
try {
|
|
37
|
-
const answer = await rl.question(chalk.bold(` ${question}${suffix}: `));
|
|
38
|
-
return answer.trim() || defaultValue || '';
|
|
39
|
-
} finally {
|
|
40
|
-
rl.close();
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
export async function choose(question, options, defaultValue) {
|
|
45
|
-
console.log(chalk.bold(` ${question}`));
|
|
46
|
-
for (const opt of options) {
|
|
47
|
-
const marker = opt.value === defaultValue ? chalk.cyan(' [default]') : '';
|
|
48
|
-
console.log(` ${opt.value} = ${opt.label}${marker}`);
|
|
49
|
-
}
|
|
50
|
-
const allowed = new Set(options.map((o) => o.value));
|
|
51
|
-
while (true) {
|
|
52
|
-
const answer = await ask('>', defaultValue);
|
|
53
|
-
if (allowed.has(answer)) return answer;
|
|
54
|
-
warn(`Invalid choice: "${answer}". Allowed: ${[...allowed].join(', ')}`);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
export async function multiSelect(question, options, defaultValues) {
|
|
59
|
-
console.log(chalk.bold(` ${question}`));
|
|
60
|
-
for (const opt of options) {
|
|
61
|
-
const marker = defaultValues?.includes(opt.value) ? chalk.dim(' (included)') : '';
|
|
62
|
-
console.log(` ${opt.key} = ${opt.label}${marker}`);
|
|
63
|
-
}
|
|
64
|
-
const hint = defaultValues ? defaultValues.join(',') : '';
|
|
65
|
-
return ask('Enter numbers separated by comma', hint);
|
|
66
|
-
}
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import { createInterface } from 'node:readline/promises';
|
|
3
|
+
import { stdin, stdout } from 'node:process';
|
|
4
|
+
|
|
5
|
+
export const BANNER = `
|
|
6
|
+
██████╗ ██╗ ██╗ ██╗ ██╗██╗████████╗
|
|
7
|
+
██╔══██╗██║ ██║ ██║ ██╔╝██║╚══██╔══╝
|
|
8
|
+
██║ ██║██║ █╗ ██║ █████╔╝ ██║ ██║
|
|
9
|
+
██║ ██║██║███╗██║ ██╔═██╗ ██║ ██║
|
|
10
|
+
██████╔╝╚███╔███╔╝ ██║ ██╗██║ ██║
|
|
11
|
+
╚═════╝ ╚══╝╚══╝ ╚═╝ ╚═╝╚═╝ ╚═╝`;
|
|
12
|
+
|
|
13
|
+
export function banner(subtitle) {
|
|
14
|
+
console.log(chalk.cyan.bold(BANNER));
|
|
15
|
+
if (subtitle) console.log(chalk.cyan(` ${subtitle}`));
|
|
16
|
+
console.log();
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function header(text) {
|
|
20
|
+
console.log();
|
|
21
|
+
console.log(chalk.bold(`══════════════════════════════════════════`));
|
|
22
|
+
console.log(chalk.bold(` ${text}`));
|
|
23
|
+
console.log(chalk.bold(`══════════════════════════════════════════`));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const log = (msg) => console.log(` ${msg}`);
|
|
27
|
+
export const ok = (msg) => console.log(chalk.green(` ✓ ${msg}`));
|
|
28
|
+
export const warn = (msg) => console.log(chalk.yellow(` ⚠ ${msg}`));
|
|
29
|
+
export const err = (msg) => console.log(chalk.red(` ✗ ${msg}`));
|
|
30
|
+
export const info = (msg) => { console.log(); console.log(chalk.cyan(`▶ ${msg}`)); };
|
|
31
|
+
export const dry = (msg) => console.log(chalk.dim(` [dry-run] ${msg}`));
|
|
32
|
+
|
|
33
|
+
export async function ask(question, defaultValue) {
|
|
34
|
+
const rl = createInterface({ input: stdin, output: stdout });
|
|
35
|
+
const suffix = defaultValue ? ` [${defaultValue}]` : '';
|
|
36
|
+
try {
|
|
37
|
+
const answer = await rl.question(chalk.bold(` ${question}${suffix}: `));
|
|
38
|
+
return answer.trim() || defaultValue || '';
|
|
39
|
+
} finally {
|
|
40
|
+
rl.close();
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export async function choose(question, options, defaultValue) {
|
|
45
|
+
console.log(chalk.bold(` ${question}`));
|
|
46
|
+
for (const opt of options) {
|
|
47
|
+
const marker = opt.value === defaultValue ? chalk.cyan(' [default]') : '';
|
|
48
|
+
console.log(` ${opt.value} = ${opt.label}${marker}`);
|
|
49
|
+
}
|
|
50
|
+
const allowed = new Set(options.map((o) => o.value));
|
|
51
|
+
while (true) {
|
|
52
|
+
const answer = await ask('>', defaultValue);
|
|
53
|
+
if (allowed.has(answer)) return answer;
|
|
54
|
+
warn(`Invalid choice: "${answer}". Allowed: ${[...allowed].join(', ')}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function multiSelect(question, options, defaultValues) {
|
|
59
|
+
console.log(chalk.bold(` ${question}`));
|
|
60
|
+
for (const opt of options) {
|
|
61
|
+
const marker = defaultValues?.includes(opt.value) ? chalk.dim(' (included)') : '';
|
|
62
|
+
console.log(` ${opt.key} = ${opt.label}${marker}`);
|
|
63
|
+
}
|
|
64
|
+
const hint = defaultValues ? defaultValues.join(',') : '';
|
|
65
|
+
return ask('Enter numbers separated by comma', hint);
|
|
66
|
+
}
|
|
@@ -1,73 +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
|
-
}
|
|
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
|
+
}
|