appforge-cli 1.1.4__tar.gz → 1.5.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/PKG-INFO +16 -0
- appforge_cli-1.5.0/appforge/ai.py +75 -0
- {appforge_cli-1.1.4 → appforge_cli-1.5.0}/appforge/capacitor.py +13 -3
- appforge_cli-1.5.0/appforge/cli.py +387 -0
- appforge_cli-1.5.0/appforge/config.py +52 -0
- appforge_cli-1.5.0/appforge/create.py +96 -0
- appforge_cli-1.5.0/appforge/default_icon.png +0 -0
- appforge_cli-1.5.0/appforge/github.py +431 -0
- appforge_cli-1.5.0/appforge/injector.py +125 -0
- appforge_cli-1.5.0/appforge/knowledge_base.json +262 -0
- appforge_cli-1.5.0/appforge/utils.py +173 -0
- appforge_cli-1.5.0/appforge_cli.egg-info/PKG-INFO +16 -0
- {appforge_cli-1.1.4 → appforge_cli-1.5.0}/appforge_cli.egg-info/SOURCES.txt +3 -1
- appforge_cli-1.5.0/appforge_cli.egg-info/requires.txt +2 -0
- {appforge_cli-1.1.4 → appforge_cli-1.5.0}/pyproject.toml +2 -1
- {appforge_cli-1.1.4 → appforge_cli-1.5.0}/setup.py +7 -6
- appforge_cli-1.1.4/PKG-INFO +0 -280
- appforge_cli-1.1.4/README.md +0 -264
- appforge_cli-1.1.4/appforge/ai.py +0 -42
- appforge_cli-1.1.4/appforge/cli.py +0 -192
- appforge_cli-1.1.4/appforge/config.py +0 -26
- appforge_cli-1.1.4/appforge/github.py +0 -207
- appforge_cli-1.1.4/appforge/knowledge_base.json +0 -181
- appforge_cli-1.1.4/appforge/utils.py +0 -100
- appforge_cli-1.1.4/appforge_cli.egg-info/PKG-INFO +0 -280
- appforge_cli-1.1.4/appforge_cli.egg-info/requires.txt +0 -1
- {appforge_cli-1.1.4 → appforge_cli-1.5.0}/appforge/__init__.py +0 -0
- {appforge_cli-1.1.4 → appforge_cli-1.5.0}/appforge/detector.py +0 -0
- {appforge_cli-1.1.4 → appforge_cli-1.5.0}/appforge_cli.egg-info/dependency_links.txt +0 -0
- {appforge_cli-1.1.4 → appforge_cli-1.5.0}/appforge_cli.egg-info/entry_points.txt +0 -0
- {appforge_cli-1.1.4 → appforge_cli-1.5.0}/appforge_cli.egg-info/top_level.txt +0 -0
- {appforge_cli-1.1.4 → appforge_cli-1.5.0}/setup.cfg +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: appforge-cli
|
|
3
|
+
Version: 1.5.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
|
+
Requires-Dist: packaging
|
|
16
|
+
Dynamic: author
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import re
|
|
4
|
+
from .utils import print_info, print_success, print_error, CYAN, RESET, GRAY
|
|
5
|
+
|
|
6
|
+
def load_knowledge_base():
|
|
7
|
+
"""Loads the AI keyword-to-plugin mappings from the JSON file."""
|
|
8
|
+
try:
|
|
9
|
+
# __file__ gives the path to the current python script (ai.py)
|
|
10
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
11
|
+
json_path = os.path.join(current_dir, 'knowledge_base.json')
|
|
12
|
+
with open(json_path, 'r') as f:
|
|
13
|
+
return json.load(f)
|
|
14
|
+
except Exception as e:
|
|
15
|
+
# Note: print_error must be imported from utils
|
|
16
|
+
print_error(f"Critical Error: Could not load AI knowledge base: {e}")
|
|
17
|
+
return {}
|
|
18
|
+
|
|
19
|
+
def scan_codebase_for_permissions():
|
|
20
|
+
"""
|
|
21
|
+
Universally scans project files. It now uses regex to robustly parse
|
|
22
|
+
Flutter pubspec.yaml files and scans web directories like 'www'.
|
|
23
|
+
"""
|
|
24
|
+
ai_knowledge_base = load_knowledge_base()
|
|
25
|
+
|
|
26
|
+
print_info(f"{CYAN}🤖 AI Agent scanning codebase for native features...{RESET}")
|
|
27
|
+
|
|
28
|
+
found_plugins = {}
|
|
29
|
+
pubspec_already_scanned = False
|
|
30
|
+
|
|
31
|
+
for root, dirs, files in os.walk('.'):
|
|
32
|
+
# FIXED: Removed 'www' from the ignore list so your HTML files are scanned!
|
|
33
|
+
dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', 'android', 'ios', 'dist', 'build', '.next', '.nuxt', '.dart_tool']]
|
|
34
|
+
|
|
35
|
+
for file in files:
|
|
36
|
+
file_path = os.path.join(root, file)
|
|
37
|
+
|
|
38
|
+
# --- FLUTTER SCANNER ---
|
|
39
|
+
if file == 'pubspec.yaml' and not pubspec_already_scanned:
|
|
40
|
+
try:
|
|
41
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
42
|
+
content = f.read()
|
|
43
|
+
|
|
44
|
+
if 'workspace:' in content:
|
|
45
|
+
print_info(f"Ignoring Flutter workspace file: {GRAY}{file_path}{RESET}")
|
|
46
|
+
continue
|
|
47
|
+
|
|
48
|
+
print_info(f"Analyzing dependencies in {GRAY}{file_path}{RESET}")
|
|
49
|
+
for keyword, data in ai_knowledge_base.items():
|
|
50
|
+
if keyword.startswith("pubspec:"):
|
|
51
|
+
package_name = keyword.split(":")[1]
|
|
52
|
+
|
|
53
|
+
# FIXED: Using fr"" (Raw F-String) to stop the SyntaxWarning
|
|
54
|
+
pattern = re.compile(fr"^\s*{re.escape(package_name)}:", re.MULTILINE)
|
|
55
|
+
|
|
56
|
+
if pattern.search(content):
|
|
57
|
+
found_plugins[data["plugin"]] = data
|
|
58
|
+
|
|
59
|
+
pubspec_already_scanned = True
|
|
60
|
+
except Exception:
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
# --- WEB SCANNER (index.html, etc.) ---
|
|
64
|
+
elif file.endswith(('.js', '.ts', '.jsx', '.tsx', '.vue', '.svelte', '.html')):
|
|
65
|
+
try:
|
|
66
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
67
|
+
content = f.read()
|
|
68
|
+
for keyword, data in ai_knowledge_base.items():
|
|
69
|
+
if not keyword.startswith("pubspec:"):
|
|
70
|
+
if keyword in content and data["plugin"] not in found_plugins:
|
|
71
|
+
found_plugins[data["plugin"]] = data
|
|
72
|
+
except Exception:
|
|
73
|
+
pass
|
|
74
|
+
|
|
75
|
+
return found_plugins
|
|
@@ -6,10 +6,20 @@ from .utils import print_success, print_error, print_info, run_with_spinner
|
|
|
6
6
|
|
|
7
7
|
def run_cmd(command, hide_output=True):
|
|
8
8
|
try:
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
if hide_output:
|
|
10
|
+
# Capture both stdout and stderr to hide them unless there's an error
|
|
11
|
+
result = subprocess.run(
|
|
12
|
+
command, shell=True, check=True, capture_output=True, text=True
|
|
13
|
+
)
|
|
14
|
+
else:
|
|
15
|
+
# Stream output directly to terminal
|
|
16
|
+
subprocess.run(command, shell=True, check=True)
|
|
11
17
|
except subprocess.CalledProcessError as e:
|
|
12
|
-
|
|
18
|
+
# If the command fails, print the actual error from the command!
|
|
19
|
+
error_message = f"Command '{command}' failed."
|
|
20
|
+
# The real error from npm/git/etc. will be in stderr
|
|
21
|
+
if e.stderr:
|
|
22
|
+
error_message += f"\n\n--- Error Details ---\n{e.stderr.strip()}\n---------------------"
|
|
13
23
|
print_error(error_message)
|
|
14
24
|
|
|
15
25
|
|
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import argparse
|
|
4
|
+
import shutil
|
|
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,
|
|
7
|
+
cursor_up, clear_from_cursor)
|
|
8
|
+
from .detector import detect_project
|
|
9
|
+
from .capacitor import setup_capacitor, apply_configuration
|
|
10
|
+
from .config import save_local_config, load_local_config, load_history, get_app_id
|
|
11
|
+
from .github import push_and_build, check_status as check_by_id, download_apk as download_by_id, check_status, download_apk, get_build_status_by_id
|
|
12
|
+
import time
|
|
13
|
+
from .ai import scan_codebase_for_permissions
|
|
14
|
+
from .capacitor import install_plugins
|
|
15
|
+
from .create import create_project
|
|
16
|
+
|
|
17
|
+
def init_project():
|
|
18
|
+
project_info = detect_project()
|
|
19
|
+
category = project_info.get("category", "web")
|
|
20
|
+
proj_type = project_info.get("type", "unknown")
|
|
21
|
+
default_icon_dest = os.path.join(os.getcwd(), "appforge_icon.png")
|
|
22
|
+
|
|
23
|
+
current_config = load_local_config()
|
|
24
|
+
if not current_config.get("iconPath") or not os.path.exists(current_config.get("iconPath")):
|
|
25
|
+
try:
|
|
26
|
+
cli_dir = os.path.dirname(os.path.abspath(__file__))
|
|
27
|
+
bundled_icon = os.path.join(cli_dir, "default_icon.png")
|
|
28
|
+
|
|
29
|
+
if os.path.exists(bundled_icon):
|
|
30
|
+
shutil.copy2(bundled_icon, default_icon_dest)
|
|
31
|
+
save_local_config({"iconPath": default_icon_dest})
|
|
32
|
+
print_info("Installed default AppForge app icon.")
|
|
33
|
+
except Exception as e:
|
|
34
|
+
pass
|
|
35
|
+
|
|
36
|
+
save_local_config({"category": category, "project_type": proj_type})
|
|
37
|
+
|
|
38
|
+
if category == "web":
|
|
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
|
+
|
|
45
|
+
save_local_config({"webDir": web_dir, "platform": platform_choice})
|
|
46
|
+
setup_capacitor(web_dir, platforms)
|
|
47
|
+
else:
|
|
48
|
+
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()
|
|
50
|
+
save_local_config({"platform": platform_choice})
|
|
51
|
+
|
|
52
|
+
print("")
|
|
53
|
+
detected_features = scan_codebase_for_permissions()
|
|
54
|
+
|
|
55
|
+
if detected_features:
|
|
56
|
+
print_success(f"AI detected {len(detected_features)} native features required by your app:")
|
|
57
|
+
|
|
58
|
+
for data in detected_features.values():
|
|
59
|
+
print(f" {CYAN}●{RESET} {data['feature']} {GRAY}({data['desc']}){RESET}")
|
|
60
|
+
|
|
61
|
+
print_info("The AppForge Cloud Server will automatically inject these permissions during the build process.")
|
|
62
|
+
else:
|
|
63
|
+
print_info("AI scan complete. No special native permissions detected.")
|
|
64
|
+
try:
|
|
65
|
+
import subprocess
|
|
66
|
+
git_root = subprocess.check_output(
|
|
67
|
+
['git', 'rev-parse', '--show-toplevel'],
|
|
68
|
+
text=True,
|
|
69
|
+
stderr=subprocess.DEVNULL
|
|
70
|
+
).strip()
|
|
71
|
+
|
|
72
|
+
sub_path = os.path.relpath(os.getcwd(), git_root)
|
|
73
|
+
|
|
74
|
+
if sub_path and sub_path != '.':
|
|
75
|
+
print_info(f"Detected sub-project path: {GRAY}{sub_path}{RESET}")
|
|
76
|
+
save_local_config({"sub_path": sub_path})
|
|
77
|
+
|
|
78
|
+
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
79
|
+
save_local_config({"sub_path": "."})
|
|
80
|
+
print_success("Ready to send to the AppForge Cloud Builder.")
|
|
81
|
+
|
|
82
|
+
def configure_project():
|
|
83
|
+
config = load_local_config()
|
|
84
|
+
print_info("Interactive Configuration")
|
|
85
|
+
app_name = prompt("App name", config.get("appName", "My App"))
|
|
86
|
+
package_id = prompt("Package ID", config.get("packageId", "com.example.app"))
|
|
87
|
+
build_mode = prompt("Default Build Mode (debug/release)", config.get("buildMode", "debug")).lower()
|
|
88
|
+
if build_mode not in ['debug', 'release']: build_mode = 'debug'
|
|
89
|
+
current_icon = config.get("iconPath", "")
|
|
90
|
+
icon_prompt = prompt("Icon path (local PNG)", current_icon)
|
|
91
|
+
|
|
92
|
+
# If they typed a new path, use it. Otherwise, keep the old one.
|
|
93
|
+
final_icon_path = icon_prompt if icon_prompt else current_icon
|
|
94
|
+
|
|
95
|
+
splash_img = prompt("Splash image (local PNG)", config.get("splashImg", ""))
|
|
96
|
+
splash_bg = prompt("Splash background color (HEX)", config.get("splashBg", "#ffffff"))
|
|
97
|
+
|
|
98
|
+
new_config = {
|
|
99
|
+
"appName": app_name,
|
|
100
|
+
"packageId": package_id,
|
|
101
|
+
"buildMode": build_mode,
|
|
102
|
+
"iconPath": final_icon_path,
|
|
103
|
+
"iconPath": icon_path,
|
|
104
|
+
"splashImg": splash_img,
|
|
105
|
+
"splashBg": splash_bg
|
|
106
|
+
}
|
|
107
|
+
save_local_config(new_config)
|
|
108
|
+
|
|
109
|
+
web_dir = config.get("webDir", "dist")
|
|
110
|
+
if config.get("category") == "web":
|
|
111
|
+
apply_configuration(app_name, package_id, final_icon_path, splash_img, splash_bg, web_dir)
|
|
112
|
+
return new_config
|
|
113
|
+
|
|
114
|
+
def display_app_details(config):
|
|
115
|
+
print(f"{BOLD}App Configuration:{RESET}")
|
|
116
|
+
print(f" {GRAY}Name:{RESET} {config.get('appName', 'Not set')}")
|
|
117
|
+
print(f" {GRAY}Package ID:{RESET} {config.get('packageId', 'Not set')}")
|
|
118
|
+
print(f" {GRAY}Default Mode:{RESET} {config.get('buildMode', 'debug').upper()}")
|
|
119
|
+
print(f" {GRAY}Web Directory:{RESET} {config.get('webDir', 'Not set')}")
|
|
120
|
+
print(f" {GRAY}Icon:{RESET} {config.get('iconPath', 'Not set')}")
|
|
121
|
+
print(f" {GRAY}Splash Image:{RESET} {config.get('splashImg', 'Not set')}")
|
|
122
|
+
print(f" {GRAY}Splash Color:{RESET} {config.get('splashBg', 'Not set')}")
|
|
123
|
+
print("-" * 30)
|
|
124
|
+
|
|
125
|
+
def build_app():
|
|
126
|
+
config = load_local_config()
|
|
127
|
+
if not config:
|
|
128
|
+
print_error("Project not initialized. Run 'appforge init' first.")
|
|
129
|
+
|
|
130
|
+
app_name = config.get("appName")
|
|
131
|
+
package_id = config.get("packageId")
|
|
132
|
+
platform = config.get("platform", "android")
|
|
133
|
+
config_was_updated = False
|
|
134
|
+
|
|
135
|
+
if not app_name or not package_id:
|
|
136
|
+
print_info("Some required app details are missing. Let's set them up.")
|
|
137
|
+
new_app_name = prompt("App name", app_name or "My App")
|
|
138
|
+
new_package_id = prompt("Package ID", package_id or "com.example.app")
|
|
139
|
+
new_platform = prompt("Platform (android, ios, both)", platform)
|
|
140
|
+
|
|
141
|
+
config['appName'] = new_app_name
|
|
142
|
+
config['packageId'] = new_package_id
|
|
143
|
+
config['platform'] = new_platform.lower()
|
|
144
|
+
config_was_updated = True
|
|
145
|
+
|
|
146
|
+
current_version_str = config.get("version", "1.0.0")
|
|
147
|
+
try:
|
|
148
|
+
parts = current_version_str.split('.')
|
|
149
|
+
parts[-1] = str(int(parts[-1]) + 1)
|
|
150
|
+
new_version_str = '.'.join(parts)
|
|
151
|
+
except:
|
|
152
|
+
new_version_str = "1.0.1"
|
|
153
|
+
|
|
154
|
+
print_info(f"Preparing build for version: {CYAN}{new_version_str}{RESET}")
|
|
155
|
+
build_type = config.get("buildMode")
|
|
156
|
+
|
|
157
|
+
if not build_type:
|
|
158
|
+
choice = prompt("Build for Production Release? (Y/n - 'n' builds Debug)", default="y").lower()
|
|
159
|
+
build_type = "release" if choice in ['y', 'yes', ''] else "debug"
|
|
160
|
+
# Save this choice so we don't have to ask again next time
|
|
161
|
+
save_local_config({"buildMode": build_type})
|
|
162
|
+
|
|
163
|
+
# Update the working config object
|
|
164
|
+
config['build_type'] = build_type
|
|
165
|
+
config['version'] = new_version_str
|
|
166
|
+
|
|
167
|
+
if config_was_updated:
|
|
168
|
+
save_local_config(config)
|
|
169
|
+
print_success("App details updated.\n")
|
|
170
|
+
|
|
171
|
+
print_info("Ready to build with the following configuration:")
|
|
172
|
+
display_app_details(config)
|
|
173
|
+
print(f" {GRAY}Target Platform:{RESET} {config.get('platform', 'android')}")
|
|
174
|
+
print(f" {GRAY}Next Version:{RESET} {new_version_str}")
|
|
175
|
+
# Show the user what they selected
|
|
176
|
+
color = GREEN if build_type == "release" else YELLOW
|
|
177
|
+
print(f" {GRAY}Build Mode:{RESET} {color}{build_type.upper()}{RESET}\n")
|
|
178
|
+
|
|
179
|
+
# Ask ONLY ONCE if they want to build
|
|
180
|
+
action = prompt("Proceed with build? (Y/n)", default="y").lower()
|
|
181
|
+
|
|
182
|
+
if action in ['y', 'yes', '']:
|
|
183
|
+
# 1. Save new version to local memory
|
|
184
|
+
save_local_config({"version": new_version_str})
|
|
185
|
+
|
|
186
|
+
# 2. Sync CLI memory to Capacitor native config (if applicable)
|
|
187
|
+
if config.get("category") == "web":
|
|
188
|
+
apply_configuration(
|
|
189
|
+
config.get('appName', 'My App'),
|
|
190
|
+
config.get('packageId', 'com.example.app'),
|
|
191
|
+
config.get('iconPath'),
|
|
192
|
+
config.get('splashImg'),
|
|
193
|
+
config.get('splashBg'),
|
|
194
|
+
config.get('webDir', 'dist')
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
# 3. Zip and send to cloud
|
|
198
|
+
push_and_build(config)
|
|
199
|
+
print_success("Your app is now building in the cloud!")
|
|
200
|
+
|
|
201
|
+
# 4. Ask to watch logs
|
|
202
|
+
watch = prompt("Watch live build logs? (Y/n)", default="y").lower()
|
|
203
|
+
if watch in ['y', 'yes', '']:
|
|
204
|
+
status_check()
|
|
205
|
+
else:
|
|
206
|
+
print_info("Run 'appforge status' later to check its progress.")
|
|
207
|
+
|
|
208
|
+
else:
|
|
209
|
+
print(f"\n{GREEN}Run 'appforge configure' to edit App configuration{RESET}")
|
|
210
|
+
print("\n")
|
|
211
|
+
print_info("Build cancelled.")
|
|
212
|
+
|
|
213
|
+
def show_history():
|
|
214
|
+
"""Displays a list of past builds with their current cloud status."""
|
|
215
|
+
history = load_history()
|
|
216
|
+
if not history:
|
|
217
|
+
print_info("No build history found. Run 'appforge build' to create your first app!")
|
|
218
|
+
return
|
|
219
|
+
|
|
220
|
+
print(f"\n{BOLD}Your Recent AppForge Builds:{RESET}")
|
|
221
|
+
print(f" {GRAY}{'ID':<10} {'PROJECT':<20} {'PLATFORM':<10} {'TRIGGERED (UTC)':<22} {'STATUS'}{RESET}")
|
|
222
|
+
print("-" * 80)
|
|
223
|
+
|
|
224
|
+
entries_to_display = history[:10]
|
|
225
|
+
|
|
226
|
+
print_info("Fetching latest statuses from the cloud...\n")
|
|
227
|
+
|
|
228
|
+
for entry in entries_to_display:
|
|
229
|
+
from datetime import datetime
|
|
230
|
+
|
|
231
|
+
dt_obj = datetime.fromisoformat(entry['triggered_at'].replace('Z', '+00:00'))
|
|
232
|
+
formatted_time = dt_obj.strftime('%Y-%m-%d %H:%M:%S')
|
|
233
|
+
|
|
234
|
+
run_id = entry.get("run_id")
|
|
235
|
+
if run_id:
|
|
236
|
+
status, conclusion = get_build_status_by_id(run_id)
|
|
237
|
+
else:
|
|
238
|
+
status, conclusion = entry.get("status", "unknown"), entry.get("conclusion", None)
|
|
239
|
+
status_text = ""
|
|
240
|
+
if status in ["in_progress", "queued"]:
|
|
241
|
+
# Static yellow text instead of animation
|
|
242
|
+
status_text = f"{YELLOW}▶ Building...{RESET}"
|
|
243
|
+
elif status == "completed":
|
|
244
|
+
if conclusion == "success":
|
|
245
|
+
status_text = f"{GREEN}✔ Success{RESET}"
|
|
246
|
+
else:
|
|
247
|
+
status_text = f"{RED}✖ Failed{RESET}"
|
|
248
|
+
else:
|
|
249
|
+
status_text = f"{GRAY}{(status or 'Unknown').capitalize()}{RESET}"
|
|
250
|
+
|
|
251
|
+
# Print the row
|
|
252
|
+
print(f" {CYAN}{entry['app_id']:<9}{RESET} {entry['project_name']:<20} {entry['platform']:<10} {formatted_time:<22} {status_text}")
|
|
253
|
+
|
|
254
|
+
print("-" * 80)
|
|
255
|
+
|
|
256
|
+
choice = prompt("Enter a number or App ID to re-download, or press Enter to exit").strip().lower()
|
|
257
|
+
|
|
258
|
+
selected_entry = None
|
|
259
|
+
|
|
260
|
+
if choice.isdigit() and 0 < int(choice) <= len(entries_to_display):
|
|
261
|
+
selected_index = int(choice) - 1
|
|
262
|
+
selected_entry = entries_to_display[selected_index]
|
|
263
|
+
|
|
264
|
+
elif choice:
|
|
265
|
+
selected_entry = next((entry for entry in history if entry['app_id'].lower().startswith(choice)), None)
|
|
266
|
+
|
|
267
|
+
if selected_entry:
|
|
268
|
+
run_id_to_download = selected_entry.get("run_id")
|
|
269
|
+
|
|
270
|
+
if run_id_to_download:
|
|
271
|
+
print_info(f"Fetching artifact for Build Run ID: {run_id_to_download} (App ID: {selected_entry['app_id']})")
|
|
272
|
+
download_apk(run_id=run_id_to_download)
|
|
273
|
+
else:
|
|
274
|
+
print_warning("This is a legacy build entry without a unique Run ID. Cannot guarantee the correct artifact.")
|
|
275
|
+
|
|
276
|
+
# Case 3: User typed something, but it was invalid
|
|
277
|
+
elif choice:
|
|
278
|
+
print_warning("Invalid input. Please enter a number from the list or a valid App ID.")
|
|
279
|
+
|
|
280
|
+
def status_check():
|
|
281
|
+
check_status()
|
|
282
|
+
|
|
283
|
+
def download_app():
|
|
284
|
+
download_apk()
|
|
285
|
+
|
|
286
|
+
def show_info():
|
|
287
|
+
print(f"\n{BOLD}▲ AppForge CLI Information{RESET}")
|
|
288
|
+
print("=" * 55)
|
|
289
|
+
|
|
290
|
+
print(f"\n{CYAN}SYSTEM STATUS{RESET}")
|
|
291
|
+
print(f" {GRAY}Current Version:{RESET} v{CLI_VERSION}")
|
|
292
|
+
print(f" {GRAY}Cloud Target:{RESET} Private GitHub Actions Runner")
|
|
293
|
+
print(f" {GRAY}Connection:{RESET} Authenticated & Active")
|
|
294
|
+
print(f"\n{CYAN}UNIVERSAL ARCHITECTURE{RESET}")
|
|
295
|
+
print(f" {GREEN}✔{RESET} {BOLD}Web to Native{RESET} (React, Vite, Next.js, HTML)")
|
|
296
|
+
print(f" {GREEN}✔{RESET} {BOLD}Flutter{RESET} (Full Native Compilation)")
|
|
297
|
+
print(f" {GREEN}✔{RESET} {BOLD}Kotlin/Java{RESET} (Standard Android Studio Projects)")
|
|
298
|
+
print(f" {GREEN}✔{RESET} {BOLD}Monorepo Support{RESET} (Sub-directory execution)")
|
|
299
|
+
|
|
300
|
+
print(f"\n{CYAN}ADVANCED FEATURES{RESET}")
|
|
301
|
+
print(f" {YELLOW}✦{RESET} {BOLD}AI Permission Scanner:{RESET} Automatically detects required")
|
|
302
|
+
print(f" hardware (Camera, GPS, Bluetooth) from source code.")
|
|
303
|
+
print(f" {YELLOW}✦{RESET} {BOLD}Cloud Injector Engine:{RESET} Dynamically writes strict")
|
|
304
|
+
print(f" Android 13+ permissions and Java 8 Desugaring rules.")
|
|
305
|
+
print(f" {YELLOW}✦{RESET} {BOLD}Permanent Keystore:{RESET} Signs Release APKs securely via")
|
|
306
|
+
print(f" cloud secrets to enable seamless OTA updates.")
|
|
307
|
+
print(f" {YELLOW}✦{RESET} {BOLD}Live Telemetry:{RESET} Streams real-time GitHub Actions")
|
|
308
|
+
print(f" build logs directly to your local terminal.")
|
|
309
|
+
|
|
310
|
+
print(f"\n{CYAN}LATEST RELEASE NOTES (v{CLI_VERSION}){RESET}")
|
|
311
|
+
print(" • Added Native App Icon & Splash Screen generation.")
|
|
312
|
+
print(" • Added intelligent 'history' command with live statuses.")
|
|
313
|
+
print(" • Fixed Flutter 'app_plugin_loader' compatibility bugs.")
|
|
314
|
+
print(" • Upgraded to robust Node.js Cloud Injection script.")
|
|
315
|
+
print(" • Added 'create' command for instant project scaffolding.")
|
|
316
|
+
|
|
317
|
+
print("\n" + "=" * 55)
|
|
318
|
+
print(f" {GRAY}Run 'appforge help' to see available commands.{RESET}\n")
|
|
319
|
+
|
|
320
|
+
def show_argu():
|
|
321
|
+
print("Commands:")
|
|
322
|
+
print(" appforge init - Initialize an AppForge project")
|
|
323
|
+
print(" appforge configure - Open interactive settings")
|
|
324
|
+
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)")
|
|
326
|
+
print(" appforge build - Package project and send to build repo")
|
|
327
|
+
print(" appforge status - Check build status")
|
|
328
|
+
print(" appforge download - Download the built APK")
|
|
329
|
+
print(f" {CYAN}history{RESET} View a list of your past builds")
|
|
330
|
+
print(" appforge help - Show this help message")
|
|
331
|
+
|
|
332
|
+
def show_help():
|
|
333
|
+
"""The animated welcome sequence for the very first run."""
|
|
334
|
+
welcome_message = f"{BOLD}▲ AppForge{RESET} - The Universal App Builder"
|
|
335
|
+
|
|
336
|
+
for char in welcome_message:
|
|
337
|
+
sys.stdout.write(char)
|
|
338
|
+
sys.stdout.flush()
|
|
339
|
+
time.sleep(0.05)
|
|
340
|
+
print("\n")
|
|
341
|
+
time.sleep(0.5)
|
|
342
|
+
|
|
343
|
+
print_success("Installation complete!")
|
|
344
|
+
print_info("Welcome to the AppForge ecosystem.")
|
|
345
|
+
|
|
346
|
+
print("\nGetting Started:")
|
|
347
|
+
print(" 1. `cd` into your project directory")
|
|
348
|
+
print(" 2. Run `appforge init` to begin\n")
|
|
349
|
+
|
|
350
|
+
show_argu()
|
|
351
|
+
sys.exit(0)
|
|
352
|
+
|
|
353
|
+
def main():
|
|
354
|
+
|
|
355
|
+
print_header()
|
|
356
|
+
|
|
357
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
358
|
+
parser.add_argument("command", nargs="?", default="help", help="Command to run")
|
|
359
|
+
parser.add_argument("args", nargs=argparse.REMAINDER, help="Additional arguments")
|
|
360
|
+
|
|
361
|
+
args, unknown = parser.parse_known_args()
|
|
362
|
+
|
|
363
|
+
if unknown:
|
|
364
|
+
args.args.extend(unknown)
|
|
365
|
+
|
|
366
|
+
try:
|
|
367
|
+
if args.command == "create":
|
|
368
|
+
if not args.args or len(args.args) < 2:
|
|
369
|
+
print_error("Usage: appforge create <framework> <project-name>\nExample: appforge create flutter my_new_app")
|
|
370
|
+
|
|
371
|
+
framework = args.args[0].lower()
|
|
372
|
+
project_name = args.args[1]
|
|
373
|
+
create_project(framework, project_name)
|
|
374
|
+
|
|
375
|
+
elif args.command == "init": init_project()
|
|
376
|
+
elif args.command == "build": build_app()
|
|
377
|
+
elif args.command == "configure": configure_project()
|
|
378
|
+
elif args.command == "history": show_history()
|
|
379
|
+
elif args.command == "info": show_info()
|
|
380
|
+
elif args.command == "status": status_check()
|
|
381
|
+
elif args.command == "download": download_apk()
|
|
382
|
+
else: show_help()
|
|
383
|
+
finally:
|
|
384
|
+
print_footer()
|
|
385
|
+
|
|
386
|
+
if __name__ == "__main__":
|
|
387
|
+
main()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
import uuid
|
|
4
|
+
from .utils import print_error
|
|
5
|
+
|
|
6
|
+
LOCAL_CONFIG_FILE = ".appforge.json"
|
|
7
|
+
HISTORY_FILE = os.path.expanduser("~/.appforge_history.json")
|
|
8
|
+
|
|
9
|
+
def load_history():
|
|
10
|
+
"""Loads the global build history from the user's home directory."""
|
|
11
|
+
if not os.path.exists(HISTORY_FILE):
|
|
12
|
+
return []
|
|
13
|
+
try:
|
|
14
|
+
with open(HISTORY_FILE, "r") as f:
|
|
15
|
+
return json.load(f)
|
|
16
|
+
except (json.JSONDecodeError, FileNotFoundError):
|
|
17
|
+
return []
|
|
18
|
+
|
|
19
|
+
def save_history(history_data):
|
|
20
|
+
"""Saves the global build history."""
|
|
21
|
+
with open(HISTORY_FILE, "w") as f:
|
|
22
|
+
json.dump(history_data, f, indent=2)
|
|
23
|
+
|
|
24
|
+
def add_to_history(new_build_entry):
|
|
25
|
+
"""Adds a new build record to the history."""
|
|
26
|
+
history = load_history()
|
|
27
|
+
# Prepend the new entry to the top of the list
|
|
28
|
+
history.insert(0, new_build_entry)
|
|
29
|
+
# Keep the history from getting too long (e.g., max 50 entries)
|
|
30
|
+
if len(history) > 50:
|
|
31
|
+
history = history[:50]
|
|
32
|
+
save_history(history)
|
|
33
|
+
|
|
34
|
+
def load_local_config():
|
|
35
|
+
if not os.path.exists(LOCAL_CONFIG_FILE):
|
|
36
|
+
return {}
|
|
37
|
+
with open(LOCAL_CONFIG_FILE, "r") as f:
|
|
38
|
+
return json.load(f)
|
|
39
|
+
|
|
40
|
+
def save_local_config(data):
|
|
41
|
+
config = load_local_config()
|
|
42
|
+
config.update(data)
|
|
43
|
+
with open(LOCAL_CONFIG_FILE, "w") as f:
|
|
44
|
+
json.dump(config, f, indent=2)
|
|
45
|
+
|
|
46
|
+
def get_app_id():
|
|
47
|
+
config = load_local_config()
|
|
48
|
+
if "app_id" not in config:
|
|
49
|
+
new_id = str(uuid.uuid4())[:8] # Generate a short 8-character unique ID
|
|
50
|
+
save_local_config({"app_id": new_id})
|
|
51
|
+
return new_id
|
|
52
|
+
return config["app_id"]
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
import re
|
|
5
|
+
from .utils import print_success, print_info, print_error, prompt, print_progress_bar, CYAN, RESET, BOLD
|
|
6
|
+
|
|
7
|
+
TEMPLATES = {
|
|
8
|
+
"vite": {
|
|
9
|
+
"cmd": "npm create vite@latest {name} -- --template react-ts",
|
|
10
|
+
"desc": "React + TypeScript + Vite (Fast Web App)"
|
|
11
|
+
},
|
|
12
|
+
"flutter": {
|
|
13
|
+
"cmd": "git clone --progress https://github.com/flutter/samples.git _temp_samples && mv _temp_samples/provider_counter {name} && rm -rf _temp_samples",
|
|
14
|
+
"desc": "Official Flutter Starter App (Native)"
|
|
15
|
+
},
|
|
16
|
+
"kotlin": {
|
|
17
|
+
"cmd": "git clone --progress https://github.com/android/architecture-samples.git _temp_kt && mv _temp_kt {name} && rm -rf _temp_kt",
|
|
18
|
+
"desc": "Native Android Kotlin (Jetpack Compose)"
|
|
19
|
+
},
|
|
20
|
+
"html": {
|
|
21
|
+
"cmd": None,
|
|
22
|
+
"desc": "Plain HTML/CSS/JS (Simple Web App)"
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
def generate_html_template(project_name):
|
|
27
|
+
# This function stays exactly the same
|
|
28
|
+
os.makedirs(project_name, exist_ok=True)
|
|
29
|
+
os.makedirs(os.path.join(project_name, "www"), exist_ok=True)
|
|
30
|
+
html_content = f"""...""" # Keep your HTML content
|
|
31
|
+
with open(os.path.join(project_name, "www", "index.html"), "w") as f:
|
|
32
|
+
f.write(html_content)
|
|
33
|
+
|
|
34
|
+
def create_project(framework, project_name):
|
|
35
|
+
"""Generates a new project with a real progress bar for git clone."""
|
|
36
|
+
if framework not in TEMPLATES:
|
|
37
|
+
print_error(f"Unknown framework: {framework}.")
|
|
38
|
+
|
|
39
|
+
if os.path.exists(project_name):
|
|
40
|
+
print_error(f"Directory '{project_name}' already exists.")
|
|
41
|
+
|
|
42
|
+
template = TEMPLATES[framework]
|
|
43
|
+
print_info(f"Creating a new {BOLD}{template['desc']}{RESET} project in {CYAN}./{project_name}{RESET}...")
|
|
44
|
+
|
|
45
|
+
if framework == "html":
|
|
46
|
+
generate_html_template(project_name)
|
|
47
|
+
else:
|
|
48
|
+
# --- NEW REAL-TIME PROGRESS BAR LOGIC ---
|
|
49
|
+
cmd = template["cmd"].format(name=project_name)
|
|
50
|
+
|
|
51
|
+
try:
|
|
52
|
+
# We open a subprocess and read its stderr line by line
|
|
53
|
+
process = subprocess.Popen(
|
|
54
|
+
cmd,
|
|
55
|
+
shell=True,
|
|
56
|
+
stderr=subprocess.PIPE,
|
|
57
|
+
stdout=subprocess.DEVNULL, # Hide normal git output
|
|
58
|
+
text=True,
|
|
59
|
+
bufsize=1 # Line-buffered
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
# Regex to find the percentage in git's output
|
|
63
|
+
# It looks for "Receiving objects: 25%"
|
|
64
|
+
progress_regex = re.compile(r"(\d+)%")
|
|
65
|
+
|
|
66
|
+
# Read each line of output from the running git command
|
|
67
|
+
for line in iter(process.stderr.readline, ''):
|
|
68
|
+
match = progress_regex.search(line)
|
|
69
|
+
if match:
|
|
70
|
+
percentage = float(match.group(1))
|
|
71
|
+
print_progress_bar(percentage, f"Downloading {framework} template")
|
|
72
|
+
|
|
73
|
+
process.wait() # Wait for the command to finish
|
|
74
|
+
if process.returncode != 0:
|
|
75
|
+
raise Exception("Git clone failed.")
|
|
76
|
+
|
|
77
|
+
print_progress_bar(100.0, f"Downloading {framework} template")
|
|
78
|
+
|
|
79
|
+
# Post-clone cleanup for Flutter
|
|
80
|
+
if framework == "flutter":
|
|
81
|
+
shutil.move("_temp_samples/provider_counter", project_name)
|
|
82
|
+
shutil.rmtree("_temp_samples", ignore_errors=True)
|
|
83
|
+
shutil.rmtree(os.path.join(project_name, ".git"), ignore_errors=True)
|
|
84
|
+
|
|
85
|
+
except Exception as e:
|
|
86
|
+
# Clean up partial downloads on failure
|
|
87
|
+
if os.path.exists("_temp_samples"):
|
|
88
|
+
shutil.rmtree("_temp_samples", ignore_errors=True)
|
|
89
|
+
print_error(f"Project creation failed: {e}")
|
|
90
|
+
# --- END OF NEW LOGIC ---
|
|
91
|
+
|
|
92
|
+
print_success(f"Project '{project_name}' created successfully!")
|
|
93
|
+
print("\nNext steps:")
|
|
94
|
+
print(f" {CYAN}cd {project_name}{RESET}")
|
|
95
|
+
print(f" {CYAN}appforge init{RESET}")
|
|
96
|
+
print("")
|
|
Binary file
|