@thewaifu/dsh-bot 1.1.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.
@@ -0,0 +1,170 @@
1
+ // DSH updates: a released DS Bot may name the DSH release it was verified
2
+ // against (`dsBot.verifiedDsh` in package.json); a release that names none
3
+ // offers nothing. The newest released bot wins: offering that version and
4
+ // nothing newer keeps the update path away from DSH releases we have not
5
+ // checked. A DSH installed from npm swaps with `npm i -g`; a packaged app's
6
+ // runtime lives inside its bundle and only its own updater moves it, to the
7
+ // newest release it finds, so the client offers it only when that release is
8
+ // the verified one.
9
+ import { spawn } from 'node:child_process'
10
+ import { readFileSync } from 'node:fs'
11
+ import { dirname, join } from 'node:path'
12
+
13
+ import {
14
+ CHECK_TTL_MS, cleanVersion, encodeName, fetchJson, installSpecOf,
15
+ newer, registryPlan, repoSlug, sourceOf,
16
+ } from './update.js'
17
+
18
+ const PKG = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'))
19
+ const APP_BOOT = '@deepseek-ai/dsh-app-boot'
20
+ const DSH_PACKAGE = '@deepseek-ai/dsh'
21
+
22
+ export function install(rt) {
23
+ const { ctx, config } = rt
24
+ const enabled = config.updateCheck !== false
25
+
26
+ const status = {
27
+ current: null, verified: null, kind: 'npm', blockedBy: null,
28
+ checkedAt: 0, phase: 'idle', installed: null, error: null,
29
+ }
30
+ let target = null
31
+ let checking = null
32
+ let running = null
33
+
34
+ // A newer `minBot` than the running bot gates the offer: the bot goes first.
35
+ const blocked = () => status.blockedBy !== null && newer(status.blockedBy, PKG.version)
36
+
37
+ const view = () => ({
38
+ current: status.current, verified: status.verified, kind: status.kind, blockedBy: status.blockedBy,
39
+ checkedAt: status.checkedAt, phase: status.phase, installed: status.installed, error: status.error,
40
+ // Nothing is offered without a current version to compare against.
41
+ available: status.phase !== 'installed' && target !== null && status.current !== null
42
+ && status.verified !== null && status.verified !== status.current && !blocked(),
43
+ })
44
+
45
+ // The version of the DSH that is running us: the app-boot export, or — where
46
+ // that import cannot see it — the nearest @deepseek-ai package.json above the
47
+ // entry script.
48
+ async function currentDsh() {
49
+ try {
50
+ const loader = ctx.get?.('loader')
51
+ const loaded = await loader?.import?.(APP_BOOT)
52
+ const appBoot = loader?.unwrapExports?.(loaded) ?? loaded?.default ?? loaded
53
+ const version = cleanVersion(appBoot?.getDshRuntimeVersion?.())
54
+ if (version) return version
55
+ } catch { /* the export is not visible here: read the install instead */ }
56
+ let dir = dirname(String(process.argv[1] ?? ''))
57
+ for (let depth = 0; depth < 12; depth += 1) {
58
+ try {
59
+ const manifest = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8'))
60
+ if (manifest?.name?.startsWith?.('@deepseek-ai/')) {
61
+ const version = cleanVersion(manifest.version)
62
+ if (version) return version
63
+ }
64
+ } catch { /* not a package directory */ }
65
+ const parent = dirname(dir)
66
+ if (parent === dir) break
67
+ dir = parent
68
+ }
69
+ return null
70
+ }
71
+
72
+ // A bundled package manager means the runtime came with the app and only the
73
+ // app's own updater can move it.
74
+ const installKind = () =>
75
+ typeof ctx.get?.('profileContext')?.packageManager?.command === 'string' ? 'app' : 'npm'
76
+
77
+ // The newest released DS Bot's `dsBot` block, through the same channel the bot
78
+ // itself updates from: its registry, or the public repo's latest release.
79
+ async function releasedDeclaration(source) {
80
+ if (source.kind === 'registry') {
81
+ for (const registry of await registryPlan(config, ctx.get?.('pluginManager'))) {
82
+ try {
83
+ const manifest = await fetchJson(`${registry}${encodeName(PKG.name)}/latest`)
84
+ if (manifest?.dsBot?.verifiedDsh) return manifest.dsBot
85
+ } catch { /* unreachable or not published there: ask the next one */ }
86
+ }
87
+ return null
88
+ }
89
+ const repo = repoSlug()
90
+ if (!repo) return null
91
+ try {
92
+ const release = await fetchJson(`https://api.github.com/repos/${repo}/releases/latest`, { 'user-agent': `${PKG.name}/${PKG.version}` })
93
+ const tag = cleanVersion(release?.tag_name)
94
+ if (!tag) return null
95
+ const manifest = await fetchJson(`https://raw.githubusercontent.com/${repo}/${release.tag_name}/ds-bot/package.json`, { 'user-agent': `${PKG.name}/${PKG.version}` })
96
+ return manifest?.dsBot?.verifiedDsh ? manifest.dsBot : null
97
+ } catch { return null }
98
+ }
99
+
100
+ async function runCheck() {
101
+ try {
102
+ status.current = await currentDsh()
103
+ status.kind = installKind()
104
+ // The release the running package itself verified stays the floor: a bot
105
+ // older than its own declaration has nothing newer to offer.
106
+ const declared = [{ verifiedDsh: PKG.dsBot?.verifiedDsh }]
107
+ const released = await releasedDeclaration(sourceOf(await installSpecOf(ctx)))
108
+ if (released) declared.push(released)
109
+ const newestDeclared = declared
110
+ .filter(entry => cleanVersion(entry.verifiedDsh))
111
+ .sort((a, b) => (newer(cleanVersion(a.verifiedDsh), cleanVersion(b.verifiedDsh)) ? -1 : 1))[0]
112
+ status.verified = cleanVersion(newestDeclared?.verifiedDsh)
113
+ // A release may gate the DSH update on a newer bot: offer nothing until
114
+ // the bot itself is at least `minBot`.
115
+ status.blockedBy = cleanVersion(newestDeclared?.minBot)
116
+ target = status.current !== null && status.verified !== null && status.verified !== status.current
117
+ ? { version: status.verified }
118
+ : null
119
+ status.error = null
120
+ } catch (error) {
121
+ rt.warn('ds-bot: dsh update check failed: %s', error?.message ?? error)
122
+ status.verified = null
123
+ status.blockedBy = null
124
+ target = null
125
+ }
126
+ status.checkedAt = Date.now()
127
+ }
128
+
129
+ /** Read current and verified DSH versions; a fresh check ignores the last answer. */
130
+ async function check({ fresh = false } = {}) {
131
+ if (!enabled) return view()
132
+ if (checking) return checking
133
+ if (!fresh && Date.now() - status.checkedAt < CHECK_TTL_MS) return view()
134
+ checking = runCheck().finally(() => { checking = null })
135
+ await checking
136
+ return view()
137
+ }
138
+
139
+ /** Swap an npm-installed DSH for the verified version; the answer comes back through `check`. */
140
+ function start() {
141
+ if (status.phase === 'installing') return view()
142
+ if (target === null || blocked()) throw new Error('No DSH update is available')
143
+ if (status.kind !== 'npm') throw new Error('This DSH updates with its app')
144
+ const chosen = target
145
+ Object.assign(status, { phase: 'installing', error: null })
146
+ const npm = process.platform === 'win32' ? 'npm.cmd' : 'npm'
147
+ const args = ['install', '-g', `${DSH_PACKAGE}@${chosen.version}`]
148
+ const runner = rt.dshSpawn ?? ((command, argv, options) => spawn(command, argv, options))
149
+ running = runner(npm, args, { windowsHide: true })
150
+ let output = ''
151
+ running.stdout?.on?.('data', chunk => { output = `${output}${chunk}`.slice(-4000) })
152
+ running.stderr?.on?.('data', chunk => { output = `${output}${chunk}`.slice(-4000) })
153
+ const done = new Promise((resolve, reject) => {
154
+ running.once?.('error', reject)
155
+ running.once?.('close', code => (code === 0 ? resolve() : reject(new Error(`npm exited ${code}`))))
156
+ })
157
+ void done
158
+ .then(() => Object.assign(status, { phase: 'installed', installed: chosen.version }))
159
+ .catch((error) => {
160
+ rt.warn('ds-bot: dsh update to %s failed: %s', chosen.version, error?.message ?? error)
161
+ Object.assign(status, { phase: 'failed', error: `${error?.message ?? error}${output ? ` — ${output.trim().slice(-400)}` : ''}`.slice(0, 600) })
162
+ })
163
+ return view()
164
+ }
165
+
166
+ // Whatever npm is still running when the plugin unloads dies with it.
167
+ ctx.effect(() => () => running?.kill?.(), 'ds-bot: dsh update install')
168
+
169
+ Object.assign(rt, { dshUpdateCheck: check, dshUpdateStart: start, dshUpdateView: view })
170
+ }