@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 +1 -1
- package/src/commands/init.js +154 -57
- package/templates/settings.json +7 -0
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -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
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
if (
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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.
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
113
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/templates/settings.json
CHANGED