@elementspanel/epanel-plugin-market 1.0.0 → 1.0.1

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.
@@ -109,7 +109,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
109
109
  return (mod && mod.__esModule) ? mod : { "default": mod };
110
110
  };
111
111
  Object.defineProperty(exports, "__esModule", ({ value: true }));
112
- exports.StorageSubsystem = exports.MCServerStatus = exports.InstanceStreamListener = exports.GlobalVariable = exports.normalizeDockerPlatform = exports.normalizeDockerOS = exports.normalizeDockerArchitecture = exports.removeTrail = exports.uninstallNpmPlugin = exports.npmPluginRoots = exports.listNpmPlugins = exports.installNpmPlugin = exports.currentProjectRoot = exports.currentPackageRoot = exports.sortPlugins = exports.resolvePluginEntry = exports.readPluginManifest = exports.createFrontendPluginMetadata = exports.discoverPluginsFromRoots = exports.discoverPlugins = exports.arrayUnique = exports.toText = exports.toNumber = exports.toBoolean = exports.supposeValue = exports.isEmpty = exports.configureEntityParams = exports.systemInfo = exports.QueryWrapper = exports.QueryMapWrapper = exports.MySqlSource = exports.LocalFileSource = exports.killProcess = exports.ProcessWrapper = void 0;
112
+ exports.StorageSubsystem = exports.MCServerStatus = exports.InstanceStreamListener = exports.GlobalVariable = exports.normalizeDockerPlatform = exports.normalizeDockerOS = exports.normalizeDockerArchitecture = exports.removeTrail = exports.uninstallNpmPlugin = exports.isEssentialNpmPlugin = exports.npmPluginOperationPending = exports.npmPluginRoots = exports.listNpmPlugins = exports.installNpmPlugin = exports.currentProjectRoot = exports.currentPackageRoot = exports.sortPlugins = exports.resolvePluginEntry = exports.readPluginManifest = exports.createFrontendPluginMetadata = exports.discoverPluginsFromRoots = exports.discoverPlugins = exports.arrayUnique = exports.toText = exports.toNumber = exports.toBoolean = exports.supposeValue = exports.isEmpty = exports.configureEntityParams = exports.systemInfo = exports.QueryWrapper = exports.QueryMapWrapper = exports.MySqlSource = exports.LocalFileSource = exports.killProcess = exports.ProcessWrapper = void 0;
113
113
  const global_variable_1 = __importDefault(__webpack_require__("../common/src/global_variable.ts"));
114
114
  exports.GlobalVariable = global_variable_1.default;
115
115
  const instance_stream_1 = __importDefault(__webpack_require__("../common/src/instance_stream.ts"));
@@ -141,6 +141,8 @@ __exportStar(__webpack_require__("../common/src/plugin_contract.ts"), exports);
141
141
  __exportStar(__webpack_require__("../common/src/plugin_overrides.ts"), exports);
142
142
  __exportStar(__webpack_require__("../common/src/plugin_lifecycle.ts"), exports);
143
143
  __exportStar(__webpack_require__("../common/src/plugin_revision.ts"), exports);
144
+ __exportStar(__webpack_require__("../common/src/plugin_modules.ts"), exports);
145
+ __exportStar(__webpack_require__("../common/src/plugin_watch.ts"), exports);
144
146
  __exportStar(__webpack_require__("../common/src/runtime_paths.ts"), exports);
145
147
  var plugin_manifest_1 = __webpack_require__("../common/src/plugin_manifest.ts");
146
148
  Object.defineProperty(exports, "discoverPlugins", ({ enumerable: true, get: function () { return plugin_manifest_1.discoverPlugins; } }));
@@ -155,6 +157,8 @@ Object.defineProperty(exports, "currentProjectRoot", ({ enumerable: true, get: f
155
157
  Object.defineProperty(exports, "installNpmPlugin", ({ enumerable: true, get: function () { return plugin_npm_1.installNpmPlugin; } }));
156
158
  Object.defineProperty(exports, "listNpmPlugins", ({ enumerable: true, get: function () { return plugin_npm_1.listNpmPlugins; } }));
157
159
  Object.defineProperty(exports, "npmPluginRoots", ({ enumerable: true, get: function () { return plugin_npm_1.npmPluginRoots; } }));
160
+ Object.defineProperty(exports, "npmPluginOperationPending", ({ enumerable: true, get: function () { return plugin_npm_1.npmPluginOperationPending; } }));
161
+ Object.defineProperty(exports, "isEssentialNpmPlugin", ({ enumerable: true, get: function () { return plugin_npm_1.isEssentialNpmPlugin; } }));
158
162
  Object.defineProperty(exports, "uninstallNpmPlugin", ({ enumerable: true, get: function () { return plugin_npm_1.uninstallNpmPlugin; } }));
159
163
  var string_utils_1 = __webpack_require__("../common/src/string_utils.ts");
160
164
  Object.defineProperty(exports, "removeTrail", ({ enumerable: true, get: function () { return string_utils_1.removeTrail; } }));
@@ -623,7 +627,45 @@ function createFrontendPluginMetadata(manifest, extras = {}) {
623
627
  };
624
628
  }
625
629
  const MANIFEST_FILES = ["plugin.json", "manifest.json", "package.json"];
626
- /** Reads the manifest of one plugin directory, or null if it has none. */
630
+ const ENTRY_FIELDS = ["backend", "frontend", "panel", "daemon", "main", "entry", "ui"];
631
+ function objectValue(value) {
632
+ return value && typeof value === "object" && !Array.isArray(value)
633
+ ? value
634
+ : {};
635
+ }
636
+ /**
637
+ * Source plugins keep package.json beside plugin.json. Published npm plugins
638
+ * keep it at the package root, with runtime metadata under elementspanel.<side>.
639
+ * Stop at the first package boundary so a host's metadata cannot reach a plugin.
640
+ */
641
+ function readPackageMetadata(directory, id) {
642
+ const pluginDirectory = node_path_1.default.resolve(directory);
643
+ let current = pluginDirectory;
644
+ while (true) {
645
+ const filename = node_path_1.default.join(current, "package.json");
646
+ if (node_fs_1.default.existsSync(filename)) {
647
+ const { elementspanel, ...metadata } = objectValue(JSON.parse(node_fs_1.default.readFileSync(filename, "utf8")));
648
+ const { id: packageId, panel, daemon, ...shared } = objectValue(elementspanel);
649
+ const definition = [panel, daemon]
650
+ .map(objectValue)
651
+ .find((side) => typeof side.directory === "string" &&
652
+ node_path_1.default.resolve(current, side.directory) === pluginDirectory);
653
+ if (current !== pluginDirectory && (packageId !== id || !definition))
654
+ return {};
655
+ const { directory: _directory, folder: _folder, ...sideMetadata } = definition !== null && definition !== void 0 ? definition : {};
656
+ const result = { ...metadata, ...shared, ...sideMetadata };
657
+ // npm's main/entry fields do not define a side's execution entry.
658
+ for (const field of ["dual", ...ENTRY_FIELDS])
659
+ delete result[field];
660
+ return result;
661
+ }
662
+ const parent = node_path_1.default.dirname(current);
663
+ if (parent === current)
664
+ return {};
665
+ current = parent;
666
+ }
667
+ }
668
+ /** Merges package metadata with a plugin's identity and entries. Legacy manifests still load. */
627
669
  function readPluginManifest(directory, onWarning, fallbackId = node_path_1.default.basename(directory)) {
628
670
  for (const file of MANIFEST_FILES) {
629
671
  const filePath = node_path_1.default.join(directory, file);
@@ -636,7 +678,18 @@ function readPluginManifest(directory, onWarning, fallbackId = node_path_1.defau
636
678
  const id = typeof value.id === "string" ? value.id : fallbackId;
637
679
  if (!id.trim())
638
680
  return null;
639
- return { ...value, id: id.trim() };
681
+ const manifest = {
682
+ ...value,
683
+ ...readPackageMetadata(directory, id.trim()),
684
+ id: id.trim()
685
+ };
686
+ // Identity and execution paths always belong to plugin.json. Package
687
+ // metadata wins for descriptions, priority, configuration and dependencies.
688
+ for (const field of ["version", "dual", ...ENTRY_FIELDS]) {
689
+ if (value[field] !== undefined)
690
+ manifest[field] = value[field];
691
+ }
692
+ return manifest;
640
693
  }
641
694
  catch (error) {
642
695
  onWarning === null || onWarning === void 0 ? void 0 : onWarning(`Failed to read plugin manifest: ${filePath}`, error);
@@ -738,6 +791,13 @@ function discoverPluginsFromRoots(roots, options) {
738
791
  discovered.push({
739
792
  ...plugin,
740
793
  manifest,
794
+ ...(root.packageRoot
795
+ ? {
796
+ packageRoot: root.packageRoot,
797
+ packageName: root.packageName,
798
+ npmSource: root.npmSource
799
+ }
800
+ : {}),
741
801
  ...(root.folder ? { folder: root.folder } : {})
742
802
  });
743
803
  }
@@ -751,6 +811,178 @@ function sortPlugins(plugins) {
751
811
  }
752
812
 
753
813
 
814
+ /***/ },
815
+
816
+ /***/ "../common/src/plugin_modules.ts"
817
+ (__unused_webpack_module, exports, __webpack_require__) {
818
+
819
+
820
+ var __importDefault = (this && this.__importDefault) || function (mod) {
821
+ return (mod && mod.__esModule) ? mod : { "default": mod };
822
+ };
823
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
824
+ exports.pluginBackendRevision = pluginBackendRevision;
825
+ exports.forgetPluginModules = forgetPluginModules;
826
+ exports.loadPluginModule = loadPluginModule;
827
+ const node_fs_1 = __importDefault(__webpack_require__("node:fs"));
828
+ const node_path_1 = __importDefault(__webpack_require__("node:path"));
829
+ const node_url_1 = __webpack_require__("node:url");
830
+ const node_crypto_1 = __webpack_require__("node:crypto");
831
+ const node_module_1 = __webpack_require__("node:module");
832
+ const plugin_revision_1 = __webpack_require__("../common/src/plugin_revision.ts");
833
+ const REVISION = "epanelPluginRevision";
834
+ const graphs = new Map();
835
+ const previousRoots = new Map();
836
+ let hooksInstalled = false;
837
+ const sharedModules = new Set([
838
+ "cordis",
839
+ "@cordisjs/core",
840
+ "@cordisjs/logger",
841
+ "@cordisjs/timer",
842
+ "@cordisjs/schema",
843
+ "cosmokit",
844
+ "schemastery",
845
+ "reggol"
846
+ ]);
847
+ function dependencyRoots(directory) {
848
+ const visited = new Set([directory]);
849
+ const walk = (root) => {
850
+ const filename = node_path_1.default.join(root, "package.json");
851
+ if (!node_fs_1.default.existsSync(filename))
852
+ return;
853
+ const metadata = JSON.parse(node_fs_1.default.readFileSync(filename, "utf8"));
854
+ const localRequire = (0, node_module_1.createRequire)(filename);
855
+ for (const name of Object.keys({
856
+ ...metadata.dependencies,
857
+ ...metadata.optionalDependencies
858
+ })) {
859
+ if (sharedModules.has(name))
860
+ continue;
861
+ const target = (localRequire.resolve.paths(name) || [])
862
+ .map((directory) => node_path_1.default.join(directory, name, "package.json"))
863
+ .find((candidate) => node_fs_1.default.existsSync(candidate));
864
+ if (!target)
865
+ continue;
866
+ const dependency = node_fs_1.default.realpathSync(node_path_1.default.dirname(target));
867
+ if (visited.has(dependency))
868
+ continue;
869
+ visited.add(dependency);
870
+ walk(dependency);
871
+ }
872
+ };
873
+ walk(directory);
874
+ visited.delete(directory);
875
+ return [...visited];
876
+ }
877
+ /** Include transitive dependency versions so npm updates replace the affected graph. */
878
+ function pluginBackendRevision(entry, directory) {
879
+ const hash = (0, node_crypto_1.createHash)("sha256").update((0, plugin_revision_1.pluginDirectoryRevision)(node_path_1.default.dirname(entry)));
880
+ for (const root of [directory, ...dependencyRoots(directory)]) {
881
+ const filename = node_path_1.default.join(root, "package.json");
882
+ if (node_fs_1.default.existsSync(filename))
883
+ hash.update(root).update(node_fs_1.default.readFileSync(filename));
884
+ }
885
+ return hash.digest("hex").slice(0, 24);
886
+ }
887
+ function within(root, filename) {
888
+ const relative = node_path_1.default.relative(root, filename);
889
+ return (relative === "" ||
890
+ (!relative.startsWith(`..${node_path_1.default.sep}`) && relative !== ".." && !node_path_1.default.isAbsolute(relative)));
891
+ }
892
+ /** Clear the plugin's complete CJS graph, keeping shared host modules intact. */
893
+ function forgetPluginModules(directory) {
894
+ const runtimeRequire = eval("require");
895
+ const owned = [directory, ...(previousRoots.get(directory) || []), ...dependencyRoots(directory)];
896
+ previousRoots.delete(directory);
897
+ const belongs = (filename) => {
898
+ const parts = filename.split(`${node_path_1.default.sep}node_modules${node_path_1.default.sep}`);
899
+ if (parts.slice(1).some((part) => {
900
+ const name = part.startsWith("@")
901
+ ? part.split(node_path_1.default.sep).slice(0, 2).join("/")
902
+ : part.split(node_path_1.default.sep)[0];
903
+ return sharedModules.has(name);
904
+ }))
905
+ return false;
906
+ return owned.some((root) => within(root, filename));
907
+ };
908
+ for (const filename of Object.keys(runtimeRequire.cache)) {
909
+ if (belongs(filename))
910
+ delete runtimeRequire.cache[filename];
911
+ }
912
+ for (const mod of Object.values(runtimeRequire.cache)) {
913
+ if (mod)
914
+ mod.children = mod.children.filter((child) => !belongs(child.filename));
915
+ }
916
+ // npm can move a dependency or change its main entry. Node's CJS resolver
917
+ // retains those paths separately from require.cache.
918
+ const pathCache = runtimeRequire("node:module")._pathCache;
919
+ for (const [key, filename] of Object.entries(pathCache)) {
920
+ if (belongs(filename) || owned.some((root) => key.includes(root)))
921
+ delete pathCache[key];
922
+ }
923
+ }
924
+ function isEsm(entry) {
925
+ if (entry.endsWith(".mjs"))
926
+ return true;
927
+ if (entry.endsWith(".cjs"))
928
+ return false;
929
+ let directory = node_path_1.default.dirname(entry);
930
+ while (true) {
931
+ const filename = node_path_1.default.join(directory, "package.json");
932
+ if (node_fs_1.default.existsSync(filename))
933
+ return JSON.parse(node_fs_1.default.readFileSync(filename, "utf8")).type === "module";
934
+ const parent = node_path_1.default.dirname(directory);
935
+ if (parent === directory)
936
+ return false;
937
+ directory = parent;
938
+ }
939
+ }
940
+ /** Give an ESM entry and its local imports the same immutable revision URL. */
941
+ async function loadPluginModule(entry, directory, revision) {
942
+ const runtimeRequire = eval("require");
943
+ previousRoots.set(directory, dependencyRoots(directory));
944
+ if (!isEsm(entry)) {
945
+ try {
946
+ return runtimeRequire(entry);
947
+ }
948
+ catch (error) {
949
+ if ((error === null || error === void 0 ? void 0 : error.code) !== "ERR_REQUIRE_ESM")
950
+ throw error;
951
+ }
952
+ }
953
+ graphs.set(revision, [directory, ...dependencyRoots(directory)]);
954
+ if (!hooksInstalled) {
955
+ const { registerHooks } = runtimeRequire("node:module");
956
+ if (typeof registerHooks !== "function")
957
+ throw new Error("ESM plugin updates require Node.js 22.15 or newer.");
958
+ registerHooks({
959
+ resolve(specifier, context, nextResolve) {
960
+ var _a;
961
+ const resolved = nextResolve(specifier, context);
962
+ if (!((_a = context.parentURL) === null || _a === void 0 ? void 0 : _a.startsWith("file:")) || !resolved.url.startsWith("file:"))
963
+ return resolved;
964
+ const parent = new URL(context.parentURL);
965
+ const parentRevision = parent.searchParams.get(REVISION);
966
+ if (!parentRevision)
967
+ return resolved;
968
+ const roots = graphs.get(parentRevision);
969
+ if (!(roots === null || roots === void 0 ? void 0 : roots.some((root) => within(root, (0, node_url_1.fileURLToPath)(parent)))) ||
970
+ !roots.some((root) => within(root, (0, node_url_1.fileURLToPath)(resolved.url))))
971
+ return resolved;
972
+ const url = new URL(resolved.url);
973
+ url.searchParams.set(REVISION, parentRevision);
974
+ return { ...resolved, url: url.href };
975
+ }
976
+ });
977
+ hooksInstalled = true;
978
+ }
979
+ const url = (0, node_url_1.pathToFileURL)(entry);
980
+ url.searchParams.set(REVISION, revision);
981
+ const dynamicImport = new Function("specifier", "return import(specifier)");
982
+ return dynamicImport(url.href);
983
+ }
984
+
985
+
754
986
  /***/ },
755
987
 
756
988
  /***/ "../common/src/plugin_npm.ts"
@@ -760,20 +992,25 @@ function sortPlugins(plugins) {
760
992
  var __importDefault = (this && this.__importDefault) || function (mod) {
761
993
  return (mod && mod.__esModule) ? mod : { "default": mod };
762
994
  };
995
+ var _a;
763
996
  Object.defineProperty(exports, "__esModule", ({ value: true }));
764
997
  exports.currentPackageRoot = currentPackageRoot;
765
998
  exports.currentProjectRoot = currentProjectRoot;
766
999
  exports.npmPluginRoots = npmPluginRoots;
767
1000
  exports.listNpmPlugins = listNpmPlugins;
1001
+ exports.npmPluginOperationPending = npmPluginOperationPending;
1002
+ exports.isEssentialNpmPlugin = isEssentialNpmPlugin;
768
1003
  exports.installNpmPlugin = installNpmPlugin;
769
1004
  exports.uninstallNpmPlugin = uninstallNpmPlugin;
770
1005
  const child_process_1 = __webpack_require__("child_process");
771
1006
  const fs_extra_1 = __importDefault(__webpack_require__("fs-extra"));
772
1007
  const path_1 = __importDefault(__webpack_require__("path"));
773
1008
  const module_1 = __webpack_require__("module");
1009
+ const plugin_manifest_1 = __webpack_require__("../common/src/plugin_manifest.ts");
1010
+ const plugin_contract_1 = __webpack_require__("../common/src/plugin_contract.ts");
774
1011
  const PLUGIN_ID = /^[a-z][a-z0-9_-]{1,63}$/;
775
1012
  const PACKAGE_NAME = /^(?:@[a-z0-9_.-]+\/)?epanel-plugin-[a-z0-9_.-]+$/;
776
- const VERSION = /^[0-9][^\s/]{0,63}$/;
1013
+ const VERSION = /^[a-zA-Z0-9][a-zA-Z0-9._+-]{0,63}$/;
777
1014
  function readJson(filename) {
778
1015
  try {
779
1016
  const parsed = JSON.parse(fs_extra_1.default.readFileSync(filename, "utf8"));
@@ -843,8 +1080,20 @@ function pluginRecord(pluginId, name, source, packageRoot, directory, folder, ve
843
1080
  return { pluginId, name, version, sides: [], directory, folder, packageRoot, source };
844
1081
  }
845
1082
  function readPackageEntry(packageRoot, name) {
846
- const hostRequire = (0, module_1.createRequire)(path_1.default.join(packageRoot, "package.json"));
847
- const manifestPath = hostRequire.resolve(`${name}/package.json`);
1083
+ // Read package boundaries directly: Node caches require.resolve() paths even
1084
+ // after npm moves an installation between nested and hoisted node_modules.
1085
+ let manifestPath;
1086
+ for (const root of [packageRoot, currentProjectRoot(packageRoot)]) {
1087
+ const localRequire = (0, module_1.createRequire)(path_1.default.join(root, "package.json"));
1088
+ manifestPath = (localRequire.resolve.paths(name) || [])
1089
+ .map((directory) => path_1.default.join(directory, name, "package.json"))
1090
+ .find((filename) => fs_extra_1.default.existsSync(filename));
1091
+ if (manifestPath)
1092
+ break;
1093
+ }
1094
+ if (!manifestPath)
1095
+ throw new Error(`Missing npm plugin package: ${name}.`);
1096
+ manifestPath = fs_extra_1.default.realpathSync(manifestPath);
848
1097
  const metadata = readJson(manifestPath);
849
1098
  if (!metadata)
850
1099
  throw new Error(`Missing package metadata for ${name}.`);
@@ -858,7 +1107,10 @@ function sideDirectory(packageRoot, metadata, side) {
858
1107
  return undefined;
859
1108
  const target = path_1.default.resolve(packageRoot, directory);
860
1109
  const relative = path_1.default.relative(packageRoot, target);
861
- if (relative === "" || relative.startsWith(`..${path_1.default.sep}`) || path_1.default.isAbsolute(relative))
1110
+ if (relative === "" ||
1111
+ relative === ".." ||
1112
+ relative.startsWith(`..${path_1.default.sep}`) ||
1113
+ path_1.default.isAbsolute(relative))
862
1114
  return undefined;
863
1115
  return target;
864
1116
  }
@@ -876,7 +1128,7 @@ function recordFromPackage(pluginId, name, source, packageRoot, metadata, side)
876
1128
  return undefined;
877
1129
  const directory = sideDirectory(packageRoot, metadata, target);
878
1130
  const folder = stringValue((_a = objectValue(elements[target])) === null || _a === void 0 ? void 0 : _a.folder);
879
- if (!directory || !folder)
1131
+ if (!directory || !PLUGIN_ID.test(folder))
880
1132
  return undefined;
881
1133
  return {
882
1134
  ...pluginRecord(pluginId, name, source, packageRoot, directory, folder, version),
@@ -892,14 +1144,29 @@ function recordFromPackage(pluginId, name, source, packageRoot, metadata, side)
892
1144
  * is what lets a two-sided project share one dependency list between sides.
893
1145
  */
894
1146
  function listNpmPlugins(side, options = {}) {
895
- var _a;
1147
+ var _a, _b;
896
1148
  const { packageRoot, projectRoot } = npmPluginRoots(options);
897
1149
  const host = readJson(path_1.default.join(packageRoot, "package.json"));
898
1150
  if (!host)
899
1151
  throw new Error(`Missing runtime package metadata: ${packageRoot}`);
900
- const project = path_1.default.resolve(projectRoot) === path_1.default.resolve(packageRoot) ? null : readProjectManifest(projectRoot);
1152
+ const project = path_1.default.resolve(projectRoot) === path_1.default.resolve(packageRoot)
1153
+ ? null
1154
+ : readProjectManifest(projectRoot);
901
1155
  const hostMappings = mappingsOf(host);
902
- const projectMappings = mappingsOf(project);
1156
+ const projectMappings = { ...mappingsOf(project) };
1157
+ for (const name of Object.keys(dependenciesOf(project))) {
1158
+ if (!PACKAGE_NAME.test(name) || Object.values(projectMappings).includes(name))
1159
+ continue;
1160
+ try {
1161
+ const entry = readPackageEntry(projectRoot, name);
1162
+ const id = stringValue((_a = objectValue(entry.metadata.elementspanel)) === null || _a === void 0 ? void 0 : _a.id);
1163
+ if (PLUGIN_ID.test(id) && projectMappings[id] === undefined)
1164
+ projectMappings[id] = name;
1165
+ }
1166
+ catch (_c) {
1167
+ /* npm may still be writing the package. */
1168
+ }
1169
+ }
903
1170
  const mappings = new Map();
904
1171
  for (const [pluginId, name] of Object.entries(projectMappings)) {
905
1172
  if (!PLUGIN_ID.test(pluginId) || typeof name !== "string" || !PACKAGE_NAME.test(name))
@@ -909,6 +1176,8 @@ function listNpmPlugins(side, options = {}) {
909
1176
  for (const [pluginId, name] of Object.entries(hostMappings)) {
910
1177
  if (!PLUGIN_ID.test(pluginId) || typeof name !== "string" || !PACKAGE_NAME.test(name))
911
1178
  continue;
1179
+ if (dependenciesOf(host)[name] === undefined)
1180
+ continue;
912
1181
  mappings.set(pluginId, { name, source: "bundled" });
913
1182
  }
914
1183
  const hostDependencies = dependenciesOf(host);
@@ -916,7 +1185,7 @@ function listNpmPlugins(side, options = {}) {
916
1185
  const folders = new Set();
917
1186
  const records = [];
918
1187
  for (const [pluginId, mapping] of mappings) {
919
- const declared = (_a = hostDependencies[mapping.name]) !== null && _a !== void 0 ? _a : projectDependencies[mapping.name];
1188
+ const declared = (_b = hostDependencies[mapping.name]) !== null && _b !== void 0 ? _b : projectDependencies[mapping.name];
920
1189
  if (declared === undefined) {
921
1190
  if (options.skipInvalid)
922
1191
  continue;
@@ -924,7 +1193,7 @@ function listNpmPlugins(side, options = {}) {
924
1193
  }
925
1194
  let entry;
926
1195
  try {
927
- entry = readPackageEntry(packageRoot, mapping.name);
1196
+ entry = readPackageEntry(mapping.source === "project" ? projectRoot : packageRoot, mapping.name);
928
1197
  }
929
1198
  catch (error) {
930
1199
  if (options.skipInvalid)
@@ -957,7 +1226,12 @@ function runNpm(args, cwd) {
957
1226
  : process.platform === "win32"
958
1227
  ? {
959
1228
  command: process.env.ComSpec || "cmd.exe",
960
- args: ["/d", "/s", "/c", `npm ${args.map((item) => `"${item.replace(/"/g, '""')}"`).join(" ")}`],
1229
+ args: [
1230
+ "/d",
1231
+ "/s",
1232
+ "/c",
1233
+ `npm ${args.map((item) => `"${item.replace(/"/g, '""')}"`).join(" ")}`
1234
+ ],
961
1235
  shell: false
962
1236
  }
963
1237
  : { command: "npm", args, shell: false };
@@ -976,10 +1250,10 @@ function runNpm(args, cwd) {
976
1250
  };
977
1251
  (_a = child.stdout) === null || _a === void 0 ? void 0 : _a.on("data", append);
978
1252
  (_b = child.stderr) === null || _b === void 0 ? void 0 : _b.on("data", append);
1253
+ const action = args.find((argument) => argument === "install" || argument === "uninstall") || "operation";
979
1254
  const timer = setTimeout(() => {
980
- var _a;
981
1255
  child.kill("SIGKILL");
982
- reject(new Error(`npm ${(_a = args[0]) !== null && _a !== void 0 ? _a : ""} timed out.`));
1256
+ reject(new Error(`npm ${action} timed out.`));
983
1257
  }, 10 * 60 * 1000);
984
1258
  (_c = timer.unref) === null || _c === void 0 ? void 0 : _c.call(timer);
985
1259
  child.once("error", (error) => {
@@ -987,20 +1261,74 @@ function runNpm(args, cwd) {
987
1261
  reject(error);
988
1262
  });
989
1263
  child.once("close", (code) => {
990
- var _a;
991
1264
  clearTimeout(timer);
992
1265
  if (code === 0)
993
1266
  return resolve(output);
994
- reject(new Error(`npm ${(_a = args[0]) !== null && _a !== void 0 ? _a : ""} failed (${code}): ${output.trim()}`));
1267
+ reject(new Error(`npm ${action} failed (${code}): ${output.trim()}`));
995
1268
  });
996
1269
  });
997
1270
  }
998
- const operations = new Map();
1271
+ const operationsKey = Symbol.for("elementspanel.npm.operations");
1272
+ const shared = globalThis;
1273
+ const operations = ((_a = shared[operationsKey]) !== null && _a !== void 0 ? _a : (shared[operationsKey] = new Map()));
1274
+ const LOCK_NAME = ".elementspanel-npm.lock";
1275
+ async function acquireProjectLock(projectRoot) {
1276
+ var _a, _b;
1277
+ await fs_extra_1.default.ensureDir(projectRoot);
1278
+ const directory = path_1.default.join(projectRoot, LOCK_NAME);
1279
+ const deadline = Date.now() + 11 * 60 * 1000;
1280
+ while (true) {
1281
+ try {
1282
+ await fs_extra_1.default.mkdir(directory);
1283
+ await fs_extra_1.default.writeJson(path_1.default.join(directory, "owner.json"), { pid: process.pid });
1284
+ return () => fs_extra_1.default.remove(directory);
1285
+ }
1286
+ catch (error) {
1287
+ if (error.code !== "EEXIST")
1288
+ throw error;
1289
+ try {
1290
+ const owner = await fs_extra_1.default.readJson(path_1.default.join(directory, "owner.json"));
1291
+ try {
1292
+ process.kill(owner.pid, 0);
1293
+ }
1294
+ catch (failure) {
1295
+ if (failure.code === "ESRCH") {
1296
+ await fs_extra_1.default.remove(directory);
1297
+ continue;
1298
+ }
1299
+ }
1300
+ }
1301
+ catch (_c) {
1302
+ if (Date.now() - ((_b = (_a = (await fs_extra_1.default.stat(directory).catch(() => null))) === null || _a === void 0 ? void 0 : _a.mtimeMs) !== null && _b !== void 0 ? _b : Date.now()) >
1303
+ 60000) {
1304
+ await fs_extra_1.default.remove(directory);
1305
+ continue;
1306
+ }
1307
+ }
1308
+ if (Date.now() > deadline)
1309
+ throw new Error("Another npm plugin operation is still running.");
1310
+ await new Promise((resolve) => setTimeout(resolve, 200));
1311
+ }
1312
+ }
1313
+ }
1314
+ function npmPluginOperationPending() {
1315
+ const { packageRoot, projectRoot } = npmPluginRoots();
1316
+ return (operations.size > 0 ||
1317
+ [packageRoot, projectRoot].some((root) => fs_extra_1.default.existsSync(path_1.default.join(root, LOCK_NAME))));
1318
+ }
999
1319
  function withProjectLock(projectRoot, operation) {
1000
1320
  var _a;
1001
1321
  const key = process.platform === "win32" ? projectRoot.toLowerCase() : projectRoot;
1002
1322
  const previous = (_a = operations.get(key)) !== null && _a !== void 0 ? _a : Promise.resolve();
1003
- const result = previous.then(operation);
1323
+ const result = previous.then(async () => {
1324
+ const release = await acquireProjectLock(projectRoot);
1325
+ try {
1326
+ return await operation();
1327
+ }
1328
+ finally {
1329
+ await release();
1330
+ }
1331
+ });
1004
1332
  const settled = result.then(() => { }, () => { });
1005
1333
  operations.set(key, settled);
1006
1334
  return result.finally(() => {
@@ -1008,85 +1336,174 @@ function withProjectLock(projectRoot, operation) {
1008
1336
  operations.delete(key);
1009
1337
  });
1010
1338
  }
1011
- /**
1012
- * Installs (or updates) one npm plugin as a project dependency.
1013
- *
1014
- * The version is pinned with `--save-exact`: the panel performs updates, so a
1015
- * later bare `npm install` must not silently move the plugin to another release.
1016
- */
1339
+ /** Core services keep the connection and management interface available. */
1340
+ function isEssentialNpmPlugin(pluginId) {
1341
+ return ["i18n", "storage", "runtime", "server", "console", "config"].includes(pluginId);
1342
+ }
1343
+ /** npm changes dependencies and lockfiles; only append our plugin mapping afterwards. */
1017
1344
  async function installNpmPlugin(options) {
1018
- const { name, pluginId, version, side } = options;
1019
- if (!PLUGIN_ID.test(pluginId))
1020
- throw new Error(`Invalid plugin id: ${pluginId}`);
1345
+ const { name, side } = options;
1346
+ const version = options.version || "latest";
1021
1347
  if (!PACKAGE_NAME.test(name))
1022
1348
  throw new Error(`Invalid npm plugin package: ${name}`);
1349
+ if (options.pluginId && !PLUGIN_ID.test(options.pluginId))
1350
+ throw new Error(`Invalid plugin id: ${options.pluginId}`);
1023
1351
  if (!VERSION.test(version))
1024
1352
  throw new Error(`Invalid plugin version: ${version}`);
1025
1353
  const { packageRoot, projectRoot } = npmPluginRoots(options);
1026
1354
  return withProjectLock(projectRoot, async () => {
1027
- var _a;
1355
+ var _a, _b;
1028
1356
  const host = readJson(path_1.default.join(packageRoot, "package.json"));
1029
1357
  if (!host)
1030
1358
  throw new Error(`Missing runtime package metadata: ${packageRoot}`);
1031
- if (mappingsOf(host)[pluginId] !== undefined)
1032
- throw new Error(`BUNDLED:${pluginId}`);
1033
- const existing = mappingsOf(readProjectManifest(projectRoot))[pluginId];
1034
- if (existing !== undefined && existing !== name)
1035
- throw new Error(`Plugin id ${pluginId} is already installed as ${String(existing)}.`);
1036
- await fs_extra_1.default.ensureDir(projectRoot);
1037
- await runNpm(["install", `${name}@${version}`, "--save", "--save-exact", "--no-fund", "--no-audit", "--ignore-scripts"], projectRoot);
1038
- let entry;
1359
+ const hostId = (_a = Object.entries(mappingsOf(host)).find(([, value]) => value === name && dependenciesOf(host)[name] !== undefined)) === null || _a === void 0 ? void 0 : _a[0];
1360
+ const targetRoot = hostId ? packageRoot : projectRoot;
1361
+ if (hostId && isEssentialNpmPlugin(hostId))
1362
+ throw new Error(`The essential plugin "${hostId}" cannot be replaced while running.`);
1363
+ if (options.pluginId &&
1364
+ mappingsOf(host)[options.pluginId] &&
1365
+ dependenciesOf(host)[String(mappingsOf(host)[options.pluginId])] !== undefined &&
1366
+ mappingsOf(host)[options.pluginId] !== name)
1367
+ throw new Error(`BUNDLED:${options.pluginId}`);
1368
+ const original = readProjectManifest(targetRoot);
1369
+ const previous = dependenciesOf(original)[name];
1370
+ let previousVersion = previous;
1371
+ if (previous) {
1372
+ try {
1373
+ previousVersion = readPackageEntry(targetRoot, name).metadata.version || previous;
1374
+ }
1375
+ catch (_c) {
1376
+ /* Keep the declared spec if the previous package is incomplete. */
1377
+ }
1378
+ }
1379
+ const backups = new Map();
1380
+ for (const filename of ["package.json", "package-lock.json", "npm-shrinkwrap.json"]) {
1381
+ const target = path_1.default.join(targetRoot, filename);
1382
+ backups.set(target, fs_extra_1.default.existsSync(target) ? fs_extra_1.default.readFileSync(target) : null);
1383
+ }
1039
1384
  try {
1040
- entry = readPackageEntry(packageRoot, name);
1385
+ await runNpm([
1386
+ "--prefix",
1387
+ targetRoot,
1388
+ "install",
1389
+ `${name}@${version}`,
1390
+ "--save",
1391
+ "--save-exact",
1392
+ "--no-fund",
1393
+ "--no-audit",
1394
+ "--ignore-scripts"
1395
+ ], targetRoot);
1396
+ const entry = readPackageEntry(targetRoot, name);
1397
+ const pluginId = options.pluginId || stringValue((_b = objectValue(entry.metadata.elementspanel)) === null || _b === void 0 ? void 0 : _b.id);
1398
+ if (!PLUGIN_ID.test(pluginId))
1399
+ throw new Error(`Invalid plugin id in ${name}.`);
1400
+ if (mappingsOf(host)[pluginId] &&
1401
+ dependenciesOf(host)[String(mappingsOf(host)[pluginId])] !== undefined &&
1402
+ mappingsOf(host)[pluginId] !== name)
1403
+ throw new Error(`Plugin id ${pluginId} belongs to a bundled dependency.`);
1404
+ if (hostId && pluginId !== hostId)
1405
+ throw new Error(`The package ${name} changed its plugin id from ${hostId} to ${pluginId}.`);
1406
+ const mapped = mappingsOf(readProjectManifest(targetRoot))[pluginId];
1407
+ if (mapped !== undefined && mapped !== name)
1408
+ throw new Error(`Plugin id ${pluginId} is already installed as ${String(mapped)}.`);
1409
+ const record = recordFromPackage(pluginId, name, hostId ? "bundled" : "project", entry.root, entry.metadata, side);
1410
+ if (!record)
1411
+ throw new Error(`The package ${name} does not provide a ${side} plugin named ${pluginId}.`);
1412
+ if (/^[0-9]/.test(version) && record.version !== version)
1413
+ throw new Error(`The installed package ${name} is ${record.version}, expected ${version}.`);
1414
+ if (!process.env.EPANEL_PACKAGE_ROOT &&
1415
+ (0, plugin_manifest_1.discoverPlugins)(path_1.default.join(packageRoot, "plugins"), {
1416
+ entryFields: [],
1417
+ includeDisabled: true
1418
+ }).some((plugin) => plugin.manifest.id === pluginId))
1419
+ throw new Error(`Source plugin ${pluginId} cannot be replaced with an npm dependency.`);
1420
+ const manifest = (0, plugin_manifest_1.readPluginManifest)(record.directory);
1421
+ if (!manifest || manifest.id !== pluginId || manifest.version !== record.version)
1422
+ throw new Error(`The plugin manifest in ${name} does not match its package.`);
1423
+ (0, plugin_contract_1.validatePluginCompatibility)(manifest.elements);
1424
+ for (const field of ["backend", "frontend", "panel", "daemon", "main", "entry", "ui"]) {
1425
+ if (typeof manifest[field] === "string" &&
1426
+ !(0, plugin_manifest_1.resolvePluginEntry)(record.directory, manifest, { entryFields: [field] }))
1427
+ throw new Error(`Invalid ${field} entry in ${name}.`);
1428
+ }
1429
+ const others = listNpmPlugins(side, { packageRoot, projectRoot, skipInvalid: true });
1430
+ if (others.some((plugin) => plugin.pluginId === pluginId && plugin.name !== name))
1431
+ throw new Error(`Plugin id ${pluginId} is already provided by another dependency.`);
1432
+ const collision = others.find((plugin) => plugin.pluginId !== pluginId && plugin.folder === record.folder);
1433
+ if (collision)
1434
+ throw new Error(`Plugin asset directory ${record.folder} is already used by ${collision.name}.`);
1435
+ const updated = readProjectManifest(targetRoot);
1436
+ updated.dependencies = { ...dependenciesOf(updated), [name]: record.version };
1437
+ updated.elementspanel = {
1438
+ ...objectValue(updated.elementspanel),
1439
+ plugins: { ...mappingsOf(updated), [pluginId]: name }
1440
+ };
1441
+ writeProjectManifest(targetRoot, updated);
1442
+ return record;
1041
1443
  }
1042
1444
  catch (error) {
1043
- await runNpm(["uninstall", name, "--no-fund", "--no-audit", "--ignore-scripts"], projectRoot).catch(() => { });
1445
+ try {
1446
+ await runNpm(previous
1447
+ ? [
1448
+ "--prefix",
1449
+ targetRoot,
1450
+ "install",
1451
+ `${name}@${String(previousVersion)}`,
1452
+ "--save",
1453
+ "--no-fund",
1454
+ "--no-audit",
1455
+ "--ignore-scripts"
1456
+ ]
1457
+ : [
1458
+ "--prefix",
1459
+ targetRoot,
1460
+ "uninstall",
1461
+ name,
1462
+ "--no-fund",
1463
+ "--no-audit",
1464
+ "--ignore-scripts"
1465
+ ], targetRoot);
1466
+ }
1467
+ catch (rollback) {
1468
+ throw new Error(`${String(error)}; npm rollback failed: ${String(rollback)}`);
1469
+ }
1470
+ finally {
1471
+ for (const [filename, content] of backups) {
1472
+ if (content === null)
1473
+ await fs_extra_1.default.remove(filename);
1474
+ else
1475
+ await fs_extra_1.default.writeFile(filename, content);
1476
+ }
1477
+ }
1044
1478
  throw error;
1045
1479
  }
1046
- const record = recordFromPackage(pluginId, name, "project", entry.root, entry.metadata, side);
1047
- if (!record) {
1048
- await runNpm(["uninstall", name, "--no-fund", "--no-audit", "--ignore-scripts"], projectRoot).catch(() => { });
1049
- throw new Error(`The package ${name} does not provide a ${side} plugin named ${pluginId}.`);
1050
- }
1051
- if (record.version !== version) {
1052
- await runNpm(["uninstall", name, "--no-fund", "--no-audit", "--ignore-scripts"], projectRoot).catch(() => { });
1053
- throw new Error(`The installed package ${name} is ${record.version}, expected ${version}.`);
1054
- }
1055
- const manifest = readProjectManifest(projectRoot);
1056
- const dependencies = { ...dependenciesOf(manifest), [name]: version };
1057
- const elementspanel = { ...((_a = objectValue(manifest.elementspanel)) !== null && _a !== void 0 ? _a : {}) };
1058
- const plugins = { ...mappingsOf(manifest), [pluginId]: name };
1059
- manifest.dependencies = dependencies;
1060
- manifest.elementspanel = { ...elementspanel, plugins };
1061
- writeProjectManifest(projectRoot, manifest);
1062
- return record;
1063
1480
  });
1064
1481
  }
1065
- /** Removes one project-installed npm plugin. Built-in packages are left alone. */
1482
+ /** Remove the dependency from the manifest that owns it, preserving user configuration. */
1066
1483
  async function uninstallNpmPlugin(pluginId, options = {}) {
1067
1484
  if (!PLUGIN_ID.test(pluginId))
1068
1485
  throw new Error(`Invalid plugin id: ${pluginId}`);
1486
+ if (isEssentialNpmPlugin(pluginId))
1487
+ throw new Error(`The essential plugin "${pluginId}" cannot be uninstalled.`);
1069
1488
  const { packageRoot, projectRoot } = npmPluginRoots(options);
1070
1489
  return withProjectLock(projectRoot, async () => {
1071
- var _a;
1490
+ var _a, _b;
1072
1491
  const host = readJson(path_1.default.join(packageRoot, "package.json"));
1073
- if (host && mappingsOf(host)[pluginId] !== undefined)
1074
- throw new Error(`BUNDLED:${pluginId}`);
1075
- const manifest = readProjectManifest(projectRoot);
1076
- const name = mappingsOf(manifest)[pluginId];
1492
+ const bundledName = mappingsOf(host)[pluginId];
1493
+ const targetRoot = typeof bundledName === "string" && dependenciesOf(host)[bundledName] !== undefined
1494
+ ? packageRoot
1495
+ : projectRoot;
1496
+ const manifest = readProjectManifest(targetRoot);
1497
+ const name = (_a = mappingsOf(manifest)[pluginId]) !== null && _a !== void 0 ? _a : (_b = listNpmPlugins(undefined, { packageRoot, projectRoot, skipInvalid: true }).find((item) => item.pluginId === pluginId)) === null || _b === void 0 ? void 0 : _b.name;
1077
1498
  if (typeof name !== "string" || !PACKAGE_NAME.test(name))
1078
1499
  return null;
1079
1500
  const record = listNpmPlugins(undefined, { packageRoot, projectRoot, skipInvalid: true }).find((item) => item.pluginId === pluginId);
1080
- await runNpm(["uninstall", name, "--no-fund", "--no-audit", "--ignore-scripts"], projectRoot);
1081
- const plugins = { ...mappingsOf(manifest) };
1501
+ await runNpm(["--prefix", targetRoot, "uninstall", name, "--no-fund", "--no-audit", "--ignore-scripts"], targetRoot);
1502
+ const updated = readProjectManifest(targetRoot);
1503
+ const plugins = { ...mappingsOf(updated) };
1082
1504
  delete plugins[pluginId];
1083
- const elementspanel = { ...((_a = objectValue(manifest.elementspanel)) !== null && _a !== void 0 ? _a : {}) };
1084
- if (Object.keys(plugins).length)
1085
- elementspanel.plugins = plugins;
1086
- else
1087
- delete elementspanel.plugins;
1088
- manifest.elementspanel = elementspanel;
1089
- writeProjectManifest(projectRoot, manifest);
1505
+ updated.elementspanel = { ...objectValue(updated.elementspanel), plugins };
1506
+ writeProjectManifest(targetRoot, updated);
1090
1507
  return record !== null && record !== void 0 ? record : null;
1091
1508
  });
1092
1509
  }
@@ -1235,6 +1652,82 @@ function pluginDirectoryRevision(directory) {
1235
1652
  }
1236
1653
 
1237
1654
 
1655
+ /***/ },
1656
+
1657
+ /***/ "../common/src/plugin_watch.ts"
1658
+ (__unused_webpack_module, exports, __webpack_require__) {
1659
+
1660
+
1661
+ var __importDefault = (this && this.__importDefault) || function (mod) {
1662
+ return (mod && mod.__esModule) ? mod : { "default": mod };
1663
+ };
1664
+ Object.defineProperty(exports, "__esModule", ({ value: true }));
1665
+ exports.watchNpmPlugins = watchNpmPlugins;
1666
+ const node_fs_1 = __importDefault(__webpack_require__("node:fs"));
1667
+ const node_path_1 = __importDefault(__webpack_require__("node:path"));
1668
+ const plugin_npm_1 = __webpack_require__("../common/src/plugin_npm.ts");
1669
+ /** Poll package boundaries, allowing npm to finish replacing files before reconciliation. */
1670
+ function watchNpmPlugins(ctx, side, reload, changed) {
1671
+ const stamp = () => {
1672
+ const { packageRoot, projectRoot } = (0, plugin_npm_1.npmPluginRoots)();
1673
+ if (!node_fs_1.default.existsSync(node_path_1.default.join(packageRoot, "package.json")))
1674
+ return "";
1675
+ const files = [];
1676
+ for (const root of [packageRoot, projectRoot])
1677
+ for (const name of [
1678
+ "package.json",
1679
+ "package-lock.json",
1680
+ "npm-shrinkwrap.json",
1681
+ "node_modules/.package-lock.json"
1682
+ ])
1683
+ files.push(node_path_1.default.join(root, name));
1684
+ for (const plugin of (0, plugin_npm_1.listNpmPlugins)(side, { skipInvalid: true }))
1685
+ files.push(node_path_1.default.join(plugin.packageRoot, "package.json"), node_path_1.default.join(plugin.directory, "plugin.json"));
1686
+ return JSON.stringify(files.map((filename) => {
1687
+ try {
1688
+ const stat = node_fs_1.default.statSync(filename);
1689
+ return [filename, stat.size, stat.mtimeMs, stat.ctimeMs];
1690
+ }
1691
+ catch (_a) {
1692
+ return [filename, null];
1693
+ }
1694
+ }));
1695
+ };
1696
+ let applied = stamp();
1697
+ let candidate = applied;
1698
+ let busy = false;
1699
+ ctx.setInterval(() => {
1700
+ if (busy || (0, plugin_npm_1.npmPluginOperationPending)())
1701
+ return;
1702
+ try {
1703
+ const next = stamp();
1704
+ if (next === applied) {
1705
+ candidate = next;
1706
+ return;
1707
+ }
1708
+ if (next !== candidate) {
1709
+ candidate = next;
1710
+ return;
1711
+ }
1712
+ busy = true;
1713
+ void reload()
1714
+ .then(() => {
1715
+ applied = next;
1716
+ candidate = next;
1717
+ changed === null || changed === void 0 ? void 0 : changed();
1718
+ })
1719
+ .catch((error) => ctx.logger.warn("Failed to synchronize npm plugins", error))
1720
+ .finally(() => {
1721
+ busy = false;
1722
+ });
1723
+ }
1724
+ catch (error) {
1725
+ ctx.logger.warn("Failed to inspect npm plugins", error);
1726
+ }
1727
+ }, 2000);
1728
+ }
1729
+
1730
+
1238
1731
  /***/ },
1239
1732
 
1240
1733
  /***/ "../common/src/process_tools.ts"
@@ -1547,6 +2040,9 @@ function npmPluginRoots(side) {
1547
2040
  const { resolveNpmPlugins } = runtimeRequire(resolver);
1548
2041
  return resolveNpmPlugins(packagePath(), side, { skipInvalid: true }).map((record) => ({
1549
2042
  directory: record.directory,
2043
+ packageRoot: record.packageRoot,
2044
+ packageName: record.name,
2045
+ npmSource: record.source,
1550
2046
  ...(record.folder ? { folder: record.folder } : {}),
1551
2047
  // Built-in packages are part of the runtime's default configuration. A
1552
2048
  // project dependency is installed but remains dormant until the user
@@ -2216,11 +2712,10 @@ async function apply(ctx) {
2216
2712
  /**
2217
2713
  * Asks the daemons the user picked to re-scan their plugin directories.
2218
2714
  *
2219
- * A daemon binds its protocol handlers onto each socket as that socket
2220
- * connects, so the node is reconnected afterwards: without that, a handler
2221
- * that has just appeared stays invisible to the connection the panel holds.
2715
+ * Current daemons dispatch through their live registry. Reconnecting also
2716
+ * keeps this panel compatible with older daemons that bind routes per socket.
2222
2717
  */
2223
- async function reloadNodes(daemonIds) {
2718
+ async function reloadNodes(daemonIds, pluginId) {
2224
2719
  let reloaded = true;
2225
2720
  for (const daemonId of daemonIds) {
2226
2721
  const node = ctx.remote.services.getInstance(daemonId);
@@ -2229,7 +2724,10 @@ async function apply(ctx) {
2229
2724
  continue;
2230
2725
  }
2231
2726
  try {
2232
- await new ctx.remote.Request(node).request("plugin/reload");
2727
+ const inventory = await new ctx.remote.Request(node).request("plugin/reload");
2728
+ if (Array.isArray(inventory) &&
2729
+ inventory.some((plugin) => plugin.id === pluginId && plugin.state === "restart-required"))
2730
+ reloaded = false;
2233
2731
  node.refreshReconnect();
2234
2732
  }
2235
2733
  catch (error) {
@@ -2246,18 +2744,21 @@ async function apply(ctx) {
2246
2744
  *
2247
2745
  * Both halves re-scan after the dependency is installed: the panel scans its
2248
2746
  * own npm plugins, and the selected daemons scan theirs before reconnecting.
2249
- * Loaders accept new npm plugins in production, but retain an already running
2250
- * backend revision; replacing that revision still requires restart.
2747
+ * Loaders dispose the previous revision before activating its replacement.
2251
2748
  */
2252
- async function hotReload(sides, daemonIds = []) {
2749
+ async function hotReload(sides, daemonIds = [], pluginId) {
2253
2750
  if (!sides.length)
2254
2751
  return true;
2255
2752
  try {
2256
2753
  if (sides.includes("panel"))
2257
2754
  await ctx.plugins.reload();
2755
+ const panelReady = !sides.includes("panel") ||
2756
+ !ctx.plugins
2757
+ .inventory()
2758
+ .some((plugin) => plugin.id === pluginId && plugin.state === "restart-required");
2258
2759
  if (sides.includes("daemon"))
2259
- return await reloadNodes(daemonIds);
2260
- return true;
2760
+ return (await reloadNodes(daemonIds, pluginId)) && panelReady;
2761
+ return panelReady;
2261
2762
  }
2262
2763
  catch (error) {
2263
2764
  ctx.logger.warn(`Failed to reload plugins after a market install: ${error}`);
@@ -2331,8 +2832,7 @@ async function apply(ctx) {
2331
2832
  });
2332
2833
  // Installing adds a dependency to this installation's package.json and sends the
2333
2834
  // same npm install to the nodes the user picked. Both sides are asked to re-scan
2334
- // after the dependency arrives. Existing backend revisions are not replaced
2335
- // in-process.
2835
+ // after the dependency arrives and replace the running plugin revision.
2336
2836
  router.post("/plugin/install", requireAdmin, ctx.middleware.speedLimit(3), ctx.middleware.validator({ body: { pluginId: String, name: String } }), async (requestCtx) => {
2337
2837
  var _a;
2338
2838
  const body = ((_a = requestCtx.request.body) !== null && _a !== void 0 ? _a : {});
@@ -2350,17 +2850,17 @@ async function apply(ctx) {
2350
2850
  version: detail.selectedVersion.version,
2351
2851
  installedAt: Date.now()
2352
2852
  };
2353
- const previous = (0, plugin_market_1.listInstalled)().find((item) => item.pluginId === info.pluginId);
2354
- // A source or bundled plugin that ships with the runtime already owns the
2355
- // id; installing the npm package would be shadowed by it.
2853
+ // Core services and source plugins cannot be replaced while running.
2356
2854
  if (sides.includes("panel") &&
2357
- (previous === null || previous === void 0 ? void 0 : previous.source) !== "project" &&
2358
- ctx.plugins.inventory().some((item) => item.id === info.pluginId))
2855
+ ctx.plugins.inventory().some((item) => item.id === info.pluginId && !item.removable))
2359
2856
  throw new plugin_market_1.PluginMarketError("BUNDLED");
2360
- const replacedVersion = Boolean(previous && previous.version !== info.version);
2361
2857
  const changedSides = [];
2362
2858
  if (sides.includes("panel")) {
2363
- await (0, plugin_market_1.installPanelPlugin)(detail);
2859
+ await ctx.plugins.install({
2860
+ name: detail.name,
2861
+ pluginId: detail.id,
2862
+ version: detail.selectedVersion.version
2863
+ });
2364
2864
  changedSides.push("panel");
2365
2865
  }
2366
2866
  const { failedNodes, changedNodes } = sides.includes("daemon") && daemonIds.length
@@ -2368,10 +2868,9 @@ async function apply(ctx) {
2368
2868
  : { failedNodes: [], changedNodes: [] };
2369
2869
  if (changedNodes.length)
2370
2870
  changedSides.push("daemon");
2371
- const reloaded = await hotReload(changedSides, changedNodes);
2871
+ const reloaded = await hotReload(changedSides, changedNodes, info.pluginId);
2372
2872
  return {
2373
- restartRequired: ((changedSides.includes("panel") || changedNodes.length > 0) && replacedVersion) ||
2374
- !reloaded,
2873
+ restartRequired: !reloaded,
2375
2874
  installedVersion: (_a = (0, plugin_market_1.listInstalled)().find((item) => item.pluginId === info.pluginId)) === null || _a === void 0 ? void 0 : _a.version,
2376
2875
  failedNodes
2377
2876
  };
@@ -2388,7 +2887,10 @@ async function apply(ctx) {
2388
2887
  requestCtx.body = await withPluginLock(pluginId, async () => {
2389
2888
  const selectedNodes = new Set(daemonIds);
2390
2889
  const installations = (0, plugin_market_1.readRemoteInstalls)(pluginId).filter((item) => selectedNodes.has(item.daemonId));
2391
- const removed = await (0, plugin_market_1.uninstallPanelPlugin)(pluginId);
2890
+ const local = ctx.plugins.inventory().find((plugin) => plugin.id === pluginId);
2891
+ if (local)
2892
+ await ctx.plugins.remove(pluginId);
2893
+ const removed = Boolean(local);
2392
2894
  const { failedNodes, changedNodes } = await removeFromNodes(installations);
2393
2895
  const sides = removed ? ["panel"] : [];
2394
2896
  if (changedNodes.length)
@@ -2398,7 +2900,7 @@ async function apply(ctx) {
2398
2900
  removed: !remaining,
2399
2901
  installedVersion: remaining === null || remaining === void 0 ? void 0 : remaining.version,
2400
2902
  failedNodes,
2401
- restartRequired: !(await hotReload(sides, changedNodes))
2903
+ restartRequired: !(await hotReload(sides, changedNodes, pluginId))
2402
2904
  };
2403
2905
  });
2404
2906
  }
@@ -2408,7 +2910,12 @@ async function apply(ctx) {
2408
2910
  });
2409
2911
  ctx.settingsForm.declare({
2410
2912
  fields: () => [
2411
- { key: "pluginMarketAddr", type: "string", title: ctx.i18n.$t("TXT_CODE_PLUGIN_MARKET_ADDR"), description: ctx.i18n.$t("TXT_CODE_PLUGIN_MARKET_ADDR_DESC") },
2913
+ {
2914
+ key: "pluginMarketAddr",
2915
+ type: "string",
2916
+ title: ctx.i18n.$t("TXT_CODE_PLUGIN_MARKET_ADDR"),
2917
+ description: ctx.i18n.$t("TXT_CODE_PLUGIN_MARKET_ADDR_DESC")
2918
+ },
2412
2919
  { type: "link", title: ctx.i18n.$t("TXT_CODE_PLUGIN_MARKET"), route: "/market/plugins" }
2413
2920
  ],
2414
2921
  read: () => ({ ...(0, market_settings_1.marketSettings)() }),
@@ -2504,8 +3011,6 @@ exports.recordRemoteInstall = recordRemoteInstall;
2504
3011
  exports.forgetRemoteInstall = forgetRemoteInstall;
2505
3012
  exports.listInstalled = listInstalled;
2506
3013
  exports.fetchMarketPluginDetail = fetchMarketPluginDetail;
2507
- exports.installPanelPlugin = installPanelPlugin;
2508
- exports.uninstallPanelPlugin = uninstallPanelPlugin;
2509
3014
  const crypto_1 = __webpack_require__("crypto");
2510
3015
  const path_1 = __importDefault(__webpack_require__("path"));
2511
3016
  const axios_1 = __importDefault(__webpack_require__("./node_modules/axios/dist/node/axios.cjs"));
@@ -2606,7 +3111,7 @@ function localRecord(record) {
2606
3111
  directories: [record.directory],
2607
3112
  daemonIds: [],
2608
3113
  source: record.source,
2609
- removable: record.source === "project"
3114
+ removable: !(0, mcsmanager_common_1.isEssentialNpmPlugin)(record.pluginId)
2610
3115
  };
2611
3116
  }
2612
3117
  /** Local npm dependencies, both bundled and project-installed, plus node records. */
@@ -2656,7 +3161,9 @@ function listInstalled() {
2656
3161
  function sidesOf(value) {
2657
3162
  if (!Array.isArray(value))
2658
3163
  return [];
2659
- return [...new Set(value.filter((item) => item === "panel" || item === "daemon"))];
3164
+ return [
3165
+ ...new Set(value.filter((item) => item === "panel" || item === "daemon"))
3166
+ ];
2660
3167
  }
2661
3168
  /**
2662
3169
  * Reads the release the page is about to install. The market's catalogue is the
@@ -2687,33 +3194,6 @@ async function fetchMarketPluginDetail(addr, pluginId, version) {
2687
3194
  }
2688
3195
  };
2689
3196
  }
2690
- /** Adds one npm package to this installation's dependencies. */
2691
- async function installPanelPlugin(detail) {
2692
- try {
2693
- return await (0, mcsmanager_common_1.installNpmPlugin)({
2694
- name: detail.name,
2695
- pluginId: detail.id,
2696
- version: detail.selectedVersion.version,
2697
- side: "panel"
2698
- });
2699
- }
2700
- catch (error) {
2701
- if (String(error === null || error === void 0 ? void 0 : error.message).startsWith("BUNDLED:"))
2702
- throw new PluginMarketError("BUNDLED");
2703
- throw error;
2704
- }
2705
- }
2706
- /** Removes one project-installed npm plugin from this installation. */
2707
- async function uninstallPanelPlugin(pluginId) {
2708
- try {
2709
- return await (0, mcsmanager_common_1.uninstallNpmPlugin)(pluginId);
2710
- }
2711
- catch (error) {
2712
- if (String(error === null || error === void 0 ? void 0 : error.message).startsWith("BUNDLED:"))
2713
- throw new PluginMarketError("BUNDLED");
2714
- throw error;
2715
- }
2716
- }
2717
3197
 
2718
3198
 
2719
3199
  /***/ },
@@ -2879,6 +3359,13 @@ module.exports = require("node:fs");
2879
3359
 
2880
3360
  /***/ },
2881
3361
 
3362
+ /***/ "node:module"
3363
+ (module) {
3364
+
3365
+ module.exports = require("node:module");
3366
+
3367
+ /***/ },
3368
+
2882
3369
  /***/ "node:os"
2883
3370
  (module) {
2884
3371
 
@@ -2893,6 +3380,13 @@ module.exports = require("node:path");
2893
3380
 
2894
3381
  /***/ },
2895
3382
 
3383
+ /***/ "node:url"
3384
+ (module) {
3385
+
3386
+ module.exports = require("node:url");
3387
+
3388
+ /***/ },
3389
+
2896
3390
  /***/ "os"
2897
3391
  (module) {
2898
3392