ctx7 0.2.1 → 0.2.3
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 +615 -277
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6,10 +6,10 @@ 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
|
-
import { join as
|
|
12
|
+
import { join as join6 } from "path";
|
|
13
13
|
|
|
14
14
|
// src/utils/parse-input.ts
|
|
15
15
|
function parseSkillInput(input2) {
|
|
@@ -138,6 +138,18 @@ async function searchSkills(query) {
|
|
|
138
138
|
const response = await fetch(`${baseUrl}/api/v2/skills?${params}`);
|
|
139
139
|
return await response.json();
|
|
140
140
|
}
|
|
141
|
+
async function suggestSkills(dependencies, accessToken) {
|
|
142
|
+
const headers = { "Content-Type": "application/json" };
|
|
143
|
+
if (accessToken) {
|
|
144
|
+
headers["Authorization"] = `Bearer ${accessToken}`;
|
|
145
|
+
}
|
|
146
|
+
const response = await fetch(`${baseUrl}/api/v2/skills/suggest`, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers,
|
|
149
|
+
body: JSON.stringify({ dependencies })
|
|
150
|
+
});
|
|
151
|
+
return await response.json();
|
|
152
|
+
}
|
|
141
153
|
async function downloadSkill(project, skillName) {
|
|
142
154
|
const skillData = await getSkill(project, skillName);
|
|
143
155
|
if (skillData.error) {
|
|
@@ -521,12 +533,17 @@ import { checkbox as checkbox2 } from "@inquirer/prompts";
|
|
|
521
533
|
import readline from "readline";
|
|
522
534
|
function terminalLink(text, url, color) {
|
|
523
535
|
const colorFn = color ?? ((s) => s);
|
|
524
|
-
return `\x1B]8;;${url}\x07${colorFn(text)}\x1B]8;;\x07`;
|
|
536
|
+
return `\x1B]8;;${url}\x07${colorFn(text)}\x1B]8;;\x07 ${pc3.white("\u2197")}`;
|
|
525
537
|
}
|
|
526
|
-
function formatInstallCount(count) {
|
|
527
|
-
if (count === void 0 || count === 0) return
|
|
538
|
+
function formatInstallCount(count, placeholder = "") {
|
|
539
|
+
if (count === void 0 || count === 0) return placeholder;
|
|
528
540
|
return pc3.yellow(String(count));
|
|
529
541
|
}
|
|
542
|
+
function formatTrustScore(score) {
|
|
543
|
+
if (score === void 0 || score < 0) return pc3.dim("-");
|
|
544
|
+
if (score < 3) return pc3.red(score.toFixed(1));
|
|
545
|
+
return pc3.yellow(score.toFixed(1));
|
|
546
|
+
}
|
|
530
547
|
async function checkboxWithHover(config, options) {
|
|
531
548
|
const choices = config.choices.filter(
|
|
532
549
|
(c) => typeof c === "object" && c !== null && !("type" in c && c.type === "separator")
|
|
@@ -608,11 +625,12 @@ function trackEvent(event, data) {
|
|
|
608
625
|
}
|
|
609
626
|
|
|
610
627
|
// src/commands/generate.ts
|
|
611
|
-
import
|
|
612
|
-
import
|
|
613
|
-
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
628
|
+
import pc6 from "picocolors";
|
|
629
|
+
import ora2 from "ora";
|
|
630
|
+
import { mkdir as mkdir2, writeFile as writeFile2, readFile, unlink } from "fs/promises";
|
|
614
631
|
import { join as join4 } from "path";
|
|
615
632
|
import { homedir as homedir3 } from "os";
|
|
633
|
+
import { spawn } from "child_process";
|
|
616
634
|
import { input, select as select2 } from "@inquirer/prompts";
|
|
617
635
|
|
|
618
636
|
// src/utils/auth.ts
|
|
@@ -807,6 +825,148 @@ function buildAuthorizationUrl(baseUrl3, clientId, redirectUri, codeChallenge, s
|
|
|
807
825
|
return url.toString();
|
|
808
826
|
}
|
|
809
827
|
|
|
828
|
+
// src/commands/auth.ts
|
|
829
|
+
import pc4 from "picocolors";
|
|
830
|
+
import ora from "ora";
|
|
831
|
+
import open from "open";
|
|
832
|
+
var CLI_CLIENT_ID = "2veBSofhicRBguUT";
|
|
833
|
+
var baseUrl2 = "https://context7.com";
|
|
834
|
+
function setAuthBaseUrl(url) {
|
|
835
|
+
baseUrl2 = url;
|
|
836
|
+
}
|
|
837
|
+
function registerAuthCommands(program2) {
|
|
838
|
+
program2.command("login").description("Log in to Context7").option("--no-browser", "Don't open browser automatically").action(async (options) => {
|
|
839
|
+
await loginCommand(options);
|
|
840
|
+
});
|
|
841
|
+
program2.command("logout").description("Log out of Context7").action(() => {
|
|
842
|
+
logoutCommand();
|
|
843
|
+
});
|
|
844
|
+
program2.command("whoami").description("Show current login status").action(async () => {
|
|
845
|
+
await whoamiCommand();
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
async function performLogin(openBrowser = true) {
|
|
849
|
+
const spinner = ora("Preparing login...").start();
|
|
850
|
+
try {
|
|
851
|
+
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
852
|
+
const state = generateState();
|
|
853
|
+
const callbackServer = createCallbackServer(state);
|
|
854
|
+
const port = await callbackServer.port;
|
|
855
|
+
const redirectUri = `http://localhost:${port}/callback`;
|
|
856
|
+
const authUrl = buildAuthorizationUrl(
|
|
857
|
+
baseUrl2,
|
|
858
|
+
CLI_CLIENT_ID,
|
|
859
|
+
redirectUri,
|
|
860
|
+
codeChallenge,
|
|
861
|
+
state
|
|
862
|
+
);
|
|
863
|
+
spinner.stop();
|
|
864
|
+
console.log("");
|
|
865
|
+
console.log(pc4.bold("Opening browser to log in..."));
|
|
866
|
+
console.log("");
|
|
867
|
+
if (openBrowser) {
|
|
868
|
+
await open(authUrl);
|
|
869
|
+
console.log(pc4.dim("If the browser didn't open, visit this URL:"));
|
|
870
|
+
} else {
|
|
871
|
+
console.log(pc4.dim("Open this URL in your browser:"));
|
|
872
|
+
}
|
|
873
|
+
console.log(pc4.cyan(authUrl));
|
|
874
|
+
console.log("");
|
|
875
|
+
const waitingSpinner = ora("Waiting for login...").start();
|
|
876
|
+
try {
|
|
877
|
+
const { code } = await callbackServer.result;
|
|
878
|
+
waitingSpinner.text = "Exchanging code for tokens...";
|
|
879
|
+
const tokens = await exchangeCodeForTokens(
|
|
880
|
+
baseUrl2,
|
|
881
|
+
code,
|
|
882
|
+
codeVerifier,
|
|
883
|
+
redirectUri,
|
|
884
|
+
CLI_CLIENT_ID
|
|
885
|
+
);
|
|
886
|
+
saveTokens(tokens);
|
|
887
|
+
callbackServer.close();
|
|
888
|
+
waitingSpinner.succeed(pc4.green("Login successful!"));
|
|
889
|
+
return tokens.access_token;
|
|
890
|
+
} catch (error) {
|
|
891
|
+
callbackServer.close();
|
|
892
|
+
waitingSpinner.fail(pc4.red("Login failed"));
|
|
893
|
+
if (error instanceof Error) {
|
|
894
|
+
console.error(pc4.red(error.message));
|
|
895
|
+
}
|
|
896
|
+
return null;
|
|
897
|
+
}
|
|
898
|
+
} catch (error) {
|
|
899
|
+
spinner.fail(pc4.red("Login failed"));
|
|
900
|
+
if (error instanceof Error) {
|
|
901
|
+
console.error(pc4.red(error.message));
|
|
902
|
+
}
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
async function loginCommand(options) {
|
|
907
|
+
trackEvent("command", { name: "login" });
|
|
908
|
+
const existingTokens = loadTokens();
|
|
909
|
+
if (existingTokens) {
|
|
910
|
+
const expired = isTokenExpired(existingTokens);
|
|
911
|
+
if (!expired || existingTokens.refresh_token) {
|
|
912
|
+
console.log(pc4.yellow("You are already logged in."));
|
|
913
|
+
console.log(
|
|
914
|
+
pc4.dim("Run 'ctx7 logout' first if you want to log in with a different account.")
|
|
915
|
+
);
|
|
916
|
+
return;
|
|
917
|
+
}
|
|
918
|
+
clearTokens();
|
|
919
|
+
}
|
|
920
|
+
const token = await performLogin(options.browser);
|
|
921
|
+
if (!token) {
|
|
922
|
+
process.exit(1);
|
|
923
|
+
}
|
|
924
|
+
console.log("");
|
|
925
|
+
console.log(pc4.dim("You can now use authenticated Context7 features."));
|
|
926
|
+
}
|
|
927
|
+
function logoutCommand() {
|
|
928
|
+
trackEvent("command", { name: "logout" });
|
|
929
|
+
if (clearTokens()) {
|
|
930
|
+
console.log(pc4.green("Logged out successfully."));
|
|
931
|
+
} else {
|
|
932
|
+
console.log(pc4.yellow("You are not logged in."));
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
async function whoamiCommand() {
|
|
936
|
+
trackEvent("command", { name: "whoami" });
|
|
937
|
+
const tokens = loadTokens();
|
|
938
|
+
if (!tokens) {
|
|
939
|
+
console.log(pc4.yellow("Not logged in."));
|
|
940
|
+
console.log(pc4.dim("Run 'ctx7 login' to authenticate."));
|
|
941
|
+
return;
|
|
942
|
+
}
|
|
943
|
+
console.log(pc4.green("Logged in"));
|
|
944
|
+
try {
|
|
945
|
+
const userInfo = await fetchUserInfo(tokens.access_token);
|
|
946
|
+
if (userInfo.name) {
|
|
947
|
+
console.log(`${pc4.dim("Name:".padEnd(9))}${userInfo.name}`);
|
|
948
|
+
}
|
|
949
|
+
if (userInfo.email) {
|
|
950
|
+
console.log(`${pc4.dim("Email:".padEnd(9))}${userInfo.email}`);
|
|
951
|
+
}
|
|
952
|
+
} catch {
|
|
953
|
+
if (isTokenExpired(tokens) && !tokens.refresh_token) {
|
|
954
|
+
console.log(pc4.dim("(Session may be expired - run 'ctx7 login' to refresh)"));
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
async function fetchUserInfo(accessToken) {
|
|
959
|
+
const response = await fetch("https://clerk.context7.com/oauth/userinfo", {
|
|
960
|
+
headers: {
|
|
961
|
+
Authorization: `Bearer ${accessToken}`
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
if (!response.ok) {
|
|
965
|
+
throw new Error("Failed to fetch user info");
|
|
966
|
+
}
|
|
967
|
+
return await response.json();
|
|
968
|
+
}
|
|
969
|
+
|
|
810
970
|
// src/utils/selectOrInput.ts
|
|
811
971
|
import {
|
|
812
972
|
createPrompt,
|
|
@@ -817,10 +977,19 @@ import {
|
|
|
817
977
|
isUpKey,
|
|
818
978
|
isDownKey
|
|
819
979
|
} from "@inquirer/core";
|
|
820
|
-
import
|
|
980
|
+
import pc5 from "picocolors";
|
|
981
|
+
function reorderOptions(options, recommendedIndex) {
|
|
982
|
+
if (recommendedIndex === 0) return options;
|
|
983
|
+
const reordered = [options[recommendedIndex]];
|
|
984
|
+
for (let i = 0; i < options.length; i++) {
|
|
985
|
+
if (i !== recommendedIndex) reordered.push(options[i]);
|
|
986
|
+
}
|
|
987
|
+
return reordered;
|
|
988
|
+
}
|
|
821
989
|
var selectOrInput = createPrompt((config, done) => {
|
|
822
|
-
const { message, options, recommendedIndex = 0 } = config;
|
|
823
|
-
const
|
|
990
|
+
const { message, options: rawOptions, recommendedIndex = 0 } = config;
|
|
991
|
+
const options = reorderOptions(rawOptions, recommendedIndex);
|
|
992
|
+
const [cursor, setCursor] = useState(0);
|
|
824
993
|
const [inputValue, setInputValue] = useState("");
|
|
825
994
|
const prefix = usePrefix({});
|
|
826
995
|
useKeypress((key, rl) => {
|
|
@@ -835,7 +1004,7 @@ var selectOrInput = createPrompt((config, done) => {
|
|
|
835
1004
|
if (isEnterKey(key)) {
|
|
836
1005
|
if (cursor === options.length) {
|
|
837
1006
|
const finalValue = inputValue.trim();
|
|
838
|
-
done(finalValue || options[
|
|
1007
|
+
done(finalValue || options[0]);
|
|
839
1008
|
} else {
|
|
840
1009
|
done(options[cursor]);
|
|
841
1010
|
}
|
|
@@ -865,16 +1034,16 @@ var selectOrInput = createPrompt((config, done) => {
|
|
|
865
1034
|
rl.line = "";
|
|
866
1035
|
}
|
|
867
1036
|
});
|
|
868
|
-
let output = `${prefix} ${
|
|
1037
|
+
let output = `${prefix} ${pc5.bold(message)}
|
|
869
1038
|
|
|
870
1039
|
`;
|
|
871
1040
|
options.forEach((opt, idx) => {
|
|
872
|
-
const isRecommended = idx ===
|
|
1041
|
+
const isRecommended = idx === 0;
|
|
873
1042
|
const isCursor = idx === cursor;
|
|
874
|
-
const number =
|
|
875
|
-
const text = isRecommended ? `${opt} ${
|
|
1043
|
+
const number = pc5.cyan(`${idx + 1}.`);
|
|
1044
|
+
const text = isRecommended ? `${opt} ${pc5.green("\u2713 Recommended")}` : opt;
|
|
876
1045
|
if (isCursor) {
|
|
877
|
-
output +=
|
|
1046
|
+
output += pc5.cyan(`\u276F ${number} ${text}
|
|
878
1047
|
`);
|
|
879
1048
|
} else {
|
|
880
1049
|
output += ` ${number} ${text}
|
|
@@ -883,9 +1052,9 @@ var selectOrInput = createPrompt((config, done) => {
|
|
|
883
1052
|
});
|
|
884
1053
|
const isCustomCursor = cursor === options.length;
|
|
885
1054
|
if (isCustomCursor) {
|
|
886
|
-
output +=
|
|
1055
|
+
output += pc5.cyan(`\u276F ${pc5.yellow("\u270E")} ${inputValue || pc5.dim("Type your own...")}`);
|
|
887
1056
|
} else {
|
|
888
|
-
output += ` ${
|
|
1057
|
+
output += ` ${pc5.yellow("\u270E")} ${pc5.dim("Type your own...")}`;
|
|
889
1058
|
}
|
|
890
1059
|
return output;
|
|
891
1060
|
});
|
|
@@ -900,52 +1069,64 @@ function registerGenerateCommand(skillCommand) {
|
|
|
900
1069
|
async function generateCommand(options) {
|
|
901
1070
|
trackEvent("command", { name: "generate" });
|
|
902
1071
|
log.blank();
|
|
1072
|
+
let accessToken = null;
|
|
903
1073
|
const tokens = loadTokens();
|
|
904
|
-
if (!tokens) {
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1074
|
+
if (tokens && !isTokenExpired(tokens)) {
|
|
1075
|
+
accessToken = tokens.access_token;
|
|
1076
|
+
} else {
|
|
1077
|
+
log.info("Authentication required. Logging in...");
|
|
1078
|
+
log.blank();
|
|
1079
|
+
accessToken = await performLogin();
|
|
1080
|
+
if (!accessToken) {
|
|
1081
|
+
log.error("Login failed. Please try again.");
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
log.blank();
|
|
911
1085
|
}
|
|
912
|
-
const
|
|
913
|
-
const initSpinner = ora().start();
|
|
1086
|
+
const initSpinner = ora2().start();
|
|
914
1087
|
const quota = await getSkillQuota(accessToken);
|
|
915
1088
|
if (quota.error) {
|
|
916
|
-
initSpinner.fail(
|
|
1089
|
+
initSpinner.fail(pc6.red("Failed to initialize"));
|
|
917
1090
|
return;
|
|
918
1091
|
}
|
|
919
1092
|
if (quota.tier !== "unlimited" && quota.remaining < 1) {
|
|
920
|
-
initSpinner.fail(
|
|
1093
|
+
initSpinner.fail(pc6.red("Weekly skill generation limit reached"));
|
|
921
1094
|
log.blank();
|
|
922
1095
|
console.log(
|
|
923
|
-
` You've used ${
|
|
1096
|
+
` You've used ${pc6.bold(pc6.white(quota.used.toString()))}/${pc6.bold(pc6.white(quota.limit.toString()))} skill generations this week.`
|
|
924
1097
|
);
|
|
925
1098
|
console.log(
|
|
926
|
-
` Your quota resets on ${
|
|
1099
|
+
` Your quota resets on ${pc6.yellow(new Date(quota.resetDate).toLocaleDateString())}.`
|
|
927
1100
|
);
|
|
928
1101
|
log.blank();
|
|
929
1102
|
if (quota.tier === "free") {
|
|
930
1103
|
console.log(
|
|
931
|
-
` ${
|
|
1104
|
+
` ${pc6.yellow("Tip:")} Upgrade to Pro for ${pc6.bold("10")} generations per week.`
|
|
932
1105
|
);
|
|
933
|
-
console.log(` Visit ${
|
|
1106
|
+
console.log(` Visit ${pc6.green("https://context7.com/dashboard")} to upgrade.`);
|
|
934
1107
|
}
|
|
935
1108
|
return;
|
|
936
1109
|
}
|
|
937
1110
|
initSpinner.stop();
|
|
938
1111
|
initSpinner.clear();
|
|
939
|
-
console.log(
|
|
1112
|
+
console.log(pc6.bold("What should your agent become an expert at?\n"));
|
|
940
1113
|
console.log(
|
|
941
|
-
|
|
1114
|
+
pc6.dim(
|
|
1115
|
+
"Skills should encode best practices, constraints, and decision-making \u2014\nnot step-by-step tutorials or one-off tasks.\n"
|
|
1116
|
+
)
|
|
942
1117
|
);
|
|
943
|
-
console.log(
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1118
|
+
console.log(pc6.yellow("Examples:"));
|
|
1119
|
+
{
|
|
1120
|
+
console.log(pc6.red(' \u2715 "Deploy a Next.js app to Vercel"'));
|
|
1121
|
+
console.log(pc6.green(' \u2713 "Best practices and constraints for deploying Next.js apps to Vercel"'));
|
|
1122
|
+
log.blank();
|
|
1123
|
+
console.log(pc6.red(' \u2715 "Use Tailwind for responsive design"'));
|
|
1124
|
+
console.log(pc6.green(' \u2713 "Responsive layout decision-making with Tailwind CSS"'));
|
|
1125
|
+
log.blank();
|
|
1126
|
+
console.log(pc6.red(' \u2715 "Build OAuth with NextAuth"'));
|
|
1127
|
+
console.log(pc6.green(' \u2713 "OAuth authentication patterns and pitfalls with NextAuth.js"'));
|
|
1128
|
+
}
|
|
1129
|
+
log.blank();
|
|
949
1130
|
let motivation;
|
|
950
1131
|
try {
|
|
951
1132
|
motivation = await input({
|
|
@@ -960,14 +1141,26 @@ async function generateCommand(options) {
|
|
|
960
1141
|
log.warn("Generation cancelled");
|
|
961
1142
|
return;
|
|
962
1143
|
}
|
|
963
|
-
|
|
1144
|
+
log.blank();
|
|
1145
|
+
console.log(
|
|
1146
|
+
pc6.dim(
|
|
1147
|
+
"To generate this skill, we will read relevant documentation and examples\nfrom Context7.\n"
|
|
1148
|
+
)
|
|
1149
|
+
);
|
|
1150
|
+
console.log(
|
|
1151
|
+
pc6.dim(
|
|
1152
|
+
"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"
|
|
1153
|
+
)
|
|
1154
|
+
);
|
|
1155
|
+
console.log(pc6.dim("You can adjust which sources the skill is based on.\n"));
|
|
1156
|
+
const searchSpinner = ora2("Finding relevant sources...").start();
|
|
964
1157
|
const searchResult = await searchLibraries(motivation, accessToken);
|
|
965
1158
|
if (searchResult.error || !searchResult.results?.length) {
|
|
966
|
-
searchSpinner.fail(
|
|
1159
|
+
searchSpinner.fail(pc6.red("No sources found"));
|
|
967
1160
|
log.warn(searchResult.message || "Try a different description");
|
|
968
1161
|
return;
|
|
969
1162
|
}
|
|
970
|
-
searchSpinner.succeed(
|
|
1163
|
+
searchSpinner.succeed(pc6.green(`Found ${searchResult.results.length} relevant sources`));
|
|
971
1164
|
log.blank();
|
|
972
1165
|
let selectedLibraries;
|
|
973
1166
|
try {
|
|
@@ -987,31 +1180,32 @@ async function generateCommand(options) {
|
|
|
987
1180
|
const libraryChoices = libraries.map((lib, index) => {
|
|
988
1181
|
const projectId = formatProjectId(lib.id);
|
|
989
1182
|
const isGitHub = isGitHubRepo(lib.id);
|
|
990
|
-
const indexStr =
|
|
1183
|
+
const indexStr = pc6.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
|
|
991
1184
|
const paddedName = lib.title.padEnd(maxNameLen);
|
|
992
1185
|
const libUrl = `https://context7.com${lib.id}`;
|
|
993
|
-
const libLink = terminalLink(lib.title, libUrl,
|
|
994
|
-
const
|
|
995
|
-
const
|
|
1186
|
+
const libLink = terminalLink(lib.title, libUrl, pc6.white);
|
|
1187
|
+
const sourceUrl = isGitHub ? `https://github.com/${projectId}` : `https://context7.com${lib.id}`;
|
|
1188
|
+
const repoLink = terminalLink(projectId, sourceUrl, pc6.white);
|
|
1189
|
+
const starsLine = lib.stars && isGitHub ? [`${pc6.yellow("Stars:")} ${lib.stars.toLocaleString()}`] : [];
|
|
996
1190
|
const metadataLines = [
|
|
997
|
-
|
|
1191
|
+
pc6.dim("\u2500".repeat(50)),
|
|
998
1192
|
"",
|
|
999
|
-
`${
|
|
1000
|
-
`${
|
|
1001
|
-
`${
|
|
1193
|
+
`${pc6.yellow("Library:")} ${libLink}`,
|
|
1194
|
+
`${pc6.yellow("Source:")} ${repoLink}`,
|
|
1195
|
+
`${pc6.yellow("Snippets:")} ${lib.totalSnippets.toLocaleString()}`,
|
|
1002
1196
|
...starsLine,
|
|
1003
|
-
`${
|
|
1004
|
-
|
|
1197
|
+
`${pc6.yellow("Description:")}`,
|
|
1198
|
+
pc6.white(lib.description || "No description")
|
|
1005
1199
|
];
|
|
1006
1200
|
return {
|
|
1007
|
-
name: `${indexStr} ${paddedName} ${
|
|
1201
|
+
name: `${indexStr} ${paddedName} ${pc6.dim(`(${projectId})`)}`,
|
|
1008
1202
|
value: lib,
|
|
1009
1203
|
description: metadataLines.join("\n")
|
|
1010
1204
|
};
|
|
1011
1205
|
});
|
|
1012
1206
|
selectedLibraries = await checkboxWithHover(
|
|
1013
1207
|
{
|
|
1014
|
-
message: "Select
|
|
1208
|
+
message: "Select sources:",
|
|
1015
1209
|
choices: libraryChoices,
|
|
1016
1210
|
pageSize: 10,
|
|
1017
1211
|
loop: false
|
|
@@ -1019,7 +1213,7 @@ async function generateCommand(options) {
|
|
|
1019
1213
|
{ getName: (lib) => `${lib.title} (${formatProjectId(lib.id)})` }
|
|
1020
1214
|
);
|
|
1021
1215
|
if (!selectedLibraries || selectedLibraries.length === 0) {
|
|
1022
|
-
log.info("No
|
|
1216
|
+
log.info("No sources selected. Try running the command again.");
|
|
1023
1217
|
return;
|
|
1024
1218
|
}
|
|
1025
1219
|
} catch {
|
|
@@ -1027,15 +1221,17 @@ async function generateCommand(options) {
|
|
|
1027
1221
|
return;
|
|
1028
1222
|
}
|
|
1029
1223
|
log.blank();
|
|
1030
|
-
const questionsSpinner =
|
|
1224
|
+
const questionsSpinner = ora2(
|
|
1225
|
+
"Preparing follow-up questions to clarify scope and constraints..."
|
|
1226
|
+
).start();
|
|
1031
1227
|
const librariesInput = selectedLibraries.map((lib) => ({ id: lib.id, name: lib.title }));
|
|
1032
1228
|
const questionsResult = await getSkillQuestions(librariesInput, motivation, accessToken);
|
|
1033
1229
|
if (questionsResult.error || !questionsResult.questions?.length) {
|
|
1034
|
-
questionsSpinner.fail(
|
|
1230
|
+
questionsSpinner.fail(pc6.red("Failed to generate questions"));
|
|
1035
1231
|
log.warn(questionsResult.message || "Please try again");
|
|
1036
1232
|
return;
|
|
1037
1233
|
}
|
|
1038
|
-
questionsSpinner.succeed(
|
|
1234
|
+
questionsSpinner.succeed(pc6.green("Questions prepared"));
|
|
1039
1235
|
log.blank();
|
|
1040
1236
|
const answers = [];
|
|
1041
1237
|
try {
|
|
@@ -1044,7 +1240,7 @@ async function generateCommand(options) {
|
|
|
1044
1240
|
const questionNum = i + 1;
|
|
1045
1241
|
const totalQuestions = questionsResult.questions.length;
|
|
1046
1242
|
const answer = await selectOrInput_default({
|
|
1047
|
-
message: `${
|
|
1243
|
+
message: `${pc6.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`,
|
|
1048
1244
|
options: q.options,
|
|
1049
1245
|
recommendedIndex: q.recommendedIndex
|
|
1050
1246
|
});
|
|
@@ -1055,8 +1251,8 @@ async function generateCommand(options) {
|
|
|
1055
1251
|
const linesToClear = 3 + q.options.length;
|
|
1056
1252
|
process.stdout.write(`\x1B[${linesToClear}A\x1B[J`);
|
|
1057
1253
|
const truncatedAnswer = answer.length > 50 ? answer.slice(0, 47) + "..." : answer;
|
|
1058
|
-
console.log(`${
|
|
1059
|
-
console.log(` ${
|
|
1254
|
+
console.log(`${pc6.green("\u2713")} ${pc6.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`);
|
|
1255
|
+
console.log(` ${pc6.cyan(truncatedAnswer)}`);
|
|
1060
1256
|
log.blank();
|
|
1061
1257
|
}
|
|
1062
1258
|
} catch {
|
|
@@ -1066,33 +1262,39 @@ async function generateCommand(options) {
|
|
|
1066
1262
|
let generatedContent = null;
|
|
1067
1263
|
let skillName = "";
|
|
1068
1264
|
let feedback;
|
|
1069
|
-
|
|
1265
|
+
let previewFile = null;
|
|
1266
|
+
let previewFileWritten = false;
|
|
1267
|
+
const cleanupPreviewFile = async () => {
|
|
1268
|
+
if (previewFile) {
|
|
1269
|
+
await unlink(previewFile).catch(() => {
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
};
|
|
1070
1273
|
const queryLog = [];
|
|
1071
1274
|
let genSpinner = null;
|
|
1072
1275
|
const formatQueryLogText = () => {
|
|
1073
1276
|
if (queryLog.length === 0) return "";
|
|
1074
1277
|
const lines = [];
|
|
1075
1278
|
const latestEntry = queryLog[queryLog.length - 1];
|
|
1076
|
-
lines.push(pc5.dim(`(${queryLog.length} ${queryLog.length === 1 ? "query" : "queries"})`));
|
|
1077
1279
|
lines.push("");
|
|
1078
1280
|
for (const result of latestEntry.results.slice(0, 3)) {
|
|
1079
1281
|
const cleanContent = result.content.replace(/Source:\s*https?:\/\/[^\s]+/gi, "").trim();
|
|
1080
1282
|
if (cleanContent) {
|
|
1081
|
-
lines.push(` ${
|
|
1283
|
+
lines.push(` ${pc6.yellow("\u2022")} ${pc6.white(result.title)}`);
|
|
1082
1284
|
const maxLen = 400;
|
|
1083
1285
|
const content = cleanContent.length > maxLen ? cleanContent.slice(0, maxLen - 3) + "..." : cleanContent;
|
|
1084
1286
|
const words = content.split(" ");
|
|
1085
1287
|
let currentLine = " ";
|
|
1086
1288
|
for (const word of words) {
|
|
1087
1289
|
if (currentLine.length + word.length > 84) {
|
|
1088
|
-
lines.push(
|
|
1290
|
+
lines.push(pc6.dim(currentLine));
|
|
1089
1291
|
currentLine = " " + word + " ";
|
|
1090
1292
|
} else {
|
|
1091
1293
|
currentLine += word + " ";
|
|
1092
1294
|
}
|
|
1093
1295
|
}
|
|
1094
1296
|
if (currentLine.trim()) {
|
|
1095
|
-
lines.push(
|
|
1297
|
+
lines.push(pc6.dim(currentLine));
|
|
1096
1298
|
}
|
|
1097
1299
|
lines.push("");
|
|
1098
1300
|
}
|
|
@@ -1100,19 +1302,20 @@ async function generateCommand(options) {
|
|
|
1100
1302
|
return "\n" + lines.join("\n");
|
|
1101
1303
|
};
|
|
1102
1304
|
let isGeneratingContent = false;
|
|
1305
|
+
let initialStatus = "Reading selected Context7 sources to generate the skill...";
|
|
1103
1306
|
const handleStreamEvent = (event) => {
|
|
1104
1307
|
if (event.type === "progress") {
|
|
1105
1308
|
if (genSpinner) {
|
|
1106
1309
|
if (event.message.startsWith("Generating skill content...") && !isGeneratingContent) {
|
|
1107
1310
|
isGeneratingContent = true;
|
|
1108
1311
|
if (queryLog.length > 0) {
|
|
1109
|
-
genSpinner.succeed(
|
|
1312
|
+
genSpinner.succeed(pc6.green(`Read Context7 sources`));
|
|
1110
1313
|
} else {
|
|
1111
|
-
genSpinner.succeed(
|
|
1314
|
+
genSpinner.succeed(pc6.green(`Ready to generate`));
|
|
1112
1315
|
}
|
|
1113
|
-
genSpinner =
|
|
1316
|
+
genSpinner = ora2("Generating skill content...").start();
|
|
1114
1317
|
} else if (!isGeneratingContent) {
|
|
1115
|
-
genSpinner.text =
|
|
1318
|
+
genSpinner.text = initialStatus + formatQueryLogText();
|
|
1116
1319
|
}
|
|
1117
1320
|
}
|
|
1118
1321
|
} else if (event.type === "tool_result") {
|
|
@@ -1136,18 +1339,19 @@ async function generateCommand(options) {
|
|
|
1136
1339
|
};
|
|
1137
1340
|
queryLog.length = 0;
|
|
1138
1341
|
isGeneratingContent = false;
|
|
1139
|
-
|
|
1140
|
-
|
|
1342
|
+
previewFileWritten = false;
|
|
1343
|
+
initialStatus = feedback ? "Regenerating skill with your feedback..." : "Reading selected Context7 sources to generate the skill...";
|
|
1344
|
+
genSpinner = ora2(initialStatus).start();
|
|
1141
1345
|
const result = await generateSkillStructured(generateInput, handleStreamEvent, accessToken);
|
|
1142
1346
|
if (result.error) {
|
|
1143
|
-
genSpinner.fail(
|
|
1347
|
+
genSpinner.fail(pc6.red(`Error: ${result.error}`));
|
|
1144
1348
|
return;
|
|
1145
1349
|
}
|
|
1146
1350
|
if (!result.content) {
|
|
1147
|
-
genSpinner.fail(
|
|
1351
|
+
genSpinner.fail(pc6.red("No content generated"));
|
|
1148
1352
|
return;
|
|
1149
1353
|
}
|
|
1150
|
-
genSpinner.succeed(
|
|
1354
|
+
genSpinner.succeed(pc6.green(`Generated skill for "${result.libraryName}"`));
|
|
1151
1355
|
generatedContent = result.content;
|
|
1152
1356
|
skillName = result.libraryName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
1153
1357
|
const contentLines = generatedContent.split("\n");
|
|
@@ -1157,29 +1361,40 @@ async function generateCommand(options) {
|
|
|
1157
1361
|
const remainingLines = contentLines.length - previewLineCount;
|
|
1158
1362
|
const showPreview = () => {
|
|
1159
1363
|
log.blank();
|
|
1160
|
-
console.log(
|
|
1161
|
-
console.log(
|
|
1162
|
-
console.log(
|
|
1364
|
+
console.log(pc6.dim("\u2501".repeat(70)));
|
|
1365
|
+
console.log(pc6.bold(`Generated Skill: `) + pc6.green(pc6.bold(skillName)));
|
|
1366
|
+
console.log(pc6.dim("\u2501".repeat(70)));
|
|
1163
1367
|
log.blank();
|
|
1164
1368
|
console.log(previewContent);
|
|
1165
1369
|
if (hasMoreLines) {
|
|
1166
1370
|
log.blank();
|
|
1167
|
-
console.log(
|
|
1371
|
+
console.log(pc6.dim(`... ${remainingLines} more lines`));
|
|
1168
1372
|
}
|
|
1169
1373
|
log.blank();
|
|
1170
|
-
console.log(
|
|
1374
|
+
console.log(pc6.dim("\u2501".repeat(70)));
|
|
1171
1375
|
log.blank();
|
|
1172
1376
|
};
|
|
1173
|
-
const
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1377
|
+
const openInEditor = async () => {
|
|
1378
|
+
const previewDir = join4(homedir3(), ".context7", "previews");
|
|
1379
|
+
await mkdir2(previewDir, { recursive: true });
|
|
1380
|
+
previewFile = join4(previewDir, `${skillName}.md`);
|
|
1381
|
+
if (!previewFileWritten) {
|
|
1382
|
+
await writeFile2(previewFile, generatedContent, "utf-8");
|
|
1383
|
+
previewFileWritten = true;
|
|
1384
|
+
}
|
|
1385
|
+
const editor = process.env.EDITOR || "open";
|
|
1386
|
+
await new Promise((resolve) => {
|
|
1387
|
+
const child = spawn(editor, [previewFile], {
|
|
1388
|
+
stdio: "inherit",
|
|
1389
|
+
shell: true
|
|
1390
|
+
});
|
|
1391
|
+
child.on("close", () => resolve());
|
|
1392
|
+
});
|
|
1393
|
+
};
|
|
1394
|
+
const syncFromPreviewFile = async () => {
|
|
1395
|
+
if (previewFile) {
|
|
1396
|
+
generatedContent = await readFile(previewFile, "utf-8");
|
|
1397
|
+
}
|
|
1183
1398
|
};
|
|
1184
1399
|
showPreview();
|
|
1185
1400
|
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
@@ -1187,24 +1402,26 @@ async function generateCommand(options) {
|
|
|
1187
1402
|
let action;
|
|
1188
1403
|
while (true) {
|
|
1189
1404
|
const choices = [
|
|
1190
|
-
{ name: `${
|
|
1191
|
-
|
|
1192
|
-
{ name: `${
|
|
1193
|
-
{ name: `${
|
|
1405
|
+
{ name: `${pc6.green("\u2713")} Install skill (save locally)`, value: "install" },
|
|
1406
|
+
{ name: `${pc6.blue("\u2922")} Edit skill in editor`, value: "view" },
|
|
1407
|
+
{ name: `${pc6.yellow("\u270E")} Request changes`, value: "feedback" },
|
|
1408
|
+
{ name: `${pc6.red("\u2715")} Cancel`, value: "cancel" }
|
|
1194
1409
|
];
|
|
1195
1410
|
action = await select2({
|
|
1196
1411
|
message: "What would you like to do?",
|
|
1197
1412
|
choices
|
|
1198
1413
|
});
|
|
1199
|
-
if (action === "
|
|
1200
|
-
|
|
1414
|
+
if (action === "view") {
|
|
1415
|
+
await openInEditor();
|
|
1201
1416
|
continue;
|
|
1202
1417
|
}
|
|
1418
|
+
await syncFromPreviewFile();
|
|
1203
1419
|
break;
|
|
1204
1420
|
}
|
|
1205
1421
|
if (action === "install") {
|
|
1206
1422
|
break;
|
|
1207
1423
|
} else if (action === "cancel") {
|
|
1424
|
+
await cleanupPreviewFile();
|
|
1208
1425
|
log.warn("Generation cancelled");
|
|
1209
1426
|
return;
|
|
1210
1427
|
} else if (action === "feedback") {
|
|
@@ -1218,6 +1435,7 @@ async function generateCommand(options) {
|
|
|
1218
1435
|
log.blank();
|
|
1219
1436
|
}
|
|
1220
1437
|
} catch {
|
|
1438
|
+
await cleanupPreviewFile();
|
|
1221
1439
|
log.warn("Generation cancelled");
|
|
1222
1440
|
return;
|
|
1223
1441
|
}
|
|
@@ -1228,7 +1446,7 @@ async function generateCommand(options) {
|
|
|
1228
1446
|
return;
|
|
1229
1447
|
}
|
|
1230
1448
|
const targetDirs = getTargetDirs(targets);
|
|
1231
|
-
const writeSpinner =
|
|
1449
|
+
const writeSpinner = ora2("Writing skill files...").start();
|
|
1232
1450
|
let permissionError = false;
|
|
1233
1451
|
const failedDirs = /* @__PURE__ */ new Set();
|
|
1234
1452
|
for (const targetDir of targetDirs) {
|
|
@@ -1252,28 +1470,116 @@ async function generateCommand(options) {
|
|
|
1252
1470
|
}
|
|
1253
1471
|
}
|
|
1254
1472
|
if (permissionError) {
|
|
1255
|
-
writeSpinner.fail(
|
|
1473
|
+
writeSpinner.fail(pc6.red("Permission denied"));
|
|
1256
1474
|
log.blank();
|
|
1257
|
-
console.log(
|
|
1475
|
+
console.log(pc6.yellow("Fix permissions with:"));
|
|
1258
1476
|
for (const dir of failedDirs) {
|
|
1259
1477
|
const parentDir = join4(dir, "..");
|
|
1260
|
-
console.log(
|
|
1478
|
+
console.log(pc6.dim(` sudo chown -R $(whoami) "${parentDir}"`));
|
|
1261
1479
|
}
|
|
1262
1480
|
log.blank();
|
|
1263
1481
|
return;
|
|
1264
1482
|
}
|
|
1265
|
-
writeSpinner.succeed(
|
|
1483
|
+
writeSpinner.succeed(pc6.green(`Created skill in ${targetDirs.length} location(s)`));
|
|
1266
1484
|
trackEvent("gen_install");
|
|
1267
1485
|
log.blank();
|
|
1268
|
-
console.log(
|
|
1486
|
+
console.log(pc6.green("Skill saved successfully"));
|
|
1269
1487
|
for (const targetDir of targetDirs) {
|
|
1270
|
-
console.log(
|
|
1488
|
+
console.log(pc6.dim(` ${targetDir}/`) + pc6.green(skillName));
|
|
1271
1489
|
}
|
|
1272
1490
|
log.blank();
|
|
1491
|
+
await cleanupPreviewFile();
|
|
1273
1492
|
}
|
|
1274
1493
|
|
|
1275
1494
|
// src/commands/skill.ts
|
|
1276
1495
|
import { homedir as homedir4 } from "os";
|
|
1496
|
+
|
|
1497
|
+
// src/utils/deps.ts
|
|
1498
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
1499
|
+
import { join as join5 } from "path";
|
|
1500
|
+
async function readFileOrNull(path2) {
|
|
1501
|
+
try {
|
|
1502
|
+
return await readFile2(path2, "utf-8");
|
|
1503
|
+
} catch {
|
|
1504
|
+
return null;
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
1507
|
+
function isSkippedLocally(name) {
|
|
1508
|
+
return name.startsWith("@types/");
|
|
1509
|
+
}
|
|
1510
|
+
async function parsePackageJson(cwd) {
|
|
1511
|
+
const content = await readFileOrNull(join5(cwd, "package.json"));
|
|
1512
|
+
if (!content) return [];
|
|
1513
|
+
try {
|
|
1514
|
+
const pkg2 = JSON.parse(content);
|
|
1515
|
+
const names = /* @__PURE__ */ new Set();
|
|
1516
|
+
for (const key of Object.keys(pkg2.dependencies || {})) {
|
|
1517
|
+
if (!isSkippedLocally(key)) names.add(key);
|
|
1518
|
+
}
|
|
1519
|
+
for (const key of Object.keys(pkg2.devDependencies || {})) {
|
|
1520
|
+
if (!isSkippedLocally(key)) names.add(key);
|
|
1521
|
+
}
|
|
1522
|
+
return [...names];
|
|
1523
|
+
} catch {
|
|
1524
|
+
return [];
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
async function parseRequirementsTxt(cwd) {
|
|
1528
|
+
const content = await readFileOrNull(join5(cwd, "requirements.txt"));
|
|
1529
|
+
if (!content) return [];
|
|
1530
|
+
const deps = [];
|
|
1531
|
+
for (const line of content.split("\n")) {
|
|
1532
|
+
const trimmed = line.trim();
|
|
1533
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("-")) continue;
|
|
1534
|
+
const name = trimmed.split(/[=<>!~;@\s\[]/)[0].trim();
|
|
1535
|
+
if (name && !isSkippedLocally(name)) {
|
|
1536
|
+
deps.push(name);
|
|
1537
|
+
}
|
|
1538
|
+
}
|
|
1539
|
+
return deps;
|
|
1540
|
+
}
|
|
1541
|
+
async function parsePyprojectToml(cwd) {
|
|
1542
|
+
const content = await readFileOrNull(join5(cwd, "pyproject.toml"));
|
|
1543
|
+
if (!content) return [];
|
|
1544
|
+
const deps = [];
|
|
1545
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1546
|
+
const projectDepsMatch = content.match(/\[project\]\s[\s\S]*?dependencies\s*=\s*\[([\s\S]*?)\]/);
|
|
1547
|
+
if (projectDepsMatch) {
|
|
1548
|
+
const entries = projectDepsMatch[1].match(/"([^"]+)"/g) || [];
|
|
1549
|
+
for (const entry of entries) {
|
|
1550
|
+
const name = entry.replace(/"/g, "").split(/[=<>!~;@\s\[]/)[0].trim();
|
|
1551
|
+
if (name && !isSkippedLocally(name) && !seen.has(name)) {
|
|
1552
|
+
seen.add(name);
|
|
1553
|
+
deps.push(name);
|
|
1554
|
+
}
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
const poetryMatch = content.match(/\[tool\.poetry\.dependencies\]([\s\S]*?)(?:\n\[|$)/);
|
|
1558
|
+
if (poetryMatch) {
|
|
1559
|
+
const lines = poetryMatch[1].split("\n");
|
|
1560
|
+
for (const line of lines) {
|
|
1561
|
+
const match = line.match(/^(\S+)\s*=/);
|
|
1562
|
+
if (match) {
|
|
1563
|
+
const name = match[1].trim();
|
|
1564
|
+
if (name && !isSkippedLocally(name) && name !== "python" && !seen.has(name)) {
|
|
1565
|
+
seen.add(name);
|
|
1566
|
+
deps.push(name);
|
|
1567
|
+
}
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
}
|
|
1571
|
+
return deps;
|
|
1572
|
+
}
|
|
1573
|
+
async function detectProjectDependencies(cwd) {
|
|
1574
|
+
const results = await Promise.all([
|
|
1575
|
+
parsePackageJson(cwd),
|
|
1576
|
+
parseRequirementsTxt(cwd),
|
|
1577
|
+
parsePyprojectToml(cwd)
|
|
1578
|
+
]);
|
|
1579
|
+
return [...new Set(results.flat())];
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// src/commands/skill.ts
|
|
1277
1583
|
function logInstallSummary(targets, targetDirs, skillNames) {
|
|
1278
1584
|
log.blank();
|
|
1279
1585
|
let dirIndex = 0;
|
|
@@ -1306,6 +1612,9 @@ function registerSkillCommands(program2) {
|
|
|
1306
1612
|
skill.command("info").argument("<repository>", "GitHub repository (/owner/repo)").description("Show skills in a repository").action(async (project) => {
|
|
1307
1613
|
await infoCommand(project);
|
|
1308
1614
|
});
|
|
1615
|
+
skill.command("suggest").option("--global", "Install globally instead of current directory").option("--claude", "Claude Code (.claude/skills/)").option("--cursor", "Cursor (.cursor/skills/)").option("--codex", "Codex (.codex/skills/)").option("--opencode", "OpenCode (.opencode/skills/)").option("--amp", "Amp (.agents/skills/)").option("--antigravity", "Antigravity (.agent/skills/)").description("Suggest skills based on your project dependencies").action(async (options) => {
|
|
1616
|
+
await suggestCommand(options);
|
|
1617
|
+
});
|
|
1309
1618
|
}
|
|
1310
1619
|
function registerSkillAliases(program2) {
|
|
1311
1620
|
program2.command("si", { hidden: true }).argument("<repository>", "GitHub repository (/owner/repo)").argument("[skill]", "Specific skill name to install").option("--all", "Install all skills without prompting").option("--global", "Install globally instead of current directory").option("--claude", "Claude Code (.claude/skills/)").option("--cursor", "Cursor (.cursor/skills/)").option("--codex", "Codex (.codex/skills/)").option("--opencode", "OpenCode (.opencode/skills/)").option("--amp", "Amp (.agents/skills/)").option("--antigravity", "Antigravity (.agent/skills/)").description("Install skills (alias for: skills install)").action(async (project, skillName, options) => {
|
|
@@ -1314,6 +1623,9 @@ function registerSkillAliases(program2) {
|
|
|
1314
1623
|
program2.command("ss", { hidden: true }).argument("<keywords...>", "Search keywords").description("Search for skills (alias for: skills search)").action(async (keywords) => {
|
|
1315
1624
|
await searchCommand(keywords.join(" "));
|
|
1316
1625
|
});
|
|
1626
|
+
program2.command("ssg", { hidden: true }).option("--global", "Install globally instead of current directory").option("--claude", "Claude Code (.claude/skills/)").option("--cursor", "Cursor (.cursor/skills/)").option("--codex", "Codex (.codex/skills/)").option("--opencode", "OpenCode (.opencode/skills/)").option("--amp", "Amp (.agents/skills/)").option("--antigravity", "Antigravity (.agent/skills/)").description("Suggest skills (alias for: skills suggest)").action(async (options) => {
|
|
1627
|
+
await suggestCommand(options);
|
|
1628
|
+
});
|
|
1317
1629
|
}
|
|
1318
1630
|
async function installCommand(input2, skillName, options) {
|
|
1319
1631
|
trackEvent("command", { name: "install" });
|
|
@@ -1327,17 +1639,17 @@ async function installCommand(input2, skillName, options) {
|
|
|
1327
1639
|
}
|
|
1328
1640
|
const repo = `/${parsed.owner}/${parsed.repo}`;
|
|
1329
1641
|
log.blank();
|
|
1330
|
-
const spinner =
|
|
1642
|
+
const spinner = ora3(`Fetching skills from ${repo}...`).start();
|
|
1331
1643
|
let selectedSkills;
|
|
1332
1644
|
if (skillName) {
|
|
1333
1645
|
spinner.text = `Fetching skill: ${skillName}...`;
|
|
1334
1646
|
const skillData = await getSkill(repo, skillName);
|
|
1335
1647
|
if (skillData.error || !skillData.name) {
|
|
1336
1648
|
if (skillData.error === "prompt_injection_detected") {
|
|
1337
|
-
spinner.fail(
|
|
1649
|
+
spinner.fail(pc7.red(`Prompt injection detected in skill: ${skillName}`));
|
|
1338
1650
|
log.warn("This skill contains potentially malicious content and cannot be installed.");
|
|
1339
1651
|
} else {
|
|
1340
|
-
spinner.fail(
|
|
1652
|
+
spinner.fail(pc7.red(`Skill not found: ${skillName}`));
|
|
1341
1653
|
}
|
|
1342
1654
|
return;
|
|
1343
1655
|
}
|
|
@@ -1353,11 +1665,11 @@ async function installCommand(input2, skillName, options) {
|
|
|
1353
1665
|
} else {
|
|
1354
1666
|
const data = await listProjectSkills(repo);
|
|
1355
1667
|
if (data.error) {
|
|
1356
|
-
spinner.fail(
|
|
1668
|
+
spinner.fail(pc7.red(`Error: ${data.message || data.error}`));
|
|
1357
1669
|
return;
|
|
1358
1670
|
}
|
|
1359
1671
|
if (!data.skills || data.skills.length === 0) {
|
|
1360
|
-
spinner.warn(
|
|
1672
|
+
spinner.warn(pc7.yellow(`No skills found in ${repo}`));
|
|
1361
1673
|
return;
|
|
1362
1674
|
}
|
|
1363
1675
|
const skillsWithRepo = data.skills.map((s) => ({ ...s, project: repo })).sort((a, b) => (b.installCount ?? 0) - (a.installCount ?? 0));
|
|
@@ -1375,19 +1687,19 @@ async function installCommand(input2, skillName, options) {
|
|
|
1375
1687
|
const indexWidth = data.skills.length.toString().length;
|
|
1376
1688
|
const maxNameLen = Math.max(...data.skills.map((s) => s.name.length));
|
|
1377
1689
|
const choices = skillsWithRepo.map((s, index) => {
|
|
1378
|
-
const indexStr =
|
|
1690
|
+
const indexStr = pc7.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
|
|
1379
1691
|
const paddedName = s.name.padEnd(maxNameLen);
|
|
1380
1692
|
const installs = formatInstallCount(s.installCount);
|
|
1381
1693
|
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}`,
|
|
1694
|
+
const skillLink = terminalLink(s.name, skillUrl, pc7.white);
|
|
1695
|
+
const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc7.white);
|
|
1384
1696
|
const metadataLines = [
|
|
1385
|
-
|
|
1697
|
+
pc7.dim("\u2500".repeat(50)),
|
|
1386
1698
|
"",
|
|
1387
|
-
`${
|
|
1388
|
-
`${
|
|
1389
|
-
`${
|
|
1390
|
-
|
|
1699
|
+
`${pc7.yellow("Skill:")} ${skillLink}`,
|
|
1700
|
+
`${pc7.yellow("Repo:")} ${repoLink}`,
|
|
1701
|
+
`${pc7.yellow("Description:")}`,
|
|
1702
|
+
pc7.white(s.description || "No description")
|
|
1391
1703
|
];
|
|
1392
1704
|
return {
|
|
1393
1705
|
name: installs ? `${indexStr} ${paddedName} ${installs}` : `${indexStr} ${paddedName}`,
|
|
@@ -1397,7 +1709,7 @@ async function installCommand(input2, skillName, options) {
|
|
|
1397
1709
|
});
|
|
1398
1710
|
log.blank();
|
|
1399
1711
|
const installsOffset = 4 + indexWidth + 1 + 1 + maxNameLen + 1 - 3;
|
|
1400
|
-
const message = "Select skills:" + " ".repeat(Math.max(1, installsOffset - 14)) +
|
|
1712
|
+
const message = "Select skills:" + " ".repeat(Math.max(1, installsOffset - 14)) + pc7.dim("installs");
|
|
1401
1713
|
try {
|
|
1402
1714
|
selectedSkills = await checkboxWithHover({
|
|
1403
1715
|
message,
|
|
@@ -1421,7 +1733,7 @@ async function installCommand(input2, skillName, options) {
|
|
|
1421
1733
|
return;
|
|
1422
1734
|
}
|
|
1423
1735
|
const targetDirs = getTargetDirs(targets);
|
|
1424
|
-
const installSpinner =
|
|
1736
|
+
const installSpinner = ora3("Installing skills...").start();
|
|
1425
1737
|
let permissionError = false;
|
|
1426
1738
|
const failedDirs = /* @__PURE__ */ new Set();
|
|
1427
1739
|
const installedSkills = [];
|
|
@@ -1445,7 +1757,7 @@ async function installCommand(input2, skillName, options) {
|
|
|
1445
1757
|
}
|
|
1446
1758
|
throw dirErr;
|
|
1447
1759
|
}
|
|
1448
|
-
const primarySkillDir =
|
|
1760
|
+
const primarySkillDir = join6(primaryDir, skill.name);
|
|
1449
1761
|
for (const targetDir of symlinkDirs) {
|
|
1450
1762
|
try {
|
|
1451
1763
|
await symlinkSkill(skill.name, primarySkillDir, targetDir);
|
|
@@ -1473,7 +1785,7 @@ async function installCommand(input2, skillName, options) {
|
|
|
1473
1785
|
log.blank();
|
|
1474
1786
|
log.warn("Fix permissions with:");
|
|
1475
1787
|
for (const dir of failedDirs) {
|
|
1476
|
-
const parentDir =
|
|
1788
|
+
const parentDir = join6(dir, "..");
|
|
1477
1789
|
log.dim(` sudo chown -R $(whoami) "${parentDir}"`);
|
|
1478
1790
|
}
|
|
1479
1791
|
log.blank();
|
|
@@ -1487,53 +1799,58 @@ async function installCommand(input2, skillName, options) {
|
|
|
1487
1799
|
async function searchCommand(query) {
|
|
1488
1800
|
trackEvent("command", { name: "search" });
|
|
1489
1801
|
log.blank();
|
|
1490
|
-
const spinner =
|
|
1802
|
+
const spinner = ora3(`Searching for "${query}"...`).start();
|
|
1491
1803
|
let data;
|
|
1492
1804
|
try {
|
|
1493
1805
|
data = await searchSkills(query);
|
|
1494
1806
|
} catch (err) {
|
|
1495
|
-
spinner.fail(
|
|
1807
|
+
spinner.fail(pc7.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
1496
1808
|
return;
|
|
1497
1809
|
}
|
|
1498
1810
|
if (data.error) {
|
|
1499
|
-
spinner.fail(
|
|
1811
|
+
spinner.fail(pc7.red(`Error: ${data.message || data.error}`));
|
|
1500
1812
|
return;
|
|
1501
1813
|
}
|
|
1502
1814
|
if (!data.results || data.results.length === 0) {
|
|
1503
|
-
spinner.warn(
|
|
1815
|
+
spinner.warn(pc7.yellow(`No skills found matching "${query}"`));
|
|
1504
1816
|
return;
|
|
1505
1817
|
}
|
|
1506
1818
|
spinner.succeed(`Found ${data.results.length} skill(s)`);
|
|
1507
1819
|
trackEvent("search_query", { query, resultCount: data.results.length });
|
|
1508
1820
|
const indexWidth = data.results.length.toString().length;
|
|
1509
1821
|
const maxNameLen = Math.max(...data.results.map((s) => s.name.length));
|
|
1822
|
+
const installsColWidth = 10;
|
|
1510
1823
|
const choices = data.results.map((s, index) => {
|
|
1511
|
-
const indexStr =
|
|
1824
|
+
const indexStr = pc7.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
|
|
1512
1825
|
const paddedName = s.name.padEnd(maxNameLen);
|
|
1513
|
-
const
|
|
1826
|
+
const installsRaw = s.installCount ? String(s.installCount) : "-";
|
|
1827
|
+
const paddedInstalls = formatInstallCount(s.installCount, pc7.dim("-")) + " ".repeat(installsColWidth - installsRaw.length);
|
|
1828
|
+
const trust = formatTrustScore(s.trustScore);
|
|
1514
1829
|
const skillLink = terminalLink(
|
|
1515
1830
|
s.name,
|
|
1516
1831
|
`https://context7.com/skills${s.project}/${s.name}`,
|
|
1517
|
-
|
|
1832
|
+
pc7.white
|
|
1518
1833
|
);
|
|
1519
|
-
const repoLink = terminalLink(s.project, `https://github.com${s.project}`,
|
|
1834
|
+
const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc7.white);
|
|
1520
1835
|
const metadataLines = [
|
|
1521
|
-
|
|
1836
|
+
pc7.dim("\u2500".repeat(50)),
|
|
1522
1837
|
"",
|
|
1523
|
-
`${
|
|
1524
|
-
`${
|
|
1525
|
-
`${
|
|
1526
|
-
|
|
1838
|
+
`${pc7.yellow("Skill:")} ${skillLink}`,
|
|
1839
|
+
`${pc7.yellow("Repo:")} ${repoLink}`,
|
|
1840
|
+
`${pc7.yellow("Description:")}`,
|
|
1841
|
+
pc7.white(s.description || "No description")
|
|
1527
1842
|
];
|
|
1528
1843
|
return {
|
|
1529
|
-
name:
|
|
1844
|
+
name: `${indexStr} ${paddedName} ${paddedInstalls}${trust}`,
|
|
1530
1845
|
value: s,
|
|
1531
1846
|
description: metadataLines.join("\n")
|
|
1532
1847
|
};
|
|
1533
1848
|
});
|
|
1534
1849
|
log.blank();
|
|
1535
|
-
const
|
|
1536
|
-
const
|
|
1850
|
+
const checkboxPrefixWidth = 3;
|
|
1851
|
+
const headerPad = " ".repeat(checkboxPrefixWidth + indexWidth + 1 + 1 + maxNameLen + 1);
|
|
1852
|
+
const headerLine = headerPad + pc7.dim("Installs".padEnd(installsColWidth)) + pc7.dim("Trust(0-10)");
|
|
1853
|
+
const message = "Select skills to install:\n" + headerLine;
|
|
1537
1854
|
let selectedSkills;
|
|
1538
1855
|
try {
|
|
1539
1856
|
selectedSkills = await checkboxWithHover({
|
|
@@ -1557,7 +1874,7 @@ async function searchCommand(query) {
|
|
|
1557
1874
|
return;
|
|
1558
1875
|
}
|
|
1559
1876
|
const targetDirs = getTargetDirs(targets);
|
|
1560
|
-
const installSpinner =
|
|
1877
|
+
const installSpinner = ora3("Installing skills...").start();
|
|
1561
1878
|
let permissionError = false;
|
|
1562
1879
|
const failedDirs = /* @__PURE__ */ new Set();
|
|
1563
1880
|
const installedSkills = [];
|
|
@@ -1581,7 +1898,7 @@ async function searchCommand(query) {
|
|
|
1581
1898
|
}
|
|
1582
1899
|
throw dirErr;
|
|
1583
1900
|
}
|
|
1584
|
-
const primarySkillDir =
|
|
1901
|
+
const primarySkillDir = join6(primaryDir, skill.name);
|
|
1585
1902
|
for (const targetDir of symlinkDirs) {
|
|
1586
1903
|
try {
|
|
1587
1904
|
await symlinkSkill(skill.name, primarySkillDir, targetDir);
|
|
@@ -1609,7 +1926,7 @@ async function searchCommand(query) {
|
|
|
1609
1926
|
log.blank();
|
|
1610
1927
|
log.warn("Fix permissions with:");
|
|
1611
1928
|
for (const dir of failedDirs) {
|
|
1612
|
-
const parentDir =
|
|
1929
|
+
const parentDir = join6(dir, "..");
|
|
1613
1930
|
log.dim(` sudo chown -R $(whoami) "${parentDir}"`);
|
|
1614
1931
|
}
|
|
1615
1932
|
log.blank();
|
|
@@ -1628,7 +1945,7 @@ async function listCommand(options) {
|
|
|
1628
1945
|
const idesToCheck = hasExplicitIdeOption(options) ? getSelectedIdes(options) : Object.keys(IDE_NAMES);
|
|
1629
1946
|
const results = [];
|
|
1630
1947
|
for (const ide of idesToCheck) {
|
|
1631
|
-
const skillsDir =
|
|
1948
|
+
const skillsDir = join6(baseDir, pathMap[ide]);
|
|
1632
1949
|
try {
|
|
1633
1950
|
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
1634
1951
|
const skillFolders = entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);
|
|
@@ -1646,9 +1963,9 @@ async function listCommand(options) {
|
|
|
1646
1963
|
for (const { ide, skills } of results) {
|
|
1647
1964
|
const ideName = IDE_NAMES[ide];
|
|
1648
1965
|
const path2 = pathMap[ide];
|
|
1649
|
-
log.plain(`${
|
|
1966
|
+
log.plain(`${pc7.bold(ideName)} ${pc7.dim(path2)}`);
|
|
1650
1967
|
for (const skill of skills) {
|
|
1651
|
-
log.plain(` ${
|
|
1968
|
+
log.plain(` ${pc7.green(skill)}`);
|
|
1652
1969
|
}
|
|
1653
1970
|
log.blank();
|
|
1654
1971
|
}
|
|
@@ -1661,7 +1978,7 @@ async function removeCommand(name, options) {
|
|
|
1661
1978
|
return;
|
|
1662
1979
|
}
|
|
1663
1980
|
const skillsDir = getTargetDirFromSelection(target.ide, target.scope);
|
|
1664
|
-
const skillPath =
|
|
1981
|
+
const skillPath = join6(skillsDir, name);
|
|
1665
1982
|
try {
|
|
1666
1983
|
await rm2(skillPath, { recursive: true });
|
|
1667
1984
|
log.success(`Removed skill: ${name}`);
|
|
@@ -1688,14 +2005,14 @@ async function infoCommand(input2) {
|
|
|
1688
2005
|
}
|
|
1689
2006
|
const repo = `/${parsed.owner}/${parsed.repo}`;
|
|
1690
2007
|
log.blank();
|
|
1691
|
-
const spinner =
|
|
2008
|
+
const spinner = ora3(`Fetching skills from ${repo}...`).start();
|
|
1692
2009
|
const data = await listProjectSkills(repo);
|
|
1693
2010
|
if (data.error) {
|
|
1694
|
-
spinner.fail(
|
|
2011
|
+
spinner.fail(pc7.red(`Error: ${data.message || data.error}`));
|
|
1695
2012
|
return;
|
|
1696
2013
|
}
|
|
1697
2014
|
if (!data.skills || data.skills.length === 0) {
|
|
1698
|
-
spinner.warn(
|
|
2015
|
+
spinner.warn(pc7.yellow(`No skills found in ${repo}`));
|
|
1699
2016
|
return;
|
|
1700
2017
|
}
|
|
1701
2018
|
spinner.succeed(`Found ${data.skills.length} skill(s)`);
|
|
@@ -1707,154 +2024,175 @@ async function infoCommand(input2) {
|
|
|
1707
2024
|
log.blank();
|
|
1708
2025
|
}
|
|
1709
2026
|
log.plain(
|
|
1710
|
-
`${
|
|
1711
|
-
Install all: ${
|
|
1712
|
-
Install one: ${
|
|
2027
|
+
`${pc7.bold("Quick commands:")}
|
|
2028
|
+
Install all: ${pc7.cyan(`ctx7 skills install ${repo} --all`)}
|
|
2029
|
+
Install one: ${pc7.cyan(`ctx7 skills install ${repo} ${data.skills[0]?.name}`)}
|
|
1713
2030
|
`
|
|
1714
2031
|
);
|
|
1715
2032
|
}
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
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();
|
|
2033
|
+
async function suggestCommand(options) {
|
|
2034
|
+
trackEvent("command", { name: "suggest" });
|
|
2035
|
+
log.blank();
|
|
2036
|
+
const scanSpinner = ora3("Scanning project dependencies...").start();
|
|
2037
|
+
const deps = await detectProjectDependencies(process.cwd());
|
|
2038
|
+
if (deps.length === 0) {
|
|
2039
|
+
scanSpinner.warn(pc7.yellow("No dependencies detected"));
|
|
2040
|
+
log.info(`Try ${pc7.cyan("ctx7 skills search <keyword>")} to search manually`);
|
|
2041
|
+
return;
|
|
1750
2042
|
}
|
|
1751
|
-
|
|
2043
|
+
scanSpinner.succeed(`Found ${deps.length} dependencies`);
|
|
2044
|
+
const searchSpinner = ora3("Finding matching skills...").start();
|
|
2045
|
+
const tokens = loadTokens();
|
|
2046
|
+
const accessToken = tokens && !isTokenExpired(tokens) ? tokens.access_token : void 0;
|
|
2047
|
+
let data;
|
|
1752
2048
|
try {
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
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);
|
|
2049
|
+
data = await suggestSkills(deps, accessToken);
|
|
2050
|
+
} catch {
|
|
2051
|
+
searchSpinner.fail(pc7.red("Failed to connect to Context7"));
|
|
2052
|
+
return;
|
|
1807
2053
|
}
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
if (clearTokens()) {
|
|
1812
|
-
console.log(pc7.green("Logged out successfully."));
|
|
1813
|
-
} else {
|
|
1814
|
-
console.log(pc7.yellow("You are not logged in."));
|
|
2054
|
+
if (data.error) {
|
|
2055
|
+
searchSpinner.fail(pc7.red(`Error: ${data.message || data.error}`));
|
|
2056
|
+
return;
|
|
1815
2057
|
}
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
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."));
|
|
2058
|
+
const skills = data.skills;
|
|
2059
|
+
if (skills.length === 0) {
|
|
2060
|
+
searchSpinner.warn(pc7.yellow("No matching skills found for your dependencies"));
|
|
1823
2061
|
return;
|
|
1824
2062
|
}
|
|
1825
|
-
|
|
2063
|
+
searchSpinner.succeed(`Found ${skills.length} relevant skill(s)`);
|
|
2064
|
+
trackEvent("suggest_results", { depCount: deps.length, skillCount: skills.length });
|
|
2065
|
+
log.blank();
|
|
2066
|
+
const maxNameLen = Math.max(...skills.map((s) => s.name.length));
|
|
2067
|
+
const installsColWidth = 10;
|
|
2068
|
+
const trustColWidth = 12;
|
|
2069
|
+
const maxMatchedLen = Math.max(...skills.map((s) => s.matchedDep.length));
|
|
2070
|
+
const indexWidth = skills.length.toString().length;
|
|
2071
|
+
const choices = skills.map((s, index) => {
|
|
2072
|
+
const indexStr = pc7.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
|
|
2073
|
+
const paddedName = s.name.padEnd(maxNameLen);
|
|
2074
|
+
const installsRaw = s.installCount ? String(s.installCount) : "-";
|
|
2075
|
+
const paddedInstalls = formatInstallCount(s.installCount, pc7.dim("-")) + " ".repeat(installsColWidth - installsRaw.length);
|
|
2076
|
+
const trustRaw = s.trustScore !== void 0 && s.trustScore >= 0 ? s.trustScore.toFixed(1) : "-";
|
|
2077
|
+
const trust = formatTrustScore(s.trustScore) + " ".repeat(trustColWidth - trustRaw.length);
|
|
2078
|
+
const matched = pc7.yellow(s.matchedDep.padEnd(maxMatchedLen));
|
|
2079
|
+
const skillLink = terminalLink(
|
|
2080
|
+
s.name,
|
|
2081
|
+
`https://context7.com/skills${s.project}/${s.name}`,
|
|
2082
|
+
pc7.white
|
|
2083
|
+
);
|
|
2084
|
+
const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc7.white);
|
|
2085
|
+
const metadataLines = [
|
|
2086
|
+
pc7.dim("\u2500".repeat(50)),
|
|
2087
|
+
"",
|
|
2088
|
+
`${pc7.yellow("Skill:")} ${skillLink}`,
|
|
2089
|
+
`${pc7.yellow("Repo:")} ${repoLink}`,
|
|
2090
|
+
`${pc7.yellow("Relevant:")} ${pc7.white(s.matchedDep)}`,
|
|
2091
|
+
`${pc7.yellow("Description:")}`,
|
|
2092
|
+
pc7.white(s.description || "No description")
|
|
2093
|
+
];
|
|
2094
|
+
return {
|
|
2095
|
+
name: `${indexStr} ${paddedName} ${paddedInstalls}${trust}${matched}`,
|
|
2096
|
+
value: s,
|
|
2097
|
+
description: metadataLines.join("\n")
|
|
2098
|
+
};
|
|
2099
|
+
});
|
|
2100
|
+
const checkboxPrefixWidth = 3;
|
|
2101
|
+
const headerPad = " ".repeat(checkboxPrefixWidth + indexWidth + 1 + 1 + maxNameLen + 1);
|
|
2102
|
+
const headerLine = headerPad + pc7.dim("Installs".padEnd(installsColWidth)) + pc7.dim("Trust(0-10)".padEnd(trustColWidth)) + pc7.dim("Relevant");
|
|
2103
|
+
const message = "Select skills to install:\n" + headerLine;
|
|
2104
|
+
let selectedSkills;
|
|
1826
2105
|
try {
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
}
|
|
2106
|
+
selectedSkills = await checkboxWithHover({
|
|
2107
|
+
message,
|
|
2108
|
+
choices,
|
|
2109
|
+
pageSize: 15,
|
|
2110
|
+
loop: false
|
|
2111
|
+
});
|
|
1834
2112
|
} catch {
|
|
1835
|
-
|
|
1836
|
-
|
|
2113
|
+
log.warn("Installation cancelled");
|
|
2114
|
+
return;
|
|
2115
|
+
}
|
|
2116
|
+
if (selectedSkills.length === 0) {
|
|
2117
|
+
log.warn("No skills selected");
|
|
2118
|
+
return;
|
|
2119
|
+
}
|
|
2120
|
+
const targets = await promptForInstallTargets(options);
|
|
2121
|
+
if (!targets) {
|
|
2122
|
+
log.warn("Installation cancelled");
|
|
2123
|
+
return;
|
|
2124
|
+
}
|
|
2125
|
+
const targetDirs = getTargetDirs(targets);
|
|
2126
|
+
const installSpinner = ora3("Installing skills...").start();
|
|
2127
|
+
let permissionError = false;
|
|
2128
|
+
const failedDirs = /* @__PURE__ */ new Set();
|
|
2129
|
+
const installedSkills = [];
|
|
2130
|
+
for (const skill of selectedSkills) {
|
|
2131
|
+
try {
|
|
2132
|
+
installSpinner.text = `Downloading ${skill.name}...`;
|
|
2133
|
+
const downloadData = await downloadSkill(skill.project, skill.name);
|
|
2134
|
+
if (downloadData.error) {
|
|
2135
|
+
log.warn(`Failed to download ${skill.name}: ${downloadData.error}`);
|
|
2136
|
+
continue;
|
|
2137
|
+
}
|
|
2138
|
+
installSpinner.text = `Installing ${skill.name}...`;
|
|
2139
|
+
const [primaryDir, ...symlinkDirs] = targetDirs;
|
|
2140
|
+
try {
|
|
2141
|
+
await installSkillFiles(skill.name, downloadData.files, primaryDir);
|
|
2142
|
+
} catch (dirErr) {
|
|
2143
|
+
const error = dirErr;
|
|
2144
|
+
if (error.code === "EACCES" || error.code === "EPERM") {
|
|
2145
|
+
permissionError = true;
|
|
2146
|
+
failedDirs.add(primaryDir);
|
|
2147
|
+
}
|
|
2148
|
+
throw dirErr;
|
|
2149
|
+
}
|
|
2150
|
+
const primarySkillDir = join6(primaryDir, skill.name);
|
|
2151
|
+
for (const targetDir of symlinkDirs) {
|
|
2152
|
+
try {
|
|
2153
|
+
await symlinkSkill(skill.name, primarySkillDir, targetDir);
|
|
2154
|
+
} catch (dirErr) {
|
|
2155
|
+
const error = dirErr;
|
|
2156
|
+
if (error.code === "EACCES" || error.code === "EPERM") {
|
|
2157
|
+
permissionError = true;
|
|
2158
|
+
failedDirs.add(targetDir);
|
|
2159
|
+
}
|
|
2160
|
+
throw dirErr;
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
installedSkills.push(`${skill.project}/${skill.name}`);
|
|
2164
|
+
} catch (err) {
|
|
2165
|
+
const error = err;
|
|
2166
|
+
if (error.code === "EACCES" || error.code === "EPERM") {
|
|
2167
|
+
continue;
|
|
2168
|
+
}
|
|
2169
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
2170
|
+
log.warn(`Failed to install ${skill.name}: ${errMsg}`);
|
|
1837
2171
|
}
|
|
1838
2172
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
2173
|
+
if (permissionError) {
|
|
2174
|
+
installSpinner.fail("Permission denied");
|
|
2175
|
+
log.blank();
|
|
2176
|
+
log.warn("Fix permissions with:");
|
|
2177
|
+
for (const dir of failedDirs) {
|
|
2178
|
+
const parentDir = join6(dir, "..");
|
|
2179
|
+
log.dim(` sudo chown -R $(whoami) "${parentDir}"`);
|
|
1844
2180
|
}
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
throw new Error("Failed to fetch user info");
|
|
2181
|
+
log.blank();
|
|
2182
|
+
return;
|
|
1848
2183
|
}
|
|
1849
|
-
|
|
2184
|
+
installSpinner.succeed(`Installed ${installedSkills.length} skill(s)`);
|
|
2185
|
+
trackEvent("suggest_install", { skills: installedSkills, ides: targets.ides });
|
|
2186
|
+
const installedNames = selectedSkills.map((s) => s.name);
|
|
2187
|
+
logInstallSummary(targets, targetDirs, installedNames);
|
|
1850
2188
|
}
|
|
1851
2189
|
|
|
1852
2190
|
// src/constants.ts
|
|
1853
2191
|
import { readFileSync as readFileSync2 } from "fs";
|
|
1854
2192
|
import { fileURLToPath } from "url";
|
|
1855
|
-
import { dirname as dirname2, join as
|
|
2193
|
+
import { dirname as dirname2, join as join7 } from "path";
|
|
1856
2194
|
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
1857
|
-
var pkg = JSON.parse(readFileSync2(
|
|
2195
|
+
var pkg = JSON.parse(readFileSync2(join7(__dirname, "../package.json"), "utf-8"));
|
|
1858
2196
|
var VERSION = pkg.version;
|
|
1859
2197
|
var NAME = pkg.name;
|
|
1860
2198
|
|