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 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
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ require('../cli.js');
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();