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.
- package/README.md +88 -0
- package/bin/cli.mjs +91 -0
- package/lib/copy.mjs +102 -0
- package/lib/init.mjs +166 -0
- package/lib/prompts.mjs +144 -0
- package/lib/update.mjs +198 -0
- package/package.json +35 -0
- package/templates/checksums.json +68 -0
- package/templates/claude/.claude/rules/context-maintenance.md +38 -0
- package/templates/claude/.claude/rules/experience-capture.md +46 -0
- package/templates/claude/.claude/settings.project.json +22 -0
- package/templates/claude/.claude/skills/bootstrap/SKILL.md +223 -0
- package/templates/claude/.claude/skills/dev/SKILL.md +119 -0
- package/templates/claude/.claude/skills/dev-capture/SKILL.md +111 -0
- package/templates/claude/.claude/skills/dev-commit/SKILL.md +90 -0
- package/templates/claude/.claude/skills/dev-decompose/SKILL.md +113 -0
- package/templates/claude/.claude/skills/dev-deps/SKILL.md +108 -0
- package/templates/claude/.claude/skills/dev-execute/SKILL.md +196 -0
- package/templates/claude/.claude/skills/dev-prd/SKILL.md +100 -0
- package/templates/claude/.claude/skills/dev-quality/SKILL.md +109 -0
- package/templates/claude/.claude/skills/dev-requirements/SKILL.md +75 -0
- package/templates/claude/.claude/skills/review-context/SKILL.md +120 -0
- package/templates/claude/.claude/skills/sync/SKILL.md +107 -0
- package/templates/claude/.claude/skills/update-context/SKILL.md +105 -0
- package/templates/claude/.claude/workflow/agents/implementer.md +65 -0
- package/templates/claude/.claude/workflow/agents/reviewer.md +96 -0
- package/templates/claude/.claude/workflow/agents/team-config.md +97 -0
- package/templates/claude/.claude/workflow/agents/tester.md +98 -0
- package/templates/claude/.claude/workflow/interfaces/phase-contract.md +157 -0
- package/templates/claude/CLAUDE.md +50 -0
- package/templates/core/.context/_meta/concepts.md +9 -0
- package/templates/core/.context/_meta/drift-report.md +16 -0
- package/templates/core/.context/_meta/last-sync.json +6 -0
- package/templates/core/.context/_meta/schema.md +242 -0
- package/templates/core/.context/architecture/api-surface.md +52 -0
- package/templates/core/.context/architecture/class-index.md +49 -0
- package/templates/core/.context/architecture/data-flow.md +103 -0
- package/templates/core/.context/architecture/data-model.md +35 -0
- package/templates/core/.context/architecture/decisions/001-template.md +35 -0
- package/templates/core/.context/architecture/dependencies.md +35 -0
- package/templates/core/.context/architecture/infrastructure.md +42 -0
- package/templates/core/.context/architecture/module-graph.md +68 -0
- package/templates/core/.context/architecture/overview.md +87 -0
- package/templates/core/.context/business/domain-model.md +43 -0
- package/templates/core/.context/business/glossary.md +23 -0
- package/templates/core/.context/business/overview.md +29 -0
- package/templates/core/.context/business/workflows.md +61 -0
- package/templates/core/.context/constitution.md +84 -0
- package/templates/core/.context/conventions/code-style.md +47 -0
- package/templates/core/.context/conventions/error-handling.md +50 -0
- package/templates/core/.context/conventions/git.md +46 -0
- package/templates/core/.context/conventions/patterns.md +41 -0
- package/templates/core/.context/conventions/testing.md +49 -0
- package/templates/core/.context/experience/debugging.md +21 -0
- package/templates/core/.context/experience/incidents.md +26 -0
- package/templates/core/.context/experience/lessons.md +23 -0
- package/templates/core/.context/experience/performance.md +29 -0
- package/templates/core/.context/index.md +93 -0
- package/templates/core/.context/progress/backlog.md +23 -0
- package/templates/core/.context/progress/status.md +30 -0
- package/templates/core/.context/workflow/artifacts/.gitkeep +0 -0
- package/templates/core/.context/workflow/config.md +35 -0
- package/templates/core/AGENTS.md +53 -0
- package/templates/core/scripts/compact-experience.sh +83 -0
- package/templates/core/scripts/detect-drift.sh +388 -0
- package/templates/core/scripts/extract-structure.sh +757 -0
- package/templates/core/scripts/sync-context.sh +510 -0
- package/templates/cursor/.cursor/rules/always.mdc +18 -0
- package/templates/cursor/.cursor/rules/backend.mdc +16 -0
- package/templates/cursor/.cursor/rules/database.mdc +16 -0
- package/templates/cursor/.cursor/rules/frontend.mdc +13 -0
- package/templates/cursor/.cursorrules +23 -0
- package/templates/github/.github/copilot-instructions.md +15 -0
- 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
|
+
}
|
package/lib/prompts.mjs
ADDED
|
@@ -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
|
+
}
|