codedthemes-cli 0.1.19__tar.gz → 0.1.21__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.19 → codedthemes_cli-0.1.21}/PKG-INFO +4 -10
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.21}/README.md +3 -9
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.21}/codedthemes/__init__.py +1 -1
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.21}/codedthemes/cli.py +43 -75
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.21}/codedthemes/config.py +17 -4
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.21}/codedthemes/mcp_client.py +11 -7
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.21}/codedthemes/patch_utils.py +1 -1
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.21}/codedthemes/repo_utils.py +45 -7
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.21}/codedthemes/sync_manager.py +18 -9
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.21}/codedthemes_cli.egg-info/PKG-INFO +4 -10
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.21}/pyproject.toml +1 -1
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.21}/codedthemes_cli.egg-info/SOURCES.txt +0 -0
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.21}/codedthemes_cli.egg-info/dependency_links.txt +0 -0
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.21}/codedthemes_cli.egg-info/entry_points.txt +0 -0
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.21}/codedthemes_cli.egg-info/requires.txt +0 -0
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.21}/codedthemes_cli.egg-info/top_level.txt +0 -0
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.21}/setup.cfg +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codedthemes-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.21
|
|
4
4
|
Summary: CLI tool for Code Theme and Integration
|
|
5
5
|
Author: codedthemes
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -46,21 +46,15 @@ Log out from the current device to free up a license slot on the server.
|
|
|
46
46
|
codedthemes logout
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
### 3. Initialization
|
|
49
|
+
### 3. Initialization (Optional)
|
|
50
|
+
|
|
50
51
|
Initialize your repository to establish a baseline for synchronization. This is recommended for first-time use in a project.
|
|
51
52
|
|
|
52
53
|
```bash
|
|
53
54
|
codedthemes init
|
|
54
55
|
```
|
|
55
56
|
|
|
56
|
-
###
|
|
57
|
-
If your project workspace has been evicted due to inactivity (30+ minutes), use `reinit` to quickly restore it.
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
codedthemes reinit
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### 5. Applying Changes
|
|
57
|
+
### 3. Applying Changes
|
|
64
58
|
|
|
65
59
|
Describe the changes you want to make in natural language. The CLI will analyze your repository, plan the changes, and ask for your approval before patching.
|
|
66
60
|
|
|
@@ -36,21 +36,15 @@ Log out from the current device to free up a license slot on the server.
|
|
|
36
36
|
codedthemes logout
|
|
37
37
|
```
|
|
38
38
|
|
|
39
|
-
### 3. Initialization
|
|
39
|
+
### 3. Initialization (Optional)
|
|
40
|
+
|
|
40
41
|
Initialize your repository to establish a baseline for synchronization. This is recommended for first-time use in a project.
|
|
41
42
|
|
|
42
43
|
```bash
|
|
43
44
|
codedthemes init
|
|
44
45
|
```
|
|
45
46
|
|
|
46
|
-
###
|
|
47
|
-
If your project workspace has been evicted due to inactivity (30+ minutes), use `reinit` to quickly restore it.
|
|
48
|
-
|
|
49
|
-
```bash
|
|
50
|
-
codedthemes reinit
|
|
51
|
-
```
|
|
52
|
-
|
|
53
|
-
### 5. Applying Changes
|
|
47
|
+
### 3. Applying Changes
|
|
54
48
|
|
|
55
49
|
Describe the changes you want to make in natural language. The CLI will analyze your repository, plan the changes, and ask for your approval before patching.
|
|
56
50
|
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import argparse
|
|
2
|
-
import
|
|
2
|
+
import sys
|
|
3
3
|
import os
|
|
4
|
+
import json
|
|
4
5
|
import shutil
|
|
5
|
-
import
|
|
6
|
-
import sys
|
|
6
|
+
from getpass import getpass
|
|
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
|
|
12
12
|
from .mcp_client import MCPClient
|
|
13
|
-
from .repo_utils import detect_repo_root
|
|
13
|
+
from .repo_utils import detect_repo_root, zip_repo
|
|
14
14
|
from .sync_manager import SyncManager
|
|
15
|
+
import socket
|
|
15
16
|
|
|
16
17
|
|
|
17
18
|
def handle_login(server_url: str = None):
|
|
@@ -28,6 +29,7 @@ def handle_login(server_url: str = None):
|
|
|
28
29
|
choice = input("Switch account? (y/N): ").strip().lower()
|
|
29
30
|
if choice not in ['y', 'yes']:
|
|
30
31
|
print("Login cancelled.")
|
|
32
|
+
print("Please enter your credentials to log in.")
|
|
31
33
|
return
|
|
32
34
|
except:
|
|
33
35
|
pass
|
|
@@ -35,7 +37,7 @@ def handle_login(server_url: str = None):
|
|
|
35
37
|
email = input("Email: ").strip()
|
|
36
38
|
license_key = input("License key: ").strip()
|
|
37
39
|
|
|
38
|
-
# Idempotent check: already logged in as this user?
|
|
40
|
+
# Idempotent check: Are we already logged in as this user?
|
|
39
41
|
if existing_token:
|
|
40
42
|
try:
|
|
41
43
|
decoded = jwt.decode(existing_token, options={"verify_signature": False})
|
|
@@ -67,7 +69,7 @@ def handle_login(server_url: str = None):
|
|
|
67
69
|
print("✖ Login failed: No access token returned. Please check your credentials.")
|
|
68
70
|
sys.exit(1)
|
|
69
71
|
|
|
70
|
-
|
|
72
|
+
# LAZY CONFIG: Only save if login succeeded
|
|
71
73
|
save_config({
|
|
72
74
|
"access_token": token,
|
|
73
75
|
"server_url": client.server_url,
|
|
@@ -112,9 +114,12 @@ def handle_logout():
|
|
|
112
114
|
"license_key": license_key,
|
|
113
115
|
"device_id": device_id
|
|
114
116
|
})
|
|
115
|
-
except Exception:
|
|
117
|
+
except Exception as e:
|
|
118
|
+
# Silently fail if server logout fails, we still want to clear local state
|
|
116
119
|
pass
|
|
117
120
|
|
|
121
|
+
# Local cleanup: Remove config.json but KEEP device.json
|
|
122
|
+
from .config import CONFIG_FILE
|
|
118
123
|
if CONFIG_FILE.exists():
|
|
119
124
|
CONFIG_FILE.unlink()
|
|
120
125
|
|
|
@@ -147,7 +152,12 @@ def handle_init():
|
|
|
147
152
|
repo_abs_path = os.path.abspath(repo_root)
|
|
148
153
|
repo_key = repo_abs_path.lower().replace(os.sep, "/")
|
|
149
154
|
|
|
155
|
+
# Check if already initialized to avoid redundant uploads
|
|
150
156
|
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"
|
|
151
161
|
|
|
152
162
|
print("📦 Zipping and uploading repository (this may take a moment)...")
|
|
153
163
|
upload_result = client.upload_workspace(repo_abs_path, user_email)
|
|
@@ -176,19 +186,23 @@ def handle_init():
|
|
|
176
186
|
|
|
177
187
|
print("Choose your editor and add the MCP server:\n")
|
|
178
188
|
|
|
189
|
+
# ---------------- ANTIGRAVITY ----------------
|
|
179
190
|
print("🔹 Antigravity")
|
|
180
191
|
print(" MCP Servers → Manage → Config\n")
|
|
192
|
+
|
|
181
193
|
print(' {')
|
|
182
194
|
print(' "mcpServers": {')
|
|
183
195
|
print(' "code-theme-mcp": {')
|
|
184
196
|
print(' "type": "sse",')
|
|
185
|
-
print(' "serverURL": "https://mcp.codedthemes.com/api/mcp"')
|
|
197
|
+
print(' "serverURL": "https://mcp.codedthemes.com/api/mcp",')
|
|
186
198
|
print(' }')
|
|
187
199
|
print(' }')
|
|
188
200
|
print(' }\n')
|
|
189
201
|
|
|
202
|
+
# ---------------- VS CODE ----------------
|
|
190
203
|
print("🔹 VS Code")
|
|
191
204
|
print(" Ctrl+Shift+P → MCP: Add MCP Server → http\n")
|
|
205
|
+
|
|
192
206
|
print(' {')
|
|
193
207
|
print(' "servers": {')
|
|
194
208
|
print(' "code-theme-mcp": {')
|
|
@@ -200,8 +214,10 @@ def handle_init():
|
|
|
200
214
|
print(' "inputs": []')
|
|
201
215
|
print(' }\n')
|
|
202
216
|
|
|
217
|
+
# ---------------- CURSOR ----------------
|
|
203
218
|
print("🔹 Cursor")
|
|
204
219
|
print(" Settings → Features → MCP Servers\n")
|
|
220
|
+
|
|
205
221
|
print(' {')
|
|
206
222
|
print(' "mcpServers": {')
|
|
207
223
|
print(' "code-theme-mcp": {')
|
|
@@ -213,63 +229,13 @@ def handle_init():
|
|
|
213
229
|
print(' }\n')
|
|
214
230
|
|
|
215
231
|
print("✔ Once saved, your AI assistant will detect available tools automatically.\n")
|
|
216
|
-
print('💡 Example: "Update the primary color to deep purple"')
|
|
217
|
-
print("="*60)
|
|
218
|
-
|
|
219
|
-
except Exception as e:
|
|
220
|
-
print(f"✖ Error during initialization: {e}")
|
|
221
|
-
sys.exit(1)
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
def handle_reinit():
|
|
225
|
-
"""
|
|
226
|
-
Reactivates an evicted workspace by re-uploading the repository to the cloud.
|
|
227
|
-
"""
|
|
228
|
-
try:
|
|
229
|
-
repo_root = detect_repo_root()
|
|
230
|
-
print(f"🔍 Found repository at {repo_root}")
|
|
231
|
-
|
|
232
|
-
client = MCPClient()
|
|
233
|
-
if not client.token:
|
|
234
|
-
print("✖ Not logged in. Please run 'codedthemes login' first.")
|
|
235
|
-
sys.exit(1)
|
|
236
|
-
|
|
237
|
-
user_email = "unknown_user"
|
|
238
|
-
try:
|
|
239
|
-
decoded = jwt.decode(client.token, options={"verify_signature": False})
|
|
240
|
-
user_email = decoded.get("email", "unknown_user")
|
|
241
|
-
except:
|
|
242
|
-
pass
|
|
243
232
|
|
|
244
|
-
|
|
245
|
-
repo_key = repo_abs_path.lower().replace(os.sep, "/")
|
|
246
|
-
|
|
247
|
-
print("🚀 Reactivating repository (this may take a moment)...")
|
|
248
|
-
upload_result = client.upload_workspace(repo_abs_path, user_email)
|
|
249
|
-
workspace_id = upload_result.get("workspace_id")
|
|
250
|
-
|
|
251
|
-
if not workspace_id:
|
|
252
|
-
print("✖ Reactivation failed: No workspace ID returned.")
|
|
253
|
-
sys.exit(1)
|
|
254
|
-
|
|
255
|
-
config = load_config()
|
|
256
|
-
last_workspaces = config.get("workspaces", {})
|
|
257
|
-
last_workspaces[repo_key] = workspace_id
|
|
258
|
-
config["workspaces"] = last_workspaces
|
|
259
|
-
save_config(config)
|
|
260
|
-
|
|
261
|
-
sync_manager = SyncManager()
|
|
262
|
-
sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
|
|
233
|
+
print('💡 Example: "Update the primary color to deep purple"')
|
|
263
234
|
|
|
264
|
-
print(f"✔ Workspace reactivated successfully.")
|
|
265
|
-
print(f"✔ Workspace ID: {workspace_id}")
|
|
266
|
-
print("\n" + "="*60)
|
|
267
|
-
print("Your repository is now synced and active in the cloud!")
|
|
268
|
-
print("You can now return to your IDE and continue using the AI assistant.")
|
|
269
235
|
print("="*60)
|
|
270
236
|
|
|
271
237
|
except Exception as e:
|
|
272
|
-
print(f"✖ Error during
|
|
238
|
+
print(f"✖ Error during initialization: {e}")
|
|
273
239
|
sys.exit(1)
|
|
274
240
|
|
|
275
241
|
|
|
@@ -319,7 +285,7 @@ def handle_apply(query: str):
|
|
|
319
285
|
last_workspaces = config.get("workspaces", {})
|
|
320
286
|
workspace_id = last_workspaces.get(repo_key)
|
|
321
287
|
|
|
322
|
-
|
|
288
|
+
# Verify or re-initialize workspace
|
|
323
289
|
if workspace_id:
|
|
324
290
|
try:
|
|
325
291
|
check = client.call("check_workspace", {"workspace_id": workspace_id})
|
|
@@ -330,6 +296,7 @@ def handle_apply(query: str):
|
|
|
330
296
|
sys.exit(0)
|
|
331
297
|
|
|
332
298
|
if isinstance(check, dict) and check.get("status") == "ok":
|
|
299
|
+
# print(f"✔ Using active workspace: {workspace_id}")
|
|
333
300
|
pass
|
|
334
301
|
else:
|
|
335
302
|
workspace_id = None
|
|
@@ -341,6 +308,7 @@ def handle_apply(query: str):
|
|
|
341
308
|
sys.exit(0)
|
|
342
309
|
workspace_id = None
|
|
343
310
|
|
|
311
|
+
|
|
344
312
|
if not workspace_id:
|
|
345
313
|
print("📦 Zipping and uploading repository (this may take a moment)...")
|
|
346
314
|
upload_result = client.upload_workspace(repo_abs_path, user_email)
|
|
@@ -350,7 +318,7 @@ def handle_apply(query: str):
|
|
|
350
318
|
save_config(config)
|
|
351
319
|
sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
|
|
352
320
|
|
|
353
|
-
|
|
321
|
+
# 1. Repository Analysis
|
|
354
322
|
print("🚀 Analyzing repository...")
|
|
355
323
|
analysis = client.call("repo_analyzer", {
|
|
356
324
|
"repo_path": repo_abs_path,
|
|
@@ -367,7 +335,7 @@ def handle_apply(query: str):
|
|
|
367
335
|
else:
|
|
368
336
|
print(f"✔ {analysis}")
|
|
369
337
|
|
|
370
|
-
|
|
338
|
+
# 2. Planning
|
|
371
339
|
print("🔍 Detecting local changes...")
|
|
372
340
|
local_changes = sync_manager.get_changed_files(repo_abs_path, user_email)
|
|
373
341
|
if local_changes:
|
|
@@ -420,7 +388,7 @@ def handle_apply(query: str):
|
|
|
420
388
|
return
|
|
421
389
|
print("Please enter 'y' or 'n'.")
|
|
422
390
|
|
|
423
|
-
|
|
391
|
+
# 3. Execution
|
|
424
392
|
print("⚙ Executing plan...")
|
|
425
393
|
result = client.call("execute_plan", {
|
|
426
394
|
"query": query,
|
|
@@ -437,7 +405,7 @@ def handle_apply(query: str):
|
|
|
437
405
|
print(f"✖ Execution failed: {error_msg}")
|
|
438
406
|
return
|
|
439
407
|
|
|
440
|
-
|
|
408
|
+
# 4. Local Patching
|
|
441
409
|
res_data = result
|
|
442
410
|
if isinstance(result, str):
|
|
443
411
|
try: res_data = json.loads(result)
|
|
@@ -446,7 +414,7 @@ def handle_apply(query: str):
|
|
|
446
414
|
updates = res_data.get("updates_for_local", [])
|
|
447
415
|
deletes = res_data.get("deleted_files", [])
|
|
448
416
|
|
|
449
|
-
|
|
417
|
+
# 1. Apply local updates
|
|
450
418
|
for item in updates:
|
|
451
419
|
rel_path = clean_remote_path(item.get("path"))
|
|
452
420
|
code = item.get("code")
|
|
@@ -461,7 +429,7 @@ def handle_apply(query: str):
|
|
|
461
429
|
except Exception as e:
|
|
462
430
|
print(f"✖ Error writing {rel_path}: {e}")
|
|
463
431
|
|
|
464
|
-
|
|
432
|
+
# 2. Apply local deletions
|
|
465
433
|
for rel_path_raw in (deletes or []):
|
|
466
434
|
rel_path = clean_remote_path(rel_path_raw)
|
|
467
435
|
abs_path = os.path.normpath(os.path.join(repo_abs_path, rel_path.replace('/', os.sep)))
|
|
@@ -472,12 +440,12 @@ def handle_apply(query: str):
|
|
|
472
440
|
print(f"✔ Deleted: {rel_path}")
|
|
473
441
|
except: pass
|
|
474
442
|
|
|
475
|
-
|
|
443
|
+
# 3. Synchronize state
|
|
476
444
|
if updates or deletes:
|
|
477
445
|
sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
|
|
478
446
|
print("✔ Local sync state updated.")
|
|
479
447
|
|
|
480
|
-
|
|
448
|
+
# 4. Show Integration Report (Always show errors or details)
|
|
481
449
|
_details_raw = res_data.get("details", [])
|
|
482
450
|
details_list = _details_raw.get("report", []) if isinstance(_details_raw, dict) else (_details_raw if isinstance(_details_raw, list) else [])
|
|
483
451
|
if details_list:
|
|
@@ -486,7 +454,7 @@ def handle_apply(query: str):
|
|
|
486
454
|
status_icon = "✔" if item.get("success") else "✖"
|
|
487
455
|
print(f" [{status_icon}] {item.get('file_path')}: {item.get('message', '')}")
|
|
488
456
|
|
|
489
|
-
|
|
457
|
+
# 5. Output Final Message if no updates were applied
|
|
490
458
|
if not updates and not deletes:
|
|
491
459
|
msg = res_data.get('message', 'Changes applied successfully.')
|
|
492
460
|
if isinstance(msg, str) and "AI MUST now sync" in msg:
|
|
@@ -508,7 +476,7 @@ def main():
|
|
|
508
476
|
parser = argparse.ArgumentParser(prog="codedthemes", description="CodedThemes CLI client")
|
|
509
477
|
parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
|
|
510
478
|
|
|
511
|
-
subparsers = parser.add_subparsers(dest="command", required=False)
|
|
479
|
+
subparsers = parser.add_subparsers(dest="command", required=False) # Changed to False to allow --version to work alone
|
|
512
480
|
|
|
513
481
|
|
|
514
482
|
login_parser = subparsers.add_parser("login", help="Login to MCP server")
|
|
@@ -518,9 +486,9 @@ def main():
|
|
|
518
486
|
apply_parser.add_argument("query", help="Description of changes to make")
|
|
519
487
|
|
|
520
488
|
subparsers.add_parser("init", help="Initialize repository and sync to cloud")
|
|
521
|
-
subparsers.add_parser("reinit", help="Reactivate an evicted workspace and sync to cloud")
|
|
522
489
|
subparsers.add_parser("logout", help="Log out from current device")
|
|
523
490
|
|
|
491
|
+
|
|
524
492
|
args = parser.parse_args()
|
|
525
493
|
|
|
526
494
|
if not args.command:
|
|
@@ -531,12 +499,12 @@ def main():
|
|
|
531
499
|
handle_login(args.server)
|
|
532
500
|
elif args.command == "init":
|
|
533
501
|
handle_init()
|
|
534
|
-
elif args.command == "reinit":
|
|
535
|
-
handle_reinit()
|
|
536
502
|
elif args.command == "apply":
|
|
537
503
|
handle_apply(args.query)
|
|
538
504
|
elif args.command == "logout":
|
|
539
505
|
handle_logout()
|
|
540
506
|
|
|
507
|
+
|
|
508
|
+
|
|
541
509
|
if __name__ == "__main__":
|
|
542
510
|
main()
|
|
@@ -4,17 +4,22 @@ import hashlib
|
|
|
4
4
|
import uuid
|
|
5
5
|
from pathlib import Path
|
|
6
6
|
|
|
7
|
+
# Stores token at ~/.codedthemes/config.json
|
|
7
8
|
CONFIG_DIR = Path.home() / ".codedthemes"
|
|
8
9
|
CONFIG_FILE = CONFIG_DIR / "config.json"
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
def ensure_config_dir():
|
|
12
|
-
"""
|
|
13
|
+
"""
|
|
14
|
+
Ensures the configuration directory exists.
|
|
15
|
+
"""
|
|
13
16
|
CONFIG_DIR.mkdir(exist_ok=True)
|
|
14
17
|
|
|
15
18
|
|
|
16
19
|
def save_config(data: dict):
|
|
17
|
-
"""
|
|
20
|
+
"""
|
|
21
|
+
Saves the provided configuration data to the config file (merging with existing).
|
|
22
|
+
"""
|
|
18
23
|
ensure_config_dir()
|
|
19
24
|
existing = load_config()
|
|
20
25
|
existing.update(data)
|
|
@@ -23,7 +28,9 @@ def save_config(data: dict):
|
|
|
23
28
|
|
|
24
29
|
|
|
25
30
|
def get_device_id():
|
|
26
|
-
"""
|
|
31
|
+
"""
|
|
32
|
+
Returns a unique device ID, stored globally in ~/.codedthemes/device.json.
|
|
33
|
+
"""
|
|
27
34
|
device_file = CONFIG_DIR / "device.json"
|
|
28
35
|
|
|
29
36
|
if device_file.exists():
|
|
@@ -35,6 +42,7 @@ def get_device_id():
|
|
|
35
42
|
except:
|
|
36
43
|
pass
|
|
37
44
|
|
|
45
|
+
# Generate new device ID
|
|
38
46
|
try:
|
|
39
47
|
hostname = socket.gethostname()
|
|
40
48
|
device_uuid = str(uuid.getnode())
|
|
@@ -43,6 +51,7 @@ def get_device_id():
|
|
|
43
51
|
except:
|
|
44
52
|
device_id = str(uuid.uuid4())[:12]
|
|
45
53
|
|
|
54
|
+
# Save globally (create dir if not exists)
|
|
46
55
|
CONFIG_DIR.mkdir(exist_ok=True)
|
|
47
56
|
try:
|
|
48
57
|
with open(device_file, "w") as f:
|
|
@@ -53,8 +62,12 @@ def get_device_id():
|
|
|
53
62
|
return device_id
|
|
54
63
|
|
|
55
64
|
|
|
65
|
+
|
|
66
|
+
|
|
56
67
|
def load_config():
|
|
57
|
-
"""
|
|
68
|
+
"""
|
|
69
|
+
Loads the configuration data from the config file.
|
|
70
|
+
"""
|
|
58
71
|
if not CONFIG_FILE.exists():
|
|
59
72
|
return {}
|
|
60
73
|
with open(CONFIG_FILE, "r") as f:
|
|
@@ -1,14 +1,12 @@
|
|
|
1
1
|
import os
|
|
2
|
-
|
|
3
2
|
import requests
|
|
4
|
-
|
|
5
3
|
from .config import load_config
|
|
6
4
|
from .repo_utils import zip_repo
|
|
7
5
|
|
|
8
|
-
|
|
9
6
|
class MCPClient:
|
|
10
|
-
"""
|
|
11
|
-
|
|
7
|
+
"""
|
|
8
|
+
Client for interacting with the CodedThemes MCP Server.
|
|
9
|
+
"""
|
|
12
10
|
def __init__(self):
|
|
13
11
|
config = load_config()
|
|
14
12
|
self.server_url = (
|
|
@@ -19,7 +17,9 @@ class MCPClient:
|
|
|
19
17
|
self.token = config.get("access_token")
|
|
20
18
|
|
|
21
19
|
def upload_workspace(self, repo_path: str, user_id: str):
|
|
22
|
-
"""
|
|
20
|
+
"""
|
|
21
|
+
Zips and uploads the repository to create a new workspace.
|
|
22
|
+
"""
|
|
23
23
|
zip_path = zip_repo(repo_path)
|
|
24
24
|
repo_name = os.path.basename(repo_path)
|
|
25
25
|
|
|
@@ -59,11 +59,14 @@ class MCPClient:
|
|
|
59
59
|
os.remove(zip_path)
|
|
60
60
|
|
|
61
61
|
def call(self, tool_name: str, payload: dict):
|
|
62
|
-
"""
|
|
62
|
+
"""
|
|
63
|
+
Calls a remote MCP tool or the login endpoint.
|
|
64
|
+
"""
|
|
63
65
|
headers = {}
|
|
64
66
|
if self.token:
|
|
65
67
|
headers["Authorization"] = f"Bearer {self.token}"
|
|
66
68
|
|
|
69
|
+
# Login and Logout use dedicated endpoints
|
|
67
70
|
if tool_name == "login":
|
|
68
71
|
url = f"{self.server_url}/auth/login"
|
|
69
72
|
elif tool_name == "logout":
|
|
@@ -71,6 +74,7 @@ class MCPClient:
|
|
|
71
74
|
else:
|
|
72
75
|
url = f"{self.server_url}/tools/{tool_name}"
|
|
73
76
|
|
|
77
|
+
|
|
74
78
|
response = requests.post(
|
|
75
79
|
url,
|
|
76
80
|
json=payload,
|
|
@@ -1,31 +1,48 @@
|
|
|
1
1
|
import os
|
|
2
|
+
import zipfile
|
|
3
|
+
import tempfile
|
|
2
4
|
import logging
|
|
3
5
|
from pathlib import Path
|
|
4
6
|
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
|
+
|
|
10
12
|
EXCLUDE_DIRS = {
|
|
13
|
+
# Version control
|
|
11
14
|
".git", ".svn", ".hg",
|
|
15
|
+
# Dependencies
|
|
12
16
|
"node_modules", "bower_components", "vendor", "venv", ".venv", "env",
|
|
17
|
+
# Build outputs
|
|
13
18
|
"build", "dist", "out", "target", ".output", "_build",
|
|
19
|
+
# Framework caches
|
|
14
20
|
".next", ".nuxt", ".svelte-kit", ".angular", ".turbo",
|
|
15
21
|
".parcel-cache", ".cache", ".temp", ".tmp",
|
|
22
|
+
# IDE / editor
|
|
16
23
|
".idea", ".vscode", ".vs",
|
|
24
|
+
# Python
|
|
17
25
|
"__pycache__", ".tox", ".mypy_cache", ".pytest_cache", ".ruff_cache",
|
|
26
|
+
# Testing
|
|
18
27
|
"coverage", ".nyc_output", "htmlcov",
|
|
28
|
+
# Misc
|
|
19
29
|
".terraform", ".serverless",
|
|
20
30
|
}
|
|
21
31
|
|
|
22
32
|
EXCLUDE_EXTENSIONS = {
|
|
33
|
+
# Images
|
|
23
34
|
".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".webp", ".ico", ".svg",
|
|
35
|
+
# Fonts
|
|
24
36
|
".woff", ".woff2", ".ttf", ".eot", ".otf",
|
|
37
|
+
# Video / audio
|
|
25
38
|
".mp4", ".webm", ".avi", ".mov", ".mp3", ".wav", ".ogg",
|
|
39
|
+
# Archives
|
|
26
40
|
".zip", ".tar", ".gz", ".rar", ".7z",
|
|
41
|
+
# Documents
|
|
27
42
|
".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
|
|
43
|
+
# Source maps
|
|
28
44
|
".map",
|
|
45
|
+
# Compiled / binary
|
|
29
46
|
".exe", ".dll", ".so", ".dylib", ".o", ".pyc", ".pyo", ".class",
|
|
30
47
|
}
|
|
31
48
|
|
|
@@ -38,8 +55,14 @@ EXCLUDE_FILES = {
|
|
|
38
55
|
MAX_SINGLE_FILE_BYTES = 2 * 1024 * 1024 # 2 MB
|
|
39
56
|
|
|
40
57
|
|
|
58
|
+
# ── .gitignore parsing ────────────────────────────────────────────────────────
|
|
59
|
+
|
|
41
60
|
def _load_gitignore_patterns(repo_root):
|
|
42
|
-
"""
|
|
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
|
+
"""
|
|
43
66
|
gitignore_path = os.path.join(repo_root, ".gitignore")
|
|
44
67
|
patterns = []
|
|
45
68
|
if not os.path.isfile(gitignore_path):
|
|
@@ -50,6 +73,7 @@ def _load_gitignore_patterns(repo_root):
|
|
|
50
73
|
line = line.strip()
|
|
51
74
|
if not line or line.startswith("#"):
|
|
52
75
|
continue
|
|
76
|
+
# Ignore negation patterns (!) for simplicity
|
|
53
77
|
if line.startswith("!"):
|
|
54
78
|
continue
|
|
55
79
|
patterns.append(line.rstrip("/"))
|
|
@@ -59,12 +83,16 @@ def _load_gitignore_patterns(repo_root):
|
|
|
59
83
|
|
|
60
84
|
|
|
61
85
|
def _is_gitignored(rel_path, patterns):
|
|
62
|
-
"""
|
|
86
|
+
"""
|
|
87
|
+
Checks if a relative path matches any .gitignore pattern.
|
|
88
|
+
"""
|
|
63
89
|
parts = rel_path.replace("\\", "/").split("/")
|
|
64
90
|
for pattern in patterns:
|
|
91
|
+
# Directory-level match: check each path component
|
|
65
92
|
for part in parts:
|
|
66
93
|
if fnmatch(part, pattern):
|
|
67
94
|
return True
|
|
95
|
+
# Full-path match
|
|
68
96
|
if fnmatch(rel_path.replace("\\", "/"), pattern):
|
|
69
97
|
return True
|
|
70
98
|
if fnmatch(rel_path.replace("\\", "/"), f"**/{pattern}"):
|
|
@@ -72,10 +100,13 @@ def _is_gitignored(rel_path, patterns):
|
|
|
72
100
|
return False
|
|
73
101
|
|
|
74
102
|
|
|
103
|
+
# ── Core functions ────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
75
105
|
def detect_repo_root(start_path=None):
|
|
76
106
|
"""
|
|
77
107
|
Checks if the current directory contains ai.json or .git.
|
|
78
|
-
Strictly restricted to the current path (no upward traversal)
|
|
108
|
+
Strictly restricted to the current path (no upward traversal) to avoid
|
|
109
|
+
incorrectly detecting parent repos.
|
|
79
110
|
"""
|
|
80
111
|
if not start_path:
|
|
81
112
|
start_path = os.getcwd()
|
|
@@ -88,6 +119,8 @@ def detect_repo_root(start_path=None):
|
|
|
88
119
|
raise Exception("No repository root found. Please run this command inside a Git repository or one initialized with 'codedthemes init'.")
|
|
89
120
|
|
|
90
121
|
|
|
122
|
+
|
|
123
|
+
|
|
91
124
|
def zip_repo(repo_root):
|
|
92
125
|
"""
|
|
93
126
|
Creates a temporary ZIP archive of the repository with aggressive filtering:
|
|
@@ -106,26 +139,30 @@ def zip_repo(repo_root):
|
|
|
106
139
|
|
|
107
140
|
with zipfile.ZipFile(temp_zip.name, "w", zipfile.ZIP_DEFLATED) as z:
|
|
108
141
|
for root, dirs, files in os.walk(repo_root):
|
|
142
|
+
# Prune excluded directories (in-place)
|
|
109
143
|
dirs[:] = [
|
|
110
144
|
d for d in dirs
|
|
111
145
|
if d not in EXCLUDE_DIRS
|
|
112
|
-
and not d.startswith(".")
|
|
113
|
-
or d in {".github"}
|
|
146
|
+
and not d.startswith(".") # skip all hidden dirs (e.g. .env, .husky)
|
|
147
|
+
or d in {".github"} # but keep .github
|
|
114
148
|
]
|
|
115
149
|
|
|
116
150
|
for file in files:
|
|
117
151
|
full_path = os.path.join(root, file)
|
|
118
152
|
rel_path = os.path.relpath(full_path, repo_root).replace(os.sep, "/")
|
|
119
153
|
|
|
154
|
+
# Skip by exact filename
|
|
120
155
|
if file in EXCLUDE_FILES:
|
|
121
156
|
skipped_count += 1
|
|
122
157
|
continue
|
|
123
158
|
|
|
159
|
+
# Skip by extension
|
|
124
160
|
_, ext = os.path.splitext(file)
|
|
125
161
|
if ext.lower() in EXCLUDE_EXTENSIONS:
|
|
126
162
|
skipped_count += 1
|
|
127
163
|
continue
|
|
128
164
|
|
|
165
|
+
# Skip large files
|
|
129
166
|
try:
|
|
130
167
|
if os.path.getsize(full_path) > MAX_SINGLE_FILE_BYTES:
|
|
131
168
|
logger.debug(f"Skipping large file: {rel_path}")
|
|
@@ -134,6 +171,7 @@ def zip_repo(repo_root):
|
|
|
134
171
|
except OSError:
|
|
135
172
|
continue
|
|
136
173
|
|
|
174
|
+
# Skip .gitignore-matched paths
|
|
137
175
|
if gitignore_patterns and _is_gitignored(rel_path, gitignore_patterns):
|
|
138
176
|
skipped_count += 1
|
|
139
177
|
continue
|
|
@@ -2,13 +2,12 @@ import os
|
|
|
2
2
|
import json
|
|
3
3
|
import hashlib
|
|
4
4
|
from datetime import datetime
|
|
5
|
-
|
|
6
5
|
from .repo_utils import EXCLUDE_DIRS
|
|
7
6
|
|
|
8
|
-
|
|
9
7
|
class SyncManager:
|
|
10
|
-
"""
|
|
11
|
-
|
|
8
|
+
"""
|
|
9
|
+
Manages local file hashes to track manual changes and maintain synchronization state.
|
|
10
|
+
"""
|
|
12
11
|
def __init__(self):
|
|
13
12
|
self.config_dir = os.path.expanduser("~/.codedthemes")
|
|
14
13
|
self.config_file = os.path.join(self.config_dir, "workspace.json")
|
|
@@ -16,7 +15,9 @@ class SyncManager:
|
|
|
16
15
|
self.workspaces = self.load()
|
|
17
16
|
|
|
18
17
|
def load(self):
|
|
19
|
-
"""
|
|
18
|
+
"""
|
|
19
|
+
Loads the workspace synchronization state from the cloud config.
|
|
20
|
+
"""
|
|
20
21
|
if os.path.exists(self.config_file):
|
|
21
22
|
try:
|
|
22
23
|
with open(self.config_file, 'r') as f: return json.load(f)
|
|
@@ -24,12 +25,16 @@ class SyncManager:
|
|
|
24
25
|
return {}
|
|
25
26
|
|
|
26
27
|
def save(self):
|
|
27
|
-
"""
|
|
28
|
+
"""
|
|
29
|
+
Saves the workspace synchronization state locally.
|
|
30
|
+
"""
|
|
28
31
|
with open(self.config_file, 'w') as f:
|
|
29
32
|
json.dump(self.workspaces, f, indent=2)
|
|
30
33
|
|
|
31
34
|
def compute_hashes(self, repo_path):
|
|
32
|
-
"""
|
|
35
|
+
"""
|
|
36
|
+
Computes MD5 hashes for all relevant files in the repository.
|
|
37
|
+
"""
|
|
33
38
|
hashes = {}
|
|
34
39
|
for root, dirs, files in os.walk(repo_path):
|
|
35
40
|
dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
|
|
@@ -45,7 +50,9 @@ class SyncManager:
|
|
|
45
50
|
return hashes
|
|
46
51
|
|
|
47
52
|
def update_sync_state(self, repo_path, workspace_id, user_id):
|
|
48
|
-
"""
|
|
53
|
+
"""
|
|
54
|
+
Updates the synchronization state with the latest file hashes.
|
|
55
|
+
"""
|
|
49
56
|
repo_key = f"{user_id}:{os.path.abspath(repo_path)}"
|
|
50
57
|
self.workspaces[repo_key] = {
|
|
51
58
|
"workspace_id": workspace_id,
|
|
@@ -56,7 +63,9 @@ class SyncManager:
|
|
|
56
63
|
self.save()
|
|
57
64
|
|
|
58
65
|
def get_changed_files(self, repo_path, user_id):
|
|
59
|
-
"""
|
|
66
|
+
"""
|
|
67
|
+
Identifies files that have been modified or deleted locally since the last sync.
|
|
68
|
+
"""
|
|
60
69
|
repo_key = f"{user_id}:{os.path.abspath(repo_path)}"
|
|
61
70
|
if repo_key not in self.workspaces: return []
|
|
62
71
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codedthemes-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.21
|
|
4
4
|
Summary: CLI tool for Code Theme and Integration
|
|
5
5
|
Author: codedthemes
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -46,21 +46,15 @@ Log out from the current device to free up a license slot on the server.
|
|
|
46
46
|
codedthemes logout
|
|
47
47
|
```
|
|
48
48
|
|
|
49
|
-
### 3. Initialization
|
|
49
|
+
### 3. Initialization (Optional)
|
|
50
|
+
|
|
50
51
|
Initialize your repository to establish a baseline for synchronization. This is recommended for first-time use in a project.
|
|
51
52
|
|
|
52
53
|
```bash
|
|
53
54
|
codedthemes init
|
|
54
55
|
```
|
|
55
56
|
|
|
56
|
-
###
|
|
57
|
-
If your project workspace has been evicted due to inactivity (30+ minutes), use `reinit` to quickly restore it.
|
|
58
|
-
|
|
59
|
-
```bash
|
|
60
|
-
codedthemes reinit
|
|
61
|
-
```
|
|
62
|
-
|
|
63
|
-
### 5. Applying Changes
|
|
57
|
+
### 3. Applying Changes
|
|
64
58
|
|
|
65
59
|
Describe the changes you want to make in natural language. The CLI will analyze your repository, plan the changes, and ask for your approval before patching.
|
|
66
60
|
|
|
File without changes
|
{codedthemes_cli-0.1.19 → codedthemes_cli-0.1.21}/codedthemes_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|