canopy-cli 3.1.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.
- canopy/__init__.py +2 -0
- canopy/actions/__init__.py +32 -0
- canopy/actions/aliases.py +421 -0
- canopy/actions/augments.py +55 -0
- canopy/actions/bootstrap.py +249 -0
- canopy/actions/bot_resolutions.py +123 -0
- canopy/actions/bot_status.py +133 -0
- canopy/actions/commit.py +511 -0
- canopy/actions/conflicts.py +314 -0
- canopy/actions/doctor.py +1459 -0
- canopy/actions/draft_replies.py +185 -0
- canopy/actions/drift.py +241 -0
- canopy/actions/errors.py +115 -0
- canopy/actions/evacuate.py +192 -0
- canopy/actions/feature_state.py +607 -0
- canopy/actions/historian.py +612 -0
- canopy/actions/ide_workspace.py +49 -0
- canopy/actions/last_visit.py +83 -0
- canopy/actions/migrate_slots.py +313 -0
- canopy/actions/preflight_state.py +97 -0
- canopy/actions/push.py +199 -0
- canopy/actions/reads.py +304 -0
- canopy/actions/resume.py +582 -0
- canopy/actions/review_filter.py +135 -0
- canopy/actions/ship.py +399 -0
- canopy/actions/slot_details.py +208 -0
- canopy/actions/slot_load.py +383 -0
- canopy/actions/slots.py +221 -0
- canopy/actions/stash.py +230 -0
- canopy/actions/switch.py +775 -0
- canopy/actions/switch_preflight.py +192 -0
- canopy/actions/thread_actions.py +88 -0
- canopy/actions/thread_resolutions.py +101 -0
- canopy/actions/triage.py +286 -0
- canopy/agent/__init__.py +5 -0
- canopy/agent/runner.py +129 -0
- canopy/agent_setup/__init__.py +264 -0
- canopy/agent_setup/skills/augment-canopy/SKILL.md +116 -0
- canopy/agent_setup/skills/using-canopy/SKILL.md +191 -0
- canopy/cli/__init__.py +0 -0
- canopy/cli/main.py +4152 -0
- canopy/cli/render.py +98 -0
- canopy/cli/ui.py +150 -0
- canopy/features/__init__.py +2 -0
- canopy/features/coordinator.py +1256 -0
- canopy/git/__init__.py +0 -0
- canopy/git/hooks.py +173 -0
- canopy/git/multi.py +435 -0
- canopy/git/repo.py +859 -0
- canopy/git/templates/post-checkout.py +67 -0
- canopy/graph/__init__.py +0 -0
- canopy/integrations/__init__.py +0 -0
- canopy/integrations/github.py +983 -0
- canopy/integrations/linear.py +307 -0
- canopy/integrations/precommit.py +239 -0
- canopy/mcp/__init__.py +0 -0
- canopy/mcp/client.py +329 -0
- canopy/mcp/server.py +1797 -0
- canopy/providers/__init__.py +105 -0
- canopy/providers/github_issues.py +289 -0
- canopy/providers/linear.py +341 -0
- canopy/providers/types.py +149 -0
- canopy/workspace/__init__.py +4 -0
- canopy/workspace/config.py +378 -0
- canopy/workspace/context.py +224 -0
- canopy/workspace/discovery.py +197 -0
- canopy/workspace/workspace.py +173 -0
- canopy_cli-3.1.0.dist-info/METADATA +282 -0
- canopy_cli-3.1.0.dist-info/RECORD +71 -0
- canopy_cli-3.1.0.dist-info/WHEEL +4 -0
- canopy_cli-3.1.0.dist-info/entry_points.txt +3 -0
canopy/cli/render.py
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
"""CLI rendering for action errors and results.
|
|
2
|
+
|
|
3
|
+
Mirrors the structured shape from canopy.actions.errors so the human-facing
|
|
4
|
+
output and the agent-facing JSON describe the same thing. The renderer
|
|
5
|
+
accepts either a live ``ActionError`` instance or a serialized ``dict``
|
|
6
|
+
(useful when the source is an MCP response).
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import Any
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
|
|
14
|
+
from ..actions.errors import ActionError
|
|
15
|
+
from .ui import console as default_console
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
_STATUS_GLYPH = {"blocked": "✗", "failed": "✗"}
|
|
19
|
+
_STATUS_LABEL = {"blocked": "blocked", "failed": "failed"}
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def render_blocker(
|
|
23
|
+
err: ActionError | dict[str, Any],
|
|
24
|
+
*,
|
|
25
|
+
action: str | None = None,
|
|
26
|
+
console: Console | None = None,
|
|
27
|
+
) -> None:
|
|
28
|
+
"""Render a structured error to the console.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
err: an ``ActionError`` or a ``to_dict()``-shaped dict.
|
|
32
|
+
action: name of the action that produced the error (e.g., ``"ship"``).
|
|
33
|
+
Used for the header line. Optional but recommended.
|
|
34
|
+
console: rich Console to write to. Defaults to canopy's themed one.
|
|
35
|
+
"""
|
|
36
|
+
out = console or default_console
|
|
37
|
+
payload = err.to_dict() if isinstance(err, ActionError) else dict(err)
|
|
38
|
+
|
|
39
|
+
status = payload.get("status", "failed")
|
|
40
|
+
code = payload.get("code", "unknown")
|
|
41
|
+
what = payload.get("what", "")
|
|
42
|
+
glyph = _STATUS_GLYPH.get(status, "✗")
|
|
43
|
+
label = _STATUS_LABEL.get(status, status)
|
|
44
|
+
head = f"{action or 'action'} {label}: {what}" if what else f"{action or 'action'} {label}: {code}"
|
|
45
|
+
|
|
46
|
+
out.print()
|
|
47
|
+
out.print(f" [error]{glyph}[/] {head} [muted]({code})[/]")
|
|
48
|
+
|
|
49
|
+
expected = payload.get("expected")
|
|
50
|
+
actual = payload.get("actual")
|
|
51
|
+
if expected is not None or actual is not None:
|
|
52
|
+
out.print()
|
|
53
|
+
if expected is not None:
|
|
54
|
+
out.print(f" [muted]expected:[/] {_fmt_value(expected)}")
|
|
55
|
+
if actual is not None:
|
|
56
|
+
out.print(f" [muted]actual: [/] {_fmt_value(actual)}")
|
|
57
|
+
|
|
58
|
+
fix_actions = payload.get("fix_actions") or []
|
|
59
|
+
if fix_actions:
|
|
60
|
+
out.print()
|
|
61
|
+
out.print(" [header]fix:[/]")
|
|
62
|
+
for fa in fix_actions:
|
|
63
|
+
label_parts = [f"canopy {fa['action']}"]
|
|
64
|
+
args = fa.get("args") or {}
|
|
65
|
+
for k, v in args.items():
|
|
66
|
+
if k == "feature":
|
|
67
|
+
label_parts.append(str(v))
|
|
68
|
+
elif isinstance(v, bool) and v:
|
|
69
|
+
label_parts.append(f"--{k}")
|
|
70
|
+
elif not isinstance(v, bool):
|
|
71
|
+
label_parts.append(f"--{k} {v}")
|
|
72
|
+
cmd = " ".join(label_parts)
|
|
73
|
+
tag = "[muted](safe)[/]" if fa.get("safe") else "[warning](needs review)[/]"
|
|
74
|
+
out.print(f" [info]{cmd}[/] {tag}")
|
|
75
|
+
preview = fa.get("preview")
|
|
76
|
+
if preview:
|
|
77
|
+
out.print(f" [muted]{preview}[/]")
|
|
78
|
+
|
|
79
|
+
details = payload.get("details") or {}
|
|
80
|
+
if details:
|
|
81
|
+
out.print()
|
|
82
|
+
out.print(" [muted]details:[/]")
|
|
83
|
+
for k, v in details.items():
|
|
84
|
+
out.print(f" [muted]{k}:[/] {_fmt_value(v)}")
|
|
85
|
+
out.print()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _fmt_value(v: Any) -> str:
|
|
89
|
+
"""Render a value compactly. Dicts inline as key=value pairs, lists as commas."""
|
|
90
|
+
if isinstance(v, dict):
|
|
91
|
+
if not v:
|
|
92
|
+
return "{}"
|
|
93
|
+
return ", ".join(f"{k}={_fmt_value(val)}" for k, val in v.items())
|
|
94
|
+
if isinstance(v, (list, tuple)):
|
|
95
|
+
if not v:
|
|
96
|
+
return "[]"
|
|
97
|
+
return ", ".join(_fmt_value(x) for x in v)
|
|
98
|
+
return str(v)
|
canopy/cli/ui.py
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Rich CLI UI helpers for canopy.
|
|
3
|
+
|
|
4
|
+
Provides consistent styling, spinners, and formatting across all commands.
|
|
5
|
+
All human-readable output goes through these helpers. --json output bypasses them.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
from typing import Generator
|
|
11
|
+
|
|
12
|
+
from rich.console import Console
|
|
13
|
+
from rich.theme import Theme
|
|
14
|
+
from rich.tree import Tree
|
|
15
|
+
from rich.table import Table
|
|
16
|
+
from rich.panel import Panel
|
|
17
|
+
from rich.text import Text
|
|
18
|
+
from rich.spinner import Spinner
|
|
19
|
+
from rich.live import Live
|
|
20
|
+
from rich.status import Status
|
|
21
|
+
|
|
22
|
+
# ── Theme ────────────────────────────────────────────────────────────────
|
|
23
|
+
|
|
24
|
+
CANOPY_THEME = Theme({
|
|
25
|
+
"header": "bold", # weight, not color — let default-fg shine
|
|
26
|
+
"subheader": "bold dim",
|
|
27
|
+
"repo": "bold",
|
|
28
|
+
"branch": "color(180)", # muted khaki
|
|
29
|
+
"feature": "bold color(218)", # soft pink — primary accent
|
|
30
|
+
"path": "color(245)", # warm grey
|
|
31
|
+
"dirty": "color(218)", # soft pink (dirty ≠ error)
|
|
32
|
+
"clean": "dim",
|
|
33
|
+
"ahead": "color(151)", # sage
|
|
34
|
+
"behind": "color(174)", # dusty rose
|
|
35
|
+
"info": "dim",
|
|
36
|
+
"success": "color(151)", # sage
|
|
37
|
+
"warning": "color(218)", # soft pink
|
|
38
|
+
"error": "bold color(174)", # dusty rose
|
|
39
|
+
"muted": "color(244)", # warm grey, slightly darker
|
|
40
|
+
"linear": "color(146)", # muted lavender
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
console = Console(theme=CANOPY_THEME)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# ── Symbols ──────────────────────────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
SYM_CHECK = "✓"
|
|
49
|
+
SYM_CROSS = "✗"
|
|
50
|
+
SYM_DOT = "●"
|
|
51
|
+
SYM_ARROW = "→"
|
|
52
|
+
SYM_TREE = "├──"
|
|
53
|
+
SYM_TREE_LAST = "└──"
|
|
54
|
+
SYM_BRANCH = "⎇"
|
|
55
|
+
SYM_DIRTY = "◌"
|
|
56
|
+
SYM_CLEAN = "◉"
|
|
57
|
+
SYM_LINK = "↗"
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ── Spinners ─────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
@contextmanager
|
|
63
|
+
def spinner(message: str) -> Generator[Status, None, None]:
|
|
64
|
+
"""Show a spinner while work is in progress."""
|
|
65
|
+
with console.status(f"[info]{message}[/]", spinner="dots") as status:
|
|
66
|
+
yield status
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
# ── Formatters ───────────────────────────────────────────────────────────
|
|
70
|
+
|
|
71
|
+
def status_badge(dirty: bool, dirty_count: int = 0) -> str:
|
|
72
|
+
"""Format a dirty/clean status badge."""
|
|
73
|
+
if dirty:
|
|
74
|
+
label = f"{dirty_count} dirty" if dirty_count else "dirty"
|
|
75
|
+
return f"[dirty]{SYM_DIRTY} {label}[/]"
|
|
76
|
+
return f"[clean]{SYM_CLEAN} clean[/]"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def divergence_str(ahead: int = 0, behind: int = 0) -> str:
|
|
80
|
+
"""Format ahead/behind as colored arrows."""
|
|
81
|
+
parts = []
|
|
82
|
+
if ahead:
|
|
83
|
+
parts.append(f"[ahead]↑{ahead}[/]")
|
|
84
|
+
if behind:
|
|
85
|
+
parts.append(f"[behind]↓{behind}[/]")
|
|
86
|
+
return " ".join(parts)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def repo_line(name: str, branch: str = "", dirty: bool = False,
|
|
90
|
+
dirty_count: int = 0, ahead: int = 0, behind: int = 0,
|
|
91
|
+
path: str = "") -> Text:
|
|
92
|
+
"""Build a formatted line for a repo entry."""
|
|
93
|
+
text = Text()
|
|
94
|
+
text.append(f" {name}", style="repo")
|
|
95
|
+
if branch:
|
|
96
|
+
text.append(f" {SYM_BRANCH} ", style="muted")
|
|
97
|
+
text.append(branch, style="branch")
|
|
98
|
+
|
|
99
|
+
parts = []
|
|
100
|
+
if dirty:
|
|
101
|
+
label = f"{dirty_count} dirty" if dirty_count else "dirty"
|
|
102
|
+
parts.append(f"[dirty]{label}[/]")
|
|
103
|
+
if ahead:
|
|
104
|
+
parts.append(f"[ahead]+{ahead}[/]")
|
|
105
|
+
if behind:
|
|
106
|
+
parts.append(f"[behind]-{behind}[/]")
|
|
107
|
+
if parts:
|
|
108
|
+
text.append(" ")
|
|
109
|
+
# Can't use rich markup inside Text.append, so use plain
|
|
110
|
+
text.append("(", style="muted")
|
|
111
|
+
for i, p in enumerate(parts):
|
|
112
|
+
if "dirty" in p:
|
|
113
|
+
text.append(p.replace("[dirty]", "").replace("[/]", ""), style="dirty")
|
|
114
|
+
elif "+" in p:
|
|
115
|
+
text.append(p.replace("[ahead]", "").replace("[/]", ""), style="ahead")
|
|
116
|
+
elif "-" in p:
|
|
117
|
+
text.append(p.replace("[behind]", "").replace("[/]", ""), style="behind")
|
|
118
|
+
if i < len(parts) - 1:
|
|
119
|
+
text.append(", ", style="muted")
|
|
120
|
+
text.append(")", style="muted")
|
|
121
|
+
|
|
122
|
+
if path:
|
|
123
|
+
text.append(f"\n {path}", style="path")
|
|
124
|
+
return text
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def section_header(title: str) -> None:
|
|
128
|
+
"""Print a section header."""
|
|
129
|
+
console.print()
|
|
130
|
+
console.print(f" [header]{title}[/]")
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def print_success(message: str) -> None:
|
|
134
|
+
"""Print a success message with checkmark."""
|
|
135
|
+
console.print(f" [success]{SYM_CHECK}[/] {message}")
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def print_warning(message: str) -> None:
|
|
139
|
+
"""Print a warning message."""
|
|
140
|
+
console.print(f" [warning]![/] {message}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def print_error(message: str) -> None:
|
|
144
|
+
"""Print an error message."""
|
|
145
|
+
console.print(f" [error]{SYM_CROSS}[/] {message}")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def separator() -> None:
|
|
149
|
+
"""Print a dim separator line."""
|
|
150
|
+
console.print(f" [muted]{'─' * 52}[/]")
|