gemiterm 2.2.0 → 2.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gemiterm",
3
- "version": "2.2.0",
3
+ "version": "2.3.0",
4
4
  "module": "index.ts",
5
5
  "type": "module",
6
6
  "repository": {
@@ -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 { getDefaultProfileName, listProfiles, setDefaultProfileName } from "../../infrastructure/config.ts";
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<{ type: "auth"; profileName: string } | null> {
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 confirm = await this.promptInput(`Delete profile '${trimmed}'? [y/N]`);
111
- if (confirm.toLowerCase().startsWith("y")) {
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 Show this help message");
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(
@@ -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
- try {
86
- return await input(
87
- {
88
- message: opts.message,
89
- default: opts.default,
90
- validate: opts.validate,
91
- theme,
92
- },
93
- { signal: getAbortSignal() },
94
- );
95
- } catch (error) {
96
- mapCancellation(error);
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 (cookie.name === "__Secure-1PSIDTS" && cookie.expires > 0) {
28
- return cookie.expires * 1000;
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 null;
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 isActive = validateCookies(cookies) && checkCookieFreshness(cookies);
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
- }