|
| 1 | +/** |
| 2 | + * charter adf context |
| 3 | + * |
| 4 | + * Resolves ADF modules based on file paths and/or explicit keywords. |
| 5 | + * Outputs the resolved module list and optionally the bundled context. |
| 6 | + */ |
| 7 | + |
| 8 | +import * as fs from 'node:fs'; |
| 9 | +import * as path from 'node:path'; |
| 10 | +import { |
| 11 | + parseAdf, |
| 12 | + parseManifest, |
| 13 | + resolveModules, |
| 14 | + bundleModules, |
| 15 | + formatAdf, |
| 16 | +} from '@stackbilt/adf'; |
| 17 | +import type { CLIOptions } from '../index'; |
| 18 | +import { CLIError, EXIT_CODE } from '../index'; |
| 19 | +import { getFlag, tokenizeTask } from '../flags'; |
| 20 | + |
| 21 | +/** |
| 22 | + * Extract keywords from file paths using extension and directory signals. |
| 23 | + * Maps file system structure to domain keywords for module resolution. |
| 24 | + */ |
| 25 | +export function filePathToKeywords(filePaths: string[]): string[] { |
| 26 | + const keywords: string[] = []; |
| 27 | + for (const fp of filePaths) { |
| 28 | + const ext = path.extname(fp).slice(1).toLowerCase(); |
| 29 | + const dir = path.dirname(fp).toLowerCase(); |
| 30 | + |
| 31 | + // Extension signals |
| 32 | + if (['tsx', 'jsx'].includes(ext)) keywords.push('react', 'ui', 'frontend'); |
| 33 | + if (['css', 'scss', 'sass', 'less'].includes(ext)) keywords.push('css', 'ui', 'frontend'); |
| 34 | + if (['prisma'].includes(ext)) keywords.push('db', 'backend'); |
| 35 | + if (['sql'].includes(ext)) keywords.push('db', 'migration'); |
| 36 | + if (ext === 'toml' && fp.includes('wrangler')) keywords.push('deploy', 'cloudflare'); |
| 37 | + |
| 38 | + // Directory signals |
| 39 | + if (/\b(component|ui|widget|page|layout|view)\b/.test(dir)) keywords.push('frontend', 'ui'); |
| 40 | + if (/\b(api|server|handler|route|middleware|controller)\b/.test(dir)) keywords.push('api', 'backend'); |
| 41 | + if (/\b(test|spec|__tests__|e2e)\b/.test(dir)) keywords.push('test', 'qa'); |
| 42 | + if (/\b(deploy|infra|docker|ci|\.github)\b/.test(dir)) keywords.push('deploy', 'infra'); |
| 43 | + if (/\b(auth|session|permission)\b/.test(dir)) keywords.push('auth', 'security'); |
| 44 | + } |
| 45 | + return [...new Set(keywords)]; |
| 46 | +} |
| 47 | + |
| 48 | +export function adfContextCommand(options: CLIOptions, args: string[]): number { |
| 49 | + const filesFlag = getFlag(args, '--files'); |
| 50 | + const keywordsFlag = getFlag(args, '--keywords'); |
| 51 | + const aiDir = getFlag(args, '--ai-dir') || '.ai'; |
| 52 | + const bundle = args.includes('--bundle'); |
| 53 | + |
| 54 | + if (!filesFlag && !keywordsFlag) { |
| 55 | + throw new CLIError( |
| 56 | + 'adf context requires --files <path,...> and/or --keywords <kw,...>.\n' + |
| 57 | + 'Usage: charter adf context --files src/components/Button.tsx,src/api/handler.ts' |
| 58 | + ); |
| 59 | + } |
| 60 | + |
| 61 | + const manifestPath = path.join(aiDir, 'manifest.adf'); |
| 62 | + if (!fs.existsSync(manifestPath)) { |
| 63 | + throw new CLIError(`manifest.adf not found at ${manifestPath}. Run: charter adf init`); |
| 64 | + } |
| 65 | + |
| 66 | + // Collect keywords from both sources |
| 67 | + const allKeywords: string[] = []; |
| 68 | + if (filesFlag) { |
| 69 | + const filePaths = filesFlag.split(',').map(f => f.trim()).filter(Boolean); |
| 70 | + allKeywords.push(...filePathToKeywords(filePaths)); |
| 71 | + } |
| 72 | + if (keywordsFlag) { |
| 73 | + allKeywords.push(...tokenizeTask(keywordsFlag)); |
| 74 | + } |
| 75 | + |
| 76 | + const dedupKeywords = [...new Set(allKeywords)]; |
| 77 | + |
| 78 | + const manifestContent = fs.readFileSync(manifestPath, 'utf-8'); |
| 79 | + const manifestDoc = parseAdf(manifestContent); |
| 80 | + const manifest = parseManifest(manifestDoc); |
| 81 | + |
| 82 | + const resolvedModules = resolveModules(manifest, dedupKeywords); |
| 83 | + |
| 84 | + if (bundle) { |
| 85 | + // Full bundle output |
| 86 | + const readFile = (modulePath: string) => { |
| 87 | + const fullPath = path.join(aiDir, modulePath); |
| 88 | + return fs.readFileSync(fullPath, 'utf-8'); |
| 89 | + }; |
| 90 | + |
| 91 | + const result = bundleModules(aiDir, resolvedModules, readFile, dedupKeywords, manifest); |
| 92 | + |
| 93 | + if (options.format === 'json') { |
| 94 | + console.log(JSON.stringify({ |
| 95 | + keywords: dedupKeywords, |
| 96 | + resolvedModules: result.resolvedModules, |
| 97 | + tokenEstimate: result.tokenEstimate, |
| 98 | + triggerMatches: result.triggerMatches, |
| 99 | + content: formatAdf(result.mergedDocument), |
| 100 | + }, null, 2)); |
| 101 | + } else { |
| 102 | + console.log(formatAdf(result.mergedDocument)); |
| 103 | + } |
| 104 | + } else { |
| 105 | + // Module list only |
| 106 | + if (options.format === 'json') { |
| 107 | + console.log(JSON.stringify({ |
| 108 | + keywords: dedupKeywords, |
| 109 | + resolvedModules, |
| 110 | + }, null, 2)); |
| 111 | + } else { |
| 112 | + console.log(` Keywords: ${dedupKeywords.join(', ')}`); |
| 113 | + console.log(` Resolved modules:`); |
| 114 | + for (const mod of resolvedModules) { |
| 115 | + const isDefault = manifest.defaultLoad.includes(mod); |
| 116 | + console.log(` ${mod} (${isDefault ? 'DEFAULT' : 'ON_DEMAND'})`); |
| 117 | + } |
| 118 | + } |
| 119 | + } |
| 120 | + |
| 121 | + return EXIT_CODE.SUCCESS; |
| 122 | +} |
0 commit comments