specrune-opencode 0.0.0-stage → 0.9.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.
@@ -0,0 +1,214 @@
1
+ // specrune-opencode 安装器 adapter。
2
+ //
3
+ // 通用编排(参数解析、CLI 定位、状态/哈希/原子写、scope 校验、互斥检测、
4
+ // skills 调用、install/uninstall/status 流程)在 specrune/lib/installer-core.mjs;
5
+ // 这里只保留 OpenCode 的落点、模板与互斥探针。
6
+ //
7
+ // 安装位置默认全局:OpenCode 会同时发现全局与项目级插件,同一个插件两处都在
8
+ // 会重复挂 hook。项目级安装只作为例外与回退路径保留。
9
+ import { existsSync, readFileSync, readdirSync } from "node:fs";
10
+ import { homedir } from "node:os";
11
+ import { dirname, join, resolve } from "node:path";
12
+ import { fileURLToPath } from "node:url";
13
+
14
+ // 发布后从 specrune 依赖导入;源码仓库未执行 npm install,回退到仓库内的父包。
15
+ // 旧版 specrune 没有 lib/installer-core.mjs:两档都解析不到时给可操作错误,
16
+ // 不让裸 ERR_MODULE_NOT_FOUND 堆栈直接抛给用户。
17
+ async function loadInstallerCore() {
18
+ try {
19
+ return await import("specrune/lib/installer-core.mjs");
20
+ } catch (err) {
21
+ if (!err || err.code !== "ERR_MODULE_NOT_FOUND") throw err;
22
+ try {
23
+ return await import("../../../npm/cli/lib/installer-core.mjs");
24
+ } catch (fallbackErr) {
25
+ if (!fallbackErr || fallbackErr.code !== "ERR_MODULE_NOT_FOUND") throw fallbackErr;
26
+ throw new Error(
27
+ "无法加载共享安装核心 specrune/lib/installer-core.mjs:当前 specrune 版本不含 installer-core。" +
28
+ "请升级 specrune 后重试(npm i -g specrune@latest);" +
29
+ "若 specrune 尚未安装,请重新安装本插件。" +
30
+ `\n原始错误: ${err.message}`,
31
+ );
32
+ }
33
+ }
34
+ }
35
+
36
+ const core = await loadInstallerCore();
37
+ const {
38
+ createInstaller,
39
+ contentHash,
40
+ findProjectRoot,
41
+ resolveWithin,
42
+ } = core;
43
+
44
+ const HERE = dirname(fileURLToPath(import.meta.url));
45
+ const PKG_ROOT = resolve(HERE, "..");
46
+ const PKG = JSON.parse(readFileSync(join(PKG_ROOT, "package.json"), "utf8"));
47
+
48
+ // protocol 是后端钩子帧协议的版本,不是产品版本。插件只检查它,产品命令可以
49
+ // 自由演进而不打断已装插件(.specrune/spec/backend/agent-plugin-contract.md)。
50
+ const PROTOCOL = 1;
51
+
52
+ // 插件声明编写时依赖的 installer-core API 版本。必须写字面量:从核心导入
53
+ // 会让声明永远等于当前核心,旧插件在新核心上就无法被拦下。
54
+ const REQUIRED_CORE_VERSION = 1;
55
+
56
+ // plugin.ts 里的带引号占位符。带引号替换才能整体换成 JSON 字面量,同时借
57
+ // JSON.stringify 处理 Windows 路径的反斜杠转义。
58
+ const BUNDLED_PLACEHOLDER = '"__SPECRUNE_BUNDLED_BIN__"';
59
+
60
+ // locateBundledBinary 以本模块为锚解析平台二进制:适配器自己的依赖树决定
61
+ // 查找起点,不能换成共享核心的位置。
62
+ export function locateBundledBinary() {
63
+ return core.locateBundledBinary({ anchor: import.meta.url });
64
+ }
65
+
66
+ export { contentHash };
67
+
68
+ // injectBundledBinary 把插件源里的占位符换成解析到的绝对路径。未找到时注入
69
+ // 空串:运行时把空串视为「未注入」,继续走 npm 平台包与 PATH 档。
70
+ export function injectBundledBinary(source, binPath) {
71
+ return source.replace(BUNDLED_PLACEHOLDER, JSON.stringify(binPath || ""));
72
+ }
73
+
74
+ export const PLUGIN_REL = "plugins/specrune.ts";
75
+ const AGENTS_REL = "agents";
76
+ const COMMANDS_SRC = "commands";
77
+ const COMMANDS_REL = "commands/specrune";
78
+ const GLOBAL_STATE_FILE = "specrune-install.json";
79
+ const PROJECT_STATE_REL = ".specrune/.state/plugins/opencode.json";
80
+
81
+ const USAGE = `specrune-opencode — 把 SpecRune 的 OpenCode 插件装进宿主目录
82
+
83
+ 用法:
84
+ specrune-opencode install [--scope global|project] [--dir <路径>] [--dry-run] [--force]
85
+ specrune-opencode uninstall [--scope global|project] [--dir <路径>] [--dry-run] [--force]
86
+ specrune-opencode status [--scope global|project] [--dir <路径>] [--json]
87
+
88
+ 选项:
89
+ --scope global|project 安装范围,默认 global
90
+ global 写 ~/.config/opencode/(--dir 可覆盖配置根,便于测试)
91
+ project 写 <项目根>/.opencode/
92
+ --dir <路径> global 下是配置根;project 下是项目根(默认当前目录)
93
+ --dry-run 只打印计划,不写盘、不改状态文件
94
+ --force install 覆盖已修改的目标文件,并在检测到另一 scope 的安装时继续;
95
+ uninstall 连改过的文件一起删
96
+ --json status 以机器可读 JSON 输出
97
+ -h, --help 显示本帮助
98
+
99
+ 安装同时调用自带 CLI 的 'specrune skills install --adapter opencode --scope <scope>';
100
+ project 模式要求目标目录已有 .specrune/(先运行 specrune init)。技能安装失败只告警,
101
+ 插件安装仍然成功。
102
+
103
+ 退出码: 0 成功,1 运行失败,2 用法错误。
104
+ `;
105
+
106
+ // listMarkdown 读包内一个目录的 .md,返回安装后的 POSIX 相对路径与正文。
107
+ // 排序让安装计划与状态文件稳定:每次 install 给出不同顺序会让「这次改了什么」
108
+ // 没法用 diff 看。
109
+ function listMarkdown(srcDir, targetDir) {
110
+ const abs = join(PKG_ROOT, srcDir);
111
+ return readdirSync(abs)
112
+ .filter((name) => name.endsWith(".md"))
113
+ .sort()
114
+ .map((name) => ({
115
+ rel: `${targetDir}/${name}`,
116
+ content: readFileSync(join(abs, name), "utf8"),
117
+ }));
118
+ }
119
+
120
+ // templateFiles 是本次安装要落盘的全部自有文件。插件本体在 src/ 下,代理与
121
+ // 命令按宿主发现规则分目录;命令的文件名不加 specrune- 前缀,子目录名
122
+ // `specrune/` 已经是命令组前缀。
123
+ //
124
+ // 插件内容按解析到的本机二进制注入后再返回:状态哈希也随即按注入后的内容计算,
125
+ // 否则升级时会把「注入路径变了」误判成用户改动。
126
+ export function templateFiles(bundledBinary = locateBundledBinary()) {
127
+ const pluginSource = readFileSync(join(PKG_ROOT, "src", "plugin.ts"), "utf8");
128
+ return [
129
+ { rel: PLUGIN_REL, content: injectBundledBinary(pluginSource, bundledBinary) },
130
+ ...listMarkdown(AGENTS_REL, "agents"),
131
+ ...listMarkdown(COMMANDS_SRC, COMMANDS_REL),
132
+ ];
133
+ }
134
+
135
+ function defaultGlobalRoot() {
136
+ return join(homedir(), ".config", "opencode");
137
+ }
138
+
139
+ export function resolveTargets(opts) {
140
+ if (opts.scope === "project") {
141
+ const projectRoot = resolve(opts.dir || process.cwd());
142
+ return {
143
+ scope: "project",
144
+ projectRoot,
145
+ root: join(projectRoot, ".opencode"),
146
+ statePath: join(projectRoot, ...PROJECT_STATE_REL.split("/")),
147
+ };
148
+ }
149
+ // global 的 --dir 覆盖配置根:测试与隔离安装都需要一个不碰真实家目录的出口。
150
+ const root = opts.dir ? resolve(opts.dir) : defaultGlobalRoot();
151
+ return {
152
+ scope: "global",
153
+ projectRoot: null,
154
+ root,
155
+ statePath: join(root, GLOBAL_STATE_FILE),
156
+ };
157
+ }
158
+
159
+ // installedAt 是互斥检测的探针:插件入口、agents、commands 都算安装痕迹。
160
+ function installedAt(root) {
161
+ const found = [];
162
+ for (const file of templateFiles("")) {
163
+ try {
164
+ if (existsSync(resolveWithin(root, file.rel))) found.push(file.rel);
165
+ } catch {
166
+ /* 越界或坏根目录,跳过 */
167
+ }
168
+ }
169
+ return found;
170
+ }
171
+
172
+ function otherScopeTarget(targets) {
173
+ if (targets.scope === "project") {
174
+ return { scope: "global", root: defaultGlobalRoot() };
175
+ }
176
+ const projectRoot = findProjectRoot(process.cwd());
177
+ return { scope: "project", root: join(projectRoot, ".opencode") };
178
+ }
179
+
180
+ function detectOtherScope(targets) {
181
+ const other = otherScopeTarget(targets);
182
+ const found = installedAt(other.root);
183
+ return { ...other, found, installed: found.length > 0 };
184
+ }
185
+
186
+ // 与迁移前逐字一致:测试锁的是「拒绝 + 指出另一 scope + 提示 --force」。
187
+ function otherScopeError(other) {
188
+ return (
189
+ `specrune-opencode: 检测到 ${other.scope} scope 已有 OpenCode 安装` +
190
+ `(${other.found.length} 个文件,位于 ${other.root});继续安装会重复加载 hook。` +
191
+ `请先卸载: specrune-opencode uninstall --scope ${other.scope};` +
192
+ `确认要并存时加 --force。\n`
193
+ );
194
+ }
195
+
196
+ const adapter = {
197
+ name: "opencode",
198
+ displayName: "specrune-opencode",
199
+ command: "specrune-opencode",
200
+ packageName: "specrune-opencode",
201
+ version: PKG.version,
202
+ protocol: PROTOCOL,
203
+ coreVersion: REQUIRED_CORE_VERSION,
204
+ moduleUrl: import.meta.url,
205
+ usage: USAGE,
206
+ rootLabel: "配置根",
207
+ resolveTargets,
208
+ templateFiles,
209
+ detectOtherScope,
210
+ otherScopeError,
211
+ };
212
+
213
+ export const { install, uninstall, status, parse, main } = createInstaller(adapter);
214
+ export { adapter };