@aiwayds/dsh-llm-proxy 0.6.0 → 0.7.1

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/README.md CHANGED
@@ -21,7 +21,7 @@ LLM 列表命中 > SYSTEM proxy(env 语义)> 直连
21
21
 
22
22
  ```bash
23
23
  # 方式一:作为 npm 插件加入 profile
24
- dsh plugin add @aiwayds/dsh-llm-proxy # 或在 settings.yaml 的 plugins 里声明
24
+ dsh plugin add @aiwayds/dsh-llm-proxy
25
25
 
26
26
  # 方式二:手动 patch 挂载(本包自带 cordis.patch.yml)
27
27
  # 在 profile 的 bundles 中插入:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aiwayds/dsh-llm-proxy",
3
- "version": "0.6.0",
3
+ "version": "0.7.1",
4
4
  "description": "dsh plugin: SYSTEM proxy (env) + LLM traffic-splitting proxy via a process-global undici routing dispatcher",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -26,6 +26,7 @@
26
26
  },
27
27
  "files": [
28
28
  "lib",
29
+ "scripts",
29
30
  "cordis.patch.yml",
30
31
  "src",
31
32
  "skills",
@@ -37,7 +38,7 @@
37
38
  "check": "tsc --noEmit -p tsconfig.json",
38
39
  "pretest": "npm run build",
39
40
  "prepare": "npm run build",
40
- "test": "node --test test/match.test.mjs test/router.test.mjs test/config.test.mjs test/skill.test.mjs test/dispose.test.mjs test/e2e.test.mjs",
41
+ "test": "node --test test/match.test.mjs test/router.test.mjs test/config.test.mjs test/skill.test.mjs test/dispose.test.mjs test/e2e.test.mjs && node test/packaged-install.test.mjs",
41
42
  "smoke": "node scripts/smoke-boot.mjs",
42
43
  "postinstall": "node scripts/link-dsh-closure.mjs"
43
44
  },
@@ -47,8 +48,8 @@
47
48
  "peerDependencies": {
48
49
  "@deepseek-ai/cordis": "4.0.4",
49
50
  "@deepseek-ai/schemastery": "3.18.4",
50
- "@deepseek-ai/dsh-settings": ">=0.1.7-rc.1",
51
- "@deepseek-ai/dsh-skill": ">=0.1.7-rc.1"
51
+ "@deepseek-ai/dsh-settings": ">=0.2.0-rc.2",
52
+ "@deepseek-ai/dsh-skill": ">=0.2.0-rc.2"
52
53
  },
53
54
  "peerDependenciesMeta": {
54
55
  "@deepseek-ai/cordis": {
@@ -64,8 +65,8 @@
64
65
  "devDependencies": {
65
66
  "@deepseek-ai/cordis": "4.0.4",
66
67
  "@deepseek-ai/schemastery": "3.18.4",
67
- "@deepseek-ai/dsh-settings": "0.1.7-rc.1",
68
- "@deepseek-ai/dsh-skill": "0.1.7-rc.1",
68
+ "@deepseek-ai/dsh-settings": "0.2.0-rc.2",
69
+ "@deepseek-ai/dsh-skill": "0.2.0-rc.2",
69
70
  "@types/node": "^24.0.0",
70
71
  "typescript": "^5.9.0"
71
72
  },
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Postinstall linker: point every `node_modules/@deepseek-ai/*` entry at the
3
+ * global dsh closure.
4
+ *
5
+ * Why this exists: dsh-tui-pi is a plugin that runs *inside* the installed
6
+ * dsh CLI, and its source imports the `@deepseek-ai/*` packages (cordis,
7
+ * dsh-session, dsh-settings, schemastery, …). Those packages are **not**
8
+ * resolvable from the public npm registry in a usable way (their rc.6
9
+ * versions live only in the dsh install's own node_modules), so the repo
10
+ * resolves them from the *global dsh closure* —
11
+ * `$(realpath $(which dsh))/node_modules/@deepseek-ai` — via plain symlinks.
12
+ *
13
+ * The contract: ALL `@deepseek-ai/*` resolve to that single closure instance,
14
+ * so there is exactly one `@deepseek-ai/cordis` in the type graph. Declaring
15
+ * any of them in package.json made `pnpm install` create a second local copy
16
+ * in `.pnpm`, which broke the cordis `declare module` augmentation
17
+ * (`Property 'settings' does not exist on type 'Context'`), so they must stay
18
+ * *undeclared*. pnpm then treats the closure links as extraneous — and, as
19
+ * it prunes entries it once managed, the links can still disappear after an
20
+ * install. This script re-creates them on every `pnpm install` (postinstall).
21
+ *
22
+ * It is a no-op (exit 0) when no global dsh install is found — a dev machine
23
+ * without dsh simply cannot typecheck against dsh types.
24
+ */
25
+
26
+ import { execFileSync } from 'node:child_process'
27
+ import { existsSync, mkdirSync, readdirSync, realpathSync, rmSync, symlinkSync } from 'node:fs'
28
+ import { dirname, join } from 'node:path'
29
+ import { fileURLToPath } from 'node:url'
30
+
31
+ const repoRoot = dirname(dirname(fileURLToPath(import.meta.url)))
32
+ // Guard: this postinstall is a dev-machine convenience for THIS repo. A
33
+ // published tarball ships scripts/ (the hook must resolve), but the linker
34
+ // must never touch a consumer's node_modules there — it would delete their
35
+ // @deepseek-ai packages and replace them with symlinks into their global dsh
36
+ // closure. pnpm ≥10 blocks dependency lifecycle scripts by default, but npm
37
+ // would run this — exit silently outside the repo. The repo markers must be
38
+ // things the files allow-list never ships: tsconfig.json (dev typecheck
39
+ // input) and .git.
40
+ if (!existsSync(join(repoRoot, 'tsconfig.json')) || !existsSync(join(repoRoot, '.git'))) {
41
+ process.exit(0)
42
+ }
43
+ const scopeDir = join(repoRoot, 'node_modules', '@deepseek-ai')
44
+
45
+ /** The global dsh package's own `node_modules/@deepseek-ai` closure dir. */
46
+ function findDshClosure() {
47
+ // 0) Explicit override for dev/typecheck against an unreleased dsh line,
48
+ // e.g. a scratch closure from: npm i --prefix ~/tmp/dsh-alpha-closure @deepseek-ai/dsh@alpha
49
+ // DSH_CLOSURE_DIR=~/tmp/dsh-alpha-closure/node_modules/@deepseek-ai node scripts/link-dsh-closure.mjs
50
+ const override = process.env.DSH_CLOSURE_DIR
51
+ if (override !== undefined && override !== "") {
52
+ const dir = realpathSync(override)
53
+ if (existsSync(join(dir, "cordis"))) return dir
54
+ console.warn(`[link-dsh-closure] DSH_CLOSURE_DIR=${override} lacks @deepseek-ai/cordis — ignoring override`)
55
+ }
56
+ // 1) Follow the `dsh` bin — the most faithful pointer to the installed CLI
57
+ // (`/opt/homebrew/bin/dsh` → …/lib/bin.js → pkg dir → its node_modules).
58
+ try {
59
+ const bin = execFileSync('which', ['dsh'], { encoding: 'utf8' }).trim()
60
+ if (bin !== '') {
61
+ const real = realpathSync(bin)
62
+ const closure = join(dirname(dirname(real)), 'node_modules', '@deepseek-ai')
63
+ if (existsSync(join(closure, 'cordis'))) return closure
64
+ }
65
+ } catch { /* dsh not on PATH */ }
66
+ // 2) Fall back to the global node_modules root — the dsh package's own
67
+ // nested closure (what current npm produces for a global install).
68
+ try {
69
+ const root = execFileSync('npm', ['root', '-g'], { encoding: 'utf8' }).trim()
70
+ const nested = join(root, '@deepseek-ai', 'dsh', 'node_modules', '@deepseek-ai')
71
+ if (existsSync(join(nested, 'cordis'))) return nested
72
+ // 3) Last resort: npm's flat global layout, where dsh's @deepseek-ai/*
73
+ // deps are hoisted straight into <npm root -g>/@deepseek-ai next to
74
+ // the dsh package itself.
75
+ const flat = join(root, '@deepseek-ai')
76
+ if (existsSync(join(flat, 'cordis'))) return flat
77
+ } catch { /* npm unavailable */ }
78
+ return undefined
79
+ }
80
+
81
+ const closure = findDshClosure()
82
+ if (closure === undefined) {
83
+ console.warn('[link-dsh-closure] global dsh not found — skipping @deepseek-ai links (dev without dsh)')
84
+ process.exit(0)
85
+ }
86
+
87
+ mkdirSync(scopeDir, { recursive: true })
88
+ let linked = 0
89
+ for (const name of readdirSync(closure)) {
90
+ const target = join(scopeDir, name)
91
+ const source = join(closure, name)
92
+ try {
93
+ // Replace any existing entry (stale symlink, or a local .pnpm copy a
94
+ // previous install created) with the closure link.
95
+ rmSync(target, { recursive: true, force: true })
96
+ symlinkSync(source, target, 'junction')
97
+ linked++
98
+ } catch (error) {
99
+ console.warn(`[link-dsh-closure] failed to link ${name}: ${(error instanceof Error ? error.message : String(error))}`)
100
+ }
101
+ }
102
+ console.log(`[link-dsh-closure] linked ${linked} @deepseek-ai/* packages from ${closure}`)
@@ -0,0 +1,154 @@
1
+ #!/usr/bin/env node
2
+ // Boot-smoke: mount the freshly built plugin into a scratch dsh profile
3
+ // (bundle patch + packed tarball) and boot it with the real dsh CLI.
4
+ //
5
+ // 1. npm pack the repo → tarball
6
+ // 2. scratch $DSH_HOME/profiles/smoke with the plugin as a file: dep and a
7
+ // dsh.profile.bundles entry (same shape as the user's real profiles)
8
+ // 3. npm install — lifecycle scripts ON, i.e. the consumer path. pnpm ≥10
9
+ // blocks dependency lifecycle scripts by default, which is exactly how
10
+ // the #2-class tarball breakage (postinstall → unshipped file) stayed
11
+ // invisible to every pnpm-based install; npm-class clients (the desktop
12
+ // plugin manager) are the ones that run it.
13
+ // 4. `dsh --profile smoke --dump-config` must compose the plugin into the
14
+ // tree (mount/patch-layer proof)
15
+ // 5. a real boot under a timeout must load the plugin tree without a
16
+ // loader error (a healthy boot is silent and survives to the kill
17
+ // signal; a broken plugin dies within ~1s with the loader error)
18
+ // 6. `dsh plugin --profile smoke remove <pkg>` must reconcile the profile
19
+ // tree back to stock: the bundles entry spliced, the patch layer
20
+ // dropped, and the plugin id gone from a fresh --dump-config
21
+ //
22
+ // Coverage notes (empirical, dsh 0.1.1-rc.2 and 0.1.2-alpha.3 hosts):
23
+ // - Everything apply() does synchronously — ctx.skills.registerProvider(),
24
+ // the entry-config router install, the global dispatcher takeover — is
25
+ // gated hard: a throw there dies with "plugin tree failed to load".
26
+ // - The ctx.inject(['settings']) listener wiring is NOT judgeable from the
27
+ // boot: a cordis inject sub-fiber that throws (missing method, rejected
28
+ // event registration) is contained silently. That seam is gated instead by
29
+ // `npm run check` against the linked dsh closure (signature-level) plus
30
+ // the unit suite (semantics-level). The valid settings.yaml section below
31
+ // exercises the host's one-shot legacy import path as far as the boot can
32
+ // show (on dsh >= 0.1.7-rc.1 hosts it feeds the entry's volatile config).
33
+ // - The host must match the plugin's target line (CI installs dsh@alpha):
34
+ // a mismatched older host can boot "cleanly" while the contained inject
35
+ // error silently drops the settings integration.
36
+ //
37
+ // Exit 0 = mounted and boots clean. Temp dir is kept and printed on failure,
38
+ // removed on success.
39
+
40
+ import { spawnSync } from 'node:child_process'
41
+ import { existsSync, mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
42
+ import { readFile } from 'node:fs/promises'
43
+ import { tmpdir } from 'node:os'
44
+ import path from 'node:path'
45
+ import { fileURLToPath } from 'node:url'
46
+
47
+ const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..')
48
+ const pkg = JSON.parse(await readFile(path.join(repoRoot, 'package.json'), 'utf8'))
49
+ const ownName = pkg.name // @aiwayds/dsh-llm-proxy
50
+
51
+ const work = mkdtempSync(path.join(tmpdir(), 'dsh-llm-proxy-smoke-'))
52
+ const home = path.join(work, 'dsh-home')
53
+ const profile = path.join(home, 'profiles', 'smoke')
54
+ mkdirSync(profile, { recursive: true })
55
+
56
+ function fail(message, output = '') {
57
+ console.error(`smoke-boot: FAIL — ${message}`)
58
+ if (output) console.error(output.split('\n').slice(0, 30).join('\n'))
59
+ console.error(`smoke-boot: scratch kept at ${work}`)
60
+ process.exit(1)
61
+ }
62
+
63
+ const pack = spawnSync('npm', ['pack', '--pack-destination', work], { cwd: repoRoot, encoding: 'utf8' })
64
+ if (pack.status !== 0 || pack.error) fail('npm pack failed', `${pack.stdout}\n${pack.stderr}`)
65
+ const tarball = path.join(work, pack.stdout.trim().split('\n').at(-1))
66
+
67
+ writeFileSync(path.join(profile, 'cordis.yml'), '# dsh profile root — empty; the tree is composed from the bundle patches\n[]\n')
68
+ writeFileSync(path.join(profile, 'cordis.patch.yml'), '# scratch smoke profile: no extra patch layer\n[]\n')
69
+ writeFileSync(path.join(profile, 'package.json'), JSON.stringify({
70
+ name: 'dsh-profile-smoke',
71
+ private: true,
72
+ dependencies: {
73
+ [ownName]: `file:${tarball}`,
74
+ },
75
+ dsh: {
76
+ profile: {
77
+ bundles: [
78
+ '@deepseek-ai/dsh-base',
79
+ ownName,
80
+ ],
81
+ },
82
+ },
83
+ }, null, 2) + '\n')
84
+
85
+ // A user settings.yaml section matching the plugin's entry id: on dsh
86
+ // >= 0.1.7-rc.1 hosts the boot's one-shot legacy import consumes it (all
87
+ // three keys are volatile config fields, so the section imports cleanly and
88
+ // the values land in the profile patch for this entry).
89
+ writeFileSync(path.join(home, 'settings.yaml'), `\
90
+ # smoke: user section consumed by the mounted dsh-llm-proxy plugin
91
+ dsh-llm-proxy:
92
+ enabled: true
93
+ systemMode: env
94
+ llmProxy:
95
+ - match: "smoke.example.test"
96
+ proxy: "http://127.0.0.1:1"
97
+ `)
98
+
99
+ const install = spawnSync('npm', ['install', '--no-audit', '--no-fund', '--loglevel=error'], { cwd: profile, encoding: 'utf8' })
100
+ if (install.status !== 0 || install.error) fail('npm install in the scratch profile failed (lifecycle scripts on — the consumer path)', `${install.stdout}\n${install.stderr}`)
101
+
102
+ // Consumer stand-down: the linker must not materialize a @deepseek-ai scope
103
+ // dir inside the installed package's own tree (that would shadow the host's
104
+ // bundled closure — the failure mode the optional-peer contract exists for).
105
+ const installedOwn = path.join(profile, 'node_modules', ownName)
106
+ if (existsSync(path.join(installedOwn, 'node_modules', '@deepseek-ai'))) {
107
+ fail('the closure linker ran inside the consumer profile — it must stand down outside the repo')
108
+ }
109
+
110
+ const dshEnv = { ...process.env, DSH_HOME: home }
111
+
112
+ // Phase 1 — mount proof: the composed tree must include the plugin.
113
+ const dump = spawnSync('dsh', ['--profile', 'smoke', '--dump-config'], { cwd: profile, encoding: 'utf8', env: dshEnv })
114
+ if (dump.status !== 0 || dump.error) fail('dsh --dump-config failed on the scratch profile', `${dump.stdout}\n${dump.stderr}`)
115
+ if (!dump.stdout.includes(ownName)) {
116
+ fail(`the composed profile tree does not contain ${ownName} — the bundle patch insert is broken`, dump.stdout)
117
+ }
118
+
119
+ // Phase 2 — boot proof: the plugin tree must LOAD without a loader error.
120
+ const bootSeconds = 25
121
+ const boot = spawnSync('dsh', ['--profile', 'smoke'], {
122
+ cwd: profile,
123
+ encoding: 'utf8',
124
+ timeout: bootSeconds * 1000,
125
+ killSignal: 'SIGKILL',
126
+ env: dshEnv,
127
+ })
128
+ const output = `${boot.stdout ?? ''}\n${boot.stderr ?? ''}`
129
+ const loaderErrors = [
130
+ /plugin tree failed to load/,
131
+ /failed to apply loader entry/,
132
+ /cannot get property ".*" without inject/,
133
+ /cannot get required service/,
134
+ /Cannot find (package|module)/,
135
+ ]
136
+ const hit = loaderErrors.filter((re) => re.test(output))
137
+ if (hit.length > 0) {
138
+ fail('the real host failed to load the plugin tree:', output.split('\n').filter((line) => hit.some((re) => re.test(line)) || /Error/.test(line)).slice(0, 15).join('\n'))
139
+ }
140
+ if (boot.signal !== 'SIGKILL' && boot.status !== 0) {
141
+ fail(`dsh exited early with code ${boot.status} and no loader error — unexpected`, output)
142
+ }
143
+
144
+ // Phase 3 — uninstall leg: removal must reconcile the profile tree back to stock.
145
+ const remove = spawnSync('dsh', ['plugin', '--profile', 'smoke', 'remove', ownName], { cwd: profile, encoding: 'utf8', env: dshEnv })
146
+ if (remove.status !== 0 || remove.error) fail('dsh plugin remove failed', `${remove.stdout}\n${remove.stderr}`)
147
+ const dumpAfter = spawnSync('dsh', ['--profile', 'smoke', '--dump-config'], { cwd: profile, encoding: 'utf8', env: dshEnv })
148
+ if (dumpAfter.status !== 0 || dumpAfter.error) fail('dsh --dump-config failed after removal', `${dumpAfter.stdout}\n${dumpAfter.stderr}`)
149
+ if (dumpAfter.stdout.includes('dsh-llm-proxy')) {
150
+ fail('the composed tree still contains the plugin entry after removal', dumpAfter.stdout)
151
+ }
152
+
153
+ console.log(`smoke-boot: PASS — ${ownName} composed into the scratch profile tree and booted clean in real dsh (${boot.signal === 'SIGKILL' ? `survived ${bootSeconds}s boot window` : `exited ${boot.status}`}); removal restored the stock tree`)
154
+ rmSync(work, { recursive: true, force: true })