55
66import { CopilotClient , SessionEvent } from "@github/copilot-sdk" ;
77import chalk from "chalk" ;
8+ import * as fs from "fs" ;
9+ import * as path from "path" ;
810import type { RepoFacts , ScanResult , RepoInfo , BootcampOptions } from "./types.js" ;
911import { getRepoTools , setToolContext , clearToolContext } from "./tools.js" ;
1012import { validateRepoFacts , getMissingFieldsSummary , type ValidatedRepoFacts } from "./schema.js" ;
@@ -161,6 +163,185 @@ Include 2-4 codeExamples showing key patterns/usage (short snippets of 5-15 line
161163REMEMBER: Limit tool calls. After reading key files, produce output immediately. Don't over-research.` ;
162164}
163165
166+ /**
167+ * Create a fast analysis prompt with inline file contents (no tools needed)
168+ */
169+ function createFastAnalysisPrompt (
170+ repoPath : string ,
171+ repoInfo : RepoInfo ,
172+ scanResult : ScanResult ,
173+ options : BootcampOptions
174+ ) : string {
175+ // Read key files inline
176+ const keyFiles = [ "README.md" , "readme.md" , "package.json" , "pyproject.toml" , "Cargo.toml" , "go.mod" ] ;
177+ const inlineContents : string [ ] = [ ] ;
178+
179+ for ( const filename of keyFiles ) {
180+ const filePath = path . join ( repoPath , filename ) ;
181+ if ( fs . existsSync ( filePath ) ) {
182+ try {
183+ const content = fs . readFileSync ( filePath , "utf-8" ) . substring ( 0 , 5000 ) ;
184+ inlineContents . push ( `### ${ filename } \n\`\`\`\n${ content } \n\`\`\`` ) ;
185+ } catch ( e ) {
186+ // Skip unreadable files
187+ }
188+ }
189+ }
190+
191+ // Also try to read main entry point
192+ const entryPoints = [ "index.ts" , "index.js" , "src/index.ts" , "src/index.js" , "main.py" , "lib.rs" , "main.go" ] ;
193+ for ( const entry of entryPoints ) {
194+ const filePath = path . join ( repoPath , entry ) ;
195+ if ( fs . existsSync ( filePath ) ) {
196+ try {
197+ const content = fs . readFileSync ( filePath , "utf-8" ) . substring ( 0 , 3000 ) ;
198+ inlineContents . push ( `### ${ entry } \n\`\`\`\n${ content } \n\`\`\`` ) ;
199+ break ; // Only include first found entry point
200+ } catch ( e ) {
201+ // Skip
202+ }
203+ }
204+ }
205+
206+ const fileList = scanResult . files
207+ . filter ( ( f ) => ! f . isDirectory )
208+ . slice ( 0 , 30 )
209+ . map ( ( f ) => f . path )
210+ . join ( "\n" ) ;
211+
212+ const cmdList = scanResult . commands . map ( ( c ) => `- ${ c . name } : ${ c . command } ` ) . join ( "\n" ) ;
213+
214+ return `Analyze this repository and produce a comprehensive onboarding kit.
215+
216+ ## Repository
217+ - Name: ${ repoInfo . fullName }
218+ - URL: ${ repoInfo . url }
219+ - Branch: ${ repoInfo . branch }
220+
221+ ## Pre-detected Information
222+ Languages: ${ scanResult . stack . languages . join ( ", " ) || "Unknown" }
223+ Frameworks: ${ scanResult . stack . frameworks . join ( ", " ) || "None detected" }
224+ Build System: ${ scanResult . stack . buildSystem || "Unknown" }
225+ Has CI: ${ scanResult . stack . hasCi }
226+
227+ ## File Tree (first 30 files)
228+ ${ fileList }
229+
230+ ## Detected Commands
231+ ${ cmdList || "None detected" }
232+
233+ ## Key File Contents (READ THESE - no tools available)
234+ ${ inlineContents . join ( "\n\n" ) }
235+
236+ ---
237+
238+ Based on the above information, produce a JSON object. Follow this EXACT structure with these EXACT field names and enum values:
239+
240+ ## CRITICAL SCHEMA REQUIREMENTS:
241+
242+ ### Enum Values (use EXACTLY these strings):
243+ - confidence: "high", "medium", or "low"
244+ - entrypoints[].type: "main", "binary", "server", "cli", "web", or "library"
245+ - firstTasks[].difficulty: "beginner", "intermediate", or "advanced"
246+ - firstTasks[].category: "bug-fix", "test", "docs", "refactor", or "feature"
247+
248+ ### Required Fields:
249+ - repoName, purpose, description (all strings)
250+ - stack.languages, stack.frameworks (arrays of strings)
251+ - stack.buildSystem (string), stack.packageManager (string or null)
252+ - stack.hasDocker, stack.hasCi (booleans)
253+ - quickstart.prerequisites, quickstart.steps (arrays of strings)
254+ - quickstart.commands (array of {name, command, source})
255+ - structure.keyDirs (array of {path, purpose})
256+ - structure.entrypoints (array of {path, type, description})
257+ - structure.testDirs, structure.docsDirs (arrays of strings)
258+ - ci.workflows (array of {name, file, triggers, mainSteps})
259+ - ci.mainChecks (array of strings)
260+ - contrib.howToAddFeature, contrib.howToAddTest (arrays of strings)
261+ - architecture.overview (string)
262+ - architecture.components (array of {name, description, directory})
263+ - firstTasks (array with title, description, difficulty, category, files, why)
264+
265+ \`\`\`json
266+ {
267+ "repoName": "${ repoInfo . fullName } ",
268+ "purpose": "one-line description of what this repo does",
269+ "description": "2-3 sentence detailed description",
270+ "sources": ["README.md", "package.json"],
271+ "confidence": "high",
272+ "stack": {
273+ "languages": ["TypeScript"],
274+ "frameworks": ["Node.js"],
275+ "buildSystem": "npm",
276+ "packageManager": "npm",
277+ "hasDocker": false,
278+ "hasCi": true
279+ },
280+ "quickstart": {
281+ "prerequisites": ["Node.js >= 18"],
282+ "steps": ["Clone the repository", "Run npm install", "Run npm test"],
283+ "commands": [{"name": "install", "command": "npm install", "source": "package.json"}],
284+ "commonErrors": [{"error": "Missing dependencies", "fix": "Run npm install"}],
285+ "sources": ["README.md"]
286+ },
287+ "structure": {
288+ "keyDirs": [{"path": "src", "purpose": "Source code", "keyFiles": ["index.ts"]}],
289+ "entrypoints": [{"path": "src/index.ts", "type": "library", "description": "Main export"}],
290+ "testDirs": ["test"],
291+ "docsDirs": [],
292+ "sources": ["package.json"]
293+ },
294+ "ci": {
295+ "workflows": [{"name": "CI", "file": ".github/workflows/main.yml", "triggers": ["push", "pull_request"], "mainSteps": ["test", "lint"]}],
296+ "mainChecks": ["Tests must pass"],
297+ "sources": [".github/workflows/main.yml"]
298+ },
299+ "contrib": {
300+ "howToAddFeature": ["Create a new file in src/", "Export from index.ts", "Add tests"],
301+ "howToAddTest": ["Add test file in test/ directory", "Run npm test"],
302+ "codeStyle": "Standard JavaScript style",
303+ "sources": ["README.md"]
304+ },
305+ "architecture": {
306+ "overview": "Simple single-purpose utility library",
307+ "components": [{"name": "Core", "description": "Main functionality", "directory": "src"}],
308+ "dataFlow": "Input -> Process -> Output",
309+ "keyAbstractions": [{"name": "Main function", "description": "Primary export"}],
310+ "codeExamples": [{"title": "Basic usage", "file": "src/index.ts", "code": "import x from 'lib'", "explanation": "Import and use"}],
311+ "sources": ["src/index.ts"]
312+ },
313+ "firstTasks": [
314+ {
315+ "title": "Add a test case",
316+ "description": "Add a new test case for edge cases",
317+ "difficulty": "beginner",
318+ "category": "test",
319+ "files": ["test/test.js"],
320+ "why": "Good first contribution to understand the codebase"
321+ }
322+ ],
323+ "runbook": {
324+ "applicable": false,
325+ "deploySteps": [],
326+ "observability": [],
327+ "incidents": [],
328+ "sources": []
329+ }
330+ }
331+ \`\`\`
332+
333+ Focus: ${ options . focus }
334+ Target audience: ${ options . audience }
335+
336+ INSTRUCTIONS:
337+ 1. Replace the example values above with actual data from this repository
338+ 2. Provide at least 3-5 firstTasks with varying difficulty levels
339+ 3. Set runbook.applicable = false for libraries that aren't deployed as services
340+ 4. Use ONLY the exact enum values listed in the CRITICAL SCHEMA REQUIREMENTS section
341+
342+ IMPORTANT: Return ONLY the JSON object, no other text or markdown.` ;
343+ }
344+
164345/**
165346 * Parse the JSON response from Copilot and validate against schema
166347 */
@@ -248,9 +429,13 @@ const PREFERRED_MODELS = [
248429async function createSessionWithFallback (
249430 client : CopilotClient ,
250431 config : Parameters < CopilotClient [ "createSession" ] > [ 0 ] ,
251- verbose : boolean = false
432+ verbose : boolean = false ,
433+ overrideModel ?: string
252434) : Promise < { session : Awaited < ReturnType < CopilotClient [ "createSession" ] > > ; model : string } > {
253- for ( const model of PREFERRED_MODELS ) {
435+ // If a specific model is requested, try it first
436+ const modelsToTry = overrideModel ? [ overrideModel , ...PREFERRED_MODELS ] : PREFERRED_MODELS ;
437+
438+ for ( const model of modelsToTry ) {
254439 try {
255440 if ( verbose ) {
256441 console . log ( chalk . gray ( `Trying model: ${ model } ` ) ) ;
@@ -304,7 +489,64 @@ export async function analyzeRepo(
304489 startTime : Date . now ( ) ,
305490 } ;
306491
307- // Set up tool context
492+ const client = new CopilotClient ( ) ;
493+
494+ // Fast mode: no tools, inline file contents
495+ if ( options . fast ) {
496+ try {
497+ const { session, model } = await createSessionWithFallback (
498+ client ,
499+ {
500+ streaming : true ,
501+ systemMessage : { content : "You are an expert software architect. Analyze repositories and produce JSON output." } ,
502+ // No tools in fast mode
503+ } ,
504+ options . verbose ,
505+ options . model
506+ ) ;
507+
508+ stats . model = model ;
509+ console . log ( chalk . blue ( `\nUsing model: ${ model } ` ) ) ;
510+ console . log ( chalk . yellow ( `⚡ Fast mode: no tools, inline file contents\n` ) ) ;
511+
512+ const prompt = createFastAnalysisPrompt ( repoPath , repoInfo , scanResult , options ) ;
513+ let fullResponse = "" ;
514+
515+ session . on ( ( event : SessionEvent ) => {
516+ stats . totalEvents ++ ;
517+ if ( event . type === "assistant.message_delta" ) {
518+ const delta = event . data . deltaContent ;
519+ if ( delta ) {
520+ fullResponse += delta ;
521+ if ( options . verbose ) {
522+ process . stdout . write ( delta ) ;
523+ }
524+ }
525+ }
526+ } ) ;
527+
528+ await session . sendAndWait ( { prompt } , 300000 ) ;
529+ stats . responseLength = fullResponse . length ;
530+ stats . endTime = Date . now ( ) ;
531+
532+ const { facts, errors, warnings } = parseAndValidateRepoFacts ( fullResponse , options . verbose ) ;
533+
534+ if ( ! facts ) {
535+ throw new Error ( `Analysis failed: ${ errors ?. join ( ", " ) || "Unknown error" } ` ) ;
536+ }
537+
538+ if ( warnings ?. length ) {
539+ console . log ( chalk . yellow ( "\n[Warnings]" ) ) ;
540+ warnings . forEach ( w => console . log ( chalk . yellow ( ` - ${ w } ` ) ) ) ;
541+ }
542+
543+ return { facts : facts as RepoFacts , stats } ;
544+ } catch ( error : any ) {
545+ throw new Error ( `Fast analysis failed: ${ error . message } ` ) ;
546+ }
547+ }
548+
549+ // Standard mode with tools
308550 setToolContext ( {
309551 repoPath,
310552 verbose : options . verbose ,
@@ -324,8 +566,6 @@ export async function analyzeRepo(
324566 } ,
325567 } ) ;
326568
327- const client = new CopilotClient ( ) ;
328-
329569 try {
330570 // Get tools for the session
331571 const tools = getRepoTools ( ) ;
@@ -338,7 +578,8 @@ export async function analyzeRepo(
338578 systemMessage : { content : SYSTEM_PROMPT } ,
339579 tools,
340580 } ,
341- options . verbose
581+ options . verbose ,
582+ options . model
342583 ) ;
343584
344585 stats . model = model ;
@@ -432,7 +673,7 @@ No markdown, no explanations, just the JSON object starting with { and ending wi
432673- firstTasks: [{ title, description, difficulty, category, files, why }]` ;
433674
434675 fullResponse = "" ;
435- await session . sendAndWait ( { prompt : retryPrompt } , 120000 ) ;
676+ await session . sendAndWait ( { prompt : retryPrompt } , 300000 ) ;
436677 result = parseAndValidateRepoFacts ( fullResponse , options . verbose ) ;
437678 }
438679
0 commit comments