@elementspanel/panel 0.0.0-stage → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +6 -2
- package/app.js +2640 -0
- package/app.js.map +1 -0
- package/bin/panel.cjs +2 -0
- package/package.json +107 -4
- package/public/assets/AppDialog.vue_vue_type_style_index_0_lang-8b56f871.js +2 -0
- package/public/assets/AppDialog.vue_vue_type_style_index_0_lang-8b56f871.js.map +1 -0
- package/public/assets/CardPanel-a704f146.js +2 -0
- package/public/assets/CardPanel-a704f146.js.map +1 -0
- package/public/assets/DesktopDialog-081410ca.js +2 -0
- package/public/assets/DesktopDialog-081410ca.js.map +1 -0
- package/public/assets/Loading.vue_vue_type_script_setup_true_lang-9e79de95.js +2 -0
- package/public/assets/Loading.vue_vue_type_script_setup_true_lang-9e79de95.js.map +1 -0
- package/public/assets/PageToolbar-5ca1a069.js +2 -0
- package/public/assets/PageToolbar-5ca1a069.js.map +1 -0
- package/public/assets/_elements-sdk_cordis-b0b88a2c.js +2 -0
- package/public/assets/_elements-sdk_cordis-b0b88a2c.js.map +1 -0
- package/public/assets/_elements-sdk_cordis-e3bebc89.js +9 -0
- package/public/assets/_elements-sdk_cordis-e3bebc89.js.map +1 -0
- package/public/assets/_elements-sdk_cosmokit-e4842f5c.js +2 -0
- package/public/assets/_elements-sdk_cosmokit-e4842f5c.js.map +1 -0
- package/public/assets/_elements-sdk_pinia-fc8ba898.js +6 -0
- package/public/assets/_elements-sdk_pinia-fc8ba898.js.map +1 -0
- package/public/assets/_elements-sdk_vue-b86b0db6.js +2 -0
- package/public/assets/_elements-sdk_vue-b86b0db6.js.map +1 -0
- package/public/assets/_elements-sdk_vue-i18n-64f8e172.js +2 -0
- package/public/assets/_elements-sdk_vue-i18n-64f8e172.js.map +1 -0
- package/public/assets/_elements-sdk_vue-router-a62f518d.js +2 -0
- package/public/assets/_elements-sdk_vue-router-a62f518d.js.map +1 -0
- package/public/assets/_plugin-vue_export-helper-c27b6911.js +2 -0
- package/public/assets/_plugin-vue_export-helper-c27b6911.js.map +1 -0
- package/public/assets/chartjs-1e331dfb.js +19 -0
- package/public/assets/chartjs-1e331dfb.js.map +1 -0
- package/public/assets/codemirror-1b74112a.js +29 -0
- package/public/assets/codemirror-1b74112a.js.map +1 -0
- package/public/assets/const-01673e6b.js +38 -0
- package/public/assets/const-01673e6b.js.map +1 -0
- package/public/assets/const-cfe7ddfe.js +2 -0
- package/public/assets/const-cfe7ddfe.js.map +1 -0
- package/public/assets/context-b16af9cc.js +2 -0
- package/public/assets/context-b16af9cc.js.map +1 -0
- package/public/assets/copy-4d64fc1f.js +2 -0
- package/public/assets/copy-4d64fc1f.js.map +1 -0
- package/public/assets/core-b6992169.js +2 -0
- package/public/assets/core-b6992169.js.map +1 -0
- package/public/assets/daemon_connection_error-9eb654cc.png +0 -0
- package/public/assets/dayjs.min-25183c0a.js +2 -0
- package/public/assets/dayjs.min-25183c0a.js.map +1 -0
- package/public/assets/desktopNotice-2c886f6b.js +2 -0
- package/public/assets/desktopNotice-2c886f6b.js.map +1 -0
- package/public/assets/dom-ff483262.js +2 -0
- package/public/assets/dom-ff483262.js.map +1 -0
- package/public/assets/htmlparser2-bcb8f9db.js +3 -0
- package/public/assets/htmlparser2-bcb8f9db.js.map +1 -0
- package/public/assets/i18n-fe0f1ee7.js +2 -0
- package/public/assets/i18n-fe0f1ee7.js.map +1 -0
- package/public/assets/index-76b0df52.js +2 -0
- package/public/assets/index-76b0df52.js.map +1 -0
- package/public/assets/index-c684efbd.js +2 -0
- package/public/assets/index-c684efbd.js.map +1 -0
- package/public/assets/index-e3f35709.js +2 -0
- package/public/assets/index-e3f35709.js.map +1 -0
- package/public/assets/instance-26cca4c8.js +2 -0
- package/public/assets/instance-26cca4c8.js.map +1 -0
- package/public/assets/instance_contract-0115efe5.js +2 -0
- package/public/assets/instance_contract-0115efe5.js.map +1 -0
- package/public/assets/lodash-001e4f93.js +28 -0
- package/public/assets/lodash-001e4f93.js.map +1 -0
- package/public/assets/logo-9e2a668b.svg +15 -0
- package/public/assets/logo_b-35ccadda.svg +8 -0
- package/public/assets/materialdesignicons-webfont-0b183104.eot +0 -0
- package/public/assets/materialdesignicons-webfont-61e8aba5.ttf +0 -0
- package/public/assets/materialdesignicons-webfont-662fefa8.woff2 +0 -0
- package/public/assets/materialdesignicons-webfont-a5928a0d.woff +0 -0
- package/public/assets/msl-logo-699c8076.png +0 -0
- package/public/assets/nodes-402cdcbd.js +2 -0
- package/public/assets/nodes-402cdcbd.js.map +1 -0
- package/public/assets/plugin_contract-691cd37a.js +2 -0
- package/public/assets/plugin_contract-691cd37a.js.map +1 -0
- package/public/assets/preload-helper-a4192956.js +2 -0
- package/public/assets/preload-helper-a4192956.js.map +1 -0
- package/public/assets/protocol-97f7bab7.js +2 -0
- package/public/assets/protocol-97f7bab7.js.map +1 -0
- package/public/assets/randId-ed0b95b7.js +2 -0
- package/public/assets/randId-ed0b95b7.js.map +1 -0
- package/public/assets/router-0f7d4714.js +3 -0
- package/public/assets/router-0f7d4714.js.map +1 -0
- package/public/assets/safe-87846d89.js +89 -0
- package/public/assets/safe-87846d89.js.map +1 -0
- package/public/assets/sdk-01dda668.js +2 -0
- package/public/assets/sdk-01dda668.js.map +1 -0
- package/public/assets/shared-f2e37690.js +2 -0
- package/public/assets/shared-f2e37690.js.map +1 -0
- package/public/assets/useAppConfigStore-289073ec.js +2 -0
- package/public/assets/useAppConfigStore-289073ec.js.map +1 -0
- package/public/assets/useAppRouters-5f58db32.js +2 -0
- package/public/assets/useAppRouters-5f58db32.js.map +1 -0
- package/public/assets/useAppStateStore-a644582a.js +2 -0
- package/public/assets/useAppStateStore-a644582a.js.map +1 -0
- package/public/assets/useAppToolsStore-3a911226.js +2 -0
- package/public/assets/useAppToolsStore-3a911226.js.map +1 -0
- package/public/assets/useDefineApi-ead2616e.js +7 -0
- package/public/assets/useDefineApi-ead2616e.js.map +1 -0
- package/public/assets/useInstanceActionAvailability-dec16c10.js +2 -0
- package/public/assets/useInstanceActionAvailability-dec16c10.js.map +1 -0
- package/public/assets/useKeyboardEvents-dba9d5ca.js +2 -0
- package/public/assets/useKeyboardEvents-dba9d5ca.js.map +1 -0
- package/public/assets/useMountComponent-dd1fcfea.js +2 -0
- package/public/assets/useMountComponent-dd1fcfea.js.map +1 -0
- package/public/assets/useOverviewChart-119bda3b.js +2 -0
- package/public/assets/useOverviewChart-119bda3b.js.map +1 -0
- package/public/assets/useOverviewInfo-a149d634.js +2 -0
- package/public/assets/useOverviewInfo-a149d634.js.map +1 -0
- package/public/assets/usePolling-f2f09d27.js +2 -0
- package/public/assets/usePolling-f2f09d27.js.map +1 -0
- package/public/assets/useScreen-e3715e1f.js +2 -0
- package/public/assets/useScreen-e3715e1f.js.map +1 -0
- package/public/assets/useSocketIo-bc0a62c2.js +2 -0
- package/public/assets/useSocketIo-bc0a62c2.js.map +1 -0
- package/public/assets/useThemeSwitch-6ff90248.js +2 -0
- package/public/assets/useThemeSwitch-6ff90248.js.map +1 -0
- package/public/assets/vue-2433bd89.js +64 -0
- package/public/assets/vue-2433bd89.js.map +1 -0
- package/public/assets/vue-4c6a2976.css +1 -0
- package/public/assets/vuetifyModal-49cb95ba.js +2 -0
- package/public/assets/vuetifyModal-49cb95ba.js.map +1 -0
- package/public/assets/xterm-62ba073f.js +67 -0
- package/public/assets/xterm-62ba073f.js.map +1 -0
- package/public/favicon.ico +0 -0
- package/public/index.html +207 -0
- package/public/robots.txt +2 -0
- package/public/stats.html +4950 -0
- package/runtime/cli.cjs +305 -0
- package/runtime/daemon-tools.cjs +113 -0
- package/runtime/npm-plugins.cjs +176 -0
- package/runtime/plugin-resolution.cjs +123 -0
- package/runtime/shared-dependencies.cjs +35 -0
- package/runtime/worker.cjs +25 -0
package/runtime/cli.cjs
ADDED
|
@@ -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,176 @@
|
|
|
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 within(root, filename) {
|
|
18
|
+
const relative = path.relative(root, filename);
|
|
19
|
+
return (
|
|
20
|
+
relative === "" ||
|
|
21
|
+
(relative !== ".." && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative))
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function projectRoot(packageRoot) {
|
|
26
|
+
if (process.env.EPANEL_PROJECT_ROOT) return path.resolve(process.env.EPANEL_PROJECT_ROOT);
|
|
27
|
+
const parent = path.resolve(packageRoot, "..");
|
|
28
|
+
// An installed runtime sits at <project>/node_modules/@scope/<side>; the
|
|
29
|
+
// project itself is three levels above the package. A development checkout
|
|
30
|
+
// sits directly under the project instead.
|
|
31
|
+
const scope = parent;
|
|
32
|
+
const nodeModules = path.dirname(scope);
|
|
33
|
+
if (path.basename(nodeModules) === "node_modules" && path.basename(scope).startsWith("@")) {
|
|
34
|
+
return path.dirname(nodeModules);
|
|
35
|
+
}
|
|
36
|
+
return parent;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function projectManifestFile(packageRoot) {
|
|
40
|
+
const root = projectRoot(packageRoot);
|
|
41
|
+
const filename = path.join(root, "package.json");
|
|
42
|
+
return path.resolve(filename) === path.resolve(path.join(packageRoot, "package.json"))
|
|
43
|
+
? null
|
|
44
|
+
: filename;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mappingOf(manifest) {
|
|
48
|
+
const plugins = manifest?.elementspanel?.plugins;
|
|
49
|
+
return plugins && typeof plugins === "object" && !Array.isArray(plugins) ? plugins : {};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function dependenciesOf(manifest) {
|
|
53
|
+
const dependencies = manifest?.dependencies;
|
|
54
|
+
return dependencies && typeof dependencies === "object" ? dependencies : {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Resolve declared plugins through the host's dependency graph, including nested
|
|
59
|
+
* installs and project-level dependencies installed by the plugin market. The
|
|
60
|
+
* resolver also returns whether the mapping belongs to the built-in or project
|
|
61
|
+
* installation layer, so project plugins can remain dormant until enabled.
|
|
62
|
+
* Runtime startup passes skipInvalid so an optional package being removed does
|
|
63
|
+
* not stop the host; the caller separately validates the small set of core
|
|
64
|
+
* plugins.
|
|
65
|
+
*/
|
|
66
|
+
function resolveNpmPlugins(packageRoot, side, options = {}) {
|
|
67
|
+
const skipInvalid = options.skipInvalid === true;
|
|
68
|
+
const host = readJson(path.join(packageRoot, "package.json"));
|
|
69
|
+
const projectFile = projectManifestFile(packageRoot);
|
|
70
|
+
const project = projectFile ? readOptional(projectFile) : null;
|
|
71
|
+
const selectedSide = side || host.elementspanel?.side;
|
|
72
|
+
|
|
73
|
+
// Project mappings are read first so the host's built-in plugins win an id
|
|
74
|
+
// collision: a market package must not shadow a plugin that ships with the runtime.
|
|
75
|
+
const configured = new Map();
|
|
76
|
+
for (const [id, name] of Object.entries(mappingOf(project))) configured.set(id, { name, source: "project" });
|
|
77
|
+
for (const [id, name] of Object.entries(mappingOf(host))) configured.set(id, { name, source: "bundled" });
|
|
78
|
+
|
|
79
|
+
const hostDependencies = dependenciesOf(host);
|
|
80
|
+
const projectDependencies = dependenciesOf(project);
|
|
81
|
+
const roots = [packageRoot];
|
|
82
|
+
const parentRoot = projectRoot(packageRoot);
|
|
83
|
+
if (path.resolve(parentRoot) !== path.resolve(packageRoot)) roots.push(parentRoot);
|
|
84
|
+
const requireAt = (root) => createRequire(path.join(root, "package.json"));
|
|
85
|
+
const folders = new Set();
|
|
86
|
+
const records = [];
|
|
87
|
+
|
|
88
|
+
for (const [id, mapping] of configured) {
|
|
89
|
+
const name = mapping.name;
|
|
90
|
+
if (
|
|
91
|
+
!validId.test(id) ||
|
|
92
|
+
typeof name !== "string" ||
|
|
93
|
+
!validName.test(name) ||
|
|
94
|
+
(hostDependencies[name] === undefined && projectDependencies[name] === undefined)
|
|
95
|
+
) {
|
|
96
|
+
if (skipInvalid) continue;
|
|
97
|
+
throw new Error(`Invalid npm plugin dependency: ${id}.`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
let manifestPath;
|
|
101
|
+
let failure;
|
|
102
|
+
for (const root of roots) {
|
|
103
|
+
try {
|
|
104
|
+
manifestPath = requireAt(root).resolve(`${name}/package.json`);
|
|
105
|
+
break;
|
|
106
|
+
} catch (error) {
|
|
107
|
+
failure = error;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
if (!manifestPath) {
|
|
111
|
+
if (skipInvalid) continue;
|
|
112
|
+
throw new Error(`Missing npm plugin ${name}. Run npm install in the project directory.`, {
|
|
113
|
+
cause: failure
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
let metadata;
|
|
117
|
+
try {
|
|
118
|
+
metadata = readJson(manifestPath);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
if (skipInvalid) continue;
|
|
121
|
+
throw error;
|
|
122
|
+
}
|
|
123
|
+
const definition = metadata.elementspanel?.[selectedSide];
|
|
124
|
+
// A project dependency may only ship one side; the other side simply does
|
|
125
|
+
// not see it. A built-in plugin declared by the runtime must carry this side.
|
|
126
|
+
if (!definition) {
|
|
127
|
+
if (mapping.source === "project" || skipInvalid) continue;
|
|
128
|
+
throw new Error(`Invalid ${selectedSide} entry in npm plugin ${name}.`);
|
|
129
|
+
}
|
|
130
|
+
const pluginRoot = path.dirname(manifestPath);
|
|
131
|
+
if (
|
|
132
|
+
typeof definition.directory !== "string" ||
|
|
133
|
+
typeof definition.folder !== "string" ||
|
|
134
|
+
!validId.test(definition.folder) ||
|
|
135
|
+
path.isAbsolute(definition.directory)
|
|
136
|
+
) {
|
|
137
|
+
if (skipInvalid) continue;
|
|
138
|
+
throw new Error(`Npm plugin entry escapes its package: ${name}.`);
|
|
139
|
+
}
|
|
140
|
+
const directory = path.resolve(pluginRoot, definition.directory);
|
|
141
|
+
if (directory === pluginRoot || !within(pluginRoot, directory)) {
|
|
142
|
+
if (skipInvalid) continue;
|
|
143
|
+
throw new Error(`Npm plugin entry escapes its package: ${name}.`);
|
|
144
|
+
}
|
|
145
|
+
let manifest;
|
|
146
|
+
try {
|
|
147
|
+
manifest = readJson(path.join(directory, "plugin.json"));
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (skipInvalid) continue;
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
if (manifest.id !== id || manifest.version !== metadata.version) {
|
|
153
|
+
if (skipInvalid) continue;
|
|
154
|
+
throw new Error(`Npm plugin identity or version does not match its manifest: ${name}.`);
|
|
155
|
+
}
|
|
156
|
+
if (folders.has(definition.folder)) {
|
|
157
|
+
if (skipInvalid) continue;
|
|
158
|
+
throw new Error(`Duplicate npm plugin asset directory: ${definition.folder}.`);
|
|
159
|
+
}
|
|
160
|
+
folders.add(definition.folder);
|
|
161
|
+
records.push({
|
|
162
|
+
id,
|
|
163
|
+
name,
|
|
164
|
+
version: metadata.version,
|
|
165
|
+
source: mapping.source,
|
|
166
|
+
defaultEnabled: mapping.source !== "project",
|
|
167
|
+
packageRoot: pluginRoot,
|
|
168
|
+
directory,
|
|
169
|
+
folder: definition.folder,
|
|
170
|
+
dependencies: Object.keys(metadata.dependencies || {})
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
return records;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = { resolveNpmPlugins, projectManifestFile };
|