ctx7 0.4.5 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1155,17 +1155,88 @@ function buildAuthorizationUrl(baseUrl3, clientId, redirectUri, codeChallenge, s
1155
1155
  url.searchParams.set("response_type", "code");
1156
1156
  return url.toString();
1157
1157
  }
1158
+ var DEVICE_CODE_GRANT = "urn:ietf:params:oauth:grant-type:device_code";
1159
+ 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
+ async function startDeviceAuthorization(baseUrl3, clientId) {
1168
+ const params = new URLSearchParams({ client_id: clientId });
1169
+ try {
1170
+ const hostname2 = os.hostname();
1171
+ if (hostname2) params.set("hostname", hostname2);
1172
+ } catch {
1173
+ }
1174
+ const response = await fetch(`${baseUrl3}/api/oauth/device/code`, {
1175
+ method: "POST",
1176
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1177
+ body: params.toString()
1178
+ });
1179
+ if (!response.ok) {
1180
+ const err = await response.json().catch(() => ({}));
1181
+ throw new Error(err.error_description || err.error || "Failed to start device authorization");
1182
+ }
1183
+ return await response.json();
1184
+ }
1185
+ async function pollDeviceToken(baseUrl3, clientId, deviceCode) {
1186
+ let response;
1187
+ try {
1188
+ response = await fetch(`${baseUrl3}/api/oauth/device/token`, {
1189
+ method: "POST",
1190
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1191
+ body: new URLSearchParams({
1192
+ grant_type: DEVICE_CODE_GRANT,
1193
+ device_code: deviceCode,
1194
+ client_id: clientId
1195
+ }).toString()
1196
+ });
1197
+ } catch (error) {
1198
+ return {
1199
+ status: "transient",
1200
+ errorMessage: error instanceof Error ? error.message : "network error"
1201
+ };
1202
+ }
1203
+ if (response.ok) {
1204
+ const tokens = await response.json();
1205
+ return { status: "approved", tokens };
1206
+ }
1207
+ if (response.status >= 500) {
1208
+ const err2 = await response.json().catch(() => ({}));
1209
+ return {
1210
+ status: "transient",
1211
+ errorMessage: err2.error_description || err2.error || `HTTP ${response.status}`
1212
+ };
1213
+ }
1214
+ const err = await response.json().catch(() => ({}));
1215
+ switch (err.error) {
1216
+ case "authorization_pending":
1217
+ return { status: "pending" };
1218
+ case "slow_down":
1219
+ return { status: "slow_down" };
1220
+ case "access_denied":
1221
+ return { status: "denied" };
1222
+ case "expired_token":
1223
+ return { status: "expired" };
1224
+ default:
1225
+ throw new Error(err.error_description || err.error || "Device token poll failed");
1226
+ }
1227
+ }
1158
1228
 
1159
1229
  // src/commands/auth.ts
1160
1230
  import pc4 from "picocolors";
1161
1231
  import ora from "ora";
1162
1232
  import open from "open";
1233
+ import boxen from "boxen";
1163
1234
  var baseUrl2 = "https://context7.com";
1164
1235
  function setAuthBaseUrl(url) {
1165
1236
  baseUrl2 = url;
1166
1237
  }
1167
1238
  function registerAuthCommands(program2) {
1168
- program2.command("login").description("Log in to Context7").option("--no-browser", "Don't open browser automatically").action(async (options) => {
1239
+ program2.command("login").description("Log in to Context7").option("--no-browser", "Don't open browser automatically").option("--device", "Force device-code flow (use on SSH / headless hosts)").action(async (options) => {
1169
1240
  await loginCommand(options);
1170
1241
  });
1171
1242
  program2.command("logout").description("Log out of Context7").action(() => {
@@ -1175,7 +1246,131 @@ function registerAuthCommands(program2) {
1175
1246
  await whoamiCommand();
1176
1247
  });
1177
1248
  }
1178
- async function performLogin(openBrowser = true) {
1249
+ function renderDeviceCodeBox(userCode, verificationUri, verificationUriComplete) {
1250
+ const codeLine = `${pc4.dim("Your one-time code:")}
1251
+
1252
+ ${pc4.green(pc4.bold(userCode))}`;
1253
+ const linkLine = verificationUriComplete ? `${pc4.dim("Open this link to approve:")}
1254
+ ${pc4.cyan(verificationUriComplete)}
1255
+
1256
+ ${pc4.dim("Or visit")} ${pc4.cyan(verificationUri)} ${pc4.dim("and enter the code above.")}` : `${pc4.dim("Visit:")} ${pc4.cyan(verificationUri)}`;
1257
+ return boxen(`${codeLine}
1258
+
1259
+ ${linkLine}`, {
1260
+ title: "Sign in to Context7",
1261
+ titleAlignment: "left",
1262
+ padding: 1,
1263
+ margin: { top: 1, bottom: 1, left: 2, right: 2 },
1264
+ borderStyle: "round",
1265
+ borderColor: "gray"
1266
+ });
1267
+ }
1268
+ function waitForEnter(prompt) {
1269
+ if (!process.stdin.isTTY) return Promise.resolve();
1270
+ return new Promise((resolve3) => {
1271
+ process.stdout.write(` ${pc4.dim(prompt)} `);
1272
+ const onData = (chunk) => {
1273
+ if (chunk[0] === 3) {
1274
+ process.stdin.removeListener("data", onData);
1275
+ process.stdin.setRawMode?.(false);
1276
+ process.stdin.pause();
1277
+ process.stdout.write("\n");
1278
+ process.exit(130);
1279
+ }
1280
+ process.stdin.removeListener("data", onData);
1281
+ process.stdin.setRawMode?.(false);
1282
+ process.stdin.pause();
1283
+ process.stdout.write("\n");
1284
+ resolve3();
1285
+ };
1286
+ process.stdin.setRawMode?.(true);
1287
+ process.stdin.resume();
1288
+ process.stdin.on("data", onData);
1289
+ });
1290
+ }
1291
+ async function announceIdentity(accessToken) {
1292
+ try {
1293
+ const whoami = await fetchWhoami(accessToken);
1294
+ const name = whoami.email || whoami.name;
1295
+ if (!name) return "Login successful!";
1296
+ const team = whoami.teamspace?.name;
1297
+ return team ? `Logged in as ${pc4.bold(name)} ${pc4.dim(`(${team})`)}` : `Logged in as ${pc4.bold(name)}`;
1298
+ } catch {
1299
+ return "Login successful!";
1300
+ }
1301
+ }
1302
+ async function performDeviceLogin(openBrowser = true) {
1303
+ const spinner = ora("Preparing login...").start();
1304
+ let authorization;
1305
+ try {
1306
+ authorization = await startDeviceAuthorization(baseUrl2, CLI_CLIENT_ID);
1307
+ } catch (error) {
1308
+ spinner.fail(pc4.red("Login failed"));
1309
+ if (error instanceof Error) console.error(pc4.red(error.message));
1310
+ return null;
1311
+ }
1312
+ spinner.stop();
1313
+ console.log(
1314
+ renderDeviceCodeBox(
1315
+ authorization.user_code,
1316
+ authorization.verification_uri,
1317
+ authorization.verification_uri_complete
1318
+ )
1319
+ );
1320
+ const target = authorization.verification_uri_complete ?? authorization.verification_uri;
1321
+ if (openBrowser) {
1322
+ await waitForEnter("Press Enter to open the browser, or Ctrl-C to quit...");
1323
+ try {
1324
+ await open(target);
1325
+ } catch {
1326
+ console.log(pc4.dim(` Couldn't open a browser \u2014 visit the link above manually.`));
1327
+ }
1328
+ } else {
1329
+ console.log(pc4.dim(" Open the link above in any browser to continue."));
1330
+ console.log("");
1331
+ }
1332
+ const waitingSpinner = ora({ text: "Waiting for authorization...", indent: 2 }).start();
1333
+ const deadline = Date.now() + authorization.expires_in * 1e3;
1334
+ let intervalMs = (authorization.interval ?? DEFAULT_DEVICE_POLL_INTERVAL_SECONDS) * 1e3;
1335
+ while (Date.now() < deadline) {
1336
+ await new Promise((resolve3) => setTimeout(resolve3, intervalMs));
1337
+ try {
1338
+ const result = await pollDeviceToken(baseUrl2, CLI_CLIENT_ID, authorization.device_code);
1339
+ if (result.status === "approved" && result.tokens) {
1340
+ saveTokens(result.tokens);
1341
+ const successText = await announceIdentity(result.tokens.access_token);
1342
+ waitingSpinner.succeed(pc4.green(successText));
1343
+ return result.tokens.access_token;
1344
+ }
1345
+ if (result.status === "slow_down") {
1346
+ intervalMs += 5e3;
1347
+ continue;
1348
+ }
1349
+ if (result.status === "denied") {
1350
+ waitingSpinner.fail(pc4.red("Authorization denied."));
1351
+ return null;
1352
+ }
1353
+ if (result.status === "expired") {
1354
+ waitingSpinner.fail(pc4.red("Code expired. Run login again."));
1355
+ return null;
1356
+ }
1357
+ if (result.status === "transient") {
1358
+ intervalMs += 5e3;
1359
+ continue;
1360
+ }
1361
+ } catch (error) {
1362
+ waitingSpinner.fail(pc4.red("Login failed"));
1363
+ if (error instanceof Error) console.error(pc4.red(error.message));
1364
+ return null;
1365
+ }
1366
+ }
1367
+ waitingSpinner.fail(pc4.red("Code expired without approval."));
1368
+ return null;
1369
+ }
1370
+ async function performLogin(openBrowser = true, forceDevice = false) {
1371
+ if (forceDevice || shouldUseDeviceFlow()) {
1372
+ return performDeviceLogin(openBrowser);
1373
+ }
1179
1374
  const spinner = ora("Preparing login...").start();
1180
1375
  try {
1181
1376
  const { codeVerifier, codeChallenge } = generatePKCE();
@@ -1242,7 +1437,7 @@ async function loginCommand(options) {
1242
1437
  return;
1243
1438
  }
1244
1439
  clearTokens();
1245
- const token = await performLogin(options.browser);
1440
+ const token = await performLogin(options.browser, options.device ?? false);
1246
1441
  if (!token) {
1247
1442
  process.exit(1);
1248
1443
  }