redgit 1.2.0__tar.gz → 1.2.2__tar.gz

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 (53) hide show
  1. {redgit-1.2.0/redgit.egg-info → redgit-1.2.2}/PKG-INFO +2 -2
  2. {redgit-1.2.0 → redgit-1.2.2}/README.md +1 -1
  3. {redgit-1.2.0 → redgit-1.2.2}/pyproject.toml +1 -1
  4. redgit-1.2.2/redgit/__init__.py +1 -0
  5. {redgit-1.2.0 → redgit-1.2.2}/redgit/cli.py +11 -3
  6. redgit-1.2.2/redgit/commands/daily.py +342 -0
  7. {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/integration.py +91 -0
  8. {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/propose.py +537 -33
  9. {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/push.py +138 -14
  10. {redgit-1.2.0 → redgit-1.2.2}/redgit/core/config.py +81 -1
  11. redgit-1.2.2/redgit/core/daily_state.py +114 -0
  12. {redgit-1.2.0 → redgit-1.2.2}/redgit/core/gitops.py +385 -10
  13. {redgit-1.2.0 → redgit-1.2.2}/redgit/core/prompt.py +1 -1
  14. {redgit-1.2.0 → redgit-1.2.2}/redgit/integrations/base.py +4 -2
  15. {redgit-1.2.0 → redgit-1.2.2}/redgit/plugins/registry.py +21 -11
  16. redgit-1.2.2/redgit/prompts/daily/default.md +36 -0
  17. {redgit-1.2.0 → redgit-1.2.2/redgit.egg-info}/PKG-INFO +2 -2
  18. {redgit-1.2.0 → redgit-1.2.2}/redgit.egg-info/SOURCES.txt +3 -0
  19. redgit-1.2.0/redgit/__init__.py +0 -1
  20. {redgit-1.2.0 → redgit-1.2.2}/LICENSE +0 -0
  21. {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/__init__.py +0 -0
  22. {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/ci.py +0 -0
  23. {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/config.py +0 -0
  24. {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/init.py +0 -0
  25. {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/notify.py +0 -0
  26. {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/plugin.py +0 -0
  27. {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/quality.py +0 -0
  28. {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/scout.py +0 -0
  29. {redgit-1.2.0 → redgit-1.2.2}/redgit/commands/tap.py +0 -0
  30. {redgit-1.2.0 → redgit-1.2.2}/redgit/core/llm.py +0 -0
  31. {redgit-1.2.0 → redgit-1.2.2}/redgit/core/llm_providers.json +0 -0
  32. {redgit-1.2.0 → redgit-1.2.2}/redgit/core/scout/__init__.py +0 -0
  33. {redgit-1.2.0 → redgit-1.2.2}/redgit/core/scout/team.py +0 -0
  34. {redgit-1.2.0 → redgit-1.2.2}/redgit/core/semgrep.py +0 -0
  35. {redgit-1.2.0 → redgit-1.2.2}/redgit/core/tap.py +0 -0
  36. {redgit-1.2.0 → redgit-1.2.2}/redgit/integrations/__init__.py +0 -0
  37. {redgit-1.2.0 → redgit-1.2.2}/redgit/integrations/registry.py +0 -0
  38. {redgit-1.2.0 → redgit-1.2.2}/redgit/plugins/__init__.py +0 -0
  39. {redgit-1.2.0 → redgit-1.2.2}/redgit/plugins/base.py +0 -0
  40. {redgit-1.2.0 → redgit-1.2.2}/redgit/prompts/__init__.py +0 -0
  41. {redgit-1.2.0 → redgit-1.2.2}/redgit/prompts/commit/default.md +0 -0
  42. {redgit-1.2.0 → redgit-1.2.2}/redgit/prompts/commit/minimal.md +0 -0
  43. {redgit-1.2.0 → redgit-1.2.2}/redgit/prompts/quality/default.md +0 -0
  44. {redgit-1.2.0 → redgit-1.2.2}/redgit/splash.py +0 -0
  45. {redgit-1.2.0 → redgit-1.2.2}/redgit/utils/__init__.py +0 -0
  46. {redgit-1.2.0 → redgit-1.2.2}/redgit/utils/console.py +0 -0
  47. {redgit-1.2.0 → redgit-1.2.2}/redgit/utils/editor.py +0 -0
  48. {redgit-1.2.0 → redgit-1.2.2}/redgit/utils/security.py +0 -0
  49. {redgit-1.2.0 → redgit-1.2.2}/redgit.egg-info/dependency_links.txt +0 -0
  50. {redgit-1.2.0 → redgit-1.2.2}/redgit.egg-info/entry_points.txt +0 -0
  51. {redgit-1.2.0 → redgit-1.2.2}/redgit.egg-info/requires.txt +0 -0
  52. {redgit-1.2.0 → redgit-1.2.2}/redgit.egg-info/top_level.txt +0 -0
  53. {redgit-1.2.0 → redgit-1.2.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: redgit
3
- Version: 1.2.0
3
+ Version: 1.2.2
4
4
  Summary: AI-powered Git workflow assistant with task management integration
5
5
  Author-email: Your Name <your.email@example.com>
6
6
  License-Expression: MIT
@@ -34,7 +34,7 @@ Requires-Dist: ruff>=0.4.0
34
34
  Dynamic: license-file
35
35
 
36
36
  <p align="center">
37
- <img src="https://raw.githubusercontent.com/ertiz82/redgit/main/assets/logo.svg?v=1.2.0" alt="RedGit Logo" width="400"/>
37
+ <img src="https://raw.githubusercontent.com/ertiz82/redgit/main/assets/logo.svg?v=1.2.2" alt="RedGit Logo" width="400"/>
38
38
  </p>
39
39
 
40
40
  <p align="center">
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/ertiz82/redgit/main/assets/logo.svg?v=1.2.0" alt="RedGit Logo" width="400"/>
2
+ <img src="https://raw.githubusercontent.com/ertiz82/redgit/main/assets/logo.svg?v=1.2.2" alt="RedGit Logo" width="400"/>
3
3
  </p>
4
4
 
5
5
  <p align="center">
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "redgit"
7
- version = "1.2.0"
7
+ version = "1.2.2"
8
8
  description = "AI-powered Git workflow assistant with task management integration"
9
9
  readme = "README.md"
10
10
  license = "MIT"
@@ -0,0 +1 @@
1
+ __version__ = "1.2.2"
@@ -8,6 +8,7 @@ from redgit.splash import splash
8
8
  from redgit.commands.init import init_cmd
9
9
  from redgit.commands.propose import propose_cmd
10
10
  from redgit.commands.push import push_cmd
11
+ from redgit.commands.daily import daily_cmd
11
12
  from redgit.commands.integration import integration_app
12
13
  from redgit.commands.plugin import plugin_app
13
14
  from redgit.commands.tap import tap_app, install_cmd as tap_install_cmd, uninstall_cmd as tap_uninstall_cmd
@@ -48,6 +49,7 @@ def main_callback(
48
49
  app.command("init")(init_cmd)
49
50
  app.command("propose")(propose_cmd)
50
51
  app.command("push")(push_cmd)
52
+ app.command("daily")(daily_cmd)
51
53
  app.command("install")(tap_install_cmd)
52
54
  app.command("uninstall")(tap_uninstall_cmd)
53
55
  app.add_typer(integration_app, name="integration")
@@ -73,10 +75,16 @@ def _load_plugin_commands():
73
75
  for name, cmd_app in commands.items():
74
76
  app.add_typer(cmd_app, name=name)
75
77
 
76
- # Load plugin shortcuts (e.g., release_shortcut -> rg release)
78
+ # Load plugin shortcuts (e.g., release_shortcut -> rg release, release_app -> rg release)
79
+ import typer
77
80
  shortcuts = get_all_plugin_shortcuts(config)
78
- for name, cmd_func in shortcuts.items():
79
- app.command(name)(cmd_func)
81
+ for name, cmd in shortcuts.items():
82
+ if isinstance(cmd, typer.Typer):
83
+ # Typer app shortcut (e.g., release_app -> rg release with subcommands)
84
+ app.add_typer(cmd, name=name)
85
+ else:
86
+ # Function shortcut (e.g., release_shortcut -> rg release)
87
+ app.command(name)(cmd)
80
88
 
81
89
  except Exception:
82
90
  # Silently fail if config not found (e.g., before init)
@@ -0,0 +1,342 @@
1
+ """
2
+ Daily command - Generate daily activity report from git history.
3
+ """
4
+
5
+ import subprocess
6
+ from typing import Optional, List, Dict
7
+ from datetime import datetime
8
+ from pathlib import Path
9
+ from collections import defaultdict
10
+ import typer
11
+ from rich.console import Console
12
+ from rich.panel import Panel
13
+ from rich.table import Table
14
+
15
+ from ..core.config import ConfigManager, RETGIT_DIR
16
+ from ..core.daily_state import DailyStateManager
17
+ from ..core.gitops import GitOps, NotAGitRepoError
18
+ from ..core.llm import LLMClient
19
+
20
+ console = Console()
21
+
22
+ # Language display names
23
+ LANGUAGE_NAMES = {
24
+ "en": "English",
25
+ "tr": "Turkish",
26
+ "de": "German",
27
+ "fr": "French",
28
+ "es": "Spanish",
29
+ "it": "Italian",
30
+ "pt": "Portuguese",
31
+ "nl": "Dutch",
32
+ "ru": "Russian",
33
+ "zh": "Chinese",
34
+ "ja": "Japanese",
35
+ "ko": "Korean",
36
+ }
37
+
38
+
39
+ def get_commits_since(since: datetime, author: Optional[str] = None) -> List[Dict]:
40
+ """
41
+ Get commits since a given timestamp.
42
+
43
+ Returns list of dicts with commit info:
44
+ - hash: commit hash
45
+ - author: author name
46
+ - email: author email
47
+ - timestamp: unix timestamp
48
+ - date: formatted date
49
+ - message: commit message
50
+ - files: list of changed files with stats
51
+ """
52
+ since_str = since.strftime("%Y-%m-%dT%H:%M:%S")
53
+
54
+ # Build git log command
55
+ cmd = [
56
+ "git", "log",
57
+ f"--since={since_str}",
58
+ "--format=%H|%an|%ae|%at|%s",
59
+ "--numstat"
60
+ ]
61
+
62
+ if author:
63
+ cmd.append(f"--author={author}")
64
+
65
+ try:
66
+ result = subprocess.run(cmd, capture_output=True, text=True, check=True)
67
+ except subprocess.CalledProcessError as e:
68
+ console.print(f"[red]Git error: {e.stderr}[/red]")
69
+ return []
70
+
71
+ output = result.stdout.strip()
72
+ if not output:
73
+ return []
74
+
75
+ commits = []
76
+ current_commit = None
77
+
78
+ for line in output.split('\n'):
79
+ if not line:
80
+ continue
81
+
82
+ # Check if it's a commit line (has | separators)
83
+ if '|' in line and line.count('|') >= 4:
84
+ # Save previous commit
85
+ if current_commit:
86
+ commits.append(current_commit)
87
+
88
+ parts = line.split('|', 4)
89
+ if len(parts) >= 5:
90
+ timestamp = int(parts[3])
91
+ # Clean author name (remove literal \n if present)
92
+ author = parts[1].replace('\\n', '').strip()
93
+ current_commit = {
94
+ "hash": parts[0][:8], # Short hash
95
+ "author": author,
96
+ "email": parts[2],
97
+ "timestamp": timestamp,
98
+ "date": datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M"),
99
+ "message": parts[4],
100
+ "files": [],
101
+ "additions": 0,
102
+ "deletions": 0,
103
+ }
104
+ elif current_commit and '\t' in line:
105
+ # File stat line: additions\tdeletions\tfilename
106
+ parts = line.split('\t')
107
+ if len(parts) >= 3:
108
+ additions = int(parts[0]) if parts[0].isdigit() else 0
109
+ deletions = int(parts[1]) if parts[1].isdigit() else 0
110
+ filename = parts[2]
111
+ current_commit["files"].append({
112
+ "name": filename,
113
+ "additions": additions,
114
+ "deletions": deletions,
115
+ })
116
+ current_commit["additions"] += additions
117
+ current_commit["deletions"] += deletions
118
+
119
+ # Don't forget the last commit
120
+ if current_commit:
121
+ commits.append(current_commit)
122
+
123
+ return commits
124
+
125
+
126
+ def format_commits_for_llm(commits: List[Dict]) -> str:
127
+ """Format commits for LLM prompt."""
128
+ lines = []
129
+ for c in commits:
130
+ lines.append(f"- [{c['hash']}] {c['message']} (by {c['author']}, {c['date']})")
131
+ if c["files"]:
132
+ for f in c["files"][:5]: # Limit files per commit
133
+ lines.append(f" {f['name']} (+{f['additions']}/-{f['deletions']})")
134
+ if len(c["files"]) > 5:
135
+ lines.append(f" ... and {len(c['files']) - 5} more files")
136
+ return '\n'.join(lines)
137
+
138
+
139
+ def calculate_stats(commits: List[Dict]) -> Dict:
140
+ """Calculate aggregate statistics from commits."""
141
+ stats = {
142
+ "total_commits": len(commits),
143
+ "total_additions": 0,
144
+ "total_deletions": 0,
145
+ "total_files": 0,
146
+ "authors": set(),
147
+ "directories": defaultdict(int),
148
+ }
149
+
150
+ seen_files = set()
151
+
152
+ for c in commits:
153
+ stats["total_additions"] += c["additions"]
154
+ stats["total_deletions"] += c["deletions"]
155
+ stats["authors"].add(c["author"].strip())
156
+
157
+ for f in c["files"]:
158
+ if f["name"] not in seen_files:
159
+ seen_files.add(f["name"])
160
+ stats["total_files"] += 1
161
+
162
+ # Track directories
163
+ if '/' in f["name"]:
164
+ dir_name = f["name"].split('/')[0]
165
+ stats["directories"][dir_name] += 1
166
+
167
+ stats["authors"] = list(stats["authors"])
168
+ stats["directories"] = dict(sorted(
169
+ stats["directories"].items(),
170
+ key=lambda x: x[1],
171
+ reverse=True
172
+ )[:10]) # Top 10 directories
173
+
174
+ return stats
175
+
176
+
177
+ def load_daily_prompt(language: str) -> str:
178
+ """Load the daily prompt template."""
179
+ # Check project-specific first
180
+ project_prompt = RETGIT_DIR / "prompts" / "daily" / "default.md"
181
+ if project_prompt.exists():
182
+ return project_prompt.read_text()
183
+
184
+ # Fallback to builtin
185
+ builtin_prompt = Path(__file__).parent.parent / "prompts" / "daily" / "default.md"
186
+ if builtin_prompt.exists():
187
+ return builtin_prompt.read_text()
188
+
189
+ # Hardcoded fallback
190
+ return f"""Analyze these git commits and create a daily report in {language}:
191
+
192
+ {{{{COMMITS}}}}
193
+
194
+ Provide:
195
+ 1. Summary (2-3 sentences)
196
+ 2. Key changes (bullet points)
197
+ 3. Affected areas
198
+ """
199
+
200
+
201
+ def daily_cmd(
202
+ since: Optional[str] = typer.Option(
203
+ None, "--since", "-s",
204
+ help="Override start time (e.g., '24h', '2d', 'yesterday', '2024-01-15')"
205
+ ),
206
+ author: Optional[str] = typer.Option(
207
+ None, "--author", "-a",
208
+ help="Filter commits by author name"
209
+ ),
210
+ verbose: bool = typer.Option(
211
+ False, "--verbose", "-v",
212
+ help="Show detailed output including raw commit data"
213
+ ),
214
+ no_ai: bool = typer.Option(
215
+ False, "--no-ai",
216
+ help="Skip AI analysis, show only raw stats"
217
+ ),
218
+ ):
219
+ """Generate a daily report of git activity since last run."""
220
+
221
+ # Check git repo
222
+ try:
223
+ GitOps()
224
+ except NotAGitRepoError:
225
+ console.print("[red]Not a git repository.[/red]")
226
+ raise typer.Exit(1)
227
+
228
+ config_manager = ConfigManager()
229
+ config = config_manager.load()
230
+ state_manager = DailyStateManager()
231
+
232
+ # Get language from config
233
+ daily_config = config.get("daily", {})
234
+ language = daily_config.get("language", "en")
235
+ language_name = LANGUAGE_NAMES.get(language, language)
236
+
237
+ # Determine since timestamp
238
+ # If author filter is used without --since, default to 24h (don't use last_run)
239
+ if since:
240
+ since_dt = state_manager.parse_since_option(since)
241
+ time_desc = f"since {since}"
242
+ elif author:
243
+ # When filtering by author, default to 24h instead of last_run
244
+ from datetime import timedelta
245
+ since_dt = datetime.now() - timedelta(hours=24)
246
+ time_desc = "last 24 hours"
247
+ else:
248
+ since_dt = state_manager.get_since_timestamp()
249
+ last_run = state_manager.get_last_run()
250
+ if last_run:
251
+ time_desc = f"since last run ({last_run.strftime('%Y-%m-%d %H:%M')})"
252
+ else:
253
+ time_desc = "last 24 hours (first run)"
254
+
255
+ if verbose:
256
+ console.print(f"[dim]Language: {language_name}[/dim]")
257
+ console.print(f"[dim]Since: {since_dt.isoformat()}[/dim]")
258
+
259
+ # Get commits
260
+ with console.status("Fetching commits..."):
261
+ commits = get_commits_since(since_dt, author)
262
+
263
+ if not commits:
264
+ console.print(Panel(
265
+ f"[yellow]No commits found {time_desc}[/yellow]",
266
+ title="Daily Report",
267
+ border_style="yellow"
268
+ ))
269
+ # Only update last run if not filtering by author
270
+ if not author:
271
+ state_manager.set_last_run()
272
+ return
273
+
274
+ # Calculate stats
275
+ stats = calculate_stats(commits)
276
+
277
+ # Show header
278
+ today = datetime.now().strftime("%d %B %Y")
279
+ header_text = f"[bold]Daily Report - {today}[/bold]\n"
280
+ header_text += f"[dim]{time_desc} | {stats['total_commits']} commits | {len(stats['authors'])} author(s)[/dim]"
281
+
282
+ console.print(Panel(header_text, border_style="cyan"))
283
+
284
+ # Show verbose commit list
285
+ if verbose:
286
+ console.print("\n[bold cyan]Commits:[/bold cyan]")
287
+ for c in commits[:20]: # Limit display
288
+ console.print(f" [dim]{c['hash']}[/dim] {c['message']} [dim]({c['author']})[/dim]")
289
+ if len(commits) > 20:
290
+ console.print(f" [dim]... and {len(commits) - 20} more[/dim]")
291
+ console.print()
292
+
293
+ # Show stats table
294
+ stats_table = Table(show_header=False, box=None, padding=(0, 2))
295
+ stats_table.add_column("Metric", style="cyan")
296
+ stats_table.add_column("Value", style="green")
297
+
298
+ stats_table.add_row("Commits", str(stats["total_commits"]))
299
+ stats_table.add_row("Lines added", f"+{stats['total_additions']}")
300
+ stats_table.add_row("Lines deleted", f"-{stats['total_deletions']}")
301
+ stats_table.add_row("Files changed", str(stats["total_files"]))
302
+ stats_table.add_row("Authors", ", ".join(stats["authors"]))
303
+
304
+ console.print(stats_table)
305
+
306
+ # Show affected directories
307
+ if stats["directories"]:
308
+ console.print("\n[bold]Affected areas:[/bold]")
309
+ for dir_name, count in list(stats["directories"].items())[:5]:
310
+ console.print(f" [dim]├──[/dim] {dir_name}/ [dim]({count} files)[/dim]")
311
+
312
+ # AI Analysis
313
+ if not no_ai:
314
+ console.print()
315
+ with console.status("Generating AI summary..."):
316
+ try:
317
+ llm_config = config.get("llm", {})
318
+ llm = LLMClient(llm_config)
319
+
320
+ # Load and prepare prompt
321
+ prompt_template = load_daily_prompt(language)
322
+ commits_text = format_commits_for_llm(commits)
323
+ prompt = prompt_template.replace("{{COMMITS}}", commits_text)
324
+ prompt = prompt.replace("{{LANGUAGE}}", language_name)
325
+
326
+ # Get AI response
327
+ report = llm.chat(prompt)
328
+
329
+ console.print(Panel(
330
+ report,
331
+ title="[bold]AI Summary[/bold]",
332
+ border_style="green"
333
+ ))
334
+
335
+ except Exception as e:
336
+ console.print(f"[yellow]AI analysis skipped: {e}[/yellow]")
337
+
338
+ # Update last run timestamp (only if not filtering by author)
339
+ if not author:
340
+ state_manager.set_last_run()
341
+ if verbose:
342
+ console.print(f"\n[dim]Last run updated: {datetime.now().isoformat()}[/dim]")
@@ -430,6 +430,97 @@ def add_cmd(name: str):
430
430
  typer.echo(f" ⚠️ Run 'redgit integration install {name}' to configure")
431
431
 
432
432
 
433
+ @integration_app.command("update")
434
+ def update_cmd(
435
+ name: str = typer.Argument(None, help="Integration name to update (or omit to update all)"),
436
+ force: bool = typer.Option(False, "--force", "-f", help="Force reinstall even if up to date")
437
+ ):
438
+ """Update installed integrations from taps"""
439
+ from ..core.tap import TapManager, find_item_in_taps
440
+ from .tap import install_from_tap
441
+
442
+ tap_mgr = TapManager()
443
+ config = ConfigManager().load()
444
+ integrations_config = config.get("integrations", {})
445
+
446
+ # Get installed integrations
447
+ installed_names = _get_installed_integrations()
448
+
449
+ # Also include integrations enabled in config
450
+ for int_name, cfg in integrations_config.items():
451
+ if isinstance(cfg, dict) and cfg.get("enabled"):
452
+ installed_names.add(int_name)
453
+
454
+ if not installed_names:
455
+ typer.echo("\n📦 No integrations installed.\n")
456
+ typer.echo(" 💡 Install from taps: rg install <name>")
457
+ return
458
+
459
+ # Determine which integrations to update
460
+ if name:
461
+ # Update specific integration
462
+ # Normalize name
463
+ normalized_name = name.replace("-", "_")
464
+ if name not in installed_names and normalized_name not in installed_names:
465
+ typer.secho(f"❌ '{name}' is not installed.", fg=typer.colors.RED)
466
+ typer.echo(f" Installed: {', '.join(sorted(installed_names))}")
467
+ raise typer.Exit(1)
468
+
469
+ target_name = name if name in installed_names else normalized_name
470
+ to_update = [target_name]
471
+ else:
472
+ # Update all integrations
473
+ to_update = sorted(installed_names)
474
+
475
+ typer.echo(f"\n🔄 Updating {len(to_update)} integration(s)...\n")
476
+
477
+ updated = 0
478
+ failed = 0
479
+ skipped = 0
480
+
481
+ for int_name in to_update:
482
+ typer.echo(f" {int_name}...", nl=False)
483
+
484
+ try:
485
+ # Find integration in taps
486
+ result = find_item_in_taps(int_name, "integration")
487
+
488
+ if not result:
489
+ # Try with hyphen variant
490
+ result = find_item_in_taps(int_name.replace("_", "-"), "integration")
491
+
492
+ if not result:
493
+ # Not from a tap, might be local custom integration
494
+ typer.secho(" skipped (local/custom)", fg=typer.colors.YELLOW)
495
+ skipped += 1
496
+ continue
497
+
498
+ # Update from tap using install_from_tap with force=True
499
+ success = install_from_tap(int_name, force=True, no_configure=True)
500
+
501
+ if success:
502
+ typer.secho(" ✓ updated", fg=typer.colors.GREEN)
503
+ updated += 1
504
+ else:
505
+ typer.secho(" ✗ failed", fg=typer.colors.RED)
506
+ failed += 1
507
+
508
+ except Exception as e:
509
+ typer.secho(f" ✗ failed: {e}", fg=typer.colors.RED)
510
+ failed += 1
511
+
512
+ # Summary
513
+ typer.echo("")
514
+ if updated > 0:
515
+ typer.secho(f"✅ Updated {updated} integration(s)", fg=typer.colors.GREEN)
516
+ if skipped > 0:
517
+ typer.echo(f" Skipped: {skipped}")
518
+ if failed > 0:
519
+ typer.secho(f" Failed: {failed}", fg=typer.colors.RED)
520
+
521
+ typer.echo("")
522
+
523
+
433
524
  @integration_app.command("remove")
434
525
  def remove_cmd(name: str):
435
526
  """Disable an integration"""