@projitive/mcp 2.0.4 → 2.1.1
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 +1 -1
- package/output/source/common/errors.test.js +59 -0
- package/output/source/common/files.js +20 -9
- package/output/source/common/index.js +1 -0
- package/output/source/common/linter.js +3 -1
- package/output/source/common/response.js +51 -67
- package/output/source/common/tool.js +43 -0
- package/output/source/common/utils.test.js +48 -0
- package/output/source/index.runtime.test.js +57 -0
- package/output/source/prompts/index.test.js +23 -0
- package/output/source/prompts/quickStart.js +58 -22
- package/output/source/prompts/quickStart.test.js +24 -0
- package/output/source/prompts/taskDiscovery.js +13 -4
- package/output/source/prompts/taskDiscovery.test.js +24 -0
- package/output/source/prompts/taskExecution.js +71 -12
- package/output/source/prompts/taskExecution.test.js +27 -0
- package/output/source/resources/designs.resources.test.js +52 -0
- package/output/source/resources/governance.test.js +35 -0
- package/output/source/resources/index.test.js +18 -0
- package/output/source/tools/index.test.js +23 -0
- package/output/source/tools/project.js +210 -257
- package/output/source/tools/project.test.js +136 -4
- package/output/source/tools/roadmap.js +182 -216
- package/output/source/tools/roadmap.test.js +187 -0
- package/output/source/tools/task.js +779 -515
- package/output/source/tools/task.test.js +323 -2
- package/output/source/types.js +6 -0
- package/package.json +1 -1
|
@@ -2,13 +2,18 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from 'vitest';
|
|
5
|
-
import { discoverProjects, discoverProjectsAcrossRoots, hasProjectMarker, initializeProjectStructure, resolveGovernanceDir, resolveScanRoots, resolveScanDepth, toProjectPath, registerProjectTools } from './project.js';
|
|
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
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 });
|
|
@@ -82,6 +87,17 @@ describe('projitive module', () => {
|
|
|
82
87
|
await fs.mkdir(deepDir, { recursive: true });
|
|
83
88
|
await expect(resolveGovernanceDir(deepDir)).rejects.toThrow('No .projitive marker found');
|
|
84
89
|
});
|
|
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
|
+
});
|
|
85
101
|
it('prefers default .projitive directory when multiple governance roots found as children', async () => {
|
|
86
102
|
const root = await createTempDir();
|
|
87
103
|
const childDir = path.join(root, 'child');
|
|
@@ -246,10 +262,8 @@ describe('projitive module', () => {
|
|
|
246
262
|
await fs.writeFile(filePath, 'content', 'utf-8');
|
|
247
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
268
|
expect(initialized.governanceDir).toBe(path.join(root, '.projitive'));
|
|
255
269
|
});
|
|
@@ -271,6 +285,19 @@ describe('projitive module', () => {
|
|
|
271
285
|
expect(initialized.directories.some(d => d.path.includes('reports'))).toBe(true);
|
|
272
286
|
expect(initialized.directories.some(d => d.path.includes('templates'))).toBe(true);
|
|
273
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');
|
|
300
|
+
});
|
|
274
301
|
});
|
|
275
302
|
describe('utility functions', () => {
|
|
276
303
|
describe('toProjectPath', () => {
|
|
@@ -330,6 +357,22 @@ describe('projitive module', () => {
|
|
|
330
357
|
expect(() => resolveScanDepth()).toThrow('Invalid PROJITIVE_SCAN_MAX_DEPTH');
|
|
331
358
|
vi.unstubAllEnvs();
|
|
332
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');
|
|
374
|
+
vi.unstubAllEnvs();
|
|
375
|
+
});
|
|
333
376
|
});
|
|
334
377
|
});
|
|
335
378
|
describe('registerProjectTools', () => {
|
|
@@ -363,5 +406,94 @@ describe('projitive module', () => {
|
|
|
363
406
|
expect(markdown).toContain(`1. ${projectRoot}`);
|
|
364
407
|
expect(markdown).not.toContain(`1. ${governanceDir}`);
|
|
365
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
|
+
});
|
|
366
498
|
});
|
|
367
499
|
});
|