swiftpatch-cli 1.1.1 → 1.1.2
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 +148 -19
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
524
|
-
{ name: "
|
|
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
|
-
|
|
3084
|
-
|
|
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
|
-
|
|
3286
|
-
|
|
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
|
-
|
|
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").
|
|
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").
|
|
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(
|
|
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").
|
|
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(
|
|
3657
|
+
const insights = await api.aiInsights(orgId, options.app, {
|
|
3529
3658
|
status: options.status
|
|
3530
3659
|
});
|
|
3531
3660
|
spinner.stop();
|