@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 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
- return response.ok;
198
+ if (!response.ok) return void 0;
199
+ return await response.json();
196
200
  } catch {
197
- return false;
201
+ return void 0;
198
202
  }
199
203
  }
200
- async function waitForHealth(url, timeoutMs) {
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
- if (await probe(url)) return true;
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 && isAlive(existing.pid)) {
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 env = buildEnv(options, port, host, workspace);
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 || !isAlive(state.pid)) {
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 || !isAlive(state.pid)) {
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 ??= this.probeDraft();
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
- this.draftCatalog = await this.options.services.registry.draftDefaults();
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) {