gemix 0.0.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.
Files changed (58) hide show
  1. package/dist/auth/oauth.d.ts +10 -0
  2. package/dist/auth/oauth.d.ts.map +1 -0
  3. package/dist/auth/oauth.js +186 -0
  4. package/dist/auth/oauth.js.map +1 -0
  5. package/dist/commands/current.d.ts +2 -0
  6. package/dist/commands/current.d.ts.map +1 -0
  7. package/dist/commands/current.js +16 -0
  8. package/dist/commands/current.js.map +1 -0
  9. package/dist/commands/list.d.ts +2 -0
  10. package/dist/commands/list.d.ts.map +1 -0
  11. package/dist/commands/list.js +19 -0
  12. package/dist/commands/list.js.map +1 -0
  13. package/dist/commands/register.d.ts +2 -0
  14. package/dist/commands/register.d.ts.map +1 -0
  15. package/dist/commands/register.js +43 -0
  16. package/dist/commands/register.js.map +1 -0
  17. package/dist/commands/remove.d.ts +2 -0
  18. package/dist/commands/remove.d.ts.map +1 -0
  19. package/dist/commands/remove.js +31 -0
  20. package/dist/commands/remove.js.map +1 -0
  21. package/dist/commands/use.d.ts +2 -0
  22. package/dist/commands/use.d.ts.map +1 -0
  23. package/dist/commands/use.js +23 -0
  24. package/dist/commands/use.js.map +1 -0
  25. package/dist/index.d.ts +3 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +51 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/storage/accounts.d.ts +46 -0
  30. package/dist/storage/accounts.d.ts.map +1 -0
  31. package/dist/storage/accounts.js +210 -0
  32. package/dist/storage/accounts.js.map +1 -0
  33. package/dist/storage/paths.d.ts +12 -0
  34. package/dist/storage/paths.d.ts.map +1 -0
  35. package/dist/storage/paths.js +45 -0
  36. package/dist/storage/paths.js.map +1 -0
  37. package/dist/utils/table.d.ts +8 -0
  38. package/dist/utils/table.d.ts.map +1 -0
  39. package/dist/utils/table.js +37 -0
  40. package/dist/utils/table.js.map +1 -0
  41. package/dist/utils/validation.d.ts +5 -0
  42. package/dist/utils/validation.d.ts.map +1 -0
  43. package/dist/utils/validation.js +22 -0
  44. package/dist/utils/validation.js.map +1 -0
  45. package/doc/README.md +90 -0
  46. package/package.json +30 -0
  47. package/src/auth/oauth.ts +231 -0
  48. package/src/commands/current.ts +18 -0
  49. package/src/commands/list.ts +20 -0
  50. package/src/commands/register.ts +53 -0
  51. package/src/commands/remove.ts +37 -0
  52. package/src/commands/use.ts +25 -0
  53. package/src/index.ts +59 -0
  54. package/src/storage/accounts.ts +285 -0
  55. package/src/storage/paths.ts +56 -0
  56. package/src/utils/table.ts +58 -0
  57. package/src/utils/validation.ts +27 -0
  58. package/tsconfig.json +20 -0
@@ -0,0 +1,231 @@
1
+ import { OAuth2Client } from 'google-auth-library';
2
+ import * as http from 'node:http';
3
+ import * as url from 'node:url';
4
+ import * as net from 'node:net';
5
+ import * as crypto from 'node:crypto';
6
+ import open from 'open';
7
+ import type { TokenData } from '../storage/accounts.js';
8
+
9
+ // OAuth Client ID (same as Gemini CLI - public for installed apps)
10
+ const OAUTH_CLIENT_ID =
11
+ '681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com';
12
+
13
+ const OAUTH_CLIENT_SECRET = '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';
14
+
15
+ const OAUTH_SCOPE = [
16
+ 'openid',
17
+ 'https://www.googleapis.com/auth/cloud-platform',
18
+ 'https://www.googleapis.com/auth/userinfo.email',
19
+ 'https://www.googleapis.com/auth/userinfo.profile',
20
+ ];
21
+
22
+ const HTTP_REDIRECT = 301;
23
+ const SIGN_IN_SUCCESS_URL =
24
+ 'https://developers.google.com/gemini-code-assist/auth_success_gemini';
25
+ const SIGN_IN_FAILURE_URL =
26
+ 'https://developers.google.com/gemini-code-assist/auth_failure_gemini';
27
+
28
+ export interface OAuthResult {
29
+ tokens: TokenData;
30
+ email: string;
31
+ name: string;
32
+ picture?: string;
33
+ }
34
+
35
+ function getAvailablePort(): Promise<number> {
36
+ return new Promise((resolve, reject) => {
37
+ const server = net.createServer();
38
+ server.listen(0, () => {
39
+ const address = server.address();
40
+ const port = address && typeof address === 'object' ? address.port : 0;
41
+ server.close(() => resolve(port));
42
+ });
43
+ server.on('error', reject);
44
+ });
45
+ }
46
+
47
+ export async function authenticateWithGoogle(): Promise<OAuthResult> {
48
+ const client = new OAuth2Client({
49
+ clientId: OAUTH_CLIENT_ID,
50
+ clientSecret: OAUTH_CLIENT_SECRET,
51
+ });
52
+
53
+ const port = await getAvailablePort();
54
+ const redirectUri = `http://127.0.0.1:${port}/oauth2callback`;
55
+ const state = crypto.randomBytes(32).toString('hex');
56
+
57
+ const authUrl = client.generateAuthUrl({
58
+ redirect_uri: redirectUri,
59
+ access_type: 'offline',
60
+ scope: OAUTH_SCOPE,
61
+ prompt: 'consent',
62
+ state,
63
+ });
64
+
65
+ const loginCompletePromise = new Promise<{
66
+ code: string;
67
+ }>((resolve, reject) => {
68
+ const server = http.createServer(async (req, res) => {
69
+ try {
70
+ if (!req.url || req.url.indexOf('/oauth2callback') === -1) {
71
+ res.writeHead(HTTP_REDIRECT, { Location: SIGN_IN_FAILURE_URL });
72
+ res.end();
73
+ reject(new Error('Unexpected request: ' + req.url));
74
+ return;
75
+ }
76
+
77
+ const qs = new url.URL(req.url, `http://127.0.0.1:${port}`).searchParams;
78
+
79
+ if (qs.get('error')) {
80
+ res.writeHead(HTTP_REDIRECT, { Location: SIGN_IN_FAILURE_URL });
81
+ res.end();
82
+ reject(new Error(`Google OAuth error: ${qs.get('error')}. ${qs.get('error_description') || ''}`));
83
+ return;
84
+ }
85
+
86
+ if (qs.get('state') !== state) {
87
+ res.writeHead(200, { 'Content-Type': 'text/html' });
88
+ res.end('<h1>State mismatch. Possible CSRF attack.</h1>');
89
+ reject(new Error('State mismatch. Possible CSRF attack.'));
90
+ return;
91
+ }
92
+
93
+ if (qs.get('code')) {
94
+ res.writeHead(HTTP_REDIRECT, { Location: SIGN_IN_SUCCESS_URL });
95
+ res.end();
96
+ resolve({ code: qs.get('code')! });
97
+ } else {
98
+ res.writeHead(HTTP_REDIRECT, { Location: SIGN_IN_FAILURE_URL });
99
+ res.end();
100
+ reject(new Error('No authorization code received.'));
101
+ }
102
+ } catch (e) {
103
+ res.writeHead(HTTP_REDIRECT, { Location: SIGN_IN_FAILURE_URL });
104
+ res.end();
105
+ reject(e instanceof Error ? e : new Error(String(e)));
106
+ } finally {
107
+ server.close();
108
+ }
109
+ });
110
+
111
+ server.listen(port, '127.0.0.1', () => {});
112
+ server.on('error', reject);
113
+ });
114
+
115
+ console.log('\nOpening browser for Google authentication...\n');
116
+ console.log(`If the browser doesn't open, visit:\n${authUrl}\n`);
117
+
118
+ const childProcess = await open(authUrl);
119
+ childProcess.on('error', (error) => {
120
+ console.error(`Failed to open browser: ${error.message}`);
121
+ console.log(`Please visit:\n${authUrl}\n`);
122
+ });
123
+
124
+ const authTimeout = 5 * 60 * 1000;
125
+ let timeoutId: NodeJS.Timeout | undefined;
126
+
127
+ const timeoutPromise = new Promise<never>((_, reject) => {
128
+ timeoutId = setTimeout(() => {
129
+ reject(new Error('Authentication timed out after 5 minutes.'));
130
+ }, authTimeout);
131
+ });
132
+
133
+ const cancellationPromise = new Promise<never>((_, reject) => {
134
+ process.on('SIGINT', () => {
135
+ reject(new Error('Authentication cancelled by user.'));
136
+ });
137
+ });
138
+
139
+ try {
140
+ const { code } = await Promise.race([
141
+ loginCompletePromise,
142
+ timeoutPromise,
143
+ cancellationPromise,
144
+ ]);
145
+
146
+ if (timeoutId) clearTimeout(timeoutId);
147
+
148
+ const { tokens } = await client.getToken({
149
+ code,
150
+ redirect_uri: redirectUri,
151
+ });
152
+
153
+ client.setCredentials(tokens);
154
+
155
+ const userInfo = await fetchUserInfo(client);
156
+
157
+ return {
158
+ tokens: {
159
+ access_token: tokens.access_token || '',
160
+ refresh_token: tokens.refresh_token || '',
161
+ id_token: tokens.id_token || '',
162
+ scope: tokens.scope || OAUTH_SCOPE.join(' '),
163
+ token_type: tokens.token_type || 'Bearer',
164
+ expiry_date: tokens.expiry_date || Date.now(),
165
+ },
166
+ email: userInfo.email,
167
+ name: userInfo.name,
168
+ picture: userInfo.picture,
169
+ };
170
+ } finally {
171
+ if (timeoutId) clearTimeout(timeoutId);
172
+ }
173
+ }
174
+
175
+ async function fetchUserInfo(client: OAuth2Client): Promise<{
176
+ email: string;
177
+ name: string;
178
+ picture?: string;
179
+ }> {
180
+ const { token } = await client.getAccessToken();
181
+ if (!token) throw new Error('No access token available');
182
+
183
+ const response = await fetch('https://www.googleapis.com/oauth2/v2/userinfo', {
184
+ headers: { Authorization: `Bearer ${token}` },
185
+ });
186
+
187
+ if (!response.ok) {
188
+ throw new Error(`Failed to fetch user info: ${response.status} ${response.statusText}`);
189
+ }
190
+
191
+ const userInfo = (await response.json()) as {
192
+ email: string;
193
+ name: string;
194
+ picture?: string;
195
+ };
196
+
197
+ return {
198
+ email: userInfo.email,
199
+ name: userInfo.name,
200
+ picture: userInfo.picture,
201
+ };
202
+ }
203
+
204
+ export async function refreshTokens(tokens: TokenData): Promise<TokenData | null> {
205
+ try {
206
+ const client = new OAuth2Client({
207
+ clientId: OAUTH_CLIENT_ID,
208
+ clientSecret: OAUTH_CLIENT_SECRET,
209
+ });
210
+
211
+ client.setCredentials({
212
+ access_token: tokens.access_token,
213
+ refresh_token: tokens.refresh_token,
214
+ });
215
+
216
+ const { token: newAccessToken } = await client.getAccessToken();
217
+ if (!newAccessToken) return null;
218
+
219
+ const credentials = client.credentials;
220
+ return {
221
+ access_token: newAccessToken,
222
+ refresh_token: credentials.refresh_token || tokens.refresh_token,
223
+ id_token: credentials.id_token || tokens.id_token || '',
224
+ scope: tokens.scope,
225
+ token_type: tokens.token_type,
226
+ expiry_date: credentials.expiry_date || Date.now() + 3600 * 1000,
227
+ };
228
+ } catch {
229
+ return null;
230
+ }
231
+ }
@@ -0,0 +1,18 @@
1
+ import { loadAccounts, getActiveUsername } from '../storage/accounts.js';
2
+ import { formatLimits } from '../utils/table.js';
3
+
4
+ export async function currentCommand(): Promise<void> {
5
+ const activeUsername = await getActiveUsername();
6
+ const accounts = await loadAccounts();
7
+
8
+ if (!activeUsername || !accounts.accounts[activeUsername]) {
9
+ console.log('No active account.');
10
+ console.log('Use "gemx list" to see available accounts.');
11
+ console.log('Use "gemx use <username>" to switch accounts.');
12
+ return;
13
+ }
14
+
15
+ const account = accounts.accounts[activeUsername];
16
+ console.log(`Active account: ${activeUsername} (${account.email})`);
17
+ console.log(`Limits: ${formatLimits(account.limits)}`);
18
+ }
@@ -0,0 +1,20 @@
1
+ import { loadAccounts } from '../storage/accounts.js';
2
+ import { formatAccountTable, formatAccountTableJson } from '../utils/table.js';
3
+
4
+ export async function listCommand(json: boolean = false): Promise<void> {
5
+ const accounts = await loadAccounts();
6
+ const activeUsername = accounts.active;
7
+
8
+ if (Object.keys(accounts.accounts).length === 0) {
9
+ console.log('No accounts registered.');
10
+ console.log('Run "gemx register <username>" to add an account.');
11
+ return;
12
+ }
13
+
14
+ if (json) {
15
+ const result = formatAccountTableJson(accounts.accounts, activeUsername);
16
+ console.log(JSON.stringify(result, null, 2));
17
+ } else {
18
+ console.log(formatAccountTable(accounts.accounts, activeUsername));
19
+ }
20
+ }
@@ -0,0 +1,53 @@
1
+ import { authenticateWithGoogle } from '../auth/oauth.js';
2
+ import { addAccount, loadAccounts } from '../storage/accounts.js';
3
+ import { validateUsername } from '../utils/validation.js';
4
+ import { formatLimits } from '../utils/table.js';
5
+
6
+ export async function registerCommand(username: string): Promise<void> {
7
+ const validation = validateUsername(username);
8
+ if (!validation.valid) {
9
+ console.error(`Error: ${validation.error}`);
10
+ process.exit(1);
11
+ }
12
+
13
+ const accounts = await loadAccounts();
14
+ if (accounts.accounts[username]) {
15
+ console.error(`Error: Account '${username}' already exists.`);
16
+ console.error(`Use 'gemx remove ${username}' to remove it first.`);
17
+ process.exit(1);
18
+ }
19
+
20
+ try {
21
+ console.log(`Registering account '${username}'...`);
22
+ const result = await authenticateWithGoogle();
23
+
24
+ await addAccount(
25
+ username,
26
+ result.email,
27
+ result.name,
28
+ result.picture,
29
+ result.tokens
30
+ );
31
+
32
+ const isFirstAccount = Object.keys(accounts.accounts).length === 0;
33
+
34
+ console.log('');
35
+ console.log(`✓ Registered ${username} (${result.email})`);
36
+ if (result.picture) {
37
+ console.log(` ${result.picture}`);
38
+ }
39
+ console.log(` Limits: ${formatLimits({ requestsPerDay: 500, tokensPerDay: 1000000 })}`);
40
+
41
+ if (isFirstAccount) {
42
+ console.log(' Set as active account');
43
+ }
44
+ console.log('');
45
+ } catch (error) {
46
+ if (error instanceof Error && error.message.includes('cancelled')) {
47
+ console.log('Registration cancelled.');
48
+ } else {
49
+ console.error('Registration failed:', error instanceof Error ? error.message : error);
50
+ }
51
+ process.exit(1);
52
+ }
53
+ }
@@ -0,0 +1,37 @@
1
+ import * as readline from 'node:readline';
2
+ import { removeAccount, loadAccounts } from '../storage/accounts.js';
3
+
4
+ export async function removeCommand(username: string, yes: boolean = false): Promise<void> {
5
+ const accounts = await loadAccounts();
6
+
7
+ if (!accounts.accounts[username]) {
8
+ console.error(`Error: Account '${username}' not found.`);
9
+ console.error('Use "gemx list" to see available accounts.');
10
+ process.exit(1);
11
+ }
12
+
13
+ if (!yes) {
14
+ const rl = readline.createInterface({
15
+ input: process.stdin,
16
+ output: process.stdout,
17
+ });
18
+
19
+ const answer = await new Promise<string>((resolve) => {
20
+ rl.question(`Remove account '${username}'? (y/N) `, resolve);
21
+ });
22
+ rl.close();
23
+
24
+ if (answer.toLowerCase() !== 'y' && answer.toLowerCase() !== 'yes') {
25
+ console.log('Removal cancelled.');
26
+ return;
27
+ }
28
+ }
29
+
30
+ const success = await removeAccount(username);
31
+ if (!success) {
32
+ console.error('Error: Failed to remove account.');
33
+ process.exit(1);
34
+ }
35
+
36
+ console.log(`✓ Removed account '${username}'`);
37
+ }
@@ -0,0 +1,25 @@
1
+ import { switchAccount, loadAccounts } from '../storage/accounts.js';
2
+
3
+ export async function useCommand(username: string): Promise<void> {
4
+ const accounts = await loadAccounts();
5
+
6
+ if (!accounts.accounts[username]) {
7
+ console.error(`Error: Account '${username}' not found.`);
8
+ console.error('Use "gemx list" to see available accounts.');
9
+ process.exit(1);
10
+ }
11
+
12
+ try {
13
+ const success = await switchAccount(username);
14
+ if (!success) {
15
+ console.error('Error: Failed to switch account.');
16
+ process.exit(1);
17
+ }
18
+
19
+ const account = accounts.accounts[username];
20
+ console.log(`✓ Switched to ${username} (${account.email})`);
21
+ } catch (error) {
22
+ console.error('Error:', error instanceof Error ? error.message : error);
23
+ process.exit(1);
24
+ }
25
+ }
package/src/index.ts ADDED
@@ -0,0 +1,59 @@
1
+ #!/usr/bin/env node
2
+
3
+ import { Command } from 'commander';
4
+ import { registerCommand } from './commands/register.js';
5
+ import { listCommand } from './commands/list.js';
6
+ import { useCommand } from './commands/use.js';
7
+ import { currentCommand } from './commands/current.js';
8
+ import { removeCommand } from './commands/remove.js';
9
+
10
+ const program = new Command();
11
+
12
+ program
13
+ .name('gemx')
14
+ .description('Gemini multi-account CLI - manage multiple Google accounts')
15
+ .version('1.0.0');
16
+
17
+ program
18
+ .command('register')
19
+ .description('Register a new Google account')
20
+ .argument('<username>', 'Unique username for this account')
21
+ .action(async (username: string) => {
22
+ await registerCommand(username);
23
+ });
24
+
25
+ program
26
+ .command('list')
27
+ .alias('ls')
28
+ .description('List all registered accounts')
29
+ .option('-j, --json', 'Output in JSON format')
30
+ .action(async (options: { json?: boolean }) => {
31
+ await listCommand(options.json);
32
+ });
33
+
34
+ program
35
+ .command('use')
36
+ .description('Switch to a different account')
37
+ .argument('<username>', 'Username of the account to switch to')
38
+ .action(async (username: string) => {
39
+ await useCommand(username);
40
+ });
41
+
42
+ program
43
+ .command('current')
44
+ .description('Show the currently active account')
45
+ .action(async () => {
46
+ await currentCommand();
47
+ });
48
+
49
+ program
50
+ .command('remove')
51
+ .alias('rm')
52
+ .description('Remove a registered account')
53
+ .argument('<username>', 'Username of the account to remove')
54
+ .option('-y, --yes', 'Skip confirmation')
55
+ .action(async (username: string, options: { yes?: boolean }) => {
56
+ await removeCommand(username, options.yes);
57
+ });
58
+
59
+ program.parse();