appforge-cli 1.0.0__tar.gz → 1.0.9__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.0.0 → appforge_cli-1.0.9}/PKG-INFO +1 -1
- appforge_cli-1.0.9/appforge/ai.py +42 -0
- {appforge_cli-1.0.0 → appforge_cli-1.0.9}/appforge/cli.py +5 -34
- {appforge_cli-1.0.0 → appforge_cli-1.0.9}/appforge/config.py +0 -9
- appforge_cli-1.0.9/appforge/github.py +207 -0
- appforge_cli-1.0.9/appforge/knowledge_base.json +181 -0
- {appforge_cli-1.0.0 → appforge_cli-1.0.9}/appforge_cli.egg-info/PKG-INFO +1 -1
- {appforge_cli-1.0.0 → appforge_cli-1.0.9}/appforge_cli.egg-info/SOURCES.txt +1 -0
- {appforge_cli-1.0.0 → appforge_cli-1.0.9}/pyproject.toml +1 -1
- {appforge_cli-1.0.0 → appforge_cli-1.0.9}/setup.py +5 -1
- appforge_cli-1.0.0/appforge/ai.py +0 -58
- appforge_cli-1.0.0/appforge/github.py +0 -139
- {appforge_cli-1.0.0 → appforge_cli-1.0.9}/appforge/__init__.py +0 -0
- {appforge_cli-1.0.0 → appforge_cli-1.0.9}/appforge/capacitor.py +0 -0
- {appforge_cli-1.0.0 → appforge_cli-1.0.9}/appforge/detector.py +0 -0
- {appforge_cli-1.0.0 → appforge_cli-1.0.9}/appforge/utils.py +0 -0
- {appforge_cli-1.0.0 → appforge_cli-1.0.9}/appforge_cli.egg-info/dependency_links.txt +0 -0
- {appforge_cli-1.0.0 → appforge_cli-1.0.9}/appforge_cli.egg-info/entry_points.txt +0 -0
- {appforge_cli-1.0.0 → appforge_cli-1.0.9}/appforge_cli.egg-info/requires.txt +0 -0
- {appforge_cli-1.0.0 → appforge_cli-1.0.9}/appforge_cli.egg-info/top_level.txt +0 -0
- {appforge_cli-1.0.0 → appforge_cli-1.0.9}/setup.cfg +0 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import json
|
|
3
|
+
from .utils import print_info, print_success, CYAN, RESET, GRAY
|
|
4
|
+
|
|
5
|
+
def load_knowledge_base():
|
|
6
|
+
"""Loads the AI keyword-to-plugin mappings from the JSON file."""
|
|
7
|
+
try:
|
|
8
|
+
# __file__ gives the path to the current python script (ai.py)
|
|
9
|
+
# We then find the directory it's in and look for the JSON file there.
|
|
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
|
+
print_error(f"Critical Error: Could not load AI knowledge base: {e}")
|
|
16
|
+
return {}
|
|
17
|
+
|
|
18
|
+
def scan_codebase_for_permissions():
|
|
19
|
+
"""Scans the project files to intelligently recommend native permissions."""
|
|
20
|
+
ai_knowledge_base = load_knowledge_base()
|
|
21
|
+
|
|
22
|
+
print_info(f"{CYAN}🤖 AI Agent scanning codebase for native features...{RESET}")
|
|
23
|
+
|
|
24
|
+
found_plugins = {}
|
|
25
|
+
|
|
26
|
+
for root, dirs, files in os.walk('.'):
|
|
27
|
+
dirs[:] = [d for d in dirs if d not in ['node_modules', '.git', 'android', 'ios', 'dist', 'build', '.next', '.nuxt', 'www']]
|
|
28
|
+
|
|
29
|
+
for file in files:
|
|
30
|
+
if file.endswith(('.js', '.ts', '.jsx', '.tsx', '.vue', '.svelte', '.html')):
|
|
31
|
+
file_path = os.path.join(root, file)
|
|
32
|
+
try:
|
|
33
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
34
|
+
content = f.read()
|
|
35
|
+
|
|
36
|
+
for keyword, data in ai_knowledge_base.items():
|
|
37
|
+
if keyword in content and data["plugin"] not in found_plugins:
|
|
38
|
+
found_plugins[data["plugin"]] = data
|
|
39
|
+
except Exception:
|
|
40
|
+
pass
|
|
41
|
+
|
|
42
|
+
return found_plugins
|
|
@@ -2,11 +2,12 @@ import sys
|
|
|
2
2
|
import argparse
|
|
3
3
|
from .utils import print_header, print_footer, print_info, print_success, print_error, prompt, GRAY, RESET, CYAN, BOLD
|
|
4
4
|
from .detector import detect_project
|
|
5
|
-
from .capacitor import setup_capacitor, apply_configuration
|
|
6
|
-
from .config import save_local_config, load_local_config
|
|
5
|
+
from .capacitor import setup_capacitor, apply_configuration
|
|
6
|
+
from .config import save_local_config, load_local_config
|
|
7
7
|
from .github import push_and_build, check_status, download_apk
|
|
8
8
|
|
|
9
9
|
from .ai import scan_codebase_for_permissions
|
|
10
|
+
from .capacitor import install_plugins
|
|
10
11
|
|
|
11
12
|
def init_project():
|
|
12
13
|
project_info = detect_project()
|
|
@@ -121,7 +122,7 @@ def build_app():
|
|
|
121
122
|
|
|
122
123
|
if action in ['y', 'yes', '']:
|
|
123
124
|
# Pass the platform to the push_and_build function!
|
|
124
|
-
push_and_build(config
|
|
125
|
+
push_and_build(config)
|
|
125
126
|
print_success("Your app is now building in the cloud!")
|
|
126
127
|
print_info("Run 'appforge status' to check its progress.")
|
|
127
128
|
else:
|
|
@@ -142,40 +143,10 @@ def show_help():
|
|
|
142
143
|
print(" appforge download - Download the built APK")
|
|
143
144
|
print(" appforge help - Show this help message")
|
|
144
145
|
|
|
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
146
|
def main():
|
|
172
|
-
|
|
173
|
-
if not global_config_exists():
|
|
174
|
-
run_first_time_setup()
|
|
175
|
-
|
|
176
147
|
print_header()
|
|
177
148
|
|
|
178
|
-
parser = argparse.ArgumentParser(
|
|
149
|
+
parser = argparse.ArgumentParser(add_help=False)
|
|
179
150
|
parser.add_argument("command", nargs="?", default="help", help="Command to run")
|
|
180
151
|
args = parser.parse_args()
|
|
181
152
|
|
|
@@ -24,12 +24,3 @@ def get_app_id():
|
|
|
24
24
|
save_local_config({"app_id": new_id})
|
|
25
25
|
return new_id
|
|
26
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,207 @@
|
|
|
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
|
+
# ... (imports and constants at the top stay the same) ...
|
|
21
|
+
|
|
22
|
+
def zip_project(category, zip_name="app_source.zip"):
|
|
23
|
+
"""Zips the project, intelligently ignoring folders based on project category."""
|
|
24
|
+
def do_zip():
|
|
25
|
+
# --- THIS IS THE NEW INTELLIGENT IGNORE LOGIC ---
|
|
26
|
+
if category == 'web':
|
|
27
|
+
# For web projects, we ignore the heavy native folders
|
|
28
|
+
ignore_list = ['node_modules', '.git', 'android', 'ios', '.next', '.nuxt', 'build', 'dist']
|
|
29
|
+
else: # This applies to 'native' projects (Flutter, Kotlin, etc.)
|
|
30
|
+
# For native projects, we MUST include the android/ios folders,
|
|
31
|
+
# but ignore caches and build artifacts.
|
|
32
|
+
ignore_list = ['node_modules', '.git', '.dart_tool', 'build', '.gradle']
|
|
33
|
+
|
|
34
|
+
with zipfile.ZipFile(zip_name, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
|
35
|
+
for root, dirs, files in os.walk('.'):
|
|
36
|
+
dirs[:] = [d for d in dirs if d not in ignore_list]
|
|
37
|
+
for file in files:
|
|
38
|
+
if file == zip_name:
|
|
39
|
+
continue
|
|
40
|
+
file_path = os.path.join(root, file)
|
|
41
|
+
zipf.write(file_path, file_path)
|
|
42
|
+
|
|
43
|
+
run_with_spinner(do_zip, "Zipping project files")
|
|
44
|
+
return zip_name
|
|
45
|
+
|
|
46
|
+
def push_and_build(config):
|
|
47
|
+
"""Pushes the zipped project and triggers the cloud build."""
|
|
48
|
+
# Extract needed info from the config
|
|
49
|
+
category = config.get("category", "web")
|
|
50
|
+
platform = config.get("platform", "android")
|
|
51
|
+
app_id = get_app_id()
|
|
52
|
+
|
|
53
|
+
# Pass the category to the zipping function
|
|
54
|
+
zip_path = zip_project(category)
|
|
55
|
+
|
|
56
|
+
headers = get_headers()
|
|
57
|
+
|
|
58
|
+
def do_upload():
|
|
59
|
+
with open(zip_path, "rb") as f:
|
|
60
|
+
content_b64 = base64.b64encode(f.read()).decode("utf-8")
|
|
61
|
+
|
|
62
|
+
url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/contents/apps/{app_id}/source.zip"
|
|
63
|
+
|
|
64
|
+
sha = None
|
|
65
|
+
res = requests.get(url, headers=headers)
|
|
66
|
+
if res.status_code == 200: sha = res.json().get("sha")
|
|
67
|
+
|
|
68
|
+
data = {"message": f"Upload for {app_id}", "content": content_b64, "branch": "main"}
|
|
69
|
+
if sha: data["sha"] = sha
|
|
70
|
+
|
|
71
|
+
put_res = requests.put(url, headers=headers, json=data)
|
|
72
|
+
if put_res.status_code not in [200, 201]:
|
|
73
|
+
raise Exception(f"Failed to upload: {put_res.json().get('message')}")
|
|
74
|
+
|
|
75
|
+
run_with_spinner(do_upload, f"Uploading app (ID: {app_id})")
|
|
76
|
+
os.remove(zip_path)
|
|
77
|
+
|
|
78
|
+
def do_trigger():
|
|
79
|
+
# Load the full config again just to be safe
|
|
80
|
+
full_config = load_local_config()
|
|
81
|
+
proj_category = full_config.get("category", "web")
|
|
82
|
+
proj_type = full_config.get("project_type", "unknown")
|
|
83
|
+
|
|
84
|
+
dispatch_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/workflows/build.yml/dispatches"
|
|
85
|
+
|
|
86
|
+
dispatch_data = {
|
|
87
|
+
"ref": "main",
|
|
88
|
+
"inputs": {
|
|
89
|
+
"app_id": app_id,
|
|
90
|
+
"platform": platform,
|
|
91
|
+
"project_category": proj_category,
|
|
92
|
+
"project_type": proj_type
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
res = requests.post(dispatch_url, headers=headers, json=dispatch_data)
|
|
96
|
+
if res.status_code != 204:
|
|
97
|
+
raise Exception(f"Failed to trigger build. Status {res.status_code}: {res.text}")
|
|
98
|
+
|
|
99
|
+
run_with_spinner(do_trigger, "Triggering cloud build")
|
|
100
|
+
|
|
101
|
+
def check_status():
|
|
102
|
+
"""Streams live build logs from GitHub Actions to the terminal."""
|
|
103
|
+
headers = get_headers()
|
|
104
|
+
|
|
105
|
+
# 1. Find the most recent workflow run
|
|
106
|
+
url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/runs?per_page=1"
|
|
107
|
+
res = requests.get(url, headers=headers)
|
|
108
|
+
|
|
109
|
+
if res.status_code != 200:
|
|
110
|
+
print_error("Could not fetch build status from cloud.")
|
|
111
|
+
return
|
|
112
|
+
|
|
113
|
+
runs = res.json().get("workflow_runs", [])
|
|
114
|
+
if not runs:
|
|
115
|
+
print_info("No builds found in the cloud yet.")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
latest_run = runs[0]
|
|
119
|
+
run_id = latest_run["id"]
|
|
120
|
+
status = latest_run["status"]
|
|
121
|
+
|
|
122
|
+
if status == "completed":
|
|
123
|
+
conclusion = latest_run.get("conclusion")
|
|
124
|
+
if conclusion == "success":
|
|
125
|
+
print_success(f"Build #{run_id} is already completed! Run 'appforge download'")
|
|
126
|
+
else:
|
|
127
|
+
print_error(f"Build #{run_id} failed with conclusion: {conclusion}")
|
|
128
|
+
return
|
|
129
|
+
|
|
130
|
+
print_info(f"Connected to Cloud Build #{run_id}. Streaming live logs...\n")
|
|
131
|
+
print("-" * 50)
|
|
132
|
+
|
|
133
|
+
# 2. Find the active "Job" inside the run (e.g., 'build-android' or 'build-ios')
|
|
134
|
+
jobs_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/runs/{run_id}/jobs"
|
|
135
|
+
|
|
136
|
+
last_step_name = ""
|
|
137
|
+
|
|
138
|
+
# 3. The Live Streaming Loop
|
|
139
|
+
try:
|
|
140
|
+
while True:
|
|
141
|
+
jobs_res = requests.get(jobs_url, headers=headers)
|
|
142
|
+
if jobs_res.status_code != 200:
|
|
143
|
+
time.sleep(3)
|
|
144
|
+
continue
|
|
145
|
+
|
|
146
|
+
jobs = jobs_res.json().get("jobs", [])
|
|
147
|
+
if not jobs:
|
|
148
|
+
time.sleep(3)
|
|
149
|
+
continue
|
|
150
|
+
|
|
151
|
+
active_job = jobs[0] # Monitor the first job for simplicity
|
|
152
|
+
|
|
153
|
+
# Print the current step the server is executing
|
|
154
|
+
steps = active_job.get("steps", [])
|
|
155
|
+
for step in steps:
|
|
156
|
+
if step["status"] == "in_progress" and step["name"] != last_step_name:
|
|
157
|
+
|
|
158
|
+
# Highlight our custom Permission Injector step!
|
|
159
|
+
if "Permission" in step["name"]:
|
|
160
|
+
print(f"{CYAN}⚙️ [Cloud] {step['name']}...{RESET}")
|
|
161
|
+
else:
|
|
162
|
+
print(f"{GRAY}▶ [Cloud] {step['name']}...{RESET}")
|
|
163
|
+
|
|
164
|
+
last_step_name = step["name"]
|
|
165
|
+
break # Only print the currently running step
|
|
166
|
+
|
|
167
|
+
# Check if the whole job finished
|
|
168
|
+
if active_job["status"] == "completed":
|
|
169
|
+
print("-" * 50)
|
|
170
|
+
if active_job["conclusion"] == "success":
|
|
171
|
+
print_success("\n✨ Cloud Build Finished Successfully!")
|
|
172
|
+
print_info("Run 'appforge download' to get your app.")
|
|
173
|
+
else:
|
|
174
|
+
print_error("\n❌ Cloud Build Failed. Check GitHub for full error logs.")
|
|
175
|
+
break
|
|
176
|
+
|
|
177
|
+
# Wait 3 seconds before polling GitHub again to avoid API rate limits
|
|
178
|
+
time.sleep(3)
|
|
179
|
+
|
|
180
|
+
except KeyboardInterrupt:
|
|
181
|
+
print(f"\n{YELLOW}⚠ Stopped watching logs. The build is still running in the cloud.{RESET}")
|
|
182
|
+
print_info("You can run 'appforge status' again later to reconnect.")
|
|
183
|
+
|
|
184
|
+
def download_apk():
|
|
185
|
+
app_id = get_app_id()
|
|
186
|
+
headers = get_headers()
|
|
187
|
+
url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/artifacts?per_page=100"
|
|
188
|
+
res = requests.get(url, headers=headers)
|
|
189
|
+
|
|
190
|
+
if res.status_code == 200:
|
|
191
|
+
artifacts = res.json().get("artifacts", [])
|
|
192
|
+
found = False
|
|
193
|
+
|
|
194
|
+
for artifact in artifacts:
|
|
195
|
+
name = artifact.get("name")
|
|
196
|
+
if name in [f"appforge-apk-{app_id}", f"appforge-ios-{app_id}"]:
|
|
197
|
+
found = True
|
|
198
|
+
print_success(f"Found artifact: {name}")
|
|
199
|
+
secure_api_url = artifact.get('archive_download_url')
|
|
200
|
+
redirect_res = requests.get(secure_api_url, headers=headers, allow_redirects=False)
|
|
201
|
+
if redirect_res.status_code == 302:
|
|
202
|
+
print_info(f"Download link (expires in 1 min):\n{redirect_res.headers['Location']}\n")
|
|
203
|
+
|
|
204
|
+
if not found:
|
|
205
|
+
print_error("Build not finished yet, or artifact not found.")
|
|
206
|
+
else:
|
|
207
|
+
print_error("Failed to fetch artifacts.")
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
{
|
|
2
|
+
"navigator.geolocation": {
|
|
3
|
+
"plugin": "@capacitor/geolocation",
|
|
4
|
+
"feature": "Geolocation (GPS)",
|
|
5
|
+
"desc": "Found location tracking code"
|
|
6
|
+
},
|
|
7
|
+
|
|
8
|
+
"getUserMedia": {
|
|
9
|
+
"plugin": "@capacitor/camera",
|
|
10
|
+
"feature": "Camera",
|
|
11
|
+
"desc": "Found camera access code"
|
|
12
|
+
},
|
|
13
|
+
|
|
14
|
+
"MediaRecorder": {
|
|
15
|
+
"plugin": "@capacitor/microphone",
|
|
16
|
+
"feature": "Microphone",
|
|
17
|
+
"desc": "Found audio recording code"
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
"Notification.requestPermission": {
|
|
21
|
+
"plugin": "@capacitor/notifications",
|
|
22
|
+
"feature": "Push Notifications",
|
|
23
|
+
"desc": "Found notification request code"
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
"navigator.vibrate": {
|
|
27
|
+
"plugin": "@capacitor/haptics",
|
|
28
|
+
"feature": "Haptics (Vibration)",
|
|
29
|
+
"desc": "Found vibration code"
|
|
30
|
+
},
|
|
31
|
+
|
|
32
|
+
"FileSystem": {
|
|
33
|
+
"plugin": "@capacitor/filesystem",
|
|
34
|
+
"feature": "Filesystem",
|
|
35
|
+
"desc": "Found file read/write code"
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
"navigator.bluetooth": {
|
|
39
|
+
"plugin": "@capacitor/bluetooth",
|
|
40
|
+
"feature": "Bluetooth (BLE)",
|
|
41
|
+
"desc": "Found Web Bluetooth API"
|
|
42
|
+
},
|
|
43
|
+
|
|
44
|
+
"navigator.contacts": {
|
|
45
|
+
"plugin": "@capacitor/contacts",
|
|
46
|
+
"feature": "Contacts",
|
|
47
|
+
"desc": "Found Web Contacts API"
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
"PublicKeyCredential": {
|
|
51
|
+
"plugin": "@capacitor/biometrics",
|
|
52
|
+
"feature": "Biometric Authentication",
|
|
53
|
+
"desc": "Found WebAuthn / biometric login code"
|
|
54
|
+
},
|
|
55
|
+
|
|
56
|
+
"DeviceMotionEvent": {
|
|
57
|
+
"plugin": "@capacitor/sensors",
|
|
58
|
+
"feature": "Motion Sensors",
|
|
59
|
+
"desc": "Found accelerometer / gyroscope usage"
|
|
60
|
+
},
|
|
61
|
+
|
|
62
|
+
"navigator.share": {
|
|
63
|
+
"plugin": "@capacitor/share",
|
|
64
|
+
"feature": "Native Share",
|
|
65
|
+
"desc": "Found Web Share API"
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
"navigator.clipboard": {
|
|
69
|
+
"plugin": "@capacitor/clipboard",
|
|
70
|
+
"feature": "Clipboard Access",
|
|
71
|
+
"desc": "Found clipboard read/write API"
|
|
72
|
+
},
|
|
73
|
+
|
|
74
|
+
"navigator.connection": {
|
|
75
|
+
"plugin": "@capacitor/network",
|
|
76
|
+
"feature": "Network Status",
|
|
77
|
+
"desc": "Found network information API"
|
|
78
|
+
},
|
|
79
|
+
|
|
80
|
+
"navigator.getBattery": {
|
|
81
|
+
"plugin": "@capacitor/device",
|
|
82
|
+
"feature": "Battery Status",
|
|
83
|
+
"desc": "Found battery API"
|
|
84
|
+
},
|
|
85
|
+
|
|
86
|
+
"navigator.mediaDevices.enumerateDevices": {
|
|
87
|
+
"plugin": "@capacitor/camera",
|
|
88
|
+
"feature": "Camera Devices",
|
|
89
|
+
"desc": "Found media device enumeration"
|
|
90
|
+
},
|
|
91
|
+
|
|
92
|
+
"navigator.usb": {
|
|
93
|
+
"plugin": "@capacitor/usb",
|
|
94
|
+
"feature": "USB Devices",
|
|
95
|
+
"desc": "Found WebUSB API"
|
|
96
|
+
},
|
|
97
|
+
|
|
98
|
+
"navigator.serial": {
|
|
99
|
+
"plugin": "@capacitor/serial",
|
|
100
|
+
"feature": "Serial Devices",
|
|
101
|
+
"desc": "Found Web Serial API"
|
|
102
|
+
},
|
|
103
|
+
|
|
104
|
+
"navigator.hid": {
|
|
105
|
+
"plugin": "@capacitor/hid",
|
|
106
|
+
"feature": "HID Devices",
|
|
107
|
+
"desc": "Found Human Interface Device API"
|
|
108
|
+
},
|
|
109
|
+
|
|
110
|
+
"navigator.nfc": {
|
|
111
|
+
"plugin": "@capacitor/nfc",
|
|
112
|
+
"feature": "NFC",
|
|
113
|
+
"desc": "Found NFC usage"
|
|
114
|
+
},
|
|
115
|
+
|
|
116
|
+
"speechSynthesis": {
|
|
117
|
+
"plugin": "@capacitor/tts",
|
|
118
|
+
"feature": "Text To Speech",
|
|
119
|
+
"desc": "Found speech synthesis API"
|
|
120
|
+
},
|
|
121
|
+
|
|
122
|
+
"SpeechRecognition": {
|
|
123
|
+
"plugin": "@capacitor/speech-recognition",
|
|
124
|
+
"feature": "Speech Recognition",
|
|
125
|
+
"desc": "Found voice recognition API"
|
|
126
|
+
},
|
|
127
|
+
|
|
128
|
+
"navigator.wakeLock": {
|
|
129
|
+
"plugin": "@capacitor/wakelock",
|
|
130
|
+
"feature": "Wake Lock",
|
|
131
|
+
"desc": "Found screen wake lock API"
|
|
132
|
+
},
|
|
133
|
+
|
|
134
|
+
"DeviceOrientationEvent": {
|
|
135
|
+
"plugin": "@capacitor/sensors",
|
|
136
|
+
"feature": "Device Orientation",
|
|
137
|
+
"desc": "Found device orientation sensor usage"
|
|
138
|
+
},
|
|
139
|
+
|
|
140
|
+
"navigator.storage": {
|
|
141
|
+
"plugin": "@capacitor/filesystem",
|
|
142
|
+
"feature": "Storage API",
|
|
143
|
+
"desc": "Found persistent storage API"
|
|
144
|
+
},
|
|
145
|
+
|
|
146
|
+
"navigator.mediaSession": {
|
|
147
|
+
"plugin": "@capacitor/media-session",
|
|
148
|
+
"feature": "Media Controls",
|
|
149
|
+
"desc": "Found media session API"
|
|
150
|
+
},
|
|
151
|
+
|
|
152
|
+
"navigator.permissions": {
|
|
153
|
+
"plugin": "@capacitor/permissions",
|
|
154
|
+
"feature": "Permissions API",
|
|
155
|
+
"desc": "Found permissions query API"
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
"navigator.hardwareConcurrency": {
|
|
159
|
+
"plugin": "@capacitor/device",
|
|
160
|
+
"feature": "Device Hardware Info",
|
|
161
|
+
"desc": "Found device hardware info access"
|
|
162
|
+
},
|
|
163
|
+
|
|
164
|
+
"navigator.deviceMemory": {
|
|
165
|
+
"plugin": "@capacitor/device",
|
|
166
|
+
"feature": "Device Memory Info",
|
|
167
|
+
"desc": "Found device memory detection"
|
|
168
|
+
},
|
|
169
|
+
|
|
170
|
+
"navigator.language": {
|
|
171
|
+
"plugin": "@capacitor/device",
|
|
172
|
+
"feature": "Device Language",
|
|
173
|
+
"desc": "Found device language detection"
|
|
174
|
+
},
|
|
175
|
+
|
|
176
|
+
"window.matchMedia": {
|
|
177
|
+
"plugin": "@capacitor/device",
|
|
178
|
+
"feature": "Device Display Detection",
|
|
179
|
+
"desc": "Found screen/media query detection"
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -2,13 +2,17 @@ from setuptools import setup
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="appforge",
|
|
5
|
-
version="1.0.
|
|
5
|
+
version="1.0.9",
|
|
6
6
|
description="Convert web apps into Android apps automatically using Capacitor and GitHub cloud builds.",
|
|
7
7
|
author="AppForge Team",
|
|
8
8
|
packages=["appforge"], # <-- CHANGE THIS LINE
|
|
9
9
|
install_requires=[
|
|
10
10
|
"requests"
|
|
11
11
|
],
|
|
12
|
+
package_data={
|
|
13
|
+
"appforge": ["knowledge_base.json"],
|
|
14
|
+
},
|
|
15
|
+
include_package_data=True,
|
|
12
16
|
entry_points={
|
|
13
17
|
"console_scripts": [
|
|
14
18
|
"appforge=appforge.cli:main",
|
|
@@ -1,58 +0,0 @@
|
|
|
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
|
|
@@ -1,139 +0,0 @@
|
|
|
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.")
|
|
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
|