superkit-mcp-server 1.2.6 → 1.2.8
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/agents/coder.md +7 -0
- package/agents/git-manager.md +7 -0
- package/agents/planner.md +7 -0
- package/agents/reviewer.md +7 -0
- package/agents/scout.md +7 -0
- package/agents/tester.md +7 -0
- package/agents/ui-designer.md +7 -0
- package/build/index.js +275 -22
- package/build/tools/ProjectAssets.js +177 -0
- package/commands/kit-setup.toml +31 -1
- package/package.json +5 -2
- package/build/__tests__/test_apply_prompt_args.js +0 -104
- 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/commands/kit-setup.toml
CHANGED
|
@@ -29,7 +29,8 @@ When you finish answering, I will:
|
|
|
29
29
|
1. Create file `docs/agents/product.md`
|
|
30
30
|
2. Continue asking about Tech Stack
|
|
31
31
|
3. Continue asking about Guidelines
|
|
32
|
-
4.
|
|
32
|
+
4. Ask about Claude Code agent installation (optional)
|
|
33
|
+
5. Complete setup!
|
|
33
34
|
|
|
34
35
|
**You can answer freely or following the format:**
|
|
35
36
|
```
|
|
@@ -38,4 +39,33 @@ Users: [who uses it]
|
|
|
38
39
|
Goals: [goals]
|
|
39
40
|
Features: [main features]
|
|
40
41
|
```
|
|
42
|
+
|
|
43
|
+
---
|
|
44
|
+
|
|
45
|
+
## Step 4: Claude Code Agent Installation (Optional)
|
|
46
|
+
|
|
47
|
+
After completing Steps 1-3, ask the user:
|
|
48
|
+
|
|
49
|
+
> "Are you using **Claude Code** as your AI assistant? If so, I can install the Super-Kit agents directly into your `.claude/agents/` folder so they appear in the `/agents` dialog — giving you one-click access to the Planner, Coder, Scout, Reviewer, Tester, and all other Super-Kit specialists."
|
|
50
|
+
|
|
51
|
+
If the user says **yes**, ask:
|
|
52
|
+
|
|
53
|
+
> "Where would you like to install them?
|
|
54
|
+
> - **a) Global** — `~/.claude/agents/` (available in every project)
|
|
55
|
+
> - **b) Project-local** — `.claude/agents/` in the current directory (this project only)"
|
|
56
|
+
|
|
57
|
+
If the user chooses **global (a)**:
|
|
58
|
+
- Use the `list_superkit_assets` MCP tool to get all agent names
|
|
59
|
+
- For each agent, use the `load_superkit_agent` MCP tool to get its content
|
|
60
|
+
- Write each agent file to `~/.claude/agents/{agent-name}.md`
|
|
61
|
+
- The files already contain the required YAML frontmatter — copy them as-is
|
|
62
|
+
|
|
63
|
+
If the user chooses **project-local (b)**:
|
|
64
|
+
- Same process, but write to `.claude/agents/{agent-name}.md` relative to the current working directory
|
|
65
|
+
- Create the `.claude/agents/` directory if it doesn't exist
|
|
66
|
+
|
|
67
|
+
After writing all files, confirm:
|
|
68
|
+
> "✅ Super-Kit agents installed! Open the `/agents` dialog in Claude Code to see them."
|
|
69
|
+
|
|
70
|
+
If the user says **no** or skips, complete setup without this step.
|
|
41
71
|
"""
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "superkit-mcp-server",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.8",
|
|
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
|
},
|
|
@@ -33,4 +36,4 @@
|
|
|
33
36
|
"README.md",
|
|
34
37
|
"ARCHITECTURE.md"
|
|
35
38
|
]
|
|
36
|
-
}
|
|
39
|
+
}
|
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { apply_prompt_args } from '../index.js';
|
|
3
|
-
describe('apply_prompt_args', () => {
|
|
4
|
-
describe('{{args}} substitution', () => {
|
|
5
|
-
it('should replace {{args}} with the provided user args', () => {
|
|
6
|
-
const result = apply_prompt_args('Task: {{args}}', 'Add user authentication');
|
|
7
|
-
expect(result).toBe('Task: Add user authentication');
|
|
8
|
-
});
|
|
9
|
-
it('should replace all occurrences of {{args}}', () => {
|
|
10
|
-
const result = apply_prompt_args('{{args}} and also {{args}}', 'hello');
|
|
11
|
-
expect(result).toBe('hello and also hello');
|
|
12
|
-
});
|
|
13
|
-
it('should replace {{args}} with an empty string when no args are provided', () => {
|
|
14
|
-
const result = apply_prompt_args('Task: {{args}}', '');
|
|
15
|
-
expect(result).toBe('Task: ');
|
|
16
|
-
});
|
|
17
|
-
it('should replace {{args}} with an empty string when args is only whitespace', () => {
|
|
18
|
-
const result = apply_prompt_args('Task: {{args}}', ' ');
|
|
19
|
-
expect(result).toBe('Task: ');
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
describe('{{#if args}} ... {{/if}} block (no else branch)', () => {
|
|
23
|
-
it('should include the block content when args is provided', () => {
|
|
24
|
-
const template = '{{#if args}}Task: {{args}}{{/if}}';
|
|
25
|
-
const result = apply_prompt_args(template, 'Fix bug');
|
|
26
|
-
expect(result).toBe('Task: Fix bug');
|
|
27
|
-
});
|
|
28
|
-
it('should remove the block entirely when args is empty', () => {
|
|
29
|
-
const template = 'Before\n{{#if args}}Task: {{args}}{{/if}}\nAfter';
|
|
30
|
-
const result = apply_prompt_args(template, '');
|
|
31
|
-
expect(result).toBe('Before\n\nAfter');
|
|
32
|
-
});
|
|
33
|
-
it('should remove the block entirely when args is only whitespace', () => {
|
|
34
|
-
const template = '{{#if args}}Task: {{args}}{{/if}}';
|
|
35
|
-
const result = apply_prompt_args(template, ' ');
|
|
36
|
-
expect(result).toBe('');
|
|
37
|
-
});
|
|
38
|
-
it('should handle multiline block content', () => {
|
|
39
|
-
const template = '{{#if args}}\n# Task\n**Input:** {{args}}\n{{/if}}';
|
|
40
|
-
const result = apply_prompt_args(template, 'Do something');
|
|
41
|
-
expect(result).toBe('\n# Task\n**Input:** Do something\n');
|
|
42
|
-
});
|
|
43
|
-
});
|
|
44
|
-
describe('{{#if args}} ... {{else}} ... {{/if}} block', () => {
|
|
45
|
-
it('should render the if-block when args is provided', () => {
|
|
46
|
-
const template = '{{#if args}}Task: {{args}}{{else}}No task provided.{{/if}}';
|
|
47
|
-
const result = apply_prompt_args(template, 'Add login');
|
|
48
|
-
expect(result).toBe('Task: Add login');
|
|
49
|
-
});
|
|
50
|
-
it('should render the else-block when args is empty', () => {
|
|
51
|
-
const template = '{{#if args}}Task: {{args}}{{else}}No task provided.{{/if}}';
|
|
52
|
-
const result = apply_prompt_args(template, '');
|
|
53
|
-
expect(result).toBe('No task provided.');
|
|
54
|
-
});
|
|
55
|
-
it('should render the else-block when args is only whitespace', () => {
|
|
56
|
-
const template = '{{#if args}}Task: {{args}}{{else}}No task provided.{{/if}}';
|
|
57
|
-
const result = apply_prompt_args(template, ' ');
|
|
58
|
-
expect(result).toBe('No task provided.');
|
|
59
|
-
});
|
|
60
|
-
it('should handle multiline if and else blocks', () => {
|
|
61
|
-
const template = [
|
|
62
|
-
'{{#if args}}',
|
|
63
|
-
'# Running: {{args}}',
|
|
64
|
-
'Execute the task above.',
|
|
65
|
-
'{{else}}',
|
|
66
|
-
'# Usage',
|
|
67
|
-
'Provide a task to run.',
|
|
68
|
-
'{{/if}}',
|
|
69
|
-
].join('\n');
|
|
70
|
-
const with_args = apply_prompt_args(template, 'Build the feature');
|
|
71
|
-
expect(with_args).toContain('# Running: Build the feature');
|
|
72
|
-
expect(with_args).not.toContain('# Usage');
|
|
73
|
-
const without_args = apply_prompt_args(template, '');
|
|
74
|
-
expect(without_args).toContain('# Usage');
|
|
75
|
-
expect(without_args).not.toContain('# Running:');
|
|
76
|
-
});
|
|
77
|
-
});
|
|
78
|
-
describe('edge cases', () => {
|
|
79
|
-
it('should return the template unchanged when it has no placeholders', () => {
|
|
80
|
-
const template = '# Plan\nThis is a static template.';
|
|
81
|
-
const result = apply_prompt_args(template, 'some args');
|
|
82
|
-
expect(result).toBe(template);
|
|
83
|
-
});
|
|
84
|
-
it('should handle a template with both {{args}} and {{#if args}} blocks', () => {
|
|
85
|
-
const template = [
|
|
86
|
-
'{{#if args}}',
|
|
87
|
-
'**Task:** {{args}}',
|
|
88
|
-
'{{else}}',
|
|
89
|
-
'**Task:** (none)',
|
|
90
|
-
'{{/if}}',
|
|
91
|
-
'',
|
|
92
|
-
'Details: {{args}}',
|
|
93
|
-
].join('\n');
|
|
94
|
-
const result = apply_prompt_args(template, 'Refactor auth');
|
|
95
|
-
expect(result).toContain('**Task:** Refactor auth');
|
|
96
|
-
expect(result).toContain('Details: Refactor auth');
|
|
97
|
-
expect(result).not.toContain('(none)');
|
|
98
|
-
});
|
|
99
|
-
it('should handle empty template string', () => {
|
|
100
|
-
const result = apply_prompt_args('', 'some args');
|
|
101
|
-
expect(result).toBe('');
|
|
102
|
-
});
|
|
103
|
-
});
|
|
104
|
-
});
|
|
@@ -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
|
-
});
|