@restforgejs/platform 5.1.16 → 5.1.20

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.
Files changed (176) hide show
  1. package/build-info.json +2 -2
  2. package/cli/consumer-deploy.js +1 -1
  3. package/cli/consumer.js +1 -1
  4. package/generators/cli/catalog/dbschema.js +2 -1
  5. package/generators/cli/endpoint/list.js +264 -0
  6. package/generators/cli/fast-track.js +395 -37
  7. package/generators/cli/processor/create.js +7 -7
  8. package/generators/cli/processor/list.js +229 -0
  9. package/generators/lib/generators/dashboard-generator.js +5 -5
  10. package/generators/lib/payload/payload-runner.js +63 -0
  11. package/generators/lib/templates/dashboard-catalog.js +1 -1
  12. package/generators/lib/templates/db-connection-env.js +1 -1
  13. package/generators/lib/templates/dbschema-catalog.js +1 -1
  14. package/generators/lib/templates/field-validation-catalog.js +1 -1
  15. package/generators/lib/templates/mysql-template.js +1 -1
  16. package/generators/lib/templates/oracle-template.js +1 -1
  17. package/generators/lib/templates/postgres-template.js +1 -1
  18. package/generators/lib/templates/query-declarative-catalog.js +1 -1
  19. package/generators/lib/templates/sqlite-template.js +1 -1
  20. package/integrity-manifest.json +18 -18
  21. package/package.json +1 -1
  22. package/scripts/check-install.js +8 -8
  23. package/scripts/verify-integrity.js +1 -1
  24. package/server.js +1 -1
  25. package/src/components/handlers/adjust_handler.js +1 -1
  26. package/src/components/handlers/audit_handler.js +1 -1
  27. package/src/components/handlers/delete_handler.js +1 -1
  28. package/src/components/handlers/export_handler.js +1 -1
  29. package/src/components/handlers/import_handler.js +1 -1
  30. package/src/components/handlers/insert_handler.js +1 -1
  31. package/src/components/handlers/update_handler.js +1 -1
  32. package/src/components/handlers/upload_handler.js +1 -1
  33. package/src/components/handlers/workflow_handler.js +1 -1
  34. package/src/components/integrations/webhook.js +1 -1
  35. package/src/consumers/baseConsumer.js +1 -1
  36. package/src/consumers/declarativeMapper.js +1 -1
  37. package/src/consumers/handlers/apiHandler.js +1 -1
  38. package/src/consumers/handlers/consoleHandler.js +1 -1
  39. package/src/consumers/handlers/databaseHandler.js +1 -1
  40. package/src/consumers/handlers/index.js +1 -1
  41. package/src/consumers/handlers/kafkaHandler.js +1 -1
  42. package/src/consumers/index.js +1 -1
  43. package/src/consumers/messageTransformer.js +1 -1
  44. package/src/consumers/validator.js +1 -1
  45. package/src/core/db/dialect/base-dialect.js +1 -1
  46. package/src/core/db/dialect/index.js +1 -1
  47. package/src/core/db/dialect/mysql-dialect.js +1 -1
  48. package/src/core/db/dialect/oracle-dialect.js +1 -1
  49. package/src/core/db/dialect/postgres-dialect.js +1 -1
  50. package/src/core/db/dialect/sqlite-dialect.js +1 -1
  51. package/src/core/db/flatten-helper.js +1 -1
  52. package/src/core/db/query-builder-error.js +1 -1
  53. package/src/core/db/query-builder.js +1 -1
  54. package/src/core/db/relation-helper.js +1 -1
  55. package/src/core/handlers/delete_handler.js +1 -1
  56. package/src/core/handlers/insert_handler.js +1 -1
  57. package/src/core/handlers/update_handler.js +1 -1
  58. package/src/core/models/base-model.js +1 -1
  59. package/src/core/utils/cache-manager.js +1 -1
  60. package/src/core/utils/component-engine.js +1 -1
  61. package/src/core/utils/context-builder.js +1 -1
  62. package/src/core/utils/datetime-formatter.js +1 -1
  63. package/src/core/utils/datetime-parser.js +1 -1
  64. package/src/core/utils/db.js +1 -1
  65. package/src/core/utils/logger.js +1 -1
  66. package/src/core/utils/payload-loader.js +1 -1
  67. package/src/core/utils/security-checks.js +1 -1
  68. package/src/middleware/body-options.js +1 -1
  69. package/src/middleware/cors.js +1 -1
  70. package/src/middleware/idempotency.js +1 -1
  71. package/src/middleware/rate-limiter.js +1 -1
  72. package/src/middleware/request-logger.js +1 -1
  73. package/src/middleware/security-headers.js +1 -1
  74. package/src/models/base-model-mysql.js +1 -1
  75. package/src/models/base-model-oracle.js +1 -1
  76. package/src/models/base-model-sqlite.js +1 -1
  77. package/src/models/base-model.js +1 -1
  78. package/src/pro/caching/redis-client.js +1 -1
  79. package/src/pro/caching/redis-helper.js +1 -1
  80. package/src/pro/consumers/baseConsumer.js +1 -1
  81. package/src/pro/consumers/declarativeMapper.js +1 -1
  82. package/src/pro/consumers/handlers/apiHandler.js +1 -1
  83. package/src/pro/consumers/handlers/consoleHandler.js +1 -1
  84. package/src/pro/consumers/handlers/databaseHandler.js +1 -1
  85. package/src/pro/consumers/handlers/index.js +1 -1
  86. package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
  87. package/src/pro/consumers/index.js +1 -1
  88. package/src/pro/consumers/messageTransformer.js +1 -1
  89. package/src/pro/consumers/validator.js +1 -1
  90. package/src/pro/database/base-model-mysql.js +1 -1
  91. package/src/pro/database/base-model-oracle.js +1 -1
  92. package/src/pro/database/base-model-sqlite.js +1 -1
  93. package/src/pro/database/db-mysql.js +1 -1
  94. package/src/pro/database/db-oracle.js +1 -1
  95. package/src/pro/database/db-sqlite.js +1 -1
  96. package/src/pro/excel/excel-generator.js +1 -1
  97. package/src/pro/excel/excel-parser.js +1 -1
  98. package/src/pro/excel/export-service.js +1 -1
  99. package/src/pro/excel/export_handler.js +1 -1
  100. package/src/pro/excel/import-service.js +1 -1
  101. package/src/pro/excel/import-validator.js +1 -1
  102. package/src/pro/excel/import_handler.js +1 -1
  103. package/src/pro/excel/upsert-builder.js +1 -1
  104. package/src/pro/idgen/idgen-routes.js +1 -1
  105. package/src/pro/integrations/lookup-resolver.js +1 -1
  106. package/src/pro/integrations/upload-handler-v2.js +1 -1
  107. package/src/pro/integrations/upload-handler.js +1 -1
  108. package/src/pro/integrations/webhook.js +1 -1
  109. package/src/pro/locking/lock-routes.js +1 -1
  110. package/src/pro/locking/resource-lock-manager.js +1 -1
  111. package/src/pro/messaging/kafkaConsumerService.js +1 -1
  112. package/src/pro/messaging/kafkaService.js +1 -1
  113. package/src/pro/messaging/messagehubService.js +1 -1
  114. package/src/pro/messaging/rabbitmqService.js +1 -1
  115. package/src/pro/scheduler/job-manager.js +1 -1
  116. package/src/pro/scheduler/job-routes.js +1 -1
  117. package/src/pro/scheduler/job-validator.js +1 -1
  118. package/src/pro/storage/base-storage-provider.js +1 -1
  119. package/src/pro/storage/file-metadata-helper.js +1 -1
  120. package/src/pro/storage/index.js +1 -1
  121. package/src/pro/storage/local-storage-provider.js +1 -1
  122. package/src/pro/storage/s3-storage-provider.js +1 -1
  123. package/src/pro/storage/upload-cleanup-job.js +1 -1
  124. package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
  125. package/src/pro/storage/upload-pending-tracker.js +1 -1
  126. package/src/pro/websocket/broadcast-helper.js +1 -1
  127. package/src/pro/websocket/index.js +1 -1
  128. package/src/pro/websocket/livesync-server.js +1 -1
  129. package/src/pro/websocket/ws-broadcaster.js +1 -1
  130. package/src/services/export-service.js +1 -1
  131. package/src/services/import-service.js +1 -1
  132. package/src/services/kafkaConsumerService.js +1 -1
  133. package/src/services/kafkaService.js +1 -1
  134. package/src/services/messagehubService.js +1 -1
  135. package/src/services/rabbitmqService.js +1 -1
  136. package/src/utils/cache-invalidation-registry.js +1 -1
  137. package/src/utils/cache-manager.js +1 -1
  138. package/src/utils/component-engine.js +1 -1
  139. package/src/utils/config-extractor.js +1 -1
  140. package/src/utils/consumerLogger.js +1 -1
  141. package/src/utils/context-builder.js +1 -1
  142. package/src/utils/dashboard-helpers.js +1 -1
  143. package/src/utils/dateHelper.js +1 -1
  144. package/src/utils/datetime-formatter.js +1 -1
  145. package/src/utils/datetime-parser.js +1 -1
  146. package/src/utils/db-bootstrap.js +1 -1
  147. package/src/utils/db-mysql.js +1 -1
  148. package/src/utils/db-oracle.js +1 -1
  149. package/src/utils/db-sqlite.js +1 -1
  150. package/src/utils/db.js +1 -1
  151. package/src/utils/demo-generator.js +1 -1
  152. package/src/utils/excel-generator.js +1 -1
  153. package/src/utils/excel-parser.js +1 -1
  154. package/src/utils/file-watcher.js +1 -1
  155. package/src/utils/id-generator.js +1 -1
  156. package/src/utils/idempotency-manager.js +1 -1
  157. package/src/utils/import-validator.js +1 -1
  158. package/src/utils/license-client.js +1 -1
  159. package/src/utils/lock-manager.js +1 -1
  160. package/src/utils/logger.js +1 -1
  161. package/src/utils/lookup-resolver.js +1 -1
  162. package/src/utils/payload-loader.js +1 -1
  163. package/src/utils/processor-response.js +1 -1
  164. package/src/utils/rabbitmq.js +1 -1
  165. package/src/utils/redis-client.js +1 -1
  166. package/src/utils/redis-helper.js +1 -1
  167. package/src/utils/request-scope.js +1 -1
  168. package/src/utils/security-checks.js +1 -1
  169. package/src/utils/service-resolver.js +1 -1
  170. package/src/utils/shutdown-coordinator.js +1 -1
  171. package/src/utils/soft-delete-dashboard-guard.js +1 -1
  172. package/src/utils/sql-table-extractor.js +1 -1
  173. package/src/utils/trusted-keys.js +1 -1
  174. package/src/utils/upload-handler.js +1 -1
  175. package/src/utils/upsert-builder.js +1 -1
  176. package/src/utils/workflow-hook-executor.js +1 -1
@@ -33,6 +33,12 @@ const { spawnSync } = require('node:child_process');
33
33
  // mengembalikan IR model dengan relasi ter-normalisasi (sumber FK yang andal).
34
34
  const { loadSchemaPath } = require('../lib/dbschema-kit/loader');
35
35
 
36
+ // Pembaca .env (parse KEY=VALUE, preserve diabaikan) dan resolver default config.
37
+ // Dipakai agar nilai fast-track mengikuti file config yang sudah ada, bukan
38
+ // semata DEFAULTS hardcode.
39
+ const { readEnvFile } = require('../lib/utils/env-manager');
40
+ const { getDefaultConfig, resolveConfigFilePath } = require('../lib/utils/config-resolver');
41
+
36
42
  // ---------------------------------------------------------------------------
37
43
  // Default konfigurasi (selaras dengan restforge-playbook/fast-track.mjs)
38
44
  // ---------------------------------------------------------------------------
@@ -162,7 +168,7 @@ function createPrompter() {
162
168
  });
163
169
  };
164
170
 
165
- return { ask, close: () => rl.close() };
171
+ return { ask, close: () => rl.close(), rl };
166
172
  }
167
173
 
168
174
  /**
@@ -275,11 +281,70 @@ function checkDesigner() {
275
281
  console.log(leader(' > restforge-designer', `${version || 'installed'} OK`, 32));
276
282
  }
277
283
 
284
+ // ---------------------------------------------------------------------------
285
+ // Resolusi file config sumber nilai (mengikuti file config yang sudah ada)
286
+ // ---------------------------------------------------------------------------
287
+
288
+ /** Daftar nama file .env di folder config/ (non-rekursif). */
289
+ function listConfigEnvFiles(cwd) {
290
+ const dir = path.join(cwd, 'config');
291
+ if (!fs.existsSync(dir)) return [];
292
+ return fs.readdirSync(dir, { withFileTypes: true })
293
+ .filter((e) => e.isFile() && e.name.endsWith('.env'))
294
+ .map((e) => e.name);
295
+ }
296
+
297
+ /**
298
+ * Tentukan file config yang menjadi SUMBER nilai prompt fast-track, lalu baca
299
+ * nilainya. Prioritas:
300
+ * 1. --config eksplisit -> hormati nama itu.
301
+ * 2. Tepat 1 file .env di config/ -> pakai file itu.
302
+ * 3. >1 file .env di config/ -> pakai default (config get-default /
303
+ * .restforge/defaults.json); bila default belum di-set -> berhenti.
304
+ * 4. 0 file -> fallback DEFAULT_CONFIG_FILE tanpa
305
+ * pre-fill (init akan membuat file ini, perilaku lama).
306
+ *
307
+ * @returns {{ configName: string, fileCfg: Object }} fileCfg = nilai env
308
+ * terbaca ({} bila file belum ada).
309
+ */
310
+ function resolveSourceConfig(cwd, explicitConfig) {
311
+ let configName;
312
+
313
+ if (explicitConfig) {
314
+ configName = explicitConfig;
315
+ } else {
316
+ const files = listConfigEnvFiles(cwd);
317
+ if (files.length === 1) {
318
+ configName = files[0];
319
+ } else if (files.length > 1) {
320
+ const def = getDefaultConfig(cwd);
321
+ if (!def) {
322
+ console.log('');
323
+ console.log(` ERROR: ${files.length} config files found in config/ but no default is set.`);
324
+ console.log(' Set which one fast-track should follow:');
325
+ console.log('');
326
+ console.log(' npx restforge config set-default --config=<file>');
327
+ console.log('');
328
+ console.log(' Or pass it explicitly: fast-track ... --config=<file>');
329
+ console.log('');
330
+ throw stop('multiple config files, no default set');
331
+ }
332
+ configName = def.config;
333
+ } else {
334
+ // 0 file: pertahankan perilaku lama (init akan membuat file ini).
335
+ return { configName: DEFAULT_CONFIG_FILE, fileCfg: {} };
336
+ }
337
+ }
338
+
339
+ const { data } = readEnvFile(resolveConfigFilePath(configName, cwd));
340
+ return { configName, fileCfg: data || {} };
341
+ }
342
+
278
343
  // ---------------------------------------------------------------------------
279
344
  // Fase input konfigurasi (LICENSE + database), mengikuti fast-track.mjs
280
345
  // ---------------------------------------------------------------------------
281
346
 
282
- async function collectConfig(args, ask) {
347
+ async function collectConfig(args, ask, fileCfg = {}) {
283
348
  console.log('');
284
349
  console.log(rule('='));
285
350
  console.log(' RESTForge Fast-Track — Configuration');
@@ -295,37 +360,39 @@ async function collectConfig(args, ask) {
295
360
 
296
361
  const cfg = {};
297
362
 
298
- // LICENSE: bila --license diberikan, dipakai sebagai default prompt.
299
- const licenseDefault = args.license || DEFAULTS.LICENSE;
363
+ // LICENSE: bila --license diberikan dipakai sebagai default; jika tidak,
364
+ // ikuti file config bila ada, baru jatuh ke DEFAULTS.
365
+ const licenseDefault = args.license || fileCfg.LICENSE || DEFAULTS.LICENSE;
300
366
  cfg.LICENSE = await askField('LICENSE', licenseDefault);
301
367
 
302
368
  // Alamat & port. Label user-facing REST_API_PORT, namun TETAP disimpan dan
303
369
  // ditulis ke db-connection.env sebagai SERVER_PORT (key yang dikonsumsi
304
370
  // `restforge serve`). WEB_SERVER_PORT adalah port frontend (`npx serve`),
305
- // dialirkan via `payload migrate --port` — tidak masuk runtime config.
306
- cfg.SERVER_ADDRESS = await askField('SERVER_ADDRESS', DEFAULTS.SERVER_ADDRESS);
307
- cfg.SERVER_PORT = await askField('REST_API_PORT', DEFAULTS.SERVER_PORT);
371
+ // dialirkan via `payload migrate --port` — tidak masuk runtime config, jadi
372
+ // selalu dari DEFAULTS (tidak tersimpan di file env).
373
+ cfg.SERVER_ADDRESS = await askField('SERVER_ADDRESS', fileCfg.SERVER_ADDRESS || DEFAULTS.SERVER_ADDRESS);
374
+ cfg.SERVER_PORT = await askField('REST_API_PORT', fileCfg.SERVER_PORT || DEFAULTS.SERVER_PORT);
308
375
  cfg.WEB_SERVER_PORT = await askField('WEB_SERVER_PORT', DEFAULTS.WEB_SERVER_PORT);
309
376
 
310
377
  // DB_TYPE menentukan atribut yang diminta berikutnya.
311
378
  console.log('');
312
379
  console.log(' Available DB_TYPE: postgresql, mysql, oracle, sqlite');
313
- cfg.DB_TYPE = (await askField('DB_TYPE', DEFAULTS.DB_TYPE)).toLowerCase();
380
+ cfg.DB_TYPE = (await askField('DB_TYPE', fileCfg.DB_TYPE || DEFAULTS.DB_TYPE)).toLowerCase();
314
381
 
315
382
  if (cfg.DB_TYPE === 'sqlite') {
316
383
  console.log('');
317
384
  console.log(' SQLite mode: DB_HOST, DB_PORT, DB_USER, DB_PASSWORD are ignored.');
318
385
  console.log(' The database file path is set in DB_FILE.');
319
386
  console.log('');
320
- cfg.DB_FILE = await askField('DB_FILE (.db file path)', DEFAULTS.DB_FILE);
387
+ cfg.DB_FILE = await askField('DB_FILE (.db file path)', fileCfg.DB_FILE || DEFAULTS.DB_FILE);
321
388
  cfg.DB_NAME = cfg.DB_FILE;
322
389
  } else {
323
390
  const dbDef = DB_TYPE_DEFAULTS[cfg.DB_TYPE] || {};
324
- cfg.DB_HOST = await askField('DB_HOST', DEFAULTS.DB_HOST);
325
- cfg.DB_PORT = await askField('DB_PORT', dbDef.DB_PORT || DEFAULTS.DB_PORT);
326
- cfg.DB_USER = await askField('DB_USER', dbDef.DB_USER || DEFAULTS.DB_USER);
327
- cfg.DB_PASSWORD = await askField('DB_PASSWORD', DEFAULTS.DB_PASSWORD);
328
- cfg.DB_NAME = await askField('DB_NAME', dbDef.DB_NAME || DEFAULTS.DB_NAME);
391
+ cfg.DB_HOST = await askField('DB_HOST', fileCfg.DB_HOST || DEFAULTS.DB_HOST);
392
+ cfg.DB_PORT = await askField('DB_PORT', fileCfg.DB_PORT || dbDef.DB_PORT || DEFAULTS.DB_PORT);
393
+ cfg.DB_USER = await askField('DB_USER', fileCfg.DB_USER || dbDef.DB_USER || DEFAULTS.DB_USER);
394
+ cfg.DB_PASSWORD = await askField('DB_PASSWORD', fileCfg.DB_PASSWORD || DEFAULTS.DB_PASSWORD);
395
+ cfg.DB_NAME = await askField('DB_NAME', fileCfg.DB_NAME || dbDef.DB_NAME || DEFAULTS.DB_NAME);
329
396
  }
330
397
 
331
398
  return cfg;
@@ -335,21 +402,277 @@ async function collectConfig(args, ask) {
335
402
  // Fase pemilihan scope generate (menu)
336
403
  // ---------------------------------------------------------------------------
337
404
 
338
- async function selectScope(ask) {
339
- console.log('');
340
- console.log(' Select what to generate:');
341
- console.log('');
342
- console.log(' 1. Generate REST API Only');
343
- console.log(' 2. Generate REST API with Frontend Application');
405
+ // Item menu scope (teks tampil sama persis dengan menu angka lama agar perubahan
406
+ // hanya pada CARA memilih, bukan label). `key` memetakan ke SCOPES.
407
+ const SCOPE_MENU = [
408
+ { key: '1', text: 'Generate REST API Only' },
409
+ { key: '2', text: 'Generate REST API with Frontend Application' }
410
+ ];
411
+
412
+ /**
413
+ * Selektor menu interaktif gaya CLI installer (create-next-app): tombol panah
414
+ * ↑/↓ menggeser sorotan, Enter mengeksekusi pilihan. HANYA untuk TTY interaktif;
415
+ * pemanggil wajib menyediakan fallback non-TTY (lihat selectScope).
416
+ *
417
+ * Handoff stdin: `createPrompter` memegang readline di process.stdin. Selama menu
418
+ * aktif, readline di-pause DAN listener `keypress`-nya dilepas sementara (disimpan
419
+ * lalu dipulihkan) agar tidak ada line-editing yang ikut bereaksi terhadap tombol.
420
+ * Setelah pilihan dibuat, raw mode, listener, dan readline dikembalikan ke semula
421
+ * sehingga prompt-prompt berikutnya tetap berfungsi normal.
422
+ *
423
+ * @param {Object} opts
424
+ * @param {string} opts.title - Judul menu
425
+ * @param {string[]} opts.items - Teks tiap opsi
426
+ * @param {number} opts.initialIndex - Indeks sorotan awal
427
+ * @param {Object} opts.prompter - Hasil createPrompter (untuk pause/resume rl)
428
+ * @returns {Promise<number>} indeks opsi yang dipilih
429
+ */
430
+ function arrowSelect({ title, items, initialIndex, prompter }) {
431
+ return new Promise((resolve) => {
432
+ const stdin = process.stdin;
433
+ const rl = prompter && prompter.rl;
434
+ let index = Math.max(0, Math.min(initialIndex || 0, items.length - 1));
435
+
436
+ // Suspend prompter: pause readline + lepas listener keypress-nya agar hanya
437
+ // handler menu yang aktif (mencegah echo/line-edit ganda).
438
+ if (rl && typeof rl.pause === 'function') rl.pause();
439
+ readline.emitKeypressEvents(stdin);
440
+ const savedKeypress = stdin.listeners('keypress').slice();
441
+ for (const l of savedKeypress) stdin.removeListener('keypress', l);
442
+
443
+ const prevRaw = stdin.isRaw === true;
444
+ if (typeof stdin.setRawMode === 'function') stdin.setRawMode(true);
445
+ stdin.resume();
446
+
447
+ console.log('');
448
+ console.log(` ${title}`);
449
+ console.log('');
450
+
451
+ const renderLines = () => {
452
+ for (let i = 0; i < items.length; i++) {
453
+ const active = i === index;
454
+ const pointer = active ? '>' : ' ';
455
+ const label = active ? `\x1b[36m${items[i]}\x1b[0m` : items[i];
456
+ process.stdout.write(`\x1b[2K ${pointer} ${label}\n`);
457
+ }
458
+ };
459
+ const moveCursorUp = () => process.stdout.write(`\x1b[${items.length}A`);
460
+
461
+ renderLines();
462
+
463
+ const cleanup = () => {
464
+ stdin.removeListener('keypress', onKeypress);
465
+ if (typeof stdin.setRawMode === 'function') stdin.setRawMode(prevRaw);
466
+ for (const l of savedKeypress) stdin.on('keypress', l);
467
+ if (rl && typeof rl.resume === 'function') rl.resume();
468
+ };
469
+
470
+ const onKeypress = (_str, key) => {
471
+ if (!key) return;
472
+ if (key.name === 'up') {
473
+ index = (index - 1 + items.length) % items.length;
474
+ moveCursorUp();
475
+ renderLines();
476
+ } else if (key.name === 'down') {
477
+ index = (index + 1) % items.length;
478
+ moveCursorUp();
479
+ renderLines();
480
+ } else if (key.name === 'return' || key.name === 'enter') {
481
+ cleanup();
482
+ process.stdout.write('\n');
483
+ resolve(index);
484
+ } else if (key.ctrl && key.name === 'c') {
485
+ cleanup();
486
+ process.stdout.write('\n');
487
+ process.exit(130); // konvensi SIGINT
488
+ }
489
+ };
490
+
491
+ stdin.on('keypress', onKeypress);
492
+ });
493
+ }
494
+
495
+ async function selectScope(prompter) {
496
+ const ask = prompter.ask;
497
+
498
+ // Fallback non-TTY (input di-pipe: smoke/CI/transcript): pertahankan PERSIS
499
+ // prompt angka lama. Selektor panah butuh raw TTY mode yang tidak tersedia di
500
+ // sini, jadi fallback menjaga alur non-interaktif tetap berfungsi byte-identik.
501
+ if (!process.stdin.isTTY) {
502
+ console.log('');
503
+ console.log(' Select what to generate:');
504
+ console.log('');
505
+ for (const m of SCOPE_MENU) console.log(` ${m.key}. ${m.text}`);
506
+ console.log('');
507
+
508
+ let scope = null;
509
+ while (!scope) {
510
+ const choice = (await ask(` Choice (1-2) [${DEFAULT_SCOPE}]: `)).trim() || DEFAULT_SCOPE;
511
+ scope = SCOPES[choice];
512
+ if (!scope) console.log(' Invalid choice. Enter 1 or 2.');
513
+ }
514
+ return scope;
515
+ }
516
+
517
+ // TTY interaktif: selektor panah ↑/↓ + Enter.
518
+ const initialIndex = SCOPE_MENU.findIndex((m) => m.key === DEFAULT_SCOPE);
519
+ const chosen = await arrowSelect({
520
+ title: 'Select what to generate:',
521
+ items: SCOPE_MENU.map((m) => m.text),
522
+ initialIndex: initialIndex >= 0 ? initialIndex : 0,
523
+ prompter
524
+ });
525
+ return SCOPES[SCOPE_MENU[chosen].key];
526
+ }
527
+
528
+ // ---------------------------------------------------------------------------
529
+ // Fase pemilihan plugin designer + toggle authentication
530
+ // ---------------------------------------------------------------------------
531
+
532
+ // Plugin default bila pilihan tidak tersedia / parsing list gagal.
533
+ const DESIGNER_DEFAULT_PLUGIN = 'vanilla-js-basic';
534
+
535
+ // Default konfigurasi auth, selaras dengan `restforge-designer init`
536
+ // (mod.rs: auth_app_code/auth_api_url/idle_timeout). Dipakai saat user memilih
537
+ // mengaktifkan authentication pada plugin yang mendukung (Auth != No).
538
+ const DESIGNER_AUTH_DEFAULTS = {
539
+ appCode: 'K5BK0H3ATT',
540
+ authApiUrl: 'https://restforge.dev/api/auth',
541
+ idleTimeoutMinutes: 30
542
+ };
543
+
544
+ const ANSI_RE = /\x1b\[[0-9;]*m/g;
545
+
546
+ /**
547
+ * Jalankan `restforge-designer plugins list` dan kembalikan stdout mentah, atau
548
+ * null bila gagal (binary tidak ada / exit non-zero). Tidak butuh aktivasi license
549
+ * (list hanya membaca metadata plugin built-in).
550
+ */
551
+ function runDesignerPluginsList() {
552
+ const r = spawnSync('cmd', ['/S', '/C', 'restforge-designer plugins list'], { encoding: 'utf8' });
553
+ if (r.error || r.status !== 0) return null;
554
+ return r.stdout || '';
555
+ }
556
+
557
+ /**
558
+ * Parse output tabel `plugins list` (comfy-table UTF8_FULL) menjadi array
559
+ * `{ id, auth }`. Baris data ditandai border luar `│` + separator kolom `┆`;
560
+ * baris pemisah (├ ╞ ┌ └) dan header (`ID`) dilewati. ANSI di-strip dulu. Kolom
561
+ * Auth (indeks 3) menentukan apakah plugin mendukung authentication.
562
+ *
563
+ * Catatan: tidak ada output JSON di designer, jadi parsing tabel adalah satu-satunya
564
+ * antarmuka. Parser defensif: bila 0 baris terbaca, pemanggil fallback ke default.
565
+ */
566
+ function parseDesignerPlugins(stdout) {
567
+ const out = [];
568
+ const seen = new Set();
569
+ for (const rawLine of String(stdout || '').split(/\r?\n/)) {
570
+ const line = rawLine.replace(ANSI_RE, '');
571
+ // Baris data wajib punya border luar `│`; separator inner bisa `┆` (UTF8_FULL)
572
+ // atau `│`. Baris pemisah (├ ┌ └ ╞) tidak mengandung `│`, jadi otomatis lewat.
573
+ if (!line.includes('│')) continue;
574
+ const cells = line.split(/[│┆]/).map((c) => c.trim()).filter((c) => c.length > 0);
575
+ // Minimal 4 kolom (ID, Name, Version, Auth). Baris wrap continuation berkolom
576
+ // sedikit tersaring di sini.
577
+ if (cells.length < 4) continue;
578
+ const id = cells[0];
579
+ if (id === 'ID' || seen.has(id)) continue;
580
+ seen.add(id);
581
+ out.push({ id, auth: cells[3] || '' });
582
+ }
583
+ return out;
584
+ }
585
+
586
+ /** Plugin mendukung authentication bila kolom Auth bukan kosong dan bukan "No". */
587
+ function pluginHasAuth(auth) {
588
+ const v = String(auth || '').trim().toLowerCase();
589
+ return v !== '' && v !== 'no';
590
+ }
591
+
592
+ /**
593
+ * Pilih plugin designer (sebelum generate) lalu tentukan toggle auth. Menetapkan
594
+ * `ctx.plugin` dan `ctx.authEnabled`. Menampilkan tabel `plugins list` asli sebagai
595
+ * konteks, lalu selektor panah (TTY) atau prompt angka (non-TTY). Untuk plugin yang
596
+ * mendukung auth (mis. vanilla-js-auth/custom), tanyakan apakah auth diaktifkan;
597
+ * vanilla-js-basic (Auth No) langsung tanpa pertanyaan.
598
+ *
599
+ * Fallback aman: bila list gagal di-parse, pakai DESIGNER_DEFAULT_PLUGIN tanpa auth.
600
+ */
601
+ async function selectDesignerPlugin(ctx, prompter) {
602
+ const raw = runDesignerPluginsList();
603
+ const plugins = raw ? parseDesignerPlugins(raw) : [];
604
+
344
605
  console.log('');
606
+ if (raw && raw.trim()) console.log(raw.trimEnd());
345
607
 
346
- let scope = null;
347
- while (!scope) {
348
- const choice = (await ask(` Choice (1-2) [${DEFAULT_SCOPE}]: `)).trim() || DEFAULT_SCOPE;
349
- scope = SCOPES[choice];
350
- if (!scope) console.log(' Invalid choice. Enter 1 or 2.');
608
+ if (plugins.length === 0) {
609
+ console.log(` (could not read plugin list; using default '${DESIGNER_DEFAULT_PLUGIN}')`);
610
+ ctx.plugin = DESIGNER_DEFAULT_PLUGIN;
611
+ ctx.authEnabled = false;
612
+ return;
351
613
  }
352
- return scope;
614
+
615
+ const defaultIdx = Math.max(0, plugins.findIndex((p) => p.id === DESIGNER_DEFAULT_PLUGIN));
616
+ const items = plugins.map((p) => `${p.id} (auth: ${p.auth || 'No'})`);
617
+
618
+ let chosenIdx;
619
+ if (process.stdin.isTTY) {
620
+ chosenIdx = await arrowSelect({
621
+ title: 'Select a designer plugin:',
622
+ items,
623
+ initialIndex: defaultIdx,
624
+ prompter
625
+ });
626
+ } else {
627
+ console.log('');
628
+ console.log(' Select a designer plugin:');
629
+ console.log('');
630
+ items.forEach((it, i) => console.log(` ${i + 1}. ${it}`));
631
+ console.log('');
632
+ const def = String(defaultIdx + 1);
633
+ chosenIdx = null;
634
+ while (chosenIdx === null) {
635
+ const c = (await prompter.ask(` Choice (1-${plugins.length}) [${def}]: `)).trim() || def;
636
+ const idx = parseInt(c, 10) - 1;
637
+ if (idx >= 0 && idx < plugins.length) chosenIdx = idx;
638
+ else console.log(` Invalid choice. Enter 1-${plugins.length}.`);
639
+ }
640
+ }
641
+
642
+ const chosen = plugins[chosenIdx];
643
+ ctx.plugin = chosen.id;
644
+
645
+ if (pluginHasAuth(chosen.auth)) {
646
+ const ans = (await prompter.ask(` Use authentication for '${chosen.id}'? (Y/n): `)).trim().toLowerCase();
647
+ ctx.authEnabled = !(ans === 'n' || ans === 'no');
648
+ } else {
649
+ ctx.authEnabled = false;
650
+ }
651
+ }
652
+
653
+ /**
654
+ * Suntikkan blok auth ke payload agregator frontend agar plugin auth (deteksi via
655
+ * `payload.auth.is_some()`) men-generate aplikasi dengan authentication. Bentuk blok
656
+ * mengikuti `restforge-designer init` (init.rs): top-level `auth` + `appConfig.authAppCode`.
657
+ * Mengembalikan true bila berhasil ditulis.
658
+ */
659
+ function injectDesignerAuth(aggregatorPath) {
660
+ if (!fs.existsSync(aggregatorPath)) return false;
661
+ let payload;
662
+ try {
663
+ payload = JSON.parse(fs.readFileSync(aggregatorPath, 'utf8'));
664
+ } catch (_err) {
665
+ return false;
666
+ }
667
+ if (!payload.appConfig || typeof payload.appConfig !== 'object') payload.appConfig = {};
668
+ payload.appConfig.authAppCode = DESIGNER_AUTH_DEFAULTS.appCode;
669
+ payload.auth = {
670
+ appCode: DESIGNER_AUTH_DEFAULTS.appCode,
671
+ authApiUrl: DESIGNER_AUTH_DEFAULTS.authApiUrl,
672
+ idleTimeoutMinutes: DESIGNER_AUTH_DEFAULTS.idleTimeoutMinutes
673
+ };
674
+ fs.writeFileSync(aggregatorPath, JSON.stringify(payload, null, 2));
675
+ return true;
353
676
  }
354
677
 
355
678
  // ---------------------------------------------------------------------------
@@ -370,6 +693,9 @@ async function confirmDefaultMode(ctx, ask) {
370
693
  console.log(` REST API : ${ctx.cfg.SERVER_ADDRESS}:${ctx.cfg.SERVER_PORT} (SERVER_PORT)`);
371
694
  console.log(` Web server : ${ctx.cfg.SERVER_ADDRESS}:${ctx.cfg.WEB_SERVER_PORT}`);
372
695
  console.log(` Database : ${describeDatabase(ctx.cfg)}`);
696
+ if (ctx.scope.frontend) {
697
+ console.log(` Plugin : ${ctx.plugin || DESIGNER_DEFAULT_PLUGIN}${ctx.authEnabled ? ' (auth enabled)' : ''}`);
698
+ }
373
699
  console.log(' Mode : sync (use --overwrite to drop & regenerate)');
374
700
  console.log('');
375
701
  const answer = (await ask(' Continue? (Y/n): ')).trim().toLowerCase();
@@ -390,6 +716,9 @@ async function confirmOverwriteMode(ctx, ask) {
390
716
  console.log(` Generate : ${ctx.scope.label}`);
391
717
  console.log(` License : ${maskLicense(ctx.cfg.LICENSE)}`);
392
718
  console.log(` Database : ${describeDatabase(ctx.cfg)}`);
719
+ if (ctx.scope.frontend) {
720
+ console.log(` Plugin : ${ctx.plugin || DESIGNER_DEFAULT_PLUGIN}${ctx.authEnabled ? ' (auth enabled)' : ''}`);
721
+ }
393
722
  console.log('');
394
723
  console.log(' The following actions will run:');
395
724
  if (ctx.scope.backend) {
@@ -682,6 +1011,9 @@ function runFrontendPipeline(ctx) {
682
1011
  const cfgArg = `--config=${ctx.configFlag}`;
683
1012
  const frontendDir = path.join(ctx.cwd, 'frontend');
684
1013
  const appCode = ctx.project;
1014
+ // Plugin terpilih (default basic bila selectDesignerPlugin tidak dijalankan).
1015
+ const plugin = ctx.plugin || DESIGNER_DEFAULT_PLUGIN;
1016
+ const pluginArg = `--plugin=${plugin}`;
685
1017
 
686
1018
  phase('[F1/3] Migrate RDF -> UDF (per tabel, agregator di-akumulasi)');
687
1019
  fs.mkdirSync(path.join(frontendDir, 'payload'), { recursive: true });
@@ -693,7 +1025,17 @@ function runFrontendPipeline(ctx) {
693
1025
  console.log(` (skip ${t.kebab}: auto-discovered via JOIN from child)`);
694
1026
  continue;
695
1027
  }
696
- run(`npx restforge payload migrate --project=${ctx.project} --name=${t.kebab}.json --output=frontend/payload ${cfgArg} --port=${ctx.cfg.WEB_SERVER_PORT} --overwrite`, ctx.cwd);
1028
+ run(`npx restforge payload migrate --project=${ctx.project} --name=${t.kebab}.json --output=frontend/payload ${cfgArg} --port=${ctx.cfg.WEB_SERVER_PORT} ${pluginArg} --overwrite`, ctx.cwd);
1029
+ }
1030
+
1031
+ // Auth: suntik blok auth ke agregator sebelum generate bila user mengaktifkannya.
1032
+ // Plugin auth mendeteksi mode lewat keberadaan blok `auth` di payload (init.rs).
1033
+ if (ctx.authEnabled) {
1034
+ const aggregatorPath = path.join(frontendDir, 'payload', `${appCode}.json`);
1035
+ const ok = injectDesignerAuth(aggregatorPath);
1036
+ console.log(ok
1037
+ ? ` ~ authentication enabled in ${appCode}.json (authAppCode=${DESIGNER_AUTH_DEFAULTS.appCode})`
1038
+ : ` [WARN] could not inject auth block into ${aggregatorPath}; app will generate without auth`);
697
1039
  }
698
1040
 
699
1041
  phase('[F2/3] Activate restforge-designer');
@@ -702,7 +1044,7 @@ function runFrontendPipeline(ctx) {
702
1044
  phase('[F3/3] Generate frontend application');
703
1045
  // Hapus index.html lama agar landing page diregenerasi sesuai set page terbaru.
704
1046
  run(`if exist apps\\${ctx.project}\\index.html del /Q apps\\${ctx.project}\\index.html`, frontendDir, { allowNonZero: true });
705
- run(`restforge-designer generate --payload=payload/${appCode}.json --output=./apps/${ctx.project} --overwrite`, frontendDir);
1047
+ run(`restforge-designer generate --payload=payload/${appCode}.json --output=./apps/${ctx.project} ${pluginArg} --overwrite`, frontendDir);
706
1048
  }
707
1049
 
708
1050
  /**
@@ -859,8 +1201,8 @@ module.exports = {
859
1201
  config: {
860
1202
  type: 'string',
861
1203
  required: false,
862
- default: DEFAULT_CONFIG_FILE,
863
- description: 'Nama file env target di folder config/ (ditulis dari input; default db-connection.env)'
1204
+ default: null,
1205
+ description: 'Nama file env target di folder config/. Bila kosong, fast-track mengikuti file config yang ada: 1 file dipakai langsung, >1 file pakai config get-default'
864
1206
  },
865
1207
  license: {
866
1208
  type: 'string',
@@ -887,11 +1229,16 @@ module.exports = {
887
1229
 
888
1230
  const prompter = createPrompter();
889
1231
  try {
1232
+ // 0) Resolusi file config sumber: tentukan file mana yang diikuti dan
1233
+ // baca nilainya untuk pre-fill default prompt.
1234
+ const { configName, fileCfg } = resolveSourceConfig(cwd, args.config);
1235
+
890
1236
  // 1) Input konfigurasi (LICENSE + database), gaya fast-track.mjs.
891
- const cfg = await collectConfig(args, prompter.ask);
1237
+ // Default tiap field mengikuti fileCfg bila tersedia.
1238
+ const cfg = await collectConfig(args, prompter.ask, fileCfg);
892
1239
 
893
1240
  // 2) Menu pemilihan scope generate (REST API / frontend / all).
894
- const scope = await selectScope(prompter.ask);
1241
+ const scope = await selectScope(prompter);
895
1242
 
896
1243
  // 3) Preflight designer hanya bila scope mencakup frontend.
897
1244
  if (scope.frontend) {
@@ -902,7 +1249,7 @@ module.exports = {
902
1249
  cwd,
903
1250
  project: args.project,
904
1251
  schemaFlag: args['schema-path'],
905
- configFlag: args.config,
1252
+ configFlag: configName,
906
1253
  tables,
907
1254
  cfg,
908
1255
  scope,
@@ -929,14 +1276,21 @@ module.exports = {
929
1276
  }
930
1277
  }
931
1278
 
932
- // 4) Preview + konfirmasi (sesuai mode).
1279
+ // 4) Pemilihan plugin designer + toggle auth (hanya bila frontend).
1280
+ // Sebelum preview agar pilihan ikut tampil di ringkasan & dipakai
1281
+ // pipeline frontend (payload migrate --plugin + generate --plugin).
1282
+ if (ctx.scope.frontend) {
1283
+ await selectDesignerPlugin(ctx, prompter);
1284
+ }
1285
+
1286
+ // 5) Preview + konfirmasi (sesuai mode).
933
1287
  if (ctx.overwrite) {
934
1288
  await confirmOverwriteMode(ctx, prompter.ask);
935
1289
  } else {
936
1290
  await confirmDefaultMode(ctx, prompter.ask);
937
1291
  }
938
1292
 
939
- // 5) Eksekusi sesuai scope.
1293
+ // 6) Eksekusi sesuai scope.
940
1294
  if (ctx.scope.backend) {
941
1295
  runBackendPipeline(ctx);
942
1296
  // Launcher start REST API mandiri (sesuai OS), di folder kerja.
@@ -948,7 +1302,7 @@ module.exports = {
948
1302
 
949
1303
  printFinalSummary(ctx);
950
1304
 
951
- // 6) Tawarkan menjalankan service: runtime server (backend) lalu
1305
+ // 7) Tawarkan menjalankan service: runtime server (backend) lalu
952
1306
  // aplikasi frontend, masing-masing di window CMD baru.
953
1307
  if (ctx.scope.backend) {
954
1308
  await maybeRunServer(ctx, prompter.ask);
@@ -966,5 +1320,9 @@ module.exports = {
966
1320
  // terpisah tanpa menjalankan pipeline penuh. Pada penggunaan CLI normal env ini
967
1321
  // tidak di-set sehingga export tetap berupa contract murni.
968
1322
  if (process.env.FASTTRACK_TEST === '1') {
969
- module.exports.__test = { loadModels, buildTableEntries, fkColumnsForEntry, tableToKebab };
1323
+ module.exports.__test = {
1324
+ loadModels, buildTableEntries, fkColumnsForEntry, tableToKebab,
1325
+ parseDesignerPlugins, pluginHasAuth, injectDesignerAuth,
1326
+ DESIGNER_DEFAULT_PLUGIN, DESIGNER_AUTH_DEFAULTS
1327
+ };
970
1328
  }
@@ -403,7 +403,7 @@ function createEndpointFile(moduleDir, moduleName, endpointName, payload) {
403
403
 
404
404
  const endpointExists = fs.existsSync(endpointFilePath);
405
405
  if (endpointExists) {
406
- console.log(` [UPDATE] ${endpointName}.js — endpoint router di-regenerate.`);
406
+ console.log(` [UPDATE] ${endpointName}.js endpoint router di-regenerate.`);
407
407
  }
408
408
 
409
409
  const importProcessors = payload.processor.map(proc => {
@@ -438,7 +438,7 @@ function createEndpointFile(moduleDir, moduleName, endpointName, payload) {
438
438
  const reason = isMutationMethod
439
439
  ? `method ${method.toUpperCase()} is a mutation operation`
440
440
  : `SQL contains mutation statement`;
441
- console.log(` [WARN] cache.enabled ignored for processor "${proc.name}" — ${reason}. Cache only applies to read (SELECT) operations.`);
441
+ console.log(` [WARN] cache.enabled ignored for processor "${proc.name}" ${reason}. Cache only applies to read (SELECT) operations.`);
442
442
  hasCache = false;
443
443
  }
444
444
  }
@@ -523,7 +523,7 @@ module.exports = router;`;
523
523
 
524
524
  fs.writeFileSync(endpointFilePath, endpointContent);
525
525
  if (!endpointExists) {
526
- console.log(` [NEW] ${endpointName}.js — endpoint router created successfully.`);
526
+ console.log(` [NEW] ${endpointName}.js endpoint router created successfully.`);
527
527
  }
528
528
  }
529
529
 
@@ -562,17 +562,17 @@ function createProcessorFiles(moduleDir, moduleName, endpointName, payload, forc
562
562
 
563
563
  if (fs.existsSync(processorFilePath)) {
564
564
  if (!force) {
565
- console.log(` [SKIP] ${proc.name}.js — file already exists, custom code preserved.`);
565
+ console.log(` [SKIP] ${proc.name}.js file already exists, custom code preserved.`);
566
566
  skippedCount++;
567
567
  continue;
568
568
  }
569
- console.log(` [ARCHIVE+OVERWRITE] ${proc.name}.js — file archived then regenerated.`);
569
+ console.log(` [ARCHIVE+OVERWRITE] ${proc.name}.js file archived then regenerated.`);
570
570
  archivedCount++;
571
571
  }
572
572
 
573
573
  const processorContent = generateProcessor(proc, payloadDir);
574
574
  fs.writeFileSync(processorFilePath, processorContent);
575
- console.log(` [NEW] ${proc.name}.js — scaffold created successfully.`);
575
+ console.log(` [NEW] ${proc.name}.js scaffold created successfully.`);
576
576
  createdCount++;
577
577
  }
578
578
 
@@ -745,7 +745,7 @@ ${validationCode}
745
745
  // { sql: 'UPDATE ...', params: [...] }
746
746
  // ]);
747
747
 
748
- // Placeholder — ganti dengan implementasi sebenarnya
748
+ // Placeholder ganti dengan implementasi sebenarnya
749
749
  const result = [];
750
750
 
751
751
  return createResponse(200, '${successMessage}', result);