multi-forge 0.2.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.
- forge/__init__.py +3 -0
- forge/_extensions/agents/.gitkeep +0 -0
- forge/_extensions/commands/.gitkeep +0 -0
- forge/_extensions/skills/analyze/SKILL.md +87 -0
- forge/_extensions/skills/challenge/SKILL.md +91 -0
- forge/_extensions/skills/consensus/SKILL.md +120 -0
- forge/_extensions/skills/consensus/resources/code_consensus_evaluation.md +94 -0
- forge/_extensions/skills/consensus/resources/consensus_evaluation.md +70 -0
- forge/_extensions/skills/consensus/resources/synthesis.md +101 -0
- forge/_extensions/skills/debate/SKILL.md +116 -0
- forge/_extensions/skills/debate/resources/code_debate_evaluation.md +101 -0
- forge/_extensions/skills/debate/resources/debate_evaluation.md +90 -0
- forge/_extensions/skills/panel/SKILL.md +141 -0
- forge/_extensions/skills/panel/resources/synthesis.md +103 -0
- forge/_extensions/skills/qa/SKILL.md +704 -0
- forge/_extensions/skills/qa/resources/checklist/0-enable.md +78 -0
- forge/_extensions/skills/qa/resources/checklist/1-preflight.md +24 -0
- forge/_extensions/skills/qa/resources/checklist/10-resume.md +143 -0
- forge/_extensions/skills/qa/resources/checklist/11-config.md +150 -0
- forge/_extensions/skills/qa/resources/checklist/12-search.md +58 -0
- forge/_extensions/skills/qa/resources/checklist/13-guard.md +237 -0
- forge/_extensions/skills/qa/resources/checklist/14-workflow.md +305 -0
- forge/_extensions/skills/qa/resources/checklist/15-skills.md +155 -0
- forge/_extensions/skills/qa/resources/checklist/16-handoff.md +224 -0
- forge/_extensions/skills/qa/resources/checklist/17-info.md +50 -0
- forge/_extensions/skills/qa/resources/checklist/18-disable.md +84 -0
- forge/_extensions/skills/qa/resources/checklist/19-uninstall.md +146 -0
- forge/_extensions/skills/qa/resources/checklist/2-extensions.md +188 -0
- forge/_extensions/skills/qa/resources/checklist/20-cleanup.md +36 -0
- forge/_extensions/skills/qa/resources/checklist/3-auth.md +234 -0
- forge/_extensions/skills/qa/resources/checklist/4-proxy.md +481 -0
- forge/_extensions/skills/qa/resources/checklist/5-session.md +541 -0
- forge/_extensions/skills/qa/resources/checklist/6-hooks.md +275 -0
- forge/_extensions/skills/qa/resources/checklist/7-costs.md +309 -0
- forge/_extensions/skills/qa/resources/checklist/8-status-line.md +174 -0
- forge/_extensions/skills/qa/resources/checklist/9-direct-commands.md +146 -0
- forge/_extensions/skills/qa/resources/checklist.md +103 -0
- forge/_extensions/skills/qa/resources/report-template.md +62 -0
- forge/_extensions/skills/qa/scripts/start-container.sh +529 -0
- forge/_extensions/skills/qa/scripts/walkthrough-state.py +1137 -0
- forge/_extensions/skills/review/SKILL.md +125 -0
- forge/_extensions/skills/review/references/claude-4.6.md +474 -0
- forge/_extensions/skills/review/references/claude-4.7.md +710 -0
- forge/_extensions/skills/review/references/gemini-3.1.md +546 -0
- forge/_extensions/skills/review/references/gpt-5.5.md +490 -0
- forge/_extensions/skills/review/references/skills-writing-guide.md +1588 -0
- forge/_extensions/skills/review/resources/code-anthropic.md +160 -0
- forge/_extensions/skills/review/resources/code-gemini.md +184 -0
- forge/_extensions/skills/review/resources/code-openai.md +203 -0
- forge/_extensions/skills/review/resources/code.md +160 -0
- forge/_extensions/skills/review-docs/SKILL.md +121 -0
- forge/_extensions/skills/review-docs/resources/docs-anthropic.md +170 -0
- forge/_extensions/skills/review-docs/resources/docs-gemini.md +204 -0
- forge/_extensions/skills/review-docs/resources/docs-openai.md +231 -0
- forge/_extensions/skills/review-docs/resources/docs.md +170 -0
- forge/_extensions/skills/smoke-test/SKILL.md +27 -0
- forge/_extensions/skills/smoke-test/scripts/smoke-test.sh +118 -0
- forge/_extensions/skills/understand/SKILL.md +148 -0
- forge/_extensions/skills/understand/resources/code-anthropic.md +163 -0
- forge/_extensions/skills/understand/resources/code-gemini.md +194 -0
- forge/_extensions/skills/understand/resources/code-openai.md +181 -0
- forge/_extensions/skills/understand/resources/code.md +163 -0
- forge/_extensions/skills/understand/resources/docs-anthropic.md +177 -0
- forge/_extensions/skills/understand/resources/docs-gemini.md +202 -0
- forge/_extensions/skills/understand/resources/docs-openai.md +191 -0
- forge/_extensions/skills/understand/resources/docs.md +177 -0
- forge/_extensions/skills/walkthrough/SKILL.md +599 -0
- forge/_extensions/skills/walkthrough/resources/checklist.md +765 -0
- forge/_extensions/skills/walkthrough/scripts/run-in-repo.sh +118 -0
- forge/_extensions/skills/walkthrough/scripts/setup-test-repo.sh +198 -0
- forge/_extensions/skills/walkthrough/scripts/walkthrough-state.py +1137 -0
- forge/backend/__init__.py +174 -0
- forge/backend/adapters/__init__.py +38 -0
- forge/backend/adapters/litellm.py +158 -0
- forge/backend/creation.py +89 -0
- forge/backend/registry.py +178 -0
- forge/cli/__init__.py +16 -0
- forge/cli/auth.py +483 -0
- forge/cli/backend.py +298 -0
- forge/cli/claude.py +411 -0
- forge/cli/config_cmd.py +303 -0
- forge/cli/extensions.py +1001 -0
- forge/cli/gc.py +165 -0
- forge/cli/guard.py +1018 -0
- forge/cli/guards.py +106 -0
- forge/cli/handoff.py +110 -0
- forge/cli/hooks/__init__.py +36 -0
- forge/cli/hooks/_group.py +20 -0
- forge/cli/hooks/_helpers.py +149 -0
- forge/cli/hooks/commands.py +1677 -0
- forge/cli/hooks/direct_commands.py +1304 -0
- forge/cli/hooks/install.py +232 -0
- forge/cli/hooks/policy.py +151 -0
- forge/cli/hooks/read_hygiene.py +74 -0
- forge/cli/hooks/verification.py +370 -0
- forge/cli/logs.py +406 -0
- forge/cli/main.py +292 -0
- forge/cli/proxy.py +1821 -0
- forge/cli/proxy_costs.py +313 -0
- forge/cli/search.py +416 -0
- forge/cli/session.py +892 -0
- forge/cli/session_addendum.py +81 -0
- forge/cli/session_fork.py +750 -0
- forge/cli/session_handoff.py +141 -0
- forge/cli/session_lifecycle.py +2053 -0
- forge/cli/session_manage.py +1336 -0
- forge/cli/session_memory.py +201 -0
- forge/cli/status_line.py +1398 -0
- forge/cli/workflow.py +1964 -0
- forge/config/__init__.py +110 -0
- forge/config/dataclass_utils.py +88 -0
- forge/config/defaults/__init__.py +0 -0
- forge/config/defaults/backends/__init__.py +0 -0
- forge/config/defaults/backends/litellm.yaml +196 -0
- forge/config/defaults/templates/__init__.py +0 -0
- forge/config/defaults/templates/litellm-anthropic-local.yaml +33 -0
- forge/config/defaults/templates/litellm-anthropic.yaml +24 -0
- forge/config/defaults/templates/litellm-gemini-flash-local.yaml +37 -0
- forge/config/defaults/templates/litellm-gemini-local.yaml +32 -0
- forge/config/defaults/templates/litellm-gemini-test.yaml +34 -0
- forge/config/defaults/templates/litellm-gemini.yaml +21 -0
- forge/config/defaults/templates/litellm-openai-codex-local.yaml +36 -0
- forge/config/defaults/templates/litellm-openai-local.yaml +38 -0
- forge/config/defaults/templates/litellm-openai.yaml +28 -0
- forge/config/defaults/templates/openrouter-anthropic.yaml +23 -0
- forge/config/defaults/templates/openrouter-deepseek.yaml +26 -0
- forge/config/defaults/templates/openrouter-gemini-flash.yaml +26 -0
- forge/config/defaults/templates/openrouter-gemini.yaml +23 -0
- forge/config/defaults/templates/openrouter-glm.yaml +23 -0
- forge/config/defaults/templates/openrouter-kimi.yaml +30 -0
- forge/config/defaults/templates/openrouter-minimax.yaml +26 -0
- forge/config/defaults/templates/openrouter-openai-codex.yaml +23 -0
- forge/config/defaults/templates/openrouter-openai.yaml +28 -0
- forge/config/defaults/templates/openrouter-qwen.yaml +25 -0
- forge/config/loader.py +675 -0
- forge/config/schema.py +448 -0
- forge/core/__init__.py +5 -0
- forge/core/auth/__init__.py +67 -0
- forge/core/auth/capabilities.py +219 -0
- forge/core/auth/credentials_file.py +244 -0
- forge/core/auth/protocols.py +18 -0
- forge/core/auth/secrets.py +243 -0
- forge/core/auth/template_secrets.py +112 -0
- forge/core/data/__init__.py +5 -0
- forge/core/data/model_catalog.yaml +1522 -0
- forge/core/data/pricing.yaml +140 -0
- forge/core/data/system_prompt_addendums/__init__.py +0 -0
- forge/core/data/system_prompt_addendums/gemini.md +330 -0
- forge/core/data/system_prompt_addendums/openai.md +328 -0
- forge/core/llm/__init__.py +231 -0
- forge/core/llm/clients/__init__.py +14 -0
- forge/core/llm/clients/base.py +115 -0
- forge/core/llm/clients/litellm.py +619 -0
- forge/core/llm/clients/openai_compat.py +244 -0
- forge/core/llm/clients/openrouter.py +234 -0
- forge/core/llm/credentials.py +439 -0
- forge/core/llm/detection.py +86 -0
- forge/core/llm/errors.py +44 -0
- forge/core/llm/protocols.py +80 -0
- forge/core/llm/types.py +176 -0
- forge/core/logging.py +146 -0
- forge/core/models/__init__.py +91 -0
- forge/core/models/catalog.py +467 -0
- forge/core/models/pricing.py +165 -0
- forge/core/models/types.py +167 -0
- forge/core/naming.py +212 -0
- forge/core/ops/__init__.py +73 -0
- forge/core/ops/context.py +141 -0
- forge/core/ops/gc.py +802 -0
- forge/core/ops/proxy.py +146 -0
- forge/core/ops/resolution.py +135 -0
- forge/core/ops/session.py +344 -0
- forge/core/ops/session_context.py +548 -0
- forge/core/paths.py +38 -0
- forge/core/process.py +54 -0
- forge/core/reactive/__init__.py +38 -0
- forge/core/reactive/cost_tracking.py +300 -0
- forge/core/reactive/env.py +180 -0
- forge/core/reactive/proxy.py +78 -0
- forge/core/reactive/routing.py +622 -0
- forge/core/reactive/session_runner.py +185 -0
- forge/core/reactive/structured_output.py +62 -0
- forge/core/reactive/tagger.py +94 -0
- forge/core/reactive/throttle.py +132 -0
- forge/core/state/__init__.py +59 -0
- forge/core/state/exceptions.py +59 -0
- forge/core/state/io.py +140 -0
- forge/core/state/lock.py +99 -0
- forge/core/state/timestamps.py +60 -0
- forge/core/transcript.py +78 -0
- forge/core/typing_helpers.py +24 -0
- forge/core/workqueue/__init__.py +67 -0
- forge/core/workqueue/queue.py +552 -0
- forge/core/workqueue/types.py +63 -0
- forge/guard/__init__.py +26 -0
- forge/guard/deterministic/__init__.py +26 -0
- forge/guard/deterministic/base.py +158 -0
- forge/guard/deterministic/coding_standards.py +256 -0
- forge/guard/deterministic/registry.py +148 -0
- forge/guard/deterministic/tdd.py +171 -0
- forge/guard/engine.py +216 -0
- forge/guard/protocols.py +91 -0
- forge/guard/queries.py +96 -0
- forge/guard/semantic/__init__.py +34 -0
- forge/guard/semantic/promotion.py +18 -0
- forge/guard/semantic/supervisor.py +813 -0
- forge/guard/semantic/verdict.py +183 -0
- forge/guard/store.py +124 -0
- forge/guard/team/__init__.py +6 -0
- forge/guard/team/config.py +24 -0
- forge/guard/team/handlers.py +209 -0
- forge/guard/team/prompts.py +41 -0
- forge/guard/types.py +125 -0
- forge/guard/workflow/__init__.py +17 -0
- forge/guard/workflow/branches.py +67 -0
- forge/guard/workflow/config.py +63 -0
- forge/guard/workflow/divergence.py +113 -0
- forge/guard/workflow/policy.py +87 -0
- forge/guard/workflow/stages.py +205 -0
- forge/install/__init__.py +55 -0
- forge/install/cli.py +281 -0
- forge/install/exceptions.py +163 -0
- forge/install/hooks.py +109 -0
- forge/install/installer.py +1037 -0
- forge/install/models.py +321 -0
- forge/install/preset.py +272 -0
- forge/install/settings_merge.py +831 -0
- forge/install/tracking.py +238 -0
- forge/install/version.py +141 -0
- forge/proxy/__init__.py +0 -0
- forge/proxy/base_client.py +181 -0
- forge/proxy/client_adapter.py +476 -0
- forge/proxy/client_factory.py +531 -0
- forge/proxy/converters.py +1206 -0
- forge/proxy/cost_logger.py +132 -0
- forge/proxy/cost_tracker.py +242 -0
- forge/proxy/data_models.py +338 -0
- forge/proxy/error_hints.py +92 -0
- forge/proxy/metrics.py +222 -0
- forge/proxy/model_spec.py +158 -0
- forge/proxy/proxies.py +333 -0
- forge/proxy/proxy_identity.py +134 -0
- forge/proxy/proxy_orchestrator.py +1018 -0
- forge/proxy/proxy_startup.py +54 -0
- forge/proxy/server.py +1561 -0
- forge/proxy/utils.py +537 -0
- forge/review/__init__.py +6 -0
- forge/review/adversarial.py +111 -0
- forge/review/consensus.py +236 -0
- forge/review/engine.py +356 -0
- forge/review/models.py +437 -0
- forge/review/resources/__init__.py +5 -0
- forge/review/resources/codereview-performance.md +85 -0
- forge/review/resources/codereview-quick.md +75 -0
- forge/review/resources/codereview-security.md +92 -0
- forge/review/resources/codereview.md +85 -0
- forge/review/resources/docreview-quick.md +75 -0
- forge/review/resources/docreview.md +86 -0
- forge/review/resources/thinkdeep.md +89 -0
- forge/review/routing.py +368 -0
- forge/review/synthesis.py +73 -0
- forge/runtime_config.py +438 -0
- forge/search/__init__.py +55 -0
- forge/search/bm25_store.py +264 -0
- forge/search/content_store.py +197 -0
- forge/search/engine.py +352 -0
- forge/search/exceptions.py +51 -0
- forge/search/extractor.py +234 -0
- forge/search/index_state.py +295 -0
- forge/search/store.py +215 -0
- forge/search/tokenizer.py +24 -0
- forge/session/__init__.py +130 -0
- forge/session/active.py +339 -0
- forge/session/artifacts.py +202 -0
- forge/session/claude/__init__.py +50 -0
- forge/session/claude/cleanup.py +105 -0
- forge/session/claude/invoke.py +236 -0
- forge/session/claude/paths.py +200 -0
- forge/session/cleanup.py +216 -0
- forge/session/config.py +34 -0
- forge/session/direct_model.py +107 -0
- forge/session/effective.py +169 -0
- forge/session/exceptions.py +255 -0
- forge/session/handoff.py +881 -0
- forge/session/handoff_agent.py +544 -0
- forge/session/hooks/__init__.py +35 -0
- forge/session/hooks/models.py +73 -0
- forge/session/hooks/session_start.py +507 -0
- forge/session/identity.py +84 -0
- forge/session/index.py +553 -0
- forge/session/manager.py +1506 -0
- forge/session/models.py +572 -0
- forge/session/overrides.py +344 -0
- forge/session/plan_resolution.py +286 -0
- forge/session/prev_sessions.py +128 -0
- forge/session/store.py +431 -0
- forge/session/validation.py +47 -0
- forge/session/worktree/__init__.py +65 -0
- forge/session/worktree/cleanup.py +262 -0
- forge/session/worktree/config_copy.py +203 -0
- forge/session/worktree/create.py +332 -0
- forge/sidecar/__init__.py +29 -0
- forge/sidecar/container.py +161 -0
- forge/sidecar/docker.py +86 -0
- forge/sidecar/secrets.py +19 -0
- multi_forge-0.2.0.dist-info/METADATA +242 -0
- multi_forge-0.2.0.dist-info/RECORD +311 -0
- multi_forge-0.2.0.dist-info/WHEEL +4 -0
- multi_forge-0.2.0.dist-info/entry_points.txt +2 -0
- multi_forge-0.2.0.dist-info/licenses/LICENSE +203 -0
- multi_forge-0.2.0.dist-info/licenses/NOTICE +14 -0
forge/cli/config_cmd.py
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
"""CLI commands for Forge runtime configuration.
|
|
2
|
+
|
|
3
|
+
Manages ~/.forge/config.yaml — global runtime preferences that affect
|
|
4
|
+
CLI and session behavior (not proxy routing).
|
|
5
|
+
|
|
6
|
+
Patterns:
|
|
7
|
+
- show: matches forge proxy show (syntax-highlighted YAML)
|
|
8
|
+
- set: matches forge proxy set (type coercion, atomic write)
|
|
9
|
+
- edit: matches forge proxy edit ($EDITOR + validation)
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import shutil
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import tempfile
|
|
19
|
+
from dataclasses import fields
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
import click
|
|
24
|
+
from rich.console import Console
|
|
25
|
+
from rich.syntax import Syntax
|
|
26
|
+
|
|
27
|
+
from forge.core.paths import display_path
|
|
28
|
+
from forge.runtime_config import (
|
|
29
|
+
RuntimeConfig,
|
|
30
|
+
ensure_config,
|
|
31
|
+
get_config_path,
|
|
32
|
+
load_runtime_config,
|
|
33
|
+
reset_runtime_config,
|
|
34
|
+
write_runtime_config,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@click.group(invoke_without_command=True)
|
|
39
|
+
@click.pass_context
|
|
40
|
+
def config(ctx: click.Context) -> None:
|
|
41
|
+
"""Manage Forge global configuration.
|
|
42
|
+
|
|
43
|
+
\b
|
|
44
|
+
Configuration file: ~/.forge/config.yaml
|
|
45
|
+
Auto-created with documented defaults on first access.
|
|
46
|
+
|
|
47
|
+
\b
|
|
48
|
+
Examples:
|
|
49
|
+
forge config # Show effective config
|
|
50
|
+
forge config set proxy_mode=sidecar
|
|
51
|
+
forge config edit # Open in $EDITOR
|
|
52
|
+
"""
|
|
53
|
+
if ctx.invoked_subcommand is None:
|
|
54
|
+
ctx.invoke(show_cmd)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
@config.command("show")
|
|
58
|
+
@click.option("--raw", is_flag=True, help="Output raw YAML without syntax highlighting")
|
|
59
|
+
def show_cmd(raw: bool = False) -> None:
|
|
60
|
+
"""Show effective runtime configuration.
|
|
61
|
+
|
|
62
|
+
Displays current values (from file + defaults + env var overrides).
|
|
63
|
+
"""
|
|
64
|
+
console = Console(width=200)
|
|
65
|
+
config_path = ensure_config()
|
|
66
|
+
|
|
67
|
+
rc = load_runtime_config()
|
|
68
|
+
env_sources: dict[str, str] = getattr(rc, "_env_sources", {})
|
|
69
|
+
|
|
70
|
+
import yaml
|
|
71
|
+
|
|
72
|
+
effective: dict[str, Any] = {}
|
|
73
|
+
for f in fields(RuntimeConfig):
|
|
74
|
+
effective[f.name] = getattr(rc, f.name)
|
|
75
|
+
|
|
76
|
+
content = yaml.dump(effective, default_flow_style=False, sort_keys=False)
|
|
77
|
+
|
|
78
|
+
if raw:
|
|
79
|
+
console.print(content, end="")
|
|
80
|
+
else:
|
|
81
|
+
console.print("[bold]Forge Runtime Config[/bold]")
|
|
82
|
+
console.print(f"[bold]Path:[/bold] {display_path(config_path)}")
|
|
83
|
+
if env_sources:
|
|
84
|
+
overrides = ", ".join(f"{v}={k}" for k, v in env_sources.items())
|
|
85
|
+
console.print(f"[bold]Env overrides:[/bold] {overrides}")
|
|
86
|
+
console.print()
|
|
87
|
+
syntax = Syntax(content, "yaml", theme="monokai")
|
|
88
|
+
console.print(syntax)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@config.command("set")
|
|
92
|
+
@click.argument("key_value")
|
|
93
|
+
def set_cmd(key_value: str) -> None:
|
|
94
|
+
"""Set a configuration value.
|
|
95
|
+
|
|
96
|
+
\b
|
|
97
|
+
Examples:
|
|
98
|
+
forge config set proxy_mode=sidecar
|
|
99
|
+
forge config set status_timeout=0.5
|
|
100
|
+
forge config set context_limit=1000000
|
|
101
|
+
"""
|
|
102
|
+
console = Console(width=200)
|
|
103
|
+
|
|
104
|
+
if "=" not in key_value:
|
|
105
|
+
console.print(f"[red]Error:[/red] Expected format: key=value (got: {key_value})")
|
|
106
|
+
sys.exit(1)
|
|
107
|
+
|
|
108
|
+
key, value = key_value.split("=", 1)
|
|
109
|
+
|
|
110
|
+
known_fields = {f.name: f for f in fields(RuntimeConfig)}
|
|
111
|
+
if key not in known_fields:
|
|
112
|
+
console.print(f"[red]Error:[/red] Unknown config key: '{key}'")
|
|
113
|
+
console.print(f"\n[dim]Available keys: {', '.join(sorted(known_fields))}[/dim]")
|
|
114
|
+
sys.exit(1)
|
|
115
|
+
|
|
116
|
+
coerced_value: Any = _coerce_value(key, value, known_fields[key])
|
|
117
|
+
if coerced_value is _COERCE_ERROR:
|
|
118
|
+
console.print(f"[red]Error:[/red] Invalid value for '{key}': {value}")
|
|
119
|
+
sys.exit(1)
|
|
120
|
+
|
|
121
|
+
config_path = get_config_path()
|
|
122
|
+
if config_path.is_file():
|
|
123
|
+
from ruamel.yaml import YAML
|
|
124
|
+
|
|
125
|
+
ruamel = YAML()
|
|
126
|
+
ruamel.preserve_quotes = True
|
|
127
|
+
with open(config_path) as f:
|
|
128
|
+
data = ruamel.load(f) or {}
|
|
129
|
+
else:
|
|
130
|
+
data = {}
|
|
131
|
+
|
|
132
|
+
data[key] = coerced_value
|
|
133
|
+
|
|
134
|
+
try:
|
|
135
|
+
RuntimeConfig(**{k: v for k, v in dict(data).items() if k in known_fields})
|
|
136
|
+
except (ValueError, TypeError) as e:
|
|
137
|
+
console.print(f"[red]Error:[/red] Invalid configuration: {e}")
|
|
138
|
+
sys.exit(1)
|
|
139
|
+
|
|
140
|
+
write_runtime_config(data)
|
|
141
|
+
console.print(f"[green]Set[/green] {key}={coerced_value}")
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
@config.command("edit")
|
|
145
|
+
def edit_cmd() -> None:
|
|
146
|
+
"""Open runtime configuration in $EDITOR.
|
|
147
|
+
|
|
148
|
+
Creates the file with defaults if it doesn't exist.
|
|
149
|
+
Validates changes before applying.
|
|
150
|
+
"""
|
|
151
|
+
console = Console(width=200)
|
|
152
|
+
|
|
153
|
+
config_path = ensure_config()
|
|
154
|
+
editor = os.environ.get("EDITOR", "vim")
|
|
155
|
+
|
|
156
|
+
if not shutil.which(editor):
|
|
157
|
+
console.print(f"[red]Error:[/red] Editor '{editor}' not found. Set $EDITOR to an available editor.")
|
|
158
|
+
sys.exit(1)
|
|
159
|
+
|
|
160
|
+
# Copy to temp file for safe editing
|
|
161
|
+
with tempfile.NamedTemporaryFile(mode="w", suffix=".yaml", delete=False) as tmp:
|
|
162
|
+
tmp.write(config_path.read_text())
|
|
163
|
+
tmp_path = Path(tmp.name)
|
|
164
|
+
|
|
165
|
+
success = False
|
|
166
|
+
try:
|
|
167
|
+
result = subprocess.run([editor, str(tmp_path)])
|
|
168
|
+
if result.returncode != 0:
|
|
169
|
+
console.print(f"[red]Error:[/red] Editor exited with code {result.returncode}")
|
|
170
|
+
console.print(f"Your changes are saved at: {display_path(tmp_path)}")
|
|
171
|
+
sys.exit(1)
|
|
172
|
+
|
|
173
|
+
# Validate edited YAML (use ruamel for consistency with write path)
|
|
174
|
+
from ruamel.yaml import YAML
|
|
175
|
+
|
|
176
|
+
ruamel = YAML()
|
|
177
|
+
try:
|
|
178
|
+
with open(tmp_path) as f:
|
|
179
|
+
edited_data = ruamel.load(f)
|
|
180
|
+
except Exception as e:
|
|
181
|
+
console.print(f"[red]Error:[/red] Invalid YAML: {e}")
|
|
182
|
+
console.print(f"Your changes are saved at: {display_path(tmp_path)}")
|
|
183
|
+
sys.exit(1)
|
|
184
|
+
|
|
185
|
+
if edited_data is None:
|
|
186
|
+
edited_data = {}
|
|
187
|
+
|
|
188
|
+
if not isinstance(edited_data, dict):
|
|
189
|
+
console.print("[red]Error:[/red] Config must be a YAML mapping")
|
|
190
|
+
console.print(f"Your changes are saved at: {display_path(tmp_path)}")
|
|
191
|
+
sys.exit(1)
|
|
192
|
+
|
|
193
|
+
known_fields = {f.name for f in fields(RuntimeConfig)}
|
|
194
|
+
try:
|
|
195
|
+
RuntimeConfig(**{k: v for k, v in dict(edited_data).items() if k in known_fields})
|
|
196
|
+
except (ValueError, TypeError) as e:
|
|
197
|
+
console.print(f"[red]Error:[/red] Invalid configuration: {e}")
|
|
198
|
+
console.print(f"Your changes are saved at: {display_path(tmp_path)}")
|
|
199
|
+
sys.exit(1)
|
|
200
|
+
|
|
201
|
+
write_runtime_config(dict(edited_data))
|
|
202
|
+
success = True
|
|
203
|
+
console.print("[green]Updated[/green] runtime configuration")
|
|
204
|
+
|
|
205
|
+
finally:
|
|
206
|
+
if success and tmp_path.exists():
|
|
207
|
+
try:
|
|
208
|
+
tmp_path.unlink()
|
|
209
|
+
except OSError:
|
|
210
|
+
pass
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
@config.command("reset")
|
|
214
|
+
@click.argument("key", required=False)
|
|
215
|
+
@click.option("--yes", "-y", is_flag=True, help="Skip confirmation prompt")
|
|
216
|
+
@click.option("--force", "-f", is_flag=True, hidden=True, help="Deprecated alias for --yes")
|
|
217
|
+
def reset_cmd(key: str | None = None, yes: bool = False, force: bool = False) -> None:
|
|
218
|
+
"""Reset configuration to defaults.
|
|
219
|
+
|
|
220
|
+
With KEY: removes that key (reverts to built-in default).
|
|
221
|
+
Without KEY: deletes the entire config file.
|
|
222
|
+
"""
|
|
223
|
+
yes = yes or force
|
|
224
|
+
console = Console(width=200)
|
|
225
|
+
config_path = get_config_path()
|
|
226
|
+
|
|
227
|
+
if not config_path.is_file():
|
|
228
|
+
console.print("[dim]No config file to reset (already using defaults).[/dim]")
|
|
229
|
+
return
|
|
230
|
+
|
|
231
|
+
if key is None:
|
|
232
|
+
if not yes:
|
|
233
|
+
if not click.confirm("Reset all configuration to defaults?"):
|
|
234
|
+
console.print("[dim]Cancelled.[/dim]")
|
|
235
|
+
return
|
|
236
|
+
config_path.unlink()
|
|
237
|
+
reset_runtime_config()
|
|
238
|
+
console.print("[green]Reset[/green] all configuration to defaults")
|
|
239
|
+
console.print(f"[dim]Removed {display_path(config_path)}[/dim]")
|
|
240
|
+
return
|
|
241
|
+
|
|
242
|
+
known_fields = {f.name for f in fields(RuntimeConfig)}
|
|
243
|
+
if key not in known_fields:
|
|
244
|
+
console.print(f"[red]Error:[/red] Unknown config key: '{key}'")
|
|
245
|
+
console.print(f"\n[dim]Available keys: {', '.join(sorted(known_fields))}[/dim]")
|
|
246
|
+
sys.exit(1)
|
|
247
|
+
|
|
248
|
+
from ruamel.yaml import YAML
|
|
249
|
+
|
|
250
|
+
ruamel = YAML()
|
|
251
|
+
ruamel.preserve_quotes = True
|
|
252
|
+
with open(config_path) as f:
|
|
253
|
+
data = ruamel.load(f) or {}
|
|
254
|
+
|
|
255
|
+
if key not in data:
|
|
256
|
+
console.print(f"[dim]Key '{key}' not in config (already using default).[/dim]")
|
|
257
|
+
return
|
|
258
|
+
|
|
259
|
+
del data[key]
|
|
260
|
+
|
|
261
|
+
if data:
|
|
262
|
+
write_runtime_config(dict(data))
|
|
263
|
+
else:
|
|
264
|
+
config_path.unlink()
|
|
265
|
+
reset_runtime_config()
|
|
266
|
+
|
|
267
|
+
default_val = getattr(RuntimeConfig(), key)
|
|
268
|
+
console.print(f"[green]Reset[/green] {key} (default: {default_val})")
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
# --- Helpers ---
|
|
272
|
+
|
|
273
|
+
_COERCE_ERROR = object()
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _coerce_value(key: str, value: str, field_info: Any) -> Any:
|
|
277
|
+
"""Coerce string CLI value to the field's expected Python type."""
|
|
278
|
+
field_type = field_info.type
|
|
279
|
+
|
|
280
|
+
# Compare actual types (not string representations)
|
|
281
|
+
# With `from __future__ import annotations`, field.type is a string,
|
|
282
|
+
# so we need to resolve it
|
|
283
|
+
if field_type is int or field_type == "int":
|
|
284
|
+
try:
|
|
285
|
+
return int(value)
|
|
286
|
+
except ValueError:
|
|
287
|
+
return _COERCE_ERROR
|
|
288
|
+
|
|
289
|
+
if field_type is float or field_type == "float":
|
|
290
|
+
try:
|
|
291
|
+
return float(value)
|
|
292
|
+
except ValueError:
|
|
293
|
+
return _COERCE_ERROR
|
|
294
|
+
|
|
295
|
+
if field_type is bool or field_type == "bool":
|
|
296
|
+
if value.lower() in ("true", "1", "yes", "on"):
|
|
297
|
+
return True
|
|
298
|
+
if value.lower() in ("false", "0", "no", "off"):
|
|
299
|
+
return False
|
|
300
|
+
return _COERCE_ERROR
|
|
301
|
+
|
|
302
|
+
# String fields: pass through
|
|
303
|
+
return value
|