@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 +2 -2
- package/dist/apps/cli/src/cli.js +10 -1
- package/dist/apps/cli/src/commands/modes.js +4 -1
- package/dist/apps/cli/src/commands/runtime.d.ts +3 -0
- package/dist/apps/cli/src/commands/runtime.js +199 -0
- package/dist/apps/cli/src/desktop-runtime.d.ts +23 -0
- package/dist/apps/cli/src/desktop-runtime.js +281 -2
- package/dist/apps/cli/src/home.js +11 -0
- package/dist/apps/cli/src/release-installer.d.ts +1 -1
- package/dist/apps/cli/src/release-installer.js +1 -1
- package/dist/apps/cli/src/slash.js +15 -0
- package/dist/apps/cli/src/version.d.ts +1 -1
- package/dist/apps/cli/src/version.js +1 -1
- package/dist/packages/local-api/src/server.js +1 -1
- package/package.json +1 -1
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.
|
|
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.
|
|
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
|
package/dist/apps/cli/src/cli.js
CHANGED
|
@@ -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
|
|
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 {
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
+
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;
|