@projitive/mcp 2.0.3 → 2.0.4
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/output/package.json +8 -2
- package/output/source/common/artifacts.js +1 -1
- package/output/source/common/artifacts.test.js +11 -11
- package/output/source/common/errors.js +19 -19
- package/output/source/common/files.js +11 -11
- package/output/source/common/files.test.js +14 -14
- package/output/source/common/index.js +10 -10
- package/output/source/common/linter.js +27 -27
- package/output/source/common/linter.test.js +9 -9
- package/output/source/common/markdown.js +3 -3
- package/output/source/common/markdown.test.js +15 -15
- package/output/source/common/response.js +74 -74
- package/output/source/common/response.test.js +30 -30
- package/output/source/common/store.js +40 -40
- package/output/source/common/store.test.js +72 -72
- package/output/source/common/types.js +3 -3
- package/output/source/common/utils.js +8 -8
- package/output/source/index.js +16 -16
- package/output/source/index.test.js +64 -64
- package/output/source/prompts/index.js +3 -3
- package/output/source/prompts/quickStart.js +96 -96
- package/output/source/prompts/taskDiscovery.js +184 -184
- package/output/source/prompts/taskExecution.js +148 -148
- package/output/source/resources/designs.js +26 -26
- package/output/source/resources/designs.test.js +88 -88
- package/output/source/resources/governance.js +19 -19
- package/output/source/resources/index.js +2 -2
- package/output/source/resources/readme.js +7 -7
- package/output/source/resources/readme.test.js +113 -113
- package/output/source/resources/reports.js +10 -10
- package/output/source/resources/reports.test.js +83 -83
- package/output/source/tools/index.js +3 -3
- package/output/source/tools/project.js +191 -191
- package/output/source/tools/project.test.js +174 -173
- package/output/source/tools/roadmap.js +110 -95
- package/output/source/tools/roadmap.test.js +54 -46
- package/output/source/tools/task.js +305 -277
- package/output/source/tools/task.test.js +117 -110
- package/output/source/types.js +22 -22
- package/package.json +8 -2
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import fs from
|
|
2
|
-
import os from
|
|
3
|
-
import path from
|
|
4
|
-
import { afterEach, describe, expect, it, vi } from
|
|
5
|
-
import { discoverProjects, discoverProjectsAcrossRoots, hasProjectMarker, initializeProjectStructure, resolveGovernanceDir, resolveScanRoots, resolveScanDepth, toProjectPath, registerProjectTools } from
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
+
import { discoverProjects, discoverProjectsAcrossRoots, hasProjectMarker, initializeProjectStructure, resolveGovernanceDir, resolveScanRoots, resolveScanDepth, toProjectPath, registerProjectTools } from './project.js';
|
|
6
6
|
const tempPaths = [];
|
|
7
7
|
async function createTempDir() {
|
|
8
|
-
const dir = await fs.mkdtemp(path.join(os.tmpdir(),
|
|
8
|
+
const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'projitive-mcp-test-'));
|
|
9
9
|
tempPaths.push(dir);
|
|
10
10
|
return dir;
|
|
11
11
|
}
|
|
@@ -15,350 +15,351 @@ afterEach(async () => {
|
|
|
15
15
|
}));
|
|
16
16
|
vi.restoreAllMocks();
|
|
17
17
|
});
|
|
18
|
-
describe(
|
|
19
|
-
describe(
|
|
20
|
-
it(
|
|
18
|
+
describe('projitive module', () => {
|
|
19
|
+
describe('hasProjectMarker', () => {
|
|
20
|
+
it('does not treat marker directory as a valid project marker', async () => {
|
|
21
21
|
const root = await createTempDir();
|
|
22
|
-
const dirMarkerPath = path.join(root,
|
|
22
|
+
const dirMarkerPath = path.join(root, '.projitive');
|
|
23
23
|
await fs.mkdir(dirMarkerPath, { recursive: true });
|
|
24
24
|
const hasMarker = await hasProjectMarker(root);
|
|
25
25
|
expect(hasMarker).toBe(false);
|
|
26
26
|
});
|
|
27
|
-
it(
|
|
27
|
+
it('returns true when .projitive marker file exists', async () => {
|
|
28
28
|
const root = await createTempDir();
|
|
29
|
-
const markerPath = path.join(root,
|
|
30
|
-
await fs.writeFile(markerPath,
|
|
29
|
+
const markerPath = path.join(root, '.projitive');
|
|
30
|
+
await fs.writeFile(markerPath, '', 'utf-8');
|
|
31
31
|
const hasMarker = await hasProjectMarker(root);
|
|
32
32
|
expect(hasMarker).toBe(true);
|
|
33
33
|
});
|
|
34
|
-
it(
|
|
34
|
+
it('returns false when .projitive marker file does not exist', async () => {
|
|
35
35
|
const root = await createTempDir();
|
|
36
36
|
const hasMarker = await hasProjectMarker(root);
|
|
37
37
|
expect(hasMarker).toBe(false);
|
|
38
38
|
});
|
|
39
|
-
it(
|
|
39
|
+
it('handles fs.stat errors gracefully', async () => {
|
|
40
40
|
const root = await createTempDir();
|
|
41
|
-
vi.spyOn(fs,
|
|
41
|
+
vi.spyOn(fs, 'stat').mockRejectedValueOnce(new Error('Permission denied'));
|
|
42
42
|
const hasMarker = await hasProjectMarker(root);
|
|
43
43
|
expect(hasMarker).toBe(false);
|
|
44
44
|
});
|
|
45
45
|
});
|
|
46
|
-
describe(
|
|
47
|
-
it(
|
|
46
|
+
describe('resolveGovernanceDir', () => {
|
|
47
|
+
it('resolves governance dir by walking upwards for .projitive', async () => {
|
|
48
48
|
const root = await createTempDir();
|
|
49
|
-
const governanceDir = path.join(root,
|
|
50
|
-
const deepDir = path.join(governanceDir,
|
|
49
|
+
const governanceDir = path.join(root, 'repo', 'governance');
|
|
50
|
+
const deepDir = path.join(governanceDir, 'nested', 'module');
|
|
51
51
|
await fs.mkdir(deepDir, { recursive: true });
|
|
52
|
-
await fs.writeFile(path.join(governanceDir,
|
|
52
|
+
await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
|
|
53
53
|
const resolved = await resolveGovernanceDir(deepDir);
|
|
54
54
|
expect(resolved).toBe(governanceDir);
|
|
55
55
|
});
|
|
56
|
-
it(
|
|
56
|
+
it('resolves nested default governance dir when input path is project root', async () => {
|
|
57
57
|
const root = await createTempDir();
|
|
58
|
-
const projectRoot = path.join(root,
|
|
59
|
-
const governanceDir = path.join(projectRoot,
|
|
58
|
+
const projectRoot = path.join(root, 'repo');
|
|
59
|
+
const governanceDir = path.join(projectRoot, '.projitive');
|
|
60
60
|
await fs.mkdir(governanceDir, { recursive: true });
|
|
61
|
-
await fs.writeFile(path.join(governanceDir,
|
|
61
|
+
await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
|
|
62
62
|
const resolved = await resolveGovernanceDir(projectRoot);
|
|
63
63
|
expect(resolved).toBe(governanceDir);
|
|
64
64
|
});
|
|
65
|
-
it(
|
|
65
|
+
it('resolves nested custom governance dir when input path is project root', async () => {
|
|
66
66
|
const root = await createTempDir();
|
|
67
|
-
const projectRoot = path.join(root,
|
|
68
|
-
const governanceDir = path.join(projectRoot,
|
|
67
|
+
const projectRoot = path.join(root, 'repo');
|
|
68
|
+
const governanceDir = path.join(projectRoot, 'governance');
|
|
69
69
|
await fs.mkdir(governanceDir, { recursive: true });
|
|
70
|
-
await fs.writeFile(path.join(governanceDir,
|
|
70
|
+
await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
|
|
71
71
|
const resolved = await resolveGovernanceDir(projectRoot);
|
|
72
72
|
expect(resolved).toBe(governanceDir);
|
|
73
73
|
});
|
|
74
|
-
it(
|
|
74
|
+
it('throws error when path not found', async () => {
|
|
75
75
|
const root = await createTempDir();
|
|
76
|
-
const nonExistentPath = path.join(root,
|
|
77
|
-
await expect(resolveGovernanceDir(nonExistentPath)).rejects.toThrow(
|
|
76
|
+
const nonExistentPath = path.join(root, 'nonexistent');
|
|
77
|
+
await expect(resolveGovernanceDir(nonExistentPath)).rejects.toThrow('Path not found');
|
|
78
78
|
});
|
|
79
|
-
it(
|
|
79
|
+
it('throws error when no .projitive marker found', async () => {
|
|
80
80
|
const root = await createTempDir();
|
|
81
|
-
const deepDir = path.join(root,
|
|
81
|
+
const deepDir = path.join(root, 'a', 'b', 'c');
|
|
82
82
|
await fs.mkdir(deepDir, { recursive: true });
|
|
83
|
-
await expect(resolveGovernanceDir(deepDir)).rejects.toThrow(
|
|
83
|
+
await expect(resolveGovernanceDir(deepDir)).rejects.toThrow('No .projitive marker found');
|
|
84
84
|
});
|
|
85
|
-
it(
|
|
85
|
+
it('prefers default .projitive directory when multiple governance roots found as children', async () => {
|
|
86
86
|
const root = await createTempDir();
|
|
87
|
-
const childDir = path.join(root,
|
|
88
|
-
const governance1 = path.join(childDir,
|
|
89
|
-
const governance2 = path.join(childDir,
|
|
87
|
+
const childDir = path.join(root, 'child');
|
|
88
|
+
const governance1 = path.join(childDir, '.projitive');
|
|
89
|
+
const governance2 = path.join(childDir, 'governance');
|
|
90
90
|
await fs.mkdir(governance1, { recursive: true });
|
|
91
91
|
await fs.mkdir(governance2, { recursive: true });
|
|
92
|
-
await fs.writeFile(path.join(governance1,
|
|
93
|
-
await fs.writeFile(path.join(governance2,
|
|
92
|
+
await fs.writeFile(path.join(governance1, '.projitive'), '', 'utf-8');
|
|
93
|
+
await fs.writeFile(path.join(governance2, '.projitive'), '', 'utf-8');
|
|
94
94
|
const resolved = await resolveGovernanceDir(childDir);
|
|
95
95
|
expect(resolved).toBe(governance1); // Should prefer default .projitive
|
|
96
96
|
});
|
|
97
|
-
it(
|
|
97
|
+
it('resolves file path by using its directory', async () => {
|
|
98
98
|
const root = await createTempDir();
|
|
99
|
-
const governanceDir = path.join(root,
|
|
100
|
-
const filePath = path.join(governanceDir,
|
|
99
|
+
const governanceDir = path.join(root, '.projitive');
|
|
100
|
+
const filePath = path.join(governanceDir, 'tasks.md');
|
|
101
101
|
await fs.mkdir(governanceDir, { recursive: true });
|
|
102
|
-
await fs.writeFile(path.join(governanceDir,
|
|
103
|
-
await fs.writeFile(filePath,
|
|
102
|
+
await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
|
|
103
|
+
await fs.writeFile(filePath, '# Tasks', 'utf-8');
|
|
104
104
|
const resolved = await resolveGovernanceDir(filePath);
|
|
105
105
|
expect(resolved).toBe(governanceDir);
|
|
106
106
|
});
|
|
107
107
|
});
|
|
108
|
-
describe(
|
|
109
|
-
it(
|
|
108
|
+
describe('discoverProjects', () => {
|
|
109
|
+
it('discovers projects by marker file', async () => {
|
|
110
110
|
const root = await createTempDir();
|
|
111
|
-
const p1 = path.join(root,
|
|
112
|
-
const p2 = path.join(root,
|
|
111
|
+
const p1 = path.join(root, 'a');
|
|
112
|
+
const p2 = path.join(root, 'b', 'c');
|
|
113
113
|
await fs.mkdir(p1, { recursive: true });
|
|
114
114
|
await fs.mkdir(p2, { recursive: true });
|
|
115
|
-
await fs.writeFile(path.join(p1,
|
|
116
|
-
await fs.writeFile(path.join(p2,
|
|
115
|
+
await fs.writeFile(path.join(p1, '.projitive'), '', 'utf-8');
|
|
116
|
+
await fs.writeFile(path.join(p2, '.projitive'), '', 'utf-8');
|
|
117
117
|
const projects = await discoverProjects(root, 4);
|
|
118
118
|
expect(projects).toContain(p1);
|
|
119
119
|
expect(projects).toContain(p2);
|
|
120
120
|
});
|
|
121
|
-
it(
|
|
121
|
+
it('discovers nested default governance directory under project root', async () => {
|
|
122
122
|
const root = await createTempDir();
|
|
123
|
-
const projectRoot = path.join(root,
|
|
124
|
-
const governanceDir = path.join(projectRoot,
|
|
123
|
+
const projectRoot = path.join(root, 'app');
|
|
124
|
+
const governanceDir = path.join(projectRoot, '.projitive');
|
|
125
125
|
await fs.mkdir(governanceDir, { recursive: true });
|
|
126
|
-
await fs.writeFile(path.join(governanceDir,
|
|
126
|
+
await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
|
|
127
127
|
const projects = await discoverProjects(root, 3);
|
|
128
128
|
expect(projects).toContain(governanceDir);
|
|
129
129
|
});
|
|
130
|
-
it(
|
|
130
|
+
it('discovers nested custom governance directory under project root', async () => {
|
|
131
131
|
const root = await createTempDir();
|
|
132
|
-
const projectRoot = path.join(root,
|
|
133
|
-
const governanceDir = path.join(projectRoot,
|
|
132
|
+
const projectRoot = path.join(root, 'app');
|
|
133
|
+
const governanceDir = path.join(projectRoot, 'governance');
|
|
134
134
|
await fs.mkdir(governanceDir, { recursive: true });
|
|
135
|
-
await fs.writeFile(path.join(governanceDir,
|
|
135
|
+
await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
|
|
136
136
|
const projects = await discoverProjects(root, 3);
|
|
137
137
|
expect(projects).toContain(governanceDir);
|
|
138
138
|
});
|
|
139
|
-
it(
|
|
139
|
+
it('respects maxDepth limit', async () => {
|
|
140
140
|
const root = await createTempDir();
|
|
141
|
-
const shallow = path.join(root,
|
|
142
|
-
const deep = path.join(root,
|
|
141
|
+
const shallow = path.join(root, 'shallow');
|
|
142
|
+
const deep = path.join(root, 'level1', 'level2', 'level3', 'level4', 'deep');
|
|
143
143
|
await fs.mkdir(shallow, { recursive: true });
|
|
144
144
|
await fs.mkdir(deep, { recursive: true });
|
|
145
|
-
await fs.writeFile(path.join(shallow,
|
|
146
|
-
await fs.writeFile(path.join(deep,
|
|
145
|
+
await fs.writeFile(path.join(shallow, '.projitive'), '', 'utf-8');
|
|
146
|
+
await fs.writeFile(path.join(deep, '.projitive'), '', 'utf-8');
|
|
147
147
|
const projects = await discoverProjects(root, 3);
|
|
148
148
|
expect(projects).toContain(shallow);
|
|
149
149
|
expect(projects).not.toContain(deep);
|
|
150
150
|
});
|
|
151
|
-
it(
|
|
151
|
+
it('ignores common ignore directories', async () => {
|
|
152
152
|
const root = await createTempDir();
|
|
153
|
-
const nodeModulesProject = path.join(root,
|
|
154
|
-
const gitProject = path.join(root,
|
|
155
|
-
const validProject = path.join(root,
|
|
153
|
+
const nodeModulesProject = path.join(root, 'node_modules', 'project');
|
|
154
|
+
const gitProject = path.join(root, '.git', 'project');
|
|
155
|
+
const validProject = path.join(root, 'valid');
|
|
156
156
|
await fs.mkdir(nodeModulesProject, { recursive: true });
|
|
157
157
|
await fs.mkdir(gitProject, { recursive: true });
|
|
158
158
|
await fs.mkdir(validProject, { recursive: true });
|
|
159
|
-
await fs.writeFile(path.join(nodeModulesProject,
|
|
160
|
-
await fs.writeFile(path.join(gitProject,
|
|
161
|
-
await fs.writeFile(path.join(validProject,
|
|
159
|
+
await fs.writeFile(path.join(nodeModulesProject, '.projitive'), '', 'utf-8');
|
|
160
|
+
await fs.writeFile(path.join(gitProject, '.projitive'), '', 'utf-8');
|
|
161
|
+
await fs.writeFile(path.join(validProject, '.projitive'), '', 'utf-8');
|
|
162
162
|
const projects = await discoverProjects(root, 3);
|
|
163
163
|
expect(projects).toContain(validProject);
|
|
164
164
|
expect(projects).not.toContain(nodeModulesProject);
|
|
165
165
|
expect(projects).not.toContain(gitProject);
|
|
166
166
|
});
|
|
167
|
-
it(
|
|
167
|
+
it('returns empty array when no projects found', async () => {
|
|
168
168
|
const root = await createTempDir();
|
|
169
169
|
const projects = await discoverProjects(root, 3);
|
|
170
170
|
expect(projects).toEqual([]);
|
|
171
171
|
});
|
|
172
|
-
it(
|
|
172
|
+
it('returns unique and sorted results', async () => {
|
|
173
173
|
const root = await createTempDir();
|
|
174
|
-
const projectB = path.join(root,
|
|
175
|
-
const projectA = path.join(root,
|
|
174
|
+
const projectB = path.join(root, 'b');
|
|
175
|
+
const projectA = path.join(root, 'a');
|
|
176
176
|
await fs.mkdir(projectB, { recursive: true });
|
|
177
177
|
await fs.mkdir(projectA, { recursive: true });
|
|
178
|
-
await fs.writeFile(path.join(projectB,
|
|
179
|
-
await fs.writeFile(path.join(projectA,
|
|
178
|
+
await fs.writeFile(path.join(projectB, '.projitive'), '', 'utf-8');
|
|
179
|
+
await fs.writeFile(path.join(projectA, '.projitive'), '', 'utf-8');
|
|
180
180
|
const projects = await discoverProjects(root, 3);
|
|
181
181
|
expect(projects).toEqual([projectA, projectB]);
|
|
182
182
|
});
|
|
183
|
-
it(
|
|
183
|
+
it('handles fs.readdir errors gracefully', async () => {
|
|
184
184
|
const root = await createTempDir();
|
|
185
|
-
vi.spyOn(fs,
|
|
185
|
+
vi.spyOn(fs, 'readdir').mockRejectedValueOnce(new Error('Permission denied'));
|
|
186
186
|
const projects = await discoverProjects(root, 3);
|
|
187
187
|
expect(projects).toEqual([]);
|
|
188
188
|
});
|
|
189
|
-
it(
|
|
189
|
+
it('ignores non-existent roots when scanning across multiple roots', async () => {
|
|
190
190
|
const validRoot = await createTempDir();
|
|
191
|
-
const validProject = path.join(validRoot,
|
|
192
|
-
const missingRoot = path.join(validRoot,
|
|
191
|
+
const validProject = path.join(validRoot, 'project-a');
|
|
192
|
+
const missingRoot = path.join(validRoot, '__missing_root__');
|
|
193
193
|
await fs.mkdir(validProject, { recursive: true });
|
|
194
|
-
await fs.writeFile(path.join(validProject,
|
|
194
|
+
await fs.writeFile(path.join(validProject, '.projitive'), '', 'utf-8');
|
|
195
195
|
const projects = await discoverProjectsAcrossRoots([missingRoot, validRoot], 3);
|
|
196
196
|
expect(projects).toContain(validProject);
|
|
197
197
|
});
|
|
198
198
|
});
|
|
199
|
-
describe(
|
|
200
|
-
it(
|
|
199
|
+
describe('initializeProjectStructure', () => {
|
|
200
|
+
it('initializes governance structure under default .projitive directory', async () => {
|
|
201
201
|
const root = await createTempDir();
|
|
202
202
|
const initialized = await initializeProjectStructure(root);
|
|
203
|
-
expect(initialized.governanceDir).toBe(path.join(root,
|
|
203
|
+
expect(initialized.governanceDir).toBe(path.join(root, '.projitive'));
|
|
204
204
|
const expectedPaths = [
|
|
205
|
-
path.join(root,
|
|
206
|
-
path.join(root,
|
|
207
|
-
path.join(root,
|
|
208
|
-
path.join(root,
|
|
209
|
-
path.join(root,
|
|
210
|
-
path.join(root,
|
|
211
|
-
path.join(root,
|
|
212
|
-
path.join(root,
|
|
213
|
-
path.join(root,
|
|
214
|
-
path.join(root,
|
|
215
|
-
path.join(root,
|
|
205
|
+
path.join(root, '.projitive', '.projitive'),
|
|
206
|
+
path.join(root, '.projitive', 'README.md'),
|
|
207
|
+
path.join(root, '.projitive', 'roadmap.md'),
|
|
208
|
+
path.join(root, '.projitive', 'tasks.md'),
|
|
209
|
+
path.join(root, '.projitive', 'templates', 'README.md'),
|
|
210
|
+
path.join(root, '.projitive', 'templates', 'tools', 'taskNext.md'),
|
|
211
|
+
path.join(root, '.projitive', 'templates', 'tools', 'taskUpdate.md'),
|
|
212
|
+
path.join(root, '.projitive', 'designs'),
|
|
213
|
+
path.join(root, '.projitive', 'reports'),
|
|
214
|
+
path.join(root, '.projitive', 'templates'),
|
|
215
|
+
path.join(root, '.projitive', 'templates', 'tools'),
|
|
216
216
|
];
|
|
217
217
|
await Promise.all(expectedPaths.map(async (targetPath) => {
|
|
218
218
|
await expect(fs.access(targetPath)).resolves.toBeUndefined();
|
|
219
219
|
}));
|
|
220
220
|
});
|
|
221
|
-
it(
|
|
221
|
+
it('overwrites template files when force is enabled', async () => {
|
|
222
222
|
const root = await createTempDir();
|
|
223
|
-
const governanceDir = path.join(root,
|
|
224
|
-
const readmePath = path.join(governanceDir,
|
|
223
|
+
const governanceDir = path.join(root, '.projitive');
|
|
224
|
+
const readmePath = path.join(governanceDir, 'README.md');
|
|
225
225
|
await initializeProjectStructure(root);
|
|
226
|
-
await fs.writeFile(readmePath,
|
|
227
|
-
const initialized = await initializeProjectStructure(root,
|
|
228
|
-
const readmeContent = await fs.readFile(readmePath,
|
|
229
|
-
expect(readmeContent).toContain(
|
|
230
|
-
expect(initialized.files.find((item) => item.path === readmePath)?.action).toBe(
|
|
226
|
+
await fs.writeFile(readmePath, 'custom-content', 'utf-8');
|
|
227
|
+
const initialized = await initializeProjectStructure(root, '.projitive', true);
|
|
228
|
+
const readmeContent = await fs.readFile(readmePath, 'utf-8');
|
|
229
|
+
expect(readmeContent).toContain('Projitive Governance Workspace');
|
|
230
|
+
expect(initialized.files.find((item) => item.path === readmePath)?.action).toBe('updated');
|
|
231
231
|
});
|
|
232
|
-
it(
|
|
232
|
+
it('uses custom governance directory when specified', async () => {
|
|
233
233
|
const root = await createTempDir();
|
|
234
|
-
const customDir =
|
|
234
|
+
const customDir = 'my-governance';
|
|
235
235
|
const initialized = await initializeProjectStructure(root, customDir);
|
|
236
236
|
expect(initialized.governanceDir).toBe(path.join(root, customDir));
|
|
237
237
|
});
|
|
238
|
-
it(
|
|
238
|
+
it('throws error when project path not found', async () => {
|
|
239
239
|
const root = await createTempDir();
|
|
240
|
-
const nonExistentPath = path.join(root,
|
|
241
|
-
await expect(initializeProjectStructure(nonExistentPath)).rejects.toThrow(
|
|
240
|
+
const nonExistentPath = path.join(root, 'nonexistent');
|
|
241
|
+
await expect(initializeProjectStructure(nonExistentPath)).rejects.toThrow('Path not found');
|
|
242
242
|
});
|
|
243
|
-
it(
|
|
243
|
+
it('throws error when project path is not a directory', async () => {
|
|
244
244
|
const root = await createTempDir();
|
|
245
|
-
const filePath = path.join(root,
|
|
246
|
-
await fs.writeFile(filePath,
|
|
247
|
-
await expect(initializeProjectStructure(filePath)).rejects.toThrow(
|
|
245
|
+
const filePath = path.join(root, 'file.txt');
|
|
246
|
+
await fs.writeFile(filePath, 'content', 'utf-8');
|
|
247
|
+
await expect(initializeProjectStructure(filePath)).rejects.toThrow('projectPath must be a directory');
|
|
248
248
|
});
|
|
249
|
-
it(
|
|
249
|
+
it('creates governance structure with default name when invalid names are provided', async () => {
|
|
250
250
|
const root = await createTempDir();
|
|
251
251
|
// When governanceDir is invalid, it should fall back to default
|
|
252
252
|
// Note: normalizeGovernanceDirName is not exported, so we test initialization behavior
|
|
253
253
|
const initialized = await initializeProjectStructure(root);
|
|
254
|
-
expect(initialized.governanceDir).toBe(path.join(root,
|
|
254
|
+
expect(initialized.governanceDir).toBe(path.join(root, '.projitive'));
|
|
255
255
|
});
|
|
256
|
-
it(
|
|
256
|
+
it('skips existing files when force is disabled', async () => {
|
|
257
257
|
const root = await createTempDir();
|
|
258
|
-
const governanceDir = path.join(root,
|
|
259
|
-
const readmePath = path.join(governanceDir,
|
|
258
|
+
const governanceDir = path.join(root, '.projitive');
|
|
259
|
+
const readmePath = path.join(governanceDir, 'README.md');
|
|
260
260
|
await initializeProjectStructure(root);
|
|
261
|
-
await fs.writeFile(readmePath,
|
|
262
|
-
const initialized = await initializeProjectStructure(root,
|
|
263
|
-
const readmeContent = await fs.readFile(readmePath,
|
|
264
|
-
expect(readmeContent).toBe(
|
|
265
|
-
expect(initialized.files.find((item) => item.path === readmePath)?.action).toBe(
|
|
261
|
+
await fs.writeFile(readmePath, 'custom-content', 'utf-8');
|
|
262
|
+
const initialized = await initializeProjectStructure(root, '.projitive', false);
|
|
263
|
+
const readmeContent = await fs.readFile(readmePath, 'utf-8');
|
|
264
|
+
expect(readmeContent).toBe('custom-content');
|
|
265
|
+
expect(initialized.files.find((item) => item.path === readmePath)?.action).toBe('skipped');
|
|
266
266
|
});
|
|
267
|
-
it(
|
|
267
|
+
it('creates all required subdirectories', async () => {
|
|
268
268
|
const root = await createTempDir();
|
|
269
269
|
const initialized = await initializeProjectStructure(root);
|
|
270
|
-
expect(initialized.directories.some(d => d.path.includes(
|
|
271
|
-
expect(initialized.directories.some(d => d.path.includes(
|
|
272
|
-
expect(initialized.directories.some(d => d.path.includes(
|
|
270
|
+
expect(initialized.directories.some(d => d.path.includes('designs'))).toBe(true);
|
|
271
|
+
expect(initialized.directories.some(d => d.path.includes('reports'))).toBe(true);
|
|
272
|
+
expect(initialized.directories.some(d => d.path.includes('templates'))).toBe(true);
|
|
273
273
|
});
|
|
274
274
|
});
|
|
275
|
-
describe(
|
|
276
|
-
describe(
|
|
277
|
-
it(
|
|
278
|
-
expect(toProjectPath(
|
|
279
|
-
expect(toProjectPath(
|
|
275
|
+
describe('utility functions', () => {
|
|
276
|
+
describe('toProjectPath', () => {
|
|
277
|
+
it('returns parent directory of governance dir', () => {
|
|
278
|
+
expect(toProjectPath('/path/to/project/.projitive')).toBe('/path/to/project');
|
|
279
|
+
expect(toProjectPath('/a/b/c')).toBe('/a/b');
|
|
280
280
|
});
|
|
281
281
|
});
|
|
282
|
-
describe(
|
|
283
|
-
it(
|
|
284
|
-
vi.stubEnv(
|
|
285
|
-
expect(resolveScanRoots()).toEqual([
|
|
282
|
+
describe('resolveScanRoots', () => {
|
|
283
|
+
it('uses legacy environment variable when no multi-root env is provided', () => {
|
|
284
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/test/root');
|
|
285
|
+
expect(resolveScanRoots()).toEqual(['/test/root']);
|
|
286
286
|
vi.unstubAllEnvs();
|
|
287
287
|
});
|
|
288
|
-
it(
|
|
289
|
-
vi.stubEnv(
|
|
290
|
-
expect(resolveScanRoots([
|
|
288
|
+
it('uses input paths when provided', () => {
|
|
289
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/test/root');
|
|
290
|
+
expect(resolveScanRoots(['/custom/path', ' /custom/path ', '/second/path'])).toEqual(['/custom/path', '/second/path']);
|
|
291
291
|
vi.unstubAllEnvs();
|
|
292
292
|
});
|
|
293
|
-
it(
|
|
294
|
-
vi.stubEnv(
|
|
295
|
-
expect(resolveScanRoots()).toEqual([
|
|
293
|
+
it('uses PROJITIVE_SCAN_ROOT_PATHS with platform delimiter', () => {
|
|
294
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATHS', ['/root/a', '/root/b', '', ' /root/c '].join(path.delimiter));
|
|
295
|
+
expect(resolveScanRoots()).toEqual(['/root/a', '/root/b', '/root/c']);
|
|
296
296
|
vi.unstubAllEnvs();
|
|
297
297
|
});
|
|
298
|
-
it(
|
|
299
|
-
vi.stubEnv(
|
|
298
|
+
it('treats JSON-like string as plain delimiter input', () => {
|
|
299
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATHS', JSON.stringify(['/json/a', '/json/b']));
|
|
300
300
|
expect(resolveScanRoots()).toHaveLength(1);
|
|
301
301
|
vi.unstubAllEnvs();
|
|
302
302
|
});
|
|
303
|
-
it(
|
|
303
|
+
it('throws error when no root environment variables are configured', () => {
|
|
304
304
|
vi.unstubAllEnvs();
|
|
305
|
-
expect(() => resolveScanRoots()).toThrow(
|
|
305
|
+
expect(() => resolveScanRoots()).toThrow('Missing required environment variable: PROJITIVE_SCAN_ROOT_PATHS');
|
|
306
306
|
});
|
|
307
307
|
});
|
|
308
|
-
describe(
|
|
309
|
-
it(
|
|
310
|
-
vi.stubEnv(
|
|
311
|
-
vi.stubEnv(
|
|
308
|
+
describe('resolveScanDepth', () => {
|
|
309
|
+
it('uses environment variable when no input depth', () => {
|
|
310
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/test/root');
|
|
311
|
+
vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', '5');
|
|
312
312
|
expect(resolveScanDepth()).toBe(5);
|
|
313
313
|
vi.unstubAllEnvs();
|
|
314
314
|
});
|
|
315
|
-
it(
|
|
316
|
-
vi.stubEnv(
|
|
317
|
-
vi.stubEnv(
|
|
315
|
+
it('uses input depth when provided', () => {
|
|
316
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/test/root');
|
|
317
|
+
vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', '5');
|
|
318
318
|
expect(resolveScanDepth(3)).toBe(3);
|
|
319
319
|
vi.unstubAllEnvs();
|
|
320
320
|
});
|
|
321
|
-
it(
|
|
322
|
-
vi.stubEnv(
|
|
323
|
-
vi.stubEnv(
|
|
321
|
+
it('clamps depth to MAX_SCAN_DEPTH', () => {
|
|
322
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/test/root');
|
|
323
|
+
vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', '10');
|
|
324
324
|
expect(resolveScanDepth()).toBe(8);
|
|
325
325
|
vi.unstubAllEnvs();
|
|
326
326
|
});
|
|
327
|
-
it(
|
|
328
|
-
vi.stubEnv(
|
|
329
|
-
vi.stubEnv(
|
|
330
|
-
expect(() => resolveScanDepth()).toThrow(
|
|
327
|
+
it('throws error for invalid depth configuration', () => {
|
|
328
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/test/root');
|
|
329
|
+
vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', 'not-a-number');
|
|
330
|
+
expect(() => resolveScanDepth()).toThrow('Invalid PROJITIVE_SCAN_MAX_DEPTH');
|
|
331
331
|
vi.unstubAllEnvs();
|
|
332
332
|
});
|
|
333
333
|
});
|
|
334
334
|
});
|
|
335
|
-
describe(
|
|
336
|
-
it(
|
|
335
|
+
describe('registerProjectTools', () => {
|
|
336
|
+
it('registers project tools without throwing', () => {
|
|
337
337
|
const mockServer = {
|
|
338
338
|
registerTool: vi.fn(),
|
|
339
339
|
};
|
|
340
340
|
expect(() => registerProjectTools(mockServer)).not.toThrow();
|
|
341
341
|
expect(mockServer.registerTool).toHaveBeenCalled();
|
|
342
342
|
});
|
|
343
|
-
it(
|
|
343
|
+
it('projectScan lists project root paths instead of governance directories', async () => {
|
|
344
344
|
const root = await createTempDir();
|
|
345
|
-
const projectRoot = path.join(root,
|
|
346
|
-
const governanceDir = path.join(projectRoot,
|
|
345
|
+
const projectRoot = path.join(root, 'app');
|
|
346
|
+
const governanceDir = path.join(projectRoot, '.projitive');
|
|
347
347
|
const templateDir = await createTempDir();
|
|
348
348
|
await fs.mkdir(governanceDir, { recursive: true });
|
|
349
|
-
await fs.writeFile(path.join(governanceDir,
|
|
350
|
-
vi.stubEnv(
|
|
351
|
-
vi.stubEnv(
|
|
352
|
-
vi.stubEnv(
|
|
349
|
+
await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
|
|
350
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATHS', root);
|
|
351
|
+
vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', '3');
|
|
352
|
+
vi.stubEnv('PROJITIVE_MESSAGE_TEMPLATE_PATH', templateDir);
|
|
353
353
|
const mockServer = {
|
|
354
354
|
registerTool: vi.fn(),
|
|
355
355
|
};
|
|
356
356
|
registerProjectTools(mockServer);
|
|
357
|
-
const projectScanCall = mockServer.registerTool.mock.calls.find((call) => call[0] ===
|
|
357
|
+
const projectScanCall = mockServer.registerTool.mock.calls.find((call) => call[0] === 'projectScan');
|
|
358
358
|
expect(projectScanCall).toBeTruthy();
|
|
359
359
|
const projectScanHandler = projectScanCall?.[2];
|
|
360
|
+
expect(projectScanHandler).toBeTruthy();
|
|
360
361
|
const result = await projectScanHandler();
|
|
361
|
-
const markdown = result.content[0]?.text ??
|
|
362
|
+
const markdown = result.content[0]?.text ?? '';
|
|
362
363
|
expect(markdown).toContain(`1. ${projectRoot}`);
|
|
363
364
|
expect(markdown).not.toContain(`1. ${governanceDir}`);
|
|
364
365
|
});
|