@iservu-inc/adf-cli 0.12.12 → 0.14.0
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/.claude/settings.local.json +6 -1
- package/.context/memory/architecture.md +40 -0
- package/.context/memory/glossary.md +19 -0
- package/.project/docs/VERSIONING-GUIDE.md +127 -0
- package/AGENTS.md +53 -0
- package/CHANGELOG.md +149 -0
- package/bin/adf.js +10 -0
- package/conductor/archive/context_synthesis_20260112/metadata.json +8 -0
- package/conductor/archive/context_synthesis_20260112/plan.md +40 -0
- package/conductor/archive/context_synthesis_20260112/spec.md +43 -0
- package/conductor/archive/verify_opencode_20260111/metadata.json +8 -0
- package/conductor/archive/verify_opencode_20260111/plan.md +34 -0
- package/conductor/archive/verify_opencode_20260111/spec.md +25 -0
- package/conductor/code_styleguides/javascript.md +51 -0
- package/conductor/product-guidelines.md +26 -0
- package/conductor/product.md +25 -0
- package/conductor/setup_state.json +1 -0
- package/conductor/tech-stack.md +28 -0
- package/conductor/tracks/bootstrap_agents_20260111/metadata.json +8 -0
- package/conductor/tracks/bootstrap_agents_20260111/plan.md +17 -0
- package/conductor/tracks/bootstrap_agents_20260111/spec.md +27 -0
- package/conductor/tracks.md +9 -0
- package/conductor/workflow.md +333 -0
- package/lib/analysis/ai-gap-analyzer.js +66 -0
- package/lib/analysis/dynamic-question-generator.js +55 -0
- package/lib/analysis/heuristic-gap-analyzer.js +45 -0
- package/lib/analysis/synthesis-engine.js +142 -0
- package/lib/commands/deploy.js +28 -2
- package/lib/commands/guide.js +173 -150
- package/lib/commands/tools.js +38 -0
- package/lib/generators/codex-cli-generator.js +41 -0
- package/lib/generators/index.js +33 -0
- package/lib/generators/kiro-generator.js +49 -0
- package/lib/generators/opencode-generator.js +332 -153
- package/lib/generators/trae-generator.js +34 -0
- package/lib/templates/scripts/analyze-framework-updates.js +361 -0
- package/lib/templates/scripts/build.js +608 -0
- package/lib/templates/scripts/check-framework-updates.js +118 -0
- package/lib/templates/scripts/config-helpers.js +1 -1
- package/lib/templates/scripts/deploy.js +13 -28
- package/lib/templates/scripts/init.js +110 -220
- package/lib/templates/scripts/postinstall.js +13 -0
- package/lib/templates/scripts/update-frameworks.js +28 -0
- package/lib/templates/scripts/validate-env.js +428 -0
- package/lib/templates/scripts/validate-mcp.js +471 -0
- package/lib/templates/scripts/validate.js +482 -0
- package/lib/templates/shared/agents/analyst.md +1 -1
- package/lib/templates/shared/agents/architect.md +13 -17
- package/lib/templates/shared/agents/dev.md +2 -2
- package/lib/templates/shared/agents/pm.md +1 -1
- package/lib/templates/shared/agents/qa.md +1 -1
- package/lib/templates/shared/agents/sm.md +2 -2
- package/lib/templates/shared/templates/README.md +2 -2
- package/lib/templates/shared/templates/openspec-proposal.md +2 -2
- package/lib/templates/shared/templates/prd-template.md +1 -1
- package/lib/templates/shared/templates/story-template.md +1 -1
- package/lib/utils/context-extractor.js +157 -0
- package/lib/utils/framework-detector.js +54 -0
- package/lib/utils/tool-feature-registry.js +102 -0
- package/package.json +1 -1
- package/tests/ai-gap-analyzer.test.js +38 -0
- package/tests/codex-cli-generator.test.js +29 -0
- package/tests/context-extractor.test.js +70 -0
- package/tests/deploy-integration.test.js +36 -0
- package/tests/deploy.test.js +57 -0
- package/tests/dynamic-question-generator.test.js +29 -0
- package/tests/framework-detector.test.js +55 -0
- package/tests/heuristic-gap-analyzer.test.js +46 -0
- package/tests/kiro-trae-generators.test.js +43 -0
- package/tests/opencode-generator.test.js +113 -0
- package/tests/synthesis-engine.test.js +52 -0
- package/tests/tool-feature-registry.test.js +23 -0
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Utility to extract project context from existing markdown files
|
|
6
|
+
* generated by different frameworks.
|
|
7
|
+
*/
|
|
8
|
+
class ContextExtractor {
|
|
9
|
+
/**
|
|
10
|
+
* Finds files matching a pattern recursively.
|
|
11
|
+
* @param {string} dir - Directory to search.
|
|
12
|
+
* @param {RegExp} pattern - Regex to match against relative path.
|
|
13
|
+
* @returns {Promise<string[]>} List of absolute paths.
|
|
14
|
+
*/
|
|
15
|
+
static async findFiles(dir, pattern) {
|
|
16
|
+
let results = [];
|
|
17
|
+
if (!(await fs.pathExists(dir))) {
|
|
18
|
+
return results;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const list = await fs.readdir(dir);
|
|
23
|
+
for (const file of list) {
|
|
24
|
+
const fullPath = path.resolve(dir, file);
|
|
25
|
+
const stat = await fs.stat(fullPath);
|
|
26
|
+
if (stat && stat.isDirectory()) {
|
|
27
|
+
results = results.concat(await this.findFiles(fullPath, pattern));
|
|
28
|
+
} else if (pattern.test(file)) {
|
|
29
|
+
results.push(fullPath);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
} catch (error) {
|
|
33
|
+
// Ignore errors reading directories
|
|
34
|
+
}
|
|
35
|
+
return results;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extracts context from existing framework files.
|
|
40
|
+
* @param {string} projectDir - The project directory.
|
|
41
|
+
* @param {string[]} frameworks - List of detected frameworks.
|
|
42
|
+
* @returns {Promise<Object>} Synthesized context object.
|
|
43
|
+
*/
|
|
44
|
+
static async extract(projectDir, frameworks) {
|
|
45
|
+
let context = {
|
|
46
|
+
name: null,
|
|
47
|
+
version: null,
|
|
48
|
+
overview: '',
|
|
49
|
+
techStack: '',
|
|
50
|
+
architecture: '',
|
|
51
|
+
proposedChanges: ''
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
if (frameworks.includes('bmad')) {
|
|
55
|
+
context = await this.extractFromBMAD(projectDir, context);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (frameworks.includes('openspec')) {
|
|
59
|
+
context = await this.extractFromOpenSpec(projectDir, context);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (frameworks.includes('spec-kit')) {
|
|
63
|
+
context = await this.extractFromSpecKit(projectDir, context);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return context;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
static async extractFromBMAD(projectDir, context) {
|
|
70
|
+
const prdDir = path.join(projectDir, 'docs/prd');
|
|
71
|
+
const prdFiles = await this.findFiles(prdDir, /\.md$/);
|
|
72
|
+
|
|
73
|
+
if (prdFiles.length > 0) {
|
|
74
|
+
const content = await fs.readFile(prdFiles[0], 'utf-8');
|
|
75
|
+
|
|
76
|
+
const nameMatch = content.match(/\*\*Project\*\*:\s*(.*)/i);
|
|
77
|
+
if (nameMatch) context.name = nameMatch[1].trim();
|
|
78
|
+
|
|
79
|
+
const versionMatch = content.match(/\*\*Version\*\*:\s*(.*)/i);
|
|
80
|
+
if (versionMatch) context.version = versionMatch[1].trim();
|
|
81
|
+
|
|
82
|
+
const summary = this.extractSection(content, 'Executive Summary');
|
|
83
|
+
if (summary) context.overview += (context.overview ? '\n\n' : '') + summary;
|
|
84
|
+
}
|
|
85
|
+
return context;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
static async extractFromOpenSpec(projectDir, context) {
|
|
89
|
+
const specsDir = path.join(projectDir, 'specs');
|
|
90
|
+
const openspecDir = path.join(projectDir, 'openspec');
|
|
91
|
+
|
|
92
|
+
const specFiles = [
|
|
93
|
+
...(await this.findFiles(specsDir, /\.md$/)),
|
|
94
|
+
...(await this.findFiles(openspecDir, /\.md$/))
|
|
95
|
+
];
|
|
96
|
+
|
|
97
|
+
for (const file of specFiles) {
|
|
98
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
99
|
+
|
|
100
|
+
const purpose = this.extractSection(content, 'Purpose');
|
|
101
|
+
if (purpose) context.overview += (context.overview ? '\n\n' : '') + purpose;
|
|
102
|
+
|
|
103
|
+
const changes = this.extractSection(content, 'Proposed Changes');
|
|
104
|
+
if (changes) context.proposedChanges += (context.proposedChanges ? '\n\n' : '') + changes;
|
|
105
|
+
}
|
|
106
|
+
return context;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
static async extractFromSpecKit(projectDir, context) {
|
|
110
|
+
const specFile = path.join(projectDir, 'specification.md');
|
|
111
|
+
if (await fs.pathExists(specFile)) {
|
|
112
|
+
const content = await fs.readFile(specFile, 'utf-8');
|
|
113
|
+
|
|
114
|
+
const techStack = this.extractSection(content, 'Tech Stack');
|
|
115
|
+
if (techStack) context.techStack += (context.techStack ? '\n\n' : '') + techStack;
|
|
116
|
+
|
|
117
|
+
const architecture = this.extractSection(content, 'Architecture');
|
|
118
|
+
if (architecture) context.architecture += (context.architecture ? '\n\n' : '') + architecture;
|
|
119
|
+
}
|
|
120
|
+
return context;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Extracts content under a specific heading.
|
|
125
|
+
* Supports both # and ## and emojis.
|
|
126
|
+
*/
|
|
127
|
+
static extractSection(content, headingName) {
|
|
128
|
+
const lines = content.split(/\r?\n/);
|
|
129
|
+
let inSection = false;
|
|
130
|
+
let sectionContent = [];
|
|
131
|
+
|
|
132
|
+
// Use a more flexible regex for headings
|
|
133
|
+
// Matches lines starting with #, followed by optional whitespace/emojis, then the heading name
|
|
134
|
+
const headingRegex = new RegExp(`^#+\\s+.*${headingName}.*`, 'i');
|
|
135
|
+
|
|
136
|
+
for (let line of lines) {
|
|
137
|
+
const trimmedLine = line.trim();
|
|
138
|
+
|
|
139
|
+
if (headingRegex.test(trimmedLine)) {
|
|
140
|
+
inSection = true;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (inSection && trimmedLine.startsWith('#')) {
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (inSection) {
|
|
149
|
+
sectionContent.push(line);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return sectionContent.join('\n').trim();
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = ContextExtractor;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Utility to detect the presence of different development frameworks
|
|
6
|
+
* in a project directory.
|
|
7
|
+
*/
|
|
8
|
+
class FrameworkDetector {
|
|
9
|
+
/**
|
|
10
|
+
* Detects which frameworks are present in the given project directory.
|
|
11
|
+
* @param {string} projectDir - The project directory to scan.
|
|
12
|
+
* @returns {Promise<string[]>} Array of detected framework IDs ('adf', 'bmad', 'openspec', 'spec-kit').
|
|
13
|
+
*/
|
|
14
|
+
static async detect(projectDir) {
|
|
15
|
+
const detected = [];
|
|
16
|
+
|
|
17
|
+
// 1. Detect .adf
|
|
18
|
+
if (await fs.pathExists(path.join(projectDir, '.adf'))) {
|
|
19
|
+
detected.push('adf');
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// 2. Detect BMAD
|
|
23
|
+
// Indicators: docs/prd, docs/architecture, stories/
|
|
24
|
+
if (
|
|
25
|
+
await fs.pathExists(path.join(projectDir, 'docs/prd')) ||
|
|
26
|
+
await fs.pathExists(path.join(projectDir, 'docs/architecture')) ||
|
|
27
|
+
await fs.pathExists(path.join(projectDir, 'stories'))
|
|
28
|
+
) {
|
|
29
|
+
detected.push('bmad');
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// 3. Detect OpenSpec
|
|
33
|
+
// Indicators: specs/, openspec/
|
|
34
|
+
if (
|
|
35
|
+
await fs.pathExists(path.join(projectDir, 'specs')) ||
|
|
36
|
+
await fs.pathExists(path.join(projectDir, 'openspec'))
|
|
37
|
+
) {
|
|
38
|
+
detected.push('openspec');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// 4. Detect Spec-Kit
|
|
42
|
+
// Indicators: constitution.md, specification.md
|
|
43
|
+
if (
|
|
44
|
+
await fs.pathExists(path.join(projectDir, 'constitution.md')) ||
|
|
45
|
+
await fs.pathExists(path.join(projectDir, 'specification.md'))
|
|
46
|
+
) {
|
|
47
|
+
detected.push('spec-kit');
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return detected;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = FrameworkDetector;
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tool Feature Registry
|
|
3
|
+
* Centralized database of supported AI tools and their integration features.
|
|
4
|
+
*/
|
|
5
|
+
class ToolFeatureRegistry {
|
|
6
|
+
static get REGISTRY() {
|
|
7
|
+
return {
|
|
8
|
+
kiro: {
|
|
9
|
+
name: 'Kiro',
|
|
10
|
+
status: 'supported',
|
|
11
|
+
configFiles: ['.kiro/product.md', '.kiro/technical.md'],
|
|
12
|
+
features: ['Product Context', 'Technical Guidelines', 'TDD Support']
|
|
13
|
+
},
|
|
14
|
+
trae: {
|
|
15
|
+
name: 'Trae',
|
|
16
|
+
status: 'supported',
|
|
17
|
+
configFiles: ['.trae/config.json'],
|
|
18
|
+
features: ['Project Structure', 'Framework Identity']
|
|
19
|
+
},
|
|
20
|
+
'codex-cli': {
|
|
21
|
+
name: 'Codex CLI',
|
|
22
|
+
status: 'supported',
|
|
23
|
+
configFiles: ['.codex/config.toml', '.codex/instructions.md'],
|
|
24
|
+
features: ['TOML Config', 'Custom Instructions']
|
|
25
|
+
},
|
|
26
|
+
windsurf: {
|
|
27
|
+
name: 'Windsurf',
|
|
28
|
+
status: 'supported',
|
|
29
|
+
configFiles: ['.windsurfrules', '.windsurf/rules/'],
|
|
30
|
+
features: ['Legacy Rules', 'Modern Rules', 'Workflows']
|
|
31
|
+
},
|
|
32
|
+
cursor: {
|
|
33
|
+
name: 'Cursor',
|
|
34
|
+
status: 'supported',
|
|
35
|
+
configFiles: ['.cursorrules', '.cursor/rules/'],
|
|
36
|
+
features: ['Context Rules']
|
|
37
|
+
},
|
|
38
|
+
vscode: {
|
|
39
|
+
name: 'VS Code',
|
|
40
|
+
status: 'supported',
|
|
41
|
+
configFiles: ['.vscode/settings.json', '.github/copilot-instructions.md'],
|
|
42
|
+
features: ['Copilot Instructions', 'Custom Chat Modes']
|
|
43
|
+
},
|
|
44
|
+
zed: {
|
|
45
|
+
name: 'Zed Editor',
|
|
46
|
+
status: 'supported',
|
|
47
|
+
configFiles: ['.zed/settings.json', '.zed/keymap.json'],
|
|
48
|
+
features: ['MCP Config', 'Keymaps']
|
|
49
|
+
},
|
|
50
|
+
antigravity: {
|
|
51
|
+
name: 'Antigravity',
|
|
52
|
+
status: 'supported',
|
|
53
|
+
configFiles: ['.antigravity/agents.yaml'],
|
|
54
|
+
features: ['YAML Agents']
|
|
55
|
+
},
|
|
56
|
+
'claude-code': {
|
|
57
|
+
name: 'Claude Code',
|
|
58
|
+
status: 'supported',
|
|
59
|
+
configFiles: ['.framework/agents/'],
|
|
60
|
+
features: ['Direct Agent Access']
|
|
61
|
+
},
|
|
62
|
+
opencode: {
|
|
63
|
+
name: 'OpenCode CLI',
|
|
64
|
+
status: 'supported',
|
|
65
|
+
configFiles: ['opencode.json'],
|
|
66
|
+
features: ['JSON Config']
|
|
67
|
+
},
|
|
68
|
+
'gemini-cli': {
|
|
69
|
+
name: 'Gemini CLI',
|
|
70
|
+
status: 'supported',
|
|
71
|
+
configFiles: ['GEMINI.md'],
|
|
72
|
+
features: ['Markdown Context']
|
|
73
|
+
},
|
|
74
|
+
deepagent: {
|
|
75
|
+
name: 'DeepAgent (Abacus.ai)',
|
|
76
|
+
status: 'supported',
|
|
77
|
+
configFiles: ['.deepagent/'],
|
|
78
|
+
features: ['Full Agent Suite']
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
static listTools() {
|
|
84
|
+
return Object.keys(this.REGISTRY);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
static getFeatures(toolId) {
|
|
88
|
+
const tool = this.REGISTRY[toolId];
|
|
89
|
+
return tool ? { supported: true, ...tool } : { supported: false };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
static audit() {
|
|
93
|
+
const tools = this.REGISTRY;
|
|
94
|
+
return {
|
|
95
|
+
totalCount: Object.keys(tools).length,
|
|
96
|
+
timestamp: new Date().toISOString(),
|
|
97
|
+
tools
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
module.exports = ToolFeatureRegistry;
|
package/package.json
CHANGED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
const AIGapAnalyzer = require('../lib/analysis/ai-gap-analyzer');
|
|
2
|
+
|
|
3
|
+
describe('AI Gap Analyzer', () => {
|
|
4
|
+
let mockAIClient;
|
|
5
|
+
|
|
6
|
+
beforeEach(() => {
|
|
7
|
+
mockAIClient = {
|
|
8
|
+
sendMessage: jest.fn()
|
|
9
|
+
};
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
test('should parse gaps from AI response', async () => {
|
|
13
|
+
const aiResponse = {
|
|
14
|
+
content: `Here is the analysis of knowledge gaps:
|
|
15
|
+
- [GAP] Authentication flow is mentioned but not defined
|
|
16
|
+
- [GAP] No mention of database schema for users
|
|
17
|
+
- [GAP] API error handling strategy is missing`
|
|
18
|
+
};
|
|
19
|
+
mockAIClient.sendMessage.mockResolvedValue(aiResponse);
|
|
20
|
+
|
|
21
|
+
const analyzer = new AIGapAnalyzer(mockAIClient);
|
|
22
|
+
const context = { overview: 'We are building a login system.' };
|
|
23
|
+
const gaps = await analyzer.analyze(context);
|
|
24
|
+
|
|
25
|
+
expect(gaps).toContain('Authentication flow is mentioned but not defined');
|
|
26
|
+
expect(gaps).toContain('No mention of database schema for users');
|
|
27
|
+
expect(gaps).toContain('API error handling strategy is missing');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('should handle empty or invalid AI response', async () => {
|
|
31
|
+
mockAIClient.sendMessage.mockResolvedValue({ content: 'No gaps found.' });
|
|
32
|
+
|
|
33
|
+
const analyzer = new AIGapAnalyzer(mockAIClient);
|
|
34
|
+
const gaps = await analyzer.analyze({ overview: 'Complete' });
|
|
35
|
+
|
|
36
|
+
expect(gaps).toEqual([]);
|
|
37
|
+
});
|
|
38
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const CodexCLIGenerator = require('../lib/generators/codex-cli-generator');
|
|
4
|
+
|
|
5
|
+
describe('Codex CLI Generator', () => {
|
|
6
|
+
const tempDir = path.join(__dirname, 'temp-codex-test');
|
|
7
|
+
const sessionPath = path.join(tempDir, '.adf/sessions/test-session');
|
|
8
|
+
|
|
9
|
+
beforeEach(async () => {
|
|
10
|
+
await fs.ensureDir(path.join(sessionPath, 'outputs'));
|
|
11
|
+
await fs.writeJson(path.join(sessionPath, '_metadata.json'), { projectName: 'Test Project' });
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
afterEach(async () => {
|
|
15
|
+
await fs.remove(tempDir);
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('should generate config.toml and instructions.md', async () => {
|
|
19
|
+
const generator = new CodexCLIGenerator(sessionPath, tempDir, 'balanced');
|
|
20
|
+
await generator.generate();
|
|
21
|
+
|
|
22
|
+
expect(await fs.pathExists(path.join(tempDir, '.codex/config.toml'))).toBe(true);
|
|
23
|
+
expect(await fs.pathExists(path.join(tempDir, '.codex/instructions.md'))).toBe(true);
|
|
24
|
+
|
|
25
|
+
const tomlContent = await fs.readFile(path.join(tempDir, '.codex/config.toml'), 'utf-8');
|
|
26
|
+
expect(tomlContent).toContain('name = "AgentDevFramework"');
|
|
27
|
+
expect(tomlContent).toContain('project = "Test Project"');
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const ContextExtractor = require('../lib/utils/context-extractor');
|
|
4
|
+
|
|
5
|
+
describe('Context Extractor', () => {
|
|
6
|
+
const tempDir = path.join(__dirname, 'temp-extractor-test');
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
await fs.ensureDir(tempDir);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
await fs.remove(tempDir);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('should extract metadata from BMAD PRD', async () => {
|
|
17
|
+
const prdContent = `# Product Requirements Document (PRD)
|
|
18
|
+
|
|
19
|
+
**Project**: Test Project
|
|
20
|
+
**Version**: 2.1
|
|
21
|
+
**Date**: 2026-01-12
|
|
22
|
+
|
|
23
|
+
## Executive Summary
|
|
24
|
+
|
|
25
|
+
This is a summary of the test project.
|
|
26
|
+
`;
|
|
27
|
+
await fs.ensureDir(path.join(tempDir, 'docs/prd'));
|
|
28
|
+
await fs.writeFile(path.join(tempDir, 'docs/prd/prd.md'), prdContent);
|
|
29
|
+
|
|
30
|
+
const context = await ContextExtractor.extract(tempDir, ['bmad']);
|
|
31
|
+
expect(context.name).toBe('Test Project');
|
|
32
|
+
expect(context.version).toBe('2.1');
|
|
33
|
+
expect(context.overview).toContain('summary of the test project');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('should extract purpose from OpenSpec proposal', async () => {
|
|
37
|
+
const proposalContent = `# OpenSpec Change Proposal: New Feature
|
|
38
|
+
|
|
39
|
+
## 🎯 Purpose
|
|
40
|
+
To solve the problem of missing features.
|
|
41
|
+
|
|
42
|
+
## 📝 Proposed Changes
|
|
43
|
+
Add many cool features.
|
|
44
|
+
`;
|
|
45
|
+
await fs.ensureDir(path.join(tempDir, 'specs'));
|
|
46
|
+
await fs.writeFile(path.join(tempDir, 'specs/proposal.md'), proposalContent);
|
|
47
|
+
|
|
48
|
+
const context = await ContextExtractor.extract(tempDir, ['openspec']);
|
|
49
|
+
expect(context.overview).toContain('solve the problem of missing features');
|
|
50
|
+
expect(context.proposedChanges).toContain('Add many cool features');
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test('should extract tech stack and architecture from Spec-Kit', async () => {
|
|
54
|
+
const specContent = `# Specification: App Core
|
|
55
|
+
|
|
56
|
+
## Tech Stack
|
|
57
|
+
- Node.js
|
|
58
|
+
- React
|
|
59
|
+
|
|
60
|
+
## Architecture
|
|
61
|
+
Clean architecture with domain-driven design.
|
|
62
|
+
`;
|
|
63
|
+
await fs.writeFile(path.join(tempDir, 'specification.md'), specContent);
|
|
64
|
+
|
|
65
|
+
const context = await ContextExtractor.extract(tempDir, ['spec-kit']);
|
|
66
|
+
expect(context.techStack).toContain('Node.js');
|
|
67
|
+
expect(context.techStack).toContain('React');
|
|
68
|
+
expect(context.architecture).toContain('Clean architecture');
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { deployToTool } = require('../lib/commands/deploy');
|
|
4
|
+
|
|
5
|
+
describe('Multi-Tool Deployment (Integration)', () => {
|
|
6
|
+
const tempDir = path.join(__dirname, 'temp-deploy-integration');
|
|
7
|
+
const adfDir = path.join(tempDir, '.adf');
|
|
8
|
+
const sessionPath = path.join(adfDir, 'sessions/2026-01-12_augmentation_v1');
|
|
9
|
+
|
|
10
|
+
beforeEach(async () => {
|
|
11
|
+
await fs.ensureDir(path.join(sessionPath, 'outputs'));
|
|
12
|
+
// Mock successful session
|
|
13
|
+
await fs.writeJson(path.join(sessionPath, '_progress.json'), { status: 'completed' });
|
|
14
|
+
await fs.writeJson(path.join(sessionPath, '_metadata.json'), { framework: 'balanced' });
|
|
15
|
+
await fs.writeFile(path.join(sessionPath, 'outputs/specification.md'), '## Architecture\nTest');
|
|
16
|
+
|
|
17
|
+
// Change current directory to tempDir for deploy command
|
|
18
|
+
jest.spyOn(process, 'cwd').mockReturnValue(tempDir);
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
afterEach(async () => {
|
|
22
|
+
await fs.remove(tempDir);
|
|
23
|
+
jest.restoreAllMocks();
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test('should deploy to Kiro, Trae, and Codex CLI', async () => {
|
|
27
|
+
// These should work once we update deploy.js
|
|
28
|
+
await deployToTool('kiro', { silent: true });
|
|
29
|
+
await deployToTool('trae', { silent: true });
|
|
30
|
+
await deployToTool('codex-cli', { silent: true });
|
|
31
|
+
|
|
32
|
+
expect(await fs.pathExists(path.join(tempDir, '.kiro/product.md'))).toBe(true);
|
|
33
|
+
expect(await fs.pathExists(path.join(tempDir, '.trae/config.json'))).toBe(true);
|
|
34
|
+
expect(await fs.pathExists(path.join(tempDir, '.codex/config.toml'))).toBe(true);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const deploy = require('../lib/commands/deploy');
|
|
4
|
+
const { deployToTool } = require('../lib/commands/deploy');
|
|
5
|
+
|
|
6
|
+
// Mock generators
|
|
7
|
+
jest.mock('../lib/generators', () => ({
|
|
8
|
+
generateAgentsMd: jest.fn(),
|
|
9
|
+
generateWindsurf: jest.fn(),
|
|
10
|
+
generateCursor: jest.fn(),
|
|
11
|
+
generateVSCode: jest.fn(),
|
|
12
|
+
generateZed: jest.fn(),
|
|
13
|
+
generateAntigravity: jest.fn(),
|
|
14
|
+
generateOpenCode: jest.fn().mockResolvedValue({ config: 'opencode.json' }),
|
|
15
|
+
generateGeminiCLI: jest.fn(),
|
|
16
|
+
generateDeepAgent: jest.fn()
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
const TEST_DIR = path.join(__dirname, 'test-deploy-opencode');
|
|
20
|
+
const ADF_DIR = path.join(TEST_DIR, '.adf');
|
|
21
|
+
|
|
22
|
+
describe('Deploy Command - OpenCode', () => {
|
|
23
|
+
beforeEach(async () => {
|
|
24
|
+
await fs.remove(TEST_DIR);
|
|
25
|
+
await fs.ensureDir(TEST_DIR);
|
|
26
|
+
await fs.ensureDir(ADF_DIR);
|
|
27
|
+
|
|
28
|
+
// Create mock session
|
|
29
|
+
const sessionDir = path.join(ADF_DIR, 'sessions', 'test-session');
|
|
30
|
+
await fs.ensureDir(path.join(sessionDir, 'outputs'));
|
|
31
|
+
await fs.writeFile(path.join(sessionDir, 'outputs', 'prp.md'), 'test content');
|
|
32
|
+
|
|
33
|
+
// Mock process.cwd
|
|
34
|
+
jest.spyOn(process, 'cwd').mockReturnValue(TEST_DIR);
|
|
35
|
+
// Mock console.log/error to suppress output
|
|
36
|
+
jest.spyOn(console, 'log').mockImplementation(() => {});
|
|
37
|
+
jest.spyOn(console, 'error').mockImplementation(() => {});
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterEach(async () => {
|
|
41
|
+
await fs.remove(TEST_DIR);
|
|
42
|
+
jest.restoreAllMocks();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should deploy to opencode with correct filename', async () => {
|
|
46
|
+
await deployToTool('opencode', { silent: true });
|
|
47
|
+
|
|
48
|
+
// Verify opencode.json was created (by the mock generator,
|
|
49
|
+
// but the deploy command also writes a fallback config if generator fails or for simple tools.
|
|
50
|
+
// In this case, we rely on the generator which we mocked.
|
|
51
|
+
// However, deploy.js writes a fallback/wrapper config at the end.
|
|
52
|
+
|
|
53
|
+
// The key check here is that it didn't crash and tried to write to opencode.json
|
|
54
|
+
const configPath = path.join(TEST_DIR, 'opencode.json');
|
|
55
|
+
expect(await fs.pathExists(configPath)).toBe(true);
|
|
56
|
+
});
|
|
57
|
+
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
const DynamicQuestionGenerator = require('../lib/analysis/dynamic-question-generator');
|
|
2
|
+
|
|
3
|
+
describe('Dynamic Question Generator', () => {
|
|
4
|
+
test('should generate questions from heuristic and AI gaps', () => {
|
|
5
|
+
const heuristicGaps = {
|
|
6
|
+
missingFields: ['architecture'],
|
|
7
|
+
missingQuestions: ['bal-37']
|
|
8
|
+
};
|
|
9
|
+
const aiGaps = [
|
|
10
|
+
'Authentication flow is mentioned but not defined',
|
|
11
|
+
'API error handling strategy is missing'
|
|
12
|
+
];
|
|
13
|
+
|
|
14
|
+
const questions = DynamicQuestionGenerator.generate(heuristicGaps, aiGaps);
|
|
15
|
+
|
|
16
|
+
// Heuristic questions (existing ones)
|
|
17
|
+
expect(questions.some(q => q.id === 'bal-37')).toBe(true);
|
|
18
|
+
|
|
19
|
+
// Dynamic AI questions (new ones)
|
|
20
|
+
expect(questions.some(q => q.text.includes('Authentication flow'))).toBe(true);
|
|
21
|
+
expect(questions.some(q => q.text.includes('API error handling'))).toBe(true);
|
|
22
|
+
expect(questions.every(q => q.phase === 'augmentation')).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('should handle empty gaps', () => {
|
|
26
|
+
const questions = DynamicQuestionGenerator.generate({ missingQuestions: [] }, []);
|
|
27
|
+
expect(questions).toEqual([]);
|
|
28
|
+
});
|
|
29
|
+
});
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const FrameworkDetector = require('../lib/utils/framework-detector');
|
|
4
|
+
|
|
5
|
+
describe('Framework Detector', () => {
|
|
6
|
+
const tempDir = path.join(__dirname, 'temp-detector-test');
|
|
7
|
+
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
await fs.ensureDir(tempDir);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
await fs.remove(tempDir);
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test('should detect .adf project', async () => {
|
|
17
|
+
await fs.ensureDir(path.join(tempDir, '.adf'));
|
|
18
|
+
const detected = await FrameworkDetector.detect(tempDir);
|
|
19
|
+
expect(detected).toContain('adf');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('should detect BMAD project', async () => {
|
|
23
|
+
await fs.ensureDir(path.join(tempDir, 'docs/prd'));
|
|
24
|
+
await fs.ensureDir(path.join(tempDir, 'docs/architecture'));
|
|
25
|
+
const detected = await FrameworkDetector.detect(tempDir);
|
|
26
|
+
expect(detected).toContain('bmad');
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('should detect OpenSpec project', async () => {
|
|
30
|
+
await fs.ensureDir(path.join(tempDir, 'specs'));
|
|
31
|
+
await fs.ensureDir(path.join(tempDir, 'openspec'));
|
|
32
|
+
const detected = await FrameworkDetector.detect(tempDir);
|
|
33
|
+
expect(detected).toContain('openspec');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('should detect Spec-Kit project', async () => {
|
|
37
|
+
await fs.writeFile(path.join(tempDir, 'constitution.md'), '# Constitution');
|
|
38
|
+
await fs.writeFile(path.join(tempDir, 'specification.md'), '# Specification');
|
|
39
|
+
const detected = await FrameworkDetector.detect(tempDir);
|
|
40
|
+
expect(detected).toContain('spec-kit');
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
test('should detect multiple frameworks', async () => {
|
|
44
|
+
await fs.ensureDir(path.join(tempDir, '.adf'));
|
|
45
|
+
await fs.ensureDir(path.join(tempDir, 'specs'));
|
|
46
|
+
const detected = await FrameworkDetector.detect(tempDir);
|
|
47
|
+
expect(detected).toContain('adf');
|
|
48
|
+
expect(detected).toContain('openspec');
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
test('should return empty array for no frameworks', async () => {
|
|
52
|
+
const detected = await FrameworkDetector.detect(tempDir);
|
|
53
|
+
expect(detected).toEqual([]);
|
|
54
|
+
});
|
|
55
|
+
});
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const FrameworkDetector = require('../lib/utils/framework-detector');
|
|
2
|
+
const HeuristicGapAnalyzer = require('../lib/analysis/heuristic-gap-analyzer');
|
|
3
|
+
|
|
4
|
+
describe('Heuristic Gap Analyzer', () => {
|
|
5
|
+
test('should identify missing architecture in balanced framework', () => {
|
|
6
|
+
const context = {
|
|
7
|
+
name: 'Test Project',
|
|
8
|
+
overview: 'Summary',
|
|
9
|
+
techStack: 'Node.js'
|
|
10
|
+
// Missing architecture
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const gaps = HeuristicGapAnalyzer.analyze(context, 'balanced');
|
|
14
|
+
|
|
15
|
+
expect(gaps.missingFields).toContain('architecture');
|
|
16
|
+
expect(gaps.missingQuestions).toContain('bal-37');
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('should identify missing tech stack in rapid framework', () => {
|
|
20
|
+
const context = {
|
|
21
|
+
name: 'Test Project',
|
|
22
|
+
overview: 'Summary'
|
|
23
|
+
// Missing tech stack
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const gaps = HeuristicGapAnalyzer.analyze(context, 'rapid');
|
|
27
|
+
|
|
28
|
+
expect(gaps.missingFields).toContain('techStack');
|
|
29
|
+
expect(gaps.missingQuestions).toContain('prp-8');
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('should find no gaps when context is complete', () => {
|
|
33
|
+
const context = {
|
|
34
|
+
name: 'Test Project',
|
|
35
|
+
overview: 'Summary',
|
|
36
|
+
techStack: 'Node.js',
|
|
37
|
+
architecture: 'Microservices',
|
|
38
|
+
proposedChanges: 'Auth'
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
const gaps = HeuristicGapAnalyzer.analyze(context, 'balanced');
|
|
42
|
+
|
|
43
|
+
expect(gaps.missingFields.length).toBe(0);
|
|
44
|
+
expect(gaps.missingQuestions.length).toBe(0);
|
|
45
|
+
});
|
|
46
|
+
});
|