burnwatch 0.4.3 → 0.5.1
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/CHANGELOG.md +19 -4
- package/dist/cli.js +185 -124
- package/dist/cli.js.map +1 -1
- package/dist/interactive-init.d.ts +18 -4
- package/dist/interactive-init.js +162 -49
- package/dist/interactive-init.js.map +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,13 +5,26 @@ All notable changes to burnwatch will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [0.
|
|
8
|
+
## [0.5.1] - 2026-03-24
|
|
9
9
|
|
|
10
10
|
### Fixed
|
|
11
11
|
|
|
12
|
-
- **
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
- **Init actually works now**: `process.stdin.isTTY` was `undefined` in Claude Code and many terminal environments, so the interactive interview never ran. Init now has three modes: (1) TTY detected = full interactive interview, (2) no TTY = smart auto-configure with defaults, (3) `--non-interactive` = minimal CI mode. The auto-configure path picks each service's default plan, detects API keys from env vars and global config, sets budget = plan cost for flat plans and $0 for usage-based, and only shows `burnwatch add` commands for the handful of usage-based services that genuinely need a human-set budget.
|
|
13
|
+
|
|
14
|
+
## [0.5.0] - 2026-03-24
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- **Interactive init rewritten as proper interview**: Each service gets a structured conversation - plan selection, API key collection with hints on where to find them, and budget. Budgets are never skipped - pressing Enter applies the plan cost (or $0 for free tiers) instead of leaving budget undefined. Summary table at the end shows total monthly budget across all services.
|
|
19
|
+
- **Env var auto-detection for API keys**: Init checks `process.env` for known key patterns (e.g., `ANTHROPIC_ADMIN_KEY`, `SCRAPFLY_KEY`) and auto-imports them to global config. No need to re-enter keys that are already in your environment.
|
|
20
|
+
- **API key prompts for all LIVE services**: Previously only fired when the chosen plan had `requiresKey: true`. Now any LIVE-capable service gets the key prompt regardless of plan, with a hint about where to find the key (e.g., "Admin key: console.anthropic.com -> Settings -> Admin API Keys").
|
|
21
|
+
- **Non-interactive init always sets budget**: Every service gets `budget: 0` at minimum (flat-rate services get budget = plan cost). No more undefined budgets showing as "-" in status output.
|
|
22
|
+
- **Non-interactive env var key detection**: Checks environment for API keys matching service patterns, auto-saves to global config for LIVE tracking.
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
|
|
26
|
+
- **13/14 services had no budget after init**: Root cause was budget prompt saying "press Enter to skip". Now pressing Enter applies the default budget instead of skipping.
|
|
27
|
+
- **Untracked message was circular**: Changed "run burnwatch status" to "run burnwatch init to configure".
|
|
15
28
|
|
|
16
29
|
## [0.4.2] - 2026-03-24
|
|
17
30
|
|
|
@@ -59,6 +72,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
59
72
|
- Snapshot system for delta computation across sessions
|
|
60
73
|
- Claude Code skills: `/spend` (on-demand brief), `/setup-burnwatch` (guided onboarding)
|
|
61
74
|
|
|
75
|
+
[0.5.1]: https://github.com/RaleighSF/burnwatch/compare/v0.5.0...v0.5.1
|
|
76
|
+
[0.5.0]: https://github.com/RaleighSF/burnwatch/compare/v0.4.3...v0.5.0
|
|
62
77
|
[0.4.3]: https://github.com/RaleighSF/burnwatch/compare/v0.4.2...v0.4.3
|
|
63
78
|
[0.4.2]: https://github.com/RaleighSF/burnwatch/compare/v0.4.0...v0.4.2
|
|
64
79
|
[0.4.0]: https://github.com/RaleighSF/burnwatch/compare/v0.1.0...v0.4.0
|
package/dist/cli.js
CHANGED
|
@@ -789,10 +789,18 @@ function saveSnapshot(brief, projectRoot) {
|
|
|
789
789
|
import * as readline from "readline";
|
|
790
790
|
var RISK_ORDER = ["llm", "usage", "infra", "flat"];
|
|
791
791
|
var RISK_LABELS = {
|
|
792
|
-
llm: "
|
|
793
|
-
usage: "
|
|
794
|
-
infra: "
|
|
795
|
-
flat: "
|
|
792
|
+
llm: "LLM / AI Services (highest variable cost)",
|
|
793
|
+
usage: "Usage-Based Services",
|
|
794
|
+
infra: "Infrastructure & Compute",
|
|
795
|
+
flat: "Flat-Rate / Free Tier Services"
|
|
796
|
+
};
|
|
797
|
+
var API_KEY_HINTS = {
|
|
798
|
+
anthropic: "Admin key: console.anthropic.com -> Settings -> Admin API Keys",
|
|
799
|
+
openai: "Org key: platform.openai.com -> Settings -> API Keys",
|
|
800
|
+
vercel: "Token: vercel.com/account/tokens",
|
|
801
|
+
supabase: "Service role key: supabase.com/dashboard -> Settings -> API",
|
|
802
|
+
stripe: "Secret key: dashboard.stripe.com -> Developers -> API Keys",
|
|
803
|
+
scrapfly: "API key: scrapfly.io/dashboard"
|
|
796
804
|
};
|
|
797
805
|
function classifyRisk(service) {
|
|
798
806
|
if (service.billingModel === "token_usage") return "llm";
|
|
@@ -829,6 +837,88 @@ async function autoDetectScrapflyPlan(apiKey) {
|
|
|
829
837
|
}
|
|
830
838
|
return null;
|
|
831
839
|
}
|
|
840
|
+
function findEnvKey(service) {
|
|
841
|
+
for (const pattern of service.envPatterns) {
|
|
842
|
+
const val = process.env[pattern];
|
|
843
|
+
if (val && val.length > 0) return val;
|
|
844
|
+
}
|
|
845
|
+
return void 0;
|
|
846
|
+
}
|
|
847
|
+
function autoConfigureServices(detected) {
|
|
848
|
+
const services = {};
|
|
849
|
+
const groups = groupByRisk(detected);
|
|
850
|
+
const globalConfig = readGlobalConfig();
|
|
851
|
+
console.log(
|
|
852
|
+
`
|
|
853
|
+
Found ${detected.length} paid service${detected.length !== 1 ? "s" : ""}. Auto-configuring with defaults.
|
|
854
|
+
`
|
|
855
|
+
);
|
|
856
|
+
console.log(" Run 'burnwatch init' from your terminal for interactive setup.\n");
|
|
857
|
+
for (const category of RISK_ORDER) {
|
|
858
|
+
const group = groups.get(category);
|
|
859
|
+
if (group.length === 0) continue;
|
|
860
|
+
console.log(` ${RISK_LABELS[category]}`);
|
|
861
|
+
for (const det of group) {
|
|
862
|
+
const service = det.service;
|
|
863
|
+
const plans = service.plans ?? [];
|
|
864
|
+
const defaultPlan = plans.find((p) => p.default) ?? plans[0];
|
|
865
|
+
const tracked = {
|
|
866
|
+
serviceId: service.id,
|
|
867
|
+
detectedVia: det.sources,
|
|
868
|
+
hasApiKey: false,
|
|
869
|
+
firstDetected: (/* @__PURE__ */ new Date()).toISOString(),
|
|
870
|
+
budget: 0
|
|
871
|
+
};
|
|
872
|
+
if (defaultPlan && defaultPlan.type !== "exclude") {
|
|
873
|
+
tracked.planName = defaultPlan.name;
|
|
874
|
+
if (defaultPlan.type === "flat" && defaultPlan.monthlyBase !== void 0) {
|
|
875
|
+
tracked.planCost = defaultPlan.monthlyBase;
|
|
876
|
+
tracked.budget = defaultPlan.monthlyBase;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
const existingKey = globalConfig.services[service.id]?.apiKey;
|
|
880
|
+
const envKey = findEnvKey(service);
|
|
881
|
+
let keySource = "";
|
|
882
|
+
if (existingKey) {
|
|
883
|
+
tracked.hasApiKey = true;
|
|
884
|
+
keySource = " (key: global config)";
|
|
885
|
+
} else if (envKey) {
|
|
886
|
+
tracked.hasApiKey = true;
|
|
887
|
+
if (!globalConfig.services[service.id]) {
|
|
888
|
+
globalConfig.services[service.id] = {};
|
|
889
|
+
}
|
|
890
|
+
globalConfig.services[service.id].apiKey = envKey;
|
|
891
|
+
keySource = ` (key: ${service.envPatterns[0]})`;
|
|
892
|
+
}
|
|
893
|
+
const tierLabel = tracked.hasApiKey ? "LIVE" : tracked.planCost !== void 0 ? "CALC" : "BLIND";
|
|
894
|
+
const planStr = tracked.planName ? ` ${tracked.planName}` : "";
|
|
895
|
+
console.log(
|
|
896
|
+
` ${service.name}:${planStr} | ${tierLabel} | $${tracked.budget}/mo${keySource}`
|
|
897
|
+
);
|
|
898
|
+
services[service.id] = tracked;
|
|
899
|
+
}
|
|
900
|
+
console.log("");
|
|
901
|
+
}
|
|
902
|
+
const trackedList = Object.values(services);
|
|
903
|
+
const liveCount = trackedList.filter((s) => s.hasApiKey).length;
|
|
904
|
+
const totalBudget = trackedList.reduce((sum, s) => sum + (s.budget ?? 0), 0);
|
|
905
|
+
console.log(" " + "-".repeat(48));
|
|
906
|
+
console.log(` ${trackedList.length} services configured | Total budget: $${totalBudget}/mo`);
|
|
907
|
+
if (liveCount > 0) console.log(` ${liveCount} with real-time billing (LIVE)`);
|
|
908
|
+
const needBudget = trackedList.filter(
|
|
909
|
+
(s) => s.budget === 0 && s.planCost === void 0
|
|
910
|
+
);
|
|
911
|
+
if (needBudget.length > 0) {
|
|
912
|
+
console.log(`
|
|
913
|
+
${needBudget.length} usage-based service${needBudget.length > 1 ? "s" : ""} need budgets:`);
|
|
914
|
+
for (const svc of needBudget) {
|
|
915
|
+
console.log(` burnwatch add ${svc.serviceId} --budget <AMOUNT>`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
console.log("");
|
|
919
|
+
writeGlobalConfig(globalConfig);
|
|
920
|
+
return { services };
|
|
921
|
+
}
|
|
832
922
|
async function runInteractiveInit(detected) {
|
|
833
923
|
const rl = readline.createInterface({
|
|
834
924
|
input: process.stdin,
|
|
@@ -838,14 +928,16 @@ async function runInteractiveInit(detected) {
|
|
|
838
928
|
const groups = groupByRisk(detected);
|
|
839
929
|
const globalConfig = readGlobalConfig();
|
|
840
930
|
console.log(
|
|
841
|
-
|
|
931
|
+
`
|
|
932
|
+
Found ${detected.length} paid service${detected.length !== 1 ? "s" : ""}. Let's configure each one.
|
|
933
|
+
`
|
|
842
934
|
);
|
|
843
935
|
for (const category of RISK_ORDER) {
|
|
844
936
|
const group = groups.get(category);
|
|
845
937
|
if (group.length === 0) continue;
|
|
846
938
|
console.log(`
|
|
847
|
-
${RISK_LABELS[category]}`);
|
|
848
|
-
console.log("
|
|
939
|
+
${RISK_LABELS[category]}`);
|
|
940
|
+
console.log(" " + "-".repeat(48));
|
|
849
941
|
for (const det of group) {
|
|
850
942
|
const service = det.service;
|
|
851
943
|
const plans = service.plans;
|
|
@@ -857,23 +949,24 @@ ${RISK_LABELS[category]}`);
|
|
|
857
949
|
serviceId: service.id,
|
|
858
950
|
detectedVia: det.sources,
|
|
859
951
|
hasApiKey: false,
|
|
860
|
-
firstDetected: (/* @__PURE__ */ new Date()).toISOString()
|
|
952
|
+
firstDetected: (/* @__PURE__ */ new Date()).toISOString(),
|
|
953
|
+
budget: 0
|
|
861
954
|
};
|
|
862
|
-
console.log("
|
|
955
|
+
console.log(" -> Configured (no plan tiers in registry, budget: $0)");
|
|
863
956
|
continue;
|
|
864
957
|
}
|
|
865
958
|
const defaultIndex = plans.findIndex((p) => p.default);
|
|
866
959
|
console.log("");
|
|
867
960
|
for (let i = 0; i < plans.length; i++) {
|
|
868
961
|
const plan = plans[i];
|
|
869
|
-
const marker = i === defaultIndex ? "
|
|
870
|
-
const costStr = plan.type === "exclude" ? "" : plan.monthlyBase !== void 0 ? `
|
|
962
|
+
const marker = i === defaultIndex ? " *" : "";
|
|
963
|
+
const costStr = plan.type === "exclude" ? "" : plan.monthlyBase !== void 0 ? ` - $${plan.monthlyBase}/mo` : " - variable";
|
|
871
964
|
console.log(` ${i + 1}) ${plan.name}${costStr}${marker}`);
|
|
872
965
|
}
|
|
873
966
|
const defaultChoice = defaultIndex >= 0 ? String(defaultIndex + 1) : "1";
|
|
874
967
|
const answer = await ask(
|
|
875
968
|
rl,
|
|
876
|
-
`
|
|
969
|
+
` Which plan? [${defaultChoice}]: `
|
|
877
970
|
);
|
|
878
971
|
const choiceIndex = (answer === "" ? parseInt(defaultChoice) : parseInt(answer)) - 1;
|
|
879
972
|
const chosen = plans[choiceIndex] ?? plans[defaultIndex >= 0 ? defaultIndex : 0];
|
|
@@ -886,10 +979,10 @@ ${RISK_LABELS[category]}`);
|
|
|
886
979
|
excluded: true,
|
|
887
980
|
planName: chosen.name
|
|
888
981
|
};
|
|
889
|
-
console.log(`
|
|
982
|
+
console.log(` -> ${service.name}: excluded`);
|
|
890
983
|
continue;
|
|
891
984
|
}
|
|
892
|
-
const
|
|
985
|
+
const tracked2 = {
|
|
893
986
|
serviceId: service.id,
|
|
894
987
|
detectedVia: det.sources,
|
|
895
988
|
hasApiKey: false,
|
|
@@ -897,67 +990,86 @@ ${RISK_LABELS[category]}`);
|
|
|
897
990
|
planName: chosen.name
|
|
898
991
|
};
|
|
899
992
|
if (chosen.type === "flat" && chosen.monthlyBase !== void 0) {
|
|
900
|
-
|
|
901
|
-
if (chosen.monthlyBase > 0) {
|
|
902
|
-
tracked.budget = chosen.monthlyBase;
|
|
903
|
-
}
|
|
993
|
+
tracked2.planCost = chosen.monthlyBase;
|
|
904
994
|
}
|
|
905
|
-
if (service.apiTier === "live"
|
|
995
|
+
if (service.apiTier === "live") {
|
|
906
996
|
const existingKey = globalConfig.services[service.id]?.apiKey;
|
|
997
|
+
const envKey = findEnvKey(service);
|
|
907
998
|
if (existingKey) {
|
|
908
|
-
console.log(`
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
tracked.planName = planName;
|
|
916
|
-
}
|
|
999
|
+
console.log(` API key: found in global config`);
|
|
1000
|
+
tracked2.hasApiKey = true;
|
|
1001
|
+
} else if (envKey) {
|
|
1002
|
+
console.log(` API key: found in environment (${service.envPatterns[0]})`);
|
|
1003
|
+
tracked2.hasApiKey = true;
|
|
1004
|
+
if (!globalConfig.services[service.id]) {
|
|
1005
|
+
globalConfig.services[service.id] = {};
|
|
917
1006
|
}
|
|
918
|
-
|
|
1007
|
+
globalConfig.services[service.id].apiKey = envKey;
|
|
1008
|
+
} else {
|
|
1009
|
+
const hint = API_KEY_HINTS[service.id];
|
|
1010
|
+
if (hint) console.log(` ${hint}`);
|
|
919
1011
|
const keyAnswer = await ask(
|
|
920
1012
|
rl,
|
|
921
|
-
`
|
|
1013
|
+
` API key for real-time tracking (Enter to skip): `
|
|
922
1014
|
);
|
|
923
1015
|
if (keyAnswer) {
|
|
924
|
-
|
|
1016
|
+
tracked2.hasApiKey = true;
|
|
925
1017
|
if (!globalConfig.services[service.id]) {
|
|
926
1018
|
globalConfig.services[service.id] = {};
|
|
927
1019
|
}
|
|
928
1020
|
globalConfig.services[service.id].apiKey = keyAnswer;
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
if (service.autoDetectPlan && service.id === "scrapfly" && tracked2.hasApiKey) {
|
|
1024
|
+
const key = globalConfig.services[service.id]?.apiKey;
|
|
1025
|
+
if (key) {
|
|
1026
|
+
console.log(" Detecting plan from API...");
|
|
1027
|
+
const planName = await autoDetectScrapflyPlan(key);
|
|
1028
|
+
if (planName) {
|
|
1029
|
+
console.log(` -> Detected plan: ${planName}`);
|
|
1030
|
+
tracked2.planName = planName;
|
|
936
1031
|
}
|
|
937
1032
|
}
|
|
938
1033
|
}
|
|
939
1034
|
}
|
|
940
|
-
|
|
941
|
-
|
|
1035
|
+
const planCost = chosen.monthlyBase ?? 0;
|
|
1036
|
+
if (chosen.type === "usage" && planCost === 0) {
|
|
1037
|
+
const budgetAnswer = await ask(
|
|
1038
|
+
rl,
|
|
1039
|
+
` Monthly budget: $`
|
|
1040
|
+
);
|
|
1041
|
+
const parsed = parseFloat(budgetAnswer);
|
|
1042
|
+
tracked2.budget = !isNaN(parsed) ? parsed : 0;
|
|
1043
|
+
} else {
|
|
942
1044
|
const budgetAnswer = await ask(
|
|
943
1045
|
rl,
|
|
944
|
-
` Monthly budget
|
|
1046
|
+
` Monthly budget [$${planCost}]: $`
|
|
945
1047
|
);
|
|
946
1048
|
if (budgetAnswer) {
|
|
947
|
-
const
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
1049
|
+
const parsed = parseFloat(budgetAnswer);
|
|
1050
|
+
tracked2.budget = !isNaN(parsed) ? parsed : planCost;
|
|
1051
|
+
} else {
|
|
1052
|
+
tracked2.budget = planCost;
|
|
951
1053
|
}
|
|
952
1054
|
}
|
|
953
|
-
services[service.id] =
|
|
954
|
-
const tierLabel =
|
|
955
|
-
const budgetStr = tracked.budget !== void 0 ? ` | Budget: $${tracked.budget}/mo` : "";
|
|
1055
|
+
services[service.id] = tracked2;
|
|
1056
|
+
const tierLabel = tracked2.hasApiKey ? "LIVE" : tracked2.planCost !== void 0 ? "CALC" : "BLIND";
|
|
956
1057
|
console.log(
|
|
957
|
-
`
|
|
1058
|
+
` -> ${service.name}: ${tracked2.planName} | ${tierLabel} | $${tracked2.budget}/mo`
|
|
958
1059
|
);
|
|
959
1060
|
}
|
|
960
1061
|
}
|
|
1062
|
+
const tracked = Object.values(services).filter((s) => !s.excluded);
|
|
1063
|
+
const excluded = Object.values(services).filter((s) => s.excluded);
|
|
1064
|
+
const liveCount = tracked.filter((s) => s.hasApiKey).length;
|
|
1065
|
+
const totalBudget = tracked.reduce((sum, s) => sum + (s.budget ?? 0), 0);
|
|
1066
|
+
console.log("\n " + "=".repeat(48));
|
|
1067
|
+
console.log(` ${tracked.length} services configured`);
|
|
1068
|
+
if (liveCount > 0) console.log(` ${liveCount} with real-time billing (LIVE)`);
|
|
1069
|
+
if (tracked.length - liveCount > 0) console.log(` ${tracked.length - liveCount} estimated/calculated`);
|
|
1070
|
+
if (excluded.length > 0) console.log(` ${excluded.length} excluded`);
|
|
1071
|
+
console.log(` Total monthly budget: $${totalBudget}`);
|
|
1072
|
+
console.log(" " + "=".repeat(48));
|
|
961
1073
|
writeGlobalConfig(globalConfig);
|
|
962
1074
|
rl.close();
|
|
963
1075
|
return { services };
|
|
@@ -1025,64 +1137,29 @@ async function cmdInit() {
|
|
|
1025
1137
|
createdAt: existingConfig?.createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
1026
1138
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1027
1139
|
};
|
|
1028
|
-
if (
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
} else {
|
|
1032
|
-
const globalConfig = readGlobalConfig();
|
|
1140
|
+
if (detected.length === 0) {
|
|
1141
|
+
console.log(" No paid services detected yet.");
|
|
1142
|
+
console.log(" Services will be detected as they enter your project.\n");
|
|
1143
|
+
} else if (nonInteractive) {
|
|
1033
1144
|
for (const det of detected) {
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
hasApiKey: existing?.hasApiKey ?? false,
|
|
1043
|
-
firstDetected: existing?.firstDetected ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
1044
|
-
};
|
|
1045
|
-
if (defaultPlan && defaultPlan.type !== "exclude") {
|
|
1046
|
-
tracked2.planName = defaultPlan.name;
|
|
1047
|
-
if (defaultPlan.type === "flat" && defaultPlan.monthlyBase !== void 0) {
|
|
1048
|
-
tracked2.planCost = defaultPlan.monthlyBase;
|
|
1049
|
-
if (defaultPlan.monthlyBase > 0) {
|
|
1050
|
-
tracked2.budget = defaultPlan.monthlyBase;
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
}
|
|
1054
|
-
const existingKey = globalConfig.services[service.id]?.apiKey;
|
|
1055
|
-
if (existingKey) {
|
|
1056
|
-
tracked2.hasApiKey = true;
|
|
1145
|
+
if (!config.services[det.service.id]) {
|
|
1146
|
+
config.services[det.service.id] = {
|
|
1147
|
+
serviceId: det.service.id,
|
|
1148
|
+
detectedVia: det.sources,
|
|
1149
|
+
hasApiKey: false,
|
|
1150
|
+
firstDetected: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1151
|
+
budget: 0
|
|
1152
|
+
};
|
|
1057
1153
|
}
|
|
1058
|
-
config.services[service.id] = tracked2;
|
|
1059
1154
|
}
|
|
1060
|
-
|
|
1061
|
-
console.log(" No paid services detected yet.");
|
|
1062
|
-
console.log(" Services will be detected as they enter your project.\n");
|
|
1063
|
-
} else {
|
|
1064
|
-
console.log(` Found ${detected.length} paid service${detected.length > 1 ? "s" : ""}:
|
|
1155
|
+
console.log(` Registered ${detected.length} services (non-interactive).
|
|
1065
1156
|
`);
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
console.log(` \u2022 ${det.service.name}${planStr} ${tierBadge}${budgetStr}`);
|
|
1073
|
-
if (!svc?.budget) {
|
|
1074
|
-
needBudget.push(det.service.id);
|
|
1075
|
-
}
|
|
1076
|
-
}
|
|
1077
|
-
console.log("");
|
|
1078
|
-
if (needBudget.length > 0) {
|
|
1079
|
-
console.log(` ${needBudget.length} service${needBudget.length > 1 ? "s" : ""} need budgets. Set them with:`);
|
|
1080
|
-
for (const id of needBudget) {
|
|
1081
|
-
console.log(` burnwatch add ${id} --budget <AMOUNT>`);
|
|
1082
|
-
}
|
|
1083
|
-
console.log("");
|
|
1084
|
-
}
|
|
1085
|
-
}
|
|
1157
|
+
} else if (process.stdin.isTTY) {
|
|
1158
|
+
const result = await runInteractiveInit(detected);
|
|
1159
|
+
config.services = result.services;
|
|
1160
|
+
} else {
|
|
1161
|
+
const result = autoConfigureServices(detected);
|
|
1162
|
+
config.services = result.services;
|
|
1086
1163
|
}
|
|
1087
1164
|
writeProjectConfig(config, projectRoot);
|
|
1088
1165
|
const gitignorePath = path5.join(projectConfigDir(projectRoot), ".gitignore");
|
|
@@ -1099,27 +1176,11 @@ async function cmdInit() {
|
|
|
1099
1176
|
);
|
|
1100
1177
|
console.log("\n\u{1F517} Registering Claude Code hooks...\n");
|
|
1101
1178
|
registerHooks(projectRoot);
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
console.log("
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
for (const svc of tracked) {
|
|
1108
|
-
const planStr = svc.planName ? ` (${svc.planName})` : "";
|
|
1109
|
-
const budgetStr = svc.budget !== void 0 ? ` \u2014 $${svc.budget}/mo budget` : "";
|
|
1110
|
-
console.log(` \u2022 ${svc.serviceId}${planStr}${budgetStr}`);
|
|
1111
|
-
}
|
|
1112
|
-
}
|
|
1113
|
-
if (excluded.length > 0) {
|
|
1114
|
-
console.log(`
|
|
1115
|
-
Excluded ${excluded.length} service${excluded.length > 1 ? "s" : ""}:`);
|
|
1116
|
-
for (const svc of excluded) {
|
|
1117
|
-
console.log(` \u2022 ${svc.serviceId}`);
|
|
1118
|
-
}
|
|
1119
|
-
}
|
|
1120
|
-
console.log("\nNext steps:");
|
|
1121
|
-
console.log(" burnwatch status \u2014 Check your spend");
|
|
1122
|
-
console.log(" burnwatch add <svc> \u2014 Configure additional services\n");
|
|
1179
|
+
console.log("\nburnwatch initialized.\n");
|
|
1180
|
+
console.log("Next steps:");
|
|
1181
|
+
console.log(" burnwatch status Show current spend");
|
|
1182
|
+
console.log(" burnwatch add <svc> Update a service's budget or API key");
|
|
1183
|
+
console.log(" burnwatch init Re-run this setup anytime\n");
|
|
1123
1184
|
}
|
|
1124
1185
|
async function cmdAdd() {
|
|
1125
1186
|
const projectRoot = process.cwd();
|