@voodocs/cli 0.1.0
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/LICENSE +37 -0
- package/README.md +153 -0
- package/USAGE.md +314 -0
- package/cli.py +1340 -0
- package/examples/.cursorrules +437 -0
- package/examples/instructions/.claude/instructions.md +372 -0
- package/examples/instructions/.cursorrules +437 -0
- package/examples/instructions/.windsurfrules +437 -0
- package/examples/instructions/VOODOCS_INSTRUCTIONS.md +437 -0
- package/examples/math_example.py +41 -0
- package/examples/phase2_test.py +24 -0
- package/examples/test_compound_conditions.py +40 -0
- package/examples/test_math_example.py +186 -0
- package/lib/darkarts/README.md +115 -0
- package/lib/darkarts/__init__.py +16 -0
- package/lib/darkarts/annotations/__init__.py +34 -0
- package/lib/darkarts/annotations/parser.py +618 -0
- package/lib/darkarts/annotations/types.py +181 -0
- package/lib/darkarts/cli.py +128 -0
- package/lib/darkarts/core/__init__.py +32 -0
- package/lib/darkarts/core/interface.py +256 -0
- package/lib/darkarts/core/loader.py +231 -0
- package/lib/darkarts/core/plugin.py +215 -0
- package/lib/darkarts/core/registry.py +146 -0
- package/lib/darkarts/exceptions.py +51 -0
- package/lib/darkarts/parsers/typescript/dist/cli.d.ts +9 -0
- package/lib/darkarts/parsers/typescript/dist/cli.d.ts.map +1 -0
- package/lib/darkarts/parsers/typescript/dist/cli.js +69 -0
- package/lib/darkarts/parsers/typescript/dist/cli.js.map +1 -0
- package/lib/darkarts/parsers/typescript/dist/parser.d.ts +111 -0
- package/lib/darkarts/parsers/typescript/dist/parser.d.ts.map +1 -0
- package/lib/darkarts/parsers/typescript/dist/parser.js +365 -0
- package/lib/darkarts/parsers/typescript/dist/parser.js.map +1 -0
- package/lib/darkarts/parsers/typescript/package-lock.json +51 -0
- package/lib/darkarts/parsers/typescript/package.json +19 -0
- package/lib/darkarts/parsers/typescript/src/cli.ts +41 -0
- package/lib/darkarts/parsers/typescript/src/parser.ts +408 -0
- package/lib/darkarts/parsers/typescript/tsconfig.json +19 -0
- package/lib/darkarts/plugins/voodocs/__init__.py +379 -0
- package/lib/darkarts/plugins/voodocs/ai_native_plugin.py +151 -0
- package/lib/darkarts/plugins/voodocs/annotation_validator.py +280 -0
- package/lib/darkarts/plugins/voodocs/api_spec_generator.py +486 -0
- package/lib/darkarts/plugins/voodocs/documentation_generator.py +610 -0
- package/lib/darkarts/plugins/voodocs/html_exporter.py +260 -0
- package/lib/darkarts/plugins/voodocs/instruction_generator.py +706 -0
- package/lib/darkarts/plugins/voodocs/pdf_exporter.py +66 -0
- package/lib/darkarts/plugins/voodocs/test_generator.py +636 -0
- package/package.json +70 -0
- package/requirements.txt +13 -0
- package/templates/ci/github-actions.yml +73 -0
- package/templates/ci/gitlab-ci.yml +35 -0
- package/templates/ci/pre-commit-hook.sh +26 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* TypeScript/JavaScript Parser for DarkArts Annotations
|
|
3
|
+
*
|
|
4
|
+
* This parser uses the TypeScript Compiler API to extract @voodocs
|
|
5
|
+
* annotations from TypeScript and JavaScript files.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import * as ts from 'typescript';
|
|
9
|
+
import * as fs from 'fs';
|
|
10
|
+
import * as path from 'path';
|
|
11
|
+
|
|
12
|
+
interface Annotation {
|
|
13
|
+
[key: string]: any;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
interface FunctionInfo {
|
|
17
|
+
name: string;
|
|
18
|
+
line: number;
|
|
19
|
+
parameters: string[];
|
|
20
|
+
return_type: string | null;
|
|
21
|
+
annotations: Annotation;
|
|
22
|
+
is_async: boolean;
|
|
23
|
+
is_exported: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface ClassInfo {
|
|
27
|
+
name: string;
|
|
28
|
+
line: number;
|
|
29
|
+
annotations: Annotation;
|
|
30
|
+
methods: FunctionInfo[];
|
|
31
|
+
is_exported: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
interface ParseResult {
|
|
35
|
+
file_path: string;
|
|
36
|
+
language: string;
|
|
37
|
+
module_annotations: Annotation;
|
|
38
|
+
classes: ClassInfo[];
|
|
39
|
+
functions: FunctionInfo[];
|
|
40
|
+
interfaces: InterfaceInfo[];
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
interface InterfaceInfo {
|
|
44
|
+
name: string;
|
|
45
|
+
line: number;
|
|
46
|
+
annotations: Annotation;
|
|
47
|
+
is_exported: boolean;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export class TypeScriptAnnotationParser {
|
|
51
|
+
private sourceFile: ts.SourceFile;
|
|
52
|
+
private sourceText: string;
|
|
53
|
+
|
|
54
|
+
constructor(private filePath: string) {
|
|
55
|
+
this.sourceText = fs.readFileSync(filePath, 'utf-8');
|
|
56
|
+
this.sourceFile = ts.createSourceFile(
|
|
57
|
+
filePath,
|
|
58
|
+
this.sourceText,
|
|
59
|
+
ts.ScriptTarget.Latest,
|
|
60
|
+
true
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Parse the TypeScript file and extract all @voodocs annotations
|
|
66
|
+
*/
|
|
67
|
+
public parse(): ParseResult {
|
|
68
|
+
const result: ParseResult = {
|
|
69
|
+
file_path: this.filePath,
|
|
70
|
+
language: this.filePath.endsWith('.ts') ? 'typescript' : 'javascript',
|
|
71
|
+
module_annotations: this.extractModuleAnnotations(),
|
|
72
|
+
classes: [],
|
|
73
|
+
functions: [],
|
|
74
|
+
interfaces: []
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Visit all nodes in the AST
|
|
78
|
+
const visit = (node: ts.Node) => {
|
|
79
|
+
if (ts.isClassDeclaration(node)) {
|
|
80
|
+
const classInfo = this.parseClass(node);
|
|
81
|
+
if (classInfo) {
|
|
82
|
+
result.classes.push(classInfo);
|
|
83
|
+
}
|
|
84
|
+
} else if (ts.isFunctionDeclaration(node)) {
|
|
85
|
+
const funcInfo = this.parseFunction(node);
|
|
86
|
+
if (funcInfo) {
|
|
87
|
+
result.functions.push(funcInfo);
|
|
88
|
+
}
|
|
89
|
+
} else if (ts.isInterfaceDeclaration(node)) {
|
|
90
|
+
const interfaceInfo = this.parseInterface(node);
|
|
91
|
+
if (interfaceInfo) {
|
|
92
|
+
result.interfaces.push(interfaceInfo);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
ts.forEachChild(node, visit);
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
visit(this.sourceFile);
|
|
100
|
+
|
|
101
|
+
return result;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Extract module-level annotations from the top of the file
|
|
106
|
+
*/
|
|
107
|
+
private extractModuleAnnotations(): Annotation {
|
|
108
|
+
const leadingComments = this.getLeadingComments(this.sourceFile);
|
|
109
|
+
for (const comment of leadingComments) {
|
|
110
|
+
if (this.isDarkArtsComment(comment)) {
|
|
111
|
+
return this.parseAnnotationText(comment);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return {};
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Parse a class declaration
|
|
119
|
+
*/
|
|
120
|
+
private parseClass(node: ts.ClassDeclaration): ClassInfo | null {
|
|
121
|
+
const name = node.name?.text;
|
|
122
|
+
if (!name) return null;
|
|
123
|
+
|
|
124
|
+
const line = this.getLineNumber(node);
|
|
125
|
+
const annotations = this.extractAnnotations(node);
|
|
126
|
+
const methods: FunctionInfo[] = [];
|
|
127
|
+
|
|
128
|
+
// Extract methods
|
|
129
|
+
node.members.forEach(member => {
|
|
130
|
+
if (ts.isMethodDeclaration(member)) {
|
|
131
|
+
const methodInfo = this.parseMethod(member);
|
|
132
|
+
if (methodInfo) {
|
|
133
|
+
methods.push(methodInfo);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return {
|
|
139
|
+
name,
|
|
140
|
+
line,
|
|
141
|
+
annotations,
|
|
142
|
+
methods,
|
|
143
|
+
is_exported: this.isExported(node)
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Parse a function declaration
|
|
149
|
+
*/
|
|
150
|
+
private parseFunction(node: ts.FunctionDeclaration): FunctionInfo | null {
|
|
151
|
+
const name = node.name?.text;
|
|
152
|
+
if (!name) return null;
|
|
153
|
+
|
|
154
|
+
return {
|
|
155
|
+
name,
|
|
156
|
+
line: this.getLineNumber(node),
|
|
157
|
+
parameters: this.extractParameters(node),
|
|
158
|
+
return_type: this.extractReturnType(node),
|
|
159
|
+
annotations: this.extractAnnotations(node),
|
|
160
|
+
is_async: this.isAsync(node),
|
|
161
|
+
is_exported: this.isExported(node)
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Parse a method declaration
|
|
167
|
+
*/
|
|
168
|
+
private parseMethod(node: ts.MethodDeclaration): FunctionInfo | null {
|
|
169
|
+
const name = (node.name as ts.Identifier).text;
|
|
170
|
+
if (!name) return null;
|
|
171
|
+
|
|
172
|
+
return {
|
|
173
|
+
name,
|
|
174
|
+
line: this.getLineNumber(node),
|
|
175
|
+
parameters: this.extractParameters(node),
|
|
176
|
+
return_type: this.extractReturnType(node),
|
|
177
|
+
annotations: this.extractAnnotations(node),
|
|
178
|
+
is_async: this.isAsync(node),
|
|
179
|
+
is_exported: false // Methods are not directly exported
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Parse an interface declaration
|
|
185
|
+
*/
|
|
186
|
+
private parseInterface(node: ts.InterfaceDeclaration): InterfaceInfo | null {
|
|
187
|
+
const name = node.name.text;
|
|
188
|
+
if (!name) return null;
|
|
189
|
+
|
|
190
|
+
return {
|
|
191
|
+
name,
|
|
192
|
+
line: this.getLineNumber(node),
|
|
193
|
+
annotations: this.extractAnnotations(node),
|
|
194
|
+
is_exported: this.isExported(node)
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Extract annotations from a node's JSDoc comments
|
|
200
|
+
*/
|
|
201
|
+
private extractAnnotations(node: ts.Node): Annotation {
|
|
202
|
+
const comments = this.getLeadingComments(node);
|
|
203
|
+
// Find the LAST (closest) @voodocs comment
|
|
204
|
+
let lastDarkArtsComment: string | null = null;
|
|
205
|
+
for (const comment of comments) {
|
|
206
|
+
if (this.isDarkArtsComment(comment)) {
|
|
207
|
+
lastDarkArtsComment = comment;
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
if (lastDarkArtsComment) {
|
|
211
|
+
return this.parseAnnotationText(lastDarkArtsComment);
|
|
212
|
+
}
|
|
213
|
+
return {};
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Get leading comments for a node
|
|
218
|
+
*/
|
|
219
|
+
private getLeadingComments(node: ts.Node): string[] {
|
|
220
|
+
const comments: string[] = [];
|
|
221
|
+
const fullText = this.sourceText;
|
|
222
|
+
const nodePos = node.getFullStart();
|
|
223
|
+
|
|
224
|
+
const ranges = ts.getLeadingCommentRanges(fullText, nodePos);
|
|
225
|
+
if (ranges) {
|
|
226
|
+
for (const range of ranges) {
|
|
227
|
+
const commentText = fullText.substring(range.pos, range.end);
|
|
228
|
+
comments.push(commentText);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return comments;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Check if a comment contains @voodocs annotation
|
|
237
|
+
*/
|
|
238
|
+
private isDarkArtsComment(comment: string): boolean {
|
|
239
|
+
return comment.includes('@voodocs');
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Parse the annotation text and extract key-value pairs
|
|
244
|
+
*/
|
|
245
|
+
private parseAnnotationText(comment: string): Annotation {
|
|
246
|
+
const annotation: Annotation = {};
|
|
247
|
+
|
|
248
|
+
// Remove comment markers
|
|
249
|
+
let text = comment.replace(/^\/\*\*?|\*\/$/g, '').trim();
|
|
250
|
+
text = text.replace(/^\s*\*\s?/gm, ''); // Remove leading asterisks
|
|
251
|
+
|
|
252
|
+
// Remove @voodocs marker
|
|
253
|
+
text = text.replace(/@voodocs\s*/i, '');
|
|
254
|
+
|
|
255
|
+
// Parse key-value pairs
|
|
256
|
+
const lines = text.split('\n');
|
|
257
|
+
let currentKey: string | null = null;
|
|
258
|
+
let currentValue: string[] = [];
|
|
259
|
+
|
|
260
|
+
for (const line of lines) {
|
|
261
|
+
const trimmed = line.trim();
|
|
262
|
+
if (!trimmed) continue;
|
|
263
|
+
|
|
264
|
+
// Check if this is a new key
|
|
265
|
+
const keyMatch = trimmed.match(/^(\w+):\s*(.*)$/);
|
|
266
|
+
if (keyMatch) {
|
|
267
|
+
// Save previous key-value pair
|
|
268
|
+
if (currentKey) {
|
|
269
|
+
annotation[currentKey] = this.parseValue(currentValue.join('\n'));
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
currentKey = keyMatch[1];
|
|
273
|
+
currentValue = [keyMatch[2]];
|
|
274
|
+
} else if (currentKey) {
|
|
275
|
+
// Continue current value
|
|
276
|
+
currentValue.push(trimmed);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
// Save last key-value pair
|
|
281
|
+
if (currentKey) {
|
|
282
|
+
annotation[currentKey] = this.parseValue(currentValue.join('\n'));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return annotation;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Parse a value (string, array, or object)
|
|
290
|
+
*/
|
|
291
|
+
private parseValue(value: string): any {
|
|
292
|
+
value = value.trim();
|
|
293
|
+
|
|
294
|
+
// Try to parse as JSON array
|
|
295
|
+
if (value.startsWith('[') && value.endsWith(']')) {
|
|
296
|
+
try {
|
|
297
|
+
// Extract array content
|
|
298
|
+
const content = value.slice(1, -1).trim();
|
|
299
|
+
if (!content) return [];
|
|
300
|
+
|
|
301
|
+
// Split by newlines first (each line is typically one item)
|
|
302
|
+
const items: string[] = [];
|
|
303
|
+
let currentItem = '';
|
|
304
|
+
let inQuotes = false;
|
|
305
|
+
let quoteChar = '';
|
|
306
|
+
|
|
307
|
+
for (let i = 0; i < content.length; i++) {
|
|
308
|
+
const char = content[i];
|
|
309
|
+
|
|
310
|
+
if ((char === '"' || char === "'") && (i === 0 || content[i-1] !== '\\')) {
|
|
311
|
+
if (!inQuotes) {
|
|
312
|
+
inQuotes = true;
|
|
313
|
+
quoteChar = char;
|
|
314
|
+
} else if (char === quoteChar) {
|
|
315
|
+
inQuotes = false;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (char === ',' && !inQuotes) {
|
|
320
|
+
if (currentItem.trim()) {
|
|
321
|
+
items.push(currentItem.trim());
|
|
322
|
+
}
|
|
323
|
+
currentItem = '';
|
|
324
|
+
} else {
|
|
325
|
+
currentItem += char;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
// Add the last item
|
|
330
|
+
if (currentItem.trim()) {
|
|
331
|
+
items.push(currentItem.trim());
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Clean up each item
|
|
335
|
+
return items.map(item => {
|
|
336
|
+
item = item.trim();
|
|
337
|
+
// Remove surrounding quotes
|
|
338
|
+
if ((item.startsWith('"') && item.endsWith('"')) ||
|
|
339
|
+
(item.startsWith("'") && item.endsWith("'"))) {
|
|
340
|
+
return item.slice(1, -1);
|
|
341
|
+
}
|
|
342
|
+
return item;
|
|
343
|
+
});
|
|
344
|
+
} catch (e) {
|
|
345
|
+
return value;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
// Remove quotes if present
|
|
350
|
+
if ((value.startsWith('"') && value.endsWith('"')) ||
|
|
351
|
+
(value.startsWith("'") && value.endsWith("'"))) {
|
|
352
|
+
return value.slice(1, -1);
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
return value;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Extract parameter names from a function/method
|
|
360
|
+
*/
|
|
361
|
+
private extractParameters(node: ts.FunctionDeclaration | ts.MethodDeclaration): string[] {
|
|
362
|
+
return node.parameters.map(param => {
|
|
363
|
+
const name = (param.name as ts.Identifier).text;
|
|
364
|
+
const type = param.type ? `: ${param.type.getText(this.sourceFile)}` : '';
|
|
365
|
+
return `${name}${type}`;
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Extract return type from a function/method
|
|
371
|
+
*/
|
|
372
|
+
private extractReturnType(node: ts.FunctionDeclaration | ts.MethodDeclaration): string | null {
|
|
373
|
+
if (node.type) {
|
|
374
|
+
return node.type.getText(this.sourceFile);
|
|
375
|
+
}
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
/**
|
|
380
|
+
* Check if a function/method is async
|
|
381
|
+
*/
|
|
382
|
+
private isAsync(node: ts.FunctionDeclaration | ts.MethodDeclaration): boolean {
|
|
383
|
+
if (ts.canHaveModifiers(node)) {
|
|
384
|
+
const modifiers = ts.getModifiers(node);
|
|
385
|
+
return modifiers?.some((m: ts.Modifier) => m.kind === ts.SyntaxKind.AsyncKeyword) || false;
|
|
386
|
+
}
|
|
387
|
+
return false;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Check if a node is exported
|
|
392
|
+
*/
|
|
393
|
+
private isExported(node: ts.Node): boolean {
|
|
394
|
+
if (ts.canHaveModifiers(node)) {
|
|
395
|
+
const modifiers = ts.getModifiers(node);
|
|
396
|
+
return modifiers?.some((m: ts.Modifier) => m.kind === ts.SyntaxKind.ExportKeyword) || false;
|
|
397
|
+
}
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Get the line number of a node
|
|
403
|
+
*/
|
|
404
|
+
private getLineNumber(node: ts.Node): number {
|
|
405
|
+
const { line } = this.sourceFile.getLineAndCharacterOfPosition(node.getStart());
|
|
406
|
+
return line + 1; // Convert to 1-indexed
|
|
407
|
+
}
|
|
408
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"outDir": "./dist",
|
|
7
|
+
"rootDir": "./src",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true
|
|
16
|
+
},
|
|
17
|
+
"include": ["src/**/*"],
|
|
18
|
+
"exclude": ["node_modules", "dist"]
|
|
19
|
+
}
|