appforge-cli 1.6.2__tar.gz → 1.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.2 → appforge_cli-1.6.6}/PKG-INFO +1 -1
- {appforge_cli-1.6.2 → appforge_cli-1.6.6}/appforge/cli.py +27 -28
- appforge_cli-1.6.6/appforge/create.py +196 -0
- {appforge_cli-1.6.2 → appforge_cli-1.6.6}/appforge/github.py +19 -10
- appforge_cli-1.6.6/appforge/injector.py +143 -0
- {appforge_cli-1.6.2 → appforge_cli-1.6.6}/appforge/utils.py +1 -1
- {appforge_cli-1.6.2 → appforge_cli-1.6.6}/appforge_cli.egg-info/PKG-INFO +1 -1
- {appforge_cli-1.6.2 → appforge_cli-1.6.6}/pyproject.toml +1 -1
- {appforge_cli-1.6.2 → appforge_cli-1.6.6}/setup.py +1 -1
- appforge_cli-1.6.2/appforge/create.py +0 -106
- appforge_cli-1.6.2/appforge/injector.py +0 -125
- {appforge_cli-1.6.2 → appforge_cli-1.6.6}/appforge/__init__.py +0 -0
- {appforge_cli-1.6.2 → appforge_cli-1.6.6}/appforge/ai.py +0 -0
- {appforge_cli-1.6.2 → appforge_cli-1.6.6}/appforge/capacitor.py +0 -0
- {appforge_cli-1.6.2 → appforge_cli-1.6.6}/appforge/config.py +0 -0
- {appforge_cli-1.6.2 → appforge_cli-1.6.6}/appforge/default_icon.png +0 -0
- {appforge_cli-1.6.2 → appforge_cli-1.6.6}/appforge/detector.py +0 -0
- {appforge_cli-1.6.2 → appforge_cli-1.6.6}/appforge/knowledge_base.json +0 -0
- {appforge_cli-1.6.2 → appforge_cli-1.6.6}/appforge_cli.egg-info/SOURCES.txt +0 -0
- {appforge_cli-1.6.2 → appforge_cli-1.6.6}/appforge_cli.egg-info/dependency_links.txt +0 -0
- {appforge_cli-1.6.2 → appforge_cli-1.6.6}/appforge_cli.egg-info/entry_points.txt +0 -0
- {appforge_cli-1.6.2 → appforge_cli-1.6.6}/appforge_cli.egg-info/requires.txt +0 -0
- {appforge_cli-1.6.2 → appforge_cli-1.6.6}/appforge_cli.egg-info/top_level.txt +0 -0
- {appforge_cli-1.6.2 → appforge_cli-1.6.6}/setup.cfg +0 -0
|
@@ -15,30 +15,34 @@ from .capacitor import install_plugins
|
|
|
15
15
|
from .create import create_project
|
|
16
16
|
|
|
17
17
|
def init_project():
|
|
18
|
+
# 1. Load existing config first to see if 'create' already set things up
|
|
19
|
+
current_config = load_local_config()
|
|
20
|
+
|
|
18
21
|
project_info = detect_project()
|
|
19
22
|
category = project_info.get("category", "web")
|
|
20
23
|
proj_type = project_info.get("type", "unknown")
|
|
21
24
|
default_icon_dest = os.path.join(os.getcwd(), "appforge_icon.png")
|
|
22
25
|
|
|
23
|
-
current_config = load_local_config()
|
|
24
26
|
if not current_config.get("iconPath") or not os.path.exists(current_config.get("iconPath")):
|
|
25
27
|
try:
|
|
26
28
|
cli_dir = os.path.dirname(os.path.abspath(__file__))
|
|
27
29
|
bundled_icon = os.path.join(cli_dir, "default_icon.png")
|
|
28
|
-
|
|
29
30
|
if os.path.exists(bundled_icon):
|
|
30
31
|
shutil.copy2(bundled_icon, default_icon_dest)
|
|
31
32
|
save_local_config({"iconPath": default_icon_dest})
|
|
32
33
|
print_info("Installed default AppForge app icon.")
|
|
33
|
-
except Exception
|
|
34
|
+
except Exception:
|
|
34
35
|
pass
|
|
35
36
|
|
|
37
|
+
# 2. Get the platform that was saved during 'create' (default to android if new)
|
|
38
|
+
saved_platform = current_config.get("platform", "android")
|
|
36
39
|
save_local_config({"category": category, "project_type": proj_type})
|
|
37
40
|
|
|
38
41
|
if category == "web":
|
|
39
|
-
web_dir = prompt("Confirm web build directory", project_info["webDir"])
|
|
40
|
-
|
|
41
|
-
|
|
42
|
+
web_dir = prompt("Confirm web build directory", current_config.get("webDir", project_info["webDir"]))
|
|
43
|
+
# --- FIXED: Uses saved_platform as default ---
|
|
44
|
+
platform_choice = prompt("Select platforms (android, ios, both)", saved_platform).lower()
|
|
45
|
+
if platform_choice in ["both", "all"]:
|
|
42
46
|
platforms = ["android", "ios"]
|
|
43
47
|
platform_choice = "android,ios"
|
|
44
48
|
elif "ios" in platform_choice:
|
|
@@ -54,43 +58,36 @@ def init_project():
|
|
|
54
58
|
print_info(f"Native {proj_type} project detected. Bypassing Capacitor setup.")
|
|
55
59
|
|
|
56
60
|
if proj_type == "flutter":
|
|
57
|
-
|
|
58
|
-
|
|
61
|
+
# --- FIXED: Uses saved_platform as default ---
|
|
62
|
+
platform_choice = prompt("Select platforms (e.g., android, ios, windows, linux, or 'all')", saved_platform).lower()
|
|
63
|
+
if platform_choice in ["all", "both"]:
|
|
59
64
|
platform_choice = "android,ios,windows,linux"
|
|
60
65
|
else:
|
|
61
|
-
|
|
66
|
+
# Standard Kotlin/Java is Android only
|
|
62
67
|
platform_choice = "android"
|
|
63
68
|
|
|
64
69
|
save_local_config({"platform": platform_choice})
|
|
65
70
|
|
|
71
|
+
# AI Scanning logic...
|
|
66
72
|
print("")
|
|
67
73
|
detected_features = scan_codebase_for_permissions()
|
|
68
|
-
|
|
69
74
|
if detected_features:
|
|
70
75
|
print_success(f"AI detected {len(detected_features)} native features required by your app:")
|
|
71
|
-
|
|
72
76
|
for data in detected_features.values():
|
|
73
77
|
print(f" {CYAN}●{RESET} {data['feature']} {GRAY}({data['desc']}){RESET}")
|
|
74
|
-
|
|
75
|
-
print_info("The AppForge Cloud Server will automatically inject these permissions during the build process.")
|
|
78
|
+
print_info("The AppForge Cloud Server will automatically inject these permissions.")
|
|
76
79
|
else:
|
|
77
80
|
print_info("AI scan complete. No special native permissions detected.")
|
|
78
81
|
|
|
82
|
+
# Monorepo logic...
|
|
79
83
|
try:
|
|
80
84
|
import subprocess
|
|
81
|
-
git_root = subprocess.check_output(
|
|
82
|
-
|
|
83
|
-
text=True,
|
|
84
|
-
stderr=subprocess.DEVNULL
|
|
85
|
-
).strip()
|
|
86
|
-
|
|
87
|
-
sub_path = os.path.relpath(os.getcwd(), git_root)
|
|
88
|
-
|
|
85
|
+
git_root = subprocess.check_output(['git', 'rev-parse', '--show-toplevel'], text=True, stderr=subprocess.DEVNULL).strip()
|
|
86
|
+
sub_path = os.relpath(os.getcwd(), git_root)
|
|
89
87
|
if sub_path and sub_path != '.':
|
|
90
88
|
print_info(f"Detected sub-project path: {GRAY}{sub_path}{RESET}")
|
|
91
89
|
save_local_config({"sub_path": sub_path})
|
|
92
|
-
|
|
93
|
-
except (subprocess.CalledProcessError, FileNotFoundError):
|
|
90
|
+
except Exception:
|
|
94
91
|
save_local_config({"sub_path": "."})
|
|
95
92
|
|
|
96
93
|
print_success("Ready to send to the AppForge Cloud Builder.")
|
|
@@ -102,29 +99,31 @@ def configure_project():
|
|
|
102
99
|
package_id = prompt("Package ID", config.get("packageId", "com.example.app"))
|
|
103
100
|
build_mode = prompt("Default Build Mode (debug/release)", config.get("buildMode", "debug")).lower()
|
|
104
101
|
if build_mode not in ['debug', 'release']: build_mode = 'debug'
|
|
102
|
+
|
|
105
103
|
current_icon = config.get("iconPath", "")
|
|
106
104
|
icon_prompt = prompt("Icon path (local PNG)", current_icon)
|
|
107
|
-
|
|
108
|
-
# If they typed a new path, use it. Otherwise, keep the old one.
|
|
109
105
|
final_icon_path = icon_prompt if icon_prompt else current_icon
|
|
110
106
|
|
|
111
107
|
splash_img = prompt("Splash image (local PNG)", config.get("splashImg", ""))
|
|
112
108
|
splash_bg = prompt("Splash background color (HEX)", config.get("splashBg", "#ffffff"))
|
|
113
109
|
|
|
110
|
+
# FIXED: Removed duplicate iconPath key
|
|
114
111
|
new_config = {
|
|
115
112
|
"appName": app_name,
|
|
116
113
|
"packageId": package_id,
|
|
117
114
|
"buildMode": build_mode,
|
|
118
115
|
"iconPath": final_icon_path,
|
|
119
|
-
"iconPath": icon_path,
|
|
120
116
|
"splashImg": splash_img,
|
|
121
117
|
"splashBg": splash_bg
|
|
122
118
|
}
|
|
123
119
|
save_local_config(new_config)
|
|
124
120
|
|
|
125
|
-
|
|
121
|
+
# FIXED: Only run apply_configuration once, and ONLY for web projects
|
|
126
122
|
if config.get("category") == "web":
|
|
123
|
+
web_dir = config.get("webDir", "dist")
|
|
127
124
|
apply_configuration(app_name, package_id, final_icon_path, splash_img, splash_bg, web_dir)
|
|
125
|
+
|
|
126
|
+
print_success("Configuration updated successfully.")
|
|
128
127
|
return new_config
|
|
129
128
|
|
|
130
129
|
def display_app_details(config):
|
|
@@ -330,7 +329,7 @@ def show_argu():
|
|
|
330
329
|
print(" appforge init - Initialize an AppForge project")
|
|
331
330
|
print(" appforge configure - Open interactive settings")
|
|
332
331
|
print(f" {CYAN}info{RESET} - View CLI version and release notes")
|
|
333
|
-
print(f" {CYAN}create <framework> <name>{RESET} Scaffold a new app (vite, flutter, html, kotlin
|
|
332
|
+
print(f" {CYAN}create <framework> <name>{RESET} Scaffold a new app (vite, flutter, html, kotlin), {BLUE}Ex - appforge create flutter myapp{RESET}")
|
|
334
333
|
print(" appforge build - Package project and send to build repo")
|
|
335
334
|
print(" appforge status - Check build status")
|
|
336
335
|
print(" appforge download - Download the built APK")
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import shutil
|
|
3
|
+
import subprocess
|
|
4
|
+
import re
|
|
5
|
+
import time
|
|
6
|
+
from .utils import print_success, print_info, print_error, prompt, print_progress_bar, run_with_spinner, CYAN, RESET, BOLD
|
|
7
|
+
from .config import save_local_config
|
|
8
|
+
|
|
9
|
+
TEMPLATES = {
|
|
10
|
+
"vite": {
|
|
11
|
+
"cmd": "npm create vite@latest {name} -- --template react-ts",
|
|
12
|
+
"desc": "React + TypeScript + Vite (Fast Web App)",
|
|
13
|
+
"type": "vite",
|
|
14
|
+
"category": "web"
|
|
15
|
+
},
|
|
16
|
+
"flutter": {
|
|
17
|
+
"cmd": "git clone --progress --depth 1 https://github.com/flutter/samples.git _temp_samples",
|
|
18
|
+
"desc": "Official Flutter Starter App (Native)",
|
|
19
|
+
"type": "flutter",
|
|
20
|
+
"category": "native"
|
|
21
|
+
},
|
|
22
|
+
"kotlin": {
|
|
23
|
+
"cmd": "git clone --progress --depth 1 https://github.com/android/architecture-samples.git _temp_kt",
|
|
24
|
+
"desc": "Native Android Kotlin (Jetpack Compose)",
|
|
25
|
+
"type": "android_native",
|
|
26
|
+
"category": "native"
|
|
27
|
+
},
|
|
28
|
+
"html": {
|
|
29
|
+
"cmd": None,
|
|
30
|
+
"desc": "Plain HTML/CSS/JS (Simple Web App)",
|
|
31
|
+
"type": "html",
|
|
32
|
+
"category": "web"
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
def generate_html_template(project_name):
|
|
37
|
+
os.makedirs(project_name, exist_ok=True)
|
|
38
|
+
os.makedirs(os.path.join(project_name, "www"), exist_ok=True)
|
|
39
|
+
|
|
40
|
+
html_content = f"""<!DOCTYPE html>
|
|
41
|
+
<html lang="en">
|
|
42
|
+
<head>
|
|
43
|
+
<meta charset="UTF-8">
|
|
44
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
|
|
45
|
+
<title>{project_name}</title>
|
|
46
|
+
<style>
|
|
47
|
+
body {{ font-family: system-ui, -apple-system, sans-serif; display: flex; justify-content: center; align-items: center; height: 100vh; margin: 0; background-color: #0f172a; color: white; }}
|
|
48
|
+
.card {{ background: rgba(255,255,255,0.1); padding: 2rem; border-radius: 16px; text-align: center; border: 1px solid rgba(255,255,255,0.2); backdrop-filter: blur(10px); }}
|
|
49
|
+
h1 {{ margin-top: 0; color: #6366f1; }}
|
|
50
|
+
</style>
|
|
51
|
+
</head>
|
|
52
|
+
<body>
|
|
53
|
+
<div class="card">
|
|
54
|
+
<h1>Welcome to {project_name}</h1>
|
|
55
|
+
<p>Built with ▲ AppForge</p>
|
|
56
|
+
</div>
|
|
57
|
+
</body>
|
|
58
|
+
</html>"""
|
|
59
|
+
|
|
60
|
+
with open(os.path.join(project_name, "www", "index.html"), "w") as f:
|
|
61
|
+
f.write(html_content)
|
|
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
|
+
|
|
94
|
+
def create_project(framework, project_name):
|
|
95
|
+
if framework not in TEMPLATES:
|
|
96
|
+
print_error(f"Unknown framework: {framework}.")
|
|
97
|
+
|
|
98
|
+
if os.path.exists(project_name):
|
|
99
|
+
print_error(f"Directory '{project_name}' already exists.")
|
|
100
|
+
|
|
101
|
+
template = TEMPLATES[framework]
|
|
102
|
+
print_info(f"Creating a new {BOLD}{template['desc']}{RESET} project in {CYAN}./{project_name}{RESET}...")
|
|
103
|
+
|
|
104
|
+
# 1. Platform selection
|
|
105
|
+
print("")
|
|
106
|
+
if framework == "flutter":
|
|
107
|
+
p_choice = prompt("Select platforms (android, ios, windows, linux, or 'all')", "android").lower()
|
|
108
|
+
if p_choice in ["all", "both"]: p_choice = "android,ios,windows,linux"
|
|
109
|
+
elif template["category"] == "web":
|
|
110
|
+
p_choice = prompt("Select platforms (android, ios, both)", "android").lower()
|
|
111
|
+
if p_choice in ["all", "both"]: p_choice = "android,ios"
|
|
112
|
+
else:
|
|
113
|
+
p_choice = "android"
|
|
114
|
+
print("")
|
|
115
|
+
|
|
116
|
+
# 2. Download with Smoothing
|
|
117
|
+
if framework == "html":
|
|
118
|
+
generate_html_template(project_name)
|
|
119
|
+
else:
|
|
120
|
+
cmd = template["cmd"].format(name=project_name)
|
|
121
|
+
try:
|
|
122
|
+
# We use non-blocking IO to allow the bar to animate even between data chunks
|
|
123
|
+
process = subprocess.Popen(
|
|
124
|
+
cmd, shell=True, stderr=subprocess.PIPE, stdout=subprocess.DEVNULL, text=True, bufsize=0
|
|
125
|
+
)
|
|
126
|
+
os.set_blocking(process.stderr.fileno(), False)
|
|
127
|
+
|
|
128
|
+
progress_regex = re.compile(r"(\d+)%")
|
|
129
|
+
real_p = 0.0
|
|
130
|
+
display_p = 0.0
|
|
131
|
+
|
|
132
|
+
# --- SMOOTHING LOOP ---
|
|
133
|
+
while process.poll() is None or display_p < real_p:
|
|
134
|
+
try:
|
|
135
|
+
# Read all available data from stderr
|
|
136
|
+
data = process.stderr.read()
|
|
137
|
+
if data:
|
|
138
|
+
matches = progress_regex.findall(data)
|
|
139
|
+
if matches:
|
|
140
|
+
real_p = float(matches[-1])
|
|
141
|
+
except (TypeError, IOError):
|
|
142
|
+
pass # No data available right now
|
|
143
|
+
|
|
144
|
+
# If real_p hasn't updated yet, keep display_p moving slowly (fake inertia)
|
|
145
|
+
if display_p < real_p:
|
|
146
|
+
# Catch up 10% of the remaining distance per frame
|
|
147
|
+
step = (real_p - display_p) * 0.15
|
|
148
|
+
display_p += max(step, 0.5)
|
|
149
|
+
elif display_p < 99.0 and process.poll() is None:
|
|
150
|
+
# Creep forward slowly while waiting for more data
|
|
151
|
+
display_p += 0.1
|
|
152
|
+
|
|
153
|
+
if display_p > 100: display_p = 100.0
|
|
154
|
+
|
|
155
|
+
print_progress_bar(display_p, f"Downloading {framework} template")
|
|
156
|
+
time.sleep(0.05) # ~20 FPS for smoothness
|
|
157
|
+
|
|
158
|
+
print_progress_bar(100.0, f"Downloading {framework} template")
|
|
159
|
+
|
|
160
|
+
# Step 2: Extracting
|
|
161
|
+
def do_extract():
|
|
162
|
+
if framework == "flutter":
|
|
163
|
+
shutil.move("_temp_samples/provider_counter", project_name)
|
|
164
|
+
elif framework == "kotlin":
|
|
165
|
+
shutil.move("_temp_kt", project_name)
|
|
166
|
+
run_with_spinner(do_extract, "Extracting starter project")
|
|
167
|
+
|
|
168
|
+
# Step 3: Cleaning
|
|
169
|
+
def do_cleanup():
|
|
170
|
+
if os.path.exists("_temp_samples"): shutil.rmtree("_temp_samples", ignore_errors=True)
|
|
171
|
+
if os.path.exists("_temp_kt"): shutil.rmtree("_temp_kt", ignore_errors=True)
|
|
172
|
+
git_dir = os.path.join(project_name, ".git")
|
|
173
|
+
if os.path.exists(git_dir): shutil.rmtree(git_dir, ignore_errors=True)
|
|
174
|
+
run_with_spinner(do_cleanup, "Optimizing project size")
|
|
175
|
+
|
|
176
|
+
except Exception as e:
|
|
177
|
+
print_error(f"Creation failed: {e}")
|
|
178
|
+
|
|
179
|
+
# Step 4: Auto-Initialization
|
|
180
|
+
def do_init():
|
|
181
|
+
orig = os.getcwd()
|
|
182
|
+
os.chdir(project_name)
|
|
183
|
+
save_local_config({
|
|
184
|
+
"category": template["category"],
|
|
185
|
+
"project_type": template["type"],
|
|
186
|
+
"platform": p_choice,
|
|
187
|
+
"appName": project_name,
|
|
188
|
+
"packageId": f"com.appforge.{project_name.lower().replace('-', '_')}",
|
|
189
|
+
"version": "1.0.0"
|
|
190
|
+
})
|
|
191
|
+
os.chdir(orig)
|
|
192
|
+
|
|
193
|
+
run_with_spinner(do_init, "Generating AppForge config")
|
|
194
|
+
|
|
195
|
+
print_success(f"\nProject '{project_name}' is ready!")
|
|
196
|
+
print(f"Next steps: {CYAN}cd {project_name}{RESET} -> {CYAN}appforge init{RESET}\n")
|
|
@@ -22,8 +22,6 @@ def get_headers():
|
|
|
22
22
|
"X-GitHub-Api-Version": "2022-11-28"
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
# --- Inside github.py ---
|
|
26
|
-
|
|
27
25
|
def zip_project(category, sub_path=".", zip_name="app_source.zip"):
|
|
28
26
|
"""Zips the project, ensuring the icon is ready for @capacitor/assets."""
|
|
29
27
|
config = load_local_config()
|
|
@@ -57,7 +55,6 @@ def zip_project(category, sub_path=".", zip_name="app_source.zip"):
|
|
|
57
55
|
|
|
58
56
|
except Exception as e:
|
|
59
57
|
print_error(f"Failed to prepare app assets for upload: {e}")
|
|
60
|
-
# ------------------------------------
|
|
61
58
|
|
|
62
59
|
def do_zip():
|
|
63
60
|
original_dir = os.getcwd()
|
|
@@ -191,8 +188,8 @@ def push_and_build(config):
|
|
|
191
188
|
"project_type": proj_type,
|
|
192
189
|
"app_version": full_config.get("version", "1.0.0"),
|
|
193
190
|
"sub_path": sub_path,
|
|
194
|
-
"app_name": app_name,
|
|
195
|
-
"package_id": package_id,
|
|
191
|
+
"app_name": app_name,
|
|
192
|
+
"package_id": package_id,
|
|
196
193
|
"build_type": build_type
|
|
197
194
|
}
|
|
198
195
|
}
|
|
@@ -209,7 +206,6 @@ def push_and_build(config):
|
|
|
209
206
|
from datetime import datetime
|
|
210
207
|
project_name = os.path.basename(os.getcwd())
|
|
211
208
|
|
|
212
|
-
# Create the history entry
|
|
213
209
|
new_entry = {
|
|
214
210
|
"app_id": app_id,
|
|
215
211
|
"run_id": run_id,
|
|
@@ -222,6 +218,7 @@ def push_and_build(config):
|
|
|
222
218
|
def check_status():
|
|
223
219
|
"""Streams live build logs from GitHub Actions with step-by-step animations."""
|
|
224
220
|
headers = get_headers()
|
|
221
|
+
app_id = get_app_id()
|
|
225
222
|
url = f"{GITHUB_API_URL}/repos/{BUILD_REPO_OWNER}/{BUILD_REPO_NAME}/actions/runs?per_page=1"
|
|
226
223
|
res = requests.get(url, headers=headers)
|
|
227
224
|
|
|
@@ -230,14 +227,26 @@ def check_status():
|
|
|
230
227
|
return
|
|
231
228
|
|
|
232
229
|
runs = res.json().get("workflow_runs", [])
|
|
230
|
+
my_run = None
|
|
231
|
+
for run in runs:
|
|
232
|
+
if f"ID: {app_id}" in run.get("display_title", ""):
|
|
233
|
+
my_run = run
|
|
234
|
+
break
|
|
235
|
+
|
|
236
|
+
if not my_run:
|
|
237
|
+
print_info(f"No active builds found for your App ID ({app_id}).")
|
|
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
|
+
|
|
233
246
|
if not runs:
|
|
234
247
|
print_info("No builds found in the cloud yet.")
|
|
235
248
|
return
|
|
236
249
|
|
|
237
|
-
latest_run = runs[0]
|
|
238
|
-
run_id = latest_run["id"]
|
|
239
|
-
status = latest_run["status"]
|
|
240
|
-
|
|
241
250
|
if status == "completed":
|
|
242
251
|
conclusion = latest_run.get("conclusion")
|
|
243
252
|
if conclusion == "success":
|
|
@@ -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.")
|
|
@@ -1,106 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
import shutil
|
|
3
|
-
import subprocess
|
|
4
|
-
import re
|
|
5
|
-
from .utils import print_success, print_info, print_error, prompt, print_progress_bar, CYAN, RESET, BOLD
|
|
6
|
-
|
|
7
|
-
TEMPLATES = {
|
|
8
|
-
"vite": {
|
|
9
|
-
"cmd": "npm create vite@latest {name} -- --template react-ts",
|
|
10
|
-
"desc": "React + TypeScript + Vite (Fast Web App)"
|
|
11
|
-
},
|
|
12
|
-
"flutter": {
|
|
13
|
-
"cmd": "git clone --progress https://github.com/flutter/samples.git _temp_samples && mv _temp_samples/provider_counter {name} && rm -rf _temp_samples",
|
|
14
|
-
"desc": "Official Flutter Starter App (Native)"
|
|
15
|
-
},
|
|
16
|
-
"kotlin": {
|
|
17
|
-
"cmd": "git clone --progress https://github.com/android/architecture-samples.git _temp_kt && mv _temp_kt {name} && rm -rf _temp_kt",
|
|
18
|
-
"desc": "Native Android Kotlin (Jetpack Compose)"
|
|
19
|
-
},
|
|
20
|
-
"html": {
|
|
21
|
-
"cmd": None,
|
|
22
|
-
"desc": "Plain HTML/CSS/JS (Simple Web App)"
|
|
23
|
-
},
|
|
24
|
-
"win": {
|
|
25
|
-
"cmd": "git clone --progress https://github.com/flutter/samples.git _temp_samples",
|
|
26
|
-
"desc": "Flutter Windows Desktop App (.exe)",
|
|
27
|
-
"base_framework": "flutter" # It uses Flutter under the hood!
|
|
28
|
-
},
|
|
29
|
-
"linux": {
|
|
30
|
-
"cmd": "git clone --progress https://github.com/flutter/samples.git _temp_samples",
|
|
31
|
-
"desc": "Flutter Linux Desktop App",
|
|
32
|
-
"base_framework": "flutter" # It uses Flutter under the hood!
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
def generate_html_template(project_name):
|
|
37
|
-
# This function stays exactly the same
|
|
38
|
-
os.makedirs(project_name, exist_ok=True)
|
|
39
|
-
os.makedirs(os.path.join(project_name, "www"), exist_ok=True)
|
|
40
|
-
html_content = f"""...""" # Keep your HTML content
|
|
41
|
-
with open(os.path.join(project_name, "www", "index.html"), "w") as f:
|
|
42
|
-
f.write(html_content)
|
|
43
|
-
|
|
44
|
-
def create_project(framework, project_name):
|
|
45
|
-
"""Generates a new project with a real progress bar for git clone."""
|
|
46
|
-
if framework not in TEMPLATES:
|
|
47
|
-
print_error(f"Unknown framework: {framework}.")
|
|
48
|
-
|
|
49
|
-
if os.path.exists(project_name):
|
|
50
|
-
print_error(f"Directory '{project_name}' already exists.")
|
|
51
|
-
|
|
52
|
-
template = TEMPLATES[framework]
|
|
53
|
-
print_info(f"Creating a new {BOLD}{template['desc']}{RESET} project in {CYAN}./{project_name}{RESET}...")
|
|
54
|
-
|
|
55
|
-
if framework == "html":
|
|
56
|
-
generate_html_template(project_name)
|
|
57
|
-
else:
|
|
58
|
-
# --- NEW REAL-TIME PROGRESS BAR LOGIC ---
|
|
59
|
-
cmd = template["cmd"].format(name=project_name)
|
|
60
|
-
|
|
61
|
-
try:
|
|
62
|
-
# We open a subprocess and read its stderr line by line
|
|
63
|
-
process = subprocess.Popen(
|
|
64
|
-
cmd,
|
|
65
|
-
shell=True,
|
|
66
|
-
stderr=subprocess.PIPE,
|
|
67
|
-
stdout=subprocess.DEVNULL, # Hide normal git output
|
|
68
|
-
text=True,
|
|
69
|
-
bufsize=1 # Line-buffered
|
|
70
|
-
)
|
|
71
|
-
|
|
72
|
-
# Regex to find the percentage in git's output
|
|
73
|
-
# It looks for "Receiving objects: 25%"
|
|
74
|
-
progress_regex = re.compile(r"(\d+)%")
|
|
75
|
-
|
|
76
|
-
# Read each line of output from the running git command
|
|
77
|
-
for line in iter(process.stderr.readline, ''):
|
|
78
|
-
match = progress_regex.search(line)
|
|
79
|
-
if match:
|
|
80
|
-
percentage = float(match.group(1))
|
|
81
|
-
print_progress_bar(percentage, f"Downloading {framework} template")
|
|
82
|
-
|
|
83
|
-
process.wait() # Wait for the command to finish
|
|
84
|
-
if process.returncode != 0:
|
|
85
|
-
raise Exception("Git clone failed.")
|
|
86
|
-
|
|
87
|
-
print_progress_bar(100.0, f"Downloading {framework} template")
|
|
88
|
-
|
|
89
|
-
# Post-clone cleanup for Flutter
|
|
90
|
-
if framework == "flutter":
|
|
91
|
-
shutil.move("_temp_samples/provider_counter", project_name)
|
|
92
|
-
shutil.rmtree("_temp_samples", ignore_errors=True)
|
|
93
|
-
shutil.rmtree(os.path.join(project_name, ".git"), ignore_errors=True)
|
|
94
|
-
|
|
95
|
-
except Exception as e:
|
|
96
|
-
# Clean up partial downloads on failure
|
|
97
|
-
if os.path.exists("_temp_samples"):
|
|
98
|
-
shutil.rmtree("_temp_samples", ignore_errors=True)
|
|
99
|
-
print_error(f"Project creation failed: {e}")
|
|
100
|
-
# --- END OF NEW LOGIC ---
|
|
101
|
-
|
|
102
|
-
print_success(f"Project '{project_name}' created successfully!")
|
|
103
|
-
print("\nNext steps:")
|
|
104
|
-
print(f" {CYAN}cd {project_name}{RESET}")
|
|
105
|
-
print(f" {CYAN}appforge init{RESET}")
|
|
106
|
-
print("")
|
|
@@ -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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|