@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.
@@ -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
+