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.
- ai_dev_harness-0.2.0.dist-info/METADATA +200 -0
- ai_dev_harness-0.2.0.dist-info/RECORD +24 -0
- ai_dev_harness-0.2.0.dist-info/WHEEL +5 -0
- ai_dev_harness-0.2.0.dist-info/entry_points.txt +3 -0
- ai_dev_harness-0.2.0.dist-info/licenses/LICENSE +21 -0
- ai_dev_harness-0.2.0.dist-info/top_level.txt +1 -0
- liteharness/__init__.py +3 -0
- liteharness/__main__.py +290 -0
- liteharness/clean.py +165 -0
- liteharness/diff.py +189 -0
- liteharness/export.py +96 -0
- liteharness/harnesses/__init__.py +349 -0
- liteharness/harnesses/claude_code.py +500 -0
- liteharness/harnesses/codex.py +164 -0
- liteharness/harnesses/copilot.py +268 -0
- liteharness/harnesses/cursor.py +221 -0
- liteharness/harnesses/windsurf.py +257 -0
- liteharness/health.py +47 -0
- liteharness/parser.py +203 -0
- liteharness/report.py +164 -0
- liteharness/scanner.py +125 -0
- liteharness/server.py +178 -0
- liteharness/style.py +41 -0
- liteharness/web/index.html +1052 -0
|
@@ -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
|
+
[](https://pypi.org/project/liteharness/)
|
|
31
|
+
[](https://pypi.org/project/liteharness/)
|
|
32
|
+
[](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,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
|
liteharness/__init__.py
ADDED
liteharness/__main__.py
ADDED
|
@@ -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())
|