appforge-cli 1.6.6__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 (22) hide show
  1. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/PKG-INFO +1 -1
  2. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/ai.py +12 -0
  3. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/cli.py +15 -0
  4. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/github.py +122 -159
  5. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/knowledge_base.json +37 -1
  6. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/utils.py +14 -2
  7. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge_cli.egg-info/PKG-INFO +1 -1
  8. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge_cli.egg-info/SOURCES.txt +1 -0
  9. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/pyproject.toml +1 -1
  10. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/setup.py +3 -2
  11. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/__init__.py +0 -0
  12. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/capacitor.py +0 -0
  13. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/config.py +0 -0
  14. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/create.py +0 -0
  15. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/default_icon.png +0 -0
  16. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/detector.py +0 -0
  17. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/injector.py +0 -0
  18. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge_cli.egg-info/dependency_links.txt +0 -0
  19. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge_cli.egg-info/entry_points.txt +0 -0
  20. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge_cli.egg-info/requires.txt +0 -0
  21. {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge_cli.egg-info/top_level.txt +0 -0
  22. {appforge_cli-1.6.6 → 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.6
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")
@@ -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,6 +23,35 @@ def get_headers():
22
23
  "X-GitHub-Api-Version": "2022-11-28"
23
24
  }
24
25
 
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
54
+
25
55
  def zip_project(category, sub_path=".", zip_name="app_source.zip"):
26
56
  """Zips the project, ensuring the icon is ready for @capacitor/assets."""
27
57
  config = load_local_config()
@@ -102,7 +132,8 @@ def push_and_build(config):
102
132
  platform = config.get("platform", "android")
103
133
  app_id = get_app_id()
104
134
  sub_path = config.get("sub_path", ".")
105
-
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.")
106
137
  try:
107
138
  inject_metadata_locally(config)
108
139
  except Exception as e:
@@ -215,154 +246,92 @@ def push_and_build(config):
215
246
  "status": "triggered" # We can update this later
216
247
  }
217
248
  add_to_history(new_entry)
249
+
218
250
  def check_status():
219
- """Streams live build logs from GitHub Actions with step-by-step animations."""
251
+ """Streams live build progress with clean, animated step tracking."""
220
252
  headers = get_headers()
221
253
  app_id = get_app_id()
222
- url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/runs?per_page=1"
223
- res = requests.get(url, headers=headers)
224
254
 
225
- if res.status_code != 200:
226
- print_error("Could not fetch build status from cloud.")
227
- return
228
-
229
- runs = res.json().get("workflow_runs", [])
255
+ # 1. Find the Run ID
256
+ print_info(f"Connecting to cloud build for App: {app_id}...")
230
257
  my_run = None
231
- for run in runs:
232
- if f"ID: {app_id}" in run.get("display_title", ""):
233
- my_run = run
234
- break
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)
235
268
 
236
269
  if not my_run:
237
- print_info(f"No active builds found for your App ID ({app_id}).")
238
- print(f"{GRAY}Note: New builds may take 10 seconds to appear in the list.{RESET}")
239
- return
240
-
241
- run_id = my_run["id"]
242
- status = my_run["status"]
243
-
244
- print_info(f"Connected to your Cloud Build #{run_id}. Streaming live logs...\n")
245
-
246
- if not runs:
247
- print_info("No builds found in the cloud yet.")
248
- return
249
-
250
- if status == "completed":
251
- conclusion = latest_run.get("conclusion")
252
- if conclusion == "success":
253
- print_success(f"Build #{run_id} is already completed! Run 'appforge download'")
254
- else:
255
- print_error(f"Build #{run_id} failed with conclusion: {conclusion}")
270
+ print_error(f"No active builds found for App ID: {app_id}.")
256
271
  return
257
272
 
258
- print_info(f"Connected to Cloud Build #{run_id}. Streaming live logs...\n")
259
-
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
+
260
279
  jobs_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/runs/{run_id}/jobs"
261
-
262
280
  completed_steps = set()
263
- current_running_step = None
264
281
  spinner_chars = "|/-\\"
265
282
  spinner_idx = 0
266
-
283
+
267
284
  try:
268
285
  while True:
269
- # Animate the currently running step while we wait for the API ping
270
- for _ in range(20): # Loop fast for smooth animation (20 * 0.15s = 3 seconds)
271
- if current_running_step:
272
- char = spinner_chars[spinner_idx % len(spinner_chars)]
273
- # Highlight custom steps in Cyan, normal steps in Gray
274
- color = CYAN if "Permission" in current_running_step else GRAY
275
- sys.stdout.write(f"\r{color}{char}{RESET} [Cloud] {current_running_step}...")
276
- sys.stdout.flush()
277
- spinner_idx += 1
278
- time.sleep(0.15)
279
-
280
- # Ping the GitHub API
281
286
  jobs_res = requests.get(jobs_url, headers=headers)
282
287
  if jobs_res.status_code != 200:
288
+ time.sleep(3)
283
289
  continue
284
-
290
+
285
291
  jobs = jobs_res.json().get("jobs", [])
286
292
  if not jobs:
293
+ time.sleep(3)
287
294
  continue
288
295
 
289
296
  active_job = jobs[0]
290
- steps = active_job.get("steps", [])
297
+ current_step = None
291
298
 
292
- new_running_step = None
293
- for step in steps:
294
- step_name = step["name"]
295
- 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"]
296
303
 
297
- if step_status == "completed" and step_name not in completed_steps:
298
- 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}")
299
308
 
300
- sys.stdout.write(f"\r{GREEN}✔{RESET} [Cloud] {step_name}... {GREEN}done!{RESET} \n")
301
- sys.stdout.flush()
302
-
303
309
  if step.get("conclusion") == "failure":
304
- print_error(f"\n❌ Cloud Build Failed at step: {step_name}")
305
-
306
- try:
307
- log_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/jobs/{active_job['id']}/logs"
308
- log_res = requests.get(log_url, headers=headers)
309
- if log_res.status_code == 200:
310
- friendly_msg = get_friendly_error(log_res.text)
311
- print(f"\n{YELLOW}💡 AppForge Diagnosis:{RESET}\n{friendly_msg}\n")
312
- except:
313
- pass
310
+ print_error(f"Cloud Build Failed at: {step['name']}")
314
311
  return
315
- if step.get("conclusion") == "failure":
316
- print_error(f"\n❌ Cloud Build Failed at step: {step_name}")
317
- return
318
-
319
- if step_status == "in_progress":
320
- new_running_step = step_name
321
-
322
- # Update the current step for the animation loop
323
- current_running_step = new_running_step
324
312
 
325
- # 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
326
321
  if active_job["status"] == "completed":
327
- print("-" * 40)
328
-
322
+ sys.stdout.write("\r" + " " * 60 + "\r")
329
323
  if active_job["conclusion"] == "success":
330
- # 1. JOB SUCCESSFUL? Check if the APK actually exists!
331
- artifacts_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/runs/{run_id}/artifacts"
332
- art_res = requests.get(artifacts_url, headers=headers)
333
-
334
- if art_res.status_code == 200 and art_res.json().get("total_count", 0) > 0:
335
- # EVERYTHING IS PERFECT
336
- print_success(f"{BOLD}✨ Cloud Build Finished Successfully!{RESET}")
337
- print_info("Run 'appforge download' to get your app.")
338
- else:
339
- # SOFT FAILURE: Success but no file!
340
- print_warning(f"\n⚠ Build finished, but NO ARTIFACT (APK) was produced.")
341
- # Fetch logs to find out why
342
- try:
343
- log_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/jobs/{active_job['id']}/logs"
344
- log_res = requests.get(log_url, headers=headers)
345
- friendly_msg = get_friendly_error(log_res.text)
346
- print(f"\n{YELLOW}💡 AppForge Diagnosis:{RESET}\n{friendly_msg}\n")
347
- except:
348
- 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.")
349
326
  else:
350
- # HARD FAILURE: Job crashed
351
- print_error("\n❌ Cloud Build Crashed.")
352
- # Fetch logs for hard failure
353
- try:
354
- log_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/jobs/{active_job['id']}/logs"
355
- log_res = requests.get(log_url, headers=headers)
356
- friendly_msg = get_friendly_error(log_res.text)
357
- print(f"\n{YELLOW}💡 AppForge Diagnosis:{RESET}\n{friendly_msg}\n")
358
- except: pass
327
+ print_error("Cloud Build Crashed.")
359
328
  break
360
329
 
330
+ time.sleep(1.5)
331
+
361
332
  except KeyboardInterrupt:
362
- # Clear the spinner line if user hits Ctrl+C
363
- sys.stdout.write("\r \r")
364
- print(f"\n{YELLOW}⚠ Stopped watching logs. The build is still running in the cloud.{RESET}")
365
- 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.")
366
335
 
367
336
  def get_build_status_by_id(run_id):
368
337
  headers = get_headers()
@@ -381,7 +350,7 @@ def get_build_status_by_id(run_id):
381
350
 
382
351
  def download_apk(run_id=None):
383
352
  """
384
- Finds and downloads ALL artifacts (Android, iOS, Windows, Linux) for a build.
353
+ Finds and downloads ALL artifacts for a build.
385
354
  """
386
355
  headers = get_headers()
387
356
  app_id = get_app_id()
@@ -396,55 +365,49 @@ def download_apk(run_id=None):
396
365
 
397
366
  if res.status_code == 200:
398
367
  artifacts = res.json().get("artifacts", [])
399
- if not artifacts:
400
- print_error("No artifacts found. The build may have failed or is still in progress.")
401
- return
402
-
403
- # --- NEW: FIND ALL MATCHING ARTIFACTS ---
404
- # Filter artifacts that belong to this specific App ID
405
- 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", "")]
406
371
 
407
- if not target_artifacts:
372
+ if not my_artifacts:
408
373
  print_error(f"Could not find any recent artifacts for App ID: {app_id}")
409
374
  return
410
375
 
411
- 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)
412
387
 
413
- # --- MULTI-DOWNLOAD LOOP ---
414
- for target_artifact in target_artifacts:
415
- artifact_name = target_artifact['name']
416
- 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"
417
391
 
418
- secure_api_url = target_artifact.get('archive_download_url')
419
- redirect_res = requests.get(secure_api_url, headers=headers, allow_redirects=False)
420
-
421
- if redirect_res.status_code == 302:
422
- public_download_url = redirect_res.headers['Location']
423
- filename = f"{artifact_name}.zip"
424
-
425
- try:
426
- # Start a streaming download
427
- with requests.get(public_download_url, stream=True) as r:
428
- r.raise_for_status()
429
- total_size = int(r.headers.get('content-length', 0))
430
- downloaded = 0
431
-
432
- with open(filename, 'wb') as f:
433
- # Download in 8KB chunks
434
- for chunk in r.iter_content(chunk_size=8192):
435
- if chunk: # filter out keep-alive new chunks
436
- f.write(chunk)
437
- downloaded += len(chunk)
438
- percentage = (downloaded / total_size * 100) if total_size > 0 else 0
439
- print_progress_bar(percentage, f"Downloading {artifact_name}")
440
-
441
- print_progress_bar(100.0, f"Downloading {artifact_name}")
442
- print_success(f"Saved: {os.path.join(os.getcwd(), filename)}\n")
443
-
444
- except Exception as e:
445
- print_error(f"Download failed for {artifact_name}: {e}")
446
- else:
447
- 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}")
448
411
 
449
412
  else:
450
413
  print_error(f"Failed to fetch artifacts. Status Code: {res.status_code}")
@@ -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.6"
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.6
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.6"
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.6",
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": [
File without changes