codedthemes-cli 0.1.16__tar.gz → 0.1.18__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.16
3
+ Version: 0.1.18
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 (Optional)
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
- ### 3. Applying Changes
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 (Optional)
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
- ### 3. Applying Changes
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,4 +1,4 @@
1
- from .config import load_config, save_config
1
+ from .config import load_config, save_config, CONFIG_FILE
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,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
- from getpass import getpass
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, zip_repo
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: Are we already logged in as this user?
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
- # LAZY CONFIG: Only save if login succeeded
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 as e:
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,10 +177,8 @@ 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": {')
@@ -200,10 +189,8 @@ def handle_init():
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
- # Verify or re-initialize workspace
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
- # 1. Repository Analysis
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
- # 2. Planning
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
- # 3. Execution
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
- # 4. Local Patching
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
- # 1. Apply local updates
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
- # 2. Apply local deletions
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
- # 3. Synchronize state
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
- # 4. Show Integration Report (Always show errors or details)
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
- # 5. Output Final Message if no updates were applied
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) # Changed to False to allow --version to work alone
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
- Client for interacting with the CodedThemes MCP Server.
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,
@@ -2,7 +2,7 @@ import subprocess
2
2
 
3
3
 
4
4
  def apply_patch(patch_text: str):
5
- # Using 'git apply -' to apply from stdin
5
+ """Applies a git patch from stdin."""
6
6
  process = subprocess.Popen(
7
7
  ["git", "apply", "-"],
8
8
  stdin=subprocess.PIPE,
@@ -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) to avoid
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(".") # skip all hidden dirs (e.g. .env, .husky)
147
- or d in {".github"} # but keep .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
- Manages local file hashes to track manual changes and maintain synchronization state.
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.16
3
+ Version: 0.1.18
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 (Optional)
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
- ### 3. Applying Changes
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
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codedthemes-cli"
7
- version = "0.1.16"
7
+ version = "0.1.18"
8
8
  description = "CLI tool for Code Theme and Integration"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"