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.
@@ -2,162 +2,208 @@
2
2
  Init command for Invar.
3
3
 
4
4
  Shell module: handles project initialization.
5
- DX-21B: Added --claude flag for Claude Code integration.
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 shutil
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.core.template_parser import ClaudeMdState
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
- # @shell_complexity: Claude init with config file detection
52
- def run_claude_init(path: Path) -> bool:
53
- """
54
- Run 'claude /init' to generate intelligent CLAUDE.md.
55
-
56
- Returns True if successful, False otherwise.
57
- """
58
- if not shutil.which("claude"):
59
- console.print(
60
- "[yellow]Warning:[/yellow] 'claude' CLI not found. "
61
- "Install Claude Code: https://claude.ai/code"
62
- )
63
- console.print("[dim]Skipping claude /init, will create basic CLAUDE.md[/dim]")
64
- return False
65
-
66
- console.print("\n[bold]Running claude /init...[/bold]")
67
- try:
68
- result = subprocess.run(
69
- ["claude", "/init"],
70
- cwd=path,
71
- capture_output=True,
72
- text=True,
73
- timeout=120,
74
- )
75
- if result.returncode == 0:
76
- console.print("[green]claude /init completed successfully[/green]")
77
- return True
78
- else:
79
- console.print(f"[yellow]Warning:[/yellow] claude /init failed: {result.stderr}")
80
- return False
81
- except subprocess.TimeoutExpired:
82
- console.print("[yellow]Warning:[/yellow] claude /init timed out")
83
- return False
84
- except Exception as e:
85
- console.print(f"[yellow]Warning:[/yellow] claude /init error: {e}")
86
- return False
87
-
88
-
89
- def append_invar_reference_to_claude_md(path: Path) -> bool:
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
- 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
- ```
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
- claude_md.write_text(content + invar_reference)
136
- console.print("[green]Updated[/green] CLAUDE.md (added Invar reference)")
137
- return True
196
+ # =============================================================================
197
+ # MCP Configuration
198
+ # =============================================================================
138
199
 
139
200
 
140
- # @shell_complexity: MCP config with method selection and validation
141
- def configure_mcp_with_method(
142
- path: Path, mcp_method: str | None
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
- # Determine method to use
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
- console.print("[dim]Skipped[/dim] .mcp.json (invar already configured)")
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
- console.print("[green]Updated[/green] .mcp.json (added invar)")
220
+ return True
176
221
  except (json.JSONDecodeError, OSError):
177
- console.print("[yellow]Warning:[/yellow] .mcp.json exists but couldn't update")
222
+ return False
178
223
  else:
179
224
  mcp_json_path.write_text(json.dumps(mcp_content, indent=2))
180
- console.print("[green]Created[/green] .mcp.json")
225
+ return True
181
226
 
182
227
 
183
- def show_available_mcp_methods() -> None:
184
- """Display available MCP execution methods."""
185
- methods = detect_available_methods()
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: Project init with config detection and template setup
233
+ # @shell_complexity: Main CLI entry point with interactive flow and file generation
193
234
  def init(
194
- path: Path = typer.Argument(Path(), help="Project root directory"),
195
- claude: bool = typer.Option(
196
- False, "--claude", help="Run 'claude /init' and integrate with Claude Code"
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
- yes: bool = typer.Option(
217
- False, "--yes", "-y", help="Accept defaults without prompting"
218
- ),
219
- check: bool = typer.Option(
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
- reset: bool = typer.Option(
226
- False, "--reset", help="Dangerous: discard all user content (DX-55)"
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 (idempotent).
251
+ Initialize or update Invar configuration.
231
252
 
232
- DX-55: This command is idempotent - safe to run multiple times.
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
- - New project: Full setup
237
- - Existing project: Update managed regions, preserve user content
238
- - Corrupted/overwritten: Smart recovery with content preservation
239
-
240
- Works with or without pyproject.toml:
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
- - If pyproject.toml exists: adds tool.invar section
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
- # DX-55: Detect project state first
260
- state = detect_project_state(path)
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
- # --check mode: preview only
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
- # --reset mode: dangerous full reset
268
- if reset:
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
- # DX-55: Handle based on detected state
284
- action = state.action if not force else "update"
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
- # DX-56: Use unified sync engine for file generation
328
- console.print("\n[bold]Creating Invar files...[/bold]")
329
-
330
- # Check for project-additions.md
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 for --no-skills
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=has_project_additions,
341
- force=force,
342
- check=False, # Already handled above
343
- reset=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, Failure):
350
- console.print(f"[yellow]Warning:[/yellow] {result.failure()}")
351
- else:
352
+ if isinstance(result, Success):
352
353
  report = result.unwrap()
353
- for file in report.created:
354
- console.print(f"[green]Created[/green] {file}")
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 for protocol governance
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
- # Configure MCP server (DX-16, DX-21B)
398
- configure_mcp_with_method(path, mcp_method)
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
- # Show available methods if user might want to change
401
- if not mcp_method and not yes:
402
- show_available_mcp_methods()
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
- # Handle directory creation based on --dirs flag
412
- if dirs is not False:
413
- create_directories(path, console)
387
+ mcp_setup.write_text(_MCP_SETUP_TEMPLATE)
414
388
 
415
- # Install pre-commit hooks if requested
416
- if hooks:
417
- install_hooks(path, console)
389
+ # Track skipped files
390
+ for file, selected in selected_files.items():
391
+ if not selected:
392
+ skipped.append(file)
418
393
 
419
- # DX-57: Handle Claude Code hooks
420
- # Determine if we should install/update Claude hooks
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
- if should_install_claude_hooks and not should_skip_claude_hooks:
428
- # Install Claude hooks
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
- if claude:
454
- console.print("[dim]Next: Review CLAUDE.md and start coding with Claude Code[/dim]")
455
-
456
-
457
- # @shell_complexity: Preview display requires multiple state-specific branches
458
- def _show_check_preview(state: ProjectState, path: Path, version: str) -> None:
459
- """Show preview of what would change (--check mode)."""
460
- console.print(f"\n[bold]Invar v{version} - Preview Mode[/bold]\n")
461
-
462
- console.print(f"Project state: [cyan]{state.claude_md_state.state}[/cyan]")
463
- console.print(f"Initialized: [cyan]{state.initialized}[/cyan]")
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
+ )