@first087/agys 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.
- package/README.md +44 -0
- package/index.ts +30 -0
- package/package.json +49 -0
- package/src/commands/add.test.ts +15 -0
- package/src/commands/add.ts +22 -0
- package/src/commands/list.test.ts +15 -0
- package/src/commands/list.ts +35 -0
- package/src/commands/switch.test.ts +20 -0
- package/src/commands/switch.ts +51 -0
- package/src/utils/fileOperations.ts +38 -0
package/README.md
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# agys
|
|
2
|
+
|
|
3
|
+
Manage multiple antigravity-cli oauth token accounts with ease.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Add Accounts**: Easily add and name new OAuth token accounts.
|
|
8
|
+
- **List Accounts**: View all managed accounts and see which one is currently active.
|
|
9
|
+
- **Interactive Switch**: Switch between accounts using an interactive CLI menu.
|
|
10
|
+
- **Persistent State**: Keeps track of the active account automatically.
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
npm install -g agys
|
|
16
|
+
# or
|
|
17
|
+
bun install -g agys
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Usage
|
|
21
|
+
|
|
22
|
+
### Add a new account
|
|
23
|
+
```bash
|
|
24
|
+
agys add <name>
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
### List all accounts
|
|
28
|
+
```bash
|
|
29
|
+
agys list
|
|
30
|
+
# or
|
|
31
|
+
agys ls
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
### Switch account
|
|
35
|
+
Run `agys` without any arguments to open the interactive selection menu:
|
|
36
|
+
```bash
|
|
37
|
+
agys
|
|
38
|
+
# or explicitly
|
|
39
|
+
agys switch
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## License
|
|
43
|
+
|
|
44
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import packageJson from './package.json' with { type: 'json' };
|
|
4
|
+
import { addCommand } from './src/commands/add';
|
|
5
|
+
import { listCommand, lsCommand } from './src/commands/list';
|
|
6
|
+
import { switchCommand, handleSwitch } from './src/commands/switch';
|
|
7
|
+
|
|
8
|
+
const program = new Command();
|
|
9
|
+
|
|
10
|
+
program
|
|
11
|
+
.name('agys')
|
|
12
|
+
.description('Manage multiple antigravity-cli oauth token accounts')
|
|
13
|
+
.version(packageJson.version);
|
|
14
|
+
|
|
15
|
+
program.addCommand(addCommand);
|
|
16
|
+
program.addCommand(listCommand);
|
|
17
|
+
program.addCommand(lsCommand);
|
|
18
|
+
program.addCommand(switchCommand);
|
|
19
|
+
|
|
20
|
+
// Set description for switch command explicitly
|
|
21
|
+
switchCommand.description('Switch to a different account (run agys without arguments)');
|
|
22
|
+
|
|
23
|
+
// Set default action if no command is provided
|
|
24
|
+
program.action(async () => {
|
|
25
|
+
if (program.args.length === 0) {
|
|
26
|
+
await handleSwitch();
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
program.parse(process.argv);
|
package/package.json
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@first087/agys",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Manage multiple antigravity-cli oauth token accounts",
|
|
5
|
+
"author": "Artit Kiuwilai",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "https://github.com/first087/agy-switch.git"
|
|
9
|
+
},
|
|
10
|
+
"engines": {
|
|
11
|
+
"node": ">=18.0.0"
|
|
12
|
+
},
|
|
13
|
+
"module": "index.ts",
|
|
14
|
+
"type": "module",
|
|
15
|
+
"bin": {
|
|
16
|
+
"agys": "index.ts"
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"src",
|
|
20
|
+
"index.ts"
|
|
21
|
+
],
|
|
22
|
+
"scripts": {
|
|
23
|
+
"start": "bun run index.ts",
|
|
24
|
+
"test": "bun test",
|
|
25
|
+
"build": "rm -rf dist && bun build index.ts --outdir ./dist --target bun --minify",
|
|
26
|
+
"lint": "bunx tsc --noEmit",
|
|
27
|
+
"publish:npm": "bun run build && npm publish --access public"
|
|
28
|
+
},
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"keywords": [
|
|
31
|
+
"cli",
|
|
32
|
+
"antigravity",
|
|
33
|
+
"oauth",
|
|
34
|
+
"token-manager"
|
|
35
|
+
],
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@types/bun": "latest",
|
|
38
|
+
"@types/fs-extra": "^11.0.4"
|
|
39
|
+
},
|
|
40
|
+
"peerDependencies": {
|
|
41
|
+
"typescript": "^5"
|
|
42
|
+
},
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"chalk": "^5.6.2",
|
|
45
|
+
"commander": "^15.0.0",
|
|
46
|
+
"fs-extra": "^11.3.5",
|
|
47
|
+
"inquirer": "^14.0.2"
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { expect, test, spyOn } from "bun:test";
|
|
2
|
+
import * as fileOps from "../utils/fileOperations";
|
|
3
|
+
|
|
4
|
+
// Mocking the utility function
|
|
5
|
+
spyOn(fileOps, "copyTokenToAccount").mockImplementation(() => Promise.resolve('/home/user/.agys/test-account'));
|
|
6
|
+
|
|
7
|
+
test("add command calls copyTokenToAccount with correct arguments", async () => {
|
|
8
|
+
// Logic to test will be added as we implement the command
|
|
9
|
+
const accountName = "test-account";
|
|
10
|
+
const sourcePath = "/home/user/.gemini/antigravity-cli/antigravity-oauth-token";
|
|
11
|
+
|
|
12
|
+
await fileOps.copyTokenToAccount(accountName, sourcePath);
|
|
13
|
+
|
|
14
|
+
expect(fileOps.copyTokenToAccount).toHaveBeenCalledWith(accountName, sourcePath);
|
|
15
|
+
});
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import * as fileOps from "../utils/fileOperations";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
import os from "os";
|
|
5
|
+
import path from "path";
|
|
6
|
+
|
|
7
|
+
export const addCommand = new Command("add")
|
|
8
|
+
.description("Add a new account token")
|
|
9
|
+
.argument("[name]", "account name") // Change <name> to [name] to make it optional
|
|
10
|
+
.action(async (name) => {
|
|
11
|
+
if (!name) {
|
|
12
|
+
addCommand.outputHelp();
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
const sourcePath = path.join(os.homedir(), ".gemini", "antigravity-cli", "antigravity-oauth-token");
|
|
16
|
+
try {
|
|
17
|
+
await fileOps.copyTokenToAccount(name, sourcePath);
|
|
18
|
+
console.log(chalk.green(`Account '${name}' added successfully.`));
|
|
19
|
+
} catch (error) {
|
|
20
|
+
console.error(chalk.red(`Failed to add account '${name}':`), error);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { expect, test, spyOn } from "bun:test";
|
|
2
|
+
import * as fileOps from "../utils/fileOperations";
|
|
3
|
+
|
|
4
|
+
// Mocking the utility functions
|
|
5
|
+
spyOn(fileOps, "getAccounts").mockImplementation(() => Promise.resolve(['test', 'test2', 'test3']));
|
|
6
|
+
spyOn(fileOps, "getActiveAccount").mockImplementation(() => Promise.resolve('test'));
|
|
7
|
+
|
|
8
|
+
test("list command returns all accounts with active one marked", async () => {
|
|
9
|
+
const accounts = await fileOps.getAccounts();
|
|
10
|
+
const active = await fileOps.getActiveAccount();
|
|
11
|
+
|
|
12
|
+
expect(accounts).toContain('test');
|
|
13
|
+
expect(accounts).toContain('test2');
|
|
14
|
+
expect(active).toBe('test');
|
|
15
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import * as fileOps from "../utils/fileOperations";
|
|
3
|
+
import chalk from "chalk";
|
|
4
|
+
|
|
5
|
+
async function listAction() {
|
|
6
|
+
try {
|
|
7
|
+
const accounts = await fileOps.getAccounts();
|
|
8
|
+
const active = await fileOps.getActiveAccount();
|
|
9
|
+
|
|
10
|
+
if (accounts.length === 0) {
|
|
11
|
+
console.log(chalk.yellow("No accounts found. Use 'add' to create one."));
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
console.log(chalk.blue("Accounts:"));
|
|
16
|
+
accounts.forEach((account: string) => {
|
|
17
|
+
if (account === active) {
|
|
18
|
+
console.log(chalk.green(`* ${account} (active)`));
|
|
19
|
+
} else {
|
|
20
|
+
console.log(` ${account}`);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
} catch (error) {
|
|
24
|
+
console.error(chalk.red("Failed to list accounts:"), error);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export const listCommand = new Command("list")
|
|
29
|
+
.description("List all accounts")
|
|
30
|
+
.action(listAction);
|
|
31
|
+
|
|
32
|
+
export const lsCommand = new Command("ls")
|
|
33
|
+
.description("Alias for list")
|
|
34
|
+
.action(listAction);
|
|
35
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { expect, test, spyOn } from "bun:test";
|
|
2
|
+
import * as fileOps from "../utils/fileOperations";
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
|
|
5
|
+
// Mocking the utility functions and inquirer
|
|
6
|
+
spyOn(fileOps, "getAccounts").mockImplementation(() => Promise.resolve(['test', 'test2', 'test3']));
|
|
7
|
+
// @ts-ignore
|
|
8
|
+
spyOn(inquirer, 'prompt').mockImplementation(() => Promise.resolve({ selectedAccount: 'test2' }));
|
|
9
|
+
|
|
10
|
+
test("switch interactive menu returns selected account", async () => {
|
|
11
|
+
const accounts = await fileOps.getAccounts();
|
|
12
|
+
const answers = await inquirer.prompt([{
|
|
13
|
+
type: 'list',
|
|
14
|
+
name: 'selectedAccount',
|
|
15
|
+
message: 'Select account:',
|
|
16
|
+
choices: accounts
|
|
17
|
+
}]);
|
|
18
|
+
|
|
19
|
+
expect(answers.selectedAccount).toBe('test2');
|
|
20
|
+
});
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { Command } from "commander";
|
|
2
|
+
import * as fileOps from "../utils/fileOperations";
|
|
3
|
+
import inquirer from 'inquirer';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import path from 'path';
|
|
6
|
+
import os from 'os';
|
|
7
|
+
import fs from 'fs-extra';
|
|
8
|
+
|
|
9
|
+
export async function handleSwitch() {
|
|
10
|
+
try {
|
|
11
|
+
const accounts = await fileOps.getAccounts();
|
|
12
|
+
const active = await fileOps.getActiveAccount();
|
|
13
|
+
|
|
14
|
+
if (accounts.length === 0) {
|
|
15
|
+
console.log(chalk.yellow("No accounts found. Use 'agys add' to create one."));
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const { selectedAccount } = await inquirer.prompt([{
|
|
20
|
+
type: 'select',
|
|
21
|
+
name: 'selectedAccount',
|
|
22
|
+
message: 'Select account (Press Ctrl+C to cancel):',
|
|
23
|
+
choices: accounts.map((account: string) => ({
|
|
24
|
+
name: account === active ? `${account} (active)` : account,
|
|
25
|
+
value: account
|
|
26
|
+
})),
|
|
27
|
+
default: active
|
|
28
|
+
}]).catch(() => {
|
|
29
|
+
console.log(chalk.yellow("\nSwitching cancelled."));
|
|
30
|
+
process.exit(0);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
if (selectedAccount !== active) {
|
|
34
|
+
const sourcePath = path.join(os.homedir(), '.agys', selectedAccount);
|
|
35
|
+
const destPath = path.join(os.homedir(), '.gemini', 'antigravity-cli', 'antigravity-oauth-token');
|
|
36
|
+
|
|
37
|
+
// Use fs.copy to copy from source to dest (token file)
|
|
38
|
+
await fs.copy(sourcePath, destPath);
|
|
39
|
+
await fileOps.setActiveAccount(selectedAccount);
|
|
40
|
+
console.log(chalk.green(`Switched to account '${selectedAccount}'`));
|
|
41
|
+
} else {
|
|
42
|
+
console.log(chalk.blue(`Already using account '${selectedAccount}'`));
|
|
43
|
+
}
|
|
44
|
+
} catch (error) {
|
|
45
|
+
console.error(chalk.red("Failed to switch account:"), error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const switchCommand = new Command("switch")
|
|
50
|
+
.description("Switch to a different account")
|
|
51
|
+
.action(handleSwitch);
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import fs from 'fs-extra';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
|
|
5
|
+
const CONFIG_DIR = path.join(os.homedir(), '.agys');
|
|
6
|
+
const ACTIVE_FILE = path.join(CONFIG_DIR, '.active');
|
|
7
|
+
|
|
8
|
+
export async function ensureConfigDir() {
|
|
9
|
+
await fs.ensureDir(CONFIG_DIR);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function copyTokenToAccount(accountName: string, sourcePath: string) {
|
|
13
|
+
const destPath = path.join(CONFIG_DIR, accountName);
|
|
14
|
+
await fs.ensureDir(CONFIG_DIR);
|
|
15
|
+
await fs.copy(sourcePath, destPath);
|
|
16
|
+
return destPath;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export async function getAccounts() {
|
|
20
|
+
await ensureConfigDir();
|
|
21
|
+
const files = await fs.readdir(CONFIG_DIR);
|
|
22
|
+
return files.filter((file: string) => file !== '.active');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function setActiveAccount(accountName: string) {
|
|
26
|
+
await ensureConfigDir();
|
|
27
|
+
await fs.writeFile(ACTIVE_FILE, accountName, 'utf-8');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function getActiveAccount() {
|
|
31
|
+
await ensureConfigDir();
|
|
32
|
+
|
|
33
|
+
if (!fs.existsSync(ACTIVE_FILE)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return await fs.readFile(ACTIVE_FILE, 'utf-8');
|
|
38
|
+
}
|