gitroster 0.2.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.
@@ -0,0 +1,9 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 gitroster contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
6
+
7
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,138 @@
1
+ Metadata-Version: 2.4
2
+ Name: gitroster
3
+ Version: 0.2.0
4
+ Summary: Find the repos, virtual envs and open ports you forgot about, and how to run them.
5
+ Author: firatsahn
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/firatsahn/gitroster
8
+ Project-URL: Source, https://github.com/firatsahn/gitroster
9
+ Project-URL: Issues, https://github.com/firatsahn/gitroster/issues
10
+ Project-URL: Changelog, https://github.com/firatsahn/gitroster/blob/main/CHANGELOG.md
11
+ Keywords: developer-tools,git,ports,virtualenv,dashboard,local-first
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Web Environment
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: POSIX :: Linux
17
+ Classifier: Operating System :: MacOS
18
+ Classifier: Operating System :: Microsoft :: Windows
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Topic :: Software Development
21
+ Requires-Python: >=3.9
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Dynamic: license-file
25
+
26
+ <p align="center">
27
+ <img src="https://raw.githubusercontent.com/firatsahn/gitroster/main/gitroster/static/icon.svg" width="72" alt="gitroster logo">
28
+ </p>
29
+
30
+ <h1 align="center">gitroster</h1>
31
+
32
+ <p align="center">
33
+ <b>A roster of every git repo on your machine: yours, forks and clones.<br>What each one is, what's running, and how to run it.</b>
34
+ </p>
35
+
36
+ ---
37
+
38
+ <p align="center">
39
+ <img src="https://raw.githubusercontent.com/firatsahn/gitroster/main/docs/home-light.png" alt="gitroster: a list of repos with running ports, uncommitted work, an AI summary and a note" width="860">
40
+ </p>
41
+
42
+ You clone a lot of open source projects, fork some, and write your own. Months later you have no idea which ones are still on disk, which are running in the background, which port belongs to what, or how you started them. gitroster scans your folders and puts it all on one page.
43
+
44
+ ## Quick start (web, recommended)
45
+
46
+ ```bash
47
+ pip install gitroster # or: pipx install gitroster
48
+ gitroster ~/projects # then open http://127.0.0.1:8765
49
+ ```
50
+
51
+ From source, without installing:
52
+
53
+ ```bash
54
+ git clone https://github.com/firatsahn/gitroster && cd gitroster
55
+ python3 -m gitroster ~/projects
56
+ ```
57
+
58
+ Want to look around first? `gitroster --demo` shows sample repos and touches nothing on your machine. Running, stopping and opening are switched off in the demo.
59
+
60
+ ### Desktop app (Electron)
61
+
62
+ Prefer a window over a browser tab? The same UI runs in an Electron shell on Windows, macOS and Linux:
63
+
64
+ ```bash
65
+ cd electron && npm install && npm start
66
+ ```
67
+
68
+ It starts the local server on a free port and stops it when you close the window. It needs Python 3.9+ on the machine (set `GITROSTER_PYTHON` if it is not on your PATH). It scans `~/projects`, or your home folder if that does not exist; set `GITROSTER_ROOTS` to scan other folders. `npm run dist` builds an installer with electron-builder.
69
+
70
+ ### Platforms
71
+
72
+ Linux, macOS and Windows are supported. Ports come from `ss` (Linux), `lsof` (macOS) or `netstat` (Windows). On Windows a process's working directory is not readable, so ports are matched to repos by the command line only.
73
+
74
+ Scan several folders at once with `gitroster ~/projects ~/tools`. Without arguments it scans your home folder. Use `gitroster --json` to print a scan for scripts.
75
+
76
+ ## What you get
77
+
78
+ **Repos**
79
+ - Every git repo, with last commit, branch, size on disk and README summary
80
+ - Sorted by origin: **mine** (you authored commits), **fork** (has an `upstream` remote), **clone** (someone else's) and **local** (no remote)
81
+ - Flags for **uncommitted changes**, **unpushed commits**, **no remote** and **dormant** (no commit in 90+ days and nothing running)
82
+ - Notes, pin and ignore, kept in `~/.config/gitroster/` (`~/Library/Application Support/gitroster` on macOS, `%APPDATA%\gitroster` on Windows)
83
+
84
+ **Look inside any repo**
85
+ - A detail page per repo: overview (languages, contributors linked to GitHub, tags, most changed files, README), file browser with a viewer, commit history with a graph and per-commit diffs, branches, dependencies
86
+ - **Search** code, file names and commit messages across every repo at once (plain text or regex)
87
+ - Search also finds **packages**: which repo uses a package and at which version, flagged when versions differ between repos
88
+ - **Related repos**: copies of the same remote, and local repos that depend on each other
89
+ - Everything is read-only
90
+
91
+ **Work you might forget**
92
+ - Stashes, branches that exist only on this machine or are not merged yet, and extra worktrees, with filters for each
93
+ - A commit graph of every branch (click a branch to jump to it)
94
+ - Light, dark or system theme in Settings
95
+ - One-click open in your editor, terminal or file manager. Settings lists the editors (VS Code, Cursor, PyCharm, IntelliJ, Zed, Sublime and more) and terminals installed on your machine so you can pick one, or set a custom command
96
+
97
+ <p align="center">
98
+ <img src="https://raw.githubusercontent.com/firatsahn/gitroster/main/docs/detail.png" alt="Repo detail page with an AI summary, languages, contributors and most changed files" width="860">
99
+ </p>
100
+
101
+ **How to run it**
102
+ - Setup and run commands detected from `package.json` (npm, pnpm, yarn), `pyproject.toml`, `requirements.txt`, `manage.py`, `main.py`/`app.py`, `Cargo.toml`, `go.mod`, `pubspec.yaml`, `Makefile`, Docker Compose and Wrangler
103
+ - Commands use the repo's own virtual environment when one exists
104
+ - Commands and ports mentioned in the README
105
+ - **▶ Start** runs a detected command in the background with a log (after you confirm)
106
+
107
+ **Ports and processes**
108
+ - Every listening port you own, matched to a repo by the process working directory, then by the command line
109
+ - Ports open on all interfaces are flagged
110
+ - One click to open it in the browser, one to stop it
111
+ - Docker containers, matched to their compose folder
112
+
113
+ **AI summary (optional)**
114
+ - Press the sparkle button next to **Details** and an agent CLI you already have writes a short summary of the repo: what it is, stack, how to run it, what you were working on, gotchas. It is labelled *AI-generated, unverified*, shown on the card and on the repo's Overview, and marked when the repo has changed since.
115
+ - gitroster finds the agent CLIs installed on your machine (Claude Code, Codex CLI, Gemini CLI) and you pick one in Settings. There is no API key to enter and no model to host: it runs the CLI you are already signed in to, with its tools turned off, and nothing is executed.
116
+ - It sends that agent the README, a file list (secret-looking files are left out), dependency names, detected commands and recent commit titles. Where that goes depends on the agent: all three are cloud services.
117
+ - Prefer another tool? Use any command that reads a prompt on stdin: a custom command in Settings, `gitroster --agent-cmd "my-agent"` or `GITROSTER_AGENT`.
118
+ - Only Claude Code is tested here. The Codex and Gemini invocations follow their documented non-interactive modes but are untested.
119
+
120
+ ## How it works
121
+
122
+ Detection is deterministic: file checks, `git`, `du`, `ss` and `/proc`. The AI part is opt-in and separate. See [SECURITY.md](SECURITY.md) for the safety model: loopback only, host and origin checks, and start/stop limited to things the scanner found.
123
+
124
+ ## Limits
125
+
126
+ - Linux, macOS and Windows. On Windows a process's working directory is not readable, so ports are matched by command line only.
127
+ - Only processes owned by your user are visible.
128
+ - Hidden folders (`~/.nvm`), `node_modules` and anything deeper than 6 levels are skipped.
129
+ - A big first scan can take a while on slow disks or `/mnt/c`. The last result is shown instantly while a new scan runs.
130
+ - Run commands are guesses from well-known files. Treat them as a starting point.
131
+
132
+ ## Contributing
133
+
134
+ Issues and PRs welcome, see [CONTRIBUTING.md](CONTRIBUTING.md). Run the tests with `python3 -m unittest discover -s tests`.
135
+
136
+ ## License
137
+
138
+ MIT
@@ -0,0 +1,113 @@
1
+ <p align="center">
2
+ <img src="https://raw.githubusercontent.com/firatsahn/gitroster/main/gitroster/static/icon.svg" width="72" alt="gitroster logo">
3
+ </p>
4
+
5
+ <h1 align="center">gitroster</h1>
6
+
7
+ <p align="center">
8
+ <b>A roster of every git repo on your machine: yours, forks and clones.<br>What each one is, what's running, and how to run it.</b>
9
+ </p>
10
+
11
+ ---
12
+
13
+ <p align="center">
14
+ <img src="https://raw.githubusercontent.com/firatsahn/gitroster/main/docs/home-light.png" alt="gitroster: a list of repos with running ports, uncommitted work, an AI summary and a note" width="860">
15
+ </p>
16
+
17
+ You clone a lot of open source projects, fork some, and write your own. Months later you have no idea which ones are still on disk, which are running in the background, which port belongs to what, or how you started them. gitroster scans your folders and puts it all on one page.
18
+
19
+ ## Quick start (web, recommended)
20
+
21
+ ```bash
22
+ pip install gitroster # or: pipx install gitroster
23
+ gitroster ~/projects # then open http://127.0.0.1:8765
24
+ ```
25
+
26
+ From source, without installing:
27
+
28
+ ```bash
29
+ git clone https://github.com/firatsahn/gitroster && cd gitroster
30
+ python3 -m gitroster ~/projects
31
+ ```
32
+
33
+ Want to look around first? `gitroster --demo` shows sample repos and touches nothing on your machine. Running, stopping and opening are switched off in the demo.
34
+
35
+ ### Desktop app (Electron)
36
+
37
+ Prefer a window over a browser tab? The same UI runs in an Electron shell on Windows, macOS and Linux:
38
+
39
+ ```bash
40
+ cd electron && npm install && npm start
41
+ ```
42
+
43
+ It starts the local server on a free port and stops it when you close the window. It needs Python 3.9+ on the machine (set `GITROSTER_PYTHON` if it is not on your PATH). It scans `~/projects`, or your home folder if that does not exist; set `GITROSTER_ROOTS` to scan other folders. `npm run dist` builds an installer with electron-builder.
44
+
45
+ ### Platforms
46
+
47
+ Linux, macOS and Windows are supported. Ports come from `ss` (Linux), `lsof` (macOS) or `netstat` (Windows). On Windows a process's working directory is not readable, so ports are matched to repos by the command line only.
48
+
49
+ Scan several folders at once with `gitroster ~/projects ~/tools`. Without arguments it scans your home folder. Use `gitroster --json` to print a scan for scripts.
50
+
51
+ ## What you get
52
+
53
+ **Repos**
54
+ - Every git repo, with last commit, branch, size on disk and README summary
55
+ - Sorted by origin: **mine** (you authored commits), **fork** (has an `upstream` remote), **clone** (someone else's) and **local** (no remote)
56
+ - Flags for **uncommitted changes**, **unpushed commits**, **no remote** and **dormant** (no commit in 90+ days and nothing running)
57
+ - Notes, pin and ignore, kept in `~/.config/gitroster/` (`~/Library/Application Support/gitroster` on macOS, `%APPDATA%\gitroster` on Windows)
58
+
59
+ **Look inside any repo**
60
+ - A detail page per repo: overview (languages, contributors linked to GitHub, tags, most changed files, README), file browser with a viewer, commit history with a graph and per-commit diffs, branches, dependencies
61
+ - **Search** code, file names and commit messages across every repo at once (plain text or regex)
62
+ - Search also finds **packages**: which repo uses a package and at which version, flagged when versions differ between repos
63
+ - **Related repos**: copies of the same remote, and local repos that depend on each other
64
+ - Everything is read-only
65
+
66
+ **Work you might forget**
67
+ - Stashes, branches that exist only on this machine or are not merged yet, and extra worktrees, with filters for each
68
+ - A commit graph of every branch (click a branch to jump to it)
69
+ - Light, dark or system theme in Settings
70
+ - One-click open in your editor, terminal or file manager. Settings lists the editors (VS Code, Cursor, PyCharm, IntelliJ, Zed, Sublime and more) and terminals installed on your machine so you can pick one, or set a custom command
71
+
72
+ <p align="center">
73
+ <img src="https://raw.githubusercontent.com/firatsahn/gitroster/main/docs/detail.png" alt="Repo detail page with an AI summary, languages, contributors and most changed files" width="860">
74
+ </p>
75
+
76
+ **How to run it**
77
+ - Setup and run commands detected from `package.json` (npm, pnpm, yarn), `pyproject.toml`, `requirements.txt`, `manage.py`, `main.py`/`app.py`, `Cargo.toml`, `go.mod`, `pubspec.yaml`, `Makefile`, Docker Compose and Wrangler
78
+ - Commands use the repo's own virtual environment when one exists
79
+ - Commands and ports mentioned in the README
80
+ - **▶ Start** runs a detected command in the background with a log (after you confirm)
81
+
82
+ **Ports and processes**
83
+ - Every listening port you own, matched to a repo by the process working directory, then by the command line
84
+ - Ports open on all interfaces are flagged
85
+ - One click to open it in the browser, one to stop it
86
+ - Docker containers, matched to their compose folder
87
+
88
+ **AI summary (optional)**
89
+ - Press the sparkle button next to **Details** and an agent CLI you already have writes a short summary of the repo: what it is, stack, how to run it, what you were working on, gotchas. It is labelled *AI-generated, unverified*, shown on the card and on the repo's Overview, and marked when the repo has changed since.
90
+ - gitroster finds the agent CLIs installed on your machine (Claude Code, Codex CLI, Gemini CLI) and you pick one in Settings. There is no API key to enter and no model to host: it runs the CLI you are already signed in to, with its tools turned off, and nothing is executed.
91
+ - It sends that agent the README, a file list (secret-looking files are left out), dependency names, detected commands and recent commit titles. Where that goes depends on the agent: all three are cloud services.
92
+ - Prefer another tool? Use any command that reads a prompt on stdin: a custom command in Settings, `gitroster --agent-cmd "my-agent"` or `GITROSTER_AGENT`.
93
+ - Only Claude Code is tested here. The Codex and Gemini invocations follow their documented non-interactive modes but are untested.
94
+
95
+ ## How it works
96
+
97
+ Detection is deterministic: file checks, `git`, `du`, `ss` and `/proc`. The AI part is opt-in and separate. See [SECURITY.md](SECURITY.md) for the safety model: loopback only, host and origin checks, and start/stop limited to things the scanner found.
98
+
99
+ ## Limits
100
+
101
+ - Linux, macOS and Windows. On Windows a process's working directory is not readable, so ports are matched by command line only.
102
+ - Only processes owned by your user are visible.
103
+ - Hidden folders (`~/.nvm`), `node_modules` and anything deeper than 6 levels are skipped.
104
+ - A big first scan can take a while on slow disks or `/mnt/c`. The last result is shown instantly while a new scan runs.
105
+ - Run commands are guesses from well-known files. Treat them as a starting point.
106
+
107
+ ## Contributing
108
+
109
+ Issues and PRs welcome, see [CONTRIBUTING.md](CONTRIBUTING.md). Run the tests with `python3 -m unittest discover -s tests`.
110
+
111
+ ## License
112
+
113
+ MIT
@@ -0,0 +1 @@
1
+ __version__ = "0.2.0"
@@ -0,0 +1,4 @@
1
+ from .cli import main
2
+ import sys
3
+
4
+ sys.exit(main())
@@ -0,0 +1,157 @@
1
+ """Optional: ask a coding agent CLI you already have to summarize a repo.
2
+
3
+ Nothing here talks to a network service itself and no API keys are handled. gitroster only runs the
4
+ agent CLI you chose, feeds it a prompt on stdin and asks it not to use tools. The answer is stored
5
+ as "AI-generated, unverified".
6
+ """
7
+ import json
8
+ import os
9
+ import re
10
+ import shlex
11
+ import subprocess
12
+ import time
13
+ from pathlib import Path
14
+
15
+ from . import scanner
16
+ from .pathscan import which
17
+
18
+ # id, label, program, arguments, how to install. The prompt is written to stdin. Every entry must keep
19
+ # the agent away from tools: the summary needs only text, and nothing should be run or edited.
20
+ AGENTS = [
21
+ ("claude", "Claude Code", "claude", ["-p", "--disallowedTools", "Bash,Edit,Write,NotebookEdit"],
22
+ "npm install -g @anthropic-ai/claude-code"),
23
+ ("codex", "Codex CLI", "codex", ["exec", "--sandbox", "read-only", "--skip-git-repo-check", "-"],
24
+ "npm install -g @openai/codex"),
25
+ ("gemini", "Gemini CLI", "gemini", ["-p", "Follow the instructions in the input exactly. Do not use tools."],
26
+ "npm install -g @google/gemini-cli"),
27
+ ]
28
+
29
+ PROMPT = """You are helping a developer remember a project they wrote or cloned long ago.
30
+ Use ONLY the facts below. If something is not in the facts, say "unknown". Do not run commands or use tools.
31
+
32
+ Answer with ONLY a JSON object (no markdown fence) with these string keys:
33
+ "what": one sentence on what this project is and does
34
+ "stack": the main languages, frameworks and services, short
35
+ "run": how to set it up and start it, 1-3 short steps, or "unknown"
36
+ "status": what the recent commits, unmerged branches and stashes suggest was being worked on, 1-2 sentences
37
+ "watch": gotchas such as API keys, GPUs, databases, large downloads; "none" if nothing is apparent
38
+
39
+ --- FACTS ---
40
+ Name: {name}
41
+ Path: {path}
42
+ Kind: {kind} Branch: {branch} Uncommitted changes: {dirty} Unpushed commits: {ahead}
43
+ Detected stack: {stack}
44
+ Setup commands: {setup}
45
+ Run commands: {run}
46
+ Dependencies: {deps}
47
+ Local-only or unmerged branches: {branches}
48
+ Stashes: {stashes}
49
+
50
+ Recent commits (newest first):
51
+ {commits}
52
+
53
+ File tree (depth 2):
54
+ {tree}
55
+
56
+ README (truncated):
57
+ {readme}
58
+ """
59
+
60
+
61
+ def detect():
62
+ """Agent CLIs installed on this machine."""
63
+ return [{"id": e[0], "label": e[1]} for e in AGENTS if which(e[2])]
64
+
65
+
66
+ def catalog():
67
+ """Every agent gitroster knows how to drive, with whether it is installed here."""
68
+ return [{"id": i, "label": label, "installed": bool(which(prog)), "install": hint} for i, label, prog, _, hint in AGENTS]
69
+
70
+
71
+ def _resolve(argv):
72
+ return [which(argv[0]) or argv[0], *argv[1:]]
73
+
74
+
75
+ def command(settings=None, override=None):
76
+ """-> (argv, label). Precedence: --agent-cmd / $GITROSTER_AGENT, a custom command from Settings,
77
+ the agent picked in Settings, then the first one found."""
78
+ explicit = override or os.environ.get("GITROSTER_AGENT")
79
+ if explicit:
80
+ return _resolve(shlex.split(explicit)), "custom command"
81
+ settings = settings or {}
82
+ if settings.get("agent") == "custom" and settings.get("agent_custom"):
83
+ return _resolve(shlex.split(settings["agent_custom"])), "custom command"
84
+ entries = [e for e in AGENTS if which(e[2])]
85
+ chosen = next((e for e in entries if e[0] == settings.get("agent")), entries[0] if entries else None)
86
+ if not chosen:
87
+ return None, ""
88
+ return [which(chosen[2]), *chosen[3]], chosen[1]
89
+
90
+
91
+ def tree(path, depth=2, limit=80):
92
+ root = Path(path)
93
+ out = []
94
+ for dirpath, dirs, files in os.walk(root):
95
+ rel = Path(dirpath).relative_to(root)
96
+ if len(rel.parts) >= depth:
97
+ dirs[:] = []
98
+ dirs[:] = sorted(d for d in dirs if d not in (".git", "node_modules", "__pycache__") and not d.startswith(".venv"))
99
+ for name in sorted(files)[:15]:
100
+ shown = str(rel / name) if rel.parts else name
101
+ if not scanner.SECRET_FILES.search(shown): # never even list secret-looking files
102
+ out.append(shown)
103
+ if len(out) >= limit:
104
+ break
105
+ return "\n".join(out[:limit])
106
+
107
+
108
+ def build_prompt(repo):
109
+ p = Path(repo["path"])
110
+ text = ""
111
+ for n in ("README.md", "readme.md", "README.rst", "README.txt"):
112
+ if (p / n).exists():
113
+ text = (p / n).read_text(errors="replace")[:6000]
114
+ break
115
+ w = repo.get("work") or {}
116
+ commits = "\n".join(f"- {c['subject']}" for c in scanner.graph(p, 15)) or "none"
117
+ deps = ", ".join(d["name"] for d in repo.get("deps", [])[:25]) or "none"
118
+ return PROMPT.format(
119
+ name=repo["name"], path=repo["path"], kind=repo.get("kind", ""), branch=repo.get("branch", ""),
120
+ dirty="yes" if repo.get("dirty") else "no", ahead=repo.get("ahead", 0),
121
+ stack=", ".join(repo["stack"]) or "unknown", setup="; ".join(repo["setup"]) or "none",
122
+ run="; ".join(repo["run"]) or "none", deps=deps,
123
+ branches=", ".join(f"{b['name']} ({b['state']})" for b in w.get("branches", [])[:8]) or "none",
124
+ stashes="; ".join(s["message"] for s in w.get("stashes", [])[:5]) or "none",
125
+ commits=commits, tree=tree(p), readme=text or "(no README)")
126
+
127
+
128
+ KEYS = ("what", "stack", "run", "status", "watch")
129
+
130
+
131
+ def parse(text):
132
+ """The agent's answer as {what, stack, run, status, watch}; falls back to the raw text."""
133
+ cleaned = re.sub(r"^```(?:json)?\s*|\s*```$", "", text.strip())
134
+ for candidate in (cleaned, cleaned[cleaned.find("{"):cleaned.rfind("}") + 1]):
135
+ try:
136
+ data = json.loads(candidate)
137
+ except ValueError:
138
+ continue
139
+ if isinstance(data, dict) and any(k in data for k in KEYS):
140
+ return {k: str(data.get(k, "")).strip()[:600] for k in KEYS}
141
+ return {"raw": text.strip()[:2000]}
142
+
143
+
144
+ def summarize(repo, settings=None, override=None, timeout=180):
145
+ argv, label = command(settings, override)
146
+ if not argv:
147
+ raise RuntimeError("no agent CLI found; install Claude Code, Codex or Gemini CLI, or set a custom command in Settings")
148
+ try:
149
+ r = subprocess.run(argv, input=build_prompt(repo), capture_output=True, text=True, encoding="utf-8",
150
+ errors="replace", timeout=timeout, cwd=repo["path"])
151
+ except FileNotFoundError:
152
+ raise RuntimeError(f"agent command not found: {argv[0]}")
153
+ except subprocess.TimeoutExpired:
154
+ raise RuntimeError("the agent took too long")
155
+ if r.returncode != 0 or not r.stdout.strip():
156
+ raise RuntimeError((r.stderr or "the agent returned nothing").strip()[:300])
157
+ return {**parse(r.stdout), "agent": label, "head": repo.get("head", ""), "at": time.time()}
@@ -0,0 +1,43 @@
1
+ import argparse
2
+ import json
3
+ import sys
4
+ import webbrowser
5
+ from pathlib import Path
6
+
7
+ from . import __version__, demo, scanner
8
+ from .server import App, serve
9
+
10
+
11
+ def main(argv=None):
12
+ ap = argparse.ArgumentParser(prog="gitroster",
13
+ description="Find the repos, envs and ports you forgot about.")
14
+ ap.add_argument("roots", nargs="*", default=[str(Path.home())], help="folders to scan (default: ~)")
15
+ ap.add_argument("--port", type=int, default=8765)
16
+ ap.add_argument("--json", action="store_true", help="print one scan as JSON and exit")
17
+ ap.add_argument("--open", action="store_true", help="open the page in a browser")
18
+ ap.add_argument("--agent-cmd", help='command for AI summaries; reads the prompt on stdin '
19
+ '(default: "claude -p", or $GITROSTER_AGENT)')
20
+ ap.add_argument("--demo", action="store_true", help="show sample repos instead of scanning your machine")
21
+ ap.add_argument("--version", action="version", version=f"gitroster {__version__}")
22
+ a = ap.parse_args(argv)
23
+
24
+ if a.json:
25
+ json.dump(scanner.scan(a.roots), sys.stdout, indent=2)
26
+ return 0
27
+
28
+ if a.demo:
29
+ roots, store = demo.setup()
30
+ app = App(roots, a.port, store=store, agent_cmd=a.agent_cmd, demo=True)
31
+ a.roots = ["sample repos"]
32
+ else:
33
+ app = App(a.roots, a.port, agent_cmd=a.agent_cmd)
34
+ url = f"http://127.0.0.1:{a.port}"
35
+ print(f"gitroster {__version__} on {url} (scanning {', '.join(a.roots)})")
36
+ if a.open:
37
+ webbrowser.open(url)
38
+ serve(app)
39
+ return 0
40
+
41
+
42
+ if __name__ == "__main__":
43
+ sys.exit(main())
@@ -0,0 +1,140 @@
1
+ """`gitroster --demo`: a throwaway set of sample repos, so the UI can be tried or photographed without
2
+ scanning a real machine. Nothing here touches your own folders, settings or processes."""
3
+ import atexit
4
+ import os
5
+ import shutil
6
+ import subprocess
7
+ import tempfile
8
+ import time
9
+ from pathlib import Path
10
+
11
+ from . import procs
12
+ from .store import Store
13
+
14
+ DAY = 86400
15
+ ME = ("Demo User", "demo@example.com")
16
+
17
+ # name, remote, files, [(days ago, message)], extra setup name
18
+ REPOS = [
19
+ ("atlas-api", "https://github.com/demo-user/atlas-api.git", {
20
+ "README.md": "# Atlas API\n\nA small REST API that serves product data for the storefront.\n\n```bash\nnpm run dev\n```\n\nOpen http://localhost:3000\n",
21
+ "package.json": '{"name":"atlas-api","scripts":{"dev":"node server.js","start":"node server.js"},"dependencies":{"express":"^4.19.0","zod":"^3.23.0"}}',
22
+ ".gitignore": "node_modules\n.env\n",
23
+ "server.js": "require('express')().listen(3000)\n"},
24
+ [(40, "Add product schema"), (9, "Validate query params"), (2, "Cache product list")], "ahead"),
25
+ ("bookshelf", "https://github.com/demo-user/bookshelf.git", {
26
+ "README.md": "# Bookshelf\n\nTrack the books you read and what you thought of them.\n",
27
+ "requirements.txt": "flask==3.0.3\nsqlalchemy==2.0.30\n",
28
+ "app.py": "print('bookshelf')\n",
29
+ ".gitignore": ".venv\n"},
30
+ [(120, "First version"), (30, "Add ratings"), (5, "Import from CSV")], "work"),
31
+ ("dashboard-ui", "https://github.com/demo-user/dashboard-ui.git", {
32
+ "README.md": "# Dashboard UI\n\nFront end for the Atlas API, built with Vite.\n\nRuns on http://localhost:5173\n",
33
+ "package.json": '{"name":"dashboard-ui","scripts":{"dev":"vite"},"dependencies":{"vite":"^5.2.0","zod":"^3.22.0"}}',
34
+ ".gitignore": "node_modules\n"},
35
+ [(60, "Scaffold with Vite"), (3, "Add chart panel")], None),
36
+ ("rust-notes", "https://github.com/someone/rust-notes.git", {
37
+ "README.md": "# rust-notes\n\nA command line notebook written in Rust.\n",
38
+ "Cargo.toml": '[package]\nname = "rust-notes"\nversion = "0.3.0"\n\n[dependencies]\nclap = "4"\n',
39
+ ".gitignore": "target\n"},
40
+ [(300, "Release 0.3.0")], "clone"),
41
+ ("weather-cli", "https://github.com/demo-user/weather-cli.git", {
42
+ "README.md": "# weather-cli\n\nPrints the weather forecast in your terminal. Forked to add metric units.\n",
43
+ "go.mod": "module example.com/weather\n\ngo 1.22\n",
44
+ "main.go": "package main\nfunc main() {}\n"},
45
+ [(210, "Fork upstream"), (190, "Add metric units")], "fork"),
46
+ ("old-experiment", None, {
47
+ "README.md": "# old experiment\n\nSomething I tried with a camera feed and never finished.\n",
48
+ "main.py": "print('todo')\n",
49
+ "requirements.txt": "opencv-python\n",
50
+ ".env": "CAMERA_KEY=demo\n"},
51
+ [(480, "wip"), (470, "wip 2")], "dirty"),
52
+ ]
53
+
54
+ SUMMARIES = {
55
+ "atlas-api": {
56
+ "what": "REST API that serves product data to the storefront.",
57
+ "stack": "Node.js, Express, Zod",
58
+ "run": "npm run dev, then open http://localhost:3000",
59
+ "status": "Active. The last work added caching for the product list.",
60
+ "watch": "Two commits are not pushed yet.",
61
+ },
62
+ "bookshelf": {
63
+ "what": "Small web app to track the books you read, with ratings and CSV import.",
64
+ "stack": "Python, Flask, SQLAlchemy",
65
+ "run": "python3 -m venv .venv, install requirements.txt, then python3 app.py",
66
+ "status": "Was working on CSV import: there are uncommitted changes and a stash.",
67
+ "watch": "Check the stash before starting something new.",
68
+ },
69
+ }
70
+
71
+ NOTES = {"bookshelf": "Finish the CSV import, then deploy to the home server."}
72
+
73
+ PORTS = [("atlas-api", 3000, "node server.js", "node"), ("dashboard-ui", 5173, "node node_modules/.bin/vite", "node")]
74
+
75
+
76
+ def _git(path, *args, when=None):
77
+ env = {**os.environ, "GIT_AUTHOR_NAME": ME[0], "GIT_AUTHOR_EMAIL": ME[1],
78
+ "GIT_COMMITTER_NAME": ME[0], "GIT_COMMITTER_EMAIL": ME[1]}
79
+ if when:
80
+ env["GIT_AUTHOR_DATE"] = env["GIT_COMMITTER_DATE"] = f"{int(when)} +0000"
81
+ subprocess.run(["git", "-C", str(path), "-c", "commit.gpgsign=false", *args], env=env, check=True, capture_output=True)
82
+
83
+
84
+ def _build(root, name, remote, files, commits, extra):
85
+ p = Path(root) / name
86
+ p.mkdir(parents=True)
87
+ _git(p, "init", "-q", "-b", "main")
88
+ _git(p, "config", "user.email", ME[1])
89
+ _git(p, "config", "user.name", ME[0])
90
+ for f, content in files.items():
91
+ (p / f).write_text(content)
92
+ now = time.time()
93
+ for i, (days, msg) in enumerate(commits):
94
+ (p / "CHANGELOG.txt").write_text("\n".join(m for _, m in commits[: i + 1]) + "\n")
95
+ _git(p, "add", "-A", "-f")
96
+ _git(p, "commit", "-q", "-m", msg, when=now - days * DAY)
97
+ if remote:
98
+ _git(p, "remote", "add", "origin", remote)
99
+ if extra == "clone": # the commits are not yours: your configured email no longer matches the author
100
+ _git(p, "config", "user.email", "me@example.org")
101
+ if extra == "fork":
102
+ _git(p, "remote", "add", "upstream", "https://github.com/original/weather-cli.git")
103
+ _git(p, "update-ref", "refs/remotes/origin/main", "HEAD~2" if extra == "ahead" else "HEAD")
104
+ _git(p, "config", "branch.main.remote", "origin")
105
+ _git(p, "config", "branch.main.merge", "refs/heads/main")
106
+ if extra == "work":
107
+ _git(p, "checkout", "-q", "-b", "csv-import")
108
+ (p / "importer.py").write_text("def load(path):\n pass\n")
109
+ _git(p, "add", "-A")
110
+ _git(p, "commit", "-q", "-m", "Start CSV importer", when=now - 5 * DAY)
111
+ _git(p, "checkout", "-q", "main")
112
+ (p / "scratch.py").write_text("print('try')\n")
113
+ _git(p, "add", "-A")
114
+ _git(p, "stash", "-q")
115
+ (p / "app.py").write_text("print('bookshelf, edited')\n")
116
+ if extra == "dirty":
117
+ (p / "main.py").write_text("print('half done')\n")
118
+ return p
119
+
120
+
121
+ def setup():
122
+ """Create the sample repos, fake the port list, and return (roots, store)."""
123
+ base = Path(tempfile.mkdtemp(prefix="gitroster-demo-")).resolve()
124
+ atexit.register(shutil.rmtree, base, ignore_errors=True)
125
+ root = base / "projects"
126
+ paths = {}
127
+ for name, remote, files, commits, extra in REPOS:
128
+ paths[name] = _build(root, name, remote, files, commits, extra)
129
+ store = Store(base / "config", base / "state") # state.parent is `base`, which the page shows as ~
130
+ for name, summary in SUMMARIES.items():
131
+ head = subprocess.run(["git", "-C", str(paths[name]), "rev-parse", "HEAD"], capture_output=True, text=True).stdout.strip()
132
+ store.update_meta(str(paths[name].resolve()), summary={**summary, "agent": "Demo", "head": head, "at": time.time() - 3600})
133
+ for name, note in NOTES.items():
134
+ store.update_meta(str(paths[name].resolve()), note=note)
135
+ store.update_meta(str(paths["atlas-api"].resolve()), pinned=True)
136
+ ports = [dict(procs._entry(port, "127.0.0.1", 40000 + i, proc, cmd, str(paths[name].resolve()), time.time() - 7200))
137
+ for i, (name, port, cmd, proc) in enumerate(PORTS)]
138
+ procs.listening = lambda: [dict(p) for p in ports] # never read the real machine's ports
139
+ procs.docker_containers = lambda: []
140
+ return [str(root)], store