@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,430 @@
1
+ import { stat } from "node:fs/promises";
2
+ import { sendSlackReport } from "../../../../packages/report/src/index.js";
3
+ import { NOTIFICATION_WIDGET_KEYS, readNotificationPreferencesFile, resolveNotificationPreferencesPath, writeNotificationPreferencesFile, } from "../../../../packages/view-model/src/index.js";
4
+ import { readSanitizedNotificationDigest } from "../summary-model.js";
5
+ import { loadCliConfig } from "./shared.js";
6
+ const NOTIFY_USAGE = [
7
+ "Usage:",
8
+ " moneysiren notify once --dry-run",
9
+ " moneysiren notify prefs list",
10
+ " moneysiren notify prefs enable <widget>",
11
+ " moneysiren notify prefs disable <widget>",
12
+ " moneysiren notify prefs hud-enable <widget>",
13
+ " moneysiren notify prefs hud-disable <widget>",
14
+ " moneysiren notify prefs threshold <widget> --gte|--lte|--eq <value> --cooldown <minutes>",
15
+ " moneysiren notify prefs quiet-hours <start> <end>",
16
+ " moneysiren notify test",
17
+ ].join("\n");
18
+ const NOTIFY_PREFS_USAGE = [
19
+ "Usage:",
20
+ " moneysiren notify prefs list",
21
+ " moneysiren notify prefs enable <widget>",
22
+ " moneysiren notify prefs disable <widget>",
23
+ " moneysiren notify prefs hud-enable <widget>",
24
+ " moneysiren notify prefs hud-disable <widget>",
25
+ " moneysiren notify prefs threshold <widget> --gte|--lte|--eq <value> --cooldown <minutes>",
26
+ " moneysiren notify prefs quiet-hours <start> <end>",
27
+ ].join("\n");
28
+ export async function runNotifyCommand(args, context) {
29
+ const [subcommand, ...rest] = args;
30
+ if (subcommand === "--help" || subcommand === "-h" || subcommand === "help") {
31
+ context.stdout(NOTIFY_USAGE);
32
+ return 0;
33
+ }
34
+ if (subcommand === "once") {
35
+ return runNotifyOnce(rest, context);
36
+ }
37
+ if (subcommand === "prefs") {
38
+ return runNotifyPrefs(rest, context);
39
+ }
40
+ if (subcommand === "test") {
41
+ return runNotifyTest(rest, context);
42
+ }
43
+ context.stderr(NOTIFY_USAGE);
44
+ return 1;
45
+ }
46
+ async function runNotifyOnce(args, context) {
47
+ if (args.length !== 1 || args[0] !== "--dry-run") {
48
+ context.stderr("Usage: moneysiren notify once --dry-run");
49
+ return 1;
50
+ }
51
+ const digest = await readSanitizedNotificationDigest(context);
52
+ context.stdout("MoneySiren notification dry run");
53
+ context.stdout(`Generated at: ${digest.generatedAt}`);
54
+ context.stdout(`Secrets returned: ${digest.secretsReturned}`);
55
+ context.stdout(`Providers: ${digest.providerCount}`);
56
+ context.stdout(`Health: ${digest.health}`);
57
+ context.stdout(`Alerts: ${digest.alertCount}`);
58
+ context.stdout(`Critical alerts: ${digest.criticalAlertCount}`);
59
+ if (digest.estimatedAmountMinorByCurrency.length === 0) {
60
+ context.stdout("Estimated totals: none");
61
+ return 0;
62
+ }
63
+ for (const total of digest.estimatedAmountMinorByCurrency) {
64
+ context.stdout(`Estimated total ${total.currency}: ${formatMinorAmount(total.amountMinor)}`);
65
+ }
66
+ return 0;
67
+ }
68
+ async function runNotifyPrefs(args, context) {
69
+ const [subcommand, ...rest] = args;
70
+ if (subcommand === "list" && rest.length === 0) {
71
+ return listNotificationPreferences(context);
72
+ }
73
+ if (subcommand === "enable" && rest.length === 1) {
74
+ return enableNotificationWidget(rest[0], context);
75
+ }
76
+ if (subcommand === "disable" && rest.length === 1) {
77
+ return disableNotificationWidget(rest[0], context);
78
+ }
79
+ if (subcommand === "hud-enable" && rest.length === 1) {
80
+ return enableHudWidget(rest[0], context);
81
+ }
82
+ if (subcommand === "hud-disable" && rest.length === 1) {
83
+ return disableHudWidget(rest[0], context);
84
+ }
85
+ if (subcommand === "threshold") {
86
+ return setNotificationThreshold(rest, context);
87
+ }
88
+ if (subcommand === "quiet-hours" && rest.length === 2) {
89
+ return setNotificationQuietHours(rest[0], rest[1], context);
90
+ }
91
+ context.stderr(NOTIFY_PREFS_USAGE);
92
+ return 1;
93
+ }
94
+ async function listNotificationPreferences(context) {
95
+ const source = await notificationPreferencesSource(context);
96
+ const preferences = await readPreferences(context);
97
+ const selectedWidgets = new Set(preferences.selectedWidgets);
98
+ const hudWidgets = new Set(preferences.hud.selectedWidgets);
99
+ context.stdout("MoneySiren notification preferences");
100
+ context.stdout(`Source: ${source}`);
101
+ context.stdout("Secrets returned: false");
102
+ context.stdout(`Notifications: ${enabledLabel(preferences.enabled)}`);
103
+ context.stdout(`Digest: ${enabledLabel(preferences.digestEnabled)} (${preferences.digestInterval})`);
104
+ context.stdout(`Desktop notifications: ${enabledLabel(preferences.desktopEnabled)}`);
105
+ context.stdout(`HUD font size: ${Math.round(preferences.hud.fontScale * 100)}%`);
106
+ context.stdout(`HUD opacity: ${Math.round(preferences.hud.opacity * 100)}%`);
107
+ context.stdout(`Quiet hours: ${preferences.quietHours.start}-${preferences.quietHours.end}`);
108
+ context.stdout("Widgets:");
109
+ for (const widgetKey of NOTIFICATION_WIDGET_KEYS) {
110
+ context.stdout(`- ${widgetKey}: ${selectedWidgets.has(widgetKey) ? "enabled" : "disabled"}`);
111
+ }
112
+ context.stdout("HUD widgets:");
113
+ for (const widgetKey of NOTIFICATION_WIDGET_KEYS) {
114
+ context.stdout(`- ${widgetKey}: ${hudWidgets.has(widgetKey) ? "enabled" : "disabled"}`);
115
+ }
116
+ if (preferences.thresholdRules.length === 0) {
117
+ context.stdout("Thresholds: none configured");
118
+ return 0;
119
+ }
120
+ context.stdout("Thresholds:");
121
+ for (const rule of orderThresholdRules(preferences.thresholdRules)) {
122
+ context.stdout(`- ${rule.widgetKey}: ${rule.operator} ${formatRuleValue(rule.value)} cooldown ${rule.cooldownMinutes}m`);
123
+ }
124
+ return 0;
125
+ }
126
+ async function enableNotificationWidget(widget, context) {
127
+ const widgetKey = parseWidgetKey(widget);
128
+ if (widgetKey === undefined) {
129
+ context.stderr("Unknown notification widget.");
130
+ return 1;
131
+ }
132
+ const preferences = await readPreferences(context);
133
+ const selectedWidgets = new Set(preferences.selectedWidgets);
134
+ selectedWidgets.add(widgetKey);
135
+ await writePreferences({
136
+ ...preferences,
137
+ selectedWidgets: orderWidgetKeys([...selectedWidgets]),
138
+ }, context);
139
+ context.stdout(`Notification widget enabled: ${widgetKey}`);
140
+ context.stdout("Secrets returned: false");
141
+ return 0;
142
+ }
143
+ async function disableNotificationWidget(widget, context) {
144
+ const widgetKey = parseWidgetKey(widget);
145
+ if (widgetKey === undefined) {
146
+ context.stderr("Unknown notification widget.");
147
+ return 1;
148
+ }
149
+ const preferences = await readPreferences(context);
150
+ const selectedWidgets = new Set(preferences.selectedWidgets);
151
+ if (!selectedWidgets.has(widgetKey)) {
152
+ context.stdout(`Notification widget already disabled: ${widgetKey}`);
153
+ context.stdout("Secrets returned: false");
154
+ return 0;
155
+ }
156
+ selectedWidgets.delete(widgetKey);
157
+ if (selectedWidgets.size === 0) {
158
+ context.stderr("At least one notification widget must remain enabled.");
159
+ return 1;
160
+ }
161
+ await writePreferences({
162
+ ...preferences,
163
+ selectedWidgets: orderWidgetKeys([...selectedWidgets]),
164
+ }, context);
165
+ context.stdout(`Notification widget disabled: ${widgetKey}`);
166
+ context.stdout("Secrets returned: false");
167
+ return 0;
168
+ }
169
+ async function enableHudWidget(widget, context) {
170
+ const widgetKey = parseWidgetKey(widget);
171
+ if (widgetKey === undefined) {
172
+ context.stderr("Unknown notification widget.");
173
+ return 1;
174
+ }
175
+ const preferences = await readPreferences(context);
176
+ const selectedWidgets = new Set(preferences.hud.selectedWidgets);
177
+ selectedWidgets.add(widgetKey);
178
+ await writePreferences({
179
+ ...preferences,
180
+ hud: {
181
+ ...preferences.hud,
182
+ selectedWidgets: orderWidgetKeys([...selectedWidgets]),
183
+ },
184
+ }, context);
185
+ context.stdout(`HUD widget enabled: ${widgetKey}`);
186
+ context.stdout("Secrets returned: false");
187
+ return 0;
188
+ }
189
+ async function disableHudWidget(widget, context) {
190
+ const widgetKey = parseWidgetKey(widget);
191
+ if (widgetKey === undefined) {
192
+ context.stderr("Unknown notification widget.");
193
+ return 1;
194
+ }
195
+ const preferences = await readPreferences(context);
196
+ const selectedWidgets = new Set(preferences.hud.selectedWidgets);
197
+ if (!selectedWidgets.has(widgetKey)) {
198
+ context.stdout(`HUD widget already disabled: ${widgetKey}`);
199
+ context.stdout("Secrets returned: false");
200
+ return 0;
201
+ }
202
+ selectedWidgets.delete(widgetKey);
203
+ if (selectedWidgets.size === 0) {
204
+ context.stderr("At least one HUD widget must remain enabled.");
205
+ return 1;
206
+ }
207
+ await writePreferences({
208
+ ...preferences,
209
+ hud: {
210
+ ...preferences.hud,
211
+ selectedWidgets: orderWidgetKeys([...selectedWidgets]),
212
+ },
213
+ }, context);
214
+ context.stdout(`HUD widget disabled: ${widgetKey}`);
215
+ context.stdout("Secrets returned: false");
216
+ return 0;
217
+ }
218
+ async function setNotificationThreshold(args, context) {
219
+ const [widget, ...flagArgs] = args;
220
+ const widgetKey = parseWidgetKey(widget);
221
+ const parsedRule = parseThresholdRuleArgs(widgetKey, flagArgs);
222
+ if (widgetKey === undefined) {
223
+ context.stderr("Unknown notification widget.");
224
+ return 1;
225
+ }
226
+ if (parsedRule === undefined) {
227
+ context.stderr("Usage: moneysiren notify prefs threshold <widget> --gte|--lte|--eq <value> --cooldown <minutes>");
228
+ return 1;
229
+ }
230
+ const preferences = await readPreferences(context);
231
+ await writePreferences({
232
+ ...preferences,
233
+ thresholdRules: orderThresholdRules([
234
+ ...preferences.thresholdRules.filter((rule) => rule.widgetKey !== widgetKey),
235
+ parsedRule,
236
+ ]),
237
+ }, context);
238
+ context.stdout(`Notification threshold set: ${parsedRule.widgetKey} ${parsedRule.operator} ${formatRuleValue(parsedRule.value)} cooldown ${parsedRule.cooldownMinutes}m`);
239
+ context.stdout("Secrets returned: false");
240
+ return 0;
241
+ }
242
+ async function setNotificationQuietHours(start, end, context) {
243
+ if (!isValidClockTime(start) || !isValidClockTime(end)) {
244
+ context.stderr("Usage: moneysiren notify prefs quiet-hours <HH:MM> <HH:MM>");
245
+ return 1;
246
+ }
247
+ const preferences = await readPreferences(context);
248
+ await writePreferences({
249
+ ...preferences,
250
+ quietHours: {
251
+ start,
252
+ end,
253
+ },
254
+ }, context);
255
+ context.stdout(`Notification quiet hours set: ${start}-${end}`);
256
+ context.stdout("Secrets returned: false");
257
+ return 0;
258
+ }
259
+ async function runNotifyTest(args, context) {
260
+ if (args.length !== 0) {
261
+ context.stderr("Usage: moneysiren notify test");
262
+ return 1;
263
+ }
264
+ const config = loadCliConfig(context.env);
265
+ const webhookEnvKey = config.slack.requiredEnvKey;
266
+ const webhookUrl = context.env[webhookEnvKey]?.trim();
267
+ if (webhookUrl === undefined || webhookUrl.length === 0) {
268
+ context.stderr(`${webhookEnvKey} is required for test notification.`);
269
+ return 1;
270
+ }
271
+ const text = [
272
+ "MoneySiren test notification",
273
+ `Generated at: ${context.now().toISOString()}`,
274
+ "Secrets returned: false",
275
+ ].join("\n");
276
+ try {
277
+ const options = context.slackTransport === undefined
278
+ ? {
279
+ webhookUrl,
280
+ text,
281
+ }
282
+ : {
283
+ webhookUrl,
284
+ text,
285
+ transport: context.slackTransport,
286
+ };
287
+ await sendSlackReport(options);
288
+ context.stdout("MoneySiren test notification sent");
289
+ context.stdout("Secrets returned: false");
290
+ return 0;
291
+ }
292
+ catch (error) {
293
+ context.stderr(error instanceof Error ? error.message : String(error));
294
+ return 1;
295
+ }
296
+ }
297
+ function formatMinorAmount(amountMinor) {
298
+ return (amountMinor / 100).toFixed(2);
299
+ }
300
+ async function readPreferences(context) {
301
+ return readNotificationPreferencesFile({
302
+ cwd: context.cwd,
303
+ env: context.env,
304
+ });
305
+ }
306
+ async function writePreferences(preferences, context) {
307
+ return writeNotificationPreferencesFile(preferences, {
308
+ cwd: context.cwd,
309
+ env: context.env,
310
+ });
311
+ }
312
+ async function notificationPreferencesSource(context) {
313
+ const path = resolveNotificationPreferencesPath({
314
+ cwd: context.cwd,
315
+ env: context.env,
316
+ });
317
+ try {
318
+ await stat(path);
319
+ return "local preference file";
320
+ }
321
+ catch {
322
+ return "default preference template";
323
+ }
324
+ }
325
+ function parseWidgetKey(value) {
326
+ return value !== undefined && NOTIFICATION_WIDGET_KEYS.includes(value)
327
+ ? value
328
+ : undefined;
329
+ }
330
+ function parseThresholdRuleArgs(widgetKey, args) {
331
+ if (widgetKey === undefined) {
332
+ return undefined;
333
+ }
334
+ let operator;
335
+ let value;
336
+ let cooldownMinutes;
337
+ for (let index = 0; index < args.length; index += 1) {
338
+ const arg = args[index];
339
+ if (arg === undefined) {
340
+ return undefined;
341
+ }
342
+ const operatorName = parseOperatorFlag(arg);
343
+ if (operatorName !== undefined) {
344
+ if (operator !== undefined) {
345
+ return undefined;
346
+ }
347
+ const inlineValue = inlineFlagValue(arg);
348
+ const rawValue = inlineValue ?? args[index + 1];
349
+ value = parseNonNegativeNumber(rawValue);
350
+ operator = operatorName;
351
+ if (inlineValue === undefined) {
352
+ index += 1;
353
+ }
354
+ continue;
355
+ }
356
+ if (arg === "--cooldown" || arg.startsWith("--cooldown=")) {
357
+ const inlineValue = inlineFlagValue(arg);
358
+ const rawValue = inlineValue ?? args[index + 1];
359
+ cooldownMinutes = parseNonNegativeInteger(rawValue);
360
+ if (inlineValue === undefined) {
361
+ index += 1;
362
+ }
363
+ continue;
364
+ }
365
+ return undefined;
366
+ }
367
+ if (operator === undefined || value === undefined || cooldownMinutes === undefined) {
368
+ return undefined;
369
+ }
370
+ return {
371
+ widgetKey,
372
+ operator,
373
+ value,
374
+ cooldownMinutes,
375
+ };
376
+ }
377
+ function parseOperatorFlag(value) {
378
+ if (value === "--gte" || value.startsWith("--gte=")) {
379
+ return "gte";
380
+ }
381
+ if (value === "--lte" || value.startsWith("--lte=")) {
382
+ return "lte";
383
+ }
384
+ if (value === "--eq" || value.startsWith("--eq=")) {
385
+ return "eq";
386
+ }
387
+ return undefined;
388
+ }
389
+ function inlineFlagValue(value) {
390
+ const separatorIndex = value.indexOf("=");
391
+ return separatorIndex === -1 ? undefined : value.slice(separatorIndex + 1);
392
+ }
393
+ function parseNonNegativeNumber(value) {
394
+ if (value === undefined || value.trim().length === 0) {
395
+ return undefined;
396
+ }
397
+ const parsed = Number(value);
398
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : undefined;
399
+ }
400
+ function parseNonNegativeInteger(value) {
401
+ const parsed = parseNonNegativeNumber(value);
402
+ return parsed !== undefined && Number.isInteger(parsed) ? parsed : undefined;
403
+ }
404
+ function orderWidgetKeys(widgetKeys) {
405
+ const selected = new Set(widgetKeys);
406
+ return NOTIFICATION_WIDGET_KEYS.filter((widgetKey) => selected.has(widgetKey));
407
+ }
408
+ function orderThresholdRules(rules) {
409
+ const byWidgetKey = new Map(rules.map((rule) => [rule.widgetKey, rule]));
410
+ return NOTIFICATION_WIDGET_KEYS.flatMap((widgetKey) => {
411
+ const rule = byWidgetKey.get(widgetKey);
412
+ return rule === undefined ? [] : [rule];
413
+ });
414
+ }
415
+ function enabledLabel(value) {
416
+ return value ? "enabled" : "disabled";
417
+ }
418
+ function formatRuleValue(value) {
419
+ return String(value);
420
+ }
421
+ function isValidClockTime(value) {
422
+ if (value === undefined || !/^\d{2}:\d{2}$/.test(value)) {
423
+ return false;
424
+ }
425
+ const [hourText, minuteText] = value.split(":");
426
+ const hour = Number(hourText);
427
+ const minute = Number(minuteText);
428
+ return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59;
429
+ }
430
+ //# sourceMappingURL=notify.js.map
@@ -0,0 +1,3 @@
1
+ import type { CliExecutionContext } from "../cli.js";
2
+ export declare function runReportCommand(args: readonly string[], context: CliExecutionContext): Promise<number>;
3
+ //# sourceMappingURL=report.d.ts.map
@@ -0,0 +1,206 @@
1
+ import { initializeLocalStore, readLocalStore, recordLocalReportRun, } from "../../../../packages/db/src/index.js";
2
+ import { renderDailyReport, sendSlackReport, } from "../../../../packages/report/src/index.js";
3
+ import { loadCliConfig, resolveDbPath } from "./shared.js";
4
+ const DAILY_REPORT_TIME_ZONE_ENV_KEY = "MONEYSIREN_REPORT_TIME_ZONE";
5
+ const DEFAULT_DAILY_REPORT_TIME_ZONE = "Asia/Seoul";
6
+ export async function runReportCommand(args, context) {
7
+ const parsedArgs = parseDailyReportArgs(args);
8
+ if (parsedArgs === undefined) {
9
+ context.stderr("Usage: moneysiren report daily --lang ko [--send slack]");
10
+ return 1;
11
+ }
12
+ const config = loadCliConfig(context.env);
13
+ const dbPath = resolveDbPath(context.cwd, config.dbPath);
14
+ await initializeLocalStore({ dbPath });
15
+ const now = context.now();
16
+ const generatedAt = now.toISOString();
17
+ const reportDate = formatDailyReportDate(now, resolveDailyReportTimeZone(context.env));
18
+ const store = await readLocalStore({ dbPath });
19
+ const reportInput = {
20
+ reportDate,
21
+ generatedAt,
22
+ providerSummaries: buildProviderSummaries(store),
23
+ reportRunStatus: parsedArgs.deliveryTarget === "slack" ? "sent" : "rendered",
24
+ };
25
+ if (parsedArgs.deliveryTarget === "slack") {
26
+ return sendDailyReportToSlack({
27
+ context,
28
+ dbPath,
29
+ reportDate,
30
+ generatedAt,
31
+ reportInput,
32
+ webhookEnvKey: config.slack.requiredEnvKey,
33
+ });
34
+ }
35
+ await recordLocalReportRun({
36
+ dbPath,
37
+ createdAt: generatedAt,
38
+ reportDate,
39
+ language: "ko",
40
+ deliveryTarget: "stdout",
41
+ status: "rendered",
42
+ });
43
+ context.stdout(renderDailyReport(reportInput, { lang: "ko" }));
44
+ context.stdout("Report run recorded: stdout");
45
+ return 0;
46
+ }
47
+ function parseDailyReportArgs(args) {
48
+ const [reportKind, ...rest] = args;
49
+ if (reportKind !== "daily") {
50
+ return undefined;
51
+ }
52
+ let lang;
53
+ let deliveryTarget = "stdout";
54
+ for (let index = 0; index < rest.length; index += 1) {
55
+ const arg = rest[index];
56
+ if (arg === "--lang") {
57
+ const value = rest[index + 1];
58
+ if (value === undefined) {
59
+ return undefined;
60
+ }
61
+ lang = value;
62
+ index += 1;
63
+ continue;
64
+ }
65
+ if (arg?.startsWith("--lang=")) {
66
+ lang = arg.slice("--lang=".length);
67
+ continue;
68
+ }
69
+ if (arg === "--send") {
70
+ const value = rest[index + 1];
71
+ if (value !== "slack") {
72
+ return undefined;
73
+ }
74
+ deliveryTarget = "slack";
75
+ index += 1;
76
+ continue;
77
+ }
78
+ if (arg?.startsWith("--send=")) {
79
+ const value = arg.slice("--send=".length);
80
+ if (value !== "slack") {
81
+ return undefined;
82
+ }
83
+ deliveryTarget = "slack";
84
+ continue;
85
+ }
86
+ return undefined;
87
+ }
88
+ if (lang !== "ko") {
89
+ return undefined;
90
+ }
91
+ return {
92
+ deliveryTarget,
93
+ };
94
+ }
95
+ function resolveDailyReportTimeZone(env) {
96
+ const configuredTimeZone = env[DAILY_REPORT_TIME_ZONE_ENV_KEY]?.trim();
97
+ return configuredTimeZone === undefined || configuredTimeZone.length === 0
98
+ ? DEFAULT_DAILY_REPORT_TIME_ZONE
99
+ : configuredTimeZone;
100
+ }
101
+ function formatDailyReportDate(date, timeZone) {
102
+ let parts;
103
+ try {
104
+ parts = new Intl.DateTimeFormat("en-US", {
105
+ timeZone,
106
+ year: "numeric",
107
+ month: "2-digit",
108
+ day: "2-digit",
109
+ }).formatToParts(date);
110
+ }
111
+ catch (error) {
112
+ if (error instanceof RangeError) {
113
+ throw new Error(`${DAILY_REPORT_TIME_ZONE_ENV_KEY} must be a valid IANA time zone.`);
114
+ }
115
+ throw error;
116
+ }
117
+ const year = parts.find((part) => part.type === "year")?.value;
118
+ const month = parts.find((part) => part.type === "month")?.value;
119
+ const day = parts.find((part) => part.type === "day")?.value;
120
+ if (year === undefined || month === undefined || day === undefined) {
121
+ throw new Error(`Could not format daily report date for ${timeZone}.`);
122
+ }
123
+ return `${year}-${month}-${day}`;
124
+ }
125
+ async function sendDailyReportToSlack(input) {
126
+ const webhookUrl = input.context.env[input.webhookEnvKey]?.trim();
127
+ if (webhookUrl === undefined || webhookUrl.length === 0) {
128
+ await recordSlackReportRun(input, "error");
129
+ input.context.stderr(`${input.webhookEnvKey} is required for Slack delivery.`);
130
+ return 1;
131
+ }
132
+ const text = renderDailyReport(input.reportInput, { lang: "ko" });
133
+ try {
134
+ const options = input.context.slackTransport === undefined
135
+ ? {
136
+ webhookUrl,
137
+ text,
138
+ }
139
+ : {
140
+ webhookUrl,
141
+ text,
142
+ transport: input.context.slackTransport,
143
+ };
144
+ await sendSlackReport(options);
145
+ await recordSlackReportRun(input, "sent");
146
+ input.context.stdout("Slack report sent: slack");
147
+ return 0;
148
+ }
149
+ catch (error) {
150
+ await recordSlackReportRun(input, "error");
151
+ input.context.stderr(error instanceof Error ? error.message : String(error));
152
+ return 1;
153
+ }
154
+ }
155
+ async function recordSlackReportRun(input, status) {
156
+ await recordLocalReportRun({
157
+ dbPath: input.dbPath,
158
+ createdAt: input.generatedAt,
159
+ reportDate: input.reportDate,
160
+ language: "ko",
161
+ deliveryTarget: "slack",
162
+ status,
163
+ });
164
+ }
165
+ function buildProviderSummaries(store) {
166
+ return store.providers.map((provider) => {
167
+ const usageSnapshots = store.usageSnapshots.filter((snapshot) => snapshot.providerKey === provider.key);
168
+ const billingSnapshots = store.billingSnapshots.filter((snapshot) => snapshot.providerKey === provider.key);
169
+ const serviceHealthSnapshots = store.serviceHealthSnapshots.filter((snapshot) => snapshot.providerKey === provider.key);
170
+ const costEstimates = store.costEstimates.filter((snapshot) => snapshot.providerKey === provider.key);
171
+ const alerts = store.alerts.filter((alert) => alert.providerKey === provider.key);
172
+ const currency = costEstimates[0]?.currency ?? billingSnapshots[0]?.currency ?? "USD";
173
+ return {
174
+ provider: provider.key,
175
+ displayName: provider.displayName,
176
+ syncStatus: summarizeSyncStatus([
177
+ usageSnapshots.length,
178
+ billingSnapshots.length,
179
+ serviceHealthSnapshots.length,
180
+ costEstimates.length,
181
+ ]),
182
+ usageSnapshotCount: usageSnapshots.length,
183
+ billingSnapshotCount: billingSnapshots.length,
184
+ healthStatus: summarizeHealth(serviceHealthSnapshots.map((snapshot) => snapshot.status)),
185
+ estimatedAmountMinor: costEstimates.reduce((total, snapshot) => total + snapshot.estimatedAmountMinor, 0),
186
+ currency,
187
+ alertCount: alerts.length,
188
+ };
189
+ });
190
+ }
191
+ function summarizeSyncStatus(snapshotCounts) {
192
+ return snapshotCounts.some((count) => count > 0) ? "ok" : "error";
193
+ }
194
+ function summarizeHealth(statuses) {
195
+ if (statuses.includes("down")) {
196
+ return "down";
197
+ }
198
+ if (statuses.includes("degraded")) {
199
+ return "degraded";
200
+ }
201
+ if (statuses.includes("unknown") || statuses.length === 0) {
202
+ return "unknown";
203
+ }
204
+ return "ok";
205
+ }
206
+ //# sourceMappingURL=report.js.map
@@ -0,0 +1,10 @@
1
+ import type { CliExecutionContext } from "../cli.js";
2
+ export declare function runServeCommand(args: readonly string[], context: CliExecutionContext): Promise<number>;
3
+ export declare function runStatusCommand(args: readonly string[], context: CliExecutionContext): Promise<number>;
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>;
7
+ export declare function runHudCommand(args: readonly string[], context: CliExecutionContext): Promise<number>;
8
+ export declare function runOpenCommand(args: readonly string[], context: CliExecutionContext): Promise<number>;
9
+ export declare function runDesktopCommand(args: readonly string[], context: CliExecutionContext): Promise<number>;
10
+ //# sourceMappingURL=runtime.d.ts.map