gemix 0.0.2 → 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/{doc/README.md → README.md} +11 -0
- package/dist/auth/oauth.d.ts +13 -0
- package/dist/auth/oauth.d.ts.map +1 -1
- package/dist/auth/oauth.js +63 -0
- package/dist/auth/oauth.js.map +1 -1
- package/dist/commands/dash.d.ts +2 -0
- package/dist/commands/dash.d.ts.map +1 -0
- package/dist/commands/dash.js +125 -0
- package/dist/commands/dash.js.map +1 -0
- package/dist/index.js +15 -1
- package/dist/index.js.map +1 -1
- package/dist/server/public/app.js +181 -0
- package/dist/server/public/index.html +77 -0
- package/dist/server/public/style.css +132 -0
- package/dist/server/quota.d.ts +22 -0
- package/dist/server/quota.d.ts.map +1 -0
- package/dist/server/quota.js +69 -0
- package/dist/server/quota.js.map +1 -0
- package/dist/server/server.d.ts +11 -0
- package/dist/server/server.d.ts.map +1 -0
- package/dist/server/server.js +170 -0
- package/dist/server/server.js.map +1 -0
- package/package.json +13 -7
- package/src/auth/oauth.ts +0 -194
- package/src/commands/current.ts +0 -18
- package/src/commands/list.ts +0 -20
- package/src/commands/register.ts +0 -53
- package/src/commands/remove.ts +0 -37
- package/src/commands/use.ts +0 -25
- package/src/index.ts +0 -59
- package/src/storage/accounts.ts +0 -285
- package/src/storage/paths.ts +0 -56
- package/src/utils/table.ts +0 -58
- package/src/utils/validation.ts +0 -27
- package/tsconfig.json +0 -20
|
@@ -39,6 +39,13 @@ gemix current
|
|
|
39
39
|
|
|
40
40
|
# Remove an account
|
|
41
41
|
gemix remove personal
|
|
42
|
+
|
|
43
|
+
# Open local web dashboard
|
|
44
|
+
gemix dash
|
|
45
|
+
|
|
46
|
+
# Check dashboard status or stop it
|
|
47
|
+
gemix dash status
|
|
48
|
+
gemix dash stop
|
|
42
49
|
```
|
|
43
50
|
|
|
44
51
|
## Commands
|
|
@@ -51,6 +58,10 @@ gemix remove personal
|
|
|
51
58
|
| `gemix use <username>` | Switch active account |
|
|
52
59
|
| `gemix current` | Show active account |
|
|
53
60
|
| `gemix remove <username>` | Remove an account |
|
|
61
|
+
| `gemix dash` | Launch local web dashboard at `http://127.0.0.1:4577` |
|
|
62
|
+
| `gemix dash stop` | Stop running dashboard server |
|
|
63
|
+
| `gemix dash status` | Check dashboard status and PID |
|
|
64
|
+
| `gemix dash -p <port>` | Launch dashboard on a custom port |
|
|
54
65
|
|
|
55
66
|
## How It Works
|
|
56
67
|
|
package/dist/auth/oauth.d.ts
CHANGED
|
@@ -5,6 +5,19 @@ export interface OAuthResult {
|
|
|
5
5
|
name: string;
|
|
6
6
|
picture?: string;
|
|
7
7
|
}
|
|
8
|
+
/**
|
|
9
|
+
* Build a Google OAuth URL for the web/dashboard flow.
|
|
10
|
+
* Returns the URL and the codeVerifier needed to exchange the code later.
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildAuthUrl(): {
|
|
13
|
+
url: string;
|
|
14
|
+
codeVerifier: string;
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* Exchange an authorization code for tokens + user info.
|
|
18
|
+
* Used by the dashboard web flow after the user pastes the code.
|
|
19
|
+
*/
|
|
20
|
+
export declare function exchangeCode(code: string, codeVerifier: string): Promise<OAuthResult>;
|
|
8
21
|
export declare function authenticateWithGoogle(): Promise<OAuthResult>;
|
|
9
22
|
export declare function refreshTokens(tokens: TokenData): Promise<TokenData | null>;
|
|
10
23
|
//# sourceMappingURL=oauth.d.ts.map
|
package/dist/auth/oauth.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/auth/oauth.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AA0BxD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,SAAS,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,WAAW,CAAC,CAuFnE;AAoCD,wBAAsB,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAkChF"}
|
|
1
|
+
{"version":3,"file":"oauth.d.ts","sourceRoot":"","sources":["../../src/auth/oauth.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AA0BxD,MAAM,WAAW,WAAW;IAC1B,MAAM,EAAE,SAAS,CAAC;IAClB,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED;;;GAGG;AACH,wBAAgB,YAAY,IAAI;IAAE,GAAG,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,CAAA;CAAE,CAepE;AAED;;;GAGG;AACH,wBAAsB,YAAY,CAChC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,MAAM,GACnB,OAAO,CAAC,WAAW,CAAC,CAgDtB;AAED,wBAAsB,sBAAsB,IAAI,OAAO,CAAC,WAAW,CAAC,CAuFnE;AAoCD,wBAAsB,aAAa,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC,SAAS,GAAG,IAAI,CAAC,CAkChF"}
|
package/dist/auth/oauth.js
CHANGED
|
@@ -18,6 +18,69 @@ function generateCodeVerifier() {
|
|
|
18
18
|
function generateCodeChallenge(verifier) {
|
|
19
19
|
return crypto.createHash('sha256').update(verifier).digest('base64url');
|
|
20
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Build a Google OAuth URL for the web/dashboard flow.
|
|
23
|
+
* Returns the URL and the codeVerifier needed to exchange the code later.
|
|
24
|
+
*/
|
|
25
|
+
export function buildAuthUrl() {
|
|
26
|
+
const state = crypto.randomBytes(32).toString('hex');
|
|
27
|
+
const codeVerifier = generateCodeVerifier();
|
|
28
|
+
const params = new URLSearchParams({
|
|
29
|
+
client_id: OAUTH_CLIENT_ID,
|
|
30
|
+
response_type: 'code',
|
|
31
|
+
redirect_uri: REDIRECT_URI,
|
|
32
|
+
scope: OAUTH_SCOPE.join(' '),
|
|
33
|
+
access_type: 'offline',
|
|
34
|
+
prompt: 'consent',
|
|
35
|
+
state,
|
|
36
|
+
code_challenge: generateCodeChallenge(codeVerifier),
|
|
37
|
+
code_challenge_method: 'S256',
|
|
38
|
+
});
|
|
39
|
+
return { url: `https://accounts.google.com/o/oauth2/auth?${params}`, codeVerifier };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Exchange an authorization code for tokens + user info.
|
|
43
|
+
* Used by the dashboard web flow after the user pastes the code.
|
|
44
|
+
*/
|
|
45
|
+
export async function exchangeCode(code, codeVerifier) {
|
|
46
|
+
const startTime = Date.now();
|
|
47
|
+
const tokenResponse = await fetch('https://oauth2.googleapis.com/token', {
|
|
48
|
+
method: 'POST',
|
|
49
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8' },
|
|
50
|
+
body: new URLSearchParams({
|
|
51
|
+
client_id: OAUTH_CLIENT_ID,
|
|
52
|
+
client_secret: OAUTH_CLIENT_SECRET,
|
|
53
|
+
code,
|
|
54
|
+
grant_type: 'authorization_code',
|
|
55
|
+
redirect_uri: REDIRECT_URI,
|
|
56
|
+
code_verifier: codeVerifier,
|
|
57
|
+
}),
|
|
58
|
+
});
|
|
59
|
+
if (!tokenResponse.ok) {
|
|
60
|
+
throw new Error(`Token exchange failed: ${tokenResponse.status} ${await tokenResponse.text()}`);
|
|
61
|
+
}
|
|
62
|
+
const t = (await tokenResponse.json());
|
|
63
|
+
const userInfoResponse = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
|
|
64
|
+
headers: { Authorization: `Bearer ${t.access_token}` },
|
|
65
|
+
});
|
|
66
|
+
if (!userInfoResponse.ok) {
|
|
67
|
+
throw new Error(`Failed to fetch user info: ${userInfoResponse.status}`);
|
|
68
|
+
}
|
|
69
|
+
const u = (await userInfoResponse.json());
|
|
70
|
+
return {
|
|
71
|
+
tokens: {
|
|
72
|
+
access_token: t.access_token,
|
|
73
|
+
refresh_token: t.refresh_token,
|
|
74
|
+
id_token: '',
|
|
75
|
+
scope: t.scope || OAUTH_SCOPE.join(' '),
|
|
76
|
+
token_type: t.token_type || 'Bearer',
|
|
77
|
+
expiry_date: startTime + t.expires_in * 1000,
|
|
78
|
+
},
|
|
79
|
+
email: u.email,
|
|
80
|
+
name: u.name,
|
|
81
|
+
picture: u.picture,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
21
84
|
export async function authenticateWithGoogle() {
|
|
22
85
|
const state = crypto.randomBytes(32).toString('hex');
|
|
23
86
|
const codeVerifier = generateCodeVerifier();
|
package/dist/auth/oauth.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../src/auth/oauth.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"oauth.js","sourceRoot":"","sources":["../../src/auth/oauth.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dash.d.ts","sourceRoot":"","sources":["../../src/commands/dash.ts"],"names":[],"mappings":"AA8DA,wBAAsB,WAAW,CAC/B,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,IAAI,CAAC,EAAE,MAAM,GACZ,OAAO,CAAC,IAAI,CAAC,CA0Ef"}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `gemix dash` command — start / stop / status the local web dashboard.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* gemix dash # start server + open browser
|
|
6
|
+
* gemix dash stop # stop running server
|
|
7
|
+
* gemix dash status # show status
|
|
8
|
+
* gemix dash -p 8080 # custom port
|
|
9
|
+
*/
|
|
10
|
+
import { createServer } from 'node:net';
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import { readFileSync, writeFileSync, rmSync } from 'node:fs';
|
|
13
|
+
import { tmpdir } from 'node:os';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
const PID_FILE = path.join(tmpdir(), 'gemix-dash.pid');
|
|
16
|
+
function readPid() {
|
|
17
|
+
try {
|
|
18
|
+
return JSON.parse(readFileSync(PID_FILE, 'utf8'));
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function writePid(pid, port) {
|
|
25
|
+
writeFileSync(PID_FILE, JSON.stringify({ pid, port }), 'utf8');
|
|
26
|
+
}
|
|
27
|
+
function removePid() {
|
|
28
|
+
try {
|
|
29
|
+
rmSync(PID_FILE);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* already gone */
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
/** Open a URL in the default browser (cross-platform). */
|
|
36
|
+
function openBrowser(url) {
|
|
37
|
+
const cmd = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'start' : 'xdg-open';
|
|
38
|
+
spawn(cmd, [url], { detached: true, stdio: 'ignore' }).unref();
|
|
39
|
+
}
|
|
40
|
+
/** Check whether a port is already listening. */
|
|
41
|
+
function portInUse(port) {
|
|
42
|
+
return new Promise((resolve) => {
|
|
43
|
+
const s = createServer();
|
|
44
|
+
s.once('error', () => resolve(true));
|
|
45
|
+
s.once('listening', () => {
|
|
46
|
+
s.close();
|
|
47
|
+
resolve(false);
|
|
48
|
+
});
|
|
49
|
+
s.listen(port, '127.0.0.1');
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
export async function dashCommand(action, port) {
|
|
53
|
+
// ── stop ───────────────────────────────────────────────────────────────────
|
|
54
|
+
if (action === 'stop') {
|
|
55
|
+
const info = readPid();
|
|
56
|
+
if (!info) {
|
|
57
|
+
console.log('gemix dash is not running.');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
process.kill(info.pid, 'SIGTERM');
|
|
62
|
+
removePid();
|
|
63
|
+
console.log(`✓ gemix dash (pid ${info.pid}) stopped.`);
|
|
64
|
+
}
|
|
65
|
+
catch {
|
|
66
|
+
console.log(`Process ${info.pid} not found — cleaning up stale PID file.`);
|
|
67
|
+
removePid();
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// ── status ─────────────────────────────────────────────────────────────────
|
|
72
|
+
if (action === 'status') {
|
|
73
|
+
const info = readPid();
|
|
74
|
+
if (!info) {
|
|
75
|
+
console.log('gemix dash is not running.');
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
process.kill(info.pid, 0); // throws if process is gone
|
|
80
|
+
console.log(`gemix dash is running pid=${info.pid} http://127.0.0.1:${info.port}`);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
console.log('gemix dash is not running (stale PID file removed).');
|
|
84
|
+
removePid();
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
// ── start ──────────────────────────────────────────────────────────────────
|
|
89
|
+
// Dynamic import so the server module is only loaded when we actually start.
|
|
90
|
+
const { server, PORT: DEFAULT_PORT } = await import('../server/server.js');
|
|
91
|
+
const listenPort = port ?? Number(process.env.PORT || DEFAULT_PORT);
|
|
92
|
+
if (await portInUse(listenPort)) {
|
|
93
|
+
const info = readPid();
|
|
94
|
+
if (info && info.port === listenPort) {
|
|
95
|
+
console.log(`gemix dash is already running at http://127.0.0.1:${listenPort}`);
|
|
96
|
+
openBrowser(`http://127.0.0.1:${listenPort}`);
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
console.error(`Error: port ${listenPort} is already in use by another process.`);
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
server.listen(listenPort, '127.0.0.1', () => {
|
|
103
|
+
const url = `http://127.0.0.1:${listenPort}`;
|
|
104
|
+
console.log(`gemix dash running at ${url}`);
|
|
105
|
+
console.log(`Run 'gemix dash stop' to shut it down.`);
|
|
106
|
+
writePid(process.pid, listenPort);
|
|
107
|
+
openBrowser(url);
|
|
108
|
+
});
|
|
109
|
+
// Clean up PID file on exit
|
|
110
|
+
const cleanup = () => {
|
|
111
|
+
const info = readPid();
|
|
112
|
+
if (info && info.pid === process.pid)
|
|
113
|
+
removePid();
|
|
114
|
+
};
|
|
115
|
+
process.on('exit', cleanup);
|
|
116
|
+
process.on('SIGINT', () => {
|
|
117
|
+
cleanup();
|
|
118
|
+
process.exit(0);
|
|
119
|
+
});
|
|
120
|
+
process.on('SIGTERM', () => {
|
|
121
|
+
cleanup();
|
|
122
|
+
process.exit(0);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
//# sourceMappingURL=dash.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dash.js","sourceRoot":"","sources":["../../src/commands/dash.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -5,11 +5,12 @@ import { listCommand } from './commands/list.js';
|
|
|
5
5
|
import { useCommand } from './commands/use.js';
|
|
6
6
|
import { currentCommand } from './commands/current.js';
|
|
7
7
|
import { removeCommand } from './commands/remove.js';
|
|
8
|
+
import { dashCommand } from './commands/dash.js';
|
|
8
9
|
const program = new Command();
|
|
9
10
|
program
|
|
10
11
|
.name('gemix')
|
|
11
12
|
.description('Gemini multi-account CLI - manage multiple Google accounts')
|
|
12
|
-
.version('1.0
|
|
13
|
+
.version('0.1.0');
|
|
13
14
|
program
|
|
14
15
|
.command('register')
|
|
15
16
|
.description('Register a new Google account')
|
|
@@ -47,5 +48,18 @@ program
|
|
|
47
48
|
.action(async (username, options) => {
|
|
48
49
|
await removeCommand(username, options.yes);
|
|
49
50
|
});
|
|
51
|
+
program
|
|
52
|
+
.command('dash [action]')
|
|
53
|
+
.description('Open the gemix web dashboard (action: stop | status)')
|
|
54
|
+
.option('-p, --port <number>', 'Port to listen on (default: 4577)')
|
|
55
|
+
.addHelpText('after', `
|
|
56
|
+
Examples:
|
|
57
|
+
gemix dash Start the dashboard and open it in your browser
|
|
58
|
+
gemix dash stop Stop the running dashboard server
|
|
59
|
+
gemix dash status Show whether the dashboard is running
|
|
60
|
+
gemix dash -p 8080 Start on a custom port`)
|
|
61
|
+
.action(async (action, options) => {
|
|
62
|
+
await dashCommand(action, options.port ? Number(options.port) : undefined);
|
|
63
|
+
});
|
|
50
64
|
program.parse();
|
|
51
65
|
//# sourceMappingURL=index.js.map
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAEA,OAAO,EAAE,OAAO,EAAE,MAAM,WAAW,CAAC;AACpC,OAAO,EAAE,eAAe,EAAE,MAAM,wBAAwB,CAAC;AACzD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAC;AACvD,OAAO,EAAE,aAAa,EAAE,MAAM,sBAAsB,CAAC;AACrD,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAEjD,MAAM,OAAO,GAAG,IAAI,OAAO,EAAE,CAAC;AAE9B,OAAO;KACJ,IAAI,CAAC,OAAO,CAAC;KACb,WAAW,CAAC,4DAA4D,CAAC;KACzE,OAAO,CAAC,OAAO,CAAC,CAAC;AAEpB,OAAO;KACJ,OAAO,CAAC,UAAU,CAAC;KACnB,WAAW,CAAC,+BAA+B,CAAC;KAC5C,QAAQ,CAAC,YAAY,EAAE,kCAAkC,CAAC;KAC1D,MAAM,CAAC,KAAK,EAAE,QAAgB,EAAE,EAAE;IACjC,MAAM,eAAe,CAAC,QAAQ,CAAC,CAAC;AAClC,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,MAAM,CAAC;KACf,KAAK,CAAC,IAAI,CAAC;KACX,WAAW,CAAC,8BAA8B,CAAC;KAC3C,MAAM,CAAC,YAAY,EAAE,uBAAuB,CAAC;KAC7C,MAAM,CAAC,KAAK,EAAE,OAA2B,EAAE,EAAE;IAC5C,MAAM,WAAW,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;AAClC,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,KAAK,CAAC;KACd,WAAW,CAAC,+BAA+B,CAAC;KAC5C,QAAQ,CAAC,YAAY,EAAE,sCAAsC,CAAC;KAC9D,MAAM,CAAC,KAAK,EAAE,QAAgB,EAAE,EAAE;IACjC,MAAM,UAAU,CAAC,QAAQ,CAAC,CAAC;AAC7B,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,SAAS,CAAC;KAClB,WAAW,CAAC,mCAAmC,CAAC;KAChD,MAAM,CAAC,KAAK,IAAI,EAAE;IACjB,MAAM,cAAc,EAAE,CAAC;AACzB,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,QAAQ,CAAC;KACjB,KAAK,CAAC,IAAI,CAAC;KACX,WAAW,CAAC,6BAA6B,CAAC;KAC1C,QAAQ,CAAC,YAAY,EAAE,mCAAmC,CAAC;KAC3D,MAAM,CAAC,WAAW,EAAE,mBAAmB,CAAC;KACxC,MAAM,CAAC,KAAK,EAAE,QAAgB,EAAE,OAA0B,EAAE,EAAE;IAC7D,MAAM,aAAa,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC;AAC7C,CAAC,CAAC,CAAC;AAEL,OAAO;KACJ,OAAO,CAAC,eAAe,CAAC;KACxB,WAAW,CAAC,sDAAsD,CAAC;KACnE,MAAM,CAAC,qBAAqB,EAAE,mCAAmC,CAAC;KAClE,WAAW,CACV,OAAO,EACP;;;;;+CAK2C,CAC5C;KACA,MAAM,CAAC,KAAK,EAAE,MAA0B,EAAE,OAA0B,EAAE,EAAE;IACvE,MAAM,WAAW,CAAC,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC;AAC7E,CAAC,CAAC,CAAC;AAEL,OAAO,CAAC,KAAK,EAAE,CAAC"}
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
const $ = (s) => document.querySelector(s);
|
|
2
|
+
const grid = $('#grid');
|
|
3
|
+
let authSession = null;
|
|
4
|
+
|
|
5
|
+
const esc = (s) => String(s ?? '').replace(/[&<>"']/g, (c) => ({ '&': '&', '<': '<', '>': '>', '"': '"', "'": ''' }[c]));
|
|
6
|
+
|
|
7
|
+
async function api(method, url, body) {
|
|
8
|
+
const r = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: body && JSON.stringify(body) });
|
|
9
|
+
const j = await r.json().catch(() => ({}));
|
|
10
|
+
if (!r.ok) throw new Error(j.error || r.statusText);
|
|
11
|
+
return j;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function toast(msg, err) {
|
|
15
|
+
const t = document.createElement('div');
|
|
16
|
+
t.className = 'toast' + (err ? ' err' : '');
|
|
17
|
+
t.textContent = msg;
|
|
18
|
+
$('#toasts').append(t);
|
|
19
|
+
setTimeout(() => t.remove(), 4000);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function confirmDialog(title, body, okLabel, destructive) {
|
|
23
|
+
$('#c-title').textContent = title;
|
|
24
|
+
$('#c-body').textContent = body;
|
|
25
|
+
const ok = $('#c-ok');
|
|
26
|
+
ok.textContent = okLabel;
|
|
27
|
+
ok.className = 'btn ' + (destructive ? 'destructive' : 'default');
|
|
28
|
+
const d = $('#dlg-confirm');
|
|
29
|
+
d.showModal();
|
|
30
|
+
return new Promise((res) => {
|
|
31
|
+
ok.onclick = () => { d.close(); res(true); };
|
|
32
|
+
d.onclose = () => res(false);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const fmtReset = (iso) => {
|
|
37
|
+
if (!iso) return '';
|
|
38
|
+
const ms = new Date(iso) - Date.now();
|
|
39
|
+
if (ms <= 0) return 'Resets now';
|
|
40
|
+
const t = Math.ceil(ms / 6e4), h = Math.floor(t / 60), m = t % 60;
|
|
41
|
+
return 'Resets in ' + (h >= 24 ? `${Math.floor(h / 24)}d ${h % 24}h` : h ? `${h}h ${m}m` : `${m}m`);
|
|
42
|
+
};
|
|
43
|
+
const barColor = (f) => (f > 0.5 ? 'var(--ok)' : f > 0.2 ? 'var(--warn)' : 'var(--destructive)');
|
|
44
|
+
|
|
45
|
+
const SEGS = 24;
|
|
46
|
+
function modelRow(m) {
|
|
47
|
+
const pct = Math.round(m.remaining * 100);
|
|
48
|
+
const on = Math.ceil(m.remaining * SEGS);
|
|
49
|
+
const cells = Array.from({ length: SEGS }, (_, i) => `<i${i < on ? ` style="background:${barColor(m.remaining)}"` : ''}></i>`).join('');
|
|
50
|
+
return `<div class="model" title="${esc(m.id)}">
|
|
51
|
+
<span class="lbl">${esc(m.name)}</span>
|
|
52
|
+
<span class="pct">${pct}<small>%</small></span>
|
|
53
|
+
<div class="seg">${cells}</div>
|
|
54
|
+
<span class="reset">${fmtReset(m.resetTime) || ' '}</span></div>`;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function health(q) {
|
|
58
|
+
if (q.status === 'relogin') return '<span class="badge bad">Re-login needed</span>';
|
|
59
|
+
if (q.status === 'error') return `<span class="badge warn" title="${esc(q.error)}">Quota unavailable</span>`;
|
|
60
|
+
return '<span class="badge ok">Token OK</span>';
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function card(a) {
|
|
64
|
+
const q = a.quota;
|
|
65
|
+
// Two pools: Gemini vs third-party (Claude, GPT-OSS). Show the tightest model of each.
|
|
66
|
+
const pool = (test) => (q.models || []).filter((m) => test(m.id)).sort((x, y) => x.remaining - y.remaining)[0];
|
|
67
|
+
const rows = [
|
|
68
|
+
['Gemini', pool((n) => /^gemini/i.test(n))],
|
|
69
|
+
['Claude & GPT', pool((n) => !/^gemini/i.test(n))],
|
|
70
|
+
].filter(([, m]) => m).map(([label, m]) => modelRow({ ...m, name: label }));
|
|
71
|
+
const body = q.status === 'ok' ? rows.join('') || '<span class="muted">No quota data</span>'
|
|
72
|
+
: q.status === 'relogin' ? '<span class="muted">Remove and add this account again.</span>'
|
|
73
|
+
: '<span class="muted">Could not load limits. Try Refresh.</span>';
|
|
74
|
+
return `<article class="card${a.active ? ' active' : ''}" data-u="${esc(a.username)}">
|
|
75
|
+
<div class="card-head">
|
|
76
|
+
<img class="avatar" src="${esc(a.picture)}" alt="" referrerpolicy="no-referrer">
|
|
77
|
+
<div class="who"><b>${esc(a.username)}</b><span>${esc(a.email)}</span></div>
|
|
78
|
+
${a.active ? '<span class="badge primary">Active</span>' : ''}
|
|
79
|
+
</div>
|
|
80
|
+
<div class="badges">${q.tier ? `<span class="badge">${esc(q.tier)}</span>` : ''}${health(q)}</div>
|
|
81
|
+
<div class="card-body">${body}</div>
|
|
82
|
+
<div class="card-foot">
|
|
83
|
+
<button class="btn ghost sm" data-act="rename">Rename</button>
|
|
84
|
+
<button class="btn ghost sm danger" data-act="remove">Remove</button>
|
|
85
|
+
<span class="grow"></span>
|
|
86
|
+
${a.active ? '<span class="in-use">In use</span>' : '<button class="btn outline sm use" data-act="switch">Use account</button>'}
|
|
87
|
+
</div></article>`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
async function load(force) {
|
|
91
|
+
if (!grid.children.length) grid.innerHTML = '<div class="skeleton"></div>'.repeat(3);
|
|
92
|
+
try {
|
|
93
|
+
const { accounts } = await api('GET', '/api/accounts' + (force ? '?refresh=1' : ''));
|
|
94
|
+
document.getElementById('summary').textContent = `${accounts.length} account${accounts.length === 1 ? '' : 's'}`;
|
|
95
|
+
grid.innerHTML = accounts.length ? accounts.map(card).join('') : '<div class="empty">No accounts yet. Click “Add account”.</div>';
|
|
96
|
+
} catch (e) { toast(e.message, true); }
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
grid.addEventListener('click', async (e) => {
|
|
100
|
+
const b = e.target.closest('[data-act]');
|
|
101
|
+
if (!b) return;
|
|
102
|
+
const u = b.closest('.card').dataset.u;
|
|
103
|
+
const act = b.dataset.act;
|
|
104
|
+
try {
|
|
105
|
+
if (act === 'switch') {
|
|
106
|
+
if (!(await confirmDialog(`Switch to ${u}?`, 'Replaces the login used by Gemini CLI and Antigravity. Running sessions may need a restart.', 'Switch'))) return;
|
|
107
|
+
await api('POST', `/api/accounts/${encodeURIComponent(u)}/switch`);
|
|
108
|
+
toast(`Switched to ${u}`);
|
|
109
|
+
}
|
|
110
|
+
if (act === 'remove') {
|
|
111
|
+
if (!(await confirmDialog(`Remove ${u}?`, 'Deletes the saved login from gemix. You can add it again later.', 'Remove', true))) return;
|
|
112
|
+
await api('DELETE', `/api/accounts/${encodeURIComponent(u)}`);
|
|
113
|
+
toast(`Removed ${u}`);
|
|
114
|
+
}
|
|
115
|
+
if (act === 'rename') {
|
|
116
|
+
$('#rn-name').value = u;
|
|
117
|
+
const d = $('#dlg-rename');
|
|
118
|
+
d.showModal();
|
|
119
|
+
$('#rn-ok').onclick = async () => {
|
|
120
|
+
try {
|
|
121
|
+
await api('PATCH', `/api/accounts/${encodeURIComponent(u)}`, { name: $('#rn-name').value.trim() });
|
|
122
|
+
d.close(); toast('Renamed'); // Theme: Auto follows the OS, Light/Dark pin it (remembered per browser).
|
|
123
|
+
function setTheme(t) {
|
|
124
|
+
if (t === 'system') delete document.documentElement.dataset.theme; else document.documentElement.dataset.theme = t;
|
|
125
|
+
try { t === 'system' ? localStorage.removeItem('theme') : localStorage.setItem('theme', t); } catch {}
|
|
126
|
+
document.querySelectorAll('[data-theme-set]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.themeSet === t)));
|
|
127
|
+
}
|
|
128
|
+
document.querySelectorAll('[data-theme-set]').forEach((b) => (b.onclick = () => setTheme(b.dataset.themeSet)));
|
|
129
|
+
setTheme(document.documentElement.dataset.theme || 'system');
|
|
130
|
+
|
|
131
|
+
load();
|
|
132
|
+
} catch (err) { toast(err.message, true); }
|
|
133
|
+
};
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
load(true);
|
|
137
|
+
} catch (err) { toast(err.message, true); }
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
document.addEventListener('click', (e) => { if (e.target.matches('[data-close]')) e.target.closest('dialog').close(); });
|
|
141
|
+
$('#refresh').onclick = () => load(true);
|
|
142
|
+
|
|
143
|
+
// Add flow: name -> link -> paste code
|
|
144
|
+
$('#add').onclick = () => {
|
|
145
|
+
authSession = null;
|
|
146
|
+
$('#add-name').value = ''; $('#add-code').value = '';
|
|
147
|
+
$('#add-link').classList.add('hidden');
|
|
148
|
+
$('#add-go').textContent = 'Get login link';
|
|
149
|
+
$('#dlg-add').showModal();
|
|
150
|
+
};
|
|
151
|
+
$('#copy-url').onclick = () => navigator.clipboard.writeText($('#add-url').value).then(() => toast('Link copied'));
|
|
152
|
+
$('#open-url').onclick = () => window.open($('#add-url').value, '_blank', 'noopener');
|
|
153
|
+
$('#add-go').onclick = async () => {
|
|
154
|
+
const name = $('#add-name').value.trim();
|
|
155
|
+
const btn = $('#add-go');
|
|
156
|
+
btn.disabled = true;
|
|
157
|
+
try {
|
|
158
|
+
if (!authSession) {
|
|
159
|
+
if (!name) throw new Error('Enter a name');
|
|
160
|
+
authSession = await api('POST', '/api/auth-url');
|
|
161
|
+
$('#add-url').value = authSession.url;
|
|
162
|
+
$('#add-link').classList.remove('hidden');
|
|
163
|
+
btn.textContent = 'Register';
|
|
164
|
+
} else {
|
|
165
|
+
await api('POST', '/api/accounts', { name, code: $('#add-code').value, id: authSession.id });
|
|
166
|
+
$('#dlg-add').close(); toast(`Added ${name}`); load(true);
|
|
167
|
+
}
|
|
168
|
+
} catch (e) { toast(e.message, true); }
|
|
169
|
+
btn.disabled = false;
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
// Theme: Auto follows the OS, Light/Dark pin it (remembered per browser).
|
|
173
|
+
function setTheme(t) {
|
|
174
|
+
if (t === 'system') delete document.documentElement.dataset.theme; else document.documentElement.dataset.theme = t;
|
|
175
|
+
try { t === 'system' ? localStorage.removeItem('theme') : localStorage.setItem('theme', t); } catch {}
|
|
176
|
+
document.querySelectorAll('[data-theme-set]').forEach((b) => b.setAttribute('aria-pressed', String(b.dataset.themeSet === t)));
|
|
177
|
+
}
|
|
178
|
+
document.querySelectorAll('[data-theme-set]').forEach((b) => (b.onclick = () => setTheme(b.dataset.themeSet)));
|
|
179
|
+
setTheme(document.documentElement.dataset.theme || 'system');
|
|
180
|
+
|
|
181
|
+
load();
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
|
+
<title>gemix dashboard</title>
|
|
7
|
+
<script>try { const t = localStorage.getItem('theme'); if (t) document.documentElement.dataset.theme = t; } catch {}</script>
|
|
8
|
+
<link rel="stylesheet" href="style.css">
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<header class="topbar"><div class="wrap">
|
|
12
|
+
<span class="wordmark">gemix</span>
|
|
13
|
+
<div class="right">
|
|
14
|
+
<span class="muted mono">local · 127.0.0.1</span>
|
|
15
|
+
<div class="theme-toggle" role="group" aria-label="Theme">
|
|
16
|
+
<button data-theme-set="system">Auto</button><button data-theme-set="light">Light</button><button data-theme-set="dark">Dark</button>
|
|
17
|
+
</div>
|
|
18
|
+
</div>
|
|
19
|
+
</div></header>
|
|
20
|
+
<div class="wrap">
|
|
21
|
+
<section class="pagehead">
|
|
22
|
+
<div>
|
|
23
|
+
<h1>Accounts</h1>
|
|
24
|
+
<p class="muted">Weekly limits per Google login. Same numbers as <code>agy /usage</code>.</p>
|
|
25
|
+
</div>
|
|
26
|
+
<div class="actions">
|
|
27
|
+
<span class="muted mono" id="summary"></span>
|
|
28
|
+
<button class="btn outline" id="refresh">Refresh limits</button>
|
|
29
|
+
<button class="btn default" id="add">Add account</button>
|
|
30
|
+
</div>
|
|
31
|
+
</section>
|
|
32
|
+
<main id="grid" class="grid"></main>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<dialog id="dlg-add" class="dialog">
|
|
36
|
+
<h2>Add account</h2>
|
|
37
|
+
<p class="muted">Name it, sign in with Google, then paste the code shown after login.</p>
|
|
38
|
+
<label class="label" for="add-name">Name</label>
|
|
39
|
+
<input class="input" id="add-name" placeholder="e.g. work" autocomplete="off">
|
|
40
|
+
<div id="add-link" class="hidden">
|
|
41
|
+
<label class="label">Login link</label>
|
|
42
|
+
<div class="row">
|
|
43
|
+
<input class="input mono" id="add-url" readonly>
|
|
44
|
+
<button class="btn outline" id="copy-url">Copy</button>
|
|
45
|
+
<button class="btn outline" id="open-url">Open</button>
|
|
46
|
+
</div>
|
|
47
|
+
<label class="label" for="add-code">Authorization code</label>
|
|
48
|
+
<input class="input mono" id="add-code" placeholder="Paste code here" autocomplete="off">
|
|
49
|
+
</div>
|
|
50
|
+
<div class="footer">
|
|
51
|
+
<button class="btn ghost" data-close>Cancel</button>
|
|
52
|
+
<button class="btn default" id="add-go">Get login link</button>
|
|
53
|
+
</div>
|
|
54
|
+
</dialog>
|
|
55
|
+
|
|
56
|
+
<dialog id="dlg-confirm" class="dialog">
|
|
57
|
+
<h2 id="c-title"></h2>
|
|
58
|
+
<p class="muted" id="c-body"></p>
|
|
59
|
+
<div class="footer">
|
|
60
|
+
<button class="btn ghost" data-close>Cancel</button>
|
|
61
|
+
<button class="btn default" id="c-ok">Confirm</button>
|
|
62
|
+
</div>
|
|
63
|
+
</dialog>
|
|
64
|
+
|
|
65
|
+
<dialog id="dlg-rename" class="dialog">
|
|
66
|
+
<h2>Rename account</h2>
|
|
67
|
+
<input class="input" id="rn-name" autocomplete="off">
|
|
68
|
+
<div class="footer">
|
|
69
|
+
<button class="btn ghost" data-close>Cancel</button>
|
|
70
|
+
<button class="btn default" id="rn-ok">Save</button>
|
|
71
|
+
</div>
|
|
72
|
+
</dialog>
|
|
73
|
+
|
|
74
|
+
<div id="toasts" class="toasts"></div>
|
|
75
|
+
<script src="app.js"></script>
|
|
76
|
+
</body>
|
|
77
|
+
</html>
|