ws-scout 0.1.0__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.
- ws_scout-0.1.0/LICENSE +21 -0
- ws_scout-0.1.0/PKG-INFO +95 -0
- ws_scout-0.1.0/README.md +52 -0
- ws_scout-0.1.0/pyproject.toml +44 -0
- ws_scout-0.1.0/setup.cfg +4 -0
- ws_scout-0.1.0/ws_scout.egg-info/PKG-INFO +95 -0
- ws_scout-0.1.0/ws_scout.egg-info/SOURCES.txt +9 -0
- ws_scout-0.1.0/ws_scout.egg-info/dependency_links.txt +1 -0
- ws_scout-0.1.0/ws_scout.egg-info/entry_points.txt +2 -0
- ws_scout-0.1.0/ws_scout.egg-info/top_level.txt +1 -0
- ws_scout-0.1.0/ws_scout.py +278 -0
ws_scout-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hokuto Shimura
|
|
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.
|
ws_scout-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ws-scout
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Scan a workspace of git repos and rank the ones that need attention — triage CLI with LLM-friendly output
|
|
5
|
+
Author: Hokuto Shimura
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Hokuto Shimura
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/hoqqun/ws-scout
|
|
29
|
+
Project-URL: Repository, https://github.com/hoqqun/ws-scout
|
|
30
|
+
Project-URL: Issues, https://github.com/hoqqun/ws-scout/issues
|
|
31
|
+
Keywords: git,cli,workspace,multi-repo,triage,developer-tools
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Environment :: Console
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Operating System :: POSIX
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
39
|
+
Requires-Python: >=3.10
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
License-File: LICENSE
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# ws-scout
|
|
45
|
+
|
|
46
|
+
`ws-scout` is a small WSL-friendly CLI that scans `~/workspace`, finds git repositories, and ranks the projects that probably deserve attention.
|
|
47
|
+
|
|
48
|
+
Unlike most multi-repo status tools, it does not just list repository states: it scores each repository (dirty tree, unpushed commits, TODO notes, recent activity) and sorts them into a triage list, with a `--brief` mode designed to be pasted into an LLM coding assistant as context.
|
|
49
|
+
|
|
50
|
+
It is useful when the workspace has many active projects and you want a quick morning triage before opening Claude Code.
|
|
51
|
+
|
|
52
|
+
## What It Checks
|
|
53
|
+
|
|
54
|
+
- Dirty git working trees
|
|
55
|
+
- Local commits ahead of the remote
|
|
56
|
+
- Branches behind the remote
|
|
57
|
+
- Recent commits
|
|
58
|
+
- TODO/FIXME/HACK notes
|
|
59
|
+
- Common `package.json` scripts
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
./ws_scout.py
|
|
65
|
+
./ws_scout.py --brief
|
|
66
|
+
./ws_scout.py --json
|
|
67
|
+
./ws_scout.py ~/workspace --top 20 --depth 2
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Install a `ws-scout` command from PyPI:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pipx install ws-scout
|
|
74
|
+
# or
|
|
75
|
+
uv tool install ws-scout
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Or install from a local checkout:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
./install.sh
|
|
82
|
+
ws-scout --brief
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The `--brief` output is intentionally compact so it can be pasted into Claude Code as context.
|
|
86
|
+
|
|
87
|
+
## Requirements
|
|
88
|
+
|
|
89
|
+
- Python 3.10+
|
|
90
|
+
- `git`
|
|
91
|
+
- [ripgrep](https://github.com/BurntSushi/ripgrep) (optional — used for TODO/FIXME scanning; without it the TODO column is simply empty)
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
[MIT](LICENSE)
|
ws_scout-0.1.0/README.md
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
# ws-scout
|
|
2
|
+
|
|
3
|
+
`ws-scout` is a small WSL-friendly CLI that scans `~/workspace`, finds git repositories, and ranks the projects that probably deserve attention.
|
|
4
|
+
|
|
5
|
+
Unlike most multi-repo status tools, it does not just list repository states: it scores each repository (dirty tree, unpushed commits, TODO notes, recent activity) and sorts them into a triage list, with a `--brief` mode designed to be pasted into an LLM coding assistant as context.
|
|
6
|
+
|
|
7
|
+
It is useful when the workspace has many active projects and you want a quick morning triage before opening Claude Code.
|
|
8
|
+
|
|
9
|
+
## What It Checks
|
|
10
|
+
|
|
11
|
+
- Dirty git working trees
|
|
12
|
+
- Local commits ahead of the remote
|
|
13
|
+
- Branches behind the remote
|
|
14
|
+
- Recent commits
|
|
15
|
+
- TODO/FIXME/HACK notes
|
|
16
|
+
- Common `package.json` scripts
|
|
17
|
+
|
|
18
|
+
## Usage
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
./ws_scout.py
|
|
22
|
+
./ws_scout.py --brief
|
|
23
|
+
./ws_scout.py --json
|
|
24
|
+
./ws_scout.py ~/workspace --top 20 --depth 2
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Install a `ws-scout` command from PyPI:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pipx install ws-scout
|
|
31
|
+
# or
|
|
32
|
+
uv tool install ws-scout
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Or install from a local checkout:
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
./install.sh
|
|
39
|
+
ws-scout --brief
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The `--brief` output is intentionally compact so it can be pasted into Claude Code as context.
|
|
43
|
+
|
|
44
|
+
## Requirements
|
|
45
|
+
|
|
46
|
+
- Python 3.10+
|
|
47
|
+
- `git`
|
|
48
|
+
- [ripgrep](https://github.com/BurntSushi/ripgrep) (optional — used for TODO/FIXME scanning; without it the TODO column is simply empty)
|
|
49
|
+
|
|
50
|
+
## License
|
|
51
|
+
|
|
52
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "ws-scout"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Scan a workspace of git repos and rank the ones that need attention — triage CLI with LLM-friendly output"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { file = "LICENSE" }
|
|
11
|
+
authors = [{ name = "Hokuto Shimura" }]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
keywords = ["git", "cli", "workspace", "multi-repo", "triage", "developer-tools"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Environment :: Console",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Operating System :: POSIX",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Topic :: Software Development :: Version Control :: Git",
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[project.urls]
|
|
25
|
+
Homepage = "https://github.com/hoqqun/ws-scout"
|
|
26
|
+
Repository = "https://github.com/hoqqun/ws-scout"
|
|
27
|
+
Issues = "https://github.com/hoqqun/ws-scout/issues"
|
|
28
|
+
|
|
29
|
+
[project.scripts]
|
|
30
|
+
ws-scout = "ws_scout:main"
|
|
31
|
+
|
|
32
|
+
[tool.setuptools]
|
|
33
|
+
py-modules = ["ws_scout"]
|
|
34
|
+
|
|
35
|
+
[tool.ruff]
|
|
36
|
+
line-length = 100
|
|
37
|
+
target-version = "py310"
|
|
38
|
+
|
|
39
|
+
[tool.ruff.lint]
|
|
40
|
+
select = ["E", "F", "W", "I", "UP", "B", "SIM"]
|
|
41
|
+
|
|
42
|
+
[tool.mypy]
|
|
43
|
+
python_version = "3.10"
|
|
44
|
+
strict = true
|
ws_scout-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ws-scout
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Scan a workspace of git repos and rank the ones that need attention — triage CLI with LLM-friendly output
|
|
5
|
+
Author: Hokuto Shimura
|
|
6
|
+
License: MIT License
|
|
7
|
+
|
|
8
|
+
Copyright (c) 2026 Hokuto Shimura
|
|
9
|
+
|
|
10
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
11
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
12
|
+
in the Software without restriction, including without limitation the rights
|
|
13
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
14
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
15
|
+
furnished to do so, subject to the following conditions:
|
|
16
|
+
|
|
17
|
+
The above copyright notice and this permission notice shall be included in all
|
|
18
|
+
copies or substantial portions of the Software.
|
|
19
|
+
|
|
20
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
21
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
22
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
23
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
24
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
25
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
26
|
+
SOFTWARE.
|
|
27
|
+
|
|
28
|
+
Project-URL: Homepage, https://github.com/hoqqun/ws-scout
|
|
29
|
+
Project-URL: Repository, https://github.com/hoqqun/ws-scout
|
|
30
|
+
Project-URL: Issues, https://github.com/hoqqun/ws-scout/issues
|
|
31
|
+
Keywords: git,cli,workspace,multi-repo,triage,developer-tools
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Environment :: Console
|
|
34
|
+
Classifier: Intended Audience :: Developers
|
|
35
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
36
|
+
Classifier: Operating System :: POSIX
|
|
37
|
+
Classifier: Programming Language :: Python :: 3
|
|
38
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
39
|
+
Requires-Python: >=3.10
|
|
40
|
+
Description-Content-Type: text/markdown
|
|
41
|
+
License-File: LICENSE
|
|
42
|
+
Dynamic: license-file
|
|
43
|
+
|
|
44
|
+
# ws-scout
|
|
45
|
+
|
|
46
|
+
`ws-scout` is a small WSL-friendly CLI that scans `~/workspace`, finds git repositories, and ranks the projects that probably deserve attention.
|
|
47
|
+
|
|
48
|
+
Unlike most multi-repo status tools, it does not just list repository states: it scores each repository (dirty tree, unpushed commits, TODO notes, recent activity) and sorts them into a triage list, with a `--brief` mode designed to be pasted into an LLM coding assistant as context.
|
|
49
|
+
|
|
50
|
+
It is useful when the workspace has many active projects and you want a quick morning triage before opening Claude Code.
|
|
51
|
+
|
|
52
|
+
## What It Checks
|
|
53
|
+
|
|
54
|
+
- Dirty git working trees
|
|
55
|
+
- Local commits ahead of the remote
|
|
56
|
+
- Branches behind the remote
|
|
57
|
+
- Recent commits
|
|
58
|
+
- TODO/FIXME/HACK notes
|
|
59
|
+
- Common `package.json` scripts
|
|
60
|
+
|
|
61
|
+
## Usage
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
./ws_scout.py
|
|
65
|
+
./ws_scout.py --brief
|
|
66
|
+
./ws_scout.py --json
|
|
67
|
+
./ws_scout.py ~/workspace --top 20 --depth 2
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Install a `ws-scout` command from PyPI:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pipx install ws-scout
|
|
74
|
+
# or
|
|
75
|
+
uv tool install ws-scout
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
Or install from a local checkout:
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
./install.sh
|
|
82
|
+
ws-scout --brief
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
The `--brief` output is intentionally compact so it can be pasted into Claude Code as context.
|
|
86
|
+
|
|
87
|
+
## Requirements
|
|
88
|
+
|
|
89
|
+
- Python 3.10+
|
|
90
|
+
- `git`
|
|
91
|
+
- [ripgrep](https://github.com/BurntSushi/ripgrep) (optional — used for TODO/FIXME scanning; without it the TODO column is simply empty)
|
|
92
|
+
|
|
93
|
+
## License
|
|
94
|
+
|
|
95
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ws_scout
|
|
@@ -0,0 +1,278 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Fast triage dashboard for a workspace full of git projects."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import concurrent.futures
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
from collections.abc import Iterable
|
|
13
|
+
from dataclasses import asdict, dataclass
|
|
14
|
+
from datetime import datetime, timezone
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
__version__ = "0.1.0"
|
|
18
|
+
|
|
19
|
+
DEFAULT_IGNORE = {
|
|
20
|
+
".cache",
|
|
21
|
+
".git",
|
|
22
|
+
".next",
|
|
23
|
+
".venv",
|
|
24
|
+
"dist",
|
|
25
|
+
"node_modules",
|
|
26
|
+
"target",
|
|
27
|
+
"tmp",
|
|
28
|
+
"vendor",
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass
|
|
33
|
+
class RepoReport:
|
|
34
|
+
name: str
|
|
35
|
+
path: str
|
|
36
|
+
branch: str | None
|
|
37
|
+
dirty: bool
|
|
38
|
+
ahead: int
|
|
39
|
+
behind: int
|
|
40
|
+
changed_files: int
|
|
41
|
+
recent_commit: str | None
|
|
42
|
+
recent_commit_time: str | None
|
|
43
|
+
todos: list[str]
|
|
44
|
+
scripts: list[str]
|
|
45
|
+
score: int
|
|
46
|
+
reasons: list[str]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def run(cmd: list[str], cwd: Path, timeout: int = 6) -> str:
|
|
50
|
+
try:
|
|
51
|
+
proc = subprocess.run(
|
|
52
|
+
cmd,
|
|
53
|
+
cwd=str(cwd),
|
|
54
|
+
text=True,
|
|
55
|
+
capture_output=True,
|
|
56
|
+
timeout=timeout,
|
|
57
|
+
check=False,
|
|
58
|
+
)
|
|
59
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
60
|
+
return ""
|
|
61
|
+
return proc.stdout.strip()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def parse_porcelain_v2(output: str) -> tuple[int, int, int]:
|
|
65
|
+
ahead = behind = changed = 0
|
|
66
|
+
for line in output.splitlines():
|
|
67
|
+
if line.startswith("# branch.ab "):
|
|
68
|
+
parts = line.split()
|
|
69
|
+
for part in parts[2:]:
|
|
70
|
+
if part.startswith("+"):
|
|
71
|
+
ahead = int(part[1:] or "0")
|
|
72
|
+
elif part.startswith("-"):
|
|
73
|
+
behind = int(part[1:] or "0")
|
|
74
|
+
elif line and not line.startswith("#"):
|
|
75
|
+
changed += 1
|
|
76
|
+
return ahead, behind, changed
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def find_repos(root: Path, max_depth: int, ignore: set[str]) -> list[Path]:
|
|
80
|
+
repos: list[Path] = []
|
|
81
|
+
root = root.resolve()
|
|
82
|
+
|
|
83
|
+
def walk(path: Path, depth: int) -> None:
|
|
84
|
+
if depth > max_depth:
|
|
85
|
+
return
|
|
86
|
+
if (path / ".git").exists():
|
|
87
|
+
repos.append(path)
|
|
88
|
+
return
|
|
89
|
+
try:
|
|
90
|
+
children = sorted(path.iterdir(), key=lambda p: p.name.lower())
|
|
91
|
+
except OSError:
|
|
92
|
+
return
|
|
93
|
+
for child in children:
|
|
94
|
+
if child.name in ignore or child.name.startswith("."):
|
|
95
|
+
continue
|
|
96
|
+
if child.is_dir():
|
|
97
|
+
walk(child, depth + 1)
|
|
98
|
+
|
|
99
|
+
walk(root, 0)
|
|
100
|
+
return repos
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def recent_todos(path: Path, limit: int) -> list[str]:
|
|
104
|
+
if limit <= 0:
|
|
105
|
+
return []
|
|
106
|
+
rg = [
|
|
107
|
+
"rg",
|
|
108
|
+
"--no-heading",
|
|
109
|
+
"--line-number",
|
|
110
|
+
"--max-count",
|
|
111
|
+
str(limit),
|
|
112
|
+
"--glob",
|
|
113
|
+
"!**/.venv/**",
|
|
114
|
+
"--glob",
|
|
115
|
+
"!**/venv/**",
|
|
116
|
+
"--glob",
|
|
117
|
+
"!**/node_modules/**",
|
|
118
|
+
"--glob",
|
|
119
|
+
"!**/dist/**",
|
|
120
|
+
"--glob",
|
|
121
|
+
"!**/*.ipynb",
|
|
122
|
+
"-S",
|
|
123
|
+
r"TODO|FIXME|HACK|XXX",
|
|
124
|
+
".",
|
|
125
|
+
]
|
|
126
|
+
out = run(rg, path, timeout=5)
|
|
127
|
+
lines = []
|
|
128
|
+
for raw in out.splitlines():
|
|
129
|
+
if any(part in raw for part in ("/node_modules/", "/.git/", "/dist/", "/venv/", "/.venv/")):
|
|
130
|
+
continue
|
|
131
|
+
lines.append(raw[:180])
|
|
132
|
+
if len(lines) >= limit:
|
|
133
|
+
break
|
|
134
|
+
return lines
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def package_scripts(path: Path) -> list[str]:
|
|
138
|
+
pkg = path / "package.json"
|
|
139
|
+
if not pkg.exists():
|
|
140
|
+
return []
|
|
141
|
+
try:
|
|
142
|
+
data = json.loads(pkg.read_text(encoding="utf-8"))
|
|
143
|
+
except (OSError, json.JSONDecodeError, UnicodeDecodeError):
|
|
144
|
+
return []
|
|
145
|
+
scripts = data.get("scripts")
|
|
146
|
+
if not isinstance(scripts, dict):
|
|
147
|
+
return []
|
|
148
|
+
preferred = ["dev", "start", "build", "test", "lint", "typecheck"]
|
|
149
|
+
found = [key for key in preferred if key in scripts]
|
|
150
|
+
extras = sorted(k for k in scripts if k not in found)
|
|
151
|
+
return found + extras[:4]
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def inspect_repo(path: Path, root: Path, todo_limit: int) -> RepoReport:
|
|
155
|
+
branch = run(["git", "branch", "--show-current"], path) or None
|
|
156
|
+
status = run(["git", "status", "--porcelain=v2", "--branch"], path)
|
|
157
|
+
ahead, behind, changed = parse_porcelain_v2(status)
|
|
158
|
+
dirty = changed > 0
|
|
159
|
+
commit = run(["git", "log", "-1", "--format=%h %s"], path) or None
|
|
160
|
+
commit_time = run(["git", "log", "-1", "--format=%cI"], path) or None
|
|
161
|
+
todos = recent_todos(path, todo_limit)
|
|
162
|
+
scripts = package_scripts(path)
|
|
163
|
+
|
|
164
|
+
score = 0
|
|
165
|
+
reasons: list[str] = []
|
|
166
|
+
if dirty:
|
|
167
|
+
score += min(40, 10 + changed * 2)
|
|
168
|
+
reasons.append(f"{changed} changed file(s)")
|
|
169
|
+
if ahead:
|
|
170
|
+
score += 18
|
|
171
|
+
reasons.append(f"{ahead} commit(s) ahead")
|
|
172
|
+
if behind:
|
|
173
|
+
score += 12
|
|
174
|
+
reasons.append(f"{behind} commit(s) behind")
|
|
175
|
+
if todos:
|
|
176
|
+
score += min(15, len(todos) * 3)
|
|
177
|
+
reasons.append(f"{len(todos)} TODO-like note(s)")
|
|
178
|
+
if commit_time:
|
|
179
|
+
try:
|
|
180
|
+
then = datetime.fromisoformat(commit_time.replace("Z", "+00:00"))
|
|
181
|
+
age_days = (datetime.now(timezone.utc) - then.astimezone(timezone.utc)).days
|
|
182
|
+
if age_days <= 2:
|
|
183
|
+
score += 10
|
|
184
|
+
reasons.append("recently touched")
|
|
185
|
+
except ValueError:
|
|
186
|
+
pass
|
|
187
|
+
|
|
188
|
+
return RepoReport(
|
|
189
|
+
name=path.name,
|
|
190
|
+
path=str(path.relative_to(root)),
|
|
191
|
+
branch=branch,
|
|
192
|
+
dirty=dirty,
|
|
193
|
+
ahead=ahead,
|
|
194
|
+
behind=behind,
|
|
195
|
+
changed_files=changed,
|
|
196
|
+
recent_commit=commit,
|
|
197
|
+
recent_commit_time=commit_time,
|
|
198
|
+
todos=todos,
|
|
199
|
+
scripts=scripts,
|
|
200
|
+
score=score,
|
|
201
|
+
reasons=reasons,
|
|
202
|
+
)
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def render_table(reports: Iterable[RepoReport], root: Path) -> str:
|
|
206
|
+
reports = list(reports)
|
|
207
|
+
lines = [f"Workspace Scout: {root}", ""]
|
|
208
|
+
if not reports:
|
|
209
|
+
return "\n".join(lines + ["No git repositories found."])
|
|
210
|
+
|
|
211
|
+
name_w = min(28, max(len(r.name) for r in reports))
|
|
212
|
+
lines.append(f"{'repo'.ljust(name_w)} {'branch'.ljust(18)} score status")
|
|
213
|
+
lines.append(f"{'-' * name_w} {'-' * 18} ----- ------")
|
|
214
|
+
for r in reports:
|
|
215
|
+
branch = (r.branch or "-")[:18].ljust(18)
|
|
216
|
+
status = ", ".join(r.reasons) if r.reasons else "quiet"
|
|
217
|
+
lines.append(f"{r.name[:name_w].ljust(name_w)} {branch} {r.score:>5} {status}")
|
|
218
|
+
return "\n".join(lines)
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
def render_brief(reports: Iterable[RepoReport], root: Path, top: int) -> str:
|
|
222
|
+
reports = list(reports)[:top]
|
|
223
|
+
lines = [f"Top workspace items in {root}:"]
|
|
224
|
+
if not reports:
|
|
225
|
+
return "No git repositories found."
|
|
226
|
+
for idx, r in enumerate(reports, 1):
|
|
227
|
+
reason = ", ".join(r.reasons) if r.reasons else "quiet"
|
|
228
|
+
lines.append(f"{idx}. {r.name} ({r.branch or '-'}) - {reason}")
|
|
229
|
+
if r.recent_commit:
|
|
230
|
+
lines.append(f" last: {r.recent_commit}")
|
|
231
|
+
if r.scripts:
|
|
232
|
+
lines.append(f" scripts: {', '.join(r.scripts)}")
|
|
233
|
+
for todo in r.todos[:2]:
|
|
234
|
+
lines.append(f" note: {todo}")
|
|
235
|
+
return "\n".join(lines)
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def main(argv: list[str] | None = None) -> int:
|
|
239
|
+
parser = argparse.ArgumentParser(
|
|
240
|
+
description="Scan a workspace and rank git repositories that need attention."
|
|
241
|
+
)
|
|
242
|
+
parser.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
243
|
+
parser.add_argument("root", nargs="?", default="~/workspace", help="workspace root")
|
|
244
|
+
parser.add_argument("--top", type=int, default=12, help="number of repos to show")
|
|
245
|
+
parser.add_argument("--depth", type=int, default=2, help="directory search depth")
|
|
246
|
+
parser.add_argument("--todos", type=int, default=3, help="TODO lines per repo")
|
|
247
|
+
parser.add_argument("--json", action="store_true", help="emit JSON")
|
|
248
|
+
parser.add_argument(
|
|
249
|
+
"--brief",
|
|
250
|
+
action="store_true",
|
|
251
|
+
help="emit a compact summary suitable for pasting into Claude Code",
|
|
252
|
+
)
|
|
253
|
+
args = parser.parse_args(argv)
|
|
254
|
+
|
|
255
|
+
root = Path(os.path.expanduser(args.root)).resolve()
|
|
256
|
+
if not root.exists():
|
|
257
|
+
print(f"Root does not exist: {root}", file=sys.stderr)
|
|
258
|
+
return 2
|
|
259
|
+
|
|
260
|
+
repos = find_repos(root, args.depth, DEFAULT_IGNORE)
|
|
261
|
+
with concurrent.futures.ThreadPoolExecutor(max_workers=min(16, max(1, len(repos)))) as pool:
|
|
262
|
+
reports = list(pool.map(lambda p: inspect_repo(p, root, args.todos), repos))
|
|
263
|
+
reports.sort(key=lambda r: (r.score, r.changed_files, r.name.lower()), reverse=True)
|
|
264
|
+
selected = reports[: max(1, args.top)]
|
|
265
|
+
|
|
266
|
+
if args.json:
|
|
267
|
+
print(json.dumps([asdict(r) for r in selected], ensure_ascii=False, indent=2))
|
|
268
|
+
elif args.brief:
|
|
269
|
+
print(render_brief(selected, root, args.top))
|
|
270
|
+
else:
|
|
271
|
+
print(render_table(selected, root))
|
|
272
|
+
print("")
|
|
273
|
+
print("Tip: run with --brief to paste the summary into Claude Code.")
|
|
274
|
+
return 0
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
if __name__ == "__main__":
|
|
278
|
+
raise SystemExit(main())
|