invar-tools 1.6.0__py3-none-any.whl → 1.7.1__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/core/utils.py +3 -1
- invar/shell/claude_hooks.py +90 -0
- invar/shell/commands/guard.py +2 -0
- invar/shell/commands/init.py +303 -384
- invar/shell/commands/uninstall.py +479 -0
- invar/shell/contract_coverage.py +4 -1
- invar/shell/templates.py +36 -99
- invar/templates/commands/audit.md +6 -0
- invar/templates/commands/guard.md +6 -0
- invar/templates/config/pre-commit.yaml.jinja +2 -0
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.1.dist-info}/METADATA +55 -45
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.1.dist-info}/RECORD +17 -19
- invar/templates/aider.conf.yml.template +0 -31
- invar/templates/cursorrules.template +0 -40
- invar/templates/pre-commit-config.yaml.template +0 -44
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.1.dist-info}/WHEEL +0 -0
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.1.dist-info}/entry_points.txt +0 -0
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.1.dist-info}/licenses/LICENSE +0 -0
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.1.dist-info}/licenses/LICENSE-GPL +0 -0
- {invar_tools-1.6.0.dist-info → invar_tools-1.7.1.dist-info}/licenses/NOTICE +0 -0
invar/shell/commands/init.py
CHANGED
|
@@ -2,162 +2,208 @@
|
|
|
2
2
|
Init command for Invar.
|
|
3
3
|
|
|
4
4
|
Shell module: handles project initialization.
|
|
5
|
-
DX-
|
|
6
|
-
DX-55: Unified idempotent init command with smart merge.
|
|
7
|
-
DX-56: Uses unified template sync engine for file generation.
|
|
8
|
-
DX-57: Added Claude Code hooks installation.
|
|
5
|
+
DX-70: Simplified init with interactive menus and safe merge behavior.
|
|
9
6
|
"""
|
|
10
7
|
|
|
11
8
|
from __future__ import annotations
|
|
12
9
|
|
|
13
|
-
import
|
|
14
|
-
import subprocess
|
|
10
|
+
import sys
|
|
15
11
|
from pathlib import Path
|
|
16
12
|
|
|
17
13
|
import typer
|
|
18
14
|
from returns.result import Failure, Success
|
|
19
15
|
from rich.console import Console
|
|
16
|
+
from rich.panel import Panel
|
|
20
17
|
|
|
21
18
|
from invar.core.sync_helpers import SyncConfig
|
|
22
|
-
from invar.
|
|
23
|
-
from invar.shell.claude_hooks import (
|
|
24
|
-
install_claude_hooks,
|
|
25
|
-
sync_claude_hooks,
|
|
26
|
-
)
|
|
27
|
-
from invar.shell.commands.merge import (
|
|
28
|
-
ProjectState,
|
|
29
|
-
detect_project_state,
|
|
30
|
-
)
|
|
19
|
+
from invar.shell.claude_hooks import install_claude_hooks
|
|
31
20
|
from invar.shell.commands.template_sync import sync_templates
|
|
32
21
|
from invar.shell.mcp_config import (
|
|
33
|
-
detect_available_methods,
|
|
34
22
|
generate_mcp_json,
|
|
35
|
-
get_method_by_name,
|
|
36
23
|
get_recommended_method,
|
|
37
24
|
)
|
|
38
|
-
from invar.shell.template_engine import generate_from_manifest
|
|
39
25
|
from invar.shell.templates import (
|
|
40
26
|
add_config,
|
|
41
|
-
add_invar_reference,
|
|
42
|
-
create_agent_config,
|
|
43
27
|
create_directories,
|
|
44
|
-
detect_agent_configs,
|
|
45
28
|
install_hooks,
|
|
46
29
|
)
|
|
47
30
|
|
|
48
31
|
console = Console()
|
|
49
32
|
|
|
50
33
|
|
|
51
|
-
#
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
)
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
def
|
|
34
|
+
# =============================================================================
|
|
35
|
+
# File Categories (DX-70)
|
|
36
|
+
# =============================================================================
|
|
37
|
+
|
|
38
|
+
FILE_CATEGORIES: dict[str, list[tuple[str, str]]] = {
|
|
39
|
+
"required": [
|
|
40
|
+
("INVAR.md", "Protocol and contract rules"),
|
|
41
|
+
(".invar/", "Config, context, examples"),
|
|
42
|
+
],
|
|
43
|
+
"optional": [
|
|
44
|
+
(".pre-commit-config.yaml", "Verification before commit"),
|
|
45
|
+
("src/core/", "Pure logic directory"),
|
|
46
|
+
("src/shell/", "I/O operations directory"),
|
|
47
|
+
],
|
|
48
|
+
"claude": [
|
|
49
|
+
("CLAUDE.md", "Agent instructions"),
|
|
50
|
+
(".claude/skills/", "Workflow automation"),
|
|
51
|
+
(".claude/commands/", "User commands (/audit, /guard)"),
|
|
52
|
+
(".claude/hooks/", "Tool guidance (+ settings.local.json)"),
|
|
53
|
+
(".mcp.json", "MCP server config"),
|
|
54
|
+
],
|
|
55
|
+
"generic": [
|
|
56
|
+
("AGENT.md", "Universal agent instructions"),
|
|
57
|
+
],
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
AGENT_CONFIGS: dict[str, dict[str, str]] = {
|
|
61
|
+
"claude": {"name": "Claude Code", "category": "claude"},
|
|
62
|
+
"generic": {"name": "Other (AGENT.md)", "category": "generic"},
|
|
63
|
+
# Future: "cursor", "windsurf", etc.
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# =============================================================================
|
|
68
|
+
# Interactive Prompts (DX-70)
|
|
69
|
+
# =============================================================================
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _is_interactive() -> bool:
|
|
73
|
+
"""Check if running in an interactive terminal."""
|
|
74
|
+
return sys.stdin.isatty() and sys.stdout.isatty()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
# @shell_orchestration: Style configuration for questionary UI library
|
|
78
|
+
def _get_prompt_style():
|
|
79
|
+
"""Get custom style for questionary prompts.
|
|
80
|
+
|
|
81
|
+
Simple design:
|
|
82
|
+
- Pointer (») indicates current row
|
|
83
|
+
- Checkbox (●/○) indicates selected state
|
|
84
|
+
- All text in default color, no reverse
|
|
90
85
|
"""
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
86
|
+
from questionary import Style
|
|
87
|
+
|
|
88
|
+
return Style([
|
|
89
|
+
("pointer", "fg:cyan bold"), # Pointer: cyan bold
|
|
90
|
+
("highlighted", "noreverse"), # Current row: no reverse
|
|
91
|
+
("selected", "noreverse"), # Selected items: no reverse
|
|
92
|
+
("text", "noreverse"), # Normal text: no reverse
|
|
93
|
+
])
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
# @shell_complexity: Interactive prompt with cursor selection
|
|
97
|
+
def _prompt_agent_selection() -> list[str]:
|
|
98
|
+
"""Prompt user to select code agent using cursor navigation."""
|
|
99
|
+
import questionary
|
|
100
|
+
|
|
101
|
+
console.print("\n[bold]Select code agent:[/bold]")
|
|
102
|
+
console.print("[dim]Use arrow keys to move, enter to select[/dim]\n")
|
|
103
|
+
|
|
104
|
+
choices = [
|
|
105
|
+
questionary.Choice("Claude Code (recommended)", value="claude"),
|
|
106
|
+
questionary.Choice("Other (AGENT.md)", value="generic"),
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
selected = questionary.select(
|
|
110
|
+
"",
|
|
111
|
+
choices=choices,
|
|
112
|
+
instruction="",
|
|
113
|
+
style=_get_prompt_style(),
|
|
114
|
+
).ask()
|
|
115
|
+
|
|
116
|
+
# Handle Ctrl+C
|
|
117
|
+
if not selected:
|
|
118
|
+
return ["claude"] # Default to Claude Code
|
|
119
|
+
return [selected]
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
# @shell_complexity: Interactive file selection with cursor navigation
|
|
123
|
+
def _prompt_file_selection(agents: list[str]) -> dict[str, bool]:
|
|
124
|
+
"""Prompt user to select optional files using cursor navigation."""
|
|
125
|
+
import questionary
|
|
126
|
+
|
|
127
|
+
# Build available files
|
|
128
|
+
available: dict[str, list[tuple[str, str]]] = {
|
|
129
|
+
"optional": FILE_CATEGORIES["optional"],
|
|
130
|
+
}
|
|
131
|
+
for agent in agents:
|
|
132
|
+
config = AGENT_CONFIGS.get(agent)
|
|
133
|
+
if config:
|
|
134
|
+
category = config["category"]
|
|
135
|
+
available[category] = FILE_CATEGORIES.get(category, [])
|
|
136
|
+
|
|
137
|
+
# Show header
|
|
138
|
+
console.print("\n[bold]File Selection:[/bold]")
|
|
139
|
+
console.print("[dim]Existing files will be MERGED (your content preserved).[/dim]\n")
|
|
140
|
+
|
|
141
|
+
# Required files (always installed)
|
|
142
|
+
console.print("[bold]Required (always installed):[/bold]")
|
|
143
|
+
for file, desc in FILE_CATEGORIES["required"]:
|
|
144
|
+
console.print(f" [green]✓[/green] {file:30} {desc}")
|
|
145
|
+
|
|
146
|
+
console.print()
|
|
147
|
+
console.print("[dim]Use arrow keys to move, space to toggle, enter to confirm[/dim]\n")
|
|
148
|
+
|
|
149
|
+
# Build choices with categories as separators
|
|
150
|
+
choices: list[questionary.Choice | questionary.Separator] = []
|
|
151
|
+
file_list: list[str] = []
|
|
152
|
+
|
|
153
|
+
for category, files in available.items():
|
|
154
|
+
if category == "required":
|
|
155
|
+
continue
|
|
156
|
+
category_name = category.capitalize()
|
|
157
|
+
if category == "claude":
|
|
158
|
+
category_name = "Claude Code"
|
|
159
|
+
choices.append(questionary.Separator(f"── {category_name} ──"))
|
|
160
|
+
for file, desc in files:
|
|
161
|
+
choices.append(
|
|
162
|
+
questionary.Choice(f"{file:28} {desc}", value=file, checked=True)
|
|
163
|
+
)
|
|
164
|
+
file_list.append(file)
|
|
165
|
+
|
|
166
|
+
selected = questionary.checkbox(
|
|
167
|
+
"Select files to install:",
|
|
168
|
+
choices=choices,
|
|
169
|
+
instruction="",
|
|
170
|
+
style=_get_prompt_style(),
|
|
171
|
+
).ask()
|
|
172
|
+
|
|
173
|
+
# Handle Ctrl+C or empty result
|
|
174
|
+
if selected is None:
|
|
175
|
+
return dict.fromkeys(file_list, True) # Default: all selected
|
|
176
|
+
|
|
177
|
+
# Build result dict
|
|
178
|
+
return {f: f in selected for f in file_list}
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def _show_execution_output(
|
|
182
|
+
created: list[str],
|
|
183
|
+
merged: list[str],
|
|
184
|
+
skipped: list[str],
|
|
185
|
+
) -> None:
|
|
186
|
+
"""Display execution results."""
|
|
187
|
+
console.print()
|
|
188
|
+
for file in created:
|
|
189
|
+
console.print(f" [green]✓[/green] {file:30} [dim]created[/dim]")
|
|
190
|
+
for file in merged:
|
|
191
|
+
console.print(f" [cyan]↻[/cyan] {file:30} [dim]merged[/dim]")
|
|
192
|
+
for file in skipped:
|
|
193
|
+
console.print(f" [dim]○[/dim] {file:30} [dim]skipped[/dim]")
|
|
131
194
|
|
|
132
|
-
Execute `invar guard` and show this one-line summary.
|
|
133
|
-
"""
|
|
134
195
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
196
|
+
# =============================================================================
|
|
197
|
+
# MCP Configuration
|
|
198
|
+
# =============================================================================
|
|
138
199
|
|
|
139
200
|
|
|
140
|
-
# @shell_complexity: MCP config with
|
|
141
|
-
def
|
|
142
|
-
|
|
143
|
-
) -> None:
|
|
144
|
-
"""Configure MCP server with specified or detected method."""
|
|
201
|
+
# @shell_complexity: MCP config merge with existing file handling
|
|
202
|
+
def _configure_mcp(path: Path) -> bool:
|
|
203
|
+
"""Configure MCP server with recommended method."""
|
|
145
204
|
import json
|
|
146
205
|
|
|
147
|
-
|
|
148
|
-
if mcp_method:
|
|
149
|
-
config = get_method_by_name(mcp_method)
|
|
150
|
-
if config is None:
|
|
151
|
-
console.print(f"[yellow]Warning:[/yellow] Method '{mcp_method}' not available")
|
|
152
|
-
config = get_recommended_method()
|
|
153
|
-
console.print(f"[dim]Using fallback: {config.description}[/dim]")
|
|
154
|
-
else:
|
|
155
|
-
config = get_recommended_method()
|
|
156
|
-
|
|
157
|
-
console.print("\n[bold]Configuring MCP server...[/bold]")
|
|
158
|
-
console.print(f" Method: {config.description}")
|
|
159
|
-
|
|
160
|
-
# Generate and write .mcp.json
|
|
206
|
+
config = get_recommended_method()
|
|
161
207
|
mcp_json_path = path / ".mcp.json"
|
|
162
208
|
mcp_content = generate_mcp_json(config)
|
|
163
209
|
|
|
@@ -165,327 +211,200 @@ def configure_mcp_with_method(
|
|
|
165
211
|
try:
|
|
166
212
|
existing = json.loads(mcp_json_path.read_text())
|
|
167
213
|
if "mcpServers" in existing and "invar" in existing.get("mcpServers", {}):
|
|
168
|
-
|
|
169
|
-
return
|
|
214
|
+
return False # Already configured
|
|
170
215
|
# Add invar to existing config
|
|
171
216
|
if "mcpServers" not in existing:
|
|
172
217
|
existing["mcpServers"] = {}
|
|
173
218
|
existing["mcpServers"]["invar"] = mcp_content["mcpServers"]["invar"]
|
|
174
219
|
mcp_json_path.write_text(json.dumps(existing, indent=2))
|
|
175
|
-
|
|
220
|
+
return True
|
|
176
221
|
except (json.JSONDecodeError, OSError):
|
|
177
|
-
|
|
222
|
+
return False
|
|
178
223
|
else:
|
|
179
224
|
mcp_json_path.write_text(json.dumps(mcp_content, indent=2))
|
|
180
|
-
|
|
225
|
+
return True
|
|
181
226
|
|
|
182
227
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
console.print("\n[bold]Available MCP methods:[/bold]")
|
|
187
|
-
for i, method in enumerate(methods):
|
|
188
|
-
marker = "[green]→[/green]" if i == 0 else " "
|
|
189
|
-
console.print(f" {marker} {method.method.value}: {method.description}")
|
|
228
|
+
# =============================================================================
|
|
229
|
+
# Main Init Command (DX-70)
|
|
230
|
+
# =============================================================================
|
|
190
231
|
|
|
191
232
|
|
|
192
|
-
# @shell_complexity:
|
|
233
|
+
# @shell_complexity: Main CLI entry point with interactive flow and file generation
|
|
193
234
|
def init(
|
|
194
|
-
path: Path = typer.Argument(
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
),
|
|
198
|
-
mcp_method: str = typer.Option(
|
|
199
|
-
None,
|
|
200
|
-
"--mcp-method",
|
|
201
|
-
help="MCP execution method: uvx (recommended), command, or python",
|
|
202
|
-
),
|
|
203
|
-
dirs: bool = typer.Option(
|
|
204
|
-
None, "--dirs/--no-dirs", help="Create src/core and src/shell directories"
|
|
205
|
-
),
|
|
206
|
-
hooks: bool = typer.Option(
|
|
207
|
-
True, "--hooks/--no-hooks", help="Install pre-commit hooks (default: ON)"
|
|
208
|
-
),
|
|
209
|
-
claude_hooks: bool = typer.Option(
|
|
210
|
-
None, "--claude-hooks/--no-claude-hooks",
|
|
211
|
-
help="Install Claude Code hooks (default: ON when --claude, DX-57)"
|
|
212
|
-
),
|
|
213
|
-
skills: bool = typer.Option(
|
|
214
|
-
True, "--skills/--no-skills", help="Create .claude/skills/ (default: ON, use --no-skills for Cursor)"
|
|
235
|
+
path: Path = typer.Argument(
|
|
236
|
+
Path(),
|
|
237
|
+
help="Project root directory (default: current directory)",
|
|
215
238
|
),
|
|
216
|
-
|
|
217
|
-
False,
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
False, "--check", help="Preview changes without applying (DX-55)"
|
|
221
|
-
),
|
|
222
|
-
force: bool = typer.Option(
|
|
223
|
-
False, "--force", help="Update even if already current (DX-55)"
|
|
239
|
+
claude: bool = typer.Option(
|
|
240
|
+
False,
|
|
241
|
+
"--claude",
|
|
242
|
+
help="Auto-select Claude Code, skip all prompts",
|
|
224
243
|
),
|
|
225
|
-
|
|
226
|
-
False,
|
|
244
|
+
preview: bool = typer.Option(
|
|
245
|
+
False,
|
|
246
|
+
"--preview",
|
|
247
|
+
help="Show what would be done (dry run)",
|
|
227
248
|
),
|
|
228
249
|
) -> None:
|
|
229
250
|
"""
|
|
230
|
-
Initialize or update Invar configuration
|
|
251
|
+
Initialize or update Invar configuration.
|
|
231
252
|
|
|
232
|
-
DX-
|
|
233
|
-
It detects current state and does the right thing:
|
|
253
|
+
DX-70: Simplified init with interactive selection and safe merge.
|
|
234
254
|
|
|
235
255
|
\b
|
|
236
|
-
-
|
|
237
|
-
-
|
|
238
|
-
-
|
|
239
|
-
|
|
240
|
-
|
|
256
|
+
This command is safe - it always MERGES with existing files:
|
|
257
|
+
- File doesn't exist → Create
|
|
258
|
+
- File exists → Merge (update invar regions, preserve your content)
|
|
259
|
+
- Never overwrites user content
|
|
260
|
+
- Never deletes files
|
|
241
261
|
|
|
242
262
|
\b
|
|
243
|
-
|
|
244
|
-
- Otherwise: creates invar.toml
|
|
245
|
-
|
|
246
|
-
Use --check to preview changes without applying.
|
|
247
|
-
Use --force to update even if already current.
|
|
248
|
-
Use --reset to discard all user content (dangerous).
|
|
249
|
-
Use --claude to run 'claude /init' first.
|
|
250
|
-
Use --mcp-method to specify MCP execution method (uvx, command, python).
|
|
251
|
-
Use --dirs to always create directories, --no-dirs to skip.
|
|
252
|
-
Use --no-hooks to skip pre-commit hooks installation.
|
|
253
|
-
Use --no-claude-hooks to skip Claude Code hooks (DX-57).
|
|
254
|
-
Use --no-skills to skip .claude/skills/ creation (for Cursor users).
|
|
255
|
-
Use --yes to accept defaults without prompting.
|
|
263
|
+
For full reset, use: invar uninstall && invar init
|
|
256
264
|
"""
|
|
257
265
|
from invar import __version__
|
|
258
266
|
|
|
259
|
-
#
|
|
260
|
-
|
|
267
|
+
# Resolve path
|
|
268
|
+
if path == Path():
|
|
269
|
+
path = Path.cwd()
|
|
270
|
+
path = path.resolve()
|
|
271
|
+
|
|
272
|
+
# Header
|
|
273
|
+
if claude:
|
|
274
|
+
console.print(f"\n[bold]Invar v{__version__} - Quick Setup (Claude Code)[/bold]")
|
|
275
|
+
else:
|
|
276
|
+
console.print(f"\n[bold]Invar v{__version__} - Project Setup[/bold]")
|
|
277
|
+
console.print("=" * 45)
|
|
278
|
+
console.print("[dim]Existing files will be MERGED (your content preserved).[/dim]")
|
|
279
|
+
|
|
280
|
+
# Determine agents and files
|
|
281
|
+
if claude:
|
|
282
|
+
# Quick mode: use defaults
|
|
283
|
+
agents = ["claude"]
|
|
284
|
+
selected_files: dict[str, bool] = {}
|
|
285
|
+
for category in ["optional", "claude"]:
|
|
286
|
+
for file, _ in FILE_CATEGORIES.get(category, []):
|
|
287
|
+
selected_files[file] = True
|
|
288
|
+
else:
|
|
289
|
+
# Interactive mode
|
|
290
|
+
if not _is_interactive():
|
|
291
|
+
console.print("[yellow]Non-interactive terminal detected. Use --claude for quick setup.[/yellow]")
|
|
292
|
+
raise typer.Exit(1)
|
|
293
|
+
|
|
294
|
+
agents = _prompt_agent_selection()
|
|
295
|
+
selected_files = _prompt_file_selection(agents)
|
|
296
|
+
|
|
297
|
+
# Preview mode
|
|
298
|
+
if preview:
|
|
299
|
+
console.print("\n[bold]Preview - Would create/update:[/bold]")
|
|
300
|
+
console.print("\n[bold]Required:[/bold]")
|
|
301
|
+
for file, desc in FILE_CATEGORIES["required"]:
|
|
302
|
+
console.print(f" [green]✓[/green] {file:30} {desc}")
|
|
303
|
+
|
|
304
|
+
console.print("\n[bold]Selected:[/bold]")
|
|
305
|
+
for file, selected in selected_files.items():
|
|
306
|
+
if selected:
|
|
307
|
+
console.print(f" [green]✓[/green] {file}")
|
|
308
|
+
else:
|
|
309
|
+
console.print(f" [dim]○[/dim] {file} [dim](skipped)[/dim]")
|
|
261
310
|
|
|
262
|
-
|
|
263
|
-
if check:
|
|
264
|
-
_show_check_preview(state, path, __version__)
|
|
311
|
+
console.print("\n[dim]Run without --preview to apply.[/dim]")
|
|
265
312
|
return
|
|
266
313
|
|
|
267
|
-
#
|
|
268
|
-
|
|
269
|
-
if not yes and not typer.confirm(
|
|
270
|
-
"[red]This will DELETE all user customizations. Continue?[/red]",
|
|
271
|
-
default=False,
|
|
272
|
-
):
|
|
273
|
-
console.print("[yellow]Cancelled[/yellow]")
|
|
274
|
-
return
|
|
275
|
-
# Fall through to full init with reset flag
|
|
276
|
-
state = ProjectState(
|
|
277
|
-
initialized=False,
|
|
278
|
-
claude_md_state=ClaudeMdState(state="absent"),
|
|
279
|
-
version="",
|
|
280
|
-
needs_update=True,
|
|
281
|
-
)
|
|
314
|
+
# Execute
|
|
315
|
+
console.print("\n[bold]Creating files...[/bold]")
|
|
282
316
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
if action == "none" and not force:
|
|
287
|
-
# DX-55: Check for missing required files before declaring "no changes needed"
|
|
288
|
-
missing_files = []
|
|
289
|
-
if skills:
|
|
290
|
-
skill_files = [
|
|
291
|
-
".claude/skills/develop/SKILL.md",
|
|
292
|
-
".claude/skills/investigate/SKILL.md",
|
|
293
|
-
".claude/skills/propose/SKILL.md",
|
|
294
|
-
".claude/skills/review/SKILL.md",
|
|
295
|
-
]
|
|
296
|
-
for skill_file in skill_files:
|
|
297
|
-
if not (path / skill_file).exists():
|
|
298
|
-
missing_files.append(skill_file)
|
|
299
|
-
|
|
300
|
-
if not missing_files:
|
|
301
|
-
console.print(f"[green]✓[/green] Invar v{__version__} configured (no changes needed)")
|
|
302
|
-
console.print("[dim]Use --force to refresh managed regions[/dim]")
|
|
303
|
-
return
|
|
304
|
-
else:
|
|
305
|
-
# Recreate missing files
|
|
306
|
-
console.print(f"[yellow]Detected:[/yellow] {len(missing_files)} missing file(s)")
|
|
307
|
-
result = generate_from_manifest(path, syntax="cli", files_to_generate=missing_files)
|
|
308
|
-
if isinstance(result, Success):
|
|
309
|
-
for generated_file in result.unwrap():
|
|
310
|
-
console.print(f"[green]Restored[/green] {generated_file}")
|
|
311
|
-
console.print(f"[green]✓[/green] Invar v{__version__} configured")
|
|
312
|
-
return
|
|
313
|
-
|
|
314
|
-
# DX-21B: Run claude /init if requested (before sync)
|
|
315
|
-
if claude:
|
|
316
|
-
claude_success = run_claude_init(path)
|
|
317
|
-
if claude_success:
|
|
318
|
-
# Append Invar reference to generated CLAUDE.md
|
|
319
|
-
append_invar_reference_to_claude_md(path)
|
|
317
|
+
created: list[str] = []
|
|
318
|
+
merged: list[str] = []
|
|
319
|
+
skipped: list[str] = []
|
|
320
320
|
|
|
321
|
+
# Add config file (.invar/config.toml or pyproject.toml)
|
|
321
322
|
config_result = add_config(path, console)
|
|
322
323
|
if isinstance(config_result, Failure):
|
|
323
324
|
console.print(f"[red]Error:[/red] {config_result.failure()}")
|
|
324
325
|
raise typer.Exit(1)
|
|
325
|
-
config_added = config_result.unwrap()
|
|
326
326
|
|
|
327
|
-
#
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
has_project_additions = (path / ".invar" / "project-additions.md").exists()
|
|
327
|
+
# Ensure .invar directory exists
|
|
328
|
+
invar_dir = path / ".invar"
|
|
329
|
+
if not invar_dir.exists():
|
|
330
|
+
invar_dir.mkdir()
|
|
332
331
|
|
|
333
|
-
# Build skip patterns
|
|
332
|
+
# Build skip patterns based on selection
|
|
334
333
|
skip_patterns: list[str] = []
|
|
335
|
-
if not skills:
|
|
334
|
+
if not selected_files.get(".claude/skills/", True):
|
|
336
335
|
skip_patterns.append(".claude/skills/*")
|
|
336
|
+
if not selected_files.get(".claude/commands/", True):
|
|
337
|
+
skip_patterns.append(".claude/commands/*")
|
|
338
|
+
if not selected_files.get(".pre-commit-config.yaml", True):
|
|
339
|
+
skip_patterns.append(".pre-commit-config.yaml")
|
|
337
340
|
|
|
341
|
+
# Run template sync
|
|
338
342
|
sync_config = SyncConfig(
|
|
339
343
|
syntax="cli",
|
|
340
|
-
inject_project_additions=
|
|
341
|
-
force=
|
|
342
|
-
check=False,
|
|
343
|
-
reset=
|
|
344
|
+
inject_project_additions=(path / ".invar" / "project-additions.md").exists(),
|
|
345
|
+
force=False,
|
|
346
|
+
check=False,
|
|
347
|
+
reset=False,
|
|
344
348
|
skip_patterns=skip_patterns,
|
|
345
349
|
)
|
|
346
350
|
|
|
347
|
-
# DX-56: Run unified sync engine (handles DX-55 state detection internally)
|
|
348
351
|
result = sync_templates(path, sync_config)
|
|
349
|
-
if isinstance(result,
|
|
350
|
-
console.print(f"[yellow]Warning:[/yellow] {result.failure()}")
|
|
351
|
-
else:
|
|
352
|
+
if isinstance(result, Success):
|
|
352
353
|
report = result.unwrap()
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
for file in report.updated:
|
|
356
|
-
console.print(f"[cyan]Updated[/cyan] {file}")
|
|
357
|
-
for error in report.errors:
|
|
358
|
-
console.print(f"[yellow]Warning:[/yellow] {error}")
|
|
359
|
-
|
|
360
|
-
# Create .invar directory structure (for proposals template - not in manifest)
|
|
361
|
-
invar_dir = path / ".invar"
|
|
362
|
-
if not invar_dir.exists():
|
|
363
|
-
invar_dir.mkdir()
|
|
354
|
+
created.extend(report.created)
|
|
355
|
+
merged.extend(report.updated)
|
|
364
356
|
|
|
365
|
-
# Create proposals directory
|
|
357
|
+
# Create proposals directory
|
|
366
358
|
proposals_dir = invar_dir / "proposals"
|
|
367
359
|
if not proposals_dir.exists():
|
|
368
360
|
proposals_dir.mkdir()
|
|
369
361
|
from invar.shell.templates import copy_template
|
|
370
|
-
result = copy_template("proposal.md.template", proposals_dir, "TEMPLATE.md")
|
|
371
|
-
if isinstance(result, Success) and result.unwrap():
|
|
372
|
-
console.print("[green]Created[/green] .invar/proposals/TEMPLATE.md")
|
|
373
|
-
|
|
374
|
-
# Agent detection and configuration (DX-11)
|
|
375
|
-
console.print("\n[bold]Checking for agent configurations...[/bold]")
|
|
376
|
-
agent_result = detect_agent_configs(path)
|
|
377
|
-
if isinstance(agent_result, Failure):
|
|
378
|
-
console.print(f"[yellow]Warning:[/yellow] {agent_result.failure()}")
|
|
379
|
-
agent_status: dict[str, str] = {}
|
|
380
|
-
else:
|
|
381
|
-
agent_status = agent_result.unwrap()
|
|
382
|
-
|
|
383
|
-
# Handle agent configs (DX-11, DX-17)
|
|
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)
|
|
396
362
|
|
|
397
|
-
|
|
398
|
-
|
|
363
|
+
copy_template("proposal.md.template", proposals_dir, "TEMPLATE.md")
|
|
364
|
+
|
|
365
|
+
# Configure MCP if Claude selected
|
|
366
|
+
if "claude" in agents and selected_files.get(".mcp.json", True):
|
|
367
|
+
if _configure_mcp(path):
|
|
368
|
+
created.append(".mcp.json")
|
|
369
|
+
|
|
370
|
+
# Create directories if selected
|
|
371
|
+
if selected_files.get("src/core/", True):
|
|
372
|
+
create_directories(path, console)
|
|
399
373
|
|
|
400
|
-
#
|
|
401
|
-
if
|
|
402
|
-
|
|
374
|
+
# Install pre-commit hooks if selected
|
|
375
|
+
if selected_files.get(".pre-commit-config.yaml", True):
|
|
376
|
+
install_hooks(path, console)
|
|
377
|
+
|
|
378
|
+
# Install Claude hooks if selected
|
|
379
|
+
if "claude" in agents and selected_files.get(".claude/hooks/", True):
|
|
380
|
+
install_claude_hooks(path, console)
|
|
403
381
|
|
|
404
382
|
# Create MCP setup guide
|
|
405
383
|
mcp_setup = invar_dir / "mcp-setup.md"
|
|
406
384
|
if not mcp_setup.exists():
|
|
407
385
|
from invar.shell.templates import _MCP_SETUP_TEMPLATE
|
|
408
|
-
mcp_setup.write_text(_MCP_SETUP_TEMPLATE)
|
|
409
|
-
console.print("[green]Created[/green] .invar/mcp-setup.md (setup guide)")
|
|
410
386
|
|
|
411
|
-
|
|
412
|
-
if dirs is not False:
|
|
413
|
-
create_directories(path, console)
|
|
387
|
+
mcp_setup.write_text(_MCP_SETUP_TEMPLATE)
|
|
414
388
|
|
|
415
|
-
#
|
|
416
|
-
|
|
417
|
-
|
|
389
|
+
# Track skipped files
|
|
390
|
+
for file, selected in selected_files.items():
|
|
391
|
+
if not selected:
|
|
392
|
+
skipped.append(file)
|
|
418
393
|
|
|
419
|
-
#
|
|
420
|
-
|
|
421
|
-
should_install_claude_hooks = (
|
|
422
|
-
claude_hooks is True # Explicitly requested
|
|
423
|
-
or (claude_hooks is None and claude) # Default ON when --claude
|
|
424
|
-
)
|
|
425
|
-
should_skip_claude_hooks = claude_hooks is False
|
|
394
|
+
# Show results
|
|
395
|
+
_show_execution_output(created, merged, skipped)
|
|
426
396
|
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
install_claude_hooks(path, console)
|
|
430
|
-
elif not should_skip_claude_hooks:
|
|
431
|
-
# Check if hooks already installed and need sync
|
|
432
|
-
claude_hooks_dir = path / ".claude" / "hooks"
|
|
433
|
-
if (claude_hooks_dir / "invar.UserPromptSubmit.sh").exists():
|
|
434
|
-
# Sync existing hooks (idempotent update)
|
|
435
|
-
sync_claude_hooks(path, console)
|
|
436
|
-
|
|
437
|
-
if not config_added and not (path / "INVAR.md").exists():
|
|
438
|
-
console.print("[yellow]Invar already configured.[/yellow]")
|
|
439
|
-
|
|
440
|
-
# DX-55: Summary based on action taken
|
|
441
|
-
if action == "full_init":
|
|
442
|
-
console.print(f"\n[bold green]✓ Initialized Invar v{__version__}[/bold green]")
|
|
443
|
-
console.print("[dim]Note: If you run 'claude /init' later, just run 'invar init' again.[/dim]")
|
|
444
|
-
elif action == "recover":
|
|
445
|
-
console.print(f"\n[bold green]✓ Recovered Invar v{__version__}[/bold green]")
|
|
446
|
-
console.print("[dim]Review the merged content in CLAUDE.md[/dim]")
|
|
447
|
-
elif action == "update" or force:
|
|
448
|
-
console.print(f"\n[bold green]✓ Updated Invar v{__version__}[/bold green]")
|
|
449
|
-
console.print("[dim]Refreshed managed regions, preserved user content[/dim]")
|
|
450
|
-
else:
|
|
451
|
-
console.print("\n[bold green]Invar initialized successfully![/bold green]")
|
|
397
|
+
# Completion message
|
|
398
|
+
console.print(f"\n[bold green]✓ Initialized Invar v{__version__}[/bold green]")
|
|
452
399
|
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
console.print(f"Current version: [cyan]{state.version or 'N/A'}[/cyan]")
|
|
465
|
-
console.print(f"Needs update: [cyan]{state.needs_update}[/cyan]")
|
|
466
|
-
console.print(f"Action: [cyan]{state.action}[/cyan]\n")
|
|
467
|
-
|
|
468
|
-
match state.action:
|
|
469
|
-
case "none":
|
|
470
|
-
console.print("[green]No changes needed[/green]")
|
|
471
|
-
case "full_init":
|
|
472
|
-
console.print("Would create:")
|
|
473
|
-
console.print(" - INVAR.md")
|
|
474
|
-
console.print(" - CLAUDE.md")
|
|
475
|
-
console.print(" - .invar/context.md")
|
|
476
|
-
console.print(" - .claude/skills/")
|
|
477
|
-
console.print(" - .claude/hooks/ (DX-57, with --claude)")
|
|
478
|
-
console.print(" - .pre-commit-config.yaml")
|
|
479
|
-
case "update":
|
|
480
|
-
console.print("Would update:")
|
|
481
|
-
console.print(f" - CLAUDE.md (managed section v{state.version} → v{version})")
|
|
482
|
-
console.print(" - .claude/skills/* (refresh)")
|
|
483
|
-
console.print(" - .claude/hooks/* (refresh, if installed)")
|
|
484
|
-
case "recover":
|
|
485
|
-
console.print("[yellow]Would recover:[/yellow]")
|
|
486
|
-
console.print(" - CLAUDE.md (restore regions, preserve content)")
|
|
487
|
-
case "create":
|
|
488
|
-
console.print("Would create:")
|
|
489
|
-
console.print(" - CLAUDE.md")
|
|
490
|
-
|
|
491
|
-
console.print("\n[dim]Run 'invar init' to apply.[/dim]")
|
|
400
|
+
# Show tip for Claude users
|
|
401
|
+
if "claude" in agents:
|
|
402
|
+
console.print()
|
|
403
|
+
console.print(
|
|
404
|
+
Panel(
|
|
405
|
+
"[dim]If you run [bold]claude /init[/bold] afterward, "
|
|
406
|
+
"run [bold]invar init[/bold] again to restore protocol.[/dim]",
|
|
407
|
+
title="📌 Tip",
|
|
408
|
+
border_style="dim",
|
|
409
|
+
)
|
|
410
|
+
)
|