@voodocs/cli 1.0.6 → 2.0.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/CHANGELOG.md +78 -0
- package/README.md +171 -241
- package/lib/cli/__init__.py +4 -4
- package/lib/cli/generate.py +5 -5
- package/lib/cli/init.py +450 -98
- package/lib/darkarts/annotations/parser.py +10 -15
- package/package.json +15 -14
package/lib/cli/init.py
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""@darkarts
|
|
2
|
-
⊢{cli:init}
|
|
3
|
-
∂{click,pathlib,json}
|
|
2
|
+
⊢{cli:init-wizard}
|
|
3
|
+
∂{click,pathlib,json,os,subprocess}
|
|
4
4
|
⚠{write-access:cwd}
|
|
5
|
-
⊨{idempotent:config-creation}
|
|
5
|
+
⊨{idempotent:config-creation,rerunnable:upgrade-support}
|
|
6
6
|
🔒{read-write:filesystem}
|
|
7
7
|
⚡{O(1):file-creation}
|
|
8
8
|
"""
|
|
@@ -10,52 +10,250 @@
|
|
|
10
10
|
"""
|
|
11
11
|
VooDocs Init Command
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
Interactive wizard to initialize or upgrade a project with VooDocs.
|
|
14
|
+
Re-runnable to add missing features after upgrades.
|
|
14
15
|
"""
|
|
15
16
|
|
|
16
17
|
import click
|
|
17
18
|
import json
|
|
19
|
+
import os
|
|
20
|
+
import subprocess
|
|
18
21
|
from pathlib import Path
|
|
22
|
+
from typing import Optional, Dict, Any
|
|
19
23
|
|
|
20
24
|
|
|
21
25
|
@click.command()
|
|
22
26
|
@click.option(
|
|
23
|
-
'--
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
help='Annotation format to use (voodocs or darkarts symbolic)'
|
|
27
|
+
'--non-interactive',
|
|
28
|
+
is_flag=True,
|
|
29
|
+
help='Skip interactive prompts and use defaults'
|
|
27
30
|
)
|
|
28
31
|
@click.option(
|
|
29
|
-
'--
|
|
32
|
+
'--upgrade',
|
|
30
33
|
is_flag=True,
|
|
31
|
-
help='
|
|
34
|
+
help='Upgrade existing configuration (add missing features)'
|
|
32
35
|
)
|
|
33
|
-
def init(
|
|
36
|
+
def init(non_interactive, upgrade):
|
|
34
37
|
"""
|
|
35
|
-
Initialize a
|
|
38
|
+
Initialize or upgrade a project with VooDocs.
|
|
39
|
+
|
|
40
|
+
This interactive wizard will:
|
|
41
|
+
- Set up your project configuration
|
|
42
|
+
- Auto-detect project details (version, repository, AI environment)
|
|
43
|
+
- Create AI instructions for your coding assistant
|
|
44
|
+
- Configure privacy settings (.gitignore)
|
|
45
|
+
- Generate example annotated files
|
|
36
46
|
|
|
37
|
-
|
|
38
|
-
example annotated files to help you get started.
|
|
47
|
+
Re-run after upgrading to add new features!
|
|
39
48
|
|
|
40
49
|
Examples:
|
|
41
|
-
voodocs init #
|
|
42
|
-
voodocs init --
|
|
43
|
-
voodocs init --
|
|
50
|
+
voodocs init # Interactive wizard
|
|
51
|
+
voodocs init --upgrade # Upgrade existing config
|
|
52
|
+
voodocs init --non-interactive # Use all defaults
|
|
44
53
|
"""
|
|
54
|
+
|
|
45
55
|
cwd = Path.cwd()
|
|
46
56
|
config_path = cwd / '.voodocs.json'
|
|
47
57
|
|
|
48
|
-
# Check if
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
58
|
+
# Check if this is an upgrade or fresh install
|
|
59
|
+
existing_config = None
|
|
60
|
+
is_upgrade = config_path.exists()
|
|
61
|
+
|
|
62
|
+
if is_upgrade:
|
|
63
|
+
try:
|
|
64
|
+
with open(config_path) as f:
|
|
65
|
+
existing_config = json.load(f)
|
|
66
|
+
except:
|
|
67
|
+
existing_config = None
|
|
68
|
+
|
|
69
|
+
# Print welcome header
|
|
70
|
+
click.echo()
|
|
71
|
+
if is_upgrade:
|
|
72
|
+
click.echo(click.style('╔══════════════════════════════════════════╗', fg='yellow', bold=True))
|
|
73
|
+
click.echo(click.style('║ 🔄 VooDocs Upgrade Wizard ║', fg='yellow', bold=True))
|
|
74
|
+
click.echo(click.style('╚══════════════════════════════════════════╝', fg='yellow', bold=True))
|
|
75
|
+
click.echo()
|
|
76
|
+
click.echo(click.style('Existing configuration detected!', fg='yellow'))
|
|
77
|
+
click.echo('This wizard will help you add any missing features.')
|
|
78
|
+
else:
|
|
79
|
+
click.echo(click.style('╔══════════════════════════════════════════╗', fg='cyan', bold=True))
|
|
80
|
+
click.echo(click.style('║ 🎉 Welcome to VooDocs Setup Wizard ║', fg='cyan', bold=True))
|
|
81
|
+
click.echo(click.style('╚══════════════════════════════════════════╝', fg='cyan', bold=True))
|
|
82
|
+
click.echo()
|
|
83
|
+
|
|
84
|
+
# Detect what's already configured
|
|
85
|
+
has_ai_instructions = _check_ai_instructions(cwd)
|
|
86
|
+
has_examples = (cwd / 'voodocs-examples').exists()
|
|
87
|
+
has_gitignore_entry = _check_gitignore(cwd, '.voodocs.json')
|
|
88
|
+
|
|
89
|
+
if is_upgrade and not non_interactive:
|
|
90
|
+
click.echo(click.style('Current Setup:', fg='cyan', bold=True))
|
|
91
|
+
click.echo(f' Configuration: {_status_icon(True)} .voodocs.json exists')
|
|
92
|
+
click.echo(f' AI Instructions: {_status_icon(has_ai_instructions)} {_get_ai_file_status(cwd)}')
|
|
93
|
+
click.echo(f' Example Files: {_status_icon(has_examples)} voodocs-examples/')
|
|
94
|
+
click.echo(f' .gitignore: {_status_icon(has_gitignore_entry)} .voodocs.json entry')
|
|
95
|
+
click.echo()
|
|
96
|
+
|
|
97
|
+
if not click.confirm('Continue with upgrade?', default=True):
|
|
98
|
+
click.echo('Aborted.')
|
|
99
|
+
return
|
|
100
|
+
click.echo()
|
|
101
|
+
|
|
102
|
+
# Step 1: Project Information
|
|
103
|
+
click.echo(click.style('📋 Step 1: Project Information', fg='blue', bold=True))
|
|
104
|
+
click.echo()
|
|
105
|
+
|
|
106
|
+
# Auto-detect or use existing config
|
|
107
|
+
project_name = _get_value(existing_config, 'project.name', _detect_project_name(cwd))
|
|
108
|
+
project_version = _get_value(existing_config, 'project.version', _detect_version(cwd))
|
|
109
|
+
repository_url = _get_value(existing_config, 'project.repository', _detect_repository(cwd))
|
|
110
|
+
project_purpose = _get_value(existing_config, 'project.purpose', f'{project_name} project')
|
|
111
|
+
|
|
112
|
+
if non_interactive:
|
|
113
|
+
name = project_name
|
|
114
|
+
version = project_version
|
|
115
|
+
repository = repository_url
|
|
116
|
+
purpose = project_purpose
|
|
117
|
+
else:
|
|
118
|
+
if is_upgrade:
|
|
119
|
+
click.echo(f'Current values (press Enter to keep):')
|
|
120
|
+
name = click.prompt('Project name', default=project_name)
|
|
121
|
+
purpose = click.prompt('Project purpose', default=project_purpose)
|
|
122
|
+
version = click.prompt('Current version', default=project_version)
|
|
123
|
+
repository = click.prompt('Repository URL (leave empty to skip)', default=repository_url or '', show_default=False)
|
|
124
|
+
|
|
125
|
+
click.echo()
|
|
126
|
+
|
|
127
|
+
# Step 2: Annotation Format
|
|
128
|
+
click.echo(click.style('✨ Step 2: Annotation Format', fg='blue', bold=True))
|
|
129
|
+
click.echo()
|
|
130
|
+
|
|
131
|
+
# DarkArts symbolic format only
|
|
132
|
+
format_choice = 'darkarts'
|
|
133
|
+
click.echo('Using DarkArts symbolic format:')
|
|
134
|
+
click.echo(' ⊢{...} module_purpose ∂{...} dependencies ⚠{...} assumptions')
|
|
135
|
+
click.echo(' ⊳{...} preconditions ⊲{...} postconditions ⊨{...} invariants')
|
|
136
|
+
click.echo(' ⚡{...} complexity 🔒{...} security')
|
|
137
|
+
click.echo(click.style(' ✓', fg='green') + ' AI-native symbolic annotations')
|
|
138
|
+
|
|
139
|
+
click.echo()
|
|
140
|
+
|
|
141
|
+
# Step 3: AI Assistant Setup
|
|
142
|
+
click.echo(click.style('🤖 Step 3: AI Assistant Setup', fg='blue', bold=True))
|
|
143
|
+
click.echo()
|
|
144
|
+
|
|
145
|
+
detected_ai = _detect_ai_environment()
|
|
146
|
+
|
|
147
|
+
if has_ai_instructions and is_upgrade:
|
|
148
|
+
click.echo(f'{_status_icon(True)} AI instructions already configured')
|
|
149
|
+
if non_interactive:
|
|
150
|
+
setup_ai = False
|
|
151
|
+
ai_choice = None
|
|
152
|
+
else:
|
|
153
|
+
setup_ai = click.confirm('Update AI instructions?', default=False)
|
|
154
|
+
if setup_ai:
|
|
155
|
+
ai_choice = click.prompt(
|
|
156
|
+
'Which AI assistant?',
|
|
157
|
+
type=click.Choice(['cursor', 'claude', 'default'], case_sensitive=False),
|
|
158
|
+
default=detected_ai or 'cursor'
|
|
159
|
+
)
|
|
160
|
+
else:
|
|
161
|
+
ai_choice = None
|
|
162
|
+
else:
|
|
163
|
+
if detected_ai:
|
|
164
|
+
click.echo(f'Detected AI environment: {click.style(detected_ai, fg="green", bold=True)}')
|
|
165
|
+
else:
|
|
166
|
+
click.echo('No AI environment detected')
|
|
167
|
+
click.echo()
|
|
168
|
+
|
|
169
|
+
if non_interactive:
|
|
170
|
+
setup_ai = detected_ai is not None
|
|
171
|
+
ai_choice = detected_ai or 'cursor'
|
|
172
|
+
else:
|
|
173
|
+
setup_ai = click.confirm('Generate AI instructions for your coding assistant?', default=True)
|
|
174
|
+
if setup_ai:
|
|
175
|
+
ai_choice = click.prompt(
|
|
176
|
+
'Which AI assistant?',
|
|
177
|
+
type=click.Choice(['cursor', 'claude', 'default'], case_sensitive=False),
|
|
178
|
+
default=detected_ai or 'cursor'
|
|
179
|
+
)
|
|
180
|
+
else:
|
|
181
|
+
ai_choice = None
|
|
182
|
+
|
|
183
|
+
click.echo()
|
|
184
|
+
|
|
185
|
+
# Step 4: Privacy Settings
|
|
186
|
+
click.echo(click.style('🔒 Step 4: Privacy Settings', fg='blue', bold=True))
|
|
187
|
+
click.echo()
|
|
188
|
+
|
|
189
|
+
if has_gitignore_entry and is_upgrade:
|
|
190
|
+
click.echo(f'{_status_icon(True)} .gitignore already configured')
|
|
191
|
+
add_to_gitignore = False
|
|
192
|
+
else:
|
|
193
|
+
if non_interactive:
|
|
194
|
+
is_private = True
|
|
195
|
+
add_to_gitignore = True
|
|
196
|
+
else:
|
|
197
|
+
is_private = click.confirm('Is this a private project?', default=True)
|
|
198
|
+
add_to_gitignore = False
|
|
199
|
+
if is_private:
|
|
200
|
+
add_to_gitignore = click.confirm('Add .voodocs.json to .gitignore?', default=True)
|
|
201
|
+
|
|
202
|
+
click.echo()
|
|
203
|
+
|
|
204
|
+
# Step 5: Example Files
|
|
205
|
+
click.echo(click.style('📝 Step 5: Example Files', fg='blue', bold=True))
|
|
206
|
+
click.echo()
|
|
207
|
+
|
|
208
|
+
if has_examples and is_upgrade:
|
|
209
|
+
click.echo(f'{_status_icon(True)} Example files already exist')
|
|
210
|
+
create_examples = False
|
|
211
|
+
else:
|
|
212
|
+
if non_interactive:
|
|
213
|
+
create_examples = True
|
|
214
|
+
else:
|
|
215
|
+
create_examples = click.confirm('Create example annotated files?', default=not is_upgrade)
|
|
216
|
+
|
|
217
|
+
click.echo()
|
|
218
|
+
|
|
219
|
+
# Show configuration summary
|
|
220
|
+
click.echo(click.style('📊 Configuration Summary', fg='cyan', bold=True))
|
|
221
|
+
click.echo(click.style('─' * 50, fg='cyan'))
|
|
222
|
+
click.echo(f' Project Name: {click.style(name, fg="green")}')
|
|
223
|
+
click.echo(f' Purpose: {purpose}')
|
|
224
|
+
click.echo(f' Version: {version}')
|
|
225
|
+
if repository:
|
|
226
|
+
click.echo(f' Repository: {repository}')
|
|
227
|
+
click.echo(f' Format: {click.style(format_choice, fg="green", bold=True)}')
|
|
228
|
+
if setup_ai and ai_choice:
|
|
229
|
+
click.echo(f' AI Assistant: {click.style(ai_choice, fg="green")} {_status_icon(True, "new") if not has_ai_instructions else ""}')
|
|
230
|
+
if add_to_gitignore:
|
|
231
|
+
click.echo(f' .gitignore: {click.style("Yes", fg="green")} {_status_icon(True, "new")}')
|
|
232
|
+
if create_examples:
|
|
233
|
+
click.echo(f' Example Files: {click.style("Yes", fg="green")} {_status_icon(True, "new")}')
|
|
234
|
+
click.echo(click.style('─' * 50, fg='cyan'))
|
|
235
|
+
click.echo()
|
|
236
|
+
|
|
237
|
+
if not non_interactive:
|
|
238
|
+
action = 'upgrade' if is_upgrade else 'create'
|
|
239
|
+
if not click.confirm(f'Proceed to {action} configuration?', default=True):
|
|
52
240
|
click.echo('Aborted.')
|
|
53
241
|
return
|
|
242
|
+
click.echo()
|
|
243
|
+
|
|
244
|
+
# Create/update configuration
|
|
245
|
+
action_verb = 'Updating' if is_upgrade else 'Creating'
|
|
246
|
+
click.echo(click.style(f'⚙️ {action_verb} configuration...', fg='blue'))
|
|
54
247
|
|
|
55
|
-
# Create default configuration
|
|
56
248
|
config = {
|
|
57
249
|
"version": "1.0",
|
|
58
|
-
"
|
|
250
|
+
"project": {
|
|
251
|
+
"name": name,
|
|
252
|
+
"purpose": purpose,
|
|
253
|
+
"version": version,
|
|
254
|
+
"repository": repository if repository else None
|
|
255
|
+
},
|
|
256
|
+
"format": format_choice,
|
|
59
257
|
"validation": {
|
|
60
258
|
"semantic": True,
|
|
61
259
|
"performance": True,
|
|
@@ -76,40 +274,252 @@ def init(format, config_only):
|
|
|
76
274
|
]
|
|
77
275
|
}
|
|
78
276
|
|
|
277
|
+
# Merge with existing config if upgrading
|
|
278
|
+
if is_upgrade and existing_config:
|
|
279
|
+
# Preserve any custom fields
|
|
280
|
+
for key in existing_config:
|
|
281
|
+
if key not in config:
|
|
282
|
+
config[key] = existing_config[key]
|
|
283
|
+
|
|
79
284
|
# Write configuration
|
|
80
285
|
with open(config_path, 'w') as f:
|
|
81
286
|
json.dump(config, f, indent=2)
|
|
82
287
|
|
|
83
|
-
|
|
288
|
+
verb = 'Updated' if is_upgrade else 'Created'
|
|
289
|
+
click.echo(click.style(f' ✓ {verb} .voodocs.json', fg='green'))
|
|
84
290
|
|
|
85
|
-
if
|
|
86
|
-
|
|
291
|
+
# Add to .gitignore if requested
|
|
292
|
+
if add_to_gitignore:
|
|
293
|
+
_add_to_gitignore(cwd, '.voodocs.json')
|
|
294
|
+
click.echo(click.style(' ✓ Added to .gitignore', fg='green'))
|
|
295
|
+
|
|
296
|
+
# Generate AI instructions
|
|
297
|
+
if setup_ai and ai_choice:
|
|
298
|
+
click.echo(click.style(f' ⚙️ Generating {ai_choice} instructions...', fg='blue'))
|
|
299
|
+
_generate_ai_instructions(cwd, ai_choice, format_choice)
|
|
300
|
+
click.echo(click.style(f' ✓ Created AI instructions', fg='green'))
|
|
301
|
+
|
|
302
|
+
# Create example files
|
|
303
|
+
if create_examples:
|
|
304
|
+
click.echo(click.style(' ⚙️ Creating example files...', fg='blue'))
|
|
87
305
|
examples_dir = cwd / 'voodocs-examples'
|
|
88
306
|
examples_dir.mkdir(exist_ok=True)
|
|
89
307
|
|
|
90
|
-
if
|
|
91
|
-
|
|
308
|
+
if format_choice == 'voodocs':
|
|
309
|
+
_create_voodocs_examples(examples_dir)
|
|
92
310
|
else:
|
|
93
|
-
|
|
311
|
+
_create_darkarts_examples(examples_dir)
|
|
94
312
|
|
|
95
|
-
click.echo(click.style(f'✓ Created
|
|
313
|
+
click.echo(click.style(f' ✓ Created examples in {examples_dir}/', fg='green'))
|
|
96
314
|
|
|
315
|
+
# Success!
|
|
97
316
|
click.echo()
|
|
98
|
-
|
|
317
|
+
if is_upgrade:
|
|
318
|
+
click.echo(click.style('╔══════════════════════════════════════════╗', fg='green', bold=True))
|
|
319
|
+
click.echo(click.style('║ ✅ VooDocs Upgrade Complete! ║', fg='green', bold=True))
|
|
320
|
+
click.echo(click.style('╚══════════════════════════════════════════╝', fg='green', bold=True))
|
|
321
|
+
else:
|
|
322
|
+
click.echo(click.style('╔══════════════════════════════════════════╗', fg='green', bold=True))
|
|
323
|
+
click.echo(click.style('║ 🎉 VooDocs Setup Complete! ║', fg='green', bold=True))
|
|
324
|
+
click.echo(click.style('╚══════════════════════════════════════════╝', fg='green', bold=True))
|
|
99
325
|
click.echo()
|
|
100
|
-
click.echo('Next steps:')
|
|
326
|
+
click.echo(click.style('Next steps:', fg='cyan', bold=True))
|
|
101
327
|
click.echo(' 1. Review .voodocs.json configuration')
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
328
|
+
if setup_ai and ai_choice:
|
|
329
|
+
ai_file = '.cursorrules' if ai_choice == 'cursor' else f'{ai_choice}-instructions.md'
|
|
330
|
+
click.echo(f' 2. Your AI assistant will use {ai_file}')
|
|
331
|
+
click.echo(' 3. Add annotations to your code')
|
|
332
|
+
click.echo(' 4. Run: voodocs validate .')
|
|
333
|
+
click.echo(' 5. Run: voodocs generate . ./docs')
|
|
334
|
+
click.echo()
|
|
335
|
+
if is_upgrade:
|
|
336
|
+
click.echo(click.style('💡 Tip:', fg='yellow') + ' Run ' + click.style('voodocs init --upgrade', fg='cyan') + ' again after future updates!')
|
|
337
|
+
click.echo()
|
|
338
|
+
click.echo(f'Documentation: {click.style("https://voodocs.com/docs", fg="blue", underline=True)}')
|
|
105
339
|
click.echo()
|
|
106
|
-
click.echo('Documentation: https://voodocs.com/docs')
|
|
107
340
|
|
|
108
341
|
|
|
109
|
-
def
|
|
342
|
+
def _status_icon(exists: bool, label: str = None) -> str:
|
|
343
|
+
"""Return a status icon."""
|
|
344
|
+
if label == "new":
|
|
345
|
+
return click.style('🆕', fg='green')
|
|
346
|
+
return click.style('✓', fg='green') if exists else click.style('✗', fg='red')
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
def _get_value(config: Optional[Dict[str, Any]], path: str, default: Any) -> Any:
|
|
350
|
+
"""Get a value from nested config dict using dot notation."""
|
|
351
|
+
if not config:
|
|
352
|
+
return default
|
|
353
|
+
|
|
354
|
+
keys = path.split('.')
|
|
355
|
+
value = config
|
|
356
|
+
for key in keys:
|
|
357
|
+
if isinstance(value, dict) and key in value:
|
|
358
|
+
value = value[key]
|
|
359
|
+
else:
|
|
360
|
+
return default
|
|
361
|
+
|
|
362
|
+
return value if value is not None else default
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
def _check_ai_instructions(cwd: Path) -> bool:
|
|
366
|
+
"""Check if AI instructions exist."""
|
|
367
|
+
return (
|
|
368
|
+
(cwd / '.cursorrules').exists() or
|
|
369
|
+
(cwd / '.claude' / 'instructions.md').exists() or
|
|
370
|
+
(cwd / 'AI_INSTRUCTIONS.md').exists()
|
|
371
|
+
)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def _get_ai_file_status(cwd: Path) -> str:
|
|
375
|
+
"""Get status of AI instruction files."""
|
|
376
|
+
files = []
|
|
377
|
+
if (cwd / '.cursorrules').exists():
|
|
378
|
+
files.append('.cursorrules')
|
|
379
|
+
if (cwd / '.claude' / 'instructions.md').exists():
|
|
380
|
+
files.append('.claude/instructions.md')
|
|
381
|
+
if (cwd / 'AI_INSTRUCTIONS.md').exists():
|
|
382
|
+
files.append('AI_INSTRUCTIONS.md')
|
|
383
|
+
|
|
384
|
+
return ', '.join(files) if files else 'none'
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
def _check_gitignore(cwd: Path, entry: str) -> bool:
|
|
388
|
+
"""Check if entry exists in .gitignore."""
|
|
389
|
+
gitignore = cwd / '.gitignore'
|
|
390
|
+
if not gitignore.exists():
|
|
391
|
+
return False
|
|
392
|
+
|
|
393
|
+
content = gitignore.read_text()
|
|
394
|
+
return entry in content
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def _detect_project_name(cwd: Path) -> str:
|
|
398
|
+
"""Detect project name from directory or package.json."""
|
|
399
|
+
package_json = cwd / 'package.json'
|
|
400
|
+
if package_json.exists():
|
|
401
|
+
try:
|
|
402
|
+
with open(package_json) as f:
|
|
403
|
+
data = json.load(f)
|
|
404
|
+
if 'name' in data:
|
|
405
|
+
return data['name']
|
|
406
|
+
except:
|
|
407
|
+
pass
|
|
408
|
+
|
|
409
|
+
return cwd.name
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _detect_version(cwd: Path) -> str:
|
|
413
|
+
"""Detect project version."""
|
|
414
|
+
package_json = cwd / 'package.json'
|
|
415
|
+
if package_json.exists():
|
|
416
|
+
try:
|
|
417
|
+
with open(package_json) as f:
|
|
418
|
+
data = json.load(f)
|
|
419
|
+
if 'version' in data:
|
|
420
|
+
return data['version']
|
|
421
|
+
except:
|
|
422
|
+
pass
|
|
423
|
+
|
|
424
|
+
pyproject = cwd / 'pyproject.toml'
|
|
425
|
+
if pyproject.exists():
|
|
426
|
+
try:
|
|
427
|
+
with open(pyproject) as f:
|
|
428
|
+
for line in f:
|
|
429
|
+
if line.startswith('version'):
|
|
430
|
+
return line.split('=')[1].strip().strip('"\'')
|
|
431
|
+
except:
|
|
432
|
+
pass
|
|
433
|
+
|
|
434
|
+
version_file = cwd / 'VERSION'
|
|
435
|
+
if version_file.exists():
|
|
436
|
+
try:
|
|
437
|
+
return version_file.read_text().strip()
|
|
438
|
+
except:
|
|
439
|
+
pass
|
|
440
|
+
|
|
441
|
+
return '1.0.0'
|
|
442
|
+
|
|
443
|
+
|
|
444
|
+
def _detect_repository(cwd: Path) -> Optional[str]:
|
|
445
|
+
"""Detect repository URL from git config."""
|
|
446
|
+
try:
|
|
447
|
+
result = subprocess.run(
|
|
448
|
+
['git', 'config', '--get', 'remote.origin.url'],
|
|
449
|
+
capture_output=True,
|
|
450
|
+
text=True,
|
|
451
|
+
cwd=cwd
|
|
452
|
+
)
|
|
453
|
+
if result.returncode == 0:
|
|
454
|
+
url = result.stdout.strip()
|
|
455
|
+
if url.startswith('git@github.com:'):
|
|
456
|
+
url = url.replace('git@github.com:', 'https://github.com/')
|
|
457
|
+
if url.endswith('.git'):
|
|
458
|
+
url = url[:-4]
|
|
459
|
+
return url
|
|
460
|
+
except:
|
|
461
|
+
pass
|
|
462
|
+
|
|
463
|
+
return None
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
def _detect_ai_environment() -> Optional[str]:
|
|
467
|
+
"""Detect which AI environment is being used."""
|
|
468
|
+
if os.environ.get('CURSOR_USER') or Path('.cursorrules').exists():
|
|
469
|
+
return 'cursor'
|
|
470
|
+
|
|
471
|
+
if os.environ.get('CLAUDE_API_KEY') or Path('.claude').exists():
|
|
472
|
+
return 'claude'
|
|
473
|
+
|
|
474
|
+
return None
|
|
475
|
+
|
|
476
|
+
|
|
477
|
+
def _add_to_gitignore(cwd: Path, entry: str):
|
|
478
|
+
"""Add an entry to .gitignore."""
|
|
479
|
+
gitignore = cwd / '.gitignore'
|
|
480
|
+
|
|
481
|
+
if gitignore.exists():
|
|
482
|
+
content = gitignore.read_text()
|
|
483
|
+
if entry in content:
|
|
484
|
+
return
|
|
485
|
+
else:
|
|
486
|
+
content = ''
|
|
487
|
+
|
|
488
|
+
if content and not content.endswith('\n'):
|
|
489
|
+
content += '\n'
|
|
490
|
+
content += f'\n# VooDocs\n{entry}\n'
|
|
491
|
+
|
|
492
|
+
gitignore.write_text(content)
|
|
493
|
+
|
|
494
|
+
|
|
495
|
+
def _generate_ai_instructions(cwd: Path, ai_type: str, format_type: str):
|
|
496
|
+
"""Generate AI instructions file."""
|
|
497
|
+
cli_dir = Path(__file__).parent
|
|
498
|
+
template_path = cli_dir.parent / 'darkarts' / 'instructions' / f'{ai_type}.md'
|
|
499
|
+
|
|
500
|
+
if not template_path.exists():
|
|
501
|
+
template_path = cli_dir.parent / 'darkarts' / 'instructions' / 'default.md'
|
|
502
|
+
|
|
503
|
+
if not template_path.exists():
|
|
504
|
+
return
|
|
505
|
+
|
|
506
|
+
instructions = template_path.read_text()
|
|
507
|
+
|
|
508
|
+
if ai_type == 'cursor':
|
|
509
|
+
output_file = cwd / '.cursorrules'
|
|
510
|
+
elif ai_type == 'claude':
|
|
511
|
+
output_dir = cwd / '.claude'
|
|
512
|
+
output_dir.mkdir(exist_ok=True)
|
|
513
|
+
output_file = output_dir / 'instructions.md'
|
|
514
|
+
else:
|
|
515
|
+
output_file = cwd / 'AI_INSTRUCTIONS.md'
|
|
516
|
+
|
|
517
|
+
output_file.write_text(instructions)
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def _create_voodocs_examples(examples_dir: Path):
|
|
110
521
|
"""Create example files with @voodocs annotations."""
|
|
111
522
|
|
|
112
|
-
# TypeScript example
|
|
113
523
|
ts_example = '''/**@voodocs
|
|
114
524
|
module_purpose: "Example TypeScript module demonstrating @voodocs annotations"
|
|
115
525
|
dependencies: []
|
|
@@ -137,41 +547,11 @@ export function greet(name: string, age: number): string {
|
|
|
137
547
|
'''
|
|
138
548
|
|
|
139
549
|
(examples_dir / 'example.ts').write_text(ts_example)
|
|
140
|
-
|
|
141
|
-
# Python example
|
|
142
|
-
py_example = '''"""@voodocs
|
|
143
|
-
module_purpose: "Example Python module demonstrating @voodocs annotations"
|
|
144
|
-
dependencies: []
|
|
145
|
-
assumptions: ["Python 3.7+"]
|
|
146
|
-
"""
|
|
147
|
-
|
|
148
|
-
def calculate_total(prices: list[float], tax_rate: float) -> float:
|
|
149
|
-
"""@voodocs
|
|
150
|
-
preconditions: [
|
|
151
|
-
"prices is a non-empty list of positive numbers",
|
|
152
|
-
"tax_rate is between 0 and 1"
|
|
153
|
-
]
|
|
154
|
-
postconditions: [
|
|
155
|
-
"Returns the total with tax applied",
|
|
156
|
-
"Result is greater than or equal to sum of prices"
|
|
157
|
-
]
|
|
158
|
-
invariants: [
|
|
159
|
-
"Does not modify the prices list"
|
|
160
|
-
]
|
|
161
|
-
side_effects: []
|
|
162
|
-
complexity: "O(n) where n is the length of prices"
|
|
163
|
-
"""
|
|
164
|
-
subtotal = sum(prices)
|
|
165
|
-
return subtotal * (1 + tax_rate)
|
|
166
|
-
'''
|
|
167
|
-
|
|
168
|
-
(examples_dir / 'example.py').write_text(py_example)
|
|
169
550
|
|
|
170
551
|
|
|
171
|
-
def
|
|
552
|
+
def _create_darkarts_examples(examples_dir: Path):
|
|
172
553
|
"""Create example files with symbolic @darkarts annotations."""
|
|
173
554
|
|
|
174
|
-
# TypeScript example
|
|
175
555
|
ts_example = '''/**@darkarts
|
|
176
556
|
⊢{Example TypeScript module demonstrating symbolic @darkarts annotations}
|
|
177
557
|
∂{}
|
|
@@ -190,7 +570,7 @@ def create_darkarts_example(examples_dir: Path):
|
|
|
190
570
|
⊨{
|
|
191
571
|
Does ¬ modify input parameters
|
|
192
572
|
}
|
|
193
|
-
⚡{}
|
|
573
|
+
⚡{O(1)}
|
|
194
574
|
*/
|
|
195
575
|
export function greet(name: string, age: number): string {
|
|
196
576
|
return `Hello, ${name}! You are ${age} years old.`;
|
|
@@ -198,31 +578,3 @@ export function greet(name: string, age: number): string {
|
|
|
198
578
|
'''
|
|
199
579
|
|
|
200
580
|
(examples_dir / 'example.ts').write_text(ts_example)
|
|
201
|
-
|
|
202
|
-
# Python example
|
|
203
|
-
py_example = '''"""@darkarts
|
|
204
|
-
⊢{Example Python module demonstrating symbolic @darkarts annotations}
|
|
205
|
-
∂{}
|
|
206
|
-
⚠{Python 3.7+}
|
|
207
|
-
"""
|
|
208
|
-
|
|
209
|
-
def calculate_total(prices: list[float], tax_rate: float) -> float:
|
|
210
|
-
"""@darkarts
|
|
211
|
-
⊳{
|
|
212
|
-
prices is a non-empty list ∧ ∀ p ∈ prices: p > 0
|
|
213
|
-
tax_rate ∈ [0, 1]
|
|
214
|
-
}
|
|
215
|
-
⊲{
|
|
216
|
-
Returns the total with tax applied
|
|
217
|
-
result ≥ sum(prices)
|
|
218
|
-
}
|
|
219
|
-
⊨{
|
|
220
|
-
Does ¬ modify the prices list
|
|
221
|
-
}
|
|
222
|
-
⚡{O(n) where n = len(prices)}
|
|
223
|
-
"""
|
|
224
|
-
subtotal = sum(prices)
|
|
225
|
-
return subtotal * (1 + tax_rate)
|
|
226
|
-
'''
|
|
227
|
-
|
|
228
|
-
(examples_dir / 'example.py').write_text(py_example)
|
|
@@ -43,23 +43,18 @@ except ImportError:
|
|
|
43
43
|
|
|
44
44
|
|
|
45
45
|
class AnnotationParser:
|
|
46
|
-
"""Parser for DarkArts annotations."""
|
|
46
|
+
"""Parser for DarkArts symbolic annotations (@darkarts only)."""
|
|
47
47
|
|
|
48
|
-
#
|
|
49
|
-
PATTERNS = {
|
|
50
|
-
Language.PYTHON: r'"""@voodocs\s*(.*?)\s*"""',
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
# DarkArts patterns (symbolic annotations) - support both @voodocs and @darkarts
|
|
48
|
+
# DarkArts patterns (symbolic annotations)
|
|
54
49
|
DARKARTS_PATTERNS = {
|
|
55
|
-
Language.PYTHON: r'"""@
|
|
56
|
-
Language.TYPESCRIPT: r'/\*\*@
|
|
57
|
-
Language.JAVASCRIPT: r'/\*\*@
|
|
58
|
-
Language.JAVA: r'/\*\*@
|
|
59
|
-
Language.CPP: r'/\*\*@
|
|
60
|
-
Language.CSHARP: r'/\*\*@
|
|
61
|
-
Language.GO: r'/\*\*@
|
|
62
|
-
Language.RUST: r'/\*\*@
|
|
50
|
+
Language.PYTHON: r'"""@darkarts\s*(.*?)\s*"""',
|
|
51
|
+
Language.TYPESCRIPT: r'/\*\*@darkarts\s*(.*?)\s*\*/',
|
|
52
|
+
Language.JAVASCRIPT: r'/\*\*@darkarts\s*(.*?)\s*\*/',
|
|
53
|
+
Language.JAVA: r'/\*\*@darkarts\s*(.*?)\s*\*/',
|
|
54
|
+
Language.CPP: r'/\*\*@darkarts\s*(.*?)\s*\*/',
|
|
55
|
+
Language.CSHARP: r'/\*\*@darkarts\s*(.*?)\s*\*/',
|
|
56
|
+
Language.GO: r'/\*\*@darkarts\s*(.*?)\s*\*/',
|
|
57
|
+
Language.RUST: r'/\*\*@darkarts\s*(.*?)\s*\*/',
|
|
63
58
|
}
|
|
64
59
|
|
|
65
60
|
def __init__(self):
|