oh-my-opencode-serverlocal 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.ja-JP.md +775 -0
- package/README.ko-KR.md +796 -0
- package/README.md +792 -0
- package/README.zh-CN.md +765 -0
- package/dist/agents/council.d.ts +27 -0
- package/dist/agents/councillor.d.ts +2 -0
- package/dist/agents/designer.d.ts +2 -0
- package/dist/agents/explorer.d.ts +2 -0
- package/dist/agents/fixer.d.ts +2 -0
- package/dist/agents/index.d.ts +31 -0
- package/dist/agents/librarian.d.ts +2 -0
- package/dist/agents/observer.d.ts +2 -0
- package/dist/agents/oracle.d.ts +2 -0
- package/dist/agents/orchestrator.d.ts +28 -0
- package/dist/agents/permissions.d.ts +10 -0
- package/dist/cli/background-subagents.d.ts +13 -0
- package/dist/cli/companion.d.ts +4 -0
- package/dist/cli/config-io.d.ts +25 -0
- package/dist/cli/config-manager.d.ts +4 -0
- package/dist/cli/custom-skills-registry.d.ts +18 -0
- package/dist/cli/custom-skills.d.ts +13 -0
- package/dist/cli/doctor.d.ts +38 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.js +3565 -0
- package/dist/cli/install.d.ts +7 -0
- package/dist/cli/model-key-normalization.d.ts +1 -0
- package/dist/cli/paths.d.ts +35 -0
- package/dist/cli/providers.d.ts +145 -0
- package/dist/cli/skills.d.ts +24 -0
- package/dist/cli/system.d.ts +6 -0
- package/dist/cli/types.d.ts +47 -0
- package/dist/companion/manager.d.ts +50 -0
- package/dist/companion/updater.d.ts +36 -0
- package/dist/config/agent-mcps.d.ts +11 -0
- package/dist/config/constants.d.ts +38 -0
- package/dist/config/council-schema.d.ts +137 -0
- package/dist/config/index.d.ts +5 -0
- package/dist/config/loader.d.ts +88 -0
- package/dist/config/runtime-preset.d.ts +11 -0
- package/dist/config/schema.d.ts +1107 -0
- package/dist/config/strip-orchestrator-model.d.ts +9 -0
- package/dist/config/utils.d.ts +18 -0
- package/dist/council/council-manager.d.ts +53 -0
- package/dist/council/index.d.ts +1 -0
- package/dist/hooks/apply-patch/codec.d.ts +7 -0
- package/dist/hooks/apply-patch/errors.d.ts +25 -0
- package/dist/hooks/apply-patch/execution-context.d.ts +27 -0
- package/dist/hooks/apply-patch/index.d.ts +15 -0
- package/dist/hooks/apply-patch/matching.d.ts +26 -0
- package/dist/hooks/apply-patch/operations.d.ts +3 -0
- package/dist/hooks/apply-patch/prepared-changes.d.ts +17 -0
- package/dist/hooks/apply-patch/resolution.d.ts +19 -0
- package/dist/hooks/apply-patch/rewrite.d.ts +7 -0
- package/dist/hooks/apply-patch/test-helpers.d.ts +6 -0
- package/dist/hooks/apply-patch/types.d.ts +80 -0
- package/dist/hooks/auto-update-checker/cache.d.ts +11 -0
- package/dist/hooks/auto-update-checker/checker.d.ts +38 -0
- package/dist/hooks/auto-update-checker/constants.d.ts +12 -0
- package/dist/hooks/auto-update-checker/index.d.ts +18 -0
- package/dist/hooks/auto-update-checker/skill-sync.d.ts +67 -0
- package/dist/hooks/auto-update-checker/types.d.ts +34 -0
- package/dist/hooks/chat-headers.d.ts +16 -0
- package/dist/hooks/command-hook-utils.d.ts +5 -0
- package/dist/hooks/deepwork/index.d.ts +13 -0
- package/dist/hooks/delegate-task-retry/hook.d.ts +8 -0
- package/dist/hooks/delegate-task-retry/patterns.d.ts +11 -0
- package/dist/hooks/filter-available-skills/index.d.ts +19 -0
- package/dist/hooks/foreground-fallback/index.d.ts +122 -0
- package/dist/hooks/image-hook.d.ts +8 -0
- package/dist/hooks/index.d.ts +16 -0
- package/dist/hooks/json-error-recovery/hook.d.ts +18 -0
- package/dist/hooks/loop-command/index.d.ts +13 -0
- package/dist/hooks/phase-reminder/index.d.ts +25 -0
- package/dist/hooks/post-file-tool-nudge/index.d.ts +23 -0
- package/dist/hooks/reflect/index.d.ts +13 -0
- package/dist/hooks/session-lifecycle.d.ts +10 -0
- package/dist/hooks/task-session-manager/index.d.ts +58 -0
- package/dist/hooks/task-session-manager/pending-call-tracker.d.ts +13 -0
- package/dist/hooks/task-session-manager/task-context-tracker.d.ts +14 -0
- package/dist/hooks/types.d.ts +26 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +39519 -0
- package/dist/interview/dashboard-manager.d.ts +21 -0
- package/dist/interview/dashboard.d.ts +67 -0
- package/dist/interview/document.d.ts +29 -0
- package/dist/interview/helpers.d.ts +10 -0
- package/dist/interview/index.d.ts +1 -0
- package/dist/interview/manager.d.ts +21 -0
- package/dist/interview/parser.d.ts +11 -0
- package/dist/interview/prompts.d.ts +7 -0
- package/dist/interview/server.d.ts +15 -0
- package/dist/interview/service.d.ts +45 -0
- package/dist/interview/session-server.d.ts +21 -0
- package/dist/interview/types.d.ts +111 -0
- package/dist/interview/ui.d.ts +11 -0
- package/dist/loop/loop-session.d.ts +64 -0
- package/dist/mcp/context7.d.ts +6 -0
- package/dist/mcp/grep-app.d.ts +6 -0
- package/dist/mcp/index.d.ts +8 -0
- package/dist/mcp/types.d.ts +12 -0
- package/dist/mcp/websearch.d.ts +9 -0
- package/dist/multiplexer/cmux/close-policy.d.ts +20 -0
- package/dist/multiplexer/cmux/index.d.ts +102 -0
- package/dist/multiplexer/cmux/session-lifecycle.d.ts +92 -0
- package/dist/multiplexer/cmux/session-state.d.ts +45 -0
- package/dist/multiplexer/factory.d.ts +17 -0
- package/dist/multiplexer/herdr/index.d.ts +35 -0
- package/dist/multiplexer/index.d.ts +12 -0
- package/dist/multiplexer/session-manager.d.ts +69 -0
- package/dist/multiplexer/shared.d.ts +31 -0
- package/dist/multiplexer/tmux/index.d.ts +26 -0
- package/dist/multiplexer/types.d.ts +57 -0
- package/dist/multiplexer/zellij/index.d.ts +47 -0
- package/dist/tools/acp-run.d.ts +3 -0
- package/dist/tools/ast-grep/cli.d.ts +15 -0
- package/dist/tools/ast-grep/constants.d.ts +25 -0
- package/dist/tools/ast-grep/downloader.d.ts +5 -0
- package/dist/tools/ast-grep/index.d.ts +10 -0
- package/dist/tools/ast-grep/tools.d.ts +3 -0
- package/dist/tools/ast-grep/types.d.ts +30 -0
- package/dist/tools/ast-grep/utils.d.ts +4 -0
- package/dist/tools/cancel-task.d.ts +16 -0
- package/dist/tools/council.d.ts +10 -0
- package/dist/tools/index.d.ts +7 -0
- package/dist/tools/preset-manager.d.ts +26 -0
- package/dist/tools/smartfetch/binary.d.ts +3 -0
- package/dist/tools/smartfetch/cache.d.ts +6 -0
- package/dist/tools/smartfetch/constants.d.ts +12 -0
- package/dist/tools/smartfetch/index.d.ts +3 -0
- package/dist/tools/smartfetch/network.d.ts +38 -0
- package/dist/tools/smartfetch/secondary-model.d.ts +35 -0
- package/dist/tools/smartfetch/tool.d.ts +3 -0
- package/dist/tools/smartfetch/types.d.ts +122 -0
- package/dist/tools/smartfetch/utils.d.ts +20 -0
- package/dist/tui-state.d.ts +18 -0
- package/dist/tui.d.ts +13 -0
- package/dist/tui.js +1112 -0
- package/dist/utils/agent-variant.d.ts +23 -0
- package/dist/utils/background-job-board.d.ts +113 -0
- package/dist/utils/background-job-coordinator.d.ts +72 -0
- package/dist/utils/background-job-store.d.ts +46 -0
- package/dist/utils/compat.d.ts +29 -0
- package/dist/utils/councillor-models.d.ts +20 -0
- package/dist/utils/env.d.ts +3 -0
- package/dist/utils/escape-html.d.ts +1 -0
- package/dist/utils/frontmatter.d.ts +6 -0
- package/dist/utils/guards.d.ts +4 -0
- package/dist/utils/index.d.ts +10 -0
- package/dist/utils/internal-initiator.d.ts +11 -0
- package/dist/utils/logger.d.ts +6 -0
- package/dist/utils/polling.d.ts +21 -0
- package/dist/utils/session.d.ts +75 -0
- package/dist/utils/subagent-depth.d.ts +35 -0
- package/dist/utils/system-collapse.d.ts +6 -0
- package/dist/utils/task.d.ts +20 -0
- package/dist/utils/zip-extractor.d.ts +1 -0
- package/package.json +108 -0
- package/src/companion/companion-manifest.json +12 -0
- package/src/skills/clonedeps/README.md +23 -0
- package/src/skills/clonedeps/SKILL.md +240 -0
- package/src/skills/clonedeps/codemap.md +32 -0
- package/src/skills/codemap/README.md +59 -0
- package/src/skills/codemap/SKILL.md +163 -0
- package/src/skills/codemap/codemap.md +36 -0
- package/src/skills/codemap/scripts/codemap.mjs +483 -0
- package/src/skills/codemap/scripts/codemap.test.ts +129 -0
- package/src/skills/codemap.md +78 -0
- package/src/skills/deepwork/SKILL.md +122 -0
- package/src/skills/loop-engineering/SKILL.md +30 -0
- package/src/skills/oh-my-opencode-slim/SKILL.md +326 -0
- package/src/skills/reflect/SKILL.md +326 -0
- package/src/skills/simplify/README.md +19 -0
- package/src/skills/simplify/SKILL.md +138 -0
- package/src/skills/simplify/codemap.md +36 -0
- package/src/skills/verification-planning/SKILL.md +102 -0
- package/src/skills/worktrees/SKILL.md +171 -0
|
@@ -0,0 +1,3565 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
import { createRequire } from "node:module";
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __returnValue = (v) => v;
|
|
6
|
+
function __exportSetter(name, newValue) {
|
|
7
|
+
this[name] = __returnValue.bind(null, newValue);
|
|
8
|
+
}
|
|
9
|
+
var __export = (target, all) => {
|
|
10
|
+
for (var name in all)
|
|
11
|
+
__defProp(target, name, {
|
|
12
|
+
get: all[name],
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true,
|
|
15
|
+
set: __exportSetter.bind(all, name)
|
|
16
|
+
});
|
|
17
|
+
};
|
|
18
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
19
|
+
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
20
|
+
|
|
21
|
+
// src/utils/compat.ts
|
|
22
|
+
import { spawn as nodeSpawn } from "node:child_process";
|
|
23
|
+
function collectStream(stream) {
|
|
24
|
+
if (!stream)
|
|
25
|
+
return () => Promise.resolve("");
|
|
26
|
+
const chunks = [];
|
|
27
|
+
stream.on("data", (chunk) => chunks.push(chunk));
|
|
28
|
+
return () => new Promise((resolve, reject) => {
|
|
29
|
+
if (!stream.readable) {
|
|
30
|
+
resolve(Buffer.concat(chunks).toString("utf-8"));
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
stream.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
|
|
34
|
+
stream.on("error", reject);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function crossSpawn(command, options) {
|
|
38
|
+
const [cmd, ...args] = command;
|
|
39
|
+
const proc = nodeSpawn(cmd, args, {
|
|
40
|
+
stdio: [
|
|
41
|
+
options?.stdin ?? "ignore",
|
|
42
|
+
options?.stdout ?? "pipe",
|
|
43
|
+
options?.stderr ?? "pipe"
|
|
44
|
+
],
|
|
45
|
+
cwd: options?.cwd,
|
|
46
|
+
env: options?.env
|
|
47
|
+
});
|
|
48
|
+
const stdoutCollector = collectStream(proc.stdout);
|
|
49
|
+
const stderrCollector = collectStream(proc.stderr);
|
|
50
|
+
const exited = new Promise((resolve, reject) => {
|
|
51
|
+
proc.on("error", reject);
|
|
52
|
+
proc.on("close", (code) => resolve(code ?? 1));
|
|
53
|
+
});
|
|
54
|
+
return {
|
|
55
|
+
proc,
|
|
56
|
+
stdout: stdoutCollector,
|
|
57
|
+
stderr: stderrCollector,
|
|
58
|
+
exited,
|
|
59
|
+
kill: (signal) => proc.kill(signal),
|
|
60
|
+
get exitCode() {
|
|
61
|
+
return proc.exitCode;
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
var init_compat = () => {};
|
|
66
|
+
|
|
67
|
+
// src/utils/zip-extractor.ts
|
|
68
|
+
var exports_zip_extractor = {};
|
|
69
|
+
__export(exports_zip_extractor, {
|
|
70
|
+
extractZip: () => extractZip
|
|
71
|
+
});
|
|
72
|
+
import { spawnSync } from "node:child_process";
|
|
73
|
+
import { release } from "node:os";
|
|
74
|
+
function getWindowsBuildNumber() {
|
|
75
|
+
if (process.platform !== "win32")
|
|
76
|
+
return null;
|
|
77
|
+
const parts = release().split(".");
|
|
78
|
+
if (parts.length >= 3) {
|
|
79
|
+
const build = parseInt(parts[2], 10);
|
|
80
|
+
if (!Number.isNaN(build))
|
|
81
|
+
return build;
|
|
82
|
+
}
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
function isPwshAvailable() {
|
|
86
|
+
if (process.platform !== "win32")
|
|
87
|
+
return false;
|
|
88
|
+
const result = spawnSync("where", ["pwsh"], {
|
|
89
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
90
|
+
});
|
|
91
|
+
return result.status === 0;
|
|
92
|
+
}
|
|
93
|
+
function escapePowerShellPath(path3) {
|
|
94
|
+
return path3.replace(/'/g, "''");
|
|
95
|
+
}
|
|
96
|
+
function getWindowsZipExtractor() {
|
|
97
|
+
const buildNumber = getWindowsBuildNumber();
|
|
98
|
+
if (buildNumber !== null && buildNumber >= WINDOWS_BUILD_WITH_TAR) {
|
|
99
|
+
return "tar";
|
|
100
|
+
}
|
|
101
|
+
if (isPwshAvailable()) {
|
|
102
|
+
return "pwsh";
|
|
103
|
+
}
|
|
104
|
+
return "powershell";
|
|
105
|
+
}
|
|
106
|
+
async function extractZip(archivePath, destDir) {
|
|
107
|
+
let proc;
|
|
108
|
+
if (process.platform === "win32") {
|
|
109
|
+
const extractor = getWindowsZipExtractor();
|
|
110
|
+
switch (extractor) {
|
|
111
|
+
case "tar":
|
|
112
|
+
proc = crossSpawn(["tar", "-xf", archivePath, "-C", destDir], {
|
|
113
|
+
stdout: "ignore",
|
|
114
|
+
stderr: "pipe"
|
|
115
|
+
});
|
|
116
|
+
break;
|
|
117
|
+
case "pwsh":
|
|
118
|
+
proc = crossSpawn([
|
|
119
|
+
"pwsh",
|
|
120
|
+
"-Command",
|
|
121
|
+
`Expand-Archive -Path '${escapePowerShellPath(archivePath)}' -DestinationPath '${escapePowerShellPath(destDir)}' -Force`
|
|
122
|
+
], {
|
|
123
|
+
stdout: "ignore",
|
|
124
|
+
stderr: "pipe"
|
|
125
|
+
});
|
|
126
|
+
break;
|
|
127
|
+
default:
|
|
128
|
+
proc = crossSpawn([
|
|
129
|
+
"powershell",
|
|
130
|
+
"-Command",
|
|
131
|
+
`Expand-Archive -Path '${escapePowerShellPath(archivePath)}' -DestinationPath '${escapePowerShellPath(destDir)}' -Force`
|
|
132
|
+
], {
|
|
133
|
+
stdout: "ignore",
|
|
134
|
+
stderr: "pipe"
|
|
135
|
+
});
|
|
136
|
+
break;
|
|
137
|
+
}
|
|
138
|
+
} else {
|
|
139
|
+
proc = crossSpawn(["unzip", "-o", archivePath, "-d", destDir], {
|
|
140
|
+
stdout: "ignore",
|
|
141
|
+
stderr: "pipe"
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
const exitCode = await proc.exited;
|
|
145
|
+
if (exitCode !== 0) {
|
|
146
|
+
const stderr = await proc.stderr();
|
|
147
|
+
throw new Error(`zip extraction failed (exit ${exitCode}): ${stderr}`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
var WINDOWS_BUILD_WITH_TAR = 17134;
|
|
151
|
+
var init_zip_extractor = __esm(() => {
|
|
152
|
+
init_compat();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
// src/cli/doctor.ts
|
|
156
|
+
import * as fs2 from "node:fs";
|
|
157
|
+
import { z as z3 } from "zod";
|
|
158
|
+
|
|
159
|
+
// src/config/loader.ts
|
|
160
|
+
import * as fs from "node:fs";
|
|
161
|
+
import * as path from "node:path";
|
|
162
|
+
|
|
163
|
+
// src/cli/config-io.ts
|
|
164
|
+
init_compat();
|
|
165
|
+
import {
|
|
166
|
+
copyFileSync,
|
|
167
|
+
existsSync as existsSync2,
|
|
168
|
+
mkdirSync as mkdirSync2,
|
|
169
|
+
readFileSync,
|
|
170
|
+
renameSync,
|
|
171
|
+
rmSync,
|
|
172
|
+
statSync,
|
|
173
|
+
writeFileSync
|
|
174
|
+
} from "node:fs";
|
|
175
|
+
import { homedir as homedir2 } from "node:os";
|
|
176
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
177
|
+
|
|
178
|
+
// src/cli/paths.ts
|
|
179
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
180
|
+
import { homedir } from "node:os";
|
|
181
|
+
import { dirname, join } from "node:path";
|
|
182
|
+
function getDefaultOpenCodeConfigDir() {
|
|
183
|
+
const userConfigDir = process.env.XDG_CONFIG_HOME ? process.env.XDG_CONFIG_HOME : join(homedir(), ".config");
|
|
184
|
+
return join(userConfigDir, "opencode");
|
|
185
|
+
}
|
|
186
|
+
function getCustomOpenCodeConfigDir() {
|
|
187
|
+
const configDir = process.env.OPENCODE_CONFIG_DIR?.trim();
|
|
188
|
+
return configDir || undefined;
|
|
189
|
+
}
|
|
190
|
+
function getCustomTuiConfigPath() {
|
|
191
|
+
const configPath = process.env.OPENCODE_TUI_CONFIG?.trim();
|
|
192
|
+
return configPath || undefined;
|
|
193
|
+
}
|
|
194
|
+
function getConfigDir() {
|
|
195
|
+
const customConfigDir = getCustomOpenCodeConfigDir();
|
|
196
|
+
if (customConfigDir) {
|
|
197
|
+
return customConfigDir;
|
|
198
|
+
}
|
|
199
|
+
return getDefaultOpenCodeConfigDir();
|
|
200
|
+
}
|
|
201
|
+
function getConfigSearchDirs() {
|
|
202
|
+
const dirs = [getCustomOpenCodeConfigDir(), getDefaultOpenCodeConfigDir()];
|
|
203
|
+
return dirs.filter((dir, index) => {
|
|
204
|
+
return Boolean(dir) && dirs.indexOf(dir) === index;
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
function getOpenCodeConfigPaths() {
|
|
208
|
+
const configDir = getConfigDir();
|
|
209
|
+
return [join(configDir, "opencode.json"), join(configDir, "opencode.jsonc")];
|
|
210
|
+
}
|
|
211
|
+
function getConfigJson() {
|
|
212
|
+
return getOpenCodeConfigPaths()[0];
|
|
213
|
+
}
|
|
214
|
+
function getConfigJsonc() {
|
|
215
|
+
return getOpenCodeConfigPaths()[1];
|
|
216
|
+
}
|
|
217
|
+
function getLiteConfig() {
|
|
218
|
+
return join(getConfigDir(), "oh-my-opencode-slim.json");
|
|
219
|
+
}
|
|
220
|
+
function getLiteConfigJsonc() {
|
|
221
|
+
return join(getConfigDir(), "oh-my-opencode-slim.jsonc");
|
|
222
|
+
}
|
|
223
|
+
function getTuiConfig() {
|
|
224
|
+
const customConfigPath = getCustomTuiConfigPath();
|
|
225
|
+
if (customConfigPath)
|
|
226
|
+
return customConfigPath;
|
|
227
|
+
return join(getConfigDir(), "tui.json");
|
|
228
|
+
}
|
|
229
|
+
function getTuiConfigJsonc() {
|
|
230
|
+
return join(getConfigDir(), "tui.jsonc");
|
|
231
|
+
}
|
|
232
|
+
function getExistingLiteConfigPath() {
|
|
233
|
+
const jsonPath = getLiteConfig();
|
|
234
|
+
if (existsSync(jsonPath))
|
|
235
|
+
return jsonPath;
|
|
236
|
+
const jsoncPath = getLiteConfigJsonc();
|
|
237
|
+
if (existsSync(jsoncPath))
|
|
238
|
+
return jsoncPath;
|
|
239
|
+
return jsonPath;
|
|
240
|
+
}
|
|
241
|
+
function getExistingTuiConfigPath() {
|
|
242
|
+
const customConfigPath = getCustomTuiConfigPath();
|
|
243
|
+
if (customConfigPath)
|
|
244
|
+
return customConfigPath;
|
|
245
|
+
const jsonPath = join(getConfigDir(), "tui.json");
|
|
246
|
+
if (existsSync(jsonPath))
|
|
247
|
+
return jsonPath;
|
|
248
|
+
const jsoncPath = getTuiConfigJsonc();
|
|
249
|
+
if (existsSync(jsoncPath))
|
|
250
|
+
return jsoncPath;
|
|
251
|
+
return jsonPath;
|
|
252
|
+
}
|
|
253
|
+
function getExistingConfigPath() {
|
|
254
|
+
const jsonPath = getConfigJson();
|
|
255
|
+
if (existsSync(jsonPath))
|
|
256
|
+
return jsonPath;
|
|
257
|
+
const jsoncPath = getConfigJsonc();
|
|
258
|
+
if (existsSync(jsoncPath))
|
|
259
|
+
return jsoncPath;
|
|
260
|
+
return jsonPath;
|
|
261
|
+
}
|
|
262
|
+
function ensureConfigDir() {
|
|
263
|
+
const configDir = getConfigDir();
|
|
264
|
+
if (!existsSync(configDir)) {
|
|
265
|
+
mkdirSync(configDir, { recursive: true });
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
function ensureTuiConfigDir() {
|
|
269
|
+
const configDir = dirname(getTuiConfig());
|
|
270
|
+
if (!existsSync(configDir)) {
|
|
271
|
+
mkdirSync(configDir, { recursive: true });
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function ensureOpenCodeConfigDir() {
|
|
275
|
+
const configDir = dirname(getConfigJson());
|
|
276
|
+
if (!existsSync(configDir)) {
|
|
277
|
+
mkdirSync(configDir, { recursive: true });
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// src/config/constants.ts
|
|
282
|
+
var SUBAGENT_NAMES = [
|
|
283
|
+
"explorer",
|
|
284
|
+
"librarian",
|
|
285
|
+
"oracle",
|
|
286
|
+
"designer",
|
|
287
|
+
"fixer",
|
|
288
|
+
"observer",
|
|
289
|
+
"council",
|
|
290
|
+
"councillor"
|
|
291
|
+
];
|
|
292
|
+
var ALL_AGENT_NAMES = ["orchestrator", ...SUBAGENT_NAMES];
|
|
293
|
+
var PROTECTED_AGENTS = new Set(["orchestrator", "councillor"]);
|
|
294
|
+
var MAX_POLL_TIME_MS = 5 * 60 * 1000;
|
|
295
|
+
var PHASE_REMINDER_TEXT = `!IMPORTANT! Scheduler workflow: First choose the lightest workflow that fits the work. If direct execution is justified, complete it and verify proportionately. Otherwise: plan lanes/dependencies → dispatch background specialists → track task IDs → wait for hook-driven completion → reconcile terminal results → verify. Do not poll running jobs, consume running-job output, or advance dependent work. !END!`;
|
|
296
|
+
function formatSystemReminder(text) {
|
|
297
|
+
return `<system-reminder>
|
|
298
|
+
${text}
|
|
299
|
+
</system-reminder>`;
|
|
300
|
+
}
|
|
301
|
+
var PHASE_REMINDER = formatSystemReminder(PHASE_REMINDER_TEXT);
|
|
302
|
+
// src/config/council-schema.ts
|
|
303
|
+
import { z } from "zod";
|
|
304
|
+
|
|
305
|
+
// src/utils/councillor-models.ts
|
|
306
|
+
function normalizeCouncillorModels(model, fallbackVariant) {
|
|
307
|
+
const raw = Array.isArray(model) ? model : [model];
|
|
308
|
+
return raw.map((entry) => typeof entry === "string" ? { id: entry, variant: fallbackVariant } : { id: entry.id, variant: entry.variant ?? fallbackVariant });
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/config/council-schema.ts
|
|
312
|
+
var ModelIdSchema = z.string().regex(/^[^/\s]+\/[^\s]+$/, 'Expected provider/model format (e.g. "openai/gpt-5.6-luna")');
|
|
313
|
+
var CouncillorModelEntrySchema = z.object({
|
|
314
|
+
id: ModelIdSchema,
|
|
315
|
+
variant: z.string().optional()
|
|
316
|
+
});
|
|
317
|
+
var CouncillorModelSchema = z.union([
|
|
318
|
+
ModelIdSchema,
|
|
319
|
+
z.array(z.union([ModelIdSchema, CouncillorModelEntrySchema])).min(1)
|
|
320
|
+
]).describe('Model ID in provider/model format (e.g. "openai/gpt-5.6-luna"), or an ' + "ordered fallback chain (array of model IDs or { id, variant } entries) " + "tried in order until one responds.");
|
|
321
|
+
var CouncillorConfigSchema = z.object({
|
|
322
|
+
model: CouncillorModelSchema,
|
|
323
|
+
variant: z.string().optional(),
|
|
324
|
+
prompt: z.string().optional().describe("Optional role/guidance injected into the councillor user prompt")
|
|
325
|
+
}).transform((c) => {
|
|
326
|
+
const models = normalizeCouncillorModels(c.model, c.variant);
|
|
327
|
+
return {
|
|
328
|
+
model: models[0].id,
|
|
329
|
+
variant: c.variant,
|
|
330
|
+
prompt: c.prompt,
|
|
331
|
+
models
|
|
332
|
+
};
|
|
333
|
+
});
|
|
334
|
+
var CouncilPresetSchema = z.record(z.string(), z.record(z.string(), z.unknown())).transform((entries, ctx) => {
|
|
335
|
+
const councillors = {};
|
|
336
|
+
for (const [key, raw] of Object.entries(entries)) {
|
|
337
|
+
if (key === "master")
|
|
338
|
+
continue;
|
|
339
|
+
if (key === "councillors" && typeof raw === "object" && raw !== null) {
|
|
340
|
+
for (const [innerKey, innerRaw] of Object.entries(raw)) {
|
|
341
|
+
const innerParsed = CouncillorConfigSchema.safeParse(innerRaw);
|
|
342
|
+
if (!innerParsed.success) {
|
|
343
|
+
ctx.addIssue({
|
|
344
|
+
code: z.ZodIssueCode.custom,
|
|
345
|
+
message: `Invalid councillor "${innerKey}" (nested under legacy "councillors" key): ${innerParsed.error.issues.map((i) => i.message).join(", ")}`
|
|
346
|
+
});
|
|
347
|
+
return z.NEVER;
|
|
348
|
+
}
|
|
349
|
+
councillors[innerKey] = innerParsed.data;
|
|
350
|
+
}
|
|
351
|
+
continue;
|
|
352
|
+
}
|
|
353
|
+
const parsed = CouncillorConfigSchema.safeParse(raw);
|
|
354
|
+
if (!parsed.success) {
|
|
355
|
+
ctx.addIssue({
|
|
356
|
+
code: z.ZodIssueCode.custom,
|
|
357
|
+
message: `Invalid councillor "${key}": ${parsed.error.issues.map((i) => i.message).join(", ")}`
|
|
358
|
+
});
|
|
359
|
+
return z.NEVER;
|
|
360
|
+
}
|
|
361
|
+
councillors[key] = parsed.data;
|
|
362
|
+
}
|
|
363
|
+
return councillors;
|
|
364
|
+
});
|
|
365
|
+
var CouncillorExecutionModeSchema = z.enum(["parallel", "serial"]).default("parallel").describe('Execution mode for councillors. Use "serial" for single-model systems to avoid conflicts. ' + 'Use "parallel" for multi-model systems for faster execution.');
|
|
366
|
+
var CouncilConfigSchema = z.object({
|
|
367
|
+
presets: z.record(z.string(), CouncilPresetSchema),
|
|
368
|
+
timeout: z.number().min(0).default(180000),
|
|
369
|
+
default_preset: z.string().default("default"),
|
|
370
|
+
councillor_execution_mode: CouncillorExecutionModeSchema.describe('Execution mode for councillors. "serial" runs them one at a time (required for single-model systems). "parallel" runs them concurrently (default, faster for multi-model systems).'),
|
|
371
|
+
councillor_retries: z.number().int().min(0).max(5).default(3).describe("Number of retry attempts for councillors that return empty responses " + "(e.g. due to provider rate limiting). Default: 3 retries."),
|
|
372
|
+
master: z.unknown().optional().describe("DEPRECATED - ignored. Council agent synthesizes directly.")
|
|
373
|
+
}).transform((data) => {
|
|
374
|
+
const deprecated = [];
|
|
375
|
+
if (data.master !== undefined)
|
|
376
|
+
deprecated.push("master");
|
|
377
|
+
const legacyMasterModel = typeof data.master === "object" && data.master !== null && "model" in data.master && typeof data.master.model === "string" ? data.master.model : undefined;
|
|
378
|
+
return {
|
|
379
|
+
presets: data.presets,
|
|
380
|
+
timeout: data.timeout,
|
|
381
|
+
default_preset: data.default_preset,
|
|
382
|
+
councillor_execution_mode: data.councillor_execution_mode,
|
|
383
|
+
councillor_retries: data.councillor_retries,
|
|
384
|
+
_deprecated: deprecated.length > 0 ? deprecated : undefined,
|
|
385
|
+
_legacyMasterModel: legacyMasterModel
|
|
386
|
+
};
|
|
387
|
+
});
|
|
388
|
+
// src/config/schema.ts
|
|
389
|
+
import { z as z2 } from "zod";
|
|
390
|
+
var ProviderModelIdSchema = z2.string().regex(/^[^/\s]+\/[^\s]+$/, "Expected provider/model format (provider/.../model)");
|
|
391
|
+
var ManualAgentPlanSchema = z2.object({
|
|
392
|
+
primary: ProviderModelIdSchema,
|
|
393
|
+
fallback1: ProviderModelIdSchema,
|
|
394
|
+
fallback2: ProviderModelIdSchema,
|
|
395
|
+
fallback3: ProviderModelIdSchema
|
|
396
|
+
}).superRefine((value, ctx) => {
|
|
397
|
+
const unique = new Set([
|
|
398
|
+
value.primary,
|
|
399
|
+
value.fallback1,
|
|
400
|
+
value.fallback2,
|
|
401
|
+
value.fallback3
|
|
402
|
+
]);
|
|
403
|
+
if (unique.size !== 4) {
|
|
404
|
+
ctx.addIssue({
|
|
405
|
+
code: z2.ZodIssueCode.custom,
|
|
406
|
+
message: "primary and fallbacks must be unique per agent"
|
|
407
|
+
});
|
|
408
|
+
}
|
|
409
|
+
});
|
|
410
|
+
var ManualPlanSchema = z2.object({
|
|
411
|
+
orchestrator: ManualAgentPlanSchema,
|
|
412
|
+
oracle: ManualAgentPlanSchema,
|
|
413
|
+
designer: ManualAgentPlanSchema,
|
|
414
|
+
explorer: ManualAgentPlanSchema,
|
|
415
|
+
librarian: ManualAgentPlanSchema,
|
|
416
|
+
fixer: ManualAgentPlanSchema
|
|
417
|
+
}).strict();
|
|
418
|
+
var PermissionActionSchema = z2.enum(["ask", "allow", "deny"]);
|
|
419
|
+
var PermissionRuleSchema = z2.union([
|
|
420
|
+
PermissionActionSchema,
|
|
421
|
+
z2.record(z2.string(), PermissionActionSchema)
|
|
422
|
+
]);
|
|
423
|
+
var PermissionObjectSchema = z2.object({
|
|
424
|
+
read: PermissionRuleSchema.optional(),
|
|
425
|
+
edit: PermissionRuleSchema.optional(),
|
|
426
|
+
glob: PermissionRuleSchema.optional(),
|
|
427
|
+
grep: PermissionRuleSchema.optional(),
|
|
428
|
+
list: PermissionRuleSchema.optional(),
|
|
429
|
+
bash: PermissionRuleSchema.optional(),
|
|
430
|
+
task: PermissionRuleSchema.optional(),
|
|
431
|
+
external_directory: PermissionRuleSchema.optional(),
|
|
432
|
+
lsp: PermissionRuleSchema.optional(),
|
|
433
|
+
skill: PermissionRuleSchema.optional(),
|
|
434
|
+
todowrite: PermissionActionSchema.optional(),
|
|
435
|
+
question: PermissionActionSchema.optional(),
|
|
436
|
+
webfetch: PermissionActionSchema.optional(),
|
|
437
|
+
websearch: PermissionActionSchema.optional(),
|
|
438
|
+
codesearch: PermissionActionSchema.optional(),
|
|
439
|
+
doom_loop: PermissionActionSchema.optional()
|
|
440
|
+
}).catchall(PermissionRuleSchema);
|
|
441
|
+
var PermissionConfigSchema = z2.union([
|
|
442
|
+
PermissionActionSchema,
|
|
443
|
+
PermissionObjectSchema
|
|
444
|
+
]);
|
|
445
|
+
var AgentOverrideConfigSchema = z2.object({
|
|
446
|
+
model: z2.union([
|
|
447
|
+
z2.string(),
|
|
448
|
+
z2.array(z2.union([
|
|
449
|
+
z2.string(),
|
|
450
|
+
z2.object({
|
|
451
|
+
id: z2.string(),
|
|
452
|
+
variant: z2.string().optional()
|
|
453
|
+
})
|
|
454
|
+
])).min(1)
|
|
455
|
+
]).optional(),
|
|
456
|
+
temperature: z2.number().min(0).max(2).optional(),
|
|
457
|
+
variant: z2.string().optional().catch(undefined),
|
|
458
|
+
skills: z2.array(z2.string()).optional(),
|
|
459
|
+
mcps: z2.array(z2.string()).optional(),
|
|
460
|
+
prompt: z2.string().min(1).optional(),
|
|
461
|
+
orchestratorPrompt: z2.string().min(1).optional(),
|
|
462
|
+
options: z2.record(z2.string(), z2.unknown()).optional(),
|
|
463
|
+
displayName: z2.string().min(1).optional(),
|
|
464
|
+
permission: PermissionConfigSchema.optional()
|
|
465
|
+
}).strict();
|
|
466
|
+
var MultiplexerTypeSchema = z2.enum([
|
|
467
|
+
"auto",
|
|
468
|
+
"tmux",
|
|
469
|
+
"zellij",
|
|
470
|
+
"herdr",
|
|
471
|
+
"cmux",
|
|
472
|
+
"none"
|
|
473
|
+
]);
|
|
474
|
+
var MultiplexerLayoutSchema = z2.enum([
|
|
475
|
+
"main-horizontal",
|
|
476
|
+
"main-vertical",
|
|
477
|
+
"tiled",
|
|
478
|
+
"even-horizontal",
|
|
479
|
+
"even-vertical"
|
|
480
|
+
]);
|
|
481
|
+
var ZellijPaneModeSchema = z2.enum(["agent-tab", "current-tab"]);
|
|
482
|
+
var TmuxLayoutSchema = MultiplexerLayoutSchema;
|
|
483
|
+
var MultiplexerConfigSchema = z2.object({
|
|
484
|
+
type: MultiplexerTypeSchema.default("none"),
|
|
485
|
+
layout: MultiplexerLayoutSchema.default("main-vertical"),
|
|
486
|
+
main_pane_size: z2.number().min(20).max(80).default(60),
|
|
487
|
+
zellij_pane_mode: ZellijPaneModeSchema.default("agent-tab")
|
|
488
|
+
});
|
|
489
|
+
var TmuxConfigSchema = z2.object({
|
|
490
|
+
enabled: z2.boolean().default(false),
|
|
491
|
+
layout: TmuxLayoutSchema.default("main-vertical"),
|
|
492
|
+
main_pane_size: z2.number().min(20).max(80).default(60)
|
|
493
|
+
});
|
|
494
|
+
var PresetSchema = z2.record(z2.string(), AgentOverrideConfigSchema);
|
|
495
|
+
var WebsearchConfigSchema = z2.object({
|
|
496
|
+
provider: z2.enum(["exa", "tavily"]).default("exa")
|
|
497
|
+
});
|
|
498
|
+
var McpNameSchema = z2.enum(["websearch", "context7", "gh_grep"]);
|
|
499
|
+
var InterviewConfigSchema = z2.object({
|
|
500
|
+
maxQuestions: z2.number().int().min(1).max(10).default(2),
|
|
501
|
+
outputFolder: z2.string().min(1).default("interview"),
|
|
502
|
+
autoOpenBrowser: z2.boolean().default(true).describe("Automatically open the interview UI in your default browser during interactive runs. Disabled automatically in tests and CI."),
|
|
503
|
+
port: z2.number().int().min(0).max(65535).default(0),
|
|
504
|
+
dashboard: z2.boolean().default(false)
|
|
505
|
+
});
|
|
506
|
+
var BackgroundJobsConfigSchema = z2.object({
|
|
507
|
+
maxSessionsPerAgent: z2.number().int().min(1).max(10).default(2),
|
|
508
|
+
readContextMinLines: z2.number().int().min(0).max(1000).default(10),
|
|
509
|
+
readContextMaxFiles: z2.number().int().min(0).max(50).default(8)
|
|
510
|
+
});
|
|
511
|
+
var FailoverConfigSchema = z2.object({
|
|
512
|
+
enabled: z2.boolean().default(true),
|
|
513
|
+
timeoutMs: z2.number().min(0).default(15000),
|
|
514
|
+
retryDelayMs: z2.number().min(0).default(500),
|
|
515
|
+
maxRetries: z2.number().int().min(0).default(3).describe("Number of consecutive 429/rate-limit responses tolerated on the " + "same model before aborting (or swapping to the next fallback " + "model when a chain is configured)."),
|
|
516
|
+
retry_on_empty: z2.boolean().default(true).describe("When true (default), empty provider responses are treated as failures, " + "triggering fallback/retry. Set to false to treat them as successes."),
|
|
517
|
+
runtimeOverride: z2.boolean().optional().describe("DEPRECATED: no longer used. Previously controlled whether out-of-chain " + "runtime model picks triggered fallback. Fallback is now always " + "disabled when a user explicitly selects a model via /model.")
|
|
518
|
+
}).strict();
|
|
519
|
+
var CompanionConfigSchema = z2.object({
|
|
520
|
+
enabled: z2.boolean().optional(),
|
|
521
|
+
binaryPath: z2.string().min(1).optional().describe("Path to a custom companion binary to launch."),
|
|
522
|
+
position: z2.enum(["bottom-right", "bottom-left", "top-right", "top-left"]).optional(),
|
|
523
|
+
size: z2.enum(["small", "medium", "large"]).optional(),
|
|
524
|
+
gifPack: z2.enum(["default"]).optional().describe("Bundled companion animation pack to use."),
|
|
525
|
+
loopStyle: z2.enum(["classic", "smooth"]).optional().describe("Companion animation playback style: classic loops or smooth ping-pong playback."),
|
|
526
|
+
speed: z2.number().min(0.25).max(4).optional().describe("Companion animation playback speed multiplier. Defaults to 1."),
|
|
527
|
+
debug: z2.boolean().optional().describe("Enable verbose native companion debug logs.")
|
|
528
|
+
});
|
|
529
|
+
var AcpAgentPermissionModeSchema = z2.enum(["ask", "allow", "reject"]);
|
|
530
|
+
var MAX_ACP_TIMEOUT_MS = 2147483647;
|
|
531
|
+
var AcpAgentConfigSchema = z2.object({
|
|
532
|
+
command: z2.string().min(1),
|
|
533
|
+
args: z2.array(z2.string()).default([]),
|
|
534
|
+
env: z2.record(z2.string(), z2.string()).default({}),
|
|
535
|
+
cwd: z2.string().min(1).optional(),
|
|
536
|
+
description: z2.string().min(1).optional(),
|
|
537
|
+
prompt: z2.string().min(1).optional(),
|
|
538
|
+
orchestratorPrompt: z2.string().min(1).optional(),
|
|
539
|
+
wrapperModel: ProviderModelIdSchema.optional(),
|
|
540
|
+
timeoutMs: z2.number().int().min(0).max(MAX_ACP_TIMEOUT_MS).default(0).describe("Timeout for a single ACP run in milliseconds. Set to 0 to disable the timeout."),
|
|
541
|
+
permissionMode: AcpAgentPermissionModeSchema.default("ask")
|
|
542
|
+
}).strict();
|
|
543
|
+
var AcpAgentsConfigSchema = z2.record(z2.string(), AcpAgentConfigSchema);
|
|
544
|
+
function rejectOrchestratorPromptOnOrchestrator(overrides, ctx, pathPrefix) {
|
|
545
|
+
for (const [name, override] of Object.entries(overrides)) {
|
|
546
|
+
if (name === "orchestrator" && override.orchestratorPrompt !== undefined) {
|
|
547
|
+
ctx.addIssue({
|
|
548
|
+
code: z2.ZodIssueCode.custom,
|
|
549
|
+
path: [...pathPrefix, name, "orchestratorPrompt"],
|
|
550
|
+
message: "orchestratorPrompt is not supported for the orchestrator agent"
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
var PluginConfigSchema = z2.object({
|
|
556
|
+
preset: z2.string().optional(),
|
|
557
|
+
setDefaultAgent: z2.boolean().optional(),
|
|
558
|
+
compactSidebar: z2.boolean().optional().describe("Use the compact TUI sidebar layout. Defaults to true; set false to use the expanded layout."),
|
|
559
|
+
stripOrchestratorModel: z2.boolean().optional().describe("When true, omit orchestrator.model and orchestrator.variant from the SDK config so OpenCode uses the session model selected with /model after subagent dispatch. An explicitly selected preset that sets orchestrator.model is preserved. Defaults to false."),
|
|
560
|
+
autoUpdate: z2.boolean().optional().describe("Disable automatic installation of plugin updates when false. Defaults to true."),
|
|
561
|
+
presets: z2.record(z2.string(), PresetSchema).optional(),
|
|
562
|
+
agents: z2.record(z2.string(), AgentOverrideConfigSchema).optional(),
|
|
563
|
+
disabled_agents: z2.array(z2.string()).optional().describe("Agent names to disable completely. " + "Disabled agents are not instantiated and cannot be delegated to. " + "Orchestrator and council internal agents (councillor) cannot be disabled. " + "By default, 'observer' is disabled. Remove it from this list and configure a vision-capable model to enable."),
|
|
564
|
+
image_routing: z2.enum(["auto", "direct"]).optional().describe("How image attachments are handled. " + "When omitted, preserves legacy conditional behavior: intercept " + 'attachments only when observer is enabled. "auto": requires ' + "observer to be enabled and saves attachments to disk before " + 'nudging delegation to @observer. "direct": always passes ' + "attachments to the orchestrator untouched."),
|
|
565
|
+
disabled_mcps: z2.array(z2.string()).optional().describe("MCP server names to disable completely. Disabled servers are not " + "started and cannot be used by agents."),
|
|
566
|
+
disabled_tools: z2.array(z2.string()).optional().describe("Tool names to disable completely. Disabled tools are not registered with OpenCode and cannot be used by agents."),
|
|
567
|
+
disabled_skills: z2.array(z2.string()).optional().describe("Skill names to disable completely. Disabled skills are not granted to agents, even when referenced by presets or agent overrides."),
|
|
568
|
+
multiplexer: MultiplexerConfigSchema.optional(),
|
|
569
|
+
tmux: TmuxConfigSchema.optional(),
|
|
570
|
+
websearch: WebsearchConfigSchema.optional(),
|
|
571
|
+
interview: InterviewConfigSchema.optional(),
|
|
572
|
+
backgroundJobs: BackgroundJobsConfigSchema.optional(),
|
|
573
|
+
fallback: FailoverConfigSchema.optional(),
|
|
574
|
+
council: CouncilConfigSchema.optional(),
|
|
575
|
+
companion: CompanionConfigSchema.optional(),
|
|
576
|
+
acpAgents: AcpAgentsConfigSchema.optional()
|
|
577
|
+
}).superRefine((value, ctx) => {
|
|
578
|
+
if (value.agents) {
|
|
579
|
+
rejectOrchestratorPromptOnOrchestrator(value.agents, ctx, ["agents"]);
|
|
580
|
+
}
|
|
581
|
+
if (value.presets) {
|
|
582
|
+
for (const [presetName, preset] of Object.entries(value.presets)) {
|
|
583
|
+
rejectOrchestratorPromptOnOrchestrator(preset, ctx, [
|
|
584
|
+
"presets",
|
|
585
|
+
presetName
|
|
586
|
+
]);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
});
|
|
590
|
+
// src/config/agent-mcps.ts
|
|
591
|
+
var DEFAULT_AGENT_MCPS = {
|
|
592
|
+
orchestrator: ["*", "!context7"],
|
|
593
|
+
designer: [],
|
|
594
|
+
oracle: [],
|
|
595
|
+
librarian: ["websearch", "context7", "gh_grep"],
|
|
596
|
+
explorer: [],
|
|
597
|
+
fixer: [],
|
|
598
|
+
observer: [],
|
|
599
|
+
council: [],
|
|
600
|
+
councillor: []
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
// src/cli/custom-skills-registry.ts
|
|
604
|
+
var CUSTOM_SKILLS = [
|
|
605
|
+
{
|
|
606
|
+
name: "simplify",
|
|
607
|
+
description: "Code simplification and readability-focused refactoring",
|
|
608
|
+
allowedAgents: ["oracle"],
|
|
609
|
+
sourcePath: "src/skills/simplify"
|
|
610
|
+
},
|
|
611
|
+
{
|
|
612
|
+
name: "codemap",
|
|
613
|
+
description: "Repository understanding and hierarchical codemap generation",
|
|
614
|
+
allowedAgents: ["orchestrator"],
|
|
615
|
+
sourcePath: "src/skills/codemap"
|
|
616
|
+
},
|
|
617
|
+
{
|
|
618
|
+
name: "clonedeps",
|
|
619
|
+
description: "Clone important dependency source for local inspection",
|
|
620
|
+
allowedAgents: ["orchestrator"],
|
|
621
|
+
sourcePath: "src/skills/clonedeps"
|
|
622
|
+
},
|
|
623
|
+
{
|
|
624
|
+
name: "deepwork",
|
|
625
|
+
description: "Heavy/complex coding sessions and large modifications workflow",
|
|
626
|
+
allowedAgents: ["orchestrator"],
|
|
627
|
+
sourcePath: "src/skills/deepwork"
|
|
628
|
+
},
|
|
629
|
+
{
|
|
630
|
+
name: "verification-planning",
|
|
631
|
+
description: "Plan credible, proportionate evidence before non-trivial implementation",
|
|
632
|
+
allowedAgents: ["orchestrator"],
|
|
633
|
+
sourcePath: "src/skills/verification-planning"
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
name: "reflect",
|
|
637
|
+
description: "Review repeated work and suggest reusable workflow improvements",
|
|
638
|
+
allowedAgents: ["orchestrator"],
|
|
639
|
+
sourcePath: "src/skills/reflect"
|
|
640
|
+
},
|
|
641
|
+
{
|
|
642
|
+
name: "oh-my-opencode-slim",
|
|
643
|
+
description: "Configure, customize, and safely improve oh-my-opencode-slim setups",
|
|
644
|
+
allowedAgents: ["orchestrator"],
|
|
645
|
+
sourcePath: "src/skills/oh-my-opencode-slim"
|
|
646
|
+
},
|
|
647
|
+
{
|
|
648
|
+
name: "worktrees",
|
|
649
|
+
description: "Manage Git worktrees as OMO safe isolated coding lanes for complex/risky/parallel work",
|
|
650
|
+
allowedAgents: ["orchestrator"],
|
|
651
|
+
sourcePath: "src/skills/worktrees"
|
|
652
|
+
}
|
|
653
|
+
];
|
|
654
|
+
|
|
655
|
+
// src/cli/providers.ts
|
|
656
|
+
var SCHEMA_URL = "https://unpkg.com/oh-my-opencode-slim@latest/oh-my-opencode-serverlocal.schema.json";
|
|
657
|
+
var GENERATED_PRESETS = ["openai", "opencode-go"];
|
|
658
|
+
var MODEL_MAPPINGS = {
|
|
659
|
+
openai: {
|
|
660
|
+
orchestrator: { model: "openai/gpt-5.6-terra", variant: "medium" },
|
|
661
|
+
oracle: { model: "openai/gpt-5.6-sol", variant: "high" },
|
|
662
|
+
librarian: { model: "openai/gpt-5.6-luna", variant: "low" },
|
|
663
|
+
explorer: { model: "openai/gpt-5.6-luna", variant: "low" },
|
|
664
|
+
designer: { model: "openai/gpt-5.6-luna", variant: "medium" },
|
|
665
|
+
fixer: { model: "openai/gpt-5.6-luna", variant: "medium" }
|
|
666
|
+
},
|
|
667
|
+
kimi: {
|
|
668
|
+
orchestrator: { model: "kimi-for-coding/k2p5", variant: "max" },
|
|
669
|
+
oracle: { model: "kimi-for-coding/k2p5", variant: "high" },
|
|
670
|
+
librarian: { model: "kimi-for-coding/k2p5", variant: "low" },
|
|
671
|
+
explorer: { model: "kimi-for-coding/k2p5", variant: "low" },
|
|
672
|
+
designer: { model: "kimi-for-coding/k2p5", variant: "medium" },
|
|
673
|
+
fixer: { model: "kimi-for-coding/k2p5", variant: "low" }
|
|
674
|
+
},
|
|
675
|
+
copilot: {
|
|
676
|
+
orchestrator: { model: "github-copilot/claude-opus-4.6", variant: "max" },
|
|
677
|
+
oracle: { model: "github-copilot/claude-opus-4.6", variant: "high" },
|
|
678
|
+
librarian: { model: "github-copilot/grok-code-fast-1", variant: "low" },
|
|
679
|
+
explorer: { model: "github-copilot/grok-code-fast-1", variant: "low" },
|
|
680
|
+
designer: {
|
|
681
|
+
model: "github-copilot/gemini-3.1-pro-preview",
|
|
682
|
+
variant: "medium"
|
|
683
|
+
},
|
|
684
|
+
fixer: { model: "github-copilot/claude-sonnet-4.6", variant: "low" }
|
|
685
|
+
},
|
|
686
|
+
"zai-plan": {
|
|
687
|
+
orchestrator: { model: "zai-coding-plan/glm-5", variant: "max" },
|
|
688
|
+
oracle: { model: "zai-coding-plan/glm-5", variant: "high" },
|
|
689
|
+
librarian: { model: "zai-coding-plan/glm-5", variant: "low" },
|
|
690
|
+
explorer: { model: "zai-coding-plan/glm-5", variant: "low" },
|
|
691
|
+
designer: { model: "zai-coding-plan/glm-5", variant: "medium" },
|
|
692
|
+
fixer: { model: "zai-coding-plan/glm-5", variant: "low" }
|
|
693
|
+
},
|
|
694
|
+
"opencode-go": {
|
|
695
|
+
orchestrator: { model: "opencode-go/minimax-m3", variant: "max" },
|
|
696
|
+
oracle: { model: "opencode-go/qwen3.7-max", variant: "max" },
|
|
697
|
+
explorer: { model: "opencode-go/deepseek-v4-flash", variant: "max" },
|
|
698
|
+
librarian: { model: "opencode-go/deepseek-v4-flash", variant: "high" },
|
|
699
|
+
designer: { model: "opencode-go/kimi-k2.7-code", variant: "medium" },
|
|
700
|
+
fixer: { model: "opencode-go/deepseek-v4-flash", variant: "high" },
|
|
701
|
+
observer: { model: "opencode-go/mimo-v2.5", variant: "max" }
|
|
702
|
+
}
|
|
703
|
+
};
|
|
704
|
+
function isGeneratedPresetName(value) {
|
|
705
|
+
return GENERATED_PRESETS.includes(value);
|
|
706
|
+
}
|
|
707
|
+
function getGeneratedPresetNames() {
|
|
708
|
+
return [...GENERATED_PRESETS];
|
|
709
|
+
}
|
|
710
|
+
function generateLiteConfig(installConfig) {
|
|
711
|
+
const preset = installConfig.preset ?? "openai";
|
|
712
|
+
if (!isGeneratedPresetName(preset)) {
|
|
713
|
+
throw new Error(`Unsupported preset "${preset}". Available generated presets: ${getGeneratedPresetNames().join(", ")}`);
|
|
714
|
+
}
|
|
715
|
+
const config = {
|
|
716
|
+
$schema: SCHEMA_URL,
|
|
717
|
+
preset,
|
|
718
|
+
presets: {}
|
|
719
|
+
};
|
|
720
|
+
if (preset === "opencode-go") {
|
|
721
|
+
config.disabled_agents = [];
|
|
722
|
+
}
|
|
723
|
+
const createAgentConfig = (agentName, modelInfo) => {
|
|
724
|
+
const isOrchestrator = agentName === "orchestrator";
|
|
725
|
+
const skills = isOrchestrator ? ["*"] : [
|
|
726
|
+
...CUSTOM_SKILLS.filter((s) => s.allowedAgents.includes("*") || s.allowedAgents.includes(agentName)).map((s) => s.name)
|
|
727
|
+
];
|
|
728
|
+
return {
|
|
729
|
+
model: modelInfo.model,
|
|
730
|
+
variant: modelInfo.variant,
|
|
731
|
+
skills,
|
|
732
|
+
mcps: DEFAULT_AGENT_MCPS[agentName] ?? []
|
|
733
|
+
};
|
|
734
|
+
};
|
|
735
|
+
const buildPreset = (mappingName) => {
|
|
736
|
+
const mapping = MODEL_MAPPINGS[mappingName];
|
|
737
|
+
return Object.fromEntries(Object.entries(mapping).map(([agentName, modelInfo]) => [
|
|
738
|
+
agentName,
|
|
739
|
+
createAgentConfig(agentName, modelInfo)
|
|
740
|
+
]));
|
|
741
|
+
};
|
|
742
|
+
const presets = config.presets;
|
|
743
|
+
for (const presetName of GENERATED_PRESETS) {
|
|
744
|
+
presets[presetName] = buildPreset(presetName);
|
|
745
|
+
}
|
|
746
|
+
if (installConfig.hasTmux) {
|
|
747
|
+
config.tmux = {
|
|
748
|
+
enabled: true,
|
|
749
|
+
layout: "main-vertical",
|
|
750
|
+
main_pane_size: 60
|
|
751
|
+
};
|
|
752
|
+
}
|
|
753
|
+
if (installConfig.companion === "yes") {
|
|
754
|
+
config.companion = {
|
|
755
|
+
enabled: true,
|
|
756
|
+
position: "bottom-right",
|
|
757
|
+
size: "medium",
|
|
758
|
+
gifPack: "default",
|
|
759
|
+
loopStyle: "classic",
|
|
760
|
+
speed: 1,
|
|
761
|
+
debug: false
|
|
762
|
+
};
|
|
763
|
+
}
|
|
764
|
+
return config;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// src/cli/config-io.ts
|
|
768
|
+
var PACKAGE_NAME = "oh-my-opencode-slim";
|
|
769
|
+
var DEFAULT_OPENCODE_AGENTS_TO_DISABLE = ["explore", "general"];
|
|
770
|
+
function isString(value) {
|
|
771
|
+
return typeof value === "string";
|
|
772
|
+
}
|
|
773
|
+
function getModelIds(model) {
|
|
774
|
+
if (isString(model))
|
|
775
|
+
return [model];
|
|
776
|
+
if (!Array.isArray(model))
|
|
777
|
+
return [];
|
|
778
|
+
return model.flatMap((entry) => {
|
|
779
|
+
if (isString(entry))
|
|
780
|
+
return [entry];
|
|
781
|
+
if (entry && typeof entry === "object" && isString(entry.id)) {
|
|
782
|
+
return [entry.id];
|
|
783
|
+
}
|
|
784
|
+
return [];
|
|
785
|
+
});
|
|
786
|
+
}
|
|
787
|
+
function getPlugins(config) {
|
|
788
|
+
return Array.isArray(config.plugin) ? config.plugin : [];
|
|
789
|
+
}
|
|
790
|
+
function getPluginEntries(config) {
|
|
791
|
+
return getPlugins(config).filter(isString);
|
|
792
|
+
}
|
|
793
|
+
function getPluginSpec(entry) {
|
|
794
|
+
if (isString(entry))
|
|
795
|
+
return entry;
|
|
796
|
+
if (!Array.isArray(entry))
|
|
797
|
+
return;
|
|
798
|
+
const spec = entry[0];
|
|
799
|
+
return isString(spec) ? spec : undefined;
|
|
800
|
+
}
|
|
801
|
+
function normalizePathForMatch(path) {
|
|
802
|
+
return path.replaceAll("\\", "/");
|
|
803
|
+
}
|
|
804
|
+
function findPackageRoot(startPath) {
|
|
805
|
+
let currentPath = dirname2(startPath);
|
|
806
|
+
while (true) {
|
|
807
|
+
const packageJsonPath = join2(currentPath, "package.json");
|
|
808
|
+
if (existsSync2(packageJsonPath)) {
|
|
809
|
+
try {
|
|
810
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
811
|
+
if (packageJson.name === PACKAGE_NAME) {
|
|
812
|
+
return currentPath;
|
|
813
|
+
}
|
|
814
|
+
} catch {}
|
|
815
|
+
}
|
|
816
|
+
const parentPath = dirname2(currentPath);
|
|
817
|
+
if (parentPath === currentPath) {
|
|
818
|
+
return null;
|
|
819
|
+
}
|
|
820
|
+
currentPath = parentPath;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
function isLocalPackageRootEntry(entry) {
|
|
824
|
+
if (!entry || entry.startsWith("file://")) {
|
|
825
|
+
return false;
|
|
826
|
+
}
|
|
827
|
+
const packageJsonPath = join2(entry, "package.json");
|
|
828
|
+
if (!existsSync2(packageJsonPath)) {
|
|
829
|
+
return false;
|
|
830
|
+
}
|
|
831
|
+
try {
|
|
832
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
833
|
+
return packageJson.name === PACKAGE_NAME;
|
|
834
|
+
} catch {
|
|
835
|
+
return false;
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
function isPackageManagerInstall(path) {
|
|
839
|
+
const normalizedPath = normalizePathForMatch(path);
|
|
840
|
+
return normalizedPath.includes(`/node_modules/${PACKAGE_NAME}`);
|
|
841
|
+
}
|
|
842
|
+
function isPluginEntry(entry) {
|
|
843
|
+
return entry === PACKAGE_NAME || entry.startsWith(`${PACKAGE_NAME}@`) || entry.startsWith("file://") && entry.includes(PACKAGE_NAME) || isLocalPackageRootEntry(entry);
|
|
844
|
+
}
|
|
845
|
+
function isMatchingPluginEntry(entry) {
|
|
846
|
+
const spec = getPluginSpec(entry);
|
|
847
|
+
return spec ? isPluginEntry(spec) : false;
|
|
848
|
+
}
|
|
849
|
+
function getPluginEntry() {
|
|
850
|
+
const cliEntryPath = process.argv[1];
|
|
851
|
+
if (!cliEntryPath) {
|
|
852
|
+
return PACKAGE_NAME;
|
|
853
|
+
}
|
|
854
|
+
try {
|
|
855
|
+
const packageRoot = findPackageRoot(cliEntryPath);
|
|
856
|
+
if (!packageRoot || isPackageManagerInstall(packageRoot)) {
|
|
857
|
+
return PACKAGE_NAME;
|
|
858
|
+
}
|
|
859
|
+
return packageRoot;
|
|
860
|
+
} catch {
|
|
861
|
+
return PACKAGE_NAME;
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
function getPinnedVersionFromConfig() {
|
|
865
|
+
try {
|
|
866
|
+
const { config } = parseConfig(getExistingConfigPath());
|
|
867
|
+
if (!config)
|
|
868
|
+
return;
|
|
869
|
+
for (const entry of getPlugins(config)) {
|
|
870
|
+
const spec = getPluginSpec(entry);
|
|
871
|
+
if (!spec)
|
|
872
|
+
continue;
|
|
873
|
+
if (spec === PACKAGE_NAME)
|
|
874
|
+
return;
|
|
875
|
+
if (spec.startsWith(`${PACKAGE_NAME}@`)) {
|
|
876
|
+
const version = spec.slice(PACKAGE_NAME.length + 1);
|
|
877
|
+
if (version && version !== "latest")
|
|
878
|
+
return version;
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
} catch {}
|
|
882
|
+
return;
|
|
883
|
+
}
|
|
884
|
+
function getRequestedPackageTag(packageRoot) {
|
|
885
|
+
const normalizedPath = normalizePathForMatch(packageRoot);
|
|
886
|
+
const marker = `/bunx-`;
|
|
887
|
+
const markerIndex = normalizedPath.lastIndexOf(marker);
|
|
888
|
+
if (markerIndex === -1)
|
|
889
|
+
return;
|
|
890
|
+
const bunxSegment = normalizedPath.slice(markerIndex + marker.length).split("/")[0];
|
|
891
|
+
const packagePrefix = `${PACKAGE_NAME}@`;
|
|
892
|
+
const packageIndex = bunxSegment.lastIndexOf(packagePrefix);
|
|
893
|
+
if (packageIndex === -1)
|
|
894
|
+
return;
|
|
895
|
+
const tag = bunxSegment.slice(packageIndex + packagePrefix.length);
|
|
896
|
+
return tag || undefined;
|
|
897
|
+
}
|
|
898
|
+
function getVersionFromPackageRoot(packageRoot) {
|
|
899
|
+
try {
|
|
900
|
+
const packageJsonPath = join2(packageRoot, "package.json");
|
|
901
|
+
if (!existsSync2(packageJsonPath))
|
|
902
|
+
return;
|
|
903
|
+
const pkg = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
|
|
904
|
+
return pkg.version;
|
|
905
|
+
} catch {
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
function getOpenCodePluginCacheDir(version) {
|
|
910
|
+
const cacheDir = process.env.XDG_CACHE_HOME?.trim() || join2(homedir2(), ".cache");
|
|
911
|
+
const suffix = version ? `${PACKAGE_NAME}@${version}` : `${PACKAGE_NAME}@latest`;
|
|
912
|
+
return join2(cacheDir, "opencode", "packages", suffix);
|
|
913
|
+
}
|
|
914
|
+
function writeOpenCodePluginCacheManifest(cacheDir, version = "latest") {
|
|
915
|
+
try {
|
|
916
|
+
writeFileSync(join2(cacheDir, "package.json"), JSON.stringify({
|
|
917
|
+
name: `${PACKAGE_NAME}-cache`,
|
|
918
|
+
private: true,
|
|
919
|
+
dependencies: {
|
|
920
|
+
[PACKAGE_NAME]: version
|
|
921
|
+
}
|
|
922
|
+
}, null, 2));
|
|
923
|
+
return null;
|
|
924
|
+
} catch (err) {
|
|
925
|
+
return {
|
|
926
|
+
success: false,
|
|
927
|
+
configPath: cacheDir,
|
|
928
|
+
error: `Failed to write cache package.json: ${err}`
|
|
929
|
+
};
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
function removeOpenCodePluginCacheArtifacts(cacheDir) {
|
|
933
|
+
rmSync(join2(cacheDir, "node_modules", PACKAGE_NAME), {
|
|
934
|
+
recursive: true,
|
|
935
|
+
force: true
|
|
936
|
+
});
|
|
937
|
+
rmSync(join2(cacheDir, "bun.lock"), { force: true });
|
|
938
|
+
rmSync(join2(cacheDir, "bun.lockb"), { force: true });
|
|
939
|
+
}
|
|
940
|
+
function verifyOpenCodePluginCache(cacheDir) {
|
|
941
|
+
const pluginPackageJsonPath = join2(cacheDir, "node_modules", PACKAGE_NAME, "package.json");
|
|
942
|
+
if (!existsSync2(pluginPackageJsonPath)) {
|
|
943
|
+
return {
|
|
944
|
+
success: false,
|
|
945
|
+
configPath: cacheDir,
|
|
946
|
+
error: `Cached plugin package not found at ${pluginPackageJsonPath}`
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
try {
|
|
950
|
+
const packageJson = JSON.parse(readFileSync(pluginPackageJsonPath, "utf-8"));
|
|
951
|
+
if (packageJson.name !== PACKAGE_NAME) {
|
|
952
|
+
return {
|
|
953
|
+
success: false,
|
|
954
|
+
configPath: cacheDir,
|
|
955
|
+
error: `Cached plugin package has unexpected name: ${packageJson.name}`
|
|
956
|
+
};
|
|
957
|
+
}
|
|
958
|
+
} catch (err) {
|
|
959
|
+
return {
|
|
960
|
+
success: false,
|
|
961
|
+
configPath: cacheDir,
|
|
962
|
+
error: `Failed to verify cached plugin package: ${err}`
|
|
963
|
+
};
|
|
964
|
+
}
|
|
965
|
+
return null;
|
|
966
|
+
}
|
|
967
|
+
async function warmOpenCodePluginCache() {
|
|
968
|
+
const cliEntryPath = process.argv[1];
|
|
969
|
+
if (!cliEntryPath) {
|
|
970
|
+
return null;
|
|
971
|
+
}
|
|
972
|
+
const packageRoot = findPackageRoot(cliEntryPath);
|
|
973
|
+
if (!packageRoot || !isPackageManagerInstall(packageRoot)) {
|
|
974
|
+
return null;
|
|
975
|
+
}
|
|
976
|
+
const pinnedVersion = getPinnedVersionFromConfig();
|
|
977
|
+
const runningVersion = getVersionFromPackageRoot(packageRoot);
|
|
978
|
+
const requestedTag = getRequestedPackageTag(packageRoot);
|
|
979
|
+
const cacheVersion = pinnedVersion ?? requestedTag ?? runningVersion;
|
|
980
|
+
const cacheDir = getOpenCodePluginCacheDir(cacheVersion);
|
|
981
|
+
try {
|
|
982
|
+
mkdirSync2(cacheDir, { recursive: true });
|
|
983
|
+
} catch (err) {
|
|
984
|
+
return {
|
|
985
|
+
success: false,
|
|
986
|
+
configPath: cacheDir,
|
|
987
|
+
error: `Failed to create OpenCode cache directory: ${err}`
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
const manifestError = writeOpenCodePluginCacheManifest(cacheDir, cacheVersion);
|
|
991
|
+
if (manifestError)
|
|
992
|
+
return manifestError;
|
|
993
|
+
removeOpenCodePluginCacheArtifacts(cacheDir);
|
|
994
|
+
try {
|
|
995
|
+
const proc = crossSpawn(["bun", "install", "--ignore-scripts"], {
|
|
996
|
+
cwd: cacheDir,
|
|
997
|
+
stdout: "pipe",
|
|
998
|
+
stderr: "pipe"
|
|
999
|
+
});
|
|
1000
|
+
await proc.exited;
|
|
1001
|
+
if (proc.exitCode !== 0) {
|
|
1002
|
+
const stderr = (await proc.stderr()).trim();
|
|
1003
|
+
return {
|
|
1004
|
+
success: false,
|
|
1005
|
+
configPath: cacheDir,
|
|
1006
|
+
error: stderr || `bun install exited with code ${proc.exitCode}`
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
const verificationError = verifyOpenCodePluginCache(cacheDir);
|
|
1010
|
+
if (verificationError)
|
|
1011
|
+
return verificationError;
|
|
1012
|
+
return { success: true, configPath: cacheDir };
|
|
1013
|
+
} catch (err) {
|
|
1014
|
+
return {
|
|
1015
|
+
success: false,
|
|
1016
|
+
configPath: cacheDir,
|
|
1017
|
+
error: `Failed to warm OpenCode cache: ${err}`
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
function stripJsonComments(json) {
|
|
1022
|
+
const commentPattern = /\\"|"(?:\\"|[^"])*"|(\/\/.*|\/\*[\s\S]*?\*\/)/g;
|
|
1023
|
+
const trailingCommaPattern = /\\"|"(?:\\"|[^"])*"|(,)(\s*[}\]])/g;
|
|
1024
|
+
return json.replace(commentPattern, (match, commentGroup) => commentGroup ? "" : match).replace(trailingCommaPattern, (match, comma, closing) => comma ? closing : match);
|
|
1025
|
+
}
|
|
1026
|
+
function parseConfigFile(path) {
|
|
1027
|
+
try {
|
|
1028
|
+
if (!existsSync2(path))
|
|
1029
|
+
return { config: null };
|
|
1030
|
+
const stat = statSync(path);
|
|
1031
|
+
if (stat.size === 0)
|
|
1032
|
+
return { config: null };
|
|
1033
|
+
const content = readFileSync(path, "utf-8");
|
|
1034
|
+
if (content.trim().length === 0)
|
|
1035
|
+
return { config: null };
|
|
1036
|
+
return { config: JSON.parse(stripJsonComments(content)) };
|
|
1037
|
+
} catch (err) {
|
|
1038
|
+
return { config: null, error: String(err) };
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
function parseConfig(path) {
|
|
1042
|
+
const result = parseConfigFile(path);
|
|
1043
|
+
if (result.config || result.error)
|
|
1044
|
+
return result;
|
|
1045
|
+
if (path.endsWith(".json")) {
|
|
1046
|
+
const jsoncPath = path.replace(/\.json$/, ".jsonc");
|
|
1047
|
+
return parseConfigFile(jsoncPath);
|
|
1048
|
+
}
|
|
1049
|
+
return { config: null };
|
|
1050
|
+
}
|
|
1051
|
+
function writeConfig(configPath, config) {
|
|
1052
|
+
if (configPath.endsWith(".jsonc")) {
|
|
1053
|
+
console.warn("[config-manager] Writing to .jsonc file - comments will not be preserved");
|
|
1054
|
+
}
|
|
1055
|
+
const tmpPath = `${configPath}.tmp`;
|
|
1056
|
+
const bakPath = `${configPath}.bak`;
|
|
1057
|
+
const content = `${JSON.stringify(config, null, 2)}
|
|
1058
|
+
`;
|
|
1059
|
+
if (existsSync2(configPath)) {
|
|
1060
|
+
copyFileSync(configPath, bakPath);
|
|
1061
|
+
}
|
|
1062
|
+
writeFileSync(tmpPath, content);
|
|
1063
|
+
renameSync(tmpPath, configPath);
|
|
1064
|
+
}
|
|
1065
|
+
async function addPluginToOpenCodeConfig() {
|
|
1066
|
+
const configPath = getExistingConfigPath();
|
|
1067
|
+
try {
|
|
1068
|
+
ensureOpenCodeConfigDir();
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
return {
|
|
1071
|
+
success: false,
|
|
1072
|
+
configPath,
|
|
1073
|
+
error: `Failed to create config directory: ${err}`
|
|
1074
|
+
};
|
|
1075
|
+
}
|
|
1076
|
+
try {
|
|
1077
|
+
const { config: parsedConfig, error } = parseConfig(configPath);
|
|
1078
|
+
if (error) {
|
|
1079
|
+
return {
|
|
1080
|
+
success: false,
|
|
1081
|
+
configPath,
|
|
1082
|
+
error: `Failed to parse config: ${error}`
|
|
1083
|
+
};
|
|
1084
|
+
}
|
|
1085
|
+
const config = parsedConfig ?? {};
|
|
1086
|
+
const plugins = getPlugins(config);
|
|
1087
|
+
const pluginEntry = getPluginEntry();
|
|
1088
|
+
const filteredPlugins = plugins.filter((plugin) => !isMatchingPluginEntry(plugin));
|
|
1089
|
+
filteredPlugins.push(pluginEntry);
|
|
1090
|
+
config.plugin = filteredPlugins;
|
|
1091
|
+
writeConfig(configPath, config);
|
|
1092
|
+
return { success: true, configPath };
|
|
1093
|
+
} catch (err) {
|
|
1094
|
+
return {
|
|
1095
|
+
success: false,
|
|
1096
|
+
configPath,
|
|
1097
|
+
error: `Failed to update opencode config: ${err}`
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
async function addPluginToOpenCodeTuiConfig() {
|
|
1102
|
+
const configPath = getExistingTuiConfigPath();
|
|
1103
|
+
try {
|
|
1104
|
+
ensureTuiConfigDir();
|
|
1105
|
+
} catch (err) {
|
|
1106
|
+
return {
|
|
1107
|
+
success: false,
|
|
1108
|
+
configPath,
|
|
1109
|
+
error: `Failed to create config directory: ${err}`
|
|
1110
|
+
};
|
|
1111
|
+
}
|
|
1112
|
+
try {
|
|
1113
|
+
const { config: parsedConfig, error } = parseConfig(configPath);
|
|
1114
|
+
if (error) {
|
|
1115
|
+
return {
|
|
1116
|
+
success: false,
|
|
1117
|
+
configPath,
|
|
1118
|
+
error: `Failed to parse TUI config: ${error}`
|
|
1119
|
+
};
|
|
1120
|
+
}
|
|
1121
|
+
const config = parsedConfig ?? {};
|
|
1122
|
+
const plugins = getPlugins(config);
|
|
1123
|
+
const pluginEntry = getPluginEntry();
|
|
1124
|
+
const filteredPlugins = plugins.filter((plugin) => !isMatchingPluginEntry(plugin));
|
|
1125
|
+
filteredPlugins.push(pluginEntry);
|
|
1126
|
+
config.plugin = filteredPlugins;
|
|
1127
|
+
writeConfig(configPath, config);
|
|
1128
|
+
return { success: true, configPath };
|
|
1129
|
+
} catch (err) {
|
|
1130
|
+
return {
|
|
1131
|
+
success: false,
|
|
1132
|
+
configPath,
|
|
1133
|
+
error: `Failed to update opencode TUI config: ${err}`
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
function writeLiteConfig(installConfig, targetPath) {
|
|
1138
|
+
const configPath = targetPath ?? getLiteConfig();
|
|
1139
|
+
try {
|
|
1140
|
+
ensureConfigDir();
|
|
1141
|
+
const config = generateLiteConfig(installConfig);
|
|
1142
|
+
const tmpPath = `${configPath}.tmp`;
|
|
1143
|
+
const bakPath = `${configPath}.bak`;
|
|
1144
|
+
const content = `${JSON.stringify(config, null, 2)}
|
|
1145
|
+
`;
|
|
1146
|
+
if (existsSync2(configPath)) {
|
|
1147
|
+
copyFileSync(configPath, bakPath);
|
|
1148
|
+
}
|
|
1149
|
+
writeFileSync(tmpPath, content);
|
|
1150
|
+
renameSync(tmpPath, configPath);
|
|
1151
|
+
return { success: true, configPath };
|
|
1152
|
+
} catch (err) {
|
|
1153
|
+
return {
|
|
1154
|
+
success: false,
|
|
1155
|
+
configPath,
|
|
1156
|
+
error: `Failed to write lite config: ${err}`
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
function disableDefaultAgents() {
|
|
1161
|
+
const configPath = getExistingConfigPath();
|
|
1162
|
+
try {
|
|
1163
|
+
ensureOpenCodeConfigDir();
|
|
1164
|
+
const { config: parsedConfig, error } = parseConfig(configPath);
|
|
1165
|
+
if (error) {
|
|
1166
|
+
return {
|
|
1167
|
+
success: false,
|
|
1168
|
+
configPath,
|
|
1169
|
+
error: `Failed to parse config: ${error}`
|
|
1170
|
+
};
|
|
1171
|
+
}
|
|
1172
|
+
const config = parsedConfig ?? {};
|
|
1173
|
+
const agent = config.agent ?? {};
|
|
1174
|
+
for (const agentName of DEFAULT_OPENCODE_AGENTS_TO_DISABLE) {
|
|
1175
|
+
const existing = agent[agentName];
|
|
1176
|
+
agent[agentName] = {
|
|
1177
|
+
...existing && typeof existing === "object" && !Array.isArray(existing) ? existing : {},
|
|
1178
|
+
disable: true
|
|
1179
|
+
};
|
|
1180
|
+
}
|
|
1181
|
+
config.agent = agent;
|
|
1182
|
+
writeConfig(configPath, config);
|
|
1183
|
+
return { success: true, configPath };
|
|
1184
|
+
} catch (err) {
|
|
1185
|
+
return {
|
|
1186
|
+
success: false,
|
|
1187
|
+
configPath,
|
|
1188
|
+
error: `Failed to disable default agents: ${err}`
|
|
1189
|
+
};
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
function enableLspByDefault() {
|
|
1193
|
+
const configPath = getExistingConfigPath();
|
|
1194
|
+
try {
|
|
1195
|
+
ensureOpenCodeConfigDir();
|
|
1196
|
+
const { config: parsedConfig, error } = parseConfig(configPath);
|
|
1197
|
+
if (error) {
|
|
1198
|
+
return {
|
|
1199
|
+
success: false,
|
|
1200
|
+
configPath,
|
|
1201
|
+
error: `Failed to parse config: ${error}`
|
|
1202
|
+
};
|
|
1203
|
+
}
|
|
1204
|
+
const config = parsedConfig ?? {};
|
|
1205
|
+
if (config.lsp === undefined) {
|
|
1206
|
+
config.lsp = true;
|
|
1207
|
+
writeConfig(configPath, config);
|
|
1208
|
+
}
|
|
1209
|
+
return { success: true, configPath };
|
|
1210
|
+
} catch (err) {
|
|
1211
|
+
return {
|
|
1212
|
+
success: false,
|
|
1213
|
+
configPath,
|
|
1214
|
+
error: `Failed to enable LSP: ${err}`
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
function detectCurrentConfig() {
|
|
1219
|
+
const result = {
|
|
1220
|
+
isInstalled: false,
|
|
1221
|
+
hasKimi: false,
|
|
1222
|
+
hasOpenAI: false,
|
|
1223
|
+
hasAnthropic: false,
|
|
1224
|
+
hasCopilot: false,
|
|
1225
|
+
hasZaiPlan: false,
|
|
1226
|
+
hasAntigravity: false,
|
|
1227
|
+
hasChutes: false,
|
|
1228
|
+
hasOpencodeZen: false,
|
|
1229
|
+
hasTmux: false
|
|
1230
|
+
};
|
|
1231
|
+
const { config } = parseConfig(getExistingConfigPath());
|
|
1232
|
+
if (!config)
|
|
1233
|
+
return result;
|
|
1234
|
+
const plugins = getPluginEntries(config);
|
|
1235
|
+
result.isInstalled = plugins.some((p) => isPluginEntry(p));
|
|
1236
|
+
result.hasAntigravity = plugins.some((p) => p.startsWith("opencode-antigravity-auth"));
|
|
1237
|
+
const providers = config.provider;
|
|
1238
|
+
result.hasKimi = !!providers?.kimi;
|
|
1239
|
+
result.hasAnthropic = !!providers?.anthropic;
|
|
1240
|
+
result.hasCopilot = !!providers?.["github-copilot"];
|
|
1241
|
+
result.hasZaiPlan = !!providers?.["zai-coding-plan"];
|
|
1242
|
+
result.hasChutes = !!providers?.chutes;
|
|
1243
|
+
if (providers?.google)
|
|
1244
|
+
result.hasAntigravity = true;
|
|
1245
|
+
const { config: liteConfig } = parseConfig(getLiteConfig());
|
|
1246
|
+
if (liteConfig && typeof liteConfig === "object") {
|
|
1247
|
+
const configObj = liteConfig;
|
|
1248
|
+
const presetName = configObj.preset;
|
|
1249
|
+
const presets = configObj.presets;
|
|
1250
|
+
const agents = presets?.[presetName];
|
|
1251
|
+
if (agents && typeof agents === "object") {
|
|
1252
|
+
const models = Object.values(agents).filter((a) => a && typeof a === "object").flatMap((a) => getModelIds(a.model));
|
|
1253
|
+
result.hasOpenAI ||= models.some((m) => m.startsWith("openai/"));
|
|
1254
|
+
result.hasAnthropic ||= models.some((m) => m.startsWith("anthropic/"));
|
|
1255
|
+
result.hasCopilot ||= models.some((m) => m.startsWith("github-copilot/"));
|
|
1256
|
+
result.hasZaiPlan ||= models.some((m) => m.startsWith("zai-coding-plan/"));
|
|
1257
|
+
result.hasOpencodeZen ||= models.some((m) => m.startsWith("opencode/"));
|
|
1258
|
+
if (models.some((m) => m.startsWith("google/"))) {
|
|
1259
|
+
result.hasAntigravity = true;
|
|
1260
|
+
}
|
|
1261
|
+
if (models.some((m) => m.startsWith("chutes/"))) {
|
|
1262
|
+
result.hasChutes = true;
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
if (configObj.tmux && typeof configObj.tmux === "object") {
|
|
1266
|
+
const tmuxConfig = configObj.tmux;
|
|
1267
|
+
result.hasTmux = tmuxConfig.enabled === true;
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
return result;
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
// src/config/loader.ts
|
|
1274
|
+
function findConfigPath(basePath) {
|
|
1275
|
+
const jsoncPath = `${basePath}.jsonc`;
|
|
1276
|
+
const jsonPath = `${basePath}.json`;
|
|
1277
|
+
if (fs.existsSync(jsoncPath)) {
|
|
1278
|
+
return jsoncPath;
|
|
1279
|
+
}
|
|
1280
|
+
if (fs.existsSync(jsonPath)) {
|
|
1281
|
+
return jsonPath;
|
|
1282
|
+
}
|
|
1283
|
+
return null;
|
|
1284
|
+
}
|
|
1285
|
+
function findConfigPathInDirs(configDirs, baseName) {
|
|
1286
|
+
for (const configDir of configDirs) {
|
|
1287
|
+
const configPath = findConfigPath(path.join(configDir, baseName));
|
|
1288
|
+
if (configPath) {
|
|
1289
|
+
return configPath;
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
return null;
|
|
1293
|
+
}
|
|
1294
|
+
function findPluginConfigPaths(directory) {
|
|
1295
|
+
const userConfigPath = findConfigPathInDirs(getConfigSearchDirs(), "oh-my-opencode-slim");
|
|
1296
|
+
const projectConfigBasePath = path.join(directory, ".opencode", "oh-my-opencode-slim");
|
|
1297
|
+
const projectConfigPath = findConfigPath(projectConfigBasePath);
|
|
1298
|
+
return { userConfigPath, projectConfigPath };
|
|
1299
|
+
}
|
|
1300
|
+
function mergePluginConfigs(base, override) {
|
|
1301
|
+
return {
|
|
1302
|
+
...base,
|
|
1303
|
+
...override,
|
|
1304
|
+
agents: deepMerge(base.agents, override.agents),
|
|
1305
|
+
presets: deepMerge(base.presets, override.presets),
|
|
1306
|
+
tmux: deepMerge(base.tmux, override.tmux),
|
|
1307
|
+
multiplexer: deepMerge(base.multiplexer, override.multiplexer),
|
|
1308
|
+
interview: deepMerge(base.interview, override.interview),
|
|
1309
|
+
backgroundJobs: deepMerge(base.backgroundJobs, override.backgroundJobs),
|
|
1310
|
+
fallback: deepMerge(base.fallback, override.fallback),
|
|
1311
|
+
council: deepMerge(base.council, override.council),
|
|
1312
|
+
acpAgents: deepMerge(base.acpAgents, override.acpAgents),
|
|
1313
|
+
companion: deepMerge(base.companion, override.companion)
|
|
1314
|
+
};
|
|
1315
|
+
}
|
|
1316
|
+
function deepMerge(base, override) {
|
|
1317
|
+
if (!base)
|
|
1318
|
+
return override;
|
|
1319
|
+
if (!override)
|
|
1320
|
+
return base;
|
|
1321
|
+
const result = { ...base };
|
|
1322
|
+
for (const key of Object.keys(override)) {
|
|
1323
|
+
const baseVal = base[key];
|
|
1324
|
+
const overrideVal = override[key];
|
|
1325
|
+
if (typeof baseVal === "object" && baseVal !== null && typeof overrideVal === "object" && overrideVal !== null && !Array.isArray(baseVal) && !Array.isArray(overrideVal)) {
|
|
1326
|
+
result[key] = deepMerge(baseVal, overrideVal);
|
|
1327
|
+
} else {
|
|
1328
|
+
result[key] = overrideVal;
|
|
1329
|
+
}
|
|
1330
|
+
}
|
|
1331
|
+
return result;
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// src/cli/doctor.ts
|
|
1335
|
+
function parseDoctorArgs(args) {
|
|
1336
|
+
const result = {};
|
|
1337
|
+
for (const arg of args) {
|
|
1338
|
+
if (arg === "--json") {
|
|
1339
|
+
result.json = true;
|
|
1340
|
+
} else if (arg === "--help" || arg === "-h") {
|
|
1341
|
+
result.help = true;
|
|
1342
|
+
} else {
|
|
1343
|
+
result.error ??= `Unknown doctor option: ${arg}`;
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
return result;
|
|
1347
|
+
}
|
|
1348
|
+
function checkConfigFile(scope, configPath) {
|
|
1349
|
+
if (configPath === null) {
|
|
1350
|
+
return { scope, path: null, exists: false, ok: true };
|
|
1351
|
+
}
|
|
1352
|
+
try {
|
|
1353
|
+
const stat = fs2.statSync(configPath);
|
|
1354
|
+
if (stat.size === 0) {
|
|
1355
|
+
return {
|
|
1356
|
+
scope,
|
|
1357
|
+
path: configPath,
|
|
1358
|
+
exists: true,
|
|
1359
|
+
ok: false,
|
|
1360
|
+
error: {
|
|
1361
|
+
kind: "invalid-json",
|
|
1362
|
+
message: "Empty file is not valid JSON"
|
|
1363
|
+
}
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
const content = fs2.readFileSync(configPath, "utf-8");
|
|
1367
|
+
const rawConfig = JSON.parse(stripJsonComments(content));
|
|
1368
|
+
const parseResult = PluginConfigSchema.safeParse(rawConfig);
|
|
1369
|
+
if (!parseResult.success) {
|
|
1370
|
+
return {
|
|
1371
|
+
scope,
|
|
1372
|
+
path: configPath,
|
|
1373
|
+
exists: true,
|
|
1374
|
+
ok: false,
|
|
1375
|
+
error: {
|
|
1376
|
+
kind: "invalid-schema",
|
|
1377
|
+
message: z3.prettifyError(parseResult.error),
|
|
1378
|
+
issues: parseResult.error.issues
|
|
1379
|
+
}
|
|
1380
|
+
};
|
|
1381
|
+
}
|
|
1382
|
+
return {
|
|
1383
|
+
scope,
|
|
1384
|
+
path: configPath,
|
|
1385
|
+
exists: true,
|
|
1386
|
+
ok: true,
|
|
1387
|
+
config: parseResult.data
|
|
1388
|
+
};
|
|
1389
|
+
} catch (err) {
|
|
1390
|
+
if (err instanceof SyntaxError) {
|
|
1391
|
+
return {
|
|
1392
|
+
scope,
|
|
1393
|
+
path: configPath,
|
|
1394
|
+
exists: true,
|
|
1395
|
+
ok: false,
|
|
1396
|
+
error: {
|
|
1397
|
+
kind: "invalid-json",
|
|
1398
|
+
message: err.message
|
|
1399
|
+
}
|
|
1400
|
+
};
|
|
1401
|
+
} else if (err instanceof Error && "code" in err && err.code === "ENOENT") {
|
|
1402
|
+
return {
|
|
1403
|
+
scope,
|
|
1404
|
+
path: configPath,
|
|
1405
|
+
exists: false,
|
|
1406
|
+
ok: false,
|
|
1407
|
+
error: {
|
|
1408
|
+
kind: "read-error",
|
|
1409
|
+
message: "File was not found while reading"
|
|
1410
|
+
}
|
|
1411
|
+
};
|
|
1412
|
+
}
|
|
1413
|
+
return {
|
|
1414
|
+
scope,
|
|
1415
|
+
path: configPath,
|
|
1416
|
+
exists: true,
|
|
1417
|
+
ok: false,
|
|
1418
|
+
error: {
|
|
1419
|
+
kind: "read-error",
|
|
1420
|
+
message: err instanceof Error ? err.message : String(err)
|
|
1421
|
+
}
|
|
1422
|
+
};
|
|
1423
|
+
}
|
|
1424
|
+
}
|
|
1425
|
+
function checkPreset(mergedConfig) {
|
|
1426
|
+
const envPreset = process.env.OH_MY_OPENCODE_SLIM_PRESET;
|
|
1427
|
+
const presetName = envPreset || mergedConfig.preset;
|
|
1428
|
+
if (presetName === undefined) {
|
|
1429
|
+
return;
|
|
1430
|
+
}
|
|
1431
|
+
if (!mergedConfig.presets?.[presetName]) {
|
|
1432
|
+
return {
|
|
1433
|
+
preset: presetName,
|
|
1434
|
+
ok: false,
|
|
1435
|
+
error: {
|
|
1436
|
+
kind: "missing-preset",
|
|
1437
|
+
message: `Preset "${presetName}" not found in config`
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
}
|
|
1441
|
+
return { preset: presetName, ok: true };
|
|
1442
|
+
}
|
|
1443
|
+
function getMergedConfig(userConfig, projectConfig) {
|
|
1444
|
+
return projectConfig ? mergePluginConfigs(userConfig ?? {}, projectConfig) : userConfig ?? {};
|
|
1445
|
+
}
|
|
1446
|
+
function runDoctorCheck(cwd) {
|
|
1447
|
+
const { userConfigPath, projectConfigPath } = findPluginConfigPaths(cwd);
|
|
1448
|
+
const userCheck = checkConfigFile("user", userConfigPath);
|
|
1449
|
+
const projectCheck = checkConfigFile("project", projectConfigPath);
|
|
1450
|
+
const configs = [userCheck, projectCheck];
|
|
1451
|
+
const hasInvalidConfig = configs.some((c) => !c.ok);
|
|
1452
|
+
let presetCheckResult;
|
|
1453
|
+
if (!hasInvalidConfig) {
|
|
1454
|
+
const mergedConfig = getMergedConfig(userCheck.config, projectCheck.config);
|
|
1455
|
+
presetCheckResult = checkPreset(mergedConfig);
|
|
1456
|
+
}
|
|
1457
|
+
return {
|
|
1458
|
+
ok: configs.every((c) => c.ok) && (!presetCheckResult || presetCheckResult.ok),
|
|
1459
|
+
project: cwd,
|
|
1460
|
+
configs,
|
|
1461
|
+
presetCheck: presetCheckResult
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
function formatHumanDoctorResult(result) {
|
|
1465
|
+
const lines = [];
|
|
1466
|
+
lines.push(`Project: ${result.project}`);
|
|
1467
|
+
lines.push("");
|
|
1468
|
+
for (const config of result.configs) {
|
|
1469
|
+
if (config.path === null) {
|
|
1470
|
+
lines.push(`[${config.scope}] No config file found`);
|
|
1471
|
+
} else {
|
|
1472
|
+
const status = config.ok ? "✓" : "✗";
|
|
1473
|
+
lines.push(`[${config.scope}] ${config.path} ${status}`);
|
|
1474
|
+
if (!config.ok && config.error) {
|
|
1475
|
+
if (config.error.kind === "invalid-json") {
|
|
1476
|
+
lines.push(` Invalid JSON: ${config.error.message}`);
|
|
1477
|
+
} else if (config.error.kind === "invalid-schema") {
|
|
1478
|
+
lines.push(" Schema error:");
|
|
1479
|
+
for (const line of config.error.message.split(`
|
|
1480
|
+
`)) {
|
|
1481
|
+
lines.push(` ${line}`);
|
|
1482
|
+
}
|
|
1483
|
+
} else if (config.error.kind === "read-error") {
|
|
1484
|
+
lines.push(` Read error: ${config.error.message}`);
|
|
1485
|
+
}
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
if (result.presetCheck) {
|
|
1490
|
+
lines.push("");
|
|
1491
|
+
const status = result.presetCheck.ok ? "✓" : "✗";
|
|
1492
|
+
lines.push(`[preset] ${result.presetCheck.preset} ${status}`);
|
|
1493
|
+
if (result.presetCheck.error) {
|
|
1494
|
+
lines.push(` ${result.presetCheck.error.message}`);
|
|
1495
|
+
}
|
|
1496
|
+
}
|
|
1497
|
+
return lines.join(`
|
|
1498
|
+
`);
|
|
1499
|
+
}
|
|
1500
|
+
function formatJsonDoctorResult(result) {
|
|
1501
|
+
return JSON.stringify({
|
|
1502
|
+
...result,
|
|
1503
|
+
configs: result.configs.map(({ config: _config, ...config }) => config)
|
|
1504
|
+
}, null, 2);
|
|
1505
|
+
}
|
|
1506
|
+
async function doctor(args) {
|
|
1507
|
+
if (args.help) {
|
|
1508
|
+
console.log(`Usage: oh-my-opencode-slim doctor [OPTIONS]
|
|
1509
|
+
|
|
1510
|
+
Options:
|
|
1511
|
+
--json Print diagnostics as JSON
|
|
1512
|
+
-h, --help Show this help message`);
|
|
1513
|
+
return 0;
|
|
1514
|
+
}
|
|
1515
|
+
if (args.error) {
|
|
1516
|
+
console.error(args.error);
|
|
1517
|
+
return 1;
|
|
1518
|
+
}
|
|
1519
|
+
const result = runDoctorCheck(process.cwd());
|
|
1520
|
+
if (args.json) {
|
|
1521
|
+
console.log(formatJsonDoctorResult(result));
|
|
1522
|
+
} else {
|
|
1523
|
+
console.log(formatHumanDoctorResult(result));
|
|
1524
|
+
}
|
|
1525
|
+
return result.ok ? 0 : 1;
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
// src/cli/install.ts
|
|
1529
|
+
import { existsSync as existsSync7 } from "node:fs";
|
|
1530
|
+
import { createInterface } from "node:readline/promises";
|
|
1531
|
+
import { fileURLToPath } from "node:url";
|
|
1532
|
+
|
|
1533
|
+
// src/hooks/auto-update-checker/skill-sync.ts
|
|
1534
|
+
import * as crypto from "node:crypto";
|
|
1535
|
+
import {
|
|
1536
|
+
copyFileSync as copyFileSync2,
|
|
1537
|
+
existsSync as existsSync4,
|
|
1538
|
+
lstatSync,
|
|
1539
|
+
mkdirSync as mkdirSync3,
|
|
1540
|
+
readdirSync,
|
|
1541
|
+
readFileSync as readFileSync4,
|
|
1542
|
+
renameSync as renameSync2,
|
|
1543
|
+
rmSync as rmSync2,
|
|
1544
|
+
unlinkSync,
|
|
1545
|
+
writeFileSync as writeFileSync2
|
|
1546
|
+
} from "node:fs";
|
|
1547
|
+
import * as os from "node:os";
|
|
1548
|
+
import * as path2 from "node:path";
|
|
1549
|
+
|
|
1550
|
+
// src/utils/logger.ts
|
|
1551
|
+
import { appendFile } from "node:fs/promises";
|
|
1552
|
+
var RETENTION_MS = 7 * 24 * 60 * 60 * 1000;
|
|
1553
|
+
var logFile = null;
|
|
1554
|
+
var writeChain = Promise.resolve();
|
|
1555
|
+
function log(message, data) {
|
|
1556
|
+
const target = logFile;
|
|
1557
|
+
if (!target)
|
|
1558
|
+
return;
|
|
1559
|
+
try {
|
|
1560
|
+
const timestamp = new Date().toISOString();
|
|
1561
|
+
let dataStr = "";
|
|
1562
|
+
if (data !== undefined) {
|
|
1563
|
+
try {
|
|
1564
|
+
dataStr = JSON.stringify(data);
|
|
1565
|
+
} catch {
|
|
1566
|
+
dataStr = "[unserializable]";
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
const logEntry = `[${timestamp}] ${message} ${dataStr}
|
|
1570
|
+
`;
|
|
1571
|
+
writeChain = writeChain.then(() => appendFile(target, logEntry)).catch(() => {});
|
|
1572
|
+
} catch {}
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
// src/hooks/auto-update-checker/skill-sync.ts
|
|
1576
|
+
var localProcessToken = globalThis.OMO_SKILL_SYNC_PROCESS_TOKEN;
|
|
1577
|
+
if (!localProcessToken) {
|
|
1578
|
+
localProcessToken = crypto.randomUUID();
|
|
1579
|
+
globalThis.OMO_SKILL_SYNC_PROCESS_TOKEN = localProcessToken;
|
|
1580
|
+
}
|
|
1581
|
+
var PROCESS_TOKEN = localProcessToken;
|
|
1582
|
+
var ACQUIRED_LOCKS = new Set;
|
|
1583
|
+
var LEGACY_MANAGED_SKILL_HASHES = {};
|
|
1584
|
+
function validateManifest(data) {
|
|
1585
|
+
if (typeof data !== "object" || data === null)
|
|
1586
|
+
return false;
|
|
1587
|
+
const d = data;
|
|
1588
|
+
if (d.schemaVersion !== 1)
|
|
1589
|
+
return false;
|
|
1590
|
+
if (typeof d.skills !== "object" || d.skills === null)
|
|
1591
|
+
return false;
|
|
1592
|
+
const allowedStatuses = new Set([
|
|
1593
|
+
"managed",
|
|
1594
|
+
"customized",
|
|
1595
|
+
"deleted",
|
|
1596
|
+
"conflict"
|
|
1597
|
+
]);
|
|
1598
|
+
const skillsObj = d.skills;
|
|
1599
|
+
for (const key of Object.keys(skillsObj)) {
|
|
1600
|
+
const entry = skillsObj[key];
|
|
1601
|
+
if (typeof entry !== "object" || entry === null)
|
|
1602
|
+
return false;
|
|
1603
|
+
if (typeof entry.status !== "string" || !allowedStatuses.has(entry.status))
|
|
1604
|
+
return false;
|
|
1605
|
+
if (typeof entry.packageVersion !== "string")
|
|
1606
|
+
return false;
|
|
1607
|
+
if (typeof entry.sourceHash !== "string")
|
|
1608
|
+
return false;
|
|
1609
|
+
if (typeof entry.lastManagedHash !== "string")
|
|
1610
|
+
return false;
|
|
1611
|
+
if (typeof entry.lastSeenHash !== "string")
|
|
1612
|
+
return false;
|
|
1613
|
+
if (entry.stagedPath !== undefined && typeof entry.stagedPath !== "string")
|
|
1614
|
+
return false;
|
|
1615
|
+
if (typeof entry.updatedAt !== "string")
|
|
1616
|
+
return false;
|
|
1617
|
+
}
|
|
1618
|
+
return true;
|
|
1619
|
+
}
|
|
1620
|
+
function copyDirRecursive(src, dest) {
|
|
1621
|
+
const stat = lstatSync(src);
|
|
1622
|
+
if (stat.isSymbolicLink()) {
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
if (stat.isDirectory()) {
|
|
1626
|
+
mkdirSync3(dest, { recursive: true });
|
|
1627
|
+
const entries = readdirSync(src);
|
|
1628
|
+
for (const entry of entries) {
|
|
1629
|
+
copyDirRecursive(path2.join(src, entry), path2.join(dest, entry));
|
|
1630
|
+
}
|
|
1631
|
+
} else if (stat.isFile()) {
|
|
1632
|
+
const destDir = path2.dirname(dest);
|
|
1633
|
+
if (!existsSync4(destDir)) {
|
|
1634
|
+
mkdirSync3(destDir, { recursive: true });
|
|
1635
|
+
}
|
|
1636
|
+
copyFileSync2(src, dest);
|
|
1637
|
+
}
|
|
1638
|
+
}
|
|
1639
|
+
function computeDirectoryHash(dirPath) {
|
|
1640
|
+
const hash = crypto.createHash("sha256");
|
|
1641
|
+
const entriesToHash = [];
|
|
1642
|
+
function traverse(currentDir) {
|
|
1643
|
+
const entries = readdirSync(currentDir);
|
|
1644
|
+
for (const entry of entries) {
|
|
1645
|
+
const absolutePath = path2.join(currentDir, entry);
|
|
1646
|
+
const stat = lstatSync(absolutePath);
|
|
1647
|
+
const relativePath = path2.relative(dirPath, absolutePath);
|
|
1648
|
+
if (stat.isSymbolicLink()) {
|
|
1649
|
+
continue;
|
|
1650
|
+
}
|
|
1651
|
+
if (stat.isDirectory()) {
|
|
1652
|
+
entriesToHash.push({
|
|
1653
|
+
relativePath,
|
|
1654
|
+
absolutePath,
|
|
1655
|
+
kind: "directory",
|
|
1656
|
+
mode: stat.mode
|
|
1657
|
+
});
|
|
1658
|
+
traverse(absolutePath);
|
|
1659
|
+
} else if (stat.isFile()) {
|
|
1660
|
+
entriesToHash.push({
|
|
1661
|
+
relativePath,
|
|
1662
|
+
absolutePath,
|
|
1663
|
+
kind: "file",
|
|
1664
|
+
mode: stat.mode
|
|
1665
|
+
});
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
}
|
|
1669
|
+
traverse(dirPath);
|
|
1670
|
+
entriesToHash.sort((a, b) => {
|
|
1671
|
+
if (a.relativePath < b.relativePath)
|
|
1672
|
+
return -1;
|
|
1673
|
+
if (a.relativePath > b.relativePath)
|
|
1674
|
+
return 1;
|
|
1675
|
+
return 0;
|
|
1676
|
+
});
|
|
1677
|
+
for (const entry of entriesToHash) {
|
|
1678
|
+
hash.update(entry.kind);
|
|
1679
|
+
hash.update("\x00");
|
|
1680
|
+
hash.update(entry.relativePath);
|
|
1681
|
+
hash.update("\x00");
|
|
1682
|
+
hash.update(String(entry.mode & 4095));
|
|
1683
|
+
hash.update("\x00");
|
|
1684
|
+
if (entry.kind === "file") {
|
|
1685
|
+
const content = readFileSync4(entry.absolutePath);
|
|
1686
|
+
hash.update(content);
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
return hash.digest("hex");
|
|
1690
|
+
}
|
|
1691
|
+
function isPidRunning(pid) {
|
|
1692
|
+
try {
|
|
1693
|
+
process.kill(pid, 0);
|
|
1694
|
+
return true;
|
|
1695
|
+
} catch (err) {
|
|
1696
|
+
return err.code === "EPERM";
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
var CROSS_HOST_LOCK_EXPIRY_MS = 5 * 60 * 1000;
|
|
1700
|
+
function acquireLock(lockDir) {
|
|
1701
|
+
const metadataPath = path2.join(lockDir, "owner.json");
|
|
1702
|
+
const currentHost = os.hostname();
|
|
1703
|
+
const currentPid = process.pid;
|
|
1704
|
+
const writeMetadata = () => {
|
|
1705
|
+
try {
|
|
1706
|
+
const metadata = {
|
|
1707
|
+
pid: currentPid,
|
|
1708
|
+
host: currentHost,
|
|
1709
|
+
time: Date.now(),
|
|
1710
|
+
token: PROCESS_TOKEN
|
|
1711
|
+
};
|
|
1712
|
+
writeFileSync2(metadataPath, JSON.stringify(metadata), "utf-8");
|
|
1713
|
+
} catch {}
|
|
1714
|
+
};
|
|
1715
|
+
try {
|
|
1716
|
+
mkdirSync3(lockDir);
|
|
1717
|
+
writeMetadata();
|
|
1718
|
+
ACQUIRED_LOCKS.add(path2.resolve(lockDir));
|
|
1719
|
+
return true;
|
|
1720
|
+
} catch (err) {
|
|
1721
|
+
if (err.code !== "EEXIST") {
|
|
1722
|
+
throw err;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
try {
|
|
1726
|
+
let shouldSteal = false;
|
|
1727
|
+
let ageMs = 0;
|
|
1728
|
+
if (existsSync4(metadataPath)) {
|
|
1729
|
+
try {
|
|
1730
|
+
const content = readFileSync4(metadataPath, "utf-8");
|
|
1731
|
+
const metadata = JSON.parse(content);
|
|
1732
|
+
ageMs = Date.now() - metadata.time;
|
|
1733
|
+
if (metadata.host === currentHost) {
|
|
1734
|
+
if (!isPidRunning(metadata.pid)) {
|
|
1735
|
+
log(`[skill-sync] Lock owner process ${metadata.pid} is not running on this host. Recovery path.`);
|
|
1736
|
+
shouldSteal = true;
|
|
1737
|
+
}
|
|
1738
|
+
} else {
|
|
1739
|
+
if (ageMs > CROSS_HOST_LOCK_EXPIRY_MS) {
|
|
1740
|
+
log(`[skill-sync] Lock owned by different host ${metadata.host} has expired (${Math.round(ageMs / 1000)}s old). Reclaiming lock.`);
|
|
1741
|
+
shouldSteal = true;
|
|
1742
|
+
} else {
|
|
1743
|
+
log(`[skill-sync] Lock is owned by different host ${metadata.host}; failing closed.`);
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
} catch {
|
|
1747
|
+
shouldSteal = true;
|
|
1748
|
+
}
|
|
1749
|
+
} else {
|
|
1750
|
+
const stat = lstatSync(lockDir);
|
|
1751
|
+
ageMs = Date.now() - stat.mtimeMs;
|
|
1752
|
+
if (ageMs > 30000) {
|
|
1753
|
+
shouldSteal = true;
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
if (!shouldSteal)
|
|
1757
|
+
return false;
|
|
1758
|
+
log(`[skill-sync] Stealing/recovering lock directory.`);
|
|
1759
|
+
rmSync2(lockDir, { recursive: true, force: true });
|
|
1760
|
+
mkdirSync3(lockDir);
|
|
1761
|
+
writeMetadata();
|
|
1762
|
+
ACQUIRED_LOCKS.add(path2.resolve(lockDir));
|
|
1763
|
+
return true;
|
|
1764
|
+
} catch (err) {
|
|
1765
|
+
log(`[skill-sync] Failed to check/recover lock at ${lockDir}:`, err);
|
|
1766
|
+
return false;
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
function releaseLock(lockDir) {
|
|
1770
|
+
const resolvedPath = path2.resolve(lockDir);
|
|
1771
|
+
try {
|
|
1772
|
+
let isOurLock = false;
|
|
1773
|
+
const metadataPath = path2.join(lockDir, "owner.json");
|
|
1774
|
+
if (existsSync4(metadataPath)) {
|
|
1775
|
+
try {
|
|
1776
|
+
const content = readFileSync4(metadataPath, "utf-8");
|
|
1777
|
+
const metadata = JSON.parse(content);
|
|
1778
|
+
if (metadata.host === os.hostname() && metadata.pid === process.pid && metadata.token === PROCESS_TOKEN) {
|
|
1779
|
+
isOurLock = true;
|
|
1780
|
+
} else {
|
|
1781
|
+
isOurLock = false;
|
|
1782
|
+
}
|
|
1783
|
+
} catch (err) {
|
|
1784
|
+
log(`[skill-sync] Lock owner.json is unreadable/corrupt:`, err);
|
|
1785
|
+
isOurLock = false;
|
|
1786
|
+
}
|
|
1787
|
+
} else if (ACQUIRED_LOCKS.has(resolvedPath)) {
|
|
1788
|
+
isOurLock = true;
|
|
1789
|
+
}
|
|
1790
|
+
if (isOurLock) {
|
|
1791
|
+
if (existsSync4(lockDir)) {
|
|
1792
|
+
rmSync2(lockDir, { recursive: true, force: true });
|
|
1793
|
+
}
|
|
1794
|
+
} else if (existsSync4(lockDir)) {
|
|
1795
|
+
log(`[skill-sync] Skipping lock directory removal: lock is not owned by this process/token or owner.json check failed.`);
|
|
1796
|
+
}
|
|
1797
|
+
} catch (err) {
|
|
1798
|
+
log(`[skill-sync] Failed to release lock at ${lockDir}:`, err);
|
|
1799
|
+
} finally {
|
|
1800
|
+
ACQUIRED_LOCKS.delete(resolvedPath);
|
|
1801
|
+
}
|
|
1802
|
+
}
|
|
1803
|
+
function atomicReplaceDir(sourceDir, destDir) {
|
|
1804
|
+
const parentDir = path2.dirname(destDir);
|
|
1805
|
+
if (!existsSync4(parentDir)) {
|
|
1806
|
+
mkdirSync3(parentDir, { recursive: true });
|
|
1807
|
+
}
|
|
1808
|
+
const uniqueSuffix = `${Date.now()}-${Math.random().toString(36).slice(2, 9)}`;
|
|
1809
|
+
const stagingDir = path2.join(parentDir, `.staging-${path2.basename(destDir)}-${uniqueSuffix}`);
|
|
1810
|
+
const backupDir = path2.join(parentDir, `.backup-${path2.basename(destDir)}-${uniqueSuffix}`);
|
|
1811
|
+
let backupCreated = false;
|
|
1812
|
+
try {
|
|
1813
|
+
copyDirRecursive(sourceDir, stagingDir);
|
|
1814
|
+
if (existsSync4(destDir)) {
|
|
1815
|
+
renameSync2(destDir, backupDir);
|
|
1816
|
+
backupCreated = true;
|
|
1817
|
+
}
|
|
1818
|
+
renameSync2(stagingDir, destDir);
|
|
1819
|
+
if (backupCreated) {
|
|
1820
|
+
rmSync2(backupDir, { recursive: true, force: true });
|
|
1821
|
+
}
|
|
1822
|
+
} catch (err) {
|
|
1823
|
+
log(`[skill-sync] Error during atomic replace for ${destDir}. Rolling back:`, err);
|
|
1824
|
+
if (backupCreated) {
|
|
1825
|
+
try {
|
|
1826
|
+
if (existsSync4(destDir)) {
|
|
1827
|
+
rmSync2(destDir, { recursive: true, force: true });
|
|
1828
|
+
}
|
|
1829
|
+
renameSync2(backupDir, destDir);
|
|
1830
|
+
} catch (rollbackErr) {
|
|
1831
|
+
log(`[skill-sync] Critical error during rollback for ${destDir}:`, rollbackErr);
|
|
1832
|
+
}
|
|
1833
|
+
}
|
|
1834
|
+
try {
|
|
1835
|
+
if (existsSync4(stagingDir)) {
|
|
1836
|
+
rmSync2(stagingDir, { recursive: true, force: true });
|
|
1837
|
+
}
|
|
1838
|
+
} catch {}
|
|
1839
|
+
throw err;
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
function matchesArtifactPattern(entry, prefix, skillName) {
|
|
1843
|
+
if (!entry.startsWith(prefix))
|
|
1844
|
+
return false;
|
|
1845
|
+
const rest = entry.slice(prefix.length);
|
|
1846
|
+
if (!rest.startsWith(`${skillName}-`))
|
|
1847
|
+
return false;
|
|
1848
|
+
const suffix = rest.slice(skillName.length + 1);
|
|
1849
|
+
const firstPart = suffix.split("-")[0];
|
|
1850
|
+
const timestamp = Number(firstPart);
|
|
1851
|
+
if (Number.isNaN(timestamp) || timestamp <= 0)
|
|
1852
|
+
return false;
|
|
1853
|
+
return true;
|
|
1854
|
+
}
|
|
1855
|
+
function recoverOrphanArtifacts(destSkillsDir, skillName) {
|
|
1856
|
+
if (!existsSync4(destSkillsDir))
|
|
1857
|
+
return false;
|
|
1858
|
+
let hadArtifacts = false;
|
|
1859
|
+
let entries = [];
|
|
1860
|
+
try {
|
|
1861
|
+
entries = readdirSync(destSkillsDir);
|
|
1862
|
+
} catch {
|
|
1863
|
+
return false;
|
|
1864
|
+
}
|
|
1865
|
+
const backups = [];
|
|
1866
|
+
const stagings = [];
|
|
1867
|
+
for (const entry of entries) {
|
|
1868
|
+
if (matchesArtifactPattern(entry, ".backup-", skillName)) {
|
|
1869
|
+
backups.push(path2.join(destSkillsDir, entry));
|
|
1870
|
+
hadArtifacts = true;
|
|
1871
|
+
} else if (matchesArtifactPattern(entry, ".staging-", skillName)) {
|
|
1872
|
+
stagings.push(path2.join(destSkillsDir, entry));
|
|
1873
|
+
hadArtifacts = true;
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
const destPath = path2.join(destSkillsDir, skillName);
|
|
1877
|
+
if (backups.length > 0) {
|
|
1878
|
+
backups.sort();
|
|
1879
|
+
const mostRecentBackup = backups[backups.length - 1];
|
|
1880
|
+
if (!existsSync4(destPath)) {
|
|
1881
|
+
backups.pop();
|
|
1882
|
+
try {
|
|
1883
|
+
renameSync2(mostRecentBackup, destPath);
|
|
1884
|
+
log(`[skill-sync] Recovered backup for ${skillName} back to destination.`);
|
|
1885
|
+
} catch (err) {
|
|
1886
|
+
log(`[skill-sync] Failed to restore backup for ${skillName}:`, err);
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
for (const backup of backups) {
|
|
1890
|
+
try {
|
|
1891
|
+
rmSync2(backup, { recursive: true, force: true });
|
|
1892
|
+
} catch (err) {
|
|
1893
|
+
log(`[skill-sync] Failed to clean up backup folder ${backup}:`, err);
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
for (const staging of stagings) {
|
|
1898
|
+
try {
|
|
1899
|
+
rmSync2(staging, { recursive: true, force: true });
|
|
1900
|
+
} catch (err) {
|
|
1901
|
+
log(`[skill-sync] Failed to clean up staging folder ${staging}:`, err);
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1904
|
+
return hadArtifacts;
|
|
1905
|
+
}
|
|
1906
|
+
function removeManagedStagedPath(stagedPath, manifestDir, skillName) {
|
|
1907
|
+
try {
|
|
1908
|
+
const absoluteStagedPath = path2.resolve(stagedPath);
|
|
1909
|
+
const absoluteAllowedRoot = path2.resolve(path2.join(manifestDir, "skill-updates"));
|
|
1910
|
+
const relative2 = path2.relative(absoluteAllowedRoot, absoluteStagedPath);
|
|
1911
|
+
const isUnderRoot = relative2 && !relative2.startsWith("..") && !path2.isAbsolute(relative2);
|
|
1912
|
+
if (isUnderRoot) {
|
|
1913
|
+
if (existsSync4(absoluteStagedPath)) {
|
|
1914
|
+
rmSync2(absoluteStagedPath, { recursive: true, force: true });
|
|
1915
|
+
log(`[skill-sync] Safely cleaned up staged path for ${skillName}: ${absoluteStagedPath}`);
|
|
1916
|
+
}
|
|
1917
|
+
} else {
|
|
1918
|
+
log(`[skill-sync] Refusing to delete staged path for ${skillName}: path ${absoluteStagedPath} is not under managed root ${absoluteAllowedRoot}`);
|
|
1919
|
+
}
|
|
1920
|
+
} catch (err) {
|
|
1921
|
+
log(`[skill-sync] Error while trying to verify and remove staged path for ${skillName} (${stagedPath}):`, err);
|
|
1922
|
+
}
|
|
1923
|
+
}
|
|
1924
|
+
function syncBundledSkillsFromPackage(packageRoot, options = {}) {
|
|
1925
|
+
const installed = [];
|
|
1926
|
+
const skippedExisting = [];
|
|
1927
|
+
const failed = [];
|
|
1928
|
+
const staged = [];
|
|
1929
|
+
const adopted = [];
|
|
1930
|
+
const customized = [];
|
|
1931
|
+
const sourceSkillsDir = path2.join(packageRoot, "src", "skills");
|
|
1932
|
+
try {
|
|
1933
|
+
const stat = lstatSync(sourceSkillsDir);
|
|
1934
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
1935
|
+
log(`[skill-sync] Source skills directory is not a valid directory: ${sourceSkillsDir}`);
|
|
1936
|
+
return {
|
|
1937
|
+
installed,
|
|
1938
|
+
skippedExisting,
|
|
1939
|
+
failed,
|
|
1940
|
+
staged,
|
|
1941
|
+
adopted,
|
|
1942
|
+
customized
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
} catch {
|
|
1946
|
+
log(`[skill-sync] Source skills directory does not exist or is unreadable: ${sourceSkillsDir}`);
|
|
1947
|
+
return {
|
|
1948
|
+
installed,
|
|
1949
|
+
skippedExisting,
|
|
1950
|
+
failed,
|
|
1951
|
+
staged,
|
|
1952
|
+
adopted,
|
|
1953
|
+
customized
|
|
1954
|
+
};
|
|
1955
|
+
}
|
|
1956
|
+
let packageVersion = "unknown";
|
|
1957
|
+
try {
|
|
1958
|
+
const pkgJsonPath = path2.join(packageRoot, "package.json");
|
|
1959
|
+
if (existsSync4(pkgJsonPath)) {
|
|
1960
|
+
const content = readFileSync4(pkgJsonPath, "utf-8");
|
|
1961
|
+
const pkg = JSON.parse(content);
|
|
1962
|
+
if (pkg.version) {
|
|
1963
|
+
packageVersion = pkg.version;
|
|
1964
|
+
}
|
|
1965
|
+
}
|
|
1966
|
+
} catch (err) {
|
|
1967
|
+
log(`[skill-sync] Failed to read package version from ${packageRoot}:`, err);
|
|
1968
|
+
}
|
|
1969
|
+
const manifestDir = path2.join(getConfigDir(), ".oh-my-opencode-slim");
|
|
1970
|
+
const lockDir = path2.join(manifestDir, "skills.lock");
|
|
1971
|
+
try {
|
|
1972
|
+
mkdirSync3(manifestDir, { recursive: true });
|
|
1973
|
+
} catch (err) {
|
|
1974
|
+
log(`[skill-sync] Failed to create manifest directory: ${manifestDir}`, err);
|
|
1975
|
+
}
|
|
1976
|
+
if (!acquireLock(lockDir)) {
|
|
1977
|
+
log("[skill-sync] Failed to acquire lock for skill synchronization. Skipping.");
|
|
1978
|
+
return {
|
|
1979
|
+
installed,
|
|
1980
|
+
skippedExisting,
|
|
1981
|
+
failed: ["__lock__"],
|
|
1982
|
+
staged,
|
|
1983
|
+
adopted,
|
|
1984
|
+
customized
|
|
1985
|
+
};
|
|
1986
|
+
}
|
|
1987
|
+
try {
|
|
1988
|
+
const manifestPath = path2.join(manifestDir, "skills-manifest.json");
|
|
1989
|
+
let manifest = {
|
|
1990
|
+
schemaVersion: 1,
|
|
1991
|
+
updatedAt: new Date().toISOString(),
|
|
1992
|
+
skills: {}
|
|
1993
|
+
};
|
|
1994
|
+
let isManifestCorrupt = false;
|
|
1995
|
+
if (existsSync4(manifestPath)) {
|
|
1996
|
+
try {
|
|
1997
|
+
const content = readFileSync4(manifestPath, "utf-8");
|
|
1998
|
+
const parsed = JSON.parse(content);
|
|
1999
|
+
if (validateManifest(parsed)) {
|
|
2000
|
+
manifest = parsed;
|
|
2001
|
+
} else {
|
|
2002
|
+
throw new Error("Manifest validation failed");
|
|
2003
|
+
}
|
|
2004
|
+
} catch (err) {
|
|
2005
|
+
log("[skill-sync] Manifest is corrupt/unreadable. Failing closed.", err);
|
|
2006
|
+
isManifestCorrupt = true;
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
const destSkillsDir = path2.join(getConfigDir(), "skills");
|
|
2010
|
+
try {
|
|
2011
|
+
if (!existsSync4(destSkillsDir)) {
|
|
2012
|
+
mkdirSync3(destSkillsDir, { recursive: true });
|
|
2013
|
+
}
|
|
2014
|
+
} catch (err) {
|
|
2015
|
+
log(`[skill-sync] Failed to create destination skills directory: ${destSkillsDir}`, err);
|
|
2016
|
+
}
|
|
2017
|
+
const skillsToProcess = (options.skills ?? CUSTOM_SKILLS).map((s) => ({
|
|
2018
|
+
name: s.name,
|
|
2019
|
+
sourcePath: s.sourcePath
|
|
2020
|
+
}));
|
|
2021
|
+
for (const skill of skillsToProcess) {
|
|
2022
|
+
try {
|
|
2023
|
+
const sourcePath = path2.join(packageRoot, skill.sourcePath);
|
|
2024
|
+
try {
|
|
2025
|
+
const stat = lstatSync(sourcePath);
|
|
2026
|
+
if (stat.isSymbolicLink() || !stat.isDirectory()) {
|
|
2027
|
+
continue;
|
|
2028
|
+
}
|
|
2029
|
+
const skillMdPath = path2.join(sourcePath, "SKILL.md");
|
|
2030
|
+
const skillMdStat = lstatSync(skillMdPath);
|
|
2031
|
+
if (skillMdStat.isSymbolicLink() || !skillMdStat.isFile()) {
|
|
2032
|
+
continue;
|
|
2033
|
+
}
|
|
2034
|
+
} catch {
|
|
2035
|
+
continue;
|
|
2036
|
+
}
|
|
2037
|
+
const destPath = path2.join(destSkillsDir, skill.name);
|
|
2038
|
+
const hadArtifacts = recoverOrphanArtifacts(destSkillsDir, skill.name);
|
|
2039
|
+
let destExists = false;
|
|
2040
|
+
let destIsDir = false;
|
|
2041
|
+
try {
|
|
2042
|
+
const destStat = lstatSync(destPath);
|
|
2043
|
+
destExists = true;
|
|
2044
|
+
destIsDir = destStat.isDirectory() && !destStat.isSymbolicLink();
|
|
2045
|
+
} catch {}
|
|
2046
|
+
if (destExists && !destIsDir) {
|
|
2047
|
+
log(`[skill-sync] Skill ${skill.name} destination is a file or symlink (conflict). Skipping.`);
|
|
2048
|
+
skippedExisting.push(skill.name);
|
|
2049
|
+
const sourceHash2 = computeDirectoryHash(sourcePath);
|
|
2050
|
+
const entry2 = manifest.skills[skill.name];
|
|
2051
|
+
if (entry2?.stagedPath) {
|
|
2052
|
+
removeManagedStagedPath(entry2.stagedPath, manifestDir, skill.name);
|
|
2053
|
+
}
|
|
2054
|
+
manifest.skills[skill.name] = {
|
|
2055
|
+
status: "conflict",
|
|
2056
|
+
packageVersion,
|
|
2057
|
+
sourceHash: sourceHash2,
|
|
2058
|
+
lastManagedHash: "",
|
|
2059
|
+
lastSeenHash: "",
|
|
2060
|
+
updatedAt: new Date().toISOString()
|
|
2061
|
+
};
|
|
2062
|
+
continue;
|
|
2063
|
+
}
|
|
2064
|
+
const sourceHash = computeDirectoryHash(sourcePath);
|
|
2065
|
+
if (isManifestCorrupt) {
|
|
2066
|
+
if (!destExists) {
|
|
2067
|
+
try {
|
|
2068
|
+
atomicReplaceDir(sourcePath, destPath);
|
|
2069
|
+
installed.push(skill.name);
|
|
2070
|
+
manifest.skills[skill.name] = {
|
|
2071
|
+
status: "managed",
|
|
2072
|
+
packageVersion,
|
|
2073
|
+
sourceHash,
|
|
2074
|
+
lastManagedHash: sourceHash,
|
|
2075
|
+
lastSeenHash: sourceHash,
|
|
2076
|
+
updatedAt: new Date().toISOString()
|
|
2077
|
+
};
|
|
2078
|
+
} catch (err) {
|
|
2079
|
+
log(`[skill-sync] Failed to install missing skill ${skill.name} (corrupt manifest mode):`, err);
|
|
2080
|
+
failed.push(skill.name);
|
|
2081
|
+
}
|
|
2082
|
+
} else {
|
|
2083
|
+
log(`[skill-sync] Skipping existing skill ${skill.name} because manifest is corrupt.`);
|
|
2084
|
+
skippedExisting.push(skill.name);
|
|
2085
|
+
const destHash2 = computeDirectoryHash(destPath);
|
|
2086
|
+
if (destHash2 === sourceHash) {
|
|
2087
|
+
manifest.skills[skill.name] = {
|
|
2088
|
+
status: "managed",
|
|
2089
|
+
packageVersion,
|
|
2090
|
+
sourceHash,
|
|
2091
|
+
lastManagedHash: sourceHash,
|
|
2092
|
+
lastSeenHash: sourceHash,
|
|
2093
|
+
updatedAt: new Date().toISOString()
|
|
2094
|
+
};
|
|
2095
|
+
} else {
|
|
2096
|
+
try {
|
|
2097
|
+
const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
2098
|
+
if (existsSync4(stagedSkillDir)) {
|
|
2099
|
+
rmSync2(stagedSkillDir, { recursive: true, force: true });
|
|
2100
|
+
}
|
|
2101
|
+
mkdirSync3(stagedSkillDir, { recursive: true });
|
|
2102
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
2103
|
+
manifest.skills[skill.name] = {
|
|
2104
|
+
status: "customized",
|
|
2105
|
+
packageVersion,
|
|
2106
|
+
sourceHash,
|
|
2107
|
+
lastManagedHash: "",
|
|
2108
|
+
lastSeenHash: destHash2,
|
|
2109
|
+
stagedPath: stagedSkillDir,
|
|
2110
|
+
updatedAt: new Date().toISOString()
|
|
2111
|
+
};
|
|
2112
|
+
staged.push(skill.name);
|
|
2113
|
+
customized.push(skill.name);
|
|
2114
|
+
} catch (err) {
|
|
2115
|
+
log(`[skill-sync] Failed to stage update for customized skill ${skill.name} during recovery:`, err);
|
|
2116
|
+
manifest.skills[skill.name] = {
|
|
2117
|
+
status: "customized",
|
|
2118
|
+
packageVersion: "unknown",
|
|
2119
|
+
sourceHash: "",
|
|
2120
|
+
lastManagedHash: "",
|
|
2121
|
+
lastSeenHash: destHash2,
|
|
2122
|
+
updatedAt: new Date().toISOString()
|
|
2123
|
+
};
|
|
2124
|
+
}
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
continue;
|
|
2128
|
+
}
|
|
2129
|
+
const entry = manifest.skills[skill.name];
|
|
2130
|
+
if (!destExists) {
|
|
2131
|
+
if (entry && entry.status === "deleted") {
|
|
2132
|
+
log(`[skill-sync] Skill ${skill.name} was deleted by user. Skipping.`);
|
|
2133
|
+
skippedExisting.push(skill.name);
|
|
2134
|
+
continue;
|
|
2135
|
+
}
|
|
2136
|
+
if (entry && entry.status !== "deleted") {
|
|
2137
|
+
if (hadArtifacts) {
|
|
2138
|
+
log(`[skill-sync] Managed skill ${skill.name} has backup/staging artifacts. Skipping delete, re-installing.`);
|
|
2139
|
+
try {
|
|
2140
|
+
atomicReplaceDir(sourcePath, destPath);
|
|
2141
|
+
installed.push(skill.name);
|
|
2142
|
+
manifest.skills[skill.name] = {
|
|
2143
|
+
status: "managed",
|
|
2144
|
+
packageVersion,
|
|
2145
|
+
sourceHash,
|
|
2146
|
+
lastManagedHash: sourceHash,
|
|
2147
|
+
lastSeenHash: sourceHash,
|
|
2148
|
+
updatedAt: new Date().toISOString()
|
|
2149
|
+
};
|
|
2150
|
+
} catch (err) {
|
|
2151
|
+
log(`[skill-sync] Failed to re-install skill ${skill.name}:`, err);
|
|
2152
|
+
failed.push(skill.name);
|
|
2153
|
+
}
|
|
2154
|
+
continue;
|
|
2155
|
+
} else {
|
|
2156
|
+
if (entry.stagedPath) {
|
|
2157
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2158
|
+
delete entry.stagedPath;
|
|
2159
|
+
}
|
|
2160
|
+
const rawEntry = entry;
|
|
2161
|
+
delete rawEntry.stagedVersion;
|
|
2162
|
+
delete rawEntry.stagedHash;
|
|
2163
|
+
entry.status = "deleted";
|
|
2164
|
+
entry.updatedAt = new Date().toISOString();
|
|
2165
|
+
log(`[skill-sync] Skill ${skill.name} was deleted by user (detected now). Skipping.`);
|
|
2166
|
+
skippedExisting.push(skill.name);
|
|
2167
|
+
continue;
|
|
2168
|
+
}
|
|
2169
|
+
}
|
|
2170
|
+
try {
|
|
2171
|
+
atomicReplaceDir(sourcePath, destPath);
|
|
2172
|
+
installed.push(skill.name);
|
|
2173
|
+
manifest.skills[skill.name] = {
|
|
2174
|
+
status: "managed",
|
|
2175
|
+
packageVersion,
|
|
2176
|
+
sourceHash,
|
|
2177
|
+
lastManagedHash: sourceHash,
|
|
2178
|
+
lastSeenHash: sourceHash,
|
|
2179
|
+
updatedAt: new Date().toISOString()
|
|
2180
|
+
};
|
|
2181
|
+
log(`[skill-sync] Successfully installed missing skill: ${skill.name}`);
|
|
2182
|
+
} catch (err) {
|
|
2183
|
+
log(`[skill-sync] Failed to install skill ${skill.name}:`, err);
|
|
2184
|
+
failed.push(skill.name);
|
|
2185
|
+
}
|
|
2186
|
+
continue;
|
|
2187
|
+
}
|
|
2188
|
+
const destHash = computeDirectoryHash(destPath);
|
|
2189
|
+
if (entry) {
|
|
2190
|
+
if (entry.status === "managed") {
|
|
2191
|
+
if (destHash === entry.lastManagedHash) {
|
|
2192
|
+
if (destHash === sourceHash) {
|
|
2193
|
+
entry.packageVersion = packageVersion;
|
|
2194
|
+
entry.sourceHash = sourceHash;
|
|
2195
|
+
entry.lastManagedHash = sourceHash;
|
|
2196
|
+
entry.lastSeenHash = sourceHash;
|
|
2197
|
+
entry.updatedAt = new Date().toISOString();
|
|
2198
|
+
skippedExisting.push(skill.name);
|
|
2199
|
+
} else {
|
|
2200
|
+
try {
|
|
2201
|
+
atomicReplaceDir(sourcePath, destPath);
|
|
2202
|
+
installed.push(skill.name);
|
|
2203
|
+
manifest.skills[skill.name] = {
|
|
2204
|
+
status: "managed",
|
|
2205
|
+
packageVersion,
|
|
2206
|
+
sourceHash,
|
|
2207
|
+
lastManagedHash: sourceHash,
|
|
2208
|
+
lastSeenHash: sourceHash,
|
|
2209
|
+
updatedAt: new Date().toISOString()
|
|
2210
|
+
};
|
|
2211
|
+
log(`[skill-sync] Updated managed skill: ${skill.name}`);
|
|
2212
|
+
} catch (err) {
|
|
2213
|
+
log(`[skill-sync] Failed to update managed skill ${skill.name}:`, err);
|
|
2214
|
+
failed.push(skill.name);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
} else {
|
|
2218
|
+
if (destHash === sourceHash) {
|
|
2219
|
+
manifest.skills[skill.name] = {
|
|
2220
|
+
status: "managed",
|
|
2221
|
+
packageVersion,
|
|
2222
|
+
sourceHash,
|
|
2223
|
+
lastManagedHash: sourceHash,
|
|
2224
|
+
lastSeenHash: sourceHash,
|
|
2225
|
+
updatedAt: new Date().toISOString()
|
|
2226
|
+
};
|
|
2227
|
+
skippedExisting.push(skill.name);
|
|
2228
|
+
} else {
|
|
2229
|
+
try {
|
|
2230
|
+
const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
2231
|
+
if (entry.stagedPath && entry.stagedPath !== stagedSkillDir) {
|
|
2232
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2233
|
+
}
|
|
2234
|
+
if (existsSync4(stagedSkillDir)) {
|
|
2235
|
+
rmSync2(stagedSkillDir, { recursive: true, force: true });
|
|
2236
|
+
}
|
|
2237
|
+
mkdirSync3(stagedSkillDir, { recursive: true });
|
|
2238
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
2239
|
+
entry.status = "customized";
|
|
2240
|
+
entry.lastSeenHash = destHash;
|
|
2241
|
+
entry.stagedPath = stagedSkillDir;
|
|
2242
|
+
entry.sourceHash = sourceHash;
|
|
2243
|
+
entry.packageVersion = packageVersion;
|
|
2244
|
+
entry.updatedAt = new Date().toISOString();
|
|
2245
|
+
staged.push(skill.name);
|
|
2246
|
+
customized.push(skill.name);
|
|
2247
|
+
skippedExisting.push(skill.name);
|
|
2248
|
+
log(`[skill-sync] Skill ${skill.name} is customized. Staged update at ${stagedSkillDir}`);
|
|
2249
|
+
} catch (err) {
|
|
2250
|
+
log(`[skill-sync] Failed to stage update for customized skill ${skill.name}:`, err);
|
|
2251
|
+
failed.push(skill.name);
|
|
2252
|
+
}
|
|
2253
|
+
}
|
|
2254
|
+
}
|
|
2255
|
+
} else if (entry.status === "customized") {
|
|
2256
|
+
if (destHash === sourceHash) {
|
|
2257
|
+
if (entry.stagedPath) {
|
|
2258
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2259
|
+
}
|
|
2260
|
+
entry.status = "managed";
|
|
2261
|
+
entry.lastManagedHash = sourceHash;
|
|
2262
|
+
entry.lastSeenHash = sourceHash;
|
|
2263
|
+
entry.sourceHash = sourceHash;
|
|
2264
|
+
entry.packageVersion = packageVersion;
|
|
2265
|
+
delete entry.stagedPath;
|
|
2266
|
+
entry.updatedAt = new Date().toISOString();
|
|
2267
|
+
adopted.push(skill.name);
|
|
2268
|
+
skippedExisting.push(skill.name);
|
|
2269
|
+
log(`[skill-sync] Customized skill ${skill.name} converged with current version. Adopted back to managed.`);
|
|
2270
|
+
} else {
|
|
2271
|
+
entry.lastSeenHash = destHash;
|
|
2272
|
+
entry.updatedAt = new Date().toISOString();
|
|
2273
|
+
if (destHash !== sourceHash && entry.sourceHash !== sourceHash) {
|
|
2274
|
+
try {
|
|
2275
|
+
const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
2276
|
+
if (entry.stagedPath && entry.stagedPath !== stagedSkillDir) {
|
|
2277
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2278
|
+
}
|
|
2279
|
+
if (existsSync4(stagedSkillDir)) {
|
|
2280
|
+
rmSync2(stagedSkillDir, { recursive: true, force: true });
|
|
2281
|
+
}
|
|
2282
|
+
mkdirSync3(stagedSkillDir, { recursive: true });
|
|
2283
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
2284
|
+
entry.stagedPath = stagedSkillDir;
|
|
2285
|
+
entry.sourceHash = sourceHash;
|
|
2286
|
+
entry.packageVersion = packageVersion;
|
|
2287
|
+
staged.push(skill.name);
|
|
2288
|
+
customized.push(skill.name);
|
|
2289
|
+
skippedExisting.push(skill.name);
|
|
2290
|
+
log(`[skill-sync] Staged new update for customized skill ${skill.name} at ${stagedSkillDir}`);
|
|
2291
|
+
} catch (err) {
|
|
2292
|
+
log(`[skill-sync] Failed to stage update for customized skill ${skill.name}:`, err);
|
|
2293
|
+
failed.push(skill.name);
|
|
2294
|
+
}
|
|
2295
|
+
} else {
|
|
2296
|
+
customized.push(skill.name);
|
|
2297
|
+
skippedExisting.push(skill.name);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
} else if (entry.status === "deleted") {
|
|
2301
|
+
if (destHash === sourceHash) {
|
|
2302
|
+
entry.status = "managed";
|
|
2303
|
+
entry.packageVersion = packageVersion;
|
|
2304
|
+
entry.sourceHash = sourceHash;
|
|
2305
|
+
entry.lastManagedHash = sourceHash;
|
|
2306
|
+
entry.lastSeenHash = sourceHash;
|
|
2307
|
+
entry.updatedAt = new Date().toISOString();
|
|
2308
|
+
skippedExisting.push(skill.name);
|
|
2309
|
+
adopted.push(skill.name);
|
|
2310
|
+
log(`[skill-sync] Skill ${skill.name} re-created by user (matching current). Adopted as managed.`);
|
|
2311
|
+
} else {
|
|
2312
|
+
try {
|
|
2313
|
+
const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
2314
|
+
if (entry.stagedPath && entry.stagedPath !== stagedSkillDir) {
|
|
2315
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2316
|
+
}
|
|
2317
|
+
if (existsSync4(stagedSkillDir)) {
|
|
2318
|
+
rmSync2(stagedSkillDir, { recursive: true, force: true });
|
|
2319
|
+
}
|
|
2320
|
+
mkdirSync3(stagedSkillDir, { recursive: true });
|
|
2321
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
2322
|
+
entry.status = "customized";
|
|
2323
|
+
entry.packageVersion = packageVersion;
|
|
2324
|
+
entry.sourceHash = sourceHash;
|
|
2325
|
+
entry.lastManagedHash = sourceHash;
|
|
2326
|
+
entry.lastSeenHash = destHash;
|
|
2327
|
+
entry.stagedPath = stagedSkillDir;
|
|
2328
|
+
entry.updatedAt = new Date().toISOString();
|
|
2329
|
+
staged.push(skill.name);
|
|
2330
|
+
customized.push(skill.name);
|
|
2331
|
+
skippedExisting.push(skill.name);
|
|
2332
|
+
log(`[skill-sync] Skill ${skill.name} re-created by user (custom). Marked customized and staged.`);
|
|
2333
|
+
} catch (err) {
|
|
2334
|
+
log(`[skill-sync] Failed to stage update for deleted/recreated skill ${skill.name}:`, err);
|
|
2335
|
+
failed.push(skill.name);
|
|
2336
|
+
}
|
|
2337
|
+
}
|
|
2338
|
+
} else if (entry.status === "conflict") {
|
|
2339
|
+
if (destHash === sourceHash) {
|
|
2340
|
+
if (entry.stagedPath) {
|
|
2341
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2342
|
+
}
|
|
2343
|
+
entry.status = "managed";
|
|
2344
|
+
entry.packageVersion = packageVersion;
|
|
2345
|
+
entry.sourceHash = sourceHash;
|
|
2346
|
+
entry.lastManagedHash = sourceHash;
|
|
2347
|
+
entry.lastSeenHash = sourceHash;
|
|
2348
|
+
delete entry.stagedPath;
|
|
2349
|
+
entry.updatedAt = new Date().toISOString();
|
|
2350
|
+
adopted.push(skill.name);
|
|
2351
|
+
} else {
|
|
2352
|
+
try {
|
|
2353
|
+
const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
2354
|
+
if (entry.stagedPath && entry.stagedPath !== stagedSkillDir) {
|
|
2355
|
+
removeManagedStagedPath(entry.stagedPath, manifestDir, skill.name);
|
|
2356
|
+
}
|
|
2357
|
+
if (existsSync4(stagedSkillDir)) {
|
|
2358
|
+
rmSync2(stagedSkillDir, { recursive: true, force: true });
|
|
2359
|
+
}
|
|
2360
|
+
mkdirSync3(stagedSkillDir, { recursive: true });
|
|
2361
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
2362
|
+
entry.status = "customized";
|
|
2363
|
+
entry.packageVersion = packageVersion;
|
|
2364
|
+
entry.sourceHash = sourceHash;
|
|
2365
|
+
entry.lastManagedHash = sourceHash;
|
|
2366
|
+
entry.lastSeenHash = destHash;
|
|
2367
|
+
entry.stagedPath = stagedSkillDir;
|
|
2368
|
+
entry.updatedAt = new Date().toISOString();
|
|
2369
|
+
staged.push(skill.name);
|
|
2370
|
+
customized.push(skill.name);
|
|
2371
|
+
skippedExisting.push(skill.name);
|
|
2372
|
+
log(`[skill-sync] Conflicted skill ${skill.name} recovered as customized and staged at ${stagedSkillDir}`);
|
|
2373
|
+
} catch (err) {
|
|
2374
|
+
log(`[skill-sync] Failed to stage update for conflicted skill ${skill.name}:`, err);
|
|
2375
|
+
failed.push(skill.name);
|
|
2376
|
+
}
|
|
2377
|
+
}
|
|
2378
|
+
}
|
|
2379
|
+
} else {
|
|
2380
|
+
if (destHash === sourceHash) {
|
|
2381
|
+
manifest.skills[skill.name] = {
|
|
2382
|
+
status: "managed",
|
|
2383
|
+
packageVersion,
|
|
2384
|
+
sourceHash,
|
|
2385
|
+
lastManagedHash: sourceHash,
|
|
2386
|
+
lastSeenHash: sourceHash,
|
|
2387
|
+
updatedAt: new Date().toISOString()
|
|
2388
|
+
};
|
|
2389
|
+
skippedExisting.push(skill.name);
|
|
2390
|
+
adopted.push(skill.name);
|
|
2391
|
+
log(`[skill-sync] Adopted existing matching skill: ${skill.name}`);
|
|
2392
|
+
} else if (LEGACY_MANAGED_SKILL_HASHES[skill.name]?.includes(destHash)) {
|
|
2393
|
+
try {
|
|
2394
|
+
atomicReplaceDir(sourcePath, destPath);
|
|
2395
|
+
installed.push(skill.name);
|
|
2396
|
+
manifest.skills[skill.name] = {
|
|
2397
|
+
status: "managed",
|
|
2398
|
+
packageVersion,
|
|
2399
|
+
sourceHash,
|
|
2400
|
+
lastManagedHash: sourceHash,
|
|
2401
|
+
lastSeenHash: sourceHash,
|
|
2402
|
+
updatedAt: new Date().toISOString()
|
|
2403
|
+
};
|
|
2404
|
+
log(`[skill-sync] Adopted and updated legacy skill: ${skill.name}`);
|
|
2405
|
+
} catch (err) {
|
|
2406
|
+
log(`[skill-sync] Failed to update legacy skill ${skill.name}:`, err);
|
|
2407
|
+
failed.push(skill.name);
|
|
2408
|
+
}
|
|
2409
|
+
} else {
|
|
2410
|
+
try {
|
|
2411
|
+
const stagedSkillDir = path2.join(manifestDir, "skill-updates", packageVersion, skill.name);
|
|
2412
|
+
if (existsSync4(stagedSkillDir)) {
|
|
2413
|
+
rmSync2(stagedSkillDir, { recursive: true, force: true });
|
|
2414
|
+
}
|
|
2415
|
+
mkdirSync3(stagedSkillDir, { recursive: true });
|
|
2416
|
+
copyDirRecursive(sourcePath, stagedSkillDir);
|
|
2417
|
+
manifest.skills[skill.name] = {
|
|
2418
|
+
status: "customized",
|
|
2419
|
+
packageVersion,
|
|
2420
|
+
sourceHash,
|
|
2421
|
+
lastManagedHash: "",
|
|
2422
|
+
lastSeenHash: destHash,
|
|
2423
|
+
stagedPath: stagedSkillDir,
|
|
2424
|
+
updatedAt: new Date().toISOString()
|
|
2425
|
+
};
|
|
2426
|
+
staged.push(skill.name);
|
|
2427
|
+
customized.push(skill.name);
|
|
2428
|
+
skippedExisting.push(skill.name);
|
|
2429
|
+
log(`[skill-sync] Skill ${skill.name} is customized (no manifest entry). Staged update at ${stagedSkillDir}`);
|
|
2430
|
+
} catch (err) {
|
|
2431
|
+
log(`[skill-sync] Failed to stage update for customized skill ${skill.name}:`, err);
|
|
2432
|
+
failed.push(skill.name);
|
|
2433
|
+
}
|
|
2434
|
+
}
|
|
2435
|
+
}
|
|
2436
|
+
} catch (err) {
|
|
2437
|
+
log(`[skill-sync] Failed processing skill ${skill.name}:`, err);
|
|
2438
|
+
failed.push(skill.name);
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
let manifestWriteFailed = false;
|
|
2442
|
+
manifest.updatedAt = new Date().toISOString();
|
|
2443
|
+
const tempManifestPath = `${manifestPath}.${Math.random().toString(36).slice(2, 9)}.tmp`;
|
|
2444
|
+
try {
|
|
2445
|
+
writeFileSync2(tempManifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
|
2446
|
+
renameSync2(tempManifestPath, manifestPath);
|
|
2447
|
+
} catch (err) {
|
|
2448
|
+
log("[skill-sync] Failed to write skills manifest atomically:", err);
|
|
2449
|
+
manifestWriteFailed = true;
|
|
2450
|
+
try {
|
|
2451
|
+
if (existsSync4(tempManifestPath)) {
|
|
2452
|
+
unlinkSync(tempManifestPath);
|
|
2453
|
+
}
|
|
2454
|
+
} catch {}
|
|
2455
|
+
}
|
|
2456
|
+
if (manifestWriteFailed) {
|
|
2457
|
+
failed.push("__manifest__");
|
|
2458
|
+
}
|
|
2459
|
+
} finally {
|
|
2460
|
+
releaseLock(lockDir);
|
|
2461
|
+
}
|
|
2462
|
+
return {
|
|
2463
|
+
installed,
|
|
2464
|
+
skippedExisting,
|
|
2465
|
+
failed,
|
|
2466
|
+
staged,
|
|
2467
|
+
adopted,
|
|
2468
|
+
customized
|
|
2469
|
+
};
|
|
2470
|
+
}
|
|
2471
|
+
|
|
2472
|
+
// src/cli/background-subagents.ts
|
|
2473
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readFileSync as readFileSync5, writeFileSync as writeFileSync3 } from "node:fs";
|
|
2474
|
+
import { homedir as homedir3 } from "node:os";
|
|
2475
|
+
import { dirname as dirname4, join as join5 } from "node:path";
|
|
2476
|
+
var ENV_NAME = "OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS";
|
|
2477
|
+
var START_MARKER = "# >>> oh-my-opencode-slim background subagents >>>";
|
|
2478
|
+
var END_MARKER = "# <<< oh-my-opencode-slim background subagents <<<";
|
|
2479
|
+
function isBackgroundSubagentsEnabled(value) {
|
|
2480
|
+
if (!value)
|
|
2481
|
+
return false;
|
|
2482
|
+
const normalized = value.trim().toLowerCase();
|
|
2483
|
+
return normalized !== "" && !["0", "false", "no", "off"].includes(normalized);
|
|
2484
|
+
}
|
|
2485
|
+
function detectShellKind(shell) {
|
|
2486
|
+
const name = shell?.split("/").at(-1);
|
|
2487
|
+
if (name === "zsh" || name === "bash" || name === "fish")
|
|
2488
|
+
return name;
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
function detectBackgroundSubagentsTarget(env = process.env) {
|
|
2492
|
+
const shell = detectShellKind(env.SHELL);
|
|
2493
|
+
const home = env.HOME || homedir3();
|
|
2494
|
+
if (shell === "zsh")
|
|
2495
|
+
return join5(home, ".zshrc");
|
|
2496
|
+
if (shell === "bash")
|
|
2497
|
+
return join5(home, ".bashrc");
|
|
2498
|
+
if (shell === "fish") {
|
|
2499
|
+
const configHome = env.XDG_CONFIG_HOME || join5(home, ".config");
|
|
2500
|
+
return join5(configHome, "fish", "conf.d", "opencode-background-subagents.fish");
|
|
2501
|
+
}
|
|
2502
|
+
return;
|
|
2503
|
+
}
|
|
2504
|
+
function getBackgroundSubagentsBlock(targetPath) {
|
|
2505
|
+
const isFish = targetPath.endsWith(".fish");
|
|
2506
|
+
const command = isFish ? `set -gx ${ENV_NAME} true` : `export ${ENV_NAME}=true`;
|
|
2507
|
+
return `${START_MARKER}
|
|
2508
|
+
${command}
|
|
2509
|
+
${END_MARKER}`;
|
|
2510
|
+
}
|
|
2511
|
+
function manualBackgroundSubagentsInstructions(options) {
|
|
2512
|
+
const shell = options?.shell ?? (options?.targetPath?.endsWith(".fish") ? "fish" : undefined) ?? detectShellKind(options?.targetPath);
|
|
2513
|
+
const bashZshSnippet = `export ${ENV_NAME}=true`;
|
|
2514
|
+
const fishSnippet = `set -gx ${ENV_NAME} true`;
|
|
2515
|
+
if (shell === "fish") {
|
|
2516
|
+
return `Start OpenCode with background subagents enabled:
|
|
2517
|
+
env ${ENV_NAME}=true opencode
|
|
2518
|
+
|
|
2519
|
+
Or add this to your fish startup file:
|
|
2520
|
+
${fishSnippet}`;
|
|
2521
|
+
}
|
|
2522
|
+
if (shell === "bash" || shell === "zsh") {
|
|
2523
|
+
return `Start OpenCode with background subagents enabled:
|
|
2524
|
+
${ENV_NAME}=true opencode
|
|
2525
|
+
|
|
2526
|
+
Or add this to your shell startup file:
|
|
2527
|
+
${bashZshSnippet}`;
|
|
2528
|
+
}
|
|
2529
|
+
return `Start OpenCode with background subagents enabled:
|
|
2530
|
+
${ENV_NAME}=true opencode
|
|
2531
|
+
|
|
2532
|
+
Or add one of these to your shell startup file:
|
|
2533
|
+
bash/zsh: ${bashZshSnippet}
|
|
2534
|
+
fish: ${fishSnippet}`;
|
|
2535
|
+
}
|
|
2536
|
+
function expandHomePath(targetPath) {
|
|
2537
|
+
if (targetPath === "~")
|
|
2538
|
+
return homedir3();
|
|
2539
|
+
if (targetPath.startsWith("~/"))
|
|
2540
|
+
return join5(homedir3(), targetPath.slice(2));
|
|
2541
|
+
return targetPath;
|
|
2542
|
+
}
|
|
2543
|
+
function upsertBackgroundSubagentsBlock(content, block) {
|
|
2544
|
+
const start = content.indexOf(START_MARKER);
|
|
2545
|
+
const end = content.indexOf(END_MARKER);
|
|
2546
|
+
if (start !== -1 && end !== -1 && end > start) {
|
|
2547
|
+
const afterEnd = end + END_MARKER.length;
|
|
2548
|
+
return `${content.slice(0, start)}${block}${content.slice(afterEnd)}`;
|
|
2549
|
+
}
|
|
2550
|
+
const separator = content.length > 0 && !content.endsWith(`
|
|
2551
|
+
`) ? `
|
|
2552
|
+
|
|
2553
|
+
` : "";
|
|
2554
|
+
const prefix = content.length > 0 && content.endsWith(`
|
|
2555
|
+
`) ? `
|
|
2556
|
+
` : separator;
|
|
2557
|
+
return `${content}${prefix}${block}
|
|
2558
|
+
`;
|
|
2559
|
+
}
|
|
2560
|
+
function writeBackgroundSubagentsBlock(targetPath) {
|
|
2561
|
+
const block = getBackgroundSubagentsBlock(targetPath);
|
|
2562
|
+
const content = existsSync5(targetPath) ? readFileSync5(targetPath, "utf8") : "";
|
|
2563
|
+
const nextContent = upsertBackgroundSubagentsBlock(content, block);
|
|
2564
|
+
mkdirSync4(dirname4(targetPath), { recursive: true });
|
|
2565
|
+
writeFileSync3(targetPath, nextContent);
|
|
2566
|
+
}
|
|
2567
|
+
|
|
2568
|
+
// src/companion/updater.ts
|
|
2569
|
+
init_compat();
|
|
2570
|
+
import { createHash as createHash2 } from "node:crypto";
|
|
2571
|
+
import {
|
|
2572
|
+
chmodSync,
|
|
2573
|
+
copyFileSync as copyFileSync3,
|
|
2574
|
+
existsSync as existsSync6,
|
|
2575
|
+
mkdirSync as mkdirSync5,
|
|
2576
|
+
mkdtempSync,
|
|
2577
|
+
readFileSync as readFileSync6,
|
|
2578
|
+
renameSync as renameSync3,
|
|
2579
|
+
rmSync as rmSync3,
|
|
2580
|
+
statSync as statSync3,
|
|
2581
|
+
writeFileSync as writeFileSync4
|
|
2582
|
+
} from "node:fs";
|
|
2583
|
+
import { homedir as homedir4, platform, tmpdir } from "node:os";
|
|
2584
|
+
import * as path3 from "node:path";
|
|
2585
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2586
|
+
var DOWNLOAD_TIMEOUT_MS = 30000;
|
|
2587
|
+
var LOCK_TIMEOUT_MS = 2000;
|
|
2588
|
+
var STALE_LOCK_MS = 5 * 60000;
|
|
2589
|
+
var FIRST_METADATA_VERSION = "0.1.2";
|
|
2590
|
+
var COMPANION_MANIFEST = {
|
|
2591
|
+
version: "0.1.3",
|
|
2592
|
+
tag: "companion-v0.1.3",
|
|
2593
|
+
repo: "alvinunreal/oh-my-opencode-slim",
|
|
2594
|
+
checksums: {
|
|
2595
|
+
"oh-my-opencode-slim-companion-v0.1.3-aarch64-apple-darwin.tar.gz": "b4885f9b1900c02376e5f8f5ae6f3b8a89d26f7514b03f836d7e3d618164a0ed",
|
|
2596
|
+
"oh-my-opencode-slim-companion-v0.1.3-aarch64-unknown-linux-gnu.tar.gz": "ed7cffc583e1eaa78c9bea702e6b6aa3bbc5bb4d881713fb2050237ba6b7aca5",
|
|
2597
|
+
"oh-my-opencode-slim-companion-v0.1.3-x86_64-apple-darwin.tar.gz": "98d8ea7c7bc4415b18e0d4c524adb4eb9a84c872919840fdc021f0f50c61f808",
|
|
2598
|
+
"oh-my-opencode-slim-companion-v0.1.3-x86_64-pc-windows-msvc.zip": "9316a49bf01f3b4fb1ce2d62edfc46094e73bb153d6ce023fb7df085afcf77bd",
|
|
2599
|
+
"oh-my-opencode-slim-companion-v0.1.3-x86_64-unknown-linux-gnu.tar.gz": "33f5fd4b6c80155a019391e5efb13904ca9531ba8dd8c6cba30a161f1b07b764"
|
|
2600
|
+
}
|
|
2601
|
+
};
|
|
2602
|
+
function getCompanionTarget() {
|
|
2603
|
+
const p = process.platform;
|
|
2604
|
+
const a = process.arch;
|
|
2605
|
+
if (p === "darwin") {
|
|
2606
|
+
if (a === "arm64")
|
|
2607
|
+
return "aarch64-apple-darwin";
|
|
2608
|
+
if (a === "x64")
|
|
2609
|
+
return "x86_64-apple-darwin";
|
|
2610
|
+
} else if (p === "linux") {
|
|
2611
|
+
if (a === "x64")
|
|
2612
|
+
return "x86_64-unknown-linux-gnu";
|
|
2613
|
+
if (a === "arm64")
|
|
2614
|
+
return "aarch64-unknown-linux-gnu";
|
|
2615
|
+
} else if (p === "win32") {
|
|
2616
|
+
if (a === "x64")
|
|
2617
|
+
return "x86_64-pc-windows-msvc";
|
|
2618
|
+
}
|
|
2619
|
+
return null;
|
|
2620
|
+
}
|
|
2621
|
+
function getCompanionBinaryPath() {
|
|
2622
|
+
const xdg = process.env.XDG_DATA_HOME?.trim();
|
|
2623
|
+
const base = xdg && path3.isAbsolute(xdg) ? xdg : path3.join(homedir4(), ".local", "share");
|
|
2624
|
+
return path3.join(base, "opencode", "storage", "oh-my-opencode-slim", "bin", platform() === "win32" ? "oh-my-opencode-slim-companion.exe" : "oh-my-opencode-slim-companion");
|
|
2625
|
+
}
|
|
2626
|
+
async function ensureCompanionVersion(options) {
|
|
2627
|
+
const { config, dryRun = false } = options;
|
|
2628
|
+
const manifest = options.manifest ?? COMPANION_MANIFEST;
|
|
2629
|
+
const binaryPath = getCompanionBinaryPath();
|
|
2630
|
+
if (config?.enabled !== true) {
|
|
2631
|
+
return { status: "skipped", reason: "disabled", binaryPath };
|
|
2632
|
+
}
|
|
2633
|
+
if (config.binaryPath?.trim()) {
|
|
2634
|
+
return { status: "skipped", reason: "custom-binary", binaryPath };
|
|
2635
|
+
}
|
|
2636
|
+
const target = getCompanionTarget();
|
|
2637
|
+
if (!target) {
|
|
2638
|
+
return {
|
|
2639
|
+
status: "failed",
|
|
2640
|
+
binaryPath,
|
|
2641
|
+
error: `Unsupported platform/architecture: ${process.platform} ${process.arch}`
|
|
2642
|
+
};
|
|
2643
|
+
}
|
|
2644
|
+
const current = readInstallMetadata(binaryPath);
|
|
2645
|
+
if (existsSync6(binaryPath) && !current && manifest.version === FIRST_METADATA_VERSION) {
|
|
2646
|
+
const archiveName = companionArchiveName(manifest.version, target);
|
|
2647
|
+
writeInstallMetadata(binaryPath, {
|
|
2648
|
+
version: manifest.version,
|
|
2649
|
+
tag: manifest.tag,
|
|
2650
|
+
target,
|
|
2651
|
+
installedAt: new Date().toISOString(),
|
|
2652
|
+
archiveName,
|
|
2653
|
+
checksum: manifest.checksums?.[archiveName]
|
|
2654
|
+
});
|
|
2655
|
+
return { status: "current", binaryPath, version: manifest.version };
|
|
2656
|
+
}
|
|
2657
|
+
if (existsSync6(binaryPath) && current?.target === target && compareSemver(current.version, manifest.version) >= 0) {
|
|
2658
|
+
return { status: "current", binaryPath, version: current.version };
|
|
2659
|
+
}
|
|
2660
|
+
if (dryRun) {
|
|
2661
|
+
return { status: "installed", binaryPath, version: manifest.version };
|
|
2662
|
+
}
|
|
2663
|
+
return withCompanionInstallLock(binaryPath, options.lockTimeoutMs, options.lockStaleMs, async () => {
|
|
2664
|
+
const lockedCurrent = readInstallMetadata(binaryPath);
|
|
2665
|
+
if (existsSync6(binaryPath) && !lockedCurrent && manifest.version === FIRST_METADATA_VERSION) {
|
|
2666
|
+
const archiveName = companionArchiveName(manifest.version, target);
|
|
2667
|
+
writeInstallMetadata(binaryPath, {
|
|
2668
|
+
version: manifest.version,
|
|
2669
|
+
tag: manifest.tag,
|
|
2670
|
+
target,
|
|
2671
|
+
installedAt: new Date().toISOString(),
|
|
2672
|
+
archiveName,
|
|
2673
|
+
checksum: manifest.checksums?.[archiveName]
|
|
2674
|
+
});
|
|
2675
|
+
return { status: "current", binaryPath, version: manifest.version };
|
|
2676
|
+
}
|
|
2677
|
+
if (existsSync6(binaryPath) && lockedCurrent?.target === target && compareSemver(lockedCurrent.version, manifest.version) >= 0) {
|
|
2678
|
+
return {
|
|
2679
|
+
status: "current",
|
|
2680
|
+
binaryPath,
|
|
2681
|
+
version: lockedCurrent.version
|
|
2682
|
+
};
|
|
2683
|
+
}
|
|
2684
|
+
return installCompanionArchive(binaryPath, target, manifest, options.downloadTimeoutMs ?? DOWNLOAD_TIMEOUT_MS);
|
|
2685
|
+
});
|
|
2686
|
+
}
|
|
2687
|
+
async function installCompanionArchive(finalBinaryPath, target, manifest, downloadTimeoutMs) {
|
|
2688
|
+
const isWindows = process.platform === "win32";
|
|
2689
|
+
const archiveName = companionArchiveName(manifest.version, target, isWindows);
|
|
2690
|
+
const downloadUrl = `https://github.com/${manifest.repo}/releases/download/${manifest.tag}/${archiveName}`;
|
|
2691
|
+
const expectedChecksum = manifest.checksums?.[archiveName];
|
|
2692
|
+
if (!expectedChecksum) {
|
|
2693
|
+
return {
|
|
2694
|
+
status: "failed",
|
|
2695
|
+
binaryPath: finalBinaryPath,
|
|
2696
|
+
error: `Missing SHA256 checksum for companion archive: ${archiveName}`
|
|
2697
|
+
};
|
|
2698
|
+
}
|
|
2699
|
+
let buffer;
|
|
2700
|
+
const controller = new AbortController;
|
|
2701
|
+
const timeout = setTimeout(() => controller.abort(), downloadTimeoutMs);
|
|
2702
|
+
try {
|
|
2703
|
+
const res = await fetch(downloadUrl, { signal: controller.signal });
|
|
2704
|
+
if (!res.ok) {
|
|
2705
|
+
return {
|
|
2706
|
+
status: "failed",
|
|
2707
|
+
binaryPath: finalBinaryPath,
|
|
2708
|
+
error: `Failed to download companion binary (HTTP ${res.status}): ${res.statusText}`
|
|
2709
|
+
};
|
|
2710
|
+
}
|
|
2711
|
+
buffer = await res.arrayBuffer();
|
|
2712
|
+
} catch (err) {
|
|
2713
|
+
return {
|
|
2714
|
+
status: "failed",
|
|
2715
|
+
binaryPath: finalBinaryPath,
|
|
2716
|
+
error: `Failed to fetch companion archive: ${formatError(err)}`
|
|
2717
|
+
};
|
|
2718
|
+
} finally {
|
|
2719
|
+
clearTimeout(timeout);
|
|
2720
|
+
}
|
|
2721
|
+
const checksum = createHash2("sha256").update(Buffer.from(buffer)).digest("hex");
|
|
2722
|
+
if (checksum !== expectedChecksum) {
|
|
2723
|
+
return {
|
|
2724
|
+
status: "failed",
|
|
2725
|
+
binaryPath: finalBinaryPath,
|
|
2726
|
+
error: "Companion archive checksum mismatch"
|
|
2727
|
+
};
|
|
2728
|
+
}
|
|
2729
|
+
let tempDir = "";
|
|
2730
|
+
try {
|
|
2731
|
+
tempDir = mkdtempSync(path3.join(tmpdir(), "companion-install-"));
|
|
2732
|
+
const archivePath = path3.join(tempDir, archiveName);
|
|
2733
|
+
writeFileSync4(archivePath, Buffer.from(buffer));
|
|
2734
|
+
const extractedDir = path3.join(tempDir, "extracted");
|
|
2735
|
+
mkdirSync5(extractedDir, { recursive: true });
|
|
2736
|
+
if (isWindows) {
|
|
2737
|
+
const { extractZip: extractZip2 } = await Promise.resolve().then(() => (init_zip_extractor(), exports_zip_extractor));
|
|
2738
|
+
await extractZip2(archivePath, extractedDir);
|
|
2739
|
+
} else {
|
|
2740
|
+
const proc = crossSpawn(["tar", "-xzf", archivePath, "-C", extractedDir]);
|
|
2741
|
+
const exitCode = await proc.exited;
|
|
2742
|
+
if (exitCode !== 0) {
|
|
2743
|
+
const stderr = await proc.stderr();
|
|
2744
|
+
return {
|
|
2745
|
+
status: "failed",
|
|
2746
|
+
binaryPath: finalBinaryPath,
|
|
2747
|
+
error: `Archive extraction failed (tar exited with ${exitCode}): ${stderr}`
|
|
2748
|
+
};
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
const binaryName = isWindows ? "oh-my-opencode-slim-companion.exe" : "oh-my-opencode-slim-companion";
|
|
2752
|
+
const extractedBinaryPath = path3.join(extractedDir, binaryName);
|
|
2753
|
+
if (!existsSync6(extractedBinaryPath)) {
|
|
2754
|
+
return {
|
|
2755
|
+
status: "failed",
|
|
2756
|
+
binaryPath: finalBinaryPath,
|
|
2757
|
+
error: `Binary ${binaryName} not found in extracted archive`
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
const binDir = path3.dirname(finalBinaryPath);
|
|
2761
|
+
mkdirSync5(binDir, { recursive: true });
|
|
2762
|
+
const tmpFinalPath = `${finalBinaryPath}.tmp`;
|
|
2763
|
+
copyFileSync3(extractedBinaryPath, tmpFinalPath);
|
|
2764
|
+
if (!isWindows) {
|
|
2765
|
+
chmodSync(tmpFinalPath, 493);
|
|
2766
|
+
}
|
|
2767
|
+
renameSync3(tmpFinalPath, finalBinaryPath);
|
|
2768
|
+
writeInstallMetadata(finalBinaryPath, {
|
|
2769
|
+
version: manifest.version,
|
|
2770
|
+
tag: manifest.tag,
|
|
2771
|
+
target,
|
|
2772
|
+
installedAt: new Date().toISOString(),
|
|
2773
|
+
archiveName,
|
|
2774
|
+
checksum
|
|
2775
|
+
});
|
|
2776
|
+
return {
|
|
2777
|
+
status: "installed",
|
|
2778
|
+
binaryPath: finalBinaryPath,
|
|
2779
|
+
version: manifest.version
|
|
2780
|
+
};
|
|
2781
|
+
} catch (err) {
|
|
2782
|
+
return {
|
|
2783
|
+
status: "failed",
|
|
2784
|
+
binaryPath: finalBinaryPath,
|
|
2785
|
+
error: `Failed to install companion: ${formatError(err)}`
|
|
2786
|
+
};
|
|
2787
|
+
} finally {
|
|
2788
|
+
if (tempDir) {
|
|
2789
|
+
try {
|
|
2790
|
+
rmSync3(tempDir, { recursive: true, force: true });
|
|
2791
|
+
} catch (err) {
|
|
2792
|
+
log("[updater] install cleanup failed", String(err));
|
|
2793
|
+
}
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
}
|
|
2797
|
+
function readInstallMetadata(binaryPath) {
|
|
2798
|
+
try {
|
|
2799
|
+
const parsed = JSON.parse(readFileSync6(metadataPath(binaryPath), "utf8"));
|
|
2800
|
+
if (parsed?.version && parsed.tag && parsed.target) {
|
|
2801
|
+
return parsed;
|
|
2802
|
+
}
|
|
2803
|
+
} catch (err) {
|
|
2804
|
+
log("[updater] metadata read failed", String(err));
|
|
2805
|
+
}
|
|
2806
|
+
return null;
|
|
2807
|
+
}
|
|
2808
|
+
function writeInstallMetadata(binaryPath, metadata) {
|
|
2809
|
+
writeFileSync4(metadataPath(binaryPath), JSON.stringify(metadata, null, 2));
|
|
2810
|
+
}
|
|
2811
|
+
function metadataPath(binaryPath) {
|
|
2812
|
+
return `${binaryPath}.json`;
|
|
2813
|
+
}
|
|
2814
|
+
async function withCompanionInstallLock(binaryPath, timeoutMs, staleMs, run) {
|
|
2815
|
+
const lock = `${binaryPath}.lock`;
|
|
2816
|
+
const deadline = Date.now() + (timeoutMs ?? LOCK_TIMEOUT_MS);
|
|
2817
|
+
const staleAfterMs = staleMs ?? STALE_LOCK_MS;
|
|
2818
|
+
mkdirSync5(path3.dirname(binaryPath), { recursive: true });
|
|
2819
|
+
while (Date.now() <= deadline) {
|
|
2820
|
+
try {
|
|
2821
|
+
mkdirSync5(lock);
|
|
2822
|
+
try {
|
|
2823
|
+
return await run();
|
|
2824
|
+
} finally {
|
|
2825
|
+
try {
|
|
2826
|
+
rmSync3(lock, { recursive: true, force: true });
|
|
2827
|
+
} catch (err) {
|
|
2828
|
+
log("[updater] lock release failed", String(err));
|
|
2829
|
+
}
|
|
2830
|
+
}
|
|
2831
|
+
} catch (err) {
|
|
2832
|
+
const code = err.code;
|
|
2833
|
+
if (code !== "EEXIST")
|
|
2834
|
+
throw err;
|
|
2835
|
+
try {
|
|
2836
|
+
const ageMs = Date.now() - statSync3(lock).mtimeMs;
|
|
2837
|
+
if (ageMs > staleAfterMs) {
|
|
2838
|
+
rmSync3(lock, { recursive: true, force: true });
|
|
2839
|
+
log("[companion] removed stale install lock", lock);
|
|
2840
|
+
continue;
|
|
2841
|
+
}
|
|
2842
|
+
} catch (statErr) {
|
|
2843
|
+
const statCode = statErr.code;
|
|
2844
|
+
if (statCode !== "ENOENT")
|
|
2845
|
+
throw statErr;
|
|
2846
|
+
}
|
|
2847
|
+
await delay(25);
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
log("[companion] install lock timed out", lock);
|
|
2851
|
+
return {
|
|
2852
|
+
status: "failed",
|
|
2853
|
+
binaryPath,
|
|
2854
|
+
error: "Timed out waiting for companion install lock"
|
|
2855
|
+
};
|
|
2856
|
+
}
|
|
2857
|
+
function companionArchiveName(version, target, isWindows = process.platform === "win32") {
|
|
2858
|
+
const ext = isWindows ? "zip" : "tar.gz";
|
|
2859
|
+
return `oh-my-opencode-slim-companion-v${version}-${target}.${ext}`;
|
|
2860
|
+
}
|
|
2861
|
+
function compareSemver(a, b) {
|
|
2862
|
+
const left = parseSemver(a);
|
|
2863
|
+
const right = parseSemver(b);
|
|
2864
|
+
if (!left || !right)
|
|
2865
|
+
return a.localeCompare(b);
|
|
2866
|
+
for (let i = 0;i < 3; i++) {
|
|
2867
|
+
const diff = left[i] - right[i];
|
|
2868
|
+
if (diff !== 0)
|
|
2869
|
+
return diff;
|
|
2870
|
+
}
|
|
2871
|
+
return 0;
|
|
2872
|
+
}
|
|
2873
|
+
function parseSemver(version) {
|
|
2874
|
+
const match = version.match(/^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/);
|
|
2875
|
+
if (!match)
|
|
2876
|
+
return null;
|
|
2877
|
+
return [Number(match[1]), Number(match[2]), Number(match[3])];
|
|
2878
|
+
}
|
|
2879
|
+
function formatError(err) {
|
|
2880
|
+
return err instanceof Error ? err.message : String(err);
|
|
2881
|
+
}
|
|
2882
|
+
|
|
2883
|
+
// src/cli/companion.ts
|
|
2884
|
+
async function installCompanion(config) {
|
|
2885
|
+
const target = getCompanionTarget();
|
|
2886
|
+
const finalBinaryPath = getCompanionBinaryPath();
|
|
2887
|
+
if (!target) {
|
|
2888
|
+
return {
|
|
2889
|
+
success: false,
|
|
2890
|
+
configPath: finalBinaryPath,
|
|
2891
|
+
error: `Unsupported platform/architecture: ${process.platform} ${process.arch}`
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
const ext = process.platform === "win32" ? "zip" : "tar.gz";
|
|
2895
|
+
const archiveName = `oh-my-opencode-slim-companion-v${COMPANION_MANIFEST.version}-${target}.${ext}`;
|
|
2896
|
+
const downloadUrl = `https://github.com/${COMPANION_MANIFEST.repo}/releases/download/${COMPANION_MANIFEST.tag}/${archiveName}`;
|
|
2897
|
+
if (config.dryRun) {
|
|
2898
|
+
console.log(` [dry-run] Detected companion target: ${target}`);
|
|
2899
|
+
console.log(` [dry-run] Would download archive: ${downloadUrl}`);
|
|
2900
|
+
console.log(` [dry-run] Would extract and install to: ${finalBinaryPath}`);
|
|
2901
|
+
return {
|
|
2902
|
+
success: true,
|
|
2903
|
+
configPath: finalBinaryPath
|
|
2904
|
+
};
|
|
2905
|
+
}
|
|
2906
|
+
const result = await ensureCompanionVersion({
|
|
2907
|
+
config: { enabled: true },
|
|
2908
|
+
manifest: COMPANION_MANIFEST
|
|
2909
|
+
});
|
|
2910
|
+
if (result.status === "installed" || result.status === "current") {
|
|
2911
|
+
return {
|
|
2912
|
+
success: true,
|
|
2913
|
+
configPath: finalBinaryPath
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
return {
|
|
2917
|
+
success: false,
|
|
2918
|
+
configPath: finalBinaryPath,
|
|
2919
|
+
error: result.status === "failed" ? result.error : `Companion install skipped: ${result.reason}`
|
|
2920
|
+
};
|
|
2921
|
+
}
|
|
2922
|
+
// src/cli/system.ts
|
|
2923
|
+
init_compat();
|
|
2924
|
+
import { spawnSync as spawnSync2 } from "node:child_process";
|
|
2925
|
+
import { statSync as statSync4 } from "node:fs";
|
|
2926
|
+
var cachedOpenCodePath = null;
|
|
2927
|
+
function resolvePathCommand(command) {
|
|
2928
|
+
try {
|
|
2929
|
+
const resolver = process.platform === "win32" ? "where" : "which";
|
|
2930
|
+
const result = spawnSync2(resolver, [command], {
|
|
2931
|
+
encoding: "utf-8",
|
|
2932
|
+
stdio: ["ignore", "pipe", "ignore"]
|
|
2933
|
+
});
|
|
2934
|
+
if (result.status !== 0) {
|
|
2935
|
+
return null;
|
|
2936
|
+
}
|
|
2937
|
+
const resolved = result.stdout.split(/\r?\n/).map((line) => line.trim()).find(Boolean);
|
|
2938
|
+
return resolved ?? null;
|
|
2939
|
+
} catch {
|
|
2940
|
+
return null;
|
|
2941
|
+
}
|
|
2942
|
+
}
|
|
2943
|
+
function canExecute(command, args) {
|
|
2944
|
+
try {
|
|
2945
|
+
const result = spawnSync2(command, args, {
|
|
2946
|
+
stdio: "ignore"
|
|
2947
|
+
});
|
|
2948
|
+
return result.status === 0;
|
|
2949
|
+
} catch {
|
|
2950
|
+
return false;
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2953
|
+
function getOpenCodePaths() {
|
|
2954
|
+
const home = process.env.HOME || process.env.USERPROFILE || "";
|
|
2955
|
+
return [
|
|
2956
|
+
"opencode",
|
|
2957
|
+
`${home}/.local/bin/opencode`,
|
|
2958
|
+
`${home}/.opencode/bin/opencode`,
|
|
2959
|
+
`${home}/bin/opencode`,
|
|
2960
|
+
"/usr/local/bin/opencode",
|
|
2961
|
+
"/opt/opencode/bin/opencode",
|
|
2962
|
+
"/usr/bin/opencode",
|
|
2963
|
+
"/bin/opencode",
|
|
2964
|
+
"/Applications/OpenCode.app/Contents/MacOS/opencode",
|
|
2965
|
+
`${home}/Applications/OpenCode.app/Contents/MacOS/opencode`,
|
|
2966
|
+
"/opt/homebrew/bin/opencode",
|
|
2967
|
+
"/home/linuxbrew/.linuxbrew/bin/opencode",
|
|
2968
|
+
`${home}/homebrew/bin/opencode`,
|
|
2969
|
+
`${home}/Library/Application Support/opencode/bin/opencode`,
|
|
2970
|
+
"/snap/bin/opencode",
|
|
2971
|
+
"/var/snap/opencode/current/bin/opencode",
|
|
2972
|
+
"/var/lib/flatpak/exports/bin/ai.opencode.OpenCode",
|
|
2973
|
+
`${home}/.local/share/flatpak/exports/bin/ai.opencode.OpenCode`,
|
|
2974
|
+
"/nix/store/opencode/bin/opencode",
|
|
2975
|
+
`${home}/.nix-profile/bin/opencode`,
|
|
2976
|
+
"/run/current-system/sw/bin/opencode",
|
|
2977
|
+
`${home}/.cargo/bin/opencode`,
|
|
2978
|
+
`${home}/.npm-global/bin/opencode`,
|
|
2979
|
+
"/usr/local/lib/node_modules/opencode/bin/opencode",
|
|
2980
|
+
`${home}/.yarn/bin/opencode`,
|
|
2981
|
+
`${home}/.pnpm-global/bin/opencode`
|
|
2982
|
+
];
|
|
2983
|
+
}
|
|
2984
|
+
function resolveOpenCodePath() {
|
|
2985
|
+
if (cachedOpenCodePath) {
|
|
2986
|
+
return cachedOpenCodePath;
|
|
2987
|
+
}
|
|
2988
|
+
const pathOpenCodePath = resolvePathCommand("opencode");
|
|
2989
|
+
if (pathOpenCodePath) {
|
|
2990
|
+
cachedOpenCodePath = pathOpenCodePath;
|
|
2991
|
+
return pathOpenCodePath;
|
|
2992
|
+
}
|
|
2993
|
+
const paths = getOpenCodePaths();
|
|
2994
|
+
for (const opencodePath of paths) {
|
|
2995
|
+
if (opencodePath === "opencode")
|
|
2996
|
+
continue;
|
|
2997
|
+
try {
|
|
2998
|
+
const stat = statSync4(opencodePath);
|
|
2999
|
+
if (stat.isFile()) {
|
|
3000
|
+
cachedOpenCodePath = opencodePath;
|
|
3001
|
+
return opencodePath;
|
|
3002
|
+
}
|
|
3003
|
+
} catch {}
|
|
3004
|
+
}
|
|
3005
|
+
return "opencode";
|
|
3006
|
+
}
|
|
3007
|
+
async function isOpenCodeInstalled() {
|
|
3008
|
+
const pathOpenCodePath = resolvePathCommand("opencode");
|
|
3009
|
+
if (pathOpenCodePath && canExecute(pathOpenCodePath, ["--version"])) {
|
|
3010
|
+
cachedOpenCodePath = pathOpenCodePath;
|
|
3011
|
+
return true;
|
|
3012
|
+
}
|
|
3013
|
+
const paths = getOpenCodePaths();
|
|
3014
|
+
for (const opencodePath of paths) {
|
|
3015
|
+
if (opencodePath === "opencode")
|
|
3016
|
+
continue;
|
|
3017
|
+
try {
|
|
3018
|
+
const proc = crossSpawn([opencodePath, "--version"], {
|
|
3019
|
+
stdout: "pipe",
|
|
3020
|
+
stderr: "pipe"
|
|
3021
|
+
});
|
|
3022
|
+
await proc.exited;
|
|
3023
|
+
if (proc.exitCode === 0) {
|
|
3024
|
+
cachedOpenCodePath = opencodePath;
|
|
3025
|
+
return true;
|
|
3026
|
+
}
|
|
3027
|
+
} catch {}
|
|
3028
|
+
}
|
|
3029
|
+
return false;
|
|
3030
|
+
}
|
|
3031
|
+
async function getOpenCodeVersion() {
|
|
3032
|
+
const opencodePath = resolveOpenCodePath();
|
|
3033
|
+
try {
|
|
3034
|
+
const proc = crossSpawn([opencodePath, "--version"], {
|
|
3035
|
+
stdout: "pipe",
|
|
3036
|
+
stderr: "pipe"
|
|
3037
|
+
});
|
|
3038
|
+
const outputPromise = proc.stdout();
|
|
3039
|
+
await proc.exited;
|
|
3040
|
+
if (proc.exitCode === 0) {
|
|
3041
|
+
return (await outputPromise).trim();
|
|
3042
|
+
}
|
|
3043
|
+
} catch {}
|
|
3044
|
+
return null;
|
|
3045
|
+
}
|
|
3046
|
+
function getOpenCodePath() {
|
|
3047
|
+
const path4 = resolveOpenCodePath();
|
|
3048
|
+
return path4 === "opencode" ? null : path4;
|
|
3049
|
+
}
|
|
3050
|
+
// src/cli/install.ts
|
|
3051
|
+
var GREEN = "\x1B[32m";
|
|
3052
|
+
var BLUE = "\x1B[34m";
|
|
3053
|
+
var YELLOW = "\x1B[33m";
|
|
3054
|
+
var RED = "\x1B[31m";
|
|
3055
|
+
var BOLD = "\x1B[1m";
|
|
3056
|
+
var DIM = "\x1B[2m";
|
|
3057
|
+
var RESET = "\x1B[0m";
|
|
3058
|
+
var SYMBOLS = {
|
|
3059
|
+
check: `${GREEN}[ok]${RESET}`,
|
|
3060
|
+
cross: `${RED}[x]${RESET}`,
|
|
3061
|
+
arrow: `${BLUE}->${RESET}`,
|
|
3062
|
+
bullet: `${DIM}-${RESET}`,
|
|
3063
|
+
info: `${BLUE}[i]${RESET}`,
|
|
3064
|
+
warn: `${YELLOW}[!]${RESET}`,
|
|
3065
|
+
star: `${YELLOW}★${RESET}`
|
|
3066
|
+
};
|
|
3067
|
+
var GITHUB_REPO = "alvinunreal/oh-my-opencode-slim";
|
|
3068
|
+
var GITHUB_URL = `https://github.com/${GITHUB_REPO}`;
|
|
3069
|
+
function printHeader(isUpdate) {
|
|
3070
|
+
console.log();
|
|
3071
|
+
console.log(`${BOLD}oh-my-opencode-slim ${isUpdate ? "Update" : "Install"}${RESET}`);
|
|
3072
|
+
console.log("=".repeat(30));
|
|
3073
|
+
console.log();
|
|
3074
|
+
}
|
|
3075
|
+
function printStep(step, total, message) {
|
|
3076
|
+
console.log(`${DIM}[${step}/${total}]${RESET} ${message}`);
|
|
3077
|
+
}
|
|
3078
|
+
function printSuccess(message) {
|
|
3079
|
+
console.log(`${SYMBOLS.check} ${message}`);
|
|
3080
|
+
}
|
|
3081
|
+
function printError(message) {
|
|
3082
|
+
console.log(`${SYMBOLS.cross} ${RED}${message}${RESET}`);
|
|
3083
|
+
}
|
|
3084
|
+
function printWarning(message) {
|
|
3085
|
+
console.log(`${SYMBOLS.warn} ${YELLOW}${message}${RESET}`);
|
|
3086
|
+
}
|
|
3087
|
+
function printInfo(message) {
|
|
3088
|
+
console.log(`${SYMBOLS.info} ${message}`);
|
|
3089
|
+
}
|
|
3090
|
+
async function confirm(message, defaultYes = true) {
|
|
3091
|
+
const suffix = defaultYes ? " (Y/n) " : " (y/N) ";
|
|
3092
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
3093
|
+
try {
|
|
3094
|
+
const answer = (await rl.question(`${message}${suffix}`)).trim().toLowerCase();
|
|
3095
|
+
if (!answer)
|
|
3096
|
+
return defaultYes;
|
|
3097
|
+
return answer === "y" || answer === "yes";
|
|
3098
|
+
} finally {
|
|
3099
|
+
rl.close();
|
|
3100
|
+
}
|
|
3101
|
+
}
|
|
3102
|
+
async function askToStarRepo(config) {
|
|
3103
|
+
if (!config.promptForStar || config.dryRun || !process.stdin.isTTY)
|
|
3104
|
+
return;
|
|
3105
|
+
console.log();
|
|
3106
|
+
const shouldStar = await confirm(`${SYMBOLS.star} Star the repo on GitHub?`, true);
|
|
3107
|
+
if (!shouldStar)
|
|
3108
|
+
return;
|
|
3109
|
+
try {
|
|
3110
|
+
const { execFileSync } = await import("node:child_process");
|
|
3111
|
+
execFileSync("gh", ["api", "--silent", "--method", "PUT", `/user/starred/${GITHUB_REPO}`], { stdio: "ignore", timeout: 1e4 });
|
|
3112
|
+
printSuccess("Thanks for starring! ★");
|
|
3113
|
+
} catch {
|
|
3114
|
+
printInfo(`Couldn't star automatically. You can star manually:
|
|
3115
|
+
${BLUE}${GITHUB_URL}${RESET}`);
|
|
3116
|
+
}
|
|
3117
|
+
}
|
|
3118
|
+
async function checkOpenCodeInstalled() {
|
|
3119
|
+
const installed = await isOpenCodeInstalled();
|
|
3120
|
+
if (!installed) {
|
|
3121
|
+
printError("OpenCode is not installed on this system.");
|
|
3122
|
+
printInfo("Install it with:");
|
|
3123
|
+
console.log(` ${BLUE}curl -fsSL https://opencode.ai/install | bash${RESET}`);
|
|
3124
|
+
console.log();
|
|
3125
|
+
printInfo("Or if already installed, add it to your PATH:");
|
|
3126
|
+
console.log(` ${BLUE}export PATH="$HOME/.local/bin:$PATH"${RESET}`);
|
|
3127
|
+
console.log(` ${BLUE}export PATH="$HOME/.opencode/bin:$PATH"${RESET}`);
|
|
3128
|
+
return { ok: false };
|
|
3129
|
+
}
|
|
3130
|
+
const version = await getOpenCodeVersion();
|
|
3131
|
+
const path4 = getOpenCodePath();
|
|
3132
|
+
const detectedVersion = version ?? "";
|
|
3133
|
+
const pathInfo = path4 ? ` (${DIM}${path4}${RESET})` : "";
|
|
3134
|
+
printSuccess(`OpenCode ${detectedVersion} detected${pathInfo}`);
|
|
3135
|
+
return { ok: true, version: version ?? undefined, path: path4 ?? undefined };
|
|
3136
|
+
}
|
|
3137
|
+
async function configureBackgroundSubagents(config) {
|
|
3138
|
+
if (isBackgroundSubagentsEnabled(process.env.OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS)) {
|
|
3139
|
+
printSuccess("OpenCode background subagents already enabled in environment");
|
|
3140
|
+
return { enabledNow: true };
|
|
3141
|
+
}
|
|
3142
|
+
const target = config.backgroundSubagentsTarget !== undefined ? expandHomePath(config.backgroundSubagentsTarget) : detectBackgroundSubagentsTarget();
|
|
3143
|
+
if (config.backgroundSubagents === "no") {
|
|
3144
|
+
printInfo("OpenCode background subagents shell setup skipped.");
|
|
3145
|
+
console.log(manualBackgroundSubagentsInstructions({ targetPath: target }));
|
|
3146
|
+
return { enabledNow: false };
|
|
3147
|
+
}
|
|
3148
|
+
if (!target) {
|
|
3149
|
+
printInfo("No safe shell startup file detected.");
|
|
3150
|
+
console.log(manualBackgroundSubagentsInstructions());
|
|
3151
|
+
return { enabledNow: false };
|
|
3152
|
+
}
|
|
3153
|
+
const block = getBackgroundSubagentsBlock(target);
|
|
3154
|
+
if (config.dryRun) {
|
|
3155
|
+
printInfo("Dry run mode - background subagents block that would be written:");
|
|
3156
|
+
console.log(`Target: ${target}`);
|
|
3157
|
+
console.log(`
|
|
3158
|
+
${block}
|
|
3159
|
+
`);
|
|
3160
|
+
return { enabledNow: false, configuredTarget: target };
|
|
3161
|
+
}
|
|
3162
|
+
if (config.backgroundSubagents === "ask") {
|
|
3163
|
+
if (!process.stdin.isTTY) {
|
|
3164
|
+
printInfo("Skipped background subagents shell setup in non-TTY mode.");
|
|
3165
|
+
console.log(manualBackgroundSubagentsInstructions({ targetPath: target }));
|
|
3166
|
+
return { enabledNow: false };
|
|
3167
|
+
}
|
|
3168
|
+
console.log();
|
|
3169
|
+
printInfo("V2 requires OpenCode background subagents for default orchestration.");
|
|
3170
|
+
printInfo(`The installer can add the required environment export to ${target}.`);
|
|
3171
|
+
const shouldWrite = await confirm("Add OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true now?", true);
|
|
3172
|
+
if (!shouldWrite) {
|
|
3173
|
+
printInfo("Skipped background subagents shell setup.");
|
|
3174
|
+
console.log(manualBackgroundSubagentsInstructions({ targetPath: target }));
|
|
3175
|
+
return { enabledNow: false };
|
|
3176
|
+
}
|
|
3177
|
+
}
|
|
3178
|
+
try {
|
|
3179
|
+
writeBackgroundSubagentsBlock(target);
|
|
3180
|
+
} catch (error) {
|
|
3181
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
3182
|
+
printError(`Could not write background subagents shell config: ${message}`);
|
|
3183
|
+
printInfo("Add the setting manually instead:");
|
|
3184
|
+
console.log(manualBackgroundSubagentsInstructions({ targetPath: target }));
|
|
3185
|
+
return { enabledNow: false };
|
|
3186
|
+
}
|
|
3187
|
+
printSuccess(`Background subagents enabled ${SYMBOLS.arrow} ${DIM}${target}${RESET}`);
|
|
3188
|
+
return { enabledNow: false, configuredTarget: target };
|
|
3189
|
+
}
|
|
3190
|
+
async function shouldInstallCompanion(config) {
|
|
3191
|
+
if (config.companion === "yes") {
|
|
3192
|
+
return true;
|
|
3193
|
+
}
|
|
3194
|
+
if (config.companion === "no")
|
|
3195
|
+
return false;
|
|
3196
|
+
if (config.dryRun) {
|
|
3197
|
+
printInfo("Dry run mode - would ask to install the desktop companion (default: no).");
|
|
3198
|
+
config.companion = "no";
|
|
3199
|
+
return false;
|
|
3200
|
+
}
|
|
3201
|
+
if (!process.stdin.isTTY) {
|
|
3202
|
+
printInfo("Skipped desktop companion prompt in non-TTY mode. Use --companion=yes to install it.");
|
|
3203
|
+
config.companion = "no";
|
|
3204
|
+
return false;
|
|
3205
|
+
}
|
|
3206
|
+
console.log();
|
|
3207
|
+
printInfo("The optional desktop companion shows live agent activity.");
|
|
3208
|
+
const shouldInstall = await confirm("Install and enable the desktop companion?", false);
|
|
3209
|
+
config.companion = shouldInstall ? "yes" : "no";
|
|
3210
|
+
if (!shouldInstall) {
|
|
3211
|
+
printInfo("Desktop companion install skipped.");
|
|
3212
|
+
}
|
|
3213
|
+
return shouldInstall;
|
|
3214
|
+
}
|
|
3215
|
+
function handleStepResult(result, successMsg) {
|
|
3216
|
+
if (!result.success) {
|
|
3217
|
+
printError(`Failed: ${result.error}`);
|
|
3218
|
+
return false;
|
|
3219
|
+
}
|
|
3220
|
+
printSuccess(`${successMsg} ${SYMBOLS.arrow} ${DIM}${result.configPath}${RESET}`);
|
|
3221
|
+
return true;
|
|
3222
|
+
}
|
|
3223
|
+
function handleOptionalCompanionResult(result) {
|
|
3224
|
+
if (result.success) {
|
|
3225
|
+
printSuccess(`Companion installed ${SYMBOLS.arrow} ${DIM}${result.configPath}${RESET}`);
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3228
|
+
printWarning(`Desktop companion install skipped: ${result.error}`);
|
|
3229
|
+
printInfo("The desktop companion is optional; continuing plugin installation without it.");
|
|
3230
|
+
}
|
|
3231
|
+
async function runInstall(config) {
|
|
3232
|
+
const detected = detectCurrentConfig();
|
|
3233
|
+
const isUpdate = detected.isInstalled;
|
|
3234
|
+
printHeader(isUpdate);
|
|
3235
|
+
const companionInstall = await shouldInstallCompanion(config);
|
|
3236
|
+
let totalSteps = 7;
|
|
3237
|
+
if (config.installCustomSkills)
|
|
3238
|
+
totalSteps += 1;
|
|
3239
|
+
if (companionInstall)
|
|
3240
|
+
totalSteps += 1;
|
|
3241
|
+
totalSteps += 1;
|
|
3242
|
+
let step = 1;
|
|
3243
|
+
printStep(step++, totalSteps, "Checking OpenCode installation...");
|
|
3244
|
+
if (config.dryRun) {
|
|
3245
|
+
printInfo("Dry run mode - skipping OpenCode check");
|
|
3246
|
+
} else {
|
|
3247
|
+
const { ok } = await checkOpenCodeInstalled();
|
|
3248
|
+
if (!ok)
|
|
3249
|
+
return 1;
|
|
3250
|
+
}
|
|
3251
|
+
printStep(step++, totalSteps, "Adding oh-my-opencode-slim plugin...");
|
|
3252
|
+
if (config.dryRun) {
|
|
3253
|
+
printInfo("Dry run mode - skipping plugin installation");
|
|
3254
|
+
} else {
|
|
3255
|
+
const pluginResult = await addPluginToOpenCodeConfig();
|
|
3256
|
+
if (!handleStepResult(pluginResult, "Plugin added"))
|
|
3257
|
+
return 1;
|
|
3258
|
+
}
|
|
3259
|
+
printStep(step++, totalSteps, "Adding TUI version badge...");
|
|
3260
|
+
if (config.dryRun) {
|
|
3261
|
+
printInfo("Dry run mode - skipping TUI plugin installation");
|
|
3262
|
+
} else {
|
|
3263
|
+
const tuiResult = await addPluginToOpenCodeTuiConfig();
|
|
3264
|
+
if (!tuiResult.success) {
|
|
3265
|
+
printInfo(`Skipped TUI badge: ${tuiResult.error}`);
|
|
3266
|
+
} else {
|
|
3267
|
+
handleStepResult(tuiResult, "TUI badge added");
|
|
3268
|
+
}
|
|
3269
|
+
}
|
|
3270
|
+
printStep(step++, totalSteps, "Warming OpenCode plugin cache...");
|
|
3271
|
+
if (config.dryRun) {
|
|
3272
|
+
printInfo("Dry run mode - skipping cache warm-up");
|
|
3273
|
+
} else {
|
|
3274
|
+
const cacheResult = await warmOpenCodePluginCache();
|
|
3275
|
+
if (cacheResult === null) {
|
|
3276
|
+
printInfo("Local development install - cache warm-up not required");
|
|
3277
|
+
} else if (!cacheResult.success) {
|
|
3278
|
+
printInfo(`Skipped cache warm-up: ${cacheResult.error}`);
|
|
3279
|
+
} else {
|
|
3280
|
+
handleStepResult(cacheResult, "OpenCode cache warmed");
|
|
3281
|
+
}
|
|
3282
|
+
}
|
|
3283
|
+
printStep(step++, totalSteps, "Disabling OpenCode default agents...");
|
|
3284
|
+
if (config.dryRun) {
|
|
3285
|
+
printInfo("Dry run mode - skipping agent disabling");
|
|
3286
|
+
} else {
|
|
3287
|
+
const agentResult = disableDefaultAgents();
|
|
3288
|
+
if (!handleStepResult(agentResult, "Default agents disabled"))
|
|
3289
|
+
return 1;
|
|
3290
|
+
}
|
|
3291
|
+
printStep(step++, totalSteps, "Enabling OpenCode LSP integration...");
|
|
3292
|
+
if (config.dryRun) {
|
|
3293
|
+
printInfo("Dry run mode - skipping LSP configuration");
|
|
3294
|
+
} else {
|
|
3295
|
+
const lspResult = enableLspByDefault();
|
|
3296
|
+
if (!handleStepResult(lspResult, "LSP enabled"))
|
|
3297
|
+
return 1;
|
|
3298
|
+
}
|
|
3299
|
+
printStep(step++, totalSteps, "Configuring OpenCode background subagents...");
|
|
3300
|
+
const backgroundSubagents = await configureBackgroundSubagents(config);
|
|
3301
|
+
if (companionInstall) {
|
|
3302
|
+
printStep(step++, totalSteps, "Installing desktop companion binary...");
|
|
3303
|
+
const companionResult = await installCompanion(config);
|
|
3304
|
+
handleOptionalCompanionResult(companionResult);
|
|
3305
|
+
if (!companionResult.success)
|
|
3306
|
+
config.companion = "no";
|
|
3307
|
+
}
|
|
3308
|
+
printStep(step++, totalSteps, "Writing oh-my-opencode-slim configuration...");
|
|
3309
|
+
if (config.dryRun) {
|
|
3310
|
+
const liteConfig = generateLiteConfig(config);
|
|
3311
|
+
printInfo("Dry run mode - configuration that would be written:");
|
|
3312
|
+
console.log(`
|
|
3313
|
+
${JSON.stringify(liteConfig, null, 2)}
|
|
3314
|
+
`);
|
|
3315
|
+
} else {
|
|
3316
|
+
const configPath2 = getExistingLiteConfigPath();
|
|
3317
|
+
const configExists = existsSync7(configPath2);
|
|
3318
|
+
if (configExists && !config.reset) {
|
|
3319
|
+
printInfo(`Configuration already exists at ${configPath2}. Use --reset to overwrite.`);
|
|
3320
|
+
} else {
|
|
3321
|
+
const liteResult = writeLiteConfig(config, configExists ? configPath2 : undefined);
|
|
3322
|
+
if (!handleStepResult(liteResult, configExists ? "Config reset" : "Config written"))
|
|
3323
|
+
return 1;
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
3326
|
+
if (config.installCustomSkills) {
|
|
3327
|
+
printStep(step++, totalSteps, "Synchronizing custom skills...");
|
|
3328
|
+
if (config.dryRun) {
|
|
3329
|
+
printInfo("Dry run mode - would synchronize custom skills:");
|
|
3330
|
+
for (const skill of CUSTOM_SKILLS) {
|
|
3331
|
+
printInfo(` - ${skill.name}`);
|
|
3332
|
+
}
|
|
3333
|
+
} else {
|
|
3334
|
+
try {
|
|
3335
|
+
const packageRoot = fileURLToPath(new URL("../..", import.meta.url));
|
|
3336
|
+
const result = syncBundledSkillsFromPackage(packageRoot);
|
|
3337
|
+
const categorizedSkipped = new Set([
|
|
3338
|
+
...result.staged,
|
|
3339
|
+
...result.adopted,
|
|
3340
|
+
...result.customized
|
|
3341
|
+
]);
|
|
3342
|
+
const preservedSkills = result.skippedExisting.filter((skill) => !categorizedSkipped.has(skill));
|
|
3343
|
+
if (result.installed.length > 0) {
|
|
3344
|
+
for (const skill of result.installed) {
|
|
3345
|
+
printSuccess(`Installed/Updated: ${skill}`);
|
|
3346
|
+
}
|
|
3347
|
+
}
|
|
3348
|
+
if (preservedSkills.length > 0) {
|
|
3349
|
+
for (const skill of preservedSkills) {
|
|
3350
|
+
printInfo(`Skipped/Preserved: ${skill}`);
|
|
3351
|
+
}
|
|
3352
|
+
}
|
|
3353
|
+
if (result.failed.length > 0) {
|
|
3354
|
+
for (const skill of result.failed) {
|
|
3355
|
+
if (skill === "__lock__") {
|
|
3356
|
+
printError("Lock acquisition failed");
|
|
3357
|
+
} else if (skill === "__manifest__") {
|
|
3358
|
+
printError("Manifest write failed");
|
|
3359
|
+
} else {
|
|
3360
|
+
printError(`Failed: ${skill}`);
|
|
3361
|
+
}
|
|
3362
|
+
}
|
|
3363
|
+
}
|
|
3364
|
+
if (result.staged.length > 0) {
|
|
3365
|
+
for (const skill of result.staged) {
|
|
3366
|
+
printInfo(`Staged for review: ${skill}`);
|
|
3367
|
+
}
|
|
3368
|
+
}
|
|
3369
|
+
if (result.adopted.length > 0) {
|
|
3370
|
+
for (const skill of result.adopted) {
|
|
3371
|
+
printInfo(`Adopted: ${skill}`);
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
if (result.customized.length > 0) {
|
|
3375
|
+
for (const skill of result.customized) {
|
|
3376
|
+
printInfo(`Customized: ${skill}`);
|
|
3377
|
+
}
|
|
3378
|
+
}
|
|
3379
|
+
const realFailed = result.failed.filter((skill) => skill !== "__lock__" && skill !== "__manifest__");
|
|
3380
|
+
printSuccess(`Skill synchronization complete: ${result.installed.length} installed/updated, ${preservedSkills.length} skipped/preserved, ${result.staged.length} staged, ${result.adopted.length} adopted, ${result.customized.length} customized, ${realFailed.length} failed.`);
|
|
3381
|
+
} catch (err) {
|
|
3382
|
+
printError(`Failed to synchronize custom skills: ${err}`);
|
|
3383
|
+
}
|
|
3384
|
+
}
|
|
3385
|
+
}
|
|
3386
|
+
const statusMsg = isUpdate ? "Configuration updated!" : "Installation complete!";
|
|
3387
|
+
console.log(`${SYMBOLS.star} ${BOLD}${GREEN}${statusMsg}${RESET}`);
|
|
3388
|
+
console.log();
|
|
3389
|
+
console.log(`${BOLD}Next steps:${RESET}`);
|
|
3390
|
+
console.log();
|
|
3391
|
+
const configPath = getExistingLiteConfigPath();
|
|
3392
|
+
console.log(" 1. Log in to the provider(s) you want to use:");
|
|
3393
|
+
console.log(` ${BLUE}$ opencode auth login${RESET}`);
|
|
3394
|
+
console.log();
|
|
3395
|
+
console.log(" 2. Refresh the models OpenCode can see:");
|
|
3396
|
+
console.log(` ${BLUE}$ opencode models --refresh${RESET}`);
|
|
3397
|
+
console.log();
|
|
3398
|
+
console.log(" 3. Review your generated config:");
|
|
3399
|
+
console.log(` ${BLUE}${configPath}${RESET}`);
|
|
3400
|
+
console.log();
|
|
3401
|
+
console.log(" 4. Start OpenCode:");
|
|
3402
|
+
if (backgroundSubagents.enabledNow) {
|
|
3403
|
+
console.log(` ${BLUE}$ opencode${RESET}`);
|
|
3404
|
+
} else if (backgroundSubagents.configuredTarget) {
|
|
3405
|
+
console.log(` ${BLUE}$ source ${backgroundSubagents.configuredTarget}${RESET}`);
|
|
3406
|
+
console.log(` ${BLUE}$ opencode${RESET}`);
|
|
3407
|
+
console.log(` ${DIM}Or restart your terminal before running opencode.${RESET}`);
|
|
3408
|
+
} else {
|
|
3409
|
+
console.log(` ${BLUE}$ OPENCODE_EXPERIMENTAL_BACKGROUND_SUBAGENTS=true opencode${RESET}`);
|
|
3410
|
+
}
|
|
3411
|
+
console.log();
|
|
3412
|
+
console.log(" 5. Verify the agents are responding:");
|
|
3413
|
+
console.log(` ${BLUE}> ping all agents${RESET}`);
|
|
3414
|
+
console.log();
|
|
3415
|
+
const modelsInfo = config.preset && config.preset !== "openai" ? `Generated OpenAI and OpenCode Go presets; ${config.preset} is active.` : "Generated OpenAI and OpenCode Go presets; OpenAI is active by default.";
|
|
3416
|
+
console.log(`${modelsInfo}`);
|
|
3417
|
+
const altProviders = "For the full configuration reference, see:";
|
|
3418
|
+
console.log(altProviders);
|
|
3419
|
+
const docsUrl = "https://github.com/alvinunreal/oh-my-opencode-slim/blob/master/docs/configuration.md";
|
|
3420
|
+
console.log(` ${BLUE}${docsUrl}${RESET}`);
|
|
3421
|
+
console.log();
|
|
3422
|
+
await askToStarRepo(config);
|
|
3423
|
+
return 0;
|
|
3424
|
+
}
|
|
3425
|
+
async function install(args) {
|
|
3426
|
+
const config = {
|
|
3427
|
+
hasTmux: false,
|
|
3428
|
+
installCustomSkills: args.skills === "yes",
|
|
3429
|
+
preset: args.preset,
|
|
3430
|
+
promptForStar: args.tui,
|
|
3431
|
+
dryRun: args.dryRun,
|
|
3432
|
+
reset: args.reset ?? false,
|
|
3433
|
+
backgroundSubagents: args.backgroundSubagents ?? "ask",
|
|
3434
|
+
backgroundSubagentsTarget: args.backgroundSubagentsTarget,
|
|
3435
|
+
companion: args.companion
|
|
3436
|
+
};
|
|
3437
|
+
return runInstall(config);
|
|
3438
|
+
}
|
|
3439
|
+
|
|
3440
|
+
// src/cli/providers.ts
|
|
3441
|
+
var GENERATED_PRESETS2 = ["openai", "opencode-go"];
|
|
3442
|
+
function isGeneratedPresetName2(value) {
|
|
3443
|
+
return GENERATED_PRESETS2.includes(value);
|
|
3444
|
+
}
|
|
3445
|
+
function getGeneratedPresetNames2() {
|
|
3446
|
+
return [...GENERATED_PRESETS2];
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
// src/cli/index.ts
|
|
3450
|
+
function parseArgs(args) {
|
|
3451
|
+
const result = {
|
|
3452
|
+
tui: true,
|
|
3453
|
+
skills: "yes",
|
|
3454
|
+
companion: "ask"
|
|
3455
|
+
};
|
|
3456
|
+
for (const arg of args) {
|
|
3457
|
+
if (arg === "--no-tui") {
|
|
3458
|
+
result.tui = false;
|
|
3459
|
+
} else if (arg.startsWith("--skills=")) {
|
|
3460
|
+
result.skills = arg.split("=")[1];
|
|
3461
|
+
} else if (arg.startsWith("--companion=")) {
|
|
3462
|
+
const mode = arg.split("=")[1];
|
|
3463
|
+
if (!["ask", "yes", "no"].includes(mode)) {
|
|
3464
|
+
console.error("Unsupported --companion value: use ask, yes, or no");
|
|
3465
|
+
process.exit(1);
|
|
3466
|
+
}
|
|
3467
|
+
result.companion = mode;
|
|
3468
|
+
} else if (arg.startsWith("--preset=")) {
|
|
3469
|
+
const preset = arg.split("=")[1];
|
|
3470
|
+
if (!isGeneratedPresetName2(preset)) {
|
|
3471
|
+
console.error(`Unsupported preset: ${preset}. Available presets: ${getGeneratedPresetNames2().join(", ")}`);
|
|
3472
|
+
process.exit(1);
|
|
3473
|
+
}
|
|
3474
|
+
result.preset = preset;
|
|
3475
|
+
} else if (arg.startsWith("--background-subagents=")) {
|
|
3476
|
+
const mode = arg.split("=")[1];
|
|
3477
|
+
if (!["ask", "yes", "no"].includes(mode)) {
|
|
3478
|
+
console.error("Unsupported --background-subagents value: use ask, yes, or no");
|
|
3479
|
+
process.exit(1);
|
|
3480
|
+
}
|
|
3481
|
+
result.backgroundSubagents = mode;
|
|
3482
|
+
} else if (arg.startsWith("--background-subagents-target=")) {
|
|
3483
|
+
result.backgroundSubagentsTarget = arg.split("=")[1];
|
|
3484
|
+
} else if (arg === "--dry-run") {
|
|
3485
|
+
result.dryRun = true;
|
|
3486
|
+
} else if (arg === "--reset") {
|
|
3487
|
+
result.reset = true;
|
|
3488
|
+
} else if (arg === "-h" || arg === "--help") {
|
|
3489
|
+
printHelp();
|
|
3490
|
+
process.exit(0);
|
|
3491
|
+
}
|
|
3492
|
+
}
|
|
3493
|
+
result.backgroundSubagents ??= "ask";
|
|
3494
|
+
return result;
|
|
3495
|
+
}
|
|
3496
|
+
function printHelp() {
|
|
3497
|
+
console.log(`
|
|
3498
|
+
oh-my-opencode-slim installer
|
|
3499
|
+
|
|
3500
|
+
Usage:
|
|
3501
|
+
bunx oh-my-opencode-slim install [OPTIONS]
|
|
3502
|
+
bunx oh-my-opencode-slim doctor [OPTIONS]
|
|
3503
|
+
|
|
3504
|
+
Options:
|
|
3505
|
+
--skills=yes|no Install bundled skills (default: yes)
|
|
3506
|
+
--companion=ask|yes|no Install desktop companion binary and enable config
|
|
3507
|
+
(default: ask; prompt defaults to no)
|
|
3508
|
+
--preset=<name> Active generated config preset (default: openai)
|
|
3509
|
+
--background-subagents=ask|yes|no
|
|
3510
|
+
Persist required OpenCode background subagent env
|
|
3511
|
+
(default: ask; prompt defaults to yes)
|
|
3512
|
+
--background-subagents-target=<path>
|
|
3513
|
+
Shell startup file to update
|
|
3514
|
+
--no-tui Non-interactive mode
|
|
3515
|
+
--dry-run Simulate install without writing files
|
|
3516
|
+
--reset Force overwrite of existing configuration
|
|
3517
|
+
-h, --help Show this help message
|
|
3518
|
+
|
|
3519
|
+
Doctor options:
|
|
3520
|
+
--json Print diagnostics as JSON
|
|
3521
|
+
|
|
3522
|
+
Available presets: ${getGeneratedPresetNames2().join(", ")}
|
|
3523
|
+
|
|
3524
|
+
The installer generates OpenAI and OpenCode Go presets by default.
|
|
3525
|
+
OpenAI is active unless --preset selects another generated preset.
|
|
3526
|
+
For the full config reference, see docs/configuration.md.
|
|
3527
|
+
|
|
3528
|
+
Examples:
|
|
3529
|
+
bunx oh-my-opencode-slim install
|
|
3530
|
+
bunx oh-my-opencode-slim install --no-tui --skills=yes
|
|
3531
|
+
bunx oh-my-opencode-slim install --background-subagents=yes
|
|
3532
|
+
bunx oh-my-opencode-slim install --preset=opencode-go
|
|
3533
|
+
bunx oh-my-opencode-slim install --reset
|
|
3534
|
+
bunx oh-my-opencode-slim doctor
|
|
3535
|
+
`);
|
|
3536
|
+
}
|
|
3537
|
+
async function main() {
|
|
3538
|
+
const args = process.argv.slice(2);
|
|
3539
|
+
if (args.length === 0 || args[0] === "install") {
|
|
3540
|
+
const hasSubcommand = args[0] === "install";
|
|
3541
|
+
const installArgs = parseArgs(args.slice(hasSubcommand ? 1 : 0));
|
|
3542
|
+
const exitCode = await install(installArgs);
|
|
3543
|
+
process.exit(exitCode);
|
|
3544
|
+
} else if (args[0] === "doctor") {
|
|
3545
|
+
const doctorArgs = parseDoctorArgs(args.slice(1));
|
|
3546
|
+
const exitCode = await doctor(doctorArgs);
|
|
3547
|
+
process.exit(exitCode);
|
|
3548
|
+
} else if (args[0] === "-h" || args[0] === "--help") {
|
|
3549
|
+
printHelp();
|
|
3550
|
+
process.exit(0);
|
|
3551
|
+
} else {
|
|
3552
|
+
console.error(`Unknown command: ${args[0]}`);
|
|
3553
|
+
console.error("Run with --help for usage information");
|
|
3554
|
+
process.exit(1);
|
|
3555
|
+
}
|
|
3556
|
+
}
|
|
3557
|
+
if (__require.main == __require.module) {
|
|
3558
|
+
main().catch((err) => {
|
|
3559
|
+
console.error("Fatal error:", err);
|
|
3560
|
+
process.exit(1);
|
|
3561
|
+
});
|
|
3562
|
+
}
|
|
3563
|
+
export {
|
|
3564
|
+
parseArgs
|
|
3565
|
+
};
|