@xfilecom/xframe 0.1.17 → 0.1.18

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/bin/xframe.js CHANGED
@@ -38,10 +38,22 @@ Options:
38
38
  --front-core <spec> @xfilecom/front-core (default: ^0.2.1)
39
39
  --no-install 스캐폴드만 하고 yarn/npm install 생략
40
40
 
41
+ MySQL (shared/config/api/application.yml 의 database.*)
42
+ --skip-db-prompt TTY여도 DB 질문 생략 → 아래 기본값 또는 플래그만 반영
43
+ --db-host <h> 기본 127.0.0.1
44
+ --db-port <n> 기본 3306
45
+ --db-user <u> 기본 root
46
+ --db-password <p> 기본 빈 문자열 (빈 값: --db-password "")
47
+ --db-name <n> 기본 app
48
+ --db-pull install 성공 후 yarn/npm run db:pull 실행 (MySQL 도달 가능할 때)
49
+
50
+ TTY + --skip-db-prompt 없음 → 위 항목을 순서대로 물어봅니다 (Enter = 기본값).
51
+ 마지막에 db:pull 여부를 [y/N] 로 묻습니다 (--db-pull 이 있으면 질문 생략하고 실행).
52
+
41
53
  Examples:
42
54
  npx @xfilecom/xframe@latest my-app
43
- npx @xfilecom/xframe my-app --backend-core file:../../../packages/backend-core \\
44
- --front-core file:../../../packages/front-core`);
55
+ npx @xfilecom/xframe my-app --db-host 127.0.0.1 --db-user app --db-password secret --db-name mydb --db-pull
56
+ npx @xfilecom/xframe my-app --skip-db-prompt --no-install`);
45
57
  }
46
58
 
47
59
  function parseArgs(argv) {
@@ -50,6 +62,20 @@ function parseArgs(argv) {
50
62
  let backendCore = '^1.0.0';
51
63
  let frontCore = '^0.2.1';
52
64
  let skipInstall = false;
65
+ let skipDbPrompt = false;
66
+ let dbPull = false;
67
+ /** @type {{ host?: string, port?: number, user?: string, password?: string, name?: string }} */
68
+ const dbFromCli = {};
69
+
70
+ function takeStringFlag(name, i) {
71
+ const v = rest[i + 1];
72
+ if (v === undefined) {
73
+ console.error(`xframe: ${name} requires a value`);
74
+ process.exit(1);
75
+ }
76
+ i += 1;
77
+ return { val: v, nextI: i };
78
+ }
53
79
 
54
80
  for (let i = 0; i < rest.length; i += 1) {
55
81
  const a = rest[i];
@@ -61,24 +87,58 @@ function parseArgs(argv) {
61
87
  skipInstall = true;
62
88
  continue;
63
89
  }
90
+ if (a === '--skip-db-prompt') {
91
+ skipDbPrompt = true;
92
+ continue;
93
+ }
94
+ if (a === '--db-pull') {
95
+ dbPull = true;
96
+ continue;
97
+ }
64
98
  if (a === '--backend-core') {
65
- const v = rest[i + 1];
66
- if (!v || v.startsWith('-')) {
67
- console.error('xframe: --backend-core requires a value');
68
- process.exit(1);
69
- }
70
- backendCore = v;
71
- i += 1;
99
+ const { val, nextI } = takeStringFlag('--backend-core', i);
100
+ backendCore = val;
101
+ i = nextI;
72
102
  continue;
73
103
  }
74
104
  if (a === '--front-core') {
75
- const v = rest[i + 1];
76
- if (!v || v.startsWith('-')) {
77
- console.error('xframe: --front-core requires a value');
105
+ const { val, nextI } = takeStringFlag('--front-core', i);
106
+ frontCore = val;
107
+ i = nextI;
108
+ continue;
109
+ }
110
+ if (a === '--db-host') {
111
+ const { val, nextI } = takeStringFlag('--db-host', i);
112
+ dbFromCli.host = val;
113
+ i = nextI;
114
+ continue;
115
+ }
116
+ if (a === '--db-port') {
117
+ const { val, nextI } = takeStringFlag('--db-port', i);
118
+ dbFromCli.port = parseInt(val, 10);
119
+ if (!Number.isFinite(dbFromCli.port)) {
120
+ console.error('xframe: --db-port must be a number');
78
121
  process.exit(1);
79
122
  }
80
- frontCore = v;
81
- i += 1;
123
+ i = nextI;
124
+ continue;
125
+ }
126
+ if (a === '--db-user') {
127
+ const { val, nextI } = takeStringFlag('--db-user', i);
128
+ dbFromCli.user = val;
129
+ i = nextI;
130
+ continue;
131
+ }
132
+ if (a === '--db-password') {
133
+ const { val, nextI } = takeStringFlag('--db-password', i);
134
+ dbFromCli.password = val;
135
+ i = nextI;
136
+ continue;
137
+ }
138
+ if (a === '--db-name') {
139
+ const { val, nextI } = takeStringFlag('--db-name', i);
140
+ dbFromCli.name = val;
141
+ i = nextI;
82
142
  continue;
83
143
  }
84
144
  if (a.startsWith('-')) {
@@ -93,7 +153,126 @@ function parseArgs(argv) {
93
153
  projectDir = a;
94
154
  }
95
155
 
96
- return { projectDir, backendCore, frontCore, skipInstall };
156
+ return {
157
+ projectDir,
158
+ backendCore,
159
+ frontCore,
160
+ skipInstall,
161
+ skipDbPrompt,
162
+ dbPull,
163
+ dbFromCli,
164
+ };
165
+ }
166
+
167
+ /** YAML 단일 인용 내용 이스케이프 */
168
+ function yamlSingleQuotedEscape(s) {
169
+ return String(s).replace(/'/g, "''");
170
+ }
171
+
172
+ /** 식별자형이면 따옴표 없이, 아니면 작은따옴표 */
173
+ function yamlScalar(v) {
174
+ const s = String(v);
175
+ if (/^[A-Za-z0-9_.@-]+$/.test(s)) return s;
176
+ return `'${yamlSingleQuotedEscape(s)}'`;
177
+ }
178
+
179
+ /**
180
+ * 최상위 `database:` 블록만 수정 (같은 들여쓰기의 `server.port` 등과 구분).
181
+ *
182
+ * @param {string} targetRoot
183
+ * @param {{ host: string, port: number, user: string, password: string, name: string }} db
184
+ */
185
+ function patchApplicationYmlDatabase(targetRoot, db) {
186
+ const p = path.join(targetRoot, 'shared', 'config', 'api', 'application.yml');
187
+ if (!fs.existsSync(p)) {
188
+ console.warn('xframe: application.yml not found, skip DB patch');
189
+ return;
190
+ }
191
+ const portN = Number.isFinite(Number(db.port)) ? Number(db.port) : 3306;
192
+ const pwd = `'${yamlSingleQuotedEscape(db.password)}'`;
193
+ const lines = fs.readFileSync(p, 'utf8').split('\n');
194
+ let i = 0;
195
+ while (i < lines.length) {
196
+ if (lines[i] === 'database:') {
197
+ i += 1;
198
+ while (i < lines.length) {
199
+ const line = lines[i];
200
+ // 루트 database 직계만 (들여쓰기 2칸 + 키). 4칸 이상은 core.database 등 하위 블록.
201
+ if (line === '' || /^ [A-Za-z_#]/.test(line)) {
202
+ if (/^ host:/.test(line)) lines[i] = ` host: ${yamlScalar(db.host)}`;
203
+ else if (/^ port:/.test(line)) lines[i] = ` port: ${portN}`;
204
+ else if (/^ user:/.test(line)) lines[i] = ` user: ${yamlScalar(db.user)}`;
205
+ else if (/^ password:/.test(line)) lines[i] = ` password: ${pwd}`;
206
+ else if (/^ name:/.test(line)) lines[i] = ` name: ${yamlScalar(db.name)}`;
207
+ i += 1;
208
+ continue;
209
+ }
210
+ break;
211
+ }
212
+ break;
213
+ }
214
+ i += 1;
215
+ }
216
+ fs.writeFileSync(p, lines.join('\n'), 'utf8');
217
+ }
218
+
219
+ const DB_DEFAULTS = {
220
+ host: '127.0.0.1',
221
+ port: 3306,
222
+ user: 'root',
223
+ password: '',
224
+ name: 'app',
225
+ };
226
+
227
+ /**
228
+ * @param {{ host?: string, port?: number, user?: string, password?: string, name?: string }} fromCli
229
+ * @param {boolean} skipDbPrompt
230
+ * @param {boolean} wantDbPullFlag
231
+ * @returns {Promise<{ db: typeof DB_DEFAULTS, runDbPull: boolean }>}
232
+ */
233
+ async function collectDatabaseConfig(fromCli, skipDbPrompt, wantDbPullFlag) {
234
+ let db = { ...DB_DEFAULTS, ...fromCli };
235
+ const interactive = process.stdin.isTTY && process.stdout.isTTY && !skipDbPrompt;
236
+ const readline = require('node:readline/promises');
237
+ const rl = interactive ? readline.createInterface({ input: process.stdin, output: process.stdout }) : null;
238
+
239
+ try {
240
+ if (interactive && rl) {
241
+ console.log('\nxframe: MySQL 연결 정보 (Enter = 괄호 안 기본값)\n');
242
+ const ask = async (label, key, def) => {
243
+ if (fromCli[key] !== undefined) return;
244
+ const hint = def === '' ? '(비워두기)' : def;
245
+ const line = (await rl.question(`${label} [${hint}]: `)).trim();
246
+ if (line !== '') {
247
+ if (key === 'port') {
248
+ const n = parseInt(line, 10);
249
+ if (Number.isFinite(n)) db[key] = n;
250
+ } else {
251
+ db[key] = line;
252
+ }
253
+ }
254
+ };
255
+ await ask('Host', 'host', DB_DEFAULTS.host);
256
+ await ask('Port', 'port', String(DB_DEFAULTS.port));
257
+ await ask('User', 'user', DB_DEFAULTS.user);
258
+ await ask('Password', 'password', DB_DEFAULTS.password);
259
+ await ask('Database name', 'name', DB_DEFAULTS.name);
260
+ } else {
261
+ db = { ...DB_DEFAULTS, ...fromCli };
262
+ }
263
+
264
+ let runDbPull = wantDbPullFlag;
265
+ if (interactive && rl && !wantDbPullFlag) {
266
+ const a = (await rl.question('\n지금 yarn/npm db:pull (스키마 역추출) 을 실행할까요? [y/N] '))
267
+ .trim()
268
+ .toLowerCase();
269
+ runDbPull = a === 'y' || a === 'yes';
270
+ }
271
+
272
+ return { db, runDbPull };
273
+ } finally {
274
+ if (rl) await rl.close();
275
+ }
97
276
  }
98
277
 
99
278
  /** yarn 우선, 없거나 실패 시 npm (부모 셸 PATH 그대로 — shell:true 쓰면 sh라 yarn/nvm PATH가 빠지는 경우가 많음) */
@@ -137,6 +316,27 @@ function installDependencies(cwd) {
137
316
  return false;
138
317
  }
139
318
 
319
+ function executeDbPull(cwd) {
320
+ console.log('\nxframe: db:pull 실행 (drizzle-kit introspect)…');
321
+ const opts = { cwd, stdio: 'inherit', env: process.env };
322
+ const y = spawnSync('yarn', ['run', 'db:pull'], opts);
323
+ if (y.status === 0) {
324
+ console.log('xframe: db:pull 완료.');
325
+ return true;
326
+ }
327
+ if (y.error && y.error.code === 'ENOENT') {
328
+ console.log('xframe: yarn 없음 → npm run db:pull 시도');
329
+ }
330
+ const n = spawnSync('npm', ['run', 'db:pull'], opts);
331
+ if (n.status === 0) {
332
+ console.log('xframe: db:pull 완료.');
333
+ return true;
334
+ }
335
+ console.error('xframe: db:pull 실패. 프로젝트 루트에서 수동으로 yarn db:pull / npm run db:pull 하세요.');
336
+ console.error('xframe: (비밀번호가 비어 있으면 drizzle-kit 이 거절할 수 있습니다. application.yml database.password 를 확인하세요.)');
337
+ return false;
338
+ }
339
+
140
340
  /** 모노레포 템플릿 tsconfig 의 @xfilecom/front-core 경로 → 생성 앱의 node_modules 기준 */
141
341
  const TEMPLATE_FRONT_CORE_TS_PATH = '../../../../front-core/src/index.ts';
142
342
  const SCAFFOLD_FRONT_CORE_TS_PATH = '../../node_modules/@xfilecom/front-core';
@@ -207,8 +407,16 @@ function readCliVersion() {
207
407
  }
208
408
  }
209
409
 
210
- function main() {
211
- const { projectDir, backendCore, frontCore, skipInstall } = parseArgs(process.argv);
410
+ async function main() {
411
+ const {
412
+ projectDir,
413
+ backendCore,
414
+ frontCore,
415
+ skipInstall,
416
+ skipDbPrompt,
417
+ dbPull: dbPullFlag,
418
+ dbFromCli,
419
+ } = parseArgs(process.argv);
212
420
 
213
421
  if (!projectDir) {
214
422
  console.error('xframe: missing <project-directory>\n');
@@ -248,6 +456,10 @@ function main() {
248
456
 
249
457
  fs.cpSync(templateRoot, targetRoot, { recursive: true });
250
458
 
459
+ const { db, runDbPull: shouldDbPull } = await collectDatabaseConfig(dbFromCli, skipDbPrompt, dbPullFlag);
460
+ patchApplicationYmlDatabase(targetRoot, db);
461
+ console.log(`xframe: wrote database.* → ${path.join('shared', 'config', 'api', 'application.yml')}`);
462
+
251
463
  const vars = {
252
464
  PACKAGE_NAME: packageName,
253
465
  SERVICE_LABEL: `${packageName}-api`,
@@ -276,13 +488,22 @@ Included:
276
488
  • shared/ — config/api·web, schema, sql, endpoint
277
489
  • .cursor/rules — @xfilecom/backend-core / front-core 사용 가이드 (.mdc)`);
278
490
 
491
+ let installOk = true;
279
492
  if (!skipInstall) {
280
- const ok = installDependencies(targetRoot);
281
- if (!ok) {
493
+ installOk = installDependencies(targetRoot);
494
+ if (!installOk) {
282
495
  process.exit(1);
283
496
  }
284
497
  }
285
498
 
499
+ if (shouldDbPull) {
500
+ if (skipInstall) {
501
+ console.warn('xframe: --no-install 이라 db:pull 을 건너뜁니다 (먼저 install 후 수동 실행).');
502
+ } else {
503
+ executeDbPull(targetRoot);
504
+ }
505
+ }
506
+
286
507
  const rel = path.relative(process.cwd(), targetRoot) || '.';
287
508
  console.log(`
288
509
  Next:
@@ -292,4 +513,7 @@ Next:
292
513
  (설치 생략: npx @xfilecom/xframe <dir> --no-install)`);
293
514
  }
294
515
 
295
- main();
516
+ main().catch((err) => {
517
+ console.error(err);
518
+ process.exit(1);
519
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xfilecom/xframe",
3
- "version": "0.1.17",
3
+ "version": "0.1.18",
4
4
  "description": "Scaffold full-stack app: Nest + @xfilecom/backend-core, Vite/React + @xfilecom/front-core",
5
5
  "license": "UNLICENSED",
6
6
  "bin": {
@@ -17,9 +17,9 @@ http:
17
17
  database:
18
18
  host: 127.0.0.1
19
19
  port: 3306
20
- user: xfilecom
21
- password: milk1209
22
- name: usdt-market-place
20
+ user: root
21
+ password: ''
22
+ name: app
23
23
  ssl: false
24
24
  connectionLimit: 10
25
25