agy-cli-usage 0.2.0 → 0.3.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/CHANGELOG.md +30 -0
- package/README.md +7 -5
- package/package.json +2 -1
- package/src/credentials.js +67 -2
- package/src/main.js +12 -2
- package/src/update.js +87 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
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.3.1](https://github.com/abruption/agy-cli-usage/compare/v0.3.0...v0.3.1) (2026-06-24)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
### Bug Fixes
|
|
17
|
+
|
|
18
|
+
* read agy token from Windows Credential Manager ([#14](https://github.com/abruption/agy-cli-usage/issues/14)) ([e5cb274](https://github.com/abruption/agy-cli-usage/commit/e5cb274c6a0a6aa1ddf6b491d417bfc81c04916a)), closes [#13](https://github.com/abruption/agy-cli-usage/issues/13)
|
|
19
|
+
|
|
20
|
+
## [0.2.0](https://github.com/abruption/agy-cli-usage/compare/v0.1.0...v0.2.0) (2026-06-24)
|
|
21
|
+
|
|
22
|
+
### Features
|
|
23
|
+
|
|
24
|
+
* **credentials:** read agy token file for headless Linux (no keyring) ([5914e6c](https://github.com/abruption/agy-cli-usage/commit/5914e6cc3cf3977bb6c6f974f68bd40209bc376e))
|
|
25
|
+
|
|
26
|
+
## [0.1.0](https://github.com/abruption/agy-cli-usage/compare/e1d582dd618fe06fd607d00846f5b7a66acf924d...v0.1.0) (2026-06-24)
|
|
27
|
+
|
|
28
|
+
### Features
|
|
29
|
+
|
|
30
|
+
* headless usage/quota monitor for Antigravity CLI (agy) ([e1d582d](https://github.com/abruption/agy-cli-usage/commit/e1d582dd618fe06fd607d00846f5b7a66acf924d))
|
package/README.md
CHANGED
|
@@ -73,12 +73,14 @@ npm run check # 구문 검사
|
|
|
73
73
|
```
|
|
74
74
|
|
|
75
75
|
- **CI** (`.github/workflows/ci.yml`): push/PR마다 ubuntu(Node 18/20/22) + macOS/Windows(Node 22)에서 테스트.
|
|
76
|
-
- **Release** (`.github/workflows/release.yml`):
|
|
76
|
+
- **Release** ([release-please](https://github.com/googleapis/release-please), `.github/workflows/release-please.yml`): Conventional Commits 기반 **완전 자동화**. `NPM_TOKEN` 시크릿 필요.
|
|
77
77
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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 머지" 한 번이 전체 배포다.
|
|
82
84
|
|
|
83
85
|
## 주의
|
|
84
86
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agy-cli-usage",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
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
|
@@ -8,9 +8,11 @@
|
|
|
8
8
|
// "auth_method": "consumer" }
|
|
9
9
|
//
|
|
10
10
|
// Read backends, tried in order:
|
|
11
|
-
// 1. @napi-rs/keyring native module (macOS /
|
|
11
|
+
// 1. @napi-rs/keyring native module (macOS / Linux Secret Service)
|
|
12
12
|
// 2. OS CLI fallback (`security` on macOS, `secret-tool` on Linux)
|
|
13
|
-
// 3.
|
|
13
|
+
// 3. Windows Credential Manager (CredRead via powershell.exe — go-keyring's
|
|
14
|
+
// target format differs from keyring-rs's)
|
|
15
|
+
// 4. File fallback (headless Linux: agy can't reach a keyring
|
|
14
16
|
// and writes the token to a plain-JSON file)
|
|
15
17
|
// If every backend fails, the caller falls back to the PTY path which drives
|
|
16
18
|
// `agy` itself.
|
|
@@ -73,6 +75,67 @@ function readViaCli() {
|
|
|
73
75
|
return null;
|
|
74
76
|
}
|
|
75
77
|
|
|
78
|
+
// On Windows, agy stores the token in Credential Manager via Go's
|
|
79
|
+
// zalando/go-keyring, whose target name is `service:account` ("gemini:antigravity").
|
|
80
|
+
// @napi-rs/keyring (keyring-rs) uses a different target format and can't find it,
|
|
81
|
+
// so we read the credential blob directly via the Win32 CredRead API through the
|
|
82
|
+
// built-in powershell.exe (no extra dependency).
|
|
83
|
+
const WIN_CRED_TARGET = `${KEYRING_SERVICE}:${KEYRING_ACCOUNT}`;
|
|
84
|
+
|
|
85
|
+
const PS_READ_CRED = `$ErrorActionPreference='Stop'
|
|
86
|
+
$sig=@'
|
|
87
|
+
using System;
|
|
88
|
+
using System.Runtime.InteropServices;
|
|
89
|
+
public class CredApi {
|
|
90
|
+
[DllImport("advapi32.dll", SetLastError=true, CharSet=CharSet.Unicode)]
|
|
91
|
+
public static extern bool CredRead(string target, int type, int flags, out IntPtr cred);
|
|
92
|
+
[DllImport("advapi32.dll")] public static extern void CredFree(IntPtr cred);
|
|
93
|
+
[StructLayout(LayoutKind.Sequential)]
|
|
94
|
+
public struct CREDENTIAL {
|
|
95
|
+
public int Flags; public int Type; public IntPtr TargetName; public IntPtr Comment;
|
|
96
|
+
public System.Runtime.InteropServices.ComTypes.FILETIME LastWritten;
|
|
97
|
+
public int CredentialBlobSize; public IntPtr CredentialBlob; public int Persist;
|
|
98
|
+
public int AttributeCount; public IntPtr Attributes; public IntPtr TargetAlias; public IntPtr UserName;
|
|
99
|
+
}
|
|
100
|
+
public static byte[] Read(string target){
|
|
101
|
+
IntPtr p; if(!CredRead(target,1,0,out p)) return null;
|
|
102
|
+
try {
|
|
103
|
+
var c=(CREDENTIAL)Marshal.PtrToStructure(p,typeof(CREDENTIAL));
|
|
104
|
+
var b=new byte[c.CredentialBlobSize];
|
|
105
|
+
if(c.CredentialBlobSize>0) Marshal.Copy(c.CredentialBlob,b,0,c.CredentialBlobSize);
|
|
106
|
+
return b;
|
|
107
|
+
} finally { CredFree(p); }
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
'@
|
|
111
|
+
Add-Type -TypeDefinition $sig | Out-Null
|
|
112
|
+
$b=[CredApi]::Read('${WIN_CRED_TARGET}')
|
|
113
|
+
if($b -eq $null){ exit 1 }
|
|
114
|
+
[Console]::Out.Write([Convert]::ToBase64String($b))`;
|
|
115
|
+
|
|
116
|
+
function readViaWindowsCredman() {
|
|
117
|
+
if (process.platform !== 'win32') return null;
|
|
118
|
+
try {
|
|
119
|
+
const encoded = Buffer.from(PS_READ_CRED, 'utf16le').toString('base64');
|
|
120
|
+
const b64 = execFileSync(
|
|
121
|
+
'powershell.exe',
|
|
122
|
+
['-NoProfile', '-NonInteractive', '-EncodedCommand', encoded],
|
|
123
|
+
{ encoding: 'utf8' },
|
|
124
|
+
).trim();
|
|
125
|
+
if (!b64) return null;
|
|
126
|
+
const raw = Buffer.from(b64, 'base64');
|
|
127
|
+
// go-keyring writes the value as UTF-8; tolerate UTF-16LE just in case.
|
|
128
|
+
const utf8 = raw.toString('utf8');
|
|
129
|
+
const looksValid = (s) => s.startsWith(B64_PREFIX) || s.trimStart().startsWith('{');
|
|
130
|
+
if (looksValid(utf8)) return utf8;
|
|
131
|
+
const utf16 = raw.toString('utf16le');
|
|
132
|
+
if (looksValid(utf16)) return utf16;
|
|
133
|
+
return utf8;
|
|
134
|
+
} catch {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
76
139
|
// On headless Linux (no Secret Service) agy persists the token to a plain-JSON
|
|
77
140
|
// file instead of the keyring. Same payload shape, no `go-keyring-base64:` prefix.
|
|
78
141
|
function readViaFile() {
|
|
@@ -98,6 +161,8 @@ async function readRawSecret() {
|
|
|
98
161
|
if (fromNapi) return fromNapi;
|
|
99
162
|
const fromCli = readViaCli();
|
|
100
163
|
if (fromCli) return fromCli;
|
|
164
|
+
const fromWin = readViaWindowsCredman();
|
|
165
|
+
if (fromWin) return fromWin;
|
|
101
166
|
const fromFile = readViaFile();
|
|
102
167
|
if (fromFile) return fromFile;
|
|
103
168
|
return null;
|
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
|
+
}
|