ctx7 0.2.0 → 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 +388 -302
- 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
|
|
|
@@ -117,6 +117,9 @@ async function downloadSkillFromGitHub(skill) {
|
|
|
117
117
|
|
|
118
118
|
// src/utils/api.ts
|
|
119
119
|
var baseUrl = "https://context7.com";
|
|
120
|
+
function getBaseUrl() {
|
|
121
|
+
return baseUrl;
|
|
122
|
+
}
|
|
120
123
|
function setBaseUrl(url) {
|
|
121
124
|
baseUrl = url;
|
|
122
125
|
}
|
|
@@ -135,15 +138,6 @@ async function searchSkills(query) {
|
|
|
135
138
|
const response = await fetch(`${baseUrl}/api/v2/skills?${params}`);
|
|
136
139
|
return await response.json();
|
|
137
140
|
}
|
|
138
|
-
function trackInstalls(skills, ides) {
|
|
139
|
-
if (process.env.CTX7_TELEMETRY_DISABLED || !skills.length) return;
|
|
140
|
-
fetch(`${baseUrl}/api/v2/skills/track`, {
|
|
141
|
-
method: "POST",
|
|
142
|
-
headers: { "Content-Type": "application/json" },
|
|
143
|
-
body: JSON.stringify({ skills, ides })
|
|
144
|
-
}).catch(() => {
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
141
|
async function downloadSkill(project, skillName) {
|
|
148
142
|
const skillData = await getSkill(project, skillName);
|
|
149
143
|
if (skillData.error) {
|
|
@@ -527,23 +521,16 @@ import { checkbox as checkbox2 } from "@inquirer/prompts";
|
|
|
527
521
|
import readline from "readline";
|
|
528
522
|
function terminalLink(text, url, color) {
|
|
529
523
|
const colorFn = color ?? ((s) => s);
|
|
530
|
-
return `\x1B]8;;${url}\x07${colorFn(text)}\x1B]8;;\x07`;
|
|
524
|
+
return `\x1B]8;;${url}\x07${colorFn(text)}\x1B]8;;\x07 ${pc3.white("\u2197")}`;
|
|
531
525
|
}
|
|
532
|
-
function formatInstallCount(count) {
|
|
533
|
-
if (count === void 0 || count === 0) return
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
} else if (count >= 10) {
|
|
541
|
-
const tens = Math.floor(count / 10) * 10;
|
|
542
|
-
display = `${tens}+`;
|
|
543
|
-
} else {
|
|
544
|
-
display = String(count);
|
|
545
|
-
}
|
|
546
|
-
return `\x1B[38;5;214m\u2193${display}\x1B[0m`;
|
|
526
|
+
function formatInstallCount(count, placeholder = "") {
|
|
527
|
+
if (count === void 0 || count === 0) return placeholder;
|
|
528
|
+
return pc3.yellow(String(count));
|
|
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));
|
|
547
534
|
}
|
|
548
535
|
async function checkboxWithHover(config, options) {
|
|
549
536
|
const choices = config.choices.filter(
|
|
@@ -614,12 +601,24 @@ async function symlinkSkill(skillName, sourcePath, targetDir) {
|
|
|
614
601
|
await symlink(sourcePath, targetPath);
|
|
615
602
|
}
|
|
616
603
|
|
|
604
|
+
// src/utils/tracking.ts
|
|
605
|
+
function trackEvent(event, data) {
|
|
606
|
+
if (process.env.CTX7_TELEMETRY_DISABLED) return;
|
|
607
|
+
fetch(`${getBaseUrl()}/api/v2/cli/events`, {
|
|
608
|
+
method: "POST",
|
|
609
|
+
headers: { "Content-Type": "application/json" },
|
|
610
|
+
body: JSON.stringify({ event, data })
|
|
611
|
+
}).catch(() => {
|
|
612
|
+
});
|
|
613
|
+
}
|
|
614
|
+
|
|
617
615
|
// src/commands/generate.ts
|
|
618
|
-
import
|
|
619
|
-
import
|
|
620
|
-
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";
|
|
621
619
|
import { join as join4 } from "path";
|
|
622
620
|
import { homedir as homedir3 } from "os";
|
|
621
|
+
import { spawn } from "child_process";
|
|
623
622
|
import { input, select as select2 } from "@inquirer/prompts";
|
|
624
623
|
|
|
625
624
|
// src/utils/auth.ts
|
|
@@ -814,6 +813,148 @@ function buildAuthorizationUrl(baseUrl3, clientId, redirectUri, codeChallenge, s
|
|
|
814
813
|
return url.toString();
|
|
815
814
|
}
|
|
816
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
|
+
|
|
817
958
|
// src/utils/selectOrInput.ts
|
|
818
959
|
import {
|
|
819
960
|
createPrompt,
|
|
@@ -824,10 +965,19 @@ import {
|
|
|
824
965
|
isUpKey,
|
|
825
966
|
isDownKey
|
|
826
967
|
} from "@inquirer/core";
|
|
827
|
-
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
|
+
}
|
|
828
977
|
var selectOrInput = createPrompt((config, done) => {
|
|
829
|
-
const { message, options, recommendedIndex = 0 } = config;
|
|
830
|
-
const
|
|
978
|
+
const { message, options: rawOptions, recommendedIndex = 0 } = config;
|
|
979
|
+
const options = reorderOptions(rawOptions, recommendedIndex);
|
|
980
|
+
const [cursor, setCursor] = useState(0);
|
|
831
981
|
const [inputValue, setInputValue] = useState("");
|
|
832
982
|
const prefix = usePrefix({});
|
|
833
983
|
useKeypress((key, rl) => {
|
|
@@ -842,7 +992,7 @@ var selectOrInput = createPrompt((config, done) => {
|
|
|
842
992
|
if (isEnterKey(key)) {
|
|
843
993
|
if (cursor === options.length) {
|
|
844
994
|
const finalValue = inputValue.trim();
|
|
845
|
-
done(finalValue || options[
|
|
995
|
+
done(finalValue || options[0]);
|
|
846
996
|
} else {
|
|
847
997
|
done(options[cursor]);
|
|
848
998
|
}
|
|
@@ -872,16 +1022,16 @@ var selectOrInput = createPrompt((config, done) => {
|
|
|
872
1022
|
rl.line = "";
|
|
873
1023
|
}
|
|
874
1024
|
});
|
|
875
|
-
let output = `${prefix} ${
|
|
1025
|
+
let output = `${prefix} ${pc5.bold(message)}
|
|
876
1026
|
|
|
877
1027
|
`;
|
|
878
1028
|
options.forEach((opt, idx) => {
|
|
879
|
-
const isRecommended = idx ===
|
|
1029
|
+
const isRecommended = idx === 0;
|
|
880
1030
|
const isCursor = idx === cursor;
|
|
881
|
-
const number =
|
|
882
|
-
const text = isRecommended ? `${opt} ${
|
|
1031
|
+
const number = pc5.cyan(`${idx + 1}.`);
|
|
1032
|
+
const text = isRecommended ? `${opt} ${pc5.green("\u2713 Recommended")}` : opt;
|
|
883
1033
|
if (isCursor) {
|
|
884
|
-
output +=
|
|
1034
|
+
output += pc5.cyan(`\u276F ${number} ${text}
|
|
885
1035
|
`);
|
|
886
1036
|
} else {
|
|
887
1037
|
output += ` ${number} ${text}
|
|
@@ -890,9 +1040,9 @@ var selectOrInput = createPrompt((config, done) => {
|
|
|
890
1040
|
});
|
|
891
1041
|
const isCustomCursor = cursor === options.length;
|
|
892
1042
|
if (isCustomCursor) {
|
|
893
|
-
output +=
|
|
1043
|
+
output += pc5.cyan(`\u276F ${pc5.yellow("\u270E")} ${inputValue || pc5.dim("Type your own...")}`);
|
|
894
1044
|
} else {
|
|
895
|
-
output += ` ${
|
|
1045
|
+
output += ` ${pc5.yellow("\u270E")} ${pc5.dim("Type your own...")}`;
|
|
896
1046
|
}
|
|
897
1047
|
return output;
|
|
898
1048
|
});
|
|
@@ -905,53 +1055,66 @@ function registerGenerateCommand(skillCommand) {
|
|
|
905
1055
|
});
|
|
906
1056
|
}
|
|
907
1057
|
async function generateCommand(options) {
|
|
1058
|
+
trackEvent("command", { name: "generate" });
|
|
908
1059
|
log.blank();
|
|
1060
|
+
let accessToken = null;
|
|
909
1061
|
const tokens = loadTokens();
|
|
910
|
-
if (!tokens) {
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
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();
|
|
917
1073
|
}
|
|
918
|
-
const
|
|
919
|
-
const initSpinner = ora().start();
|
|
1074
|
+
const initSpinner = ora2().start();
|
|
920
1075
|
const quota = await getSkillQuota(accessToken);
|
|
921
1076
|
if (quota.error) {
|
|
922
|
-
initSpinner.fail(
|
|
1077
|
+
initSpinner.fail(pc6.red("Failed to initialize"));
|
|
923
1078
|
return;
|
|
924
1079
|
}
|
|
925
1080
|
if (quota.tier !== "unlimited" && quota.remaining < 1) {
|
|
926
|
-
initSpinner.fail(
|
|
1081
|
+
initSpinner.fail(pc6.red("Weekly skill generation limit reached"));
|
|
927
1082
|
log.blank();
|
|
928
1083
|
console.log(
|
|
929
|
-
` 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.`
|
|
930
1085
|
);
|
|
931
1086
|
console.log(
|
|
932
|
-
` Your quota resets on ${
|
|
1087
|
+
` Your quota resets on ${pc6.yellow(new Date(quota.resetDate).toLocaleDateString())}.`
|
|
933
1088
|
);
|
|
934
1089
|
log.blank();
|
|
935
1090
|
if (quota.tier === "free") {
|
|
936
1091
|
console.log(
|
|
937
|
-
` ${
|
|
1092
|
+
` ${pc6.yellow("Tip:")} Upgrade to Pro for ${pc6.bold("10")} generations per week.`
|
|
938
1093
|
);
|
|
939
|
-
console.log(` Visit ${
|
|
1094
|
+
console.log(` Visit ${pc6.green("https://context7.com/dashboard")} to upgrade.`);
|
|
940
1095
|
}
|
|
941
1096
|
return;
|
|
942
1097
|
}
|
|
943
1098
|
initSpinner.stop();
|
|
944
1099
|
initSpinner.clear();
|
|
945
|
-
console.log(
|
|
1100
|
+
console.log(pc6.bold("What should your agent become an expert at?\n"));
|
|
946
1101
|
console.log(
|
|
947
|
-
|
|
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
|
+
)
|
|
948
1105
|
);
|
|
949
|
-
console.log(
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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();
|
|
955
1118
|
let motivation;
|
|
956
1119
|
try {
|
|
957
1120
|
motivation = await input({
|
|
@@ -966,14 +1129,26 @@ async function generateCommand(options) {
|
|
|
966
1129
|
log.warn("Generation cancelled");
|
|
967
1130
|
return;
|
|
968
1131
|
}
|
|
969
|
-
|
|
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();
|
|
970
1145
|
const searchResult = await searchLibraries(motivation, accessToken);
|
|
971
1146
|
if (searchResult.error || !searchResult.results?.length) {
|
|
972
|
-
searchSpinner.fail(
|
|
1147
|
+
searchSpinner.fail(pc6.red("No sources found"));
|
|
973
1148
|
log.warn(searchResult.message || "Try a different description");
|
|
974
1149
|
return;
|
|
975
1150
|
}
|
|
976
|
-
searchSpinner.succeed(
|
|
1151
|
+
searchSpinner.succeed(pc6.green(`Found ${searchResult.results.length} relevant sources`));
|
|
977
1152
|
log.blank();
|
|
978
1153
|
let selectedLibraries;
|
|
979
1154
|
try {
|
|
@@ -993,31 +1168,32 @@ async function generateCommand(options) {
|
|
|
993
1168
|
const libraryChoices = libraries.map((lib, index) => {
|
|
994
1169
|
const projectId = formatProjectId(lib.id);
|
|
995
1170
|
const isGitHub = isGitHubRepo(lib.id);
|
|
996
|
-
const indexStr =
|
|
1171
|
+
const indexStr = pc6.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
|
|
997
1172
|
const paddedName = lib.title.padEnd(maxNameLen);
|
|
998
1173
|
const libUrl = `https://context7.com${lib.id}`;
|
|
999
|
-
const libLink = terminalLink(lib.title, libUrl,
|
|
1000
|
-
const
|
|
1001
|
-
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()}`] : [];
|
|
1002
1178
|
const metadataLines = [
|
|
1003
|
-
|
|
1179
|
+
pc6.dim("\u2500".repeat(50)),
|
|
1004
1180
|
"",
|
|
1005
|
-
`${
|
|
1006
|
-
`${
|
|
1007
|
-
`${
|
|
1181
|
+
`${pc6.yellow("Library:")} ${libLink}`,
|
|
1182
|
+
`${pc6.yellow("Source:")} ${repoLink}`,
|
|
1183
|
+
`${pc6.yellow("Snippets:")} ${lib.totalSnippets.toLocaleString()}`,
|
|
1008
1184
|
...starsLine,
|
|
1009
|
-
`${
|
|
1010
|
-
|
|
1185
|
+
`${pc6.yellow("Description:")}`,
|
|
1186
|
+
pc6.white(lib.description || "No description")
|
|
1011
1187
|
];
|
|
1012
1188
|
return {
|
|
1013
|
-
name: `${indexStr} ${paddedName} ${
|
|
1189
|
+
name: `${indexStr} ${paddedName} ${pc6.dim(`(${projectId})`)}`,
|
|
1014
1190
|
value: lib,
|
|
1015
1191
|
description: metadataLines.join("\n")
|
|
1016
1192
|
};
|
|
1017
1193
|
});
|
|
1018
1194
|
selectedLibraries = await checkboxWithHover(
|
|
1019
1195
|
{
|
|
1020
|
-
message: "Select
|
|
1196
|
+
message: "Select sources:",
|
|
1021
1197
|
choices: libraryChoices,
|
|
1022
1198
|
pageSize: 10,
|
|
1023
1199
|
loop: false
|
|
@@ -1025,7 +1201,7 @@ async function generateCommand(options) {
|
|
|
1025
1201
|
{ getName: (lib) => `${lib.title} (${formatProjectId(lib.id)})` }
|
|
1026
1202
|
);
|
|
1027
1203
|
if (!selectedLibraries || selectedLibraries.length === 0) {
|
|
1028
|
-
log.info("No
|
|
1204
|
+
log.info("No sources selected. Try running the command again.");
|
|
1029
1205
|
return;
|
|
1030
1206
|
}
|
|
1031
1207
|
} catch {
|
|
@@ -1033,15 +1209,17 @@ async function generateCommand(options) {
|
|
|
1033
1209
|
return;
|
|
1034
1210
|
}
|
|
1035
1211
|
log.blank();
|
|
1036
|
-
const questionsSpinner =
|
|
1212
|
+
const questionsSpinner = ora2(
|
|
1213
|
+
"Preparing follow-up questions to clarify scope and constraints..."
|
|
1214
|
+
).start();
|
|
1037
1215
|
const librariesInput = selectedLibraries.map((lib) => ({ id: lib.id, name: lib.title }));
|
|
1038
1216
|
const questionsResult = await getSkillQuestions(librariesInput, motivation, accessToken);
|
|
1039
1217
|
if (questionsResult.error || !questionsResult.questions?.length) {
|
|
1040
|
-
questionsSpinner.fail(
|
|
1218
|
+
questionsSpinner.fail(pc6.red("Failed to generate questions"));
|
|
1041
1219
|
log.warn(questionsResult.message || "Please try again");
|
|
1042
1220
|
return;
|
|
1043
1221
|
}
|
|
1044
|
-
questionsSpinner.succeed(
|
|
1222
|
+
questionsSpinner.succeed(pc6.green("Questions prepared"));
|
|
1045
1223
|
log.blank();
|
|
1046
1224
|
const answers = [];
|
|
1047
1225
|
try {
|
|
@@ -1050,7 +1228,7 @@ async function generateCommand(options) {
|
|
|
1050
1228
|
const questionNum = i + 1;
|
|
1051
1229
|
const totalQuestions = questionsResult.questions.length;
|
|
1052
1230
|
const answer = await selectOrInput_default({
|
|
1053
|
-
message: `${
|
|
1231
|
+
message: `${pc6.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`,
|
|
1054
1232
|
options: q.options,
|
|
1055
1233
|
recommendedIndex: q.recommendedIndex
|
|
1056
1234
|
});
|
|
@@ -1061,8 +1239,8 @@ async function generateCommand(options) {
|
|
|
1061
1239
|
const linesToClear = 3 + q.options.length;
|
|
1062
1240
|
process.stdout.write(`\x1B[${linesToClear}A\x1B[J`);
|
|
1063
1241
|
const truncatedAnswer = answer.length > 50 ? answer.slice(0, 47) + "..." : answer;
|
|
1064
|
-
console.log(`${
|
|
1065
|
-
console.log(` ${
|
|
1242
|
+
console.log(`${pc6.green("\u2713")} ${pc6.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`);
|
|
1243
|
+
console.log(` ${pc6.cyan(truncatedAnswer)}`);
|
|
1066
1244
|
log.blank();
|
|
1067
1245
|
}
|
|
1068
1246
|
} catch {
|
|
@@ -1072,33 +1250,39 @@ async function generateCommand(options) {
|
|
|
1072
1250
|
let generatedContent = null;
|
|
1073
1251
|
let skillName = "";
|
|
1074
1252
|
let feedback;
|
|
1075
|
-
|
|
1253
|
+
let previewFile = null;
|
|
1254
|
+
let previewFileWritten = false;
|
|
1255
|
+
const cleanupPreviewFile = async () => {
|
|
1256
|
+
if (previewFile) {
|
|
1257
|
+
await unlink(previewFile).catch(() => {
|
|
1258
|
+
});
|
|
1259
|
+
}
|
|
1260
|
+
};
|
|
1076
1261
|
const queryLog = [];
|
|
1077
1262
|
let genSpinner = null;
|
|
1078
1263
|
const formatQueryLogText = () => {
|
|
1079
1264
|
if (queryLog.length === 0) return "";
|
|
1080
1265
|
const lines = [];
|
|
1081
1266
|
const latestEntry = queryLog[queryLog.length - 1];
|
|
1082
|
-
lines.push(pc5.dim(`(${queryLog.length} ${queryLog.length === 1 ? "query" : "queries"})`));
|
|
1083
1267
|
lines.push("");
|
|
1084
1268
|
for (const result of latestEntry.results.slice(0, 3)) {
|
|
1085
1269
|
const cleanContent = result.content.replace(/Source:\s*https?:\/\/[^\s]+/gi, "").trim();
|
|
1086
1270
|
if (cleanContent) {
|
|
1087
|
-
lines.push(` ${
|
|
1271
|
+
lines.push(` ${pc6.yellow("\u2022")} ${pc6.white(result.title)}`);
|
|
1088
1272
|
const maxLen = 400;
|
|
1089
1273
|
const content = cleanContent.length > maxLen ? cleanContent.slice(0, maxLen - 3) + "..." : cleanContent;
|
|
1090
1274
|
const words = content.split(" ");
|
|
1091
1275
|
let currentLine = " ";
|
|
1092
1276
|
for (const word of words) {
|
|
1093
1277
|
if (currentLine.length + word.length > 84) {
|
|
1094
|
-
lines.push(
|
|
1278
|
+
lines.push(pc6.dim(currentLine));
|
|
1095
1279
|
currentLine = " " + word + " ";
|
|
1096
1280
|
} else {
|
|
1097
1281
|
currentLine += word + " ";
|
|
1098
1282
|
}
|
|
1099
1283
|
}
|
|
1100
1284
|
if (currentLine.trim()) {
|
|
1101
|
-
lines.push(
|
|
1285
|
+
lines.push(pc6.dim(currentLine));
|
|
1102
1286
|
}
|
|
1103
1287
|
lines.push("");
|
|
1104
1288
|
}
|
|
@@ -1106,19 +1290,20 @@ async function generateCommand(options) {
|
|
|
1106
1290
|
return "\n" + lines.join("\n");
|
|
1107
1291
|
};
|
|
1108
1292
|
let isGeneratingContent = false;
|
|
1293
|
+
let initialStatus = "Reading selected Context7 sources to generate the skill...";
|
|
1109
1294
|
const handleStreamEvent = (event) => {
|
|
1110
1295
|
if (event.type === "progress") {
|
|
1111
1296
|
if (genSpinner) {
|
|
1112
1297
|
if (event.message.startsWith("Generating skill content...") && !isGeneratingContent) {
|
|
1113
1298
|
isGeneratingContent = true;
|
|
1114
1299
|
if (queryLog.length > 0) {
|
|
1115
|
-
genSpinner.succeed(
|
|
1300
|
+
genSpinner.succeed(pc6.green(`Read Context7 sources`));
|
|
1116
1301
|
} else {
|
|
1117
|
-
genSpinner.succeed(
|
|
1302
|
+
genSpinner.succeed(pc6.green(`Ready to generate`));
|
|
1118
1303
|
}
|
|
1119
|
-
genSpinner =
|
|
1304
|
+
genSpinner = ora2("Generating skill content...").start();
|
|
1120
1305
|
} else if (!isGeneratingContent) {
|
|
1121
|
-
genSpinner.text =
|
|
1306
|
+
genSpinner.text = initialStatus + formatQueryLogText();
|
|
1122
1307
|
}
|
|
1123
1308
|
}
|
|
1124
1309
|
} else if (event.type === "tool_result") {
|
|
@@ -1142,18 +1327,19 @@ async function generateCommand(options) {
|
|
|
1142
1327
|
};
|
|
1143
1328
|
queryLog.length = 0;
|
|
1144
1329
|
isGeneratingContent = false;
|
|
1145
|
-
|
|
1146
|
-
|
|
1330
|
+
previewFileWritten = false;
|
|
1331
|
+
initialStatus = feedback ? "Regenerating skill with your feedback..." : "Reading selected Context7 sources to generate the skill...";
|
|
1332
|
+
genSpinner = ora2(initialStatus).start();
|
|
1147
1333
|
const result = await generateSkillStructured(generateInput, handleStreamEvent, accessToken);
|
|
1148
1334
|
if (result.error) {
|
|
1149
|
-
genSpinner.fail(
|
|
1335
|
+
genSpinner.fail(pc6.red(`Error: ${result.error}`));
|
|
1150
1336
|
return;
|
|
1151
1337
|
}
|
|
1152
1338
|
if (!result.content) {
|
|
1153
|
-
genSpinner.fail(
|
|
1339
|
+
genSpinner.fail(pc6.red("No content generated"));
|
|
1154
1340
|
return;
|
|
1155
1341
|
}
|
|
1156
|
-
genSpinner.succeed(
|
|
1342
|
+
genSpinner.succeed(pc6.green(`Generated skill for "${result.libraryName}"`));
|
|
1157
1343
|
generatedContent = result.content;
|
|
1158
1344
|
skillName = result.libraryName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
1159
1345
|
const contentLines = generatedContent.split("\n");
|
|
@@ -1163,29 +1349,40 @@ async function generateCommand(options) {
|
|
|
1163
1349
|
const remainingLines = contentLines.length - previewLineCount;
|
|
1164
1350
|
const showPreview = () => {
|
|
1165
1351
|
log.blank();
|
|
1166
|
-
console.log(
|
|
1167
|
-
console.log(
|
|
1168
|
-
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)));
|
|
1169
1355
|
log.blank();
|
|
1170
1356
|
console.log(previewContent);
|
|
1171
1357
|
if (hasMoreLines) {
|
|
1172
1358
|
log.blank();
|
|
1173
|
-
console.log(
|
|
1359
|
+
console.log(pc6.dim(`... ${remainingLines} more lines`));
|
|
1174
1360
|
}
|
|
1175
1361
|
log.blank();
|
|
1176
|
-
console.log(
|
|
1362
|
+
console.log(pc6.dim("\u2501".repeat(70)));
|
|
1177
1363
|
log.blank();
|
|
1178
1364
|
};
|
|
1179
|
-
const
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
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
|
+
}
|
|
1189
1386
|
};
|
|
1190
1387
|
showPreview();
|
|
1191
1388
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
@@ -1193,27 +1390,30 @@ async function generateCommand(options) {
|
|
|
1193
1390
|
let action;
|
|
1194
1391
|
while (true) {
|
|
1195
1392
|
const choices = [
|
|
1196
|
-
{ name: `${
|
|
1197
|
-
|
|
1198
|
-
{ name: `${
|
|
1199
|
-
{ 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" }
|
|
1200
1397
|
];
|
|
1201
1398
|
action = await select2({
|
|
1202
1399
|
message: "What would you like to do?",
|
|
1203
1400
|
choices
|
|
1204
1401
|
});
|
|
1205
|
-
if (action === "
|
|
1206
|
-
|
|
1402
|
+
if (action === "view") {
|
|
1403
|
+
await openInEditor();
|
|
1207
1404
|
continue;
|
|
1208
1405
|
}
|
|
1406
|
+
await syncFromPreviewFile();
|
|
1209
1407
|
break;
|
|
1210
1408
|
}
|
|
1211
1409
|
if (action === "install") {
|
|
1212
1410
|
break;
|
|
1213
1411
|
} else if (action === "cancel") {
|
|
1412
|
+
await cleanupPreviewFile();
|
|
1214
1413
|
log.warn("Generation cancelled");
|
|
1215
1414
|
return;
|
|
1216
1415
|
} else if (action === "feedback") {
|
|
1416
|
+
trackEvent("gen_feedback");
|
|
1217
1417
|
feedback = await input({
|
|
1218
1418
|
message: "What changes would you like? (press Enter to skip)"
|
|
1219
1419
|
});
|
|
@@ -1223,6 +1423,7 @@ async function generateCommand(options) {
|
|
|
1223
1423
|
log.blank();
|
|
1224
1424
|
}
|
|
1225
1425
|
} catch {
|
|
1426
|
+
await cleanupPreviewFile();
|
|
1226
1427
|
log.warn("Generation cancelled");
|
|
1227
1428
|
return;
|
|
1228
1429
|
}
|
|
@@ -1233,7 +1434,7 @@ async function generateCommand(options) {
|
|
|
1233
1434
|
return;
|
|
1234
1435
|
}
|
|
1235
1436
|
const targetDirs = getTargetDirs(targets);
|
|
1236
|
-
const writeSpinner =
|
|
1437
|
+
const writeSpinner = ora2("Writing skill files...").start();
|
|
1237
1438
|
let permissionError = false;
|
|
1238
1439
|
const failedDirs = /* @__PURE__ */ new Set();
|
|
1239
1440
|
for (const targetDir of targetDirs) {
|
|
@@ -1257,23 +1458,25 @@ async function generateCommand(options) {
|
|
|
1257
1458
|
}
|
|
1258
1459
|
}
|
|
1259
1460
|
if (permissionError) {
|
|
1260
|
-
writeSpinner.fail(
|
|
1461
|
+
writeSpinner.fail(pc6.red("Permission denied"));
|
|
1261
1462
|
log.blank();
|
|
1262
|
-
console.log(
|
|
1463
|
+
console.log(pc6.yellow("Fix permissions with:"));
|
|
1263
1464
|
for (const dir of failedDirs) {
|
|
1264
1465
|
const parentDir = join4(dir, "..");
|
|
1265
|
-
console.log(
|
|
1466
|
+
console.log(pc6.dim(` sudo chown -R $(whoami) "${parentDir}"`));
|
|
1266
1467
|
}
|
|
1267
1468
|
log.blank();
|
|
1268
1469
|
return;
|
|
1269
1470
|
}
|
|
1270
|
-
writeSpinner.succeed(
|
|
1471
|
+
writeSpinner.succeed(pc6.green(`Created skill in ${targetDirs.length} location(s)`));
|
|
1472
|
+
trackEvent("gen_install");
|
|
1271
1473
|
log.blank();
|
|
1272
|
-
console.log(
|
|
1474
|
+
console.log(pc6.green("Skill saved successfully"));
|
|
1273
1475
|
for (const targetDir of targetDirs) {
|
|
1274
|
-
console.log(
|
|
1476
|
+
console.log(pc6.dim(` ${targetDir}/`) + pc6.green(skillName));
|
|
1275
1477
|
}
|
|
1276
1478
|
log.blank();
|
|
1479
|
+
await cleanupPreviewFile();
|
|
1277
1480
|
}
|
|
1278
1481
|
|
|
1279
1482
|
// src/commands/skill.ts
|
|
@@ -1320,6 +1523,7 @@ function registerSkillAliases(program2) {
|
|
|
1320
1523
|
});
|
|
1321
1524
|
}
|
|
1322
1525
|
async function installCommand(input2, skillName, options) {
|
|
1526
|
+
trackEvent("command", { name: "install" });
|
|
1323
1527
|
const parsed = parseSkillInput(input2);
|
|
1324
1528
|
if (!parsed) {
|
|
1325
1529
|
log.error(`Invalid input format: ${input2}`);
|
|
@@ -1330,17 +1534,17 @@ async function installCommand(input2, skillName, options) {
|
|
|
1330
1534
|
}
|
|
1331
1535
|
const repo = `/${parsed.owner}/${parsed.repo}`;
|
|
1332
1536
|
log.blank();
|
|
1333
|
-
const spinner =
|
|
1537
|
+
const spinner = ora3(`Fetching skills from ${repo}...`).start();
|
|
1334
1538
|
let selectedSkills;
|
|
1335
1539
|
if (skillName) {
|
|
1336
1540
|
spinner.text = `Fetching skill: ${skillName}...`;
|
|
1337
1541
|
const skillData = await getSkill(repo, skillName);
|
|
1338
1542
|
if (skillData.error || !skillData.name) {
|
|
1339
1543
|
if (skillData.error === "prompt_injection_detected") {
|
|
1340
|
-
spinner.fail(
|
|
1544
|
+
spinner.fail(pc7.red(`Prompt injection detected in skill: ${skillName}`));
|
|
1341
1545
|
log.warn("This skill contains potentially malicious content and cannot be installed.");
|
|
1342
1546
|
} else {
|
|
1343
|
-
spinner.fail(
|
|
1547
|
+
spinner.fail(pc7.red(`Skill not found: ${skillName}`));
|
|
1344
1548
|
}
|
|
1345
1549
|
return;
|
|
1346
1550
|
}
|
|
@@ -1356,14 +1560,14 @@ async function installCommand(input2, skillName, options) {
|
|
|
1356
1560
|
} else {
|
|
1357
1561
|
const data = await listProjectSkills(repo);
|
|
1358
1562
|
if (data.error) {
|
|
1359
|
-
spinner.fail(
|
|
1563
|
+
spinner.fail(pc7.red(`Error: ${data.message || data.error}`));
|
|
1360
1564
|
return;
|
|
1361
1565
|
}
|
|
1362
1566
|
if (!data.skills || data.skills.length === 0) {
|
|
1363
|
-
spinner.warn(
|
|
1567
|
+
spinner.warn(pc7.yellow(`No skills found in ${repo}`));
|
|
1364
1568
|
return;
|
|
1365
1569
|
}
|
|
1366
|
-
const skillsWithRepo = data.skills.map((s) => ({ ...s, project: repo }));
|
|
1570
|
+
const skillsWithRepo = data.skills.map((s) => ({ ...s, project: repo })).sort((a, b) => (b.installCount ?? 0) - (a.installCount ?? 0));
|
|
1367
1571
|
spinner.succeed(`Found ${data.skills.length} skill(s)`);
|
|
1368
1572
|
if (data.blockedSkillsCount && data.blockedSkillsCount > 0) {
|
|
1369
1573
|
log.blank();
|
|
@@ -1378,19 +1582,19 @@ async function installCommand(input2, skillName, options) {
|
|
|
1378
1582
|
const indexWidth = data.skills.length.toString().length;
|
|
1379
1583
|
const maxNameLen = Math.max(...data.skills.map((s) => s.name.length));
|
|
1380
1584
|
const choices = skillsWithRepo.map((s, index) => {
|
|
1381
|
-
const indexStr =
|
|
1585
|
+
const indexStr = pc7.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
|
|
1382
1586
|
const paddedName = s.name.padEnd(maxNameLen);
|
|
1383
1587
|
const installs = formatInstallCount(s.installCount);
|
|
1384
1588
|
const skillUrl = `https://context7.com/skills${s.project}/${s.name}`;
|
|
1385
|
-
const skillLink = terminalLink(s.name, skillUrl,
|
|
1386
|
-
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);
|
|
1387
1591
|
const metadataLines = [
|
|
1388
|
-
|
|
1592
|
+
pc7.dim("\u2500".repeat(50)),
|
|
1389
1593
|
"",
|
|
1390
|
-
`${
|
|
1391
|
-
`${
|
|
1392
|
-
`${
|
|
1393
|
-
|
|
1594
|
+
`${pc7.yellow("Skill:")} ${skillLink}`,
|
|
1595
|
+
`${pc7.yellow("Repo:")} ${repoLink}`,
|
|
1596
|
+
`${pc7.yellow("Description:")}`,
|
|
1597
|
+
pc7.white(s.description || "No description")
|
|
1394
1598
|
];
|
|
1395
1599
|
return {
|
|
1396
1600
|
name: installs ? `${indexStr} ${paddedName} ${installs}` : `${indexStr} ${paddedName}`,
|
|
@@ -1399,9 +1603,11 @@ async function installCommand(input2, skillName, options) {
|
|
|
1399
1603
|
};
|
|
1400
1604
|
});
|
|
1401
1605
|
log.blank();
|
|
1606
|
+
const installsOffset = 4 + indexWidth + 1 + 1 + maxNameLen + 1 - 3;
|
|
1607
|
+
const message = "Select skills:" + " ".repeat(Math.max(1, installsOffset - 14)) + pc7.dim("installs");
|
|
1402
1608
|
try {
|
|
1403
1609
|
selectedSkills = await checkboxWithHover({
|
|
1404
|
-
message
|
|
1610
|
+
message,
|
|
1405
1611
|
choices,
|
|
1406
1612
|
pageSize: 15,
|
|
1407
1613
|
loop: false
|
|
@@ -1422,7 +1628,7 @@ async function installCommand(input2, skillName, options) {
|
|
|
1422
1628
|
return;
|
|
1423
1629
|
}
|
|
1424
1630
|
const targetDirs = getTargetDirs(targets);
|
|
1425
|
-
const installSpinner =
|
|
1631
|
+
const installSpinner = ora3("Installing skills...").start();
|
|
1426
1632
|
let permissionError = false;
|
|
1427
1633
|
const failedDirs = /* @__PURE__ */ new Set();
|
|
1428
1634
|
const installedSkills = [];
|
|
@@ -1481,60 +1687,69 @@ async function installCommand(input2, skillName, options) {
|
|
|
1481
1687
|
return;
|
|
1482
1688
|
}
|
|
1483
1689
|
installSpinner.succeed(`Installed ${installedSkills.length} skill(s)`);
|
|
1484
|
-
|
|
1690
|
+
trackEvent("install", { skills: installedSkills, ides: targets.ides });
|
|
1485
1691
|
const installedNames = selectedSkills.map((s) => s.name);
|
|
1486
1692
|
logInstallSummary(targets, targetDirs, installedNames);
|
|
1487
1693
|
}
|
|
1488
1694
|
async function searchCommand(query) {
|
|
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)`);
|
|
1714
|
+
trackEvent("search_query", { query, resultCount: data.results.length });
|
|
1507
1715
|
const indexWidth = data.results.length.toString().length;
|
|
1508
1716
|
const maxNameLen = Math.max(...data.results.map((s) => s.name.length));
|
|
1717
|
+
const installsColWidth = 10;
|
|
1509
1718
|
const choices = data.results.map((s, index) => {
|
|
1510
|
-
const indexStr =
|
|
1719
|
+
const indexStr = pc7.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
|
|
1511
1720
|
const paddedName = s.name.padEnd(maxNameLen);
|
|
1512
|
-
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);
|
|
1513
1724
|
const skillLink = terminalLink(
|
|
1514
1725
|
s.name,
|
|
1515
1726
|
`https://context7.com/skills${s.project}/${s.name}`,
|
|
1516
|
-
|
|
1727
|
+
pc7.white
|
|
1517
1728
|
);
|
|
1518
|
-
const repoLink = terminalLink(s.project, `https://github.com${s.project}`,
|
|
1729
|
+
const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc7.white);
|
|
1519
1730
|
const metadataLines = [
|
|
1520
|
-
|
|
1731
|
+
pc7.dim("\u2500".repeat(50)),
|
|
1521
1732
|
"",
|
|
1522
|
-
`${
|
|
1523
|
-
`${
|
|
1524
|
-
`${
|
|
1525
|
-
|
|
1733
|
+
`${pc7.yellow("Skill:")} ${skillLink}`,
|
|
1734
|
+
`${pc7.yellow("Repo:")} ${repoLink}`,
|
|
1735
|
+
`${pc7.yellow("Description:")}`,
|
|
1736
|
+
pc7.white(s.description || "No description")
|
|
1526
1737
|
];
|
|
1527
1738
|
return {
|
|
1528
|
-
name:
|
|
1739
|
+
name: `${indexStr} ${paddedName} ${paddedInstalls}${trust}`,
|
|
1529
1740
|
value: s,
|
|
1530
1741
|
description: metadataLines.join("\n")
|
|
1531
1742
|
};
|
|
1532
1743
|
});
|
|
1533
1744
|
log.blank();
|
|
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;
|
|
1534
1749
|
let selectedSkills;
|
|
1535
1750
|
try {
|
|
1536
1751
|
selectedSkills = await checkboxWithHover({
|
|
1537
|
-
message
|
|
1752
|
+
message,
|
|
1538
1753
|
choices,
|
|
1539
1754
|
pageSize: 15,
|
|
1540
1755
|
loop: false
|
|
@@ -1554,7 +1769,7 @@ async function searchCommand(query) {
|
|
|
1554
1769
|
return;
|
|
1555
1770
|
}
|
|
1556
1771
|
const targetDirs = getTargetDirs(targets);
|
|
1557
|
-
const installSpinner =
|
|
1772
|
+
const installSpinner = ora3("Installing skills...").start();
|
|
1558
1773
|
let permissionError = false;
|
|
1559
1774
|
const failedDirs = /* @__PURE__ */ new Set();
|
|
1560
1775
|
const installedSkills = [];
|
|
@@ -1613,11 +1828,12 @@ async function searchCommand(query) {
|
|
|
1613
1828
|
return;
|
|
1614
1829
|
}
|
|
1615
1830
|
installSpinner.succeed(`Installed ${installedSkills.length} skill(s)`);
|
|
1616
|
-
|
|
1831
|
+
trackEvent("install", { skills: installedSkills, ides: targets.ides });
|
|
1617
1832
|
const installedNames = uniqueSkills.map((s) => s.name);
|
|
1618
1833
|
logInstallSummary(targets, targetDirs, installedNames);
|
|
1619
1834
|
}
|
|
1620
1835
|
async function listCommand(options) {
|
|
1836
|
+
trackEvent("command", { name: "list" });
|
|
1621
1837
|
const scope = options.global ? "global" : "project";
|
|
1622
1838
|
const pathMap = scope === "global" ? IDE_GLOBAL_PATHS : IDE_PATHS;
|
|
1623
1839
|
const baseDir = scope === "global" ? homedir4() : process.cwd();
|
|
@@ -1642,14 +1858,15 @@ async function listCommand(options) {
|
|
|
1642
1858
|
for (const { ide, skills } of results) {
|
|
1643
1859
|
const ideName = IDE_NAMES[ide];
|
|
1644
1860
|
const path2 = pathMap[ide];
|
|
1645
|
-
log.plain(`${
|
|
1861
|
+
log.plain(`${pc7.bold(ideName)} ${pc7.dim(path2)}`);
|
|
1646
1862
|
for (const skill of skills) {
|
|
1647
|
-
log.plain(` ${
|
|
1863
|
+
log.plain(` ${pc7.green(skill)}`);
|
|
1648
1864
|
}
|
|
1649
1865
|
log.blank();
|
|
1650
1866
|
}
|
|
1651
1867
|
}
|
|
1652
1868
|
async function removeCommand(name, options) {
|
|
1869
|
+
trackEvent("command", { name: "remove" });
|
|
1653
1870
|
const target = await promptForSingleTarget(options);
|
|
1654
1871
|
if (!target) {
|
|
1655
1872
|
log.warn("Cancelled");
|
|
@@ -1672,6 +1889,7 @@ async function removeCommand(name, options) {
|
|
|
1672
1889
|
}
|
|
1673
1890
|
}
|
|
1674
1891
|
async function infoCommand(input2) {
|
|
1892
|
+
trackEvent("command", { name: "info" });
|
|
1675
1893
|
const parsed = parseSkillInput(input2);
|
|
1676
1894
|
if (!parsed) {
|
|
1677
1895
|
log.blank();
|
|
@@ -1682,14 +1900,14 @@ async function infoCommand(input2) {
|
|
|
1682
1900
|
}
|
|
1683
1901
|
const repo = `/${parsed.owner}/${parsed.repo}`;
|
|
1684
1902
|
log.blank();
|
|
1685
|
-
const spinner =
|
|
1903
|
+
const spinner = ora3(`Fetching skills from ${repo}...`).start();
|
|
1686
1904
|
const data = await listProjectSkills(repo);
|
|
1687
1905
|
if (data.error) {
|
|
1688
|
-
spinner.fail(
|
|
1906
|
+
spinner.fail(pc7.red(`Error: ${data.message || data.error}`));
|
|
1689
1907
|
return;
|
|
1690
1908
|
}
|
|
1691
1909
|
if (!data.skills || data.skills.length === 0) {
|
|
1692
|
-
spinner.warn(
|
|
1910
|
+
spinner.warn(pc7.yellow(`No skills found in ${repo}`));
|
|
1693
1911
|
return;
|
|
1694
1912
|
}
|
|
1695
1913
|
spinner.succeed(`Found ${data.skills.length} skill(s)`);
|
|
@@ -1701,145 +1919,13 @@ async function infoCommand(input2) {
|
|
|
1701
1919
|
log.blank();
|
|
1702
1920
|
}
|
|
1703
1921
|
log.plain(
|
|
1704
|
-
`${
|
|
1705
|
-
Install all: ${
|
|
1706
|
-
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}`)}
|
|
1707
1925
|
`
|
|
1708
1926
|
);
|
|
1709
1927
|
}
|
|
1710
1928
|
|
|
1711
|
-
// src/commands/auth.ts
|
|
1712
|
-
import pc7 from "picocolors";
|
|
1713
|
-
import ora3 from "ora";
|
|
1714
|
-
import open from "open";
|
|
1715
|
-
var CLI_CLIENT_ID = "2veBSofhicRBguUT";
|
|
1716
|
-
var baseUrl2 = "https://context7.com";
|
|
1717
|
-
function setAuthBaseUrl(url) {
|
|
1718
|
-
baseUrl2 = url;
|
|
1719
|
-
}
|
|
1720
|
-
function registerAuthCommands(program2) {
|
|
1721
|
-
program2.command("login").description("Log in to Context7").option("--no-browser", "Don't open browser automatically").action(async (options) => {
|
|
1722
|
-
await loginCommand(options);
|
|
1723
|
-
});
|
|
1724
|
-
program2.command("logout").description("Log out of Context7").action(() => {
|
|
1725
|
-
logoutCommand();
|
|
1726
|
-
});
|
|
1727
|
-
program2.command("whoami").description("Show current login status").action(async () => {
|
|
1728
|
-
await whoamiCommand();
|
|
1729
|
-
});
|
|
1730
|
-
}
|
|
1731
|
-
async function loginCommand(options) {
|
|
1732
|
-
const existingTokens = loadTokens();
|
|
1733
|
-
if (existingTokens) {
|
|
1734
|
-
const expired = isTokenExpired(existingTokens);
|
|
1735
|
-
if (!expired || existingTokens.refresh_token) {
|
|
1736
|
-
console.log(pc7.yellow("You are already logged in."));
|
|
1737
|
-
console.log(
|
|
1738
|
-
pc7.dim("Run 'ctx7 logout' first if you want to log in with a different account.")
|
|
1739
|
-
);
|
|
1740
|
-
return;
|
|
1741
|
-
}
|
|
1742
|
-
clearTokens();
|
|
1743
|
-
}
|
|
1744
|
-
const spinner = ora3("Preparing login...").start();
|
|
1745
|
-
try {
|
|
1746
|
-
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
1747
|
-
const state = generateState();
|
|
1748
|
-
const callbackServer = createCallbackServer(state);
|
|
1749
|
-
const port = await callbackServer.port;
|
|
1750
|
-
const redirectUri = `http://localhost:${port}/callback`;
|
|
1751
|
-
const authUrl = buildAuthorizationUrl(
|
|
1752
|
-
baseUrl2,
|
|
1753
|
-
CLI_CLIENT_ID,
|
|
1754
|
-
redirectUri,
|
|
1755
|
-
codeChallenge,
|
|
1756
|
-
state
|
|
1757
|
-
);
|
|
1758
|
-
spinner.stop();
|
|
1759
|
-
console.log("");
|
|
1760
|
-
console.log(pc7.bold("Opening browser to log in..."));
|
|
1761
|
-
console.log("");
|
|
1762
|
-
if (options.browser) {
|
|
1763
|
-
await open(authUrl);
|
|
1764
|
-
console.log(pc7.dim("If the browser didn't open, visit this URL:"));
|
|
1765
|
-
} else {
|
|
1766
|
-
console.log(pc7.dim("Open this URL in your browser:"));
|
|
1767
|
-
}
|
|
1768
|
-
console.log(pc7.cyan(authUrl));
|
|
1769
|
-
console.log("");
|
|
1770
|
-
const waitingSpinner = ora3("Waiting for login...").start();
|
|
1771
|
-
try {
|
|
1772
|
-
const { code } = await callbackServer.result;
|
|
1773
|
-
waitingSpinner.text = "Exchanging code for tokens...";
|
|
1774
|
-
const tokens = await exchangeCodeForTokens(
|
|
1775
|
-
baseUrl2,
|
|
1776
|
-
code,
|
|
1777
|
-
codeVerifier,
|
|
1778
|
-
redirectUri,
|
|
1779
|
-
CLI_CLIENT_ID
|
|
1780
|
-
);
|
|
1781
|
-
saveTokens(tokens);
|
|
1782
|
-
callbackServer.close();
|
|
1783
|
-
waitingSpinner.succeed(pc7.green("Login successful!"));
|
|
1784
|
-
console.log("");
|
|
1785
|
-
console.log(pc7.dim("You can now use authenticated Context7 features."));
|
|
1786
|
-
} catch (error) {
|
|
1787
|
-
callbackServer.close();
|
|
1788
|
-
waitingSpinner.fail(pc7.red("Login failed"));
|
|
1789
|
-
if (error instanceof Error) {
|
|
1790
|
-
console.error(pc7.red(error.message));
|
|
1791
|
-
}
|
|
1792
|
-
process.exit(1);
|
|
1793
|
-
}
|
|
1794
|
-
} catch (error) {
|
|
1795
|
-
spinner.fail(pc7.red("Login failed"));
|
|
1796
|
-
if (error instanceof Error) {
|
|
1797
|
-
console.error(pc7.red(error.message));
|
|
1798
|
-
}
|
|
1799
|
-
process.exit(1);
|
|
1800
|
-
}
|
|
1801
|
-
}
|
|
1802
|
-
function logoutCommand() {
|
|
1803
|
-
if (clearTokens()) {
|
|
1804
|
-
console.log(pc7.green("Logged out successfully."));
|
|
1805
|
-
} else {
|
|
1806
|
-
console.log(pc7.yellow("You are not logged in."));
|
|
1807
|
-
}
|
|
1808
|
-
}
|
|
1809
|
-
async function whoamiCommand() {
|
|
1810
|
-
const tokens = loadTokens();
|
|
1811
|
-
if (!tokens) {
|
|
1812
|
-
console.log(pc7.yellow("Not logged in."));
|
|
1813
|
-
console.log(pc7.dim("Run 'ctx7 login' to authenticate."));
|
|
1814
|
-
return;
|
|
1815
|
-
}
|
|
1816
|
-
console.log(pc7.green("Logged in"));
|
|
1817
|
-
try {
|
|
1818
|
-
const userInfo = await fetchUserInfo(tokens.access_token);
|
|
1819
|
-
if (userInfo.name) {
|
|
1820
|
-
console.log(`${pc7.dim("Name:".padEnd(9))}${userInfo.name}`);
|
|
1821
|
-
}
|
|
1822
|
-
if (userInfo.email) {
|
|
1823
|
-
console.log(`${pc7.dim("Email:".padEnd(9))}${userInfo.email}`);
|
|
1824
|
-
}
|
|
1825
|
-
} catch {
|
|
1826
|
-
if (isTokenExpired(tokens) && !tokens.refresh_token) {
|
|
1827
|
-
console.log(pc7.dim("(Session may be expired - run 'ctx7 login' to refresh)"));
|
|
1828
|
-
}
|
|
1829
|
-
}
|
|
1830
|
-
}
|
|
1831
|
-
async function fetchUserInfo(accessToken) {
|
|
1832
|
-
const response = await fetch("https://clerk.context7.com/oauth/userinfo", {
|
|
1833
|
-
headers: {
|
|
1834
|
-
Authorization: `Bearer ${accessToken}`
|
|
1835
|
-
}
|
|
1836
|
-
});
|
|
1837
|
-
if (!response.ok) {
|
|
1838
|
-
throw new Error("Failed to fetch user info");
|
|
1839
|
-
}
|
|
1840
|
-
return await response.json();
|
|
1841
|
-
}
|
|
1842
|
-
|
|
1843
1929
|
// src/constants.ts
|
|
1844
1930
|
import { readFileSync as readFileSync2 } from "fs";
|
|
1845
1931
|
import { fileURLToPath } from "url";
|