codedthemes-cli 0.1.19__tar.gz → 0.1.21__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codedthemes-cli
3
- Version: 0.1.19
3
+ Version: 0.1.21
4
4
  Summary: CLI tool for Code Theme and Integration
5
5
  Author: codedthemes
6
6
  Requires-Python: >=3.10
@@ -46,21 +46,15 @@ Log out from the current device to free up a license slot on the server.
46
46
  codedthemes logout
47
47
  ```
48
48
 
49
- ### 3. Initialization
49
+ ### 3. Initialization (Optional)
50
+
50
51
  Initialize your repository to establish a baseline for synchronization. This is recommended for first-time use in a project.
51
52
 
52
53
  ```bash
53
54
  codedthemes init
54
55
  ```
55
56
 
56
- ### 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
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
- ### 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
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,4 +1,4 @@
1
- from .config import load_config, save_config, CONFIG_FILE
1
+ from .config import load_config, save_config
2
2
  from .mcp_client import MCPClient
3
3
  from .repo_utils import detect_repo_root, zip_repo
4
4
  from .patch_utils import apply_patch
@@ -1,17 +1,18 @@
1
1
  import argparse
2
- import json
2
+ import sys
3
3
  import os
4
+ import json
4
5
  import shutil
5
- import socket
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, CONFIG_FILE
11
+ from .config import save_config, load_config, get_device_id
12
12
  from .mcp_client import MCPClient
13
- from .repo_utils import detect_repo_root
13
+ from .repo_utils import detect_repo_root, zip_repo
14
14
  from .sync_manager import SyncManager
15
+ import socket
15
16
 
16
17
 
17
18
  def handle_login(server_url: str = None):
@@ -28,6 +29,7 @@ def handle_login(server_url: str = None):
28
29
  choice = input("Switch account? (y/N): ").strip().lower()
29
30
  if choice not in ['y', 'yes']:
30
31
  print("Login cancelled.")
32
+ print("Please enter your credentials to log in.")
31
33
  return
32
34
  except:
33
35
  pass
@@ -35,7 +37,7 @@ def handle_login(server_url: str = None):
35
37
  email = input("Email: ").strip()
36
38
  license_key = input("License key: ").strip()
37
39
 
38
- # Idempotent check: already logged in as this user?
40
+ # Idempotent check: Are we already logged in as this user?
39
41
  if existing_token:
40
42
  try:
41
43
  decoded = jwt.decode(existing_token, options={"verify_signature": False})
@@ -67,7 +69,7 @@ def handle_login(server_url: str = None):
67
69
  print("✖ Login failed: No access token returned. Please check your credentials.")
68
70
  sys.exit(1)
69
71
 
70
-
72
+ # LAZY CONFIG: Only save if login succeeded
71
73
  save_config({
72
74
  "access_token": token,
73
75
  "server_url": client.server_url,
@@ -112,9 +114,12 @@ def handle_logout():
112
114
  "license_key": license_key,
113
115
  "device_id": device_id
114
116
  })
115
- except Exception:
117
+ except Exception as e:
118
+ # Silently fail if server logout fails, we still want to clear local state
116
119
  pass
117
120
 
121
+ # Local cleanup: Remove config.json but KEEP device.json
122
+ from .config import CONFIG_FILE
118
123
  if CONFIG_FILE.exists():
119
124
  CONFIG_FILE.unlink()
120
125
 
@@ -147,7 +152,12 @@ def handle_init():
147
152
  repo_abs_path = os.path.abspath(repo_root)
148
153
  repo_key = repo_abs_path.lower().replace(os.sep, "/")
149
154
 
155
+ # Check if already initialized to avoid redundant uploads
150
156
  config = load_config()
157
+ existing_ws_id = config.get("workspaces", {}).get(repo_key)
158
+ if existing_ws_id:
159
+ # Fast check: exists?
160
+ pass # We could call a /verify endpoint here, but for now let's just proceed with upload as "sync"
151
161
 
152
162
  print("📦 Zipping and uploading repository (this may take a moment)...")
153
163
  upload_result = client.upload_workspace(repo_abs_path, user_email)
@@ -176,19 +186,23 @@ def handle_init():
176
186
 
177
187
  print("Choose your editor and add the MCP server:\n")
178
188
 
189
+ # ---------------- ANTIGRAVITY ----------------
179
190
  print("🔹 Antigravity")
180
191
  print(" MCP Servers → Manage → Config\n")
192
+
181
193
  print(' {')
182
194
  print(' "mcpServers": {')
183
195
  print(' "code-theme-mcp": {')
184
196
  print(' "type": "sse",')
185
- print(' "serverURL": "https://mcp.codedthemes.com/api/mcp"')
197
+ print(' "serverURL": "https://mcp.codedthemes.com/api/mcp",')
186
198
  print(' }')
187
199
  print(' }')
188
200
  print(' }\n')
189
201
 
202
+ # ---------------- VS CODE ----------------
190
203
  print("🔹 VS Code")
191
204
  print(" Ctrl+Shift+P → MCP: Add MCP Server → http\n")
205
+
192
206
  print(' {')
193
207
  print(' "servers": {')
194
208
  print(' "code-theme-mcp": {')
@@ -200,8 +214,10 @@ def handle_init():
200
214
  print(' "inputs": []')
201
215
  print(' }\n')
202
216
 
217
+ # ---------------- CURSOR ----------------
203
218
  print("🔹 Cursor")
204
219
  print(" Settings → Features → MCP Servers\n")
220
+
205
221
  print(' {')
206
222
  print(' "mcpServers": {')
207
223
  print(' "code-theme-mcp": {')
@@ -213,63 +229,13 @@ def handle_init():
213
229
  print(' }\n')
214
230
 
215
231
  print("✔ Once saved, your AI assistant will detect available tools automatically.\n")
216
- print('💡 Example: "Update the primary color to deep purple"')
217
- print("="*60)
218
-
219
- except Exception as e:
220
- print(f"✖ Error during initialization: {e}")
221
- sys.exit(1)
222
-
223
-
224
- def handle_reinit():
225
- """
226
- Reactivates an evicted workspace by re-uploading the repository to the cloud.
227
- """
228
- try:
229
- repo_root = detect_repo_root()
230
- print(f"🔍 Found repository at {repo_root}")
231
-
232
- client = MCPClient()
233
- if not client.token:
234
- print("✖ Not logged in. Please run 'codedthemes login' first.")
235
- sys.exit(1)
236
-
237
- user_email = "unknown_user"
238
- try:
239
- decoded = jwt.decode(client.token, options={"verify_signature": False})
240
- user_email = decoded.get("email", "unknown_user")
241
- except:
242
- pass
243
232
 
244
- 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
-
255
- config = load_config()
256
- last_workspaces = config.get("workspaces", {})
257
- last_workspaces[repo_key] = workspace_id
258
- config["workspaces"] = last_workspaces
259
- save_config(config)
260
-
261
- sync_manager = SyncManager()
262
- sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
233
+ print('💡 Example: "Update the primary color to deep purple"')
263
234
 
264
- print(f"✔ Workspace reactivated successfully.")
265
- print(f"✔ Workspace ID: {workspace_id}")
266
- print("\n" + "="*60)
267
- print("Your repository is now synced and active in the cloud!")
268
- print("You can now return to your IDE and continue using the AI assistant.")
269
235
  print("="*60)
270
236
 
271
237
  except Exception as e:
272
- print(f"✖ Error during reactivation: {e}")
238
+ print(f"✖ Error during initialization: {e}")
273
239
  sys.exit(1)
274
240
 
275
241
 
@@ -319,7 +285,7 @@ def handle_apply(query: str):
319
285
  last_workspaces = config.get("workspaces", {})
320
286
  workspace_id = last_workspaces.get(repo_key)
321
287
 
322
-
288
+ # Verify or re-initialize workspace
323
289
  if workspace_id:
324
290
  try:
325
291
  check = client.call("check_workspace", {"workspace_id": workspace_id})
@@ -330,6 +296,7 @@ def handle_apply(query: str):
330
296
  sys.exit(0)
331
297
 
332
298
  if isinstance(check, dict) and check.get("status") == "ok":
299
+ # print(f"✔ Using active workspace: {workspace_id}")
333
300
  pass
334
301
  else:
335
302
  workspace_id = None
@@ -341,6 +308,7 @@ def handle_apply(query: str):
341
308
  sys.exit(0)
342
309
  workspace_id = None
343
310
 
311
+
344
312
  if not workspace_id:
345
313
  print("📦 Zipping and uploading repository (this may take a moment)...")
346
314
  upload_result = client.upload_workspace(repo_abs_path, user_email)
@@ -350,7 +318,7 @@ def handle_apply(query: str):
350
318
  save_config(config)
351
319
  sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
352
320
 
353
-
321
+ # 1. Repository Analysis
354
322
  print("🚀 Analyzing repository...")
355
323
  analysis = client.call("repo_analyzer", {
356
324
  "repo_path": repo_abs_path,
@@ -367,7 +335,7 @@ def handle_apply(query: str):
367
335
  else:
368
336
  print(f"✔ {analysis}")
369
337
 
370
-
338
+ # 2. Planning
371
339
  print("🔍 Detecting local changes...")
372
340
  local_changes = sync_manager.get_changed_files(repo_abs_path, user_email)
373
341
  if local_changes:
@@ -420,7 +388,7 @@ def handle_apply(query: str):
420
388
  return
421
389
  print("Please enter 'y' or 'n'.")
422
390
 
423
-
391
+ # 3. Execution
424
392
  print("⚙ Executing plan...")
425
393
  result = client.call("execute_plan", {
426
394
  "query": query,
@@ -437,7 +405,7 @@ def handle_apply(query: str):
437
405
  print(f"✖ Execution failed: {error_msg}")
438
406
  return
439
407
 
440
-
408
+ # 4. Local Patching
441
409
  res_data = result
442
410
  if isinstance(result, str):
443
411
  try: res_data = json.loads(result)
@@ -446,7 +414,7 @@ def handle_apply(query: str):
446
414
  updates = res_data.get("updates_for_local", [])
447
415
  deletes = res_data.get("deleted_files", [])
448
416
 
449
-
417
+ # 1. Apply local updates
450
418
  for item in updates:
451
419
  rel_path = clean_remote_path(item.get("path"))
452
420
  code = item.get("code")
@@ -461,7 +429,7 @@ def handle_apply(query: str):
461
429
  except Exception as e:
462
430
  print(f"✖ Error writing {rel_path}: {e}")
463
431
 
464
-
432
+ # 2. Apply local deletions
465
433
  for rel_path_raw in (deletes or []):
466
434
  rel_path = clean_remote_path(rel_path_raw)
467
435
  abs_path = os.path.normpath(os.path.join(repo_abs_path, rel_path.replace('/', os.sep)))
@@ -472,12 +440,12 @@ def handle_apply(query: str):
472
440
  print(f"✔ Deleted: {rel_path}")
473
441
  except: pass
474
442
 
475
-
443
+ # 3. Synchronize state
476
444
  if updates or deletes:
477
445
  sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
478
446
  print("✔ Local sync state updated.")
479
447
 
480
-
448
+ # 4. Show Integration Report (Always show errors or details)
481
449
  _details_raw = res_data.get("details", [])
482
450
  details_list = _details_raw.get("report", []) if isinstance(_details_raw, dict) else (_details_raw if isinstance(_details_raw, list) else [])
483
451
  if details_list:
@@ -486,7 +454,7 @@ def handle_apply(query: str):
486
454
  status_icon = "✔" if item.get("success") else "✖"
487
455
  print(f" [{status_icon}] {item.get('file_path')}: {item.get('message', '')}")
488
456
 
489
-
457
+ # 5. Output Final Message if no updates were applied
490
458
  if not updates and not deletes:
491
459
  msg = res_data.get('message', 'Changes applied successfully.')
492
460
  if isinstance(msg, str) and "AI MUST now sync" in msg:
@@ -508,7 +476,7 @@ def main():
508
476
  parser = argparse.ArgumentParser(prog="codedthemes", description="CodedThemes CLI client")
509
477
  parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
510
478
 
511
- subparsers = parser.add_subparsers(dest="command", required=False)
479
+ subparsers = parser.add_subparsers(dest="command", required=False) # Changed to False to allow --version to work alone
512
480
 
513
481
 
514
482
  login_parser = subparsers.add_parser("login", help="Login to MCP server")
@@ -518,9 +486,9 @@ def main():
518
486
  apply_parser.add_argument("query", help="Description of changes to make")
519
487
 
520
488
  subparsers.add_parser("init", help="Initialize repository and sync to cloud")
521
- subparsers.add_parser("reinit", help="Reactivate an evicted workspace and sync to cloud")
522
489
  subparsers.add_parser("logout", help="Log out from current device")
523
490
 
491
+
524
492
  args = parser.parse_args()
525
493
 
526
494
  if not args.command:
@@ -531,12 +499,12 @@ def main():
531
499
  handle_login(args.server)
532
500
  elif args.command == "init":
533
501
  handle_init()
534
- elif args.command == "reinit":
535
- handle_reinit()
536
502
  elif args.command == "apply":
537
503
  handle_apply(args.query)
538
504
  elif args.command == "logout":
539
505
  handle_logout()
540
506
 
507
+
508
+
541
509
  if __name__ == "__main__":
542
510
  main()
@@ -4,17 +4,22 @@ import hashlib
4
4
  import uuid
5
5
  from pathlib import Path
6
6
 
7
+ # Stores token at ~/.codedthemes/config.json
7
8
  CONFIG_DIR = Path.home() / ".codedthemes"
8
9
  CONFIG_FILE = CONFIG_DIR / "config.json"
9
10
 
10
11
 
11
12
  def ensure_config_dir():
12
- """Ensures the configuration directory exists."""
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
- """Saves configuration data to the config file (merging with existing)."""
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
- """Returns a unique device ID, stored in ~/.codedthemes/device.json."""
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
- """Loads configuration data from the config file."""
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
- """Client for interacting with the CodedThemes MCP Server."""
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
- """Zips and uploads the repository to create a new workspace."""
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
- """Calls a remote MCP tool or the login/logout endpoint."""
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,
@@ -2,7 +2,7 @@ import subprocess
2
2
 
3
3
 
4
4
  def apply_patch(patch_text: str):
5
- """Applies a git patch from stdin."""
5
+ # Using 'git apply -' to apply from stdin
6
6
  process = subprocess.Popen(
7
7
  ["git", "apply", "-"],
8
8
  stdin=subprocess.PIPE,
@@ -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
- """Reads .gitignore at the repo root and returns a list of glob patterns."""
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
- """Checks if a relative path matches any .gitignore pattern."""
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
- """Manages local file hashes to track changes and maintain synchronization state."""
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
- """Loads the workspace synchronization state."""
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
- """Saves the workspace synchronization state."""
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
- """Computes MD5 hashes for all relevant files in the repository."""
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
- """Updates the synchronization state with the latest file hashes."""
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
- """Identifies files modified or deleted locally since the last sync."""
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.19
3
+ Version: 0.1.21
4
4
  Summary: CLI tool for Code Theme and Integration
5
5
  Author: codedthemes
6
6
  Requires-Python: >=3.10
@@ -46,21 +46,15 @@ Log out from the current device to free up a license slot on the server.
46
46
  codedthemes logout
47
47
  ```
48
48
 
49
- ### 3. Initialization
49
+ ### 3. Initialization (Optional)
50
+
50
51
  Initialize your repository to establish a baseline for synchronization. This is recommended for first-time use in a project.
51
52
 
52
53
  ```bash
53
54
  codedthemes init
54
55
  ```
55
56
 
56
- ### 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
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codedthemes-cli"
7
- version = "0.1.19"
7
+ version = "0.1.21"
8
8
  description = "CLI tool for Code Theme and Integration"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"