mirror of
https://github.com/Termix-SSH/Termix.git
synced 2026-10-09 13:21:47 +00:00
717 lines
20 KiB
JavaScript
717 lines
20 KiB
JavaScript
/**
|
|
* npm run dev, one command for manual testing. Run with --help for the targets
|
|
* and options.
|
|
*
|
|
* Every target builds the SDK and stages plugins (local builds from
|
|
* ../Termix-Plugins when it exists), then:
|
|
*
|
|
* - web: compiles the backend in watch mode, starts it, then starts Vite. A
|
|
* backend or SDK change restarts the backend. A plugin change rebuilds that
|
|
* plugin and reloads it inside the running backend, and the browser swaps
|
|
* it in.
|
|
* - electron: web, plus the desktop app, restarted when electron/ changes.
|
|
* - docker: builds the production image and runs it with dist/plugins
|
|
* mounted. A plugin change rebuilds it and restarts the container. Core
|
|
* changes wait for "r", since an image build is slow.
|
|
*
|
|
* --db postgres|mysql and --guacd run those in Docker next to it.
|
|
*/
|
|
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import process from "node:process";
|
|
import { spawn, spawnSync, fork } from "node:child_process";
|
|
import { createRequire } from "node:module";
|
|
import { fileURLToPath } from "node:url";
|
|
import {
|
|
HELP,
|
|
createDebouncer,
|
|
createSerialQueue,
|
|
dataDirFor,
|
|
findBuildablePlugins,
|
|
isPluginEdit,
|
|
needsDocker,
|
|
parseDevArgs,
|
|
parseTscStatus,
|
|
} from "./lib/dev-runner.mjs";
|
|
import {
|
|
APP_CONTAINER,
|
|
APP_IMAGE,
|
|
NETWORK,
|
|
SERVICES,
|
|
appRunArgs,
|
|
serviceEnv,
|
|
serviceRunArgs,
|
|
} from "./lib/dev-docker.mjs";
|
|
|
|
const require = createRequire(import.meta.url);
|
|
const { copyLocalBuild } = require("./lib/bundled-plugins.cjs");
|
|
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const BACKEND_ENTRY = path.join(
|
|
root,
|
|
"dist",
|
|
"backend",
|
|
"backend",
|
|
"starter.js",
|
|
);
|
|
const STAGED_PLUGINS = path.join(root, "dist", "plugins");
|
|
const SDK_SRC = path.join(root, "packages", "plugin-sdk", "src");
|
|
const ELECTRON_DIR = path.join(root, "electron");
|
|
const RELOAD_TIMEOUT_MS = 60_000;
|
|
const SHUTDOWN_TIMEOUT_MS = 10_000;
|
|
const DOCKER_READY_TIMEOUT_MS = 180_000;
|
|
|
|
const log = (message) => console.log(`\x1b[36m[dev]\x1b[0m ${message}`);
|
|
const warn = (message) => console.log(`\x1b[33m[dev]\x1b[0m ${message}`);
|
|
|
|
let options;
|
|
const queue = createSerialQueue();
|
|
const startedServices = [];
|
|
const watchers = [];
|
|
let shuttingDown = false;
|
|
let plugins = [];
|
|
let vite = null;
|
|
let backend = null;
|
|
let electron = null;
|
|
let tsc = null;
|
|
let dockerLogs = null;
|
|
let backendEnv = {};
|
|
|
|
function run(command, args, cwd = root) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(command, args, {
|
|
cwd,
|
|
stdio: ["ignore", "inherit", "inherit"],
|
|
shell: process.platform === "win32",
|
|
});
|
|
child.on("error", reject);
|
|
child.on("exit", (code) =>
|
|
code === 0
|
|
? resolve()
|
|
: reject(new Error(`${command} ${args.join(" ")} exited with ${code}`)),
|
|
);
|
|
});
|
|
}
|
|
|
|
function docker(args, { quiet = false } = {}) {
|
|
const result = spawnSync("docker", args, {
|
|
cwd: root,
|
|
encoding: "utf8",
|
|
stdio: quiet ? "pipe" : ["ignore", "inherit", "inherit"],
|
|
});
|
|
return { ok: result.status === 0, out: (result.stdout ?? "").trim() };
|
|
}
|
|
|
|
function watch(target, recursive, onChange) {
|
|
watchers.push(fs.watch(target, { recursive }, onChange));
|
|
}
|
|
|
|
function appUrl() {
|
|
if (options.target === "docker") return `http://localhost:${options.port}`;
|
|
return vite?.resolvedUrls?.local?.[0] ?? "http://localhost:5173";
|
|
}
|
|
|
|
function openBrowser(url) {
|
|
const [command, args] =
|
|
process.platform === "win32"
|
|
? ["cmd", ["/c", "start", "", url]]
|
|
: process.platform === "darwin"
|
|
? ["open", [url]]
|
|
: ["xdg-open", [url]];
|
|
spawn(command, args, { stdio: "ignore", detached: true }).unref();
|
|
}
|
|
|
|
// Docker side containers
|
|
|
|
function assertDocker() {
|
|
if (!docker(["info"], { quiet: true }).ok) {
|
|
throw new Error(
|
|
"Docker is not running. Start it, or drop --db, --guacd and the docker target.",
|
|
);
|
|
}
|
|
if (!docker(["network", "inspect", NETWORK], { quiet: true }).ok) {
|
|
docker(["network", "create", NETWORK], { quiet: true });
|
|
}
|
|
}
|
|
|
|
function containerState(name) {
|
|
const result = docker(["inspect", "-f", "{{.State.Running}}", name], {
|
|
quiet: true,
|
|
});
|
|
if (!result.ok) return "missing";
|
|
return result.out === "true" ? "running" : "stopped";
|
|
}
|
|
|
|
async function waitForService(service) {
|
|
if (!service.ready) return;
|
|
const deadline = Date.now() + 120_000;
|
|
while (Date.now() < deadline) {
|
|
if (
|
|
docker(["exec", service.container, ...service.ready], { quiet: true }).ok
|
|
)
|
|
return;
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
}
|
|
throw new Error(`${service.container} did not become ready`);
|
|
}
|
|
|
|
async function startService(name) {
|
|
const service = SERVICES[name];
|
|
if (options.fresh && service.volume) {
|
|
docker(["rm", "-f", service.container], { quiet: true });
|
|
docker(["volume", "rm", service.volume[0]], { quiet: true });
|
|
}
|
|
const state = containerState(service.container);
|
|
if (state === "running") {
|
|
log(`${service.container} is already running`);
|
|
} else {
|
|
log(`starting ${service.container}`);
|
|
const result =
|
|
state === "stopped"
|
|
? docker(["start", service.container], { quiet: true })
|
|
: docker(serviceRunArgs(service), { quiet: true });
|
|
if (!result.ok) throw new Error(`could not start ${service.container}`);
|
|
startedServices.push(service.container);
|
|
}
|
|
await waitForService(service);
|
|
}
|
|
|
|
async function startServices() {
|
|
if (options.db !== "sqlite") await startService(options.db);
|
|
if (options.guacd) await startService("guacd");
|
|
}
|
|
|
|
// Local backend, Vite and Electron
|
|
|
|
function notifyBrowser(data) {
|
|
vite?.ws.send({ type: "custom", event: "termix:plugins-reloaded", data });
|
|
}
|
|
|
|
function startBackend() {
|
|
const child = fork(BACKEND_ENTRY, [], {
|
|
cwd: root,
|
|
env: { ...process.env, ...backendEnv, TERMIX_DEV_RELOAD: "true" },
|
|
stdio: ["ignore", "inherit", "inherit", "ipc"],
|
|
});
|
|
const pending = new Map();
|
|
const state = { child, pending, ready: false, stopping: false };
|
|
state.readyPromise = new Promise((resolve, reject) => {
|
|
child.on("message", (msg) => {
|
|
if (msg?.type === "backend-ready") {
|
|
state.ready = true;
|
|
resolve();
|
|
return;
|
|
}
|
|
if (msg?.type === "backend-request") {
|
|
relayToElectron(child, msg);
|
|
return;
|
|
}
|
|
if (
|
|
msg?.type === "plugin-reloaded" ||
|
|
msg?.type === "plugin-reload-failed"
|
|
) {
|
|
pending.get(msg.id)?.(msg);
|
|
pending.delete(msg.id);
|
|
}
|
|
});
|
|
child.on("exit", (code) => {
|
|
for (const done of pending.values()) {
|
|
done({ type: "plugin-reload-failed", error: "backend exited" });
|
|
}
|
|
pending.clear();
|
|
if (backend === state) backend = null;
|
|
if (!state.ready) reject(new Error(`backend exited with ${code}`));
|
|
else if (!shuttingDown && !state.stopping) {
|
|
warn(
|
|
`backend exited with ${code}, press r or save a file to restart it`,
|
|
);
|
|
}
|
|
});
|
|
});
|
|
backend = state;
|
|
return state;
|
|
}
|
|
|
|
function stopChild(child, message) {
|
|
return new Promise((resolve) => {
|
|
if (!child || child.exitCode !== null || child.signalCode !== null) {
|
|
return resolve();
|
|
}
|
|
const timer = setTimeout(() => child.kill("SIGKILL"), SHUTDOWN_TIMEOUT_MS);
|
|
child.once("exit", () => {
|
|
clearTimeout(timer);
|
|
resolve();
|
|
});
|
|
try {
|
|
if (message && child.connected) child.send(message);
|
|
else child.kill();
|
|
} catch {
|
|
child.kill();
|
|
}
|
|
});
|
|
}
|
|
|
|
function stopBackend() {
|
|
const state = backend;
|
|
if (!state) return Promise.resolve();
|
|
backend = null;
|
|
state.stopping = true;
|
|
return stopChild(state.child, state.ready ? { type: "shutdown" } : null);
|
|
}
|
|
|
|
async function restartBackend(reason) {
|
|
log(reason);
|
|
await stopBackend();
|
|
if (shuttingDown) return;
|
|
fs.copyFileSync(
|
|
path.join(root, "src", "backend", "package.json"),
|
|
path.join(root, "dist", "backend", "package.json"),
|
|
);
|
|
const state = startBackend();
|
|
try {
|
|
await state.readyPromise;
|
|
} catch (error) {
|
|
warn(`${error.message}, press r or save a file to restart it`);
|
|
return;
|
|
}
|
|
log("backend ready");
|
|
if (!vite) {
|
|
await startVite();
|
|
if (options.target === "electron") startElectron();
|
|
printKeys();
|
|
} else {
|
|
notifyBrowser({});
|
|
}
|
|
}
|
|
|
|
async function startVite() {
|
|
const { createServer } = await import("vite");
|
|
vite = await createServer({
|
|
root,
|
|
configFile: path.join(root, "vite.config.ts"),
|
|
});
|
|
await vite.listen();
|
|
vite.printUrls();
|
|
}
|
|
|
|
// The packaged app forks the backend from Electron's main process, and they
|
|
// talk over that IPC channel (opening sign-in windows, the linked server's
|
|
// proxy settings). Here both are children of the runner, so it passes those
|
|
// messages along. The last proxy settings are kept for an app (re)started
|
|
// after the backend sent them.
|
|
let lastProxyConfig = null;
|
|
const REPLAY_PREFIX = "dev-replay-";
|
|
|
|
function relayToElectron(backendChild, msg) {
|
|
if (msg.channel === "sync-proxy-config") lastProxyConfig = msg;
|
|
if (electron?.connected) {
|
|
electron.send(msg);
|
|
return;
|
|
}
|
|
const ok = msg.channel === "sync-proxy-config";
|
|
try {
|
|
backendChild.send({
|
|
type: "backend-response",
|
|
id: msg.id,
|
|
ok,
|
|
...(ok
|
|
? { result: { success: true } }
|
|
: { error: "The desktop app is not running" }),
|
|
});
|
|
} catch {
|
|
// backend gone
|
|
}
|
|
}
|
|
|
|
function relayToBackend(msg) {
|
|
if (msg?.type !== "backend-response") return;
|
|
if (String(msg.id).startsWith(REPLAY_PREFIX)) return;
|
|
if (backend?.child.connected) backend.child.send(msg);
|
|
}
|
|
|
|
function startElectron() {
|
|
const env = {
|
|
...process.env,
|
|
NODE_ENV: "development",
|
|
TERMIX_DEV_RUNNER: "true",
|
|
};
|
|
// Set by some editors' terminals; Electron would start as plain Node.
|
|
delete env.ELECTRON_RUN_AS_NODE;
|
|
const child = spawn(require("electron"), ["."], {
|
|
cwd: root,
|
|
env,
|
|
stdio: ["ignore", "inherit", "inherit", "ipc"],
|
|
});
|
|
electron = child;
|
|
child.on("message", relayToBackend);
|
|
if (lastProxyConfig) {
|
|
child.send({ ...lastProxyConfig, id: `${REPLAY_PREFIX}${Date.now()}` });
|
|
}
|
|
log("desktop app started");
|
|
child.on("exit", () => {
|
|
if (electron !== child) return;
|
|
electron = null;
|
|
if (!shuttingDown) {
|
|
log("desktop app closed");
|
|
void shutdown();
|
|
}
|
|
});
|
|
}
|
|
|
|
async function restartElectron() {
|
|
const child = electron;
|
|
electron = null;
|
|
log("electron changed, restarting the desktop app");
|
|
await stopChild(child);
|
|
if (!shuttingDown) startElectron();
|
|
}
|
|
|
|
function reloadInBackend(id) {
|
|
const state = backend;
|
|
if (!state?.ready || !state.child.connected) return Promise.resolve(null);
|
|
return new Promise((resolve) => {
|
|
const timer = setTimeout(() => {
|
|
state.pending.delete(id);
|
|
resolve({ type: "plugin-reload-failed", error: "timed out" });
|
|
}, RELOAD_TIMEOUT_MS);
|
|
state.pending.set(id, (msg) => {
|
|
clearTimeout(timer);
|
|
resolve(msg);
|
|
});
|
|
state.child.send({ type: "plugin-reload", id });
|
|
});
|
|
}
|
|
|
|
function watchBackend() {
|
|
tsc = spawn(
|
|
process.execPath,
|
|
[
|
|
require.resolve("typescript/bin/tsc"),
|
|
"-p",
|
|
"tsconfig.node.json",
|
|
"--watch",
|
|
"--preserveWatchOutput",
|
|
"--pretty",
|
|
"false",
|
|
],
|
|
{ cwd: root, stdio: ["ignore", "pipe", "inherit"] },
|
|
);
|
|
let first = true;
|
|
let buffer = "";
|
|
tsc.stdout.on("data", (chunk) => {
|
|
buffer += chunk.toString();
|
|
const lines = buffer.split(/\r?\n/);
|
|
buffer = lines.pop();
|
|
for (const line of lines) {
|
|
const errors = parseTscStatus(line);
|
|
if (errors === null) {
|
|
if (line.includes("error TS")) console.log(line);
|
|
continue;
|
|
}
|
|
if (errors > 0) {
|
|
warn(`backend has ${errors} type error(s), fix them to restart it`);
|
|
continue;
|
|
}
|
|
const reason = first
|
|
? "starting the backend"
|
|
: "backend changed, restarting it";
|
|
first = false;
|
|
void queue(() => restartBackend(reason));
|
|
}
|
|
});
|
|
}
|
|
|
|
function watchSdk() {
|
|
const changed = createDebouncer(300, () =>
|
|
queue(async () => {
|
|
try {
|
|
await run("npm", ["run", "build:sdk"]);
|
|
} catch (error) {
|
|
warn(`SDK failed to build: ${error.message}`);
|
|
return;
|
|
}
|
|
await restartBackend("SDK changed, restarting the backend");
|
|
}),
|
|
);
|
|
watch(SDK_SRC, true, () => changed("sdk"));
|
|
}
|
|
|
|
function watchElectron() {
|
|
const changed = createDebouncer(300, () => restartElectron());
|
|
watch(ELECTRON_DIR, false, (_event, filename) => {
|
|
if (/\.(c?js)$/.test(String(filename)) && electron) changed("electron");
|
|
});
|
|
}
|
|
|
|
// Docker target
|
|
|
|
function dockerDataMount() {
|
|
if (options.data) return path.resolve(root, options.data);
|
|
const volume = options.fresh ? "termix-dev-data-fresh" : "termix-dev-data";
|
|
if (options.fresh) docker(["volume", "rm", "-f", volume], { quiet: true });
|
|
return volume;
|
|
}
|
|
|
|
async function buildImage() {
|
|
log(`building ${APP_IMAGE}, this takes a while`);
|
|
await run("docker", [
|
|
"build",
|
|
"-f",
|
|
"docker/Dockerfile",
|
|
"-t",
|
|
APP_IMAGE,
|
|
...(options.noCache ? ["--no-cache"] : []),
|
|
".",
|
|
]);
|
|
}
|
|
|
|
function followDockerLogs(since) {
|
|
dockerLogs?.kill();
|
|
dockerLogs = spawn(
|
|
"docker",
|
|
["logs", "-f", ...(since ? ["--since", since] : []), APP_CONTAINER],
|
|
{ cwd: root, stdio: ["ignore", "inherit", "inherit"] },
|
|
);
|
|
}
|
|
|
|
async function waitForApp() {
|
|
const deadline = Date.now() + DOCKER_READY_TIMEOUT_MS;
|
|
while (Date.now() < deadline && !shuttingDown) {
|
|
try {
|
|
const response = await fetch(appUrl());
|
|
if (response.ok) return true;
|
|
} catch {
|
|
// not up yet
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
}
|
|
return false;
|
|
}
|
|
|
|
let dockerMount = null;
|
|
|
|
async function runAppContainer() {
|
|
docker(["rm", "-f", APP_CONTAINER], { quiet: true });
|
|
dockerMount ??= dockerDataMount();
|
|
const result = docker(
|
|
appRunArgs({
|
|
port: options.port,
|
|
env: serviceEnv(options, true),
|
|
dataMount: dockerMount,
|
|
pluginsDir: STAGED_PLUGINS,
|
|
}),
|
|
{ quiet: true },
|
|
);
|
|
if (!result.ok) throw new Error(`could not start ${APP_CONTAINER}`);
|
|
followDockerLogs();
|
|
if (await waitForApp()) log(`ready on ${appUrl()}`);
|
|
else warn(`${APP_CONTAINER} is not answering on ${appUrl()} yet`);
|
|
}
|
|
|
|
async function restartAppContainer(reason) {
|
|
log(reason);
|
|
const since = new Date().toISOString();
|
|
docker(["restart", APP_CONTAINER], { quiet: true });
|
|
followDockerLogs(since);
|
|
if (await waitForApp()) log(`ready on ${appUrl()}`);
|
|
}
|
|
|
|
async function rebuildAndRunImage() {
|
|
try {
|
|
await buildImage();
|
|
} catch (error) {
|
|
warn(error.message);
|
|
return;
|
|
}
|
|
await runAppContainer();
|
|
}
|
|
|
|
function watchCoreForDocker() {
|
|
const changed = createDebouncer(500, () =>
|
|
warn("core changed, press r to rebuild the image"),
|
|
);
|
|
for (const dir of [path.join(root, "src"), SDK_SRC]) {
|
|
watch(dir, true, () => changed("core"));
|
|
}
|
|
}
|
|
|
|
// Plugins
|
|
|
|
async function rebuildPlugin({ id, repo }) {
|
|
log(`building ${id}`);
|
|
try {
|
|
await run("npm", ["run", "build"], repo);
|
|
} catch (error) {
|
|
warn(`${id} failed to build: ${error.message}`);
|
|
return;
|
|
}
|
|
const destination = path.join(STAGED_PLUGINS, id);
|
|
fs.rmSync(destination, { recursive: true, force: true });
|
|
copyLocalBuild(repo, destination);
|
|
|
|
if (options.target === "docker") {
|
|
await restartAppContainer(`${id} rebuilt, restarting ${APP_CONTAINER}`);
|
|
return;
|
|
}
|
|
const result = await reloadInBackend(id);
|
|
if (!result) {
|
|
log(`${id} staged, it loads when the backend starts`);
|
|
return;
|
|
}
|
|
if (result.type === "plugin-reload-failed") {
|
|
warn(`${id} did not reload: ${result.error}`);
|
|
return;
|
|
}
|
|
log(`${id} reloaded (${result.state})`);
|
|
notifyBrowser({ id });
|
|
}
|
|
|
|
function watchPlugins(dir) {
|
|
plugins = findBuildablePlugins(dir);
|
|
const byId = new Map(plugins.map((plugin) => [plugin.id, plugin]));
|
|
const changed = createDebouncer(300, (id) =>
|
|
queue(() => rebuildPlugin(byId.get(id))),
|
|
);
|
|
for (const plugin of plugins) {
|
|
watch(plugin.repo, true, (_event, filename) => {
|
|
if (isPluginEdit(plugin.repo, filename)) changed(plugin.id);
|
|
});
|
|
}
|
|
log(`watching ${plugins.length} plugin(s) in ${dir}`);
|
|
}
|
|
|
|
// Keys and shutdown
|
|
|
|
function printKeys() {
|
|
if (!process.stdin.isTTY) return;
|
|
log("keys: r restart, p rebuild plugins, o open browser, q quit");
|
|
}
|
|
|
|
function listenForKeys() {
|
|
if (!process.stdin.isTTY) return;
|
|
process.stdin.setRawMode(true);
|
|
process.stdin.setEncoding("utf8");
|
|
process.stdin.resume();
|
|
process.stdin.on("data", (key) => {
|
|
if (key === "\u0003" || key === "q") {
|
|
void shutdown();
|
|
} else if (key === "r") {
|
|
void queue(() =>
|
|
options.target === "docker"
|
|
? rebuildAndRunImage()
|
|
: restartBackend("restarting the backend"),
|
|
);
|
|
} else if (key === "p") {
|
|
if (plugins.length === 0) warn("no local plugins to rebuild");
|
|
for (const plugin of plugins) void queue(() => rebuildPlugin(plugin));
|
|
} else if (key === "o") {
|
|
openBrowser(appUrl());
|
|
}
|
|
});
|
|
}
|
|
|
|
async function shutdown(code = 0) {
|
|
if (shuttingDown) return;
|
|
shuttingDown = true;
|
|
log("shutting down");
|
|
if (process.stdin.isTTY) process.stdin.setRawMode(false);
|
|
for (const watcher of watchers) watcher.close();
|
|
tsc?.kill();
|
|
dockerLogs?.kill();
|
|
await Promise.allSettled([stopBackend(), stopChild(electron), vite?.close()]);
|
|
if (options.target === "docker") {
|
|
docker(["rm", "-f", APP_CONTAINER], { quiet: true });
|
|
}
|
|
for (const container of startedServices) {
|
|
docker(["stop", container], { quiet: true });
|
|
}
|
|
process.exit(code);
|
|
}
|
|
|
|
async function main() {
|
|
try {
|
|
options = parseDevArgs(process.argv.slice(2));
|
|
} catch (error) {
|
|
console.error(error.message);
|
|
process.exit(1);
|
|
}
|
|
if (options.help) {
|
|
console.log(HELP);
|
|
return;
|
|
}
|
|
|
|
process.on("SIGINT", () => shutdown());
|
|
process.on("SIGTERM", () => shutdown());
|
|
|
|
const pluginsDir = options.local
|
|
? path.resolve(
|
|
root,
|
|
options.pluginsDir ??
|
|
process.env.TERMIX_LOCAL_PLUGINS ??
|
|
"../Termix-Plugins",
|
|
)
|
|
: null;
|
|
const localPlugins =
|
|
pluginsDir && fs.existsSync(pluginsDir) ? pluginsDir : null;
|
|
if (pluginsDir && !localPlugins)
|
|
warn(`${pluginsDir} not found, using pinned plugins`);
|
|
|
|
if (needsDocker(options)) {
|
|
assertDocker();
|
|
await startServices();
|
|
}
|
|
|
|
log("building the SDK");
|
|
await run("npm", ["run", "build:sdk"]);
|
|
log(
|
|
localPlugins
|
|
? `staging plugins with local builds from ${localPlugins}`
|
|
: "staging pinned plugins",
|
|
);
|
|
await run(process.execPath, [
|
|
"scripts/build-plugins.cjs",
|
|
...(localPlugins ? ["--local", localPlugins] : []),
|
|
]);
|
|
|
|
listenForKeys();
|
|
if (localPlugins) watchPlugins(localPlugins);
|
|
|
|
if (options.target === "docker") {
|
|
await buildImage();
|
|
await runAppContainer();
|
|
watchCoreForDocker();
|
|
printKeys();
|
|
return;
|
|
}
|
|
|
|
const dataDir = dataDirFor(options);
|
|
if (dataDir) {
|
|
const full = path.resolve(root, dataDir);
|
|
if (options.fresh) fs.rmSync(full, { recursive: true, force: true });
|
|
fs.mkdirSync(full, { recursive: true });
|
|
log(`data folder ${full}`);
|
|
}
|
|
backendEnv = {
|
|
...serviceEnv(options, false),
|
|
// The desktop app runs against its own embedded backend, which is what
|
|
// turns on the local profile, sync and the desktop-only routes.
|
|
...(options.target === "electron" ? { ELECTRON_EMBEDDED: "true" } : {}),
|
|
// A folder the runner picked starts empty on purpose, so the guard
|
|
// against a mistyped DATA_DIR must not stop it.
|
|
...(dataDir
|
|
? { DATA_DIR: path.resolve(root, dataDir), ALLOW_EMPTY_DATA_DIR: "true" }
|
|
: {}),
|
|
};
|
|
|
|
log("compiling the backend");
|
|
watchBackend();
|
|
watchSdk();
|
|
if (options.target === "electron") watchElectron();
|
|
}
|
|
|
|
main().catch(async (error) => {
|
|
console.error(error?.message ?? error);
|
|
await shutdown(1);
|
|
});
|