codedthemes-cli 0.1.0__tar.gz → 0.1.2__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.
Files changed (21) hide show
  1. codedthemes_cli-0.1.2/PKG-INFO +63 -0
  2. codedthemes_cli-0.1.2/README.md +53 -0
  3. {codedthemes_cli-0.1.0 → codedthemes_cli-0.1.2}/codedthemes/cli.py +48 -91
  4. {codedthemes_cli-0.1.0 → codedthemes_cli-0.1.2}/codedthemes/config.py +9 -0
  5. {codedthemes_cli-0.1.0 → codedthemes_cli-0.1.2}/codedthemes/mcp_client.py +20 -2
  6. codedthemes_cli-0.1.2/codedthemes/repo_utils.py +190 -0
  7. {codedthemes_cli-0.1.0 → codedthemes_cli-0.1.2}/codedthemes/sync_manager.py +18 -3
  8. codedthemes_cli-0.1.2/codedthemes_cli.egg-info/PKG-INFO +63 -0
  9. {codedthemes_cli-0.1.0 → codedthemes_cli-0.1.2}/pyproject.toml +1 -1
  10. codedthemes_cli-0.1.0/PKG-INFO +0 -49
  11. codedthemes_cli-0.1.0/README.md +0 -39
  12. codedthemes_cli-0.1.0/codedthemes/repo_utils.py +0 -40
  13. codedthemes_cli-0.1.0/codedthemes_cli.egg-info/PKG-INFO +0 -49
  14. {codedthemes_cli-0.1.0 → codedthemes_cli-0.1.2}/codedthemes/__init__.py +0 -0
  15. {codedthemes_cli-0.1.0 → codedthemes_cli-0.1.2}/codedthemes/patch_utils.py +0 -0
  16. {codedthemes_cli-0.1.0 → codedthemes_cli-0.1.2}/codedthemes_cli.egg-info/SOURCES.txt +0 -0
  17. {codedthemes_cli-0.1.0 → codedthemes_cli-0.1.2}/codedthemes_cli.egg-info/dependency_links.txt +0 -0
  18. {codedthemes_cli-0.1.0 → codedthemes_cli-0.1.2}/codedthemes_cli.egg-info/entry_points.txt +0 -0
  19. {codedthemes_cli-0.1.0 → codedthemes_cli-0.1.2}/codedthemes_cli.egg-info/requires.txt +0 -0
  20. {codedthemes_cli-0.1.0 → codedthemes_cli-0.1.2}/codedthemes_cli.egg-info/top_level.txt +0 -0
  21. {codedthemes_cli-0.1.0 → codedthemes_cli-0.1.2}/setup.cfg +0 -0
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: codedthemes-cli
3
+ Version: 0.1.2
4
+ Summary: CLI tool for Code Theme and Integration
5
+ Author: codedthemes
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: requests
9
+ Requires-Dist: pyjwt
10
+
11
+ # CodedThemes CLI
12
+
13
+ CodedThemes CLI is a powerful command-line tool designed to integrate your local development environment with the CodedThemes MCP Server. It allows you to automate UI/UX modifications, refactor themes, and apply stylistic changes across your entire repository using AI-driven insights.
14
+
15
+ ## Features
16
+
17
+ - **Automated Theming**: Apply complex theme changes (e.g., branding updates) with simple natural language queries.
18
+ - **Local-to-Cloud Sync**: Securely sync your repository state to the CodedThemes engine for deep analysis.
19
+ - **Seamless Integration**: Automatically patches local files with surgical precision.
20
+ - **Sync Management**: Tracks local changes to ensure your manual edits are never overwritten.
21
+
22
+ ## Installation
23
+
24
+ Install the CLI directly from PyPI:
25
+
26
+ ```bash
27
+ pip install codedthemes-cli
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ### 1. Authentication
33
+
34
+ Authenticate with your CodedThemes account to enable secure communication with the MCP server.
35
+
36
+ ```bash
37
+ codedthemes login
38
+ ```
39
+ *Enter your registered email and license key when prompted.*
40
+
41
+ ### 2. Initialization (Optional)
42
+
43
+ Initialize your repository to establish a baseline for synchronization. This is recommended for first-time use in a project.
44
+
45
+ ```bash
46
+ codedthemes init
47
+ ```
48
+
49
+ ### 3. Applying Changes
50
+
51
+ 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.
52
+
53
+ ```bash
54
+ codedthemes apply "Update the theme branding from Mantis to Berry"
55
+ ```
56
+
57
+ ## Integration Report
58
+
59
+ After every successful `apply`, a granular report is generated showing exactly which files were modified, created, or deleted.
60
+
61
+ ## Support
62
+
63
+ For issues or feature requests, please contact the CodedThemes support team.
@@ -0,0 +1,53 @@
1
+ # CodedThemes CLI
2
+
3
+ CodedThemes CLI is a powerful command-line tool designed to integrate your local development environment with the CodedThemes MCP Server. It allows you to automate UI/UX modifications, refactor themes, and apply stylistic changes across your entire repository using AI-driven insights.
4
+
5
+ ## Features
6
+
7
+ - **Automated Theming**: Apply complex theme changes (e.g., branding updates) with simple natural language queries.
8
+ - **Local-to-Cloud Sync**: Securely sync your repository state to the CodedThemes engine for deep analysis.
9
+ - **Seamless Integration**: Automatically patches local files with surgical precision.
10
+ - **Sync Management**: Tracks local changes to ensure your manual edits are never overwritten.
11
+
12
+ ## Installation
13
+
14
+ Install the CLI directly from PyPI:
15
+
16
+ ```bash
17
+ pip install codedthemes-cli
18
+ ```
19
+
20
+ ## Usage
21
+
22
+ ### 1. Authentication
23
+
24
+ Authenticate with your CodedThemes account to enable secure communication with the MCP server.
25
+
26
+ ```bash
27
+ codedthemes login
28
+ ```
29
+ *Enter your registered email and license key when prompted.*
30
+
31
+ ### 2. Initialization (Optional)
32
+
33
+ Initialize your repository to establish a baseline for synchronization. This is recommended for first-time use in a project.
34
+
35
+ ```bash
36
+ codedthemes init
37
+ ```
38
+
39
+ ### 3. Applying Changes
40
+
41
+ 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.
42
+
43
+ ```bash
44
+ codedthemes apply "Update the theme branding from Mantis to Berry"
45
+ ```
46
+
47
+ ## Integration Report
48
+
49
+ After every successful `apply`, a granular report is generated showing exactly which files were modified, created, or deleted.
50
+
51
+ ## Support
52
+
53
+ For issues or feature requests, please contact the CodedThemes support team.
@@ -1,20 +1,23 @@
1
1
  import argparse
2
2
  import sys
3
3
  import os
4
- import jwt
5
- import requests
6
4
  import json
7
5
  import shutil
8
6
  from getpass import getpass
9
7
 
8
+ import jwt
9
+ import requests
10
+
10
11
  from .config import save_config, load_config
11
12
  from .mcp_client import MCPClient
12
13
  from .repo_utils import detect_repo_root, zip_repo
13
- from .patch_utils import apply_patch
14
14
  from .sync_manager import SyncManager
15
15
 
16
16
 
17
17
  def handle_login(server_url: str = None):
18
+ """
19
+ Handles user authentication with the MCP server.
20
+ """
18
21
  email = input("Email: ").strip()
19
22
  license_key = input("License key: ").strip()
20
23
 
@@ -28,33 +31,28 @@ def handle_login(server_url: str = None):
28
31
  "license_key": license_key
29
32
  })
30
33
 
31
-
32
34
  if isinstance(result, dict):
33
- # The server might have returned {"status": "error", "message": ...}
34
35
  if result.get("status") == "error":
35
36
  print(f"✖ Login failed: {result.get('message', 'Unknown error')}")
36
37
  sys.exit(1)
37
- # The app.py bridge wraps string returns in {"message": "..."}
38
38
  elif "message" in result and "Login failed" in result["message"]:
39
39
  print(f"✖ {result['message']}")
40
40
  sys.exit(1)
41
41
 
42
- # 1. Try to get token directly from API response (if server returns JSON)
42
+ # Get token from result or local fallback
43
43
  token = result.get("access_token") if isinstance(result, dict) else None
44
44
 
45
- # 2. Fallback: Try to read from local file (because server now returns plain text string)
46
45
  if not token:
47
46
  token_file = os.path.expanduser("~/.mcp_token")
48
47
  if os.path.exists(token_file):
49
48
  with open(token_file, 'r') as f:
50
49
  token = f.read().strip()
51
50
 
52
- # 3. Final Verification: Throw error if we STILL don't have a token
53
51
  if not token:
54
52
  print("✖ Login failed: No access token returned. Please check your credentials.")
55
53
  sys.exit(1)
56
54
 
57
- # Write the .mcp_token file natively for the user so it acts just like local MCP
55
+ # Store token for both local MCP and CLI config
58
56
  token_file = os.path.expanduser("~/.mcp_token")
59
57
  try:
60
58
  with open(token_file, 'w') as f:
@@ -78,6 +76,9 @@ def handle_login(server_url: str = None):
78
76
 
79
77
 
80
78
  def handle_init():
79
+ """
80
+ Initializes the local repository and syncs it with the cloud.
81
+ """
81
82
  try:
82
83
  repo_root = detect_repo_root()
83
84
  print(f"🔍 Found repository at {repo_root}")
@@ -87,7 +88,6 @@ def handle_init():
87
88
  print("✖ Not logged in. Please run 'codedthemes login' first.")
88
89
  sys.exit(1)
89
90
 
90
- # 0. Get user email from token for registry profiling
91
91
  user_email = "unknown_user"
92
92
  try:
93
93
  decoded = jwt.decode(client.token, options={"verify_signature": False})
@@ -104,14 +104,12 @@ def handle_init():
104
104
  print("✖ Upload failed: No workspace ID returned.")
105
105
  sys.exit(1)
106
106
 
107
- # 1. Save workspace mapping in config.json
108
107
  config = load_config()
109
108
  last_workspaces = config.get("workspaces", {})
110
109
  last_workspaces[repo_abs_path] = workspace_id
111
110
  config["workspaces"] = last_workspaces
112
111
  save_config(config)
113
112
 
114
- # 2. Initialize sync state (HASHES) in workspace.json
115
113
  sync_manager = SyncManager()
116
114
  sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
117
115
 
@@ -125,16 +123,15 @@ def handle_init():
125
123
 
126
124
 
127
125
  def handle_apply(query: str):
126
+ """
127
+ Plans and executes changes based on a natural language query.
128
+ """
128
129
  try:
129
130
  repo_root = detect_repo_root()
131
+ repo_abs_path = str(repo_root)
130
132
  print(f"🔍 Found repository at {repo_root}")
131
133
 
132
- # Note: zipping logic is there but we are calling tools sequentially as per requirements
133
- # zip_path = zip_repo(repo_root)
134
-
135
134
  client = MCPClient()
136
-
137
- # 0. Get user email from token for registry profiling
138
135
  user_email = "unknown_user"
139
136
  if client.token:
140
137
  try:
@@ -144,17 +141,14 @@ def handle_apply(query: str):
144
141
  pass
145
142
 
146
143
  sync_manager = SyncManager()
147
-
148
144
  config = load_config()
149
145
  last_workspaces = config.get("workspaces", {})
150
- repo_abs_path = str(repo_root)
151
146
  workspace_id = last_workspaces.get(repo_abs_path)
152
147
 
153
- # 1. Zip and Upload Repository (Only if not already initialized or expired)
148
+ # Verify or re-initialize workspace
154
149
  if workspace_id:
155
150
  logger_debug = False # toggle for debug
156
151
  try:
157
- # Quick check if workspace is still on server
158
152
  check = client.call("check_workspace", {"workspace_id": workspace_id})
159
153
  if isinstance(check, dict) and check.get("status") == "ok":
160
154
  print(f"✔ Using active workspace: {workspace_id}")
@@ -167,16 +161,12 @@ def handle_apply(query: str):
167
161
  print("📦 Zipping and uploading repository (this may take a moment)...")
168
162
  upload_result = client.upload_workspace(repo_abs_path, user_email)
169
163
  workspace_id = upload_result.get("workspace_id")
170
-
171
- # Save workspace_id for future reuse
172
164
  last_workspaces[repo_abs_path] = workspace_id
173
165
  config["workspaces"] = last_workspaces
174
166
  save_config(config)
175
-
176
- # Init sync state locally
177
167
  sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
178
168
 
179
- # Step 1: repo analyzer
169
+ # 1. Repository Analysis
180
170
  print("🚀 Analyzing repository...")
181
171
  analysis = client.call("repo_analyzer", {
182
172
  "repo_path": repo_abs_path,
@@ -188,13 +178,12 @@ def handle_apply(query: str):
188
178
  return
189
179
 
190
180
  if isinstance(analysis, str):
191
- # The tool might return a string message shown in CLI
192
181
  if "Analysis Skipped" in analysis:
193
182
  print("✔ Repository is up-to-date. Skipping upload.")
194
183
  else:
195
184
  print(f"✔ {analysis}")
196
185
 
197
- # Step 2: plan changes
186
+ # 2. Planning
198
187
  print("🔍 Detecting local changes...")
199
188
  local_changes = sync_manager.get_changed_files(repo_abs_path, user_email)
200
189
  if local_changes:
@@ -204,7 +193,7 @@ def handle_apply(query: str):
204
193
  plan = client.call("plan_changes", {
205
194
  "query": query,
206
195
  "workspace_id": workspace_id,
207
- "repo_path": str(repo_root),
196
+ "repo_path": repo_abs_path,
208
197
  "changes_from_cli": local_changes
209
198
  })
210
199
 
@@ -213,54 +202,50 @@ def handle_apply(query: str):
213
202
  return
214
203
 
215
204
  target_files = plan.get("files_to_modify", [])
216
- plan_instructions = plan.get("plan_instructions", [])
205
+ instructions = plan.get("plan_instructions", [])
217
206
  files_to_create = plan.get("files_to_create", [])
218
207
  files_to_delete = plan.get("files_to_delete", [])
219
208
 
220
- if not target_files and not files_to_create and not files_to_delete and not plan_instructions:
209
+ if not any([target_files, files_to_create, files_to_delete, instructions]):
221
210
  print("Information: No changes planned.")
222
211
  return
223
212
 
224
213
  print("\n📝 Execution Plan:")
225
- if plan_instructions:
214
+ if instructions:
226
215
  print("Instructions:")
227
- for idx, inst in enumerate(plan_instructions, 1):
216
+ for idx, inst in enumerate(instructions, 1):
228
217
  print(f" {idx}. {inst}")
229
218
 
230
219
  if files_to_create:
231
220
  print("\nFiles to create:")
232
- for f in files_to_create:
233
- print(f" + {f}")
234
-
221
+ for f in files_to_create: print(f" + {f}")
222
+
223
+ if target_files:
235
224
  print("\nFiles to modify:")
236
- for f in target_files:
237
- print(f" ~ {f}")
225
+ for f in target_files: print(f" ~ {f}")
238
226
 
239
227
  if files_to_delete:
240
228
  print("\nFiles to delete:")
241
- for f in files_to_delete:
242
- print(f" - {f}")
229
+ for f in files_to_delete: print(f" - {f}")
243
230
 
244
231
  while True:
245
232
  choice = input("\nDo you want to proceed with this plan? (y/n): ").strip().lower()
246
- if choice in ['y', 'yes']:
247
- break
248
- elif choice in ['n', 'no']:
249
- print("✖ Pipeline terminated by user. Please provide a new query.")
233
+ if choice in ['y', 'yes']: break
234
+ if choice in ['n', 'no']:
235
+ print("✖ Pipeline terminated by user.")
250
236
  return
251
- else:
252
- print("Please enter 'y' for yes or 'n' for no.")
237
+ print("Please enter 'y' or 'n'.")
253
238
 
254
- # Step 3: execute plan
239
+ # 3. Execution
255
240
  print("⚙ Executing plan...")
256
241
  result = client.call("execute_plan", {
257
242
  "query": query,
258
- "plan_instructions": plan_instructions,
243
+ "plan_instructions": instructions,
259
244
  "files_to_modify": target_files,
260
245
  "files_to_create": files_to_create,
261
246
  "files_to_delete": files_to_delete,
262
247
  "workspace_id": workspace_id,
263
- "repo_path": str(repo_root)
248
+ "repo_path": repo_abs_path
264
249
  })
265
250
 
266
251
  if not result or (isinstance(result, dict) and (result.get("status") == "error" or "detail" in result)):
@@ -268,8 +253,7 @@ def handle_apply(query: str):
268
253
  print(f"✖ Execution failed: {error_msg}")
269
254
  return
270
255
 
271
- # --- LOCAL PATCHING LOGIC (WINDOWS) ---
272
- # Ensure we are looking at a dictionary
256
+ # 4. Local Patching
273
257
  res_data = result
274
258
  if isinstance(result, str):
275
259
  try: res_data = json.loads(result)
@@ -278,67 +262,42 @@ def handle_apply(query: str):
278
262
  updates = res_data.get("updates_for_local", [])
279
263
  deletes = res_data.get("deleted_files", [])
280
264
 
281
- if not updates:
282
- print(" (!) No file updates received in the response payload.")
283
-
284
265
  if updates or deletes:
285
- # 1. Update/Create files
286
266
  for item in updates:
287
267
  rel_path, code = item.get("path"), item.get("code")
288
268
  if not rel_path or code is None: continue
289
-
290
- # Normalize path for Windows
291
- clean_rel_path = rel_path.replace('/', os.sep)
292
- abs_path = os.path.abspath(os.path.join(str(repo_root), clean_rel_path))
269
+ abs_path = os.path.abspath(os.path.join(repo_abs_path, rel_path.replace('/', os.sep)))
293
270
 
294
271
  try:
295
272
  os.makedirs(os.path.dirname(abs_path), exist_ok=True)
296
273
  with open(abs_path, 'w', encoding='utf-8') as f:
297
274
  f.write(code)
298
- print(f"✔ Updated local file: {rel_path}")
299
- except Exception as write_err:
300
- print(f"✖ Error writing {rel_path}: {write_err}")
275
+ print(f"✔ Updated: {rel_path}")
276
+ except Exception as e:
277
+ print(f"✖ Error writing {rel_path}: {e}")
301
278
 
302
- # 2. Delete files
303
279
  for rel_path in (deletes or []):
304
- clean_rel_path = rel_path.replace('/', os.sep)
305
- abs_path = os.path.normpath(os.path.join(str(repo_root), clean_rel_path))
306
- if os.path.exists(abs_path) and abs_path.startswith(os.path.abspath(str(repo_root))):
280
+ abs_path = os.path.normpath(os.path.join(repo_abs_path, rel_path.replace('/', os.sep)))
281
+ if os.path.exists(abs_path) and abs_path.startswith(os.path.abspath(repo_abs_path)):
307
282
  try:
308
283
  if os.path.isfile(abs_path): os.remove(abs_path)
309
284
  elif os.path.isdir(abs_path): shutil.rmtree(abs_path)
310
- print(f"✔ Deleted local file/folder: {rel_path}")
285
+ print(f"✔ Deleted: {rel_path}")
311
286
  except: pass
312
287
 
313
- # Finally, update the sync state so these aren't treated as local user changes next time
314
288
  sync_manager.update_sync_state(repo_abs_path, workspace_id, user_email)
315
289
  print("✔ Local sync state updated.")
316
290
 
317
- # Display granular report details if available
318
291
  _details_raw = res_data.get("details", [])
319
292
  details_list = _details_raw.get("report", []) if isinstance(_details_raw, dict) else (_details_raw if isinstance(_details_raw, list) else [])
320
293
  if details_list:
321
294
  print("\n📊 Integration Report:")
322
295
  for item in details_list:
323
296
  status_icon = "✔" if item.get("success") else "✖"
324
- f_name = item.get("file_path")
325
- msg = item.get("message", "")
326
- print(f" [{status_icon}] {f_name}: {msg}")
297
+ print(f" [{status_icon}] {item.get('file_path')}: {item.get('message', '')}")
327
298
  else:
328
- # Fallback for display
329
- msg = res_data.get("message", "Changes applied successfully (no local updates needed).")
330
- print(f"✔ {msg}")
299
+ print(f"✔ {res_data.get('message', 'Changes applied successfully.')}")
331
300
 
332
- # If there was a report but no updates (already up to date), display why
333
- _details_raw = res_data.get("details", [])
334
- details_list = _details_raw.get("report", []) if isinstance(_details_raw, dict) else (_details_raw if isinstance(_details_raw, list) else [])
335
- if details_list:
336
- print("\n📊 Integration Report (Files already up-to-date):")
337
- for item in details_list:
338
- status_icon = "✔" if item.get("success") else "✖"
339
- f_name = item.get("file_path")
340
- msg = item.get("message", "")
341
- print(f" [{status_icon}] {f_name}: {msg}")
342
301
  except Exception as e:
343
302
  print(f"✖ Error: {e}")
344
303
  sys.exit(1)
@@ -346,15 +305,15 @@ def handle_apply(query: str):
346
305
 
347
306
  def main():
348
307
  parser = argparse.ArgumentParser(prog="codedthemes", description="CodedThemes CLI client")
349
- subparsers = parser.add_subparsers(dest="command")
308
+ subparsers = parser.add_subparsers(dest="command", required=True)
350
309
 
351
310
  login_parser = subparsers.add_parser("login", help="Login to MCP server")
352
311
  login_parser.add_argument("--server", help="MCP server URL")
353
312
 
354
313
  apply_parser = subparsers.add_parser("apply", help="Apply changes based on a query")
355
- apply_parser.add_argument("query", help="The description of changes you want to make")
314
+ apply_parser.add_argument("query", help="Description of changes to make")
356
315
 
357
- init_parser = subparsers.add_parser("init", help="Initialize and upload repository to the cloud")
316
+ subparsers.add_parser("init", help="Initialize repository and sync to cloud")
358
317
 
359
318
  args = parser.parse_args()
360
319
 
@@ -364,8 +323,6 @@ def main():
364
323
  handle_init()
365
324
  elif args.command == "apply":
366
325
  handle_apply(args.query)
367
- else:
368
- parser.print_help()
369
326
 
370
327
 
371
328
  if __name__ == "__main__":
@@ -7,16 +7,25 @@ CONFIG_FILE = CONFIG_DIR / "config.json"
7
7
 
8
8
 
9
9
  def ensure_config_dir():
10
+ """
11
+ Ensures the configuration directory exists.
12
+ """
10
13
  CONFIG_DIR.mkdir(exist_ok=True)
11
14
 
12
15
 
13
16
  def save_config(data: dict):
17
+ """
18
+ Saves the provided configuration data to the config file.
19
+ """
14
20
  ensure_config_dir()
15
21
  with open(CONFIG_FILE, "w") as f:
16
22
  json.dump(data, f, indent=2)
17
23
 
18
24
 
19
25
  def load_config():
26
+ """
27
+ Loads the configuration data from the config file.
28
+ """
20
29
  if not CONFIG_FILE.exists():
21
30
  return {}
22
31
  with open(CONFIG_FILE, "r") as f:
@@ -1,8 +1,12 @@
1
1
  import os
2
2
  import requests
3
3
  from .config import load_config
4
+ from .repo_utils import zip_repo
4
5
 
5
6
  class MCPClient:
7
+ """
8
+ Client for interacting with the CodedThemes MCP Server.
9
+ """
6
10
  def __init__(self):
7
11
  config = load_config()
8
12
  self.server_url = (
@@ -13,8 +17,9 @@ class MCPClient:
13
17
  self.token = config.get("access_token")
14
18
 
15
19
  def upload_workspace(self, repo_path: str, user_id: str):
16
- from .repo_utils import zip_repo
17
-
20
+ """
21
+ Zips and uploads the repository to create a new workspace.
22
+ """
18
23
  zip_path = zip_repo(repo_path)
19
24
  repo_name = os.path.basename(repo_path)
20
25
 
@@ -35,6 +40,16 @@ class MCPClient:
35
40
  timeout=300
36
41
  )
37
42
 
43
+ if response.status_code == 413:
44
+ zip_size_mb = os.path.getsize(zip_path) / (1024 * 1024)
45
+ raise Exception(
46
+ f"Upload failed: File is too large (413). Zip size: {zip_size_mb:.1f} MB.\n"
47
+ " Please reduce your repository size:\n"
48
+ " • Add large files/folders to .gitignore\n"
49
+ " • Remove binary assets (images, videos, fonts)\n"
50
+ " • Delete unused dependencies"
51
+ )
52
+
38
53
  if response.status_code != 200:
39
54
  raise Exception(f"Upload failed: {response.status_code} - {response.text}")
40
55
 
@@ -44,6 +59,9 @@ class MCPClient:
44
59
  os.remove(zip_path)
45
60
 
46
61
  def call(self, tool_name: str, payload: dict):
62
+ """
63
+ Calls a remote MCP tool.
64
+ """
47
65
  headers = {}
48
66
  if self.token:
49
67
  headers["Authorization"] = f"Bearer {self.token}"
@@ -0,0 +1,190 @@
1
+ import os
2
+ import zipfile
3
+ import tempfile
4
+ import logging
5
+ from pathlib import Path
6
+ from fnmatch import fnmatch
7
+
8
+ logger = logging.getLogger("codedthemes")
9
+
10
+ # ── Shared exclusion constants (used by both zip_repo and SyncManager) ────────
11
+
12
+ EXCLUDE_DIRS = {
13
+ # Version control
14
+ ".git", ".svn", ".hg",
15
+ # Dependencies
16
+ "node_modules", "bower_components", "vendor", "venv", ".venv", "env",
17
+ # Build outputs
18
+ "build", "dist", "out", "target", ".output", "_build",
19
+ # Framework caches
20
+ ".next", ".nuxt", ".svelte-kit", ".angular", ".turbo",
21
+ ".parcel-cache", ".cache", ".temp", ".tmp",
22
+ # IDE / editor
23
+ ".idea", ".vscode", ".vs",
24
+ # Python
25
+ "__pycache__", ".tox", ".mypy_cache", ".pytest_cache", ".ruff_cache",
26
+ # Testing
27
+ "coverage", ".nyc_output", "htmlcov",
28
+ # Misc
29
+ ".terraform", ".serverless",
30
+ }
31
+
32
+ EXCLUDE_EXTENSIONS = {
33
+ # Images
34
+ ".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".webp", ".ico", ".svg",
35
+ # Fonts
36
+ ".woff", ".woff2", ".ttf", ".eot", ".otf",
37
+ # Video / audio
38
+ ".mp4", ".webm", ".avi", ".mov", ".mp3", ".wav", ".ogg",
39
+ # Archives
40
+ ".zip", ".tar", ".gz", ".rar", ".7z",
41
+ # Documents
42
+ ".pdf", ".doc", ".docx", ".xls", ".xlsx", ".ppt", ".pptx",
43
+ # Source maps
44
+ ".map",
45
+ # Compiled / binary
46
+ ".exe", ".dll", ".so", ".dylib", ".o", ".pyc", ".pyo", ".class",
47
+ }
48
+
49
+ EXCLUDE_FILES = {
50
+ "package-lock.json", "yarn.lock", "pnpm-lock.yaml",
51
+ "composer.lock", "Gemfile.lock", "poetry.lock",
52
+ ".DS_Store", "Thumbs.db", "desktop.ini",
53
+ }
54
+
55
+ MAX_SINGLE_FILE_BYTES = 2 * 1024 * 1024 # 2 MB
56
+
57
+
58
+ # ── .gitignore parsing ────────────────────────────────────────────────────────
59
+
60
+ 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
+ """
66
+ gitignore_path = os.path.join(repo_root, ".gitignore")
67
+ patterns = []
68
+ if not os.path.isfile(gitignore_path):
69
+ return patterns
70
+ try:
71
+ with open(gitignore_path, "r", encoding="utf-8", errors="ignore") as f:
72
+ for line in f:
73
+ line = line.strip()
74
+ if not line or line.startswith("#"):
75
+ continue
76
+ # Ignore negation patterns (!) for simplicity
77
+ if line.startswith("!"):
78
+ continue
79
+ patterns.append(line.rstrip("/"))
80
+ except Exception:
81
+ pass
82
+ return patterns
83
+
84
+
85
+ def _is_gitignored(rel_path, patterns):
86
+ """
87
+ Checks if a relative path matches any .gitignore pattern.
88
+ """
89
+ parts = rel_path.replace("\\", "/").split("/")
90
+ for pattern in patterns:
91
+ # Directory-level match: check each path component
92
+ for part in parts:
93
+ if fnmatch(part, pattern):
94
+ return True
95
+ # Full-path match
96
+ if fnmatch(rel_path.replace("\\", "/"), pattern):
97
+ return True
98
+ if fnmatch(rel_path.replace("\\", "/"), f"**/{pattern}"):
99
+ return True
100
+ return False
101
+
102
+
103
+ # ── Core functions ────────────────────────────────────────────────────────────
104
+
105
+ def detect_repo_root(start_path=None):
106
+ """
107
+ Traverses up from start_path to find a repository root containing
108
+ package.json, pyproject.toml, ai.json, or .git.
109
+ """
110
+ if not start_path:
111
+ start_path = os.getcwd()
112
+
113
+ current = Path(start_path).resolve()
114
+
115
+ while current != current.parent:
116
+ if any((current / f).exists() for f in ["package.json", "pyproject.toml", "ai.json", ".git"]):
117
+ return current
118
+ current = current.parent
119
+
120
+ raise Exception("No repository root found.")
121
+
122
+
123
+ def zip_repo(repo_root):
124
+ """
125
+ Creates a temporary ZIP archive of the repository with aggressive filtering:
126
+ - Excludes common dependency / build / cache directories
127
+ - Excludes binary, media, font, and archive files by extension
128
+ - Excludes lock files
129
+ - Skips files larger than 2 MB
130
+ - Respects .gitignore patterns at the repo root
131
+ """
132
+ temp_zip = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
133
+ temp_zip.close()
134
+
135
+ gitignore_patterns = _load_gitignore_patterns(str(repo_root))
136
+ file_count = 0
137
+ skipped_count = 0
138
+
139
+ with zipfile.ZipFile(temp_zip.name, "w", zipfile.ZIP_DEFLATED) as z:
140
+ for root, dirs, files in os.walk(repo_root):
141
+ # Prune excluded directories (in-place)
142
+ dirs[:] = [
143
+ d for d in dirs
144
+ if d not in EXCLUDE_DIRS
145
+ and not d.startswith(".") # skip all hidden dirs (e.g. .env, .husky)
146
+ or d in {".github"} # but keep .github
147
+ ]
148
+
149
+ for file in files:
150
+ full_path = os.path.join(root, file)
151
+ rel_path = os.path.relpath(full_path, repo_root).replace(os.sep, "/")
152
+
153
+ # Skip by exact filename
154
+ if file in EXCLUDE_FILES:
155
+ skipped_count += 1
156
+ continue
157
+
158
+ # Skip by extension
159
+ _, ext = os.path.splitext(file)
160
+ if ext.lower() in EXCLUDE_EXTENSIONS:
161
+ skipped_count += 1
162
+ continue
163
+
164
+ # Skip large files
165
+ try:
166
+ if os.path.getsize(full_path) > MAX_SINGLE_FILE_BYTES:
167
+ logger.debug(f"Skipping large file: {rel_path}")
168
+ skipped_count += 1
169
+ continue
170
+ except OSError:
171
+ continue
172
+
173
+ # Skip .gitignore-matched paths
174
+ if gitignore_patterns and _is_gitignored(rel_path, gitignore_patterns):
175
+ skipped_count += 1
176
+ continue
177
+
178
+ z.write(full_path, rel_path)
179
+ file_count += 1
180
+
181
+ zip_size_mb = os.path.getsize(temp_zip.name) / (1024 * 1024)
182
+ logger.info(f"Zip created: {file_count} files, {zip_size_mb:.1f} MB (skipped {skipped_count})")
183
+
184
+ if zip_size_mb > 50:
185
+ logger.warning(
186
+ f"Zip file is {zip_size_mb:.1f} MB — upload may be slow or fail. "
187
+ "Consider adding large assets to .gitignore."
188
+ )
189
+
190
+ return temp_zip.name
@@ -2,11 +2,11 @@ import os
2
2
  import json
3
3
  import hashlib
4
4
  from datetime import datetime
5
+ from .repo_utils import EXCLUDE_DIRS
5
6
 
6
7
  class SyncManager:
7
8
  """
8
- Manages local file hashes to identify what files the user changed manually.
9
- Ensures `.codedthemes/workspace.json` is accurately tracked across CLI runs.
9
+ Manages local file hashes to track manual changes and maintain synchronization state.
10
10
  """
11
11
  def __init__(self):
12
12
  self.config_dir = os.path.expanduser("~/.codedthemes")
@@ -15,6 +15,9 @@ class SyncManager:
15
15
  self.workspaces = self.load()
16
16
 
17
17
  def load(self):
18
+ """
19
+ Loads the workspace synchronization state from the cloud config.
20
+ """
18
21
  if os.path.exists(self.config_file):
19
22
  try:
20
23
  with open(self.config_file, 'r') as f: return json.load(f)
@@ -22,13 +25,19 @@ class SyncManager:
22
25
  return {}
23
26
 
24
27
  def save(self):
28
+ """
29
+ Saves the workspace synchronization state locally.
30
+ """
25
31
  with open(self.config_file, 'w') as f:
26
32
  json.dump(self.workspaces, f, indent=2)
27
33
 
28
34
  def compute_hashes(self, repo_path):
35
+ """
36
+ Computes MD5 hashes for all relevant files in the repository.
37
+ """
29
38
  hashes = {}
30
39
  for root, dirs, files in os.walk(repo_path):
31
- dirs[:] = [d for d in dirs if d not in {'.git', 'node_modules', '__pycache__', 'venv', '.next', 'build', 'dist', 'out', '.cache', 'target', '.idea', '.vscode'}]
40
+ dirs[:] = [d for d in dirs if d not in EXCLUDE_DIRS]
32
41
  for file in files:
33
42
  abs_path = os.path.join(root, file)
34
43
  try:
@@ -41,6 +50,9 @@ class SyncManager:
41
50
  return hashes
42
51
 
43
52
  def update_sync_state(self, repo_path, workspace_id, user_id):
53
+ """
54
+ Updates the synchronization state with the latest file hashes.
55
+ """
44
56
  repo_key = f"{user_id}:{os.path.abspath(repo_path)}"
45
57
  self.workspaces[repo_key] = {
46
58
  "workspace_id": workspace_id,
@@ -51,6 +63,9 @@ class SyncManager:
51
63
  self.save()
52
64
 
53
65
  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
+ """
54
69
  repo_key = f"{user_id}:{os.path.abspath(repo_path)}"
55
70
  if repo_key not in self.workspaces: return []
56
71
 
@@ -0,0 +1,63 @@
1
+ Metadata-Version: 2.4
2
+ Name: codedthemes-cli
3
+ Version: 0.1.2
4
+ Summary: CLI tool for Code Theme and Integration
5
+ Author: codedthemes
6
+ Requires-Python: >=3.10
7
+ Description-Content-Type: text/markdown
8
+ Requires-Dist: requests
9
+ Requires-Dist: pyjwt
10
+
11
+ # CodedThemes CLI
12
+
13
+ CodedThemes CLI is a powerful command-line tool designed to integrate your local development environment with the CodedThemes MCP Server. It allows you to automate UI/UX modifications, refactor themes, and apply stylistic changes across your entire repository using AI-driven insights.
14
+
15
+ ## Features
16
+
17
+ - **Automated Theming**: Apply complex theme changes (e.g., branding updates) with simple natural language queries.
18
+ - **Local-to-Cloud Sync**: Securely sync your repository state to the CodedThemes engine for deep analysis.
19
+ - **Seamless Integration**: Automatically patches local files with surgical precision.
20
+ - **Sync Management**: Tracks local changes to ensure your manual edits are never overwritten.
21
+
22
+ ## Installation
23
+
24
+ Install the CLI directly from PyPI:
25
+
26
+ ```bash
27
+ pip install codedthemes-cli
28
+ ```
29
+
30
+ ## Usage
31
+
32
+ ### 1. Authentication
33
+
34
+ Authenticate with your CodedThemes account to enable secure communication with the MCP server.
35
+
36
+ ```bash
37
+ codedthemes login
38
+ ```
39
+ *Enter your registered email and license key when prompted.*
40
+
41
+ ### 2. Initialization (Optional)
42
+
43
+ Initialize your repository to establish a baseline for synchronization. This is recommended for first-time use in a project.
44
+
45
+ ```bash
46
+ codedthemes init
47
+ ```
48
+
49
+ ### 3. Applying Changes
50
+
51
+ 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.
52
+
53
+ ```bash
54
+ codedthemes apply "Update the theme branding from Mantis to Berry"
55
+ ```
56
+
57
+ ## Integration Report
58
+
59
+ After every successful `apply`, a granular report is generated showing exactly which files were modified, created, or deleted.
60
+
61
+ ## Support
62
+
63
+ For issues or feature requests, please contact the CodedThemes support team.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "codedthemes-cli"
7
- version = "0.1.0"
7
+ version = "0.1.2"
8
8
  description = "CLI tool for Code Theme and Integration"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,49 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: codedthemes-cli
3
- Version: 0.1.0
4
- Summary: CLI tool for Code Theme and Integration
5
- Author: codedthemes
6
- Requires-Python: >=3.10
7
- Description-Content-Type: text/markdown
8
- Requires-Dist: requests
9
- Requires-Dist: pyjwt
10
-
11
- # CodedThemes CLI
12
-
13
- The `codedthemes` CLI is a powerful command-line interface that allows you to interact with the CodeThemeMCP server. It enables you to analyze and modify your repositories using AI, with a focus on theme and style management.
14
-
15
- ## Installation
16
-
17
- Install the CLI globally using pip:
18
-
19
- ```bash
20
- pip install code-theme-mcp-cli
21
- ```
22
-
23
- ## Getting Started
24
-
25
- ### 1. Login
26
-
27
- Before applying any changes, you must authenticate your session with the MCP Server.
28
-
29
- ```bash
30
- codedthemes login
31
- ```
32
- *You will be prompted to enter your Email and License key.*
33
-
34
- ### 2. Apply Changes
35
-
36
- Navigate to your target project repository and run the `apply` command with your desired changes in quotes:
37
-
38
- ```bash
39
- codedthemes apply "Update the theme branding from Mantis to Berry"
40
- ```
41
-
42
- The CLI will automatically:
43
- 1. Package and securely send your repository to the remote server for analysis.
44
- 2. Plan the necessary code modifications using AI.
45
- 3. Automatically apply the patches back to your local files.
46
-
47
- ## Support
48
-
49
- For issues or feature requests, please contact the CodedThemes support team.
@@ -1,39 +0,0 @@
1
- # CodedThemes CLI
2
-
3
- The `codedthemes` CLI is a powerful command-line interface that allows you to interact with the CodeThemeMCP server. It enables you to analyze and modify your repositories using AI, with a focus on theme and style management.
4
-
5
- ## Installation
6
-
7
- Install the CLI globally using pip:
8
-
9
- ```bash
10
- pip install code-theme-mcp-cli
11
- ```
12
-
13
- ## Getting Started
14
-
15
- ### 1. Login
16
-
17
- Before applying any changes, you must authenticate your session with the MCP Server.
18
-
19
- ```bash
20
- codedthemes login
21
- ```
22
- *You will be prompted to enter your Email and License key.*
23
-
24
- ### 2. Apply Changes
25
-
26
- Navigate to your target project repository and run the `apply` command with your desired changes in quotes:
27
-
28
- ```bash
29
- codedthemes apply "Update the theme branding from Mantis to Berry"
30
- ```
31
-
32
- The CLI will automatically:
33
- 1. Package and securely send your repository to the remote server for analysis.
34
- 2. Plan the necessary code modifications using AI.
35
- 3. Automatically apply the patches back to your local files.
36
-
37
- ## Support
38
-
39
- For issues or feature requests, please contact the CodedThemes support team.
@@ -1,40 +0,0 @@
1
- import os
2
- import zipfile
3
- from pathlib import Path
4
- import tempfile
5
-
6
-
7
- def detect_repo_root(start_path=None):
8
- if not start_path:
9
- start_path = os.getcwd()
10
-
11
- current = Path(start_path).resolve()
12
-
13
- while current != current.parent:
14
- if (current / "package.json").exists() or (current / "pyproject.toml").exists() or (current / "ai.json").exists() or (current / ".git").exists():
15
- return current
16
- current = current.parent
17
-
18
- raise Exception("No repository root found.")
19
-
20
-
21
- def zip_repo(repo_root):
22
- temp_zip = tempfile.NamedTemporaryFile(delete=False, suffix=".zip")
23
- temp_zip.close() # Close so zipfile can open it
24
-
25
- with zipfile.ZipFile(temp_zip.name, "w", zipfile.ZIP_DEFLATED) as z:
26
- for root, dirs, files in os.walk(repo_root):
27
- # Exclude heavy/build directories
28
- exclude_dirs = {
29
- "node_modules", ".git", "venv", ".next", "build", "dist",
30
- "out", ".cache", "target", ".idea", ".vscode", "__pycache__"
31
- }
32
- dirs[:] = [d for d in dirs if d not in exclude_dirs]
33
-
34
- for file in files:
35
- full_path = os.path.join(root, file)
36
- # FORCE FORWARD SLASHES for zip entry names to ensure Linux compatibility
37
- rel_path = os.path.relpath(full_path, repo_root).replace(os.sep, '/')
38
- z.write(full_path, rel_path)
39
-
40
- return temp_zip.name
@@ -1,49 +0,0 @@
1
- Metadata-Version: 2.4
2
- Name: codedthemes-cli
3
- Version: 0.1.0
4
- Summary: CLI tool for Code Theme and Integration
5
- Author: codedthemes
6
- Requires-Python: >=3.10
7
- Description-Content-Type: text/markdown
8
- Requires-Dist: requests
9
- Requires-Dist: pyjwt
10
-
11
- # CodedThemes CLI
12
-
13
- The `codedthemes` CLI is a powerful command-line interface that allows you to interact with the CodeThemeMCP server. It enables you to analyze and modify your repositories using AI, with a focus on theme and style management.
14
-
15
- ## Installation
16
-
17
- Install the CLI globally using pip:
18
-
19
- ```bash
20
- pip install code-theme-mcp-cli
21
- ```
22
-
23
- ## Getting Started
24
-
25
- ### 1. Login
26
-
27
- Before applying any changes, you must authenticate your session with the MCP Server.
28
-
29
- ```bash
30
- codedthemes login
31
- ```
32
- *You will be prompted to enter your Email and License key.*
33
-
34
- ### 2. Apply Changes
35
-
36
- Navigate to your target project repository and run the `apply` command with your desired changes in quotes:
37
-
38
- ```bash
39
- codedthemes apply "Update the theme branding from Mantis to Berry"
40
- ```
41
-
42
- The CLI will automatically:
43
- 1. Package and securely send your repository to the remote server for analysis.
44
- 2. Plan the necessary code modifications using AI.
45
- 3. Automatically apply the patches back to your local files.
46
-
47
- ## Support
48
-
49
- For issues or feature requests, please contact the CodedThemes support team.