opencode-conductor-cdd-plugin 1.0.0-beta.18 → 1.0.0-beta.20
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/README.md +19 -3
- package/dist/prompts/cdd/setup.json +2 -2
- package/dist/prompts/cdd/setup.test.js +40 -118
- package/dist/prompts/cdd/setup.test.ts +40 -143
- package/dist/utils/codebaseAnalysis.d.ts +61 -0
- package/dist/utils/codebaseAnalysis.js +429 -0
- package/dist/utils/codebaseAnalysis.test.d.ts +1 -0
- package/dist/utils/codebaseAnalysis.test.js +556 -0
- package/dist/utils/configDetection.d.ts +12 -0
- package/dist/utils/configDetection.js +23 -9
- package/dist/utils/configDetection.test.js +204 -7
- package/dist/utils/documentGeneration.d.ts +97 -0
- package/dist/utils/documentGeneration.js +301 -0
- package/dist/utils/documentGeneration.test.d.ts +1 -0
- package/dist/utils/documentGeneration.test.js +380 -0
- package/dist/utils/interactiveMenu.d.ts +56 -0
- package/dist/utils/interactiveMenu.js +144 -0
- package/dist/utils/interactiveMenu.test.d.ts +1 -0
- package/dist/utils/interactiveMenu.test.js +231 -0
- package/dist/utils/interactiveSetup.d.ts +43 -0
- package/dist/utils/interactiveSetup.js +131 -0
- package/dist/utils/interactiveSetup.test.d.ts +1 -0
- package/dist/utils/interactiveSetup.test.js +124 -0
- package/dist/utils/projectMaturity.d.ts +53 -0
- package/dist/utils/projectMaturity.js +179 -0
- package/dist/utils/projectMaturity.test.d.ts +1 -0
- package/dist/utils/projectMaturity.test.js +298 -0
- package/dist/utils/questionGenerator.d.ts +51 -0
- package/dist/utils/questionGenerator.js +535 -0
- package/dist/utils/questionGenerator.test.d.ts +1 -0
- package/dist/utils/questionGenerator.test.js +328 -0
- package/dist/utils/setupIntegration.d.ts +72 -0
- package/dist/utils/setupIntegration.js +179 -0
- package/dist/utils/setupIntegration.test.d.ts +1 -0
- package/dist/utils/setupIntegration.test.js +344 -0
- package/dist/utils/synergyState.test.js +17 -3
- package/package.json +2 -1
|
@@ -0,0 +1,380 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { generateDocument, presentQuestionsSequentially, draftDocumentFromAnswers, saveDocumentState, } from './documentGeneration.js';
|
|
3
|
+
describe('documentGeneration', () => {
|
|
4
|
+
const mockAnalysis = {
|
|
5
|
+
languages: {
|
|
6
|
+
'TypeScript': 60,
|
|
7
|
+
'JavaScript': 40,
|
|
8
|
+
},
|
|
9
|
+
frameworks: {
|
|
10
|
+
frontend: ['React', 'Next.js'],
|
|
11
|
+
backend: ['Express'],
|
|
12
|
+
},
|
|
13
|
+
databases: ['PostgreSQL'],
|
|
14
|
+
manifests: [{
|
|
15
|
+
type: 'package.json',
|
|
16
|
+
path: '/package.json',
|
|
17
|
+
metadata: { name: 'test-project', version: '1.0.0' },
|
|
18
|
+
dependencies: { 'react': '^18.0.0' },
|
|
19
|
+
}],
|
|
20
|
+
architecture: ['MVC'],
|
|
21
|
+
projectGoal: 'E-commerce platform',
|
|
22
|
+
};
|
|
23
|
+
const mockQuestions = [
|
|
24
|
+
{
|
|
25
|
+
id: 'product-1',
|
|
26
|
+
text: 'What is the primary purpose?',
|
|
27
|
+
type: 'exclusive',
|
|
28
|
+
section: 'product',
|
|
29
|
+
options: ['E-commerce', 'Social media', 'Dashboard', 'Custom', 'Auto-generate'],
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: 'product-2',
|
|
33
|
+
text: 'Who are the target users?',
|
|
34
|
+
type: 'exclusive',
|
|
35
|
+
section: 'product',
|
|
36
|
+
options: ['Developers', 'End users', 'Businesses', 'Custom', 'Auto-generate'],
|
|
37
|
+
},
|
|
38
|
+
];
|
|
39
|
+
describe('presentQuestionsSequentially', () => {
|
|
40
|
+
it('should present questions one at a time up to max 5', async () => {
|
|
41
|
+
const questions = Array.from({ length: 7 }, (_, i) => ({
|
|
42
|
+
id: `q-${i}`,
|
|
43
|
+
text: `Question ${i}`,
|
|
44
|
+
type: 'exclusive',
|
|
45
|
+
section: 'product',
|
|
46
|
+
options: ['A', 'B', 'C', 'D', 'E'],
|
|
47
|
+
}));
|
|
48
|
+
const mockResponder = vi.fn()
|
|
49
|
+
.mockResolvedValueOnce(['A'])
|
|
50
|
+
.mockResolvedValueOnce(['B'])
|
|
51
|
+
.mockResolvedValueOnce(['C'])
|
|
52
|
+
.mockResolvedValueOnce(['A'])
|
|
53
|
+
.mockResolvedValueOnce(['B']);
|
|
54
|
+
const session = await presentQuestionsSequentially(questions, mockResponder, { maxQuestions: 5 });
|
|
55
|
+
expect(session.questionsAsked).toHaveLength(5);
|
|
56
|
+
expect(session.answers).toHaveLength(5);
|
|
57
|
+
expect(mockResponder).toHaveBeenCalledTimes(5);
|
|
58
|
+
});
|
|
59
|
+
it('should stop early if user selects option E (auto-generate)', async () => {
|
|
60
|
+
const mockResponder = vi.fn()
|
|
61
|
+
.mockResolvedValueOnce(['A'])
|
|
62
|
+
.mockResolvedValueOnce(['E']); // Option E selected
|
|
63
|
+
const session = await presentQuestionsSequentially(mockQuestions, mockResponder);
|
|
64
|
+
expect(session.questionsAsked).toHaveLength(2);
|
|
65
|
+
expect(session.answers).toHaveLength(2);
|
|
66
|
+
expect(session.autoGenerateRequested).toBe(true);
|
|
67
|
+
expect(session.autoGenerateAtQuestion).toBe(1); // 0-based index
|
|
68
|
+
});
|
|
69
|
+
it('should handle option D (custom input) by prompting for text', async () => {
|
|
70
|
+
const mockResponder = vi.fn()
|
|
71
|
+
.mockResolvedValueOnce(['D']); // Option D selected
|
|
72
|
+
const mockCustomInputPrompt = vi.fn()
|
|
73
|
+
.mockResolvedValueOnce('Custom purpose text');
|
|
74
|
+
const session = await presentQuestionsSequentially(mockQuestions.slice(0, 1), mockResponder, { customInputPrompt: mockCustomInputPrompt });
|
|
75
|
+
expect(session.answers[0].selections).toEqual(['D']);
|
|
76
|
+
expect(session.answers[0].customText).toBe('Custom purpose text');
|
|
77
|
+
expect(mockCustomInputPrompt).toHaveBeenCalledTimes(1);
|
|
78
|
+
});
|
|
79
|
+
it('should handle additive questions with multiple selections', async () => {
|
|
80
|
+
const additiveQuestion = {
|
|
81
|
+
id: 'features-1',
|
|
82
|
+
text: 'What features are needed?',
|
|
83
|
+
type: 'additive',
|
|
84
|
+
section: 'product',
|
|
85
|
+
options: ['Auth', 'Dashboard', 'API', 'Custom', 'Auto-generate'],
|
|
86
|
+
};
|
|
87
|
+
const mockResponder = vi.fn()
|
|
88
|
+
.mockResolvedValueOnce(['A', 'B', 'C']); // Multiple selections
|
|
89
|
+
const session = await presentQuestionsSequentially([additiveQuestion], mockResponder);
|
|
90
|
+
expect(session.answers[0].selections).toEqual(['A', 'B', 'C']);
|
|
91
|
+
expect(session.answers[0].selectedOptions).toEqual(['Auth', 'Dashboard', 'API']);
|
|
92
|
+
});
|
|
93
|
+
it('should reject invalid selections and re-prompt', async () => {
|
|
94
|
+
const mockResponder = vi.fn()
|
|
95
|
+
.mockResolvedValueOnce(['F']) // Invalid
|
|
96
|
+
.mockResolvedValueOnce(['A']); // Valid
|
|
97
|
+
const session = await presentQuestionsSequentially(mockQuestions.slice(0, 1), mockResponder);
|
|
98
|
+
expect(mockResponder).toHaveBeenCalledTimes(2);
|
|
99
|
+
expect(session.answers[0].selections).toEqual(['A']);
|
|
100
|
+
});
|
|
101
|
+
it('should track question history and timestamps', async () => {
|
|
102
|
+
const mockResponder = vi.fn()
|
|
103
|
+
.mockResolvedValueOnce(['A'])
|
|
104
|
+
.mockResolvedValueOnce(['B']);
|
|
105
|
+
const session = await presentQuestionsSequentially(mockQuestions, mockResponder);
|
|
106
|
+
expect(session.questionsAsked).toHaveLength(2);
|
|
107
|
+
expect(session.answers[0].timestamp).toBeDefined();
|
|
108
|
+
expect(session.answers[1].timestamp).toBeDefined();
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
describe('draftDocumentFromAnswers', () => {
|
|
112
|
+
it('should generate document content from selected answers only', () => {
|
|
113
|
+
const session = {
|
|
114
|
+
section: 'product',
|
|
115
|
+
questionsAsked: mockQuestions.slice(0, 2),
|
|
116
|
+
answers: [
|
|
117
|
+
{
|
|
118
|
+
questionId: 'product-1',
|
|
119
|
+
selections: ['A'],
|
|
120
|
+
selectedOptions: ['E-commerce'],
|
|
121
|
+
timestamp: new Date().toISOString(),
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
questionId: 'product-2',
|
|
125
|
+
selections: ['B'],
|
|
126
|
+
selectedOptions: ['End users'],
|
|
127
|
+
timestamp: new Date().toISOString(),
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
autoGenerateRequested: false,
|
|
131
|
+
startTime: new Date().toISOString(),
|
|
132
|
+
endTime: new Date().toISOString(),
|
|
133
|
+
};
|
|
134
|
+
const draft = draftDocumentFromAnswers(session, mockAnalysis);
|
|
135
|
+
expect(draft.content).toContain('E-commerce');
|
|
136
|
+
expect(draft.content).toContain('End users');
|
|
137
|
+
expect(draft.content).not.toContain('Social media'); // Unselected option
|
|
138
|
+
expect(draft.content).not.toContain('Developers'); // Unselected option
|
|
139
|
+
});
|
|
140
|
+
it('should ignore unselected options from questions', () => {
|
|
141
|
+
const session = {
|
|
142
|
+
section: 'product',
|
|
143
|
+
questionsAsked: mockQuestions.slice(0, 1),
|
|
144
|
+
answers: [
|
|
145
|
+
{
|
|
146
|
+
questionId: 'product-1',
|
|
147
|
+
selections: ['A'],
|
|
148
|
+
selectedOptions: ['E-commerce'],
|
|
149
|
+
timestamp: new Date().toISOString(),
|
|
150
|
+
},
|
|
151
|
+
],
|
|
152
|
+
autoGenerateRequested: false,
|
|
153
|
+
startTime: new Date().toISOString(),
|
|
154
|
+
endTime: new Date().toISOString(),
|
|
155
|
+
};
|
|
156
|
+
const draft = draftDocumentFromAnswers(session, mockAnalysis);
|
|
157
|
+
// Should NOT contain unselected options B, C
|
|
158
|
+
expect(draft.content).not.toContain('Social media');
|
|
159
|
+
expect(draft.content).not.toContain('Dashboard');
|
|
160
|
+
// Should NOT contain the option letters
|
|
161
|
+
expect(draft.content).not.toMatch(/\bA\)/);
|
|
162
|
+
expect(draft.content).not.toMatch(/\bB\)/);
|
|
163
|
+
expect(draft.content).not.toMatch(/\bC\)/);
|
|
164
|
+
});
|
|
165
|
+
it('should incorporate custom text from option D', () => {
|
|
166
|
+
const session = {
|
|
167
|
+
section: 'product',
|
|
168
|
+
questionsAsked: mockQuestions.slice(0, 1),
|
|
169
|
+
answers: [
|
|
170
|
+
{
|
|
171
|
+
questionId: 'product-1',
|
|
172
|
+
selections: ['D'],
|
|
173
|
+
selectedOptions: ['Custom'],
|
|
174
|
+
customText: 'A platform for managing IoT devices',
|
|
175
|
+
timestamp: new Date().toISOString(),
|
|
176
|
+
},
|
|
177
|
+
],
|
|
178
|
+
autoGenerateRequested: false,
|
|
179
|
+
startTime: new Date().toISOString(),
|
|
180
|
+
endTime: new Date().toISOString(),
|
|
181
|
+
};
|
|
182
|
+
const draft = draftDocumentFromAnswers(session, mockAnalysis);
|
|
183
|
+
expect(draft.content).toContain('A platform for managing IoT devices');
|
|
184
|
+
});
|
|
185
|
+
it('should infer additional content when auto-generate was requested', () => {
|
|
186
|
+
const session = {
|
|
187
|
+
section: 'product',
|
|
188
|
+
questionsAsked: mockQuestions.slice(0, 1),
|
|
189
|
+
answers: [
|
|
190
|
+
{
|
|
191
|
+
questionId: 'product-1',
|
|
192
|
+
selections: ['A'],
|
|
193
|
+
selectedOptions: ['E-commerce'],
|
|
194
|
+
timestamp: new Date().toISOString(),
|
|
195
|
+
},
|
|
196
|
+
],
|
|
197
|
+
autoGenerateRequested: true,
|
|
198
|
+
autoGenerateAtQuestion: 0,
|
|
199
|
+
startTime: new Date().toISOString(),
|
|
200
|
+
endTime: new Date().toISOString(),
|
|
201
|
+
};
|
|
202
|
+
const draft = draftDocumentFromAnswers(session, mockAnalysis);
|
|
203
|
+
expect(draft.content).toContain('E-commerce');
|
|
204
|
+
// Should infer additional sections based on context
|
|
205
|
+
expect(draft.content.length).toBeGreaterThan(100);
|
|
206
|
+
expect(draft.inferredFromContext).toBe(true);
|
|
207
|
+
});
|
|
208
|
+
it('should use codebase analysis to enhance brownfield documents', () => {
|
|
209
|
+
const session = {
|
|
210
|
+
section: 'tech-stack',
|
|
211
|
+
questionsAsked: [],
|
|
212
|
+
answers: [],
|
|
213
|
+
autoGenerateRequested: true,
|
|
214
|
+
startTime: new Date().toISOString(),
|
|
215
|
+
endTime: new Date().toISOString(),
|
|
216
|
+
};
|
|
217
|
+
const draft = draftDocumentFromAnswers(session, mockAnalysis);
|
|
218
|
+
expect(draft.content).toContain('TypeScript');
|
|
219
|
+
expect(draft.content).toContain('React');
|
|
220
|
+
expect(draft.content).toContain('Next.js');
|
|
221
|
+
expect(draft.content).toContain('PostgreSQL');
|
|
222
|
+
});
|
|
223
|
+
it('should generate comprehensive content for greenfield projects', () => {
|
|
224
|
+
const greenfieldAnalysis = {
|
|
225
|
+
languages: {},
|
|
226
|
+
frameworks: { frontend: [], backend: [] },
|
|
227
|
+
databases: [],
|
|
228
|
+
manifests: [],
|
|
229
|
+
architecture: [],
|
|
230
|
+
};
|
|
231
|
+
const session = {
|
|
232
|
+
section: 'product',
|
|
233
|
+
questionsAsked: mockQuestions.slice(0, 1),
|
|
234
|
+
answers: [
|
|
235
|
+
{
|
|
236
|
+
questionId: 'product-1',
|
|
237
|
+
selections: ['A'],
|
|
238
|
+
selectedOptions: ['E-commerce'],
|
|
239
|
+
timestamp: new Date().toISOString(),
|
|
240
|
+
},
|
|
241
|
+
],
|
|
242
|
+
autoGenerateRequested: false,
|
|
243
|
+
startTime: new Date().toISOString(),
|
|
244
|
+
endTime: new Date().toISOString(),
|
|
245
|
+
};
|
|
246
|
+
const draft = draftDocumentFromAnswers(session, greenfieldAnalysis);
|
|
247
|
+
expect(draft.content).toContain('E-commerce');
|
|
248
|
+
expect(draft.content.length).toBeGreaterThan(30);
|
|
249
|
+
});
|
|
250
|
+
});
|
|
251
|
+
describe('saveDocumentState', () => {
|
|
252
|
+
it('should create state file with correct checkpoint structure', async () => {
|
|
253
|
+
const result = await saveDocumentState({
|
|
254
|
+
section: 'product',
|
|
255
|
+
checkpoint: '2.1_product_guide',
|
|
256
|
+
content: '# Product Guide\n\nContent here',
|
|
257
|
+
filePath: 'conductor-cdd/product.md',
|
|
258
|
+
});
|
|
259
|
+
expect(result.success).toBe(true);
|
|
260
|
+
expect(result.stateFile).toContain('setup_state.json');
|
|
261
|
+
expect(result.checkpoint).toBe('2.1_product_guide');
|
|
262
|
+
});
|
|
263
|
+
it('should update existing state file without overwriting other checkpoints', async () => {
|
|
264
|
+
// First save
|
|
265
|
+
await saveDocumentState({
|
|
266
|
+
section: 'product',
|
|
267
|
+
checkpoint: '2.1_product_guide',
|
|
268
|
+
content: 'Content 1',
|
|
269
|
+
filePath: 'conductor-cdd/product.md',
|
|
270
|
+
});
|
|
271
|
+
// Second save
|
|
272
|
+
const result = await saveDocumentState({
|
|
273
|
+
section: 'guidelines',
|
|
274
|
+
checkpoint: '2.2_product_guidelines',
|
|
275
|
+
content: 'Content 2',
|
|
276
|
+
filePath: 'conductor-cdd/guidelines.md',
|
|
277
|
+
});
|
|
278
|
+
expect(result.success).toBe(true);
|
|
279
|
+
expect(result.previousCheckpoints).toContain('2.1_product_guide');
|
|
280
|
+
});
|
|
281
|
+
it('should save document to correct file path', async () => {
|
|
282
|
+
const result = await saveDocumentState({
|
|
283
|
+
section: 'product',
|
|
284
|
+
checkpoint: '2.1_product_guide',
|
|
285
|
+
content: '# Product Guide\n\nTest content',
|
|
286
|
+
filePath: 'conductor-cdd/product.md',
|
|
287
|
+
});
|
|
288
|
+
expect(result.success).toBe(true);
|
|
289
|
+
expect(result.filePath).toContain('product.md');
|
|
290
|
+
});
|
|
291
|
+
it('should handle file system errors gracefully', async () => {
|
|
292
|
+
const result = await saveDocumentState({
|
|
293
|
+
section: 'product',
|
|
294
|
+
checkpoint: '2.1_product_guide',
|
|
295
|
+
content: 'Content',
|
|
296
|
+
filePath: '/invalid/path/that/does/not/exist/file.md',
|
|
297
|
+
});
|
|
298
|
+
expect(result.success).toBe(false);
|
|
299
|
+
expect(result.error).toBeDefined();
|
|
300
|
+
});
|
|
301
|
+
});
|
|
302
|
+
describe('generateDocument', () => {
|
|
303
|
+
it('should orchestrate full document generation workflow', async () => {
|
|
304
|
+
const mockResponder = vi.fn()
|
|
305
|
+
.mockResolvedValueOnce(['A'])
|
|
306
|
+
.mockResolvedValueOnce(['B']);
|
|
307
|
+
const mockApprovalFlow = vi.fn()
|
|
308
|
+
.mockResolvedValueOnce({ approved: true, finalContent: '# Final content' });
|
|
309
|
+
const options = {
|
|
310
|
+
section: 'product',
|
|
311
|
+
questions: mockQuestions,
|
|
312
|
+
analysis: mockAnalysis,
|
|
313
|
+
responder: mockResponder,
|
|
314
|
+
approvalFlow: mockApprovalFlow,
|
|
315
|
+
outputPath: 'conductor-cdd/product.md',
|
|
316
|
+
};
|
|
317
|
+
const result = await generateDocument(options);
|
|
318
|
+
expect(result.success).toBe(true);
|
|
319
|
+
expect(result.checkpoint).toBe('2.1_product_guide');
|
|
320
|
+
expect(mockResponder).toHaveBeenCalled();
|
|
321
|
+
expect(mockApprovalFlow).toHaveBeenCalled();
|
|
322
|
+
});
|
|
323
|
+
it('should handle auto-generate early exit', async () => {
|
|
324
|
+
const mockResponder = vi.fn()
|
|
325
|
+
.mockResolvedValueOnce(['E']); // Auto-generate immediately
|
|
326
|
+
const mockApprovalFlow = vi.fn()
|
|
327
|
+
.mockResolvedValueOnce({ approved: true, finalContent: '# Auto-generated content' });
|
|
328
|
+
const options = {
|
|
329
|
+
section: 'product',
|
|
330
|
+
questions: mockQuestions,
|
|
331
|
+
analysis: mockAnalysis,
|
|
332
|
+
responder: mockResponder,
|
|
333
|
+
approvalFlow: mockApprovalFlow,
|
|
334
|
+
outputPath: 'conductor-cdd/product.md',
|
|
335
|
+
};
|
|
336
|
+
const result = await generateDocument(options);
|
|
337
|
+
expect(result.success).toBe(true);
|
|
338
|
+
expect(result.autoGenerated).toBe(true);
|
|
339
|
+
expect(mockResponder).toHaveBeenCalledTimes(1); // Stopped after E
|
|
340
|
+
});
|
|
341
|
+
it('should handle approval rejection and revision loop', async () => {
|
|
342
|
+
const mockResponder = vi.fn()
|
|
343
|
+
.mockResolvedValueOnce(['A']);
|
|
344
|
+
const mockApprovalFlow = vi.fn()
|
|
345
|
+
.mockResolvedValueOnce({ approved: false, revisionGuidance: 'Add more details' })
|
|
346
|
+
.mockResolvedValueOnce({ approved: true, finalContent: '# Revised content' });
|
|
347
|
+
const options = {
|
|
348
|
+
section: 'product',
|
|
349
|
+
questions: mockQuestions.slice(0, 1),
|
|
350
|
+
analysis: mockAnalysis,
|
|
351
|
+
responder: mockResponder,
|
|
352
|
+
approvalFlow: mockApprovalFlow,
|
|
353
|
+
outputPath: 'conductor-cdd/product.md',
|
|
354
|
+
};
|
|
355
|
+
const result = await generateDocument(options);
|
|
356
|
+
expect(result.success).toBe(true);
|
|
357
|
+
expect(result.revisionCount).toBe(1);
|
|
358
|
+
expect(mockApprovalFlow).toHaveBeenCalledTimes(2);
|
|
359
|
+
});
|
|
360
|
+
it('should fail after max revision attempts', async () => {
|
|
361
|
+
const mockResponder = vi.fn()
|
|
362
|
+
.mockResolvedValueOnce(['A']);
|
|
363
|
+
const mockApprovalFlow = vi.fn()
|
|
364
|
+
.mockResolvedValue({ approved: false, revisionGuidance: 'Still not good' });
|
|
365
|
+
const options = {
|
|
366
|
+
section: 'product',
|
|
367
|
+
questions: mockQuestions.slice(0, 1),
|
|
368
|
+
analysis: mockAnalysis,
|
|
369
|
+
responder: mockResponder,
|
|
370
|
+
approvalFlow: mockApprovalFlow,
|
|
371
|
+
outputPath: 'conductor-cdd/product.md',
|
|
372
|
+
maxRevisions: 3,
|
|
373
|
+
};
|
|
374
|
+
const result = await generateDocument(options);
|
|
375
|
+
expect(result.success).toBe(false);
|
|
376
|
+
expect(result.error).toContain('revision');
|
|
377
|
+
expect(mockApprovalFlow).toHaveBeenCalledTimes(3);
|
|
378
|
+
});
|
|
379
|
+
});
|
|
380
|
+
});
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { Question, QuestionType } from './questionGenerator.js';
|
|
2
|
+
/**
|
|
3
|
+
* Interactive Menu System
|
|
4
|
+
*
|
|
5
|
+
* Provides menu rendering and input validation for the CDD setup process.
|
|
6
|
+
* Adapts to LLM-based interaction pattern (no terminal UI library required).
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Format questions with lettered options (A-E)
|
|
10
|
+
* - Validate single/multiple selections
|
|
11
|
+
* - Parse user input into selection arrays
|
|
12
|
+
* - Render complete menus with instructions
|
|
13
|
+
*
|
|
14
|
+
* Based on reference implementations:
|
|
15
|
+
* - derekbar90/opencode-conductor
|
|
16
|
+
* - gemini-cli-extensions/conductor
|
|
17
|
+
*/
|
|
18
|
+
export interface MenuOptions {
|
|
19
|
+
showInstructions?: boolean;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Format a question with numbered display and lettered options
|
|
23
|
+
*
|
|
24
|
+
* Example output:
|
|
25
|
+
* Question 1: What are the key features? (Select all that apply)
|
|
26
|
+
* A) User authentication
|
|
27
|
+
* B) Real-time updates
|
|
28
|
+
* C) Data analytics
|
|
29
|
+
* D) Enter custom features
|
|
30
|
+
* E) Auto-generate from codebase
|
|
31
|
+
*/
|
|
32
|
+
export declare function formatQuestionWithOptions(question: Question, questionNumber: number): string;
|
|
33
|
+
/**
|
|
34
|
+
* Validate user selection input
|
|
35
|
+
*
|
|
36
|
+
* Rules:
|
|
37
|
+
* - Exclusive: Single letter A-E
|
|
38
|
+
* - Additive: Single letter or comma-separated (A,B,C)
|
|
39
|
+
* - Option E cannot be combined with others in additive
|
|
40
|
+
* - No duplicates allowed
|
|
41
|
+
* - Case insensitive
|
|
42
|
+
*/
|
|
43
|
+
export declare function validateSelection(input: string, questionType: QuestionType): boolean;
|
|
44
|
+
/**
|
|
45
|
+
* Parse user selection input into array of letters
|
|
46
|
+
*
|
|
47
|
+
* Returns empty array if invalid
|
|
48
|
+
* Normalizes to uppercase, removes duplicates, and sorts for additive
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseSelection(input: string, questionType: QuestionType): string[];
|
|
51
|
+
/**
|
|
52
|
+
* Render complete interactive menu with question and instructions
|
|
53
|
+
*
|
|
54
|
+
* Generates LLM-compatible menu display suitable for text-based interaction
|
|
55
|
+
*/
|
|
56
|
+
export declare function renderMenu(question: Question, questionNumber: number, options?: MenuOptions): string;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
const VALID_OPTIONS = ['A', 'B', 'C', 'D', 'E'];
|
|
2
|
+
const MAX_OPTION_LENGTH = 80;
|
|
3
|
+
/**
|
|
4
|
+
* Format a question with numbered display and lettered options
|
|
5
|
+
*
|
|
6
|
+
* Example output:
|
|
7
|
+
* Question 1: What are the key features? (Select all that apply)
|
|
8
|
+
* A) User authentication
|
|
9
|
+
* B) Real-time updates
|
|
10
|
+
* C) Data analytics
|
|
11
|
+
* D) Enter custom features
|
|
12
|
+
* E) Auto-generate from codebase
|
|
13
|
+
*/
|
|
14
|
+
export function formatQuestionWithOptions(question, questionNumber) {
|
|
15
|
+
const lines = [];
|
|
16
|
+
// Question header with number
|
|
17
|
+
const suffix = question.type === 'additive' ? ' (Select all that apply)' : '';
|
|
18
|
+
lines.push(`Question ${questionNumber}: ${question.text}${suffix}`);
|
|
19
|
+
lines.push('');
|
|
20
|
+
// Format options A-E
|
|
21
|
+
question.options.forEach((option, index) => {
|
|
22
|
+
const letter = VALID_OPTIONS[index];
|
|
23
|
+
// Truncate option text: if > 80, cut to fit "..." within 80 char limit
|
|
24
|
+
// Target: 80 max, so truncate to 75 + "..." (3) = 78 total
|
|
25
|
+
const truncated = option.length > MAX_OPTION_LENGTH
|
|
26
|
+
? option.substring(0, 75) + '...'
|
|
27
|
+
: option;
|
|
28
|
+
lines.push(`${letter}) ${truncated}`);
|
|
29
|
+
});
|
|
30
|
+
return lines.join('\n');
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Validate user selection input
|
|
34
|
+
*
|
|
35
|
+
* Rules:
|
|
36
|
+
* - Exclusive: Single letter A-E
|
|
37
|
+
* - Additive: Single letter or comma-separated (A,B,C)
|
|
38
|
+
* - Option E cannot be combined with others in additive
|
|
39
|
+
* - No duplicates allowed
|
|
40
|
+
* - Case insensitive
|
|
41
|
+
*/
|
|
42
|
+
export function validateSelection(input, questionType) {
|
|
43
|
+
const trimmed = input.trim();
|
|
44
|
+
if (!trimmed) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
// Parse selections
|
|
48
|
+
const selections = trimmed
|
|
49
|
+
.split(',')
|
|
50
|
+
.map(s => s.trim().toUpperCase())
|
|
51
|
+
.filter(s => s.length > 0);
|
|
52
|
+
// Check for empty selections after split
|
|
53
|
+
if (selections.length === 0) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
// All selections must be valid letters (A-E)
|
|
57
|
+
for (const selection of selections) {
|
|
58
|
+
if (!VALID_OPTIONS.includes(selection)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// Check for duplicates using Set
|
|
63
|
+
const uniqueSelections = new Set(selections);
|
|
64
|
+
if (uniqueSelections.size !== selections.length) {
|
|
65
|
+
return false;
|
|
66
|
+
}
|
|
67
|
+
// Exclusive: Only one selection allowed
|
|
68
|
+
if (questionType === 'exclusive' && uniqueSelections.size > 1) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
// Option E (auto-generate) cannot be combined with others in additive
|
|
72
|
+
if (questionType === 'additive' && uniqueSelections.size > 1 && uniqueSelections.has('E')) {
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Parse user selection input into array of letters
|
|
79
|
+
*
|
|
80
|
+
* Returns empty array if invalid
|
|
81
|
+
* Normalizes to uppercase, removes duplicates, and sorts for additive
|
|
82
|
+
*/
|
|
83
|
+
export function parseSelection(input, questionType) {
|
|
84
|
+
const trimmed = input.trim();
|
|
85
|
+
if (!trimmed) {
|
|
86
|
+
return [];
|
|
87
|
+
}
|
|
88
|
+
// Parse and normalize
|
|
89
|
+
const selections = trimmed
|
|
90
|
+
.split(',')
|
|
91
|
+
.map(s => s.trim().toUpperCase())
|
|
92
|
+
.filter(s => s.length > 0);
|
|
93
|
+
// Remove duplicates
|
|
94
|
+
const uniqueSelections = [...new Set(selections)];
|
|
95
|
+
// Now validate the unique selections
|
|
96
|
+
const validationInput = uniqueSelections.join(',');
|
|
97
|
+
if (!validateSelection(validationInput, questionType)) {
|
|
98
|
+
return [];
|
|
99
|
+
}
|
|
100
|
+
// Sort additive selections for consistency
|
|
101
|
+
if (questionType === 'additive') {
|
|
102
|
+
return uniqueSelections.sort();
|
|
103
|
+
}
|
|
104
|
+
return uniqueSelections;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Get section-specific instruction text for the menu
|
|
108
|
+
*/
|
|
109
|
+
function getSectionInstructions(question) {
|
|
110
|
+
const instructions = [];
|
|
111
|
+
if (question.type === 'additive') {
|
|
112
|
+
instructions.push('- You can select multiple options by separating them with commas (e.g., "A,B,C")');
|
|
113
|
+
instructions.push('- Or select a single option (e.g., "A")');
|
|
114
|
+
}
|
|
115
|
+
else {
|
|
116
|
+
instructions.push('- Select one option (e.g., "A")');
|
|
117
|
+
}
|
|
118
|
+
instructions.push('- Option D: Enter custom text when prompted');
|
|
119
|
+
instructions.push(`- Option E: Auto-generate this section from your codebase`);
|
|
120
|
+
if (question.type === 'additive') {
|
|
121
|
+
instructions.push('- Note: Option E cannot be combined with other options');
|
|
122
|
+
}
|
|
123
|
+
return instructions.join('\n');
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Render complete interactive menu with question and instructions
|
|
127
|
+
*
|
|
128
|
+
* Generates LLM-compatible menu display suitable for text-based interaction
|
|
129
|
+
*/
|
|
130
|
+
export function renderMenu(question, questionNumber, options = {}) {
|
|
131
|
+
const { showInstructions = true } = options;
|
|
132
|
+
const lines = [];
|
|
133
|
+
// Add formatted question with options
|
|
134
|
+
lines.push(formatQuestionWithOptions(question, questionNumber));
|
|
135
|
+
// Add instructions if enabled
|
|
136
|
+
if (showInstructions) {
|
|
137
|
+
lines.push('');
|
|
138
|
+
lines.push('Instructions:');
|
|
139
|
+
lines.push('Enter your selection (e.g., "A,B,C" for multiple or "A" for single)');
|
|
140
|
+
lines.push('');
|
|
141
|
+
lines.push(getSectionInstructions(question));
|
|
142
|
+
}
|
|
143
|
+
return lines.join('\n');
|
|
144
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|