Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 62 additions & 29 deletions src/lightcone/cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
The global config at ``~/.lightcone/config.yaml`` is auto-created with
defaults on first invocation if missing.
"""

from __future__ import annotations

import json
Expand All @@ -37,7 +38,15 @@

PERMISSION_TIERS: dict[str, dict[str, list[str]]] = {
"yolo": {
"allow": ["Bash(*)", "Edit", "Read", "Write", "WebSearch", "WebFetch", "mcp__*"],
"allow": [
"Bash(*)",
"Edit",
"Read",
"Write",
"WebSearch",
"WebFetch",
"mcp__*",
],
},
"recommended": {
"allow": ["Read", "Edit", "Write", "Bash(*)", "WebSearch", "WebFetch"],
Expand Down Expand Up @@ -123,11 +132,12 @@ def _project_root(start: Path | None = None) -> Path:
# =============================================================================
_LIGHTCONE = """
_______________________
| . _ |_ _|_ _ _ _ _
| . _ |_ _|_ _ _ _ _
|_|(_|| | | (_(_)| |(/_
_____|_________________
"""


@main.command()
@click.argument("directory", type=click.Path(path_type=Path), default=".")
@click.option("--no-git", is_flag=True, help="Skip git init")
Expand Down Expand Up @@ -181,9 +191,7 @@ def init(
try:
astra_init.callback(directory=directory, no_git=True) # type: ignore[misc]
except SystemExit as e:
raise click.ClickException(
f"astra init failed (exit code {e.code})."
) from e
raise click.ClickException(f"astra init failed (exit code {e.code}).") from e

# Point the spec at our project-local Containerfile. The astra
# boilerplate ships ``container: python:3.12-slim`` so the scaffold
Expand Down Expand Up @@ -233,25 +241,44 @@ def init(
if not no_venv:
if shutil.which("uv"):
with console.status("[dim]Creating virtual environment…[/dim]"):
subprocess.run(["uv", "venv", "--python", "3.12", ".venv"], cwd=directory, check=False, capture_output=True)
subprocess.run(
["uv", "venv", "--python", "3.12", ".venv"],
cwd=directory,
check=False,
capture_output=True,
)
with console.status("[dim]Installing lightcone-cli…[/dim]"):
subprocess.run(
["uv", "pip", "install", "--python", ".venv/bin/python", "lightcone-cli"],
[
"uv",
"pip",
"install",
"--python",
".venv/bin/python",
"lightcone-cli",
],
cwd=directory,
check=False,
capture_output=True,
)
else:
with console.status("[dim]Creating virtual environment…[/dim]"):
subprocess.run(["python", "-m", "venv", ".venv"], cwd=directory, check=False, capture_output=True)
subprocess.run(
["python", "-m", "venv", ".venv"],
cwd=directory,
check=False,
capture_output=True,
)
with console.status("[dim]Installing lightcone-cli…[/dim]"):
subprocess.run(
[".venv/bin/python", "-m", "pip", "install", "-q", "lightcone-cli"],
cwd=directory,
check=False,
capture_output=True,
)
console.print(f"[green]✓[/green] Virtual environment created in [cyan]{directory}/.venv[/cyan]")
console.print(
f"[green]✓[/green] Virtual environment created in [cyan]{directory}/.venv[/cyan]"
)

console.print(f"\n[green]Project initialized at[/green] {directory}")

Expand Down Expand Up @@ -439,9 +466,7 @@ def run(
rundirs = prepare_run_dirs(project)
ensure_snakemake_symlink(project, rundirs.snakemake_state)
if verbose:
console.print(
f"[dim]Scratch root:[/dim] {resolve_scratch_root(project)}"
)
console.print(f"[dim]Scratch root:[/dim] {resolve_scratch_root(project)}")

choice = load_runtime(project_path=project)
_ensure_images(project, runtime=choice.runtime)
Expand All @@ -457,12 +482,14 @@ def run(
# removes the container declarations.
if choice.runtime == "none" and not choice.explicit:
cfg_data = json.loads(cfg_path.read_text())
declared = sorted({
entry["container_image"]
for rule_entries in cfg_data.values()
for entry in rule_entries.values()
if entry.get("container_image")
})
declared = sorted(
{
entry["container_image"]
for rule_entries in cfg_data.values()
for entry in rule_entries.values()
if entry.get("container_image")
}
)
if declared:
console.print(
"[yellow]⚠ No container runtime found on PATH "
Expand Down Expand Up @@ -595,12 +622,18 @@ def _build_snakemake_cmd(
"""
cmd: list[str] = [
"snakemake",
"-s", str(snakefile_path),
"-d", str(project),
"--cores", n,
"--jobs", n,
"--executor", "dask",
"--rerun-triggers", *rerun_triggers.split(","),
"-s",
str(snakefile_path),
"-d",
str(project),
"--cores",
n,
"--jobs",
n,
"--executor",
"dask",
"--rerun-triggers",
*rerun_triggers.split(","),
]
if force:
# ``--force`` scopes to explicit targets; ``rule all`` itself
Expand Down Expand Up @@ -647,7 +680,9 @@ def _target_for(project: Path, output_id: str, universe: str) -> str:
)

_, to = matches[0]
target = resolve_output_path(project, to, universe) / to.output_id / MANIFEST_FILENAME
target = (
resolve_output_path(project, to, universe) / to.output_id / MANIFEST_FILENAME
)
return str(target.relative_to(project))


Expand Down Expand Up @@ -832,9 +867,7 @@ def _ensure_images(project: Path, *, runtime: str, force: bool = False) -> None:
# without depending on the runtime's registry resolution.
if image_exists_locally(spec_str, runtime=runtime) and not force:
continue
console.print(
f"[cyan]Pulling[/cyan] {spec_str} [dim](via {runtime})[/dim]"
)
console.print(f"[cyan]Pulling[/cyan] {spec_str} [dim](via {runtime})[/dim]")
try:
pull_image(spec_str, runtime=runtime)
except ContainerBuildError as e:
Expand Down Expand Up @@ -882,7 +915,7 @@ def export() -> None:
@click.option(
"--author",
default=None,
help="Author override, e.g. \"Name <email@host>\". Default: git config.",
help='Author override, e.g. "Name <email@host>". Default: git config.',
)
@click.option(
"--license",
Expand Down
Loading