@haposoft/cafekit 0.8.15 → 0.8.17

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 CHANGED
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Supported platforms:
8
8
  * - claude: Claude Code (.claude/)
9
- * - antigravity: Antigravity (.agent/)
9
+ * - opencode: OpenCode (.opencode/)
10
10
  *
11
11
  * To add a new platform:
12
12
  * 1. Add to PLATFORMS registry below
@@ -57,68 +57,100 @@ const CLAUDE_MIGRATION_MANIFEST = loadClaudeMigrationManifest();
57
57
  const DEPENDENCY_TEMPLATES = {
58
58
  commands: {
59
59
  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
- }
60
+ opencode: {}
93
61
  },
94
62
  agents: {
95
63
  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
- }
64
+ opencode: {}
119
65
  }
120
66
  };
121
67
 
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
+
122
154
  // ═══════════════════════════════════════════════════════════
123
155
  // PLATFORM REGISTRY - Add new platforms here
124
156
  // ═══════════════════════════════════════════════════════════
@@ -136,18 +168,19 @@ const PLATFORMS = {
136
168
  sourceDir: 'claude', // Maps to src/claude/
137
169
  sourceSubdir: 'commands' // Source subfolder within src/claude/
138
170
  },
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',
171
+ opencode: {
172
+ id: 'opencode',
173
+ name: 'OpenCode',
174
+ description: 'OpenCode terminal AI coding agent',
175
+ folder: '.opencode',
176
+ detectFiles: ['.opencode', 'opencode.json', 'opencode.jsonc'],
177
+ commandsDir: '.opencode/commands',
178
+ skillsDir: '.claude/skills',
179
+ agentsDir: '.opencode/agents',
180
+ skillsRef: '.claude/skills',
148
181
  commandPrefix: '/',
149
- sourceDir: 'antigravity', // Maps to src/antigravity/
150
- sourceSubdir: 'workflows' // Source subfolder within src/antigravity/
182
+ sourceDir: 'claude',
183
+ sourceSubdir: 'archive-command'
151
184
  }
152
185
  // Add new platforms here:
153
186
  // cursor: {
@@ -169,7 +202,8 @@ function detectPlatforms() {
169
202
  const detected = [];
170
203
 
171
204
  for (const [key, config] of Object.entries(PLATFORMS)) {
172
- if (fs.existsSync(config.folder)) {
205
+ const markers = Array.isArray(config.detectFiles) ? config.detectFiles : [config.folder];
206
+ if (markers.some(marker => fs.existsSync(marker))) {
173
207
  detected.push(key);
174
208
  }
175
209
  }
@@ -189,6 +223,18 @@ function getPlatformKeys() {
189
223
  return Object.keys(PLATFORMS);
190
224
  }
191
225
 
226
+ function isClaudeCompatibleRuntime(platformKey) {
227
+ return platformKey === 'claude' || platformKey === 'opencode';
228
+ }
229
+
230
+ function getClaudeSupportTargetDir(platformKey, subdir) {
231
+ if (platformKey === 'opencode') {
232
+ return path.join('.claude', subdir);
233
+ }
234
+
235
+ return path.join(PLATFORMS[platformKey].folder, subdir);
236
+ }
237
+
192
238
  function parseInstallerArgs(argv) {
193
239
  const args = {
194
240
  upgrade: false
@@ -205,6 +251,149 @@ function parseInstallerArgs(argv) {
205
251
  return args;
206
252
  }
207
253
 
254
+ function splitFrontmatter(content) {
255
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
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)}`);
378
+ }
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');
395
+ }
396
+
208
397
  // ═══════════════════════════════════════════════════════════
209
398
  // USER INTERACTION
210
399
  // ═══════════════════════════════════════════════════════════
@@ -409,18 +598,8 @@ function getPlatformSpecFiles(platformKey) {
409
598
  ];
410
599
  }
411
600
 
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
- ];
601
+ if (platformKey === 'opencode') {
602
+ return [];
424
603
  }
425
604
 
426
605
  return [];
@@ -525,11 +704,8 @@ function copyPlatformFiles(platformKey, results, options = {}) {
525
704
 
526
705
  // Copy additional required skills based on platform
527
706
  let requiredSkills = [];
528
- if (platformKey === 'claude') {
707
+ if (isClaudeCompatibleRuntime(platformKey)) {
529
708
  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
709
  }
534
710
 
535
711
  requiredSkills
@@ -557,7 +733,7 @@ function copyPlatformFiles(platformKey, results, options = {}) {
557
733
  }
558
734
 
559
735
  // Copy agents
560
- if (platformKey === 'claude') {
736
+ if (isClaudeCompatibleRuntime(platformKey)) {
561
737
  if (fs.existsSync(agentsSourceDir)) {
562
738
  const requiredAgents = CLAUDE_MIGRATION_MANIFEST?.agents?.required || [
563
739
  'tester.md',
@@ -586,7 +762,12 @@ function copyPlatformFiles(platformKey, results, options = {}) {
586
762
  }
587
763
 
588
764
  fs.mkdirSync(path.dirname(dest), { recursive: true });
589
- fs.copyFileSync(source, dest);
765
+ if (platformKey === 'opencode') {
766
+ const content = fs.readFileSync(source, 'utf8');
767
+ fs.writeFileSync(dest, convertOpenCodeAgentContent(content, fileName), 'utf8');
768
+ } else {
769
+ fs.copyFileSync(source, dest);
770
+ }
590
771
  results.dependencyChecks++;
591
772
  results.installedDependencies++;
592
773
 
@@ -601,7 +782,7 @@ function copyPlatformFiles(platformKey, results, options = {}) {
601
782
  // Copy agent reference manuals (debugger manuals, etc.) recursively
602
783
  const refsSource = path.join(__dirname, '../src', platform.sourceDir, 'references');
603
784
  if (fs.existsSync(refsSource)) {
604
- const refsDest = path.join(platform.folder, 'references');
785
+ const refsDest = getClaudeSupportTargetDir(platformKey, 'references');
605
786
  const refsExisted = fs.existsSync(refsDest);
606
787
  copyRecursive(refsSource, refsDest, options);
607
788
 
@@ -622,7 +803,7 @@ function copyPlatformFiles(platformKey, results, options = {}) {
622
803
  // Copy native scripts (browser-tool, docs-fetch, validate-docs)
623
804
  const scriptsSourceDir = path.join(__dirname, '../src/claude/scripts');
624
805
  if (fs.existsSync(scriptsSourceDir)) {
625
- const scriptsDest = path.join(platform.folder, 'scripts');
806
+ const scriptsDest = getClaudeSupportTargetDir(platformKey, 'scripts');
626
807
  const scriptsExisted = fs.existsSync(scriptsDest);
627
808
  copyRecursive(scriptsSourceDir, scriptsDest, options);
628
809
 
@@ -662,6 +843,9 @@ function copyPlatformFiles(platformKey, results, options = {}) {
662
843
  fs.mkdirSync(path.dirname(dest), { recursive: true });
663
844
  let content = fs.readFileSync(source, 'utf8');
664
845
  content = content.replace(/\{\{SKILLS_DIR\}\}/g, platform.skillsRef);
846
+ if (platformKey === 'opencode') {
847
+ content = convertOpenCodeCommandContent(content, file);
848
+ }
665
849
  fs.writeFileSync(dest, content);
666
850
 
667
851
  if (destinationExists && shouldOverwriteManagedFiles) {
@@ -704,37 +888,99 @@ function copyRoutingFile(platformKey, results, options = {}) {
704
888
  }
705
889
  }
706
890
 
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');
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
+ }
714
915
 
715
- if (fs.existsSync(source)) {
716
- // Create rules directory if not exists
717
- if (!fs.existsSync(rulesDir)) {
718
- fs.mkdirSync(rulesDir, { recursive: true });
719
- }
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
+ }
720
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) {
721
954
  const destinationExists = fs.existsSync(dest);
722
955
 
723
956
  if (destinationExists && !shouldOverwriteManagedFiles) {
724
- console.log(` → Skipped: rules/GEMINI.md (already exists)`);
725
- results.skipped++;
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++;
726
969
  return;
727
970
  }
728
971
 
729
- fs.copyFileSync(source, dest);
972
+ fs.writeFileSync(dest, content, 'utf8');
730
973
 
731
974
  if (destinationExists && shouldOverwriteManagedFiles) {
732
- console.log(` ↻ Updated: rules/GEMINI.md`);
975
+ console.log(` ↻ AGENTS.md overwritten`);
733
976
  results.updated++;
734
977
  } else {
735
- console.log(` ✓ Copied: rules/GEMINI.md`);
978
+ console.log(` ✓ AGENTS.md installed`);
736
979
  results.copied++;
737
980
  }
981
+ } else {
982
+ console.log(` ⚠ AGENTS.md template source not found`);
983
+ results.missingDependencies++;
738
984
  }
739
985
  }
740
986
 
@@ -780,6 +1026,335 @@ function copyClaudeRuntimeFiles(platformKey, results, options = {}) {
780
1026
  });
781
1027
  }
782
1028
 
1029
+ function removeObsoleteClaudeRuntimeFiles(platformKey, results) {
1030
+ if (platformKey !== 'claude') return;
1031
+
1032
+ const obsoleteFiles = CLAUDE_MIGRATION_MANIFEST?.obsolete?.runtimeFiles || [];
1033
+ const targetBase = path.join(PLATFORMS.claude.folder);
1034
+
1035
+ obsoleteFiles.forEach(relPath => {
1036
+ const targetPath = path.join(targetBase, relPath);
1037
+
1038
+ if (!fs.existsSync(targetPath)) {
1039
+ return;
1040
+ }
1041
+
1042
+ fs.rmSync(targetPath, { force: true });
1043
+ console.log(` ↻ Removed obsolete runtime: ${relPath}`);
1044
+ results.updated++;
1045
+ });
1046
+ }
1047
+
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
+ function pruneObsoleteSettingsHooks(settings, results) {
1305
+ const obsoleteSubstrings = CLAUDE_MIGRATION_MANIFEST?.obsolete?.settingsHookCommandSubstrings || [];
1306
+
1307
+ if (!settings.hooks || obsoleteSubstrings.length === 0) {
1308
+ return settings;
1309
+ }
1310
+
1311
+ let removedCount = 0;
1312
+ const prunedHooks = {};
1313
+
1314
+ Object.entries(settings.hooks).forEach(([eventName, matcherEntries]) => {
1315
+ if (!Array.isArray(matcherEntries)) {
1316
+ prunedHooks[eventName] = matcherEntries;
1317
+ return;
1318
+ }
1319
+
1320
+ const prunedEntries = [];
1321
+
1322
+ matcherEntries.forEach(entry => {
1323
+ if (!Array.isArray(entry?.hooks)) {
1324
+ prunedEntries.push(entry);
1325
+ return;
1326
+ }
1327
+
1328
+ const remainingHooks = entry.hooks.filter(hook => {
1329
+ const command = hook?.command || '';
1330
+ const isObsolete = obsoleteSubstrings.some(substring => command.includes(substring));
1331
+
1332
+ if (isObsolete) {
1333
+ removedCount++;
1334
+ }
1335
+
1336
+ return !isObsolete;
1337
+ });
1338
+
1339
+ if (remainingHooks.length > 0) {
1340
+ prunedEntries.push({ ...entry, hooks: remainingHooks });
1341
+ }
1342
+ });
1343
+
1344
+ if (prunedEntries.length > 0) {
1345
+ prunedHooks[eventName] = prunedEntries;
1346
+ }
1347
+ });
1348
+
1349
+ if (removedCount > 0) {
1350
+ console.log(` ↻ Settings: removed ${removedCount} obsolete hook(s)`);
1351
+ results.updated++;
1352
+ return { ...settings, hooks: prunedHooks };
1353
+ }
1354
+
1355
+ return settings;
1356
+ }
1357
+
783
1358
  // Merge Claude settings.json
784
1359
  function mergeClaudeSettings(platformKey, results, options = {}) {
785
1360
  if (platformKey !== 'claude') return;
@@ -802,7 +1377,7 @@ function mergeClaudeSettings(platformKey, results, options = {}) {
802
1377
  existingSettings = JSON.parse(fs.readFileSync(targetPath, 'utf8'));
803
1378
  }
804
1379
 
805
- const mergedSettings = { ...existingSettings };
1380
+ const mergedSettings = pruneObsoleteSettingsHooks({ ...existingSettings }, results);
806
1381
 
807
1382
  // Merge statusLine
808
1383
  if (managedSettings.statusLine) {
@@ -871,10 +1446,10 @@ function copyClaudeMdFile(platformKey, results, options = {}) {
871
1446
 
872
1447
  // Copy rules directory (always overwrite)
873
1448
  function copyRulesDirectory(platformKey, results, options = {}) {
874
- if (platformKey !== 'claude') return;
1449
+ if (!isClaudeCompatibleRuntime(platformKey)) return;
875
1450
 
876
1451
  const source = path.join(__dirname, '../src/claude/rules');
877
- const dest = path.join(PLATFORMS.claude.folder, 'rules');
1452
+ const dest = getClaudeSupportTargetDir(platformKey, 'rules');
878
1453
 
879
1454
  if (fs.existsSync(source)) {
880
1455
  copyRecursive(source, dest, { upgrade: true });
@@ -1106,14 +1681,18 @@ async function main() {
1106
1681
  if (platformKey === 'claude') {
1107
1682
  copyRoutingFile(platformKey, results, installerOptions);
1108
1683
  copyClaudeRuntimeFiles(platformKey, results, installerOptions);
1684
+ removeObsoleteClaudeRuntimeFiles(platformKey, results);
1109
1685
  mergeClaudeSettings(platformKey, results, installerOptions);
1110
1686
  copyClaudeMdFile(platformKey, results, installerOptions);
1111
1687
  copyRulesDirectory(platformKey, results, installerOptions);
1112
1688
  }
1113
1689
 
1114
- // Copy GEMINI.md for Antigravity platform
1115
- if (platformKey === 'antigravity') {
1116
- copyGeminiFile(platformKey, results, installerOptions);
1690
+ if (platformKey === 'opencode') {
1691
+ copyOpenCodeSharedRuntimeFiles(platformKey, results, installerOptions);
1692
+ copyOpenCodeCommandTemplates(platformKey, results, installerOptions);
1693
+ copyOpenCodeAgentsMdFile(platformKey, results, installerOptions);
1694
+ copyRulesDirectory(platformKey, results, installerOptions);
1695
+ mergeOpenCodeConfig(platformKey, results);
1117
1696
  }
1118
1697
 
1119
1698
  writePlatformVersionMetadata(platformKey, results);
@@ -1128,7 +1707,8 @@ async function main() {
1128
1707
  ensureGitignore(results, installerOptions);
1129
1708
  console.log();
1130
1709
 
1131
- // Setup Gemini CLI and API Key config
1710
+ // Setup runtime-specific optional model/API configuration
1711
+ await setupOpenCodeModel(platforms, results);
1132
1712
  await setupGeminiCLI(platforms);
1133
1713
 
1134
1714
  // Summary
@@ -1164,7 +1744,11 @@ async function main() {
1164
1744
  }
1165
1745
  }
1166
1746
 
1167
- console.log('\n 2. The AI will automatically sync docs/ (Continuous Documentation)');
1747
+ if (platforms.includes('claude')) {
1748
+ console.log('\n 2. Claude Code will automatically sync docs/ (Continuous Documentation)');
1749
+ } else {
1750
+ console.log('\n 2. CafeKit skills and AGENTS.md are installed for the selected runtime');
1751
+ }
1168
1752
  console.log(' 3. Project API Keys are now securely isolated in .claude/.env');
1169
1753
  if (!installerOptions.upgrade) {
1170
1754
  console.log(' 4. To refresh managed templates later, run installer with --upgrade');