@moneysiren/app 0.1.0-alpha.9

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 (140) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +45 -0
  3. package/dist/apps/cli/src/cli.d.ts +59 -0
  4. package/dist/apps/cli/src/cli.js +199 -0
  5. package/dist/apps/cli/src/commands/dashboard.d.ts +3 -0
  6. package/dist/apps/cli/src/commands/dashboard.js +239 -0
  7. package/dist/apps/cli/src/commands/doctor.d.ts +3 -0
  8. package/dist/apps/cli/src/commands/doctor.js +25 -0
  9. package/dist/apps/cli/src/commands/init.d.ts +3 -0
  10. package/dist/apps/cli/src/commands/init.js +18 -0
  11. package/dist/apps/cli/src/commands/install.d.ts +3 -0
  12. package/dist/apps/cli/src/commands/install.js +244 -0
  13. package/dist/apps/cli/src/commands/modes.d.ts +3 -0
  14. package/dist/apps/cli/src/commands/modes.js +73 -0
  15. package/dist/apps/cli/src/commands/notify.d.ts +3 -0
  16. package/dist/apps/cli/src/commands/notify.js +430 -0
  17. package/dist/apps/cli/src/commands/report.d.ts +3 -0
  18. package/dist/apps/cli/src/commands/report.js +206 -0
  19. package/dist/apps/cli/src/commands/runtime.d.ts +10 -0
  20. package/dist/apps/cli/src/commands/runtime.js +499 -0
  21. package/dist/apps/cli/src/commands/shared.d.ts +9 -0
  22. package/dist/apps/cli/src/commands/shared.js +29 -0
  23. package/dist/apps/cli/src/commands/summary.d.ts +3 -0
  24. package/dist/apps/cli/src/commands/summary.js +15 -0
  25. package/dist/apps/cli/src/commands/sync.d.ts +3 -0
  26. package/dist/apps/cli/src/commands/sync.js +393 -0
  27. package/dist/apps/cli/src/commands/theme.d.ts +3 -0
  28. package/dist/apps/cli/src/commands/theme.js +181 -0
  29. package/dist/apps/cli/src/desktop-runtime.d.ts +54 -0
  30. package/dist/apps/cli/src/desktop-runtime.js +720 -0
  31. package/dist/apps/cli/src/home.d.ts +7 -0
  32. package/dist/apps/cli/src/home.js +124 -0
  33. package/dist/apps/cli/src/index.d.ts +3 -0
  34. package/dist/apps/cli/src/index.js +14 -0
  35. package/dist/apps/cli/src/install-profile.d.ts +35 -0
  36. package/dist/apps/cli/src/install-profile.js +124 -0
  37. package/dist/apps/cli/src/install-selector.d.ts +10 -0
  38. package/dist/apps/cli/src/install-selector.js +66 -0
  39. package/dist/apps/cli/src/interactive.d.ts +3 -0
  40. package/dist/apps/cli/src/interactive.js +32 -0
  41. package/dist/apps/cli/src/postinstall.d.ts +3 -0
  42. package/dist/apps/cli/src/postinstall.js +42 -0
  43. package/dist/apps/cli/src/release-installer.d.ts +57 -0
  44. package/dist/apps/cli/src/release-installer.js +432 -0
  45. package/dist/apps/cli/src/runtime-adapter.d.ts +24 -0
  46. package/dist/apps/cli/src/runtime-adapter.js +185 -0
  47. package/dist/apps/cli/src/slash.d.ts +15 -0
  48. package/dist/apps/cli/src/slash.js +229 -0
  49. package/dist/apps/cli/src/summary-model.d.ts +51 -0
  50. package/dist/apps/cli/src/summary-model.js +136 -0
  51. package/dist/apps/cli/src/theme.d.ts +18 -0
  52. package/dist/apps/cli/src/theme.js +118 -0
  53. package/dist/apps/cli/src/version.d.ts +2 -0
  54. package/dist/apps/cli/src/version.js +2 -0
  55. package/dist/packages/config/src/index.d.ts +3 -0
  56. package/dist/packages/config/src/index.js +3 -0
  57. package/dist/packages/config/src/load.d.ts +3 -0
  58. package/dist/packages/config/src/load.js +80 -0
  59. package/dist/packages/config/src/schema.d.ts +49 -0
  60. package/dist/packages/config/src/schema.js +28 -0
  61. package/dist/packages/connectors/aws/src/cost-explorer.d.ts +34 -0
  62. package/dist/packages/connectors/aws/src/cost-explorer.js +43 -0
  63. package/dist/packages/connectors/aws/src/index.d.ts +35 -0
  64. package/dist/packages/connectors/aws/src/index.js +67 -0
  65. package/dist/packages/connectors/aws/src/normalize.d.ts +69 -0
  66. package/dist/packages/connectors/aws/src/normalize.js +141 -0
  67. package/dist/packages/connectors/aws/src/sdk-client.d.ts +6 -0
  68. package/dist/packages/connectors/aws/src/sdk-client.js +21 -0
  69. package/dist/packages/connectors/cloudflare/src/client.d.ts +23 -0
  70. package/dist/packages/connectors/cloudflare/src/client.js +107 -0
  71. package/dist/packages/connectors/cloudflare/src/index.d.ts +33 -0
  72. package/dist/packages/connectors/cloudflare/src/index.js +81 -0
  73. package/dist/packages/connectors/cloudflare/src/normalize.d.ts +113 -0
  74. package/dist/packages/connectors/cloudflare/src/normalize.js +288 -0
  75. package/dist/packages/connectors/mock/src/index.d.ts +58 -0
  76. package/dist/packages/connectors/mock/src/index.js +66 -0
  77. package/dist/packages/connectors/openai/src/index.d.ts +55 -0
  78. package/dist/packages/connectors/openai/src/index.js +169 -0
  79. package/dist/packages/connectors/openai/src/normalize.d.ts +91 -0
  80. package/dist/packages/connectors/openai/src/normalize.js +180 -0
  81. package/dist/packages/connectors/supabase/src/client.d.ts +22 -0
  82. package/dist/packages/connectors/supabase/src/client.js +132 -0
  83. package/dist/packages/connectors/supabase/src/index.d.ts +33 -0
  84. package/dist/packages/connectors/supabase/src/index.js +87 -0
  85. package/dist/packages/connectors/supabase/src/normalize.d.ts +106 -0
  86. package/dist/packages/connectors/supabase/src/normalize.js +266 -0
  87. package/dist/packages/core/src/collector.d.ts +12 -0
  88. package/dist/packages/core/src/collector.js +68 -0
  89. package/dist/packages/core/src/index.d.ts +5 -0
  90. package/dist/packages/core/src/index.js +4 -0
  91. package/dist/packages/core/src/provider.d.ts +18 -0
  92. package/dist/packages/core/src/provider.js +2 -0
  93. package/dist/packages/core/src/risk-engine.d.ts +9 -0
  94. package/dist/packages/core/src/risk-engine.js +4 -0
  95. package/dist/packages/core/src/snapshots.d.ts +49 -0
  96. package/dist/packages/core/src/snapshots.js +9 -0
  97. package/dist/packages/db/src/client.d.ts +11 -0
  98. package/dist/packages/db/src/client.js +14 -0
  99. package/dist/packages/db/src/index.d.ts +6 -0
  100. package/dist/packages/db/src/index.js +6 -0
  101. package/dist/packages/db/src/local-store.d.ts +161 -0
  102. package/dist/packages/db/src/local-store.js +623 -0
  103. package/dist/packages/db/src/migrate.d.ts +17 -0
  104. package/dist/packages/db/src/migrate.js +35 -0
  105. package/dist/packages/db/src/schema.d.ts +5 -0
  106. package/dist/packages/db/src/schema.js +120 -0
  107. package/dist/packages/db/src/sqlite-bin.d.ts +3 -0
  108. package/dist/packages/db/src/sqlite-bin.js +16 -0
  109. package/dist/packages/local-api/src/index.d.ts +2 -0
  110. package/dist/packages/local-api/src/index.js +2 -0
  111. package/dist/packages/local-api/src/server.d.ts +36 -0
  112. package/dist/packages/local-api/src/server.js +310 -0
  113. package/dist/packages/report/src/daily.d.ts +24 -0
  114. package/dist/packages/report/src/daily.js +9 -0
  115. package/dist/packages/report/src/index.d.ts +4 -0
  116. package/dist/packages/report/src/index.js +4 -0
  117. package/dist/packages/report/src/korean.d.ts +3 -0
  118. package/dist/packages/report/src/korean.js +62 -0
  119. package/dist/packages/report/src/slack.d.ts +34 -0
  120. package/dist/packages/report/src/slack.js +134 -0
  121. package/dist/packages/runtime/src/index.d.ts +2 -0
  122. package/dist/packages/runtime/src/index.js +2 -0
  123. package/dist/packages/runtime/src/runtime.d.ts +26 -0
  124. package/dist/packages/runtime/src/runtime.js +182 -0
  125. package/dist/packages/view-model/src/hud-model.d.ts +74 -0
  126. package/dist/packages/view-model/src/hud-model.js +295 -0
  127. package/dist/packages/view-model/src/index.d.ts +6 -0
  128. package/dist/packages/view-model/src/index.js +6 -0
  129. package/dist/packages/view-model/src/notification-preferences-model.d.ts +75 -0
  130. package/dist/packages/view-model/src/notification-preferences-model.js +400 -0
  131. package/dist/packages/view-model/src/notification-preferences.d.ts +6 -0
  132. package/dist/packages/view-model/src/notification-preferences.js +36 -0
  133. package/dist/packages/view-model/src/sync-state.d.ts +47 -0
  134. package/dist/packages/view-model/src/sync-state.js +140 -0
  135. package/dist/packages/view-model/src/usage-progress.d.ts +22 -0
  136. package/dist/packages/view-model/src/usage-progress.js +57 -0
  137. package/dist/packages/view-model/src/view-model.d.ts +215 -0
  138. package/dist/packages/view-model/src/view-model.js +826 -0
  139. package/package.json +40 -0
  140. package/scripts/postinstall.mjs +69 -0
@@ -0,0 +1,393 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { isAbsolute, join } from "node:path";
3
+ import { collectProviderSnapshots } from "../../../../packages/core/src/index.js";
4
+ import { createAwsCostExplorerConnector, createAwsSdkCostExplorerClient, createStaticCostExplorerClient, } from "../../../../packages/connectors/aws/src/index.js";
5
+ import { createMockProviderConnector } from "../../../../packages/connectors/mock/src/index.js";
6
+ import { createOpenAiUsageCostsClient, createOpenAiUsageCostsConnector, createStaticOpenAiUsageCostsClient, } from "../../../../packages/connectors/openai/src/index.js";
7
+ import { createSupabaseManagementClient, createStaticSupabaseUsageHealthClient, createSupabaseUsageHealthConnector, } from "../../../../packages/connectors/supabase/src/index.js";
8
+ import { createCloudflareBillingUsageClient, createCloudflareBillingUsageConnector, createStaticCloudflareBillingUsageClient, } from "../../../../packages/connectors/cloudflare/src/index.js";
9
+ import { initializeLocalStore, saveLocalProviderCollection } from "../../../../packages/db/src/index.js";
10
+ import { loadCliConfig, readFlag, resolveDbPath } from "./shared.js";
11
+ const AWS_COST_EXPLORER_FIXTURE_ENV_KEY = "MONEYSIREN_AWS_COST_EXPLORER_FIXTURE";
12
+ const AWS_REGION_ENV_KEY = "MONEYSIREN_AWS_REGION";
13
+ const OPENAI_USAGE_FIXTURE_ENV_KEY = "MONEYSIREN_OPENAI_USAGE_FIXTURE";
14
+ const OPENAI_COSTS_FIXTURE_ENV_KEY = "MONEYSIREN_OPENAI_COSTS_FIXTURE";
15
+ const SUPABASE_FIXTURE_ENV_KEY = "MONEYSIREN_SUPABASE_FIXTURE";
16
+ const CLOUDFLARE_FIXTURE_ENV_KEY = "MONEYSIREN_CLOUDFLARE_FIXTURE";
17
+ const CLOUDFLARE_ACCOUNT_IDS_ENV_KEY = "CLOUDFLARE_ACCOUNT_IDS";
18
+ const SYNC_USAGE = "Usage: moneysiren sync --provider <mock|aws|openai|supabase|cloudflare> [--profile <aws-profile>]";
19
+ export async function runSyncCommand(args, context) {
20
+ const providerFlag = readFlag(args, "--provider");
21
+ const awsProfileOption = readAwsProfileOption(providerFlag.remainingArgs);
22
+ if (awsProfileOption.error !== undefined ||
23
+ awsProfileOption.remainingArgs.length > 0 ||
24
+ providerFlag.value === undefined ||
25
+ !isSupportedSyncProvider(providerFlag.value)) {
26
+ context.stderr(awsProfileOption.error ?? SYNC_USAGE);
27
+ return 1;
28
+ }
29
+ if (providerFlag.value !== "aws" && awsProfileOption.profile !== undefined) {
30
+ context.stderr("--profile is only supported for AWS sync.");
31
+ return 1;
32
+ }
33
+ const config = loadCliConfig(context.env);
34
+ if (providerFlag.value === "aws") {
35
+ const awsEnv = applyAwsProfileOption(context.env, awsProfileOption.profile);
36
+ const fixturePath = readConfiguredEnvValue(awsEnv[AWS_COST_EXPLORER_FIXTURE_ENV_KEY]);
37
+ if (fixturePath !== undefined) {
38
+ return syncAwsProvider(context, config.dbPath, createStaticCostExplorerClient(await loadAwsFixture(context.cwd, fixturePath)));
39
+ }
40
+ if (!isAwsLiveConfigured(awsEnv) && context.liveClients?.awsCostExplorer === undefined) {
41
+ context.stderr(`AWS sync requires AWS_PROFILE, --profile <profile>, AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY, or ` +
42
+ `${AWS_COST_EXPLORER_FIXTURE_ENV_KEY}. If you ran aws sso login --profile <profile>, pass ` +
43
+ `--profile <profile> or set AWS_PROFILE in this shell.`);
44
+ return 1;
45
+ }
46
+ return withAwsProfileOnProcessEnv(awsProfileOption.profile, () => syncAwsProvider(context, config.dbPath, context.liveClients?.awsCostExplorer ?? createAwsSdkCostExplorerClient(awsSdkOptionsFromEnv(awsEnv))));
47
+ }
48
+ if (providerFlag.value === "openai") {
49
+ const usageFixturePath = readConfiguredEnvValue(context.env[OPENAI_USAGE_FIXTURE_ENV_KEY]);
50
+ const costsFixturePath = readConfiguredEnvValue(context.env[OPENAI_COSTS_FIXTURE_ENV_KEY]);
51
+ if (usageFixturePath !== undefined || costsFixturePath !== undefined) {
52
+ if (usageFixturePath === undefined || costsFixturePath === undefined) {
53
+ context.stderr(`OpenAI fixture sync requires both ${OPENAI_USAGE_FIXTURE_ENV_KEY} and ${OPENAI_COSTS_FIXTURE_ENV_KEY}.`);
54
+ return 1;
55
+ }
56
+ return syncOpenAiProvider(context, config.dbPath, createStaticOpenAiUsageCostsClient(await loadOpenAiUsageCostsFixture(context.cwd, usageFixturePath, costsFixturePath)));
57
+ }
58
+ if (!config.providers.openai.configured && context.liveClients?.openaiUsageCosts === undefined) {
59
+ context.stderr(`OpenAI sync requires OPENAI_ADMIN_KEY or fixture mode with ${OPENAI_USAGE_FIXTURE_ENV_KEY} ` +
60
+ `and ${OPENAI_COSTS_FIXTURE_ENV_KEY}.`);
61
+ return 1;
62
+ }
63
+ return syncOpenAiProvider(context, config.dbPath, context.liveClients?.openaiUsageCosts ?? createOpenAiUsageCostsClient({
64
+ adminKey: requireConfiguredEnvValue(context.env.OPENAI_ADMIN_KEY, "OPENAI_ADMIN_KEY"),
65
+ }));
66
+ }
67
+ if (providerFlag.value === "supabase") {
68
+ const fixturePath = readConfiguredEnvValue(context.env[SUPABASE_FIXTURE_ENV_KEY]);
69
+ if (fixturePath !== undefined) {
70
+ return syncSupabaseProvider(context, config.dbPath, createStaticSupabaseUsageHealthClient(await loadSupabaseFixture(context.cwd, fixturePath)));
71
+ }
72
+ if (!config.providers.supabase.configured && context.liveClients?.supabaseUsageHealth === undefined) {
73
+ context.stderr(`Supabase sync requires SUPABASE_ACCESS_TOKEN or ${SUPABASE_FIXTURE_ENV_KEY}. ` +
74
+ `Set ${SUPABASE_FIXTURE_ENV_KEY} for fixture mode.`);
75
+ return 1;
76
+ }
77
+ return syncSupabaseProvider(context, config.dbPath, context.liveClients?.supabaseUsageHealth ?? createSupabaseManagementClient({
78
+ accessToken: requireConfiguredEnvValue(context.env.SUPABASE_ACCESS_TOKEN, "SUPABASE_ACCESS_TOKEN"),
79
+ }));
80
+ }
81
+ if (providerFlag.value === "cloudflare") {
82
+ const fixturePath = readConfiguredEnvValue(context.env[CLOUDFLARE_FIXTURE_ENV_KEY]);
83
+ if (fixturePath !== undefined) {
84
+ return syncCloudflareProvider(context, config.dbPath, createStaticCloudflareBillingUsageClient(await loadCloudflareFixture(context.cwd, fixturePath)));
85
+ }
86
+ if (!config.providers.cloudflare.configured && context.liveClients?.cloudflareBillingUsage === undefined) {
87
+ context.stderr(`Cloudflare sync requires CLOUDFLARE_API_TOKEN and ${CLOUDFLARE_ACCOUNT_IDS_ENV_KEY}, or ${CLOUDFLARE_FIXTURE_ENV_KEY}. ` +
88
+ `Set ${CLOUDFLARE_FIXTURE_ENV_KEY} for fixture mode.`);
89
+ return 1;
90
+ }
91
+ return syncCloudflareProvider(context, config.dbPath, context.liveClients?.cloudflareBillingUsage ?? createCloudflareBillingUsageClient({
92
+ apiToken: requireConfiguredEnvValue(context.env.CLOUDFLARE_API_TOKEN, "CLOUDFLARE_API_TOKEN"),
93
+ accountIds: readCloudflareAccountIds(context.env[CLOUDFLARE_ACCOUNT_IDS_ENV_KEY]),
94
+ }));
95
+ }
96
+ return syncMockProvider(context, config.dbPath);
97
+ }
98
+ function isSupportedSyncProvider(provider) {
99
+ return provider === "mock" || provider === "aws" || provider === "openai" || provider === "supabase" ||
100
+ provider === "cloudflare";
101
+ }
102
+ async function syncMockProvider(context, configuredDbPath) {
103
+ const dbPath = resolveDbPath(context.cwd, configuredDbPath);
104
+ await initializeLocalStore({ dbPath });
105
+ const connector = createMockProviderConnector();
106
+ const collection = await collectProviderSnapshots(connector, {
107
+ now: context.now,
108
+ });
109
+ await saveLocalProviderCollection({
110
+ dbPath,
111
+ provider: {
112
+ key: connector.kind,
113
+ displayName: connector.displayName,
114
+ connectorVersion: "0.1.0-alpha.0",
115
+ },
116
+ collectedAt: collection.collectedAt,
117
+ status: collection.status,
118
+ snapshots: collection.snapshots,
119
+ alerts: collection.alerts,
120
+ });
121
+ context.stdout([
122
+ "Synced mock provider snapshots:",
123
+ `usage=${collection.snapshots.usage.length}`,
124
+ `billing=${collection.snapshots.billing.length}`,
125
+ `health=${collection.snapshots.serviceHealth.length}`,
126
+ `estimates=${collection.snapshots.costEstimates.length}`,
127
+ `alerts=${collection.alerts.length}`,
128
+ ].join(" "));
129
+ return 0;
130
+ }
131
+ async function syncAwsProvider(context, configuredDbPath, costExplorerClient) {
132
+ const dbPath = resolveDbPath(context.cwd, configuredDbPath);
133
+ await initializeLocalStore({ dbPath });
134
+ const connector = createAwsCostExplorerConnector({
135
+ costExplorerClient,
136
+ });
137
+ const collection = await collectProviderSnapshots(connector, {
138
+ now: context.now,
139
+ });
140
+ await saveLocalProviderCollection({
141
+ dbPath,
142
+ provider: {
143
+ key: connector.kind,
144
+ displayName: connector.displayName,
145
+ connectorVersion: "0.1.0-alpha.0",
146
+ },
147
+ collectedAt: collection.collectedAt,
148
+ status: collection.status,
149
+ snapshots: collection.snapshots,
150
+ alerts: collection.alerts,
151
+ });
152
+ context.stdout([
153
+ "Synced AWS Cost Explorer snapshots:",
154
+ `usage=${collection.snapshots.usage.length}`,
155
+ `billing=${collection.snapshots.billing.length}`,
156
+ `health=${collection.snapshots.serviceHealth.length}`,
157
+ `estimates=${collection.snapshots.costEstimates.length}`,
158
+ `alerts=${collection.alerts.length}`,
159
+ ].join(" "));
160
+ if (collection.status === "error") {
161
+ context.stderr(collection.errors?.[0] ?? collection.alerts[0]?.message ?? "AWS Cost Explorer sync failed.");
162
+ return 1;
163
+ }
164
+ return 0;
165
+ }
166
+ async function loadAwsFixture(cwd, fixturePath) {
167
+ const resolvedPath = isAbsolute(fixturePath) ? fixturePath : join(cwd, fixturePath);
168
+ return JSON.parse(await readFile(resolvedPath, "utf8"));
169
+ }
170
+ async function syncOpenAiProvider(context, configuredDbPath, client) {
171
+ const dbPath = resolveDbPath(context.cwd, configuredDbPath);
172
+ await initializeLocalStore({ dbPath });
173
+ const connector = createOpenAiUsageCostsConnector({
174
+ client,
175
+ });
176
+ const collection = await collectProviderSnapshots(connector, {
177
+ now: context.now,
178
+ });
179
+ await saveLocalProviderCollection({
180
+ dbPath,
181
+ provider: {
182
+ key: connector.kind,
183
+ displayName: connector.displayName,
184
+ connectorVersion: "0.1.0-alpha.0",
185
+ },
186
+ collectedAt: collection.collectedAt,
187
+ status: collection.status,
188
+ snapshots: collection.snapshots,
189
+ alerts: collection.alerts,
190
+ });
191
+ context.stdout([
192
+ "Synced OpenAI usage and costs snapshots:",
193
+ `usage=${collection.snapshots.usage.length}`,
194
+ `billing=${collection.snapshots.billing.length}`,
195
+ `health=${collection.snapshots.serviceHealth.length}`,
196
+ `estimates=${collection.snapshots.costEstimates.length}`,
197
+ `alerts=${collection.alerts.length}`,
198
+ ].join(" "));
199
+ return 0;
200
+ }
201
+ async function loadOpenAiUsageCostsFixture(cwd, usageFixturePath, costsFixturePath) {
202
+ const [usage, costs] = await Promise.all([
203
+ loadOpenAiFixtureSection(cwd, usageFixturePath, "usage"),
204
+ loadOpenAiFixtureSection(cwd, costsFixturePath, "costs"),
205
+ ]);
206
+ return {
207
+ usage,
208
+ costs,
209
+ };
210
+ }
211
+ async function loadOpenAiFixtureSection(cwd, fixturePath, section) {
212
+ const resolvedPath = isAbsolute(fixturePath) ? fixturePath : join(cwd, fixturePath);
213
+ const parsed = JSON.parse(await readFile(resolvedPath, "utf8"));
214
+ if (isRecord(parsed) && section in parsed) {
215
+ return parsed[section];
216
+ }
217
+ return parsed;
218
+ }
219
+ async function syncSupabaseProvider(context, configuredDbPath, client) {
220
+ const dbPath = resolveDbPath(context.cwd, configuredDbPath);
221
+ await initializeLocalStore({ dbPath });
222
+ const connector = createSupabaseUsageHealthConnector({
223
+ client,
224
+ });
225
+ const collection = await collectProviderSnapshots(connector, {
226
+ now: context.now,
227
+ });
228
+ await saveLocalProviderCollection({
229
+ dbPath,
230
+ provider: {
231
+ key: connector.kind,
232
+ displayName: connector.displayName,
233
+ connectorVersion: "0.1.0-alpha.0",
234
+ },
235
+ collectedAt: collection.collectedAt,
236
+ status: collection.status,
237
+ snapshots: collection.snapshots,
238
+ alerts: collection.alerts,
239
+ });
240
+ context.stdout([
241
+ "Synced Supabase usage and health snapshots:",
242
+ `usage=${collection.snapshots.usage.length}`,
243
+ `billing=${collection.snapshots.billing.length}`,
244
+ `health=${collection.snapshots.serviceHealth.length}`,
245
+ `estimates=${collection.snapshots.costEstimates.length}`,
246
+ `alerts=${collection.alerts.length}`,
247
+ ].join(" "));
248
+ return 0;
249
+ }
250
+ async function loadSupabaseFixture(cwd, fixturePath) {
251
+ const resolvedPath = isAbsolute(fixturePath) ? fixturePath : join(cwd, fixturePath);
252
+ return JSON.parse(await readFile(resolvedPath, "utf8"));
253
+ }
254
+ async function syncCloudflareProvider(context, configuredDbPath, client) {
255
+ const dbPath = resolveDbPath(context.cwd, configuredDbPath);
256
+ await initializeLocalStore({ dbPath });
257
+ const connector = createCloudflareBillingUsageConnector({
258
+ client,
259
+ });
260
+ const collection = await collectProviderSnapshots(connector, {
261
+ now: context.now,
262
+ });
263
+ await saveLocalProviderCollection({
264
+ dbPath,
265
+ provider: {
266
+ key: connector.kind,
267
+ displayName: connector.displayName,
268
+ connectorVersion: "0.1.0-alpha.0",
269
+ },
270
+ collectedAt: collection.collectedAt,
271
+ status: collection.status,
272
+ snapshots: collection.snapshots,
273
+ alerts: collection.alerts,
274
+ });
275
+ context.stdout([
276
+ "Synced Cloudflare billing and usage snapshots:",
277
+ `usage=${collection.snapshots.usage.length}`,
278
+ `billing=${collection.snapshots.billing.length}`,
279
+ `health=${collection.snapshots.serviceHealth.length}`,
280
+ `estimates=${collection.snapshots.costEstimates.length}`,
281
+ `alerts=${collection.alerts.length}`,
282
+ ].join(" "));
283
+ return 0;
284
+ }
285
+ async function loadCloudflareFixture(cwd, fixturePath) {
286
+ const resolvedPath = isAbsolute(fixturePath) ? fixturePath : join(cwd, fixturePath);
287
+ return JSON.parse(await readFile(resolvedPath, "utf8"));
288
+ }
289
+ function readConfiguredEnvValue(value) {
290
+ const trimmed = value?.trim();
291
+ return trimmed === undefined || trimmed.length === 0 ? undefined : trimmed;
292
+ }
293
+ function requireConfiguredEnvValue(value, envKey) {
294
+ const trimmed = readConfiguredEnvValue(value);
295
+ if (trimmed === undefined) {
296
+ throw new Error(`${envKey} must be configured for live sync.`);
297
+ }
298
+ return trimmed;
299
+ }
300
+ function isAwsLiveConfigured(env) {
301
+ return readConfiguredEnvValue(env.AWS_PROFILE) !== undefined ||
302
+ (readConfiguredEnvValue(env.AWS_ACCESS_KEY_ID) !== undefined &&
303
+ readConfiguredEnvValue(env.AWS_SECRET_ACCESS_KEY) !== undefined);
304
+ }
305
+ function awsSdkOptionsFromEnv(env) {
306
+ const region = readConfiguredEnvValue(env[AWS_REGION_ENV_KEY]);
307
+ return region === undefined ? {} : { region };
308
+ }
309
+ function readAwsProfileOption(args) {
310
+ const remainingArgs = [];
311
+ let profile;
312
+ for (let index = 0; index < args.length; index += 1) {
313
+ const arg = args[index];
314
+ if (arg === "--profile" || arg === "--aws-profile") {
315
+ const value = args[index + 1];
316
+ const parsed = readConfiguredEnvValue(value);
317
+ if (parsed === undefined || value?.startsWith("--") === true) {
318
+ return {
319
+ remainingArgs,
320
+ error: `${arg} requires a profile name.`,
321
+ };
322
+ }
323
+ if (profile !== undefined && profile !== parsed) {
324
+ return {
325
+ remainingArgs,
326
+ error: "AWS profile was provided more than once with different values.",
327
+ };
328
+ }
329
+ profile = parsed;
330
+ index += 1;
331
+ continue;
332
+ }
333
+ if (arg?.startsWith("--profile=") || arg?.startsWith("--aws-profile=")) {
334
+ const [flagName, ...valueParts] = arg.split("=");
335
+ const parsed = readConfiguredEnvValue(valueParts.join("="));
336
+ if (parsed === undefined) {
337
+ return {
338
+ remainingArgs,
339
+ error: `${flagName} requires a profile name.`,
340
+ };
341
+ }
342
+ if (profile !== undefined && profile !== parsed) {
343
+ return {
344
+ remainingArgs,
345
+ error: "AWS profile was provided more than once with different values.",
346
+ };
347
+ }
348
+ profile = parsed;
349
+ continue;
350
+ }
351
+ if (arg !== undefined) {
352
+ remainingArgs.push(arg);
353
+ }
354
+ }
355
+ return profile === undefined ? { remainingArgs } : { profile, remainingArgs };
356
+ }
357
+ function applyAwsProfileOption(env, profile) {
358
+ if (profile === undefined) {
359
+ return env;
360
+ }
361
+ return {
362
+ ...env,
363
+ AWS_PROFILE: profile,
364
+ };
365
+ }
366
+ async function withAwsProfileOnProcessEnv(profile, run) {
367
+ if (profile === undefined) {
368
+ return run();
369
+ }
370
+ const previousProfile = process.env.AWS_PROFILE;
371
+ process.env.AWS_PROFILE = profile;
372
+ try {
373
+ return await run();
374
+ }
375
+ finally {
376
+ if (previousProfile === undefined) {
377
+ delete process.env.AWS_PROFILE;
378
+ }
379
+ else {
380
+ process.env.AWS_PROFILE = previousProfile;
381
+ }
382
+ }
383
+ }
384
+ function readCloudflareAccountIds(value) {
385
+ return (value ?? "")
386
+ .split(",")
387
+ .map((accountId) => accountId.trim())
388
+ .filter((accountId) => accountId.length > 0);
389
+ }
390
+ function isRecord(value) {
391
+ return typeof value === "object" && value !== null;
392
+ }
393
+ //# sourceMappingURL=sync.js.map
@@ -0,0 +1,3 @@
1
+ import type { CliExecutionContext } from "../cli.js";
2
+ export declare function runThemeCommand(args: readonly string[], context: CliExecutionContext): Promise<number>;
3
+ //# sourceMappingURL=theme.d.ts.map
@@ -0,0 +1,181 @@
1
+ import { Buffer } from "node:buffer";
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { dirname, isAbsolute, join } from "node:path";
4
+ const THEME_USAGE = "Usage: moneysiren theme <preview|image-prompt|image-generate> [--out <png> --theme-out <json> --model <model>]";
5
+ const DEFAULT_IMAGE_MODEL = "gpt-image-1.5";
6
+ const DEFAULT_IMAGE_OUTPUT = ".moneysiren/themes/cli-image-reference.png";
7
+ const DEFAULT_THEME_OUTPUT = ".moneysiren/themes/cli-theme.json";
8
+ const IMAGE_PROMPT = `Create a polished Image 2 reference for the MoneySiren CLI theme.
9
+
10
+ Product:
11
+ - MoneySiren is a local-first cloud/SaaS usage, status, and expected billing dashboard for individual developers and small teams.
12
+ - CLI surfaces should feel operational, calm, high-trust, and dense enough for repeated terminal use.
13
+
14
+ Image direction:
15
+ - Show a modern terminal window with a MoneySiren slash-command home screen, provider status rows, and small usage/risk indicators.
16
+ - Use a restrained professional palette with teal as the brand accent, graphite text, warm amber warnings, and clear green success states.
17
+ - Avoid marketing hero art, decorative blobs, oversized typography, and purple/blue gradient dominance.
18
+ - Make spacing comfortable enough that controls do not touch, overlap, or wrap awkwardly.
19
+
20
+ Return a visual reference plus this theme JSON shape:
21
+ {
22
+ "version": 1,
23
+ "source": "image2-dashboard",
24
+ "ansi": {
25
+ "brand": "1;38;5;30",
26
+ "heading": "1;38;5;231",
27
+ "command": "38;5;35",
28
+ "muted": "38;5;244",
29
+ "warning": "38;5;214"
30
+ }
31
+ }
32
+
33
+ Apply the JSON locally with MONEYSIREN_CLI_THEME_FILE=<path-to-json>.`;
34
+ export async function runThemeCommand(args, context) {
35
+ const [subcommand, ...rest] = args;
36
+ if (subcommand === undefined) {
37
+ context.stderr(THEME_USAGE);
38
+ return 1;
39
+ }
40
+ if (subcommand === "preview") {
41
+ if (rest.length > 0) {
42
+ context.stderr(THEME_USAGE);
43
+ return 1;
44
+ }
45
+ context.stdout(renderThemePreview(context));
46
+ return 0;
47
+ }
48
+ if (subcommand === "image-prompt") {
49
+ if (rest.length > 0) {
50
+ context.stderr(THEME_USAGE);
51
+ return 1;
52
+ }
53
+ context.stdout(IMAGE_PROMPT);
54
+ return 0;
55
+ }
56
+ if (subcommand === "image-generate") {
57
+ return runImageGenerateCommand(rest, context);
58
+ }
59
+ context.stderr(THEME_USAGE);
60
+ return 1;
61
+ }
62
+ function renderThemePreview(context) {
63
+ const { theme } = context;
64
+ return [
65
+ `${theme.brand("MoneySiren")} CLI theme`,
66
+ `Source: ${theme.source}`,
67
+ "",
68
+ `${theme.heading("Heading")} ${theme.command("/sync openai")} ${theme.muted("muted metadata")} ${theme.warning("warning")}`,
69
+ "",
70
+ "Set MONEYSIREN_CLI_THEME=image2-dashboard for the bundled image-reference palette.",
71
+ "Set MONEYSIREN_CLI_THEME_FILE=<json> to apply a palette extracted from an image reference.",
72
+ ].join("\n");
73
+ }
74
+ async function runImageGenerateCommand(args, context) {
75
+ const options = parseImageGenerateOptions(args, context.env);
76
+ const apiKey = context.env.OPENAI_API_KEY?.trim();
77
+ if (apiKey === undefined || apiKey.length === 0) {
78
+ context.stderr("OpenAI image generation requires OPENAI_API_KEY. The key is only read from env and is not persisted.");
79
+ return 1;
80
+ }
81
+ const response = await context.fetch("https://api.openai.com/v1/images/generations", {
82
+ method: "POST",
83
+ headers: {
84
+ authorization: `Bearer ${apiKey}`,
85
+ "content-type": "application/json",
86
+ },
87
+ body: JSON.stringify({
88
+ model: options.model,
89
+ prompt: IMAGE_PROMPT,
90
+ n: 1,
91
+ size: "1536x1024",
92
+ quality: "low",
93
+ }),
94
+ });
95
+ if (!response.ok) {
96
+ context.stderr(`OpenAI image generation failed with status ${response.status}.`);
97
+ return 1;
98
+ }
99
+ const payload = await response.json();
100
+ const imageBase64 = payload.data?.[0]?.b64_json;
101
+ if (typeof imageBase64 !== "string" || imageBase64.trim().length === 0) {
102
+ context.stderr("OpenAI image generation response did not include image data.");
103
+ return 1;
104
+ }
105
+ const imagePath = resolveCliPath(context.cwd, options.out);
106
+ const themePath = resolveCliPath(context.cwd, options.themeOut);
107
+ await writeLocalFile(imagePath, Buffer.from(imageBase64, "base64"));
108
+ await writeLocalFile(themePath, Buffer.from(JSON.stringify(themeFileFor(options.model), null, 2), "utf8"));
109
+ context.stdout("Generated MoneySiren CLI image theme reference.");
110
+ context.stdout(`Image: ${options.out}`);
111
+ context.stdout(`Theme: ${options.themeOut}`);
112
+ context.stdout(`Apply: MONEYSIREN_CLI_THEME_FILE=${options.themeOut} moneysiren theme preview`);
113
+ return 0;
114
+ }
115
+ function parseImageGenerateOptions(args, env) {
116
+ let out = DEFAULT_IMAGE_OUTPUT;
117
+ let themeOut = DEFAULT_THEME_OUTPUT;
118
+ let model = env.MONEYSIREN_IMAGE_MODEL?.trim() || DEFAULT_IMAGE_MODEL;
119
+ for (let index = 0; index < args.length; index += 1) {
120
+ const arg = args[index];
121
+ const value = args[index + 1];
122
+ if (arg === "--out" && value !== undefined) {
123
+ out = value;
124
+ index += 1;
125
+ continue;
126
+ }
127
+ if (arg === "--theme-out" && value !== undefined) {
128
+ themeOut = value;
129
+ index += 1;
130
+ continue;
131
+ }
132
+ if (arg === "--model" && value !== undefined) {
133
+ model = value;
134
+ index += 1;
135
+ continue;
136
+ }
137
+ throw new Error(THEME_USAGE);
138
+ }
139
+ return {
140
+ out: requireNonBlankPath(out, "--out"),
141
+ themeOut: requireNonBlankPath(themeOut, "--theme-out"),
142
+ model: requireSafeModelName(model),
143
+ };
144
+ }
145
+ function themeFileFor(model) {
146
+ return {
147
+ version: 1,
148
+ source: `image-generation:${model}`,
149
+ ansi: {
150
+ brand: "1;38;5;30",
151
+ heading: "1;38;5;231",
152
+ command: "38;5;35",
153
+ muted: "38;5;244",
154
+ warning: "38;5;214",
155
+ },
156
+ };
157
+ }
158
+ async function writeLocalFile(path, content) {
159
+ await mkdir(dirname(path), {
160
+ recursive: true,
161
+ });
162
+ await writeFile(path, content);
163
+ }
164
+ function resolveCliPath(cwd, path) {
165
+ return isAbsolute(path) ? path : join(cwd, path);
166
+ }
167
+ function requireNonBlankPath(value, label) {
168
+ const trimmed = value.trim();
169
+ if (trimmed.length === 0) {
170
+ throw new Error(`${label} must not be blank.`);
171
+ }
172
+ return trimmed;
173
+ }
174
+ function requireSafeModelName(value) {
175
+ const trimmed = value.trim();
176
+ if (!/^[A-Za-z0-9._-]{1,80}$/.test(trimmed)) {
177
+ throw new Error("Image model name is invalid.");
178
+ }
179
+ return trimmed;
180
+ }
181
+ //# sourceMappingURL=theme.js.map
@@ -0,0 +1,54 @@
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 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
+ }
30
+ export type DesktopRuntimeResult = {
31
+ status: "running" | "started";
32
+ dashboardUrl: string;
33
+ pid?: number;
34
+ notes: readonly string[];
35
+ } | DesktopRuntimeUnavailableResult;
36
+ export type DesktopShellResult = {
37
+ status: "opened" | "started";
38
+ executablePath: string;
39
+ pid?: number;
40
+ notes: readonly string[];
41
+ } | DesktopRuntimeUnavailableResult;
42
+ export interface DesktopRuntimeUnavailableResult {
43
+ status: "unavailable";
44
+ reason: string;
45
+ guidance: readonly string[];
46
+ }
47
+ export interface CliDesktopRuntimeAdapter {
48
+ startWebRuntime(options: StartWebRuntimeOptions): Promise<DesktopRuntimeResult>;
49
+ startHud(options: StartHudOptions): Promise<DesktopShellResult>;
50
+ status(): Promise<DesktopRuntimeStatus>;
51
+ stop(options: StopDesktopRuntimeOptions): Promise<readonly StopDesktopRuntimeResult[]>;
52
+ }
53
+ export declare function createFallbackDesktopRuntimeAdapter(context: CliExecutionContext): CliDesktopRuntimeAdapter;
54
+ //# sourceMappingURL=desktop-runtime.d.ts.map