@pkgseer/cli 0.1.1 → 0.1.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/cli.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  version
4
- } from "./shared/chunk-3ne7e7xh.js";
4
+ } from "./shared/chunk-2wbvwsc9.js";
5
5
 
6
6
  // src/cli.ts
7
7
  import { Command } from "commander";
@@ -824,7 +824,7 @@ class AuthStorageImpl {
824
824
  const normalizedUrl = normalizeBaseUrl(baseUrl);
825
825
  stored.tokens[normalizedUrl] = token;
826
826
  stored.version = CURRENT_VERSION;
827
- await this.fs.ensureDir(this.authPath, DIR_MODE);
827
+ await this.fs.ensureDir(this.configDir, DIR_MODE);
828
828
  await this.fs.writeFile(this.authPath, JSON.stringify(stored, null, 2), FILE_MODE);
829
829
  }
830
830
  async clear(baseUrl) {
@@ -900,29 +900,18 @@ function migrateV2ToV3(legacy) {
900
900
  }
901
901
  // src/services/auth-utils.ts
902
902
  var PROJECT_MANIFEST_UPLOAD_SCOPE = "project_manifest_upload";
903
- async function checkProjectWriteScope(authStorage, baseUrl) {
904
- const envToken = process.env.PKGSEER_API_TOKEN;
905
- if (envToken) {
906
- return {
907
- token: envToken,
908
- tokenName: "PKGSEER_API_TOKEN",
909
- scopes: [],
910
- createdAt: new Date().toISOString(),
911
- expiresAt: null,
912
- apiKeyId: 0
913
- };
914
- }
915
- const auth = await authStorage.load(baseUrl);
916
- if (!auth) {
903
+ async function checkProjectWriteScope(configService, baseUrl) {
904
+ const tokenData = await configService.getApiToken(baseUrl);
905
+ if (!tokenData) {
917
906
  return null;
918
907
  }
919
- if (auth.expiresAt && new Date(auth.expiresAt) < new Date) {
920
- return null;
908
+ if (tokenData.scopes.length === 0) {
909
+ return tokenData;
921
910
  }
922
- if (!auth.scopes.includes(PROJECT_MANIFEST_UPLOAD_SCOPE)) {
911
+ if (!tokenData.scopes.includes(PROJECT_MANIFEST_UPLOAD_SCOPE)) {
923
912
  return null;
924
913
  }
925
- return auth;
914
+ return tokenData;
926
915
  }
927
916
  // src/services/browser-service.ts
928
917
  import open from "open";
@@ -971,11 +960,14 @@ var MergedConfigSchema = z.object({
971
960
  project: z.string().optional(),
972
961
  manifests: z.array(ManifestGroupSchema).optional()
973
962
  });
963
+ var PKGSEER_API_TOKEN = "PKGSEER_API_TOKEN";
974
964
 
975
965
  class ConfigServiceImpl {
976
966
  fs;
977
- constructor(fs) {
967
+ authStorage;
968
+ constructor(fs, authStorage) {
978
969
  this.fs = fs;
970
+ this.authStorage = authStorage;
979
971
  }
980
972
  getGlobalConfigPath() {
981
973
  return this.fs.joinPath(this.fs.getHomeDir(), CONFIG_DIR2, GLOBAL_CONFIG_FILE);
@@ -1059,6 +1051,27 @@ class ConfigServiceImpl {
1059
1051
  return null;
1060
1052
  }
1061
1053
  }
1054
+ async getApiToken(baseUrl) {
1055
+ const envToken = process.env[PKGSEER_API_TOKEN];
1056
+ if (envToken) {
1057
+ return {
1058
+ token: envToken,
1059
+ tokenName: "PKGSEER_API_TOKEN",
1060
+ scopes: [],
1061
+ createdAt: new Date().toISOString(),
1062
+ expiresAt: null,
1063
+ apiKeyId: 0
1064
+ };
1065
+ }
1066
+ const stored = await this.authStorage.load(baseUrl);
1067
+ if (!stored) {
1068
+ return null;
1069
+ }
1070
+ if (stored.expiresAt && new Date(stored.expiresAt) < new Date) {
1071
+ return null;
1072
+ }
1073
+ return stored;
1074
+ }
1062
1075
  }
1063
1076
  // src/services/filesystem-service.ts
1064
1077
  import {
@@ -1097,7 +1110,7 @@ class FileSystemServiceImpl {
1097
1110
  }
1098
1111
  }
1099
1112
  async ensureDir(path, mode) {
1100
- await mkdir(dirname(path), { recursive: true, mode });
1113
+ await mkdir(path, { recursive: true, mode });
1101
1114
  }
1102
1115
  getHomeDir() {
1103
1116
  return homedir();
@@ -1523,27 +1536,17 @@ class ShellServiceImpl {
1523
1536
  }
1524
1537
  }
1525
1538
  // src/container.ts
1526
- async function resolveApiToken(authStorage, baseUrl) {
1527
- const envToken = process.env.PKGSEER_API_TOKEN;
1528
- if (envToken) {
1529
- return envToken;
1530
- }
1531
- const stored = await authStorage.load(baseUrl);
1532
- if (stored) {
1533
- if (stored.expiresAt && new Date(stored.expiresAt) < new Date) {
1534
- return;
1535
- }
1536
- return stored.token;
1537
- }
1538
- return;
1539
+ async function resolveApiToken(configService, baseUrl) {
1540
+ const tokenData = await configService.getApiToken(baseUrl);
1541
+ return tokenData?.token;
1539
1542
  }
1540
1543
  async function createContainer() {
1541
1544
  const baseUrl = getBaseUrl();
1542
1545
  const fileSystemService = new FileSystemServiceImpl;
1543
1546
  const authStorage = new AuthStorageImpl(fileSystemService);
1544
- const configService = new ConfigServiceImpl(fileSystemService);
1547
+ const configService = new ConfigServiceImpl(fileSystemService, authStorage);
1545
1548
  const [apiToken, configResult] = await Promise.all([
1546
- resolveApiToken(authStorage, baseUrl),
1549
+ resolveApiToken(configService, baseUrl),
1547
1550
  configService.loadMergedConfig()
1548
1551
  ]);
1549
1552
  const client = createClient(apiToken);
@@ -2305,1906 +2308,2425 @@ function registerDocsSearchCommand(program) {
2305
2308
  });
2306
2309
  });
2307
2310
  }
2308
- // src/commands/login.ts
2309
- import { hostname } from "node:os";
2310
- var TIMEOUT_MS = 5 * 60 * 1000;
2311
- function randomPort() {
2312
- return Math.floor(Math.random() * 2000) + 8000;
2311
+ // src/commands/shared-colors.ts
2312
+ var colors2 = {
2313
+ reset: "\x1B[0m",
2314
+ bold: "\x1B[1m",
2315
+ dim: "\x1B[2m",
2316
+ green: "\x1B[32m",
2317
+ yellow: "\x1B[33m",
2318
+ blue: "\x1B[34m",
2319
+ magenta: "\x1B[35m",
2320
+ cyan: "\x1B[36m",
2321
+ red: "\x1B[31m"
2322
+ };
2323
+ function shouldUseColors2(noColor) {
2324
+ if (noColor)
2325
+ return false;
2326
+ if (process.env.NO_COLOR !== undefined)
2327
+ return false;
2328
+ return process.stdout.isTTY ?? false;
2313
2329
  }
2314
- async function loginAction(options, deps) {
2315
- const { authService, authStorage, browserService, baseUrl } = deps;
2316
- const existing = await authStorage.load(baseUrl);
2317
- if (existing && !options.force) {
2318
- const isExpired = existing.expiresAt && new Date(existing.expiresAt) < new Date;
2319
- if (!isExpired) {
2320
- console.log(`Already logged in.
2321
- `);
2322
- console.log(` Environment: ${baseUrl}`);
2323
- console.log(` Token: ${existing.tokenName}
2330
+ function success(text, useColors) {
2331
+ const checkmark = useColors ? `${colors2.green}✓${colors2.reset}` : "✓";
2332
+ return `${checkmark} ${text}`;
2333
+ }
2334
+ function error(text, useColors) {
2335
+ const cross = useColors ? `${colors2.red}✗${colors2.reset}` : "✗";
2336
+ return `${cross} ${text}`;
2337
+ }
2338
+ function highlight(text, useColors) {
2339
+ if (!useColors)
2340
+ return text;
2341
+ return `${colors2.bold}${colors2.cyan}${text}${colors2.reset}`;
2342
+ }
2343
+ function dim(text, useColors) {
2344
+ if (!useColors)
2345
+ return text;
2346
+ return `${colors2.dim}${text}${colors2.reset}`;
2347
+ }
2348
+
2349
+ // src/commands/mcp-init.ts
2350
+ function getCursorConfigPaths(fs, scope) {
2351
+ if (scope === "project") {
2352
+ const cwd = fs.getCwd();
2353
+ const configPath2 = fs.joinPath(cwd, ".cursor", "mcp.json");
2354
+ const backupPath2 = fs.joinPath(cwd, ".cursor", "mcp.json.bak");
2355
+ return { configPath: configPath2, backupPath: backupPath2 };
2356
+ }
2357
+ const platform = process.platform;
2358
+ let configPath;
2359
+ let backupPath;
2360
+ if (platform === "win32") {
2361
+ const appData = process.env.APPDATA || fs.joinPath(fs.getHomeDir(), "AppData", "Roaming");
2362
+ configPath = fs.joinPath(appData, "Cursor", "mcp.json");
2363
+ backupPath = fs.joinPath(appData, "Cursor", "mcp.json.bak");
2364
+ } else {
2365
+ const home = fs.getHomeDir();
2366
+ configPath = fs.joinPath(home, ".cursor", "mcp.json");
2367
+ backupPath = fs.joinPath(home, ".cursor", "mcp.json.bak");
2368
+ }
2369
+ return { configPath, backupPath };
2370
+ }
2371
+ function getCodexConfigPaths(fs, scope) {
2372
+ if (scope === "project") {
2373
+ const cwd = fs.getCwd();
2374
+ const configPath2 = fs.joinPath(cwd, ".codex", "config.toml");
2375
+ const backupPath2 = fs.joinPath(cwd, ".codex", "config.toml.bak");
2376
+ return { configPath: configPath2, backupPath: backupPath2 };
2377
+ }
2378
+ const home = fs.getHomeDir();
2379
+ const configPath = fs.joinPath(home, ".codex", "config.toml");
2380
+ const backupPath = fs.joinPath(home, ".codex", "config.toml.bak");
2381
+ return { configPath, backupPath };
2382
+ }
2383
+ function getClaudeCodeConfigPaths(fs, scope) {
2384
+ if (scope === "project") {
2385
+ const cwd = fs.getCwd();
2386
+ const configPath2 = fs.joinPath(cwd, ".claude-code", "mcp.json");
2387
+ const backupPath2 = fs.joinPath(cwd, ".claude-code", "mcp.json.bak");
2388
+ return { configPath: configPath2, backupPath: backupPath2 };
2389
+ }
2390
+ const platform = process.platform;
2391
+ let configPath;
2392
+ let backupPath;
2393
+ if (platform === "win32") {
2394
+ const appData = process.env.APPDATA || fs.joinPath(fs.getHomeDir(), "AppData", "Roaming");
2395
+ configPath = fs.joinPath(appData, "Claude Code", "mcp.json");
2396
+ backupPath = fs.joinPath(appData, "Claude Code", "mcp.json.bak");
2397
+ } else {
2398
+ const home = fs.getHomeDir();
2399
+ configPath = fs.joinPath(home, ".claude-code", "mcp.json");
2400
+ backupPath = fs.joinPath(home, ".claude-code", "mcp.json.bak");
2401
+ }
2402
+ return { configPath, backupPath };
2403
+ }
2404
+ async function parseConfigFile(fs, path) {
2405
+ const exists = await fs.exists(path);
2406
+ if (!exists) {
2407
+ return null;
2408
+ }
2409
+ try {
2410
+ const content = await fs.readFile(path);
2411
+ const parsed = JSON.parse(content);
2412
+ return parsed;
2413
+ } catch {
2414
+ return null;
2415
+ }
2416
+ }
2417
+ async function writeConfigFile(fs, path, config) {
2418
+ const content = JSON.stringify(config, null, 2);
2419
+ await fs.writeFile(path, content);
2420
+ }
2421
+ async function backupConfigFile(fs, configPath, backupPath) {
2422
+ const exists = await fs.exists(configPath);
2423
+ if (!exists) {
2424
+ return;
2425
+ }
2426
+ const content = await fs.readFile(configPath);
2427
+ await fs.writeFile(backupPath, content);
2428
+ }
2429
+ async function canSafelyEdit(fs, configPath) {
2430
+ const exists = await fs.exists(configPath);
2431
+ if (!exists) {
2432
+ return { safe: true };
2433
+ }
2434
+ const config = await parseConfigFile(fs, configPath);
2435
+ if (config === null) {
2436
+ return {
2437
+ safe: false,
2438
+ reason: "Config file exists but cannot be parsed as valid JSON"
2439
+ };
2440
+ }
2441
+ if (config.mcpServers?.pkgseer) {
2442
+ return {
2443
+ safe: false,
2444
+ reason: "PkgSeer MCP server is already configured"
2445
+ };
2446
+ }
2447
+ return { safe: true };
2448
+ }
2449
+ function addPkgseerToConfig(config) {
2450
+ const updated = { ...config };
2451
+ if (!updated.mcpServers) {
2452
+ updated.mcpServers = {};
2453
+ }
2454
+ updated.mcpServers.pkgseer = {
2455
+ command: "npx",
2456
+ args: ["-y", "@pkgseer/cli", "mcp", "start"]
2457
+ };
2458
+ return updated;
2459
+ }
2460
+ function showManualInstructions(tool, scope, configPath, useColors) {
2461
+ console.log(`
2462
+ Manual Setup Instructions`);
2463
+ console.log(`─────────────────────────
2324
2464
  `);
2325
- console.log("To switch accounts, run `pkgseer logout` first.");
2326
- console.log("To re-authenticate with different scopes, use `pkgseer login --force`.");
2327
- return;
2328
- }
2329
- console.log(`Token expired. Starting new login...
2465
+ console.log(`Config file: ${highlight(configPath, useColors)}
2330
2466
  `);
2331
- } else if (existing && options.force) {
2332
- console.log(`Re-authenticating (--force flag)...
2467
+ console.log(`Add the following to your configuration:
2333
2468
  `);
2469
+ if (tool === "codex") {
2470
+ const tomlConfig = `[mcp_servers.pkgseer]
2471
+ command = "npx"
2472
+ args = ["-y", "@pkgseer/cli", "mcp", "start"]`;
2473
+ console.log(tomlConfig);
2474
+ } else {
2475
+ const configExample = {
2476
+ mcpServers: {
2477
+ pkgseer: {
2478
+ command: "npx",
2479
+ args: ["-y", "@pkgseer/cli", "mcp", "start"]
2480
+ }
2481
+ }
2482
+ };
2483
+ console.log(JSON.stringify(configExample, null, 2));
2334
2484
  }
2335
- const { verifier, challenge, state } = authService.generatePkceParams();
2336
- const port = options.port ?? randomPort();
2337
- const authUrl = authService.buildAuthUrl({
2338
- state,
2339
- port,
2340
- codeChallenge: challenge,
2341
- hostname: hostname()
2342
- });
2343
- const serverPromise = authService.startCallbackServer(port);
2344
- if (options.browser === false) {
2345
- console.log(`Open this URL in your browser:
2485
+ if ((tool === "cursor" || tool === "codex" || tool === "claude-code") && scope === "project") {
2486
+ const dirName = tool === "cursor" ? ".cursor" : tool === "codex" ? ".codex" : ".claude-code";
2487
+ console.log(dim(`
2488
+ Note: Create the ${dirName} directory if it doesn't exist.`, useColors));
2489
+ }
2490
+ console.log(dim(`
2491
+ After editing, restart your AI assistant to activate the MCP server.`, useColors));
2492
+ }
2493
+ async function mcpInitAction(deps) {
2494
+ const { fileSystemService: fs, promptService, hasProject } = deps;
2495
+ const useColors = shouldUseColors2();
2496
+ console.log("MCP Server Setup");
2497
+ console.log(`────────────────
2346
2498
  `);
2347
- console.log(` ${authUrl}
2499
+ console.log(`Configure PkgSeer as an MCP server for your AI assistant.
2500
+ `);
2501
+ if (!hasProject) {
2502
+ console.log(dim(`Note: No pkgseer.yml found in this directory.
2503
+ `, useColors));
2504
+ console.log(dim(`Without it, search_project_docs won't work (searches your project's packages).
2505
+ `, useColors));
2506
+ const setupProject = await promptService.confirm("Set up project configuration first?", false);
2507
+ if (setupProject) {
2508
+ console.log(`
2509
+ Run ${highlight("pkgseer project init", useColors)} first, then ${highlight("pkgseer mcp init", useColors)} again.
2348
2510
  `);
2511
+ return;
2512
+ }
2513
+ }
2514
+ const tool = await promptService.select("Which AI tool would you like to configure?", [
2515
+ {
2516
+ value: "cursor",
2517
+ name: "Cursor IDE",
2518
+ description: "Project-level or global configuration"
2519
+ },
2520
+ {
2521
+ value: "codex",
2522
+ name: "Codex CLI",
2523
+ description: "Project-level or global configuration"
2524
+ },
2525
+ {
2526
+ value: "claude-code",
2527
+ name: "Claude Code",
2528
+ description: "Project-level or global configuration"
2529
+ },
2530
+ {
2531
+ value: "other",
2532
+ name: "Other",
2533
+ description: "Show manual setup instructions"
2534
+ }
2535
+ ]);
2536
+ if (tool === "other") {
2537
+ const configPath2 = fs.joinPath(fs.getCwd(), "mcp-config.json");
2538
+ showManualInstructions("other", undefined, configPath2, useColors);
2539
+ return;
2540
+ }
2541
+ let scope;
2542
+ if (tool === "cursor" || tool === "codex" || tool === "claude-code") {
2543
+ const projectPath = tool === "cursor" ? ".cursor/mcp.json" : tool === "codex" ? ".codex/config.toml" : ".claude-code/mcp.json";
2544
+ const globalPath = tool === "cursor" ? "~/.cursor/mcp.json" : tool === "codex" ? "~/.codex/config.toml" : "~/.claude-code/mcp.json";
2545
+ if (hasProject) {
2546
+ scope = await promptService.select("Where should the MCP config be created?", [
2547
+ {
2548
+ value: "project",
2549
+ name: `Project (${projectPath}) – recommended`,
2550
+ description: "Uses project token; enables docs search for your packages"
2551
+ },
2552
+ {
2553
+ value: "global",
2554
+ name: `Global (${globalPath})`,
2555
+ description: "Works everywhere but without project-specific features"
2556
+ }
2557
+ ]);
2558
+ } else {
2559
+ console.log(dim(`
2560
+ Using global config (no pkgseer.yml found).
2561
+ `, useColors));
2562
+ scope = "global";
2563
+ }
2564
+ }
2565
+ let configPath;
2566
+ let backupPath;
2567
+ if (tool === "cursor") {
2568
+ if (!scope) {
2569
+ scope = "global";
2570
+ }
2571
+ const paths = getCursorConfigPaths(fs, scope);
2572
+ configPath = paths.configPath;
2573
+ backupPath = paths.backupPath;
2574
+ } else if (tool === "codex") {
2575
+ if (!scope) {
2576
+ scope = hasProject ? "project" : "global";
2577
+ }
2578
+ const paths = getCodexConfigPaths(fs, scope);
2579
+ showManualInstructions("codex", scope, paths.configPath, useColors);
2580
+ return;
2581
+ } else if (tool === "claude-code") {
2582
+ if (!scope) {
2583
+ scope = "global";
2584
+ }
2585
+ const paths = getClaudeCodeConfigPaths(fs, scope);
2586
+ configPath = paths.configPath;
2587
+ backupPath = paths.backupPath;
2349
2588
  } else {
2350
- console.log("Opening browser...");
2351
- await browserService.open(authUrl);
2589
+ const configPath2 = fs.joinPath(fs.getCwd(), "mcp-config.json");
2590
+ showManualInstructions("other", undefined, configPath2, useColors);
2591
+ return;
2352
2592
  }
2353
- console.log(`Waiting for authentication...
2354
- `);
2355
- let timeoutId;
2356
- const timeoutPromise = new Promise((_, reject) => {
2357
- timeoutId = setTimeout(() => reject(new Error("Authentication timed out")), TIMEOUT_MS);
2358
- });
2359
- let callback;
2360
- try {
2361
- callback = await Promise.race([serverPromise, timeoutPromise]);
2362
- clearTimeout(timeoutId);
2363
- } catch (error) {
2364
- clearTimeout(timeoutId);
2365
- if (error instanceof Error) {
2366
- console.log(`${error.message}.
2367
- `);
2368
- console.log("Run `pkgseer login` to try again.");
2593
+ const safetyCheck = await canSafelyEdit(fs, configPath);
2594
+ if (!safetyCheck.safe) {
2595
+ console.log(error(`Cannot safely edit config file: ${safetyCheck.reason}`, useColors));
2596
+ showManualInstructions(tool, scope, configPath, useColors);
2597
+ return;
2598
+ }
2599
+ const configExists = await fs.exists(configPath);
2600
+ if (configExists) {
2601
+ console.log(`
2602
+ Found existing config at: ${highlight(configPath, useColors)}`);
2603
+ const proceed = await promptService.confirm("Add PkgSeer to this configuration?", true);
2604
+ if (!proceed) {
2605
+ console.log(dim(`
2606
+ Setup cancelled.`, useColors));
2607
+ return;
2608
+ }
2609
+ } else {
2610
+ console.log(`
2611
+ Will create config at: ${highlight(configPath, useColors)}`);
2612
+ const proceed = await promptService.confirm("Proceed?", true);
2613
+ if (!proceed) {
2614
+ console.log(dim(`
2615
+ Setup cancelled.`, useColors));
2616
+ return;
2369
2617
  }
2370
- process.exit(1);
2371
2618
  }
2372
- if (callback.state !== state) {
2373
- console.error(`Security error: authentication state mismatch.
2374
- `);
2375
- console.log("This could indicate a security issue. Please try again.");
2376
- process.exit(1);
2619
+ const existingConfig = await parseConfigFile(fs, configPath);
2620
+ const config = existingConfig ?? {};
2621
+ if (configExists) {
2622
+ try {
2623
+ await backupConfigFile(fs, configPath, backupPath);
2624
+ console.log(dim(`
2625
+ Backup created: ${backupPath}`, useColors));
2626
+ } catch (backupError) {
2627
+ console.log(error(`Warning: Could not create backup: ${backupError instanceof Error ? backupError.message : String(backupError)}`, useColors));
2628
+ }
2377
2629
  }
2378
- let tokenResponse;
2630
+ const updatedConfig = addPkgseerToConfig(config);
2379
2631
  try {
2380
- tokenResponse = await authService.exchangeCodeForToken({
2381
- code: callback.code,
2382
- codeVerifier: verifier,
2383
- state
2384
- });
2385
- } catch (error) {
2386
- console.error(`Failed to complete authentication: ${error instanceof Error ? error.message : error}
2387
- `);
2388
- console.log("Run `pkgseer login` to try again.");
2389
- process.exit(1);
2632
+ const dirPath = fs.getDirname(configPath);
2633
+ await fs.ensureDir(dirPath);
2634
+ } catch (dirError) {
2635
+ console.log(error(`Failed to create directory: ${dirError instanceof Error ? dirError.message : String(dirError)}`, useColors));
2636
+ showManualInstructions(tool, scope, configPath, useColors);
2637
+ return;
2390
2638
  }
2391
- await authStorage.save(baseUrl, {
2392
- token: tokenResponse.token,
2393
- tokenName: tokenResponse.tokenName,
2394
- scopes: tokenResponse.scopes,
2395
- createdAt: new Date().toISOString(),
2396
- expiresAt: tokenResponse.expiresAt,
2397
- apiKeyId: tokenResponse.apiKeyId
2398
- });
2399
- console.log(`✓ Logged in
2400
- `);
2401
- console.log(` Environment: ${baseUrl}`);
2402
- console.log(` Token: ${tokenResponse.tokenName}`);
2403
- if (tokenResponse.expiresAt) {
2404
- const days = Math.ceil((new Date(tokenResponse.expiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
2405
- console.log(` Expires: in ${days} days`);
2639
+ try {
2640
+ await writeConfigFile(fs, configPath, updatedConfig);
2641
+ } catch (writeError) {
2642
+ console.log(error(`Failed to write config file: ${writeError instanceof Error ? writeError.message : String(writeError)}`, useColors));
2643
+ showManualInstructions(tool, scope, configPath, useColors);
2644
+ return;
2406
2645
  }
2646
+ const toolNames = {
2647
+ cursor: "Cursor IDE",
2648
+ codex: "Codex CLI",
2649
+ "claude-code": "Claude Code",
2650
+ other: "MCP"
2651
+ };
2652
+ console.log(success(`PkgSeer MCP server configured for ${toolNames[tool]}!`, useColors));
2407
2653
  console.log(`
2408
- You're ready to use pkgseer with your AI assistant.`);
2409
- }
2410
- var LOGIN_DESCRIPTION = `Authenticate with your PkgSeer account via browser.
2654
+ Config file: ${highlight(configPath, useColors)}`);
2655
+ if (configExists) {
2656
+ console.log(`Backup saved: ${highlight(backupPath, useColors)}`);
2657
+ }
2658
+ console.log(dim(`
2659
+ Next steps:
2660
+ 1. Restart your AI assistant to activate the MCP server
2661
+ 2. Test by asking your assistant about packages`, useColors));
2662
+ console.log(`
2663
+ Available MCP tools:`);
2664
+ console.log(" • package_summary - Get package overview");
2665
+ console.log(" • package_vulnerabilities - Check for security issues");
2666
+ console.log(" • package_quality - Get quality score");
2667
+ console.log(" • package_dependencies - List dependencies");
2668
+ console.log(" • compare_packages - Compare multiple packages");
2669
+ console.log(" • list_package_docs - List documentation pages");
2670
+ console.log(" • fetch_package_doc - Fetch documentation content");
2671
+ console.log(" • search_package_docs - Search package documentation");
2672
+ if (hasProject) {
2673
+ console.log(" • search_project_docs - Search your project's docs");
2674
+ }
2675
+ }
2676
+ var MCP_INIT_DESCRIPTION = `Configure PkgSeer's MCP server for your AI assistant.
2411
2677
 
2412
- Opens your browser to complete authentication securely. The CLI receives
2413
- a token that's stored locally and used for API requests.
2678
+ Guides you through:
2679
+ Selecting your AI tool (Cursor IDE, Codex CLI, Claude Code)
2680
+ • Choosing configuration location (project or global)
2681
+ • Safely editing config files with automatic backup
2414
2682
 
2415
- Use --no-browser in environments without a display (CI, SSH sessions)
2416
- to get a URL you can open on another device.`;
2417
- function registerLoginCommand(program) {
2418
- program.command("login").summary("Authenticate with your PkgSeer account").description(LOGIN_DESCRIPTION).option("--no-browser", "Print URL instead of opening browser").option("--port <port>", "Port for local callback server", parseInt).option("--force", "Re-authenticate even if already logged in").action(async (options) => {
2683
+ Project-level config (recommended):
2684
+ Uses your project token for authenticated features
2685
+ Enables search_project_docs (search your project's packages)
2686
+ Portable with your project`;
2687
+ function registerMcpInitCommand(mcpCommand) {
2688
+ mcpCommand.command("init").summary("Configure MCP server for AI assistants").description(MCP_INIT_DESCRIPTION).action(async () => {
2419
2689
  const deps = await createContainer();
2420
- await loginAction(options, deps);
2690
+ const hasProject = deps.config.project !== undefined;
2691
+ await mcpInitAction({
2692
+ fileSystemService: deps.fileSystemService,
2693
+ promptService: deps.promptService,
2694
+ configService: deps.configService,
2695
+ baseUrl: deps.baseUrl,
2696
+ hasProject
2697
+ });
2421
2698
  });
2422
2699
  }
2423
2700
 
2424
- // src/commands/logout.ts
2425
- async function logoutAction(deps) {
2426
- const { authService, authStorage, baseUrl } = deps;
2427
- const auth = await authStorage.load(baseUrl);
2428
- if (!auth) {
2429
- console.log(`Not currently logged in.
2430
- `);
2431
- console.log(` Environment: ${baseUrl}`);
2432
- return;
2701
+ // src/services/gitignore-parser.ts
2702
+ function parseGitIgnoreLine(line) {
2703
+ const trimmed = line.trimEnd();
2704
+ if (!trimmed || trimmed.startsWith("#")) {
2705
+ return null;
2433
2706
  }
2434
- try {
2435
- await authService.revokeToken(auth.token);
2436
- } catch {}
2437
- await authStorage.clear(baseUrl);
2438
- console.log(`✓ Logged out
2439
- `);
2440
- console.log(` Environment: ${baseUrl}`);
2441
- }
2442
- var LOGOUT_DESCRIPTION = `Remove stored credentials and revoke the token.
2443
-
2444
- Clears the locally stored authentication token and notifies the server
2445
- to revoke it. Use this when switching accounts or on shared machines.`;
2446
- function registerLogoutCommand(program) {
2447
- program.command("logout").summary("Remove stored credentials").description(LOGOUT_DESCRIPTION).action(async () => {
2448
- const deps = await createContainer();
2449
- await logoutAction(deps);
2450
- });
2451
- }
2452
-
2453
- // src/commands/mcp.ts
2454
- import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2455
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
2456
-
2457
- // src/tools/compare-packages.ts
2458
- import { z as z3 } from "zod";
2459
-
2460
- // src/tools/shared.ts
2461
- import { z as z2 } from "zod";
2462
-
2463
- // src/tools/types.ts
2464
- function textResult(text) {
2465
- return {
2466
- content: [{ type: "text", text }]
2467
- };
2468
- }
2469
- function errorResult(message) {
2707
+ const isNegation = trimmed.startsWith("!");
2708
+ const pattern = isNegation ? trimmed.slice(1) : trimmed;
2709
+ const isRootOnly = pattern.startsWith("/");
2710
+ const patternWithoutRoot = isRootOnly ? pattern.slice(1) : pattern;
2711
+ const isDirectory = patternWithoutRoot.endsWith("/");
2712
+ const cleanPattern = isDirectory ? patternWithoutRoot.slice(0, -1) : patternWithoutRoot;
2470
2713
  return {
2471
- content: [{ type: "text", text: message }],
2472
- isError: true
2473
- };
2474
- }
2475
-
2476
- // src/tools/shared.ts
2477
- function toGraphQLRegistry2(registry) {
2478
- const map = {
2479
- npm: "NPM",
2480
- pypi: "PYPI",
2481
- hex: "HEX"
2714
+ pattern: cleanPattern,
2715
+ isNegation,
2716
+ isDirectory,
2717
+ isRootOnly
2482
2718
  };
2483
- return map[registry.toLowerCase()] || "NPM";
2484
2719
  }
2485
- var schemas = {
2486
- registry: z2.enum(["npm", "pypi", "hex"]).describe("Package registry (npm, pypi, or hex)"),
2487
- packageName: z2.string().max(255).describe("Name of the package"),
2488
- version: z2.string().max(100).optional().describe("Specific version (defaults to latest)")
2489
- };
2490
- function handleGraphQLErrors(errors) {
2491
- if (errors && errors.length > 0) {
2492
- return errorResult(`Error: ${errors.map((e) => e.message).join(", ")}`);
2720
+ function matchesPattern(path, pattern) {
2721
+ const { pattern: p, isDirectory, isRootOnly } = pattern;
2722
+ let regexStr = p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "___DOUBLE_STAR___").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/___DOUBLE_STAR___/g, ".*");
2723
+ if (isRootOnly) {
2724
+ if (isDirectory) {
2725
+ regexStr = `^${regexStr}(/|$)`;
2726
+ } else {
2727
+ regexStr = `^${regexStr}(/|$)`;
2728
+ }
2729
+ } else {
2730
+ if (isDirectory) {
2731
+ regexStr = `(^|/)${regexStr}(/|$)`;
2732
+ } else {
2733
+ regexStr = `(^|/)${regexStr}(/|$)`;
2734
+ }
2493
2735
  }
2494
- return null;
2736
+ const regex = new RegExp(regexStr);
2737
+ return regex.test(path);
2495
2738
  }
2496
- async function withErrorHandling(operation, fn) {
2739
+ async function parseGitIgnore(gitignorePath, fs) {
2497
2740
  try {
2498
- return await fn();
2499
- } catch (error) {
2500
- const message = error instanceof Error ? error.message : "Unknown error";
2501
- return errorResult(`Failed to ${operation}: ${message}`);
2741
+ const content = await fs.readFile(gitignorePath);
2742
+ const lines = content.split(`
2743
+ `);
2744
+ const patterns = [];
2745
+ for (const line of lines) {
2746
+ const pattern = parseGitIgnoreLine(line);
2747
+ if (pattern) {
2748
+ patterns.push(pattern);
2749
+ }
2750
+ }
2751
+ return patterns.length > 0 ? patterns : null;
2752
+ } catch {
2753
+ return null;
2502
2754
  }
2503
2755
  }
2504
- function notFoundError(packageName, registry) {
2505
- return errorResult(`Package not found: ${packageName} in ${registry}`);
2756
+ function shouldIgnorePath(relativePath, patterns) {
2757
+ const normalized = relativePath.replace(/\\/g, "/");
2758
+ let ignored = false;
2759
+ for (const pattern of patterns) {
2760
+ if (matchesPattern(normalized, pattern)) {
2761
+ if (pattern.isNegation) {
2762
+ ignored = false;
2763
+ } else {
2764
+ ignored = true;
2765
+ }
2766
+ }
2767
+ }
2768
+ return ignored;
2769
+ }
2770
+ function shouldIgnoreDirectory(relativeDirPath, patterns) {
2771
+ const normalized = relativeDirPath.replace(/\\/g, "/").replace(/\/$/, "") + "/";
2772
+ return shouldIgnorePath(normalized, patterns);
2506
2773
  }
2507
2774
 
2508
- // src/tools/compare-packages.ts
2509
- var packageInputSchema = z3.object({
2510
- registry: z3.enum(["npm", "pypi", "hex"]),
2511
- name: z3.string().max(255),
2512
- version: z3.string().max(100).optional()
2513
- });
2514
- var argsSchema = {
2515
- packages: z3.array(packageInputSchema).min(2).max(10).describe("List of packages to compare (2-10 packages)")
2516
- };
2517
- function createComparePackagesTool(pkgseerService) {
2518
- return {
2519
- name: "compare_packages",
2520
- description: "Compares multiple packages across metadata, quality, and security dimensions",
2521
- schema: argsSchema,
2522
- handler: async ({ packages }, _extra) => {
2523
- return withErrorHandling("compare packages", async () => {
2524
- const input2 = packages.map((pkg) => ({
2525
- registry: toGraphQLRegistry2(pkg.registry),
2526
- name: pkg.name,
2527
- version: pkg.version
2528
- }));
2529
- const result = await pkgseerService.comparePackages(input2);
2530
- const graphqlError = handleGraphQLErrors(result.errors);
2531
- if (graphqlError)
2532
- return graphqlError;
2533
- if (!result.data.comparePackages) {
2534
- return errorResult("Comparison failed: no results returned");
2775
+ // src/services/manifest-detector.ts
2776
+ var DEFAULT_EXCLUDED_DIRS = [
2777
+ "node_modules",
2778
+ "vendor",
2779
+ ".git",
2780
+ "dist",
2781
+ "build",
2782
+ "__pycache__",
2783
+ ".venv",
2784
+ "venv",
2785
+ ".next"
2786
+ ];
2787
+ var MANIFEST_TYPES = [
2788
+ {
2789
+ type: "npm",
2790
+ filenames: [
2791
+ "package.json",
2792
+ "package-lock.json",
2793
+ "yarn.lock",
2794
+ "pnpm-lock.yaml"
2795
+ ]
2796
+ },
2797
+ {
2798
+ type: "pypi",
2799
+ filenames: ["requirements.txt", "pyproject.toml", "Pipfile", "poetry.lock"]
2800
+ },
2801
+ {
2802
+ type: "hex",
2803
+ filenames: ["mix.exs", "mix.lock"]
2804
+ }
2805
+ ];
2806
+ function suggestLabel(relativePath) {
2807
+ const normalized = relativePath.replace(/\\/g, "/");
2808
+ const parts = normalized.split("/").filter((p) => p.length > 0);
2809
+ if (parts.length === 1) {
2810
+ return "root";
2811
+ }
2812
+ return parts[parts.length - 2];
2813
+ }
2814
+ async function scanDirectoryRecursive(directory, fs, rootDir, options, currentDepth = 0, gitignorePatterns = null) {
2815
+ const detected = [];
2816
+ if (currentDepth > options.maxDepth) {
2817
+ return detected;
2818
+ }
2819
+ const relativeDirPath = directory === rootDir ? "." : directory.replace(rootDir, "").replace(/^[/\\]/, "");
2820
+ const baseName = directory.split(/[/\\]/).pop() ?? "";
2821
+ if (options.excludedDirs.includes(baseName)) {
2822
+ return detected;
2823
+ }
2824
+ if (gitignorePatterns && shouldIgnoreDirectory(relativeDirPath, gitignorePatterns)) {
2825
+ return detected;
2826
+ }
2827
+ for (const manifestType of MANIFEST_TYPES) {
2828
+ for (const filename of manifestType.filenames) {
2829
+ const filePath = fs.joinPath(directory, filename);
2830
+ const exists = await fs.exists(filePath);
2831
+ if (exists) {
2832
+ const relativePath = directory === rootDir ? filename : fs.joinPath(directory.replace(rootDir, "").replace(/^[/\\]/, ""), filename);
2833
+ if (gitignorePatterns && shouldIgnorePath(relativePath, gitignorePatterns)) {
2834
+ continue;
2535
2835
  }
2536
- return textResult(JSON.stringify(result.data.comparePackages, null, 2));
2537
- });
2836
+ detected.push({
2837
+ filename,
2838
+ relativePath,
2839
+ absolutePath: filePath,
2840
+ type: manifestType.type,
2841
+ suggestedLabel: suggestLabel(relativePath)
2842
+ });
2843
+ }
2538
2844
  }
2539
- };
2540
- }
2541
- // src/tools/fetch-package-doc.ts
2542
- import { z as z4 } from "zod";
2543
- var argsSchema2 = {
2544
- registry: schemas.registry,
2545
- package_name: schemas.packageName.describe("Name of the package to fetch documentation for"),
2546
- page_id: z4.string().max(500).describe("Documentation page identifier (from list_package_docs)"),
2547
- version: schemas.version
2548
- };
2549
- function createFetchPackageDocTool(pkgseerService) {
2550
- return {
2551
- name: "fetch_package_doc",
2552
- description: "Fetches the full content of a specific documentation page. Returns page title, content (markdown/HTML), breadcrumbs, and source attribution. Use list_package_docs first to get available page IDs.",
2553
- schema: argsSchema2,
2554
- handler: async ({ registry, package_name, page_id, version: version2 }, _extra) => {
2555
- return withErrorHandling("fetch documentation page", async () => {
2556
- const result = await pkgseerService.fetchPackageDoc(toGraphQLRegistry2(registry), package_name, page_id, version2);
2557
- const graphqlError = handleGraphQLErrors(result.errors);
2558
- if (graphqlError)
2559
- return graphqlError;
2560
- if (!result.data.fetchPackageDoc) {
2561
- return errorResult(`Documentation page not found: ${page_id} for ${package_name} in ${registry}`);
2845
+ }
2846
+ try {
2847
+ const entries = await fs.readdir(directory);
2848
+ for (const entry of entries) {
2849
+ const entryPath = fs.joinPath(directory, entry);
2850
+ const isDir = await fs.isDirectory(entryPath);
2851
+ if (isDir && !options.excludedDirs.includes(entry)) {
2852
+ const subRelativePath = fs.joinPath(relativeDirPath, entry);
2853
+ if (!gitignorePatterns || !shouldIgnoreDirectory(subRelativePath, gitignorePatterns)) {
2854
+ const subDetected = await scanDirectoryRecursive(entryPath, fs, rootDir, options, currentDepth + 1, gitignorePatterns);
2855
+ detected.push(...subDetected);
2562
2856
  }
2563
- return textResult(JSON.stringify(result.data.fetchPackageDoc, null, 2));
2564
- });
2857
+ }
2565
2858
  }
2566
- };
2859
+ } catch {}
2860
+ return detected;
2567
2861
  }
2568
- // src/tools/list-package-docs.ts
2569
- var argsSchema3 = {
2570
- registry: schemas.registry,
2571
- package_name: schemas.packageName.describe("Name of the package to list documentation for"),
2572
- version: schemas.version
2573
- };
2574
- function createListPackageDocsTool(pkgseerService) {
2575
- return {
2576
- name: "list_package_docs",
2577
- description: "Lists documentation pages for a package version. Returns page titles, slugs, and metadata. Use this to discover available documentation before fetching specific pages.",
2578
- schema: argsSchema3,
2579
- handler: async ({ registry, package_name, version: version2 }, _extra) => {
2580
- return withErrorHandling("list package documentation", async () => {
2581
- const result = await pkgseerService.listPackageDocs(toGraphQLRegistry2(registry), package_name, version2);
2582
- const graphqlError = handleGraphQLErrors(result.errors);
2583
- if (graphqlError)
2584
- return graphqlError;
2585
- if (!result.data.listPackageDocs) {
2586
- return notFoundError(package_name, registry);
2587
- }
2588
- return textResult(JSON.stringify(result.data.listPackageDocs, null, 2));
2589
- });
2590
- }
2862
+ async function scanForManifests(directory, fs, options) {
2863
+ const opts = {
2864
+ maxDepth: options?.maxDepth ?? 3,
2865
+ excludedDirs: options?.excludedDirs ?? DEFAULT_EXCLUDED_DIRS
2591
2866
  };
2867
+ const gitignorePath = fs.joinPath(directory, ".gitignore");
2868
+ const gitignorePatterns = await parseGitIgnore(gitignorePath, fs);
2869
+ return scanDirectoryRecursive(directory, fs, directory, opts, 0, gitignorePatterns);
2592
2870
  }
2593
- // src/tools/package-dependencies.ts
2594
- import { z as z5 } from "zod";
2595
- var argsSchema4 = {
2596
- registry: schemas.registry,
2597
- package_name: schemas.packageName.describe("Name of the package to retrieve dependencies for"),
2598
- version: schemas.version,
2599
- include_transitive: z5.boolean().optional().describe("Whether to include transitive dependency DAG"),
2600
- max_depth: z5.number().int().min(1).max(10).optional().describe("Maximum depth for transitive traversal (1-10)")
2601
- };
2602
- function createPackageDependenciesTool(pkgseerService) {
2603
- return {
2604
- name: "package_dependencies",
2605
- description: "Retrieves direct and transitive dependencies for a package version",
2606
- schema: argsSchema4,
2607
- handler: async ({ registry, package_name, version: version2, include_transitive, max_depth }, _extra) => {
2608
- return withErrorHandling("fetch package dependencies", async () => {
2609
- const result = await pkgseerService.getPackageDependencies(toGraphQLRegistry2(registry), package_name, version2, include_transitive, max_depth);
2610
- const graphqlError = handleGraphQLErrors(result.errors);
2611
- if (graphqlError)
2612
- return graphqlError;
2613
- if (!result.data.packageDependencies) {
2614
- return notFoundError(package_name, registry);
2615
- }
2616
- return textResult(JSON.stringify(result.data.packageDependencies, null, 2));
2617
- });
2871
+ function filterRedundantPackageJson(manifests) {
2872
+ const dirToManifests = new Map;
2873
+ for (const manifest of manifests) {
2874
+ const dir = manifest.relativePath.split(/[/\\]/).slice(0, -1).join("/") || ".";
2875
+ if (!dirToManifests.has(dir)) {
2876
+ dirToManifests.set(dir, []);
2618
2877
  }
2619
- };
2620
- }
2621
- // src/tools/package-quality.ts
2622
- var argsSchema5 = {
2623
- registry: schemas.registry,
2624
- package_name: schemas.packageName.describe("Name of the package to analyze"),
2625
- version: schemas.version
2626
- };
2627
- function createPackageQualityTool(pkgseerService) {
2628
- return {
2629
- name: "package_quality",
2630
- description: "Retrieves quality score and rule-level breakdown for a package",
2631
- schema: argsSchema5,
2632
- handler: async ({ registry, package_name, version: version2 }, _extra) => {
2633
- return withErrorHandling("fetch package quality", async () => {
2634
- const result = await pkgseerService.getPackageQuality(toGraphQLRegistry2(registry), package_name, version2);
2635
- const graphqlError = handleGraphQLErrors(result.errors);
2636
- if (graphqlError)
2637
- return graphqlError;
2638
- if (!result.data.packageQuality) {
2639
- return notFoundError(package_name, registry);
2640
- }
2641
- return textResult(JSON.stringify(result.data.packageQuality, null, 2));
2642
- });
2878
+ dirToManifests.get(dir).push(manifest);
2879
+ }
2880
+ const filtered = [];
2881
+ for (const [dir, dirManifests] of dirToManifests.entries()) {
2882
+ const hasLockFile = dirManifests.some((m) => m.filename === "package-lock.json");
2883
+ const hasPackageJson = dirManifests.some((m) => m.filename === "package.json");
2884
+ for (const manifest of dirManifests) {
2885
+ if (manifest.filename === "package.json" && hasLockFile) {
2886
+ continue;
2887
+ }
2888
+ filtered.push(manifest);
2643
2889
  }
2644
- };
2890
+ }
2891
+ return filtered;
2645
2892
  }
2646
- // src/tools/package-summary.ts
2647
- var argsSchema6 = {
2648
- registry: schemas.registry,
2649
- package_name: schemas.packageName.describe("Name of the package to retrieve summary for")
2650
- };
2651
- function createPackageSummaryTool(pkgseerService) {
2652
- return {
2653
- name: "package_summary",
2654
- description: "Retrieves comprehensive package summary including metadata, versions, security advisories, and quickstart information",
2655
- schema: argsSchema6,
2656
- handler: async ({ registry, package_name }, _extra) => {
2657
- return withErrorHandling("fetch package summary", async () => {
2658
- const result = await pkgseerService.getPackageSummary(toGraphQLRegistry2(registry), package_name);
2659
- const graphqlError = handleGraphQLErrors(result.errors);
2660
- if (graphqlError)
2661
- return graphqlError;
2662
- if (!result.data.packageSummary) {
2663
- return notFoundError(package_name, registry);
2664
- }
2665
- return textResult(JSON.stringify(result.data.packageSummary, null, 2));
2666
- });
2893
+ async function detectAndGroupManifests(directory, fs, options) {
2894
+ const manifests = await scanForManifests(directory, fs, options);
2895
+ const filteredManifests = filterRedundantPackageJson(manifests);
2896
+ const groupsMap = new Map;
2897
+ for (const manifest of filteredManifests) {
2898
+ const existing = groupsMap.get(manifest.suggestedLabel) ?? [];
2899
+ existing.push(manifest);
2900
+ groupsMap.set(manifest.suggestedLabel, existing);
2901
+ }
2902
+ const groups = [];
2903
+ for (const [label, manifests2] of groupsMap.entries()) {
2904
+ const ecosystems = new Set(manifests2.map((m) => m.type));
2905
+ if (ecosystems.size > 1) {
2906
+ const ecosystemGroups = new Map;
2907
+ for (const manifest of manifests2) {
2908
+ const ecosystemLabel = `${label}-${manifest.type}`;
2909
+ const existing = ecosystemGroups.get(ecosystemLabel) ?? [];
2910
+ existing.push(manifest);
2911
+ ecosystemGroups.set(ecosystemLabel, existing);
2912
+ }
2913
+ for (const [
2914
+ ecosystemLabel,
2915
+ ecosystemManifests
2916
+ ] of ecosystemGroups.entries()) {
2917
+ groups.push({ label: ecosystemLabel, manifests: ecosystemManifests });
2918
+ }
2919
+ } else {
2920
+ groups.push({ label, manifests: manifests2 });
2667
2921
  }
2668
- };
2669
- }
2670
- // src/tools/package-vulnerabilities.ts
2671
- var argsSchema7 = {
2672
- registry: schemas.registry,
2673
- package_name: schemas.packageName.describe("Name of the package to inspect for vulnerabilities"),
2674
- version: schemas.version
2675
- };
2676
- function createPackageVulnerabilitiesTool(pkgseerService) {
2677
- return {
2678
- name: "package_vulnerabilities",
2679
- description: "Retrieves vulnerability details for a package, including affected version ranges and upgrade guidance",
2680
- schema: argsSchema7,
2681
- handler: async ({ registry, package_name, version: version2 }, _extra) => {
2682
- return withErrorHandling("fetch package vulnerabilities", async () => {
2683
- const result = await pkgseerService.getPackageVulnerabilities(toGraphQLRegistry2(registry), package_name, version2);
2684
- const graphqlError = handleGraphQLErrors(result.errors);
2685
- if (graphqlError)
2686
- return graphqlError;
2687
- if (!result.data.packageVulnerabilities) {
2688
- return notFoundError(package_name, registry);
2689
- }
2690
- return textResult(JSON.stringify(result.data.packageVulnerabilities, null, 2));
2691
- });
2922
+ }
2923
+ groups.sort((a, b) => {
2924
+ if (a.label.startsWith("root")) {
2925
+ if (b.label.startsWith("root")) {
2926
+ return a.label.localeCompare(b.label);
2927
+ }
2928
+ return -1;
2692
2929
  }
2693
- };
2694
- }
2695
- // src/tools/search-package-docs.ts
2696
- import { z as z6 } from "zod";
2697
- var argsSchema8 = {
2698
- registry: schemas.registry,
2699
- package_name: schemas.packageName.describe("Name of the package to search documentation for"),
2700
- keywords: z6.array(z6.string()).optional().describe("Keywords to search for; combined into a single query"),
2701
- query: z6.string().max(500).optional().describe("Freeform search query (alternative to keywords)"),
2702
- include_snippets: z6.boolean().optional().describe("Include content excerpts around matches"),
2703
- limit: z6.number().int().min(1).max(100).optional().describe("Maximum number of results to return"),
2704
- version: schemas.version
2705
- };
2706
- function createSearchPackageDocsTool(pkgseerService) {
2707
- return {
2708
- name: "search_package_docs",
2709
- description: "Searches package documentation with keyword or freeform query support. Returns ranked results with relevance scores. Use include_snippets=true to get content excerpts showing match context.",
2710
- schema: argsSchema8,
2711
- handler: async ({
2712
- registry,
2713
- package_name,
2714
- keywords,
2715
- query,
2716
- include_snippets,
2717
- limit,
2718
- version: version2
2719
- }, _extra) => {
2720
- return withErrorHandling("search package documentation", async () => {
2721
- if (!keywords?.length && !query) {
2722
- return errorResult("Either keywords or query must be provided for search");
2723
- }
2724
- const result = await pkgseerService.searchPackageDocs(toGraphQLRegistry2(registry), package_name, {
2725
- keywords,
2726
- query,
2727
- includeSnippets: include_snippets,
2728
- limit,
2729
- version: version2
2730
- });
2731
- const graphqlError = handleGraphQLErrors(result.errors);
2732
- if (graphqlError)
2733
- return graphqlError;
2734
- if (!result.data.searchPackageDocs) {
2735
- return errorResult(`No documentation found for ${package_name} in ${registry}`);
2736
- }
2737
- return textResult(JSON.stringify(result.data.searchPackageDocs, null, 2));
2738
- });
2930
+ if (b.label.startsWith("root")) {
2931
+ return 1;
2739
2932
  }
2740
- };
2933
+ return a.label.localeCompare(b.label);
2934
+ });
2935
+ return groups;
2741
2936
  }
2742
- // src/tools/search-project-docs.ts
2743
- import { z as z7 } from "zod";
2744
- var argsSchema9 = {
2745
- project: z7.string().optional().describe("Project name to search. Optional if configured in pkgseer.yml; only needed to search a different project."),
2746
- keywords: z7.array(z7.string()).optional().describe("Keywords to search for; combined into a single query"),
2747
- query: z7.string().max(500).optional().describe("Freeform search query (alternative to keywords)"),
2748
- include_snippets: z7.boolean().optional().describe("Include content excerpts around matches"),
2749
- limit: z7.number().int().min(1).max(100).optional().describe("Maximum number of results to return")
2750
- };
2751
- function createSearchProjectDocsTool(deps) {
2752
- const { pkgseerService, config } = deps;
2753
- return {
2754
- name: "search_project_docs",
2755
- description: "Searches documentation across all dependencies in a PkgSeer project. Returns ranked results from multiple packages. Uses project from pkgseer.yml config by default.",
2756
- schema: argsSchema9,
2757
- handler: async ({ project, keywords, query, include_snippets, limit }, _extra) => {
2758
- return withErrorHandling("search project documentation", async () => {
2759
- const resolvedProject = project ?? config.project;
2760
- if (!resolvedProject) {
2761
- return errorResult("No project provided and none configured in pkgseer.yml. " + "Either pass project parameter or add project to your config.");
2762
- }
2763
- if (!keywords?.length && !query) {
2764
- return errorResult("Either keywords or query must be provided for search");
2765
- }
2766
- const result = await pkgseerService.searchProjectDocs(resolvedProject, {
2767
- keywords,
2768
- query,
2769
- includeSnippets: include_snippets,
2770
- limit
2771
- });
2772
- const graphqlError = handleGraphQLErrors(result.errors);
2773
- if (graphqlError)
2774
- return graphqlError;
2775
- if (!result.data.searchProjectDocs) {
2776
- return errorResult(`Project not found: ${resolvedProject}`);
2937
+
2938
+ // src/commands/project/manifest-upload-utils.ts
2939
+ async function processManifestFiles(params) {
2940
+ const {
2941
+ files,
2942
+ basePath,
2943
+ hasHexManifests,
2944
+ allowMixDeps,
2945
+ fileSystemService,
2946
+ shellService
2947
+ } = params;
2948
+ const hexFiles = files.filter((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock"));
2949
+ let generatedHexFiles = [];
2950
+ if (hasHexManifests && allowMixDeps && hexFiles.length > 0) {
2951
+ const firstHexFile = hexFiles[0];
2952
+ if (!firstHexFile) {
2953
+ throw new Error("No hex files found");
2954
+ }
2955
+ const absolutePath = fileSystemService.joinPath(basePath, firstHexFile);
2956
+ const manifestDir = fileSystemService.getDirname(absolutePath);
2957
+ try {
2958
+ const [depsContent, depsTreeContent] = await Promise.all([
2959
+ shellService.execute("mix deps --all", manifestDir),
2960
+ shellService.execute("mix deps.tree", manifestDir)
2961
+ ]);
2962
+ generatedHexFiles = [
2963
+ {
2964
+ filename: "deps.txt",
2965
+ path: absolutePath,
2966
+ content: depsContent
2967
+ },
2968
+ {
2969
+ filename: "deps-tree.txt",
2970
+ path: absolutePath,
2971
+ content: depsTreeContent
2777
2972
  }
2778
- return textResult(JSON.stringify(result.data.searchProjectDocs, null, 2));
2779
- });
2973
+ ];
2974
+ } catch (error2) {
2975
+ const errorMessage = error2 instanceof Error ? error2.message : String(error2);
2976
+ throw new Error(`Failed to generate dependencies for hex manifest: ${firstHexFile}
2977
+ ` + ` Error: ${errorMessage}
2978
+ ` + ` Make sure 'mix' is installed and the directory contains a valid Elixir project.`);
2780
2979
  }
2781
- };
2782
- }
2783
- // src/commands/mcp.ts
2784
- var TOOL_FACTORIES = {
2785
- package_summary: ({ pkgseerService }) => createPackageSummaryTool(pkgseerService),
2786
- package_vulnerabilities: ({ pkgseerService }) => createPackageVulnerabilitiesTool(pkgseerService),
2787
- package_dependencies: ({ pkgseerService }) => createPackageDependenciesTool(pkgseerService),
2788
- package_quality: ({ pkgseerService }) => createPackageQualityTool(pkgseerService),
2789
- compare_packages: ({ pkgseerService }) => createComparePackagesTool(pkgseerService),
2790
- list_package_docs: ({ pkgseerService }) => createListPackageDocsTool(pkgseerService),
2791
- fetch_package_doc: ({ pkgseerService }) => createFetchPackageDocTool(pkgseerService),
2792
- search_package_docs: ({ pkgseerService }) => createSearchPackageDocsTool(pkgseerService),
2793
- search_project_docs: ({ pkgseerService, config }) => createSearchProjectDocsTool({ pkgseerService, config })
2794
- };
2795
- var PUBLIC_READ_TOOLS = [
2796
- "package_summary",
2797
- "package_vulnerabilities",
2798
- "package_dependencies",
2799
- "package_quality",
2800
- "compare_packages",
2801
- "list_package_docs",
2802
- "fetch_package_doc",
2803
- "search_package_docs"
2804
- ];
2805
- var PROJECT_READ_TOOLS = ["search_project_docs"];
2806
- var ALL_TOOLS = [...PUBLIC_READ_TOOLS, ...PROJECT_READ_TOOLS];
2807
- function createMcpServer(deps) {
2808
- const { pkgseerService, config } = deps;
2809
- const server = new McpServer({
2810
- name: "pkgseer",
2811
- version: "0.1.0"
2812
- });
2813
- const enabledToolNames = config.enabled_tools ?? ALL_TOOLS;
2814
- const toolsToRegister = enabledToolNames.filter((name) => ALL_TOOLS.includes(name));
2815
- for (const toolName of toolsToRegister) {
2816
- const factory = TOOL_FACTORIES[toolName];
2817
- const tool = factory({ pkgseerService, config });
2818
- server.registerTool(tool.name, { description: tool.description, inputSchema: tool.schema }, tool.handler);
2819
- }
2820
- return server;
2821
- }
2822
- function requireAuth(deps) {
2823
- if (deps.hasValidToken) {
2824
- return;
2825
- }
2826
- console.log(`Authentication required to start MCP server.
2827
- `);
2828
- if (deps.baseUrl !== "https://pkgseer.dev") {
2829
- console.log(` Environment: ${deps.baseUrl}`);
2830
- console.log(` You're using a custom environment.
2831
- `);
2832
- }
2833
- console.log("To authenticate:");
2834
- console.log(` pkgseer login
2835
- `);
2836
- console.log("Or set PKGSEER_API_TOKEN environment variable.");
2837
- process.exit(1);
2838
- }
2839
- async function startMcpServer(deps) {
2840
- requireAuth(deps);
2841
- const server = createMcpServer(deps);
2842
- const transport = new StdioServerTransport;
2843
- await server.connect(transport);
2844
- }
2845
- function registerMcpCommand(program) {
2846
- program.command("mcp").summary("Start MCP server for AI assistants").description(`Start the Model Context Protocol (MCP) server using STDIO transport.
2847
-
2848
- This allows AI assistants like Claude, Cursor, and others to query package
2849
- information directly. Add this to your assistant's MCP configuration:
2850
-
2851
- "pkgseer": {
2852
- "command": "pkgseer",
2853
- "args": ["mcp"]
2854
2980
  }
2855
-
2856
- Available tools: package_summary, package_vulnerabilities,
2857
- package_dependencies, package_quality, compare_packages,
2858
- list_package_docs, fetch_package_doc, search_package_docs,
2859
- search_project_docs`).action(async () => {
2860
- const deps = await createContainer();
2861
- await startMcpServer(deps);
2862
- });
2863
- }
2864
-
2865
- // src/commands/pkg/compare.ts
2866
- function parsePackageSpec(spec) {
2867
- let registry = "npm";
2868
- let rest = spec;
2869
- if (spec.includes(":")) {
2870
- const colonIndex = spec.indexOf(":");
2871
- const potentialRegistry = spec.slice(0, colonIndex).toLowerCase();
2872
- if (["npm", "pypi", "hex"].includes(potentialRegistry)) {
2873
- registry = potentialRegistry;
2874
- rest = spec.slice(colonIndex + 1);
2981
+ const filePromises = files.map(async (relativePath) => {
2982
+ const isHexFile = relativePath.endsWith("mix.exs") || relativePath.endsWith("mix.lock");
2983
+ if (isHexFile) {
2984
+ if (!allowMixDeps) {
2985
+ return null;
2986
+ }
2987
+ return null;
2875
2988
  }
2876
- }
2877
- const atIndex = rest.lastIndexOf("@");
2878
- if (atIndex > 0) {
2989
+ const absolutePath = fileSystemService.joinPath(basePath, relativePath);
2990
+ const exists = await fileSystemService.exists(absolutePath);
2991
+ if (!exists) {
2992
+ throw new Error(`File not found: ${relativePath}
2993
+ Make sure the file exists and the path in pkgseer.yml is correct.`);
2994
+ }
2995
+ const content = await fileSystemService.readFile(absolutePath);
2996
+ const filename = relativePath.split(/[/\\]/).pop() ?? relativePath;
2879
2997
  return {
2880
- registry,
2881
- name: rest.slice(0, atIndex),
2882
- version: rest.slice(atIndex + 1)
2998
+ filename,
2999
+ path: absolutePath,
3000
+ content
2883
3001
  };
3002
+ });
3003
+ const regularFiles = await Promise.all(filePromises);
3004
+ const result = [];
3005
+ let hexFilesInserted = false;
3006
+ for (let i = 0;i < files.length; i++) {
3007
+ const relativePath = files[i];
3008
+ if (!relativePath) {
3009
+ continue;
3010
+ }
3011
+ const isHexFile = relativePath.endsWith("mix.exs") || relativePath.endsWith("mix.lock");
3012
+ if (isHexFile && !hexFilesInserted && generatedHexFiles.length > 0) {
3013
+ result.push(...generatedHexFiles);
3014
+ hexFilesInserted = true;
3015
+ } else if (!isHexFile) {
3016
+ const regularFile = regularFiles[i];
3017
+ if (regularFile !== undefined) {
3018
+ result.push(regularFile);
3019
+ }
3020
+ } else {
3021
+ result.push(null);
3022
+ }
2884
3023
  }
2885
- return { registry, name: rest };
3024
+ return result;
2886
3025
  }
2887
- function formatPackageComparison(comparison) {
2888
- const lines = [];
2889
- lines.push("\uD83D\uDCCA Package Comparison");
2890
- lines.push("");
2891
- if (!comparison.packages || comparison.packages.length === 0) {
2892
- lines.push("No packages to compare.");
2893
- return lines.join(`
2894
- `);
2895
- }
2896
- const packages = comparison.packages.filter((p) => p != null);
2897
- const maxNameLen = Math.max(...packages.map((p) => `${p.packageName}@${p.version}`.length));
2898
- const header = "Package".padEnd(maxNameLen + 2);
2899
- lines.push(` ${header} Quality Downloads/mo Vulns`);
2900
- lines.push(` ${"─".repeat(maxNameLen + 2)} ${"─".repeat(10)} ${"─".repeat(12)} ${"─".repeat(5)}`);
2901
- for (const pkg of packages) {
2902
- const name = `${pkg.packageName}@${pkg.version}`.padEnd(maxNameLen + 2);
2903
- const scoreValue = pkg.quality?.score;
2904
- const quality = scoreValue != null ? `${Math.round(scoreValue > 1 ? scoreValue : scoreValue * 100)}%`.padEnd(10) : "N/A".padEnd(10);
2905
- const downloads = pkg.downloadsLastMonth ? formatNumber(pkg.downloadsLastMonth).padEnd(12) : "N/A".padEnd(12);
2906
- const vulns = pkg.vulnerabilityCount != null ? pkg.vulnerabilityCount === 0 ? "✅ 0" : `⚠️ ${pkg.vulnerabilityCount}` : "N/A";
2907
- lines.push(` ${name} ${quality} ${downloads} ${vulns}`);
3026
+
3027
+ // src/commands/project/init.ts
3028
+ async function projectInitAction(options, deps) {
3029
+ const {
3030
+ projectService,
3031
+ configService,
3032
+ fileSystemService,
3033
+ gitService,
3034
+ promptService,
3035
+ shellService,
3036
+ baseUrl
3037
+ } = deps;
3038
+ const useColors = shouldUseColors2();
3039
+ const auth = await checkProjectWriteScope(configService, baseUrl);
3040
+ if (!auth) {
3041
+ console.error(error(`Authentication required with ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope`, useColors));
3042
+ console.log(`
3043
+ Your current token doesn't have the required permissions for creating projects and uploading manifests.`);
3044
+ console.log(`
3045
+ To fix this:`);
3046
+ console.log(` 1. Run: ${highlight(`pkgseer login --force`, useColors)}`);
3047
+ console.log(` 2. Make sure to grant ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope during authentication`);
3048
+ console.log(`
3049
+ Or check your current scopes with: ${highlight(`pkgseer auth status`, useColors)}`);
3050
+ process.exit(1);
2908
3051
  }
2909
- return lines.join(`
2910
- `);
2911
- }
2912
- async function pkgCompareAction(packages, options, deps) {
2913
- const { pkgseerService } = deps;
2914
- if (packages.length < 2) {
2915
- outputError("At least 2 packages required for comparison", options.json ?? false);
2916
- return;
3052
+ const existingConfig = await configService.loadProjectConfig();
3053
+ if (existingConfig?.config.project) {
3054
+ console.error(error(`A project is already configured in pkgseer.yml: ${highlight(existingConfig.config.project, useColors)}`, useColors));
3055
+ console.log(dim(`
3056
+ To reinitialize, either remove pkgseer.yml or edit it manually.`, useColors));
3057
+ console.log(dim(` To update manifest files, use: `, useColors) + highlight(`pkgseer project detect`, useColors));
3058
+ process.exit(1);
2917
3059
  }
2918
- if (packages.length > 10) {
2919
- outputError("Maximum 10 packages can be compared at once", options.json ?? false);
2920
- return;
3060
+ let projectName = options.name?.trim();
3061
+ if (!projectName) {
3062
+ const cwd2 = fileSystemService.getCwd();
3063
+ const basename = cwd2.split(/[/\\]/).pop() ?? "project";
3064
+ const input2 = await promptService.input(`Project name:`, basename);
3065
+ projectName = input2.trim();
2921
3066
  }
2922
- const input2 = packages.map((spec) => {
2923
- const parsed = parsePackageSpec(spec);
2924
- return {
2925
- registry: toGraphQLRegistry(parsed.registry),
2926
- name: parsed.name,
2927
- version: parsed.version
2928
- };
2929
- });
2930
- const result = await pkgseerService.cliComparePackages(input2);
2931
- handleErrors(result.errors, options.json ?? false);
2932
- if (!result.data.comparePackages) {
2933
- outputError("Comparison failed", options.json ?? false);
2934
- return;
2935
- }
2936
- if (options.json) {
2937
- const pkgs = result.data.comparePackages.packages?.filter((p) => p) ?? [];
2938
- const slim = pkgs.map((p) => ({
2939
- package: `${p.packageName}@${p.version}`,
2940
- quality: p.quality?.score,
2941
- downloads: p.downloadsLastMonth,
2942
- vulnerabilities: p.vulnerabilityCount
2943
- }));
2944
- output(slim, true);
2945
- } else {
2946
- console.log(formatPackageComparison(result.data.comparePackages));
2947
- }
2948
- }
2949
- var COMPARE_DESCRIPTION = `Compare multiple packages.
2950
-
2951
- Compares packages across quality, security, and popularity metrics.
2952
- Supports cross-registry comparison.
2953
-
2954
- Package format: [registry:]name[@version]
2955
- - lodash (npm, latest)
2956
- - pypi:requests (pypi, latest)
2957
- - npm:express@4.18.0 (npm, specific version)
2958
-
2959
- Examples:
2960
- pkgseer pkg compare lodash underscore ramda
2961
- pkgseer pkg compare npm:axios pypi:requests
2962
- pkgseer pkg compare express@4.18.0 express@5.0.0 --json`;
2963
- function registerPkgCompareCommand(program) {
2964
- program.command("compare <packages...>").summary("Compare multiple packages").description(COMPARE_DESCRIPTION).option("--json", "Output as JSON").action(async (packages, options) => {
2965
- await withCliErrorHandling(options.json ?? false, async () => {
2966
- const deps = await createContainer();
2967
- await pkgCompareAction(packages, options, deps);
3067
+ console.log(`
3068
+ Creating project ${highlight(projectName, useColors)}...`);
3069
+ let createResult;
3070
+ let projectAlreadyExists = false;
3071
+ try {
3072
+ createResult = await projectService.createProject({
3073
+ name: projectName
2968
3074
  });
2969
- });
2970
- }
2971
- // src/commands/pkg/deps.ts
2972
- function formatPackageDependencies(data) {
2973
- const lines = [];
2974
- const pkg = data.package;
2975
- const deps = data.dependencies;
2976
- if (!pkg) {
2977
- return "No package data available.";
2978
- }
2979
- lines.push(`\uD83D\uDCE6 ${pkg.name}@${pkg.version}`);
2980
- lines.push("");
2981
- if (deps?.summary) {
2982
- lines.push("Summary:");
2983
- lines.push(` Direct dependencies: ${deps.summary.directCount ?? 0}`);
2984
- if (deps.summary.uniquePackagesCount) {
2985
- lines.push(` Unique packages: ${deps.summary.uniquePackagesCount}`);
3075
+ } catch (createError) {
3076
+ const errorMessage = createError instanceof Error ? createError.message : String(createError);
3077
+ if (createError instanceof Error && (errorMessage.includes("already been taken") || errorMessage.includes("already exists"))) {
3078
+ console.log(dim(`
3079
+ Project ${highlight(projectName, useColors)} already exists on the server.`, useColors));
3080
+ console.log(dim(` This might happen if you previously ran init but didn't complete the setup.`, useColors));
3081
+ const useExisting = await promptService.confirm(`
3082
+ Do you want to use the existing project and continue with manifest setup?`, true);
3083
+ if (!useExisting) {
3084
+ console.log(dim(`
3085
+ Exiting. Please choose a different project name or use an existing project.`, useColors));
3086
+ process.exit(0);
3087
+ }
3088
+ projectAlreadyExists = true;
3089
+ createResult = {
3090
+ project: {
3091
+ name: projectName,
3092
+ defaultBranch: "main"
3093
+ },
3094
+ errors: null
3095
+ };
3096
+ } else {
3097
+ console.error(error(`Failed to create project: ${errorMessage}`, useColors));
3098
+ if (createError instanceof Error && (errorMessage.includes("Insufficient permissions") || errorMessage.includes("Required scopes") || errorMessage.includes(PROJECT_MANIFEST_UPLOAD_SCOPE))) {
3099
+ console.log(`
3100
+ Your current token doesn't have the required permissions for creating projects.`);
3101
+ console.log(`
3102
+ To fix this:`);
3103
+ console.log(` 1. Run: ${highlight(`pkgseer login --force`, useColors)}`);
3104
+ console.log(` 2. Make sure to grant ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope during authentication`);
3105
+ console.log(`
3106
+ Or check your current scopes with: ${highlight(`pkgseer auth status`, useColors)}`);
3107
+ } else if (createError instanceof Error && (errorMessage.includes("alphanumeric") || errorMessage.includes("hyphens") || errorMessage.includes("underscores"))) {
3108
+ console.log(dim(`
3109
+ Project name requirements:`, useColors));
3110
+ console.log(dim(` • Must start with an alphanumeric character (a-z, A-Z, 0-9)`, useColors));
3111
+ console.log(dim(` • Can contain letters, numbers, hyphens (-), and underscores (_)`, useColors));
3112
+ console.log(dim(` • Example valid names: ${highlight(`my-project`, useColors)}, ${highlight(`project_123`, useColors)}, ${highlight(`backend`, useColors)}`, useColors));
3113
+ }
3114
+ process.exit(1);
2986
3115
  }
2987
- lines.push("");
2988
3116
  }
2989
- if (deps?.direct && deps.direct.length > 0) {
2990
- lines.push(`Dependencies (${deps.direct.length}):`);
2991
- for (const dep of deps.direct) {
2992
- if (dep) {
2993
- const type = dep.type !== "RUNTIME" ? ` [${dep.type?.toLowerCase()}]` : "";
2994
- lines.push(` ${dep.name} ${dep.versionConstraint}${type}`);
3117
+ if (!createResult.project) {
3118
+ if (createResult.errors && createResult.errors.length > 0) {
3119
+ const firstError = createResult.errors[0];
3120
+ console.error(error(`Failed to create project: ${firstError?.message ?? "Unknown error"}`, useColors));
3121
+ if (firstError?.field) {
3122
+ console.log(dim(`
3123
+ Field: ${firstError.field}`, useColors));
2995
3124
  }
3125
+ process.exit(1);
2996
3126
  }
2997
- } else {
2998
- lines.push("No direct dependencies.");
2999
- }
3000
- return lines.join(`
3001
- `);
3002
- }
3003
- async function pkgDepsAction(packageName, options, deps) {
3004
- const { pkgseerService } = deps;
3005
- const registry = toGraphQLRegistry(options.registry);
3006
- const result = await pkgseerService.cliPackageDeps(registry, packageName, options.pkgVersion, options.transitive);
3007
- handleErrors(result.errors, options.json ?? false);
3008
- if (!result.data.packageDependencies) {
3009
- outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
3010
- return;
3127
+ console.error(error(`Failed to create project`, useColors));
3128
+ console.log(dim(`
3129
+ Please try again or contact support if the issue persists.`, useColors));
3130
+ process.exit(1);
3011
3131
  }
3012
- if (options.json) {
3013
- const data = result.data.packageDependencies;
3014
- const slim = {
3015
- package: `${data.package?.name}@${data.package?.version}`,
3016
- directCount: data.dependencies?.summary?.directCount ?? 0,
3017
- dependencies: data.dependencies?.direct?.filter((d) => d).map((d) => ({
3018
- name: d.name,
3019
- version: d.versionConstraint,
3020
- type: d.type
3021
- }))
3022
- };
3023
- output(slim, true);
3132
+ if (projectAlreadyExists) {
3133
+ console.log(success(`Using existing project ${highlight(createResult.project.name, useColors)}`, useColors));
3024
3134
  } else {
3025
- console.log(formatPackageDependencies(result.data.packageDependencies));
3135
+ console.log(success(`Project ${highlight(createResult.project.name, useColors)} created successfully!`, useColors));
3026
3136
  }
3027
- }
3028
- var DEPS_DESCRIPTION = `Get package dependencies.
3029
-
3030
- Lists direct dependencies and shows version constraints and
3031
- dependency types (runtime, dev, optional).
3032
-
3033
- Examples:
3034
- pkgseer pkg deps express
3035
- pkgseer pkg deps lodash --transitive
3036
- pkgseer pkg deps requests --registry pypi --json`;
3037
- function registerPkgDepsCommand(program) {
3038
- program.command("deps <package>").summary("Get package dependencies").description(DEPS_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-v, --pkg-version <version>", "Package version").option("-t, --transitive", "Include transitive dependencies").option("--json", "Output as JSON").action(async (packageName, options) => {
3039
- await withCliErrorHandling(options.json ?? false, async () => {
3040
- const deps = await createContainer();
3041
- await pkgDepsAction(packageName, options, deps);
3042
- });
3137
+ const maxDepth = options.maxDepth ?? 3;
3138
+ console.log(dim(`
3139
+ Scanning for manifest files (max depth: ${maxDepth})...`, useColors));
3140
+ const cwd = fileSystemService.getCwd();
3141
+ const manifestGroups = await detectAndGroupManifests(cwd, fileSystemService, {
3142
+ maxDepth
3043
3143
  });
3044
- }
3045
- // src/commands/pkg/info.ts
3046
- function formatPackageSummary(data) {
3047
- const lines = [];
3048
- const pkg = data.package;
3049
- if (!pkg) {
3050
- return "No package data available.";
3051
- }
3052
- lines.push(`\uD83D\uDCE6 ${pkg.name}@${pkg.latestVersion}`);
3053
- lines.push("");
3054
- if (pkg.description) {
3055
- lines.push(pkg.description);
3056
- lines.push("");
3057
- }
3058
- lines.push("Package Info:");
3059
- lines.push(keyValueTable([
3060
- ["Registry", pkg.registry],
3061
- ["Version", pkg.latestVersion],
3062
- ["License", pkg.license]
3063
- ]));
3064
- lines.push("");
3065
- if (pkg.homepage || pkg.repositoryUrl) {
3066
- lines.push("Links:");
3067
- const links = [];
3068
- if (pkg.homepage)
3069
- links.push(["Homepage", pkg.homepage]);
3070
- if (pkg.repositoryUrl)
3071
- links.push(["Repository", pkg.repositoryUrl]);
3072
- lines.push(keyValueTable(links));
3073
- lines.push("");
3074
- }
3075
- const vulnCount = data.security?.vulnerabilityCount ?? 0;
3076
- if (vulnCount > 0) {
3077
- lines.push(`⚠️ Security: ${vulnCount} vulnerabilities`);
3078
- lines.push("");
3079
- }
3080
- if (data.quickstart?.installCommand) {
3081
- lines.push("Quick Start:");
3082
- lines.push(` ${data.quickstart.installCommand}`);
3083
- }
3084
- return lines.join(`
3144
+ const configToWrite = {
3145
+ project: projectName
3146
+ };
3147
+ if (manifestGroups.length > 0) {
3148
+ console.log(`
3149
+ Found ${highlight(`${manifestGroups.length}`, useColors)} manifest group${manifestGroups.length === 1 ? "" : "s"}:
3085
3150
  `);
3086
- }
3087
- async function pkgInfoAction(packageName, options, deps) {
3088
- const { pkgseerService } = deps;
3089
- const registry = toGraphQLRegistry(options.registry);
3090
- const result = await pkgseerService.cliPackageInfo(registry, packageName);
3091
- handleErrors(result.errors, options.json ?? false);
3092
- if (!result.data.packageSummary) {
3093
- outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
3094
- return;
3095
- }
3096
- if (options.json) {
3097
- const data = result.data.packageSummary;
3098
- const pkg = data.package;
3099
- const slim = {
3100
- name: pkg?.name,
3101
- version: pkg?.latestVersion,
3102
- description: pkg?.description,
3103
- license: pkg?.license,
3104
- install: data.quickstart?.installCommand,
3105
- vulnerabilities: data.security?.vulnerabilityCount ?? 0
3106
- };
3107
- output(slim, true);
3108
- } else {
3109
- console.log(formatPackageSummary(result.data.packageSummary));
3110
- }
3111
- }
3112
- var INFO_DESCRIPTION = `Get package summary and metadata.
3113
-
3114
- Displays comprehensive information about a package including:
3115
- - Basic metadata (version, license, description)
3116
- - Download statistics
3117
- - Security advisories
3118
- - Quick start instructions
3119
-
3120
- Examples:
3121
- pkgseer pkg info lodash
3122
- pkgseer pkg info requests --registry pypi
3123
- pkgseer pkg info phoenix --registry hex --json`;
3124
- function registerPkgInfoCommand(program) {
3125
- program.command("info <package>").summary("Get package summary and metadata").description(INFO_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("--json", "Output as JSON").action(async (packageName, options) => {
3126
- await withCliErrorHandling(options.json ?? false, async () => {
3127
- const deps = await createContainer();
3128
- await pkgInfoAction(packageName, options, deps);
3151
+ for (const group of manifestGroups) {
3152
+ console.log(` Label: ${highlight(group.label, useColors)}`);
3153
+ for (const manifest of group.manifests) {
3154
+ console.log(` ${highlight(manifest.relativePath, useColors)} ${dim(`(${manifest.type})`, useColors)}`);
3155
+ }
3156
+ }
3157
+ const hasHexManifests = manifestGroups.some((group) => group.manifests.some((m) => m.type === "hex"));
3158
+ let allowMixDeps = false;
3159
+ if (hasHexManifests) {
3160
+ console.log(dim(`
3161
+ Note: Elixir/Hex manifest files (mix.exs, mix.lock) are Elixir code and cannot be directly uploaded.`, useColors));
3162
+ console.log(dim(` Instead, we need to run "mix deps --all" and "mix deps.tree" to generate dependency information.`, useColors));
3163
+ allowMixDeps = await promptService.confirm(`
3164
+ Allow running "mix deps --all" and "mix deps.tree" for hex manifests?`, true);
3165
+ }
3166
+ configToWrite.manifests = manifestGroups.map((group) => {
3167
+ const hasHexInGroup = group.manifests.some((m) => m.type === "hex");
3168
+ return {
3169
+ label: group.label,
3170
+ files: group.manifests.map((m) => m.relativePath),
3171
+ ...hasHexInGroup && allowMixDeps ? { allow_mix_deps: true } : {}
3172
+ };
3129
3173
  });
3130
- });
3131
- }
3132
- // src/commands/pkg/quality.ts
3133
- function formatPackageQuality(data) {
3134
- const lines = [];
3135
- const quality = data.quality;
3136
- if (!quality) {
3137
- return "No quality data available.";
3138
- }
3139
- lines.push(`\uD83D\uDCCA Quality Score: ${formatScore(quality.overallScore)} (Grade: ${quality.grade})`);
3140
- lines.push("");
3141
- if (quality.categories && quality.categories.length > 0) {
3142
- lines.push("Category Breakdown:");
3143
- for (const category of quality.categories) {
3144
- if (category) {
3145
- const name = (category.category || "Unknown").padEnd(20);
3146
- lines.push(` ${name} ${formatScore(category.score)}`);
3174
+ const action = await promptService.select(`
3175
+ What would you like to do next?`, [
3176
+ {
3177
+ value: "upload",
3178
+ name: "Save config and upload manifests now",
3179
+ description: "Recommended: saves configuration and uploads files immediately"
3180
+ },
3181
+ {
3182
+ value: "edit",
3183
+ name: "Save config for manual editing",
3184
+ description: "Saves configuration so you can customize labels before uploading"
3185
+ },
3186
+ {
3187
+ value: "abort",
3188
+ name: "Skip configuration for now",
3189
+ description: "Project is created but no config saved (you can run init again later)"
3190
+ }
3191
+ ]);
3192
+ if (action === "abort") {
3193
+ if (projectAlreadyExists) {
3194
+ console.log(`
3195
+ ${success(`Using existing project ${highlight(projectName, useColors)}`, useColors)}`);
3196
+ } else {
3197
+ console.log(`
3198
+ ${success(`Project ${highlight(projectName, useColors)} created successfully!`, useColors)}`);
3147
3199
  }
3200
+ console.log(dim(`
3201
+ Configuration was not saved. To configure later, run:`, useColors));
3202
+ console.log(` ${highlight(`pkgseer project init --name ${projectName}`, useColors)}`);
3203
+ console.log(dim(`
3204
+ Or manually create pkgseer.yml and run: `, useColors) + highlight(`pkgseer project upload`, useColors));
3205
+ process.exit(0);
3148
3206
  }
3149
- lines.push("");
3150
- }
3151
- return lines.join(`
3152
- `);
3153
- }
3154
- async function pkgQualityAction(packageName, options, deps) {
3155
- const { pkgseerService } = deps;
3156
- const registry = toGraphQLRegistry(options.registry);
3157
- const result = await pkgseerService.cliPackageQuality(registry, packageName, options.pkgVersion);
3158
- handleErrors(result.errors, options.json ?? false);
3159
- if (!result.data.packageQuality) {
3160
- outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
3161
- return;
3207
+ if (action === "upload") {
3208
+ await configService.writeProjectConfig(configToWrite);
3209
+ console.log(success(`Configuration saved to ${highlight("pkgseer.yml", useColors)}`, useColors));
3210
+ const branch = await gitService.getCurrentBranch() ?? createResult.project.defaultBranch;
3211
+ console.log(`
3212
+ Uploading manifest files to branch ${highlight(branch, useColors)}...`);
3213
+ const cwd2 = fileSystemService.getCwd();
3214
+ for (const group of manifestGroups) {
3215
+ try {
3216
+ const hasHexManifests2 = group.manifests.some((m) => m.type === "hex");
3217
+ const allowMixDeps2 = configToWrite.manifests?.find((m) => m.label === group.label)?.allow_mix_deps === true;
3218
+ const allFiles = await processManifestFiles({
3219
+ files: group.manifests.map((m) => m.relativePath),
3220
+ basePath: cwd2,
3221
+ hasHexManifests: hasHexManifests2,
3222
+ allowMixDeps: allowMixDeps2 ?? false,
3223
+ fileSystemService,
3224
+ shellService
3225
+ });
3226
+ const validFiles = allFiles.filter((f) => f !== null);
3227
+ if (validFiles.length === 0) {
3228
+ console.log(` ${dim(`(no files to upload for ${group.label})`, useColors)}`);
3229
+ continue;
3230
+ }
3231
+ const uploadResult = await projectService.uploadManifests({
3232
+ project: projectName,
3233
+ branch,
3234
+ label: group.label,
3235
+ files: validFiles
3236
+ });
3237
+ for (const result of uploadResult.results) {
3238
+ if (result.status === "success") {
3239
+ const depsCount = result.dependencies_count ?? 0;
3240
+ const labelText = highlight(group.label, useColors);
3241
+ const fileText = highlight(result.filename, useColors);
3242
+ const depsText = dim(`(${depsCount} dependencies)`, useColors);
3243
+ console.log(` ${success(`${labelText}: ${fileText}`, useColors)} ${depsText}`);
3244
+ } else {
3245
+ console.log(` ${error(`${group.label}: ${result.filename} - ${result.error ?? "Unknown error"}`, useColors)}`);
3246
+ }
3247
+ }
3248
+ } catch (uploadError) {
3249
+ const errorMessage = uploadError instanceof Error ? uploadError.message : String(uploadError);
3250
+ let userMessage = `Failed to upload ${group.label}: ${errorMessage}`;
3251
+ if (hasHexManifests) {
3252
+ if (errorMessage.includes("command not found") || errorMessage.includes("mix:")) {
3253
+ userMessage = `Failed to process hex manifest files for ${group.label}.
3254
+ ` + ` Error: ${errorMessage}
3255
+ ` + ` Make sure Elixir and 'mix' are installed. Install Elixir from https://elixir-lang.org/install.html`;
3256
+ } else if (errorMessage.includes("Failed to generate dependencies")) {
3257
+ userMessage = errorMessage;
3258
+ } else if (errorMessage.includes("Network") || errorMessage.includes("ECONNREFUSED")) {
3259
+ userMessage = `Failed to upload ${group.label}: Network error.
3260
+ ` + ` Error: ${errorMessage}
3261
+ ` + ` Check your internet connection and try again.`;
3262
+ }
3263
+ } else if (errorMessage.includes("Network") || errorMessage.includes("ECONNREFUSED")) {
3264
+ userMessage = `Failed to upload ${group.label}: Network error.
3265
+ ` + ` Error: ${errorMessage}
3266
+ ` + ` Check your internet connection and try again.`;
3267
+ }
3268
+ console.error(error(userMessage, useColors));
3269
+ }
3270
+ }
3271
+ console.log(`
3272
+ ${success(`Project initialization complete!`, useColors)}`);
3273
+ console.log(dim(`
3274
+ View your project: `, useColors) + highlight(`${deps.baseUrl}/projects/${projectName}`, useColors));
3275
+ console.log(dim(`
3276
+ To upload updated manifests later, run: `, useColors) + highlight(`pkgseer project upload`, useColors));
3277
+ return;
3278
+ }
3279
+ } else {
3280
+ console.log(dim(`
3281
+ No manifest files were found in the current directory.`, useColors));
3162
3282
  }
3163
- if (options.json) {
3164
- const quality = result.data.packageQuality.quality;
3165
- const slim = {
3166
- package: `${result.data.packageQuality.package?.name}@${result.data.packageQuality.package?.version}`,
3167
- score: quality?.overallScore,
3168
- grade: quality?.grade,
3169
- categories: quality?.categories?.filter((c) => c).map((c) => ({
3170
- name: c.category,
3171
- score: c.score
3172
- }))
3173
- };
3174
- output(slim, true);
3283
+ await configService.writeProjectConfig(configToWrite);
3284
+ console.log(success(`Configuration saved to ${highlight("pkgseer.yml", useColors)}`, useColors));
3285
+ if (manifestGroups.length > 0) {
3286
+ console.log(dim(`
3287
+ Next steps:`, useColors));
3288
+ console.log(dim(` 1. Edit pkgseer.yml to customize manifest labels if needed`, useColors));
3289
+ console.log(dim(` 2. Run: `, useColors) + highlight(`pkgseer project upload`, useColors));
3290
+ console.log(dim(`
3291
+ Tip: Use `, useColors) + highlight(`pkgseer project detect`, useColors) + dim(` to automatically update your config when you add new manifest files.`, useColors));
3175
3292
  } else {
3176
- console.log(formatPackageQuality(result.data.packageQuality));
3293
+ console.log(dim(`
3294
+ Next steps:`, useColors));
3295
+ console.log(dim(` 1. Add manifest files to your project`, useColors));
3296
+ console.log(dim(` 2. Edit pkgseer.yml to configure them:`, useColors));
3297
+ console.log(dim(`
3298
+ manifests:`, useColors));
3299
+ console.log(dim(` - label: backend`, useColors));
3300
+ console.log(dim(` files:`, useColors));
3301
+ console.log(dim(` - `, useColors) + highlight(`package-lock.json`, useColors));
3302
+ console.log(dim(`
3303
+ 3. Run: `, useColors) + highlight(`pkgseer project upload`, useColors));
3304
+ console.log(dim(`
3305
+ Tip: Run `, useColors) + highlight(`pkgseer project detect`, useColors) + dim(` after adding manifest files to auto-configure them.`, useColors));
3177
3306
  }
3178
3307
  }
3179
- var QUALITY_DESCRIPTION = `Get package quality score and breakdown.
3308
+ var INIT_DESCRIPTION = `Initialize a new project in the current directory.
3180
3309
 
3181
- Analyzes package quality across multiple dimensions:
3182
- - Maintenance and activity
3183
- - Documentation coverage
3184
- - Security practices
3185
- - Community engagement
3310
+ This command will:
3311
+ 1. Create a new project entry (or prompt for a project name)
3312
+ 2. Scan for manifest files (package.json, requirements.txt, etc.)
3313
+ 3. Suggest labels based on directory structure
3314
+ 4. Optionally upload manifests to the project
3186
3315
 
3187
- Examples:
3188
- pkgseer pkg quality lodash
3189
- pkgseer pkg quality express -v 4.18.0
3190
- pkgseer pkg quality requests --registry pypi --json`;
3191
- function registerPkgQualityCommand(program) {
3192
- program.command("quality <package>").summary("Get package quality score").description(QUALITY_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-v, --pkg-version <version>", "Package version").option("--json", "Output as JSON").action(async (packageName, options) => {
3193
- await withCliErrorHandling(options.json ?? false, async () => {
3194
- const deps = await createContainer();
3195
- await pkgQualityAction(packageName, options, deps);
3316
+ The project name defaults to the current directory name, or you can
3317
+ specify it with --name. Manifest files are automatically detected
3318
+ and grouped by their directory structure.`;
3319
+ function registerProjectInitCommand(program) {
3320
+ program.command("init").summary("Initialize a new project").description(INIT_DESCRIPTION).option("--name <name>", "Project name (alphanumeric, hyphens, underscores only)").option("--max-depth <depth>", "Maximum directory depth to scan for manifests", (val) => Number.parseInt(val, 10), 3).action(async (options) => {
3321
+ const deps = await createContainer();
3322
+ await projectInitAction({ name: options.name, maxDepth: options.maxDepth }, {
3323
+ projectService: deps.projectService,
3324
+ configService: deps.configService,
3325
+ fileSystemService: deps.fileSystemService,
3326
+ gitService: deps.gitService,
3327
+ promptService: deps.promptService,
3328
+ shellService: deps.shellService,
3329
+ baseUrl: deps.baseUrl
3196
3330
  });
3197
3331
  });
3198
3332
  }
3199
- // src/commands/pkg/vulns.ts
3200
- function getSeverityLabel(score) {
3201
- if (score == null)
3202
- return "UNKNOWN";
3203
- if (score >= 9)
3204
- return "CRITICAL";
3205
- if (score >= 7)
3206
- return "HIGH";
3207
- if (score >= 4)
3208
- return "MEDIUM";
3209
- return "LOW";
3210
- }
3211
- function formatPackageVulnerabilities(data) {
3212
- const lines = [];
3213
- const pkg = data.package;
3214
- const security = data.security;
3215
- if (!pkg) {
3216
- return "No package data available.";
3333
+
3334
+ // src/commands/init.ts
3335
+ async function initAction(options, deps) {
3336
+ const useColors = shouldUseColors2();
3337
+ console.log("Welcome to PkgSeer!");
3338
+ console.log(`───────────────────
3339
+ `);
3340
+ console.log(`Set up PkgSeer for your project:
3341
+ ` + ` 1. Project configuration – track dependencies and monitor vulnerabilities
3342
+ ` + ` 2. MCP server – enable AI assistant integration
3343
+ `);
3344
+ console.log(dim(`Or run separately: pkgseer project init, pkgseer mcp init
3345
+ `, useColors));
3346
+ let setupProject = !options.skipProject;
3347
+ let setupMcp = !options.skipMcp;
3348
+ if (options.skipProject && options.skipMcp) {
3349
+ showCliUsage(useColors);
3350
+ return;
3217
3351
  }
3218
- lines.push(`\uD83D\uDD12 Security Report: ${pkg.name}@${pkg.version}`);
3219
- lines.push("");
3220
- if (!security?.vulnerabilities || security.vulnerabilities.length === 0) {
3221
- lines.push("✅ No known vulnerabilities!");
3222
- return lines.join(`
3352
+ if (!options.skipProject && !options.skipMcp) {
3353
+ const choice = await deps.promptService.select("What would you like to set up?", [
3354
+ {
3355
+ value: "both",
3356
+ name: "Both project and MCP server (recommended)",
3357
+ description: "Full setup: dependency tracking + AI assistant integration"
3358
+ },
3359
+ {
3360
+ value: "project",
3361
+ name: "Project only",
3362
+ description: "Track dependencies and monitor for vulnerabilities"
3363
+ },
3364
+ {
3365
+ value: "mcp",
3366
+ name: "MCP server only",
3367
+ description: "Enable AI assistant integration"
3368
+ },
3369
+ {
3370
+ value: "cli",
3371
+ name: "CLI usage (no setup)",
3372
+ description: "Use PkgSeer as a command-line tool"
3373
+ }
3374
+ ]);
3375
+ if (choice === "cli") {
3376
+ showCliUsage(useColors);
3377
+ return;
3378
+ }
3379
+ setupProject = choice === "both" || choice === "project";
3380
+ setupMcp = choice === "both" || choice === "mcp";
3381
+ }
3382
+ const projectInit = deps.projectInitAction ?? projectInitAction;
3383
+ const mcpInit = deps.mcpInitAction ?? mcpInitAction;
3384
+ if (setupProject) {
3385
+ const existingConfig = await deps.configService.loadProjectConfig();
3386
+ if (existingConfig?.config.project) {
3387
+ console.log(`
3388
+ Project already configured: ${highlight(existingConfig.config.project, useColors)}`);
3389
+ console.log(dim(`Skipping project setup. Run 'pkgseer project init' to reinitialize.
3390
+ `, useColors));
3391
+ setupProject = false;
3392
+ } else {
3393
+ console.log(`
3394
+ ` + "=".repeat(50));
3395
+ console.log("Project Configuration Setup");
3396
+ console.log("=".repeat(50) + `
3397
+ `);
3398
+ await projectInit({}, {
3399
+ projectService: deps.projectService,
3400
+ configService: deps.configService,
3401
+ fileSystemService: deps.fileSystemService,
3402
+ gitService: deps.gitService,
3403
+ promptService: deps.promptService,
3404
+ shellService: deps.shellService,
3405
+ baseUrl: deps.baseUrl
3406
+ });
3407
+ const updatedConfig = await deps.configService.loadProjectConfig();
3408
+ if (updatedConfig?.config.project) {
3409
+ console.log(`
3410
+ ${highlight("✓", useColors)} Project setup complete!
3411
+ `);
3412
+ }
3413
+ }
3414
+ }
3415
+ if (setupMcp) {
3416
+ const currentConfig = await deps.configService.loadProjectConfig();
3417
+ const hasProjectNow = currentConfig?.config.project !== undefined;
3418
+ console.log(`
3419
+ ` + "=".repeat(50));
3420
+ console.log("MCP Server Setup");
3421
+ console.log("=".repeat(50) + `
3422
+ `);
3423
+ await mcpInit({
3424
+ fileSystemService: deps.fileSystemService,
3425
+ promptService: deps.promptService,
3426
+ configService: deps.configService,
3427
+ baseUrl: deps.baseUrl,
3428
+ hasProject: hasProjectNow
3429
+ });
3430
+ console.log(`
3431
+ ${highlight("✓", useColors)} MCP setup complete!
3223
3432
  `);
3224
3433
  }
3225
- const bySeverity = security.vulnerabilities.reduce((acc, v) => {
3226
- if (v) {
3227
- const label = getSeverityLabel(v.severityScore);
3228
- acc[label] = (acc[label] || 0) + 1;
3434
+ console.log("=".repeat(50));
3435
+ console.log("Setup Complete!");
3436
+ console.log("=".repeat(50) + `
3437
+ `);
3438
+ if (setupProject) {
3439
+ const finalConfig = await deps.configService.loadProjectConfig();
3440
+ if (finalConfig?.config.project) {
3441
+ console.log(`Project: ${highlight(finalConfig.config.project, useColors)}`);
3442
+ console.log(dim(` View at: ${deps.baseUrl}/projects/${finalConfig.config.project}`, useColors));
3229
3443
  }
3230
- return acc;
3231
- }, {});
3232
- lines.push(`⚠️ Found ${security.vulnerabilityCount} vulnerabilities:`);
3233
- for (const [severity, count] of Object.entries(bySeverity)) {
3234
- lines.push(` ${formatSeverity(severity)}: ${count}`);
3235
3444
  }
3236
- lines.push("");
3237
- lines.push("Details:");
3238
- for (const vuln of security.vulnerabilities) {
3239
- if (!vuln)
3240
- continue;
3241
- lines.push("");
3242
- lines.push(` ${formatSeverity(getSeverityLabel(vuln.severityScore))}`);
3243
- lines.push(` ${vuln.summary || vuln.osvId}`);
3244
- lines.push(` ID: ${vuln.osvId}`);
3245
- if (vuln.fixedInVersions && vuln.fixedInVersions.length > 0) {
3246
- lines.push(` Fixed in: ${vuln.fixedInVersions.join(", ")}`);
3445
+ if (setupMcp) {
3446
+ console.log("MCP Server: Configured");
3447
+ console.log(dim(" Restart your AI assistant to activate the MCP server.", useColors));
3448
+ }
3449
+ console.log(dim(`
3450
+ Next steps:
3451
+ ` + ` • Use CLI commands: pkgseer pkg info <package>
3452
+ ` + ` Search docs: pkgseer docs search <query>
3453
+ ` + " • Quick reference: pkgseer quickstart", useColors));
3454
+ }
3455
+ function showCliUsage(useColors) {
3456
+ console.log("Using PkgSeer via CLI");
3457
+ console.log(`─────────────────────
3458
+ `);
3459
+ console.log(`PkgSeer works great as a standalone CLI tool. Your AI assistant
3460
+ ` + `can run these commands directly:
3461
+ `);
3462
+ console.log("Package Commands:");
3463
+ console.log(` ${highlight("pkgseer pkg info <package>", useColors)} Get package summary`);
3464
+ console.log(` ${highlight("pkgseer pkg vulns <package>", useColors)} Check vulnerabilities`);
3465
+ console.log(` ${highlight("pkgseer pkg quality <package>", useColors)} Get quality score`);
3466
+ console.log(` ${highlight("pkgseer pkg deps <package>", useColors)} List dependencies`);
3467
+ console.log(` ${highlight("pkgseer pkg compare <pkg...>", useColors)} Compare packages
3468
+ `);
3469
+ console.log("Documentation Commands:");
3470
+ console.log(` ${highlight("pkgseer docs list <package>", useColors)} List doc pages`);
3471
+ console.log(` ${highlight("pkgseer docs get <pkg>/<page>", useColors)} Fetch doc content`);
3472
+ console.log(` ${highlight("pkgseer docs search <query>", useColors)} Search documentation
3473
+ `);
3474
+ console.log(dim(`All commands support --json for structured output.
3475
+ ` + "Tip: Run 'pkgseer quickstart' for a quick reference guide.", useColors));
3476
+ }
3477
+ function registerInitCommand(program) {
3478
+ program.command("init").summary("Set up project and MCP server").description(`Set up PkgSeer for your project.
3479
+
3480
+ Guides you through:
3481
+ • Project configuration – track dependencies, monitor vulnerabilities
3482
+ • MCP server – enable AI assistant integration
3483
+
3484
+ Run separately: pkgseer project init, pkgseer mcp init`).option("--skip-project", "Skip project setup").option("--skip-mcp", "Skip MCP setup").action(async (options) => {
3485
+ const deps = await createContainer();
3486
+ await initAction(options, deps);
3487
+ });
3488
+ }
3489
+
3490
+ // src/commands/login.ts
3491
+ import { hostname } from "node:os";
3492
+ var TIMEOUT_MS = 5 * 60 * 1000;
3493
+ function randomPort() {
3494
+ return Math.floor(Math.random() * 2000) + 8000;
3495
+ }
3496
+ async function loginAction(options, deps) {
3497
+ const { authService, authStorage, browserService, baseUrl } = deps;
3498
+ const existing = await authStorage.load(baseUrl);
3499
+ if (existing && !options.force) {
3500
+ const isExpired = existing.expiresAt && new Date(existing.expiresAt) < new Date;
3501
+ if (!isExpired) {
3502
+ console.log(`Already logged in.
3503
+ `);
3504
+ console.log(` Environment: ${baseUrl}`);
3505
+ console.log(` Token: ${existing.tokenName}
3506
+ `);
3507
+ console.log("To switch accounts, run `pkgseer logout` first.");
3508
+ console.log("To re-authenticate with different scopes, use `pkgseer login --force`.");
3509
+ return;
3510
+ }
3511
+ console.log(`Token expired. Starting new login...
3512
+ `);
3513
+ } else if (existing && options.force) {
3514
+ console.log(`Re-authenticating (--force flag)...
3515
+ `);
3516
+ }
3517
+ const { verifier, challenge, state } = authService.generatePkceParams();
3518
+ const port = options.port ?? randomPort();
3519
+ const authUrl = authService.buildAuthUrl({
3520
+ state,
3521
+ port,
3522
+ codeChallenge: challenge,
3523
+ hostname: hostname()
3524
+ });
3525
+ const serverPromise = authService.startCallbackServer(port);
3526
+ if (options.browser === false) {
3527
+ console.log(`Open this URL in your browser:
3528
+ `);
3529
+ console.log(` ${authUrl}
3530
+ `);
3531
+ } else {
3532
+ console.log("Opening browser...");
3533
+ await browserService.open(authUrl);
3534
+ }
3535
+ console.log(`Waiting for authentication...
3536
+ `);
3537
+ let timeoutId;
3538
+ const timeoutPromise = new Promise((_, reject) => {
3539
+ timeoutId = setTimeout(() => reject(new Error("Authentication timed out")), TIMEOUT_MS);
3540
+ });
3541
+ let callback;
3542
+ try {
3543
+ callback = await Promise.race([serverPromise, timeoutPromise]);
3544
+ clearTimeout(timeoutId);
3545
+ } catch (error2) {
3546
+ clearTimeout(timeoutId);
3547
+ if (error2 instanceof Error) {
3548
+ console.log(`${error2.message}.
3549
+ `);
3550
+ console.log("Run `pkgseer login` to try again.");
3551
+ }
3552
+ process.exit(1);
3553
+ }
3554
+ if (callback.state !== state) {
3555
+ console.error(`Security error: authentication state mismatch.
3556
+ `);
3557
+ console.log("This could indicate a security issue. Please try again.");
3558
+ process.exit(1);
3559
+ }
3560
+ let tokenResponse;
3561
+ try {
3562
+ tokenResponse = await authService.exchangeCodeForToken({
3563
+ code: callback.code,
3564
+ codeVerifier: verifier,
3565
+ state
3566
+ });
3567
+ } catch (error2) {
3568
+ console.error(`Failed to complete authentication: ${error2 instanceof Error ? error2.message : error2}
3569
+ `);
3570
+ console.log("Run `pkgseer login` to try again.");
3571
+ process.exit(1);
3572
+ }
3573
+ await authStorage.save(baseUrl, {
3574
+ token: tokenResponse.token,
3575
+ tokenName: tokenResponse.tokenName,
3576
+ scopes: tokenResponse.scopes,
3577
+ createdAt: new Date().toISOString(),
3578
+ expiresAt: tokenResponse.expiresAt,
3579
+ apiKeyId: tokenResponse.apiKeyId
3580
+ });
3581
+ console.log(`✓ Logged in
3582
+ `);
3583
+ console.log(` Environment: ${baseUrl}`);
3584
+ console.log(` Token: ${tokenResponse.tokenName}`);
3585
+ if (tokenResponse.expiresAt) {
3586
+ const days = Math.ceil((new Date(tokenResponse.expiresAt).getTime() - Date.now()) / (1000 * 60 * 60 * 24));
3587
+ console.log(` Expires: in ${days} days`);
3588
+ }
3589
+ console.log(`
3590
+ You're ready to use pkgseer with your AI assistant.`);
3591
+ }
3592
+ var LOGIN_DESCRIPTION = `Authenticate with your PkgSeer account via browser.
3593
+
3594
+ Opens your browser to complete authentication securely. The CLI receives
3595
+ a token that's stored locally and used for API requests.
3596
+
3597
+ Use --no-browser in environments without a display (CI, SSH sessions)
3598
+ to get a URL you can open on another device.`;
3599
+ function registerLoginCommand(program) {
3600
+ program.command("login").summary("Authenticate with your PkgSeer account").description(LOGIN_DESCRIPTION).option("--no-browser", "Print URL instead of opening browser").option("--port <port>", "Port for local callback server", parseInt).option("--force", "Re-authenticate even if already logged in").action(async (options) => {
3601
+ const deps = await createContainer();
3602
+ await loginAction(options, deps);
3603
+ });
3604
+ }
3605
+
3606
+ // src/commands/logout.ts
3607
+ async function logoutAction(deps) {
3608
+ const { authService, authStorage, baseUrl } = deps;
3609
+ const auth = await authStorage.load(baseUrl);
3610
+ if (!auth) {
3611
+ console.log(`Not currently logged in.
3612
+ `);
3613
+ console.log(` Environment: ${baseUrl}`);
3614
+ return;
3615
+ }
3616
+ try {
3617
+ await authService.revokeToken(auth.token);
3618
+ } catch {}
3619
+ await authStorage.clear(baseUrl);
3620
+ console.log(`✓ Logged out
3621
+ `);
3622
+ console.log(` Environment: ${baseUrl}`);
3623
+ }
3624
+ var LOGOUT_DESCRIPTION = `Remove stored credentials and revoke the token.
3625
+
3626
+ Clears the locally stored authentication token and notifies the server
3627
+ to revoke it. Use this when switching accounts or on shared machines.`;
3628
+ function registerLogoutCommand(program) {
3629
+ program.command("logout").summary("Remove stored credentials").description(LOGOUT_DESCRIPTION).action(async () => {
3630
+ const deps = await createContainer();
3631
+ await logoutAction(deps);
3632
+ });
3633
+ }
3634
+
3635
+ // src/commands/mcp.ts
3636
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3637
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3638
+
3639
+ // src/tools/compare-packages.ts
3640
+ import { z as z3 } from "zod";
3641
+
3642
+ // src/tools/shared.ts
3643
+ import { z as z2 } from "zod";
3644
+
3645
+ // src/tools/types.ts
3646
+ function textResult(text) {
3647
+ return {
3648
+ content: [{ type: "text", text }]
3649
+ };
3650
+ }
3651
+ function errorResult(message) {
3652
+ return {
3653
+ content: [{ type: "text", text: message }],
3654
+ isError: true
3655
+ };
3656
+ }
3657
+
3658
+ // src/tools/shared.ts
3659
+ function toGraphQLRegistry2(registry) {
3660
+ const map = {
3661
+ npm: "NPM",
3662
+ pypi: "PYPI",
3663
+ hex: "HEX"
3664
+ };
3665
+ return map[registry.toLowerCase()] || "NPM";
3666
+ }
3667
+ var schemas = {
3668
+ registry: z2.enum(["npm", "pypi", "hex"]).describe("Package registry (npm, pypi, or hex)"),
3669
+ packageName: z2.string().max(255).describe("Name of the package"),
3670
+ version: z2.string().max(100).optional().describe("Specific version (defaults to latest)")
3671
+ };
3672
+ function handleGraphQLErrors(errors) {
3673
+ if (errors && errors.length > 0) {
3674
+ return errorResult(`Error: ${errors.map((e) => e.message).join(", ")}`);
3675
+ }
3676
+ return null;
3677
+ }
3678
+ async function withErrorHandling(operation, fn) {
3679
+ try {
3680
+ return await fn();
3681
+ } catch (error2) {
3682
+ const message = error2 instanceof Error ? error2.message : "Unknown error";
3683
+ return errorResult(`Failed to ${operation}: ${message}`);
3684
+ }
3685
+ }
3686
+ function notFoundError(packageName, registry) {
3687
+ return errorResult(`Package not found: ${packageName} in ${registry}`);
3688
+ }
3689
+
3690
+ // src/tools/compare-packages.ts
3691
+ var packageInputSchema = z3.object({
3692
+ registry: z3.enum(["npm", "pypi", "hex"]),
3693
+ name: z3.string().max(255),
3694
+ version: z3.string().max(100).optional()
3695
+ });
3696
+ var argsSchema = {
3697
+ packages: z3.array(packageInputSchema).min(2).max(10).describe("List of packages to compare (2-10 packages)")
3698
+ };
3699
+ function createComparePackagesTool(pkgseerService) {
3700
+ return {
3701
+ name: "compare_packages",
3702
+ description: "Compares multiple packages across metadata, quality, and security dimensions",
3703
+ schema: argsSchema,
3704
+ handler: async ({ packages }, _extra) => {
3705
+ return withErrorHandling("compare packages", async () => {
3706
+ const input2 = packages.map((pkg) => ({
3707
+ registry: toGraphQLRegistry2(pkg.registry),
3708
+ name: pkg.name,
3709
+ version: pkg.version
3710
+ }));
3711
+ const result = await pkgseerService.comparePackages(input2);
3712
+ const graphqlError = handleGraphQLErrors(result.errors);
3713
+ if (graphqlError)
3714
+ return graphqlError;
3715
+ if (!result.data.comparePackages) {
3716
+ return errorResult("Comparison failed: no results returned");
3717
+ }
3718
+ return textResult(JSON.stringify(result.data.comparePackages, null, 2));
3719
+ });
3720
+ }
3721
+ };
3722
+ }
3723
+ // src/tools/fetch-package-doc.ts
3724
+ import { z as z4 } from "zod";
3725
+ var argsSchema2 = {
3726
+ registry: schemas.registry,
3727
+ package_name: schemas.packageName.describe("Name of the package to fetch documentation for"),
3728
+ page_id: z4.string().max(500).describe("Documentation page identifier (from list_package_docs)"),
3729
+ version: schemas.version
3730
+ };
3731
+ function createFetchPackageDocTool(pkgseerService) {
3732
+ return {
3733
+ name: "fetch_package_doc",
3734
+ description: "Fetches the full content of a specific documentation page. Returns page title, content (markdown/HTML), breadcrumbs, and source attribution. Use list_package_docs first to get available page IDs.",
3735
+ schema: argsSchema2,
3736
+ handler: async ({ registry, package_name, page_id, version: version2 }, _extra) => {
3737
+ return withErrorHandling("fetch documentation page", async () => {
3738
+ const result = await pkgseerService.fetchPackageDoc(toGraphQLRegistry2(registry), package_name, page_id, version2);
3739
+ const graphqlError = handleGraphQLErrors(result.errors);
3740
+ if (graphqlError)
3741
+ return graphqlError;
3742
+ if (!result.data.fetchPackageDoc) {
3743
+ return errorResult(`Documentation page not found: ${page_id} for ${package_name} in ${registry}`);
3744
+ }
3745
+ return textResult(JSON.stringify(result.data.fetchPackageDoc, null, 2));
3746
+ });
3747
+ }
3748
+ };
3749
+ }
3750
+ // src/tools/list-package-docs.ts
3751
+ var argsSchema3 = {
3752
+ registry: schemas.registry,
3753
+ package_name: schemas.packageName.describe("Name of the package to list documentation for"),
3754
+ version: schemas.version
3755
+ };
3756
+ function createListPackageDocsTool(pkgseerService) {
3757
+ return {
3758
+ name: "list_package_docs",
3759
+ description: "Lists documentation pages for a package version. Returns page titles, slugs, and metadata. Use this to discover available documentation before fetching specific pages.",
3760
+ schema: argsSchema3,
3761
+ handler: async ({ registry, package_name, version: version2 }, _extra) => {
3762
+ return withErrorHandling("list package documentation", async () => {
3763
+ const result = await pkgseerService.listPackageDocs(toGraphQLRegistry2(registry), package_name, version2);
3764
+ const graphqlError = handleGraphQLErrors(result.errors);
3765
+ if (graphqlError)
3766
+ return graphqlError;
3767
+ if (!result.data.listPackageDocs) {
3768
+ return notFoundError(package_name, registry);
3769
+ }
3770
+ return textResult(JSON.stringify(result.data.listPackageDocs, null, 2));
3771
+ });
3772
+ }
3773
+ };
3774
+ }
3775
+ // src/tools/package-dependencies.ts
3776
+ import { z as z5 } from "zod";
3777
+ var argsSchema4 = {
3778
+ registry: schemas.registry,
3779
+ package_name: schemas.packageName.describe("Name of the package to retrieve dependencies for"),
3780
+ version: schemas.version,
3781
+ include_transitive: z5.boolean().optional().describe("Whether to include transitive dependency DAG"),
3782
+ max_depth: z5.number().int().min(1).max(10).optional().describe("Maximum depth for transitive traversal (1-10)")
3783
+ };
3784
+ function createPackageDependenciesTool(pkgseerService) {
3785
+ return {
3786
+ name: "package_dependencies",
3787
+ description: "Retrieves direct and transitive dependencies for a package version",
3788
+ schema: argsSchema4,
3789
+ handler: async ({ registry, package_name, version: version2, include_transitive, max_depth }, _extra) => {
3790
+ return withErrorHandling("fetch package dependencies", async () => {
3791
+ const result = await pkgseerService.getPackageDependencies(toGraphQLRegistry2(registry), package_name, version2, include_transitive, max_depth);
3792
+ const graphqlError = handleGraphQLErrors(result.errors);
3793
+ if (graphqlError)
3794
+ return graphqlError;
3795
+ if (!result.data.packageDependencies) {
3796
+ return notFoundError(package_name, registry);
3797
+ }
3798
+ return textResult(JSON.stringify(result.data.packageDependencies, null, 2));
3799
+ });
3800
+ }
3801
+ };
3802
+ }
3803
+ // src/tools/package-quality.ts
3804
+ var argsSchema5 = {
3805
+ registry: schemas.registry,
3806
+ package_name: schemas.packageName.describe("Name of the package to analyze"),
3807
+ version: schemas.version
3808
+ };
3809
+ function createPackageQualityTool(pkgseerService) {
3810
+ return {
3811
+ name: "package_quality",
3812
+ description: "Retrieves quality score and rule-level breakdown for a package",
3813
+ schema: argsSchema5,
3814
+ handler: async ({ registry, package_name, version: version2 }, _extra) => {
3815
+ return withErrorHandling("fetch package quality", async () => {
3816
+ const result = await pkgseerService.getPackageQuality(toGraphQLRegistry2(registry), package_name, version2);
3817
+ const graphqlError = handleGraphQLErrors(result.errors);
3818
+ if (graphqlError)
3819
+ return graphqlError;
3820
+ if (!result.data.packageQuality) {
3821
+ return notFoundError(package_name, registry);
3822
+ }
3823
+ return textResult(JSON.stringify(result.data.packageQuality, null, 2));
3824
+ });
3825
+ }
3826
+ };
3827
+ }
3828
+ // src/tools/package-summary.ts
3829
+ var argsSchema6 = {
3830
+ registry: schemas.registry,
3831
+ package_name: schemas.packageName.describe("Name of the package to retrieve summary for")
3832
+ };
3833
+ function createPackageSummaryTool(pkgseerService) {
3834
+ return {
3835
+ name: "package_summary",
3836
+ description: "Retrieves comprehensive package summary including metadata, versions, security advisories, and quickstart information",
3837
+ schema: argsSchema6,
3838
+ handler: async ({ registry, package_name }, _extra) => {
3839
+ return withErrorHandling("fetch package summary", async () => {
3840
+ const result = await pkgseerService.getPackageSummary(toGraphQLRegistry2(registry), package_name);
3841
+ const graphqlError = handleGraphQLErrors(result.errors);
3842
+ if (graphqlError)
3843
+ return graphqlError;
3844
+ if (!result.data.packageSummary) {
3845
+ return notFoundError(package_name, registry);
3846
+ }
3847
+ return textResult(JSON.stringify(result.data.packageSummary, null, 2));
3848
+ });
3849
+ }
3850
+ };
3851
+ }
3852
+ // src/tools/package-vulnerabilities.ts
3853
+ var argsSchema7 = {
3854
+ registry: schemas.registry,
3855
+ package_name: schemas.packageName.describe("Name of the package to inspect for vulnerabilities"),
3856
+ version: schemas.version
3857
+ };
3858
+ function createPackageVulnerabilitiesTool(pkgseerService) {
3859
+ return {
3860
+ name: "package_vulnerabilities",
3861
+ description: "Retrieves vulnerability details for a package, including affected version ranges and upgrade guidance",
3862
+ schema: argsSchema7,
3863
+ handler: async ({ registry, package_name, version: version2 }, _extra) => {
3864
+ return withErrorHandling("fetch package vulnerabilities", async () => {
3865
+ const result = await pkgseerService.getPackageVulnerabilities(toGraphQLRegistry2(registry), package_name, version2);
3866
+ const graphqlError = handleGraphQLErrors(result.errors);
3867
+ if (graphqlError)
3868
+ return graphqlError;
3869
+ if (!result.data.packageVulnerabilities) {
3870
+ return notFoundError(package_name, registry);
3871
+ }
3872
+ return textResult(JSON.stringify(result.data.packageVulnerabilities, null, 2));
3873
+ });
3874
+ }
3875
+ };
3876
+ }
3877
+ // src/tools/search-package-docs.ts
3878
+ import { z as z6 } from "zod";
3879
+ var argsSchema8 = {
3880
+ registry: schemas.registry,
3881
+ package_name: schemas.packageName.describe("Name of the package to search documentation for"),
3882
+ keywords: z6.array(z6.string()).optional().describe("Keywords to search for; combined into a single query"),
3883
+ query: z6.string().max(500).optional().describe("Freeform search query (alternative to keywords)"),
3884
+ include_snippets: z6.boolean().optional().describe("Include content excerpts around matches"),
3885
+ limit: z6.number().int().min(1).max(100).optional().describe("Maximum number of results to return"),
3886
+ version: schemas.version
3887
+ };
3888
+ function createSearchPackageDocsTool(pkgseerService) {
3889
+ return {
3890
+ name: "search_package_docs",
3891
+ description: "Searches package documentation with keyword or freeform query support. Returns ranked results with relevance scores. Use include_snippets=true to get content excerpts showing match context.",
3892
+ schema: argsSchema8,
3893
+ handler: async ({
3894
+ registry,
3895
+ package_name,
3896
+ keywords,
3897
+ query,
3898
+ include_snippets,
3899
+ limit,
3900
+ version: version2
3901
+ }, _extra) => {
3902
+ return withErrorHandling("search package documentation", async () => {
3903
+ if (!keywords?.length && !query) {
3904
+ return errorResult("Either keywords or query must be provided for search");
3905
+ }
3906
+ const result = await pkgseerService.searchPackageDocs(toGraphQLRegistry2(registry), package_name, {
3907
+ keywords,
3908
+ query,
3909
+ includeSnippets: include_snippets,
3910
+ limit,
3911
+ version: version2
3912
+ });
3913
+ const graphqlError = handleGraphQLErrors(result.errors);
3914
+ if (graphqlError)
3915
+ return graphqlError;
3916
+ if (!result.data.searchPackageDocs) {
3917
+ return errorResult(`No documentation found for ${package_name} in ${registry}`);
3918
+ }
3919
+ return textResult(JSON.stringify(result.data.searchPackageDocs, null, 2));
3920
+ });
3921
+ }
3922
+ };
3923
+ }
3924
+ // src/tools/search-project-docs.ts
3925
+ import { z as z7 } from "zod";
3926
+ var argsSchema9 = {
3927
+ project: z7.string().optional().describe("Project name to search. Optional if configured in pkgseer.yml; only needed to search a different project."),
3928
+ keywords: z7.array(z7.string()).optional().describe("Keywords to search for; combined into a single query"),
3929
+ query: z7.string().max(500).optional().describe("Freeform search query (alternative to keywords)"),
3930
+ include_snippets: z7.boolean().optional().describe("Include content excerpts around matches"),
3931
+ limit: z7.number().int().min(1).max(100).optional().describe("Maximum number of results to return")
3932
+ };
3933
+ function createSearchProjectDocsTool(deps) {
3934
+ const { pkgseerService, config } = deps;
3935
+ return {
3936
+ name: "search_project_docs",
3937
+ description: "Searches documentation across all dependencies in a PkgSeer project. Returns ranked results from multiple packages. Uses project from pkgseer.yml config by default.",
3938
+ schema: argsSchema9,
3939
+ handler: async ({ project, keywords, query, include_snippets, limit }, _extra) => {
3940
+ return withErrorHandling("search project documentation", async () => {
3941
+ const resolvedProject = project ?? config.project;
3942
+ if (!resolvedProject) {
3943
+ return errorResult("No project provided and none configured in pkgseer.yml. " + "Either pass project parameter or add project to your config.");
3944
+ }
3945
+ if (!keywords?.length && !query) {
3946
+ return errorResult("Either keywords or query must be provided for search");
3947
+ }
3948
+ const result = await pkgseerService.searchProjectDocs(resolvedProject, {
3949
+ keywords,
3950
+ query,
3951
+ includeSnippets: include_snippets,
3952
+ limit
3953
+ });
3954
+ const graphqlError = handleGraphQLErrors(result.errors);
3955
+ if (graphqlError)
3956
+ return graphqlError;
3957
+ if (!result.data.searchProjectDocs) {
3958
+ return errorResult(`Project not found: ${resolvedProject}`);
3959
+ }
3960
+ return textResult(JSON.stringify(result.data.searchProjectDocs, null, 2));
3961
+ });
3247
3962
  }
3963
+ };
3964
+ }
3965
+ // src/commands/mcp.ts
3966
+ var TOOL_FACTORIES = {
3967
+ package_summary: ({ pkgseerService }) => createPackageSummaryTool(pkgseerService),
3968
+ package_vulnerabilities: ({ pkgseerService }) => createPackageVulnerabilitiesTool(pkgseerService),
3969
+ package_dependencies: ({ pkgseerService }) => createPackageDependenciesTool(pkgseerService),
3970
+ package_quality: ({ pkgseerService }) => createPackageQualityTool(pkgseerService),
3971
+ compare_packages: ({ pkgseerService }) => createComparePackagesTool(pkgseerService),
3972
+ list_package_docs: ({ pkgseerService }) => createListPackageDocsTool(pkgseerService),
3973
+ fetch_package_doc: ({ pkgseerService }) => createFetchPackageDocTool(pkgseerService),
3974
+ search_package_docs: ({ pkgseerService }) => createSearchPackageDocsTool(pkgseerService),
3975
+ search_project_docs: ({ pkgseerService, config }) => createSearchProjectDocsTool({ pkgseerService, config })
3976
+ };
3977
+ var PUBLIC_READ_TOOLS = [
3978
+ "package_summary",
3979
+ "package_vulnerabilities",
3980
+ "package_dependencies",
3981
+ "package_quality",
3982
+ "compare_packages",
3983
+ "list_package_docs",
3984
+ "fetch_package_doc",
3985
+ "search_package_docs"
3986
+ ];
3987
+ var PROJECT_READ_TOOLS = ["search_project_docs"];
3988
+ var ALL_TOOLS = [...PUBLIC_READ_TOOLS, ...PROJECT_READ_TOOLS];
3989
+ function createMcpServer(deps) {
3990
+ const { pkgseerService, config } = deps;
3991
+ const server = new McpServer({
3992
+ name: "pkgseer",
3993
+ version: "0.1.0"
3994
+ });
3995
+ const enabledToolNames = config.enabled_tools ?? ALL_TOOLS;
3996
+ const toolsToRegister = enabledToolNames.filter((name) => ALL_TOOLS.includes(name));
3997
+ for (const toolName of toolsToRegister) {
3998
+ const factory = TOOL_FACTORIES[toolName];
3999
+ const tool = factory({ pkgseerService, config });
4000
+ server.registerTool(tool.name, { description: tool.description, inputSchema: tool.schema }, tool.handler);
3248
4001
  }
3249
- return lines.join(`
3250
- `);
4002
+ return server;
3251
4003
  }
3252
- async function pkgVulnsAction(packageName, options, deps) {
3253
- const { pkgseerService } = deps;
3254
- const registry = toGraphQLRegistry(options.registry);
3255
- const result = await pkgseerService.cliPackageVulns(registry, packageName, options.pkgVersion);
3256
- handleErrors(result.errors, options.json ?? false);
3257
- if (!result.data.packageVulnerabilities) {
3258
- outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
4004
+ function requireAuth(deps) {
4005
+ if (deps.hasValidToken) {
3259
4006
  return;
3260
4007
  }
3261
- if (options.json) {
3262
- const data = result.data.packageVulnerabilities;
3263
- const vulns = data.security?.vulnerabilities ?? [];
3264
- const slim = {
3265
- package: `${data.package?.name}@${data.package?.version}`,
3266
- count: data.security?.vulnerabilityCount ?? 0,
3267
- vulnerabilities: vulns.filter((v) => v).map((v) => ({
3268
- id: v.osvId,
3269
- severity: v.severityScore,
3270
- summary: v.summary,
3271
- fixed: v.fixedInVersions
3272
- }))
3273
- };
3274
- output(slim, true);
3275
- } else {
3276
- console.log(formatPackageVulnerabilities(result.data.packageVulnerabilities));
4008
+ console.log(`Authentication required to start MCP server.
4009
+ `);
4010
+ if (deps.baseUrl !== "https://pkgseer.dev") {
4011
+ console.log(` Environment: ${deps.baseUrl}`);
4012
+ console.log(` You're using a custom environment.
4013
+ `);
3277
4014
  }
4015
+ console.log("To authenticate:");
4016
+ console.log(` pkgseer login
4017
+ `);
4018
+ console.log("Or set PKGSEER_API_TOKEN environment variable.");
4019
+ process.exit(1);
3278
4020
  }
3279
- var VULNS_DESCRIPTION = `Check package for security vulnerabilities.
3280
-
3281
- Scans for known security vulnerabilities and provides:
3282
- - Severity ratings (critical, high, medium, low)
3283
- - CVE identifiers
3284
- - Affected version ranges
3285
- - Upgrade recommendations
3286
-
3287
- Examples:
3288
- pkgseer pkg vulns lodash
3289
- pkgseer pkg vulns express -v 4.17.0
3290
- pkgseer pkg vulns requests --registry pypi --json`;
3291
- function registerPkgVulnsCommand(program) {
3292
- program.command("vulns <package>").summary("Check for security vulnerabilities").description(VULNS_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-v, --pkg-version <version>", "Package version").option("--json", "Output as JSON").action(async (packageName, options) => {
3293
- await withCliErrorHandling(options.json ?? false, async () => {
3294
- const deps = await createContainer();
3295
- await pkgVulnsAction(packageName, options, deps);
3296
- });
3297
- });
4021
+ async function startMcpServer(deps) {
4022
+ requireAuth(deps);
4023
+ const server = createMcpServer(deps);
4024
+ const transport = new StdioServerTransport;
4025
+ await server.connect(transport);
3298
4026
  }
3299
- // src/services/gitignore-parser.ts
3300
- function parseGitIgnoreLine(line) {
3301
- const trimmed = line.trimEnd();
3302
- if (!trimmed || trimmed.startsWith("#")) {
3303
- return null;
3304
- }
3305
- const isNegation = trimmed.startsWith("!");
3306
- const pattern = isNegation ? trimmed.slice(1) : trimmed;
3307
- const isRootOnly = pattern.startsWith("/");
3308
- const patternWithoutRoot = isRootOnly ? pattern.slice(1) : pattern;
3309
- const isDirectory = patternWithoutRoot.endsWith("/");
3310
- const cleanPattern = isDirectory ? patternWithoutRoot.slice(0, -1) : patternWithoutRoot;
3311
- return {
3312
- pattern: cleanPattern,
3313
- isNegation,
3314
- isDirectory,
3315
- isRootOnly
3316
- };
4027
+ function showMcpSetupInstructions(deps) {
4028
+ const useColors = shouldUseColors2();
4029
+ console.log("MCP Server Setup");
4030
+ console.log(`────────────────
4031
+ `);
4032
+ console.log(`Add PkgSeer to your AI assistant's MCP configuration.
4033
+ `);
4034
+ console.log(`${highlight("pkgseer mcp init", useColors)}`);
4035
+ console.log(dim(` Interactive setup for Cursor, Codex, or Claude Code
4036
+ `, useColors));
4037
+ console.log("Manual configuration:");
4038
+ console.log(` ${highlight(`${deps.baseUrl}/docs/mcp-server`, useColors)}
4039
+ `);
4040
+ console.log("Alternative: Use CLI directly (no MCP setup needed)");
4041
+ console.log(` ${highlight("pkgseer quickstart", useColors)}`);
3317
4042
  }
3318
- function matchesPattern(path, pattern) {
3319
- const { pattern: p, isDirectory, isRootOnly } = pattern;
3320
- let regexStr = p.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*\*/g, "___DOUBLE_STAR___").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]").replace(/___DOUBLE_STAR___/g, ".*");
3321
- if (isRootOnly) {
3322
- if (isDirectory) {
3323
- regexStr = `^${regexStr}(/|$)`;
3324
- } else {
3325
- regexStr = `^${regexStr}(/|$)`;
3326
- }
3327
- } else {
3328
- if (isDirectory) {
3329
- regexStr = `(^|/)${regexStr}(/|$)`;
3330
- } else {
3331
- regexStr = `(^|/)${regexStr}(/|$)`;
4043
+ function registerMcpCommand(program) {
4044
+ const mcpCommand = program.command("mcp").summary("Show setup instructions or start MCP server").description(`Start the Model Context Protocol (MCP) server using STDIO transport.
4045
+
4046
+ When run interactively (TTY), shows setup instructions.
4047
+ When run via stdio (non-TTY), starts the MCP server.
4048
+
4049
+ Available tools: package_summary, package_vulnerabilities,
4050
+ package_dependencies, package_quality, compare_packages,
4051
+ list_package_docs, fetch_package_doc, search_package_docs,
4052
+ search_project_docs`).action(async () => {
4053
+ const deps = await createContainer();
4054
+ if (process.stdout.isTTY && process.stdin.isTTY) {
4055
+ showMcpSetupInstructions(deps);
4056
+ return;
3332
4057
  }
3333
- }
3334
- const regex = new RegExp(regexStr);
3335
- return regex.test(path);
4058
+ await startMcpServer(deps);
4059
+ });
4060
+ mcpCommand.command("start").summary("Start MCP server (stdio mode)").description(`Start the MCP server using STDIO transport.
4061
+
4062
+ This command explicitly starts the server and is intended for use
4063
+ in MCP configuration files. Use 'pkgseer mcp' for interactive setup.`).action(async () => {
4064
+ const deps = await createContainer();
4065
+ await startMcpServer(deps);
4066
+ });
4067
+ registerMcpInitCommand(mcpCommand);
3336
4068
  }
3337
- async function parseGitIgnore(gitignorePath, fs) {
3338
- try {
3339
- const content = await fs.readFile(gitignorePath);
3340
- const lines = content.split(`
3341
- `);
3342
- const patterns = [];
3343
- for (const line of lines) {
3344
- const pattern = parseGitIgnoreLine(line);
3345
- if (pattern) {
3346
- patterns.push(pattern);
3347
- }
4069
+
4070
+ // src/commands/pkg/compare.ts
4071
+ function parsePackageSpec(spec) {
4072
+ let registry = "npm";
4073
+ let rest = spec;
4074
+ if (spec.includes(":")) {
4075
+ const colonIndex = spec.indexOf(":");
4076
+ const potentialRegistry = spec.slice(0, colonIndex).toLowerCase();
4077
+ if (["npm", "pypi", "hex"].includes(potentialRegistry)) {
4078
+ registry = potentialRegistry;
4079
+ rest = spec.slice(colonIndex + 1);
3348
4080
  }
3349
- return patterns.length > 0 ? patterns : null;
3350
- } catch {
3351
- return null;
3352
4081
  }
3353
- }
3354
- function shouldIgnorePath(relativePath, patterns) {
3355
- const normalized = relativePath.replace(/\\/g, "/");
3356
- let ignored = false;
3357
- for (const pattern of patterns) {
3358
- if (matchesPattern(normalized, pattern)) {
3359
- if (pattern.isNegation) {
3360
- ignored = false;
3361
- } else {
3362
- ignored = true;
3363
- }
3364
- }
4082
+ const atIndex = rest.lastIndexOf("@");
4083
+ if (atIndex > 0) {
4084
+ return {
4085
+ registry,
4086
+ name: rest.slice(0, atIndex),
4087
+ version: rest.slice(atIndex + 1)
4088
+ };
3365
4089
  }
3366
- return ignored;
3367
- }
3368
- function shouldIgnoreDirectory(relativeDirPath, patterns) {
3369
- const normalized = relativeDirPath.replace(/\\/g, "/").replace(/\/$/, "") + "/";
3370
- return shouldIgnorePath(normalized, patterns);
4090
+ return { registry, name: rest };
3371
4091
  }
3372
-
3373
- // src/services/manifest-detector.ts
3374
- var DEFAULT_EXCLUDED_DIRS = [
3375
- "node_modules",
3376
- "vendor",
3377
- ".git",
3378
- "dist",
3379
- "build",
3380
- "__pycache__",
3381
- ".venv",
3382
- "venv",
3383
- ".next"
3384
- ];
3385
- var MANIFEST_TYPES = [
3386
- {
3387
- type: "npm",
3388
- filenames: [
3389
- "package.json",
3390
- "package-lock.json",
3391
- "yarn.lock",
3392
- "pnpm-lock.yaml"
3393
- ]
3394
- },
3395
- {
3396
- type: "pypi",
3397
- filenames: ["requirements.txt", "pyproject.toml", "Pipfile", "poetry.lock"]
3398
- },
3399
- {
3400
- type: "hex",
3401
- filenames: ["mix.exs", "mix.lock"]
3402
- }
3403
- ];
3404
- function suggestLabel(relativePath) {
3405
- const normalized = relativePath.replace(/\\/g, "/");
3406
- const parts = normalized.split("/").filter((p) => p.length > 0);
3407
- if (parts.length === 1) {
3408
- return "root";
4092
+ function formatPackageComparison(comparison) {
4093
+ const lines = [];
4094
+ lines.push("\uD83D\uDCCA Package Comparison");
4095
+ lines.push("");
4096
+ if (!comparison.packages || comparison.packages.length === 0) {
4097
+ lines.push("No packages to compare.");
4098
+ return lines.join(`
4099
+ `);
3409
4100
  }
3410
- return parts[parts.length - 2];
4101
+ const packages = comparison.packages.filter((p) => p != null);
4102
+ const maxNameLen = Math.max(...packages.map((p) => `${p.packageName}@${p.version}`.length));
4103
+ const header = "Package".padEnd(maxNameLen + 2);
4104
+ lines.push(` ${header} Quality Downloads/mo Vulns`);
4105
+ lines.push(` ${"─".repeat(maxNameLen + 2)} ${"─".repeat(10)} ${"─".repeat(12)} ${"─".repeat(5)}`);
4106
+ for (const pkg of packages) {
4107
+ const name = `${pkg.packageName}@${pkg.version}`.padEnd(maxNameLen + 2);
4108
+ const scoreValue = pkg.quality?.score;
4109
+ const quality = scoreValue != null ? `${Math.round(scoreValue > 1 ? scoreValue : scoreValue * 100)}%`.padEnd(10) : "N/A".padEnd(10);
4110
+ const downloads = pkg.downloadsLastMonth ? formatNumber(pkg.downloadsLastMonth).padEnd(12) : "N/A".padEnd(12);
4111
+ const vulns = pkg.vulnerabilityCount != null ? pkg.vulnerabilityCount === 0 ? "✅ 0" : `⚠️ ${pkg.vulnerabilityCount}` : "N/A";
4112
+ lines.push(` ${name} ${quality} ${downloads} ${vulns}`);
4113
+ }
4114
+ return lines.join(`
4115
+ `);
3411
4116
  }
3412
- async function scanDirectoryRecursive(directory, fs, rootDir, options, currentDepth = 0, gitignorePatterns = null) {
3413
- const detected = [];
3414
- if (currentDepth > options.maxDepth) {
3415
- return detected;
4117
+ async function pkgCompareAction(packages, options, deps) {
4118
+ const { pkgseerService } = deps;
4119
+ if (packages.length < 2) {
4120
+ outputError("At least 2 packages required for comparison", options.json ?? false);
4121
+ return;
3416
4122
  }
3417
- const relativeDirPath = directory === rootDir ? "." : directory.replace(rootDir, "").replace(/^[/\\]/, "");
3418
- const baseName = directory.split(/[/\\]/).pop() ?? "";
3419
- if (options.excludedDirs.includes(baseName)) {
3420
- return detected;
4123
+ if (packages.length > 10) {
4124
+ outputError("Maximum 10 packages can be compared at once", options.json ?? false);
4125
+ return;
3421
4126
  }
3422
- if (gitignorePatterns && shouldIgnoreDirectory(relativeDirPath, gitignorePatterns)) {
3423
- return detected;
4127
+ const input2 = packages.map((spec) => {
4128
+ const parsed = parsePackageSpec(spec);
4129
+ return {
4130
+ registry: toGraphQLRegistry(parsed.registry),
4131
+ name: parsed.name,
4132
+ version: parsed.version
4133
+ };
4134
+ });
4135
+ const result = await pkgseerService.cliComparePackages(input2);
4136
+ handleErrors(result.errors, options.json ?? false);
4137
+ if (!result.data.comparePackages) {
4138
+ outputError("Comparison failed", options.json ?? false);
4139
+ return;
3424
4140
  }
3425
- for (const manifestType of MANIFEST_TYPES) {
3426
- for (const filename of manifestType.filenames) {
3427
- const filePath = fs.joinPath(directory, filename);
3428
- const exists = await fs.exists(filePath);
3429
- if (exists) {
3430
- const relativePath = directory === rootDir ? filename : fs.joinPath(directory.replace(rootDir, "").replace(/^[/\\]/, ""), filename);
3431
- if (gitignorePatterns && shouldIgnorePath(relativePath, gitignorePatterns)) {
3432
- continue;
3433
- }
3434
- detected.push({
3435
- filename,
3436
- relativePath,
3437
- absolutePath: filePath,
3438
- type: manifestType.type,
3439
- suggestedLabel: suggestLabel(relativePath)
3440
- });
3441
- }
3442
- }
4141
+ if (options.json) {
4142
+ const pkgs = result.data.comparePackages.packages?.filter((p) => p) ?? [];
4143
+ const slim = pkgs.map((p) => ({
4144
+ package: `${p.packageName}@${p.version}`,
4145
+ quality: p.quality?.score,
4146
+ downloads: p.downloadsLastMonth,
4147
+ vulnerabilities: p.vulnerabilityCount
4148
+ }));
4149
+ output(slim, true);
4150
+ } else {
4151
+ console.log(formatPackageComparison(result.data.comparePackages));
3443
4152
  }
3444
- try {
3445
- const entries = await fs.readdir(directory);
3446
- for (const entry of entries) {
3447
- const entryPath = fs.joinPath(directory, entry);
3448
- const isDir = await fs.isDirectory(entryPath);
3449
- if (isDir && !options.excludedDirs.includes(entry)) {
3450
- const subRelativePath = fs.joinPath(relativeDirPath, entry);
3451
- if (!gitignorePatterns || !shouldIgnoreDirectory(subRelativePath, gitignorePatterns)) {
3452
- const subDetected = await scanDirectoryRecursive(entryPath, fs, rootDir, options, currentDepth + 1, gitignorePatterns);
3453
- detected.push(...subDetected);
3454
- }
3455
- }
3456
- }
3457
- } catch {}
3458
- return detected;
3459
4153
  }
3460
- async function scanForManifests(directory, fs, options) {
3461
- const opts = {
3462
- maxDepth: options?.maxDepth ?? 3,
3463
- excludedDirs: options?.excludedDirs ?? DEFAULT_EXCLUDED_DIRS
3464
- };
3465
- const gitignorePath = fs.joinPath(directory, ".gitignore");
3466
- const gitignorePatterns = await parseGitIgnore(gitignorePath, fs);
3467
- return scanDirectoryRecursive(directory, fs, directory, opts, 0, gitignorePatterns);
4154
+ var COMPARE_DESCRIPTION = `Compare multiple packages.
4155
+
4156
+ Compares packages across quality, security, and popularity metrics.
4157
+ Supports cross-registry comparison.
4158
+
4159
+ Package format: [registry:]name[@version]
4160
+ - lodash (npm, latest)
4161
+ - pypi:requests (pypi, latest)
4162
+ - npm:express@4.18.0 (npm, specific version)
4163
+
4164
+ Examples:
4165
+ pkgseer pkg compare lodash underscore ramda
4166
+ pkgseer pkg compare npm:axios pypi:requests
4167
+ pkgseer pkg compare express@4.18.0 express@5.0.0 --json`;
4168
+ function registerPkgCompareCommand(program) {
4169
+ program.command("compare <packages...>").summary("Compare multiple packages").description(COMPARE_DESCRIPTION).option("--json", "Output as JSON").action(async (packages, options) => {
4170
+ await withCliErrorHandling(options.json ?? false, async () => {
4171
+ const deps = await createContainer();
4172
+ await pkgCompareAction(packages, options, deps);
4173
+ });
4174
+ });
3468
4175
  }
3469
- function filterRedundantPackageJson(manifests) {
3470
- const dirToManifests = new Map;
3471
- for (const manifest of manifests) {
3472
- const dir = manifest.relativePath.split(/[/\\]/).slice(0, -1).join("/") || ".";
3473
- if (!dirToManifests.has(dir)) {
3474
- dirToManifests.set(dir, []);
4176
+ // src/commands/pkg/deps.ts
4177
+ function formatPackageDependencies(data) {
4178
+ const lines = [];
4179
+ const pkg = data.package;
4180
+ const deps = data.dependencies;
4181
+ if (!pkg) {
4182
+ return "No package data available.";
4183
+ }
4184
+ lines.push(`\uD83D\uDCE6 ${pkg.name}@${pkg.version}`);
4185
+ lines.push("");
4186
+ if (deps?.summary) {
4187
+ lines.push("Summary:");
4188
+ lines.push(` Direct dependencies: ${deps.summary.directCount ?? 0}`);
4189
+ if (deps.summary.uniquePackagesCount) {
4190
+ lines.push(` Unique packages: ${deps.summary.uniquePackagesCount}`);
3475
4191
  }
3476
- dirToManifests.get(dir).push(manifest);
4192
+ lines.push("");
3477
4193
  }
3478
- const filtered = [];
3479
- for (const [dir, dirManifests] of dirToManifests.entries()) {
3480
- const hasLockFile = dirManifests.some((m) => m.filename === "package-lock.json");
3481
- const hasPackageJson = dirManifests.some((m) => m.filename === "package.json");
3482
- for (const manifest of dirManifests) {
3483
- if (manifest.filename === "package.json" && hasLockFile) {
3484
- continue;
4194
+ if (deps?.direct && deps.direct.length > 0) {
4195
+ lines.push(`Dependencies (${deps.direct.length}):`);
4196
+ for (const dep of deps.direct) {
4197
+ if (dep) {
4198
+ const type = dep.type !== "RUNTIME" ? ` [${dep.type?.toLowerCase()}]` : "";
4199
+ lines.push(` ${dep.name} ${dep.versionConstraint}${type}`);
3485
4200
  }
3486
- filtered.push(manifest);
3487
4201
  }
4202
+ } else {
4203
+ lines.push("No direct dependencies.");
3488
4204
  }
3489
- return filtered;
4205
+ return lines.join(`
4206
+ `);
3490
4207
  }
3491
- async function detectAndGroupManifests(directory, fs, options) {
3492
- const manifests = await scanForManifests(directory, fs, options);
3493
- const filteredManifests = filterRedundantPackageJson(manifests);
3494
- const groupsMap = new Map;
3495
- for (const manifest of filteredManifests) {
3496
- const existing = groupsMap.get(manifest.suggestedLabel) ?? [];
3497
- existing.push(manifest);
3498
- groupsMap.set(manifest.suggestedLabel, existing);
4208
+ async function pkgDepsAction(packageName, options, deps) {
4209
+ const { pkgseerService } = deps;
4210
+ const registry = toGraphQLRegistry(options.registry);
4211
+ const result = await pkgseerService.cliPackageDeps(registry, packageName, options.pkgVersion, options.transitive);
4212
+ handleErrors(result.errors, options.json ?? false);
4213
+ if (!result.data.packageDependencies) {
4214
+ outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
4215
+ return;
3499
4216
  }
3500
- const groups = [];
3501
- for (const [label, manifests2] of groupsMap.entries()) {
3502
- const ecosystems = new Set(manifests2.map((m) => m.type));
3503
- if (ecosystems.size > 1) {
3504
- const ecosystemGroups = new Map;
3505
- for (const manifest of manifests2) {
3506
- const ecosystemLabel = `${label}-${manifest.type}`;
3507
- const existing = ecosystemGroups.get(ecosystemLabel) ?? [];
3508
- existing.push(manifest);
3509
- ecosystemGroups.set(ecosystemLabel, existing);
3510
- }
3511
- for (const [
3512
- ecosystemLabel,
3513
- ecosystemManifests
3514
- ] of ecosystemGroups.entries()) {
3515
- groups.push({ label: ecosystemLabel, manifests: ecosystemManifests });
3516
- }
3517
- } else {
3518
- groups.push({ label, manifests: manifests2 });
3519
- }
4217
+ if (options.json) {
4218
+ const data = result.data.packageDependencies;
4219
+ const slim = {
4220
+ package: `${data.package?.name}@${data.package?.version}`,
4221
+ directCount: data.dependencies?.summary?.directCount ?? 0,
4222
+ dependencies: data.dependencies?.direct?.filter((d) => d).map((d) => ({
4223
+ name: d.name,
4224
+ version: d.versionConstraint,
4225
+ type: d.type
4226
+ }))
4227
+ };
4228
+ output(slim, true);
4229
+ } else {
4230
+ console.log(formatPackageDependencies(result.data.packageDependencies));
4231
+ }
4232
+ }
4233
+ var DEPS_DESCRIPTION = `Get package dependencies.
4234
+
4235
+ Lists direct dependencies and shows version constraints and
4236
+ dependency types (runtime, dev, optional).
4237
+
4238
+ Examples:
4239
+ pkgseer pkg deps express
4240
+ pkgseer pkg deps lodash --transitive
4241
+ pkgseer pkg deps requests --registry pypi --json`;
4242
+ function registerPkgDepsCommand(program) {
4243
+ program.command("deps <package>").summary("Get package dependencies").description(DEPS_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-v, --pkg-version <version>", "Package version").option("-t, --transitive", "Include transitive dependencies").option("--json", "Output as JSON").action(async (packageName, options) => {
4244
+ await withCliErrorHandling(options.json ?? false, async () => {
4245
+ const deps = await createContainer();
4246
+ await pkgDepsAction(packageName, options, deps);
4247
+ });
4248
+ });
4249
+ }
4250
+ // src/commands/pkg/info.ts
4251
+ function formatPackageSummary(data) {
4252
+ const lines = [];
4253
+ const pkg = data.package;
4254
+ if (!pkg) {
4255
+ return "No package data available.";
4256
+ }
4257
+ lines.push(`\uD83D\uDCE6 ${pkg.name}@${pkg.latestVersion}`);
4258
+ lines.push("");
4259
+ if (pkg.description) {
4260
+ lines.push(pkg.description);
4261
+ lines.push("");
4262
+ }
4263
+ lines.push("Package Info:");
4264
+ lines.push(keyValueTable([
4265
+ ["Registry", pkg.registry],
4266
+ ["Version", pkg.latestVersion],
4267
+ ["License", pkg.license]
4268
+ ]));
4269
+ lines.push("");
4270
+ if (pkg.homepage || pkg.repositoryUrl) {
4271
+ lines.push("Links:");
4272
+ const links = [];
4273
+ if (pkg.homepage)
4274
+ links.push(["Homepage", pkg.homepage]);
4275
+ if (pkg.repositoryUrl)
4276
+ links.push(["Repository", pkg.repositoryUrl]);
4277
+ lines.push(keyValueTable(links));
4278
+ lines.push("");
3520
4279
  }
3521
- groups.sort((a, b) => {
3522
- if (a.label.startsWith("root")) {
3523
- if (b.label.startsWith("root")) {
3524
- return a.label.localeCompare(b.label);
3525
- }
3526
- return -1;
3527
- }
3528
- if (b.label.startsWith("root")) {
3529
- return 1;
3530
- }
3531
- return a.label.localeCompare(b.label);
3532
- });
3533
- return groups;
4280
+ const vulnCount = data.security?.vulnerabilityCount ?? 0;
4281
+ if (vulnCount > 0) {
4282
+ lines.push(`⚠️ Security: ${vulnCount} vulnerabilities`);
4283
+ lines.push("");
4284
+ }
4285
+ if (data.quickstart?.installCommand) {
4286
+ lines.push("Quick Start:");
4287
+ lines.push(` ${data.quickstart.installCommand}`);
4288
+ }
4289
+ return lines.join(`
4290
+ `);
3534
4291
  }
3535
-
3536
- // src/commands/project/detect.ts
3537
- function matchManifestsWithConfig(detectedGroups, existingManifests) {
3538
- const fileToConfig = new Map;
3539
- for (const group of existingManifests) {
3540
- for (const file of group.files) {
3541
- fileToConfig.set(file, {
3542
- label: group.label,
3543
- allow_mix_deps: group.allow_mix_deps
3544
- });
3545
- }
4292
+ async function pkgInfoAction(packageName, options, deps) {
4293
+ const { pkgseerService } = deps;
4294
+ const registry = toGraphQLRegistry(options.registry);
4295
+ const result = await pkgseerService.cliPackageInfo(registry, packageName);
4296
+ handleErrors(result.errors, options.json ?? false);
4297
+ if (!result.data.packageSummary) {
4298
+ outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
4299
+ return;
3546
4300
  }
3547
- const labelToFiles = new Map;
3548
- const labelToConfig = new Map;
3549
- for (const detectedGroup of detectedGroups) {
3550
- for (const manifest of detectedGroup.manifests) {
3551
- const existingConfig = fileToConfig.get(manifest.relativePath);
3552
- let label;
3553
- const hasEcosystemSuffix = detectedGroup.label.endsWith("-npm") || detectedGroup.label.endsWith("-hex") || detectedGroup.label.endsWith("-pypi");
3554
- if (hasEcosystemSuffix) {
3555
- label = detectedGroup.label;
3556
- } else {
3557
- label = existingConfig?.label ?? detectedGroup.label;
3558
- }
3559
- const allowMixDeps = existingConfig?.allow_mix_deps;
3560
- if (!labelToConfig.has(label)) {
3561
- labelToConfig.set(label, {
3562
- files: new Set,
3563
- allow_mix_deps: allowMixDeps
3564
- });
3565
- }
3566
- const config = labelToConfig.get(label);
3567
- config.files.add(manifest.relativePath);
3568
- if (allowMixDeps) {
3569
- config.allow_mix_deps = true;
3570
- }
3571
- }
4301
+ if (options.json) {
4302
+ const data = result.data.packageSummary;
4303
+ const pkg = data.package;
4304
+ const slim = {
4305
+ name: pkg?.name,
4306
+ version: pkg?.latestVersion,
4307
+ description: pkg?.description,
4308
+ license: pkg?.license,
4309
+ install: data.quickstart?.installCommand,
4310
+ vulnerabilities: data.security?.vulnerabilityCount ?? 0
4311
+ };
4312
+ output(slim, true);
4313
+ } else {
4314
+ console.log(formatPackageSummary(result.data.packageSummary));
3572
4315
  }
3573
- const result = [];
3574
- for (const [label, config] of labelToConfig.entries()) {
3575
- const fileArray = Array.from(config.files);
3576
- const hasNewFiles = fileArray.some((file) => !fileToConfig.has(file));
3577
- const hasChangedLabels = fileArray.some((file) => {
3578
- const oldConfig = fileToConfig.get(file);
3579
- return oldConfig !== undefined && oldConfig.label !== label;
3580
- });
3581
- result.push({
3582
- label,
3583
- files: fileArray.sort(),
3584
- isNew: hasNewFiles,
3585
- changedLabel: hasChangedLabels ? label : undefined,
3586
- allow_mix_deps: config.allow_mix_deps
4316
+ }
4317
+ var INFO_DESCRIPTION = `Get package summary and metadata.
4318
+
4319
+ Displays comprehensive information about a package including:
4320
+ - Basic metadata (version, license, description)
4321
+ - Download statistics
4322
+ - Security advisories
4323
+ - Quick start instructions
4324
+
4325
+ Examples:
4326
+ pkgseer pkg info lodash
4327
+ pkgseer pkg info requests --registry pypi
4328
+ pkgseer pkg info phoenix --registry hex --json`;
4329
+ function registerPkgInfoCommand(program) {
4330
+ program.command("info <package>").summary("Get package summary and metadata").description(INFO_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("--json", "Output as JSON").action(async (packageName, options) => {
4331
+ await withCliErrorHandling(options.json ?? false, async () => {
4332
+ const deps = await createContainer();
4333
+ await pkgInfoAction(packageName, options, deps);
3587
4334
  });
4335
+ });
4336
+ }
4337
+ // src/commands/pkg/quality.ts
4338
+ function formatPackageQuality(data) {
4339
+ const lines = [];
4340
+ const quality = data.quality;
4341
+ if (!quality) {
4342
+ return "No quality data available.";
3588
4343
  }
3589
- return result.sort((a, b) => {
3590
- if (a.label.startsWith("root")) {
3591
- if (b.label.startsWith("root")) {
3592
- return a.label.localeCompare(b.label);
4344
+ lines.push(`\uD83D\uDCCA Quality Score: ${formatScore(quality.overallScore)} (Grade: ${quality.grade})`);
4345
+ lines.push("");
4346
+ if (quality.categories && quality.categories.length > 0) {
4347
+ lines.push("Category Breakdown:");
4348
+ for (const category of quality.categories) {
4349
+ if (category) {
4350
+ const name = (category.category || "Unknown").padEnd(20);
4351
+ lines.push(` ${name} ${formatScore(category.score)}`);
3593
4352
  }
3594
- return -1;
3595
4353
  }
3596
- if (b.label.startsWith("root")) {
3597
- return 1;
3598
- }
3599
- return a.label.localeCompare(b.label);
3600
- });
3601
- }
3602
- async function projectDetectAction(options, deps) {
3603
- const { configService, fileSystemService, promptService, shellService } = deps;
3604
- const projectConfig = await configService.loadProjectConfig();
3605
- if (!projectConfig?.config.project) {
3606
- console.error(`✗ No project is configured in pkgseer.yml`);
3607
- console.log(`
3608
- To get started, run: pkgseer project init`);
3609
- console.log(` This will create a project and detect your manifest files.`);
3610
- process.exit(1);
4354
+ lines.push("");
3611
4355
  }
3612
- const projectName = projectConfig.config.project;
3613
- const existingManifests = projectConfig.config.manifests ?? [];
3614
- console.log(`Scanning for manifest files in project "${projectName}"...
4356
+ return lines.join(`
3615
4357
  `);
3616
- const cwd = fileSystemService.getCwd();
3617
- const detectedGroups = await detectAndGroupManifests(cwd, fileSystemService, {
3618
- maxDepth: options.maxDepth ?? 3
3619
- });
3620
- if (detectedGroups.length === 0) {
3621
- console.log("No manifest files were found in the current directory.");
3622
- if (existingManifests.length > 0) {
3623
- console.log(`
3624
- Your existing configuration in pkgseer.yml will remain unchanged.`);
3625
- console.log(` Tip: Make sure you're running this command from the project root directory.`);
3626
- } else {
3627
- console.log(`
3628
- Tip: Manifest files like package.json, requirements.txt, or pyproject.toml should be in the current directory or subdirectories.`);
3629
- }
4358
+ }
4359
+ async function pkgQualityAction(packageName, options, deps) {
4360
+ const { pkgseerService } = deps;
4361
+ const registry = toGraphQLRegistry(options.registry);
4362
+ const result = await pkgseerService.cliPackageQuality(registry, packageName, options.pkgVersion);
4363
+ handleErrors(result.errors, options.json ?? false);
4364
+ if (!result.data.packageQuality) {
4365
+ outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
3630
4366
  return;
3631
4367
  }
3632
- const suggestedManifests = matchManifestsWithConfig(detectedGroups, existingManifests);
3633
- console.log("Current configuration in pkgseer.yml:");
3634
- if (existingManifests.length === 0) {
3635
- console.log(" (no manifests configured yet)");
4368
+ if (options.json) {
4369
+ const quality = result.data.packageQuality.quality;
4370
+ const slim = {
4371
+ package: `${result.data.packageQuality.package?.name}@${result.data.packageQuality.package?.version}`,
4372
+ score: quality?.overallScore,
4373
+ grade: quality?.grade,
4374
+ categories: quality?.categories?.filter((c) => c).map((c) => ({
4375
+ name: c.category,
4376
+ score: c.score
4377
+ }))
4378
+ };
4379
+ output(slim, true);
3636
4380
  } else {
3637
- for (const group of existingManifests) {
3638
- console.log(` Label: ${group.label}`);
3639
- for (const file of group.files) {
3640
- console.log(` ${file}`);
3641
- }
3642
- }
4381
+ console.log(formatPackageQuality(result.data.packageQuality));
3643
4382
  }
3644
- console.log(`
3645
- Suggested configuration:`);
3646
- for (const group of suggestedManifests) {
3647
- const markers = [];
3648
- if (group.isNew)
3649
- markers.push("new");
3650
- if (group.changedLabel)
3651
- markers.push("label changed");
3652
- const markerStr = markers.length > 0 ? ` (${markers.join(", ")})` : "";
3653
- console.log(` Label: ${group.label}${markerStr}`);
3654
- for (const file of group.files) {
3655
- const wasInConfig = existingManifests.some((g) => g.files.includes(file));
3656
- const prefix = wasInConfig ? " " : " + ";
3657
- console.log(`${prefix}${file}`);
4383
+ }
4384
+ var QUALITY_DESCRIPTION = `Get package quality score and breakdown.
4385
+
4386
+ Analyzes package quality across multiple dimensions:
4387
+ - Maintenance and activity
4388
+ - Documentation coverage
4389
+ - Security practices
4390
+ - Community engagement
4391
+
4392
+ Examples:
4393
+ pkgseer pkg quality lodash
4394
+ pkgseer pkg quality express -v 4.18.0
4395
+ pkgseer pkg quality requests --registry pypi --json`;
4396
+ function registerPkgQualityCommand(program) {
4397
+ program.command("quality <package>").summary("Get package quality score").description(QUALITY_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-v, --pkg-version <version>", "Package version").option("--json", "Output as JSON").action(async (packageName, options) => {
4398
+ await withCliErrorHandling(options.json ?? false, async () => {
4399
+ const deps = await createContainer();
4400
+ await pkgQualityAction(packageName, options, deps);
4401
+ });
4402
+ });
4403
+ }
4404
+ // src/commands/pkg/vulns.ts
4405
+ function getSeverityLabel(score) {
4406
+ if (score == null)
4407
+ return "UNKNOWN";
4408
+ if (score >= 9)
4409
+ return "CRITICAL";
4410
+ if (score >= 7)
4411
+ return "HIGH";
4412
+ if (score >= 4)
4413
+ return "MEDIUM";
4414
+ return "LOW";
4415
+ }
4416
+ function formatPackageVulnerabilities(data) {
4417
+ const lines = [];
4418
+ const pkg = data.package;
4419
+ const security = data.security;
4420
+ if (!pkg) {
4421
+ return "No package data available.";
4422
+ }
4423
+ lines.push(`\uD83D\uDD12 Security Report: ${pkg.name}@${pkg.version}`);
4424
+ lines.push("");
4425
+ if (!security?.vulnerabilities || security.vulnerabilities.length === 0) {
4426
+ lines.push("✅ No known vulnerabilities!");
4427
+ return lines.join(`
4428
+ `);
4429
+ }
4430
+ const bySeverity = security.vulnerabilities.reduce((acc, v) => {
4431
+ if (v) {
4432
+ const label = getSeverityLabel(v.severityScore);
4433
+ acc[label] = (acc[label] || 0) + 1;
3658
4434
  }
4435
+ return acc;
4436
+ }, {});
4437
+ lines.push(`⚠️ Found ${security.vulnerabilityCount} vulnerabilities:`);
4438
+ for (const [severity, count] of Object.entries(bySeverity)) {
4439
+ lines.push(` ${formatSeverity(severity)}: ${count}`);
3659
4440
  }
3660
- const hasHexManifests = detectedGroups.some((group) => group.manifests.some((m) => m.type === "hex"));
3661
- const hasHexInSuggested = suggestedManifests.some((g) => g.files.some((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock")));
3662
- let allowMixDeps = false;
3663
- if (hasHexInSuggested) {
3664
- const existingHasMixDeps = existingManifests.some((g) => g.allow_mix_deps === true);
3665
- if (!existingHasMixDeps) {
3666
- console.log(`
3667
- Note: Elixir/Hex manifest files (mix.exs, mix.lock) are Elixir code and cannot be directly uploaded.`);
3668
- console.log(` Instead, we need to run "mix deps --all" and "mix deps.tree" to generate dependency information.`);
3669
- allowMixDeps = await promptService.confirm(`
3670
- Allow running "mix deps --all" and "mix deps.tree" for hex manifests?`, true);
3671
- } else {
3672
- allowMixDeps = true;
4441
+ lines.push("");
4442
+ lines.push("Details:");
4443
+ for (const vuln of security.vulnerabilities) {
4444
+ if (!vuln)
4445
+ continue;
4446
+ lines.push("");
4447
+ lines.push(` ${formatSeverity(getSeverityLabel(vuln.severityScore))}`);
4448
+ lines.push(` ${vuln.summary || vuln.osvId}`);
4449
+ lines.push(` ID: ${vuln.osvId}`);
4450
+ if (vuln.fixedInVersions && vuln.fixedInVersions.length > 0) {
4451
+ lines.push(` Fixed in: ${vuln.fixedInVersions.join(", ")}`);
3673
4452
  }
3674
4453
  }
3675
- const finalManifests = suggestedManifests.map((g) => {
3676
- const hasHexInGroup = g.files.some((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock"));
3677
- return {
3678
- label: g.label,
3679
- files: g.files,
3680
- isNew: g.isNew,
3681
- changedLabel: g.changedLabel,
3682
- ...hasHexInGroup && allowMixDeps ? { allow_mix_deps: true } : {},
3683
- ...g.allow_mix_deps !== undefined ? { allow_mix_deps: g.allow_mix_deps } : {}
3684
- };
3685
- });
3686
- const hasChanges = finalManifests.length !== existingManifests.length || finalManifests.some((g) => g.isNew || g.changedLabel) || existingManifests.some((existing) => {
3687
- const suggested = finalManifests.find((s) => s.label === existing.label);
3688
- if (!suggested)
3689
- return true;
3690
- const existingFiles = new Set(existing.files);
3691
- const suggestedFiles = new Set(suggested.files);
3692
- return existingFiles.size !== suggestedFiles.size || !Array.from(existingFiles).every((f) => suggestedFiles.has(f));
3693
- }) || finalManifests.some((g) => {
3694
- const existing = existingManifests.find((e) => e.label === g.label);
3695
- return existing?.allow_mix_deps !== g.allow_mix_deps;
3696
- });
3697
- if (!hasChanges) {
3698
- console.log(`
3699
- ✓ Your configuration is already up to date!`);
3700
- console.log(`
3701
- All detected manifest files match your current pkgseer.yml configuration.`);
4454
+ return lines.join(`
4455
+ `);
4456
+ }
4457
+ async function pkgVulnsAction(packageName, options, deps) {
4458
+ const { pkgseerService } = deps;
4459
+ const registry = toGraphQLRegistry(options.registry);
4460
+ const result = await pkgseerService.cliPackageVulns(registry, packageName, options.pkgVersion);
4461
+ handleErrors(result.errors, options.json ?? false);
4462
+ if (!result.data.packageVulnerabilities) {
4463
+ outputError(`Package not found: ${packageName} in ${options.registry}`, options.json ?? false);
3702
4464
  return;
3703
4465
  }
3704
- if (options.update) {
3705
- await configService.writeProjectConfig({
3706
- project: projectName,
3707
- manifests: finalManifests.map((g) => ({
3708
- label: g.label,
3709
- files: g.files,
3710
- ...g.allow_mix_deps ? { allow_mix_deps: g.allow_mix_deps } : {}
4466
+ if (options.json) {
4467
+ const data = result.data.packageVulnerabilities;
4468
+ const vulns = data.security?.vulnerabilities ?? [];
4469
+ const slim = {
4470
+ package: `${data.package?.name}@${data.package?.version}`,
4471
+ count: data.security?.vulnerabilityCount ?? 0,
4472
+ vulnerabilities: vulns.filter((v) => v).map((v) => ({
4473
+ id: v.osvId,
4474
+ severity: v.severityScore,
4475
+ summary: v.summary,
4476
+ fixed: v.fixedInVersions
3711
4477
  }))
3712
- });
3713
- console.log(`
3714
- ✓ Configuration updated successfully!`);
3715
- console.log(`
3716
- Your pkgseer.yml has been updated with the detected manifest files.`);
3717
- console.log(` Run 'pkgseer project upload' to upload them to your project.`);
4478
+ };
4479
+ output(slim, true);
3718
4480
  } else {
3719
- const shouldUpdate = await promptService.confirm(`
3720
- Would you like to update pkgseer.yml with these changes?`, false);
3721
- if (shouldUpdate) {
3722
- await configService.writeProjectConfig({
3723
- project: projectName,
3724
- manifests: finalManifests.map((g) => ({
3725
- label: g.label,
3726
- files: g.files,
3727
- ...g.allow_mix_deps ? { allow_mix_deps: g.allow_mix_deps } : {}
3728
- }))
3729
- });
3730
- console.log(`
3731
- ✓ Configuration updated successfully!`);
3732
- console.log(`
3733
- Your pkgseer.yml has been updated. Run 'pkgseer project upload' to upload the manifests.`);
3734
- } else {
3735
- console.log(`
3736
- Configuration was not updated.`);
3737
- console.log(` To apply these changes automatically, run: pkgseer project detect --update`);
3738
- console.log(` Or manually edit pkgseer.yml and then run: pkgseer project upload`);
3739
- }
4481
+ console.log(formatPackageVulnerabilities(result.data.packageVulnerabilities));
3740
4482
  }
3741
4483
  }
3742
- var DETECT_DESCRIPTION = `Detect manifest files and update your project configuration.
3743
-
3744
- This command scans your project directory for manifest files (like
3745
- package.json, requirements.txt, etc.) and compares them with your
3746
- current pkgseer.yml configuration. It will:
4484
+ var VULNS_DESCRIPTION = `Check package for security vulnerabilities.
3747
4485
 
3748
- Preserve existing labels for files you've already configured
3749
- Suggest new labels for newly detected files
3750
- Show you what would change before updating
4486
+ Scans for known security vulnerabilities and provides:
4487
+ - Severity ratings (critical, high, medium, low)
4488
+ - CVE identifiers
4489
+ - Affected version ranges
4490
+ - Upgrade recommendations
3751
4491
 
3752
- Perfect for when you add new manifest files or reorganize your
3753
- project structure. Run with --update to automatically apply changes.`;
3754
- function registerProjectDetectCommand(program) {
3755
- program.command("detect").summary("Detect manifest files and suggest config updates").description(DETECT_DESCRIPTION).option("--max-depth <depth>", "Maximum directory depth to scan for manifests", (val) => Number.parseInt(val, 10), 3).option("--update", "Automatically update pkgseer.yml without prompting", false).action(async (options) => {
3756
- const deps = await createContainer();
3757
- await projectDetectAction({ maxDepth: options.maxDepth, update: options.update }, {
3758
- configService: deps.configService,
3759
- fileSystemService: deps.fileSystemService,
3760
- promptService: deps.promptService,
3761
- shellService: deps.shellService
4492
+ Examples:
4493
+ pkgseer pkg vulns lodash
4494
+ pkgseer pkg vulns express -v 4.17.0
4495
+ pkgseer pkg vulns requests --registry pypi --json`;
4496
+ function registerPkgVulnsCommand(program) {
4497
+ program.command("vulns <package>").summary("Check for security vulnerabilities").description(VULNS_DESCRIPTION).option("-r, --registry <registry>", "Package registry", "npm").option("-v, --pkg-version <version>", "Package version").option("--json", "Output as JSON").action(async (packageName, options) => {
4498
+ await withCliErrorHandling(options.json ?? false, async () => {
4499
+ const deps = await createContainer();
4500
+ await pkgVulnsAction(packageName, options, deps);
3762
4501
  });
3763
4502
  });
3764
4503
  }
3765
- // src/commands/shared-colors.ts
3766
- var colors2 = {
3767
- reset: "\x1B[0m",
3768
- bold: "\x1B[1m",
3769
- dim: "\x1B[2m",
3770
- green: "\x1B[32m",
3771
- yellow: "\x1B[33m",
3772
- blue: "\x1B[34m",
3773
- magenta: "\x1B[35m",
3774
- cyan: "\x1B[36m",
3775
- red: "\x1B[31m"
3776
- };
3777
- function shouldUseColors2(noColor) {
3778
- if (noColor)
3779
- return false;
3780
- if (process.env.NO_COLOR !== undefined)
3781
- return false;
3782
- return process.stdout.isTTY ?? false;
3783
- }
3784
- function success(text, useColors) {
3785
- const checkmark = useColors ? `${colors2.green}✓${colors2.reset}` : "✓";
3786
- return `${checkmark} ${text}`;
3787
- }
3788
- function error(text, useColors) {
3789
- const cross = useColors ? `${colors2.red}✗${colors2.reset}` : "✗";
3790
- return `${cross} ${text}`;
3791
- }
3792
- function highlight(text, useColors) {
3793
- if (!useColors)
3794
- return text;
3795
- return `${colors2.bold}${colors2.cyan}${text}${colors2.reset}`;
3796
- }
3797
- function dim(text, useColors) {
3798
- if (!useColors)
3799
- return text;
3800
- return `${colors2.dim}${text}${colors2.reset}`;
3801
- }
3802
-
3803
- // src/commands/project/manifest-upload-utils.ts
3804
- async function processManifestFiles(params) {
3805
- const {
3806
- files,
3807
- basePath,
3808
- hasHexManifests,
3809
- allowMixDeps,
3810
- fileSystemService,
3811
- shellService
3812
- } = params;
3813
- const hexFiles = files.filter((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock"));
3814
- let generatedHexFiles = [];
3815
- if (hasHexManifests && allowMixDeps && hexFiles.length > 0) {
3816
- const firstHexFile = hexFiles[0];
3817
- if (!firstHexFile) {
3818
- throw new Error("No hex files found");
4504
+ // src/commands/project/detect.ts
4505
+ function matchManifestsWithConfig(detectedGroups, existingManifests) {
4506
+ const fileToConfig = new Map;
4507
+ for (const group of existingManifests) {
4508
+ for (const file of group.files) {
4509
+ fileToConfig.set(file, {
4510
+ label: group.label,
4511
+ allow_mix_deps: group.allow_mix_deps
4512
+ });
3819
4513
  }
3820
- const absolutePath = fileSystemService.joinPath(basePath, firstHexFile);
3821
- const manifestDir = fileSystemService.getDirname(absolutePath);
3822
- try {
3823
- const [depsContent, depsTreeContent] = await Promise.all([
3824
- shellService.execute("mix deps --all", manifestDir),
3825
- shellService.execute("mix deps.tree", manifestDir)
3826
- ]);
3827
- generatedHexFiles = [
3828
- {
3829
- filename: "deps.txt",
3830
- path: absolutePath,
3831
- content: depsContent
3832
- },
3833
- {
3834
- filename: "deps-tree.txt",
3835
- path: absolutePath,
3836
- content: depsTreeContent
3837
- }
3838
- ];
3839
- } catch (error2) {
3840
- const errorMessage = error2 instanceof Error ? error2.message : String(error2);
3841
- throw new Error(`Failed to generate dependencies for hex manifest: ${firstHexFile}
3842
- ` + ` Error: ${errorMessage}
3843
- ` + ` Make sure 'mix' is installed and the directory contains a valid Elixir project.`);
4514
+ }
4515
+ const labelToFiles = new Map;
4516
+ const labelToConfig = new Map;
4517
+ for (const detectedGroup of detectedGroups) {
4518
+ for (const manifest of detectedGroup.manifests) {
4519
+ const existingConfig = fileToConfig.get(manifest.relativePath);
4520
+ let label;
4521
+ const hasEcosystemSuffix = detectedGroup.label.endsWith("-npm") || detectedGroup.label.endsWith("-hex") || detectedGroup.label.endsWith("-pypi");
4522
+ if (hasEcosystemSuffix) {
4523
+ label = detectedGroup.label;
4524
+ } else {
4525
+ label = existingConfig?.label ?? detectedGroup.label;
4526
+ }
4527
+ const allowMixDeps = existingConfig?.allow_mix_deps;
4528
+ if (!labelToConfig.has(label)) {
4529
+ labelToConfig.set(label, {
4530
+ files: new Set,
4531
+ allow_mix_deps: allowMixDeps
4532
+ });
4533
+ }
4534
+ const config = labelToConfig.get(label);
4535
+ config.files.add(manifest.relativePath);
4536
+ if (allowMixDeps) {
4537
+ config.allow_mix_deps = true;
4538
+ }
3844
4539
  }
3845
4540
  }
3846
- const filePromises = files.map(async (relativePath) => {
3847
- const isHexFile = relativePath.endsWith("mix.exs") || relativePath.endsWith("mix.lock");
3848
- if (isHexFile) {
3849
- if (!allowMixDeps) {
3850
- return null;
4541
+ const result = [];
4542
+ for (const [label, config] of labelToConfig.entries()) {
4543
+ const fileArray = Array.from(config.files);
4544
+ const hasNewFiles = fileArray.some((file) => !fileToConfig.has(file));
4545
+ const hasChangedLabels = fileArray.some((file) => {
4546
+ const oldConfig = fileToConfig.get(file);
4547
+ return oldConfig !== undefined && oldConfig.label !== label;
4548
+ });
4549
+ result.push({
4550
+ label,
4551
+ files: fileArray.sort(),
4552
+ isNew: hasNewFiles,
4553
+ changedLabel: hasChangedLabels ? label : undefined,
4554
+ allow_mix_deps: config.allow_mix_deps
4555
+ });
4556
+ }
4557
+ return result.sort((a, b) => {
4558
+ if (a.label.startsWith("root")) {
4559
+ if (b.label.startsWith("root")) {
4560
+ return a.label.localeCompare(b.label);
3851
4561
  }
3852
- return null;
4562
+ return -1;
3853
4563
  }
3854
- const absolutePath = fileSystemService.joinPath(basePath, relativePath);
3855
- const exists = await fileSystemService.exists(absolutePath);
3856
- if (!exists) {
3857
- throw new Error(`File not found: ${relativePath}
3858
- Make sure the file exists and the path in pkgseer.yml is correct.`);
4564
+ if (b.label.startsWith("root")) {
4565
+ return 1;
3859
4566
  }
3860
- const content = await fileSystemService.readFile(absolutePath);
3861
- const filename = relativePath.split(/[/\\]/).pop() ?? relativePath;
3862
- return {
3863
- filename,
3864
- path: absolutePath,
3865
- content
3866
- };
4567
+ return a.label.localeCompare(b.label);
3867
4568
  });
3868
- const regularFiles = await Promise.all(filePromises);
3869
- const result = [];
3870
- let hexFilesInserted = false;
3871
- for (let i = 0;i < files.length; i++) {
3872
- const relativePath = files[i];
3873
- if (!relativePath) {
3874
- continue;
3875
- }
3876
- const isHexFile = relativePath.endsWith("mix.exs") || relativePath.endsWith("mix.lock");
3877
- if (isHexFile && !hexFilesInserted && generatedHexFiles.length > 0) {
3878
- result.push(...generatedHexFiles);
3879
- hexFilesInserted = true;
3880
- } else if (!isHexFile) {
3881
- const regularFile = regularFiles[i];
3882
- if (regularFile !== undefined) {
3883
- result.push(regularFile);
3884
- }
3885
- } else {
3886
- result.push(null);
3887
- }
3888
- }
3889
- return result;
3890
4569
  }
3891
-
3892
- // src/commands/project/init.ts
3893
- async function projectInitAction(options, deps) {
3894
- const {
3895
- projectService,
3896
- configService,
3897
- fileSystemService,
3898
- gitService,
3899
- promptService,
3900
- shellService,
3901
- authStorage,
3902
- baseUrl
3903
- } = deps;
3904
- const useColors = shouldUseColors2();
3905
- const auth = await checkProjectWriteScope(authStorage, baseUrl);
3906
- if (!auth) {
3907
- console.error(error(`Authentication required with ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope`, useColors));
3908
- console.log(`
3909
- Your current token doesn't have the required permissions for creating projects and uploading manifests.`);
3910
- console.log(`
3911
- To fix this:`);
3912
- console.log(` 1. Run: ${highlight(`pkgseer login --force`, useColors)}`);
3913
- console.log(` 2. Make sure to grant ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope during authentication`);
3914
- console.log(`
3915
- Or check your current scopes with: ${highlight(`pkgseer auth status`, useColors)}`);
3916
- process.exit(1);
3917
- }
3918
- const isEnvToken = auth.scopes.length === 0 && auth.tokenName === "PKGSEER_API_TOKEN";
3919
- if (isEnvToken) {} else if (!auth.scopes.includes(PROJECT_MANIFEST_UPLOAD_SCOPE)) {
3920
- console.error(error(`Token missing required scope: ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)}`, useColors));
3921
- console.log(`
3922
- To fix this:`);
3923
- console.log(` 1. Run: ${highlight(`pkgseer login --force`, useColors)}`);
3924
- console.log(` 2. Make sure to grant ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope during authentication`);
4570
+ async function projectDetectAction(options, deps) {
4571
+ const { configService, fileSystemService, promptService, shellService } = deps;
4572
+ const projectConfig = await configService.loadProjectConfig();
4573
+ if (!projectConfig?.config.project) {
4574
+ console.error(`✗ No project is configured in pkgseer.yml`);
3925
4575
  console.log(`
3926
- Or check your current scopes with: ${highlight(`pkgseer auth status`, useColors)}`);
3927
- process.exit(1);
3928
- }
3929
- const existingConfig = await configService.loadProjectConfig();
3930
- if (existingConfig?.config.project) {
3931
- console.error(error(`A project is already configured in pkgseer.yml: ${highlight(existingConfig.config.project, useColors)}`, useColors));
3932
- console.log(dim(`
3933
- To reinitialize, either remove pkgseer.yml or edit it manually.`, useColors));
3934
- console.log(dim(` To update manifest files, use: `, useColors) + highlight(`pkgseer project detect`, useColors));
4576
+ To get started, run: pkgseer project init`);
4577
+ console.log(` This will create a project and detect your manifest files.`);
3935
4578
  process.exit(1);
3936
4579
  }
3937
- let projectName = options.name?.trim();
3938
- if (!projectName) {
3939
- const cwd2 = fileSystemService.getCwd();
3940
- const basename = cwd2.split(/[/\\]/).pop() ?? "project";
3941
- const input2 = await promptService.input(`Project name:`, basename);
3942
- projectName = input2.trim();
3943
- }
3944
- console.log(`
3945
- Creating project ${highlight(projectName, useColors)}...`);
3946
- let createResult;
3947
- let projectAlreadyExists = false;
3948
- try {
3949
- createResult = await projectService.createProject({
3950
- name: projectName
3951
- });
3952
- } catch (createError) {
3953
- const errorMessage = createError instanceof Error ? createError.message : String(createError);
3954
- if (createError instanceof Error && (errorMessage.includes("already been taken") || errorMessage.includes("already exists"))) {
3955
- console.log(dim(`
3956
- Project ${highlight(projectName, useColors)} already exists on the server.`, useColors));
3957
- console.log(dim(` This might happen if you previously ran init but didn't complete the setup.`, useColors));
3958
- const useExisting = await promptService.confirm(`
3959
- Do you want to use the existing project and continue with manifest setup?`, true);
3960
- if (!useExisting) {
3961
- console.log(dim(`
3962
- Exiting. Please choose a different project name or use an existing project.`, useColors));
3963
- process.exit(0);
3964
- }
3965
- projectAlreadyExists = true;
3966
- createResult = {
3967
- project: {
3968
- name: projectName,
3969
- defaultBranch: "main"
3970
- },
3971
- errors: null
3972
- };
4580
+ const projectName = projectConfig.config.project;
4581
+ const existingManifests = projectConfig.config.manifests ?? [];
4582
+ console.log(`Scanning for manifest files in project "${projectName}"...
4583
+ `);
4584
+ const cwd = fileSystemService.getCwd();
4585
+ const detectedGroups = await detectAndGroupManifests(cwd, fileSystemService, {
4586
+ maxDepth: options.maxDepth ?? 3
4587
+ });
4588
+ if (detectedGroups.length === 0) {
4589
+ console.log("No manifest files were found in the current directory.");
4590
+ if (existingManifests.length > 0) {
4591
+ console.log(`
4592
+ Your existing configuration in pkgseer.yml will remain unchanged.`);
4593
+ console.log(` Tip: Make sure you're running this command from the project root directory.`);
3973
4594
  } else {
3974
- console.error(error(`Failed to create project: ${errorMessage}`, useColors));
3975
- if (createError instanceof Error && (errorMessage.includes("Insufficient permissions") || errorMessage.includes("Required scopes") || errorMessage.includes(PROJECT_MANIFEST_UPLOAD_SCOPE))) {
3976
- console.log(`
3977
- Your current token doesn't have the required permissions for creating projects.`);
3978
- console.log(`
3979
- To fix this:`);
3980
- console.log(` 1. Run: ${highlight(`pkgseer login --force`, useColors)}`);
3981
- console.log(` 2. Make sure to grant ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope during authentication`);
3982
- console.log(`
3983
- Or check your current scopes with: ${highlight(`pkgseer auth status`, useColors)}`);
3984
- } else if (createError instanceof Error && (errorMessage.includes("alphanumeric") || errorMessage.includes("hyphens") || errorMessage.includes("underscores"))) {
3985
- console.log(dim(`
3986
- Project name requirements:`, useColors));
3987
- console.log(dim(` • Must start with an alphanumeric character (a-z, A-Z, 0-9)`, useColors));
3988
- console.log(dim(` • Can contain letters, numbers, hyphens (-), and underscores (_)`, useColors));
3989
- console.log(dim(` • Example valid names: ${highlight(`my-project`, useColors)}, ${highlight(`project_123`, useColors)}, ${highlight(`backend`, useColors)}`, useColors));
3990
- }
3991
- process.exit(1);
4595
+ console.log(`
4596
+ Tip: Manifest files like package.json, requirements.txt, or pyproject.toml should be in the current directory or subdirectories.`);
3992
4597
  }
4598
+ return;
3993
4599
  }
3994
- if (!createResult.project) {
3995
- if (createResult.errors && createResult.errors.length > 0) {
3996
- const firstError = createResult.errors[0];
3997
- console.error(error(`Failed to create project: ${firstError?.message ?? "Unknown error"}`, useColors));
3998
- if (firstError?.field) {
3999
- console.log(dim(`
4000
- Field: ${firstError.field}`, useColors));
4600
+ const suggestedManifests = matchManifestsWithConfig(detectedGroups, existingManifests);
4601
+ console.log("Current configuration in pkgseer.yml:");
4602
+ if (existingManifests.length === 0) {
4603
+ console.log(" (no manifests configured yet)");
4604
+ } else {
4605
+ for (const group of existingManifests) {
4606
+ console.log(` Label: ${group.label}`);
4607
+ for (const file of group.files) {
4608
+ console.log(` ${file}`);
4001
4609
  }
4002
- process.exit(1);
4003
4610
  }
4004
- console.error(error(`Failed to create project`, useColors));
4005
- console.log(dim(`
4006
- Please try again or contact support if the issue persists.`, useColors));
4007
- process.exit(1);
4008
- }
4009
- if (projectAlreadyExists) {
4010
- console.log(success(`Using existing project ${highlight(createResult.project.name, useColors)}`, useColors));
4011
- } else {
4012
- console.log(success(`Project ${highlight(createResult.project.name, useColors)} created successfully!`, useColors));
4013
4611
  }
4014
- const maxDepth = options.maxDepth ?? 3;
4015
- console.log(dim(`
4016
- Scanning for manifest files (max depth: ${maxDepth})...`, useColors));
4017
- const cwd = fileSystemService.getCwd();
4018
- const manifestGroups = await detectAndGroupManifests(cwd, fileSystemService, {
4019
- maxDepth
4020
- });
4021
- const configToWrite = {
4022
- project: projectName
4023
- };
4024
- if (manifestGroups.length > 0) {
4025
- console.log(`
4026
- Found ${highlight(`${manifestGroups.length}`, useColors)} manifest group${manifestGroups.length === 1 ? "" : "s"}:
4027
- `);
4028
- for (const group of manifestGroups) {
4029
- console.log(` Label: ${highlight(group.label, useColors)}`);
4030
- for (const manifest of group.manifests) {
4031
- console.log(` ${highlight(manifest.relativePath, useColors)} ${dim(`(${manifest.type})`, useColors)}`);
4032
- }
4612
+ console.log(`
4613
+ Suggested configuration:`);
4614
+ for (const group of suggestedManifests) {
4615
+ const markers = [];
4616
+ if (group.isNew)
4617
+ markers.push("new");
4618
+ if (group.changedLabel)
4619
+ markers.push("label changed");
4620
+ const markerStr = markers.length > 0 ? ` (${markers.join(", ")})` : "";
4621
+ console.log(` Label: ${group.label}${markerStr}`);
4622
+ for (const file of group.files) {
4623
+ const wasInConfig = existingManifests.some((g) => g.files.includes(file));
4624
+ const prefix = wasInConfig ? " " : " + ";
4625
+ console.log(`${prefix}${file}`);
4033
4626
  }
4034
- const hasHexManifests = manifestGroups.some((group) => group.manifests.some((m) => m.type === "hex"));
4035
- let allowMixDeps = false;
4036
- if (hasHexManifests) {
4037
- console.log(dim(`
4038
- Note: Elixir/Hex manifest files (mix.exs, mix.lock) are Elixir code and cannot be directly uploaded.`, useColors));
4039
- console.log(dim(` Instead, we need to run "mix deps --all" and "mix deps.tree" to generate dependency information.`, useColors));
4627
+ }
4628
+ const hasHexManifests = detectedGroups.some((group) => group.manifests.some((m) => m.type === "hex"));
4629
+ const hasHexInSuggested = suggestedManifests.some((g) => g.files.some((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock")));
4630
+ let allowMixDeps = false;
4631
+ if (hasHexInSuggested) {
4632
+ const existingHasMixDeps = existingManifests.some((g) => g.allow_mix_deps === true);
4633
+ if (!existingHasMixDeps) {
4634
+ console.log(`
4635
+ Note: Elixir/Hex manifest files (mix.exs, mix.lock) are Elixir code and cannot be directly uploaded.`);
4636
+ console.log(` Instead, we need to run "mix deps --all" and "mix deps.tree" to generate dependency information.`);
4040
4637
  allowMixDeps = await promptService.confirm(`
4041
4638
  Allow running "mix deps --all" and "mix deps.tree" for hex manifests?`, true);
4639
+ } else {
4640
+ allowMixDeps = true;
4042
4641
  }
4043
- configToWrite.manifests = manifestGroups.map((group) => {
4044
- const hasHexInGroup = group.manifests.some((m) => m.type === "hex");
4045
- return {
4046
- label: group.label,
4047
- files: group.manifests.map((m) => m.relativePath),
4048
- ...hasHexInGroup && allowMixDeps ? { allow_mix_deps: true } : {}
4049
- };
4642
+ }
4643
+ const finalManifests = suggestedManifests.map((g) => {
4644
+ const hasHexInGroup = g.files.some((f) => f.endsWith("mix.exs") || f.endsWith("mix.lock"));
4645
+ return {
4646
+ label: g.label,
4647
+ files: g.files,
4648
+ isNew: g.isNew,
4649
+ changedLabel: g.changedLabel,
4650
+ ...hasHexInGroup && allowMixDeps ? { allow_mix_deps: true } : {},
4651
+ ...g.allow_mix_deps !== undefined ? { allow_mix_deps: g.allow_mix_deps } : {}
4652
+ };
4653
+ });
4654
+ const hasChanges = finalManifests.length !== existingManifests.length || finalManifests.some((g) => g.isNew || g.changedLabel) || existingManifests.some((existing) => {
4655
+ const suggested = finalManifests.find((s) => s.label === existing.label);
4656
+ if (!suggested)
4657
+ return true;
4658
+ const existingFiles = new Set(existing.files);
4659
+ const suggestedFiles = new Set(suggested.files);
4660
+ return existingFiles.size !== suggestedFiles.size || !Array.from(existingFiles).every((f) => suggestedFiles.has(f));
4661
+ }) || finalManifests.some((g) => {
4662
+ const existing = existingManifests.find((e) => e.label === g.label);
4663
+ return existing?.allow_mix_deps !== g.allow_mix_deps;
4664
+ });
4665
+ if (!hasChanges) {
4666
+ console.log(`
4667
+ ✓ Your configuration is already up to date!`);
4668
+ console.log(`
4669
+ All detected manifest files match your current pkgseer.yml configuration.`);
4670
+ return;
4671
+ }
4672
+ if (options.update) {
4673
+ await configService.writeProjectConfig({
4674
+ project: projectName,
4675
+ manifests: finalManifests.map((g) => ({
4676
+ label: g.label,
4677
+ files: g.files,
4678
+ ...g.allow_mix_deps ? { allow_mix_deps: g.allow_mix_deps } : {}
4679
+ }))
4050
4680
  });
4051
- const action = await promptService.select(`
4052
- What would you like to do next?`, [
4053
- {
4054
- value: "upload",
4055
- name: "Save config and upload manifests now",
4056
- description: "Recommended: saves configuration and uploads files immediately"
4057
- },
4058
- {
4059
- value: "edit",
4060
- name: "Save config for manual editing",
4061
- description: "Saves configuration so you can customize labels before uploading"
4062
- },
4063
- {
4064
- value: "abort",
4065
- name: "Skip configuration for now",
4066
- description: "Project is created but no config saved (you can run init again later)"
4067
- }
4068
- ]);
4069
- if (action === "abort") {
4070
- if (projectAlreadyExists) {
4071
- console.log(`
4072
- ${success(`Using existing project ${highlight(projectName, useColors)}`, useColors)}`);
4073
- } else {
4074
- console.log(`
4075
- ${success(`Project ${highlight(projectName, useColors)} created successfully!`, useColors)}`);
4076
- }
4077
- console.log(dim(`
4078
- Configuration was not saved. To configure later, run:`, useColors));
4079
- console.log(` ${highlight(`pkgseer project init --name ${projectName}`, useColors)}`);
4080
- console.log(dim(`
4081
- Or manually create pkgseer.yml and run: `, useColors) + highlight(`pkgseer project upload`, useColors));
4082
- process.exit(0);
4083
- }
4084
- if (action === "upload") {
4085
- await configService.writeProjectConfig(configToWrite);
4086
- console.log(success(`Configuration saved to ${highlight("pkgseer.yml", useColors)}`, useColors));
4087
- const branch = await gitService.getCurrentBranch() ?? createResult.project.defaultBranch;
4681
+ console.log(`
4682
+ Configuration updated successfully!`);
4683
+ console.log(`
4684
+ Your pkgseer.yml has been updated with the detected manifest files.`);
4685
+ console.log(` Run 'pkgseer project upload' to upload them to your project.`);
4686
+ } else {
4687
+ const shouldUpdate = await promptService.confirm(`
4688
+ Would you like to update pkgseer.yml with these changes?`, false);
4689
+ if (shouldUpdate) {
4690
+ await configService.writeProjectConfig({
4691
+ project: projectName,
4692
+ manifests: finalManifests.map((g) => ({
4693
+ label: g.label,
4694
+ files: g.files,
4695
+ ...g.allow_mix_deps ? { allow_mix_deps: g.allow_mix_deps } : {}
4696
+ }))
4697
+ });
4088
4698
  console.log(`
4089
- Uploading manifest files to branch ${highlight(branch, useColors)}...`);
4090
- const cwd2 = fileSystemService.getCwd();
4091
- for (const group of manifestGroups) {
4092
- try {
4093
- const hasHexManifests2 = group.manifests.some((m) => m.type === "hex");
4094
- const allowMixDeps2 = configToWrite.manifests?.find((m) => m.label === group.label)?.allow_mix_deps === true;
4095
- const allFiles = await processManifestFiles({
4096
- files: group.manifests.map((m) => m.relativePath),
4097
- basePath: cwd2,
4098
- hasHexManifests: hasHexManifests2,
4099
- allowMixDeps: allowMixDeps2 ?? false,
4100
- fileSystemService,
4101
- shellService
4102
- });
4103
- const validFiles = allFiles.filter((f) => f !== null);
4104
- if (validFiles.length === 0) {
4105
- console.log(` ${dim(`(no files to upload for ${group.label})`, useColors)}`);
4106
- continue;
4107
- }
4108
- const uploadResult = await projectService.uploadManifests({
4109
- project: projectName,
4110
- branch,
4111
- label: group.label,
4112
- files: validFiles
4113
- });
4114
- for (const result of uploadResult.results) {
4115
- if (result.status === "success") {
4116
- const depsCount = result.dependencies_count ?? 0;
4117
- const labelText = highlight(group.label, useColors);
4118
- const fileText = highlight(result.filename, useColors);
4119
- const depsText = dim(`(${depsCount} dependencies)`, useColors);
4120
- console.log(` ${success(`${labelText}: ${fileText}`, useColors)} ${depsText}`);
4121
- } else {
4122
- console.log(` ${error(`${group.label}: ${result.filename} - ${result.error ?? "Unknown error"}`, useColors)}`);
4123
- }
4124
- }
4125
- } catch (uploadError) {
4126
- const errorMessage = uploadError instanceof Error ? uploadError.message : String(uploadError);
4127
- let userMessage = `Failed to upload ${group.label}: ${errorMessage}`;
4128
- if (hasHexManifests) {
4129
- if (errorMessage.includes("command not found") || errorMessage.includes("mix:")) {
4130
- userMessage = `Failed to process hex manifest files for ${group.label}.
4131
- ` + ` Error: ${errorMessage}
4132
- ` + ` Make sure Elixir and 'mix' are installed. Install Elixir from https://elixir-lang.org/install.html`;
4133
- } else if (errorMessage.includes("Failed to generate dependencies")) {
4134
- userMessage = errorMessage;
4135
- } else if (errorMessage.includes("Network") || errorMessage.includes("ECONNREFUSED")) {
4136
- userMessage = `Failed to upload ${group.label}: Network error.
4137
- ` + ` Error: ${errorMessage}
4138
- ` + ` Check your internet connection and try again.`;
4139
- }
4140
- } else if (errorMessage.includes("Network") || errorMessage.includes("ECONNREFUSED")) {
4141
- userMessage = `Failed to upload ${group.label}: Network error.
4142
- ` + ` Error: ${errorMessage}
4143
- ` + ` Check your internet connection and try again.`;
4144
- }
4145
- console.error(error(userMessage, useColors));
4146
- }
4147
- }
4699
+ Configuration updated successfully!`);
4148
4700
  console.log(`
4149
- ${success(`Project initialization complete!`, useColors)}`);
4150
- console.log(dim(`
4151
- View your project: `, useColors) + highlight(`${deps.baseUrl}/projects/${projectName}`, useColors));
4152
- console.log(dim(`
4153
- To upload updated manifests later, run: `, useColors) + highlight(`pkgseer project upload`, useColors));
4154
- return;
4701
+ Your pkgseer.yml has been updated. Run 'pkgseer project upload' to upload the manifests.`);
4702
+ } else {
4703
+ console.log(`
4704
+ Configuration was not updated.`);
4705
+ console.log(` To apply these changes automatically, run: pkgseer project detect --update`);
4706
+ console.log(` Or manually edit pkgseer.yml and then run: pkgseer project upload`);
4155
4707
  }
4156
- } else {
4157
- console.log(dim(`
4158
- No manifest files were found in the current directory.`, useColors));
4159
- }
4160
- await configService.writeProjectConfig(configToWrite);
4161
- console.log(success(`Configuration saved to ${highlight("pkgseer.yml", useColors)}`, useColors));
4162
- if (manifestGroups.length > 0) {
4163
- console.log(dim(`
4164
- Next steps:`, useColors));
4165
- console.log(dim(` 1. Edit pkgseer.yml to customize manifest labels if needed`, useColors));
4166
- console.log(dim(` 2. Run: `, useColors) + highlight(`pkgseer project upload`, useColors));
4167
- console.log(dim(`
4168
- Tip: Use `, useColors) + highlight(`pkgseer project detect`, useColors) + dim(` to automatically update your config when you add new manifest files.`, useColors));
4169
- } else {
4170
- console.log(dim(`
4171
- Next steps:`, useColors));
4172
- console.log(dim(` 1. Add manifest files to your project`, useColors));
4173
- console.log(dim(` 2. Edit pkgseer.yml to configure them:`, useColors));
4174
- console.log(dim(`
4175
- manifests:`, useColors));
4176
- console.log(dim(` - label: backend`, useColors));
4177
- console.log(dim(` files:`, useColors));
4178
- console.log(dim(` - `, useColors) + highlight(`package-lock.json`, useColors));
4179
- console.log(dim(`
4180
- 3. Run: `, useColors) + highlight(`pkgseer project upload`, useColors));
4181
- console.log(dim(`
4182
- Tip: Run `, useColors) + highlight(`pkgseer project detect`, useColors) + dim(` after adding manifest files to auto-configure them.`, useColors));
4183
4708
  }
4184
4709
  }
4185
- var INIT_DESCRIPTION = `Initialize a new project in the current directory.
4710
+ var DETECT_DESCRIPTION = `Detect manifest files and update your project configuration.
4186
4711
 
4187
- This command will:
4188
- 1. Create a new project entry (or prompt for a project name)
4189
- 2. Scan for manifest files (package.json, requirements.txt, etc.)
4190
- 3. Suggest labels based on directory structure
4191
- 4. Optionally upload manifests to the project
4712
+ This command scans your project directory for manifest files (like
4713
+ package.json, requirements.txt, etc.) and compares them with your
4714
+ current pkgseer.yml configuration. It will:
4192
4715
 
4193
- The project name defaults to the current directory name, or you can
4194
- specify it with --name. Manifest files are automatically detected
4195
- and grouped by their directory structure.`;
4196
- function registerProjectInitCommand(program) {
4197
- program.command("init").summary("Initialize a new project").description(INIT_DESCRIPTION).option("--name <name>", "Project name (alphanumeric, hyphens, underscores only)").option("--max-depth <depth>", "Maximum directory depth to scan for manifests", (val) => Number.parseInt(val, 10), 3).action(async (options) => {
4716
+ Preserve existing labels for files you've already configured
4717
+ Suggest new labels for newly detected files
4718
+ Show you what would change before updating
4719
+
4720
+ Perfect for when you add new manifest files or reorganize your
4721
+ project structure. Run with --update to automatically apply changes.`;
4722
+ function registerProjectDetectCommand(program) {
4723
+ program.command("detect").summary("Detect manifest files and suggest config updates").description(DETECT_DESCRIPTION).option("--max-depth <depth>", "Maximum directory depth to scan for manifests", (val) => Number.parseInt(val, 10), 3).option("--update", "Automatically update pkgseer.yml without prompting", false).action(async (options) => {
4198
4724
  const deps = await createContainer();
4199
- await projectInitAction({ name: options.name, maxDepth: options.maxDepth }, {
4200
- projectService: deps.projectService,
4725
+ await projectDetectAction({ maxDepth: options.maxDepth, update: options.update }, {
4201
4726
  configService: deps.configService,
4202
4727
  fileSystemService: deps.fileSystemService,
4203
- gitService: deps.gitService,
4204
4728
  promptService: deps.promptService,
4205
- shellService: deps.shellService,
4206
- authStorage: deps.authStorage,
4207
- baseUrl: deps.baseUrl
4729
+ shellService: deps.shellService
4208
4730
  });
4209
4731
  });
4210
4732
  }
@@ -4217,11 +4739,10 @@ async function projectUploadAction(options, deps) {
4217
4739
  gitService,
4218
4740
  shellService,
4219
4741
  promptService,
4220
- authStorage,
4221
4742
  baseUrl
4222
4743
  } = deps;
4223
4744
  const useColors = shouldUseColors2();
4224
- const tokenData = await checkProjectWriteScope(authStorage, baseUrl);
4745
+ const tokenData = await checkProjectWriteScope(configService, baseUrl);
4225
4746
  if (!tokenData) {
4226
4747
  console.error(error(`Authentication required. Please run 'pkgseer login'.`, useColors));
4227
4748
  console.log(`
@@ -4232,17 +4753,6 @@ async function projectUploadAction(options, deps) {
4232
4753
  Or check your current scopes with: ${highlight(`pkgseer auth status`, useColors)}`);
4233
4754
  process.exit(1);
4234
4755
  }
4235
- const isEnvToken = tokenData.scopes.length === 0 && tokenData.tokenName === "PKGSEER_API_TOKEN";
4236
- if (isEnvToken) {} else if (!tokenData.scopes.includes(PROJECT_MANIFEST_UPLOAD_SCOPE)) {
4237
- console.error(error(`Token missing required scope: ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)}`, useColors));
4238
- console.log(`
4239
- To fix this:`);
4240
- console.log(` 1. Run: ${highlight(`pkgseer login --force`, useColors)}`);
4241
- console.log(` 2. Make sure to grant ${highlight(PROJECT_MANIFEST_UPLOAD_SCOPE, useColors)} scope during authentication`);
4242
- console.log(`
4243
- Or check your current scopes with: ${highlight(`pkgseer auth status`, useColors)}`);
4244
- process.exit(1);
4245
- }
4246
4756
  const projectConfig = await configService.loadProjectConfig();
4247
4757
  if (!projectConfig?.config.project) {
4248
4758
  console.error(error(`No project is configured in pkgseer.yml`, useColors));
@@ -4411,7 +4921,6 @@ function registerProjectUploadCommand(program) {
4411
4921
  gitService: deps.gitService,
4412
4922
  shellService: deps.shellService,
4413
4923
  promptService: deps.promptService,
4414
- authStorage: deps.authStorage,
4415
4924
  baseUrl: deps.baseUrl
4416
4925
  });
4417
4926
  });
@@ -4510,9 +5019,9 @@ function registerQuickstartCommand(program) {
4510
5019
  var program = new Command;
4511
5020
  program.name("pkgseer").description("Package intelligence for your AI assistant").version(version).addHelpText("after", `
4512
5021
  Getting started:
4513
- pkgseer login Authenticate with your account
4514
- pkgseer mcp Start the MCP server for AI assistants
4515
- pkgseer quickstart Show quick reference for AI agents
5022
+ pkgseer init Interactive setup wizard (project + MCP)
5023
+ pkgseer login Authenticate with your account
5024
+ pkgseer quickstart Quick reference for AI agents
4516
5025
 
4517
5026
  Package commands:
4518
5027
  pkgseer pkg info <package> Get package summary
@@ -4527,6 +5036,7 @@ Documentation commands:
4527
5036
  pkgseer docs search <query> Search documentation
4528
5037
 
4529
5038
  Learn more at https://pkgseer.dev`);
5039
+ registerInitCommand(program);
4530
5040
  registerMcpCommand(program);
4531
5041
  registerLoginCommand(program);
4532
5042
  registerLogoutCommand(program);