gemiterm 2.2.0 → 2.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/package.json +2 -2
- package/src/cli/command-registry.ts +0 -2
- package/src/cli/commands/auth-command.ts +269 -6
- package/src/cli/utils/prompts.ts +116 -14
- package/src/infrastructure/storage.ts +12 -4
- package/src/services/auth-service.ts +51 -0
- package/src/cli/commands/profile-command.ts +0 -220
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gemiterm",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"module": "index.ts",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|
|
@@ -51,6 +51,6 @@
|
|
|
51
51
|
"chalk": "^5.6.2",
|
|
52
52
|
"cli-table3": "^0.6.5",
|
|
53
53
|
"commander": "^15.0.0",
|
|
54
|
-
"gemini-reverse": "
|
|
54
|
+
"gemini-reverse": "~1.0.12"
|
|
55
55
|
}
|
|
56
56
|
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { Mediator } from "../core/mediator.ts";
|
|
2
2
|
import type { ProfileAuthManager } from "../services/profile-auth-manager.ts";
|
|
3
3
|
import { AuthCommand } from "./commands/auth-command.ts";
|
|
4
|
-
import { ProfileCommand } from "./commands/profile-command.ts";
|
|
5
4
|
import { StatusCommand } from "./commands/status-command.ts";
|
|
6
5
|
import { ListCommand } from "./commands/list-command.ts";
|
|
7
6
|
import { FetchCommand } from "./commands/fetch-command.ts";
|
|
@@ -51,7 +50,6 @@ export class CommandRegistry {
|
|
|
51
50
|
const authCommand = new AuthCommand();
|
|
52
51
|
this.register("auth", authCommand);
|
|
53
52
|
this.register("login", authCommand);
|
|
54
|
-
this.register("profile", new ProfileCommand());
|
|
55
53
|
this.register("status", new StatusCommand());
|
|
56
54
|
this.register("list", new ListCommand());
|
|
57
55
|
this.register("fetch", new FetchCommand());
|
|
@@ -5,12 +5,27 @@ import { CookieStorage, ProfileManager } from "../../infrastructure/storage.ts";
|
|
|
5
5
|
import { PlaywrightCliDriver } from "../../services/playwright-cli-driver.ts";
|
|
6
6
|
import { CookieMonitor } from "../../services/cookie-monitor.ts";
|
|
7
7
|
import { AuthService } from "../../services/auth-service.ts";
|
|
8
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
listProfiles,
|
|
10
|
+
getDefaultProfileName,
|
|
11
|
+
setDefaultProfileName,
|
|
12
|
+
} from "../../infrastructure/config.ts";
|
|
9
13
|
import { GemitermError } from "../../core/errors.ts";
|
|
10
14
|
import { validateProfileName } from "../../infrastructure/validators.ts";
|
|
11
15
|
import { formatProfileTable } from "../../infrastructure/formatters.ts";
|
|
12
16
|
import { text } from "../utils/prompts.ts";
|
|
13
17
|
|
|
18
|
+
interface ParsedFlags {
|
|
19
|
+
list: boolean;
|
|
20
|
+
add: string | null;
|
|
21
|
+
delete: string | null;
|
|
22
|
+
rename: { old: string; new: string } | null;
|
|
23
|
+
default: string | null;
|
|
24
|
+
renew: string | null;
|
|
25
|
+
yes: boolean;
|
|
26
|
+
profileName: string | null;
|
|
27
|
+
}
|
|
28
|
+
|
|
14
29
|
export class AuthCommand implements CliCommand {
|
|
15
30
|
readonly name = "auth";
|
|
16
31
|
readonly description = "Authenticate with Google Gemini";
|
|
@@ -24,6 +39,8 @@ export class AuthCommand implements CliCommand {
|
|
|
24
39
|
const logger = new Logger("auth-command");
|
|
25
40
|
logger.debug("Executing auth command", args);
|
|
26
41
|
|
|
42
|
+
const flags = this.parseFlags(args);
|
|
43
|
+
|
|
27
44
|
const cookieStorage = new CookieStorage();
|
|
28
45
|
const profileManager = new ProfileManager(cookieStorage);
|
|
29
46
|
const driver = new PlaywrightCliDriver();
|
|
@@ -35,6 +52,41 @@ export class AuthCommand implements CliCommand {
|
|
|
35
52
|
logger,
|
|
36
53
|
});
|
|
37
54
|
|
|
55
|
+
if (flags.list) {
|
|
56
|
+
await this.listProfiles(profileManager, logger);
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (flags.add !== null) {
|
|
61
|
+
await this.addProfile(flags.add, profileManager, authService, logger);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (flags.delete !== null) {
|
|
66
|
+
await this.deleteProfile(flags.delete, flags.yes, profileManager, logger);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (flags.rename !== null) {
|
|
71
|
+
await this.renameProfile(flags.rename.old, flags.rename.new, profileManager, logger);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (flags.default !== null) {
|
|
76
|
+
await this.setDefaultProfile(flags.default, profileManager, logger);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (flags.renew !== null) {
|
|
81
|
+
await this.renewProfile(flags.renew, authService, logger);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (flags.profileName !== null) {
|
|
86
|
+
await this.authenticateToProfile(flags.profileName, profileManager, authService, logger);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
38
90
|
const profiles = listProfiles();
|
|
39
91
|
logger.debug("Found profiles", profiles);
|
|
40
92
|
|
|
@@ -59,13 +111,205 @@ export class AuthCommand implements CliCommand {
|
|
|
59
111
|
if (selected.type === "auth") {
|
|
60
112
|
logger.debug("Authenticating with profile", selected.profileName);
|
|
61
113
|
await this.authenticateWithProfile(authService, selected.profileName, profileManager, false);
|
|
114
|
+
} else if (selected.type === "renew") {
|
|
115
|
+
logger.debug("Renewing profile", selected.profileName);
|
|
116
|
+
await authService.renew(selected.profileName);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private parseFlags(args: string[]): ParsedFlags {
|
|
121
|
+
const flags: ParsedFlags = {
|
|
122
|
+
list: false,
|
|
123
|
+
add: null,
|
|
124
|
+
delete: null,
|
|
125
|
+
rename: null,
|
|
126
|
+
default: null,
|
|
127
|
+
renew: null,
|
|
128
|
+
yes: false,
|
|
129
|
+
profileName: null,
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const remaining: string[] = [];
|
|
133
|
+
|
|
134
|
+
for (let i = 0; i < args.length; i++) {
|
|
135
|
+
const arg = args[i];
|
|
136
|
+
|
|
137
|
+
if (arg === "--list" || arg === "-l") {
|
|
138
|
+
flags.list = true;
|
|
139
|
+
} else if (arg === "--yes" || arg === "-y") {
|
|
140
|
+
flags.yes = true;
|
|
141
|
+
} else if (arg === "--add" || arg === "-a") {
|
|
142
|
+
if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
143
|
+
flags.add = args[++i];
|
|
144
|
+
}
|
|
145
|
+
} else if (arg === "--delete" || arg === "-d") {
|
|
146
|
+
if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
147
|
+
flags.delete = args[++i];
|
|
148
|
+
}
|
|
149
|
+
} else if (arg === "--rename" || arg === "-r") {
|
|
150
|
+
if (i + 2 < args.length && !args[i + 1].startsWith("-") && !args[i + 2].startsWith("-")) {
|
|
151
|
+
flags.rename = { old: args[++i], new: args[++i] };
|
|
152
|
+
}
|
|
153
|
+
} else if (arg === "--default" || arg === "-s") {
|
|
154
|
+
if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
155
|
+
flags.default = args[++i];
|
|
156
|
+
}
|
|
157
|
+
} else if (arg === "--renew" || arg === "-e") {
|
|
158
|
+
if (i + 1 < args.length && !args[i + 1].startsWith("-")) {
|
|
159
|
+
flags.renew = args[++i];
|
|
160
|
+
}
|
|
161
|
+
} else if (!arg.startsWith("-")) {
|
|
162
|
+
remaining.push(arg);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
if (remaining.length > 0) {
|
|
167
|
+
flags.profileName = remaining[0];
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return flags;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private async listProfiles(profileManager: ProfileManager, logger: Logger): Promise<void> {
|
|
174
|
+
logger.debug("Listing all profiles");
|
|
175
|
+
const profileNames = listProfiles();
|
|
176
|
+
if (profileNames.length === 0) {
|
|
177
|
+
console.log(chalk.dim("No profiles found. Run 'gemiterm auth' to create one."));
|
|
178
|
+
return;
|
|
62
179
|
}
|
|
180
|
+
|
|
181
|
+
const defaultName = getDefaultProfileName();
|
|
182
|
+
const statuses = profileNames.map((name) => {
|
|
183
|
+
const status = profileManager.getStatus(name);
|
|
184
|
+
return { ...status, isDefault: name === defaultName };
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
console.log(chalk.bold("Profiles"));
|
|
188
|
+
console.log(formatProfileTable(statuses));
|
|
189
|
+
|
|
190
|
+
const active = statuses.filter((s) => s.isActive);
|
|
191
|
+
logger.info(`${active.length} of ${statuses.length} profile(s) active`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
private async addProfile(
|
|
195
|
+
profileName: string,
|
|
196
|
+
profileManager: ProfileManager,
|
|
197
|
+
authService: AuthService,
|
|
198
|
+
logger: Logger,
|
|
199
|
+
): Promise<void> {
|
|
200
|
+
validateProfileName(profileName);
|
|
201
|
+
logger.debug("Adding profile:", profileName);
|
|
202
|
+
|
|
203
|
+
if (listProfiles().includes(profileName)) {
|
|
204
|
+
throw new GemitermError(`Profile '${profileName}' already exists.`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
profileManager.create(profileName);
|
|
208
|
+
console.log(chalk.green(`Profile '${profileName}' created.`));
|
|
209
|
+
|
|
210
|
+
await this.authenticateWithProfile(authService, profileName, profileManager, false);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
private async deleteProfile(
|
|
214
|
+
profileName: string,
|
|
215
|
+
skipConfirm: boolean,
|
|
216
|
+
profileManager: ProfileManager,
|
|
217
|
+
logger: Logger,
|
|
218
|
+
): Promise<void> {
|
|
219
|
+
const profiles = listProfiles();
|
|
220
|
+
if (!profiles.includes(profileName)) {
|
|
221
|
+
throw new GemitermError(`Profile '${profileName}' does not exist.`);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
logger.debug("Deleting profile:", profileName);
|
|
225
|
+
|
|
226
|
+
if (!skipConfirm) {
|
|
227
|
+
const confirmAnswer = await this.promptInput(`Delete profile '${profileName}'? [y/N]`);
|
|
228
|
+
if (!confirmAnswer.toLowerCase().startsWith("y")) {
|
|
229
|
+
console.log(chalk.dim("Cancelled."));
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
profileManager.delete(profileName);
|
|
235
|
+
logger.info(`Deleted profile: ${profileName}`);
|
|
236
|
+
console.log(chalk.green(`Profile '${profileName}' deleted.`));
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
private async renameProfile(
|
|
240
|
+
profileName: string,
|
|
241
|
+
newName: string,
|
|
242
|
+
profileManager: ProfileManager,
|
|
243
|
+
logger: Logger,
|
|
244
|
+
): Promise<void> {
|
|
245
|
+
const profiles = listProfiles();
|
|
246
|
+
if (!profiles.includes(profileName)) {
|
|
247
|
+
throw new GemitermError(`Profile '${profileName}' does not exist.`);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
validateProfileName(newName);
|
|
251
|
+
logger.debug("Renaming profile:", profileName, "→", newName);
|
|
252
|
+
|
|
253
|
+
if (profiles.includes(newName)) {
|
|
254
|
+
throw new GemitermError(`Profile '${newName}' already exists.`);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
profileManager.rename(profileName, newName);
|
|
258
|
+
logger.info(`Renamed profile: ${profileName} → ${newName}`);
|
|
259
|
+
console.log(chalk.green(`Profile renamed: ${profileName} → ${newName}`));
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private async setDefaultProfile(
|
|
263
|
+
profileName: string,
|
|
264
|
+
profileManager: ProfileManager,
|
|
265
|
+
logger: Logger,
|
|
266
|
+
): Promise<void> {
|
|
267
|
+
const profiles = listProfiles();
|
|
268
|
+
if (!profiles.includes(profileName)) {
|
|
269
|
+
throw new GemitermError(`Profile '${profileName}' does not exist.`);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
logger.debug("Setting default profile:", profileName);
|
|
273
|
+
|
|
274
|
+
profileManager.setDefault(profileName);
|
|
275
|
+
setDefaultProfileName(profileName);
|
|
276
|
+
logger.info(`Set default profile: ${profileName}`);
|
|
277
|
+
console.log(chalk.green(`Default profile set to '${profileName}'.`));
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
private async renewProfile(
|
|
281
|
+
profileName: string,
|
|
282
|
+
authService: AuthService,
|
|
283
|
+
logger: Logger,
|
|
284
|
+
): Promise<void> {
|
|
285
|
+
const profiles = listProfiles();
|
|
286
|
+
if (!profiles.includes(profileName)) {
|
|
287
|
+
throw new GemitermError(`Profile '${profileName}' does not exist.`);
|
|
288
|
+
}
|
|
289
|
+
logger.debug("Renewing profile:", profileName);
|
|
290
|
+
await authService.renew(profileName);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private async authenticateToProfile(
|
|
294
|
+
profileName: string,
|
|
295
|
+
profileManager: ProfileManager,
|
|
296
|
+
authService: AuthService,
|
|
297
|
+
logger: Logger,
|
|
298
|
+
): Promise<void> {
|
|
299
|
+
const profiles = listProfiles();
|
|
300
|
+
if (!profiles.includes(profileName)) {
|
|
301
|
+
throw new GemitermError(`Profile '${profileName}' does not exist.`);
|
|
302
|
+
}
|
|
303
|
+
logger.debug("Authenticating with profile:", profileName);
|
|
304
|
+
await this.authenticateWithProfile(authService, profileName, profileManager, false);
|
|
63
305
|
}
|
|
64
306
|
|
|
65
307
|
private async showProfileMenu(
|
|
66
308
|
profiles: string[],
|
|
67
309
|
profileManager: ProfileManager,
|
|
68
|
-
): Promise<
|
|
310
|
+
): Promise<
|
|
311
|
+
{ type: "auth"; profileName: string } | { type: "renew"; profileName: string } | null
|
|
312
|
+
> {
|
|
69
313
|
console.log(chalk.bold("\nProfile Management"));
|
|
70
314
|
console.log("");
|
|
71
315
|
|
|
@@ -77,6 +321,7 @@ export class AuthCommand implements CliCommand {
|
|
|
77
321
|
{ key: "D", label: "Delete profile" },
|
|
78
322
|
{ key: "S", label: "Set default" },
|
|
79
323
|
{ key: "R", label: "Rename profile" },
|
|
324
|
+
{ key: "E", label: "Renew session (extend/refresh cookies)" },
|
|
80
325
|
{ key: "X", label: "Exit and continue with current default" },
|
|
81
326
|
];
|
|
82
327
|
|
|
@@ -107,8 +352,8 @@ export class AuthCommand implements CliCommand {
|
|
|
107
352
|
if (!profiles.includes(trimmed)) {
|
|
108
353
|
throw new GemitermError(`Profile '${trimmed}' does not exist.`);
|
|
109
354
|
}
|
|
110
|
-
const
|
|
111
|
-
if (
|
|
355
|
+
const confirmAnswer = await this.promptInput(`Delete profile '${trimmed}'? [y/N]`);
|
|
356
|
+
if (confirmAnswer.toLowerCase().startsWith("y")) {
|
|
112
357
|
profileManager.delete(trimmed);
|
|
113
358
|
console.log(chalk.green(`Profile '${trimmed}' deleted.`));
|
|
114
359
|
} else {
|
|
@@ -142,6 +387,14 @@ export class AuthCommand implements CliCommand {
|
|
|
142
387
|
console.log(chalk.green(`Profile renamed: ${oldTrimmed} → ${newTrimmed}`));
|
|
143
388
|
return { type: "auth", profileName: newTrimmed };
|
|
144
389
|
}
|
|
390
|
+
case "E": {
|
|
391
|
+
const name = await this.promptInput("Enter profile name to renew");
|
|
392
|
+
const trimmed = name.trim();
|
|
393
|
+
if (!profiles.includes(trimmed)) {
|
|
394
|
+
throw new GemitermError(`Profile '${trimmed}' does not exist.`);
|
|
395
|
+
}
|
|
396
|
+
return { type: "renew", profileName: trimmed };
|
|
397
|
+
}
|
|
145
398
|
case "X":
|
|
146
399
|
default:
|
|
147
400
|
return null;
|
|
@@ -163,12 +416,22 @@ export class AuthCommand implements CliCommand {
|
|
|
163
416
|
}
|
|
164
417
|
|
|
165
418
|
private showUsage(): void {
|
|
166
|
-
console.log(chalk.bold("Usage: gemiterm auth"));
|
|
419
|
+
console.log(chalk.bold("Usage: gemiterm auth [profileName] [options]"));
|
|
167
420
|
console.log("");
|
|
168
421
|
console.log("Authenticate with Google Gemini.");
|
|
169
422
|
console.log("");
|
|
423
|
+
console.log("Arguments:");
|
|
424
|
+
console.log(" profileName Authenticate to an existing profile directly");
|
|
425
|
+
console.log("");
|
|
170
426
|
console.log("Options:");
|
|
171
|
-
console.log(" -h, --help
|
|
427
|
+
console.log(" -h, --help Show this help message");
|
|
428
|
+
console.log(" -l, --list List all profiles (non-interactive)");
|
|
429
|
+
console.log(" -a, --add <name> Create a new profile and authenticate");
|
|
430
|
+
console.log(" -d, --delete <name> Delete a profile");
|
|
431
|
+
console.log(" -y, --yes Skip confirmation prompts");
|
|
432
|
+
console.log(" -r, --rename <old> <new> Rename a profile");
|
|
433
|
+
console.log(" -s, --default <name> Set default profile");
|
|
434
|
+
console.log(" -e, --renew <name> Renew session (extend/refresh cookies)");
|
|
172
435
|
}
|
|
173
436
|
|
|
174
437
|
private promptInput(
|
package/src/cli/utils/prompts.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import chalk from "chalk";
|
|
2
2
|
import figures from "@inquirer/figures";
|
|
3
3
|
import {
|
|
4
|
-
input,
|
|
5
4
|
confirm as inquirerConfirm,
|
|
6
5
|
select as inquirerSelect,
|
|
7
6
|
} from "@inquirer/prompts";
|
|
@@ -82,19 +81,122 @@ export interface TextOptions {
|
|
|
82
81
|
|
|
83
82
|
export async function text(opts: TextOptions): Promise<string> {
|
|
84
83
|
requireTty(`gemiterm new "Your message"`);
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
84
|
+
|
|
85
|
+
return new Promise<string>((resolve, reject) => {
|
|
86
|
+
const stdin = process.stdin;
|
|
87
|
+
const stdout = process.stdout;
|
|
88
|
+
|
|
89
|
+
const prefix = chalk.cyan("?");
|
|
90
|
+
const message = chalk.bold(opts.message);
|
|
91
|
+
let buffer = "";
|
|
92
|
+
let defaultValue = opts.default ?? "";
|
|
93
|
+
let finished = false;
|
|
94
|
+
|
|
95
|
+
function render(showDefault: boolean): void {
|
|
96
|
+
const display = showDefault && !buffer && defaultValue
|
|
97
|
+
? chalk.dim(defaultValue)
|
|
98
|
+
: buffer;
|
|
99
|
+
stdout.write(`\r${"\x1b[K"}${prefix} ${message} ${display}`);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function cleanup(): void {
|
|
103
|
+
stdin.removeListener("data", onData);
|
|
104
|
+
stdin.setRawMode(false);
|
|
105
|
+
stdin.resume();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function finish(value: string): void {
|
|
109
|
+
if (finished) return;
|
|
110
|
+
finished = true;
|
|
111
|
+
cleanup();
|
|
112
|
+
stdout.write(`\r${"\x1b[K"}${prefix} ${message} ${chalk.green(value)}\n`);
|
|
113
|
+
resolve(value);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function cancel(): void {
|
|
117
|
+
if (finished) return;
|
|
118
|
+
finished = true;
|
|
119
|
+
cleanup();
|
|
120
|
+
stdout.write("\n");
|
|
121
|
+
reject(new CancellationError("User cancelled."));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function onData(data: Buffer): Promise<void> {
|
|
125
|
+
const bytes = new Uint8Array(data);
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < bytes.length; i++) {
|
|
128
|
+
const byte = bytes[i];
|
|
129
|
+
|
|
130
|
+
if (byte === 0x03) {
|
|
131
|
+
cancel();
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (byte === 0x0d || byte === 0x0a) {
|
|
136
|
+
const answer = buffer || defaultValue;
|
|
137
|
+
if (opts.validate) {
|
|
138
|
+
const valid = await opts.validate(answer);
|
|
139
|
+
if (valid !== true) {
|
|
140
|
+
stdout.write(
|
|
141
|
+
`\r${"\x1b[K"}${prefix} ${message} ${buffer} ${chalk.red(String(valid))}\n`,
|
|
142
|
+
);
|
|
143
|
+
render(false);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
finish(answer);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (byte === 0x08 || byte === 0x7f) {
|
|
152
|
+
if (buffer.length > 0) {
|
|
153
|
+
buffer = buffer.slice(0, -1);
|
|
154
|
+
render(false);
|
|
155
|
+
} else if (defaultValue) {
|
|
156
|
+
defaultValue = "";
|
|
157
|
+
render(true);
|
|
158
|
+
}
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (byte === 0x1b) {
|
|
163
|
+
const next1 = bytes[i + 1];
|
|
164
|
+
const next2 = bytes[i + 2];
|
|
165
|
+
if (next1 === 0x5b && next2 === 0x33 && bytes[i + 3] === 0x7e) {
|
|
166
|
+
i += 3;
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
if (next1 !== undefined) i += 1;
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (byte >= 0x20 && byte !== 0x7f) {
|
|
174
|
+
let char: string;
|
|
175
|
+
if (byte < 0x80) {
|
|
176
|
+
char = String.fromCharCode(byte);
|
|
177
|
+
} else {
|
|
178
|
+
const decoder = new TextDecoder();
|
|
179
|
+
char = "\uFFFD";
|
|
180
|
+
for (let len = 2; len <= 4; len++) {
|
|
181
|
+
const decoded = decoder.decode(bytes.slice(i, i + len));
|
|
182
|
+
if (decoded.length > 0 && !/\uFFFD/.test(decoded)) {
|
|
183
|
+
char = decoded;
|
|
184
|
+
i += new TextEncoder().encode(char).length - 1;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
buffer += char;
|
|
190
|
+
render(false);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
stdin.setRawMode(true);
|
|
196
|
+
stdin.resume();
|
|
197
|
+
stdin.on("data", onData);
|
|
198
|
+
render(true);
|
|
199
|
+
});
|
|
98
200
|
}
|
|
99
201
|
|
|
100
202
|
export interface ConfirmOptions {
|
|
@@ -23,12 +23,19 @@ function validateCookies(cookies: Cookie[]): boolean {
|
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
function getCookieExpiryTimestamp(cookies: Cookie[]): number | null {
|
|
26
|
+
let maxExpiry: number | null = null;
|
|
26
27
|
for (const cookie of cookies) {
|
|
27
|
-
if (
|
|
28
|
-
|
|
28
|
+
if (
|
|
29
|
+
(cookie.name === "__Secure-1PSID" || cookie.name === "__Secure-1PSIDTS") &&
|
|
30
|
+
cookie.expires > 0
|
|
31
|
+
) {
|
|
32
|
+
const ms = cookie.expires * 1000;
|
|
33
|
+
if (maxExpiry === null || ms > maxExpiry) {
|
|
34
|
+
maxExpiry = ms;
|
|
35
|
+
}
|
|
29
36
|
}
|
|
30
37
|
}
|
|
31
|
-
return
|
|
38
|
+
return maxExpiry;
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
function checkCookieFreshness(cookies: Cookie[]): boolean {
|
|
@@ -147,8 +154,9 @@ export class ProfileManager {
|
|
|
147
154
|
}
|
|
148
155
|
try {
|
|
149
156
|
const cookies = this.cookieStorage.load(name);
|
|
150
|
-
const
|
|
157
|
+
const hasValidCookies = validateCookies(cookies);
|
|
151
158
|
const expiresMs = getCookieExpiryTimestamp(cookies);
|
|
159
|
+
const isActive = hasValidCookies && (expiresMs === null || expiresMs > Date.now());
|
|
152
160
|
let expiresAt: string | null = null;
|
|
153
161
|
if (expiresMs !== null) {
|
|
154
162
|
expiresAt = new Date(expiresMs).toISOString();
|
|
@@ -6,6 +6,8 @@ import type { CookieMonitor } from "./cookie-monitor.ts";
|
|
|
6
6
|
import type { CookieStorage } from "../infrastructure/storage.ts";
|
|
7
7
|
import { ensureConfigDir, getDefaultProfileName } from "../infrastructure/config.ts";
|
|
8
8
|
import { validateProfileName } from "../infrastructure/validators.ts";
|
|
9
|
+
import { getProfilePath } from "../infrastructure/path-utils.ts";
|
|
10
|
+
import { existsFile } from "../infrastructure/io.ts";
|
|
9
11
|
|
|
10
12
|
const GEMINI_AUTH_URL = "https://gemini.google.com/app";
|
|
11
13
|
const DEFAULT_AUTH_TIMEOUT_MS = 300_000;
|
|
@@ -57,6 +59,45 @@ export class AuthService {
|
|
|
57
59
|
}
|
|
58
60
|
}
|
|
59
61
|
|
|
62
|
+
async renew(profileName?: string): Promise<AuthResult> {
|
|
63
|
+
const name = profileName ?? getDefaultProfileName();
|
|
64
|
+
validateProfileName(name);
|
|
65
|
+
|
|
66
|
+
this.logger.info(`Starting session renewal for profile: ${name}`);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
console.log(
|
|
70
|
+
chalk.cyan(`\n🔄 Renewing session → ${GEMINI_AUTH_URL} (profile: ${name})`),
|
|
71
|
+
);
|
|
72
|
+
console.log(
|
|
73
|
+
chalk.dim(
|
|
74
|
+
" Loading existing cookies — if expired, log in to extend your session.\n",
|
|
75
|
+
),
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
await this.launchBrowser(name);
|
|
79
|
+
|
|
80
|
+
const statePath = getProfilePath(name);
|
|
81
|
+
if (existsFile(statePath)) {
|
|
82
|
+
try {
|
|
83
|
+
await this.driver.stateLoad(name, statePath);
|
|
84
|
+
await this.driver.evalJs(name, "location.reload()");
|
|
85
|
+
this.logger.info("Pre-loaded existing cookies into browser session");
|
|
86
|
+
} catch (err) {
|
|
87
|
+
this.logger.debug(`Could not pre-load cookies: ${err}`);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const cookies = await this.waitForLogin(name, DEFAULT_AUTH_TIMEOUT_MS);
|
|
92
|
+
await this.extractCookies(name, cookies);
|
|
93
|
+
const expiresAt = this.getCookieExpiry(cookies);
|
|
94
|
+
this.confirmRenewSuccess(cookies.length, expiresAt, cookies);
|
|
95
|
+
return { cookies, expiresAt };
|
|
96
|
+
} finally {
|
|
97
|
+
await this.closeBrowser(name);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
60
101
|
notifyUser(profileName: string): void {
|
|
61
102
|
console.log(
|
|
62
103
|
chalk.cyan(`\n🔍 Opening headed browser → ${GEMINI_AUTH_URL} (profile: ${profileName})`),
|
|
@@ -115,6 +156,16 @@ export class AuthService {
|
|
|
115
156
|
console.log(chalk.dim(` Has __Secure-1PSID: ${hasSid ? "✅" : "❌"}`));
|
|
116
157
|
}
|
|
117
158
|
|
|
159
|
+
confirmRenewSuccess(cookieCount: number, expiresAt: Date | null, cookies: Cookie[] = []): void {
|
|
160
|
+
console.log(chalk.green(`\n✅ Session renewed — saving state…`));
|
|
161
|
+
console.log(chalk.green(`Renewal successful! (${cookieCount} cookies captured)`));
|
|
162
|
+
if (expiresAt) {
|
|
163
|
+
console.log(chalk.dim(`Session expires: ${expiresAt.toLocaleString()}`));
|
|
164
|
+
}
|
|
165
|
+
const hasSid = cookies.some((c) => c.name === "__Secure-1PSID");
|
|
166
|
+
console.log(chalk.dim(` Has __Secure-1PSID: ${hasSid ? "✅" : "❌"}`));
|
|
167
|
+
}
|
|
168
|
+
|
|
118
169
|
async closeBrowser(profileName: string): Promise<void> {
|
|
119
170
|
this.logger.info(`Closing browser for profile: ${profileName}`);
|
|
120
171
|
try {
|
|
@@ -1,220 +0,0 @@
|
|
|
1
|
-
import chalk from "chalk";
|
|
2
|
-
import type { CliCommand, CliCommandContext } from "../command-registry.ts";
|
|
3
|
-
import { text } from "../utils/prompts.ts";
|
|
4
|
-
import { Logger } from "../../infrastructure/logger.ts";
|
|
5
|
-
import { CookieStorage, ProfileManager } from "../../infrastructure/storage.ts";
|
|
6
|
-
import { PlaywrightCliDriver } from "../../services/playwright-cli-driver.ts";
|
|
7
|
-
import { CookieMonitor } from "../../services/cookie-monitor.ts";
|
|
8
|
-
import { AuthService } from "../../services/auth-service.ts";
|
|
9
|
-
import {
|
|
10
|
-
listProfiles,
|
|
11
|
-
getDefaultProfileName,
|
|
12
|
-
setDefaultProfileName,
|
|
13
|
-
} from "../../infrastructure/config.ts";
|
|
14
|
-
import { GemitermError } from "../../core/errors.ts";
|
|
15
|
-
import { validateProfileName } from "../../infrastructure/validators.ts";
|
|
16
|
-
import { formatProfileTable } from "../../infrastructure/formatters.ts";
|
|
17
|
-
|
|
18
|
-
const ACTIONS = [
|
|
19
|
-
{ name: "add", usage: "profile add <name>", desc: "Create new profile and authenticate" },
|
|
20
|
-
{ name: "delete", usage: "profile delete <name>", desc: "Delete a profile" },
|
|
21
|
-
{ name: "rename", usage: "profile rename <name> <newName>", desc: "Rename a profile" },
|
|
22
|
-
{ name: "default", usage: "profile default <name>", desc: "Set default profile" },
|
|
23
|
-
{ name: "list", usage: "profile list", desc: "List all profiles with status" },
|
|
24
|
-
] as const;
|
|
25
|
-
|
|
26
|
-
type ProfileAction = (typeof ACTIONS)[number]["name"];
|
|
27
|
-
|
|
28
|
-
export class ProfileCommand implements CliCommand {
|
|
29
|
-
readonly name = "profile";
|
|
30
|
-
readonly description = "Manage authentication profiles";
|
|
31
|
-
|
|
32
|
-
async execute(args: string[], context: CliCommandContext): Promise<void> {
|
|
33
|
-
const logger = new Logger("profile-command");
|
|
34
|
-
logger.debug("Executing profile command", args);
|
|
35
|
-
|
|
36
|
-
if (args.includes("--help") || args.includes("-h")) {
|
|
37
|
-
this.showUsage();
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const action = args[0] as ProfileAction | undefined;
|
|
42
|
-
|
|
43
|
-
if (!action) {
|
|
44
|
-
this.showUsage();
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const validActions = ACTIONS.map((a) => a.name);
|
|
49
|
-
if (!validActions.includes(action)) {
|
|
50
|
-
throw new GemitermError(
|
|
51
|
-
`Unknown action '${action}'. Valid actions: ${validActions.join(", ")}`,
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
logger.debug("Profile action:", action);
|
|
56
|
-
|
|
57
|
-
switch (action) {
|
|
58
|
-
case "add":
|
|
59
|
-
await this.addProfile(args.slice(1), logger);
|
|
60
|
-
break;
|
|
61
|
-
case "delete":
|
|
62
|
-
await this.deleteProfile(args.slice(1), logger);
|
|
63
|
-
break;
|
|
64
|
-
case "rename":
|
|
65
|
-
await this.renameProfile(args.slice(1), logger);
|
|
66
|
-
break;
|
|
67
|
-
case "default":
|
|
68
|
-
await this.setDefaultProfile(args.slice(1), logger);
|
|
69
|
-
break;
|
|
70
|
-
case "list":
|
|
71
|
-
await this.listProfiles(logger);
|
|
72
|
-
break;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
private async addProfile(args: string[], logger: Logger): Promise<void> {
|
|
77
|
-
const profileName = args[0];
|
|
78
|
-
if (!profileName) {
|
|
79
|
-
throw new GemitermError("Usage: profile add <name>");
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
validateProfileName(profileName);
|
|
83
|
-
logger.debug("Adding profile:", profileName);
|
|
84
|
-
|
|
85
|
-
if (listProfiles().includes(profileName)) {
|
|
86
|
-
throw new GemitermError(`Profile '${profileName}' already exists.`);
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const cookieStorage = new CookieStorage();
|
|
90
|
-
const profileManager = new ProfileManager(cookieStorage);
|
|
91
|
-
profileManager.create(profileName);
|
|
92
|
-
console.log(chalk.green(`Profile '${profileName}' created.`));
|
|
93
|
-
|
|
94
|
-
const driver = new PlaywrightCliDriver();
|
|
95
|
-
const cookieMonitor = new CookieMonitor({ driver, logger });
|
|
96
|
-
const authService = new AuthService({
|
|
97
|
-
driver,
|
|
98
|
-
cookieMonitor,
|
|
99
|
-
cookieStorage,
|
|
100
|
-
logger,
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
await authService.authenticate(profileName);
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
private async deleteProfile(args: string[], logger: Logger): Promise<void> {
|
|
107
|
-
const profileName = args[0];
|
|
108
|
-
if (!profileName) {
|
|
109
|
-
throw new GemitermError("Usage: profile delete <name>");
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const profiles = listProfiles();
|
|
113
|
-
if (!profiles.includes(profileName)) {
|
|
114
|
-
throw new GemitermError(`Profile '${profileName}' does not exist.`);
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
logger.debug("Deleting profile:", profileName);
|
|
118
|
-
|
|
119
|
-
const confirm = await this.promptInput(`Delete profile '${profileName}'? [y/N]`);
|
|
120
|
-
if (!confirm.toLowerCase().startsWith("y")) {
|
|
121
|
-
console.log(chalk.dim("Cancelled."));
|
|
122
|
-
return;
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
const cookieStorage = new CookieStorage();
|
|
126
|
-
const profileManager = new ProfileManager(cookieStorage);
|
|
127
|
-
profileManager.delete(profileName);
|
|
128
|
-
logger.info(`Deleted profile: ${profileName}`);
|
|
129
|
-
console.log(chalk.green(`Profile '${profileName}' deleted.`));
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
private async renameProfile(args: string[], logger: Logger): Promise<void> {
|
|
133
|
-
const profileName = args[0];
|
|
134
|
-
const newName = args[1];
|
|
135
|
-
if (!profileName || !newName) {
|
|
136
|
-
throw new GemitermError("Usage: profile rename <name> <newName>");
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
const profiles = listProfiles();
|
|
140
|
-
if (!profiles.includes(profileName)) {
|
|
141
|
-
throw new GemitermError(`Profile '${profileName}' does not exist.`);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
validateProfileName(newName);
|
|
145
|
-
logger.debug("Renaming profile:", profileName, "→", newName);
|
|
146
|
-
|
|
147
|
-
if (profiles.includes(newName)) {
|
|
148
|
-
throw new GemitermError(`Profile '${newName}' already exists.`);
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
const cookieStorage = new CookieStorage();
|
|
152
|
-
const profileManager = new ProfileManager(cookieStorage);
|
|
153
|
-
profileManager.rename(profileName, newName);
|
|
154
|
-
logger.info(`Renamed profile: ${profileName} → ${newName}`);
|
|
155
|
-
console.log(chalk.green(`Profile renamed: ${profileName} → ${newName}`));
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
private async setDefaultProfile(args: string[], logger: Logger): Promise<void> {
|
|
159
|
-
const profileName = args[0];
|
|
160
|
-
if (!profileName) {
|
|
161
|
-
throw new GemitermError("Usage: profile default <name>");
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const profiles = listProfiles();
|
|
165
|
-
if (!profiles.includes(profileName)) {
|
|
166
|
-
throw new GemitermError(`Profile '${profileName}' does not exist.`);
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
logger.debug("Setting default profile:", profileName);
|
|
170
|
-
|
|
171
|
-
const cookieStorage = new CookieStorage();
|
|
172
|
-
const profileManager = new ProfileManager(cookieStorage);
|
|
173
|
-
profileManager.setDefault(profileName);
|
|
174
|
-
setDefaultProfileName(profileName);
|
|
175
|
-
logger.info(`Set default profile: ${profileName}`);
|
|
176
|
-
console.log(chalk.green(`Default profile set to '${profileName}'.`));
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
private async listProfiles(logger: Logger): Promise<void> {
|
|
180
|
-
logger.debug("Listing all profiles");
|
|
181
|
-
const profileNames = listProfiles();
|
|
182
|
-
if (profileNames.length === 0) {
|
|
183
|
-
console.log(chalk.dim("No profiles found. Run 'gemiterm login' to create one."));
|
|
184
|
-
return;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const cookieStorage = new CookieStorage();
|
|
188
|
-
const profileManager = new ProfileManager(cookieStorage);
|
|
189
|
-
const defaultName = getDefaultProfileName();
|
|
190
|
-
const statuses = profileNames.map((name) => {
|
|
191
|
-
const status = profileManager.getStatus(name);
|
|
192
|
-
return { ...status, isDefault: name === defaultName };
|
|
193
|
-
});
|
|
194
|
-
|
|
195
|
-
console.log(chalk.bold("Profiles"));
|
|
196
|
-
console.log(formatProfileTable(statuses));
|
|
197
|
-
|
|
198
|
-
const active = statuses.filter((s) => s.isActive);
|
|
199
|
-
logger.info(`${active.length} of ${statuses.length} profile(s) active`);
|
|
200
|
-
}
|
|
201
|
-
|
|
202
|
-
private showUsage(): void {
|
|
203
|
-
console.log(chalk.bold("Usage: gemiterm profile <action> [args]"));
|
|
204
|
-
console.log("");
|
|
205
|
-
console.log(chalk.bold("Actions:"));
|
|
206
|
-
|
|
207
|
-
const maxUsageLen = Math.max(...ACTIONS.map((a) => a.usage.length));
|
|
208
|
-
for (const action of ACTIONS) {
|
|
209
|
-
const padded = action.usage.padEnd(maxUsageLen + 2);
|
|
210
|
-
console.log(` ${chalk.cyan(padded)}${chalk.dim(action.desc)}`);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
console.log("");
|
|
214
|
-
console.log(chalk.dim("Run 'gemiterm profile <action> --help' for more information."));
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
private promptInput(prompt: string): Promise<string> {
|
|
218
|
-
return text({ message: prompt });
|
|
219
|
-
}
|
|
220
|
-
}
|