agy-cli-usage 0.4.2 → 0.4.3

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/CHANGELOG.md CHANGED
@@ -10,6 +10,13 @@
10
10
 
11
11
  * use plain v* tags in release-please ([#12](https://github.com/abruption/agy-cli-usage/issues/12)) ([74b648d](https://github.com/abruption/agy-cli-usage/commit/74b648df24967f71a43095a80e7340a6b5ac2e39)), closes [#9](https://github.com/abruption/agy-cli-usage/issues/9)
12
12
 
13
+ ## [0.4.3](https://github.com/abruption/agy-cli-usage/compare/v0.4.2...v0.4.3) (2026-07-02)
14
+
15
+
16
+ ### Bug Fixes
17
+
18
+ * key the quota cache by source/channel, validate --source/--channel ([#33](https://github.com/abruption/agy-cli-usage/issues/33)) ([19e6860](https://github.com/abruption/agy-cli-usage/commit/19e6860bcf2aa8a547b880a4430027539b4343fb)), closes [#30](https://github.com/abruption/agy-cli-usage/issues/30)
19
+
13
20
  ## [0.4.2](https://github.com/abruption/agy-cli-usage/compare/v0.4.1...v0.4.2) (2026-06-30)
14
21
 
15
22
 
@@ -11,10 +11,18 @@ export interface CliOptions {
11
11
  version?: boolean;
12
12
  help?: boolean;
13
13
  }
14
+ /** Exported for direct unit testing (no process.argv/exit side effects). */
15
+ export declare function parseArgs(argv: string[]): CliOptions;
16
+ /** Exported for direct unit testing via an injected `cacheFile` — not part of the CLI's public surface. */
17
+ export declare function readCache(source: SnapshotOptions['source'], channel: SnapshotOptions['channel'], cacheFile?: string): Snapshot | null;
18
+ /** Exported for direct unit testing via an injected `cacheFile` — not part of the CLI's public surface. */
19
+ export declare function writeCache(snap: Snapshot, source: SnapshotOptions['source'], channel: SnapshotOptions['channel'], cacheFile?: string): void;
14
20
  /** Subset of options needed to produce a snapshot (also usable from server.ts). */
15
21
  export interface SnapshotOptions {
16
22
  source: 'auto' | 'api' | 'pty';
17
23
  channel: 'auto' | 'daily' | 'prod';
18
24
  cache: boolean;
25
+ /** Override the cache file path — for tests only; defaults to the real user cache. */
26
+ cacheFile?: string;
19
27
  }
20
28
  export declare function getSnapshot(opts: SnapshotOptions): Promise<Snapshot>;
package/dist/src/main.js CHANGED
@@ -17,14 +17,18 @@ import { captureUsageViaPty } from './pty-fallback.js';
17
17
  import { fromApi, fromPty } from './quota.js';
18
18
  import { renderPanel } from './render.js';
19
19
  import { currentVersion, runUpdate } from './update.js';
20
- import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
20
+ import { readFileSync, writeFileSync, mkdirSync, realpathSync } from 'node:fs';
21
21
  import { homedir } from 'node:os';
22
- import { join } from 'node:path';
22
+ import { join, dirname } from 'node:path';
23
+ import { fileURLToPath } from 'node:url';
23
24
  const CACHE_DIR = join(process.env.XDG_CACHE_HOME || join(homedir(), '.cache'), 'agy-usage');
24
25
  const CACHE_FILE = join(CACHE_DIR, 'quota.json');
25
26
  const CACHE_TTL_MS = 5 * 60 * 1000;
27
+ const VALID_SOURCES = ['auto', 'api', 'pty'];
28
+ const VALID_CHANNELS = ['auto', 'daily', 'prod'];
26
29
  const errMessage = (e) => (e instanceof Error ? e.message : String(e));
27
- function parseArgs(argv) {
30
+ /** Exported for direct unit testing (no process.argv/exit side effects). */
31
+ export function parseArgs(argv) {
28
32
  const o = {
29
33
  json: false, watch: null, source: 'auto', channel: 'auto', cache: true, command: null, check: false,
30
34
  };
@@ -43,10 +47,20 @@ function parseArgs(argv) {
43
47
  else
44
48
  o.watch = 60;
45
49
  }
46
- else if (a === '--source')
47
- o.source = (argv[++i] ?? 'auto');
48
- else if (a === '--channel')
49
- o.channel = (argv[++i] ?? 'auto');
50
+ else if (a === '--source') {
51
+ const v = argv[++i];
52
+ if (!VALID_SOURCES.includes(v)) {
53
+ throw new Error(`invalid --source '${v}' — expected one of: ${VALID_SOURCES.join(', ')}`);
54
+ }
55
+ o.source = v;
56
+ }
57
+ else if (a === '--channel') {
58
+ const v = argv[++i];
59
+ if (!VALID_CHANNELS.includes(v)) {
60
+ throw new Error(`invalid --channel '${v}' — expected one of: ${VALID_CHANNELS.join(', ')}`);
61
+ }
62
+ o.channel = v;
63
+ }
50
64
  else if (a === '--no-cache' || a === '--refresh')
51
65
  o.cache = false;
52
66
  else if (a === '--check')
@@ -69,22 +83,25 @@ const HELP = `agy-cli-usage — Antigravity CLI (agy) usage/quota monitor
69
83
  agy-cli-usage update [--check] self-update via npm (--check: report only)
70
84
  agy-cli-usage --version | -v
71
85
  `;
72
- // --- cache -------------------------------------------------------------------
73
- function readCache() {
86
+ /** Exported for direct unit testing via an injected `cacheFile` — not part of the CLI's public surface. */
87
+ export function readCache(source, channel, cacheFile = CACHE_FILE) {
74
88
  try {
75
- const { ts, snap } = JSON.parse(readFileSync(CACHE_FILE, 'utf8'));
76
- if (Date.now() - ts < CACHE_TTL_MS)
77
- return snap;
89
+ const entry = JSON.parse(readFileSync(cacheFile, 'utf8'));
90
+ if (entry.source !== source || entry.channel !== channel)
91
+ return null;
92
+ if (Date.now() - entry.ts < CACHE_TTL_MS)
93
+ return entry.snap;
78
94
  }
79
95
  catch {
80
- /* no/expired cache */
96
+ /* no/expired/incompatible-format cache */
81
97
  }
82
98
  return null;
83
99
  }
84
- function writeCache(snap) {
100
+ /** Exported for direct unit testing via an injected `cacheFile` — not part of the CLI's public surface. */
101
+ export function writeCache(snap, source, channel, cacheFile = CACHE_FILE) {
85
102
  try {
86
- mkdirSync(CACHE_DIR, { recursive: true });
87
- writeFileSync(CACHE_FILE, JSON.stringify({ ts: Date.now(), snap }));
103
+ mkdirSync(dirname(cacheFile), { recursive: true });
104
+ writeFileSync(cacheFile, JSON.stringify({ ts: Date.now(), source, channel, snap }));
88
105
  }
89
106
  catch {
90
107
  /* cache is best-effort */
@@ -92,7 +109,7 @@ function writeCache(snap) {
92
109
  }
93
110
  export async function getSnapshot(opts) {
94
111
  if (opts.cache && opts.source !== 'pty') {
95
- const cached = readCache();
112
+ const cached = readCache(opts.source, opts.channel, opts.cacheFile);
96
113
  if (cached)
97
114
  return cached;
98
115
  }
@@ -114,7 +131,7 @@ export async function getSnapshot(opts) {
114
131
  snap = fromPty(await captureUsageViaPty());
115
132
  }
116
133
  }
117
- writeCache(snap);
134
+ writeCache(snap, opts.source, opts.channel, opts.cacheFile);
118
135
  return snap;
119
136
  }
120
137
  // --- main --------------------------------------------------------------------
@@ -157,13 +174,31 @@ async function main() {
157
174
  await once(opts);
158
175
  }
159
176
  }
160
- main().catch((err) => {
161
- if (err instanceof CredentialError) {
162
- process.stderr.write(`credential error: ${err.message}\n`);
177
+ // Only run the CLI when this file is executed directly (as the `bin` entry
178
+ // point) guarded so parseArgs/readCache/writeCache/etc. can be imported
179
+ // for unit testing without triggering a full live CLI run (network calls,
180
+ // process.exit()) as an import side effect. realpathSync resolves symlinks
181
+ // on process.argv[1] (npm global `bin` entries are frequently symlinks);
182
+ // import.meta.url is already symlink-resolved by Node's ESM loader.
183
+ function isMainModule() {
184
+ if (!process.argv[1])
185
+ return false;
186
+ try {
187
+ return fileURLToPath(import.meta.url) === realpathSync(process.argv[1]);
163
188
  }
164
- else {
165
- process.stderr.write(`error: ${errMessage(err)}\n`);
189
+ catch {
190
+ return false;
166
191
  }
167
- process.exit(1);
168
- });
192
+ }
193
+ if (isMainModule()) {
194
+ main().catch((err) => {
195
+ if (err instanceof CredentialError) {
196
+ process.stderr.write(`credential error: ${err.message}\n`);
197
+ }
198
+ else {
199
+ process.stderr.write(`error: ${errMessage(err)}\n`);
200
+ }
201
+ process.exit(1);
202
+ });
203
+ }
169
204
  //# sourceMappingURL=main.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agy-cli-usage",
3
- "version": "0.4.2",
3
+ "version": "0.4.3",
4
4
  "description": "Headless usage/quota monitor for the Antigravity CLI (agy) — reads Cloud Code quota directly, with a PTY fallback. No IDE required.",
5
5
  "type": "module",
6
6
  "types": "dist/src/main.d.ts",