@zebralabs/context-cli 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/application/compile/compile-context.js +119 -0
- package/src/application/compile/stage1-discovery.js +125 -0
- package/src/application/compile/stage2-extraction.js +97 -0
- package/src/application/compile/stage4-consolidation.js +235 -0
- package/src/application/compile/stage5-assets.js +61 -0
- package/src/application/compile/stage6-validation.js +133 -0
- package/src/application/ports/asset-generator.js +33 -0
- package/src/context.js +280 -15
- package/src/domain/compilation.js +77 -0
- package/src/domain/preference.js +23 -0
- package/src/domain/rule.js +71 -0
- package/src/domain/scope.js +30 -0
- package/src/infrastructure/assets/claude/claude-generator.js +95 -0
- package/src/infrastructure/assets/cursor/cursor-rules-generator.js +119 -0
- package/src/infrastructure/assets/cursor/cursor-skills-generator.js +115 -0
- package/src/infrastructure/file-system/file-reader.js +67 -0
- package/src/infrastructure/file-system/file-writer.js +40 -0
- package/src/infrastructure/parsing/markdown-parser.js +95 -0
- package/src/infrastructure/parsing/rule-extractor.js +219 -0
- package/src/infrastructure/parsing/skill-extractor.js +74 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule value object
|
|
3
|
+
* Represents a single rule extracted from documentation
|
|
4
|
+
*/
|
|
5
|
+
export class Rule {
|
|
6
|
+
/**
|
|
7
|
+
* @param {Object} data
|
|
8
|
+
* @param {string} data.id - Rule ID (e.g., "doc-structure-001")
|
|
9
|
+
* @param {string} data.level - Rule level: "must" | "should" | "prefer" | "avoid"
|
|
10
|
+
* @param {string} data.category - Category extracted from rule ID prefix
|
|
11
|
+
* @param {string[]} data.appliesTo - What this rule applies to
|
|
12
|
+
* @param {string} data.rule - The rule statement
|
|
13
|
+
* @param {string} [data.rationale] - Why this rule exists
|
|
14
|
+
* @param {Object} data.source - Source information
|
|
15
|
+
* @param {string} data.source.pack - Pack ID
|
|
16
|
+
* @param {string} data.source.packVersion - Pack version
|
|
17
|
+
* @param {string} data.source.file - Source file path
|
|
18
|
+
* @param {number} data.source.line - Line number where rule starts
|
|
19
|
+
* @param {number} data.source.precedence - Precedence index (lower = higher priority)
|
|
20
|
+
*/
|
|
21
|
+
constructor(data) {
|
|
22
|
+
this.id = data.id;
|
|
23
|
+
this.level = data.level;
|
|
24
|
+
this.category = data.category;
|
|
25
|
+
this.appliesTo = data.appliesTo;
|
|
26
|
+
this.rule = data.rule;
|
|
27
|
+
this.rationale = data.rationale;
|
|
28
|
+
this.source = data.source;
|
|
29
|
+
this.conflicts = []; // Populated during conflict resolution
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extract category from rule ID
|
|
34
|
+
* @param {string} ruleId - Rule ID (e.g., "doc-structure-001")
|
|
35
|
+
* @returns {string} Category (e.g., "doc-structure")
|
|
36
|
+
*/
|
|
37
|
+
static extractCategory(ruleId) {
|
|
38
|
+
const match = ruleId.match(/^([a-z0-9-]+)-\d+$/);
|
|
39
|
+
return match ? match[1] : "unknown";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Validate rule level
|
|
44
|
+
* @param {string} level - Level to validate
|
|
45
|
+
* @returns {boolean} True if valid
|
|
46
|
+
*/
|
|
47
|
+
static isValidLevel(level) {
|
|
48
|
+
return ["must", "should", "prefer", "avoid"].includes(level);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Validate rule ID format
|
|
53
|
+
* @param {string} ruleId - Rule ID to validate
|
|
54
|
+
* @returns {boolean} True if valid
|
|
55
|
+
*/
|
|
56
|
+
static isValidId(ruleId) {
|
|
57
|
+
return /^[a-z0-9-]+$/.test(ruleId);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Check if this rule applies to the given scope
|
|
62
|
+
* @param {string|string[]} scope - Scope to check
|
|
63
|
+
* @returns {boolean} True if rule applies
|
|
64
|
+
*/
|
|
65
|
+
appliesToScope(scope) {
|
|
66
|
+
if (this.appliesTo.includes("all")) return true;
|
|
67
|
+
const scopes = Array.isArray(scope) ? scope : [scope];
|
|
68
|
+
return scopes.some(s => this.appliesTo.includes(s));
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Scope value object
|
|
3
|
+
* Represents what rules/preferences apply to
|
|
4
|
+
*/
|
|
5
|
+
export class Scope {
|
|
6
|
+
/**
|
|
7
|
+
* @param {Object} data
|
|
8
|
+
* @param {string[]} data.appliesTo - What this applies to
|
|
9
|
+
* @param {string[]} [data.doesNotApplyTo] - What this does not apply to
|
|
10
|
+
* @param {Object} data.source - Source information
|
|
11
|
+
* @param {string} data.source.file - Source file path
|
|
12
|
+
*/
|
|
13
|
+
constructor(data) {
|
|
14
|
+
this.appliesTo = data.appliesTo || [];
|
|
15
|
+
this.doesNotApplyTo = data.doesNotApplyTo || [];
|
|
16
|
+
this.source = data.source;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Check if scope applies to the given item
|
|
21
|
+
* @param {string} item - Item to check
|
|
22
|
+
* @returns {boolean} True if applies
|
|
23
|
+
*/
|
|
24
|
+
appliesToItem(item) {
|
|
25
|
+
if (this.doesNotApplyTo.includes(item)) return false;
|
|
26
|
+
if (this.appliesTo.includes("all")) return true;
|
|
27
|
+
return this.appliesTo.includes(item);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { IAssetGenerator } from "../../../application/ports/asset-generator.js";
|
|
2
|
+
import { FileWriter } from "../../file-system/file-writer.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Claude Generator Adapter
|
|
7
|
+
* Generates CLAUDE.md file from consolidated rules
|
|
8
|
+
*/
|
|
9
|
+
export class ClaudeGenerator extends IAssetGenerator {
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
this.fileWriter = new FileWriter();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getToolName() {
|
|
16
|
+
return "claude";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async generate(compilation, outputPath) {
|
|
20
|
+
const claudePath = path.join(outputPath, "CLAUDE.md");
|
|
21
|
+
const content = this.generateClaudeContent(compilation);
|
|
22
|
+
this.fileWriter.writeFile(claudePath, content);
|
|
23
|
+
|
|
24
|
+
return {
|
|
25
|
+
filesGenerated: ["CLAUDE.md"],
|
|
26
|
+
outputPath: claudePath
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
merge(assets) {
|
|
31
|
+
// For MVP, just return assets as-is
|
|
32
|
+
return assets;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Generate CLAUDE.md content
|
|
37
|
+
*/
|
|
38
|
+
generateClaudeContent(compilation) {
|
|
39
|
+
const lines = [];
|
|
40
|
+
lines.push("# Engineering & Documentation Standards");
|
|
41
|
+
lines.push("");
|
|
42
|
+
lines.push("This document contains the consolidated standards and rules for this project.");
|
|
43
|
+
lines.push("");
|
|
44
|
+
lines.push("## Overview");
|
|
45
|
+
lines.push("");
|
|
46
|
+
lines.push(`- Total Rules: ${compilation.rules.length}`);
|
|
47
|
+
lines.push(`- Categories: ${compilation.getCategories().join(", ")}`);
|
|
48
|
+
lines.push("");
|
|
49
|
+
lines.push("---");
|
|
50
|
+
lines.push("");
|
|
51
|
+
|
|
52
|
+
// Include all "must" and "should" rules
|
|
53
|
+
const mustRules = compilation.getRulesByLevel("must");
|
|
54
|
+
const shouldRules = compilation.getRulesByLevel("should");
|
|
55
|
+
|
|
56
|
+
if (mustRules.length > 0) {
|
|
57
|
+
lines.push("## Must Rules");
|
|
58
|
+
lines.push("");
|
|
59
|
+
for (const rule of mustRules) {
|
|
60
|
+
lines.push(`### ${rule.id}`);
|
|
61
|
+
lines.push(`- **Applies to:** ${rule.appliesTo.join(", ")}`);
|
|
62
|
+
lines.push(`- ${rule.rule}`);
|
|
63
|
+
if (rule.rationale) {
|
|
64
|
+
lines.push(`- *Rationale:* ${rule.rationale}`);
|
|
65
|
+
}
|
|
66
|
+
lines.push("");
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (shouldRules.length > 0) {
|
|
71
|
+
lines.push("## Should Rules");
|
|
72
|
+
lines.push("");
|
|
73
|
+
for (const rule of shouldRules) {
|
|
74
|
+
lines.push(`### ${rule.id}`);
|
|
75
|
+
lines.push(`- **Applies to:** ${rule.appliesTo.join(", ")}`);
|
|
76
|
+
lines.push(`- ${rule.rule}`);
|
|
77
|
+
if (rule.rationale) {
|
|
78
|
+
lines.push(`- *Rationale:* ${rule.rationale}`);
|
|
79
|
+
}
|
|
80
|
+
lines.push("");
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
lines.push("---");
|
|
85
|
+
lines.push("");
|
|
86
|
+
lines.push("## References");
|
|
87
|
+
lines.push("");
|
|
88
|
+
lines.push("For complete rules including preferences and detailed documentation:");
|
|
89
|
+
lines.push("- See [CONSOLIDATED-STANDARDS.md](./CONSOLIDATED-STANDARDS.md)");
|
|
90
|
+
lines.push("- See [RULES-INDEX.md](./RULES-INDEX.md)");
|
|
91
|
+
|
|
92
|
+
return lines.join("\n");
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import { IAssetGenerator } from "../../../application/ports/asset-generator.js";
|
|
2
|
+
import { FileWriter } from "../../file-system/file-writer.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cursor Rules Generator Adapter
|
|
7
|
+
* Generates .cursor/rules/*.md files from consolidated rules
|
|
8
|
+
*/
|
|
9
|
+
export class CursorRulesGenerator extends IAssetGenerator {
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
this.fileWriter = new FileWriter();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getToolName() {
|
|
16
|
+
return "cursor";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async generate(compilation, outputPath) {
|
|
20
|
+
const rulesDir = path.join(outputPath, ".cursor", "rules");
|
|
21
|
+
this.fileWriter.ensureDir(rulesDir);
|
|
22
|
+
|
|
23
|
+
// Generate index.md
|
|
24
|
+
const indexContent = this.generateIndex(compilation);
|
|
25
|
+
this.fileWriter.writeFile(path.join(rulesDir, "index.md"), indexContent);
|
|
26
|
+
|
|
27
|
+
// Generate one file per category
|
|
28
|
+
const categories = compilation.getCategories();
|
|
29
|
+
const allFiles = ["index.md"];
|
|
30
|
+
for (const category of categories) {
|
|
31
|
+
const rules = compilation.getRulesByCategory(category);
|
|
32
|
+
const categoryContent = this.generateCategoryFile(category, rules, compilation);
|
|
33
|
+
const fileName = `${category}.md`;
|
|
34
|
+
this.fileWriter.writeFile(path.join(rulesDir, fileName), categoryContent);
|
|
35
|
+
allFiles.push(fileName);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
return {
|
|
39
|
+
filesGenerated: allFiles,
|
|
40
|
+
outputDir: rulesDir
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
merge(assets) {
|
|
45
|
+
// For MVP, just return assets as-is
|
|
46
|
+
// In Phase 2, implement proper merging logic
|
|
47
|
+
return assets;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generate index.md for Cursor rules
|
|
52
|
+
*/
|
|
53
|
+
generateIndex(compilation) {
|
|
54
|
+
const lines = [];
|
|
55
|
+
lines.push("# Cursor Rules");
|
|
56
|
+
lines.push("");
|
|
57
|
+
lines.push("This directory contains rules extracted from knowledge packs.");
|
|
58
|
+
lines.push("");
|
|
59
|
+
lines.push("## Categories");
|
|
60
|
+
lines.push("");
|
|
61
|
+
|
|
62
|
+
const categories = compilation.getCategories();
|
|
63
|
+
for (const category of categories) {
|
|
64
|
+
const rules = compilation.getRulesByCategory(category);
|
|
65
|
+
lines.push(`- [${category}](./${category}.md) (${rules.length} rules)`);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
lines.push("");
|
|
69
|
+
lines.push("## Quick Reference");
|
|
70
|
+
lines.push("");
|
|
71
|
+
lines.push(`Total Rules: ${compilation.rules.length}`);
|
|
72
|
+
lines.push("");
|
|
73
|
+
lines.push("For detailed consolidated standards, see:");
|
|
74
|
+
lines.push("- [CONSOLIDATED-STANDARDS.md](../CONSOLIDATED-STANDARDS.md)");
|
|
75
|
+
lines.push("- [RULES-INDEX.md](../RULES-INDEX.md)");
|
|
76
|
+
|
|
77
|
+
return lines.join("\n");
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Generate category file
|
|
82
|
+
*/
|
|
83
|
+
generateCategoryFile(category, rules, compilation) {
|
|
84
|
+
const lines = [];
|
|
85
|
+
lines.push(`# ${category} Rules`);
|
|
86
|
+
lines.push("");
|
|
87
|
+
lines.push(`Rules in the ${category} category.`);
|
|
88
|
+
lines.push("");
|
|
89
|
+
|
|
90
|
+
// Sort by level, then ID
|
|
91
|
+
const levelOrder = { must: 0, should: 1, prefer: 2, avoid: 3 };
|
|
92
|
+
rules.sort((a, b) => {
|
|
93
|
+
const levelDiff = levelOrder[a.level] - levelOrder[b.level];
|
|
94
|
+
if (levelDiff !== 0) return levelDiff;
|
|
95
|
+
return a.id.localeCompare(b.id);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
for (const rule of rules) {
|
|
99
|
+
lines.push(`## ${rule.id}`);
|
|
100
|
+
lines.push("");
|
|
101
|
+
lines.push(`**Level:** ${rule.level}`);
|
|
102
|
+
lines.push(`**Applies to:** ${rule.appliesTo.join(", ")}`);
|
|
103
|
+
lines.push("");
|
|
104
|
+
lines.push(rule.rule);
|
|
105
|
+
lines.push("");
|
|
106
|
+
if (rule.rationale) {
|
|
107
|
+
lines.push(`*Rationale:* ${rule.rationale}`);
|
|
108
|
+
lines.push("");
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
lines.push("---");
|
|
113
|
+
lines.push("");
|
|
114
|
+
lines.push("[Back to Rules Index](./index.md)");
|
|
115
|
+
|
|
116
|
+
return lines.join("\n");
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { IAssetGenerator } from "../../../application/ports/asset-generator.js";
|
|
2
|
+
import { FileWriter } from "../../file-system/file-writer.js";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Cursor Skills Generator Adapter
|
|
7
|
+
* Generates .cursor\skills\*\SKILL.md files from skill definitions
|
|
8
|
+
*/
|
|
9
|
+
export class CursorSkillsGenerator extends IAssetGenerator {
|
|
10
|
+
constructor() {
|
|
11
|
+
super();
|
|
12
|
+
this.fileWriter = new FileWriter();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
getToolName() {
|
|
16
|
+
return "cursor-skills";
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async generate(compilation, outputPath, skills) {
|
|
20
|
+
if (!skills || skills.length === 0) {
|
|
21
|
+
return { filesGenerated: [], outputDir: null };
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const skillsDir = path.join(outputPath, ".cursor", "skills");
|
|
25
|
+
this.fileWriter.ensureDir(skillsDir);
|
|
26
|
+
|
|
27
|
+
const generated = [];
|
|
28
|
+
|
|
29
|
+
for (const skill of skills) {
|
|
30
|
+
const skillDir = path.join(skillsDir, skill.name);
|
|
31
|
+
this.fileWriter.ensureDir(skillDir);
|
|
32
|
+
|
|
33
|
+
const skillContent = this.generateSkillFile(skill, compilation);
|
|
34
|
+
this.fileWriter.writeFile(path.join(skillDir, "SKILL.md"), skillContent);
|
|
35
|
+
|
|
36
|
+
generated.push(skill.name);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
filesGenerated: generated.map(s => `${s}/SKILL.md`),
|
|
41
|
+
outputDir: skillsDir
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
merge(assets) {
|
|
46
|
+
// For MVP, just return assets as-is
|
|
47
|
+
return assets;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Generate SKILL.md file for a skill
|
|
52
|
+
*/
|
|
53
|
+
generateSkillFile(skill, compilation) {
|
|
54
|
+
const lines = [];
|
|
55
|
+
|
|
56
|
+
// YAML frontmatter
|
|
57
|
+
lines.push("---");
|
|
58
|
+
lines.push(`name: ${skill.name}`);
|
|
59
|
+
lines.push(`description: ${skill.description}`);
|
|
60
|
+
lines.push("---");
|
|
61
|
+
lines.push("");
|
|
62
|
+
lines.push(`# ${skill.name}`);
|
|
63
|
+
lines.push("");
|
|
64
|
+
lines.push(skill.description);
|
|
65
|
+
lines.push("");
|
|
66
|
+
|
|
67
|
+
// When to Use section
|
|
68
|
+
if (skill.whenToUse && skill.whenToUse.length > 0) {
|
|
69
|
+
lines.push("## When to Use");
|
|
70
|
+
lines.push("");
|
|
71
|
+
for (const useCase of skill.whenToUse) {
|
|
72
|
+
lines.push(`- ${useCase}`);
|
|
73
|
+
}
|
|
74
|
+
lines.push("");
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Rules section
|
|
78
|
+
if (skill.rules && skill.rules.length > 0) {
|
|
79
|
+
lines.push("## Rules");
|
|
80
|
+
lines.push("");
|
|
81
|
+
for (const ruleId of skill.rules) {
|
|
82
|
+
const rule = compilation.rules.find(r => r.id === ruleId);
|
|
83
|
+
if (rule) {
|
|
84
|
+
lines.push(`### ${rule.id}`);
|
|
85
|
+
lines.push(`- **Level:** ${rule.level}`);
|
|
86
|
+
lines.push(`- **Rule:** ${rule.rule}`);
|
|
87
|
+
if (rule.rationale) {
|
|
88
|
+
lines.push(`- **Rationale:** ${rule.rationale}`);
|
|
89
|
+
}
|
|
90
|
+
lines.push("");
|
|
91
|
+
} else {
|
|
92
|
+
lines.push(`### ${ruleId}`);
|
|
93
|
+
lines.push(`- *Rule not found in compilation*`);
|
|
94
|
+
lines.push("");
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Process reference
|
|
100
|
+
if (skill.processReference) {
|
|
101
|
+
lines.push("## Process");
|
|
102
|
+
lines.push("");
|
|
103
|
+
// If processReference already includes "See", use as-is; otherwise add it
|
|
104
|
+
if (skill.processReference.startsWith("See ")) {
|
|
105
|
+
lines.push(`${skill.processReference}.`);
|
|
106
|
+
} else {
|
|
107
|
+
lines.push(`See ${skill.processReference} in the source documentation.`);
|
|
108
|
+
}
|
|
109
|
+
lines.push("");
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return lines.join("\n");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* File reader adapter
|
|
6
|
+
* Handles file system operations
|
|
7
|
+
*/
|
|
8
|
+
export class FileReader {
|
|
9
|
+
/**
|
|
10
|
+
* Read a file and return its contents
|
|
11
|
+
* @param {string} filePath - Path to file
|
|
12
|
+
* @returns {string} File contents
|
|
13
|
+
*/
|
|
14
|
+
readFile(filePath) {
|
|
15
|
+
return fs.readFileSync(filePath, "utf8");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Check if a file exists
|
|
20
|
+
* @param {string} filePath - Path to file
|
|
21
|
+
* @returns {boolean} True if exists
|
|
22
|
+
*/
|
|
23
|
+
exists(filePath) {
|
|
24
|
+
return fs.existsSync(filePath);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* List all markdown files recursively
|
|
29
|
+
* @param {string} dirPath - Directory to search
|
|
30
|
+
* @returns {string[]} Array of file paths
|
|
31
|
+
*/
|
|
32
|
+
listMarkdownFiles(dirPath) {
|
|
33
|
+
const files = [];
|
|
34
|
+
const entries = fs.readdirSync(dirPath, { withFileTypes: true });
|
|
35
|
+
|
|
36
|
+
for (const entry of entries) {
|
|
37
|
+
const fullPath = path.join(dirPath, entry.name);
|
|
38
|
+
|
|
39
|
+
// Skip hidden files and common exclusions
|
|
40
|
+
if (entry.name.startsWith(".")) continue;
|
|
41
|
+
if (entry.name === "node_modules") continue;
|
|
42
|
+
|
|
43
|
+
if (entry.isDirectory()) {
|
|
44
|
+
files.push(...this.listMarkdownFiles(fullPath));
|
|
45
|
+
} else if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
46
|
+
files.push(fullPath);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return files;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Get file metadata
|
|
55
|
+
* @param {string} filePath - Path to file
|
|
56
|
+
* @returns {Object} File metadata
|
|
57
|
+
*/
|
|
58
|
+
getFileMetadata(filePath) {
|
|
59
|
+
const stats = fs.statSync(filePath);
|
|
60
|
+
return {
|
|
61
|
+
path: filePath,
|
|
62
|
+
size: stats.size,
|
|
63
|
+
modified: stats.mtime.toISOString()
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* File writer adapter
|
|
6
|
+
* Handles file system write operations
|
|
7
|
+
*/
|
|
8
|
+
export class FileWriter {
|
|
9
|
+
/**
|
|
10
|
+
* Write content to a file
|
|
11
|
+
* @param {string} filePath - Path to file
|
|
12
|
+
* @param {string} content - Content to write
|
|
13
|
+
*/
|
|
14
|
+
writeFile(filePath, content) {
|
|
15
|
+
// Ensure directory exists
|
|
16
|
+
const dir = path.dirname(filePath);
|
|
17
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
18
|
+
|
|
19
|
+
fs.writeFileSync(filePath, content, "utf8");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Write JSON to a file
|
|
24
|
+
* @param {string} filePath - Path to file
|
|
25
|
+
* @param {Object} data - Data to write as JSON
|
|
26
|
+
*/
|
|
27
|
+
writeJson(filePath, data) {
|
|
28
|
+
const content = JSON.stringify(data, null, 2);
|
|
29
|
+
this.writeFile(filePath, content);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Ensure directory exists
|
|
34
|
+
* @param {string} dirPath - Directory path
|
|
35
|
+
*/
|
|
36
|
+
ensureDir(dirPath) {
|
|
37
|
+
fs.mkdirSync(dirPath, { recursive: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Markdown parser adapter
|
|
3
|
+
* Parses markdown files and extracts structured sections
|
|
4
|
+
*/
|
|
5
|
+
export class MarkdownParser {
|
|
6
|
+
/**
|
|
7
|
+
* Strip YAML frontmatter from markdown
|
|
8
|
+
* @param {string} content - Markdown content
|
|
9
|
+
* @returns {string} Content without frontmatter
|
|
10
|
+
*/
|
|
11
|
+
stripFrontmatter(content) {
|
|
12
|
+
if (!content.startsWith("---\n")) return content;
|
|
13
|
+
const endIndex = content.indexOf("\n---\n", 4);
|
|
14
|
+
if (endIndex === -1) return content;
|
|
15
|
+
return content.substring(endIndex + 5);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parse YAML frontmatter
|
|
20
|
+
* @param {string} content - Markdown content
|
|
21
|
+
* @returns {Object|null} Parsed frontmatter or null
|
|
22
|
+
*/
|
|
23
|
+
parseFrontmatter(content) {
|
|
24
|
+
if (!content.startsWith("---\n")) return null;
|
|
25
|
+
const endIndex = content.indexOf("\n---\n", 4);
|
|
26
|
+
if (endIndex === -1) return null;
|
|
27
|
+
|
|
28
|
+
const frontmatter = content.substring(4, endIndex);
|
|
29
|
+
// Simple YAML parsing (for now, can enhance later)
|
|
30
|
+
const result = {};
|
|
31
|
+
for (const line of frontmatter.split("\n")) {
|
|
32
|
+
const match = line.match(/^(\w+):\s*(.+)$/);
|
|
33
|
+
if (match) {
|
|
34
|
+
const key = match[1];
|
|
35
|
+
let value = match[2].trim();
|
|
36
|
+
// Remove quotes if present
|
|
37
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
38
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
39
|
+
value = value.slice(1, -1);
|
|
40
|
+
}
|
|
41
|
+
result[key] = value;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return result;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Find a section by heading
|
|
49
|
+
* @param {string} content - Markdown content
|
|
50
|
+
* @param {string} heading - Heading to find (e.g., "## @context.rules")
|
|
51
|
+
* @returns {string|null} Section content or null if not found
|
|
52
|
+
*/
|
|
53
|
+
findSection(content, heading) {
|
|
54
|
+
const lines = content.split("\n");
|
|
55
|
+
let startIndex = -1;
|
|
56
|
+
let endIndex = lines.length;
|
|
57
|
+
|
|
58
|
+
// Find start of section
|
|
59
|
+
for (let i = 0; i < lines.length; i++) {
|
|
60
|
+
if (lines[i].trim() === heading) {
|
|
61
|
+
startIndex = i + 1;
|
|
62
|
+
break;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (startIndex === -1) return null;
|
|
67
|
+
|
|
68
|
+
// Find end of section (next ## heading or end of file)
|
|
69
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
70
|
+
if (lines[i].startsWith("## ") && lines[i].trim() !== heading) {
|
|
71
|
+
endIndex = i;
|
|
72
|
+
break;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return lines.slice(startIndex, endIndex).join("\n");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get line number where a string appears
|
|
81
|
+
* @param {string} content - Full content
|
|
82
|
+
* @param {string} searchString - String to find
|
|
83
|
+
* @returns {number} Line number (1-indexed) or -1 if not found
|
|
84
|
+
*/
|
|
85
|
+
getLineNumber(content, searchString) {
|
|
86
|
+
const lines = content.split("\n");
|
|
87
|
+
for (let i = 0; i < lines.length; i++) {
|
|
88
|
+
if (lines[i].includes(searchString)) {
|
|
89
|
+
return i + 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return -1;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|