@shirayner/ace 0.1.0-snapshot.1
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/bin/ace.js +39 -0
- package/package.json +42 -0
- package/src/commands/doctor.js +86 -0
- package/src/commands/init.js +98 -0
- package/src/commands/list.js +67 -0
- package/src/core/constants.js +106 -0
- package/src/core/installer.js +206 -0
- package/src/core/merger.js +103 -0
- package/templates/CLAUDE.md +16 -0
- package/templates/commands/report.md +63 -0
- package/templates/hookify/hookify.block-dangerous-ops.local.md +16 -0
- package/templates/hookify/hookify.protect-secrets.local.md +17 -0
- package/templates/hookify/hookify.require-verification.local.md +13 -0
- package/templates/hooks/java-compile-check.sh +106 -0
- package/templates/memory/MEMORY.md +4 -0
- package/templates/memory/roles/backend.md +11 -0
- package/templates/memory/roles/client.md +11 -0
- package/templates/memory/roles/frontend.md +11 -0
- package/templates/memory/roles/fullstack.md +11 -0
- package/templates/rules/clean-code.md +33 -0
- package/templates/rules/code-quality.md +74 -0
- package/templates/rules/context-hygiene.md +29 -0
- package/templates/rules/memory-policy.md +30 -0
- package/templates/rules/reporting.md +9 -0
- package/templates/rules/task-recovery.md +13 -0
- package/templates/rules/thinking.md +19 -0
- package/templates/settings.json +11 -0
- package/templates/skills/auto-goal/SKILL.md +188 -0
- package/templates/skills/coding/SKILL.md +251 -0
- package/templates/skills/coding/references/code-review-guide.md +137 -0
- package/templates/skills/coding/references/code-smells.md +201 -0
- package/templates/skills/coding/references/implement-guide.md +123 -0
- package/templates/skills/coding/references/unit-test-guide.md +211 -0
- package/templates/skills/skill-creator/LICENSE.txt +202 -0
- package/templates/skills/skill-creator/SKILL.md +479 -0
- package/templates/skills/skill-creator/agents/analyzer.md +274 -0
- package/templates/skills/skill-creator/agents/comparator.md +202 -0
- package/templates/skills/skill-creator/agents/grader.md +223 -0
- package/templates/skills/skill-creator/assets/eval_review.html +146 -0
- package/templates/skills/skill-creator/eval-viewer/generate_review.py +471 -0
- package/templates/skills/skill-creator/eval-viewer/viewer.html +1325 -0
- package/templates/skills/skill-creator/references/schemas.md +430 -0
- package/templates/skills/skill-creator/scripts/__init__.py +0 -0
- package/templates/skills/skill-creator/scripts/aggregate_benchmark.py +401 -0
- package/templates/skills/skill-creator/scripts/generate_report.py +326 -0
- package/templates/skills/skill-creator/scripts/improve_description.py +248 -0
- package/templates/skills/skill-creator/scripts/package_skill.py +136 -0
- package/templates/skills/skill-creator/scripts/quick_validate.py +103 -0
- package/templates/skills/skill-creator/scripts/run_eval.py +310 -0
- package/templates/skills/skill-creator/scripts/run_loop.py +332 -0
- package/templates/skills/skill-creator/scripts/utils.py +47 -0
- package/templates/skills/skill-optimize/SKILL.md +287 -0
- package/templates/skills/skill-optimize/references/.claude/settings.local.json +7 -0
- package/templates/skills/skill-optimize/references/anthropic-design-philosophy.md +250 -0
- package/templates/skills/skill-optimize/references/auto-goal-optimization-directions.md +130 -0
- package/templates/skills/skill-optimize/references/cross-disciplinary-insights.md +211 -0
- package/templates/skills/skill-optimize/references/quality-checklist.md +170 -0
- package/templates/skills/skill-optimize/references/theory-foundations.md +201 -0
package/bin/ace.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import { Command } from 'commander';
|
|
5
|
+
import chalk from 'chalk';
|
|
6
|
+
import { initCommand } from '../src/commands/init.js';
|
|
7
|
+
import { doctorCommand } from '../src/commands/doctor.js';
|
|
8
|
+
import { listCommand } from '../src/commands/list.js';
|
|
9
|
+
|
|
10
|
+
const require = createRequire(import.meta.url);
|
|
11
|
+
const pkg = require('../package.json');
|
|
12
|
+
|
|
13
|
+
const program = new Command();
|
|
14
|
+
|
|
15
|
+
program
|
|
16
|
+
.name('ace')
|
|
17
|
+
.description('AI Coding Environment - One command to set up your Claude Code harness')
|
|
18
|
+
.version(pkg.version);
|
|
19
|
+
|
|
20
|
+
program
|
|
21
|
+
.command('init')
|
|
22
|
+
.description('Initialize AI coding environment')
|
|
23
|
+
.option('-p, --preset <name>', 'Installation preset: full, minimal, safe', 'full')
|
|
24
|
+
.option('-f, --force', 'Overwrite existing files', false)
|
|
25
|
+
.option('--dry-run', 'Show what would be done without making changes', false)
|
|
26
|
+
.option('--no-interaction', 'Skip interactive prompts, use defaults', false)
|
|
27
|
+
.action(initCommand);
|
|
28
|
+
|
|
29
|
+
program
|
|
30
|
+
.command('doctor')
|
|
31
|
+
.description('Verify installation integrity')
|
|
32
|
+
.action(doctorCommand);
|
|
33
|
+
|
|
34
|
+
program
|
|
35
|
+
.command('list')
|
|
36
|
+
.description('List installed components and their status')
|
|
37
|
+
.action(listCommand);
|
|
38
|
+
|
|
39
|
+
program.parse();
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@shirayner/ace",
|
|
3
|
+
"version": "0.1.0-snapshot.1",
|
|
4
|
+
"description": "AI Coding Environment - One command to set up your Claude Code harness",
|
|
5
|
+
"bin": {
|
|
6
|
+
"ace": "./bin/ace.js"
|
|
7
|
+
},
|
|
8
|
+
"main": "src/index.js",
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "node --test tests/",
|
|
11
|
+
"lint": "eslint src/"
|
|
12
|
+
},
|
|
13
|
+
"keywords": [
|
|
14
|
+
"claude-code",
|
|
15
|
+
"ai",
|
|
16
|
+
"coding",
|
|
17
|
+
"environment",
|
|
18
|
+
"harness",
|
|
19
|
+
"cli"
|
|
20
|
+
],
|
|
21
|
+
"author": "",
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"engines": {
|
|
24
|
+
"node": ">=18.0.0"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"commander": "^12.0.0",
|
|
29
|
+
"chalk": "^5.3.0",
|
|
30
|
+
"inquirer": "^9.2.0",
|
|
31
|
+
"fs-extra": "^11.2.0",
|
|
32
|
+
"deepmerge": "^4.3.1",
|
|
33
|
+
"ora": "^8.0.0"
|
|
34
|
+
},
|
|
35
|
+
"files": [
|
|
36
|
+
"bin/",
|
|
37
|
+
"src/",
|
|
38
|
+
"templates/",
|
|
39
|
+
"README.md",
|
|
40
|
+
"LICENSE"
|
|
41
|
+
]
|
|
42
|
+
}
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { CLAUDE_DIR, COMPONENTS } from '../core/constants.js';
|
|
5
|
+
|
|
6
|
+
export async function doctorCommand() {
|
|
7
|
+
console.log(chalk.bold('\n ace doctor — verifying installation\n'));
|
|
8
|
+
|
|
9
|
+
const checks = [];
|
|
10
|
+
|
|
11
|
+
// 1. Check ~/.claude/ exists
|
|
12
|
+
checks.push(await check('~/.claude/ directory', fs.pathExists(CLAUDE_DIR)));
|
|
13
|
+
|
|
14
|
+
// 2. Check core files
|
|
15
|
+
checks.push(await check('CLAUDE.md', fs.pathExists(path.join(CLAUDE_DIR, 'CLAUDE.md'))));
|
|
16
|
+
checks.push(await check('settings.json', fs.pathExists(path.join(CLAUDE_DIR, 'settings.json'))));
|
|
17
|
+
|
|
18
|
+
// 3. Check rules
|
|
19
|
+
const ruleFiles = COMPONENTS.rules.files;
|
|
20
|
+
for (const file of ruleFiles) {
|
|
21
|
+
checks.push(await check(`rules/${path.basename(file.dest)}`, fs.pathExists(path.join(CLAUDE_DIR, file.dest))));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 4. Check skills
|
|
25
|
+
const skillDirs = COMPONENTS.skills.directories;
|
|
26
|
+
for (const dir of skillDirs) {
|
|
27
|
+
const skillMd = path.join(CLAUDE_DIR, dir, 'SKILL.md');
|
|
28
|
+
checks.push(await check(dir, fs.pathExists(skillMd)));
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 5. Check memory
|
|
32
|
+
checks.push(await check('memory/MEMORY.md', fs.pathExists(path.join(CLAUDE_DIR, 'memory', 'MEMORY.md'))));
|
|
33
|
+
|
|
34
|
+
// 6. Validate settings.json structure
|
|
35
|
+
try {
|
|
36
|
+
const settings = await fs.readJson(path.join(CLAUDE_DIR, 'settings.json'));
|
|
37
|
+
checks.push({ name: 'settings.json valid JSON', ok: true });
|
|
38
|
+
|
|
39
|
+
const hasHooks = settings?.permissions?.hooks;
|
|
40
|
+
checks.push({ name: 'settings.json has hooks config', ok: !!hasHooks });
|
|
41
|
+
|
|
42
|
+
const hasMemoryDir = settings?.autoMemoryDirectory;
|
|
43
|
+
checks.push({ name: 'settings.json has autoMemoryDirectory', ok: !!hasMemoryDir });
|
|
44
|
+
} catch {
|
|
45
|
+
checks.push({ name: 'settings.json parseable', ok: false });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// 7. Validate CLAUDE.md @references
|
|
49
|
+
try {
|
|
50
|
+
const claudeMd = await fs.readFile(path.join(CLAUDE_DIR, 'CLAUDE.md'), 'utf-8');
|
|
51
|
+
const refs = claudeMd.match(/@~?\/?\.?claude\/[^\s)]+/g) || [];
|
|
52
|
+
for (const ref of refs) {
|
|
53
|
+
const refPath = ref.replace(/^@/, '').replace(/^~/, process.env.HOME || process.env.USERPROFILE);
|
|
54
|
+
checks.push(await check(`@ref: ${path.basename(refPath)}`, fs.pathExists(refPath)));
|
|
55
|
+
}
|
|
56
|
+
} catch {
|
|
57
|
+
checks.push({ name: 'CLAUDE.md readable', ok: false });
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Summary
|
|
61
|
+
const passed = checks.filter(c => c.ok).length;
|
|
62
|
+
const failed = checks.filter(c => !c.ok).length;
|
|
63
|
+
|
|
64
|
+
console.log();
|
|
65
|
+
checks.forEach(c => {
|
|
66
|
+
const icon = c.ok ? chalk.green(' pass') : chalk.red(' FAIL');
|
|
67
|
+
console.log(` ${icon} ${c.name}`);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
console.log(chalk.bold(`\n ${passed} passed, ${failed} failed\n`));
|
|
71
|
+
|
|
72
|
+
if (failed > 0) {
|
|
73
|
+
console.log(chalk.yellow(' Run `ace init` to fix missing components.\n'));
|
|
74
|
+
} else {
|
|
75
|
+
console.log(chalk.green(' All checks passed. Environment is healthy.\n'));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
async function check(name, promise) {
|
|
80
|
+
try {
|
|
81
|
+
const ok = await promise;
|
|
82
|
+
return { name, ok: !!ok };
|
|
83
|
+
} catch {
|
|
84
|
+
return { name, ok: false };
|
|
85
|
+
}
|
|
86
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
import inquirer from 'inquirer';
|
|
3
|
+
import { PRESETS, ROLES } from '../core/constants.js';
|
|
4
|
+
import { Installer } from '../core/installer.js';
|
|
5
|
+
|
|
6
|
+
export async function initCommand(options) {
|
|
7
|
+
console.log(chalk.bold('\n ace - AI Coding Environment\n'));
|
|
8
|
+
|
|
9
|
+
let role = 'fullstack';
|
|
10
|
+
let preset = options.preset;
|
|
11
|
+
|
|
12
|
+
// Interactive mode
|
|
13
|
+
if (options.interaction !== false) {
|
|
14
|
+
const answers = await inquirer.prompt([
|
|
15
|
+
{
|
|
16
|
+
type: 'list',
|
|
17
|
+
name: 'role',
|
|
18
|
+
message: 'What is your primary role?',
|
|
19
|
+
choices: Object.entries(ROLES).map(([key, val]) => ({
|
|
20
|
+
name: `${val.label} — ${val.description}`,
|
|
21
|
+
value: key,
|
|
22
|
+
})),
|
|
23
|
+
default: 'fullstack',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
type: 'list',
|
|
27
|
+
name: 'preset',
|
|
28
|
+
message: 'Installation scope?',
|
|
29
|
+
choices: [
|
|
30
|
+
{ name: 'Full — All components (rules, skills, hooks, safety guards, memory)', value: 'full' },
|
|
31
|
+
{ name: 'Safe — Core + rules + skills + safety guards + memory', value: 'safe' },
|
|
32
|
+
{ name: 'Minimal — Core + rules + skills only', value: 'minimal' },
|
|
33
|
+
],
|
|
34
|
+
default: 'full',
|
|
35
|
+
},
|
|
36
|
+
]);
|
|
37
|
+
role = answers.role;
|
|
38
|
+
preset = answers.preset;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const components = PRESETS[preset];
|
|
42
|
+
if (!components) {
|
|
43
|
+
console.error(chalk.red(`Unknown preset: ${preset}. Available: ${Object.keys(PRESETS).join(', ')}`));
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
console.log(chalk.dim(`\n Role: ${ROLES[role].label}`));
|
|
48
|
+
console.log(chalk.dim(` Preset: ${preset}`));
|
|
49
|
+
console.log(chalk.dim(` Components: ${components.join(', ')}`));
|
|
50
|
+
if (options.force) console.log(chalk.yellow(' Force mode: existing files will be overwritten'));
|
|
51
|
+
if (options.dryRun) console.log(chalk.cyan(' Dry-run mode: no changes will be made'));
|
|
52
|
+
console.log();
|
|
53
|
+
|
|
54
|
+
const installer = new Installer({
|
|
55
|
+
force: options.force,
|
|
56
|
+
dryRun: options.dryRun,
|
|
57
|
+
role,
|
|
58
|
+
components,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
const results = installer.run();
|
|
62
|
+
|
|
63
|
+
// Wait for async
|
|
64
|
+
const { installed, skipped, merged, errors } = await results;
|
|
65
|
+
|
|
66
|
+
// Summary
|
|
67
|
+
console.log(chalk.bold('\n Installation Summary\n'));
|
|
68
|
+
|
|
69
|
+
if (installed.length > 0) {
|
|
70
|
+
console.log(chalk.green(` Installed (${installed.length}):`));
|
|
71
|
+
installed.forEach(f => console.log(chalk.green(` + ${f}`)));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (merged.length > 0) {
|
|
75
|
+
console.log(chalk.blue(` Merged (${merged.length}):`));
|
|
76
|
+
merged.forEach(m => {
|
|
77
|
+
const detail = m.added ? ` (added ${m.added.length} refs)` : '';
|
|
78
|
+
console.log(chalk.blue(` ~ ${m.file}${detail}`));
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (skipped.length > 0) {
|
|
83
|
+
console.log(chalk.yellow(` Skipped (${skipped.length}) — already exist:`));
|
|
84
|
+
skipped.forEach(f => console.log(chalk.yellow(` - ${f}`)));
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (errors.length > 0) {
|
|
88
|
+
console.log(chalk.red(` Errors (${errors.length}):`));
|
|
89
|
+
errors.forEach(e => console.log(chalk.red(` ! ${e.file || e.component}: ${e.error}`)));
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (errors.length === 0) {
|
|
93
|
+
console.log(chalk.green.bold('\n Done! Your AI coding environment is ready.\n'));
|
|
94
|
+
console.log(chalk.dim(' Run `ace doctor` to verify the installation.'));
|
|
95
|
+
} else {
|
|
96
|
+
console.log(chalk.yellow('\n Completed with errors. Run `ace doctor` to diagnose.\n'));
|
|
97
|
+
}
|
|
98
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { CLAUDE_DIR, COMPONENTS } from '../core/constants.js';
|
|
5
|
+
|
|
6
|
+
export async function listCommand() {
|
|
7
|
+
console.log(chalk.bold('\n ace list — installed components\n'));
|
|
8
|
+
|
|
9
|
+
for (const [name, component] of Object.entries(COMPONENTS)) {
|
|
10
|
+
const status = await getComponentStatus(component);
|
|
11
|
+
const icon = status === 'installed' ? chalk.green('installed')
|
|
12
|
+
: status === 'partial' ? chalk.yellow(' partial')
|
|
13
|
+
: chalk.dim(' missing');
|
|
14
|
+
|
|
15
|
+
console.log(` ${icon} ${name} — ${component.description}`);
|
|
16
|
+
|
|
17
|
+
// Show details for partial status
|
|
18
|
+
if (status === 'partial') {
|
|
19
|
+
const details = await getComponentDetails(component);
|
|
20
|
+
details.missing.forEach(f => console.log(chalk.red(` missing: ${f}`)));
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function getComponentStatus(component) {
|
|
28
|
+
const allPaths = [];
|
|
29
|
+
|
|
30
|
+
if (component.files) {
|
|
31
|
+
allPaths.push(...component.files.map(f => path.join(CLAUDE_DIR, f.dest)));
|
|
32
|
+
}
|
|
33
|
+
if (component.directories) {
|
|
34
|
+
allPaths.push(...component.directories.map(d => path.join(CLAUDE_DIR, d)));
|
|
35
|
+
}
|
|
36
|
+
if (component.conditional) {
|
|
37
|
+
allPaths.push(...component.conditional.map(f => path.join(CLAUDE_DIR, f.dest)));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (allPaths.length === 0) return 'installed';
|
|
41
|
+
|
|
42
|
+
const checks = await Promise.all(allPaths.map(p => fs.pathExists(p)));
|
|
43
|
+
const existCount = checks.filter(Boolean).length;
|
|
44
|
+
|
|
45
|
+
if (existCount === allPaths.length) return 'installed';
|
|
46
|
+
if (existCount > 0) return 'partial';
|
|
47
|
+
return 'missing';
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function getComponentDetails(component) {
|
|
51
|
+
const missing = [];
|
|
52
|
+
const installed = [];
|
|
53
|
+
|
|
54
|
+
const allFiles = [
|
|
55
|
+
...(component.files || []).map(f => f.dest),
|
|
56
|
+
...(component.directories || []),
|
|
57
|
+
...(component.conditional || []).map(f => f.dest),
|
|
58
|
+
];
|
|
59
|
+
|
|
60
|
+
for (const file of allFiles) {
|
|
61
|
+
const exists = await fs.pathExists(path.join(CLAUDE_DIR, file));
|
|
62
|
+
if (exists) installed.push(file);
|
|
63
|
+
else missing.push(file);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { missing, installed };
|
|
67
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import os from 'os';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
|
|
5
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
|
|
7
|
+
export const CLAUDE_DIR = path.join(os.homedir(), '.claude');
|
|
8
|
+
export const TEMPLATES_DIR = path.join(__dirname, '..', '..', 'templates');
|
|
9
|
+
|
|
10
|
+
export const PRESETS = {
|
|
11
|
+
full: ['core', 'rules', 'skills', 'hooks', 'hookify', 'memory', 'commands'],
|
|
12
|
+
minimal: ['core', 'rules', 'skills'],
|
|
13
|
+
safe: ['core', 'rules', 'skills', 'hookify', 'memory'],
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export const ROLES = {
|
|
17
|
+
backend: {
|
|
18
|
+
label: 'Backend Developer',
|
|
19
|
+
description: 'Java/Go/Python backend, microservices, APIs',
|
|
20
|
+
primaryLang: 'Java',
|
|
21
|
+
hooks: ['java-compile-check.sh'],
|
|
22
|
+
},
|
|
23
|
+
frontend: {
|
|
24
|
+
label: 'Frontend Developer',
|
|
25
|
+
description: 'React/Vue/TypeScript, Web UI, SPA/SSR',
|
|
26
|
+
primaryLang: 'TypeScript',
|
|
27
|
+
hooks: [],
|
|
28
|
+
},
|
|
29
|
+
client: {
|
|
30
|
+
label: 'Client Developer',
|
|
31
|
+
description: 'iOS/Android/Flutter, mobile apps',
|
|
32
|
+
primaryLang: 'Kotlin/Swift',
|
|
33
|
+
hooks: [],
|
|
34
|
+
},
|
|
35
|
+
fullstack: {
|
|
36
|
+
label: 'Fullstack Developer',
|
|
37
|
+
description: 'Full-stack development, frontend + backend',
|
|
38
|
+
primaryLang: 'TypeScript + Java',
|
|
39
|
+
hooks: ['java-compile-check.sh'],
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const COMPONENTS = {
|
|
44
|
+
core: {
|
|
45
|
+
description: 'Core config (CLAUDE.md + settings.json)',
|
|
46
|
+
required: true,
|
|
47
|
+
files: [
|
|
48
|
+
{ src: 'CLAUDE.md', dest: 'CLAUDE.md', merge: 'claude-md' },
|
|
49
|
+
{ src: 'settings.json', dest: 'settings.json', merge: 'settings-json' },
|
|
50
|
+
],
|
|
51
|
+
},
|
|
52
|
+
rules: {
|
|
53
|
+
description: 'Cognitive & code quality rules',
|
|
54
|
+
required: true,
|
|
55
|
+
files: [
|
|
56
|
+
{ src: 'rules/thinking.md', dest: 'rules/thinking.md' },
|
|
57
|
+
{ src: 'rules/clean-code.md', dest: 'rules/clean-code.md' },
|
|
58
|
+
{ src: 'rules/code-quality.md', dest: 'rules/code-quality.md' },
|
|
59
|
+
{ src: 'rules/reporting.md', dest: 'rules/reporting.md' },
|
|
60
|
+
{ src: 'rules/task-recovery.md', dest: 'rules/task-recovery.md' },
|
|
61
|
+
{ src: 'rules/context-hygiene.md', dest: 'rules/context-hygiene.md' },
|
|
62
|
+
{ src: 'rules/memory-policy.md', dest: 'rules/memory-policy.md' },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
skills: {
|
|
66
|
+
description: 'AI skills (auto-goal, coding, skill-creator, skill-optimize)',
|
|
67
|
+
required: true,
|
|
68
|
+
directories: [
|
|
69
|
+
'skills/auto-goal',
|
|
70
|
+
'skills/coding',
|
|
71
|
+
'skills/skill-creator',
|
|
72
|
+
'skills/skill-optimize',
|
|
73
|
+
],
|
|
74
|
+
},
|
|
75
|
+
hooks: {
|
|
76
|
+
description: 'Hook scripts (optional, role-dependent)',
|
|
77
|
+
required: false,
|
|
78
|
+
conditional: [
|
|
79
|
+
{ src: 'hooks/java-compile-check.sh', dest: 'hooks/java-compile-check.sh', roles: ['backend', 'fullstack'] },
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
hookify: {
|
|
83
|
+
description: 'Safety guard rules (block dangerous ops, protect secrets, require verification)',
|
|
84
|
+
required: false,
|
|
85
|
+
files: [
|
|
86
|
+
{ src: 'hookify/hookify.block-dangerous-ops.local.md', dest: 'hookify.block-dangerous-ops.local.md' },
|
|
87
|
+
{ src: 'hookify/hookify.protect-secrets.local.md', dest: 'hookify.protect-secrets.local.md' },
|
|
88
|
+
{ src: 'hookify/hookify.require-verification.local.md', dest: 'hookify.require-verification.local.md' },
|
|
89
|
+
],
|
|
90
|
+
},
|
|
91
|
+
memory: {
|
|
92
|
+
description: 'Global memory templates',
|
|
93
|
+
required: false,
|
|
94
|
+
files: [
|
|
95
|
+
{ src: 'memory/MEMORY.md', dest: 'memory/MEMORY.md', merge: 'skip-existing' },
|
|
96
|
+
],
|
|
97
|
+
roleTemplates: true,
|
|
98
|
+
},
|
|
99
|
+
commands: {
|
|
100
|
+
description: 'Custom commands (report)',
|
|
101
|
+
required: false,
|
|
102
|
+
files: [
|
|
103
|
+
{ src: 'commands/report.md', dest: 'commands/report.md' },
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
};
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import ora from 'ora';
|
|
5
|
+
import { CLAUDE_DIR, TEMPLATES_DIR, COMPONENTS } from './constants.js';
|
|
6
|
+
import { mergeClaudeMd, mergeSettingsJson, conflictCheck, backupFile } from './merger.js';
|
|
7
|
+
|
|
8
|
+
export class Installer {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.targetDir = CLAUDE_DIR;
|
|
11
|
+
this.templatesDir = TEMPLATES_DIR;
|
|
12
|
+
this.force = options.force || false;
|
|
13
|
+
this.dryRun = options.dryRun || false;
|
|
14
|
+
this.role = options.role || 'fullstack';
|
|
15
|
+
this.components = options.components || [];
|
|
16
|
+
this.results = { installed: [], skipped: [], merged: [], errors: [] };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async run() {
|
|
20
|
+
if (!this.dryRun) {
|
|
21
|
+
await fs.ensureDir(this.targetDir);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
for (const componentName of this.components) {
|
|
25
|
+
const component = COMPONENTS[componentName];
|
|
26
|
+
if (!component) continue;
|
|
27
|
+
|
|
28
|
+
const spinner = ora(`Installing ${componentName}...`).start();
|
|
29
|
+
|
|
30
|
+
try {
|
|
31
|
+
await this.installComponent(componentName, component);
|
|
32
|
+
spinner.succeed(`${componentName} installed`);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
spinner.fail(`${componentName} failed: ${err.message}`);
|
|
35
|
+
this.results.errors.push({ component: componentName, error: err.message });
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return this.results;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async installComponent(name, component) {
|
|
43
|
+
// Install regular files
|
|
44
|
+
if (component.files) {
|
|
45
|
+
for (const file of component.files) {
|
|
46
|
+
await this.installFile(file);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Install conditional files (role-dependent)
|
|
51
|
+
if (component.conditional) {
|
|
52
|
+
for (const file of component.conditional) {
|
|
53
|
+
if (file.roles && file.roles.includes(this.role)) {
|
|
54
|
+
await this.installFile(file);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Install directories (e.g., skills)
|
|
60
|
+
if (component.directories) {
|
|
61
|
+
for (const dir of component.directories) {
|
|
62
|
+
await this.installDirectory(dir);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Install role-specific templates (e.g., memory/user_profile.md)
|
|
67
|
+
if (component.roleTemplates) {
|
|
68
|
+
await this.installRoleTemplate();
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async installFile(fileSpec) {
|
|
73
|
+
const srcPath = path.join(this.templatesDir, fileSpec.src);
|
|
74
|
+
const destPath = path.join(this.targetDir, fileSpec.dest);
|
|
75
|
+
|
|
76
|
+
if (!await fs.pathExists(srcPath)) {
|
|
77
|
+
this.results.errors.push({ file: fileSpec.src, error: 'Template file not found' });
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const exists = await conflictCheck(destPath);
|
|
82
|
+
|
|
83
|
+
if (exists && fileSpec.merge === 'skip-existing') {
|
|
84
|
+
this.results.skipped.push(fileSpec.dest);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (exists && !this.force) {
|
|
89
|
+
if (fileSpec.merge === 'claude-md') {
|
|
90
|
+
await this.mergeClaudeMdFile(srcPath, destPath, fileSpec);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
if (fileSpec.merge === 'settings-json') {
|
|
94
|
+
await this.mergeSettingsJsonFile(srcPath, destPath, fileSpec);
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
// No merge strategy — skip existing
|
|
98
|
+
this.results.skipped.push(fileSpec.dest);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (this.dryRun) {
|
|
103
|
+
console.log(chalk.cyan(` [dry-run] Would install: ${fileSpec.dest}`));
|
|
104
|
+
this.results.installed.push(fileSpec.dest);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
109
|
+
await fs.copy(srcPath, destPath);
|
|
110
|
+
this.results.installed.push(fileSpec.dest);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async installDirectory(dir) {
|
|
114
|
+
const srcPath = path.join(this.templatesDir, dir);
|
|
115
|
+
const destPath = path.join(this.targetDir, dir);
|
|
116
|
+
|
|
117
|
+
if (!await fs.pathExists(srcPath)) {
|
|
118
|
+
this.results.errors.push({ file: dir, error: 'Template directory not found' });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const exists = await conflictCheck(destPath);
|
|
123
|
+
|
|
124
|
+
if (exists && !this.force) {
|
|
125
|
+
this.results.skipped.push(dir);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (this.dryRun) {
|
|
130
|
+
console.log(chalk.cyan(` [dry-run] Would install directory: ${dir}`));
|
|
131
|
+
this.results.installed.push(dir);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
136
|
+
await fs.copy(srcPath, destPath, { overwrite: this.force });
|
|
137
|
+
this.results.installed.push(dir);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async mergeClaudeMdFile(srcPath, destPath, fileSpec) {
|
|
141
|
+
const existing = await fs.readFile(destPath, 'utf-8');
|
|
142
|
+
const template = await fs.readFile(srcPath, 'utf-8');
|
|
143
|
+
const { content, added } = mergeClaudeMd(existing, template);
|
|
144
|
+
|
|
145
|
+
if (added.length === 0) {
|
|
146
|
+
this.results.skipped.push(fileSpec.dest);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
if (this.dryRun) {
|
|
151
|
+
console.log(chalk.cyan(` [dry-run] Would merge CLAUDE.md, adding ${added.length} references`));
|
|
152
|
+
this.results.merged.push({ file: fileSpec.dest, added });
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
await backupFile(destPath);
|
|
157
|
+
await fs.writeFile(destPath, content, 'utf-8');
|
|
158
|
+
this.results.merged.push({ file: fileSpec.dest, added });
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async mergeSettingsJsonFile(srcPath, destPath, fileSpec) {
|
|
162
|
+
const existing = await fs.readJson(destPath);
|
|
163
|
+
const template = await fs.readJson(srcPath);
|
|
164
|
+
const merged = mergeSettingsJson(existing, template);
|
|
165
|
+
|
|
166
|
+
if (JSON.stringify(existing) === JSON.stringify(merged)) {
|
|
167
|
+
this.results.skipped.push(fileSpec.dest);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (this.dryRun) {
|
|
172
|
+
console.log(chalk.cyan(` [dry-run] Would merge settings.json`));
|
|
173
|
+
this.results.merged.push({ file: fileSpec.dest });
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
await backupFile(destPath);
|
|
178
|
+
await fs.writeJson(destPath, merged, { spaces: 2 });
|
|
179
|
+
this.results.merged.push({ file: fileSpec.dest });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
async installRoleTemplate() {
|
|
183
|
+
const templatePath = path.join(this.templatesDir, 'memory', 'roles', `${this.role}.md`);
|
|
184
|
+
const destPath = path.join(this.targetDir, 'memory', 'user_profile.md');
|
|
185
|
+
|
|
186
|
+
if (await conflictCheck(destPath)) {
|
|
187
|
+
this.results.skipped.push('memory/user_profile.md');
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!await fs.pathExists(templatePath)) {
|
|
192
|
+
this.results.errors.push({ file: `memory/roles/${this.role}.md`, error: 'Role template not found' });
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (this.dryRun) {
|
|
197
|
+
console.log(chalk.cyan(` [dry-run] Would install user profile template for role: ${this.role}`));
|
|
198
|
+
this.results.installed.push('memory/user_profile.md');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
203
|
+
await fs.copy(templatePath, destPath);
|
|
204
|
+
this.results.installed.push('memory/user_profile.md');
|
|
205
|
+
}
|
|
206
|
+
}
|