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
|
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeAll } from 'vitest';
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
|
|
5
|
-
describe('setup.json prompt structure', () => {
|
|
5
|
+
describe('setup.json prompt structure (interactive, code-driven)', () => {
|
|
6
6
|
let setupPrompt: string;
|
|
7
7
|
|
|
8
8
|
beforeAll(() => {
|
|
@@ -11,158 +11,55 @@ describe('setup.json prompt structure', () => {
|
|
|
11
11
|
setupPrompt = setupJson.prompt;
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
it('should contain Section 2.1 (Product Guide) with autogenerate mandate', () => {
|
|
19
|
-
expect(setupPrompt).toContain('### 2.1 Generate Product Guide');
|
|
20
|
-
|
|
21
|
-
const section21Start = setupPrompt.indexOf('### 2.1 Generate Product Guide');
|
|
22
|
-
const section22Start = setupPrompt.indexOf('### 2.2 Generate Product Guidelines');
|
|
23
|
-
const section21 = setupPrompt.substring(section21Start, section22Start);
|
|
24
|
-
|
|
25
|
-
expect(section21).toMatch(mandatoryInstructionPattern);
|
|
26
|
-
expect(section21).toContain(requiredText);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('should contain Section 2.2 (Product Guidelines) with autogenerate mandate', () => {
|
|
30
|
-
expect(setupPrompt).toContain('### 2.2 Generate Product Guidelines');
|
|
31
|
-
|
|
32
|
-
const section22Start = setupPrompt.indexOf('### 2.2 Generate Product Guidelines');
|
|
33
|
-
const section23Start = setupPrompt.indexOf('### 2.3 Define Technology Stack');
|
|
34
|
-
const section22 = setupPrompt.substring(section22Start, section23Start);
|
|
35
|
-
|
|
36
|
-
expect(section22).toMatch(mandatoryInstructionPattern);
|
|
37
|
-
expect(section22).toContain(requiredText);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('should contain Section 2.3 (Tech Stack) with autogenerate mandate', () => {
|
|
41
|
-
expect(setupPrompt).toContain('### 2.3 Define Technology Stack');
|
|
42
|
-
|
|
43
|
-
const section23Start = setupPrompt.indexOf('### 2.3 Define Technology Stack');
|
|
44
|
-
const section24Start = setupPrompt.indexOf('### 2.4 Select Code Styleguides');
|
|
45
|
-
const section23 = setupPrompt.substring(section23Start, section24Start);
|
|
46
|
-
|
|
47
|
-
expect(section23).toMatch(mandatoryInstructionPattern);
|
|
48
|
-
expect(section23).toContain(requiredText);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should contain Section 2.4 (Code Styleguides) with autogenerate mandate', () => {
|
|
52
|
-
expect(setupPrompt).toContain('### 2.4 Select Code Styleguides');
|
|
53
|
-
|
|
54
|
-
const section24Start = setupPrompt.indexOf('### 2.4 Select Code Styleguides');
|
|
55
|
-
const section25Start = setupPrompt.indexOf('### 2.5 Define Workflow');
|
|
56
|
-
const section24 = setupPrompt.substring(section24Start, section25Start);
|
|
57
|
-
|
|
58
|
-
expect(section24).toMatch(mandatoryInstructionPattern);
|
|
59
|
-
expect(section24).toContain(requiredText);
|
|
60
|
-
});
|
|
14
|
+
it('should have description and prompt fields', () => {
|
|
15
|
+
const setupJsonPath = path.join(__dirname, 'setup.json');
|
|
16
|
+
const setupJson = JSON.parse(fs.readFileSync(setupJsonPath, 'utf-8'));
|
|
61
17
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
const section25 = setupPrompt.substring(section25Start, section30Start);
|
|
68
|
-
|
|
69
|
-
expect(section25).toMatch(mandatoryInstructionPattern);
|
|
70
|
-
expect(section25).toContain(requiredText);
|
|
71
|
-
});
|
|
18
|
+
expect(setupJson).toHaveProperty('description');
|
|
19
|
+
expect(setupJson.description).toBeTruthy();
|
|
20
|
+
expect(setupJson).toHaveProperty('prompt');
|
|
21
|
+
expect(typeof setupJson.prompt).toBe('string');
|
|
22
|
+
expect(setupJson.prompt).toBeTruthy();
|
|
72
23
|
});
|
|
73
24
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
const section22Start = setupPrompt.indexOf('### 2.2 Generate Product Guidelines');
|
|
80
|
-
const section21 = setupPrompt.substring(section21Start, section22Start);
|
|
81
|
-
|
|
82
|
-
expect(section21).toMatch(sequentialPattern);
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('should enforce sequential questioning in Section 2.2', () => {
|
|
86
|
-
const section22Start = setupPrompt.indexOf('### 2.2 Generate Product Guidelines');
|
|
87
|
-
const section23Start = setupPrompt.indexOf('### 2.3 Define Technology Stack');
|
|
88
|
-
const section22 = setupPrompt.substring(section22Start, section23Start);
|
|
89
|
-
|
|
90
|
-
expect(section22).toMatch(sequentialPattern);
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('should enforce sequential questioning in Section 2.4', () => {
|
|
94
|
-
const section24Start = setupPrompt.indexOf('### 2.4 Select Code Styleguides');
|
|
95
|
-
const section25Start = setupPrompt.indexOf('### 2.5 Define Workflow');
|
|
96
|
-
const section24 = setupPrompt.substring(section24Start, section25Start);
|
|
97
|
-
|
|
98
|
-
expect(section24).toMatch(sequentialPattern);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('should enforce sequential questioning in Section 2.5', () => {
|
|
102
|
-
const section25Start = setupPrompt.indexOf('### 2.5 Define Workflow');
|
|
103
|
-
const section30Start = setupPrompt.indexOf('## 3.0 INITIAL TRACK');
|
|
104
|
-
const section25 = setupPrompt.substring(section25Start, section30Start);
|
|
105
|
-
|
|
106
|
-
expect(section25).toMatch(sequentialPattern);
|
|
107
|
-
});
|
|
25
|
+
it('should describe interactive, auto-generated setup', () => {
|
|
26
|
+
expect(setupPrompt).toContain('CDD SETUP');
|
|
27
|
+
expect(setupPrompt.toLowerCase()).toContain('interactive');
|
|
28
|
+
expect(setupPrompt.toLowerCase()).toContain('auto');
|
|
29
|
+
expect(setupPrompt.toLowerCase()).toContain('code-driven');
|
|
108
30
|
});
|
|
109
31
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
expect(setupJson).toHaveProperty('description');
|
|
116
|
-
expect(setupJson.description).toBeTruthy();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('should have prompt field', () => {
|
|
120
|
-
const setupJsonPath = path.join(__dirname, 'setup.json');
|
|
121
|
-
const setupJson = JSON.parse(fs.readFileSync(setupJsonPath, 'utf-8'));
|
|
122
|
-
|
|
123
|
-
expect(setupJson).toHaveProperty('prompt');
|
|
124
|
-
expect(setupJson.prompt).toBeTruthy();
|
|
125
|
-
expect(typeof setupJson.prompt).toBe('string');
|
|
126
|
-
});
|
|
32
|
+
it('should reference the TypeScript implementation entry point', () => {
|
|
33
|
+
expect(setupPrompt).toContain('src/utils/interactiveSetup.ts');
|
|
34
|
+
expect(setupPrompt).toContain('executeInteractiveSetup');
|
|
35
|
+
expect(setupPrompt).toContain('generateSetupSummary');
|
|
36
|
+
});
|
|
127
37
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
expect(setupPrompt).toContain('## 4.0 COMPLETION');
|
|
135
|
-
});
|
|
38
|
+
it('should mandate 5 options per question (A-E)', () => {
|
|
39
|
+
expect(setupPrompt).toContain('A-C');
|
|
40
|
+
expect(setupPrompt).toContain('D');
|
|
41
|
+
expect(setupPrompt).toContain('E');
|
|
42
|
+
expect(setupPrompt).toContain('A, B, C, D, E');
|
|
43
|
+
});
|
|
136
44
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
});
|
|
45
|
+
it('should mention all 5 output documents', () => {
|
|
46
|
+
expect(setupPrompt).toContain('conductor-cdd/product.md');
|
|
47
|
+
expect(setupPrompt).toContain('conductor-cdd/guidelines.md');
|
|
48
|
+
expect(setupPrompt).toContain('conductor-cdd/tech-stack.md');
|
|
49
|
+
expect(setupPrompt).toContain('conductor-cdd/styleguides.md');
|
|
50
|
+
expect(setupPrompt).toContain('conductor-cdd/workflow.md');
|
|
144
51
|
});
|
|
145
52
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
expect(setupPrompt).toContain('setup_state.json');
|
|
150
|
-
expect(setupPrompt).toContain('2.1_product_guide');
|
|
151
|
-
expect(setupPrompt).toContain('2.2_product_guidelines');
|
|
152
|
-
expect(setupPrompt).toContain('2.3_tech_stack');
|
|
153
|
-
expect(setupPrompt).toContain('2.4_code_styleguides');
|
|
154
|
-
expect(setupPrompt).toContain('2.5_workflow');
|
|
155
|
-
});
|
|
53
|
+
it('should describe resume behavior and expected checkpoints', () => {
|
|
54
|
+
expect(setupPrompt).toContain('conductor-cdd/setup_state.json');
|
|
55
|
+
expect(setupPrompt).toContain('last_successful_step');
|
|
156
56
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
57
|
+
expect(setupPrompt).toContain('2.1_product_guide');
|
|
58
|
+
expect(setupPrompt).toContain('2.2_product_guidelines');
|
|
59
|
+
expect(setupPrompt).toContain('2.3_tech_stack');
|
|
60
|
+
expect(setupPrompt).toContain('2.4_code_styleguides');
|
|
61
|
+
expect(setupPrompt).toContain('2.5_workflow');
|
|
162
62
|
|
|
163
|
-
|
|
164
|
-
expect(setupPrompt).not.toContain('MUST autogenerate');
|
|
165
|
-
expect(setupPrompt).not.toContain('MUST use autogenerate');
|
|
166
|
-
});
|
|
63
|
+
expect(setupPrompt).toContain('cdd:setup');
|
|
167
64
|
});
|
|
168
65
|
});
|
|
@@ -2,6 +2,7 @@ import { describe, it, expect } from 'vitest';
|
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
4
|
describe('Rebrand Integration Tests', () => {
|
|
5
|
+
const LEGACY_AGENT_NAME = 'orche' + 'strator';
|
|
5
6
|
describe('Package Metadata', () => {
|
|
6
7
|
it('should have correct package name', () => {
|
|
7
8
|
const pkgPath = path.join(process.cwd(), 'package.json');
|
|
@@ -17,7 +18,7 @@ describe('Rebrand Integration Tests', () => {
|
|
|
17
18
|
const pkgPath = path.join(process.cwd(), 'package.json');
|
|
18
19
|
const pkg = JSON.parse(fs.readFileSync(pkgPath, 'utf-8'));
|
|
19
20
|
expect(pkg.repository.url).toContain('opencode-conductor-cdd');
|
|
20
|
-
expect(pkg.repository.url).not.toContain('opencode-
|
|
21
|
+
expect(pkg.repository.url).not.toContain('opencode-' + LEGACY_AGENT_NAME);
|
|
21
22
|
});
|
|
22
23
|
});
|
|
23
24
|
describe('Prompt Files Structure', () => {
|
|
@@ -25,9 +26,9 @@ describe('Rebrand Integration Tests', () => {
|
|
|
25
26
|
const cddPromptsPath = path.join(process.cwd(), 'src/prompts/cdd');
|
|
26
27
|
expect(fs.existsSync(cddPromptsPath)).toBe(true);
|
|
27
28
|
});
|
|
28
|
-
it('should NOT have
|
|
29
|
-
const
|
|
30
|
-
expect(fs.existsSync(
|
|
29
|
+
it('should NOT have legacy prompts directory', () => {
|
|
30
|
+
const legacyPromptsPath = path.join(process.cwd(), 'src/prompts/' + LEGACY_AGENT_NAME);
|
|
31
|
+
expect(fs.existsSync(legacyPromptsPath)).toBe(false);
|
|
31
32
|
});
|
|
32
33
|
it('should have all required CDD prompt files', () => {
|
|
33
34
|
const cddPromptsPath = path.join(process.cwd(), 'src/prompts/cdd');
|
|
@@ -41,9 +42,9 @@ describe('Rebrand Integration Tests', () => {
|
|
|
41
42
|
const cddAgentPath = path.join(process.cwd(), 'src/prompts/agent/cdd.md');
|
|
42
43
|
expect(fs.existsSync(cddAgentPath)).toBe(true);
|
|
43
44
|
});
|
|
44
|
-
it('should NOT have
|
|
45
|
-
const
|
|
46
|
-
expect(fs.existsSync(
|
|
45
|
+
it('should NOT have legacy agent prompt file', () => {
|
|
46
|
+
const legacyAgentPath = path.join(process.cwd(), 'src/prompts/agent/' + LEGACY_AGENT_NAME + '.md');
|
|
47
|
+
expect(fs.existsSync(legacyAgentPath)).toBe(false);
|
|
47
48
|
});
|
|
48
49
|
});
|
|
49
50
|
describe('Source Code References', () => {
|
|
@@ -53,7 +54,7 @@ describe('Rebrand Integration Tests', () => {
|
|
|
53
54
|
expect(indexContent).toContain('./prompts/cdd/implement.json');
|
|
54
55
|
expect(indexContent).toContain('./prompts/cdd/newTrack.json');
|
|
55
56
|
expect(indexContent).toContain('./prompts/cdd/setup.json');
|
|
56
|
-
expect(indexContent).not.toContain('./prompts/
|
|
57
|
+
expect(indexContent).not.toContain('./prompts/' + LEGACY_AGENT_NAME + '/');
|
|
57
58
|
});
|
|
58
59
|
it('should register cdd: commands in index.ts', () => {
|
|
59
60
|
const indexPath = path.join(process.cwd(), 'src/index.ts');
|
|
@@ -63,7 +64,7 @@ describe('Rebrand Integration Tests', () => {
|
|
|
63
64
|
expect(indexContent).toContain('"cdd:setup"');
|
|
64
65
|
expect(indexContent).toContain('"cdd:status"');
|
|
65
66
|
expect(indexContent).toContain('"cdd:revert"');
|
|
66
|
-
expect(indexContent).not.toContain('"
|
|
67
|
+
expect(indexContent).not.toContain('"' + LEGACY_AGENT_NAME + ':');
|
|
67
68
|
});
|
|
68
69
|
it('should reference conductor-cdd directory in index.ts', () => {
|
|
69
70
|
const indexPath = path.join(process.cwd(), 'src/index.ts');
|
|
@@ -81,7 +82,7 @@ describe('Rebrand Integration Tests', () => {
|
|
|
81
82
|
const stateManagerPath = path.join(process.cwd(), 'src/utils/stateManager.ts');
|
|
82
83
|
const content = fs.readFileSync(stateManagerPath, 'utf-8');
|
|
83
84
|
expect(content).toContain('conductor-cdd');
|
|
84
|
-
expect(content).not.toContain('
|
|
85
|
+
expect(content).not.toContain(LEGACY_AGENT_NAME + '/');
|
|
85
86
|
});
|
|
86
87
|
it('should use detectCDDConfig function name', () => {
|
|
87
88
|
const configDetectionPath = path.join(process.cwd(), 'src/utils/configDetection.ts');
|
|
@@ -93,7 +94,7 @@ describe('Rebrand Integration Tests', () => {
|
|
|
93
94
|
const commitMessagesPath = path.join(process.cwd(), 'src/utils/commitMessages.ts');
|
|
94
95
|
const content = fs.readFileSync(commitMessagesPath, 'utf-8');
|
|
95
96
|
expect(content).toContain('cdd(');
|
|
96
|
-
expect(content).not.toContain('
|
|
97
|
+
expect(content).not.toContain(LEGACY_AGENT_NAME + '(');
|
|
97
98
|
});
|
|
98
99
|
});
|
|
99
100
|
describe('README Documentation', () => {
|
|
@@ -109,10 +110,10 @@ describe('Rebrand Integration Tests', () => {
|
|
|
109
110
|
expect(content).toContain('/cdd:newTrack');
|
|
110
111
|
expect(content).toContain('/cdd:implement');
|
|
111
112
|
});
|
|
112
|
-
it('should NOT reference
|
|
113
|
+
it('should NOT reference legacy commands in README', () => {
|
|
113
114
|
const readmePath = path.join(process.cwd(), 'README.md');
|
|
114
115
|
const content = fs.readFileSync(readmePath, 'utf-8');
|
|
115
|
-
expect(content).not.toContain('/
|
|
116
|
+
expect(content).not.toContain('/' + LEGACY_AGENT_NAME + ':');
|
|
116
117
|
});
|
|
117
118
|
});
|
|
118
119
|
describe('Build Scripts', () => {
|
|
@@ -120,7 +121,7 @@ describe('Rebrand Integration Tests', () => {
|
|
|
120
121
|
const scriptPath = path.join(process.cwd(), 'scripts/convert-legacy.cjs');
|
|
121
122
|
const content = fs.readFileSync(scriptPath, 'utf-8');
|
|
122
123
|
expect(content).toContain('src/prompts/cdd');
|
|
123
|
-
expect(content).not.toContain('src/prompts/
|
|
124
|
+
expect(content).not.toContain('src/prompts/' + LEGACY_AGENT_NAME);
|
|
124
125
|
});
|
|
125
126
|
});
|
|
126
127
|
});
|
|
@@ -14,6 +14,8 @@ const SLIM_AGENT_MAPPING = {
|
|
|
14
14
|
'librarian': 'librarian',
|
|
15
15
|
// Agents without slim equivalent
|
|
16
16
|
'sisyphus': null,
|
|
17
|
+
// Legacy name (oh-my-opencode-slim compatibility)
|
|
18
|
+
// See: https://github.com/alvinunreal/oh-my-opencode-slim
|
|
17
19
|
'orchestrator': null,
|
|
18
20
|
};
|
|
19
21
|
/**
|
|
@@ -23,3 +23,31 @@ export interface TrackPlan {
|
|
|
23
23
|
completed: boolean;
|
|
24
24
|
}[];
|
|
25
25
|
}
|
|
26
|
+
/**
|
|
27
|
+
* Parses metadata.json content and validates required fields
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseMetadata(jsonContent: string): TrackMetadata;
|
|
30
|
+
/**
|
|
31
|
+
* Checks if all tasks in a plan.md file are marked as completed
|
|
32
|
+
* Looks for task markers: [x] = complete, [ ] = incomplete, [~] = in-progress
|
|
33
|
+
*/
|
|
34
|
+
export declare function isPlanFullyCompleted(planContent: string): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Determines if a track is finished by checking both metadata status and plan completion
|
|
37
|
+
*/
|
|
38
|
+
export declare function isTrackFinished(metadata: TrackMetadata, planContent: string): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Moves a track directory from tracks/ to archive/
|
|
41
|
+
* Creates the archive directory if it doesn't exist
|
|
42
|
+
*/
|
|
43
|
+
export declare function moveTrackToArchive(projectRoot: string, trackId: string): void;
|
|
44
|
+
/**
|
|
45
|
+
* Removes a track entry from tracks.md registry file
|
|
46
|
+
* Handles the track block and its surrounding separator lines
|
|
47
|
+
*/
|
|
48
|
+
export declare function removeTrackFromRegistry(tracksFilePath: string, trackId: string): void;
|
|
49
|
+
/**
|
|
50
|
+
* Main function to archive all completed tracks in a project
|
|
51
|
+
* Scans tracks/ directory, identifies completed tracks, and archives them
|
|
52
|
+
*/
|
|
53
|
+
export declare function archiveCompletedTracks(projectRoot: string): ArchiveResult;
|
|
@@ -1 +1,154 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Parses metadata.json content and validates required fields
|
|
3
|
+
*/
|
|
4
|
+
export function parseMetadata(jsonContent) {
|
|
5
|
+
const data = JSON.parse(jsonContent);
|
|
6
|
+
// Validate required fields
|
|
7
|
+
const requiredFields = ['track_id', 'type', 'status', 'created_at', 'updated_at', 'description'];
|
|
8
|
+
for (const field of requiredFields) {
|
|
9
|
+
if (!(field in data)) {
|
|
10
|
+
throw new Error(`Invalid metadata: missing required field '${field}'`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return data;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Checks if all tasks in a plan.md file are marked as completed
|
|
17
|
+
* Looks for task markers: [x] = complete, [ ] = incomplete, [~] = in-progress
|
|
18
|
+
*/
|
|
19
|
+
export function isPlanFullyCompleted(planContent) {
|
|
20
|
+
// Match all task markers: - [x], - [ ], - [~]
|
|
21
|
+
const taskRegex = /^[\s]*-\s*\[([x ~]|\s)\]/gm;
|
|
22
|
+
const matches = planContent.match(taskRegex);
|
|
23
|
+
// If no tasks found, consider it complete (no work to do)
|
|
24
|
+
if (!matches || matches.length === 0) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
// Check if all tasks are marked with [x]
|
|
28
|
+
return matches.every(match => match.includes('[x]'));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Determines if a track is finished by checking both metadata status and plan completion
|
|
32
|
+
*/
|
|
33
|
+
export function isTrackFinished(metadata, planContent) {
|
|
34
|
+
const isMetadataComplete = metadata.status === 'completed';
|
|
35
|
+
const isPlanComplete = isPlanFullyCompleted(planContent);
|
|
36
|
+
return isMetadataComplete && isPlanComplete;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Moves a track directory from tracks/ to archive/
|
|
40
|
+
* Creates the archive directory if it doesn't exist
|
|
41
|
+
*/
|
|
42
|
+
export function moveTrackToArchive(projectRoot, trackId) {
|
|
43
|
+
const { existsSync, mkdirSync } = require('fs');
|
|
44
|
+
const { join } = require('path');
|
|
45
|
+
const { execSync } = require('child_process');
|
|
46
|
+
const tracksDir = join(projectRoot, 'conductor-cdd', 'tracks');
|
|
47
|
+
const archiveDir = join(projectRoot, 'conductor-cdd', 'archive');
|
|
48
|
+
const sourcePath = join(tracksDir, trackId);
|
|
49
|
+
const destPath = join(archiveDir, trackId);
|
|
50
|
+
// Verify source exists
|
|
51
|
+
if (!existsSync(sourcePath)) {
|
|
52
|
+
throw new Error(`Track directory not found: ${sourcePath}`);
|
|
53
|
+
}
|
|
54
|
+
// Create archive directory if needed
|
|
55
|
+
if (!existsSync(archiveDir)) {
|
|
56
|
+
mkdirSync(archiveDir, { recursive: true });
|
|
57
|
+
}
|
|
58
|
+
// Use mv command to move the directory
|
|
59
|
+
execSync(`mv "${sourcePath}" "${destPath}"`);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Removes a track entry from tracks.md registry file
|
|
63
|
+
* Handles the track block and its surrounding separator lines
|
|
64
|
+
*/
|
|
65
|
+
export function removeTrackFromRegistry(tracksFilePath, trackId) {
|
|
66
|
+
const { readFileSync, writeFileSync } = require('fs');
|
|
67
|
+
let content = readFileSync(tracksFilePath, 'utf-8');
|
|
68
|
+
// Split by separator to get track blocks
|
|
69
|
+
const separator = '\n---\n';
|
|
70
|
+
const parts = content.split(separator);
|
|
71
|
+
// Filter out the block containing the trackId
|
|
72
|
+
const filteredParts = parts.filter((part) => {
|
|
73
|
+
// Check if this part contains a link to the track we want to remove
|
|
74
|
+
return !part.includes(`./tracks/${trackId}/`);
|
|
75
|
+
});
|
|
76
|
+
// If nothing was filtered, track wasn't found - that's okay
|
|
77
|
+
if (filteredParts.length === parts.length) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
// Rejoin with separator
|
|
81
|
+
content = filteredParts.join(separator);
|
|
82
|
+
writeFileSync(tracksFilePath, content, 'utf-8');
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Main function to archive all completed tracks in a project
|
|
86
|
+
* Scans tracks/ directory, identifies completed tracks, and archives them
|
|
87
|
+
*/
|
|
88
|
+
export function archiveCompletedTracks(projectRoot) {
|
|
89
|
+
const { readdirSync, readFileSync, existsSync } = require('fs');
|
|
90
|
+
const { join } = require('path');
|
|
91
|
+
const result = {
|
|
92
|
+
archived: [],
|
|
93
|
+
skipped: [],
|
|
94
|
+
errors: []
|
|
95
|
+
};
|
|
96
|
+
const cddDir = join(projectRoot, 'conductor-cdd');
|
|
97
|
+
const tracksDir = join(cddDir, 'tracks');
|
|
98
|
+
const tracksFile = join(cddDir, 'tracks.md');
|
|
99
|
+
// Check if tracks directory exists
|
|
100
|
+
if (!existsSync(tracksDir)) {
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
// Get all track directories
|
|
104
|
+
const trackDirs = readdirSync(tracksDir, { withFileTypes: true })
|
|
105
|
+
.filter((dirent) => dirent.isDirectory())
|
|
106
|
+
.map((dirent) => dirent.name);
|
|
107
|
+
// Process each track
|
|
108
|
+
for (const trackId of trackDirs) {
|
|
109
|
+
try {
|
|
110
|
+
const trackPath = join(tracksDir, trackId);
|
|
111
|
+
const metadataPath = join(trackPath, 'metadata.json');
|
|
112
|
+
const planPath = join(trackPath, 'plan.md');
|
|
113
|
+
// Check if required files exist
|
|
114
|
+
if (!existsSync(metadataPath)) {
|
|
115
|
+
result.errors.push({
|
|
116
|
+
trackId,
|
|
117
|
+
error: 'metadata.json not found'
|
|
118
|
+
});
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (!existsSync(planPath)) {
|
|
122
|
+
result.errors.push({
|
|
123
|
+
trackId,
|
|
124
|
+
error: 'plan.md not found'
|
|
125
|
+
});
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
// Parse metadata and plan
|
|
129
|
+
const metadataContent = readFileSync(metadataPath, 'utf-8');
|
|
130
|
+
const planContent = readFileSync(planPath, 'utf-8');
|
|
131
|
+
const metadata = parseMetadata(metadataContent);
|
|
132
|
+
// Check if track is finished
|
|
133
|
+
if (isTrackFinished(metadata, planContent)) {
|
|
134
|
+
// Archive the track
|
|
135
|
+
moveTrackToArchive(projectRoot, trackId);
|
|
136
|
+
// Remove from tracks.md if it exists
|
|
137
|
+
if (existsSync(tracksFile)) {
|
|
138
|
+
removeTrackFromRegistry(tracksFile, trackId);
|
|
139
|
+
}
|
|
140
|
+
result.archived.push(trackId);
|
|
141
|
+
}
|
|
142
|
+
else {
|
|
143
|
+
result.skipped.push(trackId);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
result.errors.push({
|
|
148
|
+
trackId,
|
|
149
|
+
error: error instanceof Error ? error.message : String(error)
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|