ctx7 0.5.0 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +180 -262
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -925,58 +925,153 @@ function trackEvent(event, data) {
|
|
|
925
925
|
// src/commands/generate.ts
|
|
926
926
|
import pc6 from "picocolors";
|
|
927
927
|
import ora2 from "ora";
|
|
928
|
-
import { mkdir as
|
|
928
|
+
import { mkdir as mkdir3, writeFile as writeFile2, readFile, unlink } from "fs/promises";
|
|
929
929
|
import { join as join4 } from "path";
|
|
930
930
|
import { homedir as homedir3 } from "os";
|
|
931
931
|
import { spawn } from "child_process";
|
|
932
932
|
import { input, select as select2 } from "@inquirer/prompts";
|
|
933
933
|
|
|
934
934
|
// src/utils/auth.ts
|
|
935
|
-
import * as
|
|
936
|
-
import * as
|
|
935
|
+
import * as fs2 from "fs";
|
|
936
|
+
import * as os2 from "os";
|
|
937
|
+
|
|
938
|
+
// src/utils/storage-paths.ts
|
|
937
939
|
import * as fs from "fs";
|
|
938
|
-
import
|
|
940
|
+
import { access as access2, chmod, mkdir as mkdir2, rename } from "fs/promises";
|
|
939
941
|
import * as os from "os";
|
|
940
|
-
|
|
941
|
-
var
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
942
|
+
import * as path from "path";
|
|
943
|
+
var APP_DIR = "context7";
|
|
944
|
+
var LEGACY_DIR = ".context7";
|
|
945
|
+
var CREDENTIALS_FILE_NAME = "credentials.json";
|
|
946
|
+
var UPDATE_STATE_FILE_NAME = "cli-state.json";
|
|
947
|
+
var PREVIEWS_DIR_NAME = "previews";
|
|
948
|
+
function xdgBase(envVar, ...defaultSegments) {
|
|
949
|
+
const value = process.env[envVar];
|
|
950
|
+
const base = value && path.isAbsolute(value) ? value : path.join(os.homedir(), ...defaultSegments);
|
|
951
|
+
return path.join(base, APP_DIR);
|
|
952
|
+
}
|
|
953
|
+
function getConfigDir() {
|
|
954
|
+
return xdgBase("XDG_CONFIG_HOME", ".config");
|
|
955
|
+
}
|
|
956
|
+
function getStateDir() {
|
|
957
|
+
return xdgBase("XDG_STATE_HOME", ".local", "state");
|
|
958
|
+
}
|
|
959
|
+
function getCacheDir() {
|
|
960
|
+
return xdgBase("XDG_CACHE_HOME", ".cache");
|
|
961
|
+
}
|
|
962
|
+
function getCredentialsFilePath() {
|
|
963
|
+
return path.join(getConfigDir(), CREDENTIALS_FILE_NAME);
|
|
964
|
+
}
|
|
965
|
+
function getUpdateStateFilePath() {
|
|
966
|
+
return path.join(getStateDir(), UPDATE_STATE_FILE_NAME);
|
|
967
|
+
}
|
|
968
|
+
function getPreviewsDir() {
|
|
969
|
+
return path.join(getCacheDir(), PREVIEWS_DIR_NAME);
|
|
970
|
+
}
|
|
971
|
+
function getLegacyFilePath(fileName) {
|
|
972
|
+
return path.join(os.homedir(), LEGACY_DIR, fileName);
|
|
973
|
+
}
|
|
974
|
+
function migrateLegacyFileSync(fileName, targetPath, mode) {
|
|
975
|
+
const legacyPath = getLegacyFilePath(fileName);
|
|
976
|
+
if (legacyPath === targetPath || fs.existsSync(targetPath) || !fs.existsSync(legacyPath)) {
|
|
977
|
+
return;
|
|
978
|
+
}
|
|
979
|
+
try {
|
|
980
|
+
fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: 448 });
|
|
981
|
+
fs.renameSync(legacyPath, targetPath);
|
|
982
|
+
if (mode !== void 0) {
|
|
983
|
+
fs.chmodSync(targetPath, mode);
|
|
984
|
+
}
|
|
985
|
+
} catch {
|
|
986
|
+
}
|
|
987
|
+
}
|
|
988
|
+
async function migrateLegacyFile(fileName, targetPath, mode) {
|
|
989
|
+
const legacyPath = getLegacyFilePath(fileName);
|
|
990
|
+
if (legacyPath === targetPath || await exists(targetPath) || !await exists(legacyPath)) {
|
|
991
|
+
return;
|
|
992
|
+
}
|
|
993
|
+
try {
|
|
994
|
+
await mkdir2(path.dirname(targetPath), { recursive: true, mode: 448 });
|
|
995
|
+
await rename(legacyPath, targetPath);
|
|
996
|
+
if (mode !== void 0) {
|
|
997
|
+
await chmod(targetPath, mode);
|
|
998
|
+
}
|
|
999
|
+
} catch {
|
|
1000
|
+
}
|
|
946
1001
|
}
|
|
947
|
-
function
|
|
948
|
-
|
|
1002
|
+
function resolveReadPathSync(fileName, targetPath, mode) {
|
|
1003
|
+
migrateLegacyFileSync(fileName, targetPath, mode);
|
|
1004
|
+
if (fs.existsSync(targetPath)) {
|
|
1005
|
+
return targetPath;
|
|
1006
|
+
}
|
|
1007
|
+
const legacyPath = getLegacyFilePath(fileName);
|
|
1008
|
+
return fs.existsSync(legacyPath) ? legacyPath : targetPath;
|
|
949
1009
|
}
|
|
1010
|
+
async function resolveReadPath(fileName, targetPath, mode) {
|
|
1011
|
+
await migrateLegacyFile(fileName, targetPath, mode);
|
|
1012
|
+
if (await exists(targetPath)) {
|
|
1013
|
+
return targetPath;
|
|
1014
|
+
}
|
|
1015
|
+
const legacyPath = getLegacyFilePath(fileName);
|
|
1016
|
+
return await exists(legacyPath) ? legacyPath : targetPath;
|
|
1017
|
+
}
|
|
1018
|
+
async function exists(filePath) {
|
|
1019
|
+
try {
|
|
1020
|
+
await access2(filePath);
|
|
1021
|
+
return true;
|
|
1022
|
+
} catch {
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// src/utils/auth.ts
|
|
950
1028
|
function ensureConfigDir() {
|
|
951
|
-
|
|
952
|
-
|
|
1029
|
+
const configDir = getConfigDir();
|
|
1030
|
+
if (!fs2.existsSync(configDir)) {
|
|
1031
|
+
fs2.mkdirSync(configDir, { recursive: true, mode: 448 });
|
|
953
1032
|
}
|
|
954
1033
|
}
|
|
1034
|
+
var CREDENTIALS_MODE = 384;
|
|
955
1035
|
function saveTokens(tokens) {
|
|
1036
|
+
const credentialsFile = getCredentialsFilePath();
|
|
1037
|
+
migrateLegacyFileSync(CREDENTIALS_FILE_NAME, credentialsFile, CREDENTIALS_MODE);
|
|
956
1038
|
ensureConfigDir();
|
|
957
1039
|
const data = {
|
|
958
1040
|
...tokens,
|
|
959
1041
|
expires_at: tokens.expires_at ?? (tokens.expires_in ? Date.now() + tokens.expires_in * 1e3 : void 0)
|
|
960
1042
|
};
|
|
961
|
-
|
|
1043
|
+
fs2.writeFileSync(credentialsFile, JSON.stringify(data, null, 2), { mode: CREDENTIALS_MODE });
|
|
1044
|
+
fs2.chmodSync(credentialsFile, CREDENTIALS_MODE);
|
|
962
1045
|
}
|
|
963
1046
|
function loadTokens() {
|
|
964
|
-
|
|
1047
|
+
const credentialsFile = resolveReadPathSync(
|
|
1048
|
+
CREDENTIALS_FILE_NAME,
|
|
1049
|
+
getCredentialsFilePath(),
|
|
1050
|
+
CREDENTIALS_MODE
|
|
1051
|
+
);
|
|
1052
|
+
if (!fs2.existsSync(credentialsFile)) {
|
|
965
1053
|
return null;
|
|
966
1054
|
}
|
|
967
1055
|
try {
|
|
968
|
-
const data = JSON.parse(
|
|
1056
|
+
const data = JSON.parse(fs2.readFileSync(credentialsFile, "utf-8"));
|
|
969
1057
|
return data;
|
|
970
1058
|
} catch {
|
|
971
1059
|
return null;
|
|
972
1060
|
}
|
|
973
1061
|
}
|
|
974
1062
|
function clearTokens() {
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
1063
|
+
const credentialsFile = getCredentialsFilePath();
|
|
1064
|
+
let removed = false;
|
|
1065
|
+
if (fs2.existsSync(credentialsFile)) {
|
|
1066
|
+
fs2.unlinkSync(credentialsFile);
|
|
1067
|
+
removed = true;
|
|
978
1068
|
}
|
|
979
|
-
|
|
1069
|
+
const legacyCredentialsFile = getLegacyFilePath(CREDENTIALS_FILE_NAME);
|
|
1070
|
+
if (fs2.existsSync(legacyCredentialsFile)) {
|
|
1071
|
+
fs2.unlinkSync(legacyCredentialsFile);
|
|
1072
|
+
removed = true;
|
|
1073
|
+
}
|
|
1074
|
+
return removed;
|
|
980
1075
|
}
|
|
981
1076
|
function isTokenExpired(tokens) {
|
|
982
1077
|
if (!tokens.expires_at) {
|
|
@@ -1017,157 +1112,12 @@ async function getValidAccessToken() {
|
|
|
1017
1112
|
return null;
|
|
1018
1113
|
}
|
|
1019
1114
|
}
|
|
1020
|
-
var CALLBACK_PORT = 52417;
|
|
1021
|
-
function createCallbackServer(expectedState) {
|
|
1022
|
-
let resolvePort;
|
|
1023
|
-
let resolveResult;
|
|
1024
|
-
let rejectResult;
|
|
1025
|
-
let serverInstance = null;
|
|
1026
|
-
const portPromise = new Promise((resolve3) => {
|
|
1027
|
-
resolvePort = resolve3;
|
|
1028
|
-
});
|
|
1029
|
-
const resultPromise = new Promise((resolve3, reject) => {
|
|
1030
|
-
resolveResult = resolve3;
|
|
1031
|
-
rejectResult = reject;
|
|
1032
|
-
});
|
|
1033
|
-
const server = http.createServer((req, res) => {
|
|
1034
|
-
const url = new URL(req.url || "/", `http://localhost`);
|
|
1035
|
-
if (url.pathname === "/callback") {
|
|
1036
|
-
const code = url.searchParams.get("code");
|
|
1037
|
-
const state = url.searchParams.get("state");
|
|
1038
|
-
const error = url.searchParams.get("error");
|
|
1039
|
-
const errorDescription = url.searchParams.get("error_description");
|
|
1040
|
-
res.writeHead(200, { "Content-Type": "text/html" });
|
|
1041
|
-
if (error) {
|
|
1042
|
-
res.end(errorPage(errorDescription || error));
|
|
1043
|
-
serverInstance?.close();
|
|
1044
|
-
rejectResult(new Error(errorDescription || error));
|
|
1045
|
-
return;
|
|
1046
|
-
}
|
|
1047
|
-
if (!code || !state) {
|
|
1048
|
-
res.end(errorPage("Missing authorization code or state"));
|
|
1049
|
-
serverInstance?.close();
|
|
1050
|
-
rejectResult(new Error("Missing authorization code or state"));
|
|
1051
|
-
return;
|
|
1052
|
-
}
|
|
1053
|
-
if (state !== expectedState) {
|
|
1054
|
-
res.end(errorPage("State mismatch - possible CSRF attack"));
|
|
1055
|
-
serverInstance?.close();
|
|
1056
|
-
rejectResult(new Error("State mismatch"));
|
|
1057
|
-
return;
|
|
1058
|
-
}
|
|
1059
|
-
res.end(successPage());
|
|
1060
|
-
serverInstance?.close();
|
|
1061
|
-
resolveResult({ code, state });
|
|
1062
|
-
} else {
|
|
1063
|
-
res.writeHead(404);
|
|
1064
|
-
res.end("Not found");
|
|
1065
|
-
}
|
|
1066
|
-
});
|
|
1067
|
-
serverInstance = server;
|
|
1068
|
-
server.on("error", (err) => {
|
|
1069
|
-
rejectResult(err);
|
|
1070
|
-
});
|
|
1071
|
-
server.listen(CALLBACK_PORT, "127.0.0.1", () => {
|
|
1072
|
-
resolvePort(CALLBACK_PORT);
|
|
1073
|
-
});
|
|
1074
|
-
const timeout = setTimeout(
|
|
1075
|
-
() => {
|
|
1076
|
-
server.close();
|
|
1077
|
-
rejectResult(new Error("Login timed out after 5 minutes"));
|
|
1078
|
-
},
|
|
1079
|
-
5 * 60 * 1e3
|
|
1080
|
-
);
|
|
1081
|
-
return {
|
|
1082
|
-
port: portPromise,
|
|
1083
|
-
result: resultPromise,
|
|
1084
|
-
close: () => {
|
|
1085
|
-
clearTimeout(timeout);
|
|
1086
|
-
server.close();
|
|
1087
|
-
}
|
|
1088
|
-
};
|
|
1089
|
-
}
|
|
1090
|
-
function successPage() {
|
|
1091
|
-
return `<!DOCTYPE html>
|
|
1092
|
-
<html>
|
|
1093
|
-
<head><title>Login Successful</title></head>
|
|
1094
|
-
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f9fafb;">
|
|
1095
|
-
<div style="text-align: center; padding: 2rem;">
|
|
1096
|
-
<div style="width: 64px; height: 64px; background: #16a34a; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem;">
|
|
1097
|
-
<svg width="32" height="32" fill="none" stroke="white" stroke-width="3" viewBox="0 0 24 24">
|
|
1098
|
-
<path d="M5 13l4 4L19 7" stroke-linecap="round" stroke-linejoin="round"/>
|
|
1099
|
-
</svg>
|
|
1100
|
-
</div>
|
|
1101
|
-
<h1 style="color: #16a34a; margin: 0 0 0.5rem;">Login Successful!</h1>
|
|
1102
|
-
<p style="color: #6b7280; margin: 0;">You can close this window and return to the terminal.</p>
|
|
1103
|
-
</div>
|
|
1104
|
-
</body>
|
|
1105
|
-
</html>`;
|
|
1106
|
-
}
|
|
1107
|
-
function escapeHtml(text) {
|
|
1108
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
1109
|
-
}
|
|
1110
|
-
function errorPage(message) {
|
|
1111
|
-
const safeMessage = escapeHtml(message);
|
|
1112
|
-
return `<!DOCTYPE html>
|
|
1113
|
-
<html>
|
|
1114
|
-
<head><title>Login Failed</title></head>
|
|
1115
|
-
<body style="font-family: system-ui; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background: #f9fafb;">
|
|
1116
|
-
<div style="text-align: center; padding: 2rem;">
|
|
1117
|
-
<div style="width: 64px; height: 64px; background: #dc2626; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem;">
|
|
1118
|
-
<svg width="32" height="32" fill="none" stroke="white" stroke-width="3" viewBox="0 0 24 24">
|
|
1119
|
-
<path d="M6 18L18 6M6 6l12 12" stroke-linecap="round" stroke-linejoin="round"/>
|
|
1120
|
-
</svg>
|
|
1121
|
-
</div>
|
|
1122
|
-
<h1 style="color: #dc2626; margin: 0 0 0.5rem;">Login Failed</h1>
|
|
1123
|
-
<p style="color: #6b7280; margin: 0;">${safeMessage}</p>
|
|
1124
|
-
<p style="color: #9ca3af; margin: 1rem 0 0; font-size: 0.875rem;">You can close this window.</p>
|
|
1125
|
-
</div>
|
|
1126
|
-
</body>
|
|
1127
|
-
</html>`;
|
|
1128
|
-
}
|
|
1129
|
-
async function exchangeCodeForTokens(baseUrl3, code, codeVerifier, redirectUri, clientId) {
|
|
1130
|
-
const response = await fetch(`${baseUrl3}/api/oauth/token`, {
|
|
1131
|
-
method: "POST",
|
|
1132
|
-
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
1133
|
-
body: new URLSearchParams({
|
|
1134
|
-
grant_type: "authorization_code",
|
|
1135
|
-
client_id: clientId,
|
|
1136
|
-
code,
|
|
1137
|
-
code_verifier: codeVerifier,
|
|
1138
|
-
redirect_uri: redirectUri
|
|
1139
|
-
}).toString()
|
|
1140
|
-
});
|
|
1141
|
-
if (!response.ok) {
|
|
1142
|
-
const err = await response.json().catch(() => ({}));
|
|
1143
|
-
throw new Error(err.error_description || err.error || "Failed to exchange code for tokens");
|
|
1144
|
-
}
|
|
1145
|
-
return await response.json();
|
|
1146
|
-
}
|
|
1147
|
-
function buildAuthorizationUrl(baseUrl3, clientId, redirectUri, codeChallenge, state) {
|
|
1148
|
-
const url = new URL(`${baseUrl3}/api/oauth/authorize`);
|
|
1149
|
-
url.searchParams.set("client_id", clientId);
|
|
1150
|
-
url.searchParams.set("redirect_uri", redirectUri);
|
|
1151
|
-
url.searchParams.set("code_challenge", codeChallenge);
|
|
1152
|
-
url.searchParams.set("code_challenge_method", "S256");
|
|
1153
|
-
url.searchParams.set("state", state);
|
|
1154
|
-
url.searchParams.set("scope", "profile email");
|
|
1155
|
-
url.searchParams.set("response_type", "code");
|
|
1156
|
-
return url.toString();
|
|
1157
|
-
}
|
|
1158
1115
|
var DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
|
|
1159
1116
|
var DEFAULT_DEVICE_POLL_INTERVAL_SECONDS = 5;
|
|
1160
|
-
function shouldUseDeviceFlow() {
|
|
1161
|
-
if (process.env.SSH_CONNECTION || process.env.SSH_CLIENT || process.env.SSH_TTY) return true;
|
|
1162
|
-
if (process.platform === "linux" && !process.env.DISPLAY && !process.env.WAYLAND_DISPLAY) {
|
|
1163
|
-
return true;
|
|
1164
|
-
}
|
|
1165
|
-
return false;
|
|
1166
|
-
}
|
|
1167
1117
|
async function startDeviceAuthorization(baseUrl3, clientId) {
|
|
1168
1118
|
const params = new URLSearchParams({ client_id: clientId });
|
|
1169
1119
|
try {
|
|
1170
|
-
const hostname2 =
|
|
1120
|
+
const hostname2 = os2.hostname();
|
|
1171
1121
|
if (hostname2) params.set("hostname", hostname2);
|
|
1172
1122
|
} catch {
|
|
1173
1123
|
}
|
|
@@ -1236,7 +1186,7 @@ function setAuthBaseUrl(url) {
|
|
|
1236
1186
|
baseUrl2 = url;
|
|
1237
1187
|
}
|
|
1238
1188
|
function registerAuthCommands(program2) {
|
|
1239
|
-
program2.command("login").description("Log in to Context7").option("--no-browser", "Don't open browser automatically").
|
|
1189
|
+
program2.command("login").description("Log in to Context7").option("--no-browser", "Don't open browser automatically").action(async (options) => {
|
|
1240
1190
|
await loginCommand(options);
|
|
1241
1191
|
});
|
|
1242
1192
|
program2.command("logout").description("Log out of Context7").action(() => {
|
|
@@ -1299,7 +1249,7 @@ async function announceIdentity(accessToken) {
|
|
|
1299
1249
|
return "Login successful!";
|
|
1300
1250
|
}
|
|
1301
1251
|
}
|
|
1302
|
-
async function
|
|
1252
|
+
async function performLogin(openBrowser = true) {
|
|
1303
1253
|
const spinner = ora("Preparing login...").start();
|
|
1304
1254
|
let authorization;
|
|
1305
1255
|
try {
|
|
@@ -1367,67 +1317,6 @@ async function performDeviceLogin(openBrowser = true) {
|
|
|
1367
1317
|
waitingSpinner.fail(pc4.red("Code expired without approval."));
|
|
1368
1318
|
return null;
|
|
1369
1319
|
}
|
|
1370
|
-
async function performLogin(openBrowser = true, forceDevice = false) {
|
|
1371
|
-
if (forceDevice || shouldUseDeviceFlow()) {
|
|
1372
|
-
return performDeviceLogin(openBrowser);
|
|
1373
|
-
}
|
|
1374
|
-
const spinner = ora("Preparing login...").start();
|
|
1375
|
-
try {
|
|
1376
|
-
const { codeVerifier, codeChallenge } = generatePKCE();
|
|
1377
|
-
const state = generateState();
|
|
1378
|
-
const callbackServer = createCallbackServer(state);
|
|
1379
|
-
const port = await callbackServer.port;
|
|
1380
|
-
const redirectUri = `http://localhost:${port}/callback`;
|
|
1381
|
-
const authUrl = buildAuthorizationUrl(
|
|
1382
|
-
baseUrl2,
|
|
1383
|
-
CLI_CLIENT_ID,
|
|
1384
|
-
redirectUri,
|
|
1385
|
-
codeChallenge,
|
|
1386
|
-
state
|
|
1387
|
-
);
|
|
1388
|
-
spinner.stop();
|
|
1389
|
-
console.log("");
|
|
1390
|
-
console.log(pc4.bold("Opening browser to log in..."));
|
|
1391
|
-
console.log("");
|
|
1392
|
-
if (openBrowser) {
|
|
1393
|
-
await open(authUrl);
|
|
1394
|
-
console.log(pc4.dim("If the browser didn't open, visit this URL:"));
|
|
1395
|
-
} else {
|
|
1396
|
-
console.log(pc4.dim("Open this URL in your browser:"));
|
|
1397
|
-
}
|
|
1398
|
-
console.log(pc4.cyan(authUrl));
|
|
1399
|
-
console.log("");
|
|
1400
|
-
const waitingSpinner = ora("Waiting for login...").start();
|
|
1401
|
-
try {
|
|
1402
|
-
const { code } = await callbackServer.result;
|
|
1403
|
-
waitingSpinner.text = "Exchanging code for tokens...";
|
|
1404
|
-
const tokens = await exchangeCodeForTokens(
|
|
1405
|
-
baseUrl2,
|
|
1406
|
-
code,
|
|
1407
|
-
codeVerifier,
|
|
1408
|
-
redirectUri,
|
|
1409
|
-
CLI_CLIENT_ID
|
|
1410
|
-
);
|
|
1411
|
-
saveTokens(tokens);
|
|
1412
|
-
callbackServer.close();
|
|
1413
|
-
waitingSpinner.succeed(pc4.green("Login successful!"));
|
|
1414
|
-
return tokens.access_token;
|
|
1415
|
-
} catch (error) {
|
|
1416
|
-
callbackServer.close();
|
|
1417
|
-
waitingSpinner.fail(pc4.red("Login failed"));
|
|
1418
|
-
if (error instanceof Error) {
|
|
1419
|
-
console.error(pc4.red(error.message));
|
|
1420
|
-
}
|
|
1421
|
-
return null;
|
|
1422
|
-
}
|
|
1423
|
-
} catch (error) {
|
|
1424
|
-
spinner.fail(pc4.red("Login failed"));
|
|
1425
|
-
if (error instanceof Error) {
|
|
1426
|
-
console.error(pc4.red(error.message));
|
|
1427
|
-
}
|
|
1428
|
-
return null;
|
|
1429
|
-
}
|
|
1430
|
-
}
|
|
1431
1320
|
async function loginCommand(options) {
|
|
1432
1321
|
trackEvent("command", { name: "login" });
|
|
1433
1322
|
const existingToken = await getValidAccessToken();
|
|
@@ -1437,7 +1326,7 @@ async function loginCommand(options) {
|
|
|
1437
1326
|
return;
|
|
1438
1327
|
}
|
|
1439
1328
|
clearTokens();
|
|
1440
|
-
const token = await performLogin(options.browser
|
|
1329
|
+
const token = await performLogin(options.browser);
|
|
1441
1330
|
if (!token) {
|
|
1442
1331
|
process.exit(1);
|
|
1443
1332
|
}
|
|
@@ -1902,8 +1791,8 @@ async function generateCommand(options) {
|
|
|
1902
1791
|
log.blank();
|
|
1903
1792
|
};
|
|
1904
1793
|
const openInEditor = async () => {
|
|
1905
|
-
const previewDir =
|
|
1906
|
-
await
|
|
1794
|
+
const previewDir = getPreviewsDir();
|
|
1795
|
+
await mkdir3(previewDir, { recursive: true });
|
|
1907
1796
|
previewFile = join4(previewDir, `${skillName}.md`);
|
|
1908
1797
|
if (!previewFileWritten) {
|
|
1909
1798
|
await writeFile2(previewFile, generatedContent, "utf-8");
|
|
@@ -1983,7 +1872,7 @@ async function generateCommand(options) {
|
|
|
1983
1872
|
const skillDir = join4(finalDir, skillName);
|
|
1984
1873
|
const skillPath = join4(skillDir, "SKILL.md");
|
|
1985
1874
|
try {
|
|
1986
|
-
await
|
|
1875
|
+
await mkdir3(skillDir, { recursive: true });
|
|
1987
1876
|
await writeFile2(skillPath, generatedContent, "utf-8");
|
|
1988
1877
|
} catch (err) {
|
|
1989
1878
|
const error = err;
|
|
@@ -2835,12 +2724,12 @@ ${headerLine}`,
|
|
|
2835
2724
|
import pc8 from "picocolors";
|
|
2836
2725
|
import ora4 from "ora";
|
|
2837
2726
|
import { select as select3 } from "@inquirer/prompts";
|
|
2838
|
-
import { mkdir as
|
|
2839
|
-
import { dirname as
|
|
2840
|
-
import { randomBytes
|
|
2727
|
+
import { mkdir as mkdir5, readFile as readFile4, writeFile as writeFile4 } from "fs/promises";
|
|
2728
|
+
import { dirname as dirname7, join as join8 } from "path";
|
|
2729
|
+
import { randomBytes } from "crypto";
|
|
2841
2730
|
|
|
2842
2731
|
// src/setup/agents.ts
|
|
2843
|
-
import { access as
|
|
2732
|
+
import { access as access3 } from "fs/promises";
|
|
2844
2733
|
import { join as join7 } from "path";
|
|
2845
2734
|
import { homedir as homedir5 } from "os";
|
|
2846
2735
|
var SETUP_AGENT_NAMES = {
|
|
@@ -3044,7 +2933,7 @@ function getAgent(name) {
|
|
|
3044
2933
|
var ALL_AGENT_NAMES = Object.keys(agents);
|
|
3045
2934
|
async function pathExists(p) {
|
|
3046
2935
|
try {
|
|
3047
|
-
await
|
|
2936
|
+
await access3(p);
|
|
3048
2937
|
return true;
|
|
3049
2938
|
} catch {
|
|
3050
2939
|
return false;
|
|
@@ -3153,8 +3042,8 @@ function customizeSkillFilesForAgent(agent, skillName, files) {
|
|
|
3153
3042
|
}
|
|
3154
3043
|
|
|
3155
3044
|
// src/setup/mcp-writer.ts
|
|
3156
|
-
import { access as
|
|
3157
|
-
import { dirname as
|
|
3045
|
+
import { access as access4, readFile as readFile3, writeFile as writeFile3, mkdir as mkdir4 } from "fs/promises";
|
|
3046
|
+
import { dirname as dirname6 } from "path";
|
|
3158
3047
|
function stripJsonComments(text) {
|
|
3159
3048
|
let result = "";
|
|
3160
3049
|
let i = 0;
|
|
@@ -3225,7 +3114,7 @@ function removeServerEntry(existing, configKey, serverName) {
|
|
|
3225
3114
|
async function resolveMcpPath(candidates) {
|
|
3226
3115
|
for (const candidate of candidates) {
|
|
3227
3116
|
try {
|
|
3228
|
-
await
|
|
3117
|
+
await access4(candidate);
|
|
3229
3118
|
return candidate;
|
|
3230
3119
|
} catch {
|
|
3231
3120
|
}
|
|
@@ -3233,7 +3122,7 @@ async function resolveMcpPath(candidates) {
|
|
|
3233
3122
|
return candidates[0];
|
|
3234
3123
|
}
|
|
3235
3124
|
async function writeJsonConfig(filePath, config) {
|
|
3236
|
-
await
|
|
3125
|
+
await mkdir4(dirname6(filePath), { recursive: true });
|
|
3237
3126
|
await writeFile3(filePath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
3238
3127
|
}
|
|
3239
3128
|
async function readTomlServerExists(filePath, serverName) {
|
|
@@ -3353,11 +3242,11 @@ async function appendTomlServer(filePath, serverName, entry) {
|
|
|
3353
3242
|
const before = rawBefore.length > 0 ? rawBefore + "\n\n" : "";
|
|
3354
3243
|
const after = rawAfter.length > 0 ? "\n" + rawAfter : "";
|
|
3355
3244
|
const content = before + block + after;
|
|
3356
|
-
await
|
|
3245
|
+
await mkdir4(dirname6(filePath), { recursive: true });
|
|
3357
3246
|
await writeFile3(filePath, content, "utf-8");
|
|
3358
3247
|
} else {
|
|
3359
3248
|
const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n\n" : existing.length > 0 ? "\n" : "";
|
|
3360
|
-
await
|
|
3249
|
+
await mkdir4(dirname6(filePath), { recursive: true });
|
|
3361
3250
|
await writeFile3(filePath, existing + separator + block, "utf-8");
|
|
3362
3251
|
}
|
|
3363
3252
|
return { alreadyExists };
|
|
@@ -3390,7 +3279,7 @@ async function removeTomlServer(filePath, serverName) {
|
|
|
3390
3279
|
const rawBefore = existing.slice(0, startIdx).replace(/\n+$/, "");
|
|
3391
3280
|
const rawAfter = existing.slice(startIdx + sectionHeader.length + endOffset).replace(/^\n+/, "");
|
|
3392
3281
|
const content = [rawBefore, rawAfter].filter(Boolean).join("\n\n");
|
|
3393
|
-
await
|
|
3282
|
+
await mkdir4(dirname6(filePath), { recursive: true });
|
|
3394
3283
|
await writeFile3(filePath, content.length > 0 ? `${content}
|
|
3395
3284
|
` : "", "utf-8");
|
|
3396
3285
|
return { removed: true };
|
|
@@ -3432,7 +3321,7 @@ async function authenticateAndGenerateKey() {
|
|
|
3432
3321
|
Authorization: `Bearer ${accessToken}`,
|
|
3433
3322
|
"Content-Type": "application/json"
|
|
3434
3323
|
},
|
|
3435
|
-
body: JSON.stringify({ name: `ctx7-cli-${
|
|
3324
|
+
body: JSON.stringify({ name: `ctx7-cli-${randomBytes(3).toString("hex")}` })
|
|
3436
3325
|
});
|
|
3437
3326
|
if (!response.ok) {
|
|
3438
3327
|
const err = await response.json().catch(() => ({}));
|
|
@@ -3536,7 +3425,7 @@ async function installRule(agentName, mode, scope) {
|
|
|
3536
3425
|
if (rule.kind === "file") {
|
|
3537
3426
|
const ruleDir = scope === "global" ? rule.dir("global") : join8(process.cwd(), rule.dir("project"));
|
|
3538
3427
|
const rulePath = join8(ruleDir, rule.filename);
|
|
3539
|
-
await
|
|
3428
|
+
await mkdir5(dirname7(rulePath), { recursive: true });
|
|
3540
3429
|
await writeFile4(rulePath, content, "utf-8");
|
|
3541
3430
|
return { status: "installed", path: rulePath };
|
|
3542
3431
|
}
|
|
@@ -3556,7 +3445,7 @@ ${content}${rule.sectionMarker}`;
|
|
|
3556
3445
|
return { status: "updated", path: filePath };
|
|
3557
3446
|
}
|
|
3558
3447
|
const separator = existing.length > 0 && !existing.endsWith("\n") ? "\n\n" : existing.length > 0 ? "\n" : "";
|
|
3559
|
-
await
|
|
3448
|
+
await mkdir5(dirname7(filePath), { recursive: true });
|
|
3560
3449
|
await writeFile4(filePath, existing + separator + section + "\n", "utf-8");
|
|
3561
3450
|
return { status: "installed", path: filePath };
|
|
3562
3451
|
}
|
|
@@ -3660,7 +3549,7 @@ async function setupMcp(agents2, options, scope) {
|
|
|
3660
3549
|
log.plain(` ${pc8.dim(r.skillPath)}`);
|
|
3661
3550
|
if (r.skillStatus.includes("EACCES")) {
|
|
3662
3551
|
log.plain(
|
|
3663
|
-
` ${pc8.yellow("tip:")} fix permissions with: ${pc8.cyan(`sudo chown -R $(whoami) ${
|
|
3552
|
+
` ${pc8.yellow("tip:")} fix permissions with: ${pc8.cyan(`sudo chown -R $(whoami) ${dirname7(dirname7(r.skillPath))}`)}`
|
|
3664
3553
|
);
|
|
3665
3554
|
}
|
|
3666
3555
|
}
|
|
@@ -3721,7 +3610,7 @@ async function setupCli(options) {
|
|
|
3721
3610
|
log.plain(` ${pc8.dim(r.skillPath)}`);
|
|
3722
3611
|
if (r.skillStatus.includes("EACCES")) {
|
|
3723
3612
|
log.plain(
|
|
3724
|
-
` ${pc8.yellow("tip:")} fix permissions with: ${pc8.cyan(`sudo chown -R $(whoami) ${
|
|
3613
|
+
` ${pc8.yellow("tip:")} fix permissions with: ${pc8.cyan(`sudo chown -R $(whoami) ${dirname7(dirname7(r.skillPath))}`)}`
|
|
3725
3614
|
);
|
|
3726
3615
|
}
|
|
3727
3616
|
const ruleIcon = r.ruleStatus === "installed" || r.ruleStatus === "updated" ? pc8.green("+") : pc8.dim("~");
|
|
@@ -3754,7 +3643,7 @@ async function setupCommand(options) {
|
|
|
3754
3643
|
import pc9 from "picocolors";
|
|
3755
3644
|
import ora5 from "ora";
|
|
3756
3645
|
import { join as join9 } from "path";
|
|
3757
|
-
import { access as
|
|
3646
|
+
import { access as access5, readFile as readFile5, rm as rm3, writeFile as writeFile5 } from "fs/promises";
|
|
3758
3647
|
var CHECKBOX_THEME2 = {
|
|
3759
3648
|
style: {
|
|
3760
3649
|
highlight: (text) => pc9.green(text),
|
|
@@ -3854,7 +3743,7 @@ function resolveFlagModes(options) {
|
|
|
3854
3743
|
}
|
|
3855
3744
|
async function pathExists2(path2) {
|
|
3856
3745
|
try {
|
|
3857
|
-
await
|
|
3746
|
+
await access5(path2);
|
|
3858
3747
|
return true;
|
|
3859
3748
|
} catch {
|
|
3860
3749
|
return false;
|
|
@@ -4101,6 +3990,18 @@ async function removeCommand2(options) {
|
|
|
4101
3990
|
// src/commands/docs.ts
|
|
4102
3991
|
import pc10 from "picocolors";
|
|
4103
3992
|
import ora6 from "ora";
|
|
3993
|
+
|
|
3994
|
+
// src/utils/library-id.ts
|
|
3995
|
+
function recoverLibraryId(input2) {
|
|
3996
|
+
if (input2.startsWith("//")) return input2.replace(/^\/+/, "/");
|
|
3997
|
+
if (input2.startsWith("/")) return input2;
|
|
3998
|
+
if (!/^[A-Za-z]:[\\/]/.test(input2)) return input2;
|
|
3999
|
+
const normalized = input2.replace(/\\/g, "/");
|
|
4000
|
+
const match = normalized.match(/^[A-Za-z]:\/.*?\/(?:Git|PortableGit|git-bash)\/(.+)$/i);
|
|
4001
|
+
return match ? `/${match[1]}` : input2;
|
|
4002
|
+
}
|
|
4003
|
+
|
|
4004
|
+
// src/commands/docs.ts
|
|
4104
4005
|
var isTTY = process.stdout.isTTY;
|
|
4105
4006
|
function getReputationLabel(score) {
|
|
4106
4007
|
if (score === void 0 || score < 0) return "Unknown";
|
|
@@ -4186,10 +4087,16 @@ async function resolveCommand(library, query, options) {
|
|
|
4186
4087
|
}
|
|
4187
4088
|
async function queryCommand(libraryId, query, options) {
|
|
4188
4089
|
trackEvent("command", { name: "docs" });
|
|
4090
|
+
libraryId = recoverLibraryId(libraryId);
|
|
4189
4091
|
if (!libraryId.startsWith("/") || !/^\/[^/]+\/[^/]/.test(libraryId)) {
|
|
4190
4092
|
log.error(`Invalid library ID: "${libraryId}"`);
|
|
4191
4093
|
log.info(`Expected format: /owner/repo or /owner/repo/version (e.g., /facebook/react)`);
|
|
4192
4094
|
log.info(`Run "ctx7 library <name>" to find the correct ID`);
|
|
4095
|
+
if (process.platform === "win32") {
|
|
4096
|
+
log.info(
|
|
4097
|
+
`On Git Bash, prefix the ID with an extra slash to avoid path conversion: ctx7 docs "//facebook/react" "<your question>"`
|
|
4098
|
+
);
|
|
4099
|
+
}
|
|
4193
4100
|
process.exitCode = 1;
|
|
4194
4101
|
return;
|
|
4195
4102
|
}
|
|
@@ -4273,25 +4180,36 @@ import { spawn as spawn2 } from "child_process";
|
|
|
4273
4180
|
import pc11 from "picocolors";
|
|
4274
4181
|
|
|
4275
4182
|
// src/utils/update-check.ts
|
|
4276
|
-
import {
|
|
4277
|
-
import {
|
|
4278
|
-
import { mkdir as mkdir5, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
|
|
4183
|
+
import { dirname as dirname8 } from "path";
|
|
4184
|
+
import { mkdir as mkdir6, readFile as readFile6, writeFile as writeFile6 } from "fs/promises";
|
|
4279
4185
|
var DEFAULT_CACHE_TTL_MS = 24 * 60 * 60 * 1e3;
|
|
4280
|
-
var UPDATE_STATE_FILE = join10(homedir6(), ".context7", "cli-state.json");
|
|
4281
4186
|
function getStateFilePath(stateFile) {
|
|
4282
|
-
return stateFile ??
|
|
4187
|
+
return stateFile ?? getUpdateStateFilePath();
|
|
4188
|
+
}
|
|
4189
|
+
async function readStateFilePath(stateFile) {
|
|
4190
|
+
if (stateFile) {
|
|
4191
|
+
return stateFile;
|
|
4192
|
+
}
|
|
4193
|
+
return resolveReadPath(UPDATE_STATE_FILE_NAME, getUpdateStateFilePath());
|
|
4194
|
+
}
|
|
4195
|
+
async function writeStateFilePath(stateFile) {
|
|
4196
|
+
const path2 = getStateFilePath(stateFile);
|
|
4197
|
+
if (!stateFile) {
|
|
4198
|
+
await migrateLegacyFile(UPDATE_STATE_FILE_NAME, path2);
|
|
4199
|
+
}
|
|
4200
|
+
return path2;
|
|
4283
4201
|
}
|
|
4284
4202
|
async function readUpdateState(stateFile) {
|
|
4285
4203
|
try {
|
|
4286
|
-
const raw = await readFile6(
|
|
4204
|
+
const raw = await readFile6(await readStateFilePath(stateFile), "utf-8");
|
|
4287
4205
|
return JSON.parse(raw);
|
|
4288
4206
|
} catch {
|
|
4289
4207
|
return {};
|
|
4290
4208
|
}
|
|
4291
4209
|
}
|
|
4292
4210
|
async function writeUpdateState(state, stateFile) {
|
|
4293
|
-
const path2 =
|
|
4294
|
-
await
|
|
4211
|
+
const path2 = await writeStateFilePath(stateFile);
|
|
4212
|
+
await mkdir6(dirname8(path2), { recursive: true });
|
|
4295
4213
|
await writeFile6(path2, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
4296
4214
|
}
|
|
4297
4215
|
function compareVersions(a, b) {
|
|
@@ -4595,7 +4513,7 @@ var brand = {
|
|
|
4595
4513
|
dim: pc12.dim
|
|
4596
4514
|
};
|
|
4597
4515
|
var program = new Command();
|
|
4598
|
-
program.name("ctx7").description("Context7 CLI - Fetch documentation context and configure Context7").version(VERSION).option("--base-url <url>").hook("preAction", (thisCommand) => {
|
|
4516
|
+
program.name("ctx7").description("Context7 CLI - Fetch documentation context and configure Context7").version(VERSION, "-v, --version").option("--base-url <url>").hook("preAction", (thisCommand) => {
|
|
4599
4517
|
const opts = thisCommand.opts();
|
|
4600
4518
|
if (opts.baseUrl) {
|
|
4601
4519
|
setBaseUrl(opts.baseUrl);
|