(options: SuggestOptions)
| 825 | } |
| 826 | |
| 827 | async function suggestCommand(options: SuggestOptions): Promise<void> { |
| 828 | trackEvent("command", { name: "suggest" }); |
| 829 | log.blank(); |
| 830 | |
| 831 | // Step 1: Detect dependencies |
| 832 | const scanSpinner = ora("Scanning project dependencies...").start(); |
| 833 | const deps = await detectProjectDependencies(process.cwd()); |
| 834 | |
| 835 | if (deps.length === 0) { |
| 836 | scanSpinner.warn(pc.yellow("No dependencies detected")); |
| 837 | log.info(`Try ${pc.cyan("ctx7 skills search <keyword>")} to search manually`); |
| 838 | return; |
| 839 | } |
| 840 | |
| 841 | scanSpinner.succeed(`Found ${deps.length} dependencies`); |
| 842 | |
| 843 | // Step 2: Single API call to backend |
| 844 | const searchSpinner = ora("Finding matching skills...").start(); |
| 845 | |
| 846 | const tokens = loadTokens(); |
| 847 | const accessToken = tokens && !isTokenExpired(tokens) ? tokens.access_token : undefined; |
| 848 | |
| 849 | let data; |
| 850 | try { |
| 851 | data = await suggestSkills(deps, accessToken); |
| 852 | } catch { |
| 853 | searchSpinner.fail(pc.red("Failed to connect to Context7")); |
| 854 | return; |
| 855 | } |
| 856 | |
| 857 | if (data.error) { |
| 858 | searchSpinner.fail(pc.red(`Error: ${data.message || data.error}`)); |
| 859 | return; |
| 860 | } |
| 861 | |
| 862 | const skills = data.skills; |
| 863 | |
| 864 | if (skills.length === 0) { |
| 865 | searchSpinner.warn(pc.yellow("No matching skills found for your dependencies")); |
| 866 | return; |
| 867 | } |
| 868 | |
| 869 | searchSpinner.succeed(`Found ${skills.length} relevant skill(s)`); |
| 870 | trackEvent("suggest_results", { depCount: deps.length, skillCount: skills.length }); |
| 871 | log.blank(); |
| 872 | |
| 873 | const nameWithRepo = (s: SkillSearchResult) => `${s.name} ${pc.dim(`(${s.project})`)}`; |
| 874 | const nameWithRepoLen = (s: SkillSearchResult) => `${s.name} (${s.project})`.length; |
| 875 | const maxNameLen = Math.max(...skills.map(nameWithRepoLen)); |
| 876 | const popularityColWidth = 13; |
| 877 | const trustColWidth = 8; |
| 878 | const maxMatchedLen = Math.max(...skills.map((s) => s.matchedDep.length)); |
| 879 | const indexWidth = skills.length.toString().length; |
| 880 | |
| 881 | const choices = skills.map((s, index) => { |
| 882 | const indexStr = pc.dim(`${(index + 1).toString().padStart(indexWidth)}.`); |
| 883 | const rawLen = nameWithRepoLen(s); |
| 884 | const displayName = nameWithRepo(s) + " ".repeat(maxNameLen - rawLen); |
no test coverage detected