@haposoft/cafekit 0.8.17 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -84
- package/bin/install.js +90 -653
- package/bin/lib/copy-utils.js +61 -0
- package/bin/lib/opencode-install.js +866 -0
- package/package.json +4 -1
- package/src/claude/CLAUDE.md +2 -2
- package/src/claude/migration-manifest.json +1 -0
- package/src/claude/rules/skill-domain-routing.md +2 -0
- package/src/claude/rules/skill-workflow-routing.md +5 -1
- package/src/claude/skills/question/SKILL.md +162 -0
- package/src/claude/skills/question/templates/question.md +32 -0
- package/src/opencode/AGENTS.md +6 -6
- package/src/opencode/plugins/docs-sync.ts +181 -0
- package/src/opencode/plugins/inspect-block.ts +164 -0
- package/src/opencode/plugins/privacy-block.ts +182 -0
- package/src/opencode/plugins/rules.ts +163 -0
- package/src/opencode/plugins/session.ts +179 -0
- package/src/opencode/plugins/state.ts +253 -0
- package/src/opencode/plugins/usage.ts +178 -0
package/bin/install.js
CHANGED
|
@@ -4,9 +4,12 @@
|
|
|
4
4
|
* CafeKit Installer
|
|
5
5
|
* Multi-platform installer for AI coding assistants
|
|
6
6
|
*
|
|
7
|
-
* Supported platforms:
|
|
8
|
-
* - claude: Claude Code
|
|
9
|
-
* - opencode: OpenCode
|
|
7
|
+
* Supported platforms (each install is fully self-contained):
|
|
8
|
+
* - claude: Claude Code → .claude/ (skills, rules, scripts, references, runtime.json)
|
|
9
|
+
* - opencode: OpenCode → .opencode/ (same layout, content rewritten on copy)
|
|
10
|
+
*
|
|
11
|
+
* Selecting both creates both folders independently. There is no shared
|
|
12
|
+
* runtime directory between platforms.
|
|
10
13
|
*
|
|
11
14
|
* To add a new platform:
|
|
12
15
|
* 1. Add to PLATFORMS registry below
|
|
@@ -20,6 +23,19 @@ const os = require('os');
|
|
|
20
23
|
const readline = require('readline');
|
|
21
24
|
const { execSync } = require('child_process');
|
|
22
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');
|
|
23
39
|
const INSTALL_COMMAND = `npx ${packageJson.name}@${packageJson.version}`;
|
|
24
40
|
|
|
25
41
|
function validateManifestV2(manifest) {
|
|
@@ -65,92 +81,6 @@ const DEPENDENCY_TEMPLATES = {
|
|
|
65
81
|
}
|
|
66
82
|
};
|
|
67
83
|
|
|
68
|
-
const OPENCODE_TOOL_MAP = {
|
|
69
|
-
Read: 'read',
|
|
70
|
-
Glob: 'glob',
|
|
71
|
-
Grep: 'grep',
|
|
72
|
-
Edit: 'edit',
|
|
73
|
-
Write: 'edit',
|
|
74
|
-
MultiEdit: 'edit',
|
|
75
|
-
Bash: 'bash',
|
|
76
|
-
WebFetch: 'webfetch',
|
|
77
|
-
WebSearch: 'websearch',
|
|
78
|
-
NotebookEdit: 'edit'
|
|
79
|
-
};
|
|
80
|
-
|
|
81
|
-
const OPENCODE_COMMAND_TEMPLATES = [
|
|
82
|
-
{
|
|
83
|
-
fileName: 'brainstorm.md',
|
|
84
|
-
skillName: 'brainstorm',
|
|
85
|
-
agent: 'brainstormer',
|
|
86
|
-
subtask: true,
|
|
87
|
-
description: 'Explore and narrow an idea before writing a CafeKit spec.'
|
|
88
|
-
},
|
|
89
|
-
{
|
|
90
|
-
fileName: 'specs.md',
|
|
91
|
-
skillName: 'specs',
|
|
92
|
-
agent: 'spec-maker',
|
|
93
|
-
subtask: true,
|
|
94
|
-
description: 'Create, update, validate, or approve a CafeKit specification.'
|
|
95
|
-
},
|
|
96
|
-
{
|
|
97
|
-
fileName: 'develop.md',
|
|
98
|
-
skillName: 'develop',
|
|
99
|
-
agent: 'god-developer',
|
|
100
|
-
subtask: true,
|
|
101
|
-
description: 'Implement approved CafeKit spec tasks with scope fidelity.'
|
|
102
|
-
},
|
|
103
|
-
{
|
|
104
|
-
fileName: 'test.md',
|
|
105
|
-
skillName: 'test',
|
|
106
|
-
agent: 'test-runner',
|
|
107
|
-
subtask: true,
|
|
108
|
-
description: 'Run CafeKit verification, QA, and evidence collection.'
|
|
109
|
-
},
|
|
110
|
-
{
|
|
111
|
-
fileName: 'code-review.md',
|
|
112
|
-
skillName: 'code-review',
|
|
113
|
-
agent: 'code-auditor',
|
|
114
|
-
subtask: true,
|
|
115
|
-
description: 'Review implementation against code quality and spec compliance.'
|
|
116
|
-
},
|
|
117
|
-
{
|
|
118
|
-
fileName: 'debug.md',
|
|
119
|
-
skillName: 'debug',
|
|
120
|
-
agent: 'debugger',
|
|
121
|
-
subtask: true,
|
|
122
|
-
description: 'Diagnose a bug or failure with evidence before fixing.'
|
|
123
|
-
},
|
|
124
|
-
{
|
|
125
|
-
fileName: 'hotfix.md',
|
|
126
|
-
skillName: 'hotfix',
|
|
127
|
-
agent: 'god-developer',
|
|
128
|
-
subtask: true,
|
|
129
|
-
description: 'Apply a scout-first, narrow production hotfix.'
|
|
130
|
-
},
|
|
131
|
-
{
|
|
132
|
-
fileName: 'docs.md',
|
|
133
|
-
skillName: 'docs',
|
|
134
|
-
agent: 'docs-keeper',
|
|
135
|
-
subtask: true,
|
|
136
|
-
description: 'Create, update, or reconstruct source-backed project documentation.'
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
fileName: 'inspect.md',
|
|
140
|
-
skillName: 'inspect',
|
|
141
|
-
agent: 'inspector',
|
|
142
|
-
subtask: true,
|
|
143
|
-
description: 'Inspect a codebase, artifact, or external context before planning.'
|
|
144
|
-
},
|
|
145
|
-
{
|
|
146
|
-
fileName: 'generate-graph.md',
|
|
147
|
-
skillName: 'generate-graph',
|
|
148
|
-
agent: 'god-developer',
|
|
149
|
-
subtask: true,
|
|
150
|
-
description: 'Generate technical diagrams from project context.'
|
|
151
|
-
}
|
|
152
|
-
];
|
|
153
|
-
|
|
154
84
|
// ═══════════════════════════════════════════════════════════
|
|
155
85
|
// PLATFORM REGISTRY - Add new platforms here
|
|
156
86
|
// ═══════════════════════════════════════════════════════════
|
|
@@ -175,9 +105,9 @@ const PLATFORMS = {
|
|
|
175
105
|
folder: '.opencode',
|
|
176
106
|
detectFiles: ['.opencode', 'opencode.json', 'opencode.jsonc'],
|
|
177
107
|
commandsDir: '.opencode/commands',
|
|
178
|
-
skillsDir: '.
|
|
108
|
+
skillsDir: '.opencode/skills',
|
|
179
109
|
agentsDir: '.opencode/agents',
|
|
180
|
-
skillsRef: '.
|
|
110
|
+
skillsRef: '.opencode/skills',
|
|
181
111
|
commandPrefix: '/',
|
|
182
112
|
sourceDir: 'claude',
|
|
183
113
|
sourceSubdir: 'archive-command'
|
|
@@ -223,15 +153,27 @@ function getPlatformKeys() {
|
|
|
223
153
|
return Object.keys(PLATFORMS);
|
|
224
154
|
}
|
|
225
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
|
+
|
|
226
172
|
function isClaudeCompatibleRuntime(platformKey) {
|
|
227
173
|
return platformKey === 'claude' || platformKey === 'opencode';
|
|
228
174
|
}
|
|
229
175
|
|
|
230
|
-
function
|
|
231
|
-
if (platformKey === 'opencode') {
|
|
232
|
-
return path.join('.claude', subdir);
|
|
233
|
-
}
|
|
234
|
-
|
|
176
|
+
function getRuntimeSupportTargetDir(platformKey, subdir) {
|
|
235
177
|
return path.join(PLATFORMS[platformKey].folder, subdir);
|
|
236
178
|
}
|
|
237
179
|
|
|
@@ -251,147 +193,11 @@ function parseInstallerArgs(argv) {
|
|
|
251
193
|
return args;
|
|
252
194
|
}
|
|
253
195
|
|
|
254
|
-
function
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
if (!match) {
|
|
258
|
-
return {
|
|
259
|
-
frontmatter: {},
|
|
260
|
-
body: content
|
|
261
|
-
};
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
const frontmatter = {};
|
|
265
|
-
match[1].split(/\r?\n/).forEach(line => {
|
|
266
|
-
const separatorIndex = line.indexOf(':');
|
|
267
|
-
if (separatorIndex === -1) return;
|
|
268
|
-
|
|
269
|
-
const key = line.slice(0, separatorIndex).trim();
|
|
270
|
-
let value = line.slice(separatorIndex + 1).trim();
|
|
271
|
-
|
|
272
|
-
if (
|
|
273
|
-
(value.startsWith('"') && value.endsWith('"')) ||
|
|
274
|
-
(value.startsWith("'") && value.endsWith("'"))
|
|
275
|
-
) {
|
|
276
|
-
value = value.slice(1, -1);
|
|
277
|
-
}
|
|
278
|
-
|
|
279
|
-
if (key) {
|
|
280
|
-
frontmatter[key] = value;
|
|
281
|
-
}
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
return {
|
|
285
|
-
frontmatter,
|
|
286
|
-
body: content.slice(match[0].length)
|
|
287
|
-
};
|
|
288
|
-
}
|
|
289
|
-
|
|
290
|
-
function formatOpenCodeValue(value) {
|
|
291
|
-
return JSON.stringify(String(value || ''));
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
function normalizeOpenCodeBody(content) {
|
|
295
|
-
return content
|
|
296
|
-
.replace(/\.claude\/agents\//g, '.opencode/agents/')
|
|
297
|
-
.replace(/CLAUDE\.md/g, 'AGENTS.md');
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
function getOpenCodeToolEntries(toolsValue) {
|
|
301
|
-
const toolNames = String(toolsValue || '')
|
|
302
|
-
.split(',')
|
|
303
|
-
.map(tool => tool.trim())
|
|
304
|
-
.filter(Boolean);
|
|
305
|
-
|
|
306
|
-
const mappedTools = new Set();
|
|
307
|
-
|
|
308
|
-
toolNames.forEach(toolName => {
|
|
309
|
-
const mapped = OPENCODE_TOOL_MAP[toolName];
|
|
310
|
-
if (mapped) {
|
|
311
|
-
mappedTools.add(mapped);
|
|
312
|
-
}
|
|
313
|
-
});
|
|
314
|
-
|
|
315
|
-
return Array.from(mappedTools).sort();
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
function convertOpenCodeAgentContent(content, fileName) {
|
|
319
|
-
const { frontmatter, body } = splitFrontmatter(content);
|
|
320
|
-
const agentName = frontmatter.name || path.basename(fileName, '.md');
|
|
321
|
-
const description = frontmatter.description || `CafeKit agent: ${agentName}`;
|
|
322
|
-
const mode = agentName === 'brainstormer' ? 'primary' : 'subagent';
|
|
323
|
-
const permissionEntries = new Set(getOpenCodeToolEntries(frontmatter.tools));
|
|
324
|
-
permissionEntries.add('skill');
|
|
325
|
-
permissionEntries.add('task');
|
|
326
|
-
const lines = [
|
|
327
|
-
'---',
|
|
328
|
-
`description: ${formatOpenCodeValue(description)}`,
|
|
329
|
-
`mode: ${mode}`
|
|
330
|
-
];
|
|
331
|
-
|
|
332
|
-
if (permissionEntries.size > 0) {
|
|
333
|
-
lines.push('permission:');
|
|
334
|
-
Array.from(permissionEntries).sort().forEach(permissionName => {
|
|
335
|
-
lines.push(` ${permissionName}: allow`);
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
lines.push('---', '');
|
|
340
|
-
|
|
341
|
-
return `${lines.join('\n')}${normalizeOpenCodeBody(body).trimStart()}`;
|
|
342
|
-
}
|
|
343
|
-
|
|
344
|
-
function convertOpenCodeCommandContent(content, fileName) {
|
|
345
|
-
const { frontmatter, body } = splitFrontmatter(content);
|
|
346
|
-
const commandName = path.basename(fileName, '.md');
|
|
347
|
-
const description = frontmatter.description || `CafeKit command: ${commandName}`;
|
|
348
|
-
const lines = [
|
|
349
|
-
'---',
|
|
350
|
-
`description: ${formatOpenCodeValue(description)}`
|
|
351
|
-
];
|
|
352
|
-
|
|
353
|
-
if (frontmatter.agent) {
|
|
354
|
-
lines.push(`agent: ${formatOpenCodeValue(frontmatter.agent)}`);
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
lines.push('---', '');
|
|
358
|
-
|
|
359
|
-
return `${lines.join('\n')}${normalizeOpenCodeBody(body).trimStart()}`;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
function createOpenCodeSkillCommandContent(command) {
|
|
363
|
-
const lines = [
|
|
364
|
-
'---',
|
|
365
|
-
`description: ${formatOpenCodeValue(command.description)}`
|
|
366
|
-
];
|
|
367
|
-
|
|
368
|
-
if (command.agent) {
|
|
369
|
-
lines.push(`agent: ${formatOpenCodeValue(command.agent)}`);
|
|
370
|
-
}
|
|
371
|
-
|
|
372
|
-
if (typeof command.subtask === 'boolean') {
|
|
373
|
-
lines.push(`subtask: ${command.subtask ? 'true' : 'false'}`);
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
if (command.model) {
|
|
377
|
-
lines.push(`model: ${formatOpenCodeValue(command.model)}`);
|
|
196
|
+
function getCopyOptions(platformKey, baseOptions = {}) {
|
|
197
|
+
if (platformKey === 'opencode') {
|
|
198
|
+
return getOpenCodeCopyOptions(baseOptions);
|
|
378
199
|
}
|
|
379
|
-
|
|
380
|
-
lines.push(
|
|
381
|
-
'---',
|
|
382
|
-
'',
|
|
383
|
-
`Use the CafeKit skill at \`.claude/skills/${command.skillName}/SKILL.md\`.`,
|
|
384
|
-
'',
|
|
385
|
-
'Read that skill first, then execute its workflow with these arguments:',
|
|
386
|
-
'',
|
|
387
|
-
'$ARGUMENTS',
|
|
388
|
-
'',
|
|
389
|
-
'If the skill references supporting files, resolve them relative to the skill directory.',
|
|
390
|
-
'Keep the response aligned with the skill instructions and the current project context.',
|
|
391
|
-
''
|
|
392
|
-
);
|
|
393
|
-
|
|
394
|
-
return lines.join('\n');
|
|
200
|
+
return baseOptions;
|
|
395
201
|
}
|
|
396
202
|
|
|
397
203
|
// ═══════════════════════════════════════════════════════════
|
|
@@ -460,30 +266,6 @@ async function promptMultiPlatformConfirm(detected) {
|
|
|
460
266
|
// FILE OPERATIONS
|
|
461
267
|
// ═══════════════════════════════════════════════════════════
|
|
462
268
|
|
|
463
|
-
function copyRecursive(src, dest, options = {}) {
|
|
464
|
-
const exists = fs.existsSync(src);
|
|
465
|
-
const stats = exists && fs.statSync(src);
|
|
466
|
-
const isDirectory = exists && stats.isDirectory();
|
|
467
|
-
const shouldOverwriteManagedFiles = Boolean(options.upgrade);
|
|
468
|
-
|
|
469
|
-
if (isDirectory) {
|
|
470
|
-
if (!fs.existsSync(dest)) {
|
|
471
|
-
fs.mkdirSync(dest, { recursive: true });
|
|
472
|
-
}
|
|
473
|
-
fs.readdirSync(src).forEach(childItemName => {
|
|
474
|
-
const destItemName = childItemName === 'gitignore' ? '.gitignore' : childItemName;
|
|
475
|
-
copyRecursive(path.join(src, childItemName), path.join(dest, destItemName), options);
|
|
476
|
-
});
|
|
477
|
-
} else {
|
|
478
|
-
if (fs.existsSync(dest) && !shouldOverwriteManagedFiles) {
|
|
479
|
-
return;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
fs.mkdirSync(path.dirname(dest), { recursive: true });
|
|
483
|
-
fs.copyFileSync(src, dest);
|
|
484
|
-
}
|
|
485
|
-
}
|
|
486
|
-
|
|
487
269
|
function ensureDependencyFile(targetPath, content, results, label, options = {}) {
|
|
488
270
|
const destinationExists = fs.existsSync(targetPath);
|
|
489
271
|
const shouldOverwriteManagedFiles = Boolean(options.upgrade);
|
|
@@ -529,18 +311,6 @@ function ensureWorkflowDependencies(platformKey, platform, results, options = {}
|
|
|
529
311
|
});
|
|
530
312
|
}
|
|
531
313
|
|
|
532
|
-
function readJsonFile(filePath) {
|
|
533
|
-
if (!fs.existsSync(filePath)) {
|
|
534
|
-
return {};
|
|
535
|
-
}
|
|
536
|
-
|
|
537
|
-
try {
|
|
538
|
-
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
539
|
-
} catch {
|
|
540
|
-
return {};
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
|
|
544
314
|
function writePlatformVersionMetadata(platformKey, results) {
|
|
545
315
|
const platform = PLATFORMS[platformKey];
|
|
546
316
|
const targetPath = path.join(platform.folder, 'cafekit.json');
|
|
@@ -623,9 +393,10 @@ function copyPlatformFiles(platformKey, results, options = {}) {
|
|
|
623
393
|
const agentsSourceDir = path.join(__dirname, `../src/${platform.sourceDir}/agents`);
|
|
624
394
|
|
|
625
395
|
// Create directories
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
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.
|
|
629
400
|
if (!fs.existsSync(platform.skillsDir)) {
|
|
630
401
|
fs.mkdirSync(platform.skillsDir, { recursive: true });
|
|
631
402
|
}
|
|
@@ -640,7 +411,7 @@ function copyPlatformFiles(platformKey, results, options = {}) {
|
|
|
640
411
|
|
|
641
412
|
if (fs.existsSync(specSkillSource)) {
|
|
642
413
|
const skillExisted = fs.existsSync(specSkillDest);
|
|
643
|
-
copyRecursive(specSkillSource, specSkillDest, options);
|
|
414
|
+
copyRecursive(specSkillSource, specSkillDest, getCopyOptions(platformKey, options));
|
|
644
415
|
results.installedSkills++;
|
|
645
416
|
|
|
646
417
|
if (shouldOverwriteManagedFiles && skillExisted) {
|
|
@@ -716,7 +487,7 @@ function copyPlatformFiles(platformKey, results, options = {}) {
|
|
|
716
487
|
|
|
717
488
|
if (fs.existsSync(skillSource)) {
|
|
718
489
|
const skillExisted = fs.existsSync(skillDest);
|
|
719
|
-
copyRecursive(skillSource, skillDest, options);
|
|
490
|
+
copyRecursive(skillSource, skillDest, getCopyOptions(platformKey, options));
|
|
720
491
|
results.installedSkills++;
|
|
721
492
|
|
|
722
493
|
if (shouldOverwriteManagedFiles && skillExisted) {
|
|
@@ -782,9 +553,9 @@ function copyPlatformFiles(platformKey, results, options = {}) {
|
|
|
782
553
|
// Copy agent reference manuals (debugger manuals, etc.) recursively
|
|
783
554
|
const refsSource = path.join(__dirname, '../src', platform.sourceDir, 'references');
|
|
784
555
|
if (fs.existsSync(refsSource)) {
|
|
785
|
-
const refsDest =
|
|
556
|
+
const refsDest = getRuntimeSupportTargetDir(platformKey, 'references');
|
|
786
557
|
const refsExisted = fs.existsSync(refsDest);
|
|
787
|
-
copyRecursive(refsSource, refsDest, options);
|
|
558
|
+
copyRecursive(refsSource, refsDest, getCopyOptions(platformKey, options));
|
|
788
559
|
|
|
789
560
|
if (shouldOverwriteManagedFiles && refsExisted) {
|
|
790
561
|
console.log(` ↻ Agent reference manuals updated`);
|
|
@@ -803,9 +574,9 @@ function copyPlatformFiles(platformKey, results, options = {}) {
|
|
|
803
574
|
// Copy native scripts (browser-tool, docs-fetch, validate-docs)
|
|
804
575
|
const scriptsSourceDir = path.join(__dirname, '../src/claude/scripts');
|
|
805
576
|
if (fs.existsSync(scriptsSourceDir)) {
|
|
806
|
-
const scriptsDest =
|
|
577
|
+
const scriptsDest = getRuntimeSupportTargetDir(platformKey, 'scripts');
|
|
807
578
|
const scriptsExisted = fs.existsSync(scriptsDest);
|
|
808
|
-
copyRecursive(scriptsSourceDir, scriptsDest, options);
|
|
579
|
+
copyRecursive(scriptsSourceDir, scriptsDest, getCopyOptions(platformKey, options));
|
|
809
580
|
|
|
810
581
|
if (shouldOverwriteManagedFiles && scriptsExisted) {
|
|
811
582
|
console.log(` ↻ Native scripts updated`);
|
|
@@ -876,6 +647,9 @@ function copyRoutingFile(platformKey, results, options = {}) {
|
|
|
876
647
|
|
|
877
648
|
let content = fs.readFileSync(source, 'utf8');
|
|
878
649
|
content = content.replace(/\{\{SKILLS_DIR\}\}/g, platform.skillsRef);
|
|
650
|
+
if (platformKey === 'opencode') {
|
|
651
|
+
content = normalizeOpenCodeBody(content);
|
|
652
|
+
}
|
|
879
653
|
fs.writeFileSync(dest, content);
|
|
880
654
|
|
|
881
655
|
if (destinationExists && shouldOverwriteManagedFiles) {
|
|
@@ -888,102 +662,6 @@ function copyRoutingFile(platformKey, results, options = {}) {
|
|
|
888
662
|
}
|
|
889
663
|
}
|
|
890
664
|
|
|
891
|
-
function createOpenCodeAgentsContent(sourceContent) {
|
|
892
|
-
const adaptedContent = sourceContent
|
|
893
|
-
.replace(/^# CLAUDE\.md/m, '# AGENTS.md')
|
|
894
|
-
.replace(
|
|
895
|
-
'Primary operating instructions for Claude Code or any AI agent using this CafeKit runtime.',
|
|
896
|
-
'Primary operating instructions for OpenCode or any AI agent using this CafeKit runtime.'
|
|
897
|
-
)
|
|
898
|
-
.replace(/Claude Code/g, 'OpenCode')
|
|
899
|
-
.replace(/CLAUDE\.md/g, 'AGENTS.md');
|
|
900
|
-
|
|
901
|
-
const openCodeRuntimeBlock = [
|
|
902
|
-
'## OpenCode Runtime Mapping',
|
|
903
|
-
'',
|
|
904
|
-
'- OpenCode project instructions live in `AGENTS.md`.',
|
|
905
|
-
'- CafeKit commands live in `.opencode/commands/` and can be run as OpenCode slash commands.',
|
|
906
|
-
'- CafeKit subagents live in `.opencode/agents/` using OpenCode frontmatter.',
|
|
907
|
-
'- CafeKit skills intentionally live in `.claude/skills/` because OpenCode reads Claude-compatible skills natively.',
|
|
908
|
-
'- Shared CafeKit support files live in `.claude/rules/`, `.claude/scripts/`, `.claude/references/`, and `.claude/runtime.json`.',
|
|
909
|
-
'- OpenCode config is merged into `opencode.json`; keep project-specific model/provider choices there.',
|
|
910
|
-
''
|
|
911
|
-
].join('\n');
|
|
912
|
-
|
|
913
|
-
return adaptedContent.replace('## Core Objective', `${openCodeRuntimeBlock}\n## Core Objective`);
|
|
914
|
-
}
|
|
915
|
-
|
|
916
|
-
function upsertCafeKitAgentsBlock(existingContent, blockContent) {
|
|
917
|
-
const start = '<!-- CAFEKIT OPENCODE START -->';
|
|
918
|
-
const end = '<!-- CAFEKIT OPENCODE END -->';
|
|
919
|
-
const managedBlock = `${start}\n${blockContent.trim()}\n${end}`;
|
|
920
|
-
const blockPattern = new RegExp(`${start}[\\s\\S]*?${end}`);
|
|
921
|
-
|
|
922
|
-
if (blockPattern.test(existingContent)) {
|
|
923
|
-
return existingContent.replace(blockPattern, managedBlock);
|
|
924
|
-
}
|
|
925
|
-
|
|
926
|
-
const separator = existingContent.endsWith('\n') ? '\n' : '\n\n';
|
|
927
|
-
return `${existingContent}${separator}${managedBlock}\n`;
|
|
928
|
-
}
|
|
929
|
-
|
|
930
|
-
function getOpenCodeAgentsTemplateContent() {
|
|
931
|
-
const openCodeTemplate = path.join(__dirname, '../src/opencode/AGENTS.md');
|
|
932
|
-
|
|
933
|
-
if (fs.existsSync(openCodeTemplate)) {
|
|
934
|
-
return fs.readFileSync(openCodeTemplate, 'utf8');
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
const claudeTemplate = path.join(__dirname, '../src/claude/CLAUDE.md');
|
|
938
|
-
if (!fs.existsSync(claudeTemplate)) {
|
|
939
|
-
return null;
|
|
940
|
-
}
|
|
941
|
-
|
|
942
|
-
return createOpenCodeAgentsContent(fs.readFileSync(claudeTemplate, 'utf8'));
|
|
943
|
-
}
|
|
944
|
-
|
|
945
|
-
// Copy OpenCode project instructions to AGENTS.md at the project root.
|
|
946
|
-
function copyOpenCodeAgentsMdFile(platformKey, results, options = {}) {
|
|
947
|
-
if (platformKey !== 'opencode') return;
|
|
948
|
-
|
|
949
|
-
const shouldOverwriteManagedFiles = Boolean(options.upgrade);
|
|
950
|
-
const dest = 'AGENTS.md';
|
|
951
|
-
const content = getOpenCodeAgentsTemplateContent();
|
|
952
|
-
|
|
953
|
-
if (content) {
|
|
954
|
-
const destinationExists = fs.existsSync(dest);
|
|
955
|
-
|
|
956
|
-
if (destinationExists && !shouldOverwriteManagedFiles) {
|
|
957
|
-
const existingContent = fs.readFileSync(dest, 'utf8');
|
|
958
|
-
const nextContent = upsertCafeKitAgentsBlock(existingContent, content);
|
|
959
|
-
|
|
960
|
-
if (nextContent === existingContent) {
|
|
961
|
-
console.log(` → Skipped: AGENTS.md (already up to date)`);
|
|
962
|
-
results.skipped++;
|
|
963
|
-
return;
|
|
964
|
-
}
|
|
965
|
-
|
|
966
|
-
fs.writeFileSync(dest, nextContent, 'utf8');
|
|
967
|
-
console.log(` ↻ AGENTS.md merged`);
|
|
968
|
-
results.updated++;
|
|
969
|
-
return;
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
fs.writeFileSync(dest, content, 'utf8');
|
|
973
|
-
|
|
974
|
-
if (destinationExists && shouldOverwriteManagedFiles) {
|
|
975
|
-
console.log(` ↻ AGENTS.md overwritten`);
|
|
976
|
-
results.updated++;
|
|
977
|
-
} else {
|
|
978
|
-
console.log(` ✓ AGENTS.md installed`);
|
|
979
|
-
results.copied++;
|
|
980
|
-
}
|
|
981
|
-
} else {
|
|
982
|
-
console.log(` ⚠ AGENTS.md template source not found`);
|
|
983
|
-
results.missingDependencies++;
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
|
|
987
665
|
// Copy Claude runtime files (statusline bundle)
|
|
988
666
|
function copyClaudeRuntimeFiles(platformKey, results, options = {}) {
|
|
989
667
|
if (platformKey !== 'claude') return;
|
|
@@ -1045,262 +723,6 @@ function removeObsoleteClaudeRuntimeFiles(platformKey, results) {
|
|
|
1045
723
|
});
|
|
1046
724
|
}
|
|
1047
725
|
|
|
1048
|
-
function copyOpenCodeSharedRuntimeFiles(platformKey, results, options = {}) {
|
|
1049
|
-
if (platformKey !== 'opencode') return;
|
|
1050
|
-
|
|
1051
|
-
const shouldOverwriteManagedFiles = Boolean(options.upgrade);
|
|
1052
|
-
const srcBase = path.join(__dirname, '../src/claude');
|
|
1053
|
-
const runtimeFiles = ['gitignore', 'runtime.json'];
|
|
1054
|
-
|
|
1055
|
-
runtimeFiles.forEach(relPath => {
|
|
1056
|
-
const srcPath = path.join(srcBase, relPath);
|
|
1057
|
-
const targetRelPath = relPath === 'gitignore' ? '.gitignore' : relPath;
|
|
1058
|
-
const targetPath = path.join('.claude', targetRelPath);
|
|
1059
|
-
|
|
1060
|
-
if (!fs.existsSync(srcPath)) {
|
|
1061
|
-
console.log(` ⚠ Runtime file not found: ${relPath}`);
|
|
1062
|
-
results.missingDependencies++;
|
|
1063
|
-
return;
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
const targetExists = fs.existsSync(targetPath);
|
|
1067
|
-
const shouldCopy = shouldOverwriteManagedFiles || !targetExists;
|
|
1068
|
-
|
|
1069
|
-
if (shouldCopy) {
|
|
1070
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
1071
|
-
fs.copyFileSync(srcPath, targetPath);
|
|
1072
|
-
|
|
1073
|
-
if (targetExists) {
|
|
1074
|
-
console.log(` ↻ Shared runtime updated: ${targetRelPath}`);
|
|
1075
|
-
results.updated++;
|
|
1076
|
-
} else {
|
|
1077
|
-
console.log(` ✓ Shared runtime installed: ${targetRelPath}`);
|
|
1078
|
-
results.copied++;
|
|
1079
|
-
}
|
|
1080
|
-
} else {
|
|
1081
|
-
results.skipped++;
|
|
1082
|
-
}
|
|
1083
|
-
});
|
|
1084
|
-
}
|
|
1085
|
-
|
|
1086
|
-
function copyOpenCodeCommandTemplates(platformKey, results, options = {}) {
|
|
1087
|
-
if (platformKey !== 'opencode') return;
|
|
1088
|
-
|
|
1089
|
-
const platform = PLATFORMS[platformKey];
|
|
1090
|
-
const shouldOverwriteManagedFiles = Boolean(options.upgrade);
|
|
1091
|
-
|
|
1092
|
-
OPENCODE_COMMAND_TEMPLATES.forEach(command => {
|
|
1093
|
-
const targetPath = path.join(platform.commandsDir, command.fileName);
|
|
1094
|
-
const targetExists = fs.existsSync(targetPath);
|
|
1095
|
-
|
|
1096
|
-
if (targetExists && !shouldOverwriteManagedFiles) {
|
|
1097
|
-
console.log(` → OpenCode command exists: ${command.fileName}`);
|
|
1098
|
-
results.skipped++;
|
|
1099
|
-
return;
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
fs.mkdirSync(path.dirname(targetPath), { recursive: true });
|
|
1103
|
-
fs.writeFileSync(targetPath, createOpenCodeSkillCommandContent(command), 'utf8');
|
|
1104
|
-
|
|
1105
|
-
if (targetExists) {
|
|
1106
|
-
console.log(` ↻ OpenCode command updated: ${command.fileName}`);
|
|
1107
|
-
results.updated++;
|
|
1108
|
-
} else {
|
|
1109
|
-
console.log(` ✓ OpenCode command installed: ${command.fileName}`);
|
|
1110
|
-
results.copied++;
|
|
1111
|
-
}
|
|
1112
|
-
});
|
|
1113
|
-
}
|
|
1114
|
-
|
|
1115
|
-
function readJsonWithComments(filePath) {
|
|
1116
|
-
if (!fs.existsSync(filePath)) {
|
|
1117
|
-
return {};
|
|
1118
|
-
}
|
|
1119
|
-
|
|
1120
|
-
const rawContent = fs.readFileSync(filePath, 'utf8');
|
|
1121
|
-
const withoutBlockComments = rawContent.replace(/\/\*[\s\S]*?\*\//g, '');
|
|
1122
|
-
const withoutLineComments = withoutBlockComments.replace(/(^|[^:])\/\/.*$/gm, '$1');
|
|
1123
|
-
|
|
1124
|
-
return JSON.parse(withoutLineComments);
|
|
1125
|
-
}
|
|
1126
|
-
|
|
1127
|
-
function getOpenCodeConfigPath() {
|
|
1128
|
-
const jsonPath = 'opencode.json';
|
|
1129
|
-
const jsoncPath = 'opencode.jsonc';
|
|
1130
|
-
|
|
1131
|
-
if (fs.existsSync(jsonPath)) {
|
|
1132
|
-
return jsonPath;
|
|
1133
|
-
}
|
|
1134
|
-
|
|
1135
|
-
if (fs.existsSync(jsoncPath)) {
|
|
1136
|
-
return jsoncPath;
|
|
1137
|
-
}
|
|
1138
|
-
|
|
1139
|
-
return jsonPath;
|
|
1140
|
-
}
|
|
1141
|
-
|
|
1142
|
-
function mergeOpenCodeConfig(platformKey, results) {
|
|
1143
|
-
if (platformKey !== 'opencode') return;
|
|
1144
|
-
|
|
1145
|
-
const targetPath = getOpenCodeConfigPath();
|
|
1146
|
-
const targetExists = fs.existsSync(targetPath);
|
|
1147
|
-
let config = {};
|
|
1148
|
-
|
|
1149
|
-
try {
|
|
1150
|
-
config = readJsonWithComments(targetPath);
|
|
1151
|
-
} catch (error) {
|
|
1152
|
-
console.log(` ⚠ OpenCode config not merged: ${error.message}`);
|
|
1153
|
-
results.errors++;
|
|
1154
|
-
return;
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
const before = JSON.stringify(config);
|
|
1158
|
-
|
|
1159
|
-
if (!config.$schema) {
|
|
1160
|
-
config.$schema = 'https://opencode.ai/config.json';
|
|
1161
|
-
}
|
|
1162
|
-
|
|
1163
|
-
if (!Array.isArray(config.instructions)) {
|
|
1164
|
-
config.instructions = [];
|
|
1165
|
-
}
|
|
1166
|
-
|
|
1167
|
-
if (!config.instructions.includes('AGENTS.md')) {
|
|
1168
|
-
config.instructions.push('AGENTS.md');
|
|
1169
|
-
}
|
|
1170
|
-
|
|
1171
|
-
if (typeof config.permission === 'string') {
|
|
1172
|
-
if (config.permission === 'allow') {
|
|
1173
|
-
const afterStringPermission = JSON.stringify(config);
|
|
1174
|
-
if (before === afterStringPermission && targetExists) {
|
|
1175
|
-
console.log(` → OpenCode config already up to date: ${targetPath}`);
|
|
1176
|
-
results.skipped++;
|
|
1177
|
-
return;
|
|
1178
|
-
}
|
|
1179
|
-
|
|
1180
|
-
fs.writeFileSync(targetPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
1181
|
-
console.log(` ↻ OpenCode config merged: ${targetPath}`);
|
|
1182
|
-
results.updated++;
|
|
1183
|
-
return;
|
|
1184
|
-
}
|
|
1185
|
-
|
|
1186
|
-
console.log(` ⚠ OpenCode config permission is "${config.permission}"; preserving user setting`);
|
|
1187
|
-
} else if (!config.permission || typeof config.permission !== 'object' || Array.isArray(config.permission)) {
|
|
1188
|
-
config.permission = {};
|
|
1189
|
-
}
|
|
1190
|
-
|
|
1191
|
-
if (typeof config.permission === 'object' && !Array.isArray(config.permission)) {
|
|
1192
|
-
if (!config.permission.skill || typeof config.permission.skill !== 'object' || Array.isArray(config.permission.skill)) {
|
|
1193
|
-
config.permission.skill = {};
|
|
1194
|
-
}
|
|
1195
|
-
|
|
1196
|
-
if (!config.permission.skill['*']) {
|
|
1197
|
-
config.permission.skill['*'] = 'allow';
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
if (!config.permission.task || typeof config.permission.task !== 'object' || Array.isArray(config.permission.task)) {
|
|
1201
|
-
config.permission.task = {};
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
if (!config.permission.task['*']) {
|
|
1205
|
-
config.permission.task['*'] = 'allow';
|
|
1206
|
-
}
|
|
1207
|
-
}
|
|
1208
|
-
|
|
1209
|
-
const after = JSON.stringify(config);
|
|
1210
|
-
if (before === after && targetExists) {
|
|
1211
|
-
console.log(` → OpenCode config already up to date: ${targetPath}`);
|
|
1212
|
-
results.skipped++;
|
|
1213
|
-
return;
|
|
1214
|
-
}
|
|
1215
|
-
|
|
1216
|
-
fs.writeFileSync(targetPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
1217
|
-
|
|
1218
|
-
if (targetExists) {
|
|
1219
|
-
console.log(` ↻ OpenCode config merged: ${targetPath}`);
|
|
1220
|
-
results.updated++;
|
|
1221
|
-
} else {
|
|
1222
|
-
console.log(` ✓ OpenCode config created: ${targetPath}`);
|
|
1223
|
-
results.copied++;
|
|
1224
|
-
}
|
|
1225
|
-
}
|
|
1226
|
-
|
|
1227
|
-
function isValidOpenCodeModel(model) {
|
|
1228
|
-
return typeof model === 'string' && /^[^/\s]+\/[^/\s]+$/.test(model.trim());
|
|
1229
|
-
}
|
|
1230
|
-
|
|
1231
|
-
function writeOpenCodeModel(model, results, sourceLabel = 'user input') {
|
|
1232
|
-
const targetPath = getOpenCodeConfigPath();
|
|
1233
|
-
let config = {};
|
|
1234
|
-
|
|
1235
|
-
try {
|
|
1236
|
-
config = readJsonWithComments(targetPath);
|
|
1237
|
-
} catch (error) {
|
|
1238
|
-
console.log(` ⚠ OpenCode model not configured: ${error.message}`);
|
|
1239
|
-
results.errors++;
|
|
1240
|
-
return false;
|
|
1241
|
-
}
|
|
1242
|
-
|
|
1243
|
-
if (typeof config.model === 'string' && config.model.trim()) {
|
|
1244
|
-
console.log(` → OpenCode model already configured: ${config.model}`);
|
|
1245
|
-
results.skipped++;
|
|
1246
|
-
return false;
|
|
1247
|
-
}
|
|
1248
|
-
|
|
1249
|
-
config.model = model.trim();
|
|
1250
|
-
fs.writeFileSync(targetPath, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
1251
|
-
console.log(` ✓ OpenCode model configured from ${sourceLabel}: ${config.model}`);
|
|
1252
|
-
results.updated++;
|
|
1253
|
-
return true;
|
|
1254
|
-
}
|
|
1255
|
-
|
|
1256
|
-
async function promptOpenCodeModel() {
|
|
1257
|
-
const rl = readline.createInterface({
|
|
1258
|
-
input: process.stdin,
|
|
1259
|
-
output: process.stdout
|
|
1260
|
-
});
|
|
1261
|
-
|
|
1262
|
-
console.log('\n📝 OpenCode Model Configuration');
|
|
1263
|
-
console.log(' Optional but recommended for subagents.');
|
|
1264
|
-
console.log(' Format: provider/model-id (example: anthropic/claude-sonnet-4-20250514)');
|
|
1265
|
-
console.log();
|
|
1266
|
-
|
|
1267
|
-
return new Promise((resolve) => {
|
|
1268
|
-
rl.question('Enter OpenCode model (or press Enter to skip): ', (answer) => {
|
|
1269
|
-
rl.close();
|
|
1270
|
-
resolve(answer.trim());
|
|
1271
|
-
});
|
|
1272
|
-
});
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
|
-
async function setupOpenCodeModel(platforms, results) {
|
|
1276
|
-
if (!platforms.includes('opencode')) return;
|
|
1277
|
-
|
|
1278
|
-
const envModel = process.env.OPENCODE_MODEL || process.env.OPENCODE_DEFAULT_MODEL;
|
|
1279
|
-
if (envModel) {
|
|
1280
|
-
if (isValidOpenCodeModel(envModel)) {
|
|
1281
|
-
writeOpenCodeModel(envModel, results, 'environment');
|
|
1282
|
-
return;
|
|
1283
|
-
}
|
|
1284
|
-
|
|
1285
|
-
console.log(` ⚠ Ignoring invalid OpenCode model from environment: ${envModel}`);
|
|
1286
|
-
}
|
|
1287
|
-
|
|
1288
|
-
const model = await promptOpenCodeModel();
|
|
1289
|
-
|
|
1290
|
-
if (!model) {
|
|
1291
|
-
console.log(' ℹ Skipped OpenCode model configuration');
|
|
1292
|
-
console.log(' Set later in opencode.json as: "model": "provider/model-id"');
|
|
1293
|
-
return;
|
|
1294
|
-
}
|
|
1295
|
-
|
|
1296
|
-
if (!isValidOpenCodeModel(model)) {
|
|
1297
|
-
console.log(' ⚠ Invalid OpenCode model format. Expected provider/model-id. Skipped.');
|
|
1298
|
-
return;
|
|
1299
|
-
}
|
|
1300
|
-
|
|
1301
|
-
writeOpenCodeModel(model, results);
|
|
1302
|
-
}
|
|
1303
|
-
|
|
1304
726
|
function pruneObsoleteSettingsHooks(settings, results) {
|
|
1305
727
|
const obsoleteSubstrings = CLAUDE_MIGRATION_MANIFEST?.obsolete?.settingsHookCommandSubstrings || [];
|
|
1306
728
|
|
|
@@ -1449,10 +871,10 @@ function copyRulesDirectory(platformKey, results, options = {}) {
|
|
|
1449
871
|
if (!isClaudeCompatibleRuntime(platformKey)) return;
|
|
1450
872
|
|
|
1451
873
|
const source = path.join(__dirname, '../src/claude/rules');
|
|
1452
|
-
const dest =
|
|
874
|
+
const dest = getRuntimeSupportTargetDir(platformKey, 'rules');
|
|
1453
875
|
|
|
1454
876
|
if (fs.existsSync(source)) {
|
|
1455
|
-
copyRecursive(source, dest, { upgrade: true });
|
|
877
|
+
copyRecursive(source, dest, getCopyOptions(platformKey, { upgrade: true }));
|
|
1456
878
|
console.log(` ↻ rules/ directory overwritten`);
|
|
1457
879
|
results.updated++;
|
|
1458
880
|
} else {
|
|
@@ -1563,26 +985,37 @@ async function promptGeminiAPIKey() {
|
|
|
1563
985
|
});
|
|
1564
986
|
}
|
|
1565
987
|
|
|
1566
|
-
function configureGeminiKey(apiKey) {
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
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);
|
|
1570
993
|
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
994
|
+
if (targets.length === 0) {
|
|
995
|
+
targets.push('.claude');
|
|
996
|
+
}
|
|
1574
997
|
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
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');
|
|
1578
1003
|
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
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
|
+
}
|
|
1585
1016
|
}
|
|
1017
|
+
|
|
1018
|
+
return success;
|
|
1586
1019
|
}
|
|
1587
1020
|
|
|
1588
1021
|
async function setupGeminiCLI(platforms) {
|
|
@@ -1604,7 +1037,7 @@ async function setupGeminiCLI(platforms) {
|
|
|
1604
1037
|
// Always prompt for API config since we need it for hapo:test Multimodal
|
|
1605
1038
|
const apiKey = await promptGeminiAPIKey();
|
|
1606
1039
|
if (apiKey) {
|
|
1607
|
-
configureGeminiKey(apiKey);
|
|
1040
|
+
configureGeminiKey(apiKey, platforms);
|
|
1608
1041
|
} else {
|
|
1609
1042
|
console.log('\n ℹ Skipped API key configuration');
|
|
1610
1043
|
console.log(' Set later: export GEMINI_API_KEY="your-key"');
|
|
@@ -1651,6 +1084,8 @@ async function main() {
|
|
|
1651
1084
|
const platformNames = platforms.map(key => PLATFORMS[key].name).join(', ');
|
|
1652
1085
|
console.log(`\nInstalling for: ${platformNames}`);
|
|
1653
1086
|
|
|
1087
|
+
warnLegacyClaudeFolder(platforms);
|
|
1088
|
+
|
|
1654
1089
|
if (installerOptions.upgrade) {
|
|
1655
1090
|
console.log('Mode: upgrade (overwrite managed files)\n');
|
|
1656
1091
|
} else {
|
|
@@ -1689,6 +1124,7 @@ async function main() {
|
|
|
1689
1124
|
|
|
1690
1125
|
if (platformKey === 'opencode') {
|
|
1691
1126
|
copyOpenCodeSharedRuntimeFiles(platformKey, results, installerOptions);
|
|
1127
|
+
copyOpenCodePlugins(platformKey, results, installerOptions);
|
|
1692
1128
|
copyOpenCodeCommandTemplates(platformKey, results, installerOptions);
|
|
1693
1129
|
copyOpenCodeAgentsMdFile(platformKey, results, installerOptions);
|
|
1694
1130
|
copyRulesDirectory(platformKey, results, installerOptions);
|
|
@@ -1749,7 +1185,8 @@ async function main() {
|
|
|
1749
1185
|
} else {
|
|
1750
1186
|
console.log('\n 2. CafeKit skills and AGENTS.md are installed for the selected runtime');
|
|
1751
1187
|
}
|
|
1752
|
-
|
|
1188
|
+
const envHosts = platforms.map(key => `${PLATFORMS[key].folder}/.env`).join(' and ');
|
|
1189
|
+
console.log(` 3. Project API Keys are now securely isolated in ${envHosts}`);
|
|
1753
1190
|
if (!installerOptions.upgrade) {
|
|
1754
1191
|
console.log(' 4. To refresh managed templates later, run installer with --upgrade');
|
|
1755
1192
|
}
|