@winston.wan/burn-your-money 2.0.5 → 2.1.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/README.md +71 -131
- package/bash.exe.stackdump +28 -0
- package/bin/burn-your-money +40 -20
- package/docs/ARCHITECTURE.md +58 -0
- package/docs/images/architecture_sketch.png +0 -0
- package/fix-git-bash.ps1 +154 -0
- package/install.js +254 -25
- package/package.json +2 -2
- package/src/statusline.sh +250 -213
- package/src/token-history.sh +268 -238
- package/uninstall.js +10 -4
package/install.js
CHANGED
|
@@ -2,6 +2,8 @@ const fs = require('fs');
|
|
|
2
2
|
const path = require('path');
|
|
3
3
|
const os = require('os');
|
|
4
4
|
const { execSync } = require('child_process');
|
|
5
|
+
const https = require('https');
|
|
6
|
+
const { createWriteStream, existsSync, mkdirSync } = fs;
|
|
5
7
|
|
|
6
8
|
// ANSI colors for console output
|
|
7
9
|
const colors = {
|
|
@@ -34,36 +36,119 @@ function getHomeDir() {
|
|
|
34
36
|
return os.homedir();
|
|
35
37
|
}
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
// Download file following redirects
|
|
40
|
+
function downloadFile(url, destPath) {
|
|
41
|
+
return new Promise((resolve, reject) => {
|
|
42
|
+
const download = (url) => {
|
|
43
|
+
https.get(url, (response) => {
|
|
44
|
+
// Handle redirects (301, 302, 307, 308)
|
|
45
|
+
if ([301, 302, 307, 308].includes(response.statusCode)) {
|
|
46
|
+
const redirectUrl = response.headers.location;
|
|
47
|
+
if (redirectUrl) {
|
|
48
|
+
download(redirectUrl);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Handle errors
|
|
54
|
+
if (response.statusCode !== 200) {
|
|
55
|
+
reject(new Error(`Failed to download: HTTP ${response.statusCode}`));
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const file = createWriteStream(destPath);
|
|
60
|
+
response.pipe(file);
|
|
61
|
+
|
|
62
|
+
file.on('finish', () => {
|
|
63
|
+
file.close();
|
|
64
|
+
resolve();
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
file.on('error', (err) => {
|
|
68
|
+
fs.unlink(destPath, () => {});
|
|
69
|
+
reject(err);
|
|
70
|
+
});
|
|
71
|
+
}).on('error', (err) => {
|
|
72
|
+
reject(err);
|
|
73
|
+
});
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
download(url);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Install jq for Windows by downloading binary
|
|
81
|
+
async function installJqWindows() {
|
|
82
|
+
const home = getHomeDir();
|
|
83
|
+
const binDir = path.join(home, '.claude', 'bin');
|
|
84
|
+
|
|
85
|
+
// Create bin directory
|
|
86
|
+
if (!existsSync(binDir)) {
|
|
87
|
+
mkdirSync(binDir, { recursive: true });
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const jqPath = path.join(binDir, 'jq.exe');
|
|
91
|
+
const jqUrl = 'https://github.com/jqlang/jq/releases/latest/download/jq-windows-amd64.exe';
|
|
92
|
+
|
|
93
|
+
log(`Downloading jq to ${jqPath}...`, colors.cyan);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
await downloadFile(jqUrl, jqPath);
|
|
97
|
+
success("jq downloaded successfully!");
|
|
98
|
+
return jqPath;
|
|
99
|
+
} catch (err) {
|
|
100
|
+
error(`Failed to download jq: ${err.message}`);
|
|
101
|
+
throw err;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Install jq for macOS/Linux
|
|
106
|
+
function installJqUnix() {
|
|
107
|
+
try {
|
|
108
|
+
log("Attempting to install jq via brew...", colors.cyan);
|
|
109
|
+
execSync('brew install jq', { stdio: 'inherit' });
|
|
110
|
+
success("jq installed successfully!");
|
|
111
|
+
return 'jq';
|
|
112
|
+
} catch (brewError) {
|
|
113
|
+
try {
|
|
114
|
+
log("Attempting to install jq via apt...", colors.cyan);
|
|
115
|
+
execSync('sudo apt-get install -y jq', { stdio: 'inherit' });
|
|
116
|
+
success("jq installed successfully!");
|
|
117
|
+
return 'jq';
|
|
118
|
+
} catch (aptError) {
|
|
119
|
+
throw new Error("Could not auto-install jq. Please install manually: brew install jq or sudo apt-get install jq");
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function checkDependencies() {
|
|
38
125
|
log("Checking dependencies...", colors.cyan);
|
|
39
126
|
|
|
127
|
+
let jqPath = 'jq'; // Default to system jq
|
|
128
|
+
|
|
40
129
|
// Check for jq
|
|
41
130
|
try {
|
|
42
131
|
execSync('jq --version', { stdio: 'ignore' });
|
|
43
132
|
success("jq is installed");
|
|
44
133
|
} catch (e) {
|
|
45
|
-
warning("jq not found! Attempting
|
|
134
|
+
warning("jq not found! Attempting auto-installation...");
|
|
46
135
|
|
|
47
136
|
try {
|
|
48
137
|
if (os.platform() === 'win32') {
|
|
49
|
-
|
|
50
|
-
// silent install might need admin, but let's try
|
|
51
|
-
execSync('winget install jqlang.jq --accept-source-agreements --accept-package-agreements', { stdio: 'inherit' });
|
|
52
|
-
success("jq installed successfully!");
|
|
138
|
+
jqPath = await installJqWindows();
|
|
53
139
|
} else if (os.platform() === 'darwin') {
|
|
54
|
-
|
|
55
|
-
execSync('brew install jq', { stdio: 'inherit' });
|
|
56
|
-
success("jq installed successfully!");
|
|
140
|
+
installJqUnix();
|
|
57
141
|
} else {
|
|
58
|
-
// Linux
|
|
59
|
-
|
|
142
|
+
// Linux
|
|
143
|
+
installJqUnix();
|
|
60
144
|
}
|
|
61
145
|
} catch (installError) {
|
|
62
146
|
error("Auto-installation failed.");
|
|
63
147
|
log(" Please install jq manually:", colors.yellow);
|
|
64
|
-
log(" Windows:
|
|
148
|
+
log(" Windows: Run installer again (will download automatically)", colors.yellow);
|
|
65
149
|
log(" macOS: brew install jq", colors.yellow);
|
|
66
150
|
log(" Linux: sudo apt-get install jq", colors.yellow);
|
|
151
|
+
throw installError;
|
|
67
152
|
}
|
|
68
153
|
}
|
|
69
154
|
|
|
@@ -74,6 +159,8 @@ function checkDependencies() {
|
|
|
74
159
|
} catch (e) {
|
|
75
160
|
warning("Claude Code not found in PATH.");
|
|
76
161
|
}
|
|
162
|
+
|
|
163
|
+
return jqPath;
|
|
77
164
|
}
|
|
78
165
|
|
|
79
166
|
function createDirectories() {
|
|
@@ -120,7 +207,7 @@ function installScripts() {
|
|
|
120
207
|
});
|
|
121
208
|
}
|
|
122
209
|
|
|
123
|
-
function configureSettings() {
|
|
210
|
+
function configureSettings(jqPath) {
|
|
124
211
|
log("Configuring Claude Code...", colors.cyan);
|
|
125
212
|
const home = getHomeDir();
|
|
126
213
|
const settingsFile = path.join(home, '.claude', 'settings.json');
|
|
@@ -134,18 +221,35 @@ function configureSettings() {
|
|
|
134
221
|
}
|
|
135
222
|
}
|
|
136
223
|
|
|
137
|
-
// Update statusLine config
|
|
138
|
-
// We use forward slashes for paths in settings.json even on Windows for consistency in JSON
|
|
139
|
-
// accessing existing wsl path if needed? No, standard path is fine.
|
|
140
|
-
// The previous implementation used `~/.claude/statusline.sh`.
|
|
141
|
-
// Claude might process `~`? Let's stick to what the bash script did: `~/.claude/statusline.sh`
|
|
142
|
-
|
|
143
224
|
// Configure command based on platform
|
|
144
225
|
let commandEnv = "~/.claude/statusline.sh";
|
|
145
226
|
|
|
146
227
|
if (os.platform() === 'win32') {
|
|
147
|
-
//
|
|
148
|
-
const
|
|
228
|
+
// Get the raw Windows path first
|
|
229
|
+
const scriptPathWindows = path.join(home, '.claude', 'statusline.sh');
|
|
230
|
+
|
|
231
|
+
// Convert Windows jq path to Git Bash path format
|
|
232
|
+
// C:\Users\... -> /c/Users/... (lowercase drive letter)
|
|
233
|
+
let jqPathForBash = 'jq';
|
|
234
|
+
if (jqPath && jqPath !== 'jq' && /^[A-Z]:/.test(jqPath)) {
|
|
235
|
+
jqPathForBash = jqPath.replace(/^([A-Z]):\\/, (match, drive) => `/${drive.toLowerCase()}/`).replace(/\\/g, '/');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Also convert scriptPath to Git Bash format for sourcing
|
|
239
|
+
const scriptPathGitBash = scriptPathWindows.replace(/^([A-Z]):\\/, (match, drive) => `/${drive.toLowerCase()}/`).replace(/\\/g, '/');
|
|
240
|
+
|
|
241
|
+
// Create environment variable for jq path in the script
|
|
242
|
+
const wrapperScriptPath = path.join(home, '.claude', 'statusline-wrapper.sh');
|
|
243
|
+
const wrapperContent = `#!/bin/bash
|
|
244
|
+
# Export JQ_PATH for the scripts to use
|
|
245
|
+
export JQ_PATH="${jqPathForBash}"
|
|
246
|
+
# Source the statusline script to preserve environment
|
|
247
|
+
. "${scriptPathGitBash}"
|
|
248
|
+
`;
|
|
249
|
+
fs.writeFileSync(wrapperScriptPath, wrapperContent);
|
|
250
|
+
if (os.platform() !== 'win32') {
|
|
251
|
+
fs.chmodSync(wrapperScriptPath, '755');
|
|
252
|
+
}
|
|
149
253
|
|
|
150
254
|
// Robust bash detection: try Git Bash first, then fallback to 'bash'
|
|
151
255
|
let bashPath = 'bash';
|
|
@@ -176,7 +280,18 @@ function configureSettings() {
|
|
|
176
280
|
}
|
|
177
281
|
}
|
|
178
282
|
|
|
179
|
-
|
|
283
|
+
const wrapperPathGitBash = wrapperScriptPath.replace(/^([A-Z]):\\/, (match, drive) => `/${drive.toLowerCase()}/`).replace(/\\/g, '/');
|
|
284
|
+
commandEnv = `${bashPath} "${wrapperPathGitBash}"`;
|
|
285
|
+
} else {
|
|
286
|
+
// On Unix, just set the JQ_PATH in the wrapper script
|
|
287
|
+
const wrapperScriptPath = path.join(home, '.claude', 'statusline-wrapper.sh');
|
|
288
|
+
const wrapperContent = `#!/bin/bash
|
|
289
|
+
export JQ_PATH="${jqPath}"
|
|
290
|
+
exec ~/.claude/statusline.sh
|
|
291
|
+
`;
|
|
292
|
+
fs.writeFileSync(wrapperScriptPath, wrapperContent);
|
|
293
|
+
fs.chmodSync(wrapperScriptPath, '755');
|
|
294
|
+
commandEnv = "bash ~/.claude/statusline-wrapper.sh";
|
|
180
295
|
}
|
|
181
296
|
|
|
182
297
|
settings.statusLine = {
|
|
@@ -190,16 +305,130 @@ function configureSettings() {
|
|
|
190
305
|
} catch (e) {
|
|
191
306
|
error(`Failed to update settings.json: ${e.message}`);
|
|
192
307
|
}
|
|
308
|
+
|
|
309
|
+
// 安装自定义命令文件到 ~/.claude/commands/
|
|
310
|
+
log("Installing custom commands...", colors.cyan);
|
|
311
|
+
const commandsDir = path.join(home, '.claude', 'commands');
|
|
312
|
+
|
|
313
|
+
// 确保 commands 目录存在
|
|
314
|
+
if (!fs.existsSync(commandsDir)) {
|
|
315
|
+
fs.mkdirSync(commandsDir, { recursive: true });
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const commands = [
|
|
319
|
+
{
|
|
320
|
+
name: 'burn-your-money-stats.md',
|
|
321
|
+
content: `---
|
|
322
|
+
name: burn-your-money-stats
|
|
323
|
+
description: 📊 查看 token 使用趋势图
|
|
324
|
+
---
|
|
325
|
+
|
|
326
|
+
# Token 使用趋势图
|
|
327
|
+
|
|
328
|
+
请执行以下 bash 命令查看过去 7 天的 token 使用趋势图:
|
|
329
|
+
|
|
330
|
+
\\\`\\\`\\\`bash
|
|
331
|
+
bash ~/.claude/scripts/token-history.sh chart
|
|
332
|
+
\\\`\\\`\\\`
|
|
333
|
+
`
|
|
334
|
+
},
|
|
335
|
+
{
|
|
336
|
+
name: 'burn-your-money-today.md',
|
|
337
|
+
content: `---
|
|
338
|
+
name: burn-your-money-today
|
|
339
|
+
description: 📅 查看今日 token 使用情况
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
# 今日 Token 使用情况
|
|
343
|
+
|
|
344
|
+
请执行以下 bash 命令查看今日的 token 使用量:
|
|
345
|
+
|
|
346
|
+
\\\`\\\`\\\`bash
|
|
347
|
+
bash ~/.claude/scripts/token-history.sh today_tokens
|
|
348
|
+
\\\`\\\`\\\`
|
|
349
|
+
`
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
name: 'burn-your-money-week.md',
|
|
353
|
+
content: `---
|
|
354
|
+
name: burn-your-money-week
|
|
355
|
+
description: 📆 查看本周 token 使用情况
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
# 本周 Token 使用情况
|
|
359
|
+
|
|
360
|
+
请执行以下 bash 命令查看本周的 token 使用量:
|
|
361
|
+
|
|
362
|
+
\\\`\\\`\\\`bash
|
|
363
|
+
bash ~/.claude/scripts/token-history.sh week_tokens
|
|
364
|
+
\\\`\\\`\\\`
|
|
365
|
+
`
|
|
366
|
+
},
|
|
367
|
+
{
|
|
368
|
+
name: 'burn-your-money-month.md',
|
|
369
|
+
content: `---
|
|
370
|
+
name: burn-your-money-month
|
|
371
|
+
description: 🗓️ 查看本月 token 使用情况
|
|
372
|
+
---
|
|
373
|
+
|
|
374
|
+
# 本月 Token 使用情况
|
|
375
|
+
|
|
376
|
+
请执行以下 bash 命令查看本月的 token 使用量:
|
|
377
|
+
|
|
378
|
+
\\\`\\\`\\\`bash
|
|
379
|
+
bash ~/.claude/scripts/token-history.sh month_tokens
|
|
380
|
+
\\\`\\\`\\\`
|
|
381
|
+
`
|
|
382
|
+
},
|
|
383
|
+
{
|
|
384
|
+
name: 'burn-your-money-export.md',
|
|
385
|
+
content: `---
|
|
386
|
+
name: burn-your-money-export
|
|
387
|
+
description: 💾 导出 token 数据(JSON)
|
|
388
|
+
---
|
|
389
|
+
|
|
390
|
+
# 导出 Token 数据
|
|
391
|
+
|
|
392
|
+
请执行以下 bash 命令导出 token 数据为 JSON 格式:
|
|
393
|
+
|
|
394
|
+
\\\`\\\`\\\`bash
|
|
395
|
+
bash ~/.claude/scripts/token-history.sh export json
|
|
396
|
+
\\\`\\\`\\\`
|
|
397
|
+
`
|
|
398
|
+
},
|
|
399
|
+
{
|
|
400
|
+
name: 'burn-your-money-export-csv.md',
|
|
401
|
+
content: `---
|
|
402
|
+
name: burn-your-money-export-csv
|
|
403
|
+
description: 📄 导出 token 数据(CSV)
|
|
404
|
+
---
|
|
405
|
+
|
|
406
|
+
# 导出 Token 数据 (CSV)
|
|
407
|
+
|
|
408
|
+
请执行以下 bash 命令导出 token 数据为 CSV 格式:
|
|
409
|
+
|
|
410
|
+
\\\`\\\`\\\`bash
|
|
411
|
+
bash ~/.claude/scripts/token-history.sh export csv
|
|
412
|
+
\\\`\\\`\\\`
|
|
413
|
+
`
|
|
414
|
+
}
|
|
415
|
+
];
|
|
416
|
+
|
|
417
|
+
commands.forEach(cmd => {
|
|
418
|
+
const cmdPath = path.join(commandsDir, cmd.name);
|
|
419
|
+
fs.writeFileSync(cmdPath, cmd.content);
|
|
420
|
+
success(`Installed ${cmd.name}`);
|
|
421
|
+
});
|
|
193
422
|
}
|
|
194
423
|
|
|
195
|
-
function main() {
|
|
424
|
+
async function main() {
|
|
196
425
|
log("\n💸 Burn Your Money - Installer\n", colors.purple);
|
|
197
426
|
|
|
198
427
|
try {
|
|
199
|
-
checkDependencies();
|
|
428
|
+
const jqPath = await checkDependencies();
|
|
200
429
|
createDirectories();
|
|
201
430
|
installScripts();
|
|
202
|
-
configureSettings();
|
|
431
|
+
configureSettings(jqPath);
|
|
203
432
|
|
|
204
433
|
log("\n✅ Installation complete!", colors.green);
|
|
205
434
|
log("Please restart Claude Code to see the status bar.\n", colors.cyan);
|
package/package.json
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@winston.wan/burn-your-money",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "💸 Burn Your Money - 实时显示 Claude Code 的 token 消耗,看着你的钱包燃烧!",
|
|
5
5
|
"main": "src/statusline.sh",
|
|
6
6
|
"bin": {
|
|
7
7
|
"burn-your-money": "./bin/burn-your-money"
|
|
8
8
|
},
|
|
9
9
|
"scripts": {
|
|
10
|
-
"
|
|
10
|
+
"postinstall": "node install.js",
|
|
11
11
|
"uninstall": "node uninstall.js",
|
|
12
12
|
"test": "bash ./test.sh"
|
|
13
13
|
},
|