@supanadit/morse-web 0.2.0

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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 supanadit
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,95 @@
1
+ # @supanadit/morse-web
2
+
3
+ **Morse in your browser, as one npm package.** It serves the same Angular frontend and drives the same
4
+ [Pi](https://github.com/earendil-works/pi) coding agent as the VS Code extension — no VS Code required.
5
+
6
+ ```bash
7
+ npm install -g @supanadit/morse-web
8
+ morse start
9
+ ```
10
+
11
+ ```
12
+ ▲ Morse started
13
+ │
14
+ ◆ port 4399 (PID: 20880)
15
+ │
16
+ ● visit: http://127.0.0.1:4399/
17
+ ● logs: morse logs
18
+ │
19
+ └ daemon running — the terminal can be closed
20
+ ```
21
+
22
+ Then open <http://127.0.0.1:4399/>. The server keeps running in the background; stop it with `morse stop`.
23
+
24
+ ## Requirements
25
+
26
+ - Node.js 20+ (developed on 24).
27
+ - The `pi` CLI on your `PATH` and already authenticated (`pi --version`), or a path via `MORSE_PI_PATH`.
28
+ Morse reuses your existing pi configuration: models, credentials, tools and sessions under `~/.pi`.
29
+
30
+ ## Commands
31
+
32
+ | Command | What it does |
33
+ |---|---|
34
+ | `morse` / `morse start` | Start the server as a background daemon and print the URL |
35
+ | `morse start -f` | Run in the foreground (systemd `Type=simple`, Docker) |
36
+ | `morse stop` | Stop the running server |
37
+ | `morse restart` | Stop, then start |
38
+ | `morse status` | Show PID, URL, workspace and uptime (`--json` for scripts) |
39
+ | `morse logs` | Print the server log (`--follow` to tail) |
40
+ | `morse help`, `morse version` | Help / version |
41
+
42
+ ### Options
43
+
44
+ ```
45
+ -p, --port <n> Web server port (default: 4399, next free if taken)
46
+ --host <addr> Bind address (default: 127.0.0.1)
47
+ --lan Bind 0.0.0.0 so the LAN can reach it
48
+ -f, --foreground Run in the foreground
49
+ --workspace <dir> Directory the agent works in (default: the current one)
50
+ --projects <list> Comma/colon separated roots the agent may open
51
+ --open Open the browser once healthy
52
+ --force Replace a server this CLI already started
53
+ ```
54
+
55
+ ### Environment
56
+
57
+ ```
58
+ MORSE_HOME Data directory (default: ~/.morse)
59
+ MORSE_PORT Default port
60
+ MORSE_HOST Default bind address
61
+ MORSE_WORKSPACE Workspace the agent runs in
62
+ MORSE_PROJECTS Roots the agent may open
63
+ MORSE_HOT_SESSIONS How many pi processes stay alive (default: 4)
64
+ MORSE_PI_PATH Path to the pi executable (default: pi on PATH)
65
+ MORSE_PI_ENTRY Run a pi RPC entry with node instead of the binary
66
+ MORSE_UI_DIR Override the served frontend directory
67
+ ```
68
+
69
+ ## Security
70
+
71
+ `morse start` binds `127.0.0.1` by default. `--lan` exposes an **unauthenticated** chat UI to your network:
72
+ only do it on a trusted network, or put a TLS reverse proxy with auth in front of it.
73
+
74
+ ## How the single package is built
75
+
76
+ Everything lives in one tarball:
77
+
78
+ ```
79
+ dist/server.mjs NestJS host, with every @morse/* workspace package inlined
80
+ dist/cli.mjs the `morse` lifecycle CLI
81
+ dist/ui/ the built Angular frontend, served as static assets
82
+ ```
83
+
84
+ The `@morse/*` packages are private workspace libraries, so they cannot be installed from npm — bundling
85
+ them into `server.mjs` is what makes a single publishable artifact possible. The NestJS runtime
86
+ (`@nestjs/*`, `rxjs`, `ws`, `reflect-metadata`) stays external and is installed normally from
87
+ `dependencies`.
88
+
89
+ ## Uninstall
90
+
91
+ ```bash
92
+ morse stop
93
+ npm uninstall -g @supanadit/morse-web
94
+ rm -rf ~/.morse # state and logs
95
+ ```
package/dist/cli.mjs ADDED
@@ -0,0 +1,539 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import { spawn } from "node:child_process";
5
+ import { createServer } from "node:net";
6
+ import {
7
+ existsSync,
8
+ mkdirSync,
9
+ openSync,
10
+ readFileSync,
11
+ rmSync,
12
+ writeFileSync
13
+ } from "node:fs";
14
+ import { homedir } from "node:os";
15
+ import { dirname, join, resolve } from "node:path";
16
+ import { fileURLToPath } from "node:url";
17
+ var CLI_DIR = dirname(fileURLToPath(import.meta.url));
18
+ var PACKAGE_ROOT = join(CLI_DIR, "..");
19
+ var SERVER_ENTRY = join(CLI_DIR, "server.mjs");
20
+ var UI_DIR = join(CLI_DIR, "ui");
21
+ var PACKAGE_JSON = join(PACKAGE_ROOT, "package.json");
22
+ var MORSE_HOME = process.env.MORSE_HOME?.trim() || join(homedir(), ".morse");
23
+ var STATE_FILE = join(MORSE_HOME, "server.json");
24
+ var LOG_DIR = join(MORSE_HOME, "logs");
25
+ var LOG_FILE = join(LOG_DIR, "server.log");
26
+ var DEFAULT_PORT = 4399;
27
+ var DEFAULT_HOST = "127.0.0.1";
28
+ var HEALTH_PATH = "/api/health";
29
+ var colorEnabled = process.stdout.isTTY === true && process.env.NO_COLOR === void 0;
30
+ var paint = (code) => (text) => colorEnabled ? `\x1B[${code}m${text}\x1B[0m` : text;
31
+ var dim = paint(2);
32
+ var bold = paint(1);
33
+ var cyan = paint(36);
34
+ var green = paint(32);
35
+ var yellow = paint(33);
36
+ var red = paint(31);
37
+ var COMMANDS = /* @__PURE__ */ new Set([
38
+ "start",
39
+ "serve",
40
+ "stop",
41
+ "restart",
42
+ "status",
43
+ "logs",
44
+ "help",
45
+ "version"
46
+ ]);
47
+ function parseArgs(argv) {
48
+ const options = {
49
+ command: "start",
50
+ lan: false,
51
+ foreground: false,
52
+ open: false,
53
+ follow: false,
54
+ force: false,
55
+ json: false,
56
+ help: false,
57
+ version: false
58
+ };
59
+ let commandSeen = false;
60
+ for (let index = 0; index < argv.length; index += 1) {
61
+ const arg = argv[index];
62
+ const [flag, inline] = splitFlag(arg);
63
+ const takeValue = () => {
64
+ if (inline !== void 0) return inline;
65
+ const next = argv[index + 1];
66
+ if (next === void 0 || next.startsWith("-")) {
67
+ throw new Error(`${flag} needs a value`);
68
+ }
69
+ index += 1;
70
+ return next;
71
+ };
72
+ switch (flag) {
73
+ case "start":
74
+ case "serve":
75
+ case "stop":
76
+ case "restart":
77
+ case "status":
78
+ case "logs":
79
+ case "help":
80
+ case "version":
81
+ options.command = flag;
82
+ commandSeen = true;
83
+ break;
84
+ case "-p":
85
+ case "--port":
86
+ options.port = parsePort(takeValue(), flag);
87
+ break;
88
+ case "--host":
89
+ case "--hostname":
90
+ options.host = takeValue();
91
+ break;
92
+ case "--workspace":
93
+ case "--cwd":
94
+ options.workspace = takeValue();
95
+ break;
96
+ case "--projects":
97
+ options.projects = takeValue();
98
+ break;
99
+ case "--lan":
100
+ options.lan = true;
101
+ break;
102
+ case "-f":
103
+ case "--foreground":
104
+ case "--no-daemon":
105
+ options.foreground = true;
106
+ break;
107
+ case "--open":
108
+ options.open = true;
109
+ break;
110
+ case "--follow":
111
+ options.follow = true;
112
+ break;
113
+ case "--force":
114
+ options.force = true;
115
+ break;
116
+ case "--json":
117
+ options.json = true;
118
+ break;
119
+ case "-h":
120
+ case "--help":
121
+ options.help = true;
122
+ break;
123
+ case "-v":
124
+ case "--version":
125
+ options.version = true;
126
+ break;
127
+ default:
128
+ if (!arg.startsWith("-") && !commandSeen) {
129
+ options.command = arg;
130
+ commandSeen = true;
131
+ break;
132
+ }
133
+ throw new Error(`Unknown option "${arg}". Run \`morse help\`.`);
134
+ }
135
+ }
136
+ if (!COMMANDS.has(options.command)) {
137
+ throw new Error(`Unknown command "${options.command}". Run \`morse help\`.`);
138
+ }
139
+ return options;
140
+ }
141
+ function splitFlag(arg) {
142
+ if (!arg.startsWith("--")) return [arg, void 0];
143
+ const equals = arg.indexOf("=");
144
+ return equals === -1 ? [arg, void 0] : [arg.slice(0, equals), arg.slice(equals + 1)];
145
+ }
146
+ function parsePort(value, flag) {
147
+ const port = Number.parseInt(value, 10);
148
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
149
+ throw new Error(`${flag} expects a port between 1 and 65535`);
150
+ }
151
+ return port;
152
+ }
153
+ function readState() {
154
+ try {
155
+ const parsed = JSON.parse(readFileSync(STATE_FILE, "utf8"));
156
+ if (typeof parsed.pid !== "number" || typeof parsed.port !== "number") return void 0;
157
+ return {
158
+ pid: parsed.pid,
159
+ port: parsed.port,
160
+ host: parsed.host ?? DEFAULT_HOST,
161
+ url: parsed.url ?? `http://${DEFAULT_HOST}:${parsed.port}/`,
162
+ workspace: parsed.workspace ?? process.cwd(),
163
+ startedAt: parsed.startedAt ?? (/* @__PURE__ */ new Date()).toISOString(),
164
+ logFile: parsed.logFile ?? LOG_FILE
165
+ };
166
+ } catch {
167
+ return void 0;
168
+ }
169
+ }
170
+ function writeState(state) {
171
+ mkdirSync(MORSE_HOME, { recursive: true });
172
+ writeFileSync(STATE_FILE, `${JSON.stringify(state, null, 2)}
173
+ `, "utf8");
174
+ }
175
+ function clearState() {
176
+ rmSync(STATE_FILE, { force: true });
177
+ }
178
+ function isAlive(pid) {
179
+ try {
180
+ process.kill(pid, 0);
181
+ return true;
182
+ } catch (error) {
183
+ return error.code === "EPERM";
184
+ }
185
+ }
186
+ function displayHost(host) {
187
+ return host === "0.0.0.0" || host === "::" ? DEFAULT_HOST : host;
188
+ }
189
+ function healthUrl(state) {
190
+ return `http://${displayHost(state.host)}:${state.port}${HEALTH_PATH}`;
191
+ }
192
+ async function probe(url) {
193
+ try {
194
+ const response = await fetch(url, { signal: AbortSignal.timeout(1500) });
195
+ return response.ok;
196
+ } catch {
197
+ return false;
198
+ }
199
+ }
200
+ async function waitForHealth(url, timeoutMs) {
201
+ const deadline = Date.now() + timeoutMs;
202
+ for (; ; ) {
203
+ if (await probe(url)) return true;
204
+ if (Date.now() >= deadline) return false;
205
+ await sleep(250);
206
+ }
207
+ }
208
+ async function isPortFree(port) {
209
+ return new Promise((resolvePort) => {
210
+ const server = createServer();
211
+ server.once("error", () => resolvePort(false));
212
+ server.once("listening", () => server.close(() => resolvePort(true)));
213
+ server.listen(port, DEFAULT_HOST);
214
+ });
215
+ }
216
+ async function findFreePort(start2) {
217
+ for (let port = start2; port < start2 + 100; port += 1) {
218
+ if (await isPortFree(port)) return port;
219
+ }
220
+ throw new Error(`No free port found in ${start2}..${start2 + 99}`);
221
+ }
222
+ function sleep(ms) {
223
+ return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
224
+ }
225
+ async function start(options) {
226
+ const existing = readState();
227
+ if (existing && isAlive(existing.pid)) {
228
+ if (!options.force) {
229
+ console.log(`${yellow("Morse is already running")} (PID ${existing.pid})`);
230
+ console.log(` visit: ${cyan(existing.url)}`);
231
+ console.log(` stop: ${dim(`morse stop`)}`);
232
+ return 0;
233
+ }
234
+ await stop({ quiet: true });
235
+ }
236
+ clearState();
237
+ const workspace = options.workspace ? resolve(options.workspace) : process.cwd();
238
+ if (!existsSync(workspace)) {
239
+ throw new Error(`Workspace does not exist: ${workspace}`);
240
+ }
241
+ const envPort = process.env.MORSE_PORT ? Number.parseInt(process.env.MORSE_PORT, 10) : NaN;
242
+ let port = options.port ?? (Number.isInteger(envPort) ? envPort : void 0) ?? DEFAULT_PORT;
243
+ if (options.port === void 0 && !Number.isInteger(envPort)) {
244
+ port = await findFreePort(DEFAULT_PORT);
245
+ } else if (!await isPortFree(port)) {
246
+ throw new Error(`Port ${port} is already in use. Pass --port to pick another one.`);
247
+ }
248
+ const host = options.lan ? "0.0.0.0" : options.host ?? process.env.MORSE_HOST ?? DEFAULT_HOST;
249
+ const env = buildEnv(options, port, host, workspace);
250
+ if (options.foreground) {
251
+ return startForeground(env, port, host, workspace);
252
+ }
253
+ return startDaemon(env, port, host, workspace, options);
254
+ }
255
+ function buildEnv(options, port, host, workspace) {
256
+ const env = {
257
+ ...process.env,
258
+ MORSE_HOST: host,
259
+ MORSE_PORT: String(port),
260
+ MORSE_UI_DIR: UI_DIR,
261
+ MORSE_WORKSPACE: workspace
262
+ };
263
+ if (options.projects) env.MORSE_PROJECTS = options.projects;
264
+ return env;
265
+ }
266
+ async function startDaemon(env, port, host, workspace, options) {
267
+ mkdirSync(LOG_DIR, { recursive: true });
268
+ writeFileSync(
269
+ LOG_FILE,
270
+ `
271
+ === morse start ${(/* @__PURE__ */ new Date()).toISOString()} (port ${port}) ===
272
+ `,
273
+ { flag: "a" }
274
+ );
275
+ const logFd = openSync(LOG_FILE, "a");
276
+ const child = spawn(process.execPath, [SERVER_ENTRY], {
277
+ cwd: workspace,
278
+ env,
279
+ detached: true,
280
+ // The daemon owns no terminal: closing the shell must not send it SIGHUP.
281
+ stdio: ["ignore", logFd, logFd]
282
+ });
283
+ child.unref();
284
+ const state = {
285
+ pid: child.pid ?? -1,
286
+ port,
287
+ host,
288
+ url: `http://${displayHost(host)}:${port}/`,
289
+ workspace,
290
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
291
+ logFile: LOG_FILE
292
+ };
293
+ writeState(state);
294
+ const healthy = await waitForHealth(healthUrl(state), 25e3);
295
+ if (!healthy) {
296
+ console.error(red("Morse failed to start."));
297
+ if (!isAlive(state.pid)) {
298
+ clearState();
299
+ console.error(dim(`Last lines of ${LOG_FILE}:`));
300
+ console.error(tailLog(40));
301
+ return 1;
302
+ }
303
+ console.error(dim("The process is alive but did not answer the health probe."));
304
+ console.error(dim(`Logs: morse logs`));
305
+ return 1;
306
+ }
307
+ console.log();
308
+ console.log(` ${green("\u25B2")} ${bold("Morse started")}`);
309
+ console.log(` \u2502`);
310
+ console.log(` \u25C6 port ${port} (PID: ${state.pid})`);
311
+ console.log(` \u2502`);
312
+ console.log(` \u25CF visit: ${cyan(state.url)}`);
313
+ console.log(` \u25CF logs: ${dim("morse logs")}`);
314
+ console.log(` \u2502`);
315
+ console.log(` \u2514 daemon running \u2014 the terminal can be closed`);
316
+ console.log();
317
+ if (options.open) openBrowser(state.url);
318
+ return 0;
319
+ }
320
+ async function startForeground(env, port, host, workspace) {
321
+ const child = spawn(process.execPath, [SERVER_ENTRY], { cwd: workspace, env, stdio: "inherit" });
322
+ writeState({
323
+ pid: child.pid ?? -1,
324
+ port,
325
+ host,
326
+ url: `http://${displayHost(host)}:${port}/`,
327
+ workspace,
328
+ startedAt: (/* @__PURE__ */ new Date()).toISOString(),
329
+ logFile: LOG_FILE
330
+ });
331
+ const forward = (signal) => {
332
+ if (child.pid !== void 0) child.kill(signal);
333
+ };
334
+ process.on("SIGINT", () => forward("SIGINT"));
335
+ process.on("SIGTERM", () => forward("SIGTERM"));
336
+ const code = await new Promise((resolveExit) => {
337
+ child.on("exit", (exitCode) => resolveExit(exitCode ?? 0));
338
+ });
339
+ clearState();
340
+ return code;
341
+ }
342
+ async function stop(options = {}) {
343
+ const state = readState();
344
+ if (!state || !isAlive(state.pid)) {
345
+ clearState();
346
+ if (!options.quiet) console.log(dim("Morse is not running."));
347
+ return 0;
348
+ }
349
+ const { pid } = state;
350
+ process.kill(pid, "SIGTERM");
351
+ if (!await waitForExit(pid, 5e3)) {
352
+ process.kill(pid, "SIGKILL");
353
+ await waitForExit(pid, 2e3);
354
+ }
355
+ clearState();
356
+ if (!options.quiet) {
357
+ console.log(`${green("\u25CF")} Morse stopped (PID ${pid}).`);
358
+ }
359
+ return 0;
360
+ }
361
+ async function waitForExit(pid, timeoutMs) {
362
+ const deadline = Date.now() + timeoutMs;
363
+ while (Date.now() < deadline) {
364
+ if (!isAlive(pid)) return true;
365
+ await sleep(100);
366
+ }
367
+ return !isAlive(pid);
368
+ }
369
+ function status(options) {
370
+ const state = readState();
371
+ if (!state || !isAlive(state.pid)) {
372
+ clearState();
373
+ if (options.json) {
374
+ console.log(JSON.stringify({ running: false }));
375
+ } else {
376
+ console.log(dim("Morse is not running."));
377
+ }
378
+ return 1;
379
+ }
380
+ const uptimeMs = Date.now() - new Date(state.startedAt).getTime();
381
+ if (options.json) {
382
+ console.log(JSON.stringify({ running: true, ...state, uptimeMs }));
383
+ return 0;
384
+ }
385
+ console.log(`${green("\u25CF")} Morse is running (PID ${state.pid})`);
386
+ console.log(` visit: ${cyan(state.url)}`);
387
+ console.log(` workspace: ${state.workspace}`);
388
+ console.log(` uptime: ${formatUptime(uptimeMs)}`);
389
+ console.log(` logs: ${dim(state.logFile)}`);
390
+ return 0;
391
+ }
392
+ function formatUptime(uptimeMs) {
393
+ const seconds = Math.max(0, Math.floor(uptimeMs / 1e3));
394
+ const hours = Math.floor(seconds / 3600);
395
+ const minutes = Math.floor(seconds % 3600 / 60);
396
+ if (hours > 0) return `${hours}h ${minutes}m`;
397
+ if (minutes > 0) return `${minutes}m ${seconds % 60}s`;
398
+ return `${seconds}s`;
399
+ }
400
+ function tailLog(linesToShow) {
401
+ if (!existsSync(LOG_FILE)) return "(no log yet)";
402
+ const lines = readFileSync(LOG_FILE, "utf8").split("\n");
403
+ return lines.slice(-linesToShow).join("\n");
404
+ }
405
+ async function logs(options) {
406
+ if (!existsSync(LOG_FILE)) {
407
+ console.log(dim(`No log file yet (${LOG_FILE}).`));
408
+ return 0;
409
+ }
410
+ process.stdout.write(tailLog(200));
411
+ if (!options.follow) return 0;
412
+ let seen = readFileSync(LOG_FILE, "utf8").length;
413
+ await new Promise((resolveFollow) => {
414
+ const timer = setInterval(() => {
415
+ try {
416
+ const current = readFileSync(LOG_FILE, "utf8");
417
+ if (current.length > seen) {
418
+ process.stdout.write(current.slice(seen));
419
+ seen = current.length;
420
+ } else if (current.length < seen) {
421
+ seen = current.length;
422
+ }
423
+ } catch {
424
+ }
425
+ }, 400);
426
+ const stopFollowing = () => {
427
+ clearInterval(timer);
428
+ resolveFollow();
429
+ };
430
+ process.on("SIGINT", stopFollowing);
431
+ process.on("SIGTERM", stopFollowing);
432
+ });
433
+ return 0;
434
+ }
435
+ function openBrowser(url) {
436
+ const command = process.platform === "darwin" ? "open" : process.platform === "win32" ? "cmd" : "xdg-open";
437
+ const args = process.platform === "win32" ? ["/c", "start", "", url] : [url];
438
+ try {
439
+ const child = spawn(command, args, { stdio: "ignore", detached: true });
440
+ child.unref();
441
+ } catch {
442
+ }
443
+ }
444
+ function readVersion() {
445
+ try {
446
+ return JSON.parse(readFileSync(PACKAGE_JSON, "utf8")).version ?? "0.0.0";
447
+ } catch {
448
+ return "0.0.0";
449
+ }
450
+ }
451
+ function printHelp() {
452
+ const name = `@supanadit/morse-web`;
453
+ console.log(`
454
+ ${bold("Morse")} \u2014 the Comfortable Pi Interface, in your browser (${name})
455
+
456
+ ${bold("USAGE")}
457
+ morse [COMMAND] [OPTIONS]
458
+
459
+ ${bold("COMMANDS")}
460
+ start Start the web server in the background (default)
461
+ stop Stop the running server
462
+ restart Stop, then start the server
463
+ status Show whether the server is running
464
+ logs Print the server log (--follow to tail)
465
+ help Show this help
466
+ version Print the version
467
+
468
+ ${bold("OPTIONS")}
469
+ -p, --port <n> Web server port (default: ${DEFAULT_PORT}, next free if taken)
470
+ --host <addr> Bind address (default: ${DEFAULT_HOST})
471
+ --lan Bind 0.0.0.0 so the LAN can reach it
472
+ -f, --foreground Run in the foreground (systemd Type=simple)
473
+ --workspace <dir> Directory the agent works in (default: the current one)
474
+ --projects <list> Comma/colon separated list of roots the agent may open
475
+ --open Open the browser once the server is healthy
476
+ --force Replace a server this CLI already started
477
+ --json Machine-readable output (status)
478
+ --follow Follow the log file (logs)
479
+ -h, --help Show this help
480
+ -v, --version Print the version
481
+
482
+ ${bold("ENVIRONMENT")}
483
+ MORSE_HOME Data directory (default: ~/.morse)
484
+ MORSE_PORT Default port
485
+ MORSE_HOST Default bind address
486
+ MORSE_WORKSPACE Workspace the agent runs in
487
+ MORSE_PROJECTS Roots the agent may open
488
+ MORSE_HOT_SESSIONS How many pi processes stay alive (default: 4)
489
+ MORSE_PI_PATH Path to the pi executable (default: pi on PATH)
490
+ MORSE_PI_ENTRY Run a pi RPC entry with node instead of the binary
491
+
492
+ ${bold("EXAMPLES")}
493
+ morse # start the daemon on ${DEFAULT_PORT}
494
+ morse start --port 8080 # a specific port
495
+ morse --lan --port 3002 # reachable on the LAN
496
+ morse start --open # start and open the browser
497
+ morse logs --follow # tail the server log
498
+ morse stop # stop the daemon
499
+ `);
500
+ }
501
+ async function run(argv) {
502
+ const options = parseArgs(argv);
503
+ if (options.version || options.command === "version") {
504
+ console.log(readVersion());
505
+ return 0;
506
+ }
507
+ if (options.help || options.command === "help") {
508
+ printHelp();
509
+ return 0;
510
+ }
511
+ switch (options.command) {
512
+ case "start":
513
+ case "serve":
514
+ return start(options);
515
+ case "stop":
516
+ return stop();
517
+ case "restart":
518
+ await stop({ quiet: true });
519
+ return start(options);
520
+ case "status":
521
+ return status(options);
522
+ case "logs":
523
+ return logs(options);
524
+ default:
525
+ printHelp();
526
+ return 0;
527
+ }
528
+ }
529
+ run(process.argv.slice(2)).then((code) => {
530
+ process.exitCode = code;
531
+ }).catch((error) => {
532
+ const message = error instanceof Error ? error.message : String(error);
533
+ console.error(`${red("morse:")} ${message}`);
534
+ process.exitCode = 1;
535
+ });
536
+ export {
537
+ run
538
+ };
539
+ //# sourceMappingURL=cli.mjs.map