@dshtrading/client-ui-updater 0.1.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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","names":["useState","useRef","useCallback","css"],"sources":["../src/client/locales.ts","../src/client/api.ts","../src/client/contract.ts","../src/client/UpdaterSection.tsx","../src/client/index.ts"],"sourcesContent":["/**\n * client-ui-updater 词典(纯数据模块:零运行时依赖)。\n *\n * 单一来源:本包 client apply 注册 zh/en(typed register 编译期校验键位);\n * packages/dsh-i18n 语言包构建期 import 本模块注册 zh-CN;scripts/i18n-audit.mjs\n * 静态加载本模块做 zh/en 键对齐与占位符对齐门禁。\n */\nimport type { UpdaterLocaleKey } from './contract.ts'\n\nexport const zh: Record<UpdaterLocaleKey, string> = {\n nav: '软件更新',\n lead: '检测 GitHub Releases 上的新版本,增量更新只下载变动的交易插件包,无需重装应用。',\n currentVersion: '交易插件版本',\n desktopApp: '桌面应用版本',\n unsupported: '当前环境不支持自动增量更新(仅 DSH Trading 桌面版支持)。可以前往发布页手动下载完整安装包。',\n viewReleases: '在 GitHub 查看发布',\n checkNow: '检查更新',\n checking: '正在检查…',\n lastCheck: '上次检查',\n never: '尚未检查',\n upToDate: '已是最新版本。',\n available: '发现新版本 {version}',\n publishedAt: '发布于 {date}',\n notesTitle: '更新说明',\n applyNow: '立即更新',\n payloadMissing: '该版本未附带增量更新包,请前往发布页下载完整安装包。',\n 'phase.prepare': '正在准备…',\n 'phase.download': '正在下载增量包…',\n 'phase.verify': '正在校验增量包…',\n 'phase.install': '正在安装更新…',\n progress: '{percent}%',\n done: '已更新到 {version}',\n restartHint: '重启 DSH Trading 后生效。',\n restartNow: '重启应用',\n restartManual: '请手动重启 DSH Trading 以完成更新。',\n checkError: '检查失败:{message}',\n retry: '重试',\n}\n\nexport const en: Record<UpdaterLocaleKey, string> = {\n nav: 'Software updates',\n lead: 'Detects new versions published on GitHub Releases and applies incremental updates — only the changed trading plugin packages are downloaded, no reinstall needed.',\n currentVersion: 'Trading plugins',\n desktopApp: 'Desktop app',\n unsupported: 'Incremental updates are only available inside the DSH Trading desktop app. You can download a full installer from the releases page.',\n viewReleases: 'View releases on GitHub',\n checkNow: 'Check for updates',\n checking: 'Checking…',\n lastCheck: 'Last checked',\n never: 'Never checked',\n upToDate: 'You are up to date.',\n available: 'New version {version} available',\n publishedAt: 'Published {date}',\n notesTitle: 'Release notes',\n applyNow: 'Update now',\n payloadMissing: 'This release does not carry the incremental update payload. Please download a full installer from the releases page.',\n 'phase.prepare': 'Preparing…',\n 'phase.download': 'Downloading update payload…',\n 'phase.verify': 'Verifying update payload…',\n 'phase.install': 'Installing update…',\n progress: '{percent}%',\n done: 'Updated to {version}',\n restartHint: 'Restart DSH Trading to activate.',\n restartNow: 'Restart app',\n restartManual: 'Please restart DSH Trading manually to finish the update.',\n checkError: 'Check failed: {message}',\n retry: 'Retry',\n}\n","/**\n * Updater bridge client: same-origin fetch wrappers over\n * /dshtrading/api/updater (the node half registers the route behind the\n * browser-auth fence; same-origin fetch carries the auth cookie by default).\n * Error semantics mirror the tasks bridge: non-2xx or { ok:false } envelopes\n * become rejections carrying the business code.\n */\nimport type { UpdaterSnapshot } from '../updater-service.ts'\n\nexport class UpdaterBridgeError extends Error {\n constructor(readonly status: number, message: string, readonly code?: string) {\n super(message)\n }\n}\n\nasync function updaterJson<T>(path: string, init?: RequestInit): Promise<T> {\n const response = await fetch(path, init)\n if (!response.ok) {\n const body = await response.json().catch(() => undefined) as { code?: string; message?: string } | undefined\n const detail = typeof body?.message === 'string' && body.message !== '' ? ': ' + body.message : ''\n throw new UpdaterBridgeError(response.status, 'updater ' + path + ' failed: ' + String(response.status) + detail, body?.code)\n }\n const wire = await response.json() as T\n if (wire !== null && typeof wire === 'object' && (wire as { ok?: unknown }).ok === false) {\n const business = wire as { code?: string; message?: string }\n throw new UpdaterBridgeError(200, (business.code ?? 'UPDATER_UNKNOWN') + ': ' + (business.message ?? 'updater bridge error'), business.code)\n }\n return wire\n}\n\n/** Current updater snapshot (environment + check + apply state). */\nexport async function fetchUpdaterState(): Promise<UpdaterSnapshot> {\n return updaterJson<UpdaterSnapshot>('/dshtrading/api/updater/state', { headers: { accept: 'application/json' } })\n}\n\n/** Run a GitHub release check; force=true bypasses the host-side TTL cache. */\nexport async function requestUpdaterCheck(force: boolean): Promise<UpdaterSnapshot> {\n return updaterJson<UpdaterSnapshot>(`/dshtrading/api/updater/check?force=${force ? '1' : '0'}`, { headers: { accept: 'application/json' } })\n}\n\n/** Start the incremental apply pipeline; returns the running snapshot. */\nexport async function requestUpdaterApply(): Promise<UpdaterSnapshot> {\n return updaterJson<UpdaterSnapshot>('/dshtrading/api/updater/apply', {\n method: 'POST',\n headers: { 'content-type': 'application/json' },\n body: '{}',\n })\n}\n\n/** Electron shell bridge exposed by the desktop preload (absent in browsers). */\ninterface DesktopBridge {\n relaunch?: () => void\n}\n\n/** Resolve the desktop shell bridge, if this page runs inside DSH Trading. */\nexport function desktopBridge(): DesktopBridge | undefined {\n if (typeof window === 'undefined' || !('desktop' in window)) return undefined\n return (window as { desktop?: DesktopBridge }).desktop\n}\n","/**\n * Locale contract for the updater settings section. The zh dictionary in\n * locales.ts is the key source of truth; this union + the LocaleNamespaceMap\n * merge give the framework-injected t seat its key types.\n */\nexport type UpdaterLocaleKey =\n | 'nav'\n | 'lead'\n | 'currentVersion'\n | 'desktopApp'\n | 'unsupported'\n | 'viewReleases'\n | 'checkNow'\n | 'checking'\n | 'lastCheck'\n | 'never'\n | 'upToDate'\n | 'available'\n | 'publishedAt'\n | 'notesTitle'\n | 'applyNow'\n | 'payloadMissing'\n | 'phase.prepare'\n | 'phase.download'\n | 'phase.verify'\n | 'phase.install'\n | 'progress'\n | 'done'\n | 'restartHint'\n | 'restartNow'\n | 'restartManual'\n | 'checkError'\n | 'retry'\n\ndeclare module '@deepseek-ai/dsh-client-ui-slots' {\n interface LocaleNamespaceMap {\n /** Updater settings section copy (check / notes / incremental apply). */\n 'dshtrading.updater': UpdaterLocaleKey\n }\n}\n\n/**\n * Window event contract with the trading shell rail (client-ui-trading\n * SessionRail badge). Cross-plugin coupling is intentionally a DOM custom\n * event, not an import (client plugins never import each other's modules).\n * Detail: { available: boolean, version?: string }.\n */\nexport const UPDATE_AVAILABLE_EVENT = 'dshtrading-update-available'\n","/**\n * Software-update settings page (the first-level section registered by this\n * plugin). Pure view over /dshtrading/api/updater: renders the current\n * versions, the latest GitHub release with its notes, and drives the\n * incremental apply + restart flow. Polls the host snapshot while mounted\n * (faster while an apply is running); no SSE dependency.\n */\nimport { useCallback, useEffect, useRef, useState } from 'react'\nimport type { PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots'\nimport type { UpdaterSnapshot } from '../updater-service.ts'\nimport { desktopBridge, fetchUpdaterState, requestUpdaterApply, requestUpdaterCheck } from './api.ts'\nimport { UPDATE_AVAILABLE_EVENT } from './contract.ts'\nimport css from './updater-section.module.css'\n\nexport type UpdaterSectionProps =\n & PropsRuntime<'settings.section'>\n & PropsLocale<'dshtrading.updater'>\n\n/** Poll cadences: lazy when idle, tight while an apply pipeline is running. */\nconst POLL_IDLE_MS = 30_000\nconst POLL_RUNNING_MS = 1_200\n\n/** ISO date (yyyy-mm-dd) for the published line; empty when unknown. */\nfunction formatDay(iso: string | undefined): string {\n if (iso === undefined) return ''\n const parsed = new Date(iso)\n if (Number.isNaN(parsed.getTime())) return ''\n return parsed.toISOString().slice(0, 10)\n}\n\nexport function UpdaterSection({ t }: UpdaterSectionProps) {\n const [state, setState] = useState<UpdaterSnapshot | undefined>(undefined)\n const [actionError, setActionError] = useState<string | undefined>(undefined)\n const [checkBusy, setCheckBusy] = useState(false)\n const applyBusyRef = useRef(false)\n\n const running = state?.apply.phase === 'running'\n\n const refresh = useCallback(async () => {\n try {\n setState(await fetchUpdaterState())\n } catch {\n // Bridge absent (headless profile / old deployment): keep the page quiet.\n }\n }, [])\n\n useEffect(() => { void refresh() }, [refresh])\n\n useEffect(() => {\n const interval = setInterval(() => { void refresh() }, running ? POLL_RUNNING_MS : POLL_IDLE_MS)\n return () => { clearInterval(interval) }\n }, [refresh, running])\n\n // Rail badge sync: mirror availability onto the window event the trading\n // shell rail listens to (see UPDATE_AVAILABLE_EVENT in contract.ts).\n useEffect(() => {\n if (state === undefined) return\n const available = state.check.available === true && state.apply.phase !== 'done'\n window.dispatchEvent(new CustomEvent(UPDATE_AVAILABLE_EVENT, {\n detail: { available, version: state.check.latest?.version },\n }))\n }, [state])\n\n const handleCheck = async (): Promise<void> => {\n setCheckBusy(true)\n setActionError(undefined)\n try {\n setState(await requestUpdaterCheck(true))\n } catch (error) {\n setActionError(error instanceof Error ? error.message : String(error))\n } finally {\n setCheckBusy(false)\n }\n }\n\n const handleApply = async (): Promise<void> => {\n if (applyBusyRef.current) return\n applyBusyRef.current = true\n setActionError(undefined)\n try {\n setState(await requestUpdaterApply())\n // Pipeline started: tighten polling immediately instead of waiting a tick.\n void refresh()\n } catch (error) {\n setActionError(error instanceof Error ? error.message : String(error))\n } finally {\n applyBusyRef.current = false\n }\n }\n\n const handleRestart = (): void => {\n desktopBridge()?.relaunch?.()\n }\n\n const environment = state?.environment\n const check = state?.check\n const apply = state?.apply\n const progress = state?.progress\n const latest = check?.latest\n const releasesUrl = latest?.url ?? 'https://github.com/zhu1090093659/dsh-trading/releases'\n const publishedDay = formatDay(latest?.publishedAt)\n const hasUpdate = check?.available === true && apply?.phase !== 'done'\n const applyable = hasUpdate && latest?.payloadAvailable === true\n\n return (\n <div className={css.root}>\n <p className={css.lead}>{t('lead')}</p>\n\n <div className={css.versions}>\n <div className={css.versionItem}>\n <span className={css.versionLabel}>{t('currentVersion')}</span>\n <span className={css.versionValue}>{environment?.familyVersion ?? '—'}</span>\n </div>\n {environment?.appVersion !== undefined && (\n <div className={css.versionItem}>\n <span className={css.versionLabel}>{t('desktopApp')}</span>\n <span className={css.versionValue}>{environment.appVersion}</span>\n </div>\n )}\n </div>\n\n {environment !== undefined && !environment.supported && (\n <p className={css.statusText}>{t('unsupported')}</p>\n )}\n\n {environment?.supported === true && (\n <>\n <div className={css.statusRow}>\n {check?.status === 'error' && (\n <span className={`${css.statusText} ${css.statusTextError}`}>\n {t('checkError', { message: check.error ?? '' })}\n </span>\n )}\n {check?.status === 'ok' && !hasUpdate && apply?.phase !== 'done' && (\n <span className={`${css.statusText} ${css.statusTextOk}`}>{t('upToDate')}</span>\n )}\n {check?.at !== undefined && (\n <span className={css.statusText}>\n {t('lastCheck')}\n :\n {' '}\n {check.at.slice(0, 19).replace('T', ' ')}\n </span>\n )}\n {check?.at === undefined && check?.status !== 'ok' && (\n <span className={css.statusText}>{t('never')}</span>\n )}\n <button\n type=\"button\"\n className={css.secondaryButton}\n disabled={checkBusy || running}\n onClick={() => { void handleCheck() }}\n >\n {checkBusy ? t('checking') : t('checkNow')}\n </button>\n {actionError !== undefined && (\n <button type=\"button\" className={css.secondaryButton} onClick={() => { void handleCheck() }}>\n {t('retry')}\n </button>\n )}\n </div>\n\n {running && (\n <div className={css.progressWrap}>\n <span className={css.progressLabel}>\n <span>\n {progress?.step === 'download'\n ? t('phase.download')\n : progress?.step === 'verify'\n ? t('phase.verify')\n : progress?.step === 'install'\n ? t('phase.install')\n : t('phase.prepare')}\n </span>\n {progress?.percent !== undefined && (\n <span>{t('progress', { percent: progress.percent })}</span>\n )}\n </span>\n <div\n className={css.progressTrack}\n role=\"progressbar\"\n aria-valuemin={0}\n aria-valuemax={100}\n aria-valuenow={progress?.percent ?? undefined}\n >\n <div\n className={`${css.progressFill} ${progress?.percent === undefined ? css.progressIndeterminate : ''}`}\n style={progress?.percent === undefined ? undefined : { width: `${progress.percent}%` }}\n />\n </div>\n </div>\n )}\n\n {hasUpdate && latest !== undefined && !running && (\n <div className={css.availableCard}>\n <p className={css.availableTitle}>{t('available', { version: latest.version })}</p>\n {publishedDay !== '' && (\n <p className={css.availableMeta}>{t('publishedAt', { date: publishedDay })}</p>\n )}\n {latest.notes.trim() !== '' && (\n <>\n <span className={css.notesTitle}>{t('notesTitle')}</span>\n <pre className={css.notes}>{latest.notes}</pre>\n </>\n )}\n <div className={css.actions}>\n {latest.payloadAvailable && (\n <button\n type=\"button\"\n className={css.primaryButton}\n onClick={() => { void handleApply() }}\n >\n {t('applyNow')}\n </button>\n )}\n <a className={css.link} href={releasesUrl} target=\"_blank\" rel=\"noreferrer\">\n {t('viewReleases')}\n </a>\n </div>\n {!latest.payloadAvailable && (\n <p className={css.doneMeta}>{t('payloadMissing')}</p>\n )}\n </div>\n )}\n\n {apply?.phase === 'done' && apply.targetVersion !== undefined && (\n <div className={css.doneCard}>\n <p className={css.doneTitle}>{t('done', { version: apply.targetVersion })}</p>\n <p className={css.doneMeta}>{t('restartHint')}</p>\n <div className={css.actions}>\n {desktopBridge()?.relaunch !== undefined && (\n <button type=\"button\" className={css.primaryButton} onClick={handleRestart}>\n {t('restartNow')}\n </button>\n )}\n {desktopBridge()?.relaunch === undefined && (\n <span className={css.doneMeta}>{t('restartManual')}</span>\n )}\n <a className={css.link} href={releasesUrl} target=\"_blank\" rel=\"noreferrer\">\n {t('viewReleases')}\n </a>\n </div>\n </div>\n )}\n\n {apply?.phase === 'error' && apply.error !== undefined && (\n <p className={css.errorText}>{apply.error}</p>\n )}\n </>\n )}\n\n {environment === undefined || !environment.supported ? (\n <a className={css.link} href={releasesUrl} target=\"_blank\" rel=\"noreferrer\">\n {t('viewReleases')}\n </a>\n ) : null}\n </div>\n )\n}\n","/**\n * dsh-trading auto-update, browser half — the '软件更新' first-level section\n * inside the host settings panel (settings.section, order 20: after the\n * official general/models/plugins and the trading section at order 8).\n *\n * The page talks to the node half over /dshtrading/api/updater (same-origin\n * fetch inside the browser-auth fence). In environments without incremental\n * support (dev checkouts, headless profiles) it degrades to an information\n * surface linking the GitHub releases page.\n */\nimport type { Context as ClientContext } from '@deepseek-ai/cordis'\nimport type {} from '@deepseek-ai/dsh-client-locale/client'\nimport type {} from './contract.ts'\nimport { en, zh } from './locales.ts'\nimport { UpdaterSection } from './UpdaterSection.tsx'\n\nimport { UPDATE_AVAILABLE_EVENT } from './contract.ts'\n/** 本面板/字符串翻译的 locale namespace。 */\nconst NS = 'dshtrading.updater'\n\n/** Required services (cordis fiber inject). */\nexport const inject = ['slots', 'locale']\n\n/** 注册『软件更新』设置一级菜单。 */\nexport function apply(ctx: ClientContext): void {\n const t = ctx.locale.bind(NS)\n ctx.effect(() => ctx.locale.register(NS, { zh, en }), 'dsh-trading-updater: dictionaries')\n\n ctx.slots.inject('settings.section', () => ctx.slots.register({\n name: 'settings.section',\n id: 'updater',\n order: 20,\n label: () => t('nav'),\n locale: NS,\n }, UpdaterSection))\n}\n\nexport { UPDATE_AVAILABLE_EVENT } from './contract.ts'\n"],"mappings":"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"}
@@ -0,0 +1,30 @@
1
+ //#region src/environment.d.ts
2
+ /** Parsed .dsh-desktop-seed.json content we care about. */
3
+ interface DesktopSeedMarker {
4
+ /** Bundled runtime stamp string the profile was seeded from. */
5
+ stamp?: string;
6
+ /** Electron app version of the seeding desktop app. */
7
+ appVersion?: string;
8
+ seededAt?: string;
9
+ }
10
+ /** Everything the updater needs to know about where it runs. */
11
+ interface UpdaterEnvironment {
12
+ /** @dshtrading/* family version = this package's own manifest version. */
13
+ familyVersion: string;
14
+ /** Live profile root when running inside a desktop-seeded profile. */
15
+ profileRoot: string | undefined;
16
+ /** Seed marker content when profileRoot is desktop-owned. */
17
+ desktopMarker: DesktopSeedMarker | undefined;
18
+ /** profileRoot/node_modules/@dshtrading — the swap target root. */
19
+ packagesRoot: string | undefined;
20
+ /** Incremental lane available: desktop-seeded AND packages root writable. */
21
+ supported: boolean;
22
+ }
23
+ interface DiscoverOptions {
24
+ /** Start file for the upward walk (defaults to this module's build output). */
25
+ fromFile?: string;
26
+ }
27
+ /** Discover from this package's own build output location. */
28
+ declare function discoverEnvironment(options?: DiscoverOptions): UpdaterEnvironment;
29
+ //#endregion
30
+ export { DesktopSeedMarker, DiscoverOptions, UpdaterEnvironment, discoverEnvironment };
@@ -0,0 +1,83 @@
1
+ import path from "node:path";
2
+ import fs from "node:fs";
3
+ import { fileURLToPath } from "node:url";
4
+ //#region src/environment.ts
5
+ /**
6
+ * Runtime environment discovery for the updater.
7
+ *
8
+ * The incremental lane only exists inside a desktop-seeded profile: the DSH
9
+ * Trading desktop app seeds $DSH_HOME/profiles/trading-web from its bundled
10
+ * payload and drops a .dsh-desktop-seed.json marker into the live profile
11
+ * root (desktop/src/runtime.cjs). Walking up from this package's installed
12
+ * location (node_modules/@dshtrading/client-ui-updater/lib/index.js) reaches
13
+ * that profile root; a dev checkout never finds the marker and the updater
14
+ * degrades to an information-only surface (release page links).
15
+ */
16
+ /** Marker file the desktop seeder writes into the live profile root. */
17
+ const DESKTOP_SEED_MARKER = ".dsh-desktop-seed.json";
18
+ /** This package's own name (anchors the family-version lookup). */
19
+ const OWN_PACKAGE_NAME = "@dshtrading/client-ui-updater";
20
+ function readOwnPackageVersion(startFile) {
21
+ let dir = path.dirname(startFile);
22
+ for (let depth = 0; depth < 12; depth += 1) {
23
+ const manifestPath = path.join(dir, "package.json");
24
+ try {
25
+ const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
26
+ if (manifest.name === OWN_PACKAGE_NAME && typeof manifest.version === "string") return manifest.version;
27
+ } catch {}
28
+ const parent = path.dirname(dir);
29
+ if (parent === dir) break;
30
+ dir = parent;
31
+ }
32
+ return "unknown";
33
+ }
34
+ function readSeedMarker(profileRoot) {
35
+ try {
36
+ const parsed = JSON.parse(fs.readFileSync(path.join(profileRoot, DESKTOP_SEED_MARKER), "utf8"));
37
+ return parsed && typeof parsed === "object" ? parsed : void 0;
38
+ } catch {
39
+ return;
40
+ }
41
+ }
42
+ function isWritable(target) {
43
+ try {
44
+ fs.accessSync(target, fs.constants.W_OK);
45
+ return true;
46
+ } catch {
47
+ return false;
48
+ }
49
+ }
50
+ /**
51
+ * Walk up from startFile looking for the desktop seed marker. Stops at the
52
+ * filesystem root; returns undefined outside a desktop profile (dev checkouts,
53
+ * headless profiles installed by hand).
54
+ */
55
+ function findDesktopProfileRoot(startFile) {
56
+ let dir = path.dirname(startFile);
57
+ for (let depth = 0; depth < 16; depth += 1) {
58
+ if (fs.existsSync(path.join(dir, ".dsh-desktop-seed.json"))) return dir;
59
+ const parent = path.dirname(dir);
60
+ if (parent === dir) return void 0;
61
+ dir = parent;
62
+ }
63
+ }
64
+ /** Discover the updater environment from a module file location. */
65
+ function discoverEnvironmentFrom(startFile) {
66
+ const familyVersion = readOwnPackageVersion(startFile);
67
+ const profileRoot = findDesktopProfileRoot(startFile);
68
+ const desktopMarker = profileRoot === void 0 ? void 0 : readSeedMarker(profileRoot);
69
+ const packagesRoot = profileRoot === void 0 ? void 0 : path.join(profileRoot, "node_modules", "@dshtrading");
70
+ return {
71
+ familyVersion,
72
+ profileRoot,
73
+ desktopMarker,
74
+ packagesRoot,
75
+ supported: profileRoot !== void 0 && packagesRoot !== void 0 && fs.existsSync(packagesRoot) && isWritable(packagesRoot)
76
+ };
77
+ }
78
+ /** Discover from this package's own build output location. */
79
+ function discoverEnvironment(options = {}) {
80
+ return discoverEnvironmentFrom(options.fromFile ?? fileURLToPath(import.meta.url));
81
+ }
82
+ //#endregion
83
+ export { DESKTOP_SEED_MARKER, discoverEnvironment, discoverEnvironmentFrom };
@@ -0,0 +1,13 @@
1
+ //#region src/github.d.ts
2
+ interface FetchReleaseOptions {
3
+ /** GitHub REST API base (overridable for tests). */
4
+ apiBase?: string;
5
+ /** repo slug, e.g. "zhu1090093659/dsh-trading". */
6
+ repo: string;
7
+ /** Timeout per request (default 15s). */
8
+ timeoutMs?: number;
9
+ /** Fetch implementation (defaults to globalThis.fetch; injectable for tests). */
10
+ fetchImpl?: typeof fetch;
11
+ }
12
+ //#endregion
13
+ export { FetchReleaseOptions };
package/lib/github.js ADDED
@@ -0,0 +1,85 @@
1
+ //#region src/github.ts
2
+ const GITHUB_API_BASE = "https://api.github.com";
3
+ /** Update payload manifest asset name for a tag, e.g. updates-manifest-v0.2.0.json. */
4
+ function manifestAssetName(tagName) {
5
+ return `updates-manifest-${tagName}.json`;
6
+ }
7
+ /** Update payload zip asset name for a tag, e.g. trading-update-v0.2.0.zip. */
8
+ function payloadAssetName(tagName) {
9
+ return `trading-update-${tagName}.zip`;
10
+ }
11
+ function parseAsset(raw) {
12
+ if (raw === null || typeof raw !== "object") return void 0;
13
+ const record = raw;
14
+ if (typeof record.name !== "string" || typeof record.browser_download_url !== "string") return void 0;
15
+ return {
16
+ name: record.name,
17
+ url: record.browser_download_url,
18
+ size: typeof record.size === "number" ? record.size : 0
19
+ };
20
+ }
21
+ /**
22
+ * Fetch the latest published release. Returns undefined when the repo has no
23
+ * full releases yet (GitHub answers 404 for /releases/latest then).
24
+ */
25
+ async function fetchLatestRelease(options) {
26
+ const apiBase = options.apiBase ?? "https://api.github.com";
27
+ const fetchImpl = options.fetchImpl ?? fetch;
28
+ const timeoutMs = options.timeoutMs ?? 15e3;
29
+ const controller = new AbortController();
30
+ const timer = setTimeout(() => {
31
+ controller.abort();
32
+ }, timeoutMs);
33
+ try {
34
+ const response = await fetchImpl(`${apiBase}/repos/${options.repo}/releases/latest`, {
35
+ headers: {
36
+ accept: "application/vnd.github+json",
37
+ "user-agent": "dsh-trading-updater"
38
+ },
39
+ signal: controller.signal
40
+ });
41
+ if (response.status === 404) return void 0;
42
+ if (!response.ok) throw new Error(`GitHub API ${response.status} for ${options.repo} releases/latest`);
43
+ const body = await response.json();
44
+ const tagName = typeof body.tag_name === "string" ? body.tag_name : "";
45
+ const assets = Array.isArray(body.assets) ? body.assets.map(parseAsset).filter((asset) => asset !== void 0) : [];
46
+ return {
47
+ tagName,
48
+ version: /^v/.test(tagName) ? tagName.slice(1) : tagName,
49
+ name: typeof body.name === "string" ? body.name : "",
50
+ notes: typeof body.body === "string" ? body.body : "",
51
+ url: typeof body.html_url === "string" ? body.html_url : `https://github.com/${options.repo}/releases`,
52
+ publishedAt: typeof body.published_at === "string" ? body.published_at : void 0,
53
+ assets
54
+ };
55
+ } finally {
56
+ clearTimeout(timer);
57
+ }
58
+ }
59
+ /** Parse + structurally validate a fetched manifest body. */
60
+ function parseUpdateManifest(text) {
61
+ const raw = JSON.parse(text);
62
+ if (raw.schema !== 1) throw new Error("unsupported updates-manifest schema");
63
+ const payload = raw.payload;
64
+ if (payload === void 0 || typeof payload.file !== "string" || typeof payload.sha256 !== "string" || typeof payload.bytes !== "number") throw new Error("updates-manifest payload block malformed");
65
+ if (!Array.isArray(raw.packages)) throw new Error("updates-manifest packages missing");
66
+ const packages = raw.packages.map((item) => {
67
+ const entry = item;
68
+ if (typeof entry.name !== "string" || typeof entry.version !== "string") throw new Error("updates-manifest package entry malformed");
69
+ return {
70
+ name: entry.name,
71
+ version: entry.version
72
+ };
73
+ });
74
+ if (typeof raw.version !== "string" || typeof raw.tag !== "string") throw new Error("updates-manifest version/tag missing");
75
+ return {
76
+ schema: 1,
77
+ version: raw.version,
78
+ tag: raw.tag,
79
+ generatedAt: typeof raw.generatedAt === "string" ? raw.generatedAt : "",
80
+ payload,
81
+ packages
82
+ };
83
+ }
84
+ //#endregion
85
+ export { GITHUB_API_BASE, fetchLatestRelease, manifestAssetName, parseUpdateManifest, payloadAssetName };
package/lib/index.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { discoverEnvironment } from "./environment.js";
2
+ import { UpdaterError, UpdaterService, UpdaterSnapshot } from "./updater-service.js";
3
+ import { Context } from "@deepseek-ai/cordis";
4
+ //#region src/index.d.ts
5
+ /** 本插件不硬依赖任何服务(headless 宿主零要求);web 面依赖在 apply 内声明。 */
6
+ declare const inject: readonly string[];
7
+ /** Update channel: this repository's GitHub Releases. */
8
+ declare const UPDATE_REPO = "zhu1090093659/dsh-trading";
9
+ /**
10
+ * Host plugin body: mount the updater service + routes (web host) or stay
11
+ * quiescent (headless). The service runs even without routes so a headless
12
+ * profile keeps its persisted check state consistent — auto-check self-gates
13
+ * on environment support.
14
+ * @param ctx - Host cordis context (bundle loader entry).
15
+ */
16
+ declare function apply(ctx: Context): void;
17
+ //#endregion
18
+ export { UPDATE_REPO, UpdaterError, UpdaterService, type UpdaterSnapshot, apply, discoverEnvironment, inject };
package/lib/index.js ADDED
@@ -0,0 +1,101 @@
1
+ import { discoverEnvironment } from "./environment.js";
2
+ import { UpdaterError, UpdaterService } from "./updater-service.js";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ //#region src/index.ts
6
+ /** 本插件不硬依赖任何服务(headless 宿主零要求);web 面依赖在 apply 内声明。 */
7
+ const inject = [];
8
+ /** Update channel: this repository's GitHub Releases. */
9
+ const UPDATE_REPO = "zhu1090093659/dsh-trading";
10
+ function sendJson(res, status, payload) {
11
+ res.writeHead(status, {
12
+ "content-type": "application/json; charset=utf-8",
13
+ "cache-control": "no-store"
14
+ });
15
+ res.end(JSON.stringify(payload));
16
+ }
17
+ /** Env override for the release channel (tests / mirrors); defaults to the repo. */
18
+ function repoSlug() {
19
+ const configured = process.env.DSH_TRADING_UPDATE_REPO;
20
+ return configured !== void 0 && configured.trim() !== "" ? configured.trim() : UPDATE_REPO;
21
+ }
22
+ /**
23
+ * Host plugin body: mount the updater service + routes (web host) or stay
24
+ * quiescent (headless). The service runs even without routes so a headless
25
+ * profile keeps its persisted check state consistent — auto-check self-gates
26
+ * on environment support.
27
+ * @param ctx - Host cordis context (bundle loader entry).
28
+ */
29
+ function apply(ctx) {
30
+ const env = discoverEnvironment();
31
+ const service = new UpdaterService({
32
+ env,
33
+ repo: repoSlug(),
34
+ statePath: process.env.DSH_TRADING_UPDATER_STATE ?? path.join(os.homedir(), ".dsh", "trading-updater", "state.json"),
35
+ ...process.env.DSH_TRADING_UPDATE_API_BASE !== void 0 && process.env.DSH_TRADING_UPDATE_API_BASE !== "" ? { github: { apiBase: process.env.DSH_TRADING_UPDATE_API_BASE } } : {}
36
+ });
37
+ ctx.effect(() => {
38
+ service.start();
39
+ return () => {
40
+ service.dispose();
41
+ };
42
+ }, "dsh-trading-client-ui-updater: service");
43
+ ctx.inject(["webServer", "connection"], (webCtx) => {
44
+ const webServer = webCtx.get("webServer");
45
+ const connection = webCtx.get("connection");
46
+ if (webServer === void 0 || connection === void 0) return;
47
+ const route = {
48
+ kind: "prefix",
49
+ path: "/dshtrading/api/updater",
50
+ handler: async (req, res) => {
51
+ const rejection = connection.requestRejection(req);
52
+ if (rejection !== void 0) {
53
+ res.writeHead(rejection);
54
+ res.end(rejection === 401 ? "unauthorized" : "forbidden");
55
+ return;
56
+ }
57
+ const url = new URL(req.url ?? "/", "http://dsh.local");
58
+ const mount = "/dshtrading/api/updater";
59
+ const raw = url.pathname;
60
+ const sub = raw === mount || raw.startsWith(`${mount}/`) ? raw.slice(23) || "/" : raw;
61
+ try {
62
+ if (req.method === "GET" && sub === "/state") {
63
+ sendJson(res, 200, service.snapshot());
64
+ return;
65
+ }
66
+ if (req.method === "GET" && sub === "/check") {
67
+ const force = url.searchParams.get("force") === "1";
68
+ sendJson(res, 200, await service.check(force));
69
+ return;
70
+ }
71
+ if (req.method === "POST" && sub === "/apply") {
72
+ sendJson(res, 200, await service.apply());
73
+ return;
74
+ }
75
+ sendJson(res, 404, {
76
+ ok: false,
77
+ code: "UPDATER_ROUTE_NOT_FOUND",
78
+ message: "unknown updater route: " + sub
79
+ });
80
+ } catch (error) {
81
+ if (error instanceof UpdaterError) {
82
+ sendJson(res, error.code === "UPDATER_BUSY" ? 409 : 400, {
83
+ ok: false,
84
+ code: error.code,
85
+ message: error.message
86
+ });
87
+ return;
88
+ }
89
+ sendJson(res, 200, {
90
+ ok: false,
91
+ code: "UPDATER_UNKNOWN",
92
+ message: error instanceof Error ? error.message : String(error)
93
+ });
94
+ }
95
+ }
96
+ };
97
+ ctx.effect(() => webServer.register(route), "dsh-trading-client-ui-updater: /dshtrading/api/updater route");
98
+ });
99
+ }
100
+ //#endregion
101
+ export { UPDATE_REPO, UpdaterError, UpdaterService, apply, discoverEnvironment, inject };
package/lib/semver.js ADDED
@@ -0,0 +1,36 @@
1
+ //#region src/semver.ts
2
+ /**
3
+ * Parse "v1.2.3" / "1.2.3" (optional leading v, optional trailing junk like
4
+ * build metadata is rejected — the release pipeline only ever tags vX.Y.Z).
5
+ */
6
+ function parseVersion(text) {
7
+ const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(text.trim());
8
+ if (match === null) return void 0;
9
+ const major = Number(match[1]);
10
+ const minor = Number(match[2]);
11
+ const patch = Number(match[3]);
12
+ if (!Number.isFinite(major) || !Number.isFinite(minor) || !Number.isFinite(patch)) return void 0;
13
+ return {
14
+ major,
15
+ minor,
16
+ patch
17
+ };
18
+ }
19
+ /** -1 / 0 / 1 numeric triple comparison. */
20
+ function compareVersions(a, b) {
21
+ for (const key of [
22
+ "major",
23
+ "minor",
24
+ "patch"
25
+ ]) if (a[key] !== b[key]) return a[key] < b[key] ? -1 : 1;
26
+ return 0;
27
+ }
28
+ /** True when candidate parses AND is strictly newer than current. */
29
+ function isNewerVersion(candidate, current) {
30
+ const candidateVersion = parseVersion(candidate);
31
+ const currentVersion = parseVersion(current);
32
+ if (candidateVersion === void 0 || currentVersion === void 0) return false;
33
+ return compareVersions(candidateVersion, currentVersion) > 0;
34
+ }
35
+ //#endregion
36
+ export { compareVersions, isNewerVersion, parseVersion };
@@ -0,0 +1,127 @@
1
+ import { UpdaterEnvironment } from "./environment.js";
2
+ import { FetchReleaseOptions } from "./github.js";
3
+ //#region src/updater-service.d.ts
4
+ /** Business error with a stable code (routes map it onto HTTP payloads). */
5
+ declare class UpdaterError extends Error {
6
+ readonly code: string;
7
+ constructor(code: string, message: string);
8
+ }
9
+ /** Apply pipeline phase. */
10
+ type UpdaterApplyPhase = 'idle' | 'running' | 'done' | 'error';
11
+ /** Fine-grained step inside a running apply. */
12
+ type UpdaterApplyStep = 'prepare' | 'download' | 'verify' | 'install';
13
+ /** Latest-release view persisted into state and rendered by the UI. */
14
+ interface UpdaterLatest {
15
+ version: string;
16
+ tagName: string;
17
+ name: string;
18
+ notes: string;
19
+ url: string;
20
+ publishedAt: string | undefined;
21
+ /** Both manifest + payload assets present for this release (incremental lane). */
22
+ payloadAvailable: boolean;
23
+ }
24
+ /** Check result state (status ok keeps latest; error keeps the last good latest). */
25
+ interface UpdaterCheckState {
26
+ at: string | undefined;
27
+ status: 'idle' | 'ok' | 'error';
28
+ error: string | undefined;
29
+ available: boolean;
30
+ latest: UpdaterLatest | undefined;
31
+ }
32
+ /** Apply pipeline state. */
33
+ interface UpdaterApplyState {
34
+ phase: UpdaterApplyPhase;
35
+ step: UpdaterApplyStep | undefined;
36
+ startedAt: string | undefined;
37
+ finishedAt: string | undefined;
38
+ targetVersion: string | undefined;
39
+ updated: string[];
40
+ skippedCount: number;
41
+ error: string | undefined;
42
+ }
43
+ /** Live download/verify progress (undefined when idle or indeterminate). */
44
+ interface UpdaterProgress {
45
+ step: UpdaterApplyStep;
46
+ bytes: number | undefined;
47
+ total: number | undefined;
48
+ percent: number | undefined;
49
+ }
50
+ /** Full wire snapshot (JSON for GET /state). */
51
+ interface UpdaterSnapshot {
52
+ environment: {
53
+ familyVersion: string;
54
+ appVersion: string | undefined;
55
+ profileRoot: string | undefined;
56
+ supported: boolean;
57
+ };
58
+ check: UpdaterCheckState;
59
+ apply: UpdaterApplyState;
60
+ progress: UpdaterProgress | undefined;
61
+ }
62
+ interface UpdaterServiceOptions {
63
+ env: UpdaterEnvironment;
64
+ /** GitHub repo slug, e.g. "zhu1090093659/dsh-trading". */
65
+ repo: string;
66
+ /** Absolute path of the persisted state file. */
67
+ statePath: string;
68
+ /** Release channel options forwarded to fetchLatestRelease. */
69
+ github?: Pick<FetchReleaseOptions, 'apiBase' | 'fetchImpl' | 'timeoutMs'>;
70
+ /** Injectable clock (tests). */
71
+ now?: () => number;
72
+ /** Periodic auto-check cadence (default 6h). */
73
+ autoCheckMs?: number;
74
+ /** Delay before the first auto check after start (default 15s). */
75
+ initialDelayMs?: number;
76
+ /** Freshness window for non-forced checks (default 30min). */
77
+ checkTtlMs?: number;
78
+ logger?: (line: string) => void;
79
+ }
80
+ declare class UpdaterService {
81
+ private readonly options;
82
+ private readonly env;
83
+ private readonly now;
84
+ private readonly autoCheckMs;
85
+ private readonly initialDelayMs;
86
+ private readonly checkTtlMs;
87
+ private readonly log;
88
+ private checkState;
89
+ private applyState;
90
+ private progress;
91
+ private timers;
92
+ private checkInFlight;
93
+ private applyInFlight;
94
+ private disposed;
95
+ constructor(options: UpdaterServiceOptions);
96
+ /** Load persisted check state + schedule auto checks. Idempotent. */
97
+ start(): void;
98
+ dispose(): void;
99
+ /**
100
+ * Check GitHub for a newer stable release. Non-forced calls honor the TTL
101
+ * and double as the periodic tick; force=true bypasses the cache (UI button).
102
+ */
103
+ check(force: boolean): Promise<UpdaterSnapshot>;
104
+ private checkFresh;
105
+ private latestOf;
106
+ /**
107
+ * Kick off the incremental apply pipeline. Returns immediately with the
108
+ * running snapshot; the UI polls state. Single-flight: a second call while
109
+ * running throws UPDATER_BUSY.
110
+ */
111
+ apply(): Promise<UpdaterSnapshot>;
112
+ /** The full pipeline; only called via apply() (single-flight owner). */
113
+ private runApply;
114
+ private installedVersionOf;
115
+ /** Unzip into a staging dir under the profile and cross-check the manifest. */
116
+ private stagePackages;
117
+ /** Stream the payload zip with progress; verify sha256 + byte length. */
118
+ private downloadPayload;
119
+ private downloadText;
120
+ private downloadChunks;
121
+ snapshot(): UpdaterSnapshot;
122
+ private stateFile;
123
+ private loadPersistedCheck;
124
+ private persistCheck;
125
+ }
126
+ //#endregion
127
+ export { UpdaterApplyPhase, UpdaterApplyState, UpdaterApplyStep, UpdaterCheckState, UpdaterError, UpdaterLatest, UpdaterProgress, UpdaterService, UpdaterServiceOptions, UpdaterSnapshot };