@restforgejs/platform 5.3.6 → 5.3.9

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 (171) 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/cli/designer.js +1 -1
  5. package/generators/cli/fast-track.js +272 -53
  6. package/generators/cli/init.js +37 -3
  7. package/generators/lib/templates/dashboard-catalog.js +1 -1
  8. package/generators/lib/templates/db-connection-env.js +1 -1
  9. package/generators/lib/templates/dbschema-catalog.js +1 -1
  10. package/generators/lib/templates/field-validation-catalog.js +1 -1
  11. package/generators/lib/templates/mysql-template.js +1 -1
  12. package/generators/lib/templates/oracle-template.js +1 -1
  13. package/generators/lib/templates/postgres-template.js +1 -1
  14. package/generators/lib/templates/query-declarative-catalog.js +1 -1
  15. package/generators/lib/templates/sqlite-template.js +1 -1
  16. package/integrity-manifest.json +18 -18
  17. package/package.json +1 -1
  18. package/scripts/verify-integrity.js +1 -1
  19. package/server.js +1 -1
  20. package/src/components/handlers/adjust_handler.js +1 -1
  21. package/src/components/handlers/audit_handler.js +1 -1
  22. package/src/components/handlers/delete_handler.js +1 -1
  23. package/src/components/handlers/export_handler.js +1 -1
  24. package/src/components/handlers/import_handler.js +1 -1
  25. package/src/components/handlers/insert_handler.js +1 -1
  26. package/src/components/handlers/update_handler.js +1 -1
  27. package/src/components/handlers/upload_handler.js +1 -1
  28. package/src/components/handlers/workflow_handler.js +1 -1
  29. package/src/components/integrations/webhook.js +1 -1
  30. package/src/consumers/baseConsumer.js +1 -1
  31. package/src/consumers/declarativeMapper.js +1 -1
  32. package/src/consumers/handlers/apiHandler.js +1 -1
  33. package/src/consumers/handlers/consoleHandler.js +1 -1
  34. package/src/consumers/handlers/databaseHandler.js +1 -1
  35. package/src/consumers/handlers/index.js +1 -1
  36. package/src/consumers/handlers/kafkaHandler.js +1 -1
  37. package/src/consumers/index.js +1 -1
  38. package/src/consumers/messageTransformer.js +1 -1
  39. package/src/consumers/validator.js +1 -1
  40. package/src/core/db/dialect/base-dialect.js +1 -1
  41. package/src/core/db/dialect/index.js +1 -1
  42. package/src/core/db/dialect/mysql-dialect.js +1 -1
  43. package/src/core/db/dialect/oracle-dialect.js +1 -1
  44. package/src/core/db/dialect/postgres-dialect.js +1 -1
  45. package/src/core/db/dialect/sqlite-dialect.js +1 -1
  46. package/src/core/db/flatten-helper.js +1 -1
  47. package/src/core/db/query-builder-error.js +1 -1
  48. package/src/core/db/query-builder.js +1 -1
  49. package/src/core/db/relation-helper.js +1 -1
  50. package/src/core/handlers/delete_handler.js +1 -1
  51. package/src/core/handlers/insert_handler.js +1 -1
  52. package/src/core/handlers/update_handler.js +1 -1
  53. package/src/core/models/base-model.js +1 -1
  54. package/src/core/utils/cache-manager.js +1 -1
  55. package/src/core/utils/component-engine.js +1 -1
  56. package/src/core/utils/context-builder.js +1 -1
  57. package/src/core/utils/datetime-formatter.js +1 -1
  58. package/src/core/utils/datetime-parser.js +1 -1
  59. package/src/core/utils/db.js +1 -1
  60. package/src/core/utils/logger.js +1 -1
  61. package/src/core/utils/payload-loader.js +1 -1
  62. package/src/core/utils/security-checks.js +1 -1
  63. package/src/middleware/body-options.js +1 -1
  64. package/src/middleware/cors.js +1 -1
  65. package/src/middleware/idempotency.js +1 -1
  66. package/src/middleware/rate-limiter.js +1 -1
  67. package/src/middleware/request-logger.js +1 -1
  68. package/src/middleware/security-headers.js +1 -1
  69. package/src/models/base-model-mysql.js +1 -1
  70. package/src/models/base-model-oracle.js +1 -1
  71. package/src/models/base-model-sqlite.js +1 -1
  72. package/src/models/base-model.js +1 -1
  73. package/src/pro/caching/redis-client.js +1 -1
  74. package/src/pro/caching/redis-helper.js +1 -1
  75. package/src/pro/consumers/baseConsumer.js +1 -1
  76. package/src/pro/consumers/declarativeMapper.js +1 -1
  77. package/src/pro/consumers/handlers/apiHandler.js +1 -1
  78. package/src/pro/consumers/handlers/consoleHandler.js +1 -1
  79. package/src/pro/consumers/handlers/databaseHandler.js +1 -1
  80. package/src/pro/consumers/handlers/index.js +1 -1
  81. package/src/pro/consumers/handlers/kafkaHandler.js +1 -1
  82. package/src/pro/consumers/index.js +1 -1
  83. package/src/pro/consumers/messageTransformer.js +1 -1
  84. package/src/pro/consumers/validator.js +1 -1
  85. package/src/pro/database/base-model-mysql.js +1 -1
  86. package/src/pro/database/base-model-oracle.js +1 -1
  87. package/src/pro/database/base-model-sqlite.js +1 -1
  88. package/src/pro/database/db-mysql.js +1 -1
  89. package/src/pro/database/db-oracle.js +1 -1
  90. package/src/pro/database/db-sqlite.js +1 -1
  91. package/src/pro/excel/excel-generator.js +1 -1
  92. package/src/pro/excel/excel-parser.js +1 -1
  93. package/src/pro/excel/export-service.js +1 -1
  94. package/src/pro/excel/export_handler.js +1 -1
  95. package/src/pro/excel/import-service.js +1 -1
  96. package/src/pro/excel/import-validator.js +1 -1
  97. package/src/pro/excel/import_handler.js +1 -1
  98. package/src/pro/excel/upsert-builder.js +1 -1
  99. package/src/pro/idgen/idgen-routes.js +1 -1
  100. package/src/pro/integrations/lookup-resolver.js +1 -1
  101. package/src/pro/integrations/upload-handler-v2.js +1 -1
  102. package/src/pro/integrations/upload-handler.js +1 -1
  103. package/src/pro/integrations/webhook.js +1 -1
  104. package/src/pro/locking/lock-routes.js +1 -1
  105. package/src/pro/locking/resource-lock-manager.js +1 -1
  106. package/src/pro/messaging/kafkaConsumerService.js +1 -1
  107. package/src/pro/messaging/kafkaService.js +1 -1
  108. package/src/pro/messaging/messagehubService.js +1 -1
  109. package/src/pro/messaging/rabbitmqService.js +1 -1
  110. package/src/pro/scheduler/job-manager.js +1 -1
  111. package/src/pro/scheduler/job-routes.js +1 -1
  112. package/src/pro/scheduler/job-validator.js +1 -1
  113. package/src/pro/storage/base-storage-provider.js +1 -1
  114. package/src/pro/storage/file-metadata-helper.js +1 -1
  115. package/src/pro/storage/index.js +1 -1
  116. package/src/pro/storage/local-storage-provider.js +1 -1
  117. package/src/pro/storage/s3-storage-provider.js +1 -1
  118. package/src/pro/storage/upload-cleanup-job.js +1 -1
  119. package/src/pro/storage/upload-cleanup-scheduler.js +1 -1
  120. package/src/pro/storage/upload-pending-tracker.js +1 -1
  121. package/src/pro/websocket/broadcast-helper.js +1 -1
  122. package/src/pro/websocket/index.js +1 -1
  123. package/src/pro/websocket/livesync-server.js +1 -1
  124. package/src/pro/websocket/ws-broadcaster.js +1 -1
  125. package/src/services/export-service.js +1 -1
  126. package/src/services/import-service.js +1 -1
  127. package/src/services/kafkaConsumerService.js +1 -1
  128. package/src/services/kafkaService.js +1 -1
  129. package/src/services/messagehubService.js +1 -1
  130. package/src/services/rabbitmqService.js +1 -1
  131. package/src/utils/cache-invalidation-registry.js +1 -1
  132. package/src/utils/cache-manager.js +1 -1
  133. package/src/utils/component-engine.js +1 -1
  134. package/src/utils/config-extractor.js +1 -1
  135. package/src/utils/consumerLogger.js +1 -1
  136. package/src/utils/context-builder.js +1 -1
  137. package/src/utils/dashboard-helpers.js +1 -1
  138. package/src/utils/dateHelper.js +1 -1
  139. package/src/utils/datetime-formatter.js +1 -1
  140. package/src/utils/datetime-parser.js +1 -1
  141. package/src/utils/db-bootstrap.js +1 -1
  142. package/src/utils/db-mysql.js +1 -1
  143. package/src/utils/db-oracle.js +1 -1
  144. package/src/utils/db-sqlite.js +1 -1
  145. package/src/utils/db.js +1 -1
  146. package/src/utils/demo-generator.js +1 -1
  147. package/src/utils/excel-generator.js +1 -1
  148. package/src/utils/excel-parser.js +1 -1
  149. package/src/utils/file-watcher.js +1 -1
  150. package/src/utils/id-generator.js +1 -1
  151. package/src/utils/idempotency-manager.js +1 -1
  152. package/src/utils/import-validator.js +1 -1
  153. package/src/utils/license-client.js +1 -1
  154. package/src/utils/lock-manager.js +1 -1
  155. package/src/utils/logger.js +1 -1
  156. package/src/utils/lookup-resolver.js +1 -1
  157. package/src/utils/payload-loader.js +1 -1
  158. package/src/utils/processor-response.js +1 -1
  159. package/src/utils/rabbitmq.js +1 -1
  160. package/src/utils/redis-client.js +1 -1
  161. package/src/utils/redis-helper.js +1 -1
  162. package/src/utils/request-scope.js +1 -1
  163. package/src/utils/security-checks.js +1 -1
  164. package/src/utils/service-resolver.js +1 -1
  165. package/src/utils/shutdown-coordinator.js +1 -1
  166. package/src/utils/soft-delete-dashboard-guard.js +1 -1
  167. package/src/utils/sql-table-extractor.js +1 -1
  168. package/src/utils/trusted-keys.js +1 -1
  169. package/src/utils/upload-handler.js +1 -1
  170. package/src/utils/upsert-builder.js +1 -1
  171. package/src/utils/workflow-hook-executor.js +1 -1
package/cli/designer.js CHANGED
@@ -1,3 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- 'use strict';const a0_0x4847c4=a0_0x4fdf;(function(_0x6f86f,_0x115950){const _0x373e45=a0_0x4fdf,_0x582a23=_0x6f86f();while(!![]){try{const _0xcefdd=parseInt(_0x373e45(0x75))/0x1+parseInt(_0x373e45(0x6f))/0x2+-parseInt(_0x373e45(0x69))/0x3+-parseInt(_0x373e45(0x7b))/0x4+parseInt(_0x373e45(0x6b))/0x5*(-parseInt(_0x373e45(0x84))/0x6)+parseInt(_0x373e45(0x81))/0x7*(parseInt(_0x373e45(0x72))/0x8)+parseInt(_0x373e45(0x7e))/0x9*(parseInt(_0x373e45(0x79))/0xa);if(_0xcefdd===_0x115950)break;else _0x582a23['push'](_0x582a23['shift']());}catch(_0x3305cf){_0x582a23['push'](_0x582a23['shift']());}}}(a0_0x2899,0x96368));const os=require('os'),path=require(a0_0x4847c4(0x6a)),fs=require('fs'),{spawn}=require(a0_0x4847c4(0x80));function resolveBinaryPath(){const _0x30d11d=a0_0x4847c4,_0x436504={'AuxED':function(_0x24b112,_0x47a85b){return _0x24b112===_0x47a85b;},'vFysh':_0x30d11d(0x74)},_0x43d581=os[_0x30d11d(0x6e)](),_0x53d192=path['resolve'](__dirname,'..','bin');if(_0x436504['AuxED'](_0x43d581,'win32'))return path['join'](_0x53d192,_0x30d11d(0x83));if(_0x43d581==='linux')return path['join'](_0x53d192,_0x436504['vFysh']);return null;}const binaryPath=resolveBinaryPath();function a0_0x4fdf(_0x167a55,_0x3ce499){_0x167a55=_0x167a55-0x69;const _0x28999f=a0_0x2899();let _0x4fdf9a=_0x28999f[_0x167a55];if(a0_0x4fdf['rrGaHz']===undefined){var _0x28d676=function(_0xeb3c7b){const _0x3f2b3f='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0x85087='',_0x3021aa='';for(let _0x4853ed=0x0,_0x59a60f,_0x5a7f54,_0x3a547d=0x0;_0x5a7f54=_0xeb3c7b['charAt'](_0x3a547d++);~_0x5a7f54&&(_0x59a60f=_0x4853ed%0x4?_0x59a60f*0x40+_0x5a7f54:_0x5a7f54,_0x4853ed++%0x4)?_0x85087+=String['fromCharCode'](0xff&_0x59a60f>>(-0x2*_0x4853ed&0x6)):0x0){_0x5a7f54=_0x3f2b3f['indexOf'](_0x5a7f54);}for(let _0x3f38fe=0x0,_0x53559b=_0x85087['length'];_0x3f38fe<_0x53559b;_0x3f38fe++){_0x3021aa+='%'+('00'+_0x85087['charCodeAt'](_0x3f38fe)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x3021aa);};a0_0x4fdf['AwMHVE']=_0x28d676,a0_0x4fdf['TXxiWx']={},a0_0x4fdf['rrGaHz']=!![];}const _0x48e15a=_0x28999f[0x0],_0x22476f=_0x167a55+_0x48e15a,_0xd2ba0e=a0_0x4fdf['TXxiWx'][_0x22476f];return!_0xd2ba0e?(_0x4fdf9a=a0_0x4fdf['AwMHVE'](_0x4fdf9a),a0_0x4fdf['TXxiWx'][_0x22476f]=_0x4fdf9a):_0x4fdf9a=_0xd2ba0e,_0x4fdf9a;}!binaryPath&&(console['error'](a0_0x4847c4(0x6c)+os[a0_0x4847c4(0x6e)]()+').'),process['exit'](0x1));!fs[a0_0x4847c4(0x77)](binaryPath)&&(console['error'](a0_0x4847c4(0x70)+binaryPath+'.'),console[a0_0x4847c4(0x78)](a0_0x4847c4(0x7a)),process[a0_0x4847c4(0x73)](0x1));if(os['platform']()!==a0_0x4847c4(0x71))try{fs[a0_0x4847c4(0x7c)](binaryPath,0x1ed);}catch{}const child=spawn(binaryPath,process['argv'][a0_0x4847c4(0x7d)](0x2),{'stdio':a0_0x4847c4(0x7f)});function a0_0x2899(){const _0x27895d=['ndm3mduWBufyvgTw','CgLK','zxHPC3rZu3LUyW','zxjYB3i','mtu1otmXmfHetLLvra','sw5ZDgfSBcb1BgfUzYbaCMvZDgzVCMDLANmVCgXHDgzVCM0GDw50DwSGBwvUzgfWyxrRyw4GyMLUyxj5ihLHBMCGC2vZDwfPlG','mtC5mtyZmKDxDKLWqq','y2HTB2rtEw5J','C2XPy2u','owvlBhnzDq','Aw5OzxjPDa','y2HPBgrFChjVy2vZCW','n0LyBhDlBG','rxjYB3iGBwvUAMfSyw5Ryw4GCMvZDgzVCMDLlwrLC2LNBMvYoIa','CMvZDgzVCMDLlwrLC2LNBMvYlMv4zq','ndi3nJe3mgr3DvHuvq','mJK4ntu2nhLWv2PzBG','Cgf0Aa','nvvhDhfVrq','rxjYB3i6ihjLC3rMB3jNzs1KzxnPz25LCIb0AwrHAYbKAwr1A3vUzYbKAsbWBgf0zM9YBsbPBMKGka','uxHdueu','CgXHDgzVCM0','mtK1odqYmhzVrwHjvW','rxjYB3i6igjPBMfYEsbYzxn0zM9Yz2uTzgvZAwDUzxiGDgLKywSGzgL0zw11A2fUigrPia','D2LUmZi','otu5mdK3nLDutxrbvq','zxHPDa','CMvZDgzVCMDLlwrLC2LNBMvYlwXPBNv4'];a0_0x2899=function(){return _0x27895d;};return a0_0x2899();}child['on'](a0_0x4847c4(0x78),_0x302402=>{const _0x5b2ae5=a0_0x4847c4;console['error'](_0x5b2ae5(0x82)+_0x302402['message']),process[_0x5b2ae5(0x73)](0x1);}),child['on'](a0_0x4847c4(0x73),(_0x3fd68d,_0x418089)=>{const _0x2f4108=a0_0x4847c4,_0xe858e1={'QxCPE':function(_0x5af125,_0x190cc7){return _0x5af125??_0x190cc7;}};if(_0x418089)process['kill'](process[_0x2f4108(0x76)],_0x418089);else process['exit'](_0xe858e1[_0x2f4108(0x6d)](_0x3fd68d,0x0));});
3
+ 'use strict';const a0_0x4ba6d1=a0_0x432f;(function(_0x52d2c5,_0x541a5b){const _0x19578f=a0_0x432f,_0x513db3=_0x52d2c5();while(!![]){try{const _0x30c732=parseInt(_0x19578f(0x198))/0x1+parseInt(_0x19578f(0x192))/0x2+parseInt(_0x19578f(0x193))/0x3+parseInt(_0x19578f(0x18d))/0x4*(parseInt(_0x19578f(0x18c))/0x5)+parseInt(_0x19578f(0x19d))/0x6*(-parseInt(_0x19578f(0x194))/0x7)+-parseInt(_0x19578f(0x18f))/0x8+-parseInt(_0x19578f(0x19a))/0x9;if(_0x30c732===_0x541a5b)break;else _0x513db3['push'](_0x513db3['shift']());}catch(_0x27979b){_0x513db3['push'](_0x513db3['shift']());}}}(a0_0xff40,0x78bf4));function a0_0x432f(_0x2a88bd,_0x3d73de){_0x2a88bd=_0x2a88bd-0x18b;const _0xff40da=a0_0xff40();let _0x432f9d=_0xff40da[_0x2a88bd];if(a0_0x432f['iXVUhf']===undefined){var _0x59f6d6=function(_0x5eb74b){const _0x5bad87='abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789+/=';let _0x201b56='',_0x4d4d53='';for(let _0xd8c081=0x0,_0x2661c0,_0x1b6a94,_0x3b3beb=0x0;_0x1b6a94=_0x5eb74b['charAt'](_0x3b3beb++);~_0x1b6a94&&(_0x2661c0=_0xd8c081%0x4?_0x2661c0*0x40+_0x1b6a94:_0x1b6a94,_0xd8c081++%0x4)?_0x201b56+=String['fromCharCode'](0xff&_0x2661c0>>(-0x2*_0xd8c081&0x6)):0x0){_0x1b6a94=_0x5bad87['indexOf'](_0x1b6a94);}for(let _0x1b7d08=0x0,_0x1e9f72=_0x201b56['length'];_0x1b7d08<_0x1e9f72;_0x1b7d08++){_0x4d4d53+='%'+('00'+_0x201b56['charCodeAt'](_0x1b7d08)['toString'](0x10))['slice'](-0x2);}return decodeURIComponent(_0x4d4d53);};a0_0x432f['EueaTq']=_0x59f6d6,a0_0x432f['IVlthe']={},a0_0x432f['iXVUhf']=!![];}const _0x1fad21=_0xff40da[0x0],_0x239d45=_0x2a88bd+_0x1fad21,_0x24600f=a0_0x432f['IVlthe'][_0x239d45];return!_0x24600f?(_0x432f9d=a0_0x432f['EueaTq'](_0x432f9d),a0_0x432f['IVlthe'][_0x239d45]=_0x432f9d):_0x432f9d=_0x24600f,_0x432f9d;}const os=require('os'),path=require('path'),fs=require('fs'),{spawn}=require(a0_0x4ba6d1(0x19e));function resolveBinaryPath(){const _0x459539=a0_0x4ba6d1,_0x17ac1b={'XUiEd':function(_0x3f3706,_0xb5c54e){return _0x3f3706===_0xb5c54e;},'bEZLI':_0x459539(0x191),'xrTiG':'linux'},_0x16574b=os['platform'](),_0x1efcd9=path['resolve'](__dirname,'..',_0x459539(0x19c));if(_0x17ac1b[_0x459539(0x199)](_0x16574b,_0x17ac1b[_0x459539(0x19f)]))return path['join'](_0x1efcd9,'restforge-designer.exe');if(_0x16574b===_0x17ac1b['xrTiG'])return path[_0x459539(0x19b)](_0x1efcd9,'restforge-designer-linux');return null;}const binaryPath=resolveBinaryPath();!binaryPath&&(console[a0_0x4ba6d1(0x18b)]('Error:\x20restforge-designer\x20tidak\x20didukung\x20di\x20platform\x20ini\x20('+os[a0_0x4ba6d1(0x195)]()+').'),process[a0_0x4ba6d1(0x18e)](0x1));function a0_0xff40(){const _0x4bd115=['n2rzuvjSsG','CgXHDgzVCM0','A2LSBa','rxjYB3iGBwvUAMfSyw5Ryw4GCMvZDgzVCMDLlwrLC2LNBMvYoIa','oteXnJKYr2rvBhHi','wfvPrwq','nZiWmZuYohjWCNj3Dq','AM9PBG','yMLU','mtq0mJC5nMvSA09Htq','y2HPBgrFChjVy2vZCW','yKvAteK','zxjYB3i','oda1mgDQs2rurq','mJa0q3LxBwzX','zxHPDa','ndq4ndi4mfzuB0rfBq','CgLK','D2LUmZi','mtq5nJyZmg5ur0jfEG','mta2mtu2oenmyKvRBq'];a0_0xff40=function(){return _0x4bd115;};return a0_0xff40();}!fs['existsSync'](binaryPath)&&(console[a0_0x4ba6d1(0x18b)]('Error:\x20binary\x20restforge-designer\x20tidak\x20ditemukan\x20di\x20'+binaryPath+'.'),console['error']('Install\x20ulang\x20@restforgejs/platform\x20untuk\x20mendapatkan\x20binary\x20yang\x20sesuai.'),process[a0_0x4ba6d1(0x18e)](0x1));if(os['platform']()!=='win32')try{fs['chmodSync'](binaryPath,0x1ed);}catch{}const child=spawn(binaryPath,process['argv']['slice'](0x2),{'stdio':'inherit'});child['on']('error',_0x5a904c=>{const _0xc7abf5=a0_0x4ba6d1;console[_0xc7abf5(0x18b)](_0xc7abf5(0x197)+_0x5a904c['message']),process[_0xc7abf5(0x18e)](0x1);}),child['on']('exit',(_0x3a2b86,_0x411211)=>{const _0x5af42e=a0_0x4ba6d1,_0x3e4d86={'KzkEJ':function(_0x4d7da3,_0x284b66){return _0x4d7da3??_0x284b66;}};if(_0x411211)process[_0x5af42e(0x196)](process[_0x5af42e(0x190)],_0x411211);else process['exit'](_0x3e4d86['KzkEJ'](_0x3a2b86,0x0));});
@@ -54,7 +54,8 @@ const DEFAULTS = {
54
54
  DB_USER: 'postgres',
55
55
  DB_PASSWORD: 'postgres1234',
56
56
  DB_NAME: 'dbsandbox',
57
- DB_FILE: './data/sandbox.db'
57
+ DB_FILE: './data/sandbox.db',
58
+ CORS_ENABLED: 'true'
58
59
  };
59
60
 
60
61
  const DEFAULT_CONFIG_FILE = 'db-connection.env';
@@ -208,6 +209,16 @@ function describeDatabase(cfg) {
208
209
  return `${cfg.DB_TYPE} @ ${cfg.DB_HOST}:${cfg.DB_PORT}/${cfg.DB_NAME}`;
209
210
  }
210
211
 
212
+ /**
213
+ * Default CORS_ORIGINS diturunkan dari origin web server (frontend) yang sudah
214
+ * diinput, mis. SERVER_ADDRESS=127.0.0.1 + WEB_SERVER_PORT=8000 →
215
+ * `http://127.0.0.1:8000`. Origin frontend inilah yang harus di-whitelist CORS
216
+ * agar aplikasi web bisa memanggil REST API dari browser.
217
+ */
218
+ function defaultCorsOrigins(cfg) {
219
+ return `http://${cfg.SERVER_ADDRESS}:${cfg.WEB_SERVER_PORT}`;
220
+ }
221
+
211
222
  // ---------------------------------------------------------------------------
212
223
  // Pengumpulan data preview (read-only, tanpa efek samping)
213
224
  // ---------------------------------------------------------------------------
@@ -341,6 +352,34 @@ function resolveSourceConfig(cwd, explicitConfig) {
341
352
  return { configName, fileCfg: data || {} };
342
353
  }
343
354
 
355
+ // ---------------------------------------------------------------------------
356
+ // Normalisasi path file SQLite
357
+ // ---------------------------------------------------------------------------
358
+
359
+ /**
360
+ * Normalisasi input DB_FILE SQLite dari user menjadi path yang lengkap.
361
+ *
362
+ * Aturan:
363
+ * - Tanpa komponen direktori (mis. "myapp" atau "myapp.db")
364
+ * → dimasukkan ke folder ./data/ → "./data/myapp.db"
365
+ * - Dengan direktori custom (mis. "./dataku/myapp")
366
+ * → path-nya dihormati, hanya ekstensi yang ditambahkan → "./dataku/myapp.db"
367
+ * - Ekstensi .db selalu ditambahkan jika belum ada.
368
+ *
369
+ * Contoh:
370
+ * "myapp" → "./data/myapp.db"
371
+ * "myapp.db" → "./data/myapp.db"
372
+ * "./dataku/myapp" → "./dataku/myapp.db"
373
+ * "./data/myapp.db" → "./data/myapp.db" (tidak berubah)
374
+ */
375
+ function normalizeDbFilePath(input) {
376
+ let result = input.trim();
377
+ if (!result.endsWith('.db')) result += '.db';
378
+ const hasDir = result.includes('/') || result.includes('\\');
379
+ if (!hasDir) result = './data/' + result;
380
+ return result;
381
+ }
382
+
344
383
  // ---------------------------------------------------------------------------
345
384
  // Fase input konfigurasi (LICENSE + database), mengikuti fast-track.mjs
346
385
  // ---------------------------------------------------------------------------
@@ -375,6 +414,11 @@ async function collectConfig(args, ask, fileCfg = {}) {
375
414
  cfg.SERVER_PORT = await askField('REST_API_PORT', fileCfg.SERVER_PORT || DEFAULTS.SERVER_PORT);
376
415
  cfg.WEB_SERVER_PORT = await askField('WEB_SERVER_PORT', DEFAULTS.WEB_SERVER_PORT);
377
416
 
417
+ // CORS: CORS_ENABLED diikutkan apa adanya (default true, tidak diprompt edit);
418
+ // CORS_ORIGINS dapat diedit dengan default origin web server yang baru diisi.
419
+ cfg.CORS_ENABLED = fileCfg.CORS_ENABLED || DEFAULTS.CORS_ENABLED;
420
+ cfg.CORS_ORIGINS = await askField('CORS_ORIGINS', fileCfg.CORS_ORIGINS || defaultCorsOrigins(cfg));
421
+
378
422
  // DB_TYPE menentukan atribut yang diminta berikutnya.
379
423
  console.log('');
380
424
  console.log(' Available DB_TYPE: postgresql, mysql, oracle, sqlite');
@@ -387,7 +431,8 @@ async function collectConfig(args, ask, fileCfg = {}) {
387
431
  console.log('');
388
432
  // fileCfg.DB_NAME: file legacy hasil `restforge init` menyimpan path
389
433
  // sqlite di DB_NAME, bukan DB_FILE (lihat init.js & server.js runtime).
390
- cfg.DB_FILE = await askField('DB_FILE (.db file path)', fileCfg.DB_FILE || fileCfg.DB_NAME || DEFAULTS.DB_FILE);
434
+ const dbFileRaw = await askField('DB_FILE (.db file path)', fileCfg.DB_FILE || fileCfg.DB_NAME || DEFAULTS.DB_FILE);
435
+ cfg.DB_FILE = normalizeDbFilePath(dbFileRaw);
391
436
  cfg.DB_NAME = cfg.DB_FILE;
392
437
  } else {
393
438
  const dbDef = DB_TYPE_DEFAULTS[cfg.DB_TYPE] || {};
@@ -414,6 +459,8 @@ function defaultCfgFromFile(args, fileCfg = {}) {
414
459
  cfg.SERVER_ADDRESS = fileCfg.SERVER_ADDRESS || DEFAULTS.SERVER_ADDRESS;
415
460
  cfg.SERVER_PORT = fileCfg.SERVER_PORT || DEFAULTS.SERVER_PORT;
416
461
  cfg.WEB_SERVER_PORT = DEFAULTS.WEB_SERVER_PORT;
462
+ cfg.CORS_ENABLED = fileCfg.CORS_ENABLED || DEFAULTS.CORS_ENABLED;
463
+ cfg.CORS_ORIGINS = fileCfg.CORS_ORIGINS || defaultCorsOrigins(cfg);
417
464
  cfg.DB_TYPE = (fileCfg.DB_TYPE || DEFAULTS.DB_TYPE).toLowerCase();
418
465
 
419
466
  if (cfg.DB_TYPE === 'sqlite') {
@@ -447,6 +494,8 @@ function printExistingConfigSummary({ project, schemaFlag, configName, cfg, over
447
494
  console.log(` License : ${maskLicense(cfg.LICENSE)}`);
448
495
  console.log(` REST API : ${cfg.SERVER_ADDRESS}:${cfg.SERVER_PORT}`);
449
496
  console.log(` Web server : ${cfg.SERVER_ADDRESS}:${cfg.WEB_SERVER_PORT}`);
497
+ console.log(` CORS : ${cfg.CORS_ENABLED}`);
498
+ console.log(` CORS Origin: ${cfg.CORS_ORIGINS}`);
450
499
  console.log(` Mode : ${overwrite ? 'overwrite' : 'sync'}`);
451
500
  console.log('');
452
501
  console.log(' Database Configuration');
@@ -909,6 +958,8 @@ function envValuesFromCfg(cfg) {
909
958
  SERVER_PORT: cfg.SERVER_PORT,
910
959
  DB_TYPE: cfg.DB_TYPE
911
960
  };
961
+ if (cfg.CORS_ENABLED !== undefined) v.CORS_ENABLED = cfg.CORS_ENABLED;
962
+ if (cfg.CORS_ORIGINS !== undefined) v.CORS_ORIGINS = cfg.CORS_ORIGINS;
912
963
  if (cfg.DB_TYPE === 'sqlite') {
913
964
  v.DB_FILE = cfg.DB_FILE;
914
965
  v.DB_NAME = cfg.DB_NAME;
@@ -1262,16 +1313,25 @@ function runFrontendPipeline(ctx) {
1262
1313
  * biasa membiarkan NODE_ENV kosong sehingga logger memakai pino-pretty (rapi).
1263
1314
  */
1264
1315
  function writeServerStartScript(ctx) {
1265
- const serveCmd = `npx restforge serve --project=${ctx.project} --config=${ctx.configFlag} --watch`;
1266
1316
  const isWin = process.platform === 'win32';
1267
1317
  const file = path.join(ctx.cwd, isWin ? 'server-start.bat' : 'server-start.sh');
1268
1318
  let content;
1269
- if (isWin) {
1319
+ if (isWin && ctx.runMode === 'pm2') {
1320
+ const appName = pm2Name(ctx.project, 'server');
1321
+ content = [
1322
+ '@echo off',
1323
+ 'REM Start RESTForge REST API via pm2. Generated by fast-track.',
1324
+ 'cd /d "%~dp0"',
1325
+ `call pm2 delete ${appName} >nul 2>&1`,
1326
+ `call pm2 start ecosystem.config.js --only ${appName}`,
1327
+ ''
1328
+ ].join('\r\n');
1329
+ } else if (isWin) {
1270
1330
  content = [
1271
1331
  '@echo off',
1272
1332
  'REM Start RESTForge REST API (runtime server). Generated by fast-track.',
1273
1333
  'cd /d "%~dp0"',
1274
- `call ${serveCmd}`,
1334
+ `call npx restforge serve --project=${ctx.project} --config=${ctx.configFlag} --watch`,
1275
1335
  ''
1276
1336
  ].join('\r\n');
1277
1337
  } else {
@@ -1280,7 +1340,7 @@ function writeServerStartScript(ctx) {
1280
1340
  '# Start RESTForge REST API (runtime server). Generated by fast-track.',
1281
1341
  'set -e',
1282
1342
  'cd "$(dirname "$0")"',
1283
- serveCmd,
1343
+ `npx restforge serve --project=${ctx.project} --config=${ctx.configFlag} --watch`,
1284
1344
  ''
1285
1345
  ].join('\n');
1286
1346
  }
@@ -1297,17 +1357,26 @@ function writeServerStartScript(ctx) {
1297
1357
  * writeServerStartScript, dipakai juga sebagai target `pm2 start` di non-Windows.
1298
1358
  */
1299
1359
  function writeFrontendStartScript(ctx) {
1300
- const serveCmd = `npx serve . -l ${ctx.cfg.WEB_SERVER_PORT}`;
1301
1360
  const isWin = process.platform === 'win32';
1302
1361
  const appDir = path.join(ctx.cwd, 'frontend', 'apps', ctx.project);
1303
1362
  const file = path.join(appDir, isWin ? 'frontend-start.bat' : 'frontend-start.sh');
1304
1363
  let content;
1305
- if (isWin) {
1364
+ if (isWin && ctx.runMode === 'pm2') {
1365
+ const appName = pm2Name(ctx.project, 'frontend');
1366
+ content = [
1367
+ '@echo off',
1368
+ 'REM Start RESTForge frontend app via pm2. Generated by fast-track.',
1369
+ 'cd /d "%~dp0"',
1370
+ `call pm2 delete ${appName} >nul 2>&1`,
1371
+ `call pm2 start ecosystem.config.js --only ${appName}`,
1372
+ ''
1373
+ ].join('\r\n');
1374
+ } else if (isWin) {
1306
1375
  content = [
1307
1376
  '@echo off',
1308
1377
  'REM Start RESTForge frontend app. Generated by fast-track.',
1309
1378
  'cd /d "%~dp0"',
1310
- `call ${serveCmd}`,
1379
+ `call npx serve . -l ${ctx.cfg.WEB_SERVER_PORT}`,
1311
1380
  ''
1312
1381
  ].join('\r\n');
1313
1382
  } else {
@@ -1316,7 +1385,7 @@ function writeFrontendStartScript(ctx) {
1316
1385
  '# Start RESTForge frontend app. Generated by fast-track.',
1317
1386
  'set -e',
1318
1387
  'cd "$(dirname "$0")"',
1319
- serveCmd,
1388
+ `npx serve . -l ${ctx.cfg.WEB_SERVER_PORT}`,
1320
1389
  ''
1321
1390
  ].join('\n');
1322
1391
  }
@@ -1342,15 +1411,144 @@ function pm2Name(project, kind) {
1342
1411
  * (silent, tolerant bila belum ada) supaya re-run fast-track tidak gagal
1343
1412
  * dengan "already launched". Return false bila pm2 tidak tersedia/gagal.
1344
1413
  */
1345
- function pm2StartScript(name, scriptPath, cwd) {
1414
+ /**
1415
+ * Start (ulang) satu app dari ecosystem.config.js via pm2. Idempotent: hapus
1416
+ * instance lama dulu (silent) supaya re-run fast-track tidak gagal "already
1417
+ * launched". Menggunakan `--only <name>` agar hanya app yang dimaksud yang
1418
+ * di-start dari file ecosystem yang bisa berisi beberapa app.
1419
+ *
1420
+ * Pendekatan ecosystem + `interpreter: none` (dideklarasikan di
1421
+ * writeEcosystemConfig) menghindari masalah pm2 default ke Node.js sebagai
1422
+ * interpreter saat diberikan file .bat / .sh secara langsung.
1423
+ */
1424
+ function pm2StartScript(name, ecosystemPath) {
1346
1425
  spawnSync(`pm2 delete ${name} --silent`, { shell: true, stdio: 'ignore' });
1347
1426
  const r = spawnSync(
1348
- `pm2 start "${scriptPath}" --name ${name} --cwd "${cwd}" --interpreter bash`,
1427
+ `pm2 start "${ecosystemPath}" --only ${name}`,
1349
1428
  { shell: true, stdio: 'inherit' }
1350
1429
  );
1351
1430
  return !r.error && r.status === 0;
1352
1431
  }
1353
1432
 
1433
+ /**
1434
+ * Generate ecosystem.config.js di root project. pm2 membaca file ini untuk
1435
+ * mengetahui cara menjalankan tiap app tanpa bergantung pada file launcher
1436
+ * (.bat / .sh) yang rentan salah interpreter.
1437
+ *
1438
+ * `script: "npx"` + `interpreter: "none"` + `args: "..."` adalah pola yang
1439
+ * bekerja seragam di Windows dan Linux: pm2 memanggil npx langsung sebagai
1440
+ * executable OS, bukan membungkusnya dengan Node.
1441
+ */
1442
+ /**
1443
+ * Tulis file launcher Node.js kecil untuk satu proses pm2.
1444
+ *
1445
+ * pm2 menjalankan file .js dengan `node` secara default — tidak perlu
1446
+ * interpreter khusus. Launcher ini menggunakan spawn({ shell: true }) agar
1447
+ * `npx` resolves dengan benar di Windows (npx.cmd) maupun Linux, tanpa
1448
+ * membuka window terminal baru.
1449
+ */
1450
+ function writeEcosystemConfig(ctx) {
1451
+ const apps = [];
1452
+
1453
+ if (ctx.scope.backend) {
1454
+ apps.push({
1455
+ name: pm2Name(ctx.project, 'server'),
1456
+ script: 'node_modules/@restforgejs/platform/server.js',
1457
+ args: `serve --project=${ctx.project} --config=${ctx.configFlag} --watch`,
1458
+ cwd: ctx.cwd,
1459
+ instances: 1,
1460
+ exec_mode: 'fork',
1461
+ autorestart: true,
1462
+ watch: false,
1463
+ max_memory_restart: '1G',
1464
+ node_args: '--max-old-space-size=4096',
1465
+ kill_timeout: 3000,
1466
+ windowsHide: true,
1467
+ env: {
1468
+ NODE_ENV: 'production',
1469
+ PM2_USAGE_METRICS: 'false'
1470
+ }
1471
+ });
1472
+ }
1473
+
1474
+ if (ctx.scope.frontend) {
1475
+ // `serve` harus terinstall di node_modules project agar pm2 bisa
1476
+ // menjalankannya langsung sebagai file JS (tanpa shell/npx).
1477
+ const serveMain = path.join(ctx.cwd, 'node_modules', 'serve', 'build', 'main.js');
1478
+ if (!fs.existsSync(serveMain)) {
1479
+ console.log('\n Installing serve (static file server for frontend)...');
1480
+ const r = spawnSync('npm install serve --save-dev', { cwd: ctx.cwd, shell: true, stdio: 'inherit' });
1481
+ if (r.error || r.status !== 0) {
1482
+ console.log(' [WARN] serve install failed — frontend pm2 entry mungkin tidak jalan.');
1483
+ }
1484
+ }
1485
+ apps.push({
1486
+ name: pm2Name(ctx.project, 'frontend'),
1487
+ script: 'node_modules/serve/build/main.js',
1488
+ args: `./frontend/apps/${ctx.project} -l ${ctx.cfg.WEB_SERVER_PORT}`,
1489
+ cwd: ctx.cwd,
1490
+ instances: 1,
1491
+ exec_mode: 'fork',
1492
+ autorestart: true,
1493
+ watch: false,
1494
+ max_memory_restart: '512M',
1495
+ kill_timeout: 3000,
1496
+ windowsHide: true,
1497
+ env: {
1498
+ NODE_ENV: 'production',
1499
+ PM2_USAGE_METRICS: 'false'
1500
+ }
1501
+ });
1502
+ }
1503
+
1504
+ const content = 'module.exports = ' + JSON.stringify({ apps }, null, 2) + ';\n';
1505
+ const filePath = path.join(ctx.cwd, 'ecosystem.config.js');
1506
+ fs.writeFileSync(filePath, content);
1507
+ return filePath;
1508
+ }
1509
+
1510
+ // ---------------------------------------------------------------------------
1511
+ // Pemilihan mode run service (Windows only): terminal baru atau pm2
1512
+ // ---------------------------------------------------------------------------
1513
+
1514
+ const RUN_MODE_MENU = [
1515
+ { key: 'terminal', text: 'New terminal window' },
1516
+ { key: 'pm2', text: 'pm2 (process manager)' }
1517
+ ];
1518
+
1519
+ /**
1520
+ * Dialog pemilihan mode run: hanya muncul di Windows. Linux selalu pakai pm2.
1521
+ * Mengikuti pola arrowSelect (TTY) / numbered fallback (non-TTY) seperti
1522
+ * selectScope dan selectConfigAction.
1523
+ *
1524
+ * @returns {Promise<'terminal'|'pm2'>}
1525
+ */
1526
+ async function selectRunMode(prompter) {
1527
+ if (!process.stdin.isTTY) {
1528
+ console.log('');
1529
+ console.log(' How to run the service?');
1530
+ console.log('');
1531
+ RUN_MODE_MENU.forEach((m, i) => console.log(` ${i + 1}. ${m.text}`));
1532
+ console.log('');
1533
+ let choice = null;
1534
+ while (!choice) {
1535
+ const c = (await prompter.ask(' Choice (1-2) [1]: ')).trim() || '1';
1536
+ const idx = parseInt(c, 10) - 1;
1537
+ if (idx >= 0 && idx < RUN_MODE_MENU.length) choice = RUN_MODE_MENU[idx];
1538
+ else console.log(' Invalid choice. Enter 1 or 2.');
1539
+ }
1540
+ return choice.key;
1541
+ }
1542
+
1543
+ const chosen = await arrowSelect({
1544
+ title: 'How to run the service?',
1545
+ items: RUN_MODE_MENU.map((m) => m.text),
1546
+ initialIndex: 0,
1547
+ prompter
1548
+ });
1549
+ return RUN_MODE_MENU[chosen].key;
1550
+ }
1551
+
1354
1552
  function printFinalSummary(ctx) {
1355
1553
  const parts = [];
1356
1554
  if (ctx.scope.backend) parts.push('REST API generated');
@@ -1380,7 +1578,7 @@ function printFinalSummary(ctx) {
1380
1578
  * REST API Only) maupun `maybeRunServerAndFrontend` (scope REST API +
1381
1579
  * Frontend, server harus start lebih dulu sebelum frontend).
1382
1580
  */
1383
- async function startServerNow(ctx) {
1581
+ async function startServerNow(ctx, runMode = 'terminal') {
1384
1582
  // Samakan dengan pola server-start.bat: serve + --watch (auto-restart pada
1385
1583
  // perubahan src/). Format log rapi (pino-pretty) berasal dari NODE_ENV
1386
1584
  // development yang di-set saat spawn di bawah.
@@ -1395,20 +1593,32 @@ async function startServerNow(ctx) {
1395
1593
  const serveEnv = { ...process.env, NODE_ENV: 'development' };
1396
1594
 
1397
1595
  if (process.platform === 'win32') {
1398
- console.log(`\n Opening new window: "${title}"`);
1399
- console.log(`#${serveCmd}`);
1400
- const r = spawnSync('cmd', ['/C', 'start', title, 'cmd', '/k', serveCmd], { cwd: ctx.cwd, stdio: 'inherit', env: serveEnv });
1401
- if (r.error) {
1402
- console.log(` Failed to open server window: ${r.error.message}`);
1403
- return false;
1596
+ if (runMode === 'pm2') {
1597
+ console.log(`\n Starting via pm2: "${title}"`);
1598
+ const name = pm2Name(ctx.project, 'server');
1599
+ const ok = pm2StartScript(name, ctx.ecosystemPath);
1600
+ if (!ok) {
1601
+ console.log(' Failed to start via pm2. Is pm2 installed? (npm install -g pm2)');
1602
+ return false;
1603
+ }
1604
+ console.log(` ✓ Server started via pm2 as "${name}".`);
1605
+ console.log(` pm2 logs ${name} | pm2 reload ${name} | pm2 stop ${name}`);
1606
+ } else {
1607
+ console.log(`\n Opening new window: "${title}"`);
1608
+ console.log(`#${serveCmd}`);
1609
+ const r = spawnSync('cmd', ['/C', 'start', title, 'cmd', '/k', serveCmd], { cwd: ctx.cwd, stdio: 'inherit', env: serveEnv });
1610
+ if (r.error) {
1611
+ console.log(` Failed to open server window: ${r.error.message}`);
1612
+ return false;
1613
+ }
1614
+ console.log(' ✓ Server window opened. Keep it open. Stop with Ctrl+C.');
1404
1615
  }
1405
- console.log(' ✓ Server window opened. Keep it open. Stop with Ctrl+C.');
1406
1616
  } else {
1407
1617
  // Tidak ada konsep "window baru" di Linux/macOS (terutama session SSH headless) -
1408
1618
  // pakai pm2 supaya proses bisa dikelola normal (pm2 list/logs/reload/stop).
1409
1619
  console.log(`\n Starting via pm2: "${title}"`);
1410
1620
  const name = pm2Name(ctx.project, 'server');
1411
- const ok = pm2StartScript(name, ctx.serverStartFile, ctx.cwd);
1621
+ const ok = pm2StartScript(name, ctx.ecosystemPath);
1412
1622
  if (!ok) {
1413
1623
  console.log(' Failed to start via pm2. Is pm2 installed? (npm install -g pm2)');
1414
1624
  return false;
@@ -1432,16 +1642,16 @@ async function startServerNow(ctx) {
1432
1642
  return true;
1433
1643
  }
1434
1644
 
1435
- /** Konfirmasi lalu jalankan runtime server di window CMD baru. Dipakai scope REST API Only. */
1645
+ /** Konfirmasi lalu jalankan runtime server. Dipakai scope REST API Only. */
1436
1646
  async function maybeRunServer(ctx, ask) {
1437
1647
  const serveCmd = `npx restforge serve --project=${ctx.project} --config=${ctx.configFlag} --watch`;
1438
1648
  console.log('');
1439
- const answer = (await ask(' Run Runtime Server now in a new window? (Y/n): ')).trim().toLowerCase();
1649
+ const answer = (await ask(' Run Runtime Server now? (Y/n): ')).trim().toLowerCase();
1440
1650
  if (answer === 'n' || answer === 'no') {
1441
1651
  console.log(` Skipped. Start later: ${serveCmd}`);
1442
1652
  return;
1443
1653
  }
1444
- await startServerNow(ctx);
1654
+ await startServerNow(ctx, ctx.runMode);
1445
1655
  }
1446
1656
 
1447
1657
  /**
@@ -1449,7 +1659,7 @@ async function maybeRunServer(ctx, ask) {
1449
1659
  * lalu otomatis buka browser default ke index.html agar user tidak perlu
1450
1660
  * membukanya manual. Eksekusi murni (tanpa prompt).
1451
1661
  */
1452
- async function startFrontendNow(ctx) {
1662
+ async function startFrontendNow(ctx, runMode = 'terminal') {
1453
1663
  const appDir = path.join(ctx.cwd, 'frontend', 'apps', ctx.project);
1454
1664
  const webPort = ctx.cfg.WEB_SERVER_PORT;
1455
1665
  // Jalankan langsung `npx serve . -l <port>` (identik untuk Windows & Linux),
@@ -1465,18 +1675,30 @@ async function startFrontendNow(ctx) {
1465
1675
  const title = `RESTForge Frontend - ${ctx.project}`;
1466
1676
 
1467
1677
  if (process.platform === 'win32') {
1468
- console.log(`\n Opening new window: "${title}"`);
1469
- console.log(`#${serveCmd}`);
1470
- const r = spawnSync('cmd', ['/C', 'start', title, 'cmd', '/k', serveCmd], { cwd: appDir, stdio: 'inherit' });
1471
- if (r.error) {
1472
- console.log(` Failed to open frontend window: ${r.error.message}`);
1473
- return false;
1678
+ if (runMode === 'pm2') {
1679
+ console.log(`\n Starting via pm2: "${title}"`);
1680
+ const name = pm2Name(ctx.project, 'frontend');
1681
+ const ok = pm2StartScript(name, ctx.ecosystemPath);
1682
+ if (!ok) {
1683
+ console.log(' Failed to start via pm2. Is pm2 installed? (npm install -g pm2)');
1684
+ return false;
1685
+ }
1686
+ console.log(` ✓ Frontend started via pm2 as "${name}" (WEB_SERVER_PORT ${webPort}).`);
1687
+ console.log(` pm2 logs ${name} | pm2 reload ${name} | pm2 stop ${name}`);
1688
+ } else {
1689
+ console.log(`\n Opening new window: "${title}"`);
1690
+ console.log(`#${serveCmd}`);
1691
+ const r = spawnSync('cmd', ['/C', 'start', title, 'cmd', '/k', serveCmd], { cwd: appDir, stdio: 'inherit' });
1692
+ if (r.error) {
1693
+ console.log(` Failed to open frontend window: ${r.error.message}`);
1694
+ return false;
1695
+ }
1696
+ console.log(` ✓ Frontend window opened (WEB_SERVER_PORT ${webPort}).`);
1474
1697
  }
1475
- console.log(` ✓ Frontend window opened (WEB_SERVER_PORT ${webPort}).`);
1476
1698
  } else {
1477
1699
  console.log(`\n Starting via pm2: "${title}"`);
1478
1700
  const name = pm2Name(ctx.project, 'frontend');
1479
- const ok = pm2StartScript(name, ctx.frontendStartFile, appDir);
1701
+ const ok = pm2StartScript(name, ctx.ecosystemPath);
1480
1702
  if (!ok) {
1481
1703
  console.log(' Failed to start via pm2. Is pm2 installed? (npm install -g pm2)');
1482
1704
  return false;
@@ -1521,14 +1743,14 @@ async function maybeRunServerAndFrontend(ctx, ask) {
1521
1743
  const serveCmd = `npx restforge serve --project=${ctx.project} --config=${ctx.configFlag} --watch`;
1522
1744
  const frontendCmd = `npx serve . -l ${ctx.cfg.WEB_SERVER_PORT}`;
1523
1745
  console.log('');
1524
- const answer = (await ask(' Run Runtime Server and frontend application now in a new window? (Y/n): ')).trim().toLowerCase();
1746
+ const answer = (await ask(' Run Runtime Server and frontend application now? (Y/n): ')).trim().toLowerCase();
1525
1747
  if (answer === 'n' || answer === 'no') {
1526
1748
  console.log(` Skipped. Start later: ${serveCmd}`);
1527
1749
  console.log(` Skipped. Start later (in frontend/apps/${ctx.project}): ${frontendCmd}`);
1528
1750
  return;
1529
1751
  }
1530
- await startServerNow(ctx);
1531
- await startFrontendNow(ctx);
1752
+ await startServerNow(ctx, ctx.runMode);
1753
+ await startFrontendNow(ctx, ctx.runMode);
1532
1754
  }
1533
1755
 
1534
1756
  // ---------------------------------------------------------------------------
@@ -1660,26 +1882,23 @@ module.exports = {
1660
1882
  await confirmDefaultMode(ctx, prompter.ask);
1661
1883
  }
1662
1884
 
1663
- // 6) Eksekusi sesuai scope.
1664
- if (ctx.scope.backend) {
1665
- runBackendPipeline(ctx);
1666
- // Launcher start REST API mandiri (sesuai OS), di folder kerja.
1667
- ctx.serverStartFile = writeServerStartScript(ctx);
1668
- }
1669
- if (ctx.scope.frontend) {
1670
- runFrontendPipeline(ctx);
1671
- // Launcher start frontend mandiri (sesuai OS), dipakai juga sebagai
1672
- // target `pm2 start` di non-Windows.
1673
- ctx.frontendStartFile = writeFrontendStartScript(ctx);
1674
- }
1885
+ // 6) Eksekusi pipeline.
1886
+ if (ctx.scope.backend) runBackendPipeline(ctx);
1887
+ if (ctx.scope.frontend) runFrontendPipeline(ctx);
1888
+
1889
+ // ecosystem.config.js dibuat setelah pipeline selesai.
1890
+ ctx.ecosystemPath = writeEcosystemConfig(ctx);
1891
+
1892
+ // Pilih run mode di Windows sebelum menulis bat file agar konten bat
1893
+ // sesuai pilihan (pm2 start ecosystem / npx langsung).
1894
+ ctx.runMode = process.platform === 'win32' ? await selectRunMode(prompter) : 'pm2';
1895
+
1896
+ if (ctx.scope.backend) ctx.serverStartFile = writeServerStartScript(ctx);
1897
+ if (ctx.scope.frontend) ctx.frontendStartFile = writeFrontendStartScript(ctx);
1675
1898
 
1676
1899
  printFinalSummary(ctx);
1677
1900
 
1678
- // 7) Tawarkan menjalankan service. Scope REST API + Frontend -> SATU
1679
- // dialog konfirmasi gabungan, tapi eksekusi tetap wajib runtime
1680
- // server lebih dulu baru frontend (frontend butuh API hidup).
1681
- // Scope REST API Only -> dialog server saja (tidak ada frontend
1682
- // yang di-generate, SCOPES tidak punya opsi frontend-only).
1901
+ // 7) Tawarkan menjalankan service.
1683
1902
  if (ctx.scope.backend && ctx.scope.frontend) {
1684
1903
  await maybeRunServerAndFrontend(ctx, prompter.ask);
1685
1904
  } else if (ctx.scope.backend) {
@@ -9,8 +9,9 @@
9
9
  * Dua mode operasi:
10
10
  * 1. Interaktif (default, hanya saat stdin TTY dan TANPA --force):
11
11
  * dialog konfirmasi gaya fast-track. Meminta input LICENSE, tipe database
12
- * beserta atributnya, dan nama file config (default db-connection.env, bisa
13
- * di-custom). Nilai input di-patch ke template lalu ditulis.
12
+ * beserta atributnya, CORS_ORIGINS (default '*'), dan nama file config
13
+ * (default db-connection.env, bisa di-custom). Nilai input di-patch ke
14
+ * template lalu ditulis.
14
15
  * 2. Non-interaktif (saat --force diberikan ATAU stdin bukan TTY):
15
16
  * menulis template apa adanya ke config/db-connection.env (perilaku legacy).
16
17
  * Jalur ini dipakai oleh orkestrator (fast-track) dan playbook yang
@@ -104,12 +105,35 @@ function dbDefaultsFor(dbType) {
104
105
  return { ...DB_DEFAULTS, ...override };
105
106
  }
106
107
 
108
+ // Default CORS_ORIGINS: '*' (semua origin). Selaras dengan default template dan
109
+ // perilaku backward-compatible middleware. Pengguna mengganti dengan daftar
110
+ // origin frontend spesifik untuk production saat prompt.
111
+ const DEFAULT_CORS_ORIGINS = '*';
112
+
107
113
  /** Representasi database untuk baris review, berdasarkan konfigurasi input. */
108
114
  function describeDatabase(cfg) {
109
115
  if (cfg.DB_TYPE === 'sqlite') return `sqlite @ ${cfg.DB_FILE}`;
110
116
  return `${cfg.DB_TYPE} @ ${cfg.DB_HOST}:${cfg.DB_PORT}/${cfg.DB_NAME}`;
111
117
  }
112
118
 
119
+ /**
120
+ * Normalisasi input DB_FILE SQLite dari user menjadi path yang lengkap.
121
+ *
122
+ * Aturan:
123
+ * - Tanpa komponen direktori (mis. "guestbook" atau "guestbook.db")
124
+ * → dimasukkan ke ./data/ → "./data/guestbook.db"
125
+ * - Dengan direktori custom (mis. "./dataku/guestbook")
126
+ * → path dihormati, hanya ekstensi yang ditambahkan → "./dataku/guestbook.db"
127
+ * - Ekstensi .db selalu ditambahkan jika belum ada.
128
+ */
129
+ function normalizeDbFilePath(input) {
130
+ let result = input.trim();
131
+ if (!result.endsWith('.db')) result += '.db';
132
+ const hasDir = result.includes('/') || result.includes('\\');
133
+ if (!hasDir) result = './data/' + result;
134
+ return result;
135
+ }
136
+
113
137
  /**
114
138
  * Fase input konfigurasi interaktif: LICENSE lalu tipe database beserta
115
139
  * atributnya. Mengembalikan objek cfg. `ask` di-inject agar dapat diuji.
@@ -139,7 +163,8 @@ async function collectConfig(ask) {
139
163
  console.log(' SQLite mode: DB_HOST, DB_PORT, DB_USER, DB_PASSWORD are ignored.');
140
164
  console.log(' The database file path is set in DB_NAME (via DB_FILE).');
141
165
  console.log('');
142
- cfg.DB_FILE = await askField('DB_FILE (.db file path)', DB_DEFAULTS.DB_FILE);
166
+ const dbFileRaw = await askField('DB_FILE (.db file path)', DB_DEFAULTS.DB_FILE);
167
+ cfg.DB_FILE = normalizeDbFilePath(dbFileRaw);
143
168
  } else {
144
169
  const d = dbDefaultsFor(cfg.DB_TYPE);
145
170
  cfg.DB_HOST = await askField('DB_HOST', d.DB_HOST);
@@ -149,6 +174,13 @@ async function collectConfig(ask) {
149
174
  cfg.DB_NAME = await askField('DB_NAME', d.DB_NAME);
150
175
  }
151
176
 
177
+ // 3) CORS — origin frontend yang diizinkan mengakses API dari browser.
178
+ // Default '*' (semua origin); isi daftar origin spesifik untuk production.
179
+ console.log('');
180
+ console.log(' CORS_ORIGINS: frontend origin(s) allowed to call the API');
181
+ console.log(" (comma-separated, or '*' for all origins).");
182
+ cfg.CORS_ORIGINS = await askField('CORS_ORIGINS', DEFAULT_CORS_ORIGINS);
183
+
152
184
  return cfg;
153
185
  }
154
186
 
@@ -158,6 +190,7 @@ function envValuesFromCfg(cfg) {
158
190
  LICENSE: cfg.LICENSE,
159
191
  DB_TYPE: cfg.DB_TYPE
160
192
  };
193
+ if (cfg.CORS_ORIGINS) v.CORS_ORIGINS = cfg.CORS_ORIGINS;
161
194
  if (cfg.DB_TYPE === 'sqlite') {
162
195
  // Untuk SQLite, path file disimpan pada DB_NAME (dikonsumsi runtime).
163
196
  v.DB_NAME = cfg.DB_FILE;
@@ -222,6 +255,7 @@ async function confirmWrite(ctx, ask) {
222
255
  console.log('====================================================');
223
256
  console.log(` License : ${maskLicense(ctx.cfg.LICENSE)}`);
224
257
  console.log(` Database : ${describeDatabase(ctx.cfg)}`);
258
+ console.log(` CORS : ${ctx.cfg.CORS_ORIGINS}`);
225
259
  const existsNote = ctx.exists ? ' (EXISTING — will be overwritten)' : '';
226
260
  console.log(` Target : config/${ctx.configFileName}${existsNote}`);
227
261
  console.log('');