@mauricode/token-derby 3.1.2 → 3.1.3

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 CHANGED
@@ -97,6 +97,35 @@ at usage you didn't produce.
97
97
 
98
98
  - `~/.token-derby/stable.json` — saved horses
99
99
  - `~/.token-derby/active-races/<join-code>.json` — per-race state for rejoin
100
+ - `~/.token-derby/logs/token-derby.log` — debug log (see below)
101
+
102
+ ## Debug log
103
+
104
+ Every command appends to a rolling log, so a race that stalls overnight can be
105
+ diagnosed afterwards. The race UI takes over the terminal, which is exactly when
106
+ nothing can be printed to the screen.
107
+
108
+ ```bash
109
+ token-derby logs # print the path of the log file
110
+ token-derby logs --tail 100 # print the last 100 lines (default 50)
111
+ ```
112
+
113
+ The log rolls at 2MB and keeps five files (`token-derby.log` plus `.1`–`.4`), so
114
+ it never exceeds ~10MB. Each environment has its own, next to that environment's
115
+ identity.
116
+
117
+ What the lines mean when a race misbehaves:
118
+
119
+ - `beat.prepare.start` with no `beat.prepare.done` after it — the token scan
120
+ hung, and the poller is still waiting on it.
121
+ - repeated `beat.send.err` with a climbing `next_ms` — the heartbeat is
122
+ reaching the network and failing; `retry` counts the attempts.
123
+ - `scan.timeout` — the scan blew its budget; `reason` names the source that was
124
+ still running.
125
+
126
+ Credentials are never written: identity and horse tokens, request headers and
127
+ bodies are all omitted, and claim tokens and admin codes are masked out of the
128
+ URLs they travel in.
100
129
 
101
130
  ## Environment
102
131
 
package/dist/bin.js CHANGED
@@ -862,8 +862,8 @@ var PRIMARY_SILENT_THRESHOLD = 10;
862
862
  // src/version.ts
863
863
  import { createRequire } from "module";
864
864
  function readVersion() {
865
- if ("3.1.2".length > 0) {
866
- return "3.1.2";
865
+ if ("3.1.3".length > 0) {
866
+ return "3.1.3";
867
867
  }
868
868
  try {
869
869
  const req = createRequire(import.meta.url);
@@ -910,6 +910,12 @@ function codexSessionsDir() {
910
910
  function geminiTmpDir() {
911
911
  return process.env.TOKEN_DERBY_GEMINI_DIR ?? path2.join(os2.homedir(), ".gemini", "tmp");
912
912
  }
913
+ function logDir() {
914
+ return path2.join(homeDir(), "logs");
915
+ }
916
+ function logFile() {
917
+ return path2.join(logDir(), "token-derby.log");
918
+ }
913
919
 
914
920
  // src/identity/identity.ts
915
921
  async function readIdentityFile() {
@@ -964,6 +970,63 @@ function validateDisplayName(name) {
964
970
  return { ok: true, name: trimmed };
965
971
  }
966
972
 
973
+ // src/log/logger.ts
974
+ import * as fs2 from "fs";
975
+ var SECRET_KEY = /token|secret|authorization|credential|password/i;
976
+ function redact(fields) {
977
+ const out = {};
978
+ for (const [key, value] of Object.entries(fields)) {
979
+ out[key] = SECRET_KEY.test(key) ? "[redacted]" : value;
980
+ }
981
+ return out;
982
+ }
983
+ function formatLine(at, level, event, fields) {
984
+ const body = fields && Object.keys(fields).length > 0 ? ` ${JSON.stringify(redact(fields))}` : "";
985
+ return `${at.toISOString()} ${level.padEnd(5)} ${event}${body}
986
+ `;
987
+ }
988
+ var MAX_BYTES = 2e6;
989
+ var MAX_FILES = 5;
990
+ var currentBytes = null;
991
+ var disabled = false;
992
+ function rotate() {
993
+ const base = logFile();
994
+ fs2.rmSync(`${base}.${MAX_FILES - 1}`, { force: true });
995
+ for (let i = MAX_FILES - 2; i >= 1; i--) {
996
+ if (fs2.existsSync(`${base}.${i}`)) fs2.renameSync(`${base}.${i}`, `${base}.${i + 1}`);
997
+ }
998
+ if (fs2.existsSync(base)) fs2.renameSync(base, `${base}.1`);
999
+ currentBytes = 0;
1000
+ }
1001
+ function write(level, event, fields) {
1002
+ if (disabled) return;
1003
+ try {
1004
+ append(level, event, fields);
1005
+ } catch {
1006
+ disabled = true;
1007
+ }
1008
+ }
1009
+ function append(level, event, fields) {
1010
+ const line = formatLine(/* @__PURE__ */ new Date(), level, event, fields);
1011
+ const bytes = Buffer.byteLength(line);
1012
+ fs2.mkdirSync(logDir(), { recursive: true });
1013
+ if (currentBytes === null) {
1014
+ currentBytes = fs2.existsSync(logFile()) ? fs2.statSync(logFile()).size : 0;
1015
+ }
1016
+ if (currentBytes > 0 && currentBytes + bytes > MAX_BYTES) rotate();
1017
+ fs2.appendFileSync(logFile(), line, "utf8");
1018
+ currentBytes += bytes;
1019
+ }
1020
+ function logInfo(event, fields) {
1021
+ write("INFO", event, fields);
1022
+ }
1023
+ function logWarn(event, fields) {
1024
+ write("WARN", event, fields);
1025
+ }
1026
+ function logError(event, fields) {
1027
+ write("ERROR", event, fields);
1028
+ }
1029
+
967
1030
  // src/api/client.ts
968
1031
  var ApiError = class extends Error {
969
1032
  constructor(code, message, status) {
@@ -975,6 +1038,10 @@ var ApiError = class extends Error {
975
1038
  code;
976
1039
  status;
977
1040
  };
1041
+ var SECRET_SEGMENT = /^\/(claims|races\/admin)\/[^/]+/;
1042
+ function loggablePath(path9) {
1043
+ return path9.replace(SECRET_SEGMENT, (match) => `${match.slice(0, match.lastIndexOf("/") + 1)}[redacted]`);
1044
+ }
978
1045
  var identityCache = null;
979
1046
  function getIdentity() {
980
1047
  if (!identityCache) identityCache = loadIdentity();
@@ -995,6 +1062,9 @@ async function request(method, path9, body, horseAuthToken, fetchImpl = fetch, i
995
1062
  }
996
1063
  if (horseAuthToken) headers["authorization"] = `Bearer ${horseAuthToken}`;
997
1064
  if (body !== void 0) headers["content-type"] = "application/json";
1065
+ const safePath = loggablePath(path9);
1066
+ logInfo("http.req", { method, path: safePath });
1067
+ const startedAt2 = Date.now();
998
1068
  let res;
999
1069
  try {
1000
1070
  res = await fetchImpl(url, {
@@ -1003,8 +1073,10 @@ async function request(method, path9, body, horseAuthToken, fetchImpl = fetch, i
1003
1073
  body: body !== void 0 ? JSON.stringify(body) : void 0
1004
1074
  });
1005
1075
  } catch (e) {
1076
+ logError("http.err", { method, path: safePath, ms: Date.now() - startedAt2, message: e?.message ?? "fetch failed" });
1006
1077
  throw new ApiError("NETWORK_ERROR", e?.message ?? "fetch failed", 0);
1007
1078
  }
1079
+ logInfo("http.res", { method, path: safePath, status: res.status, ms: Date.now() - startedAt2 });
1008
1080
  const text = await res.text();
1009
1081
  const contentType = res.headers.get("content-type") ?? "";
1010
1082
  let parsed = null;
@@ -1021,6 +1093,14 @@ async function request(method, path9, body, horseAuthToken, fetchImpl = fetch, i
1021
1093
  }
1022
1094
  throw new ApiError("NETWORK_ERROR", `HTTP ${res.status}`, res.status);
1023
1095
  }
1096
+ if (parsed === null) {
1097
+ const got = contentType || "no content-type";
1098
+ throw new ApiError(
1099
+ "NETWORK_ERROR",
1100
+ `Expected JSON from ${method} ${safePath} but the server returned ${got} (HTTP ${res.status}). The API may be unavailable or the request was rejected upstream.`,
1101
+ res.status
1102
+ );
1103
+ }
1024
1104
  return parsed;
1025
1105
  }
1026
1106
 
@@ -1532,11 +1612,11 @@ function PrimaryPicker({ onPick }) {
1532
1612
  }
1533
1613
 
1534
1614
  // src/stable/active-race.ts
1535
- import * as fs2 from "fs/promises";
1615
+ import * as fs3 from "fs/promises";
1536
1616
  import * as path4 from "path";
1537
1617
  async function saveActiveRace(active) {
1538
- await fs2.mkdir(activeRacesDir(), { recursive: true });
1539
- await fs2.writeFile(
1618
+ await fs3.mkdir(activeRacesDir(), { recursive: true });
1619
+ await fs3.writeFile(
1540
1620
  activeRaceFile(active.join_code),
1541
1621
  JSON.stringify(active, null, 2) + "\n",
1542
1622
  "utf8"
@@ -1709,6 +1789,7 @@ function runHeartbeatLoop(opts) {
1709
1789
  let stopped = false;
1710
1790
  let pending = null;
1711
1791
  const stop = () => {
1792
+ if (!stopped) logInfo("beat.stop", { retry: retryIndex });
1712
1793
  stopped = true;
1713
1794
  if (timer) clearTimeout(timer);
1714
1795
  timer = null;
@@ -1721,9 +1802,16 @@ function runHeartbeatLoop(opts) {
1721
1802
  const tick = async () => {
1722
1803
  if (stopped) return;
1723
1804
  try {
1724
- if (!pending) pending = await opts.prepareBeat();
1805
+ if (!pending) {
1806
+ logInfo("beat.prepare.start");
1807
+ const startedAt2 = Date.now();
1808
+ pending = await opts.prepareBeat();
1809
+ logInfo("beat.prepare.done", { seq: pending.seq, ms: Date.now() - startedAt2 });
1810
+ }
1725
1811
  const snapshot = pending;
1812
+ const sentAt = Date.now();
1726
1813
  const resp = await opts.sendBeat(snapshot);
1814
+ logInfo("beat.send.ok", { seq: snapshot.seq, ms: Date.now() - sentAt, race_status: resp.race_status });
1727
1815
  pending = null;
1728
1816
  retryIndex = 0;
1729
1817
  opts.onSuccess(resp, snapshot);
@@ -1736,6 +1824,13 @@ function runHeartbeatLoop(opts) {
1736
1824
  } catch (err) {
1737
1825
  opts.onError(err);
1738
1826
  const delay = opts.retryDelaysMs[Math.min(retryIndex, opts.retryDelaysMs.length - 1)] ?? 1e3;
1827
+ logError("beat.send.err", {
1828
+ seq: pending?.seq,
1829
+ code: err?.code,
1830
+ message: err?.message ?? String(err),
1831
+ retry: retryIndex,
1832
+ next_ms: delay
1833
+ });
1739
1834
  retryIndex += 1;
1740
1835
  schedule(delay);
1741
1836
  }
@@ -1744,7 +1839,7 @@ function runHeartbeatLoop(opts) {
1744
1839
  }
1745
1840
 
1746
1841
  // src/tokens/transcripts.ts
1747
- import * as fs4 from "fs/promises";
1842
+ import * as fs5 from "fs/promises";
1748
1843
  import * as path6 from "path";
1749
1844
 
1750
1845
  // src/tokens/pool.ts
@@ -1764,7 +1859,7 @@ async function mapWithConcurrency(items, limit, fn) {
1764
1859
  }
1765
1860
 
1766
1861
  // src/tokens/scan-cache.ts
1767
- import * as fs3 from "fs/promises";
1862
+ import * as fs4 from "fs/promises";
1768
1863
  import * as path5 from "path";
1769
1864
  var CACHE_VERSION = 1;
1770
1865
  function isEntry(v) {
@@ -1802,7 +1897,7 @@ var ScanCache = class _ScanCache {
1802
1897
  * re-reading that line once the writer completes it.
1803
1898
  */
1804
1899
  async readIncremental(file, fold) {
1805
- const st = await fs3.stat(file);
1900
+ const st = await fs4.stat(file);
1806
1901
  const prev = this.entries.get(file);
1807
1902
  this.touched.add(file);
1808
1903
  if (prev && prev.mtimeMs === st.mtimeMs && prev.size === st.size) return prev.value;
@@ -1819,11 +1914,11 @@ var ScanCache = class _ScanCache {
1819
1914
  * chats). Gated on mtime+size, recomputed in full whenever either moves.
1820
1915
  */
1821
1916
  async readWhenChanged(file, compute) {
1822
- const st = await fs3.stat(file);
1917
+ const st = await fs4.stat(file);
1823
1918
  const prev = this.entries.get(file);
1824
1919
  this.touched.add(file);
1825
1920
  if (prev && prev.mtimeMs === st.mtimeMs && prev.size === st.size) return prev.value;
1826
- const value = await compute(await fs3.readFile(file, "utf8"));
1921
+ const value = await compute(await fs4.readFile(file, "utf8"));
1827
1922
  this.entries.set(file, { mtimeMs: st.mtimeMs, size: st.size, offset: st.size, value });
1828
1923
  return value;
1829
1924
  }
@@ -1835,9 +1930,9 @@ var ScanCache = class _ScanCache {
1835
1930
  const target = cacheFile(this.source);
1836
1931
  const tmp = `${target}.tmp`;
1837
1932
  try {
1838
- await fs3.mkdir(path5.dirname(target), { recursive: true });
1839
- await fs3.writeFile(tmp, JSON.stringify({ version: CACHE_VERSION, files: Object.fromEntries(this.entries) }));
1840
- await fs3.rename(tmp, target);
1933
+ await fs4.mkdir(path5.dirname(target), { recursive: true });
1934
+ await fs4.writeFile(tmp, JSON.stringify({ version: CACHE_VERSION, files: Object.fromEntries(this.entries) }));
1935
+ await fs4.rename(tmp, target);
1841
1936
  } catch {
1842
1937
  }
1843
1938
  }
@@ -1848,7 +1943,7 @@ function cacheFile(source) {
1848
1943
  async function loadEntries(source) {
1849
1944
  let parsed;
1850
1945
  try {
1851
- parsed = JSON.parse(await fs3.readFile(cacheFile(source), "utf8"));
1946
+ parsed = JSON.parse(await fs4.readFile(cacheFile(source), "utf8"));
1852
1947
  } catch {
1853
1948
  return /* @__PURE__ */ new Map();
1854
1949
  }
@@ -1863,7 +1958,7 @@ async function loadEntries(source) {
1863
1958
  }
1864
1959
  async function readCompleteLines(file, start, end) {
1865
1960
  if (end <= start) return { lines: [], tail: null, consumedTo: start };
1866
- const fh = await fs3.open(file, "r");
1961
+ const fh = await fs4.open(file, "r");
1867
1962
  try {
1868
1963
  const buf = Buffer.allocUnsafe(end - start);
1869
1964
  const { bytesRead } = await fh.read(buf, 0, end - start, start);
@@ -1956,13 +2051,13 @@ async function collectJsonl(dir, depth, out) {
1956
2051
  }
1957
2052
  }
1958
2053
  async function readEntries(dir, failLoud) {
1959
- if (failLoud) return readRoot(dir, () => fs4.readdir(dir, { withFileTypes: true }));
1960
- return fs4.readdir(dir, { withFileTypes: true }).catch(() => []);
2054
+ if (failLoud) return readRoot(dir, () => fs5.readdir(dir, { withFileTypes: true }));
2055
+ return fs5.readdir(dir, { withFileTypes: true }).catch(() => []);
1961
2056
  }
1962
2057
  async function isDirectory(entry, parent) {
1963
2058
  if (entry.isDirectory()) return true;
1964
2059
  if (!entry.isSymbolicLink()) return false;
1965
- return fs4.stat(path6.join(parent, entry.name)).then((st) => st.isDirectory()).catch(() => false);
2060
+ return fs5.stat(path6.join(parent, entry.name)).then((st) => st.isDirectory()).catch(() => false);
1966
2061
  }
1967
2062
  function addNum(value) {
1968
2063
  return typeof value === "number" && Number.isFinite(value) ? value : 0;
@@ -1989,14 +2084,14 @@ var CLAUDE_FOLD = {
1989
2084
  };
1990
2085
 
1991
2086
  // src/tokens/codex.ts
1992
- import * as fs5 from "fs/promises";
2087
+ import * as fs6 from "fs/promises";
1993
2088
  import * as path7 from "path";
1994
2089
  function num(v) {
1995
2090
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
1996
2091
  }
1997
2092
  async function sumCodexByConversation() {
1998
2093
  const root = codexSessionsDir();
1999
- await readRoot(root, () => fs5.stat(root));
2094
+ await readRoot(root, () => fs6.stat(root));
2000
2095
  const files = await listCodexRollouts(root);
2001
2096
  const cache = await ScanCache.open("codex");
2002
2097
  const totals = await mapWithConcurrency(
@@ -2028,7 +2123,7 @@ async function listCodexRollouts(root) {
2028
2123
  async function collectRollouts(dir) {
2029
2124
  let entries;
2030
2125
  try {
2031
- entries = await fs5.readdir(dir, { withFileTypes: true });
2126
+ entries = await fs6.readdir(dir, { withFileTypes: true });
2032
2127
  } catch (e) {
2033
2128
  if (e?.code === "ENOENT") return [];
2034
2129
  throw e;
@@ -2066,7 +2161,7 @@ var CODEX_FOLD = {
2066
2161
  };
2067
2162
 
2068
2163
  // src/tokens/gemini.ts
2069
- import * as fs6 from "fs/promises";
2164
+ import * as fs7 from "fs/promises";
2070
2165
  import * as path8 from "path";
2071
2166
  function num2(v) {
2072
2167
  return typeof v === "number" && Number.isFinite(v) ? v : 0;
@@ -2095,13 +2190,13 @@ async function sumGeminiTokens() {
2095
2190
  return { input, output };
2096
2191
  }
2097
2192
  async function listChatFiles(root) {
2098
- const entries = await readRoot(root, () => fs6.readdir(root));
2193
+ const entries = await readRoot(root, () => fs7.readdir(root));
2099
2194
  const out = [];
2100
2195
  for (const entry of entries) {
2101
2196
  const chatsDir = path8.join(root, entry, "chats");
2102
2197
  let files;
2103
2198
  try {
2104
- files = await fs6.readdir(chatsDir);
2199
+ files = await fs7.readdir(chatsDir);
2105
2200
  } catch {
2106
2201
  continue;
2107
2202
  }
@@ -2153,11 +2248,24 @@ async function scanWithTimeout(scan, timeoutMs, describeTimeout) {
2153
2248
  const budget = new Promise((resolve) => {
2154
2249
  timer = setTimeout(() => resolve(TIMED_OUT), timeoutMs);
2155
2250
  });
2251
+ const startedAt2 = Date.now();
2156
2252
  try {
2157
2253
  const result = await Promise.race([scan(), budget]);
2158
- if (result !== TIMED_OUT) return result;
2254
+ if (result !== TIMED_OUT) {
2255
+ if (isStall(result)) logWarn("scan.stall", { reason: result.stall, ms: Date.now() - startedAt2 });
2256
+ return result;
2257
+ }
2159
2258
  const detail = describeTimeout ? await describeTimeout() : null;
2160
- return { stall: detail ?? `Token scan timed out after ${Math.round(timeoutMs / 1e3)}s` };
2259
+ const stall = detail ?? `Token scan timed out after ${Math.round(timeoutMs / 1e3)}s`;
2260
+ logWarn("scan.timeout", { budget_ms: timeoutMs, reason: stall });
2261
+ return { stall };
2262
+ } catch (err) {
2263
+ logError("scan.error", {
2264
+ message: err?.message ?? String(err),
2265
+ stack: err?.stack,
2266
+ ms: Date.now() - startedAt2
2267
+ });
2268
+ throw err;
2161
2269
  } finally {
2162
2270
  clearTimeout(timer);
2163
2271
  }
@@ -2184,7 +2292,12 @@ async function readAllSources(race, primary, progress) {
2184
2292
  const secondaryKeys = MODEL_KEYS.filter((k) => k !== primary);
2185
2293
  const secondaryScans = secondaryKeys.map((k) => {
2186
2294
  progress?.begin(k);
2187
- return SCALAR_READERS[k]().then((t) => scoreFor(race, t)).catch(() => 0).finally(() => progress?.end(k));
2295
+ return SCALAR_READERS[k]().then((t) => scoreFor(race, t)).catch((err) => {
2296
+ if (!(err instanceof SourceRootMissing)) {
2297
+ logWarn("scan.source.err", { source: k, message: err?.message ?? String(err) });
2298
+ }
2299
+ return 0;
2300
+ }).finally(() => progress?.end(k));
2188
2301
  });
2189
2302
  const [primaryResult, secondaryValues] = await Promise.all([
2190
2303
  primaryScan,
@@ -2378,7 +2491,7 @@ var RaceScoreTracker = class {
2378
2491
  };
2379
2492
 
2380
2493
  // src/tokens/source-probe.ts
2381
- import * as fs7 from "fs/promises";
2494
+ import * as fs8 from "fs/promises";
2382
2495
  var ROOTS = {
2383
2496
  claude: claudeProjectsDir,
2384
2497
  codex: codexSessionsDir,
@@ -2395,9 +2508,9 @@ function sourceDir(key) {
2395
2508
  }
2396
2509
  async function probeSource(key) {
2397
2510
  const dir = ROOTS[key]();
2398
- const exists = await fs7.stat(dir).then((st) => st.isDirectory()).catch(() => false);
2511
+ const exists = await fs8.stat(dir).then((st) => st.isDirectory()).catch(() => false);
2399
2512
  if (!exists) return { key, dir, exists: false, projects: 0, transcripts: 0 };
2400
- const entries = await fs7.readdir(dir, { withFileTypes: true }).catch(() => []);
2513
+ const entries = await fs8.readdir(dir, { withFileTypes: true }).catch(() => []);
2401
2514
  const projects = entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).length;
2402
2515
  const files = await LISTERS[key](dir).catch(() => []);
2403
2516
  return { key, dir, exists: true, projects, transcripts: files.length };
@@ -4077,6 +4190,31 @@ function envCommand(arg) {
4077
4190
  return 0;
4078
4191
  }
4079
4192
 
4193
+ // src/commands/logs.ts
4194
+ import * as fs9 from "fs/promises";
4195
+ import { existsSync as existsSync2 } from "fs";
4196
+ var DEFAULT_TAIL_LINES = 50;
4197
+ function tailCount(argv) {
4198
+ const i = argv.indexOf("--tail");
4199
+ if (i === -1) return null;
4200
+ const n = Number(argv[i + 1]);
4201
+ return Number.isInteger(n) && n > 0 ? n : DEFAULT_TAIL_LINES;
4202
+ }
4203
+ async function logsCommand(argv) {
4204
+ const file = logFile();
4205
+ if (!existsSync2(file)) {
4206
+ console.log(`No log file yet \u2014 it appears at ${file} the first time a command runs.`);
4207
+ return 0;
4208
+ }
4209
+ console.log(file);
4210
+ const n = tailCount(argv);
4211
+ if (n === null) return 0;
4212
+ const lines = (await fs9.readFile(file, "utf8")).split("\n").filter((l) => l.length > 0);
4213
+ console.log("");
4214
+ for (const line of lines.slice(-n)) console.log(line);
4215
+ return 0;
4216
+ }
4217
+
4080
4218
  // src/bin.ts
4081
4219
  var HELP = `token-derby v${CLI_VERSION}
4082
4220
 
@@ -4105,6 +4243,8 @@ Identity:
4105
4243
 
4106
4244
  Maintenance:
4107
4245
  token-derby update Check for and install the latest CLI version
4246
+ token-derby logs Show the path of the debug log
4247
+ token-derby logs --tail [n] Print the last n log lines (default 50)
4108
4248
 
4109
4249
  Stable management:
4110
4250
  token-derby stable create Make a new horse (interactive)
@@ -4141,9 +4281,25 @@ Environment:
4141
4281
  TOKEN_DERBY_API_BASE Hard-override API base URL (wins over env)
4142
4282
  TOKEN_DERBY_HOME Hard-override identity/stable directory
4143
4283
  `;
4284
+ function describeInvocation(argv) {
4285
+ const cmd = argv[0] ?? "(none)";
4286
+ const container = cmd === "stable" || cmd === "organisation" || cmd === "org";
4287
+ return {
4288
+ cmd,
4289
+ sub: container ? argv[1] : void 0,
4290
+ flags: argv.filter((a) => a.startsWith("--")).map((a) => a.split("=")[0])
4291
+ };
4292
+ }
4144
4293
  async function main() {
4145
4294
  const argv = process.argv.slice(2);
4146
4295
  const cmd = argv[0];
4296
+ logInfo("cmd.start", {
4297
+ ...describeInvocation(argv),
4298
+ version: CLI_VERSION,
4299
+ node: process.version,
4300
+ pid: process.pid,
4301
+ env: selectedEnv()
4302
+ });
4147
4303
  if (!cmd || cmd === "--help" || cmd === "-h") {
4148
4304
  console.log(HELP);
4149
4305
  return 0;
@@ -4159,6 +4315,7 @@ async function main() {
4159
4315
  if (cmd === "login") return loginCommand(argv.slice(1));
4160
4316
  if (cmd === "update") return updateCommand();
4161
4317
  if (cmd === "env") return envCommand(argv[1]);
4318
+ if (cmd === "logs") return logsCommand(argv.slice(1));
4162
4319
  const identity = await loadIdentity();
4163
4320
  if (!identity) {
4164
4321
  console.error("Run `token-derby login` to set up your identity before using any other command.");
@@ -4205,9 +4362,33 @@ function parseFlag(args, flag) {
4205
4362
  }
4206
4363
  return void 0;
4207
4364
  }
4365
+ var CRASH_HANDLERS_INSTALLED = /* @__PURE__ */ Symbol.for("token-derby.crash-handlers");
4366
+ if (!(CRASH_HANDLERS_INSTALLED in process)) {
4367
+ process[CRASH_HANDLERS_INSTALLED] = true;
4368
+ process.on("uncaughtException", (err) => {
4369
+ logError("cmd.uncaught", { message: err?.message ?? String(err), stack: err?.stack });
4370
+ console.error(err?.stack ?? err);
4371
+ process.exit(1);
4372
+ });
4373
+ process.on("unhandledRejection", (reason) => {
4374
+ logError("cmd.unhandled", {
4375
+ message: reason?.message ?? String(reason),
4376
+ stack: reason?.stack
4377
+ });
4378
+ });
4379
+ }
4380
+ var startedAt = Date.now();
4208
4381
  main().then(
4209
- (code) => process.exit(code),
4382
+ (code) => {
4383
+ logInfo("cmd.exit", { code, ms: Date.now() - startedAt });
4384
+ process.exit(code);
4385
+ },
4210
4386
  (err) => {
4387
+ logError("cmd.crash", {
4388
+ message: err?.message ?? String(err),
4389
+ stack: err?.stack,
4390
+ ms: Date.now() - startedAt
4391
+ });
4211
4392
  console.error(err?.stack ?? err);
4212
4393
  process.exit(1);
4213
4394
  }