@allansantos-dev/smart-tool 0.9.0 → 0.9.2
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.
- package/CHANGELOG.md +50 -23
- package/NOTICE +32 -32
- package/README.md +117 -116
- package/atomic_io.py +94 -94
- package/bin/smart-tool.mjs +5 -1
- package/client_hooks.py +192 -192
- package/code_graph_java.mjs +49 -49
- package/code_graph_js.cjs +135 -135
- package/duplicates.py +313 -313
- package/embedding_cache.py +99 -99
- package/endpoint_sync.py +179 -179
- package/gateway.py +220 -220
- package/hook_decision.py +96 -96
- package/index_inventory.py +75 -75
- package/indexer.py +878 -878
- package/install.py +113 -11
- package/install_runtime.py +101 -35
- package/integration_metrics.py +82 -82
- package/local_embedder.py +172 -172
- package/model_defaults.py +70 -70
- package/package.json +35 -35
- package/project_insights_ui.py +38 -38
- package/project_monitor.py +204 -204
- package/project_store.py +220 -220
- package/projects_ui.py +146 -146
- package/requirements.txt +8 -8
- package/research_cache.py +171 -171
- package/research_metrics.py +110 -110
- package/router.py +582 -582
- package/secret_store.py +121 -121
- package/setup_ui.py +1011 -986
- package/smart_tool_daemon.py +3604 -3604
- package/version.py +1 -1
- package/web_adapters/browser/fetch_camoufox.py +69 -0
- package/web_adapters/node/ddg-search.mjs +89 -89
- package/web_adapters/node/open-websearch-adapter.mjs +77 -77
- package/web_adapters/node/package.json +12 -12
- package/web_adapters/node/wikipedia-search.mjs +44 -44
- package/web_search_adapters.py +193 -193
- package/web_search_external.py +288 -288
- package/web_search_health.py +88 -88
package/project_store.py
CHANGED
|
@@ -1,220 +1,220 @@
|
|
|
1
|
-
"""Cadastro de projetos e histórico de jobs. Consultas/resultados ficam sob DPAPI."""
|
|
2
|
-
import json
|
|
3
|
-
import os
|
|
4
|
-
import paths
|
|
5
|
-
import re
|
|
6
|
-
import sqlite3
|
|
7
|
-
import threading
|
|
8
|
-
import time
|
|
9
|
-
from contextlib import contextmanager
|
|
10
|
-
|
|
11
|
-
import project_identity
|
|
12
|
-
import secret_store
|
|
13
|
-
|
|
14
|
-
DB_PATH = os.path.join(paths.DATA_DIR, "projects.sqlite3")
|
|
15
|
-
_LOCK = threading.RLock()
|
|
16
|
-
MAX_JOBS = 500
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
@contextmanager
|
|
20
|
-
def _db():
|
|
21
|
-
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
|
22
|
-
db = sqlite3.connect(DB_PATH, timeout=10)
|
|
23
|
-
try:
|
|
24
|
-
db.execute("PRAGMA secure_delete=ON")
|
|
25
|
-
db.execute("CREATE TABLE IF NOT EXISTS projects(id TEXT PRIMARY KEY, root TEXT NOT NULL, data TEXT NOT NULL, updated_at REAL NOT NULL)")
|
|
26
|
-
db.execute("CREATE TABLE IF NOT EXISTS jobs(id TEXT PRIMARY KEY, project_id TEXT, status TEXT NOT NULL, updated_at REAL NOT NULL, payload TEXT NOT NULL)")
|
|
27
|
-
db.execute("CREATE INDEX IF NOT EXISTS jobs_project ON jobs(project_id,updated_at)")
|
|
28
|
-
with db:
|
|
29
|
-
yield db
|
|
30
|
-
finally:
|
|
31
|
-
db.close()
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
def register(root, watch=None):
|
|
35
|
-
display = project_identity.display_root(root)
|
|
36
|
-
if not os.path.isdir(display):
|
|
37
|
-
raise ValueError("The project folder does not exist or is not accessible.")
|
|
38
|
-
key = project_identity.project_id(display)
|
|
39
|
-
with _LOCK, _db() as db:
|
|
40
|
-
row = db.execute("SELECT data FROM projects WHERE id=?", (key,)).fetchone()
|
|
41
|
-
data = json.loads(row[0]) if row else {
|
|
42
|
-
"id": key, "root": display, "name": os.path.basename(display) or display,
|
|
43
|
-
"status": "registered", "watch": False, "paused": False, "enabled": False,
|
|
44
|
-
"update_mode": "on_search",
|
|
45
|
-
"dirty_seq": 0, "indexed_seq": 0, "created_at": time.time(),
|
|
46
|
-
"last_indexed": None, "last_checked": None, "last_error": "",
|
|
47
|
-
"last_job_id": None, "watch_backend": "off", "next_retry_at": 0,
|
|
48
|
-
}
|
|
49
|
-
data["root"] = display
|
|
50
|
-
if watch is not None:
|
|
51
|
-
data["watch"] = bool(watch)
|
|
52
|
-
db.execute("INSERT OR REPLACE INTO projects VALUES(?,?,?,?)",
|
|
53
|
-
(key, display, json.dumps(data, ensure_ascii=False), time.time()))
|
|
54
|
-
return data
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
def get(project_id):
|
|
58
|
-
with _LOCK, _db() as db:
|
|
59
|
-
row = db.execute("SELECT data FROM projects WHERE id=?", (project_id,)).fetchone()
|
|
60
|
-
return json.loads(row[0]) if row else None
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
def all_projects():
|
|
64
|
-
with _LOCK, _db() as db:
|
|
65
|
-
rows = db.execute("SELECT data FROM projects ORDER BY root COLLATE NOCASE").fetchall()
|
|
66
|
-
return [json.loads(row[0]) for row in rows]
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
def update(project_id, **fields):
|
|
70
|
-
with _LOCK, _db() as db:
|
|
71
|
-
row = db.execute("SELECT data FROM projects WHERE id=?", (project_id,)).fetchone()
|
|
72
|
-
if not row:
|
|
73
|
-
return None
|
|
74
|
-
data = json.loads(row[0])
|
|
75
|
-
fields.pop("id", None)
|
|
76
|
-
fields.pop("root", None)
|
|
77
|
-
data.update(fields)
|
|
78
|
-
db.execute("UPDATE projects SET data=?,updated_at=? WHERE id=?",
|
|
79
|
-
(json.dumps(data, ensure_ascii=False), time.time(), project_id))
|
|
80
|
-
return data
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
def mark_dirty(project_id, reason="Arquivos alterados."):
|
|
84
|
-
with _LOCK:
|
|
85
|
-
project = get(project_id)
|
|
86
|
-
if not project:
|
|
87
|
-
return None
|
|
88
|
-
return update(project_id, dirty_seq=project.get("dirty_seq", 0) + 1,
|
|
89
|
-
status="dirty" if not project.get("paused") else "paused",
|
|
90
|
-
dirty_reason=reason, last_event=time.time())
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def record_changes(project_id, paths=(), full=False, scope=False):
|
|
94
|
-
with _LOCK:
|
|
95
|
-
project = get(project_id)
|
|
96
|
-
if not project:
|
|
97
|
-
return
|
|
98
|
-
merged = set(project.get('dirty_paths', [])) | set(paths)
|
|
99
|
-
full = full or project.get('needs_full_check', False) or len(merged) > 1000
|
|
100
|
-
update(project_id, dirty_paths=[] if full else sorted(merged), needs_full_check=full,
|
|
101
|
-
scope_dirty=scope or project.get('scope_dirty', False))
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
def confirm_changes(project_id, sequence):
|
|
105
|
-
with _LOCK:
|
|
106
|
-
project = get(project_id)
|
|
107
|
-
if project and project.get('dirty_seq', 0) == sequence:
|
|
108
|
-
update(project_id, dirty_paths=[], needs_full_check=False, scope_dirty=False)
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def save_job(job_id, payload):
|
|
112
|
-
text = json.dumps(payload, ensure_ascii=False)
|
|
113
|
-
protected = secret_store.protect(text)
|
|
114
|
-
if not secret_store.is_protected(protected):
|
|
115
|
-
raise RuntimeError("Could not protect the job history on this Windows system.")
|
|
116
|
-
with _LOCK, _db() as db:
|
|
117
|
-
db.execute("INSERT OR REPLACE INTO jobs VALUES(?,?,?,?,?)",
|
|
118
|
-
(job_id, payload.get("project_id"), payload.get("status", "pending"), time.time(), protected))
|
|
119
|
-
db.execute("DELETE FROM jobs WHERE status!='pending' AND id NOT IN "
|
|
120
|
-
"(SELECT id FROM jobs ORDER BY updated_at DESC LIMIT ?)", (MAX_JOBS,))
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
def _decode(payload):
|
|
124
|
-
try:
|
|
125
|
-
if not secret_store.is_protected(payload):
|
|
126
|
-
return None
|
|
127
|
-
value = json.loads(secret_store.unprotect(payload))
|
|
128
|
-
return value if isinstance(value, dict) else None
|
|
129
|
-
except (ValueError, secret_store.SecretProtectionError):
|
|
130
|
-
return None
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
def job(job_id):
|
|
134
|
-
with _LOCK, _db() as db:
|
|
135
|
-
row = db.execute("SELECT payload FROM jobs WHERE id=?", (job_id,)).fetchone()
|
|
136
|
-
return _decode(row[0]) if row else None
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
def jobs(project_id=None, limit=30):
|
|
140
|
-
with _LOCK, _db() as db:
|
|
141
|
-
if project_id:
|
|
142
|
-
rows = db.execute("SELECT id,payload FROM jobs WHERE project_id=? ORDER BY updated_at DESC LIMIT ?",
|
|
143
|
-
(project_id, limit)).fetchall()
|
|
144
|
-
else:
|
|
145
|
-
rows = db.execute("SELECT id,payload FROM jobs ORDER BY updated_at DESC LIMIT ?", (limit,)).fetchall()
|
|
146
|
-
return [{"job_id": key, **data} for key, payload in rows if (data := _decode(payload))]
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
def recover_interrupted():
|
|
150
|
-
with _LOCK, _db() as db:
|
|
151
|
-
rows = db.execute("SELECT id,payload FROM jobs WHERE status='pending'").fetchall()
|
|
152
|
-
for key, payload in rows:
|
|
153
|
-
data = _decode(payload) or {}
|
|
154
|
-
data.update(status="interrupted", phase="interrupted", finished_at=time.time(),
|
|
155
|
-
error="The daemon restarted. Resume the job; files already committed will be reused.")
|
|
156
|
-
save_job(key, data)
|
|
157
|
-
if data.get("project_id"):
|
|
158
|
-
update(data["project_id"], status="interrupted", last_error=data["error"], last_job_id=key)
|
|
159
|
-
return len(rows)
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
def forget(project_id):
|
|
163
|
-
with _LOCK, _db() as db:
|
|
164
|
-
db.execute("DELETE FROM jobs WHERE project_id=?", (project_id,))
|
|
165
|
-
db.execute("DELETE FROM projects WHERE id=?", (project_id,))
|
|
166
|
-
for table in ('usage_daily','usage_index_jobs','usage_imports'):
|
|
167
|
-
if db.execute('SELECT 1 FROM sqlite_master WHERE name=?',(table,)).fetchone():
|
|
168
|
-
db.execute(f'DELETE FROM {table} WHERE project_id=?',(project_id,))
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
def relocate(project_id, root):
|
|
172
|
-
"""Reassocia cadastro e histórico após a cópia do índice; não move código-fonte."""
|
|
173
|
-
display = project_identity.display_root(root)
|
|
174
|
-
key = project_identity.project_id(display)
|
|
175
|
-
with _LOCK, _db() as db:
|
|
176
|
-
if db.execute("SELECT 1 FROM projects WHERE id=?", (key,)).fetchone():
|
|
177
|
-
raise ValueError("The target folder is already registered.")
|
|
178
|
-
row = db.execute("SELECT data FROM projects WHERE id=?", (project_id,)).fetchone()
|
|
179
|
-
if not row:
|
|
180
|
-
raise ValueError("Project not registered.")
|
|
181
|
-
data = json.loads(row[0])
|
|
182
|
-
data.update(id=key, root=display, name=os.path.basename(display) or display,
|
|
183
|
-
previous_root=data["root"], status="dirty", needs_full_check=True,
|
|
184
|
-
dirty_seq=data.get("dirty_seq", 0) + 1, watch_backend="off", last_error="")
|
|
185
|
-
db.execute("INSERT INTO projects VALUES(?,?,?,?)", (key, display, json.dumps(data, ensure_ascii=False), time.time()))
|
|
186
|
-
for job_id, payload in db.execute("SELECT id,payload FROM jobs WHERE project_id=?", (project_id,)).fetchall():
|
|
187
|
-
job_data = _decode(payload)
|
|
188
|
-
if job_data:
|
|
189
|
-
job_data["project_id"] = key
|
|
190
|
-
job_data.setdefault("original_project_root", data["previous_root"])
|
|
191
|
-
if job_data.get("arguments"):
|
|
192
|
-
job_data["arguments"]["project_root"] = display
|
|
193
|
-
payload = secret_store.protect(json.dumps(job_data, ensure_ascii=False))
|
|
194
|
-
db.execute("UPDATE jobs SET project_id=?,payload=? WHERE id=?", (key, payload, job_id))
|
|
195
|
-
db.execute("DELETE FROM projects WHERE id=?", (project_id,))
|
|
196
|
-
for table in ('usage_daily','usage_index_jobs','usage_imports'):
|
|
197
|
-
if db.execute('SELECT 1 FROM sqlite_master WHERE name=?',(table,)).fetchone():
|
|
198
|
-
db.execute(f'UPDATE {table} SET project_id=? WHERE project_id=?',(key,project_id))
|
|
199
|
-
return data
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
def import_known_legacy(index_dir):
|
|
203
|
-
"""Somente raízes documentadas nos registros antigos; não adivinha diretórios."""
|
|
204
|
-
import indexer
|
|
205
|
-
candidates = set()
|
|
206
|
-
for name in ("daemon.log", "daemon.log.1"):
|
|
207
|
-
try:
|
|
208
|
-
with open(os.path.join(index_dir, name), encoding="utf-8", errors="replace") as f:
|
|
209
|
-
for line in f:
|
|
210
|
-
match = re.search(r"reindex (.+?): \d+ no escopo", line)
|
|
211
|
-
if match and os.path.isdir(match.group(1)):
|
|
212
|
-
candidates.add(match.group(1))
|
|
213
|
-
except OSError:
|
|
214
|
-
pass
|
|
215
|
-
for root in candidates:
|
|
216
|
-
key = project_identity.project_id(root)
|
|
217
|
-
if get(key) or not indexer.existing_db_path(root):
|
|
218
|
-
continue
|
|
219
|
-
register(root, watch=False)
|
|
220
|
-
update(key, enabled=True, status="migration_required", legacy_discovered=True)
|
|
1
|
+
"""Cadastro de projetos e histórico de jobs. Consultas/resultados ficam sob DPAPI."""
|
|
2
|
+
import json
|
|
3
|
+
import os
|
|
4
|
+
import paths
|
|
5
|
+
import re
|
|
6
|
+
import sqlite3
|
|
7
|
+
import threading
|
|
8
|
+
import time
|
|
9
|
+
from contextlib import contextmanager
|
|
10
|
+
|
|
11
|
+
import project_identity
|
|
12
|
+
import secret_store
|
|
13
|
+
|
|
14
|
+
DB_PATH = os.path.join(paths.DATA_DIR, "projects.sqlite3")
|
|
15
|
+
_LOCK = threading.RLock()
|
|
16
|
+
MAX_JOBS = 500
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@contextmanager
|
|
20
|
+
def _db():
|
|
21
|
+
os.makedirs(os.path.dirname(DB_PATH), exist_ok=True)
|
|
22
|
+
db = sqlite3.connect(DB_PATH, timeout=10)
|
|
23
|
+
try:
|
|
24
|
+
db.execute("PRAGMA secure_delete=ON")
|
|
25
|
+
db.execute("CREATE TABLE IF NOT EXISTS projects(id TEXT PRIMARY KEY, root TEXT NOT NULL, data TEXT NOT NULL, updated_at REAL NOT NULL)")
|
|
26
|
+
db.execute("CREATE TABLE IF NOT EXISTS jobs(id TEXT PRIMARY KEY, project_id TEXT, status TEXT NOT NULL, updated_at REAL NOT NULL, payload TEXT NOT NULL)")
|
|
27
|
+
db.execute("CREATE INDEX IF NOT EXISTS jobs_project ON jobs(project_id,updated_at)")
|
|
28
|
+
with db:
|
|
29
|
+
yield db
|
|
30
|
+
finally:
|
|
31
|
+
db.close()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def register(root, watch=None):
|
|
35
|
+
display = project_identity.display_root(root)
|
|
36
|
+
if not os.path.isdir(display):
|
|
37
|
+
raise ValueError("The project folder does not exist or is not accessible.")
|
|
38
|
+
key = project_identity.project_id(display)
|
|
39
|
+
with _LOCK, _db() as db:
|
|
40
|
+
row = db.execute("SELECT data FROM projects WHERE id=?", (key,)).fetchone()
|
|
41
|
+
data = json.loads(row[0]) if row else {
|
|
42
|
+
"id": key, "root": display, "name": os.path.basename(display) or display,
|
|
43
|
+
"status": "registered", "watch": False, "paused": False, "enabled": False,
|
|
44
|
+
"update_mode": "on_search",
|
|
45
|
+
"dirty_seq": 0, "indexed_seq": 0, "created_at": time.time(),
|
|
46
|
+
"last_indexed": None, "last_checked": None, "last_error": "",
|
|
47
|
+
"last_job_id": None, "watch_backend": "off", "next_retry_at": 0,
|
|
48
|
+
}
|
|
49
|
+
data["root"] = display
|
|
50
|
+
if watch is not None:
|
|
51
|
+
data["watch"] = bool(watch)
|
|
52
|
+
db.execute("INSERT OR REPLACE INTO projects VALUES(?,?,?,?)",
|
|
53
|
+
(key, display, json.dumps(data, ensure_ascii=False), time.time()))
|
|
54
|
+
return data
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def get(project_id):
|
|
58
|
+
with _LOCK, _db() as db:
|
|
59
|
+
row = db.execute("SELECT data FROM projects WHERE id=?", (project_id,)).fetchone()
|
|
60
|
+
return json.loads(row[0]) if row else None
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def all_projects():
|
|
64
|
+
with _LOCK, _db() as db:
|
|
65
|
+
rows = db.execute("SELECT data FROM projects ORDER BY root COLLATE NOCASE").fetchall()
|
|
66
|
+
return [json.loads(row[0]) for row in rows]
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def update(project_id, **fields):
|
|
70
|
+
with _LOCK, _db() as db:
|
|
71
|
+
row = db.execute("SELECT data FROM projects WHERE id=?", (project_id,)).fetchone()
|
|
72
|
+
if not row:
|
|
73
|
+
return None
|
|
74
|
+
data = json.loads(row[0])
|
|
75
|
+
fields.pop("id", None)
|
|
76
|
+
fields.pop("root", None)
|
|
77
|
+
data.update(fields)
|
|
78
|
+
db.execute("UPDATE projects SET data=?,updated_at=? WHERE id=?",
|
|
79
|
+
(json.dumps(data, ensure_ascii=False), time.time(), project_id))
|
|
80
|
+
return data
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def mark_dirty(project_id, reason="Arquivos alterados."):
|
|
84
|
+
with _LOCK:
|
|
85
|
+
project = get(project_id)
|
|
86
|
+
if not project:
|
|
87
|
+
return None
|
|
88
|
+
return update(project_id, dirty_seq=project.get("dirty_seq", 0) + 1,
|
|
89
|
+
status="dirty" if not project.get("paused") else "paused",
|
|
90
|
+
dirty_reason=reason, last_event=time.time())
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def record_changes(project_id, paths=(), full=False, scope=False):
|
|
94
|
+
with _LOCK:
|
|
95
|
+
project = get(project_id)
|
|
96
|
+
if not project:
|
|
97
|
+
return
|
|
98
|
+
merged = set(project.get('dirty_paths', [])) | set(paths)
|
|
99
|
+
full = full or project.get('needs_full_check', False) or len(merged) > 1000
|
|
100
|
+
update(project_id, dirty_paths=[] if full else sorted(merged), needs_full_check=full,
|
|
101
|
+
scope_dirty=scope or project.get('scope_dirty', False))
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def confirm_changes(project_id, sequence):
|
|
105
|
+
with _LOCK:
|
|
106
|
+
project = get(project_id)
|
|
107
|
+
if project and project.get('dirty_seq', 0) == sequence:
|
|
108
|
+
update(project_id, dirty_paths=[], needs_full_check=False, scope_dirty=False)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def save_job(job_id, payload):
|
|
112
|
+
text = json.dumps(payload, ensure_ascii=False)
|
|
113
|
+
protected = secret_store.protect(text)
|
|
114
|
+
if not secret_store.is_protected(protected):
|
|
115
|
+
raise RuntimeError("Could not protect the job history on this Windows system.")
|
|
116
|
+
with _LOCK, _db() as db:
|
|
117
|
+
db.execute("INSERT OR REPLACE INTO jobs VALUES(?,?,?,?,?)",
|
|
118
|
+
(job_id, payload.get("project_id"), payload.get("status", "pending"), time.time(), protected))
|
|
119
|
+
db.execute("DELETE FROM jobs WHERE status!='pending' AND id NOT IN "
|
|
120
|
+
"(SELECT id FROM jobs ORDER BY updated_at DESC LIMIT ?)", (MAX_JOBS,))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def _decode(payload):
|
|
124
|
+
try:
|
|
125
|
+
if not secret_store.is_protected(payload):
|
|
126
|
+
return None
|
|
127
|
+
value = json.loads(secret_store.unprotect(payload))
|
|
128
|
+
return value if isinstance(value, dict) else None
|
|
129
|
+
except (ValueError, secret_store.SecretProtectionError):
|
|
130
|
+
return None
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def job(job_id):
|
|
134
|
+
with _LOCK, _db() as db:
|
|
135
|
+
row = db.execute("SELECT payload FROM jobs WHERE id=?", (job_id,)).fetchone()
|
|
136
|
+
return _decode(row[0]) if row else None
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def jobs(project_id=None, limit=30):
|
|
140
|
+
with _LOCK, _db() as db:
|
|
141
|
+
if project_id:
|
|
142
|
+
rows = db.execute("SELECT id,payload FROM jobs WHERE project_id=? ORDER BY updated_at DESC LIMIT ?",
|
|
143
|
+
(project_id, limit)).fetchall()
|
|
144
|
+
else:
|
|
145
|
+
rows = db.execute("SELECT id,payload FROM jobs ORDER BY updated_at DESC LIMIT ?", (limit,)).fetchall()
|
|
146
|
+
return [{"job_id": key, **data} for key, payload in rows if (data := _decode(payload))]
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def recover_interrupted():
|
|
150
|
+
with _LOCK, _db() as db:
|
|
151
|
+
rows = db.execute("SELECT id,payload FROM jobs WHERE status='pending'").fetchall()
|
|
152
|
+
for key, payload in rows:
|
|
153
|
+
data = _decode(payload) or {}
|
|
154
|
+
data.update(status="interrupted", phase="interrupted", finished_at=time.time(),
|
|
155
|
+
error="The daemon restarted. Resume the job; files already committed will be reused.")
|
|
156
|
+
save_job(key, data)
|
|
157
|
+
if data.get("project_id"):
|
|
158
|
+
update(data["project_id"], status="interrupted", last_error=data["error"], last_job_id=key)
|
|
159
|
+
return len(rows)
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def forget(project_id):
|
|
163
|
+
with _LOCK, _db() as db:
|
|
164
|
+
db.execute("DELETE FROM jobs WHERE project_id=?", (project_id,))
|
|
165
|
+
db.execute("DELETE FROM projects WHERE id=?", (project_id,))
|
|
166
|
+
for table in ('usage_daily','usage_index_jobs','usage_imports'):
|
|
167
|
+
if db.execute('SELECT 1 FROM sqlite_master WHERE name=?',(table,)).fetchone():
|
|
168
|
+
db.execute(f'DELETE FROM {table} WHERE project_id=?',(project_id,))
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def relocate(project_id, root):
|
|
172
|
+
"""Reassocia cadastro e histórico após a cópia do índice; não move código-fonte."""
|
|
173
|
+
display = project_identity.display_root(root)
|
|
174
|
+
key = project_identity.project_id(display)
|
|
175
|
+
with _LOCK, _db() as db:
|
|
176
|
+
if db.execute("SELECT 1 FROM projects WHERE id=?", (key,)).fetchone():
|
|
177
|
+
raise ValueError("The target folder is already registered.")
|
|
178
|
+
row = db.execute("SELECT data FROM projects WHERE id=?", (project_id,)).fetchone()
|
|
179
|
+
if not row:
|
|
180
|
+
raise ValueError("Project not registered.")
|
|
181
|
+
data = json.loads(row[0])
|
|
182
|
+
data.update(id=key, root=display, name=os.path.basename(display) or display,
|
|
183
|
+
previous_root=data["root"], status="dirty", needs_full_check=True,
|
|
184
|
+
dirty_seq=data.get("dirty_seq", 0) + 1, watch_backend="off", last_error="")
|
|
185
|
+
db.execute("INSERT INTO projects VALUES(?,?,?,?)", (key, display, json.dumps(data, ensure_ascii=False), time.time()))
|
|
186
|
+
for job_id, payload in db.execute("SELECT id,payload FROM jobs WHERE project_id=?", (project_id,)).fetchall():
|
|
187
|
+
job_data = _decode(payload)
|
|
188
|
+
if job_data:
|
|
189
|
+
job_data["project_id"] = key
|
|
190
|
+
job_data.setdefault("original_project_root", data["previous_root"])
|
|
191
|
+
if job_data.get("arguments"):
|
|
192
|
+
job_data["arguments"]["project_root"] = display
|
|
193
|
+
payload = secret_store.protect(json.dumps(job_data, ensure_ascii=False))
|
|
194
|
+
db.execute("UPDATE jobs SET project_id=?,payload=? WHERE id=?", (key, payload, job_id))
|
|
195
|
+
db.execute("DELETE FROM projects WHERE id=?", (project_id,))
|
|
196
|
+
for table in ('usage_daily','usage_index_jobs','usage_imports'):
|
|
197
|
+
if db.execute('SELECT 1 FROM sqlite_master WHERE name=?',(table,)).fetchone():
|
|
198
|
+
db.execute(f'UPDATE {table} SET project_id=? WHERE project_id=?',(key,project_id))
|
|
199
|
+
return data
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def import_known_legacy(index_dir):
|
|
203
|
+
"""Somente raízes documentadas nos registros antigos; não adivinha diretórios."""
|
|
204
|
+
import indexer
|
|
205
|
+
candidates = set()
|
|
206
|
+
for name in ("daemon.log", "daemon.log.1"):
|
|
207
|
+
try:
|
|
208
|
+
with open(os.path.join(index_dir, name), encoding="utf-8", errors="replace") as f:
|
|
209
|
+
for line in f:
|
|
210
|
+
match = re.search(r"reindex (.+?): \d+ no escopo", line)
|
|
211
|
+
if match and os.path.isdir(match.group(1)):
|
|
212
|
+
candidates.add(match.group(1))
|
|
213
|
+
except OSError:
|
|
214
|
+
pass
|
|
215
|
+
for root in candidates:
|
|
216
|
+
key = project_identity.project_id(root)
|
|
217
|
+
if get(key) or not indexer.existing_db_path(root):
|
|
218
|
+
continue
|
|
219
|
+
register(root, watch=False)
|
|
220
|
+
update(key, enabled=True, status="migration_required", legacy_discovered=True)
|