ctx7 0.1.4 → 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/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/index.js +1112 -94
- package/dist/index.js.map +1 -1
- package/package.json +13 -12
package/dist/index.js
CHANGED
|
@@ -2,26 +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
|
|
11
|
-
import ora from "ora";
|
|
9
|
+
import pc6 from "picocolors";
|
|
10
|
+
import ora2 from "ora";
|
|
12
11
|
import { readdir, rm as rm2 } from "fs/promises";
|
|
13
|
-
import { join as
|
|
12
|
+
import { join as join5 } from "path";
|
|
14
13
|
|
|
15
14
|
// src/utils/parse-input.ts
|
|
16
|
-
function parseSkillInput(
|
|
17
|
-
const urlMatch =
|
|
15
|
+
function parseSkillInput(input2) {
|
|
16
|
+
const urlMatch = input2.match(
|
|
18
17
|
/(?:https?:\/\/)?github\.com\/([^\/]+)\/([^\/]+)\/tree\/([^\/]+)\/(.+)/
|
|
19
18
|
);
|
|
20
19
|
if (urlMatch) {
|
|
21
|
-
const [, owner, repo, branch,
|
|
22
|
-
return { type: "url", owner, repo, branch, path };
|
|
20
|
+
const [, owner, repo, branch, path2] = urlMatch;
|
|
21
|
+
return { type: "url", owner, repo, branch, path: path2 };
|
|
23
22
|
}
|
|
24
|
-
const shortMatch =
|
|
23
|
+
const shortMatch = input2.match(/^\/?([^\/]+)\/([^\/]+)$/);
|
|
25
24
|
if (shortMatch) {
|
|
26
25
|
const [, owner, repo] = shortMatch;
|
|
27
26
|
return { type: "repo", owner, repo };
|
|
@@ -46,24 +45,24 @@ function parseGitHubUrl(url) {
|
|
|
46
45
|
if (pathParts2.length > 0 && pathParts2[pathParts2.length - 1].includes(".")) {
|
|
47
46
|
pathParts2.pop();
|
|
48
47
|
}
|
|
49
|
-
const
|
|
50
|
-
return { owner, repo, branch: branch2, path:
|
|
48
|
+
const path3 = pathParts2.join("/");
|
|
49
|
+
return { owner, repo, branch: branch2, path: path3 };
|
|
51
50
|
}
|
|
52
51
|
const branch = parts[2];
|
|
53
52
|
const pathParts = parts.slice(3);
|
|
54
53
|
if (pathParts.length > 0 && pathParts[pathParts.length - 1].includes(".")) {
|
|
55
54
|
pathParts.pop();
|
|
56
55
|
}
|
|
57
|
-
const
|
|
58
|
-
return { owner, repo, branch, path };
|
|
56
|
+
const path2 = pathParts.join("/");
|
|
57
|
+
return { owner, repo, branch, path: path2 };
|
|
59
58
|
}
|
|
60
59
|
if (urlObj.hostname === "github.com") {
|
|
61
60
|
if (parts.length < 4 || parts[2] !== "tree") return null;
|
|
62
61
|
const owner = parts[0];
|
|
63
62
|
const repo = parts[1];
|
|
64
63
|
const branch = parts[3];
|
|
65
|
-
const
|
|
66
|
-
return { owner, repo, branch, path };
|
|
64
|
+
const path2 = parts.slice(4).join("/");
|
|
65
|
+
return { owner, repo, branch, path: path2 };
|
|
67
66
|
}
|
|
68
67
|
return null;
|
|
69
68
|
} catch {
|
|
@@ -166,6 +165,124 @@ async function downloadSkill(project, skillName) {
|
|
|
166
165
|
}
|
|
167
166
|
return { skill, files };
|
|
168
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
|
+
}
|
|
169
286
|
|
|
170
287
|
// src/utils/logger.ts
|
|
171
288
|
import pc from "picocolors";
|
|
@@ -315,7 +432,9 @@ ${pc2.dim(pathList)}`,
|
|
|
315
432
|
message: `Which clients do you want to install the skill(s) for?
|
|
316
433
|
${pc2.dim(baseDir)}`,
|
|
317
434
|
choices: ideChoices,
|
|
318
|
-
required: true
|
|
435
|
+
required: true,
|
|
436
|
+
loop: false,
|
|
437
|
+
theme: { style: { highlight: (text) => pc2.green(text) } }
|
|
319
438
|
});
|
|
320
439
|
} catch {
|
|
321
440
|
return null;
|
|
@@ -343,7 +462,9 @@ async function promptForSingleTarget(options) {
|
|
|
343
462
|
selectedIde = await select({
|
|
344
463
|
message: "Which client?",
|
|
345
464
|
choices: ideChoices,
|
|
346
|
-
default: DEFAULT_CONFIG.defaultIde
|
|
465
|
+
default: DEFAULT_CONFIG.defaultIde,
|
|
466
|
+
loop: false,
|
|
467
|
+
theme: { style: { highlight: (text) => pc2.green(text) } }
|
|
347
468
|
});
|
|
348
469
|
} catch {
|
|
349
470
|
return null;
|
|
@@ -365,7 +486,9 @@ async function promptForSingleTarget(options) {
|
|
|
365
486
|
value: "global"
|
|
366
487
|
}
|
|
367
488
|
],
|
|
368
|
-
default: DEFAULT_CONFIG.defaultScope
|
|
489
|
+
default: DEFAULT_CONFIG.defaultScope,
|
|
490
|
+
loop: false,
|
|
491
|
+
theme: { style: { highlight: (text) => pc2.green(text) } }
|
|
369
492
|
});
|
|
370
493
|
} catch {
|
|
371
494
|
return null;
|
|
@@ -398,6 +521,74 @@ function getTargetDirFromSelection(ide, scope) {
|
|
|
398
521
|
return join(process.cwd(), IDE_PATHS[ide]);
|
|
399
522
|
}
|
|
400
523
|
|
|
524
|
+
// src/utils/prompts.ts
|
|
525
|
+
import pc3 from "picocolors";
|
|
526
|
+
import { checkbox as checkbox2 } from "@inquirer/prompts";
|
|
527
|
+
import readline from "readline";
|
|
528
|
+
function terminalLink(text, url, color) {
|
|
529
|
+
const colorFn = color ?? ((s) => s);
|
|
530
|
+
return `\x1B]8;;${url}\x07${colorFn(text)}\x1B]8;;\x07`;
|
|
531
|
+
}
|
|
532
|
+
function formatInstallCount(count) {
|
|
533
|
+
if (count === void 0 || count === 0) return "";
|
|
534
|
+
let display;
|
|
535
|
+
if (count >= 1e3) {
|
|
536
|
+
display = `${Math.floor(count / 1e3)}k+`;
|
|
537
|
+
} else if (count >= 100) {
|
|
538
|
+
const hundreds = Math.floor(count / 100) * 100;
|
|
539
|
+
display = `${hundreds}+`;
|
|
540
|
+
} else if (count >= 10) {
|
|
541
|
+
const tens = Math.floor(count / 10) * 10;
|
|
542
|
+
display = `${tens}+`;
|
|
543
|
+
} else {
|
|
544
|
+
display = String(count);
|
|
545
|
+
}
|
|
546
|
+
return `\x1B[38;5;214m\u2193${display}\x1B[0m`;
|
|
547
|
+
}
|
|
548
|
+
async function checkboxWithHover(config, options) {
|
|
549
|
+
const choices = config.choices.filter(
|
|
550
|
+
(c) => typeof c === "object" && c !== null && !("type" in c && c.type === "separator")
|
|
551
|
+
);
|
|
552
|
+
const values = choices.map((c) => c.value);
|
|
553
|
+
const totalItems = values.length;
|
|
554
|
+
let cursorPosition = 0;
|
|
555
|
+
const getName = options?.getName ?? ((v) => v.name);
|
|
556
|
+
const keypressHandler = (_str, key) => {
|
|
557
|
+
if (key.name === "up" && cursorPosition > 0) {
|
|
558
|
+
cursorPosition--;
|
|
559
|
+
} else if (key.name === "down" && cursorPosition < totalItems - 1) {
|
|
560
|
+
cursorPosition++;
|
|
561
|
+
}
|
|
562
|
+
};
|
|
563
|
+
readline.emitKeypressEvents(process.stdin);
|
|
564
|
+
process.stdin.on("keypress", keypressHandler);
|
|
565
|
+
const customConfig = {
|
|
566
|
+
...config,
|
|
567
|
+
theme: {
|
|
568
|
+
...config.theme,
|
|
569
|
+
style: {
|
|
570
|
+
...config.theme?.style,
|
|
571
|
+
highlight: (text) => pc3.green(text),
|
|
572
|
+
renderSelectedChoices: (selected, _allChoices) => {
|
|
573
|
+
if (selected.length === 0) {
|
|
574
|
+
return pc3.dim(getName(values[cursorPosition]));
|
|
575
|
+
}
|
|
576
|
+
return selected.map((c) => getName(c.value)).join(", ");
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
};
|
|
581
|
+
try {
|
|
582
|
+
const selected = await checkbox2(customConfig);
|
|
583
|
+
if (selected.length === 0) {
|
|
584
|
+
return [values[cursorPosition]];
|
|
585
|
+
}
|
|
586
|
+
return selected;
|
|
587
|
+
} finally {
|
|
588
|
+
process.stdin.removeListener("keypress", keypressHandler);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
|
|
401
592
|
// src/utils/installer.ts
|
|
402
593
|
import { mkdir, writeFile, rm, symlink, lstat } from "fs/promises";
|
|
403
594
|
import { join as join2 } from "path";
|
|
@@ -423,7 +614,670 @@ async function symlinkSkill(skillName, sourcePath, targetDir) {
|
|
|
423
614
|
await symlink(sourcePath, targetPath);
|
|
424
615
|
}
|
|
425
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
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
|
+
|
|
426
1279
|
// src/commands/skill.ts
|
|
1280
|
+
import { homedir as homedir4 } from "os";
|
|
427
1281
|
function logInstallSummary(targets, targetDirs, skillNames) {
|
|
428
1282
|
log.blank();
|
|
429
1283
|
let dirIndex = 0;
|
|
@@ -440,6 +1294,7 @@ function logInstallSummary(targets, targetDirs, skillNames) {
|
|
|
440
1294
|
}
|
|
441
1295
|
function registerSkillCommands(program2) {
|
|
442
1296
|
const skill = program2.command("skills").alias("skill").description("Manage AI coding skills");
|
|
1297
|
+
registerGenerateCommand(skill);
|
|
443
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) => {
|
|
444
1299
|
await installCommand(project, skillName, options);
|
|
445
1300
|
});
|
|
@@ -464,10 +1319,10 @@ function registerSkillAliases(program2) {
|
|
|
464
1319
|
await searchCommand(keywords.join(" "));
|
|
465
1320
|
});
|
|
466
1321
|
}
|
|
467
|
-
async function installCommand(
|
|
468
|
-
const parsed = parseSkillInput(
|
|
1322
|
+
async function installCommand(input2, skillName, options) {
|
|
1323
|
+
const parsed = parseSkillInput(input2);
|
|
469
1324
|
if (!parsed) {
|
|
470
|
-
log.error(`Invalid input format: ${
|
|
1325
|
+
log.error(`Invalid input format: ${input2}`);
|
|
471
1326
|
log.info(`Expected: /owner/repo or full GitHub URL`);
|
|
472
1327
|
log.info(`Example: ctx7 skills install /anthropics/skills pdf`);
|
|
473
1328
|
log.blank();
|
|
@@ -475,17 +1330,17 @@ async function installCommand(input, skillName, options) {
|
|
|
475
1330
|
}
|
|
476
1331
|
const repo = `/${parsed.owner}/${parsed.repo}`;
|
|
477
1332
|
log.blank();
|
|
478
|
-
const spinner =
|
|
1333
|
+
const spinner = ora2(`Fetching skills from ${repo}...`).start();
|
|
479
1334
|
let selectedSkills;
|
|
480
1335
|
if (skillName) {
|
|
481
1336
|
spinner.text = `Fetching skill: ${skillName}...`;
|
|
482
1337
|
const skillData = await getSkill(repo, skillName);
|
|
483
1338
|
if (skillData.error || !skillData.name) {
|
|
484
1339
|
if (skillData.error === "prompt_injection_detected") {
|
|
485
|
-
spinner.fail(
|
|
1340
|
+
spinner.fail(pc6.red(`Prompt injection detected in skill: ${skillName}`));
|
|
486
1341
|
log.warn("This skill contains potentially malicious content and cannot be installed.");
|
|
487
1342
|
} else {
|
|
488
|
-
spinner.fail(
|
|
1343
|
+
spinner.fail(pc6.red(`Skill not found: ${skillName}`));
|
|
489
1344
|
}
|
|
490
1345
|
return;
|
|
491
1346
|
}
|
|
@@ -501,11 +1356,11 @@ async function installCommand(input, skillName, options) {
|
|
|
501
1356
|
} else {
|
|
502
1357
|
const data = await listProjectSkills(repo);
|
|
503
1358
|
if (data.error) {
|
|
504
|
-
spinner.fail(
|
|
1359
|
+
spinner.fail(pc6.red(`Error: ${data.message || data.error}`));
|
|
505
1360
|
return;
|
|
506
1361
|
}
|
|
507
1362
|
if (!data.skills || data.skills.length === 0) {
|
|
508
|
-
spinner.warn(
|
|
1363
|
+
spinner.warn(pc6.yellow(`No skills found in ${repo}`));
|
|
509
1364
|
return;
|
|
510
1365
|
}
|
|
511
1366
|
const skillsWithRepo = data.skills.map((s) => ({ ...s, project: repo }));
|
|
@@ -520,26 +1375,36 @@ async function installCommand(input, skillName, options) {
|
|
|
520
1375
|
if (options.all || data.skills.length === 1) {
|
|
521
1376
|
selectedSkills = skillsWithRepo;
|
|
522
1377
|
} else {
|
|
523
|
-
const
|
|
524
|
-
const
|
|
1378
|
+
const indexWidth = data.skills.length.toString().length;
|
|
1379
|
+
const maxNameLen = Math.max(...data.skills.map((s) => s.name.length));
|
|
1380
|
+
const choices = skillsWithRepo.map((s, index) => {
|
|
1381
|
+
const indexStr = pc6.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
|
|
525
1382
|
const paddedName = s.name.padEnd(maxNameLen);
|
|
526
|
-
const
|
|
1383
|
+
const installs = formatInstallCount(s.installCount);
|
|
1384
|
+
const skillUrl = `https://context7.com/skills${s.project}/${s.name}`;
|
|
1385
|
+
const skillLink = terminalLink(s.name, skillUrl, pc6.white);
|
|
1386
|
+
const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc6.white);
|
|
1387
|
+
const metadataLines = [
|
|
1388
|
+
pc6.dim("\u2500".repeat(50)),
|
|
1389
|
+
"",
|
|
1390
|
+
`${pc6.yellow("Skill:")} ${skillLink}`,
|
|
1391
|
+
`${pc6.yellow("Repo:")} ${repoLink}`,
|
|
1392
|
+
`${pc6.yellow("Description:")}`,
|
|
1393
|
+
pc6.white(s.description || "No description")
|
|
1394
|
+
];
|
|
527
1395
|
return {
|
|
528
|
-
name:
|
|
529
|
-
value: s
|
|
1396
|
+
name: installs ? `${indexStr} ${paddedName} ${installs}` : `${indexStr} ${paddedName}`,
|
|
1397
|
+
value: s,
|
|
1398
|
+
description: metadataLines.join("\n")
|
|
530
1399
|
};
|
|
531
1400
|
});
|
|
532
1401
|
log.blank();
|
|
533
1402
|
try {
|
|
534
|
-
selectedSkills = await
|
|
1403
|
+
selectedSkills = await checkboxWithHover({
|
|
535
1404
|
message: "Select skills:",
|
|
536
1405
|
choices,
|
|
537
1406
|
pageSize: 15,
|
|
538
|
-
|
|
539
|
-
style: {
|
|
540
|
-
renderSelectedChoices: (selected) => selected.map((c) => c.value.name).join(", ")
|
|
541
|
-
}
|
|
542
|
-
}
|
|
1407
|
+
loop: false
|
|
543
1408
|
});
|
|
544
1409
|
} catch {
|
|
545
1410
|
log.warn("Installation cancelled");
|
|
@@ -557,7 +1422,7 @@ async function installCommand(input, skillName, options) {
|
|
|
557
1422
|
return;
|
|
558
1423
|
}
|
|
559
1424
|
const targetDirs = getTargetDirs(targets);
|
|
560
|
-
const installSpinner =
|
|
1425
|
+
const installSpinner = ora2("Installing skills...").start();
|
|
561
1426
|
let permissionError = false;
|
|
562
1427
|
const failedDirs = /* @__PURE__ */ new Set();
|
|
563
1428
|
const installedSkills = [];
|
|
@@ -581,7 +1446,7 @@ async function installCommand(input, skillName, options) {
|
|
|
581
1446
|
}
|
|
582
1447
|
throw dirErr;
|
|
583
1448
|
}
|
|
584
|
-
const primarySkillDir =
|
|
1449
|
+
const primarySkillDir = join5(primaryDir, skill.name);
|
|
585
1450
|
for (const targetDir of symlinkDirs) {
|
|
586
1451
|
try {
|
|
587
1452
|
await symlinkSkill(skill.name, primarySkillDir, targetDir);
|
|
@@ -609,7 +1474,7 @@ async function installCommand(input, skillName, options) {
|
|
|
609
1474
|
log.blank();
|
|
610
1475
|
log.warn("Fix permissions with:");
|
|
611
1476
|
for (const dir of failedDirs) {
|
|
612
|
-
const parentDir =
|
|
1477
|
+
const parentDir = join5(dir, "..");
|
|
613
1478
|
log.dim(` sudo chown -R $(whoami) "${parentDir}"`);
|
|
614
1479
|
}
|
|
615
1480
|
log.blank();
|
|
@@ -622,45 +1487,57 @@ async function installCommand(input, skillName, options) {
|
|
|
622
1487
|
}
|
|
623
1488
|
async function searchCommand(query) {
|
|
624
1489
|
log.blank();
|
|
625
|
-
const spinner =
|
|
1490
|
+
const spinner = ora2(`Searching for "${query}"...`).start();
|
|
626
1491
|
let data;
|
|
627
1492
|
try {
|
|
628
1493
|
data = await searchSkills(query);
|
|
629
1494
|
} catch (err) {
|
|
630
|
-
spinner.fail(
|
|
1495
|
+
spinner.fail(pc6.red(`Error: ${err instanceof Error ? err.message : String(err)}`));
|
|
631
1496
|
return;
|
|
632
1497
|
}
|
|
633
1498
|
if (data.error) {
|
|
634
|
-
spinner.fail(
|
|
1499
|
+
spinner.fail(pc6.red(`Error: ${data.message || data.error}`));
|
|
635
1500
|
return;
|
|
636
1501
|
}
|
|
637
1502
|
if (!data.results || data.results.length === 0) {
|
|
638
|
-
spinner.warn(
|
|
1503
|
+
spinner.warn(pc6.yellow(`No skills found matching "${query}"`));
|
|
639
1504
|
return;
|
|
640
1505
|
}
|
|
641
1506
|
spinner.succeed(`Found ${data.results.length} skill(s)`);
|
|
642
|
-
const
|
|
643
|
-
const
|
|
1507
|
+
const indexWidth = data.results.length.toString().length;
|
|
1508
|
+
const maxNameLen = Math.max(...data.results.map((s) => s.name.length));
|
|
1509
|
+
const choices = data.results.map((s, index) => {
|
|
1510
|
+
const indexStr = pc6.dim(`${(index + 1).toString().padStart(indexWidth)}.`);
|
|
644
1511
|
const paddedName = s.name.padEnd(maxNameLen);
|
|
645
|
-
const
|
|
646
|
-
const
|
|
1512
|
+
const installs = formatInstallCount(s.installCount);
|
|
1513
|
+
const skillLink = terminalLink(
|
|
1514
|
+
s.name,
|
|
1515
|
+
`https://context7.com/skills${s.project}/${s.name}`,
|
|
1516
|
+
pc6.white
|
|
1517
|
+
);
|
|
1518
|
+
const repoLink = terminalLink(s.project, `https://github.com${s.project}`, pc6.white);
|
|
1519
|
+
const metadataLines = [
|
|
1520
|
+
pc6.dim("\u2500".repeat(50)),
|
|
1521
|
+
"",
|
|
1522
|
+
`${pc6.yellow("Skill:")} ${skillLink}`,
|
|
1523
|
+
`${pc6.yellow("Repo:")} ${repoLink}`,
|
|
1524
|
+
`${pc6.yellow("Description:")}`,
|
|
1525
|
+
pc6.white(s.description || "No description")
|
|
1526
|
+
];
|
|
647
1527
|
return {
|
|
648
|
-
name:
|
|
649
|
-
value: s
|
|
1528
|
+
name: installs ? `${indexStr} ${paddedName} ${installs}` : `${indexStr} ${paddedName}`,
|
|
1529
|
+
value: s,
|
|
1530
|
+
description: metadataLines.join("\n")
|
|
650
1531
|
};
|
|
651
1532
|
});
|
|
652
1533
|
log.blank();
|
|
653
1534
|
let selectedSkills;
|
|
654
1535
|
try {
|
|
655
|
-
selectedSkills = await
|
|
1536
|
+
selectedSkills = await checkboxWithHover({
|
|
656
1537
|
message: "Select skills to install:",
|
|
657
1538
|
choices,
|
|
658
1539
|
pageSize: 15,
|
|
659
|
-
|
|
660
|
-
style: {
|
|
661
|
-
renderSelectedChoices: (selected) => selected.map((c) => c.value.name).join(", ")
|
|
662
|
-
}
|
|
663
|
-
}
|
|
1540
|
+
loop: false
|
|
664
1541
|
});
|
|
665
1542
|
} catch {
|
|
666
1543
|
log.warn("Installation cancelled");
|
|
@@ -677,7 +1554,7 @@ async function searchCommand(query) {
|
|
|
677
1554
|
return;
|
|
678
1555
|
}
|
|
679
1556
|
const targetDirs = getTargetDirs(targets);
|
|
680
|
-
const installSpinner =
|
|
1557
|
+
const installSpinner = ora2("Installing skills...").start();
|
|
681
1558
|
let permissionError = false;
|
|
682
1559
|
const failedDirs = /* @__PURE__ */ new Set();
|
|
683
1560
|
const installedSkills = [];
|
|
@@ -701,7 +1578,7 @@ async function searchCommand(query) {
|
|
|
701
1578
|
}
|
|
702
1579
|
throw dirErr;
|
|
703
1580
|
}
|
|
704
|
-
const primarySkillDir =
|
|
1581
|
+
const primarySkillDir = join5(primaryDir, skill.name);
|
|
705
1582
|
for (const targetDir of symlinkDirs) {
|
|
706
1583
|
try {
|
|
707
1584
|
await symlinkSkill(skill.name, primarySkillDir, targetDir);
|
|
@@ -729,7 +1606,7 @@ async function searchCommand(query) {
|
|
|
729
1606
|
log.blank();
|
|
730
1607
|
log.warn("Fix permissions with:");
|
|
731
1608
|
for (const dir of failedDirs) {
|
|
732
|
-
const parentDir =
|
|
1609
|
+
const parentDir = join5(dir, "..");
|
|
733
1610
|
log.dim(` sudo chown -R $(whoami) "${parentDir}"`);
|
|
734
1611
|
}
|
|
735
1612
|
log.blank();
|
|
@@ -741,28 +1618,35 @@ async function searchCommand(query) {
|
|
|
741
1618
|
logInstallSummary(targets, targetDirs, installedNames);
|
|
742
1619
|
}
|
|
743
1620
|
async function listCommand(options) {
|
|
744
|
-
const
|
|
745
|
-
|
|
746
|
-
|
|
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");
|
|
747
1639
|
return;
|
|
748
1640
|
}
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
const
|
|
752
|
-
const
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
}
|
|
757
|
-
log.info(`
|
|
758
|
-
\u25C6 Installed skills (${skillsDir}):`);
|
|
759
|
-
for (const folder of skillFolders) {
|
|
760
|
-
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)}`);
|
|
761
1648
|
}
|
|
762
|
-
log.
|
|
763
|
-
`);
|
|
764
|
-
} catch {
|
|
765
|
-
log.warn(`No skills directory found at ${skillsDir}`);
|
|
1649
|
+
log.blank();
|
|
766
1650
|
}
|
|
767
1651
|
}
|
|
768
1652
|
async function removeCommand(name, options) {
|
|
@@ -772,7 +1656,7 @@ async function removeCommand(name, options) {
|
|
|
772
1656
|
return;
|
|
773
1657
|
}
|
|
774
1658
|
const skillsDir = getTargetDirFromSelection(target.ide, target.scope);
|
|
775
|
-
const skillPath =
|
|
1659
|
+
const skillPath = join5(skillsDir, name);
|
|
776
1660
|
try {
|
|
777
1661
|
await rm2(skillPath, { recursive: true });
|
|
778
1662
|
log.success(`Removed skill: ${name}`);
|
|
@@ -787,25 +1671,25 @@ async function removeCommand(name, options) {
|
|
|
787
1671
|
}
|
|
788
1672
|
}
|
|
789
1673
|
}
|
|
790
|
-
async function infoCommand(
|
|
791
|
-
const parsed = parseSkillInput(
|
|
1674
|
+
async function infoCommand(input2) {
|
|
1675
|
+
const parsed = parseSkillInput(input2);
|
|
792
1676
|
if (!parsed) {
|
|
793
1677
|
log.blank();
|
|
794
|
-
log.error(`Invalid input format: ${
|
|
1678
|
+
log.error(`Invalid input format: ${input2}`);
|
|
795
1679
|
log.info(`Expected: /owner/repo or full GitHub URL`);
|
|
796
1680
|
log.blank();
|
|
797
1681
|
return;
|
|
798
1682
|
}
|
|
799
1683
|
const repo = `/${parsed.owner}/${parsed.repo}`;
|
|
800
1684
|
log.blank();
|
|
801
|
-
const spinner =
|
|
1685
|
+
const spinner = ora2(`Fetching skills from ${repo}...`).start();
|
|
802
1686
|
const data = await listProjectSkills(repo);
|
|
803
1687
|
if (data.error) {
|
|
804
|
-
spinner.fail(
|
|
1688
|
+
spinner.fail(pc6.red(`Error: ${data.message || data.error}`));
|
|
805
1689
|
return;
|
|
806
1690
|
}
|
|
807
1691
|
if (!data.skills || data.skills.length === 0) {
|
|
808
|
-
spinner.warn(
|
|
1692
|
+
spinner.warn(pc6.yellow(`No skills found in ${repo}`));
|
|
809
1693
|
return;
|
|
810
1694
|
}
|
|
811
1695
|
spinner.succeed(`Found ${data.skills.length} skill(s)`);
|
|
@@ -817,32 +1701,165 @@ async function infoCommand(input) {
|
|
|
817
1701
|
log.blank();
|
|
818
1702
|
}
|
|
819
1703
|
log.plain(
|
|
820
|
-
`${
|
|
821
|
-
Install all: ${
|
|
822
|
-
Install one: ${
|
|
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}`)}
|
|
823
1707
|
`
|
|
824
1708
|
);
|
|
825
1709
|
}
|
|
826
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
|
+
|
|
827
1843
|
// src/constants.ts
|
|
828
|
-
import { readFileSync } from "fs";
|
|
1844
|
+
import { readFileSync as readFileSync2 } from "fs";
|
|
829
1845
|
import { fileURLToPath } from "url";
|
|
830
|
-
import { dirname as dirname2, join as
|
|
1846
|
+
import { dirname as dirname2, join as join6 } from "path";
|
|
831
1847
|
var __dirname = dirname2(fileURLToPath(import.meta.url));
|
|
832
|
-
var pkg = JSON.parse(
|
|
1848
|
+
var pkg = JSON.parse(readFileSync2(join6(__dirname, "../package.json"), "utf-8"));
|
|
833
1849
|
var VERSION = pkg.version;
|
|
834
1850
|
var NAME = pkg.name;
|
|
835
1851
|
|
|
836
1852
|
// src/index.ts
|
|
837
1853
|
var brand = {
|
|
838
|
-
primary:
|
|
839
|
-
dim:
|
|
1854
|
+
primary: pc8.green,
|
|
1855
|
+
dim: pc8.dim
|
|
840
1856
|
};
|
|
841
1857
|
var program = new Command();
|
|
842
1858
|
program.name("ctx7").description("Context7 CLI - Manage AI coding skills and documentation context").version(VERSION).option("--base-url <url>").hook("preAction", (thisCommand) => {
|
|
843
1859
|
const opts = thisCommand.opts();
|
|
844
1860
|
if (opts.baseUrl) {
|
|
845
1861
|
setBaseUrl(opts.baseUrl);
|
|
1862
|
+
setAuthBaseUrl(opts.baseUrl);
|
|
846
1863
|
}
|
|
847
1864
|
}).addHelpText(
|
|
848
1865
|
"after",
|
|
@@ -869,6 +1886,7 @@ Visit ${brand.primary("https://context7.com")} to browse skills
|
|
|
869
1886
|
);
|
|
870
1887
|
registerSkillCommands(program);
|
|
871
1888
|
registerSkillAliases(program);
|
|
1889
|
+
registerAuthCommands(program);
|
|
872
1890
|
program.action(() => {
|
|
873
1891
|
console.log("");
|
|
874
1892
|
const banner = figlet.textSync("Context7", { font: "ANSI Shadow" });
|