tokmon 0.22.7 → 0.23.1
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 +1 -0
- package/dist/{bootstrap-ink-OHPQZKGB.js → bootstrap-ink-UD3KDTOB.js} +1113 -818
- package/dist/{chunk-EMQL5P5J.js → chunk-AON6GE7D.js} +368 -280
- package/dist/{chunk-TNIADJ3W.js → chunk-DJPUYMZM.js} +49 -16
- package/dist/chunk-HXDAOOSI.js +154 -0
- package/dist/{chunk-I4USANOF.js → chunk-PGQK7MOE.js} +879 -391
- package/dist/chunk-RM3ZE44Z.js +193 -0
- package/dist/cli.js +93 -54
- package/dist/{config-WLRQHLVJ.js → config-5CNJN3P7.js} +1 -1
- package/dist/daemon-QGCI46US.js +193 -0
- package/dist/daemon-handle-4F4TXGXJ.js +163 -0
- package/dist/server-4CDZMLH6.js +10 -0
- package/dist/web/assets/Area-DGo9K-U3.js +2 -0
- package/dist/web/assets/analytics-BOhbhV5Z.js +2 -0
- package/dist/web/assets/breakdown-CO04nSN-.js +4 -0
- package/dist/web/assets/chart-bNxdfXxA.js +69 -0
- package/dist/web/assets/explore-sZnmjzAj.js +22 -0
- package/dist/web/assets/index-DSj-Bxov.css +1 -0
- package/dist/web/assets/index-DnDr1VUr.js +83 -0
- package/dist/web/assets/models-Cl6D8VVF.js +2 -0
- package/dist/web/assets/overview-C18nvaAK.js +2 -0
- package/dist/web/assets/panel-DTjMnHaz.js +1 -0
- package/dist/web/assets/primitives-Dj_9YxTF.js +1 -0
- package/dist/web/assets/settings-sheet-0BGb5Ahv.js +1 -0
- package/dist/web/assets/share-sheet-BHtt-OmT.js +2 -0
- package/dist/web/assets/timeline-DmzB8Z3W.js +1 -0
- package/dist/web/assets/use-dialog-trap-DeXFtxx8.js +1 -0
- package/dist/web/index.html +3 -2
- package/package.json +9 -5
- package/dist/daemon-IP3UCTPB.js +0 -213
- package/dist/daemon-handle-HLSKLMWU.js +0 -142
- package/dist/server-GA2FKYDQ.js +0 -9
- package/dist/web/assets/breakdown-DU2j1sQ8.js +0 -4
- package/dist/web/assets/index-BCaPGYQA.css +0 -1
- package/dist/web/assets/index-ygZyXDLT.js +0 -173
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DJqRU3vO.woff +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-500-normal-DmUKJPL_.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-700-normal-BWTpRfYl.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-cyrillic-700-normal-CEoEElIJ.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-500-normal-D7SFKleX.woff +0 -0
- package/dist/web/assets/jetbrains-mono-greek-500-normal-JpySY46c.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-700-normal-C6CZE3T8.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-greek-700-normal-DEigVDxa.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-Cut-4mMH.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-500-normal-ckzbgY84.woff +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-700-normal-CZipNAKV.woff2 +0 -0
- package/dist/web/assets/jetbrains-mono-latin-ext-700-normal-CxPITLHs.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-500-normal-DNRqzVM1.woff +0 -0
- package/dist/web/assets/jetbrains-mono-vietnamese-700-normal-BDLVIk2r.woff +0 -0
- package/dist/web/assets/timeline-CO2B2c-0.js +0 -1
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
cacheDir
|
|
4
|
+
} from "./chunk-DJPUYMZM.js";
|
|
5
|
+
|
|
6
|
+
// src/web/lockfile.ts
|
|
7
|
+
import { closeSync, fchmodSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
8
|
+
import { isAbsolute, join } from "path";
|
|
9
|
+
function lockDir(opts = {}) {
|
|
10
|
+
const injected = opts.cachePath ?? process.env.TOKMON_DAEMON_CACHE_DIR;
|
|
11
|
+
return injected && isAbsolute(injected) ? injected : cacheDir();
|
|
12
|
+
}
|
|
13
|
+
function lockfilePath(opts = {}) {
|
|
14
|
+
return join(lockDir(opts), "daemon.json");
|
|
15
|
+
}
|
|
16
|
+
function isLoopbackUrl(value) {
|
|
17
|
+
try {
|
|
18
|
+
const url = new URL(value);
|
|
19
|
+
return url.protocol === "http:" && (url.hostname === "127.0.0.1" || url.hostname === "localhost" || url.hostname === "::1");
|
|
20
|
+
} catch {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function validLock(value) {
|
|
25
|
+
const lock = value;
|
|
26
|
+
return !!lock && typeof lock.pid === "number" && Number.isInteger(lock.pid) && lock.pid > 0 && typeof lock.port === "number" && Number.isInteger(lock.port) && lock.port >= 0 && lock.port <= 65535 && typeof lock.url === "string" && (lock.state === "starting" || isLoopbackUrl(lock.url)) && typeof lock.wsToken === "string" && lock.wsToken.length >= 32 && typeof lock.version === "string" && typeof lock.startedAt === "number" && typeof lock.ownerId === "string" && lock.ownerId.length >= 32 && (lock.state === "starting" || lock.state === "ready");
|
|
27
|
+
}
|
|
28
|
+
function readLock(opts = {}) {
|
|
29
|
+
try {
|
|
30
|
+
const path = lockfilePath(opts);
|
|
31
|
+
const stat = statSync(path);
|
|
32
|
+
if (!stat.isFile() || (stat.mode & 63) !== 0) return null;
|
|
33
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
34
|
+
return validLock(parsed) ? parsed : null;
|
|
35
|
+
} catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function ensureLockDir(opts) {
|
|
40
|
+
const dir = lockDir(opts);
|
|
41
|
+
mkdirSync(dir, { recursive: true, mode: 448 });
|
|
42
|
+
let fd;
|
|
43
|
+
try {
|
|
44
|
+
fd = openSync(dir, "r");
|
|
45
|
+
fchmodSync(fd, 448);
|
|
46
|
+
} catch {
|
|
47
|
+
} finally {
|
|
48
|
+
if (fd !== void 0) try {
|
|
49
|
+
closeSync(fd);
|
|
50
|
+
} catch {
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
function writeNew(path, lock) {
|
|
55
|
+
let fd;
|
|
56
|
+
let created = false;
|
|
57
|
+
try {
|
|
58
|
+
fd = openSync(path, "wx", 384);
|
|
59
|
+
created = true;
|
|
60
|
+
fchmodSync(fd, 384);
|
|
61
|
+
writeFileSync(fd, JSON.stringify(lock));
|
|
62
|
+
fsyncSync(fd);
|
|
63
|
+
return true;
|
|
64
|
+
} catch {
|
|
65
|
+
if (created) try {
|
|
66
|
+
unlinkSync(path);
|
|
67
|
+
} catch {
|
|
68
|
+
}
|
|
69
|
+
return false;
|
|
70
|
+
} finally {
|
|
71
|
+
if (fd !== void 0) try {
|
|
72
|
+
closeSync(fd);
|
|
73
|
+
} catch {
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function acquireLock(lock, opts = {}) {
|
|
78
|
+
try {
|
|
79
|
+
ensureLockDir(opts);
|
|
80
|
+
return writeNew(lockfilePath(opts), lock);
|
|
81
|
+
} catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
function writeLock(lock, opts = {}) {
|
|
86
|
+
const current = readLock(opts);
|
|
87
|
+
if (!current || current.ownerId !== lock.ownerId || current.pid !== process.pid) return false;
|
|
88
|
+
const path = lockfilePath(opts);
|
|
89
|
+
const tmp = join(lockDir(opts), `daemon.json.${process.pid}.${lock.ownerId}.tmp`);
|
|
90
|
+
let fd;
|
|
91
|
+
try {
|
|
92
|
+
writeFileSync(tmp, JSON.stringify(lock), { mode: 384 });
|
|
93
|
+
try {
|
|
94
|
+
fd = openSync(tmp, "r");
|
|
95
|
+
fchmodSync(fd, 384);
|
|
96
|
+
fsyncSync(fd);
|
|
97
|
+
} catch {
|
|
98
|
+
}
|
|
99
|
+
renameSync(tmp, path);
|
|
100
|
+
return true;
|
|
101
|
+
} catch {
|
|
102
|
+
try {
|
|
103
|
+
unlinkSync(tmp);
|
|
104
|
+
} catch {
|
|
105
|
+
}
|
|
106
|
+
return false;
|
|
107
|
+
} finally {
|
|
108
|
+
if (fd !== void 0) try {
|
|
109
|
+
closeSync(fd);
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function unlinkLock(ownerId, opts = {}) {
|
|
115
|
+
const current = readLock(opts);
|
|
116
|
+
if (!current || current.ownerId !== ownerId || current.pid !== process.pid) return false;
|
|
117
|
+
try {
|
|
118
|
+
unlinkSync(lockfilePath(opts));
|
|
119
|
+
return true;
|
|
120
|
+
} catch {
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function reclaimDeadLock(opts = {}) {
|
|
125
|
+
const current = readLock(opts);
|
|
126
|
+
if (!current || isAlive(current.pid)) return false;
|
|
127
|
+
try {
|
|
128
|
+
unlinkSync(lockfilePath(opts));
|
|
129
|
+
return true;
|
|
130
|
+
} catch {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function reclaimAbandonedLock(opts = {}, graceMs = 1e4) {
|
|
135
|
+
const path = lockfilePath(opts);
|
|
136
|
+
try {
|
|
137
|
+
const before = lstatSync(path);
|
|
138
|
+
if (!before.isFile()) return false;
|
|
139
|
+
let pid = null;
|
|
140
|
+
try {
|
|
141
|
+
const parsed = JSON.parse(readFileSync(path, "utf-8"));
|
|
142
|
+
if (Number.isInteger(parsed.pid) && parsed.pid > 0) pid = parsed.pid;
|
|
143
|
+
} catch {
|
|
144
|
+
}
|
|
145
|
+
if (pid !== null && isAlive(pid)) return false;
|
|
146
|
+
if (pid === null && Date.now() - before.mtimeMs < graceMs) return false;
|
|
147
|
+
const after = lstatSync(path);
|
|
148
|
+
if (after.dev !== before.dev || after.ino !== before.ino) return false;
|
|
149
|
+
unlinkSync(path);
|
|
150
|
+
return true;
|
|
151
|
+
} catch {
|
|
152
|
+
return false;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
function isAlive(pid) {
|
|
156
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
157
|
+
try {
|
|
158
|
+
process.kill(pid, 0);
|
|
159
|
+
return true;
|
|
160
|
+
} catch (err) {
|
|
161
|
+
return err.code === "EPERM";
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
async function probeHealth(url, token, version, timeoutMs = 500) {
|
|
165
|
+
if (!isLoopbackUrl(url) || !token) return false;
|
|
166
|
+
try {
|
|
167
|
+
const res = await fetch(`${url.replace(/\/+$/, "")}/healthz`, {
|
|
168
|
+
headers: { "x-tokmon-token": token },
|
|
169
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
170
|
+
});
|
|
171
|
+
if (!res.ok) return false;
|
|
172
|
+
const body = await res.json();
|
|
173
|
+
return body.ok === true && body.owner === true && (version === void 0 || body.version === version);
|
|
174
|
+
} catch {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
async function verifyLock(lock, version, timeoutMs) {
|
|
179
|
+
if (!lock || lock.state !== "ready" || lock.version !== version || !isAlive(lock.pid)) return null;
|
|
180
|
+
return await probeHealth(lock.url, lock.wsToken, version, timeoutMs) ? lock : null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
export {
|
|
184
|
+
readLock,
|
|
185
|
+
acquireLock,
|
|
186
|
+
writeLock,
|
|
187
|
+
unlinkLock,
|
|
188
|
+
reclaimDeadLock,
|
|
189
|
+
reclaimAbandonedLock,
|
|
190
|
+
isAlive,
|
|
191
|
+
probeHealth,
|
|
192
|
+
verifyLock
|
|
193
|
+
};
|
package/dist/cli.js
CHANGED
|
@@ -2,8 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.tsx
|
|
4
4
|
import { EventEmitter } from "events";
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
function describeError(error) {
|
|
6
|
+
if (error instanceof Error) return error.message || error.name;
|
|
7
|
+
if (typeof error === "string") return error;
|
|
8
|
+
try {
|
|
9
|
+
return JSON.stringify(error) ?? String(error);
|
|
10
|
+
} catch {
|
|
11
|
+
return String(error);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
function reportFatalError(context, error) {
|
|
15
|
+
console.error(`tokmon: ${context}: ${describeError(error)}`);
|
|
16
|
+
if (error instanceof Error && error.stack && error.stack !== error.message) console.error(error.stack);
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
}
|
|
7
19
|
EventEmitter.defaultMaxListeners = 100;
|
|
8
20
|
var emitWarning = process.emitWarning.bind(process);
|
|
9
21
|
process.emitWarning = ((warning, ...rest) => {
|
|
@@ -13,60 +25,87 @@ process.emitWarning = ((warning, ...rest) => {
|
|
|
13
25
|
});
|
|
14
26
|
var args = process.argv.slice(2);
|
|
15
27
|
var subcommand = args[0]?.toLowerCase();
|
|
16
|
-
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
|
|
28
|
+
function parsePort(value) {
|
|
29
|
+
const port = value === void 0 ? Number.NaN : Number(value);
|
|
30
|
+
if (!Number.isInteger(port) || port < 0 || port > 65535) {
|
|
31
|
+
throw new Error("--port must be an integer between 0 and 65535");
|
|
32
|
+
}
|
|
33
|
+
return port;
|
|
20
34
|
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
35
|
+
function validateServeArgs(serveArgs) {
|
|
36
|
+
for (let i = 0; i < serveArgs.length; i++) {
|
|
37
|
+
const arg = serveArgs[i];
|
|
38
|
+
if (arg === "--port" || arg === "-p") parsePort(serveArgs[++i]);
|
|
39
|
+
else if (arg.startsWith("--port=")) parsePort(arg.slice("--port=".length));
|
|
40
|
+
}
|
|
25
41
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
42
|
+
async function main() {
|
|
43
|
+
if (subcommand === "__daemon") {
|
|
44
|
+
const { runDaemon } = await import("./daemon-QGCI46US.js");
|
|
45
|
+
await runDaemon(args.slice(1), { foreground: false });
|
|
46
|
+
process.exitCode ??= 0;
|
|
47
|
+
return;
|
|
32
48
|
}
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
console.log(" tokmon serve [--port <n>] [--no-open] Launch the web dashboard\n");
|
|
40
|
-
console.log("Options:");
|
|
41
|
-
console.log(" -i, --interval <seconds> Refresh interval (default: from config or 2)");
|
|
42
|
-
console.log(" --ascii Force ASCII glyphs (also: TOKMON_ASCII=1)");
|
|
43
|
-
console.log(" --no-ascii Force Unicode glyphs");
|
|
44
|
-
console.log(" -h, --help Show this help\n");
|
|
45
|
-
console.log("Keybindings:");
|
|
46
|
-
console.log(" Tab Switch Dashboard / Table");
|
|
47
|
-
console.log(" p / P Cycle table provider");
|
|
48
|
-
console.log(" a / A Cycle account focus");
|
|
49
|
-
console.log(" 0-9 Jump to account focus");
|
|
50
|
-
console.log(" \u2191\u2193 Scroll table");
|
|
51
|
-
console.log(" w / W Toggle web dashboard");
|
|
52
|
-
console.log(" s Settings");
|
|
53
|
-
console.log(" q Quit");
|
|
54
|
-
process.exit(0);
|
|
49
|
+
if (subcommand === "serve" || subcommand === "web") {
|
|
50
|
+
validateServeArgs(args.slice(1));
|
|
51
|
+
const { runDaemon } = await import("./daemon-QGCI46US.js");
|
|
52
|
+
await runDaemon(args.slice(1), { foreground: true });
|
|
53
|
+
process.exitCode ??= 0;
|
|
54
|
+
return;
|
|
55
55
|
}
|
|
56
|
+
let interval;
|
|
57
|
+
let asciiFlag = null;
|
|
58
|
+
for (let i = 0; i < args.length; i++) {
|
|
59
|
+
if (args[i] === "--interval" || args[i] === "-i") {
|
|
60
|
+
const seconds = Number(args[i + 1]);
|
|
61
|
+
if (!Number.isFinite(seconds) || seconds <= 0) {
|
|
62
|
+
throw new Error("--interval must be a positive number of seconds");
|
|
63
|
+
}
|
|
64
|
+
interval = Math.max(500, seconds * 1e3);
|
|
65
|
+
i++;
|
|
66
|
+
}
|
|
67
|
+
if (args[i] === "--ascii") asciiFlag = "on";
|
|
68
|
+
if (args[i] === "--no-ascii") asciiFlag = "off";
|
|
69
|
+
if (args[i] === "--help" || args[i] === "-h") {
|
|
70
|
+
console.log("tokmon - Terminal usage dashboard for your AI coding tools\n");
|
|
71
|
+
console.log(" Claude \xB7 Codex \xB7 Cursor \xB7 Copilot \xB7 opencode \xB7 pi \xB7 Antigravity \xB7 Gemini \xB7 Grok\n");
|
|
72
|
+
console.log("Usage: tokmon [options]");
|
|
73
|
+
console.log(" tokmon serve [--port <n>] [--no-open] Launch the web dashboard\n");
|
|
74
|
+
console.log("Options:");
|
|
75
|
+
console.log(" -i, --interval <seconds> Refresh interval (default: from config or 2)");
|
|
76
|
+
console.log(" --ascii Force ASCII glyphs (also: TOKMON_ASCII=1)");
|
|
77
|
+
console.log(" --no-ascii Force Unicode glyphs");
|
|
78
|
+
console.log(" -h, --help Show this help\n");
|
|
79
|
+
console.log("Keybindings:");
|
|
80
|
+
console.log(" Tab Switch Dashboard / Table");
|
|
81
|
+
console.log(" p / P Cycle table provider");
|
|
82
|
+
console.log(" a / A Cycle account focus");
|
|
83
|
+
console.log(" 0-9 Jump to account focus");
|
|
84
|
+
console.log(" \u2191\u2193 Scroll table");
|
|
85
|
+
console.log(" w / W Toggle web dashboard");
|
|
86
|
+
console.log(" r / R Refresh all data");
|
|
87
|
+
console.log(" s Settings");
|
|
88
|
+
console.log(" q Quit");
|
|
89
|
+
process.exitCode = 0;
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
const { loadConfig } = await import("./config-5CNJN3P7.js");
|
|
94
|
+
const { resolveGlyphs, setGlyphs } = await import("./glyphs-NKCSZLGO.js");
|
|
95
|
+
const { attachOrSpawn } = await import("./daemon-handle-4F4TXGXJ.js");
|
|
96
|
+
const config = await loadConfig();
|
|
97
|
+
setGlyphs(resolveGlyphs({
|
|
98
|
+
flag: asciiFlag,
|
|
99
|
+
env: process.env,
|
|
100
|
+
config: config.ascii,
|
|
101
|
+
isTTY: !!process.stdout.isTTY,
|
|
102
|
+
platform: process.platform
|
|
103
|
+
}));
|
|
104
|
+
const daemon = await attachOrSpawn();
|
|
105
|
+
const mode = daemon.kind === "spawned" ? "connected" : "degraded";
|
|
106
|
+
const { bootstrapInk } = await import("./bootstrap-ink-UD3KDTOB.js");
|
|
107
|
+
await bootstrapInk({ interval, config, daemon, mode });
|
|
56
108
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
var config = await loadConfig();
|
|
61
|
-
var isTTY = process.stdout.isTTY === true;
|
|
62
|
-
setGlyphs(resolveGlyphs({
|
|
63
|
-
flag: asciiFlag,
|
|
64
|
-
env: process.env,
|
|
65
|
-
config: config.ascii,
|
|
66
|
-
isTTY: !!process.stdout.isTTY,
|
|
67
|
-
platform: process.platform
|
|
68
|
-
}));
|
|
69
|
-
var daemon = await attachOrSpawn();
|
|
70
|
-
var mode = daemon.kind === "spawned" ? "connected" : "degraded";
|
|
71
|
-
var { bootstrapInk } = await import("./bootstrap-ink-OHPQZKGB.js");
|
|
72
|
-
await bootstrapInk({ interval, config, daemon, mode });
|
|
109
|
+
void main().catch((error) => {
|
|
110
|
+
reportFatalError("fatal error", error);
|
|
111
|
+
});
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import {
|
|
3
|
+
startWebServer
|
|
4
|
+
} from "./chunk-AON6GE7D.js";
|
|
5
|
+
import {
|
|
6
|
+
acquireLock,
|
|
7
|
+
isAlive,
|
|
8
|
+
readLock,
|
|
9
|
+
reclaimAbandonedLock,
|
|
10
|
+
reclaimDeadLock,
|
|
11
|
+
unlinkLock,
|
|
12
|
+
verifyLock,
|
|
13
|
+
writeLock
|
|
14
|
+
} from "./chunk-RM3ZE44Z.js";
|
|
15
|
+
import {
|
|
16
|
+
appVersion,
|
|
17
|
+
browserUrl,
|
|
18
|
+
openBrowser
|
|
19
|
+
} from "./chunk-HXDAOOSI.js";
|
|
20
|
+
import {
|
|
21
|
+
flushDisk
|
|
22
|
+
} from "./chunk-PGQK7MOE.js";
|
|
23
|
+
import {
|
|
24
|
+
loadConfig
|
|
25
|
+
} from "./chunk-DJPUYMZM.js";
|
|
26
|
+
|
|
27
|
+
// src/web/daemon.ts
|
|
28
|
+
import { randomBytes } from "crypto";
|
|
29
|
+
function parseDaemonArgs(args) {
|
|
30
|
+
let port;
|
|
31
|
+
let open = true;
|
|
32
|
+
let help = false;
|
|
33
|
+
for (let i = 0; i < args.length; i++) {
|
|
34
|
+
const arg = args[i];
|
|
35
|
+
if ((arg === "--port" || arg === "-p") && args[i + 1]) port = Number(args[++i]);
|
|
36
|
+
else if (arg.startsWith("--port=")) port = Number(arg.slice("--port=".length));
|
|
37
|
+
else if (arg === "--no-open") open = false;
|
|
38
|
+
else if (arg === "--help" || arg === "-h") help = true;
|
|
39
|
+
}
|
|
40
|
+
if (port !== void 0 && (!Number.isInteger(port) || port < 0 || port > 65535)) port = void 0;
|
|
41
|
+
return { port, open, help };
|
|
42
|
+
}
|
|
43
|
+
var SERVE_HELP = `tokmon serve - Launch the tokmon web dashboard (local, loopback only)
|
|
44
|
+
|
|
45
|
+
Usage: tokmon serve [options]
|
|
46
|
+
|
|
47
|
+
Options:
|
|
48
|
+
-p, --port <n> Port to listen on (default: 4317, auto-falls back if taken)
|
|
49
|
+
--no-open Don't open the browser automatically
|
|
50
|
+
-h, --help Show this help
|
|
51
|
+
`;
|
|
52
|
+
function handshake(lock) {
|
|
53
|
+
process.stdout.write(JSON.stringify({ ready: 1, url: lock.url, port: lock.port, wsToken: lock.wsToken, version: lock.version }) + "\n");
|
|
54
|
+
}
|
|
55
|
+
function describeExisting(lock, open) {
|
|
56
|
+
const displayUrl = open ? lock.url : browserUrl(lock.url, lock.wsToken);
|
|
57
|
+
process.stdout.write(`
|
|
58
|
+
\u25C6 tokmon web \u2192 ${displayUrl}
|
|
59
|
+
`);
|
|
60
|
+
process.stdout.write(" reusing the live singleton daemon\n\n");
|
|
61
|
+
if (open) {
|
|
62
|
+
openBrowser(lock.url, lock.wsToken);
|
|
63
|
+
process.stdout.write(" opening browser\u2026\n");
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
async function runDaemon(args, opts) {
|
|
67
|
+
const { port, open, help } = parseDaemonArgs(args);
|
|
68
|
+
if (help && opts.foreground) {
|
|
69
|
+
process.stdout.write(SERVE_HELP);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
const version = appVersion();
|
|
73
|
+
let current = readLock();
|
|
74
|
+
if (!current && reclaimAbandonedLock()) current = readLock();
|
|
75
|
+
const live = await verifyLock(current, version);
|
|
76
|
+
if (live) {
|
|
77
|
+
if (opts.foreground) describeExisting(live, open);
|
|
78
|
+
else handshake(live);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
if (current?.state === "starting" && current.version === version && isAlive(current.pid)) {
|
|
82
|
+
for (let attempt = 0; attempt < 60; attempt++) {
|
|
83
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
84
|
+
const winner = await verifyLock(readLock(), version, 250);
|
|
85
|
+
if (winner) {
|
|
86
|
+
if (opts.foreground) describeExisting(winner, open);
|
|
87
|
+
else handshake(winner);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
if (current && isAlive(current.pid)) {
|
|
93
|
+
const message = "tokmon: another daemon owns the lock but could not be verified (version/token mismatch)";
|
|
94
|
+
if (opts.foreground) {
|
|
95
|
+
process.stderr.write(message + "\n");
|
|
96
|
+
process.exitCode = 1;
|
|
97
|
+
}
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
if (current) reclaimDeadLock();
|
|
101
|
+
const ownerId = randomBytes(32).toString("base64url");
|
|
102
|
+
const token = randomBytes(32).toString("base64url");
|
|
103
|
+
const reservation = {
|
|
104
|
+
pid: process.pid,
|
|
105
|
+
port: 0,
|
|
106
|
+
url: "",
|
|
107
|
+
wsToken: token,
|
|
108
|
+
version,
|
|
109
|
+
startedAt: Date.now(),
|
|
110
|
+
ownerId,
|
|
111
|
+
state: "starting"
|
|
112
|
+
};
|
|
113
|
+
if (!acquireLock(reservation)) {
|
|
114
|
+
for (let attempt = 0; attempt < 60; attempt++) {
|
|
115
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
116
|
+
const winner = await verifyLock(readLock(), version, 250);
|
|
117
|
+
if (winner) {
|
|
118
|
+
if (opts.foreground) describeExisting(winner, open);
|
|
119
|
+
else handshake(winner);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
if (opts.foreground) {
|
|
124
|
+
process.stderr.write("tokmon: daemon startup is already in progress\n");
|
|
125
|
+
process.exitCode = 1;
|
|
126
|
+
}
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let controller = null;
|
|
130
|
+
try {
|
|
131
|
+
const config = await loadConfig();
|
|
132
|
+
controller = await startWebServer({ config, port, log: opts.foreground, wsToken: token });
|
|
133
|
+
const ready = {
|
|
134
|
+
...reservation,
|
|
135
|
+
port: controller.port,
|
|
136
|
+
url: controller.url,
|
|
137
|
+
state: "ready"
|
|
138
|
+
};
|
|
139
|
+
if (!writeLock(ready)) throw new Error("lost daemon lock ownership during startup");
|
|
140
|
+
let shuttingDown = false;
|
|
141
|
+
const shutdown = async (exitCode = 0) => {
|
|
142
|
+
if (shuttingDown) return;
|
|
143
|
+
shuttingDown = true;
|
|
144
|
+
if (opts.foreground) process.stdout.write("\n stopping tokmon web\u2026\n");
|
|
145
|
+
try {
|
|
146
|
+
await controller?.stop();
|
|
147
|
+
} catch {
|
|
148
|
+
}
|
|
149
|
+
await flushDisk().catch(() => {
|
|
150
|
+
});
|
|
151
|
+
unlinkLock(ownerId);
|
|
152
|
+
process.exit(exitCode);
|
|
153
|
+
};
|
|
154
|
+
process.once("exit", () => {
|
|
155
|
+
unlinkLock(ownerId);
|
|
156
|
+
});
|
|
157
|
+
process.once("SIGINT", () => {
|
|
158
|
+
void shutdown(0);
|
|
159
|
+
});
|
|
160
|
+
process.once("SIGTERM", () => {
|
|
161
|
+
void shutdown(0);
|
|
162
|
+
});
|
|
163
|
+
if (opts.foreground) {
|
|
164
|
+
const displayUrl = open ? controller.url : controller.browserUrl;
|
|
165
|
+
process.stdout.write(`
|
|
166
|
+
\u25C6 tokmon web \u2192 ${displayUrl}
|
|
167
|
+
`);
|
|
168
|
+
process.stdout.write(" live dashboard \xB7 Ctrl-C to stop\n\n");
|
|
169
|
+
if (open) {
|
|
170
|
+
openBrowser(controller.url, token);
|
|
171
|
+
process.stdout.write(" opening browser\u2026\n");
|
|
172
|
+
}
|
|
173
|
+
} else {
|
|
174
|
+
handshake(ready);
|
|
175
|
+
}
|
|
176
|
+
await new Promise(() => {
|
|
177
|
+
});
|
|
178
|
+
} catch (error) {
|
|
179
|
+
try {
|
|
180
|
+
await controller?.stop();
|
|
181
|
+
} catch {
|
|
182
|
+
}
|
|
183
|
+
unlinkLock(ownerId);
|
|
184
|
+
const message = `tokmon: failed to start web server: ${error.message}`;
|
|
185
|
+
if (opts.foreground) {
|
|
186
|
+
process.stderr.write(message + "\n");
|
|
187
|
+
process.exitCode = 1;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
export {
|
|
192
|
+
runDaemon
|
|
193
|
+
};
|