@shgroup/opencode-serenity-plugin 0.2.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.
Files changed (151) hide show
  1. package/README.md +199 -0
  2. package/bin/opencode-serenity-plugin.js +316 -0
  3. package/dist/activation.d.ts +40 -0
  4. package/dist/activation.d.ts.map +1 -0
  5. package/dist/activation.js +133 -0
  6. package/dist/activation.js.map +1 -0
  7. package/dist/bash-override.d.ts +10 -0
  8. package/dist/bash-override.d.ts.map +1 -0
  9. package/dist/bash-override.js +24 -0
  10. package/dist/bash-override.js.map +1 -0
  11. package/dist/bash-toggle.d.ts +17 -0
  12. package/dist/bash-toggle.d.ts.map +1 -0
  13. package/dist/bash-toggle.js +42 -0
  14. package/dist/bash-toggle.js.map +1 -0
  15. package/dist/config-schema.d.ts +300 -0
  16. package/dist/config-schema.d.ts.map +1 -0
  17. package/dist/config-schema.js +185 -0
  18. package/dist/config-schema.js.map +1 -0
  19. package/dist/errors.d.ts +90 -0
  20. package/dist/errors.d.ts.map +1 -0
  21. package/dist/errors.js +151 -0
  22. package/dist/errors.js.map +1 -0
  23. package/dist/fs/file-system-tool.d.ts +25 -0
  24. package/dist/fs/file-system-tool.d.ts.map +1 -0
  25. package/dist/fs/file-system-tool.js +318 -0
  26. package/dist/fs/file-system-tool.js.map +1 -0
  27. package/dist/fs/resolve-path.d.ts +53 -0
  28. package/dist/fs/resolve-path.d.ts.map +1 -0
  29. package/dist/fs/resolve-path.js +100 -0
  30. package/dist/fs/resolve-path.js.map +1 -0
  31. package/dist/hooks/compacting.d.ts +23 -0
  32. package/dist/hooks/compacting.d.ts.map +1 -0
  33. package/dist/hooks/compacting.js +90 -0
  34. package/dist/hooks/compacting.js.map +1 -0
  35. package/dist/hooks/permission-auto-reply.d.ts +91 -0
  36. package/dist/hooks/permission-auto-reply.d.ts.map +1 -0
  37. package/dist/hooks/permission-auto-reply.js +158 -0
  38. package/dist/hooks/permission-auto-reply.js.map +1 -0
  39. package/dist/hooks/permission-guards.d.ts +41 -0
  40. package/dist/hooks/permission-guards.d.ts.map +1 -0
  41. package/dist/hooks/permission-guards.js +153 -0
  42. package/dist/hooks/permission-guards.js.map +1 -0
  43. package/dist/hooks/shell-env.d.ts +20 -0
  44. package/dist/hooks/shell-env.d.ts.map +1 -0
  45. package/dist/hooks/shell-env.js +38 -0
  46. package/dist/hooks/shell-env.js.map +1 -0
  47. package/dist/hooks/util.d.ts +81 -0
  48. package/dist/hooks/util.d.ts.map +1 -0
  49. package/dist/hooks/util.js +172 -0
  50. package/dist/hooks/util.js.map +1 -0
  51. package/dist/index.d.ts +26 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +63 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/init/init-wizard.d.ts +39 -0
  56. package/dist/init/init-wizard.d.ts.map +1 -0
  57. package/dist/init/init-wizard.js +297 -0
  58. package/dist/init/init-wizard.js.map +1 -0
  59. package/dist/install.d.ts +117 -0
  60. package/dist/install.d.ts.map +1 -0
  61. package/dist/install.js +255 -0
  62. package/dist/install.js.map +1 -0
  63. package/dist/msm-schema.d.ts +76 -0
  64. package/dist/msm-schema.d.ts.map +1 -0
  65. package/dist/msm-schema.js +207 -0
  66. package/dist/msm-schema.js.map +1 -0
  67. package/dist/msm.d.ts +25 -0
  68. package/dist/msm.d.ts.map +1 -0
  69. package/dist/msm.js +317 -0
  70. package/dist/msm.js.map +1 -0
  71. package/dist/session/lib.d.ts +33 -0
  72. package/dist/session/lib.d.ts.map +1 -0
  73. package/dist/session/lib.js +475 -0
  74. package/dist/session/lib.js.map +1 -0
  75. package/dist/session/session-tool.d.ts +17 -0
  76. package/dist/session/session-tool.d.ts.map +1 -0
  77. package/dist/session/session-tool.js +109 -0
  78. package/dist/session/session-tool.js.map +1 -0
  79. package/dist/skills/install-skill.d.ts +36 -0
  80. package/dist/skills/install-skill.d.ts.map +1 -0
  81. package/dist/skills/install-skill.js +91 -0
  82. package/dist/skills/install-skill.js.map +1 -0
  83. package/dist/skills/template-loader.d.ts +79 -0
  84. package/dist/skills/template-loader.d.ts.map +1 -0
  85. package/dist/skills/template-loader.js +170 -0
  86. package/dist/skills/template-loader.js.map +1 -0
  87. package/dist/state.d.ts +35 -0
  88. package/dist/state.d.ts.map +1 -0
  89. package/dist/state.js +62 -0
  90. package/dist/state.js.map +1 -0
  91. package/dist/tui.d.ts +61 -0
  92. package/dist/tui.d.ts.map +1 -0
  93. package/dist/tui.js +279 -0
  94. package/dist/tui.js.map +1 -0
  95. package/dist/types/index.d.ts +30 -0
  96. package/dist/types/index.d.ts.map +1 -0
  97. package/dist/types/index.js +17 -0
  98. package/dist/types/index.js.map +1 -0
  99. package/dist/util/config-patch.d.ts +58 -0
  100. package/dist/util/config-patch.d.ts.map +1 -0
  101. package/dist/util/config-patch.js +117 -0
  102. package/dist/util/config-patch.js.map +1 -0
  103. package/dist/util/git.d.ts +29 -0
  104. package/dist/util/git.d.ts.map +1 -0
  105. package/dist/util/git.js +74 -0
  106. package/dist/util/git.js.map +1 -0
  107. package/dist/util/init-check.d.ts +22 -0
  108. package/dist/util/init-check.d.ts.map +1 -0
  109. package/dist/util/init-check.js +76 -0
  110. package/dist/util/init-check.js.map +1 -0
  111. package/dist/util/init.d.ts +54 -0
  112. package/dist/util/init.d.ts.map +1 -0
  113. package/dist/util/init.js +87 -0
  114. package/dist/util/init.js.map +1 -0
  115. package/dist/util/log.d.ts +25 -0
  116. package/dist/util/log.d.ts.map +1 -0
  117. package/dist/util/log.js +28 -0
  118. package/dist/util/log.js.map +1 -0
  119. package/dist/util/msm-call.d.ts +48 -0
  120. package/dist/util/msm-call.d.ts.map +1 -0
  121. package/dist/util/msm-call.js +86 -0
  122. package/dist/util/msm-call.js.map +1 -0
  123. package/dist/util/msm-exec-runtime.d.ts +123 -0
  124. package/dist/util/msm-exec-runtime.d.ts.map +1 -0
  125. package/dist/util/msm-exec-runtime.js +532 -0
  126. package/dist/util/msm-exec-runtime.js.map +1 -0
  127. package/dist/util/path.d.ts +10 -0
  128. package/dist/util/path.d.ts.map +1 -0
  129. package/dist/util/path.js +21 -0
  130. package/dist/util/path.js.map +1 -0
  131. package/dist/util/ready-state.d.ts +43 -0
  132. package/dist/util/ready-state.d.ts.map +1 -0
  133. package/dist/util/ready-state.js +104 -0
  134. package/dist/util/ready-state.js.map +1 -0
  135. package/dist/util/serenity-file.d.ts +30 -0
  136. package/dist/util/serenity-file.d.ts.map +1 -0
  137. package/dist/util/serenity-file.js +69 -0
  138. package/dist/util/serenity-file.js.map +1 -0
  139. package/dist/util/tui-install.d.ts +61 -0
  140. package/dist/util/tui-install.d.ts.map +1 -0
  141. package/dist/util/tui-install.js +94 -0
  142. package/dist/util/tui-install.js.map +1 -0
  143. package/docs/architecture-v0.md +294 -0
  144. package/docs/contract-v0.md +417 -0
  145. package/docs/plugin-self-contained-msm-v1.md +182 -0
  146. package/docs/refactor-direction-v1.11.md +78 -0
  147. package/docs/requirements-v0-scope.md +104 -0
  148. package/docs/requirements-v0-summary.md +108 -0
  149. package/docs/rr7-init-design.md +304 -0
  150. package/docs/v0.1-candidates.md +132 -0
  151. package/package.json +54 -0
package/README.md ADDED
@@ -0,0 +1,199 @@
1
+ # @shgroup/opencode-serenity-plugin
2
+
3
+ > **v0.2.0** — Serenity 认知基础设施的 [OpenCode](https://github.com/open-code-ai/opencode) 平台插件层。
4
+ >
5
+ > 提供 MSM 工具调用框架(`msm_list` / `msm_exec` / `msm_admin`)、
6
+ > 安全文件系统操作(`file_system`)、会话管理(`session_tool`),
7
+ > 以及 TUI slash command(`/serenity-init`、bash 开关)。
8
+ >
9
+ > 源码:[github.com/tellmewhattodo/opencode-serenity-plugin](https://github.com/tellmewhattodo/opencode-serenity-plugin)
10
+
11
+ ---
12
+
13
+ ## 安装
14
+
15
+ ### 前置条件
16
+
17
+ | 条件 | 要求 |
18
+ |------|------|
19
+ | Node.js | >= 20 |
20
+ | 包管理器 | pnpm |
21
+ | OpenCode 版本 | >= 1.16 |
22
+
23
+ ### 标准安装(推荐)
24
+
25
+ ```bash
26
+ npm install @shgroup/opencode-serenity-plugin
27
+ npx opencode-serenity-plugin install
28
+ ```
29
+
30
+ ### 从源码构建(贡献者用)
31
+
32
+ ```bash
33
+ git clone git@github.com:tellmewhattodo/opencode-serenity-plugin.git
34
+ cd opencode-serenity-plugin
35
+ pnpm install
36
+ pnpm build
37
+ npx opencode-serenity-plugin install
38
+ ```
39
+
40
+ `install` 命令的结果:
41
+
42
+ | 配置目标 | 写入位置 | 作用 |
43
+ |---------|----------|------|
44
+ | 项目级 | 当前目录 `opencode.json` | 注册 5 个自定义工具(`msm_list` / `msm_exec` / `msm_admin` / `file_system` / `session_tool`) |
45
+ | 全局级 | `~/.config/opencode/tui.json` | 注册 slash command(`/serenity-init`、`/serenity-bash-on/off/status`) |
46
+
47
+ ### 手动配置
48
+
49
+ 当 `install` 命令不可用或不适用时,手动编辑:
50
+
51
+ ```jsonc
52
+ // opencode.json(项目根)
53
+ {
54
+ "plugin": [
55
+ "file:///absolute/path/to/opencode-serenity-plugin/dist/index.js"
56
+ ]
57
+ }
58
+
59
+ // ~/.config/opencode/tui.json(全局,slash command 可见性)
60
+ {
61
+ "plugin": [
62
+ "file:///absolute/path/to/opencode-serenity-plugin/dist/tui.js"
63
+ ]
64
+ }
65
+ ```
66
+
67
+ ### 快速验证
68
+
69
+ 插件加载后在 opencode 中查询:
70
+
71
+ ```
72
+ msm_list
73
+ ```
74
+
75
+ 期待输出包含以下注册项:
76
+
77
+ | 名称 | 技能 | 类别 | 描述 |
78
+ |------|------|------|------|
79
+ | `msm_list` | serenity-plugin | mech | List all available MSM |
80
+ | `msm_exec` | serenity-plugin | mech | Execute a registered MSM |
81
+ | `msm_admin` | serenity-plugin | mech | Register or deregister an MSM |
82
+ | `file_system` | serenity-plugin | mech | Serenity file-system utility |
83
+ | `session_tool` | serenity-plugin | semi-mech | Session lifecycle management |
84
+
85
+ ### 开发期快速迭代
86
+
87
+ 修改源码后不需要重复 clone:
88
+
89
+ ```bash
90
+ pnpm build # 仅编译
91
+ npx opencode-serenity-plugin install # 重新写入配置
92
+ # 或使用 serenity-plugin-develop-kit MSM 自动执行 typecheck → test → build → install
93
+ ```
94
+
95
+ ---
96
+
97
+ ## 功能清单
98
+
99
+ ### 5 个 OpenCode 自定义工具
100
+
101
+ | 工具名 | 类别 | 职责 |
102
+ |--------|------|------|
103
+ | `msm_list` | mech | 查询 `mech-registry.json`,列出所有已注册 MSM 及其描述。`msm_exec` 的前置查询步骤。 |
104
+ | `msm_exec` | mech | 执行已注册 MSM。参数 `msm_name` + `args: string[]`。替代裸 bash 的安全命令入口。 |
105
+ | `msm_admin` | mech | 注册/注销 MSM。单 tool + `action` enum(`register` / `deregister`)。注册表变更自动 git commit。 |
106
+ | `file_system` | mech | 安全文件系统操作。10 个子命令:`root` / `resolve` / `exists` / `list` / `relative` / `mkdir` / `rm` / `mv` / `cp` / `touch`。所有写操作限制在 `.serenity` 根目录内,保护 `.serenity` 标记文件和根目录自身不被删除。 |
107
+ | `session_tool` | semi-mech | 会话生命周期管理。7 个子命令:`list` / `show` / `create` / `health` / `archive` / `summary` / `qa`。管理 `AGENT_SESSIONS/` 目录下的会话记录。 |
108
+
109
+ ### 系统 Hook
110
+
111
+ | Hook 名 | 执行时机 | 职责 |
112
+ |---------|----------|------|
113
+ | `tool.execute.before` | 每个工具调用前 | 路径守卫:`read`/`edit`/`write`/`grep`/`glob`/`webfetch` 的 `path` 参数强制在 `cwdRoot` 内(含 symlink 防御)。bash 静默拒绝开关:被禁用时 AI 收到 `"bash is disabled by user, use msm instead"`。 |
114
+ | `experimental.chat.system.transform` | 新 session 首次激活 | 注入 `/.opencode/skills/<实例名>/SKILL.md` 全文到 system prompt 末尾。 |
115
+ | `experimental.session.compacting` | 压缩事件 | 注入 serenity 关键状态(实例名、路径、激活状态)。 |
116
+ | `experimental.tool.definition` | 工具定义阶段 | 向 `task` 工具描述注入精简 serenity context(使 subagent 也能感知 serenity 环境)。 |
117
+ | `shell.env` | shell 执行前 | 注入环境变量 `HOME_SERENITY_ROOT`(实例根路径)、`SERENITY_INSTANCE`(实例名)。 |
118
+ | `event: permission.asked` | 权限弹窗触发时 | cwdRoot 内文件操作自动回复 `always`。 |
119
+
120
+ ### TUI Slash Command
121
+
122
+ | 命令 | 作用 |
123
+ |------|------|
124
+ | `/serenity-init` | 将当前 git 仓库初始化为 serenity 实例:创建 `/.serenity` + `git commit -m "chore: initialize serenity (instance: <name>)"`。 |
125
+ | `/serenity-bash-on` | 启用 bash 工具。 |
126
+ | `/serenity-bash-off` | 禁用 bash 工具。AI 对 bash 的任何调用收到 `"bash is disabled by user, use msm instead"` 错误。 |
127
+ | `/serenity-bash-status` | 显示当前 bash 启用/禁用状态。 |
128
+
129
+ ---
130
+
131
+ ## 开发指南
132
+
133
+ ### 标准开发循环
134
+
135
+ ```bash
136
+ pnpm typecheck # 编译检查,零 LLM 推理
137
+ pnpm test # 执行全部 382 个测试(vitest)
138
+ pnpm build # 编译到 dist/
139
+ pnpm opencode-serenity-plugin install # 安装到当前 project
140
+ ```
141
+
142
+ 推荐使用 `serenity-plugin-develop-kit` MSM 工具自动执行上述 4 步(typecheck → test → build → install),遇非零即停,提供完整审计可追溯性。
143
+
144
+ ### 测试架构
145
+
146
+ ```bash
147
+ pnpm test # 全部 382 个测试
148
+ pnpm test -- --watch # 监视模式
149
+ pnpm test tests/msm.test.ts # 单文件测试
150
+ ```
151
+
152
+ 测试文件按模块分组于 `tests/`:
153
+
154
+ | 文件 | 用例数 | 覆盖范围 |
155
+ |------|--------|---------|
156
+ | `tests/msm.test.ts` | — | MSM 工具(list/exec/admin) |
157
+ | `tests/fs-file-system-tool.test.ts` | 40 | 全部 10 个文件系统子命令 + 安全边界 |
158
+ | `tests/util-*.test.ts` | — | 各 util 函数(init / serenity-file / log 等) |
159
+ | `tests/session-*.test.ts` | — | 会话管理各子命令 |
160
+ | `tests/hooks/*.test.ts` | — | Hook 行为(permission-guards / compacting /
161
+ tool-definition / auto-reply) |
162
+ | `tests/tui*.test.ts` | — | TUI slash command |
163
+ | `tests/errors.test.ts` | — | 错误类层级 |
164
+
165
+ ---
166
+
167
+ ## 架构模型:Template-Instance
168
+
169
+ ```
170
+ opencode-serenity-plugin ← template(持久源,source of truth)
171
+ ↓ build + install
172
+ serenity 实例目录(如 home-serenity/) ← instance(可重建的下游 artifact)
173
+ ```
174
+
175
+ | 角色 | 特征 |
176
+ |------|------|
177
+ | **template**(plugin 仓) | durable,不可替代,GitHub 发布源 |
178
+ | **instance**(serenity 目录) | replicable,可删除后重新 build + install |
179
+ | **关系** | 单向模板→实例。不存在"双真源防漂移"。修改 plugin 后必须重新 build + install。 |
180
+
181
+ ---
182
+
183
+ ## 关联文档
184
+
185
+ | 主题 | 文档路径 |
186
+ |------|---------|
187
+ | 范围层(RR1-RR7,终版) | [`docs/requirements-v0-scope.md`](docs/requirements-v0-scope.md) |
188
+ | 架构设计(两阶段 init + 模块分解) | [`docs/architecture-v0.md`](docs/architecture-v0.md) |
189
+ | 接口契约(6 契约 + 13 错误类) | [`docs/contract-v0.md`](docs/contract-v0.md) |
190
+ | RR7 Init 实施记录 | [`docs/rr7-init-design.md`](docs/rr7-init-design.md) |
191
+ | 重构方向(v1.11-v1.17) | [`docs/refactor-direction-v1.11.md`](docs/refactor-direction-v1.11.md) |
192
+ | v0.1 候选实施(3/3 实施) | [`docs/v0.1-candidates.md`](docs/v0.1-candidates.md) |
193
+ | MSM 自包含 RFC(S028) | [`docs/plugin-self-contained-msm-v1.md`](docs/plugin-self-contained-msm-v1.md) |
194
+ | 设计方案索引(S031) | `AGENT_SESSIONS/2026-05-17--S031--plugin-next-round-requirements/SESSION.md` |
195
+
196
+ ---
197
+
198
+ > **版本**: v0.1.0 (2026-06-15)
199
+ > **作者**: yh + 宁静号 Agent
@@ -0,0 +1,316 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * opencode-serenity-plugin CLI (v1.11 → v0.0.4 全局化)
4
+ *
5
+ * Usage:
6
+ * opencode-serenity-plugin install [flags]
7
+ *
8
+ * Flags:
9
+ * --dry-run Print what would be done without writing
10
+ * --verbose Show detailed output (default: silent on no-op success)
11
+ * --help Show this help
12
+ * --version Show version
13
+ *
14
+ * Exit codes:
15
+ * 0 Success (including no-op when already installed)
16
+ * 1 Hard error (permission denied, dist/ not built, etc.)
17
+ * 2 Conflict: plugin already installed at a different path
18
+ *
19
+ * Notes:
20
+ * - 逻辑层在 ../dist/install.js (来自 src/install.ts)。
21
+ * 跑 install 前必须 `pnpm build` (或 `npm run build`)。
22
+ * - v0.0.4: 两个 entry (server + TUI) 都装到全局 config:
23
+ * - dist/index.js → ~/.config/opencode/opencode.json#plugin
24
+ * - dist/tui.js → ~/.config/opencode/tui.json#plugin
25
+ * - 所有目录下 opencode 都加载两个 entry,由 tryActivateSync 判断是否激活。
26
+ * (之前 server entry 装项目级 opencode.json, 非 serenity 目录不加载 server)
27
+ */
28
+
29
+ import { fileURLToPath } from 'node:url';
30
+ import { dirname, resolve } from 'node:path';
31
+ import { existsSync, readFileSync } from 'node:fs';
32
+ import {
33
+ resolvePluginEntries,
34
+ resolveInstallPathFromBin,
35
+ getGlobalConfigPath,
36
+ writePluginEntry,
37
+ } from '../dist/install.js';
38
+ import { installSkill } from '../dist/skills/install-skill.js';
39
+ import { initWizard } from '../dist/init/init-wizard.js';
40
+
41
+ const __filename = fileURLToPath(import.meta.url);
42
+ const __dirname = dirname(__filename);
43
+ const INSTALL_PATH = resolveInstallPathFromBin(__filename);
44
+
45
+ // ── CLI 解析 (轻量,无 yargs 依赖) ──
46
+
47
+ function parseArgs(argv) {
48
+ const flags = {
49
+ command: null,
50
+ /** skill name for install-skill, or target path for init */
51
+ skillName: null,
52
+ prefix: null,
53
+ /** flag for non-interactive init */
54
+ nonInteractive: false,
55
+ /** flag for force overwrite */
56
+ force: false,
57
+ global: false,
58
+ dryRun: false,
59
+ verbose: false,
60
+ help: false,
61
+ version: false,
62
+ };
63
+ let expectPrefix = false;
64
+ for (let i = 0; i < argv.length; i++) {
65
+ const arg = argv[i];
66
+
67
+ // handle --prefix <value> (consumes next arg)
68
+ if (expectPrefix) {
69
+ flags.prefix = arg;
70
+ expectPrefix = false;
71
+ continue;
72
+ }
73
+
74
+ switch (arg) {
75
+ case 'init':
76
+ flags.command = 'init';
77
+ // next arg is the target path (optional)
78
+ if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
79
+ flags.skillName = argv[++i];
80
+ }
81
+ break;
82
+ case 'install': flags.command = 'install'; break;
83
+ case 'install-skill':
84
+ flags.command = 'install-skill';
85
+ // next arg is the skill name
86
+ if (i + 1 < argv.length && !argv[i + 1].startsWith('--')) {
87
+ flags.skillName = argv[++i];
88
+ }
89
+ break;
90
+ case 'uninstall': flags.command = 'uninstall'; break;
91
+ case '--global': flags.global = true; break;
92
+ case '--dry-run': flags.dryRun = true; break;
93
+ case '--verbose': case '-v': flags.verbose = true; break;
94
+ case '--non-interactive': flags.nonInteractive = true; break;
95
+ case '--force': flags.force = true; break;
96
+ case '--help': case '-h': flags.help = true; break;
97
+ case '--version': flags.version = true; break;
98
+ default:
99
+ if (arg.startsWith('--prefix=')) {
100
+ flags.prefix = arg.slice('--prefix='.length);
101
+ } else if (arg === '--prefix') {
102
+ expectPrefix = true;
103
+ } else if (arg.startsWith('--')) {
104
+ process.stderr.write(`error: unknown flag: ${arg}\n`);
105
+ process.exit(1);
106
+ }
107
+ }
108
+ }
109
+ return flags;
110
+ }
111
+
112
+ function readVersion() {
113
+ const pkgPath = resolve(INSTALL_PATH, 'package.json');
114
+ if (!existsSync(pkgPath)) return 'unknown';
115
+ try {
116
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
117
+ return pkg.version || 'unknown';
118
+ } catch {
119
+ return 'unknown';
120
+ }
121
+ }
122
+
123
+ function printHelp() {
124
+ const version = readVersion();
125
+ process.stdout.write(`opencode-serenity-plugin v${version}
126
+
127
+ Usage:
128
+ opencode-serenity-plugin init [path] [flags]
129
+ opencode-serenity-plugin install [flags]
130
+ opencode-serenity-plugin install-skill <name> [flags]
131
+
132
+ Commands:
133
+ init Create a new serenity instance (interactive wizard)
134
+ install Write server + TUI plugin entries to global opencode configs (idempotent)
135
+ install-skill Install a domain skill from built-in templates into the current serenity instance
136
+
137
+ init [path] Path for the new serenity instance (default: current directory)
138
+ install-skill <name> Available skills vary by version
139
+
140
+ Flags:
141
+ --prefix=<str> Skill prefix (default: auto-detect from .serenity)
142
+ --non-interactive Skip interactive prompts (use defaults)
143
+ --force Overwrite existing files
144
+ --dry-run Print what would be done without writing
145
+ --verbose Show detailed output
146
+ --help, -h Show this help
147
+ --version Print version and exit
148
+
149
+ Exit codes:
150
+ 0 Success
151
+ 1 Hard error
152
+ `);
153
+ }
154
+
155
+ // ── 主入口 ──
156
+
157
+ function installCommand(flags) {
158
+ // 1. 解析 entries
159
+ let entries;
160
+ try {
161
+ entries = resolvePluginEntries(INSTALL_PATH);
162
+ } catch (err) {
163
+ process.stderr.write(`error: failed to resolve plugin entries: ${err.message}\n`);
164
+ return 1;
165
+ }
166
+
167
+ // 2. 检查 dist/ 是否存在
168
+ if (!existsSync(entries.server.absPath) || !existsSync(entries.tui.absPath)) {
169
+ process.stderr.write(`error: dist/ not built at ${INSTALL_PATH}\n`);
170
+ process.stderr.write(`hint: run 'pnpm build' or 'npm run build' first\n`);
171
+ return 1;
172
+ }
173
+
174
+ // 3. 决定目标 — 两个 entry 都装到全局 config
175
+ // server entry 在全局 opencode.json,TUI entry 在全局 tui.json。
176
+ // 所有目录下 opencode 都加载两个 entry,由 tryActivateSync 判断是否激活。
177
+ // (之前 server entry 装到项目级 opencode.json,导致非 serenity 目录不加载 server)
178
+ const targets = [
179
+ {
180
+ configPath: getGlobalConfigPath('opencode.json'),
181
+ entries: [entries.server],
182
+ label: `global opencode.json`,
183
+ },
184
+ {
185
+ configPath: getGlobalConfigPath('tui.json'),
186
+ entries: [entries.tui],
187
+ label: `global tui.json`,
188
+ },
189
+ ];
190
+
191
+ // 4. 执行 — writePluginEntry 内已做幂等检查(isAlreadyInstalled)
192
+ let changed = 0;
193
+ let noop = 0;
194
+ for (const t of targets) {
195
+ if (flags.dryRun) {
196
+ const specs = t.entries.map(e => e.path).join(', ');
197
+ process.stdout.write(`[dry-run] would write ${specs} to ${t.configPath}\n`);
198
+ continue;
199
+ }
200
+ const result = writePluginEntry(t.configPath, t.entries);
201
+ if (result.error) {
202
+ process.stderr.write(`error: ${t.label}: ${result.error}\n`);
203
+ return 1;
204
+ }
205
+ if (result.changed) {
206
+ changed++;
207
+ if (flags.verbose) {
208
+ process.stdout.write(`✓ wrote ${t.configPath} (${result.addedPaths.join(', ')})\n`);
209
+ }
210
+ } else {
211
+ noop++;
212
+ if (flags.verbose) {
213
+ process.stdout.write(`✓ already installed: ${t.configPath}\n`);
214
+ }
215
+ }
216
+ }
217
+
218
+ // 6. 输出
219
+ if (flags.dryRun) {
220
+ // already printed above
221
+ } else if (changed > 0 && !flags.verbose) {
222
+ process.stdout.write(`opencode-serenity-plugin: installed (${changed} config${changed === 1 ? '' : 's'}). restart opencode to activate.\n`);
223
+ } else if (flags.verbose) {
224
+ process.stdout.write(`done. changed=${changed} noop=${noop}\n`);
225
+ }
226
+ // else: silent on no-op success
227
+
228
+ return 0;
229
+ }
230
+
231
+ // ── install-skill 命令 ──
232
+
233
+ function installSkillCommand(flags) {
234
+ const name = flags.skillName;
235
+ if (!name) {
236
+ process.stderr.write('error: install-skill requires a skill name\n');
237
+ process.stderr.write('usage: opencode-serenity-plugin install-skill <name> [--prefix=<str>]\n');
238
+ return 1;
239
+ }
240
+
241
+ const result = installSkill({
242
+ pluginRoot: INSTALL_PATH,
243
+ name,
244
+ cwd: process.cwd(),
245
+ prefix: flags.prefix || undefined,
246
+ dryRun: flags.dryRun,
247
+ });
248
+
249
+ if (!result.success) {
250
+ process.stderr.write(`error: ${result.message}\n`);
251
+ return 1;
252
+ }
253
+
254
+ if (flags.dryRun && flags.verbose && result.createdFiles?.length) {
255
+ process.stdout.write('Files to create:\n');
256
+ for (const f of result.createdFiles) {
257
+ process.stdout.write(` ${f}\n`);
258
+ }
259
+ }
260
+
261
+ process.stdout.write(result.message + '\n');
262
+ return 0;
263
+ }
264
+
265
+ // ── init 命令 ──
266
+
267
+ async function initCommand(flags) {
268
+ const targetPath = flags.skillName || process.cwd(); // reuse skillName as positional target
269
+
270
+ const result = await initWizard({
271
+ targetPath,
272
+ pluginRoot: INSTALL_PATH,
273
+ prefix: flags.prefix || undefined,
274
+ nonInteractive: flags.nonInteractive,
275
+ force: flags.force,
276
+ });
277
+
278
+ if (!result.success) {
279
+ process.stderr.write(`error: ${result.message}\n`);
280
+ return 1;
281
+ }
282
+
283
+ process.stdout.write(result.message + '\n');
284
+ return 0;
285
+ }
286
+
287
+ async function main() {
288
+ const flags = parseArgs(process.argv.slice(2));
289
+
290
+ if (flags.version) {
291
+ process.stdout.write(readVersion() + '\n');
292
+ return 0;
293
+ }
294
+ if (flags.help || !flags.command) {
295
+ printHelp();
296
+ return flags.help ? 0 : 1;
297
+ }
298
+
299
+ switch (flags.command) {
300
+ case 'init':
301
+ return await initCommand(flags);
302
+ case 'install':
303
+ return installCommand(flags);
304
+ case 'install-skill':
305
+ return installSkillCommand(flags);
306
+ case 'uninstall':
307
+ process.stderr.write(`error: 'uninstall' not implemented in v1.11 (D24 scope: install only)\n`);
308
+ process.stderr.write(`hint: manually remove entry from <config>#plugin, then delete from _plugin_origins\n`);
309
+ return 1;
310
+ default:
311
+ process.stderr.write(`error: unknown command: ${flags.command}\n`);
312
+ return 1;
313
+ }
314
+ }
315
+
316
+ main().then((code) => process.exit(code ?? 0));
@@ -0,0 +1,40 @@
1
+ /**
2
+ * 10 步启动协议 — v0.1 两阶段 init
3
+ *
4
+ * 旧 v0:one-phase sync(tryActivate 一次 await 完所有 IO)
5
+ * 新 v0.1:two-phase
6
+ * Phase 1 同步(plugin 入口立即返回):
7
+ * - RR6 检查 cwd 是否在 git repo → 失败:mark disabled,返回
8
+ * - 成功:启动 Phase 2 fire-and-forget
9
+ * Phase 2 异步(后台 IO):
10
+ * - RR1 读 /.serenity → 失败:state.error
11
+ * - RR2 验证 SKILL.md 存在 → 失败:state.error
12
+ * - 成功:setState + mark ready
13
+ *
14
+ * 关键设计:tools/hooks 通过 `ensureReady()` 阻塞等待 Phase 2 完成。
15
+ *
16
+ * 不抛错——所有失败路径通过 state/console.warn 表达。
17
+ */
18
+ import type { PluginInput } from '@opencode-ai/plugin';
19
+ export type SyncResult = {
20
+ ok: true;
21
+ cwdRoot: string;
22
+ } | {
23
+ ok: false;
24
+ reason: string;
25
+ };
26
+ /**
27
+ * getClient 工厂:可注入,便于测试 mock + 隔离 v1/v2 client 类型
28
+ *
29
+ * 返回类型用 unknown,activation 内部按需 cast 成 ToastClient(v1.18 解耦)。
30
+ */
31
+ export type GetClient = () => unknown | null;
32
+ /**
33
+ * Phase 1:同步检查 RR6(git repo)。
34
+ * - 失败:mark disabled + console.warn
35
+ * - 成功:启动 Phase 2 fire-and-forget + 立即返回 ok
36
+ *
37
+ * 注:调用方不需 await Phase 2 任何 IO;后续 tools/hooks 会自己 ensureReady()
38
+ */
39
+ export declare function tryActivateSync(input: PluginInput, getClient?: GetClient): SyncResult;
40
+ //# sourceMappingURL=activation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"activation.d.ts","sourceRoot":"","sources":["../src/activation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAQH,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AAQvD,MAAM,MAAM,UAAU,GAClB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAC7B;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAElC;;;;GAIG;AACH,MAAM,MAAM,SAAS,GAAG,MAAM,OAAO,GAAG,IAAI,CAAC;AAE7C;;;;;;GAMG;AACH,wBAAgB,eAAe,CAAC,KAAK,EAAE,WAAW,EAAE,SAAS,CAAC,EAAE,SAAS,GAAG,UAAU,CAyBrF"}
@@ -0,0 +1,133 @@
1
+ /**
2
+ * 10 步启动协议 — v0.1 两阶段 init
3
+ *
4
+ * 旧 v0:one-phase sync(tryActivate 一次 await 完所有 IO)
5
+ * 新 v0.1:two-phase
6
+ * Phase 1 同步(plugin 入口立即返回):
7
+ * - RR6 检查 cwd 是否在 git repo → 失败:mark disabled,返回
8
+ * - 成功:启动 Phase 2 fire-and-forget
9
+ * Phase 2 异步(后台 IO):
10
+ * - RR1 读 /.serenity → 失败:state.error
11
+ * - RR2 验证 SKILL.md 存在 → 失败:state.error
12
+ * - 成功:setState + mark ready
13
+ *
14
+ * 关键设计:tools/hooks 通过 `ensureReady()` 阻塞等待 Phase 2 完成。
15
+ *
16
+ * 不抛错——所有失败路径通过 state/console.warn 表达。
17
+ */
18
+ import { findGitRoot } from './util/git.js';
19
+ import { readSerenityFile } from './util/serenity-file.js';
20
+ import { buildSkillPath, validateSkillExists } from './util/path.js';
21
+ import { readFileSync } from 'node:fs';
22
+ import { setState, markReady, markDisabled, getReadyMachine } from './state.js';
23
+ import { log } from './util/log.js';
24
+ import { checkSerenityConfig } from './util/init-check.js';
25
+ import { patchMainRepoOpencodeJson, } from './util/config-patch.js';
26
+ /**
27
+ * Phase 1:同步检查 RR6(git repo)。
28
+ * - 失败:mark disabled + console.warn
29
+ * - 成功:启动 Phase 2 fire-and-forget + 立即返回 ok
30
+ *
31
+ * 注:调用方不需 await Phase 2 任何 IO;后续 tools/hooks 会自己 ensureReady()
32
+ */
33
+ export function tryActivateSync(input, getClient) {
34
+ const cwd = input.directory;
35
+ log.info('phase1', 'start sync activation', { cwd });
36
+ // Phase 1 — RR6 同步检查
37
+ let cwdRoot;
38
+ try {
39
+ cwdRoot = findGitRoot(cwd);
40
+ log.info('phase1', 'RR6 ok: cwd in git repo', { cwdRoot });
41
+ }
42
+ catch (err) {
43
+ const reason = formatErrorMessage(err, 'RR6: cwd not in git repo');
44
+ log.warn('phase1', 'RR6 failed', { reason, cwd });
45
+ markDisabled(reason);
46
+ return { ok: false, reason };
47
+ }
48
+ // Phase 2 — fire-and-forget(不 await)
49
+ const machine = getReadyMachine();
50
+ void machine.start(async () => {
51
+ log.debug('phase2', 'starting async activation', { cwdRoot });
52
+ await activateAsync(cwdRoot, getClient); // throws on RR1/RR2 failure → machine.markError()
53
+ log.info('phase2', 'activation complete', { cwdRoot });
54
+ });
55
+ return { ok: true, cwdRoot };
56
+ }
57
+ /**
58
+ * Phase 2:异步激活。
59
+ * 内部 try/catch 所有失败 → machine 自然 catch 并推 error 状态。
60
+ */
61
+ async function activateAsync(cwdRoot, getClient) {
62
+ // RR1 — 读 /.serenity
63
+ let instanceName;
64
+ try {
65
+ instanceName = readSerenityFile(cwdRoot);
66
+ }
67
+ catch (err) {
68
+ const detail = err instanceof Error ? err.message : String(err);
69
+ throw new Error(`RR1: ${detail}`);
70
+ }
71
+ // RR2 — 验证 SKILL.md
72
+ let skillPath;
73
+ try {
74
+ skillPath = buildSkillPath(cwdRoot, instanceName);
75
+ validateSkillExists(skillPath, cwdRoot, instanceName);
76
+ }
77
+ catch (err) {
78
+ const detail = err instanceof Error ? err.message : String(err);
79
+ throw new Error(`RR2: ${detail}`);
80
+ }
81
+ // RR2.5 — 读 SKILL.md 全文(用于 system.transform 注入)
82
+ // 失败:降级为 null(plugin 仍工作,只是不注 SKILL.md)
83
+ let skillContent = null;
84
+ try {
85
+ skillContent = readFileSync(skillPath, 'utf8');
86
+ log.debug('phase2', 'SKILL.md loaded', { bytes: skillContent.length, skillPath });
87
+ }
88
+ catch (err) {
89
+ const detail = err instanceof Error ? err.message : String(err);
90
+ log.warn('phase2', 'SKILL.md read failed; will skip system.transform injection', { detail, skillPath });
91
+ }
92
+ // 成功 — 写 state + mark ready
93
+ const state = Object.freeze({
94
+ activated: true,
95
+ cwdRoot,
96
+ instanceName,
97
+ skillPath,
98
+ skillContent,
99
+ });
100
+ setState(state);
101
+ markReady();
102
+ // v1.5 init-check:plugin 启动时自检 opencode.json 关键配置
103
+ // 只 warn,不 patch
104
+ checkSerenityConfig(cwdRoot, instanceName);
105
+ // v1.7 config-patch:自动改主仓 opencode.json 让 cwdRoot 内 read/edit = allow
106
+ // 用户 m0649 决定"全自动"——plugin 启动即生效(用户需重启 opencode 应用改动)
107
+ if (getClient) {
108
+ try {
109
+ const result = await patchMainRepoOpencodeJson(cwdRoot, () => {
110
+ try {
111
+ return getClient();
112
+ }
113
+ catch {
114
+ return null;
115
+ }
116
+ });
117
+ if (result.changed) {
118
+ log.info('phase2', 'main-repo opencode.json auto-patched', {
119
+ configPath: result.configPath,
120
+ diff: result.diff,
121
+ });
122
+ }
123
+ }
124
+ catch (err) {
125
+ const detail = err instanceof Error ? err.message : String(err);
126
+ log.warn('phase2', 'config-patch failed; plugin continues', { detail });
127
+ }
128
+ }
129
+ }
130
+ function formatErrorMessage(err, fallback) {
131
+ return err instanceof Error ? err.message : fallback;
132
+ }
133
+ //# sourceMappingURL=activation.js.map