dsh-plugin-shop 0.4.1 → 0.4.3

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.js CHANGED
@@ -256,7 +256,10 @@ function validateInstall(snapshot, args) {
256
256
  }
257
257
  //#endregion
258
258
  //#region src/host/executor.ts
259
- /** Install executor: spawn the dsh CLI, stream its output, serialize per profile. */
259
+ /** Plugin-command executor: spawn the dsh CLI, stream its output, serialize
260
+ * per profile. One implementation drives both `dsh plugin add` (install) and
261
+ * `dsh plugin remove` (uninstall); the only differences are the verb and the
262
+ * post-exit manifest confirmation. */
260
263
  const MAX_LOG_LINES = 200;
261
264
  const MAX_LOG_BYTES = 65536;
262
265
  const profileQueues = /* @__PURE__ */ new Map();
@@ -265,8 +268,7 @@ function chain(profile, task) {
265
268
  profileQueues.set(profile, next.catch(() => {}));
266
269
  return next;
267
270
  }
268
- /**
269
- * The §7.2 step-6 confirm: after a zero exit, re-read the profile manifest and
271
+ /** The §7.2 step-6 confirm: after a zero exit, re-read the profile manifest and
270
272
  * verify the bundle actually landed in `dsh.profile.bundles`. Exit 0 alone is
271
273
  * not success — a library-that-looked-like-a-plugin, or a stale catalog,
272
274
  * exits 0 while changing nothing (§10). The shop cannot force a client
@@ -285,18 +287,31 @@ function confirmBundleActivation(profile, home, expectedName) {
285
287
  return `installed but the profile manifest could not be read (${join(profileDir, "package.json")}) — the catalog may be stale; refresh it`;
286
288
  }
287
289
  }
288
- /**
289
- * Run one `dsh plugin --profile <profile> add <spec>` and track it.
290
+ /** The uninstall mirror of §7.2 step 6: after a zero exit, re-read the profile
291
+ * manifest and verify the bundle actually left `dsh.profile.bundles`. A zero
292
+ * exit that changed nothing must not read as success. */
293
+ function confirmBundleRemoval(profile, home, expectedName) {
294
+ const profileDir = resolveProfileDir(profile, home);
295
+ try {
296
+ if (!readProfileManifest("dsh-plugin-shop", profileDir).dsh?.profile?.bundles?.includes(expectedName)) return null;
297
+ return "removed but dsh.profile.bundles did not change — re-run the uninstall";
298
+ } catch {
299
+ return `removed but the profile manifest could not be read (${join(profileDir, "package.json")}) — re-run the uninstall`;
300
+ }
301
+ }
302
+ /** Run one `dsh plugin --profile <profile> <verb> <target>` and track it.
290
303
  * Never rolls back; a failure surfaces stderr verbatim plus the recovery hint
291
304
  * (§10). The shop never passes build-script flags: `allowBuilds` stays the
292
305
  * user's explicit decision in the CLI (§7.2).
293
306
  * The child inherits the current environment unless `env` is given — the
294
307
  * real-install test pins DSH_HOME to a temporary directory this way.
295
- * When `expectedName` is given, a zero exit is confirmed against the profile
296
- * manifest (§7.2 step 6) before the install reports `done`.
297
- */
298
- function startInstall(options) {
299
- const { profile, spec, dshBin = "dsh", env, expectedName, onStatus } = options;
308
+ * When `confirm` is given, a zero exit is checked against the profile
309
+ * manifest before the command reports `done` (§7.2 step 6 and its uninstall
310
+ * mirror). */
311
+ function spawnPluginCli(options) {
312
+ const { profile, argv, dshBin, env, confirm, onStatus } = options;
313
+ const target = argv[1];
314
+ if (target === void 0 || target.startsWith("-")) throw new Error(`dsh-plugin-shop: refusing to spawn with a flag-like operand: ${target ?? "(none)"}`);
300
315
  const installId = randomUUID();
301
316
  const log = [];
302
317
  let logBytes = 0;
@@ -325,8 +340,7 @@ function startInstall(options) {
325
340
  "plugin",
326
341
  "--profile",
327
342
  profile,
328
- "add",
329
- spec
343
+ ...argv
330
344
  ], {
331
345
  stdio: [
332
346
  "ignore",
@@ -352,8 +366,8 @@ function startInstall(options) {
352
366
  if (state !== "running") return;
353
367
  if (exitCode === 0) {
354
368
  state = "done";
355
- if (expectedName !== void 0) {
356
- const confirmDetail = confirmBundleActivation(profile, env?.DSH_HOME, expectedName);
369
+ if (confirm !== void 0) {
370
+ const confirmDetail = confirm(env?.DSH_HOME);
357
371
  if (confirmDetail !== null) {
358
372
  state = "failed";
359
373
  detail = confirmDetail;
@@ -370,6 +384,118 @@ function startInstall(options) {
370
384
  }))
371
385
  };
372
386
  }
387
+ /**
388
+ * Run one `dsh plugin --profile <profile> add <spec>` and track it.
389
+ * When `expectedName` is given, a zero exit is confirmed against the profile
390
+ * manifest (§7.2 step 6) before the install reports `done`.
391
+ */
392
+ function startInstall(options) {
393
+ const { profile, spec, dshBin = "dsh", env, expectedName, onStatus } = options;
394
+ return spawnPluginCli({
395
+ profile,
396
+ argv: ["add", spec],
397
+ dshBin,
398
+ env,
399
+ confirm: expectedName !== void 0 ? (home) => confirmBundleActivation(profile, home, expectedName) : void 0,
400
+ onStatus
401
+ });
402
+ }
403
+ /**
404
+ * Run one `dsh plugin --profile <profile> remove <name>` and track it.
405
+ * When `expectedName` is given, a zero exit is confirmed against the profile
406
+ * manifest — the bundle must actually have LEFT `dsh.profile.bundles` — before
407
+ * the uninstall reports `done`.
408
+ */
409
+ function startUninstall(options) {
410
+ const { profile, name, dshBin = "dsh", env, expectedName, onStatus } = options;
411
+ return spawnPluginCli({
412
+ profile,
413
+ argv: ["remove", name],
414
+ dshBin,
415
+ env,
416
+ confirm: expectedName !== void 0 ? (home) => confirmBundleRemoval(profile, home, expectedName) : void 0,
417
+ onStatus
418
+ });
419
+ }
420
+ //#endregion
421
+ //#region src/host/restart.ts
422
+ /** Restart executor: re-spawn the Host's own command line and hand the
423
+ * browser the new server's URL. The child is detached (its own process
424
+ * group), so it survives both the parent's exit and the launching
425
+ * terminal's. The parent exits only AFTER the child printed its
426
+ * `dsh web: <url>` line — a restart that fails to come up leaves the old
427
+ * process running untouched. */
428
+ /** How long to wait for the child to print its URL before declaring the
429
+ * restart failed and killing the child. dsh web prints the URL once the
430
+ * Loader tree settles; 20s is generous on slow machines. */
431
+ const RESTART_UP_TIMEOUT_MS = 2e4;
432
+ /** Spawn the restarted server and wait for it to announce its URL.
433
+ *
434
+ * The child runs the same `dsh` with the same argv the current process was
435
+ * launched with (`process.argv.slice(2)` — node and the CLI script path
436
+ * stripped), so the profile, port and flags reproduce the user's launch
437
+ * verbatim. `--port 0` therefore yields a NEW port, and the returned URL is
438
+ * how the browser finds it.
439
+ *
440
+ * On success the caller exits the old process — but only after delivering
441
+ * the RPC response carrying `url`, which is the caller's sequencing duty,
442
+ * not this module's. On failure (child exits before announcing, spawn
443
+ * error, or the timeout) the child is killed if still running and the old
444
+ * process is untouched. */
445
+ function startRestart(options) {
446
+ const { dshBin, argv, env, timeoutMs = RESTART_UP_TIMEOUT_MS } = options;
447
+ const stderr = [];
448
+ return new Promise((resolve) => {
449
+ const child = spawn(dshBin, argv, {
450
+ stdio: [
451
+ "ignore",
452
+ "pipe",
453
+ "pipe"
454
+ ],
455
+ env: env ?? process.env,
456
+ detached: true
457
+ });
458
+ child.unref();
459
+ const timeout = setTimeout(() => {
460
+ child.kill();
461
+ resolve({
462
+ ok: false,
463
+ detail: "the restarted server did not announce its URL in time"
464
+ });
465
+ }, timeoutMs);
466
+ child.stdout.on("data", (chunk) => {
467
+ for (const line of chunk.toString().split("\n")) {
468
+ if (line === "") continue;
469
+ const match = /dsh web: (http:\/\/\S+)/.exec(line);
470
+ if (match?.[1] !== void 0) {
471
+ clearTimeout(timeout);
472
+ resolve({
473
+ ok: true,
474
+ url: match[1]
475
+ });
476
+ }
477
+ }
478
+ });
479
+ child.stderr.on("data", (chunk) => {
480
+ for (const line of chunk.toString().split("\n")) if (line !== "") stderr.push(line);
481
+ });
482
+ child.on("error", (error) => {
483
+ const code = error.code;
484
+ clearTimeout(timeout);
485
+ resolve({
486
+ ok: false,
487
+ detail: code === "ENOENT" ? "dsh not found on PATH — restart could not be launched" : `restart spawn failed: ${error.message}`
488
+ });
489
+ });
490
+ child.on("close", () => {
491
+ clearTimeout(timeout);
492
+ resolve({
493
+ ok: false,
494
+ detail: `the restarted server exited during boot — ${stderr[stderr.length - 1] ?? "no output"}`
495
+ });
496
+ });
497
+ });
498
+ }
373
499
  //#endregion
374
500
  //#region src/host/profile.ts
375
501
  /** Profile directory discovery and user-layer writes (§8: hot enable/disable). */
@@ -494,7 +620,9 @@ let ShopGateway = (() => {
494
620
  let _catalog_decorators;
495
621
  let _install_decorators;
496
622
  let _installStatus_decorators;
497
- let _outdated_decorators;
623
+ let _installed_decorators;
624
+ let _uninstall_decorators;
625
+ let _restart_decorators;
498
626
  return class ShopGateway extends _classSuper {
499
627
  static {
500
628
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
@@ -502,7 +630,9 @@ let ShopGateway = (() => {
502
630
  _catalog_decorators = [Remote("catalog")];
503
631
  _install_decorators = [Remote("installStart")];
504
632
  _installStatus_decorators = [Remote("installStatus")];
505
- _outdated_decorators = [Remote("outdated")];
633
+ _installed_decorators = [Remote("installed")];
634
+ _uninstall_decorators = [Remote("uninstallStart")];
635
+ _restart_decorators = [Remote("restart")];
506
636
  __esDecorate(this, null, _setEnabled_decorators, {
507
637
  kind: "method",
508
638
  name: "setEnabled",
@@ -547,14 +677,36 @@ let ShopGateway = (() => {
547
677
  },
548
678
  metadata: _metadata
549
679
  }, null, _instanceExtraInitializers);
550
- __esDecorate(this, null, _outdated_decorators, {
680
+ __esDecorate(this, null, _installed_decorators, {
681
+ kind: "method",
682
+ name: "installed",
683
+ static: false,
684
+ private: false,
685
+ access: {
686
+ has: (obj) => "installed" in obj,
687
+ get: (obj) => obj.installed
688
+ },
689
+ metadata: _metadata
690
+ }, null, _instanceExtraInitializers);
691
+ __esDecorate(this, null, _uninstall_decorators, {
692
+ kind: "method",
693
+ name: "uninstall",
694
+ static: false,
695
+ private: false,
696
+ access: {
697
+ has: (obj) => "uninstall" in obj,
698
+ get: (obj) => obj.uninstall
699
+ },
700
+ metadata: _metadata
701
+ }, null, _instanceExtraInitializers);
702
+ __esDecorate(this, null, _restart_decorators, {
551
703
  kind: "method",
552
- name: "outdated",
704
+ name: "restart",
553
705
  static: false,
554
706
  private: false,
555
707
  access: {
556
- has: (obj) => "outdated" in obj,
557
- get: (obj) => obj.outdated
708
+ has: (obj) => "restart" in obj,
709
+ get: (obj) => obj.restart
558
710
  },
559
711
  metadata: _metadata
560
712
  }, null, _instanceExtraInitializers);
@@ -572,11 +724,21 @@ let ShopGateway = (() => {
572
724
  profileDir;
573
725
  inventory;
574
726
  dshBin;
727
+ /** The argv `shop/restart` re-spawns: the real process argv minus node and
728
+ * the CLI script path, or a test-provided substitute. */
729
+ restartArgv;
730
+ /** The exit the restart calls once the response is out; `process.exit` in
731
+ * production, a spy in tests. */
732
+ exit;
733
+ restartExitDelayMs;
575
734
  /** The install gate runs against the last loaded snapshot, never a fresh
576
735
  * fetch per request (§7.2: the Host's cached snapshot is the truth). */
577
736
  /** Finished install records retained, so a poll sees the true terminal
578
737
  * state (§8: done / needsRestart / failure detail). Oldest evicted on add. */
579
738
  static MAX_FINISHED_INSTALLS = 32;
739
+ /** How long the gateway waits after a successful restart response before
740
+ * exiting the old process — the browser must receive the URL first. */
741
+ static RESTART_EXIT_DELAY_MS = 2e3;
580
742
  /** The install gate runs against the last loaded snapshot, never a fresh
581
743
  * fetch per request (§7.2: the Host's cached snapshot is the truth). */
582
744
  lastSnapshot = null;
@@ -591,6 +753,9 @@ let ShopGateway = (() => {
591
753
  this.profileDir = options.profileDir;
592
754
  this.inventory = options.inventory;
593
755
  this.dshBin = options.dshBin ?? "dsh";
756
+ this.restartArgv = options.restartArgv ?? process.argv.slice(2);
757
+ this.exit = options.exit ?? ((code) => process.exit(code));
758
+ this.restartExitDelayMs = options.restartExitDelayMs ?? ShopGateway.RESTART_EXIT_DELAY_MS;
594
759
  }
595
760
  /** The boot's Loader root directory (the active profile's `cordis.yml`
596
761
  * directory, carried on `ctx.baseUrl`), when present. A `link:` install
@@ -727,8 +892,11 @@ let ShopGateway = (() => {
727
892
  ...running.status()
728
893
  };
729
894
  }
730
- /** Installed plugins whose installed version is older than the catalog's (§7.3). */
731
- async outdated() {
895
+ /** Installed catalog plugins (§7.3): every entry of the snapshot the profile
896
+ * manifest declares as a dependency, with the Host's `outdated` verdict
897
+ * attached. The tab's shelf cards and its installed section both derive
898
+ * from this one list. */
899
+ async installed() {
732
900
  if (this.lastSnapshot === null) {
733
901
  const { catalogUrl, cacheDir } = this.rowConfig();
734
902
  const { snapshot } = await (this.options.loadCatalog ?? loadCatalog)({
@@ -738,24 +906,84 @@ let ShopGateway = (() => {
738
906
  this.lastSnapshot = snapshot;
739
907
  }
740
908
  const dependencies = readProfileManifest("dsh-plugin-shop", this.profileDirResolved()).dependencies ?? {};
741
- const outdated = [];
909
+ const installed = [];
742
910
  for (const entry of this.lastSnapshot.entries) {
743
- const installed = dependencies[entry.name];
744
- if (installed === void 0) continue;
745
- let floor;
746
- if (installed === entry.version) floor = installed;
747
- else try {
748
- floor = minVersion(installed)?.version ?? null;
749
- } catch {
750
- floor = null;
751
- }
752
- if (floor !== null && lt(floor, entry.version)) outdated.push({
911
+ const spec = dependencies[entry.name];
912
+ if (spec === void 0) continue;
913
+ installed.push({
753
914
  name: entry.name,
754
- installed,
755
- latest: entry.version
915
+ installed: spec,
916
+ latest: entry.version,
917
+ outdated: this.isBehind(spec, entry.version)
756
918
  });
757
919
  }
758
- return outdated;
920
+ return installed;
921
+ }
922
+ /** Whether an installed dependency spec sits behind the catalog's version.
923
+ * A spec identical to the catalog version is current by definition; a
924
+ * pnpm-written non-semver spec like `workspace:*` is not reportable and
925
+ * reads as current rather than killing the RPC. */
926
+ isBehind(spec, latest) {
927
+ if (spec === latest) return false;
928
+ let floor;
929
+ try {
930
+ floor = minVersion(spec)?.version ?? null;
931
+ } catch {
932
+ floor = null;
933
+ }
934
+ return floor !== null && lt(floor, latest);
935
+ }
936
+ /** Uninstall one installed catalog plugin from the profile (§7.3 follow-up
937
+ * amendment). Removing revokes privilege rather than granting it, so there
938
+ * is no acknowledgement gate. The name must be a catalog entry the profile
939
+ * manifest declares as a dependency — the RPC cannot remove profile
940
+ * dependencies the shop does not manage (the base bundle, the shop
941
+ * itself). The same install records/polling serve the client. */
942
+ async uninstall(args) {
943
+ if (this.lastSnapshot === null) {
944
+ const { catalogUrl, cacheDir } = this.rowConfig();
945
+ const { snapshot } = await (this.options.loadCatalog ?? loadCatalog)({
946
+ baseUrl: catalogUrl,
947
+ cacheDir
948
+ });
949
+ this.lastSnapshot = snapshot;
950
+ }
951
+ if (!this.lastSnapshot.entries.some((entry) => entry.name === args.name)) return {
952
+ ok: false,
953
+ detail: `dsh-plugin-shop: ${args.name} is not in the catalog`
954
+ };
955
+ if ((readProfileManifest("dsh-plugin-shop", this.profileDirResolved()).dependencies ?? {})[args.name] === void 0) return {
956
+ ok: false,
957
+ detail: `dsh-plugin-shop: ${args.name} is not installed`
958
+ };
959
+ const running = startUninstall({
960
+ profile: this.profile,
961
+ name: args.name,
962
+ dshBin: this.dshBin,
963
+ expectedName: args.name
964
+ });
965
+ this.installs.set(running.installId, running);
966
+ this.installOrder.push(running.installId);
967
+ this.evictFinishedInstalls();
968
+ return {
969
+ ok: true,
970
+ installId: running.installId
971
+ };
972
+ }
973
+ /** Restart the dsh process the shop runs in (§8 amendment, 2026-08-27):
974
+ * re-spawn this process's own command line, return the new server's URL
975
+ * once it announces itself, and only then exit. A failed restart returns a
976
+ * typed failure and the old process keeps serving — the restart is
977
+ * all-or-nothing. The response must reach the browser before the exit, so
978
+ * the exit is delayed past the RPC round-trip. */
979
+ async restart() {
980
+ const outcome = await startRestart({
981
+ dshBin: this.dshBin,
982
+ argv: this.restartArgv,
983
+ env: process.env
984
+ });
985
+ if (outcome.ok) setTimeout(() => this.exit(0), this.restartExitDelayMs);
986
+ return outcome;
759
987
  }
760
988
  };
761
989
  })();
@@ -38,6 +38,12 @@ const dsh_plugin_shop_shop_catalog_result$schema = z.object({
38
38
  })),
39
39
  'stars': z.record(z.string(), z.number()),
40
40
  })
41
+ const dsh_plugin_shop_shop_installed_result$schema = z.array(z.object({
42
+ 'name': z.string(),
43
+ 'installed': z.string(),
44
+ 'latest': z.string(),
45
+ 'outdated': z.boolean(),
46
+ }))
41
47
  const dsh_plugin_shop_shop_installStart_parameter_0$schema = z.object({
42
48
  'name': z.string(),
43
49
  'version': z.string(),
@@ -61,11 +67,13 @@ const dsh_plugin_shop_shop_installStatus_result$schema = z.object({
61
67
  'needsRestart': z.union([z.undefined(), z.literal(false), z.literal(true)]).optional(),
62
68
  'detail': z.union([z.undefined(), z.string()]).optional(),
63
69
  })
64
- const dsh_plugin_shop_shop_outdated_result$schema = z.array(z.object({
65
- 'name': z.string(),
66
- 'installed': z.string(),
67
- 'latest': z.string(),
68
- }))
70
+ const dsh_plugin_shop_shop_restart_result$schema = z.union([z.object({
71
+ 'ok': z.literal(true),
72
+ 'url': z.string(),
73
+ }), z.object({
74
+ 'ok': z.literal(false),
75
+ 'detail': z.string(),
76
+ })])
69
77
  const dsh_plugin_shop_shop_setEnabled_parameter_0$schema = z.object({
70
78
  'name': z.string(),
71
79
  'enabled': z.boolean(),
@@ -74,6 +82,16 @@ const dsh_plugin_shop_shop_setEnabled_result$schema = z.object({
74
82
  'ok': z.boolean(),
75
83
  'detail': z.union([z.undefined(), z.string()]).optional(),
76
84
  })
85
+ const dsh_plugin_shop_shop_uninstallStart_parameter_0$schema = z.object({
86
+ 'name': z.string(),
87
+ })
88
+ const dsh_plugin_shop_shop_uninstallStart_result$schema = z.union([z.object({
89
+ 'ok': z.literal(true),
90
+ 'installId': z.string(),
91
+ }), z.object({
92
+ 'ok': z.literal(false),
93
+ 'detail': z.string(),
94
+ })])
77
95
 
78
96
  export const TYPERT = {
79
97
  package: 'dsh-plugin-shop',
@@ -105,7 +123,22 @@ export const TYPERT = {
105
123
  typeSymbol: 'dsh-plugin-shop/types#ShopCatalogResult',
106
124
  schema: dsh_plugin_shop_shop_catalog_result$schema,
107
125
  },
108
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":180,"column":9},
126
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":218,"column":9},
127
+ },
128
+ {
129
+ id: 'dsh-plugin-shop#shop/installed',
130
+ service: 'shop',
131
+ namespace: 'shop',
132
+ method: 'installed',
133
+ invocation: { kind: 'direct' },
134
+ parameters: [
135
+ ],
136
+ result: {
137
+ mode: 'strict',
138
+ typeSymbol: 'dsh-plugin-shop#shop/installed:result',
139
+ schema: dsh_plugin_shop_shop_installed_result$schema,
140
+ },
141
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":293,"column":9},
109
142
  },
110
143
  {
111
144
  id: 'dsh-plugin-shop#shop/installStart',
@@ -131,7 +164,7 @@ export const TYPERT = {
131
164
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallResult',
132
165
  schema: dsh_plugin_shop_shop_installStart_result$schema,
133
166
  },
134
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":206,"column":9},
167
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":244,"column":9},
135
168
  },
136
169
  {
137
170
  id: 'dsh-plugin-shop#shop/installStatus',
@@ -156,22 +189,22 @@ export const TYPERT = {
156
189
  typeSymbol: 'dsh-plugin-shop/types#ShopInstallStatusResult',
157
190
  schema: dsh_plugin_shop_shop_installStatus_result$schema,
158
191
  },
159
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":244,"column":3},
192
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":282,"column":3},
160
193
  },
161
194
  {
162
- id: 'dsh-plugin-shop#shop/outdated',
195
+ id: 'dsh-plugin-shop#shop/restart',
163
196
  service: 'shop',
164
197
  namespace: 'shop',
165
- method: 'outdated',
198
+ method: 'restart',
166
199
  invocation: { kind: 'direct' },
167
200
  parameters: [
168
201
  ],
169
202
  result: {
170
203
  mode: 'strict',
171
- typeSymbol: 'dsh-plugin-shop#shop/outdated:result',
172
- schema: dsh_plugin_shop_shop_outdated_result$schema,
204
+ typeSymbol: 'dsh-plugin-shop/types#ShopRestartResult',
205
+ schema: dsh_plugin_shop_shop_restart_result$schema,
173
206
  },
174
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":252,"column":9},
207
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":367,"column":9},
175
208
  },
176
209
  {
177
210
  id: 'dsh-plugin-shop#shop/setEnabled',
@@ -196,7 +229,33 @@ export const TYPERT = {
196
229
  typeSymbol: 'dsh-plugin-shop/types#ShopSetEnabledResult',
197
230
  schema: dsh_plugin_shop_shop_setEnabled_result$schema,
198
231
  },
199
- sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":149,"column":3},
232
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":187,"column":3},
233
+ },
234
+ {
235
+ id: 'dsh-plugin-shop#shop/uninstallStart',
236
+ service: 'shop',
237
+ namespace: 'shop',
238
+ method: 'uninstallStart',
239
+ implementation: 'uninstall',
240
+ invocation: { kind: 'direct' },
241
+ parameters: [
242
+ {
243
+ name: 'args',
244
+ wire: 'args',
245
+ source: 'json',
246
+ codec: {
247
+ mode: 'strict',
248
+ typeSymbol: 'dsh-plugin-shop#shop/uninstallStart:args',
249
+ schema: dsh_plugin_shop_shop_uninstallStart_parameter_0$schema,
250
+ },
251
+ },
252
+ ],
253
+ result: {
254
+ mode: 'strict',
255
+ typeSymbol: 'dsh-plugin-shop/types#ShopUninstallResult',
256
+ schema: dsh_plugin_shop_shop_uninstallStart_result$schema,
257
+ },
258
+ sourceLocation: {"file":"packages/dsh-plugin-shop/src/host/index.ts","line":333,"column":9},
200
259
  },
201
260
  ],
202
261
  model: {
@@ -245,10 +304,24 @@ export const TYPERT = {
245
304
  },
246
305
  {
247
306
  "kind": "method",
248
- "name": "outdated",
249
- "signature": "@Remote('outdated') async outdated(): Promise<ShopOutdatedEntry[]>",
250
- "summary": "Installed plugins whose installed version is older than the catalog's (§7.3).",
251
- "jsDoc": "/** Installed plugins whose installed version is older than the catalog's (§7.3). */"
307
+ "name": "installed",
308
+ "signature": "@Remote('installed') async installed(): Promise<ShopInstalledEntry[]>",
309
+ "summary": "Installed catalog plugins (§7.3): every entry of the snapshot the profile manifest declares as a dependency, with the Host's `outdated` verdict attached.",
310
+ "jsDoc": "/** Installed catalog plugins (§7.3): every entry of the snapshot the profile\n * manifest declares as a dependency, with the Host's `outdated` verdict\n * attached. The tab's shelf cards and its installed section both derive\n * from this one list. */"
311
+ },
312
+ {
313
+ "kind": "method",
314
+ "name": "uninstall",
315
+ "signature": "@Remote('uninstallStart') async uninstall(args: { name: string }): Promise<ShopUninstallResult>",
316
+ "summary": "Uninstall one installed catalog plugin from the profile (§7.3 follow-up amendment).",
317
+ "jsDoc": "/** Uninstall one installed catalog plugin from the profile (§7.3 follow-up\n * amendment). Removing revokes privilege rather than granting it, so there\n * is no acknowledgement gate. The name must be a catalog entry the profile\n * manifest declares as a dependency — the RPC cannot remove profile\n * dependencies the shop does not manage (the base bundle, the shop\n * itself). The same install records/polling serve the client. */"
318
+ },
319
+ {
320
+ "kind": "method",
321
+ "name": "restart",
322
+ "signature": "@Remote('restart') async restart(): Promise<ShopRestartResult>",
323
+ "summary": "Restart the dsh process the shop runs in (§8 amendment, 2026-08-27): re-spawn this process's own command line, return the new server's URL once it announces itself, and only then exit.",
324
+ "jsDoc": "/** Restart the dsh process the shop runs in (§8 amendment, 2026-08-27):\n * re-spawn this process's own command line, return the new server's URL\n * once it announces itself, and only then exit. A failed restart returns a\n * typed failure and the old process keeps serving — the restart is\n * all-or-nothing. The response must reach the browser before the exit, so\n * the exit is delayed past the RPC round-trip. */"
252
325
  }
253
326
  ],
254
327
  "types": [
@@ -284,10 +357,18 @@ export const TYPERT = {
284
357
  "name": "InstallStatus",
285
358
  "declaration": "export interface InstallStatus {\n state: InstallState;\n log: string[];\n needsRestart?: boolean;\n detail?: string;\n}"
286
359
  },
360
+ {
361
+ "name": "RestartOutcome",
362
+ "declaration": "export type RestartOutcome = { ok: true; url: string; } | { ok: false; detail: string; };"
363
+ },
287
364
  {
288
365
  "name": "ShopCatalogResult",
289
366
  "declaration": "export interface ShopCatalogResult {\n schemaVersion: number;\n builtAt: string;\n stale: boolean;\n plugins: CatalogEntry[];\n denied: DeniedEntry[];\n stars: Record<string, number>;\n}"
290
367
  },
368
+ {
369
+ "name": "ShopInstalledEntry",
370
+ "declaration": "export interface ShopInstalledEntry {\n name: string;\n installed: string;\n latest: string;\n outdated: boolean;\n}"
371
+ },
291
372
  {
292
373
  "name": "ShopInstallResult",
293
374
  "declaration": "export type ShopInstallResult = { ok: true; installId: string; } | { ok: false; code: InstallRejectionCode; detail: string; };"
@@ -297,12 +378,16 @@ export const TYPERT = {
297
378
  "declaration": "export interface ShopInstallStatusResult extends InstallStatus {\n found: boolean;\n}"
298
379
  },
299
380
  {
300
- "name": "ShopOutdatedEntry",
301
- "declaration": "export interface ShopOutdatedEntry {\n name: string;\n installed: string;\n latest: string;\n}"
381
+ "name": "ShopRestartResult",
382
+ "declaration": "export type ShopRestartResult = RestartOutcome;"
302
383
  },
303
384
  {
304
385
  "name": "ShopSetEnabledResult",
305
386
  "declaration": "export interface ShopSetEnabledResult {\n ok: boolean;\n detail?: string;\n}"
387
+ },
388
+ {
389
+ "name": "ShopUninstallResult",
390
+ "declaration": "export type ShopUninstallResult = { ok: true; installId: string; } | { ok: false; detail: string; };"
306
391
  }
307
392
  ]
308
393
  }
@@ -3,22 +3,26 @@ import type {
3
3
  RemoteResult,
4
4
  TypertRemoteContribution,
5
5
  } from '@deepseek-ai/dsh-typert-protocol'
6
- import type { InstallArgs, ShopCatalogResult, ShopInstallResult, ShopInstallStatusResult, ShopOutdatedEntry, ShopSetEnabledResult } from 'dsh-plugin-shop/types'
6
+ import type { InstallArgs, ShopCatalogResult, ShopInstalledEntry, ShopInstallResult, ShopInstallStatusResult, ShopRestartResult, ShopSetEnabledResult, ShopUninstallResult } from 'dsh-plugin-shop/types'
7
7
 
8
8
  declare module '@deepseek-ai/dsh-typert-protocol' {
9
9
  interface TypertRemoteNamespace$73686f70 {
10
10
  catalog: (args?: { refresh?: boolean; }) => Promise<RemoteResult<ShopCatalogResult>>
11
+ installed: () => Promise<RemoteResult<ShopInstalledEntry[]>>
11
12
  installStart: (args: InstallArgs) => Promise<RemoteResult<ShopInstallResult>>
12
13
  installStatus: (args: { installId: string; }) => Promise<RemoteResult<ShopInstallStatusResult>>
13
- outdated: () => Promise<RemoteResult<ShopOutdatedEntry[]>>
14
+ restart: () => Promise<RemoteResult<ShopRestartResult>>
14
15
  setEnabled: (args: { name: string; enabled: boolean; }) => Promise<RemoteResult<ShopSetEnabledResult>>
16
+ uninstallStart: (args: { name: string; }) => Promise<RemoteResult<ShopUninstallResult>>
15
17
  }
16
18
  interface TypertRemoteMap {
17
19
  'shop/catalog': (args?: { refresh?: boolean; }) => Promise<RemoteResult<ShopCatalogResult>>
20
+ 'shop/installed': () => Promise<RemoteResult<ShopInstalledEntry[]>>
18
21
  'shop/installStart': (args: InstallArgs) => Promise<RemoteResult<ShopInstallResult>>
19
22
  'shop/installStatus': (args: { installId: string; }) => Promise<RemoteResult<ShopInstallStatusResult>>
20
- 'shop/outdated': () => Promise<RemoteResult<ShopOutdatedEntry[]>>
23
+ 'shop/restart': () => Promise<RemoteResult<ShopRestartResult>>
21
24
  'shop/setEnabled': (args: { name: string; enabled: boolean; }) => Promise<RemoteResult<ShopSetEnabledResult>>
25
+ 'shop/uninstallStart': (args: { name: string; }) => Promise<RemoteResult<ShopUninstallResult>>
22
26
  }
23
27
  interface TypertRemoteNamespaceMap {
24
28
  'shop': TypertRemoteNamespace$73686f70