@moneysiren/cli 0.1.0-alpha.0 → 0.1.0-alpha.2

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.
Files changed (36) hide show
  1. package/README.md +6 -3
  2. package/dist/apps/cli/src/cli.d.ts +4 -1
  3. package/dist/apps/cli/src/cli.js +11 -3
  4. package/dist/apps/cli/src/commands/install.js +125 -6
  5. package/dist/apps/cli/src/commands/modes.js +15 -10
  6. package/dist/apps/cli/src/commands/runtime.d.ts +2 -0
  7. package/dist/apps/cli/src/commands/runtime.js +167 -0
  8. package/dist/apps/cli/src/desktop-runtime.d.ts +31 -0
  9. package/dist/apps/cli/src/desktop-runtime.js +441 -0
  10. package/dist/apps/cli/src/home.js +16 -0
  11. package/dist/apps/cli/src/index.js +0 -0
  12. package/dist/apps/cli/src/postinstall.js +1 -1
  13. package/dist/apps/cli/src/release-installer.d.ts +54 -0
  14. package/dist/apps/cli/src/release-installer.js +393 -0
  15. package/dist/apps/cli/src/slash.js +12 -0
  16. package/dist/apps/cli/src/version.d.ts +2 -0
  17. package/dist/apps/cli/src/version.js +2 -0
  18. package/dist/packages/config/src/load.js +3 -0
  19. package/dist/packages/config/src/schema.d.ts +3 -0
  20. package/dist/packages/config/src/schema.js +3 -0
  21. package/dist/packages/local-api/src/server.js +1 -1
  22. package/dist/packages/view-model/src/hud-model.d.ts +74 -0
  23. package/dist/packages/view-model/src/hud-model.js +295 -0
  24. package/dist/packages/view-model/src/index.d.ts +5 -2
  25. package/dist/packages/view-model/src/index.js +4 -1
  26. package/dist/packages/view-model/src/notification-preferences-model.d.ts +30 -2
  27. package/dist/packages/view-model/src/notification-preferences-model.js +183 -1
  28. package/dist/packages/view-model/src/notification-preferences.d.ts +1 -1
  29. package/dist/packages/view-model/src/notification-preferences.js +1 -1
  30. package/dist/packages/view-model/src/sync-state.d.ts +47 -0
  31. package/dist/packages/view-model/src/sync-state.js +140 -0
  32. package/dist/packages/view-model/src/usage-progress.d.ts +22 -0
  33. package/dist/packages/view-model/src/usage-progress.js +57 -0
  34. package/dist/packages/view-model/src/view-model.d.ts +22 -0
  35. package/dist/packages/view-model/src/view-model.js +142 -0
  36. package/package.json +3 -2
package/README.md CHANGED
@@ -20,6 +20,7 @@ moneysiren
20
20
  moneysiren --version
21
21
  moneysiren /version
22
22
  moneysiren install --status
23
+ moneysiren install --all
23
24
  moneysiren modes
24
25
  moneysiren /modes
25
26
  moneysiren doctor
@@ -33,7 +34,7 @@ During a PowerShell, cmd, or shell install with an interactive TTY, `postinstall
33
34
  - Web dashboard
34
35
  - HUD
35
36
 
36
- Press Enter to accept the recommended default, which selects all three. In CI or non-interactive npm installs, MoneySiren writes that all-selected profile automatically. Re-run `moneysiren install` later to change the profile, or use `moneysiren install --status` to inspect it.
37
+ Press Enter to accept the recommended default, which selects all three. In CI or non-interactive npm installs, MoneySiren writes that all-selected profile automatically. Run `moneysiren install --all` to download GitHub Release assets for the web runtime and HUD desktop shell. Use `moneysiren install --profile-only` to change only the local profile, or `moneysiren install --status` to inspect it.
37
38
 
38
39
  One-off execution:
39
40
 
@@ -66,7 +67,7 @@ Install the generated tarball into a temporary project:
66
67
  mkdir -p /tmp/moneysiren-alpha-review
67
68
  cd /tmp/moneysiren-alpha-review
68
69
  npm init -y
69
- npm install /path/to/moneysiren-cli-0.1.0-alpha.0.tgz
70
+ npm install /path/to/moneysiren-cli-0.1.0-alpha.2.tgz
70
71
  npm exec moneysiren
71
72
  npm exec moneysiren -- --version
72
73
  npm exec moneysiren -- /version
@@ -82,7 +83,7 @@ PowerShell equivalent for the temporary project:
82
83
  New-Item -ItemType Directory -Force -Path $env:TEMP\moneysiren-alpha-review
83
84
  Set-Location $env:TEMP\moneysiren-alpha-review
84
85
  npm init -y
85
- npm install C:\path\to\moneysiren-cli-0.1.0-alpha.0.tgz
86
+ npm install C:\path\to\moneysiren-cli-0.1.0-alpha.2.tgz
86
87
  npm exec moneysiren
87
88
  npm exec moneysiren -- --version
88
89
  npm exec moneysiren -- modes
@@ -104,6 +105,8 @@ npm run publish:cli:alpha
104
105
 
105
106
  The dry run checks the full secret scan, package metadata, npm registry version availability, and tarball contents. The publish command requires `npm login` in the local terminal and publishes this package with the `alpha` tag and public access.
106
107
 
108
+ If npm requires passkey or browser approval, complete the URL printed by npm and rerun the publish command. For CI publishing, add a granular npm token with publish access and bypass 2FA enabled as the `NPM_TOKEN` GitHub repository secret, then run the `npm-publish-cli` workflow manually.
109
+
107
110
  ## Slash Home
108
111
 
109
112
  Running `moneysiren` without subcommands prints a readable slash-command home guide. In a TTY it may enter a minimal line-based slash prompt; in CI or non-TTY package review it prints the guide and exits `0`.
@@ -1,11 +1,12 @@
1
1
  import { type CliLocalRuntimeAdapter } from "./runtime-adapter.js";
2
+ import type { CliDesktopRuntimeAdapter } from "./desktop-runtime.js";
2
3
  import { type Theme } from "./theme.js";
3
4
  import type { SlackReportTransport } from "../../../packages/report/src/index.js";
4
5
  import type { AwsCostExplorerClientAdapter } from "../../../packages/connectors/aws/src/index.js";
5
6
  import type { CloudflareBillingUsageClient } from "../../../packages/connectors/cloudflare/src/index.js";
6
7
  import type { OpenAiUsageCostsClient } from "../../../packages/connectors/openai/src/index.js";
7
8
  import type { SupabaseManagementClient } from "../../../packages/connectors/supabase/src/index.js";
8
- export declare const CLI_VERSION = "0.1.0-alpha.0";
9
+ export { CLI_VERSION } from "./version.js";
9
10
  export interface CliRuntime {
10
11
  cwd?: string;
11
12
  env?: Record<string, string | undefined>;
@@ -23,6 +24,7 @@ export interface CliRuntime {
23
24
  liveClients?: CliLiveClients;
24
25
  fetch?: typeof fetch;
25
26
  localRuntime?: CliLocalRuntimeAdapter;
27
+ desktopRuntime?: CliDesktopRuntimeAdapter;
26
28
  openUrl?: (url: string) => Promise<void> | void;
27
29
  }
28
30
  export interface CliLiveClients {
@@ -46,6 +48,7 @@ export interface CliExecutionContext {
46
48
  interactive: boolean;
47
49
  theme: Theme;
48
50
  localRuntime?: CliLocalRuntimeAdapter;
51
+ desktopRuntime?: CliDesktopRuntimeAdapter;
49
52
  }
50
53
  export interface CliResult {
51
54
  exitCode: number;
@@ -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, runOpenCommand, runServeCommand } from "./commands/runtime.js";
8
+ import { runDesktopCommand, runHudCommand, runOpenCommand, runServeCommand, runStartCommand } 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";
@@ -14,7 +14,8 @@ import { runSlashPrompt } from "./interactive.js";
14
14
  import { openUrlInBrowser } from "./runtime-adapter.js";
15
15
  import { resolveSlashCommand } from "./slash.js";
16
16
  import { createTheme } from "./theme.js";
17
- export const CLI_VERSION = "0.1.0-alpha.0";
17
+ import { CLI_VERSION } from "./version.js";
18
+ export { CLI_VERSION } from "./version.js";
18
19
  const HELP = renderHelpScreen(CLI_VERSION);
19
20
  export async function runCli(args, runtime = {}) {
20
21
  const stdout = runtime.stdoutBuffer ?? [];
@@ -60,6 +61,7 @@ export async function runCli(args, runtime = {}) {
60
61
  }),
61
62
  theme,
62
63
  ...(runtime.localRuntime === undefined ? {} : { localRuntime: runtime.localRuntime }),
64
+ ...(runtime.desktopRuntime === undefined ? {} : { desktopRuntime: runtime.desktopRuntime }),
63
65
  };
64
66
  context.stdin = runtime.stdin ?? process.stdin;
65
67
  context.output = runtime.output ?? process.stdout;
@@ -129,6 +131,12 @@ async function dispatchCommand(args, context) {
129
131
  if (command === "serve") {
130
132
  return runServeCommand(rest, context);
131
133
  }
134
+ if (command === "start") {
135
+ return runStartCommand(rest, context);
136
+ }
137
+ if (command === "hud") {
138
+ return runHudCommand(rest, context);
139
+ }
132
140
  if (command === "open") {
133
141
  return runOpenCommand(rest, context);
134
142
  }
@@ -151,7 +159,7 @@ async function dispatchCommand(args, context) {
151
159
  return runThemeCommand(rest, context);
152
160
  }
153
161
  context.stderr(`Unknown command: ${command}`);
154
- context.stderr("Run `moneysiren --help` for usage.");
162
+ context.stderr("Run `msiren --help` or `moneysiren --help` for usage.");
155
163
  return 1;
156
164
  }
157
165
  async function dispatchSlashCommand(args, context) {
@@ -1,12 +1,14 @@
1
1
  import { DEFAULT_INSTALL_SURFACES, INSTALL_SURFACES, isInstallSurface, readInstallProfileFile, resolveInstallProfilePath, writeInstallProfileFile, } from "../install-profile.js";
2
2
  import { formatInstallSelectionLine, installSelectionHelp, parseInstallSurfaceSelection, promptForInstallSurfaces, } from "../install-selector.js";
3
+ import { DEFAULT_RELEASE_REPOSITORY, DEFAULT_RELEASE_TAG, installReleaseAssets, } from "../release-installer.js";
3
4
  const INSTALL_USAGE = [
4
- "Usage: moneysiren install [--status|--all|--cli|--web|--hud|--no-cli|--no-web|--no-hud]",
5
+ "Usage: moneysiren install [--status|--all|--cli|--web|--hud|--no-cli|--no-web|--no-hud] [--profile-only] [--tag <tag>] [--repo <owner/name>] [--dir <path>]",
5
6
  "",
6
7
  "Components:",
7
8
  installSelectionHelp(),
8
9
  "",
9
10
  "Default: all components selected (recommended).",
11
+ `Release default: ${DEFAULT_RELEASE_REPOSITORY}@${DEFAULT_RELEASE_TAG}.`,
10
12
  ].join("\n");
11
13
  export async function runInstallCommand(args, context) {
12
14
  if (args.includes("--help") || args.includes("-h")) {
@@ -21,7 +23,15 @@ export async function runInstallCommand(args, context) {
21
23
  context.stderr(INSTALL_USAGE);
22
24
  return 1;
23
25
  }
24
- const selectedSurfaces = parsed ?? await selectedSurfacesFromPromptOrDefault(context);
26
+ const selectedSurfaces = parsed.selectedSurfaces ?? await selectedSurfacesFromPromptOrDefault(context);
27
+ const releaseResult = await installReleaseAssetsForSelectionSafely({
28
+ context,
29
+ parsed,
30
+ selectedSurfaces,
31
+ });
32
+ if (releaseResult === "failed") {
33
+ return 1;
34
+ }
25
35
  const profile = await writeInstallProfileFile({
26
36
  selectedSurfaces,
27
37
  source: "cli",
@@ -32,6 +42,7 @@ export async function runInstallCommand(args, context) {
32
42
  });
33
43
  context.stdout("MoneySiren install profile updated.");
34
44
  context.stdout(formatInstallSelectionLine(profile.selectedSurfaces));
45
+ writeReleaseInstallResult(context, releaseResult);
35
46
  context.stdout("Secrets returned: false");
36
47
  return 0;
37
48
  }
@@ -59,17 +70,63 @@ async function selectedSurfacesFromPromptOrDefault(context) {
59
70
  }
60
71
  function parseInstallArgs(args) {
61
72
  if (args.length === 0) {
62
- return undefined;
73
+ return {
74
+ profileOnly: false,
75
+ };
63
76
  }
64
77
  if (args.length === 1) {
65
78
  const selected = parseInstallSurfaceSelection(args[0] ?? "");
66
79
  if (selected !== null) {
67
- return selected;
80
+ return {
81
+ profileOnly: false,
82
+ selectedSurfaces: selected,
83
+ };
68
84
  }
69
85
  }
70
86
  let explicitIncludes = false;
87
+ let installDir;
88
+ let profileOnly = false;
89
+ let releaseRepository;
90
+ let releaseTag;
71
91
  const selected = new Set();
72
- for (const arg of args) {
92
+ for (let index = 0; index < args.length; index += 1) {
93
+ const arg = args[index];
94
+ if (arg === undefined) {
95
+ return null;
96
+ }
97
+ if (arg === "--profile-only") {
98
+ profileOnly = true;
99
+ continue;
100
+ }
101
+ if (arg === "--tag" || arg === "--repo" || arg === "--dir") {
102
+ const value = args[index + 1];
103
+ if (value === undefined || value.startsWith("--")) {
104
+ return null;
105
+ }
106
+ if (arg === "--tag") {
107
+ releaseTag = value;
108
+ }
109
+ else if (arg === "--repo") {
110
+ releaseRepository = value;
111
+ }
112
+ else {
113
+ installDir = value;
114
+ }
115
+ index += 1;
116
+ continue;
117
+ }
118
+ if (arg.startsWith("--tag=")) {
119
+ releaseTag = arg.slice("--tag=".length);
120
+ continue;
121
+ }
122
+ if (arg.startsWith("--repo=")) {
123
+ releaseRepository = arg.slice("--repo=".length);
124
+ continue;
125
+ }
126
+ if (arg.startsWith("--dir=")) {
127
+ installDir = arg.slice("--dir=".length);
128
+ continue;
129
+ }
73
130
  if (arg === "--all") {
74
131
  for (const surface of DEFAULT_INSTALL_SURFACES) {
75
132
  selected.add(surface);
@@ -107,10 +164,72 @@ function parseInstallArgs(args) {
107
164
  explicitIncludes = true;
108
165
  }
109
166
  const normalized = INSTALL_SURFACES.filter((surface) => selected.has(surface));
110
- return normalized.length === 0 ? null : normalized;
167
+ if (normalized.length === 0 && explicitIncludes) {
168
+ return null;
169
+ }
170
+ return {
171
+ ...(installDir === undefined ? {} : { installDir }),
172
+ profileOnly,
173
+ ...(releaseRepository === undefined ? {} : { releaseRepository }),
174
+ ...(releaseTag === undefined ? {} : { releaseTag }),
175
+ ...(normalized.length === 0 ? {} : { selectedSurfaces: normalized }),
176
+ };
111
177
  }
112
178
  function isDefaultSelection(selectedSurfaces) {
113
179
  return selectedSurfaces.length === INSTALL_SURFACES.length &&
114
180
  INSTALL_SURFACES.every((surface, index) => selectedSurfaces[index] === surface);
115
181
  }
182
+ async function installReleaseAssetsForSelection(input) {
183
+ if (input.parsed.profileOnly) {
184
+ return "profile-only";
185
+ }
186
+ if (!input.selectedSurfaces.some((surface) => surface === "web" || surface === "hud")) {
187
+ return "cli-only";
188
+ }
189
+ return installReleaseAssets({
190
+ env: input.context.env,
191
+ fetchImpl: input.context.fetch,
192
+ ...(input.parsed.installDir === undefined ? {} : { installDir: input.parsed.installDir }),
193
+ now: input.context.now,
194
+ ...(input.parsed.releaseRepository === undefined ? {} : { repository: input.parsed.releaseRepository }),
195
+ selectedSurfaces: input.selectedSurfaces,
196
+ ...(input.parsed.releaseTag === undefined ? {} : { tag: input.parsed.releaseTag }),
197
+ });
198
+ }
199
+ async function installReleaseAssetsForSelectionSafely(input) {
200
+ try {
201
+ return await installReleaseAssetsForSelection(input);
202
+ }
203
+ catch (error) {
204
+ const message = error instanceof Error ? error.message : String(error);
205
+ input.context.stderr(`Release asset installation failed: ${message}`);
206
+ if (input.selectedSurfaces.includes("hud")) {
207
+ input.context.stderr("The selected HUD desktop artifact must be present, checksummed, and signed before MoneySiren will install it.");
208
+ input.context.stderr("For now, use `moneysiren install --web` to install only the web runtime, or retry after a signed desktop release is published.");
209
+ }
210
+ input.context.stderr("Install profile was not changed.");
211
+ return "failed";
212
+ }
213
+ }
214
+ function writeReleaseInstallResult(context, result) {
215
+ if (result === "profile-only") {
216
+ context.stdout("Release assets: skipped (--profile-only).");
217
+ return;
218
+ }
219
+ if (result === "cli-only") {
220
+ context.stdout("Release assets: skipped (CLI-only selection).");
221
+ return;
222
+ }
223
+ writeReleaseInstallSummary(context, result);
224
+ }
225
+ function writeReleaseInstallSummary(context, result) {
226
+ context.stdout(`Release: ${result.repository}@${result.tag}`);
227
+ context.stdout(`Release URL: ${result.releaseUrl}`);
228
+ context.stdout(`Install directory: ${result.installDir}`);
229
+ for (const asset of result.assets) {
230
+ context.stdout(`Downloaded ${asset.surface}: ${asset.name}`);
231
+ context.stdout(` SHA256 verified: ${asset.checksumVerified ? "yes" : "checksum unavailable"}`);
232
+ context.stdout(` Signature verified: ${asset.signatureVerified ? "yes" : "not required"}`);
233
+ }
234
+ }
116
235
  //# sourceMappingURL=install.js.map
@@ -17,25 +17,30 @@ export async function runModesCommand(args, context) {
17
17
  context.stdout(`Platform: ${platformLabel()}`);
18
18
  context.stdout(`Install profile: ${formatInstallSurfaces(selectedSurfaces)}${profile === null ? " (recommended default)" : ""}`);
19
19
  context.stdout("npm install: npm install -g @moneysiren/cli@alpha");
20
+ context.stdout("Short command: msiren");
20
21
  context.stdout(`Runtime lock: ${runtimeLockHint()}`);
21
22
  context.stdout("");
22
23
  context.stdout("1. CLI automation");
23
24
  context.stdout(` Status: ${surfaceStatus("cli", selectedSurfaces)} from the npm CLI package`);
24
- context.stdout(" Try: moneysiren doctor");
25
- context.stdout(" Try: moneysiren sync --provider mock");
25
+ context.stdout(" Try: msiren doctor");
26
+ context.stdout(" Try: msiren sync --provider mock");
26
27
  context.stdout("");
27
28
  context.stdout("2. Local web dashboard/runtime");
28
- context.stdout(` Status: ${surfaceStatus("web", selectedSurfaces)} local API runtime is available from the npm CLI package`);
29
- context.stdout(" Try: moneysiren serve [--port <port>]");
30
- context.stdout(" Try: moneysiren dashboard check");
31
- context.stdout(" Note: the full Next.js dashboard is run from the repo or a future bundled desktop app.");
29
+ context.stdout(` Status: ${surfaceStatus("web", selectedSurfaces)} GitHub Release web runtime archive is installed by the CLI`);
30
+ context.stdout(" Install: msiren install --web");
31
+ context.stdout(" Try: msiren start");
32
+ context.stdout(" Try: msiren dashboard check");
33
+ context.stdout(" Note: msiren start runs the installed GitHub Release web runtime.");
32
34
  context.stdout("");
33
35
  context.stdout("3. Desktop tray/notifier");
34
- context.stdout(` Status: ${surfaceStatus("hud", selectedSurfaces)} Windows/macOS target is the thin Tauri tray shell; the native tray binary is not bundled in moneysiren`);
35
- context.stdout(" Try: moneysiren desktop status");
36
- context.stdout(" Try: moneysiren notify once --dry-run");
36
+ context.stdout(` Status: ${surfaceStatus("hud", selectedSurfaces)} Windows/macOS target is the thin Tauri tray shell from GitHub Releases`);
37
+ context.stdout(" Install: msiren install --hud");
38
+ context.stdout(" Try: msiren hud");
39
+ context.stdout(" Try: msiren desktop status");
40
+ context.stdout(" Try: msiren notify once --dry-run");
37
41
  context.stdout("");
38
- context.stdout("Change selection: moneysiren install");
42
+ context.stdout("Install recommended set: msiren install --all");
43
+ context.stdout("Change selection only: msiren install --profile-only");
39
44
  return 0;
40
45
  }
41
46
  function surfaceStatus(surface, selectedSurfaces) {
@@ -1,5 +1,7 @@
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 runStartCommand(args: readonly string[], context: CliExecutionContext): Promise<number>;
4
+ export declare function runHudCommand(args: readonly string[], context: CliExecutionContext): Promise<number>;
3
5
  export declare function runOpenCommand(args: readonly string[], context: CliExecutionContext): Promise<number>;
4
6
  export declare function runDesktopCommand(args: readonly string[], context: CliExecutionContext): Promise<number>;
5
7
  //# sourceMappingURL=runtime.d.ts.map
@@ -1,5 +1,8 @@
1
+ import { createFallbackDesktopRuntimeAdapter, } from "../desktop-runtime.js";
1
2
  import { createFallbackLocalRuntimeAdapter, } from "../runtime-adapter.js";
2
3
  const SERVE_USAGE = "Usage: moneysiren serve [--port <port>]";
4
+ const START_USAGE = "Usage: msiren start [--port <port>] [--open|--no-open] [--hud]";
5
+ const HUD_USAGE = "Usage: msiren hud [--port <port>]";
3
6
  const OPEN_USAGE = "Usage: moneysiren open";
4
7
  const DESKTOP_USAGE = "Usage: moneysiren desktop status";
5
8
  export async function runServeCommand(args, context) {
@@ -18,6 +21,62 @@ export async function runServeCommand(args, context) {
18
21
  });
19
22
  return writeStartRuntimeResult(context, result, "MoneySiren local runtime");
20
23
  }
24
+ export async function runStartCommand(args, context) {
25
+ if (args.includes("--help") || args.includes("-h")) {
26
+ context.stdout(START_USAGE);
27
+ return 0;
28
+ }
29
+ const parsed = parseStartArgs(args);
30
+ if (parsed === undefined) {
31
+ context.stderr(START_USAGE);
32
+ return 1;
33
+ }
34
+ const adapter = desktopRuntimeAdapter(context);
35
+ const web = await adapter.startWebRuntime({
36
+ openBrowser: parsed.openBrowser,
37
+ ...(parsed.port === undefined ? {} : { port: parsed.port }),
38
+ });
39
+ const webExitCode = writeDesktopRuntimeResult(context, web, "MoneySiren dashboard runtime");
40
+ if (webExitCode !== 0) {
41
+ return webExitCode;
42
+ }
43
+ if (web.status !== "unavailable" && parsed.openBrowser) {
44
+ await context.openUrl(web.dashboardUrl);
45
+ context.stdout(`Dashboard URL: ${web.dashboardUrl}`);
46
+ }
47
+ if (!parsed.launchHud) {
48
+ context.stdout("HUD: run `msiren hud` to open the floating desktop widget.");
49
+ return 0;
50
+ }
51
+ const hud = await adapter.startHud({
52
+ ...(parsed.port === undefined ? {} : { port: parsed.port }),
53
+ });
54
+ return writeDesktopShellResult(context, hud, "MoneySiren HUD");
55
+ }
56
+ export async function runHudCommand(args, context) {
57
+ if (args.includes("--help") || args.includes("-h")) {
58
+ context.stdout(HUD_USAGE);
59
+ return 0;
60
+ }
61
+ const parsed = parseHudArgs(args);
62
+ if (parsed === undefined) {
63
+ context.stderr(HUD_USAGE);
64
+ return 1;
65
+ }
66
+ const adapter = desktopRuntimeAdapter(context);
67
+ const web = await adapter.startWebRuntime({
68
+ openBrowser: false,
69
+ ...(parsed.port === undefined ? {} : { port: parsed.port }),
70
+ });
71
+ const webExitCode = writeDesktopRuntimeResult(context, web, "MoneySiren dashboard runtime");
72
+ if (webExitCode !== 0) {
73
+ return webExitCode;
74
+ }
75
+ const hud = await adapter.startHud({
76
+ ...(parsed.port === undefined ? {} : { port: parsed.port }),
77
+ });
78
+ return writeDesktopShellResult(context, hud, "MoneySiren HUD");
79
+ }
21
80
  export async function runOpenCommand(args, context) {
22
81
  if (args.includes("--help") || args.includes("-h")) {
23
82
  context.stdout(OPEN_USAGE);
@@ -81,6 +140,9 @@ export async function runDesktopCommand(args, context) {
81
140
  function runtimeAdapter(context) {
82
141
  return context.localRuntime ?? createFallbackLocalRuntimeAdapter(context);
83
142
  }
143
+ function desktopRuntimeAdapter(context) {
144
+ return context.desktopRuntime ?? createFallbackDesktopRuntimeAdapter(context);
145
+ }
84
146
  async function findHealthyRuntime(adapter) {
85
147
  const runtime = await adapter.findRuntime();
86
148
  if (runtime === null) {
@@ -103,6 +165,45 @@ function writeStartRuntimeResult(context, result, heading) {
103
165
  }
104
166
  return 1;
105
167
  }
168
+ function writeDesktopRuntimeResult(context, result, heading) {
169
+ if (result.status !== "unavailable") {
170
+ context.stdout(heading);
171
+ context.stdout(`Runtime: ${result.status}`);
172
+ context.stdout(`Dashboard URL: ${result.dashboardUrl}`);
173
+ if (result.pid !== undefined) {
174
+ context.stdout(`PID: ${result.pid}`);
175
+ }
176
+ for (const note of result.notes) {
177
+ context.stdout(`Note: ${note}`);
178
+ }
179
+ return 0;
180
+ }
181
+ context.stderr(`${heading}: unavailable`);
182
+ context.stderr(result.reason);
183
+ for (const line of result.guidance) {
184
+ context.stderr(line);
185
+ }
186
+ return 1;
187
+ }
188
+ function writeDesktopShellResult(context, result, heading) {
189
+ if (result.status !== "unavailable") {
190
+ context.stdout(heading);
191
+ context.stdout(`Desktop shell: ${result.status}`);
192
+ if (result.pid !== undefined) {
193
+ context.stdout(`PID: ${result.pid}`);
194
+ }
195
+ for (const note of result.notes) {
196
+ context.stdout(`Note: ${note}`);
197
+ }
198
+ return 0;
199
+ }
200
+ context.stderr(`${heading}: unavailable`);
201
+ context.stderr(result.reason);
202
+ for (const line of result.guidance) {
203
+ context.stderr(line);
204
+ }
205
+ return 1;
206
+ }
106
207
  function parseServeArgs(args) {
107
208
  let port;
108
209
  for (let index = 0; index < args.length; index += 1) {
@@ -127,6 +228,72 @@ function parseServeArgs(args) {
127
228
  }
128
229
  return Number.isSafeInteger(port) && port > 0 && port <= 65_535 ? { port } : undefined;
129
230
  }
231
+ function parseStartArgs(args) {
232
+ let launchHud = false;
233
+ let openBrowser = true;
234
+ let port;
235
+ for (let index = 0; index < args.length; index += 1) {
236
+ const arg = args[index];
237
+ if (arg === "--hud") {
238
+ launchHud = true;
239
+ continue;
240
+ }
241
+ if (arg === "--open") {
242
+ openBrowser = true;
243
+ continue;
244
+ }
245
+ if (arg === "--no-open") {
246
+ openBrowser = false;
247
+ continue;
248
+ }
249
+ if (arg === "--port") {
250
+ const value = args[index + 1];
251
+ if (value === undefined || value.startsWith("--")) {
252
+ return undefined;
253
+ }
254
+ port = parsePort(value);
255
+ index += 1;
256
+ continue;
257
+ }
258
+ if (arg?.startsWith("--port=")) {
259
+ port = parsePort(arg.slice("--port=".length));
260
+ continue;
261
+ }
262
+ return undefined;
263
+ }
264
+ if (port !== undefined && (!Number.isSafeInteger(port) || port <= 0 || port > 65_535)) {
265
+ return undefined;
266
+ }
267
+ return {
268
+ launchHud,
269
+ openBrowser,
270
+ ...(port === undefined ? {} : { port }),
271
+ };
272
+ }
273
+ function parseHudArgs(args) {
274
+ let port;
275
+ for (let index = 0; index < args.length; index += 1) {
276
+ const arg = args[index];
277
+ if (arg === "--port") {
278
+ const value = args[index + 1];
279
+ if (value === undefined || value.startsWith("--")) {
280
+ return undefined;
281
+ }
282
+ port = parsePort(value);
283
+ index += 1;
284
+ continue;
285
+ }
286
+ if (arg?.startsWith("--port=")) {
287
+ port = parsePort(arg.slice("--port=".length));
288
+ continue;
289
+ }
290
+ return undefined;
291
+ }
292
+ if (port !== undefined && (!Number.isSafeInteger(port) || port <= 0 || port > 65_535)) {
293
+ return undefined;
294
+ }
295
+ return port === undefined ? {} : { port };
296
+ }
130
297
  function parsePort(value) {
131
298
  return Number.parseInt(value, 10);
132
299
  }
@@ -0,0 +1,31 @@
1
+ import type { CliExecutionContext } from "./cli.js";
2
+ export interface StartWebRuntimeOptions {
3
+ openBrowser: boolean;
4
+ port?: number;
5
+ }
6
+ export interface StartHudOptions {
7
+ port?: number;
8
+ }
9
+ export type DesktopRuntimeResult = {
10
+ status: "running" | "started";
11
+ dashboardUrl: string;
12
+ pid?: number;
13
+ notes: readonly string[];
14
+ } | DesktopRuntimeUnavailableResult;
15
+ export type DesktopShellResult = {
16
+ status: "opened" | "started";
17
+ executablePath: string;
18
+ pid?: number;
19
+ notes: readonly string[];
20
+ } | DesktopRuntimeUnavailableResult;
21
+ export interface DesktopRuntimeUnavailableResult {
22
+ status: "unavailable";
23
+ reason: string;
24
+ guidance: readonly string[];
25
+ }
26
+ export interface CliDesktopRuntimeAdapter {
27
+ startWebRuntime(options: StartWebRuntimeOptions): Promise<DesktopRuntimeResult>;
28
+ startHud(options: StartHudOptions): Promise<DesktopShellResult>;
29
+ }
30
+ export declare function createFallbackDesktopRuntimeAdapter(context: CliExecutionContext): CliDesktopRuntimeAdapter;
31
+ //# sourceMappingURL=desktop-runtime.d.ts.map