appforge-cli 1.6.4__tar.gz → 2.6.6__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 (23) hide show
  1. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/PKG-INFO +1 -1
  2. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/ai.py +12 -0
  3. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/cli.py +15 -0
  4. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/create.py +31 -0
  5. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/github.py +126 -154
  6. appforge_cli-2.6.6/appforge/injector.py +143 -0
  7. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/knowledge_base.json +37 -1
  8. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/utils.py +14 -2
  9. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge_cli.egg-info/PKG-INFO +1 -1
  10. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge_cli.egg-info/SOURCES.txt +1 -0
  11. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/pyproject.toml +1 -1
  12. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/setup.py +3 -2
  13. appforge_cli-1.6.4/appforge/injector.py +0 -125
  14. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/__init__.py +0 -0
  15. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/capacitor.py +0 -0
  16. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/config.py +0 -0
  17. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/default_icon.png +0 -0
  18. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/detector.py +0 -0
  19. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge_cli.egg-info/dependency_links.txt +0 -0
  20. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge_cli.egg-info/entry_points.txt +0 -0
  21. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge_cli.egg-info/requires.txt +0 -0
  22. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge_cli.egg-info/top_level.txt +0 -0
  23. {appforge_cli-1.6.4 → appforge_cli-2.6.6}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: appforge-cli
3
- Version: 1.6.4
3
+ Version: 2.6.6
4
4
  Summary: Convert web, Flutter, and native apps into Android/iOS apps automatically using cloud builds.
5
5
  Author: AppForge Team
6
6
  Author-email: AppForge Team <juniorsir.bot@gmail.com>
@@ -35,6 +35,18 @@ def scan_codebase_for_permissions():
35
35
  for file in files:
36
36
  file_path = os.path.join(root, file)
37
37
 
38
+ if file.endswith(('.xml', '.kt', '.java')):
39
+ try:
40
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
41
+ content = f.read()
42
+ for keyword, data in ai_knowledge_base.items():
43
+ # Only scan for keywords that aren't 'pubspec' specific
44
+ if not keyword.startswith("pubspec:"):
45
+ if keyword in content and data["plugin"] not in found_plugins:
46
+ found_plugins[data["plugin"]] = data
47
+ except Exception:
48
+ pass
49
+
38
50
  # --- FLUTTER SCANNER ---
39
51
  if file == 'pubspec.yaml' and not pubspec_already_scanned:
40
52
  try:
@@ -92,6 +92,21 @@ def init_project():
92
92
 
93
93
  print_success("Ready to send to the AppForge Cloud Builder.")
94
94
 
95
+ def manage_keys():
96
+ print_info("Key Management System")
97
+ action = prompt("Action: [1] Create New Keystore [2] Upload Existing", "1")
98
+
99
+ if action == "1":
100
+ alias = prompt("Enter Key Alias", "upload")
101
+ # Run keytool locally to generate the JKS file
102
+ cmd = f"keytool -genkeypair -alias {alias} -keyalg RSA -keysize 2048 -validity 10000 -keystore appforge.jks"
103
+ run_cmd(cmd)
104
+ print_success("Keystore created: appforge.jks")
105
+ # Now prompt to upload this to GitHub Secrets
106
+ elif action == "2":
107
+ path = prompt("Path to existing .jks file")
108
+ # Logic to encode to Base64 and push to GitHub Secrets
109
+
95
110
  def configure_project():
96
111
  config = load_local_config()
97
112
  print_info("Interactive Configuration")
@@ -60,6 +60,37 @@ def generate_html_template(project_name):
60
60
  with open(os.path.join(project_name, "www", "index.html"), "w") as f:
61
61
  f.write(html_content)
62
62
 
63
+ # --- Add this new helper function inside create.py ---
64
+ def sanitize_pubspec(project_dir):
65
+ """Removes monorepo-specific broken links from pubspec.yaml"""
66
+ pubspec_path = os.path.join(project_dir, "pubspec.yaml")
67
+ if not os.path.exists(pubspec_path):
68
+ return
69
+
70
+ with open(pubspec_path, "r", encoding='utf-8') as f:
71
+ lines = f.readlines()
72
+
73
+ new_lines = []
74
+ skip_next = False
75
+ for line in lines:
76
+ if skip_next:
77
+ skip_next = False
78
+ continue
79
+
80
+ # Remove the 'analysis_defaults' dependency and its 'path:' line
81
+ if "analysis_defaults:" in line:
82
+ skip_next = True # Skip this line and the next one (path: ...)
83
+ continue
84
+
85
+ # Remove the 'resolution: workspace' line if it exists
86
+ if "resolution: workspace" in line:
87
+ continue
88
+
89
+ new_lines.append(line)
90
+
91
+ with open(pubspec_path, "w", encoding='utf-8') as f:
92
+ f.writelines(new_lines)
93
+
63
94
  def create_project(framework, project_name):
64
95
  if framework not in TEMPLATES:
65
96
  print_error(f"Unknown framework: {framework}.")
@@ -5,6 +5,7 @@ import sys
5
5
  import threading
6
6
  import shutil
7
7
  import time
8
+ from nacl import encoding, public
8
9
  import base64
9
10
  from .config import get_app_id, load_local_config, add_to_history
10
11
  from .utils import print_success, print_info, print_error, run_with_spinner, GRAY, RESET, GREEN, RED, CYAN, YELLOW, BOLD, print_progress_bar
@@ -13,7 +14,7 @@ from .injector import inject_metadata_locally
13
14
  GITHUB_API_URL = "https://api.github.com"
14
15
  BUILD_REPO_OWNER = "juniorsir"
15
16
  BUILD_REPO_NAME = "appforge-build"
16
- MASTER_SYSTEM_TOKEN = "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" # <-- PUT YOUR TOKEN HERE
17
+ MASTER_SYSTEM_TOKEN = "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"
17
18
 
18
19
  def get_headers():
19
20
  return {
@@ -22,7 +23,34 @@ def get_headers():
22
23
  "X-GitHub-Api-Version": "2022-11-28"
23
24
  }
24
25
 
25
- # --- Inside github.py ---
26
+ def upload_secret(name, value):
27
+ """Encrypted upload to GitHub Secrets."""
28
+ # 1. Get Repo Public Key
29
+ pub_key_res = requests.get(f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/secrets/public-key", headers=get_headers())
30
+ key_data = pub_key_res.json()
31
+
32
+ # 2. Encrypt value with libsodium
33
+ public_key = public.PublicKey(base64.b64decode(key_data['key']), encoding.Base64Encoder)
34
+ sealed_box = public.SealedBox(public_key)
35
+ encrypted = base64.b64encode(sealed_box.encrypt(value.encode("utf-8"))).decode("utf-8")
36
+
37
+ # 3. Push to GitHub
38
+ requests.put(
39
+ f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/secrets/{name}",
40
+ headers=get_headers(),
41
+ json={"encrypted_value": encrypted, "key_id": key_data['key_id']}
42
+ )
43
+
44
+ def is_build_in_progress(app_id):
45
+ """Checks if there is an active run for this App ID."""
46
+ url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/runs?per_page=5"
47
+ res = requests.get(url, headers=get_headers())
48
+ if res.status_code == 200:
49
+ runs = res.json().get("workflow_runs", [])
50
+ for run in runs:
51
+ if f"ID: {app_id}" in run.get("display_title", "") and run["status"] in ["in_progress", "queued"]:
52
+ return True
53
+ return False
26
54
 
27
55
  def zip_project(category, sub_path=".", zip_name="app_source.zip"):
28
56
  """Zips the project, ensuring the icon is ready for @capacitor/assets."""
@@ -57,7 +85,6 @@ def zip_project(category, sub_path=".", zip_name="app_source.zip"):
57
85
 
58
86
  except Exception as e:
59
87
  print_error(f"Failed to prepare app assets for upload: {e}")
60
- # ------------------------------------
61
88
 
62
89
  def do_zip():
63
90
  original_dir = os.getcwd()
@@ -105,7 +132,8 @@ def push_and_build(config):
105
132
  platform = config.get("platform", "android")
106
133
  app_id = get_app_id()
107
134
  sub_path = config.get("sub_path", ".")
108
-
135
+ if is_build_in_progress(app_id):
136
+ print_error(f"A build is already in progress for App ID: {app_id}. Please wait for it to finish.")
109
137
  try:
110
138
  inject_metadata_locally(config)
111
139
  except Exception as e:
@@ -191,8 +219,8 @@ def push_and_build(config):
191
219
  "project_type": proj_type,
192
220
  "app_version": full_config.get("version", "1.0.0"),
193
221
  "sub_path": sub_path,
194
- "app_name": app_name, # <-- THESE NOW SEND "Vdl"
195
- "package_id": package_id, # <-- AND "com.appforge.vdl"
222
+ "app_name": app_name,
223
+ "package_id": package_id,
196
224
  "build_type": build_type
197
225
  }
198
226
  }
@@ -209,7 +237,6 @@ def push_and_build(config):
209
237
  from datetime import datetime
210
238
  project_name = os.path.basename(os.getcwd())
211
239
 
212
- # Create the history entry
213
240
  new_entry = {
214
241
  "app_id": app_id,
215
242
  "run_id": run_id,
@@ -219,141 +246,92 @@ def push_and_build(config):
219
246
  "status": "triggered" # We can update this later
220
247
  }
221
248
  add_to_history(new_entry)
249
+
222
250
  def check_status():
223
- """Streams live build logs from GitHub Actions with step-by-step animations."""
251
+ """Streams live build progress with clean, animated step tracking."""
224
252
  headers = get_headers()
225
- url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/runs?per_page=1"
226
- res = requests.get(url, headers=headers)
227
-
228
- if res.status_code != 200:
229
- print_error("Could not fetch build status from cloud.")
230
- return
231
-
232
- runs = res.json().get("workflow_runs", [])
233
- if not runs:
234
- print_info("No builds found in the cloud yet.")
235
- return
253
+ app_id = get_app_id()
236
254
 
237
- latest_run = runs[0]
238
- run_id = latest_run["id"]
239
- status = latest_run["status"]
255
+ # 1. Find the Run ID
256
+ print_info(f"Connecting to cloud build for App: {app_id}...")
257
+ my_run = None
258
+ for i in range(5):
259
+ res = requests.get(f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/runs?per_page=5", headers=headers)
260
+ if res.status_code == 200:
261
+ runs = res.json().get("workflow_runs", [])
262
+ for run in runs:
263
+ if f"ID: {app_id}" in run.get("display_title", ""):
264
+ my_run = run
265
+ break
266
+ if my_run: break
267
+ time.sleep(2)
240
268
 
241
- if status == "completed":
242
- conclusion = latest_run.get("conclusion")
243
- if conclusion == "success":
244
- print_success(f"Build #{run_id} is already completed! Run 'appforge download'")
245
- else:
246
- print_error(f"Build #{run_id} failed with conclusion: {conclusion}")
269
+ if not my_run:
270
+ print_error(f"No active builds found for App ID: {app_id}.")
247
271
  return
248
272
 
249
- print_info(f"Connected to Cloud Build #{run_id}. Streaming live logs...\n")
250
-
273
+ run_id = my_run["id"]
274
+ workflow_url = f"https://github.com/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/runs/{run_id}"
275
+ print_success(f"Connected to Cloud Build #{run_id}.")
276
+ print(f" {GRAY}View detailed logs online:{RESET} {CYAN}{workflow_url}{RESET}")
277
+ print("\n")
278
+
251
279
  jobs_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/runs/{run_id}/jobs"
252
-
253
280
  completed_steps = set()
254
- current_running_step = None
255
281
  spinner_chars = "|/-\\"
256
282
  spinner_idx = 0
257
-
283
+
258
284
  try:
259
285
  while True:
260
- # Animate the currently running step while we wait for the API ping
261
- for _ in range(20): # Loop fast for smooth animation (20 * 0.15s = 3 seconds)
262
- if current_running_step:
263
- char = spinner_chars[spinner_idx % len(spinner_chars)]
264
- # Highlight custom steps in Cyan, normal steps in Gray
265
- color = CYAN if "Permission" in current_running_step else GRAY
266
- sys.stdout.write(f"\r{color}{char}{RESET} [Cloud] {current_running_step}...")
267
- sys.stdout.flush()
268
- spinner_idx += 1
269
- time.sleep(0.15)
270
-
271
- # Ping the GitHub API
272
286
  jobs_res = requests.get(jobs_url, headers=headers)
273
287
  if jobs_res.status_code != 200:
288
+ time.sleep(3)
274
289
  continue
275
-
290
+
276
291
  jobs = jobs_res.json().get("jobs", [])
277
292
  if not jobs:
293
+ time.sleep(3)
278
294
  continue
279
295
 
280
296
  active_job = jobs[0]
281
- steps = active_job.get("steps", [])
297
+ current_step = None
282
298
 
283
- new_running_step = None
284
- for step in steps:
285
- step_name = step["name"]
286
- step_status = step["status"]
299
+ # Identify current step
300
+ for step in active_job.get("steps", []):
301
+ if step["status"] == "in_progress":
302
+ current_step = step["name"]
287
303
 
288
- if step_status == "completed" and step_name not in completed_steps:
289
- completed_steps.add(step_name)
304
+ if step["status"] == "completed" and step["name"] not in completed_steps:
305
+ completed_steps.add(step["name"])
306
+ sys.stdout.write("\r" + " " * 60 + "\r")
307
+ print(f"{GREEN}✔{RESET} [Cloud] {step['name']}... {GREEN}done!{RESET}")
290
308
 
291
- sys.stdout.write(f"\r{GREEN}✔{RESET} [Cloud] {step_name}... {GREEN}done!{RESET} \n")
292
- sys.stdout.flush()
293
-
294
309
  if step.get("conclusion") == "failure":
295
- print_error(f"\n❌ Cloud Build Failed at step: {step_name}")
296
-
297
- try:
298
- log_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/jobs/{active_job['id']}/logs"
299
- log_res = requests.get(log_url, headers=headers)
300
- if log_res.status_code == 200:
301
- friendly_msg = get_friendly_error(log_res.text)
302
- print(f"\n{YELLOW}💡 AppForge Diagnosis:{RESET}\n{friendly_msg}\n")
303
- except:
304
- pass
310
+ print_error(f"Cloud Build Failed at: {step['name']}")
305
311
  return
306
- if step.get("conclusion") == "failure":
307
- print_error(f"\n❌ Cloud Build Failed at step: {step_name}")
308
- return
309
-
310
- if step_status == "in_progress":
311
- new_running_step = step_name
312
-
313
- # Update the current step for the animation loop
314
- current_running_step = new_running_step
315
312
 
316
- # Check if the entire cloud job is done
313
+ # Display animated spinner for the current active step
314
+ if current_step:
315
+ char = spinner_chars[spinner_idx % len(spinner_chars)]
316
+ sys.stdout.write(f"\r{CYAN}{char}{RESET} [Cloud] {current_step}...")
317
+ sys.stdout.flush()
318
+ spinner_idx += 1
319
+
320
+ # Check for completion
317
321
  if active_job["status"] == "completed":
318
- print("-" * 40)
319
-
322
+ sys.stdout.write("\r" + " " * 60 + "\r")
320
323
  if active_job["conclusion"] == "success":
321
- # 1. JOB SUCCESSFUL? Check if the APK actually exists!
322
- artifacts_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/runs/{run_id}/artifacts"
323
- art_res = requests.get(artifacts_url, headers=headers)
324
-
325
- if art_res.status_code == 200 and art_res.json().get("total_count", 0) > 0:
326
- # EVERYTHING IS PERFECT
327
- print_success(f"{BOLD}✨ Cloud Build Finished Successfully!{RESET}")
328
- print_info("Run 'appforge download' to get your app.")
329
- else:
330
- # SOFT FAILURE: Success but no file!
331
- print_warning(f"\n⚠ Build finished, but NO ARTIFACT (APK) was produced.")
332
- # Fetch logs to find out why
333
- try:
334
- log_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/jobs/{active_job['id']}/logs"
335
- log_res = requests.get(log_url, headers=headers)
336
- friendly_msg = get_friendly_error(log_res.text)
337
- print(f"\n{YELLOW}💡 AppForge Diagnosis:{RESET}\n{friendly_msg}\n")
338
- except:
339
- print_error("Could not retrieve cloud logs for diagnosis.")
324
+ print_success(f"{BOLD}✨ Cloud Build Finished Successfully!{RESET}")
325
+ print_info("Run 'appforge download' to get your app.")
340
326
  else:
341
- # HARD FAILURE: Job crashed
342
- print_error("\n❌ Cloud Build Crashed.")
343
- # Fetch logs for hard failure
344
- try:
345
- log_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/jobs/{active_job['id']}/logs"
346
- log_res = requests.get(log_url, headers=headers)
347
- friendly_msg = get_friendly_error(log_res.text)
348
- print(f"\n{YELLOW}💡 AppForge Diagnosis:{RESET}\n{friendly_msg}\n")
349
- except: pass
327
+ print_error("Cloud Build Crashed.")
350
328
  break
351
329
 
330
+ time.sleep(1.5)
331
+
352
332
  except KeyboardInterrupt:
353
- # Clear the spinner line if user hits Ctrl+C
354
- sys.stdout.write("\r \r")
355
- print(f"\n{YELLOW}⚠ Stopped watching logs. The build is still running in the cloud.{RESET}")
356
- print_info("You can run 'appforge status' again later to reconnect.")
333
+ sys.stdout.write("\r \r")
334
+ print_warning("Stopped watching. Build continues in the cloud.")
357
335
 
358
336
  def get_build_status_by_id(run_id):
359
337
  headers = get_headers()
@@ -372,7 +350,7 @@ def get_build_status_by_id(run_id):
372
350
 
373
351
  def download_apk(run_id=None):
374
352
  """
375
- Finds and downloads ALL artifacts (Android, iOS, Windows, Linux) for a build.
353
+ Finds and downloads ALL artifacts for a build.
376
354
  """
377
355
  headers = get_headers()
378
356
  app_id = get_app_id()
@@ -387,55 +365,49 @@ def download_apk(run_id=None):
387
365
 
388
366
  if res.status_code == 200:
389
367
  artifacts = res.json().get("artifacts", [])
390
- if not artifacts:
391
- print_error("No artifacts found. The build may have failed or is still in progress.")
392
- return
393
-
394
- # --- NEW: FIND ALL MATCHING ARTIFACTS ---
395
- # Filter artifacts that belong to this specific App ID
396
- target_artifacts = [a for a in artifacts if app_id in a.get("name", "")]
368
+
369
+ # 1. Filter artifacts that belong to this specific App ID
370
+ my_artifacts = [a for a in artifacts if app_id in a.get("name", "")]
397
371
 
398
- if not target_artifacts:
372
+ if not my_artifacts:
399
373
  print_error(f"Could not find any recent artifacts for App ID: {app_id}")
400
374
  return
401
375
 
402
- print_info(f"Found {len(target_artifacts)} artifact(s) for this build. Starting downloads...\n")
376
+ # 2. SORT BY CREATED DATE (Newest first)
377
+ my_artifacts.sort(key=lambda x: x['created_at'], reverse=True)
378
+
379
+ # 3. Download the newest one
380
+ target_artifact = my_artifacts[0]
381
+ artifact_name = target_artifact['name']
382
+
383
+ print_info(f"Downloading latest build: {CYAN}{artifact_name}{RESET} (Created: {target_artifact['created_at']})")
384
+
385
+ secure_api_url = target_artifact.get('archive_download_url')
386
+ redirect_res = requests.get(secure_api_url, headers=headers, allow_redirects=False)
403
387
 
404
- # --- MULTI-DOWNLOAD LOOP ---
405
- for target_artifact in target_artifacts:
406
- artifact_name = target_artifact['name']
407
- print_info(f"Preparing download for: {CYAN}{artifact_name}{RESET}")
388
+ if redirect_res.status_code == 302:
389
+ public_download_url = redirect_res.headers['Location']
390
+ filename = f"{artifact_name}.zip"
408
391
 
409
- secure_api_url = target_artifact.get('archive_download_url')
410
- redirect_res = requests.get(secure_api_url, headers=headers, allow_redirects=False)
411
-
412
- if redirect_res.status_code == 302:
413
- public_download_url = redirect_res.headers['Location']
414
- filename = f"{artifact_name}.zip"
415
-
416
- try:
417
- # Start a streaming download
418
- with requests.get(public_download_url, stream=True) as r:
419
- r.raise_for_status()
420
- total_size = int(r.headers.get('content-length', 0))
421
- downloaded = 0
422
-
423
- with open(filename, 'wb') as f:
424
- # Download in 8KB chunks
425
- for chunk in r.iter_content(chunk_size=8192):
426
- if chunk: # filter out keep-alive new chunks
427
- f.write(chunk)
428
- downloaded += len(chunk)
429
- percentage = (downloaded / total_size * 100) if total_size > 0 else 0
430
- print_progress_bar(percentage, f"Downloading {artifact_name}")
431
-
432
- print_progress_bar(100.0, f"Downloading {artifact_name}")
433
- print_success(f"Saved: {os.path.join(os.getcwd(), filename)}\n")
434
-
435
- except Exception as e:
436
- print_error(f"Download failed for {artifact_name}: {e}")
437
- else:
438
- print_error(f"Could not generate a temporary download link for {artifact_name}.")
392
+ try:
393
+ with requests.get(public_download_url, stream=True) as r:
394
+ r.raise_for_status()
395
+ total_size = int(r.headers.get('content-length', 0))
396
+ downloaded = 0
397
+ with open(filename, 'wb') as f:
398
+ for chunk in r.iter_content(chunk_size=8192):
399
+ if chunk:
400
+ f.write(chunk)
401
+ downloaded += len(chunk)
402
+ percentage = (downloaded / total_size * 100) if total_size > 0 else 0
403
+ print_progress_bar(percentage, f"Downloading {artifact_name}")
404
+
405
+ print_progress_bar(100.0, f"Downloading {artifact_name}")
406
+ print_success(f"Saved: {os.path.join(os.getcwd(), filename)}")
407
+ except Exception as e:
408
+ print_error(f"Download failed for {artifact_name}: {e}")
409
+ else:
410
+ print_error(f"Could not generate a temporary download link. Status: {redirect_res.status_code}")
439
411
 
440
412
  else:
441
413
  print_error(f"Failed to fetch artifacts. Status Code: {res.status_code}")
@@ -0,0 +1,143 @@
1
+ import os
2
+ import shutil
3
+ import re
4
+ from .utils import print_info, print_success, print_warning
5
+
6
+ def inject_metadata_locally(config):
7
+ """
8
+ Universally injects App Name, Package ID, Version, and Icons locally
9
+ for Android, iOS, Windows, and Linux before zipping.
10
+ """
11
+
12
+ app_name = config.get("appName", "AppForge App")
13
+ package_id = config.get("packageId", "com.appforge.app")
14
+ app_version = config.get("version", "1.0.0")
15
+ icon_path = config.get("iconPath")
16
+ web_dir = config.get("webDir", "dist")
17
+
18
+ # Calculate build number (e.g., 1.0.5 -> 10005)
19
+ try:
20
+ parts = app_version.split('.')
21
+ build_number = int(f"{parts[0]}{parts[1].zfill(2)}{parts[2].zfill(2)}")
22
+ except:
23
+ build_number = 1
24
+
25
+ # Slug for Binary Names (e.g., "Vault OS" -> "vault_os")
26
+ binary_slug = re.sub(r'[^a-zA-Z0-9]', '_', app_name).lower()
27
+
28
+ print_info("Injecting metadata locally before upload...")
29
+
30
+ # --- 1. ANDROID & CAPACITOR INJECTION ---
31
+ manifest_path = "app/src/main/AndroidManifest.xml" if os.path.exists("app/src/main/AndroidManifest.xml") else "android/app/src/main/AndroidManifest.xml"
32
+ gradle_groovy = "app/build.gradle" if os.path.exists("app/build.gradle") else "android/app/build.gradle"
33
+ gradle_kts = gradle_groovy + ".kts"
34
+
35
+ if os.path.exists(manifest_path):
36
+ with open(manifest_path, 'r', encoding='utf-8') as f:
37
+ manifest = f.read()
38
+ manifest = re.sub(r'android:label="[^"]+"', f'android:label="{app_name}"', manifest)
39
+ if 'xmlns:tools=' not in manifest:
40
+ manifest = manifest.replace('<manifest', '<manifest xmlns:tools="http://schemas.android.com/tools"')
41
+ if 'tools:replace="android:label"' not in manifest:
42
+ manifest = manifest.replace('<application', '<application tools:replace="android:label"')
43
+ if 'package=' in manifest:
44
+ manifest = re.sub(r'package="[^"]+"', f'package="{package_id}"', manifest)
45
+ with open(manifest_path, 'w', encoding='utf-8') as f:
46
+ f.write(manifest)
47
+ print_success(f"Injected Android Name: {app_name}")
48
+
49
+ target_gradle = gradle_kts if os.path.exists(gradle_kts) else (gradle_groovy if os.path.exists(gradle_groovy) else None)
50
+ if target_gradle:
51
+ with open(target_gradle, 'r', encoding='utf-8') as f:
52
+ gradle = f.read()
53
+ gradle = re.sub(r'applicationId\s*=?\s*["\'][^"\']+["\']', f'applicationId = "{package_id}"', gradle)
54
+ gradle = re.sub(r'namespace\s*=?\s*["\'][^"\']+["\']', f'namespace = "{package_id}"', gradle)
55
+ gradle = re.sub(r'versionName\s*=?\s*["\'][^"\']+["\']', f'versionName = "{app_version}"', gradle)
56
+ gradle = re.sub(r'versionCode\s*=?\s*\d+', f'versionCode = {build_number}', gradle)
57
+ with open(target_gradle, 'w', encoding='utf-8') as f:
58
+ f.write(gradle)
59
+ print_success(f"Injected Android Package ID: {package_id}")
60
+
61
+ # --- 2. WINDOWS DESKTOP INJECTION ---
62
+ win_cmake = "windows/CMakeLists.txt"
63
+ if os.path.exists(win_cmake):
64
+ with open(win_cmake, 'r', encoding='utf-8') as f:
65
+ content = f.read()
66
+ # Set project name and binary name
67
+ content = re.sub(r'set\(BINARY_NAME "[^"]+"\)', f'set(BINARY_NAME "{binary_slug}")', content)
68
+ content = re.sub(r'project\([^)]+\)', f'project({binary_slug} LANGUAGES CXX)', content)
69
+ with open(win_cmake, 'w', encoding='utf-8') as f:
70
+ f.write(content)
71
+
72
+ # Update Window Title in main.cpp
73
+ win_main = "windows/runner/main.cpp"
74
+ if os.path.exists(win_main):
75
+ with open(win_main, 'r', encoding='utf-8') as f:
76
+ cpp = f.read()
77
+ cpp = re.sub(r'window\.CreateAndShow\(L"[^"]+"', f'window.CreateAndShow(L"{app_name}"', cpp)
78
+ with open(win_main, 'w', encoding='utf-8') as f:
79
+ f.write(cpp)
80
+ print_success(f"Injected Windows Title: {app_name}")
81
+
82
+ # --- 3. LINUX DESKTOP INJECTION ---
83
+ linux_cmake = "linux/CMakeLists.txt"
84
+ if os.path.exists(linux_cmake):
85
+ with open(linux_cmake, 'r', encoding='utf-8') as f:
86
+ content = f.read()
87
+ content = re.sub(r'set\(BINARY_NAME "[^"]+"\)', f'set(BINARY_NAME "{binary_slug}")', content)
88
+ with open(linux_cmake, 'w', encoding='utf-8') as f:
89
+ f.write(content)
90
+
91
+ # Update Window Title in application.cc
92
+ # Usually found in linux/my_application.cc
93
+ for file in os.listdir("linux"):
94
+ if file.endswith(".cc"):
95
+ cc_path = os.path.join("linux", file)
96
+ with open(cc_path, 'r', encoding='utf-8') as f:
97
+ cc = f.read()
98
+ cc = re.sub(r'gtk_window_set_title\(GTK_WINDOW\(window\), "[^"]+"\)', f'gtk_window_set_title(GTK_WINDOW(window), "{app_name}")', cc)
99
+ with open(cc_path, 'w', encoding='utf-8') as f:
100
+ f.write(cc)
101
+ print_success(f"Injected Linux Title: {app_name}")
102
+
103
+ # --- 4. FLUTTER PUBSPEC INJECTION ---
104
+ if os.path.exists("pubspec.yaml"):
105
+ with open("pubspec.yaml", 'r', encoding='utf-8') as f:
106
+ pubspec = f.read()
107
+ pubspec = re.sub(r'^version:\s*.*$', f'version: {app_version}+{build_number}', pubspec, flags=re.MULTILINE)
108
+ pubspec = re.sub(r'^name:\s*.*$', f'name: {binary_slug}', pubspec, flags=re.MULTILINE)
109
+ with open("pubspec.yaml", 'w', encoding='utf-8') as f:
110
+ f.write(pubspec)
111
+ print_success(f"Updated pubspec.yaml version and name.")
112
+
113
+ # --- 5. UNIVERSAL ICON INJECTION ---
114
+ if icon_path and os.path.exists(icon_path):
115
+ injected_icons = False
116
+
117
+ # Android Mipmaps (Local)
118
+ if os.path.exists(manifest_path):
119
+ res_dir = os.path.join(os.path.dirname(manifest_path), 'res')
120
+ mipmaps = ['mipmap-mdpi', 'mipmap-hdpi', 'mipmap-xhdpi', 'mipmap-xxhdpi', 'mipmap-xxxhdpi']
121
+ for m in mipmaps:
122
+ target_dir = os.path.join(res_dir, m)
123
+ if os.path.exists(target_dir):
124
+ shutil.copy2(icon_path, os.path.join(target_dir, 'ic_launcher.png'))
125
+ if os.path.exists(os.path.join(target_dir, 'ic_launcher_round.png')):
126
+ shutil.copy2(icon_path, os.path.join(target_dir, 'ic_launcher_round.png'))
127
+ injected_icons = True
128
+
129
+ # Web/Flutter Web (Favicons & PWA)
130
+ web_targets = ["web", "public", web_dir]
131
+ for w_dir in web_targets:
132
+ if os.path.exists(w_dir):
133
+ shutil.copy2(icon_path, os.path.join(w_dir, 'favicon.png'))
134
+ icons_dir = os.path.join(w_dir, 'icons')
135
+ if os.path.exists(icons_dir):
136
+ for p_icon in ['Icon-192.png', 'Icon-512.png', 'Icon-maskable-192.png', 'Icon-maskable-512.png']:
137
+ shutil.copy2(icon_path, os.path.join(icons_dir, p_icon))
138
+ injected_icons = True
139
+
140
+ if injected_icons:
141
+ print_success("Injected App Icons across all detected platforms.")
142
+
143
+ print_success("Local Metadata Injection Complete.")
@@ -258,5 +258,41 @@
258
258
  "plugin": "pedometer",
259
259
  "feature": "Pedometer (Step Counter)",
260
260
  "desc": "Found Flutter pedometer dependency"
261
- }
261
+ },
262
+ "android.permission.CAMERA": { "plugin": "CAMERA", "feature": "Camera Access", "desc": "Required for taking photos or videos" },
263
+ "android.permission.ACCESS_FINE_LOCATION": { "plugin": "LOCATION", "feature": "Precise GPS", "desc": "Required for high-accuracy tracking" },
264
+ "android.permission.ACCESS_COARSE_LOCATION": { "plugin": "LOCATION", "feature": "Approximate Location", "desc": "Required for city-level positioning" },
265
+ "android.permission.ACCESS_BACKGROUND_LOCATION": { "plugin": "LOCATION", "feature": "Background Location", "desc": "Required for tracking while app is closed" },
266
+ "android.permission.RECORD_AUDIO": { "plugin": "MICROPHONE", "feature": "Microphone", "desc": "Required for audio recording" },
267
+ "android.permission.READ_EXTERNAL_STORAGE": { "plugin": "STORAGE", "feature": "Read Files", "desc": "Required to access user media" },
268
+ "android.permission.WRITE_EXTERNAL_STORAGE": { "plugin": "STORAGE", "feature": "Write Files", "desc": "Required to save files" },
269
+ "android.permission.READ_MEDIA_IMAGES": { "plugin": "STORAGE", "feature": "Photo Access", "desc": "Required for Android 13+ Media access" },
270
+ "android.permission.READ_MEDIA_VIDEO": { "plugin": "STORAGE", "feature": "Video Access", "desc": "Required for Android 13+ Media access" },
271
+ "android.permission.BLUETOOTH": { "plugin": "BLUETOOTH", "feature": "Classic Bluetooth", "desc": "Legacy Bluetooth support" },
272
+ "android.permission.BLUETOOTH_SCAN": { "plugin": "BLUETOOTH", "feature": "BLE Scanning", "desc": "Required for finding BLE devices" },
273
+ "android.permission.BLUETOOTH_CONNECT": { "plugin": "BLUETOOTH", "feature": "BLE Connecting", "desc": "Required to connect to BLE devices" },
274
+ "android.permission.USE_BIOMETRIC": { "plugin": "BIOMETRIC", "feature": "Face/Fingerprint", "desc": "Required for biometric authentication" },
275
+ "android.permission.READ_CONTACTS": { "plugin": "CONTACTS", "feature": "Contacts", "desc": "Required to access address book" },
276
+ "android.permission.CALL_PHONE": { "plugin": "PHONE", "feature": "Phone Calls", "desc": "Required to trigger phone dialer" },
277
+ "android.permission.SEND_SMS": { "plugin": "SMS", "feature": "Send SMS", "desc": "Required to send messages" },
278
+ "android.permission.RECEIVE_SMS": { "plugin": "SMS", "feature": "Receive SMS", "desc": "Required to read incoming OTPs" },
279
+ "android.permission.INTERNET": { "plugin": "NETWORK", "feature": "Internet", "desc": "Standard connectivity" },
280
+ "android.permission.ACCESS_NETWORK_STATE": { "plugin": "NETWORK", "feature": "Network Status", "desc": "Check connection quality" },
281
+ "android.permission.WAKE_LOCK": { "plugin": "POWER", "feature": "Wake Lock", "desc": "Prevent screen from dimming" },
282
+ "android.permission.RECEIVE_BOOT_COMPLETED": { "plugin": "SYSTEM", "feature": "Boot Receiver", "desc": "Required to run services on startup" },
283
+ "android.permission.VIBRATE": { "plugin": "HAPTICS", "feature": "Vibration", "desc": "Required for haptic feedback" },
284
+ "android.permission.FOREGROUND_SERVICE": { "plugin": "SERVICES", "feature": "Foreground Service", "desc": "Required for long-running tasks" },
285
+ "android.permission.POST_NOTIFICATIONS": { "plugin": "NOTIFICATIONS", "feature": "Push Notifications", "desc": "Required for Android 13+ notifications" },
286
+ "android.permission.USE_BIOMETRIC": { "plugin": "BIOMETRIC", "feature": "Biometric Authentication", "desc": "Required for fingerprint or face unlock" },
287
+ "android.permission.WRITE_SETTINGS": { "plugin": "SYSTEM_SETTINGS", "feature": "System Settings", "desc": "Required to modify system display or volume settings" },
288
+ "android.permission.BODY_SENSORS": { "plugin": "SENSORS", "feature": "Health Sensors", "desc": "Required for heart rate or step tracking" },
289
+ "android.permission.REQUEST_INSTALL_PACKAGES": { "plugin": "SYSTEM_INSTALL", "feature": "Package Installer", "desc": "Required for in-app updates/APK installation" },
290
+ "android.permission.ACCESS_WIFI_STATE": { "plugin": "NETWORK", "feature": "Wi-Fi State", "desc": "Required to check Wi-Fi connection info" },
291
+ "android.permission.CHANGE_WIFI_STATE": { "plugin": "NETWORK", "feature": "Wi-Fi Control", "desc": "Required to toggle Wi-Fi on or off" },
292
+ "android.permission.READ_CALENDAR": { "plugin": "CALENDAR", "feature": "Calendar Read", "desc": "Required to read user events" },
293
+ "android.permission.WRITE_CALENDAR": { "plugin": "CALENDAR", "feature": "Calendar Write", "desc": "Required to add events to calendar" },
294
+ "android.permission.MODIFY_AUDIO_SETTINGS": { "plugin": "AUDIO", "feature": "Audio Control", "desc": "Required to change speaker or headphone volume" },
295
+ "android.permission.SCHEDULE_EXACT_ALARM": { "plugin": "ALARMS", "feature": "Exact Alarms", "desc": "Required for precision-timed app notifications" },
296
+ "android.permission.SYSTEM_ALERT_WINDOW": { "plugin": "OVERLAY", "feature": "Display Over Other Apps", "desc": "Required to show floating windows above other applications"},
297
+ "android.permission.CHANGE_WIFI_MULTICAST_STATE": { "plugin": "NETWORK", "feature": "Wi-Fi Multicast Discovery", "desc": "Required for automatic discovery of nearby devices on the local Wi-Fi network" }
262
298
  }
@@ -14,8 +14,8 @@ RED = "\033[31m"
14
14
  GRAY = "\033[90m"
15
15
  BLUE = "\033[34m"
16
16
 
17
- CLI_VERSION = "1.6.4"
18
-
17
+ CLI_VERSION = "4.6.0"
18
+ printed_log_lines = set()
19
19
  def check_for_cli_updates():
20
20
  try:
21
21
  res = requests.get("https://pypi.org/pypi/appforge-cli/json", timeout=2)
@@ -35,6 +35,18 @@ def print_header():
35
35
  print(f"{YELLOW}⭐ A new version of AppForge is available! (v{latest}){RESET}")
36
36
  print(f"{GRAY}Run: pip install --upgrade appforge-cli{RESET}\n")
37
37
 
38
+ def print_live_log(log_text):
39
+ global printed_log_lines
40
+ lines = log_text.splitlines()
41
+
42
+ # Only look at the last 15 lines of the build
43
+ for line in lines[-15:]:
44
+ # Filter for relevant build info
45
+ if any(x in line for x in [":app:", "Task", "Building", ">"]):
46
+ if line not in printed_log_lines:
47
+ print(f"{GRAY} {line[:80]}{RESET}")
48
+ printed_log_lines.add(line)
49
+
38
50
  def print_footer():
39
51
  print(f"\n{GRAY}AppForge CLI")
40
52
  print(f"Built by AppForge Team [Maintainer - JuniorSir]{RESET}\n")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: appforge-cli
3
- Version: 1.6.4
3
+ Version: 2.6.6
4
4
  Summary: Convert web, Flutter, and native apps into Android/iOS apps automatically using cloud builds.
5
5
  Author: AppForge Team
6
6
  Author-email: AppForge Team <juniorsir.bot@gmail.com>
@@ -1,4 +1,5 @@
1
1
  pyproject.toml
2
+ setup.cfg
2
3
  setup.py
3
4
  appforge/__init__.py
4
5
  appforge/ai.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "appforge-cli"
7
- version = "1.6.4"
7
+ version = "2.6.6"
8
8
  authors = [
9
9
  { name="AppForge Team", email="juniorsir.bot@gmail.com" },
10
10
  ]
@@ -2,7 +2,7 @@ from setuptools import setup
2
2
 
3
3
  setup(
4
4
  name="appforge",
5
- version="1.6.4",
5
+ version="4.6.0",
6
6
  description="Convert web apps into Android apps automatically using Capacitor and GitHub cloud builds.",
7
7
  author="AppForge Team",
8
8
  packages=["appforge"],
@@ -12,7 +12,8 @@ setup(
12
12
  include_package_data=True,
13
13
  install_requires=[
14
14
  "requests",
15
- "packaging"
15
+ "packaging",
16
+ "nacl"
16
17
  ],
17
18
  entry_points={
18
19
  "console_scripts": [
@@ -1,125 +0,0 @@
1
- import os
2
- import shutil
3
- import re
4
- from .utils import print_info, print_success, print_warning
5
-
6
- def inject_metadata_locally(config):
7
- """Injects App Name, Package ID, Version, and Icons locally before zipping."""
8
-
9
- app_name = config.get("appName", "AppForge App")
10
- package_id = config.get("packageId", "com.appforge.app")
11
- app_version = config.get("version", "1.0.0")
12
- icon_path = config.get("iconPath")
13
- web_dir = config.get("webDir", "dist")
14
- project_category = config.get("category", "web")
15
-
16
- try:
17
- parts = app_version.split('.')
18
- build_number = int(f"{parts[0]}{parts[1].zfill(2)}{parts[2].zfill(2)}")
19
- except:
20
- build_number = 1
21
-
22
- print_info("Injecting metadata locally before upload...")
23
-
24
- # --- PATH DETECTION ---
25
- manifest_path = "app/src/main/AndroidManifest.xml" if os.path.exists("app/src/main/AndroidManifest.xml") else "android/app/src/main/AndroidManifest.xml"
26
- gradle_groovy = "app/build.gradle" if os.path.exists("app/build.gradle") else "android/app/build.gradle"
27
- gradle_kts = gradle_groovy + ".kts"
28
-
29
- # --- 1. INJECT APP NAME (AndroidManifest.xml) ---
30
- if os.path.exists(manifest_path):
31
- with open(manifest_path, 'r', encoding='utf-8') as f:
32
- manifest = f.read()
33
-
34
- manifest = re.sub(r'android:label="[^"]+"', f'android:label="{app_name}"', manifest)
35
- if 'xmlns:tools=' not in manifest:
36
- manifest = manifest.replace('<manifest', '<manifest xmlns:tools="http://schemas.android.com/tools"')
37
- if 'tools:replace="android:label"' not in manifest:
38
- manifest = manifest.replace('<application', '<application tools:replace="android:label"')
39
- if 'package=' in manifest:
40
- manifest = re.sub(r'package="[^"]+"', f'package="{package_id}"', manifest)
41
-
42
- with open(manifest_path, 'w', encoding='utf-8') as f:
43
- f.write(manifest)
44
- print_success(f"Injected App Name: {app_name}")
45
-
46
- # --- 2. INJECT PACKAGE ID & VERSION (build.gradle) ---
47
- target_gradle = gradle_kts if os.path.exists(gradle_kts) else (gradle_groovy if os.path.exists(gradle_groovy) else None)
48
-
49
- if target_gradle:
50
- with open(target_gradle, 'r', encoding='utf-8') as f:
51
- gradle = f.read()
52
-
53
- gradle = re.sub(r'applicationId\s*=?\s*["\'][^"\']+["\']', f'applicationId = "{package_id}"', gradle)
54
- gradle = re.sub(r'applicationId\s+["\'][^"\']+["\']', f'applicationId "{package_id}"', gradle)
55
- gradle = re.sub(r'namespace\s*=?\s*["\'][^"\']+["\']', f'namespace = "{package_id}"', gradle)
56
- gradle = re.sub(r'namespace\s+["\'][^"\']+["\']', f'namespace "{package_id}"', gradle)
57
- gradle = re.sub(r'versionName\s*=?\s*["\'][^"\']+["\']', f'versionName = "{app_version}"', gradle)
58
- gradle = re.sub(r'versionName\s+["\'][^"\']+["\']', f'versionName "{app_version}"', gradle)
59
- gradle = re.sub(r'versionCode\s*=?\s*\d+', f'versionCode = {build_number}', gradle)
60
- gradle = re.sub(r'versionCode\s+\d+', f'versionCode {build_number}', gradle)
61
-
62
- with open(target_gradle, 'w', encoding='utf-8') as f:
63
- f.write(gradle)
64
- print_success(f"Injected Package ID: {package_id} & Version: {app_version}")
65
-
66
- # --- 3. INJECT PUBSPEC.YAML (Flutter Specifics) ---
67
- if os.path.exists("pubspec.yaml"):
68
- with open("pubspec.yaml", 'r', encoding='utf-8') as f:
69
- pubspec = f.read()
70
-
71
- # 3a. Update Version
72
- if re.search(r'^version:\s*.*$', pubspec, re.MULTILINE):
73
- pubspec = re.sub(r'^version:\s*.*$', f'version: {app_version}+{build_number}', pubspec, flags=re.MULTILINE)
74
-
75
- # 3b. Update Project Name (Must be snake_case for Dart!)
76
- # e.g. "Vault Downloader" -> "vault_downloader"
77
- snake_case_name = re.sub(r'[^a-zA-Z0-9]', '_', app_name).lower()
78
- if re.search(r'^name:\s*.*$', pubspec, re.MULTILINE):
79
- pubspec = re.sub(r'^name:\s*.*$', f'name: {snake_case_name}', pubspec, flags=re.MULTILINE)
80
-
81
- with open("pubspec.yaml", 'w', encoding='utf-8') as f:
82
- f.write(pubspec)
83
- print_success(f"Updated pubspec.yaml (name: {snake_case_name}, version: {app_version})")
84
-
85
- # --- 4. INJECT APP ICONS (Universal) ---
86
- if icon_path and os.path.exists(icon_path):
87
- injected_icons = False
88
-
89
- # 4a. Android Mipmaps
90
- if os.path.exists(manifest_path):
91
- res_dir = os.path.join(os.path.dirname(manifest_path), 'res')
92
- mipmaps = ['mipmap-mdpi', 'mipmap-hdpi', 'mipmap-xhdpi', 'mipmap-xxhdpi', 'mipmap-xxxhdpi']
93
- for m in mipmaps:
94
- target_dir = os.path.join(res_dir, m)
95
- if os.path.exists(target_dir):
96
- shutil.copyfile(icon_path, os.path.join(target_dir, 'ic_launcher.png'))
97
- round_icon = os.path.join(target_dir, 'ic_launcher_round.png')
98
- if os.path.exists(round_icon):
99
- shutil.copyfile(icon_path, round_icon)
100
- injected_icons = True
101
-
102
- # 4b. Web / Flutter Web Icons (PWA Support)
103
- # Check standard web directories
104
- web_targets = ["web", "public", web_dir]
105
- for w_dir in web_targets:
106
- if os.path.exists(w_dir):
107
- # Replace Favicon
108
- shutil.copyfile(icon_path, os.path.join(w_dir, 'favicon.png'))
109
- if os.path.exists(os.path.join(w_dir, 'favicon.ico')):
110
- shutil.copyfile(icon_path, os.path.join(w_dir, 'favicon.ico'))
111
-
112
- # Replace PWA Icons
113
- icons_dir = os.path.join(w_dir, 'icons')
114
- if os.path.exists(icons_dir):
115
- pwa_icons = ['Icon-192.png', 'Icon-512.png', 'Icon-maskable-192.png', 'Icon-maskable-512.png']
116
- for p_icon in pwa_icons:
117
- shutil.copyfile(icon_path, os.path.join(icons_dir, p_icon))
118
- injected_icons = True
119
-
120
- if injected_icons:
121
- print_success("Injected custom App Icons across Native and Web directories.")
122
- else:
123
- print_warning("Icon path found, but no valid target directories (res/mipmap or web/icons) exist yet.")
124
-
125
- print_success("Local Metadata Injection Complete. Project is ready for upload.")
File without changes