codemap-python 0.1.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.
- analysis/__init__.py +1 -0
- analysis/architecture/__init__.py +1 -0
- analysis/architecture/architecture_engine.py +155 -0
- analysis/architecture/dependency_cycles.py +103 -0
- analysis/architecture/risk_radar.py +220 -0
- analysis/call_graph/__init__.py +1 -0
- analysis/call_graph/call_extractor.py +91 -0
- analysis/call_graph/call_graph_builder.py +1 -0
- analysis/call_graph/call_resolver.py +56 -0
- analysis/call_graph/context_models.py +1 -0
- analysis/call_graph/cross_file_resolver.py +122 -0
- analysis/call_graph/execution_tracker.py +1 -0
- analysis/call_graph/flow_builder.py +1 -0
- analysis/call_graph/models.py +1 -0
- analysis/core/__init__.py +1 -0
- analysis/core/ast_context.py +1 -0
- analysis/core/ast_parser.py +8 -0
- analysis/core/class_extractor.py +35 -0
- analysis/core/function_extractor.py +16 -0
- analysis/core/import_extractor.py +43 -0
- analysis/explain/__init__.py +1 -0
- analysis/explain/docstring_extractor.py +45 -0
- analysis/explain/explain_runner.py +177 -0
- analysis/explain/repo_summary_generator.py +138 -0
- analysis/explain/return_analyzer.py +114 -0
- analysis/explain/risk_flags.py +1 -0
- analysis/explain/signature_extractor.py +104 -0
- analysis/explain/summary_generator.py +282 -0
- analysis/graph/__init__.py +1 -0
- analysis/graph/callgraph_index.py +117 -0
- analysis/graph/entrypoint_detector.py +1 -0
- analysis/graph/impact_analyzer.py +210 -0
- analysis/indexing/__init__.py +1 -0
- analysis/indexing/import_resolver.py +156 -0
- analysis/indexing/symbol_index.py +150 -0
- analysis/runners/__init__.py +1 -0
- analysis/runners/phase4_runner.py +137 -0
- analysis/utils/__init__.py +1 -0
- analysis/utils/ast_helpers.py +1 -0
- analysis/utils/cache_manager.py +659 -0
- analysis/utils/path_resolver.py +1 -0
- analysis/utils/repo_fetcher.py +469 -0
- cli.py +1728 -0
- codemap_cli.py +11 -0
- codemap_python-0.1.0.dist-info/METADATA +399 -0
- codemap_python-0.1.0.dist-info/RECORD +58 -0
- codemap_python-0.1.0.dist-info/WHEEL +5 -0
- codemap_python-0.1.0.dist-info/entry_points.txt +2 -0
- codemap_python-0.1.0.dist-info/top_level.txt +5 -0
- security_utils.py +51 -0
- ui/__init__.py +1 -0
- ui/app.py +2160 -0
- ui/device_id.py +27 -0
- ui/static/app.js +2703 -0
- ui/static/styles.css +1268 -0
- ui/templates/index.html +231 -0
- ui/utils/__init__.py +1 -0
- ui/utils/registry_manager.py +190 -0
ui/templates/index.html
ADDED
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>CodeMap UI</title>
|
|
7
|
+
<link rel="stylesheet" href="/static/styles.css" />
|
|
8
|
+
</head>
|
|
9
|
+
<body>
|
|
10
|
+
<header class="topbar">
|
|
11
|
+
<div class="brand">
|
|
12
|
+
<div class="title">CodeMap</div>
|
|
13
|
+
<div class="repo-name-wrap">
|
|
14
|
+
<div id="repo-name" class="repo-name">No repo selected</div>
|
|
15
|
+
<span id="repo-private-badge" class="repo-badge private hidden">PRIVATE MODE</span>
|
|
16
|
+
</div>
|
|
17
|
+
</div>
|
|
18
|
+
<div class="workspace-controls">
|
|
19
|
+
<select id="repo-select" class="repo-select"></select>
|
|
20
|
+
<button id="add-repo-btn" class="repo-btn" type="button">+ Add repo...</button>
|
|
21
|
+
<button id="ai-settings-btn" class="repo-btn" type="button" onclick="var m=document.getElementById('ai-settings-modal'); if(m){ m.classList.remove('hidden'); document.body.classList.add('modal-open'); }">Settings</button>
|
|
22
|
+
<span id="repo-list-mode-pill" class="repo-badge">Session Mode</span>
|
|
23
|
+
</div>
|
|
24
|
+
<div id="meta" class="meta">Loading...</div>
|
|
25
|
+
</header>
|
|
26
|
+
<section id="add-repo-inline" class="add-repo-inline hidden">
|
|
27
|
+
<div class="add-repo-header">
|
|
28
|
+
<div class="section-title">Add Repository</div>
|
|
29
|
+
<button id="repo-inline-close" class="repo-btn" type="button">Close</button>
|
|
30
|
+
</div>
|
|
31
|
+
<div class="modal-tabs">
|
|
32
|
+
<button id="repo-tab-local" class="tab-btn active" type="button">Local Path</button>
|
|
33
|
+
<button id="repo-tab-github" class="tab-btn" type="button">GitHub</button>
|
|
34
|
+
</div>
|
|
35
|
+
<div id="repo-form-local" class="modal-form">
|
|
36
|
+
<label>Repo path
|
|
37
|
+
<input id="local-repo-path" type="text" placeholder="D:\repo\project or ./repo" />
|
|
38
|
+
</label>
|
|
39
|
+
<label>Display name (optional)
|
|
40
|
+
<input id="local-display-name" type="text" placeholder="defaults to folder name" />
|
|
41
|
+
</label>
|
|
42
|
+
</div>
|
|
43
|
+
<div id="repo-form-github" class="modal-form hidden">
|
|
44
|
+
<label>GitHub URL
|
|
45
|
+
<input id="gh-repo-url" type="text" placeholder="https://github.com/org/repo" />
|
|
46
|
+
</label>
|
|
47
|
+
<label>Ref
|
|
48
|
+
<input id="gh-ref" type="text" value="main" />
|
|
49
|
+
</label>
|
|
50
|
+
<label>Mode
|
|
51
|
+
<select id="gh-mode">
|
|
52
|
+
<option value="zip" selected>zip</option>
|
|
53
|
+
<option value="git">git</option>
|
|
54
|
+
</select>
|
|
55
|
+
</label>
|
|
56
|
+
<label>Token (optional for private repos)
|
|
57
|
+
<input id="gh-token" type="password" placeholder="Used for this run only" />
|
|
58
|
+
</label>
|
|
59
|
+
<label class="path">
|
|
60
|
+
<input id="gh-private-mode" type="checkbox" />
|
|
61
|
+
Private Repo Mode
|
|
62
|
+
</label>
|
|
63
|
+
<div id="private-mode-indicator" class="private-mode hidden" title="Token used only in memory; not stored; you can clear cached analysis anytime.">
|
|
64
|
+
🔒 Private Repo Mode: Tokens are used only in memory; nothing is uploaded; you can delete cached data anytime.
|
|
65
|
+
</div>
|
|
66
|
+
</div>
|
|
67
|
+
<div id="repo-modal-error" class="status"></div>
|
|
68
|
+
<div class="modal-actions">
|
|
69
|
+
<button id="repo-add-btn" class="repo-btn" type="button">Add repo</button>
|
|
70
|
+
<button id="repo-cancel-btn" class="repo-btn" type="button">Cancel</button>
|
|
71
|
+
</div>
|
|
72
|
+
</section>
|
|
73
|
+
<div id="ai-settings-modal" class="confirm-modal hidden">
|
|
74
|
+
<div class="confirm-card settings-card">
|
|
75
|
+
<div class="section-title">Settings</div>
|
|
76
|
+
<div class="path">General</div>
|
|
77
|
+
<label class="path">
|
|
78
|
+
<input id="ai-settings-remember-repos" type="checkbox" />
|
|
79
|
+
Remember repositories across sessions
|
|
80
|
+
</label>
|
|
81
|
+
<div class="repo-row-actions">
|
|
82
|
+
<button id="ai-settings-clear-repos" class="repo-btn danger" type="button">Clear repository list</button>
|
|
83
|
+
</div>
|
|
84
|
+
<div class="divider"></div>
|
|
85
|
+
<div id="ai-settings-status" class="status">CodeMap runs locally with deterministic summaries and analysis.</div>
|
|
86
|
+
<div class="repo-row-actions">
|
|
87
|
+
<button id="ai-settings-cancel" class="repo-btn" type="button">Close</button>
|
|
88
|
+
</div>
|
|
89
|
+
</div>
|
|
90
|
+
</div>
|
|
91
|
+
|
|
92
|
+
<main class="layout shell">
|
|
93
|
+
<section class="panel">
|
|
94
|
+
<h2>Repository Tree</h2>
|
|
95
|
+
<div id="tree-status" class="status"></div>
|
|
96
|
+
<div id="repo-list" class="card">
|
|
97
|
+
<div class="section-title">Repositories</div>
|
|
98
|
+
<div id="repo-list-content" class="content muted">No repositories added yet. Add a local path or GitHub repo to begin.</div>
|
|
99
|
+
</div>
|
|
100
|
+
<div id="tree" class="tree"></div>
|
|
101
|
+
<div class="divider"></div>
|
|
102
|
+
<div id="data-privacy" class="card">
|
|
103
|
+
<div class="section-title">Data & Privacy</div>
|
|
104
|
+
<div id="privacy-summary" class="path">Loading data retention...</div>
|
|
105
|
+
<div id="privacy-repo-list-mode" class="path">Repository list: Session-only</div>
|
|
106
|
+
<div id="privacy-private-banner" class="privacy-warning hidden">Private repo mode: caches auto-expire in 7 days unless you change retention.</div>
|
|
107
|
+
<div id="privacy-expiring" class="content muted"></div>
|
|
108
|
+
<div class="privacy-controls">
|
|
109
|
+
<label>Retention
|
|
110
|
+
<select id="repo-retention-select">
|
|
111
|
+
<option value="1">1 day</option>
|
|
112
|
+
<option value="7">7 days</option>
|
|
113
|
+
<option value="14" selected>14 days</option>
|
|
114
|
+
<option value="30">30 days</option>
|
|
115
|
+
<option value="90">90 days</option>
|
|
116
|
+
<option value="0">Never</option>
|
|
117
|
+
</select>
|
|
118
|
+
</label>
|
|
119
|
+
</div>
|
|
120
|
+
<div class="path">Shorter retention = more privacy.</div>
|
|
121
|
+
<div class="privacy-actions">
|
|
122
|
+
<button id="repo-retention-save-btn" class="repo-btn" type="button">Set retention</button>
|
|
123
|
+
<button id="cleanup-dry-btn" class="repo-btn" type="button">Run Cleanup (Dry Run)</button>
|
|
124
|
+
<button id="cleanup-now-btn" class="repo-btn" type="button">Run Cleanup Now</button>
|
|
125
|
+
<button id="delete-repo-cache-btn" class="repo-btn danger" type="button">Delete cached data (repo)</button>
|
|
126
|
+
<button id="delete-all-caches-btn" class="repo-btn danger" type="button">Delete ALL caches</button>
|
|
127
|
+
</div>
|
|
128
|
+
<div id="privacy-confirm" class="content muted hidden"></div>
|
|
129
|
+
<label class="path">
|
|
130
|
+
<input id="auto-clean-on-remove" type="checkbox" />
|
|
131
|
+
Delete cached data when I remove this repo
|
|
132
|
+
</label>
|
|
133
|
+
<div id="auto-clean-note" class="path hidden">Removing from UI will also delete local cache.</div>
|
|
134
|
+
<div class="path">Your analysis data is stored locally under .codemap_cache. You can delete it anytime.</div>
|
|
135
|
+
<div class="path">CodeMap runs locally. Analysis data is cached on this machine only.</div>
|
|
136
|
+
<div class="path">Retention cleanup runs locally; nothing is uploaded.</div>
|
|
137
|
+
<div id="privacy-result" class="content muted"></div>
|
|
138
|
+
</div>
|
|
139
|
+
</section>
|
|
140
|
+
|
|
141
|
+
<section class="panel">
|
|
142
|
+
<h2>File Intelligence</h2>
|
|
143
|
+
<div class="search-wrap">
|
|
144
|
+
<input id="symbol-search-input" type="text" autocomplete="off"
|
|
145
|
+
placeholder="Search symbols (e.g., Student.display, intro, parse...)" />
|
|
146
|
+
<div id="symbol-search-results" class="search-results hidden"></div>
|
|
147
|
+
</div>
|
|
148
|
+
<div class="recent-wrap">
|
|
149
|
+
<div id="recent-symbols-wrap">
|
|
150
|
+
<div class="section-title">Recent Symbols</div>
|
|
151
|
+
<div id="recent-symbols" class="content muted"></div>
|
|
152
|
+
</div>
|
|
153
|
+
<div id="recent-files-wrap">
|
|
154
|
+
<div class="section-title">Recent Files</div>
|
|
155
|
+
<div id="recent-files" class="content muted"></div>
|
|
156
|
+
</div>
|
|
157
|
+
</div>
|
|
158
|
+
<div id="file-view" class="content muted">Select a file from the tree.</div>
|
|
159
|
+
</section>
|
|
160
|
+
|
|
161
|
+
<section class="panel">
|
|
162
|
+
<h2>Symbol Intelligence</h2>
|
|
163
|
+
<div class="view-toggle">
|
|
164
|
+
<button id="tab-details" class="tab-btn active" type="button">Details</button>
|
|
165
|
+
<button id="tab-impact" class="tab-btn" type="button">Impact</button>
|
|
166
|
+
<button id="tab-graph" class="tab-btn" type="button">Graph</button>
|
|
167
|
+
<button id="tab-architecture" class="tab-btn" type="button">Architecture</button>
|
|
168
|
+
</div>
|
|
169
|
+
<div id="graph-controls" class="graph-controls hidden">
|
|
170
|
+
<label>Mode
|
|
171
|
+
<select id="graph-mode">
|
|
172
|
+
<option value="symbol" selected>Symbol</option>
|
|
173
|
+
<option value="file">File</option>
|
|
174
|
+
</select>
|
|
175
|
+
</label>
|
|
176
|
+
<label>Depth
|
|
177
|
+
<select id="graph-depth">
|
|
178
|
+
<option value="1" selected>1</option>
|
|
179
|
+
<option value="2">2</option>
|
|
180
|
+
<option value="3">3</option>
|
|
181
|
+
</select>
|
|
182
|
+
</label>
|
|
183
|
+
<label><input id="graph-hide-builtins" type="checkbox" checked /> Hide builtins</label>
|
|
184
|
+
<label><input id="graph-hide-external" type="checkbox" checked /> Hide external</label>
|
|
185
|
+
<input id="graph-search" type="text" placeholder="Search graph nodes..." />
|
|
186
|
+
</div>
|
|
187
|
+
<div id="impact-controls" class="graph-controls hidden">
|
|
188
|
+
<label>Depth
|
|
189
|
+
<select id="impact-depth">
|
|
190
|
+
<option value="1">1</option>
|
|
191
|
+
<option value="2" selected>2</option>
|
|
192
|
+
<option value="3">3</option>
|
|
193
|
+
</select>
|
|
194
|
+
</label>
|
|
195
|
+
<label>Max nodes
|
|
196
|
+
<select id="impact-max-nodes">
|
|
197
|
+
<option value="200" selected>200</option>
|
|
198
|
+
<option value="100">100</option>
|
|
199
|
+
<option value="50">50</option>
|
|
200
|
+
<option value="10">10</option>
|
|
201
|
+
<option value="1">1 (test truncation)</option>
|
|
202
|
+
</select>
|
|
203
|
+
</label>
|
|
204
|
+
</div>
|
|
205
|
+
<div id="symbol-view" class="content muted">Select a symbol to view summary and usages.</div>
|
|
206
|
+
<div id="impact-view" class="content muted hidden">Select a symbol to view impact.</div>
|
|
207
|
+
<div id="graph-view" class="content muted hidden">Select a symbol to view graph.</div>
|
|
208
|
+
<div id="architecture-view" class="content muted hidden">Select a repository to view architecture insights.</div>
|
|
209
|
+
</section>
|
|
210
|
+
</main>
|
|
211
|
+
|
|
212
|
+
<div id="toast" class="toast hidden"></div>
|
|
213
|
+
<div id="confirm-modal" class="confirm-modal hidden">
|
|
214
|
+
<div class="confirm-card">
|
|
215
|
+
<div id="confirm-title" class="section-title">Confirm action</div>
|
|
216
|
+
<div id="confirm-message" class="path"></div>
|
|
217
|
+
<div class="repo-row-actions">
|
|
218
|
+
<button id="confirm-yes" class="repo-btn danger" type="button">Yes</button>
|
|
219
|
+
<button id="confirm-no" class="repo-btn" type="button">Cancel</button>
|
|
220
|
+
</div>
|
|
221
|
+
</div>
|
|
222
|
+
</div>
|
|
223
|
+
|
|
224
|
+
<script>
|
|
225
|
+
window.CODEMAP_DEFAULT_REPO = "{{ default_repo }}";
|
|
226
|
+
</script>
|
|
227
|
+
<script src="/static/app.js?v=20260306b"></script>
|
|
228
|
+
</body>
|
|
229
|
+
</html>
|
|
230
|
+
|
|
231
|
+
|
ui/utils/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import threading
|
|
7
|
+
from datetime import datetime, timezone
|
|
8
|
+
from typing import Any, Dict, List, Optional
|
|
9
|
+
from urllib.parse import urlsplit, urlunsplit
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
_LOCK = threading.RLock()
|
|
13
|
+
_REGISTRY_FILE = "_registry.json"
|
|
14
|
+
_SENSITIVE_RE = re.compile(r"(?i)(api[_-]?key|token|authorization|bearer|basic|secret|password)")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _cache_root(base_dir: Optional[str] = None) -> str:
|
|
18
|
+
if base_dir:
|
|
19
|
+
return os.path.abspath(base_dir)
|
|
20
|
+
return os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", ".codemap_cache"))
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _registry_path(base_dir: Optional[str] = None) -> str:
|
|
24
|
+
return os.path.join(_cache_root(base_dir), _REGISTRY_FILE)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def _now_iso() -> str:
|
|
28
|
+
return datetime.now(timezone.utc).isoformat()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _default_registry() -> Dict[str, Any]:
|
|
32
|
+
return {
|
|
33
|
+
"version": 1,
|
|
34
|
+
"remember_repos": False,
|
|
35
|
+
"repos": [],
|
|
36
|
+
"updated_at": _now_iso(),
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _safe_repo_url(repo_url: str) -> str:
|
|
41
|
+
value = str(repo_url or "").strip()
|
|
42
|
+
if not value:
|
|
43
|
+
return ""
|
|
44
|
+
try:
|
|
45
|
+
parts = urlsplit(value)
|
|
46
|
+
hostname = parts.hostname or ""
|
|
47
|
+
if not hostname:
|
|
48
|
+
return value
|
|
49
|
+
netloc = hostname
|
|
50
|
+
if parts.port:
|
|
51
|
+
netloc = f"{hostname}:{parts.port}"
|
|
52
|
+
clean = urlunsplit((parts.scheme, netloc, parts.path, "", ""))
|
|
53
|
+
return clean.rstrip("/")
|
|
54
|
+
except Exception:
|
|
55
|
+
return value
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _sanitize_repo_entry(entry: Dict[str, Any]) -> Dict[str, Any]:
|
|
59
|
+
now = _now_iso()
|
|
60
|
+
source = str(entry.get("source", "filesystem") or "filesystem").strip().lower()
|
|
61
|
+
if source not in {"filesystem", "github"}:
|
|
62
|
+
source = "filesystem"
|
|
63
|
+
sanitized = {
|
|
64
|
+
"repo_hash": str(entry.get("repo_hash", "") or "").strip(),
|
|
65
|
+
"display_name": str(entry.get("display_name", "") or "").strip(),
|
|
66
|
+
"source": source,
|
|
67
|
+
"repo_path": str(entry.get("repo_path", "") or "").strip(),
|
|
68
|
+
"repo_url": _safe_repo_url(str(entry.get("repo_url", "") or "")),
|
|
69
|
+
"ref": str(entry.get("ref", "") or "").strip(),
|
|
70
|
+
"mode": str(entry.get("mode", "") or "").strip().lower(),
|
|
71
|
+
"added_at": str(entry.get("added_at", "") or now),
|
|
72
|
+
"last_opened_at": str(entry.get("last_opened_at", "") or ""),
|
|
73
|
+
}
|
|
74
|
+
if not sanitized["display_name"]:
|
|
75
|
+
candidate = sanitized["repo_path"] or sanitized["repo_url"] or sanitized["repo_hash"]
|
|
76
|
+
sanitized["display_name"] = os.path.basename(str(candidate).rstrip("\\/")) or str(candidate)
|
|
77
|
+
return sanitized
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _scrub_sensitive_fields(payload: Any) -> Any:
|
|
81
|
+
if isinstance(payload, dict):
|
|
82
|
+
clean: Dict[str, Any] = {}
|
|
83
|
+
for k, v in payload.items():
|
|
84
|
+
key = str(k or "")
|
|
85
|
+
if _SENSITIVE_RE.search(key):
|
|
86
|
+
continue
|
|
87
|
+
clean[key] = _scrub_sensitive_fields(v)
|
|
88
|
+
return clean
|
|
89
|
+
if isinstance(payload, list):
|
|
90
|
+
return [_scrub_sensitive_fields(v) for v in payload]
|
|
91
|
+
return payload
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def save_registry_atomic(data: Dict[str, Any], base_dir: Optional[str] = None) -> Dict[str, Any]:
|
|
95
|
+
root = _cache_root(base_dir)
|
|
96
|
+
os.makedirs(root, exist_ok=True)
|
|
97
|
+
path = _registry_path(base_dir)
|
|
98
|
+
|
|
99
|
+
payload = _default_registry()
|
|
100
|
+
payload.update({
|
|
101
|
+
"version": int(data.get("version", 1) or 1),
|
|
102
|
+
"remember_repos": bool(data.get("remember_repos", False)),
|
|
103
|
+
"updated_at": _now_iso(),
|
|
104
|
+
})
|
|
105
|
+
repos = data.get("repos", [])
|
|
106
|
+
payload["repos"] = [
|
|
107
|
+
_sanitize_repo_entry(r)
|
|
108
|
+
for r in repos
|
|
109
|
+
if isinstance(r, dict) and str(r.get("repo_hash", "") or "").strip()
|
|
110
|
+
]
|
|
111
|
+
|
|
112
|
+
tmp_path = f"{path}.tmp"
|
|
113
|
+
safe_payload = _scrub_sensitive_fields(payload)
|
|
114
|
+
with open(tmp_path, "w", encoding="utf-8") as f:
|
|
115
|
+
json.dump(safe_payload, f, indent=2)
|
|
116
|
+
os.replace(tmp_path, path)
|
|
117
|
+
return safe_payload
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def load_registry(base_dir: Optional[str] = None) -> Dict[str, Any]:
|
|
121
|
+
path = _registry_path(base_dir)
|
|
122
|
+
with _LOCK:
|
|
123
|
+
if not os.path.exists(path):
|
|
124
|
+
return save_registry_atomic(_default_registry(), base_dir=base_dir)
|
|
125
|
+
try:
|
|
126
|
+
with open(path, "r", encoding="utf-8") as f:
|
|
127
|
+
raw = json.load(f)
|
|
128
|
+
except Exception:
|
|
129
|
+
return save_registry_atomic(_default_registry(), base_dir=base_dir)
|
|
130
|
+
if not isinstance(raw, dict):
|
|
131
|
+
return save_registry_atomic(_default_registry(), base_dir=base_dir)
|
|
132
|
+
return save_registry_atomic(raw, base_dir=base_dir)
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def set_remember(remember_repos: bool, base_dir: Optional[str] = None) -> Dict[str, Any]:
|
|
136
|
+
with _LOCK:
|
|
137
|
+
reg = load_registry(base_dir=base_dir)
|
|
138
|
+
reg["remember_repos"] = bool(remember_repos)
|
|
139
|
+
return save_registry_atomic(reg, base_dir=base_dir)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def list_repos(base_dir: Optional[str] = None) -> List[Dict[str, Any]]:
|
|
143
|
+
reg = load_registry(base_dir=base_dir)
|
|
144
|
+
repos = reg.get("repos", [])
|
|
145
|
+
if not isinstance(repos, list):
|
|
146
|
+
return []
|
|
147
|
+
return [_sanitize_repo_entry(r) for r in repos if isinstance(r, dict)]
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def add_repo(entry: Dict[str, Any], base_dir: Optional[str] = None) -> Dict[str, Any]:
|
|
151
|
+
with _LOCK:
|
|
152
|
+
reg = load_registry(base_dir=base_dir)
|
|
153
|
+
repo = _sanitize_repo_entry(entry)
|
|
154
|
+
repos = reg.get("repos", [])
|
|
155
|
+
if not isinstance(repos, list):
|
|
156
|
+
repos = []
|
|
157
|
+
updated = False
|
|
158
|
+
for idx, item in enumerate(repos):
|
|
159
|
+
if isinstance(item, dict) and str(item.get("repo_hash", "") or "") == repo["repo_hash"]:
|
|
160
|
+
merged = dict(item)
|
|
161
|
+
merged.update(repo)
|
|
162
|
+
repos[idx] = _sanitize_repo_entry(merged)
|
|
163
|
+
updated = True
|
|
164
|
+
break
|
|
165
|
+
if not updated:
|
|
166
|
+
repos.append(repo)
|
|
167
|
+
reg["repos"] = repos
|
|
168
|
+
save_registry_atomic(reg, base_dir=base_dir)
|
|
169
|
+
return repo
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def remove_repo(repo_hash: str, base_dir: Optional[str] = None) -> Dict[str, Any]:
|
|
173
|
+
key = str(repo_hash or "").strip()
|
|
174
|
+
with _LOCK:
|
|
175
|
+
reg = load_registry(base_dir=base_dir)
|
|
176
|
+
repos = reg.get("repos", [])
|
|
177
|
+
if not isinstance(repos, list):
|
|
178
|
+
repos = []
|
|
179
|
+
reg["repos"] = [
|
|
180
|
+
r for r in repos
|
|
181
|
+
if not (isinstance(r, dict) and str(r.get("repo_hash", "") or "") == key)
|
|
182
|
+
]
|
|
183
|
+
return save_registry_atomic(reg, base_dir=base_dir)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def clear_repos(base_dir: Optional[str] = None) -> Dict[str, Any]:
|
|
187
|
+
with _LOCK:
|
|
188
|
+
reg = load_registry(base_dir=base_dir)
|
|
189
|
+
reg["repos"] = []
|
|
190
|
+
return save_registry_atomic(reg, base_dir=base_dir)
|