@luchenglong/cli-bridge 0.0.0-stage → 0.2.2
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/CHANGELOG.md +148 -0
- package/LICENSE +21 -0
- package/MCP-POLICY.md +53 -0
- package/PROVIDER-CONTRACT.md +235 -0
- package/README.en.md +98 -0
- package/README.md +150 -2
- package/assets/indicator-states.svg +62 -0
- package/bin/agy-quota.mjs +223 -0
- package/core/bridge-kit.mjs +549 -0
- package/core/scheduling.mjs +149 -0
- package/docs/ARCHITECTURE.md +117 -0
- package/docs/FALLBACK-AND-INDICATOR.md +93 -0
- package/docs/INSTALL.md +118 -0
- package/docs/MCP-POLICY.zh.md +49 -0
- package/docs/MIGRATION.md +43 -0
- package/docs/RELEASE-SOP.md +77 -0
- package/docs/history/agy/CHANGELOG.md +326 -0
- package/docs/history/codebuddy/CHANGELOG.md +668 -0
- package/docs/history/mimo/CHANGELOG.md +346 -0
- package/dynamic/agy.host.js +1237 -0
- package/dynamic/agy.host.template.mjs +140 -0
- package/dynamic/codebuddy.host.js +1787 -0
- package/dynamic/codebuddy.host.template.mjs +162 -0
- package/dynamic/mimo.host.js +1732 -0
- package/dynamic/mimo.host.template.mjs +169 -0
- package/home-plugin/cli-bridge-indicator/cordis.patch.yml +31 -0
- package/home-plugin/cli-bridge-indicator/lib/client.js +606 -0
- package/home-plugin/cli-bridge-indicator/lib/index.mjs +420 -0
- package/home-plugin/cli-bridge-indicator/lib/preset-declarations.mjs +137 -0
- package/home-plugin/cli-bridge-indicator/lib/settings.mjs +300 -0
- package/mcp/agy-mcp-server.mjs +6 -0
- package/mcp/cli-bridge-mcp.mjs +184 -0
- package/mcp/codebuddy-mcp-server.mjs +6 -0
- package/mcp/mimo-mcp-server.mjs +6 -0
- package/mcp/providers/agy.mjs +254 -0
- package/mcp/providers/codebuddy.mjs +397 -0
- package/mcp/providers/mimo.mjs +290 -0
- package/package.json +87 -4
- package/preset/agy-first/agent.cordis.yml +141 -0
- package/preset/agy-first/cli-bridge.mjs +169 -0
- package/preset/agy-first/plugin-list.mjs +26 -0
- package/preset/agy-first/preset.yml +3 -0
- package/preset/codebuddy-first/agent.cordis.yml +141 -0
- package/preset/codebuddy-first/cli-bridge.mjs +188 -0
- package/preset/codebuddy-first/plugin-list.mjs +20 -0
- package/preset/codebuddy-first/preset.yml +3 -0
- package/preset/mimo-first/agent.cordis.yml +143 -0
- package/preset/mimo-first/cli-bridge.mjs +236 -0
- package/preset/mimo-first/plugin-list.mjs +39 -0
- package/preset/mimo-first/preset.yml +3 -0
- package/preset/standard-base.mjs +102 -0
- package/providers/agy.mjs +398 -0
- package/providers/codebuddy.mjs +926 -0
- package/providers/mimo.mjs +864 -0
- package/providers/registry.mjs +34 -0
- package/scripts/build.mjs +235 -0
- package/scripts/deploy.mjs +61 -0
- package/scripts/verify.mjs +148 -0
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// providers/registry.mjs — provider 注册表(keystone,Lead 单作者)。
|
|
2
|
+
//
|
|
3
|
+
// 静态导入三个 provider 描述符工厂;指示灯 host 半、统一 MCP 服务器、verify 都从这里
|
|
4
|
+
// 取数。描述符契约见 PROVIDER-CONTRACT.md §1。新增 provider = 加一行 import + 一项 defs。
|
|
5
|
+
import defineAgy from './agy.mjs'
|
|
6
|
+
import defineCodebuddy from './codebuddy.mjs'
|
|
7
|
+
import defineMimo from './mimo.mjs'
|
|
8
|
+
|
|
9
|
+
/** 稳定顺序(家级灯渲染顺序 = 此顺序)。 */
|
|
10
|
+
export const PROVIDER_IDS = ['agy', 'codebuddy', 'mimo']
|
|
11
|
+
|
|
12
|
+
const defs = {
|
|
13
|
+
agy: defineAgy,
|
|
14
|
+
codebuddy: defineCodebuddy,
|
|
15
|
+
mimo: defineMimo
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* 构造某 provider 的描述符实例。
|
|
20
|
+
* @param {string} id PROVIDER_IDS 之一。
|
|
21
|
+
* @param {object} [deps] 宿主形态注入({ env, process, nodeFs, nodeChild, nodePath, log })。
|
|
22
|
+
* @returns descriptor(见 PROVIDER-CONTRACT.md §1)
|
|
23
|
+
*/
|
|
24
|
+
export function createProvider(id, deps) {
|
|
25
|
+
const factory = defs[id]
|
|
26
|
+
if (!factory) throw new Error('cli-bridge: unknown provider ' + JSON.stringify(id))
|
|
27
|
+
return factory(deps || {})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** 轻量元信息(不构造描述符也能拿到,用于路由/展示层预建骨架)。 */
|
|
31
|
+
export function providerMeta(id) {
|
|
32
|
+
const d = createProvider(id, { env: {} })
|
|
33
|
+
return { id: d.id, label: d.label, presetId: d.presetId, defaultBackend: d.defaultBackend || null }
|
|
34
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
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' }
|
|
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')
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/deploy.mjs — 本地部署暂存(安全版)。
|
|
3
|
+
//
|
|
4
|
+
// 先跑 build(生成物就位),再把工作树打包到目标目录(排除 .git/node_modules/tests/探针),
|
|
5
|
+
// 默认 {DSH_HOME}/staging/cli-bridge/,供停服后手工接线。
|
|
6
|
+
//
|
|
7
|
+
// ⚠️ 本脚本【不】改 profile 的 package.json / bundles 列表(红线 2:切换运行时接线
|
|
8
|
+
// 必须先停 DSH 后端;本次整合的交付决定是「不切」)。挂载步骤见输出提示与 README。
|
|
9
|
+
//
|
|
10
|
+
// 用法:node scripts/deploy.mjs [--target=<dir>] [--clean]
|
|
11
|
+
|
|
12
|
+
import { existsSync, mkdirSync, rmSync, cpSync, readFileSync, statSync } from 'node:fs'
|
|
13
|
+
import { spawnSync } from 'node:child_process'
|
|
14
|
+
import { dirname, join, resolve } from 'node:path'
|
|
15
|
+
import { fileURLToPath } from 'node:url'
|
|
16
|
+
|
|
17
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
18
|
+
|
|
19
|
+
function detectDshHome() {
|
|
20
|
+
const e = process.env
|
|
21
|
+
if (e.DSH_HOME) return e.DSH_HOME
|
|
22
|
+
if (process.platform === 'win32' && e.APPDATA) return join(e.APPDATA, 'DSH Desktop', 'dsh-home')
|
|
23
|
+
if (e.HOME) return join(e.HOME, '.dsh')
|
|
24
|
+
return join(ROOT, '.dsh')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const arg = process.argv.find((a) => a.startsWith('--target='))
|
|
28
|
+
const target = resolve(arg ? arg.slice('--target='.length) : join(detectDshHome(), 'staging', 'cli-bridge'))
|
|
29
|
+
if (target === ROOT) { console.error('deploy: refusing to deploy onto the repo root'); process.exit(1) }
|
|
30
|
+
// 目标在仓库内部(含 .tmp)会让 cpSync「复制进自身子目录」直接 EINVAL;提前拦截并给出人话。
|
|
31
|
+
const sep = process.platform === 'win32' ? '\\' : '/'
|
|
32
|
+
if (target.startsWith(ROOT + sep) || ROOT.startsWith(target + sep)) {
|
|
33
|
+
console.error('deploy: target must be OUTSIDE the repo (got ' + target + '); default is {DSH_HOME}/staging/cli-bridge')
|
|
34
|
+
process.exit(1)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const b = spawnSync(process.execPath, [join(ROOT, 'scripts', 'build.mjs')], { stdio: 'inherit' })
|
|
38
|
+
if (b.status !== 0) { console.error('deploy: build failed'); process.exit(1) }
|
|
39
|
+
|
|
40
|
+
if (process.argv.includes('--clean') && existsSync(target)) rmSync(target, { recursive: true, force: true })
|
|
41
|
+
mkdirSync(target, { recursive: true })
|
|
42
|
+
|
|
43
|
+
const skip = new Set(['.git', 'node_modules', 'tests', '.probe', '.tmp', 'runtime-data'])
|
|
44
|
+
cpSync(ROOT, target, {
|
|
45
|
+
recursive: true,
|
|
46
|
+
filter: (src) => {
|
|
47
|
+
if (src === ROOT) return true
|
|
48
|
+
const rel = src.slice(ROOT.length + 1).split(/[\\/]/)[0]
|
|
49
|
+
return !skip.has(rel)
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
|
|
53
|
+
const pkg = JSON.parse(readFileSync(join(target, 'package.json'), 'utf8'))
|
|
54
|
+
const clientSize = statSync(join(target, 'home-plugin', 'cli-bridge-indicator', 'lib', 'client.js')).size
|
|
55
|
+
if (clientSize < 1000) { console.error('deploy: staged client.js looks truncated (' + clientSize + 'B)'); process.exit(1) }
|
|
56
|
+
|
|
57
|
+
console.log('deploy: staged ' + pkg.name + '@' + pkg.version + ' -> ' + target)
|
|
58
|
+
console.log('下一步(手工接线;执行前先停止 DSH 后端):')
|
|
59
|
+
console.log(' 1. profile package.json 加依赖 "file:' + target.replace(/\\/g, '/') + '",删除三个旧桥接包条目')
|
|
60
|
+
console.log(' 2. bundles 列表加入 ' + pkg.name)
|
|
61
|
+
console.log(' 3. 重启 DSH,验证 GET /cli-bridge/status 与标题栏三盏 pill')
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/verify.mjs — 发布前静态红线检查(keystone)。
|
|
3
|
+
//
|
|
4
|
+
// 背景(历史事故 agy v1.6.1 / codebuddy v1.1.8):client.js 的注册 id 一旦与包名不
|
|
5
|
+
// 一致,整个 client combo 加载失败 → DSH 启动致命屏 → 强制安全模式。npm publish
|
|
6
|
+
// 之后才发现 = 必须发新版本撤销,成本极高。本脚本在 prepack/check 里把这些不变量
|
|
7
|
+
// 变成机器闸门:
|
|
8
|
+
// 1. client.js 的 load({ id }) === package.json name(红线 4)
|
|
9
|
+
// 2. bundle patch 行的 name === package.json name
|
|
10
|
+
// 3. package.json exports 指向的文件全部存在
|
|
11
|
+
// 4. presets 的桥接说明符 === name + '/preset-<id>' 且在 exports 里
|
|
12
|
+
// 5. provider 描述符可加载、必备字段齐全(含 tools/settings)
|
|
13
|
+
// 6. dsh 字段齐全(client.platform=web、bundle.patch 路径存在)
|
|
14
|
+
// 任一失败 → exit 1 并逐条列出。
|
|
15
|
+
|
|
16
|
+
import { readFileSync, existsSync, readdirSync } from 'node:fs'
|
|
17
|
+
import { dirname, join, resolve } from 'node:path'
|
|
18
|
+
import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
19
|
+
|
|
20
|
+
const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..')
|
|
21
|
+
const errors = []
|
|
22
|
+
const ok = []
|
|
23
|
+
|
|
24
|
+
function read(rel) { try { return readFileSync(join(ROOT, rel), 'utf8') } catch (e) { return null } }
|
|
25
|
+
|
|
26
|
+
const pkg = JSON.parse(read('package.json') || '{}')
|
|
27
|
+
if (!pkg.name) { console.error('verify: package.json unreadable'); process.exit(1) }
|
|
28
|
+
ok.push('package ' + pkg.name + '@' + pkg.version)
|
|
29
|
+
|
|
30
|
+
// 1. client id 红线
|
|
31
|
+
const client = read('home-plugin/cli-bridge-indicator/lib/client.js')
|
|
32
|
+
if (client === null) errors.push('client.js missing')
|
|
33
|
+
else {
|
|
34
|
+
const m = client.match(/__ModuleLoader__\.load\(\{\s*id:\s*"([^"]+)"/)
|
|
35
|
+
if (!m) errors.push('client.js: no __ModuleLoader__.load({ id }) found')
|
|
36
|
+
else if (m[1] !== pkg.name) errors.push('REDLINE client id mismatch: client.js registers "' + m[1] + '" but package name is "' + pkg.name + '" (client combo will fail to load → bricked page)')
|
|
37
|
+
else ok.push('client id === package name (' + m[1] + ')')
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// 2. bundle patch 行 name
|
|
41
|
+
const patch = read('home-plugin/cli-bridge-indicator/cordis.patch.yml')
|
|
42
|
+
if (patch === null) errors.push('cordis.patch.yml missing')
|
|
43
|
+
else {
|
|
44
|
+
const names = [...patch.matchAll(/^\s*name:\s*'([^']+)'/gm)].map((x) => x[1])
|
|
45
|
+
if (!names.includes(pkg.name)) errors.push('cordis.patch.yml: no row with name ' + pkg.name + ' (rows: ' + names.join(', ') + ')')
|
|
46
|
+
else ok.push('bundle patch row name ok')
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 3. exports 文件存在
|
|
50
|
+
for (const [spec, rel] of Object.entries(pkg.exports || {})) {
|
|
51
|
+
const p = String(rel).replace(/^\.\//, '')
|
|
52
|
+
if (!existsSync(join(ROOT, p))) errors.push('exports[' + JSON.stringify(spec) + '] -> ' + rel + ' missing')
|
|
53
|
+
}
|
|
54
|
+
ok.push('exports targets exist (' + Object.keys(pkg.exports || {}).length + ')')
|
|
55
|
+
|
|
56
|
+
// 4. preset 桥接说明符
|
|
57
|
+
const { PROVIDER_IDS, createProvider } = await import(pathToFileURL(join(ROOT, 'providers', 'registry.mjs')).href)
|
|
58
|
+
for (const id of PROVIDER_IDS) {
|
|
59
|
+
const exportKey = './preset-' + id
|
|
60
|
+
if (!pkg.exports || !pkg.exports[exportKey]) errors.push('package.json exports[' + exportKey + '] missing (preset bridge row needs it)')
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 5. 描述符完整性
|
|
64
|
+
for (const id of PROVIDER_IDS) {
|
|
65
|
+
let d
|
|
66
|
+
try { d = createProvider(id, { env: {} }) } catch (e) { errors.push('provider ' + id + ': load failed ' + String(e && e.message || e)); continue }
|
|
67
|
+
const need = ['id', 'label', 'presetId', 'backends', 'normalizeBackend', 'resolveBackend', 'buildArgv', 'parseJson', 'buildResult', 'unavailableStatus', 'tools', 'settings', 'runToolLogic', 'attach']
|
|
68
|
+
for (const k of need) if (d[k] === undefined) errors.push('provider ' + id + ': descriptor missing "' + k + '"')
|
|
69
|
+
for (const t of ['run', 'continue', 'status']) {
|
|
70
|
+
const tool = d.tools && d.tools[t]
|
|
71
|
+
if (!tool || !tool.name || !tool.description || !tool.inputSchema) errors.push('provider ' + id + ': tools.' + t + ' invalid')
|
|
72
|
+
}
|
|
73
|
+
if (d.id !== id) errors.push('provider ' + id + ': descriptor.id = ' + JSON.stringify(d.id))
|
|
74
|
+
if (!d.tools || !d.tools.run || !d.tools.run.name.startsWith(id + '_')) errors.push('provider ' + id + ': tool names must keep <id>_ prefix (prompt continuity)')
|
|
75
|
+
ok.push('provider ' + id + ' descriptor ok (' + [d.tools.run.name, d.tools.continue.name, d.tools.status.name].join(',') + ')')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// 6. dsh 字段
|
|
79
|
+
if (!pkg.dsh || !pkg.dsh.client || pkg.dsh.client.platform !== 'web') errors.push('package.json dsh.client.platform must be "web"')
|
|
80
|
+
const patchRel = pkg.dsh && pkg.dsh.bundle && pkg.dsh.bundle.patch
|
|
81
|
+
if (!patchRel || !existsSync(join(ROOT, String(patchRel).replace(/^\.\//, '')))) errors.push('package.json dsh.bundle.patch target missing')
|
|
82
|
+
|
|
83
|
+
// 7. 生成物存在(dynamic host / agent yml 至少 plugin-list 在;fresh clone 需先 build)
|
|
84
|
+
for (const id of PROVIDER_IDS) {
|
|
85
|
+
if (!read('preset/' + ({ agy: 'agy-first', codebuddy: 'codebuddy-first', mimo: 'mimo-first' })[id] + '/plugin-list.mjs')) errors.push('preset/' + id + ' plugin-list.mjs missing')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// 8. 版本双锁:mcp server const + CHANGELOG 顶部条目 === package.json version
|
|
89
|
+
// (旧仓教训:codebuddy v1.1.8「npm 先发、git 欠账」;这里把两处拉齐成机器约束)
|
|
90
|
+
const mcpSrc = read('mcp/cli-bridge-mcp.mjs') || ''
|
|
91
|
+
const mv = mcpSrc.match(/const SERVER_VERSION\s*=\s*'([^']+)'/)
|
|
92
|
+
if (mv && mv[1] !== pkg.version) errors.push('version drift: mcp SERVER_VERSION ' + mv[1] + ' !== package.json ' + pkg.version)
|
|
93
|
+
if (!mv) errors.push('mcp/cli-bridge-mcp.mjs: SERVER_VERSION const not found')
|
|
94
|
+
const cl = read('CHANGELOG.md') || ''
|
|
95
|
+
const cv = cl.match(/^## \[(\d+\.\d+\.\d+[^\]]*)\]/m)
|
|
96
|
+
if (cv && cv[1] !== pkg.version) errors.push('version drift: CHANGELOG top [' + cv[1] + '] !== package.json ' + pkg.version)
|
|
97
|
+
if (!cv) errors.push('CHANGELOG.md: no keep-a-changelog style version heading found')
|
|
98
|
+
|
|
99
|
+
// 9. 凭据泄漏扫描(发布安全红线):tests/fixtures 与全部源码里绝不能出现真实凭据
|
|
100
|
+
// 形态的长随机串。旧仓 test/fixtures/credentials/.credentials.yaml 是真实设备授权
|
|
101
|
+
// 文件(36 字节 base64 形态)—— 移植测试时照抄 fixtures 目录就会把它带进 tarball。
|
|
102
|
+
// 规则:文件名含 credential/token/auth/secret(.yaml/.yml/.json/.info)→ 拒绝;
|
|
103
|
+
// 任何被打包文本文件里 key:value 的 value 是 ≥24 位纯随机 [A-Za-z0-9+/=] 且键名
|
|
104
|
+
// 疑似凭据(token|secret|password|credential|apikey|api_key|auth)→ 拒绝。
|
|
105
|
+
// 白名单:掩码示例、假 fixture(fake-*)、env 变量名引用。
|
|
106
|
+
const CRED_FILE_RE = /\.(ya?ml|json|info|txt|local|env)$/i
|
|
107
|
+
const CRED_NAME_RE = /(credential|token|secret|password|cookie|auth)/i
|
|
108
|
+
const CRED_LINE_RE = /(^|[^A-Za-z0-9_])([A-Za-z0-9_]*(token|secret|password|credential|api_?key|auth)[A-Za-z0-9_]*)\s*[:=]\s*['"]?([A-Za-z0-9+/=._-]{24,})['"]?/gi
|
|
109
|
+
const CRED_ALLOW_RE = /(fake|example|redacted|placeholder|\*\*\*|<.*>|\{\{|process\.env|CODEBUDDY_|AGY_|MIMO_|CLI_BRIDGE_)/i
|
|
110
|
+
function walkFiles(dir, out) {
|
|
111
|
+
for (const e of readdirSync(dir, { withFileTypes: true })) {
|
|
112
|
+
if (e.name === 'node_modules' || e.name === '.git') continue
|
|
113
|
+
const p = join(dir, e.name)
|
|
114
|
+
if (e.isDirectory()) {
|
|
115
|
+
walkFiles(p, out)
|
|
116
|
+
} else out.push(p)
|
|
117
|
+
}
|
|
118
|
+
return out
|
|
119
|
+
}
|
|
120
|
+
const allFiles = walkFiles(ROOT, [])
|
|
121
|
+
for (const f of allFiles) {
|
|
122
|
+
const relc = f.slice(ROOT.length + 1)
|
|
123
|
+
if (!/\.(mjs|js|cjs|json|ya?ml|md|yml|txt|svg|css|html|info|local)$/i.test(relc)) continue
|
|
124
|
+
const base = relc.split(/[\\/]/).pop()
|
|
125
|
+
if (CRED_NAME_RE.test(base) && !/^fake/i.test(base) && CRED_FILE_RE.test(base)) {
|
|
126
|
+
const txt = readFileSync(f, 'utf8')
|
|
127
|
+
if (txt.trim().length) { errors.push('credential-shaped fixture committed: ' + relc + ' (make it fake-* or env-driven)') }
|
|
128
|
+
continue
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
for (const f of allFiles) {
|
|
132
|
+
const relc = f.slice(ROOT.length + 1)
|
|
133
|
+
if (!/\.(mjs|js|cjs|json|ya?ml|md)$/i.test(relc)) continue
|
|
134
|
+
const txt = readFileSync(f, 'utf8')
|
|
135
|
+
for (const m of txt.matchAll(CRED_LINE_RE)) {
|
|
136
|
+
if (CRED_ALLOW_RE.test(m[0])) continue
|
|
137
|
+
const val = m[4]
|
|
138
|
+
if (!/[A-Za-z]/.test(val) || !/[0-9]/.test(val) || !/[+/=._-]/.test(val)) continue
|
|
139
|
+
errors.push('possible credential literal in ' + relc + ': ' + m[2].slice(0, 40) + '=…(random24+)')
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
ok.push('credential scan clean (' + allFiles.length + ' files)')
|
|
143
|
+
|
|
144
|
+
if (errors.length) {
|
|
145
|
+
console.error('verify: FAILED\n' + errors.map((e) => ' ✗ ' + e).join('\n'))
|
|
146
|
+
process.exit(1)
|
|
147
|
+
}
|
|
148
|
+
console.log('verify: OK\n' + ok.map((e) => ' ✓ ' + e).join('\n'))
|