appforge-cli 1.2.4__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.6.0/PKG-INFO +16 -0
- {appforge_cli-1.2.4 → appforge_cli-1.6.0}/appforge/ai.py +21 -24
- {appforge_cli-1.2.4 → appforge_cli-1.6.0}/appforge/capacitor.py +18 -0
- {appforge_cli-1.2.4 → appforge_cli-1.6.0}/appforge/cli.py +115 -54
- {appforge_cli-1.2.4 → appforge_cli-1.6.0}/appforge/create.py +15 -2
- appforge_cli-1.6.0/appforge/default_icon.png +0 -0
- {appforge_cli-1.2.4 → appforge_cli-1.6.0}/appforge/github.py +161 -71
- appforge_cli-1.6.0/appforge/injector.py +125 -0
- {appforge_cli-1.2.4 → appforge_cli-1.6.0}/appforge/utils.py +46 -1
- appforge_cli-1.6.0/appforge_cli.egg-info/PKG-INFO +16 -0
- {appforge_cli-1.2.4 → appforge_cli-1.6.0}/appforge_cli.egg-info/SOURCES.txt +2 -1
- appforge_cli-1.6.0/appforge_cli.egg-info/requires.txt +2 -0
- {appforge_cli-1.2.4 → appforge_cli-1.6.0}/pyproject.toml +2 -1
- {appforge_cli-1.2.4 → appforge_cli-1.6.0}/setup.py +7 -6
- appforge_cli-1.2.4/PKG-INFO +0 -280
- appforge_cli-1.2.4/README.md +0 -264
- appforge_cli-1.2.4/appforge_cli.egg-info/PKG-INFO +0 -280
- appforge_cli-1.2.4/appforge_cli.egg-info/requires.txt +0 -1
- {appforge_cli-1.2.4 → appforge_cli-1.6.0}/appforge/__init__.py +0 -0
- {appforge_cli-1.2.4 → appforge_cli-1.6.0}/appforge/config.py +0 -0
- {appforge_cli-1.2.4 → appforge_cli-1.6.0}/appforge/detector.py +0 -0
- {appforge_cli-1.2.4 → appforge_cli-1.6.0}/appforge/knowledge_base.json +0 -0
- {appforge_cli-1.2.4 → appforge_cli-1.6.0}/appforge_cli.egg-info/dependency_links.txt +0 -0
- {appforge_cli-1.2.4 → appforge_cli-1.6.0}/appforge_cli.egg-info/entry_points.txt +0 -0
- {appforge_cli-1.2.4 → appforge_cli-1.6.0}/appforge_cli.egg-info/top_level.txt +0 -0
- {appforge_cli-1.2.4 → appforge_cli-1.6.0}/setup.cfg +0 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: appforge-cli
|
|
3
|
+
Version: 1.6.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
|
|
@@ -1,69 +1,66 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import json
|
|
3
|
-
import re
|
|
3
|
+
import re
|
|
4
4
|
from .utils import print_info, print_success, print_error, CYAN, RESET, GRAY
|
|
5
5
|
|
|
6
6
|
def load_knowledge_base():
|
|
7
7
|
"""Loads the AI keyword-to-plugin mappings from the JSON file."""
|
|
8
8
|
try:
|
|
9
|
+
# __file__ gives the path to the current python script (ai.py)
|
|
9
10
|
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
10
11
|
json_path = os.path.join(current_dir, 'knowledge_base.json')
|
|
11
12
|
with open(json_path, 'r') as f:
|
|
12
13
|
return json.load(f)
|
|
13
14
|
except Exception as e:
|
|
15
|
+
# Note: print_error must be imported from utils
|
|
14
16
|
print_error(f"Critical Error: Could not load AI knowledge base: {e}")
|
|
15
17
|
return {}
|
|
16
18
|
|
|
17
19
|
def scan_codebase_for_permissions():
|
|
18
20
|
"""
|
|
19
21
|
Universally scans project files. It now uses regex to robustly parse
|
|
20
|
-
Flutter pubspec.yaml files
|
|
22
|
+
Flutter pubspec.yaml files and scans web directories like 'www'.
|
|
21
23
|
"""
|
|
22
24
|
ai_knowledge_base = load_knowledge_base()
|
|
23
|
-
|
|
25
|
+
|
|
24
26
|
print_info(f"{CYAN}🤖 AI Agent scanning codebase for native features...{RESET}")
|
|
25
|
-
|
|
27
|
+
|
|
26
28
|
found_plugins = {}
|
|
27
29
|
pubspec_already_scanned = False
|
|
28
|
-
|
|
30
|
+
|
|
29
31
|
for root, dirs, files in os.walk('.'):
|
|
30
|
-
|
|
31
|
-
|
|
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
|
+
|
|
32
35
|
for file in files:
|
|
33
36
|
file_path = os.path.join(root, file)
|
|
34
37
|
|
|
35
|
-
# --- FLUTTER SCANNER
|
|
38
|
+
# --- FLUTTER SCANNER ---
|
|
36
39
|
if file == 'pubspec.yaml' and not pubspec_already_scanned:
|
|
37
40
|
try:
|
|
38
41
|
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
39
42
|
content = f.read()
|
|
40
|
-
|
|
43
|
+
|
|
41
44
|
if 'workspace:' in content:
|
|
42
45
|
print_info(f"Ignoring Flutter workspace file: {GRAY}{file_path}{RESET}")
|
|
43
46
|
continue
|
|
44
|
-
|
|
47
|
+
|
|
45
48
|
print_info(f"Analyzing dependencies in {GRAY}{file_path}{RESET}")
|
|
46
49
|
for keyword, data in ai_knowledge_base.items():
|
|
47
50
|
if keyword.startswith("pubspec:"):
|
|
48
51
|
package_name = keyword.split(":")[1]
|
|
49
|
-
|
|
50
|
-
#
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
# \s* - any amount of whitespace (spaces, tabs)
|
|
54
|
-
# {name} - the package name
|
|
55
|
-
# : - a colon
|
|
56
|
-
pattern = re.compile(f"^\s*{re.escape(package_name)}:", re.MULTILINE)
|
|
57
|
-
|
|
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
|
+
|
|
58
56
|
if pattern.search(content):
|
|
59
57
|
found_plugins[data["plugin"]] = data
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
pubspec_already_scanned = True
|
|
58
|
+
|
|
59
|
+
pubspec_already_scanned = True
|
|
63
60
|
except Exception:
|
|
64
61
|
pass
|
|
65
62
|
|
|
66
|
-
# --- WEB SCANNER (
|
|
63
|
+
# --- WEB SCANNER (index.html, etc.) ---
|
|
67
64
|
elif file.endswith(('.js', '.ts', '.jsx', '.tsx', '.vue', '.svelte', '.html')):
|
|
68
65
|
try:
|
|
69
66
|
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
@@ -74,5 +71,5 @@ def scan_codebase_for_permissions():
|
|
|
74
71
|
found_plugins[data["plugin"]] = data
|
|
75
72
|
except Exception:
|
|
76
73
|
pass
|
|
77
|
-
|
|
74
|
+
|
|
78
75
|
return found_plugins
|
|
@@ -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")
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import sys
|
|
3
3
|
import argparse
|
|
4
|
+
import shutil
|
|
4
5
|
from .utils import (print_header, print_footer, print_info, print_success, prompt, print_error, print_warning,
|
|
5
|
-
GRAY, RESET, BOLD, CYAN, YELLOW, GREEN, RED,
|
|
6
|
+
GRAY, RESET, BOLD, CYAN, YELLOW, GREEN, RED, CLI_VERSION, BLUE,
|
|
6
7
|
cursor_up, clear_from_cursor)
|
|
7
8
|
from .detector import detect_project
|
|
8
9
|
from .capacitor import setup_capacitor, apply_configuration
|
|
@@ -17,54 +18,81 @@ def init_project():
|
|
|
17
18
|
project_info = detect_project()
|
|
18
19
|
category = project_info.get("category", "web")
|
|
19
20
|
proj_type = project_info.get("type", "unknown")
|
|
20
|
-
|
|
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
|
+
|
|
21
36
|
save_local_config({"category": category, "project_type": proj_type})
|
|
22
37
|
|
|
23
38
|
if category == "web":
|
|
24
39
|
web_dir = prompt("Confirm web build directory", project_info["webDir"])
|
|
25
|
-
platform_choice = prompt("Select
|
|
26
|
-
if platform_choice == "both"
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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
|
+
|
|
30
51
|
save_local_config({"webDir": web_dir, "platform": platform_choice})
|
|
31
52
|
setup_capacitor(web_dir, platforms)
|
|
32
53
|
else:
|
|
33
54
|
print_info(f"Native {proj_type} project detected. Bypassing Capacitor setup.")
|
|
34
|
-
|
|
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
|
+
|
|
35
64
|
save_local_config({"platform": platform_choice})
|
|
36
65
|
|
|
37
|
-
# --- UNIVERSAL AI PERMISSION SYSTEM ---
|
|
38
66
|
print("")
|
|
39
|
-
# Pass the category to the AI!
|
|
40
67
|
detected_features = scan_codebase_for_permissions()
|
|
41
|
-
|
|
68
|
+
|
|
42
69
|
if detected_features:
|
|
43
70
|
print_success(f"AI detected {len(detected_features)} native features required by your app:")
|
|
44
|
-
|
|
71
|
+
|
|
45
72
|
for data in detected_features.values():
|
|
46
73
|
print(f" {CYAN}●{RESET} {data['feature']} {GRAY}({data['desc']}){RESET}")
|
|
47
|
-
|
|
74
|
+
|
|
48
75
|
print_info("The AppForge Cloud Server will automatically inject these permissions during the build process.")
|
|
49
76
|
else:
|
|
50
77
|
print_info("AI scan complete. No special native permissions detected.")
|
|
51
|
-
|
|
78
|
+
|
|
52
79
|
try:
|
|
53
80
|
import subprocess
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
81
|
+
git_root = subprocess.check_output(
|
|
82
|
+
['git', 'rev-parse', '--show-toplevel'],
|
|
83
|
+
text=True,
|
|
84
|
+
stderr=subprocess.DEVNULL
|
|
85
|
+
).strip()
|
|
86
|
+
|
|
58
87
|
sub_path = os.path.relpath(os.getcwd(), git_root)
|
|
59
|
-
|
|
60
|
-
# If we are in a sub-directory, save it
|
|
88
|
+
|
|
61
89
|
if sub_path and sub_path != '.':
|
|
62
90
|
print_info(f"Detected sub-project path: {GRAY}{sub_path}{RESET}")
|
|
63
91
|
save_local_config({"sub_path": sub_path})
|
|
64
|
-
|
|
92
|
+
|
|
65
93
|
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
66
|
-
# Not a git repo, or git not installed. Assume it's a standalone project.
|
|
67
94
|
save_local_config({"sub_path": "."})
|
|
95
|
+
|
|
68
96
|
print_success("Ready to send to the AppForge Cloud Builder.")
|
|
69
97
|
|
|
70
98
|
def configure_project():
|
|
@@ -72,13 +100,22 @@ def configure_project():
|
|
|
72
100
|
print_info("Interactive Configuration")
|
|
73
101
|
app_name = prompt("App name", config.get("appName", "My App"))
|
|
74
102
|
package_id = prompt("Package ID", config.get("packageId", "com.example.app"))
|
|
75
|
-
|
|
103
|
+
build_mode = prompt("Default Build Mode (debug/release)", config.get("buildMode", "debug")).lower()
|
|
104
|
+
if build_mode not in ['debug', 'release']: build_mode = 'debug'
|
|
105
|
+
current_icon = config.get("iconPath", "")
|
|
106
|
+
icon_prompt = prompt("Icon path (local PNG)", current_icon)
|
|
107
|
+
|
|
108
|
+
# If they typed a new path, use it. Otherwise, keep the old one.
|
|
109
|
+
final_icon_path = icon_prompt if icon_prompt else current_icon
|
|
110
|
+
|
|
76
111
|
splash_img = prompt("Splash image (local PNG)", config.get("splashImg", ""))
|
|
77
112
|
splash_bg = prompt("Splash background color (HEX)", config.get("splashBg", "#ffffff"))
|
|
78
113
|
|
|
79
114
|
new_config = {
|
|
80
115
|
"appName": app_name,
|
|
81
116
|
"packageId": package_id,
|
|
117
|
+
"buildMode": build_mode,
|
|
118
|
+
"iconPath": final_icon_path,
|
|
82
119
|
"iconPath": icon_path,
|
|
83
120
|
"splashImg": splash_img,
|
|
84
121
|
"splashBg": splash_bg
|
|
@@ -86,13 +123,15 @@ def configure_project():
|
|
|
86
123
|
save_local_config(new_config)
|
|
87
124
|
|
|
88
125
|
web_dir = config.get("webDir", "dist")
|
|
89
|
-
|
|
126
|
+
if config.get("category") == "web":
|
|
127
|
+
apply_configuration(app_name, package_id, final_icon_path, splash_img, splash_bg, web_dir)
|
|
90
128
|
return new_config
|
|
91
129
|
|
|
92
130
|
def display_app_details(config):
|
|
93
131
|
print(f"{BOLD}App Configuration:{RESET}")
|
|
94
132
|
print(f" {GRAY}Name:{RESET} {config.get('appName', 'Not set')}")
|
|
95
133
|
print(f" {GRAY}Package ID:{RESET} {config.get('packageId', 'Not set')}")
|
|
134
|
+
print(f" {GRAY}Default Mode:{RESET} {config.get('buildMode', 'debug').upper()}")
|
|
96
135
|
print(f" {GRAY}Web Directory:{RESET} {config.get('webDir', 'Not set')}")
|
|
97
136
|
print(f" {GRAY}Icon:{RESET} {config.get('iconPath', 'Not set')}")
|
|
98
137
|
print(f" {GRAY}Splash Image:{RESET} {config.get('splashImg', 'Not set')}")
|
|
@@ -120,7 +159,6 @@ def build_app():
|
|
|
120
159
|
config['platform'] = new_platform.lower()
|
|
121
160
|
config_was_updated = True
|
|
122
161
|
|
|
123
|
-
# --- VERSION INCREMENT LOGIC ---
|
|
124
162
|
current_version_str = config.get("version", "1.0.0")
|
|
125
163
|
try:
|
|
126
164
|
parts = current_version_str.split('.')
|
|
@@ -130,6 +168,16 @@ def build_app():
|
|
|
130
168
|
new_version_str = "1.0.1"
|
|
131
169
|
|
|
132
170
|
print_info(f"Preparing build for version: {CYAN}{new_version_str}{RESET}")
|
|
171
|
+
build_type = config.get("buildMode")
|
|
172
|
+
|
|
173
|
+
if not build_type:
|
|
174
|
+
choice = prompt("Build for Production Release? (Y/n - 'n' builds Debug)", default="y").lower()
|
|
175
|
+
build_type = "release" if choice in ['y', 'yes', ''] else "debug"
|
|
176
|
+
# Save this choice so we don't have to ask again next time
|
|
177
|
+
save_local_config({"buildMode": build_type})
|
|
178
|
+
|
|
179
|
+
# Update the working config object
|
|
180
|
+
config['build_type'] = build_type
|
|
133
181
|
config['version'] = new_version_str
|
|
134
182
|
|
|
135
183
|
if config_was_updated:
|
|
@@ -139,16 +187,15 @@ def build_app():
|
|
|
139
187
|
print_info("Ready to build with the following configuration:")
|
|
140
188
|
display_app_details(config)
|
|
141
189
|
print(f" {GRAY}Target Platform:{RESET} {config.get('platform', 'android')}")
|
|
142
|
-
print(f" {GRAY}Next Version:{RESET} {new_version_str}
|
|
190
|
+
print(f" {GRAY}Next Version:{RESET} {new_version_str}")
|
|
191
|
+
color = GREEN if build_type == "release" else YELLOW
|
|
192
|
+
print(f" {GRAY}Build Mode:{RESET} {color}{build_type.upper()}{RESET}\n")
|
|
143
193
|
|
|
144
|
-
# Ask ONLY ONCE if they want to build
|
|
145
194
|
action = prompt("Proceed with build? (Y/n)", default="y").lower()
|
|
146
195
|
|
|
147
196
|
if action in ['y', 'yes', '']:
|
|
148
|
-
# 1. Save new version to local memory
|
|
149
197
|
save_local_config({"version": new_version_str})
|
|
150
198
|
|
|
151
|
-
# 2. Sync CLI memory to Capacitor native config
|
|
152
199
|
if config.get("category") == "web":
|
|
153
200
|
apply_configuration(
|
|
154
201
|
config.get('appName', 'My App'),
|
|
@@ -159,11 +206,9 @@ def build_app():
|
|
|
159
206
|
config.get('webDir', 'dist')
|
|
160
207
|
)
|
|
161
208
|
|
|
162
|
-
# 3. Zip and send to cloud
|
|
163
209
|
push_and_build(config)
|
|
164
210
|
print_success("Your app is now building in the cloud!")
|
|
165
211
|
|
|
166
|
-
# 4. Ask to watch logs
|
|
167
212
|
watch = prompt("Watch live build logs? (Y/n)", default="y").lower()
|
|
168
213
|
if watch in ['y', 'yes', '']:
|
|
169
214
|
status_check()
|
|
@@ -174,8 +219,8 @@ def build_app():
|
|
|
174
219
|
print(f"\n{GREEN}Run 'appforge configure' to edit App configuration{RESET}")
|
|
175
220
|
print("\n")
|
|
176
221
|
print_info("Build cancelled.")
|
|
222
|
+
|
|
177
223
|
def show_history():
|
|
178
|
-
"""Displays a list of past builds with their current cloud status."""
|
|
179
224
|
history = load_history()
|
|
180
225
|
if not history:
|
|
181
226
|
print_info("No build history found. Run 'appforge build' to create your first app!")
|
|
@@ -192,19 +237,14 @@ def show_history():
|
|
|
192
237
|
for entry in entries_to_display:
|
|
193
238
|
from datetime import datetime
|
|
194
239
|
|
|
195
|
-
# Format the timestamp
|
|
196
240
|
dt_obj = datetime.fromisoformat(entry['triggered_at'].replace('Z', '+00:00'))
|
|
197
241
|
formatted_time = dt_obj.strftime('%Y-%m-%d %H:%M:%S')
|
|
198
242
|
|
|
199
|
-
# Fetch real-time status from GitHub
|
|
200
243
|
run_id = entry.get("run_id")
|
|
201
244
|
if run_id:
|
|
202
245
|
status, conclusion = get_build_status_by_id(run_id)
|
|
203
246
|
else:
|
|
204
|
-
# Fallback for old history items
|
|
205
247
|
status, conclusion = entry.get("status", "unknown"), entry.get("conclusion", None)
|
|
206
|
-
|
|
207
|
-
# --- STATIC STATUS DISPLAY LOGIC ---
|
|
208
248
|
status_text = ""
|
|
209
249
|
if status in ["in_progress", "queued"]:
|
|
210
250
|
# Static yellow text instead of animation
|
|
@@ -222,23 +262,17 @@ def show_history():
|
|
|
222
262
|
|
|
223
263
|
print("-" * 80)
|
|
224
264
|
|
|
225
|
-
# Update the prompt text to be perfectly clear
|
|
226
265
|
choice = prompt("Enter a number or App ID to re-download, or press Enter to exit").strip().lower()
|
|
227
266
|
|
|
228
|
-
selected_entry = None
|
|
267
|
+
selected_entry = None
|
|
229
268
|
|
|
230
|
-
# Case 1: User entered a number from the list
|
|
231
269
|
if choice.isdigit() and 0 < int(choice) <= len(entries_to_display):
|
|
232
270
|
selected_index = int(choice) - 1
|
|
233
271
|
selected_entry = entries_to_display[selected_index]
|
|
234
272
|
|
|
235
|
-
# Case 2: User entered text (could be an App ID)
|
|
236
273
|
elif choice:
|
|
237
|
-
# Find the *first* (most recent) entry in the full history that matches the ID.
|
|
238
|
-
# We use startswith so users can just type the first few characters (e.g., '9a6b').
|
|
239
274
|
selected_entry = next((entry for entry in history if entry['app_id'].lower().startswith(choice)), None)
|
|
240
275
|
|
|
241
|
-
# Now, if we found an entry by either method, proceed with the download
|
|
242
276
|
if selected_entry:
|
|
243
277
|
run_id_to_download = selected_entry.get("run_id")
|
|
244
278
|
|
|
@@ -248,7 +282,6 @@ def show_history():
|
|
|
248
282
|
else:
|
|
249
283
|
print_warning("This is a legacy build entry without a unique Run ID. Cannot guarantee the correct artifact.")
|
|
250
284
|
|
|
251
|
-
# Case 3: User typed something, but it was invalid
|
|
252
285
|
elif choice:
|
|
253
286
|
print_warning("Invalid input. Please enter a number from the list or a valid App ID.")
|
|
254
287
|
|
|
@@ -258,11 +291,46 @@ def status_check():
|
|
|
258
291
|
def download_app():
|
|
259
292
|
download_apk()
|
|
260
293
|
|
|
294
|
+
def show_info():
|
|
295
|
+
print(f"\n{BOLD}▲ AppForge CLI Information{RESET}")
|
|
296
|
+
print("=" * 55)
|
|
297
|
+
|
|
298
|
+
print(f"\n{CYAN}SYSTEM STATUS{RESET}")
|
|
299
|
+
print(f" {GRAY}Current Version:{RESET} v{CLI_VERSION}")
|
|
300
|
+
print(f" {GRAY}Cloud Target:{RESET} Private GitHub Actions Runner")
|
|
301
|
+
print(f" {GRAY}Connection:{RESET} Authenticated & Active")
|
|
302
|
+
print(f"\n{CYAN}UNIVERSAL ARCHITECTURE{RESET}")
|
|
303
|
+
print(f" {GREEN}✔{RESET} {BOLD}Web to Native{RESET} (React, Vite, Next.js, HTML)")
|
|
304
|
+
print(f" {GREEN}✔{RESET} {BOLD}Flutter{RESET} (Full Native Compilation)")
|
|
305
|
+
print(f" {GREEN}✔{RESET} {BOLD}Kotlin/Java{RESET} (Standard Android Studio Projects)")
|
|
306
|
+
print(f" {GREEN}✔{RESET} {BOLD}Monorepo Support{RESET} (Sub-directory execution)")
|
|
307
|
+
|
|
308
|
+
print(f"\n{CYAN}ADVANCED FEATURES{RESET}")
|
|
309
|
+
print(f" {YELLOW}✦{RESET} {BOLD}AI Permission Scanner:{RESET} Automatically detects required")
|
|
310
|
+
print(f" hardware (Camera, GPS, Bluetooth) from source code.")
|
|
311
|
+
print(f" {YELLOW}✦{RESET} {BOLD}Cloud Injector Engine:{RESET} Dynamically writes strict")
|
|
312
|
+
print(f" Android 13+ permissions and Java 8 Desugaring rules.")
|
|
313
|
+
print(f" {YELLOW}✦{RESET} {BOLD}Permanent Keystore:{RESET} Signs Release APKs securely via")
|
|
314
|
+
print(f" cloud secrets to enable seamless OTA updates.")
|
|
315
|
+
print(f" {YELLOW}✦{RESET} {BOLD}Live Telemetry:{RESET} Streams real-time GitHub Actions")
|
|
316
|
+
print(f" build logs directly to your local terminal.")
|
|
317
|
+
|
|
318
|
+
print(f"\n{CYAN}LATEST RELEASE NOTES (v{CLI_VERSION}){RESET}")
|
|
319
|
+
print(" • Added Native App Icon & Splash Screen generation.")
|
|
320
|
+
print(" • Added intelligent 'history' command with live statuses.")
|
|
321
|
+
print(" • Fixed Flutter 'app_plugin_loader' compatibility bugs.")
|
|
322
|
+
print(" • Upgraded to robust Node.js Cloud Injection script.")
|
|
323
|
+
print(" • Added 'create' command for instant project scaffolding.")
|
|
324
|
+
|
|
325
|
+
print("\n" + "=" * 55)
|
|
326
|
+
print(f" {GRAY}Run 'appforge help' to see available commands.{RESET}\n")
|
|
327
|
+
|
|
261
328
|
def show_argu():
|
|
262
329
|
print("Commands:")
|
|
263
330
|
print(" appforge init - Initialize an AppForge project")
|
|
264
331
|
print(" appforge configure - Open interactive settings")
|
|
265
|
-
print(f" {CYAN}
|
|
332
|
+
print(f" {CYAN}info{RESET} - View CLI version and release notes")
|
|
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}")
|
|
266
334
|
print(" appforge build - Package project and send to build repo")
|
|
267
335
|
print(" appforge status - Check build status")
|
|
268
336
|
print(" appforge download - Download the built APK")
|
|
@@ -273,7 +341,6 @@ def show_help():
|
|
|
273
341
|
"""The animated welcome sequence for the very first run."""
|
|
274
342
|
welcome_message = f"{BOLD}▲ AppForge{RESET} - The Universal App Builder"
|
|
275
343
|
|
|
276
|
-
# Typing animation
|
|
277
344
|
for char in welcome_message:
|
|
278
345
|
sys.stdout.write(char)
|
|
279
346
|
sys.stdout.flush()
|
|
@@ -295,24 +362,17 @@ def main():
|
|
|
295
362
|
|
|
296
363
|
print_header()
|
|
297
364
|
|
|
298
|
-
# --- FIXED ARGPARSE LOGIC ---
|
|
299
365
|
parser = argparse.ArgumentParser(add_help=False)
|
|
300
|
-
# The first word is the command (defaulting to "help")
|
|
301
366
|
parser.add_argument("command", nargs="?", default="help", help="Command to run")
|
|
302
|
-
# All remaining words get bundled into a list called "args"
|
|
303
367
|
parser.add_argument("args", nargs=argparse.REMAINDER, help="Additional arguments")
|
|
304
368
|
|
|
305
|
-
# parse_known_args is safer here than parse_args
|
|
306
369
|
args, unknown = parser.parse_known_args()
|
|
307
370
|
|
|
308
|
-
# If there were extra arguments that slipped past, add them to our list
|
|
309
371
|
if unknown:
|
|
310
372
|
args.args.extend(unknown)
|
|
311
|
-
# ----------------------------
|
|
312
373
|
|
|
313
374
|
try:
|
|
314
375
|
if args.command == "create":
|
|
315
|
-
# Check if they provided the framework and name
|
|
316
376
|
if not args.args or len(args.args) < 2:
|
|
317
377
|
print_error("Usage: appforge create <framework> <project-name>\nExample: appforge create flutter my_new_app")
|
|
318
378
|
|
|
@@ -324,6 +384,7 @@ def main():
|
|
|
324
384
|
elif args.command == "build": build_app()
|
|
325
385
|
elif args.command == "configure": configure_project()
|
|
326
386
|
elif args.command == "history": show_history()
|
|
387
|
+
elif args.command == "info": show_info()
|
|
327
388
|
elif args.command == "status": status_check()
|
|
328
389
|
elif args.command == "download": download_apk()
|
|
329
390
|
else: show_help()
|
|
@@ -4,19 +4,32 @@ import subprocess
|
|
|
4
4
|
import re
|
|
5
5
|
from .utils import print_success, print_info, print_error, prompt, print_progress_bar, CYAN, RESET, BOLD
|
|
6
6
|
|
|
7
|
-
# The TEMPLATES dictionary stays the same
|
|
8
7
|
TEMPLATES = {
|
|
9
8
|
"vite": {
|
|
10
9
|
"cmd": "npm create vite@latest {name} -- --template react-ts",
|
|
11
10
|
"desc": "React + TypeScript + Vite (Fast Web App)"
|
|
12
11
|
},
|
|
13
12
|
"flutter": {
|
|
14
|
-
"cmd": "git clone --progress https://github.com/flutter/samples.git _temp_samples",
|
|
13
|
+
"cmd": "git clone --progress https://github.com/flutter/samples.git _temp_samples && mv _temp_samples/provider_counter {name} && rm -rf _temp_samples",
|
|
15
14
|
"desc": "Official Flutter Starter App (Native)"
|
|
16
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
|
+
},
|
|
17
20
|
"html": {
|
|
18
21
|
"cmd": None,
|
|
19
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!
|
|
20
33
|
}
|
|
21
34
|
}
|
|
22
35
|
|
|
Binary file
|