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,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
+ }
52
+
53
+ if (layer === 'all' || layer === 'capability') {
54
+ totalChanges += upgradeCapability(projectDir, opts);
55
+ }
56
+
57
+ upgradeScripts(projectDir, opts);
58
+ upgradeConfigSchema(projectDir, opts);
59
+
60
+ if (!opts.dryRun && totalChanges > 0) {
61
+ updateVersionTracking(configPath, projectConfig, toolkitVersions);
62
+ }
63
+
64
+ console.log();
65
+ header(opts.dryRun ? 'DRY RUN complete — no changes made' : `Upgrade complete (${totalChanges} files updated)`);
66
+ if (opts.dryRun) log('Run without --dry-run to apply.');
67
+ console.log();
68
+ }
69
+
70
+ function upgradeCore(projectDir, opts) {
71
+ info('Layer 0: Methodology Core (.dw/core/)');
72
+ const src = join(TOOLKIT_ROOT, '.dw', 'core');
73
+ const dst = join(projectDir, '.dw', 'core');
74
+
75
+ const diff = diffDirs(src, dst);
76
+ reportDiff(diff);
77
+
78
+ if (diff.added.length === 0 && diff.modified.length === 0) {
79
+ ok('Core files are up to date');
80
+ return 0;
81
+ }
82
+
83
+ const filesToUpdate = [...diff.added, ...diff.modified];
84
+ for (const file of filesToUpdate) {
85
+ if (opts.dryRun) {
86
+ dry(`${diff.added.includes(file) ? 'add' : 'update'} .dw/core/${file}`);
87
+ } else {
88
+ copyFile(join(src, file), join(dst, file));
89
+ ok(`.dw/core/${file}`);
90
+ }
91
+ }
92
+ return filesToUpdate.length;
93
+ }
94
+
95
+ function upgradePlatform(projectDir, opts) {
96
+ info('Layer 1: Platform Files (.claude/)');
97
+ const src = join(TOOLKIT_ROOT, '.claude');
98
+ const dst = join(projectDir, '.claude');
99
+
100
+ if (!existsSync(dst)) {
101
+ warn('.claude/ not found — skipping platform upgrade (run dw init first)');
102
+ return 0;
103
+ }
104
+
105
+ const overridesDir = join(projectDir, '.dw', 'adapters', 'claude-cli', 'overrides');
106
+ const diff = diffDirs(src, dst);
107
+ reportDiff(diff);
108
+
109
+ if (diff.added.length === 0 && diff.modified.length === 0) {
110
+ ok('Platform files are up to date');
111
+ return 0;
112
+ }
113
+
114
+ let count = 0;
115
+ const filesToUpdate = [...diff.added, ...diff.modified];
116
+ for (const file of filesToUpdate) {
117
+ const overridePath = join(overridesDir, file);
118
+ if (existsSync(overridePath)) {
119
+ warn(`${file}: override exists → keeping your version`);
120
+ continue;
121
+ }
122
+
123
+ if (opts.dryRun) {
124
+ dry(`${diff.added.includes(file) ? 'add' : 'update'} .claude/${file}`);
125
+ } else {
126
+ copyFile(join(src, file), join(dst, file));
127
+ ok(`.claude/${file}`);
128
+ }
129
+ count++;
130
+ }
131
+
132
+ copyExtensions(projectDir, opts);
133
+ mergeSettingsJson(projectDir, opts);
134
+
135
+ return count;
136
+ }
137
+
138
+ function copyExtensions(projectDir, opts) {
139
+ const extDir = join(projectDir, '.dw', 'adapters', 'claude-cli', 'extensions');
140
+ const skillsDir = join(projectDir, '.claude', 'skills');
141
+
142
+ if (!existsSync(extDir)) return;
143
+
144
+ let count = 0;
145
+ try {
146
+ const entries = readdirSync(extDir, { withFileTypes: true });
147
+ for (const entry of entries) {
148
+ if (!entry.isDirectory() || entry.name === '.gitkeep') continue;
149
+ const src = join(extDir, entry.name);
150
+ const dst = join(skillsDir, entry.name);
151
+ if (opts.dryRun) {
152
+ dry(`install extension: ${entry.name}`);
153
+ } else {
154
+ copyDir(src, dst, { overwrite: true });
155
+ ok(`Extension '${entry.name}' installed`);
156
+ }
157
+ count++;
158
+ }
159
+ } catch { /* empty extensions dir */ }
160
+ if (count === 0) log('No extensions found');
161
+ }
162
+
163
+ function mergeSettingsJson(projectDir, opts) {
164
+ const toolkitSettings = join(TOOLKIT_ROOT, '.claude', 'settings.json');
165
+ const projectSettings = join(projectDir, '.claude', 'settings.json');
166
+
167
+ if (!existsSync(toolkitSettings) || !existsSync(projectSettings)) return;
168
+
169
+ if (opts.dryRun) {
170
+ dry('merge .claude/settings.json');
171
+ return;
172
+ }
173
+
174
+ try {
175
+ const template = JSON.parse(readFileSync(toolkitSettings, 'utf-8'));
176
+ const current = JSON.parse(readFileSync(projectSettings, 'utf-8'));
177
+ const merged = deepMerge(template, current);
178
+ writeFileSync(projectSettings, JSON.stringify(merged, null, 2) + '\n', 'utf-8');
179
+ ok('settings.json: merged');
180
+ } catch (e) {
181
+ warn(`settings.json merge failed: ${e.message}`);
182
+ }
183
+ }
184
+
185
+ function deepMerge(base, override) {
186
+ const result = { ...base };
187
+ for (const [key, val] of Object.entries(override)) {
188
+ if (key in result && typeof result[key] === 'object' && !Array.isArray(result[key]) && typeof val === 'object' && !Array.isArray(val)) {
189
+ result[key] = deepMerge(result[key], val);
190
+ } else {
191
+ result[key] = val;
192
+ }
193
+ }
194
+ return result;
195
+ }
196
+
197
+ function upgradeCapability(projectDir, opts) {
198
+ info('Layer 2: Capability Config');
199
+ ok('Capability layer is config-driven — no file changes needed');
200
+ log('Review claude: section in .dw/config/dw.config.yml for new options');
201
+ return 0;
202
+ }
203
+
204
+ function upgradeScripts(projectDir, opts) {
205
+ info('Scripts');
206
+ const src = join(TOOLKIT_ROOT, 'scripts');
207
+ const dst = join(projectDir, 'scripts');
208
+
209
+ if (!existsSync(src)) return;
210
+
211
+ const diff = diffDirs(src, dst);
212
+ if (diff.added.length === 0 && diff.modified.length === 0) {
213
+ ok('Scripts are up to date');
214
+ return;
215
+ }
216
+
217
+ for (const file of [...diff.added, ...diff.modified]) {
218
+ if (opts.dryRun) {
219
+ dry(`update scripts/${file}`);
220
+ } else {
221
+ copyFile(join(src, file), join(dst, file));
222
+ ok(`scripts/${file}`);
223
+ }
224
+ }
225
+ }
226
+
227
+ function upgradeConfigSchema(projectDir, opts) {
228
+ const src = join(TOOLKIT_ROOT, '.dw', 'config', 'config.schema.json');
229
+ const dst = join(projectDir, '.dw', 'config', 'config.schema.json');
230
+
231
+ if (!existsSync(src)) return;
232
+ if (existsSync(dst)) {
233
+ const srcContent = readFileSync(src, 'utf-8');
234
+ const dstContent = readFileSync(dst, 'utf-8');
235
+ if (srcContent === dstContent) return;
236
+ }
237
+
238
+ if (opts.dryRun) {
239
+ dry('update .dw/config/config.schema.json');
240
+ } else {
241
+ copyFile(src, dst);
242
+ ok('.dw/config/config.schema.json updated');
243
+ }
244
+ }
245
+
246
+ function updateVersionTracking(configPath, config, toolkitVersions) {
247
+ const today = new Date().toISOString().split('T')[0];
248
+ if (!config._toolkit) config._toolkit = {};
249
+ config._toolkit.core_version = toolkitVersions.core;
250
+ config._toolkit.platform_version = toolkitVersions.platform;
251
+ config._toolkit.capability_version = toolkitVersions.capability;
252
+ config._toolkit.last_upgrade = today;
253
+
254
+ writeConfig(configPath, config);
255
+ ok(`Version tracking updated: core=${toolkitVersions.core}, date=${today}`);
256
+ }
257
+
258
+ function reportDiff(diff) {
259
+ if (diff.added.length > 0) log(` New files: ${diff.added.length}`);
260
+ if (diff.modified.length > 0) log(` Modified: ${diff.modified.length}`);
261
+ if (diff.unchanged.length > 0) log(` Unchanged: ${diff.unchanged.length}`);
262
+ }
@@ -0,0 +1,102 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
3
+ import { fileURLToPath } from 'node:url';
4
+ import Ajv from 'ajv';
5
+ import { header, ok, warn, err, info, log } from '../lib/ui.mjs';
6
+ import { loadConfig, loadSchema } from '../lib/config.mjs';
7
+
8
+ const TOOLKIT_ROOT = resolve(fileURLToPath(import.meta.url), '..', '..', '..');
9
+
10
+ export async function validateCommand(opts) {
11
+ const configPath = resolve(opts.file);
12
+
13
+ header('dw-kit Config Validation');
14
+
15
+ if (!existsSync(configPath)) {
16
+ err(`Config file not found: ${configPath}`);
17
+ log('Run `dw init` to create a config file.');
18
+ process.exit(1);
19
+ }
20
+
21
+ info(`Loading ${opts.file}`);
22
+ const config = loadConfig(configPath);
23
+ if (!config) {
24
+ err('Failed to parse YAML config.');
25
+ process.exit(1);
26
+ }
27
+ ok('YAML syntax valid');
28
+
29
+ info('Validating against schema');
30
+ const schemaPath = resolve('.dw', 'config', 'config.schema.json');
31
+ const fallbackSchemaPath = join(TOOLKIT_ROOT, '.dw', 'config', 'config.schema.json');
32
+ let schema = loadSchema(schemaPath) || loadSchema(fallbackSchemaPath);
33
+
34
+ if (!schema) {
35
+ warn('Schema file not found — skipping schema validation');
36
+ warn('Expected at: .dw/config/config.schema.json');
37
+ return;
38
+ }
39
+
40
+ const ajv = new Ajv({ allErrors: true, strict: false });
41
+ const validate = ajv.compile(schema);
42
+ const valid = validate(config);
43
+
44
+ if (valid) {
45
+ ok('Schema validation passed');
46
+ } else {
47
+ err('Schema validation failed:');
48
+ for (const error of validate.errors) {
49
+ const path = error.instancePath || '(root)';
50
+ log(` ${path}: ${error.message}`);
51
+ if (error.params?.allowedValues) {
52
+ log(` Allowed: ${error.params.allowedValues.join(', ')}`);
53
+ }
54
+ if (error.params?.additionalProperty) {
55
+ log(` Unknown key: ${error.params.additionalProperty}`);
56
+ }
57
+ }
58
+ process.exit(1);
59
+ }
60
+
61
+ info('Semantic checks');
62
+ runSemanticChecks(config);
63
+
64
+ console.log();
65
+ ok('Config is valid.');
66
+ console.log();
67
+ }
68
+
69
+ function runSemanticChecks(config) {
70
+ const warnings = [];
71
+
72
+ if (!config.project?.name || config.project.name === 'my-project') {
73
+ warnings.push('project.name is still "my-project" — consider updating');
74
+ }
75
+
76
+ if (config.quality?.block_on_fail && !config.quality?.test_command && !config.quality?.lint_command) {
77
+ warnings.push('quality.block_on_fail is true but no test_command or lint_command configured');
78
+ }
79
+
80
+ if (config.tracking?.estimation && config.workflow?.default_depth === 'quick') {
81
+ warnings.push('tracking.estimation is enabled but depth is "quick" — estimation may add unnecessary overhead');
82
+ }
83
+
84
+ const roles = config.team?.roles || [];
85
+ if (roles.includes('pm') && !config.tracking?.log_work) {
86
+ warnings.push('PM role enabled but tracking.log_work is false — PM dashboard needs work logs');
87
+ }
88
+
89
+ if (roles.includes('qc') && !config.quality?.test_command) {
90
+ warnings.push('QC role enabled but no quality.test_command — QC workflow benefits from tests');
91
+ }
92
+
93
+ if (config.claude?.worktree_execution && config.workflow?.default_depth === 'quick') {
94
+ warnings.push('claude.worktree_execution is true with "quick" depth — worktree is typically for thorough workflows');
95
+ }
96
+
97
+ if (warnings.length === 0) {
98
+ ok('No semantic issues found');
99
+ } else {
100
+ for (const w of warnings) warn(w);
101
+ }
102
+ }
@@ -0,0 +1,75 @@
1
+ import { readFileSync, writeFileSync, existsSync } from 'node:fs';
2
+ import yaml from 'js-yaml';
3
+
4
+ export function loadConfig(configPath) {
5
+ if (!existsSync(configPath)) return null;
6
+ const content = readFileSync(configPath, 'utf-8');
7
+ return yaml.load(content);
8
+ }
9
+
10
+ export function writeConfig(configPath, data) {
11
+ const content = yaml.dump(data, {
12
+ indent: 2,
13
+ lineWidth: -1,
14
+ quotingType: '"',
15
+ forceQuotes: false,
16
+ noRefs: true,
17
+ });
18
+ writeFileSync(configPath, content, 'utf-8');
19
+ }
20
+
21
+ export function loadSchema(schemaPath) {
22
+ if (!existsSync(schemaPath)) return null;
23
+ return JSON.parse(readFileSync(schemaPath, 'utf-8'));
24
+ }
25
+
26
+ export function buildConfig({ projectName, language, depth, roles }) {
27
+ const today = new Date().toISOString().split('T')[0];
28
+ return {
29
+ project: {
30
+ name: projectName,
31
+ language: language,
32
+ },
33
+ workflow: {
34
+ default_depth: depth,
35
+ },
36
+ team: {
37
+ roles: roles,
38
+ },
39
+ quality: {
40
+ test_command: '',
41
+ lint_command: '',
42
+ block_on_fail: false,
43
+ },
44
+ tracking: {
45
+ estimation: depth !== 'quick',
46
+ log_work: depth === 'thorough',
47
+ estimation_unit: 'hours',
48
+ },
49
+ paths: {
50
+ tasks: '.dw/tasks',
51
+ docs: '.dw/docs',
52
+ },
53
+ claude: {
54
+ models: { plan: '', execute: '', review: '' },
55
+ structured_output: depth !== 'quick',
56
+ worktree_execution: false,
57
+ mcp: [],
58
+ },
59
+ _toolkit: {
60
+ core_version: '1.0',
61
+ platform_version: '1.0',
62
+ capability_version: '1.0',
63
+ installed: today,
64
+ last_upgrade: today,
65
+ },
66
+ };
67
+ }
68
+
69
+ export function getToolkitVersions(config) {
70
+ return {
71
+ core: config?._toolkit?.core_version || 'unknown',
72
+ platform: config?._toolkit?.platform_version || 'unknown',
73
+ capability: config?._toolkit?.capability_version || 'unknown',
74
+ };
75
+ }
@@ -0,0 +1,110 @@
1
+ import {
2
+ existsSync, mkdirSync, copyFileSync, readdirSync, readFileSync,
3
+ } from 'node:fs';
4
+ import { join, dirname } from 'node:path';
5
+
6
+ export function ensureDir(dir) {
7
+ if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
8
+ }
9
+
10
+ export function copyFile(src, dst, { dryRun = false } = {}) {
11
+ if (dryRun) return { action: 'copy', src, dst, applied: false };
12
+ ensureDir(dirname(dst));
13
+ copyFileSync(src, dst);
14
+ return { action: 'copy', src, dst, applied: true };
15
+ }
16
+
17
+ export function copyDir(srcDir, dstDir, { dryRun = false, overwrite = false } = {}) {
18
+ const results = [];
19
+ if (!existsSync(srcDir)) return results;
20
+
21
+ const entries = readdirSync(srcDir, { withFileTypes: true });
22
+ for (const entry of entries) {
23
+ const srcPath = join(srcDir, entry.name);
24
+ const dstPath = join(dstDir, entry.name);
25
+
26
+ if (entry.isDirectory()) {
27
+ results.push(...copyDir(srcPath, dstPath, { dryRun, overwrite }));
28
+ } else if (entry.isFile()) {
29
+ if (!overwrite && existsSync(dstPath)) {
30
+ results.push({ action: 'skip', src: srcPath, dst: dstPath, reason: 'exists' });
31
+ continue;
32
+ }
33
+ results.push(copyFile(srcPath, dstPath, { dryRun }));
34
+ }
35
+ }
36
+ return results;
37
+ }
38
+
39
+ /**
40
+ * Copy files from srcDir to dstDir, respecting an overrides directory.
41
+ * Files present in overridesDir take precedence over srcDir.
42
+ */
43
+ export function copyWithOverrides(srcDir, dstDir, overridesDir, { dryRun = false } = {}) {
44
+ const results = [];
45
+ if (!existsSync(srcDir)) return results;
46
+
47
+ const entries = readdirSync(srcDir, { withFileTypes: true });
48
+ for (const entry of entries) {
49
+ const srcPath = join(srcDir, entry.name);
50
+ const dstPath = join(dstDir, entry.name);
51
+ const overridePath = overridesDir ? join(overridesDir, entry.name) : null;
52
+
53
+ if (entry.isDirectory()) {
54
+ const subOverride = overridePath && existsSync(overridePath) ? overridePath : null;
55
+ results.push(...copyWithOverrides(srcPath, dstPath, subOverride, { dryRun }));
56
+ } else if (entry.isFile()) {
57
+ if (overridePath && existsSync(overridePath)) {
58
+ results.push({
59
+ action: 'override',
60
+ src: overridePath,
61
+ dst: dstPath,
62
+ ...(dryRun ? { applied: false } : (() => { ensureDir(dirname(dstPath)); copyFileSync(overridePath, dstPath); return { applied: true }; })()),
63
+ });
64
+ } else {
65
+ results.push(copyFile(srcPath, dstPath, { dryRun }));
66
+ }
67
+ }
68
+ }
69
+ return results;
70
+ }
71
+
72
+ /**
73
+ * Compute file differences between two directories.
74
+ * Returns { added, modified, unchanged } arrays of relative paths.
75
+ */
76
+ export function diffDirs(sourceDir, targetDir) {
77
+ const added = [];
78
+ const modified = [];
79
+ const unchanged = [];
80
+
81
+ if (!existsSync(sourceDir)) return { added, modified, unchanged };
82
+
83
+ function walk(dir, base) {
84
+ const entries = readdirSync(dir, { withFileTypes: true });
85
+ for (const entry of entries) {
86
+ const srcPath = join(dir, entry.name);
87
+ const relPath = base ? join(base, entry.name) : entry.name;
88
+
89
+ if (entry.isDirectory()) {
90
+ walk(srcPath, relPath);
91
+ } else if (entry.isFile()) {
92
+ const targetPath = join(targetDir, relPath);
93
+ if (!existsSync(targetPath)) {
94
+ added.push(relPath);
95
+ } else {
96
+ const srcContent = readFileSync(srcPath);
97
+ const tgtContent = readFileSync(targetPath);
98
+ if (srcContent.equals(tgtContent)) {
99
+ unchanged.push(relPath);
100
+ } else {
101
+ modified.push(relPath);
102
+ }
103
+ }
104
+ }
105
+ }
106
+ }
107
+
108
+ walk(sourceDir, '');
109
+ return { added, modified, unchanged };
110
+ }
@@ -0,0 +1,39 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { execSync } from 'node:child_process';
4
+
5
+ /**
6
+ * Detect which AI development platform is available.
7
+ * Returns: 'claude-cli' | 'cursor' | 'generic'
8
+ */
9
+ export function detectPlatform(projectDir = process.cwd()) {
10
+ if (isClaudeCliAvailable()) return 'claude-cli';
11
+ if (isCursorProject(projectDir)) return 'cursor';
12
+ return 'generic';
13
+ }
14
+
15
+ function isClaudeCliAvailable() {
16
+ try {
17
+ execSync('claude --version', { stdio: 'pipe', timeout: 5000 });
18
+ return true;
19
+ } catch {
20
+ return false;
21
+ }
22
+ }
23
+
24
+ function isCursorProject(projectDir) {
25
+ return (
26
+ existsSync(join(projectDir, '.cursor')) ||
27
+ existsSync(join(projectDir, '.cursorrc')) ||
28
+ process.env.CURSOR_SESSION_ID != null
29
+ );
30
+ }
31
+
32
+ export function platformLabel(platform) {
33
+ const labels = {
34
+ 'claude-cli': 'Claude Code CLI',
35
+ 'cursor': 'Cursor IDE',
36
+ 'generic': 'Generic (AGENT.md)',
37
+ };
38
+ return labels[platform] || platform;
39
+ }
package/src/lib/ui.mjs ADDED
@@ -0,0 +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
+ }