codedthemes-cli 0.1.18__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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: codedthemes-cli
3
- Version: 0.1.18
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
- ### 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):
@@ -36,7 +37,7 @@ def handle_login(server_url: str = None):
36
37
  email = input("Email: ").strip()
37
38
  license_key = input("License key: ").strip()
38
39
 
39
- # Idempotent check: already logged in as this user?
40
+ # Idempotent check: Are we already logged in as this user?
40
41
  if existing_token:
41
42
  try:
42
43
  decoded = jwt.decode(existing_token, options={"verify_signature": False})
@@ -68,7 +69,7 @@ def handle_login(server_url: str = None):
68
69
  print("✖ Login failed: No access token returned. Please check your credentials.")
69
70
  sys.exit(1)
70
71
 
71
-
72
+ # LAZY CONFIG: Only save if login succeeded
72
73
  save_config({
73
74
  "access_token": token,
74
75
  "server_url": client.server_url,
@@ -113,9 +114,12 @@ def handle_logout():
113
114
  "license_key": license_key,
114
115
  "device_id": device_id
115
116
  })
116
- except Exception:
117
+ except Exception as e:
118
+ # Silently fail if server logout fails, we still want to clear local state
117
119
  pass
118
120
 
121
+ # Local cleanup: Remove config.json but KEEP device.json
122
+ from .config import CONFIG_FILE
119
123
  if CONFIG_FILE.exists():
120
124
  CONFIG_FILE.unlink()
121
125
 
@@ -148,7 +152,12 @@ def handle_init():
148
152
  repo_abs_path = os.path.abspath(repo_root)
149
153
  repo_key = repo_abs_path.lower().replace(os.sep, "/")
150
154
 
155
+ # Check if already initialized to avoid redundant uploads
151
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"
152
161
 
153
162
  print("📦 Zipping and uploading repository (this may take a moment)...")
154
163
  upload_result = client.upload_workspace(repo_abs_path, user_email)
@@ -177,8 +186,10 @@ def handle_init():
177
186
 
178
187
  print("Choose your editor and add the MCP server:\n")
179
188
 
189
+ # ---------------- ANTIGRAVITY ----------------
180
190
  print("🔹 Antigravity")
181
191
  print(" MCP Servers → Manage → Config\n")
192
+
182
193
  print(' {')
183
194
  print(' "mcpServers": {')
184
195
  print(' "code-theme-mcp": {')
@@ -189,8 +200,10 @@ def handle_init():
189
200
  print(' }')
190
201
  print(' }\n')
191
202
 
203
+ # ---------------- VS CODE ----------------
192
204
  print("🔹 VS Code")
193
205
  print(" Ctrl+Shift+P → MCP: Add MCP Server → http\n")
206
+
194
207
  print(' {')
195
208
  print(' "servers": {')
196
209
  print(' "code-theme-mcp": {')
@@ -202,8 +215,10 @@ def handle_init():
202
215
  print(' "inputs": []')
203
216
  print(' }\n')
204
217
 
218
+ # ---------------- CURSOR ----------------
205
219
  print("🔹 Cursor")
206
220
  print(" Settings → Features → MCP Servers\n")
221
+
207
222
  print(' {')
208
223
  print(' "mcpServers": {')
209
224
  print(' "code-theme-mcp": {')
@@ -215,63 +230,13 @@ def handle_init():
215
230
  print(' }\n')
216
231
 
217
232
  print("✔ Once saved, your AI assistant will detect available tools automatically.\n")
218
- print('💡 Example: "Update the primary color to deep purple"')
219
- print("="*60)
220
-
221
- except Exception as e:
222
- print(f"✖ Error during initialization: {e}")
223
- sys.exit(1)
224
-
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
233
 
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)
234
+ print('💡 Example: "Update the primary color to deep purple"')
265
235
 
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
236
  print("="*60)
272
237
 
273
238
  except Exception as e:
274
- print(f"✖ Error during reactivation: {e}")
239
+ print(f"✖ Error during initialization: {e}")
275
240
  sys.exit(1)
276
241
 
277
242
 
@@ -321,7 +286,7 @@ def handle_apply(query: str):
321
286
  last_workspaces = config.get("workspaces", {})
322
287
  workspace_id = last_workspaces.get(repo_key)
323
288
 
324
-
289
+ # Verify or re-initialize workspace
325
290
  if workspace_id:
326
291
  try:
327
292
  check = client.call("check_workspace", {"workspace_id": workspace_id})
@@ -332,6 +297,7 @@ def handle_apply(query: str):
332
297
  sys.exit(0)
333
298
 
334
299
  if isinstance(check, dict) and check.get("status") == "ok":
300
+ # print(f"✔ Using active workspace: {workspace_id}")
335
301
  pass
336
302
  else:
337
303
  workspace_id = None
@@ -343,6 +309,7 @@ def handle_apply(query: str):
343
309
  sys.exit(0)
344
310
  workspace_id = None
345
311
 
312
+
346
313
  if not workspace_id:
347
314
  print("📦 Zipping and uploading repository (this may take a moment)...")
348
315
  upload_result = client.upload_workspace(repo_abs_path, user_email)
@@ -352,7 +319,7 @@ def handle_apply(query: str):
352
319
  save_config(config)
353
320
  sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
354
321
 
355
-
322
+ # 1. Repository Analysis
356
323
  print("🚀 Analyzing repository...")
357
324
  analysis = client.call("repo_analyzer", {
358
325
  "repo_path": repo_abs_path,
@@ -369,7 +336,7 @@ def handle_apply(query: str):
369
336
  else:
370
337
  print(f"✔ {analysis}")
371
338
 
372
-
339
+ # 2. Planning
373
340
  print("🔍 Detecting local changes...")
374
341
  local_changes = sync_manager.get_changed_files(repo_abs_path, user_email)
375
342
  if local_changes:
@@ -422,7 +389,7 @@ def handle_apply(query: str):
422
389
  return
423
390
  print("Please enter 'y' or 'n'.")
424
391
 
425
-
392
+ # 3. Execution
426
393
  print("⚙ Executing plan...")
427
394
  result = client.call("execute_plan", {
428
395
  "query": query,
@@ -439,7 +406,7 @@ def handle_apply(query: str):
439
406
  print(f"✖ Execution failed: {error_msg}")
440
407
  return
441
408
 
442
-
409
+ # 4. Local Patching
443
410
  res_data = result
444
411
  if isinstance(result, str):
445
412
  try: res_data = json.loads(result)
@@ -448,7 +415,7 @@ def handle_apply(query: str):
448
415
  updates = res_data.get("updates_for_local", [])
449
416
  deletes = res_data.get("deleted_files", [])
450
417
 
451
-
418
+ # 1. Apply local updates
452
419
  for item in updates:
453
420
  rel_path = clean_remote_path(item.get("path"))
454
421
  code = item.get("code")
@@ -463,7 +430,7 @@ def handle_apply(query: str):
463
430
  except Exception as e:
464
431
  print(f"✖ Error writing {rel_path}: {e}")
465
432
 
466
-
433
+ # 2. Apply local deletions
467
434
  for rel_path_raw in (deletes or []):
468
435
  rel_path = clean_remote_path(rel_path_raw)
469
436
  abs_path = os.path.normpath(os.path.join(repo_abs_path, rel_path.replace('/', os.sep)))
@@ -474,12 +441,12 @@ def handle_apply(query: str):
474
441
  print(f"✔ Deleted: {rel_path}")
475
442
  except: pass
476
443
 
477
-
444
+ # 3. Synchronize state
478
445
  if updates or deletes:
479
446
  sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
480
447
  print("✔ Local sync state updated.")
481
448
 
482
-
449
+ # 4. Show Integration Report (Always show errors or details)
483
450
  _details_raw = res_data.get("details", [])
484
451
  details_list = _details_raw.get("report", []) if isinstance(_details_raw, dict) else (_details_raw if isinstance(_details_raw, list) else [])
485
452
  if details_list:
@@ -488,7 +455,7 @@ def handle_apply(query: str):
488
455
  status_icon = "✔" if item.get("success") else "✖"
489
456
  print(f" [{status_icon}] {item.get('file_path')}: {item.get('message', '')}")
490
457
 
491
-
458
+ # 5. Output Final Message if no updates were applied
492
459
  if not updates and not deletes:
493
460
  msg = res_data.get('message', 'Changes applied successfully.')
494
461
  if isinstance(msg, str) and "AI MUST now sync" in msg:
@@ -510,7 +477,7 @@ def main():
510
477
  parser = argparse.ArgumentParser(prog="codedthemes", description="CodedThemes CLI client")
511
478
  parser.add_argument("-v", "--version", action="version", version=f"%(prog)s {__version__}")
512
479
 
513
- 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
514
481
 
515
482
 
516
483
  login_parser = subparsers.add_parser("login", help="Login to MCP server")
@@ -520,9 +487,9 @@ def main():
520
487
  apply_parser.add_argument("query", help="Description of changes to make")
521
488
 
522
489
  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")
524
490
  subparsers.add_parser("logout", help="Log out from current device")
525
491
 
492
+
526
493
  args = parser.parse_args()
527
494
 
528
495
  if not args.command:
@@ -533,12 +500,12 @@ def main():
533
500
  handle_login(args.server)
534
501
  elif args.command == "init":
535
502
  handle_init()
536
- elif args.command == "reinit":
537
- handle_reinit()
538
503
  elif args.command == "apply":
539
504
  handle_apply(args.query)
540
505
  elif args.command == "logout":
541
506
  handle_logout()
542
507
 
508
+
509
+
543
510
  if __name__ == "__main__":
544
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
- """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.18
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
- ### 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.18"
7
+ version = "0.1.20"
8
8
  description = "CLI tool for Code Theme and Integration"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"