appforge-cli 1.2.4__tar.gz → 1.6.0__tar.gz

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