fleetbo-cockpit-cli 1.0.74 → 1.0.76
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/cli.js +928 -112
- package/package.json +1 -1
package/cli.js
CHANGED
|
@@ -31,64 +31,238 @@ process.env.DOTENV_SILENT = 'true';
|
|
|
31
31
|
const envPath = path.join(process.cwd(), '.env');
|
|
32
32
|
|
|
33
33
|
if (!fs.existsSync(envPath)) {
|
|
34
|
-
console.error('\x1b[31m[
|
|
34
|
+
console.error('\x1b[31m%s\x1b[0m', '\n❌ Error: Configuration file (.env) not found.');
|
|
35
|
+
console.error('\x1b[90m%s\x1b[0m', 'Make sure you are in a Fleetbo project directory.\n');
|
|
35
36
|
process.exit(1);
|
|
36
37
|
}
|
|
37
38
|
|
|
38
|
-
dotenv.config({ path: envPath });
|
|
39
|
+
dotenv.config({ path: envPath, quiet: true });
|
|
39
40
|
|
|
40
|
-
const projectId = process.env.
|
|
41
|
-
const
|
|
41
|
+
const projectId = process.env.REACT_APP_ENTERPRISE_ID;
|
|
42
|
+
const keyApp = process.env.REACT_KEY_APP;
|
|
43
|
+
const testerEmail = process.env.REACT_APP_TESTER_EMAIL;
|
|
42
44
|
|
|
43
|
-
if (!projectId
|
|
44
|
-
console.error('\x1b[31m
|
|
45
|
+
if (!projectId) {
|
|
46
|
+
console.error('\n\x1b[31m❌ Error: Project ID missing in .env.\x1b[0m\n');
|
|
45
47
|
process.exit(1);
|
|
46
48
|
}
|
|
47
49
|
|
|
48
50
|
// ============================================
|
|
49
|
-
//
|
|
51
|
+
// HELPERS
|
|
50
52
|
// ============================================
|
|
53
|
+
const wrapText = (text, maxWidth) => {
|
|
54
|
+
if (!text) return "";
|
|
55
|
+
const rawLines = text.split('\n');
|
|
56
|
+
let formattedLines = [];
|
|
57
|
+
rawLines.forEach(line => {
|
|
58
|
+
if (line.trim().length === 0) {
|
|
59
|
+
formattedLines.push("");
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
62
|
+
const isSpecialFormat = /^[\s]*[-*•\d]/.test(line) || line.startsWith(" ");
|
|
63
|
+
if (isSpecialFormat) {
|
|
64
|
+
formattedLines.push(line);
|
|
65
|
+
} else {
|
|
66
|
+
const words = line.split(" ");
|
|
67
|
+
let currentLine = words[0];
|
|
68
|
+
for (let i = 1; i < words.length; i++) {
|
|
69
|
+
if (currentLine.length + 1 + words[i].length <= maxWidth) {
|
|
70
|
+
currentLine += " " + words[i];
|
|
71
|
+
} else {
|
|
72
|
+
formattedLines.push(currentLine);
|
|
73
|
+
currentLine = words[i];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
formattedLines.push(currentLine);
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
return formattedLines.join('\n ');
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const checkGitSecurity = () => {
|
|
83
|
+
const gitDir = path.join(process.cwd(), '.git');
|
|
84
|
+
const gitignorePath = path.join(process.cwd(), '.gitignore');
|
|
85
|
+
if (fs.existsSync(gitDir)) {
|
|
86
|
+
if (!fs.existsSync(gitignorePath)) {
|
|
87
|
+
console.error('\n\x1b[31m🚨 SECURITY ALERT:\x1b[0m .git detected but no .gitignore found.');
|
|
88
|
+
process.exit(1);
|
|
89
|
+
}
|
|
90
|
+
const gitignoreContent = fs.readFileSync(gitignorePath, 'utf8');
|
|
91
|
+
if (!gitignoreContent.includes('.env')) {
|
|
92
|
+
console.error('\n\x1b[31m🚨 CRITICAL RISK:\x1b[0m .env is NOT ignored by Git.');
|
|
93
|
+
process.exit(1);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const injectRouteIntoAppJs = (moduleName, subPath = '') => {
|
|
99
|
+
const appJsPath = path.join(process.cwd(), 'src', 'App.js');
|
|
100
|
+
if (!fs.existsSync(appJsPath)) {
|
|
101
|
+
console.error(` \x1b[31m[Safety Stop]\x1b[0m App.js missing.`);
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let content = fs.readFileSync(appJsPath, 'utf8');
|
|
106
|
+
const importAnchor = '// FLEETBO_MORE_IMPORTS';
|
|
107
|
+
const routeAnchor = '{/* FLEETBO_DYNAMIC ROUTES */}';
|
|
108
|
+
|
|
109
|
+
if (!content.includes(importAnchor) || !content.includes(routeAnchor)) {
|
|
110
|
+
console.log(` \x1b[33m[Skipped]\x1b[0m Anchors missing in App.js. Manual injection required.`);
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const cleanSubPath = subPath ? `${subPath}/` : '';
|
|
115
|
+
const importLine = `import ${moduleName} from './app/${cleanSubPath}${moduleName}';`;
|
|
116
|
+
const routeLine = `<Route path="/${cleanSubPath}${moduleName.toLowerCase()}" element={<${moduleName} />} />`;
|
|
117
|
+
|
|
118
|
+
let modified = false;
|
|
119
|
+
|
|
120
|
+
if (!content.includes(importLine)) {
|
|
121
|
+
content = content.replace(importAnchor, `${importLine}\n${importAnchor}`);
|
|
122
|
+
modified = true;
|
|
123
|
+
}
|
|
124
|
+
if (!content.includes(routeLine)) {
|
|
125
|
+
content = content.replace(routeAnchor, `${routeLine}\n ${routeAnchor}`);
|
|
126
|
+
modified = true;
|
|
127
|
+
}
|
|
128
|
+
if (modified) {
|
|
129
|
+
fs.writeFileSync(appJsPath, content);
|
|
130
|
+
console.log(` \x1b[32m[Routed]\x1b[0m ${moduleName} injected into App.js safely.`);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return modified;
|
|
134
|
+
};
|
|
51
135
|
|
|
52
|
-
const
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
136
|
+
const showEnergyTransfer = async () => {
|
|
137
|
+
const width = 30;
|
|
138
|
+
for (let i = 0; i <= width; i++) {
|
|
139
|
+
const dots = "█".repeat(i);
|
|
140
|
+
const empty = "░".repeat(width - i);
|
|
141
|
+
process.stdout.write(`\r \x1b[32m⚡ Propulsion Sync:\x1b[0m [${dots}${empty}] ${Math.round((i / width) * 100)}%`);
|
|
142
|
+
await new Promise(r => setTimeout(r, 45));
|
|
57
143
|
}
|
|
144
|
+
process.stdout.write('\n');
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
// Détecte TOUS les mots en PascalCase (ex: GuestCreator, CameraModule, Tab2)
|
|
148
|
+
const extractPotentialModules = (text) => {
|
|
149
|
+
// Regex stricte : Majuscule + minuscules + Majuscule (ex: ProfileCreator, UserConfig)
|
|
150
|
+
const regex = /\b[A-Z][a-z]+[A-Z][a-zA-Z0-9_]*\b/g;
|
|
151
|
+
const matches = text.match(regex) || [];
|
|
152
|
+
return [...new Set(matches)];
|
|
153
|
+
};
|
|
154
|
+
|
|
58
155
|
|
|
59
|
-
|
|
60
|
-
|
|
156
|
+
// Sert uniquement à définir le TON du contexte envoyé à Alex
|
|
157
|
+
const getContextIntent = (text) => {
|
|
158
|
+
const modifierKeywords = [
|
|
159
|
+
'modifier', 'corrige', 'ajoute', 'erreur', 'plante', 'problème', 'bug', 'change',
|
|
160
|
+
'update', 'fix', 'edit', 'error', 'fail', 'crash', 'issue', 'add'
|
|
161
|
+
];
|
|
162
|
+
const inspireKeywords = [
|
|
163
|
+
'inspire', 'base', 'comme', 'modèle', 'reference', 'reprends', 'copie',
|
|
164
|
+
'inspire', 'based on', 'model', 'reference', 'like', 'copy', 'similar'
|
|
165
|
+
];
|
|
166
|
+
|
|
167
|
+
const lower = text.toLowerCase();
|
|
168
|
+
if (modifierKeywords.some(k => lower.includes(k))) return "MODIFICATION";
|
|
169
|
+
if (inspireKeywords.some(k => lower.includes(k))) return "INSPIRATION";
|
|
170
|
+
return "REFERENCE";
|
|
171
|
+
};
|
|
61
172
|
|
|
173
|
+
const getModuleCache = async ({ projectId, moduleName }) => {
|
|
62
174
|
try {
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
175
|
+
const res = await axios.post(CACHE_URL, { projectId, moduleName });
|
|
176
|
+
if (res.data && res.data.found) {
|
|
177
|
+
// On renvoie à la fois "module" (pour un seul) et "modules" (pour la liste)
|
|
178
|
+
return { found: true, module: res.data.module, modules: res.data.modules };
|
|
179
|
+
}
|
|
180
|
+
return { found: false };
|
|
181
|
+
} catch (e) {
|
|
182
|
+
return { found: false };
|
|
183
|
+
}
|
|
184
|
+
};
|
|
70
185
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
186
|
+
const removeRouteFromAppJs = (moduleName) => {
|
|
187
|
+
const appJsPath = path.join(process.cwd(), 'src', 'App.js');
|
|
188
|
+
if (!fs.existsSync(appJsPath)) return false;
|
|
189
|
+
|
|
190
|
+
let content = fs.readFileSync(appJsPath, 'utf8');
|
|
191
|
+
|
|
192
|
+
// Pattern exact pour l'import et la route (gestion du sous-dossier mocks/)
|
|
193
|
+
const importLine = `import ${moduleName} from './app/mocks/${moduleName}';`;
|
|
194
|
+
const routeLine = `<Route path="/mocks/${moduleName.toLowerCase()}" element={<${moduleName} />} />`;
|
|
195
|
+
|
|
196
|
+
const originalContent = content;
|
|
197
|
+
|
|
198
|
+
// On retire les lignes si elles existent (avec le retour à la ligne)
|
|
199
|
+
content = content.replace(new RegExp(`${importLine.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n?`, 'g'), '');
|
|
200
|
+
content = content.replace(new RegExp(`\\s*${routeLine.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\n?`, 'g'), '');
|
|
201
|
+
|
|
202
|
+
if (content !== originalContent) {
|
|
203
|
+
fs.writeFileSync(appJsPath, content);
|
|
204
|
+
console.log(` \x1b[32m[Unrouted]\x1b[0m ${moduleName} removed from App.js.`);
|
|
205
|
+
return true;
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
};
|
|
75
209
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
210
|
+
// ============================================
|
|
211
|
+
// COMMAND: alex
|
|
212
|
+
// ============================================
|
|
213
|
+
if (command === 'alex') {
|
|
214
|
+
checkGitSecurity();
|
|
215
|
+
const initialPrompt = args.slice(1).join(' ');
|
|
216
|
+
|
|
217
|
+
const processAlexRequest = async (prompt) => {
|
|
218
|
+
if (prompt.length > 1000) {
|
|
219
|
+
console.log('\n\x1b[31m⛔ [Alex Safety] Request too long (' + prompt.length + '/1000 chars).\x1b[0m');
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
82
222
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
});
|
|
223
|
+
// 📋 INTERCEPTION: LIST MODULES
|
|
224
|
+
const isListingRequest = /^(liste|list|quels modules|montre les modules|show modules)/i.test(prompt);
|
|
225
|
+
if (isListingRequest) {
|
|
226
|
+
process.stdout.write(` \x1b[90m🔍 Scanning OS Cache...\x1b[0m\n`);
|
|
227
|
+
const cacheRes = await getModuleCache({ projectId, moduleName: null });
|
|
228
|
+
|
|
229
|
+
if (cacheRes.found !== false && cacheRes.modules && cacheRes.modules.length > 0) {
|
|
230
|
+
console.log(`\n\x1b[36m⚡ FLEETBO OS MODULES (${cacheRes.modules.length}):\x1b[0m`);
|
|
231
|
+
cacheRes.modules.forEach(m => {
|
|
232
|
+
const metalColor = m.platform === 'android' ? '\x1b[32m' : '\x1b[34m';
|
|
233
|
+
console.log(` ${metalColor}■\x1b[0m \x1b[1m${m.moduleName}\x1b[0m \x1b[90m(${m.platform})\x1b[0m`);
|
|
234
|
+
});
|
|
235
|
+
} else {
|
|
236
|
+
console.log(`\n \x1b[90mNo modules found in the infrastructure of this project.\x1b[0m`);
|
|
237
|
+
}
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
88
240
|
|
|
89
|
-
|
|
241
|
+
console.log('\x1b[33m🧠 Alex is thinking...\x1b[0m');
|
|
242
|
+
|
|
243
|
+
try {
|
|
244
|
+
// --- MEMORY SYSTEM (SMART CACHE SCANNER V3 - SOUVERAINETÉ DU MÉTAL) ---
|
|
245
|
+
let contextInjection = "";
|
|
246
|
+
const potentialModules = extractPotentialModules(prompt);
|
|
247
|
+
|
|
248
|
+
let targetModuleContext = "";
|
|
249
|
+
let referenceContexts = "";
|
|
250
|
+
|
|
251
|
+
for (let modName of potentialModules) {
|
|
252
|
+
let isReferenceOnly = false;
|
|
253
|
+
|
|
254
|
+
// 🟢 LE DÉTECTEUR D'INSPIRATION
|
|
255
|
+
if (modName.endsWith('Ref')) {
|
|
256
|
+
isReferenceOnly = true;
|
|
257
|
+
modName = modName.replace('Ref', '');
|
|
258
|
+
} else if (modName.endsWith('Schema')) {
|
|
259
|
+
isReferenceOnly = true;
|
|
260
|
+
modName = modName.replace('Schema', '');
|
|
261
|
+
}
|
|
90
262
|
|
|
91
|
-
|
|
263
|
+
process.stdout.write(` \x1b[90m🔍 Checking OS cache for ${modName}...\x1b[0m`);
|
|
264
|
+
const cache = await getModuleCache({ projectId, moduleName: modName });
|
|
265
|
+
|
|
92
266
|
if (cache.found) {
|
|
93
267
|
process.stdout.write(` \x1b[32mFOUND METAL\x1b[0m\n`);
|
|
94
268
|
|
|
@@ -97,107 +271,749 @@ const runAlexEngine = async () => {
|
|
|
97
271
|
if (cache.module.dataSchema) memoryScript += `\n[SCRIPT MÉMOIRE DONNÉES]\n${cache.module.dataSchema}\n`;
|
|
98
272
|
if (cache.module.uiSchema) memoryScript += `\n[SCRIPT MÉMOIRE UI NATIF]\n${cache.module.uiSchema}\n`;
|
|
99
273
|
|
|
100
|
-
if (!memoryScript) memoryScript = `\n[SCRIPT MÉMOIRE DU MODULE ${
|
|
274
|
+
if (!memoryScript) memoryScript = `\n[SCRIPT MÉMOIRE DU MODULE ${modName}]\nAucun schéma enregistré pour ce module.\n`;
|
|
101
275
|
|
|
102
|
-
if (
|
|
276
|
+
if (isReferenceOnly) {
|
|
103
277
|
// 🚨 CAS A : INSPIRATION (Lecture seule)
|
|
104
|
-
referenceContexts += `\n--- CONTEXTE : MODULE DE RÉFÉRENCE (${
|
|
105
|
-
} else {
|
|
106
|
-
// 🚨 CAS B :
|
|
107
|
-
|
|
278
|
+
referenceContexts += `\n--- CONTEXTE : MODULE DE RÉFÉRENCE (${modName}) ---\nDOGME : Ne modifie pas ce module (Lecture seule). Tu dois l'utiliser comme modèle. Aligne-toi sur ses Scripts Mémoires (Données et/ou UI) et sur son Code Natif en fonction de ce que le Pilote te demande d'imiter.\n${memoryScript}\n[CODE NATIF DE RÉFÉRENCE]\n${cache.module.code}\n`;
|
|
279
|
+
} else if (!targetModuleContext) {
|
|
280
|
+
// 🚨 CAS B : CIBLE PRINCIPALE (Modification)
|
|
281
|
+
// LE MOCK EST BANI ! Le Métal est la seule source de vérité.
|
|
282
|
+
const intent = getContextIntent(prompt);
|
|
283
|
+
if (intent === "MODIFICATION") {
|
|
284
|
+
targetModuleContext = `\n--- CONTEXTE : MÉTAL EXISTANT À MODIFIER (${modName}) ---\nDOGME: Tu dois modifier ce code Natif. Ensuite, tu forgeras un Mock JSX entièrement neuf basé UNIQUEMENT sur ton nouveau code Natif.\n${memoryScript}\n[CODE NATIF EXISTANT]\n${cache.module.code}\n--- FIN DU CONTEXTE ---\n`;
|
|
285
|
+
} else {
|
|
286
|
+
targetModuleContext = `\n--- CONTEXTE : BASE DE TRAVAIL (${modName}) ---\n${memoryScript}\n`;
|
|
287
|
+
}
|
|
108
288
|
}
|
|
289
|
+
} else {
|
|
290
|
+
process.stdout.write(` \x1b[31mNOT FOUND\x1b[0m\n`);
|
|
109
291
|
}
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
contextInjection = referenceContexts + targetModuleContext;
|
|
295
|
+
if (contextInjection) {
|
|
296
|
+
prompt = contextInjection + "\n\n[INSTRUCTION DU PILOTE]\n" + prompt;
|
|
297
|
+
}
|
|
298
|
+
// --- END MEMORY MODIFICATION ---
|
|
299
|
+
|
|
300
|
+
// 🟢 NEW: Real-time timestamp injection
|
|
301
|
+
const now = new Date();
|
|
302
|
+
// Clean format: YYYY-MM-DD at HH:MM:SS
|
|
303
|
+
const exactTime = now.toISOString().split('T')[0] + ' at ' + now.toTimeString().split(' ')[0];
|
|
304
|
+
|
|
305
|
+
const promptWithTime = prompt + `\n\n[SYSTEM INFO: The exact current timestamp is ${exactTime}. Use it STRICTLY for your signature '// ⚡ Forged by Alex on...' at the bottom of your files.]`;
|
|
306
|
+
|
|
307
|
+
const result = await axios.post(ALEX_ENGINE_URL, { prompt: promptWithTime, projectType: 'android' }, {
|
|
308
|
+
headers: { 'x-project-id': projectId }
|
|
110
309
|
});
|
|
111
|
-
}
|
|
112
310
|
|
|
113
|
-
|
|
311
|
+
let aiData = result.data;
|
|
312
|
+
|
|
313
|
+
// 🛡️ ROBUST PARSER: Unwrap all nesting levels until we find a valid aiData object
|
|
314
|
+
const deepUnwrap = (raw, depth = 0) => {
|
|
315
|
+
if (depth > 5) return raw; // Safety: max 5 nesting levels
|
|
316
|
+
if (typeof raw === 'string') {
|
|
317
|
+
// Try to extract a JSON object even if there's trailing garbage (truncation)
|
|
318
|
+
const jsonMatch = raw.match(/\{[\s\S]*\}/);
|
|
319
|
+
if (jsonMatch) {
|
|
320
|
+
try {
|
|
321
|
+
return deepUnwrap(JSON.parse(jsonMatch[0]), depth + 1);
|
|
322
|
+
} catch (_) {}
|
|
323
|
+
}
|
|
324
|
+
return raw;
|
|
325
|
+
}
|
|
326
|
+
if (typeof raw === 'object' && raw !== null) {
|
|
327
|
+
// If the message field itself is a JSON string, unwrap it and merge moduleData
|
|
328
|
+
if (typeof raw.message === 'string' && raw.message.trimStart().startsWith('{')) {
|
|
329
|
+
try {
|
|
330
|
+
const nested = JSON.parse(raw.message);
|
|
331
|
+
if (nested && typeof nested === 'object' && nested.status) {
|
|
332
|
+
// Merge: keep top-level moduleData if present, else take nested one
|
|
333
|
+
if (!raw.moduleData && nested.moduleData) raw.moduleData = nested.moduleData;
|
|
334
|
+
raw.message = nested.message || raw.message;
|
|
335
|
+
if (!raw.status && nested.status) raw.status = nested.status;
|
|
336
|
+
}
|
|
337
|
+
} catch (_) {}
|
|
338
|
+
}
|
|
339
|
+
return raw;
|
|
340
|
+
}
|
|
341
|
+
return raw;
|
|
342
|
+
};
|
|
114
343
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
});
|
|
344
|
+
aiData = deepUnwrap(aiData);
|
|
345
|
+
|
|
346
|
+
// 🟢 DISPLAY REASONING IN TERMINAL
|
|
347
|
+
if (aiData.thinking_process) {
|
|
348
|
+
// Show start of reasoning in gray for info
|
|
349
|
+
console.log(` \x1b[90m🧠 Alex Analysis: ${aiData.thinking_process.substring(0, 150)}...\x1b[0m`);
|
|
350
|
+
}
|
|
123
351
|
|
|
124
|
-
|
|
352
|
+
process.stdout.write('\x1b[A\r' + ' '.repeat(50) + '\r');
|
|
353
|
+
|
|
354
|
+
if (aiData.status === 'quota_exceeded') {
|
|
355
|
+
console.log(`\n\x1b[31m⛔ ARCHITECT QUOTA REACHED:\x1b[0m ${aiData.message}`);
|
|
356
|
+
return;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
if (aiData.status === 'success' || aiData.status === 'message' || aiData.status === 'complex_refusal') {
|
|
360
|
+
console.log('');
|
|
361
|
+
let rawMsg = aiData.message || "I'm ready.";
|
|
362
|
+
|
|
363
|
+
// Final safety: if rawMsg is still an object
|
|
364
|
+
if (typeof rawMsg === 'object') {
|
|
365
|
+
rawMsg = rawMsg.message || rawMsg.text || "Module generated.";
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
const formattedMsg = wrapText(rawMsg, 85);
|
|
369
|
+
console.log('\x1b[32mAlex ❯\x1b[0m ' + formattedMsg);
|
|
370
|
+
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
// --- FILE CREATION LOGIC ---
|
|
374
|
+
if (aiData.status === 'success' && aiData.moduleData) {
|
|
375
|
+
let { fileName, code, mockFileName, mockCode, moduleName, instructions, config_offload, dataSchema, uiSchema } = aiData.moduleData;
|
|
376
|
+
|
|
377
|
+
// 🛡️ ANTI-DUMP SHIELD (Prevents terminal flooding)
|
|
378
|
+
if (moduleName) {
|
|
379
|
+
// Cleanup module name
|
|
380
|
+
moduleName = moduleName.split('\n')[0].replace(/["'{}]/g, '').trim();
|
|
381
|
+
if (moduleName.length > 40) moduleName = moduleName.substring(0, 40) + "...";
|
|
382
|
+
}
|
|
125
383
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
384
|
+
console.log(` \x1b[90m Architecting: ${moduleName}\x1b[0m`);
|
|
385
|
+
|
|
386
|
+
const writeFile = (dir, name, content) => {
|
|
387
|
+
const fullPath = path.join(process.cwd(), dir);
|
|
388
|
+
const filePath = path.join(fullPath, name);
|
|
389
|
+
if (!fs.existsSync(fullPath)) fs.mkdirSync(fullPath, { recursive: true });
|
|
390
|
+
fs.writeFileSync(filePath, content);
|
|
391
|
+
console.log(` \x1b[32m[Written]\x1b[0m ${dir}${name}`);
|
|
392
|
+
};
|
|
393
|
+
|
|
394
|
+
if (instructions && Array.isArray(instructions) && instructions.length > 0) {
|
|
395
|
+
console.log('\n\x1b[33m--- GUIDE (MCI) ---\x1b[0m');
|
|
396
|
+
instructions.forEach(line => {
|
|
397
|
+
if (typeof line === 'string') {
|
|
398
|
+
const formattedLine = line.replace(/ACTION|CAPTURE|PERSPECTIVE/g, '\x1b[1m$&\x1b[0m');
|
|
399
|
+
console.log(` ${formattedLine}`);
|
|
400
|
+
}
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (code && fileName) {
|
|
405
|
+
const folder = fileName.endsWith('.kt') ? 'public/native/android/' : 'src/app/';
|
|
406
|
+
writeFile(folder, fileName, code);
|
|
407
|
+
if (fileName.endsWith('.jsx')) injectRouteIntoAppJs(fileName.replace('.jsx', ''));
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
if (mockCode && mockFileName) {
|
|
411
|
+
const pageName = mockFileName.replace('.jsx', '');
|
|
412
|
+
writeFile('src/app/mocks/', mockFileName, mockCode);
|
|
413
|
+
const injected = injectRouteIntoAppJs(pageName, 'mocks');
|
|
414
|
+
if (injected) {
|
|
415
|
+
console.log(` \x1b[32m[Routed]\x1b[0m App.js -> /mocks/${pageName.toLowerCase()}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// --- KERNEL SYNCHRONIZATION ---
|
|
420
|
+
const depsCount = config_offload?.dependencies?.length || 0;
|
|
421
|
+
process.stdout.write(` \x1b[33m[Cloud Inject]\x1b[0m Archiving ${moduleName} to OS (${depsCount} libs)...`);
|
|
422
|
+
|
|
423
|
+
try {
|
|
424
|
+
await axios.post(INJECT_DEPS_URL, {
|
|
425
|
+
projectId: projectId,
|
|
426
|
+
fileData: {
|
|
427
|
+
path: fileName,
|
|
428
|
+
moduleName: moduleName,
|
|
429
|
+
fileName: fileName,
|
|
430
|
+
code: code,
|
|
431
|
+
mockFileName: mockFileName,
|
|
432
|
+
mockCode: mockCode,
|
|
433
|
+
config_offload: config_offload || { dependencies: [], permissions: [] },
|
|
434
|
+
dataSchema: dataSchema || null, // 👈 LA LIGNE MAGIQUE
|
|
435
|
+
uiSchema: uiSchema || null
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
process.stdout.write(` \x1b[32mOK\x1b[0m\n`);
|
|
439
|
+
} catch (err) {
|
|
440
|
+
process.stdout.write(` \x1b[31mFAILED\x1b[0m\n`);
|
|
441
|
+
console.error(` ⚠️ OS sync failed: ${err.message}`);
|
|
442
|
+
}
|
|
443
|
+
// 🟢 LE FUEL EST DÉPLACÉ ICI (TOUTE FIN DU TRY)
|
|
444
|
+
if (aiData.remainingConsultations !== undefined) {
|
|
445
|
+
const remaining = aiData.remainingConsultations;
|
|
446
|
+
const limit = aiData.consultationLimit || 7;
|
|
447
|
+
const tierLabel = aiData.tier.toUpperCase();
|
|
448
|
+
const percent = Math.round((remaining / limit) * 100);
|
|
449
|
+
const energyColor = percent > 20 ? '\x1b[32m' : '\x1b[31m';
|
|
450
|
+
console.log(`\n\x1b[36m⚡ Architect Fuel:\x1b[0m ${energyColor}${percent}%\x1b[0m (${remaining}/${limit} instructions left) [${tierLabel}]\n`);
|
|
451
|
+
}
|
|
452
|
+
} else if (aiData.status === 'success' && !aiData.moduleData) {
|
|
453
|
+
// SAFETY: If formatting is broken
|
|
454
|
+
console.log(`\n\x1b[31m⚠️ Error: Alex replied, but source code could not be extracted. Try the command again.\x1b[0m\n`);
|
|
455
|
+
}
|
|
456
|
+
} catch (error) {
|
|
457
|
+
process.stdout.write('\r' + ' '.repeat(50) + '\r');
|
|
458
|
+
console.error('\n\x1b[31m Alex Error:\x1b[0m ' + (error.response?.data?.message || error.message));
|
|
130
459
|
}
|
|
460
|
+
};
|
|
461
|
+
|
|
462
|
+
const startAlexSession = async () => {
|
|
463
|
+
process.stdout.write('\x1b[33m🛡️ Alex is checking runtime state...\x1b[0m\r');
|
|
464
|
+
let attempts = 0;
|
|
465
|
+
const maxAttempts = 5;
|
|
466
|
+
let isReady = false;
|
|
467
|
+
let dynamicUsername = 'Pilot';
|
|
468
|
+
|
|
469
|
+
while (attempts < maxAttempts && !isReady) {
|
|
470
|
+
try {
|
|
471
|
+
const validation = await axios.post(ALEX_ENGINE_URL, {
|
|
472
|
+
prompt: "ping", validateProject: true, checkNetwork: true, projectKey: keyApp
|
|
473
|
+
}, { headers: { 'x-project-id': projectId }, timeout: 5000 });
|
|
474
|
+
|
|
475
|
+
if (validation.data?.isRunning) {
|
|
476
|
+
isReady = true;
|
|
477
|
+
dynamicUsername = validation.data.username || 'Pilot';
|
|
478
|
+
break;
|
|
479
|
+
}
|
|
480
|
+
attempts++;
|
|
481
|
+
if (attempts < maxAttempts) await new Promise(r => setTimeout(r, 2000));
|
|
482
|
+
} catch (error) {
|
|
483
|
+
attempts++;
|
|
484
|
+
await new Promise(r => setTimeout(r, 2000));
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (!isReady) {
|
|
489
|
+
console.error('\n\x1b[31m⚠️ ENGINE OFFLINE:\x1b[0m Start Fleetbo runtime first: "npm run fleetbo" ');
|
|
490
|
+
console.error(`\x1b[90m(Ensure you are running the runtime for project: ${keyApp})\x1b[0m`);
|
|
491
|
+
process.exit(1);
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
process.stdout.write(' '.repeat(60) + '\r');
|
|
495
|
+
|
|
496
|
+
// 1. IDENTITY
|
|
497
|
+
console.log('\n\x1b[32m🤖 Alex is online.\x1b[0m');
|
|
498
|
+
console.log('\x1b[90m Your JS stays the brain. I forge the native muscle.\x1b[0m');
|
|
499
|
+
|
|
500
|
+
// 2. FORGE CAPABILITIES
|
|
501
|
+
console.log('\n\x1b[36m⚡ WHAT I CAN FORGE:\x1b[0m');
|
|
502
|
+
console.log('');
|
|
503
|
+
console.log(' \x1b[1m📷 Hardware\x1b[0m\x1b[90m Camera, Scanner, GPS, Biometrics, Sensors\x1b[0m');
|
|
504
|
+
console.log(' \x1b[1m🎬 High-Perf\x1b[0m\x1b[90m Infinite Feeds, Video Players, Swipe Decks\x1b[0m');
|
|
505
|
+
console.log(' \x1b[1m🏗️ Sovereign\x1b[0m\x1b[90m Full screens: form + photo + save-to-cloud\x1b[0m');
|
|
506
|
+
|
|
507
|
+
// 3. COLLABORATION
|
|
508
|
+
console.log('\n\x1b[36m💡 TELL ME "WHAT + WHY":\x1b[0m');
|
|
509
|
+
console.log('\x1b[90m I will analyze your need and recommend the perfect module to forge.\x1b[0m');
|
|
510
|
+
console.log('');
|
|
511
|
+
console.log(' \x1b[33m"I need a camera [WHAT] to scan receipts for my expense tracker [WHY]"\x1b[0m');
|
|
512
|
+
console.log(' \x1b[33m"I need a form [WHAT] to add products with photos to my catalog [WHY]"\x1b[0m');
|
|
513
|
+
|
|
514
|
+
// 4. READINESS
|
|
515
|
+
console.log('\n\x1b[32mAlex ❯\x1b[0m I am ready. Describe your feature, and I will architect the solution.');
|
|
516
|
+
console.log('');
|
|
517
|
+
|
|
518
|
+
const rl = readline.createInterface({
|
|
519
|
+
input: process.stdin,
|
|
520
|
+
output: process.stdout,
|
|
521
|
+
prompt: `\x1b[34m${dynamicUsername} ❯ \x1b[0m`
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
process.stdout.write('\n\x1b[F');
|
|
525
|
+
rl.prompt();
|
|
526
|
+
|
|
527
|
+
let inputBuffer = "";
|
|
528
|
+
let isProcessing = false;
|
|
131
529
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
let { fileName, code, mockFileName, mockCode, moduleName, instructions, config_offload, dataSchema, uiSchema } = aiData.moduleData;
|
|
530
|
+
rl.on('line', async (line) => {
|
|
531
|
+
if (isProcessing) return;
|
|
135
532
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
533
|
+
const trimmedLine = line.trim();
|
|
534
|
+
|
|
535
|
+
if (['exit', 'quit'].includes(trimmedLine.toLowerCase())) {
|
|
536
|
+
console.log('\n\x1b[90m Alex session closed.\x1b[0m');
|
|
537
|
+
rl.close();
|
|
538
|
+
return;
|
|
139
539
|
}
|
|
140
540
|
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
541
|
+
if (trimmedLine !== "") {
|
|
542
|
+
inputBuffer += (inputBuffer ? "\n" : "") + line;
|
|
543
|
+
rl.setPrompt("");
|
|
544
|
+
}
|
|
545
|
+
else {
|
|
546
|
+
if (inputBuffer.trim() !== "") {
|
|
547
|
+
const finalPrompt = inputBuffer.trim();
|
|
548
|
+
inputBuffer = "";
|
|
549
|
+
|
|
550
|
+
if (finalPrompt.length > 1000) {
|
|
551
|
+
console.log(`\n\x1b[31m⛔ [Alex Safety] Mission rejected: Excessive size (${finalPrompt.length}/1000 characters).\x1b[0m`);
|
|
552
|
+
rl.setPrompt(`\x1b[34m${dynamicUsername} ❯ \x1b[0m`);
|
|
553
|
+
rl.prompt();
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
144
556
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
557
|
+
isProcessing = true;
|
|
558
|
+
rl.setPrompt("");
|
|
559
|
+
await processAlexRequest(finalPrompt);
|
|
560
|
+
isProcessing = false;
|
|
561
|
+
|
|
562
|
+
console.log('');
|
|
563
|
+
rl.setPrompt(`\x1b[34m${dynamicUsername} ❯ \x1b[0m`);
|
|
564
|
+
rl.prompt();
|
|
565
|
+
} else {
|
|
566
|
+
rl.setPrompt(`\x1b[34m${dynamicUsername} ❯ \x1b[0m`);
|
|
567
|
+
rl.prompt();
|
|
568
|
+
}
|
|
151
569
|
}
|
|
570
|
+
});
|
|
571
|
+
};
|
|
572
|
+
|
|
573
|
+
if (!initialPrompt || initialPrompt === '?') startAlexSession();
|
|
574
|
+
else processAlexRequest(initialPrompt);
|
|
575
|
+
|
|
576
|
+
}
|
|
152
577
|
|
|
153
|
-
|
|
578
|
+
// ============================================
|
|
579
|
+
// COMMAND: rm (MODULE ANNIHILATION)
|
|
580
|
+
// ============================================
|
|
581
|
+
else if (command === 'rm') {
|
|
582
|
+
const moduleName = args[1];
|
|
583
|
+
if (!moduleName) {
|
|
584
|
+
console.error('\n\x1b[31m❌ Error: Module name required.\x1b[0m');
|
|
585
|
+
console.log('\x1b[90mUsage: npm run fleetbo rm [ModuleName]\x1b[0m\n');
|
|
586
|
+
process.exit(1);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
console.log(`\n\x1b[33m🗑️ Annihilating module: ${moduleName}...\x1b[0m`);
|
|
590
|
+
|
|
591
|
+
// 1. Define physical paths
|
|
592
|
+
const ktPath = path.join(process.cwd(), 'public', 'native', 'android', `${moduleName}.kt`);
|
|
593
|
+
const jsxPath = path.join(process.cwd(), 'src', 'app', 'mocks', `${moduleName}.jsx`);
|
|
594
|
+
|
|
595
|
+
let actionsDone = 0;
|
|
596
|
+
|
|
597
|
+
// 2. Eradicate Metal Engine (Kotlin)
|
|
598
|
+
if (fs.existsSync(ktPath)) {
|
|
599
|
+
fs.unlinkSync(ktPath);
|
|
600
|
+
console.log(` \x1b[32m[Deleted]\x1b[0m Metal file (.kt) eradicated.`);
|
|
601
|
+
actionsDone++;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// 3. Eradicate Virtual Twin (Mock JSX)
|
|
605
|
+
if (fs.existsSync(jsxPath)) {
|
|
606
|
+
fs.unlinkSync(jsxPath);
|
|
607
|
+
console.log(` \x1b[32m[Deleted]\x1b[0m Virtual Twin (.jsx) eradicated.`);
|
|
608
|
+
actionsDone++;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
// 4. Disinfect System Core (App.js)
|
|
612
|
+
const unrouted = removeRouteFromAppJs(moduleName);
|
|
613
|
+
if (unrouted) actionsDone++;
|
|
614
|
+
|
|
615
|
+
if (actionsDone === 0) {
|
|
616
|
+
console.log(`\n\x1b[31m⚠️ No trace of module "${moduleName}" found in the OS.\x1b[0m\n`);
|
|
617
|
+
} else {
|
|
618
|
+
console.log(`\n\x1b[32m Module ${moduleName} successfully eradicated from the OS.\x1b[0m\n`);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// ============================================
|
|
623
|
+
// COMMAND: android / ios (PROPULSION BUILD)
|
|
624
|
+
// ============================================
|
|
625
|
+
else if (command === 'android' || command === 'ios') {
|
|
626
|
+
|
|
627
|
+
// 🟢 DÉBUT DE LA PROTECTION (Fonction Async Immédiate)
|
|
628
|
+
// Cela garantit que le code fonctionne partout, même via 'require()'
|
|
629
|
+
(async () => {
|
|
630
|
+
|
|
631
|
+
// 🛑 INTERCEPTION IOS : BLOQUAGE NET (MAINTENANCE/BETA)
|
|
632
|
+
if (command === 'ios') {
|
|
633
|
+
console.log(`\n\x1b[36m⚡ FLEETBO IOS PROPULSION\x1b[0m`);
|
|
634
|
+
console.log(`\x1b[33m[0/3] Initializing Neural Uplink...\x1b[0m`);
|
|
635
|
+
|
|
636
|
+
// ✅ Ce 'await' est maintenant sécurisé
|
|
637
|
+
await new Promise(r => setTimeout(r, 800));
|
|
638
|
+
|
|
639
|
+
console.log(`\n\x1b[31m⛔ PROPULSION ABORTED: iOS Frequency Restricted.\x1b[0m`);
|
|
640
|
+
console.log(`\x1b[90m This module is currently reserved for Vanguard Pilots (Closed Beta).\x1b[0m`);
|
|
641
|
+
console.log(`\x1b[90m Please engage propulsion on Android frequency for now.\x1b[0m\n`);
|
|
642
|
+
|
|
643
|
+
process.exit(1);
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
checkGitSecurity();
|
|
647
|
+
const platform = command;
|
|
648
|
+
const nativeDir = platform === 'android' ? 'public/native/android/' : 'public/native/ios/';
|
|
649
|
+
const extension = platform === 'android' ? '.kt' : '.swift';
|
|
650
|
+
const nativePath = path.join(process.cwd(), nativeDir);
|
|
651
|
+
|
|
652
|
+
// Vérification des modules natifs
|
|
653
|
+
let hasNativeFiles = false;
|
|
654
|
+
let nativeFileCount = 0;
|
|
655
|
+
if (fs.existsSync(nativePath)) {
|
|
656
|
+
const files = fs.readdirSync(nativePath);
|
|
657
|
+
const nativeFiles = files.filter(file => file.endsWith(extension));
|
|
658
|
+
hasNativeFiles = nativeFiles.length > 0;
|
|
659
|
+
nativeFileCount = nativeFiles.length;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
if (!hasNativeFiles) {
|
|
663
|
+
console.log(`\n\x1b[31m⚠️ ENGINE INCOMPLETE:\x1b[0m No native blueprints detected for \x1b[1m${platform.toUpperCase()}\x1b[0m.`);
|
|
664
|
+
console.log(`\x1b[90mAlex must architect at least one ${extension} module before deployment.\x1b[0m`);
|
|
665
|
+
console.log(`\x1b[90mRun: npm run fleetbo alex\x1b[0m\n`);
|
|
666
|
+
process.exit(1);
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
const targetUrl = platform === 'android' ? ANDROID_BUILD_URL : IOS_BUILD_URL;
|
|
670
|
+
|
|
671
|
+
console.log(`\n\x1b[36m⚡ FLEETBO ${platform.toUpperCase()} PROPULSION\x1b[0m`);
|
|
672
|
+
console.log(`\x1b[90m ${nativeFileCount} native module(s) detected\x1b[0m\n`);
|
|
673
|
+
|
|
674
|
+
try {
|
|
675
|
+
// ==========================================================
|
|
676
|
+
// PRE-FLIGHT CHECK QUOTAS & TIER
|
|
677
|
+
// ==========================================================
|
|
678
|
+
process.stdout.write(`\x1b[33m[0/3]\x1b[0m Checking Propulsion Access... `);
|
|
154
679
|
try {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
moduleName: moduleName || fileName.split('.')[0],
|
|
161
|
-
fileName: fileName,
|
|
162
|
-
code: code,
|
|
163
|
-
mockFileName: mockFileName,
|
|
164
|
-
mockCode: mockCode,
|
|
165
|
-
config_offload: config_offload || { dependencies: [], permissions: [] },
|
|
166
|
-
dataSchema: dataSchema || null,
|
|
167
|
-
uiSchema: uiSchema || null
|
|
680
|
+
// On envoie le signal 'x-preflight' pour tester les droits sans builder
|
|
681
|
+
await axios.post(targetUrl, {}, {
|
|
682
|
+
headers: {
|
|
683
|
+
'x-project-id': projectId,
|
|
684
|
+
'x-preflight': 'true'
|
|
168
685
|
}
|
|
169
686
|
});
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
687
|
+
// Si ça passe (200 OK), c'est un Senior ou un Junior autorisé (si vous changez d'avis)
|
|
688
|
+
process.stdout.write(`\x1b[32mOK (Senior Pilot)\x1b[0m\n\n`);
|
|
689
|
+
|
|
690
|
+
} catch (preflightError) {
|
|
691
|
+
process.stdout.write(`\x1b[31mDENIED\x1b[0m\n`);
|
|
692
|
+
|
|
693
|
+
const errData = preflightError.response?.data;
|
|
694
|
+
|
|
695
|
+
// 🛑 1. INTERCEPTION SPÉCIFIQUE JUNIOR
|
|
696
|
+
// C'est ici qu'on lit le code renvoyé par index.js
|
|
697
|
+
if (errData?.code === 'junior_restriction') {
|
|
698
|
+
console.log(`\n\x1b[31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m`);
|
|
699
|
+
console.log(`\x1b[31m⛔ ACCESS DENIED: JUNIOR PILOT DETECTED\x1b[0m`);
|
|
700
|
+
console.log(`\x1b[31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m`);
|
|
701
|
+
console.log(``);
|
|
702
|
+
console.log(` \x1b[33m This feature is locked for Junior Pilots.\x1b[0m`);
|
|
703
|
+
console.log(` \x1b[32m Upgrade to Senior on fleetbo.io to unlock Propulsion.\x1b[0m`);
|
|
704
|
+
console.log(``);
|
|
705
|
+
process.exit(1); // Arrêt immédiat et propre du script
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
// 2. Gestion des autres erreurs (Quota Senior dépassé, Serveur HS, etc.)
|
|
709
|
+
if (errData && errData.error) {
|
|
710
|
+
throw new Error(errData.error);
|
|
711
|
+
}
|
|
712
|
+
throw preflightError;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
// Étape 1: Build React
|
|
716
|
+
console.log(`\x1b[33m[1/3]\x1b[0m Synthesizing Fleetbo Core Logic...`);
|
|
717
|
+
execSync('npm run build', { stdio: 'inherit' });
|
|
718
|
+
|
|
719
|
+
let buildDir = fs.existsSync(path.join(process.cwd(), 'dist')) ? 'dist' : 'build';
|
|
720
|
+
const buildPath = path.join(process.cwd(), buildDir);
|
|
721
|
+
|
|
722
|
+
if (!fs.existsSync(buildPath)) {
|
|
723
|
+
throw new Error(`Build directory not found: ${buildDir}`);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Étape 2: Créer le ZIP
|
|
727
|
+
console.log(`\n\x1b[33m[2/3]\x1b[0m Packaging bundle + native modules...`);
|
|
728
|
+
|
|
729
|
+
const zipBuffer = await new Promise((resolve, reject) => {
|
|
730
|
+
const chunks = [];
|
|
731
|
+
const archive = archiver('zip', { zlib: { level: 9 } });
|
|
732
|
+
|
|
733
|
+
archive.on('data', chunk => chunks.push(chunk));
|
|
734
|
+
archive.on('end', () => resolve(Buffer.concat(chunks)));
|
|
735
|
+
archive.on('error', reject);
|
|
736
|
+
archive.on('warning', (err) => {
|
|
737
|
+
if (err.code !== 'ENOENT') reject(err);
|
|
738
|
+
});
|
|
739
|
+
|
|
740
|
+
archive.directory(buildPath, 'build');
|
|
741
|
+
if (fs.existsSync(nativePath)) {
|
|
742
|
+
archive.directory(nativePath, `build/native/${platform}`);
|
|
743
|
+
}
|
|
744
|
+
archive.finalize();
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
const sizeMB = (zipBuffer.length / 1024 / 1024).toFixed(2);
|
|
748
|
+
console.log(` \x1b[32m✓\x1b[0m Bundle ready: ${sizeMB} MB`);
|
|
749
|
+
|
|
750
|
+
// Étape 3: Upload
|
|
751
|
+
console.log(`\n\x1b[33m[3/3]\x1b[0m Uploading to Fleetbo OS...`);
|
|
752
|
+
await showEnergyTransfer();
|
|
753
|
+
|
|
754
|
+
let uploadResponse;
|
|
755
|
+
try {
|
|
756
|
+
uploadResponse = await axios.post(targetUrl, zipBuffer, {
|
|
757
|
+
headers: {
|
|
758
|
+
'Content-Type': 'application/zip',
|
|
759
|
+
'x-project-id': projectId
|
|
760
|
+
},
|
|
761
|
+
maxContentLength: Infinity,
|
|
762
|
+
maxBodyLength: Infinity,
|
|
763
|
+
timeout: 120000
|
|
764
|
+
});
|
|
765
|
+
} catch (axiosError) {
|
|
766
|
+
if (axiosError.response && axiosError.response.data && axiosError.response.data.error) {
|
|
767
|
+
throw new Error(axiosError.response.data.error);
|
|
768
|
+
} else {
|
|
769
|
+
throw new Error(`Connection to OS failed: ${axiosError.message}`);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
if (uploadResponse.data && uploadResponse.data.success) {
|
|
774
|
+
console.log(`\n\x1b[32m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m`);
|
|
775
|
+
console.log(`\x1b[32m✓ ${platform.toUpperCase()} PROPULSION SUCCESSFUL\x1b[0m`);
|
|
776
|
+
console.log(`\x1b[32m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m`);
|
|
777
|
+
console.log(`\x1b[90m Deployment ID: ${uploadResponse.data.deploymentId || 'N/A'}\x1b[0m`);
|
|
778
|
+
console.log(`\x1b[90m ${uploadResponse.data.message || 'Complete.'}\x1b[0m\n`);
|
|
779
|
+
} else {
|
|
780
|
+
throw new Error(uploadResponse.data?.error || 'Unknown logical error from Factory');
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
} catch (error) {
|
|
784
|
+
console.log(`\n\x1b[31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m`);
|
|
785
|
+
console.log(`\x1b[31m✗ PROPULSION FAILED\x1b[0m`);
|
|
786
|
+
console.log(`\x1b[31m━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\x1b[0m`);
|
|
787
|
+
|
|
788
|
+
console.error(`\x1b[31m Error:\x1b[0m ${error.message}`);
|
|
789
|
+
|
|
790
|
+
if (error.message.includes('Limit') || error.message.includes('Quota')) {
|
|
791
|
+
console.log(`\n\x1b[33m 💡 Tip:\x1b[0m Upgrade to Senior Pilot for more builds.`);
|
|
792
|
+
} else if (error.message.includes('No native module')) {
|
|
793
|
+
console.log(`\n\x1b[33m 💡 Tip:\x1b[0m Run "npm run fleetbo alex" to create native modules first.`);
|
|
794
|
+
} else if (error.message.includes('Trial Period Ended')) {
|
|
795
|
+
console.log(`\n\x1b[33m 💡 Tip:\x1b[0m Your free sprint is over. Upgrade to Senior Pilot on fleetbo.io.`);
|
|
173
796
|
}
|
|
174
|
-
|
|
175
|
-
|
|
797
|
+
console.log('');
|
|
798
|
+
process.exit(1);
|
|
176
799
|
}
|
|
177
800
|
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
801
|
+
})(); // 🟢 FIN DE LA PROTECTION
|
|
802
|
+
}
|
|
803
|
+
// ============================================
|
|
804
|
+
// COMMAND: page / g / generate
|
|
805
|
+
// ============================================
|
|
806
|
+
else if (['page', 'g', 'generate'].includes(command)) {
|
|
807
|
+
const pageGeneratorPath = path.join(__dirname, 'page.js');
|
|
808
|
+
try {
|
|
809
|
+
require(pageGeneratorPath);
|
|
810
|
+
} catch (e) {
|
|
811
|
+
console.error('\x1b[31m Page Generator Error:\x1b[0m', e.message);
|
|
812
|
+
process.exit(1);
|
|
182
813
|
}
|
|
183
|
-
}
|
|
184
|
-
|
|
814
|
+
}
|
|
185
815
|
// ============================================
|
|
186
|
-
// COMMAND
|
|
816
|
+
// COMMAND: (default) - Start Dev Environment
|
|
187
817
|
// ============================================
|
|
818
|
+
else {
|
|
819
|
+
const NULL_DEV = process.platform === 'win32' ? '>nul 2>&1' : '2>/dev/null';
|
|
820
|
+
|
|
821
|
+
function killProcessOnPort(port) {
|
|
822
|
+
try {
|
|
823
|
+
if (process.platform !== 'win32') {
|
|
824
|
+
const pid = execSync(`lsof -ti:${port} ${NULL_DEV}`).toString().trim();
|
|
825
|
+
if (pid) execSync(`kill -9 ${pid.split('\n').join(' ')} ${NULL_DEV}`);
|
|
826
|
+
}
|
|
827
|
+
} catch (e) {}
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
const killNetworkService = () => {
|
|
831
|
+
if (uplinkProcess) {
|
|
832
|
+
try {
|
|
833
|
+
uplinkProcess.kill('SIGINT');
|
|
834
|
+
console.log('[Fleetbo] Engine closed.');
|
|
835
|
+
} catch (e) {
|
|
836
|
+
console.error('[Fleetbo] Error closing tunnel:', e.message);
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
};
|
|
840
|
+
|
|
841
|
+
let isExiting = false;
|
|
842
|
+
|
|
843
|
+
async function cleanupAndExit(code = 0) {
|
|
844
|
+
if (isExiting) return;
|
|
845
|
+
isExiting = true;
|
|
846
|
+
console.log('\n\x1b[33m[Fleetbo] 🛑 Stopping environment & Cleaning Uplink...\x1b[0m');
|
|
847
|
+
try {
|
|
848
|
+
await axios.post(UPDATE_NETWORK_URL, { keyApp, networkUrl: '', tester: testerEmail });
|
|
849
|
+
console.log('\x1b[32m[Fleetbo] ✓ Network status reset to offline.\x1b[0m');
|
|
850
|
+
} catch (e) {
|
|
851
|
+
console.error('[Fleetbo] Network cleanup warning:', e.message);
|
|
852
|
+
}
|
|
853
|
+
killNetworkService();
|
|
854
|
+
killProcessOnPort(PORT);
|
|
855
|
+
console.log('[Fleetbo] Bye.');
|
|
856
|
+
process.exit(code);
|
|
857
|
+
}
|
|
858
|
+
|
|
859
|
+
process.on('SIGINT', () => cleanupAndExit(0));
|
|
860
|
+
process.on('SIGTERM', () => cleanupAndExit(0));
|
|
861
|
+
|
|
862
|
+
async function syncFirebase(keyApp, networkUrl, testerEmail) {
|
|
863
|
+
try {
|
|
864
|
+
await axios.post(UPDATE_NETWORK_URL, { keyApp, networkUrl, tester: testerEmail });
|
|
865
|
+
console.log('\n\x1b[32mEngine started successfully\x1b[0m');
|
|
866
|
+
console.log(`\x1b[32mFleetbo OS ❯\x1b[0m -------------------------------------------------------------`);
|
|
867
|
+
console.log('\x1b[32mFleetbo OS ❯\x1b[0m \x1b[1mGO GO GO ! OS IS READY\x1b[0m');
|
|
868
|
+
console.log('\x1b[32mFleetbo OS ❯\x1b[0m You can now start coding and previewing. 🚀');
|
|
869
|
+
console.log(`\x1b[32mFleetbo OS ❯\x1b[0m -------------------------------------------------------------`);
|
|
870
|
+
console.log(`\x1b[34mPilot Instruction ❯\x1b[0m Return to the Workspace. The Engine is ready for your orders.\n`);
|
|
871
|
+
} catch (err) {
|
|
872
|
+
console.error(`\x1b[31mFleetbo OS ❯\x1b[0m Sync Error: ${err.message}`);
|
|
873
|
+
}
|
|
874
|
+
}
|
|
875
|
+
|
|
876
|
+
async function runDevEnvironment() {
|
|
877
|
+
console.log(`[Fleetbo] 🛡️ Initializing Dev Environment...`);
|
|
878
|
+
|
|
879
|
+
// Mise à jour silencieuse de browserslist
|
|
880
|
+
try {
|
|
881
|
+
const npxExec = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
882
|
+
execSync(`${npxExec} -y update-browserslist-db@latest`, { stdio: 'ignore' });
|
|
883
|
+
} catch (e) {}
|
|
884
|
+
|
|
885
|
+
killNetworkService();
|
|
886
|
+
killProcessOnPort(PORT);
|
|
887
|
+
|
|
888
|
+
if (!testerEmail) {
|
|
889
|
+
console.error('\x1b[31mError: REACT_APP_TESTER_EMAIL missing in .env\x1b[0m');
|
|
890
|
+
process.exit(1);
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
const npmCmd = process.platform === 'win32' ? 'npm.cmd' : 'npm';
|
|
894
|
+
const devServer = spawn(npmCmd, ['start'], {
|
|
895
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
896
|
+
shell: true,
|
|
897
|
+
env: {
|
|
898
|
+
...process.env,
|
|
899
|
+
NODE_OPTIONS: '--no-deprecation',
|
|
900
|
+
BROWSER: 'none',
|
|
901
|
+
PORT: PORT.toString(),
|
|
902
|
+
DANGEROUSLY_DISABLE_HOST_CHECK: 'true',
|
|
903
|
+
HOST: '0.0.0.0',
|
|
904
|
+
WDS_SOCKET_HOST: 'localhost',
|
|
905
|
+
WDS_SOCKET_PORT: PORT.toString()
|
|
906
|
+
}
|
|
907
|
+
});
|
|
188
908
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
runAlexEngine();
|
|
192
|
-
break;
|
|
909
|
+
//devServer.stdout.pipe(process.stdout);
|
|
910
|
+
devServer.stderr.pipe(process.stderr);
|
|
193
911
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
912
|
+
let connectionStarted = false;
|
|
913
|
+
|
|
914
|
+
devServer.stdout.on('data', (data) => {
|
|
915
|
+
const output = data.toString();
|
|
916
|
+
|
|
917
|
+
// 🛡️ FILTRE ANTI-PLOMBERIE FLEETBO
|
|
918
|
+
const lines = output.split('\n');
|
|
919
|
+
const forbiddenTerms = [
|
|
920
|
+
'Attempting to bind to HOST',
|
|
921
|
+
'If this was unintentional',
|
|
922
|
+
'Learn more here:',
|
|
923
|
+
'Starting the development server',
|
|
924
|
+
'You can now view',
|
|
925
|
+
'Local:',
|
|
926
|
+
'On Your Network:',
|
|
927
|
+
'Note that the development build',
|
|
928
|
+
'To create a production build',
|
|
929
|
+
'webpack compiled successfully'
|
|
930
|
+
];
|
|
931
|
+
|
|
932
|
+
// On filtre les lignes pour ne garder que le vrai code/debug
|
|
933
|
+
const filteredOutput = lines.filter(line => {
|
|
934
|
+
return !forbiddenTerms.some(term => line.includes(term));
|
|
935
|
+
}).join('\n');
|
|
936
|
+
|
|
937
|
+
// S'il reste quelque chose d'utile (un console.log du dev, un warning, une vraie erreur), on l'affiche
|
|
938
|
+
if (filteredOutput.trim() !== '') {
|
|
939
|
+
process.stdout.write(filteredOutput + '\n');
|
|
940
|
+
}
|
|
941
|
+
|
|
942
|
+
// 🚀 DÉTECTION DU DÉMARRAGE ET LANCEMENT DE L'UPLINK
|
|
943
|
+
if (!connectionStarted && (output.includes('Local:') || output.includes('Compiled successfully'))) {
|
|
944
|
+
connectionStarted = true;
|
|
945
|
+
|
|
946
|
+
console.log('\x1b[33mFleetbo OS ❯\x1b[0m ---------------------------------------------------');
|
|
947
|
+
console.log(`\x1b[33mFleetbo OS ❯\x1b[0m 🔗 Establishing Secure Uplink...`);
|
|
948
|
+
console.log(`\x1b[33mFleetbo OS ❯\x1b[0m ⏳ Please wait for the green message...`);
|
|
949
|
+
console.log('\x1b[33mFleetbo OS ❯\x1b[0m ---------------------------------------------------');
|
|
950
|
+
|
|
951
|
+
// ============================================
|
|
952
|
+
// UPLINK avec auto-retry (Fleetbo OS Resilience)
|
|
953
|
+
// ============================================
|
|
954
|
+
const MAX_UPLINK_RETRIES = 5;
|
|
955
|
+
const RETRY_DELAYS = [0, 10, 20, 30, 45];
|
|
956
|
+
let uplinkFound = false;
|
|
957
|
+
|
|
958
|
+
const startUplink = (attempt) => {
|
|
959
|
+
if (uplinkFound) return;
|
|
960
|
+
|
|
961
|
+
const npxCmd = process.platform === 'win32' ? 'npx.cmd' : 'npx';
|
|
962
|
+
|
|
963
|
+
if (attempt > 0) {
|
|
964
|
+
console.log(`\x1b[33m[Fleetbo] 🔄 Uplink reconnection ${attempt}/${MAX_UPLINK_RETRIES - 1}...\x1b[0m`);
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
uplinkProcess = spawn(npxCmd, [
|
|
968
|
+
'-y',
|
|
969
|
+
'cloudflared',
|
|
970
|
+
'tunnel',
|
|
971
|
+
'--url', `http://127.0.0.1:${PORT}`,
|
|
972
|
+
'--http-host-header', `127.0.0.1:${PORT}`
|
|
973
|
+
], { shell: true });
|
|
974
|
+
|
|
975
|
+
const handleUplinkOutput = (chunk) => {
|
|
976
|
+
const text = chunk.toString();
|
|
977
|
+
if (uplinkFound) return;
|
|
978
|
+
const match = text.match(/https:\/\/[a-zA-Z0-9-]+\.trycloudflare\.com/);
|
|
979
|
+
if (match) {
|
|
980
|
+
uplinkFound = true;
|
|
981
|
+
// ⚡ Stabilisation du noyau : on attend 1.5s
|
|
982
|
+
setTimeout(() => {
|
|
983
|
+
syncFirebase(process.env.REACT_KEY_APP, match[0], process.env.REACT_APP_TESTER_EMAIL);
|
|
984
|
+
}, 1500);
|
|
985
|
+
}
|
|
986
|
+
};
|
|
987
|
+
|
|
988
|
+
uplinkProcess.stdout.on('data', handleUplinkOutput);
|
|
989
|
+
uplinkProcess.stderr.on('data', handleUplinkOutput);
|
|
990
|
+
|
|
991
|
+
uplinkProcess.on('error', (err) => {
|
|
992
|
+
if (uplinkFound) return;
|
|
993
|
+
console.error(`\x1b[31m[Fleetbo] ⚠️ Uplink Connection failed to establish.\x1b[0m`);
|
|
994
|
+
});
|
|
198
995
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
996
|
+
uplinkProcess.on('close', (code) => {
|
|
997
|
+
if (uplinkFound) return;
|
|
998
|
+
|
|
999
|
+
const nextAttempt = attempt + 1;
|
|
1000
|
+
if (nextAttempt < MAX_UPLINK_RETRIES) {
|
|
1001
|
+
const delay = RETRY_DELAYS[nextAttempt] || 30;
|
|
1002
|
+
console.log(`\x1b[33m[Fleetbo] ⚠️ Uplink interrupted. Fleetbo OS retrying in ${delay}s... (${nextAttempt}/${MAX_UPLINK_RETRIES - 1})\x1b[0m`);
|
|
1003
|
+
setTimeout(() => startUplink(nextAttempt), delay * 1000);
|
|
1004
|
+
} else {
|
|
1005
|
+
console.error(`\x1b[31m[Fleetbo] ❌ Secure Uplink could not be established.\x1b[0m`);
|
|
1006
|
+
console.error(`\x1b[90m[Fleetbo] Fleetbo OS network is temporarily unavailable.\x1b[0m`);
|
|
1007
|
+
console.error(`\x1b[90m[Fleetbo] Your dev server is still running on http://localhost:${PORT}\x1b[0m`);
|
|
1008
|
+
console.error(`\x1b[90m[Fleetbo] Restart with "npm run fleetbo" when the network is back.\x1b[0m`);
|
|
1009
|
+
}
|
|
1010
|
+
});
|
|
1011
|
+
};
|
|
1012
|
+
|
|
1013
|
+
startUplink(0);
|
|
1014
|
+
}
|
|
1015
|
+
});
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
runDevEnvironment();
|
|
203
1019
|
}
|