fleetbo-cockpit-cli 1.0.0
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.
- package/README.md +74 -0
- package/bin/fleetbo.js +2 -0
- package/cli.js +505 -0
- package/package.json +36 -0
- package/page.js +157 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# Fleetbo CLI
|
|
2
|
+
|
|
3
|
+
Official CLI for Fleetbo - Build native mobile apps with React.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
This CLI is typically used via `npx` in Fleetbo projects:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx fleetbo-cli
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or add it to your project:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install fleetbo-cli --save-dev
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Commands
|
|
20
|
+
|
|
21
|
+
### Start Development Environment
|
|
22
|
+
```bash
|
|
23
|
+
npm run fleetbo
|
|
24
|
+
# or
|
|
25
|
+
npx fleetbo-cli
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### AI Architect Mode (Alex)
|
|
29
|
+
```bash
|
|
30
|
+
npm run fleetbo alex
|
|
31
|
+
# or
|
|
32
|
+
npx fleetbo-cli alex
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### Generate a Page
|
|
36
|
+
```bash
|
|
37
|
+
npm run fleetbo page Dashboard
|
|
38
|
+
npm run fleetbo page admin/Settings
|
|
39
|
+
# or
|
|
40
|
+
npx fleetbo-cli page Dashboard
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Build for Android
|
|
44
|
+
```bash
|
|
45
|
+
npm run fleetbo android
|
|
46
|
+
# or
|
|
47
|
+
npx fleetbo-cli android
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Build for iOS
|
|
51
|
+
```bash
|
|
52
|
+
npm run fleetbo ios
|
|
53
|
+
# or
|
|
54
|
+
npx fleetbo-cli ios
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Requirements
|
|
58
|
+
|
|
59
|
+
- Node.js >= 16.0.0
|
|
60
|
+
- A valid Fleetbo project with `.env` configuration
|
|
61
|
+
|
|
62
|
+
## Configuration
|
|
63
|
+
|
|
64
|
+
Your project must have a `.env` file with:
|
|
65
|
+
|
|
66
|
+
```env
|
|
67
|
+
REACT_APP_ENTERPRISE_ID=your_project_id
|
|
68
|
+
REACT_KEY_APP=your_app_key
|
|
69
|
+
REACT_APP_TESTER_EMAIL=your_email
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## License
|
|
73
|
+
|
|
74
|
+
MIT © Fleetbo
|
package/bin/fleetbo.js
ADDED
package/cli.js
ADDED
|
@@ -0,0 +1,505 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
const { spawn, execSync } = require('child_process');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const path = require('path');
|
|
5
|
+
const axios = require('axios');
|
|
6
|
+
const dotenv = require('dotenv');
|
|
7
|
+
const os = require('os');
|
|
8
|
+
const archiver = require('archiver');
|
|
9
|
+
const readline = require('readline');
|
|
10
|
+
|
|
11
|
+
// ============================================
|
|
12
|
+
// FLEETBO CLI - Centralized Package
|
|
13
|
+
// ============================================
|
|
14
|
+
|
|
15
|
+
const ANDROID_BUILD_URL = "https://fandroidbuild-jqycakhlxa-uc.a.run.app";
|
|
16
|
+
const IOS_BUILD_URL = "https://fiosbuild-jqycakhlxa-uc.a.run.app";
|
|
17
|
+
const UPDATE_NETWORK_URL = 'https://updatedevelopernetwork-jqycakhlxa-uc.a.run.app';
|
|
18
|
+
const ALEX_ENGINE_URL = "https://generatenativemodule-jqycakhlxa-uc.a.run.app";
|
|
19
|
+
const INJECT_DEPS_URL = "https://savegeneratedfile-jqycakhlxa-uc.a.run.app";
|
|
20
|
+
const PORT = 3000;
|
|
21
|
+
|
|
22
|
+
let uplinkProcess = null;
|
|
23
|
+
const args = process.argv.slice(2);
|
|
24
|
+
const command = args[0];
|
|
25
|
+
|
|
26
|
+
// ============================================
|
|
27
|
+
// CONFIGURATION (.env du projet dev)
|
|
28
|
+
// ============================================
|
|
29
|
+
process.env.DOTENV_SILENT = 'true';
|
|
30
|
+
const envPath = path.join(process.cwd(), '.env');
|
|
31
|
+
|
|
32
|
+
if (!fs.existsSync(envPath)) {
|
|
33
|
+
console.error('\x1b[31m%s\x1b[0m', '\n❌ Error: Configuration file (.env) not found.');
|
|
34
|
+
console.error('\x1b[90m%s\x1b[0m', 'Make sure you are in a Fleetbo project directory.\n');
|
|
35
|
+
process.exit(1);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
dotenv.config({ path: envPath, quiet: true });
|
|
39
|
+
|
|
40
|
+
const projectId = process.env.REACT_APP_ENTERPRISE_ID;
|
|
41
|
+
const keyApp = process.env.REACT_KEY_APP;
|
|
42
|
+
const testerEmail = process.env.REACT_APP_TESTER_EMAIL;
|
|
43
|
+
|
|
44
|
+
if (!projectId) {
|
|
45
|
+
console.error('\n\x1b[31m❌ Error: Project ID missing in .env.\x1b[0m\n');
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ============================================
|
|
50
|
+
// HELPERS
|
|
51
|
+
// ============================================
|
|
52
|
+
const wrapText = (text, maxWidth) => {
|
|
53
|
+
if (!text) return "";
|
|
54
|
+
const rawLines = text.split('\n');
|
|
55
|
+
let formattedLines = [];
|
|
56
|
+
rawLines.forEach(line => {
|
|
57
|
+
if (line.trim().length === 0) {
|
|
58
|
+
formattedLines.push("");
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const isSpecialFormat = /^[\s]*[-*•\d]/.test(line) || line.startsWith(" ");
|
|
62
|
+
if (isSpecialFormat) {
|
|
63
|
+
formattedLines.push(line);
|
|
64
|
+
} else {
|
|
65
|
+
const words = line.split(" ");
|
|
66
|
+
let currentLine = words[0];
|
|
67
|
+
for (let i = 1; i < words.length; i++) {
|
|
68
|
+
if (currentLine.length + 1 + words[i].length <= maxWidth) {
|
|
69
|
+
currentLine += " " + words[i];
|
|
70
|
+
} else {
|
|
71
|
+
formattedLines.push(currentLine);
|
|
72
|
+
currentLine = words[i];
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
formattedLines.push(currentLine);
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
return formattedLines.join('\n ');
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const checkGitSecurity = () => {
|
|
82
|
+
const gitDir = path.join(process.cwd(), '.git');
|
|
83
|
+
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
84
|
+
if (fs.existsSync(gitDir)) {
|
|
85
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
86
|
+
console.error('\n\x1b[31m🚨 SECURITY ALERT:\x1b[0m .git detected but no .gitignore found.');
|
|
87
|
+
process.exit(1);
|
|
88
|
+
}
|
|
89
|
+
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
90
|
+
if (!gitignoreContent.includes('.env')) {
|
|
91
|
+
console.error('\n\x1b[31m🚨 CRITICAL RISK:\x1b[0m .env is NOT ignored by Git.');
|
|
92
|
+
process.exit(1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const injectRouteIntoAppJs = (moduleName, subPath = '') => {
|
|
98
|
+
const appJsPath = path.join(process.cwd(), 'src', 'App.js');
|
|
99
|
+
if (!fs.existsSync(appJsPath)) {
|
|
100
|
+
console.error(` \x1b[31m[Safety Stop]\x1b[0m App.js missing.`);
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
let content = fs.readFileSync(appJsPath, 'utf8');
|
|
105
|
+
const importAnchor = '// FLEETBO_MORE_IMPORTS';
|
|
106
|
+
const routeAnchor = '{/* FLEETBO_DYNAMIC ROUTES */}';
|
|
107
|
+
|
|
108
|
+
if (!content.includes(importAnchor) || !content.includes(routeAnchor)) {
|
|
109
|
+
console.log(` \x1b[33m[Skipped]\x1b[0m Anchors missing in App.js. Manual injection required.`);
|
|
110
|
+
return false;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
const cleanSubPath = subPath ? `${subPath}/` : '';
|
|
114
|
+
const importLine = `import ${moduleName} from './app/${cleanSubPath}${moduleName}';`;
|
|
115
|
+
const routeLine = `<Route path="/${cleanSubPath}${moduleName.toLowerCase()}" element={<${moduleName} />} />`;
|
|
116
|
+
|
|
117
|
+
let modified = false;
|
|
118
|
+
|
|
119
|
+
if (!content.includes(importLine)) {
|
|
120
|
+
content = content.replace(importAnchor, `${importLine}\n${importAnchor}`);
|
|
121
|
+
modified = true;
|
|
122
|
+
}
|
|
123
|
+
if (!content.includes(routeLine)) {
|
|
124
|
+
content = content.replace(routeAnchor, `${routeLine}\n ${routeAnchor}`);
|
|
125
|
+
modified = true;
|
|
126
|
+
}
|
|
127
|
+
if (modified) {
|
|
128
|
+
fs.writeFileSync(appJsPath, content);
|
|
129
|
+
console.log(` \x1b[32m[Routed]\x1b[0m ${moduleName} injected into App.js safely.`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return modified;
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
const showEnergyTransfer = async () => {
|
|
136
|
+
const width = 30;
|
|
137
|
+
for (let i = 0; i <= width; i++) {
|
|
138
|
+
const dots = "█".repeat(i);
|
|
139
|
+
const empty = "░".repeat(width - i);
|
|
140
|
+
process.stdout.write(`\r \x1b[32m⚡ Alex Energy Sync:\x1b[0m [${dots}${empty}] ${Math.round((i / width) * 100)}%`);
|
|
141
|
+
await new Promise(r => setTimeout(r, 45));
|
|
142
|
+
}
|
|
143
|
+
process.stdout.write('\n');
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
// ============================================
|
|
147
|
+
// COMMAND: alex
|
|
148
|
+
// ============================================
|
|
149
|
+
if (command === 'alex') {
|
|
150
|
+
checkGitSecurity();
|
|
151
|
+
const initialPrompt = args.slice(1).join(' ');
|
|
152
|
+
|
|
153
|
+
const processAlexRequest = async (prompt) => {
|
|
154
|
+
if (prompt.length > 300) {
|
|
155
|
+
console.log('\n\x1b[31m⛔ [Alex Safety] Request too long (' + prompt.length + '/300 chars).\x1b[0m');
|
|
156
|
+
console.log('\x1b[90mAlex prefers concise instructions. Please summarize.\x1b[0m');
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
process.stdout.write('\x1b[33m🧠 Alex is thinking...\x1b[0m');
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const result = await axios.post(ALEX_ENGINE_URL, { prompt, projectType: 'android' }, {
|
|
163
|
+
headers: { 'x-project-id': projectId }
|
|
164
|
+
});
|
|
165
|
+
const aiData = result.data;
|
|
166
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
167
|
+
|
|
168
|
+
if (aiData.status === 'quota_exceeded') {
|
|
169
|
+
console.log(`\n\x1b[31m⛔ ARCHITECT QUOTA REACHED:\x1b[0m ${aiData.message}`);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (aiData.status === 'success' || aiData.status === 'message' || aiData.status === 'complex_refusal') {
|
|
174
|
+
console.log('');
|
|
175
|
+
const rawMsg = aiData.message || "I'm ready.";
|
|
176
|
+
const formattedMsg = wrapText(rawMsg, 85);
|
|
177
|
+
console.log('\x1b[32mAlex ❯\x1b[0m ' + formattedMsg);
|
|
178
|
+
|
|
179
|
+
if (aiData.remainingConsultations !== undefined) {
|
|
180
|
+
const remaining = aiData.remainingConsultations;
|
|
181
|
+
const limit = aiData.consultationLimit || 7;
|
|
182
|
+
const tierLabel = aiData.tier === 'senior' ? 'SENIOR' : aiData.tier === 'expert' ? 'EXPERT' : 'JUNIOR';
|
|
183
|
+
const percent = Math.round((remaining / limit) * 100);
|
|
184
|
+
const energyColor = percent > 20 ? '\x1b[32m' : '\x1b[31m';
|
|
185
|
+
console.log(`\x1b[36m⚡ Architect Fuel:\x1b[0m ${energyColor}${percent}%\x1b[0m (${remaining}/${limit} instructions left) [${tierLabel}]`);
|
|
186
|
+
console.log('');
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (aiData.status === 'success' && aiData.moduleData) {
|
|
191
|
+
const { fileName, code, mockFileName, mockCode, moduleName, instructions, config_offload } = aiData.moduleData;
|
|
192
|
+
console.log(` \x1b[90m Architecting: ${moduleName}\x1b[0m`);
|
|
193
|
+
|
|
194
|
+
const writeFile = (dir, name, content) => {
|
|
195
|
+
const fullPath = path.join(process.cwd(), dir);
|
|
196
|
+
const filePath = path.join(fullPath, name);
|
|
197
|
+
if (!fs.existsSync(fullPath)) fs.mkdirSync(fullPath, { recursive: true });
|
|
198
|
+
fs.writeFileSync(filePath, content);
|
|
199
|
+
console.log(` \x1b[32m[Written]\x1b[0m ${dir}${name}`);
|
|
200
|
+
};
|
|
201
|
+
|
|
202
|
+
if (instructions && Array.isArray(instructions) && instructions.length > 0) {
|
|
203
|
+
console.log('\n\x1b[33m--- GUIDE (MCI) ---\x1b[0m');
|
|
204
|
+
instructions.forEach(line => {
|
|
205
|
+
if (typeof line === 'string') {
|
|
206
|
+
const formattedLine = line.replace(/ACTION|CAPTURE|PERSPECTIVE/g, '\x1b[1m$&\x1b[0m');
|
|
207
|
+
console.log(` ${formattedLine}`);
|
|
208
|
+
}
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (code && fileName) {
|
|
213
|
+
const folder = fileName.endsWith('.kt') ? 'public/native/android/' : 'src/app/';
|
|
214
|
+
writeFile(folder, fileName, code);
|
|
215
|
+
if (fileName.endsWith('.jsx')) injectRouteIntoAppJs(fileName.replace('.jsx', ''));
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (mockCode && mockFileName) {
|
|
219
|
+
const pageName = mockFileName.replace('.jsx', '');
|
|
220
|
+
writeFile('src/app/mocks/', mockFileName, mockCode);
|
|
221
|
+
const injected = injectRouteIntoAppJs(pageName, 'mocks');
|
|
222
|
+
if (injected) {
|
|
223
|
+
console.log(` \x1b[32m[Routed]\x1b[0m App.js -> /mocks/${pageName.toLowerCase()}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (config_offload && (config_offload.dependencies?.length > 0 || config_offload.permissions?.length > 0)) {
|
|
228
|
+
process.stdout.write(` \x1b[33m[Cloud Inject]\x1b[0m Syncing ${config_offload.dependencies.length} libs to Factory...`);
|
|
229
|
+
try {
|
|
230
|
+
await axios.post(INJECT_DEPS_URL, {
|
|
231
|
+
projectId: projectId,
|
|
232
|
+
fileData: {
|
|
233
|
+
path: fileName,
|
|
234
|
+
config_offload: config_offload
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
process.stdout.write(` \x1b[32mOK\x1b[0m\n`);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
process.stdout.write(` \x1b[31mFAILED\x1b[0m\n`);
|
|
240
|
+
console.error(` ⚠️ Config sync failed: ${err.message}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
} catch (error) {
|
|
245
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
246
|
+
console.error('\n\x1b[31m Alex Error:\x1b[0m ' + (error.response?.data?.message || error.message));
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const startAlexSession = async () => {
|
|
251
|
+
process.stdout.write('\x1b[33m🛡️ Alex is checking runtime state...\x1b[0m\r');
|
|
252
|
+
let attempts = 0;
|
|
253
|
+
const maxAttempts = 5;
|
|
254
|
+
let isReady = false;
|
|
255
|
+
let dynamicUsername = 'Pilot';
|
|
256
|
+
|
|
257
|
+
while (attempts < maxAttempts && !isReady) {
|
|
258
|
+
try {
|
|
259
|
+
const validation = await axios.post(ALEX_ENGINE_URL, {
|
|
260
|
+
prompt: "ping", validateProject: true, checkNetwork: true, projectKey: keyApp
|
|
261
|
+
}, { headers: { 'x-project-id': projectId }, timeout: 5000 });
|
|
262
|
+
|
|
263
|
+
if (validation.data?.isRunning) {
|
|
264
|
+
isReady = true;
|
|
265
|
+
dynamicUsername = validation.data.username || 'Pilot';
|
|
266
|
+
break;
|
|
267
|
+
}
|
|
268
|
+
attempts++;
|
|
269
|
+
if (attempts < maxAttempts) await new Promise(r => setTimeout(r, 2000));
|
|
270
|
+
} catch (error) {
|
|
271
|
+
attempts++;
|
|
272
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
if (!isReady) {
|
|
277
|
+
console.error('\n\x1b[31m⚠️ ENGINE OFFLINE:\x1b[0m Start Fleetbo runtime first: "npm run fleetbo" ');
|
|
278
|
+
console.error(`\x1b[90m(Ensure you are running the runtime for project: ${keyApp})\x1b[0m`);
|
|
279
|
+
process.exit(1);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
process.stdout.write(' '.repeat(60) + '\r');
|
|
283
|
+
console.log('\n\x1b[32m🤖 Alex is now online.\x1b[0m');
|
|
284
|
+
console.log('\x1b[32mAlex ❯\x1b[0m Infrastructure online. I am ready to forge. What module are we architecting today, Pilot?');
|
|
285
|
+
console.log('');
|
|
286
|
+
|
|
287
|
+
const rl = readline.createInterface({
|
|
288
|
+
input: process.stdin,
|
|
289
|
+
output: process.stdout,
|
|
290
|
+
prompt: `\x1b[34m${dynamicUsername} ❯ \x1b[0m`
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
process.stdout.write('\n\x1b[F');
|
|
294
|
+
rl.prompt();
|
|
295
|
+
|
|
296
|
+
rl.on('line', async (line) => {
|
|
297
|
+
if (['exit', 'quit'].includes(line.trim().toLowerCase())) {
|
|
298
|
+
console.log('\n\x1b[90m Alex session closed.\x1b[0m');
|
|
299
|
+
rl.close();
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (line.trim()) {
|
|
303
|
+
await processAlexRequest(line.trim());
|
|
304
|
+
console.log('');
|
|
305
|
+
}
|
|
306
|
+
process.stdout.write('\n\x1b[F');
|
|
307
|
+
rl.prompt();
|
|
308
|
+
}).on('close', () => {
|
|
309
|
+
process.exit(0);
|
|
310
|
+
});
|
|
311
|
+
};
|
|
312
|
+
|
|
313
|
+
if (!initialPrompt || initialPrompt === '?') startAlexSession();
|
|
314
|
+
else processAlexRequest(initialPrompt);
|
|
315
|
+
|
|
316
|
+
}
|
|
317
|
+
// ============================================
|
|
318
|
+
// COMMAND: android / ios
|
|
319
|
+
// ============================================
|
|
320
|
+
else if (command === 'android' || command === 'ios') {
|
|
321
|
+
checkGitSecurity();
|
|
322
|
+
const platform = command;
|
|
323
|
+
const nativeDir = platform === 'android' ? 'public/native/android/' : 'public/native/ios/';
|
|
324
|
+
const extension = platform === 'android' ? '.kt' : '.swift';
|
|
325
|
+
const fullPath = path.join(process.cwd(), nativeDir);
|
|
326
|
+
|
|
327
|
+
let hasNativeFiles = false;
|
|
328
|
+
if (fs.existsSync(fullPath)) {
|
|
329
|
+
const files = fs.readdirSync(fullPath);
|
|
330
|
+
hasNativeFiles = files.some(file => file.endsWith(extension));
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
if (!hasNativeFiles) {
|
|
334
|
+
console.log(`\n\x1b[31m⚠️ ENGINE INCOMPLETE:\x1b[0m No native blueprints detected for \x1b[1m${platform.toUpperCase()}\x1b[0m.`);
|
|
335
|
+
console.log(`\x1b[90mAlex must architect at least one ${extension} module before deployment.\x1b[0m\n`);
|
|
336
|
+
process.exit(1);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
const targetUrl = platform === 'android' ? ANDROID_BUILD_URL : IOS_BUILD_URL;
|
|
340
|
+
|
|
341
|
+
(async () => {
|
|
342
|
+
console.log(`\n\x1b[36m⚡ FLEETBO ${platform.toUpperCase()} UPLINK\x1b[0m`);
|
|
343
|
+
try {
|
|
344
|
+
execSync('npm run build', { stdio: 'inherit' });
|
|
345
|
+
let buildDir = fs.existsSync(path.join(process.cwd(), 'dist')) ? 'dist' : 'build';
|
|
346
|
+
|
|
347
|
+
const zipBuffer = await new Promise((resolve, reject) => {
|
|
348
|
+
const chunks = [];
|
|
349
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
350
|
+
archive.on('data', chunk => chunks.push(chunk));
|
|
351
|
+
archive.on('end', () => resolve(Buffer.concat(chunks)));
|
|
352
|
+
archive.on('error', reject);
|
|
353
|
+
archive.directory(path.join(process.cwd(), buildDir), false);
|
|
354
|
+
archive.finalize();
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
console.log(`\n\x1b[33mSyncing ${platform} logic bundle...\x1b[0m`);
|
|
358
|
+
await showEnergyTransfer();
|
|
359
|
+
|
|
360
|
+
const res = await axios.post(targetUrl, zipBuffer, {
|
|
361
|
+
headers: { 'Content-Type': 'application/zip', 'x-project-id': projectId }
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
if (res.data.success) {
|
|
365
|
+
console.log(`\n\x1b[1m${platform.toUpperCase()} DEPLOYED\x1b[0m | \x1b[32mAlex ❯\x1b[0m Runtime updated.`);
|
|
366
|
+
}
|
|
367
|
+
} catch (error) {
|
|
368
|
+
console.error(`\n\x1b[31m Build Error:\x1b[0m ${error.response?.data?.error || error.message}`);
|
|
369
|
+
process.exit(1);
|
|
370
|
+
}
|
|
371
|
+
})();
|
|
372
|
+
}
|
|
373
|
+
// ============================================
|
|
374
|
+
// COMMAND: page / g / generate
|
|
375
|
+
// ============================================
|
|
376
|
+
else if (['page', 'g', 'generate'].includes(command)) {
|
|
377
|
+
const pageGeneratorPath = path.join(__dirname, 'page.js');
|
|
378
|
+
try {
|
|
379
|
+
require(pageGeneratorPath);
|
|
380
|
+
} catch (e) {
|
|
381
|
+
console.error('\x1b[31m Page Generator Error:\x1b[0m', e.message);
|
|
382
|
+
process.exit(1);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// ============================================
|
|
386
|
+
// COMMAND: (default) - Start Dev Environment
|
|
387
|
+
// ============================================
|
|
388
|
+
else {
|
|
389
|
+
const NULL_DEV = process.platform === 'win32' ? '>nul 2>&1' : '2>/dev/null';
|
|
390
|
+
|
|
391
|
+
function killProcessOnPort(port) {
|
|
392
|
+
try {
|
|
393
|
+
if (process.platform !== 'win32') {
|
|
394
|
+
const pid = execSync(`lsof -ti:${port} ${NULL_DEV}`).toString().trim();
|
|
395
|
+
if (pid) execSync(`kill -9 ${pid.split('\n').join(' ')} ${NULL_DEV}`);
|
|
396
|
+
}
|
|
397
|
+
} catch (e) {}
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const killNetworkService = () => {
|
|
401
|
+
if (uplinkProcess) {
|
|
402
|
+
try {
|
|
403
|
+
uplinkProcess.kill('SIGINT');
|
|
404
|
+
console.log('[Fleetbo] Engine closed.');
|
|
405
|
+
} catch (e) {
|
|
406
|
+
console.error('[Fleetbo] Error closing tunnel:', e.message);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
let isExiting = false;
|
|
412
|
+
|
|
413
|
+
async function cleanupAndExit(code = 0) {
|
|
414
|
+
if (isExiting) return;
|
|
415
|
+
isExiting = true;
|
|
416
|
+
console.log('\n\x1b[33m[Fleetbo] 🛑 Stopping environment & Cleaning Uplink...\x1b[0m');
|
|
417
|
+
try {
|
|
418
|
+
await axios.post(UPDATE_NETWORK_URL, { keyApp, networkUrl: '', tester: testerEmail });
|
|
419
|
+
console.log('\x1b[32m[Fleetbo] ✓ Network status reset to offline.\x1b[0m');
|
|
420
|
+
} catch (e) {
|
|
421
|
+
console.error('[Fleetbo] Network cleanup warning:', e.message);
|
|
422
|
+
}
|
|
423
|
+
killNetworkService();
|
|
424
|
+
killProcessOnPort(PORT);
|
|
425
|
+
console.log('[Fleetbo] Bye.');
|
|
426
|
+
process.exit(code);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
process.on('SIGINT', () => cleanupAndExit(0));
|
|
430
|
+
process.on('SIGTERM', () => cleanupAndExit(0));
|
|
431
|
+
|
|
432
|
+
async function syncFirebase(keyApp, networkUrl, testerEmail) {
|
|
433
|
+
try {
|
|
434
|
+
await axios.post(UPDATE_NETWORK_URL, { keyApp, networkUrl, tester: testerEmail });
|
|
435
|
+
console.log('\n\x1b[32mEngine started successfully\x1b[0m');
|
|
436
|
+
console.log(`\n\x1b[32m[Fleetbo]\x1b[0m -------------------------------------------------------------`);
|
|
437
|
+
console.log('\x1b[32m[Fleetbo] \x1b[1mGO GO GO ! FLEETBO STUDIO IS READY\x1b[0m');
|
|
438
|
+
console.log('\x1b[32m[Fleetbo] You can now start coding and previewing in Studio. 🚀\x1b[0m');
|
|
439
|
+
console.log(`\x1b[32m[Fleetbo]\x1b[0m -------------------------------------------------------------`);
|
|
440
|
+
console.log(`\x1b[34mPilot Instruction ❯\x1b[0m Switch to your Fleetbo Cockpit tab to begin.\n`);
|
|
441
|
+
} catch (err) {
|
|
442
|
+
console.error(`[Fleetbo] Sync Error: ${err.message}`);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function runDevEnvironment() {
|
|
447
|
+
console.log(`[Fleetbo] 🛡️ Initializing Dev Environment...`);
|
|
448
|
+
killNetworkService();
|
|
449
|
+
killProcessOnPort(PORT);
|
|
450
|
+
|
|
451
|
+
if (!testerEmail) {
|
|
452
|
+
console.error('\x1b[31mError: REACT_APP_TESTER_EMAIL missing in .env\x1b[0m');
|
|
453
|
+
process.exit(1);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
457
|
+
const devServer = spawn(npmCmd, ['start'], {
|
|
458
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
459
|
+
shell: true,
|
|
460
|
+
env: {
|
|
461
|
+
...process.env,
|
|
462
|
+
BROWSER: 'none',
|
|
463
|
+
PORT: PORT.toString(),
|
|
464
|
+
DANGEROUSLY_DISABLE_HOST_CHECK: 'true',
|
|
465
|
+
HOST: '0.0.0.0',
|
|
466
|
+
WDS_SOCKET_HOST: 'localhost',
|
|
467
|
+
WDS_SOCKET_PORT: PORT.toString()
|
|
468
|
+
}
|
|
469
|
+
});
|
|
470
|
+
|
|
471
|
+
devServer.stdout.pipe(process.stdout);
|
|
472
|
+
devServer.stderr.pipe(process.stderr);
|
|
473
|
+
|
|
474
|
+
let connectionStarted = false;
|
|
475
|
+
|
|
476
|
+
devServer.stdout.on('data', (data) => {
|
|
477
|
+
const output = data.toString();
|
|
478
|
+
|
|
479
|
+
if (!connectionStarted && (output.includes('Local:') || output.includes('Compiled successfully'))) {
|
|
480
|
+
connectionStarted = true;
|
|
481
|
+
|
|
482
|
+
console.log('\n[Fleetbo] ---------------------------------------------------');
|
|
483
|
+
console.log(`[Fleetbo] 🔗 Establishing Secure Uplink...`);
|
|
484
|
+
console.log(`[Fleetbo] ⏳ Please wait for the green message...`);
|
|
485
|
+
console.log('[Fleetbo] ---------------------------------------------------');
|
|
486
|
+
|
|
487
|
+
const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
488
|
+
uplinkProcess = spawn(npxCmd, [
|
|
489
|
+
'cloudflared',
|
|
490
|
+
'tunnel',
|
|
491
|
+
'--url', `http://127.0.0.1:${PORT}`,
|
|
492
|
+
'--http-host-header', `127.0.0.1:${PORT}`
|
|
493
|
+
], { shell: true });
|
|
494
|
+
|
|
495
|
+
uplinkProcess.stderr.on('data', (chunk) => {
|
|
496
|
+
const text = chunk.toString();
|
|
497
|
+
const match = text.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/);
|
|
498
|
+
if (match) syncFirebase(process.env.REACT_KEY_APP, match[0], process.env.REACT_APP_TESTER_EMAIL);
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
runDevEnvironment();
|
|
505
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "fleetbo-cockpit-cli",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Fleetbo CLI - Build native mobile apps with React",
|
|
5
|
+
"author": "Fleetbo",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"fleetbo-cli": "./bin/fleetbo.js"
|
|
9
|
+
},
|
|
10
|
+
"main": "cli.js",
|
|
11
|
+
"files": [
|
|
12
|
+
"bin",
|
|
13
|
+
"cli.js",
|
|
14
|
+
"page.js"
|
|
15
|
+
],
|
|
16
|
+
"keywords": [
|
|
17
|
+
"fleetbo",
|
|
18
|
+
"react-native",
|
|
19
|
+
"mobile",
|
|
20
|
+
"cli",
|
|
21
|
+
"android",
|
|
22
|
+
"ios"
|
|
23
|
+
],
|
|
24
|
+
"repository": {
|
|
25
|
+
"type": "git",
|
|
26
|
+
"url": "https://github.com/FleetFleetbo/fleetbo-cockpit-cli.git"
|
|
27
|
+
},
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"axios": "^1.6.0",
|
|
30
|
+
"dotenv": "^16.3.0",
|
|
31
|
+
"archiver": "^6.0.0"
|
|
32
|
+
},
|
|
33
|
+
"engines": {
|
|
34
|
+
"node": ">=16.0.0"
|
|
35
|
+
}
|
|
36
|
+
}
|
package/page.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const readline = require('readline');
|
|
4
|
+
|
|
5
|
+
const ARGS = process.argv.slice(2);
|
|
6
|
+
const COMMAND = ARGS[0];
|
|
7
|
+
const TARGET_NAME = ARGS[1];
|
|
8
|
+
|
|
9
|
+
// ✅ Utilise process.cwd() = dossier du projet dev (pas le package npm)
|
|
10
|
+
const APP_JS_PATH = path.join(process.cwd(), 'src/App.js');
|
|
11
|
+
|
|
12
|
+
const GREEN = '\x1b[32m';
|
|
13
|
+
const RED = '\x1b[31m';
|
|
14
|
+
const BLUE = '\x1b[36m';
|
|
15
|
+
const YELLOW = '\x1b[33m';
|
|
16
|
+
const RESET = '\x1b[0m';
|
|
17
|
+
|
|
18
|
+
// Template Fleetbo JS
|
|
19
|
+
const getWebTemplate = (pageName) => `import React from 'react';
|
|
20
|
+
import { ArrowLeftCircle } from 'lucide-react';
|
|
21
|
+
|
|
22
|
+
const ${pageName}Header = ({ onBack }) => (
|
|
23
|
+
<header className='navbar ps-3 pt-3'>
|
|
24
|
+
<div>
|
|
25
|
+
<button onClick={onBack} className="btn-header text-success fs-5 fw-bold d-flex align-items-center">
|
|
26
|
+
<ArrowLeftCircle /> <span className='ms-3'>${pageName}</span>
|
|
27
|
+
</button>
|
|
28
|
+
</div>
|
|
29
|
+
</header>
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
const ${pageName} = () => {
|
|
33
|
+
return (
|
|
34
|
+
<>
|
|
35
|
+
<${pageName}Header onBack={() => Fleetbo.back()} />
|
|
36
|
+
<div className="p-3 fade-in">
|
|
37
|
+
<div className="text-center mt-5">
|
|
38
|
+
<h3 className="text-secondary">${pageName}</h3>
|
|
39
|
+
<p className="text-muted small">Generated by Fleetbo CLI</p>
|
|
40
|
+
</div>
|
|
41
|
+
</div>
|
|
42
|
+
</>
|
|
43
|
+
);
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export default ${pageName};
|
|
47
|
+
`;
|
|
48
|
+
|
|
49
|
+
const injectIntoAppJs = (pageName, importPath, routePath) => {
|
|
50
|
+
if (!fs.existsSync(APP_JS_PATH)) {
|
|
51
|
+
console.log(`${YELLOW}⚠️ App.js not found. Skipping auto-wiring.${RESET}`);
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
let content = fs.readFileSync(APP_JS_PATH, 'utf8');
|
|
56
|
+
|
|
57
|
+
// Support des différentes ancres d'import
|
|
58
|
+
if (!content.includes('// FLEETBO_IMPORTS') && !content.includes('// FLEETBO_MORE_IMPORTS')) {
|
|
59
|
+
console.log(`${YELLOW}⚠️ Import anchors not found in App.js. Skipping auto-wiring.${RESET}`);
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// Support des différentes ancres de routes
|
|
64
|
+
if (!content.includes('// FLEETBO_ROUTES') && !content.includes('{/* FLEETBO_ROUTES */}') && !content.includes('{/* FLEETBO_DYNAMIC ROUTES */}')) {
|
|
65
|
+
console.log(`${YELLOW}⚠️ Route anchors not found in App.js. Skipping auto-wiring.${RESET}`);
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const cleanImport = importPath ? `/${importPath}` : '';
|
|
70
|
+
const importLine = `import ${pageName} from './app${cleanImport}/${pageName}';`;
|
|
71
|
+
const routeLine = `<Route path="/${routePath}" element={<${pageName} />} />`;
|
|
72
|
+
|
|
73
|
+
let injected = false;
|
|
74
|
+
|
|
75
|
+
if (!content.includes(importLine)) {
|
|
76
|
+
// Support des deux types d'ancres d'import
|
|
77
|
+
const importAnchor = content.includes('// FLEETBO_MORE_IMPORTS') ? '// FLEETBO_MORE_IMPORTS' : '// FLEETBO_IMPORTS';
|
|
78
|
+
content = content.replace(importAnchor, `${importLine}\n${importAnchor}`);
|
|
79
|
+
injected = true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
if (!content.includes(routeLine)) {
|
|
83
|
+
// Support des différentes ancres de routes
|
|
84
|
+
let routeAnchor = '{/* FLEETBO_DYNAMIC ROUTES */}';
|
|
85
|
+
if (!content.includes(routeAnchor)) {
|
|
86
|
+
routeAnchor = content.includes('{/* FLEETBO_ROUTES */}') ? '{/* FLEETBO_ROUTES */}' : '// FLEETBO_ROUTES';
|
|
87
|
+
}
|
|
88
|
+
content = content.replace(routeAnchor, `${routeLine}\n ${routeAnchor}`);
|
|
89
|
+
injected = true;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (injected) fs.writeFileSync(APP_JS_PATH, content);
|
|
93
|
+
return injected;
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const runWebGenerator = (inputPath) => {
|
|
97
|
+
if (!inputPath) {
|
|
98
|
+
askNameAndRun('Enter Page Path (e.g. admin/Settings):', runWebGenerator);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const parts = inputPath.split(/[/\\]/);
|
|
103
|
+
const rawName = parts.pop();
|
|
104
|
+
const subDir = parts.join('/');
|
|
105
|
+
const pageName = rawName.charAt(0).toUpperCase() + rawName.slice(1);
|
|
106
|
+
|
|
107
|
+
// ✅ Utilise process.cwd() = dossier du projet dev
|
|
108
|
+
const baseDir = path.join(process.cwd(), 'src/app');
|
|
109
|
+
const targetDir = path.join(baseDir, subDir);
|
|
110
|
+
const filePath = path.join(targetDir, `${pageName}.jsx`);
|
|
111
|
+
|
|
112
|
+
if (!fs.existsSync(targetDir)) fs.mkdirSync(targetDir, { recursive: true });
|
|
113
|
+
|
|
114
|
+
if (fs.existsSync(filePath)) {
|
|
115
|
+
console.log(`${RED}❌ Error: Page "${pageName}" already exists!${RESET}`);
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
fs.writeFileSync(filePath, getWebTemplate(pageName));
|
|
120
|
+
const routePath = subDir ? `${subDir}/${pageName.toLowerCase()}` : `${pageName.toLowerCase()}`;
|
|
121
|
+
const injected = injectIntoAppJs(pageName, subDir, routePath);
|
|
122
|
+
|
|
123
|
+
console.log(`
|
|
124
|
+
${GREEN}✓ FLEETBO WEB PAGE GENERATED${RESET}
|
|
125
|
+
${BLUE}src/app/${subDir ? subDir + '/' : ''}${pageName}.jsx${RESET}`);
|
|
126
|
+
|
|
127
|
+
if (injected) {
|
|
128
|
+
console.log(` ${GREEN}✓ App.js updated: Route /${routePath}${RESET}`);
|
|
129
|
+
}
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
const askNameAndRun = (question, callback) => {
|
|
133
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
134
|
+
rl.question(`${BLUE}🧬 ${question} ${RESET}`, (n) => {
|
|
135
|
+
if(n) callback(n);
|
|
136
|
+
rl.close();
|
|
137
|
+
});
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
const main = () => {
|
|
141
|
+
if (COMMAND === 'page' || COMMAND === 'g' || COMMAND === 'generate') {
|
|
142
|
+
runWebGenerator(TARGET_NAME);
|
|
143
|
+
} else {
|
|
144
|
+
console.log(`
|
|
145
|
+
${BLUE}🧬 FLEETBO CLI${RESET}
|
|
146
|
+
-------------------
|
|
147
|
+
Usage:
|
|
148
|
+
${GREEN}npm run fleetbo${RESET} (Start dev environment)
|
|
149
|
+
${GREEN}npm run fleetbo alex${RESET} (AI Architect mode)
|
|
150
|
+
${GREEN}npm run fleetbo page [Name]${RESET} (Create Fleetbo page)
|
|
151
|
+
${GREEN}npm run fleetbo android${RESET} (Build Android)
|
|
152
|
+
${GREEN}npm run fleetbo ios${RESET} (Build iOS)
|
|
153
|
+
`);
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
main();
|