cc-dev-template 0.1.102 → 0.1.104

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cc-dev-template",
3
- "version": "0.1.102",
3
+ "version": "0.1.104",
4
4
  "description": "Structured AI-assisted development framework for Claude Code",
5
5
  "bin": {
6
6
  "cc-dev-template": "./bin/install.js"
@@ -13,15 +13,18 @@
13
13
  * Output: Formatted status to stdout
14
14
  */
15
15
 
16
- const { readFileSync, writeFileSync, readdirSync, statSync } = require('fs');
16
+ const { readFileSync, writeFileSync, readdirSync, statSync, unlinkSync } = require('fs');
17
17
  const { join, basename } = require('path');
18
18
  const { execSync, spawnSync, spawn } = require('child_process');
19
19
  const { homedir } = require('os');
20
20
 
21
21
  // Usage API cache
22
22
  const USAGE_CACHE_PATH = join(homedir(), '.claude', '.usage-cache.json');
23
- const USAGE_CACHE_TTL = 120000; // 2 minutes
24
- const USAGE_HISTORY_MAX = 20; // ~40 min of readings at 2min intervals
23
+ const USAGE_LOCK_PATH = join(homedir(), '.claude', '.usage-cache.lock');
24
+ const USAGE_CACHE_TTL = 30000; // 30s normal refresh interval
25
+ const USAGE_ERROR_TTL = 300000; // 5 min backoff on errors (rate limit, network, etc.)
26
+ const USAGE_LOCK_TTL = 15000; // 15s lock (curl timeout is 5s, 15s is generous)
27
+ const USAGE_HISTORY_MAX = 20; // ~10 min of readings at 30s intervals
25
28
 
26
29
  // Background refresh mode: fetch usage data and write cache, then exit
27
30
  if (process.argv.includes('--refresh')) {
@@ -305,13 +308,33 @@ function getOAuthToken() {
305
308
  }
306
309
  }
307
310
 
311
+ /**
312
+ * Write error backoff to cache — preserves existing data/history,
313
+ * but delays next refresh attempt by USAGE_ERROR_TTL.
314
+ */
315
+ function writeErrorBackoff() {
316
+ const now = Date.now();
317
+ try {
318
+ const existing = JSON.parse(readFileSync(USAGE_CACHE_PATH, 'utf-8'));
319
+ existing.nextRefreshAfter = now + USAGE_ERROR_TTL;
320
+ writeFileSync(USAGE_CACHE_PATH, JSON.stringify(existing));
321
+ } catch {
322
+ writeFileSync(USAGE_CACHE_PATH, JSON.stringify({
323
+ timestamp: now,
324
+ nextRefreshAfter: now + USAGE_ERROR_TTL,
325
+ data: null,
326
+ history: [],
327
+ }));
328
+ }
329
+ }
330
+
308
331
  /**
309
332
  * Fetch usage data from API and write to cache (runs in background)
310
333
  */
311
334
  function refreshUsageCache() {
312
335
  try {
313
336
  const token = getOAuthToken();
314
- if (!token) return;
337
+ if (!token) { writeErrorBackoff(); return; }
315
338
 
316
339
  const result = spawnSync('curl', [
317
340
  '-s', '--max-time', '3',
@@ -321,72 +344,77 @@ function refreshUsageCache() {
321
344
  '-H', 'Content-Type: application/json',
322
345
  ], { encoding: 'utf-8', timeout: 5000 });
323
346
 
324
- if (result.status === 0 && result.stdout) {
325
- const data = JSON.parse(result.stdout.trim());
326
- if (data.five_hour && data.seven_day) {
327
- // Load existing history and append new reading
328
- let history = [];
329
- try {
330
- const existing = JSON.parse(readFileSync(USAGE_CACHE_PATH, 'utf-8'));
331
- if (Array.isArray(existing.history)) history = existing.history;
332
- } catch {}
333
-
334
- const now = Date.now();
335
- history.push({
336
- t: now,
337
- five_hour: data.five_hour.utilization,
338
- seven_day: data.seven_day.utilization,
339
- });
347
+ if (result.status !== 0 || !result.stdout) { writeErrorBackoff(); return; }
340
348
 
341
- // Keep only the last N readings
342
- if (history.length > USAGE_HISTORY_MAX) {
343
- history = history.slice(-USAGE_HISTORY_MAX);
344
- }
349
+ let responseData;
350
+ try { responseData = JSON.parse(result.stdout.trim()); } catch { writeErrorBackoff(); return; }
345
351
 
346
- writeFileSync(USAGE_CACHE_PATH, JSON.stringify({
347
- timestamp: now,
348
- data,
349
- history,
350
- }));
351
- }
352
+ if (!responseData.five_hour || !responseData.seven_day) { writeErrorBackoff(); return; }
353
+
354
+ // --- Success path ---
355
+ let history = [];
356
+ try {
357
+ const existing = JSON.parse(readFileSync(USAGE_CACHE_PATH, 'utf-8'));
358
+ if (Array.isArray(existing.history)) history = existing.history;
359
+ } catch {}
360
+
361
+ const now = Date.now();
362
+ history.push({
363
+ t: now,
364
+ five_hour: responseData.five_hour.utilization,
365
+ seven_day: responseData.seven_day.utilization,
366
+ });
367
+
368
+ if (history.length > USAGE_HISTORY_MAX) {
369
+ history = history.slice(-USAGE_HISTORY_MAX);
352
370
  }
371
+
372
+ writeFileSync(USAGE_CACHE_PATH, JSON.stringify({
373
+ timestamp: now,
374
+ nextRefreshAfter: now + USAGE_CACHE_TTL,
375
+ data: responseData,
376
+ history,
377
+ }));
353
378
  } catch {
354
- // Silently fail - stale cache will be used on next render
379
+ writeErrorBackoff();
380
+ } finally {
381
+ try { unlinkSync(USAGE_LOCK_PATH); } catch {}
355
382
  }
356
383
  }
357
384
 
358
385
  /**
359
- * Read cached usage data, trigger background refresh if stale
386
+ * Read cached usage data, trigger background refresh if scheduled time has passed.
387
+ * All instances share the same nextRefreshAfter timestamp — only one refresh per cycle.
360
388
  */
361
389
  function getUsageData() {
362
390
  let cacheData = null;
363
391
  let cacheHistory = null;
364
- let cacheAge = Infinity;
392
+ let shouldRefresh = true; // refresh if no cache exists
365
393
 
366
394
  try {
367
395
  const raw = readFileSync(USAGE_CACHE_PATH, 'utf-8');
368
396
  const cache = JSON.parse(raw);
369
397
  cacheData = cache.data;
370
398
  cacheHistory = cache.history || null;
371
- cacheAge = Date.now() - cache.timestamp;
399
+ shouldRefresh = Date.now() > (cache.nextRefreshAfter || 0);
372
400
  } catch {}
373
401
 
374
- // Trigger background refresh if cache is stale
375
- if (cacheAge > USAGE_CACHE_TTL) {
376
- // Optimistic lock: claim the timestamp so other sessions see fresh cache
402
+ if (shouldRefresh) {
403
+ let lockHeld = false;
377
404
  try {
378
- const raw = readFileSync(USAGE_CACHE_PATH, 'utf-8');
379
- const cache = JSON.parse(raw);
380
- cache.timestamp = Date.now();
381
- writeFileSync(USAGE_CACHE_PATH, JSON.stringify(cache));
382
- } catch {}
383
- try {
384
- const child = spawn(process.execPath, [__filename, '--refresh'], {
385
- detached: true,
386
- stdio: 'ignore',
387
- });
388
- child.unref();
405
+ const lockTime = parseInt(readFileSync(USAGE_LOCK_PATH, 'utf-8'), 10);
406
+ lockHeld = (Date.now() - lockTime) < USAGE_LOCK_TTL;
389
407
  } catch {}
408
+ if (!lockHeld) {
409
+ try { writeFileSync(USAGE_LOCK_PATH, String(Date.now())); } catch {}
410
+ try {
411
+ const child = spawn(process.execPath, [__filename, '--refresh'], {
412
+ detached: true,
413
+ stdio: 'ignore',
414
+ });
415
+ child.unref();
416
+ } catch {}
417
+ }
390
418
  }
391
419
 
392
420
  return { data: cacheData, history: cacheHistory };