@deepseek-ai/dsh-plugin-manager 0.2.1-alpha.2 → 0.2.1-alpha.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
@@ -7,7 +7,7 @@ import { withFileLock, writeFileAtomic } from "@deepseek-ai/dsh-atomic-write";
7
7
  import z from "@deepseek-ai/schemastery";
8
8
  import { Remote, TypertRemoteService } from "@deepseek-ai/dsh-typert-protocol";
9
9
  import { pluginEntryId, readPluginInventory } from "@deepseek-ai/dsh-host-plugin-inventory";
10
- import { OFFICIAL_ON_DEMAND_CATALOG, OPTIONAL_BUNDLES, PROFILE_COMPATIBILITY_FILENAME, bundlePatchPaths, composeEntries, evaluatePluginCompatibility, getDshRuntimeVersion, loadOptionalPatches, loadOverlayPatches, pluginCompatibilityWarning, readPluginMeta, readProfileCompatibility, readProfileManifest, readProfilePatches, readProfileVersionExemptions, reconcileProfilePatches, resolveBundleDir, resolveProfileDir, setProfileVersionExemption } from "@deepseek-ai/dsh-app-boot";
10
+ import { OFFICIAL_ON_DEMAND_CATALOG, OPTIONAL_BUNDLES, PROFILE_COMPATIBILITY_FILENAME, ResolutionRestartRequired, bundlePatchPaths, composeEntries, evaluatePluginCompatibility, getDshRuntimeVersion, loadOptionalPatches, loadOverlayPatches, pluginCompatibilityWarning, readBundlePatches, readPluginMeta, readProfileCompatibility, readProfileManifest, readProfilePatches, readProfileVersionExemptions, resolveBundleDir, resolveProfileDir, setProfileVersionExemption } from "@deepseek-ai/dsh-app-boot";
11
11
  import { once } from "node:events";
12
12
  import { execa } from "execa";
13
13
  import { scrubbedParentEnv } from "@deepseek-ai/dsh-subprocess";
@@ -234,6 +234,64 @@ function incompatiblePlugin(issue) {
234
234
  //#endregion
235
235
  //#region lib/types/operations.js
236
236
  /** Shared profile package operations used by dsh plugin and the running manager. */
237
+ var __addDisposableResource = function(env, value, async) {
238
+ if (value !== null && value !== void 0) {
239
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
240
+ var dispose, inner;
241
+ if (async) {
242
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
243
+ dispose = value[Symbol.asyncDispose];
244
+ }
245
+ if (dispose === void 0) {
246
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
247
+ dispose = value[Symbol.dispose];
248
+ if (async) inner = dispose;
249
+ }
250
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
251
+ if (inner) dispose = function() {
252
+ try {
253
+ inner.call(this);
254
+ } catch (e) {
255
+ return Promise.reject(e);
256
+ }
257
+ };
258
+ env.stack.push({
259
+ value,
260
+ dispose,
261
+ async
262
+ });
263
+ } else if (async) env.stack.push({ async: true });
264
+ return value;
265
+ };
266
+ var __disposeResources = (function(SuppressedError) {
267
+ return function(env) {
268
+ function fail(e) {
269
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
270
+ env.hasError = true;
271
+ }
272
+ var r, s = 0;
273
+ function next() {
274
+ while (r = env.stack.pop()) try {
275
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
276
+ if (r.dispose) {
277
+ var result = r.dispose.call(r.value);
278
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) {
279
+ fail(e);
280
+ return next();
281
+ });
282
+ } else s |= 1;
283
+ } catch (e) {
284
+ fail(e);
285
+ }
286
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
287
+ if (env.hasError) throw env.error;
288
+ }
289
+ return next();
290
+ };
291
+ })(typeof SuppressedError === "function" ? SuppressedError : function(error, suppressed, message) {
292
+ var e = new Error(message);
293
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
294
+ });
237
295
  /** Resolve relative package specs against the caller's directory.
238
296
  * @param argument One pnpm argument.
239
297
  * @param cwd Invocation directory, never the profile directory.
@@ -461,254 +519,268 @@ function bundleComponentManifests(manifest, dir, anchor) {
461
519
  * @param options Output, activation and cancellation policy.
462
520
  * @returns Exit status, whether the silence bound stopped the run, and the diagnostic path.
463
521
  * A compatibility denial returns exit code 1. Service output is bounded; CLI output uses inherited descriptors.
522
+ * The diagnostic file is closed on return or rejection, including preparation and spawn failures.
464
523
  */
465
524
  async function runProfilePnpm(context, args, options) {
466
- const dir = context.dir ?? resolveProfileDir(context.profile, context.home);
467
- const active = await activeRecordedRun(dir);
468
- const logRoot = join(dir, ".plugin-manager", "logs");
469
- await mkdir(logRoot, {
470
- recursive: true,
471
- mode: 448
472
- });
473
- const logPath = join(await mkdtemp(join(logRoot, "operation-")), "pnpm.log");
474
- const log = await open(logPath, "wx", 384);
475
- if (active !== void 0) {
476
- await log.write(active);
477
- await log.close();
478
- options.onOutput?.(active, "stderr");
479
- const bytes = Buffer.from(active);
480
- return {
481
- exitCode: 1,
482
- output: bytes.subarray(Math.max(0, bytes.length - options.outputBytes)).toString("utf8"),
483
- truncated: bytes.length > options.outputBytes,
484
- logPath
485
- };
486
- }
487
- const before = readProfileManifest("dsh", dir);
488
- const savedFiles = ["package.json", "pnpm-lock.yaml"].map((name) => ({
489
- path: join(dir, name),
490
- text: optionalFile(join(dir, name))
491
- }));
492
- const beforeDependencies = directDependencies(before);
493
- const installedBefore = new Map(Object.keys(beforeDependencies).map((name) => [name, optionalFile(join(dir, "node_modules", name, "package.json"))]));
494
- let output = Buffer.alloc(0);
495
- let truncated = false;
496
- const append = (bytes) => {
497
- output = Buffer.concat([output, bytes]);
498
- if (output.length > options.outputBytes) {
499
- truncated = true;
500
- output = output.subarray(output.length - options.outputBytes);
501
- }
502
- };
503
- const environment = {
504
- ...options.execution === "cli" ? process.env : scrubbedParentEnv(),
505
- ...options.env
506
- };
507
- const restore = async () => {
508
- for (const file of savedFiles) if (file.text === void 0) await rm(file.path, { force: true });
509
- else await writeFileAtomic(file.path, file.text, { mode: 384 });
510
- };
511
- /** Packages a compatibility check refused; callers render them for their own surface. */
512
- const incompatible = [];
513
- const rejected = async (warnings, restoration) => {
514
- const diagnostic = `\ndsh: installation rejected: ${warnings.join("\n")}\ndsh: ${restoration}.\n`;
515
- await log.write(diagnostic);
516
- options.onOutput?.(diagnostic, "stderr");
517
- append(Buffer.from(diagnostic));
518
- await log.close();
519
- return {
520
- exitCode: 1,
521
- output: output.toString("utf8"),
522
- truncated,
523
- logPath,
524
- incompatible
525
- };
525
+ const env_1 = {
526
+ stack: [],
527
+ error: void 0,
528
+ hasError: false
526
529
  };
527
- const preflight = [];
528
- const exemptions = readProfileVersionExemptions(dir);
529
- const registryFlags = args.filter((argument) => argument.startsWith("--registry="));
530
- for (const raw of namedSpecs(args)) try {
531
- const manifest = await namedSpecManifest(dir, anchorPathSpec(raw, context.cwd), options, environment, registryFlags);
532
- if (manifest === void 0) continue;
533
- const issue = evaluatePluginCompatibility(manifest, exemptions);
534
- if (issue !== void 0 && !issue.exempted) {
535
- preflight.push(pluginCompatibilityWarning(issue));
536
- incompatible.push(incompatiblePlugin(issue));
537
- }
538
- } catch (error) {
539
- continue;
540
- }
541
- if (preflight.length > 0) return rejected(preflight, "nothing was installed");
542
- const cancellation = new AbortController();
543
- const grouped = leadsOwnGroup(options.execution);
544
- const child = execa(options.command ?? "pnpm", [...options.args ?? [], ...args.map((arg) => anchorPathSpec(arg, context.cwd))], {
545
- cwd: dir,
546
- env: environment,
547
- extendEnv: false,
548
- reject: false,
549
- stdout: options.execution === "cli" ? "inherit" : "pipe",
550
- stderr: options.execution === "cli" ? "inherit" : "pipe",
551
- killDescendants: options.execution === "service",
552
- buffer: false,
553
- stdin: options.execution === "cli" ? "inherit" : "ignore",
554
- cancelSignal: options.signal === void 0 ? cancellation.signal : AbortSignal.any([cancellation.signal, options.signal])
555
- });
556
- const exited = once(child.nodeChildProcess, "exit").catch(() => void 0);
557
- let writes = Promise.resolve();
558
- /** `settled` records that the process outcome is known; `stalled` that the silence bound stopped the run. */
559
- const control = {
560
- settled: false,
561
- stalled: false
562
- };
563
- /** Set once this call cuts the reading short itself, so the close it causes is not read as a run failure. */
564
- let cut = false;
565
- /** The first failure a reading hit before that cut, which the run still reports. */
566
- let failure;
567
- let idleTimer;
568
- /** The silence bound: a captured run that stops printing without exiting is terminated, never awaited. */
569
- const armIdle = () => {
570
- if (control.settled || options.idleTimeoutMs === void 0) return;
571
- clearTimeout(idleTimer);
572
- idleTimer = setTimeout(() => {
573
- control.stalled = true;
574
- child.kill();
575
- }, options.idleTimeoutMs);
576
- };
577
- const collect = async (stream, kind) => {
578
- try {
579
- for await (const chunk of stream) {
580
- armIdle();
581
- const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
582
- writes = writes.then(async () => {
583
- await log.write(bytes);
584
- });
585
- await writes;
586
- options.onOutput?.(bytes.toString("utf8"), kind);
587
- append(bytes);
588
- }
589
- } catch (error) {
590
- if (!cut) {
591
- failure ??= error instanceof Error ? error : new Error(String(error));
592
- cancellation.abort();
593
- }
594
- throw error;
595
- }
596
- };
597
- const collectors = [...child.stdout === null ? [] : [collect(child.stdout, "stdout")], ...child.stderr === null ? [] : [collect(child.stderr, "stderr")]];
598
- for (const collector of collectors) collector.catch(() => {});
599
- if (collectors.length > 0) armIdle();
600
- let exitCode;
601
530
  try {
602
- await recordRun(dir, {
603
- pid: child.pid,
604
- grouped
531
+ const dir = context.dir ?? resolveProfileDir(context.profile, context.home);
532
+ const active = await activeRecordedRun(dir);
533
+ const logRoot = join(dir, ".plugin-manager", "logs");
534
+ await mkdir(logRoot, {
535
+ recursive: true,
536
+ mode: 448
605
537
  });
606
- const settled = Promise.allSettled([child]);
607
- await Promise.race([exited, settled]);
608
- control.settled = true;
609
- clearTimeout(idleTimer);
610
- if (control.stalled) await awaitTreeGone({
611
- pid: child.pid,
612
- grouped
613
- });
614
- if (await drainWithin(collectors, DRAIN_AFTER_EXIT_MS)) {
615
- for (const stream of await Promise.allSettled(collectors)) if (stream.status === "rejected") throw stream.reason;
616
- } else {
617
- cut = true;
618
- child.stdout?.destroy();
619
- child.stderr?.destroy();
620
- const notice = "dsh: pnpm output was cut short after its process exited\n";
621
- await log.write(notice);
622
- if (failure !== void 0) throw failure;
623
- options.onOutput?.(notice, "stderr");
624
- append(Buffer.from(notice));
625
- }
626
- const [completion] = await settled;
627
- if (completion.status === "rejected") throw completion.reason;
628
- const result = completion.value;
629
- exitCode = result.exitCode ?? (result.code === "ENOENT" ? 127 : 1);
630
- if (control.stalled) {
631
- const notice = `dsh: pnpm printed nothing for ${String(options.idleTimeoutMs)}ms and was terminated\n`;
632
- await log.write(notice);
633
- options.onOutput?.(notice, "stderr");
634
- append(Buffer.from(notice));
538
+ const logPath = join(await mkdtemp(join(logRoot, "operation-")), "pnpm.log");
539
+ const log = __addDisposableResource(env_1, await open(logPath, "wx", 384), true);
540
+ if (active !== void 0) {
541
+ await log.write(active);
542
+ await log.close();
543
+ options.onOutput?.(active, "stderr");
544
+ const bytes = Buffer.from(active);
545
+ return {
546
+ exitCode: 1,
547
+ output: bytes.subarray(Math.max(0, bytes.length - options.outputBytes)).toString("utf8"),
548
+ truncated: bytes.length > options.outputBytes,
549
+ logPath
550
+ };
635
551
  }
636
- if (result.failed && output.length === 0) {
637
- const diagnostic = result.shortMessage ?? "pnpm failed";
552
+ const before = readProfileManifest("dsh", dir);
553
+ const savedFiles = ["package.json", "pnpm-lock.yaml"].map((name) => ({
554
+ path: join(dir, name),
555
+ text: optionalFile(join(dir, name))
556
+ }));
557
+ const beforeDependencies = directDependencies(before);
558
+ const installedBefore = new Map(Object.keys(beforeDependencies).map((name) => [name, optionalFile(join(dir, "node_modules", name, "package.json"))]));
559
+ let output = Buffer.alloc(0);
560
+ let truncated = false;
561
+ const append = (bytes) => {
562
+ output = Buffer.concat([output, bytes]);
563
+ if (output.length > options.outputBytes) {
564
+ truncated = true;
565
+ output = output.subarray(output.length - options.outputBytes);
566
+ }
567
+ };
568
+ const environment = {
569
+ ...options.execution === "cli" ? process.env : scrubbedParentEnv(),
570
+ ...options.env
571
+ };
572
+ const restore = async () => {
573
+ for (const file of savedFiles) if (file.text === void 0) await rm(file.path, { force: true });
574
+ else await writeFileAtomic(file.path, file.text, { mode: 384 });
575
+ };
576
+ /** Packages a compatibility check refused; callers render them for their own surface. */
577
+ const incompatible = [];
578
+ const rejected = async (warnings, restoration) => {
579
+ const diagnostic = `\ndsh: installation rejected: ${warnings.join("\n")}\ndsh: ${restoration}.\n`;
638
580
  await log.write(diagnostic);
639
- truncated = Buffer.byteLength(diagnostic) > options.outputBytes;
640
- output = Buffer.from(diagnostic).subarray(0, options.outputBytes);
581
+ options.onOutput?.(diagnostic, "stderr");
582
+ append(Buffer.from(diagnostic));
583
+ await log.close();
584
+ return {
585
+ exitCode: 1,
586
+ output: output.toString("utf8"),
587
+ truncated,
588
+ logPath,
589
+ incompatible
590
+ };
591
+ };
592
+ const preflight = [];
593
+ const exemptions = readProfileVersionExemptions(dir);
594
+ const registryFlags = args.filter((argument) => argument.startsWith("--registry="));
595
+ for (const raw of namedSpecs(args)) try {
596
+ const manifest = await namedSpecManifest(dir, anchorPathSpec(raw, context.cwd), options, environment, registryFlags);
597
+ if (manifest === void 0) continue;
598
+ const issue = evaluatePluginCompatibility(manifest, exemptions);
599
+ if (issue !== void 0 && !issue.exempted) {
600
+ preflight.push(pluginCompatibilityWarning(issue));
601
+ incompatible.push(incompatiblePlugin(issue));
602
+ }
603
+ } catch (error) {
604
+ continue;
641
605
  }
642
- if (exitCode === 0 && !control.stalled) {
643
- const after = readProfileManifest("dsh", dir);
644
- const warnings = [];
645
- for (const [name, spec] of Object.entries(directDependencies(after))) {
646
- const packageDir = join(dir, "node_modules", name);
647
- const installed = optionalFile(join(packageDir, "package.json"));
648
- if (installed === void 0) continue;
649
- const untouched = beforeDependencies[name] === spec && installedBefore.get(name) === installed;
650
- const found = [];
651
- const issues = [];
652
- try {
653
- const manifest = readProfileManifest("dsh", packageDir);
654
- for (const candidate of [manifest, ...bundleComponentManifests(manifest, packageDir, context.installAnchor)]) {
655
- const issue = evaluatePluginCompatibility(candidate, readProfileVersionExemptions(dir));
656
- if (issue !== void 0 && !issue.exempted) {
657
- found.push(pluginCompatibilityWarning(issue));
658
- issues.push(incompatiblePlugin(issue));
659
- }
660
- }
661
- } catch (error) {
662
- found.push(`Cannot validate installed package ${name}: ${String(error)}`);
606
+ if (preflight.length > 0) return await rejected(preflight, "nothing was installed");
607
+ const cancellation = new AbortController();
608
+ const grouped = leadsOwnGroup(options.execution);
609
+ const child = execa(options.command ?? "pnpm", [...options.args ?? [], ...args.map((arg) => anchorPathSpec(arg, context.cwd))], {
610
+ cwd: dir,
611
+ env: environment,
612
+ extendEnv: false,
613
+ reject: false,
614
+ stdout: options.execution === "cli" ? "inherit" : "pipe",
615
+ stderr: options.execution === "cli" ? "inherit" : "pipe",
616
+ killDescendants: options.execution === "service",
617
+ buffer: false,
618
+ stdin: options.execution === "cli" ? "inherit" : "ignore",
619
+ cancelSignal: options.signal === void 0 ? cancellation.signal : AbortSignal.any([cancellation.signal, options.signal])
620
+ });
621
+ const exited = once(child.nodeChildProcess, "exit").catch(() => void 0);
622
+ let writes = Promise.resolve();
623
+ /** `settled` records that the process outcome is known; `stalled` that the silence bound stopped the run. */
624
+ const control = {
625
+ settled: false,
626
+ stalled: false
627
+ };
628
+ /** Set once this call cuts the reading short itself, so the close it causes is not read as a run failure. */
629
+ let cut = false;
630
+ /** The first failure a reading hit before that cut, which the run still reports. */
631
+ let failure;
632
+ let idleTimer;
633
+ /** The silence bound: a captured run that stops printing without exiting is terminated, never awaited. */
634
+ const armIdle = () => {
635
+ if (control.settled || options.idleTimeoutMs === void 0) return;
636
+ clearTimeout(idleTimer);
637
+ idleTimer = setTimeout(() => {
638
+ control.stalled = true;
639
+ child.kill();
640
+ }, options.idleTimeoutMs);
641
+ };
642
+ const collect = async (stream, kind) => {
643
+ try {
644
+ for await (const chunk of stream) {
645
+ armIdle();
646
+ const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
647
+ writes = writes.then(async () => {
648
+ await log.write(bytes);
649
+ });
650
+ await writes;
651
+ options.onOutput?.(bytes.toString("utf8"), kind);
652
+ append(bytes);
663
653
  }
664
- if (found.length === 0) continue;
665
- if (!untouched) {
666
- warnings.push(...found);
667
- incompatible.push(...issues);
668
- } else {
669
- const notice = `\ndsh: warning: ${found.join("\n")}\ndsh: it stays installed but profile startup denies it until you grant an exemption for those exact versions.\n`;
670
- await log.write(notice);
671
- options.onOutput?.(notice, "stderr");
654
+ } catch (error) {
655
+ if (!cut) {
656
+ failure ??= error instanceof Error ? error : new Error(String(error));
657
+ cancellation.abort();
672
658
  }
659
+ throw error;
673
660
  }
674
- if (warnings.length > 0) {
675
- await restore();
676
- const repair = ["install", savedFiles.some((file) => file.path.endsWith("pnpm-lock.yaml") && file.text !== void 0) ? "--frozen-lockfile" : "--config.lockfile=false"];
677
- const repairing = execa(options.command ?? "pnpm", [...options.args ?? [], ...repair], {
678
- cwd: dir,
679
- env: environment,
680
- extendEnv: false,
681
- reject: false,
682
- stdin: "ignore",
683
- ...options.idleTimeoutMs === void 0 ? {} : { timeout: options.idleTimeoutMs }
684
- });
685
- await recordRun(dir, {
686
- pid: repairing.pid,
687
- grouped: false
688
- });
689
- const repaired = await repairing;
690
- exitCode = 1;
691
- const restoration = repaired.exitCode === 0 ? "restored package.json, pnpm-lock.yaml, and node_modules" : "restored package.json and pnpm-lock.yaml, but node_modules could not be reinstalled; run 'dsh plugin install'";
692
- const diagnostic = `\ndsh: installation rejected: ${warnings.join("\n")}\ndsh: ${restoration}.\n`;
661
+ };
662
+ const collectors = [...child.stdout === null ? [] : [collect(child.stdout, "stdout")], ...child.stderr === null ? [] : [collect(child.stderr, "stderr")]];
663
+ for (const collector of collectors) collector.catch(() => {});
664
+ if (collectors.length > 0) armIdle();
665
+ let exitCode;
666
+ try {
667
+ await recordRun(dir, {
668
+ pid: child.pid,
669
+ grouped
670
+ });
671
+ const settled = Promise.allSettled([child]);
672
+ await Promise.race([exited, settled]);
673
+ control.settled = true;
674
+ clearTimeout(idleTimer);
675
+ if (control.stalled) await awaitTreeGone({
676
+ pid: child.pid,
677
+ grouped
678
+ });
679
+ if (await drainWithin(collectors, DRAIN_AFTER_EXIT_MS)) {
680
+ for (const stream of await Promise.allSettled(collectors)) if (stream.status === "rejected") throw stream.reason;
681
+ } else {
682
+ cut = true;
683
+ child.stdout?.destroy();
684
+ child.stderr?.destroy();
685
+ const notice = "dsh: pnpm output was cut short after its process exited\n";
686
+ await log.write(notice);
687
+ if (failure !== void 0) throw failure;
688
+ options.onOutput?.(notice, "stderr");
689
+ append(Buffer.from(notice));
690
+ }
691
+ const [completion] = await settled;
692
+ if (completion.status === "rejected") throw completion.reason;
693
+ const result = completion.value;
694
+ exitCode = result.exitCode ?? (result.code === "ENOENT" ? 127 : 1);
695
+ if (control.stalled) {
696
+ const notice = `dsh: pnpm printed nothing for ${String(options.idleTimeoutMs)}ms and was terminated\n`;
697
+ await log.write(notice);
698
+ options.onOutput?.(notice, "stderr");
699
+ append(Buffer.from(notice));
700
+ }
701
+ if (result.failed && output.length === 0) {
702
+ const diagnostic = result.shortMessage ?? "pnpm failed";
693
703
  await log.write(diagnostic);
694
- options.onOutput?.(diagnostic, "stderr");
695
- append(Buffer.from(diagnostic));
696
- } else if (options.activateNewBundles !== false) await reconcile(before, dir, context.installAnchor, options);
704
+ truncated = Buffer.byteLength(diagnostic) > options.outputBytes;
705
+ output = Buffer.from(diagnostic).subarray(0, options.outputBytes);
706
+ }
707
+ if (exitCode === 0 && !control.stalled) {
708
+ const after = readProfileManifest("dsh", dir);
709
+ const warnings = [];
710
+ for (const [name, spec] of Object.entries(directDependencies(after))) {
711
+ const packageDir = join(dir, "node_modules", name);
712
+ const installed = optionalFile(join(packageDir, "package.json"));
713
+ if (installed === void 0) continue;
714
+ const untouched = beforeDependencies[name] === spec && installedBefore.get(name) === installed;
715
+ const found = [];
716
+ const issues = [];
717
+ try {
718
+ const manifest = readProfileManifest("dsh", packageDir);
719
+ for (const candidate of [manifest, ...bundleComponentManifests(manifest, packageDir, context.installAnchor)]) {
720
+ const issue = evaluatePluginCompatibility(candidate, readProfileVersionExemptions(dir));
721
+ if (issue !== void 0 && !issue.exempted) {
722
+ found.push(pluginCompatibilityWarning(issue));
723
+ issues.push(incompatiblePlugin(issue));
724
+ }
725
+ }
726
+ } catch (error) {
727
+ found.push(`Cannot validate installed package ${name}: ${String(error)}`);
728
+ }
729
+ if (found.length === 0) continue;
730
+ if (!untouched) {
731
+ warnings.push(...found);
732
+ incompatible.push(...issues);
733
+ } else {
734
+ const notice = `\ndsh: warning: ${found.join("\n")}\ndsh: it stays installed but profile startup denies it until you grant an exemption for those exact versions.\n`;
735
+ await log.write(notice);
736
+ options.onOutput?.(notice, "stderr");
737
+ }
738
+ }
739
+ if (warnings.length > 0) {
740
+ await restore();
741
+ const repair = ["install", savedFiles.some((file) => file.path.endsWith("pnpm-lock.yaml") && file.text !== void 0) ? "--frozen-lockfile" : "--config.lockfile=false"];
742
+ const repairing = execa(options.command ?? "pnpm", [...options.args ?? [], ...repair], {
743
+ cwd: dir,
744
+ env: environment,
745
+ extendEnv: false,
746
+ reject: false,
747
+ stdin: "ignore",
748
+ ...options.idleTimeoutMs === void 0 ? {} : { timeout: options.idleTimeoutMs }
749
+ });
750
+ await recordRun(dir, {
751
+ pid: repairing.pid,
752
+ grouped: false
753
+ });
754
+ const repaired = await repairing;
755
+ exitCode = 1;
756
+ const restoration = repaired.exitCode === 0 ? "restored package.json, pnpm-lock.yaml, and node_modules" : "restored package.json and pnpm-lock.yaml, but node_modules could not be reinstalled; run 'dsh plugin install'";
757
+ const diagnostic = `\ndsh: installation rejected: ${warnings.join("\n")}\ndsh: ${restoration}.\n`;
758
+ await log.write(diagnostic);
759
+ options.onOutput?.(diagnostic, "stderr");
760
+ append(Buffer.from(diagnostic));
761
+ } else if (options.activateNewBundles !== false) await reconcile(before, dir, context.installAnchor, options);
762
+ }
763
+ } finally {
764
+ control.settled = true;
765
+ clearTimeout(idleTimer);
766
+ await rm(runRecordPath(dir), { force: true });
767
+ await log.close();
697
768
  }
769
+ return {
770
+ exitCode,
771
+ output: output.toString("utf8"),
772
+ truncated,
773
+ logPath,
774
+ ...control.stalled ? { timedOut: true } : {},
775
+ ...incompatible.length > 0 ? { incompatible } : {}
776
+ };
777
+ } catch (e_1) {
778
+ env_1.error = e_1;
779
+ env_1.hasError = true;
698
780
  } finally {
699
- control.settled = true;
700
- clearTimeout(idleTimer);
701
- await rm(runRecordPath(dir), { force: true });
702
- await log.close();
781
+ const result_1 = __disposeResources(env_1);
782
+ if (result_1) await result_1;
703
783
  }
704
- return {
705
- exitCode,
706
- output: output.toString("utf8"),
707
- truncated,
708
- logPath,
709
- ...control.stalled ? { timedOut: true } : {},
710
- ...incompatible.length > 0 ? { incompatible } : {}
711
- };
712
784
  }
713
785
  /**
714
786
  * Read the registry pnpm's own configuration names in the profile: its `.npmrc` chain and workspace settings,
@@ -939,15 +1011,25 @@ function attributeFailure(kind, log, spec) {
939
1011
  }
940
1012
  //#endregion
941
1013
  //#region lib/types/patch.js
942
- /** Comment-preserving profile plugin enablement edits. */
943
- /** Replace the last matching override or append one after existing insertions.
944
- * @param filename Current profile patch file.
1014
+ /** Comment-preserving profile plugin enablement edits and the entry flattening they compare against. */
1015
+ /** Flatten only the groups addressable by the profile's patch composer.
1016
+ * @param rows Composed top-level entries.
1017
+ * @returns Entries in depth-first order.
1018
+ */
1019
+ function flatten(rows) {
1020
+ return rows.flatMap((row) => [row, ...row.group && Array.isArray(row.config) ? flatten(row.config) : []]);
1021
+ }
1022
+ /** Replace the last matching override or append one after existing insertions. An override that holds only `id` and
1023
+ * `disabled`, without anchors, is removed, or not appended, when the bundle layers and the remaining profile patches
1024
+ * already give the entry the requested literal `disabled` value; home patches and launch overlays are not consulted.
1025
+ * @param profile Profile whose patch file is edited and whose selected bundles supply the compared composition.
945
1026
  * @param id Unique composition entry id.
946
1027
  * @param name Module name used to match name-qualified overrides.
947
1028
  * @param enabled Desired entry enablement.
948
1029
  * @returns Whether the file changed.
949
1030
  */
950
- async function writePluginEnabled(filename, id, name, enabled) {
1031
+ async function writePluginEnabled(profile, id, name, enabled) {
1032
+ const filename = profile.patchPath;
951
1033
  let text;
952
1034
  try {
953
1035
  text = await readFile(filename, "utf8");
@@ -962,20 +1044,36 @@ async function writePluginEnabled(filename, id, name, enabled) {
962
1044
  const error = document.errors[0];
963
1045
  if (error !== void 0) throw error;
964
1046
  if (!isSeq(document.contents)) throw new Error("Profile patch must be a YAML sequence");
965
- loadOptionalPatches("dsh", filename);
1047
+ const patches = loadOptionalPatches("dsh", filename) ?? [];
1048
+ const bundlePatches = readBundlePatches("dsh", profile);
966
1049
  const items = document.contents.items;
967
1050
  const target = items.findLast((item, index) => {
968
1051
  if (!isMap(item) || document.getIn([index, "id"]) !== id || item.has("insert")) return false;
969
1052
  const expectedName = document.getIn([index, "name"]);
970
1053
  return !expectedName || expectedName === name;
971
1054
  });
1055
+ const implied = (index) => {
1056
+ const row = flatten(composeEntries([[...bundlePatches, ...patches.toSpliced(index, 1)]])).findLast((entry) => entry.id === id);
1057
+ return row !== void 0 && (row.disabled ?? false) === !enabled;
1058
+ };
972
1059
  if (isMap(target)) {
973
- if (document.getIn([items.indexOf(target), "disabled"]) === !enabled) return false;
974
- document.setIn([items.indexOf(target), "disabled"], !enabled);
975
- } else document.add({
976
- id,
977
- disabled: !enabled
978
- });
1060
+ const index = items.indexOf(target);
1061
+ if (target.anchor === void 0 && target.items.every((pair) => isScalar(pair.key) && isScalar(pair.value) && pair.key.anchor === void 0 && pair.value.anchor === void 0 && (pair.key.value === "id" || pair.key.value === "disabled")) && implied(index)) {
1062
+ document.deleteIn([index]);
1063
+ const comment = target.commentBefore;
1064
+ const next = items[index];
1065
+ if (comment && isNode(next)) next.commentBefore = next.commentBefore ? `${comment}\n${next.commentBefore}` : comment;
1066
+ else if (comment && items.length === 0) document.contents.commentBefore = comment;
1067
+ else if (comment) document.comment = document.comment ? `${comment}\n${document.comment}` : comment;
1068
+ } else if (document.getIn([index, "disabled"]) === !enabled) return false;
1069
+ else document.setIn([index, "disabled"], !enabled);
1070
+ } else {
1071
+ if (implied(items.length)) return false;
1072
+ document.add({
1073
+ id,
1074
+ disabled: !enabled
1075
+ });
1076
+ }
979
1077
  await writeFileAtomic(filename, String(document), { mode: 384 });
980
1078
  return true;
981
1079
  }
@@ -1183,6 +1281,7 @@ const protectedModules = new Set([
1183
1281
  "@deepseek-ai/dsh-host-plugin-inventory",
1184
1282
  "@deepseek-ai/dsh-typert-registry",
1185
1283
  "@deepseek-ai/dsh-api-remotes",
1284
+ "@deepseek-ai/dsh-api-remote-access",
1186
1285
  "@deepseek-ai/cordis-plugin-timer",
1187
1286
  "@deepseek-ai/dsh-client-connection",
1188
1287
  "@deepseek-ai/dsh-host-frontend-static",
@@ -1193,10 +1292,6 @@ const protectedModules = new Set([
1193
1292
  const RESTORED_FILES = ["package.json", "pnpm-lock.yaml"];
1194
1293
  /** pnpm's colour escapes, which a JSON answer may be wrapped in. */
1195
1294
  const ANSI_SEQUENCE = /\x1b\[[0-9;]*m/g;
1196
- /** Flatten only the groups addressable by the profile's patch composer. */
1197
- function flatten(rows) {
1198
- return rows.flatMap((row) => [row, ...row.group && Array.isArray(row.config) ? flatten(row.config) : []]);
1199
- }
1200
1295
  /** Preserve the exact observed diagnostic, including non-Error failures. */
1201
1296
  function messageOf(error) {
1202
1297
  return error instanceof Error ? error.message : String(error);
@@ -1262,6 +1357,33 @@ function printedError(printed) {
1262
1357
  const { code, message } = error;
1263
1358
  return [code, message].filter((part) => typeof part === "string").join(" ");
1264
1359
  }
1360
+ /** The bundles a profile manifest selects. */
1361
+ function bundleSelection(manifest) {
1362
+ return manifest.dsh?.profile?.bundles ?? [];
1363
+ }
1364
+ /**
1365
+ * The package an installation spec names before pnpm runs: a registry name, a local directory's manifest name, or the
1366
+ * dependency saved with this git or tarball spec, compared as the plugin list shows saved specs.
1367
+ * @param spec The installation spec.
1368
+ * @param dependencies The profile's saved dependencies.
1369
+ * @param profileDir The profile directory saved local paths are relative to.
1370
+ * @returns The package name, or `undefined` when only the installation tells it.
1371
+ */
1372
+ function specPackageName(spec, dependencies, profileDir) {
1373
+ const parsed = parsedForRegistry(spec);
1374
+ if (parsed.kind === "registry") return parsed.name;
1375
+ if (parsed.kind === "path") return packageName(parsed.path);
1376
+ const specs = parsed.kind === "tarball" && parsed.path !== void 0 ? [spec, `file:${parsed.path}`] : [spec];
1377
+ return Object.entries(dependencies).find(([dependency, saved]) => specs.includes(dependencySpec(dependency, saved, profileDir)))?.[0];
1378
+ }
1379
+ /** The package name a local directory declares, or `undefined` without a readable manifest. */
1380
+ function packageName(dir) {
1381
+ try {
1382
+ return readProfileManifest("dsh", dir).name;
1383
+ } catch (_error) {
1384
+ return;
1385
+ }
1386
+ }
1265
1387
  /** The spec's form, for deciding whether a failed attempt was the registry's; a form the parser refuses has no host of its own. */
1266
1388
  function parsedForRegistry(spec) {
1267
1389
  try {
@@ -1459,6 +1581,11 @@ let PluginManager = (() => {
1459
1581
  });
1460
1582
  /** Management bundles remain protected if their files become unreadable. */
1461
1583
  managementBundles = (__runInitializers(this, _instanceExtraInitializers), /* @__PURE__ */ new Set());
1584
+ /**
1585
+ * The registry each package was last installed from by this manager in this process. pnpm verifies lockfile tarball
1586
+ * URLs against its default registry unless they belong to a named registry, so a later removal names it again.
1587
+ */
1588
+ installedFrom = /* @__PURE__ */ new Map();
1462
1589
  ownerEntryId;
1463
1590
  packageOperations = /* @__PURE__ */ new Set();
1464
1591
  profile;
@@ -1515,7 +1642,7 @@ let PluginManager = (() => {
1515
1642
  setVersionExemption(packageVersion, runtimeVersion, enabled, acceptRisk) {
1516
1643
  return this.change((result) => this.configure(async () => {
1517
1644
  await setProfileVersionExemption(this.profile.dir, packageVersion, runtimeVersion, enabled, acceptRisk === true);
1518
- result.warnings = await this.reload();
1645
+ Object.assign(result, await this.reload());
1519
1646
  }), {
1520
1647
  stage: "enable",
1521
1648
  target: packageVersion,
@@ -1553,7 +1680,7 @@ let PluginManager = (() => {
1553
1680
  listBundles() {
1554
1681
  const manifest = readProfileManifest("dsh", this.profile.dir);
1555
1682
  const exemptions = readProfileVersionExemptions(this.profile.dir);
1556
- const selected = manifest.dsh?.profile?.bundles ?? [];
1683
+ const selected = bundleSelection(manifest);
1557
1684
  const recorded = manifest.dependencies ?? {};
1558
1685
  const dependencies = Object.keys(recorded);
1559
1686
  const installation = JSON.parse(readFileSync(this.profile.installAnchor, "utf8"));
@@ -1701,7 +1828,7 @@ let PluginManager = (() => {
1701
1828
  const manifest = readProfileManifest("dsh", this.profile.dir);
1702
1829
  const installation = JSON.parse(readFileSync(this.profile.installAnchor, "utf8"));
1703
1830
  const known = new Set([
1704
- ...manifest.dsh?.profile?.bundles ?? [],
1831
+ ...bundleSelection(manifest),
1705
1832
  ...Object.keys(manifest.dependencies ?? {}),
1706
1833
  ...Object.keys(installation.dependencies ?? {})
1707
1834
  ]);
@@ -1804,8 +1931,8 @@ let PluginManager = (() => {
1804
1931
  const row = (await this.listPlugins()).find((item) => item.entryId === id);
1805
1932
  if (row === void 0) throw new ManagementFailure("unknown-plugin");
1806
1933
  if (row.readOnlyReason !== void 0) throw new ManagementFailure(row.readOnlyReason);
1807
- await writePluginEnabled(this.profile.patchPath, row.patchId, row.moduleName, enabled);
1808
- result.warnings = await this.reload(enabled ? [row.patchId] : []);
1934
+ await writePluginEnabled(this.profile, row.patchId, row.moduleName, enabled);
1935
+ Object.assign(result, await this.reload(enabled ? [row.patchId] : []));
1809
1936
  return (await this.listPlugins()).find((item) => item.entryId === id)?.enabled !== enabled && this.ownerContext.get("hmr") !== void 0 ? "overridden" : void 0;
1810
1937
  }), {
1811
1938
  stage: "enable",
@@ -1821,9 +1948,7 @@ let PluginManager = (() => {
1821
1948
  setBundleEnabled(name, enabled) {
1822
1949
  return this.change((result) => this.configure(async () => {
1823
1950
  await this.selectBundle(name, enabled);
1824
- if (enabled) await this.refreshPackages();
1825
- result.warnings = await this.reload(enabled ? this.bundleRows(name).map((row) => row.id) : []);
1826
- if (!enabled && this.ownerContext.get("hmr") !== void 0) await this.refreshPackages();
1951
+ Object.assign(result, enabled ? await this.publishAndReload(this.bundleRows(name).map((row) => row.id)) : await this.reload());
1827
1952
  }), {
1828
1953
  stage: "enable",
1829
1954
  target: name,
@@ -1860,12 +1985,14 @@ let PluginManager = (() => {
1860
1985
  const result = this.change(async (result) => {
1861
1986
  if (spec.trim() === "" || spec.startsWith("-")) throw new ManagementFailure("invalid-spec");
1862
1987
  if (stopped()) throw new InstallCancelledError();
1988
+ const before = readProfileManifest("dsh", this.profile.dir).dependencies ?? {};
1989
+ const known = specPackageName(spec, before, this.profile.dir);
1990
+ if (this.ownerContext.get("hmr") !== void 0 && known !== void 0 && Object.hasOwn(before, known)) throw new ManagementFailure("already-installed");
1863
1991
  if (options?.approvedBuilds !== void 0) {
1864
1992
  await approveBuilds(this.profile.dir, options.approvedBuilds);
1865
1993
  result.approvedBuilds = options.approvedBuilds;
1866
1994
  }
1867
1995
  const files = await this.readRestoredFiles();
1868
- const before = readProfileManifest("dsh", this.profile.dir).dependencies ?? {};
1869
1996
  let name;
1870
1997
  let version;
1871
1998
  try {
@@ -1891,10 +2018,12 @@ let PluginManager = (() => {
1891
2018
  }
1892
2019
  const plan = registryPlan(options?.registry, await this.registries());
1893
2020
  let run;
2021
+ let used = null;
1894
2022
  for (const [index, registry] of plan.entries()) {
1895
2023
  if (index > 0) await this.restoreFiles(files);
1896
2024
  if (stopped()) throw new InstallCancelledError();
1897
2025
  result.registries.push(registry);
2026
+ used = registry;
1898
2027
  announce("installing", {
1899
2028
  registry,
1900
2029
  index: index + 1,
@@ -1933,9 +2062,8 @@ let PluginManager = (() => {
1933
2062
  const after = readProfileManifest("dsh", this.profile.dir).dependencies ?? {};
1934
2063
  const installed = Object.keys(after).filter((name) => before[name] !== after[name]);
1935
2064
  if (installed.length === 0) {
1936
- const parsed = parsedForRegistry(spec);
1937
- const localName = parsed.kind === "path" ? readProfileManifest("dsh", parsed.path).name : void 0;
1938
- installed.push(...Object.keys(after).filter((name) => name === localName || spec === name || spec.startsWith(`${name}@`)));
2065
+ const local = specPackageName(spec, after, this.profile.dir);
2066
+ if (local !== void 0 && Object.hasOwn(after, local)) installed.push(local);
1939
2067
  }
1940
2068
  const target = installed[0];
1941
2069
  if (installed.length !== 1 || target === void 0) throw new ManagementFailure("ambiguous-install");
@@ -1947,6 +2075,7 @@ let PluginManager = (() => {
1947
2075
  if (compatibility !== void 0 && !compatibility.exempted) throw new ManagementFailure("incompatible-version", [incompatiblePlugin(compatibility)]);
1948
2076
  for (const file of bundlePatchPaths(dir, manifest.dsh.bundle)) loadOverlayPatches("dsh", file);
1949
2077
  version = manifest.version;
2078
+ this.installedFrom.set(name, used);
1950
2079
  } catch (error) {
1951
2080
  await this.restoreFiles(files);
1952
2081
  throw error;
@@ -1959,9 +2088,9 @@ let PluginManager = (() => {
1959
2088
  result.stage = "enable";
1960
2089
  return this.configure(async () => {
1961
2090
  if (options?.enabled !== false) await this.selectBundle(name, true);
1962
- if (Object.hasOwn(before, name)) return "restart-required";
1963
- await this.refreshPackages();
1964
- if (options?.enabled !== false) result.warnings = await this.reload();
2091
+ if (Object.hasOwn(before, name) && this.ownerContext.get("hmr") === void 0) return;
2092
+ const selected = bundleSelection(readProfileManifest("dsh", this.profile.dir)).includes(name);
2093
+ Object.assign(result, await this.publishAndReload(selected ? this.bundleRows(name).map((row) => row.id) : []));
1965
2094
  });
1966
2095
  }, {
1967
2096
  stage: "install",
@@ -2013,14 +2142,20 @@ let PluginManager = (() => {
2013
2142
  const contributions = bundle.error === void 0 ? this.bundleRows(name) : [];
2014
2143
  if (bundle.enabled) {
2015
2144
  await this.selectBundle(name, false);
2016
- result.warnings = await this.reload();
2145
+ Object.assign(result, await this.reload());
2017
2146
  }
2018
2147
  if ([...this.ctx.loader.entries()].some((entry) => entry.fiber?.uid != null && contributions.some((row) => row.id === entry.options.id && row.name === entry.options.name))) throw new ManagementFailure("bundle-in-use");
2019
2148
  return bundle.installed;
2020
2149
  })) return;
2021
- result.packageResult = await this.runPnpm(["remove", name]);
2150
+ const registry = this.installedFrom.has(name) ? this.installedFrom.get(name) : this.configuredRegistries.registry;
2151
+ result.packageResult = await this.runPnpm([
2152
+ "remove",
2153
+ name,
2154
+ ...typeof registry === "string" ? [`--config.registry=${registry}`] : []
2155
+ ]);
2022
2156
  if (result.packageResult.exitCode !== 0 || result.packageResult.timedOut === true) throw new Error(result.packageResult.output);
2023
- await this.configure(() => this.refreshPackages());
2157
+ this.installedFrom.delete(name);
2158
+ Object.assign(result, await this.configure(() => this.publishAndReload()));
2024
2159
  }, {
2025
2160
  stage: "remove",
2026
2161
  target: name
@@ -2143,7 +2278,7 @@ let PluginManager = (() => {
2143
2278
  }
2144
2279
  async selectBundle(name, enabled) {
2145
2280
  const manifest = readProfileManifest("dsh", this.profile.dir);
2146
- const previous = manifest.dsh?.profile?.bundles ?? [];
2281
+ const previous = bundleSelection(manifest);
2147
2282
  if (enabled || !previous.includes(name)) {
2148
2283
  const metadata = bundleManifest(name, this.profile.dir, this.profile.installAnchor);
2149
2284
  if (metadata === void 0) throw new ManagementFailure("not-bundle");
@@ -2193,16 +2328,46 @@ let PluginManager = (() => {
2193
2328
  };
2194
2329
  return hmr === void 0 ? apply() : hmr.runExclusive(apply);
2195
2330
  }
2331
+ /**
2332
+ * Publish the profile's runtime resolution without HMR, unless a deselected startup bundle still runs and needs the
2333
+ * current table. A publication that needs a restart waits for the restart the result already reports.
2334
+ */
2196
2335
  async refreshPackages() {
2197
- if (this.ownerContext.get("hmr") === void 0) {
2198
- const selected = readProfileManifest("dsh", this.profile.dir).dsh?.profile?.bundles ?? [];
2199
- if (this.profile.startedBundles.some((name) => !selected.includes(name))) return;
2336
+ const selected = bundleSelection(readProfileManifest("dsh", this.profile.dir));
2337
+ if (this.profile.startedBundles.some((name) => !selected.includes(name))) return;
2338
+ try {
2339
+ const packages = this.ownerContext.get("pluginPackages");
2340
+ if (packages?.resolution !== void 0) await packages.refresh();
2341
+ } catch (error) {
2342
+ if (!(error instanceof ResolutionRestartRequired)) throw error;
2200
2343
  }
2201
- await this.ownerContext.get("pluginPackages")?.refresh();
2202
2344
  }
2345
+ /**
2346
+ * Apply a package change: without HMR, publish the runtime resolution when that is safe, then {@link reload}.
2347
+ * @param requiredIds Entry ids that must start.
2348
+ * @returns The reload's outcome.
2349
+ */
2350
+ async publishAndReload(requiredIds = []) {
2351
+ if (this.ownerContext.get("hmr") === void 0) await this.refreshPackages();
2352
+ return this.reload(requiredIds);
2353
+ }
2354
+ /**
2355
+ * Apply the saved profile; with HMR it recomposes live, including the runtime resolution.
2356
+ * @param requiredIds Entry ids that must start.
2357
+ * @returns Warnings about entries that were already inactive, and why the change waits for a restart, if it does.
2358
+ */
2203
2359
  async reload(requiredIds = []) {
2204
- if (this.ownerContext.get("hmr") === void 0) return [];
2205
- return reconcileProfilePatches(this.ownerContext.root, readProfilePatches("dsh", this.profile), "dsh", requiredIds);
2360
+ const hmr = this.ownerContext.get("hmr");
2361
+ if (hmr === void 0) {
2362
+ composeEntries([readProfilePatches("dsh", this.profile)]);
2363
+ return { warnings: [] };
2364
+ }
2365
+ const { warnings, pending, restartReason } = await hmr.reconcileProfile(requiredIds);
2366
+ const restart = restartReason ?? pending[0]?.reason;
2367
+ return restart === void 0 ? { warnings } : {
2368
+ warnings,
2369
+ restartReason: restart
2370
+ };
2206
2371
  }
2207
2372
  async change(operation, request, reason) {
2208
2373
  return withFileLock(join(this.profile.dir, "package.json"), async () => {
@@ -2211,10 +2376,15 @@ let PluginManager = (() => {
2211
2376
  const result = {
2212
2377
  ...request,
2213
2378
  changed: false,
2214
- application: this.ownerContext.get("hmr") !== void 0 ? "applied" : "restart-required"
2379
+ ...this.ownerContext.get("hmr") !== void 0 ? { application: "applied" } : {
2380
+ application: "restart-required",
2381
+ restartReason: "no-hmr"
2382
+ }
2215
2383
  };
2216
2384
  try {
2217
- result.application = await operation(result) ?? result.application;
2385
+ const outcome = await operation(result);
2386
+ if (outcome !== void 0) result.application = outcome;
2387
+ else if (result.application === "applied" && result.restartReason !== void 0) result.application = "restart-required";
2218
2388
  } catch (error) {
2219
2389
  if (error instanceof InstallCancelledError) result.application = "cancelled";
2220
2390
  else {
@@ -2222,6 +2392,7 @@ let PluginManager = (() => {
2222
2392
  result.error = managementError(error);
2223
2393
  }
2224
2394
  }
2395
+ if (result.application !== "restart-required") delete result.restartReason;
2225
2396
  result.changed = before !== this.diskState();
2226
2397
  this.ownerContext.emit("plugin-manager/changed", { reason });
2227
2398
  return result;