feat: integrate Pi backend and Pi Web

This commit is contained in:
luckyyzh
2026-07-30 19:37:53 +08:00
commit 7392ab9dd7
1390 changed files with 337197 additions and 0 deletions
+35
View File
@@ -0,0 +1,35 @@
"use strict";
const MIN_NODE_VERSION = "22.19.0";
function parseNodeVersion(version) {
const match = /^v?(\d+)\.(\d+)\.(\d+)/.exec(version);
if (!match) return null;
return match.slice(1).map(Number);
}
function isNodeVersionSupported(version) {
const current = parseNodeVersion(version);
const minimum = parseNodeVersion(MIN_NODE_VERSION);
if (!current || !minimum) return false;
for (let index = 0; index < minimum.length; index += 1) {
if (current[index] > minimum[index]) return true;
if (current[index] < minimum[index]) return false;
}
return true;
}
function getUnsupportedNodeVersionMessage(version) {
return [
`Pi Web requires Node.js ${MIN_NODE_VERSION} or newer.`,
`Current Node.js version: ${version}.`,
"Upgrade Node.js and try again: https://nodejs.org/",
].join("\n");
}
module.exports = {
MIN_NODE_VERSION,
getUnsupportedNodeVersionMessage,
isNodeVersionSupported,
};
+30
View File
@@ -0,0 +1,30 @@
"use strict";
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { parseArgs } = require("util");
const TRUE_VALUES = new Set(["1", "true", "yes", "on"]);
function isEnabled(value) {
return typeof value === "string" && TRUE_VALUES.has(value.trim().toLowerCase());
}
function parseLaunchOptions(args = process.argv.slice(2), env = process.env) {
const { values: cliArgs } = parseArgs({
args,
options: {
port: { type: "string", short: "p" },
hostname: { type: "string", short: "H" },
"no-open": { type: "boolean" },
},
strict: false,
});
return {
port: cliArgs.port ?? env.PORT ?? "30141",
hostname: cliArgs.hostname ?? env.PI_WEB_HOSTNAME ?? "127.0.0.1",
openBrowser: !cliArgs["no-open"] && !isEnabled(env.PI_WEB_NO_OPEN),
};
}
module.exports = { parseLaunchOptions };
+89
View File
@@ -0,0 +1,89 @@
#!/usr/bin/env node
"use strict";
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { getUnsupportedNodeVersionMessage, isNodeVersionSupported } = require("./node-version");
if (!isNodeVersionSupported(process.versions.node)) {
console.error(getUnsupportedNodeVersionMessage(process.versions.node));
process.exit(1);
}
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { spawn } = require("child_process");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const path = require("path");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const fs = require("fs");
// eslint-disable-next-line @typescript-eslint/no-require-imports
const { parseLaunchOptions } = require("./pi-web-options");
const pkgDir = path.join(__dirname, "..");
const nextDir = path.join(pkgDir, ".next");
// Resolve next's CLI entry directly to avoid relying on .bin symlinks (which
// may not exist when installed via npx).
let nextBin;
try {
nextBin = require.resolve("next/dist/bin/next", { paths: [pkgDir] });
} catch {
// Fallback: locate next package root and derive the bin path manually.
try {
const nextPkg = require.resolve("next/package.json", { paths: [pkgDir] });
nextBin = path.join(path.dirname(nextPkg), "dist", "bin", "next");
} catch {
nextBin = path.join(pkgDir, "node_modules", "next", "dist", "bin", "next");
}
}
const { port, hostname, openBrowser } = parseLaunchOptions();
const loopbackHostnames = new Set(["127.0.0.1", "localhost", "::1", "[::1]"]);
if (!fs.existsSync(nextDir)) {
console.error("Build artifacts not found. Please report this issue.");
process.exit(1);
}
if (!loopbackHostnames.has(hostname)) {
console.warn(
`Warning: pi-web is listening on ${hostname} without authentication. Only use this on a trusted network.`,
);
}
const nextArgs = ["start", "-p", port];
nextArgs.push("-H", hostname);
// Always run next's JS entry with node directly — avoids .bin symlink issues
// and path-with-spaces problems on Windows when shell: true is used.
const child = spawn(process.execPath, [nextBin, ...nextArgs], {
cwd: pkgDir,
stdio: ["inherit", "pipe", "inherit"],
env: { ...process.env, PI_WEB_HOSTNAME: hostname },
});
let browserOpened = false;
const url = `http://${hostname}:${port}`;
child.stdout.on("data", (chunk) => {
const text = chunk.toString();
process.stdout.write(text);
if (openBrowser && !browserOpened && text.includes("Ready")) {
browserOpened = true;
const isWindows = process.platform === "win32";
const isMac = process.platform === "darwin";
const openCmd = isWindows ? "start" : isMac ? "open" : "xdg-open";
const opener = spawn(openCmd, [url], {
shell: isWindows,
stdio: "ignore",
detached: true,
});
opener.on("error", (error) => {
console.warn(`Could not open browser automatically: ${error.message}`);
});
opener.unref();
}
});
child.on("exit", (code) => process.exit(code ?? 0));