appforge-cli 1.6.6__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.6 → appforge_cli-2.6.6}/PKG-INFO +1 -1
- {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/ai.py +12 -0
- {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/cli.py +15 -0
- {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/github.py +122 -159
- {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/knowledge_base.json +37 -1
- {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/utils.py +14 -2
- {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge_cli.egg-info/PKG-INFO +1 -1
- {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge_cli.egg-info/SOURCES.txt +1 -0
- {appforge_cli-1.6.6 → appforge_cli-2.6.6}/pyproject.toml +1 -1
- {appforge_cli-1.6.6 → appforge_cli-2.6.6}/setup.py +3 -2
- {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/__init__.py +0 -0
- {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/capacitor.py +0 -0
- {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/config.py +0 -0
- {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/create.py +0 -0
- {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/default_icon.png +0 -0
- {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/detector.py +0 -0
- {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge/injector.py +0 -0
- {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge_cli.egg-info/dependency_links.txt +0 -0
- {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge_cli.egg-info/entry_points.txt +0 -0
- {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge_cli.egg-info/requires.txt +0 -0
- {appforge_cli-1.6.6 → appforge_cli-2.6.6}/appforge_cli.egg-info/top_level.txt +0 -0
- {appforge_cli-1.6.6 → 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")
|
|
@@ -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,6 +23,35 @@ def get_headers():
|
|
|
22
23
|
"X-GitHub-Api-Version": "2022-11-28"
|
|
23
24
|
}
|
|
24
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
|
|
54
|
+
|
|
25
55
|
def zip_project(category, sub_path=".", zip_name="app_source.zip"):
|
|
26
56
|
"""Zips the project, ensuring the icon is ready for @capacitor/assets."""
|
|
27
57
|
config = load_local_config()
|
|
@@ -102,7 +132,8 @@ def push_and_build(config):
|
|
|
102
132
|
platform = config.get("platform", "android")
|
|
103
133
|
app_id = get_app_id()
|
|
104
134
|
sub_path = config.get("sub_path", ".")
|
|
105
|
-
|
|
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.")
|
|
106
137
|
try:
|
|
107
138
|
inject_metadata_locally(config)
|
|
108
139
|
except Exception as e:
|
|
@@ -215,154 +246,92 @@ def push_and_build(config):
|
|
|
215
246
|
"status": "triggered" # We can update this later
|
|
216
247
|
}
|
|
217
248
|
add_to_history(new_entry)
|
|
249
|
+
|
|
218
250
|
def check_status():
|
|
219
|
-
"""Streams live build
|
|
251
|
+
"""Streams live build progress with clean, animated step tracking."""
|
|
220
252
|
headers = get_headers()
|
|
221
253
|
app_id = get_app_id()
|
|
222
|
-
url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/runs?per_page=1"
|
|
223
|
-
res = requests.get(url, headers=headers)
|
|
224
254
|
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
return
|
|
228
|
-
|
|
229
|
-
runs = res.json().get("workflow_runs", [])
|
|
255
|
+
# 1. Find the Run ID
|
|
256
|
+
print_info(f"Connecting to cloud build for App: {app_id}...")
|
|
230
257
|
my_run = None
|
|
231
|
-
for
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
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)
|
|
235
268
|
|
|
236
269
|
if not my_run:
|
|
237
|
-
|
|
238
|
-
print(f"{GRAY}Note: New builds may take 10 seconds to appear in the list.{RESET}")
|
|
239
|
-
return
|
|
240
|
-
|
|
241
|
-
run_id = my_run["id"]
|
|
242
|
-
status = my_run["status"]
|
|
243
|
-
|
|
244
|
-
print_info(f"Connected to your Cloud Build #{run_id}. Streaming live logs...\n")
|
|
245
|
-
|
|
246
|
-
if not runs:
|
|
247
|
-
print_info("No builds found in the cloud yet.")
|
|
248
|
-
return
|
|
249
|
-
|
|
250
|
-
if status == "completed":
|
|
251
|
-
conclusion = latest_run.get("conclusion")
|
|
252
|
-
if conclusion == "success":
|
|
253
|
-
print_success(f"Build #{run_id} is already completed! Run 'appforge download'")
|
|
254
|
-
else:
|
|
255
|
-
print_error(f"Build #{run_id} failed with conclusion: {conclusion}")
|
|
270
|
+
print_error(f"No active builds found for App ID: {app_id}.")
|
|
256
271
|
return
|
|
257
272
|
|
|
258
|
-
|
|
259
|
-
|
|
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
|
+
|
|
260
279
|
jobs_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/runs/{run_id}/jobs"
|
|
261
|
-
|
|
262
280
|
completed_steps = set()
|
|
263
|
-
current_running_step = None
|
|
264
281
|
spinner_chars = "|/-\\"
|
|
265
282
|
spinner_idx = 0
|
|
266
|
-
|
|
283
|
+
|
|
267
284
|
try:
|
|
268
285
|
while True:
|
|
269
|
-
# Animate the currently running step while we wait for the API ping
|
|
270
|
-
for _ in range(20): # Loop fast for smooth animation (20 * 0.15s = 3 seconds)
|
|
271
|
-
if current_running_step:
|
|
272
|
-
char = spinner_chars[spinner_idx % len(spinner_chars)]
|
|
273
|
-
# Highlight custom steps in Cyan, normal steps in Gray
|
|
274
|
-
color = CYAN if "Permission" in current_running_step else GRAY
|
|
275
|
-
sys.stdout.write(f"\r{color}{char}{RESET} [Cloud] {current_running_step}...")
|
|
276
|
-
sys.stdout.flush()
|
|
277
|
-
spinner_idx += 1
|
|
278
|
-
time.sleep(0.15)
|
|
279
|
-
|
|
280
|
-
# Ping the GitHub API
|
|
281
286
|
jobs_res = requests.get(jobs_url, headers=headers)
|
|
282
287
|
if jobs_res.status_code != 200:
|
|
288
|
+
time.sleep(3)
|
|
283
289
|
continue
|
|
284
|
-
|
|
290
|
+
|
|
285
291
|
jobs = jobs_res.json().get("jobs", [])
|
|
286
292
|
if not jobs:
|
|
293
|
+
time.sleep(3)
|
|
287
294
|
continue
|
|
288
295
|
|
|
289
296
|
active_job = jobs[0]
|
|
290
|
-
|
|
297
|
+
current_step = None
|
|
291
298
|
|
|
292
|
-
|
|
293
|
-
for step in steps:
|
|
294
|
-
|
|
295
|
-
|
|
299
|
+
# Identify current step
|
|
300
|
+
for step in active_job.get("steps", []):
|
|
301
|
+
if step["status"] == "in_progress":
|
|
302
|
+
current_step = step["name"]
|
|
296
303
|
|
|
297
|
-
if
|
|
298
|
-
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}")
|
|
299
308
|
|
|
300
|
-
sys.stdout.write(f"\r{GREEN}✔{RESET} [Cloud] {step_name}... {GREEN}done!{RESET} \n")
|
|
301
|
-
sys.stdout.flush()
|
|
302
|
-
|
|
303
309
|
if step.get("conclusion") == "failure":
|
|
304
|
-
print_error(f"
|
|
305
|
-
|
|
306
|
-
try:
|
|
307
|
-
log_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/jobs/{active_job['id']}/logs"
|
|
308
|
-
log_res = requests.get(log_url, headers=headers)
|
|
309
|
-
if log_res.status_code == 200:
|
|
310
|
-
friendly_msg = get_friendly_error(log_res.text)
|
|
311
|
-
print(f"\n{YELLOW}💡 AppForge Diagnosis:{RESET}\n{friendly_msg}\n")
|
|
312
|
-
except:
|
|
313
|
-
pass
|
|
310
|
+
print_error(f"Cloud Build Failed at: {step['name']}")
|
|
314
311
|
return
|
|
315
|
-
if step.get("conclusion") == "failure":
|
|
316
|
-
print_error(f"\n❌ Cloud Build Failed at step: {step_name}")
|
|
317
|
-
return
|
|
318
|
-
|
|
319
|
-
if step_status == "in_progress":
|
|
320
|
-
new_running_step = step_name
|
|
321
|
-
|
|
322
|
-
# Update the current step for the animation loop
|
|
323
|
-
current_running_step = new_running_step
|
|
324
312
|
|
|
325
|
-
#
|
|
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
|
|
326
321
|
if active_job["status"] == "completed":
|
|
327
|
-
|
|
328
|
-
|
|
322
|
+
sys.stdout.write("\r" + " " * 60 + "\r")
|
|
329
323
|
if active_job["conclusion"] == "success":
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
art_res = requests.get(artifacts_url, headers=headers)
|
|
333
|
-
|
|
334
|
-
if art_res.status_code == 200 and art_res.json().get("total_count", 0) > 0:
|
|
335
|
-
# EVERYTHING IS PERFECT
|
|
336
|
-
print_success(f"{BOLD}✨ Cloud Build Finished Successfully!{RESET}")
|
|
337
|
-
print_info("Run 'appforge download' to get your app.")
|
|
338
|
-
else:
|
|
339
|
-
# SOFT FAILURE: Success but no file!
|
|
340
|
-
print_warning(f"\n⚠ Build finished, but NO ARTIFACT (APK) was produced.")
|
|
341
|
-
# Fetch logs to find out why
|
|
342
|
-
try:
|
|
343
|
-
log_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/jobs/{active_job['id']}/logs"
|
|
344
|
-
log_res = requests.get(log_url, headers=headers)
|
|
345
|
-
friendly_msg = get_friendly_error(log_res.text)
|
|
346
|
-
print(f"\n{YELLOW}💡 AppForge Diagnosis:{RESET}\n{friendly_msg}\n")
|
|
347
|
-
except:
|
|
348
|
-
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.")
|
|
349
326
|
else:
|
|
350
|
-
|
|
351
|
-
print_error("\n❌ Cloud Build Crashed.")
|
|
352
|
-
# Fetch logs for hard failure
|
|
353
|
-
try:
|
|
354
|
-
log_url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/jobs/{active_job['id']}/logs"
|
|
355
|
-
log_res = requests.get(log_url, headers=headers)
|
|
356
|
-
friendly_msg = get_friendly_error(log_res.text)
|
|
357
|
-
print(f"\n{YELLOW}💡 AppForge Diagnosis:{RESET}\n{friendly_msg}\n")
|
|
358
|
-
except: pass
|
|
327
|
+
print_error("Cloud Build Crashed.")
|
|
359
328
|
break
|
|
360
329
|
|
|
330
|
+
time.sleep(1.5)
|
|
331
|
+
|
|
361
332
|
except KeyboardInterrupt:
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
print(f"\n{YELLOW}⚠ Stopped watching logs. The build is still running in the cloud.{RESET}")
|
|
365
|
-
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.")
|
|
366
335
|
|
|
367
336
|
def get_build_status_by_id(run_id):
|
|
368
337
|
headers = get_headers()
|
|
@@ -381,7 +350,7 @@ def get_build_status_by_id(run_id):
|
|
|
381
350
|
|
|
382
351
|
def download_apk(run_id=None):
|
|
383
352
|
"""
|
|
384
|
-
Finds and downloads ALL artifacts
|
|
353
|
+
Finds and downloads ALL artifacts for a build.
|
|
385
354
|
"""
|
|
386
355
|
headers = get_headers()
|
|
387
356
|
app_id = get_app_id()
|
|
@@ -396,55 +365,49 @@ def download_apk(run_id=None):
|
|
|
396
365
|
|
|
397
366
|
if res.status_code == 200:
|
|
398
367
|
artifacts = res.json().get("artifacts", [])
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
# --- NEW: FIND ALL MATCHING ARTIFACTS ---
|
|
404
|
-
# Filter artifacts that belong to this specific App ID
|
|
405
|
-
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", "")]
|
|
406
371
|
|
|
407
|
-
if not
|
|
372
|
+
if not my_artifacts:
|
|
408
373
|
print_error(f"Could not find any recent artifacts for App ID: {app_id}")
|
|
409
374
|
return
|
|
410
375
|
|
|
411
|
-
|
|
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)
|
|
412
387
|
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
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"
|
|
417
391
|
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
downloaded += len(chunk)
|
|
438
|
-
percentage = (downloaded / total_size * 100) if total_size > 0 else 0
|
|
439
|
-
print_progress_bar(percentage, f"Downloading {artifact_name}")
|
|
440
|
-
|
|
441
|
-
print_progress_bar(100.0, f"Downloading {artifact_name}")
|
|
442
|
-
print_success(f"Saved: {os.path.join(os.getcwd(), filename)}\n")
|
|
443
|
-
|
|
444
|
-
except Exception as e:
|
|
445
|
-
print_error(f"Download failed for {artifact_name}: {e}")
|
|
446
|
-
else:
|
|
447
|
-
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}")
|
|
448
411
|
|
|
449
412
|
else:
|
|
450
413
|
print_error(f"Failed to fetch artifacts. Status Code: {res.status_code}")
|
|
@@ -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": [
|
|
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
|
|
File without changes
|
|
File without changes
|