@qpqjs/cli 0.3.0 → 0.5.0

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 (292) hide show
  1. package/lib/commonjs/cli/commandMenu.js +5 -4
  2. package/lib/commonjs/cli/commandMenu.js.map +1 -1
  3. package/lib/commonjs/cli/runCli.js +15 -7
  4. package/lib/commonjs/cli/runCli.js.map +1 -1
  5. package/lib/commonjs/commands/checkCircular.js +15 -7
  6. package/lib/commonjs/commands/checkCircular.js.map +1 -1
  7. package/lib/commonjs/commands/clearResources.js +8 -14
  8. package/lib/commonjs/commands/clearResources.js.map +1 -1
  9. package/lib/commonjs/commands/go.js +3 -7
  10. package/lib/commonjs/commands/go.js.map +1 -1
  11. package/lib/commonjs/commands/goDev.js +4 -2
  12. package/lib/commonjs/commands/goDev.js.map +1 -1
  13. package/lib/commonjs/commands/goDevApi.js +27 -23
  14. package/lib/commonjs/commands/goDevApi.js.map +1 -1
  15. package/lib/commonjs/commands/goDevWeb.js +14 -9
  16. package/lib/commonjs/commands/goDevWeb.js.map +1 -1
  17. package/lib/commonjs/commands/goDocker.js +10 -13
  18. package/lib/commonjs/commands/goDocker.js.map +1 -1
  19. package/lib/commonjs/commands/hooks.js +4 -2
  20. package/lib/commonjs/commands/hooks.js.map +1 -1
  21. package/lib/commonjs/commands/migrate.js +11 -9
  22. package/lib/commonjs/commands/migrate.js.map +1 -1
  23. package/lib/commonjs/commands/prep.js +4 -2
  24. package/lib/commonjs/commands/prep.js.map +1 -1
  25. package/lib/commonjs/commands/publish.js +4 -7
  26. package/lib/commonjs/commands/publish.js.map +1 -1
  27. package/lib/commonjs/commands/scriptBuild.js +7 -5
  28. package/lib/commonjs/commands/scriptBuild.js.map +1 -1
  29. package/lib/commonjs/commands/scriptBuilderLayer.js +6 -4
  30. package/lib/commonjs/commands/scriptBuilderLayer.js.map +1 -1
  31. package/lib/commonjs/commands/scriptNew.js +7 -5
  32. package/lib/commonjs/commands/scriptNew.js.map +1 -1
  33. package/lib/commonjs/commands/setup.js +14 -22
  34. package/lib/commonjs/commands/setup.js.map +1 -1
  35. package/lib/commonjs/commands/synth.js +16 -20
  36. package/lib/commonjs/commands/synth.js.map +1 -1
  37. package/lib/commonjs/commands/teardown.js +8 -14
  38. package/lib/commonjs/commands/teardown.js.map +1 -1
  39. package/lib/commonjs/lib/CliError.d.ts +4 -0
  40. package/lib/commonjs/lib/CliError.js +14 -0
  41. package/lib/commonjs/lib/CliError.js.map +1 -0
  42. package/lib/commonjs/lib/Output.d.ts +12 -0
  43. package/lib/commonjs/lib/Output.js +3 -0
  44. package/lib/commonjs/lib/Output.js.map +1 -0
  45. package/lib/commonjs/lib/circularImports.d.ts +0 -1
  46. package/lib/commonjs/lib/circularImports.js +5 -6
  47. package/lib/commonjs/lib/circularImports.js.map +1 -1
  48. package/lib/commonjs/lib/clearResourcesPlan.d.ts +0 -1
  49. package/lib/commonjs/lib/clearResourcesPlan.js +12 -13
  50. package/lib/commonjs/lib/clearResourcesPlan.js.map +1 -1
  51. package/lib/commonjs/lib/createOutput.d.ts +2 -0
  52. package/lib/commonjs/lib/createOutput.js +39 -0
  53. package/lib/commonjs/lib/createOutput.js.map +1 -0
  54. package/lib/commonjs/lib/deployPrompts.d.ts +2 -1
  55. package/lib/commonjs/lib/deployPrompts.js +16 -16
  56. package/lib/commonjs/lib/deployPrompts.js.map +1 -1
  57. package/lib/commonjs/lib/devServerEntry.js +5 -4
  58. package/lib/commonjs/lib/devServerEntry.js.map +1 -1
  59. package/lib/commonjs/lib/devServerPorts.js +2 -4
  60. package/lib/commonjs/lib/devServerPorts.js.map +1 -1
  61. package/lib/commonjs/lib/discovery.d.ts +0 -2
  62. package/lib/commonjs/lib/discovery.js +11 -44
  63. package/lib/commonjs/lib/discovery.js.map +1 -1
  64. package/lib/commonjs/lib/hooks.js +5 -3
  65. package/lib/commonjs/lib/hooks.js.map +1 -1
  66. package/lib/commonjs/lib/killChildWithEscalation.js +3 -1
  67. package/lib/commonjs/lib/killChildWithEscalation.js.map +1 -1
  68. package/lib/commonjs/lib/killStaleListeners.js +24 -37
  69. package/lib/commonjs/lib/killStaleListeners.js.map +1 -1
  70. package/lib/commonjs/lib/prompts.d.ts +1 -0
  71. package/lib/commonjs/lib/prompts.js +15 -2
  72. package/lib/commonjs/lib/prompts.js.map +1 -1
  73. package/lib/commonjs/lib/qpqConfigs.d.ts +1 -0
  74. package/lib/commonjs/lib/qpqConfigs.js +11 -6
  75. package/lib/commonjs/lib/qpqConfigs.js.map +1 -1
  76. package/lib/commonjs/lib/requireDriverCapability.d.ts +7 -0
  77. package/lib/commonjs/lib/requireDriverCapability.js +17 -0
  78. package/lib/commonjs/lib/requireDriverCapability.js.map +1 -0
  79. package/lib/commonjs/lib/resolveAppSelection.d.ts +1 -0
  80. package/lib/commonjs/lib/resolveAppSelection.js +19 -12
  81. package/lib/commonjs/lib/resolveAppSelection.js.map +1 -1
  82. package/lib/commonjs/lib/resolveCommandContext.d.ts +12 -0
  83. package/lib/commonjs/lib/resolveCommandContext.js +26 -0
  84. package/lib/commonjs/lib/resolveCommandContext.js.map +1 -0
  85. package/lib/commonjs/lib/resolveDeployment.d.ts +0 -1
  86. package/lib/commonjs/lib/resolveDeployment.js +15 -10
  87. package/lib/commonjs/lib/resolveDeployment.js.map +1 -1
  88. package/lib/commonjs/lib/runCommand.d.ts +20 -0
  89. package/lib/commonjs/lib/runCommand.js +64 -34
  90. package/lib/commonjs/lib/runCommand.js.map +1 -1
  91. package/lib/commonjs/lib/runTasks.js +8 -4
  92. package/lib/commonjs/lib/runTasks.js.map +1 -1
  93. package/lib/commonjs/lib/timing.js +5 -3
  94. package/lib/commonjs/lib/timing.js.map +1 -1
  95. package/lib/commonjs/lib/typeWatcher.d.ts +2 -1
  96. package/lib/commonjs/lib/typeWatcher.js +6 -13
  97. package/lib/commonjs/lib/typeWatcher.js.map +1 -1
  98. package/lib/commonjs/lib/userScriptBuilderLayer.d.ts +0 -2
  99. package/lib/commonjs/lib/userScriptBuilderLayer.js +7 -8
  100. package/lib/commonjs/lib/userScriptBuilderLayer.js.map +1 -1
  101. package/lib/commonjs/lib/views.js +3 -2
  102. package/lib/commonjs/lib/views.js.map +1 -1
  103. package/lib/commonjs/lib/webAddressingEnv.d.ts +0 -2
  104. package/lib/commonjs/lib/webAddressingEnv.js +4 -4
  105. package/lib/commonjs/lib/webAddressingEnv.js.map +1 -1
  106. package/lib/commonjs/platforms/aws/awsCredentials.js +3 -1
  107. package/lib/commonjs/platforms/aws/awsCredentials.js.map +1 -1
  108. package/lib/commonjs/platforms/aws/cdkApp.js +1 -1
  109. package/lib/commonjs/platforms/aws/cdkApp.js.map +1 -1
  110. package/lib/commonjs/platforms/aws/clearResources.js +17 -26
  111. package/lib/commonjs/platforms/aws/clearResources.js.map +1 -1
  112. package/lib/commonjs/platforms/aws/getDeployEnv.d.ts +1 -0
  113. package/lib/commonjs/platforms/aws/getDeployEnv.js +12 -0
  114. package/lib/commonjs/platforms/aws/getDeployEnv.js.map +1 -0
  115. package/lib/commonjs/platforms/aws/go.js +17 -15
  116. package/lib/commonjs/platforms/aws/go.js.map +1 -1
  117. package/lib/commonjs/platforms/aws/goDocker.js +29 -26
  118. package/lib/commonjs/platforms/aws/goDocker.js.map +1 -1
  119. package/lib/commonjs/platforms/aws/goDockerManifest.js +6 -1
  120. package/lib/commonjs/platforms/aws/goDockerManifest.js.map +1 -1
  121. package/lib/commonjs/platforms/aws/publish.js +13 -11
  122. package/lib/commonjs/platforms/aws/publish.js.map +1 -1
  123. package/lib/commonjs/platforms/aws/remote.js +8 -7
  124. package/lib/commonjs/platforms/aws/remote.js.map +1 -1
  125. package/lib/commonjs/platforms/aws/setup/buildBucketNamesStep.js +4 -4
  126. package/lib/commonjs/platforms/aws/setup/buildBucketNamesStep.js.map +1 -1
  127. package/lib/commonjs/platforms/aws/setup/buildGithubDeployRoleStep.js +4 -3
  128. package/lib/commonjs/platforms/aws/setup/buildGithubDeployRoleStep.js.map +1 -1
  129. package/lib/commonjs/platforms/aws/stacks.d.ts +0 -1
  130. package/lib/commonjs/platforms/aws/stacks.js +17 -33
  131. package/lib/commonjs/platforms/aws/stacks.js.map +1 -1
  132. package/lib/commonjs/platforms/aws/teardown.js +9 -7
  133. package/lib/commonjs/platforms/aws/teardown.js.map +1 -1
  134. package/lib/commonjs/platforms/aws/viewsSync.js +23 -11
  135. package/lib/commonjs/platforms/aws/viewsSync.js.map +1 -1
  136. package/lib/commonjs/platforms/docker/go.js +20 -18
  137. package/lib/commonjs/platforms/docker/go.js.map +1 -1
  138. package/lib/commonjs/platforms/docker/index.js +5 -4
  139. package/lib/commonjs/platforms/docker/index.js.map +1 -1
  140. package/lib/commonjs/platforms/docker/parseMounts.js +3 -2
  141. package/lib/commonjs/platforms/docker/parseMounts.js.map +1 -1
  142. package/lib/commonjs/platforms/index.js +4 -2
  143. package/lib/commonjs/platforms/index.js.map +1 -1
  144. package/lib/commonjs/platforms/types.d.ts +2 -2
  145. package/lib/commonjs/platforms/types.js.map +1 -1
  146. package/lib/esm/cli/commandMenu.js +5 -4
  147. package/lib/esm/cli/commandMenu.js.map +1 -1
  148. package/lib/esm/cli/runCli.js +15 -7
  149. package/lib/esm/cli/runCli.js.map +1 -1
  150. package/lib/esm/commands/checkCircular.js +15 -7
  151. package/lib/esm/commands/checkCircular.js.map +1 -1
  152. package/lib/esm/commands/clearResources.js +8 -14
  153. package/lib/esm/commands/clearResources.js.map +1 -1
  154. package/lib/esm/commands/go.js +3 -7
  155. package/lib/esm/commands/go.js.map +1 -1
  156. package/lib/esm/commands/goDev.js +4 -2
  157. package/lib/esm/commands/goDev.js.map +1 -1
  158. package/lib/esm/commands/goDevApi.js +24 -21
  159. package/lib/esm/commands/goDevApi.js.map +1 -1
  160. package/lib/esm/commands/goDevWeb.js +13 -9
  161. package/lib/esm/commands/goDevWeb.js.map +1 -1
  162. package/lib/esm/commands/goDocker.js +10 -13
  163. package/lib/esm/commands/goDocker.js.map +1 -1
  164. package/lib/esm/commands/hooks.js +4 -2
  165. package/lib/esm/commands/hooks.js.map +1 -1
  166. package/lib/esm/commands/migrate.js +9 -7
  167. package/lib/esm/commands/migrate.js.map +1 -1
  168. package/lib/esm/commands/prep.js +4 -2
  169. package/lib/esm/commands/prep.js.map +1 -1
  170. package/lib/esm/commands/publish.js +4 -7
  171. package/lib/esm/commands/publish.js.map +1 -1
  172. package/lib/esm/commands/scriptBuild.js +7 -5
  173. package/lib/esm/commands/scriptBuild.js.map +1 -1
  174. package/lib/esm/commands/scriptBuilderLayer.js +6 -4
  175. package/lib/esm/commands/scriptBuilderLayer.js.map +1 -1
  176. package/lib/esm/commands/scriptNew.js +7 -5
  177. package/lib/esm/commands/scriptNew.js.map +1 -1
  178. package/lib/esm/commands/setup.js +14 -22
  179. package/lib/esm/commands/setup.js.map +1 -1
  180. package/lib/esm/commands/synth.js +18 -22
  181. package/lib/esm/commands/synth.js.map +1 -1
  182. package/lib/esm/commands/teardown.js +8 -14
  183. package/lib/esm/commands/teardown.js.map +1 -1
  184. package/lib/esm/lib/CliError.d.ts +4 -0
  185. package/lib/esm/lib/CliError.js +11 -0
  186. package/lib/esm/lib/CliError.js.map +1 -0
  187. package/lib/esm/lib/Output.d.ts +12 -0
  188. package/lib/esm/lib/Output.js +2 -0
  189. package/lib/esm/lib/Output.js.map +1 -0
  190. package/lib/esm/lib/circularImports.d.ts +0 -1
  191. package/lib/esm/lib/circularImports.js +4 -4
  192. package/lib/esm/lib/circularImports.js.map +1 -1
  193. package/lib/esm/lib/clearResourcesPlan.d.ts +0 -1
  194. package/lib/esm/lib/clearResourcesPlan.js +12 -12
  195. package/lib/esm/lib/clearResourcesPlan.js.map +1 -1
  196. package/lib/esm/lib/createOutput.d.ts +2 -0
  197. package/lib/esm/lib/createOutput.js +35 -0
  198. package/lib/esm/lib/createOutput.js.map +1 -0
  199. package/lib/esm/lib/deployPrompts.d.ts +2 -1
  200. package/lib/esm/lib/deployPrompts.js +17 -17
  201. package/lib/esm/lib/deployPrompts.js.map +1 -1
  202. package/lib/esm/lib/devServerEntry.js +5 -4
  203. package/lib/esm/lib/devServerEntry.js.map +1 -1
  204. package/lib/esm/lib/devServerPorts.js +2 -3
  205. package/lib/esm/lib/devServerPorts.js.map +1 -1
  206. package/lib/esm/lib/discovery.d.ts +0 -2
  207. package/lib/esm/lib/discovery.js +10 -38
  208. package/lib/esm/lib/discovery.js.map +1 -1
  209. package/lib/esm/lib/hooks.js +5 -3
  210. package/lib/esm/lib/hooks.js.map +1 -1
  211. package/lib/esm/lib/killChildWithEscalation.js +3 -1
  212. package/lib/esm/lib/killChildWithEscalation.js.map +1 -1
  213. package/lib/esm/lib/killStaleListeners.js +24 -37
  214. package/lib/esm/lib/killStaleListeners.js.map +1 -1
  215. package/lib/esm/lib/prompts.d.ts +1 -0
  216. package/lib/esm/lib/prompts.js +13 -1
  217. package/lib/esm/lib/prompts.js.map +1 -1
  218. package/lib/esm/lib/qpqConfigs.d.ts +1 -0
  219. package/lib/esm/lib/qpqConfigs.js +9 -5
  220. package/lib/esm/lib/qpqConfigs.js.map +1 -1
  221. package/lib/esm/lib/requireDriverCapability.d.ts +7 -0
  222. package/lib/esm/lib/requireDriverCapability.js +13 -0
  223. package/lib/esm/lib/requireDriverCapability.js.map +1 -0
  224. package/lib/esm/lib/resolveAppSelection.d.ts +1 -0
  225. package/lib/esm/lib/resolveAppSelection.js +17 -11
  226. package/lib/esm/lib/resolveAppSelection.js.map +1 -1
  227. package/lib/esm/lib/resolveCommandContext.d.ts +12 -0
  228. package/lib/esm/lib/resolveCommandContext.js +12 -0
  229. package/lib/esm/lib/resolveCommandContext.js.map +1 -0
  230. package/lib/esm/lib/resolveDeployment.d.ts +0 -1
  231. package/lib/esm/lib/resolveDeployment.js +13 -8
  232. package/lib/esm/lib/resolveDeployment.js.map +1 -1
  233. package/lib/esm/lib/runCommand.d.ts +20 -0
  234. package/lib/esm/lib/runCommand.js +52 -31
  235. package/lib/esm/lib/runCommand.js.map +1 -1
  236. package/lib/esm/lib/runTasks.js +8 -4
  237. package/lib/esm/lib/runTasks.js.map +1 -1
  238. package/lib/esm/lib/timing.js +5 -3
  239. package/lib/esm/lib/timing.js.map +1 -1
  240. package/lib/esm/lib/typeWatcher.d.ts +2 -1
  241. package/lib/esm/lib/typeWatcher.js +6 -13
  242. package/lib/esm/lib/typeWatcher.js.map +1 -1
  243. package/lib/esm/lib/userScriptBuilderLayer.d.ts +0 -2
  244. package/lib/esm/lib/userScriptBuilderLayer.js +5 -5
  245. package/lib/esm/lib/userScriptBuilderLayer.js.map +1 -1
  246. package/lib/esm/lib/views.js +3 -2
  247. package/lib/esm/lib/views.js.map +1 -1
  248. package/lib/esm/lib/webAddressingEnv.d.ts +0 -2
  249. package/lib/esm/lib/webAddressingEnv.js +1 -1
  250. package/lib/esm/lib/webAddressingEnv.js.map +1 -1
  251. package/lib/esm/platforms/aws/awsCredentials.js +3 -1
  252. package/lib/esm/platforms/aws/awsCredentials.js.map +1 -1
  253. package/lib/esm/platforms/aws/cdkApp.js +1 -1
  254. package/lib/esm/platforms/aws/cdkApp.js.map +1 -1
  255. package/lib/esm/platforms/aws/clearResources.js +18 -26
  256. package/lib/esm/platforms/aws/clearResources.js.map +1 -1
  257. package/lib/esm/platforms/aws/getDeployEnv.d.ts +1 -0
  258. package/lib/esm/platforms/aws/getDeployEnv.js +8 -0
  259. package/lib/esm/platforms/aws/getDeployEnv.js.map +1 -0
  260. package/lib/esm/platforms/aws/go.js +17 -15
  261. package/lib/esm/platforms/aws/go.js.map +1 -1
  262. package/lib/esm/platforms/aws/goDocker.js +29 -26
  263. package/lib/esm/platforms/aws/goDocker.js.map +1 -1
  264. package/lib/esm/platforms/aws/goDockerManifest.js +6 -1
  265. package/lib/esm/platforms/aws/goDockerManifest.js.map +1 -1
  266. package/lib/esm/platforms/aws/publish.js +13 -11
  267. package/lib/esm/platforms/aws/publish.js.map +1 -1
  268. package/lib/esm/platforms/aws/remote.js +9 -7
  269. package/lib/esm/platforms/aws/remote.js.map +1 -1
  270. package/lib/esm/platforms/aws/setup/buildBucketNamesStep.js +2 -2
  271. package/lib/esm/platforms/aws/setup/buildBucketNamesStep.js.map +1 -1
  272. package/lib/esm/platforms/aws/setup/buildGithubDeployRoleStep.js +4 -3
  273. package/lib/esm/platforms/aws/setup/buildGithubDeployRoleStep.js.map +1 -1
  274. package/lib/esm/platforms/aws/stacks.d.ts +0 -1
  275. package/lib/esm/platforms/aws/stacks.js +20 -32
  276. package/lib/esm/platforms/aws/stacks.js.map +1 -1
  277. package/lib/esm/platforms/aws/teardown.js +9 -7
  278. package/lib/esm/platforms/aws/teardown.js.map +1 -1
  279. package/lib/esm/platforms/aws/viewsSync.js +22 -10
  280. package/lib/esm/platforms/aws/viewsSync.js.map +1 -1
  281. package/lib/esm/platforms/docker/go.js +21 -19
  282. package/lib/esm/platforms/docker/go.js.map +1 -1
  283. package/lib/esm/platforms/docker/index.js +5 -4
  284. package/lib/esm/platforms/docker/index.js.map +1 -1
  285. package/lib/esm/platforms/docker/parseMounts.js +3 -2
  286. package/lib/esm/platforms/docker/parseMounts.js.map +1 -1
  287. package/lib/esm/platforms/index.js +4 -2
  288. package/lib/esm/platforms/index.js.map +1 -1
  289. package/lib/esm/platforms/types.d.ts +2 -2
  290. package/lib/esm/platforms/types.js.map +1 -1
  291. package/package.json +28 -15
  292. package/templates/user-script/README.md +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"devServerPorts.js","sourceRoot":"","sources":["../../../src/lib/devServerPorts.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAK9C,wHAAwH;AACxH,MAAM,CAAC,MAAM,wBAAwB,GAA2B;IAC9D,GAAG,EAAE,IAAI;IACT,SAAS,EAAE,IAAI;IACf,WAAW,EAAE,IAAI;CAClB,CAAC;AAEF,gGAAgG;AAChG,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,OAAe,EAAU,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,qBAAqB,CAAC,CAAC;AAE9H;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,OAAe,EAA0B,EAAE;IAC5E,MAAM,UAAU,GAAG,sBAAsB,CAAC,OAAO,CAAC,CAAC;IACnD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,OAAO,wBAAwB,CAAC;IAClC,CAAC;IAED,iEAAiE;IACjE,MAAM,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;IACxD,MAAM,SAAS,GAAG,CAAC,MAAM,CAAC,OAAO,IAAI,MAAM,CAA6B,CAAC;IAEzE,OAAO,EAAE,GAAG,wBAAwB,EAAE,GAAG,SAAS,CAAC,KAAK,EAAE,CAAC;AAC7D,CAAC,CAAC","sourcesContent":["import { DevServerConfigOverrides, DevServerPorts } from '@qpqjs/dev-server';\n\nimport fs from 'fs';\nimport path from 'path';\n\nimport { getAppDirectory } from './discovery';\n\n/** Every dev server listener, with the app's overrides applied. */\nexport type ResolvedDevServerPorts = Required<DevServerPorts>;\n\n/** The ports a dev server listens on when the app sets none: api + static web, websockets, file storage secure urls. */\nexport const DEFAULT_DEV_SERVER_PORTS: ResolvedDevServerPorts = {\n api: 8080,\n webSocket: 8888,\n fileStorage: 3001,\n};\n\n/** Path of the app's optional dev server settings module (`apps/<app>/devServer.config.ts`). */\nexport const getDevServerConfigPath = (appName: string): string => path.join(getAppDirectory(appName), 'devServer.config.ts');\n\n/**\n * The app's dev server ports: `ports` from apps/<app>/devServer.config.ts over the defaults. The\n * same numbers the running server resolves, so builds that bake or expose ports agree with it.\n * Requiring the TS module relies on the caller running with TS require hooks, as every command does.\n */\nexport const readDevServerPorts = (appName: string): ResolvedDevServerPorts => {\n const configPath = getDevServerConfigPath(appName);\n if (!fs.existsSync(configPath)) {\n return DEFAULT_DEV_SERVER_PORTS;\n }\n\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const loaded = require(configPath.replace(/\\.ts$/, ''));\n const overrides = (loaded.default ?? loaded) as DevServerConfigOverrides;\n\n return { ...DEFAULT_DEV_SERVER_PORTS, ...overrides.ports };\n};\n"]}
1
+ {"version":3,"file":"devServerPorts.js","sourceRoot":"","sources":["../../../src/lib/devServerPorts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAGzD,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAK9C,wHAAwH;AACxH,MAAM,CAAC,MAAM,wBAAwB,GAA2B;IAC9D,GAAG,EAAE,IAAI;IACT,SAAS,EAAE,IAAI;IACf,WAAW,EAAE,IAAI;CAClB,CAAC;AAEF,gGAAgG;AAChG,MAAM,CAAC,MAAM,sBAAsB,GAAG,CAAC,OAAe,EAAU,EAAE,CAAC,IAAI,CAAC,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC,EAAE,qBAAqB,CAAC,CAAC;AAE9H;;;;GAIG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,OAAe,EAA0B,EAAE;IAC5E,MAAM,UAAU,GAAG,sBAAsB,CAAC,OAAO,CAAC,CAAC;IACnD,IAAI,CAAC,EAAE,CAAC,UAAU,CAAC,UAAU,CAAC,EAAE,CAAC;QAC/B,OAAO,wBAAwB,CAAC;IAClC,CAAC;IAED,MAAM,SAAS,GAAG,oBAAoB,CAA2B,UAAU,CAAC,OAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,CAAC;IAElG,OAAO,EAAE,GAAG,wBAAwB,EAAE,GAAG,SAAS,CAAC,KAAK,EAAE,CAAC;AAC7D,CAAC,CAAC","sourcesContent":["import { requireModuleDefault } from '@qpqjs/deploy-aws';\nimport { DevServerConfigOverrides, DevServerPorts } from '@qpqjs/dev-server';\n\nimport fs from 'fs';\nimport path from 'path';\n\nimport { getAppDirectory } from './discovery';\n\n/** Every dev server listener, with the app's overrides applied. */\nexport type ResolvedDevServerPorts = Required<DevServerPorts>;\n\n/** The ports a dev server listens on when the app sets none: api + static web, websockets, file storage secure urls. */\nexport const DEFAULT_DEV_SERVER_PORTS: ResolvedDevServerPorts = {\n api: 8080,\n webSocket: 8888,\n fileStorage: 3001,\n};\n\n/** Path of the app's optional dev server settings module (`apps/<app>/devServer.config.ts`). */\nexport const getDevServerConfigPath = (appName: string): string => path.join(getAppDirectory(appName), 'devServer.config.ts');\n\n/**\n * The app's dev server ports: `ports` from apps/<app>/devServer.config.ts over the defaults. The\n * same numbers the running server resolves, so builds that bake or expose ports agree with it.\n * Requiring the TS module relies on the caller running with TS require hooks, as every command does.\n */\nexport const readDevServerPorts = (appName: string): ResolvedDevServerPorts => {\n const configPath = getDevServerConfigPath(appName);\n if (!fs.existsSync(configPath)) {\n return DEFAULT_DEV_SERVER_PORTS;\n }\n\n const overrides = requireModuleDefault<DevServerConfigOverrides>(configPath.replace(/\\.ts$/, ''));\n\n return { ...DEFAULT_DEV_SERVER_PORTS, ...overrides.ports };\n};\n"]}
@@ -3,8 +3,6 @@ export declare const getAppsDirectory: () => string;
3
3
  export declare const getAppDirectory: (appName: string) => string;
4
4
  export declare const getServiceDirectory: (appName: string, service: string) => string;
5
5
  export declare const getAvailableApps: () => string[];
6
- export declare const getAllServiceNames: (appName: string) => string[];
7
- export declare const serviceHasSubdir: (appName: string, service: string, subdir: string) => boolean;
8
6
  export declare const getServiceNamesWithSubdir: (appName: string, subdir: string) => string[];
9
7
  export declare const getServiceNames: (appName: string) => string[];
10
8
  export declare const getServiceNamesWithViews: (appName: string) => string[];
@@ -1,40 +1,12 @@
1
- import fs from 'fs';
2
- import path from 'path';
3
- // The consumer repo root — qpq always runs from the workspace root (npm
4
- // scripts set cwd there).
1
+ import { getAppPath, getAppsPath, getAvailableAppNames, getBackendServiceNames, getServiceNamesWithSubdir as getServiceNamesWithSubdirIn, getServiceNamesWithViews as getServiceNamesWithViewsIn, getServicePath, } from '@qpqjs/deploy-rspack';
2
+ // The consumer repo root, qpq always runs from the workspace root (npm
3
+ // scripts set cwd there). Discovery itself is deploy-rspack's; these bind it to that root.
5
4
  export const getRoot = () => process.cwd();
6
- export const getAppsDirectory = () => path.join(getRoot(), 'apps');
7
- export const getAppDirectory = (appName) => path.join(getAppsDirectory(), appName);
8
- export const getServiceDirectory = (appName, service) => path.join(getAppDirectory(appName), 'services', service);
9
- // Apps are the directories under apps/ that contain a services/ directory.
10
- export const getAvailableApps = () => {
11
- const appsPath = getAppsDirectory();
12
- if (!fs.existsSync(appsPath)) {
13
- return [];
14
- }
15
- return fs.readdirSync(appsPath).filter((name) => {
16
- const servicesPath = path.join(getAppDirectory(name), 'services');
17
- return fs.statSync(getAppDirectory(name)).isDirectory() && fs.existsSync(servicesPath) && fs.statSync(servicesPath).isDirectory();
18
- });
19
- };
20
- // Every service directory under an app's services/ folder (no subdir filter).
21
- export const getAllServiceNames = (appName) => {
22
- const directoryPath = path.join(getAppDirectory(appName), 'services');
23
- if (!fs.existsSync(directoryPath)) {
24
- return [];
25
- }
26
- return fs.readdirSync(directoryPath).filter((name) => fs.statSync(getServiceDirectory(appName, name)).isDirectory());
27
- };
28
- // True when a service contains the given child directory (e.g. 'service' for a
29
- // backend, 'views' for a frontend).
30
- export const serviceHasSubdir = (appName, service, subdir) => {
31
- const subdirPath = path.join(getServiceDirectory(appName, service), subdir);
32
- return fs.existsSync(subdirPath) && fs.statSync(subdirPath).isDirectory();
33
- };
34
- // Services that contain a given child directory.
35
- export const getServiceNamesWithSubdir = (appName, subdir) => getAllServiceNames(appName).filter((service) => serviceHasSubdir(appName, service, subdir));
36
- // Backend services — those with a service/ subdir.
37
- export const getServiceNames = (appName) => getServiceNamesWithSubdir(appName, 'service');
38
- // Backend services that also ship a frontend (views/ subdir).
39
- export const getServiceNamesWithViews = (appName) => getServiceNames(appName).filter((service) => serviceHasSubdir(appName, service, 'views'));
5
+ export const getAppsDirectory = () => getAppsPath(getRoot());
6
+ export const getAppDirectory = (appName) => getAppPath(getRoot(), appName);
7
+ export const getServiceDirectory = (appName, service) => getServicePath(getRoot(), appName, service);
8
+ export const getAvailableApps = () => getAvailableAppNames(getRoot());
9
+ export const getServiceNamesWithSubdir = (appName, subdir) => getServiceNamesWithSubdirIn(getRoot(), appName, subdir);
10
+ export const getServiceNames = (appName) => getBackendServiceNames(getRoot(), appName);
11
+ export const getServiceNamesWithViews = (appName) => getServiceNamesWithViewsIn(getRoot(), appName);
40
12
  //# sourceMappingURL=discovery.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../../src/lib/discovery.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from 'fs';\nimport path from 'path';\n\n// The consumer repo root — qpq always runs from the workspace root (npm\n// scripts set cwd there).\nexport const getRoot = (): string => process.cwd();\n\nexport const getAppsDirectory = (): string => path.join(getRoot(), 'apps');\nexport const getAppDirectory = (appName: string): string => path.join(getAppsDirectory(), appName);\nexport const getServiceDirectory = (appName: string, service: string): string => path.join(getAppDirectory(appName), 'services', service);\n\n// Apps are the directories under apps/ that contain a services/ directory.\nexport const getAvailableApps = (): string[] => {\n const appsPath = getAppsDirectory();\n\n if (!fs.existsSync(appsPath)) {\n return [];\n }\n\n return fs.readdirSync(appsPath).filter((name) => {\n const servicesPath = path.join(getAppDirectory(name), 'services');\n return fs.statSync(getAppDirectory(name)).isDirectory() && fs.existsSync(servicesPath) && fs.statSync(servicesPath).isDirectory();\n });\n};\n\n// Every service directory under an app's services/ folder (no subdir filter).\nexport const getAllServiceNames = (appName: string): string[] => {\n const directoryPath = path.join(getAppDirectory(appName), 'services');\n\n if (!fs.existsSync(directoryPath)) {\n return [];\n }\n\n return fs.readdirSync(directoryPath).filter((name) => fs.statSync(getServiceDirectory(appName, name)).isDirectory());\n};\n\n// True when a service contains the given child directory (e.g. 'service' for a\n// backend, 'views' for a frontend).\nexport const serviceHasSubdir = (appName: string, service: string, subdir: string): boolean => {\n const subdirPath = path.join(getServiceDirectory(appName, service), subdir);\n return fs.existsSync(subdirPath) && fs.statSync(subdirPath).isDirectory();\n};\n\n// Services that contain a given child directory.\nexport const getServiceNamesWithSubdir = (appName: string, subdir: string): string[] =>\n getAllServiceNames(appName).filter((service) => serviceHasSubdir(appName, service, subdir));\n\n// Backend services — those with a service/ subdir.\nexport const getServiceNames = (appName: string): string[] => getServiceNamesWithSubdir(appName, 'service');\n\n// Backend services that also ship a frontend (views/ subdir).\nexport const getServiceNamesWithViews = (appName: string): string[] =>\n getServiceNames(appName).filter((service) => serviceHasSubdir(appName, service, 'views'));\n"]}
1
+ {"version":3,"file":"discovery.js","sourceRoot":"","sources":["../../../src/lib/discovery.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,UAAU,EACV,WAAW,EACX,oBAAoB,EACpB,sBAAsB,EACtB,yBAAyB,IAAI,2BAA2B,EACxD,wBAAwB,IAAI,0BAA0B,EACtD,cAAc,GACf,MAAM,sBAAsB,CAAC;AAE9B,uEAAuE;AACvE,2FAA2F;AAC3F,MAAM,CAAC,MAAM,OAAO,GAAG,GAAW,EAAE,CAAC,OAAO,CAAC,GAAG,EAAE,CAAC;AAEnD,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAW,EAAE,CAAC,WAAW,CAAC,OAAO,EAAE,CAAC,CAAC;AACrE,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,OAAe,EAAU,EAAE,CAAC,UAAU,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC;AAC3F,MAAM,CAAC,MAAM,mBAAmB,GAAG,CAAC,OAAe,EAAE,OAAe,EAAU,EAAE,CAAC,cAAc,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;AAE7H,MAAM,CAAC,MAAM,gBAAgB,GAAG,GAAa,EAAE,CAAC,oBAAoB,CAAC,OAAO,EAAE,CAAC,CAAC;AAEhF,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,OAAe,EAAE,MAAc,EAAY,EAAE,CAAC,2BAA2B,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,CAAC;AAEhJ,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,OAAe,EAAY,EAAE,CAAC,sBAAsB,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC;AAEzG,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,OAAe,EAAY,EAAE,CAAC,0BAA0B,CAAC,OAAO,EAAE,EAAE,OAAO,CAAC,CAAC","sourcesContent":["import {\n getAppPath,\n getAppsPath,\n getAvailableAppNames,\n getBackendServiceNames,\n getServiceNamesWithSubdir as getServiceNamesWithSubdirIn,\n getServiceNamesWithViews as getServiceNamesWithViewsIn,\n getServicePath,\n} from '@qpqjs/deploy-rspack';\n\n// The consumer repo root, qpq always runs from the workspace root (npm\n// scripts set cwd there). Discovery itself is deploy-rspack's; these bind it to that root.\nexport const getRoot = (): string => process.cwd();\n\nexport const getAppsDirectory = (): string => getAppsPath(getRoot());\nexport const getAppDirectory = (appName: string): string => getAppPath(getRoot(), appName);\nexport const getServiceDirectory = (appName: string, service: string): string => getServicePath(getRoot(), appName, service);\n\nexport const getAvailableApps = (): string[] => getAvailableAppNames(getRoot());\n\nexport const getServiceNamesWithSubdir = (appName: string, subdir: string): string[] => getServiceNamesWithSubdirIn(getRoot(), appName, subdir);\n\nexport const getServiceNames = (appName: string): string[] => getBackendServiceNames(getRoot(), appName);\n\nexport const getServiceNamesWithViews = (appName: string): string[] => getServiceNamesWithViewsIn(getRoot(), appName);\n"]}
@@ -1,8 +1,10 @@
1
1
  import fs from 'fs';
2
2
  import path from 'path';
3
+ import { createOutput } from './createOutput';
3
4
  import { getAppDirectory, getAppsDirectory, getAvailableApps, getRoot } from './discovery';
4
5
  import { nextPrefixColor, runCommand, runCommandPrefixed } from './runCommand';
5
6
  import { runDependencyTasks } from './runTasks';
7
+ const out = createOutput('hooks');
6
8
  // App lifecycle hooks: apps/<app>/package.json scripts named `qpq:<hook>`
7
9
  // (e.g. qpq:postinstall, qpq:prebuild). The CLI invokes them when present;
8
10
  // apps without the hook are skipped silently.
@@ -16,7 +18,7 @@ export const runAppHook = async (appName, hook) => {
16
18
  if (!packageJson.scripts?.[scriptName]) {
17
19
  return false;
18
20
  }
19
- console.log(`Running ${scriptName} for [${appName}]`);
21
+ out.log(`Running ${scriptName} for [${appName}]`);
20
22
  await runCommand('npm', ['run', scriptName], { cwd: getAppDirectory(appName) });
21
23
  return true;
22
24
  };
@@ -129,7 +131,7 @@ const runHookForPackages = async (candidates, hook, jobs) => {
129
131
  .map((candidate) => ({ ...candidate, scriptName: resolveHookScript(candidate.pkg, hook) }))
130
132
  .filter((candidate) => candidate.scriptName !== undefined);
131
133
  if (hookPackages.length === 0) {
132
- console.log(`No app or workspace package defines a "qpq:${hook}" or "${hook}" script, nothing to run.`);
134
+ out.log(`No app or workspace package defines a "qpq:${hook}" or "${hook}" script, nothing to run.`);
133
135
  return [];
134
136
  }
135
137
  // Dependency edges only count between packages that are part of this run:
@@ -141,7 +143,7 @@ const runHookForPackages = async (candidates, hook, jobs) => {
141
143
  deps: getDeclaredDependencyNames(hookPackage.pkg).filter((name) => runSetNames.has(name) && name !== hookPackage.pkg.name),
142
144
  run: () => runCommandPrefixed(hookPackage.label, 'npm', ['run', hookPackage.scriptName], { cwd: hookPackage.dir, color: nextPrefixColor() }),
143
145
  }));
144
- console.log(`Running ${hook} hooks in ${tasks.length} package(s) (${jobs} jobs, dependency ordered)`);
146
+ out.log(`Running ${hook} hooks in ${tasks.length} package(s) (${jobs} jobs, dependency ordered)`);
145
147
  return runDependencyTasks(tasks, jobs);
146
148
  };
147
149
  /** Runs a hook in every app and workspace package that defines it. Returns the failures; callers decide fatality. */
@@ -1 +1 @@
1
- {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../../src/lib/hooks.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from 'fs';\nimport path from 'path';\n\nimport { getAppDirectory, getAppsDirectory, getAvailableApps, getRoot } from './discovery';\nimport { nextPrefixColor, runCommand, runCommandPrefixed } from './runCommand';\nimport { runDependencyTasks, TaskFailure } from './runTasks';\n\ninterface HookPackageJson {\n name?: string;\n scripts?: Record<string, string>;\n workspaces?: string[];\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n peerDependencies?: Record<string, string>;\n}\n\n// A directory whose package.json can carry qpq:<hook> scripts: either an app\n// (apps/<app>/package.json, which need not be an npm workspace) or any package\n// matched by the consumer root's `workspaces` globs.\ninterface HookPackage {\n dir: string;\n label: string; // app name or package name, for display\n pkg: HookPackageJson;\n}\n\n// App lifecycle hooks: apps/<app>/package.json scripts named `qpq:<hook>`\n// (e.g. qpq:postinstall, qpq:prebuild). The CLI invokes them when present;\n// apps without the hook are skipped silently.\nexport const runAppHook = async (appName: string, hook: string): Promise<boolean> => {\n const packageJsonPath = path.join(getAppDirectory(appName), 'package.json');\n if (!fs.existsSync(packageJsonPath)) {\n return false;\n }\n\n const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));\n const scriptName = `qpq:${hook}`;\n if (!packageJson.scripts?.[scriptName]) {\n return false;\n }\n\n console.log(`Running ${scriptName} for [${appName}]`);\n await runCommand('npm', ['run', scriptName], { cwd: getAppDirectory(appName) });\n return true;\n};\n\nconst isDirectory = (candidate: string): boolean => fs.existsSync(candidate) && fs.statSync(candidate).isDirectory();\n\nconst listSubdirectories = (dir: string): string[] => {\n if (!isDirectory(dir)) {\n return [];\n }\n return fs\n .readdirSync(dir)\n .map((name) => path.join(dir, name))\n .filter(isDirectory);\n};\n\n// Expand one `workspaces` glob. Only literal segments and bare `*` are\n// supported, which covers the patterns qpq repos use (e.g.\n// \"apps/*/services/*/models\"); anything fancier (\"**\", braces) matches nothing.\nconst expandWorkspacePattern = (root: string, pattern: string): string[] => {\n let dirs = [root];\n for (const segment of pattern.split('/')) {\n if (segment === '*') {\n dirs = dirs.flatMap(listSubdirectories);\n } else {\n dirs = dirs.map((dir) => path.join(dir, segment)).filter(isDirectory);\n }\n }\n return dirs;\n};\n\nconst readPackageJson = (dir: string): HookPackageJson | undefined => {\n const packageJsonPath = path.join(dir, 'package.json');\n if (!fs.existsSync(packageJsonPath)) {\n return undefined;\n }\n return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as HookPackageJson;\n};\n\n// Every directory that can carry hooks: app dirs plus all workspace packages,\n// deduped (an app dir could itself be a workspace).\nconst getHookPackages = (): HookPackage[] => {\n const root = getRoot();\n const rootPkg = readPackageJson(root);\n\n const appDirs = getAvailableApps().map((appName) => ({ dir: getAppDirectory(appName), fallbackLabel: appName }));\n const workspaceDirs = (rootPkg?.workspaces ?? [])\n .flatMap((pattern) => expandWorkspacePattern(root, pattern))\n .map((dir) => ({ dir, fallbackLabel: path.relative(root, dir) }));\n\n const packages: HookPackage[] = [];\n const seen = new Set<string>();\n for (const { dir, fallbackLabel } of [...appDirs, ...workspaceDirs]) {\n const resolved = path.resolve(dir);\n if (seen.has(resolved)) {\n continue;\n }\n seen.add(resolved);\n\n const pkg = readPackageJson(dir);\n if (pkg) {\n packages.push({ dir, label: pkg.name ?? fallbackLabel, pkg });\n }\n }\n\n return packages;\n};\n\n// The script a hook resolves to in one package: `qpq:<hook>` when defined,\n// otherwise the plain `<hook>` script. Empty-string scripts count as absent.\nconst resolveHookScript = (pkg: HookPackageJson, hook: string): string | undefined => {\n if (pkg.scripts?.[`qpq:${hook}`]) {\n return `qpq:${hook}`;\n }\n if (pkg.scripts?.[hook]) {\n return hook;\n }\n return undefined;\n};\n\nconst getDeclaredDependencyNames = (pkg: HookPackageJson): string[] =>\n Object.keys({ ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies });\n\n// True for a package inside apps/<some other app>/. Packages outside apps/\n// are shared, so they never count as another app's.\nconst isOtherAppPackage = (dir: string, appName: string): boolean => {\n const relative = path.relative(getAppsDirectory(), path.resolve(dir));\n if (relative === '' || relative.startsWith('..') || path.isAbsolute(relative)) {\n return false;\n }\n return relative.split(path.sep)[0] !== appName;\n};\n\n// The app's own packages and the shared ones, plus every package they depend\n// on wherever it lives, so a cross-app dependency still gets built.\nconst getAppHookPackages = (appName: string): HookPackage[] => {\n const allPackages = getHookPackages();\n const byName = new Map<string, HookPackage>();\n for (const hookPackage of allPackages) {\n if (hookPackage.pkg.name) {\n byName.set(hookPackage.pkg.name, hookPackage);\n }\n }\n\n const selected = new Set(allPackages.filter((hookPackage) => !isOtherAppPackage(hookPackage.dir, appName)));\n const pending = [...selected];\n for (let current = pending.pop(); current; current = pending.pop()) {\n for (const depName of getDeclaredDependencyNames(current.pkg)) {\n const dep = byName.get(depName);\n if (dep && !selected.has(dep)) {\n selected.add(dep);\n pending.push(dep);\n }\n }\n }\n\n return allPackages.filter((hookPackage) => selected.has(hookPackage));\n};\n\n// Run the hook's script in each of `candidates` that defines it (qpq:<hook>\n// preferred over a plain <hook> script), in dependency order (declared deps\n// resolved by package name), with up to `jobs` running concurrently. Packages\n// whose dependencies aren't part of this run don't wait on them.\nconst runHookForPackages = async (candidates: HookPackage[], hook: string, jobs: number): Promise<TaskFailure[]> => {\n const hookPackages = candidates\n .map((candidate) => ({ ...candidate, scriptName: resolveHookScript(candidate.pkg, hook) }))\n .filter((candidate): candidate is HookPackage & { scriptName: string } => candidate.scriptName !== undefined);\n\n if (hookPackages.length === 0) {\n console.log(`No app or workspace package defines a \"qpq:${hook}\" or \"${hook}\" script, nothing to run.`);\n return [];\n }\n\n // Dependency edges only count between packages that are part of this run:\n // a dep that doesn't define the hook has nothing to wait for.\n const runSetNames = new Set(hookPackages.map((hookPackage) => hookPackage.pkg.name).filter(Boolean));\n\n const tasks = hookPackages.map((hookPackage) => ({\n id: hookPackage.pkg.name ?? hookPackage.dir,\n label: hookPackage.label,\n deps: getDeclaredDependencyNames(hookPackage.pkg).filter((name) => runSetNames.has(name) && name !== hookPackage.pkg.name),\n run: () => runCommandPrefixed(hookPackage.label, 'npm', ['run', hookPackage.scriptName], { cwd: hookPackage.dir, color: nextPrefixColor() }),\n }));\n\n console.log(`Running ${hook} hooks in ${tasks.length} package(s) (${jobs} jobs, dependency ordered)`);\n return runDependencyTasks(tasks, jobs);\n};\n\n/** Runs a hook in every app and workspace package that defines it. Returns the failures; callers decide fatality. */\nexport const runHookForAllPackages = (hook: string, jobs: number): Promise<TaskFailure[]> => runHookForPackages(getHookPackages(), hook, jobs);\n\n/**\n * Runs a hook in one app's packages, the shared packages outside apps/, and anything those depend on.\n * Other apps' packages are skipped. Returns the failures; callers decide fatality.\n */\nexport const runHookForAppPackages = (hook: string, appName: string, jobs: number): Promise<TaskFailure[]> =>\n runHookForPackages(getAppHookPackages(appName), hook, jobs);\n"]}
1
+ {"version":3,"file":"hooks.js","sourceRoot":"","sources":["../../../src/lib/hooks.ts"],"names":[],"mappings":"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","sourcesContent":["import fs from 'fs';\nimport path from 'path';\n\nimport { createOutput } from './createOutput';\nimport { getAppDirectory, getAppsDirectory, getAvailableApps, getRoot } from './discovery';\nimport { nextPrefixColor, runCommand, runCommandPrefixed } from './runCommand';\nimport { runDependencyTasks, TaskFailure } from './runTasks';\n\nconst out = createOutput('hooks');\n\ntype HookPackageJson = {\n name?: string;\n scripts?: Record<string, string>;\n workspaces?: string[];\n dependencies?: Record<string, string>;\n devDependencies?: Record<string, string>;\n peerDependencies?: Record<string, string>;\n};\n\n// A directory whose package.json can carry qpq:<hook> scripts: either an app\n// (apps/<app>/package.json, which need not be an npm workspace) or any package\n// matched by the consumer root's `workspaces` globs.\ntype HookPackage = {\n dir: string;\n label: string; // app name or package name, for display\n pkg: HookPackageJson;\n};\n\n// App lifecycle hooks: apps/<app>/package.json scripts named `qpq:<hook>`\n// (e.g. qpq:postinstall, qpq:prebuild). The CLI invokes them when present;\n// apps without the hook are skipped silently.\nexport const runAppHook = async (appName: string, hook: string): Promise<boolean> => {\n const packageJsonPath = path.join(getAppDirectory(appName), 'package.json');\n if (!fs.existsSync(packageJsonPath)) {\n return false;\n }\n\n const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));\n const scriptName = `qpq:${hook}`;\n if (!packageJson.scripts?.[scriptName]) {\n return false;\n }\n\n out.log(`Running ${scriptName} for [${appName}]`);\n await runCommand('npm', ['run', scriptName], { cwd: getAppDirectory(appName) });\n return true;\n};\n\nconst isDirectory = (candidate: string): boolean => fs.existsSync(candidate) && fs.statSync(candidate).isDirectory();\n\nconst listSubdirectories = (dir: string): string[] => {\n if (!isDirectory(dir)) {\n return [];\n }\n return fs\n .readdirSync(dir)\n .map((name) => path.join(dir, name))\n .filter(isDirectory);\n};\n\n// Expand one `workspaces` glob. Only literal segments and bare `*` are\n// supported, which covers the patterns qpq repos use (e.g.\n// \"apps/*/services/*/models\"); anything fancier (\"**\", braces) matches nothing.\nconst expandWorkspacePattern = (root: string, pattern: string): string[] => {\n let dirs = [root];\n for (const segment of pattern.split('/')) {\n if (segment === '*') {\n dirs = dirs.flatMap(listSubdirectories);\n } else {\n dirs = dirs.map((dir) => path.join(dir, segment)).filter(isDirectory);\n }\n }\n return dirs;\n};\n\nconst readPackageJson = (dir: string): HookPackageJson | undefined => {\n const packageJsonPath = path.join(dir, 'package.json');\n if (!fs.existsSync(packageJsonPath)) {\n return undefined;\n }\n return JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')) as HookPackageJson;\n};\n\n// Every directory that can carry hooks: app dirs plus all workspace packages,\n// deduped (an app dir could itself be a workspace).\nconst getHookPackages = (): HookPackage[] => {\n const root = getRoot();\n const rootPkg = readPackageJson(root);\n\n const appDirs = getAvailableApps().map((appName) => ({ dir: getAppDirectory(appName), fallbackLabel: appName }));\n const workspaceDirs = (rootPkg?.workspaces ?? [])\n .flatMap((pattern) => expandWorkspacePattern(root, pattern))\n .map((dir) => ({ dir, fallbackLabel: path.relative(root, dir) }));\n\n const packages: HookPackage[] = [];\n const seen = new Set<string>();\n for (const { dir, fallbackLabel } of [...appDirs, ...workspaceDirs]) {\n const resolved = path.resolve(dir);\n if (seen.has(resolved)) {\n continue;\n }\n seen.add(resolved);\n\n const pkg = readPackageJson(dir);\n if (pkg) {\n packages.push({ dir, label: pkg.name ?? fallbackLabel, pkg });\n }\n }\n\n return packages;\n};\n\n// The script a hook resolves to in one package: `qpq:<hook>` when defined,\n// otherwise the plain `<hook>` script. Empty-string scripts count as absent.\nconst resolveHookScript = (pkg: HookPackageJson, hook: string): string | undefined => {\n if (pkg.scripts?.[`qpq:${hook}`]) {\n return `qpq:${hook}`;\n }\n if (pkg.scripts?.[hook]) {\n return hook;\n }\n return undefined;\n};\n\nconst getDeclaredDependencyNames = (pkg: HookPackageJson): string[] =>\n Object.keys({ ...pkg.dependencies, ...pkg.devDependencies, ...pkg.peerDependencies });\n\n// True for a package inside apps/<some other app>/. Packages outside apps/\n// are shared, so they never count as another app's.\nconst isOtherAppPackage = (dir: string, appName: string): boolean => {\n const relative = path.relative(getAppsDirectory(), path.resolve(dir));\n if (relative === '' || relative.startsWith('..') || path.isAbsolute(relative)) {\n return false;\n }\n return relative.split(path.sep)[0] !== appName;\n};\n\n// The app's own packages and the shared ones, plus every package they depend\n// on wherever it lives, so a cross-app dependency still gets built.\nconst getAppHookPackages = (appName: string): HookPackage[] => {\n const allPackages = getHookPackages();\n const byName = new Map<string, HookPackage>();\n for (const hookPackage of allPackages) {\n if (hookPackage.pkg.name) {\n byName.set(hookPackage.pkg.name, hookPackage);\n }\n }\n\n const selected = new Set(allPackages.filter((hookPackage) => !isOtherAppPackage(hookPackage.dir, appName)));\n const pending = [...selected];\n for (let current = pending.pop(); current; current = pending.pop()) {\n for (const depName of getDeclaredDependencyNames(current.pkg)) {\n const dep = byName.get(depName);\n if (dep && !selected.has(dep)) {\n selected.add(dep);\n pending.push(dep);\n }\n }\n }\n\n return allPackages.filter((hookPackage) => selected.has(hookPackage));\n};\n\n// Run the hook's script in each of `candidates` that defines it (qpq:<hook>\n// preferred over a plain <hook> script), in dependency order (declared deps\n// resolved by package name), with up to `jobs` running concurrently. Packages\n// whose dependencies aren't part of this run don't wait on them.\nconst runHookForPackages = async (candidates: HookPackage[], hook: string, jobs: number): Promise<TaskFailure[]> => {\n const hookPackages = candidates\n .map((candidate) => ({ ...candidate, scriptName: resolveHookScript(candidate.pkg, hook) }))\n .filter((candidate): candidate is HookPackage & { scriptName: string } => candidate.scriptName !== undefined);\n\n if (hookPackages.length === 0) {\n out.log(`No app or workspace package defines a \"qpq:${hook}\" or \"${hook}\" script, nothing to run.`);\n return [];\n }\n\n // Dependency edges only count between packages that are part of this run:\n // a dep that doesn't define the hook has nothing to wait for.\n const runSetNames = new Set(hookPackages.map((hookPackage) => hookPackage.pkg.name).filter(Boolean));\n\n const tasks = hookPackages.map((hookPackage) => ({\n id: hookPackage.pkg.name ?? hookPackage.dir,\n label: hookPackage.label,\n deps: getDeclaredDependencyNames(hookPackage.pkg).filter((name) => runSetNames.has(name) && name !== hookPackage.pkg.name),\n run: () => runCommandPrefixed(hookPackage.label, 'npm', ['run', hookPackage.scriptName], { cwd: hookPackage.dir, color: nextPrefixColor() }),\n }));\n\n out.log(`Running ${hook} hooks in ${tasks.length} package(s) (${jobs} jobs, dependency ordered)`);\n return runDependencyTasks(tasks, jobs);\n};\n\n/** Runs a hook in every app and workspace package that defines it. Returns the failures; callers decide fatality. */\nexport const runHookForAllPackages = (hook: string, jobs: number): Promise<TaskFailure[]> => runHookForPackages(getHookPackages(), hook, jobs);\n\n/**\n * Runs a hook in one app's packages, the shared packages outside apps/, and anything those depend on.\n * Other apps' packages are skipped. Returns the failures; callers decide fatality.\n */\nexport const runHookForAppPackages = (hook: string, appName: string, jobs: number): Promise<TaskFailure[]> =>\n runHookForPackages(getAppHookPackages(appName), hook, jobs);\n"]}
@@ -1,3 +1,5 @@
1
+ import { createOutput } from './createOutput';
2
+ const out = createOutput('kill-child-with-escalation');
1
3
  // Above the dev server's 5s worst-case shutdown budget, below docker's 10s
2
4
  // default stop grace, so a container stop still exits cleanly rather than
3
5
  // being SIGKILLed by docker mid-escalation.
@@ -16,7 +18,7 @@ export const killChildWithEscalation = (child, graceMs = DEFAULT_KILL_GRACE_MS)
16
18
  }
17
19
  return new Promise((resolve) => {
18
20
  const timer = setTimeout(() => {
19
- console.warn(`Dev server did not exit within ${graceMs}ms — sending SIGKILL.`);
21
+ out.warn(`Dev server did not exit within ${graceMs}ms — sending SIGKILL.`);
20
22
  child.kill('SIGKILL');
21
23
  }, graceMs);
22
24
  child.once('exit', () => {
@@ -1 +1 @@
1
- {"version":3,"file":"killChildWithEscalation.js","sourceRoot":"","sources":["../../../src/lib/killChildWithEscalation.ts"],"names":[],"mappings":"AAWA,2EAA2E;AAC3E,0EAA0E;AAC1E,4CAA4C;AAC5C,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAEnC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,KAAmB,EAAE,OAAO,GAAG,qBAAqB,EAAiB,EAAE;IAC7G,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;QACzD,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;IAED,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QACnC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,OAAO,CAAC,IAAI,CAAC,kCAAkC,OAAO,uBAAuB,CAAC,CAAC;YAC/E,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACxB,CAAC,EAAE,OAAO,CAAC,CAAC;QAEZ,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE;YACtB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACxB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC","sourcesContent":["// Stopping the dev server child, with a hard backstop.\n//\n// The dev server now runs an ordered shutdown on SIGTERM (drain in-flight\n// work, then checkpoint the stores) instead of dying instantly. That is the\n// point - it is what stops a save-triggered restart eating a queue message\n// mid-story - but it means a bare `child.kill()` followed by waiting for exit\n// can now wait forever if a teardown wedges. Without escalation this would\n// trade a data-loss bug for a hung-restart bug, and the next launch would find\n// its ports still held.\nimport { ChildProcess } from 'child_process';\n\n// Above the dev server's 5s worst-case shutdown budget, below docker's 10s\n// default stop grace, so a container stop still exits cleanly rather than\n// being SIGKILLed by docker mid-escalation.\nconst DEFAULT_KILL_GRACE_MS = 6000;\n\n/**\n * SIGTERM the child, resolve once it has actually exited, and SIGKILL it if it\n * has not gone within the grace window.\n *\n * Resolves rather than rejects on escalation: the caller's next move is the\n * same either way (start the replacement), and a rejection would only give\n * every call site a catch to write.\n */\nexport const killChildWithEscalation = (child: ChildProcess, graceMs = DEFAULT_KILL_GRACE_MS): Promise<void> => {\n if (child.exitCode !== null || child.signalCode !== null) {\n return Promise.resolve();\n }\n\n return new Promise<void>((resolve) => {\n const timer = setTimeout(() => {\n console.warn(`Dev server did not exit within ${graceMs}ms — sending SIGKILL.`);\n child.kill('SIGKILL');\n }, graceMs);\n\n child.once('exit', () => {\n clearTimeout(timer);\n resolve();\n });\n\n child.kill('SIGTERM');\n });\n};\n"]}
1
+ {"version":3,"file":"killChildWithEscalation.js","sourceRoot":"","sources":["../../../src/lib/killChildWithEscalation.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,MAAM,GAAG,GAAG,YAAY,CAAC,4BAA4B,CAAC,CAAC;AAEvD,2EAA2E;AAC3E,0EAA0E;AAC1E,4CAA4C;AAC5C,MAAM,qBAAqB,GAAG,IAAI,CAAC;AAEnC;;;;;;;GAOG;AACH,MAAM,CAAC,MAAM,uBAAuB,GAAG,CAAC,KAAmB,EAAE,OAAO,GAAG,qBAAqB,EAAiB,EAAE;IAC7G,IAAI,KAAK,CAAC,QAAQ,KAAK,IAAI,IAAI,KAAK,CAAC,UAAU,KAAK,IAAI,EAAE,CAAC;QACzD,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;IAED,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE;QACnC,MAAM,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;YAC5B,GAAG,CAAC,IAAI,CAAC,kCAAkC,OAAO,uBAAuB,CAAC,CAAC;YAC3E,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;QACxB,CAAC,EAAE,OAAO,CAAC,CAAC;QAEZ,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,EAAE;YACtB,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,OAAO,EAAE,CAAC;QACZ,CAAC,CAAC,CAAC;QAEH,KAAK,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IACxB,CAAC,CAAC,CAAC;AACL,CAAC,CAAC","sourcesContent":["// Stopping the dev server child, with a hard backstop.\n//\n// The dev server now runs an ordered shutdown on SIGTERM (drain in-flight\n// work, then checkpoint the stores) instead of dying instantly. That is the\n// point - it is what stops a save-triggered restart eating a queue message\n// mid-story - but it means a bare `child.kill()` followed by waiting for exit\n// can now wait forever if a teardown wedges. Without escalation this would\n// trade a data-loss bug for a hung-restart bug, and the next launch would find\n// its ports still held.\nimport { ChildProcess } from 'child_process';\n\nimport { createOutput } from './createOutput';\n\nconst out = createOutput('kill-child-with-escalation');\n\n// Above the dev server's 5s worst-case shutdown budget, below docker's 10s\n// default stop grace, so a container stop still exits cleanly rather than\n// being SIGKILLed by docker mid-escalation.\nconst DEFAULT_KILL_GRACE_MS = 6000;\n\n/**\n * SIGTERM the child, resolve once it has actually exited, and SIGKILL it if it\n * has not gone within the grace window.\n *\n * Resolves rather than rejects on escalation: the caller's next move is the\n * same either way (start the replacement), and a rejection would only give\n * every call site a catch to write.\n */\nexport const killChildWithEscalation = (child: ChildProcess, graceMs = DEFAULT_KILL_GRACE_MS): Promise<void> => {\n if (child.exitCode !== null || child.signalCode !== null) {\n return Promise.resolve();\n }\n\n return new Promise<void>((resolve) => {\n const timer = setTimeout(() => {\n out.warn(`Dev server did not exit within ${graceMs}ms — sending SIGKILL.`);\n child.kill('SIGKILL');\n }, graceMs);\n\n child.once('exit', () => {\n clearTimeout(timer);\n resolve();\n });\n\n child.kill('SIGTERM');\n });\n};\n"]}
@@ -1,12 +1,8 @@
1
- // Pre-start sweep shared by the dev-server commands: a previous run whose
2
- // wrapper died without killing its listener (closed terminal, SIGKILL) leaves
3
- // an orphaned process holding a dev port, and the next launch dies with
4
- // EADDRINUSE. Before starting, kill any listener on the given ports whose
5
- // command line matches `isOurs`. Anything else on the ports is left alone
6
- // and reported — it's not ours.
7
- import { execSync } from 'child_process';
8
1
  import fs from 'fs';
9
2
  import path from 'path';
3
+ import { createOutput } from './createOutput';
4
+ import { runCommandCaptureSync, sleepSync } from './runCommand';
5
+ const out = createOutput('kill-stale-listeners');
10
6
  // Matches any invocation of the qpq CLI itself, regardless of how it was
11
7
  // launched: `node node_modules/.bin/qpq ...` (the npm-script shim, which has
12
8
  // no .js extension), a direct `node .../lib/commonjs/bin/qpq.js` call, a
@@ -21,22 +17,21 @@ export const isQpqCliCommand = (command) => command
21
17
  const withoutExt = base.endsWith('.js') ? base.slice(0, -3) : base;
22
18
  return withoutExt === 'qpq';
23
19
  });
24
- // SIGTERM only requests termination — the caller (about to bind this same
20
+ // SIGTERM only requests termination, the caller (about to bind this same
25
21
  // process's ports itself) needs it actually gone first. A stale dev server
26
22
  // can be mid-startup when killed (still resolving config, not yet bound to
27
23
  // every port it eventually binds) and keep running for a bit past the
28
- // signal, going on to bind a LATER port — waiting for just the one port that
24
+ // signal, going on to bind a LATER port, waiting for just the one port that
29
25
  // matched isn't enough; wait for the whole pid to disappear so it can't bind
30
26
  // anything else afterward. Poll synchronously (this whole sweep runs before
31
- // anything async starts) via `kill(pid, 0)` — reliable and fast here because
32
- // these are unrelated, previously-orphaned processes, not children of this
27
+ // anything async starts) via `kill(pid, 0)`, reliable and fast here because
28
+ // these are unrelated processes orphaned earlier, not children of this
33
29
  // one (a real child can sit as a zombie answering `kill(pid, 0)` until this
34
30
  // process's own event loop reaps it, which a tight sync loop would starve;
35
31
  // doesn't apply to a process we didn't spawn).
36
32
  //
37
33
  // The default covers the dev server's graceful shutdown (drain in-flight work,
38
- // then checkpoint the stores) with room to spare — it used to be 3s, which was
39
- // fine when the server died instantly and is not now. Returns whether the pid
34
+ // then checkpoint the stores) with room to spare. Returns whether the pid
40
35
  // actually went, because a caller about to bind these ports has to escalate
41
36
  // rather than hope: giving up while the old server still holds 8080 is exactly
42
37
  // the EADDRINUSE this file exists to prevent.
@@ -49,13 +44,13 @@ const waitForPidGone = (pid, timeoutMs = 8000) => {
49
44
  catch {
50
45
  return true; // throws once the pid no longer exists
51
46
  }
52
- execSync('sleep 0.05');
47
+ sleepSync(50);
53
48
  }
54
49
  return false;
55
50
  };
56
51
  // Last resort for a process that ignored, or wedged during, its graceful stop.
57
52
  const killPidHard = (pid) => {
58
- console.warn(`Process ${pid} did not exit gracefully — sending SIGKILL.`);
53
+ out.warn(`Process ${pid} did not exit gracefully — sending SIGKILL.`);
59
54
  try {
60
55
  process.kill(pid, 'SIGKILL');
61
56
  }
@@ -70,8 +65,7 @@ const killPidHard = (pid) => {
70
65
  // itself a symlink (a symlinked home directory, an NFS mount, etc).
71
66
  const getProcessCwd = (pid) => {
72
67
  try {
73
- const line = execSync(`lsof -a -p ${pid} -d cwd -Fn`, { stdio: ['ignore', 'pipe', 'ignore'] })
74
- .toString()
68
+ const line = runCommandCaptureSync('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'])
75
69
  .split('\n')
76
70
  .find((l) => l.startsWith('n'));
77
71
  return line ? fs.realpathSync(line.slice(1)) : null;
@@ -84,7 +78,7 @@ const getProcessCwd = (pid) => {
84
78
  // `npx qpq go:dev:api` (or via any shell whose -c string names qpq), the
85
79
  // ancestor chain (`sh -c "npx ... qpq ..."`, `npm exec qpq ...`) matches
86
80
  // isQpqCliCommand and shares our cwd, so excluding just process.pid isn't
87
- // enough — SIGINTing an ancestor tears down our own tree. Walk ppid to the
81
+ // enough, SIGINTing an ancestor tears down our own tree. Walk ppid to the
88
82
  // top and exclude the whole chain.
89
83
  const getOwnAncestorPids = () => {
90
84
  const ancestors = new Set();
@@ -92,9 +86,7 @@ const getOwnAncestorPids = () => {
92
86
  while (pid > 1 && !ancestors.has(pid)) {
93
87
  ancestors.add(pid);
94
88
  try {
95
- pid = Number(execSync(`ps -o ppid= -p ${pid}`, { stdio: ['ignore', 'pipe', 'ignore'] })
96
- .toString()
97
- .trim());
89
+ pid = Number(runCommandCaptureSync('ps', ['-o', 'ppid=', '-p', String(pid)]).trim());
98
90
  }
99
91
  catch {
100
92
  break;
@@ -103,11 +95,11 @@ const getOwnAncestorPids = () => {
103
95
  return ancestors;
104
96
  };
105
97
  // killStaleListeners only sees processes currently BOUND to one of the given
106
- // ports — it can't catch a still-running `qpq go:dev*` watcher whose spawned
98
+ // ports, it can't catch a still-running `qpq go:dev*` watcher whose spawned
107
99
  // dev-server child already died (a crash, or us killing just the child):
108
100
  // the watcher itself holds no port at that moment, so it's invisible to a
109
- // port scan, yet it's still alive and can spawn a fresh child — and bind a
110
- // port — moments later, racing whatever we're about to start. Close that gap
101
+ // port scan, yet it's still alive and can spawn a fresh child, and bind a
102
+ // port, moments later, racing whatever we're about to start. Close that gap
111
103
  // by finding any OTHER qpq CLI process rooted in the same repo checkout
112
104
  // (matched by cwd, not by port or command-line args, which vary by how it
113
105
  // was invoked) and killing it outright, regardless of what it currently has
@@ -118,9 +110,7 @@ export const killOtherQpqDevProcesses = (root) => {
118
110
  const ownPids = getOwnAncestorPids();
119
111
  let lines = [];
120
112
  try {
121
- lines = execSync('ps -axo pid=,command=', { stdio: ['ignore', 'pipe', 'ignore'] })
122
- .toString()
123
- .split('\n');
113
+ lines = runCommandCaptureSync('ps', ['-axo', 'pid=,command=']).split('\n');
124
114
  }
125
115
  catch {
126
116
  return;
@@ -138,7 +128,7 @@ export const killOtherQpqDevProcesses = (root) => {
138
128
  continue;
139
129
  if (getProcessCwd(pid) !== resolvedRoot)
140
130
  continue;
141
- console.log(`Killing other qpq dev process for this repo (pid ${pid}): ${command}`);
131
+ out.log(`Killing other qpq dev process for this repo (pid ${pid}): ${command}`);
142
132
  process.kill(pid, 'SIGINT');
143
133
  if (!waitForPidGone(pid)) {
144
134
  killPidHard(pid);
@@ -150,10 +140,7 @@ export const killOtherQpqDevProcesses = (root) => {
150
140
  const getPortListeners = (port) => {
151
141
  let pids = [];
152
142
  try {
153
- pids = execSync(`lsof -t -iTCP:${port} -sTCP:LISTEN`, {
154
- stdio: ['ignore', 'pipe', 'ignore'],
155
- })
156
- .toString()
143
+ pids = runCommandCaptureSync('lsof', ['-t', `-iTCP:${port}`, '-sTCP:LISTEN'])
157
144
  .split('\n')
158
145
  .map((line) => Number(line.trim()))
159
146
  .filter(Boolean);
@@ -163,7 +150,7 @@ const getPortListeners = (port) => {
163
150
  }
164
151
  return pids.flatMap((pid) => {
165
152
  try {
166
- return [{ pid, command: execSync(`ps -p ${pid} -o command=`).toString().trim() }];
153
+ return [{ pid, command: runCommandCaptureSync('ps', ['-p', String(pid), '-o', 'command=']).trim() }];
167
154
  }
168
155
  catch {
169
156
  return []; // already gone
@@ -186,8 +173,8 @@ const getPortListeners = (port) => {
186
173
  export const reportPortHolders = (ports) => {
187
174
  for (const port of ports) {
188
175
  for (const { pid, command } of getPortListeners(port)) {
189
- console.warn(`Port ${port} is currently held by pid ${pid}: ${command}`);
190
- console.warn(` Starting anyway. If it still holds port ${port} once this server binds, startup will fail with EADDRINUSE.`);
176
+ out.warn(`Port ${port} is currently held by pid ${pid}: ${command}`);
177
+ out.warn(` Starting anyway. If it still holds port ${port} once this server binds, startup will fail with EADDRINUSE.`);
191
178
  }
192
179
  }
193
180
  };
@@ -195,14 +182,14 @@ export const killStaleListeners = (ports, isOurs) => {
195
182
  for (const port of ports) {
196
183
  for (const { pid, command } of getPortListeners(port)) {
197
184
  if (isOurs(command)) {
198
- console.log(`Killing stale dev server on port ${port} (pid ${pid})`);
185
+ out.log(`Killing stale dev server on port ${port} (pid ${pid})`);
199
186
  process.kill(pid);
200
187
  if (!waitForPidGone(pid)) {
201
188
  killPidHard(pid);
202
189
  }
203
190
  }
204
191
  else {
205
- console.warn(`Port ${port} is in use by an unrelated process (pid ${pid}: ${command}) — not killing it.`);
192
+ out.warn(`Port ${port} is in use by an unrelated process (pid ${pid}: ${command}) — not killing it.`);
206
193
  }
207
194
  }
208
195
  }
@@ -1 +1 @@
1
- {"version":3,"file":"killStaleListeners.js","sourceRoot":"","sources":["../../../src/lib/killStaleListeners.ts"],"names":[],"mappings":"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","sourcesContent":["// Pre-start sweep shared by the dev-server commands: a previous run whose\n// wrapper died without killing its listener (closed terminal, SIGKILL) leaves\n// an orphaned process holding a dev port, and the next launch dies with\n// EADDRINUSE. Before starting, kill any listener on the given ports whose\n// command line matches `isOurs`. Anything else on the ports is left alone\n// and reported — it's not ours.\nimport { execSync } from 'child_process';\nimport fs from 'fs';\nimport path from 'path';\n\n// Matches any invocation of the qpq CLI itself, regardless of how it was\n// launched: `node node_modules/.bin/qpq ...` (the npm-script shim, which has\n// no .js extension), a direct `node .../lib/commonjs/bin/qpq.js` call, a\n// global `qpq` shebang script, etc. Checks each whitespace-separated token's\n// basename (extension stripped) rather than a fixed substring like\n// 'bin/qpq.js', which only matches the last of those forms.\nexport const isQpqCliCommand = (command: string): boolean =>\n command\n .trim()\n .split(/\\s+/)\n .some((token) => {\n const base = path.basename(token);\n const withoutExt = base.endsWith('.js') ? base.slice(0, -3) : base;\n return withoutExt === 'qpq';\n });\n\n// SIGTERM only requests termination — the caller (about to bind this same\n// process's ports itself) needs it actually gone first. A stale dev server\n// can be mid-startup when killed (still resolving config, not yet bound to\n// every port it eventually binds) and keep running for a bit past the\n// signal, going on to bind a LATER port — waiting for just the one port that\n// matched isn't enough; wait for the whole pid to disappear so it can't bind\n// anything else afterward. Poll synchronously (this whole sweep runs before\n// anything async starts) via `kill(pid, 0)` — reliable and fast here because\n// these are unrelated, previously-orphaned processes, not children of this\n// one (a real child can sit as a zombie answering `kill(pid, 0)` until this\n// process's own event loop reaps it, which a tight sync loop would starve;\n// doesn't apply to a process we didn't spawn).\n//\n// The default covers the dev server's graceful shutdown (drain in-flight work,\n// then checkpoint the stores) with room to spare — it used to be 3s, which was\n// fine when the server died instantly and is not now. Returns whether the pid\n// actually went, because a caller about to bind these ports has to escalate\n// rather than hope: giving up while the old server still holds 8080 is exactly\n// the EADDRINUSE this file exists to prevent.\nconst waitForPidGone = (pid: number, timeoutMs = 8000): boolean => {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n try {\n process.kill(pid, 0);\n } catch {\n return true; // throws once the pid no longer exists\n }\n execSync('sleep 0.05');\n }\n\n return false;\n};\n\n// Last resort for a process that ignored, or wedged during, its graceful stop.\nconst killPidHard = (pid: number): void => {\n console.warn(`Process ${pid} did not exit gracefully — sending SIGKILL.`);\n try {\n process.kill(pid, 'SIGKILL');\n } catch {\n return; // raced us and exited after all\n }\n waitForPidGone(pid, 2000);\n};\n\n// realpath'd: lsof reports the canonical path (e.g. /private/tmp on macOS,\n// where /tmp is a symlink), so comparing against a caller-supplied root\n// verbatim would false-negative whenever any component of the repo path is\n// itself a symlink (a symlinked home directory, an NFS mount, etc).\nconst getProcessCwd = (pid: number): string | null => {\n try {\n const line = execSync(`lsof -a -p ${pid} -d cwd -Fn`, { stdio: ['ignore', 'pipe', 'ignore'] })\n .toString()\n .split('\\n')\n .find((l) => l.startsWith('n'));\n return line ? fs.realpathSync(line.slice(1)) : null;\n } catch {\n return null; // pid gone, or we don't have permission to inspect it\n }\n};\n\n// The sweep below must never target this process's own wrappers: launched as\n// `npx qpq go:dev:api` (or via any shell whose -c string names qpq), the\n// ancestor chain (`sh -c \"npx ... qpq ...\"`, `npm exec qpq ...`) matches\n// isQpqCliCommand and shares our cwd, so excluding just process.pid isn't\n// enough — SIGINTing an ancestor tears down our own tree. Walk ppid to the\n// top and exclude the whole chain.\nconst getOwnAncestorPids = (): Set<number> => {\n const ancestors = new Set<number>();\n let pid = process.pid;\n while (pid > 1 && !ancestors.has(pid)) {\n ancestors.add(pid);\n try {\n pid = Number(\n execSync(`ps -o ppid= -p ${pid}`, { stdio: ['ignore', 'pipe', 'ignore'] })\n .toString()\n .trim(),\n );\n } catch {\n break;\n }\n }\n return ancestors;\n};\n\n// killStaleListeners only sees processes currently BOUND to one of the given\n// ports — it can't catch a still-running `qpq go:dev*` watcher whose spawned\n// dev-server child already died (a crash, or us killing just the child):\n// the watcher itself holds no port at that moment, so it's invisible to a\n// port scan, yet it's still alive and can spawn a fresh child — and bind a\n// port — moments later, racing whatever we're about to start. Close that gap\n// by finding any OTHER qpq CLI process rooted in the same repo checkout\n// (matched by cwd, not by port or command-line args, which vary by how it\n// was invoked) and killing it outright, regardless of what it currently has\n// bound. SIGINT (not SIGTERM) so goDevApiCommand/goDevWebCommand's own\n// handler runs and tears down its child cleanly rather than orphaning it.\nexport const killOtherQpqDevProcesses = (root: string): void => {\n const resolvedRoot = fs.realpathSync(root);\n const ownPids = getOwnAncestorPids();\n\n let lines: string[] = [];\n try {\n lines = execSync('ps -axo pid=,command=', { stdio: ['ignore', 'pipe', 'ignore'] })\n .toString()\n .split('\\n');\n } catch {\n return;\n }\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n const spaceIndex = trimmed.indexOf(' ');\n if (spaceIndex === -1) continue;\n\n const pid = Number(trimmed.slice(0, spaceIndex));\n const command = trimmed.slice(spaceIndex + 1).trim();\n\n if (!pid || ownPids.has(pid) || !isQpqCliCommand(command)) continue;\n if (getProcessCwd(pid) !== resolvedRoot) continue;\n\n console.log(`Killing other qpq dev process for this repo (pid ${pid}): ${command}`);\n process.kill(pid, 'SIGINT');\n if (!waitForPidGone(pid)) {\n killPidHard(pid);\n }\n }\n};\n\n// Whatever is currently listening on a port, as pid + command line. Empty when\n// the port is free, or when lsof is unavailable.\nconst getPortListeners = (port: number): { pid: number; command: string }[] => {\n let pids: number[] = [];\n try {\n pids = execSync(`lsof -t -iTCP:${port} -sTCP:LISTEN`, {\n stdio: ['ignore', 'pipe', 'ignore'],\n })\n .toString()\n .split('\\n')\n .map((line) => Number(line.trim()))\n .filter(Boolean);\n } catch {\n return []; // lsof exits non-zero when nothing is listening\n }\n\n return pids.flatMap((pid) => {\n try {\n return [{ pid, command: execSync(`ps -p ${pid} -o command=`).toString().trim() }];\n } catch {\n return []; // already gone\n }\n });\n};\n\n/**\n * Say what is holding these ports, without touching it.\n *\n * The other half of --keep-other-dev-servers: sparing a sibling checkout's dev\n * server means this one cannot bind, and an unexplained EADDRINUSE is a worse\n * experience than the silent kill it replaces. Naming the pid and the command\n * turns \"it just died\" into something you can act on.\n *\n * Worded as a prediction, not a fact, because it IS one: this runs before the\n * bundle step, so by the time anything binds - seconds later - the holder may\n * well have gone. Stating \"port in use\" and then starting cleanly reads like\n * the warning was wrong.\n */\nexport const reportPortHolders = (ports: number[]): void => {\n for (const port of ports) {\n for (const { pid, command } of getPortListeners(port)) {\n console.warn(`Port ${port} is currently held by pid ${pid}: ${command}`);\n console.warn(` Starting anyway. If it still holds port ${port} once this server binds, startup will fail with EADDRINUSE.`);\n }\n }\n};\n\nexport const killStaleListeners = (ports: number[], isOurs: (command: string) => boolean): void => {\n for (const port of ports) {\n for (const { pid, command } of getPortListeners(port)) {\n if (isOurs(command)) {\n console.log(`Killing stale dev server on port ${port} (pid ${pid})`);\n process.kill(pid);\n if (!waitForPidGone(pid)) {\n killPidHard(pid);\n }\n } else {\n console.warn(`Port ${port} is in use by an unrelated process (pid ${pid}: ${command}) — not killing it.`);\n }\n }\n }\n};\n"]}
1
+ {"version":3,"file":"killStaleListeners.js","sourceRoot":"","sources":["../../../src/lib/killStaleListeners.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,MAAM,IAAI,CAAC;AACpB,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAC9C,OAAO,EAAE,qBAAqB,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEhE,MAAM,GAAG,GAAG,YAAY,CAAC,sBAAsB,CAAC,CAAC;AAEjD,yEAAyE;AACzE,6EAA6E;AAC7E,yEAAyE;AACzE,6EAA6E;AAC7E,mEAAmE;AACnE,4DAA4D;AAC5D,MAAM,CAAC,MAAM,eAAe,GAAG,CAAC,OAAe,EAAW,EAAE,CAC1D,OAAO;KACJ,IAAI,EAAE;KACN,KAAK,CAAC,KAAK,CAAC;KACZ,IAAI,CAAC,CAAC,KAAK,EAAE,EAAE;IACd,MAAM,IAAI,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;IAClC,MAAM,UAAU,GAAG,IAAI,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACnE,OAAO,UAAU,KAAK,KAAK,CAAC;AAC9B,CAAC,CAAC,CAAC;AAEP,yEAAyE;AACzE,2EAA2E;AAC3E,2EAA2E;AAC3E,sEAAsE;AACtE,4EAA4E;AAC5E,6EAA6E;AAC7E,4EAA4E;AAC5E,4EAA4E;AAC5E,uEAAuE;AACvE,4EAA4E;AAC5E,2EAA2E;AAC3E,+CAA+C;AAC/C,EAAE;AACF,+EAA+E;AAC/E,0EAA0E;AAC1E,4EAA4E;AAC5E,+EAA+E;AAC/E,8CAA8C;AAC9C,MAAM,cAAc,GAAG,CAAC,GAAW,EAAE,SAAS,GAAG,IAAI,EAAW,EAAE;IAChE,MAAM,QAAQ,GAAG,IAAI,CAAC,GAAG,EAAE,GAAG,SAAS,CAAC;IACxC,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,QAAQ,EAAE,CAAC;QAC7B,IAAI,CAAC;YACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC,CAAC;QACvB,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,IAAI,CAAC,CAAC,uCAAuC;QACtD,CAAC;QACD,SAAS,CAAC,EAAE,CAAC,CAAC;IAChB,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAEF,+EAA+E;AAC/E,MAAM,WAAW,GAAG,CAAC,GAAW,EAAQ,EAAE;IACxC,GAAG,CAAC,IAAI,CAAC,WAAW,GAAG,6CAA6C,CAAC,CAAC;IACtE,IAAI,CAAC;QACH,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,SAAS,CAAC,CAAC;IAC/B,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,CAAC,gCAAgC;IAC1C,CAAC;IACD,cAAc,CAAC,GAAG,EAAE,IAAI,CAAC,CAAC;AAC5B,CAAC,CAAC;AAEF,2EAA2E;AAC3E,wEAAwE;AACxE,2EAA2E;AAC3E,oEAAoE;AACpE,MAAM,aAAa,GAAG,CAAC,GAAW,EAAoB,EAAE;IACtD,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,qBAAqB,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;aACtF,KAAK,CAAC,IAAI,CAAC;aACX,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC;QAClC,OAAO,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtD,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,IAAI,CAAC,CAAC,sDAAsD;IACrE,CAAC;AACH,CAAC,CAAC;AAEF,6EAA6E;AAC7E,yEAAyE;AACzE,yEAAyE;AACzE,0EAA0E;AAC1E,0EAA0E;AAC1E,mCAAmC;AACnC,MAAM,kBAAkB,GAAG,GAAgB,EAAE;IAC3C,MAAM,SAAS,GAAG,IAAI,GAAG,EAAU,CAAC;IACpC,IAAI,GAAG,GAAG,OAAO,CAAC,GAAG,CAAC;IACtB,OAAO,GAAG,GAAG,CAAC,IAAI,CAAC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,EAAE,CAAC;QACtC,SAAS,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACnB,IAAI,CAAC;YACH,GAAG,GAAG,MAAM,CAAC,qBAAqB,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;QACvF,CAAC;QAAC,MAAM,CAAC;YACP,MAAM;QACR,CAAC;IACH,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC,CAAC;AAEF,6EAA6E;AAC7E,4EAA4E;AAC5E,yEAAyE;AACzE,0EAA0E;AAC1E,0EAA0E;AAC1E,4EAA4E;AAC5E,wEAAwE;AACxE,0EAA0E;AAC1E,4EAA4E;AAC5E,uEAAuE;AACvE,0EAA0E;AAC1E,MAAM,CAAC,MAAM,wBAAwB,GAAG,CAAC,IAAY,EAAQ,EAAE;IAC7D,MAAM,YAAY,GAAG,EAAE,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IAC3C,MAAM,OAAO,GAAG,kBAAkB,EAAE,CAAC;IAErC,IAAI,KAAK,GAAa,EAAE,CAAC;IACzB,IAAI,CAAC;QACH,KAAK,GAAG,qBAAqB,CAAC,IAAI,EAAE,CAAC,MAAM,EAAE,eAAe,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;IAC7E,CAAC;IAAC,MAAM,CAAC;QACP,OAAO;IACT,CAAC;IAED,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,EAAE,CAAC;QAC5B,IAAI,CAAC,OAAO;YAAE,SAAS;QAEvB,MAAM,UAAU,GAAG,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACxC,IAAI,UAAU,KAAK,CAAC,CAAC;YAAE,SAAS;QAEhC,MAAM,GAAG,GAAG,MAAM,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,UAAU,CAAC,CAAC,CAAC;QACjD,MAAM,OAAO,GAAG,OAAO,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;QAErD,IAAI,CAAC,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,eAAe,CAAC,OAAO,CAAC;YAAE,SAAS;QACpE,IAAI,aAAa,CAAC,GAAG,CAAC,KAAK,YAAY;YAAE,SAAS;QAElD,GAAG,CAAC,GAAG,CAAC,oDAAoD,GAAG,MAAM,OAAO,EAAE,CAAC,CAAC;QAChF,OAAO,CAAC,IAAI,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QAC5B,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC;YACzB,WAAW,CAAC,GAAG,CAAC,CAAC;QACnB,CAAC;IACH,CAAC;AACH,CAAC,CAAC;AAEF,+EAA+E;AAC/E,iDAAiD;AACjD,MAAM,gBAAgB,GAAG,CAAC,IAAY,EAAsC,EAAE;IAC5E,IAAI,IAAI,GAAa,EAAE,CAAC;IACxB,IAAI,CAAC;QACH,IAAI,GAAG,qBAAqB,CAAC,MAAM,EAAE,CAAC,IAAI,EAAE,SAAS,IAAI,EAAE,EAAE,cAAc,CAAC,CAAC;aAC1E,KAAK,CAAC,IAAI,CAAC;aACX,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;aAClC,MAAM,CAAC,OAAO,CAAC,CAAC;IACrB,CAAC;IAAC,MAAM,CAAC;QACP,OAAO,EAAE,CAAC,CAAC,gDAAgD;IAC7D,CAAC;IAED,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC,GAAG,EAAE,EAAE;QAC1B,IAAI,CAAC;YACH,OAAO,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,qBAAqB,CAAC,IAAI,EAAE,CAAC,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC;QACvG,CAAC;QAAC,MAAM,CAAC;YACP,OAAO,EAAE,CAAC,CAAC,eAAe;QAC5B,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC,CAAC;AAEF;;;;;;;;;;;;GAYG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,KAAe,EAAQ,EAAE;IACzD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;YACtD,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,6BAA6B,GAAG,KAAK,OAAO,EAAE,CAAC,CAAC;YACrE,GAAG,CAAC,IAAI,CAAC,6CAA6C,IAAI,6DAA6D,CAAC,CAAC;QAC3H,CAAC;IACH,CAAC;AACH,CAAC,CAAC;AAEF,MAAM,CAAC,MAAM,kBAAkB,GAAG,CAAC,KAAe,EAAE,MAAoC,EAAQ,EAAE;IAChG,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,KAAK,MAAM,EAAE,GAAG,EAAE,OAAO,EAAE,IAAI,gBAAgB,CAAC,IAAI,CAAC,EAAE,CAAC;YACtD,IAAI,MAAM,CAAC,OAAO,CAAC,EAAE,CAAC;gBACpB,GAAG,CAAC,GAAG,CAAC,oCAAoC,IAAI,SAAS,GAAG,GAAG,CAAC,CAAC;gBACjE,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;gBAClB,IAAI,CAAC,cAAc,CAAC,GAAG,CAAC,EAAE,CAAC;oBACzB,WAAW,CAAC,GAAG,CAAC,CAAC;gBACnB,CAAC;YACH,CAAC;iBAAM,CAAC;gBACN,GAAG,CAAC,IAAI,CAAC,QAAQ,IAAI,2CAA2C,GAAG,KAAK,OAAO,qBAAqB,CAAC,CAAC;YACxG,CAAC;QACH,CAAC;IACH,CAAC;AACH,CAAC,CAAC","sourcesContent":["// Pre-start sweep shared by the dev-server commands: a previous run whose\n// wrapper died without killing its listener (closed terminal, SIGKILL) leaves\n// an orphaned process holding a dev port, and the next launch dies with\n// EADDRINUSE. Before starting, kill any listener on the given ports whose\n// command line matches `isOurs`. Anything else on the ports is left alone\n// and reported, it's not ours.\nimport { Nullable } from '@qpqjs/core';\n\nimport fs from 'fs';\nimport path from 'path';\n\nimport { createOutput } from './createOutput';\nimport { runCommandCaptureSync, sleepSync } from './runCommand';\n\nconst out = createOutput('kill-stale-listeners');\n\n// Matches any invocation of the qpq CLI itself, regardless of how it was\n// launched: `node node_modules/.bin/qpq ...` (the npm-script shim, which has\n// no .js extension), a direct `node .../lib/commonjs/bin/qpq.js` call, a\n// global `qpq` shebang script, etc. Checks each whitespace-separated token's\n// basename (extension stripped) rather than a fixed substring like\n// 'bin/qpq.js', which only matches the last of those forms.\nexport const isQpqCliCommand = (command: string): boolean =>\n command\n .trim()\n .split(/\\s+/)\n .some((token) => {\n const base = path.basename(token);\n const withoutExt = base.endsWith('.js') ? base.slice(0, -3) : base;\n return withoutExt === 'qpq';\n });\n\n// SIGTERM only requests termination, the caller (about to bind this same\n// process's ports itself) needs it actually gone first. A stale dev server\n// can be mid-startup when killed (still resolving config, not yet bound to\n// every port it eventually binds) and keep running for a bit past the\n// signal, going on to bind a LATER port, waiting for just the one port that\n// matched isn't enough; wait for the whole pid to disappear so it can't bind\n// anything else afterward. Poll synchronously (this whole sweep runs before\n// anything async starts) via `kill(pid, 0)`, reliable and fast here because\n// these are unrelated processes orphaned earlier, not children of this\n// one (a real child can sit as a zombie answering `kill(pid, 0)` until this\n// process's own event loop reaps it, which a tight sync loop would starve;\n// doesn't apply to a process we didn't spawn).\n//\n// The default covers the dev server's graceful shutdown (drain in-flight work,\n// then checkpoint the stores) with room to spare. Returns whether the pid\n// actually went, because a caller about to bind these ports has to escalate\n// rather than hope: giving up while the old server still holds 8080 is exactly\n// the EADDRINUSE this file exists to prevent.\nconst waitForPidGone = (pid: number, timeoutMs = 8000): boolean => {\n const deadline = Date.now() + timeoutMs;\n while (Date.now() < deadline) {\n try {\n process.kill(pid, 0);\n } catch {\n return true; // throws once the pid no longer exists\n }\n sleepSync(50);\n }\n\n return false;\n};\n\n// Last resort for a process that ignored, or wedged during, its graceful stop.\nconst killPidHard = (pid: number): void => {\n out.warn(`Process ${pid} did not exit gracefully — sending SIGKILL.`);\n try {\n process.kill(pid, 'SIGKILL');\n } catch {\n return; // raced us and exited after all\n }\n waitForPidGone(pid, 2000);\n};\n\n// realpath'd: lsof reports the canonical path (e.g. /private/tmp on macOS,\n// where /tmp is a symlink), so comparing against a caller-supplied root\n// verbatim would false-negative whenever any component of the repo path is\n// itself a symlink (a symlinked home directory, an NFS mount, etc).\nconst getProcessCwd = (pid: number): Nullable<string> => {\n try {\n const line = runCommandCaptureSync('lsof', ['-a', '-p', String(pid), '-d', 'cwd', '-Fn'])\n .split('\\n')\n .find((l) => l.startsWith('n'));\n return line ? fs.realpathSync(line.slice(1)) : null;\n } catch {\n return null; // pid gone, or we don't have permission to inspect it\n }\n};\n\n// The sweep below must never target this process's own wrappers: launched as\n// `npx qpq go:dev:api` (or via any shell whose -c string names qpq), the\n// ancestor chain (`sh -c \"npx ... qpq ...\"`, `npm exec qpq ...`) matches\n// isQpqCliCommand and shares our cwd, so excluding just process.pid isn't\n// enough, SIGINTing an ancestor tears down our own tree. Walk ppid to the\n// top and exclude the whole chain.\nconst getOwnAncestorPids = (): Set<number> => {\n const ancestors = new Set<number>();\n let pid = process.pid;\n while (pid > 1 && !ancestors.has(pid)) {\n ancestors.add(pid);\n try {\n pid = Number(runCommandCaptureSync('ps', ['-o', 'ppid=', '-p', String(pid)]).trim());\n } catch {\n break;\n }\n }\n return ancestors;\n};\n\n// killStaleListeners only sees processes currently BOUND to one of the given\n// ports, it can't catch a still-running `qpq go:dev*` watcher whose spawned\n// dev-server child already died (a crash, or us killing just the child):\n// the watcher itself holds no port at that moment, so it's invisible to a\n// port scan, yet it's still alive and can spawn a fresh child, and bind a\n// port, moments later, racing whatever we're about to start. Close that gap\n// by finding any OTHER qpq CLI process rooted in the same repo checkout\n// (matched by cwd, not by port or command-line args, which vary by how it\n// was invoked) and killing it outright, regardless of what it currently has\n// bound. SIGINT (not SIGTERM) so goDevApiCommand/goDevWebCommand's own\n// handler runs and tears down its child cleanly rather than orphaning it.\nexport const killOtherQpqDevProcesses = (root: string): void => {\n const resolvedRoot = fs.realpathSync(root);\n const ownPids = getOwnAncestorPids();\n\n let lines: string[] = [];\n try {\n lines = runCommandCaptureSync('ps', ['-axo', 'pid=,command=']).split('\\n');\n } catch {\n return;\n }\n\n for (const line of lines) {\n const trimmed = line.trim();\n if (!trimmed) continue;\n\n const spaceIndex = trimmed.indexOf(' ');\n if (spaceIndex === -1) continue;\n\n const pid = Number(trimmed.slice(0, spaceIndex));\n const command = trimmed.slice(spaceIndex + 1).trim();\n\n if (!pid || ownPids.has(pid) || !isQpqCliCommand(command)) continue;\n if (getProcessCwd(pid) !== resolvedRoot) continue;\n\n out.log(`Killing other qpq dev process for this repo (pid ${pid}): ${command}`);\n process.kill(pid, 'SIGINT');\n if (!waitForPidGone(pid)) {\n killPidHard(pid);\n }\n }\n};\n\n// Whatever is currently listening on a port, as pid + command line. Empty when\n// the port is free, or when lsof is unavailable.\nconst getPortListeners = (port: number): { pid: number; command: string }[] => {\n let pids: number[] = [];\n try {\n pids = runCommandCaptureSync('lsof', ['-t', `-iTCP:${port}`, '-sTCP:LISTEN'])\n .split('\\n')\n .map((line) => Number(line.trim()))\n .filter(Boolean);\n } catch {\n return []; // lsof exits non-zero when nothing is listening\n }\n\n return pids.flatMap((pid) => {\n try {\n return [{ pid, command: runCommandCaptureSync('ps', ['-p', String(pid), '-o', 'command=']).trim() }];\n } catch {\n return []; // already gone\n }\n });\n};\n\n/**\n * Say what is holding these ports, without touching it.\n *\n * The other half of --keep-other-dev-servers: sparing a sibling checkout's dev\n * server means this one cannot bind, and an unexplained EADDRINUSE is a worse\n * experience than the silent kill it replaces. Naming the pid and the command\n * turns \"it just died\" into something you can act on.\n *\n * Worded as a prediction, not a fact, because it IS one: this runs before the\n * bundle step, so by the time anything binds - seconds later - the holder may\n * well have gone. Stating \"port in use\" and then starting cleanly reads like\n * the warning was wrong.\n */\nexport const reportPortHolders = (ports: number[]): void => {\n for (const port of ports) {\n for (const { pid, command } of getPortListeners(port)) {\n out.warn(`Port ${port} is currently held by pid ${pid}: ${command}`);\n out.warn(` Starting anyway. If it still holds port ${port} once this server binds, startup will fail with EADDRINUSE.`);\n }\n }\n};\n\nexport const killStaleListeners = (ports: number[], isOurs: (command: string) => boolean): void => {\n for (const port of ports) {\n for (const { pid, command } of getPortListeners(port)) {\n if (isOurs(command)) {\n out.log(`Killing stale dev server on port ${port} (pid ${pid})`);\n process.kill(pid);\n if (!waitForPidGone(pid)) {\n killPidHard(pid);\n }\n } else {\n out.warn(`Port ${port} is in use by an unrelated process (pid ${pid}: ${command}) — not killing it.`);\n }\n }\n }\n};\n"]}
@@ -7,6 +7,7 @@ export declare const promptSelectDetailed: <T>(message: string, choices: {
7
7
  }[]) => Promise<T>;
8
8
  export declare const promptYesNo: (message: string) => Promise<boolean>;
9
9
  export declare const promptText: (message: string) => Promise<string>;
10
+ export declare const promptTypedConfirmation: (target: string) => Promise<boolean>;
10
11
  export declare const promptCheckbox: <T>(message: string, choices: {
11
12
  name: string;
12
13
  value: T;
@@ -1,3 +1,5 @@
1
+ import { createOutput } from './createOutput';
2
+ const out = createOutput('prompts');
1
3
  // Present a single-choice list prompt and return the selected value.
2
4
  // @inquirer/prompts is imported lazily so commands only pay for it when
3
5
  // actually prompting.
@@ -14,12 +16,22 @@ export const promptSelectDetailed = async (message, choices) => {
14
16
  };
15
17
  // A yes/no list prompt, returning true for "Yes".
16
18
  export const promptYesNo = async (message) => (await promptSelect(message, ['No', 'Yes'])) === 'Yes';
17
- // A free-text prompt — used where a y/n is too easy to fat-finger (destructive
19
+ // A free-text prompt, used where a y/n is too easy to fat-finger (destructive
18
20
  // confirmations that require typing the target name back).
19
21
  export const promptText = async (message) => {
20
22
  const { input } = await import('@inquirer/prompts');
21
23
  return input({ message });
22
24
  };
25
+ // The friction for destructive commands: the target has to be typed back exactly. Resolves
26
+ // true when it was; prints why it stops otherwise, so callers only have to bail out.
27
+ export const promptTypedConfirmation = async (target) => {
28
+ const confirmation = await promptText(`Type '${target}' to confirm`);
29
+ if (confirmation !== target) {
30
+ out.log('Confirmation did not match. Exiting!');
31
+ return false;
32
+ }
33
+ return true;
34
+ };
23
35
  // A multi-select checkbox prompt over labelled values; `checked` preselects a choice.
24
36
  export const promptCheckbox = async (message, choices) => {
25
37
  const { checkbox } = await import('@inquirer/prompts');
@@ -1 +1 @@
1
- {"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../../src/lib/prompts.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,wEAAwE;AACxE,sBAAsB;AACtB,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAAE,OAAe,EAAE,OAAiB,EAAE,aAAsB,EAAmB,EAAE;IAChH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;IACrD,OAAO,MAAM,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;AAC9D,CAAC,CAAC;AAEF,wEAAwE;AACxE,6EAA6E;AAC7E,gEAAgE;AAChE,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,EACvC,OAAe,EACf,OAA2E,EAC/D,EAAE;IACd,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;IACrD,OAAO,MAAM,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;AACtC,CAAC,CAAC;AAEF,kDAAkD;AAClD,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,EAAE,OAAe,EAAoB,EAAE,CAAC,CAAC,MAAM,YAAY,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC;AAE/H,+EAA+E;AAC/E,2DAA2D;AAC3D,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAAE,OAAe,EAAmB,EAAE;IACnE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;IACpD,OAAO,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;AAC5B,CAAC,CAAC;AAEF,sFAAsF;AACtF,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EACjC,OAAe,EACf,OAA8E,EAChE,EAAE;IAChB,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;IACvD,OAAO,QAAQ,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;AACxC,CAAC,CAAC","sourcesContent":["// Present a single-choice list prompt and return the selected value.\n// @inquirer/prompts is imported lazily so commands only pay for it when\n// actually prompting.\nexport const promptSelect = async (message: string, choices: string[], defaultChoice?: string): Promise<string> => {\n const { select } = await import('@inquirer/prompts');\n return select({ message, choices, default: defaultChoice });\n};\n\n// A single-choice list prompt over labelled values; a choice's optional\n// `short` replaces its name in the answer line once picked, and its optional\n// `description` renders under the list while it is highlighted.\nexport const promptSelectDetailed = async <T>(\n message: string,\n choices: { name: string; value: T; short?: string; description?: string }[],\n): Promise<T> => {\n const { select } = await import('@inquirer/prompts');\n return select({ message, choices });\n};\n\n// A yes/no list prompt, returning true for \"Yes\".\nexport const promptYesNo = async (message: string): Promise<boolean> => (await promptSelect(message, ['No', 'Yes'])) === 'Yes';\n\n// A free-text prompt — used where a y/n is too easy to fat-finger (destructive\n// confirmations that require typing the target name back).\nexport const promptText = async (message: string): Promise<string> => {\n const { input } = await import('@inquirer/prompts');\n return input({ message });\n};\n\n// A multi-select checkbox prompt over labelled values; `checked` preselects a choice.\nexport const promptCheckbox = async <T>(\n message: string,\n choices: { name: string; value: T; checked?: boolean; description?: string }[],\n): Promise<T[]> => {\n const { checkbox } = await import('@inquirer/prompts');\n return checkbox({ message, choices });\n};\n"]}
1
+ {"version":3,"file":"prompts.js","sourceRoot":"","sources":["../../../src/lib/prompts.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAE9C,MAAM,GAAG,GAAG,YAAY,CAAC,SAAS,CAAC,CAAC;AACpC,qEAAqE;AACrE,wEAAwE;AACxE,sBAAsB;AACtB,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAAE,OAAe,EAAE,OAAiB,EAAE,aAAsB,EAAmB,EAAE;IAChH,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;IACrD,OAAO,MAAM,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,CAAC,CAAC;AAC9D,CAAC,CAAC;AAEF,wEAAwE;AACxE,6EAA6E;AAC7E,gEAAgE;AAChE,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,EACvC,OAAe,EACf,OAA2E,EAC/D,EAAE;IACd,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;IACrD,OAAO,MAAM,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;AACtC,CAAC,CAAC;AAEF,kDAAkD;AAClD,MAAM,CAAC,MAAM,WAAW,GAAG,KAAK,EAAE,OAAe,EAAoB,EAAE,CAAC,CAAC,MAAM,YAAY,CAAC,OAAO,EAAE,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC;AAE/H,8EAA8E;AAC9E,2DAA2D;AAC3D,MAAM,CAAC,MAAM,UAAU,GAAG,KAAK,EAAE,OAAe,EAAmB,EAAE;IACnE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;IACpD,OAAO,KAAK,CAAC,EAAE,OAAO,EAAE,CAAC,CAAC;AAC5B,CAAC,CAAC;AAEF,2FAA2F;AAC3F,qFAAqF;AACrF,MAAM,CAAC,MAAM,uBAAuB,GAAG,KAAK,EAAE,MAAc,EAAoB,EAAE;IAChF,MAAM,YAAY,GAAG,MAAM,UAAU,CAAC,SAAS,MAAM,cAAc,CAAC,CAAC;IAErE,IAAI,YAAY,KAAK,MAAM,EAAE,CAAC;QAC5B,GAAG,CAAC,GAAG,CAAC,sCAAsC,CAAC,CAAC;QAChD,OAAO,KAAK,CAAC;IACf,CAAC;IAED,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AAEF,sFAAsF;AACtF,MAAM,CAAC,MAAM,cAAc,GAAG,KAAK,EACjC,OAAe,EACf,OAA8E,EAChE,EAAE;IAChB,MAAM,EAAE,QAAQ,EAAE,GAAG,MAAM,MAAM,CAAC,mBAAmB,CAAC,CAAC;IACvD,OAAO,QAAQ,CAAC,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC,CAAC;AACxC,CAAC,CAAC","sourcesContent":["import { createOutput } from './createOutput';\n\nconst out = createOutput('prompts');\n// Present a single-choice list prompt and return the selected value.\n// @inquirer/prompts is imported lazily so commands only pay for it when\n// actually prompting.\nexport const promptSelect = async (message: string, choices: string[], defaultChoice?: string): Promise<string> => {\n const { select } = await import('@inquirer/prompts');\n return select({ message, choices, default: defaultChoice });\n};\n\n// A single-choice list prompt over labelled values; a choice's optional\n// `short` replaces its name in the answer line once picked, and its optional\n// `description` renders under the list while it is highlighted.\nexport const promptSelectDetailed = async <T>(\n message: string,\n choices: { name: string; value: T; short?: string; description?: string }[],\n): Promise<T> => {\n const { select } = await import('@inquirer/prompts');\n return select({ message, choices });\n};\n\n// A yes/no list prompt, returning true for \"Yes\".\nexport const promptYesNo = async (message: string): Promise<boolean> => (await promptSelect(message, ['No', 'Yes'])) === 'Yes';\n\n// A free-text prompt, used where a y/n is too easy to fat-finger (destructive\n// confirmations that require typing the target name back).\nexport const promptText = async (message: string): Promise<string> => {\n const { input } = await import('@inquirer/prompts');\n return input({ message });\n};\n\n// The friction for destructive commands: the target has to be typed back exactly. Resolves\n// true when it was; prints why it stops otherwise, so callers only have to bail out.\nexport const promptTypedConfirmation = async (target: string): Promise<boolean> => {\n const confirmation = await promptText(`Type '${target}' to confirm`);\n\n if (confirmation !== target) {\n out.log('Confirmation did not match. Exiting!');\n return false;\n }\n\n return true;\n};\n\n// A multi-select checkbox prompt over labelled values; `checked` preselects a choice.\nexport const promptCheckbox = async <T>(\n message: string,\n choices: { name: string; value: T; checked?: boolean; description?: string }[],\n): Promise<T[]> => {\n const { checkbox } = await import('@inquirer/prompts');\n return checkbox({ message, choices });\n};\n"]}
@@ -1,4 +1,5 @@
1
1
  import { type QPQConfig } from '@qpqjs/core';
2
2
  export declare const loadServiceQpqConfig: (appName: string, service: string) => QPQConfig;
3
+ export declare const loadFirstServiceQpqConfig: (appName: string) => QPQConfig;
3
4
  export declare const getAppPrefix: (appName: string) => string;
4
5
  export declare const getServiceNamesWithFederation: (appName: string, serviceNames: string[]) => string[];
@@ -5,16 +5,20 @@ import { getRoot, getServiceNames } from './discovery';
5
5
  // registered, so requiring TS works and `@scope/*` imports resolve through
6
6
  // workspace symlinks).
7
7
  export const loadServiceQpqConfig = (appName, service) => getWorkspaceServiceQpqConfig(getRoot(), appName, service);
8
- export const getAppPrefix = (appName) => {
9
- const services = getServiceNames(appName);
10
- if (services.length === 0)
8
+ // App-wide facts (the application name, the domain stack) live on every service's config alike, so the first
9
+ // service found stands in for the app. An app with no services has no config to read and that is an error.
10
+ export const loadFirstServiceQpqConfig = (appName) => {
11
+ const [firstService] = getServiceNames(appName);
12
+ if (firstService === undefined) {
11
13
  throw new Error(`No services found for app ${appName}`);
12
- return qpqCoreUtils.getApplicationName(loadServiceQpqConfig(appName, services[0]));
14
+ }
15
+ return loadServiceQpqConfig(appName, firstService);
13
16
  };
17
+ export const getAppPrefix = (appName) => qpqCoreUtils.getApplicationName(loadFirstServiceQpqConfig(appName));
14
18
  // The subset of the given services that opt into module-federation publishing,
15
19
  // i.e. declare defineFederatedModuleStore(...). Federation is a config setting,
16
20
  // not a directory convention (unlike views/ backends), so detecting it means
17
- // loading each service's config — which is why this lives here and not in the
21
+ // loading each service's config, which is why this lives here and not in the
18
22
  // directory-only discovery module. Used by the deploy drivers to decide which
19
23
  // backends to publish federated remotes for after a deploy.
20
24
  export const getServiceNamesWithFederation = (appName, serviceNames) => serviceNames.filter((service) => !!qpqCoreUtils.getFederatedModuleStore(loadServiceQpqConfig(appName, service)));