ai-dev-harness 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.
@@ -0,0 +1,200 @@
1
+ Metadata-Version: 2.4
2
+ Name: ai-dev-harness
3
+ Version: 0.2.0
4
+ Summary: Multi-harness AI coding tool config inspector — scan, visualize, and audit AI assistant ecosystems.
5
+ Author: Boris Villazon-Terrazas
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/boricles/claude-lens
8
+ Project-URL: Repository, https://github.com/boricles/claude-lens
9
+ Project-URL: Bug Tracker, https://github.com/boricles/claude-lens/issues
10
+ Project-URL: Changelog, https://github.com/boricles/claude-lens/blob/main/CHANGELOG.md
11
+ Keywords: liteharness,claude-code,cursor,codex,copilot,windsurf,devtools,observability
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Topic :: Software Development :: Quality Assurance
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Provides-Extra: color
25
+ Requires-Dist: rich; extra == "color"
26
+ Dynamic: license-file
27
+
28
+ # LiteHarness
29
+
30
+ [![PyPI version](https://img.shields.io/pypi/v/liteharness.svg)](https://pypi.org/project/liteharness/)
31
+ [![Python 3.10+](https://img.shields.io/pypi/pyversions/liteharness.svg)](https://pypi.org/project/liteharness/)
32
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
33
+
34
+ **Multi-harness AI coding tool config inspector.**
35
+
36
+ Scan, visualize, and audit every AI coding assistant installed on your machine — from one place.
37
+
38
+ ---
39
+
40
+ ## Supported Tools
41
+
42
+ | Tool | Harness | Config Paths | Instruction File |
43
+ |------|---------|-------------|-----------------|
44
+ | **Claude Code** | `claude_code` | `~/.claude/`, `.claude/` | `CLAUDE.md` |
45
+ | **Cursor** | `cursor` | `~/.cursor/`, `.cursor/` | `.cursorrules`, `.cursor/rules/*.md` |
46
+ | **Codex CLI** | `codex` | `~/.codex/` | `AGENTS.md`, `codex.json` |
47
+ | **Windsurf** | `windsurf` | `~/.windsurf/`, `.windsurf/` | `.windsurfrules`, `.windsurf/rules/*.md` |
48
+ | **GitHub Copilot** | `copilot` | `~/.config/github-copilot/`, `.github/` | `.github/copilot-instructions.md` |
49
+
50
+ Each tool has its own **harness** — a self-contained module that knows how to detect the tool, discover its config files, parse them, and run health checks.
51
+
52
+ ## Installation
53
+
54
+ ```bash
55
+ # Install from PyPI
56
+ pip install liteharness
57
+ ```
58
+
59
+ Or install from source in development mode:
60
+
61
+ ```bash
62
+ git clone https://github.com/boricles/claude-lens.git
63
+ cd claude-lens
64
+ pip install -e .
65
+ ```
66
+
67
+ ## Usage
68
+
69
+ ### CLI Report
70
+
71
+ ```bash
72
+ # Scan all detected AI tools and print a report
73
+ liteharness report
74
+
75
+ # Scan a specific harness only
76
+ liteharness report --harness claude_code
77
+
78
+ # List all supported harnesses and detection status
79
+ liteharness harnesses
80
+ ```
81
+
82
+ ### JSON Output
83
+
84
+ ```bash
85
+ # Full JSON scan data
86
+ liteharness scan
87
+
88
+ # Filter to one harness
89
+ liteharness scan --harness cursor
90
+
91
+ # Pretty-printed
92
+ liteharness scan | python3 -m json.tool
93
+ ```
94
+
95
+ ### Web Dashboard
96
+
97
+ ```bash
98
+ # Launch interactive browser UI on localhost:1834
99
+ liteharness web
100
+
101
+ # Custom port
102
+ liteharness web --port 9000
103
+ ```
104
+
105
+ ### Other Commands
106
+
107
+ ```bash
108
+ # Compare two scan snapshots
109
+ liteharness diff snapshot-a.json snapshot-b.json
110
+
111
+ # Export a project's config as a portable bundle
112
+ liteharness export myproject
113
+
114
+ # Clean up stale sessions and orphaned data (interactive)
115
+ liteharness clean
116
+
117
+ # Print version
118
+ liteharness version
119
+ ```
120
+
121
+ The legacy `claude-lens` command is still available as an alias.
122
+
123
+ ## What It Detects
124
+
125
+ For each tool, LiteHarness discovers:
126
+
127
+ - **Global settings** — permissions, preferences, API config
128
+ - **Instruction files** — system prompts and rules (CLAUDE.md, .cursorrules, etc.)
129
+ - **Rules/agents** — custom agent definitions, rule files
130
+ - **Sessions** — session metadata (count, size, age — never reads content)
131
+ - **Plugins/extensions** — installed plugins with capabilities
132
+ - **Memory** — memory files and index integrity
133
+ - **Health issues** — orphaned state, stale sessions, missing instructions, config drift
134
+
135
+ ## Health Checks
136
+
137
+ Each harness runs its own health checks:
138
+
139
+ | Check | Harnesses | Severity |
140
+ |-------|-----------|----------|
141
+ | Missing instruction file | All | info |
142
+ | Orphaned project state | Claude Code | warning |
143
+ | Stale sessions (>30 days) | Claude Code | info |
144
+ | Memory index drift | Claude Code | warning |
145
+ | Disk usage >100-200 MB | Claude Code, Cursor, Windsurf | warning |
146
+ | Outdated config (>180 days) | Copilot | info |
147
+
148
+ ## Architecture
149
+
150
+ ```
151
+ liteharness/
152
+ harnesses/
153
+ __init__.py # BaseHarness ABC, schema dataclasses, registry
154
+ claude_code.py # Claude Code harness
155
+ cursor.py # Cursor harness
156
+ codex.py # Codex CLI harness
157
+ windsurf.py # Windsurf harness
158
+ copilot.py # GitHub Copilot harness
159
+ scanner.py # Orchestrator — runs all harnesses, merges results
160
+ parser.py # Shared parsers (frontmatter, JSON, etc.)
161
+ report.py # CLI text report renderer
162
+ server.py # HTTP server + web dashboard
163
+ web/index.html # Single-file web dashboard (Tailwind + vanilla JS)
164
+ export.py # Project config export
165
+ diff.py # Scan snapshot comparison
166
+ clean.py # Cleanup stale data
167
+ style.py # Shared ANSI color helpers
168
+ ```
169
+
170
+ ### Adding a New Harness
171
+
172
+ 1. Create `liteharness/harnesses/newtool.py`
173
+ 2. Subclass `BaseHarness` and implement `detect()`, `scan()`, `health_checks()`
174
+ 3. Decorate with `@register_harness`
175
+ 4. Add the module name to `_ensure_harnesses_loaded()` in `harnesses/__init__.py`
176
+
177
+ ## Design Principles
178
+
179
+ - **Zero mandatory dependencies** — stdlib only for core scanner and CLI
180
+ - **Local-first** — web server binds to `127.0.0.1` only
181
+ - **Privacy-safe** — never reads conversation content from session files
182
+ - **Non-destructive** — never modifies user files unless in explicit `clean` mode
183
+ - **Self-contained harnesses** — each harness module is independent
184
+
185
+ ## Development
186
+
187
+ ```bash
188
+ # Run all tests
189
+ python3 -m unittest discover tests/ -v
190
+
191
+ # Run a specific test file
192
+ python3 -m unittest tests.test_windsurf_harness -v
193
+
194
+ # Run as module
195
+ python3 -m liteharness version
196
+ ```
197
+
198
+ ## License
199
+
200
+ MIT
@@ -0,0 +1,24 @@
1
+ ai_dev_harness-0.2.0.dist-info/licenses/LICENSE,sha256=kV6J_7CaCkoOBNwSwsEo_d4X_ugM1Lv2xbSiORnbLno,1080
2
+ liteharness/__init__.py,sha256=BltZn43pzJ-WFvaHs26YXnkKRfyFoppmVWb6QPNKOuc,92
3
+ liteharness/__main__.py,sha256=auhimUCS1O66BP_5p8joPLVltqi-Tp2jvgbwuWSBXBo,9267
4
+ liteharness/clean.py,sha256=1Jdm8BB4qUWqPGNN09a4ycp3XNkvKbkMMcCBGKFYqz8,5591
5
+ liteharness/diff.py,sha256=1ZQzxDvtchmzTk4wyD6nXQpm-joxDZHpjquFXggQkSU,7426
6
+ liteharness/export.py,sha256=4MP8vfUS_loxaWPn-YeqCPG98rACWZ7YEhYfzgU8UpY,3287
7
+ liteharness/health.py,sha256=dbQLtH1RFSibQNikdr2IVaTnuCTDUj4yU-CQz01TLes,1614
8
+ liteharness/parser.py,sha256=TX5U7glJpa4nTwDvmng47vezrmh78_bEk7lzyQ6f4oo,6194
9
+ liteharness/report.py,sha256=x0qNlINH7ycblp-LctmV50vP_dWskDyHNAEMN3umgDg,5915
10
+ liteharness/scanner.py,sha256=S5P8Zr5JCv0ZnjeR9qdbOJDKiHa2MPi7q_mx1i9WDyk,4059
11
+ liteharness/server.py,sha256=k2QIR5aOHlVdRKgk54hbpqAXyW34efhA43NSH-Sn3Cg,6217
12
+ liteharness/style.py,sha256=R7fxUG7vPc2V7DN1aRA31B8ieEitOVcaDmFxi67hQmY,972
13
+ liteharness/harnesses/__init__.py,sha256=HQLIZ8cZvvp06GalcIURYDNGkpmGcOU81H2XV-15B1w,10478
14
+ liteharness/harnesses/claude_code.py,sha256=23bYYqHvX9vYYxUoFVPvCDepCtw4VM9dqjLF7KNPIa0,18982
15
+ liteharness/harnesses/codex.py,sha256=ayxprDV9r4jx9ax4tOq_Nvg7BY3CDBxCplIw3Gcjytw,5102
16
+ liteharness/harnesses/copilot.py,sha256=VCAikK6nVJtTH2dNbyE8AyZibRFwgtUo10TyCimyuN0,10230
17
+ liteharness/harnesses/cursor.py,sha256=vcMipYEeu2EaFAKWb2LuOOPcAkvUxcmvzW4bPL9YrEY,7870
18
+ liteharness/harnesses/windsurf.py,sha256=K88sNR56OSc5E2HVsuaRKakhCZG6V6nZyRBjMP8nDLM,9529
19
+ liteharness/web/index.html,sha256=58N7cLIJHzVGm87PqKBfGnpqFbotEDqZNGGP9RNrFjE,53910
20
+ ai_dev_harness-0.2.0.dist-info/METADATA,sha256=iLjX8l7o1PCV5wDIbrI5QethhEOfFDilPXaS_4_hMIs,6342
21
+ ai_dev_harness-0.2.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
22
+ ai_dev_harness-0.2.0.dist-info/entry_points.txt,sha256=rb3ao-HBpACplnuB9ZxIRucGFVHohwgMi2QCwPsy64A,98
23
+ ai_dev_harness-0.2.0.dist-info/top_level.txt,sha256=EKt_upwsB7kS9gPyRbtYzU1ojipHiGwEFwHy_9o0VC0,12
24
+ ai_dev_harness-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ claude-lens = liteharness.__main__:main
3
+ liteharness = liteharness.__main__:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Boris Villazon-Terrazas
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ liteharness
@@ -0,0 +1,3 @@
1
+ """LiteHarness — Multi-harness AI coding tool config inspector."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,290 @@
1
+ """CLI entry point for LiteHarness.
2
+
3
+ Usage:
4
+ python -m liteharness scan # Scan all harnesses, output JSON
5
+ python -m liteharness scan --harness cursor # Scan specific harness
6
+ python -m liteharness report # Print formatted report
7
+ python -m liteharness harnesses # List detected harnesses
8
+ python -m liteharness version # Print version
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ import argparse
14
+ import json
15
+ import sys
16
+ from pathlib import Path
17
+
18
+ from . import __version__
19
+ from .clean import find_cleanable, execute_action, render_cleanup_plan
20
+ from .diff import diff_projects, render_diff
21
+ from .export import export_project, render_export_summary
22
+ from .health import check_health
23
+ from .report import render_report
24
+ from .scanner import scan
25
+ from .server import serve
26
+ from .style import bold, dim, green, yellow, cyan
27
+
28
+
29
+ def main(argv: list[str] | None = None) -> int:
30
+ ap = argparse.ArgumentParser(
31
+ prog="liteharness",
32
+ description="Multi-harness AI coding tool config inspector.",
33
+ )
34
+ ap.add_argument(
35
+ "--home",
36
+ type=Path,
37
+ default=None,
38
+ help="Override ~/.claude/ location",
39
+ )
40
+ ap.add_argument(
41
+ "--no-color",
42
+ action="store_true",
43
+ help="Disable colored output",
44
+ )
45
+
46
+ # Shared arguments for subcommands that run scans
47
+ shared = argparse.ArgumentParser(add_help=False)
48
+ shared.add_argument("--home", type=Path, default=None, help="Override ~/.claude/ location")
49
+ shared.add_argument("--no-color", action="store_true", help="Disable colored output")
50
+ shared.add_argument(
51
+ "--harness",
52
+ type=str,
53
+ default=None,
54
+ help="Filter to specific harness (claude_code, cursor, codex, windsurf, copilot)",
55
+ )
56
+
57
+ sub = ap.add_subparsers(dest="command")
58
+
59
+ # scan
60
+ scan_cmd = sub.add_parser("scan", parents=[shared], help="Scan and output JSON")
61
+ scan_cmd.add_argument(
62
+ "--output", "-o",
63
+ type=Path,
64
+ default=None,
65
+ help="Write JSON to file instead of stdout",
66
+ )
67
+
68
+ # report
69
+ sub.add_parser("report", parents=[shared], help="Print a formatted CLI report")
70
+
71
+ # web
72
+ web_cmd = sub.add_parser("web", parents=[shared], help="Launch the web dashboard")
73
+ web_cmd.add_argument("--port", type=int, default=8500, help="Port for the web server (default: 8500)")
74
+ web_cmd.add_argument("--no-open", action="store_true", help="Don't auto-open the browser")
75
+
76
+ # diff
77
+ diff_cmd = sub.add_parser("diff", parents=[shared], help="Compare two projects' config")
78
+ diff_cmd.add_argument("project_a", help="First project name")
79
+ diff_cmd.add_argument("project_b", help="Second project name")
80
+ diff_cmd.add_argument("--json", action="store_true", help="Output raw JSON")
81
+
82
+ # export
83
+ export_cmd = sub.add_parser("export", parents=[shared], help="Export a project's config bundle")
84
+ export_cmd.add_argument("project", help="Project name to export")
85
+ export_cmd.add_argument("--output", "-o", type=Path, default=None, help="Write to file")
86
+ export_cmd.add_argument("--json", action="store_true", default=True, help="JSON format (default)")
87
+
88
+ # clean
89
+ clean_cmd = sub.add_parser("clean", parents=[shared], help="Clean up stale data")
90
+ clean_cmd.add_argument("--dry-run", action="store_true", help="Show plan without executing")
91
+ clean_cmd.add_argument("--yes", "-y", action="store_true", help="Skip confirmation prompt")
92
+
93
+ # harnesses
94
+ sub.add_parser("harnesses", parents=[shared], help="List all harnesses and detection status")
95
+
96
+ # version
97
+ sub.add_parser("version", help="Print version")
98
+
99
+ args = ap.parse_args(argv)
100
+
101
+ if args.command is None:
102
+ args.command = "report"
103
+
104
+ if args.command == "version":
105
+ print(f"liteharness v{__version__}")
106
+ return 0
107
+
108
+ if args.command == "harnesses":
109
+ return _cmd_harnesses(args)
110
+
111
+ if args.command == "scan":
112
+ return _cmd_scan(args)
113
+
114
+ if args.command == "report":
115
+ return _cmd_report(args)
116
+
117
+ if args.command == "web":
118
+ return _cmd_web(args)
119
+
120
+ if args.command == "diff":
121
+ return _cmd_diff(args)
122
+
123
+ if args.command == "export":
124
+ return _cmd_export(args)
125
+
126
+ if args.command == "clean":
127
+ return _cmd_clean(args)
128
+
129
+ ap.print_help()
130
+ return 1
131
+
132
+
133
+ def _get_harness_filter(args: argparse.Namespace) -> str | None:
134
+ return getattr(args, "harness", None)
135
+
136
+
137
+ def _cmd_harnesses(args: argparse.Namespace) -> int:
138
+ from .harnesses import get_all_harnesses
139
+ color = not args.no_color and sys.stdout.isatty()
140
+
141
+ harnesses = get_all_harnesses(claude_home=args.home)
142
+ print()
143
+ print(bold("DETECTED HARNESSES", color))
144
+ print()
145
+ for h in harnesses:
146
+ detected = h.detect()
147
+ status = green("detected", color) if detected else dim("not found", color)
148
+ name_str = bold(h.display_name, color)
149
+ print(f" {name_str:30s} ({h.name}) {status}")
150
+ print()
151
+ return 0
152
+
153
+
154
+ def _cmd_scan(args: argparse.Namespace) -> int:
155
+ data = scan(claude_home=args.home, harness_filter=_get_harness_filter(args))
156
+ health = check_health(data)
157
+ data["health"] = health
158
+
159
+ output = json.dumps(data, indent=2, ensure_ascii=False)
160
+
161
+ if args.output:
162
+ args.output.write_text(output, encoding="utf-8")
163
+ print(f"Scan written to {args.output}", file=sys.stderr)
164
+ else:
165
+ print(output)
166
+
167
+ return 0
168
+
169
+
170
+ def _cmd_report(args: argparse.Namespace) -> int:
171
+ data = scan(claude_home=args.home, harness_filter=_get_harness_filter(args))
172
+ health = check_health(data)
173
+ color = not args.no_color and sys.stdout.isatty()
174
+ output = render_report(data, health, color=color)
175
+ print(output)
176
+ return 0
177
+
178
+
179
+ def _cmd_web(args: argparse.Namespace) -> int:
180
+ import webbrowser
181
+
182
+ port = args.port
183
+ if not args.no_open:
184
+ import threading
185
+ threading.Timer(0.5, lambda: webbrowser.open(f"http://localhost:{port}")).start()
186
+
187
+ serve(port=port, claude_home=args.home)
188
+ return 0
189
+
190
+
191
+ def _find_project(data: dict, name: str) -> dict | None:
192
+ """Find a project by name (case-insensitive partial match)."""
193
+ name_lower = name.lower()
194
+ for p in data.get("projects", []):
195
+ if p.get("name", "").lower() == name_lower:
196
+ return p
197
+ for p in data.get("projects", []):
198
+ if name_lower in p.get("name", "").lower():
199
+ return p
200
+ return None
201
+
202
+
203
+ def _cmd_diff(args: argparse.Namespace) -> int:
204
+ data = scan(claude_home=args.home, harness_filter=_get_harness_filter(args))
205
+
206
+ pa = _find_project(data, args.project_a)
207
+ pb = _find_project(data, args.project_b)
208
+
209
+ if not pa:
210
+ print(f"Project not found: {args.project_a}", file=sys.stderr)
211
+ print("Available: " + ", ".join(p["name"] for p in data["projects"]), file=sys.stderr)
212
+ return 1
213
+ if not pb:
214
+ print(f"Project not found: {args.project_b}", file=sys.stderr)
215
+ print("Available: " + ", ".join(p["name"] for p in data["projects"]), file=sys.stderr)
216
+ return 1
217
+
218
+ result = diff_projects(pa, pb)
219
+
220
+ if getattr(args, "json", False):
221
+ print(json.dumps(result, indent=2, ensure_ascii=False))
222
+ else:
223
+ color = not args.no_color and sys.stdout.isatty()
224
+ print(render_diff(result, color=color))
225
+
226
+ return 0
227
+
228
+
229
+ def _cmd_export(args: argparse.Namespace) -> int:
230
+ data = scan(claude_home=args.home, harness_filter=_get_harness_filter(args))
231
+
232
+ project = _find_project(data, args.project)
233
+ if not project:
234
+ print(f"Project not found: {args.project}", file=sys.stderr)
235
+ print("Available: " + ", ".join(p["name"] for p in data["projects"]), file=sys.stderr)
236
+ return 1
237
+
238
+ bundle = export_project(project, claude_home=data.get("claude_home", ""))
239
+ output = json.dumps(bundle, indent=2, ensure_ascii=False)
240
+
241
+ if args.output:
242
+ args.output.write_text(output, encoding="utf-8")
243
+ color = not args.no_color and sys.stdout.isatty()
244
+ print(render_export_summary(bundle, color=color), file=sys.stderr)
245
+ print(f"Written to {args.output}", file=sys.stderr)
246
+ else:
247
+ print(output)
248
+
249
+ return 0
250
+
251
+
252
+ def _cmd_clean(args: argparse.Namespace) -> int:
253
+ data = scan(claude_home=args.home, harness_filter=_get_harness_filter(args))
254
+ health = check_health(data)
255
+ actions = find_cleanable(data, health)
256
+
257
+ color = not args.no_color and sys.stdout.isatty()
258
+ print(render_cleanup_plan(actions, color=color))
259
+
260
+ if not actions:
261
+ return 0
262
+
263
+ if args.dry_run:
264
+ print(" (dry run -- no changes made)")
265
+ return 0
266
+
267
+ if not args.yes:
268
+ try:
269
+ answer = input(f"Proceed with {len(actions)} cleanup action(s)? [y/N] ")
270
+ except (EOFError, KeyboardInterrupt):
271
+ print("\nAborted.")
272
+ return 1
273
+ if answer.strip().lower() not in ("y", "yes"):
274
+ print("Aborted.")
275
+ return 0
276
+
277
+ success = 0
278
+ for a in actions:
279
+ if execute_action(a):
280
+ success += 1
281
+ print(f" Cleaned: {a['description']}")
282
+ else:
283
+ print(f" FAILED: {a['description']}", file=sys.stderr)
284
+
285
+ print(f"\n {success}/{len(actions)} action(s) completed.")
286
+ return 0
287
+
288
+
289
+ if __name__ == "__main__":
290
+ sys.exit(main())