@luchenglong/cli-bridge 0.2.4 → 0.2.6

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.
@@ -1,22 +1,13 @@
1
- // providers/opencode.mjs — opencode provider descriptor(第四家 CLI)。
1
+ // providers/opencode.mjs — opencode provider descriptor(第四家 CLI,无旧包可迁)。
2
2
  //
3
- // 移植基线 = 本机实测的 OpenCode **V2** CLI(v2.0.29,桌面版自带二进制
4
- // opencode-cli.exe)+ 官方 CLI 源码 emit 全集。不是旧包(本包新增,无旧包可迁)。
5
- //
6
- // 实测事实(2026-10-15,CLI v2.0.29):
7
- // opencode run [message..] 非交互主命令;prompt 是【位置参数】(run 子命令无
8
- // --prompt;-p 是 --password,切勿当 prompt 用)
9
- // --format json NDJSON 事件流(每行一个 JSON 对象)
10
- // --auto auto-approve 未显式拒绝的权限(不加会卡在权限提示)
11
- // -m, --model provider/model V2 支持 provider/model#variant
12
- // --session <id> / --continue 会话续接(无 `opencode continue` 子命令)
13
- // --agent / --title / --file / --thinking / --standalone
14
- // ⚠️ V2 无 --dir(V1 有)→ cwd 只能靠 spawn 的工作目录
15
- // ⚠️ V2 无 --variant(V1 有)→ variant 并入 --model 的 # 后缀
16
- // 退出码:0 成功 / 1 错误 / 130 SIGINT(退出码才是成败判据)
17
- // 事件 type(源码全集):step_start / text / reasoning / tool_use / step_finish / error
18
- // ⚠️ 不存在 result/complete/init 事件
19
- // ⚠️ 文本在 part.text;统计在 step_finish.part(cost/tokens)——两者是不同事件
3
+ // 基线 = 本机实测 OpenCode V2 CLI v2.0.29(桌面版自带 opencode-cli.exe)+ 源码 emit 全集。
4
+ // 完整 CLI 事实表见 docs/HANDOVER.md §13;这里只留写代码时最容易踩的几条:
5
+ // • 非交互 = `opencode run "<prompt>"`,prompt 是【位置参数】(run 子命令无 --prompt;
6
+ // `-p` 是 --password)。`--format json` → NDJSON。`--auto` = 自动批准权限。
7
+ // • ⚠️ V2 无 --dir(cwd 靠 spawn)、无 --variant(并入 --model#variant)。
8
+ // • 退出码才是成败判据:0 成功 / 1 错误 / 130 SIGINT。
9
+ // • ⚠️ 无 result/complete/init 事件;type 全集 = step_start/text/reasoning/tool_use/
10
+ // step_finish/error。文本在 part.text,统计在 step_finish.part —— 两者是不同事件。
20
11
  //
21
12
  // 红线(PROVIDER-CONTRACT.md §8 + §1):
22
13
  // - 本文件会被 build 内联进 dynamic 沙箱(new Function):绝不出现 import.meta 字面量;
@@ -209,16 +200,35 @@ export default function defineProvider(deps = {}) {
209
200
  let settingsRef = { binPath: '', defaultModel: '', defaultAgent: '' }
210
201
 
211
202
  /**
212
- * 取当前生效的偏好设置(宿主经 hostOpts.getSettings 注入;无则退回 env/默认)。
203
+ * 取当前生效的偏好设置。三条来源按优先级合并,每次 buildArgv 都重新读 ——
204
+ * 面板改默认模型后无需重启即生效:
205
+ * 1) `host.getSettings('opencode')` —— preset/dynamic 形态(attach 时由宿主注入);
206
+ * 2) `deps.readSettings()` —— MCP 形态(真 Node 进程,每次现读设置文件);
207
+ * 3) `settingsRef` —— attach 时固化的一次性快照 / 缺省值。
208
+ * 三条都拿不到 → 缺省空值(= 不传 --model,交给 opencode 自身配置决定)。
209
+ *
210
+ * 历史坑:MCP 形态不 attach(自建 engine),只认 host.getSettings 会让面板设置的
211
+ * defaultModel 在 MCP 面对 opencode_run 完全失效 —— 只有 args.model 才落地。
212
+ * 三个形态行为不一致是缺陷,故此处统一把 readSettings 纳入(与 mimo 同构)。
213
213
  */
214
214
  function currentSettings() {
215
+ const out = Object.assign({}, settingsRef)
216
+ try {
217
+ if (typeof deps.readSettings === 'function') {
218
+ const r = deps.readSettings()
219
+ if (r && typeof r === 'object') {
220
+ const part = ('partition' in r || 'path' in r || 'home' in r) ? r.partition : r
221
+ if (part && typeof part === 'object') Object.assign(out, part)
222
+ }
223
+ }
224
+ } catch (e) { }
215
225
  try {
216
226
  if (host && typeof host.getSettings === 'function') {
217
227
  const s = host.getSettings('opencode')
218
- if (s && typeof s === 'object') return Object.assign({}, settingsRef, s)
228
+ if (s && typeof s === 'object') Object.assign(out, s)
219
229
  }
220
230
  } catch (e) { }
221
- return settingsRef
231
+ return out
222
232
  }
223
233
 
224
234
  const descriptor = {
package/scripts/build.mjs CHANGED
@@ -1,235 +1,262 @@
1
- // scripts/build.mjs — 派生产物生成器(keystone)。
2
- //
3
- // 生成物(勿手改;`--check` 模式漂移即失败,npm test 里的 build-sync 测试调用它):
4
- // 1. dynamic/<id>.host.js ← dynamic/<id>.host.template.mjs
5
- // 把 core/bridge-kit.mjs 内联进 /*__KIT__*/、providers/<id>.mjs 内联进
6
- // /*__PROVIDER__*/、core/scheduling.mjs(+SCHED_ALL_IDS 常量)内联进
7
- // /*__SCHED__*/(动态沙箱禁止 import,只能文本注入;单标记替换防递归错乱)。
8
- // 注入时剥离各段的 `import ... from '...mjs'` 行并把 `export ` 关键字去掉。
9
- // 2. preset/<id>/agent.cordis.yml ← preset/<id>/plugin-list.mjs(唯一权威源)
10
- // 纯 JS 插件清单落地为 legacy 目录形态 YAML({__jsExpr} → !!js 块)。
11
- // 两者同源生成,结构性消除「yml 与声明制漂移」这一历史风险。
12
- // 3. home-plugin/cli-bridge-indicator/lib/preset-definitions/<id>.mjs
13
- // ← 同上(声明制形态:插件数组直接给 agentPresets.register 消费)。
14
- // ⚠️ 这一份由 preset-declarations.mjs 直接 import plugin-list,本脚本不再复制;
15
- // 保留目录仅作兼容占位(旧版生成物清理)。
16
- //
17
- // 用法:node scripts/build.mjs [--check]
18
-
19
- import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync } from 'node:fs'
20
- import { dirname, join, resolve } from 'node:path'
21
- import { fileURLToPath } from 'node:url'
22
- import { pathToFileURL } from 'node:url'
23
- import { PROVIDER_IDS } from '../providers/registry.mjs'
24
-
25
- const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
26
- // 单一 id 轴:新 provider 只改 registry(PROVIDER-CONTRACT.md §3.1),build 派生
27
- // 物(dynamic 宿主 / SCHED_ALL_IDS 内联 / legacy YAML / plugin-list)自动跟上。
28
- const ids = PROVIDER_IDS
29
- const PKG = '@luchenglong/cli-bridge'
30
-
31
- const MODE = process.argv.includes('--check') ? 'check' : 'write'
32
- // --only=<id> 让单个 provider 的作者只生成/校验自己的派生物,避免并发写他人产物。
33
- const onlyArg = process.argv.find((a) => a.startsWith('--only='))
34
- const ONLY = onlyArg ? onlyArg.slice('--only='.length) : null
35
- const drift = []
36
-
37
- function read(rel) { return readFileSync(join(ROOT, rel), 'utf8') }
38
-
39
- function emit(rel, content) {
40
- const abs = join(ROOT, rel)
41
- if (MODE === 'check') {
42
- const cur = existsSync(abs) ? readFileSync(abs, 'utf8') : null
43
- if (cur !== content) drift.push(rel)
44
- return
45
- }
46
- mkdirSync(dirname(abs), { recursive: true })
47
- writeFileSync(abs, content, 'utf8')
48
- console.log('generated ' + rel)
49
- }
50
-
51
- // ── 1. dynamic host(文本内联,沙箱禁 import)──────────────────────────────────
52
- function stripModuleSyntax(src, label) {
53
- const out = src
54
- .split('\n')
55
- .filter((l) => !/^\s*import\s[^(]*from\s*['"][^'"]+['"];?\s*$/.test(l) && !/^\s*import\s*['"]/.test(l))
56
- .join('\n')
57
- .replace(/^export\s+(const|let|var|function|async function|class)\s/gm, '$1 ')
58
- .replace(/^export\s+default\s+(async\s+)?function\s+([A-Za-z0-9_$]+)\s*/gm, '$1function $2 ')
59
- .replace(/^export\s+default\s+(async\s+)?function\s*\(/gm, '$1function __default__(')
60
- .replace(/^export\s*\{[^}]*\};?\s*$/gm, '')
61
- // 内联目标是「脚本体」:残留的 export default/import 说明源用了本转换器不支持
62
- // 的形态。静默产出坏代码会让 dynamic host 在沙箱里炸得莫名其妙,必须大声失败。
63
- if (/^\s*(export\s+default|import\s|import\()/m.test(out)) {
64
- throw new Error('build: ' + (label || 'source') + ' 用了沙箱内联不支持的模块形态(provider 需具名 `export default function defineProvider(...)`;bridge-kit 不得 import)')
65
- }
66
- return out
67
- }
68
-
69
- function buildDynamicHost(id) {
70
- const tplRel = 'dynamic/' + id + '.host.template.mjs'
71
- if (!existsSync(join(ROOT, tplRel))) return null
72
- const tpl = read(tplRel)
73
- const kit = stripModuleSyntax(read('core/bridge-kit.mjs'), 'core/bridge-kit.mjs')
74
- const prov = stripModuleSyntax(read('providers/' + id + '.mjs'), 'providers/' + id + '.mjs')
75
- // 调度单源(0.2.0):core/scheduling.mjs 无 import,可安全内联进沙箱作用域。
76
- // SCHED_ALL_IDS 由构建注入(全量 provider 清单)—— 沙箱禁 import,拿不到 registry。
77
- // 模板没有 /*__SCHED__*/ 占位(旧模板)→ 不注入,保持向后兼容。
78
- const sched = stripModuleSyntax(read('core/scheduling.mjs'), 'core/scheduling.mjs')
79
- + '\nconst SCHED_ALL_IDS = ' + JSON.stringify(ids) + '\n'
80
- if (!tpl.includes('/*__KIT__*/') || !tpl.includes('/*__PROVIDER__*/')) {
81
- throw new Error(tplRel + ' missing /*__KIT__*/ or /*__PROVIDER__*/ placeholder')
82
- }
83
- let out = tpl.replace('/*__KIT__*/', () => kit).replace('/*__PROVIDER__*/', () => prov)
84
- if (out.includes('/*__SCHED__*/')) out = out.replace('/*__SCHED__*/', () => sched)
85
- else if (tpl.includes('/*__SCHED__*/')) throw new Error(tplRel + ': __SCHED__ placeholder vanished after other replacements')
86
- return out
87
- }
88
-
89
- for (const id of ids) {
90
- if (ONLY && id !== ONLY) continue
91
- const out = buildDynamicHost(id)
92
- if (out) emit('dynamic/' + id + '.host.js', out)
93
- }
94
-
95
- // ── 2. agent.cordis.yml ← plugin-list.mjs ─────────────────────────────────────
96
- function yamlString(v) {
97
- const s = String(v)
98
- if (s === '' || /[:#\n'"\[\]{}&*!|>%@`]/.test(s) || /^\s|\s$/.test(s)) {
99
- return "'" + s.replace(/'/g, "''") + "'"
100
- }
101
- return s
102
- }
103
-
104
- function yamlEntry(entry, indent) {
105
- // 递归序列化一个插件条目。字段序与官方 patch.yml 习惯一致:
106
- // id → name → entry → group → isolate → disabled/if → config。
107
- const pad = ' '.repeat(indent)
108
- const lines = [pad + '- id: ' + yamlString(entry.id)]
109
- if (entry.name !== undefined) lines.push(pad + ' name: ' + yamlString(entry.name))
110
- if (entry.entry !== undefined) lines.push(pad + ' entry: ' + yamlString(entry.entry))
111
- if (entry.group === true) lines.push(pad + ' group: true')
112
- if (entry.isolate && typeof entry.isolate === 'object') {
113
- lines.push(pad + ' isolate:')
114
- for (const k of Object.keys(entry.isolate)) lines.push(pad + ' ' + k + ': ' + String(entry.isolate[k]))
115
- }
116
- for (const k of ['disabled', 'if']) {
117
- if (entry[k] === undefined) continue
118
- const v = entry[k]
119
- if (v && typeof v === 'object' && typeof v.__jsExpr === 'string') lines.push(pad + ' ' + k + ": !!js '" + String(v.__jsExpr).replace(/'/g, "''") + "'")
120
- else lines.push(pad + ' ' + k + ': ' + String(v))
121
- }
122
- if (entry.config !== undefined) {
123
- if (Array.isArray(entry.config)) {
124
- lines.push(pad + ' config:')
125
- if (!entry.config.length) lines[lines.length - 1] = pad + ' config: []'
126
- for (const child of entry.config) lines.push(yamlEntry(child, indent + 4))
127
- } else if (entry.config && typeof entry.config === 'object') {
128
- lines.push(yamlConfig(entry.config, indent + 2))
129
- } else {
130
- lines.push(pad + ' config: ' + yamlString(entry.config))
131
- }
132
- }
133
- return lines.join('\n')
134
- }
135
-
136
- function yamlValue(v) {
137
- // '' 返回值 = 「值是嵌套结构/多行文本,需换行展开」;否则返回 ' 标量' 后缀。
138
- if (v && typeof v === 'object' && typeof v.__jsExpr === 'string') return " !!js '" + String(v.__jsExpr).replace(/'/g, "''") + "'"
139
- if (Array.isArray(v)) return v.length ? '' : ' []'
140
- if (v && typeof v === 'object') return ''
141
- if (typeof v === 'string') { if (v.indexOf('\n') >= 0) return ''; return ' ' + yamlString(v) }
142
- return ' ' + String(v)
143
- }
144
-
145
- // 多行字符串 → YAML 块标量('|' 保留结尾单换行;'|-' 裁掉)。行内引号折叠会改变
146
- // 文本语义(plan-mode 段含逐字换行),必须用块标量原样落地。
147
- function yamlBlockKey(key, v, keyPad, valPad) {
148
- const clip = v.endsWith('\n') ? '|' : '|-'
149
- const body = (clip === '|' ? v.replace(/\n$/, '') : v).split('\n')
150
- const lines = [keyPad + key + ': ' + clip]
151
- for (const l of body) lines.push(l === '' ? '' : valPad + l)
152
- return lines
153
- }
154
-
155
- function yamlConfig(config, indent) {
156
- // indent:'config:' 键本身的列;子键 +2。
157
- const pad = ' '.repeat(indent)
158
- const lines = [pad + 'config:']
159
- for (const k of Object.keys(config)) {
160
- const v = config[k]
161
- const inline = yamlValue(v)
162
- if (inline !== '') { lines.push(pad + ' ' + k + ':' + inline); continue }
163
- if (typeof v === 'string') { lines.push(...yamlBlockKey(k, v, pad + ' ', pad + ' ')); continue }
164
- if (Array.isArray(v)) {
165
- lines.push(pad + ' ' + k + ':')
166
- for (const item of v) {
167
- if (item && typeof item === 'object' && !item.__jsExpr) {
168
- const keys = Object.keys(item)
169
- lines.push(pad + ' - ' + keys[0] + ': ' + yamlString(item[keys[0]]))
170
- for (let i = 1; i < keys.length; i++) lines.push(pad + ' ' + keys[i] + ': ' + yamlString(item[keys[i]]))
171
- } else lines.push(pad + ' - ' + yamlString(item))
172
- }
173
- } else {
174
- lines.push(pad + ' ' + k + ':')
175
- lines.push(yamlInlineObject(v, indent + 4))
176
- }
177
- }
178
- return lines.join('\n')
179
- }
180
-
181
- function yamlInlineObject(obj, indent) {
182
- const pad = ' '.repeat(indent)
183
- const lines = []
184
- for (const k of Object.keys(obj)) {
185
- const v = obj[k]
186
- const inline = yamlValue(v)
187
- if (inline !== '') lines.push(pad + k + ':' + inline)
188
- else if (typeof v === 'string') lines.push(...yamlBlockKey(k, v, pad, pad + ' '))
189
- else {
190
- lines.push(pad + k + ':')
191
- if (Array.isArray(v)) { for (const item of v) lines.push(pad + ' - ' + yamlString(item)) }
192
- else lines.push(yamlInlineObject(v, indent + 2))
193
- }
194
- }
195
- return lines.join('\n')
196
- }
197
-
198
- const PRESET_DIRS = { agy: 'agy-first', codebuddy: 'codebuddy-first', mimo: 'mimo-first', opencode: 'opencode-first' }
199
-
200
- async function loadPluginList(id) {
201
- const rel = 'preset/' + PRESET_DIRS[id] + '/plugin-list.mjs'
202
- const abs = join(ROOT, rel)
203
- if (!existsSync(abs)) return null
204
- const mod = await import(pathToFileURL(abs).href)
205
- return mod
206
- }
207
-
208
- for (const id of ids) {
209
- if (ONLY && id !== ONLY) continue
210
- const mod = await loadPluginList(id)
211
- if (!mod || typeof mod.buildPlugins !== 'function') continue
212
- const dir = PRESET_DIRS[id]
213
- const meta = mod.meta || {}
214
- const plugins = mod.buildPlugins({ bridgeName: PKG + '/preset-' + id })
215
- const head = [
216
- '# 生成物 — 由 node scripts/build.mjs 从 preset/' + dir + '/plugin-list.mjs 生成,勿手改。',
217
- '# legacy 目录形态($DSH_HOME/.agent-presets/' + meta.id + '/);新 DSH 走声明制',
218
- '# (home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs 直接 import 同一源)。',
219
- ''
220
- ].join('\n')
221
- const body = plugins.map((e) => yamlEntry(e, 0)).join('\n')
222
- emit('preset/' + dir + '/agent.cordis.yml', head + body + '\n')
223
- emit('preset/' + dir + '/preset.yml', [
224
- 'name: ' + yamlString(meta.name || meta.id),
225
- 'description: ' + yamlString(meta.description || ''),
226
- 'order: ' + String(meta.order != null ? meta.order : 10),
227
- ''
228
- ].join('\n'))
229
- }
230
-
231
- if (drift.length) {
232
- console.error('[build] drift detected: ' + drift.join(', ') + ' — run `node scripts/build.mjs`')
233
- process.exit(1)
234
- }
235
- if (MODE === 'check') console.log('[build] up to date')
1
+ // scripts/build.mjs — 派生产物生成器(keystone)。
2
+ //
3
+ // 生成物(勿手改;`--check` 模式漂移即失败,npm test 里的 build-sync 测试调用它):
4
+ // 1. dynamic/<id>.host.js ← dynamic/<id>.host.template.mjs
5
+ // 把 core/bridge-kit.mjs 内联进 /*__KIT__*/、providers/<id>.mjs 内联进
6
+ // /*__PROVIDER__*/、core/scheduling.mjs(+SCHED_ALL_IDS 常量)内联进
7
+ // /*__SCHED__*/(动态沙箱禁止 import,只能文本注入;单标记替换防递归错乱)。
8
+ // 注入时剥离各段的 `import ... from '...mjs'` 行并把 `export ` 关键字去掉。
9
+ // 2. preset/<id>/agent.cordis.yml ← preset/<id>/plugin-list.mjs(唯一权威源)
10
+ // 纯 JS 插件清单落地为 legacy 目录形态 YAML({__jsExpr} → !!js 块)。
11
+ // 两者同源生成,结构性消除「yml 与声明制漂移」这一历史风险。
12
+ // 3. home-plugin/cli-bridge-indicator/lib/preset-definitions/<id>.mjs
13
+ // ← 同上(声明制形态:插件数组直接给 agentPresets.register 消费)。
14
+ // ⚠️ 这一份由 preset-declarations.mjs 直接 import plugin-list,本脚本不再复制;
15
+ // 保留目录仅作兼容占位(旧版生成物清理)。
16
+ //
17
+ // 用法:node scripts/build.mjs [--check]
18
+
19
+ import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, readdirSync } from 'node:fs'
20
+ import { dirname, join, resolve } from 'node:path'
21
+ import { fileURLToPath } from 'node:url'
22
+ import { pathToFileURL } from 'node:url'
23
+ import { PROVIDER_IDS } from '../providers/registry.mjs'
24
+
25
+ const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
26
+ // 单一 id 轴:新 provider 只改 registry(PROVIDER-CONTRACT.md §3.1),build 派生
27
+ // 物(dynamic 宿主 / SCHED_ALL_IDS 内联 / legacy YAML / plugin-list)自动跟上。
28
+ const ids = PROVIDER_IDS
29
+ const PKG = '@luchenglong/cli-bridge'
30
+
31
+ const MODE = process.argv.includes('--check') ? 'check' : 'write'
32
+ // --only=<id> 让单个 provider 的作者只生成/校验自己的派生物,避免并发写他人产物。
33
+ const onlyArg = process.argv.find((a) => a.startsWith('--only='))
34
+ const ONLY = onlyArg ? onlyArg.slice('--only='.length) : null
35
+ const drift = []
36
+
37
+ function read(rel) { return readFileSync(join(ROOT, rel), 'utf8') }
38
+
39
+ function emit(rel, content) {
40
+ const abs = join(ROOT, rel)
41
+ if (MODE === 'check') {
42
+ const cur = existsSync(abs) ? readFileSync(abs, 'utf8') : null
43
+ if (cur !== content) drift.push(rel)
44
+ return
45
+ }
46
+ mkdirSync(dirname(abs), { recursive: true })
47
+ writeFileSync(abs, content, 'utf8')
48
+ console.log('generated ' + rel)
49
+ }
50
+
51
+ // ── 1. dynamic host(文本内联,沙箱禁 import)──────────────────────────────────
52
+ function stripModuleSyntax(src, label) {
53
+ // 多行 import(`import {\n a, b\n} from '...'`)在单行过滤下会残留 `import ` 前缀并
54
+ // 触发下面的守卫误报 —— 先把跨行 import 折叠成一行再过滤(与单行同规则)。
55
+ const collapsed = src.replace(
56
+ /^\s*import\s*\{[^}]*\}\s*from\s*['"][^'"]+['"];?\s*$/gms,
57
+ (m) => '// ' + m.replace(/\s+/g, ' ').trim()
58
+ )
59
+ const out = collapsed
60
+ .split('\n')
61
+ .filter((l) => !/^\s*import\s[^(]*from\s*['"][^'"]+['"];?\s*$/.test(l) && !/^\s*import\s*['"]/.test(l))
62
+ .join('\n')
63
+ // 缩进形态也要剥:`export function f()` 可能带前导空格(从 descriptor 闭包里
64
+ // 搬出来的函数保留原缩进),只匹配行首会漏掉 → 残留 export 在脚本体里直接语法错。
65
+ .replace(/^(\s*)export\s+(const|let|var|function|async function|class)\s/gm, '$1$2 ')
66
+ .replace(/^(\s*)export\s+default\s+(async\s+)?function\s+([A-Za-z0-9_$]+)\s*/gm, '$1$2function $3 ')
67
+ .replace(/^(\s*)export\s+default\s+(async\s+)?function\s*\(/gm, '$1$2function __default__(')
68
+ .replace(/^(\s*)export\s*\{[^}]*\};?\s*$/gm, '')
69
+ // 内联目标是「脚本体」:残留的 export default/import 说明源用了本转换器不支持
70
+ // 的形态。静默产出坏代码会让 dynamic host 在沙箱里炸得莫名其妙,必须大声失败。
71
+ if (/^\s*(export\s+default|import\s|import\()/m.test(out)) {
72
+ throw new Error('build: ' + (label || 'source') + ' 用了沙箱内联不支持的模块形态(provider 需具名 `export default function defineProvider(...)`;bridge-kit 不得 import)')
73
+ }
74
+ return out
75
+ }
76
+
77
+ function buildDynamicHost(id) {
78
+ const tplRel = 'dynamic/' + id + '.host.template.mjs'
79
+ if (!existsSync(join(ROOT, tplRel))) return null
80
+ const tpl = read(tplRel)
81
+ const kit = stripModuleSyntax(read('core/bridge-kit.mjs'), 'core/bridge-kit.mjs')
82
+ const prov = stripModuleSyntax(read('providers/' + id + '.mjs'), 'providers/' + id + '.mjs')
83
+ // 调度单源(0.2.0):core/scheduling.mjs 无 import,可安全内联进沙箱作用域。
84
+ // SCHED_ALL_IDS 由构建注入(全量 provider 清单)—— 沙箱禁 import,拿不到 registry。
85
+ // 模板没有 /*__SCHED__*/ 占位(旧模板)→ 不注入,保持向后兼容。
86
+ const sched = stripModuleSyntax(read('core/scheduling.mjs'), 'core/scheduling.mjs')
87
+ + '\nconst SCHED_ALL_IDS = ' + JSON.stringify(ids) + '\n'
88
+ // 可选的多段内联:providers/<id>-*.mjs(如 <id>-catalog.mjs、<id>-models-tool.mjs)。
89
+ // 语义与 __SCHED__ 同构(模板没有该占位符 → 不注入,向后兼容)。
90
+ // 目的:providers/<id>.mjs 超 600 行红线时把纯逻辑拆出去,沙箱里仍是**单脚本**。
91
+ // 支持多个片文件(按文件名字典序拼接)而不是写死一个:每撞一次 600 行红线就改一次
92
+ // 构建器的做法不可持续,泛用性与顺序确定性都要有。
93
+ const extraFiles = readdirSync(join(ROOT, 'providers'))
94
+ .filter((f) => f.startsWith(id + '-') && f.endsWith('.mjs'))
95
+ .sort()
96
+ const extra = extraFiles.length
97
+ ? extraFiles.map((f) => {
98
+ const rel = 'providers/' + f
99
+ return '// ── inlined from ' + rel + ' ──\n' + stripModuleSyntax(read(rel), rel)
100
+ }).join('\n')
101
+ : null
102
+ if (!tpl.includes('/*__KIT__*/') || !tpl.includes('/*__PROVIDER__*/')) {
103
+ throw new Error(tplRel + ' missing /*__KIT__*/ or /*__PROVIDER__*/ placeholder')
104
+ }
105
+ let out = tpl.replace('/*__KIT__*/', () => kit).replace('/*__PROVIDER__*/', () => prov)
106
+ // __EXTRA__ 放在 KIT/PROVIDER 之后替换:避免其内容里若含同名占位符被二次展开。
107
+ if (out.includes('/*__EXTRA__*/')) {
108
+ if (!extra) throw new Error(tplRel + ': __EXTRA__ placeholder present but no providers/' + id + '-*.mjs fragment found')
109
+ out = out.replace('/*__EXTRA__*/', () => extra)
110
+ } else if (tpl.includes('/*__EXTRA__*/')) throw new Error(tplRel + ': __EXTRA__ placeholder vanished after other replacements')
111
+ if (out.includes('/*__SCHED__*/')) out = out.replace('/*__SCHED__*/', () => sched)
112
+ else if (tpl.includes('/*__SCHED__*/')) throw new Error(tplRel + ': __SCHED__ placeholder vanished after other replacements')
113
+ return out
114
+ }
115
+
116
+ for (const id of ids) {
117
+ if (ONLY && id !== ONLY) continue
118
+ const out = buildDynamicHost(id)
119
+ if (out) emit('dynamic/' + id + '.host.js', out)
120
+ }
121
+
122
+ // ── 2. agent.cordis.yml ← plugin-list.mjs ─────────────────────────────────────
123
+ function yamlString(v) {
124
+ const s = String(v)
125
+ if (s === '' || /[:#\n'"\[\]{}&*!|>%@`]/.test(s) || /^\s|\s$/.test(s)) {
126
+ return "'" + s.replace(/'/g, "''") + "'"
127
+ }
128
+ return s
129
+ }
130
+
131
+ function yamlEntry(entry, indent) {
132
+ // 递归序列化一个插件条目。字段序与官方 patch.yml 习惯一致:
133
+ // id → name → entry → group → isolate → disabled/if → config。
134
+ const pad = ' '.repeat(indent)
135
+ const lines = [pad + '- id: ' + yamlString(entry.id)]
136
+ if (entry.name !== undefined) lines.push(pad + ' name: ' + yamlString(entry.name))
137
+ if (entry.entry !== undefined) lines.push(pad + ' entry: ' + yamlString(entry.entry))
138
+ if (entry.group === true) lines.push(pad + ' group: true')
139
+ if (entry.isolate && typeof entry.isolate === 'object') {
140
+ lines.push(pad + ' isolate:')
141
+ for (const k of Object.keys(entry.isolate)) lines.push(pad + ' ' + k + ': ' + String(entry.isolate[k]))
142
+ }
143
+ for (const k of ['disabled', 'if']) {
144
+ if (entry[k] === undefined) continue
145
+ const v = entry[k]
146
+ if (v && typeof v === 'object' && typeof v.__jsExpr === 'string') lines.push(pad + ' ' + k + ": !!js '" + String(v.__jsExpr).replace(/'/g, "''") + "'")
147
+ else lines.push(pad + ' ' + k + ': ' + String(v))
148
+ }
149
+ if (entry.config !== undefined) {
150
+ if (Array.isArray(entry.config)) {
151
+ lines.push(pad + ' config:')
152
+ if (!entry.config.length) lines[lines.length - 1] = pad + ' config: []'
153
+ for (const child of entry.config) lines.push(yamlEntry(child, indent + 4))
154
+ } else if (entry.config && typeof entry.config === 'object') {
155
+ lines.push(yamlConfig(entry.config, indent + 2))
156
+ } else {
157
+ lines.push(pad + ' config: ' + yamlString(entry.config))
158
+ }
159
+ }
160
+ return lines.join('\n')
161
+ }
162
+
163
+ function yamlValue(v) {
164
+ // '' 返回值 = 「值是嵌套结构/多行文本,需换行展开」;否则返回 ' 标量' 后缀。
165
+ if (v && typeof v === 'object' && typeof v.__jsExpr === 'string') return " !!js '" + String(v.__jsExpr).replace(/'/g, "''") + "'"
166
+ if (Array.isArray(v)) return v.length ? '' : ' []'
167
+ if (v && typeof v === 'object') return ''
168
+ if (typeof v === 'string') { if (v.indexOf('\n') >= 0) return ''; return ' ' + yamlString(v) }
169
+ return ' ' + String(v)
170
+ }
171
+
172
+ // 多行字符串 → YAML 块标量('|' 保留结尾单换行;'|-' 裁掉)。行内引号折叠会改变
173
+ // 文本语义(plan-mode 段含逐字换行),必须用块标量原样落地。
174
+ function yamlBlockKey(key, v, keyPad, valPad) {
175
+ const clip = v.endsWith('\n') ? '|' : '|-'
176
+ const body = (clip === '|' ? v.replace(/\n$/, '') : v).split('\n')
177
+ const lines = [keyPad + key + ': ' + clip]
178
+ for (const l of body) lines.push(l === '' ? '' : valPad + l)
179
+ return lines
180
+ }
181
+
182
+ function yamlConfig(config, indent) {
183
+ // indent:'config:' 键本身的列;子键 +2。
184
+ const pad = ' '.repeat(indent)
185
+ const lines = [pad + 'config:']
186
+ for (const k of Object.keys(config)) {
187
+ const v = config[k]
188
+ const inline = yamlValue(v)
189
+ if (inline !== '') { lines.push(pad + ' ' + k + ':' + inline); continue }
190
+ if (typeof v === 'string') { lines.push(...yamlBlockKey(k, v, pad + ' ', pad + ' ')); continue }
191
+ if (Array.isArray(v)) {
192
+ lines.push(pad + ' ' + k + ':')
193
+ for (const item of v) {
194
+ if (item && typeof item === 'object' && !item.__jsExpr) {
195
+ const keys = Object.keys(item)
196
+ lines.push(pad + ' - ' + keys[0] + ': ' + yamlString(item[keys[0]]))
197
+ for (let i = 1; i < keys.length; i++) lines.push(pad + ' ' + keys[i] + ': ' + yamlString(item[keys[i]]))
198
+ } else lines.push(pad + ' - ' + yamlString(item))
199
+ }
200
+ } else {
201
+ lines.push(pad + ' ' + k + ':')
202
+ lines.push(yamlInlineObject(v, indent + 4))
203
+ }
204
+ }
205
+ return lines.join('\n')
206
+ }
207
+
208
+ function yamlInlineObject(obj, indent) {
209
+ const pad = ' '.repeat(indent)
210
+ const lines = []
211
+ for (const k of Object.keys(obj)) {
212
+ const v = obj[k]
213
+ const inline = yamlValue(v)
214
+ if (inline !== '') lines.push(pad + k + ':' + inline)
215
+ else if (typeof v === 'string') lines.push(...yamlBlockKey(k, v, pad, pad + ' '))
216
+ else {
217
+ lines.push(pad + k + ':')
218
+ if (Array.isArray(v)) { for (const item of v) lines.push(pad + ' - ' + yamlString(item)) }
219
+ else lines.push(yamlInlineObject(v, indent + 2))
220
+ }
221
+ }
222
+ return lines.join('\n')
223
+ }
224
+
225
+ const PRESET_DIRS = { agy: 'agy-first', codebuddy: 'codebuddy-first', mimo: 'mimo-first', opencode: 'opencode-first' }
226
+
227
+ async function loadPluginList(id) {
228
+ const rel = 'preset/' + PRESET_DIRS[id] + '/plugin-list.mjs'
229
+ const abs = join(ROOT, rel)
230
+ if (!existsSync(abs)) return null
231
+ const mod = await import(pathToFileURL(abs).href)
232
+ return mod
233
+ }
234
+
235
+ for (const id of ids) {
236
+ if (ONLY && id !== ONLY) continue
237
+ const mod = await loadPluginList(id)
238
+ if (!mod || typeof mod.buildPlugins !== 'function') continue
239
+ const dir = PRESET_DIRS[id]
240
+ const meta = mod.meta || {}
241
+ const plugins = mod.buildPlugins({ bridgeName: PKG + '/preset-' + id })
242
+ const head = [
243
+ '# 生成物 — 由 node scripts/build.mjs 从 preset/' + dir + '/plugin-list.mjs 生成,勿手改。',
244
+ '# legacy 目录形态($DSH_HOME/.agent-presets/' + meta.id + '/);新 DSH 走声明制',
245
+ '# (home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs 直接 import 同一源)。',
246
+ ''
247
+ ].join('\n')
248
+ const body = plugins.map((e) => yamlEntry(e, 0)).join('\n')
249
+ emit('preset/' + dir + '/agent.cordis.yml', head + body + '\n')
250
+ emit('preset/' + dir + '/preset.yml', [
251
+ 'name: ' + yamlString(meta.name || meta.id),
252
+ 'description: ' + yamlString(meta.description || ''),
253
+ 'order: ' + String(meta.order != null ? meta.order : 10),
254
+ ''
255
+ ].join('\n'))
256
+ }
257
+
258
+ if (drift.length) {
259
+ console.error('[build] drift detected: ' + drift.join(', ') + ' — run `node scripts/build.mjs`')
260
+ process.exit(1)
261
+ }
262
+ if (MODE === 'check') console.log('[build] up to date')