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