@moneysiren/cli 0.1.0-alpha.6 → 0.1.0-alpha.7

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/README.md CHANGED
@@ -67,7 +67,7 @@ Install the generated tarball into a temporary project:
67
67
  mkdir -p /tmp/moneysiren-alpha-review
68
68
  cd /tmp/moneysiren-alpha-review
69
69
  npm init -y
70
- npm install /path/to/moneysiren-cli-0.1.0-alpha.6.tgz
70
+ npm install /path/to/moneysiren-cli-0.1.0-alpha.7.tgz
71
71
  npm exec moneysiren
72
72
  npm exec moneysiren -- --version
73
73
  npm exec moneysiren -- /version
@@ -83,7 +83,7 @@ PowerShell equivalent for the temporary project:
83
83
  New-Item -ItemType Directory -Force -Path $env:TEMP\moneysiren-alpha-review
84
84
  Set-Location $env:TEMP\moneysiren-alpha-review
85
85
  npm init -y
86
- npm install C:\path\to\moneysiren-cli-0.1.0-alpha.6.tgz
86
+ npm install C:\path\to\moneysiren-cli-0.1.0-alpha.7.tgz
87
87
  npm exec moneysiren
88
88
  npm exec moneysiren -- --version
89
89
  npm exec moneysiren -- modes
@@ -5,7 +5,7 @@ import { runInstallCommand } from "./commands/install.js";
5
5
  import { runModesCommand } from "./commands/modes.js";
6
6
  import { runNotifyCommand } from "./commands/notify.js";
7
7
  import { runReportCommand } from "./commands/report.js";
8
- import { runDesktopCommand, runHudCommand, runOpenCommand, runServeCommand, runStartCommand } from "./commands/runtime.js";
8
+ import { runDesktopCommand, runHudCommand, runOpenCommand, runRestartCommand, runServeCommand, runStartCommand, runStatusCommand, runStopCommand, } from "./commands/runtime.js";
9
9
  import { runSummaryCommand } from "./commands/summary.js";
10
10
  import { runSyncCommand } from "./commands/sync.js";
11
11
  import { runThemeCommand } from "./commands/theme.js";
@@ -134,6 +134,15 @@ async function dispatchCommand(args, context) {
134
134
  if (command === "start") {
135
135
  return runStartCommand(rest, context);
136
136
  }
137
+ if (command === "status") {
138
+ return runStatusCommand(rest, context);
139
+ }
140
+ if (command === "stop") {
141
+ return runStopCommand(rest, context);
142
+ }
143
+ if (command === "restart") {
144
+ return runRestartCommand(rest, context);
145
+ }
137
146
  if (command === "hud") {
138
147
  return runHudCommand(rest, context);
139
148
  }
@@ -29,6 +29,7 @@ export async function runModesCommand(args, context) {
29
29
  context.stdout(` Status: ${surfaceStatus("web", selectedSurfaces)} GitHub Release web runtime archive is installed by the CLI`);
30
30
  context.stdout(" Install: msiren install --web");
31
31
  context.stdout(" Try: msiren start");
32
+ context.stdout(" Stop: msiren stop --web");
32
33
  context.stdout(" Try: msiren dashboard check");
33
34
  context.stdout(" Note: msiren start runs the installed GitHub Release web runtime.");
34
35
  context.stdout("");
@@ -36,10 +37,12 @@ export async function runModesCommand(args, context) {
36
37
  context.stdout(` Status: ${surfaceStatus("hud", selectedSurfaces)} Windows/macOS target is the thin Tauri tray shell from GitHub Releases`);
37
38
  context.stdout(" Install: msiren install --hud");
38
39
  context.stdout(" Try: msiren hud");
39
- context.stdout(" Try: msiren desktop status");
40
+ context.stdout(" Try: msiren status");
41
+ context.stdout(" Stop: msiren stop --hud");
40
42
  context.stdout(" Try: msiren notify once --dry-run");
41
43
  context.stdout("");
42
44
  context.stdout("Install recommended set: msiren install --all");
45
+ context.stdout("Stop managed runtimes: msiren stop");
43
46
  context.stdout("Change selection only: msiren install --profile-only");
44
47
  return 0;
45
48
  }
@@ -1,6 +1,9 @@
1
1
  import type { CliExecutionContext } from "../cli.js";
2
2
  export declare function runServeCommand(args: readonly string[], context: CliExecutionContext): Promise<number>;
3
+ export declare function runStatusCommand(args: readonly string[], context: CliExecutionContext): Promise<number>;
3
4
  export declare function runStartCommand(args: readonly string[], context: CliExecutionContext): Promise<number>;
5
+ export declare function runStopCommand(args: readonly string[], context: CliExecutionContext): Promise<number>;
6
+ export declare function runRestartCommand(args: readonly string[], context: CliExecutionContext): Promise<number>;
4
7
  export declare function runHudCommand(args: readonly string[], context: CliExecutionContext): Promise<number>;
5
8
  export declare function runOpenCommand(args: readonly string[], context: CliExecutionContext): Promise<number>;
6
9
  export declare function runDesktopCommand(args: readonly string[], context: CliExecutionContext): Promise<number>;
@@ -1,8 +1,12 @@
1
1
  import { createFallbackDesktopRuntimeAdapter, } from "../desktop-runtime.js";
2
2
  import { createFallbackLocalRuntimeAdapter, } from "../runtime-adapter.js";
3
+ import { removeRuntimeLock } from "../../../../packages/runtime/src/index.js";
3
4
  const SERVE_USAGE = "Usage: moneysiren serve [--port <port>]";
4
5
  const START_USAGE = "Usage: msiren start [--port <port>] [--open|--no-open] [--hud]";
5
6
  const HUD_USAGE = "Usage: msiren hud [--port <port>]";
7
+ const STATUS_USAGE = "Usage: msiren status";
8
+ const STOP_USAGE = "Usage: msiren stop [--web|--hud|--api|--all]";
9
+ const RESTART_USAGE = "Usage: msiren restart [--port <port>] [--open|--no-open] [--hud]";
6
10
  const OPEN_USAGE = "Usage: moneysiren open";
7
11
  const DESKTOP_USAGE = "Usage: moneysiren desktop status";
8
12
  export async function runServeCommand(args, context) {
@@ -21,6 +25,31 @@ export async function runServeCommand(args, context) {
21
25
  });
22
26
  return writeStartRuntimeResult(context, result, "MoneySiren local runtime");
23
27
  }
28
+ export async function runStatusCommand(args, context) {
29
+ if (args.includes("--help") || args.includes("-h")) {
30
+ context.stdout(STATUS_USAGE);
31
+ return 0;
32
+ }
33
+ if (args.length > 0) {
34
+ context.stderr(STATUS_USAGE);
35
+ return 1;
36
+ }
37
+ const desktop = await desktopRuntimeAdapter(context).status();
38
+ const api = await runtimeAdapter(context).findRuntime();
39
+ context.stdout("MoneySiren status");
40
+ writeDesktopProcessStatus(context, "Web runtime", desktop.web);
41
+ writeDesktopProcessStatus(context, "HUD", desktop.hud);
42
+ context.stdout(`Desktop state: ${desktop.statePath}`);
43
+ if (api === null) {
44
+ context.stdout("Local API runtime: not running");
45
+ return 0;
46
+ }
47
+ const healthy = await runtimeAdapter(context).assertRuntimeHealthy(api);
48
+ context.stdout(`Local API runtime: ${healthy ? "healthy" : "unhealthy"}`);
49
+ context.stdout(` PID: ${api.pid}`);
50
+ context.stdout(` URL: ${api.baseUrl}`);
51
+ return healthy ? 0 : 1;
52
+ }
24
53
  export async function runStartCommand(args, context) {
25
54
  if (args.includes("--help") || args.includes("-h")) {
26
55
  context.stdout(START_USAGE);
@@ -53,6 +82,54 @@ export async function runStartCommand(args, context) {
53
82
  });
54
83
  return writeDesktopShellResult(context, hud, "MoneySiren HUD");
55
84
  }
85
+ export async function runStopCommand(args, context) {
86
+ if (args.includes("--help") || args.includes("-h")) {
87
+ context.stdout(STOP_USAGE);
88
+ return 0;
89
+ }
90
+ const selection = parseStopArgs(args);
91
+ if (selection === undefined) {
92
+ context.stderr(STOP_USAGE);
93
+ return 1;
94
+ }
95
+ context.stdout("MoneySiren stop");
96
+ const desktopResults = await desktopRuntimeAdapter(context).stop({
97
+ hud: selection.hud,
98
+ web: selection.web,
99
+ });
100
+ for (const result of desktopResults) {
101
+ writeStopDesktopRuntimeResult(context, result);
102
+ }
103
+ let exitCode = desktopResults.some((result) => result.status === "failed") ? 1 : 0;
104
+ if (selection.api) {
105
+ const apiResult = await stopLocalApiRuntime(context);
106
+ context.stdout(`Local API runtime: ${apiResult.status}`);
107
+ context.stdout(` ${apiResult.detail}`);
108
+ if (apiResult.pid !== undefined) {
109
+ context.stdout(` PID: ${apiResult.pid}`);
110
+ }
111
+ if (apiResult.status === "failed") {
112
+ exitCode = 1;
113
+ }
114
+ }
115
+ return exitCode;
116
+ }
117
+ export async function runRestartCommand(args, context) {
118
+ if (args.includes("--help") || args.includes("-h")) {
119
+ context.stdout(RESTART_USAGE);
120
+ return 0;
121
+ }
122
+ const parsed = parseStartArgs(args);
123
+ if (parsed === undefined) {
124
+ context.stderr(RESTART_USAGE);
125
+ return 1;
126
+ }
127
+ const stopExitCode = await runStopCommand(["--web", "--hud"], context);
128
+ if (stopExitCode !== 0) {
129
+ return stopExitCode;
130
+ }
131
+ return runStartCommand(args, context);
132
+ }
56
133
  export async function runHudCommand(args, context) {
57
134
  if (args.includes("--help") || args.includes("-h")) {
58
135
  context.stdout(HUD_USAGE);
@@ -77,6 +154,54 @@ export async function runHudCommand(args, context) {
77
154
  });
78
155
  return writeDesktopShellResult(context, hud, "MoneySiren HUD");
79
156
  }
157
+ async function stopLocalApiRuntime(context) {
158
+ const adapter = runtimeAdapter(context);
159
+ const runtime = await adapter.findRuntime();
160
+ if (runtime === null) {
161
+ return {
162
+ status: "not-running",
163
+ detail: "No managed local API runtime lock was found.",
164
+ };
165
+ }
166
+ try {
167
+ process.kill(runtime.pid, "SIGTERM");
168
+ await waitForProcessExit(runtime.pid, 3_000);
169
+ if (isProcessAlive(runtime.pid)) {
170
+ return {
171
+ status: "failed",
172
+ detail: "Local API runtime did not exit after SIGTERM.",
173
+ pid: runtime.pid,
174
+ };
175
+ }
176
+ await removeRuntimeLock({
177
+ cwd: context.cwd,
178
+ env: context.env,
179
+ });
180
+ return {
181
+ status: "stopped",
182
+ detail: "Managed local API runtime stopped.",
183
+ pid: runtime.pid,
184
+ };
185
+ }
186
+ catch (error) {
187
+ if (isNodeError(error) && error.code === "ESRCH") {
188
+ await removeRuntimeLock({
189
+ cwd: context.cwd,
190
+ env: context.env,
191
+ });
192
+ return {
193
+ status: "stale",
194
+ detail: "Removed stale local API runtime lock.",
195
+ pid: runtime.pid,
196
+ };
197
+ }
198
+ return {
199
+ status: "failed",
200
+ detail: error instanceof Error ? error.message : String(error),
201
+ pid: runtime.pid,
202
+ };
203
+ }
204
+ }
80
205
  export async function runOpenCommand(args, context) {
81
206
  if (args.includes("--help") || args.includes("-h")) {
82
207
  context.stdout(OPEN_USAGE);
@@ -204,6 +329,21 @@ function writeDesktopShellResult(context, result, heading) {
204
329
  }
205
330
  return 1;
206
331
  }
332
+ function writeDesktopProcessStatus(context, label, status) {
333
+ context.stdout(`${label}: ${status.status}`);
334
+ if (status.pid !== undefined) {
335
+ context.stdout(` PID: ${status.pid}`);
336
+ }
337
+ context.stdout(` ${status.detail}`);
338
+ }
339
+ function writeStopDesktopRuntimeResult(context, result) {
340
+ const label = result.target === "web" ? "Web runtime" : "HUD";
341
+ context.stdout(`${label}: ${result.status}`);
342
+ context.stdout(` ${result.detail}`);
343
+ if (result.pid !== undefined) {
344
+ context.stdout(` PID: ${result.pid}`);
345
+ }
346
+ }
207
347
  function parseServeArgs(args) {
208
348
  let port;
209
349
  for (let index = 0; index < args.length; index += 1) {
@@ -228,6 +368,41 @@ function parseServeArgs(args) {
228
368
  }
229
369
  return Number.isSafeInteger(port) && port > 0 && port <= 65_535 ? { port } : undefined;
230
370
  }
371
+ function parseStopArgs(args) {
372
+ let web = false;
373
+ let hud = false;
374
+ let api = false;
375
+ let all = false;
376
+ for (const arg of args) {
377
+ if (arg === "--web") {
378
+ web = true;
379
+ }
380
+ else if (arg === "--hud") {
381
+ hud = true;
382
+ }
383
+ else if (arg === "--api") {
384
+ api = true;
385
+ }
386
+ else if (arg === "--all") {
387
+ all = true;
388
+ }
389
+ else {
390
+ return undefined;
391
+ }
392
+ }
393
+ if (all || (!web && !hud && !api)) {
394
+ return {
395
+ api: true,
396
+ hud: true,
397
+ web: true,
398
+ };
399
+ }
400
+ return {
401
+ api,
402
+ hud,
403
+ web,
404
+ };
405
+ }
231
406
  function parseStartArgs(args) {
232
407
  let launchHud = false;
233
408
  let openBrowser = true;
@@ -297,4 +472,28 @@ function parseHudArgs(args) {
297
472
  function parsePort(value) {
298
473
  return Number.parseInt(value, 10);
299
474
  }
475
+ function isProcessAlive(pid) {
476
+ if (pid <= 0) {
477
+ return false;
478
+ }
479
+ try {
480
+ process.kill(pid, 0);
481
+ return true;
482
+ }
483
+ catch (error) {
484
+ return isNodeError(error) && error.code === "EPERM";
485
+ }
486
+ }
487
+ async function waitForProcessExit(pid, timeoutMs) {
488
+ const deadline = Date.now() + timeoutMs;
489
+ while (Date.now() < deadline) {
490
+ if (!isProcessAlive(pid)) {
491
+ return;
492
+ }
493
+ await new Promise((resolveTimeout) => setTimeout(resolveTimeout, 100));
494
+ }
495
+ }
496
+ function isNodeError(value) {
497
+ return value instanceof Error && "code" in value;
498
+ }
300
499
  //# sourceMappingURL=runtime.js.map
@@ -6,6 +6,27 @@ export interface StartWebRuntimeOptions {
6
6
  export interface StartHudOptions {
7
7
  port?: number;
8
8
  }
9
+ export interface DesktopRuntimeStatus {
10
+ statePath: string;
11
+ web: DesktopProcessStatus;
12
+ hud: DesktopProcessStatus;
13
+ }
14
+ export interface DesktopProcessStatus {
15
+ target: "web" | "hud";
16
+ status: "running" | "not-running" | "not-managed" | "stale";
17
+ pid?: number;
18
+ detail: string;
19
+ }
20
+ export interface StopDesktopRuntimeOptions {
21
+ hud: boolean;
22
+ web: boolean;
23
+ }
24
+ export interface StopDesktopRuntimeResult {
25
+ target: "web" | "hud";
26
+ status: "stopped" | "not-running" | "not-managed" | "stale" | "failed";
27
+ pid?: number;
28
+ detail: string;
29
+ }
9
30
  export type DesktopRuntimeResult = {
10
31
  status: "running" | "started";
11
32
  dashboardUrl: string;
@@ -26,6 +47,8 @@ export interface DesktopRuntimeUnavailableResult {
26
47
  export interface CliDesktopRuntimeAdapter {
27
48
  startWebRuntime(options: StartWebRuntimeOptions): Promise<DesktopRuntimeResult>;
28
49
  startHud(options: StartHudOptions): Promise<DesktopShellResult>;
50
+ status(): Promise<DesktopRuntimeStatus>;
51
+ stop(options: StopDesktopRuntimeOptions): Promise<readonly StopDesktopRuntimeResult[]>;
29
52
  }
30
53
  export declare function createFallbackDesktopRuntimeAdapter(context: CliExecutionContext): CliDesktopRuntimeAdapter;
31
54
  //# sourceMappingURL=desktop-runtime.d.ts.map
@@ -1,12 +1,15 @@
1
1
  import { execFile, spawn } from "node:child_process";
2
2
  import { constants } from "node:fs";
3
- import { access, mkdir, readFile, readdir, stat } from "node:fs/promises";
4
- import { basename, dirname, extname, join, resolve } from "node:path";
3
+ import { access, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
4
+ import { homedir } from "node:os";
5
+ import { basename, dirname, extname, join, posix, resolve, win32 } from "node:path";
5
6
  import { promisify } from "node:util";
6
7
  import { resolveReleaseInstallDir } from "./release-installer.js";
7
8
  const execFileAsync = promisify(execFile);
8
9
  const DEFAULT_WEB_PORT = 3000;
9
10
  const DEFAULT_HEALTH_TIMEOUT_MS = 30_000;
11
+ const DESKTOP_STATE_ENV_KEY = "MONEYSIREN_DESKTOP_RUNTIME_STATE_PATH";
12
+ const STOP_TIMEOUT_MS = 3_000;
10
13
  export function createFallbackDesktopRuntimeAdapter(context) {
11
14
  return {
12
15
  async startWebRuntime(options) {
@@ -14,9 +17,11 @@ export function createFallbackDesktopRuntimeAdapter(context) {
14
17
  const dashboardUrl = `http://127.0.0.1:${port}/ko/dashboard/overview`;
15
18
  const healthUrl = `http://127.0.0.1:${port}/api/local/health`;
16
19
  if (await isWebRuntimeHealthy(healthUrl, context.fetch)) {
20
+ const status = await readDesktopRuntimeStatus(context);
17
21
  return {
18
22
  status: "running",
19
23
  dashboardUrl,
24
+ ...(status.web.status === "running" && status.web.pid !== undefined ? { pid: status.web.pid } : {}),
20
25
  notes: ["Existing local dashboard runtime is healthy."],
21
26
  };
22
27
  }
@@ -47,6 +52,17 @@ export function createFallbackDesktopRuntimeAdapter(context) {
47
52
  ],
48
53
  };
49
54
  }
55
+ if (child.pid !== undefined) {
56
+ await updateDesktopRuntimeState(context, (state) => ({
57
+ ...state,
58
+ web: {
59
+ pid: child.pid,
60
+ port,
61
+ dashboardUrl,
62
+ startedAt: new Date().toISOString(),
63
+ },
64
+ }));
65
+ }
50
66
  return {
51
67
  status: "started",
52
68
  dashboardUrl,
@@ -55,6 +71,15 @@ export function createFallbackDesktopRuntimeAdapter(context) {
55
71
  };
56
72
  },
57
73
  async startHud(options) {
74
+ const status = await readDesktopRuntimeStatus(context);
75
+ if (status.hud.status === "running" && status.hud.pid !== undefined) {
76
+ return {
77
+ status: "opened",
78
+ executablePath: status.hud.detail,
79
+ pid: status.hud.pid,
80
+ notes: ["Existing desktop HUD shell is already running."],
81
+ };
82
+ }
58
83
  const executable = await resolveDesktopExecutable(context);
59
84
  if (isUnavailable(executable)) {
60
85
  return executable;
@@ -72,6 +97,16 @@ export function createFallbackDesktopRuntimeAdapter(context) {
72
97
  windowsHide: true,
73
98
  });
74
99
  child.unref();
100
+ if (child.pid !== undefined) {
101
+ await updateDesktopRuntimeState(context, (state) => ({
102
+ ...state,
103
+ hud: {
104
+ executablePath: executable.executablePath,
105
+ pid: child.pid,
106
+ startedAt: new Date().toISOString(),
107
+ },
108
+ }));
109
+ }
75
110
  return {
76
111
  status: "started",
77
112
  executablePath: executable.executablePath,
@@ -79,8 +114,156 @@ export function createFallbackDesktopRuntimeAdapter(context) {
79
114
  notes: ["Desktop HUD shell launched with MONEYSIREN_DESKTOP_MODE=hud."],
80
115
  };
81
116
  },
117
+ async status() {
118
+ return readDesktopRuntimeStatus(context);
119
+ },
120
+ async stop(options) {
121
+ return stopDesktopRuntime(context, options);
122
+ },
123
+ };
124
+ }
125
+ async function readDesktopRuntimeStatus(context) {
126
+ const statePath = resolveDesktopRuntimeStatePath(context);
127
+ const state = await readDesktopRuntimeState(context);
128
+ const port = configuredPort(context.env);
129
+ const webHealthUrl = `http://127.0.0.1:${port}/api/local/health`;
130
+ const web = await processStatus({
131
+ context,
132
+ detail: state?.web?.dashboardUrl ?? `http://127.0.0.1:${port}`,
133
+ healthUrl: webHealthUrl,
134
+ process: state?.web,
135
+ target: "web",
136
+ });
137
+ const hud = await processStatus({
138
+ context,
139
+ detail: state?.hud?.executablePath ?? "No managed HUD process recorded.",
140
+ process: state?.hud,
141
+ target: "hud",
142
+ });
143
+ return {
144
+ statePath,
145
+ web,
146
+ hud,
147
+ };
148
+ }
149
+ async function stopDesktopRuntime(context, options) {
150
+ const state = await readDesktopRuntimeState(context);
151
+ const results = [];
152
+ let nextState = state ?? emptyDesktopRuntimeState();
153
+ if (options.web) {
154
+ const result = state?.web === undefined && await isWebRuntimeHealthy(`http://127.0.0.1:${configuredPort(context.env)}/api/local/health`, context.fetch)
155
+ ? {
156
+ target: "web",
157
+ status: "not-managed",
158
+ detail: "A local dashboard runtime is reachable, but no MoneySiren CLI PID is recorded. It was not stopped.",
159
+ }
160
+ : await stopManagedProcess("web", state?.web);
161
+ results.push(result);
162
+ if (result.status !== "failed") {
163
+ const { web: _web, ...rest } = nextState;
164
+ nextState = rest;
165
+ }
166
+ }
167
+ if (options.hud) {
168
+ const result = await stopManagedProcess("hud", state?.hud);
169
+ results.push(result);
170
+ if (result.status !== "failed") {
171
+ const { hud: _hud, ...rest } = nextState;
172
+ nextState = rest;
173
+ }
174
+ }
175
+ await writeOrRemoveDesktopRuntimeState(context, nextState);
176
+ return results;
177
+ }
178
+ async function processStatus(input) {
179
+ if (input.process === undefined) {
180
+ if (input.target === "web" && input.healthUrl !== undefined && await isWebRuntimeHealthy(input.healthUrl, input.context.fetch)) {
181
+ return {
182
+ target: input.target,
183
+ status: "not-managed",
184
+ detail: `${input.detail} is reachable, but no MoneySiren CLI PID is recorded.`,
185
+ };
186
+ }
187
+ return {
188
+ target: input.target,
189
+ status: "not-running",
190
+ detail: input.detail,
191
+ };
192
+ }
193
+ if (!isProcessAlive(input.process.pid)) {
194
+ return {
195
+ target: input.target,
196
+ status: "stale",
197
+ pid: input.process.pid,
198
+ detail: input.detail,
199
+ };
200
+ }
201
+ return {
202
+ target: input.target,
203
+ status: "running",
204
+ pid: input.process.pid,
205
+ detail: input.detail,
82
206
  };
83
207
  }
208
+ async function stopManagedProcess(target, processRecord) {
209
+ if (processRecord === undefined) {
210
+ return {
211
+ target,
212
+ status: "not-running",
213
+ detail: `No managed ${target} process is recorded.`,
214
+ };
215
+ }
216
+ if (!isProcessAlive(processRecord.pid)) {
217
+ return {
218
+ target,
219
+ status: "stale",
220
+ pid: processRecord.pid,
221
+ detail: `Removed stale ${target} process record.`,
222
+ };
223
+ }
224
+ if (processRecord.pid === process.pid) {
225
+ return {
226
+ target,
227
+ status: "failed",
228
+ pid: processRecord.pid,
229
+ detail: "Refusing to stop the current CLI process.",
230
+ };
231
+ }
232
+ try {
233
+ process.kill(processRecord.pid, "SIGTERM");
234
+ await waitForProcessExit(processRecord.pid, STOP_TIMEOUT_MS);
235
+ if (isProcessAlive(processRecord.pid)) {
236
+ return {
237
+ target,
238
+ status: "failed",
239
+ pid: processRecord.pid,
240
+ detail: `${target} process did not exit after SIGTERM.`,
241
+ };
242
+ }
243
+ return {
244
+ target,
245
+ status: "stopped",
246
+ pid: processRecord.pid,
247
+ detail: `${target} process stopped.`,
248
+ };
249
+ }
250
+ catch (error) {
251
+ if (isNodeError(error) && error.code === "ESRCH") {
252
+ return {
253
+ target,
254
+ status: "stale",
255
+ pid: processRecord.pid,
256
+ detail: `Removed stale ${target} process record.`,
257
+ };
258
+ }
259
+ return {
260
+ target,
261
+ status: "failed",
262
+ pid: processRecord.pid,
263
+ detail: error instanceof Error ? error.message : String(error),
264
+ };
265
+ }
266
+ }
84
267
  async function resolveWebRuntimeStartScript(context) {
85
268
  const configured = trimToNull(context.env.MONEYSIREN_WEB_RUNTIME_DIR);
86
269
  if (configured !== null) {
@@ -424,6 +607,99 @@ async function pathExists(path) {
424
607
  return false;
425
608
  }
426
609
  }
610
+ async function readDesktopRuntimeState(context) {
611
+ try {
612
+ const parsed = JSON.parse(await readFile(resolveDesktopRuntimeStatePath(context), "utf8"));
613
+ return parseDesktopRuntimeState(parsed);
614
+ }
615
+ catch {
616
+ return null;
617
+ }
618
+ }
619
+ async function updateDesktopRuntimeState(context, update) {
620
+ await writeOrRemoveDesktopRuntimeState(context, update(await readDesktopRuntimeState(context) ?? emptyDesktopRuntimeState()));
621
+ }
622
+ async function writeOrRemoveDesktopRuntimeState(context, state) {
623
+ const statePath = resolveDesktopRuntimeStatePath(context);
624
+ if (state.web === undefined && state.hud === undefined) {
625
+ await rm(statePath, { force: true });
626
+ return;
627
+ }
628
+ const nextState = {
629
+ ...state,
630
+ updatedAt: new Date().toISOString(),
631
+ };
632
+ await mkdir(dirname(statePath), { recursive: true });
633
+ await writeFile(statePath, `${JSON.stringify(nextState, null, 2)}\n`, "utf8");
634
+ }
635
+ function resolveDesktopRuntimeStatePath(context) {
636
+ const configured = trimToNull(context.env[DESKTOP_STATE_ENV_KEY]);
637
+ if (configured !== null) {
638
+ return resolve(context.cwd, configured);
639
+ }
640
+ if (process.platform === "win32") {
641
+ return win32.join(trimToNull(context.env.APPDATA) ?? win32.join(resolveHomeDirectory(context.env), "AppData", "Roaming"), "MoneySiren", "desktop-runtime.json");
642
+ }
643
+ if (process.platform === "darwin") {
644
+ return posix.join(resolveHomeDirectory(context.env), "Library", "Application Support", "MoneySiren", "desktop-runtime.json");
645
+ }
646
+ return posix.join(trimToNull(context.env.XDG_STATE_HOME) ?? posix.join(resolveHomeDirectory(context.env), ".local", "state"), "moneysiren", "desktop-runtime.json");
647
+ }
648
+ function parseDesktopRuntimeState(value) {
649
+ if (!isRecord(value) || value.version !== 1 || typeof value.updatedAt !== "string") {
650
+ return null;
651
+ }
652
+ const web = parseManagedDesktopProcess(value.web);
653
+ const hud = parseManagedDesktopProcess(value.hud);
654
+ return {
655
+ version: 1,
656
+ updatedAt: value.updatedAt,
657
+ ...(web === undefined ? {} : { web }),
658
+ ...(hud === undefined ? {} : { hud }),
659
+ };
660
+ }
661
+ function parseManagedDesktopProcess(value) {
662
+ if (!isRecord(value) || typeof value.pid !== "number" || typeof value.startedAt !== "string" || !Number.isSafeInteger(value.pid)) {
663
+ return undefined;
664
+ }
665
+ return {
666
+ pid: value.pid,
667
+ startedAt: value.startedAt,
668
+ ...(typeof value.port === "number" && Number.isSafeInteger(value.port) ? { port: value.port } : {}),
669
+ ...(typeof value.dashboardUrl === "string" ? { dashboardUrl: value.dashboardUrl } : {}),
670
+ ...(typeof value.executablePath === "string" ? { executablePath: value.executablePath } : {}),
671
+ };
672
+ }
673
+ function emptyDesktopRuntimeState() {
674
+ return {
675
+ version: 1,
676
+ updatedAt: new Date().toISOString(),
677
+ };
678
+ }
679
+ function isProcessAlive(pid) {
680
+ if (pid <= 0) {
681
+ return false;
682
+ }
683
+ try {
684
+ process.kill(pid, 0);
685
+ return true;
686
+ }
687
+ catch (error) {
688
+ return isNodeError(error) && error.code === "EPERM";
689
+ }
690
+ }
691
+ async function waitForProcessExit(pid, timeoutMs) {
692
+ const deadline = Date.now() + timeoutMs;
693
+ while (Date.now() < deadline) {
694
+ if (!isProcessAlive(pid)) {
695
+ return;
696
+ }
697
+ await new Promise((resolveTimeout) => setTimeout(resolveTimeout, 100));
698
+ }
699
+ }
700
+ function resolveHomeDirectory(env) {
701
+ return trimToNull(env.HOME) ?? trimToNull(env.USERPROFILE) ?? homedir();
702
+ }
427
703
  function configuredPort(env) {
428
704
  const parsed = Number.parseInt(env.PORT ?? "", 10);
429
705
  return Number.isSafeInteger(parsed) && parsed > 0 && parsed <= 65_535 ? parsed : DEFAULT_WEB_PORT;
@@ -438,4 +714,7 @@ function errorMessage(error) {
438
714
  function isRecord(value) {
439
715
  return typeof value === "object" && value !== null && !Array.isArray(value);
440
716
  }
717
+ function isNodeError(value) {
718
+ return value instanceof Error && "code" in value;
719
+ }
441
720
  //# sourceMappingURL=desktop-runtime.js.map
@@ -12,6 +12,8 @@ export function renderHomeScreen(input) {
12
12
  ` ${theme.command("/modes")} Show the CLI, web, and desktop modes`,
13
13
  ` ${theme.command("/start")} Start the installed dashboard runtime`,
14
14
  ` ${theme.command("/hud")} Start the installed runtime and open HUD`,
15
+ ` ${theme.command("/status")} Show managed web, HUD, and local API runtime status`,
16
+ ` ${theme.command("/stop")} Stop managed web, HUD, and local API runtimes`,
15
17
  ` ${theme.command("/init")} Create local SQLite storage`,
16
18
  ` ${theme.command("/dashboard")} Check the local dashboard API`,
17
19
  ` ${theme.command("/dashboard check")} Same as /dashboard`,
@@ -31,6 +33,8 @@ export function renderHomeScreen(input) {
31
33
  theme.heading("Classic CLI"),
32
34
  " msiren start",
33
35
  " msiren hud",
36
+ " msiren status",
37
+ " msiren stop",
34
38
  " msiren install --all",
35
39
  "",
36
40
  theme.heading("Full command"),
@@ -40,6 +44,8 @@ export function renderHomeScreen(input) {
40
44
  " moneysiren modes",
41
45
  " moneysiren init",
42
46
  " moneysiren serve [--port <port>]",
47
+ " moneysiren stop [--web|--hud|--api|--all]",
48
+ " moneysiren restart [--port <port>] [--open|--no-open] [--hud]",
43
49
  " moneysiren open",
44
50
  " moneysiren sync --provider mock",
45
51
  " moneysiren summary --json",
@@ -76,6 +82,9 @@ Usage:
76
82
  moneysiren modes
77
83
  moneysiren start [--port <port>] [--open|--no-open] [--hud]
78
84
  moneysiren hud [--port <port>]
85
+ moneysiren status
86
+ moneysiren stop [--web|--hud|--api|--all]
87
+ moneysiren restart [--port <port>] [--open|--no-open] [--hud]
79
88
  moneysiren dashboard check [--url <local-dashboard-url>]
80
89
  moneysiren serve [--port <port>]
81
90
  moneysiren open
@@ -97,6 +106,8 @@ Slash commands:
97
106
  moneysiren /modes
98
107
  moneysiren /start
99
108
  moneysiren /hud
109
+ moneysiren /status
110
+ moneysiren /stop [--web|--hud|--api|--all]
100
111
  moneysiren /init
101
112
  moneysiren /dashboard
102
113
  moneysiren /dashboard check
@@ -1,6 +1,6 @@
1
1
  import type { InstallSurface } from "./install-profile.js";
2
2
  export declare const DEFAULT_RELEASE_REPOSITORY = "ztwz11/moneysiren";
3
- export declare const DEFAULT_RELEASE_TAG = "v0.1.0-alpha.6";
3
+ export declare const DEFAULT_RELEASE_TAG = "v0.1.0-alpha.7";
4
4
  export interface ReleaseInstallOptions {
5
5
  env?: Record<string, string | undefined>;
6
6
  fetchImpl: typeof fetch;
@@ -7,7 +7,7 @@ import { promisify } from "node:util";
7
7
  const execFileAsync = promisify(execFile);
8
8
  export const DEFAULT_RELEASE_REPOSITORY = "ztwz11/moneysiren";
9
9
  // Keep the source-free installer pinned to the latest published desktop/web release tag.
10
- export const DEFAULT_RELEASE_TAG = "v0.1.0-alpha.6";
10
+ export const DEFAULT_RELEASE_TAG = "v0.1.0-alpha.7";
11
11
  const RELEASE_REPOSITORY_ENV_KEY = "MONEYSIREN_RELEASE_REPOSITORY";
12
12
  const RELEASE_TAG_ENV_KEY = "MONEYSIREN_RELEASE_TAG";
13
13
  const RELEASE_INSTALL_DIR_ENV_KEY = "MONEYSIREN_RELEASE_INSTALL_DIR";
@@ -46,6 +46,21 @@ export function resolveSlashCommand(args) {
46
46
  args: ["hud", ...rest],
47
47
  };
48
48
  }
49
+ if (command === "/status") {
50
+ return noExtraArgs(command, rest, ["status"]);
51
+ }
52
+ if (command === "/stop") {
53
+ return {
54
+ kind: "dispatch",
55
+ args: ["stop", ...rest],
56
+ };
57
+ }
58
+ if (command === "/restart") {
59
+ return {
60
+ kind: "dispatch",
61
+ args: ["restart", ...rest],
62
+ };
63
+ }
49
64
  if (command === "/init") {
50
65
  return noExtraArgs(command, rest, ["init"]);
51
66
  }
@@ -1,2 +1,2 @@
1
- export declare const CLI_VERSION = "0.1.0-alpha.6";
1
+ export declare const CLI_VERSION = "0.1.0-alpha.7";
2
2
  //# sourceMappingURL=version.d.ts.map
@@ -1,2 +1,2 @@
1
- export const CLI_VERSION = "0.1.0-alpha.6";
1
+ export const CLI_VERSION = "0.1.0-alpha.7";
2
2
  //# sourceMappingURL=version.js.map
@@ -3,7 +3,7 @@ import { parseNotificationPreferences, readNotificationDigest, readNotificationP
3
3
  import { assertLoopbackHost, isLoopbackHost, removeRuntimeLock, writeRuntimeLock, } from "../../runtime/src/index.js";
4
4
  const DEFAULT_HOST = "127.0.0.1";
5
5
  const DEFAULT_PORT = 47831;
6
- const DEFAULT_VERSION = "0.1.0-alpha.6";
6
+ const DEFAULT_VERSION = "0.1.0-alpha.7";
7
7
  export async function startLocalApiServer(options = {}) {
8
8
  const host = options.host ?? DEFAULT_HOST;
9
9
  const requestedPort = options.port ?? DEFAULT_PORT;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@moneysiren/cli",
3
- "version": "0.1.0-alpha.6",
3
+ "version": "0.1.0-alpha.7",
4
4
  "description": "Local-first cloud/SaaS usage, status, and expected billing CLI for MoneySiren.",
5
5
  "private": false,
6
6
  "license": "MIT",