@shirayner/ace 0.1.1-snapshot.4 → 0.1.1-snapshot.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shirayner/ace",
3
- "version": "0.1.1-snapshot.4",
3
+ "version": "0.1.1-snapshot.6",
4
4
  "description": "AI Coding Environment - One command to set up your Claude Code harness",
5
5
  "bin": {
6
6
  "ace": "./bin/ace.js"
@@ -1,7 +1,10 @@
1
1
  import * as p from '@clack/prompts';
2
+ import path from 'path';
3
+ import fs from 'fs-extra';
2
4
  import { createRequire } from 'module';
3
- import { PRESETS, COMPONENTS } from '../core/constants.js';
5
+ import { PRESETS, COMPONENTS, CLAUDE_DIR, TEMPLATES_DIR } from '../core/constants.js';
4
6
  import { Installer } from '../core/installer.js';
7
+ import { mergeClaudeMd } from '../core/merger.js';
5
8
 
6
9
  const require = createRequire(import.meta.url);
7
10
  const pkg = require('../../package.json');
@@ -19,10 +22,8 @@ export async function initCommand(options) {
19
22
  const version = pkg.version;
20
23
  const components = PRESETS['full'];
21
24
 
22
- // ─── Intro ──────────────────────────────────────────────
23
25
  p.intro(`ace v${version}`);
24
26
 
25
- // ─── Conflict detection ─────────────────────────────────
26
27
  const installer = new Installer({
27
28
  force: options.force,
28
29
  dryRun: options.dryRun,
@@ -33,89 +34,100 @@ export async function initCommand(options) {
33
34
 
34
35
  let resolutions = {};
35
36
 
37
+ // ─── Conflict detection with categorized preview ───
36
38
  if (!options.force) {
37
- const conflicts = await installer.detectConflicts();
38
- const conflictKeys = Object.keys(conflicts);
39
-
40
- if (conflictKeys.length > 0) {
41
- const totalFiles = conflictKeys.reduce((sum, k) => sum + conflicts[k].files.length, 0);
42
- const mergeComponents = conflictKeys.filter(k => conflicts[k].hasMerge);
43
-
44
- if (mergeComponents.length > 0) {
45
- p.log.info('Safe merge: CLAUDE.md, settings.json (preserves your changes)');
46
- }
47
- if (totalFiles > 0) {
48
- p.log.warn(`${totalFiles} existing file(s) found`);
39
+ const preview = await buildInstallPreview(installer, components);
40
+ const hasExisting = preview.merge.length > 0 || preview.skip.length > 0 || preview.conflict.length > 0;
41
+
42
+ if (hasExisting) {
43
+ // Show safe merge section
44
+ if (preview.merge.length > 0) {
45
+ const mergeLines = preview.merge.map(m => ` ${m.dest} — ${m.detail}`);
46
+ p.log.info(['Safe merge:', ...mergeLines].join('\n'));
49
47
  }
50
48
 
51
- const action = await p.select({
52
- message: 'How to handle existing files?',
53
- options: [
54
- { value: 'skip', label: 'Keep & merge', hint: 'recommended' },
55
- { value: 'overwrite', label: 'Overwrite all', hint: 'replace with latest' },
56
- { value: 'cancel', label: 'Cancel' },
57
- ],
58
- initialValue: 'skip',
59
- });
60
-
61
- if (p.isCancel(action) || action === 'cancel') {
62
- p.cancel('Setup cancelled.');
63
- process.exit(0);
49
+ // Show auto-skip section
50
+ if (preview.skip.length > 0) {
51
+ const skipLines = preview.skip.map(s => ` ${s} — preserves your data`);
52
+ p.log.message(['Auto-skip:', ...skipLines].join('\n'));
64
53
  }
65
54
 
66
- for (const key of conflictKeys) {
67
- resolutions[key] = action;
55
+ // Show conflict section + prompt
56
+ if (preview.conflict.length > 0) {
57
+ const conflictLines = preview.conflict.map(f => ` ${f}`);
58
+ p.log.warn([`${preview.conflict.length} existing file(s):`, ...conflictLines].join('\n'));
59
+
60
+ const action = await p.select({
61
+ message: `How to handle ${preview.conflict.length} existing files?`,
62
+ options: [
63
+ { value: 'skip', label: 'Keep existing', hint: 'recommended' },
64
+ { value: 'overwrite', label: 'Overwrite with latest' },
65
+ { value: 'cancel', label: 'Cancel' },
66
+ ],
67
+ initialValue: 'skip',
68
+ });
69
+
70
+ if (p.isCancel(action) || action === 'cancel') {
71
+ p.cancel('Setup cancelled.');
72
+ process.exit(0);
73
+ }
74
+
75
+ for (const componentName of components) {
76
+ resolutions[componentName] = action;
77
+ }
68
78
  }
69
79
  }
70
80
  }
71
81
 
72
82
  installer.resolutions = resolutions;
73
83
 
74
- // ─── Dry-run notice ─────────────────────────────────────
75
84
  if (options.dryRun) {
76
85
  p.log.warn('dry-run — no changes will be made');
77
86
  }
78
87
 
79
- // ─── Install ────────────────────────────────────────────
80
- p.log.step('Installing to ~/.claude/');
88
+ // ─── Install with single spinner ───────────────────
89
+ const s = p.spinner();
90
+ const componentResults = [];
91
+
92
+ s.start('Installing...');
81
93
 
82
94
  for (const componentName of components) {
83
95
  const component = COMPONENTS[componentName];
84
96
  if (!component) continue;
85
97
 
86
98
  const label = componentLabels[componentName] || componentName;
99
+ s.message(`Installing ${label}...`);
100
+
87
101
  const beforeInstalled = installer.results.installed.length;
88
102
  const beforeMerged = installer.results.merged.length;
89
103
  const beforeSkipped = installer.results.skipped.length;
90
104
 
91
- const s = p.spinner();
92
- s.start(label);
93
-
94
105
  try {
95
106
  await installer.installComponent(componentName, component);
96
- s.stop(label);
97
-
98
- const newInstalled = installer.results.installed.length - beforeInstalled;
99
- const newMerged = installer.results.merged.length - beforeMerged;
100
- const newSkipped = installer.results.skipped.length - beforeSkipped;
101
-
102
- if (newMerged > 0 && newInstalled === 0 && newSkipped === 0) {
103
- p.log.info(`${label} — merged`);
104
- } else if (newSkipped > 0 && newInstalled === 0 && newMerged === 0) {
105
- p.log.message(`${label} — unchanged`);
106
- } else {
107
- const count = newInstalled + newMerged;
108
- const detail = count > 0 ? `${count} file${count > 1 ? 's' : ''}` : '';
109
- p.log.success(`${label} — ${detail}`);
110
- }
107
+ componentResults.push({
108
+ label,
109
+ installed: installer.results.installed.length - beforeInstalled,
110
+ merged: installer.results.merged.length - beforeMerged,
111
+ skipped: installer.results.skipped.length - beforeSkipped,
112
+ error: null,
113
+ });
111
114
  } catch (err) {
112
- s.stop(label);
113
- p.log.error(`${label} — ${err.message}`);
115
+ componentResults.push({
116
+ label,
117
+ installed: installer.results.installed.length - beforeInstalled,
118
+ merged: installer.results.merged.length - beforeMerged,
119
+ skipped: installer.results.skipped.length - beforeSkipped,
120
+ error: err.message,
121
+ });
114
122
  installer.results.errors.push({ component: componentName, error: err.message });
115
123
  }
116
124
  }
117
125
 
118
- // ─── Summary ────────────────────────────────────────────
126
+ s.stop('Installed to ~/.claude/');
127
+
128
+ // ─── Summary table ─────────────────────────────────
129
+ p.log.message(formatSummaryTable(componentResults));
130
+
119
131
  const { installed, merged, skipped, errors } = installer.results;
120
132
  const parts = [];
121
133
  if (installed.length > 0) parts.push(`${installed.length} installed`);
@@ -128,12 +140,12 @@ export async function initCommand(options) {
128
140
  p.log.warn(`${parts.join(', ')}, ${errors.length} failed`);
129
141
  }
130
142
 
131
- // ─── Next Steps ─────────────────────────────────────────
143
+ // ─── Next Steps ────────────────────────────────────
132
144
  p.note(
133
145
  [
134
146
  'Get started',
135
147
  ' 1. cd <your-project> && ace spec init',
136
- ' 2. Open Claude Code, type: /opsx:propose 创建需求提案',
148
+ ' 2. Open Claude Code, type: /opsx:propose',
137
149
  '',
138
150
  'Customize',
139
151
  ' Change role edit ~/.claude/memory/user_profile.md',
@@ -144,10 +156,95 @@ export async function initCommand(options) {
144
156
  'Next steps'
145
157
  );
146
158
 
147
- // ─── Outro ──────────────────────────────────────────────
148
159
  if (errors.length === 0) {
149
160
  p.outro('Done. Go to your project and run ace spec init.');
150
161
  } else {
151
162
  p.outro('Done with errors. Run ace doctor to diagnose.');
152
163
  }
153
164
  }
165
+
166
+ // ─── Helpers ───────────────────────────────────────────
167
+
168
+ /**
169
+ * Scan target directory and categorize existing files by handling strategy.
170
+ */
171
+ async function buildInstallPreview(installer, components) {
172
+ const preview = { merge: [], skip: [], conflict: [] };
173
+
174
+ for (const componentName of components) {
175
+ const component = COMPONENTS[componentName];
176
+ if (!component || component.isPlugin) continue;
177
+
178
+ if (component.files) {
179
+ for (const file of component.files) {
180
+ const destPath = path.join(installer.targetDir, file.dest);
181
+ if (await fs.pathExists(destPath)) {
182
+ if (file.merge === 'claude-md' || file.merge === 'settings-json') {
183
+ preview.merge.push({ src: file.src, dest: file.dest, strategy: file.merge });
184
+ } else if (file.merge === 'skip-existing') {
185
+ preview.skip.push(file.dest);
186
+ } else {
187
+ preview.conflict.push(file.dest);
188
+ }
189
+ }
190
+ }
191
+ }
192
+
193
+ if (component.rulesDir) {
194
+ const srcDir = path.join(installer.templatesDir, component.rulesDir);
195
+ const destDir = path.join(installer.targetDir, component.rulesDir);
196
+ if (await fs.pathExists(srcDir)) {
197
+ const files = (await fs.readdir(srcDir)).filter(f => f.endsWith('.md'));
198
+ for (const f of files) {
199
+ if (await fs.pathExists(path.join(destDir, f))) {
200
+ preview.conflict.push(path.join(component.rulesDir, f).replace(/\\/g, '/'));
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ if (component.conditional) {
207
+ for (const file of component.conditional) {
208
+ if (file.roles?.includes(installer.role)) {
209
+ const destPath = path.join(installer.targetDir, file.dest);
210
+ if (await fs.pathExists(destPath)) {
211
+ preview.conflict.push(file.dest);
212
+ }
213
+ }
214
+ }
215
+ }
216
+ }
217
+
218
+ // Enrich merge files with detail
219
+ for (const item of preview.merge) {
220
+ if (item.strategy === 'claude-md') {
221
+ try {
222
+ const existing = await fs.readFile(path.join(installer.targetDir, item.dest), 'utf-8');
223
+ const template = await fs.readFile(path.join(installer.templatesDir, item.src), 'utf-8');
224
+ const { added } = mergeClaudeMd(existing, template);
225
+ item.detail = added.length > 0 ? `adds ${added.length} new @references` : 'up to date';
226
+ } catch {
227
+ item.detail = 'will merge';
228
+ }
229
+ } else if (item.strategy === 'settings-json') {
230
+ item.detail = 'merges permissions & plugins';
231
+ }
232
+ }
233
+
234
+ return preview;
235
+ }
236
+
237
+ /**
238
+ * Format component results into an aligned summary table.
239
+ */
240
+ function formatSummaryTable(results) {
241
+ const maxLen = Math.max(...results.map(r => r.label.length));
242
+ return results.map(r => {
243
+ const padded = r.label.padEnd(maxLen);
244
+ if (r.error) return `\u25A0 ${padded} ${r.error}`;
245
+ if (r.merged > 0 && r.installed === 0 && r.skipped === 0) return `\u25C6 ${padded} merged`;
246
+ if (r.skipped > 0 && r.installed === 0 && r.merged === 0) return `\u2502 ${padded} unchanged`;
247
+ const count = r.installed + r.merged;
248
+ return `\u25C6 ${padded} ${count} file${count > 1 ? 's' : ''}`;
249
+ }).join('\n');
250
+ }
@@ -18,7 +18,14 @@
18
18
  "Bash(openspec*)",
19
19
  "Bash(opc*)",
20
20
  "Bash(gh *)",
21
+ "Bash(where:*)",
22
+ "Bash(npx*)",
23
+ "Bash(python*)",
24
+ "Bash(pip*)",
21
25
  "Read",
26
+ "Write",
27
+ "Edit",
28
+ "NotebookEdit",
22
29
  "Glob",
23
30
  "Grep",
24
31
  "Skill(openspec:*)",