@supanadit/morse-web 0.4.0 → 0.4.1
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/dist/cli.mjs +37 -19
- package/dist/cli.mjs.map +2 -2
- package/dist/server.mjs +24 -4
- package/dist/server.mjs.map +2 -2
- package/dist/ui/index.html +1 -1
- package/dist/ui/{main-SZ5UAVYL.js → main-GW3NH5OS.js} +27 -27
- package/dist/ui/webview.manifest.json +2 -2
- package/package.json +1 -1
package/dist/cli.mjs
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { spawn } from "node:child_process";
|
|
5
|
+
import { randomUUID } from "node:crypto";
|
|
5
6
|
import { createServer } from "node:net";
|
|
6
7
|
import {
|
|
7
8
|
existsSync,
|
|
@@ -161,7 +162,8 @@ function readState() {
|
|
|
161
162
|
url: parsed.url ?? `http://${DEFAULT_HOST}:${parsed.port}/`,
|
|
162
163
|
workspace: parsed.workspace ?? process.cwd(),
|
|
163
164
|
startedAt: parsed.startedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
164
|
-
logFile: parsed.logFile ?? LOG_FILE
|
|
165
|
+
logFile: parsed.logFile ?? LOG_FILE,
|
|
166
|
+
instance: typeof parsed.instance === "string" ? parsed.instance : void 0
|
|
165
167
|
};
|
|
166
168
|
} catch {
|
|
167
169
|
return void 0;
|
|
@@ -176,6 +178,7 @@ function clearState() {
|
|
|
176
178
|
rmSync(STATE_FILE, { force: true });
|
|
177
179
|
}
|
|
178
180
|
function isAlive(pid) {
|
|
181
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
179
182
|
try {
|
|
180
183
|
process.kill(pid, 0);
|
|
181
184
|
return true;
|
|
@@ -192,15 +195,26 @@ function healthUrl(state) {
|
|
|
192
195
|
async function probe(url) {
|
|
193
196
|
try {
|
|
194
197
|
const response = await fetch(url, { signal: AbortSignal.timeout(1500) });
|
|
195
|
-
|
|
198
|
+
if (!response.ok) return void 0;
|
|
199
|
+
return await response.json();
|
|
196
200
|
} catch {
|
|
197
|
-
return
|
|
201
|
+
return void 0;
|
|
198
202
|
}
|
|
199
203
|
}
|
|
200
|
-
async function
|
|
204
|
+
async function isServerRunning(state) {
|
|
205
|
+
if (!isAlive(state.pid)) return false;
|
|
206
|
+
const health = await probe(healthUrl(state));
|
|
207
|
+
if (health?.status !== "ok") return false;
|
|
208
|
+
if (state.instance !== void 0 && health.instance !== state.instance) return false;
|
|
209
|
+
return true;
|
|
210
|
+
}
|
|
211
|
+
async function waitForHealth(url, expected, timeoutMs) {
|
|
201
212
|
const deadline = Date.now() + timeoutMs;
|
|
202
213
|
for (; ; ) {
|
|
203
|
-
|
|
214
|
+
const health = await probe(url);
|
|
215
|
+
if (health?.status === "ok" && (health.instance === void 0 || health.instance === expected)) {
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
204
218
|
if (Date.now() >= deadline) return false;
|
|
205
219
|
await sleep(250);
|
|
206
220
|
}
|
|
@@ -224,7 +238,7 @@ function sleep(ms) {
|
|
|
224
238
|
}
|
|
225
239
|
async function start(options) {
|
|
226
240
|
const existing = readState();
|
|
227
|
-
if (existing &&
|
|
241
|
+
if (existing && await isServerRunning(existing)) {
|
|
228
242
|
if (!options.force) {
|
|
229
243
|
console.log(`${yellow("Morse is already running")} (PID ${existing.pid})`);
|
|
230
244
|
console.log(` visit: ${cyan(existing.url)}`);
|
|
@@ -246,24 +260,26 @@ async function start(options) {
|
|
|
246
260
|
throw new Error(`Port ${port} is already in use. Pass --port to pick another one.`);
|
|
247
261
|
}
|
|
248
262
|
const host = options.lan ? "0.0.0.0" : options.host ?? process.env.MORSE_HOST ?? DEFAULT_HOST;
|
|
249
|
-
const
|
|
263
|
+
const instance = randomUUID();
|
|
264
|
+
const env = buildEnv(options, port, host, workspace, instance);
|
|
250
265
|
if (options.foreground) {
|
|
251
|
-
return startForeground(env, port, host, workspace);
|
|
266
|
+
return startForeground(env, port, host, workspace, instance);
|
|
252
267
|
}
|
|
253
|
-
return startDaemon(env, port, host, workspace, options);
|
|
268
|
+
return startDaemon(env, port, host, workspace, instance, options);
|
|
254
269
|
}
|
|
255
|
-
function buildEnv(options, port, host, workspace) {
|
|
270
|
+
function buildEnv(options, port, host, workspace, instance) {
|
|
256
271
|
const env = {
|
|
257
272
|
...process.env,
|
|
258
273
|
MORSE_HOST: host,
|
|
259
274
|
MORSE_PORT: String(port),
|
|
260
275
|
MORSE_UI_DIR: UI_DIR,
|
|
261
|
-
MORSE_WORKSPACE: workspace
|
|
276
|
+
MORSE_WORKSPACE: workspace,
|
|
277
|
+
MORSE_INSTANCE: instance
|
|
262
278
|
};
|
|
263
279
|
if (options.projects) env.MORSE_PROJECTS = options.projects;
|
|
264
280
|
return env;
|
|
265
281
|
}
|
|
266
|
-
async function startDaemon(env, port, host, workspace, options) {
|
|
282
|
+
async function startDaemon(env, port, host, workspace, instance, options) {
|
|
267
283
|
mkdirSync(LOG_DIR, { recursive: true });
|
|
268
284
|
writeFileSync(
|
|
269
285
|
LOG_FILE,
|
|
@@ -288,10 +304,11 @@ async function startDaemon(env, port, host, workspace, options) {
|
|
|
288
304
|
url: `http://${displayHost(host)}:${port}/`,
|
|
289
305
|
workspace,
|
|
290
306
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
291
|
-
logFile: LOG_FILE
|
|
307
|
+
logFile: LOG_FILE,
|
|
308
|
+
instance
|
|
292
309
|
};
|
|
293
310
|
writeState(state);
|
|
294
|
-
const healthy = await waitForHealth(healthUrl(state), 25e3);
|
|
311
|
+
const healthy = await waitForHealth(healthUrl(state), instance, 25e3);
|
|
295
312
|
if (!healthy) {
|
|
296
313
|
console.error(red("Morse failed to start."));
|
|
297
314
|
if (!isAlive(state.pid)) {
|
|
@@ -317,7 +334,7 @@ async function startDaemon(env, port, host, workspace, options) {
|
|
|
317
334
|
if (options.open) openBrowser(state.url);
|
|
318
335
|
return 0;
|
|
319
336
|
}
|
|
320
|
-
async function startForeground(env, port, host, workspace) {
|
|
337
|
+
async function startForeground(env, port, host, workspace, instance) {
|
|
321
338
|
const child = spawn(process.execPath, [SERVER_ENTRY], { cwd: workspace, env, stdio: "inherit" });
|
|
322
339
|
writeState({
|
|
323
340
|
pid: child.pid ?? -1,
|
|
@@ -326,7 +343,8 @@ async function startForeground(env, port, host, workspace) {
|
|
|
326
343
|
url: `http://${displayHost(host)}:${port}/`,
|
|
327
344
|
workspace,
|
|
328
345
|
startedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
329
|
-
logFile: LOG_FILE
|
|
346
|
+
logFile: LOG_FILE,
|
|
347
|
+
instance
|
|
330
348
|
});
|
|
331
349
|
const forward = (signal) => {
|
|
332
350
|
if (child.pid !== void 0) child.kill(signal);
|
|
@@ -341,7 +359,7 @@ async function startForeground(env, port, host, workspace) {
|
|
|
341
359
|
}
|
|
342
360
|
async function stop(options = {}) {
|
|
343
361
|
const state = readState();
|
|
344
|
-
if (!state || !
|
|
362
|
+
if (!state || !await isServerRunning(state)) {
|
|
345
363
|
clearState();
|
|
346
364
|
if (!options.quiet) console.log(dim("Morse is not running."));
|
|
347
365
|
return 0;
|
|
@@ -366,9 +384,9 @@ async function waitForExit(pid, timeoutMs) {
|
|
|
366
384
|
}
|
|
367
385
|
return !isAlive(pid);
|
|
368
386
|
}
|
|
369
|
-
function status(options) {
|
|
387
|
+
async function status(options) {
|
|
370
388
|
const state = readState();
|
|
371
|
-
if (!state || !
|
|
389
|
+
if (!state || !await isServerRunning(state)) {
|
|
372
390
|
clearState();
|
|
373
391
|
if (options.json) {
|
|
374
392
|
console.log(JSON.stringify({ running: false }));
|
package/dist/cli.mjs.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/cli.ts"],
|
|
4
|
-
"sourcesContent": ["/**\n * `morse` \u2014 lifecycle CLI for the standalone browser host.\n *\n * This file owns only the *process* concerns: daemonize, pidfile, log file,\n * health probe and the `stop`/`restart`/`status`/`logs` commands. Everything\n * protocol- and agent-shaped lives in `dist/server.mjs`, which is the same\n * NestJS composition root used in development \u2014 the CLI never reimplements it.\n *\n * morse start # daemonize, print the URL, exit \u2014 the terminal may close\n * morse start -f # stay in the foreground (systemd Type=simple)\n * morse status|stop|restart|logs\n */\nimport { spawn } from 'node:child_process';\nimport { createServer } from 'node:net';\nimport {\n existsSync,\n mkdirSync,\n openSync,\n readFileSync,\n rmSync,\n writeFileSync,\n} from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst CLI_DIR = dirname(fileURLToPath(import.meta.url));\nconst PACKAGE_ROOT = join(CLI_DIR, '..');\nconst SERVER_ENTRY = join(CLI_DIR, 'server.mjs');\nconst UI_DIR = join(CLI_DIR, 'ui');\nconst PACKAGE_JSON = join(PACKAGE_ROOT, 'package.json');\n\n/** Data directory: state, logs and (by default) nothing else. */\nconst MORSE_HOME = process.env.MORSE_HOME?.trim() || join(homedir(), '.morse');\nconst STATE_FILE = join(MORSE_HOME, 'server.json');\nconst LOG_DIR = join(MORSE_HOME, 'logs');\nconst LOG_FILE = join(LOG_DIR, 'server.log');\n\nconst DEFAULT_PORT = 4399;\nconst DEFAULT_HOST = '127.0.0.1';\nconst HEALTH_PATH = '/api/health';\n\ninterface ServerState {\n pid: number;\n port: number;\n host: string;\n url: string;\n workspace: string;\n startedAt: string;\n logFile: string;\n}\n\ninterface Options {\n command: string;\n port?: number;\n host?: string;\n workspace?: string;\n projects?: string;\n lan: boolean;\n foreground: boolean;\n open: boolean;\n follow: boolean;\n force: boolean;\n json: boolean;\n help: boolean;\n version: boolean;\n}\n\n// --- tiny terminal helpers (no dependencies) ---------------------------------\n\nconst colorEnabled = process.stdout.isTTY === true && process.env.NO_COLOR === undefined;\nconst paint = (code: number) => (text: string) =>\n colorEnabled ? `\\u001b[${code}m${text}\\u001b[0m` : text;\nconst dim = paint(2);\nconst bold = paint(1);\nconst cyan = paint(36);\nconst green = paint(32);\nconst yellow = paint(33);\nconst red = paint(31);\n\n// --- argument parsing --------------------------------------------------------\n\nconst COMMANDS = new Set([\n 'start',\n 'serve',\n 'stop',\n 'restart',\n 'status',\n 'logs',\n 'help',\n 'version',\n]);\n\nfunction parseArgs(argv: string[]): Options {\n const options: Options = {\n command: 'start',\n lan: false,\n foreground: false,\n open: false,\n follow: false,\n force: false,\n json: false,\n help: false,\n version: false,\n };\n let commandSeen = false;\n\n for (let index = 0; index < argv.length; index += 1) {\n const arg = argv[index];\n const [flag, inline] = splitFlag(arg);\n const takeValue = (): string => {\n if (inline !== undefined) return inline;\n const next = argv[index + 1];\n if (next === undefined || next.startsWith('-')) {\n throw new Error(`${flag} needs a value`);\n }\n index += 1;\n return next;\n };\n\n switch (flag) {\n case 'start':\n case 'serve':\n case 'stop':\n case 'restart':\n case 'status':\n case 'logs':\n case 'help':\n case 'version':\n options.command = flag;\n commandSeen = true;\n break;\n case '-p':\n case '--port':\n options.port = parsePort(takeValue(), flag);\n break;\n case '--host':\n case '--hostname':\n options.host = takeValue();\n break;\n case '--workspace':\n case '--cwd':\n options.workspace = takeValue();\n break;\n case '--projects':\n options.projects = takeValue();\n break;\n case '--lan':\n options.lan = true;\n break;\n case '-f':\n case '--foreground':\n case '--no-daemon':\n options.foreground = true;\n break;\n case '--open':\n options.open = true;\n break;\n case '--follow':\n options.follow = true;\n break;\n case '--force':\n options.force = true;\n break;\n case '--json':\n options.json = true;\n break;\n case '-h':\n case '--help':\n options.help = true;\n break;\n case '-v':\n case '--version':\n options.version = true;\n break;\n default:\n if (!arg.startsWith('-') && !commandSeen) {\n options.command = arg;\n commandSeen = true;\n break;\n }\n throw new Error(`Unknown option \"${arg}\". Run \\`morse help\\`.`);\n }\n }\n\n if (!COMMANDS.has(options.command)) {\n throw new Error(`Unknown command \"${options.command}\". Run \\`morse help\\`.`);\n }\n return options;\n}\n\nfunction splitFlag(arg: string): [string, string | undefined] {\n if (!arg.startsWith('--')) return [arg, undefined];\n const equals = arg.indexOf('=');\n return equals === -1 ? [arg, undefined] : [arg.slice(0, equals), arg.slice(equals + 1)];\n}\n\nfunction parsePort(value: string, flag: string): number {\n const port = Number.parseInt(value, 10);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw new Error(`${flag} expects a port between 1 and 65535`);\n }\n return port;\n}\n\n// --- state file --------------------------------------------------------------\n\nfunction readState(): ServerState | undefined {\n try {\n const parsed = JSON.parse(readFileSync(STATE_FILE, 'utf8')) as Partial<ServerState>;\n if (typeof parsed.pid !== 'number' || typeof parsed.port !== 'number') return undefined;\n return {\n pid: parsed.pid,\n port: parsed.port,\n host: parsed.host ?? DEFAULT_HOST,\n url: parsed.url ?? `http://${DEFAULT_HOST}:${parsed.port}/`,\n workspace: parsed.workspace ?? process.cwd(),\n startedAt: parsed.startedAt ?? new Date().toISOString(),\n logFile: parsed.logFile ?? LOG_FILE,\n };\n } catch {\n return undefined;\n }\n}\n\nfunction writeState(state: ServerState): void {\n mkdirSync(MORSE_HOME, { recursive: true });\n writeFileSync(STATE_FILE, `${JSON.stringify(state, null, 2)}\\n`, 'utf8');\n}\n\nfunction clearState(): void {\n rmSync(STATE_FILE, { force: true });\n}\n\nfunction isAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n // EPERM means the process exists but belongs to another user.\n return (error as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n// --- networking --------------------------------------------------------------\n\nfunction displayHost(host: string): string {\n return host === '0.0.0.0' || host === '::' ? DEFAULT_HOST : host;\n}\n\nfunction healthUrl(state: Pick<ServerState, 'host' | 'port'>): string {\n return `http://${displayHost(state.host)}:${state.port}${HEALTH_PATH}`;\n}\n\nasync function probe(url: string): Promise<boolean> {\n try {\n const response = await fetch(url, { signal: AbortSignal.timeout(1500) });\n return response.ok;\n } catch {\n return false;\n }\n}\n\nasync function waitForHealth(url: string, timeoutMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n if (await probe(url)) return true;\n if (Date.now() >= deadline) return false;\n await sleep(250);\n }\n}\n\nasync function isPortFree(port: number): Promise<boolean> {\n return new Promise((resolvePort) => {\n const server = createServer();\n server.once('error', () => resolvePort(false));\n server.once('listening', () => server.close(() => resolvePort(true)));\n server.listen(port, DEFAULT_HOST);\n });\n}\n\nasync function findFreePort(start: number): Promise<number> {\n for (let port = start; port < start + 100; port += 1) {\n if (await isPortFree(port)) return port;\n }\n throw new Error(`No free port found in ${start}..${start + 99}`);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));\n}\n\n// --- commands ----------------------------------------------------------------\n\nasync function start(options: Options): Promise<number> {\n const existing = readState();\n if (existing && isAlive(existing.pid)) {\n if (!options.force) {\n console.log(`${yellow('Morse is already running')} (PID ${existing.pid})`);\n console.log(` visit: ${cyan(existing.url)}`);\n console.log(` stop: ${dim(`morse stop`)}`);\n return 0;\n }\n await stop({ quiet: true });\n }\n clearState();\n\n const workspace = options.workspace ? resolve(options.workspace) : process.cwd();\n if (!existsSync(workspace)) {\n throw new Error(`Workspace does not exist: ${workspace}`);\n }\n\n const envPort = process.env.MORSE_PORT ? Number.parseInt(process.env.MORSE_PORT, 10) : NaN;\n let port = options.port ?? (Number.isInteger(envPort) ? envPort : undefined) ?? DEFAULT_PORT;\n if (options.port === undefined && !Number.isInteger(envPort)) {\n // Nobody asked for a specific port: avoid clobbering whatever already\n // listens on the default one (another Morse, a dev server, ...).\n port = await findFreePort(DEFAULT_PORT);\n } else if (!(await isPortFree(port))) {\n throw new Error(`Port ${port} is already in use. Pass --port to pick another one.`);\n }\n\n const host = options.lan ? '0.0.0.0' : (options.host ?? process.env.MORSE_HOST ?? DEFAULT_HOST);\n const env = buildEnv(options, port, host, workspace);\n\n if (options.foreground) {\n return startForeground(env, port, host, workspace);\n }\n return startDaemon(env, port, host, workspace, options);\n}\n\nfunction buildEnv(options: Options, port: number, host: string, workspace: string): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = {\n ...process.env,\n MORSE_HOST: host,\n MORSE_PORT: String(port),\n MORSE_UI_DIR: UI_DIR,\n MORSE_WORKSPACE: workspace,\n };\n if (options.projects) env.MORSE_PROJECTS = options.projects;\n return env;\n}\n\nasync function startDaemon(\n env: NodeJS.ProcessEnv,\n port: number,\n host: string,\n workspace: string,\n options: Options,\n): Promise<number> {\n mkdirSync(LOG_DIR, { recursive: true });\n writeFileSync(\n LOG_FILE,\n `\\n=== morse start ${new Date().toISOString()} (port ${port}) ===\\n`,\n { flag: 'a' },\n );\n const logFd = openSync(LOG_FILE, 'a');\n\n const child = spawn(process.execPath, [SERVER_ENTRY], {\n cwd: workspace,\n env,\n detached: true,\n // The daemon owns no terminal: closing the shell must not send it SIGHUP.\n stdio: ['ignore', logFd, logFd],\n });\n child.unref();\n\n const state: ServerState = {\n pid: child.pid ?? -1,\n port,\n host,\n url: `http://${displayHost(host)}:${port}/`,\n workspace,\n startedAt: new Date().toISOString(),\n logFile: LOG_FILE,\n };\n writeState(state);\n\n const healthy = await waitForHealth(healthUrl(state), 25_000);\n if (!healthy) {\n console.error(red('Morse failed to start.'));\n if (!isAlive(state.pid)) {\n clearState();\n console.error(dim(`Last lines of ${LOG_FILE}:`));\n console.error(tailLog(40));\n return 1;\n }\n console.error(dim('The process is alive but did not answer the health probe.'));\n console.error(dim(`Logs: morse logs`));\n return 1;\n }\n\n console.log();\n console.log(` ${green('\u25B2')} ${bold('Morse started')}`);\n console.log(` \u2502`);\n console.log(` \u25C6 port ${port} (PID: ${state.pid})`);\n console.log(` \u2502`);\n console.log(` \u25CF visit: ${cyan(state.url)}`);\n console.log(` \u25CF logs: ${dim('morse logs')}`);\n console.log(` \u2502`);\n console.log(` \u2514 daemon running \u2014 the terminal can be closed`);\n console.log();\n\n if (options.open) openBrowser(state.url);\n return 0;\n}\n\nasync function startForeground(\n env: NodeJS.ProcessEnv,\n port: number,\n host: string,\n workspace: string,\n): Promise<number> {\n const child = spawn(process.execPath, [SERVER_ENTRY], { cwd: workspace, env, stdio: 'inherit' });\n writeState({\n pid: child.pid ?? -1,\n port,\n host,\n url: `http://${displayHost(host)}:${port}/`,\n workspace,\n startedAt: new Date().toISOString(),\n logFile: LOG_FILE,\n });\n\n const forward = (signal: NodeJS.Signals): void => {\n if (child.pid !== undefined) child.kill(signal);\n };\n process.on('SIGINT', () => forward('SIGINT'));\n process.on('SIGTERM', () => forward('SIGTERM'));\n\n const code = await new Promise<number>((resolveExit) => {\n child.on('exit', (exitCode) => resolveExit(exitCode ?? 0));\n });\n clearState();\n return code;\n}\n\nasync function stop(options: { quiet?: boolean } = {}): Promise<number> {\n const state = readState();\n if (!state || !isAlive(state.pid)) {\n clearState();\n if (!options.quiet) console.log(dim('Morse is not running.'));\n return 0;\n }\n\n const { pid } = state;\n process.kill(pid, 'SIGTERM');\n if (!(await waitForExit(pid, 5000))) {\n process.kill(pid, 'SIGKILL');\n await waitForExit(pid, 2000);\n }\n clearState();\n if (!options.quiet) {\n console.log(`${green('\u25CF')} Morse stopped (PID ${pid}).`);\n }\n return 0;\n}\n\nasync function waitForExit(pid: number, timeoutMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n if (!isAlive(pid)) return true;\n await sleep(100);\n }\n return !isAlive(pid);\n}\n\nfunction status(options: Options): number {\n const state = readState();\n if (!state || !isAlive(state.pid)) {\n clearState();\n if (options.json) {\n console.log(JSON.stringify({ running: false }));\n } else {\n console.log(dim('Morse is not running.'));\n }\n return 1;\n }\n\n const uptimeMs = Date.now() - new Date(state.startedAt).getTime();\n if (options.json) {\n console.log(JSON.stringify({ running: true, ...state, uptimeMs }));\n return 0;\n }\n console.log(`${green('\u25CF')} Morse is running (PID ${state.pid})`);\n console.log(` visit: ${cyan(state.url)}`);\n console.log(` workspace: ${state.workspace}`);\n console.log(` uptime: ${formatUptime(uptimeMs)}`);\n console.log(` logs: ${dim(state.logFile)}`);\n return 0;\n}\n\nfunction formatUptime(uptimeMs: number): string {\n const seconds = Math.max(0, Math.floor(uptimeMs / 1000));\n const hours = Math.floor(seconds / 3600);\n const minutes = Math.floor((seconds % 3600) / 60);\n if (hours > 0) return `${hours}h ${minutes}m`;\n if (minutes > 0) return `${minutes}m ${seconds % 60}s`;\n return `${seconds}s`;\n}\n\nfunction tailLog(linesToShow: number): string {\n if (!existsSync(LOG_FILE)) return '(no log yet)';\n const lines = readFileSync(LOG_FILE, 'utf8').split('\\n');\n return lines.slice(-linesToShow).join('\\n');\n}\n\nasync function logs(options: Options): Promise<number> {\n if (!existsSync(LOG_FILE)) {\n console.log(dim(`No log file yet (${LOG_FILE}).`));\n return 0;\n }\n process.stdout.write(tailLog(200));\n if (!options.follow) return 0;\n\n // Simple, dependency-free `tail -f` so it works the same on every platform.\n let seen = readFileSync(LOG_FILE, 'utf8').length;\n await new Promise<void>((resolveFollow) => {\n const timer = setInterval(() => {\n try {\n const current = readFileSync(LOG_FILE, 'utf8');\n if (current.length > seen) {\n process.stdout.write(current.slice(seen));\n seen = current.length;\n } else if (current.length < seen) {\n seen = current.length;\n }\n } catch {\n /* the log was rotated away; keep waiting */\n }\n }, 400);\n const stopFollowing = (): void => {\n clearInterval(timer);\n resolveFollow();\n };\n process.on('SIGINT', stopFollowing);\n process.on('SIGTERM', stopFollowing);\n });\n return 0;\n}\n\nfunction openBrowser(url: string): void {\n const command =\n process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';\n const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];\n try {\n const child = spawn(command, args, { stdio: 'ignore', detached: true });\n child.unref();\n } catch {\n /* opening a browser is best-effort */\n }\n}\n\nfunction readVersion(): string {\n try {\n return (JSON.parse(readFileSync(PACKAGE_JSON, 'utf8')) as { version?: string }).version ?? '0.0.0';\n } catch {\n return '0.0.0';\n }\n}\n\nfunction printHelp(): void {\n const name = `@supanadit/morse-web`;\n console.log(`\n ${bold('Morse')} \u2014 the Comfortable Pi Interface, in your browser (${name})\n\n ${bold('USAGE')}\n morse [COMMAND] [OPTIONS]\n\n ${bold('COMMANDS')}\n start Start the web server in the background (default)\n stop Stop the running server\n restart Stop, then start the server\n status Show whether the server is running\n logs Print the server log (--follow to tail)\n help Show this help\n version Print the version\n\n ${bold('OPTIONS')}\n -p, --port <n> Web server port (default: ${DEFAULT_PORT}, next free if taken)\n --host <addr> Bind address (default: ${DEFAULT_HOST})\n --lan Bind 0.0.0.0 so the LAN can reach it\n -f, --foreground Run in the foreground (systemd Type=simple)\n --workspace <dir> Directory the agent works in (default: the current one)\n --projects <list> Comma/colon separated list of roots the agent may open\n --open Open the browser once the server is healthy\n --force Replace a server this CLI already started\n --json Machine-readable output (status)\n --follow Follow the log file (logs)\n -h, --help Show this help\n -v, --version Print the version\n\n ${bold('ENVIRONMENT')}\n MORSE_HOME Data directory (default: ~/.morse)\n MORSE_PORT Default port\n MORSE_HOST Default bind address\n MORSE_WORKSPACE Workspace the agent runs in\n MORSE_PROJECTS Roots the agent may open\n MORSE_HOT_SESSIONS How many pi processes stay alive (default: 4)\n MORSE_PI_PATH Path to the pi executable (default: pi on PATH)\n MORSE_PI_ENTRY Run a pi RPC entry with node instead of the binary\n\n ${bold('EXAMPLES')}\n morse # start the daemon on ${DEFAULT_PORT}\n morse start --port 8080 # a specific port\n morse --lan --port 3002 # reachable on the LAN\n morse start --open # start and open the browser\n morse logs --follow # tail the server log\n morse stop # stop the daemon\n`);\n}\n\n// --- entry point -------------------------------------------------------------\n\nexport async function run(argv: string[]): Promise<number> {\n const options = parseArgs(argv);\n\n if (options.version || options.command === 'version') {\n console.log(readVersion());\n return 0;\n }\n if (options.help || options.command === 'help') {\n printHelp();\n return 0;\n }\n\n switch (options.command) {\n case 'start':\n case 'serve':\n return start(options);\n case 'stop':\n return stop();\n case 'restart':\n await stop({ quiet: true });\n return start(options);\n case 'status':\n return status(options);\n case 'logs':\n return logs(options);\n default:\n printHelp();\n return 0;\n }\n}\n\nrun(process.argv.slice(2))\n .then((code) => {\n process.exitCode = code;\n })\n .catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`${red('morse:')} ${message}`);\n process.exitCode = 1;\n });\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["/**\n * `morse` \u2014 lifecycle CLI for the standalone browser host.\n *\n * This file owns only the *process* concerns: daemonize, pidfile, log file,\n * health probe and the `stop`/`restart`/`status`/`logs` commands. Everything\n * protocol- and agent-shaped lives in `dist/server.mjs`, which is the same\n * NestJS composition root used in development \u2014 the CLI never reimplements it.\n *\n * morse start # daemonize, print the URL, exit \u2014 the terminal may close\n * morse start -f # stay in the foreground (systemd Type=simple)\n * morse status|stop|restart|logs\n */\nimport { spawn } from 'node:child_process';\nimport { randomUUID } from 'node:crypto';\nimport { createServer } from 'node:net';\nimport {\n existsSync,\n mkdirSync,\n openSync,\n readFileSync,\n rmSync,\n writeFileSync,\n} from 'node:fs';\nimport { homedir } from 'node:os';\nimport { dirname, join, resolve } from 'node:path';\nimport { fileURLToPath } from 'node:url';\n\nconst CLI_DIR = dirname(fileURLToPath(import.meta.url));\nconst PACKAGE_ROOT = join(CLI_DIR, '..');\nconst SERVER_ENTRY = join(CLI_DIR, 'server.mjs');\nconst UI_DIR = join(CLI_DIR, 'ui');\nconst PACKAGE_JSON = join(PACKAGE_ROOT, 'package.json');\n\n/** Data directory: state, logs and (by default) nothing else. */\nconst MORSE_HOME = process.env.MORSE_HOME?.trim() || join(homedir(), '.morse');\nconst STATE_FILE = join(MORSE_HOME, 'server.json');\nconst LOG_DIR = join(MORSE_HOME, 'logs');\nconst LOG_FILE = join(LOG_DIR, 'server.log');\n\nconst DEFAULT_PORT = 4399;\nconst DEFAULT_HOST = '127.0.0.1';\nconst HEALTH_PATH = '/api/health';\n\ninterface ServerState {\n pid: number;\n port: number;\n host: string;\n url: string;\n workspace: string;\n startedAt: string;\n logFile: string;\n /**\n * Random token minted per start and echoed by `/api/health`. A pid alone is\n * not proof of ownership: after a force-kill the state file survives and the\n * OS may reassign that pid to an unrelated process. Optional so state written\n * by an older CLI still loads (the check then falls back to the health body).\n */\n instance?: string;\n}\n\ninterface Options {\n command: string;\n port?: number;\n host?: string;\n workspace?: string;\n projects?: string;\n lan: boolean;\n foreground: boolean;\n open: boolean;\n follow: boolean;\n force: boolean;\n json: boolean;\n help: boolean;\n version: boolean;\n}\n\n// --- tiny terminal helpers (no dependencies) ---------------------------------\n\nconst colorEnabled = process.stdout.isTTY === true && process.env.NO_COLOR === undefined;\nconst paint = (code: number) => (text: string) =>\n colorEnabled ? `\\u001b[${code}m${text}\\u001b[0m` : text;\nconst dim = paint(2);\nconst bold = paint(1);\nconst cyan = paint(36);\nconst green = paint(32);\nconst yellow = paint(33);\nconst red = paint(31);\n\n// --- argument parsing --------------------------------------------------------\n\nconst COMMANDS = new Set([\n 'start',\n 'serve',\n 'stop',\n 'restart',\n 'status',\n 'logs',\n 'help',\n 'version',\n]);\n\nfunction parseArgs(argv: string[]): Options {\n const options: Options = {\n command: 'start',\n lan: false,\n foreground: false,\n open: false,\n follow: false,\n force: false,\n json: false,\n help: false,\n version: false,\n };\n let commandSeen = false;\n\n for (let index = 0; index < argv.length; index += 1) {\n const arg = argv[index];\n const [flag, inline] = splitFlag(arg);\n const takeValue = (): string => {\n if (inline !== undefined) return inline;\n const next = argv[index + 1];\n if (next === undefined || next.startsWith('-')) {\n throw new Error(`${flag} needs a value`);\n }\n index += 1;\n return next;\n };\n\n switch (flag) {\n case 'start':\n case 'serve':\n case 'stop':\n case 'restart':\n case 'status':\n case 'logs':\n case 'help':\n case 'version':\n options.command = flag;\n commandSeen = true;\n break;\n case '-p':\n case '--port':\n options.port = parsePort(takeValue(), flag);\n break;\n case '--host':\n case '--hostname':\n options.host = takeValue();\n break;\n case '--workspace':\n case '--cwd':\n options.workspace = takeValue();\n break;\n case '--projects':\n options.projects = takeValue();\n break;\n case '--lan':\n options.lan = true;\n break;\n case '-f':\n case '--foreground':\n case '--no-daemon':\n options.foreground = true;\n break;\n case '--open':\n options.open = true;\n break;\n case '--follow':\n options.follow = true;\n break;\n case '--force':\n options.force = true;\n break;\n case '--json':\n options.json = true;\n break;\n case '-h':\n case '--help':\n options.help = true;\n break;\n case '-v':\n case '--version':\n options.version = true;\n break;\n default:\n if (!arg.startsWith('-') && !commandSeen) {\n options.command = arg;\n commandSeen = true;\n break;\n }\n throw new Error(`Unknown option \"${arg}\". Run \\`morse help\\`.`);\n }\n }\n\n if (!COMMANDS.has(options.command)) {\n throw new Error(`Unknown command \"${options.command}\". Run \\`morse help\\`.`);\n }\n return options;\n}\n\nfunction splitFlag(arg: string): [string, string | undefined] {\n if (!arg.startsWith('--')) return [arg, undefined];\n const equals = arg.indexOf('=');\n return equals === -1 ? [arg, undefined] : [arg.slice(0, equals), arg.slice(equals + 1)];\n}\n\nfunction parsePort(value: string, flag: string): number {\n const port = Number.parseInt(value, 10);\n if (!Number.isInteger(port) || port < 1 || port > 65535) {\n throw new Error(`${flag} expects a port between 1 and 65535`);\n }\n return port;\n}\n\n// --- state file --------------------------------------------------------------\n\nfunction readState(): ServerState | undefined {\n try {\n const parsed = JSON.parse(readFileSync(STATE_FILE, 'utf8')) as Partial<ServerState>;\n if (typeof parsed.pid !== 'number' || typeof parsed.port !== 'number') return undefined;\n return {\n pid: parsed.pid,\n port: parsed.port,\n host: parsed.host ?? DEFAULT_HOST,\n url: parsed.url ?? `http://${DEFAULT_HOST}:${parsed.port}/`,\n workspace: parsed.workspace ?? process.cwd(),\n startedAt: parsed.startedAt ?? new Date().toISOString(),\n logFile: parsed.logFile ?? LOG_FILE,\n instance: typeof parsed.instance === 'string' ? parsed.instance : undefined,\n };\n } catch {\n return undefined;\n }\n}\n\nfunction writeState(state: ServerState): void {\n mkdirSync(MORSE_HOME, { recursive: true });\n writeFileSync(STATE_FILE, `${JSON.stringify(state, null, 2)}\\n`, 'utf8');\n}\n\nfunction clearState(): void {\n rmSync(STATE_FILE, { force: true });\n}\n\nfunction isAlive(pid: number): boolean {\n // `process.kill(0, 0)` and `process.kill(-1, 0)` are not \"does this pid\n // exist\" \u2014 the latter answers true whenever *any* process is signalable.\n if (!Number.isInteger(pid) || pid <= 0) return false;\n try {\n process.kill(pid, 0);\n return true;\n } catch (error) {\n // EPERM means the process exists but belongs to another user.\n return (error as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n// --- networking --------------------------------------------------------------\n\nfunction displayHost(host: string): string {\n return host === '0.0.0.0' || host === '::' ? DEFAULT_HOST : host;\n}\n\nfunction healthUrl(state: Pick<ServerState, 'host' | 'port'>): string {\n return `http://${displayHost(state.host)}:${state.port}${HEALTH_PATH}`;\n}\n\ninterface HealthPayload {\n status?: string;\n instance?: string;\n}\n\nasync function probe(url: string): Promise<HealthPayload | undefined> {\n try {\n const response = await fetch(url, { signal: AbortSignal.timeout(1500) });\n if (!response.ok) return undefined;\n return (await response.json()) as HealthPayload;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Is the daemon recorded in `state` really the one that is running?\n *\n * A live pid is necessary but not sufficient: a force-killed daemon leaves the\n * state file behind and its pid can be recycled by any later process. Only a\n * matching `instance` (or, for legacy state, a healthy Morse payload) counts.\n */\nasync function isServerRunning(state: ServerState): Promise<boolean> {\n if (!isAlive(state.pid)) return false;\n const health = await probe(healthUrl(state));\n if (health?.status !== 'ok') return false;\n if (state.instance !== undefined && health.instance !== state.instance) return false;\n return true;\n}\n\nasync function waitForHealth(url: string, expected: string, timeoutMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n for (;;) {\n const health = await probe(url);\n if (health?.status === 'ok' && (health.instance === undefined || health.instance === expected)) {\n return true;\n }\n if (Date.now() >= deadline) return false;\n await sleep(250);\n }\n}\n\nasync function isPortFree(port: number): Promise<boolean> {\n return new Promise((resolvePort) => {\n const server = createServer();\n server.once('error', () => resolvePort(false));\n server.once('listening', () => server.close(() => resolvePort(true)));\n server.listen(port, DEFAULT_HOST);\n });\n}\n\nasync function findFreePort(start: number): Promise<number> {\n for (let port = start; port < start + 100; port += 1) {\n if (await isPortFree(port)) return port;\n }\n throw new Error(`No free port found in ${start}..${start + 99}`);\n}\n\nfunction sleep(ms: number): Promise<void> {\n return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));\n}\n\n// --- commands ----------------------------------------------------------------\n\nasync function start(options: Options): Promise<number> {\n const existing = readState();\n if (existing && (await isServerRunning(existing))) {\n if (!options.force) {\n console.log(`${yellow('Morse is already running')} (PID ${existing.pid})`);\n console.log(` visit: ${cyan(existing.url)}`);\n console.log(` stop: ${dim(`morse stop`)}`);\n return 0;\n }\n await stop({ quiet: true });\n }\n // Either no state, or state whose process is gone / no longer Morse.\n clearState();\n\n const workspace = options.workspace ? resolve(options.workspace) : process.cwd();\n if (!existsSync(workspace)) {\n throw new Error(`Workspace does not exist: ${workspace}`);\n }\n\n const envPort = process.env.MORSE_PORT ? Number.parseInt(process.env.MORSE_PORT, 10) : NaN;\n let port = options.port ?? (Number.isInteger(envPort) ? envPort : undefined) ?? DEFAULT_PORT;\n if (options.port === undefined && !Number.isInteger(envPort)) {\n // Nobody asked for a specific port: avoid clobbering whatever already\n // listens on the default one (another Morse, a dev server, ...).\n port = await findFreePort(DEFAULT_PORT);\n } else if (!(await isPortFree(port))) {\n throw new Error(`Port ${port} is already in use. Pass --port to pick another one.`);\n }\n\n const host = options.lan ? '0.0.0.0' : (options.host ?? process.env.MORSE_HOST ?? DEFAULT_HOST);\n const instance = randomUUID();\n const env = buildEnv(options, port, host, workspace, instance);\n\n if (options.foreground) {\n return startForeground(env, port, host, workspace, instance);\n }\n return startDaemon(env, port, host, workspace, instance, options);\n}\n\nfunction buildEnv(\n options: Options,\n port: number,\n host: string,\n workspace: string,\n instance: string,\n): NodeJS.ProcessEnv {\n const env: NodeJS.ProcessEnv = {\n ...process.env,\n MORSE_HOST: host,\n MORSE_PORT: String(port),\n MORSE_UI_DIR: UI_DIR,\n MORSE_WORKSPACE: workspace,\n MORSE_INSTANCE: instance,\n };\n if (options.projects) env.MORSE_PROJECTS = options.projects;\n return env;\n}\n\nasync function startDaemon(\n env: NodeJS.ProcessEnv,\n port: number,\n host: string,\n workspace: string,\n instance: string,\n options: Options,\n): Promise<number> {\n mkdirSync(LOG_DIR, { recursive: true });\n writeFileSync(\n LOG_FILE,\n `\\n=== morse start ${new Date().toISOString()} (port ${port}) ===\\n`,\n { flag: 'a' },\n );\n const logFd = openSync(LOG_FILE, 'a');\n\n const child = spawn(process.execPath, [SERVER_ENTRY], {\n cwd: workspace,\n env,\n detached: true,\n // The daemon owns no terminal: closing the shell must not send it SIGHUP.\n stdio: ['ignore', logFd, logFd],\n });\n child.unref();\n\n const state: ServerState = {\n pid: child.pid ?? -1,\n port,\n host,\n url: `http://${displayHost(host)}:${port}/`,\n workspace,\n startedAt: new Date().toISOString(),\n logFile: LOG_FILE,\n instance,\n };\n writeState(state);\n\n const healthy = await waitForHealth(healthUrl(state), instance, 25_000);\n if (!healthy) {\n console.error(red('Morse failed to start.'));\n if (!isAlive(state.pid)) {\n clearState();\n console.error(dim(`Last lines of ${LOG_FILE}:`));\n console.error(tailLog(40));\n return 1;\n }\n console.error(dim('The process is alive but did not answer the health probe.'));\n console.error(dim(`Logs: morse logs`));\n return 1;\n }\n\n console.log();\n console.log(` ${green('\u25B2')} ${bold('Morse started')}`);\n console.log(` \u2502`);\n console.log(` \u25C6 port ${port} (PID: ${state.pid})`);\n console.log(` \u2502`);\n console.log(` \u25CF visit: ${cyan(state.url)}`);\n console.log(` \u25CF logs: ${dim('morse logs')}`);\n console.log(` \u2502`);\n console.log(` \u2514 daemon running \u2014 the terminal can be closed`);\n console.log();\n\n if (options.open) openBrowser(state.url);\n return 0;\n}\n\nasync function startForeground(\n env: NodeJS.ProcessEnv,\n port: number,\n host: string,\n workspace: string,\n instance: string,\n): Promise<number> {\n const child = spawn(process.execPath, [SERVER_ENTRY], { cwd: workspace, env, stdio: 'inherit' });\n writeState({\n pid: child.pid ?? -1,\n port,\n host,\n url: `http://${displayHost(host)}:${port}/`,\n workspace,\n startedAt: new Date().toISOString(),\n logFile: LOG_FILE,\n instance,\n });\n\n const forward = (signal: NodeJS.Signals): void => {\n if (child.pid !== undefined) child.kill(signal);\n };\n process.on('SIGINT', () => forward('SIGINT'));\n process.on('SIGTERM', () => forward('SIGTERM'));\n\n const code = await new Promise<number>((resolveExit) => {\n child.on('exit', (exitCode) => resolveExit(exitCode ?? 0));\n });\n clearState();\n return code;\n}\n\nasync function stop(options: { quiet?: boolean } = {}): Promise<number> {\n const state = readState();\n if (!state || !(await isServerRunning(state))) {\n clearState();\n if (!options.quiet) console.log(dim('Morse is not running.'));\n return 0;\n }\n\n const { pid } = state;\n process.kill(pid, 'SIGTERM');\n if (!(await waitForExit(pid, 5000))) {\n process.kill(pid, 'SIGKILL');\n await waitForExit(pid, 2000);\n }\n clearState();\n if (!options.quiet) {\n console.log(`${green('\u25CF')} Morse stopped (PID ${pid}).`);\n }\n return 0;\n}\n\nasync function waitForExit(pid: number, timeoutMs: number): Promise<boolean> {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n if (!isAlive(pid)) return true;\n await sleep(100);\n }\n return !isAlive(pid);\n}\n\nasync function status(options: Options): Promise<number> {\n const state = readState();\n if (!state || !(await isServerRunning(state))) {\n clearState();\n if (options.json) {\n console.log(JSON.stringify({ running: false }));\n } else {\n console.log(dim('Morse is not running.'));\n }\n return 1;\n }\n\n const uptimeMs = Date.now() - new Date(state.startedAt).getTime();\n if (options.json) {\n console.log(JSON.stringify({ running: true, ...state, uptimeMs }));\n return 0;\n }\n console.log(`${green('\u25CF')} Morse is running (PID ${state.pid})`);\n console.log(` visit: ${cyan(state.url)}`);\n console.log(` workspace: ${state.workspace}`);\n console.log(` uptime: ${formatUptime(uptimeMs)}`);\n console.log(` logs: ${dim(state.logFile)}`);\n return 0;\n}\n\nfunction formatUptime(uptimeMs: number): string {\n const seconds = Math.max(0, Math.floor(uptimeMs / 1000));\n const hours = Math.floor(seconds / 3600);\n const minutes = Math.floor((seconds % 3600) / 60);\n if (hours > 0) return `${hours}h ${minutes}m`;\n if (minutes > 0) return `${minutes}m ${seconds % 60}s`;\n return `${seconds}s`;\n}\n\nfunction tailLog(linesToShow: number): string {\n if (!existsSync(LOG_FILE)) return '(no log yet)';\n const lines = readFileSync(LOG_FILE, 'utf8').split('\\n');\n return lines.slice(-linesToShow).join('\\n');\n}\n\nasync function logs(options: Options): Promise<number> {\n if (!existsSync(LOG_FILE)) {\n console.log(dim(`No log file yet (${LOG_FILE}).`));\n return 0;\n }\n process.stdout.write(tailLog(200));\n if (!options.follow) return 0;\n\n // Simple, dependency-free `tail -f` so it works the same on every platform.\n let seen = readFileSync(LOG_FILE, 'utf8').length;\n await new Promise<void>((resolveFollow) => {\n const timer = setInterval(() => {\n try {\n const current = readFileSync(LOG_FILE, 'utf8');\n if (current.length > seen) {\n process.stdout.write(current.slice(seen));\n seen = current.length;\n } else if (current.length < seen) {\n seen = current.length;\n }\n } catch {\n /* the log was rotated away; keep waiting */\n }\n }, 400);\n const stopFollowing = (): void => {\n clearInterval(timer);\n resolveFollow();\n };\n process.on('SIGINT', stopFollowing);\n process.on('SIGTERM', stopFollowing);\n });\n return 0;\n}\n\nfunction openBrowser(url: string): void {\n const command =\n process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';\n const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];\n try {\n const child = spawn(command, args, { stdio: 'ignore', detached: true });\n child.unref();\n } catch {\n /* opening a browser is best-effort */\n }\n}\n\nfunction readVersion(): string {\n try {\n return (JSON.parse(readFileSync(PACKAGE_JSON, 'utf8')) as { version?: string }).version ?? '0.0.0';\n } catch {\n return '0.0.0';\n }\n}\n\nfunction printHelp(): void {\n const name = `@supanadit/morse-web`;\n console.log(`\n ${bold('Morse')} \u2014 the Comfortable Pi Interface, in your browser (${name})\n\n ${bold('USAGE')}\n morse [COMMAND] [OPTIONS]\n\n ${bold('COMMANDS')}\n start Start the web server in the background (default)\n stop Stop the running server\n restart Stop, then start the server\n status Show whether the server is running\n logs Print the server log (--follow to tail)\n help Show this help\n version Print the version\n\n ${bold('OPTIONS')}\n -p, --port <n> Web server port (default: ${DEFAULT_PORT}, next free if taken)\n --host <addr> Bind address (default: ${DEFAULT_HOST})\n --lan Bind 0.0.0.0 so the LAN can reach it\n -f, --foreground Run in the foreground (systemd Type=simple)\n --workspace <dir> Directory the agent works in (default: the current one)\n --projects <list> Comma/colon separated list of roots the agent may open\n --open Open the browser once the server is healthy\n --force Replace a server this CLI already started\n --json Machine-readable output (status)\n --follow Follow the log file (logs)\n -h, --help Show this help\n -v, --version Print the version\n\n ${bold('ENVIRONMENT')}\n MORSE_HOME Data directory (default: ~/.morse)\n MORSE_PORT Default port\n MORSE_HOST Default bind address\n MORSE_WORKSPACE Workspace the agent runs in\n MORSE_PROJECTS Roots the agent may open\n MORSE_HOT_SESSIONS How many pi processes stay alive (default: 4)\n MORSE_PI_PATH Path to the pi executable (default: pi on PATH)\n MORSE_PI_ENTRY Run a pi RPC entry with node instead of the binary\n\n ${bold('EXAMPLES')}\n morse # start the daemon on ${DEFAULT_PORT}\n morse start --port 8080 # a specific port\n morse --lan --port 3002 # reachable on the LAN\n morse start --open # start and open the browser\n morse logs --follow # tail the server log\n morse stop # stop the daemon\n`);\n}\n\n// --- entry point -------------------------------------------------------------\n\nexport async function run(argv: string[]): Promise<number> {\n const options = parseArgs(argv);\n\n if (options.version || options.command === 'version') {\n console.log(readVersion());\n return 0;\n }\n if (options.help || options.command === 'help') {\n printHelp();\n return 0;\n }\n\n switch (options.command) {\n case 'start':\n case 'serve':\n return start(options);\n case 'stop':\n return stop();\n case 'restart':\n await stop({ quiet: true });\n return start(options);\n case 'status':\n return status(options);\n case 'logs':\n return logs(options);\n default:\n printHelp();\n return 0;\n }\n}\n\nrun(process.argv.slice(2))\n .then((code) => {\n process.exitCode = code;\n })\n .catch((error: unknown) => {\n const message = error instanceof Error ? error.message : String(error);\n console.error(`${red('morse:')} ${message}`);\n process.exitCode = 1;\n });\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": ["start"]
|
|
7
7
|
}
|
package/dist/server.mjs
CHANGED
|
@@ -211,7 +211,12 @@ var SessionRegistry = class {
|
|
|
211
211
|
if (this.draftCache) {
|
|
212
212
|
return this.draftCache;
|
|
213
213
|
}
|
|
214
|
-
this.draftProbe
|
|
214
|
+
if (!this.draftProbe) {
|
|
215
|
+
this.draftProbe = this.probeDraft().catch((error) => {
|
|
216
|
+
this.draftProbe = void 0;
|
|
217
|
+
throw error;
|
|
218
|
+
});
|
|
219
|
+
}
|
|
215
220
|
return this.draftProbe;
|
|
216
221
|
}
|
|
217
222
|
/** Opens a new (or resumes an existing) session and makes it active. */
|
|
@@ -441,6 +446,9 @@ var SessionRegistry = class {
|
|
|
441
446
|
await gateway.dispose();
|
|
442
447
|
return state;
|
|
443
448
|
} catch (error) {
|
|
449
|
+
if (error instanceof AgentUnavailableError) {
|
|
450
|
+
throw error;
|
|
451
|
+
}
|
|
444
452
|
this.deps.logger.warn("Could not probe the draft model catalog", error);
|
|
445
453
|
return void 0;
|
|
446
454
|
}
|
|
@@ -2817,7 +2825,15 @@ var HostSessionController = class {
|
|
|
2817
2825
|
}
|
|
2818
2826
|
/** Fills the draft pickers from the registry's session-less probe. */
|
|
2819
2827
|
async warmDraft() {
|
|
2820
|
-
|
|
2828
|
+
try {
|
|
2829
|
+
this.draftCatalog = await this.options.services.registry.draftDefaults();
|
|
2830
|
+
} catch (error) {
|
|
2831
|
+
this.agentError = describeError(error);
|
|
2832
|
+
this.agentFailure = describeAgentFailure(error, this.options.agentHint);
|
|
2833
|
+
this.options.logger.warn("Could not probe the draft model catalog", error);
|
|
2834
|
+
this.emitState();
|
|
2835
|
+
return;
|
|
2836
|
+
}
|
|
2821
2837
|
if (this.activeKey === void 0 && !this.disposed) {
|
|
2822
2838
|
this.emitState();
|
|
2823
2839
|
}
|
|
@@ -3544,7 +3560,10 @@ var HealthController = class {
|
|
|
3544
3560
|
status: "ok",
|
|
3545
3561
|
protocolVersion: PROTOCOL_VERSION,
|
|
3546
3562
|
workspace: this.config.workspace.cwd,
|
|
3547
|
-
frontend: this.config.uiDir
|
|
3563
|
+
frontend: this.config.uiDir,
|
|
3564
|
+
// Echoed so the CLI that spawned this daemon can tell it apart from an
|
|
3565
|
+
// unrelated process that later reused the same pid.
|
|
3566
|
+
...this.config.instance ? { instance: this.config.instance } : {}
|
|
3548
3567
|
};
|
|
3549
3568
|
}
|
|
3550
3569
|
async manifest() {
|
|
@@ -4116,7 +4135,8 @@ function loadConfig(env = process.env) {
|
|
|
4116
4135
|
hotSessions: parsePositiveInt(env.MORSE_HOT_SESSIONS, 4),
|
|
4117
4136
|
projects: parseList(env.MORSE_PROJECTS),
|
|
4118
4137
|
uploadDir: env.MORSE_UPLOAD_DIR?.trim() || DEFAULT_UPLOAD_DIR,
|
|
4119
|
-
updateCheck: !isOff(env.MORSE_UPDATE_CHECK)
|
|
4138
|
+
updateCheck: !isOff(env.MORSE_UPDATE_CHECK),
|
|
4139
|
+
instance: env.MORSE_INSTANCE?.trim() || void 0
|
|
4120
4140
|
};
|
|
4121
4141
|
}
|
|
4122
4142
|
function readFrontendIdentity(uiDir) {
|