@rankcli/cli 0.0.24 → 0.0.25
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 +89 -51
- package/dist/index.mjs +92 -52
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -373,11 +373,14 @@ function isApiKeyAuth() {
|
|
|
373
373
|
function getApiKey() {
|
|
374
374
|
return process.env.RANKCLI_API_KEY || config.get("apiKey") || null;
|
|
375
375
|
}
|
|
376
|
-
function saveApiKey(apiKey, keyName) {
|
|
376
|
+
function saveApiKey(apiKey, keyName, email) {
|
|
377
377
|
config.set("apiKey", apiKey);
|
|
378
378
|
if (keyName) {
|
|
379
379
|
config.set("apiKeyName", keyName);
|
|
380
380
|
}
|
|
381
|
+
if (email) {
|
|
382
|
+
config.set("email", email);
|
|
383
|
+
}
|
|
381
384
|
}
|
|
382
385
|
async function validateApiKey(apiKey) {
|
|
383
386
|
try {
|
|
@@ -1354,11 +1357,18 @@ async function apply(options) {
|
|
|
1354
1357
|
generateFixes: true
|
|
1355
1358
|
});
|
|
1356
1359
|
issues = auditResult.report.issues.map((issue) => ({
|
|
1357
|
-
severity
|
|
1360
|
+
// Squash the audit engine's 5-level severity into generateFixes's
|
|
1361
|
+
// 3-level SEOIssue scheme - matches how audit.ts already treats
|
|
1362
|
+
// 'error' as critical-weight (red ✗) and 'notice' as info-weight.
|
|
1363
|
+
severity: issue.severity === "error" ? "critical" : issue.severity === "notice" ? "info" : issue.severity,
|
|
1358
1364
|
category: issue.category,
|
|
1359
1365
|
code: issue.code,
|
|
1360
1366
|
message: issue.title,
|
|
1361
1367
|
impact: issue.description || void 0
|
|
1368
|
+
// Cast covers `category`: the audit engine's categories (17 values,
|
|
1369
|
+
// e.g. 'crawlability', 'links') don't map cleanly onto SEOIssue's
|
|
1370
|
+
// narrower set - generateFixes never reads category off its input
|
|
1371
|
+
// (each fixer hardcodes its own), so this is safe, not just silenced.
|
|
1362
1372
|
}));
|
|
1363
1373
|
score = auditResult.score;
|
|
1364
1374
|
} catch (error) {
|
|
@@ -1471,8 +1481,7 @@ function generateCommitMessage(fixes, url) {
|
|
|
1471
1481
|
(f) => f.issue.code.includes("MISSING") || f.issue.message.toLowerCase().includes("missing") || f.issue.message.toLowerCase().includes("add")
|
|
1472
1482
|
);
|
|
1473
1483
|
const type = isAddingNew ? "feat" : "fix";
|
|
1474
|
-
const
|
|
1475
|
-
const categoryStr = categories.size === 1 ? Array.from(categories)[0] : "seo";
|
|
1484
|
+
const categoryStr = "seo";
|
|
1476
1485
|
const issueCount = applied.length;
|
|
1477
1486
|
const title = `${type}(${categoryStr}): fix ${issueCount} SEO issue${issueCount > 1 ? "s" : ""}`;
|
|
1478
1487
|
const fixList = applied.map((f) => `- ${f.issue.code}: ${f.issue.message}`).join("\n");
|
|
@@ -1585,7 +1594,7 @@ async function fix(options) {
|
|
|
1585
1594
|
let issues;
|
|
1586
1595
|
let score;
|
|
1587
1596
|
try {
|
|
1588
|
-
const analysis = await (0, import_agent_runtime3.runDirectAnalysis)(
|
|
1597
|
+
const analysis = await (0, import_agent_runtime3.runDirectAnalysis)(url);
|
|
1589
1598
|
issues = analysis.issues;
|
|
1590
1599
|
score = analysis.score;
|
|
1591
1600
|
} catch (error) {
|
|
@@ -1894,7 +1903,7 @@ async function trackAdd(options) {
|
|
|
1894
1903
|
const newCount = options.keywords.length;
|
|
1895
1904
|
if (existingCount + newCount > maxKeywords) {
|
|
1896
1905
|
spinner.fail(`Keyword limit reached (${existingCount}/${maxKeywords})`);
|
|
1897
|
-
showUpsell("keyword_limit", { tier, current: existingCount,
|
|
1906
|
+
showUpsell("keyword_limit", { tier, current: existingCount, max: maxKeywords });
|
|
1898
1907
|
return { success: false, error: `Keyword limit reached. Upgrade to track more keywords.` };
|
|
1899
1908
|
}
|
|
1900
1909
|
const keywordRecords = options.keywords.map((keyword) => ({
|
|
@@ -2768,15 +2777,22 @@ async function analyzeContent(options) {
|
|
|
2768
2777
|
if (options.url) {
|
|
2769
2778
|
console.log(`Fetching content from ${options.url}...`);
|
|
2770
2779
|
try {
|
|
2771
|
-
const
|
|
2772
|
-
if (!
|
|
2780
|
+
const crawlResult = await (0, import_agent_runtime7.crawlUrl)({ url: options.url });
|
|
2781
|
+
if (!crawlResult.success || !crawlResult.data) {
|
|
2773
2782
|
console.log("\u274C Could not fetch content from URL");
|
|
2774
2783
|
return;
|
|
2775
2784
|
}
|
|
2776
|
-
const
|
|
2777
|
-
const
|
|
2778
|
-
|
|
2779
|
-
|
|
2785
|
+
const { html } = crawlResult.data;
|
|
2786
|
+
const [metaResult, headingsResult] = await Promise.all([
|
|
2787
|
+
(0, import_agent_runtime7.extractMeta)({ html, url: options.url }),
|
|
2788
|
+
(0, import_agent_runtime7.analyzeHeadings)({ html })
|
|
2789
|
+
]);
|
|
2790
|
+
const meta = metaResult.success ? metaResult.data : void 0;
|
|
2791
|
+
const headingStructure = headingsResult.success ? headingsResult.data : [];
|
|
2792
|
+
const title = meta?.title || "";
|
|
2793
|
+
const h1 = headingStructure.find((h) => h.level === 1)?.text || "";
|
|
2794
|
+
const headings = headingStructure.map((h) => h.text);
|
|
2795
|
+
const bodyText = extractBodyText(html);
|
|
2780
2796
|
const paragraphs = bodyText.split(/\n\n+/);
|
|
2781
2797
|
console.log(`
|
|
2782
2798
|
Analyzing: "${title || options.url}"
|
|
@@ -2937,7 +2953,9 @@ async function interactiveSetup(projectPath) {
|
|
|
2937
2953
|
const schedule = scheduleMap[scheduleChoice] || "weekly";
|
|
2938
2954
|
const siteUrl = await ask2("Enter your site URL (https://example.com): ");
|
|
2939
2955
|
if (siteUrl) {
|
|
2940
|
-
await
|
|
2956
|
+
const syncChoice = await ask2("Send scheduled audits to your RankCLI dashboard? (y/n): ");
|
|
2957
|
+
const syncToDashboard = syncChoice.toLowerCase() === "y" || syncChoice.toLowerCase() === "yes";
|
|
2958
|
+
await setupGitHubAction(projectPath, schedule, siteUrl, syncToDashboard);
|
|
2941
2959
|
}
|
|
2942
2960
|
} else {
|
|
2943
2961
|
console.log("Skipping GitHub Action setup.\n");
|
|
@@ -2999,18 +3017,18 @@ async function setupGSC(projectPath, verificationCode) {
|
|
|
2999
3017
|
}
|
|
3000
3018
|
console.log("");
|
|
3001
3019
|
}
|
|
3002
|
-
async function setupGitHubAction(projectPath, schedule, siteUrl) {
|
|
3020
|
+
async function setupGitHubAction(projectPath, schedule, siteUrl, syncToDashboard) {
|
|
3003
3021
|
console.log(`Setting up GitHub Action (${schedule} schedule)...`);
|
|
3004
3022
|
const config2 = {
|
|
3005
3023
|
schedule,
|
|
3006
3024
|
siteUrl: siteUrl || "https://example.com",
|
|
3007
3025
|
features: {
|
|
3008
3026
|
audit: true,
|
|
3009
|
-
tracking: true,
|
|
3010
3027
|
autoFix: true,
|
|
3011
3028
|
createIssues: true,
|
|
3012
3029
|
createPRs: true
|
|
3013
|
-
}
|
|
3030
|
+
},
|
|
3031
|
+
syncToDashboard
|
|
3014
3032
|
};
|
|
3015
3033
|
const { files, instructions } = (0, import_agent_runtime8.writeGitHubActionFiles)(projectPath, config2);
|
|
3016
3034
|
console.log("\u2705 GitHub Action files created:");
|
|
@@ -3193,6 +3211,57 @@ var import_chalk6 = __toESM(require("chalk"));
|
|
|
3193
3211
|
var import_ora5 = __toESM(require("ora"));
|
|
3194
3212
|
var import_inquirer = __toESM(require("inquirer"));
|
|
3195
3213
|
var import_open = __toESM(require("open"));
|
|
3214
|
+
async function browserLogin(spinner) {
|
|
3215
|
+
spinner.start("Starting login...");
|
|
3216
|
+
let start;
|
|
3217
|
+
try {
|
|
3218
|
+
const res = await fetch(`${getApiUrl()}/functions/v1/mcp-connect-start`, { method: "POST" });
|
|
3219
|
+
if (!res.ok) throw new Error(`status ${res.status}`);
|
|
3220
|
+
start = await res.json();
|
|
3221
|
+
} catch (err) {
|
|
3222
|
+
spinner.fail("Could not start login");
|
|
3223
|
+
console.log(import_chalk6.default.red(err instanceof Error ? err.message : "Network error"));
|
|
3224
|
+
return;
|
|
3225
|
+
}
|
|
3226
|
+
spinner.text = "Opening browser to approve...";
|
|
3227
|
+
try {
|
|
3228
|
+
await (0, import_open.default)(start.url);
|
|
3229
|
+
} catch {
|
|
3230
|
+
}
|
|
3231
|
+
console.log(import_chalk6.default.dim(`
|
|
3232
|
+
If a browser didn't open, visit:
|
|
3233
|
+
${start.url}
|
|
3234
|
+
`));
|
|
3235
|
+
const deadline = Date.now() + start.expiresInSeconds * 1e3;
|
|
3236
|
+
spinner.text = "Waiting for approval in your browser...";
|
|
3237
|
+
while (Date.now() < deadline) {
|
|
3238
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
3239
|
+
let status;
|
|
3240
|
+
try {
|
|
3241
|
+
const res = await fetch(`${getApiUrl()}/functions/v1/mcp-connect-status`, {
|
|
3242
|
+
method: "POST",
|
|
3243
|
+
headers: { "Content-Type": "application/json" },
|
|
3244
|
+
body: JSON.stringify({ connectId: start.connectId })
|
|
3245
|
+
});
|
|
3246
|
+
status = await res.json();
|
|
3247
|
+
} catch {
|
|
3248
|
+
continue;
|
|
3249
|
+
}
|
|
3250
|
+
if (status.status === "claimed" && status.apiKey) {
|
|
3251
|
+
saveApiKey(status.apiKey, status.apiKeyName, status.email);
|
|
3252
|
+
await fetchAndSaveSubscription();
|
|
3253
|
+
spinner.succeed(`Logged in as ${import_chalk6.default.green(status.email)}`);
|
|
3254
|
+
return;
|
|
3255
|
+
}
|
|
3256
|
+
if (status.status === "expired" || status.status === "not_found") {
|
|
3257
|
+
spinner.fail("Login link expired");
|
|
3258
|
+
console.log(import_chalk6.default.dim('Run "rankcli login" again.'));
|
|
3259
|
+
return;
|
|
3260
|
+
}
|
|
3261
|
+
}
|
|
3262
|
+
spinner.fail("Timed out waiting for approval");
|
|
3263
|
+
console.log(import_chalk6.default.dim(`Run "rankcli login" again when you're ready.`));
|
|
3264
|
+
}
|
|
3196
3265
|
async function login(options) {
|
|
3197
3266
|
if (isLoggedIn()) {
|
|
3198
3267
|
const { email } = getUserInfo();
|
|
@@ -3230,39 +3299,8 @@ Email: ${result.user?.email}`));
|
|
|
3230
3299
|
console.log(import_chalk6.default.dim(`Plan: ${result.subscription?.planName || "Free"}`));
|
|
3231
3300
|
return;
|
|
3232
3301
|
}
|
|
3233
|
-
if (options.browser) {
|
|
3234
|
-
|
|
3235
|
-
console.log(import_chalk6.default.dim("Complete the login in your browser, then return here.\n"));
|
|
3236
|
-
const loginUrl = `${WEB_APP_URL}/login?cli=true`;
|
|
3237
|
-
await (0, import_open.default)(loginUrl);
|
|
3238
|
-
const { token } = await import_inquirer.default.prompt([
|
|
3239
|
-
{
|
|
3240
|
-
type: "password",
|
|
3241
|
-
name: "token",
|
|
3242
|
-
message: "Paste your CLI token from the browser:",
|
|
3243
|
-
mask: "*"
|
|
3244
|
-
}
|
|
3245
|
-
]);
|
|
3246
|
-
spinner.start("Verifying token...");
|
|
3247
|
-
const supabase = getSupabaseClient();
|
|
3248
|
-
const { data, error } = await supabase.auth.getUser(token);
|
|
3249
|
-
if (error || !data.user) {
|
|
3250
|
-
spinner.fail("Invalid token");
|
|
3251
|
-
console.log(import_chalk6.default.red("The token is invalid or expired. Please try again."));
|
|
3252
|
-
return;
|
|
3253
|
-
}
|
|
3254
|
-
const { data: sessionData, error: sessionError } = await supabase.auth.setSession({
|
|
3255
|
-
access_token: token,
|
|
3256
|
-
refresh_token: ""
|
|
3257
|
-
// Will need to handle this differently
|
|
3258
|
-
});
|
|
3259
|
-
if (sessionError || !sessionData.session) {
|
|
3260
|
-
spinner.fail("Failed to establish session");
|
|
3261
|
-
return;
|
|
3262
|
-
}
|
|
3263
|
-
saveSession(sessionData.session);
|
|
3264
|
-
await fetchAndSaveSubscription();
|
|
3265
|
-
spinner.succeed(`Logged in as ${import_chalk6.default.green(data.user.email)}`);
|
|
3302
|
+
if (options.browser !== false && !options.email) {
|
|
3303
|
+
await browserLogin(spinner);
|
|
3266
3304
|
} else {
|
|
3267
3305
|
let email = options.email;
|
|
3268
3306
|
if (!email) {
|
|
@@ -3406,12 +3444,12 @@ async function fetchAndSaveSubscription() {
|
|
|
3406
3444
|
}
|
|
3407
3445
|
|
|
3408
3446
|
// src/version.ts
|
|
3409
|
-
var VERSION = "0.0.
|
|
3447
|
+
var VERSION = "0.0.24";
|
|
3410
3448
|
|
|
3411
3449
|
// src/index.ts
|
|
3412
3450
|
var program = new import_commander.Command();
|
|
3413
3451
|
program.name("rankcli").description("RankCLI - Ship code, get ranked. SEO meets CI/CD.").version(VERSION);
|
|
3414
|
-
program.command("login").description("Log in to your RankCLI account").option("-e, --email <email>", "
|
|
3452
|
+
program.command("login").description("Log in to your RankCLI account (opens your browser by default)").option("-e, --email <email>", "Log in with email/password instead of the browser").option("--no-browser", "Skip the browser and log in with email/password interactively").option("-t, --token <apiKey>", "API key for non-interactive authentication").action(login);
|
|
3415
3453
|
program.command("logout").description("Log out of your RankCLI account").action(logout);
|
|
3416
3454
|
program.command("whoami").description("Show current logged in user").action(whoami);
|
|
3417
3455
|
program.command("init").description("Initialize SEO Autopilot in your project").option("-y, --yes", "Skip prompts and use defaults").action(init);
|
package/dist/index.mjs
CHANGED
|
@@ -354,11 +354,14 @@ function isApiKeyAuth() {
|
|
|
354
354
|
function getApiKey() {
|
|
355
355
|
return process.env.RANKCLI_API_KEY || config.get("apiKey") || null;
|
|
356
356
|
}
|
|
357
|
-
function saveApiKey(apiKey, keyName) {
|
|
357
|
+
function saveApiKey(apiKey, keyName, email) {
|
|
358
358
|
config.set("apiKey", apiKey);
|
|
359
359
|
if (keyName) {
|
|
360
360
|
config.set("apiKeyName", keyName);
|
|
361
361
|
}
|
|
362
|
+
if (email) {
|
|
363
|
+
config.set("email", email);
|
|
364
|
+
}
|
|
362
365
|
}
|
|
363
366
|
async function validateApiKey(apiKey) {
|
|
364
367
|
try {
|
|
@@ -1175,11 +1178,18 @@ async function apply(options) {
|
|
|
1175
1178
|
generateFixes: true
|
|
1176
1179
|
});
|
|
1177
1180
|
issues = auditResult.report.issues.map((issue) => ({
|
|
1178
|
-
severity
|
|
1181
|
+
// Squash the audit engine's 5-level severity into generateFixes's
|
|
1182
|
+
// 3-level SEOIssue scheme - matches how audit.ts already treats
|
|
1183
|
+
// 'error' as critical-weight (red ✗) and 'notice' as info-weight.
|
|
1184
|
+
severity: issue.severity === "error" ? "critical" : issue.severity === "notice" ? "info" : issue.severity,
|
|
1179
1185
|
category: issue.category,
|
|
1180
1186
|
code: issue.code,
|
|
1181
1187
|
message: issue.title,
|
|
1182
1188
|
impact: issue.description || void 0
|
|
1189
|
+
// Cast covers `category`: the audit engine's categories (17 values,
|
|
1190
|
+
// e.g. 'crawlability', 'links') don't map cleanly onto SEOIssue's
|
|
1191
|
+
// narrower set - generateFixes never reads category off its input
|
|
1192
|
+
// (each fixer hardcodes its own), so this is safe, not just silenced.
|
|
1183
1193
|
}));
|
|
1184
1194
|
score = auditResult.score;
|
|
1185
1195
|
} catch (error) {
|
|
@@ -1297,8 +1307,7 @@ function generateCommitMessage(fixes, url) {
|
|
|
1297
1307
|
(f) => f.issue.code.includes("MISSING") || f.issue.message.toLowerCase().includes("missing") || f.issue.message.toLowerCase().includes("add")
|
|
1298
1308
|
);
|
|
1299
1309
|
const type = isAddingNew ? "feat" : "fix";
|
|
1300
|
-
const
|
|
1301
|
-
const categoryStr = categories.size === 1 ? Array.from(categories)[0] : "seo";
|
|
1310
|
+
const categoryStr = "seo";
|
|
1302
1311
|
const issueCount = applied.length;
|
|
1303
1312
|
const title = `${type}(${categoryStr}): fix ${issueCount} SEO issue${issueCount > 1 ? "s" : ""}`;
|
|
1304
1313
|
const fixList = applied.map((f) => `- ${f.issue.code}: ${f.issue.message}`).join("\n");
|
|
@@ -1411,7 +1420,7 @@ async function fix(options) {
|
|
|
1411
1420
|
let issues;
|
|
1412
1421
|
let score;
|
|
1413
1422
|
try {
|
|
1414
|
-
const analysis = await runDirectAnalysis2(
|
|
1423
|
+
const analysis = await runDirectAnalysis2(url);
|
|
1415
1424
|
issues = analysis.issues;
|
|
1416
1425
|
score = analysis.score;
|
|
1417
1426
|
} catch (error) {
|
|
@@ -1720,7 +1729,7 @@ async function trackAdd(options) {
|
|
|
1720
1729
|
const newCount = options.keywords.length;
|
|
1721
1730
|
if (existingCount + newCount > maxKeywords) {
|
|
1722
1731
|
spinner.fail(`Keyword limit reached (${existingCount}/${maxKeywords})`);
|
|
1723
|
-
showUpsell("keyword_limit", { tier, current: existingCount,
|
|
1732
|
+
showUpsell("keyword_limit", { tier, current: existingCount, max: maxKeywords });
|
|
1724
1733
|
return { success: false, error: `Keyword limit reached. Upgrade to track more keywords.` };
|
|
1725
1734
|
}
|
|
1726
1735
|
const keywordRecords = options.keywords.map((keyword) => ({
|
|
@@ -2590,7 +2599,9 @@ import {
|
|
|
2590
2599
|
formatFeaturedSnippetReport,
|
|
2591
2600
|
analyzeKeywordDensity,
|
|
2592
2601
|
formatKeywordDensityReport,
|
|
2593
|
-
crawlUrl
|
|
2602
|
+
crawlUrl,
|
|
2603
|
+
extractMeta,
|
|
2604
|
+
analyzeHeadings
|
|
2594
2605
|
} from "@rankcli/agent-runtime";
|
|
2595
2606
|
async function analyzeContent(options) {
|
|
2596
2607
|
console.log("\n\u{1F4DD} SEO Autopilot - Content Analysis\n");
|
|
@@ -2617,15 +2628,22 @@ async function analyzeContent(options) {
|
|
|
2617
2628
|
if (options.url) {
|
|
2618
2629
|
console.log(`Fetching content from ${options.url}...`);
|
|
2619
2630
|
try {
|
|
2620
|
-
const
|
|
2621
|
-
if (!
|
|
2631
|
+
const crawlResult = await crawlUrl({ url: options.url });
|
|
2632
|
+
if (!crawlResult.success || !crawlResult.data) {
|
|
2622
2633
|
console.log("\u274C Could not fetch content from URL");
|
|
2623
2634
|
return;
|
|
2624
2635
|
}
|
|
2625
|
-
const
|
|
2626
|
-
const
|
|
2627
|
-
|
|
2628
|
-
|
|
2636
|
+
const { html } = crawlResult.data;
|
|
2637
|
+
const [metaResult, headingsResult] = await Promise.all([
|
|
2638
|
+
extractMeta({ html, url: options.url }),
|
|
2639
|
+
analyzeHeadings({ html })
|
|
2640
|
+
]);
|
|
2641
|
+
const meta = metaResult.success ? metaResult.data : void 0;
|
|
2642
|
+
const headingStructure = headingsResult.success ? headingsResult.data : [];
|
|
2643
|
+
const title = meta?.title || "";
|
|
2644
|
+
const h1 = headingStructure.find((h) => h.level === 1)?.text || "";
|
|
2645
|
+
const headings = headingStructure.map((h) => h.text);
|
|
2646
|
+
const bodyText = extractBodyText(html);
|
|
2629
2647
|
const paragraphs = bodyText.split(/\n\n+/);
|
|
2630
2648
|
console.log(`
|
|
2631
2649
|
Analyzing: "${title || options.url}"
|
|
@@ -2792,7 +2810,9 @@ async function interactiveSetup(projectPath) {
|
|
|
2792
2810
|
const schedule = scheduleMap[scheduleChoice] || "weekly";
|
|
2793
2811
|
const siteUrl = await ask2("Enter your site URL (https://example.com): ");
|
|
2794
2812
|
if (siteUrl) {
|
|
2795
|
-
await
|
|
2813
|
+
const syncChoice = await ask2("Send scheduled audits to your RankCLI dashboard? (y/n): ");
|
|
2814
|
+
const syncToDashboard = syncChoice.toLowerCase() === "y" || syncChoice.toLowerCase() === "yes";
|
|
2815
|
+
await setupGitHubAction(projectPath, schedule, siteUrl, syncToDashboard);
|
|
2796
2816
|
}
|
|
2797
2817
|
} else {
|
|
2798
2818
|
console.log("Skipping GitHub Action setup.\n");
|
|
@@ -2854,18 +2874,18 @@ async function setupGSC(projectPath, verificationCode) {
|
|
|
2854
2874
|
}
|
|
2855
2875
|
console.log("");
|
|
2856
2876
|
}
|
|
2857
|
-
async function setupGitHubAction(projectPath, schedule, siteUrl) {
|
|
2877
|
+
async function setupGitHubAction(projectPath, schedule, siteUrl, syncToDashboard) {
|
|
2858
2878
|
console.log(`Setting up GitHub Action (${schedule} schedule)...`);
|
|
2859
2879
|
const config2 = {
|
|
2860
2880
|
schedule,
|
|
2861
2881
|
siteUrl: siteUrl || "https://example.com",
|
|
2862
2882
|
features: {
|
|
2863
2883
|
audit: true,
|
|
2864
|
-
tracking: true,
|
|
2865
2884
|
autoFix: true,
|
|
2866
2885
|
createIssues: true,
|
|
2867
2886
|
createPRs: true
|
|
2868
|
-
}
|
|
2887
|
+
},
|
|
2888
|
+
syncToDashboard
|
|
2869
2889
|
};
|
|
2870
2890
|
const { files, instructions } = writeGitHubActionFiles(projectPath, config2);
|
|
2871
2891
|
console.log("\u2705 GitHub Action files created:");
|
|
@@ -3048,6 +3068,57 @@ import chalk6 from "chalk";
|
|
|
3048
3068
|
import ora5 from "ora";
|
|
3049
3069
|
import inquirer from "inquirer";
|
|
3050
3070
|
import open from "open";
|
|
3071
|
+
async function browserLogin(spinner) {
|
|
3072
|
+
spinner.start("Starting login...");
|
|
3073
|
+
let start;
|
|
3074
|
+
try {
|
|
3075
|
+
const res = await fetch(`${getApiUrl()}/functions/v1/mcp-connect-start`, { method: "POST" });
|
|
3076
|
+
if (!res.ok) throw new Error(`status ${res.status}`);
|
|
3077
|
+
start = await res.json();
|
|
3078
|
+
} catch (err) {
|
|
3079
|
+
spinner.fail("Could not start login");
|
|
3080
|
+
console.log(chalk6.red(err instanceof Error ? err.message : "Network error"));
|
|
3081
|
+
return;
|
|
3082
|
+
}
|
|
3083
|
+
spinner.text = "Opening browser to approve...";
|
|
3084
|
+
try {
|
|
3085
|
+
await open(start.url);
|
|
3086
|
+
} catch {
|
|
3087
|
+
}
|
|
3088
|
+
console.log(chalk6.dim(`
|
|
3089
|
+
If a browser didn't open, visit:
|
|
3090
|
+
${start.url}
|
|
3091
|
+
`));
|
|
3092
|
+
const deadline = Date.now() + start.expiresInSeconds * 1e3;
|
|
3093
|
+
spinner.text = "Waiting for approval in your browser...";
|
|
3094
|
+
while (Date.now() < deadline) {
|
|
3095
|
+
await new Promise((resolve) => setTimeout(resolve, 2e3));
|
|
3096
|
+
let status;
|
|
3097
|
+
try {
|
|
3098
|
+
const res = await fetch(`${getApiUrl()}/functions/v1/mcp-connect-status`, {
|
|
3099
|
+
method: "POST",
|
|
3100
|
+
headers: { "Content-Type": "application/json" },
|
|
3101
|
+
body: JSON.stringify({ connectId: start.connectId })
|
|
3102
|
+
});
|
|
3103
|
+
status = await res.json();
|
|
3104
|
+
} catch {
|
|
3105
|
+
continue;
|
|
3106
|
+
}
|
|
3107
|
+
if (status.status === "claimed" && status.apiKey) {
|
|
3108
|
+
saveApiKey(status.apiKey, status.apiKeyName, status.email);
|
|
3109
|
+
await fetchAndSaveSubscription();
|
|
3110
|
+
spinner.succeed(`Logged in as ${chalk6.green(status.email)}`);
|
|
3111
|
+
return;
|
|
3112
|
+
}
|
|
3113
|
+
if (status.status === "expired" || status.status === "not_found") {
|
|
3114
|
+
spinner.fail("Login link expired");
|
|
3115
|
+
console.log(chalk6.dim('Run "rankcli login" again.'));
|
|
3116
|
+
return;
|
|
3117
|
+
}
|
|
3118
|
+
}
|
|
3119
|
+
spinner.fail("Timed out waiting for approval");
|
|
3120
|
+
console.log(chalk6.dim(`Run "rankcli login" again when you're ready.`));
|
|
3121
|
+
}
|
|
3051
3122
|
async function login(options) {
|
|
3052
3123
|
if (isLoggedIn()) {
|
|
3053
3124
|
const { email } = getUserInfo();
|
|
@@ -3085,39 +3156,8 @@ Email: ${result.user?.email}`));
|
|
|
3085
3156
|
console.log(chalk6.dim(`Plan: ${result.subscription?.planName || "Free"}`));
|
|
3086
3157
|
return;
|
|
3087
3158
|
}
|
|
3088
|
-
if (options.browser) {
|
|
3089
|
-
|
|
3090
|
-
console.log(chalk6.dim("Complete the login in your browser, then return here.\n"));
|
|
3091
|
-
const loginUrl = `${WEB_APP_URL}/login?cli=true`;
|
|
3092
|
-
await open(loginUrl);
|
|
3093
|
-
const { token } = await inquirer.prompt([
|
|
3094
|
-
{
|
|
3095
|
-
type: "password",
|
|
3096
|
-
name: "token",
|
|
3097
|
-
message: "Paste your CLI token from the browser:",
|
|
3098
|
-
mask: "*"
|
|
3099
|
-
}
|
|
3100
|
-
]);
|
|
3101
|
-
spinner.start("Verifying token...");
|
|
3102
|
-
const supabase = getSupabaseClient();
|
|
3103
|
-
const { data, error } = await supabase.auth.getUser(token);
|
|
3104
|
-
if (error || !data.user) {
|
|
3105
|
-
spinner.fail("Invalid token");
|
|
3106
|
-
console.log(chalk6.red("The token is invalid or expired. Please try again."));
|
|
3107
|
-
return;
|
|
3108
|
-
}
|
|
3109
|
-
const { data: sessionData, error: sessionError } = await supabase.auth.setSession({
|
|
3110
|
-
access_token: token,
|
|
3111
|
-
refresh_token: ""
|
|
3112
|
-
// Will need to handle this differently
|
|
3113
|
-
});
|
|
3114
|
-
if (sessionError || !sessionData.session) {
|
|
3115
|
-
spinner.fail("Failed to establish session");
|
|
3116
|
-
return;
|
|
3117
|
-
}
|
|
3118
|
-
saveSession(sessionData.session);
|
|
3119
|
-
await fetchAndSaveSubscription();
|
|
3120
|
-
spinner.succeed(`Logged in as ${chalk6.green(data.user.email)}`);
|
|
3159
|
+
if (options.browser !== false && !options.email) {
|
|
3160
|
+
await browserLogin(spinner);
|
|
3121
3161
|
} else {
|
|
3122
3162
|
let email = options.email;
|
|
3123
3163
|
if (!email) {
|
|
@@ -3261,12 +3301,12 @@ async function fetchAndSaveSubscription() {
|
|
|
3261
3301
|
}
|
|
3262
3302
|
|
|
3263
3303
|
// src/version.ts
|
|
3264
|
-
var VERSION = "0.0.
|
|
3304
|
+
var VERSION = "0.0.24";
|
|
3265
3305
|
|
|
3266
3306
|
// src/index.ts
|
|
3267
3307
|
var program = new Command();
|
|
3268
3308
|
program.name("rankcli").description("RankCLI - Ship code, get ranked. SEO meets CI/CD.").version(VERSION);
|
|
3269
|
-
program.command("login").description("Log in to your RankCLI account").option("-e, --email <email>", "
|
|
3309
|
+
program.command("login").description("Log in to your RankCLI account (opens your browser by default)").option("-e, --email <email>", "Log in with email/password instead of the browser").option("--no-browser", "Skip the browser and log in with email/password interactively").option("-t, --token <apiKey>", "API key for non-interactive authentication").action(login);
|
|
3270
3310
|
program.command("logout").description("Log out of your RankCLI account").action(logout);
|
|
3271
3311
|
program.command("whoami").description("Show current logged in user").action(whoami);
|
|
3272
3312
|
program.command("init").description("Initialize SEO Autopilot in your project").option("-y, --yes", "Skip prompts and use defaults").action(init);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rankcli/cli",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.25",
|
|
4
4
|
"description": "RankCLI - Ship code, get ranked. SEO meets CI/CD.",
|
|
5
5
|
"homepage": "https://rankcli.dev",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"update-version": "node scripts/update-version.js"
|
|
46
46
|
},
|
|
47
47
|
"dependencies": {
|
|
48
|
-
"@rankcli/agent-runtime": "^0.0.
|
|
49
|
-
"@supabase/supabase-js": "^2.
|
|
48
|
+
"@rankcli/agent-runtime": "^0.0.18",
|
|
49
|
+
"@supabase/supabase-js": "^2.98.0",
|
|
50
50
|
"chalk": "^5.3.0",
|
|
51
51
|
"commander": "^12.0.0",
|
|
52
52
|
"conf": "^12.0.0",
|