swiftpatch-cli 1.1.1 → 1.1.3

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/dist/index.js CHANGED
@@ -110,11 +110,29 @@ var init_api = __esm({
110
110
  });
111
111
  this.client.interceptors.response.use(
112
112
  (response) => response,
113
- (error) => {
113
+ async (error) => {
114
114
  if (error.response) {
115
115
  const data = error.response.data;
116
116
  const message = data?.error?.message || error.message;
117
- if (error.response.status === 401) {
117
+ if (error.response.status === 401 && error.config && !error.config._isRetry) {
118
+ const refreshToken = auth.getRefreshToken();
119
+ if (refreshToken) {
120
+ try {
121
+ error.config._isRetry = true;
122
+ const { data: refreshData } = await axios.post(
123
+ `${baseURL}/auth/refresh`,
124
+ { refreshToken }
125
+ );
126
+ const newAccessToken = refreshData.data?.accessToken;
127
+ const newRefreshToken = refreshData.data?.refreshToken;
128
+ if (newAccessToken) {
129
+ auth.saveToken(newAccessToken, newRefreshToken);
130
+ error.config.headers["Authorization"] = `Bearer ${newAccessToken}`;
131
+ return this.client.request(error.config);
132
+ }
133
+ } catch {
134
+ }
135
+ }
118
136
  throw new Error("Authentication failed. Please run: swiftpatch login");
119
137
  }
120
138
  throw new Error(message);
@@ -124,6 +142,22 @@ var init_api = __esm({
124
142
  );
125
143
  }
126
144
  // AUTH
145
+ async loginWithCredentials(email, password) {
146
+ const response = await this.client.post("/auth/login", { email, password });
147
+ const result = normalizeIds(response.data.data);
148
+ let refreshToken;
149
+ const cookies = response.headers["set-cookie"];
150
+ if (cookies) {
151
+ for (const cookie of cookies) {
152
+ const match = cookie.match(/refreshToken=([^;]+)/);
153
+ if (match) {
154
+ refreshToken = match[1];
155
+ break;
156
+ }
157
+ }
158
+ }
159
+ return { accessToken: result.accessToken, refreshToken, user: result.user };
160
+ }
127
161
  async createLoginSession() {
128
162
  const { data } = await this.client.post("/auth/cli/session");
129
163
  return normalizeIds(data.data);
@@ -379,10 +413,16 @@ var init_auth = __esm({
379
413
  if (!token) return {};
380
414
  return { Authorization: `Bearer ${token}` };
381
415
  },
382
- saveToken(token) {
416
+ saveToken(token, refreshToken) {
383
417
  store2.set("token", token);
418
+ if (refreshToken) {
419
+ store2.set("refreshToken", refreshToken);
420
+ }
384
421
  store2.delete("apiKey");
385
422
  },
423
+ getRefreshToken() {
424
+ return store2.get("refreshToken");
425
+ },
386
426
  async loginWithApiKey(apiKey) {
387
427
  const originalToken = store2.get("token");
388
428
  const originalApiKey = store2.get("apiKey");
@@ -408,6 +448,7 @@ var init_auth = __esm({
408
448
  },
409
449
  logout() {
410
450
  store2.delete("token");
451
+ store2.delete("refreshToken");
411
452
  store2.delete("apiKey");
412
453
  store2.delete("user");
413
454
  },
@@ -479,9 +520,9 @@ var logger = {
479
520
  };
480
521
 
481
522
  // src/commands/login.ts
482
- var loginCommand = new Command("login").description("Authenticate with SwiftPatch").option("-k, --api-key <key>", "Login with API key (for CI/CD)").option("-i, --interactive", "Force interactive login").action(async (options) => {
523
+ var loginCommand = new Command("login").description("Authenticate with SwiftPatch").option("-k, --api-key <key>", "Login with API key (for CI/CD)").option("-e, --email <email>", "Login with email and password").option("-p, --password <password>", "Password (used with --email)").option("-i, --interactive", "Force interactive login").action(async (options) => {
483
524
  const existingToken = auth.getToken();
484
- if (existingToken && !options.interactive && !options.apiKey) {
525
+ if (existingToken && !options.interactive && !options.apiKey && !options.email) {
485
526
  const user = await api.getCurrentUser().catch(() => null);
486
527
  if (user) {
487
528
  logger.info(`Already logged in as ${chalk2.cyan(user.email)}`);
@@ -514,23 +555,87 @@ var loginCommand = new Command("login").description("Authenticate with SwiftPatc
514
555
  }
515
556
  return;
516
557
  }
558
+ if (options.email) {
559
+ await emailLogin(options.email, options.password);
560
+ return;
561
+ }
517
562
  const { method } = await inquirer.prompt([
518
563
  {
519
564
  type: "list",
520
565
  name: "method",
521
566
  message: "How would you like to authenticate?",
522
567
  choices: [
523
- { name: "Login with browser (recommended)", value: "browser" },
524
- { name: "Enter API key", value: "apikey" }
568
+ { name: "Login with email & password", value: "email" },
569
+ { name: "Login with browser", value: "browser" },
570
+ { name: "Enter API key (for CI/CD)", value: "apikey" }
525
571
  ]
526
572
  }
527
573
  ]);
528
574
  if (method === "browser") {
529
575
  await browserLogin();
576
+ } else if (method === "email") {
577
+ await emailLoginInteractive();
530
578
  } else {
531
579
  await apiKeyLogin();
532
580
  }
533
581
  });
582
+ async function emailLogin(email, password) {
583
+ if (!password) {
584
+ const answers = await inquirer.prompt([
585
+ {
586
+ type: "password",
587
+ name: "password",
588
+ message: "Password:",
589
+ mask: "*",
590
+ validate: (input) => input ? true : "Password is required"
591
+ }
592
+ ]);
593
+ password = answers.password;
594
+ }
595
+ const spinner = ora("Logging in...").start();
596
+ try {
597
+ const result = await api.loginWithCredentials(email, password);
598
+ auth.saveToken(result.accessToken, result.refreshToken);
599
+ auth.saveUser({
600
+ id: result.user.id || result.user._id,
601
+ email: result.user.email,
602
+ name: result.user.name
603
+ });
604
+ spinner.succeed("Login successful!");
605
+ console.log("");
606
+ logger.success(`Welcome, ${chalk2.bold(result.user.name)}!`);
607
+ logger.info(`Email: ${chalk2.cyan(result.user.email)}`);
608
+ const orgs = await api.getOrganizations();
609
+ if (orgs.length > 0) {
610
+ logger.info(`Organizations: ${orgs.map((o) => chalk2.cyan(o.name)).join(", ")}`);
611
+ }
612
+ } catch (error) {
613
+ spinner.fail(`Login failed: ${error.message}`);
614
+ process.exit(1);
615
+ }
616
+ }
617
+ async function emailLoginInteractive() {
618
+ const { email, password } = await inquirer.prompt([
619
+ {
620
+ type: "input",
621
+ name: "email",
622
+ message: "Email:",
623
+ validate: (input) => {
624
+ if (!input) return "Email is required";
625
+ if (!input.includes("@")) return "Invalid email format";
626
+ return true;
627
+ }
628
+ },
629
+ {
630
+ type: "password",
631
+ name: "password",
632
+ message: "Password:",
633
+ mask: "*",
634
+ validate: (input) => input ? true : "Password is required"
635
+ }
636
+ ]);
637
+ await emailLogin(email, password);
638
+ }
534
639
  async function browserLogin() {
535
640
  const spinner = ora("Opening browser...").start();
536
641
  try {
@@ -3080,8 +3185,16 @@ var aiExplainCommand = new Command35("explain").description("Generate a changelo
3080
3185
  if (!fromRef) {
3081
3186
  fromRef = await getLastReleaseTag(cwd);
3082
3187
  if (!fromRef) {
3083
- fromRef = `HEAD~${Math.min(parseInt(options.maxCommits, 10), 50)}`;
3084
- spinner.text = `No release tags found. Using last ${options.maxCommits} commits...`;
3188
+ const countOutput = await gitCommand(["rev-list", "--count", "HEAD"], cwd);
3189
+ const totalCommits = countOutput ? parseInt(countOutput, 10) : 1;
3190
+ const maxFallback = Math.min(parseInt(options.maxCommits, 10), 50, totalCommits - 1);
3191
+ if (maxFallback <= 0) {
3192
+ const root = await gitCommand(["rev-list", "--max-parents=0", "HEAD"], cwd);
3193
+ fromRef = root || "HEAD";
3194
+ } else {
3195
+ fromRef = `HEAD~${maxFallback}`;
3196
+ }
3197
+ spinner.text = `No release tags found. Using last ${maxFallback > 0 ? maxFallback : "all"} commits...`;
3085
3198
  }
3086
3199
  }
3087
3200
  const toRef = options.to;
@@ -3282,8 +3395,16 @@ var aiReviewCommand = new Command36("review").description("AI-powered safety rev
3282
3395
  if (!fromRef) {
3283
3396
  fromRef = await gitCommand2(["describe", "--tags", "--abbrev=0", "HEAD~1"], cwd);
3284
3397
  if (!fromRef) {
3285
- fromRef = "HEAD~10";
3286
- spinner.text = "No tags found. Comparing against last 10 commits...";
3398
+ const countOutput = await gitCommand2(["rev-list", "--count", "HEAD"], cwd);
3399
+ const totalCommits = countOutput ? parseInt(countOutput, 10) : 1;
3400
+ const maxFallback = Math.min(10, totalCommits - 1);
3401
+ if (maxFallback <= 0) {
3402
+ const root = await gitCommand2(["rev-list", "--max-parents=0", "HEAD"], cwd);
3403
+ fromRef = root || "HEAD";
3404
+ } else {
3405
+ fromRef = `HEAD~${maxFallback}`;
3406
+ }
3407
+ spinner.text = `No tags found. Comparing against last ${maxFallback > 0 ? maxFallback : "all"} commits...`;
3287
3408
  }
3288
3409
  }
3289
3410
  const toRef = options.to;
@@ -3422,9 +3543,15 @@ aiCommand.command("ask").description("Ask AI about your releases, crashes, or de
3422
3543
  buffer = lines.pop() || "";
3423
3544
  for (const line of lines) {
3424
3545
  if (line.startsWith("data: ")) {
3425
- const data = line.slice(6);
3546
+ const data = line.slice(6).trim();
3426
3547
  if (data === "[DONE]") break;
3427
- process.stdout.write(data);
3548
+ try {
3549
+ const parsed = JSON.parse(data);
3550
+ const text = parsed.content || parsed.text || parsed.delta?.content || "";
3551
+ if (text) process.stdout.write(text);
3552
+ } catch {
3553
+ process.stdout.write(data);
3554
+ }
3428
3555
  }
3429
3556
  }
3430
3557
  }
@@ -3442,9 +3569,9 @@ aiCommand.command("ask").description("Ask AI about your releases, crashes, or de
3442
3569
  clearTimeout(timeout);
3443
3570
  }
3444
3571
  });
3445
- aiCommand.command("risk-score").description("Get AI risk assessment for a release").requiredOption("-o, --org <org-id>", "Organization ID").requiredOption("-a, --app <app-id>", "App ID").requiredOption("-r, --release <release-id>", "Release ID").option("--json", "Output as JSON").action(async (options) => {
3572
+ aiCommand.command("risk-score").description("Get AI risk assessment for a release").option("-o, --org <org-id>", "Organization ID").requiredOption("-a, --app <app-id>", "App ID").requiredOption("-r, --release <release-id>", "Release ID").option("--json", "Output as JSON").action(async (options) => {
3446
3573
  await requireAuth();
3447
- const orgId = options.org;
3574
+ const orgId = await resolveOrgId(options.org);
3448
3575
  const spinner = ora30("Assessing release risk...").start();
3449
3576
  try {
3450
3577
  const assessment = await api.aiRiskAssessment(orgId, options.app, options.release);
@@ -3481,11 +3608,12 @@ aiCommand.command("risk-score").description("Get AI risk assessment for a releas
3481
3608
  process.exit(1);
3482
3609
  }
3483
3610
  });
3484
- aiCommand.command("crashes").description("List AI-grouped crash reports for an app").requiredOption("-o, --org <org-id>", "Organization ID").requiredOption("-a, --app <app-id>", "App ID").option("-s, --status <status>", "Filter by status (OPEN, RESOLVED, IGNORED)", "OPEN").option("-l, --limit <limit>", "Maximum number of results", "10").option("--json", "Output as JSON").action(async (options) => {
3611
+ aiCommand.command("crashes").description("List AI-grouped crash reports for an app").option("-o, --org <org-id>", "Organization ID").requiredOption("-a, --app <app-id>", "App ID").option("-s, --status <status>", "Filter by status (OPEN, RESOLVED, IGNORED)", "OPEN").option("-l, --limit <limit>", "Maximum number of results", "10").option("--json", "Output as JSON").action(async (options) => {
3485
3612
  await requireAuth();
3613
+ const orgId = await resolveOrgId(options.org);
3486
3614
  const spinner = ora30("Fetching crash groups...").start();
3487
3615
  try {
3488
- const groups = await api.aiCrashGroups(options.org, options.app, {
3616
+ const groups = await api.aiCrashGroups(orgId, options.app, {
3489
3617
  status: options.status,
3490
3618
  limit: parseInt(options.limit, 10)
3491
3619
  });
@@ -3521,11 +3649,12 @@ aiCommand.command("crashes").description("List AI-grouped crash reports for an a
3521
3649
  process.exit(1);
3522
3650
  }
3523
3651
  });
3524
- aiCommand.command("insights").description("View AI-generated insights for an app").requiredOption("-o, --org <org-id>", "Organization ID").requiredOption("-a, --app <app-id>", "App ID").option("-s, --status <status>", "Filter by status (ACTIVE, ACKNOWLEDGED, DISMISSED)", "ACTIVE").option("--json", "Output as JSON").action(async (options) => {
3652
+ aiCommand.command("insights").description("View AI-generated insights for an app").option("-o, --org <org-id>", "Organization ID").requiredOption("-a, --app <app-id>", "App ID").option("-s, --status <status>", "Filter by status (ACTIVE, ACKNOWLEDGED, DISMISSED)", "ACTIVE").option("--json", "Output as JSON").action(async (options) => {
3525
3653
  await requireAuth();
3654
+ const orgId = await resolveOrgId(options.org);
3526
3655
  const spinner = ora30("Fetching AI insights...").start();
3527
3656
  try {
3528
- const insights = await api.aiInsights(options.org, options.app, {
3657
+ const insights = await api.aiInsights(orgId, options.app, {
3529
3658
  status: options.status
3530
3659
  });
3531
3660
  spinner.stop();