ctx7 0.2.1 → 0.2.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 CHANGED
@@ -6,8 +6,8 @@ import pc8 from "picocolors";
6
6
  import figlet from "figlet";
7
7
 
8
8
  // src/commands/skill.ts
9
- import pc6 from "picocolors";
10
- import ora2 from "ora";
9
+ import pc7 from "picocolors";
10
+ import ora3 from "ora";
11
11
  import { readdir, rm as rm2 } from "fs/promises";
12
12
  import { join as join5 } from "path";
13
13
 
@@ -521,12 +521,17 @@ import { checkbox as checkbox2 } from "@inquirer/prompts";
521
521
  import readline from "readline";
522
522
  function terminalLink(text, url, color) {
523
523
  const colorFn = color ?? ((s) => s);
524
- return `\x1B]8;;${url}\x07${colorFn(text)}\x1B]8;;\x07`;
524
+ return `\x1B]8;;${url}\x07${colorFn(text)}\x1B]8;;\x07 ${pc3.white("\u2197")}`;
525
525
  }
526
- function formatInstallCount(count) {
527
- if (count === void 0 || count === 0) return "";
526
+ function formatInstallCount(count, placeholder = "") {
527
+ if (count === void 0 || count === 0) return placeholder;
528
528
  return pc3.yellow(String(count));
529
529
  }
530
+ function formatTrustScore(score) {
531
+ if (score === void 0 || score < 0) return pc3.dim("-");
532
+ if (score < 3) return pc3.red(score.toFixed(1));
533
+ return pc3.yellow(score.toFixed(1));
534
+ }
530
535
  async function checkboxWithHover(config, options) {
531
536
  const choices = config.choices.filter(
532
537
  (c) => typeof c === "object" && c !== null && !("type" in c && c.type === "separator")
@@ -608,11 +613,12 @@ function trackEvent(event, data) {
608
613
  }
609
614
 
610
615
  // src/commands/generate.ts
611
- import pc5 from "picocolors";
612
- import ora from "ora";
613
- import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
616
+ import pc6 from "picocolors";
617
+ import ora2 from "ora";
618
+ import { mkdir as mkdir2, writeFile as writeFile2, readFile, unlink } from "fs/promises";
614
619
  import { join as join4 } from "path";
615
620
  import { homedir as homedir3 } from "os";
621
+ import { spawn } from "child_process";
616
622
  import { input, select as select2 } from "@inquirer/prompts";
617
623
 
618
624
  // src/utils/auth.ts
@@ -807,6 +813,148 @@ function buildAuthorizationUrl(baseUrl3, clientId, redirectUri, codeChallenge, s
807
813
  return url.toString();
808
814
  }
809
815
 
816
+ // src/commands/auth.ts
817
+ import pc4 from "picocolors";
818
+ import ora from "ora";
819
+ import open from "open";
820
+ var CLI_CLIENT_ID = "2veBSofhicRBguUT";
821
+ var baseUrl2 = "https://context7.com";
822
+ function setAuthBaseUrl(url) {
823
+ baseUrl2 = url;
824
+ }
825
+ function registerAuthCommands(program2) {
826
+ program2.command("login").description("Log in to Context7").option("--no-browser", "Don't open browser automatically").action(async (options) => {
827
+ await loginCommand(options);
828
+ });
829
+ program2.command("logout").description("Log out of Context7").action(() => {
830
+ logoutCommand();
831
+ });
832
+ program2.command("whoami").description("Show current login status").action(async () => {
833
+ await whoamiCommand();
834
+ });
835
+ }
836
+ async function performLogin(openBrowser = true) {
837
+ const spinner = ora("Preparing login...").start();
838
+ try {
839
+ const { codeVerifier, codeChallenge } = generatePKCE();
840
+ const state = generateState();
841
+ const callbackServer = createCallbackServer(state);
842
+ const port = await callbackServer.port;
843
+ const redirectUri = `http://localhost:${port}/callback`;
844
+ const authUrl = buildAuthorizationUrl(
845
+ baseUrl2,
846
+ CLI_CLIENT_ID,
847
+ redirectUri,
848
+ codeChallenge,
849
+ state
850
+ );
851
+ spinner.stop();
852
+ console.log("");
853
+ console.log(pc4.bold("Opening browser to log in..."));
854
+ console.log("");
855
+ if (openBrowser) {
856
+ await open(authUrl);
857
+ console.log(pc4.dim("If the browser didn't open, visit this URL:"));
858
+ } else {
859
+ console.log(pc4.dim("Open this URL in your browser:"));
860
+ }
861
+ console.log(pc4.cyan(authUrl));
862
+ console.log("");
863
+ const waitingSpinner = ora("Waiting for login...").start();
864
+ try {
865
+ const { code } = await callbackServer.result;
866
+ waitingSpinner.text = "Exchanging code for tokens...";
867
+ const tokens = await exchangeCodeForTokens(
868
+ baseUrl2,
869
+ code,
870
+ codeVerifier,
871
+ redirectUri,
872
+ CLI_CLIENT_ID
873
+ );
874
+ saveTokens(tokens);
875
+ callbackServer.close();
876
+ waitingSpinner.succeed(pc4.green("Login successful!"));
877
+ return tokens.access_token;
878
+ } catch (error) {
879
+ callbackServer.close();
880
+ waitingSpinner.fail(pc4.red("Login failed"));
881
+ if (error instanceof Error) {
882
+ console.error(pc4.red(error.message));
883
+ }
884
+ return null;
885
+ }
886
+ } catch (error) {
887
+ spinner.fail(pc4.red("Login failed"));
888
+ if (error instanceof Error) {
889
+ console.error(pc4.red(error.message));
890
+ }
891
+ return null;
892
+ }
893
+ }
894
+ async function loginCommand(options) {
895
+ trackEvent("command", { name: "login" });
896
+ const existingTokens = loadTokens();
897
+ if (existingTokens) {
898
+ const expired = isTokenExpired(existingTokens);
899
+ if (!expired || existingTokens.refresh_token) {
900
+ console.log(pc4.yellow("You are already logged in."));
901
+ console.log(
902
+ pc4.dim("Run 'ctx7 logout' first if you want to log in with a different account.")
903
+ );
904
+ return;
905
+ }
906
+ clearTokens();
907
+ }
908
+ const token = await performLogin(options.browser);
909
+ if (!token) {
910
+ process.exit(1);
911
+ }
912
+ console.log("");
913
+ console.log(pc4.dim("You can now use authenticated Context7 features."));
914
+ }
915
+ function logoutCommand() {
916
+ trackEvent("command", { name: "logout" });
917
+ if (clearTokens()) {
918
+ console.log(pc4.green("Logged out successfully."));
919
+ } else {
920
+ console.log(pc4.yellow("You are not logged in."));
921
+ }
922
+ }
923
+ async function whoamiCommand() {
924
+ trackEvent("command", { name: "whoami" });
925
+ const tokens = loadTokens();
926
+ if (!tokens) {
927
+ console.log(pc4.yellow("Not logged in."));
928
+ console.log(pc4.dim("Run 'ctx7 login' to authenticate."));
929
+ return;
930
+ }
931
+ console.log(pc4.green("Logged in"));
932
+ try {
933
+ const userInfo = await fetchUserInfo(tokens.access_token);
934
+ if (userInfo.name) {
935
+ console.log(`${pc4.dim("Name:".padEnd(9))}${userInfo.name}`);
936
+ }
937
+ if (userInfo.email) {
938
+ console.log(`${pc4.dim("Email:".padEnd(9))}${userInfo.email}`);
939
+ }
940
+ } catch {
941
+ if (isTokenExpired(tokens) && !tokens.refresh_token) {
942
+ console.log(pc4.dim("(Session may be expired - run 'ctx7 login' to refresh)"));
943
+ }
944
+ }
945
+ }
946
+ async function fetchUserInfo(accessToken) {
947
+ const response = await fetch("https://clerk.context7.com/oauth/userinfo", {
948
+ headers: {
949
+ Authorization: `Bearer ${accessToken}`
950
+ }
951
+ });
952
+ if (!response.ok) {
953
+ throw new Error("Failed to fetch user info");
954
+ }
955
+ return await response.json();
956
+ }
957
+
810
958
  // src/utils/selectOrInput.ts
811
959
  import {
812
960
  createPrompt,
@@ -817,10 +965,19 @@ import {
817
965
  isUpKey,
818
966
  isDownKey
819
967
  } from "@inquirer/core";
820
- import pc4 from "picocolors";
968
+ import pc5 from "picocolors";
969
+ function reorderOptions(options, recommendedIndex) {
970
+ if (recommendedIndex === 0) return options;
971
+ const reordered = [options[recommendedIndex]];
972
+ for (let i = 0; i < options.length; i++) {
973
+ if (i !== recommendedIndex) reordered.push(options[i]);
974
+ }
975
+ return reordered;
976
+ }
821
977
  var selectOrInput = createPrompt((config, done) => {
822
- const { message, options, recommendedIndex = 0 } = config;
823
- const [cursor, setCursor] = useState(recommendedIndex);
978
+ const { message, options: rawOptions, recommendedIndex = 0 } = config;
979
+ const options = reorderOptions(rawOptions, recommendedIndex);
980
+ const [cursor, setCursor] = useState(0);
824
981
  const [inputValue, setInputValue] = useState("");
825
982
  const prefix = usePrefix({});
826
983
  useKeypress((key, rl) => {
@@ -835,7 +992,7 @@ var selectOrInput = createPrompt((config, done) => {
835
992
  if (isEnterKey(key)) {
836
993
  if (cursor === options.length) {
837
994
  const finalValue = inputValue.trim();
838
- done(finalValue || options[recommendedIndex]);
995
+ done(finalValue || options[0]);
839
996
  } else {
840
997
  done(options[cursor]);
841
998
  }
@@ -865,16 +1022,16 @@ var selectOrInput = createPrompt((config, done) => {
865
1022
  rl.line = "";
866
1023
  }
867
1024
  });
868
- let output = `${prefix} ${pc4.bold(message)}
1025
+ let output = `${prefix} ${pc5.bold(message)}
869
1026
 
870
1027
  `;
871
1028
  options.forEach((opt, idx) => {
872
- const isRecommended = idx === recommendedIndex;
1029
+ const isRecommended = idx === 0;
873
1030
  const isCursor = idx === cursor;
874
- const number = pc4.cyan(`${idx + 1}.`);
875
- const text = isRecommended ? `${opt} ${pc4.green("\u2713 Recommended")}` : opt;
1031
+ const number = pc5.cyan(`${idx + 1}.`);
1032
+ const text = isRecommended ? `${opt} ${pc5.green("\u2713 Recommended")}` : opt;
876
1033
  if (isCursor) {
877
- output += pc4.cyan(`\u276F ${number} ${text}
1034
+ output += pc5.cyan(`\u276F ${number} ${text}
878
1035
  `);
879
1036
  } else {
880
1037
  output += ` ${number} ${text}
@@ -883,9 +1040,9 @@ var selectOrInput = createPrompt((config, done) => {
883
1040
  });
884
1041
  const isCustomCursor = cursor === options.length;
885
1042
  if (isCustomCursor) {
886
- output += pc4.cyan(`\u276F ${pc4.yellow("\u270E")} ${inputValue || pc4.dim("Type your own...")}`);
1043
+ output += pc5.cyan(`\u276F ${pc5.yellow("\u270E")} ${inputValue || pc5.dim("Type your own...")}`);
887
1044
  } else {
888
- output += ` ${pc4.yellow("\u270E")} ${pc4.dim("Type your own...")}`;
1045
+ output += ` ${pc5.yellow("\u270E")} ${pc5.dim("Type your own...")}`;
889
1046
  }
890
1047
  return output;
891
1048
  });
@@ -900,52 +1057,64 @@ function registerGenerateCommand(skillCommand) {
900
1057
  async function generateCommand(options) {
901
1058
  trackEvent("command", { name: "generate" });
902
1059
  log.blank();
1060
+ let accessToken = null;
903
1061
  const tokens = loadTokens();
904
- if (!tokens) {
905
- log.error("Authentication required. Please run 'ctx7 login' first.");
906
- return;
907
- }
908
- if (isTokenExpired(tokens)) {
909
- log.error("Session expired. Please run 'ctx7 login' to refresh.");
910
- return;
1062
+ if (tokens && !isTokenExpired(tokens)) {
1063
+ accessToken = tokens.access_token;
1064
+ } else {
1065
+ log.info("Authentication required. Logging in...");
1066
+ log.blank();
1067
+ accessToken = await performLogin();
1068
+ if (!accessToken) {
1069
+ log.error("Login failed. Please try again.");
1070
+ return;
1071
+ }
1072
+ log.blank();
911
1073
  }
912
- const accessToken = tokens.access_token;
913
- const initSpinner = ora().start();
1074
+ const initSpinner = ora2().start();
914
1075
  const quota = await getSkillQuota(accessToken);
915
1076
  if (quota.error) {
916
- initSpinner.fail(pc5.red("Failed to initialize"));
1077
+ initSpinner.fail(pc6.red("Failed to initialize"));
917
1078
  return;
918
1079
  }
919
1080
  if (quota.tier !== "unlimited" && quota.remaining < 1) {
920
- initSpinner.fail(pc5.red("Weekly skill generation limit reached"));
1081
+ initSpinner.fail(pc6.red("Weekly skill generation limit reached"));
921
1082
  log.blank();
922
1083
  console.log(
923
- ` You've used ${pc5.bold(pc5.white(quota.used.toString()))}/${pc5.bold(pc5.white(quota.limit.toString()))} skill generations this week.`
1084
+ ` You've used ${pc6.bold(pc6.white(quota.used.toString()))}/${pc6.bold(pc6.white(quota.limit.toString()))} skill generations this week.`
924
1085
  );
925
1086
  console.log(
926
- ` Your quota resets on ${pc5.yellow(new Date(quota.resetDate).toLocaleDateString())}.`
1087
+ ` Your quota resets on ${pc6.yellow(new Date(quota.resetDate).toLocaleDateString())}.`
927
1088
  );
928
1089
  log.blank();
929
1090
  if (quota.tier === "free") {
930
1091
  console.log(
931
- ` ${pc5.yellow("Tip:")} Upgrade to Pro for ${pc5.bold("10")} generations per week.`
1092
+ ` ${pc6.yellow("Tip:")} Upgrade to Pro for ${pc6.bold("10")} generations per week.`
932
1093
  );
933
- console.log(` Visit ${pc5.green("https://context7.com/dashboard")} to upgrade.`);
1094
+ console.log(` Visit ${pc6.green("https://context7.com/dashboard")} to upgrade.`);
934
1095
  }
935
1096
  return;
936
1097
  }
937
1098
  initSpinner.stop();
938
1099
  initSpinner.clear();
939
- console.log(pc5.bold("What should your agent become an expert at?\n"));
1100
+ console.log(pc6.bold("What should your agent become an expert at?\n"));
940
1101
  console.log(
941
- pc5.dim("Skills teach agents best practices, design patterns, and domain expertise.\n")
1102
+ pc6.dim(
1103
+ "Skills should encode best practices, constraints, and decision-making \u2014\nnot step-by-step tutorials or one-off tasks.\n"
1104
+ )
942
1105
  );
943
- console.log(pc5.yellow("Examples:"));
944
- console.log(pc5.dim(' "React component optimization and performance best practices"'));
945
- console.log(pc5.dim(' "Responsive web design with Tailwind CSS"'));
946
- console.log(pc5.dim(' "Writing effective landing page copy"'));
947
- console.log(pc5.dim(' "Deploying Next.js apps to Vercel"'));
948
- console.log(pc5.dim(' "OAuth authentication with NextAuth.js"\n'));
1106
+ console.log(pc6.yellow("Examples:"));
1107
+ {
1108
+ console.log(pc6.red(' \u2715 "Deploy a Next.js app to Vercel"'));
1109
+ console.log(pc6.green(' \u2713 "Best practices and constraints for deploying Next.js apps to Vercel"'));
1110
+ log.blank();
1111
+ console.log(pc6.red(' \u2715 "Use Tailwind for responsive design"'));
1112
+ console.log(pc6.green(' \u2713 "Responsive layout decision-making with Tailwind CSS"'));
1113
+ log.blank();
1114
+ console.log(pc6.red(' \u2715 "Build OAuth with NextAuth"'));
1115
+ console.log(pc6.green(' \u2713 "OAuth authentication patterns and pitfalls with NextAuth.js"'));
1116
+ }
1117
+ log.blank();
949
1118
  let motivation;
950
1119
  try {
951
1120
  motivation = await input({
@@ -960,14 +1129,26 @@ async function generateCommand(options) {
960
1129
  log.warn("Generation cancelled");
961
1130
  return;
962
1131
  }
963
- const searchSpinner = ora("Finding relevant libraries...").start();
1132
+ log.blank();
1133
+ console.log(
1134
+ pc6.dim(
1135
+ "To generate this skill, we will read relevant documentation and examples\nfrom Context7.\n"
1136
+ )
1137
+ );
1138
+ console.log(
1139
+ pc6.dim(
1140
+ "These sources are used to:\n\u2022 extract best practices and constraints\n\u2022 compare patterns across official docs and examples\n\u2022 avoid outdated or incorrect guidance\n"
1141
+ )
1142
+ );
1143
+ console.log(pc6.dim("You can adjust which sources the skill is based on.\n"));
1144
+ const searchSpinner = ora2("Finding relevant sources...").start();
964
1145
  const searchResult = await searchLibraries(motivation, accessToken);
965
1146
  if (searchResult.error || !searchResult.results?.length) {
966
- searchSpinner.fail(pc5.red("No libraries found"));
1147
+ searchSpinner.fail(pc6.red("No sources found"));
967
1148
  log.warn(searchResult.message || "Try a different description");
968
1149
  return;
969
1150
  }
970
- searchSpinner.succeed(pc5.green(`Found ${searchResult.results.length} relevant libraries`));
1151
+ searchSpinner.succeed(pc6.green(`Found ${searchResult.results.length} relevant sources`));
971
1152
  log.blank();
972
1153
  let selectedLibraries;
973
1154
  try {
@@ -987,31 +1168,32 @@ async function generateCommand(options) {
987
1168
  const libraryChoices = libraries.map((lib, index) => {
988
1169
  const projectId = formatProjectId(lib.id);
989
1170
  const isGitHub = isGitHubRepo(lib.id);
990
- const indexStr = pc5.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
1171
+ const indexStr = pc6.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
991
1172
  const paddedName = lib.title.padEnd(maxNameLen);
992
1173
  const libUrl = `https://context7.com${lib.id}`;
993
- const libLink = terminalLink(lib.title, libUrl, pc5.white);
994
- const repoLink = isGitHub ? terminalLink(projectId, `https://github.com/${projectId}`, pc5.white) : pc5.white(projectId);
995
- const starsLine = lib.stars && isGitHub ? [`${pc5.yellow("Stars:")} ${lib.stars.toLocaleString()}`] : [];
1174
+ const libLink = terminalLink(lib.title, libUrl, pc6.white);
1175
+ const sourceUrl = isGitHub ? `https://github.com/${projectId}` : `https://context7.com${lib.id}`;
1176
+ const repoLink = terminalLink(projectId, sourceUrl, pc6.white);
1177
+ const starsLine = lib.stars && isGitHub ? [`${pc6.yellow("Stars:")} ${lib.stars.toLocaleString()}`] : [];
996
1178
  const metadataLines = [
997
- pc5.dim("\u2500".repeat(50)),
1179
+ pc6.dim("\u2500".repeat(50)),
998
1180
  "",
999
- `${pc5.yellow("Library:")} ${libLink}`,
1000
- `${pc5.yellow("Source:")} ${repoLink}`,
1001
- `${pc5.yellow("Snippets:")} ${lib.totalSnippets.toLocaleString()}`,
1181
+ `${pc6.yellow("Library:")} ${libLink}`,
1182
+ `${pc6.yellow("Source:")} ${repoLink}`,
1183
+ `${pc6.yellow("Snippets:")} ${lib.totalSnippets.toLocaleString()}`,
1002
1184
  ...starsLine,
1003
- `${pc5.yellow("Description:")}`,
1004
- pc5.white(lib.description || "No description")
1185
+ `${pc6.yellow("Description:")}`,
1186
+ pc6.white(lib.description || "No description")
1005
1187
  ];
1006
1188
  return {
1007
- name: `${indexStr} ${paddedName} ${pc5.dim(`(${projectId})`)}`,
1189
+ name: `${indexStr} ${paddedName} ${pc6.dim(`(${projectId})`)}`,
1008
1190
  value: lib,
1009
1191
  description: metadataLines.join("\n")
1010
1192
  };
1011
1193
  });
1012
1194
  selectedLibraries = await checkboxWithHover(
1013
1195
  {
1014
- message: "Select libraries:",
1196
+ message: "Select sources:",
1015
1197
  choices: libraryChoices,
1016
1198
  pageSize: 10,
1017
1199
  loop: false
@@ -1019,7 +1201,7 @@ async function generateCommand(options) {
1019
1201
  { getName: (lib) => `${lib.title} (${formatProjectId(lib.id)})` }
1020
1202
  );
1021
1203
  if (!selectedLibraries || selectedLibraries.length === 0) {
1022
- log.info("No libraries selected. Try running the command again.");
1204
+ log.info("No sources selected. Try running the command again.");
1023
1205
  return;
1024
1206
  }
1025
1207
  } catch {
@@ -1027,15 +1209,17 @@ async function generateCommand(options) {
1027
1209
  return;
1028
1210
  }
1029
1211
  log.blank();
1030
- const questionsSpinner = ora("Preparing questions...").start();
1212
+ const questionsSpinner = ora2(
1213
+ "Preparing follow-up questions to clarify scope and constraints..."
1214
+ ).start();
1031
1215
  const librariesInput = selectedLibraries.map((lib) => ({ id: lib.id, name: lib.title }));
1032
1216
  const questionsResult = await getSkillQuestions(librariesInput, motivation, accessToken);
1033
1217
  if (questionsResult.error || !questionsResult.questions?.length) {
1034
- questionsSpinner.fail(pc5.red("Failed to generate questions"));
1218
+ questionsSpinner.fail(pc6.red("Failed to generate questions"));
1035
1219
  log.warn(questionsResult.message || "Please try again");
1036
1220
  return;
1037
1221
  }
1038
- questionsSpinner.succeed(pc5.green("Questions prepared"));
1222
+ questionsSpinner.succeed(pc6.green("Questions prepared"));
1039
1223
  log.blank();
1040
1224
  const answers = [];
1041
1225
  try {
@@ -1044,7 +1228,7 @@ async function generateCommand(options) {
1044
1228
  const questionNum = i + 1;
1045
1229
  const totalQuestions = questionsResult.questions.length;
1046
1230
  const answer = await selectOrInput_default({
1047
- message: `${pc5.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`,
1231
+ message: `${pc6.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`,
1048
1232
  options: q.options,
1049
1233
  recommendedIndex: q.recommendedIndex
1050
1234
  });
@@ -1055,8 +1239,8 @@ async function generateCommand(options) {
1055
1239
  const linesToClear = 3 + q.options.length;
1056
1240
  process.stdout.write(`\x1B[${linesToClear}A\x1B[J`);
1057
1241
  const truncatedAnswer = answer.length > 50 ? answer.slice(0, 47) + "..." : answer;
1058
- console.log(`${pc5.green("\u2713")} ${pc5.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`);
1059
- console.log(` ${pc5.cyan(truncatedAnswer)}`);
1242
+ console.log(`${pc6.green("\u2713")} ${pc6.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`);
1243
+ console.log(` ${pc6.cyan(truncatedAnswer)}`);
1060
1244
  log.blank();
1061
1245
  }
1062
1246
  } catch {
@@ -1066,33 +1250,39 @@ async function generateCommand(options) {
1066
1250
  let generatedContent = null;
1067
1251
  let skillName = "";
1068
1252
  let feedback;
1069
- const libraryNames = selectedLibraries.map((lib) => lib.title).join(", ");
1253
+ let previewFile = null;
1254
+ let previewFileWritten = false;
1255
+ const cleanupPreviewFile = async () => {
1256
+ if (previewFile) {
1257
+ await unlink(previewFile).catch(() => {
1258
+ });
1259
+ }
1260
+ };
1070
1261
  const queryLog = [];
1071
1262
  let genSpinner = null;
1072
1263
  const formatQueryLogText = () => {
1073
1264
  if (queryLog.length === 0) return "";
1074
1265
  const lines = [];
1075
1266
  const latestEntry = queryLog[queryLog.length - 1];
1076
- lines.push(pc5.dim(`(${queryLog.length} ${queryLog.length === 1 ? "query" : "queries"})`));
1077
1267
  lines.push("");
1078
1268
  for (const result of latestEntry.results.slice(0, 3)) {
1079
1269
  const cleanContent = result.content.replace(/Source:\s*https?:\/\/[^\s]+/gi, "").trim();
1080
1270
  if (cleanContent) {
1081
- lines.push(` ${pc5.yellow("\u2022")} ${pc5.white(result.title)}`);
1271
+ lines.push(` ${pc6.yellow("\u2022")} ${pc6.white(result.title)}`);
1082
1272
  const maxLen = 400;
1083
1273
  const content = cleanContent.length > maxLen ? cleanContent.slice(0, maxLen - 3) + "..." : cleanContent;
1084
1274
  const words = content.split(" ");
1085
1275
  let currentLine = " ";
1086
1276
  for (const word of words) {
1087
1277
  if (currentLine.length + word.length > 84) {
1088
- lines.push(pc5.dim(currentLine));
1278
+ lines.push(pc6.dim(currentLine));
1089
1279
  currentLine = " " + word + " ";
1090
1280
  } else {
1091
1281
  currentLine += word + " ";
1092
1282
  }
1093
1283
  }
1094
1284
  if (currentLine.trim()) {
1095
- lines.push(pc5.dim(currentLine));
1285
+ lines.push(pc6.dim(currentLine));
1096
1286
  }
1097
1287
  lines.push("");
1098
1288
  }
@@ -1100,19 +1290,20 @@ async function generateCommand(options) {
1100
1290
  return "\n" + lines.join("\n");
1101
1291
  };
1102
1292
  let isGeneratingContent = false;
1293
+ let initialStatus = "Reading selected Context7 sources to generate the skill...";
1103
1294
  const handleStreamEvent = (event) => {
1104
1295
  if (event.type === "progress") {
1105
1296
  if (genSpinner) {
1106
1297
  if (event.message.startsWith("Generating skill content...") && !isGeneratingContent) {
1107
1298
  isGeneratingContent = true;
1108
1299
  if (queryLog.length > 0) {
1109
- genSpinner.succeed(pc5.green(`Queried documentation`));
1300
+ genSpinner.succeed(pc6.green(`Read Context7 sources`));
1110
1301
  } else {
1111
- genSpinner.succeed(pc5.green(`Ready to generate`));
1302
+ genSpinner.succeed(pc6.green(`Ready to generate`));
1112
1303
  }
1113
- genSpinner = ora("Generating skill content...").start();
1304
+ genSpinner = ora2("Generating skill content...").start();
1114
1305
  } else if (!isGeneratingContent) {
1115
- genSpinner.text = event.message + formatQueryLogText();
1306
+ genSpinner.text = initialStatus + formatQueryLogText();
1116
1307
  }
1117
1308
  }
1118
1309
  } else if (event.type === "tool_result") {
@@ -1136,18 +1327,19 @@ async function generateCommand(options) {
1136
1327
  };
1137
1328
  queryLog.length = 0;
1138
1329
  isGeneratingContent = false;
1139
- const initialStatus = feedback ? "Regenerating skill with your feedback..." : `Generating skill for "${libraryNames}"...`;
1140
- genSpinner = ora(initialStatus).start();
1330
+ previewFileWritten = false;
1331
+ initialStatus = feedback ? "Regenerating skill with your feedback..." : "Reading selected Context7 sources to generate the skill...";
1332
+ genSpinner = ora2(initialStatus).start();
1141
1333
  const result = await generateSkillStructured(generateInput, handleStreamEvent, accessToken);
1142
1334
  if (result.error) {
1143
- genSpinner.fail(pc5.red(`Error: ${result.error}`));
1335
+ genSpinner.fail(pc6.red(`Error: ${result.error}`));
1144
1336
  return;
1145
1337
  }
1146
1338
  if (!result.content) {
1147
- genSpinner.fail(pc5.red("No content generated"));
1339
+ genSpinner.fail(pc6.red("No content generated"));
1148
1340
  return;
1149
1341
  }
1150
- genSpinner.succeed(pc5.green(`Generated skill for "${result.libraryName}"`));
1342
+ genSpinner.succeed(pc6.green(`Generated skill for "${result.libraryName}"`));
1151
1343
  generatedContent = result.content;
1152
1344
  skillName = result.libraryName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
1153
1345
  const contentLines = generatedContent.split("\n");
@@ -1157,29 +1349,40 @@ async function generateCommand(options) {
1157
1349
  const remainingLines = contentLines.length - previewLineCount;
1158
1350
  const showPreview = () => {
1159
1351
  log.blank();
1160
- console.log(pc5.dim("\u2501".repeat(70)));
1161
- console.log(pc5.bold(`Generated Skill: `) + pc5.green(pc5.bold(skillName)));
1162
- console.log(pc5.dim("\u2501".repeat(70)));
1352
+ console.log(pc6.dim("\u2501".repeat(70)));
1353
+ console.log(pc6.bold(`Generated Skill: `) + pc6.green(pc6.bold(skillName)));
1354
+ console.log(pc6.dim("\u2501".repeat(70)));
1163
1355
  log.blank();
1164
1356
  console.log(previewContent);
1165
1357
  if (hasMoreLines) {
1166
1358
  log.blank();
1167
- console.log(pc5.dim(`... ${remainingLines} more lines`));
1359
+ console.log(pc6.dim(`... ${remainingLines} more lines`));
1168
1360
  }
1169
1361
  log.blank();
1170
- console.log(pc5.dim("\u2501".repeat(70)));
1362
+ console.log(pc6.dim("\u2501".repeat(70)));
1171
1363
  log.blank();
1172
1364
  };
1173
- const showFullContent = () => {
1174
- log.blank();
1175
- console.log(pc5.dim("\u2501".repeat(70)));
1176
- console.log(pc5.bold(`Generated Skill: `) + pc5.green(pc5.bold(skillName)));
1177
- console.log(pc5.dim("\u2501".repeat(70)));
1178
- log.blank();
1179
- console.log(generatedContent);
1180
- log.blank();
1181
- console.log(pc5.dim("\u2501".repeat(70)));
1182
- log.blank();
1365
+ const openInEditor = async () => {
1366
+ const previewDir = join4(homedir3(), ".context7", "previews");
1367
+ await mkdir2(previewDir, { recursive: true });
1368
+ previewFile = join4(previewDir, `${skillName}.md`);
1369
+ if (!previewFileWritten) {
1370
+ await writeFile2(previewFile, generatedContent, "utf-8");
1371
+ previewFileWritten = true;
1372
+ }
1373
+ const editor = process.env.EDITOR || "open";
1374
+ await new Promise((resolve) => {
1375
+ const child = spawn(editor, [previewFile], {
1376
+ stdio: "inherit",
1377
+ shell: true
1378
+ });
1379
+ child.on("close", () => resolve());
1380
+ });
1381
+ };
1382
+ const syncFromPreviewFile = async () => {
1383
+ if (previewFile) {
1384
+ generatedContent = await readFile(previewFile, "utf-8");
1385
+ }
1183
1386
  };
1184
1387
  showPreview();
1185
1388
  await new Promise((resolve) => setTimeout(resolve, 100));
@@ -1187,24 +1390,26 @@ async function generateCommand(options) {
1187
1390
  let action;
1188
1391
  while (true) {
1189
1392
  const choices = [
1190
- { name: `${pc5.green("\u2713")} Install skill`, value: "install" },
1191
- ...hasMoreLines ? [{ name: `${pc5.blue("\u2922")} View full skill`, value: "expand" }] : [],
1192
- { name: `${pc5.yellow("\u270E")} Request changes`, value: "feedback" },
1193
- { name: `${pc5.red("\u2715")} Cancel`, value: "cancel" }
1393
+ { name: `${pc6.green("\u2713")} Install skill (save locally)`, value: "install" },
1394
+ { name: `${pc6.blue("\u2922")} Edit skill in editor`, value: "view" },
1395
+ { name: `${pc6.yellow("\u270E")} Request changes`, value: "feedback" },
1396
+ { name: `${pc6.red("\u2715")} Cancel`, value: "cancel" }
1194
1397
  ];
1195
1398
  action = await select2({
1196
1399
  message: "What would you like to do?",
1197
1400
  choices
1198
1401
  });
1199
- if (action === "expand") {
1200
- showFullContent();
1402
+ if (action === "view") {
1403
+ await openInEditor();
1201
1404
  continue;
1202
1405
  }
1406
+ await syncFromPreviewFile();
1203
1407
  break;
1204
1408
  }
1205
1409
  if (action === "install") {
1206
1410
  break;
1207
1411
  } else if (action === "cancel") {
1412
+ await cleanupPreviewFile();
1208
1413
  log.warn("Generation cancelled");
1209
1414
  return;
1210
1415
  } else if (action === "feedback") {
@@ -1218,6 +1423,7 @@ async function generateCommand(options) {
1218
1423
  log.blank();
1219
1424
  }
1220
1425
  } catch {
1426
+ await cleanupPreviewFile();
1221
1427
  log.warn("Generation cancelled");
1222
1428
  return;
1223
1429
  }
@@ -1228,7 +1434,7 @@ async function generateCommand(options) {
1228
1434
  return;
1229
1435
  }
1230
1436
  const targetDirs = getTargetDirs(targets);
1231
- const writeSpinner = ora("Writing skill files...").start();
1437
+ const writeSpinner = ora2("Writing skill files...").start();
1232
1438
  let permissionError = false;
1233
1439
  const failedDirs = /* @__PURE__ */ new Set();
1234
1440
  for (const targetDir of targetDirs) {
@@ -1252,24 +1458,25 @@ async function generateCommand(options) {
1252
1458
  }
1253
1459
  }
1254
1460
  if (permissionError) {
1255
- writeSpinner.fail(pc5.red("Permission denied"));
1461
+ writeSpinner.fail(pc6.red("Permission denied"));
1256
1462
  log.blank();
1257
- console.log(pc5.yellow("Fix permissions with:"));
1463
+ console.log(pc6.yellow("Fix permissions with:"));
1258
1464
  for (const dir of failedDirs) {
1259
1465
  const parentDir = join4(dir, "..");
1260
- console.log(pc5.dim(` sudo chown -R $(whoami) "${parentDir}"`));
1466
+ console.log(pc6.dim(` sudo chown -R $(whoami) "${parentDir}"`));
1261
1467
  }
1262
1468
  log.blank();
1263
1469
  return;
1264
1470
  }
1265
- writeSpinner.succeed(pc5.green(`Created skill in ${targetDirs.length} location(s)`));
1471
+ writeSpinner.succeed(pc6.green(`Created skill in ${targetDirs.length} location(s)`));
1266
1472
  trackEvent("gen_install");
1267
1473
  log.blank();
1268
- console.log(pc5.green(pc5.bold("Skill saved successfully")));
1474
+ console.log(pc6.green("Skill saved successfully"));
1269
1475
  for (const targetDir of targetDirs) {
1270
- console.log(pc5.dim(` ${targetDir}/`) + pc5.green(skillName));
1476
+ console.log(pc6.dim(` ${targetDir}/`) + pc6.green(skillName));
1271
1477
  }
1272
1478
  log.blank();
1479
+ await cleanupPreviewFile();
1273
1480
  }
1274
1481
 
1275
1482
  // src/commands/skill.ts
@@ -1327,17 +1534,17 @@ async function installCommand(input2, skillName, options) {
1327
1534
  }
1328
1535
  const repo = `/${parsed.owner}/${parsed.repo}`;
1329
1536
  log.blank();
1330
- const spinner = ora2(`Fetching skills from ${repo}...`).start();
1537
+ const spinner = ora3(`Fetching skills from ${repo}...`).start();
1331
1538
  let selectedSkills;
1332
1539
  if (skillName) {
1333
1540
  spinner.text = `Fetching skill: ${skillName}...`;
1334
1541
  const skillData = await getSkill(repo, skillName);
1335
1542
  if (skillData.error || !skillData.name) {
1336
1543
  if (skillData.error === "prompt_injection_detected") {
1337
- spinner.fail(pc6.red(`Prompt injection detected in skill: ${skillName}`));
1544
+ spinner.fail(pc7.red(`Prompt injection detected in skill: ${skillName}`));
1338
1545
  log.warn("This skill contains potentially malicious content and cannot be installed.");
1339
1546
  } else {
1340
- spinner.fail(pc6.red(`Skill not found: ${skillName}`));
1547
+ spinner.fail(pc7.red(`Skill not found: ${skillName}`));
1341
1548
  }
1342
1549
  return;
1343
1550
  }
@@ -1353,11 +1560,11 @@ async function installCommand(input2, skillName, options) {
1353
1560
  } else {
1354
1561
  const data = await listProjectSkills(repo);
1355
1562
  if (data.error) {
1356
- spinner.fail(pc6.red(`Error: ${data.message || data.error}`));
1563
+ spinner.fail(pc7.red(`Error: ${data.message || data.error}`));
1357
1564
  return;
1358
1565
  }
1359
1566
  if (!data.skills || data.skills.length === 0) {
1360
- spinner.warn(pc6.yellow(`No skills found in ${repo}`));
1567
+ spinner.warn(pc7.yellow(`No skills found in ${repo}`));
1361
1568
  return;
1362
1569
  }
1363
1570
  const skillsWithRepo = data.skills.map((s) => ({ ...s, project: repo })).sort((a, b) => (b.installCount ?? 0) - (a.installCount ?? 0));
@@ -1375,19 +1582,19 @@ async function installCommand(input2, skillName, options) {
1375
1582
  const indexWidth = data.skills.length.toString().length;
1376
1583
  const maxNameLen = Math.max(...data.skills.map((s) => s.name.length));
1377
1584
  const choices = skillsWithRepo.map((s, index) => {
1378
- const indexStr = pc6.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
1585
+ const indexStr = pc7.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
1379
1586
  const paddedName = s.name.padEnd(maxNameLen);
1380
1587
  const installs = formatInstallCount(s.installCount);
1381
1588
  const skillUrl = `https://context7.com/skills${s.project}/${s.name}`;
1382
- const skillLink = terminalLink(s.name, skillUrl, pc6.white);
1383
- const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc6.white);
1589
+ const skillLink = terminalLink(s.name, skillUrl, pc7.white);
1590
+ const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc7.white);
1384
1591
  const metadataLines = [
1385
- pc6.dim("\u2500".repeat(50)),
1592
+ pc7.dim("\u2500".repeat(50)),
1386
1593
  "",
1387
- `${pc6.yellow("Skill:")} ${skillLink}`,
1388
- `${pc6.yellow("Repo:")} ${repoLink}`,
1389
- `${pc6.yellow("Description:")}`,
1390
- pc6.white(s.description || "No description")
1594
+ `${pc7.yellow("Skill:")} ${skillLink}`,
1595
+ `${pc7.yellow("Repo:")} ${repoLink}`,
1596
+ `${pc7.yellow("Description:")}`,
1597
+ pc7.white(s.description || "No description")
1391
1598
  ];
1392
1599
  return {
1393
1600
  name: installs ? `${indexStr} ${paddedName} ${installs}` : `${indexStr} ${paddedName}`,
@@ -1397,7 +1604,7 @@ async function installCommand(input2, skillName, options) {
1397
1604
  });
1398
1605
  log.blank();
1399
1606
  const installsOffset = 4 + indexWidth + 1 + 1 + maxNameLen + 1 - 3;
1400
- const message = "Select skills:" + " ".repeat(Math.max(1, installsOffset - 14)) + pc6.dim("installs");
1607
+ const message = "Select skills:" + " ".repeat(Math.max(1, installsOffset - 14)) + pc7.dim("installs");
1401
1608
  try {
1402
1609
  selectedSkills = await checkboxWithHover({
1403
1610
  message,
@@ -1421,7 +1628,7 @@ async function installCommand(input2, skillName, options) {
1421
1628
  return;
1422
1629
  }
1423
1630
  const targetDirs = getTargetDirs(targets);
1424
- const installSpinner = ora2("Installing skills...").start();
1631
+ const installSpinner = ora3("Installing skills...").start();
1425
1632
  let permissionError = false;
1426
1633
  const failedDirs = /* @__PURE__ */ new Set();
1427
1634
  const installedSkills = [];
@@ -1487,53 +1694,58 @@ async function installCommand(input2, skillName, options) {
1487
1694
  async function searchCommand(query) {
1488
1695
  trackEvent("command", { name: "search" });
1489
1696
  log.blank();
1490
- const spinner = ora2(`Searching for "${query}"...`).start();
1697
+ const spinner = ora3(`Searching for "${query}"...`).start();
1491
1698
  let data;
1492
1699
  try {
1493
1700
  data = await searchSkills(query);
1494
1701
  } catch (err) {
1495
- spinner.fail(pc6.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
1702
+ spinner.fail(pc7.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
1496
1703
  return;
1497
1704
  }
1498
1705
  if (data.error) {
1499
- spinner.fail(pc6.red(`Error: ${data.message || data.error}`));
1706
+ spinner.fail(pc7.red(`Error: ${data.message || data.error}`));
1500
1707
  return;
1501
1708
  }
1502
1709
  if (!data.results || data.results.length === 0) {
1503
- spinner.warn(pc6.yellow(`No skills found matching "${query}"`));
1710
+ spinner.warn(pc7.yellow(`No skills found matching "${query}"`));
1504
1711
  return;
1505
1712
  }
1506
1713
  spinner.succeed(`Found ${data.results.length} skill(s)`);
1507
1714
  trackEvent("search_query", { query, resultCount: data.results.length });
1508
1715
  const indexWidth = data.results.length.toString().length;
1509
1716
  const maxNameLen = Math.max(...data.results.map((s) => s.name.length));
1717
+ const installsColWidth = 10;
1510
1718
  const choices = data.results.map((s, index) => {
1511
- const indexStr = pc6.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
1719
+ const indexStr = pc7.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
1512
1720
  const paddedName = s.name.padEnd(maxNameLen);
1513
- const installs = formatInstallCount(s.installCount);
1721
+ const installsRaw = s.installCount ? String(s.installCount) : "-";
1722
+ const paddedInstalls = formatInstallCount(s.installCount, pc7.dim("-")) + " ".repeat(installsColWidth - installsRaw.length);
1723
+ const trust = formatTrustScore(s.trustScore);
1514
1724
  const skillLink = terminalLink(
1515
1725
  s.name,
1516
1726
  `https://context7.com/skills${s.project}/${s.name}`,
1517
- pc6.white
1727
+ pc7.white
1518
1728
  );
1519
- const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc6.white);
1729
+ const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc7.white);
1520
1730
  const metadataLines = [
1521
- pc6.dim("\u2500".repeat(50)),
1731
+ pc7.dim("\u2500".repeat(50)),
1522
1732
  "",
1523
- `${pc6.yellow("Skill:")} ${skillLink}`,
1524
- `${pc6.yellow("Repo:")} ${repoLink}`,
1525
- `${pc6.yellow("Description:")}`,
1526
- pc6.white(s.description || "No description")
1733
+ `${pc7.yellow("Skill:")} ${skillLink}`,
1734
+ `${pc7.yellow("Repo:")} ${repoLink}`,
1735
+ `${pc7.yellow("Description:")}`,
1736
+ pc7.white(s.description || "No description")
1527
1737
  ];
1528
1738
  return {
1529
- name: installs ? `${indexStr} ${paddedName} ${installs}` : `${indexStr} ${paddedName}`,
1739
+ name: `${indexStr} ${paddedName} ${paddedInstalls}${trust}`,
1530
1740
  value: s,
1531
1741
  description: metadataLines.join("\n")
1532
1742
  };
1533
1743
  });
1534
1744
  log.blank();
1535
- const installsOffset = 4 + indexWidth + 1 + 1 + maxNameLen + 1 - 3;
1536
- const message = "Select skills to install:" + " ".repeat(Math.max(1, installsOffset - 25)) + pc6.dim("installs");
1745
+ const checkboxPrefixWidth = 3;
1746
+ const headerPad = " ".repeat(checkboxPrefixWidth + indexWidth + 1 + 1 + maxNameLen + 1);
1747
+ const headerLine = headerPad + pc7.dim("Installs".padEnd(installsColWidth)) + pc7.dim("Trust(0-10)");
1748
+ const message = "Select skills to install:\n" + headerLine;
1537
1749
  let selectedSkills;
1538
1750
  try {
1539
1751
  selectedSkills = await checkboxWithHover({
@@ -1557,7 +1769,7 @@ async function searchCommand(query) {
1557
1769
  return;
1558
1770
  }
1559
1771
  const targetDirs = getTargetDirs(targets);
1560
- const installSpinner = ora2("Installing skills...").start();
1772
+ const installSpinner = ora3("Installing skills...").start();
1561
1773
  let permissionError = false;
1562
1774
  const failedDirs = /* @__PURE__ */ new Set();
1563
1775
  const installedSkills = [];
@@ -1646,9 +1858,9 @@ async function listCommand(options) {
1646
1858
  for (const { ide, skills } of results) {
1647
1859
  const ideName = IDE_NAMES[ide];
1648
1860
  const path2 = pathMap[ide];
1649
- log.plain(`${pc6.bold(ideName)} ${pc6.dim(path2)}`);
1861
+ log.plain(`${pc7.bold(ideName)} ${pc7.dim(path2)}`);
1650
1862
  for (const skill of skills) {
1651
- log.plain(` ${pc6.green(skill)}`);
1863
+ log.plain(` ${pc7.green(skill)}`);
1652
1864
  }
1653
1865
  log.blank();
1654
1866
  }
@@ -1688,14 +1900,14 @@ async function infoCommand(input2) {
1688
1900
  }
1689
1901
  const repo = `/${parsed.owner}/${parsed.repo}`;
1690
1902
  log.blank();
1691
- const spinner = ora2(`Fetching skills from ${repo}...`).start();
1903
+ const spinner = ora3(`Fetching skills from ${repo}...`).start();
1692
1904
  const data = await listProjectSkills(repo);
1693
1905
  if (data.error) {
1694
- spinner.fail(pc6.red(`Error: ${data.message || data.error}`));
1906
+ spinner.fail(pc7.red(`Error: ${data.message || data.error}`));
1695
1907
  return;
1696
1908
  }
1697
1909
  if (!data.skills || data.skills.length === 0) {
1698
- spinner.warn(pc6.yellow(`No skills found in ${repo}`));
1910
+ spinner.warn(pc7.yellow(`No skills found in ${repo}`));
1699
1911
  return;
1700
1912
  }
1701
1913
  spinner.succeed(`Found ${data.skills.length} skill(s)`);
@@ -1707,148 +1919,13 @@ async function infoCommand(input2) {
1707
1919
  log.blank();
1708
1920
  }
1709
1921
  log.plain(
1710
- `${pc6.bold("Quick commands:")}
1711
- Install all: ${pc6.cyan(`ctx7 skills install ${repo} --all`)}
1712
- Install one: ${pc6.cyan(`ctx7 skills install ${repo} ${data.skills[0]?.name}`)}
1922
+ `${pc7.bold("Quick commands:")}
1923
+ Install all: ${pc7.cyan(`ctx7 skills install ${repo} --all`)}
1924
+ Install one: ${pc7.cyan(`ctx7 skills install ${repo} ${data.skills[0]?.name}`)}
1713
1925
  `
1714
1926
  );
1715
1927
  }
1716
1928
 
1717
- // src/commands/auth.ts
1718
- import pc7 from "picocolors";
1719
- import ora3 from "ora";
1720
- import open from "open";
1721
- var CLI_CLIENT_ID = "2veBSofhicRBguUT";
1722
- var baseUrl2 = "https://context7.com";
1723
- function setAuthBaseUrl(url) {
1724
- baseUrl2 = url;
1725
- }
1726
- function registerAuthCommands(program2) {
1727
- program2.command("login").description("Log in to Context7").option("--no-browser", "Don't open browser automatically").action(async (options) => {
1728
- await loginCommand(options);
1729
- });
1730
- program2.command("logout").description("Log out of Context7").action(() => {
1731
- logoutCommand();
1732
- });
1733
- program2.command("whoami").description("Show current login status").action(async () => {
1734
- await whoamiCommand();
1735
- });
1736
- }
1737
- async function loginCommand(options) {
1738
- trackEvent("command", { name: "login" });
1739
- const existingTokens = loadTokens();
1740
- if (existingTokens) {
1741
- const expired = isTokenExpired(existingTokens);
1742
- if (!expired || existingTokens.refresh_token) {
1743
- console.log(pc7.yellow("You are already logged in."));
1744
- console.log(
1745
- pc7.dim("Run 'ctx7 logout' first if you want to log in with a different account.")
1746
- );
1747
- return;
1748
- }
1749
- clearTokens();
1750
- }
1751
- const spinner = ora3("Preparing login...").start();
1752
- try {
1753
- const { codeVerifier, codeChallenge } = generatePKCE();
1754
- const state = generateState();
1755
- const callbackServer = createCallbackServer(state);
1756
- const port = await callbackServer.port;
1757
- const redirectUri = `http://localhost:${port}/callback`;
1758
- const authUrl = buildAuthorizationUrl(
1759
- baseUrl2,
1760
- CLI_CLIENT_ID,
1761
- redirectUri,
1762
- codeChallenge,
1763
- state
1764
- );
1765
- spinner.stop();
1766
- console.log("");
1767
- console.log(pc7.bold("Opening browser to log in..."));
1768
- console.log("");
1769
- if (options.browser) {
1770
- await open(authUrl);
1771
- console.log(pc7.dim("If the browser didn't open, visit this URL:"));
1772
- } else {
1773
- console.log(pc7.dim("Open this URL in your browser:"));
1774
- }
1775
- console.log(pc7.cyan(authUrl));
1776
- console.log("");
1777
- const waitingSpinner = ora3("Waiting for login...").start();
1778
- try {
1779
- const { code } = await callbackServer.result;
1780
- waitingSpinner.text = "Exchanging code for tokens...";
1781
- const tokens = await exchangeCodeForTokens(
1782
- baseUrl2,
1783
- code,
1784
- codeVerifier,
1785
- redirectUri,
1786
- CLI_CLIENT_ID
1787
- );
1788
- saveTokens(tokens);
1789
- callbackServer.close();
1790
- waitingSpinner.succeed(pc7.green("Login successful!"));
1791
- console.log("");
1792
- console.log(pc7.dim("You can now use authenticated Context7 features."));
1793
- } catch (error) {
1794
- callbackServer.close();
1795
- waitingSpinner.fail(pc7.red("Login failed"));
1796
- if (error instanceof Error) {
1797
- console.error(pc7.red(error.message));
1798
- }
1799
- process.exit(1);
1800
- }
1801
- } catch (error) {
1802
- spinner.fail(pc7.red("Login failed"));
1803
- if (error instanceof Error) {
1804
- console.error(pc7.red(error.message));
1805
- }
1806
- process.exit(1);
1807
- }
1808
- }
1809
- function logoutCommand() {
1810
- trackEvent("command", { name: "logout" });
1811
- if (clearTokens()) {
1812
- console.log(pc7.green("Logged out successfully."));
1813
- } else {
1814
- console.log(pc7.yellow("You are not logged in."));
1815
- }
1816
- }
1817
- async function whoamiCommand() {
1818
- trackEvent("command", { name: "whoami" });
1819
- const tokens = loadTokens();
1820
- if (!tokens) {
1821
- console.log(pc7.yellow("Not logged in."));
1822
- console.log(pc7.dim("Run 'ctx7 login' to authenticate."));
1823
- return;
1824
- }
1825
- console.log(pc7.green("Logged in"));
1826
- try {
1827
- const userInfo = await fetchUserInfo(tokens.access_token);
1828
- if (userInfo.name) {
1829
- console.log(`${pc7.dim("Name:".padEnd(9))}${userInfo.name}`);
1830
- }
1831
- if (userInfo.email) {
1832
- console.log(`${pc7.dim("Email:".padEnd(9))}${userInfo.email}`);
1833
- }
1834
- } catch {
1835
- if (isTokenExpired(tokens) && !tokens.refresh_token) {
1836
- console.log(pc7.dim("(Session may be expired - run 'ctx7 login' to refresh)"));
1837
- }
1838
- }
1839
- }
1840
- async function fetchUserInfo(accessToken) {
1841
- const response = await fetch("https://clerk.context7.com/oauth/userinfo", {
1842
- headers: {
1843
- Authorization: `Bearer ${accessToken}`
1844
- }
1845
- });
1846
- if (!response.ok) {
1847
- throw new Error("Failed to fetch user info");
1848
- }
1849
- return await response.json();
1850
- }
1851
-
1852
1929
  // src/constants.ts
1853
1930
  import { readFileSync as readFileSync2 } from "fs";
1854
1931
  import { fileURLToPath } from "url";