@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
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
const os = require("node:os");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
const fs = require("node:fs/promises");
|
|
4
|
+
|
|
5
|
+
const { readJson } = require("./fs");
|
|
6
|
+
const { readCodexNotify, readEveryCodeNotify } = require("./codex-config");
|
|
7
|
+
const { isClaudeHookConfigured, buildClaudeHookCommand } = require("./claude-config");
|
|
8
|
+
const {
|
|
9
|
+
resolveGeminiConfigDir,
|
|
10
|
+
resolveGeminiSettingsPath,
|
|
11
|
+
buildGeminiHookCommand,
|
|
12
|
+
isGeminiHookConfigured,
|
|
13
|
+
} = require("./gemini-config");
|
|
14
|
+
const { resolveOpencodeConfigDir, isOpencodePluginInstalled } = require("./opencode-config");
|
|
15
|
+
const { normalizeState: normalizeUploadState } = require("./upload-throttle");
|
|
16
|
+
const { probeOpenclawHookState } = require("./openclaw-hook");
|
|
17
|
+
const { probeOpenclawSessionPluginState } = require("./openclaw-session-plugin");
|
|
18
|
+
const { probeGrokHookState } = require("./grok-hook");
|
|
19
|
+
const { resolveTrackerPaths } = require("./tracker-paths");
|
|
20
|
+
// TASK-011: Kiro CLI DB path inlined here to avoid pulling the ~4000-line
|
|
21
|
+
// rollout module on every `tokentracker status` / `diagnostics` call.
|
|
22
|
+
// rollout.js still exports resolveKiroCliDbPath for external callers.
|
|
23
|
+
function resolveKiroCliDbPathInline(env, home) {
|
|
24
|
+
if (env.KIRO_CLI_DB_PATH) return env.KIRO_CLI_DB_PATH;
|
|
25
|
+
const effectiveHome = env.HOME || home;
|
|
26
|
+
return path.join(
|
|
27
|
+
effectiveHome,
|
|
28
|
+
"Library",
|
|
29
|
+
"Application Support",
|
|
30
|
+
"kiro-cli",
|
|
31
|
+
"data.sqlite3",
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function collectTrackerDiagnostics({
|
|
36
|
+
home = os.homedir(),
|
|
37
|
+
codexHome = process.env.CODEX_HOME || path.join(home, ".codex"),
|
|
38
|
+
codeHome = process.env.CODE_HOME || path.join(home, ".code"),
|
|
39
|
+
} = {}) {
|
|
40
|
+
const { trackerDir, binDir } = await resolveTrackerPaths({ home });
|
|
41
|
+
const configPath = path.join(trackerDir, "config.json");
|
|
42
|
+
const queuePath = path.join(trackerDir, "queue.jsonl");
|
|
43
|
+
const queueStatePath = path.join(trackerDir, "queue.state.json");
|
|
44
|
+
const cursorsPath = path.join(trackerDir, "cursors.json");
|
|
45
|
+
const notifySignalPath = path.join(trackerDir, "notify.signal");
|
|
46
|
+
const openclawSignalPath = path.join(trackerDir, "openclaw.signal");
|
|
47
|
+
const throttlePath = path.join(trackerDir, "sync.throttle");
|
|
48
|
+
const uploadThrottlePath = path.join(trackerDir, "upload.throttle.json");
|
|
49
|
+
const autoRetryPath = path.join(trackerDir, "auto.retry.json");
|
|
50
|
+
const codexConfigPath = path.join(codexHome, "config.toml");
|
|
51
|
+
const codeConfigPath = path.join(codeHome, "config.toml");
|
|
52
|
+
const claudeConfigPath = path.join(home, ".claude", "settings.json");
|
|
53
|
+
const geminiConfigDir = resolveGeminiConfigDir({ home, env: process.env });
|
|
54
|
+
const geminiSettingsPath = resolveGeminiSettingsPath({ configDir: geminiConfigDir });
|
|
55
|
+
const opencodeConfigDir = resolveOpencodeConfigDir({ home, env: process.env });
|
|
56
|
+
const grokHome =
|
|
57
|
+
process.env.TOKENTRACKER_GROK_HOME ||
|
|
58
|
+
process.env.GROK_HOME ||
|
|
59
|
+
path.join(home, ".grok");
|
|
60
|
+
|
|
61
|
+
const config = await readJson(configPath);
|
|
62
|
+
const cursors = await readJson(cursorsPath);
|
|
63
|
+
const queueState = (await readJson(queueStatePath)) || { offset: 0 };
|
|
64
|
+
const uploadThrottle = normalizeUploadState(await readJson(uploadThrottlePath));
|
|
65
|
+
const autoRetry = await readJson(autoRetryPath);
|
|
66
|
+
|
|
67
|
+
const queueSize = await safeStatSize(queuePath);
|
|
68
|
+
const offsetBytes = Number(queueState.offset || 0);
|
|
69
|
+
const pendingBytes = Math.max(0, queueSize - offsetBytes);
|
|
70
|
+
|
|
71
|
+
const lastNotify = (await safeReadText(notifySignalPath))?.trim() || null;
|
|
72
|
+
const lastOpenclawSync = (await safeReadText(openclawSignalPath))?.trim() || null;
|
|
73
|
+
const lastNotifySpawn = parseEpochMsToIso((await safeReadText(throttlePath))?.trim() || null);
|
|
74
|
+
|
|
75
|
+
const codexNotifyRaw = await readCodexNotify(codexConfigPath);
|
|
76
|
+
const notifyConfigured = Array.isArray(codexNotifyRaw) && codexNotifyRaw.length > 0;
|
|
77
|
+
const codexNotify = notifyConfigured ? codexNotifyRaw.map((v) => redactValue(v, home)) : null;
|
|
78
|
+
const everyCodeNotifyRaw = await readEveryCodeNotify(codeConfigPath);
|
|
79
|
+
const everyCodeConfigured = Array.isArray(everyCodeNotifyRaw) && everyCodeNotifyRaw.length > 0;
|
|
80
|
+
const everyCodeNotify = everyCodeConfigured
|
|
81
|
+
? everyCodeNotifyRaw.map((v) => redactValue(v, home))
|
|
82
|
+
: null;
|
|
83
|
+
const claudeHookCommand = buildClaudeHookCommand(path.join(binDir, "notify.cjs"));
|
|
84
|
+
const claudeHookConfigured = await isClaudeHookConfigured({
|
|
85
|
+
settingsPath: claudeConfigPath,
|
|
86
|
+
hookCommand: claudeHookCommand,
|
|
87
|
+
});
|
|
88
|
+
const geminiHookCommand = buildGeminiHookCommand(path.join(binDir, "notify.cjs"));
|
|
89
|
+
const geminiHookConfigured = await isGeminiHookConfigured({
|
|
90
|
+
settingsPath: geminiSettingsPath,
|
|
91
|
+
hookCommand: geminiHookCommand,
|
|
92
|
+
});
|
|
93
|
+
const opencodePluginConfigured = await isOpencodePluginInstalled({
|
|
94
|
+
configDir: opencodeConfigDir,
|
|
95
|
+
});
|
|
96
|
+
const openclawSessionPluginState = await probeOpenclawSessionPluginState({
|
|
97
|
+
home,
|
|
98
|
+
trackerDir,
|
|
99
|
+
env: process.env,
|
|
100
|
+
});
|
|
101
|
+
const openclawHookState = await probeOpenclawHookState({ home, trackerDir, env: process.env });
|
|
102
|
+
const grokHookState = await probeGrokHookState({ home, trackerDir, env: process.env });
|
|
103
|
+
|
|
104
|
+
// Kiro IDE and Kiro CLI sub-path presence — merged under one "kiro" source
|
|
105
|
+
// at token/cost aggregation level; operators need visibility of both
|
|
106
|
+
// sub-paths here for debugging.
|
|
107
|
+
const kiroIdeDevDataDir = path.join(
|
|
108
|
+
home,
|
|
109
|
+
"Library",
|
|
110
|
+
"Application Support",
|
|
111
|
+
"Kiro",
|
|
112
|
+
"User",
|
|
113
|
+
"globalStorage",
|
|
114
|
+
"kiro.kiroagent",
|
|
115
|
+
"dev_data",
|
|
116
|
+
);
|
|
117
|
+
const kiroIdePresent =
|
|
118
|
+
(await safeStatSize(path.join(kiroIdeDevDataDir, "devdata.sqlite"))) > 0 ||
|
|
119
|
+
(await safeStatSize(path.join(kiroIdeDevDataDir, "tokens_generated.jsonl"))) > 0;
|
|
120
|
+
const kiroCliDbPath = resolveKiroCliDbPathInline(process.env, home);
|
|
121
|
+
const kiroCliPresent = require("node:fs").existsSync(kiroCliDbPath);
|
|
122
|
+
|
|
123
|
+
const lastSuccessAt = uploadThrottle.lastSuccessMs
|
|
124
|
+
? new Date(uploadThrottle.lastSuccessMs).toISOString()
|
|
125
|
+
: null;
|
|
126
|
+
const autoRetryAt = parseEpochMsToIso(autoRetry?.retryAtMs);
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
ok: true,
|
|
130
|
+
version: 1,
|
|
131
|
+
generated_at: new Date().toISOString(),
|
|
132
|
+
env: {
|
|
133
|
+
node: process.version,
|
|
134
|
+
platform: process.platform,
|
|
135
|
+
arch: process.arch,
|
|
136
|
+
},
|
|
137
|
+
paths: {
|
|
138
|
+
tracker_dir: redactValue(trackerDir, home),
|
|
139
|
+
codex_home: redactValue(codexHome, home),
|
|
140
|
+
codex_config: redactValue(codexConfigPath, home),
|
|
141
|
+
code_home: redactValue(codeHome, home),
|
|
142
|
+
code_config: redactValue(codeConfigPath, home),
|
|
143
|
+
claude_config: redactValue(claudeConfigPath, home),
|
|
144
|
+
gemini_config: redactValue(geminiSettingsPath, home),
|
|
145
|
+
opencode_config: redactValue(opencodeConfigDir, home),
|
|
146
|
+
grok_home: redactValue(grokHome, home),
|
|
147
|
+
grok_hooks: redactValue(grokHookState?.grokHooksDir, home),
|
|
148
|
+
grok_handler: redactValue(grokHookState?.handlerPath, home),
|
|
149
|
+
kiro_ide_dev_data: redactValue(kiroIdeDevDataDir, home),
|
|
150
|
+
kiro_cli_db: redactValue(kiroCliDbPath, home),
|
|
151
|
+
},
|
|
152
|
+
kiro: {
|
|
153
|
+
ide_present: kiroIdePresent,
|
|
154
|
+
cli_present: kiroCliPresent,
|
|
155
|
+
cli_approximation:
|
|
156
|
+
"Kiro CLI does not persist explicit token counts (billing is credit-based on Bedrock). Tokens are approximated at 4 chars/token from user prompt chars and assistant response chars. Source rows that came through this path have model='kiro-cli-agent' when the underlying model is unknown (auto-routing); known Bedrock ARNs canonicalize to their short name (e.g. claude-sonnet-4).",
|
|
157
|
+
merge_policy:
|
|
158
|
+
"Kiro IDE and Kiro CLI both emit source='kiro' in queue.jsonl so token, cost, heatmap, and leaderboard aggregations merge transparently. Use this block to distinguish sub-path contributions.",
|
|
159
|
+
},
|
|
160
|
+
config: {
|
|
161
|
+
base_url: typeof config?.baseUrl === "string" ? config.baseUrl : null,
|
|
162
|
+
device_token: config?.deviceToken ? "set" : "unset",
|
|
163
|
+
device_id: maskId(config?.deviceId),
|
|
164
|
+
installed_at: typeof config?.installedAt === "string" ? config.installedAt : null,
|
|
165
|
+
},
|
|
166
|
+
parse: {
|
|
167
|
+
updated_at: typeof cursors?.updatedAt === "string" ? cursors.updatedAt : null,
|
|
168
|
+
file_count:
|
|
169
|
+
cursors?.files && typeof cursors.files === "object"
|
|
170
|
+
? Object.keys(cursors.files).length
|
|
171
|
+
: null,
|
|
172
|
+
},
|
|
173
|
+
queue: {
|
|
174
|
+
size_bytes: queueSize,
|
|
175
|
+
offset_bytes: offsetBytes,
|
|
176
|
+
pending_bytes: pendingBytes,
|
|
177
|
+
updated_at: typeof queueState.updatedAt === "string" ? queueState.updatedAt : null,
|
|
178
|
+
},
|
|
179
|
+
notify: {
|
|
180
|
+
last_notify: lastNotify,
|
|
181
|
+
last_openclaw_triggered_sync: lastOpenclawSync,
|
|
182
|
+
last_notify_triggered_sync: lastNotifySpawn,
|
|
183
|
+
codex_notify_configured: notifyConfigured,
|
|
184
|
+
codex_notify: codexNotify,
|
|
185
|
+
every_code_notify_configured: everyCodeConfigured,
|
|
186
|
+
every_code_notify: everyCodeNotify,
|
|
187
|
+
claude_hook_configured: claudeHookConfigured,
|
|
188
|
+
gemini_hook_configured: geminiHookConfigured,
|
|
189
|
+
opencode_plugin_configured: opencodePluginConfigured,
|
|
190
|
+
openclaw_session_plugin_configured: Boolean(openclawSessionPluginState?.configured),
|
|
191
|
+
openclaw_session_plugin_linked: Boolean(openclawSessionPluginState?.linked),
|
|
192
|
+
openclaw_session_plugin_enabled: Boolean(openclawSessionPluginState?.enabled),
|
|
193
|
+
openclaw_hook_configured: Boolean(openclawHookState?.configured),
|
|
194
|
+
openclaw_hook_linked: Boolean(openclawHookState?.linked),
|
|
195
|
+
openclaw_hook_enabled: Boolean(openclawHookState?.enabled),
|
|
196
|
+
grok_hook_configured: Boolean(grokHookState?.configured),
|
|
197
|
+
grok_hook_exists: Boolean(grokHookState?.hookExists),
|
|
198
|
+
grok_hook_handler_exists: Boolean(grokHookState?.handlerExists),
|
|
199
|
+
grok_sessions_dir: redactValue(grokHookState?.sessionsDir, home),
|
|
200
|
+
},
|
|
201
|
+
upload: {
|
|
202
|
+
last_success_at: lastSuccessAt,
|
|
203
|
+
next_allowed_after: parseEpochMsToIso(uploadThrottle.nextAllowedAtMs || null),
|
|
204
|
+
backoff_until: parseEpochMsToIso(uploadThrottle.backoffUntilMs || null),
|
|
205
|
+
last_error: uploadThrottle.lastError
|
|
206
|
+
? {
|
|
207
|
+
at: uploadThrottle.lastErrorAt || null,
|
|
208
|
+
message: redactError(String(uploadThrottle.lastError), home),
|
|
209
|
+
}
|
|
210
|
+
: null,
|
|
211
|
+
},
|
|
212
|
+
auto_retry: autoRetryAt
|
|
213
|
+
? {
|
|
214
|
+
next_retry_at: autoRetryAt,
|
|
215
|
+
reason: typeof autoRetry?.reason === "string" ? autoRetry.reason : null,
|
|
216
|
+
pending_bytes: Number.isFinite(Number(autoRetry?.pendingBytes))
|
|
217
|
+
? Math.max(0, Number(autoRetry.pendingBytes))
|
|
218
|
+
: null,
|
|
219
|
+
scheduled_at: typeof autoRetry?.scheduledAt === "string" ? autoRetry.scheduledAt : null,
|
|
220
|
+
source: typeof autoRetry?.source === "string" ? autoRetry.source : null,
|
|
221
|
+
}
|
|
222
|
+
: null,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function maskId(v) {
|
|
227
|
+
if (typeof v !== "string") return null;
|
|
228
|
+
const s = v.trim();
|
|
229
|
+
if (s.length < 12) return null;
|
|
230
|
+
return `${s.slice(0, 8)}…${s.slice(-4)}`;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function redactValue(value, home) {
|
|
234
|
+
if (typeof value !== "string") return value;
|
|
235
|
+
if (typeof home !== "string" || home.length === 0) return value;
|
|
236
|
+
const homeNorm = home.endsWith(path.sep) ? home.slice(0, -1) : home;
|
|
237
|
+
return value.startsWith(homeNorm) ? `~${value.slice(homeNorm.length)}` : value;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function redactError(message, home) {
|
|
241
|
+
if (typeof message !== "string") return message;
|
|
242
|
+
if (typeof home !== "string" || home.length === 0) return message;
|
|
243
|
+
const homeNorm = home.endsWith(path.sep) ? home.slice(0, -1) : home;
|
|
244
|
+
return message.split(homeNorm).join("~");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
async function safeStatSize(p) {
|
|
248
|
+
try {
|
|
249
|
+
const st = await fs.stat(p);
|
|
250
|
+
return st && st.isFile() ? st.size : 0;
|
|
251
|
+
} catch (_e) {
|
|
252
|
+
return 0;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function safeReadText(p) {
|
|
257
|
+
try {
|
|
258
|
+
return await fs.readFile(p, "utf8");
|
|
259
|
+
} catch (_e) {
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
function parseEpochMsToIso(v) {
|
|
265
|
+
const ms = Number(v);
|
|
266
|
+
if (!Number.isFinite(ms) || ms <= 0) return null;
|
|
267
|
+
const d = new Date(ms);
|
|
268
|
+
if (Number.isNaN(d.getTime())) return null;
|
|
269
|
+
return d.toISOString();
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module.exports = { collectTrackerDiagnostics };
|
|
@@ -0,0 +1,338 @@
|
|
|
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 =
|
|
48
|
+
typeof runtime.baseUrl === "string" && runtime.baseUrl.trim() ? runtime.baseUrl.trim() : null;
|
|
49
|
+
const deviceToken =
|
|
50
|
+
typeof runtime.deviceToken === "string" && runtime.deviceToken.trim() ? "set" : "unset";
|
|
51
|
+
const dashboardUrl =
|
|
52
|
+
typeof runtime.dashboardUrl === "string" && runtime.dashboardUrl.trim()
|
|
53
|
+
? runtime.dashboardUrl.trim()
|
|
54
|
+
: null;
|
|
55
|
+
const httpTimeoutMs = Number.isFinite(Number(runtime.httpTimeoutMs))
|
|
56
|
+
? Number(runtime.httpTimeoutMs)
|
|
57
|
+
: null;
|
|
58
|
+
const debug = Boolean(runtime.debug);
|
|
59
|
+
const autoRetryNoSpawn = Boolean(runtime.autoRetryNoSpawn);
|
|
60
|
+
|
|
61
|
+
checks.push({
|
|
62
|
+
id: "runtime.base_url",
|
|
63
|
+
status: baseUrl ? "ok" : "fail",
|
|
64
|
+
detail: baseUrl ? "base_url set" : "base_url missing",
|
|
65
|
+
critical: false,
|
|
66
|
+
meta: {
|
|
67
|
+
base_url: baseUrl,
|
|
68
|
+
source: runtime?.sources?.baseUrl || null,
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
checks.push({
|
|
73
|
+
id: "runtime.device_token",
|
|
74
|
+
status: deviceToken === "set" ? "ok" : "warn",
|
|
75
|
+
detail: deviceToken === "set" ? "device token set" : "device token missing",
|
|
76
|
+
critical: false,
|
|
77
|
+
meta: {
|
|
78
|
+
device_token: deviceToken,
|
|
79
|
+
source: runtime?.sources?.deviceToken || null,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
checks.push({
|
|
84
|
+
id: "runtime.dashboard_url",
|
|
85
|
+
status: "ok",
|
|
86
|
+
detail: dashboardUrl ? "dashboard_url set" : "dashboard_url unset",
|
|
87
|
+
critical: false,
|
|
88
|
+
meta: {
|
|
89
|
+
dashboard_url: dashboardUrl,
|
|
90
|
+
source: runtime?.sources?.dashboardUrl || null,
|
|
91
|
+
},
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
checks.push({
|
|
95
|
+
id: "runtime.http_timeout_ms",
|
|
96
|
+
status: "ok",
|
|
97
|
+
detail: "http timeout resolved",
|
|
98
|
+
critical: false,
|
|
99
|
+
meta: {
|
|
100
|
+
http_timeout_ms: httpTimeoutMs,
|
|
101
|
+
source: runtime?.sources?.httpTimeoutMs || null,
|
|
102
|
+
},
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
checks.push({
|
|
106
|
+
id: "runtime.debug",
|
|
107
|
+
status: "ok",
|
|
108
|
+
detail: debug ? "debug enabled" : "debug disabled",
|
|
109
|
+
critical: false,
|
|
110
|
+
meta: {
|
|
111
|
+
debug,
|
|
112
|
+
source: runtime?.sources?.debug || null,
|
|
113
|
+
},
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
checks.push({
|
|
117
|
+
id: "runtime.auto_retry_no_spawn",
|
|
118
|
+
status: "ok",
|
|
119
|
+
detail: autoRetryNoSpawn ? "auto retry spawn disabled" : "auto retry spawn enabled",
|
|
120
|
+
critical: false,
|
|
121
|
+
meta: {
|
|
122
|
+
auto_retry_no_spawn: autoRetryNoSpawn,
|
|
123
|
+
source: runtime?.sources?.autoRetryNoSpawn || null,
|
|
124
|
+
},
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return checks;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
async function checkTrackerDir(trackerDir) {
|
|
131
|
+
try {
|
|
132
|
+
const st = await fs.stat(trackerDir);
|
|
133
|
+
if (!st.isDirectory()) {
|
|
134
|
+
return {
|
|
135
|
+
id: "fs.tracker_dir",
|
|
136
|
+
status: "fail",
|
|
137
|
+
detail: "tracker dir is not a directory",
|
|
138
|
+
critical: true,
|
|
139
|
+
meta: { path: trackerDir },
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
await fs.access(trackerDir, constants.R_OK);
|
|
143
|
+
return {
|
|
144
|
+
id: "fs.tracker_dir",
|
|
145
|
+
status: "ok",
|
|
146
|
+
detail: "tracker dir readable",
|
|
147
|
+
critical: false,
|
|
148
|
+
meta: { path: trackerDir },
|
|
149
|
+
};
|
|
150
|
+
} catch (err) {
|
|
151
|
+
if (err && (err.code === "ENOENT" || err.code === "ENOTDIR")) {
|
|
152
|
+
return {
|
|
153
|
+
id: "fs.tracker_dir",
|
|
154
|
+
status: "warn",
|
|
155
|
+
detail: "tracker dir missing",
|
|
156
|
+
critical: false,
|
|
157
|
+
meta: { path: trackerDir },
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
if (err && (err.code === "EACCES" || err.code === "EPERM")) {
|
|
161
|
+
return {
|
|
162
|
+
id: "fs.tracker_dir",
|
|
163
|
+
status: "fail",
|
|
164
|
+
detail: "tracker dir permission denied",
|
|
165
|
+
critical: true,
|
|
166
|
+
meta: { path: trackerDir, code: err.code },
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
return {
|
|
170
|
+
id: "fs.tracker_dir",
|
|
171
|
+
status: "fail",
|
|
172
|
+
detail: "tracker dir error",
|
|
173
|
+
critical: true,
|
|
174
|
+
meta: { path: trackerDir, code: err?.code || "error" },
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
async function checkConfigJson(configPath) {
|
|
180
|
+
const res = await readJsonStrict(configPath);
|
|
181
|
+
if (res.status === "ok") {
|
|
182
|
+
return {
|
|
183
|
+
id: "fs.config_json",
|
|
184
|
+
status: "ok",
|
|
185
|
+
detail: "config.json readable",
|
|
186
|
+
critical: false,
|
|
187
|
+
meta: { path: configPath },
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
if (res.status === "missing") {
|
|
191
|
+
return {
|
|
192
|
+
id: "fs.config_json",
|
|
193
|
+
status: "warn",
|
|
194
|
+
detail: "config.json missing",
|
|
195
|
+
critical: false,
|
|
196
|
+
meta: { path: configPath },
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
if (res.status === "invalid") {
|
|
200
|
+
return {
|
|
201
|
+
id: "fs.config_json",
|
|
202
|
+
status: "fail",
|
|
203
|
+
detail: "config.json invalid",
|
|
204
|
+
critical: true,
|
|
205
|
+
meta: { path: configPath },
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
return {
|
|
209
|
+
id: "fs.config_json",
|
|
210
|
+
status: "fail",
|
|
211
|
+
detail: "config.json read error",
|
|
212
|
+
critical: true,
|
|
213
|
+
meta: { path: configPath },
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
async function checkCliEntrypoint(cliPath) {
|
|
218
|
+
try {
|
|
219
|
+
const st = await fs.stat(cliPath);
|
|
220
|
+
if (!st.isFile()) {
|
|
221
|
+
return {
|
|
222
|
+
id: "cli.entrypoint",
|
|
223
|
+
status: "fail",
|
|
224
|
+
detail: "cli entrypoint is not a file",
|
|
225
|
+
critical: false,
|
|
226
|
+
meta: { path: cliPath },
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
await fs.access(cliPath, constants.R_OK);
|
|
230
|
+
if (process.platform !== "win32") {
|
|
231
|
+
await fs.access(cliPath, constants.X_OK);
|
|
232
|
+
}
|
|
233
|
+
return {
|
|
234
|
+
id: "cli.entrypoint",
|
|
235
|
+
status: "ok",
|
|
236
|
+
detail: "cli entrypoint readable",
|
|
237
|
+
critical: false,
|
|
238
|
+
meta: { path: cliPath },
|
|
239
|
+
};
|
|
240
|
+
} catch (err) {
|
|
241
|
+
return {
|
|
242
|
+
id: "cli.entrypoint",
|
|
243
|
+
status: "fail",
|
|
244
|
+
detail: "cli entrypoint not accessible",
|
|
245
|
+
critical: false,
|
|
246
|
+
meta: { path: cliPath, code: err?.code || "error" },
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
async function checkNetwork({ baseUrl, fetch }) {
|
|
252
|
+
if (!baseUrl) {
|
|
253
|
+
return {
|
|
254
|
+
id: "network.base_url",
|
|
255
|
+
status: "warn",
|
|
256
|
+
detail: "base_url missing (skipped)",
|
|
257
|
+
critical: false,
|
|
258
|
+
meta: { base_url: null },
|
|
259
|
+
};
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
const start = Date.now();
|
|
263
|
+
try {
|
|
264
|
+
if (typeof fetch !== "function") throw new Error("Missing fetch");
|
|
265
|
+
const res = await fetch(baseUrl, { method: "GET" });
|
|
266
|
+
const latency = Date.now() - start;
|
|
267
|
+
return {
|
|
268
|
+
id: "network.base_url",
|
|
269
|
+
status: "ok",
|
|
270
|
+
detail: `HTTP ${res.status} (reachable)`,
|
|
271
|
+
critical: false,
|
|
272
|
+
meta: {
|
|
273
|
+
status_code: res.status,
|
|
274
|
+
latency_ms: latency,
|
|
275
|
+
base_url: baseUrl,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
} catch (err) {
|
|
279
|
+
const latency = Date.now() - start;
|
|
280
|
+
return {
|
|
281
|
+
id: "network.base_url",
|
|
282
|
+
status: "fail",
|
|
283
|
+
detail: "Network error",
|
|
284
|
+
critical: false,
|
|
285
|
+
meta: {
|
|
286
|
+
error: err?.message || String(err),
|
|
287
|
+
latency_ms: latency,
|
|
288
|
+
base_url: baseUrl,
|
|
289
|
+
},
|
|
290
|
+
};
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function buildDiagnosticsChecks(diagnostics) {
|
|
295
|
+
const checks = [];
|
|
296
|
+
const notify = diagnostics?.notify || {};
|
|
297
|
+
const notifyConfigured = Boolean(
|
|
298
|
+
notify.codex_notify_configured ||
|
|
299
|
+
notify.every_code_notify_configured ||
|
|
300
|
+
notify.claude_hook_configured ||
|
|
301
|
+
notify.gemini_hook_configured ||
|
|
302
|
+
notify.opencode_plugin_configured ||
|
|
303
|
+
notify.openclaw_hook_configured,
|
|
304
|
+
);
|
|
305
|
+
|
|
306
|
+
checks.push({
|
|
307
|
+
id: "notify.configured",
|
|
308
|
+
status: notifyConfigured ? "ok" : "warn",
|
|
309
|
+
detail: notifyConfigured ? "notify configured" : "notify not configured",
|
|
310
|
+
critical: false,
|
|
311
|
+
meta: { configured: notifyConfigured },
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const uploadError = diagnostics?.upload?.last_error || null;
|
|
315
|
+
checks.push({
|
|
316
|
+
id: "upload.last_error",
|
|
317
|
+
status: uploadError ? "warn" : "ok",
|
|
318
|
+
detail: uploadError ? "last upload error present" : "no upload errors",
|
|
319
|
+
critical: false,
|
|
320
|
+
meta: { last_error: uploadError ? uploadError.message || null : null },
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
return checks;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function summarizeChecks(checks = []) {
|
|
327
|
+
const summary = { ok: 0, warn: 0, fail: 0, critical: 0 };
|
|
328
|
+
for (const check of checks) {
|
|
329
|
+
if (!check || typeof check.status !== "string") continue;
|
|
330
|
+
if (check.status === "ok") summary.ok += 1;
|
|
331
|
+
else if (check.status === "warn") summary.warn += 1;
|
|
332
|
+
else if (check.status === "fail") summary.fail += 1;
|
|
333
|
+
if (check.status === "fail" && check.critical) summary.critical += 1;
|
|
334
|
+
}
|
|
335
|
+
return summary;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
module.exports = { buildDoctorReport };
|
package/src/lib/fs.js
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
const fs = require("node:fs/promises");
|
|
2
|
+
const path = require("node:path");
|
|
3
|
+
|
|
4
|
+
async function ensureDir(p) {
|
|
5
|
+
await fs.mkdir(p, { recursive: true });
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
async function writeFileAtomic(filePath, content) {
|
|
9
|
+
const dir = path.dirname(filePath);
|
|
10
|
+
await ensureDir(dir);
|
|
11
|
+
const tmp = `${filePath}.tmp.${Date.now()}`;
|
|
12
|
+
await fs.writeFile(tmp, content, { encoding: "utf8" });
|
|
13
|
+
await fs.rename(tmp, filePath);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function readJson(filePath) {
|
|
17
|
+
try {
|
|
18
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
19
|
+
return JSON.parse(raw);
|
|
20
|
+
} catch (_e) {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
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
|
+
|
|
40
|
+
async function writeJson(filePath, obj) {
|
|
41
|
+
await writeFileAtomic(filePath, JSON.stringify(obj, null, 2) + "\n");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function chmod600IfPossible(filePath) {
|
|
45
|
+
try {
|
|
46
|
+
await fs.chmod(filePath, 0o600);
|
|
47
|
+
} catch (_e) {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const LOCK_STALE_MS = 5 * 60 * 1000; // 5 minutes
|
|
51
|
+
|
|
52
|
+
async function openLock(lockPath, { quietIfLocked }) {
|
|
53
|
+
try {
|
|
54
|
+
const handle = await fs.open(lockPath, "wx");
|
|
55
|
+
return {
|
|
56
|
+
async release() {
|
|
57
|
+
await handle.close().catch(() => {});
|
|
58
|
+
await fs.unlink(lockPath).catch(() => {});
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
} catch (e) {
|
|
62
|
+
if (e && e.code === "EEXIST") {
|
|
63
|
+
// Check if lock is stale
|
|
64
|
+
try {
|
|
65
|
+
const stat = await fs.stat(lockPath);
|
|
66
|
+
if (Date.now() - stat.mtimeMs > LOCK_STALE_MS) {
|
|
67
|
+
await fs.unlink(lockPath).catch(() => {});
|
|
68
|
+
return openLock(lockPath, { quietIfLocked });
|
|
69
|
+
}
|
|
70
|
+
} catch (_statErr) {
|
|
71
|
+
// Lock file disappeared between checks, retry
|
|
72
|
+
return openLock(lockPath, { quietIfLocked });
|
|
73
|
+
}
|
|
74
|
+
if (!quietIfLocked) {
|
|
75
|
+
process.stdout.write("Another sync is already running.\n");
|
|
76
|
+
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
throw e;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = {
|
|
84
|
+
ensureDir,
|
|
85
|
+
writeFileAtomic,
|
|
86
|
+
readJson,
|
|
87
|
+
readJsonStrict,
|
|
88
|
+
writeJson,
|
|
89
|
+
chmod600IfPossible,
|
|
90
|
+
openLock,
|
|
91
|
+
};
|