appforge-cli 1.0.0__tar.gz → 1.0.9__tar.gz

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