@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,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
|
+
};
|