"use strict"; const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const { DEFAULT_FRP_VERSION, DEFAULT_FRP_ARCH, DEFAULT_FRP_SERVER_ADDR, DEFAULT_FRP_SERVER_PORT, DEFAULT_SERVICE_NAME, defaultInstallDir, defaultConfigPath, parseFrpConfig, renderParsedFrpConfig, renderFrpConfig, buildGlobals, createUniqueProxyName, assertProxyName, } = require("./frp-config"); const NVM_INSTALL_VERSION = "v0.40.4"; const NVM_INSTALL_COMMAND = `set -eo pipefail export NVM_DIR="$HOME/.nvm" if [ ! -s "$NVM_DIR/nvm.sh" ]; then if command -v curl >/dev/null 2>&1; then curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_INSTALL_VERSION}/install.sh | bash elif command -v wget >/dev/null 2>&1; then wget -qO- https://raw.githubusercontent.com/nvm-sh/nvm/${NVM_INSTALL_VERSION}/install.sh | bash else echo "curl or wget is required to install nvm" >&2 exit 1 fi fi . "$NVM_DIR/nvm.sh" nvm install --lts nvm alias default 'lts/*' nvm use --lts`; const OH_MY_ZSH_COMMAND = 'RUNZSH=no CHSH=no KEEP_ZSHRC=yes sh -c "$(curl -fsSL https://gitee.com/pocmon/ohmyzsh/raw/master/tools/install.sh)"'; function zshInstallPlan() { const customDir = process.env.ZSH_CUSTOM || path.join(os.homedir(), ".oh-my-zsh", "custom"); const autosuggestionsDir = path.join(customDir, "plugins", "zsh-autosuggestions"); const syntaxHighlightingDir = path.join(customDir, "plugins", "zsh-syntax-highlighting"); const steps = [ run("apt update", "sudo", ["apt", "update"]), run("apt upgrade", "sudo", ["apt", "upgrade", "-y"]), run("install zsh & friends", "sudo", ["apt", "install", "zsh", "git", "curl", "wget", "-y"]), run("change shell to zsh", "sudo", ["chsh", "-s", "/bin/zsh", os.userInfo().username]), run("install oh-my-zsh", "sh", ["-c", OH_MY_ZSH_COMMAND]), run("install nvm + node LTS", "bash", ["-c", NVM_INSTALL_COMMAND]), gitPluginStep("zsh-autosuggestions", "https://github.com/zsh-users/zsh-autosuggestions", autosuggestionsDir), gitPluginStep( "zsh-syntax-highlighting", "https://github.com/zsh-users/zsh-syntax-highlighting.git", syntaxHighlightingDir, ), { kind: "zshrc-plugins", label: "update ~/.zshrc plugin list", path: path.join(os.homedir(), ".zshrc"), plugins: ["git", "zsh-autosuggestions", "zsh-syntax-highlighting"], }, ]; return { title: "Install zsh + oh-my-zsh + nvm", steps }; } function gitPluginStep(name, repo, destination) { if (fs.existsSync(destination)) { return run(`update ${name}`, "git", ["-C", destination, "pull", "--ff-only"]); } return run(`clone ${name}`, "git", ["clone", repo, destination]); } function sshInstallPlan() { return { title: "Install OpenSSH server", steps: [ run("apt update", "sudo", ["apt", "update"]), run("install openssh-server", "sudo", ["apt", "install", "openssh-server", "-y"]), run("enable + start ssh", "sudo", ["systemctl", "enable", "--now", "ssh"]), ], }; } function frpInstallPlan(options = {}) { const version = options.version || DEFAULT_FRP_VERSION; const arch = options.arch || DEFAULT_FRP_ARCH; const installDir = options.installDir || defaultInstallDir(version, arch); const configPath = options.configPath || defaultConfigPath(installDir); const serviceName = options.serviceName || DEFAULT_SERVICE_NAME; const archiveName = `frp_${version}_linux_${arch}.tar.gz`; const archivePath = `/tmp/${archiveName}`; const extractedDir = `/tmp/frp_${version}_linux_${arch}`; const url = `https://github.com/fatedier/frp/releases/download/v${version}/${archiveName}`; const token = requireToken(options.token); const force = Boolean(options.force); const proxies = (fs.existsSync(configPath) && force) ? [...parseFrpConfig(fs.readFileSync(configPath, "utf8")).sections.values()].map((section) => ({ name: section.name, ...section.values, })) : []; const configContent = renderFrpConfig(buildGlobalsFromOptions(token, options), proxies); const serviceContent = renderSystemdService(installDir, configPath); const steps = [ run("download frp archive", "wget", ["-O", archivePath, url]), run("extract archive", "tar", ["-zxf", archivePath, "-C", "/tmp"]), run("create install dir", "sudo", ["mkdir", "-p", installDir]), run("copy frpc binary", "sudo", [ "cp", "-f", path.join(extractedDir, "frpc"), path.join(installDir, "frpc"), ]), run("chmod +x frpc", "sudo", ["chmod", "+x", path.join(installDir, "frpc")]), ]; if (!fs.existsSync(configPath) || force) { steps.push(writeFile(`write ${path.basename(configPath)}`, configPath, configContent)); } steps.push(writeFile(`write ${serviceName}.service`, `/etc/systemd/system/${serviceName}.service`, serviceContent)); steps.push(run("systemctl daemon-reload", "sudo", ["systemctl", "daemon-reload"])); steps.push(run(`enable + start ${serviceName}`, "sudo", ["systemctl", "enable", "--now", serviceName])); return { title: `Install frp ${version} (${serviceName})`, steps }; } function frpInitConfigPlan(options = {}) { const installDir = options.installDir || defaultInstallDir(); const configPath = options.configPath || defaultConfigPath(installDir); const exists = fs.existsSync(configPath); if (exists && !options.force) { throw new Error(`${configPath} already exists. Re-run with --force to rewrite.`); } const token = requireToken(options.token); const proxies = (exists && options.force) ? [...parseFrpConfig(fs.readFileSync(configPath, "utf8")).sections.values()].map((section) => ({ name: section.name, ...section.values, })) : []; const content = renderFrpConfig(buildGlobalsFromOptions(token, options), proxies); return { title: `Rewrite ${configPath}`, steps: [writeFile(`write ${path.basename(configPath)}`, configPath, content)], }; } function frpAddProxyPlan(options) { const baseName = options.name; if (!baseName) { throw new Error("Proxy name is required."); } assertProxyName(baseName); if (!options.localPort || !options.remotePort) { throw new Error("local port and remote port are required."); } const installDir = options.installDir || defaultInstallDir(); const configPath = options.configPath || defaultConfigPath(installDir); if (!fs.existsSync(configPath)) { throw new Error(`${configPath} does not exist. Install frp or run init first.`); } const config = parseFrpConfig(fs.readFileSync(configPath, "utf8")); const proxyName = createUniqueProxyName(baseName, config.sections); config.sections.set(proxyName, { name: proxyName, values: { type: options.type || "tcp", local_ip: options.localIp || "127.0.0.1", local_port: options.localPort, remote_port: options.remotePort, }, }); const content = renderParsedFrpConfig(config); const steps = [writeFile(`write proxy ${proxyName}`, configPath, content)]; if (options.restart) { steps.push(run(`restart ${options.serviceName || DEFAULT_SERVICE_NAME}`, "sudo", [ "systemctl", "restart", options.serviceName || DEFAULT_SERVICE_NAME, ])); } return { title: `Add proxy ${proxyName}`, steps, meta: { proxyName }, }; } function frpRemoveProxyPlan(options) { if (!options.name) { throw new Error("Proxy name is required."); } const installDir = options.installDir || defaultInstallDir(); const configPath = options.configPath || defaultConfigPath(installDir); if (!fs.existsSync(configPath)) { throw new Error(`${configPath} does not exist.`); } const config = parseFrpConfig(fs.readFileSync(configPath, "utf8")); if (!config.sections.delete(options.name)) { throw new Error(`No proxy named ${options.name} in ${configPath}.`); } const content = renderParsedFrpConfig(config); const steps = [writeFile(`remove proxy ${options.name}`, configPath, content)]; if (options.restart) { steps.push(run(`restart ${options.serviceName || DEFAULT_SERVICE_NAME}`, "sudo", [ "systemctl", "restart", options.serviceName || DEFAULT_SERVICE_NAME, ])); } return { title: `Remove proxy ${options.name}`, steps }; } function frpRestartPlan(options = {}) { const serviceName = options.serviceName || DEFAULT_SERVICE_NAME; return { title: `Restart ${serviceName}`, steps: [run(`restart ${serviceName}`, "sudo", ["systemctl", "restart", serviceName])], }; } function bootstrapPlan(options) { const zsh = zshInstallPlan(); const ssh = sshInstallPlan(); const frp = frpInstallPlan(options); return { title: "Bootstrap: zsh + ssh + frp", steps: [...zsh.steps, ...ssh.steps, ...frp.steps], }; } function listProxies(options = {}) { const installDir = options.installDir || defaultInstallDir(); const configPath = options.configPath || defaultConfigPath(installDir); if (!fs.existsSync(configPath)) { throw new Error(`${configPath} does not exist.`); } const config = parseFrpConfig(fs.readFileSync(configPath, "utf8")); return [...config.sections.values()].map((section) => ({ name: section.name, type: section.values.type || "tcp", localIp: section.values.local_ip || "127.0.0.1", localPort: section.values.local_port, remotePort: section.values.remote_port, })); } function buildGlobalsFromOptions(token, options) { return buildGlobals({ serverAddr: options.serverAddr || DEFAULT_FRP_SERVER_ADDR, serverPort: options.serverPort || DEFAULT_FRP_SERVER_PORT, token, tlsEnable: options.tlsEnable ?? false, tcpMux: options.tcpMux ?? true, logFile: options.logFile || "/var/log/frpc.log", logLevel: options.logLevel || "info", logMaxDays: options.logMaxDays ?? 7, }); } function renderSystemdService(installDir, configPath) { return `[Unit] Description=frp client After=network.target [Service] Type=simple ExecStart=${path.join(installDir, "frpc")} -c ${configPath} Restart=always RestartSec=5 [Install] WantedBy=multi-user.target `; } function requireToken(value) { const token = value || process.env.FRP_TOKEN || ""; if (!token) { throw new Error("frp token is required."); } return token; } function run(label, command, args) { return { kind: "run", label, command, args }; } function writeFile(label, filePath, content) { return { kind: "write", label, path: filePath, content }; } module.exports = { zshInstallPlan, sshInstallPlan, frpInstallPlan, frpInitConfigPlan, frpAddProxyPlan, frpRemoveProxyPlan, frpRestartPlan, bootstrapPlan, listProxies, DEFAULTS: { version: DEFAULT_FRP_VERSION, arch: DEFAULT_FRP_ARCH, serverAddr: DEFAULT_FRP_SERVER_ADDR, serverPort: DEFAULT_FRP_SERVER_PORT, serviceName: DEFAULT_SERVICE_NAME, }, };