invar-tools 1.6.0__py3-none-any.whl → 1.7.0__py3-none-any.whl
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.
- invar/shell/commands/guard.py +2 -0
- invar/shell/commands/init.py +8 -79
- invar/shell/commands/uninstall.py +341 -0
- invar/shell/templates.py +1 -70
- invar/templates/commands/audit.md +6 -0
- invar/templates/commands/guard.md +6 -0
- invar/templates/pre-commit-config.yaml.template +2 -0
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.0.dist-info}/METADATA +1 -1
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.0.dist-info}/RECORD +14 -15
- invar/templates/aider.conf.yml.template +0 -31
- invar/templates/cursorrules.template +0 -40
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.0.dist-info}/WHEEL +0 -0
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.0.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.0.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.0.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.0.dist-info}/licenses/NOTICE +0 -0
invar/shell/commands/guard.py
CHANGED
|
@@ -521,9 +521,11 @@ from invar.shell.commands.init import init
|
|
|
521
521
|
from invar.shell.commands.mutate import mutate # DX-28
|
|
522
522
|
from invar.shell.commands.sync_self import sync_self # DX-49
|
|
523
523
|
from invar.shell.commands.test import test, verify
|
|
524
|
+
from invar.shell.commands.uninstall import uninstall # DX-69
|
|
524
525
|
from invar.shell.commands.update import update
|
|
525
526
|
|
|
526
527
|
app.command()(init)
|
|
528
|
+
app.command()(uninstall) # DX-69: Remove Invar from project
|
|
527
529
|
app.command()(update)
|
|
528
530
|
app.command()(test)
|
|
529
531
|
app.command()(verify)
|
invar/shell/commands/init.py
CHANGED
|
@@ -38,8 +38,6 @@ from invar.shell.mcp_config import (
|
|
|
38
38
|
from invar.shell.template_engine import generate_from_manifest
|
|
39
39
|
from invar.shell.templates import (
|
|
40
40
|
add_config,
|
|
41
|
-
add_invar_reference,
|
|
42
|
-
create_agent_config,
|
|
43
41
|
create_directories,
|
|
44
42
|
detect_agent_configs,
|
|
45
43
|
install_hooks,
|
|
@@ -65,18 +63,17 @@ def run_claude_init(path: Path) -> bool:
|
|
|
65
63
|
|
|
66
64
|
console.print("\n[bold]Running claude /init...[/bold]")
|
|
67
65
|
try:
|
|
66
|
+
# Don't capture output - claude /init is interactive and needs user input
|
|
68
67
|
result = subprocess.run(
|
|
69
68
|
["claude", "/init"],
|
|
70
69
|
cwd=path,
|
|
71
|
-
capture_output=True,
|
|
72
|
-
text=True,
|
|
73
70
|
timeout=120,
|
|
74
71
|
)
|
|
75
72
|
if result.returncode == 0:
|
|
76
73
|
console.print("[green]claude /init completed successfully[/green]")
|
|
77
74
|
return True
|
|
78
75
|
else:
|
|
79
|
-
console.print(
|
|
76
|
+
console.print("[yellow]Warning:[/yellow] claude /init failed")
|
|
80
77
|
return False
|
|
81
78
|
except subprocess.TimeoutExpired:
|
|
82
79
|
console.print("[yellow]Warning:[/yellow] claude /init timed out")
|
|
@@ -86,57 +83,6 @@ def run_claude_init(path: Path) -> bool:
|
|
|
86
83
|
return False
|
|
87
84
|
|
|
88
85
|
|
|
89
|
-
def append_invar_reference_to_claude_md(path: Path) -> bool:
|
|
90
|
-
"""
|
|
91
|
-
Append Invar reference to existing CLAUDE.md.
|
|
92
|
-
|
|
93
|
-
Preserves content generated by 'claude /init'.
|
|
94
|
-
Returns True if modified, False otherwise.
|
|
95
|
-
"""
|
|
96
|
-
claude_md = path / "CLAUDE.md"
|
|
97
|
-
if not claude_md.exists():
|
|
98
|
-
return False
|
|
99
|
-
|
|
100
|
-
content = claude_md.read_text()
|
|
101
|
-
if "INVAR.md" in content:
|
|
102
|
-
console.print("[dim]CLAUDE.md already references INVAR.md[/dim]")
|
|
103
|
-
return False
|
|
104
|
-
|
|
105
|
-
# Append reference at the end
|
|
106
|
-
invar_reference = """
|
|
107
|
-
|
|
108
|
-
---
|
|
109
|
-
|
|
110
|
-
## Invar Protocol
|
|
111
|
-
|
|
112
|
-
> **Protocol:** Follow [INVAR.md](./INVAR.md) — includes Check-In, USBV workflow, and Task Completion.
|
|
113
|
-
|
|
114
|
-
### Check-In
|
|
115
|
-
|
|
116
|
-
Your first message MUST display:
|
|
117
|
-
|
|
118
|
-
```
|
|
119
|
-
✓ Check-In: [project] | [branch] | [clean/dirty]
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
Read `.invar/context.md` first. Do NOT run guard/map at Check-In.
|
|
123
|
-
|
|
124
|
-
### Final
|
|
125
|
-
|
|
126
|
-
Your last message MUST display:
|
|
127
|
-
|
|
128
|
-
```
|
|
129
|
-
✓ Final: guard PASS | 0 errors, 2 warnings
|
|
130
|
-
```
|
|
131
|
-
|
|
132
|
-
Execute `invar guard` and show this one-line summary.
|
|
133
|
-
"""
|
|
134
|
-
|
|
135
|
-
claude_md.write_text(content + invar_reference)
|
|
136
|
-
console.print("[green]Updated[/green] CLAUDE.md (added Invar reference)")
|
|
137
|
-
return True
|
|
138
|
-
|
|
139
|
-
|
|
140
86
|
# @shell_complexity: MCP config with method selection and validation
|
|
141
87
|
def configure_mcp_with_method(
|
|
142
88
|
path: Path, mcp_method: str | None
|
|
@@ -312,11 +258,9 @@ def init(
|
|
|
312
258
|
return
|
|
313
259
|
|
|
314
260
|
# DX-21B: Run claude /init if requested (before sync)
|
|
261
|
+
# DX-69: sync_templates() will merge claude's CLAUDE.md with invar template
|
|
315
262
|
if claude:
|
|
316
|
-
|
|
317
|
-
if claude_success:
|
|
318
|
-
# Append Invar reference to generated CLAUDE.md
|
|
319
|
-
append_invar_reference_to_claude_md(path)
|
|
263
|
+
run_claude_init(path)
|
|
320
264
|
|
|
321
265
|
config_result = add_config(path, console)
|
|
322
266
|
if isinstance(config_result, Failure):
|
|
@@ -371,28 +315,13 @@ def init(
|
|
|
371
315
|
if isinstance(result, Success) and result.unwrap():
|
|
372
316
|
console.print("[green]Created[/green] .invar/proposals/TEMPLATE.md")
|
|
373
317
|
|
|
374
|
-
# Agent detection
|
|
318
|
+
# Agent detection (DX-69: simplified, only Claude Code supported)
|
|
375
319
|
console.print("\n[bold]Checking for agent configurations...[/bold]")
|
|
376
320
|
agent_result = detect_agent_configs(path)
|
|
377
|
-
if isinstance(agent_result,
|
|
378
|
-
console.print(f"[yellow]Warning:[/yellow] {agent_result.failure()}")
|
|
379
|
-
agent_status: dict[str, str] = {}
|
|
380
|
-
else:
|
|
321
|
+
if isinstance(agent_result, Success):
|
|
381
322
|
agent_status = agent_result.unwrap()
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
for agent, status in agent_status.items():
|
|
385
|
-
if status == "configured":
|
|
386
|
-
console.print(f" [green]✓[/green] {agent}: already configured")
|
|
387
|
-
elif status == "found":
|
|
388
|
-
# Existing file without Invar reference - ask before modifying
|
|
389
|
-
if yes or typer.confirm(f" Add Invar reference to {agent} config?", default=True):
|
|
390
|
-
add_invar_reference(path, agent, console)
|
|
391
|
-
else:
|
|
392
|
-
console.print(f" [yellow]○[/yellow] {agent}: skipped")
|
|
393
|
-
elif status == "not_found":
|
|
394
|
-
# Create full template with workflow enforcement (DX-17)
|
|
395
|
-
create_agent_config(path, agent, console)
|
|
323
|
+
if agent_status.get("claude") == "configured":
|
|
324
|
+
console.print(" [green]✓[/green] claude: already configured")
|
|
396
325
|
|
|
397
326
|
# Configure MCP server (DX-16, DX-21B)
|
|
398
327
|
configure_mcp_with_method(path, mcp_method)
|
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DX-69: Uninstall Invar from a project.
|
|
3
|
+
|
|
4
|
+
Safely removes Invar files and configurations while preserving user content.
|
|
5
|
+
Uses marker-based detection to identify Invar-generated content.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import json
|
|
11
|
+
import re
|
|
12
|
+
import shutil
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
|
|
15
|
+
import typer
|
|
16
|
+
from rich.console import Console
|
|
17
|
+
|
|
18
|
+
console = Console()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def has_invar_marker(path: Path) -> bool:
|
|
22
|
+
"""Check if a file has Invar markers (_invar: or <!--invar:)."""
|
|
23
|
+
try:
|
|
24
|
+
content = path.read_text()
|
|
25
|
+
return "_invar:" in content or "<!--invar:" in content
|
|
26
|
+
except (OSError, UnicodeDecodeError):
|
|
27
|
+
return False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def has_invar_region_marker(path: Path) -> bool:
|
|
31
|
+
"""Check if a file has # invar:begin marker."""
|
|
32
|
+
try:
|
|
33
|
+
content = path.read_text()
|
|
34
|
+
return "# invar:begin" in content
|
|
35
|
+
except (OSError, UnicodeDecodeError):
|
|
36
|
+
return False
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def has_invar_hook_marker(path: Path) -> bool:
|
|
40
|
+
"""Check if a hook file has invar marker."""
|
|
41
|
+
try:
|
|
42
|
+
content = path.read_text()
|
|
43
|
+
# Invar hooks have specific patterns
|
|
44
|
+
return "invar" in content.lower() and (
|
|
45
|
+
"INVAR_" in content
|
|
46
|
+
or "invar guard" in content
|
|
47
|
+
or "invar_guard" in content
|
|
48
|
+
or "invar." in content.lower() # wrapper files: source invar.PreToolUse.sh
|
|
49
|
+
or "invar hook" in content.lower() # comment: # Invar hook wrapper
|
|
50
|
+
)
|
|
51
|
+
except (OSError, UnicodeDecodeError):
|
|
52
|
+
return False
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# @shell_orchestration: Regex patterns tightly coupled to file removal logic
|
|
56
|
+
def remove_invar_regions(content: str) -> str:
|
|
57
|
+
"""Remove <!--invar:xxx-->...<!--/invar:xxx--> regions except user region."""
|
|
58
|
+
patterns = [
|
|
59
|
+
# HTML-style regions (CLAUDE.md)
|
|
60
|
+
(r"<!--invar:critical-->.*?<!--/invar:critical-->\n?", ""),
|
|
61
|
+
(r"<!--invar:managed[^>]*-->.*?<!--/invar:managed-->\n?", ""),
|
|
62
|
+
(r"<!--invar:project-->.*?<!--/invar:project-->\n?", ""),
|
|
63
|
+
# Comment-style regions (.pre-commit-config.yaml)
|
|
64
|
+
(r"# invar:begin\n.*?# invar:end\n?", ""),
|
|
65
|
+
]
|
|
66
|
+
for pattern, replacement in patterns:
|
|
67
|
+
content = re.sub(pattern, replacement, content, flags=re.DOTALL)
|
|
68
|
+
return content.strip()
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def remove_mcp_invar_entry(path: Path) -> tuple[bool, str]:
|
|
72
|
+
"""Remove invar entry from .mcp.json, return (modified, new_content)."""
|
|
73
|
+
try:
|
|
74
|
+
content = path.read_text()
|
|
75
|
+
data = json.loads(content)
|
|
76
|
+
if "mcpServers" in data and "invar" in data["mcpServers"]:
|
|
77
|
+
del data["mcpServers"]["invar"]
|
|
78
|
+
# If no servers left, indicate file can be deleted
|
|
79
|
+
if not data["mcpServers"]:
|
|
80
|
+
return True, ""
|
|
81
|
+
return True, json.dumps(data, indent=2)
|
|
82
|
+
return False, content
|
|
83
|
+
except (OSError, json.JSONDecodeError):
|
|
84
|
+
return False, ""
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# @shell_complexity: Multi-file type detection requires comprehensive branching
|
|
88
|
+
def collect_removal_targets(path: Path) -> dict:
|
|
89
|
+
"""Collect files and directories to remove/modify."""
|
|
90
|
+
targets = {
|
|
91
|
+
"delete_dirs": [],
|
|
92
|
+
"delete_files": [],
|
|
93
|
+
"modify_files": [],
|
|
94
|
+
"skip": [],
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Directories to delete entirely
|
|
98
|
+
invar_dir = path / ".invar"
|
|
99
|
+
if invar_dir.exists():
|
|
100
|
+
targets["delete_dirs"].append((".invar/", "directory"))
|
|
101
|
+
|
|
102
|
+
# Files to delete entirely
|
|
103
|
+
for file_name, description in [
|
|
104
|
+
("invar.toml", "config"),
|
|
105
|
+
("INVAR.md", "protocol"),
|
|
106
|
+
]:
|
|
107
|
+
file_path = path / file_name
|
|
108
|
+
if file_path.exists():
|
|
109
|
+
targets["delete_files"].append((file_name, description))
|
|
110
|
+
|
|
111
|
+
# Skills with _invar marker
|
|
112
|
+
skills_dir = path / ".claude" / "skills"
|
|
113
|
+
if skills_dir.exists():
|
|
114
|
+
for skill_dir in skills_dir.iterdir():
|
|
115
|
+
if skill_dir.is_dir():
|
|
116
|
+
skill_file = skill_dir / "SKILL.md"
|
|
117
|
+
if skill_file.exists():
|
|
118
|
+
if has_invar_marker(skill_file):
|
|
119
|
+
targets["delete_dirs"].append(
|
|
120
|
+
(f".claude/skills/{skill_dir.name}/", "skill, has _invar marker")
|
|
121
|
+
)
|
|
122
|
+
else:
|
|
123
|
+
targets["skip"].append(
|
|
124
|
+
(f".claude/skills/{skill_dir.name}/", "no _invar marker")
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
# Commands with _invar marker
|
|
128
|
+
commands_dir = path / ".claude" / "commands"
|
|
129
|
+
if commands_dir.exists():
|
|
130
|
+
for cmd_file in commands_dir.glob("*.md"):
|
|
131
|
+
if has_invar_marker(cmd_file):
|
|
132
|
+
targets["delete_files"].append(
|
|
133
|
+
(f".claude/commands/{cmd_file.name}", "command, has _invar marker")
|
|
134
|
+
)
|
|
135
|
+
else:
|
|
136
|
+
targets["skip"].append(
|
|
137
|
+
(f".claude/commands/{cmd_file.name}", "no _invar marker")
|
|
138
|
+
)
|
|
139
|
+
|
|
140
|
+
# Hooks with invar marker
|
|
141
|
+
hooks_dir = path / ".claude" / "hooks"
|
|
142
|
+
if hooks_dir.exists():
|
|
143
|
+
for hook_file in hooks_dir.glob("*.sh"):
|
|
144
|
+
if has_invar_hook_marker(hook_file):
|
|
145
|
+
targets["delete_files"].append(
|
|
146
|
+
(f".claude/hooks/{hook_file.name}", "hook, has invar marker")
|
|
147
|
+
)
|
|
148
|
+
|
|
149
|
+
# CLAUDE.md - modify, not delete
|
|
150
|
+
claude_md = path / "CLAUDE.md"
|
|
151
|
+
if claude_md.exists():
|
|
152
|
+
content = claude_md.read_text()
|
|
153
|
+
if "<!--invar:" in content:
|
|
154
|
+
# Check if there's user content
|
|
155
|
+
has_user_region = "<!--invar:user-->" in content
|
|
156
|
+
targets["modify_files"].append(
|
|
157
|
+
("CLAUDE.md", f"remove invar regions{', keep user region' if has_user_region else ''}")
|
|
158
|
+
)
|
|
159
|
+
|
|
160
|
+
# .mcp.json - modify or delete
|
|
161
|
+
mcp_json = path / ".mcp.json"
|
|
162
|
+
if mcp_json.exists():
|
|
163
|
+
modified, new_content = remove_mcp_invar_entry(mcp_json)
|
|
164
|
+
if modified:
|
|
165
|
+
if new_content:
|
|
166
|
+
targets["modify_files"].append((".mcp.json", "remove mcpServers.invar"))
|
|
167
|
+
else:
|
|
168
|
+
targets["delete_files"].append((".mcp.json", "only had invar config"))
|
|
169
|
+
|
|
170
|
+
# Config files with region markers (DX-69: cursor/aider removed)
|
|
171
|
+
for file_name in [".pre-commit-config.yaml"]:
|
|
172
|
+
file_path = path / file_name
|
|
173
|
+
if file_path.exists():
|
|
174
|
+
if has_invar_region_marker(file_path):
|
|
175
|
+
content = file_path.read_text()
|
|
176
|
+
cleaned = remove_invar_regions(content)
|
|
177
|
+
if cleaned:
|
|
178
|
+
targets["modify_files"].append((file_name, "remove invar:begin..end block"))
|
|
179
|
+
else:
|
|
180
|
+
targets["delete_files"].append((file_name, "only had invar content"))
|
|
181
|
+
else:
|
|
182
|
+
targets["skip"].append((file_name, "no invar:begin marker"))
|
|
183
|
+
|
|
184
|
+
# Empty directories to clean up
|
|
185
|
+
for dir_name in ["src/core", "src/shell"]:
|
|
186
|
+
dir_path = path / dir_name
|
|
187
|
+
if dir_path.exists() and dir_path.is_dir():
|
|
188
|
+
if not any(dir_path.iterdir()):
|
|
189
|
+
targets["delete_dirs"].append((dir_name, "empty directory"))
|
|
190
|
+
|
|
191
|
+
return targets
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
# @shell_complexity: Rich output formatting for different target categories
|
|
195
|
+
def show_preview(targets: dict) -> None:
|
|
196
|
+
"""Display what would be removed/modified."""
|
|
197
|
+
console.print("\n[bold]Invar Uninstall Preview[/bold]")
|
|
198
|
+
console.print("=" * 40)
|
|
199
|
+
|
|
200
|
+
if targets["delete_dirs"] or targets["delete_files"]:
|
|
201
|
+
console.print("\n[red]Will DELETE:[/red]")
|
|
202
|
+
for item, desc in targets["delete_dirs"]:
|
|
203
|
+
console.print(f" {item:40} ({desc})")
|
|
204
|
+
for item, desc in targets["delete_files"]:
|
|
205
|
+
console.print(f" {item:40} ({desc})")
|
|
206
|
+
|
|
207
|
+
if targets["modify_files"]:
|
|
208
|
+
console.print("\n[yellow]Will MODIFY:[/yellow]")
|
|
209
|
+
for item, desc in targets["modify_files"]:
|
|
210
|
+
console.print(f" {item:40} ({desc})")
|
|
211
|
+
|
|
212
|
+
if targets["skip"]:
|
|
213
|
+
console.print("\n[dim]Will SKIP:[/dim]")
|
|
214
|
+
for item, desc in targets["skip"]:
|
|
215
|
+
console.print(f" {item:40} ({desc})")
|
|
216
|
+
|
|
217
|
+
console.print()
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
# @shell_complexity: Different file types require different removal strategies
|
|
221
|
+
def execute_removal(path: Path, targets: dict) -> None:
|
|
222
|
+
"""Execute the removal/modification operations."""
|
|
223
|
+
# Delete directories
|
|
224
|
+
for dir_name, _ in targets["delete_dirs"]:
|
|
225
|
+
dir_path = path / dir_name.rstrip("/")
|
|
226
|
+
if dir_path.exists():
|
|
227
|
+
shutil.rmtree(dir_path)
|
|
228
|
+
console.print(f"[red]Deleted[/red] {dir_name}")
|
|
229
|
+
|
|
230
|
+
# Delete files
|
|
231
|
+
for file_name, _ in targets["delete_files"]:
|
|
232
|
+
file_path = path / file_name
|
|
233
|
+
if file_path.exists():
|
|
234
|
+
file_path.unlink()
|
|
235
|
+
console.print(f"[red]Deleted[/red] {file_name}")
|
|
236
|
+
|
|
237
|
+
# Modify files
|
|
238
|
+
for file_name, _desc in targets["modify_files"]:
|
|
239
|
+
file_path = path / file_name
|
|
240
|
+
if not file_path.exists():
|
|
241
|
+
continue
|
|
242
|
+
|
|
243
|
+
if file_name == ".mcp.json":
|
|
244
|
+
modified, new_content = remove_mcp_invar_entry(file_path)
|
|
245
|
+
if modified and new_content:
|
|
246
|
+
file_path.write_text(new_content)
|
|
247
|
+
console.print(f"[yellow]Modified[/yellow] {file_name}")
|
|
248
|
+
else:
|
|
249
|
+
content = file_path.read_text()
|
|
250
|
+
cleaned = remove_invar_regions(content)
|
|
251
|
+
if cleaned:
|
|
252
|
+
file_path.write_text(cleaned + "\n")
|
|
253
|
+
console.print(f"[yellow]Modified[/yellow] {file_name}")
|
|
254
|
+
else:
|
|
255
|
+
file_path.unlink()
|
|
256
|
+
console.print(f"[red]Deleted[/red] {file_name} (empty after cleanup)")
|
|
257
|
+
|
|
258
|
+
# Clean up empty .claude directory if it exists and is empty
|
|
259
|
+
claude_dir = path / ".claude"
|
|
260
|
+
if claude_dir.exists():
|
|
261
|
+
# Check subdirectories
|
|
262
|
+
for subdir in ["skills", "commands", "hooks"]:
|
|
263
|
+
subdir_path = claude_dir / subdir
|
|
264
|
+
if subdir_path.exists() and not any(subdir_path.iterdir()):
|
|
265
|
+
subdir_path.rmdir()
|
|
266
|
+
console.print(f"[dim]Removed empty[/dim] .claude/{subdir}/")
|
|
267
|
+
# Check if .claude itself is empty
|
|
268
|
+
if not any(claude_dir.iterdir()):
|
|
269
|
+
claude_dir.rmdir()
|
|
270
|
+
console.print("[dim]Removed empty[/dim] .claude/")
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def uninstall(
|
|
274
|
+
path: Path = typer.Argument(
|
|
275
|
+
Path(),
|
|
276
|
+
help="Project path",
|
|
277
|
+
exists=True,
|
|
278
|
+
file_okay=False,
|
|
279
|
+
dir_okay=True,
|
|
280
|
+
resolve_path=True,
|
|
281
|
+
),
|
|
282
|
+
dry_run: bool = typer.Option(
|
|
283
|
+
False,
|
|
284
|
+
"--dry-run",
|
|
285
|
+
"-n",
|
|
286
|
+
help="Show what would be removed without removing",
|
|
287
|
+
),
|
|
288
|
+
force: bool = typer.Option(
|
|
289
|
+
False,
|
|
290
|
+
"--force",
|
|
291
|
+
"-f",
|
|
292
|
+
help="Skip confirmation prompt",
|
|
293
|
+
),
|
|
294
|
+
) -> None:
|
|
295
|
+
"""Remove Invar from a project.
|
|
296
|
+
|
|
297
|
+
Safely removes Invar-generated files and configurations while
|
|
298
|
+
preserving user content. Uses marker-based detection.
|
|
299
|
+
|
|
300
|
+
Examples:
|
|
301
|
+
invar uninstall --dry-run # Preview changes
|
|
302
|
+
invar uninstall # Remove with confirmation
|
|
303
|
+
invar uninstall --force # Remove without confirmation
|
|
304
|
+
"""
|
|
305
|
+
# Check if this is an Invar project
|
|
306
|
+
invar_toml = path / "invar.toml"
|
|
307
|
+
invar_md = path / "INVAR.md"
|
|
308
|
+
invar_dir = path / ".invar"
|
|
309
|
+
|
|
310
|
+
if not (invar_toml.exists() or invar_md.exists() or invar_dir.exists()):
|
|
311
|
+
console.print("[yellow]Warning:[/yellow] This doesn't appear to be an Invar project.")
|
|
312
|
+
console.print("No invar.toml, INVAR.md, or .invar/ directory found.")
|
|
313
|
+
raise typer.Exit(1)
|
|
314
|
+
|
|
315
|
+
# Collect targets
|
|
316
|
+
targets = collect_removal_targets(path)
|
|
317
|
+
|
|
318
|
+
# Check if there's anything to do
|
|
319
|
+
if not any([targets["delete_dirs"], targets["delete_files"], targets["modify_files"]]):
|
|
320
|
+
console.print("[green]Nothing to remove.[/green] Project is clean.")
|
|
321
|
+
raise typer.Exit(0)
|
|
322
|
+
|
|
323
|
+
# Show preview
|
|
324
|
+
show_preview(targets)
|
|
325
|
+
|
|
326
|
+
# Dry run exits here
|
|
327
|
+
if dry_run:
|
|
328
|
+
console.print("[dim]Dry run complete. No changes made.[/dim]")
|
|
329
|
+
raise typer.Exit(0)
|
|
330
|
+
|
|
331
|
+
# Confirmation
|
|
332
|
+
if not force:
|
|
333
|
+
confirm = typer.confirm("Proceed with uninstall?", default=False)
|
|
334
|
+
if not confirm:
|
|
335
|
+
console.print("[dim]Cancelled.[/dim]")
|
|
336
|
+
raise typer.Exit(0)
|
|
337
|
+
|
|
338
|
+
# Execute
|
|
339
|
+
execute_removal(path, targets)
|
|
340
|
+
|
|
341
|
+
console.print("\n[green]✓ Invar has been removed from the project.[/green]")
|
invar/shell/templates.py
CHANGED
|
@@ -187,30 +187,16 @@ def copy_skills_directory(dest: Path, console) -> Result[bool, str]:
|
|
|
187
187
|
return Failure(f"Failed to copy skills: {e}")
|
|
188
188
|
|
|
189
189
|
|
|
190
|
-
# Agent configuration
|
|
190
|
+
# Agent configuration - Claude Code only (DX-69: simplified, cursor/aider removed)
|
|
191
191
|
AGENT_CONFIGS = {
|
|
192
192
|
"claude": {
|
|
193
193
|
"file": "CLAUDE.md",
|
|
194
194
|
"template": "CLAUDE.md.template",
|
|
195
|
-
"reference": '> **Protocol:** Follow [INVAR.md](./INVAR.md) for the Invar development methodology.\n',
|
|
196
|
-
"check_pattern": "INVAR.md",
|
|
197
|
-
},
|
|
198
|
-
"cursor": {
|
|
199
|
-
"file": ".cursorrules",
|
|
200
|
-
"template": "cursorrules.template",
|
|
201
|
-
"reference": "Follow the Invar Protocol in INVAR.md.\n\n",
|
|
202
|
-
"check_pattern": "INVAR.md",
|
|
203
|
-
},
|
|
204
|
-
"aider": {
|
|
205
|
-
"file": ".aider.conf.yml",
|
|
206
|
-
"template": "aider.conf.yml.template",
|
|
207
|
-
"reference": "# Follow the Invar Protocol in INVAR.md\nread:\n - INVAR.md\n",
|
|
208
195
|
"check_pattern": "INVAR.md",
|
|
209
196
|
},
|
|
210
197
|
}
|
|
211
198
|
|
|
212
199
|
|
|
213
|
-
# @shell_complexity: Agent config detection across multiple locations
|
|
214
200
|
def detect_agent_configs(path: Path) -> Result[dict[str, str], str]:
|
|
215
201
|
"""
|
|
216
202
|
Detect existing agent configuration files.
|
|
@@ -244,61 +230,6 @@ def detect_agent_configs(path: Path) -> Result[dict[str, str], str]:
|
|
|
244
230
|
return Failure(f"Failed to detect agent configs: {e}")
|
|
245
231
|
|
|
246
232
|
|
|
247
|
-
# @shell_complexity: Reference addition with existing check
|
|
248
|
-
def add_invar_reference(path: Path, agent: str, console) -> Result[bool, str]:
|
|
249
|
-
"""Add Invar reference to an existing agent config file."""
|
|
250
|
-
if agent not in AGENT_CONFIGS:
|
|
251
|
-
return Failure(f"Unknown agent: {agent}")
|
|
252
|
-
|
|
253
|
-
config = AGENT_CONFIGS[agent]
|
|
254
|
-
config_path = path / config["file"]
|
|
255
|
-
|
|
256
|
-
if not config_path.exists():
|
|
257
|
-
return Failure(f"Config file not found: {config['file']}")
|
|
258
|
-
|
|
259
|
-
try:
|
|
260
|
-
content = config_path.read_text()
|
|
261
|
-
if config["check_pattern"] in content:
|
|
262
|
-
return Success(False) # Already configured
|
|
263
|
-
|
|
264
|
-
# Prepend reference
|
|
265
|
-
new_content = config["reference"] + content
|
|
266
|
-
config_path.write_text(new_content)
|
|
267
|
-
console.print(f"[green]Updated[/green] {config['file']} (added Invar reference)")
|
|
268
|
-
return Success(True)
|
|
269
|
-
except OSError as e:
|
|
270
|
-
return Failure(f"Failed to update {config['file']}: {e}")
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
# @shell_complexity: Config creation with template selection
|
|
274
|
-
def create_agent_config(path: Path, agent: str, console) -> Result[bool, str]:
|
|
275
|
-
"""
|
|
276
|
-
Create agent config from template (DX-17).
|
|
277
|
-
|
|
278
|
-
Creates full template file for agents that don't have an existing config.
|
|
279
|
-
"""
|
|
280
|
-
if agent not in AGENT_CONFIGS:
|
|
281
|
-
return Failure(f"Unknown agent: {agent}")
|
|
282
|
-
|
|
283
|
-
config = AGENT_CONFIGS[agent]
|
|
284
|
-
config_path = path / config["file"]
|
|
285
|
-
|
|
286
|
-
if config_path.exists():
|
|
287
|
-
return Success(False) # Already exists
|
|
288
|
-
|
|
289
|
-
# Use template if available
|
|
290
|
-
template_name = config.get("template")
|
|
291
|
-
if template_name:
|
|
292
|
-
result = copy_template(template_name, path, config["file"])
|
|
293
|
-
if isinstance(result, Success) and result.unwrap():
|
|
294
|
-
console.print(f"[green]Created[/green] {config['file']} (Invar workflow enforcement)")
|
|
295
|
-
return Success(True)
|
|
296
|
-
elif isinstance(result, Failure):
|
|
297
|
-
return result
|
|
298
|
-
|
|
299
|
-
return Success(False)
|
|
300
|
-
|
|
301
|
-
|
|
302
233
|
# @shell_complexity: MCP server config with JSON manipulation
|
|
303
234
|
def configure_mcp_server(path: Path, console) -> Result[list[str], str]:
|
|
304
235
|
"""
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: invar-tools
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.7.0
|
|
4
4
|
Summary: AI-native software engineering tools with design-by-contract verification
|
|
5
5
|
Project-URL: Homepage, https://github.com/tefx/invar
|
|
6
6
|
Project-URL: Documentation, https://github.com/tefx/invar#readme
|
|
@@ -59,18 +59,19 @@ invar/shell/pattern_integration.py,sha256=pRcjfq3NvMW_tvQCnaXZnD1k5AVEWK8CYOE2jN
|
|
|
59
59
|
invar/shell/property_tests.py,sha256=N9JreyH5PqR89oF5yLcX7ZAV-Koyg5BKo-J05-GUPsA,9109
|
|
60
60
|
invar/shell/subprocess_env.py,sha256=9oXl3eMEbzLsFEgMHqobEw6oW_wV0qMEP7pklwm58Pw,11453
|
|
61
61
|
invar/shell/template_engine.py,sha256=IzOiGsKVFo0lDUdtg27wMzIJJKToclv151RDZuDnHHo,11027
|
|
62
|
-
invar/shell/templates.py,sha256=
|
|
62
|
+
invar/shell/templates.py,sha256=wVG78wBoVB-SOugfqaKj6xklwSDpEyjeEOGE6EKcS0s,13387
|
|
63
63
|
invar/shell/testing.py,sha256=rTNBH0Okh2qtG9ohSXOz487baQ2gXrWT3s_WECW3HJs,11143
|
|
64
64
|
invar/shell/commands/__init__.py,sha256=MEkKwVyjI9DmkvBpJcuumXo2Pg_FFkfEr-Rr3nrAt7A,284
|
|
65
|
-
invar/shell/commands/guard.py,sha256=
|
|
65
|
+
invar/shell/commands/guard.py,sha256=vDBGOFb9mQ1D8eXrMvQB505GpjO1XLeCLrv2ig9-6dU,21718
|
|
66
66
|
invar/shell/commands/hooks.py,sha256=W-SOnT4VQyUvXwipozkJwgEYfiOJGz7wksrbcdWegUg,2356
|
|
67
|
-
invar/shell/commands/init.py,sha256=
|
|
67
|
+
invar/shell/commands/init.py,sha256=7PhXVjnhsY-1o2kBWRfoUPvSjopB0jRjazLe4P_FyZQ,16339
|
|
68
68
|
invar/shell/commands/merge.py,sha256=nuvKo8m32-OL-SCQlS4SLKmOZxQ3qj-1nGCx1Pgzifw,8183
|
|
69
69
|
invar/shell/commands/mutate.py,sha256=GwemiO6LlbGCBEQsBFnzZuKhF-wIMEl79GAMnKUWc8U,5765
|
|
70
70
|
invar/shell/commands/perception.py,sha256=TyH_HpqyKkmE3-zcU4YyBG8ghwJaSFeRC-OQMVBDTbQ,3837
|
|
71
71
|
invar/shell/commands/sync_self.py,sha256=nmqBry7V2_enKwy2zzHg8UoedZNicLe3yKDhjmBeZ68,3880
|
|
72
72
|
invar/shell/commands/template_sync.py,sha256=wVZ-UvJ1wpN2UBcWMfbei0n46XHYx-zRbMA2oX6FSi4,13723
|
|
73
73
|
invar/shell/commands/test.py,sha256=goMf-ovvzEyWQMheq4YlJ-mwK5-w3lDj0cq0IA_1-_c,4205
|
|
74
|
+
invar/shell/commands/uninstall.py,sha256=f8kGBwkdYycQ5eIdfveIefTw9ZSU6D4OyUuHt8ScUTY,12166
|
|
74
75
|
invar/shell/commands/update.py,sha256=0V5F8vxQ6PHPHPVYDmxdRD7xXeQEFypiJMYpY5ryiek,1349
|
|
75
76
|
invar/shell/prove/__init__.py,sha256=ZqlbmyMFJf6yAle8634jFuPRv8wNvHps8loMlOJyf8A,240
|
|
76
77
|
invar/shell/prove/accept.py,sha256=cnY_6jzU1EBnpLF8-zWUWcXiSXtCwxPsXEYXsSVPG38,3717
|
|
@@ -79,14 +80,12 @@ invar/shell/prove/crosshair.py,sha256=4Z_iIYBlkp-I6FqSYZa89wWB09V4Ouw2PduYhTn6rf
|
|
|
79
80
|
invar/shell/prove/hypothesis.py,sha256=QUclOOUg_VB6wbmHw8O2EPiL5qBOeBRqQeM04AVuLw0,9880
|
|
80
81
|
invar/templates/CLAUDE.md.template,sha256=eaGU3SyRO_NEifw5b26k3srgQH4jyeujjCJ-HbM36_w,4913
|
|
81
82
|
invar/templates/__init__.py,sha256=cb3ht8KPK5oBn5oG6HsTznujmo9WriJ_P--fVxJwycc,45
|
|
82
|
-
invar/templates/aider.conf.yml.template,sha256=4xzSs3BXzFJvwdhnWbmzSY0yCbfx5oxqnV8ZjehqHBg,853
|
|
83
83
|
invar/templates/context.md.template,sha256=FKyI1ghpqcf4wftyv9-auIFHor8Nm8lETN45Ja-L8Og,2386
|
|
84
|
-
invar/templates/cursorrules.template,sha256=N6AiEJRJHGkHm2tswh3PnZ_07ozeyQQI8iEOGK5Aqoc,1023
|
|
85
84
|
invar/templates/manifest.toml,sha256=cEe7yEOOeaLmOF-PrwZXxiPGjHhsSJYkWBKRHDmSbac,4268
|
|
86
|
-
invar/templates/pre-commit-config.yaml.template,sha256=
|
|
85
|
+
invar/templates/pre-commit-config.yaml.template,sha256=ZMqiStLCf9cC4uL2JoF59aYv_G4AV-roGdkj1tWHCBc,1763
|
|
87
86
|
invar/templates/proposal.md.template,sha256=UP7SpQ7gk8jVlHGLQCSQ5c-kCj1DBQEz8M-vEStK77I,1573
|
|
88
|
-
invar/templates/commands/audit.md,sha256=
|
|
89
|
-
invar/templates/commands/guard.md,sha256=
|
|
87
|
+
invar/templates/commands/audit.md,sha256=OrotO8420zTKnlNyAyL1Eos0VIaihzEU4AHdfDv68Oc,4162
|
|
88
|
+
invar/templates/commands/guard.md,sha256=N_C_AXd9kI85W1B0aTEycjiDp_jdaP8eeq8O0FQ_WQ8,1227
|
|
90
89
|
invar/templates/config/CLAUDE.md.jinja,sha256=VbtDWxn3H8qiE9-DV1hlG3DJ-GcBQU4ZiUHbFh6Bxxk,7814
|
|
91
90
|
invar/templates/config/context.md.jinja,sha256=_kJ8erEQNJMLDCKrv4BXWkO6OaGzE-zW9biCf7144aY,3103
|
|
92
91
|
invar/templates/config/pre-commit.yaml.jinja,sha256=Qflmii8hngHciSgfa8mIlg3-E3D4b0xflm0-Q-cWcCc,1752
|
|
@@ -105,10 +104,10 @@ invar/templates/skills/develop/SKILL.md.jinja,sha256=3coPSZGh1-YKN9Xc_xcEkfEP3S0
|
|
|
105
104
|
invar/templates/skills/investigate/SKILL.md.jinja,sha256=bOLdLMH5WUVBYOo4NpsfyvI6xx7I1lCNr_X-8bMe_kg,2744
|
|
106
105
|
invar/templates/skills/propose/SKILL.md.jinja,sha256=_iDLYN6-cfzA8n0_8sv-Dnpm1xq9IIpcDyM10mU2WUA,2420
|
|
107
106
|
invar/templates/skills/review/SKILL.md.jinja,sha256=e7HULz1jjLOlk2LYejQMk2F-cu7dDIwvh6lWNjx3j-Q,14123
|
|
108
|
-
invar_tools-1.
|
|
109
|
-
invar_tools-1.
|
|
110
|
-
invar_tools-1.
|
|
111
|
-
invar_tools-1.
|
|
112
|
-
invar_tools-1.
|
|
113
|
-
invar_tools-1.
|
|
114
|
-
invar_tools-1.
|
|
107
|
+
invar_tools-1.7.0.dist-info/METADATA,sha256=JKBCWf7HkEIrD3m3VC7fHLA7REki-dlk1l0HxsgPFW4,16964
|
|
108
|
+
invar_tools-1.7.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
109
|
+
invar_tools-1.7.0.dist-info/entry_points.txt,sha256=RwH_EhqgtFPsnO6RcrwrAb70Zyfb8Mh6uUtztWnUxGk,102
|
|
110
|
+
invar_tools-1.7.0.dist-info/licenses/LICENSE,sha256=qeFksp4H4kfTgQxPCIu3OdagXyiZcgBlVfsQ6M5oFyk,10767
|
|
111
|
+
invar_tools-1.7.0.dist-info/licenses/LICENSE-GPL,sha256=IvZfC6ZbP7CLjytoHVzvpDZpD-Z3R_qa1GdMdWlWQ6Q,35157
|
|
112
|
+
invar_tools-1.7.0.dist-info/licenses/NOTICE,sha256=joEyMyFhFY8Vd8tTJ-a3SirI0m2Sd0WjzqYt3sdcglc,2561
|
|
113
|
+
invar_tools-1.7.0.dist-info/RECORD,,
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# Invar Protocol Configuration for Aider
|
|
2
|
-
# Follow the Invar Protocol in INVAR.md
|
|
3
|
-
|
|
4
|
-
# Auto-read protocol files at session start
|
|
5
|
-
read:
|
|
6
|
-
- INVAR.md
|
|
7
|
-
- .invar/examples/contracts.py
|
|
8
|
-
- .invar/examples/core_shell.py
|
|
9
|
-
- .invar/context.md
|
|
10
|
-
|
|
11
|
-
# System prompt addition
|
|
12
|
-
system-prompt: |
|
|
13
|
-
Follow the Invar Protocol in INVAR.md.
|
|
14
|
-
|
|
15
|
-
## Check-In
|
|
16
|
-
Your first message MUST display:
|
|
17
|
-
✓ Check-In: [project] | [branch] | [clean/dirty]
|
|
18
|
-
|
|
19
|
-
Read .invar/context.md first. Do NOT run guard/map at Check-In.
|
|
20
|
-
This is your sign-in. No visible check-in = Session not started.
|
|
21
|
-
|
|
22
|
-
## Final
|
|
23
|
-
Your last message MUST display:
|
|
24
|
-
✓ Final: guard PASS | 0 errors, 2 warnings
|
|
25
|
-
|
|
26
|
-
Execute: invar guard
|
|
27
|
-
This is your sign-out. Completes the Check-In/Final pair.
|
|
28
|
-
|
|
29
|
-
## Workflow (USBV)
|
|
30
|
-
Understand → Specify → Build → Validate
|
|
31
|
-
Inspect before Contract. Depth varies naturally.
|
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
# Invar Protocol
|
|
2
|
-
|
|
3
|
-
Follow the Invar Protocol in INVAR.md — includes Check-In, USBV workflow, and Task Completion requirements.
|
|
4
|
-
|
|
5
|
-
## Check-In
|
|
6
|
-
|
|
7
|
-
Your first message MUST display:
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
✓ Check-In: [project] | [branch] | [clean/dirty]
|
|
11
|
-
```
|
|
12
|
-
|
|
13
|
-
Actions:
|
|
14
|
-
1. Read `.invar/context.md` (Key Rules + Current State + Lessons Learned)
|
|
15
|
-
2. Show one-line status
|
|
16
|
-
|
|
17
|
-
**Do NOT execute guard or map at Check-In.**
|
|
18
|
-
Guard is for VALIDATE phase and Final only.
|
|
19
|
-
|
|
20
|
-
This is your sign-in. The user sees it immediately.
|
|
21
|
-
No visible check-in = Session not started.
|
|
22
|
-
|
|
23
|
-
## Final
|
|
24
|
-
|
|
25
|
-
Your last message for an implementation task MUST display:
|
|
26
|
-
|
|
27
|
-
```
|
|
28
|
-
✓ Final: guard PASS | 0 errors, 2 warnings
|
|
29
|
-
```
|
|
30
|
-
|
|
31
|
-
Execute `invar guard` and show this one-line summary.
|
|
32
|
-
|
|
33
|
-
This is your sign-out. Completes the Check-In/Final pair.
|
|
34
|
-
|
|
35
|
-
## Quick Reference
|
|
36
|
-
|
|
37
|
-
- Core (`**/core/**`): @pre/@post contracts, doctests, pure (no I/O)
|
|
38
|
-
- Shell (`**/shell/**`): Result[T, E] return type
|
|
39
|
-
- Workflow: Understand → Specify → Build → Validate (USBV)
|
|
40
|
-
- Inspect before Contract. Depth varies naturally.
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|