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.
380 lines
13 KiB
Python
380 lines
13 KiB
Python
"""CLI commands for intent-based developer environment.
|
|
|
|
Provides insightscli dev:* commands for managing the development environment.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import click
|
|
|
|
from .generator import (
|
|
TRACKED_MPROCS_FILES,
|
|
DevenvConfig,
|
|
MprocsGenerator,
|
|
build_docker_compose_command,
|
|
get_generated_mprocs_path,
|
|
is_mprocs_shell_up_to_date,
|
|
load_devenv_config,
|
|
regenerate_mprocs_shell,
|
|
)
|
|
from .registry import create_mprocs_registry
|
|
from .resolver import IntentResolver, load_intent_map
|
|
|
|
|
|
def _create_resolver() -> IntentResolver:
|
|
"""Create an IntentResolver with the default intent map and mprocs registry."""
|
|
intent_map = load_intent_map()
|
|
registry = create_mprocs_registry()
|
|
return IntentResolver(intent_map, registry)
|
|
|
|
|
|
@click.command(name="dev:generate", help="Regenerate mprocs config from saved settings")
|
|
@click.option(
|
|
"--with",
|
|
"with_intents",
|
|
multiple=True,
|
|
help="Temporarily add intent(s) for this generation",
|
|
)
|
|
@click.option(
|
|
"--without",
|
|
"without_units",
|
|
multiple=True,
|
|
help="Temporarily exclude unit(s) for this generation (e.g., --without typegen)",
|
|
)
|
|
def dev_generate(
|
|
with_intents: tuple[str, ...],
|
|
without_units: tuple[str, ...],
|
|
) -> None:
|
|
"""Regenerate mprocs config from saved settings.
|
|
|
|
Picks up changes from intent-map.yaml without starting mprocs.
|
|
Run 'insightscli start' to start the dev environment.
|
|
"""
|
|
try:
|
|
resolver = _create_resolver()
|
|
except FileNotFoundError as e:
|
|
click.echo(f"Error: {e}", err=True)
|
|
click.echo("Are you in the Insights repository root?", err=True)
|
|
raise SystemExit(1)
|
|
|
|
output_path = get_generated_mprocs_path()
|
|
|
|
# Load saved config
|
|
saved_config = load_devenv_config(output_path)
|
|
|
|
if saved_config is None:
|
|
click.echo("No dev environment config found.")
|
|
click.echo("Run 'insightscli dev:setup' to configure your environment.")
|
|
click.echo("")
|
|
click.echo("Using default intents for now...")
|
|
saved_config = DevenvConfig(intents=["product_analytics"])
|
|
|
|
# Build effective intents with temporary overrides
|
|
intents = saved_config.intents.copy()
|
|
intents.extend(with_intents)
|
|
|
|
# Merge exclude_units
|
|
exclude_units = list(saved_config.exclude_units) + list(without_units)
|
|
|
|
try:
|
|
resolved = resolver.resolve(
|
|
intents,
|
|
include_units=saved_config.include_units,
|
|
exclude_units=exclude_units,
|
|
skip_autostart=saved_config.skip_autostart,
|
|
enable_autostart=saved_config.enable_autostart,
|
|
)
|
|
except ValueError as e:
|
|
click.echo(f"Error: {e}", err=True)
|
|
raise SystemExit(1)
|
|
|
|
# Generate mprocs config
|
|
registry = create_mprocs_registry()
|
|
generator = MprocsGenerator(registry)
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
generator.generate_and_save(resolved, output_path, saved_config)
|
|
|
|
# Stash devenv-specific properties so _fire_telemetry includes them
|
|
# in the command_completed event for dev:generate.
|
|
ctx = click.get_current_context()
|
|
ctx.meta["insightscli.devenv"] = {
|
|
"intents": sorted(resolved.intents),
|
|
"intent_count": len(resolved.intents),
|
|
"unit_count": len(resolved.units),
|
|
"docker_profiles": sorted(resolved.docker_profiles),
|
|
}
|
|
|
|
click.echo("Generated mprocs config from saved config")
|
|
click.echo(f" Products: {', '.join(sorted(resolved.intents))}")
|
|
click.echo(f" Units: {len(resolved.units)} processes")
|
|
click.echo(f" Config: {output_path}")
|
|
click.echo("")
|
|
click.echo("Run 'insightscli dev:setup' to change your environment.")
|
|
|
|
|
|
@click.command(name="dev:explain", help="Show what services would be started for intents")
|
|
@click.argument("intents", nargs=-1)
|
|
def dev_explain(intents: tuple[str, ...]) -> None:
|
|
"""Show resolution of intents to services.
|
|
|
|
If no intents are provided, shows resolution for your current config.
|
|
"""
|
|
try:
|
|
resolver = _create_resolver()
|
|
except FileNotFoundError:
|
|
click.echo("Error: intent-map.yaml not found in devenv/", err=True)
|
|
raise SystemExit(1)
|
|
|
|
if intents:
|
|
try:
|
|
resolved = resolver.resolve(list(intents))
|
|
except ValueError as e:
|
|
click.echo(f"Error: {e}", err=True)
|
|
raise SystemExit(1)
|
|
else:
|
|
# Use saved config
|
|
output_path = get_generated_mprocs_path()
|
|
saved_config = load_devenv_config(output_path)
|
|
|
|
if saved_config is None:
|
|
click.echo("No config found and no intents specified.")
|
|
click.echo("")
|
|
click.echo("Usage:")
|
|
click.echo(" insightscli dev:explain error_tracking session_replay")
|
|
click.echo(" insightscli dev:setup # to create a config")
|
|
return
|
|
|
|
try:
|
|
resolved = resolver.resolve(
|
|
saved_config.intents,
|
|
include_units=saved_config.include_units,
|
|
exclude_units=saved_config.exclude_units,
|
|
skip_autostart=saved_config.skip_autostart,
|
|
enable_autostart=saved_config.enable_autostart,
|
|
)
|
|
except ValueError as e:
|
|
click.echo(f"Error: {e}", err=True)
|
|
raise SystemExit(1)
|
|
|
|
click.echo(resolver.explain_resolution(resolved))
|
|
|
|
|
|
@click.command(name="dev:intents", help="List available intents")
|
|
def dev_intents() -> None:
|
|
"""List all available intents with descriptions."""
|
|
try:
|
|
resolver = _create_resolver()
|
|
except FileNotFoundError:
|
|
click.echo("Error: intent-map.yaml not found", err=True)
|
|
raise SystemExit(1)
|
|
|
|
click.echo("Available intents:")
|
|
click.echo("")
|
|
|
|
for name, description in resolver.get_available_intents():
|
|
intent = resolver.intent_map.intents[name]
|
|
click.echo(f" {name}")
|
|
click.echo(f" {description}")
|
|
click.echo(f" Capabilities: {', '.join(intent.capabilities)}")
|
|
click.echo("")
|
|
|
|
|
|
@click.command(name="dev:list-units", help="List autostart units for given intents (used by phrocs)")
|
|
@click.argument("intents", nargs=-1, required=True)
|
|
def dev_list_units(intents: tuple[str, ...]) -> None:
|
|
"""Resolve intents and print autostart unit names, one per line."""
|
|
try:
|
|
intent_map = load_intent_map()
|
|
registry = create_mprocs_registry()
|
|
resolver = IntentResolver(intent_map, registry)
|
|
except FileNotFoundError as e:
|
|
click.echo(f"Error: {e}", err=True)
|
|
raise SystemExit(1)
|
|
|
|
try:
|
|
resolved = resolver.resolve(list(intents))
|
|
except ValueError as e:
|
|
click.echo(f"Error: {e}", err=True)
|
|
raise SystemExit(1)
|
|
|
|
for unit in sorted(resolved.units):
|
|
proc_config = registry.get_process_config(unit)
|
|
if proc_config.get("autostart") is not False:
|
|
click.echo(unit)
|
|
|
|
|
|
@click.command(name="dev:apply", help="Apply intent config non-interactively (used by phrocs)")
|
|
@click.argument("intents", nargs=-1, required=True)
|
|
@click.option(
|
|
"--include",
|
|
"include_units",
|
|
multiple=True,
|
|
default=None,
|
|
help="Units to include (saved to config, persists across runs)",
|
|
)
|
|
@click.option(
|
|
"--exclude",
|
|
"exclude_units",
|
|
multiple=True,
|
|
default=None,
|
|
help="Units to exclude (saved to config, persists across runs)",
|
|
)
|
|
@click.option(
|
|
"--skip-autostart",
|
|
"skip_autostart",
|
|
multiple=True,
|
|
default=None,
|
|
help="Skip autostart units (saved to config, persists across runs)",
|
|
)
|
|
@click.option(
|
|
"--enable-autostart",
|
|
"enable_autostart",
|
|
multiple=True,
|
|
default=None,
|
|
help="Enable autostart units (saved to config, persists across runs)",
|
|
)
|
|
def dev_apply(
|
|
intents: tuple[str, ...],
|
|
include_units: tuple[str, ...] | None,
|
|
exclude_units: tuple[str, ...] | None,
|
|
skip_autostart: tuple[str, ...] | None,
|
|
enable_autostart: tuple[str, ...] | None,
|
|
) -> None:
|
|
"""Apply a set of intents non-interactively, regenerating the mprocs config."""
|
|
try:
|
|
intent_map = load_intent_map()
|
|
registry = create_mprocs_registry()
|
|
resolver = IntentResolver(intent_map, registry)
|
|
except FileNotFoundError as e:
|
|
click.echo(f"Error: {e}", err=True)
|
|
raise SystemExit(1)
|
|
|
|
output_path = get_generated_mprocs_path()
|
|
|
|
# Load existing config to preserve overrides
|
|
saved_config = load_devenv_config(output_path)
|
|
if saved_config is None:
|
|
saved_config = DevenvConfig()
|
|
|
|
# Replace intents with the provided args
|
|
saved_config.intents = list(intents)
|
|
|
|
# Replace options only when explicitly provided (default=None)
|
|
if include_units is not None:
|
|
saved_config.include_units = list(include_units)
|
|
if exclude_units is not None:
|
|
saved_config.exclude_units = list(exclude_units)
|
|
if skip_autostart is not None:
|
|
saved_config.skip_autostart = list(skip_autostart)
|
|
if enable_autostart is not None:
|
|
saved_config.enable_autostart = list(enable_autostart)
|
|
|
|
try:
|
|
resolved = resolver.resolve(
|
|
saved_config.intents,
|
|
include_units=saved_config.include_units,
|
|
exclude_units=saved_config.exclude_units,
|
|
skip_autostart=saved_config.skip_autostart,
|
|
enable_autostart=saved_config.enable_autostart,
|
|
)
|
|
except ValueError as e:
|
|
click.echo(f"Error: {e}", err=True)
|
|
raise SystemExit(1)
|
|
|
|
generator = MprocsGenerator(registry)
|
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
generator.generate_and_save(resolved, output_path, saved_config)
|
|
|
|
click.echo(str(output_path))
|
|
|
|
|
|
@click.command(name="dev:setup", help="Interactive wizard to configure your dev environment")
|
|
@click.option("--log", "log_to_files", is_flag=True, help="Log process output to /tmp/insights-*.log files")
|
|
def dev_setup(log_to_files: bool) -> None:
|
|
"""Run the interactive setup wizard to configure your development environment."""
|
|
from .wizard import run_setup_wizard
|
|
|
|
try:
|
|
intent_map = load_intent_map()
|
|
except FileNotFoundError:
|
|
click.echo("Error: intent-map.yaml not found in devenv/", err=True)
|
|
click.echo("Are you in the Insights repository root?", err=True)
|
|
raise SystemExit(1)
|
|
|
|
run_setup_wizard(intent_map, log_to_files=log_to_files)
|
|
|
|
|
|
@click.command(
|
|
name="dev:regenerate-mprocs",
|
|
help="Sync bin/mprocs.yaml and bin/mprocs-e2e.yaml's docker-compose shells with generator.py",
|
|
)
|
|
@click.option("--check", is_flag=True, help="Report staleness without rewriting the files; exit 1 if any are stale")
|
|
def dev_regenerate_mprocs(check: bool) -> None:
|
|
"""Write each TRACKED_MPROCS_FILES entry's built shell into its file."""
|
|
if check:
|
|
stale = [t for t in TRACKED_MPROCS_FILES if not is_mprocs_shell_up_to_date(t.path, t.build_shell())]
|
|
if not stale:
|
|
click.echo(f"{', '.join(t.name for t in TRACKED_MPROCS_FILES)} are up to date")
|
|
return
|
|
for target in stale:
|
|
click.echo(f"{target.path} is stale", err=True)
|
|
click.echo("run `insightscli dev:regenerate-mprocs` to fix", err=True)
|
|
raise SystemExit(1)
|
|
|
|
for target in TRACKED_MPROCS_FILES:
|
|
rewrote = regenerate_mprocs_shell(target.path, target.build_shell())
|
|
click.echo(f"Regenerated {target.path}" if rewrote else f"{target.path} was already up to date")
|
|
|
|
|
|
def _get_docker_profiles_from_config() -> list[str]:
|
|
"""Get docker profiles from saved intent config."""
|
|
try:
|
|
resolver = _create_resolver()
|
|
except FileNotFoundError:
|
|
return []
|
|
|
|
output_path = get_generated_mprocs_path()
|
|
saved_config = load_devenv_config(output_path)
|
|
|
|
if saved_config is None:
|
|
return []
|
|
|
|
try:
|
|
resolved = resolver.resolve(
|
|
saved_config.intents,
|
|
include_units=saved_config.include_units,
|
|
exclude_units=saved_config.exclude_units,
|
|
)
|
|
return sorted(resolved.docker_profiles)
|
|
except ValueError:
|
|
return []
|
|
|
|
|
|
@click.command(
|
|
name="docker:services:up",
|
|
help="Start Docker services based on your intent config",
|
|
context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
|
|
)
|
|
@click.option("--yes", is_flag=True, hidden=True, help="Ignored, for composite command compatibility")
|
|
def docker_services_up(yes: bool) -> None:
|
|
"""Start Docker infrastructure services respecting intent configuration.
|
|
|
|
Uses docker compose profiles based on your configured intents.
|
|
Run 'insightscli dev:setup' to configure which services to start.
|
|
"""
|
|
import subprocess
|
|
|
|
profiles = _get_docker_profiles_from_config()
|
|
cmd = build_docker_compose_command(profiles, "up -d")
|
|
|
|
if profiles:
|
|
click.echo(f"Starting Docker services with profiles: {', '.join(profiles)}")
|
|
else:
|
|
click.echo("Starting Docker core services only (no intent config found)")
|
|
click.echo(f" {cmd}")
|
|
|
|
# cmd is built from internal config (intent-map.yaml), not user input
|
|
result = subprocess.run(cmd, shell=True) # nosemgrep: subprocess-shell-true
|
|
raise SystemExit(result.returncode)
|