create-pardx-scaffold 0.1.5 → 0.1.7

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-pardx-scaffold",
3
- "version": "0.1.5",
3
+ "version": "0.1.7",
4
4
  "description": "Scaffold a new project from PardxAI monorepo (git-tracked files)",
5
5
  "license": "MIT",
6
6
  "bin": "./cli.js",
@@ -59,6 +59,13 @@ function main() {
59
59
  copied++;
60
60
  }
61
61
 
62
+ // 确保脚手架模板根目录包含 .gitignore
63
+ const gitignoreSrc = path.join(REPO_ROOT, '.gitignore');
64
+ const gitignoreDest = path.join(TEMPLATE_ROOT, '.gitignore');
65
+ if (fs.existsSync(gitignoreSrc)) {
66
+ fs.copyFileSync(gitignoreSrc, gitignoreDest);
67
+ }
68
+
62
69
  console.log('export-scaffold: copied', copied, 'files to packages/create-pardx-scaffold/template/');
63
70
  }
64
71
 
@@ -38,15 +38,17 @@ const rl = readline.createInterface({
38
38
  const question = (query) => new Promise((resolve) => rl.question(query, resolve));
39
39
 
40
40
  // Project configuration
41
+ // 注意:API 端口在 apps/api/config.local.yaml 的 app.port 配置;Web 端口由 Next.js 自动配置,此处不配置
41
42
  const config = {
42
43
  projectName: '',
43
44
  projectDescription: '',
44
45
  authorName: '',
45
46
  authorEmail: '',
46
47
  databaseUrl: '',
48
+ readDatabaseUrl: '',
47
49
  redisUrl: '',
48
- apiPort: '',
49
- webPort: '',
50
+ rabbitmqUrl: '',
51
+ baseHost: '127.0.0.1',
50
52
  };
51
53
 
52
54
  async function main() {
@@ -81,26 +83,31 @@ async function main() {
81
83
 
82
84
  log.header('\n📦 Configuration');
83
85
 
84
- config.apiPort = await question(
85
- `${colors.cyan}API port${colors.reset} [3100]: `
86
- );
87
- config.apiPort = config.apiPort.trim() || '3100';
88
-
89
- config.webPort = await question(
90
- `${colors.cyan}Web port${colors.reset} [3000]: `
91
- );
92
- config.webPort = config.webPort.trim() || '3000';
93
-
94
86
  config.databaseUrl = await question(
95
87
  `${colors.cyan}Database URL${colors.reset} [postgresql://user:password@localhost:5432/dbname]: `
96
88
  );
97
89
  config.databaseUrl = config.databaseUrl.trim() || 'postgresql://user:password@localhost:5432/dbname';
98
90
 
91
+ config.readDatabaseUrl = await question(
92
+ `${colors.cyan}Read Database URL${colors.reset} [same as Database URL]: `
93
+ );
94
+ config.readDatabaseUrl = config.readDatabaseUrl.trim() || config.databaseUrl;
95
+
99
96
  config.redisUrl = await question(
100
97
  `${colors.cyan}Redis URL${colors.reset} [redis://localhost:6379]: `
101
98
  );
102
99
  config.redisUrl = config.redisUrl.trim() || 'redis://localhost:6379';
103
100
 
101
+ config.rabbitmqUrl = await question(
102
+ `${colors.cyan}RabbitMQ URL${colors.reset} [amqp://localhost:5672]: `
103
+ );
104
+ config.rabbitmqUrl = config.rabbitmqUrl.trim() || 'amqp://localhost:5672';
105
+
106
+ config.baseHost = await question(
107
+ `${colors.cyan}Base Host${colors.reset} [127.0.0.1]: `
108
+ );
109
+ config.baseHost = config.baseHost.trim() || '127.0.0.1';
110
+
104
111
  rl.close();
105
112
 
106
113
  // Display configuration summary
@@ -108,10 +115,11 @@ async function main() {
108
115
  console.log(` Project Name: ${colors.green}${config.projectName}${colors.reset}`);
109
116
  console.log(` Description: ${config.projectDescription}`);
110
117
  console.log(` Author: ${config.authorName} <${config.authorEmail}>`);
111
- console.log(` API Port: ${config.apiPort}`);
112
- console.log(` Web Port: ${config.webPort}`);
118
+ console.log(` Base Host: ${config.baseHost}`);
113
119
  console.log(` Database: ${config.databaseUrl}`);
120
+ console.log(` Read DB: ${config.readDatabaseUrl}`);
114
121
  console.log(` Redis: ${config.redisUrl}`);
122
+ console.log(` RabbitMQ: ${config.rabbitmqUrl}`);
115
123
 
116
124
  // Apply configuration
117
125
  log.header('\n🔧 Applying Configuration');
@@ -158,27 +166,9 @@ async function applyConfiguration() {
158
166
  });
159
167
  log.success('API package.json updated');
160
168
 
161
- // Create .env files
162
- log.info('Creating environment files...');
163
- createEnvFile(
164
- path.join(rootDir, 'apps/web/.env.local'),
165
- {
166
- NEXT_PUBLIC_API_BASE_URL: `http://localhost:${config.apiPort}/api`,
167
- }
168
- );
169
- createEnvFile(
170
- path.join(rootDir, 'apps/api/.env'),
171
- {
172
- NODE_ENV: 'development',
173
- PORT: config.apiPort,
174
- HOST: '0.0.0.0',
175
- DATABASE_URL: config.databaseUrl,
176
- REDIS_URL: config.redisUrl,
177
- JWT_SECRET: generateRandomSecret(),
178
- JWT_EXPIRES_IN: '7d',
179
- CORS_ORIGIN: `http://localhost:${config.webPort}`,
180
- }
181
- );
169
+ // Create .env files from .env.example
170
+ log.info('Creating environment files from .env.example...');
171
+ createEnvFromExample(rootDir, config);
182
172
  log.success('Environment files created');
183
173
 
184
174
  // Update README
@@ -198,11 +188,117 @@ function updatePackageJson(filePath, updates) {
198
188
  fs.writeFileSync(filePath, JSON.stringify(packageJson, null, 2) + '\n');
199
189
  }
200
190
 
191
+ /**
192
+ * 解析 .env.example 文件,提取 KEY=VALUE 行(含注释、空行)
193
+ */
194
+ function parseEnvExample(content) {
195
+ const lines = [];
196
+ const vars = new Map();
197
+ for (const line of content.split(/\n/)) {
198
+ const trimmed = line.trimEnd();
199
+ if (!trimmed || trimmed.startsWith('#')) {
200
+ lines.push({ type: 'raw', value: trimmed });
201
+ } else {
202
+ const match = trimmed.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/);
203
+ if (match) {
204
+ const key = match[1];
205
+ let value = match[2];
206
+ if (value.startsWith('"') && value.endsWith('"')) {
207
+ value = value.slice(1, -1);
208
+ }
209
+ lines.push({ type: 'var', key, raw: trimmed });
210
+ vars.set(key, value);
211
+ } else {
212
+ lines.push({ type: 'raw', value: trimmed });
213
+ }
214
+ }
215
+ }
216
+ return { lines, vars };
217
+ }
218
+
219
+ /**
220
+ * 根据 apps/api/.env.example 和 apps/web/.env.example 生成 .env 文件
221
+ */
222
+ function createEnvFromExample(rootDir, config) {
223
+ const apiExamplePath = path.join(rootDir, 'apps/api/.env.example');
224
+ const webExamplePath = path.join(rootDir, 'apps/web/.env.example');
225
+
226
+ // 变量替换映射(apps/api),参考 apps/api/.env.example
227
+ // API 端口由 apps/api/config.local.yaml 的 app.port 配置,此处不覆盖 API_BASE_URL / INTERNAL_API_BASE_URL
228
+ const apiReplacements = {
229
+ BASE_HOST: config.baseHost,
230
+ DATABASE_URL: config.databaseUrl,
231
+ READ_DATABASE_URL: config.readDatabaseUrl,
232
+ REDIS_URL: config.redisUrl,
233
+ RABBITMQ_URL: config.rabbitmqUrl,
234
+ };
235
+
236
+ if (fs.existsSync(apiExamplePath)) {
237
+ const content = fs.readFileSync(apiExamplePath, 'utf8');
238
+ const { lines, vars } = parseEnvExample(content);
239
+ const outLines = [];
240
+ for (const item of lines) {
241
+ if (item.type === 'raw') {
242
+ outLines.push(item.value);
243
+ } else {
244
+ let val = apiReplacements[item.key];
245
+ if (val === undefined && item.key === 'RABBITMQ_EVENTS_URL') {
246
+ const orig = vars.get(item.key) || '';
247
+ val = orig.replace(/\$\{BASE_HOST\}/g, config.baseHost);
248
+ } else if (val === undefined) {
249
+ val = null;
250
+ }
251
+ const final = val !== null && val !== undefined ? `${item.key}=${val}` : item.raw;
252
+ outLines.push(final);
253
+ }
254
+ }
255
+ const apiEnvPath = path.join(rootDir, 'apps/api/.env');
256
+ fs.writeFileSync(apiEnvPath, outLines.join('\n') + '\n');
257
+
258
+ // 追加 JWT_SECRET(若 .env.example 中无)
259
+ const apiEnvContent = fs.readFileSync(apiEnvPath, 'utf8');
260
+ if (!/^JWT_SECRET=/m.test(apiEnvContent)) {
261
+ fs.appendFileSync(apiEnvPath, `\n# JWT\nJWT_SECRET=${generateRandomSecret()}\nJWT_EXPIRES_IN=7d\n`);
262
+ }
263
+ } else {
264
+ createEnvFile(path.join(rootDir, 'apps/api/.env'), {
265
+ NODE_ENV: 'development',
266
+ DATABASE_URL: config.databaseUrl,
267
+ REDIS_URL: config.redisUrl,
268
+ RABBITMQ_URL: config.rabbitmqUrl,
269
+ JWT_SECRET: generateRandomSecret(),
270
+ });
271
+ }
272
+
273
+ // apps/web:NEXT_PUBLIC_API_BASE_URL 等由 .env.example 提供,Next.js 端口自动配置,此处不覆盖
274
+ const webReplacements = {};
275
+
276
+ if (fs.existsSync(webExamplePath)) {
277
+ const content = fs.readFileSync(webExamplePath, 'utf8');
278
+ const { lines } = parseEnvExample(content);
279
+ const outLines = [];
280
+ for (const item of lines) {
281
+ if (item.type === 'raw') {
282
+ outLines.push(item.value);
283
+ } else {
284
+ const val = webReplacements[item.key] ?? null;
285
+ const final = val !== null ? `${item.key}=${val}` : item.raw;
286
+ outLines.push(final);
287
+ }
288
+ }
289
+ fs.writeFileSync(
290
+ path.join(rootDir, 'apps/web/.env.local'),
291
+ outLines.join('\n') + '\n'
292
+ );
293
+ } else {
294
+ createEnvFile(path.join(rootDir, 'apps/web/.env.local'), {});
295
+ }
296
+ }
297
+
201
298
  function createEnvFile(filePath, variables) {
202
299
  const content = Object.entries(variables)
203
300
  .map(([key, value]) => `${key}=${value}`)
204
301
  .join('\n') + '\n';
205
-
206
302
  fs.writeFileSync(filePath, content);
207
303
  }
208
304
 
@@ -213,9 +309,9 @@ function updateReadme(filePath) {
213
309
  }
214
310
 
215
311
  let content = fs.readFileSync(filePath, 'utf8');
216
- content = content.replace(/# PardxAI Monorepo Scaffold/, `# ${config.projectName}`);
312
+ content = content.replace(/# PardxAI Monorepo Scaffold[^\n]*/, `# ${config.projectName}`);
217
313
  content = content.replace(
218
- /A production-ready monorepo scaffold based on PardxAI architecture\./,
314
+ /A comprehensive production-ready monorepo scaffold with complete implementations\./,
219
315
  config.projectDescription
220
316
  );
221
317
  fs.writeFileSync(filePath, content);