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 +358 -281
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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
|
|
10
|
-
import
|
|
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
|
|
612
|
-
import
|
|
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
|
|
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
|
|
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[
|
|
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} ${
|
|
1025
|
+
let output = `${prefix} ${pc5.bold(message)}
|
|
869
1026
|
|
|
870
1027
|
`;
|
|
871
1028
|
options.forEach((opt, idx) => {
|
|
872
|
-
const isRecommended = idx ===
|
|
1029
|
+
const isRecommended = idx === 0;
|
|
873
1030
|
const isCursor = idx === cursor;
|
|
874
|
-
const number =
|
|
875
|
-
const text = isRecommended ? `${opt} ${
|
|
1031
|
+
const number = pc5.cyan(`${idx + 1}.`);
|
|
1032
|
+
const text = isRecommended ? `${opt} ${pc5.green("\u2713 Recommended")}` : opt;
|
|
876
1033
|
if (isCursor) {
|
|
877
|
-
output +=
|
|
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 +=
|
|
1043
|
+
output += pc5.cyan(`\u276F ${pc5.yellow("\u270E")} ${inputValue || pc5.dim("Type your own...")}`);
|
|
887
1044
|
} else {
|
|
888
|
-
output += ` ${
|
|
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
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
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
|
|
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(
|
|
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(
|
|
1081
|
+
initSpinner.fail(pc6.red("Weekly skill generation limit reached"));
|
|
921
1082
|
log.blank();
|
|
922
1083
|
console.log(
|
|
923
|
-
` You've used ${
|
|
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 ${
|
|
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
|
-
` ${
|
|
1092
|
+
` ${pc6.yellow("Tip:")} Upgrade to Pro for ${pc6.bold("10")} generations per week.`
|
|
932
1093
|
);
|
|
933
|
-
console.log(` Visit ${
|
|
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(
|
|
1100
|
+
console.log(pc6.bold("What should your agent become an expert at?\n"));
|
|
940
1101
|
console.log(
|
|
941
|
-
|
|
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(
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
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
|
-
|
|
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(
|
|
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(
|
|
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 =
|
|
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,
|
|
994
|
-
const
|
|
995
|
-
const
|
|
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
|
-
|
|
1179
|
+
pc6.dim("\u2500".repeat(50)),
|
|
998
1180
|
"",
|
|
999
|
-
`${
|
|
1000
|
-
`${
|
|
1001
|
-
`${
|
|
1181
|
+
`${pc6.yellow("Library:")} ${libLink}`,
|
|
1182
|
+
`${pc6.yellow("Source:")} ${repoLink}`,
|
|
1183
|
+
`${pc6.yellow("Snippets:")} ${lib.totalSnippets.toLocaleString()}`,
|
|
1002
1184
|
...starsLine,
|
|
1003
|
-
`${
|
|
1004
|
-
|
|
1185
|
+
`${pc6.yellow("Description:")}`,
|
|
1186
|
+
pc6.white(lib.description || "No description")
|
|
1005
1187
|
];
|
|
1006
1188
|
return {
|
|
1007
|
-
name: `${indexStr} ${paddedName} ${
|
|
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
|
|
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
|
|
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 =
|
|
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(
|
|
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(
|
|
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: `${
|
|
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(`${
|
|
1059
|
-
console.log(` ${
|
|
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
|
-
|
|
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(` ${
|
|
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(
|
|
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(
|
|
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(
|
|
1300
|
+
genSpinner.succeed(pc6.green(`Read Context7 sources`));
|
|
1110
1301
|
} else {
|
|
1111
|
-
genSpinner.succeed(
|
|
1302
|
+
genSpinner.succeed(pc6.green(`Ready to generate`));
|
|
1112
1303
|
}
|
|
1113
|
-
genSpinner =
|
|
1304
|
+
genSpinner = ora2("Generating skill content...").start();
|
|
1114
1305
|
} else if (!isGeneratingContent) {
|
|
1115
|
-
genSpinner.text =
|
|
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
|
-
|
|
1140
|
-
|
|
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(
|
|
1335
|
+
genSpinner.fail(pc6.red(`Error: ${result.error}`));
|
|
1144
1336
|
return;
|
|
1145
1337
|
}
|
|
1146
1338
|
if (!result.content) {
|
|
1147
|
-
genSpinner.fail(
|
|
1339
|
+
genSpinner.fail(pc6.red("No content generated"));
|
|
1148
1340
|
return;
|
|
1149
1341
|
}
|
|
1150
|
-
genSpinner.succeed(
|
|
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(
|
|
1161
|
-
console.log(
|
|
1162
|
-
console.log(
|
|
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(
|
|
1359
|
+
console.log(pc6.dim(`... ${remainingLines} more lines`));
|
|
1168
1360
|
}
|
|
1169
1361
|
log.blank();
|
|
1170
|
-
console.log(
|
|
1362
|
+
console.log(pc6.dim("\u2501".repeat(70)));
|
|
1171
1363
|
log.blank();
|
|
1172
1364
|
};
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
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: `${
|
|
1191
|
-
|
|
1192
|
-
{ name: `${
|
|
1193
|
-
{ name: `${
|
|
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 === "
|
|
1200
|
-
|
|
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 =
|
|
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(
|
|
1461
|
+
writeSpinner.fail(pc6.red("Permission denied"));
|
|
1256
1462
|
log.blank();
|
|
1257
|
-
console.log(
|
|
1463
|
+
console.log(pc6.yellow("Fix permissions with:"));
|
|
1258
1464
|
for (const dir of failedDirs) {
|
|
1259
1465
|
const parentDir = join4(dir, "..");
|
|
1260
|
-
console.log(
|
|
1466
|
+
console.log(pc6.dim(` sudo chown -R $(whoami) "${parentDir}"`));
|
|
1261
1467
|
}
|
|
1262
1468
|
log.blank();
|
|
1263
1469
|
return;
|
|
1264
1470
|
}
|
|
1265
|
-
writeSpinner.succeed(
|
|
1471
|
+
writeSpinner.succeed(pc6.green(`Created skill in ${targetDirs.length} location(s)`));
|
|
1266
1472
|
trackEvent("gen_install");
|
|
1267
1473
|
log.blank();
|
|
1268
|
-
console.log(
|
|
1474
|
+
console.log(pc6.green("Skill saved successfully"));
|
|
1269
1475
|
for (const targetDir of targetDirs) {
|
|
1270
|
-
console.log(
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
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,
|
|
1383
|
-
const repoLink = terminalLink(s.project, `https://github.com${s.project}`,
|
|
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
|
-
|
|
1592
|
+
pc7.dim("\u2500".repeat(50)),
|
|
1386
1593
|
"",
|
|
1387
|
-
`${
|
|
1388
|
-
`${
|
|
1389
|
-
`${
|
|
1390
|
-
|
|
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)) +
|
|
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 =
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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 =
|
|
1719
|
+
const indexStr = pc7.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
|
|
1512
1720
|
const paddedName = s.name.padEnd(maxNameLen);
|
|
1513
|
-
const
|
|
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
|
-
|
|
1727
|
+
pc7.white
|
|
1518
1728
|
);
|
|
1519
|
-
const repoLink = terminalLink(s.project, `https://github.com${s.project}`,
|
|
1729
|
+
const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc7.white);
|
|
1520
1730
|
const metadataLines = [
|
|
1521
|
-
|
|
1731
|
+
pc7.dim("\u2500".repeat(50)),
|
|
1522
1732
|
"",
|
|
1523
|
-
`${
|
|
1524
|
-
`${
|
|
1525
|
-
`${
|
|
1526
|
-
|
|
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:
|
|
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
|
|
1536
|
-
const
|
|
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 =
|
|
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(`${
|
|
1861
|
+
log.plain(`${pc7.bold(ideName)} ${pc7.dim(path2)}`);
|
|
1650
1862
|
for (const skill of skills) {
|
|
1651
|
-
log.plain(` ${
|
|
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 =
|
|
1903
|
+
const spinner = ora3(`Fetching skills from ${repo}...`).start();
|
|
1692
1904
|
const data = await listProjectSkills(repo);
|
|
1693
1905
|
if (data.error) {
|
|
1694
|
-
spinner.fail(
|
|
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(
|
|
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
|
-
`${
|
|
1711
|
-
Install all: ${
|
|
1712
|
-
Install one: ${
|
|
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";
|