@ipv9/tokentracker-cli 0.39.38 → 0.39.39
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 +41 -9
- package/dashboard/dist/assets/{Card-DD4s0V6T.js → Card-BbRYRVXf.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-DYKHv0m3.js +60 -0
- package/dashboard/dist/assets/{FadeIn-CUSBK0jm.js → FadeIn-CjAacJ5t.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-PHF4lPax.js → IpCheckPage-BGALK8Vy.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-dS9C9Vdt.js → LimitsPage-LkZGPYvg.js} +2 -2
- package/dashboard/dist/assets/{LocalOnlyNotice-DCmtS_j5.js → LocalOnlyNotice-Dxo2vFMQ.js} +1 -1
- package/dashboard/dist/assets/PopoverPopup-DP9fwdtl.js +1 -0
- package/dashboard/dist/assets/Select-DPE53NmM.js +1 -0
- package/dashboard/dist/assets/SelectItemText-D69v77is.js +12 -0
- package/dashboard/dist/assets/SettingsPage-Dxzqpi02.js +1 -0
- package/dashboard/dist/assets/SkillsPage-Bo7e39AB.js +1 -0
- package/dashboard/dist/assets/WidgetsPage-HHcIr_Eq.js +1 -0
- package/dashboard/dist/assets/WrappedPage-C-_jLXXU.js +1 -0
- package/dashboard/dist/assets/{arrow-up-right-CjaBQnwI.js → arrow-up-right-BJvJHUGj.js} +1 -1
- package/dashboard/dist/assets/{download-DLBqkSwp.js → download-CnzY_EgN.js} +1 -1
- package/dashboard/dist/assets/format-1w-q5n2f.js +1 -0
- package/dashboard/dist/assets/main-1kxAVM3m.css +1 -0
- package/dashboard/dist/assets/main-3gBEI6Jl.js +694 -0
- package/dashboard/dist/assets/mock-data-CJOMGcL9.js +1 -0
- package/dashboard/dist/assets/use-limits-display-prefs-D1nT9-mr.js +1 -0
- package/dashboard/dist/assets/use-native-settings-Dh6NzqjY.js +1 -0
- package/dashboard/dist/assets/use-usage-limits-BXR6lzGf.js +1 -0
- package/dashboard/dist/assets/useCurrency-Hhs5jbWw.js +1 -0
- package/dashboard/dist/index.html +15 -27
- package/dashboard/dist/llms.txt +0 -2
- package/dashboard/dist/sitemap.xml +0 -12
- package/package.json +7 -4
- package/src/cli.js +4 -13
- package/src/commands/doctor.js +1 -3
- package/src/commands/init.js +63 -53
- package/src/commands/serve.js +2 -50
- package/src/commands/status.js +1 -55
- package/src/commands/sync.js +134 -227
- package/src/lib/{browser-auth.js → browser-open.js} +3 -149
- package/src/lib/claude-categorizer.js +204 -32
- package/src/lib/codex-context-breakdown.js +83 -7
- package/src/lib/codex-rollout-parser.js +52 -10
- package/src/lib/diagnostics.js +1 -49
- package/src/lib/doctor.js +12 -87
- package/src/lib/file-identity.js +69 -0
- package/src/lib/init-flow.js +6 -3
- package/src/lib/local-api.js +6 -364
- package/src/lib/pricing/curated-overrides.json +1 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +392 -144
- package/src/lib/runtime-config.js +0 -32
- package/src/lib/sqlite-reader.js +83 -8
- package/dashboard/dist/assets/ActivityHeatmap-BrehqUIb.js +0 -42
- package/dashboard/dist/assets/DashboardPage-CyUShBRJ.js +0 -19
- package/dashboard/dist/assets/DevicePage-BA74Su1l.js +0 -1
- package/dashboard/dist/assets/DialogTitle-DoKsz5wi.js +0 -1
- package/dashboard/dist/assets/InsforgeUserHeaderControls-Bk_U2dHr.js +0 -1
- package/dashboard/dist/assets/LandingPage-Bbeu84Iu.js +0 -4356
- package/dashboard/dist/assets/LeaderboardAvatar-ByqeB6bM.js +0 -1
- package/dashboard/dist/assets/LeaderboardPage-BeWXF00S.js +0 -6
- package/dashboard/dist/assets/LeaderboardProfileModal-C4yvbZd_.js +0 -72
- package/dashboard/dist/assets/LeaderboardProfilePage-D3cbSh5o.js +0 -1
- package/dashboard/dist/assets/LoginPage-Ch3UQoWj.js +0 -1
- package/dashboard/dist/assets/PopoverPopup-DjG0gjlA.js +0 -1
- package/dashboard/dist/assets/Select-HzIjWJpE.js +0 -1
- package/dashboard/dist/assets/SelectItemText-f0tCwtvZ.js +0 -1
- package/dashboard/dist/assets/SettingsPage-DJOZAlZy.js +0 -1
- package/dashboard/dist/assets/SkillsPage-BQcyJIdx.js +0 -1
- package/dashboard/dist/assets/WidgetsPage-B_vxuV09.js +0 -1
- package/dashboard/dist/assets/WrappedPage-CZN9Y3Vz.js +0 -1
- package/dashboard/dist/assets/index-BeoRn2gJ.js +0 -2
- package/dashboard/dist/assets/info-BAIxlHIB.js +0 -1
- package/dashboard/dist/assets/main-DAcSUW2t.css +0 -1
- package/dashboard/dist/assets/main-pi07CfeC.js +0 -1004
- package/dashboard/dist/assets/use-limits-display-prefs-CMfASZsh.js +0 -1
- package/dashboard/dist/assets/use-native-settings-CfPik-Ao.js +0 -1
- package/dashboard/dist/assets/use-spring-wsmJQRqb.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-Bt0Dp6kx.js +0 -1
- package/dashboard/dist/assets/useCurrency-CsaAMc3j.js +0 -1
- package/dashboard/dist/assets/useOpenInteractionType-CPzi5Z8M.js +0 -12
- package/dashboard/dist/share.html +0 -140
- package/src/commands/device-login.js +0 -161
- package/src/lib/upload-throttle.js +0 -148
package/src/lib/diagnostics.js
CHANGED
|
@@ -12,7 +12,6 @@ const {
|
|
|
12
12
|
isGeminiHookConfigured,
|
|
13
13
|
} = require("./gemini-config");
|
|
14
14
|
const { resolveOpencodeConfigDir, isOpencodePluginInstalled } = require("./opencode-config");
|
|
15
|
-
const { normalizeState: normalizeUploadState } = require("./upload-throttle");
|
|
16
15
|
const { probeOpenclawHookState } = require("./openclaw-hook");
|
|
17
16
|
const { probeOpenclawSessionPluginState } = require("./openclaw-session-plugin");
|
|
18
17
|
const { probeGrokHookState } = require("./grok-hook");
|
|
@@ -40,13 +39,10 @@ async function collectTrackerDiagnostics({
|
|
|
40
39
|
const { trackerDir, binDir } = await resolveTrackerPaths({ home });
|
|
41
40
|
const configPath = path.join(trackerDir, "config.json");
|
|
42
41
|
const queuePath = path.join(trackerDir, "queue.jsonl");
|
|
43
|
-
const queueStatePath = path.join(trackerDir, "queue.state.json");
|
|
44
42
|
const cursorsPath = path.join(trackerDir, "cursors.json");
|
|
45
43
|
const notifySignalPath = path.join(trackerDir, "notify.signal");
|
|
46
44
|
const openclawSignalPath = path.join(trackerDir, "openclaw.signal");
|
|
47
45
|
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
46
|
const codexConfigPath = path.join(codexHome, "config.toml");
|
|
51
47
|
const codeConfigPath = path.join(codeHome, "config.toml");
|
|
52
48
|
const claudeConfigPath = path.join(home, ".claude", "settings.json");
|
|
@@ -60,13 +56,8 @@ async function collectTrackerDiagnostics({
|
|
|
60
56
|
|
|
61
57
|
const config = await readJson(configPath);
|
|
62
58
|
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
59
|
|
|
67
60
|
const queueSize = await safeStatSize(queuePath);
|
|
68
|
-
const offsetBytes = Number(queueState.offset || 0);
|
|
69
|
-
const pendingBytes = Math.max(0, queueSize - offsetBytes);
|
|
70
61
|
|
|
71
62
|
const lastNotify = (await safeReadText(notifySignalPath))?.trim() || null;
|
|
72
63
|
const lastOpenclawSync = (await safeReadText(openclawSignalPath))?.trim() || null;
|
|
@@ -120,11 +111,6 @@ async function collectTrackerDiagnostics({
|
|
|
120
111
|
const kiroCliDbPath = resolveKiroCliDbPathInline(process.env, home);
|
|
121
112
|
const kiroCliPresent = require("node:fs").existsSync(kiroCliDbPath);
|
|
122
113
|
|
|
123
|
-
const lastSuccessAt = uploadThrottle.lastSuccessMs
|
|
124
|
-
? new Date(uploadThrottle.lastSuccessMs).toISOString()
|
|
125
|
-
: null;
|
|
126
|
-
const autoRetryAt = parseEpochMsToIso(autoRetry?.retryAtMs);
|
|
127
|
-
|
|
128
114
|
return {
|
|
129
115
|
ok: true,
|
|
130
116
|
version: 1,
|
|
@@ -155,11 +141,9 @@ async function collectTrackerDiagnostics({
|
|
|
155
141
|
cli_approximation:
|
|
156
142
|
"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
143
|
merge_policy:
|
|
158
|
-
"Kiro IDE and Kiro CLI both emit source='kiro' in queue.jsonl so token, cost,
|
|
144
|
+
"Kiro IDE and Kiro CLI both emit source='kiro' in queue.jsonl so token, cost, and heatmap aggregations merge transparently. Use this block to distinguish sub-path contributions.",
|
|
159
145
|
},
|
|
160
146
|
config: {
|
|
161
|
-
base_url: typeof config?.baseUrl === "string" ? config.baseUrl : null,
|
|
162
|
-
device_token: config?.deviceToken ? "set" : "unset",
|
|
163
147
|
device_id: maskId(config?.deviceId),
|
|
164
148
|
installed_at: typeof config?.installedAt === "string" ? config.installedAt : null,
|
|
165
149
|
},
|
|
@@ -172,9 +156,6 @@ async function collectTrackerDiagnostics({
|
|
|
172
156
|
},
|
|
173
157
|
queue: {
|
|
174
158
|
size_bytes: queueSize,
|
|
175
|
-
offset_bytes: offsetBytes,
|
|
176
|
-
pending_bytes: pendingBytes,
|
|
177
|
-
updated_at: typeof queueState.updatedAt === "string" ? queueState.updatedAt : null,
|
|
178
159
|
},
|
|
179
160
|
notify: {
|
|
180
161
|
last_notify: lastNotify,
|
|
@@ -198,28 +179,6 @@ async function collectTrackerDiagnostics({
|
|
|
198
179
|
grok_hook_handler_exists: Boolean(grokHookState?.handlerExists),
|
|
199
180
|
grok_sessions_dir: redactValue(grokHookState?.sessionsDir, home),
|
|
200
181
|
},
|
|
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
182
|
};
|
|
224
183
|
}
|
|
225
184
|
|
|
@@ -237,13 +196,6 @@ function redactValue(value, home) {
|
|
|
237
196
|
return value.startsWith(homeNorm) ? `~${value.slice(homeNorm.length)}` : value;
|
|
238
197
|
}
|
|
239
198
|
|
|
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
199
|
async function safeStatSize(p) {
|
|
248
200
|
try {
|
|
249
201
|
const st = await fs.stat(p);
|
package/src/lib/doctor.js
CHANGED
|
@@ -29,7 +29,8 @@ async function buildDoctorReport({
|
|
|
29
29
|
checks.push(await checkCliEntrypoint(paths.cliPath));
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
// No cloud reachability check: TokenTracker is local-only, so there is no
|
|
33
|
+
// remote endpoint whose availability could affect anything here.
|
|
33
34
|
|
|
34
35
|
if (diagnostics) {
|
|
35
36
|
checks.push(...buildDiagnosticsChecks(diagnostics));
|
|
@@ -142,10 +143,6 @@ async function commandExistsOnPath(command, env = process.env) {
|
|
|
142
143
|
|
|
143
144
|
function buildRuntimeChecks(runtime = {}) {
|
|
144
145
|
const checks = [];
|
|
145
|
-
const baseUrl =
|
|
146
|
-
typeof runtime.baseUrl === "string" && runtime.baseUrl.trim() ? runtime.baseUrl.trim() : null;
|
|
147
|
-
const deviceToken =
|
|
148
|
-
typeof runtime.deviceToken === "string" && runtime.deviceToken.trim() ? "set" : "unset";
|
|
149
146
|
const dashboardUrl =
|
|
150
147
|
typeof runtime.dashboardUrl === "string" && runtime.dashboardUrl.trim()
|
|
151
148
|
? runtime.dashboardUrl.trim()
|
|
@@ -154,29 +151,9 @@ function buildRuntimeChecks(runtime = {}) {
|
|
|
154
151
|
? Number(runtime.httpTimeoutMs)
|
|
155
152
|
: null;
|
|
156
153
|
const debug = Boolean(runtime.debug);
|
|
157
|
-
const autoRetryNoSpawn = Boolean(runtime.autoRetryNoSpawn);
|
|
158
154
|
|
|
159
|
-
checks
|
|
160
|
-
|
|
161
|
-
status: baseUrl ? "ok" : "fail",
|
|
162
|
-
detail: baseUrl ? "base_url set" : "base_url missing",
|
|
163
|
-
critical: false,
|
|
164
|
-
meta: {
|
|
165
|
-
base_url: baseUrl,
|
|
166
|
-
source: runtime?.sources?.baseUrl || null,
|
|
167
|
-
},
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
checks.push({
|
|
171
|
-
id: "runtime.device_token",
|
|
172
|
-
status: deviceToken === "set" ? "ok" : "warn",
|
|
173
|
-
detail: deviceToken === "set" ? "device token set" : "device token missing",
|
|
174
|
-
critical: false,
|
|
175
|
-
meta: {
|
|
176
|
-
device_token: deviceToken,
|
|
177
|
-
source: runtime?.sources?.deviceToken || null,
|
|
178
|
-
},
|
|
179
|
-
});
|
|
155
|
+
// base URL / device token checks removed: local-only, there is no remote
|
|
156
|
+
// endpoint to point at and no credential to hold.
|
|
180
157
|
|
|
181
158
|
checks.push({
|
|
182
159
|
id: "runtime.dashboard_url",
|
|
@@ -211,16 +188,11 @@ function buildRuntimeChecks(runtime = {}) {
|
|
|
211
188
|
},
|
|
212
189
|
});
|
|
213
190
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
meta: {
|
|
220
|
-
auto_retry_no_spawn: autoRetryNoSpawn,
|
|
221
|
-
source: runtime?.sources?.autoRetryNoSpawn || null,
|
|
222
|
-
},
|
|
223
|
-
});
|
|
191
|
+
// runtime.auto_retry_no_spawn removed: scheduleAutoRetry and
|
|
192
|
+
// spawnAutoRetryProcess went with cloud upload, so this reported on a spawn
|
|
193
|
+
// path that cannot happen — and told anyone setting the env var that their
|
|
194
|
+
// opt-out had taken effect on nothing.
|
|
195
|
+
|
|
224
196
|
|
|
225
197
|
return checks;
|
|
226
198
|
}
|
|
@@ -346,48 +318,6 @@ async function checkCliEntrypoint(cliPath) {
|
|
|
346
318
|
}
|
|
347
319
|
}
|
|
348
320
|
|
|
349
|
-
async function checkNetwork({ baseUrl, fetch }) {
|
|
350
|
-
if (!baseUrl) {
|
|
351
|
-
return {
|
|
352
|
-
id: "network.base_url",
|
|
353
|
-
status: "warn",
|
|
354
|
-
detail: "base_url missing (skipped)",
|
|
355
|
-
critical: false,
|
|
356
|
-
meta: { base_url: null },
|
|
357
|
-
};
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
const start = Date.now();
|
|
361
|
-
try {
|
|
362
|
-
if (typeof fetch !== "function") throw new Error("Missing fetch");
|
|
363
|
-
const res = await fetch(baseUrl, { method: "GET" });
|
|
364
|
-
const latency = Date.now() - start;
|
|
365
|
-
return {
|
|
366
|
-
id: "network.base_url",
|
|
367
|
-
status: "ok",
|
|
368
|
-
detail: `HTTP ${res.status} (reachable)`,
|
|
369
|
-
critical: false,
|
|
370
|
-
meta: {
|
|
371
|
-
status_code: res.status,
|
|
372
|
-
latency_ms: latency,
|
|
373
|
-
base_url: baseUrl,
|
|
374
|
-
},
|
|
375
|
-
};
|
|
376
|
-
} catch (err) {
|
|
377
|
-
const latency = Date.now() - start;
|
|
378
|
-
return {
|
|
379
|
-
id: "network.base_url",
|
|
380
|
-
status: "fail",
|
|
381
|
-
detail: "Network error",
|
|
382
|
-
critical: false,
|
|
383
|
-
meta: {
|
|
384
|
-
error: err?.message || String(err),
|
|
385
|
-
latency_ms: latency,
|
|
386
|
-
base_url: baseUrl,
|
|
387
|
-
},
|
|
388
|
-
};
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
321
|
|
|
392
322
|
function buildDiagnosticsChecks(diagnostics) {
|
|
393
323
|
const checks = [];
|
|
@@ -409,14 +339,9 @@ function buildDiagnosticsChecks(diagnostics) {
|
|
|
409
339
|
meta: { configured: notifyConfigured },
|
|
410
340
|
});
|
|
411
341
|
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
status: uploadError ? "warn" : "ok",
|
|
416
|
-
detail: uploadError ? "last upload error present" : "no upload errors",
|
|
417
|
-
critical: false,
|
|
418
|
-
meta: { last_error: uploadError ? uploadError.message || null : null },
|
|
419
|
-
});
|
|
342
|
+
// The upload.last_error check is gone with cloud upload: `diagnostics.upload`
|
|
343
|
+
// no longer exists, so the check could only ever report "no upload errors" —
|
|
344
|
+
// a permanent green that says nothing.
|
|
420
345
|
|
|
421
346
|
return checks;
|
|
422
347
|
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
const crypto = require("node:crypto");
|
|
2
|
+
const fssync = require("node:fs");
|
|
3
|
+
|
|
4
|
+
// Fingerprint the first bytes of a file so rotation is detected even when the
|
|
5
|
+
// OS reuses the inode after unlink+recreate (Linux logrotate). Returns "" on
|
|
6
|
+
// any error so callers treat "no fingerprint" as "not changed". Also returns
|
|
7
|
+
// "" when the file is shorter than the fingerprint window: for a sub-256B
|
|
8
|
+
// file that is still being appended to, the window would extend into the
|
|
9
|
+
// not-yet-written append zone, so plain growth (not rotation) would shift
|
|
10
|
+
// the hash and produce a false "rotated" signal.
|
|
11
|
+
function readFileHeadSignature(filePath) {
|
|
12
|
+
try {
|
|
13
|
+
const fd = fssync.openSync(filePath, "r");
|
|
14
|
+
try {
|
|
15
|
+
const buf = Buffer.alloc(256);
|
|
16
|
+
const bytes = fssync.readSync(fd, buf, 0, 256, 0);
|
|
17
|
+
if (bytes < 256) return "";
|
|
18
|
+
return crypto.createHash("sha1").update(buf.subarray(0, bytes)).digest("hex");
|
|
19
|
+
} finally {
|
|
20
|
+
fssync.closeSync(fd);
|
|
21
|
+
}
|
|
22
|
+
} catch {
|
|
23
|
+
return "";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// A token that changes whenever a file's contents could have changed. Returns
|
|
28
|
+
// null when the path is not a readable regular file, which callers must treat
|
|
29
|
+
// as "cannot be reused" rather than "unchanged".
|
|
30
|
+
function fileIdentity(filePath) {
|
|
31
|
+
let st = null;
|
|
32
|
+
try {
|
|
33
|
+
st = fssync.statSync(filePath);
|
|
34
|
+
} catch {
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
if (!st.isFile()) return null;
|
|
38
|
+
return {
|
|
39
|
+
inode: st.ino || 0,
|
|
40
|
+
size: Number.isFinite(st.size) ? st.size : 0,
|
|
41
|
+
mtimeMs: Number.isFinite(st.mtimeMs) ? st.mtimeMs : 0,
|
|
42
|
+
head: readFileHeadSignature(filePath),
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Equality, not ordering. Deliberately not `cur.mtimeMs > prev.mtimeMs`: a
|
|
47
|
+
// clock that moves backwards, a restored mtime, or a truncation must all read
|
|
48
|
+
// as "changed" and force a re-read. The cost of a false "changed" is a wasted
|
|
49
|
+
// parse; the cost of a false "unchanged" is serving stale data forever.
|
|
50
|
+
function isUnchanged(prev, cur) {
|
|
51
|
+
if (!prev || !cur) return false;
|
|
52
|
+
// An empty head means the file was too short to fingerprint, so it carries
|
|
53
|
+
// no rotation signal — fall back to inode/size/mtime alone rather than
|
|
54
|
+
// treating "" as a mismatch.
|
|
55
|
+
const headChanged =
|
|
56
|
+
typeof prev.head === "string" && prev.head !== "" && prev.head !== cur.head;
|
|
57
|
+
return (
|
|
58
|
+
prev.inode === cur.inode &&
|
|
59
|
+
prev.size === cur.size &&
|
|
60
|
+
prev.mtimeMs === cur.mtimeMs &&
|
|
61
|
+
!headChanged
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
module.exports = {
|
|
66
|
+
readFileHeadSignature,
|
|
67
|
+
fileIdentity,
|
|
68
|
+
isUnchanged,
|
|
69
|
+
};
|
package/src/lib/init-flow.js
CHANGED
|
@@ -27,9 +27,12 @@ function renderAuthTransition({ authUrl, canAutoOpen }) {
|
|
|
27
27
|
}
|
|
28
28
|
|
|
29
29
|
function renderSuccessBox({ configPath, dashboardUrl }) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
// Local-only: there is no account and no remote dashboard. `dashboardUrl` is
|
|
31
|
+
// null on a clean install now that the cloud default is gone, so fall back to
|
|
32
|
+
// the address `serve` actually listens on — otherwise setup finishes without
|
|
33
|
+
// telling the user where to look.
|
|
34
|
+
const lines = ["You are all set!", "", `Config saved to: ${configPath}`, ""];
|
|
35
|
+
lines.push(`View your stats at: ${dashboardUrl || "http://localhost:7680"}`);
|
|
33
36
|
lines.push("You can close this terminal window.");
|
|
34
37
|
process.stdout.write(`${renderBox(lines)}\n`);
|
|
35
38
|
}
|