opencode-setup 0.1.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.
- package/LICENSE +21 -0
- package/README.md +242 -0
- package/dist/chunk-PKHQD5QT.js +1106 -0
- package/dist/cli.js +339 -0
- package/dist/index.js +170 -0
- package/dist/templates/agents/planner.md +10 -0
- package/dist/templates/agents/reviewer.md +12 -0
- package/dist/templates/agents/tester.md +5 -0
- package/dist/templates/agents-md/backend-go.md +28 -0
- package/dist/templates/agents-md/backend-python.md +27 -0
- package/dist/templates/agents-md/base.md +29 -0
- package/dist/templates/agents-md/frontend-ts.md +39 -0
- package/dist/templates/agents-md/fullstack.md +25 -0
- package/dist/templates/commands/lint.md +2 -0
- package/dist/templates/commands/plan.md +10 -0
- package/dist/templates/commands/review.md +6 -0
- package/dist/templates/commands/test.md +2 -0
- package/dist/templates/configs/balanced.json +10 -0
- package/dist/templates/configs/budget.json +10 -0
- package/dist/templates/configs/google-only.json +10 -0
- package/dist/templates/configs/minimax.json +10 -0
- package/dist/templates/configs/power.json +10 -0
- package/dist/templates/skills/code-review/SKILL.md +27 -0
- package/dist/templates/skills/frontend-design/SKILL.md +20 -0
- package/dist/templates/skills/testing/SKILL.md +19 -0
- package/package.json +83 -0
- package/templates/agents/planner.md +10 -0
- package/templates/agents/reviewer.md +12 -0
- package/templates/agents/tester.md +5 -0
- package/templates/agents-md/backend-go.md +28 -0
- package/templates/agents-md/backend-python.md +27 -0
- package/templates/agents-md/base.md +29 -0
- package/templates/agents-md/frontend-ts.md +39 -0
- package/templates/agents-md/fullstack.md +25 -0
- package/templates/commands/lint.md +2 -0
- package/templates/commands/plan.md +10 -0
- package/templates/commands/review.md +6 -0
- package/templates/commands/test.md +2 -0
- package/templates/configs/balanced.json +10 -0
- package/templates/configs/budget.json +10 -0
- package/templates/configs/google-only.json +10 -0
- package/templates/configs/minimax.json +10 -0
- package/templates/configs/power.json +10 -0
- package/templates/skills/code-review/SKILL.md +27 -0
- package/templates/skills/frontend-design/SKILL.md +20 -0
- package/templates/skills/testing/SKILL.md +19 -0
|
@@ -0,0 +1,1106 @@
|
|
|
1
|
+
// src/preset/registry.ts
|
|
2
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync, cpSync } from "fs";
|
|
3
|
+
import { join, dirname } from "path";
|
|
4
|
+
import { fileURLToPath } from "url";
|
|
5
|
+
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
var TEMPLATES_DIR = join(__dirname, "templates");
|
|
7
|
+
var MODEL_PRESETS = [
|
|
8
|
+
{
|
|
9
|
+
name: "budget",
|
|
10
|
+
description: "Big Pickle \uBB34\uB8CC \uC870\uD569",
|
|
11
|
+
monthlyCost: "\uBB34\uB8CC ~ $10",
|
|
12
|
+
config: JSON.parse(readFileSync(join(TEMPLATES_DIR, "configs/budget.json"), "utf-8"))
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
name: "balanced",
|
|
16
|
+
description: "Sonnet + Flash",
|
|
17
|
+
monthlyCost: "$20 ~ $40",
|
|
18
|
+
config: JSON.parse(readFileSync(join(TEMPLATES_DIR, "configs/balanced.json"), "utf-8"))
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: "power",
|
|
22
|
+
description: "Sonnet + Opus",
|
|
23
|
+
monthlyCost: "$50+",
|
|
24
|
+
config: JSON.parse(readFileSync(join(TEMPLATES_DIR, "configs/power.json"), "utf-8"))
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
name: "minimax",
|
|
28
|
+
description: "M2.5 + Big Pickle",
|
|
29
|
+
monthlyCost: "$5 ~ $15",
|
|
30
|
+
config: JSON.parse(readFileSync(join(TEMPLATES_DIR, "configs/minimax.json"), "utf-8"))
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "google-only",
|
|
34
|
+
description: "Gemini Pro + Flash",
|
|
35
|
+
monthlyCost: "$15 ~ $30",
|
|
36
|
+
config: JSON.parse(readFileSync(join(TEMPLATES_DIR, "configs/google-only.json"), "utf-8"))
|
|
37
|
+
}
|
|
38
|
+
];
|
|
39
|
+
var STACK_PRESETS = [
|
|
40
|
+
{
|
|
41
|
+
name: "frontend-ts",
|
|
42
|
+
description: "TypeScript \uD504\uB860\uD2B8\uC5D4\uB4DC \uCD5C\uC801\uD654 (Next.js/React)",
|
|
43
|
+
modelPreset: "balanced",
|
|
44
|
+
includes: {
|
|
45
|
+
agentsMD: "frontend-ts.md",
|
|
46
|
+
commands: ["test.md", "lint.md", "review.md", "plan.md"],
|
|
47
|
+
agents: ["reviewer.md", "tester.md", "planner.md"],
|
|
48
|
+
skills: ["code-review", "testing", "frontend-design"]
|
|
49
|
+
}
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "backend-go",
|
|
53
|
+
description: "Go \uBC31\uC5D4\uB4DC \uCD5C\uC801\uD654",
|
|
54
|
+
modelPreset: "balanced",
|
|
55
|
+
includes: {
|
|
56
|
+
agentsMD: "backend-go.md",
|
|
57
|
+
commands: ["test.md", "lint.md", "review.md", "plan.md"],
|
|
58
|
+
agents: ["reviewer.md", "tester.md", "planner.md"],
|
|
59
|
+
skills: ["code-review", "testing"]
|
|
60
|
+
}
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: "backend-python",
|
|
64
|
+
description: "Python \uBC31\uC5D4\uB4DC \uCD5C\uC801\uD654 (FastAPI)",
|
|
65
|
+
modelPreset: "balanced",
|
|
66
|
+
includes: {
|
|
67
|
+
agentsMD: "backend-python.md",
|
|
68
|
+
commands: ["test.md", "lint.md", "review.md", "plan.md"],
|
|
69
|
+
agents: ["reviewer.md", "tester.md", "planner.md"],
|
|
70
|
+
skills: ["code-review", "testing"]
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
{
|
|
74
|
+
name: "fullstack",
|
|
75
|
+
description: "\uD480\uC2A4\uD0DD \uD504\uB85C\uC81D\uD2B8 \uCD5C\uC801\uD654",
|
|
76
|
+
modelPreset: "balanced",
|
|
77
|
+
includes: {
|
|
78
|
+
agentsMD: "fullstack.md",
|
|
79
|
+
commands: ["test.md", "lint.md", "review.md", "plan.md"],
|
|
80
|
+
agents: ["reviewer.md", "tester.md", "planner.md"],
|
|
81
|
+
skills: ["code-review", "testing"]
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
];
|
|
85
|
+
function listPresets() {
|
|
86
|
+
const lines = ["# Available Presets\n"];
|
|
87
|
+
lines.push("## Model Presets");
|
|
88
|
+
lines.push("");
|
|
89
|
+
lines.push("| Name | Description | Monthly Cost |");
|
|
90
|
+
lines.push("|------|-------------|--------------|");
|
|
91
|
+
for (const preset of MODEL_PRESETS) {
|
|
92
|
+
lines.push(`| \`${preset.name}\` | ${preset.description} | ${preset.monthlyCost} |`);
|
|
93
|
+
}
|
|
94
|
+
lines.push("");
|
|
95
|
+
lines.push("## Stack Presets");
|
|
96
|
+
lines.push("");
|
|
97
|
+
lines.push("| Name | Description |");
|
|
98
|
+
lines.push("|------|-------------|");
|
|
99
|
+
for (const preset of STACK_PRESETS) {
|
|
100
|
+
lines.push(`| \`${preset.name}\` | ${preset.description} |`);
|
|
101
|
+
}
|
|
102
|
+
return lines.join("\n");
|
|
103
|
+
}
|
|
104
|
+
function getModelPreset(name) {
|
|
105
|
+
return MODEL_PRESETS.find((p) => p.name === name);
|
|
106
|
+
}
|
|
107
|
+
function getStackPreset(name) {
|
|
108
|
+
return STACK_PRESETS.find((p) => p.name === name);
|
|
109
|
+
}
|
|
110
|
+
async function applyPreset(name, projectDir, options = {}) {
|
|
111
|
+
const files = [];
|
|
112
|
+
const warnings = [];
|
|
113
|
+
const modelPreset = getModelPreset(name);
|
|
114
|
+
if (modelPreset) {
|
|
115
|
+
const configPath = join(projectDir, "opencode.json");
|
|
116
|
+
const configDir = dirname(configPath);
|
|
117
|
+
if (!existsSync(configDir)) {
|
|
118
|
+
mkdirSync(configDir, { recursive: true });
|
|
119
|
+
}
|
|
120
|
+
let existingConfig = {};
|
|
121
|
+
if (existsSync(configPath)) {
|
|
122
|
+
try {
|
|
123
|
+
existingConfig = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
124
|
+
} catch {
|
|
125
|
+
warnings.push(`Could not parse existing opencode.json at ${configPath}`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
const newConfig = {
|
|
129
|
+
...existingConfig,
|
|
130
|
+
...modelPreset.config
|
|
131
|
+
};
|
|
132
|
+
writeFileSync(configPath, JSON.stringify(newConfig, null, 2) + "\n");
|
|
133
|
+
files.push(configPath);
|
|
134
|
+
return { success: true, files, warnings };
|
|
135
|
+
}
|
|
136
|
+
const stackPreset = getStackPreset(name);
|
|
137
|
+
if (stackPreset) {
|
|
138
|
+
const { includes } = stackPreset;
|
|
139
|
+
const opencodeDir = join(projectDir, ".opencode");
|
|
140
|
+
const agentsDir = join(opencodeDir, "agents");
|
|
141
|
+
const commandsDir = join(opencodeDir, "commands");
|
|
142
|
+
const skillsDir = join(opencodeDir, "skills");
|
|
143
|
+
mkdirSync(agentsDir, { recursive: true });
|
|
144
|
+
mkdirSync(commandsDir, { recursive: true });
|
|
145
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
146
|
+
const agentsMDSource = join(TEMPLATES_DIR, "agents-md", includes.agentsMD);
|
|
147
|
+
const agentsMDDest = join(projectDir, "AGENTS.md");
|
|
148
|
+
if (existsSync(agentsMDSource)) {
|
|
149
|
+
let content = readFileSync(agentsMDSource, "utf-8");
|
|
150
|
+
content = content.replace(/\{\{projectName\}\}/g, options.projectName || "My Project");
|
|
151
|
+
content = content.replace(/\{\{testRunner\}\}/g, options.testRunner || "vitest");
|
|
152
|
+
content = content.replace(/\{\{linter\}\}/g, options.linter || "biome");
|
|
153
|
+
writeFileSync(agentsMDDest, content);
|
|
154
|
+
files.push(agentsMDDest);
|
|
155
|
+
}
|
|
156
|
+
for (const cmd of includes.commands) {
|
|
157
|
+
const src = join(TEMPLATES_DIR, "commands", cmd);
|
|
158
|
+
const dest = join(commandsDir, cmd.replace(".md", ".md"));
|
|
159
|
+
if (existsSync(src)) {
|
|
160
|
+
cpSync(src, dest);
|
|
161
|
+
files.push(dest);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
for (const agent of includes.agents) {
|
|
165
|
+
const src = join(TEMPLATES_DIR, "agents", agent);
|
|
166
|
+
const dest = join(agentsDir, agent);
|
|
167
|
+
if (existsSync(src)) {
|
|
168
|
+
cpSync(src, dest);
|
|
169
|
+
files.push(dest);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
for (const skill of includes.skills) {
|
|
173
|
+
const src = join(TEMPLATES_DIR, "skills", skill);
|
|
174
|
+
const dest = join(skillsDir, skill);
|
|
175
|
+
if (existsSync(src)) {
|
|
176
|
+
cpSync(src, dest, { recursive: true });
|
|
177
|
+
files.push(dest);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
const modelResult = await applyPreset(stackPreset.modelPreset, projectDir, options);
|
|
181
|
+
files.push(...modelResult.files);
|
|
182
|
+
warnings.push(...modelResult.warnings);
|
|
183
|
+
return { success: true, files, warnings };
|
|
184
|
+
}
|
|
185
|
+
return {
|
|
186
|
+
success: false,
|
|
187
|
+
files: [],
|
|
188
|
+
warnings: [`Unknown preset: ${name}. Use 'preset list' to see available presets.`]
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// src/validator/config-validator.ts
|
|
193
|
+
import { join as join2 } from "path";
|
|
194
|
+
import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
|
|
195
|
+
async function runValidation(directory) {
|
|
196
|
+
const errors = [];
|
|
197
|
+
const globalConfigPath = join2(process.env.HOME || "~", ".config/opencode/opencode.json");
|
|
198
|
+
const expandedGlobalPath = globalConfigPath.replace("~", process.env.HOME || "");
|
|
199
|
+
if (existsSync2(expandedGlobalPath)) {
|
|
200
|
+
try {
|
|
201
|
+
const content = readFileSync2(expandedGlobalPath, "utf-8");
|
|
202
|
+
const config = JSON.parse(content);
|
|
203
|
+
if (!config.$schema) {
|
|
204
|
+
errors.push({ type: "warning", file: "~/.config/opencode/opencode.json", message: "Missing $schema field" });
|
|
205
|
+
}
|
|
206
|
+
if (config.model) {
|
|
207
|
+
const modelPattern = /^[a-z0-9-]+\/[a-z0-9-]+$/i;
|
|
208
|
+
if (!modelPattern.test(config.model)) {
|
|
209
|
+
errors.push({ type: "error", file: "~/.config/opencode/opencode.json", message: `Invalid model format: '${config.model}'. Expected 'provider/model'` });
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (config.permission) {
|
|
213
|
+
const validValues = ["allow", "ask", "deny"];
|
|
214
|
+
if (config.permission.edit && !validValues.includes(config.permission.edit)) {
|
|
215
|
+
errors.push({ type: "error", file: "~/.config/opencode/opencode.json", message: `Invalid permission.edit value: '${config.permission.edit}'` });
|
|
216
|
+
}
|
|
217
|
+
if (config.permission.webfetch && !validValues.includes(config.permission.webfetch)) {
|
|
218
|
+
errors.push({ type: "error", file: "~/.config/opencode/opencode.json", message: `Invalid permission.webfetch value: '${config.permission.webfetch}'` });
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
if (config.plugin) {
|
|
222
|
+
if (!Array.isArray(config.plugin)) {
|
|
223
|
+
errors.push({ type: "error", file: "~/.config/opencode/opencode.json", message: "plugin must be an array" });
|
|
224
|
+
} else if (!config.plugin.every((p) => typeof p === "string" || Array.isArray(p) && typeof p[0] === "string")) {
|
|
225
|
+
errors.push({ type: "error", file: "~/.config/opencode/opencode.json", message: "plugin array must contain strings or [string, options] tuples" });
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
} catch (e) {
|
|
229
|
+
errors.push({ type: "error", file: "~/.config/opencode/opencode.json", message: `Invalid JSON: ${e instanceof Error ? e.message : "Unknown error"}` });
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const projectConfigPath = join2(directory, "opencode.json");
|
|
233
|
+
if (existsSync2(projectConfigPath)) {
|
|
234
|
+
try {
|
|
235
|
+
const content = readFileSync2(projectConfigPath, "utf-8");
|
|
236
|
+
const config = JSON.parse(content);
|
|
237
|
+
if (config.permission) {
|
|
238
|
+
const validValues = ["allow", "ask", "deny"];
|
|
239
|
+
if (config.permission.edit && !validValues.includes(config.permission.edit)) {
|
|
240
|
+
errors.push({ type: "error", file: "opencode.json", message: `Invalid permission.edit value: '${config.permission.edit}'` });
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (config.plugin && !Array.isArray(config.plugin)) {
|
|
244
|
+
errors.push({ type: "error", file: "opencode.json", message: "plugin must be an array" });
|
|
245
|
+
}
|
|
246
|
+
} catch (e) {
|
|
247
|
+
errors.push({ type: "error", file: "opencode.json", message: `Invalid JSON: ${e instanceof Error ? e.message : "Unknown error"}` });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
const agentsPath = join2(directory, "AGENTS.md");
|
|
251
|
+
if (!existsSync2(agentsPath)) {
|
|
252
|
+
errors.push({ type: "warning", file: "AGENTS.md", message: "AGENTS.md not found in project root" });
|
|
253
|
+
} else {
|
|
254
|
+
const content = readFileSync2(agentsPath, "utf-8");
|
|
255
|
+
if (content.trim().length === 0) {
|
|
256
|
+
errors.push({ type: "error", file: "AGENTS.md", message: "AGENTS.md is empty" });
|
|
257
|
+
}
|
|
258
|
+
if (!content.includes("## ")) {
|
|
259
|
+
errors.push({ type: "warning", file: "AGENTS.md", message: "AGENTS.md has no section headers (##)" });
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const errorCount = errors.filter((e) => e.type === "error").length;
|
|
263
|
+
const warningCount = errors.filter((e) => e.type === "warning").length;
|
|
264
|
+
const lines = ["\u{1F4CB} Configuration Validation\n"];
|
|
265
|
+
if (errors.length === 0) {
|
|
266
|
+
lines.push("\x1B[32m\u2713 All checks passed!\x1B[0m");
|
|
267
|
+
} else {
|
|
268
|
+
for (const error of errors) {
|
|
269
|
+
const icon = error.type === "error" ? "\u2717" : "\u26A0";
|
|
270
|
+
const color = error.type === "error" ? "31" : "33";
|
|
271
|
+
lines.push(`\x1B[${color}m${icon} ${error.file}\x1B[0m ${error.message}`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
lines.push("");
|
|
275
|
+
lines.push("---");
|
|
276
|
+
lines.push(`Summary: ${errorCount} errors, ${warningCount} warnings`);
|
|
277
|
+
return lines.join("\n");
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// src/doctor/checks.ts
|
|
281
|
+
import { join as join3 } from "path";
|
|
282
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
283
|
+
import { execSync } from "child_process";
|
|
284
|
+
function execCommand(cmd) {
|
|
285
|
+
try {
|
|
286
|
+
const stdout = execSync(cmd.join(" "), { encoding: "utf-8", stdio: ["pipe", "pipe", "ignore"] });
|
|
287
|
+
return { exitCode: 0, stdout: stdout.trim() };
|
|
288
|
+
} catch (e) {
|
|
289
|
+
const err = e;
|
|
290
|
+
return { exitCode: err.status || 1, stdout: "" };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function checkOpenCode() {
|
|
294
|
+
const whichResult = execCommand(["which", "opencode"]);
|
|
295
|
+
if (whichResult.exitCode === 0) {
|
|
296
|
+
const versionResult = execCommand(["opencode", "--version"]);
|
|
297
|
+
const version = versionResult.stdout || "unknown";
|
|
298
|
+
return { name: "OpenCode", status: "pass", message: `v${version}` };
|
|
299
|
+
}
|
|
300
|
+
const npmResult = execCommand(["npm", "list", "-g", "opencode-ai"]);
|
|
301
|
+
if (npmResult.exitCode === 0) {
|
|
302
|
+
return { name: "OpenCode", status: "warn", message: "OpenCode is installed via npm", fix: "Install OpenCode from https://opencode.ai or use npm install -g opencode-ai" };
|
|
303
|
+
}
|
|
304
|
+
return { name: "OpenCode", status: "fail", message: "OpenCode not found", fix: "Install OpenCode from https://opencode.ai" };
|
|
305
|
+
}
|
|
306
|
+
function checkBun() {
|
|
307
|
+
const result = execCommand(["bun", "--version"]);
|
|
308
|
+
if (result.exitCode === 0) {
|
|
309
|
+
const version = result.stdout || "unknown";
|
|
310
|
+
return { name: "Bun", status: "pass", message: `v${version}` };
|
|
311
|
+
}
|
|
312
|
+
const nodeResult = execCommand(["node", "--version"]);
|
|
313
|
+
if (nodeResult.exitCode === 0) {
|
|
314
|
+
return { name: "Bun", status: "warn", message: "Bun not found, Node.js detected", fix: "Install Bun: https://bun.sh" };
|
|
315
|
+
}
|
|
316
|
+
return { name: "Bun", status: "fail", message: "Bun not found", fix: "Install Bun: https://bun.sh" };
|
|
317
|
+
}
|
|
318
|
+
function checkApiKeys() {
|
|
319
|
+
const keys = ["ANTHROPIC_API_KEY", "GEMINI_API_KEY", "OPENAI_API_KEY", "DEEPSEEK_API_KEY", "OPENROUTER_API_KEY"];
|
|
320
|
+
const found = [];
|
|
321
|
+
const missing = [];
|
|
322
|
+
for (const key of keys) {
|
|
323
|
+
if (process.env[key]) {
|
|
324
|
+
found.push(key);
|
|
325
|
+
} else {
|
|
326
|
+
missing.push(key);
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
if (found.length === 0) {
|
|
330
|
+
return { name: "API Keys", status: "fail", message: "No API keys configured", fix: "Set at least one API key: ANTHROPIC_API_KEY, GEMINI_API_KEY, or OPENAI_API_KEY" };
|
|
331
|
+
}
|
|
332
|
+
return { name: "API Keys", status: "pass", message: `Configured: ${found.join(", ")}` };
|
|
333
|
+
}
|
|
334
|
+
function checkAuthFile() {
|
|
335
|
+
const authPath = join3(process.env.HOME || "~", ".local/share/opencode/auth.json");
|
|
336
|
+
const expandedPath = authPath.replace("~", process.env.HOME || "");
|
|
337
|
+
if (existsSync3(expandedPath)) {
|
|
338
|
+
return { name: "Auth File", status: "pass", message: "auth.json exists" };
|
|
339
|
+
}
|
|
340
|
+
return { name: "Auth File", status: "warn", message: "auth.json not found", fix: "Run /connect in OpenCode to authenticate with providers" };
|
|
341
|
+
}
|
|
342
|
+
function checkConfig() {
|
|
343
|
+
const configPath = join3(process.env.HOME || "~", ".config/opencode/opencode.json");
|
|
344
|
+
const expandedPath = configPath.replace("~", process.env.HOME || "");
|
|
345
|
+
if (!existsSync3(expandedPath)) {
|
|
346
|
+
return { name: "Global Config", status: "warn", message: "Global opencode.json not found", fix: "Run 'opencode-setup init' or configure OpenCode manually" };
|
|
347
|
+
}
|
|
348
|
+
try {
|
|
349
|
+
const content = readFileSync3(expandedPath, "utf-8");
|
|
350
|
+
JSON.parse(content);
|
|
351
|
+
return { name: "Global Config", status: "pass", message: "Valid JSON configuration" };
|
|
352
|
+
} catch {
|
|
353
|
+
return { name: "Global Config", status: "fail", message: "Invalid JSON in opencode.json", fix: "Check ~/.config/opencode/opencode.json for syntax errors" };
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
function checkProjectConfig(directory) {
|
|
357
|
+
const configPath = join3(directory, "opencode.json");
|
|
358
|
+
if (!existsSync3(configPath)) {
|
|
359
|
+
return { name: "Project Config", status: "warn", message: "No project opencode.json", fix: "Run 'opencode-setup init' or create opencode.json in project root" };
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
const content = readFileSync3(configPath, "utf-8");
|
|
363
|
+
JSON.parse(content);
|
|
364
|
+
return { name: "Project Config", status: "pass", message: "Valid project configuration" };
|
|
365
|
+
} catch {
|
|
366
|
+
return { name: "Project Config", status: "fail", message: "Invalid JSON in project opencode.json", fix: "Check opencode.json for syntax errors" };
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
function checkAgentsMD(directory) {
|
|
370
|
+
const agentsPath = join3(directory, "AGENTS.md");
|
|
371
|
+
if (!existsSync3(agentsPath)) {
|
|
372
|
+
return { name: "AGENTS.md", status: "warn", message: "AGENTS.md not found", fix: "Run 'opencode-setup init' or create AGENTS.md with project guidelines" };
|
|
373
|
+
}
|
|
374
|
+
const content = readFileSync3(agentsPath, "utf-8");
|
|
375
|
+
if (content.trim().length === 0) {
|
|
376
|
+
return { name: "AGENTS.md", status: "warn", message: "AGENTS.md is empty", fix: "Add project-specific guidelines to AGENTS.md" };
|
|
377
|
+
}
|
|
378
|
+
return { name: "AGENTS.md", status: "pass", message: "AGENTS.md exists" };
|
|
379
|
+
}
|
|
380
|
+
function checkLSP() {
|
|
381
|
+
const lspServers = ["typescript-language-server", "gopls", "rust-analyzer", "pyright"];
|
|
382
|
+
for (const lsp of lspServers) {
|
|
383
|
+
const result = execCommand(["which", lsp]);
|
|
384
|
+
if (result.exitCode === 0) {
|
|
385
|
+
return { name: "LSP", status: "pass", message: `${lsp} found` };
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
return { name: "LSP", status: "warn", message: "No LSP servers found", fix: "Install LSP servers: typescript-language-server, gopls, rust-analyzer, or pyright" };
|
|
389
|
+
}
|
|
390
|
+
function checkPlugins() {
|
|
391
|
+
const configPath = join3(process.env.HOME || "~", ".config/opencode/opencode.json");
|
|
392
|
+
const expandedPath = configPath.replace("~", process.env.HOME || "");
|
|
393
|
+
if (!existsSync3(expandedPath)) {
|
|
394
|
+
return { name: "Plugins", status: "warn", message: "Cannot check plugins - no global config" };
|
|
395
|
+
}
|
|
396
|
+
try {
|
|
397
|
+
const content = readFileSync3(expandedPath, "utf-8");
|
|
398
|
+
const config = JSON.parse(content);
|
|
399
|
+
const plugins = config.plugin || [];
|
|
400
|
+
if (plugins.length === 0) {
|
|
401
|
+
return { name: "Plugins", status: "warn", message: "No plugins configured", fix: "Consider installing oh-my-opencode for enhanced multi-agent orchestration" };
|
|
402
|
+
}
|
|
403
|
+
return { name: "Plugins", status: "pass", message: `Configured: ${plugins.join(", ")}` };
|
|
404
|
+
} catch {
|
|
405
|
+
return { name: "Plugins", status: "warn", message: "Could not read plugin config" };
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
async function runAllChecks(directory) {
|
|
409
|
+
return [
|
|
410
|
+
checkOpenCode(),
|
|
411
|
+
checkBun(),
|
|
412
|
+
checkApiKeys(),
|
|
413
|
+
checkAuthFile(),
|
|
414
|
+
checkConfig(),
|
|
415
|
+
checkProjectConfig(directory),
|
|
416
|
+
checkAgentsMD(directory),
|
|
417
|
+
checkLSP(),
|
|
418
|
+
checkPlugins()
|
|
419
|
+
];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
// src/doctor/reporter.ts
|
|
423
|
+
function formatReport(results) {
|
|
424
|
+
const lines = ["\u{1FA7A} Environment Diagnosis\n"];
|
|
425
|
+
let passCount = 0;
|
|
426
|
+
let warnCount = 0;
|
|
427
|
+
let failCount = 0;
|
|
428
|
+
for (const result of results) {
|
|
429
|
+
if (result.status === "pass") passCount++;
|
|
430
|
+
else if (result.status === "warn") warnCount++;
|
|
431
|
+
else failCount++;
|
|
432
|
+
const icon = result.status === "pass" ? "\u2713" : result.status === "warn" ? "\u26A0" : "\u2717";
|
|
433
|
+
const color = result.status === "pass" ? "32" : result.status === "warn" ? "33" : "31";
|
|
434
|
+
lines.push(`\x1B[${color}m${icon} ${result.name}\x1B[0m ${result.message}`);
|
|
435
|
+
if (result.status === "fail" && result.fix) {
|
|
436
|
+
lines.push(` \x1B[36m\u2192\x1B[0m ${result.fix}`);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
lines.push("");
|
|
440
|
+
lines.push("---");
|
|
441
|
+
lines.push(`Summary: ${passCount} passed, ${warnCount} warnings, ${failCount} failures`);
|
|
442
|
+
return lines.join("\n");
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// src/migrate/common.ts
|
|
446
|
+
import { readFileSync as readFileSync4, writeFileSync as writeFileSync2, existsSync as existsSync4, cpSync as cpSync2, mkdirSync as mkdirSync2, readdirSync } from "fs";
|
|
447
|
+
import { join as join4, dirname as dirname2 } from "path";
|
|
448
|
+
function readFileSafe(filePath) {
|
|
449
|
+
try {
|
|
450
|
+
if (existsSync4(filePath)) return readFileSync4(filePath, "utf-8");
|
|
451
|
+
return null;
|
|
452
|
+
} catch {
|
|
453
|
+
return null;
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
function writeFileSafe(filePath, content) {
|
|
457
|
+
const dir = dirname2(filePath);
|
|
458
|
+
if (!existsSync4(dir)) mkdirSync2(dir, { recursive: true });
|
|
459
|
+
writeFileSync2(filePath, content, "utf-8");
|
|
460
|
+
}
|
|
461
|
+
function backupFile(filePath) {
|
|
462
|
+
if (existsSync4(filePath)) cpSync2(filePath, `${filePath}.bak`);
|
|
463
|
+
}
|
|
464
|
+
function copyDirectory(src, dest) {
|
|
465
|
+
const copied = [];
|
|
466
|
+
if (!existsSync4(src)) return copied;
|
|
467
|
+
mkdirSync2(dest, { recursive: true });
|
|
468
|
+
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
469
|
+
const srcPath = join4(src, entry.name);
|
|
470
|
+
const destPath = join4(dest, entry.name);
|
|
471
|
+
if (entry.isDirectory()) {
|
|
472
|
+
copied.push(...copyDirectory(srcPath, destPath));
|
|
473
|
+
} else {
|
|
474
|
+
try {
|
|
475
|
+
cpSync2(srcPath, destPath);
|
|
476
|
+
copied.push(destPath);
|
|
477
|
+
} catch {
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
return copied;
|
|
482
|
+
}
|
|
483
|
+
function detectTool(rootPath) {
|
|
484
|
+
const checks = [
|
|
485
|
+
{ file: join4(rootPath, ".claude"), name: "claude-code" },
|
|
486
|
+
{ file: join4(rootPath, ".cursorrules"), name: "cursor" },
|
|
487
|
+
{ file: join4(rootPath, ".aider.conf.yml"), name: "aider" },
|
|
488
|
+
{ file: join4(rootPath, "CLAUDE.md"), name: "claude-code" },
|
|
489
|
+
{ file: join4(rootPath, "claude_desktop_config.json"), name: "claude-code" }
|
|
490
|
+
];
|
|
491
|
+
for (const check of checks) {
|
|
492
|
+
if (existsSync4(check.file)) return check.name;
|
|
493
|
+
}
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
function parseYAML(content) {
|
|
497
|
+
const result = {};
|
|
498
|
+
for (const line of content.split("\n")) {
|
|
499
|
+
const trimmed = line.trim();
|
|
500
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
501
|
+
const colonIndex = trimmed.indexOf(":");
|
|
502
|
+
if (colonIndex > 0) {
|
|
503
|
+
const key = trimmed.slice(0, colonIndex).trim();
|
|
504
|
+
const value = trimmed.slice(colonIndex + 1).trim().replace(/^["']|["']$/g, "");
|
|
505
|
+
if (value) result[key] = value;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
return result;
|
|
509
|
+
}
|
|
510
|
+
function formatMigrationResult(toolName, files, warnings, suggestions) {
|
|
511
|
+
const lines = [
|
|
512
|
+
`\u2705 ${toolName} \u2192 OpenCode \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uC644\uB8CC!`,
|
|
513
|
+
"",
|
|
514
|
+
"\uC0DD\uC131\uB41C \uD30C\uC77C:",
|
|
515
|
+
...files.map((f) => ` \u2022 ${f}`)
|
|
516
|
+
];
|
|
517
|
+
if (warnings.length > 0) {
|
|
518
|
+
lines.push("", "\uACBD\uACE0:", ...warnings.map((w) => ` \u26A0\uFE0F ${w}`));
|
|
519
|
+
}
|
|
520
|
+
if (suggestions.length > 0) {
|
|
521
|
+
lines.push("", "\uAD8C\uC7A5 \uC0AC\uD56D:", ...suggestions.map((s) => ` \u2192 ${s}`));
|
|
522
|
+
}
|
|
523
|
+
lines.push("", "\uB2E4\uC74C \uB2E8\uACC4:", "1. \uC0DD\uC131\uB41C \uD30C\uC77C\uB4E4\uC744 \uD655\uC778\uD558\uC138\uC694", "2. opencode.json\uC758 API \uD0A4\uB97C \uC124\uC815\uD558\uC138\uC694", "3. 'opencode doctor'\uB85C \uD658\uACBD\uC744 \uD655\uC778\uD558\uC138\uC694");
|
|
524
|
+
return lines.join("\n");
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// src/migrate/claude-code.ts
|
|
528
|
+
import { existsSync as existsSync5 } from "fs";
|
|
529
|
+
import { join as join5 } from "path";
|
|
530
|
+
function migrateClaudeCode(rootPath) {
|
|
531
|
+
const files = [];
|
|
532
|
+
const warnings = [];
|
|
533
|
+
const suggestions = [];
|
|
534
|
+
const claudeMD = readFileSafe(join5(rootPath, "CLAUDE.md"));
|
|
535
|
+
if (claudeMD) {
|
|
536
|
+
const destPath = join5(rootPath, "AGENTS.md");
|
|
537
|
+
backupFile(destPath);
|
|
538
|
+
writeFileSafe(destPath, claudeMD);
|
|
539
|
+
files.push("AGENTS.md (\uAE30\uC874 CLAUDE.md)");
|
|
540
|
+
}
|
|
541
|
+
const skillsSrc = join5(rootPath, ".claude", "skills");
|
|
542
|
+
const skillsDest = join5(rootPath, ".opencode", "skills");
|
|
543
|
+
if (existsSync5(skillsSrc)) {
|
|
544
|
+
const copied = copyDirectory(skillsSrc, skillsDest);
|
|
545
|
+
files.push(...copied.map((f) => f.replace(rootPath + "/", "")));
|
|
546
|
+
}
|
|
547
|
+
const rulesSrc = join5(rootPath, ".claude", "rules");
|
|
548
|
+
const rulesDest = join5(rootPath, ".opencode", "rules");
|
|
549
|
+
if (existsSync5(rulesSrc)) {
|
|
550
|
+
const copied = copyDirectory(rulesSrc, rulesDest);
|
|
551
|
+
files.push(...copied.map((f) => f.replace(rootPath + "/", "")));
|
|
552
|
+
suggestions.push(".claude/rules/ \u2192 .opencode/rules/ \uB9C8\uC774\uADF8\uB808\uC774\uC158\uB428");
|
|
553
|
+
}
|
|
554
|
+
const mcpConfig = readFileSafe(join5(rootPath, ".mcp.json")) || readFileSafe(join5(rootPath, "claude_desktop_config.json"));
|
|
555
|
+
if (mcpConfig) {
|
|
556
|
+
suggestions.push("MCP \uC124\uC815 \uAC10\uC9C0\uB428 - opencode.json\uC758 mcp \uC139\uC158\uC5D0 \uC218\uB3D9\uC73C\uB85C \uCD94\uAC00 \uD544\uC694");
|
|
557
|
+
}
|
|
558
|
+
const settings = readFileSafe(join5(rootPath, ".claude", "settings.json"));
|
|
559
|
+
if (settings) {
|
|
560
|
+
const parsed = JSON.parse(settings);
|
|
561
|
+
if (parsed.model) {
|
|
562
|
+
suggestions.push(`\uBAA8\uB378 \uC124\uC815 \uAC10\uC9C0\uB428: ${parsed.model} - opencode.json\uC5D0 \uC218\uB3D9\uC73C\uB85C \uCD94\uAC00 \uD544\uC694`);
|
|
563
|
+
}
|
|
564
|
+
if (parsed["dangerously-skip-permissions"]) {
|
|
565
|
+
warnings.push("dangerously-skip-permissions\uB294 OpenCode\uC5D0\uC11C \uC9C0\uC6D0\uB418\uC9C0 \uC54A\uC2B5\uB2C8\uB2E4");
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
const omccPath = join5(rootPath, ".claude", "hooks", "oh-my-claude-code");
|
|
569
|
+
if (existsSync5(omccPath)) {
|
|
570
|
+
suggestions.push("oh-my-claude-code \uAC10\uC9C0\uB428 - oh-my-opencode \uC124\uCE58 \uAD8C\uC7A5: npm install -g oh-my-opencode");
|
|
571
|
+
}
|
|
572
|
+
const opencodeConfig = {
|
|
573
|
+
$schema: "https://opencode.ai/config.json",
|
|
574
|
+
theme: "opencode",
|
|
575
|
+
autoupdate: true
|
|
576
|
+
};
|
|
577
|
+
const settingsData = readFileSafe(join5(rootPath, ".claude", "settings.json"));
|
|
578
|
+
if (settingsData) {
|
|
579
|
+
try {
|
|
580
|
+
const settingsObj = JSON.parse(settingsData);
|
|
581
|
+
if (settingsObj.model) opencodeConfig.model = settingsObj.model;
|
|
582
|
+
} catch {
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
const configPath = join5(rootPath, "opencode.json");
|
|
586
|
+
backupFile(configPath);
|
|
587
|
+
writeFileSafe(configPath, JSON.stringify(opencodeConfig, null, 2) + "\n");
|
|
588
|
+
files.push("opencode.json");
|
|
589
|
+
return formatMigrationResult("Claude Code", files, warnings, suggestions);
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
// src/migrate/cursor.ts
|
|
593
|
+
import { join as join6 } from "path";
|
|
594
|
+
function migrateCursor(rootPath) {
|
|
595
|
+
const files = [];
|
|
596
|
+
const warnings = [];
|
|
597
|
+
const suggestions = [];
|
|
598
|
+
const cursorRules = readFileSafe(join6(rootPath, ".cursorrules"));
|
|
599
|
+
if (cursorRules) {
|
|
600
|
+
const destPath = join6(rootPath, "AGENTS.md");
|
|
601
|
+
backupFile(destPath);
|
|
602
|
+
writeFileSafe(destPath, cursorRules);
|
|
603
|
+
files.push("AGENTS.md (\uAE30\uC874 .cursorrules)");
|
|
604
|
+
}
|
|
605
|
+
const mcpConfig = readFileSafe(join6(rootPath, ".cursor", "mcp.json"));
|
|
606
|
+
if (mcpConfig) {
|
|
607
|
+
try {
|
|
608
|
+
const mcpData = JSON.parse(mcpConfig);
|
|
609
|
+
if (mcpData.mcpServers) {
|
|
610
|
+
suggestions.push("MCP \uC11C\uBC84 \uC124\uC815 \uAC10\uC9C0\uB428 - opencode.json\uC758 mcp \uC139\uC158\uC5D0 \uCD94\uAC00 \uD544\uC694");
|
|
611
|
+
}
|
|
612
|
+
} catch {
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
const cursorSettings = readFileSafe(join6(rootPath, ".cursor", "settings.json"));
|
|
616
|
+
if (cursorSettings) {
|
|
617
|
+
suggestions.push("Cursor \uC124\uC815 \uAC10\uC9C0\uB428 - \uC218\uB3D9\uC73C\uB85C OpenCode \uC124\uC815\uC5D0 \uBC18\uC601 \uD544\uC694");
|
|
618
|
+
}
|
|
619
|
+
const cursorRulesContents = readFileSafe(join6(rootPath, ".cursor", "rules"));
|
|
620
|
+
if (cursorRulesContents) {
|
|
621
|
+
warnings.push("Cursor Rules \uAC10\uC9C0\uB428 - AGENTS.md\uC5D0 \uBCD1\uD569 \uD544\uC694");
|
|
622
|
+
}
|
|
623
|
+
const opencodeConfig = {
|
|
624
|
+
$schema: "https://opencode.ai/config.json",
|
|
625
|
+
theme: "opencode",
|
|
626
|
+
autoupdate: true
|
|
627
|
+
};
|
|
628
|
+
const configPath = join6(rootPath, "opencode.json");
|
|
629
|
+
backupFile(configPath);
|
|
630
|
+
writeFileSafe(configPath, JSON.stringify(opencodeConfig, null, 2) + "\n");
|
|
631
|
+
files.push("opencode.json");
|
|
632
|
+
return formatMigrationResult("Cursor", files, warnings, suggestions);
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// src/migrate/aider.ts
|
|
636
|
+
import { join as join7 } from "path";
|
|
637
|
+
function migrateAider(rootPath) {
|
|
638
|
+
const files = [];
|
|
639
|
+
const warnings = [];
|
|
640
|
+
const suggestions = [];
|
|
641
|
+
const aiderConfig = readFileSafe(join7(rootPath, ".aider.conf.yml")) || readFileSafe(join7(rootPath, ".aider.conf.yaml"));
|
|
642
|
+
if (aiderConfig) {
|
|
643
|
+
const parsed = parseYAML(aiderConfig);
|
|
644
|
+
const opencodeConfig = {
|
|
645
|
+
$schema: "https://opencode.ai/config.json",
|
|
646
|
+
theme: "opencode",
|
|
647
|
+
autoupdate: true
|
|
648
|
+
};
|
|
649
|
+
if (parsed.model) {
|
|
650
|
+
opencodeConfig.model = parsed.model;
|
|
651
|
+
suggestions.push(`\uBAA8\uB378: ${parsed.model}`);
|
|
652
|
+
}
|
|
653
|
+
if (parsed["completion-model"]) {
|
|
654
|
+
suggestions.push(`\uC644\uB8CC \uBAA8\uB378: ${parsed["completion-model"]}`);
|
|
655
|
+
}
|
|
656
|
+
if (parsed["no-fast-subattempts"]) {
|
|
657
|
+
warnings.push("no-fast-subattempts \uC124\uC815\uC740 OpenCode\uC5D0\uC11C \uC9C0\uC6D0\uB418\uC9C0 \uC54A\uC74C");
|
|
658
|
+
}
|
|
659
|
+
const configPath = join7(rootPath, "opencode.json");
|
|
660
|
+
backupFile(configPath);
|
|
661
|
+
writeFileSafe(configPath, JSON.stringify(opencodeConfig, null, 2) + "\n");
|
|
662
|
+
files.push("opencode.json");
|
|
663
|
+
}
|
|
664
|
+
const conventions = readFileSafe(join7(rootPath, ".aider.conventions"));
|
|
665
|
+
if (conventions) {
|
|
666
|
+
const agentsMDPath = join7(rootPath, "AGENTS.md");
|
|
667
|
+
backupFile(agentsMDPath);
|
|
668
|
+
writeFileSafe(agentsMDPath, `# Project Conventions
|
|
669
|
+
|
|
670
|
+
${conventions}
|
|
671
|
+
`);
|
|
672
|
+
files.push("AGENTS.md (.aider.conventions)");
|
|
673
|
+
}
|
|
674
|
+
const chatHistory = readFileSafe(join7(rootPath, ".aider.chat.history.md"));
|
|
675
|
+
if (chatHistory) {
|
|
676
|
+
warnings.push(".aider.chat.history.md\uB294 \uB9C8\uC774\uADF8\uB808\uC774\uC158 \uBD88\uAC00 - \uC218\uB3D9 \uBC31\uC5C5 \uAD8C\uC7A5");
|
|
677
|
+
}
|
|
678
|
+
return formatMigrationResult("Aider", files, warnings, suggestions);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// src/migrate/index.ts
|
|
682
|
+
async function runMigration(tool, rootPath) {
|
|
683
|
+
switch (tool) {
|
|
684
|
+
case "claude-code":
|
|
685
|
+
return migrateClaudeCode(rootPath);
|
|
686
|
+
case "cursor":
|
|
687
|
+
return migrateCursor(rootPath);
|
|
688
|
+
case "aider":
|
|
689
|
+
return migrateAider(rootPath);
|
|
690
|
+
default:
|
|
691
|
+
return `\u274C \uC9C0\uC6D0\uD558\uC9C0 \uC54A\uB294 \uB3C4\uAD6C\uC785\uB2C8\uB2E4: ${tool}
|
|
692
|
+
|
|
693
|
+
\uC9C0\uC6D0: claude-code, cursor, aider`;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function autoMigrate(rootPath) {
|
|
697
|
+
const detected = detectTool(rootPath);
|
|
698
|
+
if (!detected) {
|
|
699
|
+
return `\u274C \uB9C8\uC774\uADF8\uB808\uC774\uC158\uD560 \uB3C4\uAD6C\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC2B5\uB2C8\uB2E4.
|
|
700
|
+
|
|
701
|
+
\uAC10\uC9C0 \uAC00\uB2A5\uD55C \uB3C4\uAD6C:
|
|
702
|
+
- Claude Code: .claude/, CLAUDE.md
|
|
703
|
+
- Cursor: .cursorrules
|
|
704
|
+
- Aider: .aider.conf.yml
|
|
705
|
+
|
|
706
|
+
\uC218\uB3D9\uC73C\uB85C \uC9C0\uC815: opencode-setup migrate <tool-name>`;
|
|
707
|
+
}
|
|
708
|
+
switch (detected) {
|
|
709
|
+
case "claude-code":
|
|
710
|
+
return migrateClaudeCode(rootPath);
|
|
711
|
+
case "cursor":
|
|
712
|
+
return migrateCursor(rootPath);
|
|
713
|
+
case "aider":
|
|
714
|
+
return migrateAider(rootPath);
|
|
715
|
+
default:
|
|
716
|
+
return `\u274C \uC54C \uC218 \uC5C6\uB294 \uB3C4\uAD6C: ${detected}`;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// src/core/config-generator.ts
|
|
721
|
+
import { mkdirSync as mkdirSync3, existsSync as existsSync6, writeFileSync as writeFileSync3, cpSync as cpSync3 } from "fs";
|
|
722
|
+
import { join as join8, dirname as dirname3 } from "path";
|
|
723
|
+
var PROVIDER_API_KEYS = {
|
|
724
|
+
anthropic: "ANTHROPIC_API_KEY",
|
|
725
|
+
google: "GEMINI_API_KEY",
|
|
726
|
+
openai: "OPENAI_API_KEY",
|
|
727
|
+
deepseek: "DEEPSEEK_API_KEY",
|
|
728
|
+
openrouter: "OPENROUTER_API_KEY",
|
|
729
|
+
minimax: "MINIMAX_API_KEY"
|
|
730
|
+
};
|
|
731
|
+
var BUDGET_MODELS = {
|
|
732
|
+
free: { build: "opencode/big-pickle", plan: "opencode/big-pickle" },
|
|
733
|
+
low: { build: "minimax/minimax-m2.5", plan: "opencode/big-pickle" },
|
|
734
|
+
mid: { build: "anthropic/claude-sonnet-4-5", plan: "google/gemini-2.5-flash" },
|
|
735
|
+
high: { build: "anthropic/claude-sonnet-4-5", plan: "anthropic/claude-opus-4-6" }
|
|
736
|
+
};
|
|
737
|
+
var LSP_BY_LANGUAGE = {
|
|
738
|
+
typescript: "typescript-language-server",
|
|
739
|
+
javascript: "typescript-language-server",
|
|
740
|
+
go: "gopls",
|
|
741
|
+
python: "pyright",
|
|
742
|
+
rust: "rust-analyzer"
|
|
743
|
+
};
|
|
744
|
+
function getPermissionConfig(level) {
|
|
745
|
+
switch (level) {
|
|
746
|
+
case "safe":
|
|
747
|
+
return { edit: "ask", bash: { "*": "ask" } };
|
|
748
|
+
case "balanced":
|
|
749
|
+
return {
|
|
750
|
+
edit: "allow",
|
|
751
|
+
bash: { "npm *": "allow", "git *": "allow", "rm *": "ask", "*": "ask" }
|
|
752
|
+
};
|
|
753
|
+
case "auto":
|
|
754
|
+
return { edit: "allow", bash: { "*": "allow" } };
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
function getAgentConfig(budget) {
|
|
758
|
+
const models = BUDGET_MODELS[budget] || BUDGET_MODELS.mid;
|
|
759
|
+
return {
|
|
760
|
+
build: { model: models.build },
|
|
761
|
+
plan: { model: models.plan }
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
function getMCPConfig(mcpServers) {
|
|
765
|
+
const result = {};
|
|
766
|
+
for (const mcp of mcpServers) {
|
|
767
|
+
result[mcp.name] = {
|
|
768
|
+
type: "sse",
|
|
769
|
+
...mcp.config
|
|
770
|
+
};
|
|
771
|
+
}
|
|
772
|
+
return result;
|
|
773
|
+
}
|
|
774
|
+
function getLSPConfig(language) {
|
|
775
|
+
const lspCommand = LSP_BY_LANGUAGE[language];
|
|
776
|
+
if (!lspCommand) return {};
|
|
777
|
+
return {
|
|
778
|
+
[language]: {
|
|
779
|
+
command: lspCommand
|
|
780
|
+
}
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
function generateGlobalConfig(profile) {
|
|
784
|
+
const config = {
|
|
785
|
+
$schema: "https://opencode.ai/config.json",
|
|
786
|
+
theme: "opencode",
|
|
787
|
+
autoupdate: true,
|
|
788
|
+
plugin: profile.plugins
|
|
789
|
+
};
|
|
790
|
+
if (profile.providers.length > 0) {
|
|
791
|
+
config.provider = {};
|
|
792
|
+
for (const provider of profile.providers) {
|
|
793
|
+
const apiKey = PROVIDER_API_KEYS[provider];
|
|
794
|
+
if (apiKey) {
|
|
795
|
+
config.provider[provider] = {
|
|
796
|
+
options: { apiKey: `{env:${apiKey}}` }
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
const models = BUDGET_MODELS[profile.budget] || BUDGET_MODELS.mid;
|
|
802
|
+
config.model = models.build;
|
|
803
|
+
return config;
|
|
804
|
+
}
|
|
805
|
+
function generateProjectConfig(profile) {
|
|
806
|
+
const config = {
|
|
807
|
+
$schema: "https://opencode.ai/config.json",
|
|
808
|
+
permission: getPermissionConfig(profile.permissionLevel),
|
|
809
|
+
agent: getAgentConfig(profile.budget)
|
|
810
|
+
};
|
|
811
|
+
if (profile.mcpServers.length > 0) {
|
|
812
|
+
config.mcp = getMCPConfig(profile.mcpServers);
|
|
813
|
+
}
|
|
814
|
+
const lspConfig = getLSPConfig(profile.projectLanguage);
|
|
815
|
+
if (Object.keys(lspConfig).length > 0) {
|
|
816
|
+
config.lsp = lspConfig;
|
|
817
|
+
}
|
|
818
|
+
if (profile.projectScale === "monorepo") {
|
|
819
|
+
config.instructions = ["packages/*/AGENTS.md"];
|
|
820
|
+
}
|
|
821
|
+
return config;
|
|
822
|
+
}
|
|
823
|
+
function backupFile2(path) {
|
|
824
|
+
if (existsSync6(path)) {
|
|
825
|
+
const backupPath = `${path}.bak`;
|
|
826
|
+
cpSync3(path, backupPath);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
function writeConfig(config, filePath) {
|
|
830
|
+
const dir = dirname3(filePath);
|
|
831
|
+
if (!existsSync6(dir)) {
|
|
832
|
+
mkdirSync3(dir, { recursive: true });
|
|
833
|
+
}
|
|
834
|
+
backupFile2(filePath);
|
|
835
|
+
const content = JSON.stringify(config, null, 2) + "\n";
|
|
836
|
+
writeFileSync3(filePath, content, "utf-8");
|
|
837
|
+
}
|
|
838
|
+
function writeGlobalConfig(profile, homeDir) {
|
|
839
|
+
const config = generateGlobalConfig(profile);
|
|
840
|
+
const path = join8(homeDir, ".config/opencode/opencode.json");
|
|
841
|
+
writeConfig(config, path);
|
|
842
|
+
}
|
|
843
|
+
function writeProjectConfig(profile, projectDir) {
|
|
844
|
+
const config = generateProjectConfig(profile);
|
|
845
|
+
const path = join8(projectDir, "opencode.json");
|
|
846
|
+
writeConfig(config, path);
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
// src/core/agents-md-generator.ts
|
|
850
|
+
import { existsSync as existsSync7, readFileSync as readFileSync5, writeFileSync as writeFileSync4, mkdirSync as mkdirSync4, cpSync as cpSync4 } from "fs";
|
|
851
|
+
import { join as join9, dirname as dirname4 } from "path";
|
|
852
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
853
|
+
var __dirname2 = dirname4(fileURLToPath2(import.meta.url));
|
|
854
|
+
var TEMPLATES_DIR2 = join9(__dirname2, "templates/agents-md");
|
|
855
|
+
var FRAMEWORK_TEMPLATE_MAP = {
|
|
856
|
+
nextjs: "frontend-ts.md",
|
|
857
|
+
react: "frontend-ts.md",
|
|
858
|
+
svelte: "frontend-ts.md",
|
|
859
|
+
vue: "frontend-ts.md",
|
|
860
|
+
go: "backend-go.md",
|
|
861
|
+
gin: "backend-go.md",
|
|
862
|
+
fastapi: "backend-python.md",
|
|
863
|
+
python: "backend-python.md",
|
|
864
|
+
django: "backend-python.md",
|
|
865
|
+
flask: "backend-python.md"
|
|
866
|
+
};
|
|
867
|
+
var DEFAULT_TEMPLATE = "base.md";
|
|
868
|
+
function getTemplateForFramework(framework) {
|
|
869
|
+
const lowerFramework = framework.toLowerCase();
|
|
870
|
+
for (const [key, template] of Object.entries(FRAMEWORK_TEMPLATE_MAP)) {
|
|
871
|
+
if (lowerFramework.includes(key)) {
|
|
872
|
+
return template;
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
return DEFAULT_TEMPLATE;
|
|
876
|
+
}
|
|
877
|
+
function replaceVariables(content, profile) {
|
|
878
|
+
const projectName = "My Project";
|
|
879
|
+
const testRunner = profile.testRunner || "vitest";
|
|
880
|
+
const linter = profile.linter || "biome";
|
|
881
|
+
let result = content.replace(/\{\{projectName\}\}/g, projectName);
|
|
882
|
+
result = result.replace(/\{\{testRunner\}\}/g, testRunner);
|
|
883
|
+
result = result.replace(/\{\{linter\}\}/g, linter);
|
|
884
|
+
result = result.replace(/\{\{projectDescription\}\}/g, "This project...");
|
|
885
|
+
result = result.replace(/\{\{structure\}\}/g, "See project structure");
|
|
886
|
+
result = result.replace(/\{\{codeStandards\}\}/g, "Follow TypeScript best practices");
|
|
887
|
+
result = result.replace(/\{\{testingGuidelines\}\}/g, "Run tests with " + testRunner);
|
|
888
|
+
if (profile.migrationSource?.rules && profile.migrationSource.rules.length > 0) {
|
|
889
|
+
const rulesContent = profile.migrationSource.rules.join("\n");
|
|
890
|
+
if (!result.includes("## Rules")) {
|
|
891
|
+
result += "\n\n## Rules\n\n" + rulesContent;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
return result;
|
|
895
|
+
}
|
|
896
|
+
function generateAgentsMD(profile) {
|
|
897
|
+
const templateName = getTemplateForFramework(profile.projectFramework);
|
|
898
|
+
const templatePath = join9(TEMPLATES_DIR2, templateName);
|
|
899
|
+
if (!existsSync7(templatePath)) {
|
|
900
|
+
const fallbackPath = join9(TEMPLATES_DIR2, DEFAULT_TEMPLATE);
|
|
901
|
+
if (!existsSync7(fallbackPath)) {
|
|
902
|
+
return "# " + (profile.projectFramework || "My Project") + "\n\nProject configuration.";
|
|
903
|
+
}
|
|
904
|
+
const fallbackContent = readFileSync5(fallbackPath, "utf-8");
|
|
905
|
+
return replaceVariables(fallbackContent, profile);
|
|
906
|
+
}
|
|
907
|
+
const content = readFileSync5(templatePath, "utf-8");
|
|
908
|
+
return replaceVariables(content, profile);
|
|
909
|
+
}
|
|
910
|
+
function backupFile3(path) {
|
|
911
|
+
if (existsSync7(path)) {
|
|
912
|
+
const backupPath = `${path}.bak`;
|
|
913
|
+
cpSync4(path, backupPath);
|
|
914
|
+
}
|
|
915
|
+
}
|
|
916
|
+
function writeAgentsMD(content, dir) {
|
|
917
|
+
const filePath = join9(dir, "AGENTS.md");
|
|
918
|
+
const dirPath = dirname4(filePath);
|
|
919
|
+
if (!existsSync7(dirPath)) {
|
|
920
|
+
mkdirSync4(dirPath, { recursive: true });
|
|
921
|
+
}
|
|
922
|
+
backupFile3(filePath);
|
|
923
|
+
writeFileSync4(filePath, content, "utf-8");
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// src/core/command-generator.ts
|
|
927
|
+
import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync as writeFileSync5, mkdirSync as mkdirSync5, cpSync as cpSync5 } from "fs";
|
|
928
|
+
import { join as join10, dirname as dirname5 } from "path";
|
|
929
|
+
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
930
|
+
var __dirname3 = dirname5(fileURLToPath3(import.meta.url));
|
|
931
|
+
var TEMPLATES_DIR3 = join10(__dirname3, "templates/commands");
|
|
932
|
+
var DEFAULT_COMMANDS = ["test.md", "lint.md", "review.md", "plan.md"];
|
|
933
|
+
function generateCommands() {
|
|
934
|
+
const commands = {};
|
|
935
|
+
for (const cmd of DEFAULT_COMMANDS) {
|
|
936
|
+
const cmdPath = join10(TEMPLATES_DIR3, cmd);
|
|
937
|
+
if (existsSync8(cmdPath)) {
|
|
938
|
+
commands[cmd] = readFileSync6(cmdPath, "utf-8");
|
|
939
|
+
}
|
|
940
|
+
}
|
|
941
|
+
return commands;
|
|
942
|
+
}
|
|
943
|
+
function backupFile4(path) {
|
|
944
|
+
if (existsSync8(path)) {
|
|
945
|
+
const backupPath = `${path}.bak`;
|
|
946
|
+
cpSync5(path, backupPath);
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
function writeCommands(commands, dir) {
|
|
950
|
+
const commandsDir = join10(dir, ".opencode", "commands");
|
|
951
|
+
if (!existsSync8(commandsDir)) {
|
|
952
|
+
mkdirSync5(commandsDir, { recursive: true });
|
|
953
|
+
}
|
|
954
|
+
for (const [filename, content] of Object.entries(commands)) {
|
|
955
|
+
const filePath = join10(commandsDir, filename);
|
|
956
|
+
backupFile4(filePath);
|
|
957
|
+
writeFileSync5(filePath, content, "utf-8");
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
// src/core/agent-generator.ts
|
|
962
|
+
import { existsSync as existsSync9, readFileSync as readFileSync7, writeFileSync as writeFileSync6, mkdirSync as mkdirSync6, cpSync as cpSync6 } from "fs";
|
|
963
|
+
import { join as join11, dirname as dirname6 } from "path";
|
|
964
|
+
import { fileURLToPath as fileURLToPath4 } from "url";
|
|
965
|
+
var __dirname4 = dirname6(fileURLToPath4(import.meta.url));
|
|
966
|
+
var TEMPLATES_DIR4 = join11(__dirname4, "templates/agents");
|
|
967
|
+
var DEFAULT_AGENTS = ["reviewer.md", "tester.md", "planner.md"];
|
|
968
|
+
function generateAgents() {
|
|
969
|
+
const agents = {};
|
|
970
|
+
for (const agent of DEFAULT_AGENTS) {
|
|
971
|
+
const agentPath = join11(TEMPLATES_DIR4, agent);
|
|
972
|
+
if (existsSync9(agentPath)) {
|
|
973
|
+
agents[agent] = readFileSync7(agentPath, "utf-8");
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
return agents;
|
|
977
|
+
}
|
|
978
|
+
function backupFile5(path) {
|
|
979
|
+
if (existsSync9(path)) {
|
|
980
|
+
const backupPath = `${path}.bak`;
|
|
981
|
+
cpSync6(path, backupPath);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
function writeAgents(agents, dir) {
|
|
985
|
+
const agentsDir = join11(dir, ".opencode", "agents");
|
|
986
|
+
if (!existsSync9(agentsDir)) {
|
|
987
|
+
mkdirSync6(agentsDir, { recursive: true });
|
|
988
|
+
}
|
|
989
|
+
for (const [filename, content] of Object.entries(agents)) {
|
|
990
|
+
const filePath = join11(agentsDir, filename);
|
|
991
|
+
backupFile5(filePath);
|
|
992
|
+
writeFileSync6(filePath, content, "utf-8");
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
// src/core/skill-generator.ts
|
|
997
|
+
import { existsSync as existsSync10, readFileSync as readFileSync8, writeFileSync as writeFileSync7, mkdirSync as mkdirSync7, cpSync as cpSync7 } from "fs";
|
|
998
|
+
import { join as join12, dirname as dirname7 } from "path";
|
|
999
|
+
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
1000
|
+
var __dirname5 = dirname7(fileURLToPath5(import.meta.url));
|
|
1001
|
+
var TEMPLATES_DIR5 = join12(__dirname5, "templates/skills");
|
|
1002
|
+
var BASE_SKILLS = ["code-review", "testing"];
|
|
1003
|
+
var FRONTEND_SKILL = "frontend-design";
|
|
1004
|
+
function generateSkills(profile) {
|
|
1005
|
+
const skills = {};
|
|
1006
|
+
const language = profile.projectLanguage?.toLowerCase() || "";
|
|
1007
|
+
const isFrontend = ["typescript", "javascript"].includes(language) || profile.projectFramework?.toLowerCase().includes("react") || profile.projectFramework?.toLowerCase().includes("next");
|
|
1008
|
+
const skillDirs = [...BASE_SKILLS];
|
|
1009
|
+
if (isFrontend) {
|
|
1010
|
+
skillDirs.push(FRONTEND_SKILL);
|
|
1011
|
+
}
|
|
1012
|
+
for (const skill of skillDirs) {
|
|
1013
|
+
const skillDir = join12(TEMPLATES_DIR5, skill);
|
|
1014
|
+
if (existsSync10(skillDir)) {
|
|
1015
|
+
const files = [
|
|
1016
|
+
{ src: join12(skillDir, "SKILL.md"), dest: skill + "/SKILL.md" }
|
|
1017
|
+
];
|
|
1018
|
+
for (const file of files) {
|
|
1019
|
+
if (existsSync10(file.src)) {
|
|
1020
|
+
skills[file.dest] = readFileSync8(file.src, "utf-8");
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
return skills;
|
|
1026
|
+
}
|
|
1027
|
+
function backupFile6(path) {
|
|
1028
|
+
if (existsSync10(path)) {
|
|
1029
|
+
const backupPath = `${path}.bak`;
|
|
1030
|
+
cpSync7(path, backupPath);
|
|
1031
|
+
}
|
|
1032
|
+
}
|
|
1033
|
+
function writeSkills(skills, dir) {
|
|
1034
|
+
const skillsDir = join12(dir, ".opencode", "skills");
|
|
1035
|
+
if (!existsSync10(skillsDir)) {
|
|
1036
|
+
mkdirSync7(skillsDir, { recursive: true });
|
|
1037
|
+
}
|
|
1038
|
+
for (const [relativePath, content] of Object.entries(skills)) {
|
|
1039
|
+
const skillSubDir = relativePath.split("/")[0];
|
|
1040
|
+
const fileName = relativePath.split("/")[1] || "SKILL.md";
|
|
1041
|
+
const skillDir = join12(skillsDir, skillSubDir);
|
|
1042
|
+
const filePath = join12(skillDir, fileName);
|
|
1043
|
+
if (!existsSync10(skillDir)) {
|
|
1044
|
+
mkdirSync7(skillDir, { recursive: true });
|
|
1045
|
+
}
|
|
1046
|
+
backupFile6(filePath);
|
|
1047
|
+
writeFileSync7(filePath, content, "utf-8");
|
|
1048
|
+
}
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
// src/core/env-generator.ts
|
|
1052
|
+
import { writeFileSync as writeFileSync8 } from "fs";
|
|
1053
|
+
import { join as join13 } from "path";
|
|
1054
|
+
var PROVIDER_ENV_VARS = {
|
|
1055
|
+
anthropic: { key: "ANTHROPIC_API_KEY", name: "Anthropic" },
|
|
1056
|
+
google: { key: "GEMINI_API_KEY", name: "Google Gemini" },
|
|
1057
|
+
openai: { key: "OPENAI_API_KEY", name: "OpenAI" },
|
|
1058
|
+
deepseek: { key: "DEEPSEEK_API_KEY", name: "DeepSeek" },
|
|
1059
|
+
openrouter: { key: "OPENROUTER_API_KEY", name: "OpenRouter" },
|
|
1060
|
+
minimax: { key: "MINIMAX_API_KEY", name: "MiniMax" },
|
|
1061
|
+
ollama: { key: "OLLAMA_BASE_URL", name: "Ollama" }
|
|
1062
|
+
};
|
|
1063
|
+
function generateEnvExample(profile) {
|
|
1064
|
+
const lines = [
|
|
1065
|
+
"# OpenCode Environment Variables",
|
|
1066
|
+
"# Copy this file to .env and fill in your values",
|
|
1067
|
+
""
|
|
1068
|
+
];
|
|
1069
|
+
for (const provider of profile.providers) {
|
|
1070
|
+
const envConfig = PROVIDER_ENV_VARS[provider];
|
|
1071
|
+
if (envConfig) {
|
|
1072
|
+
lines.push(`# ${envConfig.name}`);
|
|
1073
|
+
lines.push(`${envConfig.key}=your_key_here`);
|
|
1074
|
+
lines.push("");
|
|
1075
|
+
}
|
|
1076
|
+
}
|
|
1077
|
+
lines.push("# Optional: Custom OpenCode settings");
|
|
1078
|
+
lines.push("# OPENCODE_CONFIG=/path/to/custom/config.json");
|
|
1079
|
+
return lines.join("\n");
|
|
1080
|
+
}
|
|
1081
|
+
function writeEnvExample(content, dir) {
|
|
1082
|
+
const filePath = join13(dir, ".env.example");
|
|
1083
|
+
writeFileSync8(filePath, content, "utf-8");
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
export {
|
|
1087
|
+
writeGlobalConfig,
|
|
1088
|
+
writeProjectConfig,
|
|
1089
|
+
generateAgentsMD,
|
|
1090
|
+
writeAgentsMD,
|
|
1091
|
+
generateCommands,
|
|
1092
|
+
writeCommands,
|
|
1093
|
+
generateAgents,
|
|
1094
|
+
writeAgents,
|
|
1095
|
+
generateSkills,
|
|
1096
|
+
writeSkills,
|
|
1097
|
+
generateEnvExample,
|
|
1098
|
+
writeEnvExample,
|
|
1099
|
+
listPresets,
|
|
1100
|
+
applyPreset,
|
|
1101
|
+
runValidation,
|
|
1102
|
+
runAllChecks,
|
|
1103
|
+
formatReport,
|
|
1104
|
+
runMigration,
|
|
1105
|
+
autoMigrate
|
|
1106
|
+
};
|