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.
Files changed (71) hide show
  1. canopy/__init__.py +2 -0
  2. canopy/actions/__init__.py +32 -0
  3. canopy/actions/aliases.py +421 -0
  4. canopy/actions/augments.py +55 -0
  5. canopy/actions/bootstrap.py +249 -0
  6. canopy/actions/bot_resolutions.py +123 -0
  7. canopy/actions/bot_status.py +133 -0
  8. canopy/actions/commit.py +511 -0
  9. canopy/actions/conflicts.py +314 -0
  10. canopy/actions/doctor.py +1459 -0
  11. canopy/actions/draft_replies.py +185 -0
  12. canopy/actions/drift.py +241 -0
  13. canopy/actions/errors.py +115 -0
  14. canopy/actions/evacuate.py +192 -0
  15. canopy/actions/feature_state.py +607 -0
  16. canopy/actions/historian.py +612 -0
  17. canopy/actions/ide_workspace.py +49 -0
  18. canopy/actions/last_visit.py +83 -0
  19. canopy/actions/migrate_slots.py +313 -0
  20. canopy/actions/preflight_state.py +97 -0
  21. canopy/actions/push.py +199 -0
  22. canopy/actions/reads.py +304 -0
  23. canopy/actions/resume.py +582 -0
  24. canopy/actions/review_filter.py +135 -0
  25. canopy/actions/ship.py +399 -0
  26. canopy/actions/slot_details.py +208 -0
  27. canopy/actions/slot_load.py +383 -0
  28. canopy/actions/slots.py +221 -0
  29. canopy/actions/stash.py +230 -0
  30. canopy/actions/switch.py +775 -0
  31. canopy/actions/switch_preflight.py +192 -0
  32. canopy/actions/thread_actions.py +88 -0
  33. canopy/actions/thread_resolutions.py +101 -0
  34. canopy/actions/triage.py +286 -0
  35. canopy/agent/__init__.py +5 -0
  36. canopy/agent/runner.py +129 -0
  37. canopy/agent_setup/__init__.py +264 -0
  38. canopy/agent_setup/skills/augment-canopy/SKILL.md +116 -0
  39. canopy/agent_setup/skills/using-canopy/SKILL.md +191 -0
  40. canopy/cli/__init__.py +0 -0
  41. canopy/cli/main.py +4152 -0
  42. canopy/cli/render.py +98 -0
  43. canopy/cli/ui.py +150 -0
  44. canopy/features/__init__.py +2 -0
  45. canopy/features/coordinator.py +1256 -0
  46. canopy/git/__init__.py +0 -0
  47. canopy/git/hooks.py +173 -0
  48. canopy/git/multi.py +435 -0
  49. canopy/git/repo.py +859 -0
  50. canopy/git/templates/post-checkout.py +67 -0
  51. canopy/graph/__init__.py +0 -0
  52. canopy/integrations/__init__.py +0 -0
  53. canopy/integrations/github.py +983 -0
  54. canopy/integrations/linear.py +307 -0
  55. canopy/integrations/precommit.py +239 -0
  56. canopy/mcp/__init__.py +0 -0
  57. canopy/mcp/client.py +329 -0
  58. canopy/mcp/server.py +1797 -0
  59. canopy/providers/__init__.py +105 -0
  60. canopy/providers/github_issues.py +289 -0
  61. canopy/providers/linear.py +341 -0
  62. canopy/providers/types.py +149 -0
  63. canopy/workspace/__init__.py +4 -0
  64. canopy/workspace/config.py +378 -0
  65. canopy/workspace/context.py +224 -0
  66. canopy/workspace/discovery.py +197 -0
  67. canopy/workspace/workspace.py +173 -0
  68. canopy_cli-3.1.0.dist-info/METADATA +282 -0
  69. canopy_cli-3.1.0.dist-info/RECORD +71 -0
  70. canopy_cli-3.1.0.dist-info/WHEEL +4 -0
  71. 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}[/]")
@@ -0,0 +1,2 @@
1
+ """Feature lane coordination."""
2
+ from .coordinator import FeatureLane, FeatureCoordinator