@shirayner/ace 0.1.0 → 0.1.1-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/{README.zh-CN.md → README.en-US.md} +11 -1
- package/README.md +276 -63
- package/bin/ace.js +1 -1
- package/package.json +1 -1
- package/plugin/skills/auto-goal/SKILL.md +97 -12
- package/src/commands/init.js +156 -39
- package/src/core/constants.js +8 -4
- package/src/core/installer.js +93 -16
- package/src/core/ui.js +182 -0
- package/templates/CLAUDE.md +6 -0
- package/templates/hookify/ace.hookify.code-quality-gate.local.md +45 -0
- package/templates/hookify/ace.hookify.safe-git-commands.local.md +38 -0
- package/templates/hookify/hookify.dangerous-commands.local.md +20 -0
- package/templates/hookify/hookify.sensitive-data.local.md +22 -0
- package/templates/openspec/config.yaml +4 -4
- package/templates/openspec/procedures/evolution-system.md +1 -1
- package/templates/openspec/procedures/interactive-clarification-protocol.md +1 -1
- package/templates/settings.json +39 -1
package/src/commands/init.js
CHANGED
|
@@ -1,37 +1,63 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import inquirer from 'inquirer';
|
|
3
|
-
import {
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import { PRESETS, ROLES, COMPONENTS } from '../core/constants.js';
|
|
4
5
|
import { Installer } from '../core/installer.js';
|
|
6
|
+
import {
|
|
7
|
+
printBanner, sectionHeader, stepDone, stepSkip, stepFail,
|
|
8
|
+
fileEntry, summaryBox, doneMessage, doneWithErrors, separator,
|
|
9
|
+
conflictHeader, conflictFile,
|
|
10
|
+
colors, icons, componentIcons, componentLabels,
|
|
11
|
+
} from '../core/ui.js';
|
|
12
|
+
|
|
13
|
+
const require = createRequire(import.meta.url);
|
|
14
|
+
const pkg = require('../../package.json');
|
|
5
15
|
|
|
6
16
|
export async function initCommand(options) {
|
|
7
|
-
|
|
17
|
+
// ─── Banner ──────────────────────────────────────────────────────
|
|
18
|
+
printBanner(pkg.version);
|
|
8
19
|
|
|
9
20
|
let role = 'fullstack';
|
|
10
21
|
let preset = options.preset;
|
|
11
22
|
|
|
12
|
-
// Interactive
|
|
23
|
+
// ─── Interactive prompts ─────────────────────────────────────────
|
|
13
24
|
if (options.interaction !== false) {
|
|
14
25
|
const answers = await inquirer.prompt([
|
|
15
26
|
{
|
|
16
27
|
type: 'list',
|
|
17
28
|
name: 'role',
|
|
18
|
-
message:
|
|
29
|
+
message: `${icons.gear} Your primary role?`,
|
|
19
30
|
choices: Object.entries(ROLES).map(([key, val]) => ({
|
|
20
|
-
name: `${val.label} — ${val.description}`,
|
|
31
|
+
name: `${colors.white(val.label)} ${colors.dim('—')} ${colors.muted(val.description)}`,
|
|
21
32
|
value: key,
|
|
33
|
+
short: val.label,
|
|
22
34
|
})),
|
|
23
35
|
default: 'fullstack',
|
|
36
|
+
prefix: colors.brand('?'),
|
|
24
37
|
},
|
|
25
38
|
{
|
|
26
39
|
type: 'list',
|
|
27
40
|
name: 'preset',
|
|
28
|
-
message:
|
|
41
|
+
message: `${icons.package} Installation scope?`,
|
|
29
42
|
choices: [
|
|
30
|
-
{
|
|
31
|
-
|
|
32
|
-
|
|
43
|
+
{
|
|
44
|
+
name: `${colors.white('Full')} ${colors.dim('—')} ${colors.muted('All components (rules, plugin, hooks, safety guards, memory)')}`,
|
|
45
|
+
value: 'full',
|
|
46
|
+
short: 'Full',
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: `${colors.white('Safe')} ${colors.dim('—')} ${colors.muted('Core + rules + plugin + safety guards + memory')}`,
|
|
50
|
+
value: 'safe',
|
|
51
|
+
short: 'Safe',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
name: `${colors.white('Minimal')} ${colors.dim('—')} ${colors.muted('Core + rules + plugin only')}`,
|
|
55
|
+
value: 'minimal',
|
|
56
|
+
short: 'Minimal',
|
|
57
|
+
},
|
|
33
58
|
],
|
|
34
59
|
default: 'full',
|
|
60
|
+
prefix: colors.brand('?'),
|
|
35
61
|
},
|
|
36
62
|
]);
|
|
37
63
|
role = answers.role;
|
|
@@ -40,59 +66,150 @@ export async function initCommand(options) {
|
|
|
40
66
|
|
|
41
67
|
const components = PRESETS[preset];
|
|
42
68
|
if (!components) {
|
|
43
|
-
console.error(
|
|
69
|
+
console.error(colors.error(` ${icons.cross} Unknown preset: ${preset}. Available: ${Object.keys(PRESETS).join(', ')}`));
|
|
44
70
|
process.exit(1);
|
|
45
71
|
}
|
|
46
72
|
|
|
47
|
-
|
|
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'));
|
|
73
|
+
// ─── Config summary ──────────────────────────────────────────────
|
|
52
74
|
console.log();
|
|
75
|
+
console.log(` ${colors.dim('│')} ${colors.dim('Role')} ${colors.white(ROLES[role].label)}`);
|
|
76
|
+
console.log(` ${colors.dim('│')} ${colors.dim('Preset')} ${colors.white(preset)}`);
|
|
77
|
+
console.log(` ${colors.dim('│')} ${colors.dim('Scope')} ${components.map(c => componentLabels[c] || c).join(colors.dim(', '))}`);
|
|
78
|
+
if (options.force) {
|
|
79
|
+
console.log(` ${colors.dim('│')} ${colors.warning(`${icons.warn} Force mode — existing files will be overwritten`)}`);
|
|
80
|
+
}
|
|
81
|
+
if (options.dryRun) {
|
|
82
|
+
console.log(` ${colors.dim('│')} ${colors.accent(`${icons.info} Dry-run mode — no changes will be made`)}`);
|
|
83
|
+
}
|
|
53
84
|
|
|
85
|
+
// ─── Conflict detection & category confirmation ──────────────────
|
|
54
86
|
const installer = new Installer({
|
|
55
87
|
force: options.force,
|
|
56
88
|
dryRun: options.dryRun,
|
|
57
89
|
role,
|
|
58
90
|
components,
|
|
91
|
+
quiet: true,
|
|
59
92
|
});
|
|
60
93
|
|
|
61
|
-
|
|
94
|
+
let resolutions = {};
|
|
62
95
|
|
|
63
|
-
|
|
64
|
-
|
|
96
|
+
if (!options.force && options.interaction !== false) {
|
|
97
|
+
const conflicts = await installer.detectConflicts();
|
|
98
|
+
const conflictKeys = Object.keys(conflicts);
|
|
65
99
|
|
|
66
|
-
|
|
67
|
-
|
|
100
|
+
if (conflictKeys.length > 0) {
|
|
101
|
+
console.log();
|
|
102
|
+
sectionHeader(icons.warn, 'Existing files detected');
|
|
103
|
+
console.log(` ${colors.dim('│')} ${colors.muted('Some files already exist. Choose how to handle each category:')}`);
|
|
104
|
+
console.log(` ${colors.dim('│')} ${colors.muted(`CLAUDE.md ${icons.arrowR} always smart-merge (append new refs only)`)}`);
|
|
105
|
+
console.log(` ${colors.dim('│')} ${colors.muted(`settings.json ${icons.arrowR} always deep-merge (preserve your settings)`)}`);
|
|
68
106
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
107
|
+
for (const componentName of conflictKeys) {
|
|
108
|
+
const { files } = conflicts[componentName];
|
|
109
|
+
const icon = componentIcons[componentName] || icons.file;
|
|
110
|
+
const label = componentLabels[componentName] || componentName;
|
|
111
|
+
|
|
112
|
+
conflictHeader(label, icon, files.length);
|
|
113
|
+
for (const f of files) {
|
|
114
|
+
conflictFile(f.replace(/\\/g, '/'));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
73
117
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
118
|
+
console.log();
|
|
119
|
+
const conflictAnswers = await inquirer.prompt(
|
|
120
|
+
conflictKeys.map((componentName) => {
|
|
121
|
+
const icon = componentIcons[componentName] || icons.file;
|
|
122
|
+
const label = componentLabels[componentName] || componentName;
|
|
123
|
+
const count = conflicts[componentName].files.length;
|
|
124
|
+
return {
|
|
125
|
+
type: 'list',
|
|
126
|
+
name: componentName,
|
|
127
|
+
message: `${icon} ${label} (${count} files)`,
|
|
128
|
+
choices: [
|
|
129
|
+
{
|
|
130
|
+
name: `${colors.muted('Skip')} ${colors.dim('— keep existing files')}`,
|
|
131
|
+
value: 'skip',
|
|
132
|
+
short: 'Skip',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
name: `${colors.warning('Overwrite')} ${colors.dim('— replace with ace templates')}`,
|
|
136
|
+
value: 'overwrite',
|
|
137
|
+
short: 'Overwrite',
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
default: 'skip',
|
|
141
|
+
prefix: colors.brand('?'),
|
|
142
|
+
};
|
|
143
|
+
})
|
|
144
|
+
);
|
|
145
|
+
|
|
146
|
+
resolutions = conflictAnswers;
|
|
147
|
+
}
|
|
80
148
|
}
|
|
81
149
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
150
|
+
// Apply resolutions to installer
|
|
151
|
+
installer.resolutions = resolutions;
|
|
152
|
+
|
|
153
|
+
// ─── Installation ────────────────────────────────────────────────
|
|
154
|
+
console.log();
|
|
155
|
+
sectionHeader(icons.rocket, 'Installing components');
|
|
156
|
+
|
|
157
|
+
for (const componentName of components) {
|
|
158
|
+
const component = COMPONENTS[componentName];
|
|
159
|
+
if (!component) continue;
|
|
160
|
+
|
|
161
|
+
const icon = componentIcons[componentName] || icons.file;
|
|
162
|
+
const label = componentLabels[componentName] || componentName;
|
|
163
|
+
|
|
164
|
+
const beforeInstalled = installer.results.installed.length;
|
|
165
|
+
const beforeMerged = installer.results.merged.length;
|
|
166
|
+
const beforeSkipped = installer.results.skipped.length;
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
await installer.installComponent(componentName, component);
|
|
170
|
+
const newInstalled = installer.results.installed.length - beforeInstalled;
|
|
171
|
+
const newMerged = installer.results.merged.length - beforeMerged;
|
|
172
|
+
const newSkipped = installer.results.skipped.length - beforeSkipped;
|
|
173
|
+
|
|
174
|
+
const parts = [];
|
|
175
|
+
if (newInstalled > 0) parts.push(colors.success(`${newInstalled} installed`));
|
|
176
|
+
if (newMerged > 0) parts.push(colors.blue(`${newMerged} merged`));
|
|
177
|
+
if (newSkipped > 0) parts.push(colors.muted(`${newSkipped} skipped`));
|
|
178
|
+
const detail = parts.length > 0 ? ` ${colors.dim('—')} ${parts.join(colors.dim(', '))}` : '';
|
|
179
|
+
|
|
180
|
+
stepDone(`${icon} ${label}${detail}`);
|
|
181
|
+
} catch (err) {
|
|
182
|
+
stepFail(`${icon} ${label} ${colors.dim('—')} ${colors.error(err.message)}`);
|
|
183
|
+
installer.results.errors.push({ component: componentName, error: err.message });
|
|
184
|
+
}
|
|
85
185
|
}
|
|
86
186
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
187
|
+
const { installed, skipped, merged, errors } = installer.results;
|
|
188
|
+
|
|
189
|
+
// ─── Detailed file list ──────────────────────────────────────────
|
|
190
|
+
if (installed.length > 0 || merged.length > 0 || skipped.length > 0) {
|
|
191
|
+
separator();
|
|
192
|
+
sectionHeader(icons.file, 'File details');
|
|
193
|
+
for (const f of installed) fileEntry('install', f.replace(/\\/g, '/'));
|
|
194
|
+
for (const m of merged) {
|
|
195
|
+
const detail = m.added ? ` (${m.added.length} refs)` : '';
|
|
196
|
+
fileEntry('merge', `${m.file.replace(/\\/g, '/')}${detail}`);
|
|
197
|
+
}
|
|
198
|
+
for (const f of skipped) fileEntry('skip', f.replace(/\\/g, '/'));
|
|
199
|
+
for (const e of errors) fileEntry('error', `${(e.file || e.component).replace(/\\/g, '/')}: ${e.error}`);
|
|
90
200
|
}
|
|
91
201
|
|
|
202
|
+
// ─── Summary ─────────────────────────────────────────────────────
|
|
203
|
+
summaryBox({
|
|
204
|
+
installed: installed.length,
|
|
205
|
+
merged: merged.length,
|
|
206
|
+
skipped: skipped.length,
|
|
207
|
+
errors: errors.length,
|
|
208
|
+
});
|
|
209
|
+
|
|
92
210
|
if (errors.length === 0) {
|
|
93
|
-
|
|
94
|
-
console.log(chalk.dim(' Run `ace doctor` to verify the installation.'));
|
|
211
|
+
doneMessage();
|
|
95
212
|
} else {
|
|
96
|
-
|
|
213
|
+
doneWithErrors();
|
|
97
214
|
}
|
|
98
215
|
}
|
package/src/core/constants.js
CHANGED
|
@@ -94,12 +94,16 @@ export const COMPONENTS = {
|
|
|
94
94
|
],
|
|
95
95
|
},
|
|
96
96
|
hookify: {
|
|
97
|
-
description: 'Safety guard rules (block dangerous ops, protect secrets, require verification)',
|
|
97
|
+
description: 'Safety guard rules (block dangerous ops, protect secrets, safe git, code quality, require verification)',
|
|
98
98
|
required: false,
|
|
99
99
|
files: [
|
|
100
|
-
{ src: 'hookify/ace.hookify.block-dangerous-ops.local.md', dest: 'ace.hookify.block-dangerous-ops.local.md' },
|
|
101
|
-
{ src: 'hookify/ace.hookify.protect-secrets.local.md', dest: 'ace.hookify.protect-secrets.local.md' },
|
|
102
|
-
{ src: 'hookify/ace.hookify.
|
|
100
|
+
{ src: 'hookify/ace.hookify.block-dangerous-ops.local.md', dest: 'hooks/ace.hookify.block-dangerous-ops.local.md' },
|
|
101
|
+
{ src: 'hookify/ace.hookify.protect-secrets.local.md', dest: 'hooks/ace.hookify.protect-secrets.local.md' },
|
|
102
|
+
{ src: 'hookify/ace.hookify.safe-git-commands.local.md', dest: 'hooks/ace.hookify.safe-git-commands.local.md' },
|
|
103
|
+
{ src: 'hookify/ace.hookify.code-quality-gate.local.md', dest: 'hooks/ace.hookify.code-quality-gate.local.md' },
|
|
104
|
+
{ src: 'hookify/ace.hookify.require-verification.local.md', dest: 'hooks/ace.hookify.require-verification.local.md' },
|
|
105
|
+
{ src: 'hookify/hookify.dangerous-commands.local.md', dest: 'hooks/hookify.dangerous-commands.local.md' },
|
|
106
|
+
{ src: 'hookify/hookify.sensitive-data.local.md', dest: 'hooks/hookify.sensitive-data.local.md' },
|
|
103
107
|
],
|
|
104
108
|
},
|
|
105
109
|
memory: {
|
package/src/core/installer.js
CHANGED
|
@@ -19,6 +19,76 @@ export class Installer {
|
|
|
19
19
|
this.role = options.role || 'fullstack';
|
|
20
20
|
this.components = options.components || [];
|
|
21
21
|
this.results = { installed: [], skipped: [], merged: [], errors: [] };
|
|
22
|
+
// Per-component resolution: { componentName: 'overwrite' | 'skip' }
|
|
23
|
+
this.resolutions = options.resolutions || {};
|
|
24
|
+
// Suppress inline console.log when caller handles UI
|
|
25
|
+
this.quiet = options.quiet || false;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Detect conflicts for all components before installation.
|
|
30
|
+
* Returns: { componentName: { files: [...conflicting dest paths], hasMerge: bool } }
|
|
31
|
+
*/
|
|
32
|
+
async detectConflicts() {
|
|
33
|
+
const conflicts = {};
|
|
34
|
+
|
|
35
|
+
for (const componentName of this.components) {
|
|
36
|
+
const component = COMPONENTS[componentName];
|
|
37
|
+
if (!component) continue;
|
|
38
|
+
|
|
39
|
+
// Plugin always overwrites — no conflict prompt needed
|
|
40
|
+
if (component.isPlugin) continue;
|
|
41
|
+
|
|
42
|
+
const conflicting = [];
|
|
43
|
+
let hasMerge = false;
|
|
44
|
+
|
|
45
|
+
// Check rulesDir files
|
|
46
|
+
if (component.rulesDir) {
|
|
47
|
+
const srcDir = path.join(this.templatesDir, component.rulesDir);
|
|
48
|
+
if (await fs.pathExists(srcDir)) {
|
|
49
|
+
const files = await fs.readdir(srcDir);
|
|
50
|
+
for (const file of files) {
|
|
51
|
+
if (!file.endsWith('.md')) continue;
|
|
52
|
+
const destPath = path.join(this.targetDir, component.rulesDir, file);
|
|
53
|
+
if (await fs.pathExists(destPath)) {
|
|
54
|
+
conflicting.push(path.join(component.rulesDir, file));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check regular files
|
|
61
|
+
if (component.files) {
|
|
62
|
+
for (const file of component.files) {
|
|
63
|
+
const destPath = path.join(this.targetDir, file.dest);
|
|
64
|
+
if (await fs.pathExists(destPath)) {
|
|
65
|
+
if (file.merge === 'claude-md' || file.merge === 'settings-json') {
|
|
66
|
+
hasMerge = true;
|
|
67
|
+
} else if (file.merge !== 'skip-existing') {
|
|
68
|
+
conflicting.push(file.dest);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Check conditional files
|
|
75
|
+
if (component.conditional) {
|
|
76
|
+
for (const file of component.conditional) {
|
|
77
|
+
if (file.roles && file.roles.includes(this.role)) {
|
|
78
|
+
const destPath = path.join(this.targetDir, file.dest);
|
|
79
|
+
if (await fs.pathExists(destPath)) {
|
|
80
|
+
conflicting.push(file.dest);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (conflicting.length > 0) {
|
|
87
|
+
conflicts[componentName] = { files: conflicting, hasMerge };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return conflicts;
|
|
22
92
|
}
|
|
23
93
|
|
|
24
94
|
async run() {
|
|
@@ -51,19 +121,19 @@ export class Installer {
|
|
|
51
121
|
}
|
|
52
122
|
|
|
53
123
|
if (component.rulesDir) {
|
|
54
|
-
await this.installRulesDir(component.rulesDir);
|
|
124
|
+
await this.installRulesDir(component.rulesDir, name);
|
|
55
125
|
}
|
|
56
126
|
|
|
57
127
|
if (component.files) {
|
|
58
128
|
for (const file of component.files) {
|
|
59
|
-
await this.installFile(file);
|
|
129
|
+
await this.installFile(file, name);
|
|
60
130
|
}
|
|
61
131
|
}
|
|
62
132
|
|
|
63
133
|
if (component.conditional) {
|
|
64
134
|
for (const file of component.conditional) {
|
|
65
135
|
if (file.roles && file.roles.includes(this.role)) {
|
|
66
|
-
await this.installFile(file);
|
|
136
|
+
await this.installFile(file, name);
|
|
67
137
|
}
|
|
68
138
|
}
|
|
69
139
|
}
|
|
@@ -91,9 +161,9 @@ export class Installer {
|
|
|
91
161
|
const destDir = path.join(PLUGIN_CACHE_DIR, version);
|
|
92
162
|
|
|
93
163
|
if (this.dryRun) {
|
|
94
|
-
console.log(chalk.cyan(` [dry-run] Would create marketplace ${MARKETPLACE_NAME}`));
|
|
95
|
-
console.log(chalk.cyan(` [dry-run] Would install plugin ${PLUGIN_KEY} v${version} to ${destDir}`));
|
|
96
|
-
console.log(chalk.cyan(` [dry-run] Would update ${INSTALLED_PLUGINS_FILE}`));
|
|
164
|
+
!this.quiet && console.log(chalk.cyan(` [dry-run] Would create marketplace ${MARKETPLACE_NAME}`));
|
|
165
|
+
!this.quiet && console.log(chalk.cyan(` [dry-run] Would install plugin ${PLUGIN_KEY} v${version} to ${destDir}`));
|
|
166
|
+
!this.quiet && console.log(chalk.cyan(` [dry-run] Would update ${INSTALLED_PLUGINS_FILE}`));
|
|
97
167
|
this.results.installed.push(`plugin:${PLUGIN_KEY} v${version}`);
|
|
98
168
|
return;
|
|
99
169
|
}
|
|
@@ -152,7 +222,7 @@ export class Installer {
|
|
|
152
222
|
this.results.merged.push({ file: 'plugins/known_marketplaces.json' });
|
|
153
223
|
}
|
|
154
224
|
|
|
155
|
-
async installRulesDir(rulesDir) {
|
|
225
|
+
async installRulesDir(rulesDir, componentName) {
|
|
156
226
|
const srcDir = path.join(this.templatesDir, rulesDir);
|
|
157
227
|
if (!await fs.pathExists(srcDir)) {
|
|
158
228
|
this.results.errors.push({ file: rulesDir, error: 'Rules directory not found' });
|
|
@@ -164,11 +234,11 @@ export class Installer {
|
|
|
164
234
|
await this.installFile({
|
|
165
235
|
src: path.join(rulesDir, file),
|
|
166
236
|
dest: path.join(rulesDir, file),
|
|
167
|
-
});
|
|
237
|
+
}, componentName);
|
|
168
238
|
}
|
|
169
239
|
}
|
|
170
240
|
|
|
171
|
-
async installFile(fileSpec) {
|
|
241
|
+
async installFile(fileSpec, componentName) {
|
|
172
242
|
const srcPath = path.join(this.templatesDir, fileSpec.src);
|
|
173
243
|
const destPath = path.join(this.targetDir, fileSpec.dest);
|
|
174
244
|
|
|
@@ -193,12 +263,19 @@ export class Installer {
|
|
|
193
263
|
await this.mergeSettingsJsonFile(srcPath, destPath, fileSpec);
|
|
194
264
|
return;
|
|
195
265
|
}
|
|
196
|
-
|
|
197
|
-
|
|
266
|
+
// Check per-component resolution
|
|
267
|
+
const resolution = this.resolutions[componentName];
|
|
268
|
+
if (resolution === 'overwrite') {
|
|
269
|
+
// fall through to install
|
|
270
|
+
} else {
|
|
271
|
+
// default: skip
|
|
272
|
+
this.results.skipped.push(fileSpec.dest);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
198
275
|
}
|
|
199
276
|
|
|
200
277
|
if (this.dryRun) {
|
|
201
|
-
console.log(chalk.cyan(` [dry-run] Would install: ${fileSpec.dest}`));
|
|
278
|
+
!this.quiet && console.log(chalk.cyan(` [dry-run] Would install: ${fileSpec.dest}`));
|
|
202
279
|
this.results.installed.push(fileSpec.dest);
|
|
203
280
|
return;
|
|
204
281
|
}
|
|
@@ -228,7 +305,7 @@ export class Installer {
|
|
|
228
305
|
}
|
|
229
306
|
|
|
230
307
|
if (this.dryRun) {
|
|
231
|
-
console.log(chalk.cyan(` [dry-run] Would install directory: ${dir}`));
|
|
308
|
+
!this.quiet && console.log(chalk.cyan(` [dry-run] Would install directory: ${dir}`));
|
|
232
309
|
this.results.installed.push(dir);
|
|
233
310
|
return;
|
|
234
311
|
}
|
|
@@ -249,7 +326,7 @@ export class Installer {
|
|
|
249
326
|
}
|
|
250
327
|
|
|
251
328
|
if (this.dryRun) {
|
|
252
|
-
console.log(chalk.cyan(` [dry-run] Would merge CLAUDE.md, adding ${added.length} references`));
|
|
329
|
+
!this.quiet && console.log(chalk.cyan(` [dry-run] Would merge CLAUDE.md, adding ${added.length} references`));
|
|
253
330
|
this.results.merged.push({ file: fileSpec.dest, added });
|
|
254
331
|
return;
|
|
255
332
|
}
|
|
@@ -271,7 +348,7 @@ export class Installer {
|
|
|
271
348
|
}
|
|
272
349
|
|
|
273
350
|
if (this.dryRun) {
|
|
274
|
-
console.log(chalk.cyan(` [dry-run] Would merge settings.json`));
|
|
351
|
+
!this.quiet && console.log(chalk.cyan(` [dry-run] Would merge settings.json`));
|
|
275
352
|
this.results.merged.push({ file: fileSpec.dest });
|
|
276
353
|
return;
|
|
277
354
|
}
|
|
@@ -297,7 +374,7 @@ export class Installer {
|
|
|
297
374
|
}
|
|
298
375
|
|
|
299
376
|
if (this.dryRun) {
|
|
300
|
-
console.log(chalk.cyan(` [dry-run] Would install user profile template for role: ${this.role}`));
|
|
377
|
+
!this.quiet && console.log(chalk.cyan(` [dry-run] Would install user profile template for role: ${this.role}`));
|
|
301
378
|
this.results.installed.push('memory/user_profile.md');
|
|
302
379
|
return;
|
|
303
380
|
}
|
package/src/core/ui.js
ADDED
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import chalk from 'chalk';
|
|
2
|
+
|
|
3
|
+
// ─── Icons ───────────────────────────────────────────────────────────
|
|
4
|
+
export const icons = {
|
|
5
|
+
ace: '◆',
|
|
6
|
+
check: '✔',
|
|
7
|
+
cross: '✖',
|
|
8
|
+
arrow: '▶',
|
|
9
|
+
arrowR: '→',
|
|
10
|
+
dot: '●',
|
|
11
|
+
circle: '○',
|
|
12
|
+
warn: '⚠',
|
|
13
|
+
info: 'ℹ',
|
|
14
|
+
skip: '◇',
|
|
15
|
+
merge: '⇄',
|
|
16
|
+
folder: '▸',
|
|
17
|
+
shield: '🛡',
|
|
18
|
+
gear: '⚙',
|
|
19
|
+
rocket: '🚀',
|
|
20
|
+
sparkles: '✨',
|
|
21
|
+
package: '📦',
|
|
22
|
+
file: '📄',
|
|
23
|
+
brain: '🧠',
|
|
24
|
+
hook: '🪝',
|
|
25
|
+
guard: '🔒',
|
|
26
|
+
memory: '💾',
|
|
27
|
+
plug: '🔌',
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// ─── Colors ──────────────────────────────────────────────────────────
|
|
31
|
+
export const colors = {
|
|
32
|
+
brand: chalk.hex('#7C3AED'), // vibrant purple
|
|
33
|
+
accent: chalk.hex('#06B6D4'), // cyan
|
|
34
|
+
success: chalk.hex('#10B981'), // emerald
|
|
35
|
+
warning: chalk.hex('#F59E0B'), // amber
|
|
36
|
+
error: chalk.hex('#EF4444'), // red
|
|
37
|
+
dim: chalk.hex('#6B7280'), // gray
|
|
38
|
+
muted: chalk.hex('#9CA3AF'), // light gray
|
|
39
|
+
white: chalk.hex('#F9FAFB'), // near white
|
|
40
|
+
blue: chalk.hex('#3B82F6'), // blue
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// ─── ASCII Banner ────────────────────────────────────────────────────
|
|
44
|
+
export function printBanner(version) {
|
|
45
|
+
const purple = colors.brand;
|
|
46
|
+
const dim = colors.dim;
|
|
47
|
+
|
|
48
|
+
console.log();
|
|
49
|
+
console.log(purple(' ╔═══╗ ╔═══╗ ╔═══╗'));
|
|
50
|
+
console.log(purple(' ╠═══╣ ║ ╠═══╝'));
|
|
51
|
+
console.log(purple(' ║ ║ ╚═══╝ ╚═══╗'));
|
|
52
|
+
console.log();
|
|
53
|
+
console.log(` ${purple.bold('ace')} ${dim(`v${version}`)} ${dim('— AI Coding Environment')}`);
|
|
54
|
+
console.log();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ─── Section Header ──────────────────────────────────────────────────
|
|
58
|
+
export function sectionHeader(icon, title) {
|
|
59
|
+
console.log(` ${icon} ${colors.white.bold(title)}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ─── Step indicator ──────────────────────────────────────────────────
|
|
63
|
+
export function stepStart(label) {
|
|
64
|
+
process.stdout.write(` ${colors.dim('│')}\n`);
|
|
65
|
+
process.stdout.write(` ${colors.brand(icons.dot)} ${label}\n`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function stepDone(label) {
|
|
69
|
+
process.stdout.write(` ${colors.success(icons.check)} ${label}\n`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function stepSkip(label) {
|
|
73
|
+
process.stdout.write(` ${colors.muted(icons.skip)} ${colors.muted(label)}\n`);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export function stepWarn(label) {
|
|
77
|
+
process.stdout.write(` ${colors.warning(icons.warn)} ${label}\n`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function stepFail(label) {
|
|
81
|
+
process.stdout.write(` ${colors.error(icons.cross)} ${label}\n`);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── File list (compact) ─────────────────────────────────────────────
|
|
85
|
+
export function fileEntry(action, filePath) {
|
|
86
|
+
const prefix = {
|
|
87
|
+
install: ` ${colors.success('+')}`,
|
|
88
|
+
merge: ` ${colors.blue('~')}`,
|
|
89
|
+
skip: ` ${colors.muted('-')}`,
|
|
90
|
+
overwrite:` ${colors.warning('!')}`,
|
|
91
|
+
error: ` ${colors.error('✖')}`,
|
|
92
|
+
};
|
|
93
|
+
const color = {
|
|
94
|
+
install: colors.success,
|
|
95
|
+
merge: colors.blue,
|
|
96
|
+
skip: colors.muted,
|
|
97
|
+
overwrite:colors.warning,
|
|
98
|
+
error: colors.error,
|
|
99
|
+
};
|
|
100
|
+
console.log(`${prefix[action] || ' '} ${(color[action] || colors.dim)(filePath)}`);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Summary Box ─────────────────────────────────────────────────────
|
|
104
|
+
export function summaryBox(stats) {
|
|
105
|
+
const { installed, merged, skipped, errors } = stats;
|
|
106
|
+
|
|
107
|
+
console.log();
|
|
108
|
+
console.log(` ${colors.dim('╭─────────────────────────────────────╮')}`);
|
|
109
|
+
console.log(` ${colors.dim('│')} ${colors.white.bold('Installation Summary')} ${colors.dim('│')}`);
|
|
110
|
+
console.log(` ${colors.dim('├─────────────────────────────────────┤')}`);
|
|
111
|
+
|
|
112
|
+
if (installed > 0) {
|
|
113
|
+
console.log(` ${colors.dim('│')} ${colors.success(icons.check)} ${colors.success(`${installed} installed`)}${pad(installed, 'installed')}${colors.dim('│')}`);
|
|
114
|
+
}
|
|
115
|
+
if (merged > 0) {
|
|
116
|
+
console.log(` ${colors.dim('│')} ${colors.blue(icons.merge)} ${colors.blue(`${merged} merged`)}${pad(merged, 'merged')}${colors.dim('│')}`);
|
|
117
|
+
}
|
|
118
|
+
if (skipped > 0) {
|
|
119
|
+
console.log(` ${colors.dim('│')} ${colors.muted(icons.skip)} ${colors.muted(`${skipped} skipped`)}${pad(skipped, 'skipped')}${colors.dim('│')}`);
|
|
120
|
+
}
|
|
121
|
+
if (errors > 0) {
|
|
122
|
+
console.log(` ${colors.dim('│')} ${colors.error(icons.cross)} ${colors.error(`${errors} errors`)}${pad(errors, 'errors')}${colors.dim('│')}`);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
console.log(` ${colors.dim('╰─────────────────────────────────────╯')}`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function pad(count, label) {
|
|
129
|
+
const text = `${count} ${label}`;
|
|
130
|
+
const total = 33;
|
|
131
|
+
const spaces = total - text.length - 4; // 4 = icon + spaces
|
|
132
|
+
return ' '.repeat(Math.max(1, spaces));
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// ─── Final message ───────────────────────────────────────────────────
|
|
136
|
+
export function doneMessage() {
|
|
137
|
+
console.log();
|
|
138
|
+
console.log(` ${icons.rocket} ${colors.success.bold('Your AI coding environment is ready.')}`);
|
|
139
|
+
console.log(` ${colors.dim(`Run ${chalk.white('ace doctor')} to verify the installation.`)}`);
|
|
140
|
+
console.log();
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function doneWithErrors() {
|
|
144
|
+
console.log();
|
|
145
|
+
console.log(` ${icons.warn} ${colors.warning.bold('Completed with errors.')}`);
|
|
146
|
+
console.log(` ${colors.dim(`Run ${chalk.white('ace doctor')} to diagnose.`)}`);
|
|
147
|
+
console.log();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// ─── Separator ───────────────────────────────────────────────────────
|
|
151
|
+
export function separator() {
|
|
152
|
+
console.log(` ${colors.dim('│')}`);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// ─── Conflict prompt helpers ─────────────────────────────────────────
|
|
156
|
+
export function conflictHeader(componentName, icon, fileCount) {
|
|
157
|
+
console.log();
|
|
158
|
+
console.log(` ${colors.warning(icons.warn)} ${icon} ${colors.white.bold(componentName)} — ${colors.warning(`${fileCount} file(s) already exist`)}`);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export function conflictFile(filePath) {
|
|
162
|
+
console.log(` ${colors.dim(icons.arrowR)} ${colors.muted(filePath)}`);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─── Component icons ─────────────────────────────────────────────────
|
|
166
|
+
export const componentIcons = {
|
|
167
|
+
core: icons.gear,
|
|
168
|
+
rules: icons.brain,
|
|
169
|
+
plugin: icons.plug,
|
|
170
|
+
hooks: icons.hook,
|
|
171
|
+
hookify: icons.guard,
|
|
172
|
+
memory: icons.memory,
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const componentLabels = {
|
|
176
|
+
core: 'Core Config',
|
|
177
|
+
rules: 'Rules',
|
|
178
|
+
plugin: 'Plugin',
|
|
179
|
+
hooks: 'Hooks',
|
|
180
|
+
hookify: 'Safety Guards',
|
|
181
|
+
memory: 'Memory',
|
|
182
|
+
};
|
package/templates/CLAUDE.md
CHANGED
|
@@ -15,5 +15,11 @@
|
|
|
15
15
|
## 质量控制
|
|
16
16
|
- @~/.claude/rules/ace/memory-policy.md - Memory 质量策略
|
|
17
17
|
|
|
18
|
+
## Hookify 规则
|
|
19
|
+
- @~/.claude/hooks/ace.hookify.block-dangerous-ops.local.md - 阻止危险操作
|
|
20
|
+
- @~/.claude/hooks/ace.hookify.protect-secrets.local.md - 保护敏感信息
|
|
21
|
+
- @~/.claude/hooks/ace.hookify.safe-git-commands.local.md - Git 安全命令
|
|
22
|
+
- @~/.claude/hooks/ace.hookify.code-quality-gate.local.md - 代码质量门禁
|
|
23
|
+
|
|
18
24
|
## 交互规则
|
|
19
25
|
- @~/.claude/rules/ace/interactive-clarify.md - 交互式澄清规则
|