@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,219 @@
|
|
|
1
|
+
import { Rule } from "../../domain/rule.js";
|
|
2
|
+
import { Preference } from "../../domain/preference.js";
|
|
3
|
+
import { Scope } from "../../domain/scope.js";
|
|
4
|
+
import { MarkdownParser } from "./markdown-parser.js";
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Rule extractor adapter
|
|
8
|
+
* Extracts rules, preferences, and scope from markdown files
|
|
9
|
+
*/
|
|
10
|
+
export class RuleExtractor {
|
|
11
|
+
constructor() {
|
|
12
|
+
this.parser = new MarkdownParser();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extract rules from markdown content
|
|
17
|
+
* @param {string} content - Markdown content
|
|
18
|
+
* @param {Object} source - Source information
|
|
19
|
+
* @returns {Rule[]} Extracted rules
|
|
20
|
+
*/
|
|
21
|
+
extractRules(content, source) {
|
|
22
|
+
const rules = [];
|
|
23
|
+
const rulesSection = this.parser.findSection(content, "## @context.rules");
|
|
24
|
+
|
|
25
|
+
if (!rulesSection) return rules;
|
|
26
|
+
|
|
27
|
+
const lines = rulesSection.split("\n");
|
|
28
|
+
let currentRule = null;
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < lines.length; i++) {
|
|
31
|
+
const line = lines[i].trim();
|
|
32
|
+
|
|
33
|
+
// Rule header: ### rule-id: Title
|
|
34
|
+
const ruleHeaderMatch = line.match(/^###\s+([a-z0-9-]+):\s*(.+)$/);
|
|
35
|
+
if (ruleHeaderMatch) {
|
|
36
|
+
// Save previous rule if exists
|
|
37
|
+
if (currentRule) {
|
|
38
|
+
if (currentRule.rule) {
|
|
39
|
+
rules.push(this.createRule(currentRule, source, content));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Start new rule
|
|
44
|
+
currentRule = {
|
|
45
|
+
id: ruleHeaderMatch[1],
|
|
46
|
+
title: ruleHeaderMatch[2],
|
|
47
|
+
level: null,
|
|
48
|
+
appliesTo: null,
|
|
49
|
+
rule: null,
|
|
50
|
+
rationale: null
|
|
51
|
+
};
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!currentRule) continue;
|
|
56
|
+
|
|
57
|
+
// Parse rule fields
|
|
58
|
+
const levelMatch = line.match(/^-\s*\*\*Level:\*\*\s*(.+)$/);
|
|
59
|
+
if (levelMatch) {
|
|
60
|
+
currentRule.level = levelMatch[1].trim();
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const appliesToMatch = line.match(/^-\s*\*\*Applies to:\*\*\s*(.+)$/);
|
|
65
|
+
if (appliesToMatch) {
|
|
66
|
+
const appliesToStr = appliesToMatch[1].trim();
|
|
67
|
+
currentRule.appliesTo = appliesToStr === "all"
|
|
68
|
+
? ["all"]
|
|
69
|
+
: appliesToStr.split(",").map(s => s.trim());
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const ruleMatch = line.match(/^-\s*\*\*Rule:\*\*\s*(.+)$/);
|
|
74
|
+
if (ruleMatch) {
|
|
75
|
+
currentRule.rule = ruleMatch[1].trim();
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const rationaleMatch = line.match(/^-\s*\*\*Rationale:\*\*\s*(.+)$/);
|
|
80
|
+
if (rationaleMatch) {
|
|
81
|
+
currentRule.rationale = rationaleMatch[1].trim();
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Don't forget the last rule
|
|
87
|
+
if (currentRule && currentRule.rule) {
|
|
88
|
+
rules.push(this.createRule(currentRule, source, content));
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return rules;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Create a Rule object from extracted data
|
|
96
|
+
* @param {Object} data - Extracted rule data
|
|
97
|
+
* @param {Object} source - Source information
|
|
98
|
+
* @param {string} content - Full content for line number calculation
|
|
99
|
+
* @returns {Rule} Rule object
|
|
100
|
+
*/
|
|
101
|
+
createRule(data, source, content) {
|
|
102
|
+
// Validate level
|
|
103
|
+
if (!Rule.isValidLevel(data.level)) {
|
|
104
|
+
console.warn(`Invalid level "${data.level}" for rule ${data.id}, defaulting to "should"`);
|
|
105
|
+
data.level = "should";
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Validate rule ID
|
|
109
|
+
if (!Rule.isValidId(data.id)) {
|
|
110
|
+
throw new Error(`Invalid rule ID format: ${data.id} (must be lowercase, numbers, hyphens only)`);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Get line number
|
|
114
|
+
const lineNumber = this.parser.getLineNumber(content, `### ${data.id}:`);
|
|
115
|
+
|
|
116
|
+
return new Rule({
|
|
117
|
+
id: data.id,
|
|
118
|
+
level: data.level,
|
|
119
|
+
category: Rule.extractCategory(data.id),
|
|
120
|
+
appliesTo: data.appliesTo || [],
|
|
121
|
+
rule: data.rule,
|
|
122
|
+
rationale: data.rationale,
|
|
123
|
+
source: {
|
|
124
|
+
pack: source.pack,
|
|
125
|
+
packVersion: source.packVersion,
|
|
126
|
+
file: source.file,
|
|
127
|
+
line: lineNumber,
|
|
128
|
+
precedence: source.precedence
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Extract preferences from markdown content
|
|
135
|
+
* @param {string} content - Markdown content
|
|
136
|
+
* @param {Object} source - Source information
|
|
137
|
+
* @returns {Preference[]} Extracted preferences
|
|
138
|
+
*/
|
|
139
|
+
extractPreferences(content, source) {
|
|
140
|
+
const preferences = [];
|
|
141
|
+
const prefsSection = this.parser.findSection(content, "## @context.preferences");
|
|
142
|
+
|
|
143
|
+
if (!prefsSection) return preferences;
|
|
144
|
+
|
|
145
|
+
const lines = prefsSection.split("\n");
|
|
146
|
+
for (const line of lines) {
|
|
147
|
+
const match = line.match(/^-\s*\*\*prefer:\*\*\s*(.+?)(?:\s*-\s*(.+))?$/);
|
|
148
|
+
if (match) {
|
|
149
|
+
const name = match[1].trim();
|
|
150
|
+
const description = match[2] ? match[2].trim() : null;
|
|
151
|
+
const lineNumber = this.parser.getLineNumber(content, line.trim());
|
|
152
|
+
|
|
153
|
+
preferences.push(new Preference({
|
|
154
|
+
name,
|
|
155
|
+
description,
|
|
156
|
+
source: {
|
|
157
|
+
pack: source.pack,
|
|
158
|
+
packVersion: source.packVersion,
|
|
159
|
+
file: source.file,
|
|
160
|
+
line: lineNumber,
|
|
161
|
+
precedence: source.precedence
|
|
162
|
+
}
|
|
163
|
+
}));
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return preferences;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Extract scope from markdown content
|
|
172
|
+
* @param {string} content - Markdown content
|
|
173
|
+
* @param {Object} source - Source information
|
|
174
|
+
* @returns {Scope|null} Extracted scope or null
|
|
175
|
+
*/
|
|
176
|
+
extractScope(content, source) {
|
|
177
|
+
const scopeSection = this.parser.findSection(content, "## @context.scope");
|
|
178
|
+
|
|
179
|
+
if (!scopeSection) return null;
|
|
180
|
+
|
|
181
|
+
const appliesTo = [];
|
|
182
|
+
const doesNotApplyTo = [];
|
|
183
|
+
let inAppliesTo = false;
|
|
184
|
+
let inDoesNotApplyTo = false;
|
|
185
|
+
|
|
186
|
+
const lines = scopeSection.split("\n");
|
|
187
|
+
for (const line of lines) {
|
|
188
|
+
if (line.includes("**Applies to:**")) {
|
|
189
|
+
inAppliesTo = true;
|
|
190
|
+
inDoesNotApplyTo = false;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (line.includes("**Does not apply to:**")) {
|
|
194
|
+
inAppliesTo = false;
|
|
195
|
+
inDoesNotApplyTo = true;
|
|
196
|
+
continue;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const listMatch = line.match(/^-\s*(.+)$/);
|
|
200
|
+
if (listMatch) {
|
|
201
|
+
const item = listMatch[1].trim();
|
|
202
|
+
if (inAppliesTo) {
|
|
203
|
+
appliesTo.push(item);
|
|
204
|
+
} else if (inDoesNotApplyTo) {
|
|
205
|
+
doesNotApplyTo.push(item);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return new Scope({
|
|
211
|
+
appliesTo,
|
|
212
|
+
doesNotApplyTo,
|
|
213
|
+
source: {
|
|
214
|
+
file: source.file
|
|
215
|
+
}
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { MarkdownParser } from "./markdown-parser.js";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Skill extractor adapter
|
|
5
|
+
* Extracts skill definitions from @context.skills sections
|
|
6
|
+
*/
|
|
7
|
+
export class SkillExtractor {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.parser = new MarkdownParser();
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extracts skill definitions from markdown content.
|
|
14
|
+
* @param {string} markdown - The markdown content.
|
|
15
|
+
* @param {Object} sourceMetadata - Metadata about the source file (packId, file, etc.).
|
|
16
|
+
* @returns {Object[]} Extracted skill definitions.
|
|
17
|
+
*/
|
|
18
|
+
extractSkills(markdown, sourceMetadata) {
|
|
19
|
+
const skills = [];
|
|
20
|
+
const skillsSection = this.parser.findSection(markdown, "## @context.skills");
|
|
21
|
+
|
|
22
|
+
if (!skillsSection) return skills;
|
|
23
|
+
|
|
24
|
+
const skillBlocks = skillsSection.split(/\r?\n### /).filter(Boolean);
|
|
25
|
+
for (const skillText of skillBlocks) {
|
|
26
|
+
try {
|
|
27
|
+
const skill = this._parseSkill(skillText, sourceMetadata);
|
|
28
|
+
if (skill) skills.push(skill);
|
|
29
|
+
} catch (e) {
|
|
30
|
+
console.warn(`Warning: Malformed skill in ${sourceMetadata.file}: ${e.message}`);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return skills;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
_parseSkill(skillText, sourceMetadata) {
|
|
37
|
+
const lines = skillText.split(/\r?\n/);
|
|
38
|
+
// Handle first line - might have "### " prefix or not
|
|
39
|
+
const firstLine = lines[0].trim();
|
|
40
|
+
const nameMatch = firstLine.match(/^(?:###\s+)?skill-([a-z0-9-]+)$/i);
|
|
41
|
+
if (!nameMatch) throw new Error(`Invalid skill ID format: ${firstLine}`);
|
|
42
|
+
|
|
43
|
+
const id = nameMatch[1].toLowerCase();
|
|
44
|
+
const skill = {
|
|
45
|
+
id: id,
|
|
46
|
+
name: id, // Default name to id, can be overridden by explicit 'Name' field
|
|
47
|
+
description: '',
|
|
48
|
+
rules: [],
|
|
49
|
+
whenToUse: [],
|
|
50
|
+
processReference: '',
|
|
51
|
+
source: sourceMetadata
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
for (let i = 1; i < lines.length; i++) {
|
|
55
|
+
const line = lines[i].trim();
|
|
56
|
+
if (line.startsWith('- **Name:**')) {
|
|
57
|
+
skill.name = line.replace('- **Name:**', '').trim();
|
|
58
|
+
} else if (line.startsWith('- **Description:**')) {
|
|
59
|
+
skill.description = line.replace('- **Description:**', '').trim();
|
|
60
|
+
} else if (line.startsWith('- **Rules:**')) {
|
|
61
|
+
skill.rules = line.replace('- **Rules:**', '').split(',').map(s => s.trim()).filter(Boolean);
|
|
62
|
+
} else if (line.startsWith('- **When to use:**')) {
|
|
63
|
+
skill.whenToUse = line.replace('- **When to use:**', '').split(',').map(s => s.trim()).filter(Boolean);
|
|
64
|
+
} else if (line.startsWith('- **Process reference:**')) {
|
|
65
|
+
skill.processReference = line.replace('- **Process reference:**', '').trim();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (!skill.description) throw new Error(`Skill '${skill.id}' is missing a description.`);
|
|
70
|
+
|
|
71
|
+
return skill;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|