superkit-mcp-server 1.2.5 → 1.2.7
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/ARCHITECTURE.md +52 -3
- package/SUPERKIT.md +32 -2
- package/build/index.js +293 -22
- package/build/tools/ProjectAssets.js +177 -0
- package/package.json +4 -1
- package/build/tools/__tests__/archTools.test.js +0 -42
- package/build/tools/__tests__/compoundTools.test.js +0 -60
- package/build/tools/__tests__/docsTools.test.js +0 -44
- package/build/tools/__tests__/gitTools.test.js +0 -45
- package/build/tools/__tests__/loggerTools.test.js +0 -74
- package/build/tools/__tests__/todoTools.test.js +0 -73
- package/build/tools/validators/__tests__/apiSchema.test.js +0 -77
- package/build/tools/validators/__tests__/convertRules.test.js +0 -38
- package/build/tools/validators/__tests__/frontendDesign.test.js +0 -55
- package/build/tools/validators/__tests__/geoChecker.test.js +0 -45
- package/build/tools/validators/__tests__/i18nChecker.test.js +0 -32
- package/build/tools/validators/__tests__/lintRunner.test.js +0 -65
- package/build/tools/validators/__tests__/mobileAudit.test.js +0 -40
- package/build/tools/validators/__tests__/playwrightRunner.test.js +0 -55
- package/build/tools/validators/__tests__/reactPerformanceChecker.test.js +0 -49
- package/build/tools/validators/__tests__/securityScan.test.js +0 -42
- package/build/tools/validators/__tests__/seoChecker.test.js +0 -44
- package/build/tools/validators/__tests__/testRunner.test.js +0 -49
- package/build/tools/validators/__tests__/typeCoverage.test.js +0 -62
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
import * as fs from 'fs/promises';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
// The conventional folder name for project-local Super-Kit assets
|
|
4
|
+
export const PROJECT_ASSETS_DIR = '.agents';
|
|
5
|
+
/**
|
|
6
|
+
* Resolves the effective project root path.
|
|
7
|
+
* - If an explicit projectPath is provided, it is resolved to an absolute path.
|
|
8
|
+
* - Otherwise, falls back to process.cwd().
|
|
9
|
+
*/
|
|
10
|
+
export function resolve_project_path(projectPath) {
|
|
11
|
+
if (projectPath) {
|
|
12
|
+
return path.resolve(projectPath);
|
|
13
|
+
}
|
|
14
|
+
return process.cwd();
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Returns the absolute path to the .agents/ root for the given project.
|
|
18
|
+
*/
|
|
19
|
+
export function get_project_agents_root(projectPath) {
|
|
20
|
+
return path.join(resolve_project_path(projectPath), PROJECT_ASSETS_DIR);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Internal guard: ensures the resolved path stays strictly within the .agents/ root.
|
|
24
|
+
* Returns the resolved absolute path, or null if a traversal attempt is detected.
|
|
25
|
+
*/
|
|
26
|
+
function safe_project_path(agentsRoot, relative) {
|
|
27
|
+
// Normalize the root so the startsWith check is reliable on all platforms
|
|
28
|
+
const normalizedRoot = path.resolve(agentsRoot);
|
|
29
|
+
const resolved = path.resolve(agentsRoot, relative);
|
|
30
|
+
// On Windows path.resolve produces lower-cased drive letters consistently,
|
|
31
|
+
// so this comparison is safe cross-platform.
|
|
32
|
+
if (!resolved.startsWith(normalizedRoot + path.sep) && resolved !== normalizedRoot) {
|
|
33
|
+
return null; // traversal detected
|
|
34
|
+
}
|
|
35
|
+
return resolved;
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Validates a user-supplied asset name (agentName, skillName, workflowName).
|
|
39
|
+
* Rejects absolute paths and anything containing path-separator characters.
|
|
40
|
+
*/
|
|
41
|
+
function validate_asset_name(name) {
|
|
42
|
+
if (!name || typeof name !== 'string') {
|
|
43
|
+
throw new Error('Asset name must be a non-empty string.');
|
|
44
|
+
}
|
|
45
|
+
if (path.isAbsolute(name)) {
|
|
46
|
+
throw new Error(`Asset name must not be an absolute path: "${name}"`);
|
|
47
|
+
}
|
|
48
|
+
// Reject any component that contains a path separator or navigates upward
|
|
49
|
+
const normalized = path.normalize(name);
|
|
50
|
+
if (normalized.includes('..') || normalized.startsWith('/') || normalized.startsWith('\\')) {
|
|
51
|
+
throw new Error(`Asset name contains invalid path components: "${name}"`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Listing helpers
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
/**
|
|
58
|
+
* Lists project-scoped agent names (without .md extension) from .agents/agents/.
|
|
59
|
+
* Returns an empty array if the directory does not exist.
|
|
60
|
+
*/
|
|
61
|
+
export async function list_project_agents(projectPath) {
|
|
62
|
+
const agentsRoot = get_project_agents_root(projectPath);
|
|
63
|
+
const agentsDir = path.join(agentsRoot, 'agents');
|
|
64
|
+
try {
|
|
65
|
+
const entries = await fs.readdir(agentsDir, { withFileTypes: true });
|
|
66
|
+
return entries
|
|
67
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
68
|
+
.map((e) => e.name.replace(/\.md$/, ''));
|
|
69
|
+
}
|
|
70
|
+
catch {
|
|
71
|
+
// Directory does not exist — graceful degradation
|
|
72
|
+
return [];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Lists project-scoped skill directory names from .agents/skills/tech/ and .agents/skills/meta/.
|
|
77
|
+
* Returns empty arrays for each category if the directories do not exist.
|
|
78
|
+
*/
|
|
79
|
+
export async function list_project_skills(projectPath) {
|
|
80
|
+
const agentsRoot = get_project_agents_root(projectPath);
|
|
81
|
+
const list_category_skills = async (category) => {
|
|
82
|
+
const categoryDir = path.join(agentsRoot, 'skills', category);
|
|
83
|
+
try {
|
|
84
|
+
const entries = await fs.readdir(categoryDir, { withFileTypes: true });
|
|
85
|
+
return entries.filter((e) => e.isDirectory()).map((e) => e.name);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
};
|
|
91
|
+
const [tech, meta] = await Promise.all([
|
|
92
|
+
list_category_skills('tech'),
|
|
93
|
+
list_category_skills('meta'),
|
|
94
|
+
]);
|
|
95
|
+
return { tech, meta };
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Lists project-scoped workflow names (without .md extension) from .agents/workflows/.
|
|
99
|
+
* Returns an empty array if the directory does not exist.
|
|
100
|
+
*/
|
|
101
|
+
export async function list_project_workflows(projectPath) {
|
|
102
|
+
const agentsRoot = get_project_agents_root(projectPath);
|
|
103
|
+
const workflowsDir = path.join(agentsRoot, 'workflows');
|
|
104
|
+
try {
|
|
105
|
+
const entries = await fs.readdir(workflowsDir, { withFileTypes: true });
|
|
106
|
+
return entries
|
|
107
|
+
.filter((e) => e.isFile() && e.name.endsWith('.md'))
|
|
108
|
+
.map((e) => e.name.replace(/\.md$/, ''));
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return [];
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
// ---------------------------------------------------------------------------
|
|
115
|
+
// Loading helpers
|
|
116
|
+
// ---------------------------------------------------------------------------
|
|
117
|
+
/**
|
|
118
|
+
* Loads a project-scoped agent's markdown content from:
|
|
119
|
+
* {projectPath}/.agents/agents/{agentName}.md
|
|
120
|
+
*/
|
|
121
|
+
export async function load_project_agent_file(agentName, projectPath) {
|
|
122
|
+
validate_asset_name(agentName);
|
|
123
|
+
const agentsRoot = get_project_agents_root(projectPath);
|
|
124
|
+
const relative = path.join('agents', `${agentName}.md`);
|
|
125
|
+
const safePath = safe_project_path(agentsRoot, relative);
|
|
126
|
+
if (!safePath) {
|
|
127
|
+
throw new Error(`Path traversal detected for agent name: "${agentName}"`);
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
return await fs.readFile(safePath, 'utf-8');
|
|
131
|
+
}
|
|
132
|
+
catch {
|
|
133
|
+
throw new Error(`Project agent not found: "${agentName}". ` +
|
|
134
|
+
`Expected file at: ${safePath}`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Loads a project-scoped skill's SKILL.md content from:
|
|
139
|
+
* {projectPath}/.agents/skills/{category}/{skillName}/SKILL.md
|
|
140
|
+
*/
|
|
141
|
+
export async function load_project_skill_file(category, skillName, projectPath) {
|
|
142
|
+
validate_asset_name(category);
|
|
143
|
+
validate_asset_name(skillName);
|
|
144
|
+
const agentsRoot = get_project_agents_root(projectPath);
|
|
145
|
+
const relative = path.join('skills', category, skillName, 'SKILL.md');
|
|
146
|
+
const safePath = safe_project_path(agentsRoot, relative);
|
|
147
|
+
if (!safePath) {
|
|
148
|
+
throw new Error(`Path traversal detected for skill: category="${category}", skillName="${skillName}"`);
|
|
149
|
+
}
|
|
150
|
+
try {
|
|
151
|
+
return await fs.readFile(safePath, 'utf-8');
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
throw new Error(`Project skill not found: category="${category}", skillName="${skillName}". ` +
|
|
155
|
+
`Expected SKILL.md at: ${safePath}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* Loads a project-scoped workflow's markdown content from:
|
|
160
|
+
* {projectPath}/.agents/workflows/{workflowName}.md
|
|
161
|
+
*/
|
|
162
|
+
export async function load_project_workflow_file(workflowName, projectPath) {
|
|
163
|
+
validate_asset_name(workflowName);
|
|
164
|
+
const agentsRoot = get_project_agents_root(projectPath);
|
|
165
|
+
const relative = path.join('workflows', `${workflowName}.md`);
|
|
166
|
+
const safePath = safe_project_path(agentsRoot, relative);
|
|
167
|
+
if (!safePath) {
|
|
168
|
+
throw new Error(`Path traversal detected for workflow name: "${workflowName}"`);
|
|
169
|
+
}
|
|
170
|
+
try {
|
|
171
|
+
return await fs.readFile(safePath, 'utf-8');
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
throw new Error(`Project workflow not found: "${workflowName}". ` +
|
|
175
|
+
`Expected file at: ${safePath}`);
|
|
176
|
+
}
|
|
177
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superkit-mcp-server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.7",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "An MCP server for exploring and loading Super-Kit AI agent resources.",
|
|
6
6
|
"main": "build/index.js",
|
|
@@ -8,6 +8,8 @@
|
|
|
8
8
|
"superkit-mcp-server": "./build/index.js"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
+
"clean": "rimraf build",
|
|
12
|
+
"prebuild": "rimraf build",
|
|
11
13
|
"build": "tsc",
|
|
12
14
|
"start": "node build/index.js",
|
|
13
15
|
"dev": "tsc --watch"
|
|
@@ -21,6 +23,7 @@
|
|
|
21
23
|
},
|
|
22
24
|
"devDependencies": {
|
|
23
25
|
"@types/node": "^22.19.13",
|
|
26
|
+
"rimraf": "^6.1.3",
|
|
24
27
|
"typescript": "^5.7.2",
|
|
25
28
|
"vitest": "^4.0.18"
|
|
26
29
|
},
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { completePlan, validateArchitecture } from '../archTools.js';
|
|
3
|
-
import * as fs from 'fs/promises';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import * as os from 'os';
|
|
6
|
-
describe('Arch Tools', () => {
|
|
7
|
-
let tempDir;
|
|
8
|
-
beforeEach(async () => {
|
|
9
|
-
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'arch-test-'));
|
|
10
|
-
});
|
|
11
|
-
afterEach(async () => {
|
|
12
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
13
|
-
});
|
|
14
|
-
it('should complete a plan successfully', async () => {
|
|
15
|
-
const planPath = path.join(tempDir, 'plan1.md');
|
|
16
|
-
await fs.writeFile(planPath, '> Status: Draft\n\n- [x] Done item');
|
|
17
|
-
const res = await completePlan('plan1.md', false, tempDir);
|
|
18
|
-
expect(res).toContain('Plan marked as Implemented');
|
|
19
|
-
const content = await fs.readFile(planPath, 'utf8');
|
|
20
|
-
expect(content).toContain('> Status: Implemented');
|
|
21
|
-
});
|
|
22
|
-
it('should fail to complete a plan if unchecked items exist', async () => {
|
|
23
|
-
const planPath = path.join(tempDir, 'plan2.md');
|
|
24
|
-
await fs.writeFile(planPath, '> Status: Draft\n\n- [ ] Pending item');
|
|
25
|
-
const res = await completePlan('plan2.md', false, tempDir);
|
|
26
|
-
expect(res).toContain('unchecked acceptance criteria found');
|
|
27
|
-
});
|
|
28
|
-
it('should force complete a plan with unchecked items', async () => {
|
|
29
|
-
const planPath = path.join(tempDir, 'plan3.md');
|
|
30
|
-
await fs.writeFile(planPath, '> Status: Draft\n\n- [ ] Pending item');
|
|
31
|
-
const res = await completePlan('plan3.md', true, tempDir);
|
|
32
|
-
expect(res).toContain('Plan marked as Implemented');
|
|
33
|
-
});
|
|
34
|
-
it('should validate architecture counts', async () => {
|
|
35
|
-
const archDir = path.join(tempDir, 'docs', 'architecture');
|
|
36
|
-
await fs.mkdir(archDir, { recursive: true });
|
|
37
|
-
await fs.writeFile(path.join(archDir, 'compound-system.md'), '---\nskills: 1\nworkflows: 0\nscripts: 0\npatterns: 0\n---');
|
|
38
|
-
const res = await validateArchitecture(tempDir);
|
|
39
|
-
expect(res).toContain('Architecture Document is stale!');
|
|
40
|
-
expect(res).toContain('Skills mismatch');
|
|
41
|
-
});
|
|
42
|
-
});
|
|
@@ -1,60 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { compoundSearch, updateSolutionRef, validateCompound, auditStateDrift } from '../compoundTools.js';
|
|
3
|
-
import * as fs from 'fs/promises';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import * as os from 'os';
|
|
6
|
-
describe('Compound Tools', () => {
|
|
7
|
-
let tempDir;
|
|
8
|
-
beforeEach(async () => {
|
|
9
|
-
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'compound-test-'));
|
|
10
|
-
});
|
|
11
|
-
afterEach(async () => {
|
|
12
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
13
|
-
});
|
|
14
|
-
it('should search for solutions', async () => {
|
|
15
|
-
const solDir = path.join(tempDir, 'docs', 'solutions');
|
|
16
|
-
await fs.mkdir(solDir, { recursive: true });
|
|
17
|
-
await fs.writeFile(path.join(solDir, 'test.md'), '# Test Solution\n\nThis mentions React and Next.js.');
|
|
18
|
-
await fs.writeFile(path.join(solDir, 'test2.md'), '# Other Solution\n\nThis mentions Vue.');
|
|
19
|
-
const res = await compoundSearch(['React'], tempDir);
|
|
20
|
-
expect(res).toContain('test.md');
|
|
21
|
-
expect(res).toContain('Test Solution');
|
|
22
|
-
expect(res).not.toContain('test2.md');
|
|
23
|
-
});
|
|
24
|
-
it('should update solution ref', async () => {
|
|
25
|
-
const solDir = path.join(tempDir, 'docs', 'solutions');
|
|
26
|
-
await fs.mkdir(solDir, { recursive: true });
|
|
27
|
-
const filePath = path.join(solDir, 'test.md');
|
|
28
|
-
await fs.writeFile(filePath, '---\ntags: [test]\n---\n# Test Solution');
|
|
29
|
-
const res = await updateSolutionRef([path.relative(tempDir, filePath)], tempDir);
|
|
30
|
-
expect(res).toContain('Updated 1 files');
|
|
31
|
-
const content = await fs.readFile(filePath, 'utf-8');
|
|
32
|
-
expect(content).toContain('last_referenced:');
|
|
33
|
-
});
|
|
34
|
-
it('should validate compound health', async () => {
|
|
35
|
-
const planPath = path.join(tempDir, 'implementation_plan.md');
|
|
36
|
-
await fs.writeFile(planPath, '- [ ] todo 1\n- [ ] todo 2\n');
|
|
37
|
-
const res = await validateCompound(tempDir);
|
|
38
|
-
expect(res).toContain('⚠️ Found 2 unchecked items in implementation_plan.md');
|
|
39
|
-
expect(res).toContain('❌ Validation failed.');
|
|
40
|
-
});
|
|
41
|
-
it('should audit state drift and report drift', async () => {
|
|
42
|
-
const todosDir = path.join(tempDir, 'todos');
|
|
43
|
-
await fs.mkdir(todosDir, { recursive: true });
|
|
44
|
-
const todoPath = path.join(todosDir, '001-pending-p1-test.md');
|
|
45
|
-
await fs.writeFile(todoPath, 'status: pending\n\n- [x] tick 1\n- [x] tick 2');
|
|
46
|
-
const res = await auditStateDrift(tempDir, false);
|
|
47
|
-
expect(res).toContain('DRIFT: 001-pending-p1-test.md');
|
|
48
|
-
expect(res).toContain('Checked: 2/2');
|
|
49
|
-
});
|
|
50
|
-
it('should audit state drift and fix drift', async () => {
|
|
51
|
-
const todosDir = path.join(tempDir, 'todos');
|
|
52
|
-
await fs.mkdir(todosDir, { recursive: true });
|
|
53
|
-
const todoPath = path.join(todosDir, '001-pending-p1-test.md');
|
|
54
|
-
await fs.writeFile(todoPath, 'status: pending\n\n- [x] tick 1\n- [x] tick 2');
|
|
55
|
-
const res = await auditStateDrift(tempDir, true);
|
|
56
|
-
expect(res).toContain('FIXED: 001-pending-p1-test.md');
|
|
57
|
-
const content = await fs.readFile(todoPath, 'utf8');
|
|
58
|
-
expect(content).toContain('status: done');
|
|
59
|
-
});
|
|
60
|
-
});
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { bootstrapFolderDocs, checkDocsFreshness, discoverUndocumentedFolders, validateFolderDocs } from '../docsTools.js';
|
|
3
|
-
import * as fs from 'fs/promises';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import * as os from 'os';
|
|
6
|
-
describe('Docs Tools', () => {
|
|
7
|
-
let tempDir;
|
|
8
|
-
beforeEach(async () => {
|
|
9
|
-
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'docs-test-'));
|
|
10
|
-
});
|
|
11
|
-
afterEach(async () => {
|
|
12
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
13
|
-
});
|
|
14
|
-
it('should bootstrap folder docs', async () => {
|
|
15
|
-
const targetDir = path.join(tempDir, 'src');
|
|
16
|
-
await fs.mkdir(targetDir, { recursive: true });
|
|
17
|
-
await fs.writeFile(path.join(targetDir, 'utils.ts'), 'content');
|
|
18
|
-
const result = await bootstrapFolderDocs('src', tempDir);
|
|
19
|
-
expect(result).toContain('Bootstrapped');
|
|
20
|
-
const readme = await fs.readFile(path.join(targetDir, 'README.md'), 'utf8');
|
|
21
|
-
expect(readme).toContain('# src');
|
|
22
|
-
expect(readme).toContain('`utils.ts`');
|
|
23
|
-
});
|
|
24
|
-
it('should discover undocumented folders', async () => {
|
|
25
|
-
const srcDir = path.join(tempDir, 'src');
|
|
26
|
-
await fs.mkdir(srcDir, { recursive: true });
|
|
27
|
-
await fs.writeFile(path.join(srcDir, 'logic.ts'), 'content');
|
|
28
|
-
// no readme -> should be discovered
|
|
29
|
-
const result = await discoverUndocumentedFolders(tempDir);
|
|
30
|
-
expect(result).toContain('src');
|
|
31
|
-
});
|
|
32
|
-
it('should validate folder docs', async () => {
|
|
33
|
-
const targetDir = path.join(tempDir, 'src');
|
|
34
|
-
await fs.mkdir(targetDir, { recursive: true });
|
|
35
|
-
await fs.writeFile(path.join(targetDir, 'README.md'), '# src\n');
|
|
36
|
-
const result = await validateFolderDocs(false, ['src'], tempDir);
|
|
37
|
-
expect(result).toContain('missing sections: Purpose, Components, Component Details, Changelog');
|
|
38
|
-
});
|
|
39
|
-
it('should check docs freshness without failing', async () => {
|
|
40
|
-
// Just tests the skip-docs flag since git repo isn't present
|
|
41
|
-
const result = await checkDocsFreshness(true, tempDir);
|
|
42
|
-
expect(result).toContain('Skipping documentation freshness check');
|
|
43
|
-
});
|
|
44
|
-
});
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { validateChangelog, archiveCompleted, prePushHousekeeping } from '../gitTools.js';
|
|
3
|
-
import * as fs from 'fs/promises';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import * as os from 'os';
|
|
6
|
-
describe('Git Tools', () => {
|
|
7
|
-
let tempDir;
|
|
8
|
-
beforeEach(async () => {
|
|
9
|
-
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'git-test-'));
|
|
10
|
-
});
|
|
11
|
-
afterEach(async () => {
|
|
12
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
13
|
-
});
|
|
14
|
-
it('should validate changelog', async () => {
|
|
15
|
-
const changelogPath = path.join(tempDir, 'CHANGELOG.md');
|
|
16
|
-
await fs.writeFile(changelogPath, '## [Unreleased]\n## [Unreleased]\n<<<<<<< HEAD');
|
|
17
|
-
const result = await validateChangelog(tempDir);
|
|
18
|
-
expect(result).toContain('Multiple [Unreleased] sections found');
|
|
19
|
-
expect(result).toContain('Merge conflict markers found');
|
|
20
|
-
});
|
|
21
|
-
it('should archive completed items in dry-run mode', async () => {
|
|
22
|
-
await fs.mkdir(path.join(tempDir, 'todos'), { recursive: true });
|
|
23
|
-
const todoPath = path.join(tempDir, 'todos', '001-done-test.md');
|
|
24
|
-
await fs.writeFile(todoPath, 'content');
|
|
25
|
-
const res = await archiveCompleted(tempDir, false);
|
|
26
|
-
expect(res).toContain('DRY-RUN');
|
|
27
|
-
expect(res).toContain('[ARCHIVED] 001-done-test.md');
|
|
28
|
-
// Still exists
|
|
29
|
-
await fs.access(todoPath);
|
|
30
|
-
});
|
|
31
|
-
it('should archive completed items and move them', async () => {
|
|
32
|
-
await fs.mkdir(path.join(tempDir, 'todos'), { recursive: true });
|
|
33
|
-
const todoPath = path.join(tempDir, 'todos', '001-done-test.md');
|
|
34
|
-
await fs.writeFile(todoPath, 'content');
|
|
35
|
-
const res = await archiveCompleted(tempDir, true);
|
|
36
|
-
expect(res).toContain('APPLYING CHANGES');
|
|
37
|
-
const archivedPath = path.join(tempDir, 'todos', 'archive', '001-done-test.md');
|
|
38
|
-
await fs.access(archivedPath); // Should not throw
|
|
39
|
-
});
|
|
40
|
-
it('should run pre-push housekeeping', async () => {
|
|
41
|
-
const res = await prePushHousekeeping(tempDir, false);
|
|
42
|
-
expect(res).toContain('Pre-Push Housekeeping Check');
|
|
43
|
-
expect(res).toContain('All checks passed');
|
|
44
|
-
});
|
|
45
|
-
});
|
|
@@ -1,74 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { logSkill, logWorkflow, rotateLogs } from '../loggerTools.js';
|
|
3
|
-
import * as fs from 'fs/promises';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import * as os from 'os';
|
|
6
|
-
describe('Logger Tools', () => {
|
|
7
|
-
let tempDir;
|
|
8
|
-
beforeEach(async () => {
|
|
9
|
-
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'logger-test-'));
|
|
10
|
-
});
|
|
11
|
-
afterEach(async () => {
|
|
12
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
13
|
-
vi.restoreAllMocks();
|
|
14
|
-
});
|
|
15
|
-
it('should log skill usage', async () => {
|
|
16
|
-
vi.useFakeTimers();
|
|
17
|
-
vi.setSystemTime(new Date('2026-03-04T00:00:00Z'));
|
|
18
|
-
const result = await logSkill('test-skill', 'manual', 'context', tempDir);
|
|
19
|
-
expect(result).toBe('Successfully logged skill usage for test-skill');
|
|
20
|
-
const logFile = path.join(tempDir, 'docs', 'agents', 'logs', 'skill_usage.log');
|
|
21
|
-
const content = await fs.readFile(logFile, 'utf-8');
|
|
22
|
-
expect(content).toContain('2026-03-04T00:00:00Z|test-skill|manual|context\n');
|
|
23
|
-
vi.useRealTimers();
|
|
24
|
-
});
|
|
25
|
-
it('should log workflow usage', async () => {
|
|
26
|
-
vi.useFakeTimers();
|
|
27
|
-
vi.setSystemTime(new Date('2026-03-04T00:00:00Z'));
|
|
28
|
-
const result = await logWorkflow('test-workflow', 'session-123', tempDir);
|
|
29
|
-
expect(result).toBe('Successfully logged workflow usage for test-workflow');
|
|
30
|
-
const logFile = path.join(tempDir, 'docs', 'agents', 'logs', 'workflow_usage.log');
|
|
31
|
-
const content = await fs.readFile(logFile, 'utf-8');
|
|
32
|
-
expect(content).toContain('2026-03-04T00:00:00Z|test-workflow|session-123\n');
|
|
33
|
-
vi.useRealTimers();
|
|
34
|
-
});
|
|
35
|
-
it('should generate a default session id if not provided', async () => {
|
|
36
|
-
vi.useFakeTimers();
|
|
37
|
-
vi.setSystemTime(new Date('2026-03-04T00:00:00Z')); // 1772582400 in seconds
|
|
38
|
-
await logWorkflow('test-workflow', '', tempDir);
|
|
39
|
-
const logFile = path.join(tempDir, 'docs', 'agents', 'logs', 'workflow_usage.log');
|
|
40
|
-
const content = await fs.readFile(logFile, 'utf-8');
|
|
41
|
-
expect(content).toContain('2026-03-04T00:00:00Z|test-workflow|1772582400\n');
|
|
42
|
-
vi.useRealTimers();
|
|
43
|
-
});
|
|
44
|
-
it('should rotate logs older than retention days', async () => {
|
|
45
|
-
const logDir = path.join(tempDir, 'docs', 'agents', 'logs');
|
|
46
|
-
await fs.mkdir(logDir, { recursive: true });
|
|
47
|
-
// Old log line (e.g. 100 days old)
|
|
48
|
-
const oldTimestamp = new Date();
|
|
49
|
-
oldTimestamp.setDate(oldTimestamp.getDate() - 100);
|
|
50
|
-
const oldStr = oldTimestamp.toISOString().replace(/\.[0-9]{3}Z$/, 'Z');
|
|
51
|
-
// New log line (10 days old)
|
|
52
|
-
const newTimestamp = new Date();
|
|
53
|
-
newTimestamp.setDate(newTimestamp.getDate() - 10);
|
|
54
|
-
const newStr = newTimestamp.toISOString().replace(/\.[0-9]{3}Z$/, 'Z');
|
|
55
|
-
const logContent = `${oldStr}|test|manual|context1\n${newStr}|test|manual|context2\n`;
|
|
56
|
-
await fs.writeFile(path.join(logDir, 'skill_usage.log'), logContent);
|
|
57
|
-
const output = await rotateLogs(tempDir, 90);
|
|
58
|
-
expect(output).toContain('Pruned 1 lines');
|
|
59
|
-
const finalContent = await fs.readFile(path.join(logDir, 'skill_usage.log'), 'utf-8');
|
|
60
|
-
expect(finalContent).not.toContain('context1');
|
|
61
|
-
expect(finalContent).toContain('context2');
|
|
62
|
-
});
|
|
63
|
-
it('should report if no logs need rotation', async () => {
|
|
64
|
-
const logDir = path.join(tempDir, 'docs', 'agents', 'logs');
|
|
65
|
-
await fs.mkdir(logDir, { recursive: true });
|
|
66
|
-
const newTimestamp = new Date();
|
|
67
|
-
newTimestamp.setDate(newTimestamp.getDate() - 10);
|
|
68
|
-
const newStr = newTimestamp.toISOString().replace(/\.[0-9]{3}Z$/, 'Z');
|
|
69
|
-
const logContent = `${newStr}|test|manual|context2\n`;
|
|
70
|
-
await fs.writeFile(path.join(logDir, 'skill_usage.log'), logContent);
|
|
71
|
-
const output = await rotateLogs(tempDir, 90);
|
|
72
|
-
expect(output).toContain('(No logs needed rotation)');
|
|
73
|
-
});
|
|
74
|
-
});
|
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
-
import { createTodo, startTodo, doneTodo, getNextTodoId } from '../todoTools.js';
|
|
3
|
-
import * as fs from 'fs/promises';
|
|
4
|
-
import * as path from 'path';
|
|
5
|
-
import * as os from 'os';
|
|
6
|
-
describe('Todo Tools', () => {
|
|
7
|
-
let tempDir;
|
|
8
|
-
beforeEach(async () => {
|
|
9
|
-
tempDir = await fs.mkdtemp(path.join(os.tmpdir(), 'todo-test-'));
|
|
10
|
-
});
|
|
11
|
-
afterEach(async () => {
|
|
12
|
-
await fs.rm(tempDir, { recursive: true, force: true });
|
|
13
|
-
});
|
|
14
|
-
it('should calculate next todo ID correctly', async () => {
|
|
15
|
-
const id1 = await getNextTodoId(tempDir);
|
|
16
|
-
expect(id1).toBe('001');
|
|
17
|
-
await fs.mkdir(path.join(tempDir, 'todos'), { recursive: true });
|
|
18
|
-
await fs.writeFile(path.join(tempDir, 'todos', '005-pending-p2-test.md'), 'test');
|
|
19
|
-
const id2 = await getNextTodoId(tempDir);
|
|
20
|
-
expect(id2).toBe('006');
|
|
21
|
-
});
|
|
22
|
-
it('should create a new todo file', async () => {
|
|
23
|
-
const result = await createTodo('p2', 'Fix Bug', 'The bug is terrible', ['Check 1', 'Check 2'], tempDir);
|
|
24
|
-
expect(result).toContain('001-pending-p2-fix-bug');
|
|
25
|
-
const files = await fs.readdir(path.join(tempDir, 'todos'));
|
|
26
|
-
expect(files).toContain('001-pending-p2-fix-bug.md');
|
|
27
|
-
const content = await fs.readFile(path.join(tempDir, 'todos', '001-pending-p2-fix-bug.md'), 'utf-8');
|
|
28
|
-
expect(content).toContain('status: pending');
|
|
29
|
-
expect(content).toContain('priority: p2');
|
|
30
|
-
expect(content).toContain('- [ ] Check 1');
|
|
31
|
-
});
|
|
32
|
-
it('should start a todo file', async () => {
|
|
33
|
-
await createTodo('p2', 'Fix Bug', 'The bug is terrible', ['Check 1'], tempDir);
|
|
34
|
-
const todoFile = path.join('todos', '001-pending-p2-fix-bug.md');
|
|
35
|
-
const result = await startTodo(todoFile, false, tempDir);
|
|
36
|
-
expect(result).toContain('001-in-progress-p2-fix-bug.md');
|
|
37
|
-
const newPath = path.join(tempDir, 'todos', '001-in-progress-p2-fix-bug.md');
|
|
38
|
-
const content = await fs.readFile(newPath, 'utf-8');
|
|
39
|
-
expect(content).toContain('status: in-progress');
|
|
40
|
-
});
|
|
41
|
-
it('should fail to done a todo if unchecked items exist', async () => {
|
|
42
|
-
await createTodo('p2', 'Fix Bug', 'The bug is terrible', ['Check 1'], tempDir);
|
|
43
|
-
// It's still pending so its name has pending
|
|
44
|
-
const todoFile = path.join('todos', '001-pending-p2-fix-bug.md');
|
|
45
|
-
await expect(doneTodo(todoFile, false, tempDir)).rejects.toThrow(/Unchecked items found/);
|
|
46
|
-
});
|
|
47
|
-
it('should done a todo if force is true with unchecked items', async () => {
|
|
48
|
-
await createTodo('p2', 'Fix Bug', 'The bug is terrible', ['Check 1'], tempDir);
|
|
49
|
-
const todoFile = path.join('todos', '001-pending-p2-fix-bug.md');
|
|
50
|
-
const result = await doneTodo(todoFile, true, tempDir);
|
|
51
|
-
expect(result).toContain('001-done-p2-fix-bug.md');
|
|
52
|
-
});
|
|
53
|
-
it('should done a todo if all items are checked', async () => {
|
|
54
|
-
await createTodo('p2', 'Fix Bug', '', ['Check 1'], tempDir);
|
|
55
|
-
const todoFile = path.normalize(path.join(tempDir, 'todos', '001-pending-p2-fix-bug.md'));
|
|
56
|
-
let content = await fs.readFile(todoFile, 'utf-8');
|
|
57
|
-
content = content.replace('- [ ] Check 1', '- [x] Check 1');
|
|
58
|
-
await fs.writeFile(todoFile, content);
|
|
59
|
-
const result = await doneTodo(path.join('todos', '001-pending-p2-fix-bug.md'), false, tempDir);
|
|
60
|
-
expect(result).toContain('001-done-p2-fix-bug.md');
|
|
61
|
-
const newPath = path.join(tempDir, 'todos', '001-done-p2-fix-bug.md');
|
|
62
|
-
const newContent = await fs.readFile(newPath, 'utf-8');
|
|
63
|
-
expect(newContent).toContain('status: done');
|
|
64
|
-
});
|
|
65
|
-
it('should reject startTodo if in terminal state without force', async () => {
|
|
66
|
-
await createTodo('p2', 'Fix Bug', '', [], tempDir);
|
|
67
|
-
const todoFile = path.normalize(path.join(tempDir, 'todos', '001-pending-p2-fix-bug.md'));
|
|
68
|
-
let content = await fs.readFile(todoFile, 'utf-8');
|
|
69
|
-
content = content.replace('status: pending', 'status: done');
|
|
70
|
-
await fs.writeFile(todoFile, content);
|
|
71
|
-
await expect(startTodo(path.join('todos', '001-pending-p2-fix-bug.md'), false, tempDir)).rejects.toThrow(/terminal state/);
|
|
72
|
-
});
|
|
73
|
-
});
|
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { validatePrismaSchema } from '../schemaValidator.js';
|
|
3
|
-
import { checkApiCode, checkOpenApiSpec } from '../apiValidator.js';
|
|
4
|
-
import * as fs from 'fs/promises';
|
|
5
|
-
vi.mock('fs/promises');
|
|
6
|
-
describe('schemaValidator', () => {
|
|
7
|
-
beforeEach(() => {
|
|
8
|
-
vi.clearAllMocks();
|
|
9
|
-
});
|
|
10
|
-
describe('validatePrismaSchema', () => {
|
|
11
|
-
it('should detect bad model names and missing fields', async () => {
|
|
12
|
-
vi.mocked(fs.readFile).mockResolvedValue(`
|
|
13
|
-
model user {
|
|
14
|
-
name String
|
|
15
|
-
}
|
|
16
|
-
model Post {
|
|
17
|
-
id String @id
|
|
18
|
-
userId String
|
|
19
|
-
createdAt DateTime
|
|
20
|
-
}
|
|
21
|
-
enum role { ADMIN, USER }
|
|
22
|
-
`);
|
|
23
|
-
const issues = await validatePrismaSchema('/mock.prisma');
|
|
24
|
-
expect(issues.some(i => i.includes("Model 'user' should be PascalCase"))).toBe(true);
|
|
25
|
-
expect(issues.some(i => i.includes("Enum 'role' should be PascalCase"))).toBe(true);
|
|
26
|
-
expect(issues.some(i => i.includes("missing createdAt"))).toBe(true); // for user
|
|
27
|
-
expect(issues.some(i => i.includes("adding @@index([userId])"))).toBe(true); // for Post
|
|
28
|
-
});
|
|
29
|
-
});
|
|
30
|
-
});
|
|
31
|
-
describe('apiValidator', () => {
|
|
32
|
-
beforeEach(() => {
|
|
33
|
-
vi.clearAllMocks();
|
|
34
|
-
});
|
|
35
|
-
describe('checkOpenApiSpec', () => {
|
|
36
|
-
it('should validate openapi json', async () => {
|
|
37
|
-
vi.mocked(fs.readFile).mockResolvedValue(JSON.stringify({
|
|
38
|
-
openapi: "3.0.0",
|
|
39
|
-
info: { title: "Test", version: "1", description: "Test API" },
|
|
40
|
-
paths: {
|
|
41
|
-
"/test": { get: { responses: { 200: {} }, description: "desc" } }
|
|
42
|
-
}
|
|
43
|
-
}));
|
|
44
|
-
const res = await checkOpenApiSpec('api.json');
|
|
45
|
-
expect(res.issues.length).toBe(0);
|
|
46
|
-
});
|
|
47
|
-
});
|
|
48
|
-
describe('checkApiCode', () => {
|
|
49
|
-
it('should detect missing api practices', async () => {
|
|
50
|
-
vi.mocked(fs.readFile).mockResolvedValue(`
|
|
51
|
-
function handler() {
|
|
52
|
-
// no try, no status, no security check
|
|
53
|
-
return "hello";
|
|
54
|
-
}
|
|
55
|
-
`);
|
|
56
|
-
const res = await checkApiCode('route.ts');
|
|
57
|
-
expect(res.issues.some(i => i.includes('No error handling'))).toBe(true);
|
|
58
|
-
expect(res.issues.some(i => i.includes('No explicit HTTP status'))).toBe(true);
|
|
59
|
-
expect(res.passed.length).toBe(0);
|
|
60
|
-
});
|
|
61
|
-
it('should pass good practices', async () => {
|
|
62
|
-
vi.mocked(fs.readFile).mockResolvedValue(`
|
|
63
|
-
import { z } from 'zod';
|
|
64
|
-
function handler(req, res) {
|
|
65
|
-
try {
|
|
66
|
-
const jwtToken = "123";
|
|
67
|
-
return res.status(200).send("hello");
|
|
68
|
-
} catch(e) {}
|
|
69
|
-
}
|
|
70
|
-
`);
|
|
71
|
-
const res = await checkApiCode('route.ts');
|
|
72
|
-
expect(res.passed.some(i => i.includes('Error handling'))).toBe(true);
|
|
73
|
-
expect(res.passed.some(i => i.includes('validation present'))).toBe(true);
|
|
74
|
-
expect(res.passed.some(i => i.includes('status codes used'))).toBe(true);
|
|
75
|
-
});
|
|
76
|
-
});
|
|
77
|
-
});
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
-
import { runConvertRules } from '../convertRules.js';
|
|
3
|
-
import * as fs from 'fs/promises';
|
|
4
|
-
vi.mock('fs/promises');
|
|
5
|
-
describe('convertRules', () => {
|
|
6
|
-
beforeEach(() => {
|
|
7
|
-
vi.clearAllMocks();
|
|
8
|
-
});
|
|
9
|
-
it('should fail if the rules directory does not exist', async () => {
|
|
10
|
-
vi.mocked(fs.stat).mockRejectedValue(new Error('ENOENT'));
|
|
11
|
-
const res = await runConvertRules('.');
|
|
12
|
-
expect(res.passed).toBe(false);
|
|
13
|
-
expect(res.report).toContain('[ERROR] Rules directory not found');
|
|
14
|
-
});
|
|
15
|
-
it('should correctly parse frontmatter and generate rules', async () => {
|
|
16
|
-
vi.mocked(fs.stat).mockResolvedValue({ isDirectory: () => true });
|
|
17
|
-
vi.mocked(fs.readdir).mockResolvedValue(['async-waterfall.md']);
|
|
18
|
-
vi.mocked(fs.readFile).mockResolvedValue(`---
|
|
19
|
-
title: Waterfall check
|
|
20
|
-
impact: HIGH
|
|
21
|
-
tags: perf
|
|
22
|
-
---
|
|
23
|
-
Content body of the rule here.`);
|
|
24
|
-
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
|
|
25
|
-
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
|
|
26
|
-
const res = await runConvertRules('.');
|
|
27
|
-
console.log("ACTUAL REPORT:", res.report);
|
|
28
|
-
expect(res.passed).toBe(true);
|
|
29
|
-
expect(res.report).toContain('Generated 8 section files from 1 rules');
|
|
30
|
-
// ensure valid output creation
|
|
31
|
-
expect(fs.writeFile).toHaveBeenCalled();
|
|
32
|
-
const callArgs = vi.mocked(fs.writeFile).mock.calls[0];
|
|
33
|
-
const writtenContent = callArgs[1];
|
|
34
|
-
expect(writtenContent).toContain('## Rule 1.1: Waterfall check');
|
|
35
|
-
expect(writtenContent).toContain('**Impact:** HIGH');
|
|
36
|
-
expect(writtenContent).toContain('Content body of the rule here.');
|
|
37
|
-
});
|
|
38
|
-
});
|