codedthemes-cli 0.1.21__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.21 → codedthemes_cli-0.1.22}/PKG-INFO +10 -4
- {codedthemes_cli-0.1.21 → codedthemes_cli-0.1.22}/README.md +9 -3
- {codedthemes_cli-0.1.21 → codedthemes_cli-0.1.22}/codedthemes/__init__.py +1 -1
- {codedthemes_cli-0.1.21 → codedthemes_cli-0.1.22}/codedthemes/cli.py +76 -42
- {codedthemes_cli-0.1.21 → codedthemes_cli-0.1.22}/codedthemes/config.py +4 -17
- {codedthemes_cli-0.1.21 → codedthemes_cli-0.1.22}/codedthemes/mcp_client.py +7 -11
- {codedthemes_cli-0.1.21 → codedthemes_cli-0.1.22}/codedthemes/patch_utils.py +1 -1
- {codedthemes_cli-0.1.21 → codedthemes_cli-0.1.22}/codedthemes/repo_utils.py +7 -45
- {codedthemes_cli-0.1.21 → codedthemes_cli-0.1.22}/codedthemes/sync_manager.py +9 -18
- {codedthemes_cli-0.1.21 → codedthemes_cli-0.1.22}/codedthemes_cli.egg-info/PKG-INFO +10 -4
- {codedthemes_cli-0.1.21 → codedthemes_cli-0.1.22}/pyproject.toml +1 -1
- {codedthemes_cli-0.1.21 → codedthemes_cli-0.1.22}/codedthemes_cli.egg-info/SOURCES.txt +0 -0
- {codedthemes_cli-0.1.21 → codedthemes_cli-0.1.22}/codedthemes_cli.egg-info/dependency_links.txt +0 -0
- {codedthemes_cli-0.1.21 → codedthemes_cli-0.1.22}/codedthemes_cli.egg-info/entry_points.txt +0 -0
- {codedthemes_cli-0.1.21 → codedthemes_cli-0.1.22}/codedthemes_cli.egg-info/requires.txt +0 -0
- {codedthemes_cli-0.1.21 → codedthemes_cli-0.1.22}/codedthemes_cli.egg-info/top_level.txt +0 -0
- {codedthemes_cli-0.1.21 → 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,23 +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"')
|
|
187
|
+
print(f' "env": {{ "CODEDTHEMES_TOKEN": "{client.token}" }}')
|
|
198
188
|
print(' }')
|
|
199
189
|
print(' }')
|
|
200
190
|
print(' }\n')
|
|
201
191
|
|
|
202
|
-
# ---------------- VS CODE ----------------
|
|
203
192
|
print("🔹 VS Code")
|
|
204
193
|
print(" Ctrl+Shift+P → MCP: Add MCP Server → http\n")
|
|
205
|
-
|
|
206
194
|
print(' {')
|
|
207
195
|
print(' "servers": {')
|
|
208
196
|
print(' "code-theme-mcp": {')
|
|
@@ -214,10 +202,8 @@ def handle_init():
|
|
|
214
202
|
print(' "inputs": []')
|
|
215
203
|
print(' }\n')
|
|
216
204
|
|
|
217
|
-
# ---------------- CURSOR ----------------
|
|
218
205
|
print("🔹 Cursor")
|
|
219
206
|
print(" Settings → Features → MCP Servers\n")
|
|
220
|
-
|
|
221
207
|
print(' {')
|
|
222
208
|
print(' "mcpServers": {')
|
|
223
209
|
print(' "code-theme-mcp": {')
|
|
@@ -229,9 +215,7 @@ def handle_init():
|
|
|
229
215
|
print(' }\n')
|
|
230
216
|
|
|
231
217
|
print("✔ Once saved, your AI assistant will detect available tools automatically.\n")
|
|
232
|
-
|
|
233
218
|
print('💡 Example: "Update the primary color to deep purple"')
|
|
234
|
-
|
|
235
219
|
print("="*60)
|
|
236
220
|
|
|
237
221
|
except Exception as e:
|
|
@@ -239,6 +223,58 @@ def handle_init():
|
|
|
239
223
|
sys.exit(1)
|
|
240
224
|
|
|
241
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
|
+
|
|
242
278
|
def clean_remote_path(p: str) -> str:
|
|
243
279
|
"""
|
|
244
280
|
Strips the deeply nested remote container path if present.
|
|
@@ -285,7 +321,7 @@ def handle_apply(query: str):
|
|
|
285
321
|
last_workspaces = config.get("workspaces", {})
|
|
286
322
|
workspace_id = last_workspaces.get(repo_key)
|
|
287
323
|
|
|
288
|
-
|
|
324
|
+
|
|
289
325
|
if workspace_id:
|
|
290
326
|
try:
|
|
291
327
|
check = client.call("check_workspace", {"workspace_id": workspace_id})
|
|
@@ -296,7 +332,6 @@ def handle_apply(query: str):
|
|
|
296
332
|
sys.exit(0)
|
|
297
333
|
|
|
298
334
|
if isinstance(check, dict) and check.get("status") == "ok":
|
|
299
|
-
# print(f"✔ Using active workspace: {workspace_id}")
|
|
300
335
|
pass
|
|
301
336
|
else:
|
|
302
337
|
workspace_id = None
|
|
@@ -308,7 +343,6 @@ def handle_apply(query: str):
|
|
|
308
343
|
sys.exit(0)
|
|
309
344
|
workspace_id = None
|
|
310
345
|
|
|
311
|
-
|
|
312
346
|
if not workspace_id:
|
|
313
347
|
print("📦 Zipping and uploading repository (this may take a moment)...")
|
|
314
348
|
upload_result = client.upload_workspace(repo_abs_path, user_email)
|
|
@@ -318,7 +352,7 @@ def handle_apply(query: str):
|
|
|
318
352
|
save_config(config)
|
|
319
353
|
sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
|
|
320
354
|
|
|
321
|
-
|
|
355
|
+
|
|
322
356
|
print("🚀 Analyzing repository...")
|
|
323
357
|
analysis = client.call("repo_analyzer", {
|
|
324
358
|
"repo_path": repo_abs_path,
|
|
@@ -335,7 +369,7 @@ def handle_apply(query: str):
|
|
|
335
369
|
else:
|
|
336
370
|
print(f"✔ {analysis}")
|
|
337
371
|
|
|
338
|
-
|
|
372
|
+
|
|
339
373
|
print("🔍 Detecting local changes...")
|
|
340
374
|
local_changes = sync_manager.get_changed_files(repo_abs_path, user_email)
|
|
341
375
|
if local_changes:
|
|
@@ -388,7 +422,7 @@ def handle_apply(query: str):
|
|
|
388
422
|
return
|
|
389
423
|
print("Please enter 'y' or 'n'.")
|
|
390
424
|
|
|
391
|
-
|
|
425
|
+
|
|
392
426
|
print("⚙ Executing plan...")
|
|
393
427
|
result = client.call("execute_plan", {
|
|
394
428
|
"query": query,
|
|
@@ -405,7 +439,7 @@ def handle_apply(query: str):
|
|
|
405
439
|
print(f"✖ Execution failed: {error_msg}")
|
|
406
440
|
return
|
|
407
441
|
|
|
408
|
-
|
|
442
|
+
|
|
409
443
|
res_data = result
|
|
410
444
|
if isinstance(result, str):
|
|
411
445
|
try: res_data = json.loads(result)
|
|
@@ -414,7 +448,7 @@ def handle_apply(query: str):
|
|
|
414
448
|
updates = res_data.get("updates_for_local", [])
|
|
415
449
|
deletes = res_data.get("deleted_files", [])
|
|
416
450
|
|
|
417
|
-
|
|
451
|
+
|
|
418
452
|
for item in updates:
|
|
419
453
|
rel_path = clean_remote_path(item.get("path"))
|
|
420
454
|
code = item.get("code")
|
|
@@ -429,7 +463,7 @@ def handle_apply(query: str):
|
|
|
429
463
|
except Exception as e:
|
|
430
464
|
print(f"✖ Error writing {rel_path}: {e}")
|
|
431
465
|
|
|
432
|
-
|
|
466
|
+
|
|
433
467
|
for rel_path_raw in (deletes or []):
|
|
434
468
|
rel_path = clean_remote_path(rel_path_raw)
|
|
435
469
|
abs_path = os.path.normpath(os.path.join(repo_abs_path, rel_path.replace('/', os.sep)))
|
|
@@ -440,12 +474,12 @@ def handle_apply(query: str):
|
|
|
440
474
|
print(f"✔ Deleted: {rel_path}")
|
|
441
475
|
except: pass
|
|
442
476
|
|
|
443
|
-
|
|
477
|
+
|
|
444
478
|
if updates or deletes:
|
|
445
479
|
sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
|
|
446
480
|
print("✔ Local sync state updated.")
|
|
447
481
|
|
|
448
|
-
|
|
482
|
+
|
|
449
483
|
_details_raw = res_data.get("details", [])
|
|
450
484
|
details_list = _details_raw.get("report", []) if isinstance(_details_raw, dict) else (_details_raw if isinstance(_details_raw, list) else [])
|
|
451
485
|
if details_list:
|
|
@@ -454,7 +488,7 @@ def handle_apply(query: str):
|
|
|
454
488
|
status_icon = "✔" if item.get("success") else "✖"
|
|
455
489
|
print(f" [{status_icon}] {item.get('file_path')}: {item.get('message', '')}")
|
|
456
490
|
|
|
457
|
-
|
|
491
|
+
|
|
458
492
|
if not updates and not deletes:
|
|
459
493
|
msg = res_data.get('message', 'Changes applied successfully.')
|
|
460
494
|
if isinstance(msg, str) and "AI MUST now sync" in msg:
|
|
@@ -476,7 +510,7 @@ def main():
|
|
|
476
510
|
parser = argparse.ArgumentParser(prog="codedthemes", description="CodedThemes CLI client")
|
|
477
511
|
parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
|
|
478
512
|
|
|
479
|
-
subparsers = parser.add_subparsers(dest="command", required=False)
|
|
513
|
+
subparsers = parser.add_subparsers(dest="command", required=False)
|
|
480
514
|
|
|
481
515
|
|
|
482
516
|
login_parser = subparsers.add_parser("login", help="Login to MCP server")
|
|
@@ -486,9 +520,9 @@ def main():
|
|
|
486
520
|
apply_parser.add_argument("query", help="Description of changes to make")
|
|
487
521
|
|
|
488
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")
|
|
489
524
|
subparsers.add_parser("logout", help="Log out from current device")
|
|
490
525
|
|
|
491
|
-
|
|
492
526
|
args = parser.parse_args()
|
|
493
527
|
|
|
494
528
|
if not args.command:
|
|
@@ -499,12 +533,12 @@ def main():
|
|
|
499
533
|
handle_login(args.server)
|
|
500
534
|
elif args.command == "init":
|
|
501
535
|
handle_init()
|
|
536
|
+
elif args.command == "reinit":
|
|
537
|
+
handle_reinit()
|
|
502
538
|
elif args.command == "apply":
|
|
503
539
|
handle_apply(args.query)
|
|
504
540
|
elif args.command == "logout":
|
|
505
541
|
handle_logout()
|
|
506
542
|
|
|
507
|
-
|
|
508
|
-
|
|
509
543
|
if __name__ == "__main__":
|
|
510
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.21 → 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
|