@ugurcandede/cc-cost 0.1.0 → 0.1.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 CHANGED
@@ -3,7 +3,7 @@
3
3
  <h1>cc-cost</h1>
4
4
  <p>What your Claude Code usage would cost at API list prices, totalled across all your machines (Windows, Mac, Linux), with an archive that outlives Claude Code's transcript cleanup and a dashboard you open with a double-click.</p>
5
5
  <br>
6
- <a href="https://github.com/ugurcandede/cc-cost" target="_blank"><img src="https://img.shields.io/badge/version-0.1.0-blue?style=flat-square" alt="Version 0.1.0"></a>
6
+ <a href="https://www.npmjs.com/package/@ugurcandede/cc-cost" target="_blank"><img src="https://img.shields.io/npm/v/@ugurcandede/cc-cost?style=flat-square&label=version" alt="npm version"></a>
7
7
  <img src="https://img.shields.io/badge/Node.js-22%2B%20·%20zero%20dependencies-339933?style=flat-square&logo=node.js&logoColor=white" alt="Node.js 22+, zero dependencies">
8
8
  <a href="LICENSE"><img src="https://img.shields.io/badge/license-MIT-blue?style=flat-square" alt="License: MIT"></a>
9
9
  <br>
@@ -32,7 +32,7 @@ npm i -g @ugurcandede/cc-cost # or, with Yarn 1: yarn global add @ugurc
32
32
  cc-cost setup
33
33
 
34
34
  cc-cost # sync this machine, print a summary
35
- cc-cost report --open # the dashboard
35
+ cc-cost report # open the dashboard
36
36
  ```
37
37
 
38
38
  Run `setup` on every machine and point them at the same folder. Each one adds its numbers; every one
@@ -59,6 +59,7 @@ cc-cost insights # context size, cache efficiency, what drives the c
59
59
  cc-cost plan # API equivalent vs Pro / Max 5x / Max 20x, rate-limit hits
60
60
  cc-cost blocks # usage in Claude's 5-hour windows
61
61
  cc-cost status # settings, machines, schedule, hook
62
+ cc-cost update # update to the latest version
62
63
  ```
63
64
 
64
65
  ```
package/dist/cli.js CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { spawn } from 'node:child_process';
2
+ import { spawn, spawnSync } from 'node:child_process';
3
3
  import fs from 'node:fs';
4
4
  import { createRequire } from 'node:module';
5
5
  import path from 'node:path';
@@ -16,6 +16,7 @@ import { blocksReport, dimensionJson, dimensionReport, DIMENSIONS, insights, lim
16
16
  import { localClock } from "./scan.js";
17
17
  import { claudeSettingsFile, currentRunner, dataFolderIn, hookInstalled, installHook, installSchedule, isEphemeral, removeHook, removeSchedule, SCHEDULE_TIME, scheduleInstalled, syncFolders, } from "./setup.js";
18
18
  import { loadAll } from "./snapshot.js";
19
+ import { installMethod, isNewer, latestVersion, updateCommand } from "./update.js";
19
20
  import { dashboardPath, sync } from "./sync.js";
20
21
  // Name, version and project page come from package.json only
21
22
  const pkg = createRequire(import.meta.url)('../package.json');
@@ -44,7 +45,7 @@ const { values: opt, positionals } = parseArgs({
44
45
  'no-sync': { type: 'boolean' },
45
46
  offline: { type: 'boolean' },
46
47
  quiet: { type: 'boolean' },
47
- open: { type: 'boolean' },
48
+ location: { type: 'boolean' },
48
49
  refresh: { type: 'boolean' },
49
50
  'sync-dir': { type: 'string' },
50
51
  yes: { type: 'boolean', short: 'y' },
@@ -109,9 +110,11 @@ function openFile(file) {
109
110
  const [cmd, args] = process.platform === 'win32' ? ['cmd', ['/c', 'start', '""', file]]
110
111
  : process.platform === 'darwin' ? ['open', [file]]
111
112
  : ['xdg-open', [file]];
112
- spawn(cmd, args, { detached: true, stdio: 'ignore' }).unref();
113
+ // No browser opener (a server, SSH): the path printed alongside is enough, so ignore the failure.
114
+ spawn(cmd, args, { detached: true, stdio: 'ignore' }).on('error', () => { }).unref();
113
115
  }
114
116
  const pricesCache = () => path.join(configDir(), 'pricing-cache.json');
117
+ const updateCache = () => path.join(configDir(), 'update-check.json');
115
118
  async function data(verbose) {
116
119
  if (!opt['no-sync']) {
117
120
  const r = await sync(settings);
@@ -142,11 +145,42 @@ function dashboard(d) {
142
145
  site: pkg.author.url,
143
146
  }, dashboardPath(settings));
144
147
  }
145
- function footer(d) {
148
+ async function footer(d) {
146
149
  if (d.prices.note)
147
150
  out(fill(L().pricesFallback, { reason: d.prices.note, source: d.prices.table.source, date: d.prices.table.date.slice(0, 10) }));
148
151
  if (d.unknown.size)
149
152
  out(fill(L().unknownModels, { list: [...d.unknown].join(', ') }));
153
+ // At most one registry check a day, and never in machine-readable or offline output
154
+ if (opt.json || opt.csv || opt.offline || opt.quiet)
155
+ return;
156
+ const latest = await latestVersion(pkg.name, updateCache());
157
+ if (latest && isNewer(latest, pkg.version))
158
+ out('\n' + paint('yellow', fill(L().update.available, { latest, current: pkg.version })));
159
+ }
160
+ async function update() {
161
+ const U = L().update;
162
+ const script = currentRunner().script;
163
+ const method = installMethod(script);
164
+ if (method === 'ephemeral')
165
+ return out(fill(U.ephemeral, { pkg: pkg.name }));
166
+ if (method === 'unknown')
167
+ return out(fill(U.manual, { path: script }));
168
+ let latest;
169
+ try {
170
+ latest = await latestVersion(pkg.name, updateCache(), { force: true });
171
+ }
172
+ catch {
173
+ fail(U.checkFailed);
174
+ }
175
+ if (!latest || !isNewer(latest, pkg.version))
176
+ return out(fill(U.upToDate, { current: pkg.version }));
177
+ const command = updateCommand[method](pkg.name).join(' ');
178
+ out(fill(U.running, { latest, command }));
179
+ // a fixed command line, no user input; the shell finds npm.cmd / yarn.cmd on Windows
180
+ const res = spawnSync(command, { stdio: 'inherit', shell: true });
181
+ if (res.status !== 0)
182
+ fail(fill(U.failed, { code: String(res.status), command }));
183
+ out(fill(U.done, { latest }));
150
184
  }
151
185
  // Print a table report as a table, CSV or JSON
152
186
  function emit(report, json, empty) {
@@ -307,13 +341,13 @@ async function main() {
307
341
  out(opt.json ? JSON.stringify(r.json, null, 2) : rows.length ? r.text : L().noData);
308
342
  }
309
343
  if (!opt.json && !opt.csv)
310
- footer(d);
344
+ await footer(d);
311
345
  return;
312
346
  }
313
347
  if (cmd === 'report') {
314
348
  const file = dashboard(await data(false));
315
349
  out(fill(L().dashboard, { file }));
316
- if (opt.open)
350
+ if (!opt.location)
317
351
  openFile(file);
318
352
  return;
319
353
  }
@@ -333,6 +367,8 @@ async function main() {
333
367
  return setup();
334
368
  if (cmd === 'status')
335
369
  return status();
370
+ if (cmd === 'update')
371
+ return update();
336
372
  if (cmd === 'config') {
337
373
  if (rest[0] === 'set') {
338
374
  const [key, value = ''] = rest.slice(1);
package/dist/i18n.js CHANGED
@@ -14,10 +14,11 @@ Commands:
14
14
  blocks Usage in 5-hour windows
15
15
  insights Where the money goes and what drives it
16
16
  plan API equivalent vs subscription prices, rate-limit hits
17
- report Write the HTML dashboard (--open to open it)
17
+ report Open the HTML dashboard (--location prints its path instead)
18
18
  pricing Prices in use (--refresh to fetch them again)
19
19
  setup Pick the shared folder, schedule daily runs, add the Claude Code hook
20
20
  status Settings, machines, last sync, scheduler and hook
21
+ update Update cc-cost to the latest version
21
22
  config Show settings; "config set <key> <value>" changes one
22
23
 
23
24
  Filters:
@@ -126,6 +127,16 @@ Docs and issues: {url}`,
126
127
  blocks: {
127
128
  active: 'active, {left} left',
128
129
  },
130
+ update: {
131
+ available: 'cc-cost {latest} is available (you have {current}). Run: cc-cost update',
132
+ upToDate: 'cc-cost {current} is up to date.',
133
+ running: 'Updating to {latest}: {command}',
134
+ done: 'Updated to cc-cost {latest}. The schedule and hook keep working; no need to run setup again.',
135
+ failed: 'The update failed (exit code {code}). Run it yourself: {command}',
136
+ checkFailed: 'Could not reach the npm registry to check for a newer version.',
137
+ ephemeral: 'cc-cost is running through npx or dlx, which fetch it each time. For the newest version: npx {pkg}@latest',
138
+ manual: 'cc-cost runs from {path}, not from a package-manager install. Update it the way you installed it (a git checkout: git pull, then yarn build).',
139
+ },
129
140
  setup: {
130
141
  title: 'cc-cost {version} setup',
131
142
  found: 'Shared folders found: {list}',
@@ -216,10 +227,11 @@ Komutlar:
216
227
  blocks 5 saatlik pencerelerde kullanım
217
228
  insights Para nereye gidiyor, neyden kaynaklanıyor
218
229
  plan API karşılığı ve abonelik fiyatları, limit aşımları
219
- report HTML dashboard'u yaz (--open ile )
230
+ report HTML dashboard'u (--location sadece yolunu basar)
220
231
  pricing Kullanılan fiyatlar (--refresh ile yeniden çek)
221
232
  setup Paylaşılan klasörü seç, günlük çalışmayı zamanla, Claude Code hook'unu ekle
222
233
  status Ayarlar, makineler, son senkron, zamanlayıcı ve hook
234
+ update cc-cost'u en son sürüme güncelle
223
235
  config Ayarları göster; "config set <anahtar> <değer>" ile değiştir
224
236
 
225
237
  Filtreler:
@@ -328,6 +340,16 @@ Dokümantasyon ve hata bildirimi: {url}`,
328
340
  blocks: {
329
341
  active: 'aktif, {left} kaldı',
330
342
  },
343
+ update: {
344
+ available: 'cc-cost {latest} yayında (sizdeki {current}). Güncellemek için: cc-cost update',
345
+ upToDate: 'cc-cost {current} güncel.',
346
+ running: '{latest} sürümüne güncelleniyor: {command}',
347
+ done: 'cc-cost {latest} sürümüne güncellendi. Zamanlama ve hook çalışmaya devam eder; setup\'ı yeniden çalıştırmaya gerek yok.',
348
+ failed: 'Güncelleme başarısız oldu (çıkış kodu {code}). Kendiniz çalıştırın: {command}',
349
+ checkFailed: 'Yeni sürümü kontrol etmek için npm registry\'ye ulaşılamadı.',
350
+ ephemeral: 'cc-cost npx ya da dlx ile çalışıyor; bunlar her seferinde paketi indirir. En yeni sürüm için: npx {pkg}@latest',
351
+ manual: 'cc-cost {path} konumundan çalışıyor, bir paket yöneticisi kurulumu değil. Nasıl kurduysanız öyle güncelleyin (git kopyası: git pull, ardından yarn build).',
352
+ },
331
353
  setup: {
332
354
  title: 'cc-cost {version} kurulumu',
333
355
  found: 'Bulunan paylaşılan klasörler: {list}',
package/dist/update.js ADDED
@@ -0,0 +1,61 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ const MAX_AGE_MS = 24 * 60 * 60 * 1000;
4
+ // x.y.z comparison; a pre-release (x.y.z-beta) never counts as newer than its release
5
+ export function isNewer(candidate, current) {
6
+ const parse = (v) => {
7
+ const [core, pre] = v.split('-');
8
+ return { n: (core ?? '').split('.').map((x) => Number(x) || 0), pre: !!pre };
9
+ };
10
+ const a = parse(candidate), b = parse(current);
11
+ for (let i = 0; i < 3; i++)
12
+ if ((a.n[i] ?? 0) !== (b.n[i] ?? 0))
13
+ return (a.n[i] ?? 0) > (b.n[i] ?? 0);
14
+ return !a.pre && b.pre;
15
+ }
16
+ // Latest published version, asked of the registry at most once a day unless `force`.
17
+ export async function latestVersion(name, cacheFile, opts = {}) {
18
+ let cached;
19
+ try {
20
+ cached = JSON.parse(fs.readFileSync(cacheFile, 'utf8'));
21
+ }
22
+ catch {
23
+ // first check
24
+ }
25
+ const fresh = cached && Date.now() - Date.parse(cached.checked) < MAX_AGE_MS;
26
+ if (opts.offline || (fresh && !opts.force))
27
+ return cached?.latest;
28
+ try {
29
+ const res = await fetch(`https://registry.npmjs.org/${name.replace('/', '%2f')}/latest`, { signal: AbortSignal.timeout(3000) });
30
+ if (!res.ok)
31
+ throw new Error(`HTTP ${res.status}`);
32
+ const latest = String((await res.json()).version ?? '');
33
+ if (!latest)
34
+ throw new Error('no version');
35
+ fs.mkdirSync(path.dirname(cacheFile), { recursive: true });
36
+ fs.writeFileSync(cacheFile, JSON.stringify({ checked: new Date().toISOString(), latest }));
37
+ return latest;
38
+ }
39
+ catch {
40
+ if (opts.force)
41
+ throw new Error('registry unreachable');
42
+ return cached?.latest; // offline or registry down: say nothing new
43
+ }
44
+ }
45
+ // How this copy was installed, read from where its script lives
46
+ export function installMethod(script) {
47
+ if (/[\\/](_npx|dlx(-\d+)?)[\\/]/.test(script))
48
+ return 'ephemeral';
49
+ if (!/[\\/]node_modules[\\/]@ugurcandede[\\/]cc-cost[\\/]/.test(script))
50
+ return 'unknown'; // e.g. a git checkout
51
+ if (/[\\/]pnpm[\\/]/i.test(script))
52
+ return 'pnpm';
53
+ if (/[\\/]yarn[\\/](data[\\/])?global[\\/]/i.test(script))
54
+ return 'yarn';
55
+ return 'npm';
56
+ }
57
+ export const updateCommand = {
58
+ npm: (pkg) => ['npm', 'i', '-g', `${pkg}@latest`],
59
+ yarn: (pkg) => ['yarn', 'global', 'add', `${pkg}@latest`],
60
+ pnpm: (pkg) => ['pnpm', 'add', '-g', `${pkg}@latest`],
61
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ugurcandede/cc-cost",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "API-equivalent cost of your Claude Code usage across all your machines, with an archive that outlives transcript cleanup and an offline dashboard.",
5
5
  "keywords": [
6
6
  "claude",