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.
Files changed (55) hide show
  1. package/dist/prompts/agent/cdd.md +16 -16
  2. package/dist/prompts/agent/implementer.md +5 -5
  3. package/dist/prompts/agent.md +7 -7
  4. package/dist/prompts/cdd/implement.json +1 -1
  5. package/dist/prompts/cdd/revert.json +1 -1
  6. package/dist/prompts/cdd/setup.json +2 -2
  7. package/dist/prompts/cdd/setup.test.js +40 -118
  8. package/dist/prompts/cdd/setup.test.ts +40 -143
  9. package/dist/test/integration/rebrand.test.js +15 -14
  10. package/dist/utils/agentMapping.js +2 -0
  11. package/dist/utils/archive-tracks.d.ts +28 -0
  12. package/dist/utils/archive-tracks.js +154 -1
  13. package/dist/utils/archive-tracks.test.d.ts +1 -0
  14. package/dist/utils/archive-tracks.test.js +495 -0
  15. package/dist/utils/codebaseAnalysis.d.ts +61 -0
  16. package/dist/utils/codebaseAnalysis.js +429 -0
  17. package/dist/utils/codebaseAnalysis.test.d.ts +1 -0
  18. package/dist/utils/codebaseAnalysis.test.js +556 -0
  19. package/dist/utils/documentGeneration.d.ts +97 -0
  20. package/dist/utils/documentGeneration.js +301 -0
  21. package/dist/utils/documentGeneration.test.d.ts +1 -0
  22. package/dist/utils/documentGeneration.test.js +380 -0
  23. package/dist/utils/interactiveMenu.d.ts +56 -0
  24. package/dist/utils/interactiveMenu.js +144 -0
  25. package/dist/utils/interactiveMenu.test.d.ts +1 -0
  26. package/dist/utils/interactiveMenu.test.js +231 -0
  27. package/dist/utils/interactiveSetup.d.ts +43 -0
  28. package/dist/utils/interactiveSetup.js +131 -0
  29. package/dist/utils/interactiveSetup.test.d.ts +1 -0
  30. package/dist/utils/interactiveSetup.test.js +124 -0
  31. package/dist/utils/metadataTracker.d.ts +39 -0
  32. package/dist/utils/metadataTracker.js +105 -0
  33. package/dist/utils/metadataTracker.test.d.ts +1 -0
  34. package/dist/utils/metadataTracker.test.js +265 -0
  35. package/dist/utils/planParser.d.ts +25 -0
  36. package/dist/utils/planParser.js +107 -0
  37. package/dist/utils/planParser.test.d.ts +1 -0
  38. package/dist/utils/planParser.test.js +119 -0
  39. package/dist/utils/projectMaturity.d.ts +53 -0
  40. package/dist/utils/projectMaturity.js +179 -0
  41. package/dist/utils/projectMaturity.test.d.ts +1 -0
  42. package/dist/utils/projectMaturity.test.js +298 -0
  43. package/dist/utils/questionGenerator.d.ts +51 -0
  44. package/dist/utils/questionGenerator.js +535 -0
  45. package/dist/utils/questionGenerator.test.d.ts +1 -0
  46. package/dist/utils/questionGenerator.test.js +328 -0
  47. package/dist/utils/setupIntegration.d.ts +72 -0
  48. package/dist/utils/setupIntegration.js +179 -0
  49. package/dist/utils/setupIntegration.test.d.ts +1 -0
  50. package/dist/utils/setupIntegration.test.js +344 -0
  51. package/dist/utils/statusDisplay.d.ts +35 -0
  52. package/dist/utils/statusDisplay.js +81 -0
  53. package/dist/utils/statusDisplay.test.d.ts +1 -0
  54. package/dist/utils/statusDisplay.test.js +102 -0
  55. 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.17",
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": {