Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

156 lines
5.1 KiB
Go

// Package opencode implements the Gortex init integration for
// OpenCode. OpenCode reads its project config from `opencode.json` or
// `opencode.jsonc` at the repo root (and `~/.config/opencode/` for the
// global config); it does NOT read `.opencode/config.json` — Gortex
// wrote there historically and OpenCode silently ignored it, so the MCP
// server was never registered. We now write the MCP stanza to a root
// `opencode.json` (or merge into an existing `opencode.json` /
// `opencode.jsonc`).
//
// OpenCode uses a different MCP schema than the canonical
// Claude / Cursor shape:
//
// {
// "$schema": "https://opencode.ai/config.json",
// "mcp": {
// "gortex": {
// "type": "local",
// "command": ["gortex", "mcp", ...],
// "environment": {...},
// "enabled": true
// }
// }
// }
//
// We preserve any existing $schema reference and add one if absent.
package opencode
import (
"os"
"os/exec"
"path/filepath"
"github.com/zzet/gortex/internal/agents"
"github.com/zzet/gortex/internal/agents/internalutil"
)
const Name = "opencode"
const DocsURL = "https://opencode.ai/docs/mcp"
const SchemaURL = "https://opencode.ai/config.json"
type Adapter struct{}
func New() *Adapter { return &Adapter{} }
func (a *Adapter) Name() string { return Name }
func (a *Adapter) DocsURL() string { return DocsURL }
func (a *Adapter) Detect(env agents.Env) (bool, error) {
if _, err := os.Stat(filepath.Join(env.Root, ".opencode")); err == nil {
return true, nil
}
if p, err := exec.LookPath("opencode"); err == nil && p != "" {
return true, nil
}
if env.Home != "" {
if _, err := os.Stat(filepath.Join(env.Home, ".config", "opencode")); err == nil {
return true, nil
}
}
return false, nil
}
// projectConfigPath resolves the file the MCP stanza should be written
// into. OpenCode reads `opencode.json` / `opencode.jsonc` from the repo
// root, so we merge into an existing one (preferring `.jsonc`, since a
// hand-authored, comment-bearing config keeps its extension) and
// otherwise default to a fresh `opencode.json`.
func projectConfigPath(root string) string {
for _, name := range []string{"opencode.jsonc", "opencode.json"} {
p := filepath.Join(root, name)
if _, err := os.Stat(p); err == nil {
return p
}
}
return filepath.Join(root, "opencode.json")
}
func (a *Adapter) Plan(env agents.Env) (*agents.Plan, error) {
p := &agents.Plan{Files: []agents.FileAction{
{Path: projectConfigPath(env.Root), Action: agents.ActionWouldMerge, Keys: []string{"mcp"}},
}}
if env.Mode != agents.ModeGlobal && env.SkillsRouting != "" {
p.Files = append(p.Files, agents.FileAction{
Path: filepath.Join(env.Root, "AGENTS.md"), Action: agents.ActionWouldMerge,
Keys: []string{"communities-block"},
})
}
return p, nil
}
func (a *Adapter) Apply(env agents.Env, opts agents.ApplyOpts) (*agents.Result, error) {
res := &agents.Result{Name: Name, DocsURL: DocsURL}
if env.Mode == agents.ModeGlobal {
return res, nil
}
detected, _ := a.Detect(env)
res.Detected = detected
if !detected && !opts.ForceDetect {
internalutil.Logf(env.Stderr, "[gortex init] skip OpenCode setup (OpenCode not detected)")
return res, nil
}
internalutil.Logf(env.Stderr, "[gortex init] setting up OpenCode integration...")
path := projectConfigPath(env.Root)
// Gortex used to write `.opencode/config.json`, which OpenCode does
// not read. If that stale file is still around, point the user at
// the config we're actually writing now.
if legacy := filepath.Join(env.Root, ".opencode", "config.json"); legacy != path {
if _, err := os.Stat(legacy); err == nil {
internalutil.Logf(env.Stderr, "[gortex init] note: %s is no longer read by OpenCode; writing the MCP config to %s instead", legacy, filepath.Base(path))
}
}
action, err := agents.MergeJSON(env.Stderr, path, func(root map[string]any, _ bool) (bool, error) {
mcpSection, ok := root["mcp"].(map[string]any)
if !ok {
mcpSection = make(map[string]any)
}
if _, exists := mcpSection["gortex"]; exists && !opts.Force {
return false, nil
}
mcpSection["gortex"] = map[string]any{
"type": "local",
"command": []string{"gortex", "mcp"},
"environment": map[string]string{
"GORTEX_INDEX_WORKERS": "8",
},
"enabled": true,
}
root["mcp"] = mcpSection
if _, hasSchema := root["$schema"]; !hasSchema {
root["$schema"] = SchemaURL
}
return true, nil
}, opts)
if err != nil {
return res, err
}
res.Files = append(res.Files, action)
// AGENTS.md gets a marker-guarded community-routing block when
// skills were generated (--skills, default on in `gortex init`).
// The codex adapter targets the same file with the same markers
// so a repo running both adapters converges on one block.
if env.SkillsRouting != "" {
agentsMdPath := filepath.Join(env.Root, "AGENTS.md")
routingAction, err := agents.UpsertMarkedBlock(env.Stderr, agentsMdPath, env.SkillsRouting,
agents.CommunitiesStartMarker, agents.CommunitiesEndMarker, opts)
if err != nil {
return res, err
}
res.Files = append(res.Files, routingAction)
}
res.Configured = true
return res, nil
}