Skip to content

Commit 3ba3957

Browse files
feat(cli): apply logical updates
1 parent 600e222 commit 3ba3957

8 files changed

Lines changed: 475 additions & 41 deletions

File tree

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { filePathToKeywords } from '../commands/adf-context';
3+
4+
describe('filePathToKeywords', () => {
5+
it('extracts react/frontend keywords from .tsx files', () => {
6+
const kw = filePathToKeywords(['src/components/Button.tsx']);
7+
expect(kw).toContain('react');
8+
expect(kw).toContain('frontend');
9+
});
10+
11+
it('extracts css/frontend keywords from .css files', () => {
12+
const kw = filePathToKeywords(['src/styles/main.css']);
13+
expect(kw).toContain('css');
14+
expect(kw).toContain('frontend');
15+
});
16+
17+
it('extracts api/backend keywords from api directory paths', () => {
18+
const kw = filePathToKeywords(['src/api/handler.ts']);
19+
expect(kw).toContain('api');
20+
expect(kw).toContain('backend');
21+
});
22+
23+
it('extracts test/qa keywords from test directory paths', () => {
24+
const kw = filePathToKeywords(['src/__tests__/foo.test.ts']);
25+
expect(kw).toContain('test');
26+
expect(kw).toContain('qa');
27+
});
28+
29+
it('extracts deploy/infra keywords from infra directories', () => {
30+
const kw = filePathToKeywords(['deploy/Dockerfile']);
31+
expect(kw).toContain('deploy');
32+
expect(kw).toContain('infra');
33+
});
34+
35+
it('extracts auth/security keywords from auth directories', () => {
36+
const kw = filePathToKeywords(['src/auth/session.ts']);
37+
expect(kw).toContain('auth');
38+
expect(kw).toContain('security');
39+
});
40+
41+
it('extracts db/backend keywords from .prisma files', () => {
42+
const kw = filePathToKeywords(['prisma/schema.prisma']);
43+
expect(kw).toContain('db');
44+
expect(kw).toContain('backend');
45+
});
46+
47+
it('deduplicates keywords across multiple files', () => {
48+
const kw = filePathToKeywords(['src/components/A.tsx', 'src/components/B.tsx']);
49+
const reactCount = kw.filter(k => k === 'react').length;
50+
expect(reactCount).toBe(1);
51+
});
52+
53+
it('returns empty array for unknown file types and directories', () => {
54+
const kw = filePathToKeywords(['README.md']);
55+
expect(kw).toHaveLength(0);
56+
});
57+
58+
it('extracts cloudflare/deploy keywords from wrangler.toml', () => {
59+
const kw = filePathToKeywords(['wrangler.toml']);
60+
expect(kw).toContain('deploy');
61+
expect(kw).toContain('cloudflare');
62+
});
63+
64+
it('handles component directory signal', () => {
65+
const kw = filePathToKeywords(['src/ui/widget/DatePicker.ts']);
66+
expect(kw).toContain('frontend');
67+
});
68+
});
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
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

Comments
 (0)