codedthemes-cli 0.1.19__tar.gz → 0.1.20__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.20}/PKG-INFO +4 -10
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.20}/README.md +3 -9
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.20}/codedthemes/__init__.py +1 -1
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.20}/codedthemes/cli.py +44 -75
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.20}/codedthemes/config.py +17 -4
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.20}/codedthemes/mcp_client.py +11 -7
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.20}/codedthemes/patch_utils.py +1 -1
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.20}/codedthemes/repo_utils.py +45 -7
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.20}/codedthemes/sync_manager.py +18 -9
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.20}/codedthemes_cli.egg-info/PKG-INFO +4 -10
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.20}/pyproject.toml +1 -1
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.20}/codedthemes_cli.egg-info/SOURCES.txt +0 -0
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.20}/codedthemes_cli.egg-info/dependency_links.txt +0 -0
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.20}/codedthemes_cli.egg-info/entry_points.txt +0 -0
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.20}/codedthemes_cli.egg-info/requires.txt +0 -0
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.20}/codedthemes_cli.egg-info/top_level.txt +0 -0
- {codedthemes_cli-0.1.19 → codedthemes_cli-0.1.20}/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.20
|
|
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,24 @@ 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",')
|
|
198
|
+
print(f' "env": {{ "CODEDTHEMES_TOKEN": "{client.token}" }}')
|
|
186
199
|
print(' }')
|
|
187
200
|
print(' }')
|
|
188
201
|
print(' }\n')
|
|
189
202
|
|
|
203
|
+
# ---------------- VS CODE ----------------
|
|
190
204
|
print("🔹 VS Code")
|
|
191
205
|
print(" Ctrl+Shift+P → MCP: Add MCP Server → http\n")
|
|
206
|
+
|
|
192
207
|
print(' {')
|
|
193
208
|
print(' "servers": {')
|
|
194
209
|
print(' "code-theme-mcp": {')
|
|
@@ -200,8 +215,10 @@ def handle_init():
|
|
|
200
215
|
print(' "inputs": []')
|
|
201
216
|
print(' }\n')
|
|
202
217
|
|
|
218
|
+
# ---------------- CURSOR ----------------
|
|
203
219
|
print("🔹 Cursor")
|
|
204
220
|
print(" Settings → Features → MCP Servers\n")
|
|
221
|
+
|
|
205
222
|
print(' {')
|
|
206
223
|
print(' "mcpServers": {')
|
|
207
224
|
print(' "code-theme-mcp": {')
|
|
@@ -213,63 +230,13 @@ def handle_init():
|
|
|
213
230
|
print(' }\n')
|
|
214
231
|
|
|
215
232
|
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
|
-
|
|
244
|
-
repo_abs_path = os.path.abspath(repo_root)
|
|
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
233
|
|
|
255
|
-
|
|
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)
|
|
234
|
+
print('💡 Example: "Update the primary color to deep purple"')
|
|
263
235
|
|
|
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
236
|
print("="*60)
|
|
270
237
|
|
|
271
238
|
except Exception as e:
|
|
272
|
-
print(f"✖ Error during
|
|
239
|
+
print(f"✖ Error during initialization: {e}")
|
|
273
240
|
sys.exit(1)
|
|
274
241
|
|
|
275
242
|
|
|
@@ -319,7 +286,7 @@ def handle_apply(query: str):
|
|
|
319
286
|
last_workspaces = config.get("workspaces", {})
|
|
320
287
|
workspace_id = last_workspaces.get(repo_key)
|
|
321
288
|
|
|
322
|
-
|
|
289
|
+
# Verify or re-initialize workspace
|
|
323
290
|
if workspace_id:
|
|
324
291
|
try:
|
|
325
292
|
check = client.call("check_workspace", {"workspace_id": workspace_id})
|
|
@@ -330,6 +297,7 @@ def handle_apply(query: str):
|
|
|
330
297
|
sys.exit(0)
|
|
331
298
|
|
|
332
299
|
if isinstance(check, dict) and check.get("status") == "ok":
|
|
300
|
+
# print(f"✔ Using active workspace: {workspace_id}")
|
|
333
301
|
pass
|
|
334
302
|
else:
|
|
335
303
|
workspace_id = None
|
|
@@ -341,6 +309,7 @@ def handle_apply(query: str):
|
|
|
341
309
|
sys.exit(0)
|
|
342
310
|
workspace_id = None
|
|
343
311
|
|
|
312
|
+
|
|
344
313
|
if not workspace_id:
|
|
345
314
|
print("📦 Zipping and uploading repository (this may take a moment)...")
|
|
346
315
|
upload_result = client.upload_workspace(repo_abs_path, user_email)
|
|
@@ -350,7 +319,7 @@ def handle_apply(query: str):
|
|
|
350
319
|
save_config(config)
|
|
351
320
|
sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
|
|
352
321
|
|
|
353
|
-
|
|
322
|
+
# 1. Repository Analysis
|
|
354
323
|
print("🚀 Analyzing repository...")
|
|
355
324
|
analysis = client.call("repo_analyzer", {
|
|
356
325
|
"repo_path": repo_abs_path,
|
|
@@ -367,7 +336,7 @@ def handle_apply(query: str):
|
|
|
367
336
|
else:
|
|
368
337
|
print(f"✔ {analysis}")
|
|
369
338
|
|
|
370
|
-
|
|
339
|
+
# 2. Planning
|
|
371
340
|
print("🔍 Detecting local changes...")
|
|
372
341
|
local_changes = sync_manager.get_changed_files(repo_abs_path, user_email)
|
|
373
342
|
if local_changes:
|
|
@@ -420,7 +389,7 @@ def handle_apply(query: str):
|
|
|
420
389
|
return
|
|
421
390
|
print("Please enter 'y' or 'n'.")
|
|
422
391
|
|
|
423
|
-
|
|
392
|
+
# 3. Execution
|
|
424
393
|
print("⚙ Executing plan...")
|
|
425
394
|
result = client.call("execute_plan", {
|
|
426
395
|
"query": query,
|
|
@@ -437,7 +406,7 @@ def handle_apply(query: str):
|
|
|
437
406
|
print(f"✖ Execution failed: {error_msg}")
|
|
438
407
|
return
|
|
439
408
|
|
|
440
|
-
|
|
409
|
+
# 4. Local Patching
|
|
441
410
|
res_data = result
|
|
442
411
|
if isinstance(result, str):
|
|
443
412
|
try: res_data = json.loads(result)
|
|
@@ -446,7 +415,7 @@ def handle_apply(query: str):
|
|
|
446
415
|
updates = res_data.get("updates_for_local", [])
|
|
447
416
|
deletes = res_data.get("deleted_files", [])
|
|
448
417
|
|
|
449
|
-
|
|
418
|
+
# 1. Apply local updates
|
|
450
419
|
for item in updates:
|
|
451
420
|
rel_path = clean_remote_path(item.get("path"))
|
|
452
421
|
code = item.get("code")
|
|
@@ -461,7 +430,7 @@ def handle_apply(query: str):
|
|
|
461
430
|
except Exception as e:
|
|
462
431
|
print(f"✖ Error writing {rel_path}: {e}")
|
|
463
432
|
|
|
464
|
-
|
|
433
|
+
# 2. Apply local deletions
|
|
465
434
|
for rel_path_raw in (deletes or []):
|
|
466
435
|
rel_path = clean_remote_path(rel_path_raw)
|
|
467
436
|
abs_path = os.path.normpath(os.path.join(repo_abs_path, rel_path.replace('/', os.sep)))
|
|
@@ -472,12 +441,12 @@ def handle_apply(query: str):
|
|
|
472
441
|
print(f"✔ Deleted: {rel_path}")
|
|
473
442
|
except: pass
|
|
474
443
|
|
|
475
|
-
|
|
444
|
+
# 3. Synchronize state
|
|
476
445
|
if updates or deletes:
|
|
477
446
|
sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
|
|
478
447
|
print("✔ Local sync state updated.")
|
|
479
448
|
|
|
480
|
-
|
|
449
|
+
# 4. Show Integration Report (Always show errors or details)
|
|
481
450
|
_details_raw = res_data.get("details", [])
|
|
482
451
|
details_list = _details_raw.get("report", []) if isinstance(_details_raw, dict) else (_details_raw if isinstance(_details_raw, list) else [])
|
|
483
452
|
if details_list:
|
|
@@ -486,7 +455,7 @@ def handle_apply(query: str):
|
|
|
486
455
|
status_icon = "✔" if item.get("success") else "✖"
|
|
487
456
|
print(f" [{status_icon}] {item.get('file_path')}: {item.get('message', '')}")
|
|
488
457
|
|
|
489
|
-
|
|
458
|
+
# 5. Output Final Message if no updates were applied
|
|
490
459
|
if not updates and not deletes:
|
|
491
460
|
msg = res_data.get('message', 'Changes applied successfully.')
|
|
492
461
|
if isinstance(msg, str) and "AI MUST now sync" in msg:
|
|
@@ -508,7 +477,7 @@ def main():
|
|
|
508
477
|
parser = argparse.ArgumentParser(prog="codedthemes", description="CodedThemes CLI client")
|
|
509
478
|
parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
|
|
510
479
|
|
|
511
|
-
subparsers = parser.add_subparsers(dest="command", required=False)
|
|
480
|
+
subparsers = parser.add_subparsers(dest="command", required=False) # Changed to False to allow --version to work alone
|
|
512
481
|
|
|
513
482
|
|
|
514
483
|
login_parser = subparsers.add_parser("login", help="Login to MCP server")
|
|
@@ -518,9 +487,9 @@ def main():
|
|
|
518
487
|
apply_parser.add_argument("query", help="Description of changes to make")
|
|
519
488
|
|
|
520
489
|
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
490
|
subparsers.add_parser("logout", help="Log out from current device")
|
|
523
491
|
|
|
492
|
+
|
|
524
493
|
args = parser.parse_args()
|
|
525
494
|
|
|
526
495
|
if not args.command:
|
|
@@ -531,12 +500,12 @@ def main():
|
|
|
531
500
|
handle_login(args.server)
|
|
532
501
|
elif args.command == "init":
|
|
533
502
|
handle_init()
|
|
534
|
-
elif args.command == "reinit":
|
|
535
|
-
handle_reinit()
|
|
536
503
|
elif args.command == "apply":
|
|
537
504
|
handle_apply(args.query)
|
|
538
505
|
elif args.command == "logout":
|
|
539
506
|
handle_logout()
|
|
540
507
|
|
|
508
|
+
|
|
509
|
+
|
|
541
510
|
if __name__ == "__main__":
|
|
542
511
|
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.20
|
|
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.20}/codedthemes_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|