@haposoft/cafekit 0.8.16 → 0.9.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.
Files changed (33) hide show
  1. package/README.md +63 -67
  2. package/bin/install.js +141 -194
  3. package/bin/lib/copy-utils.js +61 -0
  4. package/bin/lib/opencode-install.js +866 -0
  5. package/package.json +4 -1
  6. package/src/claude/CLAUDE.md +2 -2
  7. package/src/claude/migration-manifest.json +1 -0
  8. package/src/claude/rules/skill-domain-routing.md +2 -0
  9. package/src/claude/rules/skill-workflow-routing.md +3 -1
  10. package/src/claude/skills/qs/SKILL.md +177 -0
  11. package/src/claude/skills/qs/templates/qs.md +30 -0
  12. package/src/opencode/AGENTS.md +140 -0
  13. package/src/opencode/plugins/docs-sync.ts +181 -0
  14. package/src/opencode/plugins/inspect-block.ts +164 -0
  15. package/src/opencode/plugins/privacy-block.ts +182 -0
  16. package/src/opencode/plugins/rules.ts +163 -0
  17. package/src/opencode/plugins/session.ts +179 -0
  18. package/src/opencode/plugins/state.ts +253 -0
  19. package/src/opencode/plugins/usage.ts +178 -0
  20. package/src/antigravity/GEMINI.md +0 -162
  21. package/src/antigravity/workflows/code.md +0 -16
  22. package/src/antigravity/workflows/docs-init.md +0 -432
  23. package/src/antigravity/workflows/docs-update.md +0 -245
  24. package/src/antigravity/workflows/impact-analysis-output-example.md +0 -313
  25. package/src/antigravity/workflows/impact-analysis.md +0 -730
  26. package/src/antigravity/workflows/review.md +0 -15
  27. package/src/antigravity/workflows/spec-design.md +0 -220
  28. package/src/antigravity/workflows/spec-init.md +0 -78
  29. package/src/antigravity/workflows/spec-requirements.md +0 -122
  30. package/src/antigravity/workflows/spec-status.md +0 -95
  31. package/src/antigravity/workflows/spec-tasks.md +0 -156
  32. package/src/antigravity/workflows/spec-validate.md +0 -106
  33. package/src/antigravity/workflows/test.md +0 -13
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 (.claude/)
9
- * - antigravity: Antigravity (.agent/)
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) {
@@ -57,65 +73,11 @@ const CLAUDE_MIGRATION_MANIFEST = loadClaudeMigrationManifest();
57
73
  const DEPENDENCY_TEMPLATES = {
58
74
  commands: {
59
75
  claude: {},
60
- antigravity: {
61
- 'code.md': `---
62
- description: Implement approved work from specification tasks and then hand off to test and hapo:code-review.
63
- allowed-tools: Read, Glob, Grep, Edit, Write, Bash
64
- argument-hint: <feature-name>
65
- ---
66
-
67
- # /code
68
-
69
- Use this workflow after /spec-tasks.
70
-
71
- 1. Read .specs/$ARGUMENTS/tasks.md and identify the next pending task.
72
- 2. Implement only that task following project standards.
73
- 3. Run /test.
74
- 4. Run /hapo:code-review.
75
-
76
- Preferred flow: /spec-init -> /spec-requirements -> /spec-design -> /spec-tasks -> /code -> /test -> /hapo:code-review
77
- `,
78
- 'test.md': `---
79
- description: Run project tests and report failures concisely.
80
- allowed-tools: Bash, Read, Grep
81
- argument-hint: [scope]
82
- ---
83
-
84
- # /test
85
-
86
- Run the project's test command and report:
87
- - total passed/failed
88
- - failing test names
89
- - root cause hints
90
- - next fix action
91
- `,
92
- }
76
+ opencode: {}
93
77
  },
94
78
  agents: {
95
79
  claude: {},
96
- antigravity: {
97
- 'frontend-specialist.md': `---
98
- name: frontend-specialist
99
- description: Implement approved UI and interaction tasks from specifications.
100
- ---
101
-
102
- Implement UI tasks from approved specs with accessibility and responsive behavior.
103
- `,
104
- 'test-engineer.md': `---
105
- name: test-engineer
106
- description: Execute tests and report reliability issues.
107
- ---
108
-
109
- Run test suites, highlight failures, and propose precise fixes.
110
- `,
111
- 'code-archaeologist.md': `---
112
- name: code-archaeologist
113
- description: Review recent changes for regressions and hidden impacts.
114
- ---
115
-
116
- Inspect changed code paths, dependencies, and potential regressions.
117
- `
118
- }
80
+ opencode: {}
119
81
  }
120
82
  };
121
83
 
@@ -136,18 +98,19 @@ const PLATFORMS = {
136
98
  sourceDir: 'claude', // Maps to src/claude/
137
99
  sourceSubdir: 'commands' // Source subfolder within src/claude/
138
100
  },
139
- antigravity: {
140
- id: 'antigravity',
141
- name: 'Antigravity',
142
- description: 'Google\'s Antigravity Kit',
143
- folder: '.agent',
144
- commandsDir: '.agent/workflows', // Antigravity uses workflows/ not commands/
145
- skillsDir: '.agent/skills',
146
- agentsDir: '.agent/agents',
147
- skillsRef: '.agent/skills',
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',
148
111
  commandPrefix: '/',
149
- sourceDir: 'antigravity', // Maps to src/antigravity/
150
- sourceSubdir: 'workflows' // Source subfolder within src/antigravity/
112
+ sourceDir: 'claude',
113
+ sourceSubdir: 'archive-command'
151
114
  }
152
115
  // Add new platforms here:
153
116
  // cursor: {
@@ -169,7 +132,8 @@ function detectPlatforms() {
169
132
  const detected = [];
170
133
 
171
134
  for (const [key, config] of Object.entries(PLATFORMS)) {
172
- if (fs.existsSync(config.folder)) {
135
+ const markers = Array.isArray(config.detectFiles) ? config.detectFiles : [config.folder];
136
+ if (markers.some(marker => fs.existsSync(marker))) {
173
137
  detected.push(key);
174
138
  }
175
139
  }
@@ -189,6 +153,30 @@ function getPlatformKeys() {
189
153
  return Object.keys(PLATFORMS);
190
154
  }
191
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
+
192
180
  function parseInstallerArgs(argv) {
193
181
  const args = {
194
182
  upgrade: false
@@ -205,6 +193,13 @@ function parseInstallerArgs(argv) {
205
193
  return args;
206
194
  }
207
195
 
196
+ function getCopyOptions(platformKey, baseOptions = {}) {
197
+ if (platformKey === 'opencode') {
198
+ return getOpenCodeCopyOptions(baseOptions);
199
+ }
200
+ return baseOptions;
201
+ }
202
+
208
203
  // ═══════════════════════════════════════════════════════════
209
204
  // USER INTERACTION
210
205
  // ═══════════════════════════════════════════════════════════
@@ -271,30 +266,6 @@ async function promptMultiPlatformConfirm(detected) {
271
266
  // FILE OPERATIONS
272
267
  // ═══════════════════════════════════════════════════════════
273
268
 
274
- function copyRecursive(src, dest, options = {}) {
275
- const exists = fs.existsSync(src);
276
- const stats = exists && fs.statSync(src);
277
- const isDirectory = exists && stats.isDirectory();
278
- const shouldOverwriteManagedFiles = Boolean(options.upgrade);
279
-
280
- if (isDirectory) {
281
- if (!fs.existsSync(dest)) {
282
- fs.mkdirSync(dest, { recursive: true });
283
- }
284
- fs.readdirSync(src).forEach(childItemName => {
285
- const destItemName = childItemName === 'gitignore' ? '.gitignore' : childItemName;
286
- copyRecursive(path.join(src, childItemName), path.join(dest, destItemName), options);
287
- });
288
- } else {
289
- if (fs.existsSync(dest) && !shouldOverwriteManagedFiles) {
290
- return;
291
- }
292
-
293
- fs.mkdirSync(path.dirname(dest), { recursive: true });
294
- fs.copyFileSync(src, dest);
295
- }
296
- }
297
-
298
269
  function ensureDependencyFile(targetPath, content, results, label, options = {}) {
299
270
  const destinationExists = fs.existsSync(targetPath);
300
271
  const shouldOverwriteManagedFiles = Boolean(options.upgrade);
@@ -340,18 +311,6 @@ function ensureWorkflowDependencies(platformKey, platform, results, options = {}
340
311
  });
341
312
  }
342
313
 
343
- function readJsonFile(filePath) {
344
- if (!fs.existsSync(filePath)) {
345
- return {};
346
- }
347
-
348
- try {
349
- return JSON.parse(fs.readFileSync(filePath, 'utf8'));
350
- } catch {
351
- return {};
352
- }
353
- }
354
-
355
314
  function writePlatformVersionMetadata(platformKey, results) {
356
315
  const platform = PLATFORMS[platformKey];
357
316
  const targetPath = path.join(platform.folder, 'cafekit.json');
@@ -409,18 +368,8 @@ function getPlatformSpecFiles(platformKey) {
409
368
  ];
410
369
  }
411
370
 
412
- if (platformKey === 'antigravity') {
413
- return [
414
- 'spec-init.md',
415
- 'spec-requirements.md',
416
- 'spec-design.md',
417
- 'spec-validate.md',
418
- 'spec-tasks.md',
419
- 'spec-status.md',
420
- 'impact-analysis.md',
421
- 'docs-init.md',
422
- 'docs-update.md'
423
- ];
371
+ if (platformKey === 'opencode') {
372
+ return [];
424
373
  }
425
374
 
426
375
  return [];
@@ -444,9 +393,10 @@ function copyPlatformFiles(platformKey, results, options = {}) {
444
393
  const agentsSourceDir = path.join(__dirname, `../src/${platform.sourceDir}/agents`);
445
394
 
446
395
  // Create directories
447
- if (!fs.existsSync(platform.commandsDir)) {
448
- fs.mkdirSync(platform.commandsDir, { recursive: true });
449
- }
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.
450
400
  if (!fs.existsSync(platform.skillsDir)) {
451
401
  fs.mkdirSync(platform.skillsDir, { recursive: true });
452
402
  }
@@ -461,7 +411,7 @@ function copyPlatformFiles(platformKey, results, options = {}) {
461
411
 
462
412
  if (fs.existsSync(specSkillSource)) {
463
413
  const skillExisted = fs.existsSync(specSkillDest);
464
- copyRecursive(specSkillSource, specSkillDest, options);
414
+ copyRecursive(specSkillSource, specSkillDest, getCopyOptions(platformKey, options));
465
415
  results.installedSkills++;
466
416
 
467
417
  if (shouldOverwriteManagedFiles && skillExisted) {
@@ -525,11 +475,8 @@ function copyPlatformFiles(platformKey, results, options = {}) {
525
475
 
526
476
  // Copy additional required skills based on platform
527
477
  let requiredSkills = [];
528
- if (platformKey === 'claude') {
478
+ if (isClaudeCompatibleRuntime(platformKey)) {
529
479
  requiredSkills = CLAUDE_MIGRATION_MANIFEST?.skills?.required || [];
530
- } else if (platformKey === 'antigravity') {
531
- // Antigravity also needs shared investigation and impact-analysis skills
532
- requiredSkills = ['impact-analysis', 'debug', 'ai-multimodal', 'generate-graph'];
533
480
  }
534
481
 
535
482
  requiredSkills
@@ -540,7 +487,7 @@ function copyPlatformFiles(platformKey, results, options = {}) {
540
487
 
541
488
  if (fs.existsSync(skillSource)) {
542
489
  const skillExisted = fs.existsSync(skillDest);
543
- copyRecursive(skillSource, skillDest, options);
490
+ copyRecursive(skillSource, skillDest, getCopyOptions(platformKey, options));
544
491
  results.installedSkills++;
545
492
 
546
493
  if (shouldOverwriteManagedFiles && skillExisted) {
@@ -557,7 +504,7 @@ function copyPlatformFiles(platformKey, results, options = {}) {
557
504
  }
558
505
 
559
506
  // Copy agents
560
- if (platformKey === 'claude') {
507
+ if (isClaudeCompatibleRuntime(platformKey)) {
561
508
  if (fs.existsSync(agentsSourceDir)) {
562
509
  const requiredAgents = CLAUDE_MIGRATION_MANIFEST?.agents?.required || [
563
510
  'tester.md',
@@ -586,7 +533,12 @@ function copyPlatformFiles(platformKey, results, options = {}) {
586
533
  }
587
534
 
588
535
  fs.mkdirSync(path.dirname(dest), { recursive: true });
589
- fs.copyFileSync(source, dest);
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
+ }
590
542
  results.dependencyChecks++;
591
543
  results.installedDependencies++;
592
544
 
@@ -601,9 +553,9 @@ function copyPlatformFiles(platformKey, results, options = {}) {
601
553
  // Copy agent reference manuals (debugger manuals, etc.) recursively
602
554
  const refsSource = path.join(__dirname, '../src', platform.sourceDir, 'references');
603
555
  if (fs.existsSync(refsSource)) {
604
- const refsDest = path.join(platform.folder, 'references');
556
+ const refsDest = getRuntimeSupportTargetDir(platformKey, 'references');
605
557
  const refsExisted = fs.existsSync(refsDest);
606
- copyRecursive(refsSource, refsDest, options);
558
+ copyRecursive(refsSource, refsDest, getCopyOptions(platformKey, options));
607
559
 
608
560
  if (shouldOverwriteManagedFiles && refsExisted) {
609
561
  console.log(` ↻ Agent reference manuals updated`);
@@ -622,9 +574,9 @@ function copyPlatformFiles(platformKey, results, options = {}) {
622
574
  // Copy native scripts (browser-tool, docs-fetch, validate-docs)
623
575
  const scriptsSourceDir = path.join(__dirname, '../src/claude/scripts');
624
576
  if (fs.existsSync(scriptsSourceDir)) {
625
- const scriptsDest = path.join(platform.folder, 'scripts');
577
+ const scriptsDest = getRuntimeSupportTargetDir(platformKey, 'scripts');
626
578
  const scriptsExisted = fs.existsSync(scriptsDest);
627
- copyRecursive(scriptsSourceDir, scriptsDest, options);
579
+ copyRecursive(scriptsSourceDir, scriptsDest, getCopyOptions(platformKey, options));
628
580
 
629
581
  if (shouldOverwriteManagedFiles && scriptsExisted) {
630
582
  console.log(` ↻ Native scripts updated`);
@@ -662,6 +614,9 @@ function copyPlatformFiles(platformKey, results, options = {}) {
662
614
  fs.mkdirSync(path.dirname(dest), { recursive: true });
663
615
  let content = fs.readFileSync(source, 'utf8');
664
616
  content = content.replace(/\{\{SKILLS_DIR\}\}/g, platform.skillsRef);
617
+ if (platformKey === 'opencode') {
618
+ content = convertOpenCodeCommandContent(content, file);
619
+ }
665
620
  fs.writeFileSync(dest, content);
666
621
 
667
622
  if (destinationExists && shouldOverwriteManagedFiles) {
@@ -692,6 +647,9 @@ function copyRoutingFile(platformKey, results, options = {}) {
692
647
 
693
648
  let content = fs.readFileSync(source, 'utf8');
694
649
  content = content.replace(/\{\{SKILLS_DIR\}\}/g, platform.skillsRef);
650
+ if (platformKey === 'opencode') {
651
+ content = normalizeOpenCodeBody(content);
652
+ }
695
653
  fs.writeFileSync(dest, content);
696
654
 
697
655
  if (destinationExists && shouldOverwriteManagedFiles) {
@@ -704,40 +662,6 @@ function copyRoutingFile(platformKey, results, options = {}) {
704
662
  }
705
663
  }
706
664
 
707
- // Copy GEMINI.md rule file to .agent/rules/ for Antigravity
708
- function copyGeminiFile(platformKey, results, options = {}) {
709
- const platform = PLATFORMS[platformKey];
710
- const shouldOverwriteManagedFiles = Boolean(options.upgrade);
711
- const rulesDir = path.join(platform.folder, 'rules');
712
- const source = path.join(__dirname, `../src/${platform.sourceDir}/GEMINI.md`);
713
- const dest = path.join(rulesDir, 'GEMINI.md');
714
-
715
- if (fs.existsSync(source)) {
716
- // Create rules directory if not exists
717
- if (!fs.existsSync(rulesDir)) {
718
- fs.mkdirSync(rulesDir, { recursive: true });
719
- }
720
-
721
- const destinationExists = fs.existsSync(dest);
722
-
723
- if (destinationExists && !shouldOverwriteManagedFiles) {
724
- console.log(` → Skipped: rules/GEMINI.md (already exists)`);
725
- results.skipped++;
726
- return;
727
- }
728
-
729
- fs.copyFileSync(source, dest);
730
-
731
- if (destinationExists && shouldOverwriteManagedFiles) {
732
- console.log(` ↻ Updated: rules/GEMINI.md`);
733
- results.updated++;
734
- } else {
735
- console.log(` ✓ Copied: rules/GEMINI.md`);
736
- results.copied++;
737
- }
738
- }
739
- }
740
-
741
665
  // Copy Claude runtime files (statusline bundle)
742
666
  function copyClaudeRuntimeFiles(platformKey, results, options = {}) {
743
667
  if (platformKey !== 'claude') return;
@@ -944,13 +868,13 @@ function copyClaudeMdFile(platformKey, results, options = {}) {
944
868
 
945
869
  // Copy rules directory (always overwrite)
946
870
  function copyRulesDirectory(platformKey, results, options = {}) {
947
- if (platformKey !== 'claude') return;
871
+ if (!isClaudeCompatibleRuntime(platformKey)) return;
948
872
 
949
873
  const source = path.join(__dirname, '../src/claude/rules');
950
- const dest = path.join(PLATFORMS.claude.folder, 'rules');
874
+ const dest = getRuntimeSupportTargetDir(platformKey, 'rules');
951
875
 
952
876
  if (fs.existsSync(source)) {
953
- copyRecursive(source, dest, { upgrade: true });
877
+ copyRecursive(source, dest, getCopyOptions(platformKey, { upgrade: true }));
954
878
  console.log(` ↻ rules/ directory overwritten`);
955
879
  results.updated++;
956
880
  } else {
@@ -1061,26 +985,37 @@ async function promptGeminiAPIKey() {
1061
985
  });
1062
986
  }
1063
987
 
1064
- function configureGeminiKey(apiKey) {
1065
- try {
1066
- const targetDir = path.join(process.cwd(), '.claude');
1067
- const localEnvFile = path.join(targetDir, '.env');
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);
1068
993
 
1069
- if (!fs.existsSync(targetDir)) {
1070
- fs.mkdirSync(targetDir, { recursive: true });
1071
- }
994
+ if (targets.length === 0) {
995
+ targets.push('.claude');
996
+ }
1072
997
 
1073
- // Luôn ghi trực tiếp key vào rốn của não bộ AI
1074
- fs.writeFileSync(localEnvFile, `GEMINI_API_KEY=${apiKey}\nVISUAL_MODEL=gemma-4-31b-it\nSEARCH_MODEL=gemini-2.5-pro\n`, { mode: 0o600 });
1075
- console.log(' ✓ Gemini API key configured securely in project (.claude/.env)');
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');
1076
1003
 
1077
- return true;
1078
- } catch (error) {
1079
- console.log(' ✗ Failed to configure Gemini API key');
1080
- console.log(` Error: ${error.message}`);
1081
- console.log(' You can manually set: export GEMINI_API_KEY="your-key"');
1082
- return false;
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
+ }
1083
1016
  }
1017
+
1018
+ return success;
1084
1019
  }
1085
1020
 
1086
1021
  async function setupGeminiCLI(platforms) {
@@ -1102,7 +1037,7 @@ async function setupGeminiCLI(platforms) {
1102
1037
  // Always prompt for API config since we need it for hapo:test Multimodal
1103
1038
  const apiKey = await promptGeminiAPIKey();
1104
1039
  if (apiKey) {
1105
- configureGeminiKey(apiKey);
1040
+ configureGeminiKey(apiKey, platforms);
1106
1041
  } else {
1107
1042
  console.log('\n ℹ Skipped API key configuration');
1108
1043
  console.log(' Set later: export GEMINI_API_KEY="your-key"');
@@ -1149,6 +1084,8 @@ async function main() {
1149
1084
  const platformNames = platforms.map(key => PLATFORMS[key].name).join(', ');
1150
1085
  console.log(`\nInstalling for: ${platformNames}`);
1151
1086
 
1087
+ warnLegacyClaudeFolder(platforms);
1088
+
1152
1089
  if (installerOptions.upgrade) {
1153
1090
  console.log('Mode: upgrade (overwrite managed files)\n');
1154
1091
  } else {
@@ -1185,9 +1122,13 @@ async function main() {
1185
1122
  copyRulesDirectory(platformKey, results, installerOptions);
1186
1123
  }
1187
1124
 
1188
- // Copy GEMINI.md for Antigravity platform
1189
- if (platformKey === 'antigravity') {
1190
- copyGeminiFile(platformKey, results, installerOptions);
1125
+ if (platformKey === 'opencode') {
1126
+ copyOpenCodeSharedRuntimeFiles(platformKey, results, installerOptions);
1127
+ copyOpenCodePlugins(platformKey, results, installerOptions);
1128
+ copyOpenCodeCommandTemplates(platformKey, results, installerOptions);
1129
+ copyOpenCodeAgentsMdFile(platformKey, results, installerOptions);
1130
+ copyRulesDirectory(platformKey, results, installerOptions);
1131
+ mergeOpenCodeConfig(platformKey, results);
1191
1132
  }
1192
1133
 
1193
1134
  writePlatformVersionMetadata(platformKey, results);
@@ -1202,7 +1143,8 @@ async function main() {
1202
1143
  ensureGitignore(results, installerOptions);
1203
1144
  console.log();
1204
1145
 
1205
- // Setup Gemini CLI and API Key config
1146
+ // Setup runtime-specific optional model/API configuration
1147
+ await setupOpenCodeModel(platforms, results);
1206
1148
  await setupGeminiCLI(platforms);
1207
1149
 
1208
1150
  // Summary
@@ -1238,8 +1180,13 @@ async function main() {
1238
1180
  }
1239
1181
  }
1240
1182
 
1241
- console.log('\n 2. The AI will automatically sync docs/ (Continuous Documentation)');
1242
- console.log(' 3. Project API Keys are now securely isolated in .claude/.env');
1183
+ if (platforms.includes('claude')) {
1184
+ console.log('\n 2. Claude Code will automatically sync docs/ (Continuous Documentation)');
1185
+ } else {
1186
+ console.log('\n 2. CafeKit skills and AGENTS.md are installed for the selected runtime');
1187
+ }
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}`);
1243
1190
  if (!installerOptions.upgrade) {
1244
1191
  console.log(' 4. To refresh managed templates later, run installer with --upgrade');
1245
1192
  }
@@ -0,0 +1,61 @@
1
+ 'use strict';
2
+
3
+ const fs = require('fs');
4
+ const path = require('path');
5
+
6
+ const TEXT_REWRITE_EXTENSIONS = new Set([
7
+ '.md', '.mdx', '.txt', '.json', '.cjs', '.js', '.mjs', '.ts', '.sh', '.yml', '.yaml'
8
+ ]);
9
+
10
+ function isTextAsset(filePath) {
11
+ return TEXT_REWRITE_EXTENSIONS.has(path.extname(filePath).toLowerCase());
12
+ }
13
+
14
+ function copyRecursive(src, dest, options = {}) {
15
+ const exists = fs.existsSync(src);
16
+ const stats = exists && fs.statSync(src);
17
+ const isDirectory = exists && stats.isDirectory();
18
+ const shouldOverwriteManagedFiles = Boolean(options.upgrade);
19
+
20
+ if (isDirectory) {
21
+ if (!fs.existsSync(dest)) {
22
+ fs.mkdirSync(dest, { recursive: true });
23
+ }
24
+ fs.readdirSync(src).forEach(childItemName => {
25
+ const destItemName = childItemName === 'gitignore' ? '.gitignore' : childItemName;
26
+ copyRecursive(path.join(src, childItemName), path.join(dest, destItemName), options);
27
+ });
28
+ } else {
29
+ if (fs.existsSync(dest) && !shouldOverwriteManagedFiles) {
30
+ return;
31
+ }
32
+
33
+ fs.mkdirSync(path.dirname(dest), { recursive: true });
34
+
35
+ if (typeof options.transform === 'function' && isTextAsset(src)) {
36
+ const original = fs.readFileSync(src, 'utf8');
37
+ fs.writeFileSync(dest, options.transform(original, src), 'utf8');
38
+ } else {
39
+ fs.copyFileSync(src, dest);
40
+ }
41
+ }
42
+ }
43
+
44
+ function readJsonFile(filePath) {
45
+ if (!fs.existsSync(filePath)) {
46
+ return {};
47
+ }
48
+
49
+ try {
50
+ return JSON.parse(fs.readFileSync(filePath, 'utf8'));
51
+ } catch {
52
+ return {};
53
+ }
54
+ }
55
+
56
+ module.exports = {
57
+ TEXT_REWRITE_EXTENSIONS,
58
+ isTextAsset,
59
+ copyRecursive,
60
+ readJsonFile
61
+ };