22 * Postrun hook — uses dynamic imports to avoid loading heavy modules (base-command, analytics)
33 * at module evaluation time. These are only needed after the command has already finished.
44 */
5+ import { treeKill } from '../tree-kill.js'
56import { Command , Hook } from '@oclif/core'
67
78let postRunHookCompleted = false
@@ -15,6 +16,42 @@ export function postRunHookHasCompleted(): boolean {
1516 return postRunHookCompleted
1617}
1718
19+ /**
20+ * Wait for the postrun hook to finish (so auto-upgrade has a chance to run) and then
21+ * tree-kill the current process tree before exiting.
22+ *
23+ * Long-running interactive commands like `app dev` need this when the user terminates
24+ * the command via `q` or Ctrl+C. The dev sub-processes such as servers and watchers keep
25+ * the event loop alive, so even after oclif's postrun hook completes the node process
26+ * won't exit on its own and we have to `treeKill` the process tree. We must not do that
27+ * before the postrun hook has actually finished running auto-upgrade, otherwise we would
28+ * kill the upgrade mid-way while `npm install` is still running.
29+ *
30+ * The flag `postRunHookCompleted` is flipped at the very end of the hook after
31+ * `autoUpgradeIfNeeded` resolves, so polling it here is safe.
32+ */
33+ export function waitForPostRunHookAndExit ( ) : void {
34+ const pollIntervalMs = 100
35+ // Auto-upgrade can take a while (npm/pnpm/yarn install). Cap the wait generously so
36+ // a stuck upgrade still terminates the process eventually.
37+ const maxWaitMs = 120000
38+
39+ let elapsed = 0
40+ let terminating = false
41+ const handle = setInterval ( ( ) => {
42+ if ( terminating ) return
43+ if ( postRunHookHasCompleted ( ) || elapsed >= maxWaitMs ) {
44+ terminating = true
45+ clearInterval ( handle )
46+ treeKill ( process . pid , 'SIGINT' , false , ( ) => {
47+ process . exit ( 0 )
48+ } )
49+ return
50+ }
51+ elapsed += pollIntervalMs
52+ } , pollIntervalMs )
53+ }
54+
1855// This hook is called after each successful command run. More info: https://oclif.io/docs/hooks
1956export const hook : Hook . Postrun = async ( { config, Command} ) => {
2057 await detectStopCommand ( Command as unknown as typeof Command )
@@ -28,9 +65,9 @@ export const hook: Hook.Postrun = async ({config, Command}) => {
2865 const { outputDebug} = await import ( '../output.js' )
2966 const command = Command . id . replace ( / : / g, ' ' )
3067 outputDebug ( `Completed command ${ command } ` )
31- postRunHookCompleted = true
3268
3369 if ( ! command . includes ( 'notifications' ) && ! command . includes ( 'upgrade' ) ) await autoUpgradeIfNeeded ( )
70+ postRunHookCompleted = true
3471}
3572
3673/**
0 commit comments