@zhangferry-dev/tokendash 1.6.0 → 1.6.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/dist/client/popover.html +4 -3
- package/dist/electron-server.cjs +29 -4
- package/dist/electron-server.cjs.map +2 -2
- package/dist/server/index.js +18 -3
- package/dist/server/routes/api.d.ts +6 -1
- package/dist/server/routes/api.js +11 -1
- package/electron/main.cjs +34 -8
- package/electron/npmSync.cjs +62 -0
- package/electron/serverReuse.cjs +59 -0
- package/electron/updateService.cjs +75 -3
- package/package.json +1 -1
- package/dist/server/ccusage.d.ts +0 -7
- package/dist/server/ccusage.js +0 -69
- package/electron/main.js +0 -291
- package/electron/trayBadge.js +0 -30
- package/resources/entitlements.mac.plist +0 -10
- package/resources/icon.png +0 -0
package/dist/server/index.js
CHANGED
|
@@ -12,11 +12,22 @@ const CLI_USAGE = [
|
|
|
12
12
|
' tokendash --port <number> [--no-open]',
|
|
13
13
|
' tokendash --tray [--port <number>]',
|
|
14
14
|
].join('\n');
|
|
15
|
+
const PACKAGE_NAME = '@zhangferry-dev/tokendash';
|
|
15
16
|
function getPackageVersion() {
|
|
16
17
|
const __filename = fileURLToPath(import.meta.url);
|
|
17
18
|
const __dirname = dirname(__filename);
|
|
18
|
-
const
|
|
19
|
-
|
|
19
|
+
const packageJsonPaths = [
|
|
20
|
+
join(__dirname, '..', '..', 'package.json'), // dist/server/index.js
|
|
21
|
+
join(__dirname, '..', 'package.json'), // dist/electron-server.cjs
|
|
22
|
+
];
|
|
23
|
+
for (const packageJsonPath of packageJsonPaths) {
|
|
24
|
+
if (!existsSync(packageJsonPath))
|
|
25
|
+
continue;
|
|
26
|
+
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
|
|
27
|
+
if (packageJson.version)
|
|
28
|
+
return packageJson.version;
|
|
29
|
+
}
|
|
30
|
+
return 'unknown';
|
|
20
31
|
}
|
|
21
32
|
function exitWithCliError(message) {
|
|
22
33
|
console.error(message);
|
|
@@ -137,7 +148,11 @@ export function createApp(_port, baseDir) {
|
|
|
137
148
|
const app = express();
|
|
138
149
|
const router = express.Router();
|
|
139
150
|
// Register API routes
|
|
140
|
-
registerApiRoutes(router
|
|
151
|
+
registerApiRoutes(router, {
|
|
152
|
+
packageName: PACKAGE_NAME,
|
|
153
|
+
version: getPackageVersion(),
|
|
154
|
+
dashboardUrl: `http://localhost:${resolvePort(_port)}`,
|
|
155
|
+
});
|
|
141
156
|
app.use('/api', router);
|
|
142
157
|
const { baseDir: _baseDir, isProduction } = resolveStaticAssetBaseDir(import.meta.url, baseDir);
|
|
143
158
|
const popoverPath = isProduction
|
|
@@ -1,2 +1,7 @@
|
|
|
1
1
|
import { type Router } from 'express';
|
|
2
|
-
export
|
|
2
|
+
export interface AppInfo {
|
|
3
|
+
packageName: string;
|
|
4
|
+
version: string;
|
|
5
|
+
dashboardUrl?: string;
|
|
6
|
+
}
|
|
7
|
+
export declare function registerApiRoutes(router: Router, appInfo: AppInfo): void;
|
|
@@ -26,7 +26,17 @@ function getAgents(_req, res) {
|
|
|
26
26
|
res.status(500).json({ error: 'Failed to detect agents', hint: message });
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
-
|
|
29
|
+
function getAppInfo(info) {
|
|
30
|
+
return (req, res) => {
|
|
31
|
+
const host = req.get('host');
|
|
32
|
+
res.json({
|
|
33
|
+
...info,
|
|
34
|
+
dashboardUrl: host ? `${req.protocol}://${host}` : info.dashboardUrl,
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
export function registerApiRoutes(router, appInfo) {
|
|
39
|
+
router.get('/app-info', getAppInfo(appInfo));
|
|
30
40
|
router.get('/agents', getAgents);
|
|
31
41
|
router.get('/daily', getDaily);
|
|
32
42
|
router.get('/monthly', getMonthly);
|
package/electron/main.cjs
CHANGED
|
@@ -19,6 +19,8 @@ try {
|
|
|
19
19
|
|
|
20
20
|
const { formatTokens } = require('./trayBadge.cjs');
|
|
21
21
|
const { checkForUpdates, downloadUpdateAsset } = require('./updateService.cjs');
|
|
22
|
+
const { syncNpmPackageVersion } = require('./npmSync.cjs');
|
|
23
|
+
const { findCompatibleServer, getDashboardUrl } = require('./serverReuse.cjs');
|
|
22
24
|
|
|
23
25
|
// Resolve trayHelper binary: extract from asar if needed
|
|
24
26
|
function resolveTrayHelperPath() {
|
|
@@ -52,6 +54,7 @@ let server = null;
|
|
|
52
54
|
let trayProcess = null;
|
|
53
55
|
let selectedAgents = null; // null = use all available agents
|
|
54
56
|
let serverPort = parseInt(process.env.TOKENDASH_PORT || '3456', 10);
|
|
57
|
+
let dashboardUrl = getDashboardUrl(serverPort);
|
|
55
58
|
let lastUpdateInfo = null;
|
|
56
59
|
let isDownloadingUpdate = false;
|
|
57
60
|
const POPOVER_WIDTH = 380;
|
|
@@ -99,6 +102,10 @@ function fetchJson(url) {
|
|
|
99
102
|
});
|
|
100
103
|
}
|
|
101
104
|
|
|
105
|
+
function getServerBaseUrl() {
|
|
106
|
+
return dashboardUrl || getDashboardUrl(serverPort);
|
|
107
|
+
}
|
|
108
|
+
|
|
102
109
|
function getAppInfo() {
|
|
103
110
|
// app.getVersion() returns Electron's version in dev mode (e.g. 41.5).
|
|
104
111
|
// Always read from package.json to get the app's own version.
|
|
@@ -245,7 +252,8 @@ function updateTrayBadge() {
|
|
|
245
252
|
const d = new Date(); const today = d.getFullYear() + "-" + String(d.getMonth()+1).padStart(2,"0") + "-" + String(d.getDate()).padStart(2,"0");
|
|
246
253
|
|
|
247
254
|
// Fetch agents list, then fetch daily data for each agent in parallel
|
|
248
|
-
|
|
255
|
+
const serverBaseUrl = getServerBaseUrl();
|
|
256
|
+
fetchJson(`${serverBaseUrl}/api/agents`)
|
|
249
257
|
.then((agentData) => {
|
|
250
258
|
let agents = (agentData && Array.isArray(agentData.available)) ? agentData.available : ['claude'];
|
|
251
259
|
if (agents.length === 0) {
|
|
@@ -262,7 +270,7 @@ function updateTrayBadge() {
|
|
|
262
270
|
const agentKey = getTrayAgentKey(agents);
|
|
263
271
|
return Promise.all(
|
|
264
272
|
agents.map(agent =>
|
|
265
|
-
fetchJson(
|
|
273
|
+
fetchJson(`${serverBaseUrl}/api/daily?agent=${agent}`)
|
|
266
274
|
.catch(() => null)
|
|
267
275
|
)
|
|
268
276
|
).then(results => ({ agentKey, results }));
|
|
@@ -350,7 +358,7 @@ function createPopoverWindow() {
|
|
|
350
358
|
},
|
|
351
359
|
});
|
|
352
360
|
|
|
353
|
-
popover.loadURL(
|
|
361
|
+
popover.loadURL(`${getServerBaseUrl()}/popover.html`);
|
|
354
362
|
|
|
355
363
|
popover.on('blur', () => {
|
|
356
364
|
popover.hide();
|
|
@@ -366,8 +374,7 @@ function createPopoverWindow() {
|
|
|
366
374
|
|
|
367
375
|
function registerIpcHandlers() {
|
|
368
376
|
ipcMain.handle('tokendash:open-dashboard', (_event, url) => {
|
|
369
|
-
|
|
370
|
-
return shell.openExternal(target);
|
|
377
|
+
return shell.openExternal(getServerBaseUrl());
|
|
371
378
|
});
|
|
372
379
|
|
|
373
380
|
ipcMain.handle('tokendash:get-app-info', () => {
|
|
@@ -462,15 +469,34 @@ app.whenReady().then(async () => {
|
|
|
462
469
|
if (server) server.close();
|
|
463
470
|
});
|
|
464
471
|
|
|
465
|
-
|
|
466
|
-
|
|
472
|
+
const currentVersion = getAppInfo().version;
|
|
473
|
+
syncNpmPackageVersion(PACKAGE_NAME, currentVersion).then((result) => {
|
|
474
|
+
if (!result || result.ok) return;
|
|
475
|
+
console.warn('Could not sync npm package version:', result.error || 'unknown error');
|
|
476
|
+
}).catch((error) => {
|
|
477
|
+
console.warn('Could not sync npm package version:', error instanceof Error ? error.message : String(error));
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
const existingServer = await findCompatibleServer(serverPort, currentVersion, PACKAGE_NAME);
|
|
481
|
+
if (existingServer) {
|
|
482
|
+
serverPort = existingServer.port;
|
|
483
|
+
dashboardUrl = existingServer.dashboardUrl;
|
|
484
|
+
console.log(`tokendash reusing CLI server on ${dashboardUrl}`);
|
|
485
|
+
startTrayHelper();
|
|
486
|
+
createPopoverWindow();
|
|
487
|
+
return;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
// Create and bind Express server.
|
|
491
|
+
// Pass dist/ directory so createApp resolves client assets correctly.
|
|
467
492
|
const distDir = path.join(__dirname, '..', 'dist');
|
|
468
493
|
const expressApp = createApp(serverPort, distDir);
|
|
469
494
|
try {
|
|
470
495
|
const result = await listenWithFallback(expressApp, serverPort);
|
|
471
496
|
server = result.server;
|
|
472
497
|
serverPort = result.port;
|
|
473
|
-
|
|
498
|
+
dashboardUrl = getDashboardUrl(result.port);
|
|
499
|
+
console.log(`tokendash running on ${dashboardUrl}`);
|
|
474
500
|
} catch (err) {
|
|
475
501
|
console.error('Failed to start server:', err);
|
|
476
502
|
app.quit();
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
const { spawn } = require('node:child_process');
|
|
2
|
+
|
|
3
|
+
function normalizeVersion(version) {
|
|
4
|
+
return String(version || '').trim().replace(/^v/, '');
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function shouldInstallPackage(installedVersion, targetVersion) {
|
|
8
|
+
const installed = normalizeVersion(installedVersion);
|
|
9
|
+
const target = normalizeVersion(targetVersion);
|
|
10
|
+
return Boolean(target) && installed !== target;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function buildNpmInstallArgs(packageName, version) {
|
|
14
|
+
return ['install', '-g', `${packageName}@${version}`];
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function runCommand(command, args) {
|
|
18
|
+
return new Promise((resolve) => {
|
|
19
|
+
const child = spawn(command, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
20
|
+
let stdout = '';
|
|
21
|
+
let stderr = '';
|
|
22
|
+
|
|
23
|
+
child.stdout.on('data', (chunk) => { stdout += chunk.toString(); });
|
|
24
|
+
child.stderr.on('data', (chunk) => { stderr += chunk.toString(); });
|
|
25
|
+
child.on('error', (error) => resolve({ ok: false, stdout, stderr, error }));
|
|
26
|
+
child.on('close', (code) => resolve({ ok: code === 0, code, stdout, stderr }));
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function getInstalledPackageVersion(packageName) {
|
|
31
|
+
const result = await runCommand('npm', ['list', '-g', packageName, '--depth=0', '--json']);
|
|
32
|
+
if (!result.ok) return null;
|
|
33
|
+
try {
|
|
34
|
+
const data = JSON.parse(result.stdout);
|
|
35
|
+
return normalizeVersion(data && data.dependencies && data.dependencies[packageName] && data.dependencies[packageName].version);
|
|
36
|
+
} catch (_) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function syncNpmPackageVersion(packageName, version) {
|
|
42
|
+
const installedVersion = await getInstalledPackageVersion(packageName);
|
|
43
|
+
if (!shouldInstallPackage(installedVersion, version)) {
|
|
44
|
+
return { ok: true, installedVersion, targetVersion: version, changed: false };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = await runCommand('npm', buildNpmInstallArgs(packageName, version));
|
|
48
|
+
return {
|
|
49
|
+
ok: result.ok,
|
|
50
|
+
installedVersion,
|
|
51
|
+
targetVersion: version,
|
|
52
|
+
changed: result.ok,
|
|
53
|
+
error: result.ok ? null : (result.error ? result.error.message : result.stderr || `npm exited with ${result.code}`),
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
module.exports = {
|
|
58
|
+
buildNpmInstallArgs,
|
|
59
|
+
getInstalledPackageVersion,
|
|
60
|
+
shouldInstallPackage,
|
|
61
|
+
syncNpmPackageVersion,
|
|
62
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
const http = require('node:http');
|
|
2
|
+
|
|
3
|
+
function normalizePort(port) {
|
|
4
|
+
const value = parseInt(String(port || ''), 10);
|
|
5
|
+
return Number.isInteger(value) && value > 0 ? value : 3456;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function getDashboardUrl(port) {
|
|
9
|
+
return `http://localhost:${normalizePort(port)}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function isCompatibleServerInfo(info, expectedVersion, expectedPackageName) {
|
|
13
|
+
return Boolean(
|
|
14
|
+
info &&
|
|
15
|
+
info.packageName === expectedPackageName &&
|
|
16
|
+
String(info.version || '').replace(/^v/, '') === String(expectedVersion || '').replace(/^v/, '')
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function fetchJson(url, timeoutMs = 1000) {
|
|
21
|
+
return new Promise((resolve, reject) => {
|
|
22
|
+
const req = http.get(url, (res) => {
|
|
23
|
+
let data = '';
|
|
24
|
+
res.on('data', (chunk) => { data += chunk; });
|
|
25
|
+
res.on('end', () => {
|
|
26
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
27
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
try { resolve(JSON.parse(data)); }
|
|
31
|
+
catch (error) { reject(error); }
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
req.setTimeout(timeoutMs, () => {
|
|
36
|
+
req.destroy(new Error('Request timed out'));
|
|
37
|
+
});
|
|
38
|
+
req.on('error', reject);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function findCompatibleServer(preferredPort, expectedVersion, expectedPackageName) {
|
|
43
|
+
const port = normalizePort(preferredPort);
|
|
44
|
+
try {
|
|
45
|
+
const info = await fetchJson(`${getDashboardUrl(port)}/api/app-info`);
|
|
46
|
+
if (isCompatibleServerInfo(info, expectedVersion, expectedPackageName)) {
|
|
47
|
+
return { port, dashboardUrl: info.dashboardUrl || getDashboardUrl(port), info };
|
|
48
|
+
}
|
|
49
|
+
} catch (_) {}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
fetchJson,
|
|
55
|
+
findCompatibleServer,
|
|
56
|
+
getDashboardUrl,
|
|
57
|
+
isCompatibleServerInfo,
|
|
58
|
+
normalizePort,
|
|
59
|
+
};
|
|
@@ -30,6 +30,35 @@ function fetchHttpsJson(url) {
|
|
|
30
30
|
});
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
function fetchLatestReleaseUrl(repo) {
|
|
34
|
+
const url = `https://github.com/${repo}/releases/latest`;
|
|
35
|
+
return new Promise((resolve, reject) => {
|
|
36
|
+
const opts = new URL(url);
|
|
37
|
+
const reqOpts = {
|
|
38
|
+
hostname: opts.hostname,
|
|
39
|
+
path: opts.pathname + opts.search,
|
|
40
|
+
method: 'HEAD',
|
|
41
|
+
headers: {
|
|
42
|
+
'User-Agent': 'TokenDash',
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
https.request(reqOpts, (res) => {
|
|
47
|
+
if ([301, 302, 303, 307, 308].includes(res.statusCode) && res.headers.location) {
|
|
48
|
+
resolve(new URL(res.headers.location, url).toString());
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (res.statusCode && res.statusCode >= 400) {
|
|
53
|
+
reject(new Error(`HTTP ${res.statusCode}`));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
resolve(url);
|
|
58
|
+
}).on('error', reject).end();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
33
62
|
function compareVersions(a, b) {
|
|
34
63
|
const aParts = String(a).replace(/^v/, '').split(/[.-]/).map((part) => parseInt(part, 10) || 0);
|
|
35
64
|
const bParts = String(b).replace(/^v/, '').split(/[.-]/).map((part) => parseInt(part, 10) || 0);
|
|
@@ -78,9 +107,50 @@ function getReleaseUpdateInfo(release, currentVersion, arch = process.arch) {
|
|
|
78
107
|
};
|
|
79
108
|
}
|
|
80
109
|
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
110
|
+
function buildDmgAssetFromVersion(repo, tagName, arch = process.arch) {
|
|
111
|
+
const version = String(tagName || '').replace(/^v/, '');
|
|
112
|
+
const archSuffix = arch === 'arm64' ? 'arm64' : arch === 'x64' ? 'x64' : 'universal';
|
|
113
|
+
const name = `TokenDash-${version}-${archSuffix}.dmg`;
|
|
114
|
+
|
|
115
|
+
return {
|
|
116
|
+
name,
|
|
117
|
+
size: 0,
|
|
118
|
+
url: `https://github.com/${repo}/releases/download/${tagName}/${name}`,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getRedirectReleaseUpdateInfo(repo, releaseUrl, currentVersion, arch = process.arch) {
|
|
123
|
+
const parsedUrl = new URL(releaseUrl);
|
|
124
|
+
const tagMatch = parsedUrl.pathname.match(/\/releases\/tag\/([^/]+)\/?$/);
|
|
125
|
+
if (!tagMatch) throw new Error('Unable to determine the latest release tag.');
|
|
126
|
+
|
|
127
|
+
const tagName = decodeURIComponent(tagMatch[1]);
|
|
128
|
+
const latestVersion = tagName.replace(/^v/, '');
|
|
129
|
+
const upToDate = compareVersions(currentVersion, latestVersion) >= 0;
|
|
130
|
+
|
|
131
|
+
return {
|
|
132
|
+
currentVersion,
|
|
133
|
+
latestVersion,
|
|
134
|
+
upToDate,
|
|
135
|
+
releaseUrl: parsedUrl.toString(),
|
|
136
|
+
asset: upToDate ? null : buildDmgAssetFromVersion(repo, tagName, arch),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function checkForUpdates({
|
|
141
|
+
repo,
|
|
142
|
+
currentVersion,
|
|
143
|
+
arch = process.arch,
|
|
144
|
+
fetchReleaseJson = fetchHttpsJson,
|
|
145
|
+
fetchLatestReleaseUrl: fetchLatestReleaseUrlOverride = fetchLatestReleaseUrl,
|
|
146
|
+
}) {
|
|
147
|
+
try {
|
|
148
|
+
const release = await fetchReleaseJson(`https://api.github.com/repos/${repo}/releases/latest`);
|
|
149
|
+
return getReleaseUpdateInfo(release, currentVersion, arch);
|
|
150
|
+
} catch (error) {
|
|
151
|
+
const releaseUrl = await fetchLatestReleaseUrlOverride(repo);
|
|
152
|
+
return getRedirectReleaseUpdateInfo(repo, releaseUrl, currentVersion, arch);
|
|
153
|
+
}
|
|
84
154
|
}
|
|
85
155
|
|
|
86
156
|
function safeDownloadName(name) {
|
|
@@ -143,6 +213,8 @@ module.exports = {
|
|
|
143
213
|
checkForUpdates,
|
|
144
214
|
compareVersions,
|
|
145
215
|
downloadUpdateAsset,
|
|
216
|
+
fetchLatestReleaseUrl,
|
|
217
|
+
getRedirectReleaseUpdateInfo,
|
|
146
218
|
getReleaseUpdateInfo,
|
|
147
219
|
selectMacDmgAsset,
|
|
148
220
|
};
|
package/package.json
CHANGED
package/dist/server/ccusage.d.ts
DELETED
|
@@ -1,7 +0,0 @@
|
|
|
1
|
-
export declare function runCcusage(args: string[], timeout?: number): Promise<string>;
|
|
2
|
-
export declare function ensureUsageToolsReady(): Promise<void>;
|
|
3
|
-
export declare function isClaudeCodeAvailable(): Promise<boolean>;
|
|
4
|
-
export declare function detectAvailableAgents(): Promise<{
|
|
5
|
-
claude: boolean;
|
|
6
|
-
codex: boolean;
|
|
7
|
-
}>;
|
package/dist/server/ccusage.js
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
import { execFile } from 'node:child_process';
|
|
2
|
-
import { promisify } from 'node:util';
|
|
3
|
-
import { isSessionsDirAccessible } from './codexParser.js';
|
|
4
|
-
const execFileAsync = promisify(execFile);
|
|
5
|
-
function withJsonFlag(args, asJson) {
|
|
6
|
-
if (!asJson || args.includes('--json')) {
|
|
7
|
-
return args;
|
|
8
|
-
}
|
|
9
|
-
return [...args, '--json'];
|
|
10
|
-
}
|
|
11
|
-
async function runCommand(spec, timeout) {
|
|
12
|
-
const { stdout } = await execFileAsync(spec.command, spec.args, {
|
|
13
|
-
timeout,
|
|
14
|
-
maxBuffer: 10 * 1024 * 1024,
|
|
15
|
-
});
|
|
16
|
-
return stdout;
|
|
17
|
-
}
|
|
18
|
-
function isMissingCommand(error) {
|
|
19
|
-
return typeof error === 'object'
|
|
20
|
-
&& error !== null
|
|
21
|
-
&& 'code' in error
|
|
22
|
-
&& error.code === 'ENOENT';
|
|
23
|
-
}
|
|
24
|
-
async function runCcusageCommand(args, timeout, asJson) {
|
|
25
|
-
const primary = {
|
|
26
|
-
command: 'ccusage',
|
|
27
|
-
args: withJsonFlag(args, asJson),
|
|
28
|
-
};
|
|
29
|
-
const fallback = {
|
|
30
|
-
command: 'npx',
|
|
31
|
-
args: ['--yes', 'ccusage@latest', ...withJsonFlag(args, asJson)],
|
|
32
|
-
};
|
|
33
|
-
try {
|
|
34
|
-
return await runCommand(primary, timeout);
|
|
35
|
-
}
|
|
36
|
-
catch (error) {
|
|
37
|
-
if (isMissingCommand(error)) {
|
|
38
|
-
return runCommand(fallback, timeout);
|
|
39
|
-
}
|
|
40
|
-
throw error;
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
export async function runCcusage(args, timeout = 30_000) {
|
|
44
|
-
return runCcusageCommand(args, timeout, true);
|
|
45
|
-
}
|
|
46
|
-
export async function ensureUsageToolsReady() {
|
|
47
|
-
// Claude Code: check ccusage CLI
|
|
48
|
-
await runCcusageCommand(['--version'], 120_000, false);
|
|
49
|
-
// Codex: check local sessions directory (instant, no npm subprocess)
|
|
50
|
-
if (!isSessionsDirAccessible()) {
|
|
51
|
-
throw new Error('Codex sessions directory not found at ~/.codex/sessions/');
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
export async function isClaudeCodeAvailable() {
|
|
55
|
-
try {
|
|
56
|
-
await runCcusageCommand(['--version'], 120_000, false);
|
|
57
|
-
return true;
|
|
58
|
-
}
|
|
59
|
-
catch {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
export async function detectAvailableAgents() {
|
|
64
|
-
const [claude, codex] = await Promise.all([
|
|
65
|
-
isClaudeCodeAvailable(),
|
|
66
|
-
Promise.resolve(isSessionsDirAccessible()),
|
|
67
|
-
]);
|
|
68
|
-
return { claude, codex };
|
|
69
|
-
}
|