codedthemes-cli 0.1.17__tar.gz → 0.1.18__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.
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.18}/PKG-INFO +1 -1
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.18}/codedthemes/__init__.py +1 -1
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.18}/codedthemes/cli.py +19 -42
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.18}/codedthemes/config.py +4 -17
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.18}/codedthemes/mcp_client.py +7 -11
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.18}/codedthemes/patch_utils.py +1 -1
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.18}/codedthemes/repo_utils.py +7 -45
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.18}/codedthemes/sync_manager.py +9 -18
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.18}/codedthemes_cli.egg-info/PKG-INFO +1 -1
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.18}/pyproject.toml +1 -1
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.18}/README.md +0 -0
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.18}/codedthemes_cli.egg-info/SOURCES.txt +0 -0
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.18}/codedthemes_cli.egg-info/dependency_links.txt +0 -0
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.18}/codedthemes_cli.egg-info/entry_points.txt +0 -0
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.18}/codedthemes_cli.egg-info/requires.txt +0 -0
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.18}/codedthemes_cli.egg-info/top_level.txt +0 -0
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.18}/setup.cfg +0 -0
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import argparse
|
|
2
|
-
import sys
|
|
3
|
-
import os
|
|
4
2
|
import json
|
|
3
|
+
import os
|
|
5
4
|
import shutil
|
|
6
|
-
|
|
5
|
+
import socket
|
|
6
|
+
import sys
|
|
7
7
|
|
|
8
8
|
import jwt
|
|
9
9
|
import requests
|
|
10
10
|
|
|
11
|
-
from .config import save_config, load_config, get_device_id
|
|
11
|
+
from .config import save_config, load_config, get_device_id, CONFIG_FILE
|
|
12
12
|
from .mcp_client import MCPClient
|
|
13
|
-
from .repo_utils import detect_repo_root
|
|
13
|
+
from .repo_utils import detect_repo_root
|
|
14
14
|
from .sync_manager import SyncManager
|
|
15
|
-
import socket
|
|
16
15
|
|
|
17
16
|
|
|
18
17
|
def handle_login(server_url: str = None):
|
|
@@ -37,7 +36,7 @@ def handle_login(server_url: str = None):
|
|
|
37
36
|
email = input("Email: ").strip()
|
|
38
37
|
license_key = input("License key: ").strip()
|
|
39
38
|
|
|
40
|
-
# Idempotent check:
|
|
39
|
+
# Idempotent check: already logged in as this user?
|
|
41
40
|
if existing_token:
|
|
42
41
|
try:
|
|
43
42
|
decoded = jwt.decode(existing_token, options={"verify_signature": False})
|
|
@@ -69,7 +68,7 @@ def handle_login(server_url: str = None):
|
|
|
69
68
|
print("✖ Login failed: No access token returned. Please check your credentials.")
|
|
70
69
|
sys.exit(1)
|
|
71
70
|
|
|
72
|
-
|
|
71
|
+
|
|
73
72
|
save_config({
|
|
74
73
|
"access_token": token,
|
|
75
74
|
"server_url": client.server_url,
|
|
@@ -114,12 +113,9 @@ def handle_logout():
|
|
|
114
113
|
"license_key": license_key,
|
|
115
114
|
"device_id": device_id
|
|
116
115
|
})
|
|
117
|
-
except Exception
|
|
118
|
-
# Silently fail if server logout fails, we still want to clear local state
|
|
116
|
+
except Exception:
|
|
119
117
|
pass
|
|
120
118
|
|
|
121
|
-
# Local cleanup: Remove config.json but KEEP device.json
|
|
122
|
-
from .config import CONFIG_FILE
|
|
123
119
|
if CONFIG_FILE.exists():
|
|
124
120
|
CONFIG_FILE.unlink()
|
|
125
121
|
|
|
@@ -152,12 +148,7 @@ def handle_init():
|
|
|
152
148
|
repo_abs_path = os.path.abspath(repo_root)
|
|
153
149
|
repo_key = repo_abs_path.lower().replace(os.sep, "/")
|
|
154
150
|
|
|
155
|
-
# Check if already initialized to avoid redundant uploads
|
|
156
151
|
config = load_config()
|
|
157
|
-
existing_ws_id = config.get("workspaces", {}).get(repo_key)
|
|
158
|
-
if existing_ws_id:
|
|
159
|
-
# Fast check: exists?
|
|
160
|
-
pass # We could call a /verify endpoint here, but for now let's just proceed with upload as "sync"
|
|
161
152
|
|
|
162
153
|
print("📦 Zipping and uploading repository (this may take a moment)...")
|
|
163
154
|
upload_result = client.upload_workspace(repo_abs_path, user_email)
|
|
@@ -186,10 +177,8 @@ def handle_init():
|
|
|
186
177
|
|
|
187
178
|
print("Choose your editor and add the MCP server:\n")
|
|
188
179
|
|
|
189
|
-
# ---------------- ANTIGRAVITY ----------------
|
|
190
180
|
print("🔹 Antigravity")
|
|
191
181
|
print(" MCP Servers → Manage → Config\n")
|
|
192
|
-
|
|
193
182
|
print(' {')
|
|
194
183
|
print(' "mcpServers": {')
|
|
195
184
|
print(' "code-theme-mcp": {')
|
|
@@ -200,10 +189,8 @@ def handle_init():
|
|
|
200
189
|
print(' }')
|
|
201
190
|
print(' }\n')
|
|
202
191
|
|
|
203
|
-
# ---------------- VS CODE ----------------
|
|
204
192
|
print("🔹 VS Code")
|
|
205
193
|
print(" Ctrl+Shift+P → MCP: Add MCP Server → http\n")
|
|
206
|
-
|
|
207
194
|
print(' {')
|
|
208
195
|
print(' "servers": {')
|
|
209
196
|
print(' "code-theme-mcp": {')
|
|
@@ -215,10 +202,8 @@ def handle_init():
|
|
|
215
202
|
print(' "inputs": []')
|
|
216
203
|
print(' }\n')
|
|
217
204
|
|
|
218
|
-
# ---------------- CURSOR ----------------
|
|
219
205
|
print("🔹 Cursor")
|
|
220
206
|
print(" Settings → Features → MCP Servers\n")
|
|
221
|
-
|
|
222
207
|
print(' {')
|
|
223
208
|
print(' "mcpServers": {')
|
|
224
209
|
print(' "code-theme-mcp": {')
|
|
@@ -230,9 +215,7 @@ def handle_init():
|
|
|
230
215
|
print(' }\n')
|
|
231
216
|
|
|
232
217
|
print("✔ Once saved, your AI assistant will detect available tools automatically.\n")
|
|
233
|
-
|
|
234
218
|
print('💡 Example: "Update the primary color to deep purple"')
|
|
235
|
-
|
|
236
219
|
print("="*60)
|
|
237
220
|
|
|
238
221
|
except Exception as e:
|
|
@@ -338,7 +321,7 @@ def handle_apply(query: str):
|
|
|
338
321
|
last_workspaces = config.get("workspaces", {})
|
|
339
322
|
workspace_id = last_workspaces.get(repo_key)
|
|
340
323
|
|
|
341
|
-
|
|
324
|
+
|
|
342
325
|
if workspace_id:
|
|
343
326
|
try:
|
|
344
327
|
check = client.call("check_workspace", {"workspace_id": workspace_id})
|
|
@@ -349,7 +332,6 @@ def handle_apply(query: str):
|
|
|
349
332
|
sys.exit(0)
|
|
350
333
|
|
|
351
334
|
if isinstance(check, dict) and check.get("status") == "ok":
|
|
352
|
-
# print(f"✔ Using active workspace: {workspace_id}")
|
|
353
335
|
pass
|
|
354
336
|
else:
|
|
355
337
|
workspace_id = None
|
|
@@ -361,7 +343,6 @@ def handle_apply(query: str):
|
|
|
361
343
|
sys.exit(0)
|
|
362
344
|
workspace_id = None
|
|
363
345
|
|
|
364
|
-
|
|
365
346
|
if not workspace_id:
|
|
366
347
|
print("📦 Zipping and uploading repository (this may take a moment)...")
|
|
367
348
|
upload_result = client.upload_workspace(repo_abs_path, user_email)
|
|
@@ -371,7 +352,7 @@ def handle_apply(query: str):
|
|
|
371
352
|
save_config(config)
|
|
372
353
|
sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
|
|
373
354
|
|
|
374
|
-
|
|
355
|
+
|
|
375
356
|
print("🚀 Analyzing repository...")
|
|
376
357
|
analysis = client.call("repo_analyzer", {
|
|
377
358
|
"repo_path": repo_abs_path,
|
|
@@ -388,7 +369,7 @@ def handle_apply(query: str):
|
|
|
388
369
|
else:
|
|
389
370
|
print(f"✔ {analysis}")
|
|
390
371
|
|
|
391
|
-
|
|
372
|
+
|
|
392
373
|
print("🔍 Detecting local changes...")
|
|
393
374
|
local_changes = sync_manager.get_changed_files(repo_abs_path, user_email)
|
|
394
375
|
if local_changes:
|
|
@@ -441,7 +422,7 @@ def handle_apply(query: str):
|
|
|
441
422
|
return
|
|
442
423
|
print("Please enter 'y' or 'n'.")
|
|
443
424
|
|
|
444
|
-
|
|
425
|
+
|
|
445
426
|
print("⚙ Executing plan...")
|
|
446
427
|
result = client.call("execute_plan", {
|
|
447
428
|
"query": query,
|
|
@@ -458,7 +439,7 @@ def handle_apply(query: str):
|
|
|
458
439
|
print(f"✖ Execution failed: {error_msg}")
|
|
459
440
|
return
|
|
460
441
|
|
|
461
|
-
|
|
442
|
+
|
|
462
443
|
res_data = result
|
|
463
444
|
if isinstance(result, str):
|
|
464
445
|
try: res_data = json.loads(result)
|
|
@@ -467,7 +448,7 @@ def handle_apply(query: str):
|
|
|
467
448
|
updates = res_data.get("updates_for_local", [])
|
|
468
449
|
deletes = res_data.get("deleted_files", [])
|
|
469
450
|
|
|
470
|
-
|
|
451
|
+
|
|
471
452
|
for item in updates:
|
|
472
453
|
rel_path = clean_remote_path(item.get("path"))
|
|
473
454
|
code = item.get("code")
|
|
@@ -482,7 +463,7 @@ def handle_apply(query: str):
|
|
|
482
463
|
except Exception as e:
|
|
483
464
|
print(f"✖ Error writing {rel_path}: {e}")
|
|
484
465
|
|
|
485
|
-
|
|
466
|
+
|
|
486
467
|
for rel_path_raw in (deletes or []):
|
|
487
468
|
rel_path = clean_remote_path(rel_path_raw)
|
|
488
469
|
abs_path = os.path.normpath(os.path.join(repo_abs_path, rel_path.replace('/', os.sep)))
|
|
@@ -493,12 +474,12 @@ def handle_apply(query: str):
|
|
|
493
474
|
print(f"✔ Deleted: {rel_path}")
|
|
494
475
|
except: pass
|
|
495
476
|
|
|
496
|
-
|
|
477
|
+
|
|
497
478
|
if updates or deletes:
|
|
498
479
|
sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
|
|
499
480
|
print("✔ Local sync state updated.")
|
|
500
481
|
|
|
501
|
-
|
|
482
|
+
|
|
502
483
|
_details_raw = res_data.get("details", [])
|
|
503
484
|
details_list = _details_raw.get("report", []) if isinstance(_details_raw, dict) else (_details_raw if isinstance(_details_raw, list) else [])
|
|
504
485
|
if details_list:
|
|
@@ -507,7 +488,7 @@ def handle_apply(query: str):
|
|
|
507
488
|
status_icon = "✔" if item.get("success") else "✖"
|
|
508
489
|
print(f" [{status_icon}] {item.get('file_path')}: {item.get('message', '')}")
|
|
509
490
|
|
|
510
|
-
|
|
491
|
+
|
|
511
492
|
if not updates and not deletes:
|
|
512
493
|
msg = res_data.get('message', 'Changes applied successfully.')
|
|
513
494
|
if isinstance(msg, str) and "AI MUST now sync" in msg:
|
|
@@ -529,7 +510,7 @@ def main():
|
|
|
529
510
|
parser = argparse.ArgumentParser(prog="codedthemes", description="CodedThemes CLI client")
|
|
530
511
|
parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
|
|
531
512
|
|
|
532
|
-
subparsers = parser.add_subparsers(dest="command", required=False)
|
|
513
|
+
subparsers = parser.add_subparsers(dest="command", required=False)
|
|
533
514
|
|
|
534
515
|
|
|
535
516
|
login_parser = subparsers.add_parser("login", help="Login to MCP server")
|
|
@@ -542,8 +523,6 @@ def main():
|
|
|
542
523
|
subparsers.add_parser("reinit", help="Reactivate an evicted workspace and sync to cloud")
|
|
543
524
|
subparsers.add_parser("logout", help="Log out from current device")
|
|
544
525
|
|
|
545
|
-
|
|
546
|
-
|
|
547
526
|
args = parser.parse_args()
|
|
548
527
|
|
|
549
528
|
if not args.command:
|
|
@@ -561,7 +540,5 @@ def main():
|
|
|
561
540
|
elif args.command == "logout":
|
|
562
541
|
handle_logout()
|
|
563
542
|
|
|
564
|
-
|
|
565
|
-
|
|
566
543
|
if __name__ == "__main__":
|
|
567
544
|
main()
|
|
@@ -4,22 +4,17 @@ import hashlib
|
|
|
4
4
|
import uuid
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
-
# Stores token at ~/.codedthemes/config.json
|
|
8
7
|
CONFIG_DIR = Path.home() / ".codedthemes"
|
|
9
8
|
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
10
9
|
|
|
11
10
|
|
|
12
11
|
def ensure_config_dir():
|
|
13
|
-
"""
|
|
14
|
-
Ensures the configuration directory exists.
|
|
15
|
-
"""
|
|
12
|
+
"""Ensures the configuration directory exists."""
|
|
16
13
|
CONFIG_DIR.mkdir(exist_ok=True)
|
|
17
14
|
|
|
18
15
|
|
|
19
16
|
def save_config(data: dict):
|
|
20
|
-
"""
|
|
21
|
-
Saves the provided configuration data to the config file (merging with existing).
|
|
22
|
-
"""
|
|
17
|
+
"""Saves configuration data to the config file (merging with existing)."""
|
|
23
18
|
ensure_config_dir()
|
|
24
19
|
existing = load_config()
|
|
25
20
|
existing.update(data)
|
|
@@ -28,9 +23,7 @@ def save_config(data: dict):
|
|
|
28
23
|
|
|
29
24
|
|
|
30
25
|
def get_device_id():
|
|
31
|
-
"""
|
|
32
|
-
Returns a unique device ID, stored globally in ~/.codedthemes/device.json.
|
|
33
|
-
"""
|
|
26
|
+
"""Returns a unique device ID, stored in ~/.codedthemes/device.json."""
|
|
34
27
|
device_file = CONFIG_DIR / "device.json"
|
|
35
28
|
|
|
36
29
|
if device_file.exists():
|
|
@@ -42,7 +35,6 @@ def get_device_id():
|
|
|
42
35
|
except:
|
|
43
36
|
pass
|
|
44
37
|
|
|
45
|
-
# Generate new device ID
|
|
46
38
|
try:
|
|
47
39
|
hostname = socket.gethostname()
|
|
48
40
|
device_uuid = str(uuid.getnode())
|
|
@@ -51,7 +43,6 @@ def get_device_id():
|
|
|
51
43
|
except:
|
|
52
44
|
device_id = str(uuid.uuid4())[:12]
|
|
53
45
|
|
|
54
|
-
# Save globally (create dir if not exists)
|
|
55
46
|
CONFIG_DIR.mkdir(exist_ok=True)
|
|
56
47
|
try:
|
|
57
48
|
with open(device_file, "w") as f:
|
|
@@ -62,12 +53,8 @@ def get_device_id():
|
|
|
62
53
|
return device_id
|
|
63
54
|
|
|
64
55
|
|
|
65
|
-
|
|
66
|
-
|
|
67
56
|
def load_config():
|
|
68
|
-
"""
|
|
69
|
-
Loads the configuration data from the config file.
|
|
70
|
-
"""
|
|
57
|
+
"""Loads configuration data from the config file."""
|
|
71
58
|
if not CONFIG_FILE.exists():
|
|
72
59
|
return {}
|
|
73
60
|
with open(CONFIG_FILE, "r") as f:
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import os
|
|
2
|
+
|
|
2
3
|
import requests
|
|
4
|
+
|
|
3
5
|
from .config import load_config
|
|
4
6
|
from .repo_utils import zip_repo
|
|
5
7
|
|
|
8
|
+
|
|
6
9
|
class MCPClient:
|
|
7
|
-
"""
|
|
8
|
-
|
|
9
|
-
"""
|
|
10
|
+
"""Client for interacting with the CodedThemes MCP Server."""
|
|
11
|
+
|
|
10
12
|
def __init__(self):
|
|
11
13
|
config = load_config()
|
|
12
14
|
self.server_url = (
|
|
@@ -17,9 +19,7 @@ class MCPClient:
|
|
|
17
19
|
self.token = config.get("access_token")
|
|
18
20
|
|
|
19
21
|
def upload_workspace(self, repo_path: str, user_id: str):
|
|
20
|
-
"""
|
|
21
|
-
Zips and uploads the repository to create a new workspace.
|
|
22
|
-
"""
|
|
22
|
+
"""Zips and uploads the repository to create a new workspace."""
|
|
23
23
|
zip_path = zip_repo(repo_path)
|
|
24
24
|
repo_name = os.path.basename(repo_path)
|
|
25
25
|
|
|
@@ -59,14 +59,11 @@ class MCPClient:
|
|
|
59
59
|
os.remove(zip_path)
|
|
60
60
|
|
|
61
61
|
def call(self, tool_name: str, payload: dict):
|
|
62
|
-
"""
|
|
63
|
-
Calls a remote MCP tool or the login endpoint.
|
|
64
|
-
"""
|
|
62
|
+
"""Calls a remote MCP tool or the login/logout endpoint."""
|
|
65
63
|
headers = {}
|
|
66
64
|
if self.token:
|
|
67
65
|
headers["Authorization"] = f"Bearer {self.token}"
|
|
68
66
|
|
|
69
|
-
# Login and Logout use dedicated endpoints
|
|
70
67
|
if tool_name == "login":
|
|
71
68
|
url = f"{self.server_url}/auth/login"
|
|
72
69
|
elif tool_name == "logout":
|
|
@@ -74,7 +71,6 @@ class MCPClient:
|
|
|
74
71
|
else:
|
|
75
72
|
url = f"{self.server_url}/tools/{tool_name}"
|
|
76
73
|
|
|
77
|
-
|
|
78
74
|
response = requests.post(
|
|
79
75
|
url,
|
|
80
76
|
json=payload,
|
|
@@ -1,48 +1,31 @@
|
|
|
1
1
|
import os
|
|
2
|
-
import zipfile
|
|
3
|
-
import tempfile
|
|
4
2
|
import logging
|
|
5
3
|
from pathlib import Path
|
|
6
4
|
from fnmatch import fnmatch
|
|
5
|
+
import zipfile
|
|
6
|
+
import tempfile
|
|
7
7
|
|
|
8
8
|
logger = logging.getLogger("codedthemes")
|
|
9
9
|
|
|
10
|
-
# ── Shared exclusion constants (used by both zip_repo and SyncManager) ────────
|
|
11
|
-
|
|
12
10
|
EXCLUDE_DIRS = {
|
|
13
|
-
# Version control
|
|
14
11
|
".git", ".svn", ".hg",
|
|
15
|
-
# Dependencies
|
|
16
12
|
"node_modules", "bower_components", "vendor", "venv", ".venv", "env",
|
|
17
|
-
# Build outputs
|
|
18
13
|
"build", "dist", "out", "target", ".output", "_build",
|
|
19
|
-
# Framework caches
|
|
20
14
|
".next", ".nuxt", ".svelte-kit", ".angular", ".turbo",
|
|
21
15
|
".parcel-cache", ".cache", ".temp", ".tmp",
|
|
22
|
-
# IDE / editor
|
|
23
16
|
".idea", ".vscode", ".vs",
|
|
24
|
-
# Python
|
|
25
17
|
"__pycache__", ".tox", ".mypy_cache", ".pytest_cache", ".ruff_cache",
|
|
26
|
-
# Testing
|
|
27
18
|
"coverage", ".nyc_output", "htmlcov",
|
|
28
|
-
# Misc
|
|
29
19
|
".terraform", ".serverless",
|
|
30
20
|
}
|
|
31
21
|
|
|
32
22
|
EXCLUDE_EXTENSIONS = {
|
|
33
|
-
# Images
|
|
34
23
|
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".webp", ".ico", ".svg",
|
|
35
|
-
# Fonts
|
|
36
24
|
".woff", ".woff2", ".ttf", ".eot", ".otf",
|
|
37
|
-
# Video / audio
|
|
38
25
|
".mp4", ".webm", ".avi", ".mov", ".mp3", ".wav", ".ogg",
|
|
39
|
-
# Archives
|
|
40
26
|
".zip", ".tar", ".gz", ".rar", ".7z",
|
|
41
|
-
# Documents
|
|
42
27
|
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
|
|
43
|
-
# Source maps
|
|
44
28
|
".map",
|
|
45
|
-
# Compiled / binary
|
|
46
29
|
".exe", ".dll", ".so", ".dylib", ".o", ".pyc", ".pyo", ".class",
|
|
47
30
|
}
|
|
48
31
|
|
|
@@ -55,14 +38,8 @@ EXCLUDE_FILES = {
|
|
|
55
38
|
MAX_SINGLE_FILE_BYTES = 2 * 1024 * 1024 # 2 MB
|
|
56
39
|
|
|
57
40
|
|
|
58
|
-
# ── .gitignore parsing ────────────────────────────────────────────────────────
|
|
59
|
-
|
|
60
41
|
def _load_gitignore_patterns(repo_root):
|
|
61
|
-
"""
|
|
62
|
-
Reads .gitignore at the repo root and returns a list of patterns.
|
|
63
|
-
Supports basic glob patterns; does NOT implement full git-ignore spec
|
|
64
|
-
(negation, nested .gitignore, etc.) but covers the common cases.
|
|
65
|
-
"""
|
|
42
|
+
"""Reads .gitignore at the repo root and returns a list of glob patterns."""
|
|
66
43
|
gitignore_path = os.path.join(repo_root, ".gitignore")
|
|
67
44
|
patterns = []
|
|
68
45
|
if not os.path.isfile(gitignore_path):
|
|
@@ -73,7 +50,6 @@ def _load_gitignore_patterns(repo_root):
|
|
|
73
50
|
line = line.strip()
|
|
74
51
|
if not line or line.startswith("#"):
|
|
75
52
|
continue
|
|
76
|
-
# Ignore negation patterns (!) for simplicity
|
|
77
53
|
if line.startswith("!"):
|
|
78
54
|
continue
|
|
79
55
|
patterns.append(line.rstrip("/"))
|
|
@@ -83,16 +59,12 @@ def _load_gitignore_patterns(repo_root):
|
|
|
83
59
|
|
|
84
60
|
|
|
85
61
|
def _is_gitignored(rel_path, patterns):
|
|
86
|
-
"""
|
|
87
|
-
Checks if a relative path matches any .gitignore pattern.
|
|
88
|
-
"""
|
|
62
|
+
"""Checks if a relative path matches any .gitignore pattern."""
|
|
89
63
|
parts = rel_path.replace("\\", "/").split("/")
|
|
90
64
|
for pattern in patterns:
|
|
91
|
-
# Directory-level match: check each path component
|
|
92
65
|
for part in parts:
|
|
93
66
|
if fnmatch(part, pattern):
|
|
94
67
|
return True
|
|
95
|
-
# Full-path match
|
|
96
68
|
if fnmatch(rel_path.replace("\\", "/"), pattern):
|
|
97
69
|
return True
|
|
98
70
|
if fnmatch(rel_path.replace("\\", "/"), f"**/{pattern}"):
|
|
@@ -100,13 +72,10 @@ def _is_gitignored(rel_path, patterns):
|
|
|
100
72
|
return False
|
|
101
73
|
|
|
102
74
|
|
|
103
|
-
# ── Core functions ────────────────────────────────────────────────────────────
|
|
104
|
-
|
|
105
75
|
def detect_repo_root(start_path=None):
|
|
106
76
|
"""
|
|
107
77
|
Checks if the current directory contains ai.json or .git.
|
|
108
|
-
Strictly restricted to the current path (no upward traversal)
|
|
109
|
-
incorrectly detecting parent repos.
|
|
78
|
+
Strictly restricted to the current path (no upward traversal).
|
|
110
79
|
"""
|
|
111
80
|
if not start_path:
|
|
112
81
|
start_path = os.getcwd()
|
|
@@ -119,8 +88,6 @@ def detect_repo_root(start_path=None):
|
|
|
119
88
|
raise Exception("No repository root found. Please run this command inside a Git repository or one initialized with 'codedthemes init'.")
|
|
120
89
|
|
|
121
90
|
|
|
122
|
-
|
|
123
|
-
|
|
124
91
|
def zip_repo(repo_root):
|
|
125
92
|
"""
|
|
126
93
|
Creates a temporary ZIP archive of the repository with aggressive filtering:
|
|
@@ -139,30 +106,26 @@ def zip_repo(repo_root):
|
|
|
139
106
|
|
|
140
107
|
with zipfile.ZipFile(temp_zip.name, "w", zipfile.ZIP_DEFLATED) as z:
|
|
141
108
|
for root, dirs, files in os.walk(repo_root):
|
|
142
|
-
# Prune excluded directories (in-place)
|
|
143
109
|
dirs[:] = [
|
|
144
110
|
d for d in dirs
|
|
145
111
|
if d not in EXCLUDE_DIRS
|
|
146
|
-
and not d.startswith(".")
|
|
147
|
-
or d in {".github"}
|
|
112
|
+
and not d.startswith(".")
|
|
113
|
+
or d in {".github"}
|
|
148
114
|
]
|
|
149
115
|
|
|
150
116
|
for file in files:
|
|
151
117
|
full_path = os.path.join(root, file)
|
|
152
118
|
rel_path = os.path.relpath(full_path, repo_root).replace(os.sep, "/")
|
|
153
119
|
|
|
154
|
-
# Skip by exact filename
|
|
155
120
|
if file in EXCLUDE_FILES:
|
|
156
121
|
skipped_count += 1
|
|
157
122
|
continue
|
|
158
123
|
|
|
159
|
-
# Skip by extension
|
|
160
124
|
_, ext = os.path.splitext(file)
|
|
161
125
|
if ext.lower() in EXCLUDE_EXTENSIONS:
|
|
162
126
|
skipped_count += 1
|
|
163
127
|
continue
|
|
164
128
|
|
|
165
|
-
# Skip large files
|
|
166
129
|
try:
|
|
167
130
|
if os.path.getsize(full_path) > MAX_SINGLE_FILE_BYTES:
|
|
168
131
|
logger.debug(f"Skipping large file: {rel_path}")
|
|
@@ -171,7 +134,6 @@ def zip_repo(repo_root):
|
|
|
171
134
|
except OSError:
|
|
172
135
|
continue
|
|
173
136
|
|
|
174
|
-
# Skip .gitignore-matched paths
|
|
175
137
|
if gitignore_patterns and _is_gitignored(rel_path, gitignore_patterns):
|
|
176
138
|
skipped_count += 1
|
|
177
139
|
continue
|
|
@@ -2,12 +2,13 @@ import os
|
|
|
2
2
|
import json
|
|
3
3
|
import hashlib
|
|
4
4
|
from datetime import datetime
|
|
5
|
+
|
|
5
6
|
from .repo_utils import EXCLUDE_DIRS
|
|
6
7
|
|
|
8
|
+
|
|
7
9
|
class SyncManager:
|
|
8
|
-
"""
|
|
9
|
-
|
|
10
|
-
"""
|
|
10
|
+
"""Manages local file hashes to track changes and maintain synchronization state."""
|
|
11
|
+
|
|
11
12
|
def __init__(self):
|
|
12
13
|
self.config_dir = os.path.expanduser("~/.codedthemes")
|
|
13
14
|
self.config_file = os.path.join(self.config_dir, "workspace.json")
|
|
@@ -15,9 +16,7 @@ class SyncManager:
|
|
|
15
16
|
self.workspaces = self.load()
|
|
16
17
|
|
|
17
18
|
def load(self):
|
|
18
|
-
"""
|
|
19
|
-
Loads the workspace synchronization state from the cloud config.
|
|
20
|
-
"""
|
|
19
|
+
"""Loads the workspace synchronization state."""
|
|
21
20
|
if os.path.exists(self.config_file):
|
|
22
21
|
try:
|
|
23
22
|
with open(self.config_file, 'r') as f: return json.load(f)
|
|
@@ -25,16 +24,12 @@ class SyncManager:
|
|
|
25
24
|
return {}
|
|
26
25
|
|
|
27
26
|
def save(self):
|
|
28
|
-
"""
|
|
29
|
-
Saves the workspace synchronization state locally.
|
|
30
|
-
"""
|
|
27
|
+
"""Saves the workspace synchronization state."""
|
|
31
28
|
with open(self.config_file, 'w') as f:
|
|
32
29
|
json.dump(self.workspaces, f, indent=2)
|
|
33
30
|
|
|
34
31
|
def compute_hashes(self, repo_path):
|
|
35
|
-
"""
|
|
36
|
-
Computes MD5 hashes for all relevant files in the repository.
|
|
37
|
-
"""
|
|
32
|
+
"""Computes MD5 hashes for all relevant files in the repository."""
|
|
38
33
|
hashes = {}
|
|
39
34
|
for root, dirs, files in os.walk(repo_path):
|
|
40
35
|
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
|
|
@@ -50,9 +45,7 @@ class SyncManager:
|
|
|
50
45
|
return hashes
|
|
51
46
|
|
|
52
47
|
def update_sync_state(self, repo_path, workspace_id, user_id):
|
|
53
|
-
"""
|
|
54
|
-
Updates the synchronization state with the latest file hashes.
|
|
55
|
-
"""
|
|
48
|
+
"""Updates the synchronization state with the latest file hashes."""
|
|
56
49
|
repo_key = f"{user_id}:{os.path.abspath(repo_path)}"
|
|
57
50
|
self.workspaces[repo_key] = {
|
|
58
51
|
"workspace_id": workspace_id,
|
|
@@ -63,9 +56,7 @@ class SyncManager:
|
|
|
63
56
|
self.save()
|
|
64
57
|
|
|
65
58
|
def get_changed_files(self, repo_path, user_id):
|
|
66
|
-
"""
|
|
67
|
-
Identifies files that have been modified or deleted locally since the last sync.
|
|
68
|
-
"""
|
|
59
|
+
"""Identifies files modified or deleted locally since the last sync."""
|
|
69
60
|
repo_key = f"{user_id}:{os.path.abspath(repo_path)}"
|
|
70
61
|
if repo_key not in self.workspaces: return []
|
|
71
62
|
|
|
File without changes
|
|
File without changes
|
{codedthemes_cli-0.1.17 → codedthemes_cli-0.1.18}/codedthemes_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|