@shirayner/ace 0.1.0-snapshot.3 → 0.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 +13 -2
- package/README.zh-CN.md +12 -3
- package/bin/ace.js +23 -0
- package/package.json +2 -1
- package/src/commands/doctor.js +14 -5
- package/src/commands/list.js +21 -3
- package/src/commands/spec.js +124 -0
- package/src/core/constants.js +19 -9
- package/src/core/installer.js +20 -0
- package/src/core/spec-installer.js +205 -0
- package/src/core/yaml-merger.js +45 -0
- package/templates/CLAUDE.md +3 -0
- package/templates/openspec/config.yaml +150 -0
- package/templates/openspec/evolution/adr.md +23 -0
- package/templates/openspec/evolution/glossary.md +24 -0
- package/templates/openspec/evolution/risk-map.md +43 -0
- package/templates/openspec/issues/design-issues.md +218 -0
- package/templates/openspec/issues/requirement-issues.md +214 -0
- package/templates/openspec/issues/retrospective-notes.md +40 -0
- package/templates/openspec/procedures/design-clarification-flow.md +57 -0
- package/templates/openspec/procedures/evolution-system.md +128 -0
- package/templates/openspec/procedures/interactive-clarification-protocol.md +54 -0
- package/templates/openspec/procedures/requirement-clarification-flow.md +55 -0
- package/templates/openspec/retrospective-template.md +81 -0
- package/templates/openspec/taxonomy/design-issue-taxonomy.md +180 -0
- package/templates/openspec/taxonomy/requirement-issue-taxonomy.md +155 -0
package/README.md
CHANGED
|
@@ -27,6 +27,15 @@ $ ace init
|
|
|
27
27
|
Done! Your AI coding environment is ready.
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
+
### Spec-driven Development
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
$ ace spec init ./my-project
|
|
34
|
+
✔ openspec config installed
|
|
35
|
+
✔ spec templates installed (taxonomy, issues, procedures, evolution)
|
|
36
|
+
Done! Spec workflow is ready.
|
|
37
|
+
```
|
|
38
|
+
|
|
30
39
|
## Why ace?
|
|
31
40
|
|
|
32
41
|
Claude Code is powerful out of the box — but configuring rules, skills,
|
|
@@ -34,8 +43,9 @@ safety guards, and memory templates by hand is tedious and error-prone.
|
|
|
34
43
|
|
|
35
44
|
**ace solves this in one command:**
|
|
36
45
|
|
|
37
|
-
- **Rules** —
|
|
46
|
+
- **Rules** — 8 cognitive and code-quality rules (deep thinking, clean code, ...)
|
|
38
47
|
- **Skills** — 4 AI skills with namespace isolation (`ace:auto-goal`, `ace:coding`, ...)
|
|
48
|
+
- **Spec** — Spec-driven development workflow with OpenSpec integration
|
|
39
49
|
- **Safety** — Hookify guards that block dangerous ops and protect secrets
|
|
40
50
|
- **Memory** — Templates for cross-session memory and developer profiles
|
|
41
51
|
- **Non-destructive** — Smart merge preserves your existing config; uninstall restores it
|
|
@@ -54,11 +64,12 @@ That's it. Run `ace doctor` to verify.
|
|
|
54
64
|
| Component | Contents | Preset |
|
|
55
65
|
|-----------|----------|--------|
|
|
56
66
|
| **Core** | `CLAUDE.md` + `settings.json` (smart merge) | all |
|
|
57
|
-
| **Rules** |
|
|
67
|
+
| **Rules** | 8 rules: thinking, clean-code, code-quality, ... | all |
|
|
58
68
|
| **Plugin** | 4 skills + 1 command (`ace:auto-goal`, `ace:coding`, ...) | all |
|
|
59
69
|
| **Hookify** | 3 safety guards (block-dangerous-ops, protect-secrets, ...) | full, safe |
|
|
60
70
|
| **Hooks** | Role-dependent scripts (e.g., Java compile check) | full |
|
|
61
71
|
| **Memory** | MEMORY.md template + role-based developer profile | full, safe |
|
|
72
|
+
| **Spec** | Spec-driven workflow templates (taxonomy, issues, procedures) | `ace spec init` |
|
|
62
73
|
|
|
63
74
|
## Design Philosophy
|
|
64
75
|
|
package/README.zh-CN.md
CHANGED
|
@@ -27,14 +27,22 @@ $ ace init
|
|
|
27
27
|
Done! Your AI coding environment is ready.
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
### Spec 驱动开发
|
|
31
|
+
|
|
32
|
+
```
|
|
33
|
+
$ ace spec init ./my-project
|
|
34
|
+
✔ openspec config installed
|
|
35
|
+
✔ spec templates installed (taxonomy, issues, procedures, evolution)
|
|
36
|
+
Done! Spec workflow is ready.
|
|
37
|
+
```
|
|
31
38
|
|
|
32
39
|
Claude Code 开箱即用已经很强大——但手动配置规则、技能、安全守卫和记忆模板既繁琐又容易出错。
|
|
33
40
|
|
|
34
41
|
**ace 用一条命令解决这个问题:**
|
|
35
42
|
|
|
36
|
-
- **规则** —
|
|
43
|
+
- **规则** — 8 条认知与代码质量规则(深度思考、Clean Code……)
|
|
37
44
|
- **技能** — 4 个 AI 技能,命名空间隔离(`ace:auto-goal`、`ace:coding`……)
|
|
45
|
+
- **规约** — Spec 驱动开发工作流,集成 OpenSpec
|
|
38
46
|
- **安全** — Hookify 守卫,拦截危险操作并保护密钥
|
|
39
47
|
- **记忆** — 跨会话记忆模板和开发者画像
|
|
40
48
|
- **无损安装** — 智能合并保留你的已有配置;卸载可完整还原
|
|
@@ -53,11 +61,12 @@ ace init
|
|
|
53
61
|
| 组件 | 内容 | 预设 |
|
|
54
62
|
|------|------|------|
|
|
55
63
|
| **Core** | `CLAUDE.md` + `settings.json`(智能合并) | 全部 |
|
|
56
|
-
| **Rules** |
|
|
64
|
+
| **Rules** | 8 条规则:thinking、clean-code、code-quality…… | 全部 |
|
|
57
65
|
| **Plugin** | 4 个技能 + 1 个命令(`ace:auto-goal`、`ace:coding`……) | 全部 |
|
|
58
66
|
| **Hookify** | 3 条安全守卫(block-dangerous-ops、protect-secrets……) | full、safe |
|
|
59
67
|
| **Hooks** | 角色相关脚本(如 Java 编译检查) | full |
|
|
60
68
|
| **Memory** | MEMORY.md 模板 + 角色开发者画像 | full、safe |
|
|
69
|
+
| **Spec** | Spec 驱动工作流模板(分类、问题、流程) | `ace spec init` |
|
|
61
70
|
|
|
62
71
|
## 设计理念
|
|
63
72
|
|
package/bin/ace.js
CHANGED
|
@@ -7,6 +7,7 @@ import { initCommand } from '../src/commands/init.js';
|
|
|
7
7
|
import { doctorCommand } from '../src/commands/doctor.js';
|
|
8
8
|
import { listCommand } from '../src/commands/list.js';
|
|
9
9
|
import { uninstallCommand } from '../src/commands/uninstall.js';
|
|
10
|
+
import { specInitCommand, specDoctorCommand, specUpdateCommand } from '../src/commands/spec.js';
|
|
10
11
|
|
|
11
12
|
const require = createRequire(import.meta.url);
|
|
12
13
|
const pkg = require('../package.json');
|
|
@@ -43,4 +44,26 @@ program
|
|
|
43
44
|
.option('-y, --yes', 'Skip confirmation prompt', false)
|
|
44
45
|
.action(uninstallCommand);
|
|
45
46
|
|
|
47
|
+
const spec = program
|
|
48
|
+
.command('spec')
|
|
49
|
+
.description('Manage spec-driven development workflow (project-level)');
|
|
50
|
+
|
|
51
|
+
spec
|
|
52
|
+
.command('init [path]')
|
|
53
|
+
.description('Initialize spec workflow in a project')
|
|
54
|
+
.option('-f, --force', 'Overwrite existing configuration', false)
|
|
55
|
+
.option('--dry-run', 'Preview without making changes', false)
|
|
56
|
+
.option('--skip-openspec', 'Skip openspec CLI installation', false)
|
|
57
|
+
.action(specInitCommand);
|
|
58
|
+
|
|
59
|
+
spec
|
|
60
|
+
.command('doctor [path]')
|
|
61
|
+
.description('Check spec workflow health')
|
|
62
|
+
.action(specDoctorCommand);
|
|
63
|
+
|
|
64
|
+
spec
|
|
65
|
+
.command('update [path]')
|
|
66
|
+
.description('Update spec templates to latest version')
|
|
67
|
+
.action(specUpdateCommand);
|
|
68
|
+
|
|
46
69
|
program.parse();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shirayner/ace",
|
|
3
|
-
"version": "0.1.0
|
|
3
|
+
"version": "0.1.0",
|
|
4
4
|
"description": "AI Coding Environment - One command to set up your Claude Code harness",
|
|
5
5
|
"bin": {
|
|
6
6
|
"ace": "./bin/ace.js"
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"inquirer": "^9.2.0",
|
|
31
31
|
"fs-extra": "^11.2.0",
|
|
32
32
|
"deepmerge": "^4.3.1",
|
|
33
|
+
"js-yaml": "^4.1.0",
|
|
33
34
|
"ora": "^8.0.0"
|
|
34
35
|
},
|
|
35
36
|
"files": [
|
package/src/commands/doctor.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import {
|
|
5
|
-
CLAUDE_DIR, COMPONENTS,
|
|
5
|
+
CLAUDE_DIR, COMPONENTS, TEMPLATES_DIR,
|
|
6
6
|
PLUGIN_CACHE_DIR, INSTALLED_PLUGINS_FILE, PLUGIN_KEY,
|
|
7
7
|
KNOWN_MARKETPLACES_FILE, MARKETPLACE_DIR, MARKETPLACE_NAME,
|
|
8
8
|
} from '../core/constants.js';
|
|
@@ -19,10 +19,19 @@ export async function doctorCommand() {
|
|
|
19
19
|
checks.push(await check('CLAUDE.md', fs.pathExists(path.join(CLAUDE_DIR, 'CLAUDE.md'))));
|
|
20
20
|
checks.push(await check('settings.json', fs.pathExists(path.join(CLAUDE_DIR, 'settings.json'))));
|
|
21
21
|
|
|
22
|
-
// 3. Check rules (
|
|
23
|
-
const
|
|
24
|
-
|
|
25
|
-
|
|
22
|
+
// 3. Check rules (dynamically scan templates directory)
|
|
23
|
+
const rulesDir = COMPONENTS.rules.rulesDir;
|
|
24
|
+
if (rulesDir) {
|
|
25
|
+
const templateRulesDir = path.join(TEMPLATES_DIR, rulesDir);
|
|
26
|
+
try {
|
|
27
|
+
const ruleFiles = await fs.readdir(templateRulesDir);
|
|
28
|
+
for (const file of ruleFiles) {
|
|
29
|
+
if (!file.endsWith('.md')) continue;
|
|
30
|
+
checks.push(await check(`rules/ace/${file}`, fs.pathExists(path.join(CLAUDE_DIR, rulesDir, file))));
|
|
31
|
+
}
|
|
32
|
+
} catch {
|
|
33
|
+
checks.push({ name: 'rules/ace/ directory', ok: false });
|
|
34
|
+
}
|
|
26
35
|
}
|
|
27
36
|
|
|
28
37
|
// 4. Check plugin installation
|
package/src/commands/list.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'fs-extra';
|
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import chalk from 'chalk';
|
|
4
4
|
import {
|
|
5
|
-
CLAUDE_DIR, COMPONENTS,
|
|
5
|
+
CLAUDE_DIR, COMPONENTS, TEMPLATES_DIR,
|
|
6
6
|
PLUGIN_CACHE_DIR, INSTALLED_PLUGINS_FILE, PLUGIN_KEY,
|
|
7
7
|
} from '../core/constants.js';
|
|
8
8
|
|
|
@@ -59,6 +59,14 @@ async function getPluginVersion() {
|
|
|
59
59
|
async function getComponentStatus(component) {
|
|
60
60
|
const allPaths = [];
|
|
61
61
|
|
|
62
|
+
if (component.rulesDir) {
|
|
63
|
+
try {
|
|
64
|
+
const templateDir = path.join(TEMPLATES_DIR, component.rulesDir);
|
|
65
|
+
const files = await fs.readdir(templateDir);
|
|
66
|
+
allPaths.push(...files.filter(f => f.endsWith('.md')).map(f => path.join(CLAUDE_DIR, component.rulesDir, f)));
|
|
67
|
+
} catch { /* ignore */ }
|
|
68
|
+
}
|
|
69
|
+
|
|
62
70
|
if (component.files) {
|
|
63
71
|
allPaths.push(...component.files.map(f => path.join(CLAUDE_DIR, f.dest)));
|
|
64
72
|
}
|
|
@@ -83,11 +91,21 @@ async function getComponentDetails(component) {
|
|
|
83
91
|
const missing = [];
|
|
84
92
|
const installed = [];
|
|
85
93
|
|
|
86
|
-
const allFiles = [
|
|
94
|
+
const allFiles = [];
|
|
95
|
+
|
|
96
|
+
if (component.rulesDir) {
|
|
97
|
+
try {
|
|
98
|
+
const templateDir = path.join(TEMPLATES_DIR, component.rulesDir);
|
|
99
|
+
const files = await fs.readdir(templateDir);
|
|
100
|
+
allFiles.push(...files.filter(f => f.endsWith('.md')).map(f => path.join(component.rulesDir, f)));
|
|
101
|
+
} catch { /* ignore */ }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
allFiles.push(
|
|
87
105
|
...(component.files || []).map(f => f.dest),
|
|
88
106
|
...(component.directories || []),
|
|
89
107
|
...(component.conditional || []).map(f => f.dest),
|
|
90
|
-
|
|
108
|
+
);
|
|
91
109
|
|
|
92
110
|
for (const file of allFiles) {
|
|
93
111
|
const exists = await fs.pathExists(path.join(CLAUDE_DIR, file));
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import chalk from 'chalk';
|
|
3
|
+
import ora from 'ora';
|
|
4
|
+
import { SpecInstaller } from '../core/spec-installer.js';
|
|
5
|
+
|
|
6
|
+
export async function specInitCommand(targetPath, options) {
|
|
7
|
+
const targetDir = targetPath ? path.resolve(targetPath) : process.cwd();
|
|
8
|
+
|
|
9
|
+
console.log(chalk.bold('\n ace spec init — Initialize spec-driven workflow\n'));
|
|
10
|
+
console.log(chalk.dim(` Target: ${targetDir}`));
|
|
11
|
+
if (options.force) console.log(chalk.yellow(' Force mode: existing files will be overwritten'));
|
|
12
|
+
if (options.dryRun) console.log(chalk.cyan(' Dry-run mode: no changes will be made'));
|
|
13
|
+
if (options.skipOpenspec) console.log(chalk.dim(' Skipping openspec CLI installation'));
|
|
14
|
+
console.log();
|
|
15
|
+
|
|
16
|
+
const installer = new SpecInstaller({
|
|
17
|
+
targetDir,
|
|
18
|
+
force: options.force,
|
|
19
|
+
dryRun: options.dryRun,
|
|
20
|
+
skipOpenspec: options.skipOpenspec,
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
const spinner = ora('Setting up spec workflow...').start();
|
|
24
|
+
|
|
25
|
+
try {
|
|
26
|
+
const { installed, skipped, merged, errors } = await installer.run();
|
|
27
|
+
spinner.stop();
|
|
28
|
+
|
|
29
|
+
printSummary(installed, skipped, merged, errors);
|
|
30
|
+
} catch (err) {
|
|
31
|
+
spinner.fail(`Unexpected error: ${err.message}`);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function specDoctorCommand(targetPath) {
|
|
37
|
+
const targetDir = targetPath ? path.resolve(targetPath) : process.cwd();
|
|
38
|
+
|
|
39
|
+
console.log(chalk.bold('\n ace spec doctor — Checking spec workflow health\n'));
|
|
40
|
+
console.log(chalk.dim(` Target: ${targetDir}\n`));
|
|
41
|
+
|
|
42
|
+
const installer = new SpecInstaller({ targetDir });
|
|
43
|
+
const checks = await installer.doctor();
|
|
44
|
+
|
|
45
|
+
const passed = checks.filter(c => c.ok).length;
|
|
46
|
+
const failed = checks.filter(c => !c.ok).length;
|
|
47
|
+
|
|
48
|
+
checks.forEach(c => {
|
|
49
|
+
const icon = c.ok ? chalk.green(' pass') : chalk.red(' FAIL');
|
|
50
|
+
console.log(` ${icon} ${c.name}`);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
console.log(chalk.bold(`\n ${passed} passed, ${failed} failed\n`));
|
|
54
|
+
|
|
55
|
+
if (failed > 0) {
|
|
56
|
+
console.log(chalk.yellow(' Run `ace spec init` to fix missing components.\n'));
|
|
57
|
+
} else {
|
|
58
|
+
console.log(chalk.green(' Spec workflow is healthy.\n'));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export async function specUpdateCommand(targetPath, options) {
|
|
63
|
+
const targetDir = targetPath ? path.resolve(targetPath) : process.cwd();
|
|
64
|
+
|
|
65
|
+
console.log(chalk.bold('\n ace spec update — Updating spec templates\n'));
|
|
66
|
+
console.log(chalk.dim(` Target: ${targetDir}`));
|
|
67
|
+
console.log();
|
|
68
|
+
|
|
69
|
+
const installer = new SpecInstaller({
|
|
70
|
+
targetDir,
|
|
71
|
+
force: true,
|
|
72
|
+
skipOpenspec: true,
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
const spinner = ora('Updating spec templates...').start();
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
// Only run template + config installation (skip openspec init)
|
|
79
|
+
await installer.installTemplates();
|
|
80
|
+
await installer.installConfig();
|
|
81
|
+
spinner.stop();
|
|
82
|
+
|
|
83
|
+
const { installed, skipped, merged, errors } = installer.results;
|
|
84
|
+
printSummary(installed, skipped, merged, errors);
|
|
85
|
+
} catch (err) {
|
|
86
|
+
spinner.fail(`Unexpected error: ${err.message}`);
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function printSummary(installed, skipped, merged, errors) {
|
|
92
|
+
console.log(chalk.bold('\n Summary\n'));
|
|
93
|
+
|
|
94
|
+
if (installed.length > 0) {
|
|
95
|
+
console.log(chalk.green(` Installed (${installed.length}):`));
|
|
96
|
+
installed.forEach(f => console.log(chalk.green(` + ${f}`)));
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
if (merged.length > 0) {
|
|
100
|
+
console.log(chalk.blue(` Merged (${merged.length}):`));
|
|
101
|
+
merged.forEach(m => {
|
|
102
|
+
const detail = m.version ? ` (v${m.version})` : '';
|
|
103
|
+
console.log(chalk.blue(` ~ ${m.file}${detail}`));
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (skipped.length > 0) {
|
|
108
|
+
console.log(chalk.yellow(` Skipped (${skipped.length}):`));
|
|
109
|
+
skipped.forEach(f => console.log(chalk.yellow(` - ${f}`)));
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (errors.length > 0) {
|
|
113
|
+
console.log(chalk.red(` Errors (${errors.length}):`));
|
|
114
|
+
errors.forEach(e => console.log(chalk.red(` ! ${e.file || e.component}: ${e.error}`)));
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
if (errors.length === 0) {
|
|
118
|
+
console.log(chalk.green.bold('\n Done! Spec workflow is ready.\n'));
|
|
119
|
+
console.log(chalk.dim(' Use /opsx:propose to start a new change.'));
|
|
120
|
+
console.log(chalk.dim(' Run `ace spec doctor` to verify.\n'));
|
|
121
|
+
} else {
|
|
122
|
+
console.log(chalk.yellow('\n Completed with errors. Run `ace spec doctor` to diagnose.\n'));
|
|
123
|
+
}
|
|
124
|
+
}
|
package/src/core/constants.js
CHANGED
|
@@ -49,6 +49,24 @@ export const ROLES = {
|
|
|
49
49
|
},
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
// Spec (project-level) constants
|
|
53
|
+
export const OPENSPEC_TEMPLATES_DIR = path.join(__dirname, '..', '..', 'templates', 'openspec');
|
|
54
|
+
export const SPEC_TEMPLATE_FILES = [
|
|
55
|
+
'taxonomy/requirement-issue-taxonomy.md',
|
|
56
|
+
'taxonomy/design-issue-taxonomy.md',
|
|
57
|
+
'issues/requirement-issues.md',
|
|
58
|
+
'issues/design-issues.md',
|
|
59
|
+
'issues/retrospective-notes.md',
|
|
60
|
+
'evolution/adr.md',
|
|
61
|
+
'evolution/glossary.md',
|
|
62
|
+
'evolution/risk-map.md',
|
|
63
|
+
'procedures/requirement-clarification-flow.md',
|
|
64
|
+
'procedures/design-clarification-flow.md',
|
|
65
|
+
'procedures/interactive-clarification-protocol.md',
|
|
66
|
+
'procedures/evolution-system.md',
|
|
67
|
+
'retrospective-template.md',
|
|
68
|
+
];
|
|
69
|
+
|
|
52
70
|
export const COMPONENTS = {
|
|
53
71
|
core: {
|
|
54
72
|
description: 'Core config (CLAUDE.md + settings.json)',
|
|
@@ -61,15 +79,7 @@ export const COMPONENTS = {
|
|
|
61
79
|
rules: {
|
|
62
80
|
description: 'Cognitive & code quality rules',
|
|
63
81
|
required: true,
|
|
64
|
-
|
|
65
|
-
{ src: 'rules/ace/thinking.md', dest: 'rules/ace/thinking.md' },
|
|
66
|
-
{ src: 'rules/ace/clean-code.md', dest: 'rules/ace/clean-code.md' },
|
|
67
|
-
{ src: 'rules/ace/code-quality.md', dest: 'rules/ace/code-quality.md' },
|
|
68
|
-
{ src: 'rules/ace/reporting.md', dest: 'rules/ace/reporting.md' },
|
|
69
|
-
{ src: 'rules/ace/task-recovery.md', dest: 'rules/ace/task-recovery.md' },
|
|
70
|
-
{ src: 'rules/ace/context-hygiene.md', dest: 'rules/ace/context-hygiene.md' },
|
|
71
|
-
{ src: 'rules/ace/memory-policy.md', dest: 'rules/ace/memory-policy.md' },
|
|
72
|
-
],
|
|
82
|
+
rulesDir: 'rules/ace',
|
|
73
83
|
},
|
|
74
84
|
plugin: {
|
|
75
85
|
description: 'Ace plugin (skills: auto-goal, coding, skill-creator, skill-optimize; commands: report)',
|
package/src/core/installer.js
CHANGED
|
@@ -50,6 +50,10 @@ export class Installer {
|
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
if (component.rulesDir) {
|
|
54
|
+
await this.installRulesDir(component.rulesDir);
|
|
55
|
+
}
|
|
56
|
+
|
|
53
57
|
if (component.files) {
|
|
54
58
|
for (const file of component.files) {
|
|
55
59
|
await this.installFile(file);
|
|
@@ -148,6 +152,22 @@ export class Installer {
|
|
|
148
152
|
this.results.merged.push({ file: 'plugins/known_marketplaces.json' });
|
|
149
153
|
}
|
|
150
154
|
|
|
155
|
+
async installRulesDir(rulesDir) {
|
|
156
|
+
const srcDir = path.join(this.templatesDir, rulesDir);
|
|
157
|
+
if (!await fs.pathExists(srcDir)) {
|
|
158
|
+
this.results.errors.push({ file: rulesDir, error: 'Rules directory not found' });
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
const files = await fs.readdir(srcDir);
|
|
162
|
+
for (const file of files) {
|
|
163
|
+
if (!file.endsWith('.md')) continue;
|
|
164
|
+
await this.installFile({
|
|
165
|
+
src: path.join(rulesDir, file),
|
|
166
|
+
dest: path.join(rulesDir, file),
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
151
171
|
async installFile(fileSpec) {
|
|
152
172
|
const srcPath = path.join(this.templatesDir, fileSpec.src);
|
|
153
173
|
const destPath = path.join(this.targetDir, fileSpec.dest);
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { execSync } from 'child_process';
|
|
4
|
+
import yaml from 'js-yaml';
|
|
5
|
+
import { OPENSPEC_TEMPLATES_DIR, SPEC_TEMPLATE_FILES } from './constants.js';
|
|
6
|
+
import { mergeSpecConfig } from './yaml-merger.js';
|
|
7
|
+
import { backupFile } from './merger.js';
|
|
8
|
+
|
|
9
|
+
export class SpecInstaller {
|
|
10
|
+
constructor(options = {}) {
|
|
11
|
+
this.targetDir = options.targetDir || process.cwd();
|
|
12
|
+
this.templatesDir = OPENSPEC_TEMPLATES_DIR;
|
|
13
|
+
this.force = options.force || false;
|
|
14
|
+
this.dryRun = options.dryRun || false;
|
|
15
|
+
this.skipOpenspec = options.skipOpenspec || false;
|
|
16
|
+
this.results = { installed: [], skipped: [], merged: [], errors: [] };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get openspecDir() {
|
|
20
|
+
return path.join(this.targetDir, 'openspec');
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async run() {
|
|
24
|
+
await this.ensureOpenspecCli();
|
|
25
|
+
await this.runOpenspecInit();
|
|
26
|
+
await this.installTemplates();
|
|
27
|
+
await this.installConfig();
|
|
28
|
+
return this.results;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async ensureOpenspecCli() {
|
|
32
|
+
if (this.skipOpenspec) return;
|
|
33
|
+
|
|
34
|
+
if (this.isOpenspecInstalled()) {
|
|
35
|
+
this.results.skipped.push('@fission-ai/openspec (already installed)');
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (this.dryRun) {
|
|
40
|
+
this.results.installed.push('@fission-ai/openspec (npm global)');
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
execSync('npm install -g @fission-ai/openspec', { stdio: 'pipe' });
|
|
46
|
+
this.results.installed.push('@fission-ai/openspec (npm global)');
|
|
47
|
+
} catch (err) {
|
|
48
|
+
this.results.errors.push({
|
|
49
|
+
component: 'openspec-cli',
|
|
50
|
+
error: `Failed to install @fission-ai/openspec: ${err.message}. Use --skip-openspec to skip.`,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
isOpenspecInstalled() {
|
|
56
|
+
try {
|
|
57
|
+
execSync('openspec --version', { stdio: 'pipe' });
|
|
58
|
+
return true;
|
|
59
|
+
} catch {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async runOpenspecInit() {
|
|
65
|
+
if (this.skipOpenspec) return;
|
|
66
|
+
|
|
67
|
+
if (await fs.pathExists(this.openspecDir) && !this.force) {
|
|
68
|
+
this.results.skipped.push('openspec/ (already exists)');
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (this.dryRun) {
|
|
73
|
+
this.results.installed.push('openspec/ (via openspec init)');
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (!this.isOpenspecInstalled()) return;
|
|
78
|
+
|
|
79
|
+
try {
|
|
80
|
+
execSync('openspec init', { cwd: this.targetDir, stdio: 'inherit' });
|
|
81
|
+
this.results.installed.push('openspec/ (via openspec init)');
|
|
82
|
+
} catch (err) {
|
|
83
|
+
this.results.errors.push({
|
|
84
|
+
component: 'openspec-init',
|
|
85
|
+
error: `openspec init failed: ${err.message}`,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async installTemplates() {
|
|
91
|
+
for (const file of SPEC_TEMPLATE_FILES) {
|
|
92
|
+
const srcPath = path.join(this.templatesDir, file);
|
|
93
|
+
const destPath = path.join(this.openspecDir, 'templates', file);
|
|
94
|
+
|
|
95
|
+
if (!await fs.pathExists(srcPath)) {
|
|
96
|
+
this.results.errors.push({ file, error: 'Template not found' });
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const exists = await fs.pathExists(destPath);
|
|
101
|
+
if (exists && !this.force) {
|
|
102
|
+
this.results.skipped.push(`openspec/templates/${file}`);
|
|
103
|
+
continue;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (this.dryRun) {
|
|
107
|
+
this.results.installed.push(`openspec/templates/${file}`);
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
112
|
+
await fs.copy(srcPath, destPath, { overwrite: this.force });
|
|
113
|
+
this.results.installed.push(`openspec/templates/${file}`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async installConfig() {
|
|
118
|
+
const srcPath = path.join(this.templatesDir, 'config.yaml');
|
|
119
|
+
const destPath = path.join(this.openspecDir, 'config.yaml');
|
|
120
|
+
|
|
121
|
+
if (!await fs.pathExists(srcPath)) {
|
|
122
|
+
this.results.errors.push({ file: 'config.yaml', error: 'Template not found' });
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const exists = await fs.pathExists(destPath);
|
|
127
|
+
|
|
128
|
+
if (exists && !this.force) {
|
|
129
|
+
// Merge: system fields overwrite, user fields preserve
|
|
130
|
+
if (this.dryRun) {
|
|
131
|
+
this.results.merged.push({ file: 'openspec/config.yaml' });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const existingContent = await fs.readFile(destPath, 'utf-8');
|
|
136
|
+
const templateContent = await fs.readFile(srcPath, 'utf-8');
|
|
137
|
+
const existing = yaml.load(existingContent);
|
|
138
|
+
const template = yaml.load(templateContent);
|
|
139
|
+
const merged = mergeSpecConfig(existing, template);
|
|
140
|
+
|
|
141
|
+
await backupFile(destPath);
|
|
142
|
+
await fs.writeFile(destPath, yaml.dump(merged, { lineWidth: -1 }), 'utf-8');
|
|
143
|
+
this.results.merged.push({ file: 'openspec/config.yaml', version: merged.version });
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (this.dryRun) {
|
|
148
|
+
this.results.installed.push('openspec/config.yaml');
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
await fs.ensureDir(path.dirname(destPath));
|
|
153
|
+
await fs.copy(srcPath, destPath);
|
|
154
|
+
this.results.installed.push('openspec/config.yaml');
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async doctor() {
|
|
158
|
+
const checks = [];
|
|
159
|
+
|
|
160
|
+
// 1. Node.js version
|
|
161
|
+
const nodeVersion = process.version;
|
|
162
|
+
const major = parseInt(nodeVersion.slice(1));
|
|
163
|
+
checks.push({ name: 'Node.js >= 18', ok: major >= 18 });
|
|
164
|
+
|
|
165
|
+
// 2. openspec CLI
|
|
166
|
+
checks.push({ name: '@fission-ai/openspec CLI', ok: this.isOpenspecInstalled() });
|
|
167
|
+
|
|
168
|
+
// 3. openspec/ directory
|
|
169
|
+
checks.push({ name: 'openspec/ directory', ok: await fs.pathExists(this.openspecDir) });
|
|
170
|
+
|
|
171
|
+
// 4. config.yaml
|
|
172
|
+
const configPath = path.join(this.openspecDir, 'config.yaml');
|
|
173
|
+
const configExists = await fs.pathExists(configPath);
|
|
174
|
+
checks.push({ name: 'openspec/config.yaml', ok: configExists });
|
|
175
|
+
|
|
176
|
+
if (configExists) {
|
|
177
|
+
try {
|
|
178
|
+
const config = yaml.load(await fs.readFile(configPath, 'utf-8'));
|
|
179
|
+
checks.push({ name: 'config.yaml has schema field', ok: !!config?.schema });
|
|
180
|
+
checks.push({ name: `config.yaml version: ${config?.version || 'unknown'}`, ok: !!config?.version });
|
|
181
|
+
} catch {
|
|
182
|
+
checks.push({ name: 'config.yaml valid YAML', ok: false });
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// 5. Required template files
|
|
187
|
+
for (const file of SPEC_TEMPLATE_FILES) {
|
|
188
|
+
const filePath = path.join(this.openspecDir, 'templates', file);
|
|
189
|
+
checks.push({
|
|
190
|
+
name: `template: ${file}`,
|
|
191
|
+
ok: await fs.pathExists(filePath),
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// 6. Git
|
|
196
|
+
try {
|
|
197
|
+
execSync('git --version', { stdio: 'pipe' });
|
|
198
|
+
checks.push({ name: 'Git available', ok: true });
|
|
199
|
+
} catch {
|
|
200
|
+
checks.push({ name: 'Git available', ok: false });
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return checks;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import yaml from 'js-yaml';
|
|
3
|
+
|
|
4
|
+
const SYSTEM_FIELDS = ['schema', 'version', 'context'];
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Merge spec config.yaml: system fields overwrite, user fields preserve, rules merge.
|
|
8
|
+
*/
|
|
9
|
+
export function mergeSpecConfig(existing, template) {
|
|
10
|
+
const merged = { ...existing };
|
|
11
|
+
|
|
12
|
+
for (const field of SYSTEM_FIELDS) {
|
|
13
|
+
if (template[field] !== undefined) {
|
|
14
|
+
merged[field] = template[field];
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
if (template.rules) {
|
|
19
|
+
merged.rules = { ...(existing.rules || {}), ...template.rules };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return merged;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Read, merge, and write config.yaml with backup.
|
|
27
|
+
* Returns { merged: true, version } or { merged: false, reason }.
|
|
28
|
+
*/
|
|
29
|
+
export async function mergeSpecConfigFile(existingPath, templatePath, backupDir) {
|
|
30
|
+
const existingContent = await fs.readFile(existingPath, 'utf-8');
|
|
31
|
+
const templateContent = await fs.readFile(templatePath, 'utf-8');
|
|
32
|
+
|
|
33
|
+
const existing = yaml.load(existingContent);
|
|
34
|
+
const template = yaml.load(templateContent);
|
|
35
|
+
const merged = mergeSpecConfig(existing, template);
|
|
36
|
+
|
|
37
|
+
if (backupDir) {
|
|
38
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
39
|
+
const backupPath = `${existingPath}.ace-backup.${timestamp}`;
|
|
40
|
+
await fs.copy(existingPath, backupPath);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await fs.writeFile(existingPath, yaml.dump(merged, { lineWidth: -1 }), 'utf-8');
|
|
44
|
+
return { merged: true, version: merged.version };
|
|
45
|
+
}
|