dsh-update-status 0.1.5 → 0.1.7

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/lib/index.d.ts CHANGED
@@ -22,6 +22,23 @@ type ReleaseCompatibility = 'verified' | 'unverified' | 'incompatible';
22
22
  */
23
23
  type UpdateWarningKind = 'failure' | 'notice';
24
24
  type InstallKind = 'npm-global' | 'pnpm-global' | 'source-checkout' | 'unknown';
25
+ /** Language-neutral warning facts; the browser renders them in its own locale. */
26
+ type UpdateWarning = {
27
+ code: 'registry-unavailable';
28
+ detail: string;
29
+ } | {
30
+ code: 'channel-unavailable';
31
+ channel: ReleaseChannel;
32
+ } | {
33
+ code: 'version-incomparable';
34
+ currentVersion: string;
35
+ channel: ReleaseChannel;
36
+ selectedVersion: string;
37
+ } | {
38
+ code: 'preview-unverified';
39
+ channel: ReleaseChannel;
40
+ version: string;
41
+ };
25
42
  interface ChannelRelease {
26
43
  channel: ReleaseChannel;
27
44
  version: string | null;
@@ -39,9 +56,12 @@ interface UpdateStatus {
39
56
  hasUpdate: boolean;
40
57
  cached: boolean;
41
58
  checkedAt: string | null;
59
+ /** English fallback for older clients; current clients localize `warnings`. */
42
60
  warning: string | null;
43
61
  /** Severity of `warning`; absent from a Host older than the field. */
44
62
  warningKind?: UpdateWarningKind | null;
63
+ /** Structured warning facts; absent from a Host older than this field. */
64
+ warnings?: UpdateWarning[];
45
65
  installKind: InstallKind;
46
66
  upgradeCommand: string;
47
67
  releaseUrl: string;
@@ -56,6 +76,32 @@ interface UpdateStatus {
56
76
  canApplyInPlace: false;
57
77
  }
58
78
  //#endregion
79
+ //#region src/host/settings.d.ts
80
+ /** Deployment config (ordinary) plus the volatile user preferences the form edits. */
81
+ interface UpdateStatusConfig {
82
+ /** Deployment: on-demand registry cache duration, in hours. */
83
+ cacheTtlHours?: number;
84
+ /** Deployment: registry request timeout, in milliseconds. */
85
+ timeoutMs?: number;
86
+ /** Deployment: run one metadata check when the process mounts the plugin. */
87
+ autoCheckOnMount?: boolean;
88
+ /** Preference: show the sidebar version chip and its update panel. */
89
+ sidebarEnabled?: boolean;
90
+ /** Preference: release channel followed for comparison and commands. */
91
+ channel?: ReleaseChannel;
92
+ /** Preference: on-demand registry cache duration, in minutes. */
93
+ cacheTtlMinutes?: number;
94
+ }
95
+ /**
96
+ * The plugin's Config schema; its volatile fields ARE the settings form.
97
+ *
98
+ * The explicit two-argument annotation is load-bearing: a `.volatile()` field's
99
+ * output is a stable reference (`Volatile<T>`) rather than the bare value the
100
+ * input side takes, so the inferred schema type cannot be named by the emitted
101
+ * `.d.ts` (TS2883) without stating the input side here.
102
+ */
103
+ export declare const Config: z<UpdateStatusConfig, Record<string, unknown>>;
104
+ //#endregion
59
105
  //#region src/host/installation.d.ts
60
106
  interface InstallationInfo {
61
107
  currentVersion: string;
@@ -78,7 +124,7 @@ interface UpdateStatusServiceOptions {
78
124
  ttlMs?: number;
79
125
  releaseUrl?: string;
80
126
  }
81
- declare class UpdateStatusService {
127
+ export declare class UpdateStatusService {
82
128
  private readonly installation;
83
129
  private readonly fetchLatest;
84
130
  private readonly now;
@@ -93,21 +139,14 @@ declare class UpdateStatusService {
93
139
  private refreshRelease;
94
140
  private statusAfterFailure;
95
141
  private statusFromCache;
96
- /** No remote data at all: the registry read itself failed, never a notice. */
97
142
  private statusWithoutRemoteRelease;
98
143
  }
99
144
  //#endregion
100
145
  //#region src/index.d.ts
101
- declare const name = "dsh-update-status";
146
+ export declare const name = "dsh-update-status";
102
147
  /** Connection supplies the authenticated transport; settings remains optional. */
103
- declare const inject: string[];
104
- interface Config {
105
- cacheTtlHours?: number;
106
- timeoutMs?: number;
107
- autoCheckOnMount?: boolean;
108
- }
109
- /** Deployment config only controls metadata-check timing; it never authorizes upgrades. */
110
- declare const Config: z<Config>;
111
- declare function apply(ctx: Context, config?: Config): void;
148
+ export declare const inject: string[];
149
+ export type Config = UpdateStatusConfig;
150
+ export declare function apply(ctx: Context, config?: Config): void;
112
151
  //#endregion
113
- export { Config, type InstallKind, type UpdateStatus, UpdateStatusService, apply, inject, name };
152
+ export type { InstallKind, UpdateStatus };
package/lib/index.js CHANGED
@@ -1,17 +1,8 @@
1
1
  import { createRequire } from "node:module";
2
- import z from "@deepseek-ai/schemastery";
3
2
  import { existsSync, readFileSync, realpathSync } from "node:fs";
4
3
  import { dirname, join, normalize, sep } from "node:path";
4
+ import z from "@deepseek-ai/schemastery";
5
5
  //#region src/shared/types.ts
6
- /**
7
- * JSON-only contract shared by the Host and Web halves.
8
- *
9
- * The static package uses the authenticated Connection RPC channel because
10
- * `harness.handle` / `host.call` are dynamic-Cordis-only closure APIs in DSH
11
- * 0.1.5-rc.1. The endpoint vocabulary remains deliberately small and private
12
- * to this plugin channel.
13
- */
14
- const PLUGIN_ID = "dsh-update-status";
15
6
  const PACKAGE_NAME = "@deepseek-ai/dsh";
16
7
  /** Shared Connection RPC channel. Custom prefixes 405 on the SPA fallback. */
17
8
  const UPDATE_STATUS_CHANNEL = "/api";
@@ -21,11 +12,7 @@ const UPDATE_STATUS_CHANNEL = "/api";
21
12
  * release the registry reports stays `unverified` — the plugin never claims a
22
13
  * compatibility nobody checked.
23
14
  */
24
- const VERIFIED_DSH_VERSIONS = [
25
- "0.1.5-rc.1",
26
- "0.1.6-alpha.1",
27
- "0.1.6-alpha.2"
28
- ];
15
+ const VERIFIED_DSH_VERSIONS = ["0.1.7-alpha.2", "0.1.7-rc.1"];
29
16
  const RELEASE_CHANNELS = [
30
17
  "latest",
31
18
  "next",
@@ -137,8 +124,8 @@ function upgradeCommandFor(installKind, packageName = PACKAGE_NAME, channel = "l
137
124
  switch (installKind) {
138
125
  case "npm-global": return `npm install -g ${specifier}`;
139
126
  case "pnpm-global": return `pnpm add -g ${specifier}`;
140
- case "source-checkout": return "在 DSH checkout 中拉取代码、安装依赖并重新构建;插件不会从 GUI 原地替换";
141
- default: return "请确认 dsh 安装方式后再升级;当前插件不会代为执行";
127
+ case "source-checkout": return "Update the DSH source checkout, install its dependencies, and rebuild it; this plugin cannot replace it in place from the GUI";
128
+ default: return "Confirm how DSH was installed before upgrading; this plugin cannot perform the upgrade for you";
142
129
  }
143
130
  }
144
131
  /**
@@ -266,34 +253,6 @@ function installUpdateStatusRpc(ctx, service) {
266
253
  });
267
254
  }
268
255
  //#endregion
269
- //#region src/host/settings.ts
270
- const SettingsSchema = z.object({
271
- sidebarEnabled: z.boolean().default(true),
272
- channel: z.union([
273
- "latest",
274
- "next",
275
- "alpha"
276
- ]).default("latest").loose(),
277
- cacheTtlMinutes: z.number().min(30).max(1440).default(360)
278
- });
279
- /** Settings are optional composition; absent providers leave the Web default on. */
280
- function installSettings(ctx) {
281
- ctx.inject(["settings"], (settingsCtx) => {
282
- try {
283
- settingsCtx.settings.register(PLUGIN_ID, SettingsSchema, {
284
- base: {
285
- sidebarEnabled: true,
286
- channel: "latest",
287
- cacheTtlMinutes: 360
288
- },
289
- applies: "live"
290
- });
291
- } catch (error) {
292
- console.error("[dsh-update-status] settings namespace registration failed:", error);
293
- }
294
- });
295
- }
296
- //#endregion
297
256
  //#region src/shared/semver.ts
298
257
  const SEMVER = /^(?:v)?(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?$/;
299
258
  const NUMERIC_IDENTIFIER = /^(0|[1-9]\d*)$/;
@@ -354,10 +313,20 @@ function boundedMessage(error) {
354
313
  const trimmed = (error instanceof Error ? error.message : String(error)).replace(/\s+/g, " ").trim();
355
314
  return trimmed === "" ? "unknown error" : trimmed.slice(0, 220);
356
315
  }
357
- function warningWith(base, addition) {
358
- if (base === null || base === "") return addition;
359
- if (addition === null || addition === "") return base;
360
- return `${base} ${addition}`;
316
+ function warningText(warning) {
317
+ switch (warning.code) {
318
+ case "registry-unavailable": return `Unable to check the npm registry: ${warning.detail}`;
319
+ case "channel-unavailable": return `The npm registry does not publish a ${warning.channel} channel.`;
320
+ case "version-incomparable": return `Unable to compare the current version ${warning.currentVersion} with ${warning.channel} channel version ${warning.selectedVersion} using SemVer.`;
321
+ case "preview-unverified": return `${warning.channel} is a preview channel; version ${warning.version} has not been verified as compatible with this plugin.`;
322
+ }
323
+ }
324
+ function warningFallback(warnings) {
325
+ return warnings.length === 0 ? null : warnings.map(warningText).join(" ");
326
+ }
327
+ function warningKindOf(warnings) {
328
+ if (warnings.length === 0) return null;
329
+ return warnings.every((warning) => warning.code === "preview-unverified") ? "notice" : "failure";
361
330
  }
362
331
  function dateOrNull(value) {
363
332
  if (typeof value !== "string" || value === "") return null;
@@ -442,16 +411,16 @@ var UpdateStatusService = class {
442
411
  async check(force = false, channel = "latest", cacheTtlMinutes) {
443
412
  const ttlMs = isCacheTtlMinutes(cacheTtlMinutes) ? cacheTtlMinutes * 60 * 1e3 : this.ttlMs;
444
413
  const cached = this.cache;
445
- if (!force && cached !== void 0 && this.now() - cached.checkedAtMs < ttlMs) return this.statusFromCache(cached, channel, true, null);
414
+ if (!force && cached !== void 0 && this.now() - cached.checkedAtMs < ttlMs) return this.statusFromCache(cached, channel, true, []);
446
415
  if (this.inFlight !== void 0) try {
447
- return this.statusFromCache(await this.inFlight, channel, false, null);
416
+ return this.statusFromCache(await this.inFlight, channel, false, []);
448
417
  } catch (error) {
449
418
  return this.statusAfterFailure(channel, error);
450
419
  }
451
420
  const run = this.refreshRelease();
452
421
  this.inFlight = run;
453
422
  try {
454
- return this.statusFromCache(await run, channel, false, null);
423
+ return this.statusFromCache(await run, channel, false, []);
455
424
  } catch (error) {
456
425
  return this.statusAfterFailure(channel, error);
457
426
  } finally {
@@ -467,10 +436,13 @@ var UpdateStatusService = class {
467
436
  return cache;
468
437
  }
469
438
  statusAfterFailure(channel, error) {
470
- const warning = `无法检查 npm registry:${boundedMessage(error)}`;
471
- return this.cache === void 0 ? this.statusWithoutRemoteRelease(channel, warning) : this.statusFromCache(this.cache, channel, true, warning);
439
+ const warnings = [{
440
+ code: "registry-unavailable",
441
+ detail: boundedMessage(error)
442
+ }];
443
+ return this.cache === void 0 ? this.statusWithoutRemoteRelease(channel, warnings) : this.statusFromCache(this.cache, channel, true, warnings);
472
444
  }
473
- statusFromCache(cache, channel, cached, initialWarning) {
445
+ statusFromCache(cache, channel, cached, initialWarnings) {
474
446
  const selected = cache.release.channels.find((release) => release.channel === channel) ?? {
475
447
  channel,
476
448
  version: null,
@@ -478,19 +450,31 @@ var UpdateStatusService = class {
478
450
  compatibility: "unverified"
479
451
  };
480
452
  const comparison = selected.version === null ? void 0 : compareSemver(this.installation.currentVersion, selected.version);
481
- const missingWarning = selected.version === null ? `npm registry 未发布 ${channel} 通道。` : null;
482
- const comparisonWarning = selected.version !== null && comparison === void 0 ? `无法按 SemVer 比较当前版本 ${this.installation.currentVersion} 与 ${channel} 通道版本 ${selected.version}。` : null;
483
- const previewWarning = channel !== "latest" && selected.version !== null && selected.compatibility !== "verified" ? `${channel} 是预览通道,版本 ${selected.version} 尚未验证与本插件兼容。` : null;
484
- const failureWarning = warningWith(warningWith(initialWarning, missingWarning), comparisonWarning);
485
- const warning = warningWith(failureWarning, previewWarning);
453
+ const warnings = [...initialWarnings];
454
+ if (selected.version === null) warnings.push({
455
+ code: "channel-unavailable",
456
+ channel
457
+ });
458
+ if (selected.version !== null && comparison === void 0) warnings.push({
459
+ code: "version-incomparable",
460
+ currentVersion: this.installation.currentVersion,
461
+ channel,
462
+ selectedVersion: selected.version
463
+ });
464
+ if (channel !== "latest" && selected.version !== null && selected.compatibility !== "verified") warnings.push({
465
+ code: "preview-unverified",
466
+ channel,
467
+ version: selected.version
468
+ });
486
469
  return {
487
470
  currentVersion: this.installation.currentVersion,
488
471
  latestVersion: selected.version,
489
472
  hasUpdate: comparison !== void 0 && comparison < 0,
490
473
  cached,
491
474
  checkedAt: new Date(cache.checkedAtMs).toISOString(),
492
- warning,
493
- warningKind: warning === null ? null : failureWarning === null ? "notice" : "failure",
475
+ warning: warningFallback(warnings),
476
+ warningKind: warningKindOf(warnings),
477
+ warnings,
494
478
  installKind: this.installation.installKind,
495
479
  upgradeCommand: upgradeCommandFor(this.installation.installKind, this.installation.packageName || "@deepseek-ai/dsh", channel),
496
480
  releaseUrl: this.releaseUrl,
@@ -502,16 +486,16 @@ var UpdateStatusService = class {
502
486
  canApplyInPlace: false
503
487
  };
504
488
  }
505
- /** No remote data at all: the registry read itself failed, never a notice. */
506
- statusWithoutRemoteRelease(channel, warning) {
489
+ statusWithoutRemoteRelease(channel, warnings) {
507
490
  return {
508
491
  currentVersion: this.installation.currentVersion,
509
492
  latestVersion: null,
510
493
  hasUpdate: false,
511
494
  cached: false,
512
495
  checkedAt: null,
513
- warning,
514
- warningKind: "failure",
496
+ warning: warningFallback(warnings),
497
+ warningKind: warningKindOf(warnings),
498
+ warnings,
515
499
  installKind: this.installation.installKind,
516
500
  upgradeCommand: upgradeCommandFor(this.installation.installKind, this.installation.packageName || "@deepseek-ai/dsh", channel),
517
501
  releaseUrl: this.releaseUrl,
@@ -530,16 +514,57 @@ var UpdateStatusService = class {
530
514
  }
531
515
  };
532
516
  //#endregion
533
- //#region src/index.ts
534
- const name = "dsh-update-status";
535
- /** Connection supplies the authenticated transport; settings remains optional. */
536
- const inject = ["connection"];
537
- /** Deployment config only controls metadata-check timing; it never authorizes upgrades. */
517
+ //#region src/host/settings.ts
518
+ /**
519
+ * The plugin's Config schema; its volatile fields ARE the settings form.
520
+ *
521
+ * The explicit two-argument annotation is load-bearing: a `.volatile()` field's
522
+ * output is a stable reference (`Volatile<T>`) rather than the bare value the
523
+ * input side takes, so the inferred schema type cannot be named by the emitted
524
+ * `.d.ts` (TS2883) without stating the input side here.
525
+ */
538
526
  const Config = z.object({
539
527
  cacheTtlHours: z.number().step(1).min(1).max(24).default(6),
540
528
  timeoutMs: z.number().step(1).min(1e3).max(3e4).default(DEFAULT_TIMEOUT_MS),
541
- autoCheckOnMount: z.boolean().default(true)
529
+ autoCheckOnMount: z.boolean().default(true),
530
+ sidebarEnabled: z.boolean().default(true).volatile(),
531
+ channel: z.union([
532
+ "latest",
533
+ "next",
534
+ "alpha"
535
+ ]).default("latest").loose().volatile(),
536
+ cacheTtlMinutes: z.number().min(30).max(1440).default(360).volatile()
542
537
  });
538
+ /**
539
+ * Suppress the auto-generated settings page for this entry.
540
+ *
541
+ * `autoGenerate` defaults to true, which would put a second, generic form beside
542
+ * the plugin's own `settings.section` page. Registering the policy is the
543
+ * documented move for a plugin that owns its preference UI (`ui-theme`,
544
+ * `ui-chat`, `ui-conversation` all do exactly this), and the entry's row stays
545
+ * in `settings.describe()` either way — the browser half still reads and writes
546
+ * it through `ctx.configForms`.
547
+ *
548
+ * Guarded twice: a deployment without the settings service never runs the inject
549
+ * callback (the plugin keeps working from its defaults), and a refused policy
550
+ * registration is contained instead of taking the plugin down.
551
+ *
552
+ * @param ctx - Host plugin context owning the entry's page policy.
553
+ */
554
+ function installSettings(ctx) {
555
+ try {
556
+ ctx.inject(["settings"], (child) => {
557
+ child.effect(() => child.settings.configure({ auto: false }, ctx.fiber));
558
+ });
559
+ } catch (error) {
560
+ console.error("[dsh-update-status] settings page policy registration failed:", error);
561
+ }
562
+ }
563
+ //#endregion
564
+ //#region src/index.ts
565
+ const name = "dsh-update-status";
566
+ /** Connection supplies the authenticated transport; settings remains optional. */
567
+ const inject = ["connection"];
543
568
  function boundedNumber(value, fallback, min, max) {
544
569
  if (typeof value !== "number" || !Number.isFinite(value)) return fallback;
545
570
  return Math.min(max, Math.max(min, Math.floor(value)));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-update-status",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Read-only DeepSeek Harness Web plugin that shows the running DSH version, discovers latest/next/alpha releases, warns about compatibility, and generates copy-only upgrade commands.",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
@@ -42,12 +42,17 @@
42
42
  "access": "public"
43
43
  },
44
44
  "dsh": {
45
+ "manifestVersion": 1,
46
+ "engines": {
47
+ "dsh": ">=0.1.7-alpha.2 <0.2.0"
48
+ },
45
49
  "bundle": {
46
50
  "patch": "./cordis.patch.yml"
47
51
  },
48
52
  "client": {
49
53
  "platform": "web",
50
54
  "inject": [
55
+ "@deepseek-ai/dsh-api-remotes",
51
56
  "@deepseek-ai/dsh-client-connection",
52
57
  "@deepseek-ai/dsh-client-ui-layout",
53
58
  "@deepseek-ai/dsh-client-ui-sidebar",
@@ -57,9 +62,8 @@
57
62
  },
58
63
  "compatibility": {
59
64
  "dshReleases": {
60
- "0.1.5-rc.1": "compatible",
61
- "0.1.6-alpha.1": "compatible",
62
- "0.1.6-alpha.2": "compatible"
65
+ "0.1.7-alpha.2": "compatible",
66
+ "0.1.7-rc.1": "compatible"
63
67
  }
64
68
  }
65
69
  },
@@ -72,23 +76,21 @@
72
76
  "version"
73
77
  ],
74
78
  "license": "MIT",
75
- "dependencies": {
76
- "@deepseek-ai/schemastery": "^3.18.2"
77
- },
78
79
  "peerDependencies": {
79
- "@deepseek-ai/cordis": "^4.0.2",
80
- "@deepseek-ai/dsh-settings": ">=0.1.5-rc.1"
80
+ "@deepseek-ai/cordis": "^4.0.4",
81
+ "@deepseek-ai/dsh-settings": ">=0.1.7-alpha.2 <0.2.0",
82
+ "@deepseek-ai/schemastery": "^3.18.4"
81
83
  },
82
84
  "devDependencies": {
83
- "@deepseek-ai/cordis": "^4.0.2",
84
- "@deepseek-ai/dsh-settings": "0.1.5-rc.1",
85
- "@deepseek-ai/schemastery": "^3.18.2",
85
+ "@deepseek-ai/cordis": "^4.0.4",
86
+ "@deepseek-ai/dsh-settings": "0.1.7-alpha.2",
87
+ "@deepseek-ai/schemastery": "^3.18.4",
86
88
  "@types/node": "^26.2.0",
87
89
  "@types/react": "^18.3.31",
88
90
  "react": "^18.3.1",
89
- "tsdown": "^0.22.14",
91
+ "tsdown": "^0.23.0",
90
92
  "typescript": "^7.0.2",
91
- "vitest": "^4.1.11"
93
+ "vitest": "^5.0.1"
92
94
  },
93
95
  "scripts": {
94
96
  "build": "tsdown",