tokalytics 2.0.0 → 2.0.7

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 CHANGED
@@ -41,6 +41,20 @@ npm run start # compila e em seguida executa ./tokalytics
41
41
 
42
42
  Na primeira execução o app sobe o **servidor HTTP na porta `3456`** e o ícone na barra de menus. Use **«Abrir Dashboard»** no menu ou acesse [http://127.0.0.1:3456](http://127.0.0.1:3456).
43
43
 
44
+ ### Instalação global pelo npm
45
+
46
+ ```bash
47
+ npm install -g tokalytics
48
+ ```
49
+
50
+ Em instalação **global**, o `postinstall` tenta **abrir o Tokalytics em segundo plano** (ícone na barra). Para instalar sem iniciar: `TOKALYTICS_NO_AUTOSTART=1 npm install -g tokalytics`.
51
+
52
+ Instalação a partir do repositório (sempre o `postinstall` da branch atual):
53
+
54
+ ```bash
55
+ npm install -g "github:kaicmurilo/Tokalytics"
56
+ ```
57
+
44
58
  ## Dashboard
45
59
 
46
60
  Interface web em abas:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tokalytics",
3
- "version": "2.0.0",
3
+ "version": "2.0.7",
4
4
  "description": "Veja para onde vão seus tokens do Claude Code, Gemini CLI, Cursor e Codex. Reescrito em Go como Tokalytics.",
5
5
  "bin": {
6
6
  "tokalytics": "bin/run.js"
@@ -13,7 +13,8 @@
13
13
  "postinstall": "node scripts/postinstall.js",
14
14
  "start": "go build -o tokalytics main.go && ./tokalytics",
15
15
  "dev": "go run main.go",
16
- "build": "go build -o tokalytics main.go"
16
+ "build": "go build -o tokalytics main.go",
17
+ "release:npm": "npm publish --access public"
17
18
  },
18
19
  "keywords": [
19
20
  "claude",
@@ -31,6 +32,6 @@
31
32
  "license": "MIT",
32
33
  "repository": {
33
34
  "type": "git",
34
- "url": "git+https://github.com/kaicmurilo/tokalytics.git"
35
+ "url": "git+https://github.com/kaicmurilo/Tokalytics.git"
35
36
  }
36
37
  }
@@ -4,9 +4,19 @@
4
4
  const https = require('https');
5
5
  const fs = require('fs');
6
6
  const path = require('path');
7
- const { execSync } = require('child_process');
7
+ const { spawn } = require('child_process');
8
8
 
9
- const REPO = 'kaicmurilo/tokalytics';
9
+ const pkgPath = path.join(__dirname, '..', 'package.json');
10
+ const pkgVersion = (() => {
11
+ try {
12
+ return JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version || '?';
13
+ } catch {
14
+ return '?';
15
+ }
16
+ })();
17
+
18
+ // Repositório público no GitHub (nome real: Tokalytics)
19
+ const REPO = 'kaicmurilo/Tokalytics';
10
20
  const BIN_DIR = path.join(__dirname, '..', 'bin');
11
21
  const BIN_PATH = path.join(BIN_DIR, process.platform === 'win32' ? 'tokalytics.exe' : 'tokalytics');
12
22
 
@@ -30,9 +40,17 @@ function getPlatformBinary() {
30
40
  return name;
31
41
  }
32
42
 
33
- function fetchJson(url) {
43
+ function fetchJson(url, extraHeaders = {}) {
34
44
  return new Promise((resolve, reject) => {
35
- const options = { headers: { 'User-Agent': 'tokalytics-installer' } };
45
+ const headers = {
46
+ 'User-Agent': 'tokalytics-installer',
47
+ Accept: 'application/vnd.github+json',
48
+ ...extraHeaders,
49
+ };
50
+ if (process.env.GITHUB_TOKEN) {
51
+ headers.Authorization = `Bearer ${process.env.GITHUB_TOKEN}`;
52
+ }
53
+ const options = { headers };
36
54
  https.get(url, options, (res) => {
37
55
  if (res.statusCode === 302 || res.statusCode === 301) {
38
56
  return fetchJson(res.headers.location).then(resolve).catch(reject);
@@ -40,8 +58,24 @@ function fetchJson(url) {
40
58
  let data = '';
41
59
  res.on('data', (chunk) => (data += chunk));
42
60
  res.on('end', () => {
43
- try { resolve(JSON.parse(data)); }
44
- catch (e) { reject(new Error('Resposta inválida da API do GitHub')); }
61
+ if (res.statusCode && res.statusCode >= 400) {
62
+ try {
63
+ const j = JSON.parse(data);
64
+ if (j && j.message) {
65
+ return reject(
66
+ new Error(`GitHub HTTP ${res.statusCode}: ${j.message}`)
67
+ );
68
+ }
69
+ } catch (_) {
70
+ /* fallthrough */
71
+ }
72
+ return reject(new Error(`GitHub HTTP ${res.statusCode}`));
73
+ }
74
+ try {
75
+ resolve(JSON.parse(data));
76
+ } catch {
77
+ reject(new Error('Resposta inválida da API do GitHub'));
78
+ }
45
79
  });
46
80
  }).on('error', reject);
47
81
  });
@@ -70,14 +104,60 @@ function downloadFile(url, dest) {
70
104
  });
71
105
  }
72
106
 
107
+ function assertReleasePayload(release) {
108
+ if (!release || typeof release !== 'object') {
109
+ throw new Error('Resposta inválida da API do GitHub (corpo vazio).');
110
+ }
111
+ if (release.message && !release.tag_name) {
112
+ let hint = '';
113
+ if (/rate limit/i.test(release.message) && !process.env.GITHUB_TOKEN) {
114
+ hint = ' Defina GITHUB_TOKEN no ambiente para aumentar o limite.';
115
+ } else if (/not found/i.test(release.message)) {
116
+ hint =
117
+ ` Confirme que https://github.com/${REPO} existe, é público e tem pelo menos uma release com binários anexados.`;
118
+ }
119
+ throw new Error(`GitHub: ${release.message}${hint}`);
120
+ }
121
+ if (!release.tag_name) {
122
+ throw new Error('Release sem tag_name; verifique se existe release no repositório.');
123
+ }
124
+ if (!Array.isArray(release.assets)) {
125
+ throw new Error(
126
+ 'Resposta da API sem lista de assets. Possível rate limit ou repositório/release inexistente.'
127
+ );
128
+ }
129
+ }
130
+
131
+ /** Só em `npm install -g` (npm_config_global). Ignora CI e TOKALYTICS_NO_AUTOSTART=1. */
132
+ function tryLaunchAfterGlobalInstall() {
133
+ if (process.env.CI === 'true') return false;
134
+ if (process.env.TOKALYTICS_NO_AUTOSTART === '1') return false;
135
+ const g = process.env.npm_config_global;
136
+ if (g !== 'true' && g !== '1') return false;
137
+ if (!fs.existsSync(BIN_PATH)) return false;
138
+ try {
139
+ const child = spawn(BIN_PATH, [], {
140
+ detached: true,
141
+ stdio: 'ignore',
142
+ windowsHide: true,
143
+ });
144
+ child.unref();
145
+ return true;
146
+ } catch {
147
+ return false;
148
+ }
149
+ }
150
+
73
151
  async function install() {
74
- console.log('Tokalytics: buscando última versão...');
152
+ console.log(`Tokalytics installer v${pkgVersion}: buscando última versão...`);
75
153
 
76
154
  const release = await fetchJson(`https://api.github.com/repos/${REPO}/releases/latest`);
155
+ assertReleasePayload(release);
77
156
  const version = release.tag_name;
78
157
  const binaryName = getPlatformBinary();
79
158
 
80
- const asset = release.assets.find((a) => a.name === binaryName);
159
+ const assets = Array.isArray(release.assets) ? release.assets : [];
160
+ const asset = assets.find((a) => a && a.name === binaryName);
81
161
  if (!asset) {
82
162
  throw new Error(`Binário "${binaryName}" não encontrado na release ${version}`);
83
163
  }
@@ -89,7 +169,13 @@ async function install() {
89
169
  fs.chmodSync(BIN_PATH, 0o755);
90
170
 
91
171
  console.log(`Tokalytics ${version} instalado com sucesso!`);
92
- console.log('Execute: tokalytics');
172
+ if (tryLaunchAfterGlobalInstall()) {
173
+ console.log(
174
+ 'Tokalytics iniciado em segundo plano (ícone na barra de menus / bandeja).'
175
+ );
176
+ } else {
177
+ console.log('Execute: tokalytics');
178
+ }
93
179
  }
94
180
 
95
181
  install().catch((err) => {