vibeusage 0.2.11 → 0.2.13
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 +16 -4
- package/README.zh-CN.md +16 -4
- package/package.json +2 -3
- package/src/cli.js +5 -0
- package/src/commands/doctor.js +91 -0
- package/src/commands/init.js +24 -55
- package/src/commands/status.js +1 -1
- package/src/commands/sync.js +14 -12
- package/src/commands/uninstall.js +1 -5
- package/src/lib/browser-auth.js +1 -1
- package/src/lib/debug-flags.js +1 -3
- package/src/lib/diagnostics.js +1 -1
- package/src/lib/doctor.js +343 -0
- package/src/lib/fs.js +16 -1
- package/src/lib/insforge-client.js +2 -7
- package/src/lib/insforge.js +1 -1
- package/src/lib/runtime-config.js +120 -0
- package/src/lib/tracker-paths.js +4 -55
- package/src/lib/uploader.js +1 -1
- package/src/lib/{vibescore-api.js → vibeusage-api.js} +4 -4
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
# 🟢
|
|
3
|
+
# 🟢 VIBEUSAGE
|
|
4
4
|
|
|
5
5
|
**QUANTIFY YOUR AI OUTPUT**
|
|
6
6
|
_Real-time AI Analytics for Codex CLI_
|
|
@@ -81,6 +81,19 @@ npx --yes vibeusage sync
|
|
|
81
81
|
npx --yes vibeusage status
|
|
82
82
|
````
|
|
83
83
|
|
|
84
|
+
### Doctor
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# Run health checks
|
|
88
|
+
npx --yes vibeusage doctor
|
|
89
|
+
|
|
90
|
+
# Emit JSON report (and write to file)
|
|
91
|
+
npx --yes vibeusage doctor --json --out doctor.json
|
|
92
|
+
|
|
93
|
+
# Override base URL (diagnostics only)
|
|
94
|
+
npx --yes vibeusage doctor --base-url https://example.invalid
|
|
95
|
+
```
|
|
96
|
+
|
|
84
97
|
### Sources
|
|
85
98
|
|
|
86
99
|
- Codex CLI logs: `~/.codex/sessions/**/rollout-*.jsonl` (override with `CODEX_HOME`)
|
|
@@ -89,10 +102,9 @@ npx --yes vibeusage status
|
|
|
89
102
|
|
|
90
103
|
## 🔧 Environment Variables
|
|
91
104
|
|
|
92
|
-
- `
|
|
93
|
-
- `
|
|
105
|
+
- `VIBEUSAGE_HTTP_TIMEOUT_MS`: CLI HTTP timeout in ms (default `20000`, `0` disables, clamped to `1000..120000`).
|
|
106
|
+
- `VITE_VIBEUSAGE_HTTP_TIMEOUT_MS`: Dashboard request timeout in ms (default `15000`, `0` disables, clamped to `1000..30000`).
|
|
94
107
|
- `VIBEUSAGE_ROLLUP_ENABLED`: Currently ignored; rollup aggregation is disabled in code until the daily rollup table is deployed.
|
|
95
|
-
- `VIBESCORE_ROLLUP_ENABLED`: Legacy alias for `VIBEUSAGE_ROLLUP_ENABLED` (ignored).
|
|
96
108
|
- `GEMINI_HOME`: Override Gemini CLI home (defaults to `~/.gemini`).
|
|
97
109
|
|
|
98
110
|
## 🧰 Troubleshooting
|
package/README.zh-CN.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
<div align="center">
|
|
2
2
|
|
|
3
|
-
# 🟢
|
|
3
|
+
# 🟢 VIBEUSAGE
|
|
4
4
|
|
|
5
5
|
**量化你的 AI 产出**
|
|
6
6
|
_Codex CLI 实时 AI 分析工具_
|
|
@@ -81,6 +81,19 @@ npx --yes vibeusage sync
|
|
|
81
81
|
npx --yes vibeusage status
|
|
82
82
|
````
|
|
83
83
|
|
|
84
|
+
### Doctor
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
# 运行健康检查
|
|
88
|
+
npx --yes vibeusage doctor
|
|
89
|
+
|
|
90
|
+
# 输出 JSON 报告(同时写文件)
|
|
91
|
+
npx --yes vibeusage doctor --json --out doctor.json
|
|
92
|
+
|
|
93
|
+
# 覆盖 base URL(仅诊断)
|
|
94
|
+
npx --yes vibeusage doctor --base-url https://example.invalid
|
|
95
|
+
```
|
|
96
|
+
|
|
84
97
|
### 日志来源
|
|
85
98
|
|
|
86
99
|
- Codex CLI 日志:`~/.codex/sessions/**/rollout-*.jsonl`(可用 `CODEX_HOME` 覆盖)
|
|
@@ -89,10 +102,9 @@ npx --yes vibeusage status
|
|
|
89
102
|
|
|
90
103
|
## 🔧 环境变量
|
|
91
104
|
|
|
92
|
-
- `
|
|
93
|
-
- `
|
|
105
|
+
- `VIBEUSAGE_HTTP_TIMEOUT_MS`:CLI 请求超时(毫秒,默认 `20000`,`0` 表示关闭,范围 `1000..120000`)。
|
|
106
|
+
- `VITE_VIBEUSAGE_HTTP_TIMEOUT_MS`:Dashboard 请求超时(毫秒,默认 `15000`,`0` 表示关闭,范围 `1000..30000`)。
|
|
94
107
|
- `VIBEUSAGE_ROLLUP_ENABLED`:当前被忽略,rollup 聚合在代码层禁用,等待 rollup 表部署完成后再恢复。
|
|
95
|
-
- `VIBESCORE_ROLLUP_ENABLED`:`VIBEUSAGE_ROLLUP_ENABLED` 的兼容别名(同样无效)。
|
|
96
108
|
- `GEMINI_HOME`:覆盖 Gemini CLI 的 home(默认 `~/.gemini`)。
|
|
97
109
|
|
|
98
110
|
## 🧰 常见问题
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibeusage",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.13",
|
|
4
4
|
"description": "Codex CLI token usage tracker (macOS-first, notify-driven).",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -27,8 +27,7 @@
|
|
|
27
27
|
"bin": {
|
|
28
28
|
"tracker": "bin/tracker.js",
|
|
29
29
|
"vibeusage": "bin/tracker.js",
|
|
30
|
-
"vibeusage-tracker": "bin/tracker.js"
|
|
31
|
-
"vibescore-tracker": "bin/tracker.js"
|
|
30
|
+
"vibeusage-tracker": "bin/tracker.js"
|
|
32
31
|
},
|
|
33
32
|
"files": [
|
|
34
33
|
"bin/",
|
package/src/cli.js
CHANGED
|
@@ -2,6 +2,7 @@ const { cmdInit } = require('./commands/init');
|
|
|
2
2
|
const { cmdSync } = require('./commands/sync');
|
|
3
3
|
const { cmdStatus } = require('./commands/status');
|
|
4
4
|
const { cmdDiagnostics } = require('./commands/diagnostics');
|
|
5
|
+
const { cmdDoctor } = require('./commands/doctor');
|
|
5
6
|
const { cmdUninstall } = require('./commands/uninstall');
|
|
6
7
|
|
|
7
8
|
async function run(argv) {
|
|
@@ -25,6 +26,9 @@ async function run(argv) {
|
|
|
25
26
|
case 'diagnostics':
|
|
26
27
|
await cmdDiagnostics(rest);
|
|
27
28
|
return;
|
|
29
|
+
case 'doctor':
|
|
30
|
+
await cmdDoctor(rest);
|
|
31
|
+
return;
|
|
28
32
|
case 'uninstall':
|
|
29
33
|
await cmdUninstall(rest);
|
|
30
34
|
return;
|
|
@@ -44,6 +48,7 @@ function printHelp() {
|
|
|
44
48
|
' npx vibeusage [--debug] sync [--auto] [--drain]',
|
|
45
49
|
' npx vibeusage [--debug] status',
|
|
46
50
|
' npx vibeusage [--debug] diagnostics [--out diagnostics.json]',
|
|
51
|
+
' npx vibeusage [--debug] doctor [--json] [--out doctor.json] [--base-url <url>]',
|
|
47
52
|
' npx vibeusage [--debug] uninstall [--purge]',
|
|
48
53
|
'',
|
|
49
54
|
'Notes:',
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const os = require('node:os');
|
|
2
|
+
const path = require('node:path');
|
|
3
|
+
|
|
4
|
+
const { readJsonStrict, writeFileAtomic, chmod600IfPossible } = require('../lib/fs');
|
|
5
|
+
const { resolveTrackerPaths } = require('../lib/tracker-paths');
|
|
6
|
+
const { collectTrackerDiagnostics } = require('../lib/diagnostics');
|
|
7
|
+
const { resolveRuntimeConfig } = require('../lib/runtime-config');
|
|
8
|
+
const { buildDoctorReport } = require('../lib/doctor');
|
|
9
|
+
|
|
10
|
+
async function cmdDoctor(argv = []) {
|
|
11
|
+
const opts = parseArgs(argv);
|
|
12
|
+
const home = os.homedir();
|
|
13
|
+
const { trackerDir } = await resolveTrackerPaths({ home });
|
|
14
|
+
const configPath = path.join(trackerDir, 'config.json');
|
|
15
|
+
|
|
16
|
+
const configStatus = await readJsonStrict(configPath);
|
|
17
|
+
const config = configStatus.status === 'ok' && isPlainObject(configStatus.value) ? configStatus.value : {};
|
|
18
|
+
const runtime = resolveRuntimeConfig({ cli: { baseUrl: opts.baseUrl }, config, env: process.env });
|
|
19
|
+
const diagnostics = await collectTrackerDiagnostics({ home });
|
|
20
|
+
const cliPath = process.argv[1] ? path.resolve(process.argv[1]) : null;
|
|
21
|
+
|
|
22
|
+
const report = await buildDoctorReport({
|
|
23
|
+
runtime,
|
|
24
|
+
diagnostics,
|
|
25
|
+
fetch: globalThis.fetch,
|
|
26
|
+
paths: {
|
|
27
|
+
trackerDir,
|
|
28
|
+
configPath,
|
|
29
|
+
cliPath
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const jsonOutput = opts.json || Boolean(opts.out);
|
|
34
|
+
const payload = JSON.stringify(report, null, jsonOutput ? 2 : 0) + '\n';
|
|
35
|
+
|
|
36
|
+
if (opts.out) {
|
|
37
|
+
const outPath = path.resolve(process.cwd(), opts.out);
|
|
38
|
+
await writeFileAtomic(outPath, payload);
|
|
39
|
+
await chmod600IfPossible(outPath);
|
|
40
|
+
process.stderr.write(`Wrote doctor report to: ${outPath}\n`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (jsonOutput) {
|
|
44
|
+
process.stdout.write(payload);
|
|
45
|
+
} else {
|
|
46
|
+
process.stdout.write(renderHumanReport(report));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (report.summary.critical > 0) {
|
|
50
|
+
process.exitCode = 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function parseArgs(argv) {
|
|
55
|
+
const out = { json: false, out: null, baseUrl: null };
|
|
56
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
57
|
+
const arg = argv[i];
|
|
58
|
+
if (arg === '--json') out.json = true;
|
|
59
|
+
else if (arg === '--out') out.out = argv[++i] || null;
|
|
60
|
+
else if (arg === '--base-url') out.baseUrl = argv[++i] || null;
|
|
61
|
+
else throw new Error(`Unknown option: ${arg}`);
|
|
62
|
+
}
|
|
63
|
+
return out;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function renderHumanReport(report) {
|
|
67
|
+
const lines = [];
|
|
68
|
+
lines.push('Doctor report');
|
|
69
|
+
lines.push('');
|
|
70
|
+
for (const check of report.checks || []) {
|
|
71
|
+
lines.push(formatCheckLine(check));
|
|
72
|
+
}
|
|
73
|
+
lines.push('');
|
|
74
|
+
lines.push(
|
|
75
|
+
`Summary: ok ${report.summary.ok} | warn ${report.summary.warn} | fail ${report.summary.fail} | critical ${report.summary.critical}`
|
|
76
|
+
);
|
|
77
|
+
lines.push('');
|
|
78
|
+
return lines.join('\n');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function formatCheckLine(check = {}) {
|
|
82
|
+
const status = String(check.status || 'unknown').toUpperCase();
|
|
83
|
+
const detail = check.detail ? ` - ${check.detail}` : '';
|
|
84
|
+
return `- [${status}] ${check.id || 'unknown'}${detail}`;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isPlainObject(value) {
|
|
88
|
+
return Boolean(value && typeof value === 'object' && !Array.isArray(value));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
module.exports = { cmdDoctor };
|
package/src/commands/init.js
CHANGED
|
@@ -29,6 +29,7 @@ const {
|
|
|
29
29
|
issueDeviceTokenWithLinkCode
|
|
30
30
|
} = require('../lib/insforge');
|
|
31
31
|
const { resolveTrackerPaths } = require('../lib/tracker-paths');
|
|
32
|
+
const { resolveRuntimeConfig } = require('../lib/runtime-config');
|
|
32
33
|
const {
|
|
33
34
|
BOLD,
|
|
34
35
|
DIM,
|
|
@@ -51,25 +52,26 @@ const ASCII_LOGO = [
|
|
|
51
52
|
].join('\n');
|
|
52
53
|
|
|
53
54
|
const DIVIDER = '----------------------------------------------';
|
|
55
|
+
const DEFAULT_DASHBOARD_URL = 'https://www.vibeusage.cc';
|
|
54
56
|
|
|
55
57
|
async function cmdInit(argv) {
|
|
56
58
|
const opts = parseArgs(argv);
|
|
57
59
|
const home = os.homedir();
|
|
58
60
|
|
|
59
|
-
const { rootDir, trackerDir, binDir } = await resolveTrackerPaths({ home
|
|
61
|
+
const { rootDir, trackerDir, binDir } = await resolveTrackerPaths({ home });
|
|
60
62
|
|
|
61
63
|
const configPath = path.join(trackerDir, 'config.json');
|
|
62
64
|
const notifyOriginalPath = path.join(trackerDir, 'codex_notify_original.json');
|
|
63
65
|
const linkCodeStatePath = path.join(trackerDir, 'link_code_state.json');
|
|
64
66
|
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
const existingConfig = await readJson(configPath);
|
|
68
|
+
const runtime = resolveRuntimeConfig({
|
|
69
|
+
cli: { baseUrl: opts.baseUrl, dashboardUrl: opts.dashboardUrl },
|
|
70
|
+
config: existingConfig || {},
|
|
71
|
+
env: process.env
|
|
72
|
+
});
|
|
73
|
+
const baseUrl = runtime.baseUrl;
|
|
74
|
+
let dashboardUrl = runtime.dashboardUrl || DEFAULT_DASHBOARD_URL;
|
|
73
75
|
const notifyPath = path.join(binDir, 'notify.cjs');
|
|
74
76
|
const appDir = path.join(trackerDir, 'app');
|
|
75
77
|
const trackerBinPath = path.join(appDir, 'bin', 'tracker.js');
|
|
@@ -98,8 +100,8 @@ async function cmdInit(argv) {
|
|
|
98
100
|
opts,
|
|
99
101
|
home,
|
|
100
102
|
trackerDir,
|
|
101
|
-
|
|
102
|
-
|
|
103
|
+
notifyPath,
|
|
104
|
+
runtime
|
|
103
105
|
});
|
|
104
106
|
renderLocalReport({ summary: preview.summary, isDryRun: true });
|
|
105
107
|
if (preview.pendingBrowserAuth) {
|
|
@@ -125,7 +127,9 @@ async function cmdInit(argv) {
|
|
|
125
127
|
linkCodeStatePath,
|
|
126
128
|
notifyPath,
|
|
127
129
|
appDir,
|
|
128
|
-
trackerBinPath
|
|
130
|
+
trackerBinPath,
|
|
131
|
+
runtime,
|
|
132
|
+
existingConfig
|
|
129
133
|
});
|
|
130
134
|
} catch (err) {
|
|
131
135
|
spinner.stop();
|
|
@@ -140,7 +144,6 @@ async function cmdInit(argv) {
|
|
|
140
144
|
|
|
141
145
|
if (setup.pendingBrowserAuth) {
|
|
142
146
|
const deviceName = opts.deviceName || os.hostname();
|
|
143
|
-
if (!dashboardUrl) dashboardUrl = await detectLocalDashboardUrl();
|
|
144
147
|
const flow = await beginBrowserAuth({ baseUrl, dashboardUrl, timeoutMs: 10 * 60_000, open: false });
|
|
145
148
|
const canAutoOpen = !opts.noOpen;
|
|
146
149
|
renderAuthTransition({ authUrl: flow.authUrl, canAutoOpen });
|
|
@@ -157,7 +160,6 @@ async function cmdInit(argv) {
|
|
|
157
160
|
const resolvedDashboardUrl = dashboardUrl || null;
|
|
158
161
|
renderSuccessBox({ configPath, dashboardUrl: resolvedDashboardUrl });
|
|
159
162
|
} else if (deviceToken) {
|
|
160
|
-
if (!dashboardUrl) dashboardUrl = await detectLocalDashboardUrl();
|
|
161
163
|
const resolvedDashboardUrl = dashboardUrl || null;
|
|
162
164
|
renderSuccessBox({ configPath, dashboardUrl: resolvedDashboardUrl });
|
|
163
165
|
} else {
|
|
@@ -209,10 +211,8 @@ function shouldUseBrowserAuth({ deviceToken, opts }) {
|
|
|
209
211
|
return true;
|
|
210
212
|
}
|
|
211
213
|
|
|
212
|
-
async function buildDryRunSummary({ opts, home, trackerDir,
|
|
213
|
-
const
|
|
214
|
-
const deviceTokenFromEnv = process.env.VIBEUSAGE_DEVICE_TOKEN || process.env.VIBESCORE_DEVICE_TOKEN || null;
|
|
215
|
-
const deviceToken = deviceTokenFromEnv || existingConfig?.deviceToken || null;
|
|
214
|
+
async function buildDryRunSummary({ opts, home, trackerDir, notifyPath, runtime }) {
|
|
215
|
+
const deviceToken = runtime?.deviceToken || null;
|
|
216
216
|
const pendingBrowserAuth = shouldUseBrowserAuth({ deviceToken, opts });
|
|
217
217
|
const context = buildIntegrationTargets({ home, trackerDir, notifyPath });
|
|
218
218
|
const summary = await previewIntegrations({ context });
|
|
@@ -230,15 +230,13 @@ async function runSetup({
|
|
|
230
230
|
linkCodeStatePath,
|
|
231
231
|
notifyPath,
|
|
232
232
|
appDir,
|
|
233
|
-
trackerBinPath
|
|
233
|
+
trackerBinPath,
|
|
234
|
+
runtime,
|
|
235
|
+
existingConfig
|
|
234
236
|
}) {
|
|
235
237
|
await ensureDir(trackerDir);
|
|
236
238
|
await ensureDir(binDir);
|
|
237
|
-
|
|
238
|
-
const existingConfig = await readJson(configPath);
|
|
239
|
-
const deviceTokenFromEnv = process.env.VIBEUSAGE_DEVICE_TOKEN || process.env.VIBESCORE_DEVICE_TOKEN || null;
|
|
240
|
-
|
|
241
|
-
let deviceToken = deviceTokenFromEnv || existingConfig?.deviceToken || null;
|
|
239
|
+
let deviceToken = runtime?.deviceToken || null;
|
|
242
240
|
let deviceId = existingConfig?.deviceId || null;
|
|
243
241
|
const installedAt = existingConfig?.installedAt || new Date().toISOString();
|
|
244
242
|
let pendingBrowserAuth = false;
|
|
@@ -611,7 +609,7 @@ try {
|
|
|
611
609
|
// Throttle spawn: at most once per 20 seconds.
|
|
612
610
|
try {
|
|
613
611
|
const throttlePath = path.join(trackerDir, 'sync.throttle');
|
|
614
|
-
let deviceToken = process.env.VIBEUSAGE_DEVICE_TOKEN ||
|
|
612
|
+
let deviceToken = process.env.VIBEUSAGE_DEVICE_TOKEN || null;
|
|
615
613
|
if (!deviceToken) {
|
|
616
614
|
try {
|
|
617
615
|
const cfg = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
@@ -686,35 +684,6 @@ function isSelfNotify(cmd) {
|
|
|
686
684
|
|
|
687
685
|
module.exports = { cmdInit };
|
|
688
686
|
|
|
689
|
-
async function detectLocalDashboardUrl() {
|
|
690
|
-
// Dev-only convenience: prefer a local dashboard (if running) so the user sees our own UI first.
|
|
691
|
-
// Vite defaults to 5173, but may auto-increment if the port is taken.
|
|
692
|
-
const hosts = ['127.0.0.1', 'localhost'];
|
|
693
|
-
const ports = [5173, 5174, 5175, 5176, 5177];
|
|
694
|
-
|
|
695
|
-
for (const port of ports) {
|
|
696
|
-
for (const host of hosts) {
|
|
697
|
-
const base = `http://${host}:${port}`;
|
|
698
|
-
const ok = await checkUrlReachable(base);
|
|
699
|
-
if (ok) return base;
|
|
700
|
-
}
|
|
701
|
-
}
|
|
702
|
-
return null;
|
|
703
|
-
}
|
|
704
|
-
|
|
705
|
-
async function checkUrlReachable(url) {
|
|
706
|
-
const timeoutMs = 250;
|
|
707
|
-
try {
|
|
708
|
-
const controller = new AbortController();
|
|
709
|
-
const t = setTimeout(() => controller.abort(), timeoutMs);
|
|
710
|
-
const res = await fetch(url, { method: 'GET', signal: controller.signal });
|
|
711
|
-
clearTimeout(t);
|
|
712
|
-
return Boolean(res && res.ok);
|
|
713
|
-
} catch (_e) {
|
|
714
|
-
return false;
|
|
715
|
-
}
|
|
716
|
-
}
|
|
717
|
-
|
|
718
687
|
async function probeFile(p) {
|
|
719
688
|
try {
|
|
720
689
|
const st = await fs.stat(p);
|
|
@@ -794,5 +763,5 @@ async function copyRuntimeDependencies({ from, to }) {
|
|
|
794
763
|
}
|
|
795
764
|
|
|
796
765
|
function isDebugEnabled() {
|
|
797
|
-
return process.env.VIBEUSAGE_DEBUG === '1'
|
|
766
|
+
return process.env.VIBEUSAGE_DEBUG === '1';
|
|
798
767
|
}
|
package/src/commands/status.js
CHANGED
|
@@ -25,7 +25,7 @@ async function cmdStatus(argv = []) {
|
|
|
25
25
|
}
|
|
26
26
|
|
|
27
27
|
const home = os.homedir();
|
|
28
|
-
const { trackerDir, binDir } = await resolveTrackerPaths({ home
|
|
28
|
+
const { trackerDir, binDir } = await resolveTrackerPaths({ home });
|
|
29
29
|
const configPath = path.join(trackerDir, 'config.json');
|
|
30
30
|
const queuePath = path.join(trackerDir, 'queue.jsonl');
|
|
31
31
|
const queueStatePath = path.join(trackerDir, 'queue.state.json');
|
package/src/commands/sync.js
CHANGED
|
@@ -16,7 +16,7 @@ const {
|
|
|
16
16
|
} = require('../lib/rollout');
|
|
17
17
|
const { drainQueueToCloud } = require('../lib/uploader');
|
|
18
18
|
const { createProgress, renderBar, formatNumber, formatBytes } = require('../lib/progress');
|
|
19
|
-
const { syncHeartbeat } = require('../lib/
|
|
19
|
+
const { syncHeartbeat } = require('../lib/vibeusage-api');
|
|
20
20
|
const {
|
|
21
21
|
DEFAULTS: UPLOAD_DEFAULTS,
|
|
22
22
|
normalizeState: normalizeUploadState,
|
|
@@ -25,11 +25,12 @@ const {
|
|
|
25
25
|
recordUploadFailure
|
|
26
26
|
} = require('../lib/upload-throttle');
|
|
27
27
|
const { resolveTrackerPaths } = require('../lib/tracker-paths');
|
|
28
|
+
const { resolveRuntimeConfig } = require('../lib/runtime-config');
|
|
28
29
|
|
|
29
30
|
async function cmdSync(argv) {
|
|
30
31
|
const opts = parseArgs(argv);
|
|
31
32
|
const home = os.homedir();
|
|
32
|
-
const { trackerDir } = await resolveTrackerPaths({ home
|
|
33
|
+
const { trackerDir } = await resolveTrackerPaths({ home });
|
|
33
34
|
|
|
34
35
|
await ensureDir(trackerDir);
|
|
35
36
|
|
|
@@ -169,11 +170,9 @@ async function cmdSync(argv) {
|
|
|
169
170
|
|
|
170
171
|
progress?.stop();
|
|
171
172
|
|
|
172
|
-
const
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
process.env.VIBESCORE_INSFORGE_BASE_URL ||
|
|
176
|
-
'https://5tmappuk.us-east.insforge.app';
|
|
173
|
+
const runtime = resolveRuntimeConfig({ config: config || {}, env: process.env });
|
|
174
|
+
const deviceToken = runtime.deviceToken;
|
|
175
|
+
const baseUrl = runtime.baseUrl;
|
|
177
176
|
|
|
178
177
|
let uploadResult = null;
|
|
179
178
|
let uploadAttempted = false;
|
|
@@ -202,7 +201,8 @@ async function cmdSync(argv) {
|
|
|
202
201
|
retryAtMs: autoDecision.blockedUntilMs,
|
|
203
202
|
reason,
|
|
204
203
|
pendingBytes,
|
|
205
|
-
source: 'auto-throttled'
|
|
204
|
+
source: 'auto-throttled',
|
|
205
|
+
autoRetryNoSpawn: runtime.autoRetryNoSpawn
|
|
206
206
|
});
|
|
207
207
|
}
|
|
208
208
|
}
|
|
@@ -251,7 +251,8 @@ async function cmdSync(argv) {
|
|
|
251
251
|
retryAtMs,
|
|
252
252
|
reason: 'backoff',
|
|
253
253
|
pendingBytes,
|
|
254
|
-
source: 'auto-error'
|
|
254
|
+
source: 'auto-error',
|
|
255
|
+
autoRetryNoSpawn: runtime.autoRetryNoSpawn
|
|
255
256
|
});
|
|
256
257
|
}
|
|
257
258
|
}
|
|
@@ -278,7 +279,8 @@ async function cmdSync(argv) {
|
|
|
278
279
|
retryAtMs,
|
|
279
280
|
reason: 'backlog',
|
|
280
281
|
pendingBytes,
|
|
281
|
-
source: 'auto-backlog'
|
|
282
|
+
source: 'auto-backlog',
|
|
283
|
+
autoRetryNoSpawn: runtime.autoRetryNoSpawn
|
|
282
284
|
});
|
|
283
285
|
}
|
|
284
286
|
}
|
|
@@ -385,7 +387,7 @@ function deriveAutoSkipReason({ decision, state }) {
|
|
|
385
387
|
return 'throttled';
|
|
386
388
|
}
|
|
387
389
|
|
|
388
|
-
async function scheduleAutoRetry({ trackerDir, retryAtMs, reason, pendingBytes, source }) {
|
|
390
|
+
async function scheduleAutoRetry({ trackerDir, retryAtMs, reason, pendingBytes, source, autoRetryNoSpawn }) {
|
|
389
391
|
const retryMs = coerceRetryMs(retryAtMs);
|
|
390
392
|
if (!retryMs) return { scheduled: false, retryAtMs: 0 };
|
|
391
393
|
|
|
@@ -411,7 +413,7 @@ async function scheduleAutoRetry({ trackerDir, retryAtMs, reason, pendingBytes,
|
|
|
411
413
|
|
|
412
414
|
const delayMs = Math.min(AUTO_RETRY_MAX_DELAY_MS, Math.max(0, retryMs - nowMs));
|
|
413
415
|
if (delayMs <= 0) return { scheduled: false, retryAtMs: retryMs };
|
|
414
|
-
if (
|
|
416
|
+
if (autoRetryNoSpawn) {
|
|
415
417
|
return { scheduled: false, retryAtMs: retryMs };
|
|
416
418
|
}
|
|
417
419
|
|
|
@@ -16,10 +16,7 @@ const { resolveTrackerPaths } = require('../lib/tracker-paths');
|
|
|
16
16
|
async function cmdUninstall(argv) {
|
|
17
17
|
const opts = parseArgs(argv);
|
|
18
18
|
const home = os.homedir();
|
|
19
|
-
const { trackerDir, binDir
|
|
20
|
-
home,
|
|
21
|
-
migrate: false
|
|
22
|
-
});
|
|
19
|
+
const { trackerDir, binDir } = await resolveTrackerPaths({ home });
|
|
23
20
|
const codexHome = process.env.CODEX_HOME || path.join(home, '.codex');
|
|
24
21
|
const codexConfigPath = path.join(codexHome, 'config.toml');
|
|
25
22
|
const codeHome = process.env.CODE_HOME || path.join(home, '.code');
|
|
@@ -73,7 +70,6 @@ async function cmdUninstall(argv) {
|
|
|
73
70
|
|
|
74
71
|
if (opts.purge) {
|
|
75
72
|
await fs.rm(path.join(home, '.vibeusage'), { recursive: true, force: true }).catch(() => {});
|
|
76
|
-
await fs.rm(legacyRootDir, { recursive: true, force: true }).catch(() => {});
|
|
77
73
|
}
|
|
78
74
|
|
|
79
75
|
process.stdout.write(
|
package/src/lib/browser-auth.js
CHANGED
|
@@ -6,7 +6,7 @@ const DEFAULT_BASE_URL = 'https://5tmappuk.us-east.insforge.app';
|
|
|
6
6
|
|
|
7
7
|
async function beginBrowserAuth({ baseUrl, dashboardUrl, timeoutMs, open }) {
|
|
8
8
|
const nonce = crypto.randomBytes(16).toString('hex');
|
|
9
|
-
const callbackPath = `/
|
|
9
|
+
const callbackPath = `/vibeusage/callback/${nonce}`;
|
|
10
10
|
const authUrl = dashboardUrl ? new URL('/', dashboardUrl) : new URL('/auth/sign-up', baseUrl);
|
|
11
11
|
const postAuthRedirect = resolvePostAuthRedirect({ dashboardUrl, authUrl });
|
|
12
12
|
const { callbackUrl, waitForCallback } = await startLocalCallbackServer({
|
package/src/lib/debug-flags.js
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
function stripDebugFlag(argv, env = process.env) {
|
|
2
2
|
const filtered = Array.isArray(argv) ? argv.filter((arg) => arg !== '--debug') : [];
|
|
3
|
-
const debugEnv =
|
|
4
|
-
String(env?.VIBEUSAGE_DEBUG || '') === '1' ||
|
|
5
|
-
String(env?.VIBESCORE_DEBUG || '') === '1';
|
|
3
|
+
const debugEnv = String(env?.VIBEUSAGE_DEBUG || '') === '1';
|
|
6
4
|
return { argv: filtered, debug: filtered.length !== (argv || []).length || debugEnv };
|
|
7
5
|
}
|
|
8
6
|
|
package/src/lib/diagnostics.js
CHANGED
|
@@ -20,7 +20,7 @@ async function collectTrackerDiagnostics({
|
|
|
20
20
|
codexHome = process.env.CODEX_HOME || path.join(home, '.codex'),
|
|
21
21
|
codeHome = process.env.CODE_HOME || path.join(home, '.code')
|
|
22
22
|
} = {}) {
|
|
23
|
-
const { trackerDir, binDir } = await resolveTrackerPaths({ home
|
|
23
|
+
const { trackerDir, binDir } = await resolveTrackerPaths({ home });
|
|
24
24
|
const configPath = path.join(trackerDir, 'config.json');
|
|
25
25
|
const queuePath = path.join(trackerDir, 'queue.jsonl');
|
|
26
26
|
const queueStatePath = path.join(trackerDir, 'queue.state.json');
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
const fs = require('node:fs/promises');
|
|
2
|
+
const { constants } = require('node:fs');
|
|
3
|
+
|
|
4
|
+
const { readJsonStrict } = require('./fs');
|
|
5
|
+
|
|
6
|
+
async function buildDoctorReport({
|
|
7
|
+
runtime = {},
|
|
8
|
+
diagnostics = null,
|
|
9
|
+
fetch = globalThis.fetch,
|
|
10
|
+
now = () => new Date(),
|
|
11
|
+
paths = {}
|
|
12
|
+
} = {}) {
|
|
13
|
+
const checks = [];
|
|
14
|
+
|
|
15
|
+
checks.push(...buildRuntimeChecks(runtime));
|
|
16
|
+
|
|
17
|
+
if (paths.trackerDir) {
|
|
18
|
+
checks.push(await checkTrackerDir(paths.trackerDir));
|
|
19
|
+
}
|
|
20
|
+
if (paths.configPath) {
|
|
21
|
+
checks.push(await checkConfigJson(paths.configPath));
|
|
22
|
+
}
|
|
23
|
+
if (paths.cliPath) {
|
|
24
|
+
checks.push(await checkCliEntrypoint(paths.cliPath));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
checks.push(await checkNetwork({ baseUrl: runtime?.baseUrl || null, fetch }));
|
|
28
|
+
|
|
29
|
+
if (diagnostics) {
|
|
30
|
+
checks.push(...buildDiagnosticsChecks(diagnostics));
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const summary = summarizeChecks(checks);
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
version: 1,
|
|
37
|
+
generated_at: now().toISOString(),
|
|
38
|
+
ok: summary.critical === 0,
|
|
39
|
+
summary,
|
|
40
|
+
checks,
|
|
41
|
+
diagnostics
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function buildRuntimeChecks(runtime = {}) {
|
|
46
|
+
const checks = [];
|
|
47
|
+
const baseUrl = typeof runtime.baseUrl === 'string' && runtime.baseUrl.trim() ? runtime.baseUrl.trim() : null;
|
|
48
|
+
const deviceToken = typeof runtime.deviceToken === 'string' && runtime.deviceToken.trim() ? 'set' : 'unset';
|
|
49
|
+
const dashboardUrl =
|
|
50
|
+
typeof runtime.dashboardUrl === 'string' && runtime.dashboardUrl.trim() ? runtime.dashboardUrl.trim() : null;
|
|
51
|
+
const httpTimeoutMs = Number.isFinite(Number(runtime.httpTimeoutMs)) ? Number(runtime.httpTimeoutMs) : null;
|
|
52
|
+
const debug = Boolean(runtime.debug);
|
|
53
|
+
const insforgeAnonKey = typeof runtime.insforgeAnonKey === 'string' && runtime.insforgeAnonKey.trim() ? 'set' : 'unset';
|
|
54
|
+
const autoRetryNoSpawn = Boolean(runtime.autoRetryNoSpawn);
|
|
55
|
+
|
|
56
|
+
checks.push({
|
|
57
|
+
id: 'runtime.base_url',
|
|
58
|
+
status: baseUrl ? 'ok' : 'fail',
|
|
59
|
+
detail: baseUrl ? 'base_url set' : 'base_url missing',
|
|
60
|
+
critical: false,
|
|
61
|
+
meta: {
|
|
62
|
+
base_url: baseUrl,
|
|
63
|
+
source: runtime?.sources?.baseUrl || null
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
checks.push({
|
|
68
|
+
id: 'runtime.device_token',
|
|
69
|
+
status: deviceToken === 'set' ? 'ok' : 'warn',
|
|
70
|
+
detail: deviceToken === 'set' ? 'device token set' : 'device token missing',
|
|
71
|
+
critical: false,
|
|
72
|
+
meta: {
|
|
73
|
+
device_token: deviceToken,
|
|
74
|
+
source: runtime?.sources?.deviceToken || null
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
checks.push({
|
|
79
|
+
id: 'runtime.dashboard_url',
|
|
80
|
+
status: 'ok',
|
|
81
|
+
detail: dashboardUrl ? 'dashboard_url set' : 'dashboard_url unset',
|
|
82
|
+
critical: false,
|
|
83
|
+
meta: {
|
|
84
|
+
dashboard_url: dashboardUrl,
|
|
85
|
+
source: runtime?.sources?.dashboardUrl || null
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
checks.push({
|
|
90
|
+
id: 'runtime.http_timeout_ms',
|
|
91
|
+
status: 'ok',
|
|
92
|
+
detail: 'http timeout resolved',
|
|
93
|
+
critical: false,
|
|
94
|
+
meta: {
|
|
95
|
+
http_timeout_ms: httpTimeoutMs,
|
|
96
|
+
source: runtime?.sources?.httpTimeoutMs || null
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
checks.push({
|
|
101
|
+
id: 'runtime.debug',
|
|
102
|
+
status: 'ok',
|
|
103
|
+
detail: debug ? 'debug enabled' : 'debug disabled',
|
|
104
|
+
critical: false,
|
|
105
|
+
meta: {
|
|
106
|
+
debug,
|
|
107
|
+
source: runtime?.sources?.debug || null
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
checks.push({
|
|
112
|
+
id: 'runtime.insforge_anon_key',
|
|
113
|
+
status: 'ok',
|
|
114
|
+
detail: insforgeAnonKey === 'set' ? 'anon key set' : 'anon key unset',
|
|
115
|
+
critical: false,
|
|
116
|
+
meta: {
|
|
117
|
+
anon_key: insforgeAnonKey,
|
|
118
|
+
source: runtime?.sources?.insforgeAnonKey || null
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
checks.push({
|
|
123
|
+
id: 'runtime.auto_retry_no_spawn',
|
|
124
|
+
status: 'ok',
|
|
125
|
+
detail: autoRetryNoSpawn ? 'auto retry spawn disabled' : 'auto retry spawn enabled',
|
|
126
|
+
critical: false,
|
|
127
|
+
meta: {
|
|
128
|
+
auto_retry_no_spawn: autoRetryNoSpawn,
|
|
129
|
+
source: runtime?.sources?.autoRetryNoSpawn || null
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return checks;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function checkTrackerDir(trackerDir) {
|
|
137
|
+
try {
|
|
138
|
+
const st = await fs.stat(trackerDir);
|
|
139
|
+
if (!st.isDirectory()) {
|
|
140
|
+
return {
|
|
141
|
+
id: 'fs.tracker_dir',
|
|
142
|
+
status: 'fail',
|
|
143
|
+
detail: 'tracker dir is not a directory',
|
|
144
|
+
critical: true,
|
|
145
|
+
meta: { path: trackerDir }
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
await fs.access(trackerDir, constants.R_OK);
|
|
149
|
+
return {
|
|
150
|
+
id: 'fs.tracker_dir',
|
|
151
|
+
status: 'ok',
|
|
152
|
+
detail: 'tracker dir readable',
|
|
153
|
+
critical: false,
|
|
154
|
+
meta: { path: trackerDir }
|
|
155
|
+
};
|
|
156
|
+
} catch (err) {
|
|
157
|
+
if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) {
|
|
158
|
+
return {
|
|
159
|
+
id: 'fs.tracker_dir',
|
|
160
|
+
status: 'warn',
|
|
161
|
+
detail: 'tracker dir missing',
|
|
162
|
+
critical: false,
|
|
163
|
+
meta: { path: trackerDir }
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
if (err && (err.code === 'EACCES' || err.code === 'EPERM')) {
|
|
167
|
+
return {
|
|
168
|
+
id: 'fs.tracker_dir',
|
|
169
|
+
status: 'fail',
|
|
170
|
+
detail: 'tracker dir permission denied',
|
|
171
|
+
critical: true,
|
|
172
|
+
meta: { path: trackerDir, code: err.code }
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
id: 'fs.tracker_dir',
|
|
177
|
+
status: 'fail',
|
|
178
|
+
detail: 'tracker dir error',
|
|
179
|
+
critical: true,
|
|
180
|
+
meta: { path: trackerDir, code: err?.code || 'error' }
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
async function checkConfigJson(configPath) {
|
|
186
|
+
const res = await readJsonStrict(configPath);
|
|
187
|
+
if (res.status === 'ok') {
|
|
188
|
+
return {
|
|
189
|
+
id: 'fs.config_json',
|
|
190
|
+
status: 'ok',
|
|
191
|
+
detail: 'config.json readable',
|
|
192
|
+
critical: false,
|
|
193
|
+
meta: { path: configPath }
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (res.status === 'missing') {
|
|
197
|
+
return {
|
|
198
|
+
id: 'fs.config_json',
|
|
199
|
+
status: 'warn',
|
|
200
|
+
detail: 'config.json missing',
|
|
201
|
+
critical: false,
|
|
202
|
+
meta: { path: configPath }
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (res.status === 'invalid') {
|
|
206
|
+
return {
|
|
207
|
+
id: 'fs.config_json',
|
|
208
|
+
status: 'fail',
|
|
209
|
+
detail: 'config.json invalid',
|
|
210
|
+
critical: true,
|
|
211
|
+
meta: { path: configPath }
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
return {
|
|
215
|
+
id: 'fs.config_json',
|
|
216
|
+
status: 'fail',
|
|
217
|
+
detail: 'config.json read error',
|
|
218
|
+
critical: true,
|
|
219
|
+
meta: { path: configPath }
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function checkCliEntrypoint(cliPath) {
|
|
224
|
+
try {
|
|
225
|
+
const st = await fs.stat(cliPath);
|
|
226
|
+
if (!st.isFile()) {
|
|
227
|
+
return {
|
|
228
|
+
id: 'cli.entrypoint',
|
|
229
|
+
status: 'fail',
|
|
230
|
+
detail: 'cli entrypoint is not a file',
|
|
231
|
+
critical: false,
|
|
232
|
+
meta: { path: cliPath }
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
await fs.access(cliPath, constants.R_OK);
|
|
236
|
+
if (process.platform !== 'win32') {
|
|
237
|
+
await fs.access(cliPath, constants.X_OK);
|
|
238
|
+
}
|
|
239
|
+
return {
|
|
240
|
+
id: 'cli.entrypoint',
|
|
241
|
+
status: 'ok',
|
|
242
|
+
detail: 'cli entrypoint readable',
|
|
243
|
+
critical: false,
|
|
244
|
+
meta: { path: cliPath }
|
|
245
|
+
};
|
|
246
|
+
} catch (err) {
|
|
247
|
+
return {
|
|
248
|
+
id: 'cli.entrypoint',
|
|
249
|
+
status: 'fail',
|
|
250
|
+
detail: 'cli entrypoint not accessible',
|
|
251
|
+
critical: false,
|
|
252
|
+
meta: { path: cliPath, code: err?.code || 'error' }
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function checkNetwork({ baseUrl, fetch }) {
|
|
258
|
+
if (!baseUrl) {
|
|
259
|
+
return {
|
|
260
|
+
id: 'network.base_url',
|
|
261
|
+
status: 'warn',
|
|
262
|
+
detail: 'base_url missing (skipped)',
|
|
263
|
+
critical: false,
|
|
264
|
+
meta: { base_url: null }
|
|
265
|
+
};
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const start = Date.now();
|
|
269
|
+
try {
|
|
270
|
+
if (typeof fetch !== 'function') throw new Error('Missing fetch');
|
|
271
|
+
const res = await fetch(baseUrl, { method: 'GET' });
|
|
272
|
+
const latency = Date.now() - start;
|
|
273
|
+
return {
|
|
274
|
+
id: 'network.base_url',
|
|
275
|
+
status: 'ok',
|
|
276
|
+
detail: `HTTP ${res.status} (reachable)`,
|
|
277
|
+
critical: false,
|
|
278
|
+
meta: {
|
|
279
|
+
status_code: res.status,
|
|
280
|
+
latency_ms: latency,
|
|
281
|
+
base_url: baseUrl
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
} catch (err) {
|
|
285
|
+
const latency = Date.now() - start;
|
|
286
|
+
return {
|
|
287
|
+
id: 'network.base_url',
|
|
288
|
+
status: 'fail',
|
|
289
|
+
detail: 'Network error',
|
|
290
|
+
critical: false,
|
|
291
|
+
meta: {
|
|
292
|
+
error: err?.message || String(err),
|
|
293
|
+
latency_ms: latency,
|
|
294
|
+
base_url: baseUrl
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
function buildDiagnosticsChecks(diagnostics) {
|
|
301
|
+
const checks = [];
|
|
302
|
+
const notify = diagnostics?.notify || {};
|
|
303
|
+
const notifyConfigured = Boolean(
|
|
304
|
+
notify.codex_notify_configured ||
|
|
305
|
+
notify.every_code_notify_configured ||
|
|
306
|
+
notify.claude_hook_configured ||
|
|
307
|
+
notify.gemini_hook_configured ||
|
|
308
|
+
notify.opencode_plugin_configured
|
|
309
|
+
);
|
|
310
|
+
|
|
311
|
+
checks.push({
|
|
312
|
+
id: 'notify.configured',
|
|
313
|
+
status: notifyConfigured ? 'ok' : 'warn',
|
|
314
|
+
detail: notifyConfigured ? 'notify configured' : 'notify not configured',
|
|
315
|
+
critical: false,
|
|
316
|
+
meta: { configured: notifyConfigured }
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
const uploadError = diagnostics?.upload?.last_error || null;
|
|
320
|
+
checks.push({
|
|
321
|
+
id: 'upload.last_error',
|
|
322
|
+
status: uploadError ? 'warn' : 'ok',
|
|
323
|
+
detail: uploadError ? 'last upload error present' : 'no upload errors',
|
|
324
|
+
critical: false,
|
|
325
|
+
meta: { last_error: uploadError ? uploadError.message || null : null }
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
return checks;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
function summarizeChecks(checks = []) {
|
|
332
|
+
const summary = { ok: 0, warn: 0, fail: 0, critical: 0 };
|
|
333
|
+
for (const check of checks) {
|
|
334
|
+
if (!check || typeof check.status !== 'string') continue;
|
|
335
|
+
if (check.status === 'ok') summary.ok += 1;
|
|
336
|
+
else if (check.status === 'warn') summary.warn += 1;
|
|
337
|
+
else if (check.status === 'fail') summary.fail += 1;
|
|
338
|
+
if (check.status === 'fail' && check.critical) summary.critical += 1;
|
|
339
|
+
}
|
|
340
|
+
return summary;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
module.exports = { buildDoctorReport };
|
package/src/lib/fs.js
CHANGED
|
@@ -22,6 +22,21 @@ async function readJson(filePath) {
|
|
|
22
22
|
}
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
+
async function readJsonStrict(filePath) {
|
|
26
|
+
try {
|
|
27
|
+
const raw = await fs.readFile(filePath, 'utf8');
|
|
28
|
+
return { status: 'ok', value: JSON.parse(raw), error: null };
|
|
29
|
+
} catch (err) {
|
|
30
|
+
if (err && (err.code === 'ENOENT' || err.code === 'ENOTDIR')) {
|
|
31
|
+
return { status: 'missing', value: null, error: err };
|
|
32
|
+
}
|
|
33
|
+
if (err && err.name === 'SyntaxError') {
|
|
34
|
+
return { status: 'invalid', value: null, error: err };
|
|
35
|
+
}
|
|
36
|
+
return { status: 'error', value: null, error: err };
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
25
40
|
async function writeJson(filePath, obj) {
|
|
26
41
|
await writeFileAtomic(filePath, JSON.stringify(obj, null, 2) + '\n');
|
|
27
42
|
}
|
|
@@ -55,8 +70,8 @@ module.exports = {
|
|
|
55
70
|
ensureDir,
|
|
56
71
|
writeFileAtomic,
|
|
57
72
|
readJson,
|
|
73
|
+
readJsonStrict,
|
|
58
74
|
writeJson,
|
|
59
75
|
chmod600IfPossible,
|
|
60
76
|
openLock
|
|
61
77
|
};
|
|
62
|
-
|
|
@@ -11,16 +11,11 @@ function loadInsforgeSdk() {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
function getAnonKey({ env = process.env } = {}) {
|
|
14
|
-
return
|
|
15
|
-
env.VIBEUSAGE_INSFORGE_ANON_KEY ||
|
|
16
|
-
env.VIBESCORE_INSFORGE_ANON_KEY ||
|
|
17
|
-
env.INSFORGE_ANON_KEY ||
|
|
18
|
-
''
|
|
19
|
-
);
|
|
14
|
+
return env.VIBEUSAGE_INSFORGE_ANON_KEY || '';
|
|
20
15
|
}
|
|
21
16
|
|
|
22
17
|
function getHttpTimeoutMs({ env = process.env } = {}) {
|
|
23
|
-
const raw = readEnvValue(env, ['VIBEUSAGE_HTTP_TIMEOUT_MS'
|
|
18
|
+
const raw = readEnvValue(env, ['VIBEUSAGE_HTTP_TIMEOUT_MS']);
|
|
24
19
|
if (raw == null || raw === '') return 20_000;
|
|
25
20
|
const n = Number(raw);
|
|
26
21
|
if (!Number.isFinite(n)) return 20_000;
|
package/src/lib/insforge.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { exchangeLinkCode, issueDeviceToken, signInWithPassword } = require('./
|
|
1
|
+
const { exchangeLinkCode, issueDeviceToken, signInWithPassword } = require('./vibeusage-api');
|
|
2
2
|
|
|
3
3
|
async function issueDeviceTokenWithPassword({ baseUrl, email, password, deviceName }) {
|
|
4
4
|
const accessToken = await signInWithPassword({ baseUrl, email, password });
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const DEFAULT_BASE_URL = 'https://5tmappuk.us-east.insforge.app';
|
|
2
|
+
const DEFAULT_DASHBOARD_URL = 'https://www.vibeusage.cc';
|
|
3
|
+
const DEFAULT_HTTP_TIMEOUT_MS = 20_000;
|
|
4
|
+
|
|
5
|
+
function resolveRuntimeConfig({ cli = {}, config = {}, env = process.env, defaults = {} } = {}) {
|
|
6
|
+
const baseUrl = pickString(cli.baseUrl, config.baseUrl, env?.VIBEUSAGE_INSFORGE_BASE_URL, defaults.baseUrl, DEFAULT_BASE_URL);
|
|
7
|
+
const dashboardUrl = pickString(
|
|
8
|
+
cli.dashboardUrl,
|
|
9
|
+
config.dashboardUrl,
|
|
10
|
+
env?.VIBEUSAGE_DASHBOARD_URL,
|
|
11
|
+
defaults.dashboardUrl,
|
|
12
|
+
DEFAULT_DASHBOARD_URL
|
|
13
|
+
);
|
|
14
|
+
const deviceToken = pickString(cli.deviceToken, config.deviceToken, env?.VIBEUSAGE_DEVICE_TOKEN, defaults.deviceToken, null);
|
|
15
|
+
const httpTimeoutMs = pickHttpTimeoutMs(
|
|
16
|
+
cli.httpTimeoutMs,
|
|
17
|
+
config.httpTimeoutMs,
|
|
18
|
+
env?.VIBEUSAGE_HTTP_TIMEOUT_MS,
|
|
19
|
+
defaults.httpTimeoutMs,
|
|
20
|
+
DEFAULT_HTTP_TIMEOUT_MS
|
|
21
|
+
);
|
|
22
|
+
const debug = pickBoolean(cli.debug, config.debug, env?.VIBEUSAGE_DEBUG, defaults.debug, false);
|
|
23
|
+
const insforgeAnonKey = pickString(
|
|
24
|
+
cli.insforgeAnonKey,
|
|
25
|
+
config.insforgeAnonKey,
|
|
26
|
+
env?.VIBEUSAGE_INSFORGE_ANON_KEY,
|
|
27
|
+
defaults.insforgeAnonKey,
|
|
28
|
+
''
|
|
29
|
+
);
|
|
30
|
+
if (insforgeAnonKey.value == null) insforgeAnonKey.value = '';
|
|
31
|
+
const autoRetryNoSpawn = pickBoolean(
|
|
32
|
+
cli.autoRetryNoSpawn,
|
|
33
|
+
config.autoRetryNoSpawn,
|
|
34
|
+
env?.VIBEUSAGE_AUTO_RETRY_NO_SPAWN,
|
|
35
|
+
defaults.autoRetryNoSpawn,
|
|
36
|
+
false
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
baseUrl: baseUrl.value,
|
|
41
|
+
dashboardUrl: dashboardUrl.value,
|
|
42
|
+
deviceToken: deviceToken.value,
|
|
43
|
+
httpTimeoutMs: httpTimeoutMs.value,
|
|
44
|
+
debug: debug.value,
|
|
45
|
+
insforgeAnonKey: insforgeAnonKey.value,
|
|
46
|
+
autoRetryNoSpawn: autoRetryNoSpawn.value,
|
|
47
|
+
sources: {
|
|
48
|
+
baseUrl: baseUrl.source,
|
|
49
|
+
dashboardUrl: dashboardUrl.source,
|
|
50
|
+
deviceToken: deviceToken.source,
|
|
51
|
+
httpTimeoutMs: httpTimeoutMs.source,
|
|
52
|
+
debug: debug.source,
|
|
53
|
+
insforgeAnonKey: insforgeAnonKey.source,
|
|
54
|
+
autoRetryNoSpawn: autoRetryNoSpawn.source
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function pickString(...candidates) {
|
|
60
|
+
return pickValue(candidates, normalizeString);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function pickBoolean(...candidates) {
|
|
64
|
+
return pickValue(candidates, normalizeBoolean);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function pickHttpTimeoutMs(...candidates) {
|
|
68
|
+
return pickValue(candidates, normalizeHttpTimeoutMs);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function pickValue(candidates, normalize) {
|
|
72
|
+
const labels = ['cli', 'config', 'env', 'default', 'default'];
|
|
73
|
+
for (let i = 0; i < candidates.length; i += 1) {
|
|
74
|
+
const value = normalize(candidates[i]);
|
|
75
|
+
if (value !== undefined) {
|
|
76
|
+
return { value, source: labels[i] || 'default' };
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return { value: null, source: 'default' };
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function normalizeString(value) {
|
|
83
|
+
if (typeof value !== 'string') return undefined;
|
|
84
|
+
const trimmed = value.trim();
|
|
85
|
+
if (!trimmed) return undefined;
|
|
86
|
+
return trimmed;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function normalizeBoolean(value) {
|
|
90
|
+
if (typeof value === 'boolean') return value;
|
|
91
|
+
if (typeof value === 'number') return value !== 0;
|
|
92
|
+
if (typeof value === 'string') {
|
|
93
|
+
const trimmed = value.trim().toLowerCase();
|
|
94
|
+
if (!trimmed) return undefined;
|
|
95
|
+
if (trimmed === '1' || trimmed === 'true') return true;
|
|
96
|
+
if (trimmed === '0' || trimmed === 'false') return false;
|
|
97
|
+
}
|
|
98
|
+
return undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function normalizeHttpTimeoutMs(value) {
|
|
102
|
+
if (value == null || value === '') return undefined;
|
|
103
|
+
const n = Number(value);
|
|
104
|
+
if (!Number.isFinite(n)) return undefined;
|
|
105
|
+
if (n <= 0) return 0;
|
|
106
|
+
return clampInt(n, 1000, 120_000);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function clampInt(value, min, max) {
|
|
110
|
+
const n = Number(value);
|
|
111
|
+
if (!Number.isFinite(n)) return min;
|
|
112
|
+
return Math.min(max, Math.max(min, Math.floor(n)));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
module.exports = {
|
|
116
|
+
DEFAULT_BASE_URL,
|
|
117
|
+
DEFAULT_DASHBOARD_URL,
|
|
118
|
+
DEFAULT_HTTP_TIMEOUT_MS,
|
|
119
|
+
resolveRuntimeConfig
|
|
120
|
+
};
|
package/src/lib/tracker-paths.js
CHANGED
|
@@ -1,66 +1,15 @@
|
|
|
1
1
|
const os = require('node:os');
|
|
2
2
|
const path = require('node:path');
|
|
3
|
-
const fs = require('node:fs/promises');
|
|
4
3
|
|
|
5
|
-
async function resolveTrackerPaths({ home = os.homedir()
|
|
6
|
-
const legacyRootDir = path.join(home, '.vibescore');
|
|
4
|
+
async function resolveTrackerPaths({ home = os.homedir() } = {}) {
|
|
7
5
|
const rootDir = path.join(home, '.vibeusage');
|
|
8
|
-
const legacyTrackerDir = path.join(legacyRootDir, 'tracker');
|
|
9
|
-
const legacyBinDir = path.join(legacyRootDir, 'bin');
|
|
10
|
-
const trackerDir = path.join(rootDir, 'tracker');
|
|
11
|
-
const binDir = path.join(rootDir, 'bin');
|
|
12
|
-
|
|
13
|
-
const legacyExists = await pathExists(legacyRootDir);
|
|
14
|
-
const newExists = await pathExists(rootDir);
|
|
15
|
-
|
|
16
|
-
let usingLegacy = false;
|
|
17
|
-
let migrated = false;
|
|
18
|
-
|
|
19
|
-
if (migrate && legacyExists && !newExists) {
|
|
20
|
-
const result = await migrateLegacyRoot({ legacyRootDir, rootDir });
|
|
21
|
-
usingLegacy = result.usingLegacy;
|
|
22
|
-
migrated = result.migrated;
|
|
23
|
-
} else if (!newExists && legacyExists) {
|
|
24
|
-
usingLegacy = true;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const activeRootDir = usingLegacy ? legacyRootDir : rootDir;
|
|
28
6
|
return {
|
|
29
|
-
rootDir
|
|
30
|
-
trackerDir: path.join(
|
|
31
|
-
binDir: path.join(
|
|
32
|
-
legacyRootDir,
|
|
33
|
-
legacyTrackerDir,
|
|
34
|
-
legacyBinDir,
|
|
35
|
-
migrated,
|
|
36
|
-
usingLegacy
|
|
7
|
+
rootDir,
|
|
8
|
+
trackerDir: path.join(rootDir, 'tracker'),
|
|
9
|
+
binDir: path.join(rootDir, 'bin')
|
|
37
10
|
};
|
|
38
11
|
}
|
|
39
12
|
|
|
40
|
-
async function migrateLegacyRoot({ legacyRootDir, rootDir }) {
|
|
41
|
-
try {
|
|
42
|
-
await fs.rename(legacyRootDir, rootDir);
|
|
43
|
-
return { migrated: true, usingLegacy: false };
|
|
44
|
-
} catch (err) {
|
|
45
|
-
try {
|
|
46
|
-
await fs.cp(legacyRootDir, rootDir, { recursive: true });
|
|
47
|
-
return { migrated: true, usingLegacy: false };
|
|
48
|
-
} catch (copyErr) {
|
|
49
|
-
return { migrated: false, usingLegacy: true, error: copyErr };
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
async function pathExists(target) {
|
|
55
|
-
try {
|
|
56
|
-
await fs.stat(target);
|
|
57
|
-
return true;
|
|
58
|
-
} catch (err) {
|
|
59
|
-
if (err && err.code === 'ENOENT') return false;
|
|
60
|
-
throw err;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
|
|
64
13
|
module.exports = {
|
|
65
14
|
resolveTrackerPaths
|
|
66
15
|
};
|
package/src/lib/uploader.js
CHANGED
|
@@ -3,7 +3,7 @@ const fssync = require('node:fs');
|
|
|
3
3
|
const readline = require('node:readline');
|
|
4
4
|
|
|
5
5
|
const { ensureDir, readJson, writeJson } = require('./fs');
|
|
6
|
-
const { ingestHourly } = require('./
|
|
6
|
+
const { ingestHourly } = require('./vibeusage-api');
|
|
7
7
|
|
|
8
8
|
const DEFAULT_SOURCE = 'codex';
|
|
9
9
|
const DEFAULT_MODEL = 'unknown';
|
|
@@ -19,7 +19,7 @@ async function issueDeviceToken({ baseUrl, accessToken, deviceName, platform = '
|
|
|
19
19
|
const data = await invokeFunction({
|
|
20
20
|
baseUrl,
|
|
21
21
|
accessToken,
|
|
22
|
-
slug: '
|
|
22
|
+
slug: 'vibeusage-device-token-issue',
|
|
23
23
|
method: 'POST',
|
|
24
24
|
body: { device_name: deviceName, platform },
|
|
25
25
|
errorPrefix: 'Device token issue failed'
|
|
@@ -39,7 +39,7 @@ async function exchangeLinkCode({ baseUrl, linkCode, requestId, deviceName, plat
|
|
|
39
39
|
const data = await invokeFunction({
|
|
40
40
|
baseUrl,
|
|
41
41
|
accessToken: null,
|
|
42
|
-
slug: '
|
|
42
|
+
slug: 'vibeusage-link-code-exchange',
|
|
43
43
|
method: 'POST',
|
|
44
44
|
body: {
|
|
45
45
|
link_code: linkCode,
|
|
@@ -66,7 +66,7 @@ async function ingestHourly({ baseUrl, deviceToken, hourly }) {
|
|
|
66
66
|
const data = await invokeFunctionWithRetry({
|
|
67
67
|
baseUrl,
|
|
68
68
|
accessToken: deviceToken,
|
|
69
|
-
slug: '
|
|
69
|
+
slug: 'vibeusage-ingest',
|
|
70
70
|
method: 'POST',
|
|
71
71
|
body: { hourly },
|
|
72
72
|
errorPrefix: 'Ingest failed',
|
|
@@ -83,7 +83,7 @@ async function syncHeartbeat({ baseUrl, deviceToken }) {
|
|
|
83
83
|
const data = await invokeFunction({
|
|
84
84
|
baseUrl,
|
|
85
85
|
accessToken: deviceToken,
|
|
86
|
-
slug: '
|
|
86
|
+
slug: 'vibeusage-sync-ping',
|
|
87
87
|
method: 'POST',
|
|
88
88
|
body: {},
|
|
89
89
|
errorPrefix: 'Sync heartbeat failed'
|