@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.
Files changed (40) hide show
  1. package/output/package.json +8 -2
  2. package/output/source/common/artifacts.js +1 -1
  3. package/output/source/common/artifacts.test.js +11 -11
  4. package/output/source/common/errors.js +19 -19
  5. package/output/source/common/files.js +11 -11
  6. package/output/source/common/files.test.js +14 -14
  7. package/output/source/common/index.js +10 -10
  8. package/output/source/common/linter.js +27 -27
  9. package/output/source/common/linter.test.js +9 -9
  10. package/output/source/common/markdown.js +3 -3
  11. package/output/source/common/markdown.test.js +15 -15
  12. package/output/source/common/response.js +74 -74
  13. package/output/source/common/response.test.js +30 -30
  14. package/output/source/common/store.js +40 -40
  15. package/output/source/common/store.test.js +72 -72
  16. package/output/source/common/types.js +3 -3
  17. package/output/source/common/utils.js +8 -8
  18. package/output/source/index.js +16 -16
  19. package/output/source/index.test.js +64 -64
  20. package/output/source/prompts/index.js +3 -3
  21. package/output/source/prompts/quickStart.js +96 -96
  22. package/output/source/prompts/taskDiscovery.js +184 -184
  23. package/output/source/prompts/taskExecution.js +148 -148
  24. package/output/source/resources/designs.js +26 -26
  25. package/output/source/resources/designs.test.js +88 -88
  26. package/output/source/resources/governance.js +19 -19
  27. package/output/source/resources/index.js +2 -2
  28. package/output/source/resources/readme.js +7 -7
  29. package/output/source/resources/readme.test.js +113 -113
  30. package/output/source/resources/reports.js +10 -10
  31. package/output/source/resources/reports.test.js +83 -83
  32. package/output/source/tools/index.js +3 -3
  33. package/output/source/tools/project.js +191 -191
  34. package/output/source/tools/project.test.js +174 -173
  35. package/output/source/tools/roadmap.js +110 -95
  36. package/output/source/tools/roadmap.test.js +54 -46
  37. package/output/source/tools/task.js +305 -277
  38. package/output/source/tools/task.test.js +117 -110
  39. package/output/source/types.js +22 -22
  40. package/package.json +8 -2
@@ -1,11 +1,11 @@
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";
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(), "projitive-mcp-test-"));
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("projitive module", () => {
19
- describe("hasProjectMarker", () => {
20
- it("does not treat marker directory as a valid project marker", async () => {
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, ".projitive");
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("returns true when .projitive marker file exists", async () => {
27
+ it('returns true when .projitive marker file exists', async () => {
28
28
  const root = await createTempDir();
29
- const markerPath = path.join(root, ".projitive");
30
- await fs.writeFile(markerPath, "", "utf-8");
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("returns false when .projitive marker file does not exist", async () => {
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("handles fs.stat errors gracefully", async () => {
39
+ it('handles fs.stat errors gracefully', async () => {
40
40
  const root = await createTempDir();
41
- vi.spyOn(fs, "stat").mockRejectedValueOnce(new Error("Permission denied"));
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("resolveGovernanceDir", () => {
47
- it("resolves governance dir by walking upwards for .projitive", async () => {
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, "repo", "governance");
50
- const deepDir = path.join(governanceDir, "nested", "module");
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, ".projitive"), "", "utf-8");
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("resolves nested default governance dir when input path is project root", async () => {
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, "repo");
59
- const governanceDir = path.join(projectRoot, ".projitive");
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, ".projitive"), "", "utf-8");
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("resolves nested custom governance dir when input path is project root", async () => {
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, "repo");
68
- const governanceDir = path.join(projectRoot, "governance");
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, ".projitive"), "", "utf-8");
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("throws error when path not found", async () => {
74
+ it('throws error when path not found', async () => {
75
75
  const root = await createTempDir();
76
- const nonExistentPath = path.join(root, "nonexistent");
77
- await expect(resolveGovernanceDir(nonExistentPath)).rejects.toThrow("Path not found");
76
+ const nonExistentPath = path.join(root, 'nonexistent');
77
+ await expect(resolveGovernanceDir(nonExistentPath)).rejects.toThrow('Path not found');
78
78
  });
79
- it("throws error when no .projitive marker found", async () => {
79
+ it('throws error when no .projitive marker found', async () => {
80
80
  const root = await createTempDir();
81
- const deepDir = path.join(root, "a", "b", "c");
81
+ const deepDir = path.join(root, 'a', 'b', 'c');
82
82
  await fs.mkdir(deepDir, { recursive: true });
83
- await expect(resolveGovernanceDir(deepDir)).rejects.toThrow("No .projitive marker found");
83
+ await expect(resolveGovernanceDir(deepDir)).rejects.toThrow('No .projitive marker found');
84
84
  });
85
- it("prefers default .projitive directory when multiple governance roots found as children", async () => {
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, "child");
88
- const governance1 = path.join(childDir, ".projitive");
89
- const governance2 = path.join(childDir, "governance");
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, ".projitive"), "", "utf-8");
93
- await fs.writeFile(path.join(governance2, ".projitive"), "", "utf-8");
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("resolves file path by using its directory", async () => {
97
+ it('resolves file path by using its directory', async () => {
98
98
  const root = await createTempDir();
99
- const governanceDir = path.join(root, ".projitive");
100
- const filePath = path.join(governanceDir, "tasks.md");
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, ".projitive"), "", "utf-8");
103
- await fs.writeFile(filePath, "# Tasks", "utf-8");
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("discoverProjects", () => {
109
- it("discovers projects by marker file", async () => {
108
+ describe('discoverProjects', () => {
109
+ it('discovers projects by marker file', async () => {
110
110
  const root = await createTempDir();
111
- const p1 = path.join(root, "a");
112
- const p2 = path.join(root, "b", "c");
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, ".projitive"), "", "utf-8");
116
- await fs.writeFile(path.join(p2, ".projitive"), "", "utf-8");
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("discovers nested default governance directory under project root", async () => {
121
+ it('discovers nested default governance directory under project root', async () => {
122
122
  const root = await createTempDir();
123
- const projectRoot = path.join(root, "app");
124
- const governanceDir = path.join(projectRoot, ".projitive");
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, ".projitive"), "", "utf-8");
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("discovers nested custom governance directory under project root", async () => {
130
+ it('discovers nested custom governance directory under project root', async () => {
131
131
  const root = await createTempDir();
132
- const projectRoot = path.join(root, "app");
133
- const governanceDir = path.join(projectRoot, "governance");
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, ".projitive"), "", "utf-8");
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("respects maxDepth limit", async () => {
139
+ it('respects maxDepth limit', async () => {
140
140
  const root = await createTempDir();
141
- const shallow = path.join(root, "shallow");
142
- const deep = path.join(root, "level1", "level2", "level3", "level4", "deep");
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, ".projitive"), "", "utf-8");
146
- await fs.writeFile(path.join(deep, ".projitive"), "", "utf-8");
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("ignores common ignore directories", async () => {
151
+ it('ignores common ignore directories', async () => {
152
152
  const root = await createTempDir();
153
- const nodeModulesProject = path.join(root, "node_modules", "project");
154
- const gitProject = path.join(root, ".git", "project");
155
- const validProject = path.join(root, "valid");
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, ".projitive"), "", "utf-8");
160
- await fs.writeFile(path.join(gitProject, ".projitive"), "", "utf-8");
161
- await fs.writeFile(path.join(validProject, ".projitive"), "", "utf-8");
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("returns empty array when no projects found", async () => {
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("returns unique and sorted results", async () => {
172
+ it('returns unique and sorted results', async () => {
173
173
  const root = await createTempDir();
174
- const projectB = path.join(root, "b");
175
- const projectA = path.join(root, "a");
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, ".projitive"), "", "utf-8");
179
- await fs.writeFile(path.join(projectA, ".projitive"), "", "utf-8");
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("handles fs.readdir errors gracefully", async () => {
183
+ it('handles fs.readdir errors gracefully', async () => {
184
184
  const root = await createTempDir();
185
- vi.spyOn(fs, "readdir").mockRejectedValueOnce(new Error("Permission denied"));
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("ignores non-existent roots when scanning across multiple roots", async () => {
189
+ it('ignores non-existent roots when scanning across multiple roots', async () => {
190
190
  const validRoot = await createTempDir();
191
- const validProject = path.join(validRoot, "project-a");
192
- const missingRoot = path.join(validRoot, "__missing_root__");
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, ".projitive"), "", "utf-8");
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("initializeProjectStructure", () => {
200
- it("initializes governance structure under default .projitive directory", async () => {
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, ".projitive"));
203
+ expect(initialized.governanceDir).toBe(path.join(root, '.projitive'));
204
204
  const expectedPaths = [
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"),
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("overwrites template files when force is enabled", async () => {
221
+ it('overwrites template files when force is enabled', async () => {
222
222
  const root = await createTempDir();
223
- const governanceDir = path.join(root, ".projitive");
224
- const readmePath = path.join(governanceDir, "README.md");
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, "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");
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("uses custom governance directory when specified", async () => {
232
+ it('uses custom governance directory when specified', async () => {
233
233
  const root = await createTempDir();
234
- const customDir = "my-governance";
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("throws error when project path not found", async () => {
238
+ it('throws error when project path not found', async () => {
239
239
  const root = await createTempDir();
240
- const nonExistentPath = path.join(root, "nonexistent");
241
- await expect(initializeProjectStructure(nonExistentPath)).rejects.toThrow("Path not found");
240
+ const nonExistentPath = path.join(root, 'nonexistent');
241
+ await expect(initializeProjectStructure(nonExistentPath)).rejects.toThrow('Path not found');
242
242
  });
243
- it("throws error when project path is not a directory", async () => {
243
+ it('throws error when project path is not a directory', async () => {
244
244
  const root = await createTempDir();
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");
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("creates governance structure with default name when invalid names are provided", async () => {
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, ".projitive"));
254
+ expect(initialized.governanceDir).toBe(path.join(root, '.projitive'));
255
255
  });
256
- it("skips existing files when force is disabled", async () => {
256
+ it('skips existing files when force is disabled', async () => {
257
257
  const root = await createTempDir();
258
- const governanceDir = path.join(root, ".projitive");
259
- const readmePath = path.join(governanceDir, "README.md");
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, "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");
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("creates all required subdirectories", async () => {
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("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);
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("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");
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("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"]);
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("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"]);
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("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"]);
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("treats JSON-like string as plain delimiter input", () => {
299
- vi.stubEnv("PROJITIVE_SCAN_ROOT_PATHS", JSON.stringify(["/json/a", "/json/b"]));
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("throws error when no root environment variables are configured", () => {
303
+ it('throws error when no root environment variables are configured', () => {
304
304
  vi.unstubAllEnvs();
305
- expect(() => resolveScanRoots()).toThrow("Missing required environment variable: PROJITIVE_SCAN_ROOT_PATHS");
305
+ expect(() => resolveScanRoots()).toThrow('Missing required environment variable: PROJITIVE_SCAN_ROOT_PATHS');
306
306
  });
307
307
  });
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");
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("uses input depth when provided", () => {
316
- vi.stubEnv("PROJITIVE_SCAN_ROOT_PATH", "/test/root");
317
- vi.stubEnv("PROJITIVE_SCAN_MAX_DEPTH", "5");
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("clamps depth to MAX_SCAN_DEPTH", () => {
322
- vi.stubEnv("PROJITIVE_SCAN_ROOT_PATH", "/test/root");
323
- vi.stubEnv("PROJITIVE_SCAN_MAX_DEPTH", "10");
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("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");
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("registerProjectTools", () => {
336
- it("registers project tools without throwing", () => {
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("projectScan lists project root paths instead of governance directories", async () => {
343
+ it('projectScan lists project root paths instead of governance directories', async () => {
344
344
  const root = await createTempDir();
345
- const projectRoot = path.join(root, "app");
346
- const governanceDir = path.join(projectRoot, ".projitive");
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, ".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);
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] === "projectScan");
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
  });