dsh-keys-manager 0.2.0 → 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 +33 -0
- package/README.md +108 -15
- package/README.zh-CN.md +80 -2
- package/bundle/cordis.patch.yml +5 -2
- package/lib/client/types.d.ts +27 -2
- package/lib/client.js +320 -3
- package/lib/domain.d.ts +55 -0
- package/lib/host-types.d.ts +29 -0
- package/lib/index.d.ts +865 -23
- package/lib/index.js +3453 -189
- package/lib/index.js.map +1 -1
- package/lib/providers.js +426 -0
- package/lib/providers.js.map +1 -1
- package/lib/store.js +4 -0
- package/lib/store.js.map +1 -1
- package/locale/en.json +17 -1
- package/locale/zh.json +17 -1
- package/package.json +14 -12
- package/scripts/prepare-release.mjs +161 -74
- package/scripts/verify-install.mjs +156 -0
package/locale/zh.json
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
"accountProjected": "服务中",
|
|
10
10
|
"accountReauth": "重新认证",
|
|
11
11
|
"accountActivate": "设为当前",
|
|
12
|
+
"testKey": "测试连接",
|
|
13
|
+
"testRunning": "测试中…",
|
|
14
|
+
"testPassed": "连接正常 · {ms} ms",
|
|
15
|
+
"testFailed": "连接失败:{error}",
|
|
16
|
+
"testTimedOut": "测试超时,未收到结果",
|
|
17
|
+
"testFailedUnknown": "测试未完成,未返回原因",
|
|
12
18
|
"keyStatusEmpty": "暂无密钥",
|
|
13
19
|
"noKeysAccount": "还没有账户。添加一个账户后即可使用该 Provider。",
|
|
14
20
|
"loginTitle": "账户登录",
|
|
@@ -21,5 +27,15 @@
|
|
|
21
27
|
"loginReady": "登录成功。",
|
|
22
28
|
"deleteAccountTitle": "删除账户“{alias}”?",
|
|
23
29
|
"deleteAccountBody": "这会从凭证库中删除该账户保存的 OAuth 授权。如果它是当前账户,该 Provider 路由将停用它并回落到 API-key 池;Provider 仍保持接管状态。",
|
|
24
|
-
"deleteAccountBodyNoKeys": "这会从凭证库中删除该账户保存的 OAuth 授权。该路由没有 API-key 池,删除后将停止服务,直到重新登录账户。"
|
|
30
|
+
"deleteAccountBodyNoKeys": "这会从凭证库中删除该账户保存的 OAuth 授权。该路由没有 API-key 池,删除后将停止服务,直到重新登录账户。",
|
|
31
|
+
"transportAccounts": "传输账户",
|
|
32
|
+
"transportTag": "传输",
|
|
33
|
+
"transportSignIn": "登录",
|
|
34
|
+
"transportExpires": "有效期至 {time}",
|
|
35
|
+
"transportStatusActive": "正常",
|
|
36
|
+
"transportStatusExpired": "令牌已过期",
|
|
37
|
+
"transportStatusNeedsReauth": "需重新登录",
|
|
38
|
+
"transportStatusSignedOut": "未登录",
|
|
39
|
+
"transportRemoveTitle": "移除 {name} 传输账户?",
|
|
40
|
+
"transportRemoveBody": "这会从凭证库中删除该传输路由保存的授权,并取消仍在进行的登录;路由自身不会重建它。要恢复服务需要重新登录。"
|
|
25
41
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-keys-manager",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Multi-key management for DSH providers: sticky per-session round-robin, retry failover, cooldown recovery, and searchable extra providers.",
|
|
6
6
|
"repository": {
|
|
@@ -54,15 +54,6 @@
|
|
|
54
54
|
]
|
|
55
55
|
}
|
|
56
56
|
},
|
|
57
|
-
"scripts": {
|
|
58
|
-
"typecheck": "tsc --noEmit && tsc -p tsconfig.client.json --noEmit && tsc -p tsconfig.spec.json --noEmit",
|
|
59
|
-
"build": "tsc -p tsconfig.build.json && tsc -p tsconfig.client.json && node scripts/build-client.mjs && node scripts/verify-dist.mjs",
|
|
60
|
-
"build:client": "tsc -p tsconfig.client.json && node scripts/build-client.mjs",
|
|
61
|
-
"test": "vitest run",
|
|
62
|
-
"release:prepare": "node scripts/prepare-release.mjs",
|
|
63
|
-
"prepare": "pnpm run build",
|
|
64
|
-
"prepack": "pnpm run build"
|
|
65
|
-
},
|
|
66
57
|
"engines": {
|
|
67
58
|
"node": "^22.19 || >=24"
|
|
68
59
|
},
|
|
@@ -80,6 +71,9 @@
|
|
|
80
71
|
"@deepseek-ai/dsh-typert-registry": "^0.2.1-rc.2",
|
|
81
72
|
"react": "^18.2.0"
|
|
82
73
|
},
|
|
74
|
+
"dependencies": {
|
|
75
|
+
"dsh-llm-omp-transports": "^0.2.2"
|
|
76
|
+
},
|
|
83
77
|
"devDependencies": {
|
|
84
78
|
"@deepseek-ai/cordis": "^4.0.5",
|
|
85
79
|
"@types/node": "^22.20.0",
|
|
@@ -87,6 +81,14 @@
|
|
|
87
81
|
"esbuild": "^0.28.2",
|
|
88
82
|
"react": "^18.3.1",
|
|
89
83
|
"typescript": "^5.9.3",
|
|
90
|
-
"vitest": "^4.1.8"
|
|
84
|
+
"vitest": "^4.1.8",
|
|
85
|
+
"yaml": "^2.9.1"
|
|
86
|
+
},
|
|
87
|
+
"scripts": {
|
|
88
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.client.json --noEmit && tsc -p tsconfig.spec.json --noEmit",
|
|
89
|
+
"build": "tsc -p tsconfig.build.json && tsc -p tsconfig.client.json && node scripts/build-client.mjs && node scripts/verify-dist.mjs",
|
|
90
|
+
"build:client": "tsc -p tsconfig.client.json && node scripts/build-client.mjs",
|
|
91
|
+
"test": "vitest run",
|
|
92
|
+
"release:prepare": "node scripts/prepare-release.mjs"
|
|
91
93
|
}
|
|
92
|
-
}
|
|
94
|
+
}
|
|
@@ -1,24 +1,33 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
/**
|
|
3
|
-
* Release-prep version helper (
|
|
3
|
+
* Release-prep version helper (plans dsh-keys-manager-ci-release, task 2 + 4;
|
|
4
|
+
* omp-transport-cursor-tools, task 5):
|
|
4
5
|
* resolves the target version — explicit `X.Y.Z` / `X.Y.Z-prerelease`
|
|
5
6
|
* argument (e.g. 0.1.1-alpha.1) or an auto patch bump from package.json —
|
|
6
7
|
* refuses already-released versions (existing `v<version>` git tag), aborts
|
|
7
8
|
* when there are no commits since the previous release tag (nothing to
|
|
8
9
|
* release), bumps package.json, inserts the `## [<version>]` section into
|
|
9
|
-
* CHANGELOG.md (
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
10
|
+
* CHANGELOG.md (root only — the transports package has no generated
|
|
11
|
+
* changelog), and prints the root and transports versions and tags for the
|
|
12
|
+
* workflow to capture. Node built-ins only, no new dependencies.
|
|
13
|
+
*
|
|
14
|
+
* The workspace package `packages/llm-omp` (`dsh-llm-omp-transports`) ships as
|
|
15
|
+
* part of the same release and always carries the same version: the resolved
|
|
16
|
+
* version is written to both package.json files, so one version input drives
|
|
17
|
+
* both tags (`v<version>` and `dsh-llm-omp-transports@<version>`). The
|
|
18
|
+
* transport version is checked against the tag the Release workflow creates
|
|
19
|
+
* after publishing, and the version written here is exactly the one
|
|
20
|
+
* `release.yml` publishes (that workflow skips an already-published version).
|
|
13
21
|
*
|
|
14
22
|
* Usage:
|
|
15
|
-
* node scripts/prepare-release.mjs
|
|
16
|
-
* node scripts/prepare-release.mjs 0.2.0
|
|
17
|
-
* node scripts/prepare-release.mjs
|
|
23
|
+
* node scripts/prepare-release.mjs # auto bump
|
|
24
|
+
* node scripts/prepare-release.mjs 0.2.0 # explicit version
|
|
25
|
+
* node scripts/prepare-release.mjs --dry-run # validate + print, write nothing
|
|
18
26
|
*
|
|
19
|
-
* Exit codes: 0 = bumped and printed
|
|
20
|
-
* version, existing tag, no commits since the previous
|
|
21
|
-
* unparseable current package.json version,
|
|
27
|
+
* Exit codes: 0 = bumped (or dry-run resolved) and printed versions; 1 =
|
|
28
|
+
* rejected (unparseable version, existing tag, no commits since the previous
|
|
29
|
+
* release tag, unparseable current package.json version, invalid prerelease
|
|
30
|
+
* dist-tag, or CHANGELOG.md write error).
|
|
22
31
|
*/
|
|
23
32
|
|
|
24
33
|
import { execFileSync } from 'node:child_process'
|
|
@@ -28,6 +37,8 @@ import { fileURLToPath } from 'node:url'
|
|
|
28
37
|
|
|
29
38
|
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
30
39
|
const PKG_PATH = join(REPO_ROOT, 'package.json')
|
|
40
|
+
const TRANSPORTS_PKG_PATH = join(REPO_ROOT, 'packages/llm-omp/package.json')
|
|
41
|
+
const TRANSPORT_PACKAGE = 'dsh-llm-omp-transports'
|
|
31
42
|
const CHANGELOG_PATH = join(REPO_ROOT, 'CHANGELOG.md')
|
|
32
43
|
const CHANGELOG_HEADER =
|
|
33
44
|
'# Changelog\n\nAll notable changes to dsh-keys-manager are documented here. Generated from git log by the release-prep workflow.\n'
|
|
@@ -53,82 +64,145 @@ function parseVersion(value) {
|
|
|
53
64
|
}
|
|
54
65
|
}
|
|
55
66
|
|
|
67
|
+
/**
|
|
68
|
+
* Refuse a prerelease whose derived npm dist-tag the release workflow would
|
|
69
|
+
* reject, for either package. The tag is the text between the first '-' and
|
|
70
|
+
* the first '.' after it ("alpha.1" -> "alpha").
|
|
71
|
+
*/
|
|
72
|
+
function assertPublishablePrerelease(prerelease) {
|
|
73
|
+
if (prerelease === undefined) return
|
|
74
|
+
const distTag = prerelease.split('.')[0]
|
|
75
|
+
if (distTag === 'latest' || !/^[a-z][a-z0-9-]*$/.test(distTag)) {
|
|
76
|
+
console.error(
|
|
77
|
+
`Error: prerelease "${prerelease}" maps to invalid npm dist-tag "${distTag}". ` +
|
|
78
|
+
'Use a prerelease such as alpha.1, beta.1, or rc.1.',
|
|
79
|
+
)
|
|
80
|
+
process.exit(1)
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
56
84
|
const pkg = JSON.parse(readFileSync(PKG_PATH, 'utf8'))
|
|
85
|
+
const transportsPkg = JSON.parse(readFileSync(TRANSPORTS_PKG_PATH, 'utf8'))
|
|
57
86
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
87
|
+
const { versionArg, dryRun } = parseArgs(process.argv.slice(2))
|
|
88
|
+
|
|
89
|
+
const target = resolveTarget(versionArg, pkg.version, 'package.json')
|
|
90
|
+
const version = formatVersion(target)
|
|
91
|
+
|
|
92
|
+
// Validate the npm dist-tag derived by the release workflow. A prerelease
|
|
93
|
+
// identifier like "latest" would move the stable tag, and a numeric one like
|
|
94
|
+
// "1" is not a legal npm tag. Reject both before the release PR is opened.
|
|
95
|
+
assertPublishablePrerelease(target.prerelease)
|
|
96
|
+
|
|
97
|
+
// The transports package ships from the same version as the root: one input
|
|
98
|
+
// drives both package.json files, so the two can never drift.
|
|
99
|
+
const transportVersion = version
|
|
100
|
+
const transportTag = `${TRANSPORT_PACKAGE}@${transportVersion}`
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Parse CLI flags. The first non-flag argument is the target version.
|
|
104
|
+
*/
|
|
105
|
+
function parseArgs(argv) {
|
|
106
|
+
const parsed = { versionArg: undefined, dryRun: false }
|
|
107
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
108
|
+
const arg = argv[i]
|
|
109
|
+
if (arg === '--dry-run') parsed.dryRun = true
|
|
110
|
+
else if (arg.startsWith('--')) usage(`unknown option "${arg}"`)
|
|
111
|
+
else if (parsed.versionArg === undefined) parsed.versionArg = arg
|
|
112
|
+
else usage(`unexpected extra argument "${arg}"`)
|
|
113
|
+
}
|
|
114
|
+
return parsed
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function usage(message) {
|
|
118
|
+
console.error(`Error: ${message}.`)
|
|
119
|
+
console.error(
|
|
120
|
+
'Usage: node scripts/prepare-release.mjs [X.Y.Z[-prerelease]] [--dry-run]',
|
|
121
|
+
)
|
|
122
|
+
process.exit(1)
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Resolve an explicit target version, or auto-bump the current one.
|
|
127
|
+
*
|
|
128
|
+
* Auto bump (mirrors sibling dsh-llm-fallbacks autoBumpPatch): parse the
|
|
129
|
+
* current version as X.Y.Z or X.Y.Z-pre, then:
|
|
130
|
+
* - formal version X.Y.Z -> X.Y.(Z+1) (0.1.2 -> 0.1.3)
|
|
131
|
+
* - prerelease with numeric tail -> bump only the tail, staying on the
|
|
132
|
+
* prerelease line (0.1.3-alpha.3 -> 0.1.3-alpha.4)
|
|
133
|
+
* - prerelease with non-numeric tail (e.g. 0.1.0-alpha) -> cannot auto
|
|
134
|
+
* bump; require an explicit version argument.
|
|
135
|
+
* The looser regex is deliberate: parseVersion (explicit args) requires a
|
|
136
|
+
* digit in the suffix, so it cannot tell "alpha" from a malformed version;
|
|
137
|
+
* this path needs the raw suffix to reject non-numeric tails with the
|
|
138
|
+
* explicit-version hint.
|
|
139
|
+
*/
|
|
140
|
+
function resolveTarget(explicit, current, label) {
|
|
141
|
+
if (explicit !== undefined) {
|
|
142
|
+
const target = parseVersion(explicit)
|
|
143
|
+
if (target === null) {
|
|
144
|
+
console.error(
|
|
145
|
+
`Error: invalid version "${explicit}". Expected X.Y.Z or X.Y.Z-prerelease (e.g. 0.2.0, 0.1.1-alpha.1).`,
|
|
146
|
+
)
|
|
147
|
+
process.exit(1)
|
|
148
|
+
}
|
|
149
|
+
return target
|
|
64
150
|
}
|
|
65
|
-
|
|
66
|
-
// Auto patch bump (mirrors sibling dsh-llm-fallbacks autoBumpPatch): parse
|
|
67
|
-
// the current version as X.Y.Z or X.Y.Z-pre, then:
|
|
68
|
-
// - formal version X.Y.Z -> X.Y.(Z+1) (0.1.2 -> 0.1.3)
|
|
69
|
-
// - prerelease with numeric tail -> bump only the tail, staying on the
|
|
70
|
-
// prerelease line (0.1.3-alpha.3 -> 0.1.3-alpha.4)
|
|
71
|
-
// - prerelease with non-numeric tail (e.g. 0.1.0-alpha) -> cannot auto
|
|
72
|
-
// bump; require an explicit version argument.
|
|
73
|
-
// The looser regex is deliberate: parseVersion (explicit args) requires a
|
|
74
|
-
// digit in the suffix, so it cannot tell "alpha" from a malformed version;
|
|
75
|
-
// this path needs the raw suffix to reject non-numeric tails with the
|
|
76
|
-
// explicit-version hint.
|
|
77
|
-
const m = /^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/.exec(pkg.version)
|
|
151
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:-(.+))?$/.exec(current)
|
|
78
152
|
if (!m) {
|
|
79
|
-
console.error(`Error: cannot parse current
|
|
153
|
+
console.error(`Error: cannot parse current ${label} version "${current}".`)
|
|
80
154
|
process.exit(1)
|
|
81
155
|
}
|
|
82
156
|
const [, major, minor, patch, pre] = m
|
|
83
157
|
if (pre === undefined) {
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
}
|
|
158
|
+
return { major: Number(major), minor: Number(minor), patch: Number(patch) + 1 }
|
|
159
|
+
}
|
|
160
|
+
const tail = pre.split('.').at(-1)
|
|
161
|
+
if (tail === undefined || !/^\d+$/.test(tail)) {
|
|
162
|
+
console.error(
|
|
163
|
+
`Error: cannot auto-bump "${current}": prerelease tail "${pre}" is not numeric. ` +
|
|
164
|
+
'Pass an explicit version instead (e.g. `node scripts/prepare-release.mjs 0.1.1-alpha.1`).',
|
|
165
|
+
)
|
|
166
|
+
process.exit(1)
|
|
167
|
+
}
|
|
168
|
+
const prefix = pre.slice(0, pre.length - tail.length) // "alpha." for "alpha.1"
|
|
169
|
+
return {
|
|
170
|
+
major: Number(major),
|
|
171
|
+
minor: Number(minor),
|
|
172
|
+
patch: Number(patch),
|
|
173
|
+
prerelease: `${prefix}${Number(tail) + 1}`,
|
|
101
174
|
}
|
|
102
175
|
}
|
|
103
176
|
|
|
104
|
-
|
|
105
|
-
target.prerelease === undefined
|
|
177
|
+
function formatVersion(target) {
|
|
178
|
+
return target.prerelease === undefined
|
|
106
179
|
? `${target.major}.${target.minor}.${target.patch}`
|
|
107
180
|
: `${target.major}.${target.minor}.${target.patch}-${target.prerelease}`
|
|
181
|
+
}
|
|
108
182
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
process.exit(1)
|
|
183
|
+
/** Whether an exact tag ref exists. */
|
|
184
|
+
function tagExists(tag) {
|
|
185
|
+
try {
|
|
186
|
+
execFileSync('git', ['rev-parse', '--verify', '--quiet', `refs/tags/${tag}`], {
|
|
187
|
+
cwd: REPO_ROOT,
|
|
188
|
+
stdio: 'ignore',
|
|
189
|
+
})
|
|
190
|
+
return true
|
|
191
|
+
} catch {
|
|
192
|
+
return false
|
|
120
193
|
}
|
|
121
194
|
}
|
|
122
195
|
|
|
123
|
-
// Reject already-released versions:
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
execFileSync('git', ['rev-parse', `v${version}`], { cwd: REPO_ROOT, stdio: 'ignore' })
|
|
196
|
+
// Reject already-released versions: the tag exists iff the version was
|
|
197
|
+
// released. Works unchanged for prerelease tags (v0.1.1-alpha.1 is a plain
|
|
198
|
+
// tag name). Run from REPO_ROOT so the check is cwd-independent.
|
|
199
|
+
if (tagExists(`v${version}`)) {
|
|
128
200
|
console.error(`Error: tag v${version} already exists — this version is already released.`)
|
|
129
201
|
process.exit(1)
|
|
130
|
-
}
|
|
131
|
-
|
|
202
|
+
}
|
|
203
|
+
if (tagExists(transportTag)) {
|
|
204
|
+
console.error(`Error: tag ${transportTag} already exists — this transport version is already released.`)
|
|
205
|
+
process.exit(1)
|
|
132
206
|
}
|
|
133
207
|
|
|
134
208
|
// Fail fast on a no-change release: an empty notes range means no commits
|
|
@@ -144,11 +218,17 @@ if (notes === '') {
|
|
|
144
218
|
process.exit(1)
|
|
145
219
|
}
|
|
146
220
|
|
|
147
|
-
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
221
|
+
// Write only outside a dry run: a dry run validates and prints the resolved
|
|
222
|
+
// versions exactly like a real run, then leaves the working tree untouched.
|
|
223
|
+
if (!dryRun) {
|
|
224
|
+
pkg.version = version
|
|
225
|
+
// Preserve repo formatting: 2-space indent + trailing newline. Bumping only
|
|
226
|
+
// the version keeps `git diff package.json` to that one line.
|
|
227
|
+
writeFileSync(PKG_PATH, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8')
|
|
228
|
+
|
|
229
|
+
transportsPkg.version = transportVersion
|
|
230
|
+
writeFileSync(TRANSPORTS_PKG_PATH, `${JSON.stringify(transportsPkg, null, 2)}\n`, 'utf8')
|
|
231
|
+
}
|
|
152
232
|
|
|
153
233
|
/**
|
|
154
234
|
* Resolve the previous release tag (nearest ancestor tag of HEAD; empty
|
|
@@ -238,6 +318,13 @@ function updateChangelog(version, notes) {
|
|
|
238
318
|
}
|
|
239
319
|
}
|
|
240
320
|
|
|
241
|
-
|
|
321
|
+
// Root changelog only: the transports package has no generated changelog.
|
|
322
|
+
if (!dryRun) updateChangelog(version, notes)
|
|
242
323
|
|
|
324
|
+
// The workflow captures VERSION; the remaining lines are informational and
|
|
325
|
+
// let a dry run show both derived tags without touching the tree.
|
|
326
|
+
if (dryRun) console.log('DRY_RUN=1')
|
|
243
327
|
console.log(`VERSION=${version}`)
|
|
328
|
+
console.log(`TAG=v${version}`)
|
|
329
|
+
console.log(`TRANSPORT_VERSION=${transportVersion}`)
|
|
330
|
+
console.log(`TRANSPORT_TAG=${transportTag}`)
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install/load integration evidence.
|
|
3
|
+
*
|
|
4
|
+
* Proves: packing this repository and installing the root tarball into a fresh
|
|
5
|
+
* DSH profile yields BOTH bundle components — the profile's node_modules
|
|
6
|
+
* resolves `dsh-keys-manager` and `dsh-llm-omp-transports`, and the composed
|
|
7
|
+
* profile tree (`dsh --profile <name> --dump-config`) contains both rows, so
|
|
8
|
+
* DSH's loader can mount two components from one installed plugin.
|
|
9
|
+
*
|
|
10
|
+
* Does NOT prove: the plugin-manager detail page and its enable/disable
|
|
11
|
+
* toggles (that rendering is DSH's own Web UI), or any Cursor network behavior.
|
|
12
|
+
*
|
|
13
|
+
* The run is fully local and needs no registry access: both tarballs come from
|
|
14
|
+
* `pnpm pack`, the root's `dsh-llm-omp-transports@^0.1.0` dependency is mapped
|
|
15
|
+
* to the local transports tarball through a profile override, and the profile
|
|
16
|
+
* keeps `autoInstallPeers: false` (DSH peers are provided by the installation).
|
|
17
|
+
* On this pnpm 12 the override lives in pnpm-workspace.yaml; the package.json
|
|
18
|
+
* `pnpm.overrides` copy keeps pnpm 9/10 working.
|
|
19
|
+
*
|
|
20
|
+
* Requires `pnpm` and `dsh` on PATH. Usage: node scripts/verify-install.mjs [--keep]
|
|
21
|
+
*/
|
|
22
|
+
import { execFile } from 'node:child_process'
|
|
23
|
+
import { access, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'
|
|
24
|
+
import { tmpdir } from 'node:os'
|
|
25
|
+
import { dirname, join } from 'node:path'
|
|
26
|
+
import { fileURLToPath } from 'node:url'
|
|
27
|
+
import { promisify } from 'node:util'
|
|
28
|
+
|
|
29
|
+
const execFileAsync = promisify(execFile)
|
|
30
|
+
const repoRoot = dirname(dirname(fileURLToPath(import.meta.url)))
|
|
31
|
+
const transportsDir = join(repoRoot, 'packages/llm-omp')
|
|
32
|
+
const TRANSPORTS = 'dsh-llm-omp-transports'
|
|
33
|
+
const PROFILE = 'install-proof'
|
|
34
|
+
/** The two loader rows the composed profile must carry. */
|
|
35
|
+
const ROWS = [
|
|
36
|
+
{ id: 'dsh-keys-manager', name: 'dsh-keys-manager' },
|
|
37
|
+
{ id: 'llm-omp-transports', name: TRANSPORTS },
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
async function run(command, args, options = {}) {
|
|
41
|
+
try {
|
|
42
|
+
return await execFileAsync(command, args, {
|
|
43
|
+
encoding: 'utf8',
|
|
44
|
+
maxBuffer: 64 * 1024 * 1024,
|
|
45
|
+
...options,
|
|
46
|
+
})
|
|
47
|
+
} catch (error) {
|
|
48
|
+
const output = [error.stdout, error.stderr].filter(Boolean).join('\n')
|
|
49
|
+
throw new Error(`${command} ${args.join(' ')} failed (${error.code ?? 'error'}):\n${output}`)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function manifestOf(dir) {
|
|
54
|
+
return JSON.parse(await readFile(join(dir, 'package.json'), 'utf8'))
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** One loader row, quoted or plain, exactly once in the dumped tree. */
|
|
58
|
+
function assertRow(dump, { id, name }) {
|
|
59
|
+
const row = new RegExp(`^- id: ${id}\\n name: '?${name}'?$`, 'mu')
|
|
60
|
+
if (!row.test(dump)) throw new Error(`dump-config is missing the ${id} row:\n${dump}`)
|
|
61
|
+
const count = dump.split(`- id: ${id}\n`).length - 1
|
|
62
|
+
if (count !== 1) throw new Error(`dump-config carries the ${id} row ${count} times:\n${dump}`)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let work
|
|
66
|
+
try {
|
|
67
|
+
for (const command of ['pnpm', 'dsh']) await run(command, ['--version'])
|
|
68
|
+
|
|
69
|
+
work = await mkdtemp(join(tmpdir(), 'dsh-install-proof-'))
|
|
70
|
+
const artifacts = join(work, 'artifacts')
|
|
71
|
+
await mkdir(artifacts, { recursive: true })
|
|
72
|
+
|
|
73
|
+
const root = await manifestOf(repoRoot)
|
|
74
|
+
const transports = await manifestOf(transportsDir)
|
|
75
|
+
await run('pnpm', ['pack', '--pack-destination', artifacts], { cwd: repoRoot })
|
|
76
|
+
await run('pnpm', ['pack', '--pack-destination', artifacts], { cwd: transportsDir })
|
|
77
|
+
const rootTarball = join(artifacts, `${root.name}-${root.version}.tgz`)
|
|
78
|
+
const transportsTarball = join(artifacts, `${TRANSPORTS}-${transports.version}.tgz`)
|
|
79
|
+
await access(rootTarball)
|
|
80
|
+
await access(transportsTarball)
|
|
81
|
+
|
|
82
|
+
// A packed workspace dependency must be a registry range, not `workspace:^`.
|
|
83
|
+
// The transports package ships from the same release version, so the
|
|
84
|
+
// expected range follows its manifest rather than a pinned literal.
|
|
85
|
+
const expectedRange = `^${transports.version}`
|
|
86
|
+
const packed = JSON.parse((await run('tar', ['-xzOf', rootTarball, 'package/package.json'])).stdout)
|
|
87
|
+
const pinned = packed.dependencies?.[TRANSPORTS]
|
|
88
|
+
if (pinned !== expectedRange) {
|
|
89
|
+
throw new Error(`packed root dependency ${TRANSPORTS} is ${JSON.stringify(pinned)}, expected ${JSON.stringify(expectedRange)}`)
|
|
90
|
+
}
|
|
91
|
+
console.log(`install-proof: packed dependency ${TRANSPORTS} -> ${pinned}`)
|
|
92
|
+
|
|
93
|
+
// A fresh profile: `dsh plugin install` initializes it (and runs pnpm).
|
|
94
|
+
const home = join(work, 'home')
|
|
95
|
+
const profileDir = join(home, 'profiles', PROFILE)
|
|
96
|
+
const env = { ...process.env, DSH_HOME: home }
|
|
97
|
+
await run('dsh', ['plugin', '--profile', PROFILE, 'install'], { env })
|
|
98
|
+
|
|
99
|
+
// Map the root's registry dependency to the local tarball before installing.
|
|
100
|
+
const profileManifest = await manifestOf(profileDir)
|
|
101
|
+
profileManifest.pnpm = {
|
|
102
|
+
...profileManifest.pnpm,
|
|
103
|
+
overrides: { ...profileManifest.pnpm?.overrides, [TRANSPORTS]: `file:${transportsTarball}` },
|
|
104
|
+
}
|
|
105
|
+
await writeFile(join(profileDir, 'package.json'), `${JSON.stringify(profileManifest, undefined, 2)}\n`)
|
|
106
|
+
await writeFile(
|
|
107
|
+
join(profileDir, 'pnpm-workspace.yaml'),
|
|
108
|
+
[
|
|
109
|
+
'# Local tarball mapping for the install proof; the packed root asks for',
|
|
110
|
+
`# ${TRANSPORTS}@${expectedRange}, which no registry here serves.`,
|
|
111
|
+
'overrides:',
|
|
112
|
+
` ${TRANSPORTS}: ${JSON.stringify(`file:${transportsTarball}`)}`,
|
|
113
|
+
'packages:',
|
|
114
|
+
' - .',
|
|
115
|
+
'',
|
|
116
|
+
'nodeLinker: hoisted',
|
|
117
|
+
'autoInstallPeers: false',
|
|
118
|
+
'',
|
|
119
|
+
].join('\n'),
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
// The alias form (`name@file:...`) is what pnpm 12 accepts for tarballs.
|
|
123
|
+
await run('dsh', ['plugin', '--profile', PROFILE, 'add', `${root.name}@file:${rootTarball}`], {
|
|
124
|
+
cwd: work,
|
|
125
|
+
env,
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
const installed = await manifestOf(profileDir)
|
|
129
|
+
if (installed.dependencies?.[root.name] !== `file:${rootTarball}`) {
|
|
130
|
+
throw new Error(`profile does not depend on the packed root: ${JSON.stringify(installed.dependencies)}`)
|
|
131
|
+
}
|
|
132
|
+
if (!installed.dsh?.profile?.bundles?.includes(root.name)) {
|
|
133
|
+
throw new Error(`profile did not select ${root.name} as a bundle: ${JSON.stringify(installed.dsh)}`)
|
|
134
|
+
}
|
|
135
|
+
const versions = []
|
|
136
|
+
for (const name of [root.name, TRANSPORTS]) {
|
|
137
|
+
const manifest = JSON.parse(await readFile(join(profileDir, 'node_modules', name, 'package.json'), 'utf8'))
|
|
138
|
+
if (manifest.name !== name) throw new Error(`${name} resolves to ${JSON.stringify(manifest.name)}`)
|
|
139
|
+
versions.push(`${name}@${manifest.version}`)
|
|
140
|
+
}
|
|
141
|
+
console.log(`install-proof: both components installed: ${versions.join(' + ')}`)
|
|
142
|
+
|
|
143
|
+
const dump = (await run('dsh', ['--profile', PROFILE, '--dump-config'], { env })).stdout
|
|
144
|
+
for (const row of ROWS) assertRow(dump, row)
|
|
145
|
+
console.log('install-proof: dump-config rows:')
|
|
146
|
+
for (const { id, name } of ROWS) console.log(` - id: ${id}\n name: ${name}`)
|
|
147
|
+
console.log('install-proof: OK')
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error(`install-proof: FAILED: ${error instanceof Error ? error.message : String(error)}`)
|
|
150
|
+
process.exitCode = 1
|
|
151
|
+
} finally {
|
|
152
|
+
if (work !== undefined) {
|
|
153
|
+
if (process.argv.includes('--keep')) console.log(`install-proof: kept ${work}`)
|
|
154
|
+
else await rm(work, { recursive: true, force: true })
|
|
155
|
+
}
|
|
156
|
+
}
|