@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.
@@ -1,37 +1,63 @@
1
1
  import chalk from 'chalk';
2
2
  import inquirer from 'inquirer';
3
- import { PRESETS, ROLES } from '../core/constants.js';
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
- console.log(chalk.bold('\n ace - AI Coding Environment\n'));
17
+ // ─── Banner ──────────────────────────────────────────────────────
18
+ printBanner(pkg.version);
8
19
 
9
20
  let role = 'fullstack';
10
21
  let preset = options.preset;
11
22
 
12
- // Interactive mode
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: 'What is your primary role?',
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: 'Installation scope?',
41
+ message: `${icons.package} Installation scope?`,
29
42
  choices: [
30
- { name: 'Full — All components (rules, skills, hooks, safety guards, memory)', value: 'full' },
31
- { name: 'SafeCore + rules + skills + safety guards + memory', value: 'safe' },
32
- { name: 'Minimal — Core + rules + skills only', value: 'minimal' },
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(chalk.red(`Unknown preset: ${preset}. Available: ${Object.keys(PRESETS).join(', ')}`));
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
- 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'));
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
- const results = installer.run();
94
+ let resolutions = {};
62
95
 
63
- // Wait for async
64
- const { installed, skipped, merged, errors } = await results;
96
+ if (!options.force && options.interaction !== false) {
97
+ const conflicts = await installer.detectConflicts();
98
+ const conflictKeys = Object.keys(conflicts);
65
99
 
66
- // Summary
67
- console.log(chalk.bold('\n Installation Summary\n'));
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
- if (installed.length > 0) {
70
- console.log(chalk.green(` Installed (${installed.length}):`));
71
- installed.forEach(f => console.log(chalk.green(` + ${f}`)));
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
- 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
- });
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
- if (skipped.length > 0) {
83
- console.log(chalk.yellow(` Skipped (${skipped.length}) already exist:`));
84
- skipped.forEach(f => console.log(chalk.yellow(` - ${f}`)));
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
- 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}`)));
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
- 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.'));
211
+ doneMessage();
95
212
  } else {
96
- console.log(chalk.yellow('\n Completed with errors. Run `ace doctor` to diagnose.\n'));
213
+ doneWithErrors();
97
214
  }
98
215
  }
@@ -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.require-verification.local.md', dest: 'ace.hookify.require-verification.local.md' },
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: {
@@ -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
- this.results.skipped.push(fileSpec.dest);
197
- return;
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
+ };
@@ -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 - 交互式澄清规则