opencode-conductor-cdd-plugin 1.0.0-beta.17 → 1.0.0-beta.19
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/dist/prompts/agent/cdd.md +16 -16
- package/dist/prompts/agent/implementer.md +5 -5
- package/dist/prompts/agent.md +7 -7
- package/dist/prompts/cdd/implement.json +1 -1
- package/dist/prompts/cdd/revert.json +1 -1
- 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/test/integration/rebrand.test.js +15 -14
- package/dist/utils/agentMapping.js +2 -0
- package/dist/utils/archive-tracks.d.ts +28 -0
- package/dist/utils/archive-tracks.js +154 -1
- package/dist/utils/archive-tracks.test.d.ts +1 -0
- package/dist/utils/archive-tracks.test.js +495 -0
- 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/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/metadataTracker.d.ts +39 -0
- package/dist/utils/metadataTracker.js +105 -0
- package/dist/utils/metadataTracker.test.d.ts +1 -0
- package/dist/utils/metadataTracker.test.js +265 -0
- package/dist/utils/planParser.d.ts +25 -0
- package/dist/utils/planParser.js +107 -0
- package/dist/utils/planParser.test.d.ts +1 -0
- package/dist/utils/planParser.test.js +119 -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/statusDisplay.d.ts +35 -0
- package/dist/utils/statusDisplay.js +81 -0
- package/dist/utils/statusDisplay.test.d.ts +1 -0
- package/dist/utils/statusDisplay.test.js +102 -0
- package/package.json +1 -1
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { setupProductGuide, setupGuidelines, setupTechStack, setupStyleguides, setupWorkflow, runFullSetup, } from './setupIntegration.js';
|
|
5
|
+
describe('setupIntegration', () => {
|
|
6
|
+
const testOutputDir = path.join(process.cwd(), 'test-output-setup-integration');
|
|
7
|
+
const testProjectDir = path.join(testOutputDir, 'test-project');
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
// Create test directories
|
|
10
|
+
if (!fs.existsSync(testOutputDir)) {
|
|
11
|
+
fs.mkdirSync(testOutputDir, { recursive: true });
|
|
12
|
+
}
|
|
13
|
+
if (!fs.existsSync(testProjectDir)) {
|
|
14
|
+
fs.mkdirSync(testProjectDir, { recursive: true });
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
afterEach(() => {
|
|
18
|
+
// Clean up test files
|
|
19
|
+
if (fs.existsSync(testOutputDir)) {
|
|
20
|
+
fs.rmSync(testOutputDir, { recursive: true, force: true });
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
describe('setupProductGuide - End-to-End Integration', () => {
|
|
24
|
+
it('should generate product.md for greenfield project with user selections', async () => {
|
|
25
|
+
// Mock user responses: Select A, B, C for 3 questions
|
|
26
|
+
const mockResponder = vi.fn()
|
|
27
|
+
.mockResolvedValueOnce(['A']) // Question 1: Primary goal
|
|
28
|
+
.mockResolvedValueOnce(['B']) // Question 2: Target users
|
|
29
|
+
.mockResolvedValueOnce(['A']); // Question 3: Key features
|
|
30
|
+
const mockApproval = vi.fn()
|
|
31
|
+
.mockResolvedValueOnce({ approved: true, finalContent: undefined });
|
|
32
|
+
const options = {
|
|
33
|
+
projectPath: testProjectDir,
|
|
34
|
+
outputDir: path.join(testProjectDir, 'conductor-cdd'),
|
|
35
|
+
responder: mockResponder,
|
|
36
|
+
approvalFlow: mockApproval,
|
|
37
|
+
};
|
|
38
|
+
const result = await setupProductGuide(options);
|
|
39
|
+
expect(result.success).toBe(true);
|
|
40
|
+
expect(result.checkpoint).toBe('2.1_product_guide');
|
|
41
|
+
expect(result.filePath).toContain('product.md');
|
|
42
|
+
// Verify file was created
|
|
43
|
+
const productPath = path.join(testProjectDir, 'conductor-cdd', 'product.md');
|
|
44
|
+
expect(fs.existsSync(productPath)).toBe(true);
|
|
45
|
+
// Verify state file was created
|
|
46
|
+
const statePath = path.join(testProjectDir, 'conductor-cdd', 'setup_state.json');
|
|
47
|
+
expect(fs.existsSync(statePath)).toBe(true);
|
|
48
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
49
|
+
expect(state.last_successful_step).toBe('2.1_product_guide');
|
|
50
|
+
});
|
|
51
|
+
it('should generate product.md for brownfield project with context awareness', async () => {
|
|
52
|
+
// Create brownfield project structure
|
|
53
|
+
const packageJson = {
|
|
54
|
+
name: 'test-app',
|
|
55
|
+
version: '1.0.0',
|
|
56
|
+
dependencies: {
|
|
57
|
+
'react': '^18.0.0',
|
|
58
|
+
'express': '^4.18.0',
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
fs.writeFileSync(path.join(testProjectDir, 'package.json'), JSON.stringify(packageJson, null, 2));
|
|
62
|
+
fs.mkdirSync(path.join(testProjectDir, 'src'), { recursive: true });
|
|
63
|
+
fs.writeFileSync(path.join(testProjectDir, 'src', 'index.ts'), 'console.log("test");');
|
|
64
|
+
const mockResponder = vi.fn()
|
|
65
|
+
.mockResolvedValueOnce(['A']) // Question 1
|
|
66
|
+
.mockResolvedValueOnce(['B']) // Question 2
|
|
67
|
+
.mockResolvedValueOnce(['A']); // Question 3
|
|
68
|
+
const mockApproval = vi.fn()
|
|
69
|
+
.mockResolvedValueOnce({ approved: true });
|
|
70
|
+
const options = {
|
|
71
|
+
projectPath: testProjectDir,
|
|
72
|
+
outputDir: path.join(testProjectDir, 'conductor-cdd'),
|
|
73
|
+
responder: mockResponder,
|
|
74
|
+
approvalFlow: mockApproval,
|
|
75
|
+
};
|
|
76
|
+
const result = await setupProductGuide(options);
|
|
77
|
+
expect(result.success).toBe(true);
|
|
78
|
+
expect(result.isBrownfield).toBe(true);
|
|
79
|
+
// Verify content includes detected technologies
|
|
80
|
+
const productPath = path.join(testProjectDir, 'conductor-cdd', 'product.md');
|
|
81
|
+
const content = fs.readFileSync(productPath, 'utf-8');
|
|
82
|
+
// Should detect context from package.json
|
|
83
|
+
expect(content.length).toBeGreaterThan(50);
|
|
84
|
+
});
|
|
85
|
+
it('should handle option E (auto-generate) early exit', async () => {
|
|
86
|
+
const mockResponder = vi.fn()
|
|
87
|
+
.mockResolvedValueOnce(['E']); // Auto-generate immediately
|
|
88
|
+
const mockApproval = vi.fn()
|
|
89
|
+
.mockResolvedValueOnce({ approved: true });
|
|
90
|
+
const options = {
|
|
91
|
+
projectPath: testProjectDir,
|
|
92
|
+
outputDir: path.join(testProjectDir, 'conductor-cdd'),
|
|
93
|
+
responder: mockResponder,
|
|
94
|
+
approvalFlow: mockApproval,
|
|
95
|
+
};
|
|
96
|
+
const result = await setupProductGuide(options);
|
|
97
|
+
if (!result.success) {
|
|
98
|
+
console.error('[Test Debug] Option E test failed:', result.error);
|
|
99
|
+
}
|
|
100
|
+
expect(result.success).toBe(true);
|
|
101
|
+
expect(result.autoGenerated).toBe(true);
|
|
102
|
+
expect(mockResponder).toHaveBeenCalledTimes(1); // Only asked once
|
|
103
|
+
// Verify file was still created
|
|
104
|
+
const productPath = path.join(testProjectDir, 'conductor-cdd', 'product.md');
|
|
105
|
+
expect(fs.existsSync(productPath)).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
it('should handle option D (custom input) with user text', async () => {
|
|
108
|
+
const mockResponder = vi.fn()
|
|
109
|
+
.mockResolvedValueOnce(['D']) // Question 1: Custom input
|
|
110
|
+
.mockResolvedValueOnce(['A']) // Question 2
|
|
111
|
+
.mockResolvedValueOnce(['B']); // Question 3
|
|
112
|
+
const mockCustomInput = vi.fn()
|
|
113
|
+
.mockResolvedValueOnce('A revolutionary AI-powered task management platform');
|
|
114
|
+
const mockApproval = vi.fn()
|
|
115
|
+
.mockResolvedValueOnce({ approved: true });
|
|
116
|
+
const options = {
|
|
117
|
+
projectPath: testProjectDir,
|
|
118
|
+
outputDir: path.join(testProjectDir, 'conductor-cdd'),
|
|
119
|
+
responder: mockResponder,
|
|
120
|
+
approvalFlow: mockApproval,
|
|
121
|
+
customInputPrompt: mockCustomInput,
|
|
122
|
+
};
|
|
123
|
+
const result = await setupProductGuide(options);
|
|
124
|
+
expect(result.success).toBe(true);
|
|
125
|
+
expect(mockCustomInput).toHaveBeenCalled();
|
|
126
|
+
const productPath = path.join(testProjectDir, 'conductor-cdd', 'product.md');
|
|
127
|
+
const content = fs.readFileSync(productPath, 'utf-8');
|
|
128
|
+
expect(content).toContain('AI-powered task management platform');
|
|
129
|
+
});
|
|
130
|
+
it('should handle approval rejection and revision loop', async () => {
|
|
131
|
+
const mockResponder = vi.fn()
|
|
132
|
+
.mockResolvedValueOnce(['A']) // Question 1
|
|
133
|
+
.mockResolvedValueOnce(['B']) // Question 2
|
|
134
|
+
.mockResolvedValueOnce(['A']); // Question 3
|
|
135
|
+
const mockApproval = vi.fn()
|
|
136
|
+
.mockResolvedValueOnce({ approved: false, revisionGuidance: 'Add more details about users' })
|
|
137
|
+
.mockResolvedValueOnce({ approved: true, finalContent: 'Revised content with more details' });
|
|
138
|
+
const options = {
|
|
139
|
+
projectPath: testProjectDir,
|
|
140
|
+
outputDir: path.join(testProjectDir, 'conductor-cdd'),
|
|
141
|
+
responder: mockResponder,
|
|
142
|
+
approvalFlow: mockApproval,
|
|
143
|
+
};
|
|
144
|
+
const result = await setupProductGuide(options);
|
|
145
|
+
expect(result.success).toBe(true);
|
|
146
|
+
expect(result.revisionCount).toBe(1);
|
|
147
|
+
expect(mockApproval).toHaveBeenCalledTimes(2);
|
|
148
|
+
const productPath = path.join(testProjectDir, 'conductor-cdd', 'product.md');
|
|
149
|
+
const content = fs.readFileSync(productPath, 'utf-8');
|
|
150
|
+
expect(content).toContain('Revised content with more details');
|
|
151
|
+
});
|
|
152
|
+
it('should enforce max 5 questions limit', async () => {
|
|
153
|
+
// Setup to answer 10 times, but should only be asked 5
|
|
154
|
+
const mockResponder = vi.fn()
|
|
155
|
+
.mockResolvedValue(['A']);
|
|
156
|
+
const mockApproval = vi.fn()
|
|
157
|
+
.mockResolvedValueOnce({ approved: true });
|
|
158
|
+
const options = {
|
|
159
|
+
projectPath: testProjectDir,
|
|
160
|
+
outputDir: path.join(testProjectDir, 'conductor-cdd'),
|
|
161
|
+
responder: mockResponder,
|
|
162
|
+
approvalFlow: mockApproval,
|
|
163
|
+
};
|
|
164
|
+
const result = await setupProductGuide(options);
|
|
165
|
+
expect(result.success).toBe(true);
|
|
166
|
+
expect(mockResponder.mock.calls.length).toBeLessThanOrEqual(5);
|
|
167
|
+
});
|
|
168
|
+
it('should NOT include unselected options in final document', async () => {
|
|
169
|
+
const mockResponder = vi.fn()
|
|
170
|
+
.mockResolvedValueOnce(['A']) // Question 1: Select only A
|
|
171
|
+
.mockResolvedValueOnce(['A']) // Question 2
|
|
172
|
+
.mockResolvedValueOnce(['A']); // Question 3
|
|
173
|
+
const mockApproval = vi.fn()
|
|
174
|
+
.mockResolvedValueOnce({ approved: true });
|
|
175
|
+
const options = {
|
|
176
|
+
projectPath: testProjectDir,
|
|
177
|
+
outputDir: path.join(testProjectDir, 'conductor-cdd'),
|
|
178
|
+
responder: mockResponder,
|
|
179
|
+
approvalFlow: mockApproval,
|
|
180
|
+
};
|
|
181
|
+
const result = await setupProductGuide(options);
|
|
182
|
+
expect(result.success).toBe(true);
|
|
183
|
+
const productPath = path.join(testProjectDir, 'conductor-cdd', 'product.md');
|
|
184
|
+
const content = fs.readFileSync(productPath, 'utf-8');
|
|
185
|
+
// Should NOT contain option markers
|
|
186
|
+
expect(content).not.toMatch(/\bA\)/);
|
|
187
|
+
expect(content).not.toMatch(/\bB\)/);
|
|
188
|
+
expect(content).not.toMatch(/\bC\)/);
|
|
189
|
+
expect(content).not.toMatch(/\bD\)/);
|
|
190
|
+
expect(content).not.toMatch(/\bE\)/);
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
describe('runFullSetup - Complete Workflow Integration', () => {
|
|
194
|
+
it('should generate all 5 documents in sequence', async () => {
|
|
195
|
+
const mockResponder = vi.fn()
|
|
196
|
+
.mockResolvedValue(['A']); // Simple answer for all questions
|
|
197
|
+
const mockApproval = vi.fn()
|
|
198
|
+
.mockResolvedValue({ approved: true });
|
|
199
|
+
const options = {
|
|
200
|
+
projectPath: testProjectDir,
|
|
201
|
+
outputDir: path.join(testProjectDir, 'conductor-cdd'),
|
|
202
|
+
responder: mockResponder,
|
|
203
|
+
approvalFlow: mockApproval,
|
|
204
|
+
};
|
|
205
|
+
const result = await runFullSetup(options);
|
|
206
|
+
expect(result.success).toBe(true);
|
|
207
|
+
expect(result.documentsCreated).toHaveLength(5);
|
|
208
|
+
// Verify all 5 documents were created
|
|
209
|
+
const cddDir = path.join(testProjectDir, 'conductor-cdd');
|
|
210
|
+
expect(fs.existsSync(path.join(cddDir, 'product.md'))).toBe(true);
|
|
211
|
+
expect(fs.existsSync(path.join(cddDir, 'guidelines.md'))).toBe(true);
|
|
212
|
+
expect(fs.existsSync(path.join(cddDir, 'tech-stack.md'))).toBe(true);
|
|
213
|
+
expect(fs.existsSync(path.join(cddDir, 'styleguides.md'))).toBe(true);
|
|
214
|
+
expect(fs.existsSync(path.join(cddDir, 'workflow.md'))).toBe(true);
|
|
215
|
+
// Verify final state
|
|
216
|
+
const statePath = path.join(cddDir, 'setup_state.json');
|
|
217
|
+
const state = JSON.parse(fs.readFileSync(statePath, 'utf-8'));
|
|
218
|
+
expect(state.last_successful_step).toBe('2.5_workflow');
|
|
219
|
+
});
|
|
220
|
+
it('should resume from checkpoint if interrupted', async () => {
|
|
221
|
+
// First, create partial state (stopped after product.md)
|
|
222
|
+
const cddDir = path.join(testProjectDir, 'conductor-cdd');
|
|
223
|
+
fs.mkdirSync(cddDir, { recursive: true });
|
|
224
|
+
const partialState = {
|
|
225
|
+
last_successful_step: '2.1_product_guide',
|
|
226
|
+
product_timestamp: new Date().toISOString(),
|
|
227
|
+
};
|
|
228
|
+
fs.writeFileSync(path.join(cddDir, 'setup_state.json'), JSON.stringify(partialState, null, 2));
|
|
229
|
+
// Mock responder should only be called for remaining sections
|
|
230
|
+
const mockResponder = vi.fn()
|
|
231
|
+
.mockResolvedValue(['A']);
|
|
232
|
+
const mockApproval = vi.fn()
|
|
233
|
+
.mockResolvedValue({ approved: true });
|
|
234
|
+
const options = {
|
|
235
|
+
projectPath: testProjectDir,
|
|
236
|
+
outputDir: cddDir,
|
|
237
|
+
responder: mockResponder,
|
|
238
|
+
approvalFlow: mockApproval,
|
|
239
|
+
resume: true,
|
|
240
|
+
};
|
|
241
|
+
const result = await runFullSetup(options);
|
|
242
|
+
expect(result.success).toBe(true);
|
|
243
|
+
expect(result.resumed).toBe(true);
|
|
244
|
+
expect(result.resumedFrom).toBe('2.1_product_guide');
|
|
245
|
+
// Should have created remaining 4 documents
|
|
246
|
+
expect(result.documentsCreated).toHaveLength(4);
|
|
247
|
+
});
|
|
248
|
+
it('should handle failures and report partial completion', async () => {
|
|
249
|
+
const mockResponder = vi.fn()
|
|
250
|
+
.mockResolvedValue(['A']);
|
|
251
|
+
// Fail on guidelines approval
|
|
252
|
+
const mockApproval = vi.fn()
|
|
253
|
+
.mockResolvedValueOnce({ approved: true }) // product: success
|
|
254
|
+
.mockResolvedValue({ approved: false, revisionGuidance: 'Keep rejecting' }); // guidelines: fail
|
|
255
|
+
const options = {
|
|
256
|
+
projectPath: testProjectDir,
|
|
257
|
+
outputDir: path.join(testProjectDir, 'conductor-cdd'),
|
|
258
|
+
responder: mockResponder,
|
|
259
|
+
approvalFlow: mockApproval,
|
|
260
|
+
maxRevisions: 2,
|
|
261
|
+
};
|
|
262
|
+
const result = await runFullSetup(options);
|
|
263
|
+
expect(result.success).toBe(false);
|
|
264
|
+
expect(result.documentsCreated.length).toBeLessThan(5);
|
|
265
|
+
expect(result.error).toContain('revision');
|
|
266
|
+
// Product should be created, but not all others
|
|
267
|
+
const cddDir = path.join(testProjectDir, 'conductor-cdd');
|
|
268
|
+
expect(fs.existsSync(path.join(cddDir, 'product.md'))).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
describe('Individual Section Setup Functions', () => {
|
|
272
|
+
it('should setup guidelines.md independently', async () => {
|
|
273
|
+
const mockResponder = vi.fn().mockResolvedValue(['A']);
|
|
274
|
+
const mockApproval = vi.fn().mockResolvedValue({ approved: true });
|
|
275
|
+
const options = {
|
|
276
|
+
projectPath: testProjectDir,
|
|
277
|
+
outputDir: path.join(testProjectDir, 'conductor-cdd'),
|
|
278
|
+
responder: mockResponder,
|
|
279
|
+
approvalFlow: mockApproval,
|
|
280
|
+
};
|
|
281
|
+
const result = await setupGuidelines(options);
|
|
282
|
+
expect(result.success).toBe(true);
|
|
283
|
+
expect(result.checkpoint).toBe('2.2_product_guidelines');
|
|
284
|
+
const guidelinesPath = path.join(testProjectDir, 'conductor-cdd', 'guidelines.md');
|
|
285
|
+
expect(fs.existsSync(guidelinesPath)).toBe(true);
|
|
286
|
+
});
|
|
287
|
+
it('should setup tech-stack.md with auto-detected dependencies', async () => {
|
|
288
|
+
// Create package.json for detection
|
|
289
|
+
const packageJson = {
|
|
290
|
+
name: 'test-app',
|
|
291
|
+
dependencies: {
|
|
292
|
+
'react': '^18.0.0',
|
|
293
|
+
'typescript': '^5.0.0',
|
|
294
|
+
},
|
|
295
|
+
};
|
|
296
|
+
fs.writeFileSync(path.join(testProjectDir, 'package.json'), JSON.stringify(packageJson, null, 2));
|
|
297
|
+
const mockResponder = vi.fn().mockResolvedValue(['E']); // Auto-generate
|
|
298
|
+
const mockApproval = vi.fn().mockResolvedValue({ approved: true });
|
|
299
|
+
const options = {
|
|
300
|
+
projectPath: testProjectDir,
|
|
301
|
+
outputDir: path.join(testProjectDir, 'conductor-cdd'),
|
|
302
|
+
responder: mockResponder,
|
|
303
|
+
approvalFlow: mockApproval,
|
|
304
|
+
};
|
|
305
|
+
const result = await setupTechStack(options);
|
|
306
|
+
expect(result.success).toBe(true);
|
|
307
|
+
expect(result.checkpoint).toBe('2.3_tech_stack');
|
|
308
|
+
const techStackPath = path.join(testProjectDir, 'conductor-cdd', 'tech-stack.md');
|
|
309
|
+
const content = fs.readFileSync(techStackPath, 'utf-8');
|
|
310
|
+
// Should detect React and TypeScript
|
|
311
|
+
expect(content.toLowerCase()).toMatch(/react|typescript/);
|
|
312
|
+
});
|
|
313
|
+
it('should setup styleguides.md', async () => {
|
|
314
|
+
const mockResponder = vi.fn().mockResolvedValue(['A']);
|
|
315
|
+
const mockApproval = vi.fn().mockResolvedValue({ approved: true });
|
|
316
|
+
const options = {
|
|
317
|
+
projectPath: testProjectDir,
|
|
318
|
+
outputDir: path.join(testProjectDir, 'conductor-cdd'),
|
|
319
|
+
responder: mockResponder,
|
|
320
|
+
approvalFlow: mockApproval,
|
|
321
|
+
};
|
|
322
|
+
const result = await setupStyleguides(options);
|
|
323
|
+
expect(result.success).toBe(true);
|
|
324
|
+
expect(result.checkpoint).toBe('2.4_code_styleguides');
|
|
325
|
+
const styleguidesPath = path.join(testProjectDir, 'conductor-cdd', 'styleguides.md');
|
|
326
|
+
expect(fs.existsSync(styleguidesPath)).toBe(true);
|
|
327
|
+
});
|
|
328
|
+
it('should setup workflow.md', async () => {
|
|
329
|
+
const mockResponder = vi.fn().mockResolvedValue(['A']);
|
|
330
|
+
const mockApproval = vi.fn().mockResolvedValue({ approved: true });
|
|
331
|
+
const options = {
|
|
332
|
+
projectPath: testProjectDir,
|
|
333
|
+
outputDir: path.join(testProjectDir, 'conductor-cdd'),
|
|
334
|
+
responder: mockResponder,
|
|
335
|
+
approvalFlow: mockApproval,
|
|
336
|
+
};
|
|
337
|
+
const result = await setupWorkflow(options);
|
|
338
|
+
expect(result.success).toBe(true);
|
|
339
|
+
expect(result.checkpoint).toBe('2.5_workflow');
|
|
340
|
+
const workflowPath = path.join(testProjectDir, 'conductor-cdd', 'workflow.md');
|
|
341
|
+
expect(fs.existsSync(workflowPath)).toBe(true);
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
});
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface TrackStatusInfo {
|
|
2
|
+
trackId: string;
|
|
3
|
+
trackName: string;
|
|
4
|
+
totalTasks: number;
|
|
5
|
+
completedTasks: number;
|
|
6
|
+
inProgressTasks: number;
|
|
7
|
+
currentPhase: string;
|
|
8
|
+
lastUpdated: string;
|
|
9
|
+
}
|
|
10
|
+
export interface StatusFormatOptions {
|
|
11
|
+
compact?: boolean;
|
|
12
|
+
noColor?: boolean;
|
|
13
|
+
barLength?: number;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Calculates progress percentage
|
|
17
|
+
* @param completed Number of completed items
|
|
18
|
+
* @param total Total number of items
|
|
19
|
+
* @returns Progress percentage (0-100)
|
|
20
|
+
*/
|
|
21
|
+
export declare function calculateProgress(completed: number, total: number): number;
|
|
22
|
+
/**
|
|
23
|
+
* Generates a visual progress bar
|
|
24
|
+
* @param percentage Progress percentage (0-100)
|
|
25
|
+
* @param length Length of the progress bar (default: 20)
|
|
26
|
+
* @returns Progress bar string
|
|
27
|
+
*/
|
|
28
|
+
export declare function generateProgressBar(percentage: number, length?: number): string;
|
|
29
|
+
/**
|
|
30
|
+
* Formats track status into a visually appealing display
|
|
31
|
+
* @param track Track status information
|
|
32
|
+
* @param options Formatting options
|
|
33
|
+
* @returns Formatted status string
|
|
34
|
+
*/
|
|
35
|
+
export declare function formatTrackStatus(track: TrackStatusInfo, options?: StatusFormatOptions): string;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
// ANSI color codes
|
|
2
|
+
const colors = {
|
|
3
|
+
reset: "\x1b[0m",
|
|
4
|
+
bright: "\x1b[1m",
|
|
5
|
+
dim: "\x1b[2m",
|
|
6
|
+
green: "\x1b[32m",
|
|
7
|
+
yellow: "\x1b[33m",
|
|
8
|
+
blue: "\x1b[34m",
|
|
9
|
+
cyan: "\x1b[36m",
|
|
10
|
+
gray: "\x1b[90m"
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Calculates progress percentage
|
|
14
|
+
* @param completed Number of completed items
|
|
15
|
+
* @param total Total number of items
|
|
16
|
+
* @returns Progress percentage (0-100)
|
|
17
|
+
*/
|
|
18
|
+
export function calculateProgress(completed, total) {
|
|
19
|
+
if (total === 0)
|
|
20
|
+
return 0;
|
|
21
|
+
return Math.round((completed / total) * 100);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Generates a visual progress bar
|
|
25
|
+
* @param percentage Progress percentage (0-100)
|
|
26
|
+
* @param length Length of the progress bar (default: 20)
|
|
27
|
+
* @returns Progress bar string
|
|
28
|
+
*/
|
|
29
|
+
export function generateProgressBar(percentage, length = 20) {
|
|
30
|
+
const filled = Math.round((percentage / 100) * length);
|
|
31
|
+
const empty = length - filled;
|
|
32
|
+
return "█".repeat(filled) + "░".repeat(empty);
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Formats track status into a visually appealing display
|
|
36
|
+
* @param track Track status information
|
|
37
|
+
* @param options Formatting options
|
|
38
|
+
* @returns Formatted status string
|
|
39
|
+
*/
|
|
40
|
+
export function formatTrackStatus(track, options = {}) {
|
|
41
|
+
const { compact = false, noColor = false, barLength = 20 } = options;
|
|
42
|
+
const c = noColor
|
|
43
|
+
? {
|
|
44
|
+
reset: "",
|
|
45
|
+
bright: "",
|
|
46
|
+
dim: "",
|
|
47
|
+
green: "",
|
|
48
|
+
yellow: "",
|
|
49
|
+
blue: "",
|
|
50
|
+
cyan: "",
|
|
51
|
+
gray: ""
|
|
52
|
+
}
|
|
53
|
+
: colors;
|
|
54
|
+
const progress = calculateProgress(track.completedTasks, track.totalTasks);
|
|
55
|
+
const progressBar = generateProgressBar(progress, barLength);
|
|
56
|
+
if (compact) {
|
|
57
|
+
return `${c.bright}${track.trackName}${c.reset} ${c.gray}[${track.trackId}]${c.reset}
|
|
58
|
+
${c.cyan}${track.currentPhase}${c.reset} ${c.dim}|${c.reset} ${c.green}${track.completedTasks}${c.reset}/${track.totalTasks} tasks ${c.dim}(${progress}%)${c.reset}`;
|
|
59
|
+
}
|
|
60
|
+
const inProgressInfo = track.inProgressTasks > 0 ? `, ${track.inProgressTasks} in progress` : "";
|
|
61
|
+
const formattedDate = new Date(track.lastUpdated).toLocaleString("en-US", {
|
|
62
|
+
year: "numeric",
|
|
63
|
+
month: "short",
|
|
64
|
+
day: "numeric",
|
|
65
|
+
hour: "2-digit",
|
|
66
|
+
minute: "2-digit"
|
|
67
|
+
});
|
|
68
|
+
return `${c.bright}${c.blue}━━━ Track Status ━━━${c.reset}
|
|
69
|
+
|
|
70
|
+
${c.bright}Track:${c.reset} ${track.trackName}
|
|
71
|
+
${c.dim}ID: ${track.trackId}${c.reset}
|
|
72
|
+
|
|
73
|
+
${c.bright}Phase:${c.reset} ${c.cyan}${track.currentPhase}${c.reset}
|
|
74
|
+
|
|
75
|
+
${c.bright}Progress:${c.reset} ${c.green}${track.completedTasks}${c.reset}/${track.totalTasks} tasks ${c.dim}(${progress}%)${c.reset}${inProgressInfo}
|
|
76
|
+
${progressBar}
|
|
77
|
+
|
|
78
|
+
${c.gray}Last updated: ${formattedDate}${c.reset}
|
|
79
|
+
|
|
80
|
+
${c.bright}${c.blue}━━━━━━━━━━━━━━━━━━━━${c.reset}`;
|
|
81
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { formatTrackStatus, calculateProgress, generateProgressBar } from "./statusDisplay.js";
|
|
3
|
+
describe("statusDisplay", () => {
|
|
4
|
+
describe("calculateProgress", () => {
|
|
5
|
+
it("should calculate progress percentage correctly", () => {
|
|
6
|
+
expect(calculateProgress(3, 10)).toBe(30);
|
|
7
|
+
expect(calculateProgress(5, 5)).toBe(100);
|
|
8
|
+
expect(calculateProgress(0, 10)).toBe(0);
|
|
9
|
+
});
|
|
10
|
+
it("should handle zero total gracefully", () => {
|
|
11
|
+
expect(calculateProgress(0, 0)).toBe(0);
|
|
12
|
+
});
|
|
13
|
+
it("should round to nearest integer", () => {
|
|
14
|
+
expect(calculateProgress(1, 3)).toBe(33);
|
|
15
|
+
expect(calculateProgress(2, 3)).toBe(67);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
describe("generateProgressBar", () => {
|
|
19
|
+
it("should generate a progress bar with default length", () => {
|
|
20
|
+
const bar = generateProgressBar(50);
|
|
21
|
+
expect(bar).toHaveLength(20); // Default bar length
|
|
22
|
+
expect(bar).toContain("█");
|
|
23
|
+
expect(bar).toContain("░");
|
|
24
|
+
});
|
|
25
|
+
it("should generate full bar at 100%", () => {
|
|
26
|
+
const bar = generateProgressBar(100, 10);
|
|
27
|
+
expect(bar).toBe("██████████");
|
|
28
|
+
});
|
|
29
|
+
it("should generate empty bar at 0%", () => {
|
|
30
|
+
const bar = generateProgressBar(0, 10);
|
|
31
|
+
expect(bar).toBe("░░░░░░░░░░");
|
|
32
|
+
});
|
|
33
|
+
it("should handle custom bar length", () => {
|
|
34
|
+
const bar = generateProgressBar(50, 30);
|
|
35
|
+
expect(bar).toHaveLength(30);
|
|
36
|
+
});
|
|
37
|
+
it("should show correct filled/unfilled ratio", () => {
|
|
38
|
+
const bar = generateProgressBar(25, 8);
|
|
39
|
+
expect(bar).toBe("██░░░░░░"); // 25% of 8 = 2 filled
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
describe("formatTrackStatus", () => {
|
|
43
|
+
const sampleTrack = {
|
|
44
|
+
trackId: "track_test_123",
|
|
45
|
+
trackName: "Implement Authentication",
|
|
46
|
+
totalTasks: 10,
|
|
47
|
+
completedTasks: 7,
|
|
48
|
+
inProgressTasks: 1,
|
|
49
|
+
currentPhase: "Phase 2: Implementation",
|
|
50
|
+
lastUpdated: "2026-01-18T12:00:00Z"
|
|
51
|
+
};
|
|
52
|
+
it("should format a complete track status display", () => {
|
|
53
|
+
const status = formatTrackStatus(sampleTrack);
|
|
54
|
+
expect(status).toContain("Implement Authentication");
|
|
55
|
+
expect(status).toContain("Phase 2: Implementation");
|
|
56
|
+
expect(status).toMatch(/7.*\/.*10.*tasks/); // Match "7/10 tasks" with potential ANSI codes
|
|
57
|
+
expect(status).toContain("█"); // Progress bar
|
|
58
|
+
expect(status).toContain("Last updated:");
|
|
59
|
+
});
|
|
60
|
+
it("should show 100% completion for fully complete track", () => {
|
|
61
|
+
const completeTrack = {
|
|
62
|
+
...sampleTrack,
|
|
63
|
+
completedTasks: 10,
|
|
64
|
+
inProgressTasks: 0,
|
|
65
|
+
currentPhase: "Completed"
|
|
66
|
+
};
|
|
67
|
+
const status = formatTrackStatus(completeTrack);
|
|
68
|
+
expect(status).toContain("100%");
|
|
69
|
+
expect(status).toMatch(/10.*\/.*10.*tasks/); // Match "10/10 tasks" with potential ANSI codes
|
|
70
|
+
});
|
|
71
|
+
it("should indicate in-progress tasks", () => {
|
|
72
|
+
const status = formatTrackStatus(sampleTrack);
|
|
73
|
+
expect(status).toContain("1 in progress");
|
|
74
|
+
});
|
|
75
|
+
it("should handle track with no tasks", () => {
|
|
76
|
+
const emptyTrack = {
|
|
77
|
+
...sampleTrack,
|
|
78
|
+
totalTasks: 0,
|
|
79
|
+
completedTasks: 0,
|
|
80
|
+
inProgressTasks: 0
|
|
81
|
+
};
|
|
82
|
+
const status = formatTrackStatus(emptyTrack);
|
|
83
|
+
expect(status).toMatch(/0.*\/.*0.*tasks/); // Match "0/0 tasks" with potential ANSI codes
|
|
84
|
+
expect(status).toContain("0%");
|
|
85
|
+
});
|
|
86
|
+
it("should include color indicators in status (ANSI codes)", () => {
|
|
87
|
+
const status = formatTrackStatus(sampleTrack);
|
|
88
|
+
// Should contain ANSI escape codes for colors
|
|
89
|
+
expect(status).toMatch(/\x1b\[\d+m/); // Contains ANSI color codes
|
|
90
|
+
});
|
|
91
|
+
it("should format status with compact mode option", () => {
|
|
92
|
+
const status = formatTrackStatus(sampleTrack, { compact: true });
|
|
93
|
+
// Compact mode should be shorter (no progress bar or timestamps)
|
|
94
|
+
expect(status.split("\n").length).toBeLessThan(5);
|
|
95
|
+
});
|
|
96
|
+
it("should allow disabling color output", () => {
|
|
97
|
+
const status = formatTrackStatus(sampleTrack, { noColor: true });
|
|
98
|
+
// Should not contain ANSI escape codes
|
|
99
|
+
expect(status).not.toMatch(/\x1b\[\d+m/);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-conductor-cdd-plugin",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.19",
|
|
4
4
|
"description": "Context-Driven Development (CDD) plugin for OpenCode - Transform your AI coding workflow with structured specifications, plans, and implementation tracking",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"repository": {
|