appforge-cli 1.5.0__tar.gz → 1.6.0__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.
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/PKG-INFO +1 -1
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/appforge/capacitor.py +18 -0
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/appforge/cli.py +32 -24
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/appforge/create.py +10 -0
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/appforge/github.py +50 -40
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/appforge/utils.py +2 -1
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/appforge_cli.egg-info/PKG-INFO +1 -1
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/pyproject.toml +1 -1
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/setup.py +1 -1
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/appforge/__init__.py +0 -0
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/appforge/ai.py +0 -0
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/appforge/config.py +0 -0
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/appforge/default_icon.png +0 -0
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/appforge/detector.py +0 -0
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/appforge/injector.py +0 -0
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/appforge/knowledge_base.json +0 -0
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/appforge_cli.egg-info/SOURCES.txt +0 -0
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/appforge_cli.egg-info/dependency_links.txt +0 -0
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/appforge_cli.egg-info/entry_points.txt +0 -0
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/appforge_cli.egg-info/requires.txt +0 -0
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/appforge_cli.egg-info/top_level.txt +0 -0
- {appforge_cli-1.5.0 → appforge_cli-1.6.0}/setup.cfg +0 -0
|
@@ -35,6 +35,24 @@ 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"]):
|
|
39
57
|
if not os.path.exists("package.json"):
|
|
40
58
|
run_with_spinner(lambda: run_cmd("npm init -y"), "Initializing npm project")
|
|
@@ -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
|
|
41
|
-
if platform_choice == "both"
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
#
|
|
394
|
-
#
|
|
395
|
-
|
|
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
|
|
398
|
-
print_error(f"Could not find
|
|
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
|
-
|
|
402
|
-
|
|
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
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
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
|
-
|
|
441
|
+
print_error(f"Failed to fetch artifacts. Status Code: {res.status_code}")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|