opencode.ts 4.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133
  1. /**
  2. * opencode target.
  3. *
  4. * - MCP server entry to `~/.config/opencode/opencode.json` (global,
  5. * XDG-style; `%APPDATA%/opencode/opencode.json` on Windows) or
  6. * `./opencode.json` (local).
  7. * - No instructions file built in (opencode doesn't have a
  8. * conventional agent-rules surface as of 2026-05).
  9. * - No permissions concept.
  10. *
  11. * Config shape uses opencode's wrapper:
  12. * {
  13. * "$schema": "https://opencode.ai/config.json",
  14. * "mcp": { "codegraph": { "type": "local", "command": [...], "enabled": true } }
  15. * }
  16. *
  17. * The shape differs from Claude/Cursor — opencode uses `mcp.<name>`
  18. * (not `mcpServers`), takes `command` as a string array combining
  19. * binary + args, and includes an explicit `enabled` flag.
  20. */
  21. import * as fs from 'fs';
  22. import * as path from 'path';
  23. import * as os from 'os';
  24. import {
  25. AgentTarget,
  26. DetectionResult,
  27. InstallOptions,
  28. Location,
  29. WriteResult,
  30. } from './types';
  31. import {
  32. jsonDeepEqual,
  33. readJsonFile,
  34. writeJsonFile,
  35. } from './shared';
  36. function globalConfigDir(): string {
  37. if (process.platform === 'win32') {
  38. const appData = process.env.APPDATA ?? path.join(os.homedir(), 'AppData', 'Roaming');
  39. return path.join(appData, 'opencode');
  40. }
  41. // XDG_CONFIG_HOME if set, else ~/.config — matches opencode's docs.
  42. const xdg = process.env.XDG_CONFIG_HOME && process.env.XDG_CONFIG_HOME.trim().length > 0
  43. ? process.env.XDG_CONFIG_HOME
  44. : path.join(os.homedir(), '.config');
  45. return path.join(xdg, 'opencode');
  46. }
  47. function configPath(loc: Location): string {
  48. return loc === 'global'
  49. ? path.join(globalConfigDir(), 'opencode.json')
  50. : path.join(process.cwd(), 'opencode.json');
  51. }
  52. function getOpencodeServerEntry(): { type: string; command: string[]; enabled: boolean } {
  53. return {
  54. type: 'local',
  55. command: ['codegraph', 'serve', '--mcp'],
  56. enabled: true,
  57. };
  58. }
  59. class OpencodeTarget implements AgentTarget {
  60. readonly id = 'opencode' as const;
  61. readonly displayName = 'opencode';
  62. readonly docsUrl = 'https://opencode.ai/docs/config';
  63. supportsLocation(_loc: Location): boolean {
  64. return true;
  65. }
  66. detect(loc: Location): DetectionResult {
  67. const file = configPath(loc);
  68. const config = readJsonFile(file);
  69. const alreadyConfigured = !!config.mcp?.codegraph;
  70. const installed = loc === 'global'
  71. ? fs.existsSync(globalConfigDir())
  72. : fs.existsSync(file);
  73. return { installed, alreadyConfigured, configPath: file };
  74. }
  75. install(loc: Location, _opts: InstallOptions): WriteResult {
  76. const file = configPath(loc);
  77. const existing = readJsonFile(file);
  78. const before = existing.mcp?.codegraph;
  79. const after = getOpencodeServerEntry();
  80. if (jsonDeepEqual(before, after)) {
  81. return { files: [{ path: file, action: 'unchanged' }] };
  82. }
  83. const created = !fs.existsSync(file);
  84. if (!existing.$schema) existing.$schema = 'https://opencode.ai/config.json';
  85. if (!existing.mcp) existing.mcp = {};
  86. existing.mcp.codegraph = after;
  87. writeJsonFile(file, existing);
  88. return {
  89. files: [{ path: file, action: created ? 'created' : 'updated' }],
  90. };
  91. }
  92. uninstall(loc: Location): WriteResult {
  93. const file = configPath(loc);
  94. const config = readJsonFile(file);
  95. if (!config.mcp?.codegraph) {
  96. return { files: [{ path: file, action: 'not-found' }] };
  97. }
  98. delete config.mcp.codegraph;
  99. if (Object.keys(config.mcp).length === 0) {
  100. delete config.mcp;
  101. }
  102. // If the file is now degenerate (only $schema or empty), leave it
  103. // — the user may have other config we shouldn't nuke.
  104. writeJsonFile(file, config);
  105. return { files: [{ path: file, action: 'removed' }] };
  106. }
  107. printConfig(loc: Location): string {
  108. const target = configPath(loc);
  109. const snippet = JSON.stringify({
  110. $schema: 'https://opencode.ai/config.json',
  111. mcp: { codegraph: getOpencodeServerEntry() },
  112. }, null, 2);
  113. return `# Add to ${target}\n\n${snippet}\n`;
  114. }
  115. describePaths(loc: Location): string[] {
  116. return [configPath(loc)];
  117. }
  118. }
  119. export const opencodeTarget: AgentTarget = new OpencodeTarget();