@timetotest/cli 0.3.2 → 0.3.4

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 (30) hide show
  1. package/dist/bin/ttt.js +12 -6
  2. package/dist/bin/ttt.js.map +1 -1
  3. package/dist/package.json +1 -1
  4. package/dist/src/commands/chat/ChatApp.js +133 -69
  5. package/dist/src/commands/chat/ChatApp.js.map +1 -1
  6. package/dist/src/commands/chat/OnboardingApp.js +20 -6
  7. package/dist/src/commands/chat/OnboardingApp.js.map +1 -1
  8. package/dist/src/commands/chat/components/ChatInput.js +6 -6
  9. package/dist/src/commands/chat/components/ChatInput.js.map +1 -1
  10. package/dist/src/commands/chat/components/ModelPicker.js +62 -0
  11. package/dist/src/commands/chat/components/ModelPicker.js.map +1 -0
  12. package/dist/src/commands/chat/components/ToolCallDisplay.js +45 -28
  13. package/dist/src/commands/chat/components/ToolCallDisplay.js.map +1 -1
  14. package/dist/src/commands/chat/tool-message-matcher.js +20 -0
  15. package/dist/src/commands/chat/tool-message-matcher.js.map +1 -0
  16. package/dist/src/commands/chat-ink.js +150 -64
  17. package/dist/src/commands/chat-ink.js.map +1 -1
  18. package/dist/src/commands/login.js +466 -104
  19. package/dist/src/commands/login.js.map +1 -1
  20. package/dist/src/lib/__tests__/login-auth-flow.test.js +117 -0
  21. package/dist/src/lib/__tests__/login-auth-flow.test.js.map +1 -0
  22. package/dist/src/lib/__tests__/model-picker-window.test.js +20 -0
  23. package/dist/src/lib/__tests__/model-picker-window.test.js.map +1 -0
  24. package/dist/src/lib/__tests__/tool-message-matcher.test.js +26 -0
  25. package/dist/src/lib/__tests__/tool-message-matcher.test.js.map +1 -0
  26. package/dist/src/lib/backend-loop-client.js +15 -2
  27. package/dist/src/lib/backend-loop-client.js.map +1 -1
  28. package/dist/src/lib/tui/ink/components/TimetoTestLogo.js +1 -1
  29. package/dist/src/lib/tui/ink/components/TimetoTestLogo.js.map +1 -1
  30. package/package.json +1 -1
@@ -5,6 +5,9 @@ import ora from "ora";
5
5
  import http from "http";
6
6
  import crypto from "crypto";
7
7
  import inquirer from "inquirer";
8
+ import fs from "fs-extra";
9
+ import os from "os";
10
+ import path from "path";
8
11
  import { setAuthToken, resolveApiUrl } from "../lib/config.js";
9
12
  const HANDSHAKE_PAGE = `<!DOCTYPE html>
10
13
  <html lang="en">
@@ -61,95 +64,410 @@ const HANDSHAKE_PAGE = `<!DOCTYPE html>
61
64
  </script>
62
65
  </body>
63
66
  </html>`;
64
- async function performCliSignup() {
65
- console.error("\nWelcome! Let's create your Time to Test account.\n");
66
- // Prompt for email
67
- const { email } = await inquirer.prompt([
68
- {
69
- type: "input",
70
- name: "email",
71
- message: "Email address:",
72
- validate: (input) => {
73
- const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
74
- if (!emailRegex.test(input)) {
75
- return "Please enter a valid email address";
76
- }
77
- return true;
78
- },
79
- },
80
- ]);
81
- // Prompt for password (with confirmation)
82
- const { password } = await inquirer.prompt([
83
- {
84
- type: "password",
85
- name: "password",
86
- message: "Password (min 6 characters):",
87
- mask: "*",
88
- validate: (input) => {
89
- if (input.length < 6) {
90
- return "Password must be at least 6 characters";
91
- }
92
- return true;
93
- },
94
- },
95
- ]);
96
- const { confirmPassword } = await inquirer.prompt([
97
- {
98
- type: "password",
99
- name: "confirmPassword",
100
- message: "Confirm password:",
101
- mask: "*",
102
- validate: (input) => {
103
- if (input !== password) {
104
- return "Passwords do not match";
105
- }
106
- return true;
107
- },
108
- },
109
- ]);
110
- const spinner = ora("Creating your account...").start();
67
+ const DEFAULT_FRONTEND_URL = "https://timetotest.tech";
68
+ const PASSWORD_RESET_PATH = "/auth/reset-password";
69
+ const AUTH_UX_LOG_PATH = path.join(os.homedir(), ".timetotest", "auth-ux-events.log");
70
+ const SIGNUP_EMAIL_HINT = "Email address: (/menu = auth menu, Ctrl+C = exit)";
71
+ const SIGNUP_PASSWORD_HINT = "Password (min 6 characters): (/back = edit email, /menu = auth menu)";
72
+ const SIGNUP_CONFIRM_HINT = "Confirm password: (/back = re-enter password, /menu = auth menu)";
73
+ function recordAuthUxEvent(event, payload = {}) {
111
74
  try {
112
- const apiUrl = resolveApiUrl();
113
- // Call our backend signup endpoint with CLI flag
114
- const signUpResp = await axios.post(`${apiUrl}/api/v1/auth/signup`, {
115
- email: email,
116
- password: password,
117
- }, {
118
- headers: {
119
- "X-CLI-Signup": "true",
120
- },
121
- timeout: 20000,
122
- });
123
- // Backend should return a CLI token directly for CLI signups
124
- const cliToken = signUpResp.data.cli_token;
125
- if (cliToken) {
126
- setAuthToken(cliToken);
127
- spinner.succeed("Account created successfully!");
128
- console.error("\nYou're all set! You can now start using Time to Test.");
129
- }
130
- else {
131
- throw new Error("No CLI token returned from signup");
75
+ fs.ensureDirSync(path.dirname(AUTH_UX_LOG_PATH));
76
+ fs.appendFileSync(AUTH_UX_LOG_PATH, `${JSON.stringify({
77
+ ts: new Date().toISOString(),
78
+ event,
79
+ ...payload,
80
+ })}\n`, "utf8");
81
+ }
82
+ catch {
83
+ // Telemetry must never break auth flows.
84
+ }
85
+ }
86
+ function frontendBaseUrl() {
87
+ return (process.env.TTT_FRONTEND_URL || DEFAULT_FRONTEND_URL).replace(/\/$/, "");
88
+ }
89
+ export function getPasswordResetUrl(email) {
90
+ const base = `${frontendBaseUrl()}${PASSWORD_RESET_PATH}`;
91
+ const query = new URLSearchParams();
92
+ query.set("source", "cli");
93
+ if (email) {
94
+ query.set("email", email);
95
+ }
96
+ return `${base}?${query.toString()}`;
97
+ }
98
+ function getEmailDomain(email) {
99
+ const at = email.indexOf("@");
100
+ if (at <= 0 || at >= email.length - 1)
101
+ return null;
102
+ return email.slice(at + 1).toLowerCase();
103
+ }
104
+ function isPromptCancelled(error) {
105
+ const message = error && typeof error === "object" && "message" in error
106
+ ? String(error.message || "").toLowerCase()
107
+ : "";
108
+ const name = error && typeof error === "object" && "name" in error
109
+ ? String(error.name || "").toLowerCase()
110
+ : "";
111
+ return (name.includes("exitprompt") ||
112
+ message.includes("force closed") ||
113
+ message.includes("canceled"));
114
+ }
115
+ function asLowerMessage(value) {
116
+ if (typeof value === "string")
117
+ return value.toLowerCase();
118
+ if (value && typeof value === "object" && "message" in value) {
119
+ return String(value.message || "").toLowerCase();
120
+ }
121
+ return "";
122
+ }
123
+ export function formatActionableNetworkError(error) {
124
+ const status = error && typeof error === "object"
125
+ ? error.response?.status
126
+ : undefined;
127
+ const code = error && typeof error === "object" && "code" in error
128
+ ? String(error.code || "")
129
+ : "";
130
+ const message = asLowerMessage(error);
131
+ if (status && status >= 500) {
132
+ return `TimetoTest backend is temporarily unavailable (HTTP ${status}). Please retry in 30-60 seconds.`;
133
+ }
134
+ if (status === 429) {
135
+ return "Too many auth attempts right now. Wait a minute, then try again.";
136
+ }
137
+ if (code === "ECONNABORTED" || message.includes("timeout")) {
138
+ return "Authentication request timed out. Check your connection and retry.";
139
+ }
140
+ if (code === "ENOTFOUND" ||
141
+ code === "EAI_AGAIN" ||
142
+ code === "ECONNREFUSED" ||
143
+ code === "ECONNRESET" ||
144
+ message.includes("network error") ||
145
+ message.includes("failed to fetch")) {
146
+ return "Cannot reach TimetoTest servers. Check internet/VPN/firewall and retry.";
147
+ }
148
+ return null;
149
+ }
150
+ export function formatAuthFlowError(error) {
151
+ const actionable = formatActionableNetworkError(error);
152
+ if (actionable)
153
+ return actionable;
154
+ const message = typeof error === "string"
155
+ ? error
156
+ : error && typeof error === "object" && "message" in error
157
+ ? String(error.message || "")
158
+ : "";
159
+ const normalized = message.toLowerCase();
160
+ if (normalized.includes("timed out waiting for browser authentication")) {
161
+ return "Timed out waiting for browser authentication. Keep this terminal open, complete login in the browser, then try again.";
162
+ }
163
+ if (normalized.includes("invalid request")) {
164
+ return "Login callback was invalid or expired. Please retry sign-in from the beginning.";
165
+ }
166
+ return message || "Authentication failed.";
167
+ }
168
+ export function restoreInteractiveStdin(stdin = process.stdin) {
169
+ try {
170
+ stdin.ref?.();
171
+ }
172
+ catch { }
173
+ if (stdin.isPaused()) {
174
+ stdin.resume();
175
+ }
176
+ if (stdin.setRawMode) {
177
+ try {
178
+ stdin.setRawMode(false);
132
179
  }
180
+ catch { }
133
181
  }
134
- catch (err) {
135
- spinner.fail("Sign up failed");
136
- const errorMessage = err?.response?.data?.detail || err?.response?.data?.message || err?.message;
137
- if (errorMessage?.includes("already exists") || errorMessage?.includes("EMAIL_EXISTS")) {
138
- console.error("\nThis email is already registered. Please use the 'Sign In' option instead.");
182
+ }
183
+ function extractNestedMessage(value) {
184
+ if (!value)
185
+ return null;
186
+ if (typeof value === "string")
187
+ return value;
188
+ if (Array.isArray(value)) {
189
+ const items = value
190
+ .map((entry) => {
191
+ if (typeof entry === "string")
192
+ return entry;
193
+ if (entry && typeof entry === "object") {
194
+ const msg = entry.msg;
195
+ if (typeof msg === "string")
196
+ return msg;
197
+ }
198
+ return null;
199
+ })
200
+ .filter((entry) => Boolean(entry));
201
+ return items.length > 0 ? items.join("; ") : null;
202
+ }
203
+ if (typeof value === "object") {
204
+ const message = value.message;
205
+ if (message !== undefined) {
206
+ const nestedMessage = extractNestedMessage(message);
207
+ if (nestedMessage)
208
+ return nestedMessage;
139
209
  }
140
- else if (errorMessage?.includes("WEAK_PASSWORD") || errorMessage?.includes("weak password")) {
141
- console.error("\nPassword is too weak. Please use a stronger password.");
210
+ const detail = value.detail;
211
+ if (detail !== undefined) {
212
+ const nestedDetail = extractNestedMessage(detail);
213
+ if (nestedDetail)
214
+ return nestedDetail;
142
215
  }
143
- else if (errorMessage?.includes("INVALID_EMAIL") || errorMessage?.includes("Invalid email")) {
144
- console.error("\nInvalid email address. Please check and try again.");
216
+ }
217
+ return null;
218
+ }
219
+ export function extractSignupErrorMessage(err) {
220
+ const responseData = err && typeof err === "object"
221
+ ? err.response?.data
222
+ : undefined;
223
+ const fromResponse = extractNestedMessage(responseData);
224
+ if (fromResponse)
225
+ return fromResponse;
226
+ const directMessage = err && typeof err === "object" ? err.message : undefined;
227
+ if (typeof directMessage === "string" && directMessage.trim().length > 0) {
228
+ return directMessage;
229
+ }
230
+ return "Unknown error occurred";
231
+ }
232
+ export function normalizeSignupErrorMessage(errorMessage) {
233
+ const normalized = errorMessage.toLowerCase();
234
+ if (normalized.includes("already exists") ||
235
+ normalized.includes("already registered") ||
236
+ normalized.includes("email_exists")) {
237
+ return "This email is already registered. Please use the 'Sign In' option instead.";
238
+ }
239
+ if (normalized.includes("weak_password") || normalized.includes("weak password")) {
240
+ return "Password is too weak. Please use a stronger password.";
241
+ }
242
+ if (normalized.includes("invalid_email") || normalized.includes("invalid email")) {
243
+ return "Invalid email address. Please check and try again.";
244
+ }
245
+ return errorMessage;
246
+ }
247
+ function classifySignupError(errorMessage) {
248
+ const normalized = errorMessage.toLowerCase();
249
+ if (normalized.includes("already exists") ||
250
+ normalized.includes("already registered") ||
251
+ normalized.includes("email_exists")) {
252
+ return "existing_email";
253
+ }
254
+ if (normalized.includes("weak_password") || normalized.includes("weak password")) {
255
+ return "weak_password";
256
+ }
257
+ if (normalized.includes("invalid_email") || normalized.includes("invalid email")) {
258
+ return "invalid_email";
259
+ }
260
+ if (normalized.includes("cannot reach timetotest servers") ||
261
+ normalized.includes("backend is temporarily unavailable") ||
262
+ normalized.includes("timed out")) {
263
+ return "network";
264
+ }
265
+ return "other";
266
+ }
267
+ async function openPasswordReset(email) {
268
+ const url = getPasswordResetUrl(email);
269
+ console.error(`\nPassword reset: ${url}`);
270
+ try {
271
+ await open(url);
272
+ console.error("Opened password reset in your browser.");
273
+ }
274
+ catch {
275
+ console.error("Couldn't open browser automatically. Open the URL above manually.");
276
+ }
277
+ }
278
+ export async function performCliSignup() {
279
+ // Ink onboarding may unref stdin; restore it before running inquirer prompts.
280
+ restoreInteractiveStdin();
281
+ console.error("\nWelcome! Let's create your Time to Test account.\n");
282
+ let step = "email";
283
+ let email = "";
284
+ let password = "";
285
+ while (true) {
286
+ try {
287
+ if (step === "email") {
288
+ const result = await inquirer.prompt([
289
+ {
290
+ type: "input",
291
+ name: "email",
292
+ message: SIGNUP_EMAIL_HINT,
293
+ validate: (input) => {
294
+ const trimmed = input.trim();
295
+ if (trimmed === "/menu")
296
+ return true;
297
+ const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
298
+ if (!emailRegex.test(trimmed)) {
299
+ return "Please enter a valid email address (or /menu)";
300
+ }
301
+ return true;
302
+ },
303
+ },
304
+ ]);
305
+ const candidate = String(result.email || "").trim();
306
+ if (candidate === "/menu") {
307
+ recordAuthUxEvent("signup_user_back_to_auth_menu", { stage: "email" });
308
+ return "back";
309
+ }
310
+ email = candidate;
311
+ step = "password";
312
+ recordAuthUxEvent("signup_email_collected", {
313
+ email_domain: getEmailDomain(email),
314
+ });
315
+ continue;
316
+ }
317
+ if (step === "password") {
318
+ const result = await inquirer.prompt([
319
+ {
320
+ type: "password",
321
+ name: "password",
322
+ message: SIGNUP_PASSWORD_HINT,
323
+ mask: "*",
324
+ validate: (input) => {
325
+ if (input === "/back" || input === "/menu")
326
+ return true;
327
+ if (input.length < 6) {
328
+ return "Password must be at least 6 characters (or /back, /menu)";
329
+ }
330
+ return true;
331
+ },
332
+ },
333
+ ]);
334
+ const candidate = String(result.password || "");
335
+ if (candidate === "/menu") {
336
+ recordAuthUxEvent("signup_user_back_to_auth_menu", { stage: "password" });
337
+ return "back";
338
+ }
339
+ if (candidate === "/back") {
340
+ step = "email";
341
+ continue;
342
+ }
343
+ password = candidate;
344
+ step = "confirm";
345
+ continue;
346
+ }
347
+ const confirmResult = await inquirer.prompt([
348
+ {
349
+ type: "password",
350
+ name: "confirmPassword",
351
+ message: SIGNUP_CONFIRM_HINT,
352
+ mask: "*",
353
+ validate: (input) => {
354
+ if (input === "/back" || input === "/menu")
355
+ return true;
356
+ if (input !== password) {
357
+ return "Passwords do not match (or /back, /menu)";
358
+ }
359
+ return true;
360
+ },
361
+ },
362
+ ]);
363
+ const confirmPassword = String(confirmResult.confirmPassword || "");
364
+ if (confirmPassword === "/menu") {
365
+ recordAuthUxEvent("signup_user_back_to_auth_menu", { stage: "confirm" });
366
+ return "back";
367
+ }
368
+ if (confirmPassword === "/back") {
369
+ step = "password";
370
+ continue;
371
+ }
372
+ const spinner = ora("Creating your account...").start();
373
+ try {
374
+ const apiUrl = resolveApiUrl();
375
+ recordAuthUxEvent("signup_submit_attempt", {
376
+ email_domain: getEmailDomain(email),
377
+ });
378
+ const signUpResp = await axios.post(`${apiUrl}/api/v1/auth/signup`, {
379
+ email,
380
+ password,
381
+ }, {
382
+ headers: {
383
+ "X-CLI-Signup": "true",
384
+ },
385
+ timeout: 20000,
386
+ });
387
+ const cliToken = signUpResp.data.cli_token;
388
+ if (!cliToken) {
389
+ throw new Error("No CLI token returned from signup");
390
+ }
391
+ setAuthToken(cliToken);
392
+ spinner.succeed("Account created successfully!");
393
+ console.error(`\nYou're all set! Logged in as ${email}.`);
394
+ recordAuthUxEvent("signup_success", {
395
+ email_domain: getEmailDomain(email),
396
+ });
397
+ return "authenticated";
398
+ }
399
+ catch (err) {
400
+ spinner.fail("Sign up failed");
401
+ const actionableNetworkError = formatActionableNetworkError(err);
402
+ const normalizedMessage = actionableNetworkError
403
+ ? actionableNetworkError
404
+ : normalizeSignupErrorMessage(extractSignupErrorMessage(err));
405
+ const kind = classifySignupError(normalizedMessage);
406
+ recordAuthUxEvent("signup_failed", {
407
+ kind,
408
+ email_domain: getEmailDomain(email),
409
+ });
410
+ if (kind === "weak_password") {
411
+ console.error(`\n${normalizedMessage}`);
412
+ step = "password";
413
+ continue;
414
+ }
415
+ if (kind === "invalid_email") {
416
+ console.error(`\n${normalizedMessage}`);
417
+ step = "email";
418
+ continue;
419
+ }
420
+ if (kind === "existing_email") {
421
+ const { recoveryChoice } = await inquirer.prompt([
422
+ {
423
+ type: "list",
424
+ name: "recoveryChoice",
425
+ message: `${normalizedMessage}\nChoose next step:`,
426
+ choices: [
427
+ { name: "Sign In with this account now", value: "signin" },
428
+ { name: "Try a different email", value: "retry_email" },
429
+ { name: "Forgot password (open reset page)", value: "forgot_password" },
430
+ { name: "Back to auth menu", value: "menu" },
431
+ ],
432
+ },
433
+ ]);
434
+ recordAuthUxEvent("signup_existing_email_recovery_choice", {
435
+ choice: recoveryChoice,
436
+ email_domain: getEmailDomain(email),
437
+ });
438
+ if (recoveryChoice === "signin") {
439
+ await performInteractiveLogin();
440
+ return "authenticated";
441
+ }
442
+ if (recoveryChoice === "forgot_password") {
443
+ await openPasswordReset(email);
444
+ step = "email";
445
+ continue;
446
+ }
447
+ if (recoveryChoice === "retry_email") {
448
+ step = "email";
449
+ continue;
450
+ }
451
+ return "back";
452
+ }
453
+ throw new Error(normalizedMessage);
454
+ }
145
455
  }
146
- else {
147
- console.error(`\nError: ${errorMessage || "Unknown error occurred"}`);
456
+ catch (error) {
457
+ if (isPromptCancelled(error)) {
458
+ recordAuthUxEvent("signup_cancelled", {
459
+ email_domain: getEmailDomain(email),
460
+ stage: step,
461
+ });
462
+ return "back";
463
+ }
464
+ throw error;
148
465
  }
149
- process.exitCode = 1;
150
466
  }
151
467
  }
152
468
  export async function performInteractiveLogin() {
469
+ restoreInteractiveStdin();
470
+ recordAuthUxEvent("signin_started");
153
471
  const spinner = ora("Starting local callback server").start();
154
472
  const state = crypto.randomBytes(16).toString("hex");
155
473
  let resolveLogin = null;
@@ -178,7 +496,7 @@ export async function performInteractiveLogin() {
178
496
  if (!token || !incomingState || incomingState !== state) {
179
497
  res.statusCode = 400;
180
498
  res.setHeader("Content-Type", "text/html; charset=utf-8");
181
- res.end("<html><body><h3>Invalid request</h3></body></html>");
499
+ res.end("<html><body><h3>Invalid request</h3><p>The login callback was missing data or expired. Please return to the terminal and retry.</p></body></html>");
182
500
  return;
183
501
  }
184
502
  setAuthToken(token);
@@ -259,26 +577,33 @@ export async function performInteractiveLogin() {
259
577
  });
260
578
  await loginPromise;
261
579
  const callbackUrl = `http://127.0.0.1:${serverPort}/ttt-cli-callback`;
262
- const frontendBase = (process.env.TTT_FRONTEND_URL || "https://timetotest.tech").replace(/\/$/, "");
580
+ const frontendBase = frontendBaseUrl();
263
581
  const pageUrl = `${frontendBase}/cli-auth?callback=${encodeURIComponent(callbackUrl)}&state=${encodeURIComponent(state)}`;
264
582
  spinner.succeed("Local callback ready");
265
583
  console.error("Opening browser to authenticate…");
266
584
  try {
267
- console.error("Auth URL:", pageUrl);
585
+ await open(pageUrl);
586
+ }
587
+ catch (openError) {
588
+ recordAuthUxEvent("signin_browser_open_failed");
589
+ console.error("Could not open your browser automatically.");
590
+ console.error(`Open this URL manually: ${pageUrl}`);
591
+ throw new Error(formatAuthFlowError(openError));
268
592
  }
269
- catch { }
270
- await open(pageUrl);
271
593
  const waitForCallback = new Promise((resolve, reject) => {
272
594
  resolveLogin = () => resolve();
273
- setTimeout(() => reject(new Error("Timed out waiting for browser authentication")), 5 * 60 * 1000);
595
+ setTimeout(() => reject(new Error("Timed out waiting for browser authentication. Complete sign in and callback within 5 minutes.")), 5 * 60 * 1000);
274
596
  });
275
597
  try {
276
598
  await waitForCallback;
277
599
  ora().succeed("Login success");
600
+ recordAuthUxEvent("signin_success");
278
601
  }
279
602
  catch (e) {
280
- ora().fail(e?.message || "Login failed");
281
- throw e;
603
+ const formattedError = formatAuthFlowError(e);
604
+ recordAuthUxEvent("signin_failed", { message: formattedError });
605
+ ora().fail(formattedError);
606
+ throw new Error(formattedError);
282
607
  }
283
608
  finally {
284
609
  server.close();
@@ -290,26 +615,63 @@ export const login = new Command("login")
290
615
  .action(async (opts) => {
291
616
  if (opts.token) {
292
617
  setAuthToken(opts.token);
618
+ recordAuthUxEvent("login_token_success");
293
619
  console.error("Login success via token");
294
620
  return;
295
621
  }
296
- // Show interactive prompt to choose Sign In or Sign Up
297
- const { authChoice } = await inquirer.prompt([
298
- {
299
- type: "list",
300
- name: "authChoice",
301
- message: "What would you like to do?",
302
- choices: [
303
- { name: "Sign In (existing user)", value: "signin" },
304
- { name: "Sign Up (new user)", value: "signup" },
305
- ],
306
- },
307
- ]);
308
- if (authChoice === "signup") {
309
- await performCliSignup();
310
- return;
622
+ while (true) {
623
+ restoreInteractiveStdin();
624
+ let authChoice;
625
+ try {
626
+ // Show interactive prompt to choose Sign In or Sign Up
627
+ const promptResult = await inquirer.prompt([
628
+ {
629
+ type: "list",
630
+ name: "authChoice",
631
+ message: "What would you like to do?",
632
+ choices: [
633
+ { name: "Sign In (existing user)", value: "signin" },
634
+ { name: "Sign Up (new user)", value: "signup" },
635
+ { name: "Forgot Password", value: "forgot_password" },
636
+ { name: "Exit", value: "exit" },
637
+ ],
638
+ },
639
+ ]);
640
+ authChoice = promptResult.authChoice;
641
+ }
642
+ catch (promptError) {
643
+ if (isPromptCancelled(promptError)) {
644
+ recordAuthUxEvent("login_menu_cancelled");
645
+ return;
646
+ }
647
+ throw promptError;
648
+ }
649
+ recordAuthUxEvent("login_menu_choice", { choice: authChoice });
650
+ if (authChoice === "exit") {
651
+ return;
652
+ }
653
+ if (authChoice === "forgot_password") {
654
+ await openPasswordReset();
655
+ continue;
656
+ }
657
+ try {
658
+ if (authChoice === "signup") {
659
+ const result = await performCliSignup();
660
+ if (result === "authenticated") {
661
+ return;
662
+ }
663
+ continue;
664
+ }
665
+ // If signin, use browser-based flow
666
+ await performInteractiveLogin();
667
+ return;
668
+ }
669
+ catch (error) {
670
+ const actionable = formatActionableNetworkError(error);
671
+ const message = actionable || error?.message || "Authentication failed.";
672
+ recordAuthUxEvent("login_flow_error", { message });
673
+ console.error(`\n❌ ${message}\nRetry with Sign In/Sign Up, choose Forgot Password, or Exit.`);
674
+ }
311
675
  }
312
- // If signin, use browser-based flow
313
- await performInteractiveLogin();
314
676
  });
315
677
  //# sourceMappingURL=login.js.map