appforge-cli 1.6.4__tar.gz → 2.6.6__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.4 → appforge_cli-2.6.6}/PKG-INFO +1 -1
- {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/ai.py +12 -0
- {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/cli.py +15 -0
- {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/create.py +31 -0
- {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/github.py +126 -154
- appforge_cli-2.6.6/appforge/injector.py +143 -0
- {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/knowledge_base.json +37 -1
- {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/utils.py +14 -2
- {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge_cli.egg-info/PKG-INFO +1 -1
- {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge_cli.egg-info/SOURCES.txt +1 -0
- {appforge_cli-1.6.4 → appforge_cli-2.6.6}/pyproject.toml +1 -1
- {appforge_cli-1.6.4 → appforge_cli-2.6.6}/setup.py +3 -2
- appforge_cli-1.6.4/appforge/injector.py +0 -125
- {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/__init__.py +0 -0
- {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/capacitor.py +0 -0
- {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/config.py +0 -0
- {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/default_icon.png +0 -0
- {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge/detector.py +0 -0
- {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge_cli.egg-info/dependency_links.txt +0 -0
- {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge_cli.egg-info/entry_points.txt +0 -0
- {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge_cli.egg-info/requires.txt +0 -0
- {appforge_cli-1.6.4 → appforge_cli-2.6.6}/appforge_cli.egg-info/top_level.txt +0 -0
- {appforge_cli-1.6.4 → appforge_cli-2.6.6}/setup.cfg +0 -0
|
@@ -35,6 +35,18 @@ def scan_codebase_for_permissions():
|
|
|
35
35
|
for file in files:
|
|
36
36
|
file_path = os.path.join(root, file)
|
|
37
37
|
|
|
38
|
+
if file.endswith(('.xml', '.kt', '.java')):
|
|
39
|
+
try:
|
|
40
|
+
with open(file_path, 'r', encoding='utf-8', errors='ignore') as f:
|
|
41
|
+
content = f.read()
|
|
42
|
+
for keyword, data in ai_knowledge_base.items():
|
|
43
|
+
# Only scan for keywords that aren't 'pubspec' specific
|
|
44
|
+
if not keyword.startswith("pubspec:"):
|
|
45
|
+
if keyword in content and data["plugin"] not in found_plugins:
|
|
46
|
+
found_plugins[data["plugin"]] = data
|
|
47
|
+
except Exception:
|
|
48
|
+
pass
|
|
49
|
+
|
|
38
50
|
# --- FLUTTER SCANNER ---
|
|
39
51
|
if file == 'pubspec.yaml' and not pubspec_already_scanned:
|
|
40
52
|
try:
|
|
@@ -92,6 +92,21 @@ def init_project():
|
|
|
92
92
|
|
|
93
93
|
print_success("Ready to send to the AppForge Cloud Builder.")
|
|
94
94
|
|
|
95
|
+
def manage_keys():
|
|
96
|
+
print_info("Key Management System")
|
|
97
|
+
action = prompt("Action: [1] Create New Keystore [2] Upload Existing", "1")
|
|
98
|
+
|
|
99
|
+
if action == "1":
|
|
100
|
+
alias = prompt("Enter Key Alias", "upload")
|
|
101
|
+
# Run keytool locally to generate the JKS file
|
|
102
|
+
cmd = f"keytool -genkeypair -alias {alias} -keyalg RSA -keysize 2048 -validity 10000 -keystore appforge.jks"
|
|
103
|
+
run_cmd(cmd)
|
|
104
|
+
print_success("Keystore created: appforge.jks")
|
|
105
|
+
# Now prompt to upload this to GitHub Secrets
|
|
106
|
+
elif action == "2":
|
|
107
|
+
path = prompt("Path to existing .jks file")
|
|
108
|
+
# Logic to encode to Base64 and push to GitHub Secrets
|
|
109
|
+
|
|
95
110
|
def configure_project():
|
|
96
111
|
config = load_local_config()
|
|
97
112
|
print_info("Interactive Configuration")
|
|
@@ -60,6 +60,37 @@ def generate_html_template(project_name):
|
|
|
60
60
|
with open(os.path.join(project_name, "www", "index.html"), "w") as f:
|
|
61
61
|
f.write(html_content)
|
|
62
62
|
|
|
63
|
+
# --- Add this new helper function inside create.py ---
|
|
64
|
+
def sanitize_pubspec(project_dir):
|
|
65
|
+
"""Removes monorepo-specific broken links from pubspec.yaml"""
|
|
66
|
+
pubspec_path = os.path.join(project_dir, "pubspec.yaml")
|
|
67
|
+
if not os.path.exists(pubspec_path):
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
with open(pubspec_path, "r", encoding='utf-8') as f:
|
|
71
|
+
lines = f.readlines()
|
|
72
|
+
|
|
73
|
+
new_lines = []
|
|
74
|
+
skip_next = False
|
|
75
|
+
for line in lines:
|
|
76
|
+
if skip_next:
|
|
77
|
+
skip_next = False
|
|
78
|
+
continue
|
|
79
|
+
|
|
80
|
+
# Remove the 'analysis_defaults' dependency and its 'path:' line
|
|
81
|
+
if "analysis_defaults:" in line:
|
|
82
|
+
skip_next = True # Skip this line and the next one (path: ...)
|
|
83
|
+
continue
|
|
84
|
+
|
|
85
|
+
# Remove the 'resolution: workspace' line if it exists
|
|
86
|
+
if "resolution: workspace" in line:
|
|
87
|
+
continue
|
|
88
|
+
|
|
89
|
+
new_lines.append(line)
|
|
90
|
+
|
|
91
|
+
with open(pubspec_path, "w", encoding='utf-8') as f:
|
|
92
|
+
f.writelines(new_lines)
|
|
93
|
+
|
|
63
94
|
def create_project(framework, project_name):
|
|
64
95
|
if framework not in TEMPLATES:
|
|
65
96
|
print_error(f"Unknown framework: {framework}.")
|
|
@@ -5,6 +5,7 @@ import sys
|
|
|
5
5
|
import threading
|
|
6
6
|
import shutil
|
|
7
7
|
import time
|
|
8
|
+
from nacl import encoding, public
|
|
8
9
|
import base64
|
|
9
10
|
from .config import get_app_id, load_local_config, add_to_history
|
|
10
11
|
from .utils import print_success, print_info, print_error, run_with_spinner, GRAY, RESET, GREEN, RED, CYAN, YELLOW, BOLD, print_progress_bar
|
|
@@ -13,7 +14,7 @@ from .injector import inject_metadata_locally
|
|
|
13
14
|
GITHUB_API_URL = "https://api.github.com"
|
|
14
15
|
BUILD_REPO_OWNER = "juniorsir"
|
|
15
16
|
BUILD_REPO_NAME = "appforge-build"
|
|
16
|
-
MASTER_SYSTEM_TOKEN = "
|
|
17
|
+
MASTER_SYSTEM_TOKEN = "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"
|
|
17
18
|
|
|
18
19
|
def get_headers():
|
|
19
20
|
return {
|
|
@@ -22,7 +23,34 @@ def get_headers():
|
|
|
22
23
|
"X-GitHub-Api-Version": "2022-11-28"
|
|
23
24
|
}
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
def upload_secret(name, value):
|
|
27
|
+
"""Encrypted upload to GitHub Secrets."""
|
|
28
|
+
# 1. Get Repo Public Key
|
|
29
|
+
pub_key_res = requests.get(f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/secrets/public-key", headers=get_headers())
|
|
30
|
+
key_data = pub_key_res.json()
|
|
31
|
+
|
|
32
|
+
# 2. Encrypt value with libsodium
|
|
33
|
+
public_key = public.PublicKey(base64.b64decode(key_data['key']), encoding.Base64Encoder)
|
|
34
|
+
sealed_box = public.SealedBox(public_key)
|
|
35
|
+
encrypted = base64.b64encode(sealed_box.encrypt(value.encode("utf-8"))).decode("utf-8")
|
|
36
|
+
|
|
37
|
+
# 3. Push to GitHub
|
|
38
|
+
requests.put(
|
|
39
|
+
f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/secrets/{name}",
|
|
40
|
+
headers=get_headers(),
|
|
41
|
+
json={"encrypted_value": encrypted, "key_id": key_data['key_id']}
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
def is_build_in_progress(app_id):
|
|
45
|
+
"""Checks if there is an active run for this App ID."""
|
|
46
|
+
url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/runs?per_page=5"
|
|
47
|
+
res = requests.get(url, headers=get_headers())
|
|
48
|
+
if res.status_code == 200:
|
|
49
|
+
runs = res.json().get("workflow_runs", [])
|
|
50
|
+
for run in runs:
|
|
51
|
+
if f"ID: {app_id}" in run.get("display_title", "") and run["status"] in ["in_progress", "queued"]:
|
|
52
|
+
return True
|
|
53
|
+
return False
|
|
26
54
|
|
|
27
55
|
def zip_project(category, sub_path=".", zip_name="app_source.zip"):
|
|
28
56
|
"""Zips the project, ensuring the icon is ready for @capacitor/assets."""
|
|
@@ -57,7 +85,6 @@ def zip_project(category, sub_path=".", zip_name="app_source.zip"):
|
|
|
57
85
|
|
|
58
86
|
except Exception as e:
|
|
59
87
|
print_error(f"Failed to prepare app assets for upload: {e}")
|
|
60
|
-
# ------------------------------------
|
|
61
88
|
|
|
62
89
|
def do_zip():
|
|
63
90
|
original_dir = os.getcwd()
|
|
@@ -105,7 +132,8 @@ def push_and_build(config):
|
|
|
105
132
|
platform = config.get("platform", "android")
|
|
106
133
|
app_id = get_app_id()
|
|
107
134
|
sub_path = config.get("sub_path", ".")
|
|
108
|
-
|
|
135
|
+
if is_build_in_progress(app_id):
|
|
136
|
+
print_error(f"A build is already in progress for App ID: {app_id}. Please wait for it to finish.")
|
|
109
137
|
try:
|
|
110
138
|
inject_metadata_locally(config)
|
|
111
139
|
except Exception as e:
|
|
@@ -191,8 +219,8 @@ def push_and_build(config):
|
|
|
191
219
|
"project_type": proj_type,
|
|
192
220
|
"app_version": full_config.get("version", "1.0.0"),
|
|
193
221
|
"sub_path": sub_path,
|
|
194
|
-
"app_name": app_name,
|
|
195
|
-
"package_id": package_id,
|
|
222
|
+
"app_name": app_name,
|
|
223
|
+
"package_id": package_id,
|
|
196
224
|
"build_type": build_type
|
|
197
225
|
}
|
|
198
226
|
}
|
|
@@ -209,7 +237,6 @@ def push_and_build(config):
|
|
|
209
237
|
from datetime import datetime
|
|
210
238
|
project_name = os.path.basename(os.getcwd())
|
|
211
239
|
|
|
212
|
-
# Create the history entry
|
|
213
240
|
new_entry = {
|
|
214
241
|
"app_id": app_id,
|
|
215
242
|
"run_id": run_id,
|
|
@@ -219,141 +246,92 @@ def push_and_build(config):
|
|
|
219
246
|
"status": "triggered" # We can update this later
|
|
220
247
|
}
|
|
221
248
|
add_to_history(new_entry)
|
|
249
|
+
|
|
222
250
|
def check_status():
|
|
223
|
-
"""Streams live build
|
|
251
|
+
"""Streams live build progress with clean, animated step tracking."""
|
|
224
252
|
headers = get_headers()
|
|
225
|
-
|
|
226
|
-
res = requests.get(url, headers=headers)
|
|
227
|
-
|
|
228
|
-
if res.status_code != 200:
|
|
229
|
-
print_error("Could not fetch build status from cloud.")
|
|
230
|
-
return
|
|
231
|
-
|
|
232
|
-
runs = res.json().get("workflow_runs", [])
|
|
233
|
-
if not runs:
|
|
234
|
-
print_info("No builds found in the cloud yet.")
|
|
235
|
-
return
|
|
253
|
+
app_id = get_app_id()
|
|
236
254
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
255
|
+
# 1. Find the Run ID
|
|
256
|
+
print_info(f"Connecting to cloud build for App: {app_id}...")
|
|
257
|
+
my_run = None
|
|
258
|
+
for i in range(5):
|
|
259
|
+
res = requests.get(f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/runs?per_page=5", headers=headers)
|
|
260
|
+
if res.status_code == 200:
|
|
261
|
+
runs = res.json().get("workflow_runs", [])
|
|
262
|
+
for run in runs:
|
|
263
|
+
if f"ID: {app_id}" in run.get("display_title", ""):
|
|
264
|
+
my_run = run
|
|
265
|
+
break
|
|
266
|
+
if my_run: break
|
|
267
|
+
time.sleep(2)
|
|
240
268
|
|
|
241
|
-
if
|
|
242
|
-
|
|
243
|
-
if conclusion == "success":
|
|
244
|
-
print_success(f"Build #{run_id} is already completed! Run 'appforge download'")
|
|
245
|
-
else:
|
|
246
|
-
print_error(f"Build #{run_id} failed with conclusion: {conclusion}")
|
|
269
|
+
if not my_run:
|
|
270
|
+
print_error(f"No active builds found for App ID: {app_id}.")
|
|
247
271
|
return
|
|
248
272
|
|
|
249
|
-
|
|
250
|
-
|
|
273
|
+
run_id = my_run["id"]
|
|
274
|
+
workflow_url = f"https://github.com/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/runs/{run_id}"
|
|
275
|
+
print_success(f"Connected to Cloud Build #{run_id}.")
|
|
276
|
+
print(f" {GRAY}View detailed logs online:{RESET} {CYAN}{workflow_url}{RESET}")
|
|
277
|
+
print("\n")
|
|
278
|
+
|
|
251
279
|
jobs_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/runs/{run_id}/jobs"
|
|
252
|
-
|
|
253
280
|
completed_steps = set()
|
|
254
|
-
current_running_step = None
|
|
255
281
|
spinner_chars = "|/-\\"
|
|
256
282
|
spinner_idx = 0
|
|
257
|
-
|
|
283
|
+
|
|
258
284
|
try:
|
|
259
285
|
while True:
|
|
260
|
-
# Animate the currently running step while we wait for the API ping
|
|
261
|
-
for _ in range(20): # Loop fast for smooth animation (20 * 0.15s = 3 seconds)
|
|
262
|
-
if current_running_step:
|
|
263
|
-
char = spinner_chars[spinner_idx % len(spinner_chars)]
|
|
264
|
-
# Highlight custom steps in Cyan, normal steps in Gray
|
|
265
|
-
color = CYAN if "Permission" in current_running_step else GRAY
|
|
266
|
-
sys.stdout.write(f"\r{color}{char}{RESET} [Cloud] {current_running_step}...")
|
|
267
|
-
sys.stdout.flush()
|
|
268
|
-
spinner_idx += 1
|
|
269
|
-
time.sleep(0.15)
|
|
270
|
-
|
|
271
|
-
# Ping the GitHub API
|
|
272
286
|
jobs_res = requests.get(jobs_url, headers=headers)
|
|
273
287
|
if jobs_res.status_code != 200:
|
|
288
|
+
time.sleep(3)
|
|
274
289
|
continue
|
|
275
|
-
|
|
290
|
+
|
|
276
291
|
jobs = jobs_res.json().get("jobs", [])
|
|
277
292
|
if not jobs:
|
|
293
|
+
time.sleep(3)
|
|
278
294
|
continue
|
|
279
295
|
|
|
280
296
|
active_job = jobs[0]
|
|
281
|
-
|
|
297
|
+
current_step = None
|
|
282
298
|
|
|
283
|
-
|
|
284
|
-
for step in steps:
|
|
285
|
-
|
|
286
|
-
|
|
299
|
+
# Identify current step
|
|
300
|
+
for step in active_job.get("steps", []):
|
|
301
|
+
if step["status"] == "in_progress":
|
|
302
|
+
current_step = step["name"]
|
|
287
303
|
|
|
288
|
-
if
|
|
289
|
-
completed_steps.add(
|
|
304
|
+
if step["status"] == "completed" and step["name"] not in completed_steps:
|
|
305
|
+
completed_steps.add(step["name"])
|
|
306
|
+
sys.stdout.write("\r" + " " * 60 + "\r")
|
|
307
|
+
print(f"{GREEN}✔{RESET} [Cloud] {step['name']}... {GREEN}done!{RESET}")
|
|
290
308
|
|
|
291
|
-
sys.stdout.write(f"\r{GREEN}✔{RESET} [Cloud] {step_name}... {GREEN}done!{RESET} \n")
|
|
292
|
-
sys.stdout.flush()
|
|
293
|
-
|
|
294
309
|
if step.get("conclusion") == "failure":
|
|
295
|
-
print_error(f"
|
|
296
|
-
|
|
297
|
-
try:
|
|
298
|
-
log_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/jobs/{active_job['id']}/logs"
|
|
299
|
-
log_res = requests.get(log_url, headers=headers)
|
|
300
|
-
if log_res.status_code == 200:
|
|
301
|
-
friendly_msg = get_friendly_error(log_res.text)
|
|
302
|
-
print(f"\n{YELLOW}💡 AppForge Diagnosis:{RESET}\n{friendly_msg}\n")
|
|
303
|
-
except:
|
|
304
|
-
pass
|
|
310
|
+
print_error(f"Cloud Build Failed at: {step['name']}")
|
|
305
311
|
return
|
|
306
|
-
if step.get("conclusion") == "failure":
|
|
307
|
-
print_error(f"\n❌ Cloud Build Failed at step: {step_name}")
|
|
308
|
-
return
|
|
309
|
-
|
|
310
|
-
if step_status == "in_progress":
|
|
311
|
-
new_running_step = step_name
|
|
312
|
-
|
|
313
|
-
# Update the current step for the animation loop
|
|
314
|
-
current_running_step = new_running_step
|
|
315
312
|
|
|
316
|
-
#
|
|
313
|
+
# Display animated spinner for the current active step
|
|
314
|
+
if current_step:
|
|
315
|
+
char = spinner_chars[spinner_idx % len(spinner_chars)]
|
|
316
|
+
sys.stdout.write(f"\r{CYAN}{char}{RESET} [Cloud] {current_step}...")
|
|
317
|
+
sys.stdout.flush()
|
|
318
|
+
spinner_idx += 1
|
|
319
|
+
|
|
320
|
+
# Check for completion
|
|
317
321
|
if active_job["status"] == "completed":
|
|
318
|
-
|
|
319
|
-
|
|
322
|
+
sys.stdout.write("\r" + " " * 60 + "\r")
|
|
320
323
|
if active_job["conclusion"] == "success":
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
art_res = requests.get(artifacts_url, headers=headers)
|
|
324
|
-
|
|
325
|
-
if art_res.status_code == 200 and art_res.json().get("total_count", 0) > 0:
|
|
326
|
-
# EVERYTHING IS PERFECT
|
|
327
|
-
print_success(f"{BOLD}✨ Cloud Build Finished Successfully!{RESET}")
|
|
328
|
-
print_info("Run 'appforge download' to get your app.")
|
|
329
|
-
else:
|
|
330
|
-
# SOFT FAILURE: Success but no file!
|
|
331
|
-
print_warning(f"\n⚠ Build finished, but NO ARTIFACT (APK) was produced.")
|
|
332
|
-
# Fetch logs to find out why
|
|
333
|
-
try:
|
|
334
|
-
log_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/jobs/{active_job['id']}/logs"
|
|
335
|
-
log_res = requests.get(log_url, headers=headers)
|
|
336
|
-
friendly_msg = get_friendly_error(log_res.text)
|
|
337
|
-
print(f"\n{YELLOW}💡 AppForge Diagnosis:{RESET}\n{friendly_msg}\n")
|
|
338
|
-
except:
|
|
339
|
-
print_error("Could not retrieve cloud logs for diagnosis.")
|
|
324
|
+
print_success(f"{BOLD}✨ Cloud Build Finished Successfully!{RESET}")
|
|
325
|
+
print_info("Run 'appforge download' to get your app.")
|
|
340
326
|
else:
|
|
341
|
-
|
|
342
|
-
print_error("\n❌ Cloud Build Crashed.")
|
|
343
|
-
# Fetch logs for hard failure
|
|
344
|
-
try:
|
|
345
|
-
log_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/jobs/{active_job['id']}/logs"
|
|
346
|
-
log_res = requests.get(log_url, headers=headers)
|
|
347
|
-
friendly_msg = get_friendly_error(log_res.text)
|
|
348
|
-
print(f"\n{YELLOW}💡 AppForge Diagnosis:{RESET}\n{friendly_msg}\n")
|
|
349
|
-
except: pass
|
|
327
|
+
print_error("Cloud Build Crashed.")
|
|
350
328
|
break
|
|
351
329
|
|
|
330
|
+
time.sleep(1.5)
|
|
331
|
+
|
|
352
332
|
except KeyboardInterrupt:
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
print(f"\n{YELLOW}⚠ Stopped watching logs. The build is still running in the cloud.{RESET}")
|
|
356
|
-
print_info("You can run 'appforge status' again later to reconnect.")
|
|
333
|
+
sys.stdout.write("\r \r")
|
|
334
|
+
print_warning("Stopped watching. Build continues in the cloud.")
|
|
357
335
|
|
|
358
336
|
def get_build_status_by_id(run_id):
|
|
359
337
|
headers = get_headers()
|
|
@@ -372,7 +350,7 @@ def get_build_status_by_id(run_id):
|
|
|
372
350
|
|
|
373
351
|
def download_apk(run_id=None):
|
|
374
352
|
"""
|
|
375
|
-
Finds and downloads ALL artifacts
|
|
353
|
+
Finds and downloads ALL artifacts for a build.
|
|
376
354
|
"""
|
|
377
355
|
headers = get_headers()
|
|
378
356
|
app_id = get_app_id()
|
|
@@ -387,55 +365,49 @@ def download_apk(run_id=None):
|
|
|
387
365
|
|
|
388
366
|
if res.status_code == 200:
|
|
389
367
|
artifacts = res.json().get("artifacts", [])
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
# --- NEW: FIND ALL MATCHING ARTIFACTS ---
|
|
395
|
-
# Filter artifacts that belong to this specific App ID
|
|
396
|
-
target_artifacts = [a for a in artifacts if app_id in a.get("name", "")]
|
|
368
|
+
|
|
369
|
+
# 1. Filter artifacts that belong to this specific App ID
|
|
370
|
+
my_artifacts = [a for a in artifacts if app_id in a.get("name", "")]
|
|
397
371
|
|
|
398
|
-
if not
|
|
372
|
+
if not my_artifacts:
|
|
399
373
|
print_error(f"Could not find any recent artifacts for App ID: {app_id}")
|
|
400
374
|
return
|
|
401
375
|
|
|
402
|
-
|
|
376
|
+
# 2. SORT BY CREATED DATE (Newest first)
|
|
377
|
+
my_artifacts.sort(key=lambda x: x['created_at'], reverse=True)
|
|
378
|
+
|
|
379
|
+
# 3. Download the newest one
|
|
380
|
+
target_artifact = my_artifacts[0]
|
|
381
|
+
artifact_name = target_artifact['name']
|
|
382
|
+
|
|
383
|
+
print_info(f"Downloading latest build: {CYAN}{artifact_name}{RESET} (Created: {target_artifact['created_at']})")
|
|
384
|
+
|
|
385
|
+
secure_api_url = target_artifact.get('archive_download_url')
|
|
386
|
+
redirect_res = requests.get(secure_api_url, headers=headers, allow_redirects=False)
|
|
403
387
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
print_info(f"Preparing download for: {CYAN}{artifact_name}{RESET}")
|
|
388
|
+
if redirect_res.status_code == 302:
|
|
389
|
+
public_download_url = redirect_res.headers['Location']
|
|
390
|
+
filename = f"{artifact_name}.zip"
|
|
408
391
|
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
downloaded += len(chunk)
|
|
429
|
-
percentage = (downloaded / total_size * 100) if total_size > 0 else 0
|
|
430
|
-
print_progress_bar(percentage, f"Downloading {artifact_name}")
|
|
431
|
-
|
|
432
|
-
print_progress_bar(100.0, f"Downloading {artifact_name}")
|
|
433
|
-
print_success(f"Saved: {os.path.join(os.getcwd(), filename)}\n")
|
|
434
|
-
|
|
435
|
-
except Exception as e:
|
|
436
|
-
print_error(f"Download failed for {artifact_name}: {e}")
|
|
437
|
-
else:
|
|
438
|
-
print_error(f"Could not generate a temporary download link for {artifact_name}.")
|
|
392
|
+
try:
|
|
393
|
+
with requests.get(public_download_url, stream=True) as r:
|
|
394
|
+
r.raise_for_status()
|
|
395
|
+
total_size = int(r.headers.get('content-length', 0))
|
|
396
|
+
downloaded = 0
|
|
397
|
+
with open(filename, 'wb') as f:
|
|
398
|
+
for chunk in r.iter_content(chunk_size=8192):
|
|
399
|
+
if chunk:
|
|
400
|
+
f.write(chunk)
|
|
401
|
+
downloaded += len(chunk)
|
|
402
|
+
percentage = (downloaded / total_size * 100) if total_size > 0 else 0
|
|
403
|
+
print_progress_bar(percentage, f"Downloading {artifact_name}")
|
|
404
|
+
|
|
405
|
+
print_progress_bar(100.0, f"Downloading {artifact_name}")
|
|
406
|
+
print_success(f"Saved: {os.path.join(os.getcwd(), filename)}")
|
|
407
|
+
except Exception as e:
|
|
408
|
+
print_error(f"Download failed for {artifact_name}: {e}")
|
|
409
|
+
else:
|
|
410
|
+
print_error(f"Could not generate a temporary download link. Status: {redirect_res.status_code}")
|
|
439
411
|
|
|
440
412
|
else:
|
|
441
413
|
print_error(f"Failed to fetch artifacts. Status Code: {res.status_code}")
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import re
|
|
4
|
+
from .utils import print_info, print_success, print_warning
|
|
5
|
+
|
|
6
|
+
def inject_metadata_locally(config):
|
|
7
|
+
"""
|
|
8
|
+
Universally injects App Name, Package ID, Version, and Icons locally
|
|
9
|
+
for Android, iOS, Windows, and Linux before zipping.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
app_name = config.get("appName", "AppForge App")
|
|
13
|
+
package_id = config.get("packageId", "com.appforge.app")
|
|
14
|
+
app_version = config.get("version", "1.0.0")
|
|
15
|
+
icon_path = config.get("iconPath")
|
|
16
|
+
web_dir = config.get("webDir", "dist")
|
|
17
|
+
|
|
18
|
+
# Calculate build number (e.g., 1.0.5 -> 10005)
|
|
19
|
+
try:
|
|
20
|
+
parts = app_version.split('.')
|
|
21
|
+
build_number = int(f"{parts[0]}{parts[1].zfill(2)}{parts[2].zfill(2)}")
|
|
22
|
+
except:
|
|
23
|
+
build_number = 1
|
|
24
|
+
|
|
25
|
+
# Slug for Binary Names (e.g., "Vault OS" -> "vault_os")
|
|
26
|
+
binary_slug = re.sub(r'[^a-zA-Z0-9]', '_', app_name).lower()
|
|
27
|
+
|
|
28
|
+
print_info("Injecting metadata locally before upload...")
|
|
29
|
+
|
|
30
|
+
# --- 1. ANDROID & CAPACITOR INJECTION ---
|
|
31
|
+
manifest_path = "app/src/main/AndroidManifest.xml" if os.path.exists("app/src/main/AndroidManifest.xml") else "android/app/src/main/AndroidManifest.xml"
|
|
32
|
+
gradle_groovy = "app/build.gradle" if os.path.exists("app/build.gradle") else "android/app/build.gradle"
|
|
33
|
+
gradle_kts = gradle_groovy + ".kts"
|
|
34
|
+
|
|
35
|
+
if os.path.exists(manifest_path):
|
|
36
|
+
with open(manifest_path, 'r', encoding='utf-8') as f:
|
|
37
|
+
manifest = f.read()
|
|
38
|
+
manifest = re.sub(r'android:label="[^"]+"', f'android:label="{app_name}"', manifest)
|
|
39
|
+
if 'xmlns:tools=' not in manifest:
|
|
40
|
+
manifest = manifest.replace('<manifest', '<manifest xmlns:tools="http://schemas.android.com/tools"')
|
|
41
|
+
if 'tools:replace="android:label"' not in manifest:
|
|
42
|
+
manifest = manifest.replace('<application', '<application tools:replace="android:label"')
|
|
43
|
+
if 'package=' in manifest:
|
|
44
|
+
manifest = re.sub(r'package="[^"]+"', f'package="{package_id}"', manifest)
|
|
45
|
+
with open(manifest_path, 'w', encoding='utf-8') as f:
|
|
46
|
+
f.write(manifest)
|
|
47
|
+
print_success(f"Injected Android Name: {app_name}")
|
|
48
|
+
|
|
49
|
+
target_gradle = gradle_kts if os.path.exists(gradle_kts) else (gradle_groovy if os.path.exists(gradle_groovy) else None)
|
|
50
|
+
if target_gradle:
|
|
51
|
+
with open(target_gradle, 'r', encoding='utf-8') as f:
|
|
52
|
+
gradle = f.read()
|
|
53
|
+
gradle = re.sub(r'applicationId\s*=?\s*["\'][^"\']+["\']', f'applicationId = "{package_id}"', gradle)
|
|
54
|
+
gradle = re.sub(r'namespace\s*=?\s*["\'][^"\']+["\']', f'namespace = "{package_id}"', gradle)
|
|
55
|
+
gradle = re.sub(r'versionName\s*=?\s*["\'][^"\']+["\']', f'versionName = "{app_version}"', gradle)
|
|
56
|
+
gradle = re.sub(r'versionCode\s*=?\s*\d+', f'versionCode = {build_number}', gradle)
|
|
57
|
+
with open(target_gradle, 'w', encoding='utf-8') as f:
|
|
58
|
+
f.write(gradle)
|
|
59
|
+
print_success(f"Injected Android Package ID: {package_id}")
|
|
60
|
+
|
|
61
|
+
# --- 2. WINDOWS DESKTOP INJECTION ---
|
|
62
|
+
win_cmake = "windows/CMakeLists.txt"
|
|
63
|
+
if os.path.exists(win_cmake):
|
|
64
|
+
with open(win_cmake, 'r', encoding='utf-8') as f:
|
|
65
|
+
content = f.read()
|
|
66
|
+
# Set project name and binary name
|
|
67
|
+
content = re.sub(r'set\(BINARY_NAME "[^"]+"\)', f'set(BINARY_NAME "{binary_slug}")', content)
|
|
68
|
+
content = re.sub(r'project\([^)]+\)', f'project({binary_slug} LANGUAGES CXX)', content)
|
|
69
|
+
with open(win_cmake, 'w', encoding='utf-8') as f:
|
|
70
|
+
f.write(content)
|
|
71
|
+
|
|
72
|
+
# Update Window Title in main.cpp
|
|
73
|
+
win_main = "windows/runner/main.cpp"
|
|
74
|
+
if os.path.exists(win_main):
|
|
75
|
+
with open(win_main, 'r', encoding='utf-8') as f:
|
|
76
|
+
cpp = f.read()
|
|
77
|
+
cpp = re.sub(r'window\.CreateAndShow\(L"[^"]+"', f'window.CreateAndShow(L"{app_name}"', cpp)
|
|
78
|
+
with open(win_main, 'w', encoding='utf-8') as f:
|
|
79
|
+
f.write(cpp)
|
|
80
|
+
print_success(f"Injected Windows Title: {app_name}")
|
|
81
|
+
|
|
82
|
+
# --- 3. LINUX DESKTOP INJECTION ---
|
|
83
|
+
linux_cmake = "linux/CMakeLists.txt"
|
|
84
|
+
if os.path.exists(linux_cmake):
|
|
85
|
+
with open(linux_cmake, 'r', encoding='utf-8') as f:
|
|
86
|
+
content = f.read()
|
|
87
|
+
content = re.sub(r'set\(BINARY_NAME "[^"]+"\)', f'set(BINARY_NAME "{binary_slug}")', content)
|
|
88
|
+
with open(linux_cmake, 'w', encoding='utf-8') as f:
|
|
89
|
+
f.write(content)
|
|
90
|
+
|
|
91
|
+
# Update Window Title in application.cc
|
|
92
|
+
# Usually found in linux/my_application.cc
|
|
93
|
+
for file in os.listdir("linux"):
|
|
94
|
+
if file.endswith(".cc"):
|
|
95
|
+
cc_path = os.path.join("linux", file)
|
|
96
|
+
with open(cc_path, 'r', encoding='utf-8') as f:
|
|
97
|
+
cc = f.read()
|
|
98
|
+
cc = re.sub(r'gtk_window_set_title\(GTK_WINDOW\(window\), "[^"]+"\)', f'gtk_window_set_title(GTK_WINDOW(window), "{app_name}")', cc)
|
|
99
|
+
with open(cc_path, 'w', encoding='utf-8') as f:
|
|
100
|
+
f.write(cc)
|
|
101
|
+
print_success(f"Injected Linux Title: {app_name}")
|
|
102
|
+
|
|
103
|
+
# --- 4. FLUTTER PUBSPEC INJECTION ---
|
|
104
|
+
if os.path.exists("pubspec.yaml"):
|
|
105
|
+
with open("pubspec.yaml", 'r', encoding='utf-8') as f:
|
|
106
|
+
pubspec = f.read()
|
|
107
|
+
pubspec = re.sub(r'^version:\s*.*$', f'version: {app_version}+{build_number}', pubspec, flags=re.MULTILINE)
|
|
108
|
+
pubspec = re.sub(r'^name:\s*.*$', f'name: {binary_slug}', pubspec, flags=re.MULTILINE)
|
|
109
|
+
with open("pubspec.yaml", 'w', encoding='utf-8') as f:
|
|
110
|
+
f.write(pubspec)
|
|
111
|
+
print_success(f"Updated pubspec.yaml version and name.")
|
|
112
|
+
|
|
113
|
+
# --- 5. UNIVERSAL ICON INJECTION ---
|
|
114
|
+
if icon_path and os.path.exists(icon_path):
|
|
115
|
+
injected_icons = False
|
|
116
|
+
|
|
117
|
+
# Android Mipmaps (Local)
|
|
118
|
+
if os.path.exists(manifest_path):
|
|
119
|
+
res_dir = os.path.join(os.path.dirname(manifest_path), 'res')
|
|
120
|
+
mipmaps = ['mipmap-mdpi', 'mipmap-hdpi', 'mipmap-xhdpi', 'mipmap-xxhdpi', 'mipmap-xxxhdpi']
|
|
121
|
+
for m in mipmaps:
|
|
122
|
+
target_dir = os.path.join(res_dir, m)
|
|
123
|
+
if os.path.exists(target_dir):
|
|
124
|
+
shutil.copy2(icon_path, os.path.join(target_dir, 'ic_launcher.png'))
|
|
125
|
+
if os.path.exists(os.path.join(target_dir, 'ic_launcher_round.png')):
|
|
126
|
+
shutil.copy2(icon_path, os.path.join(target_dir, 'ic_launcher_round.png'))
|
|
127
|
+
injected_icons = True
|
|
128
|
+
|
|
129
|
+
# Web/Flutter Web (Favicons & PWA)
|
|
130
|
+
web_targets = ["web", "public", web_dir]
|
|
131
|
+
for w_dir in web_targets:
|
|
132
|
+
if os.path.exists(w_dir):
|
|
133
|
+
shutil.copy2(icon_path, os.path.join(w_dir, 'favicon.png'))
|
|
134
|
+
icons_dir = os.path.join(w_dir, 'icons')
|
|
135
|
+
if os.path.exists(icons_dir):
|
|
136
|
+
for p_icon in ['Icon-192.png', 'Icon-512.png', 'Icon-maskable-192.png', 'Icon-maskable-512.png']:
|
|
137
|
+
shutil.copy2(icon_path, os.path.join(icons_dir, p_icon))
|
|
138
|
+
injected_icons = True
|
|
139
|
+
|
|
140
|
+
if injected_icons:
|
|
141
|
+
print_success("Injected App Icons across all detected platforms.")
|
|
142
|
+
|
|
143
|
+
print_success("Local Metadata Injection Complete.")
|
|
@@ -258,5 +258,41 @@
|
|
|
258
258
|
"plugin": "pedometer",
|
|
259
259
|
"feature": "Pedometer (Step Counter)",
|
|
260
260
|
"desc": "Found Flutter pedometer dependency"
|
|
261
|
-
}
|
|
261
|
+
},
|
|
262
|
+
"android.permission.CAMERA": { "plugin": "CAMERA", "feature": "Camera Access", "desc": "Required for taking photos or videos" },
|
|
263
|
+
"android.permission.ACCESS_FINE_LOCATION": { "plugin": "LOCATION", "feature": "Precise GPS", "desc": "Required for high-accuracy tracking" },
|
|
264
|
+
"android.permission.ACCESS_COARSE_LOCATION": { "plugin": "LOCATION", "feature": "Approximate Location", "desc": "Required for city-level positioning" },
|
|
265
|
+
"android.permission.ACCESS_BACKGROUND_LOCATION": { "plugin": "LOCATION", "feature": "Background Location", "desc": "Required for tracking while app is closed" },
|
|
266
|
+
"android.permission.RECORD_AUDIO": { "plugin": "MICROPHONE", "feature": "Microphone", "desc": "Required for audio recording" },
|
|
267
|
+
"android.permission.READ_EXTERNAL_STORAGE": { "plugin": "STORAGE", "feature": "Read Files", "desc": "Required to access user media" },
|
|
268
|
+
"android.permission.WRITE_EXTERNAL_STORAGE": { "plugin": "STORAGE", "feature": "Write Files", "desc": "Required to save files" },
|
|
269
|
+
"android.permission.READ_MEDIA_IMAGES": { "plugin": "STORAGE", "feature": "Photo Access", "desc": "Required for Android 13+ Media access" },
|
|
270
|
+
"android.permission.READ_MEDIA_VIDEO": { "plugin": "STORAGE", "feature": "Video Access", "desc": "Required for Android 13+ Media access" },
|
|
271
|
+
"android.permission.BLUETOOTH": { "plugin": "BLUETOOTH", "feature": "Classic Bluetooth", "desc": "Legacy Bluetooth support" },
|
|
272
|
+
"android.permission.BLUETOOTH_SCAN": { "plugin": "BLUETOOTH", "feature": "BLE Scanning", "desc": "Required for finding BLE devices" },
|
|
273
|
+
"android.permission.BLUETOOTH_CONNECT": { "plugin": "BLUETOOTH", "feature": "BLE Connecting", "desc": "Required to connect to BLE devices" },
|
|
274
|
+
"android.permission.USE_BIOMETRIC": { "plugin": "BIOMETRIC", "feature": "Face/Fingerprint", "desc": "Required for biometric authentication" },
|
|
275
|
+
"android.permission.READ_CONTACTS": { "plugin": "CONTACTS", "feature": "Contacts", "desc": "Required to access address book" },
|
|
276
|
+
"android.permission.CALL_PHONE": { "plugin": "PHONE", "feature": "Phone Calls", "desc": "Required to trigger phone dialer" },
|
|
277
|
+
"android.permission.SEND_SMS": { "plugin": "SMS", "feature": "Send SMS", "desc": "Required to send messages" },
|
|
278
|
+
"android.permission.RECEIVE_SMS": { "plugin": "SMS", "feature": "Receive SMS", "desc": "Required to read incoming OTPs" },
|
|
279
|
+
"android.permission.INTERNET": { "plugin": "NETWORK", "feature": "Internet", "desc": "Standard connectivity" },
|
|
280
|
+
"android.permission.ACCESS_NETWORK_STATE": { "plugin": "NETWORK", "feature": "Network Status", "desc": "Check connection quality" },
|
|
281
|
+
"android.permission.WAKE_LOCK": { "plugin": "POWER", "feature": "Wake Lock", "desc": "Prevent screen from dimming" },
|
|
282
|
+
"android.permission.RECEIVE_BOOT_COMPLETED": { "plugin": "SYSTEM", "feature": "Boot Receiver", "desc": "Required to run services on startup" },
|
|
283
|
+
"android.permission.VIBRATE": { "plugin": "HAPTICS", "feature": "Vibration", "desc": "Required for haptic feedback" },
|
|
284
|
+
"android.permission.FOREGROUND_SERVICE": { "plugin": "SERVICES", "feature": "Foreground Service", "desc": "Required for long-running tasks" },
|
|
285
|
+
"android.permission.POST_NOTIFICATIONS": { "plugin": "NOTIFICATIONS", "feature": "Push Notifications", "desc": "Required for Android 13+ notifications" },
|
|
286
|
+
"android.permission.USE_BIOMETRIC": { "plugin": "BIOMETRIC", "feature": "Biometric Authentication", "desc": "Required for fingerprint or face unlock" },
|
|
287
|
+
"android.permission.WRITE_SETTINGS": { "plugin": "SYSTEM_SETTINGS", "feature": "System Settings", "desc": "Required to modify system display or volume settings" },
|
|
288
|
+
"android.permission.BODY_SENSORS": { "plugin": "SENSORS", "feature": "Health Sensors", "desc": "Required for heart rate or step tracking" },
|
|
289
|
+
"android.permission.REQUEST_INSTALL_PACKAGES": { "plugin": "SYSTEM_INSTALL", "feature": "Package Installer", "desc": "Required for in-app updates/APK installation" },
|
|
290
|
+
"android.permission.ACCESS_WIFI_STATE": { "plugin": "NETWORK", "feature": "Wi-Fi State", "desc": "Required to check Wi-Fi connection info" },
|
|
291
|
+
"android.permission.CHANGE_WIFI_STATE": { "plugin": "NETWORK", "feature": "Wi-Fi Control", "desc": "Required to toggle Wi-Fi on or off" },
|
|
292
|
+
"android.permission.READ_CALENDAR": { "plugin": "CALENDAR", "feature": "Calendar Read", "desc": "Required to read user events" },
|
|
293
|
+
"android.permission.WRITE_CALENDAR": { "plugin": "CALENDAR", "feature": "Calendar Write", "desc": "Required to add events to calendar" },
|
|
294
|
+
"android.permission.MODIFY_AUDIO_SETTINGS": { "plugin": "AUDIO", "feature": "Audio Control", "desc": "Required to change speaker or headphone volume" },
|
|
295
|
+
"android.permission.SCHEDULE_EXACT_ALARM": { "plugin": "ALARMS", "feature": "Exact Alarms", "desc": "Required for precision-timed app notifications" },
|
|
296
|
+
"android.permission.SYSTEM_ALERT_WINDOW": { "plugin": "OVERLAY", "feature": "Display Over Other Apps", "desc": "Required to show floating windows above other applications"},
|
|
297
|
+
"android.permission.CHANGE_WIFI_MULTICAST_STATE": { "plugin": "NETWORK", "feature": "Wi-Fi Multicast Discovery", "desc": "Required for automatic discovery of nearby devices on the local Wi-Fi network" }
|
|
262
298
|
}
|
|
@@ -14,8 +14,8 @@ RED = "\033[31m"
|
|
|
14
14
|
GRAY = "\033[90m"
|
|
15
15
|
BLUE = "\033[34m"
|
|
16
16
|
|
|
17
|
-
CLI_VERSION = "
|
|
18
|
-
|
|
17
|
+
CLI_VERSION = "4.6.0"
|
|
18
|
+
printed_log_lines = set()
|
|
19
19
|
def check_for_cli_updates():
|
|
20
20
|
try:
|
|
21
21
|
res = requests.get("https://pypi.org/pypi/appforge-cli/json", timeout=2)
|
|
@@ -35,6 +35,18 @@ def print_header():
|
|
|
35
35
|
print(f"{YELLOW}⭐ A new version of AppForge is available! (v{latest}){RESET}")
|
|
36
36
|
print(f"{GRAY}Run: pip install --upgrade appforge-cli{RESET}\n")
|
|
37
37
|
|
|
38
|
+
def print_live_log(log_text):
|
|
39
|
+
global printed_log_lines
|
|
40
|
+
lines = log_text.splitlines()
|
|
41
|
+
|
|
42
|
+
# Only look at the last 15 lines of the build
|
|
43
|
+
for line in lines[-15:]:
|
|
44
|
+
# Filter for relevant build info
|
|
45
|
+
if any(x in line for x in [":app:", "Task", "Building", ">"]):
|
|
46
|
+
if line not in printed_log_lines:
|
|
47
|
+
print(f"{GRAY} {line[:80]}{RESET}")
|
|
48
|
+
printed_log_lines.add(line)
|
|
49
|
+
|
|
38
50
|
def print_footer():
|
|
39
51
|
print(f"\n{GRAY}AppForge CLI")
|
|
40
52
|
print(f"Built by AppForge Team [Maintainer - JuniorSir]{RESET}\n")
|
|
@@ -2,7 +2,7 @@ from setuptools import setup
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name="appforge",
|
|
5
|
-
version="
|
|
5
|
+
version="4.6.0",
|
|
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"],
|
|
@@ -12,7 +12,8 @@ setup(
|
|
|
12
12
|
include_package_data=True,
|
|
13
13
|
install_requires=[
|
|
14
14
|
"requests",
|
|
15
|
-
"packaging"
|
|
15
|
+
"packaging",
|
|
16
|
+
"nacl"
|
|
16
17
|
],
|
|
17
18
|
entry_points={
|
|
18
19
|
"console_scripts": [
|
|
@@ -1,125 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import shutil
|
|
3
|
-
import re
|
|
4
|
-
from .utils import print_info, print_success, print_warning
|
|
5
|
-
|
|
6
|
-
def inject_metadata_locally(config):
|
|
7
|
-
"""Injects App Name, Package ID, Version, and Icons locally before zipping."""
|
|
8
|
-
|
|
9
|
-
app_name = config.get("appName", "AppForge App")
|
|
10
|
-
package_id = config.get("packageId", "com.appforge.app")
|
|
11
|
-
app_version = config.get("version", "1.0.0")
|
|
12
|
-
icon_path = config.get("iconPath")
|
|
13
|
-
web_dir = config.get("webDir", "dist")
|
|
14
|
-
project_category = config.get("category", "web")
|
|
15
|
-
|
|
16
|
-
try:
|
|
17
|
-
parts = app_version.split('.')
|
|
18
|
-
build_number = int(f"{parts[0]}{parts[1].zfill(2)}{parts[2].zfill(2)}")
|
|
19
|
-
except:
|
|
20
|
-
build_number = 1
|
|
21
|
-
|
|
22
|
-
print_info("Injecting metadata locally before upload...")
|
|
23
|
-
|
|
24
|
-
# --- PATH DETECTION ---
|
|
25
|
-
manifest_path = "app/src/main/AndroidManifest.xml" if os.path.exists("app/src/main/AndroidManifest.xml") else "android/app/src/main/AndroidManifest.xml"
|
|
26
|
-
gradle_groovy = "app/build.gradle" if os.path.exists("app/build.gradle") else "android/app/build.gradle"
|
|
27
|
-
gradle_kts = gradle_groovy + ".kts"
|
|
28
|
-
|
|
29
|
-
# --- 1. INJECT APP NAME (AndroidManifest.xml) ---
|
|
30
|
-
if os.path.exists(manifest_path):
|
|
31
|
-
with open(manifest_path, 'r', encoding='utf-8') as f:
|
|
32
|
-
manifest = f.read()
|
|
33
|
-
|
|
34
|
-
manifest = re.sub(r'android:label="[^"]+"', f'android:label="{app_name}"', manifest)
|
|
35
|
-
if 'xmlns:tools=' not in manifest:
|
|
36
|
-
manifest = manifest.replace('<manifest', '<manifest xmlns:tools="http://schemas.android.com/tools"')
|
|
37
|
-
if 'tools:replace="android:label"' not in manifest:
|
|
38
|
-
manifest = manifest.replace('<application', '<application tools:replace="android:label"')
|
|
39
|
-
if 'package=' in manifest:
|
|
40
|
-
manifest = re.sub(r'package="[^"]+"', f'package="{package_id}"', manifest)
|
|
41
|
-
|
|
42
|
-
with open(manifest_path, 'w', encoding='utf-8') as f:
|
|
43
|
-
f.write(manifest)
|
|
44
|
-
print_success(f"Injected App Name: {app_name}")
|
|
45
|
-
|
|
46
|
-
# --- 2. INJECT PACKAGE ID & VERSION (build.gradle) ---
|
|
47
|
-
target_gradle = gradle_kts if os.path.exists(gradle_kts) else (gradle_groovy if os.path.exists(gradle_groovy) else None)
|
|
48
|
-
|
|
49
|
-
if target_gradle:
|
|
50
|
-
with open(target_gradle, 'r', encoding='utf-8') as f:
|
|
51
|
-
gradle = f.read()
|
|
52
|
-
|
|
53
|
-
gradle = re.sub(r'applicationId\s*=?\s*["\'][^"\']+["\']', f'applicationId = "{package_id}"', gradle)
|
|
54
|
-
gradle = re.sub(r'applicationId\s+["\'][^"\']+["\']', f'applicationId "{package_id}"', gradle)
|
|
55
|
-
gradle = re.sub(r'namespace\s*=?\s*["\'][^"\']+["\']', f'namespace = "{package_id}"', gradle)
|
|
56
|
-
gradle = re.sub(r'namespace\s+["\'][^"\']+["\']', f'namespace "{package_id}"', gradle)
|
|
57
|
-
gradle = re.sub(r'versionName\s*=?\s*["\'][^"\']+["\']', f'versionName = "{app_version}"', gradle)
|
|
58
|
-
gradle = re.sub(r'versionName\s+["\'][^"\']+["\']', f'versionName "{app_version}"', gradle)
|
|
59
|
-
gradle = re.sub(r'versionCode\s*=?\s*\d+', f'versionCode = {build_number}', gradle)
|
|
60
|
-
gradle = re.sub(r'versionCode\s+\d+', f'versionCode {build_number}', gradle)
|
|
61
|
-
|
|
62
|
-
with open(target_gradle, 'w', encoding='utf-8') as f:
|
|
63
|
-
f.write(gradle)
|
|
64
|
-
print_success(f"Injected Package ID: {package_id} & Version: {app_version}")
|
|
65
|
-
|
|
66
|
-
# --- 3. INJECT PUBSPEC.YAML (Flutter Specifics) ---
|
|
67
|
-
if os.path.exists("pubspec.yaml"):
|
|
68
|
-
with open("pubspec.yaml", 'r', encoding='utf-8') as f:
|
|
69
|
-
pubspec = f.read()
|
|
70
|
-
|
|
71
|
-
# 3a. Update Version
|
|
72
|
-
if re.search(r'^version:\s*.*$', pubspec, re.MULTILINE):
|
|
73
|
-
pubspec = re.sub(r'^version:\s*.*$', f'version: {app_version}+{build_number}', pubspec, flags=re.MULTILINE)
|
|
74
|
-
|
|
75
|
-
# 3b. Update Project Name (Must be snake_case for Dart!)
|
|
76
|
-
# e.g. "Vault Downloader" -> "vault_downloader"
|
|
77
|
-
snake_case_name = re.sub(r'[^a-zA-Z0-9]', '_', app_name).lower()
|
|
78
|
-
if re.search(r'^name:\s*.*$', pubspec, re.MULTILINE):
|
|
79
|
-
pubspec = re.sub(r'^name:\s*.*$', f'name: {snake_case_name}', pubspec, flags=re.MULTILINE)
|
|
80
|
-
|
|
81
|
-
with open("pubspec.yaml", 'w', encoding='utf-8') as f:
|
|
82
|
-
f.write(pubspec)
|
|
83
|
-
print_success(f"Updated pubspec.yaml (name: {snake_case_name}, version: {app_version})")
|
|
84
|
-
|
|
85
|
-
# --- 4. INJECT APP ICONS (Universal) ---
|
|
86
|
-
if icon_path and os.path.exists(icon_path):
|
|
87
|
-
injected_icons = False
|
|
88
|
-
|
|
89
|
-
# 4a. Android Mipmaps
|
|
90
|
-
if os.path.exists(manifest_path):
|
|
91
|
-
res_dir = os.path.join(os.path.dirname(manifest_path), 'res')
|
|
92
|
-
mipmaps = ['mipmap-mdpi', 'mipmap-hdpi', 'mipmap-xhdpi', 'mipmap-xxhdpi', 'mipmap-xxxhdpi']
|
|
93
|
-
for m in mipmaps:
|
|
94
|
-
target_dir = os.path.join(res_dir, m)
|
|
95
|
-
if os.path.exists(target_dir):
|
|
96
|
-
shutil.copyfile(icon_path, os.path.join(target_dir, 'ic_launcher.png'))
|
|
97
|
-
round_icon = os.path.join(target_dir, 'ic_launcher_round.png')
|
|
98
|
-
if os.path.exists(round_icon):
|
|
99
|
-
shutil.copyfile(icon_path, round_icon)
|
|
100
|
-
injected_icons = True
|
|
101
|
-
|
|
102
|
-
# 4b. Web / Flutter Web Icons (PWA Support)
|
|
103
|
-
# Check standard web directories
|
|
104
|
-
web_targets = ["web", "public", web_dir]
|
|
105
|
-
for w_dir in web_targets:
|
|
106
|
-
if os.path.exists(w_dir):
|
|
107
|
-
# Replace Favicon
|
|
108
|
-
shutil.copyfile(icon_path, os.path.join(w_dir, 'favicon.png'))
|
|
109
|
-
if os.path.exists(os.path.join(w_dir, 'favicon.ico')):
|
|
110
|
-
shutil.copyfile(icon_path, os.path.join(w_dir, 'favicon.ico'))
|
|
111
|
-
|
|
112
|
-
# Replace PWA Icons
|
|
113
|
-
icons_dir = os.path.join(w_dir, 'icons')
|
|
114
|
-
if os.path.exists(icons_dir):
|
|
115
|
-
pwa_icons = ['Icon-192.png', 'Icon-512.png', 'Icon-maskable-192.png', 'Icon-maskable-512.png']
|
|
116
|
-
for p_icon in pwa_icons:
|
|
117
|
-
shutil.copyfile(icon_path, os.path.join(icons_dir, p_icon))
|
|
118
|
-
injected_icons = True
|
|
119
|
-
|
|
120
|
-
if injected_icons:
|
|
121
|
-
print_success("Injected custom App Icons across Native and Web directories.")
|
|
122
|
-
else:
|
|
123
|
-
print_warning("Icon path found, but no valid target directories (res/mipmap or web/icons) exist yet.")
|
|
124
|
-
|
|
125
|
-
print_success("Local Metadata Injection Complete. Project is ready for upload.")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|