specsmd 0.0.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/README.md +300 -0
- package/bin/cli.js +21 -0
- package/flows/aidlc/README.md +372 -0
- package/flows/aidlc/agents/construction-agent.md +81 -0
- package/flows/aidlc/agents/inception-agent.md +95 -0
- package/flows/aidlc/agents/master-agent.md +61 -0
- package/flows/aidlc/agents/operations-agent.md +89 -0
- package/flows/aidlc/commands/construction-agent.md +63 -0
- package/flows/aidlc/commands/inception-agent.md +55 -0
- package/flows/aidlc/commands/master-agent.md +47 -0
- package/flows/aidlc/commands/operations-agent.md +77 -0
- package/flows/aidlc/context-config.yaml +41 -0
- package/flows/aidlc/memory-bank.yaml +104 -0
- package/flows/aidlc/quick-start.md +315 -0
- package/flows/aidlc/skills/construction/bolt-list.md +163 -0
- package/flows/aidlc/skills/construction/bolt-replan.md +343 -0
- package/flows/aidlc/skills/construction/bolt-start.md +289 -0
- package/flows/aidlc/skills/construction/bolt-status.md +185 -0
- package/flows/aidlc/skills/construction/navigator.md +196 -0
- package/flows/aidlc/skills/inception/bolt-plan.md +338 -0
- package/flows/aidlc/skills/inception/context.md +171 -0
- package/flows/aidlc/skills/inception/intent-create.md +211 -0
- package/flows/aidlc/skills/inception/intent-list.md +124 -0
- package/flows/aidlc/skills/inception/navigator.md +207 -0
- package/flows/aidlc/skills/inception/requirements.md +227 -0
- package/flows/aidlc/skills/inception/review.md +248 -0
- package/flows/aidlc/skills/inception/story-create.md +304 -0
- package/flows/aidlc/skills/inception/units.md +271 -0
- package/flows/aidlc/skills/master/analyze-context.md +132 -0
- package/flows/aidlc/skills/master/answer-question.md +141 -0
- package/flows/aidlc/skills/master/explain-flow.md +146 -0
- package/flows/aidlc/skills/master/project-init.md +281 -0
- package/flows/aidlc/skills/master/route-request.md +126 -0
- package/flows/aidlc/skills/operations/build.md +237 -0
- package/flows/aidlc/skills/operations/deploy.md +259 -0
- package/flows/aidlc/skills/operations/monitor.md +265 -0
- package/flows/aidlc/skills/operations/navigator.md +209 -0
- package/flows/aidlc/skills/operations/verify.md +224 -0
- package/flows/aidlc/templates/construction/bolt-template.md +193 -0
- package/flows/aidlc/templates/construction/bolt-types/bdd-construction-bolt.md +250 -0
- package/flows/aidlc/templates/construction/bolt-types/ddd-construction-bolt/adr-template.md +49 -0
- package/flows/aidlc/templates/construction/bolt-types/ddd-construction-bolt/ddd-01-domain-model-template.md +55 -0
- package/flows/aidlc/templates/construction/bolt-types/ddd-construction-bolt/ddd-02-technical-design-template.md +67 -0
- package/flows/aidlc/templates/construction/bolt-types/ddd-construction-bolt/ddd-03-test-report-template.md +62 -0
- package/flows/aidlc/templates/construction/bolt-types/ddd-construction-bolt.md +528 -0
- package/flows/aidlc/templates/construction/bolt-types/simple-construction-bolt.md +273 -0
- package/flows/aidlc/templates/construction/bolt-types/spike-bolt.md +240 -0
- package/flows/aidlc/templates/construction/bolt-types/tdd-construction-bolt.md +259 -0
- package/flows/aidlc/templates/construction/construction-log-template.md +129 -0
- package/flows/aidlc/templates/construction/standards/coding-standards.md +29 -0
- package/flows/aidlc/templates/construction/standards/system-architecture.md +22 -0
- package/flows/aidlc/templates/construction/standards/tech-stack.md +19 -0
- package/flows/aidlc/templates/inception/inception-log-template.md +134 -0
- package/flows/aidlc/templates/inception/project/README.md +55 -0
- package/flows/aidlc/templates/inception/requirements-template.md +144 -0
- package/flows/aidlc/templates/inception/stories-template.md +38 -0
- package/flows/aidlc/templates/inception/story-template.md +147 -0
- package/flows/aidlc/templates/inception/system-context-template.md +29 -0
- package/flows/aidlc/templates/inception/unit-brief-template.md +177 -0
- package/flows/aidlc/templates/inception/units-template.md +52 -0
- package/flows/aidlc/templates/standards/catalog.yaml +345 -0
- package/flows/aidlc/templates/standards/coding-standards.guide.md +553 -0
- package/flows/aidlc/templates/standards/data-stack.guide.md +162 -0
- package/flows/aidlc/templates/standards/tech-stack.guide.md +280 -0
- package/lib/InstallerFactory.js +36 -0
- package/lib/cli-utils.js +372 -0
- package/lib/constants.js +31 -0
- package/lib/installer.js +314 -0
- package/lib/installers/AntigravityInstaller.js +22 -0
- package/lib/installers/ClaudeInstaller.js +85 -0
- package/lib/installers/ClineInstaller.js +21 -0
- package/lib/installers/CodexInstaller.js +21 -0
- package/lib/installers/CopilotInstaller.js +113 -0
- package/lib/installers/CursorInstaller.js +63 -0
- package/lib/installers/GeminiInstaller.js +75 -0
- package/lib/installers/KiroInstaller.js +22 -0
- package/lib/installers/OpenCodeInstaller.js +22 -0
- package/lib/installers/RooInstaller.js +22 -0
- package/lib/installers/ToolInstaller.js +73 -0
- package/lib/installers/WindsurfInstaller.js +76 -0
- package/lib/markdown-validator.ts +175 -0
- package/lib/yaml-validator.ts +99 -0
- package/package.json +65 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
const ToolInstaller = require('./ToolInstaller');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const CLIUtils = require('../cli-utils');
|
|
5
|
+
const { theme } = CLIUtils;
|
|
6
|
+
|
|
7
|
+
class GeminiInstaller extends ToolInstaller {
|
|
8
|
+
get key() {
|
|
9
|
+
return 'gemini';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get name() {
|
|
13
|
+
return 'Gemini CLI';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get commandsDir() {
|
|
17
|
+
return path.join('.gemini', 'commands');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get detectPath() {
|
|
21
|
+
return '.gemini';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Override to convert markdown to TOML format for Gemini CLI
|
|
26
|
+
*/
|
|
27
|
+
async installCommands(flowPath, config) {
|
|
28
|
+
const targetDir = this.commandsDir;
|
|
29
|
+
console.log(theme.dim(` Installing commands to ${targetDir}/...`));
|
|
30
|
+
await fs.ensureDir(targetDir);
|
|
31
|
+
|
|
32
|
+
const sourceDir = path.join(flowPath, 'commands');
|
|
33
|
+
|
|
34
|
+
if (!await fs.pathExists(sourceDir)) {
|
|
35
|
+
console.log(theme.dim(` No commands folder found at ${sourceDir}`));
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const files = await fs.readdir(sourceDir);
|
|
40
|
+
const installedFiles = [];
|
|
41
|
+
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
if (file.endsWith('.md')) {
|
|
44
|
+
const sourcePath = path.join(sourceDir, file);
|
|
45
|
+
const prefix = (config && config.command && config.command.prefix) ? `${config.command.prefix}-` : '';
|
|
46
|
+
|
|
47
|
+
// Transform .md to .toml for Gemini CLI
|
|
48
|
+
const targetFileName = `specsmd-${prefix}${file}`.replace(/\.md$/, '.toml');
|
|
49
|
+
const targetPath = path.join(targetDir, targetFileName);
|
|
50
|
+
|
|
51
|
+
// Read source content
|
|
52
|
+
const content = await fs.readFile(sourcePath, 'utf8');
|
|
53
|
+
|
|
54
|
+
// Extract description from filename (e.g., "master-agent" -> "Master Agent")
|
|
55
|
+
const agentName = file.replace(/\.md$/, '').replace(/-/g, ' ').replace(/\b\w/g, c => c.toUpperCase());
|
|
56
|
+
|
|
57
|
+
// Convert to TOML format
|
|
58
|
+
const tomlContent = `description = "specsmd ${agentName}"
|
|
59
|
+
prompt = """
|
|
60
|
+
${content}
|
|
61
|
+
"""`;
|
|
62
|
+
|
|
63
|
+
await fs.writeFile(targetPath, tomlContent);
|
|
64
|
+
installedFiles.push(targetFileName);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
if (installedFiles.length > 0) {
|
|
69
|
+
CLIUtils.displayStatus('', `Installed ${installedFiles.length} commands for ${this.name}`, 'success');
|
|
70
|
+
}
|
|
71
|
+
return installedFiles;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
module.exports = GeminiInstaller;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const ToolInstaller = require('./ToolInstaller');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class KiroInstaller extends ToolInstaller {
|
|
5
|
+
get key() {
|
|
6
|
+
return 'kiro';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
get name() {
|
|
10
|
+
return 'Kiro CLI';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get commandsDir() {
|
|
14
|
+
return path.join('.kiro', 'steering');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get detectPath() {
|
|
18
|
+
return '.kiro';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = KiroInstaller;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const ToolInstaller = require('./ToolInstaller');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class OpenCodeInstaller extends ToolInstaller {
|
|
5
|
+
get key() {
|
|
6
|
+
return 'opencode';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
get name() {
|
|
10
|
+
return 'OpenCode';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get commandsDir() {
|
|
14
|
+
return path.join('.opencode', 'agent');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get detectPath() {
|
|
18
|
+
return '.opencode';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = OpenCodeInstaller;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const ToolInstaller = require('./ToolInstaller');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class RooInstaller extends ToolInstaller {
|
|
5
|
+
get key() {
|
|
6
|
+
return 'roo';
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
get name() {
|
|
10
|
+
return 'Roo Code';
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get commandsDir() {
|
|
14
|
+
return path.join('.roo', 'commands');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
get detectPath() {
|
|
18
|
+
return '.roo';
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
module.exports = RooInstaller;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
const fs = require('fs-extra');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const CLIUtils = require('../cli-utils');
|
|
4
|
+
const { theme } = CLIUtils;
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Base class for Agentic Coding Tool Installers
|
|
8
|
+
*/
|
|
9
|
+
class ToolInstaller {
|
|
10
|
+
constructor() {
|
|
11
|
+
if (this.constructor === ToolInstaller) {
|
|
12
|
+
throw new Error("Abstract classes can't be instantiated.");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get key() {
|
|
17
|
+
throw new Error("Method 'key' must be implemented.");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get name() {
|
|
21
|
+
throw new Error("Method 'name' must be implemented.");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
get commandsDir() {
|
|
25
|
+
throw new Error("Method 'commandsDir' must be implemented.");
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
get detectPath() {
|
|
29
|
+
throw new Error("Method 'detectPath' must be implemented.");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async detect() {
|
|
33
|
+
return await fs.pathExists(this.detectPath);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
async installCommands(flowPath, config) {
|
|
37
|
+
const targetCommandsDir = this.commandsDir;
|
|
38
|
+
console.log(theme.dim(` Installing commands to ${targetCommandsDir}/...`));
|
|
39
|
+
await fs.ensureDir(targetCommandsDir);
|
|
40
|
+
|
|
41
|
+
const commandsSourceDir = path.join(flowPath, 'commands');
|
|
42
|
+
|
|
43
|
+
if (!await fs.pathExists(commandsSourceDir)) {
|
|
44
|
+
console.log(theme.warning(` No commands folder found at ${commandsSourceDir}`));
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const commandFiles = await fs.readdir(commandsSourceDir);
|
|
49
|
+
const installedFiles = [];
|
|
50
|
+
|
|
51
|
+
for (const cmdFile of commandFiles) {
|
|
52
|
+
if (cmdFile.endsWith('.md')) {
|
|
53
|
+
try {
|
|
54
|
+
const sourcePath = path.join(commandsSourceDir, cmdFile);
|
|
55
|
+
const prefix = (config && config.command && config.command.prefix) ? `${config.command.prefix}-` : '';
|
|
56
|
+
const targetFileName = `specsmd-${prefix}${cmdFile}`;
|
|
57
|
+
const targetPath = path.join(targetCommandsDir, targetFileName);
|
|
58
|
+
|
|
59
|
+
const content = await fs.readFile(sourcePath, 'utf8');
|
|
60
|
+
await fs.outputFile(targetPath, content, 'utf8');
|
|
61
|
+
installedFiles.push(targetFileName);
|
|
62
|
+
} catch (err) {
|
|
63
|
+
console.log(theme.warning(` Failed to install ${cmdFile}: ${err.message}`));
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
CLIUtils.displayStatus('', `Installed ${installedFiles.length} commands for ${this.name}`, 'success');
|
|
69
|
+
return installedFiles;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = ToolInstaller;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
const ToolInstaller = require('./ToolInstaller');
|
|
2
|
+
const fs = require('fs-extra');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const CLIUtils = require('../cli-utils');
|
|
5
|
+
const { theme } = CLIUtils;
|
|
6
|
+
|
|
7
|
+
class WindsurfInstaller extends ToolInstaller {
|
|
8
|
+
get key() {
|
|
9
|
+
return 'windsurf';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
get name() {
|
|
13
|
+
return 'Windsurf';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
get commandsDir() {
|
|
17
|
+
return path.join('.windsurf', 'workflows');
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
get detectPath() {
|
|
21
|
+
return '.windsurf';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Override to add frontmatter for Windsurf workflows
|
|
26
|
+
*/
|
|
27
|
+
async installCommands(flowPath, config) {
|
|
28
|
+
const targetCommandsDir = this.commandsDir;
|
|
29
|
+
console.log(theme.dim(` Installing workflows to ${targetCommandsDir}/...`));
|
|
30
|
+
await fs.ensureDir(targetCommandsDir);
|
|
31
|
+
|
|
32
|
+
const commandsSourceDir = path.join(flowPath, 'commands');
|
|
33
|
+
|
|
34
|
+
if (!await fs.pathExists(commandsSourceDir)) {
|
|
35
|
+
console.log(theme.warning(` No commands folder found at ${commandsSourceDir}`));
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const commandFiles = await fs.readdir(commandsSourceDir);
|
|
40
|
+
const installedFiles = [];
|
|
41
|
+
|
|
42
|
+
for (const cmdFile of commandFiles) {
|
|
43
|
+
if (cmdFile.endsWith('.md')) {
|
|
44
|
+
const sourcePath = path.join(commandsSourceDir, cmdFile);
|
|
45
|
+
const prefix = (config && config.command && config.command.prefix) ? `${config.command.prefix}-` : '';
|
|
46
|
+
|
|
47
|
+
const targetFileName = `specsmd-${prefix}${cmdFile}`;
|
|
48
|
+
const targetPath = path.join(targetCommandsDir, targetFileName);
|
|
49
|
+
|
|
50
|
+
// Extract agent name from target filename (e.g., "specsmd-master-agent.md" -> "specsmd-master-agent")
|
|
51
|
+
const agentName = targetFileName.replace(/\.md$/, '');
|
|
52
|
+
|
|
53
|
+
// Read source content and add Windsurf frontmatter
|
|
54
|
+
let content = await fs.readFile(sourcePath, 'utf8');
|
|
55
|
+
|
|
56
|
+
// Add Windsurf-specific frontmatter if not present
|
|
57
|
+
if (!content.startsWith('---')) {
|
|
58
|
+
const frontmatter = `---
|
|
59
|
+
description: ${agentName}
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
`;
|
|
63
|
+
content = frontmatter + content;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
await fs.writeFile(targetPath, content);
|
|
67
|
+
installedFiles.push(targetFileName);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
CLIUtils.displayStatus('', `Installed ${installedFiles.length} workflows for ${this.name}`, 'success');
|
|
72
|
+
return installedFiles;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = WindsurfInstaller;
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Template-based markdown validator
|
|
3
|
+
* Validates markdown files against YAML schema definitions
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { unified } from 'unified';
|
|
7
|
+
import remarkParse from 'remark-parse';
|
|
8
|
+
import { visit } from 'unist-util-visit';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as yaml from 'js-yaml';
|
|
11
|
+
import type { Root, Heading, Text } from 'mdast';
|
|
12
|
+
|
|
13
|
+
// Schema types
|
|
14
|
+
export interface SectionRequirement {
|
|
15
|
+
heading: string;
|
|
16
|
+
children?: string[];
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface MarkdownSchema {
|
|
20
|
+
name: string;
|
|
21
|
+
description: string;
|
|
22
|
+
file_pattern: string;
|
|
23
|
+
required_sections: SectionRequirement[];
|
|
24
|
+
optional_sections?: SectionRequirement[];
|
|
25
|
+
rules?: {
|
|
26
|
+
min_content_length?: number;
|
|
27
|
+
must_start_with_h1?: boolean;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ValidationResult {
|
|
32
|
+
valid: boolean;
|
|
33
|
+
errors: string[];
|
|
34
|
+
warnings: string[];
|
|
35
|
+
file?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract all headings from markdown AST
|
|
40
|
+
*/
|
|
41
|
+
function extractHeadings(tree: Root): { text: string; depth: number }[] {
|
|
42
|
+
const headings: { text: string; depth: number }[] = [];
|
|
43
|
+
|
|
44
|
+
visit(tree, 'heading', (node: Heading) => {
|
|
45
|
+
const text = node.children
|
|
46
|
+
.filter((child): child is Text => child.type === 'text')
|
|
47
|
+
.map(child => child.value)
|
|
48
|
+
.join('');
|
|
49
|
+
headings.push({ text, depth: node.depth });
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return headings;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Normalize heading for comparison
|
|
57
|
+
*/
|
|
58
|
+
function normalizeHeading(heading: string): string {
|
|
59
|
+
return heading.toLowerCase().replace(/^#+\s*/, '').trim();
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Check if headings contain a section
|
|
64
|
+
*/
|
|
65
|
+
function hasSection(
|
|
66
|
+
headings: { text: string; depth: number }[],
|
|
67
|
+
sectionPattern: string
|
|
68
|
+
): boolean {
|
|
69
|
+
const normalized = normalizeHeading(sectionPattern);
|
|
70
|
+
return headings.some(h => normalizeHeading(h.text).includes(normalized));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Validate markdown content against a schema
|
|
75
|
+
*/
|
|
76
|
+
export function validateMarkdown(
|
|
77
|
+
content: string,
|
|
78
|
+
schema: MarkdownSchema,
|
|
79
|
+
fileName?: string
|
|
80
|
+
): ValidationResult {
|
|
81
|
+
const errors: string[] = [];
|
|
82
|
+
const warnings: string[] = [];
|
|
83
|
+
const prefix = fileName ? `${fileName}: ` : '';
|
|
84
|
+
|
|
85
|
+
// Parse markdown
|
|
86
|
+
const tree = unified().use(remarkParse).parse(content) as Root;
|
|
87
|
+
const headings = extractHeadings(tree);
|
|
88
|
+
|
|
89
|
+
// Check required sections
|
|
90
|
+
for (const req of schema.required_sections) {
|
|
91
|
+
const sectionExists = hasSection(headings, req.heading);
|
|
92
|
+
|
|
93
|
+
if (!sectionExists) {
|
|
94
|
+
errors.push(`${prefix}Missing required section "${req.heading}"`);
|
|
95
|
+
} else if (req.children) {
|
|
96
|
+
// Check child sections
|
|
97
|
+
for (const child of req.children) {
|
|
98
|
+
if (!hasSection(headings, child)) {
|
|
99
|
+
errors.push(`${prefix}Missing required subsection "${child}"`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Check optional sections (warnings only)
|
|
106
|
+
if (schema.optional_sections) {
|
|
107
|
+
for (const opt of schema.optional_sections) {
|
|
108
|
+
if (!hasSection(headings, opt.heading)) {
|
|
109
|
+
warnings.push(`${prefix}Missing optional section "${opt.heading}"`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Check rules
|
|
115
|
+
if (schema.rules) {
|
|
116
|
+
if (schema.rules.min_content_length && content.length < schema.rules.min_content_length) {
|
|
117
|
+
errors.push(`${prefix}Content too short (${content.length} < ${schema.rules.min_content_length})`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (schema.rules.must_start_with_h1) {
|
|
121
|
+
const firstHeading = headings[0];
|
|
122
|
+
if (!firstHeading || firstHeading.depth !== 1) {
|
|
123
|
+
errors.push(`${prefix}Must start with H1 heading`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return {
|
|
129
|
+
valid: errors.length === 0,
|
|
130
|
+
errors,
|
|
131
|
+
warnings,
|
|
132
|
+
file: fileName,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Load a schema from YAML file
|
|
138
|
+
*/
|
|
139
|
+
export function loadSchema(schemaPath: string): MarkdownSchema {
|
|
140
|
+
const content = fs.readFileSync(schemaPath, 'utf-8');
|
|
141
|
+
return yaml.load(content) as MarkdownSchema;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Validate a file against a schema
|
|
146
|
+
*/
|
|
147
|
+
export function validateFile(
|
|
148
|
+
filePath: string,
|
|
149
|
+
schema: MarkdownSchema
|
|
150
|
+
): ValidationResult {
|
|
151
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
152
|
+
return validateMarkdown(content, schema, filePath);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Validate multiple files against a schema
|
|
157
|
+
*/
|
|
158
|
+
export function validateFiles(
|
|
159
|
+
filePaths: string[],
|
|
160
|
+
schema: MarkdownSchema
|
|
161
|
+
): { results: ValidationResult[]; summary: ValidationResult } {
|
|
162
|
+
const results = filePaths.map(fp => validateFile(fp, schema));
|
|
163
|
+
|
|
164
|
+
const allErrors = results.flatMap(r => r.errors);
|
|
165
|
+
const allWarnings = results.flatMap(r => r.warnings);
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
results,
|
|
169
|
+
summary: {
|
|
170
|
+
valid: allErrors.length === 0,
|
|
171
|
+
errors: allErrors,
|
|
172
|
+
warnings: allWarnings,
|
|
173
|
+
},
|
|
174
|
+
};
|
|
175
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Simple YAML config validator
|
|
3
|
+
* Validates required keys and structure without strict schema
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import * as fs from 'fs';
|
|
7
|
+
import * as yaml from 'js-yaml';
|
|
8
|
+
|
|
9
|
+
export interface YamlSchema {
|
|
10
|
+
name: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
required_keys: string[];
|
|
13
|
+
nested_required?: Record<string, string[]>;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export interface ValidationResult {
|
|
17
|
+
valid: boolean;
|
|
18
|
+
errors: string[];
|
|
19
|
+
file?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check if an object has a key (supports dot notation)
|
|
24
|
+
*/
|
|
25
|
+
function hasKey(obj: Record<string, unknown>, key: string): boolean {
|
|
26
|
+
const parts = key.split('.');
|
|
27
|
+
let current: unknown = obj;
|
|
28
|
+
|
|
29
|
+
for (const part of parts) {
|
|
30
|
+
if (current === null || current === undefined || typeof current !== 'object') {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
if (!(part in (current as Record<string, unknown>))) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
current = (current as Record<string, unknown>)[part];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate YAML content against a schema
|
|
44
|
+
*/
|
|
45
|
+
export function validateYaml(
|
|
46
|
+
content: Record<string, unknown>,
|
|
47
|
+
schema: YamlSchema,
|
|
48
|
+
fileName?: string
|
|
49
|
+
): ValidationResult {
|
|
50
|
+
const errors: string[] = [];
|
|
51
|
+
const prefix = fileName ? `${fileName}: ` : '';
|
|
52
|
+
|
|
53
|
+
// Check required keys
|
|
54
|
+
for (const key of schema.required_keys) {
|
|
55
|
+
if (!hasKey(content, key)) {
|
|
56
|
+
errors.push(`${prefix}Missing required key "${key}"`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Check nested required keys
|
|
61
|
+
if (schema.nested_required) {
|
|
62
|
+
for (const [parent, children] of Object.entries(schema.nested_required)) {
|
|
63
|
+
if (hasKey(content, parent)) {
|
|
64
|
+
for (const child of children) {
|
|
65
|
+
const fullKey = `${parent}.${child}`;
|
|
66
|
+
if (!hasKey(content, fullKey)) {
|
|
67
|
+
errors.push(`${prefix}Missing required key "${fullKey}"`);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
valid: errors.length === 0,
|
|
76
|
+
errors,
|
|
77
|
+
file: fileName,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Validate a YAML file against a schema
|
|
83
|
+
*/
|
|
84
|
+
export function validateYamlFile(
|
|
85
|
+
filePath: string,
|
|
86
|
+
schema: YamlSchema
|
|
87
|
+
): ValidationResult {
|
|
88
|
+
const content = fs.readFileSync(filePath, 'utf-8');
|
|
89
|
+
const parsed = yaml.load(content) as Record<string, unknown>;
|
|
90
|
+
return validateYaml(parsed, schema, filePath);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Load a schema from YAML file
|
|
95
|
+
*/
|
|
96
|
+
export function loadYamlSchema(schemaPath: string): YamlSchema {
|
|
97
|
+
const content = fs.readFileSync(schemaPath, 'utf-8');
|
|
98
|
+
return yaml.load(content) as YamlSchema;
|
|
99
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "specsmd",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Multi-agent orchestration system for AI-native software development. Delivers AI-DLC, Agile, and custom SDLC flows as markdown-based agent systems.",
|
|
5
|
+
"main": "lib/installer.js",
|
|
6
|
+
"bin": {
|
|
7
|
+
"specsmd": "./bin/cli.js"
|
|
8
|
+
},
|
|
9
|
+
"scripts": {
|
|
10
|
+
"test": "vitest run",
|
|
11
|
+
"test:watch": "vitest",
|
|
12
|
+
"test:schema": "vitest run __tests__/unit/schema-validation/",
|
|
13
|
+
"lint:md": "markdownlint 'specs/**/*.md' 'flows/**/*.md' --config ../.markdownlint.yaml",
|
|
14
|
+
"lint:md:fix": "markdownlint 'specs/**/*.md' 'flows/**/*.md' --config ../.markdownlint.yaml --fix",
|
|
15
|
+
"validate": "npm run test:schema && npm run lint:md"
|
|
16
|
+
},
|
|
17
|
+
"keywords": [
|
|
18
|
+
"ai-dlc",
|
|
19
|
+
"agile",
|
|
20
|
+
"sdlc",
|
|
21
|
+
"claude-code",
|
|
22
|
+
"cursor",
|
|
23
|
+
"copilot",
|
|
24
|
+
"ai-native",
|
|
25
|
+
"agents",
|
|
26
|
+
"workflow"
|
|
27
|
+
],
|
|
28
|
+
"author": "specsmd team",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/fabriqaai/specsmd.git"
|
|
33
|
+
},
|
|
34
|
+
"files": [
|
|
35
|
+
"bin/",
|
|
36
|
+
"flows/",
|
|
37
|
+
"lib/",
|
|
38
|
+
"README.md"
|
|
39
|
+
],
|
|
40
|
+
"dependencies": {
|
|
41
|
+
"chalk": "^4.1.2",
|
|
42
|
+
"commander": "^11.1.0",
|
|
43
|
+
"figlet": "^1.9.4",
|
|
44
|
+
"fs-extra": "^11.1.1",
|
|
45
|
+
"gradient-string": "^2.0.2",
|
|
46
|
+
"js-yaml": "^4.1.0",
|
|
47
|
+
"oh-my-logo": "^0.4.0",
|
|
48
|
+
"prompts": "^2.4.2"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=14.0.0"
|
|
52
|
+
},
|
|
53
|
+
"devDependencies": {
|
|
54
|
+
"@types/js-yaml": "^4.0.9",
|
|
55
|
+
"@types/node": "^24.10.2",
|
|
56
|
+
"glob": "^13.0.0",
|
|
57
|
+
"markdownlint": "^0.40.0",
|
|
58
|
+
"markdownlint-cli": "^0.46.0",
|
|
59
|
+
"remark-parse": "^11.0.0",
|
|
60
|
+
"typescript": "^5.9.3",
|
|
61
|
+
"unified": "^11.0.5",
|
|
62
|
+
"unist-util-visit": "^5.0.0",
|
|
63
|
+
"vitest": "^4.0.15"
|
|
64
|
+
}
|
|
65
|
+
}
|