dsh-plugin-shop 0.4.3 → 0.4.5

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.
@@ -5,7 +5,7 @@
5
5
  * §11.3.4): no render path here may ever use dangerouslySetInnerHTML. */
6
6
  import { type ReactNode } from 'react';
7
7
  import type { InjectFace, PropsLocale, PropsRuntime } from '@deepseek-ai/dsh-client-ui-slots';
8
- import type { InstallArgs, ShopCatalogResult, ShopInstalledEntry, ShopInstallResult, ShopInstallStatusResult, ShopRestartResult, ShopSetEnabledResult, ShopUninstallResult } from '../host/index.ts';
8
+ import type { InstallArgs, ShopCatalogResult, ShopInstalledEntry, ShopInstallResult, ShopInstallStatusResult, ShopRestartResult, ShopSetEnabledResult, ShopUninstallResult, ShopUpdateResult, ShopVersionResult } from '../host/index.ts';
9
9
  /** The tab's Remote face: the Host result types, already unwrapped from the
10
10
  * wire envelope by `index.ts`; `catalog` throws on a wire error so the tab's
11
11
  * error state renders. */
@@ -26,6 +26,10 @@ export interface ShopTabInjected {
26
26
  name: string;
27
27
  }) => Promise<ShopUninstallResult>;
28
28
  restart: () => Promise<ShopRestartResult>;
29
+ version: () => Promise<ShopVersionResult>;
30
+ updateStart: (args: {
31
+ version: string;
32
+ }) => Promise<ShopUpdateResult>;
29
33
  }
30
34
  /** Full component props assembled by the Settings slot renderer. */
31
35
  export type ShopTabProps = PropsRuntime<'settings.plugins.tab'> & PropsLocale<'settings.shop'> & InjectFace<ShopTabInjected>;
@@ -59,7 +59,10 @@ export declare const zh: {
59
59
  restartBody: string;
60
60
  restartConfirm: string;
61
61
  restarting: string;
62
+ restartFailedNotice: string;
62
63
  restartTransportFailed: string;
64
+ updateFailed: string;
65
+ updateTransportFailed: string;
63
66
  enabledSwitch: string;
64
67
  toggleFailed: string;
65
68
  hotApplyNote: string;
@@ -126,7 +129,10 @@ export declare const en: {
126
129
  restartBody: string;
127
130
  restartConfirm: string;
128
131
  restarting: string;
132
+ restartFailedNotice: string;
129
133
  restartTransportFailed: string;
134
+ updateFailed: string;
135
+ updateTransportFailed: string;
130
136
  enabledSwitch: string;
131
137
  toggleFailed: string;
132
138
  hotApplyNote: string;
@@ -58,6 +58,13 @@ export type InstallEvent = {
58
58
  };
59
59
  /** §7.2 once-per-second poll cadence, as a named constant. */
60
60
  export declare const INSTALL_POLL_MS = 1000;
61
+ /** §8 restart handoff: the client polls the origin only after this grace
62
+ * period (the host's exit delay plus margin), so an origin that answers is
63
+ * the NEW server, never the dying old one. */
64
+ export declare const RESTART_GRACE_MS = 3000;
65
+ /** §8 restart handoff: how long the client waits for the new server to
66
+ * answer before reporting the manual restart command. */
67
+ export declare const RESTART_WAIT_MS = 30000;
61
68
  /** How many shelf cards mount at a time. The shelf holds ~1900 entries; one
62
69
  * commit of the whole grid is ~28k DOM nodes, so the list renders in batches
63
70
  * behind a sentinel (§A1) and grows on scroll. 48 is ~3-4 rows. */
@@ -0,0 +1,20 @@
1
+ /** Self-update driving hook for the shop itself: start, poll to terminal.
2
+ * Shares the install view machine and poll loop; the start mapping is the
3
+ * uninstall one — a business refusal (a non-semver version) lands in the
4
+ * `failed` view with the host's detail, never in the install `rejected`
5
+ * codes. */
6
+ import type { ShopInstallStatusResult, ShopUpdateResult } from '../host/index.ts';
7
+ import type { InstallView } from './present.ts';
8
+ export interface UseUpdateSelfResult {
9
+ view: InstallView;
10
+ start: (args: {
11
+ version: string;
12
+ }) => Promise<void>;
13
+ }
14
+ /** Drive one self-update: the host's business union, the shared polling
15
+ * loop, and terminal states. The `rejected` install state never occurs. */
16
+ export declare function useUpdateSelf(updateStart: (args: {
17
+ version: string;
18
+ }) => Promise<ShopUpdateResult>, installStatus: (args: {
19
+ installId: string;
20
+ }) => Promise<ShopInstallStatusResult>): UseUpdateSelfResult;
@@ -36,6 +36,13 @@ export interface ShopGatewayOptions {
36
36
  /** Test-only injection: the exit the restart calls after the response is
37
37
  * delivered. Production uses `process.exit`. */
38
38
  exit?: (code?: number) => void;
39
+ /** The pid the restart helper waits on before exec'ing the new dsh;
40
+ * defaults to this process. Tests point it at a dead pid so the fixture
41
+ * runs immediately instead of waiting for the vitest worker to exit. */
42
+ restartParentPid?: number;
43
+ /** Test-only injection: the shop's latest-version lookup; production
44
+ * fetches the npm packument. */
45
+ fetchLatestVersion?: () => Promise<string | null>;
39
46
  /** How long the gateway waits after a successful restart response before
40
47
  * exiting the old process; test-only shortening, production uses 2s. */
41
48
  restartExitDelayMs?: number;
@@ -72,6 +79,24 @@ export type ShopUninstallResult = {
72
79
  /** `shop/restart` result (§7.3): the restarted server's URL, or a typed
73
80
  * failure — on failure the old process is still serving. */
74
81
  export type ShopRestartResult = RestartOutcome;
82
+ /** `shop/version` result (§7.3): the RUNNING shop version (from the shipped
83
+ * package.json, not the manifest's range), the npm latest when the check
84
+ * could answer (`null` = no answer — advisory, never an error), and the
85
+ * comparison verdict. */
86
+ export interface ShopVersionResult {
87
+ installed: string;
88
+ latest: string | null;
89
+ outdated: boolean;
90
+ }
91
+ /** `shop/updateStart` result (§7.3): the self-update spawn, or a typed
92
+ * refusal (a version that is not plain semver). */
93
+ export type ShopUpdateResult = {
94
+ ok: true;
95
+ installId: string;
96
+ } | {
97
+ ok: false;
98
+ detail: string;
99
+ };
75
100
  /** `shop/installed` entry (§7.3): one installed catalog plugin. `installed`
76
101
  * is the profile manifest's dependency spec verbatim (a range, a tag, or
77
102
  * `workspace:*`); `outdated` is the Host's verdict that the installed version
@@ -111,6 +136,8 @@ export declare class ShopGateway extends TypertRemoteService {
111
136
  * production, a spy in tests. */
112
137
  private readonly exit;
113
138
  private readonly restartExitDelayMs;
139
+ private readonly restartParentPid;
140
+ private readonly latestVersion;
114
141
  /** The install gate runs against the last loaded snapshot, never a fresh
115
142
  * fetch per request (§7.2: the Host's cached snapshot is the truth). */
116
143
  /** Finished install records retained, so a poll sees the true terminal
@@ -184,11 +211,22 @@ export declare class ShopGateway extends TypertRemoteService {
184
211
  name: string;
185
212
  }): Promise<ShopUninstallResult>;
186
213
  /** Restart the dsh process the shop runs in (§8 amendment, 2026-08-27):
187
- * re-spawn this process's own command line, return the new server's URL
188
- * once it announces itself, and only then exit. A failed restart returns a
189
- * typed failure and the old process keeps serving — the restart is
190
- * all-or-nothing. The response must reach the browser before the exit, so
191
- * the exit is delayed past the RPC round-trip. */
214
+ * commit a two-phase handoff — a detached helper waits for this pid to
215
+ * exit, then re-runs this process's own command line — and exit once the
216
+ * response is out. The browser monitors the origin and refreshes when the
217
+ * new server answers. Refusals are issued before anything is torn down. */
192
218
  restart(): Promise<ShopRestartResult>;
219
+ /** The shop's own version and whether npm has a newer one (§7.3). The
220
+ * check is advisory: a registry that cannot answer leaves `latest` null
221
+ * and the client shows the version alone. `installed` is the RUNNING
222
+ * version (own-version.ts), not the manifest's range spec. */
223
+ version(): Promise<ShopVersionResult>;
224
+ /** Update the shop itself to a published version (§7.3): the explicit pin
225
+ * is the only install form that bypasses pnpm's release cooldown. The
226
+ * version is re-validated as plain semver at the boundary — the spec
227
+ * `dsh-plugin-shop@<version>` is built here, never from the wire. */
228
+ updateStart(args: {
229
+ version: string;
230
+ }): Promise<ShopUpdateResult>;
193
231
  }
194
232
  export default ShopGateway;
@@ -1,35 +1,38 @@
1
- /** Restart executor: re-spawn the Host's own command line and hand the
2
- * browser the new server's URL. The child is detached (its own process
3
- * group), so it survives both the parent's exit and the launching
4
- * terminal's. The parent exits only AFTER the child printed its
5
- * `dsh web: <url>` line — a restart that fails to come up leaves the old
6
- * process running untouched. */
7
- /** `shop/restart` outcome: the new server's URL, or a typed failure with an
8
- * author-readable detail. A failure means the OLD process is still serving —
9
- * the restart is all-or-nothing. */
1
+ /** Restart executor: hand the port to a new dsh instance, two-phase.
2
+ *
3
+ * The old process cannot wait for the new one: the new one must bind the
4
+ * port the old one still holds, and two live processes cannot bind it at
5
+ * once — the first implementation spawned the child and waited for its URL,
6
+ * and the child crashed in boot with EADDRINUSE every time. The handoff is
7
+ * therefore inverted: the parent commits and exits FIRST, and a detached
8
+ * helper waits for the parent's pid to disappear before exec'ing the same
9
+ * dsh command line. The browser monitors the origin and refreshes once the
10
+ * new server answers; a boot that fails is diagnosed from the log file,
11
+ * since nobody is attached to the child's pipes. */
12
+ /** `shop/restart` result: committed, or a typed refusal issued BEFORE
13
+ * anything is torn down. Once `ok` is returned the old process WILL exit —
14
+ * the client monitors the new server and reports a failed boot with the
15
+ * manual command. */
10
16
  export type RestartOutcome = {
11
17
  ok: true;
12
- url: string;
13
18
  } | {
14
19
  ok: false;
15
20
  detail: string;
16
21
  };
17
- /** Spawn the restarted server and wait for it to announce its URL.
18
- *
19
- * The child runs the same `dsh` with the same argv the current process was
20
- * launched with (`process.argv.slice(2)` — node and the CLI script path
21
- * stripped), so the profile, port and flags reproduce the user's launch
22
- * verbatim. `--port 0` therefore yields a NEW port, and the returned URL is
23
- * how the browser finds it.
22
+ /** Spawn the two-phase handoff. The helper is a POSIX shell wrapper that
23
+ * polls the parent pid until it is gone, then replaces itself with the dsh
24
+ * command — `exec "$@"` keeps the argv verbatim, so no argument quoting is
25
+ * involved. The child's stdout/stderr go to `logFile`, opened here in
26
+ * append mode; opening throws on failure, and the caller treats a throw as
27
+ * a refusal (the restart is never committed without its log).
24
28
  *
25
- * On success the caller exits the old process — but only after delivering
26
- * the RPC response carrying `url`, which is the caller's sequencing duty,
27
- * not this module's. On failure (child exits before announcing, spawn
28
- * error, or the timeout) the child is killed if still running and the old
29
- * process is untouched. */
29
+ * The pid-poll has the usual tiny reuse race — if the parent's pid is
30
+ * recycled within the 0.2s polling gap the helper waits for the unrelated
31
+ * process too. Harmless: it only delays the boot. */
30
32
  export declare function startRestart(options: {
31
33
  dshBin: string;
32
34
  argv: string[];
35
+ parentPid: number;
36
+ logFile: string;
33
37
  env?: NodeJS.ProcessEnv;
34
- timeoutMs?: number;
35
- }): Promise<RestartOutcome>;
38
+ }): void;
@@ -0,0 +1,9 @@
1
+ /** Self-update version check: the shop's latest published version, from the
2
+ * npm packument. Advisory by design — like the stars sidecar, a failed
3
+ * check degrades to `null` and never throws, never blocks a publish. The
4
+ * catalog cannot serve here: the shop is bootstrap-installed and is not
5
+ * harvested into its own catalog. */
6
+ /** Fetch the shop's `latest` dist-tag, or `null` when the registry cannot
7
+ * answer (network failure, unexpected payload — anything). The caller
8
+ * renders `null` as "no update check", never as a failure. */
9
+ export declare function fetchLatestVersion(fetchFn?: typeof fetch): Promise<string | null>;
@@ -0,0 +1,7 @@
1
+ /** The shop's own published version, read from the package.json that ships
2
+ * next to this package — the RUNNING version, not the manifest's range
3
+ * spec. This lives at the package root (not under src/host) on purpose:
4
+ * both the source tree (tests) and the bundled `lib/index.js` sit exactly
5
+ * one level below the package root, so the same relative URL resolves in
6
+ * both. */
7
+ export declare function ownVersion(): string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-plugin-shop",
3
- "version": "0.4.3",
3
+ "version": "0.4.5",
4
4
  "description": "The DeepSeek Harness plugin shop: browse, install, enable, and update dsh plugins from a git-auditable catalog.",
5
5
  "repository": {
6
6
  "type": "git",