@projitive/mcp 2.0.3 → 2.1.0
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/errors.test.js +59 -0
- package/output/source/common/files.js +30 -19
- package/output/source/common/files.test.js +14 -14
- package/output/source/common/index.js +11 -10
- package/output/source/common/linter.js +29 -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 +91 -107
- 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/tool.js +43 -0
- package/output/source/common/types.js +3 -3
- package/output/source/common/utils.js +8 -8
- package/output/source/common/utils.test.js +48 -0
- package/output/source/index.js +16 -16
- package/output/source/index.runtime.test.js +57 -0
- package/output/source/index.test.js +64 -64
- package/output/source/prompts/index.js +3 -3
- package/output/source/prompts/index.test.js +23 -0
- package/output/source/prompts/quickStart.js +96 -96
- package/output/source/prompts/quickStart.test.js +24 -0
- package/output/source/prompts/taskDiscovery.js +184 -184
- package/output/source/prompts/taskDiscovery.test.js +24 -0
- package/output/source/prompts/taskExecution.js +164 -148
- package/output/source/prompts/taskExecution.test.js +27 -0
- package/output/source/resources/designs.js +26 -26
- package/output/source/resources/designs.resources.test.js +52 -0
- package/output/source/resources/designs.test.js +88 -88
- package/output/source/resources/governance.js +19 -19
- package/output/source/resources/governance.test.js +35 -0
- package/output/source/resources/index.js +2 -2
- package/output/source/resources/index.test.js +18 -0
- 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/index.test.js +23 -0
- package/output/source/tools/project.js +330 -377
- package/output/source/tools/project.test.js +308 -175
- package/output/source/tools/roadmap.js +236 -255
- package/output/source/tools/roadmap.test.js +241 -46
- package/output/source/tools/task.js +770 -652
- package/output/source/tools/task.test.js +433 -105
- package/output/source/types.js +28 -22
- package/package.json +8 -2
|
@@ -1,366 +1,499 @@
|
|
|
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, resolveScanRoot, 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
|
}
|
|
12
|
+
function getProjectToolHandler(mockServer, toolName) {
|
|
13
|
+
const call = mockServer.registerTool.mock.calls.find((entry) => entry[0] === toolName);
|
|
14
|
+
expect(call).toBeTruthy();
|
|
15
|
+
return call?.[2];
|
|
16
|
+
}
|
|
12
17
|
afterEach(async () => {
|
|
13
18
|
await Promise.all(tempPaths.splice(0).map(async (dir) => {
|
|
14
19
|
await fs.rm(dir, { recursive: true, force: true });
|
|
15
20
|
}));
|
|
16
21
|
vi.restoreAllMocks();
|
|
17
22
|
});
|
|
18
|
-
describe(
|
|
19
|
-
describe(
|
|
20
|
-
it(
|
|
23
|
+
describe('projitive module', () => {
|
|
24
|
+
describe('hasProjectMarker', () => {
|
|
25
|
+
it('does not treat marker directory as a valid project marker', async () => {
|
|
21
26
|
const root = await createTempDir();
|
|
22
|
-
const dirMarkerPath = path.join(root,
|
|
27
|
+
const dirMarkerPath = path.join(root, '.projitive');
|
|
23
28
|
await fs.mkdir(dirMarkerPath, { recursive: true });
|
|
24
29
|
const hasMarker = await hasProjectMarker(root);
|
|
25
30
|
expect(hasMarker).toBe(false);
|
|
26
31
|
});
|
|
27
|
-
it(
|
|
32
|
+
it('returns true when .projitive marker file exists', async () => {
|
|
28
33
|
const root = await createTempDir();
|
|
29
|
-
const markerPath = path.join(root,
|
|
30
|
-
await fs.writeFile(markerPath,
|
|
34
|
+
const markerPath = path.join(root, '.projitive');
|
|
35
|
+
await fs.writeFile(markerPath, '', 'utf-8');
|
|
31
36
|
const hasMarker = await hasProjectMarker(root);
|
|
32
37
|
expect(hasMarker).toBe(true);
|
|
33
38
|
});
|
|
34
|
-
it(
|
|
39
|
+
it('returns false when .projitive marker file does not exist', async () => {
|
|
35
40
|
const root = await createTempDir();
|
|
36
41
|
const hasMarker = await hasProjectMarker(root);
|
|
37
42
|
expect(hasMarker).toBe(false);
|
|
38
43
|
});
|
|
39
|
-
it(
|
|
44
|
+
it('handles fs.stat errors gracefully', async () => {
|
|
40
45
|
const root = await createTempDir();
|
|
41
|
-
vi.spyOn(fs,
|
|
46
|
+
vi.spyOn(fs, 'stat').mockRejectedValueOnce(new Error('Permission denied'));
|
|
42
47
|
const hasMarker = await hasProjectMarker(root);
|
|
43
48
|
expect(hasMarker).toBe(false);
|
|
44
49
|
});
|
|
45
50
|
});
|
|
46
|
-
describe(
|
|
47
|
-
it(
|
|
51
|
+
describe('resolveGovernanceDir', () => {
|
|
52
|
+
it('resolves governance dir by walking upwards for .projitive', async () => {
|
|
48
53
|
const root = await createTempDir();
|
|
49
|
-
const governanceDir = path.join(root,
|
|
50
|
-
const deepDir = path.join(governanceDir,
|
|
54
|
+
const governanceDir = path.join(root, 'repo', 'governance');
|
|
55
|
+
const deepDir = path.join(governanceDir, 'nested', 'module');
|
|
51
56
|
await fs.mkdir(deepDir, { recursive: true });
|
|
52
|
-
await fs.writeFile(path.join(governanceDir,
|
|
57
|
+
await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
|
|
53
58
|
const resolved = await resolveGovernanceDir(deepDir);
|
|
54
59
|
expect(resolved).toBe(governanceDir);
|
|
55
60
|
});
|
|
56
|
-
it(
|
|
61
|
+
it('resolves nested default governance dir when input path is project root', async () => {
|
|
57
62
|
const root = await createTempDir();
|
|
58
|
-
const projectRoot = path.join(root,
|
|
59
|
-
const governanceDir = path.join(projectRoot,
|
|
63
|
+
const projectRoot = path.join(root, 'repo');
|
|
64
|
+
const governanceDir = path.join(projectRoot, '.projitive');
|
|
60
65
|
await fs.mkdir(governanceDir, { recursive: true });
|
|
61
|
-
await fs.writeFile(path.join(governanceDir,
|
|
66
|
+
await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
|
|
62
67
|
const resolved = await resolveGovernanceDir(projectRoot);
|
|
63
68
|
expect(resolved).toBe(governanceDir);
|
|
64
69
|
});
|
|
65
|
-
it(
|
|
70
|
+
it('resolves nested custom governance dir when input path is project root', async () => {
|
|
66
71
|
const root = await createTempDir();
|
|
67
|
-
const projectRoot = path.join(root,
|
|
68
|
-
const governanceDir = path.join(projectRoot,
|
|
72
|
+
const projectRoot = path.join(root, 'repo');
|
|
73
|
+
const governanceDir = path.join(projectRoot, 'governance');
|
|
69
74
|
await fs.mkdir(governanceDir, { recursive: true });
|
|
70
|
-
await fs.writeFile(path.join(governanceDir,
|
|
75
|
+
await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
|
|
71
76
|
const resolved = await resolveGovernanceDir(projectRoot);
|
|
72
77
|
expect(resolved).toBe(governanceDir);
|
|
73
78
|
});
|
|
74
|
-
it(
|
|
79
|
+
it('throws error when path not found', async () => {
|
|
75
80
|
const root = await createTempDir();
|
|
76
|
-
const nonExistentPath = path.join(root,
|
|
77
|
-
await expect(resolveGovernanceDir(nonExistentPath)).rejects.toThrow(
|
|
81
|
+
const nonExistentPath = path.join(root, 'nonexistent');
|
|
82
|
+
await expect(resolveGovernanceDir(nonExistentPath)).rejects.toThrow('Path not found');
|
|
78
83
|
});
|
|
79
|
-
it(
|
|
84
|
+
it('throws error when no .projitive marker found', async () => {
|
|
80
85
|
const root = await createTempDir();
|
|
81
|
-
const deepDir = path.join(root,
|
|
86
|
+
const deepDir = path.join(root, 'a', 'b', 'c');
|
|
82
87
|
await fs.mkdir(deepDir, { recursive: true });
|
|
83
|
-
await expect(resolveGovernanceDir(deepDir)).rejects.toThrow(
|
|
88
|
+
await expect(resolveGovernanceDir(deepDir)).rejects.toThrow('No .projitive marker found');
|
|
84
89
|
});
|
|
85
|
-
it(
|
|
90
|
+
it('throws when multiple non-default governance roots exist under same parent', async () => {
|
|
91
|
+
const root = await createTempDir();
|
|
92
|
+
const childDir = path.join(root, 'child');
|
|
93
|
+
const gov1 = path.join(childDir, 'governance-a');
|
|
94
|
+
const gov2 = path.join(childDir, 'governance-b');
|
|
95
|
+
await fs.mkdir(gov1, { recursive: true });
|
|
96
|
+
await fs.mkdir(gov2, { recursive: true });
|
|
97
|
+
await fs.writeFile(path.join(gov1, '.projitive'), '', 'utf-8');
|
|
98
|
+
await fs.writeFile(path.join(gov2, '.projitive'), '', 'utf-8');
|
|
99
|
+
await expect(resolveGovernanceDir(childDir)).rejects.toThrow('Multiple governance roots found');
|
|
100
|
+
});
|
|
101
|
+
it('prefers default .projitive directory when multiple governance roots found as children', async () => {
|
|
86
102
|
const root = await createTempDir();
|
|
87
|
-
const childDir = path.join(root,
|
|
88
|
-
const governance1 = path.join(childDir,
|
|
89
|
-
const governance2 = path.join(childDir,
|
|
103
|
+
const childDir = path.join(root, 'child');
|
|
104
|
+
const governance1 = path.join(childDir, '.projitive');
|
|
105
|
+
const governance2 = path.join(childDir, 'governance');
|
|
90
106
|
await fs.mkdir(governance1, { recursive: true });
|
|
91
107
|
await fs.mkdir(governance2, { recursive: true });
|
|
92
|
-
await fs.writeFile(path.join(governance1,
|
|
93
|
-
await fs.writeFile(path.join(governance2,
|
|
108
|
+
await fs.writeFile(path.join(governance1, '.projitive'), '', 'utf-8');
|
|
109
|
+
await fs.writeFile(path.join(governance2, '.projitive'), '', 'utf-8');
|
|
94
110
|
const resolved = await resolveGovernanceDir(childDir);
|
|
95
111
|
expect(resolved).toBe(governance1); // Should prefer default .projitive
|
|
96
112
|
});
|
|
97
|
-
it(
|
|
113
|
+
it('resolves file path by using its directory', async () => {
|
|
98
114
|
const root = await createTempDir();
|
|
99
|
-
const governanceDir = path.join(root,
|
|
100
|
-
const filePath = path.join(governanceDir,
|
|
115
|
+
const governanceDir = path.join(root, '.projitive');
|
|
116
|
+
const filePath = path.join(governanceDir, 'tasks.md');
|
|
101
117
|
await fs.mkdir(governanceDir, { recursive: true });
|
|
102
|
-
await fs.writeFile(path.join(governanceDir,
|
|
103
|
-
await fs.writeFile(filePath,
|
|
118
|
+
await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
|
|
119
|
+
await fs.writeFile(filePath, '# Tasks', 'utf-8');
|
|
104
120
|
const resolved = await resolveGovernanceDir(filePath);
|
|
105
121
|
expect(resolved).toBe(governanceDir);
|
|
106
122
|
});
|
|
107
123
|
});
|
|
108
|
-
describe(
|
|
109
|
-
it(
|
|
124
|
+
describe('discoverProjects', () => {
|
|
125
|
+
it('discovers projects by marker file', async () => {
|
|
110
126
|
const root = await createTempDir();
|
|
111
|
-
const p1 = path.join(root,
|
|
112
|
-
const p2 = path.join(root,
|
|
127
|
+
const p1 = path.join(root, 'a');
|
|
128
|
+
const p2 = path.join(root, 'b', 'c');
|
|
113
129
|
await fs.mkdir(p1, { recursive: true });
|
|
114
130
|
await fs.mkdir(p2, { recursive: true });
|
|
115
|
-
await fs.writeFile(path.join(p1,
|
|
116
|
-
await fs.writeFile(path.join(p2,
|
|
131
|
+
await fs.writeFile(path.join(p1, '.projitive'), '', 'utf-8');
|
|
132
|
+
await fs.writeFile(path.join(p2, '.projitive'), '', 'utf-8');
|
|
117
133
|
const projects = await discoverProjects(root, 4);
|
|
118
134
|
expect(projects).toContain(p1);
|
|
119
135
|
expect(projects).toContain(p2);
|
|
120
136
|
});
|
|
121
|
-
it(
|
|
137
|
+
it('discovers nested default governance directory under project root', async () => {
|
|
122
138
|
const root = await createTempDir();
|
|
123
|
-
const projectRoot = path.join(root,
|
|
124
|
-
const governanceDir = path.join(projectRoot,
|
|
139
|
+
const projectRoot = path.join(root, 'app');
|
|
140
|
+
const governanceDir = path.join(projectRoot, '.projitive');
|
|
125
141
|
await fs.mkdir(governanceDir, { recursive: true });
|
|
126
|
-
await fs.writeFile(path.join(governanceDir,
|
|
142
|
+
await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
|
|
127
143
|
const projects = await discoverProjects(root, 3);
|
|
128
144
|
expect(projects).toContain(governanceDir);
|
|
129
145
|
});
|
|
130
|
-
it(
|
|
146
|
+
it('discovers nested custom governance directory under project root', async () => {
|
|
131
147
|
const root = await createTempDir();
|
|
132
|
-
const projectRoot = path.join(root,
|
|
133
|
-
const governanceDir = path.join(projectRoot,
|
|
148
|
+
const projectRoot = path.join(root, 'app');
|
|
149
|
+
const governanceDir = path.join(projectRoot, 'governance');
|
|
134
150
|
await fs.mkdir(governanceDir, { recursive: true });
|
|
135
|
-
await fs.writeFile(path.join(governanceDir,
|
|
151
|
+
await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
|
|
136
152
|
const projects = await discoverProjects(root, 3);
|
|
137
153
|
expect(projects).toContain(governanceDir);
|
|
138
154
|
});
|
|
139
|
-
it(
|
|
155
|
+
it('respects maxDepth limit', async () => {
|
|
140
156
|
const root = await createTempDir();
|
|
141
|
-
const shallow = path.join(root,
|
|
142
|
-
const deep = path.join(root,
|
|
157
|
+
const shallow = path.join(root, 'shallow');
|
|
158
|
+
const deep = path.join(root, 'level1', 'level2', 'level3', 'level4', 'deep');
|
|
143
159
|
await fs.mkdir(shallow, { recursive: true });
|
|
144
160
|
await fs.mkdir(deep, { recursive: true });
|
|
145
|
-
await fs.writeFile(path.join(shallow,
|
|
146
|
-
await fs.writeFile(path.join(deep,
|
|
161
|
+
await fs.writeFile(path.join(shallow, '.projitive'), '', 'utf-8');
|
|
162
|
+
await fs.writeFile(path.join(deep, '.projitive'), '', 'utf-8');
|
|
147
163
|
const projects = await discoverProjects(root, 3);
|
|
148
164
|
expect(projects).toContain(shallow);
|
|
149
165
|
expect(projects).not.toContain(deep);
|
|
150
166
|
});
|
|
151
|
-
it(
|
|
167
|
+
it('ignores common ignore directories', async () => {
|
|
152
168
|
const root = await createTempDir();
|
|
153
|
-
const nodeModulesProject = path.join(root,
|
|
154
|
-
const gitProject = path.join(root,
|
|
155
|
-
const validProject = path.join(root,
|
|
169
|
+
const nodeModulesProject = path.join(root, 'node_modules', 'project');
|
|
170
|
+
const gitProject = path.join(root, '.git', 'project');
|
|
171
|
+
const validProject = path.join(root, 'valid');
|
|
156
172
|
await fs.mkdir(nodeModulesProject, { recursive: true });
|
|
157
173
|
await fs.mkdir(gitProject, { recursive: true });
|
|
158
174
|
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,
|
|
175
|
+
await fs.writeFile(path.join(nodeModulesProject, '.projitive'), '', 'utf-8');
|
|
176
|
+
await fs.writeFile(path.join(gitProject, '.projitive'), '', 'utf-8');
|
|
177
|
+
await fs.writeFile(path.join(validProject, '.projitive'), '', 'utf-8');
|
|
162
178
|
const projects = await discoverProjects(root, 3);
|
|
163
179
|
expect(projects).toContain(validProject);
|
|
164
180
|
expect(projects).not.toContain(nodeModulesProject);
|
|
165
181
|
expect(projects).not.toContain(gitProject);
|
|
166
182
|
});
|
|
167
|
-
it(
|
|
183
|
+
it('returns empty array when no projects found', async () => {
|
|
168
184
|
const root = await createTempDir();
|
|
169
185
|
const projects = await discoverProjects(root, 3);
|
|
170
186
|
expect(projects).toEqual([]);
|
|
171
187
|
});
|
|
172
|
-
it(
|
|
188
|
+
it('returns unique and sorted results', async () => {
|
|
173
189
|
const root = await createTempDir();
|
|
174
|
-
const projectB = path.join(root,
|
|
175
|
-
const projectA = path.join(root,
|
|
190
|
+
const projectB = path.join(root, 'b');
|
|
191
|
+
const projectA = path.join(root, 'a');
|
|
176
192
|
await fs.mkdir(projectB, { recursive: true });
|
|
177
193
|
await fs.mkdir(projectA, { recursive: true });
|
|
178
|
-
await fs.writeFile(path.join(projectB,
|
|
179
|
-
await fs.writeFile(path.join(projectA,
|
|
194
|
+
await fs.writeFile(path.join(projectB, '.projitive'), '', 'utf-8');
|
|
195
|
+
await fs.writeFile(path.join(projectA, '.projitive'), '', 'utf-8');
|
|
180
196
|
const projects = await discoverProjects(root, 3);
|
|
181
197
|
expect(projects).toEqual([projectA, projectB]);
|
|
182
198
|
});
|
|
183
|
-
it(
|
|
199
|
+
it('handles fs.readdir errors gracefully', async () => {
|
|
184
200
|
const root = await createTempDir();
|
|
185
|
-
vi.spyOn(fs,
|
|
201
|
+
vi.spyOn(fs, 'readdir').mockRejectedValueOnce(new Error('Permission denied'));
|
|
186
202
|
const projects = await discoverProjects(root, 3);
|
|
187
203
|
expect(projects).toEqual([]);
|
|
188
204
|
});
|
|
189
|
-
it(
|
|
205
|
+
it('ignores non-existent roots when scanning across multiple roots', async () => {
|
|
190
206
|
const validRoot = await createTempDir();
|
|
191
|
-
const validProject = path.join(validRoot,
|
|
192
|
-
const missingRoot = path.join(validRoot,
|
|
207
|
+
const validProject = path.join(validRoot, 'project-a');
|
|
208
|
+
const missingRoot = path.join(validRoot, '__missing_root__');
|
|
193
209
|
await fs.mkdir(validProject, { recursive: true });
|
|
194
|
-
await fs.writeFile(path.join(validProject,
|
|
210
|
+
await fs.writeFile(path.join(validProject, '.projitive'), '', 'utf-8');
|
|
195
211
|
const projects = await discoverProjectsAcrossRoots([missingRoot, validRoot], 3);
|
|
196
212
|
expect(projects).toContain(validProject);
|
|
197
213
|
});
|
|
198
214
|
});
|
|
199
|
-
describe(
|
|
200
|
-
it(
|
|
215
|
+
describe('initializeProjectStructure', () => {
|
|
216
|
+
it('initializes governance structure under default .projitive directory', async () => {
|
|
201
217
|
const root = await createTempDir();
|
|
202
218
|
const initialized = await initializeProjectStructure(root);
|
|
203
|
-
expect(initialized.governanceDir).toBe(path.join(root,
|
|
219
|
+
expect(initialized.governanceDir).toBe(path.join(root, '.projitive'));
|
|
204
220
|
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,
|
|
221
|
+
path.join(root, '.projitive', '.projitive'),
|
|
222
|
+
path.join(root, '.projitive', 'README.md'),
|
|
223
|
+
path.join(root, '.projitive', 'roadmap.md'),
|
|
224
|
+
path.join(root, '.projitive', 'tasks.md'),
|
|
225
|
+
path.join(root, '.projitive', 'templates', 'README.md'),
|
|
226
|
+
path.join(root, '.projitive', 'templates', 'tools', 'taskNext.md'),
|
|
227
|
+
path.join(root, '.projitive', 'templates', 'tools', 'taskUpdate.md'),
|
|
228
|
+
path.join(root, '.projitive', 'designs'),
|
|
229
|
+
path.join(root, '.projitive', 'reports'),
|
|
230
|
+
path.join(root, '.projitive', 'templates'),
|
|
231
|
+
path.join(root, '.projitive', 'templates', 'tools'),
|
|
216
232
|
];
|
|
217
233
|
await Promise.all(expectedPaths.map(async (targetPath) => {
|
|
218
234
|
await expect(fs.access(targetPath)).resolves.toBeUndefined();
|
|
219
235
|
}));
|
|
220
236
|
});
|
|
221
|
-
it(
|
|
237
|
+
it('overwrites template files when force is enabled', async () => {
|
|
222
238
|
const root = await createTempDir();
|
|
223
|
-
const governanceDir = path.join(root,
|
|
224
|
-
const readmePath = path.join(governanceDir,
|
|
239
|
+
const governanceDir = path.join(root, '.projitive');
|
|
240
|
+
const readmePath = path.join(governanceDir, 'README.md');
|
|
225
241
|
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(
|
|
242
|
+
await fs.writeFile(readmePath, 'custom-content', 'utf-8');
|
|
243
|
+
const initialized = await initializeProjectStructure(root, '.projitive', true);
|
|
244
|
+
const readmeContent = await fs.readFile(readmePath, 'utf-8');
|
|
245
|
+
expect(readmeContent).toContain('Projitive Governance Workspace');
|
|
246
|
+
expect(initialized.files.find((item) => item.path === readmePath)?.action).toBe('updated');
|
|
231
247
|
});
|
|
232
|
-
it(
|
|
248
|
+
it('uses custom governance directory when specified', async () => {
|
|
233
249
|
const root = await createTempDir();
|
|
234
|
-
const customDir =
|
|
250
|
+
const customDir = 'my-governance';
|
|
235
251
|
const initialized = await initializeProjectStructure(root, customDir);
|
|
236
252
|
expect(initialized.governanceDir).toBe(path.join(root, customDir));
|
|
237
253
|
});
|
|
238
|
-
it(
|
|
254
|
+
it('throws error when project path not found', async () => {
|
|
239
255
|
const root = await createTempDir();
|
|
240
|
-
const nonExistentPath = path.join(root,
|
|
241
|
-
await expect(initializeProjectStructure(nonExistentPath)).rejects.toThrow(
|
|
256
|
+
const nonExistentPath = path.join(root, 'nonexistent');
|
|
257
|
+
await expect(initializeProjectStructure(nonExistentPath)).rejects.toThrow('Path not found');
|
|
242
258
|
});
|
|
243
|
-
it(
|
|
259
|
+
it('throws error when project path is not a directory', async () => {
|
|
244
260
|
const root = await createTempDir();
|
|
245
|
-
const filePath = path.join(root,
|
|
246
|
-
await fs.writeFile(filePath,
|
|
247
|
-
await expect(initializeProjectStructure(filePath)).rejects.toThrow(
|
|
261
|
+
const filePath = path.join(root, 'file.txt');
|
|
262
|
+
await fs.writeFile(filePath, 'content', 'utf-8');
|
|
263
|
+
await expect(initializeProjectStructure(filePath)).rejects.toThrow('projectPath must be a directory');
|
|
248
264
|
});
|
|
249
|
-
it(
|
|
265
|
+
it('uses default governance dir when governanceDir is omitted', async () => {
|
|
250
266
|
const root = await createTempDir();
|
|
251
|
-
// When governanceDir is invalid, it should fall back to default
|
|
252
|
-
// Note: normalizeGovernanceDirName is not exported, so we test initialization behavior
|
|
253
267
|
const initialized = await initializeProjectStructure(root);
|
|
254
|
-
expect(initialized.governanceDir).toBe(path.join(root,
|
|
268
|
+
expect(initialized.governanceDir).toBe(path.join(root, '.projitive'));
|
|
255
269
|
});
|
|
256
|
-
it(
|
|
270
|
+
it('skips existing files when force is disabled', async () => {
|
|
257
271
|
const root = await createTempDir();
|
|
258
|
-
const governanceDir = path.join(root,
|
|
259
|
-
const readmePath = path.join(governanceDir,
|
|
272
|
+
const governanceDir = path.join(root, '.projitive');
|
|
273
|
+
const readmePath = path.join(governanceDir, 'README.md');
|
|
260
274
|
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(
|
|
275
|
+
await fs.writeFile(readmePath, 'custom-content', 'utf-8');
|
|
276
|
+
const initialized = await initializeProjectStructure(root, '.projitive', false);
|
|
277
|
+
const readmeContent = await fs.readFile(readmePath, 'utf-8');
|
|
278
|
+
expect(readmeContent).toBe('custom-content');
|
|
279
|
+
expect(initialized.files.find((item) => item.path === readmePath)?.action).toBe('skipped');
|
|
266
280
|
});
|
|
267
|
-
it(
|
|
281
|
+
it('creates all required subdirectories', async () => {
|
|
268
282
|
const root = await createTempDir();
|
|
269
283
|
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(
|
|
284
|
+
expect(initialized.directories.some(d => d.path.includes('designs'))).toBe(true);
|
|
285
|
+
expect(initialized.directories.some(d => d.path.includes('reports'))).toBe(true);
|
|
286
|
+
expect(initialized.directories.some(d => d.path.includes('templates'))).toBe(true);
|
|
287
|
+
});
|
|
288
|
+
it('throws when governanceDir is an absolute path', async () => {
|
|
289
|
+
const root = await createTempDir();
|
|
290
|
+
await expect(initializeProjectStructure(root, '/absolute/path')).rejects.toThrow('relative directory name');
|
|
291
|
+
});
|
|
292
|
+
it('throws when governanceDir contains path separators', async () => {
|
|
293
|
+
const root = await createTempDir();
|
|
294
|
+
await expect(initializeProjectStructure(root, 'path/with/slash')).rejects.toThrow('path separators');
|
|
295
|
+
});
|
|
296
|
+
it('throws when governanceDir is a dot or double-dot', async () => {
|
|
297
|
+
const root = await createTempDir();
|
|
298
|
+
await expect(initializeProjectStructure(root, '.')).rejects.toThrow('normal directory name');
|
|
299
|
+
await expect(initializeProjectStructure(root, '..')).rejects.toThrow('normal directory name');
|
|
273
300
|
});
|
|
274
301
|
});
|
|
275
|
-
describe(
|
|
276
|
-
describe(
|
|
277
|
-
it(
|
|
278
|
-
expect(toProjectPath(
|
|
279
|
-
expect(toProjectPath(
|
|
302
|
+
describe('utility functions', () => {
|
|
303
|
+
describe('toProjectPath', () => {
|
|
304
|
+
it('returns parent directory of governance dir', () => {
|
|
305
|
+
expect(toProjectPath('/path/to/project/.projitive')).toBe('/path/to/project');
|
|
306
|
+
expect(toProjectPath('/a/b/c')).toBe('/a/b');
|
|
280
307
|
});
|
|
281
308
|
});
|
|
282
|
-
describe(
|
|
283
|
-
it(
|
|
284
|
-
vi.stubEnv(
|
|
285
|
-
expect(resolveScanRoots()).toEqual([
|
|
309
|
+
describe('resolveScanRoots', () => {
|
|
310
|
+
it('uses legacy environment variable when no multi-root env is provided', () => {
|
|
311
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/test/root');
|
|
312
|
+
expect(resolveScanRoots()).toEqual(['/test/root']);
|
|
286
313
|
vi.unstubAllEnvs();
|
|
287
314
|
});
|
|
288
|
-
it(
|
|
289
|
-
vi.stubEnv(
|
|
290
|
-
expect(resolveScanRoots([
|
|
315
|
+
it('uses input paths when provided', () => {
|
|
316
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/test/root');
|
|
317
|
+
expect(resolveScanRoots(['/custom/path', ' /custom/path ', '/second/path'])).toEqual(['/custom/path', '/second/path']);
|
|
291
318
|
vi.unstubAllEnvs();
|
|
292
319
|
});
|
|
293
|
-
it(
|
|
294
|
-
vi.stubEnv(
|
|
295
|
-
expect(resolveScanRoots()).toEqual([
|
|
320
|
+
it('uses PROJITIVE_SCAN_ROOT_PATHS with platform delimiter', () => {
|
|
321
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATHS', ['/root/a', '/root/b', '', ' /root/c '].join(path.delimiter));
|
|
322
|
+
expect(resolveScanRoots()).toEqual(['/root/a', '/root/b', '/root/c']);
|
|
296
323
|
vi.unstubAllEnvs();
|
|
297
324
|
});
|
|
298
|
-
it(
|
|
299
|
-
vi.stubEnv(
|
|
325
|
+
it('treats JSON-like string as plain delimiter input', () => {
|
|
326
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATHS', JSON.stringify(['/json/a', '/json/b']));
|
|
300
327
|
expect(resolveScanRoots()).toHaveLength(1);
|
|
301
328
|
vi.unstubAllEnvs();
|
|
302
329
|
});
|
|
303
|
-
it(
|
|
330
|
+
it('throws error when no root environment variables are configured', () => {
|
|
304
331
|
vi.unstubAllEnvs();
|
|
305
|
-
expect(() => resolveScanRoots()).toThrow(
|
|
332
|
+
expect(() => resolveScanRoots()).toThrow('Missing required environment variable: PROJITIVE_SCAN_ROOT_PATHS');
|
|
306
333
|
});
|
|
307
334
|
});
|
|
308
|
-
describe(
|
|
309
|
-
it(
|
|
310
|
-
vi.stubEnv(
|
|
311
|
-
vi.stubEnv(
|
|
335
|
+
describe('resolveScanDepth', () => {
|
|
336
|
+
it('uses environment variable when no input depth', () => {
|
|
337
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/test/root');
|
|
338
|
+
vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', '5');
|
|
312
339
|
expect(resolveScanDepth()).toBe(5);
|
|
313
340
|
vi.unstubAllEnvs();
|
|
314
341
|
});
|
|
315
|
-
it(
|
|
316
|
-
vi.stubEnv(
|
|
317
|
-
vi.stubEnv(
|
|
342
|
+
it('uses input depth when provided', () => {
|
|
343
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/test/root');
|
|
344
|
+
vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', '5');
|
|
318
345
|
expect(resolveScanDepth(3)).toBe(3);
|
|
319
346
|
vi.unstubAllEnvs();
|
|
320
347
|
});
|
|
321
|
-
it(
|
|
322
|
-
vi.stubEnv(
|
|
323
|
-
vi.stubEnv(
|
|
348
|
+
it('clamps depth to MAX_SCAN_DEPTH', () => {
|
|
349
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/test/root');
|
|
350
|
+
vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', '10');
|
|
324
351
|
expect(resolveScanDepth()).toBe(8);
|
|
325
352
|
vi.unstubAllEnvs();
|
|
326
353
|
});
|
|
327
|
-
it(
|
|
328
|
-
vi.stubEnv(
|
|
329
|
-
vi.stubEnv(
|
|
330
|
-
expect(() => resolveScanDepth()).toThrow(
|
|
354
|
+
it('throws error for invalid depth configuration', () => {
|
|
355
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/test/root');
|
|
356
|
+
vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', 'not-a-number');
|
|
357
|
+
expect(() => resolveScanDepth()).toThrow('Invalid PROJITIVE_SCAN_MAX_DEPTH');
|
|
358
|
+
vi.unstubAllEnvs();
|
|
359
|
+
});
|
|
360
|
+
it('throws when PROJITIVE_SCAN_MAX_DEPTH env var is missing', () => {
|
|
361
|
+
vi.unstubAllEnvs();
|
|
362
|
+
expect(() => resolveScanDepth()).toThrow('Missing required environment variable: PROJITIVE_SCAN_MAX_DEPTH');
|
|
363
|
+
});
|
|
364
|
+
});
|
|
365
|
+
describe('resolveScanRoot', () => {
|
|
366
|
+
it('returns first scan root from env', () => {
|
|
367
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/test/root');
|
|
368
|
+
expect(resolveScanRoot()).toBe('/test/root');
|
|
369
|
+
vi.unstubAllEnvs();
|
|
370
|
+
});
|
|
371
|
+
it('returns normalized path when inputPath is provided', () => {
|
|
372
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATH', '/fallback');
|
|
373
|
+
expect(resolveScanRoot('/custom/path')).toBe('/custom/path');
|
|
331
374
|
vi.unstubAllEnvs();
|
|
332
375
|
});
|
|
333
376
|
});
|
|
334
377
|
});
|
|
335
|
-
describe(
|
|
336
|
-
it(
|
|
378
|
+
describe('registerProjectTools', () => {
|
|
379
|
+
it('registers project tools without throwing', () => {
|
|
337
380
|
const mockServer = {
|
|
338
381
|
registerTool: vi.fn(),
|
|
339
382
|
};
|
|
340
383
|
expect(() => registerProjectTools(mockServer)).not.toThrow();
|
|
341
384
|
expect(mockServer.registerTool).toHaveBeenCalled();
|
|
342
385
|
});
|
|
343
|
-
it(
|
|
386
|
+
it('projectScan lists project root paths instead of governance directories', async () => {
|
|
344
387
|
const root = await createTempDir();
|
|
345
|
-
const projectRoot = path.join(root,
|
|
346
|
-
const governanceDir = path.join(projectRoot,
|
|
388
|
+
const projectRoot = path.join(root, 'app');
|
|
389
|
+
const governanceDir = path.join(projectRoot, '.projitive');
|
|
347
390
|
const templateDir = await createTempDir();
|
|
348
391
|
await fs.mkdir(governanceDir, { recursive: true });
|
|
349
|
-
await fs.writeFile(path.join(governanceDir,
|
|
350
|
-
vi.stubEnv(
|
|
351
|
-
vi.stubEnv(
|
|
352
|
-
vi.stubEnv(
|
|
392
|
+
await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
|
|
393
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATHS', root);
|
|
394
|
+
vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', '3');
|
|
395
|
+
vi.stubEnv('PROJITIVE_MESSAGE_TEMPLATE_PATH', templateDir);
|
|
353
396
|
const mockServer = {
|
|
354
397
|
registerTool: vi.fn(),
|
|
355
398
|
};
|
|
356
399
|
registerProjectTools(mockServer);
|
|
357
|
-
const projectScanCall = mockServer.registerTool.mock.calls.find((call) => call[0] ===
|
|
400
|
+
const projectScanCall = mockServer.registerTool.mock.calls.find((call) => call[0] === 'projectScan');
|
|
358
401
|
expect(projectScanCall).toBeTruthy();
|
|
359
402
|
const projectScanHandler = projectScanCall?.[2];
|
|
403
|
+
expect(projectScanHandler).toBeTruthy();
|
|
360
404
|
const result = await projectScanHandler();
|
|
361
|
-
const markdown = result.content[0]?.text ??
|
|
405
|
+
const markdown = result.content[0]?.text ?? '';
|
|
362
406
|
expect(markdown).toContain(`1. ${projectRoot}`);
|
|
363
407
|
expect(markdown).not.toContain(`1. ${governanceDir}`);
|
|
364
408
|
});
|
|
409
|
+
it('projectScan returns no-project guidance when scan root is empty', async () => {
|
|
410
|
+
const emptyRoot = await createTempDir();
|
|
411
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATHS', emptyRoot);
|
|
412
|
+
vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', '2');
|
|
413
|
+
const mockServer = { registerTool: vi.fn() };
|
|
414
|
+
registerProjectTools(mockServer);
|
|
415
|
+
const projectScan = getProjectToolHandler(mockServer, 'projectScan');
|
|
416
|
+
const result = await projectScan();
|
|
417
|
+
expect(result.isError).toBeUndefined();
|
|
418
|
+
expect(result.content[0].text).toContain('No governance root discovered');
|
|
419
|
+
});
|
|
420
|
+
it('projectNext ranks multiple actionable projects by score', async () => {
|
|
421
|
+
const scanRoot = await createTempDir();
|
|
422
|
+
const projectA = path.join(scanRoot, 'app-a');
|
|
423
|
+
const projectB = path.join(scanRoot, 'app-b');
|
|
424
|
+
await fs.mkdir(projectA, { recursive: true });
|
|
425
|
+
await fs.mkdir(projectB, { recursive: true });
|
|
426
|
+
await initializeProjectStructure(projectA);
|
|
427
|
+
await initializeProjectStructure(projectB);
|
|
428
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATHS', scanRoot);
|
|
429
|
+
vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', '3');
|
|
430
|
+
const mockServer = { registerTool: vi.fn() };
|
|
431
|
+
registerProjectTools(mockServer);
|
|
432
|
+
const projectNext = getProjectToolHandler(mockServer, 'projectNext');
|
|
433
|
+
const result = await projectNext({});
|
|
434
|
+
expect(result.isError).toBeUndefined();
|
|
435
|
+
expect(result.content[0].text).toContain('actionableProjects:');
|
|
436
|
+
});
|
|
437
|
+
it('projectInit handler initializes project structure', async () => {
|
|
438
|
+
const root = await createTempDir();
|
|
439
|
+
const mockServer = { registerTool: vi.fn() };
|
|
440
|
+
registerProjectTools(mockServer);
|
|
441
|
+
const projectInit = getProjectToolHandler(mockServer, 'projectInit');
|
|
442
|
+
const result = await projectInit({ projectPath: root });
|
|
443
|
+
expect(result.isError).toBeUndefined();
|
|
444
|
+
expect(result.content[0].text).toContain('governanceDir:');
|
|
445
|
+
expect(result.content[0].text).toContain('createdFiles:');
|
|
446
|
+
});
|
|
447
|
+
it('projectLocate resolves governance dir from any inner path', async () => {
|
|
448
|
+
const root = await createTempDir();
|
|
449
|
+
const projectRoot = path.join(root, 'myapp');
|
|
450
|
+
const governanceDir = path.join(projectRoot, '.projitive');
|
|
451
|
+
await fs.mkdir(governanceDir, { recursive: true });
|
|
452
|
+
await fs.writeFile(path.join(governanceDir, '.projitive'), '', 'utf-8');
|
|
453
|
+
const mockServer = { registerTool: vi.fn() };
|
|
454
|
+
registerProjectTools(mockServer);
|
|
455
|
+
const projectLocate = getProjectToolHandler(mockServer, 'projectLocate');
|
|
456
|
+
const result = await projectLocate({ inputPath: governanceDir });
|
|
457
|
+
expect(result.isError).toBeUndefined();
|
|
458
|
+
expect(result.content[0].text).toContain(`projectPath: ${projectRoot}`);
|
|
459
|
+
expect(result.content[0].text).toContain(`governanceDir: ${governanceDir}`);
|
|
460
|
+
});
|
|
461
|
+
it('projectNext ranks actionable projects by score', async () => {
|
|
462
|
+
const scanRoot = await createTempDir();
|
|
463
|
+
const projectRoot = path.join(scanRoot, 'myapp');
|
|
464
|
+
await fs.mkdir(projectRoot, { recursive: true });
|
|
465
|
+
await initializeProjectStructure(projectRoot);
|
|
466
|
+
vi.stubEnv('PROJITIVE_SCAN_ROOT_PATHS', scanRoot);
|
|
467
|
+
vi.stubEnv('PROJITIVE_SCAN_MAX_DEPTH', '3');
|
|
468
|
+
const mockServer = { registerTool: vi.fn() };
|
|
469
|
+
registerProjectTools(mockServer);
|
|
470
|
+
const projectNext = getProjectToolHandler(mockServer, 'projectNext');
|
|
471
|
+
const result = await projectNext({});
|
|
472
|
+
expect(result.isError).toBeUndefined();
|
|
473
|
+
expect(result.content[0].text).toContain('actionableProjects:');
|
|
474
|
+
expect(result.content[0].text).toContain('myapp');
|
|
475
|
+
});
|
|
476
|
+
it('projectContext shows task stats and governance artifacts', async () => {
|
|
477
|
+
const root = await createTempDir();
|
|
478
|
+
await initializeProjectStructure(root);
|
|
479
|
+
const mockServer = { registerTool: vi.fn() };
|
|
480
|
+
registerProjectTools(mockServer);
|
|
481
|
+
const projectContext = getProjectToolHandler(mockServer, 'projectContext');
|
|
482
|
+
const result = await projectContext({ projectPath: root });
|
|
483
|
+
expect(result.isError).toBeUndefined();
|
|
484
|
+
expect(result.content[0].text).toContain('Task Summary');
|
|
485
|
+
expect(result.content[0].text).toContain('Artifacts');
|
|
486
|
+
});
|
|
487
|
+
it('syncViews materializes both tasks and roadmap markdown views', async () => {
|
|
488
|
+
const root = await createTempDir();
|
|
489
|
+
await initializeProjectStructure(root);
|
|
490
|
+
const mockServer = { registerTool: vi.fn() };
|
|
491
|
+
registerProjectTools(mockServer);
|
|
492
|
+
const syncViews = getProjectToolHandler(mockServer, 'syncViews');
|
|
493
|
+
const result = await syncViews({ projectPath: root, views: ['tasks', 'roadmap'], force: true });
|
|
494
|
+
expect(result.isError).toBeUndefined();
|
|
495
|
+
expect(result.content[0].text).toContain('tasks.md synced');
|
|
496
|
+
expect(result.content[0].text).toContain('roadmap.md synced');
|
|
497
|
+
});
|
|
365
498
|
});
|
|
366
499
|
});
|