@ipv9/tokentracker-cli 0.39.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.ja.md +464 -0
- package/README.ko.md +464 -0
- package/README.md +476 -0
- package/README.zh-CN.md +469 -0
- package/bin/tracker.js +52 -0
- package/dashboard/dist/app-icon.png +0 -0
- package/dashboard/dist/apple-touch-icon.png +0 -0
- package/dashboard/dist/assets/ActivityHeatmap-BxBGHY1z.js +42 -0
- package/dashboard/dist/assets/Card-GROxUc_6.js +1 -0
- package/dashboard/dist/assets/DashboardPage-BZ5VFoBk.js +19 -0
- package/dashboard/dist/assets/DevicePage-CQts7ivB.js +1 -0
- package/dashboard/dist/assets/DialogTitle-DqFsk26U.js +1 -0
- package/dashboard/dist/assets/FadeIn-HBBXW_3q.js +1 -0
- package/dashboard/dist/assets/HeaderGithubStar-COD2HfaF.js +1 -0
- package/dashboard/dist/assets/IpCheckPage-Cn0l8-BQ.js +15 -0
- package/dashboard/dist/assets/LandingPage-CmV1BMU4.js +4356 -0
- package/dashboard/dist/assets/LeaderboardAvatar-CiIpWZwb.js +1 -0
- package/dashboard/dist/assets/LeaderboardPage-BU0mlR3g.js +6 -0
- package/dashboard/dist/assets/LeaderboardProfileModal-C49_3Xm9.js +72 -0
- package/dashboard/dist/assets/LeaderboardProfilePage-Bh1n0BAM.js +1 -0
- package/dashboard/dist/assets/LimitsPage-fZAmwLWT.js +2 -0
- package/dashboard/dist/assets/LocalOnlyNotice-D3rQdA-P.js +1 -0
- package/dashboard/dist/assets/LoginPage-enO4XP-c.js +1 -0
- package/dashboard/dist/assets/PopoverPopup-pitJEpN1.js +1 -0
- package/dashboard/dist/assets/Select-tT9x4ntD.js +1 -0
- package/dashboard/dist/assets/SelectItemText-C7F7eIm0.js +1 -0
- package/dashboard/dist/assets/SettingsPage-pDasZNU2.js +1 -0
- package/dashboard/dist/assets/SkillsPage-DEu7FY-2.js +1 -0
- package/dashboard/dist/assets/WidgetsPage-DGcO7rbp.js +1 -0
- package/dashboard/dist/assets/WrappedPage-D_1ID213.js +1 -0
- package/dashboard/dist/assets/agent-logos-DXQVg1xy.js +1 -0
- package/dashboard/dist/assets/arrow-up-right-DNLo92Ba.js +1 -0
- package/dashboard/dist/assets/download-DbBBBpJF.js +1 -0
- package/dashboard/dist/assets/geist-mono-cyrillic-400-normal-BPBWmzPh.woff +0 -0
- package/dashboard/dist/assets/geist-mono-cyrillic-400-normal-Ce5q_31Z.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-cyrillic-500-normal-CJBLNVQT.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-cyrillic-500-normal-mNhfPmgl.woff +0 -0
- package/dashboard/dist/assets/geist-mono-cyrillic-700-normal-DH5Q319x.woff +0 -0
- package/dashboard/dist/assets/geist-mono-cyrillic-700-normal-VCNRadI3.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-cyrillic-900-normal-BKC3PZkM.woff +0 -0
- package/dashboard/dist/assets/geist-mono-cyrillic-900-normal-DdrQ8KBw.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-latin-400-normal-CoULgQGM.woff +0 -0
- package/dashboard/dist/assets/geist-mono-latin-400-normal-LC9RFr9I.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-latin-500-normal-D3o2eNa9.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-latin-500-normal-DOxI7kZ4.woff +0 -0
- package/dashboard/dist/assets/geist-mono-latin-700-normal-D6izGJRP.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-latin-700-normal-QGw08Lff.woff +0 -0
- package/dashboard/dist/assets/geist-mono-latin-900-normal-CmoKXrdK.woff +0 -0
- package/dashboard/dist/assets/geist-mono-latin-900-normal-Cu5MFKsu.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-latin-ext-400-normal-Cgks_Qgx.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-latin-ext-400-normal-CxNRRMGd.woff +0 -0
- package/dashboard/dist/assets/geist-mono-latin-ext-500-normal-CQcGuCNt.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-latin-ext-500-normal-diTenJ8L.woff +0 -0
- package/dashboard/dist/assets/geist-mono-latin-ext-700-normal-BX9f1BHp.woff +0 -0
- package/dashboard/dist/assets/geist-mono-latin-ext-700-normal-YOllDaLV.woff2 +0 -0
- package/dashboard/dist/assets/geist-mono-latin-ext-900-normal-Br6WNUGb.woff +0 -0
- package/dashboard/dist/assets/geist-mono-latin-ext-900-normal-CrmBTzU2.woff2 +0 -0
- package/dashboard/dist/assets/index-BeoRn2gJ.js +2 -0
- package/dashboard/dist/assets/info-Bh5qTz6k.js +1 -0
- package/dashboard/dist/assets/main-BIY3Nj6I.js +969 -0
- package/dashboard/dist/assets/main-G_F9Iu_x.css +1 -0
- package/dashboard/dist/assets/use-limits-display-prefs-B4iJ-bBc.js +1 -0
- package/dashboard/dist/assets/use-native-settings-DHjPFDv8.js +1 -0
- package/dashboard/dist/assets/use-usage-limits-odqe9OY7.js +1 -0
- package/dashboard/dist/assets/useCurrency-BsSZiZMP.js +1 -0
- package/dashboard/dist/assets/useOpenInteractionType-BQhgq4rv.js +12 -0
- package/dashboard/dist/brand-logos/antigravity.svg +1 -0
- package/dashboard/dist/brand-logos/claude-code.svg +1 -0
- package/dashboard/dist/brand-logos/codebuddy.svg +1 -0
- package/dashboard/dist/brand-logos/codex.svg +15 -0
- package/dashboard/dist/brand-logos/copilot.svg +1 -0
- package/dashboard/dist/brand-logos/cursor.svg +1 -0
- package/dashboard/dist/brand-logos/gemini.svg +1 -0
- package/dashboard/dist/brand-logos/grok.svg +1 -0
- package/dashboard/dist/brand-logos/hermes.svg +1 -0
- package/dashboard/dist/brand-logos/kilo.svg +4 -0
- package/dashboard/dist/brand-logos/kimi.svg +1 -0
- package/dashboard/dist/brand-logos/kiro.svg +1 -0
- package/dashboard/dist/brand-logos/openclaw.svg +22 -0
- package/dashboard/dist/brand-logos/opencode.svg +3 -0
- package/dashboard/dist/clawd/happy.svg +161 -0
- package/dashboard/dist/clawd/idle/collapse.svg +101 -0
- package/dashboard/dist/clawd/idle/doze.svg +72 -0
- package/dashboard/dist/clawd/idle/follow.svg +64 -0
- package/dashboard/dist/clawd/idle/living.svg +196 -0
- package/dashboard/dist/clawd/idle/look.svg +115 -0
- package/dashboard/dist/clawd/idle/yawn.svg +158 -0
- package/dashboard/dist/clawd/mini/alert.svg +129 -0
- package/dashboard/dist/clawd/mini/crabwalk.svg +76 -0
- package/dashboard/dist/clawd/mini/enter-sleep.svg +65 -0
- package/dashboard/dist/clawd/mini/enter.svg +115 -0
- package/dashboard/dist/clawd/mini/happy.svg +124 -0
- package/dashboard/dist/clawd/mini/idle-tight.svg +15 -0
- package/dashboard/dist/clawd/mini/idle.svg +83 -0
- package/dashboard/dist/clawd/mini/peek.svg +82 -0
- package/dashboard/dist/clawd/mini/sleep.svg +112 -0
- package/dashboard/dist/clawd/react/double.svg +108 -0
- package/dashboard/dist/clawd/react/drag.svg +102 -0
- package/dashboard/dist/clawd/react/left.svg +102 -0
- package/dashboard/dist/clawd/react/right.svg +102 -0
- package/dashboard/dist/clawd/sleep/collapse-sleep.svg +247 -0
- package/dashboard/dist/clawd/sleep/sleeping.svg +118 -0
- package/dashboard/dist/clawd/sleep/wake.svg +277 -0
- package/dashboard/dist/clawd/static-base.svg +21 -0
- package/dashboard/dist/clawd/status/disconnected.svg +136 -0
- package/dashboard/dist/clawd/status/error.svg +94 -0
- package/dashboard/dist/clawd/status/notification.svg +134 -0
- package/dashboard/dist/clawd/working/building.svg +231 -0
- package/dashboard/dist/clawd/working/carrying.svg +107 -0
- package/dashboard/dist/clawd/working/conducting.svg +118 -0
- package/dashboard/dist/clawd/working/confused.svg +113 -0
- package/dashboard/dist/clawd/working/debugger.svg +91 -0
- package/dashboard/dist/clawd/working/juggling.svg +82 -0
- package/dashboard/dist/clawd/working/overheated.svg +75 -0
- package/dashboard/dist/clawd/working/pushing.svg +125 -0
- package/dashboard/dist/clawd/working/sweeping.svg +79 -0
- package/dashboard/dist/clawd/working/thinking.svg +119 -0
- package/dashboard/dist/clawd/working/typing.svg +153 -0
- package/dashboard/dist/clawd/working/ultrathink.svg +166 -0
- package/dashboard/dist/clawd/working/wizard.svg +98 -0
- package/dashboard/dist/dashboard-dark.png +0 -0
- package/dashboard/dist/favicon-16.png +0 -0
- package/dashboard/dist/favicon-32.png +0 -0
- package/dashboard/dist/favicon.ico +0 -0
- package/dashboard/dist/feed.xml +34 -0
- package/dashboard/dist/icon-192.png +0 -0
- package/dashboard/dist/icon-512.png +0 -0
- package/dashboard/dist/index.html +355 -0
- package/dashboard/dist/llms.txt +53 -0
- package/dashboard/dist/neon_bg.png +0 -0
- package/dashboard/dist/robots.txt +7 -0
- package/dashboard/dist/share.html +140 -0
- package/dashboard/dist/sitemap.xml +27 -0
- package/dashboard/dist/widgets-overview.png +0 -0
- package/package.json +78 -0
- package/scripts/install-local-service.sh +140 -0
- package/scripts/uninstall-local-service.sh +21 -0
- package/src/cli.js +95 -0
- package/src/commands/device-login.js +161 -0
- package/src/commands/diagnostics.js +38 -0
- package/src/commands/doctor.js +96 -0
- package/src/commands/init.js +1195 -0
- package/src/commands/serve.js +351 -0
- package/src/commands/status.js +609 -0
- package/src/commands/sync.js +2285 -0
- package/src/commands/uninstall.js +189 -0
- package/src/commands/wrapped.js +150 -0
- package/src/lib/antigravity-paths.js +48 -0
- package/src/lib/browser-auth.js +281 -0
- package/src/lib/categorizer-utils.js +232 -0
- package/src/lib/claude-categorizer.js +1319 -0
- package/src/lib/claude-config.js +204 -0
- package/src/lib/cli-ui.js +179 -0
- package/src/lib/codex-config.js +350 -0
- package/src/lib/codex-context-breakdown.js +400 -0
- package/src/lib/codex-rollout-parser.js +623 -0
- package/src/lib/codex-token-refresh.js +112 -0
- package/src/lib/cursor-config.js +425 -0
- package/src/lib/debug-flags.js +7 -0
- package/src/lib/diagnostics.js +272 -0
- package/src/lib/doctor.js +338 -0
- package/src/lib/fs.js +91 -0
- package/src/lib/gemini-config.js +288 -0
- package/src/lib/grok-hook.js +351 -0
- package/src/lib/init-flow.js +42 -0
- package/src/lib/local-api.js +1861 -0
- package/src/lib/openclaw-hook.js +420 -0
- package/src/lib/openclaw-session-plugin.js +520 -0
- package/src/lib/opencode-config.js +99 -0
- package/src/lib/passive-mode.js +185 -0
- package/src/lib/pricing/curated-overrides.json +79 -0
- package/src/lib/pricing/index.js +143 -0
- package/src/lib/pricing/litellm-fetcher.js +172 -0
- package/src/lib/pricing/matcher.js +255 -0
- package/src/lib/pricing/seed-snapshot.json +1 -0
- package/src/lib/progress.js +76 -0
- package/src/lib/project-usage-purge.js +106 -0
- package/src/lib/prompt.js +24 -0
- package/src/lib/proxy-env.js +131 -0
- package/src/lib/rollout.js +8442 -0
- package/src/lib/runtime-config.js +122 -0
- package/src/lib/skill-usage.js +267 -0
- package/src/lib/skills-manager.js +1127 -0
- package/src/lib/source-metadata.js +48 -0
- package/src/lib/sqlite-reader.js +136 -0
- package/src/lib/static-server.js +59 -0
- package/src/lib/subscriptions.js +453 -0
- package/src/lib/tracker-paths.js +16 -0
- package/src/lib/upload-throttle.js +148 -0
- package/src/lib/usage-limits.js +1806 -0
- package/src/lib/wrapped-aggregator.js +225 -0
package/package.json
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ipv9/tokentracker-cli",
|
|
3
|
+
"version": "0.39.0",
|
|
4
|
+
"description": "Token usage tracker for AI agent CLIs (Claude Code, Codex, Cursor, Gemini, Kiro, OpenCode, OpenClaw, Every Code, Hermes, GitHub Copilot, Kimi Code, CodeBuddy, Grok Build, oh-my-pi, pi, Craft Agents, Kilo CLI, Kilo Code, Roo Code, Zed Agent, Goose)",
|
|
5
|
+
"main": "src/cli.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"tokentracker-cli": "bin/tracker.js",
|
|
8
|
+
"tracker": "bin/tracker.js",
|
|
9
|
+
"tokentracker": "bin/tracker.js",
|
|
10
|
+
"tokentracker-tracker": "bin/tracker.js"
|
|
11
|
+
},
|
|
12
|
+
"files": [
|
|
13
|
+
"bin/",
|
|
14
|
+
"src/",
|
|
15
|
+
"dashboard/dist/",
|
|
16
|
+
"scripts/install-local-service.sh",
|
|
17
|
+
"scripts/uninstall-local-service.sh",
|
|
18
|
+
"LICENSE",
|
|
19
|
+
"README.md"
|
|
20
|
+
],
|
|
21
|
+
"type": "commonjs",
|
|
22
|
+
"publishConfig": {
|
|
23
|
+
"access": "public"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"ci:local": "npm test && npm run validate:copy && npm run validate:ui-hardcode && npm run validate:guardrails && node --test test/architecture-guardrails.test.js && npm --prefix dashboard run build",
|
|
27
|
+
"copy:pull": "node scripts/copy-sync.cjs pull",
|
|
28
|
+
"copy:push": "node scripts/copy-sync.cjs push",
|
|
29
|
+
"dashboard:build": "npm --prefix dashboard run build",
|
|
30
|
+
"dashboard:dev": "npm --prefix dashboard run dev",
|
|
31
|
+
"dashboard:open": "bash scripts/open-dashboard.sh",
|
|
32
|
+
"dashboard:preview": "npm --prefix dashboard run preview",
|
|
33
|
+
"dev:shim": "node scripts/dev-bin-shim.cjs",
|
|
34
|
+
"audit:tokens": "node scripts/audit-token-correctness.cjs",
|
|
35
|
+
"graph:auto-index": "node scripts/graph/auto-index.cjs",
|
|
36
|
+
"graph:scip": "node scripts/graph/generate-scip.cjs",
|
|
37
|
+
"pricing:build-seed": "node scripts/build-pricing-seed.cjs",
|
|
38
|
+
"prepublishOnly": "node scripts/build-pricing-seed.cjs",
|
|
39
|
+
"test": "node --test test/*.test.js",
|
|
40
|
+
"validate:copy": "node scripts/validate-copy-registry.cjs",
|
|
41
|
+
"validate:guardrails": "node scripts/validate-architecture-guardrails.cjs",
|
|
42
|
+
"validate:retros": "node scripts/validate-retros.cjs",
|
|
43
|
+
"validate:ui-hardcode": "node scripts/ops/validate-ui-hardcode.cjs"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@mongodb-js/zstd": "^2.0.1",
|
|
47
|
+
"undici": "^8.2.0"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@sourcegraph/scip-typescript": "^0.3.6",
|
|
51
|
+
"esbuild": "0.27.2"
|
|
52
|
+
},
|
|
53
|
+
"repository": {
|
|
54
|
+
"type": "git",
|
|
55
|
+
"url": "git+https://github.com/pitimon/TokenTracker.git"
|
|
56
|
+
},
|
|
57
|
+
"homepage": "https://github.com/pitimon/TokenTracker#readme",
|
|
58
|
+
"bugs": {
|
|
59
|
+
"url": "https://github.com/pitimon/TokenTracker/issues"
|
|
60
|
+
},
|
|
61
|
+
"keywords": [
|
|
62
|
+
"token",
|
|
63
|
+
"tracker",
|
|
64
|
+
"ai",
|
|
65
|
+
"claude",
|
|
66
|
+
"codex",
|
|
67
|
+
"cursor",
|
|
68
|
+
"gemini",
|
|
69
|
+
"kiro",
|
|
70
|
+
"opencode",
|
|
71
|
+
"usage",
|
|
72
|
+
"cost"
|
|
73
|
+
],
|
|
74
|
+
"license": "MIT",
|
|
75
|
+
"engines": {
|
|
76
|
+
"node": ">=20"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
PORT="${TOKENTRACKER_DASHBOARD_PORT:-7680}"
|
|
5
|
+
PACKAGE_NAME="${TOKENTRACKER_NPM_PACKAGE:-@ipv9/tokentracker-cli}"
|
|
6
|
+
TRACKER_HOME="${TOKENTRACKER_HOME:-$HOME/.tokentracker}"
|
|
7
|
+
SERVICE_PATH="${TOKENTRACKER_SERVICE_PATH:-/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin}"
|
|
8
|
+
LOG_DIR="$TRACKER_HOME/tracker/logs"
|
|
9
|
+
BIN_DIR="$TRACKER_HOME/bin"
|
|
10
|
+
AGENTS_DIR="$HOME/Library/LaunchAgents"
|
|
11
|
+
DASHBOARD_LABEL="com.pitimon.tokentracker.dashboard"
|
|
12
|
+
LOCAL_SYNC_LABEL="com.pitimon.tokentracker.local-sync"
|
|
13
|
+
DASHBOARD_PLIST="$AGENTS_DIR/$DASHBOARD_LABEL.plist"
|
|
14
|
+
LOCAL_SYNC_PLIST="$AGENTS_DIR/$LOCAL_SYNC_LABEL.plist"
|
|
15
|
+
LOCAL_SYNC_WRAPPER="$BIN_DIR/tokentracker-local-sync-service.sh"
|
|
16
|
+
|
|
17
|
+
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
18
|
+
echo "TokenTracker local service installer currently supports macOS launchd only." >&2
|
|
19
|
+
exit 1
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
mkdir -p "$LOG_DIR" "$BIN_DIR" "$AGENTS_DIR"
|
|
23
|
+
|
|
24
|
+
cat > "$LOCAL_SYNC_WRAPPER" <<'WRAPPER'
|
|
25
|
+
#!/usr/bin/env bash
|
|
26
|
+
set -euo pipefail
|
|
27
|
+
|
|
28
|
+
CONFIG_PATH="${TOKENTRACKER_CONFIG_PATH:-$HOME/.tokentracker/tracker/config.json}"
|
|
29
|
+
PACKAGE_NAME="${TOKENTRACKER_NPM_PACKAGE:-@ipv9/tokentracker-cli}"
|
|
30
|
+
export PATH="${TOKENTRACKER_SERVICE_PATH:-/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin}"
|
|
31
|
+
|
|
32
|
+
if [[ -n "${TOKENTRACKER_DEVICE_TOKEN:-}" ]]; then
|
|
33
|
+
echo "TokenTracker local-sync skipped: TOKENTRACKER_DEVICE_TOKEN is configured."
|
|
34
|
+
exit 0
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
if [[ -f "$CONFIG_PATH" ]]; then
|
|
38
|
+
HAS_DEVICE_TOKEN="$(node -e '
|
|
39
|
+
const fs = require("node:fs");
|
|
40
|
+
const p = process.argv[1];
|
|
41
|
+
try {
|
|
42
|
+
const cfg = JSON.parse(fs.readFileSync(p, "utf8"));
|
|
43
|
+
process.stdout.write(typeof cfg.deviceToken === "string" && cfg.deviceToken.trim() ? "1" : "0");
|
|
44
|
+
} catch {
|
|
45
|
+
process.stdout.write("0");
|
|
46
|
+
}
|
|
47
|
+
' "$CONFIG_PATH")"
|
|
48
|
+
if [[ "$HAS_DEVICE_TOKEN" == "1" ]]; then
|
|
49
|
+
echo "TokenTracker local-sync skipped: deviceToken is configured."
|
|
50
|
+
exit 0
|
|
51
|
+
fi
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
exec npx --yes "$PACKAGE_NAME" sync --auto
|
|
55
|
+
WRAPPER
|
|
56
|
+
chmod 0755 "$LOCAL_SYNC_WRAPPER"
|
|
57
|
+
|
|
58
|
+
launchctl bootout "gui/$(id -u)" "$DASHBOARD_PLIST" >/dev/null 2>&1 || true
|
|
59
|
+
launchctl bootout "gui/$(id -u)" "$LOCAL_SYNC_PLIST" >/dev/null 2>&1 || true
|
|
60
|
+
|
|
61
|
+
cat > "$DASHBOARD_PLIST" <<PLIST
|
|
62
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
63
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
64
|
+
<plist version="1.0">
|
|
65
|
+
<dict>
|
|
66
|
+
<key>Label</key>
|
|
67
|
+
<string>$DASHBOARD_LABEL</string>
|
|
68
|
+
<key>ProgramArguments</key>
|
|
69
|
+
<array>
|
|
70
|
+
<string>/usr/bin/env</string>
|
|
71
|
+
<string>npx</string>
|
|
72
|
+
<string>--yes</string>
|
|
73
|
+
<string>$PACKAGE_NAME</string>
|
|
74
|
+
<string>serve</string>
|
|
75
|
+
<string>--no-open</string>
|
|
76
|
+
<string>--port</string>
|
|
77
|
+
<string>$PORT</string>
|
|
78
|
+
</array>
|
|
79
|
+
<key>RunAtLoad</key>
|
|
80
|
+
<true/>
|
|
81
|
+
<key>KeepAlive</key>
|
|
82
|
+
<true/>
|
|
83
|
+
<key>EnvironmentVariables</key>
|
|
84
|
+
<dict>
|
|
85
|
+
<key>PATH</key>
|
|
86
|
+
<string>$SERVICE_PATH</string>
|
|
87
|
+
<key>TOKENTRACKER_NPM_PACKAGE</key>
|
|
88
|
+
<string>$PACKAGE_NAME</string>
|
|
89
|
+
</dict>
|
|
90
|
+
<key>StandardOutPath</key>
|
|
91
|
+
<string>$LOG_DIR/dashboard.log</string>
|
|
92
|
+
<key>StandardErrorPath</key>
|
|
93
|
+
<string>$LOG_DIR/dashboard.err.log</string>
|
|
94
|
+
<key>WorkingDirectory</key>
|
|
95
|
+
<string>$HOME</string>
|
|
96
|
+
</dict>
|
|
97
|
+
</plist>
|
|
98
|
+
PLIST
|
|
99
|
+
|
|
100
|
+
cat > "$LOCAL_SYNC_PLIST" <<PLIST
|
|
101
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
102
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
103
|
+
<plist version="1.0">
|
|
104
|
+
<dict>
|
|
105
|
+
<key>Label</key>
|
|
106
|
+
<string>$LOCAL_SYNC_LABEL</string>
|
|
107
|
+
<key>ProgramArguments</key>
|
|
108
|
+
<array>
|
|
109
|
+
<string>$LOCAL_SYNC_WRAPPER</string>
|
|
110
|
+
</array>
|
|
111
|
+
<key>RunAtLoad</key>
|
|
112
|
+
<true/>
|
|
113
|
+
<key>StartInterval</key>
|
|
114
|
+
<integer>300</integer>
|
|
115
|
+
<key>EnvironmentVariables</key>
|
|
116
|
+
<dict>
|
|
117
|
+
<key>PATH</key>
|
|
118
|
+
<string>$SERVICE_PATH</string>
|
|
119
|
+
<key>TOKENTRACKER_NPM_PACKAGE</key>
|
|
120
|
+
<string>$PACKAGE_NAME</string>
|
|
121
|
+
<key>TOKENTRACKER_SERVICE_PATH</key>
|
|
122
|
+
<string>$SERVICE_PATH</string>
|
|
123
|
+
</dict>
|
|
124
|
+
<key>StandardOutPath</key>
|
|
125
|
+
<string>$LOG_DIR/local-sync.log</string>
|
|
126
|
+
<key>StandardErrorPath</key>
|
|
127
|
+
<string>$LOG_DIR/local-sync.err.log</string>
|
|
128
|
+
<key>WorkingDirectory</key>
|
|
129
|
+
<string>$HOME</string>
|
|
130
|
+
</dict>
|
|
131
|
+
</plist>
|
|
132
|
+
PLIST
|
|
133
|
+
|
|
134
|
+
launchctl bootstrap "gui/$(id -u)" "$DASHBOARD_PLIST"
|
|
135
|
+
launchctl bootstrap "gui/$(id -u)" "$LOCAL_SYNC_PLIST"
|
|
136
|
+
|
|
137
|
+
echo "Installed TokenTracker local services:"
|
|
138
|
+
echo " $DASHBOARD_LABEL -> http://127.0.0.1:$PORT/dashboard"
|
|
139
|
+
echo " $LOCAL_SYNC_LABEL -> local sync every 5 minutes, skips when deviceToken is configured"
|
|
140
|
+
echo "Logs: $LOG_DIR"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
AGENTS_DIR="$HOME/Library/LaunchAgents"
|
|
5
|
+
DASHBOARD_LABEL="com.pitimon.tokentracker.dashboard"
|
|
6
|
+
LOCAL_SYNC_LABEL="com.pitimon.tokentracker.local-sync"
|
|
7
|
+
DASHBOARD_PLIST="$AGENTS_DIR/$DASHBOARD_LABEL.plist"
|
|
8
|
+
LOCAL_SYNC_PLIST="$AGENTS_DIR/$LOCAL_SYNC_LABEL.plist"
|
|
9
|
+
LOCAL_SYNC_WRAPPER="${TOKENTRACKER_HOME:-$HOME/.tokentracker}/bin/tokentracker-local-sync-service.sh"
|
|
10
|
+
|
|
11
|
+
if [[ "$(uname -s)" != "Darwin" ]]; then
|
|
12
|
+
echo "TokenTracker local service uninstaller currently supports macOS launchd only." >&2
|
|
13
|
+
exit 1
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
launchctl bootout "gui/$(id -u)" "$DASHBOARD_PLIST" >/dev/null 2>&1 || true
|
|
17
|
+
launchctl bootout "gui/$(id -u)" "$LOCAL_SYNC_PLIST" >/dev/null 2>&1 || true
|
|
18
|
+
|
|
19
|
+
rm -f "$DASHBOARD_PLIST" "$LOCAL_SYNC_PLIST" "$LOCAL_SYNC_WRAPPER"
|
|
20
|
+
|
|
21
|
+
echo "Uninstalled TokenTracker local services."
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const { cmdInit } = require("./commands/init");
|
|
2
|
+
const { cmdSync } = require("./commands/sync");
|
|
3
|
+
const { cmdStatus } = require("./commands/status");
|
|
4
|
+
const { cmdDiagnostics } = require("./commands/diagnostics");
|
|
5
|
+
const { cmdDoctor } = require("./commands/doctor");
|
|
6
|
+
const { cmdUninstall } = require("./commands/uninstall");
|
|
7
|
+
const { cmdServe } = require("./commands/serve");
|
|
8
|
+
const { cmdDeviceLogin } = require("./commands/device-login");
|
|
9
|
+
const { cmdWrapped } = require("./commands/wrapped");
|
|
10
|
+
|
|
11
|
+
async function run(argv) {
|
|
12
|
+
const [command, ...rest] = argv;
|
|
13
|
+
|
|
14
|
+
// No args → launch dashboard
|
|
15
|
+
if (!command) {
|
|
16
|
+
await cmdServe(argv);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (command === "-h" || command === "--help") {
|
|
21
|
+
printHelp();
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
switch (command) {
|
|
26
|
+
case "serve":
|
|
27
|
+
await cmdServe(rest);
|
|
28
|
+
return;
|
|
29
|
+
case "init":
|
|
30
|
+
await cmdInit(rest);
|
|
31
|
+
return;
|
|
32
|
+
case "sync":
|
|
33
|
+
await cmdSync(rest);
|
|
34
|
+
return;
|
|
35
|
+
case "status":
|
|
36
|
+
await cmdStatus(rest);
|
|
37
|
+
return;
|
|
38
|
+
case "diagnostics":
|
|
39
|
+
await cmdDiagnostics(rest);
|
|
40
|
+
return;
|
|
41
|
+
case "doctor":
|
|
42
|
+
await cmdDoctor(rest);
|
|
43
|
+
return;
|
|
44
|
+
case "uninstall":
|
|
45
|
+
await cmdUninstall(rest);
|
|
46
|
+
return;
|
|
47
|
+
case "device-login":
|
|
48
|
+
await cmdDeviceLogin(rest);
|
|
49
|
+
return;
|
|
50
|
+
case "wrapped":
|
|
51
|
+
await cmdWrapped(rest);
|
|
52
|
+
return;
|
|
53
|
+
default:
|
|
54
|
+
throw new Error(`Unknown command: ${command}`);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function printHelp() {
|
|
59
|
+
// Keep this short; npx users want quick guidance.
|
|
60
|
+
process.stdout.write(
|
|
61
|
+
[
|
|
62
|
+
"tokentracker",
|
|
63
|
+
"",
|
|
64
|
+
"Usage:",
|
|
65
|
+
" npx --yes @ipv9/tokentracker-cli Open local dashboard",
|
|
66
|
+
" npx --yes @ipv9/tokentracker-cli [--debug] serve [--port 7680] [--no-open] [--sync]",
|
|
67
|
+
" npx --yes @ipv9/tokentracker-cli [--debug] init [--yes] [--dry-run] [--no-open] [--link-code <code>]",
|
|
68
|
+
" npx --yes @ipv9/tokentracker-cli [--debug] sync [--auto] [--drain] [--from-openclaw]",
|
|
69
|
+
" npx --yes @ipv9/tokentracker-cli [--debug] status [--probe-keychain] [--probe-keychain-details]",
|
|
70
|
+
" npx --yes @ipv9/tokentracker-cli [--debug] diagnostics [--out diagnostics.json]",
|
|
71
|
+
" npx --yes @ipv9/tokentracker-cli [--debug] doctor [--json] [--out doctor.json] [--base-url <url>]",
|
|
72
|
+
" npx --yes @ipv9/tokentracker-cli [--debug] uninstall [--purge]",
|
|
73
|
+
" npx --yes @ipv9/tokentracker-cli [--debug] device-login [--json] [--base-url <url>]",
|
|
74
|
+
" npx --yes @ipv9/tokentracker-cli [--debug] wrapped [--year 2026] [--json]",
|
|
75
|
+
"",
|
|
76
|
+
"Notes:",
|
|
77
|
+
" - init: consent first, local setup next, browser sign-in last.",
|
|
78
|
+
" - --yes skips the consent menu (non-interactive safe).",
|
|
79
|
+
" - --dry-run previews changes without writing files.",
|
|
80
|
+
" - optional: --link-code <code> skips browser login when provided by Dashboard.",
|
|
81
|
+
" - Every Code notify installs when ~/.code/config.toml exists.",
|
|
82
|
+
" - OpenClaw hook auto-links when OpenClaw is installed (requires gateway restart).",
|
|
83
|
+
" - auto sync waits for a device token.",
|
|
84
|
+
" - optional: --dashboard-url for hosted landing.",
|
|
85
|
+
" - serve opens the local dashboard without syncing; pass --sync to parse local logs first.",
|
|
86
|
+
" - sync parses ~/.codex/sessions/**/rollout-*.jsonl and ~/.code/sessions/**/rollout-*.jsonl, then uploads token deltas only when cloud credentials are configured.",
|
|
87
|
+
" - --from-openclaw marks sync runs triggered by OpenClaw hooks.",
|
|
88
|
+
" - --debug shows original backend errors.",
|
|
89
|
+
" - device-login pairs a headless CLI / SSH session with a browser sign-in (15-min code).",
|
|
90
|
+
"",
|
|
91
|
+
].join("\n"),
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = { run };
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const fs = require("node:fs/promises");
|
|
6
|
+
|
|
7
|
+
const { readJson, writeJson } = require("../lib/fs");
|
|
8
|
+
const { resolveTrackerPaths } = require("../lib/tracker-paths");
|
|
9
|
+
|
|
10
|
+
const DEFAULT_BASE_URL = "https://srctyff5.us-east.insforge.app";
|
|
11
|
+
const POLL_INTERVAL_MS = 5_000;
|
|
12
|
+
const ABSOLUTE_TIMEOUT_MS = 16 * 60 * 1000; // matches the 15-min server window with a small buffer
|
|
13
|
+
|
|
14
|
+
function readBaseUrl(config) {
|
|
15
|
+
return (
|
|
16
|
+
process.env.TOKENTRACKER_BASE_URL ||
|
|
17
|
+
process.env.TOKENTRACKER_API_URL ||
|
|
18
|
+
config?.baseUrl ||
|
|
19
|
+
DEFAULT_BASE_URL
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function authorize({ baseUrl, clientInfo }) {
|
|
24
|
+
const res = await fetch(`${baseUrl}/functions/tokentracker-device-flow-authorize`, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { "Content-Type": "application/json" },
|
|
27
|
+
body: JSON.stringify({ client_info: clientInfo }),
|
|
28
|
+
});
|
|
29
|
+
if (!res.ok) {
|
|
30
|
+
const text = await res.text();
|
|
31
|
+
throw new Error(`authorize failed (HTTP ${res.status}): ${text.slice(0, 200)}`);
|
|
32
|
+
}
|
|
33
|
+
return res.json();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async function pollOnce({ baseUrl, deviceCode }) {
|
|
37
|
+
const res = await fetch(`${baseUrl}/functions/tokentracker-device-flow-poll`, {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: { "Content-Type": "application/json" },
|
|
40
|
+
body: JSON.stringify({ device_code: deviceCode }),
|
|
41
|
+
});
|
|
42
|
+
const data = await res.json().catch(() => ({}));
|
|
43
|
+
// 404 = unknown, 410 = expired, 200 = {status, user_id?, device_token?}.
|
|
44
|
+
// Anything else (502 from a misconfigured edge, 5xx during deploy, …) must
|
|
45
|
+
// bubble up as a network-style error — masking it as "unknown" would tell
|
|
46
|
+
// the user their device_code was evicted when it wasn't.
|
|
47
|
+
if (!res.ok && res.status !== 404 && res.status !== 410) {
|
|
48
|
+
throw new Error(`poll HTTP ${res.status}: ${(data?.error ?? "").toString().slice(0, 200)}`);
|
|
49
|
+
}
|
|
50
|
+
return {
|
|
51
|
+
status: data.status ?? "unknown",
|
|
52
|
+
user_id: data.user_id ?? null,
|
|
53
|
+
deviceToken: data.device_token ?? data.deviceToken ?? null,
|
|
54
|
+
deviceId: data.device_id ?? data.deviceId ?? null,
|
|
55
|
+
httpStatus: res.status,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function sleep(ms) {
|
|
60
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function cmdDeviceLogin(argv = [], options = {}) {
|
|
64
|
+
const opts = parseArgs(argv);
|
|
65
|
+
const home = options.home || os.homedir();
|
|
66
|
+
const sleepFn = options.sleep || sleep;
|
|
67
|
+
const { trackerDir } = await resolveTrackerPaths({ home });
|
|
68
|
+
const configPath = path.join(trackerDir, "config.json");
|
|
69
|
+
const config = (await readJson(configPath)) || {};
|
|
70
|
+
const baseUrl = opts.baseUrl || readBaseUrl(config);
|
|
71
|
+
|
|
72
|
+
const clientInfo = `${os.platform()}-${os.arch()} ${os.hostname()}`;
|
|
73
|
+
process.stdout.write(`Requesting device code from ${baseUrl}...\n`);
|
|
74
|
+
const authResp = await authorize({ baseUrl, clientInfo });
|
|
75
|
+
|
|
76
|
+
if (opts.json) {
|
|
77
|
+
process.stdout.write(JSON.stringify(authResp, null, 2) + "\n");
|
|
78
|
+
} else {
|
|
79
|
+
process.stdout.write(
|
|
80
|
+
[
|
|
81
|
+
"",
|
|
82
|
+
" Sign in from a browser:",
|
|
83
|
+
` ${authResp.verification_uri_complete || authResp.verification_uri}`,
|
|
84
|
+
"",
|
|
85
|
+
` Or visit ${authResp.verification_uri} and enter the code:`,
|
|
86
|
+
"",
|
|
87
|
+
` ${authResp.user_code}`,
|
|
88
|
+
"",
|
|
89
|
+
` This code expires in ${Math.round(authResp.expires_in / 60)} minutes.`,
|
|
90
|
+
" Polling every 5 seconds until you approve…",
|
|
91
|
+
"",
|
|
92
|
+
].join("\n"),
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const startedAt = Date.now();
|
|
97
|
+
let consecutiveErrors = 0;
|
|
98
|
+
const MAX_BACKOFF_MS = 30_000;
|
|
99
|
+
while (Date.now() - startedAt < ABSOLUTE_TIMEOUT_MS) {
|
|
100
|
+
// Exponential backoff on consecutive network failures (capped at 30s) so
|
|
101
|
+
// a flaky network doesn't hammer the server at the full 5s cadence for
|
|
102
|
+
// the entire 15-minute window. ±20% jitter on retries prevents
|
|
103
|
+
// thundering-herd reconnects when many CLIs lose connectivity at once.
|
|
104
|
+
let wait =
|
|
105
|
+
consecutiveErrors === 0
|
|
106
|
+
? POLL_INTERVAL_MS
|
|
107
|
+
: Math.min(POLL_INTERVAL_MS * Math.pow(2, consecutiveErrors - 1), MAX_BACKOFF_MS);
|
|
108
|
+
if (consecutiveErrors > 0) {
|
|
109
|
+
const jitter = wait * 0.2 * (Math.random() * 2 - 1);
|
|
110
|
+
wait = Math.max(POLL_INTERVAL_MS, wait + jitter);
|
|
111
|
+
}
|
|
112
|
+
await sleepFn(wait);
|
|
113
|
+
let result;
|
|
114
|
+
try {
|
|
115
|
+
result = await pollOnce({ baseUrl, deviceCode: authResp.device_code });
|
|
116
|
+
consecutiveErrors = 0;
|
|
117
|
+
} catch (e) {
|
|
118
|
+
consecutiveErrors++;
|
|
119
|
+
process.stderr.write(`poll error (retry ${consecutiveErrors}): ${e?.message || e}\n`);
|
|
120
|
+
continue;
|
|
121
|
+
}
|
|
122
|
+
if (result.status === "approved" && result.user_id) {
|
|
123
|
+
if (!result.deviceToken) {
|
|
124
|
+
throw new Error("device login approved but server did not return a device token");
|
|
125
|
+
}
|
|
126
|
+
const next = {
|
|
127
|
+
...config,
|
|
128
|
+
baseUrl,
|
|
129
|
+
user_id: result.user_id,
|
|
130
|
+
deviceToken: result.deviceToken,
|
|
131
|
+
deviceId: result.deviceId || config.deviceId,
|
|
132
|
+
device_login_at: new Date().toISOString(),
|
|
133
|
+
};
|
|
134
|
+
await writeJson(configPath, next);
|
|
135
|
+
process.stdout.write(`\n✓ Approved. device token written to ${configPath}\n`);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
if (result.status === "expired") {
|
|
139
|
+
throw new Error("device_code expired — re-run `tracker device-login`");
|
|
140
|
+
}
|
|
141
|
+
if (result.status === "unknown") {
|
|
142
|
+
throw new Error("device_code is unknown — server may have evicted it");
|
|
143
|
+
}
|
|
144
|
+
// status === "pending" → just keep polling silently
|
|
145
|
+
}
|
|
146
|
+
throw new Error("device-login timed out without approval");
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function parseArgs(argv) {
|
|
150
|
+
const out = { json: false, baseUrl: null };
|
|
151
|
+
for (let i = 0; i < argv.length; i++) {
|
|
152
|
+
const a = argv[i];
|
|
153
|
+
if (a === "--json") out.json = true;
|
|
154
|
+
else if (a === "--base-url") {
|
|
155
|
+
out.baseUrl = argv[++i] || null;
|
|
156
|
+
} else throw new Error(`Unknown option: ${a}`);
|
|
157
|
+
}
|
|
158
|
+
return out;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
module.exports = { cmdDeviceLogin, authorize, pollOnce };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const path = require("node:path");
|
|
2
|
+
|
|
3
|
+
const { writeFileAtomic, chmod600IfPossible } = require("../lib/fs");
|
|
4
|
+
const { collectTrackerDiagnostics } = require("../lib/diagnostics");
|
|
5
|
+
|
|
6
|
+
async function cmdDiagnostics(argv = []) {
|
|
7
|
+
const opts = parseArgs(argv);
|
|
8
|
+
const diagnostics = await collectTrackerDiagnostics();
|
|
9
|
+
const json = JSON.stringify(diagnostics, null, opts.compact ? 0 : 2) + "\n";
|
|
10
|
+
|
|
11
|
+
if (opts.out) {
|
|
12
|
+
const outPath = path.resolve(process.cwd(), opts.out);
|
|
13
|
+
await writeFileAtomic(outPath, json);
|
|
14
|
+
await chmod600IfPossible(outPath);
|
|
15
|
+
process.stderr.write(`Wrote diagnostics to: ${outPath}\n`);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
process.stdout.write(json);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function parseArgs(argv) {
|
|
22
|
+
const out = {
|
|
23
|
+
out: null,
|
|
24
|
+
compact: false,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
for (let i = 0; i < argv.length; i++) {
|
|
28
|
+
const a = argv[i];
|
|
29
|
+
if (a === "--out") out.out = argv[++i] || null;
|
|
30
|
+
else if (a === "--compact") out.compact = true;
|
|
31
|
+
else if (a === "--pretty") out.compact = false;
|
|
32
|
+
else throw new Error(`Unknown option: ${a}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return out;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { cmdDiagnostics };
|
|
@@ -0,0 +1,96 @@
|
|
|
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 =
|
|
18
|
+
configStatus.status === "ok" && isPlainObject(configStatus.value) ? configStatus.value : {};
|
|
19
|
+
const runtime = resolveRuntimeConfig({
|
|
20
|
+
cli: { baseUrl: opts.baseUrl },
|
|
21
|
+
config,
|
|
22
|
+
env: process.env,
|
|
23
|
+
});
|
|
24
|
+
const diagnostics = await collectTrackerDiagnostics({ home });
|
|
25
|
+
const cliPath = process.argv[1] ? path.resolve(process.argv[1]) : null;
|
|
26
|
+
|
|
27
|
+
const report = await buildDoctorReport({
|
|
28
|
+
runtime,
|
|
29
|
+
diagnostics,
|
|
30
|
+
fetch: globalThis.fetch,
|
|
31
|
+
paths: {
|
|
32
|
+
trackerDir,
|
|
33
|
+
configPath,
|
|
34
|
+
cliPath,
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
const jsonOutput = opts.json || Boolean(opts.out);
|
|
39
|
+
const payload = JSON.stringify(report, null, jsonOutput ? 2 : 0) + "\n";
|
|
40
|
+
|
|
41
|
+
if (opts.out) {
|
|
42
|
+
const outPath = path.resolve(process.cwd(), opts.out);
|
|
43
|
+
await writeFileAtomic(outPath, payload);
|
|
44
|
+
await chmod600IfPossible(outPath);
|
|
45
|
+
process.stderr.write(`Wrote doctor report to: ${outPath}\n`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (jsonOutput) {
|
|
49
|
+
process.stdout.write(payload);
|
|
50
|
+
} else {
|
|
51
|
+
process.stdout.write(renderHumanReport(report));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (report.summary.critical > 0) {
|
|
55
|
+
process.exitCode = 1;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function parseArgs(argv) {
|
|
60
|
+
const out = { json: false, out: null, baseUrl: null };
|
|
61
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
62
|
+
const arg = argv[i];
|
|
63
|
+
if (arg === "--json") out.json = true;
|
|
64
|
+
else if (arg === "--out") out.out = argv[++i] || null;
|
|
65
|
+
else if (arg === "--base-url") out.baseUrl = argv[++i] || null;
|
|
66
|
+
else throw new Error(`Unknown option: ${arg}`);
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function renderHumanReport(report) {
|
|
72
|
+
const lines = [];
|
|
73
|
+
lines.push("Doctor report");
|
|
74
|
+
lines.push("");
|
|
75
|
+
for (const check of report.checks || []) {
|
|
76
|
+
lines.push(formatCheckLine(check));
|
|
77
|
+
}
|
|
78
|
+
lines.push("");
|
|
79
|
+
lines.push(
|
|
80
|
+
`Summary: ok ${report.summary.ok} | warn ${report.summary.warn} | fail ${report.summary.fail} | critical ${report.summary.critical}`,
|
|
81
|
+
);
|
|
82
|
+
lines.push("");
|
|
83
|
+
return lines.join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function formatCheckLine(check = {}) {
|
|
87
|
+
const status = String(check.status || "unknown").toUpperCase();
|
|
88
|
+
const detail = check.detail ? ` - ${check.detail}` : "";
|
|
89
|
+
return `- [${status}] ${check.id || "unknown"}${detail}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function isPlainObject(value) {
|
|
93
|
+
return Boolean(value && typeof value === "object" && !Array.isArray(value));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = { cmdDoctor };
|