ctx7 0.1.5 → 0.2.1
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/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/index.js +1046 -113
- package/dist/index.js.map +1 -1
- package/package.json +13 -12
package/dist/index.js
CHANGED
|
@@ -2,25 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
// src/index.ts
|
|
4
4
|
import { Command } from "commander";
|
|
5
|
-
import
|
|
5
|
+
import pc8 from "picocolors";
|
|
6
6
|
import figlet from "figlet";
|
|
7
7
|
|
|
8
8
|
// src/commands/skill.ts
|
|
9
|
-
import
|
|
10
|
-
import
|
|
9
|
+
import pc6 from "picocolors";
|
|
10
|
+
import ora2 from "ora";
|
|
11
11
|
import { readdir, rm as rm2 } from "fs/promises";
|
|
12
|
-
import { join as
|
|
12
|
+
import { join as join5 } from "path";
|
|
13
13
|
|
|
14
14
|
// src/utils/parse-input.ts
|
|
15
|
-
function parseSkillInput(
|
|
16
|
-
const urlMatch =
|
|
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,
|
|
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 =
|
|
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
|
|
49
|
-
return { owner, repo, branch: branch2, path:
|
|
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
|
|
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
|
|
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 {
|
|
@@ -117,6 +117,9 @@ async function downloadSkillFromGitHub(skill) {
|
|
|
117
117
|
|
|
118
118
|
// src/utils/api.ts
|
|
119
119
|
var baseUrl = "https://context7.com";
|
|
120
|
+
function getBaseUrl() {
|
|
121
|
+
return baseUrl;
|
|
122
|
+
}
|
|
120
123
|
function setBaseUrl(url) {
|
|
121
124
|
baseUrl = url;
|
|
122
125
|
}
|
|
@@ -135,15 +138,6 @@ async function searchSkills(query) {
|
|
|
135
138
|
const response = await fetch(`${baseUrl}/api/v2/skills?${params}`);
|
|
136
139
|
return await response.json();
|
|
137
140
|
}
|
|
138
|
-
function trackInstalls(skills, ides) {
|
|
139
|
-
if (process.env.CTX7_TELEMETRY_DISABLED || !skills.length) return;
|
|
140
|
-
fetch(`${baseUrl}/api/v2/skills/track`, {
|
|
141
|
-
method: "POST",
|
|
142
|
-
headers: { "Content-Type": "application/json" },
|
|
143
|
-
body: JSON.stringify({ skills, ides })
|
|
144
|
-
}).catch(() => {
|
|
145
|
-
});
|
|
146
|
-
}
|
|
147
141
|
async function downloadSkill(project, skillName) {
|
|
148
142
|
const skillData = await getSkill(project, skillName);
|
|
149
143
|
if (skillData.error) {
|
|
@@ -165,6 +159,124 @@ async function downloadSkill(project, skillName) {
|
|
|
165
159
|
}
|
|
166
160
|
return { skill, files };
|
|
167
161
|
}
|
|
162
|
+
async function searchLibraries(query, accessToken) {
|
|
163
|
+
const params = new URLSearchParams({ query });
|
|
164
|
+
const headers = {};
|
|
165
|
+
if (accessToken) {
|
|
166
|
+
headers["Authorization"] = `Bearer ${accessToken}`;
|
|
167
|
+
}
|
|
168
|
+
const response = await fetch(`${baseUrl}/api/v2/libs/search?${params}`, { headers });
|
|
169
|
+
return await response.json();
|
|
170
|
+
}
|
|
171
|
+
async function getSkillQuota(accessToken) {
|
|
172
|
+
const response = await fetch(`${baseUrl}/api/v2/skills/quota`, {
|
|
173
|
+
headers: { Authorization: `Bearer ${accessToken}` }
|
|
174
|
+
});
|
|
175
|
+
if (!response.ok) {
|
|
176
|
+
const errorData = await response.json().catch(() => ({}));
|
|
177
|
+
return {
|
|
178
|
+
used: 0,
|
|
179
|
+
limit: 0,
|
|
180
|
+
remaining: 0,
|
|
181
|
+
tier: "free",
|
|
182
|
+
resetDate: null,
|
|
183
|
+
error: errorData.message || `HTTP error ${response.status}`
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return await response.json();
|
|
187
|
+
}
|
|
188
|
+
async function getSkillQuestions(libraries, motivation, accessToken) {
|
|
189
|
+
const headers = { "Content-Type": "application/json" };
|
|
190
|
+
if (accessToken) {
|
|
191
|
+
headers["Authorization"] = `Bearer ${accessToken}`;
|
|
192
|
+
}
|
|
193
|
+
const response = await fetch(`${baseUrl}/api/v2/skills/questions`, {
|
|
194
|
+
method: "POST",
|
|
195
|
+
headers,
|
|
196
|
+
body: JSON.stringify({ libraries, motivation })
|
|
197
|
+
});
|
|
198
|
+
if (!response.ok) {
|
|
199
|
+
const errorData = await response.json().catch(() => ({}));
|
|
200
|
+
return {
|
|
201
|
+
questions: [],
|
|
202
|
+
error: errorData.message || `HTTP error ${response.status}`
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
return await response.json();
|
|
206
|
+
}
|
|
207
|
+
async function generateSkillStructured(input2, onEvent, accessToken) {
|
|
208
|
+
const headers = { "Content-Type": "application/json" };
|
|
209
|
+
if (accessToken) {
|
|
210
|
+
headers["Authorization"] = `Bearer ${accessToken}`;
|
|
211
|
+
}
|
|
212
|
+
const response = await fetch(`${baseUrl}/api/v2/skills/generate`, {
|
|
213
|
+
method: "POST",
|
|
214
|
+
headers,
|
|
215
|
+
body: JSON.stringify(input2)
|
|
216
|
+
});
|
|
217
|
+
const libraryName = input2.libraries[0]?.name || "skill";
|
|
218
|
+
return handleGenerateResponse(response, libraryName, onEvent);
|
|
219
|
+
}
|
|
220
|
+
async function handleGenerateResponse(response, libraryName, onEvent) {
|
|
221
|
+
if (!response.ok) {
|
|
222
|
+
const errorData = await response.json().catch(() => ({}));
|
|
223
|
+
return {
|
|
224
|
+
content: "",
|
|
225
|
+
libraryName,
|
|
226
|
+
error: errorData.message || `HTTP error ${response.status}`
|
|
227
|
+
};
|
|
228
|
+
}
|
|
229
|
+
const reader = response.body?.getReader();
|
|
230
|
+
if (!reader) {
|
|
231
|
+
return { content: "", libraryName, error: "No response body" };
|
|
232
|
+
}
|
|
233
|
+
const decoder = new TextDecoder();
|
|
234
|
+
let content = "";
|
|
235
|
+
let finalLibraryName = libraryName;
|
|
236
|
+
let error;
|
|
237
|
+
let buffer = "";
|
|
238
|
+
while (true) {
|
|
239
|
+
const { done, value } = await reader.read();
|
|
240
|
+
if (done) break;
|
|
241
|
+
const chunk = decoder.decode(value, { stream: true });
|
|
242
|
+
buffer += chunk;
|
|
243
|
+
const lines = buffer.split("\n");
|
|
244
|
+
buffer = lines.pop() || "";
|
|
245
|
+
for (const line of lines) {
|
|
246
|
+
const trimmedLine = line.trim();
|
|
247
|
+
if (!trimmedLine) continue;
|
|
248
|
+
try {
|
|
249
|
+
const data = JSON.parse(trimmedLine);
|
|
250
|
+
if (onEvent) {
|
|
251
|
+
onEvent(data);
|
|
252
|
+
}
|
|
253
|
+
if (data.type === "complete") {
|
|
254
|
+
content = data.content || "";
|
|
255
|
+
finalLibraryName = data.libraryName || libraryName;
|
|
256
|
+
} else if (data.type === "error") {
|
|
257
|
+
error = data.message;
|
|
258
|
+
}
|
|
259
|
+
} catch {
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (buffer.trim()) {
|
|
264
|
+
try {
|
|
265
|
+
const data = JSON.parse(buffer.trim());
|
|
266
|
+
if (onEvent) {
|
|
267
|
+
onEvent(data);
|
|
268
|
+
}
|
|
269
|
+
if (data.type === "complete") {
|
|
270
|
+
content = data.content || "";
|
|
271
|
+
finalLibraryName = data.libraryName || libraryName;
|
|
272
|
+
} else if (data.type === "error") {
|
|
273
|
+
error = data.message;
|
|
274
|
+
}
|
|
275
|
+
} catch {
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return { content, libraryName: finalLibraryName, error };
|
|
279
|
+
}
|
|
168
280
|
|
|
169
281
|
// src/utils/logger.ts
|
|
170
282
|
import pc from "picocolors";
|
|
@@ -413,27 +525,16 @@ function terminalLink(text, url, color) {
|
|
|
413
525
|
}
|
|
414
526
|
function formatInstallCount(count) {
|
|
415
527
|
if (count === void 0 || count === 0) return "";
|
|
416
|
-
|
|
417
|
-
if (count >= 1e3) {
|
|
418
|
-
display = `${Math.floor(count / 1e3)}k+`;
|
|
419
|
-
} else if (count >= 100) {
|
|
420
|
-
const hundreds = Math.floor(count / 100) * 100;
|
|
421
|
-
display = `${hundreds}+`;
|
|
422
|
-
} else if (count >= 10) {
|
|
423
|
-
const tens = Math.floor(count / 10) * 10;
|
|
424
|
-
display = `${tens}+`;
|
|
425
|
-
} else {
|
|
426
|
-
display = String(count);
|
|
427
|
-
}
|
|
428
|
-
return `\x1B[38;5;214m\u2193${display}\x1B[0m`;
|
|
528
|
+
return pc3.yellow(String(count));
|
|
429
529
|
}
|
|
430
|
-
async function checkboxWithHover(config) {
|
|
530
|
+
async function checkboxWithHover(config, options) {
|
|
431
531
|
const choices = config.choices.filter(
|
|
432
532
|
(c) => typeof c === "object" && c !== null && !("type" in c && c.type === "separator")
|
|
433
533
|
);
|
|
434
534
|
const values = choices.map((c) => c.value);
|
|
435
535
|
const totalItems = values.length;
|
|
436
536
|
let cursorPosition = 0;
|
|
537
|
+
const getName = options?.getName ?? ((v) => v.name);
|
|
437
538
|
const keypressHandler = (_str, key) => {
|
|
438
539
|
if (key.name === "up" && cursorPosition > 0) {
|
|
439
540
|
cursorPosition--;
|
|
@@ -452,9 +553,9 @@ async function checkboxWithHover(config) {
|
|
|
452
553
|
highlight: (text) => pc3.green(text),
|
|
453
554
|
renderSelectedChoices: (selected, _allChoices) => {
|
|
454
555
|
if (selected.length === 0) {
|
|
455
|
-
return pc3.dim(values[cursorPosition]
|
|
556
|
+
return pc3.dim(getName(values[cursorPosition]));
|
|
456
557
|
}
|
|
457
|
-
return selected.map((c) => c.value
|
|
558
|
+
return selected.map((c) => getName(c.value)).join(", ");
|
|
458
559
|
}
|
|
459
560
|
}
|
|
460
561
|
}
|
|
@@ -495,7 +596,684 @@ async function symlinkSkill(skillName, sourcePath, targetDir) {
|
|
|
495
596
|
await symlink(sourcePath, targetPath);
|
|
496
597
|
}
|
|
497
598
|
|
|
599
|
+
// src/utils/tracking.ts
|
|
600
|
+
function trackEvent(event, data) {
|
|
601
|
+
if (process.env.CTX7_TELEMETRY_DISABLED) return;
|
|
602
|
+
fetch(`${getBaseUrl()}/api/v2/cli/events`, {
|
|
603
|
+
method: "POST",
|
|
604
|
+
headers: { "Content-Type": "application/json" },
|
|
605
|
+
body: JSON.stringify({ event, data })
|
|
606
|
+
}).catch(() => {
|
|
607
|
+
});
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
// src/commands/generate.ts
|
|
611
|
+
import pc5 from "picocolors";
|
|
612
|
+
import ora from "ora";
|
|
613
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "fs/promises";
|
|
614
|
+
import { join as join4 } from "path";
|
|
615
|
+
import { homedir as homedir3 } from "os";
|
|
616
|
+
import { input, select as select2 } from "@inquirer/prompts";
|
|
617
|
+
|
|
618
|
+
// src/utils/auth.ts
|
|
619
|
+
import * as crypto from "crypto";
|
|
620
|
+
import * as http from "http";
|
|
621
|
+
import * as fs from "fs";
|
|
622
|
+
import * as path from "path";
|
|
623
|
+
import * as os from "os";
|
|
624
|
+
var CONFIG_DIR = path.join(os.homedir(), ".context7");
|
|
625
|
+
var CREDENTIALS_FILE = path.join(CONFIG_DIR, "credentials.json");
|
|
626
|
+
function generatePKCE() {
|
|
627
|
+
const codeVerifier = crypto.randomBytes(32).toString("base64url");
|
|
628
|
+
const codeChallenge = crypto.createHash("sha256").update(codeVerifier).digest("base64url");
|
|
629
|
+
return { codeVerifier, codeChallenge };
|
|
630
|
+
}
|
|
631
|
+
function generateState() {
|
|
632
|
+
return crypto.randomBytes(16).toString("base64url");
|
|
633
|
+
}
|
|
634
|
+
function ensureConfigDir() {
|
|
635
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
636
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true, mode: 448 });
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
function saveTokens(tokens) {
|
|
640
|
+
ensureConfigDir();
|
|
641
|
+
const data = {
|
|
642
|
+
...tokens,
|
|
643
|
+
expires_at: tokens.expires_at ?? (tokens.expires_in ? Date.now() + tokens.expires_in * 1e3 : void 0)
|
|
644
|
+
};
|
|
645
|
+
fs.writeFileSync(CREDENTIALS_FILE, JSON.stringify(data, null, 2), { mode: 384 });
|
|
646
|
+
}
|
|
647
|
+
function loadTokens() {
|
|
648
|
+
if (!fs.existsSync(CREDENTIALS_FILE)) {
|
|
649
|
+
return null;
|
|
650
|
+
}
|
|
651
|
+
try {
|
|
652
|
+
const data = JSON.parse(fs.readFileSync(CREDENTIALS_FILE, "utf-8"));
|
|
653
|
+
return data;
|
|
654
|
+
} catch {
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
function clearTokens() {
|
|
659
|
+
if (fs.existsSync(CREDENTIALS_FILE)) {
|
|
660
|
+
fs.unlinkSync(CREDENTIALS_FILE);
|
|
661
|
+
return true;
|
|
662
|
+
}
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
function isTokenExpired(tokens) {
|
|
666
|
+
if (!tokens.expires_at) {
|
|
667
|
+
return false;
|
|
668
|
+
}
|
|
669
|
+
return Date.now() > tokens.expires_at - 6e4;
|
|
670
|
+
}
|
|
671
|
+
var CALLBACK_PORT = 52417;
|
|
672
|
+
function createCallbackServer(expectedState) {
|
|
673
|
+
let resolvePort;
|
|
674
|
+
let resolveResult;
|
|
675
|
+
let rejectResult;
|
|
676
|
+
let serverInstance = null;
|
|
677
|
+
const portPromise = new Promise((resolve) => {
|
|
678
|
+
resolvePort = resolve;
|
|
679
|
+
});
|
|
680
|
+
const resultPromise = new Promise((resolve, reject) => {
|
|
681
|
+
resolveResult = resolve;
|
|
682
|
+
rejectResult = reject;
|
|
683
|
+
});
|
|
684
|
+
const server = http.createServer((req, res) => {
|
|
685
|
+
const url = new URL(req.url || "/", `http://localhost`);
|
|
686
|
+
if (url.pathname === "/callback") {
|
|
687
|
+
const code = url.searchParams.get("code");
|
|
688
|
+
const state = url.searchParams.get("state");
|
|
689
|
+
const error = url.searchParams.get("error");
|
|
690
|
+
const errorDescription = url.searchParams.get("error_description");
|
|
691
|
+
res.writeHead(200, { "Content-Type": "text/html" });
|
|
692
|
+
if (error) {
|
|
693
|
+
res.end(errorPage(errorDescription || error));
|
|
694
|
+
serverInstance?.close();
|
|
695
|
+
rejectResult(new Error(errorDescription || error));
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
if (!code || !state) {
|
|
699
|
+
res.end(errorPage("Missing authorization code or state"));
|
|
700
|
+
serverInstance?.close();
|
|
701
|
+
rejectResult(new Error("Missing authorization code or state"));
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
if (state !== expectedState) {
|
|
705
|
+
res.end(errorPage("State mismatch - possible CSRF attack"));
|
|
706
|
+
serverInstance?.close();
|
|
707
|
+
rejectResult(new Error("State mismatch"));
|
|
708
|
+
return;
|
|
709
|
+
}
|
|
710
|
+
res.end(successPage());
|
|
711
|
+
serverInstance?.close();
|
|
712
|
+
resolveResult({ code, state });
|
|
713
|
+
} else {
|
|
714
|
+
res.writeHead(404);
|
|
715
|
+
res.end("Not found");
|
|
716
|
+
}
|
|
717
|
+
});
|
|
718
|
+
serverInstance = server;
|
|
719
|
+
server.on("error", (err) => {
|
|
720
|
+
rejectResult(err);
|
|
721
|
+
});
|
|
722
|
+
server.listen(CALLBACK_PORT, "127.0.0.1", () => {
|
|
723
|
+
resolvePort(CALLBACK_PORT);
|
|
724
|
+
});
|
|
725
|
+
const timeout = setTimeout(
|
|
726
|
+
() => {
|
|
727
|
+
server.close();
|
|
728
|
+
rejectResult(new Error("Login timed out after 5 minutes"));
|
|
729
|
+
},
|
|
730
|
+
5 * 60 * 1e3
|
|
731
|
+
);
|
|
732
|
+
return {
|
|
733
|
+
port: portPromise,
|
|
734
|
+
result: resultPromise,
|
|
735
|
+
close: () => {
|
|
736
|
+
clearTimeout(timeout);
|
|
737
|
+
server.close();
|
|
738
|
+
}
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
function successPage() {
|
|
742
|
+
return `<!DOCTYPE html>
|
|
743
|
+
<html>
|
|
744
|
+
<head><title>Login Successful</title></head>
|
|
745
|
+
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f9fafb;">
|
|
746
|
+
<div style="text-align: center; padding: 2rem;">
|
|
747
|
+
<div style="width: 64px; height: 64px; background: #16a34a; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem;">
|
|
748
|
+
<svg width="32" height="32" fill="none" stroke="white" stroke-width="3" viewBox="0 0 24 24">
|
|
749
|
+
<path d="M5 13l4 4L19 7" stroke-linecap="round" stroke-linejoin="round"/>
|
|
750
|
+
</svg>
|
|
751
|
+
</div>
|
|
752
|
+
<h1 style="color: #16a34a; margin: 0 0 0.5rem;">Login Successful!</h1>
|
|
753
|
+
<p style="color: #6b7280; margin: 0;">You can close this window and return to the terminal.</p>
|
|
754
|
+
</div>
|
|
755
|
+
</body>
|
|
756
|
+
</html>`;
|
|
757
|
+
}
|
|
758
|
+
function escapeHtml(text) {
|
|
759
|
+
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
760
|
+
}
|
|
761
|
+
function errorPage(message) {
|
|
762
|
+
const safeMessage = escapeHtml(message);
|
|
763
|
+
return `<!DOCTYPE html>
|
|
764
|
+
<html>
|
|
765
|
+
<head><title>Login Failed</title></head>
|
|
766
|
+
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f9fafb;">
|
|
767
|
+
<div style="text-align: center; padding: 2rem;">
|
|
768
|
+
<div style="width: 64px; height: 64px; background: #dc2626; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem;">
|
|
769
|
+
<svg width="32" height="32" fill="none" stroke="white" stroke-width="3" viewBox="0 0 24 24">
|
|
770
|
+
<path d="M6 18L18 6M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/>
|
|
771
|
+
</svg>
|
|
772
|
+
</div>
|
|
773
|
+
<h1 style="color: #dc2626; margin: 0 0 0.5rem;">Login Failed</h1>
|
|
774
|
+
<p style="color: #6b7280; margin: 0;">${safeMessage}</p>
|
|
775
|
+
<p style="color: #9ca3af; margin: 1rem 0 0; font-size: 0.875rem;">You can close this window.</p>
|
|
776
|
+
</div>
|
|
777
|
+
</body>
|
|
778
|
+
</html>`;
|
|
779
|
+
}
|
|
780
|
+
async function exchangeCodeForTokens(baseUrl3, code, codeVerifier, redirectUri, clientId) {
|
|
781
|
+
const response = await fetch(`${baseUrl3}/api/oauth/token`, {
|
|
782
|
+
method: "POST",
|
|
783
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
784
|
+
body: new URLSearchParams({
|
|
785
|
+
grant_type: "authorization_code",
|
|
786
|
+
client_id: clientId,
|
|
787
|
+
code,
|
|
788
|
+
code_verifier: codeVerifier,
|
|
789
|
+
redirect_uri: redirectUri
|
|
790
|
+
}).toString()
|
|
791
|
+
});
|
|
792
|
+
if (!response.ok) {
|
|
793
|
+
const err = await response.json().catch(() => ({}));
|
|
794
|
+
throw new Error(err.error_description || err.error || "Failed to exchange code for tokens");
|
|
795
|
+
}
|
|
796
|
+
return await response.json();
|
|
797
|
+
}
|
|
798
|
+
function buildAuthorizationUrl(baseUrl3, clientId, redirectUri, codeChallenge, state) {
|
|
799
|
+
const url = new URL(`${baseUrl3}/api/oauth/authorize`);
|
|
800
|
+
url.searchParams.set("client_id", clientId);
|
|
801
|
+
url.searchParams.set("redirect_uri", redirectUri);
|
|
802
|
+
url.searchParams.set("code_challenge", codeChallenge);
|
|
803
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
804
|
+
url.searchParams.set("state", state);
|
|
805
|
+
url.searchParams.set("scope", "profile email");
|
|
806
|
+
url.searchParams.set("response_type", "code");
|
|
807
|
+
return url.toString();
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
// src/utils/selectOrInput.ts
|
|
811
|
+
import {
|
|
812
|
+
createPrompt,
|
|
813
|
+
useState,
|
|
814
|
+
useKeypress,
|
|
815
|
+
usePrefix,
|
|
816
|
+
isEnterKey,
|
|
817
|
+
isUpKey,
|
|
818
|
+
isDownKey
|
|
819
|
+
} from "@inquirer/core";
|
|
820
|
+
import pc4 from "picocolors";
|
|
821
|
+
var selectOrInput = createPrompt((config, done) => {
|
|
822
|
+
const { message, options, recommendedIndex = 0 } = config;
|
|
823
|
+
const [cursor, setCursor] = useState(recommendedIndex);
|
|
824
|
+
const [inputValue, setInputValue] = useState("");
|
|
825
|
+
const prefix = usePrefix({});
|
|
826
|
+
useKeypress((key, rl) => {
|
|
827
|
+
if (isUpKey(key)) {
|
|
828
|
+
setCursor(Math.max(0, cursor - 1));
|
|
829
|
+
return;
|
|
830
|
+
}
|
|
831
|
+
if (isDownKey(key)) {
|
|
832
|
+
setCursor(Math.min(options.length, cursor + 1));
|
|
833
|
+
return;
|
|
834
|
+
}
|
|
835
|
+
if (isEnterKey(key)) {
|
|
836
|
+
if (cursor === options.length) {
|
|
837
|
+
const finalValue = inputValue.trim();
|
|
838
|
+
done(finalValue || options[recommendedIndex]);
|
|
839
|
+
} else {
|
|
840
|
+
done(options[cursor]);
|
|
841
|
+
}
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
if (cursor === options.length && key.name !== "return") {
|
|
845
|
+
if (key.name === "w" && key.ctrl || key.name === "backspace") {
|
|
846
|
+
if (key.name === "w" && key.ctrl) {
|
|
847
|
+
const words = inputValue.trimEnd().split(/\s+/);
|
|
848
|
+
if (words.length > 0) {
|
|
849
|
+
words.pop();
|
|
850
|
+
setInputValue(
|
|
851
|
+
words.join(" ") + (inputValue.endsWith(" ") && words.length > 0 ? " " : "")
|
|
852
|
+
);
|
|
853
|
+
}
|
|
854
|
+
} else {
|
|
855
|
+
setInputValue(inputValue.slice(0, -1));
|
|
856
|
+
}
|
|
857
|
+
} else if (key.name === "u" && key.ctrl) {
|
|
858
|
+
setInputValue("");
|
|
859
|
+
} else if (key.name === "space") {
|
|
860
|
+
setInputValue(inputValue + " ");
|
|
861
|
+
} else if (key.name && key.name.length === 1 && !key.ctrl) {
|
|
862
|
+
setInputValue(inputValue + key.name);
|
|
863
|
+
}
|
|
864
|
+
} else if (rl.line) {
|
|
865
|
+
rl.line = "";
|
|
866
|
+
}
|
|
867
|
+
});
|
|
868
|
+
let output = `${prefix} ${pc4.bold(message)}
|
|
869
|
+
|
|
870
|
+
`;
|
|
871
|
+
options.forEach((opt, idx) => {
|
|
872
|
+
const isRecommended = idx === recommendedIndex;
|
|
873
|
+
const isCursor = idx === cursor;
|
|
874
|
+
const number = pc4.cyan(`${idx + 1}.`);
|
|
875
|
+
const text = isRecommended ? `${opt} ${pc4.green("\u2713 Recommended")}` : opt;
|
|
876
|
+
if (isCursor) {
|
|
877
|
+
output += pc4.cyan(`\u276F ${number} ${text}
|
|
878
|
+
`);
|
|
879
|
+
} else {
|
|
880
|
+
output += ` ${number} ${text}
|
|
881
|
+
`;
|
|
882
|
+
}
|
|
883
|
+
});
|
|
884
|
+
const isCustomCursor = cursor === options.length;
|
|
885
|
+
if (isCustomCursor) {
|
|
886
|
+
output += pc4.cyan(`\u276F ${pc4.yellow("\u270E")} ${inputValue || pc4.dim("Type your own...")}`);
|
|
887
|
+
} else {
|
|
888
|
+
output += ` ${pc4.yellow("\u270E")} ${pc4.dim("Type your own...")}`;
|
|
889
|
+
}
|
|
890
|
+
return output;
|
|
891
|
+
});
|
|
892
|
+
var selectOrInput_default = selectOrInput;
|
|
893
|
+
|
|
894
|
+
// src/commands/generate.ts
|
|
895
|
+
function registerGenerateCommand(skillCommand) {
|
|
896
|
+
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) => {
|
|
897
|
+
await generateCommand(options);
|
|
898
|
+
});
|
|
899
|
+
}
|
|
900
|
+
async function generateCommand(options) {
|
|
901
|
+
trackEvent("command", { name: "generate" });
|
|
902
|
+
log.blank();
|
|
903
|
+
const tokens = loadTokens();
|
|
904
|
+
if (!tokens) {
|
|
905
|
+
log.error("Authentication required. Please run 'ctx7 login' first.");
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (isTokenExpired(tokens)) {
|
|
909
|
+
log.error("Session expired. Please run 'ctx7 login' to refresh.");
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
const accessToken = tokens.access_token;
|
|
913
|
+
const initSpinner = ora().start();
|
|
914
|
+
const quota = await getSkillQuota(accessToken);
|
|
915
|
+
if (quota.error) {
|
|
916
|
+
initSpinner.fail(pc5.red("Failed to initialize"));
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (quota.tier !== "unlimited" && quota.remaining < 1) {
|
|
920
|
+
initSpinner.fail(pc5.red("Weekly skill generation limit reached"));
|
|
921
|
+
log.blank();
|
|
922
|
+
console.log(
|
|
923
|
+
` You've used ${pc5.bold(pc5.white(quota.used.toString()))}/${pc5.bold(pc5.white(quota.limit.toString()))} skill generations this week.`
|
|
924
|
+
);
|
|
925
|
+
console.log(
|
|
926
|
+
` Your quota resets on ${pc5.yellow(new Date(quota.resetDate).toLocaleDateString())}.`
|
|
927
|
+
);
|
|
928
|
+
log.blank();
|
|
929
|
+
if (quota.tier === "free") {
|
|
930
|
+
console.log(
|
|
931
|
+
` ${pc5.yellow("Tip:")} Upgrade to Pro for ${pc5.bold("10")} generations per week.`
|
|
932
|
+
);
|
|
933
|
+
console.log(` Visit ${pc5.green("https://context7.com/dashboard")} to upgrade.`);
|
|
934
|
+
}
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
initSpinner.stop();
|
|
938
|
+
initSpinner.clear();
|
|
939
|
+
console.log(pc5.bold("What should your agent become an expert at?\n"));
|
|
940
|
+
console.log(
|
|
941
|
+
pc5.dim("Skills teach agents best practices, design patterns, and domain expertise.\n")
|
|
942
|
+
);
|
|
943
|
+
console.log(pc5.yellow("Examples:"));
|
|
944
|
+
console.log(pc5.dim(' "React component optimization and performance best practices"'));
|
|
945
|
+
console.log(pc5.dim(' "Responsive web design with Tailwind CSS"'));
|
|
946
|
+
console.log(pc5.dim(' "Writing effective landing page copy"'));
|
|
947
|
+
console.log(pc5.dim(' "Deploying Next.js apps to Vercel"'));
|
|
948
|
+
console.log(pc5.dim(' "OAuth authentication with NextAuth.js"\n'));
|
|
949
|
+
let motivation;
|
|
950
|
+
try {
|
|
951
|
+
motivation = await input({
|
|
952
|
+
message: "Describe the expertise:"
|
|
953
|
+
});
|
|
954
|
+
if (!motivation.trim()) {
|
|
955
|
+
log.warn("Expertise description is required");
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
motivation = motivation.trim();
|
|
959
|
+
} catch {
|
|
960
|
+
log.warn("Generation cancelled");
|
|
961
|
+
return;
|
|
962
|
+
}
|
|
963
|
+
const searchSpinner = ora("Finding relevant libraries...").start();
|
|
964
|
+
const searchResult = await searchLibraries(motivation, accessToken);
|
|
965
|
+
if (searchResult.error || !searchResult.results?.length) {
|
|
966
|
+
searchSpinner.fail(pc5.red("No libraries found"));
|
|
967
|
+
log.warn(searchResult.message || "Try a different description");
|
|
968
|
+
return;
|
|
969
|
+
}
|
|
970
|
+
searchSpinner.succeed(pc5.green(`Found ${searchResult.results.length} relevant libraries`));
|
|
971
|
+
log.blank();
|
|
972
|
+
let selectedLibraries;
|
|
973
|
+
try {
|
|
974
|
+
const formatProjectId = (id) => {
|
|
975
|
+
return id.startsWith("/") ? id.slice(1) : id;
|
|
976
|
+
};
|
|
977
|
+
const isGitHubRepo = (id) => {
|
|
978
|
+
const cleanId = id.startsWith("/") ? id.slice(1) : id;
|
|
979
|
+
const parts = cleanId.split("/");
|
|
980
|
+
if (parts.length !== 2) return false;
|
|
981
|
+
const nonGitHubPrefixes = ["websites", "packages", "npm", "docs", "libraries", "llmstxt"];
|
|
982
|
+
return !nonGitHubPrefixes.includes(parts[0].toLowerCase());
|
|
983
|
+
};
|
|
984
|
+
const libraries = searchResult.results.slice(0, 5);
|
|
985
|
+
const indexWidth = libraries.length.toString().length;
|
|
986
|
+
const maxNameLen = Math.max(...libraries.map((lib) => lib.title.length));
|
|
987
|
+
const libraryChoices = libraries.map((lib, index) => {
|
|
988
|
+
const projectId = formatProjectId(lib.id);
|
|
989
|
+
const isGitHub = isGitHubRepo(lib.id);
|
|
990
|
+
const indexStr = pc5.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
|
|
991
|
+
const paddedName = lib.title.padEnd(maxNameLen);
|
|
992
|
+
const libUrl = `https://context7.com${lib.id}`;
|
|
993
|
+
const libLink = terminalLink(lib.title, libUrl, pc5.white);
|
|
994
|
+
const repoLink = isGitHub ? terminalLink(projectId, `https://github.com/${projectId}`, pc5.white) : pc5.white(projectId);
|
|
995
|
+
const starsLine = lib.stars && isGitHub ? [`${pc5.yellow("Stars:")} ${lib.stars.toLocaleString()}`] : [];
|
|
996
|
+
const metadataLines = [
|
|
997
|
+
pc5.dim("\u2500".repeat(50)),
|
|
998
|
+
"",
|
|
999
|
+
`${pc5.yellow("Library:")} ${libLink}`,
|
|
1000
|
+
`${pc5.yellow("Source:")} ${repoLink}`,
|
|
1001
|
+
`${pc5.yellow("Snippets:")} ${lib.totalSnippets.toLocaleString()}`,
|
|
1002
|
+
...starsLine,
|
|
1003
|
+
`${pc5.yellow("Description:")}`,
|
|
1004
|
+
pc5.white(lib.description || "No description")
|
|
1005
|
+
];
|
|
1006
|
+
return {
|
|
1007
|
+
name: `${indexStr} ${paddedName} ${pc5.dim(`(${projectId})`)}`,
|
|
1008
|
+
value: lib,
|
|
1009
|
+
description: metadataLines.join("\n")
|
|
1010
|
+
};
|
|
1011
|
+
});
|
|
1012
|
+
selectedLibraries = await checkboxWithHover(
|
|
1013
|
+
{
|
|
1014
|
+
message: "Select libraries:",
|
|
1015
|
+
choices: libraryChoices,
|
|
1016
|
+
pageSize: 10,
|
|
1017
|
+
loop: false
|
|
1018
|
+
},
|
|
1019
|
+
{ getName: (lib) => `${lib.title} (${formatProjectId(lib.id)})` }
|
|
1020
|
+
);
|
|
1021
|
+
if (!selectedLibraries || selectedLibraries.length === 0) {
|
|
1022
|
+
log.info("No libraries selected. Try running the command again.");
|
|
1023
|
+
return;
|
|
1024
|
+
}
|
|
1025
|
+
} catch {
|
|
1026
|
+
log.warn("Generation cancelled");
|
|
1027
|
+
return;
|
|
1028
|
+
}
|
|
1029
|
+
log.blank();
|
|
1030
|
+
const questionsSpinner = ora("Preparing questions...").start();
|
|
1031
|
+
const librariesInput = selectedLibraries.map((lib) => ({ id: lib.id, name: lib.title }));
|
|
1032
|
+
const questionsResult = await getSkillQuestions(librariesInput, motivation, accessToken);
|
|
1033
|
+
if (questionsResult.error || !questionsResult.questions?.length) {
|
|
1034
|
+
questionsSpinner.fail(pc5.red("Failed to generate questions"));
|
|
1035
|
+
log.warn(questionsResult.message || "Please try again");
|
|
1036
|
+
return;
|
|
1037
|
+
}
|
|
1038
|
+
questionsSpinner.succeed(pc5.green("Questions prepared"));
|
|
1039
|
+
log.blank();
|
|
1040
|
+
const answers = [];
|
|
1041
|
+
try {
|
|
1042
|
+
for (let i = 0; i < questionsResult.questions.length; i++) {
|
|
1043
|
+
const q = questionsResult.questions[i];
|
|
1044
|
+
const questionNum = i + 1;
|
|
1045
|
+
const totalQuestions = questionsResult.questions.length;
|
|
1046
|
+
const answer = await selectOrInput_default({
|
|
1047
|
+
message: `${pc5.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`,
|
|
1048
|
+
options: q.options,
|
|
1049
|
+
recommendedIndex: q.recommendedIndex
|
|
1050
|
+
});
|
|
1051
|
+
answers.push({
|
|
1052
|
+
question: q.question,
|
|
1053
|
+
answer
|
|
1054
|
+
});
|
|
1055
|
+
const linesToClear = 3 + q.options.length;
|
|
1056
|
+
process.stdout.write(`\x1B[${linesToClear}A\x1B[J`);
|
|
1057
|
+
const truncatedAnswer = answer.length > 50 ? answer.slice(0, 47) + "..." : answer;
|
|
1058
|
+
console.log(`${pc5.green("\u2713")} ${pc5.dim(`[${questionNum}/${totalQuestions}]`)} ${q.question}`);
|
|
1059
|
+
console.log(` ${pc5.cyan(truncatedAnswer)}`);
|
|
1060
|
+
log.blank();
|
|
1061
|
+
}
|
|
1062
|
+
} catch {
|
|
1063
|
+
log.warn("Generation cancelled");
|
|
1064
|
+
return;
|
|
1065
|
+
}
|
|
1066
|
+
let generatedContent = null;
|
|
1067
|
+
let skillName = "";
|
|
1068
|
+
let feedback;
|
|
1069
|
+
const libraryNames = selectedLibraries.map((lib) => lib.title).join(", ");
|
|
1070
|
+
const queryLog = [];
|
|
1071
|
+
let genSpinner = null;
|
|
1072
|
+
const formatQueryLogText = () => {
|
|
1073
|
+
if (queryLog.length === 0) return "";
|
|
1074
|
+
const lines = [];
|
|
1075
|
+
const latestEntry = queryLog[queryLog.length - 1];
|
|
1076
|
+
lines.push(pc5.dim(`(${queryLog.length} ${queryLog.length === 1 ? "query" : "queries"})`));
|
|
1077
|
+
lines.push("");
|
|
1078
|
+
for (const result of latestEntry.results.slice(0, 3)) {
|
|
1079
|
+
const cleanContent = result.content.replace(/Source:\s*https?:\/\/[^\s]+/gi, "").trim();
|
|
1080
|
+
if (cleanContent) {
|
|
1081
|
+
lines.push(` ${pc5.yellow("\u2022")} ${pc5.white(result.title)}`);
|
|
1082
|
+
const maxLen = 400;
|
|
1083
|
+
const content = cleanContent.length > maxLen ? cleanContent.slice(0, maxLen - 3) + "..." : cleanContent;
|
|
1084
|
+
const words = content.split(" ");
|
|
1085
|
+
let currentLine = " ";
|
|
1086
|
+
for (const word of words) {
|
|
1087
|
+
if (currentLine.length + word.length > 84) {
|
|
1088
|
+
lines.push(pc5.dim(currentLine));
|
|
1089
|
+
currentLine = " " + word + " ";
|
|
1090
|
+
} else {
|
|
1091
|
+
currentLine += word + " ";
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
if (currentLine.trim()) {
|
|
1095
|
+
lines.push(pc5.dim(currentLine));
|
|
1096
|
+
}
|
|
1097
|
+
lines.push("");
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return "\n" + lines.join("\n");
|
|
1101
|
+
};
|
|
1102
|
+
let isGeneratingContent = false;
|
|
1103
|
+
const handleStreamEvent = (event) => {
|
|
1104
|
+
if (event.type === "progress") {
|
|
1105
|
+
if (genSpinner) {
|
|
1106
|
+
if (event.message.startsWith("Generating skill content...") && !isGeneratingContent) {
|
|
1107
|
+
isGeneratingContent = true;
|
|
1108
|
+
if (queryLog.length > 0) {
|
|
1109
|
+
genSpinner.succeed(pc5.green(`Queried documentation`));
|
|
1110
|
+
} else {
|
|
1111
|
+
genSpinner.succeed(pc5.green(`Ready to generate`));
|
|
1112
|
+
}
|
|
1113
|
+
genSpinner = ora("Generating skill content...").start();
|
|
1114
|
+
} else if (!isGeneratingContent) {
|
|
1115
|
+
genSpinner.text = event.message + formatQueryLogText();
|
|
1116
|
+
}
|
|
1117
|
+
}
|
|
1118
|
+
} else if (event.type === "tool_result") {
|
|
1119
|
+
queryLog.push({
|
|
1120
|
+
query: event.query,
|
|
1121
|
+
libraryId: event.libraryId,
|
|
1122
|
+
results: event.results
|
|
1123
|
+
});
|
|
1124
|
+
if (genSpinner && !isGeneratingContent) {
|
|
1125
|
+
genSpinner.text = genSpinner.text.split("\n")[0] + formatQueryLogText();
|
|
1126
|
+
}
|
|
1127
|
+
}
|
|
1128
|
+
};
|
|
1129
|
+
while (true) {
|
|
1130
|
+
const generateInput = {
|
|
1131
|
+
motivation,
|
|
1132
|
+
libraries: librariesInput,
|
|
1133
|
+
answers,
|
|
1134
|
+
feedback,
|
|
1135
|
+
previousContent: feedback && generatedContent ? generatedContent : void 0
|
|
1136
|
+
};
|
|
1137
|
+
queryLog.length = 0;
|
|
1138
|
+
isGeneratingContent = false;
|
|
1139
|
+
const initialStatus = feedback ? "Regenerating skill with your feedback..." : `Generating skill for "${libraryNames}"...`;
|
|
1140
|
+
genSpinner = ora(initialStatus).start();
|
|
1141
|
+
const result = await generateSkillStructured(generateInput, handleStreamEvent, accessToken);
|
|
1142
|
+
if (result.error) {
|
|
1143
|
+
genSpinner.fail(pc5.red(`Error: ${result.error}`));
|
|
1144
|
+
return;
|
|
1145
|
+
}
|
|
1146
|
+
if (!result.content) {
|
|
1147
|
+
genSpinner.fail(pc5.red("No content generated"));
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
genSpinner.succeed(pc5.green(`Generated skill for "${result.libraryName}"`));
|
|
1151
|
+
generatedContent = result.content;
|
|
1152
|
+
skillName = result.libraryName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
1153
|
+
const contentLines = generatedContent.split("\n");
|
|
1154
|
+
const previewLineCount = 20;
|
|
1155
|
+
const hasMoreLines = contentLines.length > previewLineCount;
|
|
1156
|
+
const previewContent = contentLines.slice(0, previewLineCount).join("\n");
|
|
1157
|
+
const remainingLines = contentLines.length - previewLineCount;
|
|
1158
|
+
const showPreview = () => {
|
|
1159
|
+
log.blank();
|
|
1160
|
+
console.log(pc5.dim("\u2501".repeat(70)));
|
|
1161
|
+
console.log(pc5.bold(`Generated Skill: `) + pc5.green(pc5.bold(skillName)));
|
|
1162
|
+
console.log(pc5.dim("\u2501".repeat(70)));
|
|
1163
|
+
log.blank();
|
|
1164
|
+
console.log(previewContent);
|
|
1165
|
+
if (hasMoreLines) {
|
|
1166
|
+
log.blank();
|
|
1167
|
+
console.log(pc5.dim(`... ${remainingLines} more lines`));
|
|
1168
|
+
}
|
|
1169
|
+
log.blank();
|
|
1170
|
+
console.log(pc5.dim("\u2501".repeat(70)));
|
|
1171
|
+
log.blank();
|
|
1172
|
+
};
|
|
1173
|
+
const showFullContent = () => {
|
|
1174
|
+
log.blank();
|
|
1175
|
+
console.log(pc5.dim("\u2501".repeat(70)));
|
|
1176
|
+
console.log(pc5.bold(`Generated Skill: `) + pc5.green(pc5.bold(skillName)));
|
|
1177
|
+
console.log(pc5.dim("\u2501".repeat(70)));
|
|
1178
|
+
log.blank();
|
|
1179
|
+
console.log(generatedContent);
|
|
1180
|
+
log.blank();
|
|
1181
|
+
console.log(pc5.dim("\u2501".repeat(70)));
|
|
1182
|
+
log.blank();
|
|
1183
|
+
};
|
|
1184
|
+
showPreview();
|
|
1185
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
1186
|
+
try {
|
|
1187
|
+
let action;
|
|
1188
|
+
while (true) {
|
|
1189
|
+
const choices = [
|
|
1190
|
+
{ name: `${pc5.green("\u2713")} Install skill`, value: "install" },
|
|
1191
|
+
...hasMoreLines ? [{ name: `${pc5.blue("\u2922")} View full skill`, value: "expand" }] : [],
|
|
1192
|
+
{ name: `${pc5.yellow("\u270E")} Request changes`, value: "feedback" },
|
|
1193
|
+
{ name: `${pc5.red("\u2715")} Cancel`, value: "cancel" }
|
|
1194
|
+
];
|
|
1195
|
+
action = await select2({
|
|
1196
|
+
message: "What would you like to do?",
|
|
1197
|
+
choices
|
|
1198
|
+
});
|
|
1199
|
+
if (action === "expand") {
|
|
1200
|
+
showFullContent();
|
|
1201
|
+
continue;
|
|
1202
|
+
}
|
|
1203
|
+
break;
|
|
1204
|
+
}
|
|
1205
|
+
if (action === "install") {
|
|
1206
|
+
break;
|
|
1207
|
+
} else if (action === "cancel") {
|
|
1208
|
+
log.warn("Generation cancelled");
|
|
1209
|
+
return;
|
|
1210
|
+
} else if (action === "feedback") {
|
|
1211
|
+
trackEvent("gen_feedback");
|
|
1212
|
+
feedback = await input({
|
|
1213
|
+
message: "What changes would you like? (press Enter to skip)"
|
|
1214
|
+
});
|
|
1215
|
+
if (!feedback.trim()) {
|
|
1216
|
+
feedback = void 0;
|
|
1217
|
+
}
|
|
1218
|
+
log.blank();
|
|
1219
|
+
}
|
|
1220
|
+
} catch {
|
|
1221
|
+
log.warn("Generation cancelled");
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
const targets = await promptForInstallTargets(options);
|
|
1226
|
+
if (!targets) {
|
|
1227
|
+
log.warn("Generation cancelled");
|
|
1228
|
+
return;
|
|
1229
|
+
}
|
|
1230
|
+
const targetDirs = getTargetDirs(targets);
|
|
1231
|
+
const writeSpinner = ora("Writing skill files...").start();
|
|
1232
|
+
let permissionError = false;
|
|
1233
|
+
const failedDirs = /* @__PURE__ */ new Set();
|
|
1234
|
+
for (const targetDir of targetDirs) {
|
|
1235
|
+
let finalDir = targetDir;
|
|
1236
|
+
if (options.output && !targetDir.includes("/.config/") && !targetDir.startsWith(homedir3())) {
|
|
1237
|
+
finalDir = targetDir.replace(process.cwd(), options.output);
|
|
1238
|
+
}
|
|
1239
|
+
const skillDir = join4(finalDir, skillName);
|
|
1240
|
+
const skillPath = join4(skillDir, "SKILL.md");
|
|
1241
|
+
try {
|
|
1242
|
+
await mkdir2(skillDir, { recursive: true });
|
|
1243
|
+
await writeFile2(skillPath, generatedContent, "utf-8");
|
|
1244
|
+
} catch (err) {
|
|
1245
|
+
const error = err;
|
|
1246
|
+
if (error.code === "EACCES" || error.code === "EPERM") {
|
|
1247
|
+
permissionError = true;
|
|
1248
|
+
failedDirs.add(skillDir);
|
|
1249
|
+
} else {
|
|
1250
|
+
log.warn(`Failed to write to ${skillPath}: ${error.message}`);
|
|
1251
|
+
}
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
if (permissionError) {
|
|
1255
|
+
writeSpinner.fail(pc5.red("Permission denied"));
|
|
1256
|
+
log.blank();
|
|
1257
|
+
console.log(pc5.yellow("Fix permissions with:"));
|
|
1258
|
+
for (const dir of failedDirs) {
|
|
1259
|
+
const parentDir = join4(dir, "..");
|
|
1260
|
+
console.log(pc5.dim(` sudo chown -R $(whoami) "${parentDir}"`));
|
|
1261
|
+
}
|
|
1262
|
+
log.blank();
|
|
1263
|
+
return;
|
|
1264
|
+
}
|
|
1265
|
+
writeSpinner.succeed(pc5.green(`Created skill in ${targetDirs.length} location(s)`));
|
|
1266
|
+
trackEvent("gen_install");
|
|
1267
|
+
log.blank();
|
|
1268
|
+
console.log(pc5.green(pc5.bold("Skill saved successfully")));
|
|
1269
|
+
for (const targetDir of targetDirs) {
|
|
1270
|
+
console.log(pc5.dim(` ${targetDir}/`) + pc5.green(skillName));
|
|
1271
|
+
}
|
|
1272
|
+
log.blank();
|
|
1273
|
+
}
|
|
1274
|
+
|
|
498
1275
|
// src/commands/skill.ts
|
|
1276
|
+
import { homedir as homedir4 } from "os";
|
|
499
1277
|
function logInstallSummary(targets, targetDirs, skillNames) {
|
|
500
1278
|
log.blank();
|
|
501
1279
|
let dirIndex = 0;
|
|
@@ -512,6 +1290,7 @@ function logInstallSummary(targets, targetDirs, skillNames) {
|
|
|
512
1290
|
}
|
|
513
1291
|
function registerSkillCommands(program2) {
|
|
514
1292
|
const skill = program2.command("skills").alias("skill").description("Manage AI coding skills");
|
|
1293
|
+
registerGenerateCommand(skill);
|
|
515
1294
|
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
1295
|
await installCommand(project, skillName, options);
|
|
517
1296
|
});
|
|
@@ -536,10 +1315,11 @@ function registerSkillAliases(program2) {
|
|
|
536
1315
|
await searchCommand(keywords.join(" "));
|
|
537
1316
|
});
|
|
538
1317
|
}
|
|
539
|
-
async function installCommand(
|
|
540
|
-
|
|
1318
|
+
async function installCommand(input2, skillName, options) {
|
|
1319
|
+
trackEvent("command", { name: "install" });
|
|
1320
|
+
const parsed = parseSkillInput(input2);
|
|
541
1321
|
if (!parsed) {
|
|
542
|
-
log.error(`Invalid input format: ${
|
|
1322
|
+
log.error(`Invalid input format: ${input2}`);
|
|
543
1323
|
log.info(`Expected: /owner/repo or full GitHub URL`);
|
|
544
1324
|
log.info(`Example: ctx7 skills install /anthropics/skills pdf`);
|
|
545
1325
|
log.blank();
|
|
@@ -547,17 +1327,17 @@ async function installCommand(input, skillName, options) {
|
|
|
547
1327
|
}
|
|
548
1328
|
const repo = `/${parsed.owner}/${parsed.repo}`;
|
|
549
1329
|
log.blank();
|
|
550
|
-
const spinner =
|
|
1330
|
+
const spinner = ora2(`Fetching skills from ${repo}...`).start();
|
|
551
1331
|
let selectedSkills;
|
|
552
1332
|
if (skillName) {
|
|
553
1333
|
spinner.text = `Fetching skill: ${skillName}...`;
|
|
554
1334
|
const skillData = await getSkill(repo, skillName);
|
|
555
1335
|
if (skillData.error || !skillData.name) {
|
|
556
1336
|
if (skillData.error === "prompt_injection_detected") {
|
|
557
|
-
spinner.fail(
|
|
1337
|
+
spinner.fail(pc6.red(`Prompt injection detected in skill: ${skillName}`));
|
|
558
1338
|
log.warn("This skill contains potentially malicious content and cannot be installed.");
|
|
559
1339
|
} else {
|
|
560
|
-
spinner.fail(
|
|
1340
|
+
spinner.fail(pc6.red(`Skill not found: ${skillName}`));
|
|
561
1341
|
}
|
|
562
1342
|
return;
|
|
563
1343
|
}
|
|
@@ -573,14 +1353,14 @@ async function installCommand(input, skillName, options) {
|
|
|
573
1353
|
} else {
|
|
574
1354
|
const data = await listProjectSkills(repo);
|
|
575
1355
|
if (data.error) {
|
|
576
|
-
spinner.fail(
|
|
1356
|
+
spinner.fail(pc6.red(`Error: ${data.message || data.error}`));
|
|
577
1357
|
return;
|
|
578
1358
|
}
|
|
579
1359
|
if (!data.skills || data.skills.length === 0) {
|
|
580
|
-
spinner.warn(
|
|
1360
|
+
spinner.warn(pc6.yellow(`No skills found in ${repo}`));
|
|
581
1361
|
return;
|
|
582
1362
|
}
|
|
583
|
-
const skillsWithRepo = data.skills.map((s) => ({ ...s, project: repo }));
|
|
1363
|
+
const skillsWithRepo = data.skills.map((s) => ({ ...s, project: repo })).sort((a, b) => (b.installCount ?? 0) - (a.installCount ?? 0));
|
|
584
1364
|
spinner.succeed(`Found ${data.skills.length} skill(s)`);
|
|
585
1365
|
if (data.blockedSkillsCount && data.blockedSkillsCount > 0) {
|
|
586
1366
|
log.blank();
|
|
@@ -595,19 +1375,19 @@ async function installCommand(input, skillName, options) {
|
|
|
595
1375
|
const indexWidth = data.skills.length.toString().length;
|
|
596
1376
|
const maxNameLen = Math.max(...data.skills.map((s) => s.name.length));
|
|
597
1377
|
const choices = skillsWithRepo.map((s, index) => {
|
|
598
|
-
const indexStr =
|
|
1378
|
+
const indexStr = pc6.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
|
|
599
1379
|
const paddedName = s.name.padEnd(maxNameLen);
|
|
600
1380
|
const installs = formatInstallCount(s.installCount);
|
|
601
1381
|
const skillUrl = `https://context7.com/skills${s.project}/${s.name}`;
|
|
602
|
-
const skillLink = terminalLink(s.name, skillUrl,
|
|
603
|
-
const repoLink = terminalLink(s.project, `https://github.com${s.project}`,
|
|
1382
|
+
const skillLink = terminalLink(s.name, skillUrl, pc6.white);
|
|
1383
|
+
const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc6.white);
|
|
604
1384
|
const metadataLines = [
|
|
605
|
-
|
|
1385
|
+
pc6.dim("\u2500".repeat(50)),
|
|
606
1386
|
"",
|
|
607
|
-
`${
|
|
608
|
-
`${
|
|
609
|
-
`${
|
|
610
|
-
|
|
1387
|
+
`${pc6.yellow("Skill:")} ${skillLink}`,
|
|
1388
|
+
`${pc6.yellow("Repo:")} ${repoLink}`,
|
|
1389
|
+
`${pc6.yellow("Description:")}`,
|
|
1390
|
+
pc6.white(s.description || "No description")
|
|
611
1391
|
];
|
|
612
1392
|
return {
|
|
613
1393
|
name: installs ? `${indexStr} ${paddedName} ${installs}` : `${indexStr} ${paddedName}`,
|
|
@@ -616,9 +1396,11 @@ async function installCommand(input, skillName, options) {
|
|
|
616
1396
|
};
|
|
617
1397
|
});
|
|
618
1398
|
log.blank();
|
|
1399
|
+
const installsOffset = 4 + indexWidth + 1 + 1 + maxNameLen + 1 - 3;
|
|
1400
|
+
const message = "Select skills:" + " ".repeat(Math.max(1, installsOffset - 14)) + pc6.dim("installs");
|
|
619
1401
|
try {
|
|
620
1402
|
selectedSkills = await checkboxWithHover({
|
|
621
|
-
message
|
|
1403
|
+
message,
|
|
622
1404
|
choices,
|
|
623
1405
|
pageSize: 15,
|
|
624
1406
|
loop: false
|
|
@@ -639,7 +1421,7 @@ async function installCommand(input, skillName, options) {
|
|
|
639
1421
|
return;
|
|
640
1422
|
}
|
|
641
1423
|
const targetDirs = getTargetDirs(targets);
|
|
642
|
-
const installSpinner =
|
|
1424
|
+
const installSpinner = ora2("Installing skills...").start();
|
|
643
1425
|
let permissionError = false;
|
|
644
1426
|
const failedDirs = /* @__PURE__ */ new Set();
|
|
645
1427
|
const installedSkills = [];
|
|
@@ -663,7 +1445,7 @@ async function installCommand(input, skillName, options) {
|
|
|
663
1445
|
}
|
|
664
1446
|
throw dirErr;
|
|
665
1447
|
}
|
|
666
|
-
const primarySkillDir =
|
|
1448
|
+
const primarySkillDir = join5(primaryDir, skill.name);
|
|
667
1449
|
for (const targetDir of symlinkDirs) {
|
|
668
1450
|
try {
|
|
669
1451
|
await symlinkSkill(skill.name, primarySkillDir, targetDir);
|
|
@@ -691,55 +1473,57 @@ async function installCommand(input, skillName, options) {
|
|
|
691
1473
|
log.blank();
|
|
692
1474
|
log.warn("Fix permissions with:");
|
|
693
1475
|
for (const dir of failedDirs) {
|
|
694
|
-
const parentDir =
|
|
1476
|
+
const parentDir = join5(dir, "..");
|
|
695
1477
|
log.dim(` sudo chown -R $(whoami) "${parentDir}"`);
|
|
696
1478
|
}
|
|
697
1479
|
log.blank();
|
|
698
1480
|
return;
|
|
699
1481
|
}
|
|
700
1482
|
installSpinner.succeed(`Installed ${installedSkills.length} skill(s)`);
|
|
701
|
-
|
|
1483
|
+
trackEvent("install", { skills: installedSkills, ides: targets.ides });
|
|
702
1484
|
const installedNames = selectedSkills.map((s) => s.name);
|
|
703
1485
|
logInstallSummary(targets, targetDirs, installedNames);
|
|
704
1486
|
}
|
|
705
1487
|
async function searchCommand(query) {
|
|
1488
|
+
trackEvent("command", { name: "search" });
|
|
706
1489
|
log.blank();
|
|
707
|
-
const spinner =
|
|
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(
|
|
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(
|
|
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(
|
|
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)`);
|
|
1507
|
+
trackEvent("search_query", { query, resultCount: data.results.length });
|
|
724
1508
|
const indexWidth = data.results.length.toString().length;
|
|
725
1509
|
const maxNameLen = Math.max(...data.results.map((s) => s.name.length));
|
|
726
1510
|
const choices = data.results.map((s, index) => {
|
|
727
|
-
const indexStr =
|
|
1511
|
+
const indexStr = pc6.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
|
|
728
1512
|
const paddedName = s.name.padEnd(maxNameLen);
|
|
729
1513
|
const installs = formatInstallCount(s.installCount);
|
|
730
1514
|
const skillLink = terminalLink(
|
|
731
1515
|
s.name,
|
|
732
1516
|
`https://context7.com/skills${s.project}/${s.name}`,
|
|
733
|
-
|
|
1517
|
+
pc6.white
|
|
734
1518
|
);
|
|
735
|
-
const repoLink = terminalLink(s.project, `https://github.com${s.project}`,
|
|
1519
|
+
const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc6.white);
|
|
736
1520
|
const metadataLines = [
|
|
737
|
-
|
|
1521
|
+
pc6.dim("\u2500".repeat(50)),
|
|
738
1522
|
"",
|
|
739
|
-
`${
|
|
740
|
-
`${
|
|
741
|
-
`${
|
|
742
|
-
|
|
1523
|
+
`${pc6.yellow("Skill:")} ${skillLink}`,
|
|
1524
|
+
`${pc6.yellow("Repo:")} ${repoLink}`,
|
|
1525
|
+
`${pc6.yellow("Description:")}`,
|
|
1526
|
+
pc6.white(s.description || "No description")
|
|
743
1527
|
];
|
|
744
1528
|
return {
|
|
745
1529
|
name: installs ? `${indexStr} ${paddedName} ${installs}` : `${indexStr} ${paddedName}`,
|
|
@@ -748,10 +1532,12 @@ async function searchCommand(query) {
|
|
|
748
1532
|
};
|
|
749
1533
|
});
|
|
750
1534
|
log.blank();
|
|
1535
|
+
const installsOffset = 4 + indexWidth + 1 + 1 + maxNameLen + 1 - 3;
|
|
1536
|
+
const message = "Select skills to install:" + " ".repeat(Math.max(1, installsOffset - 25)) + pc6.dim("installs");
|
|
751
1537
|
let selectedSkills;
|
|
752
1538
|
try {
|
|
753
1539
|
selectedSkills = await checkboxWithHover({
|
|
754
|
-
message
|
|
1540
|
+
message,
|
|
755
1541
|
choices,
|
|
756
1542
|
pageSize: 15,
|
|
757
1543
|
loop: false
|
|
@@ -771,7 +1557,7 @@ async function searchCommand(query) {
|
|
|
771
1557
|
return;
|
|
772
1558
|
}
|
|
773
1559
|
const targetDirs = getTargetDirs(targets);
|
|
774
|
-
const installSpinner =
|
|
1560
|
+
const installSpinner = ora2("Installing skills...").start();
|
|
775
1561
|
let permissionError = false;
|
|
776
1562
|
const failedDirs = /* @__PURE__ */ new Set();
|
|
777
1563
|
const installedSkills = [];
|
|
@@ -795,7 +1581,7 @@ async function searchCommand(query) {
|
|
|
795
1581
|
}
|
|
796
1582
|
throw dirErr;
|
|
797
1583
|
}
|
|
798
|
-
const primarySkillDir =
|
|
1584
|
+
const primarySkillDir = join5(primaryDir, skill.name);
|
|
799
1585
|
for (const targetDir of symlinkDirs) {
|
|
800
1586
|
try {
|
|
801
1587
|
await symlinkSkill(skill.name, primarySkillDir, targetDir);
|
|
@@ -823,50 +1609,59 @@ async function searchCommand(query) {
|
|
|
823
1609
|
log.blank();
|
|
824
1610
|
log.warn("Fix permissions with:");
|
|
825
1611
|
for (const dir of failedDirs) {
|
|
826
|
-
const parentDir =
|
|
1612
|
+
const parentDir = join5(dir, "..");
|
|
827
1613
|
log.dim(` sudo chown -R $(whoami) "${parentDir}"`);
|
|
828
1614
|
}
|
|
829
1615
|
log.blank();
|
|
830
1616
|
return;
|
|
831
1617
|
}
|
|
832
1618
|
installSpinner.succeed(`Installed ${installedSkills.length} skill(s)`);
|
|
833
|
-
|
|
1619
|
+
trackEvent("install", { skills: installedSkills, ides: targets.ides });
|
|
834
1620
|
const installedNames = uniqueSkills.map((s) => s.name);
|
|
835
1621
|
logInstallSummary(targets, targetDirs, installedNames);
|
|
836
1622
|
}
|
|
837
1623
|
async function listCommand(options) {
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
1624
|
+
trackEvent("command", { name: "list" });
|
|
1625
|
+
const scope = options.global ? "global" : "project";
|
|
1626
|
+
const pathMap = scope === "global" ? IDE_GLOBAL_PATHS : IDE_PATHS;
|
|
1627
|
+
const baseDir = scope === "global" ? homedir4() : process.cwd();
|
|
1628
|
+
const idesToCheck = hasExplicitIdeOption(options) ? getSelectedIdes(options) : Object.keys(IDE_NAMES);
|
|
1629
|
+
const results = [];
|
|
1630
|
+
for (const ide of idesToCheck) {
|
|
1631
|
+
const skillsDir = join5(baseDir, pathMap[ide]);
|
|
1632
|
+
try {
|
|
1633
|
+
const entries = await readdir(skillsDir, { withFileTypes: true });
|
|
1634
|
+
const skillFolders = entries.filter((e) => e.isDirectory() || e.isSymbolicLink()).map((e) => e.name);
|
|
1635
|
+
if (skillFolders.length > 0) {
|
|
1636
|
+
results.push({ ide, skills: skillFolders });
|
|
1637
|
+
}
|
|
1638
|
+
} catch {
|
|
1639
|
+
}
|
|
1640
|
+
}
|
|
1641
|
+
if (results.length === 0) {
|
|
1642
|
+
log.warn("No skills installed");
|
|
841
1643
|
return;
|
|
842
1644
|
}
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
const
|
|
846
|
-
const
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
}
|
|
851
|
-
log.info(`
|
|
852
|
-
\u25C6 Installed skills (${skillsDir}):`);
|
|
853
|
-
for (const folder of skillFolders) {
|
|
854
|
-
log.item(folder.name);
|
|
1645
|
+
log.blank();
|
|
1646
|
+
for (const { ide, skills } of results) {
|
|
1647
|
+
const ideName = IDE_NAMES[ide];
|
|
1648
|
+
const path2 = pathMap[ide];
|
|
1649
|
+
log.plain(`${pc6.bold(ideName)} ${pc6.dim(path2)}`);
|
|
1650
|
+
for (const skill of skills) {
|
|
1651
|
+
log.plain(` ${pc6.green(skill)}`);
|
|
855
1652
|
}
|
|
856
|
-
log.
|
|
857
|
-
`);
|
|
858
|
-
} catch {
|
|
859
|
-
log.warn(`No skills directory found at ${skillsDir}`);
|
|
1653
|
+
log.blank();
|
|
860
1654
|
}
|
|
861
1655
|
}
|
|
862
1656
|
async function removeCommand(name, options) {
|
|
1657
|
+
trackEvent("command", { name: "remove" });
|
|
863
1658
|
const target = await promptForSingleTarget(options);
|
|
864
1659
|
if (!target) {
|
|
865
1660
|
log.warn("Cancelled");
|
|
866
1661
|
return;
|
|
867
1662
|
}
|
|
868
1663
|
const skillsDir = getTargetDirFromSelection(target.ide, target.scope);
|
|
869
|
-
const skillPath =
|
|
1664
|
+
const skillPath = join5(skillsDir, name);
|
|
870
1665
|
try {
|
|
871
1666
|
await rm2(skillPath, { recursive: true });
|
|
872
1667
|
log.success(`Removed skill: ${name}`);
|
|
@@ -881,25 +1676,26 @@ async function removeCommand(name, options) {
|
|
|
881
1676
|
}
|
|
882
1677
|
}
|
|
883
1678
|
}
|
|
884
|
-
async function infoCommand(
|
|
885
|
-
|
|
1679
|
+
async function infoCommand(input2) {
|
|
1680
|
+
trackEvent("command", { name: "info" });
|
|
1681
|
+
const parsed = parseSkillInput(input2);
|
|
886
1682
|
if (!parsed) {
|
|
887
1683
|
log.blank();
|
|
888
|
-
log.error(`Invalid input format: ${
|
|
1684
|
+
log.error(`Invalid input format: ${input2}`);
|
|
889
1685
|
log.info(`Expected: /owner/repo or full GitHub URL`);
|
|
890
1686
|
log.blank();
|
|
891
1687
|
return;
|
|
892
1688
|
}
|
|
893
1689
|
const repo = `/${parsed.owner}/${parsed.repo}`;
|
|
894
1690
|
log.blank();
|
|
895
|
-
const spinner =
|
|
1691
|
+
const spinner = ora2(`Fetching skills from ${repo}...`).start();
|
|
896
1692
|
const data = await listProjectSkills(repo);
|
|
897
1693
|
if (data.error) {
|
|
898
|
-
spinner.fail(
|
|
1694
|
+
spinner.fail(pc6.red(`Error: ${data.message || data.error}`));
|
|
899
1695
|
return;
|
|
900
1696
|
}
|
|
901
1697
|
if (!data.skills || data.skills.length === 0) {
|
|
902
|
-
spinner.warn(
|
|
1698
|
+
spinner.warn(pc6.yellow(`No skills found in ${repo}`));
|
|
903
1699
|
return;
|
|
904
1700
|
}
|
|
905
1701
|
spinner.succeed(`Found ${data.skills.length} skill(s)`);
|
|
@@ -911,32 +1707,168 @@ async function infoCommand(input) {
|
|
|
911
1707
|
log.blank();
|
|
912
1708
|
}
|
|
913
1709
|
log.plain(
|
|
914
|
-
`${
|
|
915
|
-
Install all: ${
|
|
916
|
-
Install one: ${
|
|
1710
|
+
`${pc6.bold("Quick commands:")}
|
|
1711
|
+
Install all: ${pc6.cyan(`ctx7 skills install ${repo} --all`)}
|
|
1712
|
+
Install one: ${pc6.cyan(`ctx7 skills install ${repo} ${data.skills[0]?.name}`)}
|
|
917
1713
|
`
|
|
918
1714
|
);
|
|
919
1715
|
}
|
|
920
1716
|
|
|
1717
|
+
// src/commands/auth.ts
|
|
1718
|
+
import pc7 from "picocolors";
|
|
1719
|
+
import ora3 from "ora";
|
|
1720
|
+
import open from "open";
|
|
1721
|
+
var CLI_CLIENT_ID = "2veBSofhicRBguUT";
|
|
1722
|
+
var baseUrl2 = "https://context7.com";
|
|
1723
|
+
function setAuthBaseUrl(url) {
|
|
1724
|
+
baseUrl2 = url;
|
|
1725
|
+
}
|
|
1726
|
+
function registerAuthCommands(program2) {
|
|
1727
|
+
program2.command("login").description("Log in to Context7").option("--no-browser", "Don't open browser automatically").action(async (options) => {
|
|
1728
|
+
await loginCommand(options);
|
|
1729
|
+
});
|
|
1730
|
+
program2.command("logout").description("Log out of Context7").action(() => {
|
|
1731
|
+
logoutCommand();
|
|
1732
|
+
});
|
|
1733
|
+
program2.command("whoami").description("Show current login status").action(async () => {
|
|
1734
|
+
await whoamiCommand();
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
async function loginCommand(options) {
|
|
1738
|
+
trackEvent("command", { name: "login" });
|
|
1739
|
+
const existingTokens = loadTokens();
|
|
1740
|
+
if (existingTokens) {
|
|
1741
|
+
const expired = isTokenExpired(existingTokens);
|
|
1742
|
+
if (!expired || existingTokens.refresh_token) {
|
|
1743
|
+
console.log(pc7.yellow("You are already logged in."));
|
|
1744
|
+
console.log(
|
|
1745
|
+
pc7.dim("Run 'ctx7 logout' first if you want to log in with a different account.")
|
|
1746
|
+
);
|
|
1747
|
+
return;
|
|
1748
|
+
}
|
|
1749
|
+
clearTokens();
|
|
1750
|
+
}
|
|
1751
|
+
const spinner = ora3("Preparing login...").start();
|
|
1752
|
+
try {
|
|
1753
|
+
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
1754
|
+
const state = generateState();
|
|
1755
|
+
const callbackServer = createCallbackServer(state);
|
|
1756
|
+
const port = await callbackServer.port;
|
|
1757
|
+
const redirectUri = `http://localhost:${port}/callback`;
|
|
1758
|
+
const authUrl = buildAuthorizationUrl(
|
|
1759
|
+
baseUrl2,
|
|
1760
|
+
CLI_CLIENT_ID,
|
|
1761
|
+
redirectUri,
|
|
1762
|
+
codeChallenge,
|
|
1763
|
+
state
|
|
1764
|
+
);
|
|
1765
|
+
spinner.stop();
|
|
1766
|
+
console.log("");
|
|
1767
|
+
console.log(pc7.bold("Opening browser to log in..."));
|
|
1768
|
+
console.log("");
|
|
1769
|
+
if (options.browser) {
|
|
1770
|
+
await open(authUrl);
|
|
1771
|
+
console.log(pc7.dim("If the browser didn't open, visit this URL:"));
|
|
1772
|
+
} else {
|
|
1773
|
+
console.log(pc7.dim("Open this URL in your browser:"));
|
|
1774
|
+
}
|
|
1775
|
+
console.log(pc7.cyan(authUrl));
|
|
1776
|
+
console.log("");
|
|
1777
|
+
const waitingSpinner = ora3("Waiting for login...").start();
|
|
1778
|
+
try {
|
|
1779
|
+
const { code } = await callbackServer.result;
|
|
1780
|
+
waitingSpinner.text = "Exchanging code for tokens...";
|
|
1781
|
+
const tokens = await exchangeCodeForTokens(
|
|
1782
|
+
baseUrl2,
|
|
1783
|
+
code,
|
|
1784
|
+
codeVerifier,
|
|
1785
|
+
redirectUri,
|
|
1786
|
+
CLI_CLIENT_ID
|
|
1787
|
+
);
|
|
1788
|
+
saveTokens(tokens);
|
|
1789
|
+
callbackServer.close();
|
|
1790
|
+
waitingSpinner.succeed(pc7.green("Login successful!"));
|
|
1791
|
+
console.log("");
|
|
1792
|
+
console.log(pc7.dim("You can now use authenticated Context7 features."));
|
|
1793
|
+
} catch (error) {
|
|
1794
|
+
callbackServer.close();
|
|
1795
|
+
waitingSpinner.fail(pc7.red("Login failed"));
|
|
1796
|
+
if (error instanceof Error) {
|
|
1797
|
+
console.error(pc7.red(error.message));
|
|
1798
|
+
}
|
|
1799
|
+
process.exit(1);
|
|
1800
|
+
}
|
|
1801
|
+
} catch (error) {
|
|
1802
|
+
spinner.fail(pc7.red("Login failed"));
|
|
1803
|
+
if (error instanceof Error) {
|
|
1804
|
+
console.error(pc7.red(error.message));
|
|
1805
|
+
}
|
|
1806
|
+
process.exit(1);
|
|
1807
|
+
}
|
|
1808
|
+
}
|
|
1809
|
+
function logoutCommand() {
|
|
1810
|
+
trackEvent("command", { name: "logout" });
|
|
1811
|
+
if (clearTokens()) {
|
|
1812
|
+
console.log(pc7.green("Logged out successfully."));
|
|
1813
|
+
} else {
|
|
1814
|
+
console.log(pc7.yellow("You are not logged in."));
|
|
1815
|
+
}
|
|
1816
|
+
}
|
|
1817
|
+
async function whoamiCommand() {
|
|
1818
|
+
trackEvent("command", { name: "whoami" });
|
|
1819
|
+
const tokens = loadTokens();
|
|
1820
|
+
if (!tokens) {
|
|
1821
|
+
console.log(pc7.yellow("Not logged in."));
|
|
1822
|
+
console.log(pc7.dim("Run 'ctx7 login' to authenticate."));
|
|
1823
|
+
return;
|
|
1824
|
+
}
|
|
1825
|
+
console.log(pc7.green("Logged in"));
|
|
1826
|
+
try {
|
|
1827
|
+
const userInfo = await fetchUserInfo(tokens.access_token);
|
|
1828
|
+
if (userInfo.name) {
|
|
1829
|
+
console.log(`${pc7.dim("Name:".padEnd(9))}${userInfo.name}`);
|
|
1830
|
+
}
|
|
1831
|
+
if (userInfo.email) {
|
|
1832
|
+
console.log(`${pc7.dim("Email:".padEnd(9))}${userInfo.email}`);
|
|
1833
|
+
}
|
|
1834
|
+
} catch {
|
|
1835
|
+
if (isTokenExpired(tokens) && !tokens.refresh_token) {
|
|
1836
|
+
console.log(pc7.dim("(Session may be expired - run 'ctx7 login' to refresh)"));
|
|
1837
|
+
}
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
async function fetchUserInfo(accessToken) {
|
|
1841
|
+
const response = await fetch("https://clerk.context7.com/oauth/userinfo", {
|
|
1842
|
+
headers: {
|
|
1843
|
+
Authorization: `Bearer ${accessToken}`
|
|
1844
|
+
}
|
|
1845
|
+
});
|
|
1846
|
+
if (!response.ok) {
|
|
1847
|
+
throw new Error("Failed to fetch user info");
|
|
1848
|
+
}
|
|
1849
|
+
return await response.json();
|
|
1850
|
+
}
|
|
1851
|
+
|
|
921
1852
|
// src/constants.ts
|
|
922
|
-
import { readFileSync } from "fs";
|
|
1853
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
923
1854
|
import { fileURLToPath } from "url";
|
|
924
|
-
import { dirname as dirname2, join as
|
|
1855
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
925
1856
|
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
926
|
-
var pkg = JSON.parse(
|
|
1857
|
+
var pkg = JSON.parse(readFileSync2(join6(__dirname, "../package.json"), "utf-8"));
|
|
927
1858
|
var VERSION = pkg.version;
|
|
928
1859
|
var NAME = pkg.name;
|
|
929
1860
|
|
|
930
1861
|
// src/index.ts
|
|
931
1862
|
var brand = {
|
|
932
|
-
primary:
|
|
933
|
-
dim:
|
|
1863
|
+
primary: pc8.green,
|
|
1864
|
+
dim: pc8.dim
|
|
934
1865
|
};
|
|
935
1866
|
var program = new Command();
|
|
936
1867
|
program.name("ctx7").description("Context7 CLI - Manage AI coding skills and documentation context").version(VERSION).option("--base-url <url>").hook("preAction", (thisCommand) => {
|
|
937
1868
|
const opts = thisCommand.opts();
|
|
938
1869
|
if (opts.baseUrl) {
|
|
939
1870
|
setBaseUrl(opts.baseUrl);
|
|
1871
|
+
setAuthBaseUrl(opts.baseUrl);
|
|
940
1872
|
}
|
|
941
1873
|
}).addHelpText(
|
|
942
1874
|
"after",
|
|
@@ -963,6 +1895,7 @@ Visit ${brand.primary("https://context7.com")} to browse skills
|
|
|
963
1895
|
);
|
|
964
1896
|
registerSkillCommands(program);
|
|
965
1897
|
registerSkillAliases(program);
|
|
1898
|
+
registerAuthCommands(program);
|
|
966
1899
|
program.action(() => {
|
|
967
1900
|
console.log("");
|
|
968
1901
|
const banner = figlet.textSync("Context7", { font: "ANSI Shadow" });
|