feat(desktop): Electron shell M2 — embedded server, desktop login, instance lock (#173)
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
@@ -16,7 +16,8 @@
|
||||
*/
|
||||
import { spawn } from "node:child_process";
|
||||
import path from "node:path";
|
||||
import { DEFAULT_SERVER_PORT } from "@prismshadow/penguin-core";
|
||||
import { DEFAULT_SERVER_PORT, resolveRoot } from "@prismshadow/penguin-core";
|
||||
import { liveServerLock } from "@prismshadow/penguin-server/lock";
|
||||
import type { Command } from "commander";
|
||||
import type { Messages, WebProbeFailureKind } from "../i18n.js";
|
||||
|
||||
@@ -84,6 +85,19 @@ export function cliEntryFor(argv1: string | undefined): string | null {
|
||||
* so the values written here are the ones that take effect (options take priority over
|
||||
* .env and any pre-existing env vars).
|
||||
*/
|
||||
/**
|
||||
* Pre-start lock check: the App URL of a live server already owning the data root this
|
||||
* process would use (same resolution as the server: PENGUIN_HOME or the default root),
|
||||
* or null. The server itself re-checks on startup (the in-process backstop); checking
|
||||
* here keeps the friendly path — `penguin server` refuses with the URL, `penguin web`
|
||||
* simply opens the existing instance. Locks live per data root, so a second server on a
|
||||
* DIFFERENT root is untouched. See @prismshadow/penguin-server/lock.
|
||||
*/
|
||||
async function existingInstanceUrl(): Promise<string | null> {
|
||||
const lock = await liveServerLock(process.env.PENGUIN_HOME ?? resolveRoot());
|
||||
return lock === null ? null : `http://localhost:${lock.port}/`;
|
||||
}
|
||||
|
||||
async function startServer(opts: {
|
||||
port?: string;
|
||||
host?: string;
|
||||
@@ -243,6 +257,12 @@ export function registerServeCommands(program: Command, t: Messages): void {
|
||||
.option("--port <port>", t.serve.port)
|
||||
.option("--host <host>", t.serve.host)
|
||||
.action(async (opts: { port?: string; host?: string }) => {
|
||||
const existing = await existingInstanceUrl();
|
||||
if (existing !== null) {
|
||||
process.stderr.write(t.serverAlreadyRunning(existing) + "\n");
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
await startServer(opts);
|
||||
});
|
||||
|
||||
@@ -253,6 +273,12 @@ export function registerServeCommands(program: Command, t: Messages): void {
|
||||
.option("--host <host>", t.serve.host)
|
||||
.option("--no-open", t.serve.noOpen)
|
||||
.action(async (opts: { port?: string; host?: string; open: boolean }) => {
|
||||
const existing = await existingInstanceUrl();
|
||||
if (existing !== null) {
|
||||
process.stdout.write(t.webAlreadyRunning(existing) + "\n");
|
||||
if (opts.open) openBrowser(existing);
|
||||
return;
|
||||
}
|
||||
const { host, port } = await startServer(opts);
|
||||
const url = browserUrl(host, port);
|
||||
const readiness = await waitForReady(url);
|
||||
|
||||
@@ -223,6 +223,10 @@ export interface Messages {
|
||||
vaultListEmpty(): string;
|
||||
/** URL prompt once the `penguin web` service is ready. */
|
||||
webReady(url: string): string;
|
||||
/** Refusal when `penguin server` finds a live server on the same data root. */
|
||||
serverAlreadyRunning(url: string): string;
|
||||
/** Notice when `penguin web` finds a live server on the same data root (it opens that instance instead). */
|
||||
webAlreadyRunning(url: string): string;
|
||||
/** Diagnostic shown after the `penguin web` ready-poll times out (15s). */
|
||||
webProbeFailed(url: string, detail: string, kind: WebProbeFailureKind, port: number): string;
|
||||
}
|
||||
@@ -455,6 +459,11 @@ const en: Messages = {
|
||||
vaultListTitle: () => "Vault environment variables (values masked):",
|
||||
vaultListEmpty: () => "The vault is empty. Add one with `penguin config vault set`.",
|
||||
webReady: (url) => `Web UI ready: ${url}`,
|
||||
serverAlreadyRunning: (url) =>
|
||||
`A PenguinHarness server is already running on this data root: ${url}\n` +
|
||||
`Stop it first, or point PENGUIN_HOME at a separate data root.`,
|
||||
webAlreadyRunning: (url) =>
|
||||
`Already running on this data root — opening the existing instance: ${url}`,
|
||||
webProbeFailed: (url, detail, kind, port) => {
|
||||
const hint = {
|
||||
timeout:
|
||||
@@ -664,6 +673,9 @@ const zh: Messages = {
|
||||
vaultListTitle: () => "vault 环境变量(值已掩码):",
|
||||
vaultListEmpty: () => "vault 为空。用 `penguin config vault set` 添加。",
|
||||
webReady: (url) => `Web 界面已就绪:${url}`,
|
||||
serverAlreadyRunning: (url) =>
|
||||
`该数据根目录已有 PenguinHarness 服务在运行:${url}\n请先停止它,或用 PENGUIN_HOME 指定另一个数据根目录。`,
|
||||
webAlreadyRunning: (url) => `该数据根目录已有服务在运行,打开既有实例:${url}`,
|
||||
webProbeFailed: (url, detail, kind, port) => {
|
||||
const hint = {
|
||||
timeout: `连接超时。请检查防火墙或安全软件是否拦截。请允许 PenguinHarness 在本机端口 ${port} 上通信。`,
|
||||
|
||||
Reference in New Issue
Block a user