ylyx-cli 1.0.16 → 1.1.1

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 (289) hide show
  1. package/README.md +313 -276
  2. package/bin/ylyx.js +176 -168
  3. package/lib/config.js +325 -283
  4. package/lib/deploy.js +509 -529
  5. package/lib/environments.js +111 -0
  6. package/lib/generator.js +192 -192
  7. package/lib/index.js +113 -113
  8. package/lib/interactive.js +87 -87
  9. package/lib/remote.js +179 -179
  10. package/lib/template.js +174 -174
  11. package/lib/utils.js +148 -148
  12. package/package.json +61 -59
  13. package/templates/react-app/files/.gitignore +22 -0
  14. package/templates/react-app/files/README.md +23 -23
  15. package/templates/react-app/files/package.json +28 -28
  16. package/templates/react-app/files/public/index.html +11 -11
  17. package/templates/react-app/files/src/App.css +15 -15
  18. package/templates/react-app/files/src/App.js +15 -15
  19. package/templates/react-app/files/src/index.css +10 -10
  20. package/templates/react-app/files/src/index.js +11 -11
  21. package/templates/react-app/template.json +11 -11
  22. package/templates/react-component/files/Component.module.css +5 -5
  23. package/templates/react-component/files/Component.tsx +31 -31
  24. package/templates/react-component/files/index.ts +2 -2
  25. package/templates/react-component/template.json +48 -48
  26. package/templates/vue-app/files/.editorconfig +14 -14
  27. package/templates/vue-app/files/.env.development +21 -21
  28. package/templates/vue-app/files/.env.production +18 -18
  29. package/templates/vue-app/files/.eslintignore +5 -5
  30. package/templates/vue-app/files/.eslintrc.js +267 -267
  31. package/templates/vue-app/files/.gitignore +24 -0
  32. package/templates/vue-app/files/.prettierignore +11 -11
  33. package/templates/vue-app/files/.travis.yml +5 -5
  34. package/templates/vue-app/files/.vscode/extensions.json +7 -7
  35. package/templates/vue-app/files/.vscode/settings.json +39 -39
  36. package/templates/vue-app/files/LICENSE +191 -191
  37. package/templates/vue-app/files/README.md +87 -87
  38. package/templates/vue-app/files/babel.config.js +11 -11
  39. package/templates/vue-app/files/jest.config.js +24 -24
  40. package/templates/vue-app/files/package.json +136 -136
  41. package/templates/vue-app/files/plopfile.js +7 -7
  42. package/templates/vue-app/files/postcss.config.js +5 -5
  43. package/templates/vue-app/files/public/default.js +25 -25
  44. package/templates/vue-app/files/public/index.html +26 -26
  45. package/templates/vue-app/files/src/App.vue +54 -54
  46. package/templates/vue-app/files/src/api/bigDataV/index.js +198 -198
  47. package/templates/vue-app/files/src/api/data.js +42 -42
  48. package/templates/vue-app/files/src/api/dataArchive/index.js +186 -186
  49. package/templates/vue-app/files/src/api/generator/genConfig.js +16 -16
  50. package/templates/vue-app/files/src/api/generator/generator.js +32 -32
  51. package/templates/vue-app/files/src/api/indexManagement/index.js +39 -39
  52. package/templates/vue-app/files/src/api/institutionMonitor/index.js +396 -396
  53. package/templates/vue-app/files/src/api/institutionSet/index.js +120 -120
  54. package/templates/vue-app/files/src/api/login.js +44 -44
  55. package/templates/vue-app/files/src/api/selfStatistics/index.js +50 -50
  56. package/templates/vue-app/files/src/api/system/code.js +15 -15
  57. package/templates/vue-app/files/src/api/system/dict.js +34 -34
  58. package/templates/vue-app/files/src/api/system/dictDetail.js +52 -52
  59. package/templates/vue-app/files/src/api/system/equipment.js +35 -35
  60. package/templates/vue-app/files/src/api/system/examManage.js +49 -49
  61. package/templates/vue-app/files/src/api/system/examProject.js +48 -48
  62. package/templates/vue-app/files/src/api/system/hosModel.js +35 -35
  63. package/templates/vue-app/files/src/api/system/hospital.js +88 -88
  64. package/templates/vue-app/files/src/api/system/job.js +40 -40
  65. package/templates/vue-app/files/src/api/system/menu.js +67 -67
  66. package/templates/vue-app/files/src/api/system/model.js +42 -42
  67. package/templates/vue-app/files/src/api/system/risConfig.js +9 -9
  68. package/templates/vue-app/files/src/api/system/role.js +64 -64
  69. package/templates/vue-app/files/src/api/system/sysSet.js +19 -19
  70. package/templates/vue-app/files/src/api/system/timing.js +41 -41
  71. package/templates/vue-app/files/src/api/system/user.js +121 -121
  72. package/templates/vue-app/files/src/api/tools/alipay.js +25 -25
  73. package/templates/vue-app/files/src/api/tools/email.js +24 -24
  74. package/templates/vue-app/files/src/api/tools/localStorage.js +27 -27
  75. package/templates/vue-app/files/src/api/tools/qiniu.js +40 -40
  76. package/templates/vue-app/files/src/assets/css/font-awesome.css +2342 -2342
  77. package/templates/vue-app/files/src/assets/css/font-awesome.min.css +2336 -2336
  78. package/templates/vue-app/files/src/assets/fonts/fontawesome-webfont.svg +2671 -2671
  79. package/templates/vue-app/files/src/assets/icons/index.js +9 -9
  80. package/templates/vue-app/files/src/assets/icons/svg/hospital.svg +1 -1
  81. package/templates/vue-app/files/src/assets/icons/svg/hospital1.svg +1 -1
  82. package/templates/vue-app/files/src/assets/icons/svg/loginOne.svg +35 -35
  83. package/templates/vue-app/files/src/assets/icons/svg/loginOneWhite.svg +19 -19
  84. package/templates/vue-app/files/src/assets/icons/svg/loginOr.svg +93 -93
  85. package/templates/vue-app/files/src/assets/icons/svg/loginUp.svg +96 -96
  86. package/templates/vue-app/files/src/assets/icons/svg/monitor.svg +1 -1
  87. package/templates/vue-app/files/src/assets/icons/svg/offline.svg +1 -1
  88. package/templates/vue-app/files/src/assets/icons/svg/online.svg +1 -1
  89. package/templates/vue-app/files/src/assets/icons/svg/system.svg +1 -1
  90. package/templates/vue-app/files/src/assets/icons/svgo.yml +21 -21
  91. package/templates/vue-app/files/src/assets/styles/btn.scss +103 -103
  92. package/templates/vue-app/files/src/assets/styles/eladmin.scss +124 -124
  93. package/templates/vue-app/files/src/assets/styles/element-ui.scss +217 -217
  94. package/templates/vue-app/files/src/assets/styles/element-variables.scss +31 -31
  95. package/templates/vue-app/files/src/assets/styles/global.scss +73 -73
  96. package/templates/vue-app/files/src/assets/styles/index.scss +220 -220
  97. package/templates/vue-app/files/src/assets/styles/mixin.scss +60 -60
  98. package/templates/vue-app/files/src/assets/styles/publicStyle.scss +224 -224
  99. package/templates/vue-app/files/src/assets/styles/sidebar.scss +239 -239
  100. package/templates/vue-app/files/src/assets/styles/transition.scss +48 -48
  101. package/templates/vue-app/files/src/assets/styles/variables.scss +44 -44
  102. package/templates/vue-app/files/src/common/flexible.js +145 -145
  103. package/templates/vue-app/files/src/components/Breadcrumb/index.vue +87 -87
  104. package/templates/vue-app/files/src/components/Crud/CRUD.operation.vue +248 -248
  105. package/templates/vue-app/files/src/components/Crud/Pagination.vue +20 -20
  106. package/templates/vue-app/files/src/components/Crud/RR.operation.vue +39 -39
  107. package/templates/vue-app/files/src/components/Crud/UD.operation.vue +106 -106
  108. package/templates/vue-app/files/src/components/Crud/crud.js +907 -907
  109. package/templates/vue-app/files/src/components/Crud/user/UR.operation.vue +154 -154
  110. package/templates/vue-app/files/src/components/DateRangePicker/index.vue +42 -42
  111. package/templates/vue-app/files/src/components/DateRangePickerProhibitNext/index.vue +42 -42
  112. package/templates/vue-app/files/src/components/DateRangePickerProhibitTodayNext/index.vue +48 -48
  113. package/templates/vue-app/files/src/components/Dict/Dict.js +31 -31
  114. package/templates/vue-app/files/src/components/Dict/index.js +29 -29
  115. package/templates/vue-app/files/src/components/Doc/index.vue +16 -16
  116. package/templates/vue-app/files/src/components/Echarts/BarChart.vue +115 -115
  117. package/templates/vue-app/files/src/components/Echarts/Category.vue +768 -768
  118. package/templates/vue-app/files/src/components/Echarts/Funnel.vue +120 -120
  119. package/templates/vue-app/files/src/components/Echarts/Gauge.vue +74 -74
  120. package/templates/vue-app/files/src/components/Echarts/Graph.vue +101 -101
  121. package/templates/vue-app/files/src/components/Echarts/HeatMap.vue +301 -301
  122. package/templates/vue-app/files/src/components/Echarts/Line3D.vue +110 -110
  123. package/templates/vue-app/files/src/components/Echarts/PieChart.vue +84 -84
  124. package/templates/vue-app/files/src/components/Echarts/Point.vue +161 -161
  125. package/templates/vue-app/files/src/components/Echarts/RadarChart.vue +123 -123
  126. package/templates/vue-app/files/src/components/Echarts/Rich.vue +160 -160
  127. package/templates/vue-app/files/src/components/Echarts/Sankey.vue +114 -114
  128. package/templates/vue-app/files/src/components/Echarts/Scatter.vue +192 -192
  129. package/templates/vue-app/files/src/components/Echarts/Sunburst.vue +127 -127
  130. package/templates/vue-app/files/src/components/Echarts/ThemeRiver.vue +233 -233
  131. package/templates/vue-app/files/src/components/Echarts/WordCloud.vue +201 -201
  132. package/templates/vue-app/files/src/components/GithubCorner/index.vue +55 -55
  133. package/templates/vue-app/files/src/components/Hamburger/index.vue +44 -44
  134. package/templates/vue-app/files/src/components/HeaderSearch/index.vue +196 -196
  135. package/templates/vue-app/files/src/components/IconSelect/index.vue +74 -74
  136. package/templates/vue-app/files/src/components/IconSelect/requireIcons.js +10 -10
  137. package/templates/vue-app/files/src/components/Iframe/index.vue +30 -30
  138. package/templates/vue-app/files/src/components/JavaEdit/index.vue +78 -78
  139. package/templates/vue-app/files/src/components/Pagination/index.vue +100 -100
  140. package/templates/vue-app/files/src/components/PanThumb/index.vue +149 -149
  141. package/templates/vue-app/files/src/components/ParentView/index.vue +3 -3
  142. package/templates/vue-app/files/src/components/Permission/index.js +13 -13
  143. package/templates/vue-app/files/src/components/Permission/permission.js +21 -21
  144. package/templates/vue-app/files/src/components/RightPanel/index.vue +149 -149
  145. package/templates/vue-app/files/src/components/Screenfull/index.vue +60 -60
  146. package/templates/vue-app/files/src/components/SizeSelect/index.vue +60 -60
  147. package/templates/vue-app/files/src/components/SvgIcon/index.vue +66 -66
  148. package/templates/vue-app/files/src/components/ThemePicker/index.vue +177 -177
  149. package/templates/vue-app/files/src/components/UploadExcel/index.vue +149 -149
  150. package/templates/vue-app/files/src/components/WaterMark/setWaterMark.js +68 -68
  151. package/templates/vue-app/files/src/components/WaterMark/waterMark.vue +69 -69
  152. package/templates/vue-app/files/src/components/Xgplayer/index.vue +49 -49
  153. package/templates/vue-app/files/src/components/YamlEdit/index.vue +81 -81
  154. package/templates/vue-app/files/src/layout/components/AppMain.vue +67 -67
  155. package/templates/vue-app/files/src/layout/components/Navbar.vue +209 -209
  156. package/templates/vue-app/files/src/layout/components/Settings/index.vue +125 -125
  157. package/templates/vue-app/files/src/layout/components/Sidebar/FixiOSBug.js +26 -26
  158. package/templates/vue-app/files/src/layout/components/Sidebar/Item.vue +29 -29
  159. package/templates/vue-app/files/src/layout/components/Sidebar/Link.vue +35 -35
  160. package/templates/vue-app/files/src/layout/components/Sidebar/Logo.vue +93 -93
  161. package/templates/vue-app/files/src/layout/components/Sidebar/SidebarItem.vue +104 -104
  162. package/templates/vue-app/files/src/layout/components/Sidebar/index.vue +79 -79
  163. package/templates/vue-app/files/src/layout/components/TagsView/ScrollPane.vue +90 -90
  164. package/templates/vue-app/files/src/layout/components/TagsView/index.vue +299 -299
  165. package/templates/vue-app/files/src/layout/components/index.js +5 -5
  166. package/templates/vue-app/files/src/layout/index.vue +119 -119
  167. package/templates/vue-app/files/src/layout/mixin/ResizeHandler.js +45 -45
  168. package/templates/vue-app/files/src/main.js +89 -89
  169. package/templates/vue-app/files/src/mixins/crud.js +348 -348
  170. package/templates/vue-app/files/src/router/index.js +156 -156
  171. package/templates/vue-app/files/src/router/routers.js +203 -203
  172. package/templates/vue-app/files/src/settings.js +57 -57
  173. package/templates/vue-app/files/src/store/getters.js +29 -29
  174. package/templates/vue-app/files/src/store/index.js +25 -25
  175. package/templates/vue-app/files/src/store/modules/api.js +33 -33
  176. package/templates/vue-app/files/src/store/modules/app.js +55 -55
  177. package/templates/vue-app/files/src/store/modules/permission.js +79 -79
  178. package/templates/vue-app/files/src/store/modules/register.js +17 -17
  179. package/templates/vue-app/files/src/store/modules/settings.js +46 -46
  180. package/templates/vue-app/files/src/store/modules/tagsView.js +168 -168
  181. package/templates/vue-app/files/src/store/modules/user.js +119 -119
  182. package/templates/vue-app/files/src/utils/auth.js +18 -18
  183. package/templates/vue-app/files/src/utils/base64ToFile.js +27 -27
  184. package/templates/vue-app/files/src/utils/clipboard.js +36 -36
  185. package/templates/vue-app/files/src/utils/datetime.js +289 -289
  186. package/templates/vue-app/files/src/utils/getBlobFile.js +10 -10
  187. package/templates/vue-app/files/src/utils/getTime.js +29 -29
  188. package/templates/vue-app/files/src/utils/index.js +520 -520
  189. package/templates/vue-app/files/src/utils/permission.js +29 -29
  190. package/templates/vue-app/files/src/utils/pinyin.js +450 -450
  191. package/templates/vue-app/files/src/utils/request.js +139 -139
  192. package/templates/vue-app/files/src/utils/rsaEncrypt.js +37 -37
  193. package/templates/vue-app/files/src/utils/shortcuts.js +210 -210
  194. package/templates/vue-app/files/src/utils/upload.js +12 -12
  195. package/templates/vue-app/files/src/utils/validate.js +262 -262
  196. package/templates/vue-app/files/src/views/components/Echarts.vue +124 -124
  197. package/templates/vue-app/files/src/views/components/Editor.vue +75 -75
  198. package/templates/vue-app/files/src/views/components/MarkDown.vue +44 -44
  199. package/templates/vue-app/files/src/views/components/UVideo.vue +37 -37
  200. package/templates/vue-app/files/src/views/components/YamlEdit.vue +206 -206
  201. package/templates/vue-app/files/src/views/components/camera/camera.vue +115 -115
  202. package/templates/vue-app/files/src/views/components/camera/cameraTest.vue +158 -158
  203. package/templates/vue-app/files/src/views/components/excel/upload-excel.vue +41 -41
  204. package/templates/vue-app/files/src/views/components/icons/element-icons.js +74 -74
  205. package/templates/vue-app/files/src/views/components/icons/index.vue +106 -106
  206. package/templates/vue-app/files/src/views/components/icons/svg-icons.js +10 -10
  207. package/templates/vue-app/files/src/views/components/scroll/index.vue +107 -107
  208. package/templates/vue-app/files/src/views/components/scrollFull/index.vue +99 -99
  209. package/templates/vue-app/files/src/views/dashboard/LineChart.vue +138 -138
  210. package/templates/vue-app/files/src/views/dashboard/PanelGroup.vue +173 -173
  211. package/templates/vue-app/files/src/views/dashboard/mixins/resize.js +57 -57
  212. package/templates/vue-app/files/src/views/dataArchive/delTable.vue +354 -354
  213. package/templates/vue-app/files/src/views/dataArchive/exportDetail.vue +121 -121
  214. package/templates/vue-app/files/src/views/dataArchive/exportList.vue +160 -160
  215. package/templates/vue-app/files/src/views/dataArchive/index.vue +2104 -2104
  216. package/templates/vue-app/files/src/views/features/401.vue +83 -83
  217. package/templates/vue-app/files/src/views/features/404.vue +226 -226
  218. package/templates/vue-app/files/src/views/features/redirect.vue +12 -12
  219. package/templates/vue-app/files/src/views/generator/config.vue +348 -348
  220. package/templates/vue-app/files/src/views/generator/index.vue +143 -143
  221. package/templates/vue-app/files/src/views/generator/preview.vue +39 -39
  222. package/templates/vue-app/files/src/views/home.vue +80 -80
  223. package/templates/vue-app/files/src/views/indexManagement/index.vue +422 -422
  224. package/templates/vue-app/files/src/views/indexManagement/other.vue +286 -286
  225. package/templates/vue-app/files/src/views/institutionMonitor/components/LineChart.vue +191 -191
  226. package/templates/vue-app/files/src/views/institutionMonitor/index.scss +339 -339
  227. package/templates/vue-app/files/src/views/institutionMonitor/index.vue +1385 -1385
  228. package/templates/vue-app/files/src/views/institutionMonitor/mixins/resize.js +57 -57
  229. package/templates/vue-app/files/src/views/institutionMonitor/monitorDetail/checkProjectCode.vue +267 -267
  230. package/templates/vue-app/files/src/views/institutionMonitor/monitorDetail/imageAbnormalMonitor.vue +361 -361
  231. package/templates/vue-app/files/src/views/institutionMonitor/monitorDetail/index.vue +686 -686
  232. package/templates/vue-app/files/src/views/institutionMonitor/monitorDetail/leakageAnomaly.vue +270 -270
  233. package/templates/vue-app/files/src/views/institutionMonitor/monitorDetail/moveMonitore.vue +114 -114
  234. package/templates/vue-app/files/src/views/institutionMonitor/monitorDetail/qualityMonitore.vue +338 -338
  235. package/templates/vue-app/files/src/views/institutionSet/cloudSet.vue +534 -534
  236. package/templates/vue-app/files/src/views/institutionSet/index.vue +2550 -2550
  237. package/templates/vue-app/files/src/views/login.vue +243 -243
  238. package/templates/vue-app/files/src/views/qualityLogin.vue +280 -280
  239. package/templates/vue-app/files/src/views/selfStatistics/components/LineChart.vue +296 -296
  240. package/templates/vue-app/files/src/views/selfStatistics/hospitalDetail/index.vue +533 -533
  241. package/templates/vue-app/files/src/views/selfStatistics/index.scss +447 -447
  242. package/templates/vue-app/files/src/views/selfStatistics/index.vue +903 -903
  243. package/templates/vue-app/files/src/views/selfStatistics/mixins/resize.js +57 -57
  244. package/templates/vue-app/files/src/views/selfStatistics/monitorDetail/index.vue +285 -285
  245. package/templates/vue-app/files/src/views/selfStatistics/offlineDetail/index.vue +104 -104
  246. package/templates/vue-app/files/src/views/selfStatistics/selfActiveDetail/index.vue +104 -104
  247. package/templates/vue-app/files/src/views/selfStatistics/selfMonitorDetail/index.vue +420 -420
  248. package/templates/vue-app/files/src/views/selfStatistics/setWaterMark/setWaterMark.js +68 -68
  249. package/templates/vue-app/files/src/views/selfStatistics/setWaterMark/waterMark.vue +69 -69
  250. package/templates/vue-app/files/src/views/system/dept/index.vue +144 -144
  251. package/templates/vue-app/files/src/views/system/dict/dictDetail.vue +146 -146
  252. package/templates/vue-app/files/src/views/system/dict/index.vue +159 -159
  253. package/templates/vue-app/files/src/views/system/examModality/index.vue +174 -174
  254. package/templates/vue-app/files/src/views/system/examPart/index.vue +184 -184
  255. package/templates/vue-app/files/src/views/system/examProject/index.vue +321 -321
  256. package/templates/vue-app/files/src/views/system/examine/index.vue +66 -66
  257. package/templates/vue-app/files/src/views/system/job/index.vue +119 -119
  258. package/templates/vue-app/files/src/views/system/job/module/form.vue +72 -72
  259. package/templates/vue-app/files/src/views/system/job/module/header.vue +48 -48
  260. package/templates/vue-app/files/src/views/system/menu/index.vue +327 -327
  261. package/templates/vue-app/files/src/views/system/model/index.vue +285 -285
  262. package/templates/vue-app/files/src/views/system/role/index.vue +434 -434
  263. package/templates/vue-app/files/src/views/system/sysSet/index.vue +84 -84
  264. package/templates/vue-app/files/src/views/system/timing/index.vue +289 -289
  265. package/templates/vue-app/files/src/views/system/timing/log.vue +143 -143
  266. package/templates/vue-app/files/src/views/system/user/admin.vue +414 -414
  267. package/templates/vue-app/files/src/views/system/user/center/updateEmail.vue +146 -146
  268. package/templates/vue-app/files/src/views/system/user/center/updatePass.vue +168 -168
  269. package/templates/vue-app/files/src/views/system/user/center.vue +140 -140
  270. package/templates/vue-app/files/src/views/system/user/index.vue +972 -972
  271. package/templates/vue-app/files/src/views/systemSet/role/index.vue +314 -314
  272. package/templates/vue-app/files/src/views/systemSet/user/admin.vue +414 -414
  273. package/templates/vue-app/files/src/views/systemSet/user/center/updateEmail.vue +146 -146
  274. package/templates/vue-app/files/src/views/systemSet/user/center/updatePass.vue +115 -115
  275. package/templates/vue-app/files/src/views/systemSet/user/center.vue +351 -351
  276. package/templates/vue-app/files/src/views/systemSet/user/index.vue +928 -928
  277. package/templates/vue-app/files/src/views/tools/aliPay/config.vue +102 -102
  278. package/templates/vue-app/files/src/views/tools/aliPay/index.vue +48 -48
  279. package/templates/vue-app/files/src/views/tools/aliPay/toPay.vue +91 -91
  280. package/templates/vue-app/files/src/views/tools/email/config.vue +91 -91
  281. package/templates/vue-app/files/src/views/tools/email/index.vue +42 -42
  282. package/templates/vue-app/files/src/views/tools/email/send.vue +159 -159
  283. package/templates/vue-app/files/src/views/tools/storage/index.vue +35 -35
  284. package/templates/vue-app/files/src/views/tools/storage/local/index.vue +207 -207
  285. package/templates/vue-app/files/src/views/tools/storage/qiniu/form.vue +108 -108
  286. package/templates/vue-app/files/src/views/tools/storage/qiniu/index.vue +241 -241
  287. package/templates/vue-app/files/src/views/tools/swagger/index.vue +14 -14
  288. package/templates/vue-app/files/vue.config.js +173 -173
  289. package/templates/vue-app/template.json +29 -29
package/lib/deploy.js CHANGED
@@ -1,529 +1,509 @@
1
- const path = require('path');
2
- const fs = require('fs-extra');
3
- const ora = require('ora');
4
- const inquirer = require('inquirer');
5
- const archiver = require('archiver');
6
- const { readProjectConfig } = require('./utils');
7
-
8
- let spinner = null; // 加载实例
9
-
10
- function formatTimestampCompact(date = new Date()) {
11
- const pad2 = (n) => String(n).padStart(2, '0');
12
- const yyyy = date.getFullYear();
13
- const MM = pad2(date.getMonth() + 1);
14
- const dd = pad2(date.getDate());
15
- const HH = pad2(date.getHours());
16
- const mm = pad2(date.getMinutes());
17
- const ss = pad2(date.getSeconds());
18
- return `${yyyy}${MM}${dd}${HH}${mm}${ss}`;
19
- }
20
-
21
- function formatTimestampForLog(date = new Date()) {
22
- const pad2 = (n) => String(n).padStart(2, '0');
23
- const yyyy = date.getFullYear();
24
- const MM = pad2(date.getMonth() + 1);
25
- const dd = pad2(date.getDate());
26
- const HH = pad2(date.getHours());
27
- const mm = pad2(date.getMinutes());
28
- const ss = pad2(date.getSeconds());
29
- return `${yyyy}-${MM}-${dd} ${HH}:${mm}:${ss}`;
30
- }
31
-
32
- function formatBytes(bytes) {
33
- const n = Number(bytes) || 0;
34
- const units = ['B', 'KB', 'MB', 'GB', 'TB'];
35
- let v = n;
36
- let i = 0;
37
- while (v >= 1024 && i < units.length - 1) {
38
- v /= 1024;
39
- i++;
40
- }
41
- const fixed = i === 0 ? 0 : v < 10 ? 2 : v < 100 ? 1 : 0;
42
- return `${v.toFixed(fixed)}${units[i]}`;
43
- }
44
-
45
- function createDeployLogger({ env, buildDirName, outDir }) {
46
- const logDir = path.join(outDir || path.join(process.cwd(), '.ylyx-deploy'), 'logs');
47
- fs.ensureDirSync(logDir);
48
- const fileName = `deploy-${env || 'unknown'}-${buildDirName || 'build'}-${formatTimestampCompact(
49
- new Date()
50
- )}.log`;
51
- const logPath = path.join(logDir, fileName);
52
- const stream = fs.createWriteStream(logPath, { flags: 'a' });
53
-
54
- const log = (msg) => {
55
- const line = `[${formatTimestampForLog(new Date())}] ${msg}\n`;
56
- try {
57
- stream.write(line);
58
- } catch (e) {
59
- // ignore
60
- }
61
- };
62
-
63
- // 只记录“操作/目录/时间”的精简日志
64
- const logOp = (op, target, extra) => {
65
- const parts = [`op=${op}`];
66
- if (target) parts.push(`target=${target}`);
67
- if (extra) parts.push(`extra=${extra}`);
68
- log(parts.join(' '));
69
- };
70
-
71
- const close = () =>
72
- new Promise((resolve) => {
73
- try {
74
- stream.end(resolve);
75
- } catch (e) {
76
- resolve();
77
- }
78
- });
79
-
80
- return { logPath, log, logOp, close };
81
- }
82
-
83
- function listLocalFilesWithSize(rootDir) {
84
- const out = [];
85
- const stack = [rootDir];
86
- while (stack.length) {
87
- const current = stack.pop();
88
- const entries = fs.readdirSync(current);
89
- for (const e of entries) {
90
- const p = path.join(current, e);
91
- const st = fs.statSync(p);
92
- if (st.isDirectory()) stack.push(p);
93
- else if (st.isFile()) {
94
- out.push({
95
- localPath: p,
96
- relPosix: path.relative(rootDir, p).replace(/\\/g, '/'),
97
- size: st.size,
98
- mtimeMs: st.mtimeMs,
99
- });
100
- }
101
- }
102
- }
103
- out.sort((a, b) => a.relPosix.localeCompare(b.relPosix));
104
- return out;
105
- }
106
-
107
- function sanitizeBuildDirName(name) {
108
- // 用户要求:项目名称来源于 buildDir 字段;同时确保文件名安全
109
- return String(name || 'build')
110
- .replace(/[\\/]/g, '')
111
- .replace(/[^a-zA-Z0-9._-]/g, '')
112
- .slice(0, 80);
113
- }
114
-
115
- function getBuildDirBaseName({ buildDir, localDir }) {
116
- // buildDir 可能是路径:只取最后一级目录名
117
- if (buildDir != null && String(buildDir).trim()) {
118
- const normalized = String(buildDir).trim().replace(/\\/g, '/').replace(/\/+$/, '');
119
- const last = normalized.split('/').filter(Boolean).pop();
120
- if (last) return sanitizeBuildDirName(last);
121
- }
122
- // 兜底:使用 localDir 的最后一段目录名
123
- return sanitizeBuildDirName(path.basename(path.resolve(localDir || process.cwd())));
124
- }
125
-
126
- function normalizeAndValidateRemoteDir(remoteDir) {
127
- if (!remoteDir || typeof remoteDir !== 'string') return null;
128
- // 统一成 Linux 风格
129
- const raw = remoteDir.trim().replace(/\\/g, '/');
130
- // 必须是绝对路径
131
- if (!raw.startsWith('/')) {
132
- throw new Error(
133
- `deploy.remoteDir 必须是 Linux 绝对路径,并且只能在 /usr/local/nginx/html/* 下(当前: ${remoteDir})`
134
- );
135
- }
136
-
137
- // 归一化,消除 .. 等
138
- const resolved = path.posix.resolve(raw);
139
- const base = '/usr/local/nginx/html';
140
- const baseWithSlash = base + '/';
141
-
142
- // 只允许 /usr/local/nginx/html/*(必须是子目录,不允许刚好等于 base)
143
- if (!resolved.startsWith(baseWithSlash)) {
144
- throw new Error(
145
- `deploy.remoteDir 只能设置为 ${baseWithSlash}* 下的目录(当前: ${resolved})`
146
- );
147
- }
148
- if (resolved === base) {
149
- throw new Error(`deploy.remoteDir 不能是 ${base} 根目录,请设置为其子目录(例如 ${baseWithSlash}my-app)`);
150
- }
151
- // 再防一次:禁止路径段包含 ..(即便 resolve 已处理,这里保证输入约束更明确)
152
- const rel = resolved.slice(baseWithSlash.length);
153
- if (!rel || rel.split('/').some((seg) => seg === '..')) {
154
- throw new Error(`deploy.remoteDir 非法:不允许包含 ..(当前: ${resolved})`);
155
- }
156
-
157
- return resolved;
158
- }
159
-
160
- async function zipDirAsTopFolder({ srcDir, zipPath, topFolderName }) {
161
- fs.ensureDirSync(path.dirname(zipPath));
162
- await new Promise((resolve, reject) => {
163
- const output = fs.createWriteStream(zipPath);
164
- const archive = archiver('zip', { zlib: { level: 9 } });
165
-
166
- output.on('close', resolve);
167
- output.on('error', reject);
168
- archive.on('warning', (err) => {
169
- if (err && err.code === 'ENOENT') return;
170
- reject(err);
171
- });
172
- archive.on('error', reject);
173
-
174
- archive.pipe(output);
175
- // 关键:zip 内顶层目录名强制为 buildDirName
176
- archive.directory(srcDir, topFolderName);
177
- archive.finalize();
178
- });
179
- }
180
-
181
- function hashLettersFromSha256(buf, lettersLen = 12) {
182
- const alphabet = 'abcdefghijklmnop'; // 16 letters for 0..15
183
- let out = '';
184
- for (let i = 0; i < buf.length && out.length < lettersLen; i++) {
185
- const b = buf[i];
186
- out += alphabet[(b >> 4) & 0xf];
187
- if (out.length >= lettersLen) break;
188
- out += alphabet[b & 0xf];
189
- }
190
- return out.slice(0, lettersLen);
191
- }
192
-
193
- function computeDirHashLetters(localDir) {
194
- const crypto = require('crypto');
195
- const h = crypto.createHash('sha256');
196
- const files = listLocalFilesWithSize(localDir);
197
- for (const f of files) {
198
- h.update(f.relPosix);
199
- h.update('\n');
200
- h.update(String(f.size || 0));
201
- h.update('\n');
202
- h.update(String(Math.floor(f.mtimeMs || 0)));
203
- h.update('\n');
204
- }
205
- const digest = h.digest();
206
- return hashLettersFromSha256(digest, 12);
207
- }
208
-
209
- async function chooseDeployEnv(env) {
210
- if (env === 'test' || env === 'prod') return env;
211
- const ans = await inquirer.prompt([
212
- {
213
- type: 'list',
214
- name: 'env',
215
- message: '请选择部署环境',
216
- choices: [
217
- { name: '部署测试服(上传到服务器)', value: 'test' },
218
- { name: '部署正式服(不上传,仅压缩)', value: 'prod' },
219
- ],
220
- default: 'test',
221
- },
222
- ]);
223
- return ans.env;
224
- }
225
-
226
- async function uploadDirWithProgress({ sftp, localDir, remoteDirPosix, onProgress }) {
227
- const files = listLocalFilesWithSize(localDir);
228
- const totalBytes = files.reduce((sum, f) => sum + (f.size || 0), 0);
229
- if (files.length === 0) return { totalBytes: 0, uploadedBytes: 0, fileCount: 0 };
230
-
231
- // 先创建需要的目录(去重)
232
- const dirSet = new Set();
233
- for (const f of files) {
234
- const remoteFile = path.posix.join(remoteDirPosix, f.relPosix);
235
- dirSet.add(path.posix.dirname(remoteFile));
236
- }
237
- // 目录从浅到深创建,避免多余失败
238
- const dirs = Array.from(dirSet).sort((a, b) => a.length - b.length);
239
- for (const d of dirs) {
240
- await sftp.mkdir(d, true);
241
- }
242
-
243
- let uploadedBytes = 0;
244
- let lastRenderAt = 0;
245
- const render = (state) => {
246
- if (typeof onProgress !== 'function') return;
247
- const now = Date.now();
248
- // 轻微节流,避免刷屏
249
- if (now - lastRenderAt < 80 && state && state.force !== true) return;
250
- lastRenderAt = now;
251
- onProgress(state);
252
- };
253
-
254
- for (let i = 0; i < files.length; i++) {
255
- const f = files[i];
256
- const remoteFile = path.posix.join(remoteDirPosix, f.relPosix);
257
- let lastTransferred = 0;
258
-
259
- await sftp.fastPut(f.localPath, remoteFile, {
260
- step: (transferred) => {
261
- // transferred:当前文件已传输的总字节数(累计)
262
- const delta = Math.max(0, Number(transferred || 0) - lastTransferred);
263
- lastTransferred = Number(transferred || 0);
264
- uploadedBytes += delta;
265
- render({
266
- uploadedBytes,
267
- totalBytes,
268
- fileIndex: i + 1,
269
- fileCount: files.length,
270
- currentFile: f.relPosix,
271
- });
272
- },
273
- });
274
-
275
- // fastPut 有时不会触发 step(极小文件/实现差异),这里兜底补齐本文件剩余字节
276
- if (lastTransferred < f.size) {
277
- uploadedBytes += f.size - lastTransferred;
278
- }
279
- render({
280
- uploadedBytes,
281
- totalBytes,
282
- fileIndex: i + 1,
283
- fileCount: files.length,
284
- currentFile: f.relPosix,
285
- force: true,
286
- });
287
- }
288
-
289
- return { totalBytes, uploadedBytes, fileCount: files.length };
290
- }
291
-
292
- function resolveDeployConfig(overrides = {}) {
293
- const projectConfig = readProjectConfig();
294
- const deploy = (projectConfig && projectConfig.deploy) || {};
295
-
296
- function parseDotEnvFile(envPath) {
297
- if (!fs.existsSync(envPath)) return {};
298
- const content = fs.readFileSync(envPath, 'utf-8');
299
- const lines = content.split(/\r?\n/);
300
- const out = {};
301
- for (const raw of lines) {
302
- const line = raw.trim();
303
- if (!line || line.startsWith('#')) continue;
304
- const idx = line.indexOf('=');
305
- if (idx < 0) continue;
306
- const key = line.slice(0, idx).trim();
307
- let value = line.slice(idx + 1).trim();
308
- if (
309
- (value.startsWith('"') && value.endsWith('"')) ||
310
- (value.startsWith("'") && value.endsWith("'"))
311
- ) {
312
- value = value.slice(1, -1);
313
- }
314
- out[key] = value;
315
- }
316
- return out;
317
- }
318
-
319
- function normalizePublicUrlToDir(publicUrl) {
320
- // config.js 规则保持一致:忽略完整 URL;去掉首尾 /;把 \ 转 /
321
- if (!publicUrl || typeof publicUrl !== 'string') return '';
322
- let v = publicUrl.trim();
323
- v = v.replace(/\\/g, '/');
324
- if (/^https?:\/\//i.test(v)) return '';
325
- v = v.replace(/^\/+/, '').replace(/\/+$/, '');
326
- return v;
327
- }
328
-
329
- // localDir 默认优先从项目 .env.production 推导(VUE_APP_PUBLIC_URL -> 目录名)
330
- const envProdPath = path.join(process.cwd(), '.env.production');
331
- const envProd = parseDotEnvFile(envProdPath);
332
- const publicUrlDir = normalizePublicUrlToDir(envProd.VUE_APP_PUBLIC_URL);
333
- const inferredFromEnvProd = publicUrlDir ? path.resolve(process.cwd(), publicUrlDir) : null;
334
-
335
- // localDir 默认使用项目 buildDir(打包输出目录)
336
- // 规则:优先 deploy.localDir;否则使用 .env.production 的 VUE_APP_PUBLIC_URL;否则使用 .ylyxrc.json 的 buildDir;最后才回退 ./EXTERNAL_DIGIC
337
- const inferredLocalDir =
338
- deploy.localDir ||
339
- inferredFromEnvProd ||
340
- (projectConfig && projectConfig.buildDir ? path.resolve(process.cwd(), projectConfig.buildDir) : null);
341
-
342
- if (!inferredLocalDir) {
343
- throw new Error(
344
- '无法确定 deploy.localDir(本地打包目录)。请设置以下任意一种:\n' +
345
- '- 在 .ylyxrc.json 配置 deploy.localDir\n' +
346
- '- 在项目 .env.production 配置 VUE_APP_PUBLIC_URL(相对路径,如 /dist/app/)\n' +
347
- '- 在 .ylyxrc.json 配置 buildDir(可相对/绝对路径)'
348
- );
349
- }
350
-
351
- const cfg = {
352
- env: overrides.env ?? deploy.env,
353
- buildDir: overrides.buildDir ?? deploy.buildDir ?? projectConfig.buildDir,
354
- host: overrides.host ?? deploy.host,
355
- port: String(overrides.port ?? deploy.port ?? '22'),
356
- username: overrides.username ?? deploy.username,
357
- password: overrides.password ?? deploy.password,
358
- localDir: overrides.localDir ?? inferredLocalDir,
359
- remoteDir: normalizeAndValidateRemoteDir(overrides.remoteDir ?? deploy.remoteDir),
360
- zipAfter: overrides.zipAfter ?? deploy.zipAfter ?? false,
361
- zipOutDir: overrides.zipOutDir ?? deploy.zipOutDir ?? path.join(process.cwd(), '.ylyx-deploy'),
362
- };
363
-
364
- return cfg;
365
- }
366
-
367
- /**
368
- * 执行部署(递归上传目录到远端)
369
- * 测试服:上传到服务器(可选部署后压缩)
370
- * 正式服:不上传,仅压缩
371
- * @param {{env?:'test'|'prod',host?:string,port?:string|number,username?:string,password?:string,localDir?:string,remoteDir?:string,zipAfter?:boolean,zipOutDir?:string}} overrides
372
- */
373
- async function deploy(overrides = {}) {
374
- const cfg = resolveDeployConfig(overrides);
375
- const localFolderPath = cfg.localDir;
376
- const remoteFolderPath = cfg.remoteDir;
377
-
378
- const env = await chooseDeployEnv(cfg.env);
379
- const buildDirNameForLog = getBuildDirBaseName({ buildDir: cfg.buildDir, localDir: localFolderPath });
380
- const logger = createDeployLogger({ env, buildDirName: buildDirNameForLog, outDir: cfg.zipOutDir });
381
- logger.logOp('DEPLOY_START', '', `env=${env}`);
382
- logger.logOp('LOCAL_DIR', localFolderPath);
383
- if (remoteFolderPath) logger.logOp('REMOTE_DIR', remoteFolderPath);
384
-
385
- if (!fs.existsSync(localFolderPath)) {
386
- logger.logOp('ERROR', localFolderPath, 'localDir_not_exists');
387
- await logger.close();
388
- throw new Error(`本地目录不存在:${path.resolve(localFolderPath)}`);
389
- }
390
-
391
- // 正式服:仅压缩
392
- if (env === 'prod') {
393
- const buildDirName = buildDirNameForLog;
394
- const ts = formatTimestampCompact(new Date());
395
- const hashLetters = computeDirHashLetters(localFolderPath); // 纯字母
396
- // 优化命名:更可读、更易分割(buildDir-时间-哈希)
397
- const zipBaseName = `${buildDirName}-${ts}-${hashLetters}`;
398
- fs.ensureDirSync(cfg.zipOutDir);
399
- const zipName = `${zipBaseName}.zip`;
400
- const zipPath = path.join(cfg.zipOutDir, zipName);
401
- logger.logOp('ZIP_START', zipPath);
402
-
403
- const zipSpinner = ora(`开始压缩:${zipName}`).start();
404
- try {
405
- await zipDirAsTopFolder({ srcDir: localFolderPath, zipPath, topFolderName: buildDirName });
406
- zipSpinner.succeed(`压缩完成:${path.relative(process.cwd(), zipPath)}`);
407
- logger.logOp('ZIP_DONE', zipPath);
408
- } catch (e) {
409
- zipSpinner.fail('压缩失败');
410
- logger.logOp('ERROR', zipPath, `zip_failed:${e && e.message ? e.message : String(e)}`);
411
- await logger.close();
412
- throw e;
413
- }
414
- logger.logOp('DEPLOY_END', '', 'prod');
415
- await logger.close();
416
- return;
417
- }
418
-
419
- // 测试服:上传到服务器
420
- if (!cfg.host) throw new Error('缺少 deploy.host(测试服上传需要)');
421
- if (!cfg.username) throw new Error('缺少 deploy.username(测试服上传需要)');
422
- if (!cfg.password) throw new Error('缺少 deploy.password(测试服上传需要)');
423
- if (!remoteFolderPath) {
424
- logger.logOp('ERROR', '', 'missing_remoteDir');
425
- await logger.close();
426
- throw new Error('缺少 deploy.remoteDir(测试服上传需要):请在 .ylyxrc.json 的 deploy.remoteDir 配置远端部署目录');
427
- }
428
-
429
- let Client;
430
- try {
431
- Client = require('ssh2-sftp-client');
432
- } catch (e) {
433
- throw new Error(
434
- "缺少依赖:ssh2-sftp-client。\n" +
435
- "如果你在 ylyx-cli 仓库本地跑:请先在 ylyx-cli 目录执行 `npm i`。\n" +
436
- "如果你用 npx:请确认你运行的是已发布且包含依赖的版本,或清理 npx 缓存后重试。"
437
- );
438
- }
439
-
440
- const sftp = new Client();
441
- try {
442
- spinner = ora('连接服务器...').start();
443
- logger.logOp('SFTP_CONNECT', cfg.host, `port=${cfg.port} username=${cfg.username}`);
444
- await sftp.connect({
445
- host: cfg.host,
446
- port: cfg.port,
447
- username: cfg.username,
448
- password: cfg.password,
449
- });
450
-
451
- spinner.text = '服务器连接成功,准备清理远端目录...';
452
- logger.logOp('REMOTE_DELETE_START', remoteFolderPath);
453
- try {
454
- await sftp.rmdir(remoteFolderPath, true);
455
- logger.logOp('REMOTE_DELETE_DONE', remoteFolderPath);
456
- } catch (e) {
457
- // ignore
458
- logger.logOp('REMOTE_DELETE_SKIP', remoteFolderPath, e && e.message ? e.message : 'ignored');
459
- }
460
- await sftp.mkdir(remoteFolderPath, true);
461
- logger.logOp('REMOTE_MKDIR', remoteFolderPath);
462
-
463
- const remoteDirPosix = remoteFolderPath.replace(/\\/g, '/');
464
- spinner.text = '开始上传目录...';
465
- logger.logOp('UPLOAD_START', remoteDirPosix, `from=${localFolderPath}`);
466
- const { uploadedBytes, totalBytes, fileCount } = await uploadDirWithProgress({
467
- sftp,
468
- localDir: localFolderPath,
469
- remoteDirPosix,
470
- onProgress: ({ uploadedBytes: up, totalBytes: tot, fileIndex, fileCount: cnt, currentFile }) => {
471
- const percent = tot ? ((up / tot) * 100).toFixed(2) : '100.00';
472
- const shortFile = currentFile ? currentFile.slice(-60) : '';
473
- spinner.text = `上传进度: ${percent}% (${formatBytes(up)}/${formatBytes(tot)}) 文件:${fileIndex}/${cnt} ${shortFile}`;
474
- },
475
- });
476
- spinner.succeed(`上传成功:${formatBytes(uploadedBytes)}/${formatBytes(totalBytes)},共 ${fileCount} 个文件`);
477
- logger.logOp('UPLOAD_DONE', remoteDirPosix, `files=${fileCount} bytes=${uploadedBytes}/${totalBytes}`);
478
- } catch (err) {
479
- if (spinner) spinner.fail('部署失败');
480
- logger.logOp('ERROR', '', err && err.message ? err.message : String(err));
481
- await logger.close();
482
- throw err;
483
- } finally {
484
- try {
485
- await sftp.end();
486
- } catch (e) {}
487
- if (spinner) spinner.info('部署结束');
488
- }
489
-
490
- // 测试服可选:部署后压缩(延续之前的可选开关)
491
- if (cfg.zipAfter) {
492
- const buildDirName = buildDirNameForLog;
493
- const ts = formatTimestampCompact(new Date());
494
- const hashLetters = computeDirHashLetters(localFolderPath); // 纯字母
495
- const zipBaseName = `${buildDirName}-${ts}-${hashLetters}`;
496
- fs.ensureDirSync(cfg.zipOutDir);
497
- const zipName = `${zipBaseName}.zip`;
498
- const zipPath = path.join(cfg.zipOutDir, zipName);
499
- logger.logOp('ZIP_START', zipPath);
500
-
501
- const zipSpinner = ora(`开始压缩:${zipName}`).start();
502
- try {
503
- await zipDirAsTopFolder({ srcDir: localFolderPath, zipPath, topFolderName: buildDirName });
504
- zipSpinner.succeed(`压缩完成:${path.relative(process.cwd(), zipPath)}`);
505
- logger.logOp('ZIP_DONE', zipPath);
506
- } catch (e) {
507
- zipSpinner.fail('压缩失败');
508
- logger.logOp('ERROR', zipPath, `zip_failed:${e && e.message ? e.message : String(e)}`);
509
- await logger.close();
510
- throw e;
511
- }
512
- }
513
-
514
- logger.logOp('DEPLOY_END', '', 'test');
515
- await logger.close();
516
- }
517
-
518
- module.exports = {
519
- deploy,
520
- resolveDeployConfig,
521
- };
522
-
523
- // 允许直接 node lib/deploy.js 运行(从 .ylyxrc.json 读取 deploy 配置)
524
- if (require.main === module) {
525
- deploy().catch((e) => {
526
- console.error(e);
527
- process.exit(1);
528
- });
529
- }
1
+ const path = require('path');
2
+ const fs = require('fs-extra');
3
+ const ora = require('ora');
4
+ const inquirer = require('inquirer');
5
+ const archiver = require('archiver');
6
+ const { readProjectConfig } = require('./utils');
7
+ const { assertConfigMigrated, findEnvironment, getConfigPath } = require('./environments');
8
+
9
+ let spinner = null; // 加载实例
10
+
11
+ function formatTimestampCompact(date = new Date()) {
12
+ const pad2 = (n) => String(n).padStart(2, '0');
13
+ const yyyy = date.getFullYear();
14
+ const MM = pad2(date.getMonth() + 1);
15
+ const dd = pad2(date.getDate());
16
+ const HH = pad2(date.getHours());
17
+ const mm = pad2(date.getMinutes());
18
+ const ss = pad2(date.getSeconds());
19
+ return `${yyyy}${MM}${dd}${HH}${mm}${ss}`;
20
+ }
21
+
22
+ function formatTimestampForLog(date = new Date()) {
23
+ const pad2 = (n) => String(n).padStart(2, '0');
24
+ const yyyy = date.getFullYear();
25
+ const MM = pad2(date.getMonth() + 1);
26
+ const dd = pad2(date.getDate());
27
+ const HH = pad2(date.getHours());
28
+ const mm = pad2(date.getMinutes());
29
+ const ss = pad2(date.getSeconds());
30
+ return `${yyyy}-${MM}-${dd} ${HH}:${mm}:${ss}`;
31
+ }
32
+
33
+ function formatBytes(bytes) {
34
+ const n = Number(bytes) || 0;
35
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
36
+ let v = n;
37
+ let i = 0;
38
+ while (v >= 1024 && i < units.length - 1) {
39
+ v /= 1024;
40
+ i++;
41
+ }
42
+ const fixed = i === 0 ? 0 : v < 10 ? 2 : v < 100 ? 1 : 0;
43
+ return `${v.toFixed(fixed)}${units[i]}`;
44
+ }
45
+
46
+ function createDeployLogger({ env, buildDirName, outDir }) {
47
+ const logDir = path.join(outDir || path.join(process.cwd(), '.ylyx-deploy'), 'logs');
48
+ fs.ensureDirSync(logDir);
49
+ const fileName = `deploy-${env || 'unknown'}-${buildDirName || 'build'}-${formatTimestampCompact(
50
+ new Date()
51
+ )}.log`;
52
+ const logPath = path.join(logDir, fileName);
53
+ const stream = fs.createWriteStream(logPath, { flags: 'a' });
54
+
55
+ const log = (msg) => {
56
+ const line = `[${formatTimestampForLog(new Date())}] ${msg}\n`;
57
+ try {
58
+ stream.write(line);
59
+ } catch (e) {
60
+ // ignore
61
+ }
62
+ };
63
+
64
+ // 只记录“操作/目录/时间”的精简日志
65
+ const logOp = (op, target, extra) => {
66
+ const parts = [`op=${op}`];
67
+ if (target) parts.push(`target=${target}`);
68
+ if (extra) parts.push(`extra=${extra}`);
69
+ log(parts.join(' '));
70
+ };
71
+
72
+ const close = () =>
73
+ new Promise((resolve) => {
74
+ try {
75
+ stream.end(resolve);
76
+ } catch (e) {
77
+ resolve();
78
+ }
79
+ });
80
+
81
+ return { logPath, log, logOp, close };
82
+ }
83
+
84
+ function listLocalFilesWithSize(rootDir) {
85
+ const out = [];
86
+ const stack = [rootDir];
87
+ while (stack.length) {
88
+ const current = stack.pop();
89
+ const entries = fs.readdirSync(current);
90
+ for (const e of entries) {
91
+ const p = path.join(current, e);
92
+ const st = fs.statSync(p);
93
+ if (st.isDirectory()) stack.push(p);
94
+ else if (st.isFile()) {
95
+ out.push({
96
+ localPath: p,
97
+ relPosix: path.relative(rootDir, p).replace(/\\/g, '/'),
98
+ size: st.size,
99
+ mtimeMs: st.mtimeMs,
100
+ });
101
+ }
102
+ }
103
+ }
104
+ out.sort((a, b) => a.relPosix.localeCompare(b.relPosix));
105
+ return out;
106
+ }
107
+
108
+ function sanitizeBuildDirName(name) {
109
+ // 用户要求:项目名称来源于 buildDir 字段;同时确保文件名安全
110
+ return String(name || 'build')
111
+ .replace(/[\\/]/g, '')
112
+ .replace(/[^a-zA-Z0-9._-]/g, '')
113
+ .slice(0, 80);
114
+ }
115
+
116
+ function getBuildDirBaseName({ buildDir, localDir }) {
117
+ // buildDir 可能是路径:只取最后一级目录名
118
+ if (buildDir != null && String(buildDir).trim()) {
119
+ const normalized = String(buildDir).trim().replace(/\\/g, '/').replace(/\/+$/, '');
120
+ const last = normalized.split('/').filter(Boolean).pop();
121
+ if (last) return sanitizeBuildDirName(last);
122
+ }
123
+ // 兜底:使用 localDir 的最后一段目录名
124
+ return sanitizeBuildDirName(path.basename(path.resolve(localDir || process.cwd())));
125
+ }
126
+
127
+ function normalizeAndValidateRemoteDir(remoteDir) {
128
+ if (!remoteDir || typeof remoteDir !== 'string') return null;
129
+ // 统一成 Linux 风格
130
+ const raw = remoteDir.trim().replace(/\\/g, '/');
131
+ // 必须是绝对路径
132
+ if (!raw.startsWith('/')) {
133
+ throw new Error(
134
+ `deploy.remoteDir 必须是 Linux 绝对路径,并且只能在 /usr/local/nginx/html/* 下(当前: ${remoteDir})`
135
+ );
136
+ }
137
+
138
+ // 归一化,消除 ..
139
+ const resolved = path.posix.resolve(raw);
140
+ const base = '/usr/local/nginx/html';
141
+ const baseWithSlash = base + '/';
142
+
143
+ // 只允许 /usr/local/nginx/html/*(必须是子目录,不允许刚好等于 base)
144
+ if (!resolved.startsWith(baseWithSlash)) {
145
+ throw new Error(
146
+ `deploy.remoteDir 只能设置为 ${baseWithSlash}* 下的目录(当前: ${resolved})`
147
+ );
148
+ }
149
+ if (resolved === base) {
150
+ throw new Error(`deploy.remoteDir 不能是 ${base} 根目录,请设置为其子目录(例如 ${baseWithSlash}my-app)`);
151
+ }
152
+ // 再防一次:禁止路径段包含 ..(即便 resolve 已处理,这里保证输入约束更明确)
153
+ const rel = resolved.slice(baseWithSlash.length);
154
+ if (!rel || rel.split('/').some((seg) => seg === '..')) {
155
+ throw new Error(`deploy.remoteDir 非法:不允许包含 ..(当前: ${resolved})`);
156
+ }
157
+
158
+ return resolved;
159
+ }
160
+
161
+ async function zipDirAsTopFolder({ srcDir, zipPath, topFolderName }) {
162
+ fs.ensureDirSync(path.dirname(zipPath));
163
+ await new Promise((resolve, reject) => {
164
+ const output = fs.createWriteStream(zipPath);
165
+ const archive = archiver('zip', { zlib: { level: 9 } });
166
+
167
+ output.on('close', resolve);
168
+ output.on('error', reject);
169
+ archive.on('warning', (err) => {
170
+ if (err && err.code === 'ENOENT') return;
171
+ reject(err);
172
+ });
173
+ archive.on('error', reject);
174
+
175
+ archive.pipe(output);
176
+ // 关键:zip 内顶层目录名强制为 buildDirName
177
+ archive.directory(srcDir, topFolderName);
178
+ archive.finalize();
179
+ });
180
+ }
181
+
182
+ function hashLettersFromSha256(buf, lettersLen = 12) {
183
+ const alphabet = 'abcdefghijklmnop'; // 16 letters for 0..15
184
+ let out = '';
185
+ for (let i = 0; i < buf.length && out.length < lettersLen; i++) {
186
+ const b = buf[i];
187
+ out += alphabet[(b >> 4) & 0xf];
188
+ if (out.length >= lettersLen) break;
189
+ out += alphabet[b & 0xf];
190
+ }
191
+ return out.slice(0, lettersLen);
192
+ }
193
+
194
+ function computeDirHashLetters(localDir) {
195
+ const crypto = require('crypto');
196
+ const h = crypto.createHash('sha256');
197
+ const files = listLocalFilesWithSize(localDir);
198
+ for (const f of files) {
199
+ h.update(f.relPosix);
200
+ h.update('\n');
201
+ h.update(String(f.size || 0));
202
+ h.update('\n');
203
+ h.update(String(Math.floor(f.mtimeMs || 0)));
204
+ h.update('\n');
205
+ }
206
+ const digest = h.digest();
207
+ return hashLettersFromSha256(digest, 12);
208
+ }
209
+
210
+ // 注意:这里"环境不存在或不可部署"的判断逻辑与 resolveDeployConfig 内部的
211
+ // findEnvironment + deployMethod==='none' 检查在语义上等价,只是分别在
212
+ // "交互选择阶段"和"最终解析阶段"各自独立校验一次(后者作为独立导出函数,
213
+ // 必须能脱离 chooseDeployEnv 单独工作,因此不合并成一个函数)。
214
+ async function chooseDeployEnv(projectConfig, envArg) {
215
+ const environments = (projectConfig.environments || []).filter((e) => e.deployMethod !== 'none');
216
+
217
+ if (envArg) {
218
+ if (!environments.some((e) => e.name === envArg)) {
219
+ const available = environments.map((e) => e.name).join(', ') || '(无)';
220
+ throw new Error(`环境 "${envArg}" 不可部署或不存在\n可部署环境: ${available}`);
221
+ }
222
+ return envArg;
223
+ }
224
+
225
+ if (environments.length === 0) {
226
+ throw new Error('没有可部署的环境(所有环境的 deployMethod 均为 none,或未定义 environments)');
227
+ }
228
+
229
+ const ans = await inquirer.prompt([
230
+ {
231
+ type: 'list',
232
+ name: 'env',
233
+ message: '请选择部署环境',
234
+ choices: environments.map((e) => ({
235
+ name: `${e.name}(${e.deployMethod === 'upload' ? '上传到服务器' : '仅压缩'})`,
236
+ value: e.name,
237
+ })),
238
+ },
239
+ ]);
240
+ return ans.env;
241
+ }
242
+
243
+ async function uploadDirWithProgress({ sftp, localDir, remoteDirPosix, onProgress }) {
244
+ const files = listLocalFilesWithSize(localDir);
245
+ const totalBytes = files.reduce((sum, f) => sum + (f.size || 0), 0);
246
+ if (files.length === 0) return { totalBytes: 0, uploadedBytes: 0, fileCount: 0 };
247
+
248
+ // 先创建需要的目录(去重)
249
+ const dirSet = new Set();
250
+ for (const f of files) {
251
+ const remoteFile = path.posix.join(remoteDirPosix, f.relPosix);
252
+ dirSet.add(path.posix.dirname(remoteFile));
253
+ }
254
+ // 目录从浅到深创建,避免多余失败
255
+ const dirs = Array.from(dirSet).sort((a, b) => a.length - b.length);
256
+ for (const d of dirs) {
257
+ await sftp.mkdir(d, true);
258
+ }
259
+
260
+ let uploadedBytes = 0;
261
+ let lastRenderAt = 0;
262
+ const render = (state) => {
263
+ if (typeof onProgress !== 'function') return;
264
+ const now = Date.now();
265
+ // 轻微节流,避免刷屏
266
+ if (now - lastRenderAt < 80 && state && state.force !== true) return;
267
+ lastRenderAt = now;
268
+ onProgress(state);
269
+ };
270
+
271
+ for (let i = 0; i < files.length; i++) {
272
+ const f = files[i];
273
+ const remoteFile = path.posix.join(remoteDirPosix, f.relPosix);
274
+ let lastTransferred = 0;
275
+
276
+ await sftp.fastPut(f.localPath, remoteFile, {
277
+ step: (transferred) => {
278
+ // transferred:当前文件已传输的总字节数(累计)
279
+ const delta = Math.max(0, Number(transferred || 0) - lastTransferred);
280
+ lastTransferred = Number(transferred || 0);
281
+ uploadedBytes += delta;
282
+ render({
283
+ uploadedBytes,
284
+ totalBytes,
285
+ fileIndex: i + 1,
286
+ fileCount: files.length,
287
+ currentFile: f.relPosix,
288
+ });
289
+ },
290
+ });
291
+
292
+ // fastPut 有时不会触发 step(极小文件/实现差异),这里兜底补齐本文件剩余字节
293
+ if (lastTransferred < f.size) {
294
+ uploadedBytes += f.size - lastTransferred;
295
+ }
296
+ render({
297
+ uploadedBytes,
298
+ totalBytes,
299
+ fileIndex: i + 1,
300
+ fileCount: files.length,
301
+ currentFile: f.relPosix,
302
+ force: true,
303
+ });
304
+ }
305
+
306
+ return { totalBytes, uploadedBytes, fileCount: files.length };
307
+ }
308
+
309
+ function resolveDeployConfig(envName, overrides = {}, projectConfig) {
310
+ if (!projectConfig) {
311
+ projectConfig = readProjectConfig();
312
+ assertConfigMigrated(projectConfig, fs.existsSync(getConfigPath()));
313
+ }
314
+ const env = findEnvironment(projectConfig, envName);
315
+
316
+ if (env.deployMethod === 'none') {
317
+ throw new Error(`环境 "${env.name}" 不支持部署(deployMethod=none)`);
318
+ }
319
+
320
+ const buildDir = overrides.buildDir ?? env.buildDir;
321
+ const localDirRaw = overrides.localDir ?? env.localDir ?? buildDir;
322
+ const localDir = localDirRaw ? path.resolve(process.cwd(), localDirRaw) : null;
323
+
324
+ if (!localDir) {
325
+ throw new Error(
326
+ `无法确定环境 "${env.name}" 的本地目录(用于上传/压缩)。请在 .ylyxrc.json 该环境条目配置 localDir 或 buildDir`
327
+ );
328
+ }
329
+
330
+ const cfg = {
331
+ name: env.name,
332
+ deployMethod: env.deployMethod,
333
+ buildDir,
334
+ host: overrides.host ?? env.host,
335
+ port: String(overrides.port ?? env.port ?? '22'),
336
+ username: overrides.username ?? env.username,
337
+ password: overrides.password ?? env.password,
338
+ localDir,
339
+ remoteDir: normalizeAndValidateRemoteDir(overrides.remoteDir ?? env.remoteDir),
340
+ zipAfter: overrides.zipAfter ?? env.zipAfter ?? false,
341
+ zipOutDir: path.resolve(process.cwd(), overrides.zipOutDir ?? env.zipOutDir ?? './.ylyx-deploy'),
342
+ };
343
+
344
+ return cfg;
345
+ }
346
+
347
+ /**
348
+ * 执行部署(按环境的 deployMethod 分支:upload=SFTP 上传,zip=仅压缩)
349
+ * @param {{env?:string,host?:string,port?:string|number,username?:string,password?:string,localDir?:string,remoteDir?:string,zipAfter?:boolean,zipOutDir?:string,buildDir?:string}} overrides
350
+ */
351
+ async function deploy(overrides = {}) {
352
+ const projectConfig = readProjectConfig();
353
+ assertConfigMigrated(projectConfig, fs.existsSync(getConfigPath()));
354
+
355
+ const envName = await chooseDeployEnv(projectConfig, overrides.env);
356
+ const cfg = resolveDeployConfig(envName, overrides, projectConfig);
357
+ const localFolderPath = cfg.localDir;
358
+ const remoteFolderPath = cfg.remoteDir;
359
+
360
+ const buildDirNameForLog = getBuildDirBaseName({ buildDir: cfg.buildDir, localDir: localFolderPath });
361
+ const logger = createDeployLogger({ env: cfg.name, buildDirName: buildDirNameForLog, outDir: cfg.zipOutDir });
362
+ logger.logOp('DEPLOY_START', '', `env=${cfg.name}`);
363
+ logger.logOp('LOCAL_DIR', localFolderPath);
364
+ if (remoteFolderPath) logger.logOp('REMOTE_DIR', remoteFolderPath);
365
+
366
+ if (!fs.existsSync(localFolderPath)) {
367
+ logger.logOp('ERROR', localFolderPath, 'localDir_not_exists');
368
+ await logger.close();
369
+ throw new Error(`本地目录不存在:${path.resolve(localFolderPath)}`);
370
+ }
371
+
372
+ // deployMethod=zip:仅压缩,不上传
373
+ if (cfg.deployMethod === 'zip') {
374
+ const buildDirName = buildDirNameForLog;
375
+ const ts = formatTimestampCompact(new Date());
376
+ const hashLetters = computeDirHashLetters(localFolderPath); // 纯字母
377
+ const zipBaseName = `${buildDirName}-${ts}-${hashLetters}`;
378
+ fs.ensureDirSync(cfg.zipOutDir);
379
+ const zipName = `${zipBaseName}.zip`;
380
+ const zipPath = path.join(cfg.zipOutDir, zipName);
381
+ logger.logOp('ZIP_START', zipPath);
382
+
383
+ const zipSpinner = ora(`开始压缩:${zipName}`).start();
384
+ try {
385
+ await zipDirAsTopFolder({ srcDir: localFolderPath, zipPath, topFolderName: buildDirName });
386
+ zipSpinner.succeed(`压缩完成:${path.relative(process.cwd(), zipPath)}`);
387
+ logger.logOp('ZIP_DONE', zipPath);
388
+ } catch (e) {
389
+ zipSpinner.fail('压缩失败');
390
+ logger.logOp('ERROR', zipPath, `zip_failed:${e && e.message ? e.message : String(e)}`);
391
+ await logger.close();
392
+ throw e;
393
+ }
394
+ logger.logOp('DEPLOY_END', '', cfg.name);
395
+ await logger.close();
396
+ return;
397
+ }
398
+
399
+ // deployMethod=upload:上传到服务器
400
+ if (!cfg.host) throw new Error(`缺少环境 "${cfg.name}" 的 host(上传需要)`);
401
+ if (!cfg.username) throw new Error(`缺少环境 "${cfg.name}" 的 username(上传需要)`);
402
+ if (!cfg.password) throw new Error(`缺少环境 "${cfg.name}" 的 password(上传需要)`);
403
+ if (!remoteFolderPath) {
404
+ logger.logOp('ERROR', '', 'missing_remoteDir');
405
+ await logger.close();
406
+ throw new Error(`缺少环境 "${cfg.name}" 的 remoteDir(上传需要):请在 .ylyxrc.json 该环境条目配置 remoteDir`);
407
+ }
408
+
409
+ let Client;
410
+ try {
411
+ Client = require('ssh2-sftp-client');
412
+ } catch (e) {
413
+ throw new Error(
414
+ "缺少依赖:ssh2-sftp-client。\n" +
415
+ "如果你在 ylyx-cli 仓库本地跑:请先在 ylyx-cli 目录执行 `npm i`。\n" +
416
+ "如果你用 npx:请确认你运行的是已发布且包含依赖的版本,或清理 npx 缓存后重试。"
417
+ );
418
+ }
419
+
420
+ const sftp = new Client();
421
+ try {
422
+ spinner = ora('连接服务器...').start();
423
+ logger.logOp('SFTP_CONNECT', cfg.host, `port=${cfg.port} username=${cfg.username}`);
424
+ await sftp.connect({
425
+ host: cfg.host,
426
+ port: cfg.port,
427
+ username: cfg.username,
428
+ password: cfg.password,
429
+ });
430
+
431
+ spinner.text = '服务器连接成功,准备清理远端目录...';
432
+ logger.logOp('REMOTE_DELETE_START', remoteFolderPath);
433
+ try {
434
+ await sftp.rmdir(remoteFolderPath, true);
435
+ logger.logOp('REMOTE_DELETE_DONE', remoteFolderPath);
436
+ } catch (e) {
437
+ // ignore
438
+ logger.logOp('REMOTE_DELETE_SKIP', remoteFolderPath, e && e.message ? e.message : 'ignored');
439
+ }
440
+ await sftp.mkdir(remoteFolderPath, true);
441
+ logger.logOp('REMOTE_MKDIR', remoteFolderPath);
442
+
443
+ const remoteDirPosix = remoteFolderPath.replace(/\\/g, '/');
444
+ spinner.text = '开始上传目录...';
445
+ logger.logOp('UPLOAD_START', remoteDirPosix, `from=${localFolderPath}`);
446
+ const { uploadedBytes, totalBytes, fileCount } = await uploadDirWithProgress({
447
+ sftp,
448
+ localDir: localFolderPath,
449
+ remoteDirPosix,
450
+ onProgress: ({ uploadedBytes: up, totalBytes: tot, fileIndex, fileCount: cnt, currentFile }) => {
451
+ const percent = tot ? ((up / tot) * 100).toFixed(2) : '100.00';
452
+ const shortFile = currentFile ? currentFile.slice(-60) : '';
453
+ spinner.text = `上传进度: ${percent}% (${formatBytes(up)}/${formatBytes(tot)}) 文件:${fileIndex}/${cnt} ${shortFile}`;
454
+ },
455
+ });
456
+ spinner.succeed(`上传成功:${formatBytes(uploadedBytes)}/${formatBytes(totalBytes)},共 ${fileCount} 个文件`);
457
+ logger.logOp('UPLOAD_DONE', remoteDirPosix, `files=${fileCount} bytes=${uploadedBytes}/${totalBytes}`);
458
+ } catch (err) {
459
+ if (spinner) spinner.fail('部署失败');
460
+ logger.logOp('ERROR', '', err && err.message ? err.message : String(err));
461
+ await logger.close();
462
+ throw err;
463
+ } finally {
464
+ try {
465
+ await sftp.end();
466
+ } catch (e) {}
467
+ if (spinner) spinner.info('部署结束');
468
+ }
469
+
470
+ // 上传后可选:再压缩一份(延续之前的可选开关)
471
+ if (cfg.zipAfter) {
472
+ const buildDirName = buildDirNameForLog;
473
+ const ts = formatTimestampCompact(new Date());
474
+ const hashLetters = computeDirHashLetters(localFolderPath); // 纯字母
475
+ const zipBaseName = `${buildDirName}-${ts}-${hashLetters}`;
476
+ fs.ensureDirSync(cfg.zipOutDir);
477
+ const zipName = `${zipBaseName}.zip`;
478
+ const zipPath = path.join(cfg.zipOutDir, zipName);
479
+ logger.logOp('ZIP_START', zipPath);
480
+
481
+ const zipSpinner = ora(`开始压缩:${zipName}`).start();
482
+ try {
483
+ await zipDirAsTopFolder({ srcDir: localFolderPath, zipPath, topFolderName: buildDirName });
484
+ zipSpinner.succeed(`压缩完成:${path.relative(process.cwd(), zipPath)}`);
485
+ logger.logOp('ZIP_DONE', zipPath);
486
+ } catch (e) {
487
+ zipSpinner.fail('压缩失败');
488
+ logger.logOp('ERROR', zipPath, `zip_failed:${e && e.message ? e.message : String(e)}`);
489
+ await logger.close();
490
+ throw e;
491
+ }
492
+ }
493
+
494
+ logger.logOp('DEPLOY_END', '', cfg.name);
495
+ await logger.close();
496
+ }
497
+
498
+ module.exports = {
499
+ deploy,
500
+ resolveDeployConfig,
501
+ };
502
+
503
+ // 允许直接 node lib/deploy.js 运行(从 .ylyxrc.json 读取 deploy 配置)
504
+ if (require.main === module) {
505
+ deploy().catch((e) => {
506
+ console.error(e);
507
+ process.exit(1);
508
+ });
509
+ }