@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.
Files changed (192) hide show
  1. package/LICENSE +21 -0
  2. package/README.ja.md +464 -0
  3. package/README.ko.md +464 -0
  4. package/README.md +476 -0
  5. package/README.zh-CN.md +469 -0
  6. package/bin/tracker.js +52 -0
  7. package/dashboard/dist/app-icon.png +0 -0
  8. package/dashboard/dist/apple-touch-icon.png +0 -0
  9. package/dashboard/dist/assets/ActivityHeatmap-BxBGHY1z.js +42 -0
  10. package/dashboard/dist/assets/Card-GROxUc_6.js +1 -0
  11. package/dashboard/dist/assets/DashboardPage-BZ5VFoBk.js +19 -0
  12. package/dashboard/dist/assets/DevicePage-CQts7ivB.js +1 -0
  13. package/dashboard/dist/assets/DialogTitle-DqFsk26U.js +1 -0
  14. package/dashboard/dist/assets/FadeIn-HBBXW_3q.js +1 -0
  15. package/dashboard/dist/assets/HeaderGithubStar-COD2HfaF.js +1 -0
  16. package/dashboard/dist/assets/IpCheckPage-Cn0l8-BQ.js +15 -0
  17. package/dashboard/dist/assets/LandingPage-CmV1BMU4.js +4356 -0
  18. package/dashboard/dist/assets/LeaderboardAvatar-CiIpWZwb.js +1 -0
  19. package/dashboard/dist/assets/LeaderboardPage-BU0mlR3g.js +6 -0
  20. package/dashboard/dist/assets/LeaderboardProfileModal-C49_3Xm9.js +72 -0
  21. package/dashboard/dist/assets/LeaderboardProfilePage-Bh1n0BAM.js +1 -0
  22. package/dashboard/dist/assets/LimitsPage-fZAmwLWT.js +2 -0
  23. package/dashboard/dist/assets/LocalOnlyNotice-D3rQdA-P.js +1 -0
  24. package/dashboard/dist/assets/LoginPage-enO4XP-c.js +1 -0
  25. package/dashboard/dist/assets/PopoverPopup-pitJEpN1.js +1 -0
  26. package/dashboard/dist/assets/Select-tT9x4ntD.js +1 -0
  27. package/dashboard/dist/assets/SelectItemText-C7F7eIm0.js +1 -0
  28. package/dashboard/dist/assets/SettingsPage-pDasZNU2.js +1 -0
  29. package/dashboard/dist/assets/SkillsPage-DEu7FY-2.js +1 -0
  30. package/dashboard/dist/assets/WidgetsPage-DGcO7rbp.js +1 -0
  31. package/dashboard/dist/assets/WrappedPage-D_1ID213.js +1 -0
  32. package/dashboard/dist/assets/agent-logos-DXQVg1xy.js +1 -0
  33. package/dashboard/dist/assets/arrow-up-right-DNLo92Ba.js +1 -0
  34. package/dashboard/dist/assets/download-DbBBBpJF.js +1 -0
  35. package/dashboard/dist/assets/geist-mono-cyrillic-400-normal-BPBWmzPh.woff +0 -0
  36. package/dashboard/dist/assets/geist-mono-cyrillic-400-normal-Ce5q_31Z.woff2 +0 -0
  37. package/dashboard/dist/assets/geist-mono-cyrillic-500-normal-CJBLNVQT.woff2 +0 -0
  38. package/dashboard/dist/assets/geist-mono-cyrillic-500-normal-mNhfPmgl.woff +0 -0
  39. package/dashboard/dist/assets/geist-mono-cyrillic-700-normal-DH5Q319x.woff +0 -0
  40. package/dashboard/dist/assets/geist-mono-cyrillic-700-normal-VCNRadI3.woff2 +0 -0
  41. package/dashboard/dist/assets/geist-mono-cyrillic-900-normal-BKC3PZkM.woff +0 -0
  42. package/dashboard/dist/assets/geist-mono-cyrillic-900-normal-DdrQ8KBw.woff2 +0 -0
  43. package/dashboard/dist/assets/geist-mono-latin-400-normal-CoULgQGM.woff +0 -0
  44. package/dashboard/dist/assets/geist-mono-latin-400-normal-LC9RFr9I.woff2 +0 -0
  45. package/dashboard/dist/assets/geist-mono-latin-500-normal-D3o2eNa9.woff2 +0 -0
  46. package/dashboard/dist/assets/geist-mono-latin-500-normal-DOxI7kZ4.woff +0 -0
  47. package/dashboard/dist/assets/geist-mono-latin-700-normal-D6izGJRP.woff2 +0 -0
  48. package/dashboard/dist/assets/geist-mono-latin-700-normal-QGw08Lff.woff +0 -0
  49. package/dashboard/dist/assets/geist-mono-latin-900-normal-CmoKXrdK.woff +0 -0
  50. package/dashboard/dist/assets/geist-mono-latin-900-normal-Cu5MFKsu.woff2 +0 -0
  51. package/dashboard/dist/assets/geist-mono-latin-ext-400-normal-Cgks_Qgx.woff2 +0 -0
  52. package/dashboard/dist/assets/geist-mono-latin-ext-400-normal-CxNRRMGd.woff +0 -0
  53. package/dashboard/dist/assets/geist-mono-latin-ext-500-normal-CQcGuCNt.woff2 +0 -0
  54. package/dashboard/dist/assets/geist-mono-latin-ext-500-normal-diTenJ8L.woff +0 -0
  55. package/dashboard/dist/assets/geist-mono-latin-ext-700-normal-BX9f1BHp.woff +0 -0
  56. package/dashboard/dist/assets/geist-mono-latin-ext-700-normal-YOllDaLV.woff2 +0 -0
  57. package/dashboard/dist/assets/geist-mono-latin-ext-900-normal-Br6WNUGb.woff +0 -0
  58. package/dashboard/dist/assets/geist-mono-latin-ext-900-normal-CrmBTzU2.woff2 +0 -0
  59. package/dashboard/dist/assets/index-BeoRn2gJ.js +2 -0
  60. package/dashboard/dist/assets/info-Bh5qTz6k.js +1 -0
  61. package/dashboard/dist/assets/main-BIY3Nj6I.js +969 -0
  62. package/dashboard/dist/assets/main-G_F9Iu_x.css +1 -0
  63. package/dashboard/dist/assets/use-limits-display-prefs-B4iJ-bBc.js +1 -0
  64. package/dashboard/dist/assets/use-native-settings-DHjPFDv8.js +1 -0
  65. package/dashboard/dist/assets/use-usage-limits-odqe9OY7.js +1 -0
  66. package/dashboard/dist/assets/useCurrency-BsSZiZMP.js +1 -0
  67. package/dashboard/dist/assets/useOpenInteractionType-BQhgq4rv.js +12 -0
  68. package/dashboard/dist/brand-logos/antigravity.svg +1 -0
  69. package/dashboard/dist/brand-logos/claude-code.svg +1 -0
  70. package/dashboard/dist/brand-logos/codebuddy.svg +1 -0
  71. package/dashboard/dist/brand-logos/codex.svg +15 -0
  72. package/dashboard/dist/brand-logos/copilot.svg +1 -0
  73. package/dashboard/dist/brand-logos/cursor.svg +1 -0
  74. package/dashboard/dist/brand-logos/gemini.svg +1 -0
  75. package/dashboard/dist/brand-logos/grok.svg +1 -0
  76. package/dashboard/dist/brand-logos/hermes.svg +1 -0
  77. package/dashboard/dist/brand-logos/kilo.svg +4 -0
  78. package/dashboard/dist/brand-logos/kimi.svg +1 -0
  79. package/dashboard/dist/brand-logos/kiro.svg +1 -0
  80. package/dashboard/dist/brand-logos/openclaw.svg +22 -0
  81. package/dashboard/dist/brand-logos/opencode.svg +3 -0
  82. package/dashboard/dist/clawd/happy.svg +161 -0
  83. package/dashboard/dist/clawd/idle/collapse.svg +101 -0
  84. package/dashboard/dist/clawd/idle/doze.svg +72 -0
  85. package/dashboard/dist/clawd/idle/follow.svg +64 -0
  86. package/dashboard/dist/clawd/idle/living.svg +196 -0
  87. package/dashboard/dist/clawd/idle/look.svg +115 -0
  88. package/dashboard/dist/clawd/idle/yawn.svg +158 -0
  89. package/dashboard/dist/clawd/mini/alert.svg +129 -0
  90. package/dashboard/dist/clawd/mini/crabwalk.svg +76 -0
  91. package/dashboard/dist/clawd/mini/enter-sleep.svg +65 -0
  92. package/dashboard/dist/clawd/mini/enter.svg +115 -0
  93. package/dashboard/dist/clawd/mini/happy.svg +124 -0
  94. package/dashboard/dist/clawd/mini/idle-tight.svg +15 -0
  95. package/dashboard/dist/clawd/mini/idle.svg +83 -0
  96. package/dashboard/dist/clawd/mini/peek.svg +82 -0
  97. package/dashboard/dist/clawd/mini/sleep.svg +112 -0
  98. package/dashboard/dist/clawd/react/double.svg +108 -0
  99. package/dashboard/dist/clawd/react/drag.svg +102 -0
  100. package/dashboard/dist/clawd/react/left.svg +102 -0
  101. package/dashboard/dist/clawd/react/right.svg +102 -0
  102. package/dashboard/dist/clawd/sleep/collapse-sleep.svg +247 -0
  103. package/dashboard/dist/clawd/sleep/sleeping.svg +118 -0
  104. package/dashboard/dist/clawd/sleep/wake.svg +277 -0
  105. package/dashboard/dist/clawd/static-base.svg +21 -0
  106. package/dashboard/dist/clawd/status/disconnected.svg +136 -0
  107. package/dashboard/dist/clawd/status/error.svg +94 -0
  108. package/dashboard/dist/clawd/status/notification.svg +134 -0
  109. package/dashboard/dist/clawd/working/building.svg +231 -0
  110. package/dashboard/dist/clawd/working/carrying.svg +107 -0
  111. package/dashboard/dist/clawd/working/conducting.svg +118 -0
  112. package/dashboard/dist/clawd/working/confused.svg +113 -0
  113. package/dashboard/dist/clawd/working/debugger.svg +91 -0
  114. package/dashboard/dist/clawd/working/juggling.svg +82 -0
  115. package/dashboard/dist/clawd/working/overheated.svg +75 -0
  116. package/dashboard/dist/clawd/working/pushing.svg +125 -0
  117. package/dashboard/dist/clawd/working/sweeping.svg +79 -0
  118. package/dashboard/dist/clawd/working/thinking.svg +119 -0
  119. package/dashboard/dist/clawd/working/typing.svg +153 -0
  120. package/dashboard/dist/clawd/working/ultrathink.svg +166 -0
  121. package/dashboard/dist/clawd/working/wizard.svg +98 -0
  122. package/dashboard/dist/dashboard-dark.png +0 -0
  123. package/dashboard/dist/favicon-16.png +0 -0
  124. package/dashboard/dist/favicon-32.png +0 -0
  125. package/dashboard/dist/favicon.ico +0 -0
  126. package/dashboard/dist/feed.xml +34 -0
  127. package/dashboard/dist/icon-192.png +0 -0
  128. package/dashboard/dist/icon-512.png +0 -0
  129. package/dashboard/dist/index.html +355 -0
  130. package/dashboard/dist/llms.txt +53 -0
  131. package/dashboard/dist/neon_bg.png +0 -0
  132. package/dashboard/dist/robots.txt +7 -0
  133. package/dashboard/dist/share.html +140 -0
  134. package/dashboard/dist/sitemap.xml +27 -0
  135. package/dashboard/dist/widgets-overview.png +0 -0
  136. package/package.json +78 -0
  137. package/scripts/install-local-service.sh +140 -0
  138. package/scripts/uninstall-local-service.sh +21 -0
  139. package/src/cli.js +95 -0
  140. package/src/commands/device-login.js +161 -0
  141. package/src/commands/diagnostics.js +38 -0
  142. package/src/commands/doctor.js +96 -0
  143. package/src/commands/init.js +1195 -0
  144. package/src/commands/serve.js +351 -0
  145. package/src/commands/status.js +609 -0
  146. package/src/commands/sync.js +2285 -0
  147. package/src/commands/uninstall.js +189 -0
  148. package/src/commands/wrapped.js +150 -0
  149. package/src/lib/antigravity-paths.js +48 -0
  150. package/src/lib/browser-auth.js +281 -0
  151. package/src/lib/categorizer-utils.js +232 -0
  152. package/src/lib/claude-categorizer.js +1319 -0
  153. package/src/lib/claude-config.js +204 -0
  154. package/src/lib/cli-ui.js +179 -0
  155. package/src/lib/codex-config.js +350 -0
  156. package/src/lib/codex-context-breakdown.js +400 -0
  157. package/src/lib/codex-rollout-parser.js +623 -0
  158. package/src/lib/codex-token-refresh.js +112 -0
  159. package/src/lib/cursor-config.js +425 -0
  160. package/src/lib/debug-flags.js +7 -0
  161. package/src/lib/diagnostics.js +272 -0
  162. package/src/lib/doctor.js +338 -0
  163. package/src/lib/fs.js +91 -0
  164. package/src/lib/gemini-config.js +288 -0
  165. package/src/lib/grok-hook.js +351 -0
  166. package/src/lib/init-flow.js +42 -0
  167. package/src/lib/local-api.js +1861 -0
  168. package/src/lib/openclaw-hook.js +420 -0
  169. package/src/lib/openclaw-session-plugin.js +520 -0
  170. package/src/lib/opencode-config.js +99 -0
  171. package/src/lib/passive-mode.js +185 -0
  172. package/src/lib/pricing/curated-overrides.json +79 -0
  173. package/src/lib/pricing/index.js +143 -0
  174. package/src/lib/pricing/litellm-fetcher.js +172 -0
  175. package/src/lib/pricing/matcher.js +255 -0
  176. package/src/lib/pricing/seed-snapshot.json +1 -0
  177. package/src/lib/progress.js +76 -0
  178. package/src/lib/project-usage-purge.js +106 -0
  179. package/src/lib/prompt.js +24 -0
  180. package/src/lib/proxy-env.js +131 -0
  181. package/src/lib/rollout.js +8442 -0
  182. package/src/lib/runtime-config.js +122 -0
  183. package/src/lib/skill-usage.js +267 -0
  184. package/src/lib/skills-manager.js +1127 -0
  185. package/src/lib/source-metadata.js +48 -0
  186. package/src/lib/sqlite-reader.js +136 -0
  187. package/src/lib/static-server.js +59 -0
  188. package/src/lib/subscriptions.js +453 -0
  189. package/src/lib/tracker-paths.js +16 -0
  190. package/src/lib/upload-throttle.js +148 -0
  191. package/src/lib/usage-limits.js +1806 -0
  192. package/src/lib/wrapped-aggregator.js +225 -0
@@ -0,0 +1,351 @@
1
+ const http = require("node:http");
2
+ const os = require("node:os");
3
+ const path = require("node:path");
4
+ const fssync = require("node:fs");
5
+ const cp = require("node:child_process");
6
+
7
+ const { resolveTrackerPaths } = require("../lib/tracker-paths");
8
+ const { createLocalApiHandler, resolveQueuePath } = require("../lib/local-api");
9
+ const { ensurePricingLoaded } = require("../lib/pricing");
10
+ const { serveStaticFile } = require("../lib/static-server");
11
+ const { openInBrowser } = require("../lib/browser-auth");
12
+
13
+ const DEFAULT_PORT = 7680;
14
+ const DEFAULT_MAX_PORT_ATTEMPTS = 20;
15
+ const NPM_PACKAGE_NAME = "@ipv9/tokentracker-cli";
16
+ const LOCAL_BIND_HOST = "127.0.0.1";
17
+
18
+ function buildPortInUseHint(port) {
19
+ return `Port ${port} is still in use after cleanup. Try: npx ${NPM_PACKAGE_NAME} serve --port ${port + 1}\n`;
20
+ }
21
+
22
+ function isPortUnavailableError(error) {
23
+ return error?.code === "EADDRINUSE" || error?.code === "EACCES" || error?.code === "EPERM";
24
+ }
25
+
26
+ function getLocalServerUrl(port) {
27
+ return `http://${LOCAL_BIND_HOST}:${port}`;
28
+ }
29
+
30
+ async function cmdServe(argv) {
31
+ const opts = parseArgs(argv);
32
+
33
+ // 0. First-time setup: if tracker dir doesn't exist, run init first
34
+ const { trackerDir } = await resolveTrackerPaths();
35
+ if (!fssync.existsSync(path.join(trackerDir, "cursors.json"))) {
36
+ process.stdout.write("First time? Setting up Token Tracker...\n\n");
37
+ try {
38
+ const { cmdInit } = require("./init");
39
+ await cmdInit(["--yes"]);
40
+ } catch (e) {
41
+ process.stdout.write(`Init warning: ${e?.message || e}\n`);
42
+ }
43
+ }
44
+
45
+ try {
46
+ const { installLocalTrackerApp } = require("./init");
47
+ await installLocalTrackerApp({ appDir: path.join(trackerDir, "app") });
48
+ } catch (e) {
49
+ process.stdout.write(`Runtime refresh warning: ${e?.message || e}\n`);
50
+ }
51
+
52
+ // 1. Optional sync
53
+ if (opts.sync) {
54
+ process.stdout.write("Syncing local data...\n");
55
+ try {
56
+ const { cmdSync } = require("./sync");
57
+ await cmdSync(["--auto"]);
58
+ process.stdout.write("Sync done.\n");
59
+ } catch (e) {
60
+ process.stdout.write(`Sync warning: ${e?.message || e}\n`);
61
+ }
62
+ }
63
+
64
+ // 2. Resolve paths
65
+ const queuePath = resolveQueuePath();
66
+ const dashboardDir = resolveDashboardDir();
67
+
68
+ // 2.1 Refresh LiteLLM pricing data in the background. The seed snapshot is
69
+ // already loaded synchronously at require-time, so cost calculation is
70
+ // functional right now; ensurePricingLoaded() only upgrades to fresh
71
+ // disk cache or upstream data. Awaiting it here would block startup
72
+ // for the full 10s fetch timeout when offline / behind a firewall.
73
+ const { cacheDir } = await resolveTrackerPaths();
74
+ ensurePricingLoaded({ cachePath: path.join(cacheDir, "pricing.json") }).catch(
75
+ (e) => process.stdout.write(`Pricing refresh warning: ${e?.message || e}\n`),
76
+ );
77
+
78
+ if (!dashboardDir) {
79
+ process.stderr.write(
80
+ [
81
+ "Dashboard not found.",
82
+ "",
83
+ "If you cloned the repo, run:",
84
+ " cd dashboard && npm run build",
85
+ "",
86
+ "If you installed via npm, the package may be missing dashboard/dist/.",
87
+ "",
88
+ ].join("\n"),
89
+ );
90
+ process.exitCode = 1;
91
+ return;
92
+ }
93
+
94
+ // 3. Create handler
95
+ const handleApi = createLocalApiHandler({ queuePath });
96
+
97
+ const server = http.createServer(async (req, res) => {
98
+ try {
99
+ const url = new URL(req.url || "/", `http://${req.headers.host || "localhost"}`);
100
+
101
+ // CORS preflight
102
+ if (req.method === "OPTIONS") {
103
+ res.writeHead(204, {
104
+ "Access-Control-Allow-Methods": "GET, POST, OPTIONS",
105
+ "Access-Control-Allow-Headers": "Content-Type, Authorization",
106
+ });
107
+ res.end();
108
+ return;
109
+ }
110
+
111
+ // API routes
112
+ if (
113
+ url.pathname.startsWith("/functions/")
114
+ || url.pathname.startsWith("/api/")
115
+ || url.pathname.startsWith("/proxy/")
116
+ ) {
117
+ const handled = await handleApi(req, res, url);
118
+ if (handled) return;
119
+ }
120
+
121
+ // Static files
122
+ const served = await serveStaticFile(dashboardDir, url.pathname, res);
123
+ if (served) return;
124
+
125
+ // SPA fallback
126
+ await serveStaticFile(dashboardDir, "/index.html", res);
127
+ } catch (e) {
128
+ if (!res.headersSent) {
129
+ res.writeHead(500, { "Content-Type": "text/plain" });
130
+ res.end("Internal Server Error");
131
+ }
132
+ }
133
+ });
134
+
135
+ // 4. Listen. Default startup follows README behavior and picks the next
136
+ // available port; an explicit --port/PORT remains strict.
137
+ let port;
138
+ try {
139
+ port = await listenOnAvailablePort(server, opts.port, {
140
+ allowFallback: !opts.portExplicit,
141
+ ensurePortFreeFn: opts.portExplicit ? ensurePortFree : null,
142
+ onRetry: (failedPort) => {
143
+ process.stdout.write(`Port ${failedPort} unavailable, trying ${failedPort + 1}...\n`);
144
+ },
145
+ });
146
+ } catch (e) {
147
+ if (isPortUnavailableError(e)) {
148
+ process.stderr.write(buildPortInUseHint(opts.port));
149
+ } else {
150
+ process.stderr.write(`Server error: ${e.message}\n`);
151
+ }
152
+ process.exitCode = 1;
153
+ return;
154
+ }
155
+
156
+ {
157
+ const url = getLocalServerUrl(port);
158
+ process.stdout.write(
159
+ [
160
+ "",
161
+ ` tokentracker dashboard running at:`,
162
+ "",
163
+ ` ${url}`,
164
+ "",
165
+ ` Data: ${queuePath}`,
166
+ ` Press Ctrl+C to stop.`,
167
+ "",
168
+ ].join("\n"),
169
+ );
170
+
171
+ if (opts.open) {
172
+ openInBrowser(url);
173
+ }
174
+ }
175
+
176
+ server.on("error", (e) => {
177
+ process.stderr.write(`Server error: ${e.message}\n`);
178
+ process.exitCode = 1;
179
+ });
180
+
181
+ // 5. Graceful shutdown
182
+ const shutdown = () => {
183
+ process.stdout.write("\nShutting down...\n");
184
+ server.close(() => process.exit(0));
185
+ setTimeout(() => process.exit(0), 3000);
186
+ };
187
+ process.on("SIGINT", shutdown);
188
+ process.on("SIGTERM", shutdown);
189
+
190
+ // Keep process alive
191
+ await new Promise(() => {});
192
+ }
193
+
194
+ function findPidOnPort(port) {
195
+ try {
196
+ const out = cp.execFileSync("lsof", ["-ti", `tcp:${port}`], { encoding: "utf8", timeout: 5000 });
197
+ const pids = out.trim().split(/\s+/).map(Number).filter((n) => Number.isFinite(n) && n > 0);
198
+ return pids;
199
+ } catch (_e) {
200
+ return [];
201
+ }
202
+ }
203
+
204
+ async function ensurePortFree(port) {
205
+ const pids = findPidOnPort(port);
206
+ if (pids.length === 0) return;
207
+
208
+ // Don't kill ourselves
209
+ const self = process.pid;
210
+ const targets = pids.filter((p) => p !== self);
211
+ if (targets.length === 0) return;
212
+
213
+ process.stdout.write(`Stopping previous server on port ${port} (pid ${targets.join(", ")})...\n`);
214
+ for (const pid of targets) {
215
+ try {
216
+ process.kill(pid, "SIGTERM");
217
+ } catch (_e) {}
218
+ }
219
+
220
+ // Wait briefly for port to free up
221
+ for (let i = 0; i < 10; i++) {
222
+ await new Promise((r) => setTimeout(r, 300));
223
+ if (findPidOnPort(port).length === 0) return;
224
+ }
225
+
226
+ // Force kill if still alive
227
+ for (const pid of targets) {
228
+ try {
229
+ process.kill(pid, "SIGKILL");
230
+ } catch (_e) {}
231
+ }
232
+ await new Promise((r) => setTimeout(r, 500));
233
+ }
234
+
235
+ function listenOnce(server, port, host) {
236
+ return new Promise((resolve, reject) => {
237
+ let settled = false;
238
+
239
+ const cleanup = () => {
240
+ server.off("listening", onListening);
241
+ server.off("error", onError);
242
+ };
243
+ const finish = (fn, value) => {
244
+ if (settled) return;
245
+ settled = true;
246
+ cleanup();
247
+ fn(value);
248
+ };
249
+ const onListening = () => finish(resolve);
250
+ const onError = (error) => finish(reject, error);
251
+
252
+ server.once("listening", onListening);
253
+ server.once("error", onError);
254
+ try {
255
+ server.listen(port, host);
256
+ } catch (error) {
257
+ finish(reject, error);
258
+ }
259
+ });
260
+ }
261
+
262
+ async function listenOnAvailablePort(
263
+ server,
264
+ startPort,
265
+ {
266
+ host = LOCAL_BIND_HOST,
267
+ allowFallback = false,
268
+ maxAttempts = DEFAULT_MAX_PORT_ATTEMPTS,
269
+ ensurePortFreeFn = null,
270
+ onRetry = null,
271
+ } = {},
272
+ ) {
273
+ const attempts = allowFallback ? Math.max(1, maxAttempts) : 1;
274
+ let port = startPort;
275
+ let lastError = null;
276
+
277
+ for (let i = 0; i < attempts && port < 65536; i++, port++) {
278
+ if (ensurePortFreeFn) {
279
+ await ensurePortFreeFn(port);
280
+ }
281
+
282
+ try {
283
+ await listenOnce(server, port, host);
284
+ return port;
285
+ } catch (error) {
286
+ lastError = error;
287
+ if (!allowFallback || !isPortUnavailableError(error) || port >= 65535) {
288
+ throw error;
289
+ }
290
+ if (typeof onRetry === "function") {
291
+ onRetry(port, error);
292
+ }
293
+ }
294
+ }
295
+
296
+ throw lastError || new Error(`No available port found from ${startPort}`);
297
+ }
298
+
299
+ function resolveDashboardDir() {
300
+ const candidates = [
301
+ path.resolve(__dirname, "../../dashboard/dist"),
302
+ path.resolve(__dirname, "../dashboard/dist"),
303
+ ];
304
+ for (const dir of candidates) {
305
+ if (fssync.existsSync(path.join(dir, "index.html"))) return dir;
306
+ }
307
+ return null;
308
+ }
309
+
310
+ function parsePort(value) {
311
+ const n = parseInt(value, 10);
312
+ return Number.isFinite(n) && n > 0 && n < 65536 ? n : null;
313
+ }
314
+
315
+ function parseArgs(argv, env = process.env) {
316
+ const envPort = parsePort(env.PORT);
317
+ const opts = {
318
+ port: envPort || DEFAULT_PORT,
319
+ portExplicit: Boolean(envPort),
320
+ open: true,
321
+ sync: false,
322
+ };
323
+ for (let i = 0; i < argv.length; i++) {
324
+ const arg = argv[i];
325
+ if (arg === "--port" && i + 1 < argv.length) {
326
+ const n = parsePort(argv[++i]);
327
+ if (n) {
328
+ opts.port = n;
329
+ opts.portExplicit = true;
330
+ }
331
+ } else if (arg === "--no-open") {
332
+ opts.open = false;
333
+ } else if (arg === "--sync") {
334
+ opts.sync = true;
335
+ } else if (arg === "--no-sync") {
336
+ opts.sync = false;
337
+ }
338
+ }
339
+ return opts;
340
+ }
341
+
342
+ module.exports = {
343
+ cmdServe,
344
+ buildPortInUseHint,
345
+ NPM_PACKAGE_NAME,
346
+ LOCAL_BIND_HOST,
347
+ isPortUnavailableError,
348
+ listenOnAvailablePort,
349
+ getLocalServerUrl,
350
+ parseArgs,
351
+ };