@elementspanel/panel 0.0.0-stage → 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.
Files changed (139) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +6 -2
  3. package/app.js +6400 -0
  4. package/app.js.map +1 -0
  5. package/bin/panel.cjs +2 -0
  6. package/package.json +107 -4
  7. package/public/assets/AppDialog.vue_vue_type_style_index_0_lang-e6e38e8b.js +2 -0
  8. package/public/assets/AppDialog.vue_vue_type_style_index_0_lang-e6e38e8b.js.map +1 -0
  9. package/public/assets/CardPanel-59b7da2e.js +2 -0
  10. package/public/assets/CardPanel-59b7da2e.js.map +1 -0
  11. package/public/assets/DesktopDialog-e4041e4e.js +2 -0
  12. package/public/assets/DesktopDialog-e4041e4e.js.map +1 -0
  13. package/public/assets/Loading.vue_vue_type_script_setup_true_lang-1b74dd54.js +2 -0
  14. package/public/assets/Loading.vue_vue_type_script_setup_true_lang-1b74dd54.js.map +1 -0
  15. package/public/assets/PageToolbar-7130e13d.js +2 -0
  16. package/public/assets/PageToolbar-7130e13d.js.map +1 -0
  17. package/public/assets/_elements-sdk_cordis-b0b88a2c.js +2 -0
  18. package/public/assets/_elements-sdk_cordis-b0b88a2c.js.map +1 -0
  19. package/public/assets/_elements-sdk_cordis-e3bebc89.js +9 -0
  20. package/public/assets/_elements-sdk_cordis-e3bebc89.js.map +1 -0
  21. package/public/assets/_elements-sdk_cosmokit-e4842f5c.js +2 -0
  22. package/public/assets/_elements-sdk_cosmokit-e4842f5c.js.map +1 -0
  23. package/public/assets/_elements-sdk_pinia-2c56d2c1.js +6 -0
  24. package/public/assets/_elements-sdk_pinia-2c56d2c1.js.map +1 -0
  25. package/public/assets/_elements-sdk_vue-80f42d87.js +2 -0
  26. package/public/assets/_elements-sdk_vue-80f42d87.js.map +1 -0
  27. package/public/assets/_elements-sdk_vue-i18n-4b8a106a.js +2 -0
  28. package/public/assets/_elements-sdk_vue-i18n-4b8a106a.js.map +1 -0
  29. package/public/assets/_elements-sdk_vue-router-a171591a.js +2 -0
  30. package/public/assets/_elements-sdk_vue-router-a171591a.js.map +1 -0
  31. package/public/assets/_plugin-vue_export-helper-c27b6911.js +2 -0
  32. package/public/assets/_plugin-vue_export-helper-c27b6911.js.map +1 -0
  33. package/public/assets/chartjs-1e331dfb.js +19 -0
  34. package/public/assets/chartjs-1e331dfb.js.map +1 -0
  35. package/public/assets/codemirror-1b74112a.js +29 -0
  36. package/public/assets/codemirror-1b74112a.js.map +1 -0
  37. package/public/assets/const-93b324c0.js +2 -0
  38. package/public/assets/const-93b324c0.js.map +1 -0
  39. package/public/assets/const-ea30795c.js +38 -0
  40. package/public/assets/const-ea30795c.js.map +1 -0
  41. package/public/assets/context-cc90576e.js +2 -0
  42. package/public/assets/context-cc90576e.js.map +1 -0
  43. package/public/assets/copy-b106bb10.js +2 -0
  44. package/public/assets/copy-b106bb10.js.map +1 -0
  45. package/public/assets/core-d3bfdede.js +2 -0
  46. package/public/assets/core-d3bfdede.js.map +1 -0
  47. package/public/assets/daemon_connection_error-9eb654cc.png +0 -0
  48. package/public/assets/dayjs.min-25183c0a.js +2 -0
  49. package/public/assets/dayjs.min-25183c0a.js.map +1 -0
  50. package/public/assets/desktopNotice-5ca3448c.js +2 -0
  51. package/public/assets/desktopNotice-5ca3448c.js.map +1 -0
  52. package/public/assets/dom-ff483262.js +2 -0
  53. package/public/assets/dom-ff483262.js.map +1 -0
  54. package/public/assets/htmlparser2-bcb8f9db.js +3 -0
  55. package/public/assets/htmlparser2-bcb8f9db.js.map +1 -0
  56. package/public/assets/i18n-e810b9fa.js +2 -0
  57. package/public/assets/i18n-e810b9fa.js.map +1 -0
  58. package/public/assets/index-6e8d74f8.js +2 -0
  59. package/public/assets/index-6e8d74f8.js.map +1 -0
  60. package/public/assets/index-c684efbd.js +2 -0
  61. package/public/assets/index-c684efbd.js.map +1 -0
  62. package/public/assets/index-e3f35709.js +2 -0
  63. package/public/assets/index-e3f35709.js.map +1 -0
  64. package/public/assets/instance-cc59e210.js +2 -0
  65. package/public/assets/instance-cc59e210.js.map +1 -0
  66. package/public/assets/instance_contract-0115efe5.js +2 -0
  67. package/public/assets/instance_contract-0115efe5.js.map +1 -0
  68. package/public/assets/lodash-001e4f93.js +28 -0
  69. package/public/assets/lodash-001e4f93.js.map +1 -0
  70. package/public/assets/logo-9e2a668b.svg +15 -0
  71. package/public/assets/logo_b-35ccadda.svg +8 -0
  72. package/public/assets/materialdesignicons-webfont-0b183104.eot +0 -0
  73. package/public/assets/materialdesignicons-webfont-61e8aba5.ttf +0 -0
  74. package/public/assets/materialdesignicons-webfont-662fefa8.woff2 +0 -0
  75. package/public/assets/materialdesignicons-webfont-a5928a0d.woff +0 -0
  76. package/public/assets/msl-logo-699c8076.png +0 -0
  77. package/public/assets/nodes-a56b1b35.js +2 -0
  78. package/public/assets/nodes-a56b1b35.js.map +1 -0
  79. package/public/assets/plugin_contract-691cd37a.js +2 -0
  80. package/public/assets/plugin_contract-691cd37a.js.map +1 -0
  81. package/public/assets/preload-helper-a4192956.js +2 -0
  82. package/public/assets/preload-helper-a4192956.js.map +1 -0
  83. package/public/assets/protocol-97f7bab7.js +2 -0
  84. package/public/assets/protocol-97f7bab7.js.map +1 -0
  85. package/public/assets/randId-56f93901.js +2 -0
  86. package/public/assets/randId-56f93901.js.map +1 -0
  87. package/public/assets/router-eff205cc.js +3 -0
  88. package/public/assets/router-eff205cc.js.map +1 -0
  89. package/public/assets/safe-87846d89.js +89 -0
  90. package/public/assets/safe-87846d89.js.map +1 -0
  91. package/public/assets/sdk-3f2277ab.js +2 -0
  92. package/public/assets/sdk-3f2277ab.js.map +1 -0
  93. package/public/assets/shared-597ef52b.js +2 -0
  94. package/public/assets/shared-597ef52b.js.map +1 -0
  95. package/public/assets/useAppConfigStore-352fe033.js +2 -0
  96. package/public/assets/useAppConfigStore-352fe033.js.map +1 -0
  97. package/public/assets/useAppRouters-747b627a.js +2 -0
  98. package/public/assets/useAppRouters-747b627a.js.map +1 -0
  99. package/public/assets/useAppStateStore-a438b9a6.js +2 -0
  100. package/public/assets/useAppStateStore-a438b9a6.js.map +1 -0
  101. package/public/assets/useAppToolsStore-288c4fe8.js +2 -0
  102. package/public/assets/useAppToolsStore-288c4fe8.js.map +1 -0
  103. package/public/assets/useDefineApi-b726db1e.js +7 -0
  104. package/public/assets/useDefineApi-b726db1e.js.map +1 -0
  105. package/public/assets/useInstanceActionAvailability-16de3001.js +2 -0
  106. package/public/assets/useInstanceActionAvailability-16de3001.js.map +1 -0
  107. package/public/assets/useKeyboardEvents-6f7f845c.js +2 -0
  108. package/public/assets/useKeyboardEvents-6f7f845c.js.map +1 -0
  109. package/public/assets/useMountComponent-4043cb4c.js +2 -0
  110. package/public/assets/useMountComponent-4043cb4c.js.map +1 -0
  111. package/public/assets/useOverviewChart-7d758bc4.js +2 -0
  112. package/public/assets/useOverviewChart-7d758bc4.js.map +1 -0
  113. package/public/assets/useOverviewInfo-9ca25ab9.js +2 -0
  114. package/public/assets/useOverviewInfo-9ca25ab9.js.map +1 -0
  115. package/public/assets/usePolling-1bc2009b.js +2 -0
  116. package/public/assets/usePolling-1bc2009b.js.map +1 -0
  117. package/public/assets/useScreen-fad29b7c.js +2 -0
  118. package/public/assets/useScreen-fad29b7c.js.map +1 -0
  119. package/public/assets/useSocketIo-ea3c9a61.js +2 -0
  120. package/public/assets/useSocketIo-ea3c9a61.js.map +1 -0
  121. package/public/assets/useThemeSwitch-dc70883c.js +2 -0
  122. package/public/assets/useThemeSwitch-dc70883c.js.map +1 -0
  123. package/public/assets/vue-4c6a2976.css +1 -0
  124. package/public/assets/vue-6b52a630.js +64 -0
  125. package/public/assets/vue-6b52a630.js.map +1 -0
  126. package/public/assets/vuetifyModal-8382b175.js +2 -0
  127. package/public/assets/vuetifyModal-8382b175.js.map +1 -0
  128. package/public/assets/xterm-62ba073f.js +67 -0
  129. package/public/assets/xterm-62ba073f.js.map +1 -0
  130. package/public/favicon.ico +0 -0
  131. package/public/index.html +207 -0
  132. package/public/robots.txt +2 -0
  133. package/public/stats.html +4950 -0
  134. package/runtime/cli.cjs +305 -0
  135. package/runtime/daemon-tools.cjs +113 -0
  136. package/runtime/npm-plugins.cjs +207 -0
  137. package/runtime/plugin-resolution.cjs +141 -0
  138. package/runtime/shared-dependencies.cjs +35 -0
  139. package/runtime/worker.cjs +25 -0
@@ -0,0 +1,305 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const { createRequire, registerHooks } = require("node:module");
4
+ const { resolveNpmPlugins } = require("./npm-plugins.cjs");
5
+ const { ensureRuntimeTools } = require("./daemon-tools.cjs");
6
+ const { fork } = require("node:child_process");
7
+
8
+ function parseArgs(argv, side) {
9
+ const options = { command: "start", withDaemon: false, open: false };
10
+ if (argv[0] && !argv[0].startsWith("-")) options.command = argv.shift();
11
+ for (let index = 0; index < argv.length; index++) {
12
+ const argument = argv[index];
13
+ if (["--help", "-h"].includes(argument)) options.command = "help";
14
+ else if (["--version", "-v"].includes(argument)) options.command = "version";
15
+ else if (argument === "--with-daemon") options.withDaemon = true;
16
+ else if (argument === "--open") options.open = true;
17
+ else if (["--cwd", "--panel-dir", "--daemon-dir"].includes(argument)) {
18
+ const value = argv[++index];
19
+ if (!value || value.startsWith("--")) throw new Error(`Missing value for ${argument}.`);
20
+ options[argument.slice(2)] = value;
21
+ } else throw new Error(`Unknown option: ${argument}`);
22
+ }
23
+ if (!["start", "help", "version"].includes(options.command))
24
+ throw new Error(`Unknown command: ${options.command}`);
25
+ if (side === "daemon" && (options.withDaemon || options.open || options["panel-dir"])) {
26
+ throw new Error("--with-daemon, --open and --panel-dir belong to the panel command.");
27
+ }
28
+ return options;
29
+ }
30
+
31
+ function help(side) {
32
+ return `Usage: ${side === "panel" ? "epanel" : "epanel-daemon"} start [options]
33
+
34
+ --cwd <directory> Project directory (default: current directory)
35
+ --${side}-dir <directory> Existing runtime directory (default: ./${side})
36
+ ${
37
+ side === "panel"
38
+ ? " --with-daemon Start the local daemon before the panel\n --daemon-dir <directory> Local daemon directory (default: ./daemon)\n --open Open the panel in a browser on Windows\n"
39
+ : ""
40
+ } --help Show this help
41
+ --version Show the installed package version
42
+ `;
43
+ }
44
+
45
+ function runtimeDirectory(project, override, side) {
46
+ return path.resolve(project, override || side);
47
+ }
48
+
49
+ function workerEnvironment(plan, worker) {
50
+ const environment = { ...process.env, EPANEL_PROJECT_ROOT: plan.project };
51
+ delete environment.EPANEL_PACKAGE_ROOT;
52
+ delete environment.EPANEL_LOCAL_DAEMON_CONFIG;
53
+ if (worker.side === "panel" && plan.localDaemonConfig)
54
+ environment.EPANEL_LOCAL_DAEMON_CONFIG = plan.localDaemonConfig;
55
+
56
+ // fork() pipes worker output so the parent can add a side prefix. That makes
57
+ // supports-color think stdout is not a terminal, even when npm start is
58
+ // running in one. Preserve explicit color settings and otherwise propagate
59
+ // the parent's terminal capability to the worker.
60
+ if (
61
+ environment.FORCE_COLOR === undefined &&
62
+ environment.NO_COLOR === undefined &&
63
+ (process.stdout.isTTY || process.stderr.isTTY)
64
+ ) {
65
+ environment.FORCE_COLOR = "3";
66
+ }
67
+ return environment;
68
+ }
69
+
70
+ function workerPrefix(side, target) {
71
+ const color = side === "daemon" ? "\u001b[36m" : "\u001b[32m";
72
+ const forceColor = process.env.FORCE_COLOR;
73
+ const colored =
74
+ process.env.NO_COLOR === undefined &&
75
+ (target.isTTY ||
76
+ (forceColor !== undefined && forceColor !== "0" && forceColor !== "false"));
77
+ return colored ? `${color}[${side}]\u001b[39m ` : `[${side}] `;
78
+ }
79
+
80
+ /** Prefix worker output without breaking lines split across child-process chunks. */
81
+ function pipeWorkerOutput(stream, target, side) {
82
+ let pending = "";
83
+ const prefix = workerPrefix(side, target);
84
+ const write = (chunk) => {
85
+ pending += chunk.toString();
86
+ const lines = pending.split("\n");
87
+ pending = lines.pop() || "";
88
+ for (const line of lines) target.write(`${prefix}${line.replace(/\r$/, "")}\n`);
89
+ };
90
+ const flush = () => {
91
+ if (!pending) return;
92
+ target.write(`${prefix}${pending.replace(/\r$/, "")}\n`);
93
+ pending = "";
94
+ };
95
+ stream.on("data", write);
96
+ stream.once("end", flush);
97
+ return flush;
98
+ }
99
+
100
+ function validateRuntime(packageRoot, side) {
101
+ for (const item of ["app.js", "runtime/worker.cjs"]) {
102
+ if (!fs.existsSync(path.join(packageRoot, item)))
103
+ throw new Error(`Incomplete ${side} package: missing ${item}.`);
104
+ }
105
+ const plugins = resolveNpmPlugins(packageRoot, side, { skipInvalid: true });
106
+ for (const id of [
107
+ "i18n",
108
+ "storage",
109
+ "runtime",
110
+ "server",
111
+ ...(side === "panel" ? ["console"] : [])
112
+ ]) {
113
+ if (!plugins.some((plugin) => plugin.id === id))
114
+ throw new Error(`Incomplete ${side} installation: missing npm plugin ${id}.`);
115
+ }
116
+ if (side === "panel" && !fs.existsSync(path.join(packageRoot, "public/index.html"))) {
117
+ throw new Error("Incomplete panel package: missing public/index.html.");
118
+ }
119
+ }
120
+
121
+ function planWorkers(side, packageRoot, options) {
122
+ const project = path.resolve(options.cwd || process.cwd());
123
+ const panelDirectory = runtimeDirectory(project, options["panel-dir"], "panel");
124
+ const daemonDirectory = runtimeDirectory(project, options["daemon-dir"], "daemon");
125
+ const workers = [];
126
+ if (side === "panel" && options.withDaemon) {
127
+ let daemonRoot;
128
+ try {
129
+ const projectRequire = createRequire(path.join(project, "package.json"));
130
+ daemonRoot = path.dirname(projectRequire.resolve("@elementspanel/daemon/package.json"));
131
+ } catch {
132
+ throw new Error(
133
+ "The project needs @elementspanel/daemon. Run npm install @elementspanel/daemon or start without --with-daemon."
134
+ );
135
+ }
136
+ if (panelDirectory === daemonDirectory)
137
+ throw new Error("Panel and daemon need different runtime directories.");
138
+ workers.push({ side: "daemon", packageRoot: daemonRoot, directory: daemonDirectory });
139
+ }
140
+ workers.push({
141
+ side,
142
+ packageRoot,
143
+ directory: side === "panel" ? panelDirectory : daemonDirectory
144
+ });
145
+ for (const worker of workers) validateRuntime(worker.packageRoot, worker.side);
146
+ return {
147
+ workers,
148
+ project,
149
+ localDaemonConfig: options.withDaemon
150
+ ? path.join(daemonDirectory, "data/Config/global.json")
151
+ : undefined
152
+ };
153
+ }
154
+
155
+ /** Supervise foreground workers. No shells, detached children or background servers. */
156
+ async function startWorkers(plan, options = {}) {
157
+ if (!plan.workers.length) throw new Error("No runtime was selected.");
158
+ const startupTimeout = options.startupTimeout ?? 120_000;
159
+ const shutdownTimeout = options.shutdownTimeout ?? 90_000;
160
+ const children = new Set();
161
+ let stopping = false;
162
+ let exitCode = 0;
163
+ let forceTimer;
164
+ let done;
165
+ const finished = new Promise((resolve) => {
166
+ done = resolve;
167
+ });
168
+ const finish = () => {
169
+ if (!stopping || children.size) return;
170
+ clearTimeout(forceTimer);
171
+ done(exitCode);
172
+ };
173
+ const forceStop = () => {
174
+ for (const child of children) child.kill("SIGKILL");
175
+ };
176
+ const stop = (code = 0) => {
177
+ if (stopping) return;
178
+ stopping = true;
179
+ exitCode = code;
180
+ for (const child of children) {
181
+ if (child.connected)
182
+ child.send({ type: "elementspanel:stop" }, (error) => {
183
+ if (error && children.has(child)) child.kill("SIGTERM");
184
+ });
185
+ else child.kill("SIGTERM");
186
+ }
187
+ forceTimer = setTimeout(forceStop, shutdownTimeout);
188
+ forceTimer.unref();
189
+ finish();
190
+ };
191
+ const onSignal = () => (stopping ? forceStop() : stop(0));
192
+ const onInput = (data) => {
193
+ if (data.toString().trim().toLowerCase() === "exit") stop(0);
194
+ };
195
+ process.on("SIGINT", onSignal);
196
+ process.on("SIGTERM", onSignal);
197
+ process.stdin.on("data", onInput);
198
+ process.stdin.resume();
199
+
200
+ try {
201
+ for (const worker of plan.workers) {
202
+ if (stopping) break;
203
+ fs.mkdirSync(worker.directory, { recursive: true });
204
+ if (worker.side === "daemon") {
205
+ await ensureRuntimeTools(worker.directory, {
206
+ onDownload: (spec) =>
207
+ console.log(`${workerPrefix("daemon", process.stdout)}Downloading ${spec.filename}...`)
208
+ });
209
+ }
210
+ const environment = workerEnvironment(plan, worker);
211
+ const child = fork(
212
+ path.join(worker.packageRoot, "runtime/worker.cjs"),
213
+ options.open && worker.side === "panel" ? ["--open"] : [],
214
+ {
215
+ cwd: worker.directory,
216
+ env: environment,
217
+ execArgv: ["--enable-source-maps"],
218
+ stdio: ["ignore", "pipe", "pipe", "ipc"]
219
+ }
220
+ );
221
+ const flushOutput = [
222
+ pipeWorkerOutput(child.stdout, process.stdout, worker.side),
223
+ pipeWorkerOutput(child.stderr, process.stderr, worker.side)
224
+ ];
225
+ children.add(child);
226
+ let rejectReady;
227
+ child.on("error", (error) => {
228
+ if (!stopping) console.error(error.message);
229
+ rejectReady?.(error);
230
+ stop(1);
231
+ });
232
+ child.once("close", (code, signal) => {
233
+ flushOutput.forEach((flush) => flush());
234
+ children.delete(child);
235
+ const starting = Boolean(rejectReady);
236
+ const error = new Error(`${worker.side} exited before becoming ready (${signal || code}).`);
237
+ if (starting && !stopping) console.error(error.message);
238
+ rejectReady?.(error);
239
+ if (!stopping) stop(starting || code !== 0 ? 1 : 0);
240
+ finish();
241
+ });
242
+ await new Promise((resolve, reject) => {
243
+ const timer = setTimeout(
244
+ () =>
245
+ settle(
246
+ new Error(
247
+ `${worker.side} did not become ready within ${startupTimeout / 1000} seconds.`
248
+ )
249
+ ),
250
+ startupTimeout
251
+ );
252
+ const onMessage = (message) => {
253
+ if (message?.type === "elementspanel:ready") settle();
254
+ };
255
+ const settle = (error) => {
256
+ clearTimeout(timer);
257
+ child.off("message", onMessage);
258
+ rejectReady = undefined;
259
+ if (error) reject(error);
260
+ else resolve();
261
+ };
262
+ rejectReady = settle;
263
+ child.on("message", onMessage);
264
+ });
265
+ }
266
+ } catch (error) {
267
+ if (!stopping) {
268
+ console.error(error.message);
269
+ stop(1);
270
+ }
271
+ }
272
+ try {
273
+ return await finished;
274
+ } finally {
275
+ clearTimeout(forceTimer);
276
+ process.off("SIGINT", onSignal);
277
+ process.off("SIGTERM", onSignal);
278
+ process.stdin.off("data", onInput);
279
+ process.stdin.pause();
280
+ }
281
+ }
282
+
283
+ async function run(side, packageRoot, argv = process.argv.slice(2)) {
284
+ try {
285
+ const options = parseArgs([...argv], side);
286
+ if (options.command === "help") return void console.log(help(side));
287
+ const metadata = JSON.parse(fs.readFileSync(path.join(packageRoot, "package.json"), "utf8"));
288
+ if (options.command === "version") return void console.log(metadata.version);
289
+ if (typeof registerHooks !== "function")
290
+ throw new Error("ElementsPanel requires Node.js 22.15 or newer.");
291
+ process.exitCode = await startWorkers(planWorkers(side, packageRoot, options), options);
292
+ } catch (error) {
293
+ console.error(error.message);
294
+ process.exitCode = 1;
295
+ }
296
+ }
297
+
298
+ module.exports = {
299
+ parseArgs,
300
+ planWorkers,
301
+ startWorkers,
302
+ pipeWorkerOutput,
303
+ workerEnvironment,
304
+ run
305
+ };
@@ -0,0 +1,113 @@
1
+ const fs = require("node:fs");
2
+ const http = require("node:http");
3
+ const https = require("node:https");
4
+ const path = require("node:path");
5
+
6
+ const sources = [
7
+ ["file_zip", "linux", "arm64", "file_zip_linux_arm64", "https://github.com/MCSManager/Zip-Tools/releases/latest/download/file_zip_linux_arm64"],
8
+ ["file_zip", "linux", "x64", "file_zip_linux_x64", "https://github.com/MCSManager/Zip-Tools/releases/latest/download/file_zip_linux_x64"],
9
+ ["file_zip", "win32", "x64", "file_zip_win32_x64.exe", "https://github.com/MCSManager/Zip-Tools/releases/latest/download/file_zip_win32_x64.exe"],
10
+ ["file_zip", "darwin", "arm64", "file_zip_darwin_arm64", "https://github.com/MCSManager/Zip-Tools/releases/latest/download/file_zip_darwin_arm64"],
11
+ ["file_zip", "darwin", "x64", "file_zip_darwin_amd64", "https://github.com/MCSManager/Zip-Tools/releases/latest/download/file_zip_darwin_amd64"],
12
+ ["7z", "linux", "arm64", "7z_linux_arm64", "https://github.com/MCSManager/Zip-Tools/releases/latest/download/7z_linux_arm64"],
13
+ ["7z", "linux", "x64", "7z_linux_x64", "https://github.com/MCSManager/Zip-Tools/releases/latest/download/7z_linux_x64"],
14
+ ["7z", "win32", "arm64", "7z_win32_arm64.exe", "https://github.com/MCSManager/Zip-Tools/releases/latest/download/7z_win32_arm64.exe"],
15
+ ["7z", "win32", "x64", "7z_win32_x64.exe", "https://github.com/MCSManager/Zip-Tools/releases/latest/download/7z_win32_x64.exe"],
16
+ ["7z", "darwin", "arm64", "7z_darwin_arm64", "https://github.com/MCSManager/Zip-Tools/releases/latest/download/7z_darwin_arm64"],
17
+ ["7z", "darwin", "x64", "7z_darwin_x64", "https://github.com/MCSManager/Zip-Tools/releases/latest/download/7z_darwin_x64"],
18
+ ["pty", "linux", "arm64", "pty_linux_arm64", "https://github.com/MCSManager/PTY/releases/download/latest/pty_linux_arm64"],
19
+ ["pty", "linux", "loong64", "pty_linux_loong64", "https://github.com/MCSManager/PTY/releases/download/latest/pty_linux_loong64"],
20
+ ["pty", "linux", "x64", "pty_linux_x64", "https://github.com/MCSManager/PTY/releases/download/latest/pty_linux_x64"],
21
+ ["pty", "win32", "x64", "pty_win32_x64.exe", "https://github.com/MCSManager/PTY/releases/download/latest/pty_win32_x64.exe"],
22
+ ["pty", "darwin", "arm64", "pty_darwin_arm64", "https://github.com/MCSManager/PTY/releases/download/latest/pty_darwin_arm64"],
23
+ ["pty", "darwin", "x64", "pty_darwin_x64", "https://github.com/MCSManager/PTY/releases/download/latest/pty_darwin_x64"]
24
+ ].map(([name, platform, arch, filename, url]) => ({ name, platform, arch, filename, url }));
25
+
26
+ const requiredTools = ["file_zip", "pty", "7z"];
27
+
28
+ function toolSpec(name, platform = process.platform, arch = process.arch) {
29
+ return sources.find((item) => item.name === name && item.platform === platform && item.arch === arch);
30
+ }
31
+
32
+ function toolPath(directory, name, platform = process.platform, arch = process.arch) {
33
+ const spec = toolSpec(name, platform, arch);
34
+ return spec ? path.join(directory, "lib", spec.filename) : undefined;
35
+ }
36
+
37
+ function downloadFile(url, target, redirects = 0) {
38
+ if (redirects > 5) return Promise.reject(new Error(`Too many redirects while downloading ${url}.`));
39
+ const client = url.startsWith("https:") ? https : http;
40
+ return new Promise((resolve, reject) => {
41
+ const temporary = `${target}.download-${process.pid}-${Date.now()}`;
42
+ let settled = false;
43
+ const cleanup = () => fs.rm(temporary, { force: true }, () => {});
44
+ const fail = (error) => {
45
+ if (settled) return;
46
+ settled = true;
47
+ cleanup();
48
+ reject(error);
49
+ };
50
+ const request = client.get(url, { headers: { "User-Agent": "ElementsPanel daemon" } }, (response) => {
51
+ const status = response.statusCode || 0;
52
+ if (status >= 300 && status < 400 && response.headers.location) {
53
+ response.resume();
54
+ downloadFile(new URL(response.headers.location, url).href, target, redirects + 1).then(resolve, reject);
55
+ return;
56
+ }
57
+ if (status < 200 || status >= 300) {
58
+ response.resume();
59
+ fail(new Error(`Download failed (${status}) for ${url}.`));
60
+ return;
61
+ }
62
+ const output = fs.createWriteStream(temporary, { flags: "wx" });
63
+ output.on("error", fail);
64
+ response.on("error", fail);
65
+ output.once("finish", () => {
66
+ output.close(() => {
67
+ if (settled) return;
68
+ fs.chmod(temporary, process.platform === "win32" ? 0o666 : 0o755, (chmodError) => {
69
+ if (chmodError) return fail(chmodError);
70
+ fs.rename(temporary, target, (renameError) => {
71
+ if (renameError) return fail(renameError);
72
+ settled = true;
73
+ resolve();
74
+ });
75
+ });
76
+ });
77
+ });
78
+ response.pipe(output);
79
+ });
80
+ request.on("error", fail);
81
+ });
82
+ }
83
+
84
+ function usable(filename) {
85
+ try {
86
+ return fs.statSync(filename).isFile() && fs.statSync(filename).size > 0;
87
+ } catch {
88
+ return false;
89
+ }
90
+ }
91
+
92
+ async function ensureRuntimeTools(directory, options = {}) {
93
+ const targetDirectory = path.join(directory, "lib");
94
+ fs.mkdirSync(targetDirectory, { recursive: true });
95
+ const download = options.download || downloadFile;
96
+ for (const name of requiredTools) {
97
+ const spec = toolSpec(name, options.platform || process.platform, options.arch || process.arch);
98
+ if (!spec) {
99
+ throw new Error(
100
+ `No daemon binary download is available for ${name} on ${options.platform || process.platform}/${options.arch || process.arch}.`
101
+ );
102
+ }
103
+ const target = path.join(targetDirectory, spec.filename);
104
+ if (usable(target)) continue;
105
+ fs.rmSync(target, { force: true });
106
+ options.onDownload?.(spec);
107
+ await download(spec.url, target);
108
+ if (!usable(target)) throw new Error(`Downloaded daemon binary is empty: ${spec.filename}.`);
109
+ }
110
+ return targetDirectory;
111
+ }
112
+
113
+ module.exports = { requiredTools, sources, toolSpec, toolPath, downloadFile, ensureRuntimeTools };
@@ -0,0 +1,207 @@
1
+ const fs = require("node:fs");
2
+ const path = require("node:path");
3
+ const { createRequire } = require("node:module");
4
+
5
+ const readJson = (filename) => JSON.parse(fs.readFileSync(filename, "utf8"));
6
+ const readOptional = (filename) => {
7
+ try {
8
+ return readJson(filename);
9
+ } catch (error) {
10
+ if (error.code === "ENOENT") return null;
11
+ throw error;
12
+ }
13
+ };
14
+ const validId = /^[a-z][a-z0-9_-]{1,63}$/;
15
+ const validName = /^(?:@[a-z0-9_.-]+\/)?epanel-plugin-[a-z0-9_.-]+$/;
16
+
17
+ function packageManifest(root, name) {
18
+ const localRequire = createRequire(path.join(root, "package.json"));
19
+ const filename = (localRequire.resolve.paths(name) || [])
20
+ .map((directory) => path.join(directory, name, "package.json"))
21
+ .find((candidate) => fs.existsSync(candidate));
22
+ if (!filename) throw new Error(`Missing npm package: ${name}`);
23
+ return fs.realpathSync(filename);
24
+ }
25
+
26
+ function within(root, filename) {
27
+ const relative = path.relative(root, filename);
28
+ return (
29
+ relative === "" ||
30
+ (relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
31
+ );
32
+ }
33
+
34
+ function projectRoot(packageRoot) {
35
+ if (process.env.EPANEL_PROJECT_ROOT) return path.resolve(process.env.EPANEL_PROJECT_ROOT);
36
+ const parent = path.resolve(packageRoot, "..");
37
+ if (path.basename(parent) === "core") return path.dirname(parent);
38
+ // An installed runtime sits at <project>/node_modules/@scope/<side>; the
39
+ // project itself is three levels above the package. A development checkout
40
+ // sits below core/ and is handled above.
41
+ const scope = parent;
42
+ const nodeModules = path.dirname(scope);
43
+ if (path.basename(nodeModules) === "node_modules" && path.basename(scope).startsWith("@")) {
44
+ return path.dirname(nodeModules);
45
+ }
46
+ return parent;
47
+ }
48
+
49
+ function projectManifestFile(packageRoot) {
50
+ const root = projectRoot(packageRoot);
51
+ const filename = path.join(root, "package.json");
52
+ return path.resolve(filename) === path.resolve(path.join(packageRoot, "package.json"))
53
+ ? null
54
+ : filename;
55
+ }
56
+
57
+ function mappingOf(manifest) {
58
+ const plugins = manifest?.elementspanel?.plugins;
59
+ return plugins && typeof plugins === "object" && !Array.isArray(plugins) ? plugins : {};
60
+ }
61
+
62
+ function dependenciesOf(manifest) {
63
+ const dependencies = manifest?.dependencies;
64
+ return dependencies && typeof dependencies === "object" ? dependencies : {};
65
+ }
66
+
67
+ /**
68
+ * Resolve declared plugins through the host's dependency graph, including nested
69
+ * installs and project-level dependencies installed by the plugin market. The
70
+ * resolver also returns whether the mapping belongs to the built-in or project
71
+ * installation layer, so project plugins can remain dormant until enabled.
72
+ * Runtime startup passes skipInvalid so an optional package being removed does
73
+ * not stop the host; the caller separately validates the small set of core
74
+ * plugins.
75
+ */
76
+ function resolveNpmPlugins(packageRoot, side, options = {}) {
77
+ const skipInvalid = options.skipInvalid === true;
78
+ const host = readJson(path.join(packageRoot, "package.json"));
79
+ const projectFile = projectManifestFile(packageRoot);
80
+ const project = projectFile ? readOptional(projectFile) : null;
81
+ const selectedSide = side || host.elementspanel?.side;
82
+
83
+ // Project mappings are read first so the host's built-in plugins win an id
84
+ // collision: a market package must not shadow a plugin that ships with the runtime.
85
+ const configured = new Map();
86
+ for (const [id, name] of Object.entries(mappingOf(project)))
87
+ configured.set(id, { name, source: "project" });
88
+ for (const name of Object.keys(dependenciesOf(project))) {
89
+ if (!validName.test(name) || [...configured.values()].some((item) => item.name === name))
90
+ continue;
91
+ try {
92
+ const metadata = readJson(packageManifest(projectRoot(packageRoot), name));
93
+ const id = metadata.elementspanel?.id;
94
+ if (validId.test(id) && !configured.has(id))
95
+ configured.set(id, { name, source: "project", automatic: true });
96
+ } catch {
97
+ /* Ignore incomplete npm installations until the next scan. */
98
+ }
99
+ }
100
+ for (const [id, name] of Object.entries(mappingOf(host))) {
101
+ if (dependenciesOf(host)[name] !== undefined) configured.set(id, { name, source: "bundled" });
102
+ }
103
+
104
+ const hostDependencies = dependenciesOf(host);
105
+ const projectDependencies = dependenciesOf(project);
106
+ const roots = [packageRoot];
107
+ const parentRoot = projectRoot(packageRoot);
108
+ if (path.resolve(parentRoot) !== path.resolve(packageRoot)) roots.push(parentRoot);
109
+ const folders = new Set();
110
+ const records = [];
111
+
112
+ for (const [id, mapping] of configured) {
113
+ const name = mapping.name;
114
+ if (
115
+ !validId.test(id) ||
116
+ typeof name !== "string" ||
117
+ !validName.test(name) ||
118
+ (hostDependencies[name] === undefined && projectDependencies[name] === undefined)
119
+ ) {
120
+ if (skipInvalid) continue;
121
+ throw new Error(`Invalid npm plugin dependency: ${id}.`);
122
+ }
123
+
124
+ let manifestPath;
125
+ let failure;
126
+ for (const root of mapping.source === "project" ? [...roots].reverse() : roots) {
127
+ try {
128
+ manifestPath = packageManifest(root, name);
129
+ break;
130
+ } catch (error) {
131
+ failure = error;
132
+ }
133
+ }
134
+ if (!manifestPath) {
135
+ if (skipInvalid) continue;
136
+ throw new Error(`Missing npm plugin ${name}. Run npm install in the project directory.`, {
137
+ cause: failure
138
+ });
139
+ }
140
+ let metadata;
141
+ try {
142
+ metadata = readJson(manifestPath);
143
+ } catch (error) {
144
+ if (skipInvalid) continue;
145
+ throw error;
146
+ }
147
+ const definition = metadata.elementspanel?.[selectedSide];
148
+ // A project dependency may only ship one side; the other side simply does
149
+ // not see it. A built-in plugin declared by the runtime must carry this side.
150
+ if (!definition) {
151
+ if (mapping.source === "project" || skipInvalid) continue;
152
+ throw new Error(`Invalid ${selectedSide} entry in npm plugin ${name}.`);
153
+ }
154
+ const pluginRoot = path.dirname(manifestPath);
155
+ if (
156
+ typeof definition.directory !== "string" ||
157
+ typeof definition.folder !== "string" ||
158
+ !validId.test(definition.folder) ||
159
+ path.isAbsolute(definition.directory)
160
+ ) {
161
+ if (skipInvalid) continue;
162
+ throw new Error(`Npm plugin entry escapes its package: ${name}.`);
163
+ }
164
+ const directory = path.resolve(pluginRoot, definition.directory);
165
+ if (directory === pluginRoot || !within(pluginRoot, directory)) {
166
+ if (skipInvalid) continue;
167
+ throw new Error(`Npm plugin entry escapes its package: ${name}.`);
168
+ }
169
+ let entries;
170
+ try {
171
+ entries = readJson(path.join(directory, "plugin.json"));
172
+ } catch (error) {
173
+ if (skipInvalid) continue;
174
+ throw error;
175
+ }
176
+ if (
177
+ metadata.elementspanel?.id !== id ||
178
+ typeof metadata.version !== "string" ||
179
+ !metadata.version.trim() ||
180
+ !entries ||
181
+ typeof entries !== "object" ||
182
+ Array.isArray(entries)
183
+ ) {
184
+ if (skipInvalid) continue;
185
+ throw new Error(`Invalid npm plugin identity or version in package.json: ${name}.`);
186
+ }
187
+ if (folders.has(definition.folder)) {
188
+ if (skipInvalid) continue;
189
+ throw new Error(`Duplicate npm plugin asset directory: ${definition.folder}.`);
190
+ }
191
+ folders.add(definition.folder);
192
+ records.push({
193
+ id,
194
+ name,
195
+ version: metadata.version,
196
+ source: mapping.source,
197
+ defaultEnabled: mapping.automatic === true || mapping.source !== "project",
198
+ packageRoot: pluginRoot,
199
+ directory,
200
+ folder: definition.folder,
201
+ dependencies: Object.keys(metadata.dependencies || {})
202
+ });
203
+ }
204
+ return records;
205
+ }
206
+
207
+ module.exports = { resolveNpmPlugins, projectManifestFile };