bro-framework 2.4.5 → 3.0.1
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 +44 -0
- package/bin/bro.js +279 -292
- package/package.json +138 -103
- package/src/dashboard.js +81 -0
- package/src/database.js +105 -0
- package/src/edge.js +244 -0
- package/src/engine.js +251 -0
- package/src/index.d.ts +46 -9
- package/src/index.js +10 -0
- package/src/logger.js +47 -0
- package/src/next.d.ts +51 -10
- package/src/next.js +82 -156
- package/src/observability.js +81 -0
- package/src/plugins.js +135 -0
- package/src/policy-auth.js +88 -0
- package/src/router.js +1 -1
- package/src/sdk.js +229 -169
- package/src/server.js +181 -170
- package/src/studio.js +162 -0
- package/src/task-engine.js +88 -0
- package/src/testing.js +142 -0
- package/src/uploads.js +129 -0
package/bin/bro.js
CHANGED
|
@@ -1,293 +1,280 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
|
|
3
|
-
import path from 'path';
|
|
4
|
-
import fs from 'fs';
|
|
5
|
-
import { pathToFileURL } from 'url';
|
|
6
|
-
import { register } from 'tsx/esm/api';
|
|
7
|
-
|
|
8
|
-
register();
|
|
9
|
-
|
|
10
|
-
import { createServer } from '../src/server.js';
|
|
11
|
-
import { colors, printBanner, printRoute, printHotReload } from '../src/logger.js';
|
|
12
|
-
import { generateSDK } from '../src/sdk.js';
|
|
13
|
-
import dotenv from 'dotenv';
|
|
14
|
-
import chokidar from 'chokidar';
|
|
15
|
-
|
|
16
|
-
dotenv.config();
|
|
17
|
-
|
|
18
|
-
const command = process.argv[2] || 'dev';
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
};
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (command === 'dev' || command === 'start') {
|
|
284
|
-
bootstrap();
|
|
285
|
-
} else if (!['sdk', 'generate-client', 'client', 'init'].includes(command)) {
|
|
286
|
-
console.log(`\n ${colors.bold}${colors.green}bro.js CLI${colors.reset}\n`);
|
|
287
|
-
console.log(` ${colors.bold}Usage:${colors.reset} bro <command>\n`);
|
|
288
|
-
console.log(` ${colors.bold}Commands:${colors.reset}`);
|
|
289
|
-
console.log(` ${colors.cyan}dev${colors.reset} Start the development server with hot-reload`);
|
|
290
|
-
console.log(` ${colors.cyan}start${colors.reset} Start the production server gracefully`);
|
|
291
|
-
console.log(` ${colors.cyan}init${colors.reset} Scaffold a new bro.config.js workspace`);
|
|
292
|
-
console.log(` ${colors.cyan}sdk${colors.reset} Generate a typed frontend client\n`);
|
|
293
|
-
}
|
|
2
|
+
|
|
3
|
+
import path from 'path';
|
|
4
|
+
import fs from 'fs';
|
|
5
|
+
import { pathToFileURL } from 'url';
|
|
6
|
+
import { register } from 'tsx/esm/api';
|
|
7
|
+
|
|
8
|
+
register();
|
|
9
|
+
|
|
10
|
+
import { createServer } from '../src/server.js';
|
|
11
|
+
import { colors, printBanner, printRoute, printHotReload } from '../src/logger.js';
|
|
12
|
+
import { generateSDK } from '../src/sdk.js';
|
|
13
|
+
import dotenv from 'dotenv';
|
|
14
|
+
import chokidar from 'chokidar';
|
|
15
|
+
|
|
16
|
+
dotenv.config();
|
|
17
|
+
|
|
18
|
+
const command = process.argv[2] || 'dev';
|
|
19
|
+
|
|
20
|
+
async function bootstrap() {
|
|
21
|
+
const startTime = performance.now();
|
|
22
|
+
const configPath = path.resolve(process.cwd(), 'bro.config.js');
|
|
23
|
+
let globalConfig = {};
|
|
24
|
+
|
|
25
|
+
if (fs.existsSync(configPath)) {
|
|
26
|
+
try {
|
|
27
|
+
const configModule = await import(pathToFileURL(configPath).href);
|
|
28
|
+
globalConfig = configModule.default || configModule;
|
|
29
|
+
} catch (err) {
|
|
30
|
+
console.error('[bro.js] Error loading bro.config.js:', err);
|
|
31
|
+
process.exit(1);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const routesDir = globalConfig.routesDir || path.resolve(process.cwd(), 'routes');
|
|
36
|
+
const localeDir = globalConfig.locale?.directory || path.resolve(process.cwd(), 'locale');
|
|
37
|
+
const tasksDir = globalConfig.tasksDir || path.resolve(process.cwd(), 'tasks');
|
|
38
|
+
const port = globalConfig.server?.port || process.env.PORT || 3000;
|
|
39
|
+
|
|
40
|
+
let db = null;
|
|
41
|
+
// Initialize db from globalConfig if provided (mocked here for CLI boot)
|
|
42
|
+
if (globalConfig.db && typeof globalConfig.db === 'function') {
|
|
43
|
+
db = await globalConfig.db();
|
|
44
|
+
} else {
|
|
45
|
+
db = globalConfig.db;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const { app, server, routes, reload, reloadLocale, shutdown } = await createServer(globalConfig, routesDir, db);
|
|
49
|
+
let currentRoutes = routes;
|
|
50
|
+
|
|
51
|
+
server.listen(port, () => {
|
|
52
|
+
if (command === 'dev') {
|
|
53
|
+
console.clear();
|
|
54
|
+
printBanner(port, performance.now() - startTime);
|
|
55
|
+
|
|
56
|
+
if (process.argv.includes('--ui')) {
|
|
57
|
+
import('../src/dashboard.js').then(({ startDashboard }) => {
|
|
58
|
+
startDashboard(globalConfig, currentRoutes);
|
|
59
|
+
}).catch(err => console.error('[bro.js] Error starting dashboard:', err));
|
|
60
|
+
}
|
|
61
|
+
} else if (command === 'start') {
|
|
62
|
+
console.log(`[bro.js] Server running in production on port ${port}`);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (command === 'dev') {
|
|
66
|
+
const printCurrentRoutes = (routesToPrint) => {
|
|
67
|
+
if (routesToPrint.length > 0) {
|
|
68
|
+
routesToPrint.forEach((r, i) => {
|
|
69
|
+
printRoute(r.method, r.path, r.auth, i === routesToPrint.length - 1);
|
|
70
|
+
});
|
|
71
|
+
console.log("");
|
|
72
|
+
} else {
|
|
73
|
+
console.log(" No routes found.\n");
|
|
74
|
+
}
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
printCurrentRoutes(currentRoutes);
|
|
78
|
+
|
|
79
|
+
const localeGlob = localeDir.replace(/\\/g, '/') + '/*.{js,mjs,ts,json}';
|
|
80
|
+
const watcher = chokidar.watch([routesDir, localeGlob, tasksDir], { ignoreInitial: true });
|
|
81
|
+
|
|
82
|
+
watcher.on('all', async (event, filepath) => {
|
|
83
|
+
const isValidFile = filepath.match(/\.(js|ts|mjs|json)$/);
|
|
84
|
+
if (!isValidFile) return;
|
|
85
|
+
const relLocale = path.relative(path.resolve(localeDir), filepath);
|
|
86
|
+
const isLocaleFile = !relLocale.startsWith('..') && !path.isAbsolute(relLocale);
|
|
87
|
+
|
|
88
|
+
const relTask = path.relative(path.resolve(tasksDir), filepath);
|
|
89
|
+
const isTaskFile = !relTask.startsWith('..') && !path.isAbsolute(relTask);
|
|
90
|
+
|
|
91
|
+
try {
|
|
92
|
+
const reloadStartTime = performance.now();
|
|
93
|
+
if (isLocaleFile) {
|
|
94
|
+
await reloadLocale();
|
|
95
|
+
} else if (isTaskFile) {
|
|
96
|
+
// Tasks reload
|
|
97
|
+
} else {
|
|
98
|
+
currentRoutes = await reload();
|
|
99
|
+
}
|
|
100
|
+
const reloadTimeMs = performance.now() - reloadStartTime;
|
|
101
|
+
|
|
102
|
+
const fileType = isTaskFile ? 'Task' : (isLocaleFile ? 'Locale' : 'Route');
|
|
103
|
+
printHotReload(path.basename(filepath), event, reloadTimeMs, fileType);
|
|
104
|
+
printCurrentRoutes(currentRoutes);
|
|
105
|
+
} catch (err) {
|
|
106
|
+
const fileType = isTaskFile ? 'tasks' : (isLocaleFile ? 'locale' : 'routes');
|
|
107
|
+
console.error(`\n ✗ Error hot-reloading ${fileType}:`, err);
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const handleShutdown = async (signal) => {
|
|
113
|
+
console.log(`\n[bro.js] Received ${signal}. Shutting down gracefully...`);
|
|
114
|
+
if (shutdown) await shutdown();
|
|
115
|
+
console.log('[bro.js] HTTP server closed.');
|
|
116
|
+
process.exit(0);
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
process.on('SIGINT', () => handleShutdown('SIGINT'));
|
|
120
|
+
process.on('SIGTERM', () => handleShutdown('SIGTERM'));
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
if (command === 'test') {
|
|
126
|
+
const hasVitest = fs.existsSync(path.resolve(process.cwd(), 'node_modules', 'vitest'));
|
|
127
|
+
if (!hasVitest) {
|
|
128
|
+
console.error(colors.red + 'Vitest is not installed. Please run: npm install -D vitest' + colors.reset);
|
|
129
|
+
process.exit(1);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
import('child_process').then(cp => {
|
|
133
|
+
console.log('\x1b[36m[bro.js]\x1b[0m Starting tests via vitest...');
|
|
134
|
+
cp.spawn('npx', ['vitest', ...process.argv.slice(3)], { stdio: 'inherit' });
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
} else if (command === 'init') {
|
|
138
|
+
const configPath = path.resolve(process.cwd(), 'bro.config.js');
|
|
139
|
+
const envPath = path.resolve(process.cwd(), '.env.example');
|
|
140
|
+
if (!fs.existsSync(configPath)) {
|
|
141
|
+
fs.writeFileSync(configPath, `import { defineConfig } from 'bro-framework';
|
|
142
|
+
|
|
143
|
+
export default defineConfig({
|
|
144
|
+
// Server Settings
|
|
145
|
+
server: {
|
|
146
|
+
port: 5000,
|
|
147
|
+
cors: process.env.NODE_ENV === 'production' ? ['https://yourdomain.com'] : true, // Set to true to allow all, or pass a CORS options object
|
|
148
|
+
helmet: true // Enable security headers
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
// Authentication Settings
|
|
152
|
+
auth: {
|
|
153
|
+
jwtSecret: process.env.JWT_SECRET || 'dev_secret_please_change', // Must be at least 32 characters in production
|
|
154
|
+
expiresIn: '7d',
|
|
155
|
+
//apiKey: process.env.API_KEY || ['dev_key_1', 'dev_key_2'] // Supports array for zero-downtime rotation
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
// Trust reverse proxy IP headers (Nginx/Cloudflare)
|
|
159
|
+
trustProxy: true,
|
|
160
|
+
|
|
161
|
+
// Observability & Telemetry
|
|
162
|
+
observability: {
|
|
163
|
+
// Output structured JSON logs with request IDs and execution timing (ideal for CloudWatch/Datadog)
|
|
164
|
+
// Options: 'json' | 'pretty' (default: 'pretty' in dev, 'json' in production)
|
|
165
|
+
logging: 'json',
|
|
166
|
+
|
|
167
|
+
// Enable OpenTelemetry W3C trace propagation and HTTP span generation
|
|
168
|
+
openTelemetry: true
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// Optional file-based API translations
|
|
172
|
+
// Add locale/en.js, locale/fr.js, etc.
|
|
173
|
+
locale: {
|
|
174
|
+
defaultLocale: 'en'
|
|
175
|
+
},
|
|
176
|
+
|
|
177
|
+
// API Documentation (Scalar UI)
|
|
178
|
+
docs: process.env.NODE_ENV !== 'production', // Set to false to disable completely, or true to force in prod
|
|
179
|
+
|
|
180
|
+
// Rate Limiting
|
|
181
|
+
rateLimit: {
|
|
182
|
+
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
183
|
+
max: 100 // limit each IP to 100 requests per windowMs
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
// Redis Configuration (Auto-scales WebSockets, distributed caches & rate-limiting)
|
|
187
|
+
redisUrl: process.env.REDIS_URL, // e.g., 'redis://localhost:6379'
|
|
188
|
+
|
|
189
|
+
// WebSockets Setup
|
|
190
|
+
sockets: async (io, db) => {
|
|
191
|
+
io.on('connection', (socket) => {
|
|
192
|
+
console.log('Client connected:', socket.id);
|
|
193
|
+
});
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
// Database Context Injection
|
|
197
|
+
// This instance will be injected into every route's ctx.db (if defined)
|
|
198
|
+
db: async () => {
|
|
199
|
+
// If you use a database, set up your connection here
|
|
200
|
+
// and return the connection instance or an object of your models.
|
|
201
|
+
// Could be MongoDB, MySQL, etc. (your choice)
|
|
202
|
+
// --- MONGOOSE EXAMPLE ---
|
|
203
|
+
// import mongoose from 'mongoose';
|
|
204
|
+
|
|
205
|
+
// await mongoose.connect(process.env.MONGO_URI || 'mongodb://localhost:27017/bro_database');
|
|
206
|
+
// console.log("Connected to MongoDB");
|
|
207
|
+
|
|
208
|
+
// You can return mongoose itself, or an object of your models
|
|
209
|
+
// to access them instantly in your routes without importing them!
|
|
210
|
+
// Example: return { User, Post };
|
|
211
|
+
|
|
212
|
+
// return mongoose.connection;
|
|
213
|
+
// --------------------------
|
|
214
|
+
return null;
|
|
215
|
+
},
|
|
216
|
+
|
|
217
|
+
// Graceful Teardown Hook
|
|
218
|
+
onShutdown: async (db) => {
|
|
219
|
+
// Close application-owned database resources gracefully here
|
|
220
|
+
}
|
|
221
|
+
});
|
|
222
|
+
`);
|
|
223
|
+
console.log('[bro.js] Created bro.config.js');
|
|
224
|
+
}
|
|
225
|
+
if (!fs.existsSync(envPath)) {
|
|
226
|
+
fs.writeFileSync(envPath, 'JWT_SECRET=your_jwt_secret_here\nNODE_ENV=development\nREDIS_URL=redis://localhost:6379\n');
|
|
227
|
+
console.log('[bro.js] Created .env.example');
|
|
228
|
+
}
|
|
229
|
+
} else if (command === 'doctor') {
|
|
230
|
+
console.log('[bro.js] Running doctor...');
|
|
231
|
+
const configPath = path.resolve(process.cwd(), 'bro.config.js');
|
|
232
|
+
if (!fs.existsSync(configPath)) {
|
|
233
|
+
console.error('✗ No bro.config.js found.');
|
|
234
|
+
} else {
|
|
235
|
+
import(pathToFileURL(configPath).href).then(m => {
|
|
236
|
+
const config = m.default || m;
|
|
237
|
+
if (config.server?.cors === true) console.error('✗ Permissive CORS is enabled (cors: true). Use an array of allowed origins.');
|
|
238
|
+
else console.log('✓ CORS is strict.');
|
|
239
|
+
|
|
240
|
+
if (['dev_secret_please_change', 'bro_default_secret_key', 'your_jwt_secret_here', ''].includes(config.auth?.jwtSecret?.trim())) {
|
|
241
|
+
console.error('✗ Hardcoded insecure JWT secret detected.');
|
|
242
|
+
} else {
|
|
243
|
+
console.log('✓ Secrets look ok.');
|
|
244
|
+
}
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
} else if (command === 'sdk' || command === 'client' || command === 'generate-client') {
|
|
248
|
+
const sdkOutPath = process.argv[3] || './client.ts';
|
|
249
|
+
const configPath = path.resolve(process.cwd(), 'bro.config.js');
|
|
250
|
+
let globalConfig = {};
|
|
251
|
+
if (fs.existsSync(configPath)) {
|
|
252
|
+
try {
|
|
253
|
+
const configModule = await import(configPath);
|
|
254
|
+
globalConfig = configModule.default || configModule;
|
|
255
|
+
} catch (e) {}
|
|
256
|
+
}
|
|
257
|
+
const routesDir = globalConfig.routesDir || path.resolve(process.cwd(), 'routes');
|
|
258
|
+
generateSDK(routesDir, sdkOutPath).then(() => {
|
|
259
|
+
console.log(`\x1b[32m✓ SDK successfully generated at ${sdkOutPath}\x1b[0m`);
|
|
260
|
+
}).catch(err => {
|
|
261
|
+
console.error('\x1b[31m✗ Failed to generate SDK:\x1b[0m', err);
|
|
262
|
+
});
|
|
263
|
+
} else if (command === 'studio') {
|
|
264
|
+
import('../src/studio.js').then(({ startStudio }) => {
|
|
265
|
+
startStudio(process.cwd());
|
|
266
|
+
}).catch(err => console.error('[bro.js] Error starting studio:', err));
|
|
267
|
+
} else if (command === 'dev' || command === 'start') {
|
|
268
|
+
bootstrap();
|
|
269
|
+
} else if (!['init', 'doctor'].includes(command)) {
|
|
270
|
+
console.log(`\n ${colors.bold}${colors.green}bro.js CLI${colors.reset}\n`);
|
|
271
|
+
console.log(` ${colors.bold}Usage:${colors.reset} bro <command>\n`);
|
|
272
|
+
console.log(` ${colors.bold}Commands:${colors.reset}`);
|
|
273
|
+
console.log(` ${colors.cyan}dev${colors.reset} Start the development server with hot-reload`);
|
|
274
|
+
console.log(` ${colors.cyan}start${colors.reset} Start the production server gracefully`);
|
|
275
|
+
console.log(` ${colors.cyan}init${colors.reset} Scaffold a new bro.config.js workspace`);
|
|
276
|
+
console.log(` ${colors.cyan}sdk${colors.reset} Generate a typed frontend client`);
|
|
277
|
+
console.log(` ${colors.cyan}studio${colors.reset} Generate TS client, OpenAPI, and MSW mocks`);
|
|
278
|
+
console.log(` ${colors.cyan}doctor${colors.reset} Diagnose security and configuration issues`);
|
|
279
|
+
console.log(` ${colors.cyan}test${colors.reset} Run tests using vitest\n`);
|
|
280
|
+
}
|