agy-cli-usage 0.1.0 → 0.3.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/CHANGELOG.md +23 -0
- package/README.md +14 -9
- package/package.json +2 -1
- package/src/credentials.js +33 -5
- package/src/main.js +12 -2
- package/src/update.js +87 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
## [0.3.0](https://github.com/abruption/agy-cli-usage/compare/v0.2.0...v0.3.0) (2026-06-24)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Features
|
|
5
|
+
|
|
6
|
+
* add --version flag and self-update command ([#8](https://github.com/abruption/agy-cli-usage/issues/8)) ([811e82f](https://github.com/abruption/agy-cli-usage/commit/811e82f057e709b92b7a7665b01c54dde913b0d4)), closes [#7](https://github.com/abruption/agy-cli-usage/issues/7)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
### Bug Fixes
|
|
10
|
+
|
|
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
|
+
|
|
13
|
+
## [0.2.0](https://github.com/abruption/agy-cli-usage/compare/v0.1.0...v0.2.0) (2026-06-24)
|
|
14
|
+
|
|
15
|
+
### Features
|
|
16
|
+
|
|
17
|
+
* **credentials:** read agy token file for headless Linux (no keyring) ([5914e6c](https://github.com/abruption/agy-cli-usage/commit/5914e6cc3cf3977bb6c6f974f68bd40209bc376e))
|
|
18
|
+
|
|
19
|
+
## [0.1.0](https://github.com/abruption/agy-cli-usage/compare/e1d582dd618fe06fd607d00846f5b7a66acf924d...v0.1.0) (2026-06-24)
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* headless usage/quota monitor for Antigravity CLI (agy) ([e1d582d](https://github.com/abruption/agy-cli-usage/commit/e1d582dd618fe06fd607d00846f5b7a66acf924d))
|
package/README.md
CHANGED
|
@@ -39,20 +39,23 @@ agy-usage --no-cache # 5분 캐시 무시하고 강제 조회
|
|
|
39
39
|
|
|
40
40
|
`agy`는 OAuth 토큰을 OS 키링(zalando/go-keyring 규약, service=`gemini`/account=`antigravity`)에 저장한다.
|
|
41
41
|
|
|
42
|
-
1. **직접 API (기본)** —
|
|
42
|
+
1. **직접 API (기본)** — 토큰을 읽고(만료 시 자동 refresh), `agy`와 동일하게:
|
|
43
43
|
- `POST https://<host>/v1internal:loadCodeAssist` → `cloudaicompanionProject` 획득
|
|
44
44
|
- `POST https://<host>/v1internal:retrieveUserQuotaSummary {project}` → 쿼타
|
|
45
45
|
- 호스트: `daily-cloudcode-pa.googleapis.com`(현재 CLI) 또는 `cloudcode-pa.googleapis.com`
|
|
46
|
-
|
|
46
|
+
- 토큰 소스: **키링 → 토큰 파일 → (실패 시) PTY** 순으로 자동 시도
|
|
47
|
+
2. **PTY 폴백** — 위 토큰 소스를 모두 읽을 수 없거나 내부 API가 바뀌면, `agy`를 가상 터미널로 띄워 `/usage`를 보내고, `@xterm/headless`로 alt-screen을 재구성해 패널을 파싱한다.
|
|
47
48
|
|
|
48
49
|
### 크로스플랫폼 자격증명
|
|
49
50
|
|
|
50
|
-
| OS |
|
|
51
|
+
| OS | 토큰 소스 | 비고 |
|
|
51
52
|
|----|-----------|------|
|
|
52
53
|
| macOS | Keychain | `@napi-rs/keyring`, CLI 폴백 `security` |
|
|
53
54
|
| Windows | Credential Manager | `@napi-rs/keyring` |
|
|
54
55
|
| Linux (데스크톱) | Secret Service | `@napi-rs/keyring`, CLI 폴백 `secret-tool` |
|
|
55
|
-
| Linux (헤드리스) |
|
|
56
|
+
| Linux (헤드리스) | **토큰 파일** | 키링 부재 시 `~/.gemini/antigravity-cli/antigravity-oauth-token`(순수 JSON) 읽음 → **API 경로 동작**. `AGY_OAUTH_TOKEN_FILE`로 경로 override |
|
|
57
|
+
|
|
58
|
+
> 헤드리스 서버에서도 토큰 파일을 읽어 빠른 API 경로를 쓴다. 파일조차 없으면 `--source pty`로 우회(python3 필요).
|
|
56
59
|
|
|
57
60
|
## HTTP 엔드포인트 (선택)
|
|
58
61
|
|
|
@@ -70,12 +73,14 @@ npm run check # 구문 검사
|
|
|
70
73
|
```
|
|
71
74
|
|
|
72
75
|
- **CI** (`.github/workflows/ci.yml`): push/PR마다 ubuntu(Node 18/20/22) + macOS/Windows(Node 22)에서 테스트.
|
|
73
|
-
- **Release** (`.github/workflows/release.yml`):
|
|
76
|
+
- **Release** ([release-please](https://github.com/googleapis/release-please), `.github/workflows/release-please.yml`): Conventional Commits 기반 **완전 자동화**. `NPM_TOKEN` 시크릿 필요.
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
릴리스 절차 — 수동 버전 범프 없음:
|
|
79
|
+
|
|
80
|
+
1. `feat:`/`fix:` 등 Conventional Commits를 main에 머지하면, release-please가 **Release PR**(버전 범프 + CHANGELOG 갱신)을 자동 생성·유지.
|
|
81
|
+
2. 그 **Release PR을 머지**하면 → GitHub Release + 태그 생성 → 같은 워크플로가 `npm publish --provenance` 실행.
|
|
82
|
+
|
|
83
|
+
> 즉 "Release PR 머지" 한 번이 전체 배포다.
|
|
79
84
|
|
|
80
85
|
## 주의
|
|
81
86
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agy-cli-usage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
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
|
"bin": {
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"src/",
|
|
21
21
|
"server.js",
|
|
22
22
|
"README.md",
|
|
23
|
+
"CHANGELOG.md",
|
|
23
24
|
"LICENSE"
|
|
24
25
|
],
|
|
25
26
|
"keywords": [
|
package/src/credentials.js
CHANGED
|
@@ -10,10 +10,15 @@
|
|
|
10
10
|
// Read backends, tried in order:
|
|
11
11
|
// 1. @napi-rs/keyring native module (macOS / Windows / Linux Secret Service)
|
|
12
12
|
// 2. OS CLI fallback (`security` on macOS, `secret-tool` on Linux)
|
|
13
|
-
//
|
|
14
|
-
// the
|
|
13
|
+
// 3. File fallback (headless Linux: agy can't reach a keyring
|
|
14
|
+
// and writes the token to a plain-JSON file)
|
|
15
|
+
// If every backend fails, the caller falls back to the PTY path which drives
|
|
16
|
+
// `agy` itself.
|
|
15
17
|
|
|
16
18
|
import { execFileSync } from 'node:child_process';
|
|
19
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
20
|
+
import { homedir } from 'node:os';
|
|
21
|
+
import { join } from 'node:path';
|
|
17
22
|
|
|
18
23
|
// OAuth client for the Antigravity CLI. This is an installed/desktop ("public")
|
|
19
24
|
// OAuth client: per Google's own docs the client secret of an installed app is
|
|
@@ -68,17 +73,39 @@ function readViaCli() {
|
|
|
68
73
|
return null;
|
|
69
74
|
}
|
|
70
75
|
|
|
76
|
+
// On headless Linux (no Secret Service) agy persists the token to a plain-JSON
|
|
77
|
+
// file instead of the keyring. Same payload shape, no `go-keyring-base64:` prefix.
|
|
78
|
+
function readViaFile() {
|
|
79
|
+
const candidates = [
|
|
80
|
+
process.env.AGY_OAUTH_TOKEN_FILE,
|
|
81
|
+
join(homedir(), '.gemini', 'antigravity-cli', 'antigravity-oauth-token'),
|
|
82
|
+
].filter(Boolean);
|
|
83
|
+
for (const path of candidates) {
|
|
84
|
+
try {
|
|
85
|
+
if (existsSync(path)) {
|
|
86
|
+
const content = readFileSync(path, 'utf8').trim();
|
|
87
|
+
if (content) return content;
|
|
88
|
+
}
|
|
89
|
+
} catch {
|
|
90
|
+
// unreadable (perms) — try next candidate
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
71
96
|
async function readRawSecret() {
|
|
72
97
|
const fromNapi = await readViaNapiEsm();
|
|
73
98
|
if (fromNapi) return fromNapi;
|
|
74
99
|
const fromCli = readViaCli();
|
|
75
100
|
if (fromCli) return fromCli;
|
|
101
|
+
const fromFile = readViaFile();
|
|
102
|
+
if (fromFile) return fromFile;
|
|
76
103
|
return null;
|
|
77
104
|
}
|
|
78
105
|
|
|
79
106
|
// --- decode ------------------------------------------------------------------
|
|
80
107
|
|
|
81
|
-
function decodeSecret(raw) {
|
|
108
|
+
export function decodeSecret(raw) {
|
|
82
109
|
const payload = raw.startsWith(B64_PREFIX)
|
|
83
110
|
? Buffer.from(raw.slice(B64_PREFIX.length), 'base64').toString('utf8')
|
|
84
111
|
: raw;
|
|
@@ -138,8 +165,9 @@ export async function getAccessToken() {
|
|
|
138
165
|
const raw = await readRawSecret();
|
|
139
166
|
if (!raw) {
|
|
140
167
|
throw new CredentialError(
|
|
141
|
-
'Could not read agy credential from the OS keyring. ' +
|
|
142
|
-
'Is agy logged in on this machine? (
|
|
168
|
+
'Could not read agy credential from the OS keyring or token file. ' +
|
|
169
|
+
'Is agy logged in on this machine? (set AGY_OAUTH_TOKEN_FILE to override the path, ' +
|
|
170
|
+
'or use --source pty)',
|
|
143
171
|
);
|
|
144
172
|
}
|
|
145
173
|
const cred = decodeSecret(raw);
|
package/src/main.js
CHANGED
|
@@ -9,12 +9,15 @@
|
|
|
9
9
|
// agy-usage --channel daily|prod Cloud Code host (default: auto-detect)
|
|
10
10
|
// agy-usage --no-cache bypass the 5-minute cache
|
|
11
11
|
// agy-usage --refresh force a fresh fetch (alias for --no-cache)
|
|
12
|
+
// agy-usage update [--check] self-update via npm
|
|
13
|
+
// agy-usage --version | -v print the installed version
|
|
12
14
|
|
|
13
15
|
import { getAccessToken, CredentialError } from './credentials.js';
|
|
14
16
|
import { fetchQuotaSummary } from './api.js';
|
|
15
17
|
import { captureUsageViaPty } from './pty-fallback.js';
|
|
16
18
|
import { fromApi, fromPty } from './quota.js';
|
|
17
19
|
import { renderPanel } from './render.js';
|
|
20
|
+
import { currentVersion, runUpdate } from './update.js';
|
|
18
21
|
import { readFileSync, writeFileSync, mkdirSync } from 'node:fs';
|
|
19
22
|
import { homedir } from 'node:os';
|
|
20
23
|
import { join } from 'node:path';
|
|
@@ -24,16 +27,19 @@ const CACHE_FILE = join(CACHE_DIR, 'quota.json');
|
|
|
24
27
|
const CACHE_TTL_MS = 5 * 60 * 1000;
|
|
25
28
|
|
|
26
29
|
function parseArgs(argv) {
|
|
27
|
-
const o = { json: false, watch: null, source: 'auto', channel: 'auto', cache: true };
|
|
30
|
+
const o = { json: false, watch: null, source: 'auto', channel: 'auto', cache: true, command: null, check: false };
|
|
28
31
|
for (let i = 0; i < argv.length; i++) {
|
|
29
32
|
const a = argv[i];
|
|
30
|
-
if (a === '
|
|
33
|
+
if (a === 'update' && o.command == null) o.command = 'update';
|
|
34
|
+
else if (a === '--json') o.json = true;
|
|
31
35
|
else if (a === '--watch') {
|
|
32
36
|
const n = Number(argv[i + 1]);
|
|
33
37
|
if (Number.isFinite(n)) { o.watch = n; i++; } else o.watch = 60;
|
|
34
38
|
} else if (a === '--source') o.source = argv[++i];
|
|
35
39
|
else if (a === '--channel') o.channel = argv[++i];
|
|
36
40
|
else if (a === '--no-cache' || a === '--refresh') o.cache = false;
|
|
41
|
+
else if (a === '--check') o.check = true;
|
|
42
|
+
else if (a === '-v' || a === '--version') o.version = true;
|
|
37
43
|
else if (a === '-h' || a === '--help') o.help = true;
|
|
38
44
|
}
|
|
39
45
|
return o;
|
|
@@ -47,6 +53,8 @@ const HELP = `agy-usage — Antigravity CLI (agy) usage/quota monitor
|
|
|
47
53
|
agy-usage --source <auto|api|pty>
|
|
48
54
|
agy-usage --channel <auto|daily|prod>
|
|
49
55
|
agy-usage --no-cache | --refresh
|
|
56
|
+
agy-usage update [--check] self-update via npm (--check: report only)
|
|
57
|
+
agy-usage --version | -v
|
|
50
58
|
`;
|
|
51
59
|
|
|
52
60
|
// --- cache -------------------------------------------------------------------
|
|
@@ -104,6 +112,8 @@ async function once(opts) {
|
|
|
104
112
|
async function main() {
|
|
105
113
|
const opts = parseArgs(process.argv.slice(2));
|
|
106
114
|
if (opts.help) { process.stdout.write(HELP); return; }
|
|
115
|
+
if (opts.version) { process.stdout.write(currentVersion() + '\n'); return; }
|
|
116
|
+
if (opts.command === 'update') { process.exit(await runUpdate({ checkOnly: opts.check })); }
|
|
107
117
|
|
|
108
118
|
if (opts.watch != null) {
|
|
109
119
|
const intervalMs = Math.max(5, opts.watch) * 1000;
|
package/src/update.js
ADDED
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Self-update + version helpers for the CLI.
|
|
2
|
+
//
|
|
3
|
+
// `agy-cli-usage update` check the registry and `npm install -g` if newer
|
|
4
|
+
// `agy-cli-usage update --check` report only, don't install
|
|
5
|
+
// `agy-cli-usage --version` print the installed version
|
|
6
|
+
|
|
7
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
8
|
+
import { readFileSync } from 'node:fs';
|
|
9
|
+
|
|
10
|
+
const PKG_NAME = 'agy-cli-usage';
|
|
11
|
+
|
|
12
|
+
/** Installed version, read from this package's package.json. */
|
|
13
|
+
export function currentVersion() {
|
|
14
|
+
const pkg = JSON.parse(readFileSync(new URL('../package.json', import.meta.url), 'utf8'));
|
|
15
|
+
return pkg.version;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Compare two dotted versions numerically (prerelease tags ignored).
|
|
20
|
+
* @returns negative if a<b, 0 if equal, positive if a>b
|
|
21
|
+
*/
|
|
22
|
+
export function semverCompare(a, b) {
|
|
23
|
+
const norm = (v) =>
|
|
24
|
+
String(v)
|
|
25
|
+
.replace(/^v/, '')
|
|
26
|
+
.split('-')[0]
|
|
27
|
+
.split('.')
|
|
28
|
+
.map((n) => parseInt(n, 10) || 0);
|
|
29
|
+
const pa = norm(a);
|
|
30
|
+
const pb = norm(b);
|
|
31
|
+
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
32
|
+
const d = (pa[i] || 0) - (pb[i] || 0);
|
|
33
|
+
if (d !== 0) return d;
|
|
34
|
+
}
|
|
35
|
+
return 0;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Latest published version: prefer the user's configured registry (npm view), fall back to public. */
|
|
39
|
+
export async function latestVersion() {
|
|
40
|
+
try {
|
|
41
|
+
const out = execFileSync('npm', ['view', PKG_NAME, 'version'], {
|
|
42
|
+
encoding: 'utf8',
|
|
43
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
44
|
+
}).trim();
|
|
45
|
+
if (out) return out;
|
|
46
|
+
} catch {
|
|
47
|
+
// npm missing or offline — try the public registry directly
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const res = await fetch(`https://registry.npmjs.org/${PKG_NAME}/latest`);
|
|
51
|
+
if (res.ok) return (await res.json()).version;
|
|
52
|
+
} catch {
|
|
53
|
+
// offline
|
|
54
|
+
}
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Run the update flow.
|
|
60
|
+
* @param {{ checkOnly?: boolean }} opts
|
|
61
|
+
* @returns {Promise<number>} process exit code
|
|
62
|
+
*/
|
|
63
|
+
export async function runUpdate({ checkOnly = false } = {}) {
|
|
64
|
+
const current = currentVersion();
|
|
65
|
+
const latest = await latestVersion();
|
|
66
|
+
if (!latest) {
|
|
67
|
+
process.stderr.write('Could not determine the latest version (offline or npm unavailable).\n');
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
if (semverCompare(latest, current) <= 0) {
|
|
71
|
+
process.stdout.write(`agy-cli-usage is up to date (${current}).\n`);
|
|
72
|
+
return 0;
|
|
73
|
+
}
|
|
74
|
+
process.stdout.write(`Update available: ${current} -> ${latest}\n`);
|
|
75
|
+
if (checkOnly) {
|
|
76
|
+
process.stdout.write('Run `agy-cli-usage update` to install it.\n');
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
process.stdout.write(`Installing ${PKG_NAME}@${latest} globally…\n`);
|
|
80
|
+
const r = spawnSync('npm', ['install', '-g', `${PKG_NAME}@${latest}`], { stdio: 'inherit' });
|
|
81
|
+
if (r.error) {
|
|
82
|
+
process.stderr.write(`Failed to run npm: ${r.error.message}\nInstall manually: npm install -g ${PKG_NAME}@latest\n`);
|
|
83
|
+
return 1;
|
|
84
|
+
}
|
|
85
|
+
if (r.status === 0) process.stdout.write(`Updated to ${latest}.\n`);
|
|
86
|
+
return r.status ?? 0;
|
|
87
|
+
}
|