@haposoft/cafekit 0.9.7 → 0.10.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/bin/install.js +93 -1254
- package/bin/lib/backup.js +111 -0
- package/bin/lib/context.js +273 -0
- package/bin/lib/lock.js +86 -0
- package/bin/lib/managed-writer.js +167 -0
- package/bin/lib/manifest.js +182 -0
- package/bin/lib/ui.js +108 -0
- package/bin/phases/claude-runtime.js +132 -0
- package/bin/phases/claude-settings.js +130 -0
- package/bin/phases/copy-payload.js +220 -0
- package/bin/phases/opencode-runtime.js +38 -0
- package/bin/phases/post-install.js +141 -0
- package/bin/phases/report.js +66 -0
- package/bin/phases/root-config.js +49 -0
- package/bin/phases/select-platform.js +94 -0
- package/bin/phases/summary.js +54 -0
- package/bin/phases/write-metadata.js +60 -0
- package/package.json +5 -1
package/bin/install.js
CHANGED
|
@@ -1,1297 +1,136 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* CafeKit Installer
|
|
5
|
-
* Multi-platform installer for AI coding assistants
|
|
4
|
+
* CafeKit Installer (orchestrator)
|
|
6
5
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
6
|
+
* Thin coordinator that runs the install as a sequence of phase handlers. Each
|
|
7
|
+
* phase receives and returns the run context (`ctx`). Heavy logic lives in
|
|
8
|
+
* bin/phases/* and bin/lib/*. Terminal UX is rendered via ctx.ui (clack in an
|
|
9
|
+
* interactive TTY; plain logs when piped/CI/--yes).
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* 2. Add platform-specific commands in src/[platform]/commands/
|
|
17
|
-
* 3. Update detectPlatforms() if using different folder names
|
|
11
|
+
* Safety features:
|
|
12
|
+
* - Process lock (lib/lock) — refuses to run concurrently.
|
|
13
|
+
* - Pre-run snapshot (lib/backup) — rolls back on a mid-install crash.
|
|
14
|
+
* - Ownership manifest (lib/manifest) — selective update; preserves user edits.
|
|
15
|
+
* - --dry-run — preview only, no filesystem changes.
|
|
18
16
|
*/
|
|
19
17
|
|
|
20
|
-
const
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
24
|
-
const { execSync } = require('child_process');
|
|
25
|
-
const packageJson = require('../package.json');
|
|
26
|
-
const { copyRecursive, isTextAsset, readJsonFile } = require('./lib/copy-utils');
|
|
27
|
-
const {
|
|
28
|
-
normalizeOpenCodeBody,
|
|
29
|
-
getOpenCodeCopyOptions,
|
|
30
|
-
convertOpenCodeAgentContent,
|
|
31
|
-
convertOpenCodeCommandContent,
|
|
32
|
-
copyOpenCodeAgentsMdFile,
|
|
33
|
-
copyOpenCodeSharedRuntimeFiles,
|
|
34
|
-
copyOpenCodePlugins,
|
|
35
|
-
copyOpenCodeCommandTemplates,
|
|
36
|
-
mergeOpenCodeConfig,
|
|
37
|
-
setupOpenCodeModel
|
|
38
|
-
} = require('./lib/opencode-install');
|
|
39
|
-
const INSTALL_COMMAND = `npx ${packageJson.name}@${packageJson.version}`;
|
|
40
|
-
|
|
41
|
-
function validateManifestV2(manifest) {
|
|
42
|
-
if (!manifest || manifest.version !== 2) return false;
|
|
43
|
-
if (!manifest.runtime?.files || !Array.isArray(manifest.runtime.files)) return false;
|
|
44
|
-
if (!manifest.settings?.template) return false;
|
|
45
|
-
return true;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
function loadClaudeMigrationManifest() {
|
|
49
|
-
const manifestPath = path.join(__dirname, '../src/claude/migration-manifest.json');
|
|
50
|
-
|
|
51
|
-
if (!fs.existsSync(manifestPath)) {
|
|
52
|
-
return null;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
try {
|
|
56
|
-
const manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
57
|
-
|
|
58
|
-
if (!validateManifestV2(manifest)) {
|
|
59
|
-
console.error('✗ Invalid manifest v2 schema - missing required fields');
|
|
60
|
-
console.error(' Expected: version=2, runtime.files[], settings.template');
|
|
61
|
-
process.exit(1);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
return manifest;
|
|
65
|
-
} catch (error) {
|
|
66
|
-
console.warn(`⚠ Failed to parse Claude migration manifest: ${error.message}`);
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const CLAUDE_MIGRATION_MANIFEST = loadClaudeMigrationManifest();
|
|
72
|
-
|
|
73
|
-
const DEPENDENCY_TEMPLATES = {
|
|
74
|
-
commands: {
|
|
75
|
-
claude: {},
|
|
76
|
-
opencode: {}
|
|
77
|
-
},
|
|
78
|
-
agents: {
|
|
79
|
-
claude: {},
|
|
80
|
-
opencode: {}
|
|
81
|
-
}
|
|
82
|
-
};
|
|
83
|
-
|
|
84
|
-
// ═══════════════════════════════════════════════════════════
|
|
85
|
-
// PLATFORM REGISTRY - Add new platforms here
|
|
86
|
-
// ═══════════════════════════════════════════════════════════
|
|
87
|
-
const PLATFORMS = {
|
|
88
|
-
claude: {
|
|
89
|
-
id: 'claude',
|
|
90
|
-
name: 'Claude Code',
|
|
91
|
-
description: 'Anthropic\'s Claude Code CLI',
|
|
92
|
-
folder: '.claude',
|
|
93
|
-
commandsDir: '.claude/commands',
|
|
94
|
-
skillsDir: '.claude/skills',
|
|
95
|
-
agentsDir: '.claude/agents',
|
|
96
|
-
skillsRef: '.claude/skills',
|
|
97
|
-
commandPrefix: '/',
|
|
98
|
-
sourceDir: 'claude', // Maps to src/claude/
|
|
99
|
-
sourceSubdir: 'commands' // Source subfolder within src/claude/
|
|
100
|
-
},
|
|
101
|
-
opencode: {
|
|
102
|
-
id: 'opencode',
|
|
103
|
-
name: 'OpenCode',
|
|
104
|
-
description: 'OpenCode terminal AI coding agent',
|
|
105
|
-
folder: '.opencode',
|
|
106
|
-
detectFiles: ['.opencode', 'opencode.json', 'opencode.jsonc'],
|
|
107
|
-
commandsDir: '.opencode/commands',
|
|
108
|
-
skillsDir: '.opencode/skills',
|
|
109
|
-
agentsDir: '.opencode/agents',
|
|
110
|
-
skillsRef: '.opencode/skills',
|
|
111
|
-
commandPrefix: '/',
|
|
112
|
-
sourceDir: 'claude',
|
|
113
|
-
sourceSubdir: 'archive-command'
|
|
114
|
-
}
|
|
115
|
-
// Add new platforms here:
|
|
116
|
-
// cursor: {
|
|
117
|
-
// id: 'cursor',
|
|
118
|
-
// name: 'Cursor',
|
|
119
|
-
// description: 'Cursor IDE',
|
|
120
|
-
// folder: '.cursor',
|
|
121
|
-
// commandsDir: '.cursor/commands',
|
|
122
|
-
// sourceDir: 'cursor',
|
|
123
|
-
// sourceSubdir: 'commands'
|
|
124
|
-
// }
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
// ═══════════════════════════════════════════════════════════
|
|
128
|
-
// DETECTION
|
|
129
|
-
// ═══════════════════════════════════════════════════════════
|
|
130
|
-
|
|
131
|
-
function detectPlatforms() {
|
|
132
|
-
const detected = [];
|
|
133
|
-
|
|
134
|
-
for (const [key, config] of Object.entries(PLATFORMS)) {
|
|
135
|
-
const markers = Array.isArray(config.detectFiles) ? config.detectFiles : [config.folder];
|
|
136
|
-
if (markers.some(marker => fs.existsSync(marker))) {
|
|
137
|
-
detected.push(key);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
return detected;
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function formatPlatformList() {
|
|
145
|
-
return Object.entries(PLATFORMS)
|
|
146
|
-
.map(([key, config], index) => {
|
|
147
|
-
return `${index + 1}) ${config.name} (${config.folder}/)\n ${config.description}`;
|
|
148
|
-
})
|
|
149
|
-
.join('\n');
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function getPlatformKeys() {
|
|
153
|
-
return Object.keys(PLATFORMS);
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
// Warn opencode-only installs when a legacy .claude/ folder remains from
|
|
157
|
-
// pre-0.9.0 installs (when OpenCode shared the .claude/ runtime). We do not
|
|
158
|
-
// delete anything; user must remove it explicitly to opt in.
|
|
159
|
-
function warnLegacyClaudeFolder(platforms) {
|
|
160
|
-
if (!platforms.includes('opencode')) return;
|
|
161
|
-
if (platforms.includes('claude')) return;
|
|
162
|
-
if (!fs.existsSync('.claude')) return;
|
|
163
|
-
|
|
164
|
-
console.log('');
|
|
165
|
-
console.log('⚠ Legacy .claude/ folder detected.');
|
|
166
|
-
console.log(' As of CafeKit 0.9.0, OpenCode installs are self-contained under .opencode/.');
|
|
167
|
-
console.log(' The .claude/ folder from older installs is no longer used by OpenCode.');
|
|
168
|
-
console.log(' After this install finishes you can remove it: rm -rf .claude');
|
|
169
|
-
console.log('');
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function isClaudeCompatibleRuntime(platformKey) {
|
|
173
|
-
return platformKey === 'claude' || platformKey === 'opencode';
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
function getRuntimeSupportTargetDir(platformKey, subdir) {
|
|
177
|
-
return path.join(PLATFORMS[platformKey].folder, subdir);
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function parseInstallerArgs(argv) {
|
|
181
|
-
const args = {
|
|
182
|
-
upgrade: false
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
for (let i = 2; i < argv.length; i++) {
|
|
186
|
-
const arg = argv[i];
|
|
187
|
-
|
|
188
|
-
if (arg === '--upgrade' || arg === '-u' || arg === '--force' || arg === '-f') {
|
|
189
|
-
args.upgrade = true;
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
return args;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
function getCopyOptions(platformKey, baseOptions = {}) {
|
|
197
|
-
if (platformKey === 'opencode') {
|
|
198
|
-
return getOpenCodeCopyOptions(baseOptions);
|
|
199
|
-
}
|
|
200
|
-
return baseOptions;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
// ═══════════════════════════════════════════════════════════
|
|
204
|
-
// USER INTERACTION
|
|
205
|
-
// ═══════════════════════════════════════════════════════════
|
|
206
|
-
|
|
207
|
-
async function promptPlatformSelection() {
|
|
208
|
-
const rl = readline.createInterface({
|
|
209
|
-
input: process.stdin,
|
|
210
|
-
output: process.stdout
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
const maxChoice = Object.keys(PLATFORMS).length + 1;
|
|
214
|
-
|
|
215
|
-
console.log('╔════════════════════════════════════════════════════════╗');
|
|
216
|
-
console.log('║ CafeKit - Platform Selection ║');
|
|
217
|
-
console.log('╚════════════════════════════════════════════════════════╝');
|
|
218
|
-
console.log();
|
|
219
|
-
console.log('No existing AI editor configuration detected.\n');
|
|
220
|
-
console.log('Select which platform(s) to install for:\n');
|
|
221
|
-
console.log(formatPlatformList());
|
|
222
|
-
console.log(`${maxChoice}) All platforms`);
|
|
223
|
-
console.log('0) Cancel');
|
|
224
|
-
console.log();
|
|
225
|
-
|
|
226
|
-
return new Promise((resolve) => {
|
|
227
|
-
rl.question(`Select (0-${maxChoice}): `, (answer) => {
|
|
228
|
-
rl.close();
|
|
229
|
-
const choice = parseInt(answer.trim(), 10);
|
|
230
|
-
|
|
231
|
-
if (choice === 0 || isNaN(choice)) {
|
|
232
|
-
resolve([]);
|
|
233
|
-
} else if (choice === maxChoice) {
|
|
234
|
-
resolve(getPlatformKeys());
|
|
235
|
-
} else if (choice >= 1 && choice <= Object.keys(PLATFORMS).length) {
|
|
236
|
-
resolve([getPlatformKeys()[choice - 1]]);
|
|
237
|
-
} else {
|
|
238
|
-
console.log('Invalid selection. Please run the installer again.');
|
|
239
|
-
resolve([]);
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
async function promptMultiPlatformConfirm(detected) {
|
|
246
|
-
const rl = readline.createInterface({
|
|
247
|
-
input: process.stdin,
|
|
248
|
-
output: process.stdout
|
|
249
|
-
});
|
|
250
|
-
|
|
251
|
-
const platformNames = detected.map(key => PLATFORMS[key].name).join(', ');
|
|
252
|
-
|
|
253
|
-
console.log(`\nDetected existing configurations: ${platformNames}`);
|
|
254
|
-
console.log();
|
|
255
|
-
|
|
256
|
-
return new Promise((resolve) => {
|
|
257
|
-
rl.question('Install for all detected platforms? (Y/n): ', (answer) => {
|
|
258
|
-
rl.close();
|
|
259
|
-
const response = answer.trim().toLowerCase();
|
|
260
|
-
resolve(response === '' || response === 'y' || response === 'yes');
|
|
261
|
-
});
|
|
262
|
-
});
|
|
263
|
-
}
|
|
18
|
+
const { buildContext, PLATFORMS, packageJson } = require('./lib/context');
|
|
19
|
+
const lock = require('./lib/lock');
|
|
20
|
+
const backup = require('./lib/backup');
|
|
21
|
+
const manifestLib = require('./lib/manifest');
|
|
264
22
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return;
|
|
284
|
-
}
|
|
285
|
-
|
|
286
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
287
|
-
fs.writeFileSync(targetPath, content, 'utf8');
|
|
288
|
-
|
|
289
|
-
if (destinationExists && shouldOverwriteManagedFiles) {
|
|
290
|
-
console.log(` ↻ Dependency updated: ${label}`);
|
|
291
|
-
results.updated++;
|
|
292
|
-
} else {
|
|
293
|
-
console.log(` ✓ Dependency installed: ${label}`);
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
results.dependencyChecks++;
|
|
297
|
-
results.installedDependencies++;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function ensureWorkflowDependencies(platformKey, platform, results, options = {}) {
|
|
301
|
-
const commandTemplates = DEPENDENCY_TEMPLATES.commands[platformKey] || {};
|
|
302
|
-
Object.entries(commandTemplates).forEach(([fileName, content]) => {
|
|
303
|
-
const targetPath = path.join(platform.commandsDir, fileName);
|
|
304
|
-
ensureDependencyFile(targetPath, content, results, path.join(platform.commandsDir, fileName), options);
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
const agentTemplates = DEPENDENCY_TEMPLATES.agents[platformKey] || {};
|
|
308
|
-
Object.entries(agentTemplates).forEach(([fileName, content]) => {
|
|
309
|
-
const targetPath = path.join(platform.agentsDir, fileName);
|
|
310
|
-
ensureDependencyFile(targetPath, content, results, path.join(platform.agentsDir, fileName), options);
|
|
311
|
-
});
|
|
312
|
-
}
|
|
313
|
-
|
|
314
|
-
function writePlatformVersionMetadata(platformKey, results) {
|
|
23
|
+
const { resolvePlatforms } = require('./phases/select-platform');
|
|
24
|
+
const { copyPlatformFiles } = require('./phases/copy-payload');
|
|
25
|
+
const {
|
|
26
|
+
copyRoutingFile,
|
|
27
|
+
copyClaudeRuntimeFiles,
|
|
28
|
+
removeObsoleteClaudeRuntimeFiles,
|
|
29
|
+
copyClaudeMdFile,
|
|
30
|
+
copyRulesDirectory
|
|
31
|
+
} = require('./phases/claude-runtime');
|
|
32
|
+
const { mergeClaudeSettings } = require('./phases/claude-settings');
|
|
33
|
+
const { installOpenCodeRuntime } = require('./phases/opencode-runtime');
|
|
34
|
+
const { writePlatformVersionMetadata } = require('./phases/write-metadata');
|
|
35
|
+
const { ensureGitignore } = require('./phases/root-config');
|
|
36
|
+
const { runPostInstall } = require('./phases/post-install');
|
|
37
|
+
const { printSummary } = require('./phases/summary');
|
|
38
|
+
|
|
39
|
+
/** Install a single platform: payload + runtime + metadata, under one spinner. */
|
|
40
|
+
function installPlatform(ctx, platformKey) {
|
|
315
41
|
const platform = PLATFORMS[platformKey];
|
|
316
|
-
const targetPath = path.join(platform.folder, 'cafekit.json');
|
|
317
|
-
const targetExists = fs.existsSync(targetPath);
|
|
318
|
-
const existingMetadata = readJsonFile(targetPath);
|
|
319
|
-
const now = new Date().toISOString();
|
|
320
|
-
const previousVersion = typeof existingMetadata.version === 'string'
|
|
321
|
-
? existingMetadata.version
|
|
322
|
-
: null;
|
|
323
42
|
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
version: packageJson.version,
|
|
328
|
-
platform: platform.id,
|
|
329
|
-
platformName: platform.name,
|
|
330
|
-
installedAt: existingMetadata.installedAt || now,
|
|
331
|
-
lastInstalledAt: now,
|
|
332
|
-
installCommand: INSTALL_COMMAND
|
|
333
|
-
};
|
|
43
|
+
// Read ownership baseline + start a fresh tracker for this platform.
|
|
44
|
+
ctx.ownership[platform.folder] = manifestLib.read(platform.folder);
|
|
45
|
+
ctx.trackers[platformKey] = manifestLib.createTracker(platform.folder, packageJson.version);
|
|
334
46
|
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
}
|
|
47
|
+
const before = { copied: ctx.results.copied, updated: ctx.results.updated, skills: ctx.results.installedSkills };
|
|
48
|
+
ctx.ui.startSpinner(`Installing ${platform.name} (${platform.folder}/)`);
|
|
338
49
|
|
|
339
|
-
|
|
340
|
-
fs.writeFileSync(targetPath, `${JSON.stringify(metadata, null, 2)}\n`, 'utf8');
|
|
50
|
+
copyPlatformFiles(ctx, platformKey);
|
|
341
51
|
|
|
342
|
-
if (targetExists) {
|
|
343
|
-
console.log(` ↻ Version metadata updated: ${targetPath}`);
|
|
344
|
-
results.updated++;
|
|
345
|
-
} else {
|
|
346
|
-
console.log(` ✓ Version metadata installed: ${targetPath}`);
|
|
347
|
-
results.copied++;
|
|
348
|
-
}
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
function getPlatformSpecFiles(platformKey) {
|
|
352
52
|
if (platformKey === 'claude') {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
'spec-init.md',
|
|
360
|
-
'spec-requirements.md',
|
|
361
|
-
'spec-design.md',
|
|
362
|
-
'spec-validate.md',
|
|
363
|
-
'spec-tasks.md',
|
|
364
|
-
'spec-status.md',
|
|
365
|
-
'code.md',
|
|
366
|
-
'test.md',
|
|
367
|
-
'docs.md'
|
|
368
|
-
];
|
|
53
|
+
copyRoutingFile(ctx, platformKey);
|
|
54
|
+
copyClaudeRuntimeFiles(ctx, platformKey);
|
|
55
|
+
removeObsoleteClaudeRuntimeFiles(ctx, platformKey);
|
|
56
|
+
mergeClaudeSettings(ctx, platformKey);
|
|
57
|
+
copyClaudeMdFile(ctx, platformKey);
|
|
58
|
+
copyRulesDirectory(ctx, platformKey);
|
|
369
59
|
}
|
|
370
60
|
|
|
371
61
|
if (platformKey === 'opencode') {
|
|
372
|
-
|
|
62
|
+
installOpenCodeRuntime(ctx, platformKey);
|
|
373
63
|
}
|
|
374
64
|
|
|
375
|
-
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
function copyPlatformFiles(platformKey, results, options = {}) {
|
|
379
|
-
const platform = PLATFORMS[platformKey];
|
|
380
|
-
const shouldOverwriteManagedFiles = Boolean(options.upgrade);
|
|
381
|
-
const recordWriteResult = (didOverwrite) => {
|
|
382
|
-
if (didOverwrite) {
|
|
383
|
-
results.updated++;
|
|
384
|
-
} else {
|
|
385
|
-
results.copied++;
|
|
386
|
-
}
|
|
387
|
-
};
|
|
388
|
-
|
|
389
|
-
// Source directories - support different subfolder names per platform
|
|
390
|
-
const sourceSubdir = platform.sourceSubdir || 'commands';
|
|
391
|
-
const commandsSourceDir = path.join(__dirname, `../src/${platform.sourceDir}/${sourceSubdir}`);
|
|
392
|
-
const skillsSourceDir = path.join(__dirname, '../src/claude/skills');
|
|
393
|
-
const agentsSourceDir = path.join(__dirname, `../src/${platform.sourceDir}/agents`);
|
|
394
|
-
|
|
395
|
-
// Create directories
|
|
396
|
-
// commandsDir is created on-demand per platform: Claude only mkdirs when
|
|
397
|
-
// getPlatformSpecFiles returns files; OpenCode mkdirs inside
|
|
398
|
-
// copyOpenCodeCommandTemplates. Skip the unconditional mkdir to avoid
|
|
399
|
-
// leaving empty commands/ folders behind.
|
|
400
|
-
if (!fs.existsSync(platform.skillsDir)) {
|
|
401
|
-
fs.mkdirSync(platform.skillsDir, { recursive: true });
|
|
402
|
-
}
|
|
403
|
-
if (!fs.existsSync(platform.agentsDir)) {
|
|
404
|
-
fs.mkdirSync(platform.agentsDir, { recursive: true });
|
|
405
|
-
}
|
|
406
|
-
|
|
407
|
-
// Copy skills (shared across all platforms)
|
|
408
|
-
if (fs.existsSync(skillsSourceDir)) {
|
|
409
|
-
const specSkillSource = path.join(skillsSourceDir, 'specs');
|
|
410
|
-
const specSkillDest = path.join(platform.skillsDir, 'specs');
|
|
411
|
-
|
|
412
|
-
if (fs.existsSync(specSkillSource)) {
|
|
413
|
-
const skillExisted = fs.existsSync(specSkillDest);
|
|
414
|
-
copyRecursive(specSkillSource, specSkillDest, getCopyOptions(platformKey, options));
|
|
415
|
-
results.installedSkills++;
|
|
416
|
-
|
|
417
|
-
if (shouldOverwriteManagedFiles && skillExisted) {
|
|
418
|
-
console.log(` ↻ Skill updated: specs`);
|
|
419
|
-
results.updated++;
|
|
420
|
-
} else {
|
|
421
|
-
console.log(` ✓ Skill installed: specs`);
|
|
422
|
-
}
|
|
423
|
-
}
|
|
424
|
-
|
|
425
|
-
// Keep templates in sync for claude command runtime copies under .claude/skills/specs
|
|
426
|
-
if (platformKey === 'claude') {
|
|
427
|
-
const legacyInitTemplate = path.join(platform.skillsDir, 'specs', 'templates', 'init.json');
|
|
428
|
-
if (fs.existsSync(legacyInitTemplate)) {
|
|
429
|
-
fs.rmSync(legacyInitTemplate, { force: true });
|
|
430
|
-
console.log(` ↻ Removed legacy template: ${legacyInitTemplate}`);
|
|
431
|
-
results.updated++;
|
|
432
|
-
}
|
|
433
|
-
|
|
434
|
-
const specTemplates = [
|
|
435
|
-
'spec-state.json',
|
|
436
|
-
'requirements-init.md',
|
|
437
|
-
'requirements.md',
|
|
438
|
-
'design.md',
|
|
439
|
-
'research.md',
|
|
440
|
-
'task.md'
|
|
441
|
-
];
|
|
442
|
-
|
|
443
|
-
specTemplates.forEach((fileName) => {
|
|
444
|
-
const source = path.join(specSkillSource, 'templates', fileName);
|
|
445
|
-
const dest = path.join(platform.skillsDir, 'specs', 'templates', fileName);
|
|
446
|
-
|
|
447
|
-
if (!fs.existsSync(source)) {
|
|
448
|
-
console.log(` ⚠ Missing dependency template: ${path.join(platform.skillsDir, 'specs', 'templates', fileName)}`);
|
|
449
|
-
results.missingDependencies++;
|
|
450
|
-
results.dependencyChecks++;
|
|
451
|
-
return;
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
const destinationExists = fs.existsSync(dest);
|
|
455
|
-
|
|
456
|
-
if (destinationExists && !shouldOverwriteManagedFiles) {
|
|
457
|
-
console.log(` → Dependency exists: ${path.join(platform.skillsDir, 'specs', 'templates', fileName)}`);
|
|
458
|
-
results.dependencyChecks++;
|
|
459
|
-
return;
|
|
460
|
-
}
|
|
65
|
+
writePlatformVersionMetadata(ctx, platformKey);
|
|
461
66
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
results.installedDependencies++;
|
|
466
|
-
|
|
467
|
-
if (destinationExists && shouldOverwriteManagedFiles) {
|
|
468
|
-
console.log(` ↻ Dependency updated: ${path.join(platform.skillsDir, 'specs', 'templates', fileName)}`);
|
|
469
|
-
results.updated++;
|
|
470
|
-
} else {
|
|
471
|
-
console.log(` ✓ Dependency installed: ${path.join(platform.skillsDir, 'specs', 'templates', fileName)}`);
|
|
472
|
-
}
|
|
473
|
-
});
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
// Copy additional required skills based on platform
|
|
477
|
-
let requiredSkills = [];
|
|
478
|
-
if (isClaudeCompatibleRuntime(platformKey)) {
|
|
479
|
-
requiredSkills = CLAUDE_MIGRATION_MANIFEST?.skills?.required || [];
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
requiredSkills
|
|
483
|
-
.filter((skillName) => skillName !== 'specs')
|
|
484
|
-
.forEach((skillName) => {
|
|
485
|
-
const skillSource = path.join(skillsSourceDir, skillName);
|
|
486
|
-
const skillDest = path.join(platform.skillsDir, skillName);
|
|
487
|
-
|
|
488
|
-
if (fs.existsSync(skillSource)) {
|
|
489
|
-
const skillExisted = fs.existsSync(skillDest);
|
|
490
|
-
copyRecursive(skillSource, skillDest, getCopyOptions(platformKey, options));
|
|
491
|
-
results.installedSkills++;
|
|
492
|
-
|
|
493
|
-
if (shouldOverwriteManagedFiles && skillExisted) {
|
|
494
|
-
console.log(` ↻ Skill updated: ${skillName}`);
|
|
495
|
-
results.updated++;
|
|
496
|
-
} else {
|
|
497
|
-
console.log(` ✓ Skill installed: ${skillName}`);
|
|
498
|
-
}
|
|
499
|
-
} else {
|
|
500
|
-
results.missingDependencies++;
|
|
501
|
-
console.log(` ⚠ Missing dependency template: ${path.join(platform.skillsDir, skillName)}`);
|
|
502
|
-
}
|
|
503
|
-
});
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
// Copy agents
|
|
507
|
-
if (isClaudeCompatibleRuntime(platformKey)) {
|
|
508
|
-
if (fs.existsSync(agentsSourceDir)) {
|
|
509
|
-
const requiredAgents = CLAUDE_MIGRATION_MANIFEST?.agents?.required || [
|
|
510
|
-
'tester.md',
|
|
511
|
-
'code-reviewer.md',
|
|
512
|
-
'fullstack-developer.md',
|
|
513
|
-
'debugger.md'
|
|
514
|
-
];
|
|
515
|
-
|
|
516
|
-
requiredAgents.forEach((fileName) => {
|
|
517
|
-
const source = path.join(agentsSourceDir, fileName);
|
|
518
|
-
const dest = path.join(platform.agentsDir, fileName);
|
|
519
|
-
|
|
520
|
-
if (!fs.existsSync(source)) {
|
|
521
|
-
console.log(` ⚠ Missing dependency template: ${path.join(platform.agentsDir, fileName)}`);
|
|
522
|
-
results.missingDependencies++;
|
|
523
|
-
results.dependencyChecks++;
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
|
|
527
|
-
const destinationExists = fs.existsSync(dest);
|
|
528
|
-
|
|
529
|
-
if (destinationExists && !shouldOverwriteManagedFiles) {
|
|
530
|
-
console.log(` → Dependency exists: ${path.join(platform.agentsDir, fileName)}`);
|
|
531
|
-
results.dependencyChecks++;
|
|
532
|
-
return;
|
|
533
|
-
}
|
|
534
|
-
|
|
535
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
536
|
-
if (platformKey === 'opencode') {
|
|
537
|
-
const content = fs.readFileSync(source, 'utf8');
|
|
538
|
-
fs.writeFileSync(dest, convertOpenCodeAgentContent(content, fileName), 'utf8');
|
|
539
|
-
} else {
|
|
540
|
-
fs.copyFileSync(source, dest);
|
|
541
|
-
}
|
|
542
|
-
results.dependencyChecks++;
|
|
543
|
-
results.installedDependencies++;
|
|
544
|
-
|
|
545
|
-
if (destinationExists && shouldOverwriteManagedFiles) {
|
|
546
|
-
console.log(` ↻ Dependency updated: ${path.join(platform.agentsDir, fileName)}`);
|
|
547
|
-
results.updated++;
|
|
548
|
-
} else {
|
|
549
|
-
console.log(` ✓ Dependency installed: ${path.join(platform.agentsDir, fileName)}`);
|
|
550
|
-
}
|
|
551
|
-
});
|
|
552
|
-
|
|
553
|
-
// Copy agent reference manuals (debugger manuals, etc.) recursively
|
|
554
|
-
const refsSource = path.join(__dirname, '../src', platform.sourceDir, 'references');
|
|
555
|
-
if (fs.existsSync(refsSource)) {
|
|
556
|
-
const refsDest = getRuntimeSupportTargetDir(platformKey, 'references');
|
|
557
|
-
const refsExisted = fs.existsSync(refsDest);
|
|
558
|
-
copyRecursive(refsSource, refsDest, getCopyOptions(platformKey, options));
|
|
559
|
-
|
|
560
|
-
if (shouldOverwriteManagedFiles && refsExisted) {
|
|
561
|
-
console.log(` ↻ Agent reference manuals updated`);
|
|
562
|
-
results.updated++;
|
|
563
|
-
} else {
|
|
564
|
-
console.log(` ✓ Agent reference manuals installed`);
|
|
565
|
-
results.copied++;
|
|
566
|
-
}
|
|
567
|
-
}
|
|
568
|
-
|
|
569
|
-
} else {
|
|
570
|
-
results.missingDependencies++;
|
|
571
|
-
console.log(` ⚠ Missing dependency template: ${platform.agentsDir}`);
|
|
572
|
-
}
|
|
67
|
+
const wrote = (ctx.results.copied - before.copied) + (ctx.results.updated - before.updated);
|
|
68
|
+
const verb = ctx.dryRun ? 'would install' : 'installed';
|
|
69
|
+
ctx.ui.stopSpinner(`${platform.name} ${verb} — ${wrote} file(s), ${ctx.results.installedSkills - before.skills} skill(s)`);
|
|
573
70
|
|
|
574
|
-
|
|
575
|
-
const scriptsSourceDir = path.join(__dirname, '../src/claude/scripts');
|
|
576
|
-
if (fs.existsSync(scriptsSourceDir)) {
|
|
577
|
-
const scriptsDest = getRuntimeSupportTargetDir(platformKey, 'scripts');
|
|
578
|
-
const scriptsExisted = fs.existsSync(scriptsDest);
|
|
579
|
-
copyRecursive(scriptsSourceDir, scriptsDest, getCopyOptions(platformKey, options));
|
|
580
|
-
|
|
581
|
-
if (shouldOverwriteManagedFiles && scriptsExisted) {
|
|
582
|
-
console.log(` ↻ Native scripts updated`);
|
|
583
|
-
results.updated++;
|
|
584
|
-
} else {
|
|
585
|
-
console.log(` ✓ Native scripts installed`);
|
|
586
|
-
results.copied++;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
}
|
|
591
|
-
|
|
592
|
-
ensureWorkflowDependencies(platformKey, platform, results, options);
|
|
593
|
-
|
|
594
|
-
// Copy commands/workflows
|
|
595
|
-
const specFiles = getPlatformSpecFiles(platformKey);
|
|
596
|
-
|
|
597
|
-
specFiles.forEach(file => {
|
|
598
|
-
const source = path.join(commandsSourceDir, file);
|
|
599
|
-
const dest = path.join(platform.commandsDir, file);
|
|
600
|
-
const destinationExists = fs.existsSync(dest);
|
|
601
|
-
|
|
602
|
-
if (!fs.existsSync(source)) {
|
|
603
|
-
console.error(` ✗ Error: Source file not found: ${file}`);
|
|
604
|
-
results.errors++;
|
|
605
|
-
return;
|
|
606
|
-
}
|
|
607
|
-
|
|
608
|
-
if (destinationExists && !shouldOverwriteManagedFiles) {
|
|
609
|
-
console.log(` → Skipped: ${file} (already exists)`);
|
|
610
|
-
results.skipped++;
|
|
611
|
-
return;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
615
|
-
let content = fs.readFileSync(source, 'utf8');
|
|
616
|
-
content = content.replace(/\{\{SKILLS_DIR\}\}/g, platform.skillsRef);
|
|
617
|
-
if (platformKey === 'opencode') {
|
|
618
|
-
content = convertOpenCodeCommandContent(content, file);
|
|
619
|
-
}
|
|
620
|
-
fs.writeFileSync(dest, content);
|
|
621
|
-
|
|
622
|
-
if (destinationExists && shouldOverwriteManagedFiles) {
|
|
623
|
-
console.log(` ↻ Updated: ${file}`);
|
|
624
|
-
recordWriteResult(true);
|
|
625
|
-
} else {
|
|
626
|
-
console.log(` ✓ Copied: ${file}`);
|
|
627
|
-
recordWriteResult(false);
|
|
628
|
-
}
|
|
629
|
-
});
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
// Copy ROUTING.md to .claude/
|
|
633
|
-
function copyRoutingFile(platformKey, results, options = {}) {
|
|
634
|
-
const platform = PLATFORMS[platformKey];
|
|
635
|
-
const shouldOverwriteManagedFiles = Boolean(options.upgrade);
|
|
636
|
-
const source = path.join(__dirname, `../src/${platform.sourceDir}/ROUTING.md`);
|
|
637
|
-
const dest = path.join(platform.folder, 'ROUTING.md');
|
|
638
|
-
|
|
639
|
-
if (fs.existsSync(source)) {
|
|
640
|
-
const destinationExists = fs.existsSync(dest);
|
|
641
|
-
|
|
642
|
-
if (destinationExists && !shouldOverwriteManagedFiles) {
|
|
643
|
-
console.log(` → Skipped: ROUTING.md (already exists)`);
|
|
644
|
-
results.skipped++;
|
|
645
|
-
return;
|
|
646
|
-
}
|
|
647
|
-
|
|
648
|
-
let content = fs.readFileSync(source, 'utf8');
|
|
649
|
-
content = content.replace(/\{\{SKILLS_DIR\}\}/g, platform.skillsRef);
|
|
650
|
-
if (platformKey === 'opencode') {
|
|
651
|
-
content = normalizeOpenCodeBody(content);
|
|
652
|
-
}
|
|
653
|
-
fs.writeFileSync(dest, content);
|
|
654
|
-
|
|
655
|
-
if (destinationExists && shouldOverwriteManagedFiles) {
|
|
656
|
-
console.log(` ↻ Updated: ROUTING.md`);
|
|
657
|
-
results.updated++;
|
|
658
|
-
} else {
|
|
659
|
-
console.log(` ✓ Copied: ROUTING.md`);
|
|
660
|
-
results.copied++;
|
|
661
|
-
}
|
|
662
|
-
}
|
|
663
|
-
}
|
|
664
|
-
|
|
665
|
-
// Copy Claude runtime files (statusline bundle)
|
|
666
|
-
function copyClaudeRuntimeFiles(platformKey, results, options = {}) {
|
|
667
|
-
if (platformKey !== 'claude') return;
|
|
668
|
-
|
|
669
|
-
const manifest = CLAUDE_MIGRATION_MANIFEST;
|
|
670
|
-
if (!manifest?.runtime?.files) return;
|
|
671
|
-
|
|
672
|
-
const shouldOverwriteManagedFiles = Boolean(options.upgrade);
|
|
673
|
-
const srcBase = path.join(__dirname, '../src/claude');
|
|
674
|
-
const targetBase = path.join(PLATFORMS.claude.folder);
|
|
675
|
-
|
|
676
|
-
manifest.runtime.files.forEach(relPath => {
|
|
677
|
-
const srcPath = path.join(srcBase, relPath);
|
|
678
|
-
const targetRelPath = relPath === 'gitignore' ? '.gitignore' : relPath;
|
|
679
|
-
const targetPath = path.join(targetBase, targetRelPath);
|
|
680
|
-
|
|
681
|
-
if (!fs.existsSync(srcPath)) {
|
|
682
|
-
console.log(` ⚠ Runtime file not found: ${relPath}`);
|
|
683
|
-
results.missingDependencies++;
|
|
684
|
-
return;
|
|
685
|
-
}
|
|
686
|
-
|
|
687
|
-
const targetExists = fs.existsSync(targetPath);
|
|
688
|
-
const shouldCopy = shouldOverwriteManagedFiles || !targetExists;
|
|
689
|
-
|
|
690
|
-
if (shouldCopy) {
|
|
691
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
692
|
-
fs.copyFileSync(srcPath, targetPath);
|
|
693
|
-
|
|
694
|
-
if (targetExists) {
|
|
695
|
-
console.log(` ↻ Runtime updated: ${targetRelPath}`);
|
|
696
|
-
results.updated++;
|
|
697
|
-
} else {
|
|
698
|
-
console.log(` ✓ Runtime installed: ${targetRelPath}`);
|
|
699
|
-
results.copied++;
|
|
700
|
-
}
|
|
701
|
-
} else {
|
|
702
|
-
results.skipped++;
|
|
703
|
-
}
|
|
704
|
-
});
|
|
71
|
+
ctx.results.targets.push(platform.commandsDir);
|
|
705
72
|
}
|
|
706
73
|
|
|
707
|
-
function
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
const targetPath = path.join(targetBase, relPath);
|
|
715
|
-
|
|
716
|
-
if (!fs.existsSync(targetPath)) {
|
|
717
|
-
return;
|
|
718
|
-
}
|
|
719
|
-
|
|
720
|
-
fs.rmSync(targetPath, { force: true });
|
|
721
|
-
console.log(` ↻ Removed obsolete runtime: ${relPath}`);
|
|
722
|
-
results.updated++;
|
|
723
|
-
});
|
|
724
|
-
}
|
|
725
|
-
|
|
726
|
-
function pruneObsoleteSettingsHooks(settings, results) {
|
|
727
|
-
const obsoleteSubstrings = CLAUDE_MIGRATION_MANIFEST?.obsolete?.settingsHookCommandSubstrings || [];
|
|
728
|
-
|
|
729
|
-
if (!settings.hooks || obsoleteSubstrings.length === 0) {
|
|
730
|
-
return settings;
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
let removedCount = 0;
|
|
734
|
-
const prunedHooks = {};
|
|
735
|
-
|
|
736
|
-
Object.entries(settings.hooks).forEach(([eventName, matcherEntries]) => {
|
|
737
|
-
if (!Array.isArray(matcherEntries)) {
|
|
738
|
-
prunedHooks[eventName] = matcherEntries;
|
|
739
|
-
return;
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
const prunedEntries = [];
|
|
743
|
-
|
|
744
|
-
matcherEntries.forEach(entry => {
|
|
745
|
-
if (!Array.isArray(entry?.hooks)) {
|
|
746
|
-
prunedEntries.push(entry);
|
|
747
|
-
return;
|
|
748
|
-
}
|
|
749
|
-
|
|
750
|
-
const remainingHooks = entry.hooks.filter(hook => {
|
|
751
|
-
const command = hook?.command || '';
|
|
752
|
-
const isObsolete = obsoleteSubstrings.some(substring => command.includes(substring));
|
|
753
|
-
|
|
754
|
-
if (isObsolete) {
|
|
755
|
-
removedCount++;
|
|
756
|
-
}
|
|
757
|
-
|
|
758
|
-
return !isObsolete;
|
|
759
|
-
});
|
|
760
|
-
|
|
761
|
-
if (remainingHooks.length > 0) {
|
|
762
|
-
prunedEntries.push({ ...entry, hooks: remainingHooks });
|
|
763
|
-
}
|
|
764
|
-
});
|
|
765
|
-
|
|
766
|
-
if (prunedEntries.length > 0) {
|
|
767
|
-
prunedHooks[eventName] = prunedEntries;
|
|
768
|
-
}
|
|
769
|
-
});
|
|
770
|
-
|
|
771
|
-
if (removedCount > 0) {
|
|
772
|
-
console.log(` ↻ Settings: removed ${removedCount} obsolete hook(s)`);
|
|
773
|
-
results.updated++;
|
|
774
|
-
return { ...settings, hooks: prunedHooks };
|
|
775
|
-
}
|
|
776
|
-
|
|
777
|
-
return settings;
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
// Merge Claude settings.json
|
|
781
|
-
function mergeClaudeSettings(platformKey, results, options = {}) {
|
|
782
|
-
if (platformKey !== 'claude') return;
|
|
783
|
-
|
|
784
|
-
const manifest = CLAUDE_MIGRATION_MANIFEST;
|
|
785
|
-
if (!manifest?.settings?.template) return;
|
|
786
|
-
|
|
787
|
-
const templatePath = path.join(__dirname, '../src/claude', manifest.settings.template);
|
|
788
|
-
const targetPath = path.join(PLATFORMS.claude.folder, 'settings.json');
|
|
789
|
-
|
|
790
|
-
if (!fs.existsSync(templatePath)) {
|
|
791
|
-
console.log(` ⚠ Settings template not found: ${manifest.settings.template}`);
|
|
792
|
-
return;
|
|
793
|
-
}
|
|
794
|
-
|
|
795
|
-
const managedSettings = JSON.parse(fs.readFileSync(templatePath, 'utf8'));
|
|
796
|
-
let existingSettings = {};
|
|
797
|
-
|
|
798
|
-
if (fs.existsSync(targetPath)) {
|
|
799
|
-
existingSettings = JSON.parse(fs.readFileSync(targetPath, 'utf8'));
|
|
800
|
-
}
|
|
801
|
-
|
|
802
|
-
const mergedSettings = pruneObsoleteSettingsHooks({ ...existingSettings }, results);
|
|
803
|
-
|
|
804
|
-
// Merge statusLine
|
|
805
|
-
if (managedSettings.statusLine) {
|
|
806
|
-
const existingCommand = existingSettings.statusLine?.command || '';
|
|
807
|
-
const isCafeKitOwned = existingCommand.includes('status.cjs') || existingCommand.includes('statusline.cjs');
|
|
808
|
-
|
|
809
|
-
if (options.upgrade || !existingSettings.statusLine || isCafeKitOwned) {
|
|
810
|
-
mergedSettings.statusLine = managedSettings.statusLine;
|
|
811
|
-
console.log(` ✓ Settings: statusLine merged`);
|
|
812
|
-
}
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
// Merge hooks
|
|
816
|
-
if (managedSettings.hooks) {
|
|
817
|
-
mergedSettings.hooks = mergedSettings.hooks || {};
|
|
818
|
-
|
|
819
|
-
Object.keys(managedSettings.hooks).forEach(eventName => {
|
|
820
|
-
const managedHooks = managedSettings.hooks[eventName];
|
|
821
|
-
const existingHooks = mergedSettings.hooks[eventName] || [];
|
|
822
|
-
const mergedHooks = [...existingHooks];
|
|
823
|
-
|
|
824
|
-
managedHooks.forEach(managedHook => {
|
|
825
|
-
const managedCommand = managedHook.hooks?.[0]?.command || '';
|
|
826
|
-
const isDuplicate = mergedHooks.some(existingHook => {
|
|
827
|
-
return existingHook.hooks?.some(h => h.command === managedCommand);
|
|
828
|
-
});
|
|
829
|
-
|
|
830
|
-
if (!isDuplicate) {
|
|
831
|
-
mergedHooks.push(managedHook);
|
|
832
|
-
console.log(` ✓ Settings: hook ${eventName} merged`);
|
|
833
|
-
}
|
|
834
|
-
});
|
|
835
|
-
|
|
836
|
-
mergedSettings.hooks[eventName] = mergedHooks;
|
|
837
|
-
});
|
|
838
|
-
}
|
|
839
|
-
|
|
840
|
-
fs.writeFileSync(targetPath, JSON.stringify(mergedSettings, null, 2), 'utf8');
|
|
841
|
-
}
|
|
842
|
-
|
|
843
|
-
// Copy CLAUDE.md template (always overwrite at Project Root)
|
|
844
|
-
function copyClaudeMdFile(platformKey, results, options = {}) {
|
|
845
|
-
if (platformKey !== 'claude') return;
|
|
846
|
-
|
|
847
|
-
const source = path.join(__dirname, '../src/claude/CLAUDE.md');
|
|
848
|
-
const dest = 'CLAUDE.md'; // Xuất thẳng ra thư mục ROOT thay vì nhét vào trong .claude/
|
|
849
|
-
|
|
850
|
-
if (fs.existsSync(source)) {
|
|
851
|
-
const destinationExists = fs.existsSync(dest);
|
|
852
|
-
|
|
853
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
854
|
-
fs.copyFileSync(source, dest);
|
|
855
|
-
|
|
856
|
-
if (destinationExists) {
|
|
857
|
-
console.log(` ↻ CLAUDE.md overwritten`);
|
|
858
|
-
results.updated++;
|
|
859
|
-
} else {
|
|
860
|
-
console.log(` ✓ CLAUDE.md installed`);
|
|
861
|
-
results.copied++;
|
|
862
|
-
}
|
|
863
|
-
} else {
|
|
864
|
-
console.log(` ⚠ CLAUDE.md template not found`);
|
|
865
|
-
results.missingDependencies++;
|
|
866
|
-
}
|
|
867
|
-
}
|
|
868
|
-
|
|
869
|
-
// Copy rules directory (always overwrite)
|
|
870
|
-
function copyRulesDirectory(platformKey, results, options = {}) {
|
|
871
|
-
if (!isClaudeCompatibleRuntime(platformKey)) return;
|
|
872
|
-
|
|
873
|
-
const source = path.join(__dirname, '../src/claude/rules');
|
|
874
|
-
const dest = getRuntimeSupportTargetDir(platformKey, 'rules');
|
|
875
|
-
|
|
876
|
-
if (fs.existsSync(source)) {
|
|
877
|
-
copyRecursive(source, dest, getCopyOptions(platformKey, { upgrade: true }));
|
|
878
|
-
console.log(` ↻ rules/ directory overwritten`);
|
|
879
|
-
results.updated++;
|
|
880
|
-
} else {
|
|
881
|
-
console.log(` ⚠ rules/ directory not found`);
|
|
882
|
-
results.missingDependencies++;
|
|
883
|
-
}
|
|
884
|
-
}
|
|
885
|
-
|
|
886
|
-
// Ensure .gitignore configured at root
|
|
887
|
-
function ensureGitignore(results, options = {}) {
|
|
888
|
-
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
889
|
-
const header = '# CafeKit / Ecosystem';
|
|
890
|
-
const patterns = [
|
|
891
|
-
'specs/_shared/',
|
|
892
|
-
'plans/',
|
|
893
|
-
'!plans/templates/'
|
|
894
|
-
];
|
|
895
|
-
|
|
896
|
-
if (!fs.existsSync(gitignorePath)) {
|
|
897
|
-
const content = ['# Git Ignore', '', header, ...patterns, ''].join('\n');
|
|
898
|
-
fs.writeFileSync(gitignorePath, content, 'utf8');
|
|
899
|
-
console.log(' ✓ .gitignore created at root');
|
|
900
|
-
results.copied++;
|
|
901
|
-
return;
|
|
902
|
-
}
|
|
903
|
-
|
|
904
|
-
const content = fs.readFileSync(gitignorePath, 'utf8');
|
|
905
|
-
const lines = content.split('\n').map(l => l.trim());
|
|
906
|
-
const missing = patterns.filter(p => !lines.includes(p));
|
|
907
|
-
|
|
908
|
-
if (missing.length > 0) {
|
|
909
|
-
let newContent = content;
|
|
910
|
-
if (!newContent.endsWith('\n')) newContent += '\n';
|
|
911
|
-
if (!content.includes(header)) newContent += `\n${header}\n`;
|
|
912
|
-
|
|
913
|
-
newContent += missing.join('\n') + '\n';
|
|
914
|
-
fs.writeFileSync(gitignorePath, newContent, 'utf8');
|
|
915
|
-
console.log(` ↻ .gitignore updated: added ${missing.join(', ')}`);
|
|
916
|
-
results.updated++;
|
|
917
|
-
} else {
|
|
918
|
-
console.log(' → .gitignore already up to date');
|
|
919
|
-
results.skipped++;
|
|
920
|
-
}
|
|
921
|
-
}
|
|
922
|
-
|
|
923
|
-
// ═══════════════════════════════════════════════════════════
|
|
924
|
-
// GEMINI CLI SETUP
|
|
925
|
-
// ═══════════════════════════════════════════════════════════
|
|
926
|
-
|
|
927
|
-
function checkGeminiCLI() {
|
|
928
|
-
try {
|
|
929
|
-
execSync('which gemini', { stdio: 'ignore' });
|
|
930
|
-
return true;
|
|
931
|
-
} catch {
|
|
932
|
-
return false;
|
|
933
|
-
}
|
|
934
|
-
}
|
|
935
|
-
|
|
936
|
-
function installGeminiCLI() {
|
|
937
|
-
console.log('\n⚙ Installing gemini-cli...');
|
|
938
|
-
try {
|
|
939
|
-
execSync('npm install -g @google/gemini-cli', { stdio: 'inherit' });
|
|
940
|
-
console.log(' ✓ gemini-cli installed successfully');
|
|
941
|
-
return true;
|
|
942
|
-
} catch (error) {
|
|
943
|
-
console.log(' ✗ Failed to install gemini-cli automatically');
|
|
944
|
-
console.log(' Please run manually: npm install -g @google/gemini-cli');
|
|
945
|
-
return false;
|
|
946
|
-
}
|
|
947
|
-
}
|
|
948
|
-
|
|
949
|
-
async function promptInstallGemini() {
|
|
950
|
-
const rl = readline.createInterface({
|
|
951
|
-
input: process.stdin,
|
|
952
|
-
output: process.stdout
|
|
953
|
-
});
|
|
954
|
-
|
|
955
|
-
console.log('\n📦 Optional: Gemini CLI Installation');
|
|
956
|
-
console.log(' hapo:inspect with ext mode requires gemini-cli');
|
|
957
|
-
console.log(' • Install now: Auto-install and configure API key');
|
|
958
|
-
console.log(' • Skip: You can still use hapo:inspect in internal mode');
|
|
959
|
-
console.log();
|
|
960
|
-
|
|
961
|
-
return new Promise((resolve) => {
|
|
962
|
-
rl.question('Install gemini-cli? (Y/n): ', (answer) => {
|
|
963
|
-
rl.close();
|
|
964
|
-
const response = answer.trim().toLowerCase();
|
|
965
|
-
resolve(response === '' || response === 'y' || response === 'yes');
|
|
966
|
-
});
|
|
967
|
-
});
|
|
968
|
-
}
|
|
969
|
-
|
|
970
|
-
async function promptGeminiAPIKey() {
|
|
971
|
-
const rl = readline.createInterface({
|
|
972
|
-
input: process.stdin,
|
|
973
|
-
output: process.stdout
|
|
974
|
-
});
|
|
975
|
-
|
|
976
|
-
console.log('\n📝 Gemini API Key Configuration');
|
|
977
|
-
console.log(' Get your API key: https://aistudio.google.com/apikey');
|
|
978
|
-
console.log();
|
|
979
|
-
|
|
980
|
-
return new Promise((resolve) => {
|
|
981
|
-
rl.question('Enter your Gemini API key (or press Enter to skip): ', (answer) => {
|
|
982
|
-
rl.close();
|
|
983
|
-
resolve(answer.trim());
|
|
984
|
-
});
|
|
985
|
-
});
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
function configureGeminiKey(apiKey, platforms = ['claude']) {
|
|
989
|
-
const envBody = `GEMINI_API_KEY=${apiKey}\nVISUAL_MODEL=gemma-4-31b-it\nSEARCH_MODEL=gemini-2.5-pro\n`;
|
|
990
|
-
const targets = platforms
|
|
991
|
-
.filter(key => PLATFORMS[key])
|
|
992
|
-
.map(key => PLATFORMS[key].folder);
|
|
993
|
-
|
|
994
|
-
if (targets.length === 0) {
|
|
995
|
-
targets.push('.claude');
|
|
996
|
-
}
|
|
997
|
-
|
|
998
|
-
let success = true;
|
|
999
|
-
for (const folder of targets) {
|
|
1000
|
-
try {
|
|
1001
|
-
const targetDir = path.join(process.cwd(), folder);
|
|
1002
|
-
const localEnvFile = path.join(targetDir, '.env');
|
|
1003
|
-
|
|
1004
|
-
if (!fs.existsSync(targetDir)) {
|
|
1005
|
-
fs.mkdirSync(targetDir, { recursive: true });
|
|
1006
|
-
}
|
|
1007
|
-
|
|
1008
|
-
fs.writeFileSync(localEnvFile, envBody, { mode: 0o600 });
|
|
1009
|
-
console.log(` ✓ Gemini API key configured securely in project (${folder}/.env)`);
|
|
1010
|
-
} catch (error) {
|
|
1011
|
-
success = false;
|
|
1012
|
-
console.log(' ✗ Failed to configure Gemini API key');
|
|
1013
|
-
console.log(` Error: ${error.message}`);
|
|
1014
|
-
console.log(' You can manually set: export GEMINI_API_KEY="your-key"');
|
|
1015
|
-
}
|
|
1016
|
-
}
|
|
1017
|
-
|
|
1018
|
-
return success;
|
|
1019
|
-
}
|
|
1020
|
-
|
|
1021
|
-
async function promptAddressingConfig() {
|
|
1022
|
-
const rl = readline.createInterface({
|
|
1023
|
-
input: process.stdin,
|
|
1024
|
-
output: process.stdout
|
|
1025
|
-
});
|
|
1026
|
-
|
|
1027
|
-
return new Promise((resolve) => {
|
|
1028
|
-
rl.question('\nHow should the AI address you? (e.g. boss, sir, master - Enter to skip): ', (answer) => {
|
|
1029
|
-
rl.close();
|
|
1030
|
-
const userAddress = answer.trim();
|
|
1031
|
-
|
|
1032
|
-
// Blank = skip, no addressing configured
|
|
1033
|
-
if (!userAddress || userAddress.length === 0) {
|
|
1034
|
-
resolve({ enabled: false, userAddress: '' });
|
|
1035
|
-
return;
|
|
1036
|
-
}
|
|
1037
|
-
|
|
1038
|
-
// Validate: letters and spaces only
|
|
1039
|
-
if (/[^a-zA-ZÀ-ỹ\s]/.test(userAddress)) {
|
|
1040
|
-
console.log(' ⚠ Invalid input (letters only), skipping addressing setup');
|
|
1041
|
-
resolve({ enabled: false, userAddress: '' });
|
|
1042
|
-
return;
|
|
1043
|
-
}
|
|
1044
|
-
|
|
1045
|
-
resolve({ enabled: true, userAddress: userAddress });
|
|
1046
|
-
});
|
|
1047
|
-
});
|
|
1048
|
-
}
|
|
1049
|
-
|
|
1050
|
-
function configureAddressing(addressingConfig, platforms = ['claude']) {
|
|
1051
|
-
// CLAUDE.md is installed at the project root (see copyClaudeMdFile),
|
|
1052
|
-
// and only the Claude platform ships it. Skip if Claude isn't installed.
|
|
1053
|
-
if (!platforms.includes('claude')) {
|
|
1054
|
-
return;
|
|
1055
|
-
}
|
|
1056
|
-
|
|
1057
|
-
// Skip if addressing not enabled (user left blank or invalid input)
|
|
1058
|
-
if (!addressingConfig.enabled || !addressingConfig.userAddress) {
|
|
1059
|
-
console.log(' → Bỏ qua thiết lập xưng hô');
|
|
1060
|
-
return;
|
|
74
|
+
async function main() {
|
|
75
|
+
// ── Concurrency guard (before context; plain output) ────
|
|
76
|
+
const got = lock.acquire();
|
|
77
|
+
if (!got.acquired) {
|
|
78
|
+
console.error(`✗ Another CafeKit install is in progress (pid ${got.pid}, since ${got.since}).`);
|
|
79
|
+
console.error(' If that process is gone, delete .cafekit.lock and retry.');
|
|
80
|
+
process.exit(1);
|
|
1061
81
|
}
|
|
1062
82
|
|
|
1063
|
-
|
|
83
|
+
let ctx;
|
|
84
|
+
let exitCode = 0;
|
|
1064
85
|
|
|
1065
86
|
try {
|
|
1066
|
-
|
|
87
|
+
ctx = buildContext(process.argv, `${Date.now()}`);
|
|
88
|
+
ctx.ui.intro(`${ctx.ui.pc.bgCyan(ctx.ui.pc.black(' CafeKit '))} Installer v${packageJson.version} · Multi-platform SDD`);
|
|
89
|
+
if (got.reclaimed) ctx.ui.info('Reclaimed a stale install lock from a dead process.');
|
|
1067
90
|
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
91
|
+
await resolvePlatforms(ctx);
|
|
92
|
+
if (ctx.cancelled) {
|
|
93
|
+
lock.release();
|
|
94
|
+
process.exit(0);
|
|
1071
95
|
}
|
|
1072
96
|
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
//
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
// Match the addressing section by its shared marker ("Context Overflow
|
|
1081
|
-
// Indicator"), covering both the older Vietnamese heading and the new one,
|
|
1082
|
-
// up to the next ## header or end of file. Keeps reinstall idempotent.
|
|
1083
|
-
const regex = /##[^\n]*Context Overflow Indicator[^\n]*[\s\S]*?(?=\n##|\n*$)/;
|
|
1084
|
-
|
|
1085
|
-
if (regex.test(content)) {
|
|
1086
|
-
// Replace existing section
|
|
1087
|
-
content = content.replace(regex, addressingSection);
|
|
1088
|
-
} else {
|
|
1089
|
-
// Append at the end
|
|
1090
|
-
if (!content.endsWith('\n')) {
|
|
1091
|
-
content += '\n';
|
|
1092
|
-
}
|
|
1093
|
-
content += '\n' + addressingSection + '\n';
|
|
97
|
+
// ── Pre-run snapshot for rollback ─────────────────────
|
|
98
|
+
// Capture platform folders AND the root files the pipeline mutates
|
|
99
|
+
// (CLAUDE.md, .gitignore) so a mid-run failure rolls back cleanly.
|
|
100
|
+
if (!ctx.dryRun) {
|
|
101
|
+
const folders = ctx.platforms.map((key) => PLATFORMS[key].folder);
|
|
102
|
+
ctx.backupDir = backup.snapshot([...folders, 'CLAUDE.md', '.gitignore'], ctx.runId);
|
|
1094
103
|
}
|
|
1095
104
|
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
} catch (error) {
|
|
1099
|
-
console.error(' ✗ Failed to configure addressing in CLAUDE.md');
|
|
1100
|
-
console.error(` Error: ${error.message}`);
|
|
1101
|
-
if (process.env.DEBUG) {
|
|
1102
|
-
console.error(error.stack);
|
|
105
|
+
for (const platformKey of ctx.platforms) {
|
|
106
|
+
installPlatform(ctx, platformKey);
|
|
1103
107
|
}
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
108
|
|
|
1107
|
-
|
|
1108
|
-
|
|
109
|
+
ensureGitignore(ctx);
|
|
110
|
+
await runPostInstall(ctx);
|
|
111
|
+
printSummary(ctx);
|
|
1109
112
|
|
|
1110
|
-
|
|
1111
|
-
console.log(' ✓ gemini-cli already installed');
|
|
1112
|
-
} else {
|
|
1113
|
-
const shouldInstall = await promptInstallGemini();
|
|
1114
|
-
if (shouldInstall) {
|
|
1115
|
-
installGeminiCLI();
|
|
1116
|
-
} else {
|
|
1117
|
-
console.log('\n ℹ Skipped gemini-cli installation');
|
|
1118
|
-
console.log(' • hapo:inspect will work in internal mode (default)');
|
|
1119
|
-
console.log(' • To install later: npm install -g @google/gemini-cli');
|
|
1120
|
-
}
|
|
1121
|
-
}
|
|
113
|
+
if (!ctx.dryRun) backup.prune(3);
|
|
1122
114
|
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
}
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
// MAIN
|
|
1135
|
-
// ═══════════════════════════════════════════════════════════
|
|
1136
|
-
|
|
1137
|
-
async function main() {
|
|
1138
|
-
const installerOptions = parseInstallerArgs(process.argv);
|
|
1139
|
-
|
|
1140
|
-
console.log();
|
|
1141
|
-
console.log('╔════════════════════════════════════════════════════════╗');
|
|
1142
|
-
console.log(`║ CafeKit Installer v${String(packageJson.version).padEnd(5, ' ')} ║`);
|
|
1143
|
-
console.log('║ Multi-platform SDD Workflow ║');
|
|
1144
|
-
console.log('╚════════════════════════════════════════════════════════╝');
|
|
1145
|
-
console.log();
|
|
1146
|
-
|
|
1147
|
-
let platforms = detectPlatforms();
|
|
1148
|
-
|
|
1149
|
-
if (platforms.length === 0) {
|
|
1150
|
-
// No platforms detected - prompt user
|
|
1151
|
-
platforms = await promptPlatformSelection();
|
|
1152
|
-
|
|
1153
|
-
if (platforms.length === 0) {
|
|
1154
|
-
console.log('\nInstallation cancelled.');
|
|
1155
|
-
process.exit(0);
|
|
1156
|
-
}
|
|
1157
|
-
} else if (platforms.length > 1) {
|
|
1158
|
-
// Multiple platforms detected - confirm with user
|
|
1159
|
-
const proceed = await promptMultiPlatformConfirm(platforms);
|
|
1160
|
-
if (!proceed) {
|
|
1161
|
-
platforms = await promptPlatformSelection();
|
|
1162
|
-
if (platforms.length === 0) {
|
|
1163
|
-
console.log('\nInstallation cancelled.');
|
|
1164
|
-
process.exit(0);
|
|
115
|
+
exitCode = ctx.results.errors > 0 ? 1 : 0;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
const log = ctx && ctx.ui ? ctx.ui : { error: (m) => console.error(m) };
|
|
118
|
+
log.error(`Installation failed: ${error.message}`);
|
|
119
|
+
if (ctx && ctx.backupDir && !ctx.dryRun) {
|
|
120
|
+
try {
|
|
121
|
+
backup.restore(ctx.backupDir);
|
|
122
|
+
log.error('Rolled back to the pre-install state from snapshot.');
|
|
123
|
+
} catch (restoreError) {
|
|
124
|
+
log.error(`Rollback failed: ${restoreError.message}`);
|
|
125
|
+
log.error(`Manual restore available at: ${ctx.backupDir}`);
|
|
1165
126
|
}
|
|
1166
127
|
}
|
|
128
|
+
exitCode = 1;
|
|
129
|
+
} finally {
|
|
130
|
+
lock.release();
|
|
1167
131
|
}
|
|
1168
132
|
|
|
1169
|
-
|
|
1170
|
-
const platformNames = platforms.map(key => PLATFORMS[key].name).join(', ');
|
|
1171
|
-
console.log(`\nInstalling for: ${platformNames}`);
|
|
1172
|
-
|
|
1173
|
-
warnLegacyClaudeFolder(platforms);
|
|
1174
|
-
|
|
1175
|
-
if (installerOptions.upgrade) {
|
|
1176
|
-
console.log('Mode: upgrade (overwrite managed files)\n');
|
|
1177
|
-
} else {
|
|
1178
|
-
console.log('Mode: install (skip existing files)\n');
|
|
1179
|
-
}
|
|
1180
|
-
|
|
1181
|
-
const results = {
|
|
1182
|
-
copied: 0,
|
|
1183
|
-
updated: 0,
|
|
1184
|
-
skipped: 0,
|
|
1185
|
-
installedSkills: 0,
|
|
1186
|
-
dependencyChecks: 0,
|
|
1187
|
-
installedDependencies: 0,
|
|
1188
|
-
missingDependencies: 0,
|
|
1189
|
-
errors: 0,
|
|
1190
|
-
targets: []
|
|
1191
|
-
};
|
|
1192
|
-
|
|
1193
|
-
try {
|
|
1194
|
-
for (const platformKey of platforms) {
|
|
1195
|
-
const platform = PLATFORMS[platformKey];
|
|
1196
|
-
console.log(`${platform.name} (${platform.folder}/)`);
|
|
1197
|
-
console.log('-'.repeat(40));
|
|
1198
|
-
|
|
1199
|
-
copyPlatformFiles(platformKey, results, installerOptions);
|
|
1200
|
-
|
|
1201
|
-
// Copy ROUTING.md for Claude Code platform
|
|
1202
|
-
if (platformKey === 'claude') {
|
|
1203
|
-
copyRoutingFile(platformKey, results, installerOptions);
|
|
1204
|
-
copyClaudeRuntimeFiles(platformKey, results, installerOptions);
|
|
1205
|
-
removeObsoleteClaudeRuntimeFiles(platformKey, results);
|
|
1206
|
-
mergeClaudeSettings(platformKey, results, installerOptions);
|
|
1207
|
-
copyClaudeMdFile(platformKey, results, installerOptions);
|
|
1208
|
-
copyRulesDirectory(platformKey, results, installerOptions);
|
|
1209
|
-
}
|
|
1210
|
-
|
|
1211
|
-
if (platformKey === 'opencode') {
|
|
1212
|
-
copyOpenCodeSharedRuntimeFiles(platformKey, results, installerOptions);
|
|
1213
|
-
copyOpenCodePlugins(platformKey, results, installerOptions);
|
|
1214
|
-
copyOpenCodeCommandTemplates(platformKey, results, installerOptions);
|
|
1215
|
-
copyOpenCodeAgentsMdFile(platformKey, results, installerOptions);
|
|
1216
|
-
copyRulesDirectory(platformKey, results, installerOptions);
|
|
1217
|
-
mergeOpenCodeConfig(platformKey, results);
|
|
1218
|
-
}
|
|
1219
|
-
|
|
1220
|
-
writePlatformVersionMetadata(platformKey, results);
|
|
1221
|
-
|
|
1222
|
-
results.targets.push(platform.commandsDir);
|
|
1223
|
-
console.log();
|
|
1224
|
-
}
|
|
1225
|
-
|
|
1226
|
-
// Ensure root .gitignore is configured correctly
|
|
1227
|
-
console.log('Root Configuration');
|
|
1228
|
-
console.log('-'.repeat(40));
|
|
1229
|
-
ensureGitignore(results, installerOptions);
|
|
1230
|
-
console.log();
|
|
1231
|
-
|
|
1232
|
-
// Setup runtime-specific optional model/API configuration
|
|
1233
|
-
await setupOpenCodeModel(platforms, results);
|
|
1234
|
-
await setupGeminiCLI(platforms);
|
|
1235
|
-
|
|
1236
|
-
// Setup addressing configuration
|
|
1237
|
-
const addressingConfig = await promptAddressingConfig();
|
|
1238
|
-
configureAddressing(addressingConfig, platforms);
|
|
1239
|
-
|
|
1240
|
-
// Summary
|
|
1241
|
-
console.log('╔════════════════════════════════════════════════════════╗');
|
|
1242
|
-
console.log('║ Installation Complete! ║');
|
|
1243
|
-
console.log('╚════════════════════════════════════════════════════════╝');
|
|
1244
|
-
console.log();
|
|
1245
|
-
console.log(` Copied Files: ${results.copied}`);
|
|
1246
|
-
console.log(` Updated Files: ${results.updated}`);
|
|
1247
|
-
console.log(` CafeKit Version: ${packageJson.version}`);
|
|
1248
|
-
console.log(` Skipped Files: ${results.skipped}`);
|
|
1249
|
-
console.log(` Installed Skills: ${results.installedSkills > 0 ? 'Yes ✓' : 'No'}`);
|
|
1250
|
-
console.log(` Dependency Checks: ${results.dependencyChecks}`);
|
|
1251
|
-
console.log(` Installed Deps: ${results.installedDependencies}`);
|
|
1252
|
-
console.log(` Missing Deps: ${results.missingDependencies}`);
|
|
1253
|
-
console.log(` Target Directories: ${results.targets.join(', ')}`);
|
|
1254
|
-
if (results.errors > 0) {
|
|
1255
|
-
console.log(` Errors: ${results.errors} ⚠`);
|
|
1256
|
-
}
|
|
1257
|
-
console.log();
|
|
1258
|
-
console.log('Next steps:');
|
|
1259
|
-
const nextEditorLabel = platforms.length === 1 ? PLATFORMS[platforms[0]].name : 'your AI editor';
|
|
1260
|
-
console.log(` 1. Start ${nextEditorLabel}`);
|
|
1261
|
-
|
|
1262
|
-
// Show platform-specific hints
|
|
1263
|
-
for (const platformKey of platforms) {
|
|
1264
|
-
const platform = PLATFORMS[platformKey];
|
|
1265
|
-
console.log(`\n For ${platform.name}:`);
|
|
1266
|
-
if (platformKey === 'claude') {
|
|
1267
|
-
console.log(' Use skill: /hapo:specs <feature-description>');
|
|
1268
|
-
} else {
|
|
1269
|
-
console.log(` Instruct the agent to start a new feature or brainstorm`);
|
|
1270
|
-
}
|
|
1271
|
-
}
|
|
1272
|
-
|
|
1273
|
-
if (platforms.includes('claude')) {
|
|
1274
|
-
console.log('\n 2. Claude Code will automatically sync docs/ (Continuous Documentation)');
|
|
1275
|
-
} else {
|
|
1276
|
-
console.log('\n 2. CafeKit skills and AGENTS.md are installed for the selected runtime');
|
|
1277
|
-
}
|
|
1278
|
-
const envHosts = platforms.map(key => `${PLATFORMS[key].folder}/.env`).join(' and ');
|
|
1279
|
-
console.log(` 3. Project API Keys are now securely isolated in ${envHosts}`);
|
|
1280
|
-
if (!installerOptions.upgrade) {
|
|
1281
|
-
console.log(' 4. To refresh managed templates later, run installer with --upgrade');
|
|
1282
|
-
}
|
|
1283
|
-
console.log();
|
|
1284
|
-
console.log('Documentation: https://github.com/haposoft/cafekit');
|
|
1285
|
-
if (results.missingDependencies > 0) {
|
|
1286
|
-
console.log('Note: some dependency templates could not be installed. Please check command/agent directories.');
|
|
1287
|
-
}
|
|
1288
|
-
console.log();
|
|
1289
|
-
|
|
1290
|
-
process.exit(results.errors > 0 ? 1 : 0);
|
|
1291
|
-
} catch (error) {
|
|
1292
|
-
console.error(`\n✗ Installation failed: ${error.message}`);
|
|
1293
|
-
process.exit(1);
|
|
1294
|
-
}
|
|
133
|
+
process.exit(exitCode);
|
|
1295
134
|
}
|
|
1296
135
|
|
|
1297
136
|
main();
|