context-engineer 1.1.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 (74) hide show
  1. package/README.md +88 -0
  2. package/bin/cli.mjs +91 -0
  3. package/lib/copy.mjs +102 -0
  4. package/lib/init.mjs +166 -0
  5. package/lib/prompts.mjs +144 -0
  6. package/lib/update.mjs +198 -0
  7. package/package.json +35 -0
  8. package/templates/checksums.json +68 -0
  9. package/templates/claude/.claude/rules/context-maintenance.md +38 -0
  10. package/templates/claude/.claude/rules/experience-capture.md +46 -0
  11. package/templates/claude/.claude/settings.project.json +22 -0
  12. package/templates/claude/.claude/skills/bootstrap/SKILL.md +223 -0
  13. package/templates/claude/.claude/skills/dev/SKILL.md +119 -0
  14. package/templates/claude/.claude/skills/dev-capture/SKILL.md +111 -0
  15. package/templates/claude/.claude/skills/dev-commit/SKILL.md +90 -0
  16. package/templates/claude/.claude/skills/dev-decompose/SKILL.md +113 -0
  17. package/templates/claude/.claude/skills/dev-deps/SKILL.md +108 -0
  18. package/templates/claude/.claude/skills/dev-execute/SKILL.md +196 -0
  19. package/templates/claude/.claude/skills/dev-prd/SKILL.md +100 -0
  20. package/templates/claude/.claude/skills/dev-quality/SKILL.md +109 -0
  21. package/templates/claude/.claude/skills/dev-requirements/SKILL.md +75 -0
  22. package/templates/claude/.claude/skills/review-context/SKILL.md +120 -0
  23. package/templates/claude/.claude/skills/sync/SKILL.md +107 -0
  24. package/templates/claude/.claude/skills/update-context/SKILL.md +105 -0
  25. package/templates/claude/.claude/workflow/agents/implementer.md +65 -0
  26. package/templates/claude/.claude/workflow/agents/reviewer.md +96 -0
  27. package/templates/claude/.claude/workflow/agents/team-config.md +97 -0
  28. package/templates/claude/.claude/workflow/agents/tester.md +98 -0
  29. package/templates/claude/.claude/workflow/interfaces/phase-contract.md +157 -0
  30. package/templates/claude/CLAUDE.md +50 -0
  31. package/templates/core/.context/_meta/concepts.md +9 -0
  32. package/templates/core/.context/_meta/drift-report.md +16 -0
  33. package/templates/core/.context/_meta/last-sync.json +6 -0
  34. package/templates/core/.context/_meta/schema.md +242 -0
  35. package/templates/core/.context/architecture/api-surface.md +52 -0
  36. package/templates/core/.context/architecture/class-index.md +49 -0
  37. package/templates/core/.context/architecture/data-flow.md +103 -0
  38. package/templates/core/.context/architecture/data-model.md +35 -0
  39. package/templates/core/.context/architecture/decisions/001-template.md +35 -0
  40. package/templates/core/.context/architecture/dependencies.md +35 -0
  41. package/templates/core/.context/architecture/infrastructure.md +42 -0
  42. package/templates/core/.context/architecture/module-graph.md +68 -0
  43. package/templates/core/.context/architecture/overview.md +87 -0
  44. package/templates/core/.context/business/domain-model.md +43 -0
  45. package/templates/core/.context/business/glossary.md +23 -0
  46. package/templates/core/.context/business/overview.md +29 -0
  47. package/templates/core/.context/business/workflows.md +61 -0
  48. package/templates/core/.context/constitution.md +84 -0
  49. package/templates/core/.context/conventions/code-style.md +47 -0
  50. package/templates/core/.context/conventions/error-handling.md +50 -0
  51. package/templates/core/.context/conventions/git.md +46 -0
  52. package/templates/core/.context/conventions/patterns.md +41 -0
  53. package/templates/core/.context/conventions/testing.md +49 -0
  54. package/templates/core/.context/experience/debugging.md +21 -0
  55. package/templates/core/.context/experience/incidents.md +26 -0
  56. package/templates/core/.context/experience/lessons.md +23 -0
  57. package/templates/core/.context/experience/performance.md +29 -0
  58. package/templates/core/.context/index.md +93 -0
  59. package/templates/core/.context/progress/backlog.md +23 -0
  60. package/templates/core/.context/progress/status.md +30 -0
  61. package/templates/core/.context/workflow/artifacts/.gitkeep +0 -0
  62. package/templates/core/.context/workflow/config.md +35 -0
  63. package/templates/core/AGENTS.md +53 -0
  64. package/templates/core/scripts/compact-experience.sh +83 -0
  65. package/templates/core/scripts/detect-drift.sh +388 -0
  66. package/templates/core/scripts/extract-structure.sh +757 -0
  67. package/templates/core/scripts/sync-context.sh +510 -0
  68. package/templates/cursor/.cursor/rules/always.mdc +18 -0
  69. package/templates/cursor/.cursor/rules/backend.mdc +16 -0
  70. package/templates/cursor/.cursor/rules/database.mdc +16 -0
  71. package/templates/cursor/.cursor/rules/frontend.mdc +13 -0
  72. package/templates/cursor/.cursorrules +23 -0
  73. package/templates/github/.github/copilot-instructions.md +15 -0
  74. package/templates/github/.github/workflows/context-drift.yml +73 -0
package/README.md ADDED
@@ -0,0 +1,88 @@
1
+ # context-engineer
2
+
3
+ Structured context management for AI coding agents. Install the `.context/` system into any project with a single command.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ npx context-engineer init
9
+ ```
10
+
11
+ This installs the context engineering system into your current project directory. Then open Claude Code and run:
12
+
13
+ ```
14
+ /bootstrap-context
15
+ ```
16
+
17
+ This analyzes your codebase and populates the context files with your project's specifics.
18
+
19
+ ## What Gets Installed
20
+
21
+ | Component | Contents | Default |
22
+ |-----------|----------|---------|
23
+ | **Core** | `.context/` directory, `scripts/`, `AGENTS.md` | Always |
24
+ | **Claude Code** | `.claude/` (skills, rules, hooks), `CLAUDE.md` | Yes |
25
+ | **Cursor** | `.cursor/rules/`, `.cursorrules` | Optional |
26
+ | **GitHub** | Copilot instructions, CI drift detection workflow | Optional |
27
+
28
+ ## Usage
29
+
30
+ ### Interactive Install
31
+
32
+ ```bash
33
+ npx context-engineer init
34
+ ```
35
+
36
+ Prompts you to select which AI tool integrations to install.
37
+
38
+ ### Preset Install
39
+
40
+ ```bash
41
+ # Core + Claude Code (most common)
42
+ npx context-engineer init --preset claude
43
+
44
+ # Everything
45
+ npx context-engineer init --preset all
46
+
47
+ # Core only (no tool integrations)
48
+ npx context-engineer init --preset core
49
+
50
+ # Core + Cursor
51
+ npx context-engineer init --preset cursor
52
+ ```
53
+
54
+ ### Options
55
+
56
+ ```bash
57
+ npx context-engineer init --force # Overwrite existing files
58
+ npx context-engineer init --dry-run # Preview without writing
59
+ npx context-engineer init --dir ./myapp # Install to a specific directory
60
+ ```
61
+
62
+ ### Update Templates
63
+
64
+ ```bash
65
+ npx context-engineer update # Update to latest templates
66
+ npx context-engineer update --check # Check for updates without applying
67
+ ```
68
+
69
+ ## After Installation
70
+
71
+ 1. **Claude Code**: Run `/bootstrap-context` to populate templates from your codebase
72
+ 2. **Cursor**: The `.cursor/rules/` files activate automatically
73
+ 3. **GitHub Copilot**: The `.github/copilot-instructions.md` activates automatically
74
+ 4. **All tools**: Read `AGENTS.md` for the universal entry point
75
+
76
+ ## How It Works
77
+
78
+ The context system provides AI coding agents with structured project knowledge:
79
+
80
+ - **`.context/constitution.md`** — Project identity, principles, and a route table that tells agents what to load
81
+ - **`.context/architecture/`** — Tech stack, data models, API surface, module graph
82
+ - **`.context/conventions/`** — Code style, patterns, testing, error handling
83
+ - **`.context/experience/`** — Lessons learned, debugging solutions, performance insights
84
+ - **`scripts/`** — Drift detection, context sync, structure extraction
85
+
86
+ ## License
87
+
88
+ MIT
package/bin/cli.mjs ADDED
@@ -0,0 +1,91 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { resolve } from 'path';
4
+ import { readFileSync } from 'fs';
5
+ import { fileURLToPath } from 'url';
6
+ import { dirname, join } from 'path';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ const pkgPath = join(__dirname, '..', 'package.json');
11
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
12
+
13
+ const args = process.argv.slice(2);
14
+ const command = args[0];
15
+
16
+ function parseFlags(args) {
17
+ const flags = {};
18
+ for (let i = 1; i < args.length; i++) {
19
+ const arg = args[i];
20
+ if (arg === '--preset' && args[i + 1]) {
21
+ flags.preset = args[++i];
22
+ } else if (arg === '--dir' && args[i + 1]) {
23
+ flags.dir = resolve(args[++i]);
24
+ } else if (arg === '--force') {
25
+ flags.force = true;
26
+ } else if (arg === '--dry-run') {
27
+ flags.dryRun = true;
28
+ } else if (arg === '--check') {
29
+ flags.check = true;
30
+ }
31
+ }
32
+ return flags;
33
+ }
34
+
35
+ function printHelp() {
36
+ console.log(`
37
+ context-engineer v${pkg.version}
38
+ Structured context management for AI coding agents
39
+
40
+ Usage:
41
+ context-engineer init [options] Install context system into current project
42
+ context-engineer update [options] Update templates to latest version
43
+
44
+ Init options:
45
+ --preset <name> Skip prompts (all, claude, cursor, core)
46
+ --dir <path> Target directory (default: current directory)
47
+ --force Overwrite existing files without asking
48
+ --dry-run Show what would be installed without writing
49
+
50
+ Update options:
51
+ --check Only check for updates, don't apply
52
+ --force Overwrite all files including customized ones
53
+
54
+ Examples:
55
+ npx context-engineer init
56
+ npx context-engineer init --preset claude
57
+ npx context-engineer init --preset all --dry-run
58
+ npx context-engineer update --check
59
+ `);
60
+ }
61
+
62
+ async function main() {
63
+ if (args.includes('--version') || args.includes('-v')) {
64
+ console.log(pkg.version);
65
+ return;
66
+ }
67
+
68
+ if (args.includes('--help') || args.includes('-h') || !command) {
69
+ printHelp();
70
+ return;
71
+ }
72
+
73
+ const flags = parseFlags(args);
74
+
75
+ if (command === 'init') {
76
+ const { runInit } = await import('../lib/init.mjs');
77
+ await runInit(flags);
78
+ } else if (command === 'update') {
79
+ const { runUpdate } = await import('../lib/update.mjs');
80
+ await runUpdate(flags);
81
+ } else {
82
+ console.error(` Unknown command: ${command}\n`);
83
+ printHelp();
84
+ process.exit(1);
85
+ }
86
+ }
87
+
88
+ main().catch((err) => {
89
+ console.error('\n Error:', err.message);
90
+ process.exit(1);
91
+ });
package/lib/copy.mjs ADDED
@@ -0,0 +1,102 @@
1
+ import { readFileSync, writeFileSync, mkdirSync, existsSync, chmodSync, readdirSync, statSync } from 'fs';
2
+ import { join, dirname, relative } from 'path';
3
+ import { createHash } from 'crypto';
4
+
5
+ /**
6
+ * Recursively list all files in a directory (relative paths).
7
+ */
8
+ export function walkDir(dir, base = dir) {
9
+ const results = [];
10
+ for (const entry of readdirSync(dir)) {
11
+ const full = join(dir, entry);
12
+ const stat = statSync(full);
13
+ if (stat.isDirectory()) {
14
+ results.push(...walkDir(full, base));
15
+ } else {
16
+ results.push(relative(base, full));
17
+ }
18
+ }
19
+ return results;
20
+ }
21
+
22
+ /**
23
+ * Compute SHA-256 hash of a file's contents.
24
+ */
25
+ export function fileHash(filePath) {
26
+ const content = readFileSync(filePath);
27
+ return createHash('sha256').update(content).digest('hex');
28
+ }
29
+
30
+ /**
31
+ * Copy a template group to the target directory.
32
+ *
33
+ * @param {string} templateDir - Absolute path to the group's template directory
34
+ * @param {string} targetDir - Absolute path to the target project directory
35
+ * @param {object} options
36
+ * @param {boolean} options.force - Overwrite without asking
37
+ * @param {boolean} options.dryRun - Don't actually write files
38
+ * @param {function} options.onConflict - async (relPath) => 'skip' | 'overwrite'
39
+ * @param {object} options.replacements - key-value pairs to replace in file contents
40
+ * @returns {{ created: string[], skipped: string[], overwritten: string[] }}
41
+ */
42
+ export async function copyTemplateGroup(templateDir, targetDir, options = {}) {
43
+ const { force = false, dryRun = false, onConflict, replacements = {} } = options;
44
+ const report = { created: [], skipped: [], overwritten: [] };
45
+
46
+ if (!existsSync(templateDir)) {
47
+ return report;
48
+ }
49
+
50
+ const files = walkDir(templateDir);
51
+ const isWindows = process.platform === 'win32';
52
+
53
+ for (const relPath of files) {
54
+ const srcPath = join(templateDir, relPath);
55
+ const destPath = join(targetDir, relPath);
56
+
57
+ // Read source file as binary buffer
58
+ let content = readFileSync(srcPath);
59
+
60
+ // Apply replacements (only for text files)
61
+ if (Object.keys(replacements).length > 0) {
62
+ let text = content.toString('utf8');
63
+ for (const [key, value] of Object.entries(replacements)) {
64
+ text = text.split(key).join(value);
65
+ }
66
+ content = Buffer.from(text, 'utf8');
67
+ }
68
+
69
+ const destExists = existsSync(destPath);
70
+
71
+ if (destExists) {
72
+ if (force) {
73
+ // Overwrite
74
+ } else if (onConflict) {
75
+ const action = await onConflict(relPath);
76
+ if (action === 'skip') {
77
+ report.skipped.push(relPath);
78
+ continue;
79
+ }
80
+ } else {
81
+ report.skipped.push(relPath);
82
+ continue;
83
+ }
84
+ report.overwritten.push(relPath);
85
+ } else {
86
+ report.created.push(relPath);
87
+ }
88
+
89
+ if (!dryRun) {
90
+ mkdirSync(dirname(destPath), { recursive: true });
91
+ // Write in binary mode to preserve LF line endings
92
+ writeFileSync(destPath, content);
93
+
94
+ // Set executable permission on scripts (POSIX only)
95
+ if (!isWindows && relPath.startsWith('scripts/') && relPath.endsWith('.sh')) {
96
+ chmodSync(destPath, 0o755);
97
+ }
98
+ }
99
+ }
100
+
101
+ return report;
102
+ }
package/lib/init.mjs ADDED
@@ -0,0 +1,166 @@
1
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
2
+ import { resolve, join } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { dirname } from 'path';
5
+ import { copyTemplateGroup, walkDir, fileHash } from './copy.mjs';
6
+ import { multiSelect, conflictStrategy, askFileConflict } from './prompts.mjs';
7
+
8
+ const __filename = fileURLToPath(import.meta.url);
9
+ const __dirname = dirname(__filename);
10
+ const TEMPLATES_DIR = join(__dirname, '..', 'templates');
11
+ const PKG = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
12
+
13
+ const GROUPS = [
14
+ {
15
+ id: 'core',
16
+ label: 'Core (.context/ + scripts/ + AGENTS.md)',
17
+ checked: true,
18
+ required: true,
19
+ },
20
+ {
21
+ id: 'claude',
22
+ label: 'Claude Code (.claude/ + CLAUDE.md)',
23
+ checked: true,
24
+ required: false,
25
+ },
26
+ {
27
+ id: 'cursor',
28
+ label: 'Cursor (.cursor/ + .cursorrules)',
29
+ checked: false,
30
+ required: false,
31
+ },
32
+ {
33
+ id: 'github',
34
+ label: 'GitHub (Copilot instructions + CI drift check)',
35
+ checked: false,
36
+ required: false,
37
+ },
38
+ ];
39
+
40
+ const PRESETS = {
41
+ all: ['core', 'claude', 'cursor', 'github'],
42
+ claude: ['core', 'claude'],
43
+ cursor: ['core', 'cursor'],
44
+ core: ['core'],
45
+ };
46
+
47
+ export async function runInit(flags) {
48
+ const targetDir = flags.dir || process.cwd();
49
+ const { force = false, dryRun = false } = flags;
50
+
51
+ console.log(`\n context-engineer v${PKG.version}`);
52
+ console.log(' Structured context for AI coding agents\n');
53
+
54
+ // Determine selected groups
55
+ let selectedIds;
56
+ if (flags.preset) {
57
+ selectedIds = PRESETS[flags.preset];
58
+ if (!selectedIds) {
59
+ console.error(` Unknown preset: ${flags.preset}`);
60
+ console.error(` Available: ${Object.keys(PRESETS).join(', ')}`);
61
+ process.exit(1);
62
+ }
63
+ console.log(` Using preset: ${flags.preset}`);
64
+ } else {
65
+ selectedIds = await multiSelect('Which AI tool integrations?', GROUPS);
66
+ }
67
+
68
+ console.log(` Target: ${targetDir}`);
69
+ if (dryRun) console.log(' Mode: dry run (no files will be written)\n');
70
+ else console.log('');
71
+
72
+ // Scan for conflicts across all selected groups
73
+ let totalConflicts = 0;
74
+ const allConflicts = [];
75
+ for (const groupId of selectedIds) {
76
+ const groupDir = join(TEMPLATES_DIR, groupId);
77
+ if (!existsSync(groupDir)) continue;
78
+ for (const relPath of walkDir(groupDir)) {
79
+ if (existsSync(join(targetDir, relPath))) {
80
+ allConflicts.push(relPath);
81
+ totalConflicts++;
82
+ }
83
+ }
84
+ }
85
+
86
+ // Determine conflict strategy
87
+ let strategy = 'skip';
88
+ if (totalConflicts > 0 && !force) {
89
+ strategy = await conflictStrategy(totalConflicts);
90
+ } else if (force) {
91
+ strategy = 'overwrite';
92
+ }
93
+
94
+ // Copy each group
95
+ const totals = { created: [], skipped: [], overwritten: [] };
96
+
97
+ for (const groupId of selectedIds) {
98
+ const groupDir = join(TEMPLATES_DIR, groupId);
99
+
100
+ const onConflict =
101
+ strategy === 'overwrite'
102
+ ? () => 'overwrite'
103
+ : strategy === 'ask'
104
+ ? (relPath) => askFileConflict(relPath)
105
+ : () => 'skip';
106
+
107
+ const report = await copyTemplateGroup(groupDir, targetDir, {
108
+ force: strategy === 'overwrite',
109
+ dryRun,
110
+ onConflict: strategy !== 'overwrite' ? onConflict : undefined,
111
+ replacements: {
112
+ __CE_VERSION__: PKG.version,
113
+ },
114
+ });
115
+
116
+ totals.created.push(...report.created);
117
+ totals.skipped.push(...report.skipped);
118
+ totals.overwritten.push(...report.overwritten);
119
+ }
120
+
121
+ // Save installed checksums for future update diffing
122
+ if (!dryRun) {
123
+ const installedChecksums = {};
124
+ for (const groupId of selectedIds) {
125
+ const groupDir = join(TEMPLATES_DIR, groupId);
126
+ if (!existsSync(groupDir)) continue;
127
+ for (const relPath of walkDir(groupDir)) {
128
+ const destPath = join(targetDir, relPath);
129
+ if (existsSync(destPath)) {
130
+ installedChecksums[relPath] = fileHash(destPath);
131
+ }
132
+ }
133
+ }
134
+ const ceMetaDir = join(targetDir, '.context', '_meta');
135
+ mkdirSync(ceMetaDir, { recursive: true });
136
+ writeFileSync(
137
+ join(ceMetaDir, '.ce-checksums.json'),
138
+ JSON.stringify(installedChecksums, null, 2) + '\n'
139
+ );
140
+ }
141
+
142
+ // Print summary
143
+ const prefix = dryRun ? 'Would install' : 'Installed';
144
+ console.log(` ${prefix}: ${totals.created.length} new file(s)`);
145
+ if (totals.overwritten.length > 0) {
146
+ console.log(` Overwritten: ${totals.overwritten.length} file(s)`);
147
+ }
148
+ if (totals.skipped.length > 0) {
149
+ console.log(` Skipped: ${totals.skipped.length} existing file(s)`);
150
+ }
151
+
152
+ // Next steps
153
+ if (!dryRun) {
154
+ console.log('\n Next steps:');
155
+ if (selectedIds.includes('claude')) {
156
+ console.log(' 1. Open Claude Code in this project');
157
+ console.log(' 2. Run /bootstrap-context to populate templates');
158
+ } else {
159
+ console.log(' 1. Fill in the template placeholders in .context/ files');
160
+ console.log(' 2. Edit AGENTS.md with your project details');
161
+ }
162
+ console.log(` ${selectedIds.includes('claude') ? '3' : '3'}. Commit: git add .context ${selectedIds.includes('claude') ? '.claude ' : ''}AGENTS.md && git commit -m "Add context engineering system"`);
163
+ }
164
+
165
+ console.log('');
166
+ }
@@ -0,0 +1,144 @@
1
+ import { createInterface } from 'readline';
2
+
3
+ function createRL() {
4
+ return createInterface({
5
+ input: process.stdin,
6
+ output: process.stdout,
7
+ });
8
+ }
9
+
10
+ /**
11
+ * Ask a yes/no question. Returns true for yes.
12
+ */
13
+ export async function confirm(message, defaultYes = true) {
14
+ const rl = createRL();
15
+ const suffix = defaultYes ? '[Y/n]' : '[y/N]';
16
+ return new Promise((resolve) => {
17
+ rl.question(` ${message} ${suffix} `, (answer) => {
18
+ rl.close();
19
+ const a = answer.trim().toLowerCase();
20
+ if (a === '') resolve(defaultYes);
21
+ else resolve(a === 'y' || a === 'yes');
22
+ });
23
+ });
24
+ }
25
+
26
+ /**
27
+ * Multi-select checkbox prompt.
28
+ * Returns array of selected item ids.
29
+ *
30
+ * @param {string} message
31
+ * @param {{ id: string, label: string, checked: boolean, required?: boolean }[]} items
32
+ */
33
+ export async function multiSelect(message, items) {
34
+ // In non-TTY environments (piped input), return defaults
35
+ if (!process.stdin.isTTY) {
36
+ return items.filter((i) => i.checked).map((i) => i.id);
37
+ }
38
+
39
+ const state = items.map((item) => ({ ...item }));
40
+ let cursor = 0;
41
+
42
+ function render() {
43
+ // Move cursor up to re-render (except first render)
44
+ const lines = [];
45
+ lines.push(` ${message} (space=toggle, enter=confirm)\n`);
46
+ for (let i = 0; i < state.length; i++) {
47
+ const item = state[i];
48
+ const pointer = i === cursor ? '>' : ' ';
49
+ const check = item.checked ? 'x' : ' ';
50
+ const suffix = item.required ? ' (required)' : '';
51
+ lines.push(` ${pointer} [${check}] ${item.label}${suffix}`);
52
+ }
53
+ return lines.join('\n');
54
+ }
55
+
56
+ return new Promise((resolve) => {
57
+ process.stdout.write(render());
58
+
59
+ const stdin = process.stdin;
60
+ stdin.setRawMode(true);
61
+ stdin.resume();
62
+ stdin.setEncoding('utf8');
63
+
64
+ const onKey = (key) => {
65
+ // Ctrl+C
66
+ if (key === '\u0003') {
67
+ stdin.setRawMode(false);
68
+ stdin.removeListener('data', onKey);
69
+ process.stdout.write('\n');
70
+ process.exit(0);
71
+ }
72
+
73
+ // Enter
74
+ if (key === '\r' || key === '\n') {
75
+ stdin.setRawMode(false);
76
+ stdin.removeListener('data', onKey);
77
+ process.stdout.write('\n\n');
78
+ resolve(state.filter((i) => i.checked).map((i) => i.id));
79
+ return;
80
+ }
81
+
82
+ // Space - toggle
83
+ if (key === ' ') {
84
+ if (!state[cursor].required) {
85
+ state[cursor].checked = !state[cursor].checked;
86
+ }
87
+ }
88
+
89
+ // Arrow up / k
90
+ if (key === '\u001b[A' || key === 'k') {
91
+ cursor = (cursor - 1 + state.length) % state.length;
92
+ }
93
+
94
+ // Arrow down / j
95
+ if (key === '\u001b[B' || key === 'j') {
96
+ cursor = (cursor + 1) % state.length;
97
+ }
98
+
99
+ // Re-render
100
+ // Clear previous render
101
+ const lineCount = state.length + 1;
102
+ process.stdout.write(`\u001b[${lineCount}A\u001b[0J`);
103
+ process.stdout.write(render());
104
+ };
105
+
106
+ stdin.on('data', onKey);
107
+ });
108
+ }
109
+
110
+ /**
111
+ * Ask how to handle file conflicts.
112
+ * Returns 'skip' | 'overwrite' | 'ask'
113
+ */
114
+ export async function conflictStrategy(conflictCount) {
115
+ const rl = createRL();
116
+ return new Promise((resolve) => {
117
+ console.log(`\n ${conflictCount} existing file(s) detected. How to handle?`);
118
+ console.log(' 1) Skip existing files (safe default)');
119
+ console.log(' 2) Overwrite all');
120
+ console.log(' 3) Ask for each file');
121
+ rl.question(' Choice [1]: ', (answer) => {
122
+ rl.close();
123
+ const a = answer.trim();
124
+ if (a === '2') resolve('overwrite');
125
+ else if (a === '3') resolve('ask');
126
+ else resolve('skip');
127
+ });
128
+ });
129
+ }
130
+
131
+ /**
132
+ * Ask about a single file conflict.
133
+ * Returns 'skip' | 'overwrite'
134
+ */
135
+ export async function askFileConflict(relPath) {
136
+ const rl = createRL();
137
+ return new Promise((resolve) => {
138
+ rl.question(` Overwrite ${relPath}? [y/N] `, (answer) => {
139
+ rl.close();
140
+ const a = answer.trim().toLowerCase();
141
+ resolve(a === 'y' || a === 'yes' ? 'overwrite' : 'skip');
142
+ });
143
+ });
144
+ }