token-usage-insights 0.9.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/npm/cli.cjs ADDED
@@ -0,0 +1,30 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { spawnSync } = require('node:child_process');
5
+ const { join } = require('node:path');
6
+ const { installBinary, isReleaseRoot } = require('./install.cjs');
7
+
8
+ const BINARY_NAME = 'token-usage-insights';
9
+ const executableName = process.platform === 'win32' ? `${BINARY_NAME}.exe` : BINARY_NAME;
10
+ const installDirectory = join(__dirname, `${BINARY_NAME}-bin`);
11
+ const executable = join(__dirname, `${BINARY_NAME}-bin`, executableName);
12
+
13
+ async function main() {
14
+ if (!isReleaseRoot(installDirectory, executableName)) await installBinary();
15
+
16
+ if (!isReleaseRoot(installDirectory, executableName)) {
17
+ throw new Error(
18
+ `${BINARY_NAME} 原生執行檔尚未建置;請先執行 cargo build --release。`,
19
+ );
20
+ }
21
+
22
+ const result = spawnSync(executable, process.argv.slice(2), { stdio: 'inherit' });
23
+ if (result.error) throw result.error;
24
+ process.exitCode = result.status ?? 1;
25
+ }
26
+
27
+ main().catch((error) => {
28
+ console.error(`${BINARY_NAME} 啟動失敗:${error.message}`);
29
+ process.exitCode = 1;
30
+ });
@@ -0,0 +1,232 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { createHash } = require('node:crypto');
5
+ const { spawnSync } = require('node:child_process');
6
+ const {
7
+ chmodSync,
8
+ copyFileSync,
9
+ cpSync,
10
+ createWriteStream,
11
+ existsSync,
12
+ mkdirSync,
13
+ mkdtempSync,
14
+ readFileSync,
15
+ readdirSync,
16
+ rmSync,
17
+ } = require('node:fs');
18
+ const { get } = require('node:https');
19
+ const { tmpdir } = require('node:os');
20
+ const { basename, join } = require('node:path');
21
+ const { pipeline } = require('node:stream/promises');
22
+ const { URL } = require('node:url');
23
+
24
+ const PACKAGE_ROOT = join(__dirname, '..');
25
+ const BINARY_NAME = 'token-usage-insights';
26
+ const GITHUB_OWNER = 'doggy8088';
27
+ const GITHUB_REPO = 'TokenUsageInsights';
28
+ const INSTALL_DIR = join(__dirname, `${BINARY_NAME}-bin`);
29
+ const EXECUTABLE_NAME = process.platform === 'win32' ? `${BINARY_NAME}.exe` : BINARY_NAME;
30
+ const TARGETS = Object.freeze({
31
+ 'darwin-arm64': 'aarch64-apple-darwin',
32
+ 'darwin-x64': 'x86_64-apple-darwin',
33
+ 'linux-x64': 'x86_64-unknown-linux-gnu',
34
+ 'win32-x64': 'x86_64-pc-windows-msvc',
35
+ });
36
+
37
+ function platformKey(platform = process.platform, arch = process.arch) {
38
+ return `${platform}-${arch}`;
39
+ }
40
+
41
+ function cargoTarget(platform = process.platform, arch = process.arch) {
42
+ const target = TARGETS[platformKey(platform, arch)];
43
+ if (!target) {
44
+ throw new Error(
45
+ `不支援的平台:${platform}/${arch}。` +
46
+ '目前支援 Windows x64、Linux x64、Intel Mac 與 Apple Silicon Mac。',
47
+ );
48
+ }
49
+ return target;
50
+ }
51
+
52
+ function packageVersion() {
53
+ return require(join(PACKAGE_ROOT, 'package.json')).version;
54
+ }
55
+
56
+ function artifactName(target, version = packageVersion()) {
57
+ const extension = target.includes('windows') ? 'zip' : 'tar.gz';
58
+ return `${BINARY_NAME}-v${version}-${target}.${extension}`;
59
+ }
60
+
61
+ function releaseBaseUrl(version = packageVersion()) {
62
+ return `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/releases/download/v${version}`;
63
+ }
64
+
65
+ function sha256(filePath) {
66
+ return createHash('sha256').update(readFileSync(filePath)).digest('hex');
67
+ }
68
+
69
+ function checksumForArtifact(checksumText, artifact) {
70
+ for (const line of checksumText.split(/\r?\n/)) {
71
+ const match = line.match(/^([a-fA-F0-9]{64})\s+\*?(.+?)\s*$/);
72
+ if (match && basename(match[2]) === artifact) return match[1].toLowerCase();
73
+ }
74
+ throw new Error(`SHA256SUMS 找不到 ${artifact}`);
75
+ }
76
+
77
+ function verifyChecksum(filePath, checksumText, artifact = basename(filePath)) {
78
+ const expected = checksumForArtifact(checksumText, artifact);
79
+ const actual = sha256(filePath);
80
+ if (actual !== expected) {
81
+ throw new Error(`${artifact} 校驗失敗:預期 ${expected},實際 ${actual}`);
82
+ }
83
+ }
84
+
85
+ function download(url, destination, redirectsRemaining = 5) {
86
+ return new Promise((resolve, reject) => {
87
+ const request = get(url, { headers: { 'User-Agent': `${BINARY_NAME}-npm-installer` } }, (response) => {
88
+ const { statusCode, headers } = response;
89
+ if (statusCode >= 300 && statusCode < 400 && headers.location && redirectsRemaining > 0) {
90
+ response.resume();
91
+ const nextUrl = new URL(headers.location, url).toString();
92
+ download(nextUrl, destination, redirectsRemaining - 1).then(resolve, reject);
93
+ return;
94
+ }
95
+ if (statusCode !== 200) {
96
+ response.resume();
97
+ reject(new Error(`下載失敗 HTTP ${statusCode}:${url}`));
98
+ return;
99
+ }
100
+ pipeline(response, createWriteStream(destination, { mode: 0o600 })).then(resolve, reject);
101
+ });
102
+ request.setTimeout(30_000, () => request.destroy(new Error(`下載逾時:${url}`)));
103
+ request.on('error', reject);
104
+ });
105
+ }
106
+
107
+ function run(command, args, options = {}) {
108
+ const result = spawnSync(command, args, { stdio: 'inherit', ...options });
109
+ if (result.error) throw result.error;
110
+ if (result.status !== 0) throw new Error(`命令執行失敗:${command}`);
111
+ }
112
+
113
+ function extract(archive, destination) {
114
+ mkdirSync(destination, { recursive: true });
115
+ if (archive.endsWith('.zip') && process.platform === 'win32') {
116
+ run(
117
+ 'powershell.exe',
118
+ [
119
+ '-NoProfile',
120
+ '-NonInteractive',
121
+ '-Command',
122
+ 'Expand-Archive -LiteralPath $env:TUI_ARCHIVE -DestinationPath $env:TUI_DESTINATION -Force',
123
+ ],
124
+ { env: { ...process.env, TUI_ARCHIVE: archive, TUI_DESTINATION: destination } },
125
+ );
126
+ return;
127
+ }
128
+ run('tar', [archive.endsWith('.tar.gz') ? '-xzf' : '-xf', archive, '-C', destination]);
129
+ }
130
+
131
+ function isReleaseRoot(directory, executableName = EXECUTABLE_NAME) {
132
+ return (
133
+ existsSync(join(directory, executableName)) &&
134
+ existsSync(join(directory, 'static')) &&
135
+ existsSync(join(directory, 'pricing.csv'))
136
+ );
137
+ }
138
+
139
+ function findReleaseRoot(directory, executableName = EXECUTABLE_NAME) {
140
+ if (isReleaseRoot(directory, executableName)) return directory;
141
+ for (const entry of readdirSync(directory, { withFileTypes: true })) {
142
+ if (!entry.isDirectory()) continue;
143
+ const candidate = join(directory, entry.name);
144
+ if (isReleaseRoot(candidate, executableName)) return candidate;
145
+ }
146
+ throw new Error('GitHub Release 壓縮包缺少執行檔、static 或 pricing.csv');
147
+ }
148
+
149
+ function copyReleaseContents(source, destination, executableName = EXECUTABLE_NAME) {
150
+ rmSync(destination, { recursive: true, force: true });
151
+ mkdirSync(destination, { recursive: true });
152
+ for (const entry of readdirSync(source, { withFileTypes: true })) {
153
+ cpSync(join(source, entry.name), join(destination, entry.name), {
154
+ recursive: entry.isDirectory(),
155
+ force: true,
156
+ });
157
+ }
158
+ chmodSync(join(destination, executableName), 0o755);
159
+ }
160
+
161
+ function installFromLocalBuild() {
162
+ const localBinary = join(PACKAGE_ROOT, 'target', 'release', EXECUTABLE_NAME);
163
+ if (!existsSync(localBinary)) return false;
164
+
165
+ rmSync(INSTALL_DIR, { recursive: true, force: true });
166
+ mkdirSync(INSTALL_DIR, { recursive: true });
167
+ copyFileSync(localBinary, join(INSTALL_DIR, EXECUTABLE_NAME));
168
+ chmodSync(join(INSTALL_DIR, EXECUTABLE_NAME), 0o755);
169
+ for (const item of ['static', 'shell', 'scripts', 'pricing.csv', 'README.md', 'LICENSE']) {
170
+ const source = join(PACKAGE_ROOT, item);
171
+ if (existsSync(source)) {
172
+ cpSync(source, join(INSTALL_DIR, item), { recursive: true, force: true });
173
+ }
174
+ }
175
+ console.log(`已安裝本機建置的 ${BINARY_NAME}`);
176
+ return true;
177
+ }
178
+
179
+ async function installFromRelease() {
180
+ const version = packageVersion();
181
+ const target = cargoTarget();
182
+ const artifact = artifactName(target, version);
183
+ const baseUrl = releaseBaseUrl(version);
184
+ const temporaryDirectory = mkdtempSync(join(tmpdir(), `${BINARY_NAME}-`));
185
+ const archive = join(temporaryDirectory, artifact);
186
+ const checksums = join(temporaryDirectory, 'SHA256SUMS');
187
+ const extracted = join(temporaryDirectory, 'extracted');
188
+
189
+ try {
190
+ console.log(`正在下載 ${BINARY_NAME} v${version}:${target}`);
191
+ await download(`${baseUrl}/${artifact}`, archive);
192
+ await download(`${baseUrl}/SHA256SUMS`, checksums);
193
+ verifyChecksum(archive, readFileSync(checksums, 'utf8'), artifact);
194
+ extract(archive, extracted);
195
+ copyReleaseContents(findReleaseRoot(extracted), INSTALL_DIR);
196
+ console.log(`已安裝 ${BINARY_NAME} v${version}`);
197
+ } finally {
198
+ rmSync(temporaryDirectory, { recursive: true, force: true });
199
+ }
200
+ }
201
+
202
+ async function installBinary() {
203
+ if (installFromLocalBuild()) return;
204
+ if (existsSync(join(PACKAGE_ROOT, 'Cargo.toml'))) {
205
+ console.log('偵測到原始碼工作目錄;略過 npm 原生執行檔下載。');
206
+ return;
207
+ }
208
+ await installFromRelease();
209
+ }
210
+
211
+ if (require.main === module) {
212
+ installBinary().catch((error) => {
213
+ rmSync(INSTALL_DIR, { recursive: true, force: true });
214
+ console.error(`安裝 ${BINARY_NAME} 失敗:${error.message}`);
215
+ process.exit(1);
216
+ });
217
+ }
218
+
219
+ module.exports = {
220
+ TARGETS,
221
+ artifactName,
222
+ cargoTarget,
223
+ checksumForArtifact,
224
+ copyReleaseContents,
225
+ findReleaseRoot,
226
+ installBinary,
227
+ isReleaseRoot,
228
+ platformKey,
229
+ releaseBaseUrl,
230
+ sha256,
231
+ verifyChecksum,
232
+ };
@@ -0,0 +1,133 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { spawnSync } = require('node:child_process');
5
+ const { readFileSync } = require('node:fs');
6
+ const { request } = require('node:https');
7
+ const { join } = require('node:path');
8
+ const { URL } = require('node:url');
9
+ const { artifactName, releaseBaseUrl, TARGETS } = require('./install.cjs');
10
+
11
+ const PACKAGE_ROOT = join(__dirname, '..');
12
+ const MAX_REDIRECTS = 5;
13
+
14
+ function packageVersion() {
15
+ return require('../package.json').version;
16
+ }
17
+
18
+ function cargoVersion() {
19
+ const cargo = readFileSync(join(PACKAGE_ROOT, 'Cargo.toml'), 'utf8');
20
+ const packageSection = cargo.match(/^\[package\]\s*([\s\S]*?)(?=^\[|\Z)/m)?.[1] ?? '';
21
+ const version = packageSection.match(/^version\s*=\s*"([^"]+)"/m)?.[1];
22
+ if (!version) throw new Error('無法讀取 Cargo.toml 的 package.version');
23
+ return version;
24
+ }
25
+
26
+ function expectedReleaseUrls(version = packageVersion()) {
27
+ const base = releaseBaseUrl(version);
28
+ return [
29
+ ...Object.values(TARGETS).map((target) => `${base}/${artifactName(target, version)}`),
30
+ `${base}/SHA256SUMS`,
31
+ ];
32
+ }
33
+
34
+ function checkUrl(url, redirectsRemaining = MAX_REDIRECTS) {
35
+ return new Promise((resolve) => {
36
+ const req = request(
37
+ url,
38
+ { method: 'HEAD', headers: { 'User-Agent': 'token-usage-insights-publish-check' } },
39
+ (response) => {
40
+ const { statusCode, headers } = response;
41
+ response.resume();
42
+ if (statusCode >= 300 && statusCode < 400 && headers.location && redirectsRemaining > 0) {
43
+ const nextUrl = new URL(headers.location, url).toString();
44
+ checkUrl(nextUrl, redirectsRemaining - 1).then((result) => resolve({ ...result, url }));
45
+ return;
46
+ }
47
+ resolve({ url, ok: statusCode >= 200 && statusCode < 300, statusCode });
48
+ },
49
+ );
50
+ req.setTimeout(30_000, () => req.destroy(new Error(`request timed out: ${url}`)));
51
+ req.on('error', (error) => resolve({ url, ok: false, errorMessage: error.message }));
52
+ req.end();
53
+ });
54
+ }
55
+
56
+ function retryCountFromEnv() {
57
+ return Number.parseInt(process.env.TOKEN_USAGE_INSIGHTS_RELEASE_ASSET_RETRIES ?? '1', 10);
58
+ }
59
+
60
+ function retryDelayMsFromEnv() {
61
+ return Number.parseInt(
62
+ process.env.TOKEN_USAGE_INSIGHTS_RELEASE_ASSET_RETRY_DELAY_MS ?? '1000',
63
+ 10,
64
+ );
65
+ }
66
+
67
+ function sleep(milliseconds) {
68
+ return new Promise((resolve) => setTimeout(resolve, milliseconds));
69
+ }
70
+
71
+ function assertVersionAlignment(version = packageVersion()) {
72
+ const rustVersion = cargoVersion();
73
+ if (version !== rustVersion) {
74
+ throw new Error(`版本不一致:package.json=${version},Cargo.toml=${rustVersion}`);
75
+ }
76
+ }
77
+
78
+ function assertExactReleaseTag(version = packageVersion()) {
79
+ const result = spawnSync('git', ['describe', '--tags', '--exact-match', 'HEAD'], {
80
+ cwd: PACKAGE_ROOT,
81
+ encoding: 'utf8',
82
+ });
83
+ const actual = result.status === 0 ? result.stdout.trim() : '';
84
+ if (actual !== `v${version}`) {
85
+ throw new Error(`npm 僅能從對應 Release commit 發布;預期目前 tag 為 v${version}`);
86
+ }
87
+ }
88
+
89
+ async function verifyReleaseAssets({
90
+ version = packageVersion(),
91
+ check = checkUrl,
92
+ retries = retryCountFromEnv(),
93
+ retryDelayMs = retryDelayMsFromEnv(),
94
+ } = {}) {
95
+ const urls = expectedReleaseUrls(version);
96
+ let failures = [];
97
+ for (let attempt = 1; attempt <= retries; attempt += 1) {
98
+ const results = await Promise.all(urls.map((url) => check(url)));
99
+ failures = results.filter((result) => !result.ok);
100
+ if (failures.length === 0) return urls;
101
+ if (attempt < retries) await sleep(retryDelayMs);
102
+ }
103
+ const details = failures.map((failure) => {
104
+ const reason = failure.statusCode ? `HTTP ${failure.statusCode}` : failure.errorMessage;
105
+ return `- ${failure.url}:${reason}`;
106
+ });
107
+ throw new Error(
108
+ [`v${version} 的 GitHub Release 資產尚未備妥:`, ...details].join('\n'),
109
+ );
110
+ }
111
+
112
+ async function main() {
113
+ const version = packageVersion();
114
+ assertVersionAlignment(version);
115
+ assertExactReleaseTag(version);
116
+ const urls = await verifyReleaseAssets({ version });
117
+ console.log(`已驗證 v${version} 的 ${urls.length} 個 GitHub Release 下載項目。`);
118
+ }
119
+
120
+ if (require.main === module) {
121
+ main().catch((error) => {
122
+ console.error(error.message);
123
+ process.exit(1);
124
+ });
125
+ }
126
+
127
+ module.exports = {
128
+ assertVersionAlignment,
129
+ cargoVersion,
130
+ checkUrl,
131
+ expectedReleaseUrls,
132
+ verifyReleaseAssets,
133
+ };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "token-usage-insights",
3
+ "version": "0.9.0",
4
+ "description": "Local-first AI coding agent token usage dashboard and CLI",
5
+ "bin": {
6
+ "token-usage-insights": "npm/cli.cjs"
7
+ },
8
+ "scripts": {
9
+ "test": "node --test tests/npm-package.test.cjs",
10
+ "check:package": "npm test && npm pack --dry-run --ignore-scripts && node npm/prepublish-check.cjs",
11
+ "prepublishOnly": "npm run check:package"
12
+ },
13
+ "files": [
14
+ "npm/cli.cjs",
15
+ "npm/install.cjs",
16
+ "npm/prepublish-check.cjs",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "engines": {
21
+ "node": ">=18.18"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/doggy8088/TokenUsageInsights.git"
26
+ },
27
+ "homepage": "https://token.gh.miniasp.com/",
28
+ "bugs": {
29
+ "url": "https://github.com/doggy8088/TokenUsageInsights/issues"
30
+ },
31
+ "keywords": [
32
+ "ai",
33
+ "coding-agent",
34
+ "token-usage",
35
+ "codex",
36
+ "claude-code",
37
+ "copilot",
38
+ "rust",
39
+ "cli"
40
+ ],
41
+ "author": "Will and contributors",
42
+ "license": "MIT",
43
+ "publishConfig": {
44
+ "access": "public",
45
+ "registry": "https://registry.npmjs.org/"
46
+ }
47
+ }