@seip/blue-bird 0.7.5 → 0.7.6

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/core/cli/init.js CHANGED
@@ -12,220 +12,318 @@ import { execSync } from "node:child_process";
12
12
  * Initializes a new Blue Bird project by copying the base structure.
13
13
  */
14
14
  class ProjectInit {
15
- constructor() {
16
- this.appDir = process.cwd();
17
- this.sourceDir = path.resolve(import.meta.dirname, "../../");
15
+ constructor() {
16
+ this.appDir = process.cwd();
17
+ this.sourceDir = path.resolve(import.meta.dirname, "../../");
18
+ }
19
+
20
+ /**
21
+ * Runs the project initialization process.
22
+ */
23
+ async run() {
24
+ console.log(chalk.cyan("Starting Blue Bird project initialization..."));
25
+
26
+ const rl = readline.createInterface({ input, output });
27
+
28
+ let title = "Blue-Bird";
29
+ let port = 3000;
30
+ let appUrl = "http://localhost:3000";
31
+ let dbType = "none";
32
+ let dbName = "blue_bird";
33
+ let dbUser = "root";
34
+ let dbPassword = "root";
35
+ let dbPort = 3306;
36
+
37
+ try {
38
+ const ask = async (query, defaultValue) => {
39
+ const formattedQuery =
40
+ defaultValue !== undefined
41
+ ? `${query} [${defaultValue}]: `
42
+ : `${query}: `;
43
+ const answer = await rl.question(formattedQuery);
44
+ return answer.trim() || defaultValue;
45
+ };
46
+
47
+ title = await ask("Project Title", title);
48
+ const portInput = await ask("Server Port", port);
49
+ port = parseInt(portInput, 10);
50
+ if (Number.isNaN(port)) {
51
+ port = 3000;
52
+ }
53
+
54
+ const defaultAppUrl = `http://localhost:${port}`;
55
+ appUrl = await ask("Application URL", defaultAppUrl);
56
+
57
+ const dbTypeAns = await ask(
58
+ "Which database do you want to configure? (none / mysql / postgres)",
59
+ "none",
60
+ );
61
+ const cleanDbTypeAns = dbTypeAns.toLowerCase().trim();
62
+ if (
63
+ cleanDbTypeAns === "postgres" ||
64
+ cleanDbTypeAns === "postgresql" ||
65
+ cleanDbTypeAns === "pg" ||
66
+ cleanDbTypeAns === "postgr"
67
+ ) {
68
+ dbType = "postgres";
69
+ } else if (cleanDbTypeAns === "mysql") {
70
+ dbType = "mysql";
71
+ } else {
72
+ dbType = "none";
73
+ }
74
+
75
+ if (dbType !== "none") {
76
+ dbName = await ask("Database Name", dbName);
77
+ dbUser = await ask(
78
+ "Database User",
79
+ dbType === "postgres" ? "postgres" : "root",
80
+ );
81
+ dbPassword = await ask("Database Password", dbPassword);
82
+ const defaultDbPort = dbType === "postgres" ? 5432 : 3306;
83
+ const dbPortInput = await ask("Database Port", defaultDbPort);
84
+ dbPort = parseInt(dbPortInput, 10);
85
+ if (Number.isNaN(dbPort)) {
86
+ dbPort = defaultDbPort;
87
+ }
88
+ }
89
+ } catch (error) {
90
+ console.error(
91
+ chalk.red("[ERROR] Error reading configuration input:"),
92
+ error.message,
93
+ );
94
+ rl.close();
95
+ return;
96
+ } finally {
97
+ rl.close();
18
98
  }
19
99
 
20
- /**
21
- * Runs the project initialization process.
22
- */
23
- async run() {
24
- console.log(chalk.cyan("Starting Blue Bird project initialization..."));
25
-
26
- const rl = readline.createInterface({ input, output });
27
-
28
- let title = "Blue-Bird";
29
- let port = 3000;
30
- let appUrl = "http://localhost:3000";
31
- let useMysql = false;
32
- let dbName = "blue_bird";
33
- let dbUser = "root";
34
- let dbPassword = "root";
35
- let dbPort = 3306;
36
-
37
- try {
38
- const ask = async (query, defaultValue) => {
39
- const formattedQuery = defaultValue !== undefined ? `${query} [${defaultValue}]: ` : `${query}: `;
40
- const answer = await rl.question(formattedQuery);
41
- return answer.trim() || defaultValue;
42
- };
43
-
44
- title = await ask("Project Title", title);
45
- const portInput = await ask("Server Port", port);
46
- port = parseInt(portInput, 10);
47
- if (Number.isNaN(port)) {
48
- port = 3000;
49
- }
50
-
51
- const defaultAppUrl = `http://localhost:${port}`;
52
- appUrl = await ask("Application URL", defaultAppUrl);
53
-
54
- const mysqlAns = await ask("Do you want to configure MySQL? (y/n)", "n");
55
- useMysql = mysqlAns.toLowerCase() === "y" || mysqlAns.toLowerCase() === "yes";
56
-
57
- if (useMysql) {
58
- dbName = await ask("Database Name", dbName);
59
- dbUser = await ask("Database User", dbUser);
60
- dbPassword = await ask("Database Password", dbPassword);
61
- const dbPortInput = await ask("Database Port", dbPort);
62
- dbPort = parseInt(dbPortInput, 10);
63
- if (Number.isNaN(dbPort)) {
64
- dbPort = 3306;
65
- }
66
- }
67
- } catch (error) {
68
- console.error(chalk.red("[ERROR] Error reading configuration input:"), error.message);
69
- rl.close();
70
- return;
71
- } finally {
72
- rl.close();
100
+ const itemsToCopy = [
101
+ "backend",
102
+ "frontend",
103
+ "docker",
104
+ ".env_example",
105
+ "AGENTS.md",
106
+ "index.js",
107
+ ];
108
+
109
+ try {
110
+ itemsToCopy.forEach((item) => {
111
+ const src = path.join(this.sourceDir, item);
112
+ const dest = path.join(this.appDir, item);
113
+
114
+ if (fs.existsSync(src)) {
115
+ if (!fs.existsSync(dest)) {
116
+ this.copyRecursive(src, dest);
117
+ console.log(chalk.green(`[OK] Copied ${item} to root.`));
118
+ } else {
119
+ console.log(
120
+ chalk.yellow(`[SKIP] ${item} already exists, skipping.`),
121
+ );
122
+ }
123
+ } else {
124
+ console.warn(
125
+ chalk.red(`[ERROR] Source ${item} not found in ${this.sourceDir}`),
126
+ );
127
+ }
128
+ });
129
+
130
+ const composeTemplateName =
131
+ dbType === "postgres"
132
+ ? "docker-compose.postgres.yml"
133
+ : dbType === "mysql"
134
+ ? "docker-compose.mysql.yml"
135
+ : "docker-compose.none.yml";
136
+ const composeSrc = path.join(
137
+ this.sourceDir,
138
+ "docker",
139
+ composeTemplateName,
140
+ );
141
+ const composeDest = path.join(this.appDir, "docker-compose.yml");
142
+ if (fs.existsSync(composeSrc)) {
143
+ if (!fs.existsSync(composeDest)) {
144
+ fs.copyFileSync(composeSrc, composeDest);
145
+ console.log(
146
+ chalk.green(`[OK] Created docker-compose.yml (${dbType} mode).`),
147
+ );
148
+ } else {
149
+ console.log(
150
+ chalk.yellow(`[SKIP] docker-compose.yml already exists, skipping.`),
151
+ );
152
+ }
153
+ } else {
154
+ const fallbackSrc = path.join(this.sourceDir, "docker-compose.yml");
155
+ if (fs.existsSync(fallbackSrc) && !fs.existsSync(composeDest)) {
156
+ fs.copyFileSync(fallbackSrc, composeDest);
157
+ console.log(chalk.green(`[OK] Created docker-compose.yml.`));
158
+ }
159
+ }
160
+
161
+ const envPath = path.join(this.appDir, ".env");
162
+ const envExamplePath = path.join(this.appDir, ".env_example");
163
+
164
+ if (fs.existsSync(envExamplePath)) {
165
+ let envContent = fs.readFileSync(envExamplePath, "utf-8");
166
+
167
+ const jwtSecret = crypto.randomBytes(32).toString("hex");
168
+
169
+ const updates = {
170
+ TITLE: title,
171
+ PORT: port,
172
+ APP_URL: appUrl,
173
+ JWT_SECRET: jwtSecret,
174
+ DB_TYPE: dbType,
175
+ };
176
+
177
+ if (dbType === "mysql") {
178
+ updates.DB_NAME = dbName;
179
+ updates.DB_USER = dbUser;
180
+ updates.DB_PASSWORD = dbPassword;
181
+ updates.DB_PORT = dbPort;
182
+ updates.DATABASE_URL = `mysql://${dbUser}:${dbPassword}@localhost:${dbPort}/${dbName}`;
183
+ } else if (dbType === "postgres") {
184
+ updates.DB_NAME = dbName;
185
+ updates.DB_USER = dbUser;
186
+ updates.DB_PASSWORD = dbPassword;
187
+ updates.DB_PORT = dbPort;
188
+ updates.DATABASE_URL = `postgresql://${dbUser}:${dbPassword}@localhost:${dbPort}/${dbName}?schema=public`;
73
189
  }
74
190
 
75
- const itemsToCopy = [
76
- "backend",
77
- "frontend",
78
- "docker",
79
- "docker-compose.yml",
80
- ".env_example",
81
- "AGENTS.md",
82
- "index.js"
83
- ];
84
-
85
- try {
86
- itemsToCopy.forEach(item => {
87
- const src = path.join(this.sourceDir, item);
88
- const dest = path.join(this.appDir, item);
89
-
90
- if (fs.existsSync(src)) {
91
- if (!fs.existsSync(dest)) {
92
- this.copyRecursive(src, dest);
93
- console.log(chalk.green(`[OK] Copied ${item} to root.`));
94
- } else {
95
- console.log(chalk.yellow(`[SKIP] ${item} already exists, skipping.`));
96
- }
97
- } else {
98
- console.warn(chalk.red(`[ERROR] Source ${item} not found in ${this.sourceDir}`));
99
- }
100
- });
101
-
102
- const envPath = path.join(this.appDir, ".env");
103
- const envExamplePath = path.join(this.appDir, ".env_example");
104
-
105
- if (fs.existsSync(envExamplePath)) {
106
- let envContent = fs.readFileSync(envExamplePath, "utf-8");
107
-
108
- const jwtSecret = crypto.randomBytes(32).toString("hex");
109
-
110
- const updates = {
111
- TITLE: title,
112
- PORT: port,
113
- APP_URL: appUrl,
114
- JWT_SECRET: jwtSecret,
115
- };
116
-
117
- if (useMysql) {
118
- updates.DB_NAME = dbName;
119
- updates.DB_USER = dbUser;
120
- updates.DB_PASSWORD = dbPassword;
121
- updates.DB_PORT = dbPort;
122
- updates.DATABASE_URL = `mysql://${dbUser}:${dbPassword}@localhost:${dbPort}/${dbName}`;
123
- }
124
-
125
- const lines = envContent.split(/\r?\n/);
126
- const updatedLines = lines.map(line => {
127
- const match = line.match(/^([A-Z_]+)=(.+)/);
128
- if (match) {
129
- const key = match[1];
130
- if (updates[key] !== undefined) {
131
- const value = updates[key];
132
- if (typeof value === "string" && !value.startsWith('"')) {
133
- return `${key}="${value}"`;
134
- }
135
- return `${key}=${value}`;
136
- }
137
- }
138
- return line;
139
- });
140
- envContent = updatedLines.join("\n");
141
-
142
- fs.writeFileSync(envPath, envContent, "utf-8");
143
- console.log(chalk.green("[OK] Created and configured .env file."));
191
+ const lines = envContent.split(/\r?\n/);
192
+ const updatedLines = lines.map((line) => {
193
+ const match = line.match(/^([A-Z_]+)=(.+)/);
194
+ if (match) {
195
+ const key = match[1];
196
+ if (updates[key] !== undefined) {
197
+ const value = updates[key];
198
+ if (typeof value === "string" && !value.startsWith('"')) {
199
+ return `${key}="${value}"`;
200
+ }
201
+ return `${key}=${value}`;
144
202
  }
203
+ }
204
+ return line;
205
+ });
206
+ envContent = updatedLines.join("\n");
145
207
 
146
- this.updatePackageJson();
147
-
148
- if (useMysql) {
149
- console.log(chalk.cyan("[INFO] Installing mysql2 and redis packages..."));
150
- try {
151
- execSync("npm install mysql2 redis", { stdio: "inherit", cwd: this.appDir });
152
- console.log(chalk.green("[OK] Successfully installed mysql2 and redis."));
153
- } catch (error) {
154
- console.warn(chalk.yellow("[ERROR] Automatic package installation failed. Please run 'npm install mysql2 redis' manually."));
155
- }
156
- }
208
+ fs.writeFileSync(envPath, envContent, "utf-8");
209
+ console.log(chalk.green("[OK] Created and configured .env file."));
210
+ }
157
211
 
158
- console.log(chalk.blue("\nBlue Bird initialization completed!"));
159
- console.log(chalk.white("Next steps:"));
160
- console.log(chalk.bold(" npm install"));
161
- console.log(chalk.bold(" npm run dev"));
212
+ this.updatePackageJson();
162
213
 
214
+ if (dbType !== "none") {
215
+ console.log(
216
+ chalk.cyan(`[INFO] Installing ${dbType} and redis packages...`),
217
+ );
218
+ try {
219
+ let packagesToInstall = ["redis"];
220
+ if (dbType === "postgres") {
221
+ packagesToInstall.push("pg");
222
+ } else {
223
+ packagesToInstall.push("mysql2");
224
+ }
225
+ execSync(`npm install ${packagesToInstall.join(" ")}`, {
226
+ stdio: "inherit",
227
+ cwd: this.appDir,
228
+ });
229
+ console.log(
230
+ chalk.green(
231
+ `[OK] Successfully installed ${packagesToInstall.join(" ")}.`,
232
+ ),
233
+ );
163
234
  } catch (error) {
164
- console.error(chalk.red("[ERROR] Error during initialization:"), error.message);
235
+ console.warn(
236
+ chalk.yellow(
237
+ "[ERROR] Automatic package installation failed. Please run 'npm install' manually.",
238
+ ),
239
+ );
165
240
  }
241
+ }
242
+
243
+ console.log(chalk.blue("\nBlue Bird initialization completed!"));
244
+ console.log(chalk.white("Next steps:"));
245
+ console.log(chalk.bold(" npm install"));
246
+ console.log(chalk.bold(" npm run dev"));
247
+ } catch (error) {
248
+ console.error(
249
+ chalk.red("[ERROR] Error during initialization:"),
250
+ error.message,
251
+ );
166
252
  }
253
+ }
254
+
255
+ /**
256
+ * Updates package.json with needed scripts and module type if not set.
257
+ */
258
+ updatePackageJson() {
259
+ const pkgPath = path.join(this.appDir, "package.json");
260
+ if (fs.existsSync(pkgPath)) {
261
+ const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
262
+ pkg.scripts = pkg.scripts || {};
263
+
264
+ const scriptsToAdd = {
265
+ dev: "node --watch --env-file=.env index.js",
266
+ "dev:astro": "astro dev --root frontend",
267
+ "dev:api": "node --watch --env-file=.env index.js",
268
+ start: "node --env-file=.env index.js",
269
+ build: "astro build --root frontend",
270
+ init: "blue-bird",
271
+ route: "blue-bird route",
272
+ "swagger-install": "blue-bird swagger-install",
273
+ docker: "blue-bird docker",
274
+ };
275
+
276
+ let updated = false;
277
+ for (const [key, value] of Object.entries(scriptsToAdd)) {
278
+ if (!pkg.scripts[key]) {
279
+ pkg.scripts[key] = value;
280
+ updated = true;
281
+ }
282
+ }
167
283
 
168
- /**
169
- * Updates the user's package.json with Blue Bird scripts.
170
- */
171
- updatePackageJson() {
172
- const pkgPath = path.join(this.appDir, "package.json");
173
- if (fs.existsSync(pkgPath)) {
174
- const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8"));
175
- pkg.scripts = pkg.scripts || {};
176
-
177
- const scriptsToAdd = {
178
- "dev": "node --watch --env-file=.env index.js",
179
- "dev:astro": "astro dev --root frontend",
180
- "dev:api": "node --watch --env-file=.env index.js",
181
- "start": "node --env-file=.env index.js",
182
- "build": "astro build --root frontend",
183
- "init": "blue-bird",
184
- "route": "blue-bird route",
185
- "swagger-install": "blue-bird swagger-install",
186
- "docker": "blue-bird docker"
187
- };
188
-
189
- let updated = false;
190
- for (const [key, value] of Object.entries(scriptsToAdd)) {
191
- if (!pkg.scripts[key]) {
192
- pkg.scripts[key] = value;
193
- updated = true;
194
- }
195
- }
196
-
197
- if (pkg.type !== "module") {
198
- pkg.type = "module";
199
- updated = true;
200
- }
284
+ if (pkg.type !== "module") {
285
+ pkg.type = "module";
286
+ updated = true;
287
+ }
201
288
 
202
- if (updated) {
203
- fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
204
- console.log(chalk.green("[OK] Updated package.json configuration."));
205
- }
206
- }
289
+ if (updated) {
290
+ fs.writeFileSync(pkgPath, JSON.stringify(pkg, null, 2));
291
+ console.log(chalk.green("[OK] Updated package.json configuration."));
292
+ }
207
293
  }
208
-
209
- /**
210
- * Copies a file or directory recursively.
211
- * @param {string} src - Source path.
212
- * @param {string} dest - Destination path.
213
- */
214
- copyRecursive(src, dest) {
215
- const stats = fs.statSync(src);
216
- const isDirectory = stats.isDirectory();
217
-
218
- if (isDirectory) {
219
- if (!fs.existsSync(dest)) {
220
- fs.mkdirSync(dest, { recursive: true });
221
- }
222
- fs.readdirSync(src).forEach(childItemName => {
223
- this.copyRecursive(path.join(src, childItemName), path.join(dest, childItemName));
224
- });
225
- } else {
226
- fs.copyFileSync(src, dest);
294
+ }
295
+
296
+ /**
297
+ * Copies a file or directory recursively.
298
+ * @param {string} src - Source path.
299
+ * @param {string} dest - Destination path.
300
+ */
301
+ copyRecursive(src, dest) {
302
+ const stats = fs.statSync(src);
303
+ const isDirectory = stats.isDirectory();
304
+
305
+ if (isDirectory) {
306
+ if (!fs.existsSync(dest)) {
307
+ fs.mkdirSync(dest, { recursive: true });
308
+ }
309
+ fs.readdirSync(src).forEach((childItemName) => {
310
+ if (
311
+ childItemName === "node_modules" ||
312
+ childItemName === ".astro" ||
313
+ childItemName === "dist" ||
314
+ childItemName === ".git"
315
+ ) {
316
+ return;
227
317
  }
318
+ this.copyRecursive(
319
+ path.join(src, childItemName),
320
+ path.join(dest, childItemName),
321
+ );
322
+ });
323
+ } else {
324
+ fs.copyFileSync(src, dest);
228
325
  }
326
+ }
229
327
  }
230
328
 
231
329
  const initializer = new ProjectInit();