appforge-cli 1.5.0__tar.gz → 1.6.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 (22) hide show
  1. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/PKG-INFO +1 -1
  2. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/appforge/capacitor.py +19 -0
  3. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/appforge/cli.py +32 -24
  4. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/appforge/create.py +10 -0
  5. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/appforge/github.py +50 -40
  6. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/appforge/utils.py +2 -1
  7. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/appforge_cli.egg-info/PKG-INFO +1 -1
  8. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/pyproject.toml +1 -1
  9. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/setup.py +1 -1
  10. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/appforge/__init__.py +0 -0
  11. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/appforge/ai.py +0 -0
  12. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/appforge/config.py +0 -0
  13. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/appforge/default_icon.png +0 -0
  14. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/appforge/detector.py +0 -0
  15. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/appforge/injector.py +0 -0
  16. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/appforge/knowledge_base.json +0 -0
  17. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/appforge_cli.egg-info/SOURCES.txt +0 -0
  18. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/appforge_cli.egg-info/dependency_links.txt +0 -0
  19. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/appforge_cli.egg-info/entry_points.txt +0 -0
  20. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/appforge_cli.egg-info/requires.txt +0 -0
  21. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/appforge_cli.egg-info/top_level.txt +0 -0
  22. {appforge_cli-1.5.0 → appforge_cli-1.6.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: appforge-cli
3
- Version: 1.5.0
3
+ Version: 1.6.2
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,7 +35,26 @@ def generate_capacitor_config(app_name, package_id, web_dir):
35
35
  with open("capacitor.config.json", "w") as f:
36
36
  json.dump(config, f, indent=2)
37
37
 
38
+ def check_npm_installed():
39
+ """Verifies that npm is installed before attempting Capacitor setup."""
40
+ try:
41
+ # We use 'npm -v' to check if the command exists and is executable
42
+ subprocess.run(['npm', '-v'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True)
43
+ except (subprocess.CalledProcessError, FileNotFoundError):
44
+ print_error(
45
+ "Node.js and npm are required to initialize Web/Capacitor projects, but they are not installed!\n\n"
46
+ "💡 How to fix:\n"
47
+ " If you are on Termux (Android), run:\n"
48
+ " pkg install nodejs\n\n"
49
+ " If you are on Ubuntu/Debian, run:\n"
50
+ " sudo apt install nodejs npm\n\n"
51
+ " If you are on Mac, use Homebrew:\n"
52
+ " brew install node\n\n"
53
+ "After installing, run 'appforge init' again."
54
+ )
55
+
38
56
  def setup_capacitor(web_dir, platforms=["android"]):
57
+ check_npm_installed()
39
58
  if not os.path.exists("package.json"):
40
59
  run_with_spinner(lambda: run_cmd("npm init -y"), "Initializing npm project")
41
60
 
@@ -3,7 +3,7 @@ import sys
3
3
  import argparse
4
4
  import shutil
5
5
  from .utils import (print_header, print_footer, print_info, print_success, prompt, print_error, print_warning,
6
- GRAY, RESET, BOLD, CYAN, YELLOW, GREEN, RED, CLI_VERSION,
6
+ GRAY, RESET, BOLD, CYAN, YELLOW, GREEN, RED, CLI_VERSION, BLUE,
7
7
  cursor_up, clear_from_cursor)
8
8
  from .detector import detect_project
9
9
  from .capacitor import setup_capacitor, apply_configuration
@@ -25,7 +25,7 @@ def init_project():
25
25
  try:
26
26
  cli_dir = os.path.dirname(os.path.abspath(__file__))
27
27
  bundled_icon = os.path.join(cli_dir, "default_icon.png")
28
-
28
+
29
29
  if os.path.exists(bundled_icon):
30
30
  shutil.copy2(bundled_icon, default_icon_dest)
31
31
  save_local_config({"iconPath": default_icon_dest})
@@ -37,46 +37,62 @@ def init_project():
37
37
 
38
38
  if category == "web":
39
39
  web_dir = prompt("Confirm web build directory", project_info["webDir"])
40
- platform_choice = prompt("Select platform (android, ios, both)", "android").lower()
41
- if platform_choice == "both": platforms = ["android", "ios"]
42
- elif platform_choice == "ios": platforms = ["ios"]
43
- else: platforms = ["android"]
44
-
40
+ platform_choice = prompt("Select platforms (android, ios, both)", "android").lower()
41
+ if platform_choice == "both" or platform_choice == "all":
42
+ platforms = ["android", "ios"]
43
+ platform_choice = "android,ios"
44
+ elif "ios" in platform_choice:
45
+ platforms = ["ios"]
46
+ platform_choice = "ios"
47
+ else:
48
+ platforms = ["android"]
49
+ platform_choice = "android"
50
+
45
51
  save_local_config({"webDir": web_dir, "platform": platform_choice})
46
52
  setup_capacitor(web_dir, platforms)
47
53
  else:
48
54
  print_info(f"Native {proj_type} project detected. Bypassing Capacitor setup.")
49
- platform_choice = prompt("Select platform to build in cloud (android, ios, both)", "android").lower()
55
+
56
+ if proj_type == "flutter":
57
+ platform_choice = prompt("Select platforms (e.g., android, ios, windows, linux, or 'all')", "android").lower()
58
+ if platform_choice == "all" or platform_choice == "both":
59
+ platform_choice = "android,ios,windows,linux"
60
+ else:
61
+ platform_choice = prompt("Select platform to build in cloud (android)", "android").lower()
62
+ platform_choice = "android"
63
+
50
64
  save_local_config({"platform": platform_choice})
51
65
 
52
66
  print("")
53
67
  detected_features = scan_codebase_for_permissions()
54
-
68
+
55
69
  if detected_features:
56
70
  print_success(f"AI detected {len(detected_features)} native features required by your app:")
57
-
71
+
58
72
  for data in detected_features.values():
59
73
  print(f" {CYAN}●{RESET} {data['feature']} {GRAY}({data['desc']}){RESET}")
60
-
74
+
61
75
  print_info("The AppForge Cloud Server will automatically inject these permissions during the build process.")
62
76
  else:
63
77
  print_info("AI scan complete. No special native permissions detected.")
78
+
64
79
  try:
65
80
  import subprocess
66
81
  git_root = subprocess.check_output(
67
- ['git', 'rev-parse', '--show-toplevel'],
82
+ ['git', 'rev-parse', '--show-toplevel'],
68
83
  text=True,
69
84
  stderr=subprocess.DEVNULL
70
85
  ).strip()
71
-
86
+
72
87
  sub_path = os.path.relpath(os.getcwd(), git_root)
73
-
88
+
74
89
  if sub_path and sub_path != '.':
75
90
  print_info(f"Detected sub-project path: {GRAY}{sub_path}{RESET}")
76
91
  save_local_config({"sub_path": sub_path})
77
-
92
+
78
93
  except (subprocess.CalledProcessError, FileNotFoundError):
79
94
  save_local_config({"sub_path": "."})
95
+
80
96
  print_success("Ready to send to the AppForge Cloud Builder.")
81
97
 
82
98
  def configure_project():
@@ -172,18 +188,14 @@ def build_app():
172
188
  display_app_details(config)
173
189
  print(f" {GRAY}Target Platform:{RESET} {config.get('platform', 'android')}")
174
190
  print(f" {GRAY}Next Version:{RESET} {new_version_str}")
175
- # Show the user what they selected
176
191
  color = GREEN if build_type == "release" else YELLOW
177
192
  print(f" {GRAY}Build Mode:{RESET} {color}{build_type.upper()}{RESET}\n")
178
193
 
179
- # Ask ONLY ONCE if they want to build
180
194
  action = prompt("Proceed with build? (Y/n)", default="y").lower()
181
195
 
182
196
  if action in ['y', 'yes', '']:
183
- # 1. Save new version to local memory
184
197
  save_local_config({"version": new_version_str})
185
198
 
186
- # 2. Sync CLI memory to Capacitor native config (if applicable)
187
199
  if config.get("category") == "web":
188
200
  apply_configuration(
189
201
  config.get('appName', 'My App'),
@@ -194,11 +206,9 @@ def build_app():
194
206
  config.get('webDir', 'dist')
195
207
  )
196
208
 
197
- # 3. Zip and send to cloud
198
209
  push_and_build(config)
199
210
  print_success("Your app is now building in the cloud!")
200
211
 
201
- # 4. Ask to watch logs
202
212
  watch = prompt("Watch live build logs? (Y/n)", default="y").lower()
203
213
  if watch in ['y', 'yes', '']:
204
214
  status_check()
@@ -211,7 +221,6 @@ def build_app():
211
221
  print_info("Build cancelled.")
212
222
 
213
223
  def show_history():
214
- """Displays a list of past builds with their current cloud status."""
215
224
  history = load_history()
216
225
  if not history:
217
226
  print_info("No build history found. Run 'appforge build' to create your first app!")
@@ -273,7 +282,6 @@ def show_history():
273
282
  else:
274
283
  print_warning("This is a legacy build entry without a unique Run ID. Cannot guarantee the correct artifact.")
275
284
 
276
- # Case 3: User typed something, but it was invalid
277
285
  elif choice:
278
286
  print_warning("Invalid input. Please enter a number from the list or a valid App ID.")
279
287
 
@@ -322,7 +330,7 @@ def show_argu():
322
330
  print(" appforge init - Initialize an AppForge project")
323
331
  print(" appforge configure - Open interactive settings")
324
332
  print(f" {CYAN}info{RESET} - View CLI version and release notes")
325
- print(f" {CYAN}create <framework> <name>{RESET} Scaffold a new app (vite, flutter, html)")
333
+ print(f" {CYAN}create <framework> <name>{RESET} Scaffold a new app (vite, flutter, html, kotlin, win, linux), {BLUE}Ex - appforge create flutter myapp{RESET}")
326
334
  print(" appforge build - Package project and send to build repo")
327
335
  print(" appforge status - Check build status")
328
336
  print(" appforge download - Download the built APK")
@@ -20,6 +20,16 @@ TEMPLATES = {
20
20
  "html": {
21
21
  "cmd": None,
22
22
  "desc": "Plain HTML/CSS/JS (Simple Web App)"
23
+ },
24
+ "win": {
25
+ "cmd": "git clone --progress https://github.com/flutter/samples.git _temp_samples",
26
+ "desc": "Flutter Windows Desktop App (.exe)",
27
+ "base_framework": "flutter" # It uses Flutter under the hood!
28
+ },
29
+ "linux": {
30
+ "cmd": "git clone --progress https://github.com/flutter/samples.git _temp_samples",
31
+ "desc": "Flutter Linux Desktop App",
32
+ "base_framework": "flutter" # It uses Flutter under the hood!
23
33
  }
24
34
  }
25
35
 
@@ -371,61 +371,71 @@ def get_build_status_by_id(run_id):
371
371
  # --- Replace the download_apk function in github.py ---
372
372
 
373
373
  def download_apk(run_id=None):
374
+ """
375
+ Finds and downloads ALL artifacts (Android, iOS, Windows, Linux) for a build.
376
+ """
374
377
  headers = get_headers()
375
-
378
+ app_id = get_app_id()
379
+
376
380
  if run_id:
377
- # User selected a specific build from history. Find its artifact directly.
378
381
  url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/runs/{run_id}/artifacts"
379
382
  else:
380
- # Standard 'appforge download' command. Find latest for current app.
381
- app_id = get_app_id()
382
- print_info(f"Searching for latest artifact for App ID: {app_id}")
383
+ print_info(f"Searching for latest artifacts for App ID: {app_id}")
383
384
  url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/artifacts?per_page=100"
384
385
 
385
386
  res = requests.get(url, headers=headers)
386
-
387
+
387
388
  if res.status_code == 200:
388
389
  artifacts = res.json().get("artifacts", [])
389
390
  if not artifacts:
390
391
  print_error("No artifacts found. The build may have failed or is still in progress.")
391
392
  return
392
393
 
393
- # If a run_id was provided, there will only be one or two artifacts.
394
- # Otherwise, we need to find the latest one matching the current app_id.
395
- target_artifact = artifacts[0] if run_id else next((a for a in artifacts if get_app_id() in a.get("name")), None)
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", "")]
396
397
 
397
- if not target_artifact:
398
- print_error(f"Could not find a recent artifact for App ID: {get_app_id()}")
398
+ if not target_artifacts:
399
+ print_error(f"Could not find any recent artifacts for App ID: {app_id}")
399
400
  return
401
+
402
+ print_info(f"Found {len(target_artifacts)} artifact(s) for this build. Starting downloads...\n")
403
+
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}")
400
408
 
401
- print_success(f"Found artifact: {target_artifact['name']}")
402
- secure_api_url = target_artifact.get('archive_download_url')
403
- redirect_res = requests.get(secure_api_url, headers=headers, allow_redirects=False)
404
-
405
- if redirect_res.status_code == 302:
406
- public_download_url = redirect_res.headers['Location']
407
- filename = target_artifact['name'] + ".zip"
408
- try:
409
- # Start a streaming download
410
- with requests.get(public_download_url, stream=True) as r:
411
- r.raise_for_status()
412
- total_size = int(r.headers.get('content-length', 0))
413
- downloaded = 0
414
-
415
- with open(filename, 'wb') as f:
416
- # Download in 8KB chunks
417
- for chunk in r.iter_content(chunk_size=8192):
418
- f.write(chunk)
419
- downloaded += len(chunk)
420
- percentage = (downloaded / total_size * 100) if total_size > 0 else 0
421
- print_progress_bar(percentage, "Downloading")
422
-
423
- print_progress_bar(100.0, "Downloading")
424
- print_success(f"App artifact saved to: {os.path.join(os.getcwd(), filename)}")
409
+ secure_api_url = target_artifact.get('archive_download_url')
410
+ redirect_res = requests.get(secure_api_url, headers=headers, allow_redirects=False)
425
411
 
426
- except Exception as e:
427
- print_error(f"Download failed: {e}")
428
- else:
429
- print_error("Could not generate a temporary download link.")
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}.")
439
+
430
440
  else:
431
- print_error(f"Failed to fetch artifacts: {res.text}")
441
+ print_error(f"Failed to fetch artifacts. Status Code: {res.status_code}")
@@ -12,8 +12,9 @@ CYAN = "\033[36m"
12
12
  YELLOW = "\033[33m"
13
13
  RED = "\033[31m"
14
14
  GRAY = "\033[90m"
15
+ BLUE = "\033[34m"
15
16
 
16
- CLI_VERSION = "1.4.4"
17
+ CLI_VERSION = "1.6.2"
17
18
 
18
19
  def check_for_cli_updates():
19
20
  try:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: appforge-cli
3
- Version: 1.5.0
3
+ Version: 1.6.2
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>
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "appforge-cli"
7
- version = "1.5.0"
7
+ version = "1.6.2"
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.5.0",
5
+ version="1.6.2",
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"],
File without changes