dsh-plugin-shop 0.7.4 → 0.8.0-beta.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.
@@ -55,6 +55,9 @@ const dsh_plugin_shop_shop_catalog_result$schema = z.object({
55
55
  })
56
56
  const dsh_plugin_shop_shop_installed_result$schema = z.array(z.object({
57
57
  'name': z.string(),
58
+ 'source': z.union([z.literal("npm"), z.literal("github")]),
59
+ 'repo': z.union([z.undefined(), z.string()]).optional(),
60
+ 'subdir': z.union([z.undefined(), z.string()]).optional(),
58
61
  'installed': z.string(),
59
62
  'latest': z.string(),
60
63
  'outdated': z.boolean(),
@@ -64,13 +67,16 @@ const dsh_plugin_shop_shop_installStart_parameter_0$schema = z.object({
64
67
  'name': z.string(),
65
68
  'version': z.string(),
66
69
  'acknowledged': z.union([z.undefined(), z.literal(false), z.literal(true)]).optional(),
70
+ 'source': z.union([z.undefined(), z.literal("npm"), z.literal("github")]).optional(),
71
+ 'repo': z.union([z.undefined(), z.string()]).optional(),
72
+ 'subdir': z.union([z.undefined(), z.string()]).optional(),
67
73
  })
68
74
  const dsh_plugin_shop_shop_installStart_result$schema = z.union([z.object({
69
75
  'ok': z.literal(true),
70
76
  'installId': z.string(),
71
77
  }), z.object({
72
78
  'ok': z.literal(false),
73
- 'code': z.union([z.literal("not-in-catalog"), z.literal("denied"), z.literal("version-mismatch"), z.literal("needs-acknowledgement"), z.literal("tarball-integrity")]),
79
+ 'code': z.union([z.literal("not-in-catalog"), z.literal("denied"), z.literal("version-mismatch"), z.literal("needs-acknowledgement"), z.literal("tarball-integrity"), z.literal("ambiguous-identity")]),
74
80
  'detail': z.string(),
75
81
  })])
76
82
  const dsh_plugin_shop_shop_installStatus_parameter_0$schema = z.object({
@@ -155,7 +161,7 @@ export const TYPERT = {
155
161
  typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
156
162
  schema: dsh_plugin_shop_shop_catalog_result$schema,
157
163
  },
158
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":577,"column":9},
164
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":678,"column":9},
159
165
  },
160
166
  {
161
167
  id: 'dsh-plugin-shop#shop/installed',
@@ -170,7 +176,7 @@ export const TYPERT = {
170
176
  typeSymbol: 'dsh-plugin-shop#shop/installed:result',
171
177
  schema: dsh_plugin_shop_shop_installed_result$schema,
172
178
  },
173
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":763,"column":9},
179
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":863,"column":9},
174
180
  },
175
181
  {
176
182
  id: 'dsh-plugin-shop#shop/installStart',
@@ -196,7 +202,7 @@ export const TYPERT = {
196
202
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
197
203
  schema: dsh_plugin_shop_shop_installStart_result$schema,
198
204
  },
199
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":628,"column":9},
205
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":726,"column":9},
200
206
  },
201
207
  {
202
208
  id: 'dsh-plugin-shop#shop/installStatus',
@@ -221,7 +227,7 @@ export const TYPERT = {
221
227
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
222
228
  schema: dsh_plugin_shop_shop_installStatus_result$schema,
223
229
  },
224
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":752,"column":3},
230
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":852,"column":3},
225
231
  },
226
232
  {
227
233
  id: 'dsh-plugin-shop#shop/restart',
@@ -236,7 +242,7 @@ export const TYPERT = {
236
242
  typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
237
243
  schema: dsh_plugin_shop_shop_restart_result$schema,
238
244
  },
239
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":913,"column":9},
245
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":1021,"column":9},
240
246
  },
241
247
  {
242
248
  id: 'dsh-plugin-shop#shop/setEnabled',
@@ -261,7 +267,7 @@ export const TYPERT = {
261
267
  typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
262
268
  schema: dsh_plugin_shop_shop_setEnabled_result$schema,
263
269
  },
264
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":469,"column":9},
270
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":535,"column":9},
265
271
  },
266
272
  {
267
273
  id: 'dsh-plugin-shop#shop/uninstallStart',
@@ -287,7 +293,7 @@ export const TYPERT = {
287
293
  typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
288
294
  schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
289
295
  },
290
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":855,"column":9},
296
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":956,"column":9},
291
297
  },
292
298
  {
293
299
  id: 'dsh-plugin-shop#shop/updateStart',
@@ -312,7 +318,7 @@ export const TYPERT = {
312
318
  typeSymbol: 'dsh-plugin-shop/types#ShopUpdateResult',
313
319
  schema: dsh_plugin_shop_shop_updateStart_result$schema,
314
320
  },
315
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":981,"column":9},
321
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":1106,"column":9},
316
322
  },
317
323
  {
318
324
  id: 'dsh-plugin-shop#shop/version',
@@ -327,7 +333,7 @@ export const TYPERT = {
327
333
  typeSymbol: 'dsh-plugin-shop/types#ShopVersionResult',
328
334
  schema: dsh_plugin_shop_shop_version_result$schema,
329
335
  },
330
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":964,"column":9},
336
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":1089,"column":9},
331
337
  },
332
338
  ],
333
339
  model: {
@@ -433,11 +439,11 @@ export const TYPERT = {
433
439
  },
434
440
  {
435
441
  "name": "InstallArgs",
436
- "declaration": "export interface InstallArgs {\n name: string;\n version: string;\n acknowledged?: boolean;\n}"
442
+ "declaration": "export interface InstallArgs {\n name: string;\n version: string;\n acknowledged?: boolean;\n source?: 'npm' | 'github';\n repo?: string;\n subdir?: string;\n}"
437
443
  },
438
444
  {
439
445
  "name": "InstallRejectionCode",
440
- "declaration": "export type InstallRejectionCode = 'not-in-catalog' | 'denied' | 'version-mismatch' | 'needs-acknowledgement' | 'tarball-integrity';"
446
+ "declaration": "export type InstallRejectionCode = 'not-in-catalog' | 'denied' | 'version-mismatch' | 'needs-acknowledgement' | 'tarball-integrity' | 'ambiguous-identity';"
441
447
  },
442
448
  {
443
449
  "name": "InstallState",
@@ -457,7 +463,7 @@ export const TYPERT = {
457
463
  },
458
464
  {
459
465
  "name": "ShopInstalledEntry",
460
- "declaration": "export interface ShopInstalledEntry {\n name: string;\n installed: string;\n latest: string;\n outdated: boolean;\n enabled: boolean;\n}"
466
+ "declaration": "export interface ShopInstalledEntry {\n name: string;\n source: 'npm' | 'github';\n repo?: string;\n subdir?: string;\n installed: string;\n latest: string;\n outdated: boolean;\n enabled: boolean;\n}"
461
467
  },
462
468
  {
463
469
  "name": "ShopInstallResult",
@@ -55,6 +55,9 @@ const dsh_plugin_shop_shop_catalog_result$schema = z.object({
55
55
  })
56
56
  const dsh_plugin_shop_shop_installed_result$schema = z.array(z.object({
57
57
  'name': z.string(),
58
+ 'source': z.union([z.literal("npm"), z.literal("github")]),
59
+ 'repo': z.union([z.undefined(), z.string()]).optional(),
60
+ 'subdir': z.union([z.undefined(), z.string()]).optional(),
58
61
  'installed': z.string(),
59
62
  'latest': z.string(),
60
63
  'outdated': z.boolean(),
@@ -64,13 +67,16 @@ const dsh_plugin_shop_shop_installStart_parameter_0$schema = z.object({
64
67
  'name': z.string(),
65
68
  'version': z.string(),
66
69
  'acknowledged': z.union([z.undefined(), z.literal(false), z.literal(true)]).optional(),
70
+ 'source': z.union([z.undefined(), z.literal("npm"), z.literal("github")]).optional(),
71
+ 'repo': z.union([z.undefined(), z.string()]).optional(),
72
+ 'subdir': z.union([z.undefined(), z.string()]).optional(),
67
73
  })
68
74
  const dsh_plugin_shop_shop_installStart_result$schema = z.union([z.object({
69
75
  'ok': z.literal(true),
70
76
  'installId': z.string(),
71
77
  }), z.object({
72
78
  'ok': z.literal(false),
73
- 'code': z.union([z.literal("not-in-catalog"), z.literal("denied"), z.literal("version-mismatch"), z.literal("needs-acknowledgement"), z.literal("tarball-integrity")]),
79
+ 'code': z.union([z.literal("not-in-catalog"), z.literal("denied"), z.literal("version-mismatch"), z.literal("needs-acknowledgement"), z.literal("tarball-integrity"), z.literal("ambiguous-identity")]),
74
80
  'detail': z.string(),
75
81
  })])
76
82
  const dsh_plugin_shop_shop_installStatus_parameter_0$schema = z.object({
@@ -152,7 +158,7 @@ export const TYPERT_REMOTE = {
152
158
  typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
153
159
  schema: dsh_plugin_shop_shop_catalog_result$schema,
154
160
  },
155
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":577,"column":9},
161
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":678,"column":9},
156
162
  },
157
163
  {
158
164
  id: 'dsh-plugin-shop#shop/installed',
@@ -167,7 +173,7 @@ export const TYPERT_REMOTE = {
167
173
  typeSymbol: 'dsh-plugin-shop#shop/installed:result',
168
174
  schema: dsh_plugin_shop_shop_installed_result$schema,
169
175
  },
170
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":763,"column":9},
176
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":863,"column":9},
171
177
  },
172
178
  {
173
179
  id: 'dsh-plugin-shop#shop/installStart',
@@ -193,7 +199,7 @@ export const TYPERT_REMOTE = {
193
199
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
194
200
  schema: dsh_plugin_shop_shop_installStart_result$schema,
195
201
  },
196
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":628,"column":9},
202
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":726,"column":9},
197
203
  },
198
204
  {
199
205
  id: 'dsh-plugin-shop#shop/installStatus',
@@ -218,7 +224,7 @@ export const TYPERT_REMOTE = {
218
224
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
219
225
  schema: dsh_plugin_shop_shop_installStatus_result$schema,
220
226
  },
221
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":752,"column":3},
227
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":852,"column":3},
222
228
  },
223
229
  {
224
230
  id: 'dsh-plugin-shop#shop/restart',
@@ -233,7 +239,7 @@ export const TYPERT_REMOTE = {
233
239
  typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
234
240
  schema: dsh_plugin_shop_shop_restart_result$schema,
235
241
  },
236
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":913,"column":9},
242
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":1021,"column":9},
237
243
  },
238
244
  {
239
245
  id: 'dsh-plugin-shop#shop/setEnabled',
@@ -258,7 +264,7 @@ export const TYPERT_REMOTE = {
258
264
  typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
259
265
  schema: dsh_plugin_shop_shop_setEnabled_result$schema,
260
266
  },
261
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":469,"column":9},
267
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":535,"column":9},
262
268
  },
263
269
  {
264
270
  id: 'dsh-plugin-shop#shop/uninstallStart',
@@ -284,7 +290,7 @@ export const TYPERT_REMOTE = {
284
290
  typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
285
291
  schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
286
292
  },
287
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":855,"column":9},
293
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":956,"column":9},
288
294
  },
289
295
  {
290
296
  id: 'dsh-plugin-shop#shop/updateStart',
@@ -309,7 +315,7 @@ export const TYPERT_REMOTE = {
309
315
  typeSymbol: 'dsh-plugin-shop/types#ShopUpdateResult',
310
316
  schema: dsh_plugin_shop_shop_updateStart_result$schema,
311
317
  },
312
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":981,"column":9},
318
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":1106,"column":9},
313
319
  },
314
320
  {
315
321
  id: 'dsh-plugin-shop#shop/version',
@@ -324,7 +330,7 @@ export const TYPERT_REMOTE = {
324
330
  typeSymbol: 'dsh-plugin-shop/types#ShopVersionResult',
325
331
  schema: dsh_plugin_shop_shop_version_result$schema,
326
332
  },
327
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":964,"column":9},
333
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":1089,"column":9},
328
334
  },
329
335
  ],
330
336
  }
@@ -15,6 +15,9 @@ declare module '@deepseek-ai/dsh-client-ui-slots' {
15
15
  }
16
16
  /** Dictionary namespace owned by this plugin. */
17
17
  export declare const NS = "settings.shop";
18
+ /** How long a stashed catalog may satisfy a plain open. The host applies the
19
+ * same five-minute freshness window, so the client stash must not outlive it. */
20
+ export declare const WARM_TTL_MS: number;
18
21
  /** Services required by the tab registration and the Remote mount.
19
22
  *
20
23
  * `remote.shop` is deliberately ABSENT: this package both mounts its own
@@ -57,6 +57,7 @@ export declare const zh: {
57
57
  versionMismatchCode: string;
58
58
  needsAcknowledgementCode: string;
59
59
  tarballIntegrityCode: string;
60
+ ambiguousIdentityCode: string;
60
61
  npmSource: string;
61
62
  githubSource: string;
62
63
  installedSection: string;
@@ -148,6 +149,7 @@ export declare const en: {
148
149
  versionMismatchCode: string;
149
150
  needsAcknowledgementCode: string;
150
151
  tarballIntegrityCode: string;
152
+ ambiguousIdentityCode: string;
151
153
  npmSource: string;
152
154
  githubSource: string;
153
155
  installedSection: string;
@@ -2,6 +2,7 @@
2
2
  * timers, no I/O: every function here maps a value to a value, so fixtures
3
3
  * drive all of it. */
4
4
  export { isShopLike } from '../shared/shop-like.ts';
5
+ export { identityKey, installedSpecMatches, parseRepoSpec, type EntryIdentity } from '../shared/identity.ts';
5
6
  import type { ShopLocaleKey } from './locales.ts';
6
7
  import type { CatalogEntry, HotRestartReason, InstallRejectionCode } from '../host/index.ts';
7
8
  /** Hot-mount reason code → locale key, so the notice reads in the language
@@ -12,8 +13,9 @@ export declare function restartReasonKey(reason: HotRestartReason | undefined):
12
13
  export declare function tierKey(tier: CatalogEntry['tier']): ShopLocaleKey;
13
14
  /** The peers the Host said this installation does not provide, or none when it
14
15
  * said nothing — a plugin that runs here and one the Host could not judge are
15
- * both rendered as no warning at all. */
16
- export declare function missingPeersOf(incompatible: Record<string, string[]>, name: string): string[];
16
+ * both rendered as no warning at all. The map is keyed by install identity,
17
+ * never by name, because two entries can share a name. */
18
+ export declare function missingPeersOf(incompatible: Record<string, string[]>, key: string): string[];
17
19
  /** Spec §9.3 verbatim — the community-tier acknowledgement. The zh dictionary
18
20
  * states the same facts in its own register; never soften this wording. */
19
21
  export declare const ACKNOWLEDGEMENT_EN = "Once installed, this plugin holds the same privileges as a built-in one: reading and writing your files, running shell commands, and reading and modifying the requests sent to the model. It has not been reviewed.";
@@ -21,3 +21,21 @@ export declare function usePollStatus(view: InstallView, setView: Dispatch<SetSt
21
21
  export declare function useInstall(install: (args: InstallArgs) => Promise<ShopInstallResult>, installStatus: (args: {
22
22
  installId: string;
23
23
  }) => Promise<ShopInstallStatusResult>): UseInstallResult;
24
+ /** One entry's install flow, as the tab hands it to a panel. */
25
+ export interface InstallFlow {
26
+ view: InstallView;
27
+ start: (args: InstallArgs) => Promise<void>;
28
+ reset: () => void;
29
+ }
30
+ export interface UseInstallFlows {
31
+ /** Two panels asking for the same install identity receive one flow. */
32
+ flowFor: (key: string) => InstallFlow;
33
+ }
34
+ /**
35
+ * Install flows owned by the tab and keyed by install identity. Keeping the
36
+ * state above individual cards preserves a running operation when filtering
37
+ * unmounts its card, and makes the shelf and Outdated panels agree.
38
+ */
39
+ export declare function useInstallFlows(install: (args: InstallArgs) => Promise<ShopInstallResult>, installStatus: (args: {
40
+ installId: string;
41
+ }) => Promise<ShopInstallStatusResult>, onSettled?: (key: string) => void): UseInstallFlows;
@@ -45,6 +45,22 @@ export declare function installFailureDetail(profile: string, log: readonly stri
45
45
  * 2026-09-02 as "Update failed / dsh not found on PATH" on a working install.
46
46
  */
47
47
  export declare function spawnFailureDetail(code: string | undefined, message: string, dshBin: string, platform: NodeJS.Platform): string;
48
+ /** The failure detail for a command stopped by the shop's deadline. */
49
+ export declare function installTimeoutDetail(profile: string, timeoutMs: number): string;
50
+ /** Kill primitives injected by tests so both platform branches are testable. */
51
+ export interface KillFns {
52
+ killGroup: (pid: number) => void;
53
+ killPid: (pid: number) => void;
54
+ taskkill: (pid: number) => void;
55
+ }
56
+ /** Kill a child and its descendants (process group on POSIX, taskkill tree on Windows). */
57
+ export declare function killTree(pid: number | undefined, platform: NodeJS.Platform, kills?: KillFns): void;
58
+ /** One stream's line assembler, retaining partial UTF-8 and text lines. */
59
+ export interface LineSink {
60
+ write: (chunk: Buffer) => void;
61
+ flush: () => void;
62
+ }
63
+ export declare function lineSink(emit: (line: string) => void): LineSink;
48
64
  /**
49
65
  * Run one `dsh plugin --profile <profile> add <spec>` and track it.
50
66
  * When `expectedName` is given, a zero exit is confirmed against the profile
@@ -62,6 +78,7 @@ export declare function startInstall(options: {
62
78
  restartReason?: HotRestartReason;
63
79
  } | void>;
64
80
  onStatus?: (status: InstallStatus) => void;
81
+ timeoutMs?: number;
65
82
  }): RunningInstall;
66
83
  /**
67
84
  * Run one `dsh plugin --profile <profile> remove <name>` and track it.
@@ -81,5 +98,6 @@ export declare function startUninstall(options: {
81
98
  restartReason?: HotRestartReason;
82
99
  } | void>;
83
100
  onStatus?: (status: InstallStatus) => void;
101
+ timeoutMs?: number;
84
102
  }): RunningInstall;
85
103
  export {};
@@ -91,6 +91,11 @@ export interface HotFs {
91
91
  * nothing in the patch is evaluated. Pure: string in, rows out.
92
92
  */
93
93
  export declare function parseSimplePatch(patchText: string): HotRow[] | null;
94
+ /** Render the accepted rows with prefixed ids — the exact input file the
95
+ * Include tree mounts. Only values the line scan accepted are emitted, and
96
+ * the output is itself a simple patch (round-trips through
97
+ * parseSimplePatch). */
98
+ export declare function renderRows(rows: HotRow[], prefix: string): string;
94
99
  /**
95
100
  * Mount one installed package into the running composition through an
96
101
  * Include subtree: read its bundle patch, replicate the simple rows under
@@ -8,7 +8,7 @@ import { type InstallStatus } from './executor.ts';
8
8
  import { hotMount, hotUnmount } from './hot.ts';
9
9
  import { type RestartOutcome } from './restart.ts';
10
10
  import { type RepoPinFs } from './repo-pins.ts';
11
- import { type PeerResolver } from './peers.ts';
11
+ import { type PeerResolver, type PeerVersionResolver } from './peers.ts';
12
12
  export type { InstallArgs, InstallRejectionCode } from './install.ts';
13
13
  export type { HotRestartReason } from './hot.ts';
14
14
  export type { CatalogEntry } from './types.ts';
@@ -66,6 +66,9 @@ export interface ShopGatewayOptions {
66
66
  /** The dsh argv this process was launched with, for `shop/restart`;
67
67
  * defaults to the real `process.argv` minus node and the script path. */
68
68
  restartArgv?: string[];
69
+ /** The JS entry `shop/restart` re-runs; defaults to `process.argv[1]`, the
70
+ * script this dsh was started with. */
71
+ restartScript?: string;
69
72
  /** Test-only injection: the exit the restart calls after the response is
70
73
  * delivered. Production uses `process.exit`. */
71
74
  exit?: (code?: number) => void;
@@ -97,6 +100,12 @@ export interface ShopGatewayOptions {
97
100
  /** Test-only injection: answers whether a peer resolves. Production builds
98
101
  * one from the profile anchor. */
99
102
  resolvePeer?: PeerResolver;
103
+ /** Test-only injection: answers which version a peer resolves at, for the
104
+ * load-time self-check. Production builds one from the profile anchor. */
105
+ resolvePeerVersion?: PeerVersionResolver;
106
+ /** Test-only injection: the peer ranges the self-check judges against;
107
+ * production reads them from the shipped package.json. */
108
+ peerRanges?: Record<string, string>;
100
109
  }
101
110
  /** `shop/installStart` result (§7.3): rejections are typed wire values with an
102
111
  * author-readable `detail`, not thrown RPC errors. */
@@ -152,14 +161,15 @@ export type ShopUpdateResult = {
152
161
  ok: false;
153
162
  detail: string;
154
163
  };
155
- /** `shop/installed` entry (§7.3): one installed catalog plugin. `installed`
156
- * is the profile manifest's dependency spec verbatim (a range, a tag, or
157
- * `workspace:*`) — for a github entry it is the pinned commit the shop
158
- * installed, falling back to the `github:owner/slug` spec when the pin is
159
- * unknown; `outdated` is the Host's verdict that the installed version sits
160
- * behind the catalog's — the client never does version math. */
164
+ /** `shop/installed` entry (§7.3): one installed catalog plugin. The identity
165
+ * fields distinguish same-named npm and GitHub rows; `installed` remains the
166
+ * manifest spec or the recorded GitHub pin, and the client never does version
167
+ * math. */
161
168
  export interface ShopInstalledEntry {
162
169
  name: string;
170
+ source: 'npm' | 'github';
171
+ repo?: string;
172
+ subdir?: string;
163
173
  installed: string;
164
174
  latest: string;
165
175
  outdated: boolean;
@@ -192,9 +202,10 @@ export interface ShopCatalogResult {
192
202
  /** GitHub star counts by package name; {} when the pointer names no sidecar
193
203
  * or the sidecar could not be fetched/verified (§5). */
194
204
  stars: Record<string, number>;
195
- /** Package name → the declared peers this installation does not provide
196
- * (design 2026-09-01). A name is absent when the plugin runs here or when
197
- * no verdict could be formed; the client renders nothing for both. */
205
+ /** Install identity (`npm:<name>` / `github:<repo>#<subdir>`) → the declared
206
+ * peers this installation does not provide. A key is absent when the plugin
207
+ * runs here or when no verdict could be formed; same-named entries stay
208
+ * independent. */
198
209
  incompatible: Record<string, string[]>;
199
210
  }
200
211
  /** Remote-only service exposing the shop Remote methods of §7.3.
@@ -213,6 +224,8 @@ export declare class ShopGateway extends TypertRemoteService {
213
224
  /** The argv `shop/restart` re-spawns: the real process argv minus node and
214
225
  * the CLI script path, or a test-provided substitute. */
215
226
  private readonly restartArgv;
227
+ /** The script the restart re-runs — this process's own entry. */
228
+ private readonly restartScript;
216
229
  /** The exit the restart calls once the response is out; `process.exit` in
217
230
  * production, a spy in tests. */
218
231
  private readonly exit;
@@ -242,9 +255,14 @@ export declare class ShopGateway extends TypertRemoteService {
242
255
  /** The install gate runs against the last loaded snapshot, never a fresh
243
256
  * fetch per request (§7.2: the Host's cached snapshot is the truth). */
244
257
  private lastSnapshot;
258
+ /** A cold-cache catalog load shared by concurrent RPC calls. */
259
+ private inFlightLoad;
245
260
  /** The origin list built for the last-seen `catalogUrl`, memoised so the
246
261
  * user's npmrc is read at most once per gateway (see `originsFor`). */
247
262
  private originCache;
263
+ /** The user's own `registry=` from `~/.npmrc`, read at most once per
264
+ * gateway. A wrapper distinguishes a genuine `null` from an unread value. */
265
+ private npmRegistryCache;
248
266
  /** The incompatibility map already computed for `lastSnapshot`, keyed by
249
267
  * that snapshot's own object identity. Design §3 asks for the verdict
250
268
  * once per loaded snapshot, not once per RPC call: `loadCatalog` serves
@@ -260,6 +278,13 @@ export declare class ShopGateway extends TypertRemoteService {
260
278
  /** Every install id in insertion order, oldest first; finished-record eviction walks this from the front. */
261
279
  private readonly installOrder;
262
280
  constructor(ctx: Context, options?: ShopGatewayOptions);
281
+ /**
282
+ * Warn once, at load, about any declared peer the harness provides outside
283
+ * its declared range. Advisory by construction: it cannot throw, and it
284
+ * never refuses the load — losing the whole shop is a worse outcome than
285
+ * running against a harness that moved.
286
+ */
287
+ private checkPeerVersions;
263
288
  /** The pins file lives in the shop's own cache, next to the catalog cache. */
264
289
  private pinsPath;
265
290
  /** The boot's Loader root directory (the active profile's `cordis.yml`
@@ -301,10 +326,17 @@ export declare class ShopGateway extends TypertRemoteService {
301
326
  enabled: boolean;
302
327
  }): Promise<ShopSetEnabledResult>;
303
328
  private rowConfig;
329
+ /** The user's own registry, read once. The self-update check and catalog
330
+ * race share this preference so neither path repeatedly touches ~/.npmrc. */
331
+ private npmRegistry;
304
332
  /** The origins to race for this row's catalog. Read once per gateway: the
305
333
  * user's npmrc does not change under a running dsh, and re-reading it on
306
334
  * every catalog call would put a filesystem read on the hot path. */
307
335
  private originsFor;
336
+ /** Join an in-flight load; refresh requests always start a new load. */
337
+ private loadCatalogOnce;
338
+ /** Return the current snapshot, loading it once when needed. */
339
+ private snapshotNow;
308
340
  /** The explicit restart override. Only the row's `config:` sub-object is
309
341
  * passed to a plugin — a top-level `allowRestart:` beside `name:` would be
310
342
  * silently ignored by the loader (dsh-market README, #227). */
@@ -326,6 +358,8 @@ export declare class ShopGateway extends TypertRemoteService {
326
358
  * oldest finished ones (insertion order, oldest first). Running records
327
359
  * are never evicted; an id absent from the map reports `found: false`. */
328
360
  private evictFinishedInstalls;
361
+ /** Whether any command this gateway started is still running. */
362
+ private hasRunningCommand;
329
363
  /** Poll one install's progress (§7.2); unknown ids report `found: false`. */
330
364
  installStatus(args: {
331
365
  installId: string;
@@ -1,21 +1,24 @@
1
1
  /** Install gate: the gate rejection paths of §7.2, as a pure function. */
2
2
  import type { CatalogSnapshot } from './catalog.ts';
3
- export type InstallRejectionCode = 'not-in-catalog' | 'denied' | 'version-mismatch' | 'needs-acknowledgement' | 'tarball-integrity';
3
+ import type { CatalogEntry } from './types.ts';
4
+ export type InstallRejectionCode = 'not-in-catalog' | 'denied' | 'version-mismatch' | 'needs-acknowledgement' | 'tarball-integrity' | 'ambiguous-identity';
4
5
  export interface InstallArgs {
5
6
  name: string;
6
7
  version: string;
7
8
  acknowledged?: boolean;
9
+ /** Optional identity fields keep old clients readable while refusing an
10
+ * ambiguous name-only request when the catalog has duplicate names. */
11
+ source?: 'npm' | 'github';
12
+ repo?: string;
13
+ subdir?: string;
8
14
  }
9
15
  export type ValidateResult = {
10
16
  ok: true;
17
+ entry: CatalogEntry;
11
18
  } | {
12
19
  ok: false;
13
20
  code: InstallRejectionCode;
14
21
  detail: string;
15
22
  };
16
- /**
17
- * Decide whether one install request may proceed, against the Host's own
18
- * snapshot (§5.3). The browser sends a name; nothing the browser says about
19
- * the package is trusted.
20
- */
23
+ /** Decide whether an install request may proceed and return the resolved row. */
21
24
  export declare function validateInstall(snapshot: CatalogSnapshot, args: InstallArgs): ValidateResult;
@@ -5,6 +5,10 @@
5
5
  * bytes reach a China-side machine at 12.53 MB/s from npmmirror against
6
6
  * 0.03 MB/s from GitHub Pages. */
7
7
  import { type CatalogOrigin } from './origin.ts';
8
+ /** Maximum compressed package bytes read from a registry. */
9
+ export declare const MAX_PACKAGE_BYTES: number;
10
+ /** Maximum bytes allowed after gzip inflation. */
11
+ export declare const MAX_INFLATED_BYTES: number;
8
12
  /** Normalise to a trailing slash so relative `URL` resolution against a
9
13
  * registry that carries a path — every corporate registry, e.g.
10
14
  * `https://artifactory.corp/api/npm/npm-repo` — keeps that path instead of
@@ -20,7 +20,7 @@ export interface OriginHandle {
20
20
  /** One file named by the pointer, by the pointer's own raw url string.
21
21
  * Callers pass that string verbatim — never a basename, which would strip
22
22
  * a hostile absolute url into a fetchable relative one. */
23
- file: (url: string) => Promise<string>;
23
+ file: (url: string, signal?: AbortSignal) => Promise<string>;
24
24
  }
25
25
  export interface CatalogOrigin {
26
26
  readonly id: string;
@@ -36,5 +36,11 @@ export interface CatalogOrigin {
36
36
  * before the string could be inspected, so only comparing the resolved URL's
37
37
  * origin to the base's closes every spelling class. */
38
38
  export declare function resolveDataUrl(baseUrl: string, url: string): string;
39
+ /** The largest catalog body this host reads into memory. */
40
+ export declare const MAX_BODY_BYTES: number;
41
+ /** Read a response body through a byte cap, converting transport failures. */
42
+ export declare function readCappedBytes(response: Response, label: string, maxBytes?: number): Promise<Buffer>;
43
+ /** Read a capped response body as UTF-8 text. */
44
+ export declare function readCappedText(response: Response, label: string, maxBytes?: number): Promise<string>;
39
45
  /** The transport this project has always used: a static `v1/` tree. */
40
46
  export declare function httpOrigin(baseUrl: string, fetchImpl: typeof fetch): CatalogOrigin;
@@ -1,5 +1,8 @@
1
1
  /** Harness compatibility: which declared peers the running installation does
2
- * not provide (design 2026-09-01-harness-compatibility). */
2
+ * not provide (design 2026-09-01-harness-compatibility), and — for the
3
+ * shop's OWN declared peers — which ones it provides at a version outside
4
+ * the declared range. */
5
+ import { type EntryIdentity } from '../shared/identity.ts';
3
6
  /** Answers "can this installation provide `spec`?" — injected so fixtures
4
7
  * drive every verdict and exactly one call site touches the filesystem. */
5
8
  export type PeerResolver = (spec: string) => boolean;
@@ -19,7 +22,75 @@ export declare function nodeResolver(baseUrl: string): PeerResolver;
19
22
  * never read as an accusation, because one false warning teaches a reader to
20
23
  * ignore every warning.
21
24
  */
22
- export declare function incompatibilityMap(entries: readonly {
23
- name: string;
25
+ export declare function incompatibilityMap(entries: readonly (EntryIdentity & {
24
26
  peers?: string[];
25
- }[], resolve: PeerResolver): Record<string, string[]>;
27
+ })[], resolve: PeerResolver): Record<string, string[]>;
28
+ /**
29
+ * The same question one step further on: which VERSION does this
30
+ * installation provide for `spec`? `null` is the no-verdict signal — the
31
+ * peer is absent, or its manifest could not be read. Injected exactly like
32
+ * `PeerResolver`, so fixtures drive every verdict and only one call site
33
+ * touches the filesystem.
34
+ */
35
+ export type PeerVersionResolver = (spec: string) => string | null;
36
+ /** One declared peer whose provided version is outside its declared range. */
37
+ export interface PeerVersionMismatch {
38
+ spec: string;
39
+ /** The range this build declares in `peerDependencies`. */
40
+ range: string;
41
+ /** The version the installation actually provides. */
42
+ found: string;
43
+ }
44
+ /**
45
+ * The production resolver: `nodeResolver`'s own resolution, kept this time
46
+ * instead of collapsed to a boolean. Anything that leaves no version to read
47
+ * answers null — a peer that is absent, one that restricts `./package.json`
48
+ * in its exports, an unreadable or malformed manifest, a manifest with no
49
+ * `version`. Absence is deliberately NOT reported as a version violation:
50
+ * `incompatibilityMap` is what covers a missing peer.
51
+ */
52
+ export declare function nodeVersionResolver(baseUrl: string): PeerVersionResolver;
53
+ /**
54
+ * The declared peers whose provided version is outside the declared range,
55
+ * ordered by peer name. Pure: the module graph arrives through `resolve`.
56
+ *
57
+ * `includePrerelease` is load-bearing, not a convenience. The harness ships
58
+ * nothing but `-rc` versions, so under strict semver `^0.1.1-rc.2` excludes
59
+ * `0.1.2-rc.1` — the version that is installed and works — and every future
60
+ * rc bump would raise a false alarm. With it on, the range still excludes an
61
+ * older prerelease (`0.1.1-rc.1`) and a minor- or major-line move
62
+ * (`0.2.0-rc.1`, `1.0.0`), which are the moves that actually break a plugin
63
+ * path. Discrimination on both sides is the whole point: one false warning
64
+ * teaches a reader to ignore every warning.
65
+ *
66
+ * Anything unreadable yields NO verdict for that peer and never a violation:
67
+ * a resolver that answers null or throws, a found version that is not
68
+ * semver, a declared range semver cannot parse. The other peers are still
69
+ * judged.
70
+ */
71
+ export declare function peerVersionMismatches(ranges: Readonly<Record<string, string>>, resolve: PeerVersionResolver): PeerVersionMismatch[];
72
+ /**
73
+ * The load-time message for a set of mismatches, or null when there is
74
+ * nothing to say. Every mismatch is named with its declared range and the
75
+ * version found, in ONE message: the reader needs the whole picture in the
76
+ * line they will scroll past, and the message says the shop still loads
77
+ * because refusing to load would cost them the shop over a diagnostic.
78
+ */
79
+ export declare function peerVersionWarning(mismatches: readonly PeerVersionMismatch[]): string | null;
80
+ /** What a load-time check needs: the ranges this build declares, a way to
81
+ * read what the installation provides, and somewhere to say it once. */
82
+ export interface PeerVersionCheckDeps {
83
+ ranges: Readonly<Record<string, string>>;
84
+ resolve: PeerVersionResolver;
85
+ warn: (message: string) => void;
86
+ }
87
+ /**
88
+ * A self-check that warns AT MOST ONCE, however many times it is called.
89
+ * The guard is what keeps a diagnostic a diagnostic: a message repeated per
90
+ * call is noise a reader learns to skip, and the shop's load path is not the
91
+ * only place that could reasonably ask this question.
92
+ *
93
+ * It never throws. A check that could fail a load would be worse than the
94
+ * mismatch it reports — the user would lose the whole shop over a warning.
95
+ */
96
+ export declare function createPeerVersionCheck(deps: PeerVersionCheckDeps): () => void;