mirror of
https://github.com/hanzoai/insights.git
synced 2026-10-30 16:15:31 +00:00
The tree was a debranded snapshot of upstream taken around 2026-02-20, and upstream has moved about five months since. This replaces it with upstream master, mechanically re-derived: bin/debrand does the rename, and the pipeline and squashed migrations are restored from the shipped tree. Not a merge. The previous import replaced the tree wholesale in a single commit, so there is no shared content history to merge against. Reproducing the rename on the new tree is what actually converges. What survives: .hanzo/workflows, the Dockerfiles and bin/docker* entrypoints, compose files, LLM.md, and insights/migrations. The squashed initial migration is load-bearing — the live database records it by name, so upstream's ~918 individual migrations cannot come back. Nine products are dropped as unreferenced. Twenty-one more that we do not run are kept anyway because the core app still imports them; unpicking those is a refactor and does not belong in a rebase. This base does not build yet, and the reasons are not in the rename: upstream now wants Python 3.13.13 against our 3.12.12 and Django 5.2 against our 4.2, and the dependency manifests still name upstream packages that we republish under our own names. Those are decisions, not renames.
245 lines
8.1 KiB
Python
245 lines
8.1 KiB
Python
"""Interactive wizard for developer environment setup.
|
|
|
|
Guides users through selecting intents and generates mprocs.yaml directly.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
|
|
import click
|
|
|
|
from .generator import (
|
|
DevenvConfig,
|
|
MprocsGenerator,
|
|
get_generated_mprocs_path,
|
|
get_main_repo_from_worktree,
|
|
load_devenv_config,
|
|
)
|
|
from .registry import create_mprocs_registry
|
|
from .resolver import IntentMap, IntentResolver
|
|
|
|
|
|
def _choose_config_location(local_path: Path) -> Path:
|
|
"""Ask where to save config if in a worktree."""
|
|
main_repo = get_main_repo_from_worktree()
|
|
if not main_repo:
|
|
return local_path # Not in a worktree
|
|
|
|
main_path = main_repo / ".insights" / ".generated" / "mprocs.yaml"
|
|
|
|
click.echo("You're in a git worktree. Where should the config be saved?")
|
|
click.echo(" 1. This worktree only")
|
|
click.echo(" 2. Main repo (symlinked here, shared across worktrees)")
|
|
click.echo("")
|
|
|
|
choice = click.prompt("Choice", type=click.Choice(["1", "2"]), default="2")
|
|
if choice == "1":
|
|
return local_path
|
|
|
|
# Create symlink from worktree to main repo (config file created after this)
|
|
local_path.parent.mkdir(parents=True, exist_ok=True)
|
|
main_path.parent.mkdir(parents=True, exist_ok=True)
|
|
# exists() is False for broken symlinks, so check both
|
|
if local_path.exists() or local_path.is_symlink():
|
|
local_path.unlink()
|
|
local_path.symlink_to(main_path)
|
|
return main_path
|
|
|
|
|
|
def run_setup_wizard(intent_map: IntentMap, log_to_files: bool = False) -> DevenvConfig | None:
|
|
"""Run the setup wizard.
|
|
|
|
Args:
|
|
intent_map: The intent map
|
|
log_to_files: Whether to log process output to /tmp/insights-*.log files
|
|
|
|
Returns:
|
|
The created config, or None if cancelled
|
|
"""
|
|
registry = create_mprocs_registry()
|
|
resolver = IntentResolver(intent_map, registry)
|
|
local_path = get_generated_mprocs_path()
|
|
|
|
click.echo("")
|
|
click.echo(click.style("Insights Developer Environment Setup", fg="green", bold=True))
|
|
click.echo("")
|
|
click.echo("Configure which services to start based on the products you're working on.")
|
|
click.echo("")
|
|
|
|
# Check for existing config (follows symlinks)
|
|
existing = load_devenv_config(local_path)
|
|
if existing:
|
|
if local_path.is_symlink():
|
|
click.echo("Found config (symlinked from main repo):")
|
|
else:
|
|
click.echo("Found existing config:")
|
|
_show_config_summary(existing)
|
|
click.echo("")
|
|
if not click.confirm("Replace it?", default=True):
|
|
return None
|
|
output_path = local_path.resolve() # Follow symlink for actual save location
|
|
else:
|
|
# No config - ask where to save if in worktree
|
|
output_path = _choose_config_location(local_path)
|
|
|
|
# Select products/intents
|
|
config = _setup_from_intents(intent_map)
|
|
|
|
# Ask about overrides
|
|
config = _configure_overrides(config, registry, resolver)
|
|
|
|
# Set log mode if requested
|
|
config.log_to_files = log_to_files
|
|
|
|
# Show summary
|
|
click.echo("")
|
|
click.echo(click.style("Summary", fg="cyan", bold=True))
|
|
click.echo("-" * 40)
|
|
_show_config_summary(config)
|
|
|
|
# Show what would be started
|
|
resolved = resolver.resolve(
|
|
config.intents,
|
|
include_units=config.include_units,
|
|
exclude_units=config.exclude_units,
|
|
skip_autostart=config.skip_autostart,
|
|
enable_autostart=config.enable_autostart,
|
|
)
|
|
click.echo("")
|
|
click.echo(f"This will start {len(resolved.units)} processes.")
|
|
click.echo("")
|
|
|
|
if not click.confirm("Save this configuration?", default=True):
|
|
click.echo("Cancelled.")
|
|
return None
|
|
|
|
# Generate and save mprocs config
|
|
generator = MprocsGenerator(registry)
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
generator.generate_and_save(resolved, output_path, config)
|
|
|
|
click.echo("")
|
|
click.echo(click.style("✓ Config saved!", fg="green"))
|
|
click.echo(f" Location: {output_path}")
|
|
click.echo("")
|
|
click.echo("Run 'insightscli start' to start.")
|
|
|
|
return config
|
|
|
|
|
|
def _show_config_summary(config: DevenvConfig) -> None:
|
|
"""Show config summary."""
|
|
if config.intents:
|
|
click.echo(f" Products: {', '.join(config.intents)}")
|
|
|
|
if config.include_units:
|
|
click.echo(f" Include: {', '.join(config.include_units)}")
|
|
if config.exclude_units:
|
|
click.echo(f" Exclude: {', '.join(config.exclude_units)}")
|
|
if config.skip_autostart:
|
|
click.echo(f" Manual start: {', '.join(config.skip_autostart)}")
|
|
if config.enable_autostart:
|
|
click.echo(f" Auto-start: {', '.join(config.enable_autostart)}")
|
|
if config.log_to_files:
|
|
click.echo(" Log mode: /tmp/insights-*.log")
|
|
|
|
|
|
def _setup_from_intents(intent_map: IntentMap) -> DevenvConfig:
|
|
"""Set up by selecting specific intents."""
|
|
select_all = "all"
|
|
click.echo("")
|
|
click.echo(click.style("Products", fg="cyan"))
|
|
click.echo(f"Select products (comma-separated numbers, or '{select_all}').")
|
|
click.echo("")
|
|
|
|
intents = list(intent_map.intents.items())
|
|
for i, (name, intent) in enumerate(intents, 1):
|
|
click.echo(f" {i:2}. {name} - {intent.description}")
|
|
|
|
click.echo("")
|
|
|
|
selection = click.prompt(f"Enter numbers (e.g., 1,3,5) or '{select_all}'", default="1")
|
|
|
|
selected_intents = []
|
|
if selection.strip().lower() == select_all:
|
|
selected_intents = [name for name, _ in intents]
|
|
else:
|
|
try:
|
|
indices = [int(x.strip()) for x in selection.split(",")]
|
|
for idx in indices:
|
|
if 1 <= idx <= len(intents):
|
|
selected_intents.append(intents[idx - 1][0])
|
|
except ValueError:
|
|
click.echo("Invalid selection, using product_analytics.")
|
|
selected_intents = ["product_analytics"]
|
|
|
|
if not selected_intents:
|
|
selected_intents = ["product_analytics"]
|
|
|
|
return DevenvConfig(intents=selected_intents)
|
|
|
|
|
|
def _configure_overrides(config: DevenvConfig, registry, resolver: IntentResolver) -> DevenvConfig:
|
|
"""Configure overrides."""
|
|
# Show what will be started
|
|
resolved = resolver.resolve(config.intents)
|
|
units_list = sorted(resolved.units)
|
|
|
|
# Find manual-start processes
|
|
manual_start = set()
|
|
for unit in units_list:
|
|
proc_config = registry.get_process_config(unit)
|
|
if proc_config.get("autostart") is False:
|
|
manual_start.add(unit)
|
|
|
|
click.echo("")
|
|
click.echo(click.style("Processes that will start:", fg="cyan"))
|
|
for i, unit in enumerate(units_list, 1):
|
|
if unit in manual_start:
|
|
click.echo(f" {i:2}. {unit} (manual start)")
|
|
else:
|
|
click.echo(f" {i:2}. {unit}")
|
|
|
|
click.echo("")
|
|
click.echo("Units to exclude (enter numbers or names from above, or blank to skip):")
|
|
exclude = click.prompt("Comma-separated", default="")
|
|
excluded, out_of_range = _parse_exclude_input(exclude, units_list)
|
|
for idx in out_of_range:
|
|
click.echo(f" Ignoring out-of-range number: {idx}")
|
|
if excluded:
|
|
config.exclude_units = excluded
|
|
|
|
# Ask about manual-start processes (those with ask_skip: true)
|
|
ask_skip_processes = registry.get_ask_skip_processes()
|
|
for process_name in ask_skip_processes:
|
|
click.echo("")
|
|
if click.confirm(f"Auto-start {process_name}?", default=False):
|
|
config.enable_autostart.append(process_name)
|
|
|
|
return config
|
|
|
|
|
|
def _parse_exclude_input(raw: str, units: list[str]) -> tuple[list[str], list[int]]:
|
|
"""Parse the 'Units to exclude' prompt input.
|
|
|
|
Returns (resolved unit names, out-of-range numbers seen). A token that
|
|
parses as a digit is treated as a 1-based index into `units`; anything
|
|
else is passed through as a name.
|
|
"""
|
|
excluded: list[str] = []
|
|
out_of_range: list[int] = []
|
|
for token in raw.split(","):
|
|
token = token.strip()
|
|
if not token:
|
|
continue
|
|
if token.isdecimal():
|
|
idx = int(token)
|
|
if 1 <= idx <= len(units):
|
|
excluded.append(units[idx - 1])
|
|
else:
|
|
out_of_range.append(idx)
|
|
else:
|
|
excluded.append(token)
|
|
return excluded, out_of_range
|