@moneysiren/cli 0.1.0-alpha.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +155 -0
- package/dist/apps/cli/src/cli.d.ts +56 -0
- package/dist/apps/cli/src/cli.js +182 -0
- package/dist/apps/cli/src/commands/dashboard.d.ts +3 -0
- package/dist/apps/cli/src/commands/dashboard.js +239 -0
- package/dist/apps/cli/src/commands/doctor.d.ts +3 -0
- package/dist/apps/cli/src/commands/doctor.js +25 -0
- package/dist/apps/cli/src/commands/init.d.ts +3 -0
- package/dist/apps/cli/src/commands/init.js +18 -0
- package/dist/apps/cli/src/commands/install.d.ts +3 -0
- package/dist/apps/cli/src/commands/install.js +116 -0
- package/dist/apps/cli/src/commands/modes.d.ts +3 -0
- package/dist/apps/cli/src/commands/modes.js +65 -0
- package/dist/apps/cli/src/commands/notify.d.ts +3 -0
- package/dist/apps/cli/src/commands/notify.js +430 -0
- package/dist/apps/cli/src/commands/report.d.ts +3 -0
- package/dist/apps/cli/src/commands/report.js +206 -0
- package/dist/apps/cli/src/commands/runtime.d.ts +5 -0
- package/dist/apps/cli/src/commands/runtime.js +133 -0
- package/dist/apps/cli/src/commands/shared.d.ts +9 -0
- package/dist/apps/cli/src/commands/shared.js +29 -0
- package/dist/apps/cli/src/commands/summary.d.ts +3 -0
- package/dist/apps/cli/src/commands/summary.js +15 -0
- package/dist/apps/cli/src/commands/sync.d.ts +3 -0
- package/dist/apps/cli/src/commands/sync.js +393 -0
- package/dist/apps/cli/src/commands/theme.d.ts +3 -0
- package/dist/apps/cli/src/commands/theme.js +181 -0
- package/dist/apps/cli/src/home.d.ts +7 -0
- package/dist/apps/cli/src/home.js +97 -0
- package/dist/apps/cli/src/index.d.ts +3 -0
- package/dist/apps/cli/src/index.js +14 -0
- package/dist/apps/cli/src/install-profile.d.ts +35 -0
- package/dist/apps/cli/src/install-profile.js +124 -0
- package/dist/apps/cli/src/install-selector.d.ts +10 -0
- package/dist/apps/cli/src/install-selector.js +66 -0
- package/dist/apps/cli/src/interactive.d.ts +3 -0
- package/dist/apps/cli/src/interactive.js +32 -0
- package/dist/apps/cli/src/postinstall.d.ts +3 -0
- package/dist/apps/cli/src/postinstall.js +42 -0
- package/dist/apps/cli/src/runtime-adapter.d.ts +24 -0
- package/dist/apps/cli/src/runtime-adapter.js +185 -0
- package/dist/apps/cli/src/slash.d.ts +15 -0
- package/dist/apps/cli/src/slash.js +202 -0
- package/dist/apps/cli/src/summary-model.d.ts +51 -0
- package/dist/apps/cli/src/summary-model.js +136 -0
- package/dist/apps/cli/src/theme.d.ts +18 -0
- package/dist/apps/cli/src/theme.js +118 -0
- package/dist/packages/config/src/index.d.ts +3 -0
- package/dist/packages/config/src/index.js +3 -0
- package/dist/packages/config/src/load.d.ts +3 -0
- package/dist/packages/config/src/load.js +77 -0
- package/dist/packages/config/src/schema.d.ts +46 -0
- package/dist/packages/config/src/schema.js +25 -0
- package/dist/packages/connectors/aws/src/cost-explorer.d.ts +34 -0
- package/dist/packages/connectors/aws/src/cost-explorer.js +43 -0
- package/dist/packages/connectors/aws/src/index.d.ts +35 -0
- package/dist/packages/connectors/aws/src/index.js +67 -0
- package/dist/packages/connectors/aws/src/normalize.d.ts +69 -0
- package/dist/packages/connectors/aws/src/normalize.js +141 -0
- package/dist/packages/connectors/aws/src/sdk-client.d.ts +6 -0
- package/dist/packages/connectors/aws/src/sdk-client.js +21 -0
- package/dist/packages/connectors/cloudflare/src/client.d.ts +23 -0
- package/dist/packages/connectors/cloudflare/src/client.js +107 -0
- package/dist/packages/connectors/cloudflare/src/index.d.ts +33 -0
- package/dist/packages/connectors/cloudflare/src/index.js +81 -0
- package/dist/packages/connectors/cloudflare/src/normalize.d.ts +113 -0
- package/dist/packages/connectors/cloudflare/src/normalize.js +288 -0
- package/dist/packages/connectors/mock/src/index.d.ts +58 -0
- package/dist/packages/connectors/mock/src/index.js +66 -0
- package/dist/packages/connectors/openai/src/index.d.ts +55 -0
- package/dist/packages/connectors/openai/src/index.js +169 -0
- package/dist/packages/connectors/openai/src/normalize.d.ts +91 -0
- package/dist/packages/connectors/openai/src/normalize.js +180 -0
- package/dist/packages/connectors/supabase/src/client.d.ts +22 -0
- package/dist/packages/connectors/supabase/src/client.js +132 -0
- package/dist/packages/connectors/supabase/src/index.d.ts +33 -0
- package/dist/packages/connectors/supabase/src/index.js +87 -0
- package/dist/packages/connectors/supabase/src/normalize.d.ts +106 -0
- package/dist/packages/connectors/supabase/src/normalize.js +266 -0
- package/dist/packages/core/src/collector.d.ts +12 -0
- package/dist/packages/core/src/collector.js +68 -0
- package/dist/packages/core/src/index.d.ts +5 -0
- package/dist/packages/core/src/index.js +4 -0
- package/dist/packages/core/src/provider.d.ts +18 -0
- package/dist/packages/core/src/provider.js +2 -0
- package/dist/packages/core/src/risk-engine.d.ts +9 -0
- package/dist/packages/core/src/risk-engine.js +4 -0
- package/dist/packages/core/src/snapshots.d.ts +49 -0
- package/dist/packages/core/src/snapshots.js +9 -0
- package/dist/packages/db/src/client.d.ts +11 -0
- package/dist/packages/db/src/client.js +14 -0
- package/dist/packages/db/src/index.d.ts +6 -0
- package/dist/packages/db/src/index.js +6 -0
- package/dist/packages/db/src/local-store.d.ts +161 -0
- package/dist/packages/db/src/local-store.js +623 -0
- package/dist/packages/db/src/migrate.d.ts +17 -0
- package/dist/packages/db/src/migrate.js +35 -0
- package/dist/packages/db/src/schema.d.ts +5 -0
- package/dist/packages/db/src/schema.js +120 -0
- package/dist/packages/db/src/sqlite-bin.d.ts +3 -0
- package/dist/packages/db/src/sqlite-bin.js +16 -0
- package/dist/packages/local-api/src/index.d.ts +2 -0
- package/dist/packages/local-api/src/index.js +2 -0
- package/dist/packages/local-api/src/server.d.ts +36 -0
- package/dist/packages/local-api/src/server.js +310 -0
- package/dist/packages/report/src/daily.d.ts +24 -0
- package/dist/packages/report/src/daily.js +9 -0
- package/dist/packages/report/src/index.d.ts +4 -0
- package/dist/packages/report/src/index.js +4 -0
- package/dist/packages/report/src/korean.d.ts +3 -0
- package/dist/packages/report/src/korean.js +62 -0
- package/dist/packages/report/src/slack.d.ts +34 -0
- package/dist/packages/report/src/slack.js +134 -0
- package/dist/packages/runtime/src/index.d.ts +2 -0
- package/dist/packages/runtime/src/index.js +2 -0
- package/dist/packages/runtime/src/runtime.d.ts +26 -0
- package/dist/packages/runtime/src/runtime.js +182 -0
- package/dist/packages/view-model/src/index.d.ts +3 -0
- package/dist/packages/view-model/src/index.js +3 -0
- package/dist/packages/view-model/src/notification-preferences-model.d.ts +47 -0
- package/dist/packages/view-model/src/notification-preferences-model.js +218 -0
- package/dist/packages/view-model/src/notification-preferences.d.ts +6 -0
- package/dist/packages/view-model/src/notification-preferences.js +36 -0
- package/dist/packages/view-model/src/view-model.d.ts +193 -0
- package/dist/packages/view-model/src/view-model.js +684 -0
- package/package.json +49 -0
- package/scripts/postinstall.mjs +11 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { initializeLocalStore } from "../../../../packages/db/src/index.js";
|
|
2
|
+
import { loadCliConfig, resolveDbPath } from "./shared.js";
|
|
3
|
+
export async function runInitCommand(args, context) {
|
|
4
|
+
if (args.length > 0) {
|
|
5
|
+
context.stderr("Usage: moneysiren init");
|
|
6
|
+
return 1;
|
|
7
|
+
}
|
|
8
|
+
const config = loadCliConfig(context.env);
|
|
9
|
+
const dbPath = resolveDbPath(context.cwd, config.dbPath);
|
|
10
|
+
const result = await initializeLocalStore({ dbPath });
|
|
11
|
+
const migrationSummary = result.appliedMigrationIds.length > 0
|
|
12
|
+
? `applied migrations: ${result.appliedMigrationIds.join(", ")}`
|
|
13
|
+
: `migrations already applied: ${result.skippedMigrationIds.join(", ")}`;
|
|
14
|
+
context.stdout(`Initialized MoneySiren local storage at ${config.dbPath}`);
|
|
15
|
+
context.stdout(migrationSummary);
|
|
16
|
+
return 0;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=init.js.map
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { DEFAULT_INSTALL_SURFACES, INSTALL_SURFACES, isInstallSurface, readInstallProfileFile, resolveInstallProfilePath, writeInstallProfileFile, } from "../install-profile.js";
|
|
2
|
+
import { formatInstallSelectionLine, installSelectionHelp, parseInstallSurfaceSelection, promptForInstallSurfaces, } from "../install-selector.js";
|
|
3
|
+
const INSTALL_USAGE = [
|
|
4
|
+
"Usage: moneysiren install [--status|--all|--cli|--web|--hud|--no-cli|--no-web|--no-hud]",
|
|
5
|
+
"",
|
|
6
|
+
"Components:",
|
|
7
|
+
installSelectionHelp(),
|
|
8
|
+
"",
|
|
9
|
+
"Default: all components selected (recommended).",
|
|
10
|
+
].join("\n");
|
|
11
|
+
export async function runInstallCommand(args, context) {
|
|
12
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
13
|
+
context.stdout(INSTALL_USAGE);
|
|
14
|
+
return 0;
|
|
15
|
+
}
|
|
16
|
+
if (args.length === 1 && args[0] === "--status") {
|
|
17
|
+
return writeInstallStatus(context);
|
|
18
|
+
}
|
|
19
|
+
const parsed = parseInstallArgs(args);
|
|
20
|
+
if (parsed === null) {
|
|
21
|
+
context.stderr(INSTALL_USAGE);
|
|
22
|
+
return 1;
|
|
23
|
+
}
|
|
24
|
+
const selectedSurfaces = parsed ?? await selectedSurfacesFromPromptOrDefault(context);
|
|
25
|
+
const profile = await writeInstallProfileFile({
|
|
26
|
+
selectedSurfaces,
|
|
27
|
+
source: "cli",
|
|
28
|
+
recommendedDefault: isDefaultSelection(selectedSurfaces),
|
|
29
|
+
}, {
|
|
30
|
+
env: context.env,
|
|
31
|
+
now: context.now,
|
|
32
|
+
});
|
|
33
|
+
context.stdout("MoneySiren install profile updated.");
|
|
34
|
+
context.stdout(formatInstallSelectionLine(profile.selectedSurfaces));
|
|
35
|
+
context.stdout("Secrets returned: false");
|
|
36
|
+
return 0;
|
|
37
|
+
}
|
|
38
|
+
async function writeInstallStatus(context) {
|
|
39
|
+
const profile = await readInstallProfileFile({
|
|
40
|
+
env: context.env,
|
|
41
|
+
});
|
|
42
|
+
const selectedSurfaces = profile?.selectedSurfaces ?? DEFAULT_INSTALL_SURFACES;
|
|
43
|
+
context.stdout("MoneySiren install profile");
|
|
44
|
+
context.stdout(`Status: ${profile === null ? "not configured; using recommended default" : "configured"}`);
|
|
45
|
+
context.stdout(formatInstallSelectionLine(selectedSurfaces));
|
|
46
|
+
context.stdout(`Recommended default: ${isDefaultSelection(selectedSurfaces) ? "yes" : "no"}`);
|
|
47
|
+
context.stdout(`Profile path: ${resolveInstallProfilePath({ env: context.env })}`);
|
|
48
|
+
context.stdout("Secrets returned: false");
|
|
49
|
+
return 0;
|
|
50
|
+
}
|
|
51
|
+
async function selectedSurfacesFromPromptOrDefault(context) {
|
|
52
|
+
if (!context.interactive || context.stdin === undefined || context.output === undefined) {
|
|
53
|
+
return DEFAULT_INSTALL_SURFACES;
|
|
54
|
+
}
|
|
55
|
+
return promptForInstallSurfaces({
|
|
56
|
+
stdin: context.stdin,
|
|
57
|
+
output: context.output,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function parseInstallArgs(args) {
|
|
61
|
+
if (args.length === 0) {
|
|
62
|
+
return undefined;
|
|
63
|
+
}
|
|
64
|
+
if (args.length === 1) {
|
|
65
|
+
const selected = parseInstallSurfaceSelection(args[0] ?? "");
|
|
66
|
+
if (selected !== null) {
|
|
67
|
+
return selected;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
let explicitIncludes = false;
|
|
71
|
+
const selected = new Set();
|
|
72
|
+
for (const arg of args) {
|
|
73
|
+
if (arg === "--all") {
|
|
74
|
+
for (const surface of DEFAULT_INSTALL_SURFACES) {
|
|
75
|
+
selected.add(surface);
|
|
76
|
+
}
|
|
77
|
+
explicitIncludes = true;
|
|
78
|
+
continue;
|
|
79
|
+
}
|
|
80
|
+
if (arg.startsWith("--no-")) {
|
|
81
|
+
const surface = arg.slice("--no-".length);
|
|
82
|
+
if (!isInstallSurface(surface)) {
|
|
83
|
+
return null;
|
|
84
|
+
}
|
|
85
|
+
if (!explicitIncludes) {
|
|
86
|
+
for (const defaultSurface of DEFAULT_INSTALL_SURFACES) {
|
|
87
|
+
selected.add(defaultSurface);
|
|
88
|
+
}
|
|
89
|
+
explicitIncludes = true;
|
|
90
|
+
}
|
|
91
|
+
selected.delete(surface);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (arg.startsWith("--")) {
|
|
95
|
+
const surface = arg.slice("--".length);
|
|
96
|
+
if (!isInstallSurface(surface)) {
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
selected.add(surface);
|
|
100
|
+
explicitIncludes = true;
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (!isInstallSurface(arg)) {
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
selected.add(arg);
|
|
107
|
+
explicitIncludes = true;
|
|
108
|
+
}
|
|
109
|
+
const normalized = INSTALL_SURFACES.filter((surface) => selected.has(surface));
|
|
110
|
+
return normalized.length === 0 ? null : normalized;
|
|
111
|
+
}
|
|
112
|
+
function isDefaultSelection(selectedSurfaces) {
|
|
113
|
+
return selectedSurfaces.length === INSTALL_SURFACES.length &&
|
|
114
|
+
INSTALL_SURFACES.every((surface, index) => selectedSurfaces[index] === surface);
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=install.js.map
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { DEFAULT_INSTALL_SURFACES, formatInstallSurfaces, readInstallProfileFile, } from "../install-profile.js";
|
|
2
|
+
const MODES_USAGE = "Usage: moneysiren modes";
|
|
3
|
+
export async function runModesCommand(args, context) {
|
|
4
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
5
|
+
context.stdout(MODES_USAGE);
|
|
6
|
+
return 0;
|
|
7
|
+
}
|
|
8
|
+
if (args.length > 0) {
|
|
9
|
+
context.stderr(MODES_USAGE);
|
|
10
|
+
return 1;
|
|
11
|
+
}
|
|
12
|
+
const profile = await readInstallProfileFile({
|
|
13
|
+
env: context.env,
|
|
14
|
+
});
|
|
15
|
+
const selectedSurfaces = profile?.selectedSurfaces ?? DEFAULT_INSTALL_SURFACES;
|
|
16
|
+
context.stdout("MoneySiren modes");
|
|
17
|
+
context.stdout(`Platform: ${platformLabel()}`);
|
|
18
|
+
context.stdout(`Install profile: ${formatInstallSurfaces(selectedSurfaces)}${profile === null ? " (recommended default)" : ""}`);
|
|
19
|
+
context.stdout("npm install: npm install -g @moneysiren/cli@alpha");
|
|
20
|
+
context.stdout(`Runtime lock: ${runtimeLockHint()}`);
|
|
21
|
+
context.stdout("");
|
|
22
|
+
context.stdout("1. CLI automation");
|
|
23
|
+
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");
|
|
26
|
+
context.stdout("");
|
|
27
|
+
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.");
|
|
32
|
+
context.stdout("");
|
|
33
|
+
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");
|
|
37
|
+
context.stdout("");
|
|
38
|
+
context.stdout("Change selection: moneysiren install");
|
|
39
|
+
return 0;
|
|
40
|
+
}
|
|
41
|
+
function surfaceStatus(surface, selectedSurfaces) {
|
|
42
|
+
return selectedSurfaces.includes(surface) ? "selected;" : "not selected;";
|
|
43
|
+
}
|
|
44
|
+
function platformLabel() {
|
|
45
|
+
if (process.platform === "darwin") {
|
|
46
|
+
return `macOS (${process.platform} ${process.arch})`;
|
|
47
|
+
}
|
|
48
|
+
if (process.platform === "win32") {
|
|
49
|
+
return `Windows (${process.platform} ${process.arch})`;
|
|
50
|
+
}
|
|
51
|
+
if (process.platform === "linux") {
|
|
52
|
+
return `Linux (${process.platform} ${process.arch})`;
|
|
53
|
+
}
|
|
54
|
+
return `${process.platform} ${process.arch}`;
|
|
55
|
+
}
|
|
56
|
+
function runtimeLockHint() {
|
|
57
|
+
if (process.platform === "darwin") {
|
|
58
|
+
return "~/Library/Application Support/MoneySiren/runtime.json";
|
|
59
|
+
}
|
|
60
|
+
if (process.platform === "win32") {
|
|
61
|
+
return "%APPDATA%\\MoneySiren\\runtime.json";
|
|
62
|
+
}
|
|
63
|
+
return "${XDG_CONFIG_HOME:-~/.config}/moneysiren/runtime.json";
|
|
64
|
+
}
|
|
65
|
+
//# sourceMappingURL=modes.js.map
|
|
@@ -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
|