@iservu-inc/adf-cli 0.3.6 → 0.4.13

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.
Files changed (29) hide show
  1. package/.project/chats/{current → complete}/2025-10-03_AI-PROVIDER-INTEGRATION.md +34 -35
  2. package/.project/chats/complete/2025-10-04_CONFIG-COMMAND.md +503 -0
  3. package/.project/chats/complete/2025-10-04_PHASE-4-1-SMART-FILTERING.md +381 -0
  4. package/.project/chats/current/2025-10-04_PHASE-4-2-COMPLETION-AND-ROADMAP.md +344 -0
  5. package/.project/chats/current/2025-10-04_PHASE-4-2-LEARNING-SYSTEM.md +239 -0
  6. package/.project/chats/current/SESSION-STATUS.md +142 -33
  7. package/.project/docs/PHASE-4-2-LEARNING-SYSTEM.md +881 -0
  8. package/.project/docs/ROADMAP.md +540 -0
  9. package/.project/docs/SMART-FILTERING-SYSTEM.md +385 -0
  10. package/.project/docs/goals/PROJECT-VISION.md +63 -11
  11. package/CHANGELOG.md +125 -1
  12. package/README.md +476 -381
  13. package/lib/analyzers/project-analyzer.js +380 -0
  14. package/lib/commands/config.js +68 -1
  15. package/lib/commands/init.js +3 -22
  16. package/lib/filters/question-filter.js +480 -0
  17. package/lib/frameworks/interviewer.js +208 -4
  18. package/lib/learning/learning-manager.js +447 -0
  19. package/lib/learning/pattern-detector.js +376 -0
  20. package/lib/learning/rule-generator.js +304 -0
  21. package/lib/learning/skip-tracker.js +260 -0
  22. package/lib/learning/storage.js +296 -0
  23. package/package.json +70 -69
  24. package/tests/learning-storage.test.js +184 -0
  25. package/tests/pattern-detector.test.js +297 -0
  26. package/tests/project-analyzer.test.js +221 -0
  27. package/tests/question-filter.test.js +297 -0
  28. package/tests/skip-tracker.test.js +198 -0
  29. /package/.project/chats/{current → complete}/2025-10-03_FRAMEWORK-UPDATE-SYSTEM.md +0 -0
@@ -0,0 +1,380 @@
1
+ const fs = require('fs-extra');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+
5
+ /**
6
+ * Project Analyzer - Analyzes existing project files for context
7
+ *
8
+ * This module provides intelligent project analysis to:
9
+ * - Detect project type (web app, CLI, library, API, etc.)
10
+ * - Identify tech stack and frameworks
11
+ * - Extract project metadata
12
+ * - Provide context for smart question filtering
13
+ */
14
+
15
+ /**
16
+ * Project types that can be detected
17
+ */
18
+ const PROJECT_TYPES = {
19
+ WEB_APP: 'web-app',
20
+ API_SERVER: 'api-server',
21
+ CLI_TOOL: 'cli-tool',
22
+ LIBRARY: 'library',
23
+ MOBILE_APP: 'mobile-app',
24
+ DESKTOP_APP: 'desktop-app',
25
+ FULLSTACK: 'fullstack',
26
+ MICROSERVICE: 'microservice',
27
+ UNKNOWN: 'unknown'
28
+ };
29
+
30
+ /**
31
+ * Analyze project and extract context
32
+ * @param {string} projectPath - Path to project root
33
+ * @returns {Promise<Object>} Project context object
34
+ */
35
+ async function analyzeProject(projectPath) {
36
+ const context = {
37
+ type: PROJECT_TYPES.UNKNOWN,
38
+ subtype: null,
39
+ frameworks: [],
40
+ languages: [],
41
+ dependencies: {},
42
+ devDependencies: {},
43
+ scripts: {},
44
+ hasTests: false,
45
+ hasCI: false,
46
+ hasDocker: false,
47
+ teamSize: 'unknown',
48
+ description: null,
49
+ confidence: 0
50
+ };
51
+
52
+ // Analyze package.json if exists (Node.js project)
53
+ const packageJsonPath = path.join(projectPath, 'package.json');
54
+ if (await fs.pathExists(packageJsonPath)) {
55
+ const packageData = await analyzePackageJson(packageJsonPath);
56
+ Object.assign(context, packageData);
57
+ context.languages.push('JavaScript/TypeScript');
58
+ }
59
+
60
+ // Analyze Python projects
61
+ const requirementsTxt = path.join(projectPath, 'requirements.txt');
62
+ const pipfile = path.join(projectPath, 'Pipfile');
63
+ if (await fs.pathExists(requirementsTxt) || await fs.pathExists(pipfile)) {
64
+ const pythonData = await analyzePythonProject(projectPath);
65
+ Object.assign(context, pythonData);
66
+ context.languages.push('Python');
67
+ }
68
+
69
+ // Analyze file structure
70
+ const fileStructure = await analyzeFileStructure(projectPath);
71
+ context.hasTests = context.hasTests || fileStructure.hasTests; // Preserve if already detected from package.json
72
+ context.hasCI = fileStructure.hasCI;
73
+ context.hasDocker = fileStructure.hasDocker;
74
+
75
+ // Try to read README for description
76
+ const readmePath = await findReadme(projectPath);
77
+ if (readmePath) {
78
+ context.description = await extractDescriptionFromReadme(readmePath);
79
+ }
80
+
81
+ // Detect project type if still unknown
82
+ if (context.type === PROJECT_TYPES.UNKNOWN) {
83
+ context.type = inferProjectType(context);
84
+ }
85
+
86
+ // Calculate confidence score (0-100)
87
+ context.confidence = calculateConfidence(context);
88
+
89
+ return context;
90
+ }
91
+
92
+ /**
93
+ * Analyze package.json for Node.js projects
94
+ */
95
+ async function analyzePackageJson(packageJsonPath) {
96
+ try {
97
+ const packageJson = await fs.readJSON(packageJsonPath);
98
+ const context = {
99
+ dependencies: packageJson.dependencies || {},
100
+ devDependencies: packageJson.devDependencies || {},
101
+ scripts: packageJson.scripts || {},
102
+ frameworks: [],
103
+ type: PROJECT_TYPES.UNKNOWN
104
+ };
105
+
106
+ // Detect frameworks
107
+ const deps = { ...context.dependencies, ...context.devDependencies };
108
+
109
+ // Frontend frameworks
110
+ if (deps.react) context.frameworks.push('React');
111
+ if (deps.vue) context.frameworks.push('Vue');
112
+ if (deps.angular || deps['@angular/core']) context.frameworks.push('Angular');
113
+ if (deps.svelte) context.frameworks.push('Svelte');
114
+ if (deps.next) context.frameworks.push('Next.js');
115
+ if (deps.nuxt) context.frameworks.push('Nuxt');
116
+ if (deps.gatsby) context.frameworks.push('Gatsby');
117
+
118
+ // Backend frameworks
119
+ if (deps.express) context.frameworks.push('Express');
120
+ if (deps.koa) context.frameworks.push('Koa');
121
+ if (deps.fastify) context.frameworks.push('Fastify');
122
+ if (deps.hapi || deps['@hapi/hapi']) context.frameworks.push('Hapi');
123
+ if (deps.nestjs || deps['@nestjs/core']) context.frameworks.push('NestJS');
124
+
125
+ // Testing frameworks
126
+ if (deps.jest || deps.mocha || deps.jasmine || deps.vitest) {
127
+ context.hasTests = true;
128
+ }
129
+
130
+ // Detect project type from package.json
131
+ if (packageJson.bin) {
132
+ context.type = PROJECT_TYPES.CLI_TOOL;
133
+ context.subtype = 'cli';
134
+ } else if (packageJson.main && !packageJson.private) {
135
+ context.type = PROJECT_TYPES.LIBRARY;
136
+ context.subtype = 'npm-package';
137
+ } else if (context.frameworks.some(f => ['React', 'Vue', 'Angular', 'Svelte'].includes(f))) {
138
+ if (context.frameworks.some(f => ['Express', 'NestJS', 'Fastify'].includes(f))) {
139
+ context.type = PROJECT_TYPES.FULLSTACK;
140
+ context.subtype = 'frontend-backend';
141
+ } else {
142
+ context.type = PROJECT_TYPES.WEB_APP;
143
+ context.subtype = 'frontend';
144
+ }
145
+ } else if (context.frameworks.some(f => ['Express', 'NestJS', 'Fastify', 'Koa', 'Hapi'].includes(f))) {
146
+ // Check if it's an API server
147
+ if (deps.cors || packageJson.name?.includes('api') || packageJson.description?.toLowerCase().includes('api')) {
148
+ context.type = PROJECT_TYPES.API_SERVER;
149
+ context.subtype = 'rest-api';
150
+ } else {
151
+ context.type = PROJECT_TYPES.WEB_APP;
152
+ context.subtype = 'backend';
153
+ }
154
+ }
155
+
156
+ context.description = packageJson.description || null;
157
+
158
+ return context;
159
+ } catch (error) {
160
+ console.warn(chalk.yellow(`Warning: Could not parse package.json: ${error.message}`));
161
+ return {};
162
+ }
163
+ }
164
+
165
+ /**
166
+ * Analyze Python project
167
+ */
168
+ async function analyzePythonProject(projectPath) {
169
+ const context = {
170
+ frameworks: [],
171
+ type: PROJECT_TYPES.UNKNOWN
172
+ };
173
+
174
+ // Check for common Python frameworks
175
+ const requirementsTxt = path.join(projectPath, 'requirements.txt');
176
+ if (await fs.pathExists(requirementsTxt)) {
177
+ const requirements = await fs.readFile(requirementsTxt, 'utf-8');
178
+
179
+ if (requirements.includes('django')) context.frameworks.push('Django');
180
+ if (requirements.includes('flask')) context.frameworks.push('Flask');
181
+ if (requirements.includes('fastapi')) context.frameworks.push('FastAPI');
182
+ if (requirements.includes('streamlit')) context.frameworks.push('Streamlit');
183
+
184
+ // Detect type
185
+ if (context.frameworks.some(f => ['Django', 'Flask', 'FastAPI'].includes(f))) {
186
+ context.type = PROJECT_TYPES.WEB_APP;
187
+ context.subtype = 'python-web';
188
+ }
189
+ }
190
+
191
+ return context;
192
+ }
193
+
194
+ /**
195
+ * Analyze file structure for indicators
196
+ */
197
+ async function analyzeFileStructure(projectPath) {
198
+ const structure = {
199
+ hasTests: false,
200
+ hasCI: false,
201
+ hasDocker: false
202
+ };
203
+
204
+ try {
205
+ const files = await fs.readdir(projectPath);
206
+
207
+ // Check for test directories
208
+ if (files.includes('test') || files.includes('tests') || files.includes('__tests__') || files.includes('spec')) {
209
+ structure.hasTests = true;
210
+ }
211
+
212
+ // Check for CI configuration
213
+ if (files.includes('.github') || files.includes('.gitlab-ci.yml') || files.includes('.travis.yml') || files.includes('.circleci')) {
214
+ structure.hasCI = true;
215
+ }
216
+
217
+ // Check for Docker
218
+ if (files.includes('Dockerfile') || files.includes('docker-compose.yml')) {
219
+ structure.hasDocker = true;
220
+ }
221
+ } catch (error) {
222
+ // Silently fail - not critical
223
+ }
224
+
225
+ return structure;
226
+ }
227
+
228
+ /**
229
+ * Find README file (case-insensitive)
230
+ */
231
+ async function findReadme(projectPath) {
232
+ const readmeNames = ['README.md', 'readme.md', 'README.MD', 'README', 'Readme.md'];
233
+
234
+ for (const name of readmeNames) {
235
+ const readmePath = path.join(projectPath, name);
236
+ if (await fs.pathExists(readmePath)) {
237
+ return readmePath;
238
+ }
239
+ }
240
+
241
+ return null;
242
+ }
243
+
244
+ /**
245
+ * Extract description from README (first paragraph)
246
+ */
247
+ async function extractDescriptionFromReadme(readmePath) {
248
+ try {
249
+ const content = await fs.readFile(readmePath, 'utf-8');
250
+
251
+ // Remove title (first # line)
252
+ const lines = content.split('\n');
253
+ let description = '';
254
+ let foundTitle = false;
255
+
256
+ for (const line of lines) {
257
+ const trimmed = line.trim();
258
+
259
+ // Skip title
260
+ if (!foundTitle && trimmed.startsWith('#')) {
261
+ foundTitle = true;
262
+ continue;
263
+ }
264
+
265
+ // Skip empty lines
266
+ if (!trimmed) continue;
267
+
268
+ // Stop at next heading or code block
269
+ if (trimmed.startsWith('#') || trimmed.startsWith('```')) break;
270
+
271
+ // Add to description
272
+ description += trimmed + ' ';
273
+
274
+ // Stop after first paragraph (roughly 200 chars)
275
+ if (description.length > 200) break;
276
+ }
277
+
278
+ return description.trim().substring(0, 300) || null;
279
+ } catch (error) {
280
+ return null;
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Infer project type from context when not explicitly detected
286
+ */
287
+ function inferProjectType(context) {
288
+ // If we have frameworks, make educated guess
289
+ if (context.frameworks.length > 0) {
290
+ const frontend = ['React', 'Vue', 'Angular', 'Svelte', 'Next.js', 'Nuxt', 'Gatsby'];
291
+ const backend = ['Express', 'NestJS', 'Fastify', 'Koa', 'Django', 'Flask', 'FastAPI'];
292
+
293
+ const hasFrontend = context.frameworks.some(f => frontend.includes(f));
294
+ const hasBackend = context.frameworks.some(f => backend.includes(f));
295
+
296
+ if (hasFrontend && hasBackend) return PROJECT_TYPES.FULLSTACK;
297
+ if (hasFrontend) return PROJECT_TYPES.WEB_APP;
298
+ if (hasBackend) return PROJECT_TYPES.API_SERVER;
299
+ }
300
+
301
+ // Check description for hints
302
+ if (context.description) {
303
+ const desc = context.description.toLowerCase();
304
+ if (desc.includes('api') || desc.includes('server')) return PROJECT_TYPES.API_SERVER;
305
+ if (desc.includes('cli') || desc.includes('command-line')) return PROJECT_TYPES.CLI_TOOL;
306
+ if (desc.includes('library') || desc.includes('package')) return PROJECT_TYPES.LIBRARY;
307
+ if (desc.includes('web') || desc.includes('app')) return PROJECT_TYPES.WEB_APP;
308
+ }
309
+
310
+ return PROJECT_TYPES.UNKNOWN;
311
+ }
312
+
313
+ /**
314
+ * Calculate confidence score (0-100) for the analysis
315
+ */
316
+ function calculateConfidence(context) {
317
+ let score = 0;
318
+
319
+ // Type detection (+30)
320
+ if (context.type !== PROJECT_TYPES.UNKNOWN) score += 30;
321
+
322
+ // Framework detection (+20)
323
+ if (context.frameworks.length > 0) score += 20;
324
+
325
+ // Language detection (+15)
326
+ if (context.languages.length > 0) score += 15;
327
+
328
+ // Dependencies (+10)
329
+ if (Object.keys(context.dependencies).length > 0) score += 10;
330
+
331
+ // Description (+15)
332
+ if (context.description) score += 15;
333
+
334
+ // Additional indicators (+10)
335
+ if (context.hasTests) score += 3;
336
+ if (context.hasCI) score += 3;
337
+ if (context.hasDocker) score += 4;
338
+
339
+ return Math.min(score, 100);
340
+ }
341
+
342
+ /**
343
+ * Get human-readable project summary
344
+ */
345
+ function getProjectSummary(context) {
346
+ const parts = [];
347
+
348
+ // Type
349
+ const typeNames = {
350
+ [PROJECT_TYPES.WEB_APP]: 'Web Application',
351
+ [PROJECT_TYPES.API_SERVER]: 'API Server',
352
+ [PROJECT_TYPES.CLI_TOOL]: 'CLI Tool',
353
+ [PROJECT_TYPES.LIBRARY]: 'Library/Package',
354
+ [PROJECT_TYPES.MOBILE_APP]: 'Mobile App',
355
+ [PROJECT_TYPES.DESKTOP_APP]: 'Desktop App',
356
+ [PROJECT_TYPES.FULLSTACK]: 'Full-stack Application',
357
+ [PROJECT_TYPES.MICROSERVICE]: 'Microservice',
358
+ [PROJECT_TYPES.UNKNOWN]: 'Unknown Type'
359
+ };
360
+
361
+ parts.push(typeNames[context.type] || 'Unknown');
362
+
363
+ // Frameworks
364
+ if (context.frameworks.length > 0) {
365
+ parts.push(`using ${context.frameworks.join(', ')}`);
366
+ }
367
+
368
+ // Languages
369
+ if (context.languages.length > 0) {
370
+ parts.push(`(${context.languages.join(', ')})`);
371
+ }
372
+
373
+ return parts.join(' ');
374
+ }
375
+
376
+ module.exports = {
377
+ analyzeProject,
378
+ getProjectSummary,
379
+ PROJECT_TYPES
380
+ };
@@ -3,6 +3,8 @@ const chalk = require('chalk');
3
3
  const fs = require('fs-extra');
4
4
  const path = require('path');
5
5
  const { configureAIProvider, getEnvFilePath, loadEnvFile } = require('../ai/ai-config');
6
+ const LearningManager = require('../learning/learning-manager');
7
+ const { getLearningStats, getLearningConfig } = require('../learning/storage');
6
8
 
7
9
  /**
8
10
  * Configuration categories available in ADF CLI
@@ -12,6 +14,11 @@ const CONFIG_CATEGORIES = {
12
14
  name: 'AI Provider Setup',
13
15
  description: 'Configure AI provider (Anthropic, OpenAI, Google Gemini, OpenRouter)',
14
16
  value: 'ai-provider'
17
+ },
18
+ LEARNING_SYSTEM: {
19
+ name: 'Learning System',
20
+ description: 'Manage interview learning data and preferences',
21
+ value: 'learning'
15
22
  }
16
23
  // Future config categories can be added here:
17
24
  // PROJECT_SETTINGS: { name: 'Project Settings', description: '...', value: 'project' },
@@ -50,6 +57,30 @@ async function isAIConfigured(projectPath = process.cwd()) {
50
57
  return { configured: false };
51
58
  }
52
59
 
60
+ /**
61
+ * Check if Learning System has data
62
+ */
63
+ async function getLearningStatus(projectPath = process.cwd()) {
64
+ try {
65
+ const stats = await getLearningStats(projectPath);
66
+ const config = await getLearningConfig(projectPath);
67
+
68
+ return {
69
+ hasData: stats.totalSessions > 0,
70
+ enabled: config.enabled,
71
+ totalSessions: stats.totalSessions,
72
+ totalPatterns: stats.totalPatterns
73
+ };
74
+ } catch (error) {
75
+ return {
76
+ hasData: false,
77
+ enabled: true,
78
+ totalSessions: 0,
79
+ totalPatterns: 0
80
+ };
81
+ }
82
+ }
83
+
53
84
  /**
54
85
  * Display configuration status for AI provider
55
86
  */
@@ -62,6 +93,21 @@ function displayAIStatus(status) {
62
93
  }
63
94
  }
64
95
 
96
+ /**
97
+ * Display configuration status for Learning System
98
+ */
99
+ function displayLearningStatus(status) {
100
+ if (status.hasData && status.enabled) {
101
+ return `${chalk.green('✓ Active')} ${chalk.gray(`(${status.totalSessions} sessions, ${status.totalPatterns} patterns)`)}`;
102
+ } else if (status.hasData && !status.enabled) {
103
+ return `${chalk.yellow('○ Disabled')} ${chalk.gray(`(${status.totalSessions} sessions)`)}`;
104
+ } else if (!status.hasData && status.enabled) {
105
+ return chalk.gray('○ No data yet');
106
+ } else {
107
+ return chalk.gray('○ Not configured');
108
+ }
109
+ }
110
+
65
111
  /**
66
112
  * Main config command
67
113
  */
@@ -70,8 +116,9 @@ async function config() {
70
116
 
71
117
  const cwd = process.cwd();
72
118
 
73
- // Check AI configuration status
119
+ // Check configuration status for all categories
74
120
  const aiStatus = await isAIConfigured(cwd);
121
+ const learningStatus = await getLearningStatus(cwd);
75
122
 
76
123
  // Build choices with status indicators
77
124
  const choices = [
@@ -80,6 +127,11 @@ async function config() {
80
127
  value: CONFIG_CATEGORIES.AI_PROVIDER.value,
81
128
  short: CONFIG_CATEGORIES.AI_PROVIDER.name
82
129
  },
130
+ {
131
+ name: `${CONFIG_CATEGORIES.LEARNING_SYSTEM.name} - ${displayLearningStatus(learningStatus)}`,
132
+ value: CONFIG_CATEGORIES.LEARNING_SYSTEM.value,
133
+ short: CONFIG_CATEGORIES.LEARNING_SYSTEM.name
134
+ },
83
135
  new inquirer.Separator(),
84
136
  {
85
137
  name: chalk.gray('← Back'),
@@ -108,6 +160,10 @@ async function config() {
108
160
  await configureAIProviderCategory(cwd, aiStatus);
109
161
  break;
110
162
 
163
+ case 'learning':
164
+ await configureLearningCategory(cwd, learningStatus);
165
+ break;
166
+
111
167
  // Future categories will be handled here
112
168
  default:
113
169
  console.log(chalk.red('\n❌ Configuration category not implemented yet.\n'));
@@ -151,4 +207,15 @@ async function configureAIProviderCategory(cwd, aiStatus) {
151
207
  }
152
208
  }
153
209
 
210
+ /**
211
+ * Configure Learning System category
212
+ */
213
+ async function configureLearningCategory(cwd, learningStatus) {
214
+ console.log(chalk.gray('\n' + '─'.repeat(60) + '\n'));
215
+
216
+ // Create and show learning manager menu
217
+ const learningManager = new LearningManager(cwd);
218
+ await learningManager.showMenu();
219
+ }
220
+
154
221
  module.exports = config;
@@ -105,28 +105,9 @@ async function init(options) {
105
105
  // Create .adf directory
106
106
  await fs.ensureDir(adfDir);
107
107
 
108
- // Configure AI Provider (OPTIONAL - can be done later with 'adf config')
109
- let aiConfig = null;
110
-
111
- const { configureAI } = await inquirer.prompt([
112
- {
113
- type: 'confirm',
114
- name: 'configureAI',
115
- message: 'Configure AI provider now? (Enables intelligent follow-up questions)',
116
- default: true
117
- }
118
- ]);
119
-
120
- if (configureAI) {
121
- aiConfig = await configureAIProvider(cwd);
122
- } else {
123
- console.log(chalk.yellow('\n💡 You can configure AI later by running: adf config\n'));
124
- }
125
-
126
- // Start interview (with or without AI)
127
- console.log(chalk.gray('\n' + '━'.repeat(60)) + '\n');
128
-
129
- const interviewer = new Interviewer(workflow, cwd, null, aiConfig);
108
+ // AI will be configured as first block in the interview
109
+ // Start interview
110
+ const interviewer = new Interviewer(workflow, cwd, null, null);
130
111
  const sessionPath = await interviewer.start();
131
112
 
132
113
  // Show completion message