@buenojs/bueno 0.8.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/.env.example +109 -0
- package/.github/workflows/ci.yml +31 -0
- package/LICENSE +21 -0
- package/README.md +892 -0
- package/architecture.md +652 -0
- package/bun.lock +70 -0
- package/dist/cli/index.js +3233 -0
- package/dist/index.js +9014 -0
- package/package.json +77 -0
- package/src/cache/index.ts +795 -0
- package/src/cli/ARCHITECTURE.md +837 -0
- package/src/cli/bin.ts +10 -0
- package/src/cli/commands/build.ts +425 -0
- package/src/cli/commands/dev.ts +248 -0
- package/src/cli/commands/generate.ts +541 -0
- package/src/cli/commands/help.ts +55 -0
- package/src/cli/commands/index.ts +112 -0
- package/src/cli/commands/migration.ts +355 -0
- package/src/cli/commands/new.ts +804 -0
- package/src/cli/commands/start.ts +208 -0
- package/src/cli/core/args.ts +283 -0
- package/src/cli/core/console.ts +349 -0
- package/src/cli/core/index.ts +60 -0
- package/src/cli/core/prompt.ts +424 -0
- package/src/cli/core/spinner.ts +265 -0
- package/src/cli/index.ts +135 -0
- package/src/cli/templates/deploy.ts +295 -0
- package/src/cli/templates/docker.ts +307 -0
- package/src/cli/templates/index.ts +24 -0
- package/src/cli/utils/fs.ts +428 -0
- package/src/cli/utils/index.ts +8 -0
- package/src/cli/utils/strings.ts +197 -0
- package/src/config/env.ts +408 -0
- package/src/config/index.ts +506 -0
- package/src/config/loader.ts +329 -0
- package/src/config/merge.ts +285 -0
- package/src/config/types.ts +320 -0
- package/src/config/validation.ts +441 -0
- package/src/container/forward-ref.ts +143 -0
- package/src/container/index.ts +386 -0
- package/src/context/index.ts +360 -0
- package/src/database/index.ts +1142 -0
- package/src/database/migrations/index.ts +371 -0
- package/src/database/schema/index.ts +619 -0
- package/src/frontend/api-routes.ts +640 -0
- package/src/frontend/bundler.ts +643 -0
- package/src/frontend/console-client.ts +419 -0
- package/src/frontend/console-stream.ts +587 -0
- package/src/frontend/dev-server.ts +846 -0
- package/src/frontend/file-router.ts +611 -0
- package/src/frontend/frameworks/index.ts +106 -0
- package/src/frontend/frameworks/react.ts +85 -0
- package/src/frontend/frameworks/solid.ts +104 -0
- package/src/frontend/frameworks/svelte.ts +110 -0
- package/src/frontend/frameworks/vue.ts +92 -0
- package/src/frontend/hmr-client.ts +663 -0
- package/src/frontend/hmr.ts +728 -0
- package/src/frontend/index.ts +342 -0
- package/src/frontend/islands.ts +552 -0
- package/src/frontend/isr.ts +555 -0
- package/src/frontend/layout.ts +475 -0
- package/src/frontend/ssr/react.ts +446 -0
- package/src/frontend/ssr/solid.ts +523 -0
- package/src/frontend/ssr/svelte.ts +546 -0
- package/src/frontend/ssr/vue.ts +504 -0
- package/src/frontend/ssr.ts +699 -0
- package/src/frontend/types.ts +2274 -0
- package/src/health/index.ts +604 -0
- package/src/index.ts +410 -0
- package/src/lock/index.ts +587 -0
- package/src/logger/index.ts +444 -0
- package/src/logger/transports/index.ts +969 -0
- package/src/metrics/index.ts +494 -0
- package/src/middleware/built-in.ts +360 -0
- package/src/middleware/index.ts +94 -0
- package/src/modules/filters.ts +458 -0
- package/src/modules/guards.ts +405 -0
- package/src/modules/index.ts +1256 -0
- package/src/modules/interceptors.ts +574 -0
- package/src/modules/lazy.ts +418 -0
- package/src/modules/lifecycle.ts +478 -0
- package/src/modules/metadata.ts +90 -0
- package/src/modules/pipes.ts +626 -0
- package/src/router/index.ts +339 -0
- package/src/router/linear.ts +371 -0
- package/src/router/regex.ts +292 -0
- package/src/router/tree.ts +562 -0
- package/src/rpc/index.ts +1263 -0
- package/src/security/index.ts +436 -0
- package/src/ssg/index.ts +631 -0
- package/src/storage/index.ts +456 -0
- package/src/telemetry/index.ts +1097 -0
- package/src/testing/index.ts +1586 -0
- package/src/types/index.ts +236 -0
- package/src/types/optional-deps.d.ts +219 -0
- package/src/validation/index.ts +276 -0
- package/src/websocket/index.ts +1004 -0
- package/tests/integration/cli.test.ts +1016 -0
- package/tests/integration/fullstack.test.ts +234 -0
- package/tests/unit/cache.test.ts +174 -0
- package/tests/unit/cli-commands.test.ts +892 -0
- package/tests/unit/cli.test.ts +1258 -0
- package/tests/unit/container.test.ts +279 -0
- package/tests/unit/context.test.ts +221 -0
- package/tests/unit/database.test.ts +183 -0
- package/tests/unit/linear-router.test.ts +280 -0
- package/tests/unit/lock.test.ts +336 -0
- package/tests/unit/middleware.test.ts +184 -0
- package/tests/unit/modules.test.ts +142 -0
- package/tests/unit/pubsub.test.ts +257 -0
- package/tests/unit/regex-router.test.ts +265 -0
- package/tests/unit/router.test.ts +373 -0
- package/tests/unit/rpc.test.ts +1248 -0
- package/tests/unit/security.test.ts +174 -0
- package/tests/unit/telemetry.test.ts +371 -0
- package/tests/unit/test-cache.test.ts +110 -0
- package/tests/unit/test-database.test.ts +282 -0
- package/tests/unit/tree-router.test.ts +325 -0
- package/tests/unit/validation.test.ts +794 -0
- package/tsconfig.json +27 -0
|
@@ -0,0 +1,3233 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @bun
|
|
3
|
+
var __defProp = Object.defineProperty;
|
|
4
|
+
var __export = (target, all) => {
|
|
5
|
+
for (var name in all)
|
|
6
|
+
__defProp(target, name, {
|
|
7
|
+
get: all[name],
|
|
8
|
+
enumerable: true,
|
|
9
|
+
configurable: true,
|
|
10
|
+
set: (newValue) => all[name] = () => newValue
|
|
11
|
+
});
|
|
12
|
+
};
|
|
13
|
+
var __legacyDecorateClassTS = function(decorators, target, key, desc) {
|
|
14
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
15
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function")
|
|
16
|
+
r = Reflect.decorate(decorators, target, key, desc);
|
|
17
|
+
else
|
|
18
|
+
for (var i = decorators.length - 1;i >= 0; i--)
|
|
19
|
+
if (d = decorators[i])
|
|
20
|
+
r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
21
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
22
|
+
};
|
|
23
|
+
var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
24
|
+
var __require = import.meta.require;
|
|
25
|
+
|
|
26
|
+
// src/cli/core/args.ts
|
|
27
|
+
function parseArgs(argv = process.argv.slice(2)) {
|
|
28
|
+
const result = {
|
|
29
|
+
command: "",
|
|
30
|
+
positionals: [],
|
|
31
|
+
options: {},
|
|
32
|
+
flags: new Set
|
|
33
|
+
};
|
|
34
|
+
for (let i = 0;i < argv.length; i++) {
|
|
35
|
+
const arg = argv[i];
|
|
36
|
+
if (!arg)
|
|
37
|
+
continue;
|
|
38
|
+
if (arg.startsWith("--")) {
|
|
39
|
+
const eqIndex = arg.indexOf("=");
|
|
40
|
+
if (eqIndex !== -1) {
|
|
41
|
+
const name = arg.slice(2, eqIndex);
|
|
42
|
+
const value = arg.slice(eqIndex + 1);
|
|
43
|
+
result.options[name] = value;
|
|
44
|
+
} else {
|
|
45
|
+
const name = arg.slice(2);
|
|
46
|
+
const nextArg = argv[i + 1];
|
|
47
|
+
if (!nextArg || nextArg.startsWith("-")) {
|
|
48
|
+
result.options[name] = true;
|
|
49
|
+
result.flags.add(name);
|
|
50
|
+
} else {
|
|
51
|
+
result.options[name] = nextArg;
|
|
52
|
+
i++;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} else if (arg.startsWith("-") && arg.length > 1) {
|
|
56
|
+
const chars = arg.slice(1);
|
|
57
|
+
if (chars.length > 1) {
|
|
58
|
+
for (const char of chars) {
|
|
59
|
+
result.options[char] = true;
|
|
60
|
+
result.flags.add(char);
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
const name = chars;
|
|
64
|
+
const nextArg = argv[i + 1];
|
|
65
|
+
if (!nextArg || nextArg.startsWith("-")) {
|
|
66
|
+
result.options[name] = true;
|
|
67
|
+
result.flags.add(name);
|
|
68
|
+
} else {
|
|
69
|
+
result.options[name] = nextArg;
|
|
70
|
+
i++;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
} else {
|
|
74
|
+
if (!result.command) {
|
|
75
|
+
result.command = arg;
|
|
76
|
+
} else {
|
|
77
|
+
result.positionals.push(arg);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
function getOption(parsed, name, definition) {
|
|
84
|
+
const value = parsed.options[name] ?? parsed.options[definition.alias ?? ""];
|
|
85
|
+
if (value === undefined) {
|
|
86
|
+
return definition.default;
|
|
87
|
+
}
|
|
88
|
+
if (definition.type === "boolean") {
|
|
89
|
+
return value === true || value === "true";
|
|
90
|
+
}
|
|
91
|
+
if (definition.type === "number") {
|
|
92
|
+
return typeof value === "number" ? value : typeof value === "string" ? parseInt(value, 10) : NaN;
|
|
93
|
+
}
|
|
94
|
+
return value;
|
|
95
|
+
}
|
|
96
|
+
function hasFlag(parsed, name, alias) {
|
|
97
|
+
return parsed.flags.has(name) || (alias ? parsed.flags.has(alias) : false);
|
|
98
|
+
}
|
|
99
|
+
function getOptionValues(parsed, name, alias) {
|
|
100
|
+
const values = [];
|
|
101
|
+
const argv = process.argv.slice(2);
|
|
102
|
+
for (let i = 0;i < argv.length; i++) {
|
|
103
|
+
const arg = argv[i];
|
|
104
|
+
if (!arg)
|
|
105
|
+
continue;
|
|
106
|
+
if (arg === `--${name}`) {
|
|
107
|
+
const nextArg = argv[i + 1];
|
|
108
|
+
if (nextArg && !nextArg.startsWith("-")) {
|
|
109
|
+
values.push(nextArg);
|
|
110
|
+
i++;
|
|
111
|
+
}
|
|
112
|
+
} else if (arg.startsWith(`--${name}=`)) {
|
|
113
|
+
const value = arg.slice(name.length + 3);
|
|
114
|
+
values.push(value);
|
|
115
|
+
} else if (alias && arg === `-${alias}`) {
|
|
116
|
+
const nextArg = argv[i + 1];
|
|
117
|
+
if (nextArg && !nextArg.startsWith("-")) {
|
|
118
|
+
values.push(nextArg);
|
|
119
|
+
i++;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
return values;
|
|
124
|
+
}
|
|
125
|
+
function generateHelpText(command, cliName = "bueno") {
|
|
126
|
+
const lines = [];
|
|
127
|
+
lines.push(`
|
|
128
|
+
${command.description}
|
|
129
|
+
`);
|
|
130
|
+
lines.push("Usage:");
|
|
131
|
+
let usage = ` ${cliName} ${command.name}`;
|
|
132
|
+
if (command.positionals) {
|
|
133
|
+
for (const pos of command.positionals) {
|
|
134
|
+
usage += pos.required ? ` <${pos.name}>` : ` [${pos.name}]`;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
usage += " [options]";
|
|
138
|
+
lines.push(usage + `
|
|
139
|
+
`);
|
|
140
|
+
if (command.positionals && command.positionals.length > 0) {
|
|
141
|
+
lines.push("Arguments:");
|
|
142
|
+
for (const pos of command.positionals) {
|
|
143
|
+
const required = pos.required ? " (required)" : "";
|
|
144
|
+
lines.push(` ${pos.name.padEnd(20)} ${pos.description}${required}`);
|
|
145
|
+
}
|
|
146
|
+
lines.push("");
|
|
147
|
+
}
|
|
148
|
+
if (command.options && command.options.length > 0) {
|
|
149
|
+
lines.push("Options:");
|
|
150
|
+
for (const opt of command.options) {
|
|
151
|
+
let flag = `--${opt.name}`;
|
|
152
|
+
if (opt.alias) {
|
|
153
|
+
flag = `-${opt.alias}, ${flag}`;
|
|
154
|
+
}
|
|
155
|
+
let defaultValue = "";
|
|
156
|
+
if (opt.default !== undefined) {
|
|
157
|
+
defaultValue = ` (default: ${opt.default})`;
|
|
158
|
+
}
|
|
159
|
+
lines.push(` ${flag.padEnd(20)} ${opt.description}${defaultValue}`);
|
|
160
|
+
}
|
|
161
|
+
lines.push("");
|
|
162
|
+
}
|
|
163
|
+
if (command.examples && command.examples.length > 0) {
|
|
164
|
+
lines.push("Examples:");
|
|
165
|
+
for (const example of command.examples) {
|
|
166
|
+
lines.push(` ${example}`);
|
|
167
|
+
}
|
|
168
|
+
lines.push("");
|
|
169
|
+
}
|
|
170
|
+
return lines.join(`
|
|
171
|
+
`);
|
|
172
|
+
}
|
|
173
|
+
function generateGlobalHelpText(commands, cliName = "bueno") {
|
|
174
|
+
const lines = [];
|
|
175
|
+
lines.push(`
|
|
176
|
+
${cliName} - A Bun-Native Full-Stack Framework CLI
|
|
177
|
+
`);
|
|
178
|
+
lines.push("Usage:");
|
|
179
|
+
lines.push(` ${cliName} <command> [options]
|
|
180
|
+
`);
|
|
181
|
+
lines.push("Commands:");
|
|
182
|
+
for (const cmd of commands) {
|
|
183
|
+
const name = cmd.alias ? `${cmd.name} (${cmd.alias})` : cmd.name;
|
|
184
|
+
lines.push(` ${name.padEnd(20)} ${cmd.description}`);
|
|
185
|
+
}
|
|
186
|
+
lines.push("");
|
|
187
|
+
lines.push("Global Options:");
|
|
188
|
+
lines.push(" --help, -h Show help for command");
|
|
189
|
+
lines.push(" --version, -v Show CLI version");
|
|
190
|
+
lines.push(" --verbose Enable verbose output");
|
|
191
|
+
lines.push(" --quiet Suppress non-essential output");
|
|
192
|
+
lines.push(" --no-color Disable colored output");
|
|
193
|
+
lines.push("");
|
|
194
|
+
lines.push(`Run '${cliName} <command> --help' for more information about a command.
|
|
195
|
+
`);
|
|
196
|
+
return lines.join(`
|
|
197
|
+
`);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// src/cli/core/console.ts
|
|
201
|
+
var COLORS = {
|
|
202
|
+
reset: "\x1B[0m",
|
|
203
|
+
bold: "\x1B[1m",
|
|
204
|
+
dim: "\x1B[2m",
|
|
205
|
+
italic: "\x1B[3m",
|
|
206
|
+
underline: "\x1B[4m",
|
|
207
|
+
black: "\x1B[30m",
|
|
208
|
+
red: "\x1B[31m",
|
|
209
|
+
green: "\x1B[32m",
|
|
210
|
+
yellow: "\x1B[33m",
|
|
211
|
+
blue: "\x1B[34m",
|
|
212
|
+
magenta: "\x1B[35m",
|
|
213
|
+
cyan: "\x1B[36m",
|
|
214
|
+
white: "\x1B[37m",
|
|
215
|
+
brightRed: "\x1B[91m",
|
|
216
|
+
brightGreen: "\x1B[92m",
|
|
217
|
+
brightYellow: "\x1B[93m",
|
|
218
|
+
brightBlue: "\x1B[94m",
|
|
219
|
+
brightMagenta: "\x1B[95m",
|
|
220
|
+
brightCyan: "\x1B[96m",
|
|
221
|
+
brightWhite: "\x1B[97m",
|
|
222
|
+
bgBlack: "\x1B[40m",
|
|
223
|
+
bgRed: "\x1B[41m",
|
|
224
|
+
bgGreen: "\x1B[42m",
|
|
225
|
+
bgYellow: "\x1B[43m",
|
|
226
|
+
bgBlue: "\x1B[44m",
|
|
227
|
+
bgMagenta: "\x1B[45m",
|
|
228
|
+
bgCyan: "\x1B[46m",
|
|
229
|
+
bgWhite: "\x1B[47m"
|
|
230
|
+
};
|
|
231
|
+
var colorEnabled = !process.env.NO_COLOR && process.env.BUENO_NO_COLOR !== "true" && process.stdout.isTTY;
|
|
232
|
+
function setColorEnabled(enabled) {
|
|
233
|
+
colorEnabled = enabled;
|
|
234
|
+
}
|
|
235
|
+
function isColorEnabled() {
|
|
236
|
+
return colorEnabled;
|
|
237
|
+
}
|
|
238
|
+
function colorize(text, color) {
|
|
239
|
+
if (!colorEnabled)
|
|
240
|
+
return text;
|
|
241
|
+
return `${COLORS[color]}${text}${COLORS.reset}`;
|
|
242
|
+
}
|
|
243
|
+
var colors = {
|
|
244
|
+
red: (text) => colorize(text, "red"),
|
|
245
|
+
green: (text) => colorize(text, "green"),
|
|
246
|
+
yellow: (text) => colorize(text, "yellow"),
|
|
247
|
+
blue: (text) => colorize(text, "blue"),
|
|
248
|
+
magenta: (text) => colorize(text, "magenta"),
|
|
249
|
+
cyan: (text) => colorize(text, "cyan"),
|
|
250
|
+
white: (text) => colorize(text, "white"),
|
|
251
|
+
brightRed: (text) => colorize(text, "brightRed"),
|
|
252
|
+
brightGreen: (text) => colorize(text, "brightGreen"),
|
|
253
|
+
brightYellow: (text) => colorize(text, "brightYellow"),
|
|
254
|
+
brightBlue: (text) => colorize(text, "brightBlue"),
|
|
255
|
+
brightCyan: (text) => colorize(text, "brightCyan"),
|
|
256
|
+
dim: (text) => colorize(text, "dim"),
|
|
257
|
+
bold: (text) => colorize(text, "bold"),
|
|
258
|
+
underline: (text) => colorize(text, "underline"),
|
|
259
|
+
italic: (text) => colorize(text, "italic")
|
|
260
|
+
};
|
|
261
|
+
var cliConsole = {
|
|
262
|
+
log(message, ...args) {
|
|
263
|
+
globalThis.console.log(message, ...args);
|
|
264
|
+
},
|
|
265
|
+
info(message, ...args) {
|
|
266
|
+
globalThis.console.log(colors.cyan("\u2139"), message, ...args);
|
|
267
|
+
},
|
|
268
|
+
success(message, ...args) {
|
|
269
|
+
globalThis.console.log(colors.green("\u2713"), message, ...args);
|
|
270
|
+
},
|
|
271
|
+
warn(message, ...args) {
|
|
272
|
+
globalThis.console.log(colors.yellow("\u26A0"), message, ...args);
|
|
273
|
+
},
|
|
274
|
+
error(message, ...args) {
|
|
275
|
+
globalThis.console.error(colors.red("\u2717"), message, ...args);
|
|
276
|
+
},
|
|
277
|
+
debug(message, ...args) {
|
|
278
|
+
if (process.env.BUENO_VERBOSE === "true") {
|
|
279
|
+
globalThis.console.log(colors.dim("\u22EF"), colors.dim(message), ...args);
|
|
280
|
+
}
|
|
281
|
+
},
|
|
282
|
+
header(title) {
|
|
283
|
+
globalThis.console.log();
|
|
284
|
+
globalThis.console.log(colors.bold(colors.cyan(title)));
|
|
285
|
+
globalThis.console.log();
|
|
286
|
+
},
|
|
287
|
+
subheader(title) {
|
|
288
|
+
globalThis.console.log();
|
|
289
|
+
globalThis.console.log(colors.bold(title));
|
|
290
|
+
},
|
|
291
|
+
newline() {
|
|
292
|
+
globalThis.console.log();
|
|
293
|
+
},
|
|
294
|
+
clear() {
|
|
295
|
+
process.stdout.write("\x1B[2J\x1B[0f");
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
function formatTable(headers, rows, options = {}) {
|
|
299
|
+
const padding = options.padding ?? 2;
|
|
300
|
+
const widths = headers.map((h, i) => {
|
|
301
|
+
const maxRowWidth = Math.max(...rows.map((r) => r[i]?.length ?? 0));
|
|
302
|
+
return Math.max(h.length, maxRowWidth);
|
|
303
|
+
});
|
|
304
|
+
const pad = " ".repeat(padding);
|
|
305
|
+
const headerLine = headers.map((h, i) => h.padEnd(widths[i] ?? 0)).join(pad);
|
|
306
|
+
const separator = widths.map((w) => "\u2500".repeat(w)).join(pad);
|
|
307
|
+
const rowLines = rows.map((row) => row.map((cell, i) => (cell ?? "").padEnd(widths[i] ?? 0)).join(pad));
|
|
308
|
+
return [
|
|
309
|
+
colors.bold(headerLine),
|
|
310
|
+
colors.dim(separator),
|
|
311
|
+
...rowLines
|
|
312
|
+
].join(`
|
|
313
|
+
`);
|
|
314
|
+
}
|
|
315
|
+
function printTable(headers, rows, options) {
|
|
316
|
+
globalThis.console.log(formatTable(headers, rows, options));
|
|
317
|
+
}
|
|
318
|
+
function formatSize(bytes) {
|
|
319
|
+
const units = ["B", "KB", "MB", "GB"];
|
|
320
|
+
let size = bytes;
|
|
321
|
+
let unitIndex = 0;
|
|
322
|
+
while (size >= 1024 && unitIndex < units.length - 1) {
|
|
323
|
+
size /= 1024;
|
|
324
|
+
unitIndex++;
|
|
325
|
+
}
|
|
326
|
+
return `${size.toFixed(1)} ${units[unitIndex]}`;
|
|
327
|
+
}
|
|
328
|
+
function formatDuration(ms) {
|
|
329
|
+
if (ms < 1000)
|
|
330
|
+
return `${ms}ms`;
|
|
331
|
+
if (ms < 60000)
|
|
332
|
+
return `${(ms / 1000).toFixed(1)}s`;
|
|
333
|
+
return `${(ms / 60000).toFixed(1)}m`;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// src/cli/commands/index.ts
|
|
337
|
+
class CommandRegistry {
|
|
338
|
+
commands = new Map;
|
|
339
|
+
aliases = new Map;
|
|
340
|
+
register(definition, handler) {
|
|
341
|
+
this.commands.set(definition.name, {
|
|
342
|
+
definition,
|
|
343
|
+
handler
|
|
344
|
+
});
|
|
345
|
+
if (definition.alias) {
|
|
346
|
+
this.aliases.set(definition.alias, definition.name);
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
get(name) {
|
|
350
|
+
const commandName = this.aliases.get(name) ?? name;
|
|
351
|
+
return this.commands.get(commandName);
|
|
352
|
+
}
|
|
353
|
+
has(name) {
|
|
354
|
+
const commandName = this.aliases.get(name) ?? name;
|
|
355
|
+
return this.commands.has(commandName);
|
|
356
|
+
}
|
|
357
|
+
getAll() {
|
|
358
|
+
return Array.from(this.commands.values()).map((c) => c.definition);
|
|
359
|
+
}
|
|
360
|
+
getCommands() {
|
|
361
|
+
return new Map(this.commands);
|
|
362
|
+
}
|
|
363
|
+
async execute(name, args) {
|
|
364
|
+
const command = this.get(name);
|
|
365
|
+
if (!command) {
|
|
366
|
+
throw new Error(`Unknown command: ${name}`);
|
|
367
|
+
}
|
|
368
|
+
await command.handler(args);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
var registry = new CommandRegistry;
|
|
372
|
+
function defineCommand(definition, handler) {
|
|
373
|
+
registry.register(definition, handler);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// src/cli/core/prompt.ts
|
|
377
|
+
import * as readline from "readline";
|
|
378
|
+
function isInteractive() {
|
|
379
|
+
return !!(process.stdin.isTTY && process.stdout.isTTY);
|
|
380
|
+
}
|
|
381
|
+
function createRL() {
|
|
382
|
+
return readline.createInterface({
|
|
383
|
+
input: process.stdin,
|
|
384
|
+
output: process.stdout
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
async function prompt(message, options = {}) {
|
|
388
|
+
const defaultValue = options.default;
|
|
389
|
+
const promptText = defaultValue ? `${colors.cyan("?")} ${message} ${colors.dim(`(${defaultValue})`)}: ` : `${colors.cyan("?")} ${message}: `;
|
|
390
|
+
if (!isInteractive()) {
|
|
391
|
+
return defaultValue ?? "";
|
|
392
|
+
}
|
|
393
|
+
return new Promise((resolve) => {
|
|
394
|
+
const rl = createRL();
|
|
395
|
+
rl.question(promptText, (answer) => {
|
|
396
|
+
rl.close();
|
|
397
|
+
const value = answer.trim() || defaultValue || "";
|
|
398
|
+
if (options.validate) {
|
|
399
|
+
const result = options.validate(value);
|
|
400
|
+
if (result !== true) {
|
|
401
|
+
const errorMsg = typeof result === "string" ? result : "Invalid value";
|
|
402
|
+
process.stdout.write(`${colors.red("\u2717")} ${errorMsg}
|
|
403
|
+
`);
|
|
404
|
+
prompt(message, options).then(resolve);
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
resolve(value);
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
async function confirm(message, options = {}) {
|
|
413
|
+
const defaultValue = options.default ?? false;
|
|
414
|
+
const hint = defaultValue ? "Y/n" : "y/N";
|
|
415
|
+
if (!isInteractive()) {
|
|
416
|
+
return defaultValue;
|
|
417
|
+
}
|
|
418
|
+
const answer = await prompt(`${message} ${colors.dim(`(${hint})`)}`, {
|
|
419
|
+
default: defaultValue ? "y" : "n",
|
|
420
|
+
validate: (value) => {
|
|
421
|
+
if (!value)
|
|
422
|
+
return true;
|
|
423
|
+
return ["y", "yes", "n", "no"].includes(value.toLowerCase()) || "Please enter y or n";
|
|
424
|
+
}
|
|
425
|
+
});
|
|
426
|
+
return ["y", "yes"].includes(answer.toLowerCase());
|
|
427
|
+
}
|
|
428
|
+
async function select(message, choices, options = {}) {
|
|
429
|
+
if (!isInteractive()) {
|
|
430
|
+
return options.default ?? choices[0]?.value;
|
|
431
|
+
}
|
|
432
|
+
const pageSize = options.pageSize ?? 10;
|
|
433
|
+
let selectedIndex = choices.findIndex((c) => c.value === options.default && !c.disabled);
|
|
434
|
+
if (selectedIndex === -1) {
|
|
435
|
+
selectedIndex = choices.findIndex((c) => !c.disabled);
|
|
436
|
+
}
|
|
437
|
+
return new Promise((resolve) => {
|
|
438
|
+
process.stdout.write("\x1B[?25l");
|
|
439
|
+
const render = () => {
|
|
440
|
+
const lines = Math.min(choices.length, pageSize);
|
|
441
|
+
process.stdout.write(`\x1B[${lines + 1}A\x1B[0J`);
|
|
442
|
+
process.stdout.write(`${colors.cyan("?")} ${message}
|
|
443
|
+
`);
|
|
444
|
+
const start = Math.max(0, selectedIndex - pageSize + 1);
|
|
445
|
+
const end = Math.min(choices.length, start + pageSize);
|
|
446
|
+
for (let i = start;i < end; i++) {
|
|
447
|
+
const choice = choices[i];
|
|
448
|
+
if (!choice)
|
|
449
|
+
continue;
|
|
450
|
+
const isSelected = i === selectedIndex;
|
|
451
|
+
const prefix = isSelected ? `${colors.cyan("\u276F")} ` : " ";
|
|
452
|
+
const name = choice.name ?? choice.value;
|
|
453
|
+
const text = choice.disabled ? colors.dim(`${name} (disabled)`) : isSelected ? colors.cyan(name) : name;
|
|
454
|
+
process.stdout.write(`${prefix}${text}
|
|
455
|
+
`);
|
|
456
|
+
}
|
|
457
|
+
};
|
|
458
|
+
process.stdout.write(`${colors.cyan("?")} ${message}
|
|
459
|
+
`);
|
|
460
|
+
render();
|
|
461
|
+
const stdin = process.stdin;
|
|
462
|
+
stdin.setRawMode(true);
|
|
463
|
+
stdin.resume();
|
|
464
|
+
stdin.setEncoding("utf8");
|
|
465
|
+
const cleanup = () => {
|
|
466
|
+
stdin.setRawMode(false);
|
|
467
|
+
stdin.pause();
|
|
468
|
+
stdin.removeListener("data", handler);
|
|
469
|
+
process.stdout.write("\x1B[?25h");
|
|
470
|
+
};
|
|
471
|
+
const handler = (key) => {
|
|
472
|
+
if (key === "\x1B[A" || key === "k") {
|
|
473
|
+
do {
|
|
474
|
+
selectedIndex = (selectedIndex - 1 + choices.length) % choices.length;
|
|
475
|
+
} while (choices[selectedIndex]?.disabled);
|
|
476
|
+
render();
|
|
477
|
+
} else if (key === "\x1B[B" || key === "j") {
|
|
478
|
+
do {
|
|
479
|
+
selectedIndex = (selectedIndex + 1) % choices.length;
|
|
480
|
+
} while (choices[selectedIndex]?.disabled);
|
|
481
|
+
render();
|
|
482
|
+
} else if (key === "\r" || key === `
|
|
483
|
+
`) {
|
|
484
|
+
cleanup();
|
|
485
|
+
const selected = choices[selectedIndex];
|
|
486
|
+
if (selected) {
|
|
487
|
+
process.stdout.write(`\x1B[${Math.min(choices.length, pageSize) + 1}A\x1B[0J`);
|
|
488
|
+
process.stdout.write(`${colors.cyan("?")} ${message} ${colors.cyan(selected.name ?? selected.value)}
|
|
489
|
+
`);
|
|
490
|
+
resolve(selected.value);
|
|
491
|
+
}
|
|
492
|
+
} else if (key === "\x1B" || key === "\x03") {
|
|
493
|
+
cleanup();
|
|
494
|
+
process.stdout.write(`
|
|
495
|
+
`);
|
|
496
|
+
process.exit(130);
|
|
497
|
+
}
|
|
498
|
+
};
|
|
499
|
+
stdin.on("data", handler);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
// src/cli/core/spinner.ts
|
|
504
|
+
var SPINNER_FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
|
|
505
|
+
var SPINNER_INTERVAL = 80;
|
|
506
|
+
|
|
507
|
+
class Spinner {
|
|
508
|
+
text;
|
|
509
|
+
color;
|
|
510
|
+
frameIndex = 0;
|
|
511
|
+
interval = null;
|
|
512
|
+
isSpinning = false;
|
|
513
|
+
stream = process.stdout;
|
|
514
|
+
constructor(options = {}) {
|
|
515
|
+
this.text = options.text ?? "";
|
|
516
|
+
this.color = options.color ?? "cyan";
|
|
517
|
+
}
|
|
518
|
+
start(text) {
|
|
519
|
+
if (text)
|
|
520
|
+
this.text = text;
|
|
521
|
+
if (this.isSpinning)
|
|
522
|
+
return this;
|
|
523
|
+
this.isSpinning = true;
|
|
524
|
+
this.frameIndex = 0;
|
|
525
|
+
this.stream.write("\x1B[?25l");
|
|
526
|
+
this.interval = setInterval(() => {
|
|
527
|
+
this.render();
|
|
528
|
+
}, SPINNER_INTERVAL);
|
|
529
|
+
return this;
|
|
530
|
+
}
|
|
531
|
+
update(text) {
|
|
532
|
+
this.text = text;
|
|
533
|
+
if (this.isSpinning) {
|
|
534
|
+
this.render();
|
|
535
|
+
}
|
|
536
|
+
return this;
|
|
537
|
+
}
|
|
538
|
+
success(text) {
|
|
539
|
+
return this.stop(colors.green("\u2713"), text);
|
|
540
|
+
}
|
|
541
|
+
error(text) {
|
|
542
|
+
return this.stop(colors.red("\u2717"), text);
|
|
543
|
+
}
|
|
544
|
+
warn(text) {
|
|
545
|
+
return this.stop(colors.yellow("\u26A0"), text);
|
|
546
|
+
}
|
|
547
|
+
info(text) {
|
|
548
|
+
return this.stop(colors.cyan("\u2139"), text);
|
|
549
|
+
}
|
|
550
|
+
stop(symbol, text) {
|
|
551
|
+
if (!this.isSpinning)
|
|
552
|
+
return this;
|
|
553
|
+
this.isSpinning = false;
|
|
554
|
+
if (this.interval) {
|
|
555
|
+
clearInterval(this.interval);
|
|
556
|
+
this.interval = null;
|
|
557
|
+
}
|
|
558
|
+
this.stream.write("\r\x1B[K");
|
|
559
|
+
const finalText = text ?? this.text;
|
|
560
|
+
if (symbol) {
|
|
561
|
+
this.stream.write(`${symbol} ${finalText}
|
|
562
|
+
`);
|
|
563
|
+
} else {
|
|
564
|
+
this.stream.write(`${finalText}
|
|
565
|
+
`);
|
|
566
|
+
}
|
|
567
|
+
this.stream.write("\x1B[?25h");
|
|
568
|
+
return this;
|
|
569
|
+
}
|
|
570
|
+
clear() {
|
|
571
|
+
if (!this.isSpinning)
|
|
572
|
+
return this;
|
|
573
|
+
this.stream.write("\r\x1B[K");
|
|
574
|
+
return this;
|
|
575
|
+
}
|
|
576
|
+
render() {
|
|
577
|
+
if (!isColorEnabled()) {
|
|
578
|
+
const dots = ".".repeat(this.frameIndex % 3 + 1);
|
|
579
|
+
this.stream.write(`\r${this.text}${dots} `);
|
|
580
|
+
this.frameIndex++;
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
const frame = SPINNER_FRAMES[this.frameIndex % SPINNER_FRAMES.length];
|
|
584
|
+
const coloredFrame = colors[this.color](frame);
|
|
585
|
+
this.stream.write(`\r${coloredFrame} ${this.text}`);
|
|
586
|
+
this.frameIndex++;
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
function spinner(text, options) {
|
|
590
|
+
return new Spinner({ text, ...options }).start();
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
class ProgressBar {
|
|
594
|
+
total;
|
|
595
|
+
width;
|
|
596
|
+
text;
|
|
597
|
+
completeChar;
|
|
598
|
+
incompleteChar;
|
|
599
|
+
current = 0;
|
|
600
|
+
stream = process.stdout;
|
|
601
|
+
constructor(options) {
|
|
602
|
+
this.total = options.total;
|
|
603
|
+
this.width = options.width ?? 40;
|
|
604
|
+
this.text = options.text ?? "";
|
|
605
|
+
this.completeChar = options.completeChar ?? "\u2588";
|
|
606
|
+
this.incompleteChar = options.incompleteChar ?? "\u2591";
|
|
607
|
+
}
|
|
608
|
+
start() {
|
|
609
|
+
this.current = 0;
|
|
610
|
+
this.render();
|
|
611
|
+
return this;
|
|
612
|
+
}
|
|
613
|
+
update(current) {
|
|
614
|
+
this.current = Math.min(current, this.total);
|
|
615
|
+
this.render();
|
|
616
|
+
return this;
|
|
617
|
+
}
|
|
618
|
+
increment(amount = 1) {
|
|
619
|
+
return this.update(this.current + amount);
|
|
620
|
+
}
|
|
621
|
+
complete() {
|
|
622
|
+
this.current = this.total;
|
|
623
|
+
this.render();
|
|
624
|
+
this.stream.write(`
|
|
625
|
+
`);
|
|
626
|
+
return this;
|
|
627
|
+
}
|
|
628
|
+
render() {
|
|
629
|
+
const percent = this.current / this.total;
|
|
630
|
+
const completeWidth = Math.round(this.width * percent);
|
|
631
|
+
const incompleteWidth = this.width - completeWidth;
|
|
632
|
+
const complete = this.completeChar.repeat(completeWidth);
|
|
633
|
+
const incomplete = this.incompleteChar.repeat(incompleteWidth);
|
|
634
|
+
const bar = colors.green(complete) + colors.dim(incomplete);
|
|
635
|
+
const percentText = `${Math.round(percent * 100)}%`.padStart(4);
|
|
636
|
+
const line = `\r${this.text} [${bar}] ${percentText} ${this.current}/${this.total}`;
|
|
637
|
+
this.stream.write(`\r\x1B[K${line}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
async function runTasks(tasks) {
|
|
641
|
+
for (const task of tasks) {
|
|
642
|
+
const s = spinner(task.text);
|
|
643
|
+
try {
|
|
644
|
+
await task.task();
|
|
645
|
+
s.success();
|
|
646
|
+
} catch (error) {
|
|
647
|
+
s.error();
|
|
648
|
+
throw error;
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
// src/cli/utils/fs.ts
|
|
654
|
+
import * as fs from "fs";
|
|
655
|
+
import * as path from "path";
|
|
656
|
+
async function fileExists(filePath) {
|
|
657
|
+
try {
|
|
658
|
+
return await Bun.file(filePath).exists();
|
|
659
|
+
} catch {
|
|
660
|
+
return false;
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
async function createDirectory(dirPath) {
|
|
664
|
+
await fs.promises.mkdir(dirPath, { recursive: true });
|
|
665
|
+
}
|
|
666
|
+
async function readFile(filePath) {
|
|
667
|
+
return await Bun.file(filePath).text();
|
|
668
|
+
}
|
|
669
|
+
async function writeFile(filePath, content) {
|
|
670
|
+
const dir = path.dirname(filePath);
|
|
671
|
+
await createDirectory(dir);
|
|
672
|
+
await Bun.write(filePath, content);
|
|
673
|
+
}
|
|
674
|
+
async function deleteDirectory(dirPath) {
|
|
675
|
+
await fs.promises.rm(dirPath, { recursive: true, force: true });
|
|
676
|
+
}
|
|
677
|
+
async function listFiles(dirPath, options = {}) {
|
|
678
|
+
const files = [];
|
|
679
|
+
async function walk(dir) {
|
|
680
|
+
const entries = await fs.promises.readdir(dir, { withFileTypes: true });
|
|
681
|
+
for (const entry of entries) {
|
|
682
|
+
const fullPath = path.join(dir, entry.name);
|
|
683
|
+
if (entry.isDirectory() && options.recursive) {
|
|
684
|
+
await walk(fullPath);
|
|
685
|
+
} else if (entry.isFile()) {
|
|
686
|
+
if (!options.pattern || options.pattern.test(entry.name)) {
|
|
687
|
+
files.push(fullPath);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
await walk(dirPath);
|
|
693
|
+
return files;
|
|
694
|
+
}
|
|
695
|
+
async function findFileUp(startDir, fileName, options = {}) {
|
|
696
|
+
let currentDir = startDir;
|
|
697
|
+
const stopAt = options.stopAt ?? "/";
|
|
698
|
+
while (currentDir !== stopAt && currentDir !== "/") {
|
|
699
|
+
const filePath = path.join(currentDir, fileName);
|
|
700
|
+
if (await fileExists(filePath)) {
|
|
701
|
+
return filePath;
|
|
702
|
+
}
|
|
703
|
+
currentDir = path.dirname(currentDir);
|
|
704
|
+
}
|
|
705
|
+
return null;
|
|
706
|
+
}
|
|
707
|
+
async function getProjectRoot(startDir = process.cwd()) {
|
|
708
|
+
const packageJsonPath = await findFileUp(startDir, "package.json");
|
|
709
|
+
if (packageJsonPath) {
|
|
710
|
+
return path.dirname(packageJsonPath);
|
|
711
|
+
}
|
|
712
|
+
return null;
|
|
713
|
+
}
|
|
714
|
+
async function isBuenoProject(dir = process.cwd()) {
|
|
715
|
+
const root = await getProjectRoot(dir);
|
|
716
|
+
if (!root)
|
|
717
|
+
return false;
|
|
718
|
+
const configPath = path.join(root, "bueno.config.ts");
|
|
719
|
+
if (await fileExists(configPath))
|
|
720
|
+
return true;
|
|
721
|
+
const packageJsonPath = path.join(root, "package.json");
|
|
722
|
+
if (await fileExists(packageJsonPath)) {
|
|
723
|
+
const content = await readFile(packageJsonPath);
|
|
724
|
+
try {
|
|
725
|
+
const pkg = JSON.parse(content);
|
|
726
|
+
return !!(pkg.dependencies?.bueno || pkg.devDependencies?.bueno);
|
|
727
|
+
} catch {
|
|
728
|
+
return false;
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
return false;
|
|
732
|
+
}
|
|
733
|
+
function joinPaths(...paths) {
|
|
734
|
+
return path.join(...paths);
|
|
735
|
+
}
|
|
736
|
+
function processTemplate(template, data) {
|
|
737
|
+
let result = template;
|
|
738
|
+
result = result.replace(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (_, key, content) => {
|
|
739
|
+
const value = data[key];
|
|
740
|
+
return value ? content : "";
|
|
741
|
+
});
|
|
742
|
+
result = result.replace(/\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (_, key, content) => {
|
|
743
|
+
const items = data[key];
|
|
744
|
+
if (!Array.isArray(items))
|
|
745
|
+
return "";
|
|
746
|
+
return items.map((item) => {
|
|
747
|
+
let itemContent = content;
|
|
748
|
+
if (typeof item === "object" && item !== null) {
|
|
749
|
+
for (const [k, v] of Object.entries(item)) {
|
|
750
|
+
itemContent = itemContent.replace(new RegExp(`\\{\\{${k}\\}\\}`, "g"), String(v));
|
|
751
|
+
}
|
|
752
|
+
}
|
|
753
|
+
return itemContent;
|
|
754
|
+
}).join("");
|
|
755
|
+
});
|
|
756
|
+
const helpers = {
|
|
757
|
+
camelCase: (v) => v.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (c) => c.toLowerCase()),
|
|
758
|
+
pascalCase: (v) => v.replace(/[-_\s]+(.)?/g, (_, c) => c ? c.toUpperCase() : "").replace(/^(.)/, (c) => c.toUpperCase()),
|
|
759
|
+
kebabCase: (v) => v.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[-_\s]+/g, "-").toLowerCase(),
|
|
760
|
+
snakeCase: (v) => v.replace(/([a-z])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase(),
|
|
761
|
+
upperCase: (v) => v.toUpperCase(),
|
|
762
|
+
lowerCase: (v) => v.toLowerCase(),
|
|
763
|
+
capitalize: (v) => v.charAt(0).toUpperCase() + v.slice(1),
|
|
764
|
+
pluralize: (v) => {
|
|
765
|
+
if (v.endsWith("y") && !["ay", "ey", "iy", "oy", "uy"].some((e) => v.endsWith(e))) {
|
|
766
|
+
return v.slice(0, -1) + "ies";
|
|
767
|
+
}
|
|
768
|
+
if (v.endsWith("s") || v.endsWith("x") || v.endsWith("z") || v.endsWith("ch") || v.endsWith("sh")) {
|
|
769
|
+
return v + "es";
|
|
770
|
+
}
|
|
771
|
+
return v + "s";
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
for (const [helperName, helperFn] of Object.entries(helpers)) {
|
|
775
|
+
const regex = new RegExp(`\\{\\{${helperName}\\s+(\\w+)\\}\\}`, "g");
|
|
776
|
+
result = result.replace(regex, (_, key) => {
|
|
777
|
+
const value = data[key];
|
|
778
|
+
if (typeof value === "string") {
|
|
779
|
+
return helperFn(value);
|
|
780
|
+
}
|
|
781
|
+
return String(value);
|
|
782
|
+
});
|
|
783
|
+
}
|
|
784
|
+
for (const [key, value] of Object.entries(data)) {
|
|
785
|
+
const regex = new RegExp(`\\{\\{${key}\\}\\}`, "g");
|
|
786
|
+
result = result.replace(regex, String(value));
|
|
787
|
+
}
|
|
788
|
+
result = result.replace(/^\s*\n/gm, `
|
|
789
|
+
`);
|
|
790
|
+
result = result.replace(/\n{3,}/g, `
|
|
791
|
+
|
|
792
|
+
`);
|
|
793
|
+
return result.trim();
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// src/cli/utils/strings.ts
|
|
797
|
+
function kebabCase(str) {
|
|
798
|
+
return str.replace(/([a-z])([A-Z])/g, "$1-$2").replace(/[-_\s]+/g, "-").toLowerCase();
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
// src/cli/templates/docker.ts
|
|
802
|
+
function getDockerfileTemplate(projectName, database) {
|
|
803
|
+
return `# ${projectName} - Production Dockerfile
|
|
804
|
+
# Multi-stage build for optimized production image
|
|
805
|
+
|
|
806
|
+
# Stage 1: Install dependencies
|
|
807
|
+
FROM oven/bun:1 AS deps
|
|
808
|
+
|
|
809
|
+
WORKDIR /app
|
|
810
|
+
|
|
811
|
+
# Copy package files first for better layer caching
|
|
812
|
+
COPY package.json bun.lock* ./
|
|
813
|
+
|
|
814
|
+
# Install dependencies
|
|
815
|
+
RUN bun install --frozen-lockfile --production
|
|
816
|
+
|
|
817
|
+
# Stage 2: Build the application
|
|
818
|
+
FROM oven/bun:1 AS builder
|
|
819
|
+
|
|
820
|
+
WORKDIR /app
|
|
821
|
+
|
|
822
|
+
# Copy package files
|
|
823
|
+
COPY package.json bun.lock* ./
|
|
824
|
+
|
|
825
|
+
# Install all dependencies (including devDependencies for build)
|
|
826
|
+
RUN bun install --frozen-lockfile
|
|
827
|
+
|
|
828
|
+
# Copy source code
|
|
829
|
+
COPY . .
|
|
830
|
+
|
|
831
|
+
# Build the application
|
|
832
|
+
RUN bun run build
|
|
833
|
+
|
|
834
|
+
# Stage 3: Production image
|
|
835
|
+
FROM oven/bun:1 AS runner
|
|
836
|
+
|
|
837
|
+
WORKDIR /app
|
|
838
|
+
|
|
839
|
+
# Set production environment
|
|
840
|
+
ENV NODE_ENV=production
|
|
841
|
+
ENV BUN_ENV=production
|
|
842
|
+
|
|
843
|
+
# Create non-root user for security
|
|
844
|
+
RUN addgroup --system --gid 1001 bunjs \\
|
|
845
|
+
&& adduser --system --uid 1001 --ingroup bunjs bunuser
|
|
846
|
+
|
|
847
|
+
# Copy built application from builder
|
|
848
|
+
COPY --from=builder /app/dist ./dist
|
|
849
|
+
COPY --from=builder /app/node_modules ./node_modules
|
|
850
|
+
COPY --from=builder /app/package.json ./
|
|
851
|
+
|
|
852
|
+
# Copy config files if they exist
|
|
853
|
+
COPY --from=builder /app/bueno.config.ts* ./
|
|
854
|
+
|
|
855
|
+
# Set proper ownership
|
|
856
|
+
RUN chown -R bunuser:bunjs /app
|
|
857
|
+
|
|
858
|
+
# Switch to non-root user
|
|
859
|
+
USER bunuser
|
|
860
|
+
|
|
861
|
+
# Expose the application port
|
|
862
|
+
EXPOSE 3000
|
|
863
|
+
|
|
864
|
+
# Health check
|
|
865
|
+
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \\
|
|
866
|
+
CMD curl -f http://localhost:3000/health || exit 1
|
|
867
|
+
|
|
868
|
+
# Start the application
|
|
869
|
+
CMD ["bun", "run", "dist/main.js"]
|
|
870
|
+
`;
|
|
871
|
+
}
|
|
872
|
+
function getDockerignoreTemplate() {
|
|
873
|
+
return `# Dependencies
|
|
874
|
+
node_modules/
|
|
875
|
+
|
|
876
|
+
# Build output
|
|
877
|
+
dist/
|
|
878
|
+
|
|
879
|
+
# Environment files
|
|
880
|
+
.env
|
|
881
|
+
.env.local
|
|
882
|
+
.env.*.local
|
|
883
|
+
|
|
884
|
+
# IDE
|
|
885
|
+
.idea/
|
|
886
|
+
.vscode/
|
|
887
|
+
*.swp
|
|
888
|
+
*.swo
|
|
889
|
+
|
|
890
|
+
# OS
|
|
891
|
+
.DS_Store
|
|
892
|
+
Thumbs.db
|
|
893
|
+
|
|
894
|
+
# Git
|
|
895
|
+
.git/
|
|
896
|
+
.gitignore
|
|
897
|
+
|
|
898
|
+
# Docker
|
|
899
|
+
Dockerfile
|
|
900
|
+
docker-compose*.yml
|
|
901
|
+
.dockerignore
|
|
902
|
+
|
|
903
|
+
# Test files
|
|
904
|
+
tests/
|
|
905
|
+
coverage/
|
|
906
|
+
*.test.ts
|
|
907
|
+
*.spec.ts
|
|
908
|
+
|
|
909
|
+
# Documentation
|
|
910
|
+
*.md
|
|
911
|
+
!README.md
|
|
912
|
+
|
|
913
|
+
# Database files (local)
|
|
914
|
+
*.db
|
|
915
|
+
*.sqlite
|
|
916
|
+
*.sqlite3
|
|
917
|
+
|
|
918
|
+
# Logs
|
|
919
|
+
*.log
|
|
920
|
+
logs/
|
|
921
|
+
|
|
922
|
+
# Misc
|
|
923
|
+
.editorconfig
|
|
924
|
+
.eslintrc*
|
|
925
|
+
.prettierrc*
|
|
926
|
+
tsconfig.json
|
|
927
|
+
`;
|
|
928
|
+
}
|
|
929
|
+
function getDockerComposeTemplate(projectName, database) {
|
|
930
|
+
const kebabName = projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
931
|
+
let databaseServices = "";
|
|
932
|
+
let dependsOn = "";
|
|
933
|
+
if (database === "postgresql") {
|
|
934
|
+
databaseServices = `
|
|
935
|
+
# PostgreSQL Database
|
|
936
|
+
postgres:
|
|
937
|
+
image: postgres:16-alpine
|
|
938
|
+
container_name: ${kebabName}-postgres
|
|
939
|
+
restart: unless-stopped
|
|
940
|
+
environment:
|
|
941
|
+
POSTGRES_USER: \${POSTGRES_USER:-postgres}
|
|
942
|
+
POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-postgres}
|
|
943
|
+
POSTGRES_DB: \${POSTGRES_DB:-${kebabName}}
|
|
944
|
+
volumes:
|
|
945
|
+
- postgres_data:/var/lib/postgresql/data
|
|
946
|
+
ports:
|
|
947
|
+
- "\${POSTGRES_PORT:-5432}:5432"
|
|
948
|
+
healthcheck:
|
|
949
|
+
test: ["CMD-SHELL", "pg_isready -U \${POSTGRES_USER:-postgres} -d \${POSTGRES_DB:-${kebabName}}"]
|
|
950
|
+
interval: 10s
|
|
951
|
+
timeout: 5s
|
|
952
|
+
retries: 5
|
|
953
|
+
networks:
|
|
954
|
+
- bueno-network
|
|
955
|
+
|
|
956
|
+
`;
|
|
957
|
+
dependsOn = `
|
|
958
|
+
depends_on:
|
|
959
|
+
postgres:
|
|
960
|
+
condition: service_healthy
|
|
961
|
+
`;
|
|
962
|
+
} else if (database === "mysql") {
|
|
963
|
+
databaseServices = `
|
|
964
|
+
# MySQL Database
|
|
965
|
+
mysql:
|
|
966
|
+
image: mysql:8.0
|
|
967
|
+
container_name: ${kebabName}-mysql
|
|
968
|
+
restart: unless-stopped
|
|
969
|
+
environment:
|
|
970
|
+
MYSQL_ROOT_PASSWORD: \${MYSQL_ROOT_PASSWORD:-root}
|
|
971
|
+
MYSQL_USER: \${MYSQL_USER:-mysql}
|
|
972
|
+
MYSQL_PASSWORD: \${MYSQL_PASSWORD:-mysql}
|
|
973
|
+
MYSQL_DATABASE: \${MYSQL_DATABASE:-${kebabName}}
|
|
974
|
+
volumes:
|
|
975
|
+
- mysql_data:/var/lib/mysql
|
|
976
|
+
ports:
|
|
977
|
+
- "\${MYSQL_PORT:-3306}:3306"
|
|
978
|
+
healthcheck:
|
|
979
|
+
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p\${MYSQL_ROOT_PASSWORD:-root}"]
|
|
980
|
+
interval: 10s
|
|
981
|
+
timeout: 5s
|
|
982
|
+
retries: 5
|
|
983
|
+
networks:
|
|
984
|
+
- bueno-network
|
|
985
|
+
|
|
986
|
+
`;
|
|
987
|
+
dependsOn = `
|
|
988
|
+
depends_on:
|
|
989
|
+
mysql:
|
|
990
|
+
condition: service_healthy
|
|
991
|
+
`;
|
|
992
|
+
}
|
|
993
|
+
const volumes = database === "postgresql" ? `
|
|
994
|
+
volumes:
|
|
995
|
+
postgres_data:
|
|
996
|
+
driver: local
|
|
997
|
+
` : database === "mysql" ? `
|
|
998
|
+
volumes:
|
|
999
|
+
mysql_data:
|
|
1000
|
+
driver: local
|
|
1001
|
+
` : "";
|
|
1002
|
+
const databaseEnv = database === "postgresql" ? ` DATABASE_URL: postgresql://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:-postgres}@postgres:5432/\${POSTGRES_DB:-${kebabName}}
|
|
1003
|
+
` : database === "mysql" ? ` DATABASE_URL: mysql://\${MYSQL_USER:-mysql}:\${MYSQL_PASSWORD:-mysql}@mysql:3306/\${MYSQL_DATABASE:-${kebabName}}
|
|
1004
|
+
` : "";
|
|
1005
|
+
return `# ${projectName} - Docker Compose for Local Development
|
|
1006
|
+
# Usage: docker-compose up -d
|
|
1007
|
+
|
|
1008
|
+
services:
|
|
1009
|
+
# Application Service
|
|
1010
|
+
app:
|
|
1011
|
+
build:
|
|
1012
|
+
context: .
|
|
1013
|
+
dockerfile: Dockerfile
|
|
1014
|
+
container_name: ${kebabName}-app
|
|
1015
|
+
restart: unless-stopped
|
|
1016
|
+
ports:
|
|
1017
|
+
- "\${APP_PORT:-3000}:3000"
|
|
1018
|
+
environment:
|
|
1019
|
+
NODE_ENV: production
|
|
1020
|
+
BUN_ENV: production
|
|
1021
|
+
${databaseEnv}${dependsOn} networks:
|
|
1022
|
+
- bueno-network
|
|
1023
|
+
healthcheck:
|
|
1024
|
+
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
|
|
1025
|
+
interval: 30s
|
|
1026
|
+
timeout: 10s
|
|
1027
|
+
retries: 3
|
|
1028
|
+
start_period: 10s
|
|
1029
|
+
${databaseServices}networks:
|
|
1030
|
+
bueno-network:
|
|
1031
|
+
driver: bridge
|
|
1032
|
+
${volumes}
|
|
1033
|
+
`;
|
|
1034
|
+
}
|
|
1035
|
+
function getDockerEnvTemplate(projectName, database) {
|
|
1036
|
+
const kebabName = projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
1037
|
+
let dbEnv = "";
|
|
1038
|
+
if (database === "postgresql") {
|
|
1039
|
+
dbEnv = `
|
|
1040
|
+
# PostgreSQL Configuration
|
|
1041
|
+
POSTGRES_USER=postgres
|
|
1042
|
+
POSTGRES_PASSWORD=postgres
|
|
1043
|
+
POSTGRES_DB=${kebabName}
|
|
1044
|
+
POSTGRES_PORT=5432
|
|
1045
|
+
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/${kebabName}
|
|
1046
|
+
`;
|
|
1047
|
+
} else if (database === "mysql") {
|
|
1048
|
+
dbEnv = `
|
|
1049
|
+
# MySQL Configuration
|
|
1050
|
+
MYSQL_ROOT_PASSWORD=root
|
|
1051
|
+
MYSQL_USER=mysql
|
|
1052
|
+
MYSQL_PASSWORD=mysql
|
|
1053
|
+
MYSQL_DATABASE=${kebabName}
|
|
1054
|
+
MYSQL_PORT=3306
|
|
1055
|
+
DATABASE_URL=mysql://mysql:mysql@localhost:3306/${kebabName}
|
|
1056
|
+
`;
|
|
1057
|
+
}
|
|
1058
|
+
return `# ${projectName} - Docker Environment Variables
|
|
1059
|
+
# Copy this file to .env and update values as needed
|
|
1060
|
+
|
|
1061
|
+
# Application
|
|
1062
|
+
APP_PORT=3000
|
|
1063
|
+
NODE_ENV=production
|
|
1064
|
+
${dbEnv}`;
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
// src/cli/templates/deploy.ts
|
|
1068
|
+
function getRenderYamlTemplate(projectName, database) {
|
|
1069
|
+
const kebabName = projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
1070
|
+
let databaseSection = "";
|
|
1071
|
+
let envVars = "";
|
|
1072
|
+
if (database === "postgresql") {
|
|
1073
|
+
databaseSection = `
|
|
1074
|
+
# PostgreSQL Database
|
|
1075
|
+
- type: pserv
|
|
1076
|
+
name: ${kebabName}-db
|
|
1077
|
+
env: docker
|
|
1078
|
+
region: oregon
|
|
1079
|
+
plan: starter
|
|
1080
|
+
envVars:
|
|
1081
|
+
- key: POSTGRES_USER
|
|
1082
|
+
generateValue: true
|
|
1083
|
+
- key: POSTGRES_PASSWORD
|
|
1084
|
+
generateValue: true
|
|
1085
|
+
- key: POSTGRES_DB
|
|
1086
|
+
value: ${kebabName}
|
|
1087
|
+
disk:
|
|
1088
|
+
name: postgres-data
|
|
1089
|
+
mountPath: /var/lib/postgresql/data
|
|
1090
|
+
sizeGB: 10
|
|
1091
|
+
|
|
1092
|
+
`;
|
|
1093
|
+
envVars = `
|
|
1094
|
+
envVars:
|
|
1095
|
+
- key: DATABASE_URL
|
|
1096
|
+
fromDatabase:
|
|
1097
|
+
name: ${kebabName}-db
|
|
1098
|
+
property: connectionString
|
|
1099
|
+
- key: NODE_ENV
|
|
1100
|
+
value: production
|
|
1101
|
+
- key: BUN_ENV
|
|
1102
|
+
value: production
|
|
1103
|
+
`;
|
|
1104
|
+
} else if (database === "mysql") {
|
|
1105
|
+
databaseSection = `
|
|
1106
|
+
# MySQL Database (using Render's managed MySQL)
|
|
1107
|
+
- type: pserv
|
|
1108
|
+
name: ${kebabName}-db
|
|
1109
|
+
env: docker
|
|
1110
|
+
region: oregon
|
|
1111
|
+
plan: starter
|
|
1112
|
+
envVars:
|
|
1113
|
+
- key: MYSQL_ROOT_PASSWORD
|
|
1114
|
+
generateValue: true
|
|
1115
|
+
- key: MYSQL_USER
|
|
1116
|
+
generateValue: true
|
|
1117
|
+
- key: MYSQL_PASSWORD
|
|
1118
|
+
generateValue: true
|
|
1119
|
+
- key: MYSQL_DATABASE
|
|
1120
|
+
value: ${kebabName}
|
|
1121
|
+
disk:
|
|
1122
|
+
name: mysql-data
|
|
1123
|
+
mountPath: /var/lib/mysql
|
|
1124
|
+
sizeGB: 10
|
|
1125
|
+
|
|
1126
|
+
`;
|
|
1127
|
+
envVars = `
|
|
1128
|
+
envVars:
|
|
1129
|
+
- key: DATABASE_URL
|
|
1130
|
+
fromDatabase:
|
|
1131
|
+
name: ${kebabName}-db
|
|
1132
|
+
property: connectionString
|
|
1133
|
+
- key: NODE_ENV
|
|
1134
|
+
value: production
|
|
1135
|
+
- key: BUN_ENV
|
|
1136
|
+
value: production
|
|
1137
|
+
`;
|
|
1138
|
+
} else {
|
|
1139
|
+
envVars = `
|
|
1140
|
+
envVars:
|
|
1141
|
+
- key: NODE_ENV
|
|
1142
|
+
value: production
|
|
1143
|
+
- key: BUN_ENV
|
|
1144
|
+
value: production
|
|
1145
|
+
`;
|
|
1146
|
+
}
|
|
1147
|
+
return `# ${projectName} - Render.com Deployment Configuration
|
|
1148
|
+
# https://render.com/docs/blueprint-spec
|
|
1149
|
+
|
|
1150
|
+
services:
|
|
1151
|
+
# Web Service
|
|
1152
|
+
- type: web
|
|
1153
|
+
name: ${kebabName}
|
|
1154
|
+
env: docker
|
|
1155
|
+
region: oregon
|
|
1156
|
+
plan: starter
|
|
1157
|
+
branch: main
|
|
1158
|
+
dockerfilePath: ./Dockerfile
|
|
1159
|
+
# dockerContext: .
|
|
1160
|
+
numInstances: 1
|
|
1161
|
+
healthCheckPath: /health
|
|
1162
|
+
${envVars} # Auto-deploy on push to main branch
|
|
1163
|
+
autoDeploy: true
|
|
1164
|
+
${databaseSection}
|
|
1165
|
+
# Blueprint metadata
|
|
1166
|
+
metadata:
|
|
1167
|
+
name: ${projectName}
|
|
1168
|
+
description: A Bueno application deployed on Render
|
|
1169
|
+
`;
|
|
1170
|
+
}
|
|
1171
|
+
function getFlyTomlTemplate(projectName) {
|
|
1172
|
+
const kebabName = projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
1173
|
+
return `# ${projectName} - Fly.io Deployment Configuration
|
|
1174
|
+
# https://fly.io/docs/reference/configuration/
|
|
1175
|
+
|
|
1176
|
+
app = "${kebabName}"
|
|
1177
|
+
primary_region = "sea"
|
|
1178
|
+
|
|
1179
|
+
[build]
|
|
1180
|
+
dockerfile = "Dockerfile"
|
|
1181
|
+
|
|
1182
|
+
[env]
|
|
1183
|
+
NODE_ENV = "production"
|
|
1184
|
+
BUN_ENV = "production"
|
|
1185
|
+
PORT = "3000"
|
|
1186
|
+
|
|
1187
|
+
[http_service]
|
|
1188
|
+
internal_port = 3000
|
|
1189
|
+
force_https = true
|
|
1190
|
+
auto_stop_machines = "stop"
|
|
1191
|
+
auto_start_machines = true
|
|
1192
|
+
min_machines_running = 0
|
|
1193
|
+
processes = ["app"]
|
|
1194
|
+
|
|
1195
|
+
[http_service.concurrency]
|
|
1196
|
+
type = "connections"
|
|
1197
|
+
hard_limit = 100
|
|
1198
|
+
soft_limit = 80
|
|
1199
|
+
|
|
1200
|
+
[[http_service.checks]]
|
|
1201
|
+
grace_period = "10s"
|
|
1202
|
+
interval = "30s"
|
|
1203
|
+
method = "GET"
|
|
1204
|
+
timeout = "5s"
|
|
1205
|
+
path = "/health"
|
|
1206
|
+
|
|
1207
|
+
[http_service.checks.headers]
|
|
1208
|
+
Content-Type = "application/json"
|
|
1209
|
+
|
|
1210
|
+
[[vm]]
|
|
1211
|
+
cpu_kind = "shared"
|
|
1212
|
+
cpus = 1
|
|
1213
|
+
memory_mb = 512
|
|
1214
|
+
|
|
1215
|
+
[[mounts]]
|
|
1216
|
+
source = "data"
|
|
1217
|
+
destination = "/app/data"
|
|
1218
|
+
initial_size = "1GB"
|
|
1219
|
+
|
|
1220
|
+
# Scale configuration
|
|
1221
|
+
# Use: fly scale count 2 # Scale to 2 machines
|
|
1222
|
+
# Use: fly scale vm shared-cpu-2x --memory 1024 # Upgrade VM
|
|
1223
|
+
|
|
1224
|
+
# Secrets (set via: fly secrets set KEY=VALUE)
|
|
1225
|
+
# DATABASE_URL=your-database-url
|
|
1226
|
+
# Any other sensitive environment variables
|
|
1227
|
+
`;
|
|
1228
|
+
}
|
|
1229
|
+
function getRailwayTomlTemplate(projectName) {
|
|
1230
|
+
const kebabName = projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-");
|
|
1231
|
+
return `# ${projectName} - Railway Deployment Configuration
|
|
1232
|
+
# https://docs.railway.app/reference/config-as-code
|
|
1233
|
+
|
|
1234
|
+
[build]
|
|
1235
|
+
builder = "DOCKERFILE"
|
|
1236
|
+
dockerfilePath = "Dockerfile"
|
|
1237
|
+
|
|
1238
|
+
[deploy]
|
|
1239
|
+
startCommand = "bun run dist/main.js"
|
|
1240
|
+
healthcheckPath = "/health"
|
|
1241
|
+
healthcheckTimeout = 300
|
|
1242
|
+
restartPolicyType = "ON_FAILURE"
|
|
1243
|
+
restartPolicyMaxRetries = 3
|
|
1244
|
+
|
|
1245
|
+
# Environment variables
|
|
1246
|
+
# Set these in Railway dashboard or via CLI:
|
|
1247
|
+
# railway variables set NODE_ENV=production
|
|
1248
|
+
# railway variables set DATABASE_URL=your-database-url
|
|
1249
|
+
|
|
1250
|
+
[[services]]
|
|
1251
|
+
name = "${kebabName}"
|
|
1252
|
+
|
|
1253
|
+
[services.variables]
|
|
1254
|
+
NODE_ENV = "production"
|
|
1255
|
+
BUN_ENV = "production"
|
|
1256
|
+
PORT = "3000"
|
|
1257
|
+
|
|
1258
|
+
# Health check configuration
|
|
1259
|
+
[[services.healthchecks]]
|
|
1260
|
+
path = "/health"
|
|
1261
|
+
interval = 30
|
|
1262
|
+
timeout = 10
|
|
1263
|
+
threshold = 3
|
|
1264
|
+
|
|
1265
|
+
# Resource configuration
|
|
1266
|
+
# Adjust in Railway dashboard or via CLI:
|
|
1267
|
+
# railway up --memory 512 --cpu 0.5
|
|
1268
|
+
|
|
1269
|
+
# Scaling configuration
|
|
1270
|
+
# Use Railway's autoscaling in dashboard:
|
|
1271
|
+
# Min instances: 0 (scale to zero)
|
|
1272
|
+
# Max instances: 3
|
|
1273
|
+
# Target CPU: 70%
|
|
1274
|
+
# Target Memory: 80%
|
|
1275
|
+
`;
|
|
1276
|
+
}
|
|
1277
|
+
function getDeployTemplate(platform, projectName, database) {
|
|
1278
|
+
switch (platform) {
|
|
1279
|
+
case "render":
|
|
1280
|
+
return getRenderYamlTemplate(projectName, database);
|
|
1281
|
+
case "fly":
|
|
1282
|
+
return getFlyTomlTemplate(projectName);
|
|
1283
|
+
case "railway":
|
|
1284
|
+
return getRailwayTomlTemplate(projectName);
|
|
1285
|
+
default:
|
|
1286
|
+
throw new Error(`Unknown deployment platform: ${platform}`);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
function getDeployFilename(platform) {
|
|
1290
|
+
switch (platform) {
|
|
1291
|
+
case "render":
|
|
1292
|
+
return "render.yaml";
|
|
1293
|
+
case "fly":
|
|
1294
|
+
return "fly.toml";
|
|
1295
|
+
case "railway":
|
|
1296
|
+
return "railway.toml";
|
|
1297
|
+
default:
|
|
1298
|
+
throw new Error(`Unknown deployment platform: ${platform}`);
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
function getDeployPlatformName(platform) {
|
|
1302
|
+
switch (platform) {
|
|
1303
|
+
case "render":
|
|
1304
|
+
return "Render.com";
|
|
1305
|
+
case "fly":
|
|
1306
|
+
return "Fly.io";
|
|
1307
|
+
case "railway":
|
|
1308
|
+
return "Railway";
|
|
1309
|
+
default:
|
|
1310
|
+
return platform;
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
// src/cli/commands/new.ts
|
|
1315
|
+
function validateProjectName(name) {
|
|
1316
|
+
if (!name || name.length === 0) {
|
|
1317
|
+
return "Project name is required";
|
|
1318
|
+
}
|
|
1319
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
1320
|
+
return "Project name can only contain letters, numbers, hyphens, and underscores";
|
|
1321
|
+
}
|
|
1322
|
+
if (name.startsWith("-") || name.startsWith("_")) {
|
|
1323
|
+
return "Project name cannot start with a hyphen or underscore";
|
|
1324
|
+
}
|
|
1325
|
+
if (name.length > 100) {
|
|
1326
|
+
return "Project name is too long (max 100 characters)";
|
|
1327
|
+
}
|
|
1328
|
+
return true;
|
|
1329
|
+
}
|
|
1330
|
+
function getPackageJsonTemplate(config) {
|
|
1331
|
+
const dependencies = {
|
|
1332
|
+
bueno: "^0.1.0"
|
|
1333
|
+
};
|
|
1334
|
+
const devDependencies = {
|
|
1335
|
+
"@types/bun": "latest",
|
|
1336
|
+
typescript: "^5.3.0"
|
|
1337
|
+
};
|
|
1338
|
+
if (config.template === "fullstack" || config.template === "default") {
|
|
1339
|
+
dependencies.zod = "^4.0.0";
|
|
1340
|
+
}
|
|
1341
|
+
const scripts = {
|
|
1342
|
+
dev: "bun run --watch server/main.ts",
|
|
1343
|
+
build: "bun build ./server/main.ts --outdir ./dist --target bun",
|
|
1344
|
+
start: "bun run dist/main.js",
|
|
1345
|
+
test: "bun test"
|
|
1346
|
+
};
|
|
1347
|
+
if (config.template === "fullstack") {
|
|
1348
|
+
scripts["dev:frontend"] = "bun run --watch client/index.html";
|
|
1349
|
+
}
|
|
1350
|
+
return JSON.stringify({
|
|
1351
|
+
name: kebabCase(config.name),
|
|
1352
|
+
version: "0.1.0",
|
|
1353
|
+
type: "module",
|
|
1354
|
+
scripts,
|
|
1355
|
+
dependencies,
|
|
1356
|
+
devDependencies
|
|
1357
|
+
}, null, 2);
|
|
1358
|
+
}
|
|
1359
|
+
function getTsConfigTemplate() {
|
|
1360
|
+
return JSON.stringify({
|
|
1361
|
+
compilerOptions: {
|
|
1362
|
+
target: "ESNext",
|
|
1363
|
+
module: "ESNext",
|
|
1364
|
+
moduleResolution: "bundler",
|
|
1365
|
+
strict: true,
|
|
1366
|
+
skipLibCheck: true,
|
|
1367
|
+
esModuleInterop: true,
|
|
1368
|
+
allowSyntheticDefaultImports: true,
|
|
1369
|
+
jsx: "react-jsx",
|
|
1370
|
+
paths: {
|
|
1371
|
+
bueno: ["./node_modules/bueno/dist/index.d.ts"]
|
|
1372
|
+
}
|
|
1373
|
+
},
|
|
1374
|
+
include: ["server/**/*", "client/**/*"],
|
|
1375
|
+
exclude: ["node_modules", "dist"]
|
|
1376
|
+
}, null, 2);
|
|
1377
|
+
}
|
|
1378
|
+
function getMainTemplate(config) {
|
|
1379
|
+
if (config.template === "minimal") {
|
|
1380
|
+
return `import { createServer } from 'bueno';
|
|
1381
|
+
|
|
1382
|
+
const app = createServer();
|
|
1383
|
+
|
|
1384
|
+
app.router.get('/', () => {
|
|
1385
|
+
return { message: 'Hello, Bueno!' };
|
|
1386
|
+
});
|
|
1387
|
+
|
|
1388
|
+
await app.listen(3000);
|
|
1389
|
+
`;
|
|
1390
|
+
}
|
|
1391
|
+
return `import { createApp, Module, Controller, Get, Injectable } from 'bueno';
|
|
1392
|
+
import type { Context } from 'bueno';
|
|
1393
|
+
|
|
1394
|
+
// Services
|
|
1395
|
+
@Injectable()
|
|
1396
|
+
export class AppService {
|
|
1397
|
+
findAll() {
|
|
1398
|
+
return { message: 'Welcome to Bueno!', items: [] };
|
|
1399
|
+
}
|
|
1400
|
+
}
|
|
1401
|
+
|
|
1402
|
+
// Controllers
|
|
1403
|
+
@Controller()
|
|
1404
|
+
export class AppController {
|
|
1405
|
+
constructor(private readonly appService: AppService) {}
|
|
1406
|
+
|
|
1407
|
+
@Get()
|
|
1408
|
+
findAll(ctx: Context) {
|
|
1409
|
+
return this.appService.findAll();
|
|
1410
|
+
}
|
|
1411
|
+
|
|
1412
|
+
@Get('health')
|
|
1413
|
+
health(ctx: Context) {
|
|
1414
|
+
return { status: 'ok', timestamp: new Date().toISOString() };
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1418
|
+
// Module
|
|
1419
|
+
@Module({
|
|
1420
|
+
controllers: [AppController],
|
|
1421
|
+
providers: [AppService],
|
|
1422
|
+
})
|
|
1423
|
+
export class AppModule {}
|
|
1424
|
+
|
|
1425
|
+
// Bootstrap
|
|
1426
|
+
const app = createApp(AppModule);
|
|
1427
|
+
await app.listen(3000);
|
|
1428
|
+
`;
|
|
1429
|
+
}
|
|
1430
|
+
function getConfigTemplate(config) {
|
|
1431
|
+
const dbConfig = config.database === "sqlite" ? `{ url: 'sqlite:./data.db' }` : `{ url: process.env.DATABASE_URL ?? '${config.database}://localhost/${kebabCase(config.name)}' }`;
|
|
1432
|
+
return `import { defineConfig } from 'bueno';
|
|
1433
|
+
|
|
1434
|
+
export default defineConfig({
|
|
1435
|
+
server: {
|
|
1436
|
+
port: 3000,
|
|
1437
|
+
host: 'localhost',
|
|
1438
|
+
},
|
|
1439
|
+
|
|
1440
|
+
database: ${dbConfig},
|
|
1441
|
+
|
|
1442
|
+
logger: {
|
|
1443
|
+
level: 'info',
|
|
1444
|
+
pretty: true,
|
|
1445
|
+
},
|
|
1446
|
+
|
|
1447
|
+
health: {
|
|
1448
|
+
enabled: true,
|
|
1449
|
+
healthPath: '/health',
|
|
1450
|
+
readyPath: '/ready',
|
|
1451
|
+
},
|
|
1452
|
+
});
|
|
1453
|
+
`;
|
|
1454
|
+
}
|
|
1455
|
+
function getEnvExampleTemplate(config) {
|
|
1456
|
+
if (config.database === "sqlite") {
|
|
1457
|
+
return `# Bueno Environment Variables
|
|
1458
|
+
NODE_ENV=development
|
|
1459
|
+
`;
|
|
1460
|
+
}
|
|
1461
|
+
return `# Bueno Environment Variables
|
|
1462
|
+
NODE_ENV=development
|
|
1463
|
+
DATABASE_URL=${config.database}://user:password@localhost:5432/${kebabCase(config.name)}
|
|
1464
|
+
`;
|
|
1465
|
+
}
|
|
1466
|
+
function getGitignoreTemplate() {
|
|
1467
|
+
return `# Dependencies
|
|
1468
|
+
node_modules/
|
|
1469
|
+
|
|
1470
|
+
# Build output
|
|
1471
|
+
dist/
|
|
1472
|
+
|
|
1473
|
+
# Environment files
|
|
1474
|
+
.env
|
|
1475
|
+
.env.local
|
|
1476
|
+
.env.*.local
|
|
1477
|
+
|
|
1478
|
+
# IDE
|
|
1479
|
+
.idea/
|
|
1480
|
+
.vscode/
|
|
1481
|
+
*.swp
|
|
1482
|
+
*.swo
|
|
1483
|
+
|
|
1484
|
+
# OS
|
|
1485
|
+
.DS_Store
|
|
1486
|
+
Thumbs.db
|
|
1487
|
+
|
|
1488
|
+
# Logs
|
|
1489
|
+
*.log
|
|
1490
|
+
logs/
|
|
1491
|
+
|
|
1492
|
+
# Database
|
|
1493
|
+
*.db
|
|
1494
|
+
*.sqlite
|
|
1495
|
+
*.sqlite3
|
|
1496
|
+
|
|
1497
|
+
# Test coverage
|
|
1498
|
+
coverage/
|
|
1499
|
+
`;
|
|
1500
|
+
}
|
|
1501
|
+
function getReadmeTemplate(config) {
|
|
1502
|
+
return `# ${config.name}
|
|
1503
|
+
|
|
1504
|
+
A Bueno application.
|
|
1505
|
+
|
|
1506
|
+
## Getting Started
|
|
1507
|
+
|
|
1508
|
+
\`\`\`bash
|
|
1509
|
+
# Install dependencies
|
|
1510
|
+
bun install
|
|
1511
|
+
|
|
1512
|
+
# Start development server
|
|
1513
|
+
bun run dev
|
|
1514
|
+
|
|
1515
|
+
# Build for production
|
|
1516
|
+
bun run build
|
|
1517
|
+
|
|
1518
|
+
# Start production server
|
|
1519
|
+
bun run start
|
|
1520
|
+
\`\`\`
|
|
1521
|
+
|
|
1522
|
+
## Project Structure
|
|
1523
|
+
|
|
1524
|
+
\`\`\`
|
|
1525
|
+
\u251C\u2500\u2500 server/ # Server-side code
|
|
1526
|
+
\u2502 \u251C\u2500\u2500 main.ts # Entry point
|
|
1527
|
+
\u2502 \u251C\u2500\u2500 modules/ # Feature modules
|
|
1528
|
+
\u2502 \u2514\u2500\u2500 database/ # Database files
|
|
1529
|
+
\u251C\u2500\u2500 client/ # Client-side code (if applicable)
|
|
1530
|
+
\u251C\u2500\u2500 tests/ # Test files
|
|
1531
|
+
\u2514\u2500\u2500 bueno.config.ts # Configuration
|
|
1532
|
+
\`\`\`
|
|
1533
|
+
|
|
1534
|
+
## Learn More
|
|
1535
|
+
|
|
1536
|
+
- [Bueno Documentation](https://github.com/sivaraj/bueno#readme)
|
|
1537
|
+
- [Bun Documentation](https://bun.sh/docs)
|
|
1538
|
+
`;
|
|
1539
|
+
}
|
|
1540
|
+
async function createProjectFiles(projectPath, config) {
|
|
1541
|
+
const tasks = [];
|
|
1542
|
+
tasks.push({
|
|
1543
|
+
text: "Creating project structure",
|
|
1544
|
+
task: async () => {
|
|
1545
|
+
await createDirectory(joinPaths(projectPath, "server", "modules", "app"));
|
|
1546
|
+
await createDirectory(joinPaths(projectPath, "server", "common", "middleware"));
|
|
1547
|
+
await createDirectory(joinPaths(projectPath, "server", "common", "guards"));
|
|
1548
|
+
await createDirectory(joinPaths(projectPath, "server", "common", "interceptors"));
|
|
1549
|
+
await createDirectory(joinPaths(projectPath, "server", "common", "pipes"));
|
|
1550
|
+
await createDirectory(joinPaths(projectPath, "server", "common", "filters"));
|
|
1551
|
+
await createDirectory(joinPaths(projectPath, "server", "database", "migrations"));
|
|
1552
|
+
await createDirectory(joinPaths(projectPath, "server", "config"));
|
|
1553
|
+
await createDirectory(joinPaths(projectPath, "tests", "unit"));
|
|
1554
|
+
await createDirectory(joinPaths(projectPath, "tests", "integration"));
|
|
1555
|
+
}
|
|
1556
|
+
});
|
|
1557
|
+
tasks.push({
|
|
1558
|
+
text: "Creating package.json",
|
|
1559
|
+
task: async () => {
|
|
1560
|
+
await writeFile(joinPaths(projectPath, "package.json"), getPackageJsonTemplate(config));
|
|
1561
|
+
}
|
|
1562
|
+
});
|
|
1563
|
+
tasks.push({
|
|
1564
|
+
text: "Creating tsconfig.json",
|
|
1565
|
+
task: async () => {
|
|
1566
|
+
await writeFile(joinPaths(projectPath, "tsconfig.json"), getTsConfigTemplate());
|
|
1567
|
+
}
|
|
1568
|
+
});
|
|
1569
|
+
tasks.push({
|
|
1570
|
+
text: "Creating server/main.ts",
|
|
1571
|
+
task: async () => {
|
|
1572
|
+
await writeFile(joinPaths(projectPath, "server", "main.ts"), getMainTemplate(config));
|
|
1573
|
+
}
|
|
1574
|
+
});
|
|
1575
|
+
tasks.push({
|
|
1576
|
+
text: "Creating bueno.config.ts",
|
|
1577
|
+
task: async () => {
|
|
1578
|
+
await writeFile(joinPaths(projectPath, "bueno.config.ts"), getConfigTemplate(config));
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
tasks.push({
|
|
1582
|
+
text: "Creating .env.example",
|
|
1583
|
+
task: async () => {
|
|
1584
|
+
await writeFile(joinPaths(projectPath, ".env.example"), getEnvExampleTemplate(config));
|
|
1585
|
+
}
|
|
1586
|
+
});
|
|
1587
|
+
tasks.push({
|
|
1588
|
+
text: "Creating .gitignore",
|
|
1589
|
+
task: async () => {
|
|
1590
|
+
await writeFile(joinPaths(projectPath, ".gitignore"), getGitignoreTemplate());
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
tasks.push({
|
|
1594
|
+
text: "Creating README.md",
|
|
1595
|
+
task: async () => {
|
|
1596
|
+
await writeFile(joinPaths(projectPath, "README.md"), getReadmeTemplate(config));
|
|
1597
|
+
}
|
|
1598
|
+
});
|
|
1599
|
+
if (config.docker) {
|
|
1600
|
+
tasks.push({
|
|
1601
|
+
text: "Creating Dockerfile",
|
|
1602
|
+
task: async () => {
|
|
1603
|
+
await writeFile(joinPaths(projectPath, "Dockerfile"), getDockerfileTemplate(config.name, config.database));
|
|
1604
|
+
}
|
|
1605
|
+
});
|
|
1606
|
+
tasks.push({
|
|
1607
|
+
text: "Creating .dockerignore",
|
|
1608
|
+
task: async () => {
|
|
1609
|
+
await writeFile(joinPaths(projectPath, ".dockerignore"), getDockerignoreTemplate());
|
|
1610
|
+
}
|
|
1611
|
+
});
|
|
1612
|
+
tasks.push({
|
|
1613
|
+
text: "Creating docker-compose.yml",
|
|
1614
|
+
task: async () => {
|
|
1615
|
+
await writeFile(joinPaths(projectPath, "docker-compose.yml"), getDockerComposeTemplate(config.name, config.database));
|
|
1616
|
+
}
|
|
1617
|
+
});
|
|
1618
|
+
tasks.push({
|
|
1619
|
+
text: "Creating .env.docker",
|
|
1620
|
+
task: async () => {
|
|
1621
|
+
await writeFile(joinPaths(projectPath, ".env.docker"), getDockerEnvTemplate(config.name, config.database));
|
|
1622
|
+
}
|
|
1623
|
+
});
|
|
1624
|
+
}
|
|
1625
|
+
for (const platform of config.deploy) {
|
|
1626
|
+
const filename = getDeployFilename(platform);
|
|
1627
|
+
tasks.push({
|
|
1628
|
+
text: `Creating ${filename} for ${getDeployPlatformName(platform)}`,
|
|
1629
|
+
task: async () => {
|
|
1630
|
+
await writeFile(joinPaths(projectPath, filename), getDeployTemplate(platform, config.name, config.database));
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
await runTasks(tasks);
|
|
1635
|
+
}
|
|
1636
|
+
async function handleNew(args) {
|
|
1637
|
+
let name = args.positionals[0];
|
|
1638
|
+
const useDefaults = hasFlag(args, "yes") || hasFlag(args, "y");
|
|
1639
|
+
if (!name && isInteractive()) {
|
|
1640
|
+
name = await prompt("Project name:", {
|
|
1641
|
+
validate: validateProjectName
|
|
1642
|
+
});
|
|
1643
|
+
}
|
|
1644
|
+
if (!name) {
|
|
1645
|
+
throw new CLIError("Project name is required. Usage: bueno new <project-name>", "INVALID_ARGS" /* INVALID_ARGS */);
|
|
1646
|
+
}
|
|
1647
|
+
const validation = validateProjectName(name);
|
|
1648
|
+
if (validation !== true) {
|
|
1649
|
+
throw new CLIError(validation, "INVALID_ARGS" /* INVALID_ARGS */);
|
|
1650
|
+
}
|
|
1651
|
+
let template = getOption(args, "template", {
|
|
1652
|
+
name: "template",
|
|
1653
|
+
alias: "t",
|
|
1654
|
+
type: "string",
|
|
1655
|
+
description: ""
|
|
1656
|
+
});
|
|
1657
|
+
let framework = getOption(args, "framework", {
|
|
1658
|
+
name: "framework",
|
|
1659
|
+
alias: "f",
|
|
1660
|
+
type: "string",
|
|
1661
|
+
description: ""
|
|
1662
|
+
});
|
|
1663
|
+
let database = getOption(args, "database", {
|
|
1664
|
+
name: "database",
|
|
1665
|
+
alias: "d",
|
|
1666
|
+
type: "string",
|
|
1667
|
+
description: ""
|
|
1668
|
+
});
|
|
1669
|
+
const skipInstall = hasFlag(args, "skip-install");
|
|
1670
|
+
const skipGit = hasFlag(args, "skip-git");
|
|
1671
|
+
const docker = hasFlag(args, "docker");
|
|
1672
|
+
const deployPlatforms = getOptionValues(args, "deploy");
|
|
1673
|
+
const validPlatforms = ["render", "fly", "railway"];
|
|
1674
|
+
const deploy = [];
|
|
1675
|
+
for (const platform of deployPlatforms) {
|
|
1676
|
+
if (validPlatforms.includes(platform)) {
|
|
1677
|
+
if (!deploy.includes(platform)) {
|
|
1678
|
+
deploy.push(platform);
|
|
1679
|
+
}
|
|
1680
|
+
} else {
|
|
1681
|
+
throw new CLIError(`Invalid deployment platform: ${platform}. Valid options are: ${validPlatforms.join(", ")}`, "INVALID_ARGS" /* INVALID_ARGS */);
|
|
1682
|
+
}
|
|
1683
|
+
}
|
|
1684
|
+
if (!useDefaults && isInteractive()) {
|
|
1685
|
+
if (!template) {
|
|
1686
|
+
template = await select("Select a template:", [
|
|
1687
|
+
{ value: "default", name: "Default - Standard project with modules and database" },
|
|
1688
|
+
{ value: "minimal", name: "Minimal - Bare minimum project structure" },
|
|
1689
|
+
{ value: "fullstack", name: "Fullstack - Full-stack project with SSR and auth" },
|
|
1690
|
+
{ value: "api", name: "API - API-only project without frontend" }
|
|
1691
|
+
], { default: "default" });
|
|
1692
|
+
}
|
|
1693
|
+
if ((template === "fullstack" || template === "default") && !framework) {
|
|
1694
|
+
framework = await select("Select a frontend framework:", [
|
|
1695
|
+
{ value: "react", name: "React" },
|
|
1696
|
+
{ value: "vue", name: "Vue" },
|
|
1697
|
+
{ value: "svelte", name: "Svelte" },
|
|
1698
|
+
{ value: "solid", name: "Solid" }
|
|
1699
|
+
], { default: "react" });
|
|
1700
|
+
}
|
|
1701
|
+
if (!database) {
|
|
1702
|
+
database = await select("Select a database:", [
|
|
1703
|
+
{ value: "sqlite", name: "SQLite - Local file-based database" },
|
|
1704
|
+
{ value: "postgresql", name: "PostgreSQL - Production-ready relational database" },
|
|
1705
|
+
{ value: "mysql", name: "MySQL - Popular relational database" }
|
|
1706
|
+
], { default: "sqlite" });
|
|
1707
|
+
}
|
|
1708
|
+
}
|
|
1709
|
+
template = template || "default";
|
|
1710
|
+
framework = framework || "react";
|
|
1711
|
+
database = database || "sqlite";
|
|
1712
|
+
const config = {
|
|
1713
|
+
name,
|
|
1714
|
+
template,
|
|
1715
|
+
framework,
|
|
1716
|
+
database,
|
|
1717
|
+
skipInstall,
|
|
1718
|
+
skipGit,
|
|
1719
|
+
docker,
|
|
1720
|
+
deploy
|
|
1721
|
+
};
|
|
1722
|
+
const projectPath = joinPaths(process.cwd(), kebabCase(name));
|
|
1723
|
+
if (await fileExists(projectPath)) {
|
|
1724
|
+
throw new CLIError(`Directory already exists: ${kebabCase(name)}`, "FILE_EXISTS" /* FILE_EXISTS */);
|
|
1725
|
+
}
|
|
1726
|
+
cliConsole.header(`Creating a new Bueno project: ${colors.cyan(name)}`);
|
|
1727
|
+
const rows = [
|
|
1728
|
+
["Template", template],
|
|
1729
|
+
["Framework", framework],
|
|
1730
|
+
["Database", database],
|
|
1731
|
+
["Docker", docker ? colors.green("Yes") : colors.red("No")],
|
|
1732
|
+
["Deploy", deploy.length > 0 ? colors.green(deploy.map(getDeployPlatformName).join(", ")) : colors.red("None")],
|
|
1733
|
+
["Install dependencies", skipInstall ? colors.red("No") : colors.green("Yes")],
|
|
1734
|
+
["Initialize git", skipGit ? colors.red("No") : colors.green("Yes")]
|
|
1735
|
+
];
|
|
1736
|
+
printTable(["Setting", "Value"], rows);
|
|
1737
|
+
cliConsole.log("");
|
|
1738
|
+
cliConsole.subheader("Creating project files...");
|
|
1739
|
+
await createProjectFiles(projectPath, config);
|
|
1740
|
+
if (!skipInstall) {
|
|
1741
|
+
cliConsole.subheader("Installing dependencies...");
|
|
1742
|
+
const installSpinner = spinner("Running bun install...");
|
|
1743
|
+
try {
|
|
1744
|
+
const proc = Bun.spawn(["bun", "install"], {
|
|
1745
|
+
cwd: projectPath,
|
|
1746
|
+
stdout: "pipe",
|
|
1747
|
+
stderr: "pipe"
|
|
1748
|
+
});
|
|
1749
|
+
const exitCode = await proc.exited;
|
|
1750
|
+
if (exitCode === 0) {
|
|
1751
|
+
installSpinner.success("Dependencies installed");
|
|
1752
|
+
} else {
|
|
1753
|
+
installSpinner.warn("Failed to install dependencies. Run `bun install` manually.");
|
|
1754
|
+
}
|
|
1755
|
+
} catch {
|
|
1756
|
+
installSpinner.warn("Failed to install dependencies. Run `bun install` manually.");
|
|
1757
|
+
}
|
|
1758
|
+
}
|
|
1759
|
+
if (!skipGit) {
|
|
1760
|
+
cliConsole.subheader("Initializing git repository...");
|
|
1761
|
+
const gitSpinner = spinner("Running git init...");
|
|
1762
|
+
try {
|
|
1763
|
+
const proc = Bun.spawn(["git", "init"], {
|
|
1764
|
+
cwd: projectPath,
|
|
1765
|
+
stdout: "pipe",
|
|
1766
|
+
stderr: "pipe"
|
|
1767
|
+
});
|
|
1768
|
+
const exitCode = await proc.exited;
|
|
1769
|
+
if (exitCode === 0) {
|
|
1770
|
+
Bun.spawn(["git", "add", "."], { cwd: projectPath });
|
|
1771
|
+
Bun.spawn(["git", "commit", "-m", "Initial commit from Bueno CLI"], {
|
|
1772
|
+
cwd: projectPath
|
|
1773
|
+
});
|
|
1774
|
+
gitSpinner.success("Git repository initialized");
|
|
1775
|
+
} else {
|
|
1776
|
+
gitSpinner.warn("Failed to initialize git. Run `git init` manually.");
|
|
1777
|
+
}
|
|
1778
|
+
} catch {
|
|
1779
|
+
gitSpinner.warn("Failed to initialize git. Run `git init` manually.");
|
|
1780
|
+
}
|
|
1781
|
+
}
|
|
1782
|
+
cliConsole.log("");
|
|
1783
|
+
cliConsole.success(`Project created successfully!`);
|
|
1784
|
+
cliConsole.log("");
|
|
1785
|
+
cliConsole.log("Next steps:");
|
|
1786
|
+
cliConsole.log(` ${colors.cyan(`cd ${kebabCase(name)}`)}`);
|
|
1787
|
+
cliConsole.log(` ${colors.cyan("bun run dev")}`);
|
|
1788
|
+
cliConsole.log("");
|
|
1789
|
+
cliConsole.log(`Documentation: ${colors.dim("https://github.com/sivaraj/bueno")}`);
|
|
1790
|
+
}
|
|
1791
|
+
defineCommand({
|
|
1792
|
+
name: "new",
|
|
1793
|
+
description: "Create a new Bueno project",
|
|
1794
|
+
positionals: [
|
|
1795
|
+
{
|
|
1796
|
+
name: "name",
|
|
1797
|
+
required: false,
|
|
1798
|
+
description: "Project name"
|
|
1799
|
+
}
|
|
1800
|
+
],
|
|
1801
|
+
options: [
|
|
1802
|
+
{
|
|
1803
|
+
name: "template",
|
|
1804
|
+
alias: "t",
|
|
1805
|
+
type: "string",
|
|
1806
|
+
description: "Project template (default, minimal, fullstack, api)"
|
|
1807
|
+
},
|
|
1808
|
+
{
|
|
1809
|
+
name: "framework",
|
|
1810
|
+
alias: "f",
|
|
1811
|
+
type: "string",
|
|
1812
|
+
description: "Frontend framework (react, vue, svelte, solid)"
|
|
1813
|
+
},
|
|
1814
|
+
{
|
|
1815
|
+
name: "database",
|
|
1816
|
+
alias: "d",
|
|
1817
|
+
type: "string",
|
|
1818
|
+
description: "Database driver (sqlite, postgresql, mysql)"
|
|
1819
|
+
},
|
|
1820
|
+
{
|
|
1821
|
+
name: "skip-install",
|
|
1822
|
+
type: "boolean",
|
|
1823
|
+
default: false,
|
|
1824
|
+
description: "Skip dependency installation"
|
|
1825
|
+
},
|
|
1826
|
+
{
|
|
1827
|
+
name: "skip-git",
|
|
1828
|
+
type: "boolean",
|
|
1829
|
+
default: false,
|
|
1830
|
+
description: "Skip git initialization"
|
|
1831
|
+
},
|
|
1832
|
+
{
|
|
1833
|
+
name: "docker",
|
|
1834
|
+
type: "boolean",
|
|
1835
|
+
default: false,
|
|
1836
|
+
description: "Include Docker configuration (Dockerfile, docker-compose.yml)"
|
|
1837
|
+
},
|
|
1838
|
+
{
|
|
1839
|
+
name: "deploy",
|
|
1840
|
+
type: "string",
|
|
1841
|
+
description: "Deployment platform configuration (render, fly, railway). Can be specified multiple times."
|
|
1842
|
+
},
|
|
1843
|
+
{
|
|
1844
|
+
name: "yes",
|
|
1845
|
+
alias: "y",
|
|
1846
|
+
type: "boolean",
|
|
1847
|
+
default: false,
|
|
1848
|
+
description: "Use default options without prompts"
|
|
1849
|
+
}
|
|
1850
|
+
],
|
|
1851
|
+
examples: [
|
|
1852
|
+
"bueno new my-app",
|
|
1853
|
+
"bueno new my-api --template api",
|
|
1854
|
+
"bueno new my-fullstack --template fullstack --framework react",
|
|
1855
|
+
"bueno new my-project --database postgresql",
|
|
1856
|
+
"bueno new my-app --docker",
|
|
1857
|
+
"bueno new my-app --docker --database postgresql",
|
|
1858
|
+
"bueno new my-app --deploy render",
|
|
1859
|
+
"bueno new my-app --deploy fly",
|
|
1860
|
+
"bueno new my-app --deploy render --deploy fly",
|
|
1861
|
+
"bueno new my-app --docker --deploy render",
|
|
1862
|
+
"bueno new my-app --docker --database postgresql --deploy render",
|
|
1863
|
+
"bueno new my-app -y"
|
|
1864
|
+
]
|
|
1865
|
+
}, handleNew);
|
|
1866
|
+
|
|
1867
|
+
// src/cli/commands/generate.ts
|
|
1868
|
+
var GENERATOR_ALIASES = {
|
|
1869
|
+
c: "controller",
|
|
1870
|
+
s: "service",
|
|
1871
|
+
m: "module",
|
|
1872
|
+
gu: "guard",
|
|
1873
|
+
i: "interceptor",
|
|
1874
|
+
p: "pipe",
|
|
1875
|
+
f: "filter",
|
|
1876
|
+
d: "dto",
|
|
1877
|
+
mw: "middleware",
|
|
1878
|
+
mi: "migration"
|
|
1879
|
+
};
|
|
1880
|
+
function getTemplate(type) {
|
|
1881
|
+
const templates = {
|
|
1882
|
+
controller: `import { Controller, Get, Post, Put, Delete{{#if path}} } from 'bueno'{{/if}}{{#if service}}, { {{pascalCase service}}Service } from './{{kebabCase service}}.service'{{/if}};
|
|
1883
|
+
import type { Context } from 'bueno';
|
|
1884
|
+
|
|
1885
|
+
@Controller('{{path}}')
|
|
1886
|
+
export class {{pascalCase name}}Controller {
|
|
1887
|
+
{{#if service}}
|
|
1888
|
+
constructor(private readonly {{camelCase service}}Service: {{pascalCase service}}Service) {}
|
|
1889
|
+
{{/if}}
|
|
1890
|
+
|
|
1891
|
+
@Get()
|
|
1892
|
+
async findAll(ctx: Context) {
|
|
1893
|
+
return { message: '{{pascalCase name}} controller' };
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
@Get(':id')
|
|
1897
|
+
async findOne(ctx: Context) {
|
|
1898
|
+
const id = ctx.params.id;
|
|
1899
|
+
return { id, message: '{{pascalCase name}} item' };
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1902
|
+
@Post()
|
|
1903
|
+
async create(ctx: Context) {
|
|
1904
|
+
const body = await ctx.body();
|
|
1905
|
+
return { message: 'Created', data: body };
|
|
1906
|
+
}
|
|
1907
|
+
|
|
1908
|
+
@Put(':id')
|
|
1909
|
+
async update(ctx: Context) {
|
|
1910
|
+
const id = ctx.params.id;
|
|
1911
|
+
const body = await ctx.body();
|
|
1912
|
+
return { id, message: 'Updated', data: body };
|
|
1913
|
+
}
|
|
1914
|
+
|
|
1915
|
+
@Delete(':id')
|
|
1916
|
+
async remove(ctx: Context) {
|
|
1917
|
+
const id = ctx.params.id;
|
|
1918
|
+
return { id, message: 'Deleted' };
|
|
1919
|
+
}
|
|
1920
|
+
}
|
|
1921
|
+
`,
|
|
1922
|
+
service: `import { Injectable } from 'bueno';
|
|
1923
|
+
|
|
1924
|
+
@Injectable()
|
|
1925
|
+
export class {{pascalCase name}}Service {
|
|
1926
|
+
async findAll() {
|
|
1927
|
+
// TODO: Implement findAll
|
|
1928
|
+
return [];
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
async findOne(id: string) {
|
|
1932
|
+
// TODO: Implement findOne
|
|
1933
|
+
return { id };
|
|
1934
|
+
}
|
|
1935
|
+
|
|
1936
|
+
async create(data: unknown) {
|
|
1937
|
+
// TODO: Implement create
|
|
1938
|
+
return data;
|
|
1939
|
+
}
|
|
1940
|
+
|
|
1941
|
+
async update(id: string, data: unknown) {
|
|
1942
|
+
// TODO: Implement update
|
|
1943
|
+
return { id, ...data };
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
async remove(id: string) {
|
|
1947
|
+
// TODO: Implement remove
|
|
1948
|
+
return { id };
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
`,
|
|
1952
|
+
module: `import { Module } from 'bueno';
|
|
1953
|
+
import { {{pascalCase name}}Controller } from './{{kebabCase name}}.controller';
|
|
1954
|
+
import { {{pascalCase name}}Service } from './{{kebabCase name}}.service';
|
|
1955
|
+
|
|
1956
|
+
@Module({
|
|
1957
|
+
controllers: [{{pascalCase name}}Controller],
|
|
1958
|
+
providers: [{{pascalCase name}}Service],
|
|
1959
|
+
exports: [{{pascalCase name}}Service],
|
|
1960
|
+
})
|
|
1961
|
+
export class {{pascalCase name}}Module {}
|
|
1962
|
+
`,
|
|
1963
|
+
guard: `import { Injectable, type CanActivate, type Context } from 'bueno';
|
|
1964
|
+
|
|
1965
|
+
@Injectable()
|
|
1966
|
+
export class {{pascalCase name}}Guard implements CanActivate {
|
|
1967
|
+
async canActivate(ctx: Context): Promise<boolean> {
|
|
1968
|
+
// TODO: Implement guard logic
|
|
1969
|
+
// Return true to allow access, false to deny
|
|
1970
|
+
return true;
|
|
1971
|
+
}
|
|
1972
|
+
}
|
|
1973
|
+
`,
|
|
1974
|
+
interceptor: `import { Injectable, type NestInterceptor, type CallHandler, type Context } from 'bueno';
|
|
1975
|
+
import type { Observable } from 'rxjs';
|
|
1976
|
+
|
|
1977
|
+
@Injectable()
|
|
1978
|
+
export class {{pascalCase name}}Interceptor implements NestInterceptor {
|
|
1979
|
+
async intercept(ctx: Context, next: CallHandler): Promise<Observable<unknown>> {
|
|
1980
|
+
// Before handler execution
|
|
1981
|
+
console.log('{{pascalCase name}}Interceptor - Before');
|
|
1982
|
+
|
|
1983
|
+
// Call the handler
|
|
1984
|
+
const result = await next.handle();
|
|
1985
|
+
|
|
1986
|
+
// After handler execution
|
|
1987
|
+
console.log('{{pascalCase name}}Interceptor - After');
|
|
1988
|
+
|
|
1989
|
+
return result;
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
`,
|
|
1993
|
+
pipe: `import { Injectable, type PipeTransform, type Context } from 'bueno';
|
|
1994
|
+
|
|
1995
|
+
@Injectable()
|
|
1996
|
+
export class {{pascalCase name}}Pipe implements PipeTransform {
|
|
1997
|
+
async transform(value: unknown, ctx: Context): Promise<unknown> {
|
|
1998
|
+
// TODO: Implement transformation/validation logic
|
|
1999
|
+
// Throw an error to reject the value
|
|
2000
|
+
return value;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
`,
|
|
2004
|
+
filter: `import { Injectable, type ExceptionFilter, type Context } from 'bueno';
|
|
2005
|
+
import type { Response } from 'bueno';
|
|
2006
|
+
|
|
2007
|
+
@Injectable()
|
|
2008
|
+
export class {{pascalCase name}}Filter implements ExceptionFilter {
|
|
2009
|
+
async catch(exception: Error, ctx: Context): Promise<Response> {
|
|
2010
|
+
// TODO: Implement exception handling
|
|
2011
|
+
console.error('{{pascalCase name}}Filter caught:', exception);
|
|
2012
|
+
|
|
2013
|
+
return new Response(
|
|
2014
|
+
JSON.stringify({
|
|
2015
|
+
statusCode: 500,
|
|
2016
|
+
message: 'Internal Server Error',
|
|
2017
|
+
error: exception.message,
|
|
2018
|
+
}),
|
|
2019
|
+
{
|
|
2020
|
+
status: 500,
|
|
2021
|
+
headers: { 'Content-Type': 'application/json' },
|
|
2022
|
+
}
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
`,
|
|
2027
|
+
dto: `/**
|
|
2028
|
+
* {{pascalCase name}} DTO
|
|
2029
|
+
*/
|
|
2030
|
+
export interface {{pascalCase name}}Dto {
|
|
2031
|
+
// TODO: Define properties
|
|
2032
|
+
id?: string;
|
|
2033
|
+
createdAt?: Date;
|
|
2034
|
+
updatedAt?: Date;
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
/**
|
|
2038
|
+
* Create {{pascalCase name}} DTO
|
|
2039
|
+
*/
|
|
2040
|
+
export interface Create{{pascalCase name}}Dto {
|
|
2041
|
+
// TODO: Define required properties for creation
|
|
2042
|
+
}
|
|
2043
|
+
|
|
2044
|
+
/**
|
|
2045
|
+
* Update {{pascalCase name}} DTO
|
|
2046
|
+
*/
|
|
2047
|
+
export interface Update{{pascalCase name}}Dto extends Partial<Create{{pascalCase name}}Dto> {
|
|
2048
|
+
// TODO: Define optional properties for update
|
|
2049
|
+
}
|
|
2050
|
+
`,
|
|
2051
|
+
middleware: `import type { Middleware, Context, Handler } from 'bueno';
|
|
2052
|
+
|
|
2053
|
+
/**
|
|
2054
|
+
* {{pascalCase name}} Middleware
|
|
2055
|
+
*/
|
|
2056
|
+
export const {{camelCase name}}Middleware: Middleware = async (
|
|
2057
|
+
ctx: Context,
|
|
2058
|
+
next: Handler
|
|
2059
|
+
) => {
|
|
2060
|
+
// Before handler execution
|
|
2061
|
+
console.log('{{pascalCase name}}Middleware - Before');
|
|
2062
|
+
|
|
2063
|
+
// Call the next handler
|
|
2064
|
+
const result = await next();
|
|
2065
|
+
|
|
2066
|
+
// After handler execution
|
|
2067
|
+
console.log('{{pascalCase name}}Middleware - After');
|
|
2068
|
+
|
|
2069
|
+
return result;
|
|
2070
|
+
};
|
|
2071
|
+
`,
|
|
2072
|
+
migration: `import { createMigration, type MigrationRunner } from 'bueno';
|
|
2073
|
+
|
|
2074
|
+
export default createMigration('{{migrationId}}', '{{migrationName}}')
|
|
2075
|
+
.up(async (db: MigrationRunner) => {
|
|
2076
|
+
// TODO: Add migration logic
|
|
2077
|
+
// Example:
|
|
2078
|
+
// await db.createTable({
|
|
2079
|
+
// name: '{{tableName}}',
|
|
2080
|
+
// columns: [
|
|
2081
|
+
// { name: 'id', type: 'uuid', primary: true },
|
|
2082
|
+
// { name: 'created_at', type: 'timestamp', default: 'NOW()' },
|
|
2083
|
+
// ],
|
|
2084
|
+
// });
|
|
2085
|
+
})
|
|
2086
|
+
.down(async (db: MigrationRunner) => {
|
|
2087
|
+
// TODO: Add rollback logic
|
|
2088
|
+
// Example:
|
|
2089
|
+
// await db.dropTable('{{tableName}}');
|
|
2090
|
+
});
|
|
2091
|
+
`
|
|
2092
|
+
};
|
|
2093
|
+
return templates[type];
|
|
2094
|
+
}
|
|
2095
|
+
function getFileExtension(type) {
|
|
2096
|
+
return type === "dto" ? ".dto.ts" : ".ts";
|
|
2097
|
+
}
|
|
2098
|
+
function getDefaultDirectory(type) {
|
|
2099
|
+
switch (type) {
|
|
2100
|
+
case "controller":
|
|
2101
|
+
case "service":
|
|
2102
|
+
case "module":
|
|
2103
|
+
case "dto":
|
|
2104
|
+
return "modules";
|
|
2105
|
+
case "guard":
|
|
2106
|
+
return "common/guards";
|
|
2107
|
+
case "interceptor":
|
|
2108
|
+
return "common/interceptors";
|
|
2109
|
+
case "pipe":
|
|
2110
|
+
return "common/pipes";
|
|
2111
|
+
case "filter":
|
|
2112
|
+
return "common/filters";
|
|
2113
|
+
case "middleware":
|
|
2114
|
+
return "common/middleware";
|
|
2115
|
+
case "migration":
|
|
2116
|
+
return "database/migrations";
|
|
2117
|
+
default:
|
|
2118
|
+
return "";
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
2121
|
+
async function generateFile(config) {
|
|
2122
|
+
const { type, name, module, path: customPath, dryRun, force } = config;
|
|
2123
|
+
const projectRoot = await getProjectRoot();
|
|
2124
|
+
if (!projectRoot) {
|
|
2125
|
+
throw new CLIError("Not in a Bueno project directory", "NOT_FOUND" /* NOT_FOUND */);
|
|
2126
|
+
}
|
|
2127
|
+
const kebabName = kebabCase(name);
|
|
2128
|
+
const defaultDir = getDefaultDirectory(type);
|
|
2129
|
+
let targetDir;
|
|
2130
|
+
if (customPath) {
|
|
2131
|
+
targetDir = joinPaths(projectRoot, customPath);
|
|
2132
|
+
} else if (module) {
|
|
2133
|
+
targetDir = joinPaths(projectRoot, "server", defaultDir, kebabCase(module));
|
|
2134
|
+
} else if (type === "migration") {
|
|
2135
|
+
targetDir = joinPaths(projectRoot, "server", defaultDir);
|
|
2136
|
+
} else {
|
|
2137
|
+
targetDir = joinPaths(projectRoot, "server", defaultDir, kebabName);
|
|
2138
|
+
}
|
|
2139
|
+
const fileName = type === "migration" ? `${generateMigrationId()}_${kebabName}${getFileExtension(type)}` : `${kebabName}${getFileExtension(type)}`;
|
|
2140
|
+
const filePath = joinPaths(targetDir, fileName);
|
|
2141
|
+
if (!force && await fileExists(filePath)) {
|
|
2142
|
+
if (isInteractive()) {
|
|
2143
|
+
const shouldOverwrite = await confirm(`File ${colors.cyan(filePath)} already exists. Overwrite?`, { default: false });
|
|
2144
|
+
if (!shouldOverwrite) {
|
|
2145
|
+
throw new CLIError("File already exists. Use --force to overwrite.", "FILE_EXISTS" /* FILE_EXISTS */);
|
|
2146
|
+
}
|
|
2147
|
+
} else {
|
|
2148
|
+
throw new CLIError(`File already exists: ${filePath}. Use --force to overwrite.`, "FILE_EXISTS" /* FILE_EXISTS */);
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
const template = getTemplate(type);
|
|
2152
|
+
const content = processTemplate(template, {
|
|
2153
|
+
name,
|
|
2154
|
+
module: module ?? "",
|
|
2155
|
+
path: customPath ?? kebabName,
|
|
2156
|
+
service: type === "controller" ? name : "",
|
|
2157
|
+
migrationId: generateMigrationId(),
|
|
2158
|
+
migrationName: name,
|
|
2159
|
+
tableName: kebabName
|
|
2160
|
+
});
|
|
2161
|
+
if (dryRun) {
|
|
2162
|
+
cliConsole.log(`
|
|
2163
|
+
${colors.bold("File:")} ${filePath}`);
|
|
2164
|
+
cliConsole.log(colors.bold("Content:"));
|
|
2165
|
+
cliConsole.log(content);
|
|
2166
|
+
cliConsole.log("");
|
|
2167
|
+
} else {
|
|
2168
|
+
await writeFile(filePath, content);
|
|
2169
|
+
}
|
|
2170
|
+
return filePath;
|
|
2171
|
+
}
|
|
2172
|
+
function generateMigrationId() {
|
|
2173
|
+
const now = new Date;
|
|
2174
|
+
const year = now.getFullYear();
|
|
2175
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
2176
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
2177
|
+
const hour = String(now.getHours()).padStart(2, "0");
|
|
2178
|
+
const minute = String(now.getMinutes()).padStart(2, "0");
|
|
2179
|
+
const second = String(now.getSeconds()).padStart(2, "0");
|
|
2180
|
+
return `${year}${month}${day}${hour}${minute}${second}`;
|
|
2181
|
+
}
|
|
2182
|
+
async function handleGenerate(args) {
|
|
2183
|
+
const typeArg = args.positionals[0];
|
|
2184
|
+
if (!typeArg) {
|
|
2185
|
+
throw new CLIError("Generator type is required. Usage: bueno generate <type> <name>", "INVALID_ARGS" /* INVALID_ARGS */);
|
|
2186
|
+
}
|
|
2187
|
+
const type = GENERATOR_ALIASES[typeArg] ?? typeArg;
|
|
2188
|
+
if (!getTemplate(type)) {
|
|
2189
|
+
throw new CLIError(`Unknown generator type: ${typeArg}. Available types: controller, service, module, guard, interceptor, pipe, filter, dto, middleware, migration`, "INVALID_ARGS" /* INVALID_ARGS */);
|
|
2190
|
+
}
|
|
2191
|
+
const name = args.positionals[1];
|
|
2192
|
+
if (!name) {
|
|
2193
|
+
throw new CLIError("Name is required. Usage: bueno generate <type> <name>", "INVALID_ARGS" /* INVALID_ARGS */);
|
|
2194
|
+
}
|
|
2195
|
+
const config = {
|
|
2196
|
+
type,
|
|
2197
|
+
name,
|
|
2198
|
+
module: getOption(args, "module", {
|
|
2199
|
+
name: "module",
|
|
2200
|
+
type: "string",
|
|
2201
|
+
description: ""
|
|
2202
|
+
}),
|
|
2203
|
+
path: getOption(args, "path", {
|
|
2204
|
+
name: "path",
|
|
2205
|
+
type: "string",
|
|
2206
|
+
description: ""
|
|
2207
|
+
}),
|
|
2208
|
+
dryRun: hasFlag(args, "dry-run"),
|
|
2209
|
+
force: hasFlag(args, "force")
|
|
2210
|
+
};
|
|
2211
|
+
if (!config.dryRun && !await isBuenoProject()) {
|
|
2212
|
+
throw new CLIError("Not in a Bueno project directory. Run this command from a Bueno project.", "NOT_FOUND" /* NOT_FOUND */);
|
|
2213
|
+
}
|
|
2214
|
+
const s = spinner(`Generating ${colors.cyan(type)} ${colors.cyan(name)}...`);
|
|
2215
|
+
try {
|
|
2216
|
+
const filePath = await generateFile(config);
|
|
2217
|
+
if (config.dryRun) {
|
|
2218
|
+
s.info("Dry run complete");
|
|
2219
|
+
} else {
|
|
2220
|
+
s.success(`Created ${colors.green(filePath)}`);
|
|
2221
|
+
}
|
|
2222
|
+
} catch (error) {
|
|
2223
|
+
s.error();
|
|
2224
|
+
throw error;
|
|
2225
|
+
}
|
|
2226
|
+
}
|
|
2227
|
+
defineCommand({
|
|
2228
|
+
name: "generate",
|
|
2229
|
+
alias: "g",
|
|
2230
|
+
description: "Generate code artifacts (controllers, services, modules, etc.)",
|
|
2231
|
+
positionals: [
|
|
2232
|
+
{
|
|
2233
|
+
name: "type",
|
|
2234
|
+
required: true,
|
|
2235
|
+
description: "Type of artifact to generate (controller, service, module, guard, interceptor, pipe, filter, dto, middleware, migration)"
|
|
2236
|
+
},
|
|
2237
|
+
{
|
|
2238
|
+
name: "name",
|
|
2239
|
+
required: true,
|
|
2240
|
+
description: "Name of the artifact"
|
|
2241
|
+
}
|
|
2242
|
+
],
|
|
2243
|
+
options: [
|
|
2244
|
+
{
|
|
2245
|
+
name: "module",
|
|
2246
|
+
alias: "m",
|
|
2247
|
+
type: "string",
|
|
2248
|
+
description: "Parent module to register with"
|
|
2249
|
+
},
|
|
2250
|
+
{
|
|
2251
|
+
name: "path",
|
|
2252
|
+
type: "string",
|
|
2253
|
+
description: "Custom path for controller routes"
|
|
2254
|
+
},
|
|
2255
|
+
{
|
|
2256
|
+
name: "dry-run",
|
|
2257
|
+
type: "boolean",
|
|
2258
|
+
default: false,
|
|
2259
|
+
description: "Show what would be created without writing"
|
|
2260
|
+
},
|
|
2261
|
+
{
|
|
2262
|
+
name: "force",
|
|
2263
|
+
type: "boolean",
|
|
2264
|
+
default: false,
|
|
2265
|
+
description: "Overwrite existing files"
|
|
2266
|
+
}
|
|
2267
|
+
],
|
|
2268
|
+
examples: [
|
|
2269
|
+
"bueno generate controller users",
|
|
2270
|
+
"bueno g service auth",
|
|
2271
|
+
"bueno g module posts",
|
|
2272
|
+
"bueno g guard auth-guard --module auth",
|
|
2273
|
+
"bueno g dto create-user --module users"
|
|
2274
|
+
]
|
|
2275
|
+
}, handleGenerate);
|
|
2276
|
+
|
|
2277
|
+
// src/cli/commands/migration.ts
|
|
2278
|
+
function generateMigrationId2() {
|
|
2279
|
+
const now = new Date;
|
|
2280
|
+
const year = now.getFullYear();
|
|
2281
|
+
const month = String(now.getMonth() + 1).padStart(2, "0");
|
|
2282
|
+
const day = String(now.getDate()).padStart(2, "0");
|
|
2283
|
+
const hour = String(now.getHours()).padStart(2, "0");
|
|
2284
|
+
const minute = String(now.getMinutes()).padStart(2, "0");
|
|
2285
|
+
const second = String(now.getSeconds()).padStart(2, "0");
|
|
2286
|
+
return `${year}${month}${day}${hour}${minute}${second}`;
|
|
2287
|
+
}
|
|
2288
|
+
async function getMigrationsDir() {
|
|
2289
|
+
const projectRoot = await getProjectRoot();
|
|
2290
|
+
if (!projectRoot) {
|
|
2291
|
+
throw new CLIError("Not in a project directory", "NOT_FOUND" /* NOT_FOUND */);
|
|
2292
|
+
}
|
|
2293
|
+
const possibleDirs = [
|
|
2294
|
+
joinPaths(projectRoot, "server", "database", "migrations"),
|
|
2295
|
+
joinPaths(projectRoot, "database", "migrations"),
|
|
2296
|
+
joinPaths(projectRoot, "migrations")
|
|
2297
|
+
];
|
|
2298
|
+
for (const dir of possibleDirs) {
|
|
2299
|
+
if (await fileExists(dir)) {
|
|
2300
|
+
return dir;
|
|
2301
|
+
}
|
|
2302
|
+
}
|
|
2303
|
+
return possibleDirs[0] ?? "";
|
|
2304
|
+
}
|
|
2305
|
+
async function getMigrationFiles(dir) {
|
|
2306
|
+
if (!await fileExists(dir)) {
|
|
2307
|
+
return [];
|
|
2308
|
+
}
|
|
2309
|
+
const files = await listFiles(dir, {
|
|
2310
|
+
recursive: false,
|
|
2311
|
+
pattern: /\.ts$/
|
|
2312
|
+
});
|
|
2313
|
+
return files.sort();
|
|
2314
|
+
}
|
|
2315
|
+
function parseMigrationFile(filename) {
|
|
2316
|
+
const match = filename.match(/^(\d+)_(.+)\.ts$/);
|
|
2317
|
+
if (!match || !match[1] || !match[2]) {
|
|
2318
|
+
return { id: filename, name: filename };
|
|
2319
|
+
}
|
|
2320
|
+
return { id: match[1], name: match[2] };
|
|
2321
|
+
}
|
|
2322
|
+
async function createMigration(name, dryRun) {
|
|
2323
|
+
const migrationsDir = await getMigrationsDir();
|
|
2324
|
+
const id = generateMigrationId2();
|
|
2325
|
+
const kebabName = name.toLowerCase().replace(/\s+/g, "-");
|
|
2326
|
+
const fileName = `${id}_${kebabName}.ts`;
|
|
2327
|
+
const filePath = joinPaths(migrationsDir, fileName);
|
|
2328
|
+
const template = `import { createMigration, type MigrationRunner } from 'bueno';
|
|
2329
|
+
|
|
2330
|
+
export default createMigration('${id}', '${kebabName}')
|
|
2331
|
+
.up(async (db: MigrationRunner) => {
|
|
2332
|
+
// TODO: Add migration logic
|
|
2333
|
+
// Example:
|
|
2334
|
+
// await db.createTable({
|
|
2335
|
+
// name: '${kebabName}',
|
|
2336
|
+
// columns: [
|
|
2337
|
+
// { name: 'id', type: 'uuid', primary: true },
|
|
2338
|
+
// { name: 'created_at', type: 'timestamp', default: 'NOW()' },
|
|
2339
|
+
// ],
|
|
2340
|
+
// });
|
|
2341
|
+
})
|
|
2342
|
+
.down(async (db: MigrationRunner) => {
|
|
2343
|
+
// TODO: Add rollback logic
|
|
2344
|
+
// Example:
|
|
2345
|
+
// await db.dropTable('${kebabName}');
|
|
2346
|
+
});
|
|
2347
|
+
`;
|
|
2348
|
+
if (dryRun) {
|
|
2349
|
+
cliConsole.log(`
|
|
2350
|
+
${colors.bold("File:")} ${filePath}`);
|
|
2351
|
+
cliConsole.log(colors.bold("Content:"));
|
|
2352
|
+
cliConsole.log(template);
|
|
2353
|
+
cliConsole.log("");
|
|
2354
|
+
return filePath;
|
|
2355
|
+
}
|
|
2356
|
+
await writeFile(filePath, template);
|
|
2357
|
+
return filePath;
|
|
2358
|
+
}
|
|
2359
|
+
async function showStatus() {
|
|
2360
|
+
const migrationsDir = await getMigrationsDir();
|
|
2361
|
+
const files = await getMigrationFiles(migrationsDir);
|
|
2362
|
+
if (files.length === 0) {
|
|
2363
|
+
cliConsole.info("No migrations found");
|
|
2364
|
+
return;
|
|
2365
|
+
}
|
|
2366
|
+
cliConsole.header("Migration Status");
|
|
2367
|
+
const rows = files.map((file) => {
|
|
2368
|
+
const info = parseMigrationFile(file.split("/").pop() ?? "");
|
|
2369
|
+
return [info.id, info.name, colors.yellow("Pending")];
|
|
2370
|
+
});
|
|
2371
|
+
printTable(["ID", "Name", "Status"], rows);
|
|
2372
|
+
cliConsole.log("");
|
|
2373
|
+
cliConsole.log(`Total: ${files.length} migration(s)`);
|
|
2374
|
+
}
|
|
2375
|
+
async function handleMigration(args) {
|
|
2376
|
+
const action = args.positionals[0];
|
|
2377
|
+
if (!action) {
|
|
2378
|
+
throw new CLIError("Action is required. Usage: bueno migration <action>", "INVALID_ARGS" /* INVALID_ARGS */);
|
|
2379
|
+
}
|
|
2380
|
+
const validActions = ["create", "up", "down", "reset", "refresh", "status"];
|
|
2381
|
+
if (!validActions.includes(action)) {
|
|
2382
|
+
throw new CLIError(`Unknown action: ${action}. Valid actions: ${validActions.join(", ")}`, "INVALID_ARGS" /* INVALID_ARGS */);
|
|
2383
|
+
}
|
|
2384
|
+
const dryRun = hasFlag(args, "dry-run");
|
|
2385
|
+
const steps = getOption(args, "steps", {
|
|
2386
|
+
name: "steps",
|
|
2387
|
+
alias: "n",
|
|
2388
|
+
type: "number",
|
|
2389
|
+
default: 1,
|
|
2390
|
+
description: ""
|
|
2391
|
+
});
|
|
2392
|
+
if (action !== "create" || !dryRun) {
|
|
2393
|
+
if (!await isBuenoProject()) {
|
|
2394
|
+
throw new CLIError("Not in a Bueno project directory. Run this command from a Bueno project.", "NOT_FOUND" /* NOT_FOUND */);
|
|
2395
|
+
}
|
|
2396
|
+
}
|
|
2397
|
+
switch (action) {
|
|
2398
|
+
case "create": {
|
|
2399
|
+
const name = args.positionals[1];
|
|
2400
|
+
if (!name) {
|
|
2401
|
+
throw new CLIError("Migration name is required. Usage: bueno migration create <name>", "INVALID_ARGS" /* INVALID_ARGS */);
|
|
2402
|
+
}
|
|
2403
|
+
const s = spinner(`Creating migration ${colors.cyan(name)}...`);
|
|
2404
|
+
try {
|
|
2405
|
+
const filePath = await createMigration(name, dryRun);
|
|
2406
|
+
if (dryRun) {
|
|
2407
|
+
s.info("Dry run complete");
|
|
2408
|
+
} else {
|
|
2409
|
+
s.success(`Created ${colors.green(filePath)}`);
|
|
2410
|
+
}
|
|
2411
|
+
} catch (error) {
|
|
2412
|
+
s.error();
|
|
2413
|
+
throw error;
|
|
2414
|
+
}
|
|
2415
|
+
break;
|
|
2416
|
+
}
|
|
2417
|
+
case "up": {
|
|
2418
|
+
cliConsole.info("Running pending migrations...");
|
|
2419
|
+
cliConsole.log("");
|
|
2420
|
+
cliConsole.warn("Migration execution requires database connection. Use the MigrationRunner in your application code.");
|
|
2421
|
+
cliConsole.log("");
|
|
2422
|
+
cliConsole.log("Example:");
|
|
2423
|
+
cliConsole.log(colors.cyan(`
|
|
2424
|
+
import { createMigrationRunner, loadMigrations } from 'bueno';
|
|
2425
|
+
import { db } from './database';
|
|
2426
|
+
|
|
2427
|
+
const runner = createMigrationRunner(db);
|
|
2428
|
+
const migrations = await loadMigrations('./database/migrations');
|
|
2429
|
+
await runner.migrate(migrations);
|
|
2430
|
+
`));
|
|
2431
|
+
break;
|
|
2432
|
+
}
|
|
2433
|
+
case "down": {
|
|
2434
|
+
cliConsole.info(`Rolling back ${steps} migration(s)...`);
|
|
2435
|
+
cliConsole.log("");
|
|
2436
|
+
cliConsole.warn("Migration rollback requires database connection. Use the MigrationRunner in your application code.");
|
|
2437
|
+
cliConsole.log("");
|
|
2438
|
+
cliConsole.log("Example:");
|
|
2439
|
+
cliConsole.log(colors.cyan(`
|
|
2440
|
+
import { createMigrationRunner, loadMigrations } from 'bueno';
|
|
2441
|
+
import { db } from './database';
|
|
2442
|
+
|
|
2443
|
+
const runner = createMigrationRunner(db);
|
|
2444
|
+
const migrations = await loadMigrations('./database/migrations');
|
|
2445
|
+
await runner.rollback(migrations, ${steps});
|
|
2446
|
+
`));
|
|
2447
|
+
break;
|
|
2448
|
+
}
|
|
2449
|
+
case "reset": {
|
|
2450
|
+
cliConsole.info("Rolling back all migrations...");
|
|
2451
|
+
cliConsole.log("");
|
|
2452
|
+
cliConsole.warn("Migration reset requires database connection. Use the MigrationRunner in your application code.");
|
|
2453
|
+
cliConsole.log("");
|
|
2454
|
+
cliConsole.log("Example:");
|
|
2455
|
+
cliConsole.log(colors.cyan(`
|
|
2456
|
+
import { createMigrationRunner, loadMigrations } from 'bueno';
|
|
2457
|
+
import { db } from './database';
|
|
2458
|
+
|
|
2459
|
+
const runner = createMigrationRunner(db);
|
|
2460
|
+
const migrations = await loadMigrations('./database/migrations');
|
|
2461
|
+
await runner.reset(migrations);
|
|
2462
|
+
`));
|
|
2463
|
+
break;
|
|
2464
|
+
}
|
|
2465
|
+
case "refresh": {
|
|
2466
|
+
cliConsole.info("Refreshing all migrations...");
|
|
2467
|
+
cliConsole.log("");
|
|
2468
|
+
cliConsole.warn("Migration refresh requires database connection. Use the MigrationRunner in your application code.");
|
|
2469
|
+
cliConsole.log("");
|
|
2470
|
+
cliConsole.log("Example:");
|
|
2471
|
+
cliConsole.log(colors.cyan(`
|
|
2472
|
+
import { createMigrationRunner, loadMigrations } from 'bueno';
|
|
2473
|
+
import { db } from './database';
|
|
2474
|
+
|
|
2475
|
+
const runner = createMigrationRunner(db);
|
|
2476
|
+
const migrations = await loadMigrations('./database/migrations');
|
|
2477
|
+
await runner.refresh(migrations);
|
|
2478
|
+
`));
|
|
2479
|
+
break;
|
|
2480
|
+
}
|
|
2481
|
+
case "status": {
|
|
2482
|
+
await showStatus();
|
|
2483
|
+
break;
|
|
2484
|
+
}
|
|
2485
|
+
}
|
|
2486
|
+
}
|
|
2487
|
+
defineCommand({
|
|
2488
|
+
name: "migration",
|
|
2489
|
+
description: "Manage database migrations",
|
|
2490
|
+
positionals: [
|
|
2491
|
+
{
|
|
2492
|
+
name: "action",
|
|
2493
|
+
required: true,
|
|
2494
|
+
description: "Action to perform (create, up, down, reset, refresh, status)"
|
|
2495
|
+
},
|
|
2496
|
+
{
|
|
2497
|
+
name: "name",
|
|
2498
|
+
required: false,
|
|
2499
|
+
description: "Migration name (required for create action)"
|
|
2500
|
+
}
|
|
2501
|
+
],
|
|
2502
|
+
options: [
|
|
2503
|
+
{
|
|
2504
|
+
name: "steps",
|
|
2505
|
+
alias: "n",
|
|
2506
|
+
type: "number",
|
|
2507
|
+
default: 1,
|
|
2508
|
+
description: "Number of migrations to rollback"
|
|
2509
|
+
},
|
|
2510
|
+
{
|
|
2511
|
+
name: "dry-run",
|
|
2512
|
+
type: "boolean",
|
|
2513
|
+
default: false,
|
|
2514
|
+
description: "Show what would happen without executing"
|
|
2515
|
+
}
|
|
2516
|
+
],
|
|
2517
|
+
examples: [
|
|
2518
|
+
"bueno migration create add-users-table",
|
|
2519
|
+
"bueno migration up",
|
|
2520
|
+
"bueno migration down --steps 3",
|
|
2521
|
+
"bueno migration reset",
|
|
2522
|
+
"bueno migration refresh",
|
|
2523
|
+
"bueno migration status"
|
|
2524
|
+
]
|
|
2525
|
+
}, handleMigration);
|
|
2526
|
+
|
|
2527
|
+
// src/cli/commands/dev.ts
|
|
2528
|
+
async function findEntryPoint(projectRoot) {
|
|
2529
|
+
const possibleEntries = [
|
|
2530
|
+
"server/main.ts",
|
|
2531
|
+
"src/main.ts",
|
|
2532
|
+
"src/index.ts",
|
|
2533
|
+
"main.ts",
|
|
2534
|
+
"index.ts",
|
|
2535
|
+
"server.ts",
|
|
2536
|
+
"app.ts"
|
|
2537
|
+
];
|
|
2538
|
+
for (const entry of possibleEntries) {
|
|
2539
|
+
const entryPath = joinPaths(projectRoot, entry);
|
|
2540
|
+
if (await fileExists(entryPath)) {
|
|
2541
|
+
return entry;
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
return null;
|
|
2545
|
+
}
|
|
2546
|
+
async function handleDev(args) {
|
|
2547
|
+
const port = getOption(args, "port", {
|
|
2548
|
+
name: "port",
|
|
2549
|
+
alias: "p",
|
|
2550
|
+
type: "number",
|
|
2551
|
+
default: 3000,
|
|
2552
|
+
description: ""
|
|
2553
|
+
});
|
|
2554
|
+
const host = getOption(args, "host", {
|
|
2555
|
+
name: "host",
|
|
2556
|
+
alias: "H",
|
|
2557
|
+
type: "string",
|
|
2558
|
+
default: "localhost",
|
|
2559
|
+
description: ""
|
|
2560
|
+
});
|
|
2561
|
+
const hmr = !hasFlag(args, "no-hmr");
|
|
2562
|
+
const watch = !hasFlag(args, "no-watch");
|
|
2563
|
+
const openBrowser = hasFlag(args, "open") || hasFlag(args, "o");
|
|
2564
|
+
const configPath = getOption(args, "config", {
|
|
2565
|
+
name: "config",
|
|
2566
|
+
alias: "c",
|
|
2567
|
+
type: "string",
|
|
2568
|
+
description: ""
|
|
2569
|
+
});
|
|
2570
|
+
const projectRoot = await getProjectRoot();
|
|
2571
|
+
if (!projectRoot) {
|
|
2572
|
+
throw new CLIError("Not in a project directory. Run this command from a Bueno project.", "NOT_FOUND" /* NOT_FOUND */);
|
|
2573
|
+
}
|
|
2574
|
+
if (!await isBuenoProject()) {
|
|
2575
|
+
throw new CLIError("Not a Bueno project. Make sure you have a bueno.config.ts or bueno in your dependencies.", "NOT_FOUND" /* NOT_FOUND */);
|
|
2576
|
+
}
|
|
2577
|
+
const entryPoint = await findEntryPoint(projectRoot);
|
|
2578
|
+
if (!entryPoint) {
|
|
2579
|
+
throw new CLIError("Could not find entry point. Make sure you have a main.ts or index.ts file.", "FILE_NOT_FOUND" /* FILE_NOT_FOUND */);
|
|
2580
|
+
}
|
|
2581
|
+
const bunArgs = [];
|
|
2582
|
+
if (watch) {
|
|
2583
|
+
bunArgs.push("--watch");
|
|
2584
|
+
}
|
|
2585
|
+
if (hmr) {
|
|
2586
|
+
cliConsole.debug("HMR enabled");
|
|
2587
|
+
}
|
|
2588
|
+
bunArgs.push(entryPoint);
|
|
2589
|
+
const env = {
|
|
2590
|
+
NODE_ENV: "development",
|
|
2591
|
+
PORT: String(port),
|
|
2592
|
+
HOST: host
|
|
2593
|
+
};
|
|
2594
|
+
if (configPath) {
|
|
2595
|
+
env.BUENO_CONFIG = configPath;
|
|
2596
|
+
}
|
|
2597
|
+
cliConsole.header("Starting Development Server");
|
|
2598
|
+
cliConsole.log(`${colors.bold("Entry:")} ${entryPoint}`);
|
|
2599
|
+
cliConsole.log(`${colors.bold("Port:")} ${port}`);
|
|
2600
|
+
cliConsole.log(`${colors.bold("Host:")} ${host}`);
|
|
2601
|
+
cliConsole.log(`${colors.bold("Watch:")} ${watch ? colors.green("enabled") : colors.red("disabled")}`);
|
|
2602
|
+
cliConsole.log(`${colors.bold("HMR:")} ${hmr ? colors.green("enabled") : colors.red("disabled")}`);
|
|
2603
|
+
cliConsole.log("");
|
|
2604
|
+
const s = spinner("Starting development server...");
|
|
2605
|
+
try {
|
|
2606
|
+
const proc = Bun.spawn(["bun", "run", ...bunArgs], {
|
|
2607
|
+
cwd: projectRoot,
|
|
2608
|
+
env: { ...process.env, ...env },
|
|
2609
|
+
stdout: "inherit",
|
|
2610
|
+
stderr: "inherit"
|
|
2611
|
+
});
|
|
2612
|
+
s.success(`Development server running at ${colors.cyan(`http://${host}:${port}`)}`);
|
|
2613
|
+
if (openBrowser) {
|
|
2614
|
+
const openCommand = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
2615
|
+
Bun.spawn([openCommand, `http://${host}:${port}`], {
|
|
2616
|
+
cwd: projectRoot
|
|
2617
|
+
});
|
|
2618
|
+
}
|
|
2619
|
+
const exitCode = await proc.exited;
|
|
2620
|
+
if (exitCode !== 0 && exitCode !== null) {
|
|
2621
|
+
cliConsole.error(`Server exited with code ${exitCode}`);
|
|
2622
|
+
process.exit(exitCode);
|
|
2623
|
+
}
|
|
2624
|
+
} catch (error) {
|
|
2625
|
+
s.error();
|
|
2626
|
+
throw error;
|
|
2627
|
+
}
|
|
2628
|
+
}
|
|
2629
|
+
defineCommand({
|
|
2630
|
+
name: "dev",
|
|
2631
|
+
description: "Start the development server with hot reload",
|
|
2632
|
+
options: [
|
|
2633
|
+
{
|
|
2634
|
+
name: "port",
|
|
2635
|
+
alias: "p",
|
|
2636
|
+
type: "number",
|
|
2637
|
+
default: 3000,
|
|
2638
|
+
description: "Server port"
|
|
2639
|
+
},
|
|
2640
|
+
{
|
|
2641
|
+
name: "host",
|
|
2642
|
+
alias: "H",
|
|
2643
|
+
type: "string",
|
|
2644
|
+
default: "localhost",
|
|
2645
|
+
description: "Server hostname"
|
|
2646
|
+
},
|
|
2647
|
+
{
|
|
2648
|
+
name: "no-hmr",
|
|
2649
|
+
type: "boolean",
|
|
2650
|
+
default: false,
|
|
2651
|
+
description: "Disable hot module replacement"
|
|
2652
|
+
},
|
|
2653
|
+
{
|
|
2654
|
+
name: "no-watch",
|
|
2655
|
+
type: "boolean",
|
|
2656
|
+
default: false,
|
|
2657
|
+
description: "Disable file watching"
|
|
2658
|
+
},
|
|
2659
|
+
{
|
|
2660
|
+
name: "open",
|
|
2661
|
+
alias: "o",
|
|
2662
|
+
type: "boolean",
|
|
2663
|
+
default: false,
|
|
2664
|
+
description: "Open browser on start"
|
|
2665
|
+
},
|
|
2666
|
+
{
|
|
2667
|
+
name: "config",
|
|
2668
|
+
alias: "c",
|
|
2669
|
+
type: "string",
|
|
2670
|
+
description: "Path to config file"
|
|
2671
|
+
}
|
|
2672
|
+
],
|
|
2673
|
+
examples: [
|
|
2674
|
+
"bueno dev",
|
|
2675
|
+
"bueno dev --port 4000",
|
|
2676
|
+
"bueno dev --host 0.0.0.0",
|
|
2677
|
+
"bueno dev --no-hmr",
|
|
2678
|
+
"bueno dev --open"
|
|
2679
|
+
]
|
|
2680
|
+
}, handleDev);
|
|
2681
|
+
|
|
2682
|
+
// src/cli/commands/build.ts
|
|
2683
|
+
async function findEntryPoint2(projectRoot) {
|
|
2684
|
+
const possibleEntries = [
|
|
2685
|
+
"server/main.ts",
|
|
2686
|
+
"src/main.ts",
|
|
2687
|
+
"src/index.ts",
|
|
2688
|
+
"main.ts",
|
|
2689
|
+
"index.ts",
|
|
2690
|
+
"server.ts",
|
|
2691
|
+
"app.ts"
|
|
2692
|
+
];
|
|
2693
|
+
for (const entry of possibleEntries) {
|
|
2694
|
+
const entryPath = joinPaths(projectRoot, entry);
|
|
2695
|
+
if (await fileExists(entryPath)) {
|
|
2696
|
+
return entry;
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
return null;
|
|
2700
|
+
}
|
|
2701
|
+
async function handleBuild(args) {
|
|
2702
|
+
const target = getOption(args, "target", {
|
|
2703
|
+
name: "target",
|
|
2704
|
+
alias: "t",
|
|
2705
|
+
type: "string",
|
|
2706
|
+
default: "bun",
|
|
2707
|
+
description: ""
|
|
2708
|
+
});
|
|
2709
|
+
const outDir = getOption(args, "outdir", {
|
|
2710
|
+
name: "outdir",
|
|
2711
|
+
alias: "o",
|
|
2712
|
+
type: "string",
|
|
2713
|
+
default: "./dist",
|
|
2714
|
+
description: ""
|
|
2715
|
+
});
|
|
2716
|
+
const minify = !hasFlag(args, "no-minify");
|
|
2717
|
+
const sourcemap = hasFlag(args, "sourcemap");
|
|
2718
|
+
const analyze = hasFlag(args, "analyze");
|
|
2719
|
+
const configPath = getOption(args, "config", {
|
|
2720
|
+
name: "config",
|
|
2721
|
+
alias: "c",
|
|
2722
|
+
type: "string",
|
|
2723
|
+
description: ""
|
|
2724
|
+
});
|
|
2725
|
+
const compile = hasFlag(args, "compile");
|
|
2726
|
+
const crossCompile = getOption(args, "cross-compile", {
|
|
2727
|
+
name: "cross-compile",
|
|
2728
|
+
type: "string",
|
|
2729
|
+
description: ""
|
|
2730
|
+
});
|
|
2731
|
+
const executableName = getOption(args, "executable-name", {
|
|
2732
|
+
name: "executable-name",
|
|
2733
|
+
type: "string",
|
|
2734
|
+
default: "main",
|
|
2735
|
+
description: ""
|
|
2736
|
+
});
|
|
2737
|
+
if (crossCompile) {
|
|
2738
|
+
const validCrossCompileTargets = [
|
|
2739
|
+
"linux-x64",
|
|
2740
|
+
"linux-arm64",
|
|
2741
|
+
"windows-x64",
|
|
2742
|
+
"darwin-x64",
|
|
2743
|
+
"darwin-arm64"
|
|
2744
|
+
];
|
|
2745
|
+
if (!validCrossCompileTargets.includes(crossCompile)) {
|
|
2746
|
+
throw new CLIError(`Invalid cross-compile target: ${crossCompile}. Valid targets: ${validCrossCompileTargets.join(", ")}`, "INVALID_ARGS" /* INVALID_ARGS */);
|
|
2747
|
+
}
|
|
2748
|
+
}
|
|
2749
|
+
if (crossCompile && !compile) {
|
|
2750
|
+
throw new CLIError("--cross-compile requires --compile flag", "INVALID_ARGS" /* INVALID_ARGS */);
|
|
2751
|
+
}
|
|
2752
|
+
const validTargets = ["bun", "node", "standalone"];
|
|
2753
|
+
if (!validTargets.includes(target)) {
|
|
2754
|
+
throw new CLIError(`Invalid target: ${target}. Valid targets: ${validTargets.join(", ")}`, "INVALID_ARGS" /* INVALID_ARGS */);
|
|
2755
|
+
}
|
|
2756
|
+
const projectRoot = await getProjectRoot();
|
|
2757
|
+
if (!projectRoot) {
|
|
2758
|
+
throw new CLIError("Not in a project directory. Run this command from a Bueno project.", "NOT_FOUND" /* NOT_FOUND */);
|
|
2759
|
+
}
|
|
2760
|
+
if (!await isBuenoProject()) {
|
|
2761
|
+
throw new CLIError("Not a Bueno project. Make sure you have a bueno.config.ts or bueno in your dependencies.", "NOT_FOUND" /* NOT_FOUND */);
|
|
2762
|
+
}
|
|
2763
|
+
const entryPoint = await findEntryPoint2(projectRoot);
|
|
2764
|
+
if (!entryPoint) {
|
|
2765
|
+
throw new CLIError("Could not find entry point. Make sure you have a main.ts or index.ts file.", "FILE_NOT_FOUND" /* FILE_NOT_FOUND */);
|
|
2766
|
+
}
|
|
2767
|
+
if (compile) {
|
|
2768
|
+
cliConsole.header("Compiling Single-File Executable");
|
|
2769
|
+
} else {
|
|
2770
|
+
cliConsole.header("Building for Production");
|
|
2771
|
+
}
|
|
2772
|
+
cliConsole.log(`${colors.bold("Entry:")} ${entryPoint}`);
|
|
2773
|
+
cliConsole.log(`${colors.bold("Target:")} ${target}`);
|
|
2774
|
+
if (compile) {
|
|
2775
|
+
cliConsole.log(`${colors.bold("Compile:")} ${colors.green("enabled")}`);
|
|
2776
|
+
if (crossCompile) {
|
|
2777
|
+
cliConsole.log(`${colors.bold("Cross-compile:")} ${colors.cyan(crossCompile)}`);
|
|
2778
|
+
}
|
|
2779
|
+
cliConsole.log(`${colors.bold("Executable:")} ${executableName}`);
|
|
2780
|
+
}
|
|
2781
|
+
cliConsole.log(`${colors.bold("Output:")} ${outDir}`);
|
|
2782
|
+
cliConsole.log(`${colors.bold("Minify:")} ${minify ? colors.green("enabled") : colors.red("disabled")}`);
|
|
2783
|
+
cliConsole.log(`${colors.bold("Sourcemap:")} ${sourcemap ? colors.green("enabled") : colors.red("disabled")}`);
|
|
2784
|
+
cliConsole.log("");
|
|
2785
|
+
const startTime = Date.now();
|
|
2786
|
+
const s = spinner("Building...");
|
|
2787
|
+
try {
|
|
2788
|
+
const fullOutDir = joinPaths(projectRoot, outDir);
|
|
2789
|
+
if (await fileExists(fullOutDir)) {
|
|
2790
|
+
await deleteDirectory(fullOutDir);
|
|
2791
|
+
}
|
|
2792
|
+
if (compile) {
|
|
2793
|
+
const isWindows = crossCompile === "windows-x64";
|
|
2794
|
+
const executableFileName = isWindows ? `${executableName}.exe` : executableName;
|
|
2795
|
+
const executablePath = joinPaths(fullOutDir, executableFileName);
|
|
2796
|
+
const buildOptions = {
|
|
2797
|
+
entrypoints: [joinPaths(projectRoot, entryPoint)],
|
|
2798
|
+
outdir: fullOutDir,
|
|
2799
|
+
target: crossCompile || "bun",
|
|
2800
|
+
minify,
|
|
2801
|
+
sourcemap: sourcemap ? "external" : undefined,
|
|
2802
|
+
naming: executableFileName,
|
|
2803
|
+
compile: true,
|
|
2804
|
+
define: {
|
|
2805
|
+
"process.env.NODE_ENV": '"production"'
|
|
2806
|
+
}
|
|
2807
|
+
};
|
|
2808
|
+
const buildResult2 = await Bun.build(buildOptions);
|
|
2809
|
+
if (!buildResult2.success) {
|
|
2810
|
+
s.error();
|
|
2811
|
+
for (const error of buildResult2.logs) {
|
|
2812
|
+
cliConsole.error(error.message);
|
|
2813
|
+
}
|
|
2814
|
+
throw new CLIError("Compile failed", "TEMPLATE_ERROR" /* TEMPLATE_ERROR */);
|
|
2815
|
+
}
|
|
2816
|
+
const elapsed2 = Date.now() - startTime;
|
|
2817
|
+
s.success(`Compile completed in ${formatDuration(elapsed2)}`);
|
|
2818
|
+
cliConsole.log("");
|
|
2819
|
+
cliConsole.log(`${colors.bold("Output Executable:")}`);
|
|
2820
|
+
const fs2 = __require("fs");
|
|
2821
|
+
let executableSize = 0;
|
|
2822
|
+
try {
|
|
2823
|
+
const stat = fs2.statSync(executablePath);
|
|
2824
|
+
executableSize = stat.size;
|
|
2825
|
+
} catch (e) {
|
|
2826
|
+
if (buildResult2.outputs.length > 0) {
|
|
2827
|
+
executableSize = buildResult2.outputs[0].size;
|
|
2828
|
+
}
|
|
2829
|
+
}
|
|
2830
|
+
cliConsole.log(` ${colors.cyan(executablePath.replace(projectRoot, "."))} ${colors.dim(`(${formatSize(executableSize)})`)}`);
|
|
2831
|
+
cliConsole.log("");
|
|
2832
|
+
cliConsole.success("Single-file executable created successfully!");
|
|
2833
|
+
if (crossCompile) {
|
|
2834
|
+
cliConsole.log(`${colors.bold("Target Platform:")} ${crossCompile}`);
|
|
2835
|
+
}
|
|
2836
|
+
cliConsole.log("");
|
|
2837
|
+
cliConsole.info("You can run the executable directly:");
|
|
2838
|
+
if (isWindows) {
|
|
2839
|
+
cliConsole.log(colors.cyan(` .${outDir}/${executableFileName}`));
|
|
2840
|
+
} else {
|
|
2841
|
+
cliConsole.log(colors.cyan(` .${outDir}/${executableFileName}`));
|
|
2842
|
+
}
|
|
2843
|
+
if (analyze) {
|
|
2844
|
+
cliConsole.log("");
|
|
2845
|
+
cliConsole.header("Bundle Analysis");
|
|
2846
|
+
cliConsole.log("Output:");
|
|
2847
|
+
for (const entry of buildResult2.outputs) {
|
|
2848
|
+
cliConsole.log(` ${entry.path} (${formatSize(entry.size)})`);
|
|
2849
|
+
}
|
|
2850
|
+
}
|
|
2851
|
+
return;
|
|
2852
|
+
}
|
|
2853
|
+
const buildResult = await Bun.build({
|
|
2854
|
+
entrypoints: [joinPaths(projectRoot, entryPoint)],
|
|
2855
|
+
outdir: fullOutDir,
|
|
2856
|
+
target: target === "node" ? "node" : "bun",
|
|
2857
|
+
minify,
|
|
2858
|
+
sourcemap: sourcemap ? "external" : undefined,
|
|
2859
|
+
splitting: true,
|
|
2860
|
+
format: "esm",
|
|
2861
|
+
external: target === "standalone" ? [] : ["bun:*"],
|
|
2862
|
+
define: {
|
|
2863
|
+
"process.env.NODE_ENV": '"production"'
|
|
2864
|
+
}
|
|
2865
|
+
});
|
|
2866
|
+
if (!buildResult.success) {
|
|
2867
|
+
s.error();
|
|
2868
|
+
for (const error of buildResult.logs) {
|
|
2869
|
+
cliConsole.error(error.message);
|
|
2870
|
+
}
|
|
2871
|
+
throw new CLIError("Build failed", "TEMPLATE_ERROR" /* TEMPLATE_ERROR */);
|
|
2872
|
+
}
|
|
2873
|
+
const elapsed = Date.now() - startTime;
|
|
2874
|
+
const outputFiles = await listFiles(fullOutDir, { recursive: true });
|
|
2875
|
+
const totalSize = outputFiles.reduce((acc, file) => {
|
|
2876
|
+
const stat = __require("fs").statSync(file);
|
|
2877
|
+
return acc + stat.size;
|
|
2878
|
+
}, 0);
|
|
2879
|
+
s.success(`Build completed in ${formatDuration(elapsed)}`);
|
|
2880
|
+
cliConsole.log("");
|
|
2881
|
+
cliConsole.log(`${colors.bold("Output Files:")}`);
|
|
2882
|
+
for (const file of outputFiles.slice(0, 10)) {
|
|
2883
|
+
const stat = __require("fs").statSync(file);
|
|
2884
|
+
const relativePath = file.replace(projectRoot, ".");
|
|
2885
|
+
cliConsole.log(` ${colors.dim(relativePath)} ${colors.dim(`(${formatSize(stat.size)})`)}`);
|
|
2886
|
+
}
|
|
2887
|
+
if (outputFiles.length > 10) {
|
|
2888
|
+
cliConsole.log(` ${colors.dim(`... and ${outputFiles.length - 10} more files`)}`);
|
|
2889
|
+
}
|
|
2890
|
+
cliConsole.log("");
|
|
2891
|
+
cliConsole.log(`${colors.bold("Total Size:")} ${formatSize(totalSize)}`);
|
|
2892
|
+
if (analyze) {
|
|
2893
|
+
cliConsole.log("");
|
|
2894
|
+
cliConsole.header("Bundle Analysis");
|
|
2895
|
+
cliConsole.log("Entry points:");
|
|
2896
|
+
for (const entry of buildResult.outputs) {
|
|
2897
|
+
cliConsole.log(` ${entry.path} (${formatSize(entry.size)})`);
|
|
2898
|
+
}
|
|
2899
|
+
}
|
|
2900
|
+
if (target === "standalone") {
|
|
2901
|
+
cliConsole.log("");
|
|
2902
|
+
cliConsole.info("Standalone bundle created. You can run it with:");
|
|
2903
|
+
cliConsole.log(colors.cyan(` bun .${outDir}/${entryPoint.replace(".ts", ".js")}`));
|
|
2904
|
+
}
|
|
2905
|
+
} catch (error) {
|
|
2906
|
+
s.error();
|
|
2907
|
+
throw error;
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
defineCommand({
|
|
2911
|
+
name: "build",
|
|
2912
|
+
description: "Build the application for production",
|
|
2913
|
+
options: [
|
|
2914
|
+
{
|
|
2915
|
+
name: "target",
|
|
2916
|
+
alias: "t",
|
|
2917
|
+
type: "string",
|
|
2918
|
+
default: "bun",
|
|
2919
|
+
description: "Build target (bun, node, standalone)"
|
|
2920
|
+
},
|
|
2921
|
+
{
|
|
2922
|
+
name: "outdir",
|
|
2923
|
+
alias: "o",
|
|
2924
|
+
type: "string",
|
|
2925
|
+
default: "./dist",
|
|
2926
|
+
description: "Output directory"
|
|
2927
|
+
},
|
|
2928
|
+
{
|
|
2929
|
+
name: "no-minify",
|
|
2930
|
+
type: "boolean",
|
|
2931
|
+
default: false,
|
|
2932
|
+
description: "Disable minification"
|
|
2933
|
+
},
|
|
2934
|
+
{
|
|
2935
|
+
name: "sourcemap",
|
|
2936
|
+
type: "boolean",
|
|
2937
|
+
default: false,
|
|
2938
|
+
description: "Generate source maps"
|
|
2939
|
+
},
|
|
2940
|
+
{
|
|
2941
|
+
name: "analyze",
|
|
2942
|
+
type: "boolean",
|
|
2943
|
+
default: false,
|
|
2944
|
+
description: "Analyze bundle size"
|
|
2945
|
+
},
|
|
2946
|
+
{
|
|
2947
|
+
name: "config",
|
|
2948
|
+
alias: "c",
|
|
2949
|
+
type: "string",
|
|
2950
|
+
description: "Path to config file"
|
|
2951
|
+
},
|
|
2952
|
+
{
|
|
2953
|
+
name: "compile",
|
|
2954
|
+
type: "boolean",
|
|
2955
|
+
default: false,
|
|
2956
|
+
description: "Create a single-file executable using Bun compile"
|
|
2957
|
+
},
|
|
2958
|
+
{
|
|
2959
|
+
name: "cross-compile",
|
|
2960
|
+
type: "string",
|
|
2961
|
+
description: "Cross-compile for different platforms (linux-x64, linux-arm64, windows-x64, darwin-x64, darwin-arm64)"
|
|
2962
|
+
},
|
|
2963
|
+
{
|
|
2964
|
+
name: "executable-name",
|
|
2965
|
+
type: "string",
|
|
2966
|
+
default: "main",
|
|
2967
|
+
description: "Custom name for the output executable (default: main)"
|
|
2968
|
+
}
|
|
2969
|
+
],
|
|
2970
|
+
examples: [
|
|
2971
|
+
"bueno build",
|
|
2972
|
+
"bueno build --target node",
|
|
2973
|
+
"bueno build --target standalone",
|
|
2974
|
+
"bueno build --sourcemap",
|
|
2975
|
+
"bueno build --analyze",
|
|
2976
|
+
"bueno build --compile",
|
|
2977
|
+
"bueno build --compile --outdir ./bin",
|
|
2978
|
+
"bueno build --compile --cross-compile linux-x64",
|
|
2979
|
+
"bueno build --compile --executable-name myapp"
|
|
2980
|
+
]
|
|
2981
|
+
}, handleBuild);
|
|
2982
|
+
|
|
2983
|
+
// src/cli/commands/start.ts
|
|
2984
|
+
async function findEntryPoint3(projectRoot) {
|
|
2985
|
+
const possibleBuiltEntries = [
|
|
2986
|
+
"dist/index.js",
|
|
2987
|
+
"dist/main.js",
|
|
2988
|
+
"dist/server.js",
|
|
2989
|
+
"dist/app.js"
|
|
2990
|
+
];
|
|
2991
|
+
for (const entry of possibleBuiltEntries) {
|
|
2992
|
+
const entryPath = joinPaths(projectRoot, entry);
|
|
2993
|
+
if (await fileExists(entryPath)) {
|
|
2994
|
+
return entry;
|
|
2995
|
+
}
|
|
2996
|
+
}
|
|
2997
|
+
const possibleSourceEntries = [
|
|
2998
|
+
"server/main.ts",
|
|
2999
|
+
"src/main.ts",
|
|
3000
|
+
"src/index.ts",
|
|
3001
|
+
"main.ts",
|
|
3002
|
+
"index.ts",
|
|
3003
|
+
"server.ts",
|
|
3004
|
+
"app.ts"
|
|
3005
|
+
];
|
|
3006
|
+
for (const entry of possibleSourceEntries) {
|
|
3007
|
+
const entryPath = joinPaths(projectRoot, entry);
|
|
3008
|
+
if (await fileExists(entryPath)) {
|
|
3009
|
+
return entry;
|
|
3010
|
+
}
|
|
3011
|
+
}
|
|
3012
|
+
return null;
|
|
3013
|
+
}
|
|
3014
|
+
async function handleStart(args) {
|
|
3015
|
+
const port = getOption(args, "port", {
|
|
3016
|
+
name: "port",
|
|
3017
|
+
alias: "p",
|
|
3018
|
+
type: "number",
|
|
3019
|
+
default: 3000,
|
|
3020
|
+
description: ""
|
|
3021
|
+
});
|
|
3022
|
+
const host = getOption(args, "host", {
|
|
3023
|
+
name: "host",
|
|
3024
|
+
alias: "H",
|
|
3025
|
+
type: "string",
|
|
3026
|
+
default: "0.0.0.0",
|
|
3027
|
+
description: ""
|
|
3028
|
+
});
|
|
3029
|
+
const workers = getOption(args, "workers", {
|
|
3030
|
+
name: "workers",
|
|
3031
|
+
alias: "w",
|
|
3032
|
+
type: "string",
|
|
3033
|
+
default: "auto",
|
|
3034
|
+
description: ""
|
|
3035
|
+
});
|
|
3036
|
+
const configPath = getOption(args, "config", {
|
|
3037
|
+
name: "config",
|
|
3038
|
+
alias: "c",
|
|
3039
|
+
type: "string",
|
|
3040
|
+
description: ""
|
|
3041
|
+
});
|
|
3042
|
+
const projectRoot = await getProjectRoot();
|
|
3043
|
+
if (!projectRoot) {
|
|
3044
|
+
throw new CLIError("Not in a project directory. Run this command from a Bueno project.", "NOT_FOUND" /* NOT_FOUND */);
|
|
3045
|
+
}
|
|
3046
|
+
if (!await isBuenoProject()) {
|
|
3047
|
+
throw new CLIError("Not a Bueno project. Make sure you have a bueno.config.ts or bueno in your dependencies.", "NOT_FOUND" /* NOT_FOUND */);
|
|
3048
|
+
}
|
|
3049
|
+
const entryPoint = await findEntryPoint3(projectRoot);
|
|
3050
|
+
if (!entryPoint) {
|
|
3051
|
+
throw new CLIError("Could not find entry point. Make sure you have built the application or have a main.ts file.", "FILE_NOT_FOUND" /* FILE_NOT_FOUND */);
|
|
3052
|
+
}
|
|
3053
|
+
cliConsole.header("Starting Production Server");
|
|
3054
|
+
cliConsole.log(`${colors.bold("Entry:")} ${entryPoint}`);
|
|
3055
|
+
cliConsole.log(`${colors.bold("Port:")} ${port}`);
|
|
3056
|
+
cliConsole.log(`${colors.bold("Host:")} ${host}`);
|
|
3057
|
+
cliConsole.log(`${colors.bold("Workers:")} ${workers}`);
|
|
3058
|
+
cliConsole.log("");
|
|
3059
|
+
const env = {
|
|
3060
|
+
NODE_ENV: "production",
|
|
3061
|
+
PORT: String(port),
|
|
3062
|
+
HOST: host
|
|
3063
|
+
};
|
|
3064
|
+
if (configPath) {
|
|
3065
|
+
env.BUENO_CONFIG = configPath;
|
|
3066
|
+
}
|
|
3067
|
+
const s = spinner("Starting production server...");
|
|
3068
|
+
try {
|
|
3069
|
+
const proc = Bun.spawn(["bun", "run", entryPoint], {
|
|
3070
|
+
cwd: projectRoot,
|
|
3071
|
+
env: { ...process.env, ...env },
|
|
3072
|
+
stdout: "inherit",
|
|
3073
|
+
stderr: "inherit"
|
|
3074
|
+
});
|
|
3075
|
+
s.success(`Production server running at ${colors.cyan(`http://${host}:${port}`)}`);
|
|
3076
|
+
const exitCode = await proc.exited;
|
|
3077
|
+
if (exitCode !== 0 && exitCode !== null) {
|
|
3078
|
+
cliConsole.error(`Server exited with code ${exitCode}`);
|
|
3079
|
+
process.exit(exitCode);
|
|
3080
|
+
}
|
|
3081
|
+
} catch (error) {
|
|
3082
|
+
s.error();
|
|
3083
|
+
throw error;
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
defineCommand({
|
|
3087
|
+
name: "start",
|
|
3088
|
+
description: "Start the production server",
|
|
3089
|
+
options: [
|
|
3090
|
+
{
|
|
3091
|
+
name: "port",
|
|
3092
|
+
alias: "p",
|
|
3093
|
+
type: "number",
|
|
3094
|
+
default: 3000,
|
|
3095
|
+
description: "Server port"
|
|
3096
|
+
},
|
|
3097
|
+
{
|
|
3098
|
+
name: "host",
|
|
3099
|
+
alias: "H",
|
|
3100
|
+
type: "string",
|
|
3101
|
+
default: "0.0.0.0",
|
|
3102
|
+
description: "Server hostname"
|
|
3103
|
+
},
|
|
3104
|
+
{
|
|
3105
|
+
name: "workers",
|
|
3106
|
+
alias: "w",
|
|
3107
|
+
type: "string",
|
|
3108
|
+
default: "auto",
|
|
3109
|
+
description: "Number of worker threads"
|
|
3110
|
+
},
|
|
3111
|
+
{
|
|
3112
|
+
name: "config",
|
|
3113
|
+
alias: "c",
|
|
3114
|
+
type: "string",
|
|
3115
|
+
description: "Path to config file"
|
|
3116
|
+
}
|
|
3117
|
+
],
|
|
3118
|
+
examples: [
|
|
3119
|
+
"bueno start",
|
|
3120
|
+
"bueno start --port 8080",
|
|
3121
|
+
"bueno start --host 0.0.0.0",
|
|
3122
|
+
"bueno start --workers 4"
|
|
3123
|
+
]
|
|
3124
|
+
}, handleStart);
|
|
3125
|
+
|
|
3126
|
+
// src/cli/commands/help.ts
|
|
3127
|
+
defineCommand({
|
|
3128
|
+
name: "help",
|
|
3129
|
+
description: "Show help information for commands",
|
|
3130
|
+
positionals: [
|
|
3131
|
+
{
|
|
3132
|
+
name: "command",
|
|
3133
|
+
required: false,
|
|
3134
|
+
description: "Command to show help for"
|
|
3135
|
+
}
|
|
3136
|
+
],
|
|
3137
|
+
options: [
|
|
3138
|
+
{
|
|
3139
|
+
name: "all",
|
|
3140
|
+
alias: "a",
|
|
3141
|
+
type: "boolean",
|
|
3142
|
+
default: false,
|
|
3143
|
+
description: "Show help for all commands"
|
|
3144
|
+
}
|
|
3145
|
+
]
|
|
3146
|
+
}, async (args) => {
|
|
3147
|
+
const commandName = args.positionals[0];
|
|
3148
|
+
if (commandName && registry.has(commandName)) {
|
|
3149
|
+
const cmd = registry.get(commandName);
|
|
3150
|
+
if (cmd) {
|
|
3151
|
+
cliConsole.log(generateHelpText(cmd.definition));
|
|
3152
|
+
}
|
|
3153
|
+
} else if (hasFlag(args, "all")) {
|
|
3154
|
+
cliConsole.log(`
|
|
3155
|
+
Bueno CLI - Available Commands
|
|
3156
|
+
`);
|
|
3157
|
+
for (const cmd of registry.getAll()) {
|
|
3158
|
+
cliConsole.log(generateHelpText(cmd));
|
|
3159
|
+
cliConsole.log("---");
|
|
3160
|
+
}
|
|
3161
|
+
} else {
|
|
3162
|
+
cliConsole.log(generateGlobalHelpText(registry.getAll()));
|
|
3163
|
+
}
|
|
3164
|
+
});
|
|
3165
|
+
// src/cli/index.ts
|
|
3166
|
+
var VERSION = "0.1.0";
|
|
3167
|
+
class CLIError extends Error {
|
|
3168
|
+
type;
|
|
3169
|
+
exitCode;
|
|
3170
|
+
constructor(message, type, exitCode = 1) {
|
|
3171
|
+
super(message);
|
|
3172
|
+
this.type = type;
|
|
3173
|
+
this.exitCode = exitCode;
|
|
3174
|
+
this.name = "CLIError";
|
|
3175
|
+
}
|
|
3176
|
+
}
|
|
3177
|
+
async function run(argv = process.argv.slice(2)) {
|
|
3178
|
+
const args = parseArgs(argv);
|
|
3179
|
+
if (hasFlag(args, "no-color")) {
|
|
3180
|
+
setColorEnabled(false);
|
|
3181
|
+
}
|
|
3182
|
+
if (hasFlag(args, "verbose")) {
|
|
3183
|
+
process.env.BUENO_VERBOSE = "true";
|
|
3184
|
+
}
|
|
3185
|
+
if (hasFlag(args, "quiet")) {
|
|
3186
|
+
process.env.BUENO_QUIET = "true";
|
|
3187
|
+
}
|
|
3188
|
+
if (hasFlag(args, "version") || hasFlag(args, "v")) {
|
|
3189
|
+
cliConsole.log(`bueno v${VERSION}`);
|
|
3190
|
+
process.exit(0);
|
|
3191
|
+
}
|
|
3192
|
+
if (!args.command || hasFlag(args, "help") || hasFlag(args, "h")) {
|
|
3193
|
+
if (args.command && registry.has(args.command)) {
|
|
3194
|
+
const cmd = registry.get(args.command);
|
|
3195
|
+
if (cmd) {
|
|
3196
|
+
cliConsole.log(generateHelpText(cmd.definition));
|
|
3197
|
+
}
|
|
3198
|
+
} else {
|
|
3199
|
+
cliConsole.log(generateGlobalHelpText(registry.getAll()));
|
|
3200
|
+
}
|
|
3201
|
+
process.exit(0);
|
|
3202
|
+
}
|
|
3203
|
+
try {
|
|
3204
|
+
await registry.execute(args.command, args);
|
|
3205
|
+
} catch (error) {
|
|
3206
|
+
if (error instanceof CLIError) {
|
|
3207
|
+
cliConsole.error(error.message);
|
|
3208
|
+
process.exit(error.exitCode);
|
|
3209
|
+
}
|
|
3210
|
+
if (error instanceof Error) {
|
|
3211
|
+
if (process.env.BUENO_VERBOSE === "true") {
|
|
3212
|
+
cliConsole.error(error.stack ?? error.message);
|
|
3213
|
+
} else {
|
|
3214
|
+
cliConsole.error(error.message);
|
|
3215
|
+
}
|
|
3216
|
+
}
|
|
3217
|
+
process.exit(1);
|
|
3218
|
+
}
|
|
3219
|
+
}
|
|
3220
|
+
async function main() {
|
|
3221
|
+
process.on("SIGINT", () => {
|
|
3222
|
+
cliConsole.newline();
|
|
3223
|
+
process.exit(130);
|
|
3224
|
+
});
|
|
3225
|
+
process.on("unhandledRejection", (reason) => {
|
|
3226
|
+
cliConsole.error("Unhandled rejection:", reason);
|
|
3227
|
+
process.exit(1);
|
|
3228
|
+
});
|
|
3229
|
+
await run();
|
|
3230
|
+
}
|
|
3231
|
+
|
|
3232
|
+
// src/cli/bin.ts
|
|
3233
|
+
main();
|