codedthemes-cli 0.1.17__tar.gz → 0.1.19__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.19}/PKG-INFO +1 -1
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.19}/codedthemes/__init__.py +1 -1
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.19}/codedthemes/cli.py +20 -45
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.19}/codedthemes/config.py +4 -17
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.19}/codedthemes/mcp_client.py +7 -11
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.19}/codedthemes/patch_utils.py +1 -1
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.19}/codedthemes/repo_utils.py +7 -45
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.19}/codedthemes/sync_manager.py +9 -18
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.19}/codedthemes_cli.egg-info/PKG-INFO +1 -1
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.19}/pyproject.toml +1 -1
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.19}/README.md +0 -0
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.19}/codedthemes_cli.egg-info/SOURCES.txt +0 -0
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.19}/codedthemes_cli.egg-info/dependency_links.txt +0 -0
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.19}/codedthemes_cli.egg-info/entry_points.txt +0 -0
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.19}/codedthemes_cli.egg-info/requires.txt +0 -0
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.19}/codedthemes_cli.egg-info/top_level.txt +0 -0
- {codedthemes_cli-0.1.17 → codedthemes_cli-0.1.19}/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):
|
|
@@ -29,7 +28,6 @@ def handle_login(server_url: str = None):
|
|
|
29
28
|
choice = input("Switch account? (y/N): ").strip().lower()
|
|
30
29
|
if choice not in ['y', 'yes']:
|
|
31
30
|
print("Login cancelled.")
|
|
32
|
-
print("Please enter your credentials to log in.")
|
|
33
31
|
return
|
|
34
32
|
except:
|
|
35
33
|
pass
|
|
@@ -37,7 +35,7 @@ def handle_login(server_url: str = None):
|
|
|
37
35
|
email = input("Email: ").strip()
|
|
38
36
|
license_key = input("License key: ").strip()
|
|
39
37
|
|
|
40
|
-
# Idempotent check:
|
|
38
|
+
# Idempotent check: already logged in as this user?
|
|
41
39
|
if existing_token:
|
|
42
40
|
try:
|
|
43
41
|
decoded = jwt.decode(existing_token, options={"verify_signature": False})
|
|
@@ -69,7 +67,7 @@ def handle_login(server_url: str = None):
|
|
|
69
67
|
print("✖ Login failed: No access token returned. Please check your credentials.")
|
|
70
68
|
sys.exit(1)
|
|
71
69
|
|
|
72
|
-
|
|
70
|
+
|
|
73
71
|
save_config({
|
|
74
72
|
"access_token": token,
|
|
75
73
|
"server_url": client.server_url,
|
|
@@ -114,12 +112,9 @@ def handle_logout():
|
|
|
114
112
|
"license_key": license_key,
|
|
115
113
|
"device_id": device_id
|
|
116
114
|
})
|
|
117
|
-
except Exception
|
|
118
|
-
# Silently fail if server logout fails, we still want to clear local state
|
|
115
|
+
except Exception:
|
|
119
116
|
pass
|
|
120
117
|
|
|
121
|
-
# Local cleanup: Remove config.json but KEEP device.json
|
|
122
|
-
from .config import CONFIG_FILE
|
|
123
118
|
if CONFIG_FILE.exists():
|
|
124
119
|
CONFIG_FILE.unlink()
|
|
125
120
|
|
|
@@ -152,12 +147,7 @@ def handle_init():
|
|
|
152
147
|
repo_abs_path = os.path.abspath(repo_root)
|
|
153
148
|
repo_key = repo_abs_path.lower().replace(os.sep, "/")
|
|
154
149
|
|
|
155
|
-
# Check if already initialized to avoid redundant uploads
|
|
156
150
|
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
151
|
|
|
162
152
|
print("📦 Zipping and uploading repository (this may take a moment)...")
|
|
163
153
|
upload_result = client.upload_workspace(repo_abs_path, user_email)
|
|
@@ -186,24 +176,19 @@ def handle_init():
|
|
|
186
176
|
|
|
187
177
|
print("Choose your editor and add the MCP server:\n")
|
|
188
178
|
|
|
189
|
-
# ---------------- ANTIGRAVITY ----------------
|
|
190
179
|
print("🔹 Antigravity")
|
|
191
180
|
print(" MCP Servers → Manage → Config\n")
|
|
192
|
-
|
|
193
181
|
print(' {')
|
|
194
182
|
print(' "mcpServers": {')
|
|
195
183
|
print(' "code-theme-mcp": {')
|
|
196
184
|
print(' "type": "sse",')
|
|
197
|
-
print(' "serverURL": "https://mcp.codedthemes.com/api/mcp"
|
|
198
|
-
print(f' "env": {{ "CODEDTHEMES_TOKEN": "{client.token}" }}')
|
|
185
|
+
print(' "serverURL": "https://mcp.codedthemes.com/api/mcp"')
|
|
199
186
|
print(' }')
|
|
200
187
|
print(' }')
|
|
201
188
|
print(' }\n')
|
|
202
189
|
|
|
203
|
-
# ---------------- VS CODE ----------------
|
|
204
190
|
print("🔹 VS Code")
|
|
205
191
|
print(" Ctrl+Shift+P → MCP: Add MCP Server → http\n")
|
|
206
|
-
|
|
207
192
|
print(' {')
|
|
208
193
|
print(' "servers": {')
|
|
209
194
|
print(' "code-theme-mcp": {')
|
|
@@ -215,10 +200,8 @@ def handle_init():
|
|
|
215
200
|
print(' "inputs": []')
|
|
216
201
|
print(' }\n')
|
|
217
202
|
|
|
218
|
-
# ---------------- CURSOR ----------------
|
|
219
203
|
print("🔹 Cursor")
|
|
220
204
|
print(" Settings → Features → MCP Servers\n")
|
|
221
|
-
|
|
222
205
|
print(' {')
|
|
223
206
|
print(' "mcpServers": {')
|
|
224
207
|
print(' "code-theme-mcp": {')
|
|
@@ -230,9 +213,7 @@ def handle_init():
|
|
|
230
213
|
print(' }\n')
|
|
231
214
|
|
|
232
215
|
print("✔ Once saved, your AI assistant will detect available tools automatically.\n")
|
|
233
|
-
|
|
234
216
|
print('💡 Example: "Update the primary color to deep purple"')
|
|
235
|
-
|
|
236
217
|
print("="*60)
|
|
237
218
|
|
|
238
219
|
except Exception as e:
|
|
@@ -338,7 +319,7 @@ def handle_apply(query: str):
|
|
|
338
319
|
last_workspaces = config.get("workspaces", {})
|
|
339
320
|
workspace_id = last_workspaces.get(repo_key)
|
|
340
321
|
|
|
341
|
-
|
|
322
|
+
|
|
342
323
|
if workspace_id:
|
|
343
324
|
try:
|
|
344
325
|
check = client.call("check_workspace", {"workspace_id": workspace_id})
|
|
@@ -349,7 +330,6 @@ def handle_apply(query: str):
|
|
|
349
330
|
sys.exit(0)
|
|
350
331
|
|
|
351
332
|
if isinstance(check, dict) and check.get("status") == "ok":
|
|
352
|
-
# print(f"✔ Using active workspace: {workspace_id}")
|
|
353
333
|
pass
|
|
354
334
|
else:
|
|
355
335
|
workspace_id = None
|
|
@@ -361,7 +341,6 @@ def handle_apply(query: str):
|
|
|
361
341
|
sys.exit(0)
|
|
362
342
|
workspace_id = None
|
|
363
343
|
|
|
364
|
-
|
|
365
344
|
if not workspace_id:
|
|
366
345
|
print("📦 Zipping and uploading repository (this may take a moment)...")
|
|
367
346
|
upload_result = client.upload_workspace(repo_abs_path, user_email)
|
|
@@ -371,7 +350,7 @@ def handle_apply(query: str):
|
|
|
371
350
|
save_config(config)
|
|
372
351
|
sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
|
|
373
352
|
|
|
374
|
-
|
|
353
|
+
|
|
375
354
|
print("🚀 Analyzing repository...")
|
|
376
355
|
analysis = client.call("repo_analyzer", {
|
|
377
356
|
"repo_path": repo_abs_path,
|
|
@@ -388,7 +367,7 @@ def handle_apply(query: str):
|
|
|
388
367
|
else:
|
|
389
368
|
print(f"✔ {analysis}")
|
|
390
369
|
|
|
391
|
-
|
|
370
|
+
|
|
392
371
|
print("🔍 Detecting local changes...")
|
|
393
372
|
local_changes = sync_manager.get_changed_files(repo_abs_path, user_email)
|
|
394
373
|
if local_changes:
|
|
@@ -441,7 +420,7 @@ def handle_apply(query: str):
|
|
|
441
420
|
return
|
|
442
421
|
print("Please enter 'y' or 'n'.")
|
|
443
422
|
|
|
444
|
-
|
|
423
|
+
|
|
445
424
|
print("⚙ Executing plan...")
|
|
446
425
|
result = client.call("execute_plan", {
|
|
447
426
|
"query": query,
|
|
@@ -458,7 +437,7 @@ def handle_apply(query: str):
|
|
|
458
437
|
print(f"✖ Execution failed: {error_msg}")
|
|
459
438
|
return
|
|
460
439
|
|
|
461
|
-
|
|
440
|
+
|
|
462
441
|
res_data = result
|
|
463
442
|
if isinstance(result, str):
|
|
464
443
|
try: res_data = json.loads(result)
|
|
@@ -467,7 +446,7 @@ def handle_apply(query: str):
|
|
|
467
446
|
updates = res_data.get("updates_for_local", [])
|
|
468
447
|
deletes = res_data.get("deleted_files", [])
|
|
469
448
|
|
|
470
|
-
|
|
449
|
+
|
|
471
450
|
for item in updates:
|
|
472
451
|
rel_path = clean_remote_path(item.get("path"))
|
|
473
452
|
code = item.get("code")
|
|
@@ -482,7 +461,7 @@ def handle_apply(query: str):
|
|
|
482
461
|
except Exception as e:
|
|
483
462
|
print(f"✖ Error writing {rel_path}: {e}")
|
|
484
463
|
|
|
485
|
-
|
|
464
|
+
|
|
486
465
|
for rel_path_raw in (deletes or []):
|
|
487
466
|
rel_path = clean_remote_path(rel_path_raw)
|
|
488
467
|
abs_path = os.path.normpath(os.path.join(repo_abs_path, rel_path.replace('/', os.sep)))
|
|
@@ -493,12 +472,12 @@ def handle_apply(query: str):
|
|
|
493
472
|
print(f"✔ Deleted: {rel_path}")
|
|
494
473
|
except: pass
|
|
495
474
|
|
|
496
|
-
|
|
475
|
+
|
|
497
476
|
if updates or deletes:
|
|
498
477
|
sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
|
|
499
478
|
print("✔ Local sync state updated.")
|
|
500
479
|
|
|
501
|
-
|
|
480
|
+
|
|
502
481
|
_details_raw = res_data.get("details", [])
|
|
503
482
|
details_list = _details_raw.get("report", []) if isinstance(_details_raw, dict) else (_details_raw if isinstance(_details_raw, list) else [])
|
|
504
483
|
if details_list:
|
|
@@ -507,7 +486,7 @@ def handle_apply(query: str):
|
|
|
507
486
|
status_icon = "✔" if item.get("success") else "✖"
|
|
508
487
|
print(f" [{status_icon}] {item.get('file_path')}: {item.get('message', '')}")
|
|
509
488
|
|
|
510
|
-
|
|
489
|
+
|
|
511
490
|
if not updates and not deletes:
|
|
512
491
|
msg = res_data.get('message', 'Changes applied successfully.')
|
|
513
492
|
if isinstance(msg, str) and "AI MUST now sync" in msg:
|
|
@@ -529,7 +508,7 @@ def main():
|
|
|
529
508
|
parser = argparse.ArgumentParser(prog="codedthemes", description="CodedThemes CLI client")
|
|
530
509
|
parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
|
|
531
510
|
|
|
532
|
-
subparsers = parser.add_subparsers(dest="command", required=False)
|
|
511
|
+
subparsers = parser.add_subparsers(dest="command", required=False)
|
|
533
512
|
|
|
534
513
|
|
|
535
514
|
login_parser = subparsers.add_parser("login", help="Login to MCP server")
|
|
@@ -542,8 +521,6 @@ def main():
|
|
|
542
521
|
subparsers.add_parser("reinit", help="Reactivate an evicted workspace and sync to cloud")
|
|
543
522
|
subparsers.add_parser("logout", help="Log out from current device")
|
|
544
523
|
|
|
545
|
-
|
|
546
|
-
|
|
547
524
|
args = parser.parse_args()
|
|
548
525
|
|
|
549
526
|
if not args.command:
|
|
@@ -561,7 +538,5 @@ def main():
|
|
|
561
538
|
elif args.command == "logout":
|
|
562
539
|
handle_logout()
|
|
563
540
|
|
|
564
|
-
|
|
565
|
-
|
|
566
541
|
if __name__ == "__main__":
|
|
567
542
|
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.19}/codedthemes_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|