appforge-cli 1.0.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.
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: appforge-cli
3
+ Version: 1.0.0
4
+ Summary: Convert web, Flutter, and native apps into Android/iOS apps automatically using cloud builds.
5
+ Author: AppForge Team
6
+ Author-email: AppForge Team <juniorsir.bot@gmail.com>
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Software Development :: Build Tools
11
+ Classifier: Environment :: Console
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: requests
15
+ Dynamic: author
File without changes
@@ -0,0 +1,58 @@
1
+ import os
2
+ from .utils import print_info, print_success, CYAN, RESET, GRAY
3
+
4
+ # Mapping keywords in JS/TS to Capacitor Plugins
5
+ AI_KNOWLEDGE_BASE = {
6
+ "navigator.geolocation": {
7
+ "plugin": "@capacitor/geolocation",
8
+ "feature": "Geolocation (GPS)",
9
+ "desc": "Found location tracking code"
10
+ },
11
+ "getUserMedia": {
12
+ "plugin": "@capacitor/camera",
13
+ "feature": "Camera / Microphone",
14
+ "desc": "Found camera/audio access code"
15
+ },
16
+ "Notification.requestPermission": {
17
+ "plugin": "@capacitor/push-notifications",
18
+ "feature": "Push Notifications",
19
+ "desc": "Found notification request code"
20
+ },
21
+ "navigator.vibrate": {
22
+ "plugin": "@capacitor/haptics",
23
+ "feature": "Haptics (Vibration)",
24
+ "desc": "Found vibration code"
25
+ },
26
+ "localStorage": {
27
+ "plugin": "@capacitor/preferences",
28
+ "feature": "Secure Storage",
29
+ "desc": "Found local storage usage"
30
+ }
31
+ }
32
+
33
+ def scan_codebase_for_permissions():
34
+ """Scans the project files to intelligently recommend native permissions."""
35
+ print_info(f"{CYAN}🤖 AI Agent scanning codebase for native features...{RESET}")
36
+
37
+ found_plugins = {}
38
+
39
+ # Walk through the project directory
40
+ for root, dirs, files in os.walk('.'):
41
+ # Ignore heavy or irrelevant folders
42
+ dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', 'android', 'ios', 'dist', 'build', '.next', '.nuxt']]
43
+
44
+ for file in files:
45
+ if file.endswith(('.js', '.ts', '.jsx', '.tsx', '.vue', '.svelte', '.html')):
46
+ file_path = os.path.join(root, file)
47
+ try:
48
+ with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
49
+ content = f.read()
50
+
51
+ # Check against our AI knowledge base
52
+ for keyword, data in AI_KNOWLEDGE_BASE.items():
53
+ if keyword in content and data["plugin"] not in found_plugins:
54
+ found_plugins[data["plugin"]] = data
55
+ except Exception:
56
+ pass
57
+
58
+ return found_plugins
@@ -0,0 +1,80 @@
1
+ import os
2
+ import json
3
+ import shutil
4
+ import subprocess
5
+ from .utils import print_success, print_error, print_info, run_with_spinner
6
+
7
+ def run_cmd(command, hide_output=True):
8
+ try:
9
+ stdout = subprocess.DEVNULL if hide_output else None
10
+ subprocess.run(command, shell=True, check=True, stdout=stdout, stderr=stdout)
11
+ except subprocess.CalledProcessError as e:
12
+ error_message = f"Command '{command}' failed. Please check your setup."
13
+ print_error(error_message)
14
+
15
+
16
+ def generate_capacitor_config(app_name, package_id, web_dir):
17
+ config = {
18
+ "appId": package_id,
19
+ "appName": app_name,
20
+ "webDir": web_dir,
21
+ "server": {
22
+ "androidScheme": "https"
23
+ }
24
+ }
25
+ with open("capacitor.config.json", "w") as f:
26
+ json.dump(config, f, indent=2)
27
+
28
+ def setup_capacitor(web_dir, platforms=["android"]):
29
+ if not os.path.exists("package.json"):
30
+ run_with_spinner(lambda: run_cmd("npm init -y"), "Initializing npm project")
31
+
32
+ run_with_spinner(lambda: run_cmd("npm install @capacitor/core"), "Installing Capacitor Core")
33
+ run_with_spinner(lambda: run_cmd("npm install -D @capacitor/cli"), "Installing Capacitor CLI")
34
+
35
+ app_name = "Generated App"
36
+ package_id = "com.appforge.generated"
37
+ generate_capacitor_config(app_name, package_id, web_dir)
38
+ print_success("Config generated (capacitor.config.json)")
39
+
40
+ if not os.path.exists(web_dir):
41
+ os.makedirs(web_dir)
42
+ if os.path.exists("index.html"):
43
+ shutil.copy("index.html", os.path.join(web_dir, "index.html"))
44
+ else:
45
+ with open(os.path.join(web_dir, "index.html"), "w") as f:
46
+ f.write("<h1>AppForge Built App</h1>")
47
+
48
+ # Add selected platforms
49
+ if "android" in platforms:
50
+ run_with_spinner(lambda: run_cmd("npm install @capacitor/android"), "Installing Android dependencies")
51
+ if not os.path.exists("android"):
52
+ run_with_spinner(lambda: run_cmd("npx cap add android", hide_output=True), "Adding native Android platform")
53
+
54
+ if "ios" in platforms:
55
+ run_with_spinner(lambda: run_cmd("npm install @capacitor/ios"), "Installing iOS dependencies")
56
+ if not os.path.exists("ios"):
57
+ run_with_spinner(lambda: run_cmd("npx cap add ios", hide_output=True), "Adding native iOS platform")
58
+
59
+ def apply_configuration(app_name, package_id, icon_path, splash_img, splash_bg, web_dir):
60
+ generate_capacitor_config(app_name, package_id, web_dir)
61
+ print_success(f"Config updated for '{app_name}'")
62
+
63
+ if icon_path and os.path.exists(icon_path):
64
+ print_success("Icon path saved")
65
+
66
+ if splash_img and os.path.exists(splash_img):
67
+ print_success("Splash image path saved")
68
+
69
+ if splash_bg:
70
+ print_success(f"Splash background color set to {splash_bg}")
71
+
72
+ def install_plugins(plugins_list):
73
+ if not plugins_list:
74
+ return
75
+
76
+ for plugin in plugins_list:
77
+ run_with_spinner(
78
+ lambda p=plugin: run_cmd(f"npm install {p}", hide_output=True),
79
+ f"Installing native plugin: {plugin}" # <-- FIXED THIS LINE
80
+ )
@@ -0,0 +1,199 @@
1
+ import sys
2
+ import argparse
3
+ from .utils import print_header, print_footer, print_info, print_success, print_error, prompt, GRAY, RESET, CYAN, BOLD
4
+ from .detector import detect_project
5
+ from .capacitor import setup_capacitor, apply_configuration, install_plugins
6
+ from .config import save_local_config, load_local_config, global_config_exists, create_global_config
7
+ from .github import push_and_build, check_status, download_apk
8
+
9
+ from .ai import scan_codebase_for_permissions
10
+
11
+ def init_project():
12
+ project_info = detect_project()
13
+ category = project_info.get("category", "web")
14
+ proj_type = project_info.get("type", "unknown")
15
+
16
+ save_local_config({"category": category, "project_type": proj_type})
17
+
18
+ if category == "web":
19
+ web_dir = prompt("Confirm web build directory", project_info["webDir"])
20
+ platform_choice = prompt("Select platform (android, ios, both)", "android").lower()
21
+ if platform_choice == "both": platforms = ["android", "ios"]
22
+ elif platform_choice == "ios": platforms = ["ios"]
23
+ else: platforms = ["android"]
24
+
25
+ save_local_config({"webDir": web_dir, "platform": platform_choice})
26
+ setup_capacitor(web_dir, platforms)
27
+
28
+ # --- AI PERMISSION SYSTEM (FIXED) ---
29
+ print("")
30
+ detected_features = scan_codebase_for_permissions()
31
+ if detected_features:
32
+ print_success(f"AI detected {len(detected_features)} native features required by your code:")
33
+
34
+ # This is the loop I accidentally deleted!
35
+ for data in detected_features.values():
36
+ print(f" {CYAN}●{RESET} {data['feature']} {GRAY}({data['desc']}){RESET}")
37
+
38
+ apply_ai = prompt("Auto-configure these native permissions?", "y").lower()
39
+ if apply_ai in ['y', 'yes']:
40
+ install_plugins(list(detected_features.keys()))
41
+ print_success("Native permissions and plugins configured securely.")
42
+ else:
43
+ print_info("Skipped auto-configuration.")
44
+ else:
45
+ print_info("AI scan complete. No special native permissions detected.")
46
+ # ------------------------------------
47
+
48
+ else:
49
+ print_info(f"Native {proj_type} project detected. Bypassing Capacitor setup.")
50
+ platform_choice = prompt("Select platform to build in cloud (android, ios, both)", "android").lower()
51
+ save_local_config({"platform": platform_choice})
52
+
53
+ print_success("Ready to send to the AppForge Cloud Builder.")
54
+
55
+ def configure_project():
56
+ config = load_local_config()
57
+ print_info("Interactive Configuration")
58
+ app_name = prompt("App name", config.get("appName", "My App"))
59
+ package_id = prompt("Package ID", config.get("packageId", "com.example.app"))
60
+ icon_path = prompt("Icon path (local PNG)", config.get("iconPath", ""))
61
+ splash_img = prompt("Splash image (local PNG)", config.get("splashImg", ""))
62
+ splash_bg = prompt("Splash background color (HEX)", config.get("splashBg", "#ffffff"))
63
+
64
+ new_config = {
65
+ "appName": app_name,
66
+ "packageId": package_id,
67
+ "iconPath": icon_path,
68
+ "splashImg": splash_img,
69
+ "splashBg": splash_bg
70
+ }
71
+ save_local_config(new_config)
72
+
73
+ web_dir = config.get("webDir", "dist")
74
+ apply_configuration(app_name, package_id, icon_path, splash_img, splash_bg, web_dir)
75
+ return new_config
76
+
77
+ def display_app_details(config):
78
+ print(f"{BOLD}App Configuration:{RESET}")
79
+ print(f" {GRAY}Name:{RESET} {config.get('appName', 'Not set')}")
80
+ print(f" {GRAY}Package ID:{RESET} {config.get('packageId', 'Not set')}")
81
+ print(f" {GRAY}Web Directory:{RESET} {config.get('webDir', 'Not set')}")
82
+ print(f" {GRAY}Icon:{RESET} {config.get('iconPath', 'Not set')}")
83
+ print(f" {GRAY}Splash Image:{RESET} {config.get('splashImg', 'Not set')}")
84
+ print(f" {GRAY}Splash Color:{RESET} {config.get('splashBg', 'Not set')}")
85
+ print("-" * 30)
86
+
87
+ def build_app():
88
+ config = load_local_config()
89
+ if not config:
90
+ print_error("Project not initialized. Run 'appforge init' first.")
91
+
92
+ app_name = config.get("appName")
93
+ package_id = config.get("packageId")
94
+ platform = config.get("platform", "android")
95
+ config_was_updated = False
96
+
97
+ if not app_name or not package_id:
98
+ print_info("Some required app details are missing. Let's set them up.")
99
+ new_app_name = prompt("App name", app_name or "My App")
100
+ new_package_id = prompt("Package ID", package_id or "com.example.app")
101
+ new_platform = prompt("Platform (android, ios, both)", platform)
102
+
103
+ config['appName'] = new_app_name
104
+ config['packageId'] = new_package_id
105
+ config['platform'] = new_platform.lower()
106
+ config_was_updated = True
107
+
108
+ if config_was_updated:
109
+ save_local_config(config)
110
+ apply_configuration(
111
+ config['appName'], config['packageId'], config.get('iconPath'),
112
+ config.get('splashImg'), config.get('splashBg'), config.get('webDir')
113
+ )
114
+ print_success("App details updated.\n")
115
+
116
+ print_info("Ready to build with the following configuration:")
117
+ display_app_details(config)
118
+ print(f" {GRAY}Target Platform:{RESET} {config.get('platform', 'android')}\n")
119
+
120
+ action = prompt("Proceed with build? (Y/n)", default="y").lower()
121
+
122
+ if action in ['y', 'yes', '']:
123
+ # Pass the platform to the push_and_build function!
124
+ push_and_build(config.get('platform', 'android'))
125
+ print_success("Your app is now building in the cloud!")
126
+ print_info("Run 'appforge status' to check its progress.")
127
+ else:
128
+ print_info("Build cancelled.")
129
+
130
+ def status_check():
131
+ check_status()
132
+
133
+ def download_app():
134
+ download_apk()
135
+
136
+ def show_help():
137
+ print("Commands:")
138
+ print(" appforge init - Initialize an AppForge project")
139
+ print(" appforge configure - Open interactive settings")
140
+ print(" appforge build - Package project and send to build repo")
141
+ print(" appforge status - Check build status")
142
+ print(" appforge download - Download the built APK")
143
+ print(" appforge help - Show this help message")
144
+
145
+ def run_first_time_setup():
146
+ """The animated welcome sequence for the very first run."""
147
+ welcome_message = f"{BOLD}▲ AppForge{RESET} - Universal App Builder"
148
+
149
+ # Typing animation
150
+ for char in welcome_message:
151
+ sys.stdout.write(char)
152
+ sys.stdout.flush()
153
+ time.sleep(0.05)
154
+ print("\n")
155
+ time.sleep(0.5)
156
+
157
+ print_success("Installation complete!")
158
+ print_info("Welcome to the AppForge ecosystem.")
159
+
160
+ # Create the global config to ensure this only runs once
161
+ create_global_config()
162
+
163
+ print("\nGetting Started:")
164
+ print(" 1. `cd` into your project directory")
165
+ print(" 2. Run `appforge init` to begin\n")
166
+
167
+ show_help()
168
+ print_footer()
169
+ sys.exit(0)
170
+
171
+ def main():
172
+
173
+ if not global_config_exists():
174
+ run_first_time_setup()
175
+
176
+ print_header()
177
+
178
+ parser = argparse.ArgumentParser(description="AppForge CLI", add_help=False)
179
+ parser.add_argument("command", nargs="?", default="help", help="Command to run")
180
+ args = parser.parse_args()
181
+
182
+ try:
183
+ if args.command == "init":
184
+ init_project()
185
+ elif args.command == "configure":
186
+ configure_project()
187
+ elif args.command == "build":
188
+ build_app()
189
+ elif args.command == "status":
190
+ status_check()
191
+ elif args.command == "download":
192
+ download_app()
193
+ else:
194
+ show_help()
195
+ finally:
196
+ print_footer()
197
+
198
+ if __name__ == "__main__":
199
+ main()
@@ -0,0 +1,35 @@
1
+ import os
2
+ import json
3
+ import uuid
4
+ from .utils import print_error
5
+
6
+ LOCAL_CONFIG_FILE = ".appforge.json"
7
+
8
+ def load_local_config():
9
+ if not os.path.exists(LOCAL_CONFIG_FILE):
10
+ return {}
11
+ with open(LOCAL_CONFIG_FILE, "r") as f:
12
+ return json.load(f)
13
+
14
+ def save_local_config(data):
15
+ config = load_local_config()
16
+ config.update(data)
17
+ with open(LOCAL_CONFIG_FILE, "w") as f:
18
+ json.dump(config, f, indent=2)
19
+
20
+ def get_app_id():
21
+ config = load_local_config()
22
+ if "app_id" not in config:
23
+ new_id = str(uuid.uuid4())[:8] # Generate a short 8-character unique ID
24
+ save_local_config({"app_id": new_id})
25
+ return new_id
26
+ return config["app_id"]
27
+
28
+ def global_config_exists():
29
+ """Checks if the global configuration file has been created."""
30
+ return os.path.exists(GLOBAL_CONFIG_FILE)
31
+
32
+ def create_global_config(initial_data={}):
33
+ """Creates the global config file, marking the first-run as complete."""
34
+ with open(GLOBAL_CONFIG_FILE, "w") as f:
35
+ json.dump(initial_data, f, indent=2)
@@ -0,0 +1,49 @@
1
+ import os
2
+ import json
3
+ from .utils import print_info, print_warning
4
+
5
+ def detect_project():
6
+ # 1. Detect Flutter
7
+ if os.path.exists("pubspec.yaml"):
8
+ print_info("Detected Flutter project")
9
+ return {"type": "flutter", "category": "native", "webDir": None}
10
+
11
+ # 2. Detect Native Android (Kotlin/Java)
12
+ if os.path.exists("build.gradle") or os.path.exists("build.gradle.kts"):
13
+ print_info("Detected Native Android (Kotlin/Java) project")
14
+ return {"type": "android_native", "category": "native", "webDir": None}
15
+
16
+ # 3. Detect Native iOS (Swift/Objective-C)
17
+ if glob_exists("*.xcodeproj") or glob_exists("*.xcworkspace"):
18
+ print_info("Detected Native iOS (Swift/Objective-C) project")
19
+ return {"type": "ios_native", "category": "native", "webDir": None}
20
+
21
+ # 4. Detect Web Frameworks (Needs Capacitor)
22
+ if not os.path.exists("package.json"):
23
+ if os.path.exists("index.html"):
24
+ print_info("Detected plain HTML/CSS project")
25
+ return {"type": "html", "category": "web", "webDir": "www"}
26
+ return {"type": "unknown", "category": "web", "webDir": "www"}
27
+
28
+ with open("package.json", "r") as f:
29
+ try: pkg = json.load(f)
30
+ except json.JSONDecodeError: return {"type": "unknown", "category": "web", "webDir": "www"}
31
+
32
+ deps = pkg.get("dependencies", {})
33
+ dev_deps = pkg.get("devDependencies", {})
34
+ all_deps = {**deps, **dev_deps}
35
+ scripts_str = json.dumps(pkg.get("scripts", {}))
36
+
37
+ if "nuxt" in all_deps: return {"type": "nuxt", "category": "web", "webDir": ".output/public"}
38
+ if "@sveltejs/kit" in all_deps: return {"type": "sveltekit", "category": "web", "webDir": "build"}
39
+ if "@angular/core" in all_deps: return {"type": "angular", "category": "web", "webDir": "dist"}
40
+ if "next" in all_deps: return {"type": "next", "category": "web", "webDir": "out"}
41
+ if "vite" in all_deps or "vite build" in scripts_str: return {"type": "vite", "category": "web", "webDir": "dist"}
42
+ if "react-scripts" in all_deps: return {"type": "react", "category": "web", "webDir": "build"}
43
+ if "typescript" in all_deps: return {"type": "ts", "category": "web", "webDir": "dist"}
44
+
45
+ return {"type": "static", "category": "web", "webDir": "build"}
46
+
47
+ def glob_exists(pattern):
48
+ import glob
49
+ return len(glob.glob(pattern)) > 0
@@ -0,0 +1,139 @@
1
+ import os
2
+ import zipfile
3
+ import requests
4
+ import base64
5
+ from .config import get_app_id, load_local_config
6
+ from .utils import print_success, print_info, print_error, run_with_spinner
7
+
8
+ GITHUB_API_URL = "https://api.github.com"
9
+ BUILD_REPO_OWNER = "juniorsir"
10
+ BUILD_REPO_NAME = "appforge-build"
11
+ MASTER_SYSTEM_TOKEN = "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" # <-- PUT YOUR TOKEN HERE
12
+
13
+ def get_headers():
14
+ return {
15
+ "Authorization": f"Bearer {MASTER_SYSTEM_TOKEN}",
16
+ "Accept": "application/vnd.github.v3+json",
17
+ "X-GitHub-Api-Version": "2022-11-28"
18
+ }
19
+
20
+ def zip_project(zip_name="app_source.zip"):
21
+ def do_zip():
22
+ with zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED) as zipf:
23
+ for root, dirs, files in os.walk('.'):
24
+ # We now ignore Flutter (.dart_tool) and Native Gradle (.gradle) caches
25
+ dirs[:] = [d for d in dirs if d not in [
26
+ 'node_modules', '.git', '.dart_tool', 'build', '.gradle', 'android', 'ios'
27
+ ]]
28
+ # IMPORTANT: We MUST allow 'android' and 'build' IF the project is natively android/flutter!
29
+ # To keep it simple, we use a basic ignore list.
30
+ for file in files:
31
+ if file == zip_name: continue
32
+ file_path = os.path.join(root, file)
33
+ zipf.write(file_path, file_path)
34
+ run_with_spinner(do_zip, "Zipping project files")
35
+ return zip_name
36
+
37
+ def push_and_build(platform="android"):
38
+ app_id = get_app_id()
39
+ zip_path = zip_project()
40
+ headers = get_headers()
41
+
42
+ def do_upload():
43
+ with open(zip_path, "rb") as f:
44
+ content_b64 = base64.b64encode(f.read()).decode("utf-8")
45
+
46
+ url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/contents/apps/{app_id}/source.zip"
47
+
48
+ # Check if exists to overwrite
49
+ sha = None
50
+ res = requests.get(url, headers=headers)
51
+ if res.status_code == 200:
52
+ sha = res.json().get("sha")
53
+
54
+ data = {
55
+ "message": f"Upload app source for {app_id}",
56
+ "content": content_b64,
57
+ "branch": "main"
58
+ }
59
+ if sha: data["sha"] = sha
60
+
61
+ put_res = requests.put(url, headers=headers, json=data)
62
+ if put_res.status_code not in [200, 201]:
63
+ raise Exception(f"Failed to upload: {put_res.json().get('message')}")
64
+
65
+ run_with_spinner(do_upload, f"Uploading app (ID: {app_id})...")
66
+ os.remove(zip_path)
67
+
68
+ def do_trigger():
69
+ config = load_local_config()
70
+ proj_category = config.get("category", "web")
71
+ proj_type = config.get("project_type", "unknown")
72
+ platform = config.get("platform", "android")
73
+
74
+ dispatch_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/workflows/build.yml/dispatches"
75
+ dispatch_data = {
76
+ "ref": "main",
77
+ "inputs": {
78
+ "app_id": app_id,
79
+ "platform": platform,
80
+ "project_category": proj_category,
81
+ "project_type": proj_type
82
+ }
83
+ }
84
+ res = requests.post(dispatch_url, headers=headers, json=dispatch_data)
85
+ if res.status_code != 204:
86
+ raise Exception(f"Failed to trigger build. Status {res.status_code}: {res.text}")
87
+
88
+ run_with_spinner(do_trigger, "Triggering cloud build...")
89
+
90
+ def check_status():
91
+ headers = get_headers()
92
+ url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/runs?per_page=5"
93
+ res = requests.get(url, headers=headers)
94
+
95
+ if res.status_code == 200:
96
+ runs = res.json().get("workflow_runs", [])
97
+ if not runs:
98
+ print_info("No builds found in the cloud yet.")
99
+ return
100
+
101
+ # Grab the most recent workflow run
102
+ latest_run = runs[0]
103
+ status = latest_run.get("status")
104
+ conclusion = latest_run.get("conclusion")
105
+
106
+ if status == "completed":
107
+ if conclusion == "success":
108
+ print_success("Build completed! APK ready.")
109
+ else:
110
+ print_error(f"Build failed with conclusion: {conclusion}")
111
+ else:
112
+ print_info(f"Build status: {status} (Please wait...)")
113
+ else:
114
+ print_error("Could not fetch build status.")
115
+
116
+ def download_apk():
117
+ app_id = get_app_id()
118
+ headers = get_headers()
119
+ url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/artifacts?per_page=100"
120
+ res = requests.get(url, headers=headers)
121
+
122
+ if res.status_code == 200:
123
+ artifacts = res.json().get("artifacts", [])
124
+ found = False
125
+
126
+ for artifact in artifacts:
127
+ name = artifact.get("name")
128
+ if name in [f"appforge-apk-{app_id}", f"appforge-ios-{app_id}"]:
129
+ found = True
130
+ print_success(f"Found artifact: {name}")
131
+ secure_api_url = artifact.get('archive_download_url')
132
+ redirect_res = requests.get(secure_api_url, headers=headers, allow_redirects=False)
133
+ if redirect_res.status_code == 302:
134
+ print_info(f"Download link (expires in 1 min):\n{redirect_res.headers['Location']}\n")
135
+
136
+ if not found:
137
+ print_error("Build not finished yet, or artifact not found.")
138
+ else:
139
+ print_error("Failed to fetch artifacts.")
@@ -0,0 +1,100 @@
1
+ import sys
2
+ import threading
3
+ import time
4
+
5
+ # ANSI Color Codes
6
+ RESET = "\033[0m"
7
+ BOLD = "\033[1m"
8
+ GREEN = "\033[32m"
9
+ CYAN = "\033[36m"
10
+ YELLOW = "\033[33m"
11
+ RED = "\033[31m"
12
+ GRAY = "\033[90m"
13
+
14
+ def print_header():
15
+ print(f"\n{BOLD}▲ AppForge{RESET}\n")
16
+
17
+ def print_footer():
18
+ print(f"\n{GRAY}AppForge CLI")
19
+ print(f"Built by AppForge Team [Maintainer - JuniorSir{RESET}\n")
20
+
21
+ def print_success(msg):
22
+ print(f"{GREEN}✔{RESET} {msg}")
23
+
24
+ def print_info(msg):
25
+ print(f"{CYAN}?{RESET} {msg}")
26
+
27
+ def print_warning(msg):
28
+ print(f"{YELLOW}⚠{RESET} {msg}")
29
+
30
+ def print_error(msg):
31
+ print(f"{RED}✖{RESET} {msg}")
32
+ sys.exit(1)
33
+
34
+ def prompt(question, default=""):
35
+ prompt_text = f"{CYAN}?{RESET} {question}"
36
+ if default:
37
+ prompt_text += f" {GRAY}({default}){RESET}"
38
+ prompt_text += ": "
39
+
40
+ try:
41
+ response = input(prompt_text).strip()
42
+ return response if response else default
43
+ except KeyboardInterrupt:
44
+ print_error("\nOperation cancelled.")
45
+
46
+ # --- NEW TYPEWRITER & SPINNER ANIMATION ---
47
+
48
+ def _spinner_animation(stop_event, message):
49
+ """Letter-by-letter typing followed by spinner."""
50
+
51
+ displayed_text = ""
52
+
53
+ # Phase 1: Letter typewriter
54
+ for char in message:
55
+ if stop_event.is_set():
56
+ break
57
+
58
+ displayed_text += char
59
+ sys.stdout.write(f"\r{CYAN}●{RESET} {displayed_text}")
60
+ sys.stdout.flush()
61
+ time.sleep(0.03) # typing speed
62
+
63
+ # Phase 2: Spinner
64
+ spinner_chars = "|/-\\"
65
+ idx = 0
66
+
67
+ while not stop_event.is_set():
68
+ char = spinner_chars[idx % len(spinner_chars)]
69
+
70
+ sys.stdout.write(f"\r{CYAN}{char}{RESET} {displayed_text}")
71
+ sys.stdout.flush()
72
+
73
+ idx += 1
74
+ time.sleep(0.1)
75
+
76
+ def run_with_spinner(target_func, message):
77
+ stop_event = threading.Event()
78
+ spinner_thread = threading.Thread(target=_spinner_animation, args=(stop_event, message))
79
+
80
+ spinner_thread.start()
81
+
82
+ try:
83
+ result = target_func()
84
+ except Exception as e:
85
+ stop_event.set()
86
+ spinner_thread.join()
87
+ # Clean failure output
88
+ sys.stdout.write(f"\r{RED}✖{RESET} {message.strip()} {GRAY}failed!{RESET} \n")
89
+ print_error(f"Error: {e}")
90
+ return None
91
+
92
+ stop_event.set()
93
+ spinner_thread.join()
94
+
95
+ # Clean success output with PERFECT spacing
96
+ # The trailing spaces ensure any leftover spinner characters are erased
97
+ sys.stdout.write(f"\r{GREEN}✔{RESET} {message.strip()} {GREEN}done!{RESET} \n")
98
+ sys.stdout.flush()
99
+
100
+ return result
@@ -0,0 +1,15 @@
1
+ Metadata-Version: 2.4
2
+ Name: appforge-cli
3
+ Version: 1.0.0
4
+ Summary: Convert web, Flutter, and native apps into Android/iOS apps automatically using cloud builds.
5
+ Author: AppForge Team
6
+ Author-email: AppForge Team <juniorsir.bot@gmail.com>
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Topic :: Software Development :: Build Tools
11
+ Classifier: Environment :: Console
12
+ Requires-Python: >=3.8
13
+ Description-Content-Type: text/markdown
14
+ Requires-Dist: requests
15
+ Dynamic: author
@@ -0,0 +1,16 @@
1
+ pyproject.toml
2
+ setup.py
3
+ appforge/__init__.py
4
+ appforge/ai.py
5
+ appforge/capacitor.py
6
+ appforge/cli.py
7
+ appforge/config.py
8
+ appforge/detector.py
9
+ appforge/github.py
10
+ appforge/utils.py
11
+ appforge_cli.egg-info/PKG-INFO
12
+ appforge_cli.egg-info/SOURCES.txt
13
+ appforge_cli.egg-info/dependency_links.txt
14
+ appforge_cli.egg-info/entry_points.txt
15
+ appforge_cli.egg-info/requires.txt
16
+ appforge_cli.egg-info/top_level.txt
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ appforge = appforge.cli:main
@@ -0,0 +1 @@
1
+ requests
@@ -0,0 +1 @@
1
+ appforge
@@ -0,0 +1,26 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "appforge-cli"
7
+ version = "1.0.0"
8
+ authors = [
9
+ { name="AppForge Team", email="juniorsir.bot@gmail.com" },
10
+ ]
11
+ description = "Convert web, Flutter, and native apps into Android/iOS apps automatically using cloud builds."
12
+ readme = "README.md"
13
+ requires-python = ">=3.8"
14
+ classifiers = [
15
+ "Programming Language :: Python :: 3",
16
+ "License :: OSI Approved :: MIT License",
17
+ "Operating System :: OS Independent",
18
+ "Topic :: Software Development :: Build Tools",
19
+ "Environment :: Console",
20
+ ]
21
+ dependencies = [
22
+ "requests",
23
+ ]
24
+
25
+ [project.scripts]
26
+ appforge = "appforge.cli:main"
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,17 @@
1
+ from setuptools import setup
2
+
3
+ setup(
4
+ name="appforge",
5
+ version="1.0.0",
6
+ description="Convert web apps into Android apps automatically using Capacitor and GitHub cloud builds.",
7
+ author="AppForge Team",
8
+ packages=["appforge"], # <-- CHANGE THIS LINE
9
+ install_requires=[
10
+ "requests"
11
+ ],
12
+ entry_points={
13
+ "console_scripts": [
14
+ "appforge=appforge.cli:main",
15
+ ],
16
+ },
17
+ )