dsh-keys-manager 0.0.0 → 0.2.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.
package/package.json CHANGED
@@ -1,27 +1,92 @@
1
1
  {
2
2
  "name": "dsh-keys-manager",
3
- "version": "0.0.0",
4
- "description": "Placeholder release that reserves the package name. The real package ships from https://github.com/btspoony/dsh-keys-manager via CI trusted publishing. Do not install 0.0.0.",
5
- "license": "MIT",
6
- "main": "index.js",
3
+ "version": "0.2.0",
4
+ "type": "module",
5
+ "description": "Multi-key management for DSH providers: sticky per-session round-robin, retry failover, cooldown recovery, and searchable extra providers.",
7
6
  "repository": {
8
7
  "type": "git",
9
- "url": "git+https://github.com/btspoony/dsh-keys-manager.git"
10
- },
11
- "bugs": {
12
- "url": "https://github.com/btspoony/dsh-keys-manager/issues"
8
+ "url": "https://github.com/btspoony/dsh-keys-manager"
13
9
  },
14
- "homepage": "https://github.com/btspoony/dsh-keys-manager#readme",
15
10
  "keywords": [
16
11
  "dsh",
17
12
  "dsh-plugin",
18
13
  "keys",
19
14
  "provider",
20
- "llm",
21
- "placeholder"
15
+ "llm"
16
+ ],
17
+ "license": "MIT",
18
+ "main": "./lib/index.js",
19
+ "types": "./lib/index.d.ts",
20
+ "icon": "./icon.svg",
21
+ "exports": {
22
+ ".": {
23
+ "types": "./lib/index.d.ts",
24
+ "default": "./lib/index.js"
25
+ },
26
+ "./client": {
27
+ "types": "./lib/client/index.d.ts",
28
+ "default": "./lib/client.js"
29
+ },
30
+ "./bundle/cordis.patch.yml": "./bundle/cordis.patch.yml",
31
+ "./locale/*.json": "./locale/*.json",
32
+ "./icon": "./icon.svg",
33
+ "./package.json": "./package.json"
34
+ },
35
+ "files": [
36
+ "lib",
37
+ "bundle",
38
+ "locale",
39
+ "icon.svg",
40
+ "scripts"
22
41
  ],
23
- "publishConfig": {
24
- "access": "public",
25
- "registry": "https://registry.npmjs.org/"
42
+ "dsh": {
43
+ "bundle": {
44
+ "patch": "./bundle/cordis.patch.yml"
45
+ },
46
+ "client": {
47
+ "platform": "web",
48
+ "inject": [
49
+ "@deepseek-ai/dsh-client-ui-plugin-manager",
50
+ "@deepseek-ai/dsh-client-ui-settings",
51
+ "@deepseek-ai/dsh-client-ui-settings-models",
52
+ "@deepseek-ai/dsh-client-locale",
53
+ "@deepseek-ai/dsh-client-connection"
54
+ ]
55
+ }
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
+ "engines": {
67
+ "node": "^22.19 || >=24"
68
+ },
69
+ "peerDependencies": {
70
+ "@deepseek-ai/cordis": "^4.0.5",
71
+ "@deepseek-ai/dsh-client-connection": "^0.2.1-rc.2",
72
+ "@deepseek-ai/dsh-client-locale": "^0.2.1-rc.2",
73
+ "@deepseek-ai/dsh-client-ui-plugin-manager": "^0.2.1-rc.2",
74
+ "@deepseek-ai/dsh-client-ui-settings": "^0.2.1-rc.2",
75
+ "@deepseek-ai/dsh-client-ui-settings-models": "^0.2.1-rc.2",
76
+ "@deepseek-ai/dsh-client-ui-slots": "^0.2.1-rc.2",
77
+ "@deepseek-ai/dsh-credentials": "^0.2.1-rc.2",
78
+ "@deepseek-ai/dsh-llm": "^0.2.1-rc.2",
79
+ "@deepseek-ai/dsh-settings": "^0.2.1-rc.2",
80
+ "@deepseek-ai/dsh-typert-registry": "^0.2.1-rc.2",
81
+ "react": "^18.2.0"
82
+ },
83
+ "devDependencies": {
84
+ "@deepseek-ai/cordis": "^4.0.5",
85
+ "@types/node": "^22.20.0",
86
+ "@types/react": "~18.3.31",
87
+ "esbuild": "^0.28.2",
88
+ "react": "^18.3.1",
89
+ "typescript": "^5.9.3",
90
+ "vitest": "^4.1.8"
26
91
  }
27
92
  }
@@ -0,0 +1,53 @@
1
+ /**
2
+ * Bundle the browser half into the module-loader artifact the DSH Web client
3
+ * executes.
4
+ *
5
+ * The source is a normal ESM TypeScript entry; esbuild emits CommonJS and the
6
+ * banner/footer wrap it in the loader's closure factory. React stays external
7
+ * because the browser module table provides it. Host and DSH packages must
8
+ * never appear as runtime imports: the Client half is rendered inside the
9
+ * harness, so only React is loaded from the module table.
10
+ */
11
+ import { build } from 'esbuild'
12
+ import { readFile } from 'node:fs/promises'
13
+
14
+ const ID = 'dsh-keys-manager'
15
+ const OUT = 'lib/client.js'
16
+
17
+ const result = await build({
18
+ entryPoints: ['src/client/index.ts'],
19
+ outfile: OUT,
20
+ bundle: true,
21
+ format: 'cjs',
22
+ platform: 'browser',
23
+ target: 'es2020',
24
+ external: ['react', 'react/jsx-runtime'],
25
+ sourcemap: false,
26
+ logLevel: 'warning',
27
+ banner: {
28
+ js: [
29
+ '/* Keys Manager Client bundle; generated by scripts/build-client.mjs. */',
30
+ `window.__ModuleLoader__.load({ id: ${JSON.stringify(ID)}, factory: (require) => {`,
31
+ 'var module = { exports: {} }; var exports = module.exports;',
32
+ ].join('\n'),
33
+ },
34
+ footer: {
35
+ js: 'return module.exports; } });',
36
+ },
37
+ })
38
+
39
+ if (result.errors.length > 0) {
40
+ throw new Error(`client bundle build failed:\n${result.errors.map(error => error.text).join('\n')}`)
41
+ }
42
+
43
+ const text = await readFile(OUT, 'utf8')
44
+ if (!text.includes(`window.__ModuleLoader__.load({ id: ${JSON.stringify(ID)}`)) {
45
+ throw new Error('client bundle is missing the module-loader handoff')
46
+ }
47
+ if (text.includes('import.meta')) {
48
+ throw new Error('client bundle must not contain import.meta; the loader executes it as a classic script')
49
+ }
50
+ if (/require\(["']@deepseek-ai\//.test(text)) {
51
+ throw new Error('client bundle must not require @deepseek-ai packages at runtime')
52
+ }
53
+ console.log(`built ${OUT} (${Buffer.byteLength(text)} bytes)`)
@@ -0,0 +1,243 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Release-prep version helper (plan dsh-keys-manager-ci-release, task 2 + 4):
4
+ * resolves the target version — explicit `X.Y.Z` / `X.Y.Z-prerelease`
5
+ * argument (e.g. 0.1.1-alpha.1) or an auto patch bump from package.json —
6
+ * refuses already-released versions (existing `v<version>` git tag), aborts
7
+ * when there are no commits since the previous release tag (nothing to
8
+ * release), bumps package.json, inserts the `## [<version>]` section into
9
+ * CHANGELOG.md (from the same git-log notes the workflows use; keeps
10
+ * existing sections, no-op when already present), and prints
11
+ * `VERSION=<version>` for the workflow to capture. Node built-ins only, no
12
+ * new dependencies.
13
+ *
14
+ * Usage:
15
+ * node scripts/prepare-release.mjs # auto patch bump
16
+ * node scripts/prepare-release.mjs 0.2.0 # explicit version
17
+ * node scripts/prepare-release.mjs 0.1.1-alpha.1 # explicit prerelease
18
+ *
19
+ * Exit codes: 0 = bumped and printed VERSION; 1 = rejected (unparseable
20
+ * version, existing tag, no commits since the previous release tag,
21
+ * unparseable current package.json version, or CHANGELOG.md write error).
22
+ */
23
+
24
+ import { execFileSync } from 'node:child_process'
25
+ import { readFileSync, writeFileSync } from 'node:fs'
26
+ import { dirname, join } from 'node:path'
27
+ import { fileURLToPath } from 'node:url'
28
+
29
+ const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
30
+ const PKG_PATH = join(REPO_ROOT, 'package.json')
31
+ const CHANGELOG_PATH = join(REPO_ROOT, 'CHANGELOG.md')
32
+ const CHANGELOG_HEADER =
33
+ '# Changelog\n\nAll notable changes to dsh-keys-manager are documented here. Generated from git log by the release-prep workflow.\n'
34
+
35
+ /**
36
+ * Parse X.Y.Z or X.Y.Z[-prerelease] (numeric parts; optional prerelease
37
+ * suffix); null when not parseable. The suffix character set matches the
38
+ * workflows' guard regex `[0-9A-Za-z.-]+` (keep-simple: no leading-zero or
39
+ * empty-identifier rules), with one tightening: the suffix MUST contain at
40
+ * least one digit, so `1.2.3-alpha.1` / `1.2.3-rc.2` parse but `1.2.3-alpha`
41
+ * (no numeric identifier) and `1.2.3-` (empty suffix) are rejected. Returns
42
+ * { major, minor, patch, prerelease? } — prerelease is the raw suffix text
43
+ * (e.g. 'alpha.1') when present, undefined for formal versions.
44
+ */
45
+ function parseVersion(value) {
46
+ const match = /^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]*[0-9][0-9A-Za-z.-]*))?$/.exec(value)
47
+ if (!match) return null
48
+ return {
49
+ major: Number(match[1]),
50
+ minor: Number(match[2]),
51
+ patch: Number(match[3]),
52
+ prerelease: match[4] === undefined ? undefined : match[4],
53
+ }
54
+ }
55
+
56
+ const pkg = JSON.parse(readFileSync(PKG_PATH, 'utf8'))
57
+
58
+ let target
59
+ if (process.argv[2] !== undefined) {
60
+ target = parseVersion(process.argv[2])
61
+ if (target === null) {
62
+ console.error(`Error: invalid version "${process.argv[2]}". Expected X.Y.Z or X.Y.Z-prerelease (e.g. 0.2.0, 0.1.1-alpha.1).`)
63
+ process.exit(1)
64
+ }
65
+ } else {
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)
78
+ if (!m) {
79
+ console.error(`Error: cannot parse current package.json version "${pkg.version}".`)
80
+ process.exit(1)
81
+ }
82
+ const [, major, minor, patch, pre] = m
83
+ if (pre === undefined) {
84
+ target = { major: Number(major), minor: Number(minor), patch: Number(patch) + 1 }
85
+ } else {
86
+ const tail = pre.split('.').at(-1)
87
+ if (tail === undefined || !/^\d+$/.test(tail)) {
88
+ console.error(
89
+ `Error: cannot auto-bump "${pkg.version}": prerelease tail "${pre}" is not numeric. ` +
90
+ 'Pass an explicit version instead (e.g. `node scripts/prepare-release.mjs 0.1.1-alpha.1`).',
91
+ )
92
+ process.exit(1)
93
+ }
94
+ const prefix = pre.slice(0, pre.length - tail.length) // "alpha." for "alpha.1"
95
+ target = {
96
+ major: Number(major),
97
+ minor: Number(minor),
98
+ patch: Number(patch),
99
+ prerelease: `${prefix}${Number(tail) + 1}`,
100
+ }
101
+ }
102
+ }
103
+
104
+ const version =
105
+ target.prerelease === undefined
106
+ ? `${target.major}.${target.minor}.${target.patch}`
107
+ : `${target.major}.${target.minor}.${target.patch}-${target.prerelease}`
108
+
109
+ // Validate the npm dist-tag derived by the release workflow. A prerelease
110
+ // identifier like "latest" would move the stable tag, and a numeric one like
111
+ // "1" is not a legal npm tag. Reject both before the release PR is opened.
112
+ if (target.prerelease !== undefined) {
113
+ const distTag = target.prerelease.split('.')[0]
114
+ if (distTag === 'latest' || !/^[a-z][a-z0-9-]*$/.test(distTag)) {
115
+ console.error(
116
+ `Error: prerelease "${target.prerelease}" maps to invalid npm dist-tag "${distTag}". ` +
117
+ 'Use a prerelease such as alpha.1, beta.1, or rc.1.',
118
+ )
119
+ process.exit(1)
120
+ }
121
+ }
122
+
123
+ // Reject already-released versions: `git rev-parse v<version>` succeeds iff
124
+ // the tag exists. Works unchanged for prerelease tags (v0.1.1-alpha.1 is a
125
+ // plain tag name). Run from REPO_ROOT so the check is cwd-independent.
126
+ try {
127
+ execFileSync('git', ['rev-parse', `v${version}`], { cwd: REPO_ROOT, stdio: 'ignore' })
128
+ console.error(`Error: tag v${version} already exists — this version is already released.`)
129
+ process.exit(1)
130
+ } catch {
131
+ // Tag absent → OK to prepare.
132
+ }
133
+
134
+ // Fail fast on a no-change release: an empty notes range means no commits
135
+ // since the previous release tag — prepare-release must not bump the version
136
+ // or touch CHANGELOG.md for an empty release (no empty release PR). First-
137
+ // ever releases (no ancestor tag yet) span the full first-parent history,
138
+ // which is non-empty by construction, so they still pass.
139
+ const { prevTag, notes } = resolveReleaseNotes()
140
+ if (notes === '') {
141
+ console.error(
142
+ `Error: no commits since previous release tag ${prevTag === '' ? '<first commit>' : prevTag} — nothing to release. Aborting.`,
143
+ )
144
+ process.exit(1)
145
+ }
146
+
147
+ pkg.version = version
148
+ // Preserve repo formatting: 2-space indent + trailing newline. The version
149
+ // line must be the only diff (verified by `git diff package.json` in the
150
+ // dry-run validation).
151
+ writeFileSync(PKG_PATH, `${JSON.stringify(pkg, null, 2)}\n`, 'utf8')
152
+
153
+ /**
154
+ * Resolve the previous release tag (nearest ancestor tag of HEAD; empty
155
+ * when none exists yet) and the first-parent commit subjects since that
156
+ * tag (full first-parent history when no tag exists). Same git-log snippet
157
+ * the workflows use for the PR body — the CHANGELOG section and the PR body
158
+ * stay consistent. Do NOT use "v$VERSION^": the version string is not a
159
+ * git ref — the bump commit is not tagged yet.
160
+ */
161
+ function resolveReleaseNotes() {
162
+ let prevTag = ''
163
+ try {
164
+ prevTag = execFileSync('git', ['describe', '--tags', '--abbrev=0', 'HEAD'], {
165
+ cwd: REPO_ROOT,
166
+ encoding: 'utf8',
167
+ stdio: ['ignore', 'pipe', 'ignore'],
168
+ }).trim()
169
+ } catch {
170
+ // No ancestor tag yet → full history.
171
+ }
172
+ const range = prevTag === '' ? 'HEAD' : `${prevTag}..HEAD`
173
+ const notes = execFileSync('git', ['log', '--oneline', '--first-parent', range], {
174
+ cwd: REPO_ROOT,
175
+ encoding: 'utf8',
176
+ stdio: ['ignore', 'pipe', 'ignore'],
177
+ }).trim()
178
+ return { prevTag, notes }
179
+ }
180
+
181
+ /**
182
+ * Insert a `## [<version>] - <YYYY-MM-DD>` section under the `# Changelog`
183
+ * header, above the existing sections, from the pre-collected git-log notes
184
+ * (guaranteed non-empty by the no-change guard, so no fallback text is ever
185
+ * written). Preserves all existing sections; no-op when the section for this
186
+ * version already exists (idempotent re-runs must not duplicate it); write
187
+ * failures exit 1.
188
+ */
189
+ function updateChangelog(version, notes) {
190
+ let content
191
+ try {
192
+ content = readFileSync(CHANGELOG_PATH, 'utf8')
193
+ } catch {
194
+ content = CHANGELOG_HEADER
195
+ }
196
+ // A 0-byte or whitespace-only CHANGELOG.md (e.g. manual truncation) is
197
+ // treated as missing: such a file must still bootstrap with the standard
198
+ // header, otherwise the new section would land at the top with leading
199
+ // blank lines and no `# Changelog` header.
200
+ if (content.trim() === '') {
201
+ content = CHANGELOG_HEADER
202
+ }
203
+
204
+ if (content.includes(`## [${version}]`)) {
205
+ return
206
+ }
207
+
208
+ const sectionHeader = `## [${version}] - ${new Date().toISOString().slice(0, 10)}`
209
+ const sectionBody = notes
210
+ .split('\n')
211
+ .map((line) => `- ${line}`)
212
+ .join('\n')
213
+ // The section's own trailing `\n` plus the inserted `\n` below form the
214
+ // blank-line separator between the new section and the existing sections.
215
+ const section = `${sectionHeader}\n\n${sectionBody}\n`
216
+
217
+ // Insert on the first `## [` section header via string slice: everything
218
+ // at and below the insertion point — including the file's exact trailing
219
+ // newline — is preserved byte-for-byte. (A split('\n')/splice/join('\n')
220
+ // round-trip would re-normalize line breaks and could alter the tail, e.g.
221
+ // appending a trailing newline that was not in the source file.)
222
+ const firstSectionMatch = /^## \[/m.exec(content)
223
+ const firstSectionIdx = firstSectionMatch === null ? -1 : firstSectionMatch.index
224
+ let next
225
+ if (firstSectionIdx === -1) {
226
+ // No sections yet — append the new section after the header block,
227
+ // keeping the file's own trailing-newline state.
228
+ next = content.endsWith('\n') ? `${content}\n${section}` : `${content}\n\n${section}`
229
+ } else {
230
+ next = `${content.slice(0, firstSectionIdx)}${section}\n${content.slice(firstSectionIdx)}`
231
+ }
232
+
233
+ try {
234
+ writeFileSync(CHANGELOG_PATH, next, 'utf8')
235
+ } catch (error) {
236
+ console.error(`Error: cannot write ${CHANGELOG_PATH}: ${error.message}`)
237
+ process.exit(1)
238
+ }
239
+ }
240
+
241
+ updateChangelog(version, notes)
242
+
243
+ console.log(`VERSION=${version}`)
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Assert the built package contains every path the bundle manifest and the
3
+ * Web module loader resolve at runtime.
4
+ */
5
+ import { access, readFile } from 'node:fs/promises'
6
+
7
+ const REQUIRED = [
8
+ 'lib/index.js',
9
+ 'lib/index.d.ts',
10
+ 'lib/client.js',
11
+ 'lib/client/index.d.ts',
12
+ 'bundle/cordis.patch.yml',
13
+ 'locale/en.json',
14
+ 'locale/zh.json',
15
+ 'icon.svg',
16
+ ]
17
+
18
+ for (const path of REQUIRED) {
19
+ try {
20
+ await access(path)
21
+ } catch {
22
+ throw new Error(`missing built artifact: ${path}`)
23
+ }
24
+ }
25
+
26
+ const client = await readFile('lib/client.js', 'utf8')
27
+ if (!client.includes('dsh-keys-manager')) throw new Error('lib/client.js does not identify the bundle')
28
+ if (!client.includes('__ModuleLoader__')) throw new Error('lib/client.js is not a module-loader bundle')
29
+
30
+ console.log(`verified ${REQUIRED.length} artifacts`)
package/index.js DELETED
@@ -1,30 +0,0 @@
1
- // Placeholder stub for the `dsh-keys-manager` package name.
2
- // The real package ships from https://github.com/btspoony/dsh-keys-manager
3
- // via GitHub Actions OIDC trusted publishing (.github/workflows/release.yml).
4
- //
5
- // 0.0.0 is permanently reserved as the placeholder version: npm never allows
6
- // republishing the same version, so every real release must use a version
7
- // greater than 0.0.0.
8
- //
9
- // Load-time behavior: warn only, so bundler static analysis and accidental
10
- // transitive installs never crash at require/import time. Call-time behavior:
11
- // the exported function throws, so actual usage of this stub fails loudly
12
- // with an actionable message instead of being silently broken.
13
-
14
- 'use strict';
15
-
16
- const MESSAGE =
17
- 'dsh-keys-manager@0.0.0 is a placeholder package. Install a real version (> 0.0.0) from https://github.com/btspoony/dsh-keys-manager';
18
-
19
- // Warn once at load time except in production builds (set
20
- // DSH_KEYS_MANAGER_WARN=1 to force the warning there too).
21
- if (process.env.NODE_ENV !== 'production' || process.env.DSH_KEYS_MANAGER_WARN === '1') {
22
- console.warn(`[dsh-keys-manager] ${MESSAGE}`);
23
- }
24
-
25
- module.exports = function dshKeysManagerPlaceholder() {
26
- throw new Error(MESSAGE);
27
- };
28
-
29
- // Machine-readable marker so tooling can detect the stub without importing it.
30
- module.exports.isPlaceholder = true;