ctx7 0.1.5 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -2,25 +2,25 @@
2
2
 
3
3
  // src/index.ts
4
4
  import { Command } from "commander";
5
- import pc5 from "picocolors";
5
+ import pc8 from "picocolors";
6
6
  import figlet from "figlet";
7
7
 
8
8
  // src/commands/skill.ts
9
- import pc4 from "picocolors";
10
- import ora from "ora";
9
+ import pc6 from "picocolors";
10
+ import ora2 from "ora";
11
11
  import { readdir, rm as rm2 } from "fs/promises";
12
- import { join as join3 } from "path";
12
+ import { join as join5 } from "path";
13
13
 
14
14
  // src/utils/parse-input.ts
15
- function parseSkillInput(input) {
16
- const urlMatch = input.match(
15
+ function parseSkillInput(input2) {
16
+ const urlMatch = input2.match(
17
17
  /(?:https?:\/\/)?github\.com\/([^\/]+)\/([^\/]+)\/tree\/([^\/]+)\/(.+)/
18
18
  );
19
19
  if (urlMatch) {
20
- const [, owner, repo, branch, path] = urlMatch;
21
- return { type: "url", owner, repo, branch, path };
20
+ const [, owner, repo, branch, path2] = urlMatch;
21
+ return { type: "url", owner, repo, branch, path: path2 };
22
22
  }
23
- const shortMatch = input.match(/^\/?([^\/]+)\/([^\/]+)$/);
23
+ const shortMatch = input2.match(/^\/?([^\/]+)\/([^\/]+)$/);
24
24
  if (shortMatch) {
25
25
  const [, owner, repo] = shortMatch;
26
26
  return { type: "repo", owner, repo };
@@ -45,24 +45,24 @@ function parseGitHubUrl(url) {
45
45
  if (pathParts2.length > 0 && pathParts2[pathParts2.length - 1].includes(".")) {
46
46
  pathParts2.pop();
47
47
  }
48
- const path2 = pathParts2.join("/");
49
- return { owner, repo, branch: branch2, path: path2 };
48
+ const path3 = pathParts2.join("/");
49
+ return { owner, repo, branch: branch2, path: path3 };
50
50
  }
51
51
  const branch = parts[2];
52
52
  const pathParts = parts.slice(3);
53
53
  if (pathParts.length > 0 && pathParts[pathParts.length - 1].includes(".")) {
54
54
  pathParts.pop();
55
55
  }
56
- const path = pathParts.join("/");
57
- return { owner, repo, branch, path };
56
+ const path2 = pathParts.join("/");
57
+ return { owner, repo, branch, path: path2 };
58
58
  }
59
59
  if (urlObj.hostname === "github.com") {
60
60
  if (parts.length < 4 || parts[2] !== "tree") return null;
61
61
  const owner = parts[0];
62
62
  const repo = parts[1];
63
63
  const branch = parts[3];
64
- const path = parts.slice(4).join("/");
65
- return { owner, repo, branch, path };
64
+ const path2 = parts.slice(4).join("/");
65
+ return { owner, repo, branch, path: path2 };
66
66
  }
67
67
  return null;
68
68
  } catch {
@@ -165,6 +165,124 @@ async function downloadSkill(project, skillName) {
165
165
  }
166
166
  return { skill, files };
167
167
  }
168
+ async function searchLibraries(query, accessToken) {
169
+ const params = new URLSearchParams({ query });
170
+ const headers = {};
171
+ if (accessToken) {
172
+ headers["Authorization"] = `Bearer ${accessToken}`;
173
+ }
174
+ const response = await fetch(`${baseUrl}/api/v2/libs/search?${params}`, { headers });
175
+ return await response.json();
176
+ }
177
+ async function getSkillQuota(accessToken) {
178
+ const response = await fetch(`${baseUrl}/api/v2/skills/quota`, {
179
+ headers: { Authorization: `Bearer ${accessToken}` }
180
+ });
181
+ if (!response.ok) {
182
+ const errorData = await response.json().catch(() => ({}));
183
+ return {
184
+ used: 0,
185
+ limit: 0,
186
+ remaining: 0,
187
+ tier: "free",
188
+ resetDate: null,
189
+ error: errorData.message || `HTTP error ${response.status}`
190
+ };
191
+ }
192
+ return await response.json();
193
+ }
194
+ async function getSkillQuestions(libraries, motivation, accessToken) {
195
+ const headers = { "Content-Type": "application/json" };
196
+ if (accessToken) {
197
+ headers["Authorization"] = `Bearer ${accessToken}`;
198
+ }
199
+ const response = await fetch(`${baseUrl}/api/v2/skills/questions`, {
200
+ method: "POST",
201
+ headers,
202
+ body: JSON.stringify({ libraries, motivation })
203
+ });
204
+ if (!response.ok) {
205
+ const errorData = await response.json().catch(() => ({}));
206
+ return {
207
+ questions: [],
208
+ error: errorData.message || `HTTP error ${response.status}`
209
+ };
210
+ }
211
+ return await response.json();
212
+ }
213
+ async function generateSkillStructured(input2, onEvent, accessToken) {
214
+ const headers = { "Content-Type": "application/json" };
215
+ if (accessToken) {
216
+ headers["Authorization"] = `Bearer ${accessToken}`;
217
+ }
218
+ const response = await fetch(`${baseUrl}/api/v2/skills/generate`, {
219
+ method: "POST",
220
+ headers,
221
+ body: JSON.stringify(input2)
222
+ });
223
+ const libraryName = input2.libraries[0]?.name || "skill";
224
+ return handleGenerateResponse(response, libraryName, onEvent);
225
+ }
226
+ async function handleGenerateResponse(response, libraryName, onEvent) {
227
+ if (!response.ok) {
228
+ const errorData = await response.json().catch(() => ({}));
229
+ return {
230
+ content: "",
231
+ libraryName,
232
+ error: errorData.message || `HTTP error ${response.status}`
233
+ };
234
+ }
235
+ const reader = response.body?.getReader();
236
+ if (!reader) {
237
+ return { content: "", libraryName, error: "No response body" };
238
+ }
239
+ const decoder = new TextDecoder();
240
+ let content = "";
241
+ let finalLibraryName = libraryName;
242
+ let error;
243
+ let buffer = "";
244
+ while (true) {
245
+ const { done, value } = await reader.read();
246
+ if (done) break;
247
+ const chunk = decoder.decode(value, { stream: true });
248
+ buffer += chunk;
249
+ const lines = buffer.split("\n");
250
+ buffer = lines.pop() || "";
251
+ for (const line of lines) {
252
+ const trimmedLine = line.trim();
253
+ if (!trimmedLine) continue;
254
+ try {
255
+ const data = JSON.parse(trimmedLine);
256
+ if (onEvent) {
257
+ onEvent(data);
258
+ }
259
+ if (data.type === "complete") {
260
+ content = data.content || "";
261
+ finalLibraryName = data.libraryName || libraryName;
262
+ } else if (data.type === "error") {
263
+ error = data.message;
264
+ }
265
+ } catch {
266
+ }
267
+ }
268
+ }
269
+ if (buffer.trim()) {
270
+ try {
271
+ const data = JSON.parse(buffer.trim());
272
+ if (onEvent) {
273
+ onEvent(data);
274
+ }
275
+ if (data.type === "complete") {
276
+ content = data.content || "";
277
+ finalLibraryName = data.libraryName || libraryName;
278
+ } else if (data.type === "error") {
279
+ error = data.message;
280
+ }
281
+ } catch {
282
+ }
283
+ }
284
+ return { content, libraryName: finalLibraryName, error };
285
+ }
168
286
 
169
287
  // src/utils/logger.ts
170
288
  import pc from "picocolors";
@@ -427,13 +545,14 @@ function formatInstallCount(count) {
427
545
  }
428
546
  return `\x1B[38;5;214m\u2193${display}\x1B[0m`;
429
547
  }
430
- async function checkboxWithHover(config) {
548
+ async function checkboxWithHover(config, options) {
431
549
  const choices = config.choices.filter(
432
550
  (c) => typeof c === "object" && c !== null && !("type" in c && c.type === "separator")
433
551
  );
434
552
  const values = choices.map((c) => c.value);
435
553
  const totalItems = values.length;
436
554
  let cursorPosition = 0;
555
+ const getName = options?.getName ?? ((v) => v.name);
437
556
  const keypressHandler = (_str, key) => {
438
557
  if (key.name === "up" && cursorPosition > 0) {
439
558
  cursorPosition--;
@@ -452,9 +571,9 @@ async function checkboxWithHover(config) {
452
571
  highlight: (text) => pc3.green(text),
453
572
  renderSelectedChoices: (selected, _allChoices) => {
454
573
  if (selected.length === 0) {
455
- return pc3.dim(values[cursorPosition].name);
574
+ return pc3.dim(getName(values[cursorPosition]));
456
575
  }
457
- return selected.map((c) => c.value.name).join(", ");
576
+ return selected.map((c) => getName(c.value)).join(", ");
458
577
  }
459
578
  }
460
579
  }
@@ -495,7 +614,670 @@ async function symlinkSkill(skillName, sourcePath, targetDir) {
495
614
  await symlink(sourcePath, targetPath);
496
615
  }
497
616
 
617
+ // src/commands/generate.ts
618
+ import pc5 from "picocolors";
619
+ import ora from "ora";
620
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
621
+ import { join as join4 } from "path";
622
+ import { homedir as homedir3 } from "os";
623
+ import { input, select as select2 } from "@inquirer/prompts";
624
+
625
+ // src/utils/auth.ts
626
+ import * as crypto from "crypto";
627
+ import * as http from "http";
628
+ import * as fs from "fs";
629
+ import * as path from "path";
630
+ import * as os from "os";
631
+ var CONFIG_DIR = path.join(os.homedir(), ".context7");
632
+ var CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
633
+ function generatePKCE() {
634
+ const codeVerifier = crypto.randomBytes(32).toString("base64url");
635
+ const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");
636
+ return { codeVerifier, codeChallenge };
637
+ }
638
+ function generateState() {
639
+ return crypto.randomBytes(16).toString("base64url");
640
+ }
641
+ function ensureConfigDir() {
642
+ if (!fs.existsSync(CONFIG_DIR)) {
643
+ fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
644
+ }
645
+ }
646
+ function saveTokens(tokens) {
647
+ ensureConfigDir();
648
+ const data = {
649
+ ...tokens,
650
+ expires_at: tokens.expires_at ?? (tokens.expires_in ? Date.now() + tokens.expires_in * 1e3 : void 0)
651
+ };
652
+ fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(data, null, 2), { mode: 384 });
653
+ }
654
+ function loadTokens() {
655
+ if (!fs.existsSync(CREDENTIALS_FILE)) {
656
+ return null;
657
+ }
658
+ try {
659
+ const data = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, "utf-8"));
660
+ return data;
661
+ } catch {
662
+ return null;
663
+ }
664
+ }
665
+ function clearTokens() {
666
+ if (fs.existsSync(CREDENTIALS_FILE)) {
667
+ fs.unlinkSync(CREDENTIALS_FILE);
668
+ return true;
669
+ }
670
+ return false;
671
+ }
672
+ function isTokenExpired(tokens) {
673
+ if (!tokens.expires_at) {
674
+ return false;
675
+ }
676
+ return Date.now() > tokens.expires_at - 6e4;
677
+ }
678
+ var CALLBACK_PORT = 52417;
679
+ function createCallbackServer(expectedState) {
680
+ let resolvePort;
681
+ let resolveResult;
682
+ let rejectResult;
683
+ let serverInstance = null;
684
+ const portPromise = new Promise((resolve) => {
685
+ resolvePort = resolve;
686
+ });
687
+ const resultPromise = new Promise((resolve, reject) => {
688
+ resolveResult = resolve;
689
+ rejectResult = reject;
690
+ });
691
+ const server = http.createServer((req, res) => {
692
+ const url = new URL(req.url || "/", `http://localhost`);
693
+ if (url.pathname === "/callback") {
694
+ const code = url.searchParams.get("code");
695
+ const state = url.searchParams.get("state");
696
+ const error = url.searchParams.get("error");
697
+ const errorDescription = url.searchParams.get("error_description");
698
+ res.writeHead(200, { "Content-Type": "text/html" });
699
+ if (error) {
700
+ res.end(errorPage(errorDescription || error));
701
+ serverInstance?.close();
702
+ rejectResult(new Error(errorDescription || error));
703
+ return;
704
+ }
705
+ if (!code || !state) {
706
+ res.end(errorPage("Missing authorization code or state"));
707
+ serverInstance?.close();
708
+ rejectResult(new Error("Missing authorization code or state"));
709
+ return;
710
+ }
711
+ if (state !== expectedState) {
712
+ res.end(errorPage("State mismatch - possible CSRF attack"));
713
+ serverInstance?.close();
714
+ rejectResult(new Error("State mismatch"));
715
+ return;
716
+ }
717
+ res.end(successPage());
718
+ serverInstance?.close();
719
+ resolveResult({ code, state });
720
+ } else {
721
+ res.writeHead(404);
722
+ res.end("Not found");
723
+ }
724
+ });
725
+ serverInstance = server;
726
+ server.on("error", (err) => {
727
+ rejectResult(err);
728
+ });
729
+ server.listen(CALLBACK_PORT, "127.0.0.1", () => {
730
+ resolvePort(CALLBACK_PORT);
731
+ });
732
+ const timeout = setTimeout(
733
+ () => {
734
+ server.close();
735
+ rejectResult(new Error("Login timed out after 5 minutes"));
736
+ },
737
+ 5 * 60 * 1e3
738
+ );
739
+ return {
740
+ port: portPromise,
741
+ result: resultPromise,
742
+ close: () => {
743
+ clearTimeout(timeout);
744
+ server.close();
745
+ }
746
+ };
747
+ }
748
+ function successPage() {
749
+ return `<!DOCTYPE html>
750
+ <html>
751
+ <head><title>Login Successful</title></head>
752
+ <body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f9fafb;">
753
+ <div style="text-align: center; padding: 2rem;">
754
+ <div style="width: 64px; height: 64px; background: #16a34a; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem;">
755
+ <svg width="32" height="32" fill="none" stroke="white" stroke-width="3" viewBox="0 0 24 24">
756
+ <path d="M5 13l4 4L19 7" stroke-linecap="round" stroke-linejoin="round"/>
757
+ </svg>
758
+ </div>
759
+ <h1 style="color: #16a34a; margin: 0 0 0.5rem;">Login Successful!</h1>
760
+ <p style="color: #6b7280; margin: 0;">You can close this window and return to the terminal.</p>
761
+ </div>
762
+ </body>
763
+ </html>`;
764
+ }
765
+ function escapeHtml(text) {
766
+ return text.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
767
+ }
768
+ function errorPage(message) {
769
+ const safeMessage = escapeHtml(message);
770
+ return `<!DOCTYPE html>
771
+ <html>
772
+ <head><title>Login Failed</title></head>
773
+ <body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f9fafb;">
774
+ <div style="text-align: center; padding: 2rem;">
775
+ <div style="width: 64px; height: 64px; background: #dc2626; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem;">
776
+ <svg width="32" height="32" fill="none" stroke="white" stroke-width="3" viewBox="0 0 24 24">
777
+ <path d="M6 18L18 6M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/>
778
+ </svg>
779
+ </div>
780
+ <h1 style="color: #dc2626; margin: 0 0 0.5rem;">Login Failed</h1>
781
+ <p style="color: #6b7280; margin: 0;">${safeMessage}</p>
782
+ <p style="color: #9ca3af; margin: 1rem 0 0; font-size: 0.875rem;">You can close this window.</p>
783
+ </div>
784
+ </body>
785
+ </html>`;
786
+ }
787
+ async function exchangeCodeForTokens(baseUrl3, code, codeVerifier, redirectUri, clientId) {
788
+ const response = await fetch(`${baseUrl3}/api/oauth/token`, {
789
+ method: "POST",
790
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
791
+ body: new URLSearchParams({
792
+ grant_type: "authorization_code",
793
+ client_id: clientId,
794
+ code,
795
+ code_verifier: codeVerifier,
796
+ redirect_uri: redirectUri
797
+ }).toString()
798
+ });
799
+ if (!response.ok) {
800
+ const err = await response.json().catch(() => ({}));
801
+ throw new Error(err.error_description || err.error || "Failed to exchange code for tokens");
802
+ }
803
+ return await response.json();
804
+ }
805
+ function buildAuthorizationUrl(baseUrl3, clientId, redirectUri, codeChallenge, state) {
806
+ const url = new URL(`${baseUrl3}/api/oauth/authorize`);
807
+ url.searchParams.set("client_id", clientId);
808
+ url.searchParams.set("redirect_uri", redirectUri);
809
+ url.searchParams.set("code_challenge", codeChallenge);
810
+ url.searchParams.set("code_challenge_method", "S256");
811
+ url.searchParams.set("state", state);
812
+ url.searchParams.set("scope", "profile email");
813
+ url.searchParams.set("response_type", "code");
814
+ return url.toString();
815
+ }
816
+
817
+ // src/utils/selectOrInput.ts
818
+ import {
819
+ createPrompt,
820
+ useState,
821
+ useKeypress,
822
+ usePrefix,
823
+ isEnterKey,
824
+ isUpKey,
825
+ isDownKey
826
+ } from "@inquirer/core";
827
+ import pc4 from "picocolors";
828
+ var selectOrInput = createPrompt((config, done) => {
829
+ const { message, options, recommendedIndex = 0 } = config;
830
+ const [cursor, setCursor] = useState(recommendedIndex);
831
+ const [inputValue, setInputValue] = useState("");
832
+ const prefix = usePrefix({});
833
+ useKeypress((key, rl) => {
834
+ if (isUpKey(key)) {
835
+ setCursor(Math.max(0, cursor - 1));
836
+ return;
837
+ }
838
+ if (isDownKey(key)) {
839
+ setCursor(Math.min(options.length, cursor + 1));
840
+ return;
841
+ }
842
+ if (isEnterKey(key)) {
843
+ if (cursor === options.length) {
844
+ const finalValue = inputValue.trim();
845
+ done(finalValue || options[recommendedIndex]);
846
+ } else {
847
+ done(options[cursor]);
848
+ }
849
+ return;
850
+ }
851
+ if (cursor === options.length && key.name !== "return") {
852
+ if (key.name === "w" && key.ctrl || key.name === "backspace") {
853
+ if (key.name === "w" && key.ctrl) {
854
+ const words = inputValue.trimEnd().split(/\s+/);
855
+ if (words.length > 0) {
856
+ words.pop();
857
+ setInputValue(
858
+ words.join(" ") + (inputValue.endsWith(" ") && words.length > 0 ? " " : "")
859
+ );
860
+ }
861
+ } else {
862
+ setInputValue(inputValue.slice(0, -1));
863
+ }
864
+ } else if (key.name === "u" && key.ctrl) {
865
+ setInputValue("");
866
+ } else if (key.name === "space") {
867
+ setInputValue(inputValue + " ");
868
+ } else if (key.name && key.name.length === 1 && !key.ctrl) {
869
+ setInputValue(inputValue + key.name);
870
+ }
871
+ } else if (rl.line) {
872
+ rl.line = "";
873
+ }
874
+ });
875
+ let output = `${prefix} ${pc4.bold(message)}
876
+
877
+ `;
878
+ options.forEach((opt, idx) => {
879
+ const isRecommended = idx === recommendedIndex;
880
+ const isCursor = idx === cursor;
881
+ const number = pc4.cyan(`${idx + 1}.`);
882
+ const text = isRecommended ? `${opt} ${pc4.green("\u2713 Recommended")}` : opt;
883
+ if (isCursor) {
884
+ output += pc4.cyan(`\u276F ${number} ${text}
885
+ `);
886
+ } else {
887
+ output += ` ${number} ${text}
888
+ `;
889
+ }
890
+ });
891
+ const isCustomCursor = cursor === options.length;
892
+ if (isCustomCursor) {
893
+ output += pc4.cyan(`\u276F ${pc4.yellow("\u270E")} ${inputValue || pc4.dim("Type your own...")}`);
894
+ } else {
895
+ output += ` ${pc4.yellow("\u270E")} ${pc4.dim("Type your own...")}`;
896
+ }
897
+ return output;
898
+ });
899
+ var selectOrInput_default = selectOrInput;
900
+
901
+ // src/commands/generate.ts
902
+ function registerGenerateCommand(skillCommand) {
903
+ skillCommand.command("generate").alias("gen").alias("g").option("-o, --output <dir>", "Output directory (default: current directory)").option("--all", "Generate for all detected IDEs").option("--global", "Generate in global skills 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("Generate a skill for a library using AI").action(async (options) => {
904
+ await generateCommand(options);
905
+ });
906
+ }
907
+ async function generateCommand(options) {
908
+ log.blank();
909
+ const tokens = loadTokens();
910
+ if (!tokens) {
911
+ log.error("Authentication required. Please run 'ctx7 login' first.");
912
+ return;
913
+ }
914
+ if (isTokenExpired(tokens)) {
915
+ log.error("Session expired. Please run 'ctx7 login' to refresh.");
916
+ return;
917
+ }
918
+ const accessToken = tokens.access_token;
919
+ const initSpinner = ora().start();
920
+ const quota = await getSkillQuota(accessToken);
921
+ if (quota.error) {
922
+ initSpinner.fail(pc5.red("Failed to initialize"));
923
+ return;
924
+ }
925
+ if (quota.tier !== "unlimited" && quota.remaining < 1) {
926
+ initSpinner.fail(pc5.red("Weekly skill generation limit reached"));
927
+ log.blank();
928
+ console.log(
929
+ ` You've used ${pc5.bold(pc5.white(quota.used.toString()))}/${pc5.bold(pc5.white(quota.limit.toString()))} skill generations this week.`
930
+ );
931
+ console.log(
932
+ ` Your quota resets on ${pc5.yellow(new Date(quota.resetDate).toLocaleDateString())}.`
933
+ );
934
+ log.blank();
935
+ if (quota.tier === "free") {
936
+ console.log(
937
+ ` ${pc5.yellow("Tip:")} Upgrade to Pro for ${pc5.bold("10")} generations per week.`
938
+ );
939
+ console.log(` Visit ${pc5.green("https://context7.com/dashboard")} to upgrade.`);
940
+ }
941
+ return;
942
+ }
943
+ initSpinner.stop();
944
+ initSpinner.clear();
945
+ console.log(pc5.bold("What should your agent become an expert at?\n"));
946
+ console.log(
947
+ pc5.dim("Skills teach agents best practices, design patterns, and domain expertise.\n")
948
+ );
949
+ console.log(pc5.yellow("Examples:"));
950
+ console.log(pc5.dim(' "React component optimization and performance best practices"'));
951
+ console.log(pc5.dim(' "Responsive web design with Tailwind CSS"'));
952
+ console.log(pc5.dim(' "Writing effective landing page copy"'));
953
+ console.log(pc5.dim(' "Deploying Next.js apps to Vercel"'));
954
+ console.log(pc5.dim(' "OAuth authentication with NextAuth.js"\n'));
955
+ let motivation;
956
+ try {
957
+ motivation = await input({
958
+ message: "Describe the expertise:"
959
+ });
960
+ if (!motivation.trim()) {
961
+ log.warn("Expertise description is required");
962
+ return;
963
+ }
964
+ motivation = motivation.trim();
965
+ } catch {
966
+ log.warn("Generation cancelled");
967
+ return;
968
+ }
969
+ const searchSpinner = ora("Finding relevant libraries...").start();
970
+ const searchResult = await searchLibraries(motivation, accessToken);
971
+ if (searchResult.error || !searchResult.results?.length) {
972
+ searchSpinner.fail(pc5.red("No libraries found"));
973
+ log.warn(searchResult.message || "Try a different description");
974
+ return;
975
+ }
976
+ searchSpinner.succeed(pc5.green(`Found ${searchResult.results.length} relevant libraries`));
977
+ log.blank();
978
+ let selectedLibraries;
979
+ try {
980
+ const formatProjectId = (id) => {
981
+ return id.startsWith("/") ? id.slice(1) : id;
982
+ };
983
+ const isGitHubRepo = (id) => {
984
+ const cleanId = id.startsWith("/") ? id.slice(1) : id;
985
+ const parts = cleanId.split("/");
986
+ if (parts.length !== 2) return false;
987
+ const nonGitHubPrefixes = ["websites", "packages", "npm", "docs", "libraries", "llmstxt"];
988
+ return !nonGitHubPrefixes.includes(parts[0].toLowerCase());
989
+ };
990
+ const libraries = searchResult.results.slice(0, 5);
991
+ const indexWidth = libraries.length.toString().length;
992
+ const maxNameLen = Math.max(...libraries.map((lib) => lib.title.length));
993
+ const libraryChoices = libraries.map((lib, index) => {
994
+ const projectId = formatProjectId(lib.id);
995
+ const isGitHub = isGitHubRepo(lib.id);
996
+ const indexStr = pc5.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
997
+ const paddedName = lib.title.padEnd(maxNameLen);
998
+ const libUrl = `https://context7.com${lib.id}`;
999
+ const libLink = terminalLink(lib.title, libUrl, pc5.white);
1000
+ const repoLink = isGitHub ? terminalLink(projectId, `https://github.com/${projectId}`, pc5.white) : pc5.white(projectId);
1001
+ const starsLine = lib.stars && isGitHub ? [`${pc5.yellow("Stars:")} ${lib.stars.toLocaleString()}`] : [];
1002
+ const metadataLines = [
1003
+ pc5.dim("\u2500".repeat(50)),
1004
+ "",
1005
+ `${pc5.yellow("Library:")} ${libLink}`,
1006
+ `${pc5.yellow("Source:")} ${repoLink}`,
1007
+ `${pc5.yellow("Snippets:")} ${lib.totalSnippets.toLocaleString()}`,
1008
+ ...starsLine,
1009
+ `${pc5.yellow("Description:")}`,
1010
+ pc5.white(lib.description || "No description")
1011
+ ];
1012
+ return {
1013
+ name: `${indexStr} ${paddedName} ${pc5.dim(`(${projectId})`)}`,
1014
+ value: lib,
1015
+ description: metadataLines.join("\n")
1016
+ };
1017
+ });
1018
+ selectedLibraries = await checkboxWithHover(
1019
+ {
1020
+ message: "Select libraries:",
1021
+ choices: libraryChoices,
1022
+ pageSize: 10,
1023
+ loop: false
1024
+ },
1025
+ { getName: (lib) => `${lib.title} (${formatProjectId(lib.id)})` }
1026
+ );
1027
+ if (!selectedLibraries || selectedLibraries.length === 0) {
1028
+ log.info("No libraries selected. Try running the command again.");
1029
+ return;
1030
+ }
1031
+ } catch {
1032
+ log.warn("Generation cancelled");
1033
+ return;
1034
+ }
1035
+ log.blank();
1036
+ const questionsSpinner = ora("Preparing questions...").start();
1037
+ const librariesInput = selectedLibraries.map((lib) => ({ id: lib.id, name: lib.title }));
1038
+ const questionsResult = await getSkillQuestions(librariesInput, motivation, accessToken);
1039
+ if (questionsResult.error || !questionsResult.questions?.length) {
1040
+ questionsSpinner.fail(pc5.red("Failed to generate questions"));
1041
+ log.warn(questionsResult.message || "Please try again");
1042
+ return;
1043
+ }
1044
+ questionsSpinner.succeed(pc5.green("Questions prepared"));
1045
+ log.blank();
1046
+ const answers = [];
1047
+ try {
1048
+ for (let i = 0; i < questionsResult.questions.length; i++) {
1049
+ const q = questionsResult.questions[i];
1050
+ const questionNum = i + 1;
1051
+ const totalQuestions = questionsResult.questions.length;
1052
+ const answer = await selectOrInput_default({
1053
+ message: `${pc5.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`,
1054
+ options: q.options,
1055
+ recommendedIndex: q.recommendedIndex
1056
+ });
1057
+ answers.push({
1058
+ question: q.question,
1059
+ answer
1060
+ });
1061
+ const linesToClear = 3 + q.options.length;
1062
+ process.stdout.write(`\x1B[${linesToClear}A\x1B[J`);
1063
+ const truncatedAnswer = answer.length > 50 ? answer.slice(0, 47) + "..." : answer;
1064
+ console.log(`${pc5.green("\u2713")} ${pc5.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`);
1065
+ console.log(` ${pc5.cyan(truncatedAnswer)}`);
1066
+ log.blank();
1067
+ }
1068
+ } catch {
1069
+ log.warn("Generation cancelled");
1070
+ return;
1071
+ }
1072
+ let generatedContent = null;
1073
+ let skillName = "";
1074
+ let feedback;
1075
+ const libraryNames = selectedLibraries.map((lib) => lib.title).join(", ");
1076
+ const queryLog = [];
1077
+ let genSpinner = null;
1078
+ const formatQueryLogText = () => {
1079
+ if (queryLog.length === 0) return "";
1080
+ const lines = [];
1081
+ const latestEntry = queryLog[queryLog.length - 1];
1082
+ lines.push(pc5.dim(`(${queryLog.length} ${queryLog.length === 1 ? "query" : "queries"})`));
1083
+ lines.push("");
1084
+ for (const result of latestEntry.results.slice(0, 3)) {
1085
+ const cleanContent = result.content.replace(/Source:\s*https?:\/\/[^\s]+/gi, "").trim();
1086
+ if (cleanContent) {
1087
+ lines.push(` ${pc5.yellow("\u2022")} ${pc5.white(result.title)}`);
1088
+ const maxLen = 400;
1089
+ const content = cleanContent.length > maxLen ? cleanContent.slice(0, maxLen - 3) + "..." : cleanContent;
1090
+ const words = content.split(" ");
1091
+ let currentLine = " ";
1092
+ for (const word of words) {
1093
+ if (currentLine.length + word.length > 84) {
1094
+ lines.push(pc5.dim(currentLine));
1095
+ currentLine = " " + word + " ";
1096
+ } else {
1097
+ currentLine += word + " ";
1098
+ }
1099
+ }
1100
+ if (currentLine.trim()) {
1101
+ lines.push(pc5.dim(currentLine));
1102
+ }
1103
+ lines.push("");
1104
+ }
1105
+ }
1106
+ return "\n" + lines.join("\n");
1107
+ };
1108
+ let isGeneratingContent = false;
1109
+ const handleStreamEvent = (event) => {
1110
+ if (event.type === "progress") {
1111
+ if (genSpinner) {
1112
+ if (event.message.startsWith("Generating skill content...") && !isGeneratingContent) {
1113
+ isGeneratingContent = true;
1114
+ if (queryLog.length > 0) {
1115
+ genSpinner.succeed(pc5.green(`Queried documentation`));
1116
+ } else {
1117
+ genSpinner.succeed(pc5.green(`Ready to generate`));
1118
+ }
1119
+ genSpinner = ora("Generating skill content...").start();
1120
+ } else if (!isGeneratingContent) {
1121
+ genSpinner.text = event.message + formatQueryLogText();
1122
+ }
1123
+ }
1124
+ } else if (event.type === "tool_result") {
1125
+ queryLog.push({
1126
+ query: event.query,
1127
+ libraryId: event.libraryId,
1128
+ results: event.results
1129
+ });
1130
+ if (genSpinner && !isGeneratingContent) {
1131
+ genSpinner.text = genSpinner.text.split("\n")[0] + formatQueryLogText();
1132
+ }
1133
+ }
1134
+ };
1135
+ while (true) {
1136
+ const generateInput = {
1137
+ motivation,
1138
+ libraries: librariesInput,
1139
+ answers,
1140
+ feedback,
1141
+ previousContent: feedback && generatedContent ? generatedContent : void 0
1142
+ };
1143
+ queryLog.length = 0;
1144
+ isGeneratingContent = false;
1145
+ const initialStatus = feedback ? "Regenerating skill with your feedback..." : `Generating skill for "${libraryNames}"...`;
1146
+ genSpinner = ora(initialStatus).start();
1147
+ const result = await generateSkillStructured(generateInput, handleStreamEvent, accessToken);
1148
+ if (result.error) {
1149
+ genSpinner.fail(pc5.red(`Error: ${result.error}`));
1150
+ return;
1151
+ }
1152
+ if (!result.content) {
1153
+ genSpinner.fail(pc5.red("No content generated"));
1154
+ return;
1155
+ }
1156
+ genSpinner.succeed(pc5.green(`Generated skill for "${result.libraryName}"`));
1157
+ generatedContent = result.content;
1158
+ skillName = result.libraryName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
1159
+ const contentLines = generatedContent.split("\n");
1160
+ const previewLineCount = 20;
1161
+ const hasMoreLines = contentLines.length > previewLineCount;
1162
+ const previewContent = contentLines.slice(0, previewLineCount).join("\n");
1163
+ const remainingLines = contentLines.length - previewLineCount;
1164
+ const showPreview = () => {
1165
+ log.blank();
1166
+ console.log(pc5.dim("\u2501".repeat(70)));
1167
+ console.log(pc5.bold(`Generated Skill: `) + pc5.green(pc5.bold(skillName)));
1168
+ console.log(pc5.dim("\u2501".repeat(70)));
1169
+ log.blank();
1170
+ console.log(previewContent);
1171
+ if (hasMoreLines) {
1172
+ log.blank();
1173
+ console.log(pc5.dim(`... ${remainingLines} more lines`));
1174
+ }
1175
+ log.blank();
1176
+ console.log(pc5.dim("\u2501".repeat(70)));
1177
+ log.blank();
1178
+ };
1179
+ const showFullContent = () => {
1180
+ log.blank();
1181
+ console.log(pc5.dim("\u2501".repeat(70)));
1182
+ console.log(pc5.bold(`Generated Skill: `) + pc5.green(pc5.bold(skillName)));
1183
+ console.log(pc5.dim("\u2501".repeat(70)));
1184
+ log.blank();
1185
+ console.log(generatedContent);
1186
+ log.blank();
1187
+ console.log(pc5.dim("\u2501".repeat(70)));
1188
+ log.blank();
1189
+ };
1190
+ showPreview();
1191
+ await new Promise((resolve) => setTimeout(resolve, 100));
1192
+ try {
1193
+ let action;
1194
+ while (true) {
1195
+ const choices = [
1196
+ { name: `${pc5.green("\u2713")} Install skill`, value: "install" },
1197
+ ...hasMoreLines ? [{ name: `${pc5.blue("\u2922")} View full skill`, value: "expand" }] : [],
1198
+ { name: `${pc5.yellow("\u270E")} Request changes`, value: "feedback" },
1199
+ { name: `${pc5.red("\u2715")} Cancel`, value: "cancel" }
1200
+ ];
1201
+ action = await select2({
1202
+ message: "What would you like to do?",
1203
+ choices
1204
+ });
1205
+ if (action === "expand") {
1206
+ showFullContent();
1207
+ continue;
1208
+ }
1209
+ break;
1210
+ }
1211
+ if (action === "install") {
1212
+ break;
1213
+ } else if (action === "cancel") {
1214
+ log.warn("Generation cancelled");
1215
+ return;
1216
+ } else if (action === "feedback") {
1217
+ feedback = await input({
1218
+ message: "What changes would you like? (press Enter to skip)"
1219
+ });
1220
+ if (!feedback.trim()) {
1221
+ feedback = void 0;
1222
+ }
1223
+ log.blank();
1224
+ }
1225
+ } catch {
1226
+ log.warn("Generation cancelled");
1227
+ return;
1228
+ }
1229
+ }
1230
+ const targets = await promptForInstallTargets(options);
1231
+ if (!targets) {
1232
+ log.warn("Generation cancelled");
1233
+ return;
1234
+ }
1235
+ const targetDirs = getTargetDirs(targets);
1236
+ const writeSpinner = ora("Writing skill files...").start();
1237
+ let permissionError = false;
1238
+ const failedDirs = /* @__PURE__ */ new Set();
1239
+ for (const targetDir of targetDirs) {
1240
+ let finalDir = targetDir;
1241
+ if (options.output && !targetDir.includes("/.config/") && !targetDir.startsWith(homedir3())) {
1242
+ finalDir = targetDir.replace(process.cwd(), options.output);
1243
+ }
1244
+ const skillDir = join4(finalDir, skillName);
1245
+ const skillPath = join4(skillDir, "SKILL.md");
1246
+ try {
1247
+ await mkdir2(skillDir, { recursive: true });
1248
+ await writeFile2(skillPath, generatedContent, "utf-8");
1249
+ } catch (err) {
1250
+ const error = err;
1251
+ if (error.code === "EACCES" || error.code === "EPERM") {
1252
+ permissionError = true;
1253
+ failedDirs.add(skillDir);
1254
+ } else {
1255
+ log.warn(`Failed to write to ${skillPath}: ${error.message}`);
1256
+ }
1257
+ }
1258
+ }
1259
+ if (permissionError) {
1260
+ writeSpinner.fail(pc5.red("Permission denied"));
1261
+ log.blank();
1262
+ console.log(pc5.yellow("Fix permissions with:"));
1263
+ for (const dir of failedDirs) {
1264
+ const parentDir = join4(dir, "..");
1265
+ console.log(pc5.dim(` sudo chown -R $(whoami) "${parentDir}"`));
1266
+ }
1267
+ log.blank();
1268
+ return;
1269
+ }
1270
+ writeSpinner.succeed(pc5.green(`Created skill in ${targetDirs.length} location(s)`));
1271
+ log.blank();
1272
+ console.log(pc5.green(pc5.bold("Skill saved successfully")));
1273
+ for (const targetDir of targetDirs) {
1274
+ console.log(pc5.dim(` ${targetDir}/`) + pc5.green(skillName));
1275
+ }
1276
+ log.blank();
1277
+ }
1278
+
498
1279
  // src/commands/skill.ts
1280
+ import { homedir as homedir4 } from "os";
499
1281
  function logInstallSummary(targets, targetDirs, skillNames) {
500
1282
  log.blank();
501
1283
  let dirIndex = 0;
@@ -512,6 +1294,7 @@ function logInstallSummary(targets, targetDirs, skillNames) {
512
1294
  }
513
1295
  function registerSkillCommands(program2) {
514
1296
  const skill = program2.command("skills").alias("skill").description("Manage AI coding skills");
1297
+ registerGenerateCommand(skill);
515
1298
  skill.command("install").alias("i").alias("add").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 from a repository").action(async (project, skillName, options) => {
516
1299
  await installCommand(project, skillName, options);
517
1300
  });
@@ -536,10 +1319,10 @@ function registerSkillAliases(program2) {
536
1319
  await searchCommand(keywords.join(" "));
537
1320
  });
538
1321
  }
539
- async function installCommand(input, skillName, options) {
540
- const parsed = parseSkillInput(input);
1322
+ async function installCommand(input2, skillName, options) {
1323
+ const parsed = parseSkillInput(input2);
541
1324
  if (!parsed) {
542
- log.error(`Invalid input format: ${input}`);
1325
+ log.error(`Invalid input format: ${input2}`);
543
1326
  log.info(`Expected: /owner/repo or full GitHub URL`);
544
1327
  log.info(`Example: ctx7 skills install /anthropics/skills pdf`);
545
1328
  log.blank();
@@ -547,17 +1330,17 @@ async function installCommand(input, skillName, options) {
547
1330
  }
548
1331
  const repo = `/${parsed.owner}/${parsed.repo}`;
549
1332
  log.blank();
550
- const spinner = ora(`Fetching skills from ${repo}...`).start();
1333
+ const spinner = ora2(`Fetching skills from ${repo}...`).start();
551
1334
  let selectedSkills;
552
1335
  if (skillName) {
553
1336
  spinner.text = `Fetching skill: ${skillName}...`;
554
1337
  const skillData = await getSkill(repo, skillName);
555
1338
  if (skillData.error || !skillData.name) {
556
1339
  if (skillData.error === "prompt_injection_detected") {
557
- spinner.fail(pc4.red(`Prompt injection detected in skill: ${skillName}`));
1340
+ spinner.fail(pc6.red(`Prompt injection detected in skill: ${skillName}`));
558
1341
  log.warn("This skill contains potentially malicious content and cannot be installed.");
559
1342
  } else {
560
- spinner.fail(pc4.red(`Skill not found: ${skillName}`));
1343
+ spinner.fail(pc6.red(`Skill not found: ${skillName}`));
561
1344
  }
562
1345
  return;
563
1346
  }
@@ -573,11 +1356,11 @@ async function installCommand(input, skillName, options) {
573
1356
  } else {
574
1357
  const data = await listProjectSkills(repo);
575
1358
  if (data.error) {
576
- spinner.fail(pc4.red(`Error: ${data.message || data.error}`));
1359
+ spinner.fail(pc6.red(`Error: ${data.message || data.error}`));
577
1360
  return;
578
1361
  }
579
1362
  if (!data.skills || data.skills.length === 0) {
580
- spinner.warn(pc4.yellow(`No skills found in ${repo}`));
1363
+ spinner.warn(pc6.yellow(`No skills found in ${repo}`));
581
1364
  return;
582
1365
  }
583
1366
  const skillsWithRepo = data.skills.map((s) => ({ ...s, project: repo }));
@@ -595,19 +1378,19 @@ async function installCommand(input, skillName, options) {
595
1378
  const indexWidth = data.skills.length.toString().length;
596
1379
  const maxNameLen = Math.max(...data.skills.map((s) => s.name.length));
597
1380
  const choices = skillsWithRepo.map((s, index) => {
598
- const indexStr = pc4.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
1381
+ const indexStr = pc6.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
599
1382
  const paddedName = s.name.padEnd(maxNameLen);
600
1383
  const installs = formatInstallCount(s.installCount);
601
1384
  const skillUrl = `https://context7.com/skills${s.project}/${s.name}`;
602
- const skillLink = terminalLink(s.name, skillUrl, pc4.white);
603
- const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc4.white);
1385
+ const skillLink = terminalLink(s.name, skillUrl, pc6.white);
1386
+ const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc6.white);
604
1387
  const metadataLines = [
605
- pc4.dim("\u2500".repeat(50)),
1388
+ pc6.dim("\u2500".repeat(50)),
606
1389
  "",
607
- `${pc4.yellow("Skill:")} ${skillLink}`,
608
- `${pc4.yellow("Repo:")} ${repoLink}`,
609
- `${pc4.yellow("Description:")}`,
610
- pc4.white(s.description || "No description")
1390
+ `${pc6.yellow("Skill:")} ${skillLink}`,
1391
+ `${pc6.yellow("Repo:")} ${repoLink}`,
1392
+ `${pc6.yellow("Description:")}`,
1393
+ pc6.white(s.description || "No description")
611
1394
  ];
612
1395
  return {
613
1396
  name: installs ? `${indexStr} ${paddedName} ${installs}` : `${indexStr} ${paddedName}`,
@@ -639,7 +1422,7 @@ async function installCommand(input, skillName, options) {
639
1422
  return;
640
1423
  }
641
1424
  const targetDirs = getTargetDirs(targets);
642
- const installSpinner = ora("Installing skills...").start();
1425
+ const installSpinner = ora2("Installing skills...").start();
643
1426
  let permissionError = false;
644
1427
  const failedDirs = /* @__PURE__ */ new Set();
645
1428
  const installedSkills = [];
@@ -663,7 +1446,7 @@ async function installCommand(input, skillName, options) {
663
1446
  }
664
1447
  throw dirErr;
665
1448
  }
666
- const primarySkillDir = join3(primaryDir, skill.name);
1449
+ const primarySkillDir = join5(primaryDir, skill.name);
667
1450
  for (const targetDir of symlinkDirs) {
668
1451
  try {
669
1452
  await symlinkSkill(skill.name, primarySkillDir, targetDir);
@@ -691,7 +1474,7 @@ async function installCommand(input, skillName, options) {
691
1474
  log.blank();
692
1475
  log.warn("Fix permissions with:");
693
1476
  for (const dir of failedDirs) {
694
- const parentDir = join3(dir, "..");
1477
+ const parentDir = join5(dir, "..");
695
1478
  log.dim(` sudo chown -R $(whoami) "${parentDir}"`);
696
1479
  }
697
1480
  log.blank();
@@ -704,42 +1487,42 @@ async function installCommand(input, skillName, options) {
704
1487
  }
705
1488
  async function searchCommand(query) {
706
1489
  log.blank();
707
- const spinner = ora(`Searching for "${query}"...`).start();
1490
+ const spinner = ora2(`Searching for "${query}"...`).start();
708
1491
  let data;
709
1492
  try {
710
1493
  data = await searchSkills(query);
711
1494
  } catch (err) {
712
- spinner.fail(pc4.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
1495
+ spinner.fail(pc6.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
713
1496
  return;
714
1497
  }
715
1498
  if (data.error) {
716
- spinner.fail(pc4.red(`Error: ${data.message || data.error}`));
1499
+ spinner.fail(pc6.red(`Error: ${data.message || data.error}`));
717
1500
  return;
718
1501
  }
719
1502
  if (!data.results || data.results.length === 0) {
720
- spinner.warn(pc4.yellow(`No skills found matching "${query}"`));
1503
+ spinner.warn(pc6.yellow(`No skills found matching "${query}"`));
721
1504
  return;
722
1505
  }
723
1506
  spinner.succeed(`Found ${data.results.length} skill(s)`);
724
1507
  const indexWidth = data.results.length.toString().length;
725
1508
  const maxNameLen = Math.max(...data.results.map((s) => s.name.length));
726
1509
  const choices = data.results.map((s, index) => {
727
- const indexStr = pc4.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
1510
+ const indexStr = pc6.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
728
1511
  const paddedName = s.name.padEnd(maxNameLen);
729
1512
  const installs = formatInstallCount(s.installCount);
730
1513
  const skillLink = terminalLink(
731
1514
  s.name,
732
1515
  `https://context7.com/skills${s.project}/${s.name}`,
733
- pc4.white
1516
+ pc6.white
734
1517
  );
735
- const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc4.white);
1518
+ const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc6.white);
736
1519
  const metadataLines = [
737
- pc4.dim("\u2500".repeat(50)),
1520
+ pc6.dim("\u2500".repeat(50)),
738
1521
  "",
739
- `${pc4.yellow("Skill:")} ${skillLink}`,
740
- `${pc4.yellow("Repo:")} ${repoLink}`,
741
- `${pc4.yellow("Description:")}`,
742
- pc4.white(s.description || "No description")
1522
+ `${pc6.yellow("Skill:")} ${skillLink}`,
1523
+ `${pc6.yellow("Repo:")} ${repoLink}`,
1524
+ `${pc6.yellow("Description:")}`,
1525
+ pc6.white(s.description || "No description")
743
1526
  ];
744
1527
  return {
745
1528
  name: installs ? `${indexStr} ${paddedName} ${installs}` : `${indexStr} ${paddedName}`,
@@ -771,7 +1554,7 @@ async function searchCommand(query) {
771
1554
  return;
772
1555
  }
773
1556
  const targetDirs = getTargetDirs(targets);
774
- const installSpinner = ora("Installing skills...").start();
1557
+ const installSpinner = ora2("Installing skills...").start();
775
1558
  let permissionError = false;
776
1559
  const failedDirs = /* @__PURE__ */ new Set();
777
1560
  const installedSkills = [];
@@ -795,7 +1578,7 @@ async function searchCommand(query) {
795
1578
  }
796
1579
  throw dirErr;
797
1580
  }
798
- const primarySkillDir = join3(primaryDir, skill.name);
1581
+ const primarySkillDir = join5(primaryDir, skill.name);
799
1582
  for (const targetDir of symlinkDirs) {
800
1583
  try {
801
1584
  await symlinkSkill(skill.name, primarySkillDir, targetDir);
@@ -823,7 +1606,7 @@ async function searchCommand(query) {
823
1606
  log.blank();
824
1607
  log.warn("Fix permissions with:");
825
1608
  for (const dir of failedDirs) {
826
- const parentDir = join3(dir, "..");
1609
+ const parentDir = join5(dir, "..");
827
1610
  log.dim(` sudo chown -R $(whoami) "${parentDir}"`);
828
1611
  }
829
1612
  log.blank();
@@ -835,28 +1618,35 @@ async function searchCommand(query) {
835
1618
  logInstallSummary(targets, targetDirs, installedNames);
836
1619
  }
837
1620
  async function listCommand(options) {
838
- const target = await promptForSingleTarget(options);
839
- if (!target) {
840
- log.warn("Cancelled");
1621
+ const scope = options.global ? "global" : "project";
1622
+ const pathMap = scope === "global" ? IDE_GLOBAL_PATHS : IDE_PATHS;
1623
+ const baseDir = scope === "global" ? homedir4() : process.cwd();
1624
+ const idesToCheck = hasExplicitIdeOption(options) ? getSelectedIdes(options) : Object.keys(IDE_NAMES);
1625
+ const results = [];
1626
+ for (const ide of idesToCheck) {
1627
+ const skillsDir = join5(baseDir, pathMap[ide]);
1628
+ try {
1629
+ const entries = await readdir(skillsDir, { withFileTypes: true });
1630
+ const skillFolders = entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);
1631
+ if (skillFolders.length > 0) {
1632
+ results.push({ ide, skills: skillFolders });
1633
+ }
1634
+ } catch {
1635
+ }
1636
+ }
1637
+ if (results.length === 0) {
1638
+ log.warn("No skills installed");
841
1639
  return;
842
1640
  }
843
- const skillsDir = getTargetDirFromSelection(target.ide, target.scope);
844
- try {
845
- const entries = await readdir(skillsDir, { withFileTypes: true });
846
- const skillFolders = entries.filter((e) => e.isDirectory() || e.isSymbolicLink());
847
- if (skillFolders.length === 0) {
848
- log.warn(`No skills installed in ${skillsDir}`);
849
- return;
850
- }
851
- log.info(`
852
- \u25C6 Installed skills (${skillsDir}):`);
853
- for (const folder of skillFolders) {
854
- log.item(folder.name);
1641
+ log.blank();
1642
+ for (const { ide, skills } of results) {
1643
+ const ideName = IDE_NAMES[ide];
1644
+ const path2 = pathMap[ide];
1645
+ log.plain(`${pc6.bold(ideName)} ${pc6.dim(path2)}`);
1646
+ for (const skill of skills) {
1647
+ log.plain(` ${pc6.green(skill)}`);
855
1648
  }
856
- log.success(`${skillFolders.length} skill(s) installed
857
- `);
858
- } catch {
859
- log.warn(`No skills directory found at ${skillsDir}`);
1649
+ log.blank();
860
1650
  }
861
1651
  }
862
1652
  async function removeCommand(name, options) {
@@ -866,7 +1656,7 @@ async function removeCommand(name, options) {
866
1656
  return;
867
1657
  }
868
1658
  const skillsDir = getTargetDirFromSelection(target.ide, target.scope);
869
- const skillPath = join3(skillsDir, name);
1659
+ const skillPath = join5(skillsDir, name);
870
1660
  try {
871
1661
  await rm2(skillPath, { recursive: true });
872
1662
  log.success(`Removed skill: ${name}`);
@@ -881,25 +1671,25 @@ async function removeCommand(name, options) {
881
1671
  }
882
1672
  }
883
1673
  }
884
- async function infoCommand(input) {
885
- const parsed = parseSkillInput(input);
1674
+ async function infoCommand(input2) {
1675
+ const parsed = parseSkillInput(input2);
886
1676
  if (!parsed) {
887
1677
  log.blank();
888
- log.error(`Invalid input format: ${input}`);
1678
+ log.error(`Invalid input format: ${input2}`);
889
1679
  log.info(`Expected: /owner/repo or full GitHub URL`);
890
1680
  log.blank();
891
1681
  return;
892
1682
  }
893
1683
  const repo = `/${parsed.owner}/${parsed.repo}`;
894
1684
  log.blank();
895
- const spinner = ora(`Fetching skills from ${repo}...`).start();
1685
+ const spinner = ora2(`Fetching skills from ${repo}...`).start();
896
1686
  const data = await listProjectSkills(repo);
897
1687
  if (data.error) {
898
- spinner.fail(pc4.red(`Error: ${data.message || data.error}`));
1688
+ spinner.fail(pc6.red(`Error: ${data.message || data.error}`));
899
1689
  return;
900
1690
  }
901
1691
  if (!data.skills || data.skills.length === 0) {
902
- spinner.warn(pc4.yellow(`No skills found in ${repo}`));
1692
+ spinner.warn(pc6.yellow(`No skills found in ${repo}`));
903
1693
  return;
904
1694
  }
905
1695
  spinner.succeed(`Found ${data.skills.length} skill(s)`);
@@ -911,32 +1701,165 @@ async function infoCommand(input) {
911
1701
  log.blank();
912
1702
  }
913
1703
  log.plain(
914
- `${pc4.bold("Quick commands:")}
915
- Install all: ${pc4.cyan(`ctx7 skills install ${repo} --all`)}
916
- Install one: ${pc4.cyan(`ctx7 skills install ${repo} ${data.skills[0]?.name}`)}
1704
+ `${pc6.bold("Quick commands:")}
1705
+ Install all: ${pc6.cyan(`ctx7 skills install ${repo} --all`)}
1706
+ Install one: ${pc6.cyan(`ctx7 skills install ${repo} ${data.skills[0]?.name}`)}
917
1707
  `
918
1708
  );
919
1709
  }
920
1710
 
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
+
921
1843
  // src/constants.ts
922
- import { readFileSync } from "fs";
1844
+ import { readFileSync as readFileSync2 } from "fs";
923
1845
  import { fileURLToPath } from "url";
924
- import { dirname as dirname2, join as join4 } from "path";
1846
+ import { dirname as dirname2, join as join6 } from "path";
925
1847
  var __dirname = dirname2(fileURLToPath(import.meta.url));
926
- var pkg = JSON.parse(readFileSync(join4(__dirname, "../package.json"), "utf-8"));
1848
+ var pkg = JSON.parse(readFileSync2(join6(__dirname, "../package.json"), "utf-8"));
927
1849
  var VERSION = pkg.version;
928
1850
  var NAME = pkg.name;
929
1851
 
930
1852
  // src/index.ts
931
1853
  var brand = {
932
- primary: pc5.green,
933
- dim: pc5.dim
1854
+ primary: pc8.green,
1855
+ dim: pc8.dim
934
1856
  };
935
1857
  var program = new Command();
936
1858
  program.name("ctx7").description("Context7 CLI - Manage AI coding skills and documentation context").version(VERSION).option("--base-url <url>").hook("preAction", (thisCommand) => {
937
1859
  const opts = thisCommand.opts();
938
1860
  if (opts.baseUrl) {
939
1861
  setBaseUrl(opts.baseUrl);
1862
+ setAuthBaseUrl(opts.baseUrl);
940
1863
  }
941
1864
  }).addHelpText(
942
1865
  "after",
@@ -963,6 +1886,7 @@ Visit ${brand.primary("https://context7.com")} to browse skills
963
1886
  );
964
1887
  registerSkillCommands(program);
965
1888
  registerSkillAliases(program);
1889
+ registerAuthCommands(program);
966
1890
  program.action(() => {
967
1891
  console.log("");
968
1892
  const banner = figlet.textSync("Context7", { font: "ANSI Shadow" });