@wenaixi/cfbridge 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,255 @@
1
+ // 仓库配置与安全检查(零副作用)。
2
+ //
3
+ // v0.3.0 调整:
4
+ // 1. 主入口从 agent.cordis.yml 切到 cordis.patch.yml。
5
+ // 2. 新增 bundle manifest 断言(dsh.bundle.patch、files[])。
6
+ // 3. 反转 web profile 污染断言:现在允许 disabled 覆写与全局 `mcp-cloudflare`
7
+ // 行(来自 cfbridge bundle),但禁止用户手写重复启用;旧时代的 mcp-cloudflare
8
+ // 残留仍应被清掉。
9
+ // 4. 增加 deprecated/ 旧 preset 文件归档检查。
10
+ //
11
+ // 使用:npm run check
12
+
13
+ const fs = require('fs')
14
+ const path = require('path')
15
+ const { execSync } = require('child_process')
16
+
17
+ const ROOT = path.resolve(__dirname, '..')
18
+
19
+ const checks = []
20
+ function check(name, ok, detail = '') { checks.push({ name, ok, detail }) }
21
+
22
+ function readJson(p) { try { return JSON.parse(fs.readFileSync(p, 'utf8')) } catch { return null } }
23
+ function readText(p) { try { return fs.readFileSync(p, 'utf8') } catch { return '' } }
24
+ function fileExists(p) { return fs.existsSync(p) }
25
+
26
+ function hasTokenLeak(text) {
27
+ const patterns = [
28
+ /cfat_[A-Za-z0-9]{16,}/,
29
+ /cfut_[A-Za-z0-9]{16,}/,
30
+ /cfoat_[A-Za-z0-9]{16,}/,
31
+ /sk-[A-Za-z0-9-]{16,}/,
32
+ /Bearer\s+[A-Za-z0-9_-]{40,}/,
33
+ /gh[pousr]_[A-Za-z0-9]{30,}/,
34
+ /AKIA[0-9A-Z]{16}/,
35
+ /-----BEGIN [A-Z ]*PRIVATE KEY-----/,
36
+ /AIza[0-9A-Za-z_-]{35}/,
37
+ ]
38
+ return patterns.some((re) => re.test(text))
39
+ }
40
+
41
+ // === 1. package.json 元数据 + dsh.bundle ===
42
+ const pkg = readJson(path.join(ROOT, 'package.json'))
43
+ if (pkg) {
44
+ check('package name is @wenaixi/cfbridge', pkg.name === '@wenaixi/cfbridge', `got: ${pkg.name}`)
45
+ check('package version is 0.1.0', pkg.version === '0.1.0', `got: ${pkg.version}`)
46
+ check('package is not private', pkg.private !== true, pkg.private ? 'package.json must not be private for npm publish' : '')
47
+ check('package publishConfig.access is public', pkg.publishConfig?.access === 'public', `got: ${pkg.publishConfig?.access}`)
48
+ check('package repository points to Wenaixi/dsh-cfbridge', /github\.com\/Wenaixi\/dsh-cfbridge(\.git)?$/i.test(pkg.repository?.url || ''), `got: ${pkg.repository?.url}`)
49
+ check('package author is Wenaixi', /Wenaixi/.test(pkg.author || ''), `got: ${pkg.author}`)
50
+ check('package license is MIT', pkg.license === 'MIT', `got: ${pkg.license}`)
51
+ check('package declares dsh.bundle.patch', !!pkg.dsh?.bundle?.patch)
52
+ if (pkg.dsh?.bundle?.patch) {
53
+ const abs = path.resolve(ROOT, pkg.dsh.bundle.patch)
54
+ check('dsh.bundle.patch target file exists', fileExists(abs), abs)
55
+ }
56
+ const files = Array.isArray(pkg.files) ? pkg.files : []
57
+ for (const entry of ['cordis.patch.yml', 'skills/', 'scripts/', 'README.md', 'LICENSE']) {
58
+ check(`package.files[] includes ${entry}`, files.includes(entry))
59
+ }
60
+ for (const s of ['install:bundle', 'uninstall:bundle', 'validate:bundle', 'migrate:from-preset', 'check', 'test']) {
61
+ check(`package has ${s} script`, !!pkg.scripts?.[s])
62
+ }
63
+ } else {
64
+ check('package.json readable', false)
65
+ }
66
+
67
+ // === 2. Wrangler 安装 ===
68
+ const wranglerPkg = readJson(path.join(ROOT, 'node_modules', 'wrangler', 'package.json'))
69
+ if (wranglerPkg) {
70
+ const major = parseInt(String(wranglerPkg.version).split('.')[0], 10)
71
+ check('Wrangler is pinned to major version 4', major === 4, `got: ${wranglerPkg.version}`)
72
+ } else {
73
+ check('local Wrangler is installed', false, 'run npm ci first')
74
+ }
75
+
76
+ // === 3. cordis.patch.yml(v0.3.0 主入口) ===
77
+ const patchText = readText(path.join(ROOT, 'cordis.patch.yml'))
78
+ check('cordis.patch.yml exists', fileExists(path.join(ROOT, 'cordis.patch.yml')))
79
+ if (patchText) {
80
+ check('cordis.patch.yml uses DSH MCP client', patchText.includes('@deepseek-ai/dsh-mcp-client'))
81
+ check('cordis.patch.yml targets Cloudflare MCP', patchText.includes('mcp.cloudflare.com/mcp'))
82
+ check('cordis.patch.yml reads token from environment', patchText.includes('process.env.CLOUDFLARE_API_TOKEN'))
83
+ check('cordis.patch.yml has no literal Cloudflare token', !hasTokenLeak(patchText))
84
+ check('cordis.patch.yml has mcp-cloudflare id', /^\s*-\s+id:\s*mcp-cloudflare\b/m.test(patchText))
85
+ check('cordis.patch.yml uses !!js Authorization template',
86
+ /Authorization:\s*!!js\s+'`Bearer \$\{process\.env\.CLOUDFLARE_API_TOKEN\}`'/.test(patchText))
87
+ check('cordis.patch.yml keeps failOnStartupError: false',
88
+ /failOnStartupError:\s*false\b/.test(patchText))
89
+ check('cordis.patch.yml has cfbridge-skill id', /^\s*-\s+id:\s*cfbridge-skill\b/m.test(patchText))
90
+ check('cordis.patch.yml cfbridge-skill references src/cfbridge-skill.js',
91
+ /cfbridge-skill\.js/.test(patchText))
92
+
93
+ // 不应再注入 agent 栈行(plan §4.1);重复注入会与 host 冲突。
94
+ const banned = ['persona', 'agent-instructions', 'tool-bash', 'tool-pwsh', 'tool-fs', 'skill-filesystem', 'tool-skill', 'planning', 'tool-goal', 'tool-web']
95
+ for (const k of banned) {
96
+ const re = new RegExp(`^\\s*-\\s+id:\\s*${k}\\b`, 'm')
97
+ if (re.test(patchText)) check(`cordis.patch.yml does NOT redeclare ${k}`, false, 'host composition already provides this row')
98
+ }
99
+ check('cordis.patch.yml does not redeclare agent-stack rows', true, 'banned keys absent or already failed individually')
100
+ }
101
+ // 运行时 Skill 插件
102
+ const runtimeSkillJs = readText(path.join(ROOT, 'src', 'cfbridge-skill.js'))
103
+ check('src/cfbridge-skill.js exists', fileExists(path.join(ROOT, 'src', 'cfbridge-skill.js')))
104
+ if (runtimeSkillJs) {
105
+ check('src/cfbridge-skill.js injects skills', /inject\s*=\s*\['skills'\]/.test(runtimeSkillJs))
106
+ check('src/cfbridge-skill.js registers via ctx.skills.register', /ctx\.skills\.register/.test(runtimeSkillJs))
107
+ }
108
+
109
+ // === 4. Skill 文件 ===
110
+ const skillMd = readText(path.join(ROOT, 'skills', 'cfbridge', 'SKILL.md'))
111
+ check('SKILL.md exists', fileExists(path.join(ROOT, 'skills', 'cfbridge', 'SKILL.md')))
112
+ if (skillMd) {
113
+ check('SKILL.md has title', /^#\s+cfbridge\b/m.test(skillMd))
114
+ check('SKILL.md has no token-like content', !hasTokenLeak(skillMd))
115
+ check('SKILL.md mentions global bundle trigger', /全局|安装 cfbridge bundle|install:bundle/.test(skillMd))
116
+ }
117
+
118
+ // === 5. 关键脚本 ===
119
+ const SCRIPTS = [
120
+ 'install-bundle.js',
121
+ 'uninstall-bundle.js',
122
+ 'validate-bundle.js',
123
+ 'migrate-from-preset.js',
124
+ 'dump-config.js',
125
+ 'check.js',
126
+ 'test.js',
127
+ 'wrangler.js',
128
+ 'test-wrangler.js',
129
+ // 旧 preset 脚本保留为 shim
130
+ 'install-preset.js',
131
+ 'uninstall-preset.js',
132
+ 'validate-preset.js',
133
+ ]
134
+ for (const s of SCRIPTS) {
135
+ check(`scripts/${s} exists`, fileExists(path.join(ROOT, 'scripts', s)))
136
+ }
137
+
138
+ // 旧 preset shim 必须打印 deprecation
139
+ for (const s of ['install-preset.js', 'uninstall-preset.js', 'validate-preset.js']) {
140
+ const t = readText(path.join(ROOT, 'scripts', s))
141
+ check(`scripts/${s} is marked deprecated`, /DEPRECATED|deprecated/i.test(t))
142
+ }
143
+
144
+ // === 6. gitignore ===
145
+ const gitignore = readText(path.join(ROOT, '.gitignore'))
146
+ check('.env is ignored', /^\.env$/m.test(gitignore) || /^\.env\b/m.test(gitignore))
147
+ check('.env.example IS tracked', /^\!\.env\.example$/m.test(gitignore))
148
+ check('PEM files are ignored', /\.pem$/m.test(gitignore))
149
+ check('secrets/credentials directories are ignored', /secrets\/|credentials\//.test(gitignore))
150
+ check('node_modules is ignored', /^node_modules\/?$/m.test(gitignore) || /^node_modules\b/m.test(gitignore))
151
+ check('package-lock.json IS tracked', !/^package-lock\.json$/m.test(gitignore))
152
+ check('.wrangler directory is ignored', /\.wrangler/.test(gitignore))
153
+ check('.dsh/ is ignored', /\.dsh\//.test(gitignore))
154
+ check('deprecated dynamic plugin residue ignored',
155
+ /cfbridge-host-/.test(gitignore))
156
+ check('IDE/temp files ignored', /\.vscode|\.idea|\.swp|\.DS_Store/.test(gitignore))
157
+
158
+ // === 7. 所有跟踪文件 token 扫描 ===
159
+ function listTracked() {
160
+ try {
161
+ return execSync('git ls-files', { cwd: ROOT, encoding: 'utf8' }).trim().split('\n').filter(Boolean)
162
+ } catch { return [] }
163
+ }
164
+ const tracked = listTracked()
165
+ let tokenLeak = null
166
+ for (const f of tracked) {
167
+ const p = path.join(ROOT, f)
168
+ if (!fs.existsSync(p)) continue
169
+ if (hasTokenLeak(readText(p))) { tokenLeak = f; break }
170
+ }
171
+ check('no tracked file contains token-like content', !tokenLeak, tokenLeak ? `found in ${tokenLeak}` : '')
172
+
173
+ // === 8. web profile 状态(v0.3.0 翻转断言) ===
174
+ const dshHome = process.env.DSH_HOME || path.join(process.env.USERPROFILE || process.env.HOME || '', '.dsh')
175
+ const webProfile = path.join(dshHome, 'profiles', 'web')
176
+ const webPatch = path.join(webProfile, 'cordis.patch.yml')
177
+ const webManifest = readJson(path.join(webProfile, 'package.json'))
178
+ const bundleInstalledInProfile = !!(
179
+ webManifest?.dependencies?.['@wenaixi/cfbridge']
180
+ && (webManifest?.dsh?.profile?.bundles || []).includes('@wenaixi/cfbridge')
181
+ )
182
+
183
+ if (fs.existsSync(webPatch)) {
184
+ const text = readText(webPatch)
185
+ if (bundleInstalledInProfile) {
186
+ // bundle 已装 → web patch 中不应再出现 mcp-cloudflare(防止工具重复注册)
187
+ const dupes = text.split('\n').filter((l) => /^\s*-\s+id:\s*mcp-cloudflare\b/.test(l))
188
+ check('web patch does not redeclare mcp-cloudflare when bundle is installed', dupes.length === 0,
189
+ dupes.length ? `${dupes.length} duplicate mcp-cloudflare row(s) in web patch` : '')
190
+ // 允许 disabled 覆写行
191
+ const disabledOverride = /^\s*-\s+id:\s*mcp-cloudflare\b[\s\S]*?disabled:\s*true\b/m.test(text)
192
+ check('web patch may optionally carry disabled: true mcp-cloudflare override (informational)', true,
193
+ disabledOverride ? 'disabled override present' : 'no override (tools active)')
194
+ } else {
195
+ // bundle 未装 → web patch 中不应有 cloudflare 残留(v0.2.0 时代的污染)
196
+ const cloudflareResidue = /cloudflare|mcp-cloudflare|CLOUDFLARE_API_TOKEN|cfbridge/i.test(text)
197
+ check('web patch has no cloudflare residue (bundle not installed)', !cloudflareResidue,
198
+ cloudflareResidue ? 'found cloudflare/cfbridge related lines' : '')
199
+ }
200
+ } else {
201
+ check('web profile cordis.patch.yml does not exist (or web profile not initialized)', bundleInstalledInProfile ? false : true,
202
+ bundleInstalledInProfile ? 'expected patch file when bundle is installed' : 'no web patch to inspect')
203
+ }
204
+
205
+ // === 9. 系统预设目录未被污染 ===
206
+ const SYSTEM_DSH = 'C:\\Users\\Administrator\\AppData\\Roaming\\npm\\node_modules\\@deepseek-ai\\dsh'
207
+ const systemSkills = path.join(SYSTEM_DSH, 'config', 'agent-presets')
208
+ let systemPolluted = null
209
+ if (fs.existsSync(systemSkills)) {
210
+ for (const sub of ['standard', 'code', 'cordis', 'minimal']) {
211
+ const skillsDir = path.join(systemSkills, sub, 'skills')
212
+ if (!fs.existsSync(skillsDir)) continue
213
+ const stack = [skillsDir]
214
+ while (stack.length) {
215
+ const dir = stack.pop()
216
+ for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
217
+ if (entry.isDirectory()) stack.push(path.join(dir, entry.name))
218
+ else if (entry.name.toLowerCase().includes('cfbridge')) { systemPolluted = `${sub}: ${path.join(dir, entry.name)}`; break }
219
+ }
220
+ if (systemPolluted) break
221
+ }
222
+ if (systemPolluted) break
223
+ }
224
+ }
225
+ check('DSH system presets have no cfbridge', !systemPolluted, systemPolluted || '')
226
+
227
+ // === 10. legacy v0.2.0 preset 状态 ===
228
+ const legacyPreset = path.join(dshHome, '.agent-presets', 'cfbridge')
229
+ if (fs.existsSync(legacyPreset)) {
230
+ check('legacy v0.2.0 preset directory noted', true, `still present at ${legacyPreset}; run \`npm run migrate:from-preset -- --yes\` to clean`)
231
+ } else {
232
+ check('legacy v0.2.0 preset absent', true, 'no agent-presets/cfbridge/ residue')
233
+ }
234
+
235
+ // === 11. Git remote ===
236
+ let hasRemote = false
237
+ let remoteText = ''
238
+ try { remoteText = String(execSync('git remote -v', { cwd: ROOT, encoding: 'utf8' })).trim(); hasRemote = remoteText.length > 0 } catch {}
239
+ const remoteOk = !hasRemote || /github\.com[:/]Wenaixi\/dsh-cfbridge(\.git)?/i.test(remoteText)
240
+ check('git remote points to Wenaixi/dsh-cfbridge (or not yet configured)', remoteOk,
241
+ hasRemote ? remoteText.split('\n')[0] : 'no remote yet (ok before publish)')
242
+
243
+ // === 12. deprecated/ 归档 ===
244
+ check('deprecated/preset/preset.yml archived', fileExists(path.join(ROOT, 'deprecated', 'preset', 'preset.yml')))
245
+ check('deprecated/preset/agent.cordis.yml archived', fileExists(path.join(ROOT, 'deprecated', 'preset', 'agent.cordis.yml')))
246
+ check('deprecated/preset/README.md exists', fileExists(path.join(ROOT, 'deprecated', 'preset', 'README.md')))
247
+
248
+ // === 输出 ===
249
+ let pass = 0
250
+ for (const c of checks) {
251
+ if (c.ok) { console.log(`PASS ${c.name}${c.detail ? ` — ${c.detail}` : ''}`); pass++ }
252
+ else console.log(`FAIL ${c.name}${c.detail ? ` — ${c.detail}` : ''}`)
253
+ }
254
+ console.log(`\n${pass === checks.length ? 'PASS' : 'FAIL'}: ${pass}/${checks.length} checks`)
255
+ process.exit(pass === checks.length ? 0 : 1)
@@ -0,0 +1,71 @@
1
+ // 封装 `dsh --profile <name> --dump-config` 并过滤出 cfbridge 相关层。
2
+ //
3
+ // 用于本机快速验证 cfbridge bundle 是否被 DSH 加载,以及是否包含 mcp-cloudflare 行。
4
+ //
5
+ // 使用:
6
+ // npm run dump:config
7
+ // npm run dump:config -- --profile tui
8
+ // npm run dump:config -- --raw # 不过滤,原样输出
9
+
10
+ const { spawnSync } = require('child_process')
11
+
12
+ function parseArgs(argv) {
13
+ const opts = { profile: 'web', raw: false }
14
+ for (let i = 0; i < argv.length; i++) {
15
+ const a = argv[i]
16
+ if (a === '--profile' || a === '-p') opts.profile = argv[++i]
17
+ else if (a.startsWith('--profile=')) opts.profile = a.slice('--profile='.length)
18
+ else if (a === '--raw') opts.raw = true
19
+ else if (a === '--help' || a === '-h') { printHelp(); process.exit(0) }
20
+ }
21
+ return opts
22
+ }
23
+
24
+ function printHelp() {
25
+ console.log(`Usage: dump-config.js [--profile <name>] [--raw]
26
+
27
+ Options:
28
+ --profile <name> Profile to dump (default: web).
29
+ --raw Print the full --dump-config output without filtering.
30
+ --help, -h Show this message.`)
31
+ }
32
+
33
+ function main() {
34
+ const opts = parseArgs(process.argv.slice(2))
35
+ const r = spawnSync('dsh', ['--profile', opts.profile, '--dump-config'], {
36
+ stdio: 'pipe',
37
+ shell: process.platform === 'win32',
38
+ })
39
+ const out = String(r.stdout || '')
40
+ const err = String(r.stderr || '')
41
+ if (r.status !== 0) {
42
+ if (err) process.stderr.write(err + '\n')
43
+ process.exit(r.status ?? 1)
44
+ }
45
+
46
+ if (opts.raw) {
47
+ process.stdout.write(out)
48
+ return
49
+ }
50
+
51
+ const lines = out.split('\n')
52
+ const cfbridgeStart = lines.findIndex((l) => /==\s*@wenaixi\/cfbridge\b/.test(l))
53
+ if (cfbridgeStart < 0) {
54
+ console.error('cfbridge layer not found. Run `npm run install:bundle` first.')
55
+ process.exit(1)
56
+ }
57
+ // 找到下一个 "# ==" 标记或 EOF
58
+ let end = lines.length
59
+ for (let i = cfbridgeStart + 1; i < lines.length; i++) {
60
+ if (/^# ==\s/.test(lines[i])) { end = i; break }
61
+ }
62
+ const slice = lines.slice(cfbridgeStart, end).join('\n')
63
+ process.stdout.write(slice + '\n')
64
+ }
65
+
66
+ try {
67
+ main()
68
+ } catch (e) {
69
+ console.error(e.stack || e.message)
70
+ process.exit(1)
71
+ }
@@ -0,0 +1,160 @@
1
+ // 一键安装 cfbridge v0.3.0 Bundle 到指定 DSH profile。
2
+ //
3
+ // 流程:
4
+ // 1. 解析参数(默认 profile=web,可通过 --profile <name> 覆盖)。
5
+ // 2. 校验本机有 dsh 与 pnpm。
6
+ // 3. 调用 `dsh plugin --profile <name> add <绝对路径>`:DSH plugin 转发器
7
+ // 自动 reconcile dsh.profile.bundles,把本 bundle append 到末尾。
8
+ // 4. 跑 `dsh --profile <name> --dump-config` 验证层出现
9
+ // `# == @wenaixi/cfbridge` 且含 mcp-cloudflare / cfbridge-skill 两行。
10
+ // 5. 若发现旧软链 $DSH_HOME/skills/cfbridge 指向本 bundle 的 skills 目录,
11
+ // 清理之(v0.3.0 之前版本残留;当前版本已改用运行时 Skill 注册)。
12
+ //
13
+ // 使用:
14
+ // npm run install:bundle # 默认 web profile
15
+ // npm run install:bundle -- --profile tui
16
+
17
+ const fs = require('fs')
18
+ const path = require('path')
19
+ const { spawnSync } = require('child_process')
20
+
21
+ const ROOT = path.resolve(__dirname, '..')
22
+ const PKG = JSON.parse(fs.readFileSync(path.join(ROOT, 'package.json'), 'utf8'))
23
+ const DSH_HOME = process.env.DSH_HOME || path.join(process.env.USERPROFILE || process.env.HOME || '', '.dsh')
24
+
25
+ function log(level, msg) {
26
+ const prefix = { info: 'INFO', ok: 'OK ', warn: 'WARN', err: 'ERR ' }[level] || 'INFO'
27
+ console.log(`[${prefix}] ${msg}`)
28
+ }
29
+
30
+ function parseArgs(argv) {
31
+ const opts = { profile: 'web' }
32
+ for (let i = 0; i < argv.length; i++) {
33
+ const a = argv[i]
34
+ if (a === '--profile' || a === '-p') {
35
+ opts.profile = argv[++i]
36
+ } else if (a.startsWith('--profile=')) {
37
+ opts.profile = a.slice('--profile='.length)
38
+ } else if (a === '--help' || a === '-h') {
39
+ printHelp()
40
+ process.exit(0)
41
+ } else {
42
+ log('warn', `Unknown argument ignored: ${a}`)
43
+ }
44
+ }
45
+ return opts
46
+ }
47
+
48
+ function printHelp() {
49
+ console.log(`Usage: install-bundle.js [--profile <name>]
50
+
51
+ Options:
52
+ --profile <name> Target DSH profile (default: web).
53
+ --help, -h Show this message.`)
54
+ }
55
+
56
+ function run(cmd, args, opts = {}) {
57
+ return spawnSync(cmd, args, { stdio: 'inherit', shell: process.platform === 'win32', ...opts })
58
+ }
59
+
60
+ function runCapture(cmd, args, opts = {}) {
61
+ return spawnSync(cmd, args, { stdio: 'pipe', shell: process.platform === 'win32', ...opts })
62
+ }
63
+
64
+ function checkPrereqs() {
65
+ const dsh = runCapture('dsh', ['--version'])
66
+ if (dsh.status !== 0) {
67
+ log('err', 'dsh CLI not found on PATH. Install DeepSeek Harness first.')
68
+ process.exit(1)
69
+ }
70
+ const version = String(dsh.stdout || '').trim().split('\n').pop()
71
+ log('ok', `dsh detected: ${version || 'unknown version'}`)
72
+
73
+ const pnpm = runCapture('pnpm', ['--version'])
74
+ if (pnpm.status !== 0) {
75
+ log('err', 'pnpm not found on PATH. dsh plugin forwards to pnpm; install pnpm first.')
76
+ process.exit(1)
77
+ }
78
+ log('ok', `pnpm detected: ${String(pnpm.stdout || '').trim()}`)
79
+ }
80
+
81
+ function profileDir(name) {
82
+ return path.join(DSH_HOME, 'profiles', name)
83
+ }
84
+
85
+ function cleanupLegacySymlink() {
86
+ const link = path.join(DSH_HOME, 'skills', 'cfbridge')
87
+ try {
88
+ const stat = fs.lstatSync(link)
89
+ if (!stat.isSymbolicLink() && !stat.isDirectory()) return
90
+ const raw = stat.isSymbolicLink() ? fs.readlinkSync(link) : ''
91
+ const pointsToBundle =
92
+ stat.isDirectory() ||
93
+ (raw && String(raw).includes('cfbridge') && String(raw).includes('skills'))
94
+ if (!pointsToBundle) return
95
+ fs.rmSync(link, { recursive: true, force: true })
96
+ log('ok', `Cleaned legacy skill link: ${link}`)
97
+ } catch (e) {
98
+ if (e.code !== 'ENOENT') log('warn', `Could not inspect legacy skill link at ${link}: ${e.message}`)
99
+ }
100
+ }
101
+
102
+ function dumpConfig(profile) {
103
+ log('info', `Verifying composed config for profile "${profile}"…`)
104
+ const r = runCapture('dsh', ['--profile', profile, '--dump-config'])
105
+ const out = String(r.stdout || '')
106
+ const err = String(r.stderr || '')
107
+ if (r.status !== 0) {
108
+ log('err', `dsh --dump-config failed (exit ${r.status}).`)
109
+ if (err) process.stderr.write(err + '\n')
110
+ process.exit(r.status ?? 1)
111
+ }
112
+ if (!/==\s*@wenaixi\/cfbridge\b/.test(out)) {
113
+ log('err', 'cfbridge layer not found in --dump-config output.')
114
+ log('info', '--- dump-config ---')
115
+ process.stdout.write(out)
116
+ log('info', '--- end ---')
117
+ process.exit(1)
118
+ }
119
+ log('ok', 'cfbridge layer present in composed profile tree.')
120
+ const mcpLine = out.split('\n').find((l) => /id:\s*mcp-cloudflare\b/.test(l))
121
+ if (mcpLine) log('ok', `Found bundle row: ${mcpLine.trim()}`)
122
+ const skillLine = out.split('\n').find((l) => /id:\s*cfbridge-skill\b/.test(l))
123
+ if (skillLine) log('ok', `Found bundle row: ${skillLine.trim()}`)
124
+ else log('warn', 'cfbridge-skill row not found in dump-config; bundle may be outdated. Run `npm install` or verify cordis.patch.yml.')
125
+ }
126
+
127
+ function main() {
128
+ const opts = parseArgs(process.argv.slice(2))
129
+ log('info', `Source bundle: ${ROOT} (${PKG.name}@${PKG.version})`)
130
+ log('info', `DSH home: ${DSH_HOME}`)
131
+ log('info', `Target profile: ${opts.profile}`)
132
+
133
+ checkPrereqs()
134
+
135
+ const profDir = profileDir(opts.profile)
136
+ fs.mkdirSync(profDir, { recursive: true })
137
+
138
+ log('info', `Running: dsh plugin --profile ${opts.profile} add ${ROOT}`)
139
+ const r = run('dsh', ['plugin', '--profile', opts.profile, 'add', ROOT])
140
+ if (r.status !== 0) {
141
+ log('err', `dsh plugin add failed (exit ${r.status}).`)
142
+ process.exit(r.status ?? 1)
143
+ }
144
+
145
+ cleanupLegacySymlink()
146
+ dumpConfig(opts.profile)
147
+
148
+ log('ok', 'Install complete.')
149
+ log('info', 'Next:')
150
+ log('info', ` 1. Restart DSH if it was running: dsh --profile ${opts.profile}`)
151
+ log('info', ' 2. Open any new session; the model should see mcp__cloudflare__* and the cfbridge skill.')
152
+ log('info', ` 3. To uninstall later: npm run uninstall:bundle -- --profile ${opts.profile}`)
153
+ }
154
+
155
+ try {
156
+ main()
157
+ } catch (e) {
158
+ log('err', e.stack || e.message)
159
+ process.exit(1)
160
+ }
@@ -0,0 +1,19 @@
1
+ // cfbridge v0.3.0 起 bundle 形态取代 agent preset。
2
+ // 本脚本保留为 deprecated shim,仅打印警告并退出非零以引导用户走新路径。
3
+ //
4
+ // 旧形态:
5
+ // 复制 preset.yml / agent.cordis.yml / skills/ 到 ~/.dsh/.agent-presets/cfbridge/
6
+ // 之后在 DSH 新会话选择器中选「Cloudflare 模式」才能挂载。
7
+ //
8
+ // 新形态(v0.3.0,推荐):
9
+ // npm run install:bundle
10
+ // 装完所有会话全局可见;不需要选模式。
11
+ //
12
+ // 仍想暂时使用旧 preset 形态?请直接从 git history 拉取 v0.2.0 标签并按其 README 操作。
13
+
14
+ const log = (msg) => console.log(`[WARN] ${msg}`)
15
+ log('install:preset is deprecated since v0.3.0 (cfbridge is now a DSH bundle).')
16
+ log('Use `npm run install:bundle` to install globally for the web profile.')
17
+ log('For other profiles: `npm run install:bundle -- --profile <name>`.')
18
+ log('This shim does not modify your DSH profile.')
19
+ process.exit(1)
@@ -0,0 +1,110 @@
1
+ // 检测并清理 v0.2.0 时代的 Agent Preset 残留(~/.dsh/.agent-presets/cfbridge/)。
2
+ //
3
+ // 使用:
4
+ // npm run migrate:from-preset # 仅检测,打印提示
5
+ // npm run migrate:from-preset -- --yes # 直接删除
6
+ //
7
+ // 删除是单向不可逆;脚本默认要求显式 --yes 才执行。
8
+
9
+ const fs = require('fs')
10
+ const path = require('path')
11
+
12
+ const DSH_HOME = process.env.DSH_HOME || path.join(process.env.USERPROFILE || process.env.HOME || '', '.dsh')
13
+ const LEGACY = path.join(DSH_HOME, '.agent-presets', 'cfbridge')
14
+
15
+ function log(level, msg) {
16
+ const prefix = { info: 'INFO', ok: 'OK ', warn: 'WARN', err: 'ERR ' }[level] || 'INFO'
17
+ console.log(`[${prefix}] ${msg}`)
18
+ }
19
+
20
+ function parseArgs(argv) {
21
+ const opts = { yes: false, profile: 'web' }
22
+ for (let i = 0; i < argv.length; i++) {
23
+ const a = argv[i]
24
+ if (a === '--yes' || a === '-y') opts.yes = true
25
+ else if (a === '--profile' || a === '-p') opts.profile = argv[++i]
26
+ else if (a === '--help' || a === '-h') { printHelp(); process.exit(0) }
27
+ else log('warn', `Unknown argument ignored: ${a}`)
28
+ }
29
+ return opts
30
+ }
31
+
32
+ function printHelp() {
33
+ console.log(`Usage: migrate-from-preset.js [--yes] [--profile <name>]
34
+
35
+ Options:
36
+ --yes, -y Delete the legacy preset directory without prompting.
37
+ --profile <name> Profile to verify post-cleanup (default: web).
38
+ --help, -h Show this message.`)
39
+ }
40
+
41
+ function listFiles(dir) {
42
+ const out = []
43
+ if (!fs.existsSync(dir)) return out
44
+ const stack = [dir]
45
+ while (stack.length) {
46
+ const cur = stack.pop()
47
+ for (const entry of fs.readdirSync(cur, { withFileTypes: true })) {
48
+ const p = path.join(cur, entry.name)
49
+ if (entry.isDirectory()) stack.push(p)
50
+ else if (entry.isFile()) out.push(p)
51
+ }
52
+ }
53
+ return out
54
+ }
55
+
56
+ function main() {
57
+ const opts = parseArgs(process.argv.slice(2))
58
+ log('info', `DSH home: ${DSH_HOME}`)
59
+ log('info', `Legacy v0.2.0 preset directory: ${LEGACY}`)
60
+
61
+ if (!fs.existsSync(LEGACY)) {
62
+ log('ok', 'No legacy preset directory found. Nothing to migrate.')
63
+ return
64
+ }
65
+
66
+ const files = listFiles(LEGACY)
67
+ log('warn', `Found ${files.length} file(s) inside legacy preset directory.`)
68
+ for (const f of files.slice(0, 20)) console.log(` ${f}`)
69
+ if (files.length > 20) console.log(` …and ${files.length - 20} more`)
70
+
71
+ // 与 web patch 层共存可能造成 mcp 工具重复注册;建议清理。
72
+ log('info', 'Why clean it? In v0.3.0 the Cloudflare tools are registered globally via the bundle layer;')
73
+ log('info', 'leaving the v0.2.0 preset can cause duplicate `mcp__cloudflare__*` tool names. The new')
74
+ log('info', 'behavioral model (plan §5.2) recommends NOT coexisting. Cleaning also avoids the old')
75
+ log('info', 'agent-plane rows from showing up in the preset picker.')
76
+
77
+ if (!opts.yes) {
78
+ if (process.env.CI === 'true' || process.env.NONINTERACTIVE === 'true') {
79
+ log('info', 'Non-interactive environment detected; will not delete without --yes.')
80
+ return
81
+ }
82
+ const rl = require('readline').createInterface({ input: process.stdin, output: process.stdout })
83
+ rl.question(`Delete ${LEGACY}? [y/N] `, (answer) => {
84
+ rl.close()
85
+ if (/^y(es)?$/i.test(String(answer).trim())) doDelete()
86
+ else log('info', 'Aborted by user. Re-run with --yes to delete without prompt.')
87
+ })
88
+ return
89
+ }
90
+
91
+ doDelete()
92
+ }
93
+
94
+ function doDelete() {
95
+ try {
96
+ fs.rmSync(LEGACY, { recursive: true, force: true })
97
+ log('ok', `Removed legacy preset: ${LEGACY}`)
98
+ log('info', 'Next: restart DSH and pick a non-Cloudflare preset (or stay in default mode).')
99
+ } catch (e) {
100
+ log('err', `Failed to remove ${LEGACY}: ${e.message}`)
101
+ process.exit(1)
102
+ }
103
+ }
104
+
105
+ try {
106
+ main()
107
+ } catch (e) {
108
+ log('err', e.stack || e.message)
109
+ process.exit(1)
110
+ }
@@ -0,0 +1,58 @@
1
+ // Wrangler CLI 只读验证套件。
2
+ // 仅调用只读命令,不修改任何 Cloudflare 资源。
3
+ // 每个测试独立执行,失败不阻断其它测试。
4
+
5
+ const { spawnSync } = require('child_process')
6
+ const path = require('path')
7
+
8
+ const ROOT = path.resolve(__dirname, '..')
9
+ const WRANGLER = path.join(__dirname, 'wrangler.js')
10
+
11
+ const TESTS = [
12
+ { name: 'wrangler --version', args: ['--version'], expectInStdout: /\d+\.\d+\.\d+/ },
13
+ { name: 'wrangler whoami', args: ['whoami'], expectInStdout: /Account/ },
14
+ { name: 'wrangler d1 list --json', args: ['d1', 'list', '--json'], expectValidJson: true },
15
+ { name: 'wrangler pages project list --json', args: ['pages', 'project', 'list', '--json'], expectValidJson: true },
16
+ ]
17
+
18
+ function runTest(test) {
19
+ const result = spawnSync(process.execPath, [WRANGLER, ...test.args], {
20
+ cwd: ROOT,
21
+ encoding: 'utf8',
22
+ env: process.env,
23
+ })
24
+ const stdout = result.stdout || ''
25
+ const stderr = result.stderr || ''
26
+ const ok = result.status === 0
27
+
28
+ let detail = ok ? 'PASS' : `FAIL (exit ${result.status})`
29
+ if (test.expectInStdout && !test.expectInStdout.test(stdout)) {
30
+ return { ok: false, name: test.name, detail: 'stdout did not match expected pattern' }
31
+ }
32
+ if (test.expectValidJson) {
33
+ try {
34
+ JSON.parse(stdout)
35
+ } catch (e) {
36
+ return { ok: false, name: test.name, detail: `invalid JSON: ${e.message}` }
37
+ }
38
+ }
39
+
40
+ return { ok, name: test.name, detail }
41
+ }
42
+
43
+ function main() {
44
+ console.log('Wrangler CLI 只读验证')
45
+ console.log('─'.repeat(50))
46
+
47
+ let passed = 0
48
+ for (const test of TESTS) {
49
+ const r = runTest(test)
50
+ console.log(`${r.ok ? 'PASS' : 'FAIL'} ${r.name} ${r.ok ? '' : `— ${r.detail}`}`)
51
+ if (r.ok) passed++
52
+ }
53
+ console.log('─'.repeat(50))
54
+ console.log(`${passed}/${TESTS.length} passed`)
55
+ process.exit(passed === TESTS.length ? 0 : 1)
56
+ }
57
+
58
+ main()