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