burnwatch 0.2.0 → 0.3.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/dist/cli.js CHANGED
@@ -122,7 +122,7 @@ function getAllServices(projectRoot) {
122
122
  function detectServices(projectRoot) {
123
123
  const registry = loadRegistry(projectRoot);
124
124
  const results = /* @__PURE__ */ new Map();
125
- const pkgDeps = scanPackageJson(projectRoot);
125
+ const pkgDeps = scanAllPackageJsons(projectRoot);
126
126
  for (const [serviceId, service] of registry) {
127
127
  const matchedPkgs = service.packageNames.filter(
128
128
  (pkg) => pkgDeps.has(pkg)
@@ -134,7 +134,7 @@ function detectServices(projectRoot) {
134
134
  );
135
135
  }
136
136
  }
137
- const envVars = new Set(Object.keys(process.env));
137
+ const envVars = collectEnvVars(projectRoot);
138
138
  for (const [serviceId, service] of registry) {
139
139
  const matchedEnvs = service.envPatterns.filter(
140
140
  (pattern) => envVars.has(pattern)
@@ -172,40 +172,118 @@ function getOrCreate(map, serviceId, service) {
172
172
  }
173
173
  return result;
174
174
  }
175
- function scanPackageJson(projectRoot) {
175
+ function scanAllPackageJsons(projectRoot) {
176
176
  const deps = /* @__PURE__ */ new Set();
177
- const pkgPath = path3.join(projectRoot, "package.json");
177
+ const pkgFiles = findFiles(projectRoot, "package.json", 4);
178
+ for (const pkgPath of pkgFiles) {
179
+ try {
180
+ const raw = fs3.readFileSync(pkgPath, "utf-8");
181
+ const pkg = JSON.parse(raw);
182
+ for (const name of Object.keys(pkg.dependencies ?? {})) deps.add(name);
183
+ for (const name of Object.keys(pkg.devDependencies ?? {})) deps.add(name);
184
+ } catch {
185
+ }
186
+ }
187
+ return deps;
188
+ }
189
+ function collectEnvVars(projectRoot) {
190
+ const envVars = new Set(Object.keys(process.env));
191
+ const envFiles = findEnvFiles(projectRoot, 3);
192
+ for (const envFile of envFiles) {
193
+ try {
194
+ const content = fs3.readFileSync(envFile, "utf-8");
195
+ const keys = content.split("\n").filter((line) => line.includes("=") && !line.startsWith("#")).map((line) => line.split("=")[0].trim()).filter(Boolean);
196
+ for (const key of keys) {
197
+ envVars.add(key);
198
+ }
199
+ } catch {
200
+ }
201
+ }
202
+ return envVars;
203
+ }
204
+ function findEnvFiles(dir, maxDepth) {
205
+ const results = [];
206
+ if (maxDepth <= 0) return results;
178
207
  try {
179
- const raw = fs3.readFileSync(pkgPath, "utf-8");
180
- const pkg = JSON.parse(raw);
181
- for (const name of Object.keys(pkg.dependencies ?? {})) deps.add(name);
182
- for (const name of Object.keys(pkg.devDependencies ?? {})) deps.add(name);
208
+ const entries = fs3.readdirSync(dir, { withFileTypes: true });
209
+ for (const entry of entries) {
210
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") continue;
211
+ const fullPath = path3.join(dir, entry.name);
212
+ if (entry.isDirectory()) {
213
+ results.push(...findEnvFiles(fullPath, maxDepth - 1));
214
+ } else if (entry.name.startsWith(".env")) {
215
+ results.push(fullPath);
216
+ }
217
+ }
183
218
  } catch {
184
219
  }
185
- return deps;
220
+ return results;
221
+ }
222
+ function findFiles(dir, fileName, maxDepth) {
223
+ const results = [];
224
+ if (maxDepth <= 0) return results;
225
+ try {
226
+ const entries = fs3.readdirSync(dir, { withFileTypes: true });
227
+ for (const entry of entries) {
228
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist") continue;
229
+ const fullPath = path3.join(dir, entry.name);
230
+ if (entry.isDirectory()) {
231
+ results.push(...findFiles(fullPath, fileName, maxDepth - 1));
232
+ } else if (entry.name === fileName) {
233
+ results.push(fullPath);
234
+ }
235
+ }
236
+ } catch {
237
+ }
238
+ return results;
186
239
  }
187
240
  function scanImports(projectRoot) {
188
241
  const imports = /* @__PURE__ */ new Set();
189
- const srcDir = path3.join(projectRoot, "src");
190
- if (!fs3.existsSync(srcDir)) return imports;
191
- const files = walkDir(srcDir, /\.(ts|tsx|js|jsx|mjs|cjs)$/);
192
- for (const file of files) {
193
- try {
194
- const content = fs3.readFileSync(file, "utf-8");
195
- const importRegex = /(?:from\s+["']|require\s*\(\s*["'])([^./][^"']*?)(?:["'])/g;
196
- let match;
197
- while ((match = importRegex.exec(content)) !== null) {
198
- const pkg = match[1];
199
- if (pkg) {
200
- const parts = pkg.split("/");
201
- if (parts[0]?.startsWith("@") && parts.length >= 2) {
202
- imports.add(`${parts[0]}/${parts[1]}`);
203
- } else if (parts[0]) {
204
- imports.add(parts[0]);
242
+ const codeDirs = ["src", "app", "lib", "pages", "components", "utils", "services", "hooks"];
243
+ const dirsToScan = [];
244
+ for (const dir of codeDirs) {
245
+ const fullPath = path3.join(projectRoot, dir);
246
+ if (fs3.existsSync(fullPath)) {
247
+ dirsToScan.push(fullPath);
248
+ }
249
+ }
250
+ try {
251
+ const entries = fs3.readdirSync(projectRoot, { withFileTypes: true });
252
+ for (const entry of entries) {
253
+ if (!entry.isDirectory()) continue;
254
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name === "dist" || entry.name.startsWith(".")) continue;
255
+ const subPkgPath = path3.join(projectRoot, entry.name, "package.json");
256
+ if (fs3.existsSync(subPkgPath)) {
257
+ for (const dir of codeDirs) {
258
+ const fullPath = path3.join(projectRoot, entry.name, dir);
259
+ if (fs3.existsSync(fullPath)) {
260
+ dirsToScan.push(fullPath);
205
261
  }
206
262
  }
207
263
  }
208
- } catch {
264
+ }
265
+ } catch {
266
+ }
267
+ for (const dir of dirsToScan) {
268
+ const files = walkDir(dir, /\.(ts|tsx|js|jsx|mjs|cjs)$/);
269
+ for (const file of files) {
270
+ try {
271
+ const content = fs3.readFileSync(file, "utf-8");
272
+ const importRegex = /(?:from\s+["']|require\s*\(\s*["'])([^./][^"']*?)(?:["'])/g;
273
+ let match;
274
+ while ((match = importRegex.exec(content)) !== null) {
275
+ const pkg = match[1];
276
+ if (pkg) {
277
+ const parts = pkg.split("/");
278
+ if (parts[0]?.startsWith("@") && parts.length >= 2) {
279
+ imports.add(`${parts[0]}/${parts[1]}`);
280
+ } else if (parts[0]) {
281
+ imports.add(parts[0]);
282
+ }
283
+ }
284
+ }
285
+ } catch {
286
+ }
209
287
  }
210
288
  }
211
289
  return imports;
@@ -704,6 +782,7 @@ var command = args[0];
704
782
  async function main() {
705
783
  switch (command) {
706
784
  case "init":
785
+ case "setup":
707
786
  await cmdInit();
708
787
  break;
709
788
  case "add":
@@ -718,9 +797,6 @@ async function main() {
718
797
  case "reconcile":
719
798
  await cmdReconcile();
720
799
  break;
721
- case "setup":
722
- await cmdSetup();
723
- break;
724
800
  case "help":
725
801
  case "--help":
726
802
  case "-h":
@@ -920,69 +996,6 @@ async function cmdStatus() {
920
996
  console.log("");
921
997
  }
922
998
  }
923
- async function cmdSetup() {
924
- const projectRoot = process.cwd();
925
- if (!isInitialized(projectRoot)) {
926
- await cmdInit();
927
- }
928
- const config = readProjectConfig(projectRoot);
929
- const detected = Object.values(config.services);
930
- if (detected.length === 0) {
931
- console.log("No paid services detected. You're all set!");
932
- return;
933
- }
934
- console.log("\u{1F4CB} Auto-configuring detected services...\n");
935
- const globalConfig = readGlobalConfig();
936
- const liveServices = [];
937
- const calcServices = [];
938
- const estServices = [];
939
- const blindServices = [];
940
- for (const tracked of detected) {
941
- const definition = getService(tracked.serviceId, projectRoot);
942
- if (!definition) continue;
943
- const hasKey = !!globalConfig.services[tracked.serviceId]?.apiKey;
944
- if (hasKey && definition.apiTier === "live") {
945
- tracked.hasApiKey = true;
946
- liveServices.push(`${definition.name}`);
947
- } else if (definition.apiTier === "calc") {
948
- calcServices.push(`${definition.name}`);
949
- } else if (definition.apiTier === "est") {
950
- estServices.push(`${definition.name}`);
951
- } else {
952
- blindServices.push(`${definition.name}`);
953
- }
954
- }
955
- writeProjectConfig(config, projectRoot);
956
- if (liveServices.length > 0) {
957
- console.log(` \u2705 LIVE (real billing data): ${liveServices.join(", ")}`);
958
- }
959
- if (calcServices.length > 0) {
960
- console.log(` \u{1F7E1} CALC (flat-rate tracking): ${calcServices.join(", ")}`);
961
- }
962
- if (estServices.length > 0) {
963
- console.log(` \u{1F7E0} EST (estimated from usage): ${estServices.join(", ")}`);
964
- }
965
- if (blindServices.length > 0) {
966
- console.log(` \u{1F534} BLIND (detected, need API key): ${blindServices.join(", ")}`);
967
- }
968
- console.log("");
969
- if (blindServices.length > 0) {
970
- console.log("To upgrade BLIND services to LIVE, add API keys:");
971
- for (const tracked of detected) {
972
- const definition = getService(tracked.serviceId, projectRoot);
973
- if (definition?.apiTier === "live" && !tracked.hasApiKey) {
974
- const envHint = definition.envPatterns[0] ?? "YOUR_KEY";
975
- console.log(` burnwatch add ${tracked.serviceId} --key $${envHint} --budget <N>`);
976
- }
977
- }
978
- console.log("");
979
- }
980
- console.log("To set budgets for any service:");
981
- console.log(" burnwatch add <service> --budget <monthly_amount>");
982
- console.log("");
983
- console.log("Or use /setup-burnwatch in Claude Code for guided setup with budget suggestions.\n");
984
- await cmdStatus();
985
- }
986
999
  function cmdServices() {
987
1000
  const services = getAllServices();
988
1001
  console.log(`
@@ -1066,24 +1079,47 @@ function cmdVersion() {
1066
1079
  }
1067
1080
  }
1068
1081
  function registerHooks(projectRoot) {
1082
+ const sourceHooksDir = path5.resolve(
1083
+ path5.dirname(new URL(import.meta.url).pathname),
1084
+ "hooks"
1085
+ );
1086
+ const localHooksDir = path5.join(projectRoot, ".burnwatch", "hooks");
1087
+ fs5.mkdirSync(localHooksDir, { recursive: true });
1088
+ const hookFiles = [
1089
+ "on-session-start.js",
1090
+ "on-prompt.js",
1091
+ "on-file-change.js",
1092
+ "on-stop.js"
1093
+ ];
1094
+ for (const file of hookFiles) {
1095
+ const src = path5.join(sourceHooksDir, file);
1096
+ const dest = path5.join(localHooksDir, file);
1097
+ try {
1098
+ fs5.copyFileSync(src, dest);
1099
+ const mapSrc = src + ".map";
1100
+ if (fs5.existsSync(mapSrc)) {
1101
+ fs5.copyFileSync(mapSrc, dest + ".map");
1102
+ }
1103
+ } catch (err) {
1104
+ console.error(` Warning: Could not copy hook ${file}: ${err instanceof Error ? err.message : err}`);
1105
+ }
1106
+ }
1107
+ console.log(` Hook scripts copied to ${localHooksDir}`);
1069
1108
  const claudeDir = path5.join(projectRoot, ".claude");
1070
1109
  const settingsPath = path5.join(claudeDir, "settings.json");
1071
1110
  fs5.mkdirSync(claudeDir, { recursive: true });
1072
1111
  let settings = {};
1073
1112
  try {
1074
- settings = JSON.parse(fs5.readFileSync(settingsPath, "utf-8"));
1113
+ const existing = fs5.readFileSync(settingsPath, "utf-8");
1114
+ settings = JSON.parse(existing);
1115
+ console.log(` Merging into existing ${settingsPath}`);
1075
1116
  } catch {
1076
1117
  }
1077
- const hookBase = `node "${path5.join(projectRoot, "node_modules", "burnwatch", "dist", "hooks")}"`;
1078
- const useNpx = !fs5.existsSync(
1079
- path5.join(projectRoot, "node_modules", "burnwatch")
1080
- );
1081
- const prefix = useNpx ? "npx --yes burnwatch-hook" : hookBase;
1082
- const hooksDir = path5.resolve(
1083
- path5.dirname(new URL(import.meta.url).pathname),
1084
- "hooks"
1085
- );
1086
- const hooks = settings["hooks"] ?? {};
1118
+ if (!settings["hooks"] || typeof settings["hooks"] !== "object") {
1119
+ settings["hooks"] = {};
1120
+ }
1121
+ const hooks = settings["hooks"];
1122
+ const hooksDir = localHooksDir;
1087
1123
  if (!hooks["SessionStart"]) hooks["SessionStart"] = [];
1088
1124
  addHookIfMissing(hooks["SessionStart"], "SessionStart", {
1089
1125
  matcher: "startup|resume",