switch-acc-agy 0.1.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.
Files changed (3) hide show
  1. package/README.md +382 -0
  2. package/package.json +31 -0
  3. package/switch-acc-agy +236 -0
package/README.md ADDED
@@ -0,0 +1,382 @@
1
+ # switch-agy
2
+
3
+ `switch-agy` là tool switch account local cho Antigravity CLI (`agy`) trên
4
+ macOS.
5
+
6
+ Mục tiêu: sau khi bạn login thủ công mỗi account một lần, bạn có thể switch
7
+ account ngay từ terminal mà không cần mở browser hay nhập code lại:
8
+
9
+ ```bash
10
+ switch-acc-agy-to acc1
11
+ agy --prompt "hello"
12
+ ```
13
+
14
+ Tool hoạt động bằng cách lưu và khôi phục 2 nơi chứa credential mà `agy` đang
15
+ dùng trên máy này:
16
+
17
+ - `~/.gemini/oauth_creds.json`
18
+ - macOS Keychain generic password `service=gemini`, `account=antigravity`
19
+
20
+ Tool không tự động login Google. Nó chỉ snapshot credential đã có sẵn trên máy
21
+ và restore lại khi cần switch.
22
+
23
+ ## Yêu Cầu
24
+
25
+ - macOS
26
+ - Node.js và `npm`
27
+ - `agy` đã cài và chạy được
28
+ - Ít nhất một account Antigravity/Gemini đã login sẵn
29
+
30
+ Kiểm tra:
31
+
32
+ ```bash
33
+ node --version
34
+ npm --version
35
+ command -v agy
36
+ ```
37
+
38
+ ## Cài Đặt
39
+
40
+ Cách gọn nhất trên máy mới là cài global qua `npm`. Không cần `cd` vào repo để
41
+ tạo symlink tay nữa.
42
+
43
+ Nếu máy có quyền pull repo qua GitHub SSH:
44
+
45
+ ```bash
46
+ npm install -g git+ssh://git@github.com/XuanMaiHieu/switch-acc-agy.git
47
+ ```
48
+
49
+ Nếu đã clone repo sẵn trên máy:
50
+
51
+ ```bash
52
+ npm install -g /path/to/switch-agy
53
+ ```
54
+
55
+ Verify:
56
+
57
+ ```bash
58
+ command -v switch-acc-agy
59
+ switch-acc-agy paths
60
+ ```
61
+
62
+ `npm` sẽ tự tạo các command global:
63
+
64
+ - `switch-acc-agy`
65
+ - `switch-acc-agy-save`
66
+ - `switch-acc-agy-to`
67
+
68
+ Nếu shell chưa nhận ra command ngay, mở terminal mới hoặc reload shell config.
69
+
70
+ Path kỳ vọng trên máy này:
71
+
72
+ ```text
73
+ store: /Users/abc-dev/.switch-agy
74
+ profiles: /Users/abc-dev/.switch-agy/profiles
75
+ backups: /Users/abc-dev/.switch-agy/backups
76
+ oauth_creds: /Users/abc-dev/.gemini/oauth_creds.json
77
+ keychain_service: gemini
78
+ keychain_account: antigravity
79
+ ```
80
+
81
+ ## Setup Lần Đầu Cho Nhiều Account
82
+
83
+ Bạn cần login thủ công từng account một lần. Sau mỗi lần login, lưu trạng thái
84
+ credential hiện tại thành một profile.
85
+
86
+ ### Lưu Account 1
87
+
88
+ Login account 1 bằng flow bình thường của Antigravity/Gemini. Kiểm tra account
89
+ đang chạy được:
90
+
91
+ ```bash
92
+ agy --prompt "say account one works"
93
+ ```
94
+
95
+ Lưu lại:
96
+
97
+ ```bash
98
+ switch-acc-agy-save acc1
99
+ ```
100
+
101
+ ### Lưu Account 2
102
+
103
+ Logout hoặc đổi login thủ công sang account 2 bằng flow bình thường của
104
+ Antigravity/Gemini. Kiểm tra account đang chạy được:
105
+
106
+ ```bash
107
+ agy --prompt "say account two works"
108
+ ```
109
+
110
+ Lưu lại:
111
+
112
+ ```bash
113
+ switch-acc-agy-save acc2
114
+ ```
115
+
116
+ Xem danh sách profile đã lưu:
117
+
118
+ ```bash
119
+ switch-acc-agy list
120
+ ```
121
+
122
+ Ví dụ output:
123
+
124
+ ```text
125
+ acc1
126
+ acc2 *
127
+ ```
128
+
129
+ Dấu `*` là profile mà tool lưu hoặc switch gần nhất.
130
+
131
+ ## Sử Dụng Hàng Ngày
132
+
133
+ Switch sang account 1:
134
+
135
+ ```bash
136
+ switch-acc-agy-to acc1
137
+ agy --prompt "hello from acc1"
138
+ ```
139
+
140
+ Switch sang account 2:
141
+
142
+ ```bash
143
+ switch-acc-agy-to acc2
144
+ agy --prompt "hello from acc2"
145
+ ```
146
+
147
+ Lưu ý quan trọng: sau khi switch, hãy chạy một process `agy` mới. Những session
148
+ `agy` đang mở từ trước có thể vẫn cache token cũ trong memory.
149
+
150
+ ## Lệnh Có Sẵn
151
+
152
+ Lưu auth hiện tại thành profile:
153
+
154
+ ```bash
155
+ switch-acc-agy save <profile>
156
+ switch-acc-agy-save <profile>
157
+ ```
158
+
159
+ Switch sang một profile đã lưu:
160
+
161
+ ```bash
162
+ switch-acc-agy to <profile>
163
+ switch-acc-agy-to <profile>
164
+ ```
165
+
166
+ Liệt kê profile:
167
+
168
+ ```bash
169
+ switch-acc-agy list
170
+ ```
171
+
172
+ Xem profile được chọn gần nhất:
173
+
174
+ ```bash
175
+ switch-acc-agy current
176
+ ```
177
+
178
+ Xem path và Keychain identifier mà tool đang dùng:
179
+
180
+ ```bash
181
+ switch-acc-agy paths
182
+ ```
183
+
184
+ Xem help:
185
+
186
+ ```bash
187
+ switch-acc-agy --help
188
+ ```
189
+
190
+ ## Quy Tắc Đặt Tên Profile
191
+
192
+ Tên profile hợp lệ:
193
+
194
+ - Bắt đầu bằng chữ cái
195
+ - Chỉ dùng chữ cái, số, dấu gạch ngang, dấu gạch dưới
196
+ - Tối đa 32 ký tự
197
+
198
+ Ví dụ hợp lệ:
199
+
200
+ ```text
201
+ acc1
202
+ work
203
+ personal
204
+ gmail_main
205
+ team-pro
206
+ ```
207
+
208
+ Không hợp lệ:
209
+
210
+ ```text
211
+ 1acc
212
+ my.account
213
+ work/email
214
+ ```
215
+
216
+ ## Dữ Liệu Được Lưu Ở Đâu
217
+
218
+ Profiles:
219
+
220
+ ```text
221
+ ~/.switch-agy/profiles/<profile>/
222
+ oauth_creds.json
223
+ keychain_secret
224
+ metadata.json
225
+ ```
226
+
227
+ Backups:
228
+
229
+ ```text
230
+ ~/.switch-agy/backups/<timestamp>/
231
+ oauth_creds.json
232
+ keychain_secret
233
+ ```
234
+
235
+ Credential live sẽ bị thay khi switch:
236
+
237
+ ```text
238
+ ~/.gemini/oauth_creds.json
239
+ macOS Keychain: service=gemini account=antigravity
240
+ ```
241
+
242
+ Tool cố gắng set quyền file credential là `0600` và quyền thư mục profile là
243
+ `0700`.
244
+
245
+ ## Lưu Ý Bảo Mật
246
+
247
+ Profile đã lưu chứa OAuth credential thật. Hãy coi `~/.switch-agy` là dữ liệu
248
+ bí mật.
249
+
250
+ Không commit thư mục này:
251
+
252
+ ```bash
253
+ echo ".switch-agy/" >> ~/.gitignore_global
254
+ git config --global core.excludesfile ~/.gitignore_global
255
+ ```
256
+
257
+ Không paste `oauth_creds.json` hoặc `keychain_secret` vào chat, log, issue,
258
+ ticket.
259
+
260
+ Nếu một profile bị lộ, hãy revoke session Google đó, login thủ công lại, rồi
261
+ lưu lại profile:
262
+
263
+ ```bash
264
+ switch-acc-agy-save acc1
265
+ ```
266
+
267
+ ## Rollback
268
+
269
+ Mỗi lần switch, tool tự tạo backup:
270
+
271
+ ```text
272
+ ~/.switch-agy/backups/YYYYMMDD-HHMMSS
273
+ ```
274
+
275
+ Ví dụ rollback thủ công:
276
+
277
+ ```bash
278
+ backup="$HOME/.switch-agy/backups/20260520-223404"
279
+ cp "$backup/oauth_creds.json" "$HOME/.gemini/oauth_creds.json"
280
+ chmod 600 "$HOME/.gemini/oauth_creds.json"
281
+ security add-generic-password -U -a antigravity -s gemini -w "$(cat "$backup/keychain_secret")"
282
+ ```
283
+
284
+ Sau đó chạy một process `agy` mới:
285
+
286
+ ```bash
287
+ agy --prompt "hello"
288
+ ```
289
+
290
+ ## Troubleshooting
291
+
292
+ ### `switch-acc-agy-to: command not found`
293
+
294
+ Khả năng cao là thư mục global prefix của `npm` chưa nằm trong `PATH`.
295
+
296
+ Xem prefix hiện tại:
297
+
298
+ ```bash
299
+ npm prefix -g
300
+ ```
301
+
302
+ Thêm dòng này vào shell config:
303
+
304
+ ```bash
305
+ export PATH="$(npm prefix -g)/bin:$PATH"
306
+ ```
307
+
308
+ Sau đó restart terminal hoặc chạy:
309
+
310
+ ```bash
311
+ source ~/.zshrc
312
+ ```
313
+
314
+ ### `Keychain item not found`
315
+
316
+ Account hiện tại chưa được Antigravity lưu vào Keychain item kỳ vọng.
317
+
318
+ Chạy:
319
+
320
+ ```bash
321
+ agy --prompt "hello"
322
+ ```
323
+
324
+ Nếu bị yêu cầu login thì login thủ công. Sau đó lưu profile:
325
+
326
+ ```bash
327
+ switch-acc-agy-save acc1
328
+ ```
329
+
330
+ ### `agy` vẫn dùng account cũ
331
+
332
+ Đóng các session `agy` cũ và chạy process mới:
333
+
334
+ ```bash
335
+ switch-acc-agy-to acc1
336
+ agy --prompt "which account is active?"
337
+ ```
338
+
339
+ Tool đã đổi credential local, nhưng process đang chạy có thể vẫn cache token
340
+ cũ.
341
+
342
+ ### Browser vẫn mở sau khi switch
343
+
344
+ Profile đã lưu có thể thiếu credential, hết hạn, hoặc đã bị revoke.
345
+
346
+ Cách sửa: login thủ công lại account đó rồi ghi đè profile:
347
+
348
+ ```bash
349
+ switch-acc-agy-save acc1
350
+ ```
351
+
352
+ ### `agy --prompt` không in output
353
+
354
+ Kiểm tra log mới nhất của Antigravity CLI:
355
+
356
+ ```bash
357
+ tail -120 ~/.gemini/antigravity-cli/cli.log
358
+ ```
359
+
360
+ Silent auth thành công thường có các dòng:
361
+
362
+ ```text
363
+ ChainedAuth: authenticated via keyring (effective: keyring)
364
+ Print mode: silent auth succeeded
365
+ ```
366
+
367
+ ## Tóm Tắt Cơ Chế
368
+
369
+ `save`:
370
+
371
+ 1. Đọc `~/.gemini/oauth_creds.json`.
372
+ 2. Đọc Keychain item `service=gemini`, `account=antigravity`.
373
+ 3. Lưu cả hai vào `~/.switch-agy/profiles/<profile>`.
374
+
375
+ `to`:
376
+
377
+ 1. Backup credential live hiện tại.
378
+ 2. Copy `oauth_creds.json` đã lưu vào `~/.gemini/oauth_creds.json`.
379
+ 3. Thay Keychain item bằng secret đã lưu của profile.
380
+ 4. Cập nhật `~/.switch-agy/current_profile`.
381
+
382
+ Sau đó `agy` sẽ silent auth qua Keychain và dùng account vừa restore.
package/package.json ADDED
@@ -0,0 +1,31 @@
1
+ {
2
+ "name": "switch-acc-agy",
3
+ "version": "0.1.0",
4
+ "description": "Local account switcher for Antigravity CLI auth on macOS",
5
+ "bin": {
6
+ "switch-acc-agy": "switch-acc-agy",
7
+ "switch-acc-agy-save": "switch-acc-agy",
8
+ "switch-acc-agy-to": "switch-acc-agy"
9
+ },
10
+ "files": [
11
+ "switch-acc-agy",
12
+ "README.md"
13
+ ],
14
+ "engines": {
15
+ "node": ">=18"
16
+ },
17
+ "os": [
18
+ "darwin"
19
+ ],
20
+ "keywords": [
21
+ "antigravity",
22
+ "auth",
23
+ "cli",
24
+ "gemini",
25
+ "switch"
26
+ ],
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+ssh://git@github.com/XuanMaiHieu/switch-acc-agy.git"
30
+ }
31
+ }
package/switch-acc-agy ADDED
@@ -0,0 +1,236 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ APP_NAME="switch-acc-agy"
5
+ STORE_DIR="${SWITCH_AGY_STORE:-$HOME/.switch-agy}"
6
+ PROFILES_DIR="$STORE_DIR/profiles"
7
+ BACKUPS_DIR="$STORE_DIR/backups"
8
+ GEMINI_DIR="${GEMINI_DIR:-$HOME/.gemini}"
9
+ OAUTH_CREDS="$GEMINI_DIR/oauth_creds.json"
10
+ KEYCHAIN_SERVICE="${SWITCH_AGY_KEYCHAIN_SERVICE:-gemini}"
11
+ KEYCHAIN_ACCOUNT="${SWITCH_AGY_KEYCHAIN_ACCOUNT:-antigravity}"
12
+
13
+ usage() {
14
+ cat <<EOF
15
+ Usage:
16
+ $APP_NAME save <profile> Save current Antigravity/Gemini auth as profile
17
+ $APP_NAME to <profile> Switch current auth to saved profile
18
+ $APP_NAME list List saved profiles
19
+ $APP_NAME current Show active profile marker
20
+ $APP_NAME paths Show credential paths used by this tool
21
+
22
+ Aliases:
23
+ switch-acc-agy-save <profile>
24
+ switch-acc-agy-to <profile>
25
+ EOF
26
+ }
27
+
28
+ die() {
29
+ printf 'Error: %s\n' "$*" >&2
30
+ exit 1
31
+ }
32
+
33
+ info() {
34
+ printf '%s\n' "$*"
35
+ }
36
+
37
+ validate_profile() {
38
+ local name="${1:-}"
39
+ [[ "$name" =~ ^[A-Za-z][A-Za-z0-9_-]{0,31}$ ]] || \
40
+ die "invalid profile name '$name'. Use letters, numbers, hyphen, underscore; start with a letter; max 32 chars."
41
+ printf '%s' "$name" | tr '[:upper:]' '[:lower:]'
42
+ }
43
+
44
+ ensure_store() {
45
+ mkdir -p "$PROFILES_DIR" "$BACKUPS_DIR" "$GEMINI_DIR"
46
+ chmod 700 "$STORE_DIR" "$PROFILES_DIR" "$BACKUPS_DIR" 2>/dev/null || true
47
+ }
48
+
49
+ profile_dir() {
50
+ local profile
51
+ profile="$(validate_profile "$1")"
52
+ printf '%s/%s' "$PROFILES_DIR" "$profile"
53
+ }
54
+
55
+ require_security() {
56
+ command -v security >/dev/null 2>&1 || die "macOS security command not found"
57
+ }
58
+
59
+ read_keychain_secret() {
60
+ require_security
61
+ security find-generic-password -a "$KEYCHAIN_ACCOUNT" -s "$KEYCHAIN_SERVICE" -w 2>/dev/null
62
+ }
63
+
64
+ write_keychain_secret() {
65
+ local secret_file="$1"
66
+ require_security
67
+ [[ -s "$secret_file" ]] || die "missing saved keychain secret: $secret_file"
68
+
69
+ # Command substitution intentionally removes the display newline that
70
+ # `security -w` appends; the keychain password itself is the credential blob.
71
+ local secret
72
+ secret="$(LC_ALL=C cat "$secret_file")"
73
+ security add-generic-password \
74
+ -U \
75
+ -a "$KEYCHAIN_ACCOUNT" \
76
+ -s "$KEYCHAIN_SERVICE" \
77
+ -w "$secret" >/dev/null
78
+ }
79
+
80
+ save_current_backup() {
81
+ local stamp backup_dir
82
+ stamp="$(date +%Y%m%d-%H%M%S)"
83
+ backup_dir="$BACKUPS_DIR/$stamp"
84
+ mkdir -p "$backup_dir"
85
+ chmod 700 "$backup_dir" 2>/dev/null || true
86
+
87
+ if [[ -f "$OAUTH_CREDS" ]]; then
88
+ cp "$OAUTH_CREDS" "$backup_dir/oauth_creds.json"
89
+ chmod 600 "$backup_dir/oauth_creds.json" 2>/dev/null || true
90
+ fi
91
+
92
+ if read_keychain_secret >"$backup_dir/keychain_secret" 2>/dev/null; then
93
+ chmod 600 "$backup_dir/keychain_secret" 2>/dev/null || true
94
+ else
95
+ rm -f "$backup_dir/keychain_secret"
96
+ fi
97
+
98
+ printf '%s' "$backup_dir"
99
+ }
100
+
101
+ save_profile() {
102
+ local profile dir
103
+ profile="$(validate_profile "$1")"
104
+ dir="$(profile_dir "$profile")"
105
+
106
+ [[ -f "$OAUTH_CREDS" ]] || die "OAuth file not found: $OAUTH_CREDS"
107
+ read_keychain_secret >/dev/null || die "Keychain item not found: service=$KEYCHAIN_SERVICE account=$KEYCHAIN_ACCOUNT"
108
+
109
+ ensure_store
110
+ mkdir -p "$dir"
111
+ chmod 700 "$dir" 2>/dev/null || true
112
+
113
+ cp "$OAUTH_CREDS" "$dir/oauth_creds.json"
114
+ chmod 600 "$dir/oauth_creds.json" 2>/dev/null || true
115
+
116
+ read_keychain_secret >"$dir/keychain_secret"
117
+ chmod 600 "$dir/keychain_secret" 2>/dev/null || true
118
+
119
+ {
120
+ printf '{\n'
121
+ printf ' "profile": "%s",\n' "$profile"
122
+ printf ' "savedAt": "%s",\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
123
+ printf ' "oauthPath": "%s",\n' "$OAUTH_CREDS"
124
+ printf ' "keychainService": "%s",\n' "$KEYCHAIN_SERVICE"
125
+ printf ' "keychainAccount": "%s"\n' "$KEYCHAIN_ACCOUNT"
126
+ printf '}\n'
127
+ } >"$dir/metadata.json"
128
+ chmod 600 "$dir/metadata.json" 2>/dev/null || true
129
+
130
+ printf '%s\n' "$profile" >"$STORE_DIR/current_profile"
131
+ chmod 600 "$STORE_DIR/current_profile" 2>/dev/null || true
132
+ info "Saved profile '$profile'."
133
+ }
134
+
135
+ switch_profile() {
136
+ local profile dir backup_dir
137
+ profile="$(validate_profile "$1")"
138
+ dir="$(profile_dir "$profile")"
139
+
140
+ [[ -d "$dir" ]] || die "profile '$profile' not found"
141
+ [[ -f "$dir/oauth_creds.json" ]] || die "profile '$profile' missing oauth_creds.json"
142
+ [[ -f "$dir/keychain_secret" ]] || die "profile '$profile' missing keychain_secret"
143
+
144
+ ensure_store
145
+ backup_dir="$(save_current_backup)"
146
+
147
+ cp "$dir/oauth_creds.json" "$OAUTH_CREDS"
148
+ chmod 600 "$OAUTH_CREDS" 2>/dev/null || true
149
+ write_keychain_secret "$dir/keychain_secret"
150
+
151
+ printf '%s\n' "$profile" >"$STORE_DIR/current_profile"
152
+ chmod 600 "$STORE_DIR/current_profile" 2>/dev/null || true
153
+
154
+ info "Switched Antigravity auth to '$profile'."
155
+ info "Backup of previous auth: $backup_dir"
156
+ info "Run a new agy process for the switch to take effect."
157
+ }
158
+
159
+ list_profiles() {
160
+ ensure_store
161
+ local current=""
162
+ [[ -f "$STORE_DIR/current_profile" ]] && current="$(tr -d '\n' <"$STORE_DIR/current_profile")"
163
+
164
+ local found=0 dir name marker
165
+ for dir in "$PROFILES_DIR"/*; do
166
+ [[ -d "$dir" ]] || continue
167
+ found=1
168
+ name="$(basename "$dir")"
169
+ marker=""
170
+ [[ "$name" == "$current" ]] && marker=" *"
171
+ printf '%s%s\n' "$name" "$marker"
172
+ done
173
+ [[ "$found" -eq 1 ]] || info "No profiles saved."
174
+ }
175
+
176
+ show_current() {
177
+ if [[ -f "$STORE_DIR/current_profile" ]]; then
178
+ tr -d '\n' <"$STORE_DIR/current_profile"
179
+ printf '\n'
180
+ else
181
+ info "No active profile marker."
182
+ fi
183
+ }
184
+
185
+ show_paths() {
186
+ cat <<EOF
187
+ store: $STORE_DIR
188
+ profiles: $PROFILES_DIR
189
+ backups: $BACKUPS_DIR
190
+ oauth_creds: $OAUTH_CREDS
191
+ keychain_service: $KEYCHAIN_SERVICE
192
+ keychain_account: $KEYCHAIN_ACCOUNT
193
+ EOF
194
+ }
195
+
196
+ cmd="${1:-}"
197
+ alias_cmd=0
198
+ case "$(basename "$0")" in
199
+ switch-acc-agy-save)
200
+ cmd="save"
201
+ alias_cmd=1
202
+ ;;
203
+ switch-acc-agy-to)
204
+ cmd="to"
205
+ alias_cmd=1
206
+ ;;
207
+ esac
208
+
209
+ case "$cmd" in
210
+ save)
211
+ [[ "$alias_cmd" -eq 1 ]] || shift || true
212
+ [[ $# -eq 1 ]] || { usage; exit 2; }
213
+ save_profile "$1"
214
+ ;;
215
+ to|switch)
216
+ [[ "$alias_cmd" -eq 1 ]] || shift || true
217
+ [[ $# -eq 1 ]] || { usage; exit 2; }
218
+ switch_profile "$1"
219
+ ;;
220
+ list|ls)
221
+ list_profiles
222
+ ;;
223
+ current)
224
+ show_current
225
+ ;;
226
+ paths)
227
+ show_paths
228
+ ;;
229
+ help|-h|--help|"")
230
+ usage
231
+ ;;
232
+ *)
233
+ usage
234
+ exit 2
235
+ ;;
236
+ esac