codedthemes-cli 0.1.20__tar.gz → 0.1.22__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.20 → codedthemes_cli-0.1.22}/PKG-INFO +10 -4
- {codedthemes_cli-0.1.20 → codedthemes_cli-0.1.22}/README.md +9 -3
- {codedthemes_cli-0.1.20 → codedthemes_cli-0.1.22}/codedthemes/__init__.py +1 -1
- {codedthemes_cli-0.1.20 → codedthemes_cli-0.1.22}/codedthemes/cli.py +75 -42
- {codedthemes_cli-0.1.20 → codedthemes_cli-0.1.22}/codedthemes/config.py +4 -17
- {codedthemes_cli-0.1.20 → codedthemes_cli-0.1.22}/codedthemes/mcp_client.py +7 -11
- {codedthemes_cli-0.1.20 → codedthemes_cli-0.1.22}/codedthemes/patch_utils.py +1 -1
- {codedthemes_cli-0.1.20 → codedthemes_cli-0.1.22}/codedthemes/repo_utils.py +7 -45
- {codedthemes_cli-0.1.20 → codedthemes_cli-0.1.22}/codedthemes/sync_manager.py +9 -18
- {codedthemes_cli-0.1.20 → codedthemes_cli-0.1.22}/codedthemes_cli.egg-info/PKG-INFO +10 -4
- {codedthemes_cli-0.1.20 → codedthemes_cli-0.1.22}/pyproject.toml +1 -1
- {codedthemes_cli-0.1.20 → codedthemes_cli-0.1.22}/codedthemes_cli.egg-info/SOURCES.txt +0 -0
- {codedthemes_cli-0.1.20 → codedthemes_cli-0.1.22}/codedthemes_cli.egg-info/dependency_links.txt +0 -0
- {codedthemes_cli-0.1.20 → codedthemes_cli-0.1.22}/codedthemes_cli.egg-info/entry_points.txt +0 -0
- {codedthemes_cli-0.1.20 → codedthemes_cli-0.1.22}/codedthemes_cli.egg-info/requires.txt +0 -0
- {codedthemes_cli-0.1.20 → codedthemes_cli-0.1.22}/codedthemes_cli.egg-info/top_level.txt +0 -0
- {codedthemes_cli-0.1.20 → codedthemes_cli-0.1.22}/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.22
|
|
4
4
|
Summary: CLI tool for Code Theme and Integration
|
|
5
5
|
Author: codedthemes
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -46,15 +46,21 @@ 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
|
|
50
|
-
|
|
49
|
+
### 3. Initialization
|
|
51
50
|
Initialize your repository to establish a baseline for synchronization. This is recommended for first-time use in a project.
|
|
52
51
|
|
|
53
52
|
```bash
|
|
54
53
|
codedthemes init
|
|
55
54
|
```
|
|
56
55
|
|
|
57
|
-
###
|
|
56
|
+
### 4. Reactivation
|
|
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
|
|
58
64
|
|
|
59
65
|
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.
|
|
60
66
|
|
|
@@ -36,15 +36,21 @@ 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
|
|
40
|
-
|
|
39
|
+
### 3. Initialization
|
|
41
40
|
Initialize your repository to establish a baseline for synchronization. This is recommended for first-time use in a project.
|
|
42
41
|
|
|
43
42
|
```bash
|
|
44
43
|
codedthemes init
|
|
45
44
|
```
|
|
46
45
|
|
|
47
|
-
###
|
|
46
|
+
### 4. Reactivation
|
|
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
|
|
48
54
|
|
|
49
55
|
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.
|
|
50
56
|
|
|
@@ -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,24 +177,20 @@ 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": {')
|
|
196
185
|
print(' "type": "sse",')
|
|
197
|
-
print(' "serverURL": "https://mcp.codedthemes.com/api/mcp"
|
|
186
|
+
print(' "serverURL": "https://mcp.codedthemes.com/api/mcp"')
|
|
198
187
|
print(f' "env": {{ "CODEDTHEMES_TOKEN": "{client.token}" }}')
|
|
199
188
|
print(' }')
|
|
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:
|
|
@@ -240,6 +223,58 @@ def handle_init():
|
|
|
240
223
|
sys.exit(1)
|
|
241
224
|
|
|
242
225
|
|
|
226
|
+
def handle_reinit():
|
|
227
|
+
"""
|
|
228
|
+
Reactivates an evicted workspace by re-uploading the repository to the cloud.
|
|
229
|
+
"""
|
|
230
|
+
try:
|
|
231
|
+
repo_root = detect_repo_root()
|
|
232
|
+
print(f"🔍 Found repository at {repo_root}")
|
|
233
|
+
|
|
234
|
+
client = MCPClient()
|
|
235
|
+
if not client.token:
|
|
236
|
+
print("✖ Not logged in. Please run 'codedthemes login' first.")
|
|
237
|
+
sys.exit(1)
|
|
238
|
+
|
|
239
|
+
user_email = "unknown_user"
|
|
240
|
+
try:
|
|
241
|
+
decoded = jwt.decode(client.token, options={"verify_signature": False})
|
|
242
|
+
user_email = decoded.get("email", "unknown_user")
|
|
243
|
+
except:
|
|
244
|
+
pass
|
|
245
|
+
|
|
246
|
+
repo_abs_path = os.path.abspath(repo_root)
|
|
247
|
+
repo_key = repo_abs_path.lower().replace(os.sep, "/")
|
|
248
|
+
|
|
249
|
+
print("🚀 Reactivating repository (this may take a moment)...")
|
|
250
|
+
upload_result = client.upload_workspace(repo_abs_path, user_email)
|
|
251
|
+
workspace_id = upload_result.get("workspace_id")
|
|
252
|
+
|
|
253
|
+
if not workspace_id:
|
|
254
|
+
print("✖ Reactivation failed: No workspace ID returned.")
|
|
255
|
+
sys.exit(1)
|
|
256
|
+
|
|
257
|
+
config = load_config()
|
|
258
|
+
last_workspaces = config.get("workspaces", {})
|
|
259
|
+
last_workspaces[repo_key] = workspace_id
|
|
260
|
+
config["workspaces"] = last_workspaces
|
|
261
|
+
save_config(config)
|
|
262
|
+
|
|
263
|
+
sync_manager = SyncManager()
|
|
264
|
+
sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
|
|
265
|
+
|
|
266
|
+
print(f"✔ Workspace reactivated successfully.")
|
|
267
|
+
print(f"✔ Workspace ID: {workspace_id}")
|
|
268
|
+
print("\n" + "="*60)
|
|
269
|
+
print("Your repository is now synced and active in the cloud!")
|
|
270
|
+
print("You can now return to your IDE and continue using the AI assistant.")
|
|
271
|
+
print("="*60)
|
|
272
|
+
|
|
273
|
+
except Exception as e:
|
|
274
|
+
print(f"✖ Error during reactivation: {e}")
|
|
275
|
+
sys.exit(1)
|
|
276
|
+
|
|
277
|
+
|
|
243
278
|
def clean_remote_path(p: str) -> str:
|
|
244
279
|
"""
|
|
245
280
|
Strips the deeply nested remote container path if present.
|
|
@@ -286,7 +321,7 @@ def handle_apply(query: str):
|
|
|
286
321
|
last_workspaces = config.get("workspaces", {})
|
|
287
322
|
workspace_id = last_workspaces.get(repo_key)
|
|
288
323
|
|
|
289
|
-
|
|
324
|
+
|
|
290
325
|
if workspace_id:
|
|
291
326
|
try:
|
|
292
327
|
check = client.call("check_workspace", {"workspace_id": workspace_id})
|
|
@@ -297,7 +332,6 @@ def handle_apply(query: str):
|
|
|
297
332
|
sys.exit(0)
|
|
298
333
|
|
|
299
334
|
if isinstance(check, dict) and check.get("status") == "ok":
|
|
300
|
-
# print(f"✔ Using active workspace: {workspace_id}")
|
|
301
335
|
pass
|
|
302
336
|
else:
|
|
303
337
|
workspace_id = None
|
|
@@ -309,7 +343,6 @@ def handle_apply(query: str):
|
|
|
309
343
|
sys.exit(0)
|
|
310
344
|
workspace_id = None
|
|
311
345
|
|
|
312
|
-
|
|
313
346
|
if not workspace_id:
|
|
314
347
|
print("📦 Zipping and uploading repository (this may take a moment)...")
|
|
315
348
|
upload_result = client.upload_workspace(repo_abs_path, user_email)
|
|
@@ -319,7 +352,7 @@ def handle_apply(query: str):
|
|
|
319
352
|
save_config(config)
|
|
320
353
|
sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
|
|
321
354
|
|
|
322
|
-
|
|
355
|
+
|
|
323
356
|
print("🚀 Analyzing repository...")
|
|
324
357
|
analysis = client.call("repo_analyzer", {
|
|
325
358
|
"repo_path": repo_abs_path,
|
|
@@ -336,7 +369,7 @@ def handle_apply(query: str):
|
|
|
336
369
|
else:
|
|
337
370
|
print(f"✔ {analysis}")
|
|
338
371
|
|
|
339
|
-
|
|
372
|
+
|
|
340
373
|
print("🔍 Detecting local changes...")
|
|
341
374
|
local_changes = sync_manager.get_changed_files(repo_abs_path, user_email)
|
|
342
375
|
if local_changes:
|
|
@@ -389,7 +422,7 @@ def handle_apply(query: str):
|
|
|
389
422
|
return
|
|
390
423
|
print("Please enter 'y' or 'n'.")
|
|
391
424
|
|
|
392
|
-
|
|
425
|
+
|
|
393
426
|
print("⚙ Executing plan...")
|
|
394
427
|
result = client.call("execute_plan", {
|
|
395
428
|
"query": query,
|
|
@@ -406,7 +439,7 @@ def handle_apply(query: str):
|
|
|
406
439
|
print(f"✖ Execution failed: {error_msg}")
|
|
407
440
|
return
|
|
408
441
|
|
|
409
|
-
|
|
442
|
+
|
|
410
443
|
res_data = result
|
|
411
444
|
if isinstance(result, str):
|
|
412
445
|
try: res_data = json.loads(result)
|
|
@@ -415,7 +448,7 @@ def handle_apply(query: str):
|
|
|
415
448
|
updates = res_data.get("updates_for_local", [])
|
|
416
449
|
deletes = res_data.get("deleted_files", [])
|
|
417
450
|
|
|
418
|
-
|
|
451
|
+
|
|
419
452
|
for item in updates:
|
|
420
453
|
rel_path = clean_remote_path(item.get("path"))
|
|
421
454
|
code = item.get("code")
|
|
@@ -430,7 +463,7 @@ def handle_apply(query: str):
|
|
|
430
463
|
except Exception as e:
|
|
431
464
|
print(f"✖ Error writing {rel_path}: {e}")
|
|
432
465
|
|
|
433
|
-
|
|
466
|
+
|
|
434
467
|
for rel_path_raw in (deletes or []):
|
|
435
468
|
rel_path = clean_remote_path(rel_path_raw)
|
|
436
469
|
abs_path = os.path.normpath(os.path.join(repo_abs_path, rel_path.replace('/', os.sep)))
|
|
@@ -441,12 +474,12 @@ def handle_apply(query: str):
|
|
|
441
474
|
print(f"✔ Deleted: {rel_path}")
|
|
442
475
|
except: pass
|
|
443
476
|
|
|
444
|
-
|
|
477
|
+
|
|
445
478
|
if updates or deletes:
|
|
446
479
|
sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
|
|
447
480
|
print("✔ Local sync state updated.")
|
|
448
481
|
|
|
449
|
-
|
|
482
|
+
|
|
450
483
|
_details_raw = res_data.get("details", [])
|
|
451
484
|
details_list = _details_raw.get("report", []) if isinstance(_details_raw, dict) else (_details_raw if isinstance(_details_raw, list) else [])
|
|
452
485
|
if details_list:
|
|
@@ -455,7 +488,7 @@ def handle_apply(query: str):
|
|
|
455
488
|
status_icon = "✔" if item.get("success") else "✖"
|
|
456
489
|
print(f" [{status_icon}] {item.get('file_path')}: {item.get('message', '')}")
|
|
457
490
|
|
|
458
|
-
|
|
491
|
+
|
|
459
492
|
if not updates and not deletes:
|
|
460
493
|
msg = res_data.get('message', 'Changes applied successfully.')
|
|
461
494
|
if isinstance(msg, str) and "AI MUST now sync" in msg:
|
|
@@ -477,7 +510,7 @@ def main():
|
|
|
477
510
|
parser = argparse.ArgumentParser(prog="codedthemes", description="CodedThemes CLI client")
|
|
478
511
|
parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
|
|
479
512
|
|
|
480
|
-
subparsers = parser.add_subparsers(dest="command", required=False)
|
|
513
|
+
subparsers = parser.add_subparsers(dest="command", required=False)
|
|
481
514
|
|
|
482
515
|
|
|
483
516
|
login_parser = subparsers.add_parser("login", help="Login to MCP server")
|
|
@@ -487,9 +520,9 @@ def main():
|
|
|
487
520
|
apply_parser.add_argument("query", help="Description of changes to make")
|
|
488
521
|
|
|
489
522
|
subparsers.add_parser("init", help="Initialize repository and sync to cloud")
|
|
523
|
+
subparsers.add_parser("reinit", help="Reactivate an evicted workspace and sync to cloud")
|
|
490
524
|
subparsers.add_parser("logout", help="Log out from current device")
|
|
491
525
|
|
|
492
|
-
|
|
493
526
|
args = parser.parse_args()
|
|
494
527
|
|
|
495
528
|
if not args.command:
|
|
@@ -500,12 +533,12 @@ def main():
|
|
|
500
533
|
handle_login(args.server)
|
|
501
534
|
elif args.command == "init":
|
|
502
535
|
handle_init()
|
|
536
|
+
elif args.command == "reinit":
|
|
537
|
+
handle_reinit()
|
|
503
538
|
elif args.command == "apply":
|
|
504
539
|
handle_apply(args.query)
|
|
505
540
|
elif args.command == "logout":
|
|
506
541
|
handle_logout()
|
|
507
542
|
|
|
508
|
-
|
|
509
|
-
|
|
510
543
|
if __name__ == "__main__":
|
|
511
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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: codedthemes-cli
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.22
|
|
4
4
|
Summary: CLI tool for Code Theme and Integration
|
|
5
5
|
Author: codedthemes
|
|
6
6
|
Requires-Python: >=3.10
|
|
@@ -46,15 +46,21 @@ 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
|
|
50
|
-
|
|
49
|
+
### 3. Initialization
|
|
51
50
|
Initialize your repository to establish a baseline for synchronization. This is recommended for first-time use in a project.
|
|
52
51
|
|
|
53
52
|
```bash
|
|
54
53
|
codedthemes init
|
|
55
54
|
```
|
|
56
55
|
|
|
57
|
-
###
|
|
56
|
+
### 4. Reactivation
|
|
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
|
|
58
64
|
|
|
59
65
|
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.
|
|
60
66
|
|
|
File without changes
|
{codedthemes_cli-0.1.20 → codedthemes_cli-0.1.22}/codedthemes_cli.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|