gflows 1.1.0 → 1.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.1.1] - 2026-07-25
11
+
12
+ ### Fixed
13
+
14
+ - Hub returns after dispatch: re-arm stdin after “Press enter to return to hub” (no pause/unref), and trap `process.exit` from commands so the hub loop is not killed
15
+
10
16
  ## [1.1.0] - 2026-07-25
11
17
 
12
18
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "gflows",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "author": "Ali AlNaghmoush (https://github.com/alialnaghmoush)",
5
5
  "repository": {
6
6
  "type": "git",
@@ -0,0 +1,36 @@
1
+ /**
2
+ * Soft process.exit for hub-embedded command dispatch.
3
+ * Commands that call process.exit would otherwise kill the hub loop.
4
+ * @module soft-exit
5
+ */
6
+
7
+ /**
8
+ * Thrown instead of terminating the process when soft-exit mode is active.
9
+ */
10
+ export class SoftExitError extends Error {
11
+ /**
12
+ * @param code - Exit code the command requested.
13
+ */
14
+ constructor(readonly code: number) {
15
+ super(`process.exit(${code})`);
16
+ this.name = "SoftExitError";
17
+ }
18
+ }
19
+
20
+ /**
21
+ * Runs `fn` while intercepting `process.exit` as {@link SoftExitError}.
22
+ * Restores the real `process.exit` afterward.
23
+ */
24
+ export async function runWithSoftExit<T>(fn: () => Promise<T>): Promise<T> {
25
+ const realExit = process.exit;
26
+ const softExit = ((code?: number) => {
27
+ throw new SoftExitError(code ?? 0);
28
+ }) as typeof process.exit;
29
+
30
+ process.exit = softExit;
31
+ try {
32
+ return await fn();
33
+ } finally {
34
+ process.exit = realExit;
35
+ }
36
+ }
package/src/tui/hub.ts CHANGED
@@ -7,6 +7,7 @@ import { render } from "ink";
7
7
  import React from "react";
8
8
  import { dispatch } from "../dispatch.js";
9
9
  import { PromptCancelledError } from "../prompts.js";
10
+ import { runWithSoftExit, SoftExitError } from "../soft-exit.js";
10
11
  import { type HubSessionResult, HubShell } from "./HubShell.js";
11
12
  import { prepareStdinAfterInk } from "./stdin.js";
12
13
 
@@ -25,17 +26,22 @@ export async function runTuiHub(cwd: string): Promise<boolean> {
25
26
  if (!canUseTui()) return false;
26
27
 
27
28
  for (;;) {
29
+ // Ink needs a live, referenced stdin after waitEnter / Clack / prior unmount.
30
+ prepareStdinAfterInk();
28
31
  const result = await runHubSession(cwd);
29
32
  if (result.kind === "quit") break;
30
33
 
31
34
  if (result.kind === "run") {
32
- // Ink unrefs stdin on unmount — re-arm before Clack / git prompts.
33
35
  prepareStdinAfterInk();
34
36
  console.log("");
35
37
  try {
36
- await dispatch(cwd, result.argv);
38
+ // Many commands call process.exit on validation/success paths — trap them
39
+ // so the hub can show “press enter” and remount instead of dying.
40
+ await runWithSoftExit(() => dispatch(cwd, result.argv));
37
41
  } catch (err) {
38
- if (!(err instanceof PromptCancelledError)) {
42
+ if (err instanceof SoftExitError) {
43
+ // Command already printed its output / error; continue to waitEnter.
44
+ } else if (!(err instanceof PromptCancelledError)) {
39
45
  console.error("gflows:", err instanceof Error ? err.message : String(err));
40
46
  }
41
47
  }
@@ -78,15 +84,18 @@ function runHubSession(cwd: string): Promise<HubSessionResult> {
78
84
 
79
85
  /**
80
86
  * Raw stdin “press enter” (no Clack / no Ink).
81
- * Ink unrefs stdin on unmount we must ref it again or the process exits
82
- * before the user can return to the hub.
87
+ * Leaves stdin armed for the next Ink remount (does not pause/unref).
83
88
  */
84
89
  function waitEnterRaw(label: string): Promise<void> {
85
90
  return new Promise((resolve) => {
86
91
  const stdin = process.stdin;
87
92
  process.stdout.write(`${label}\n`);
88
93
  if (typeof stdin.setRawMode === "function") {
89
- stdin.setRawMode(true);
94
+ try {
95
+ stdin.setRawMode(true);
96
+ } catch {
97
+ // ignore
98
+ }
90
99
  }
91
100
  stdin.resume();
92
101
  stdin.ref();
@@ -99,20 +108,17 @@ function waitEnterRaw(label: string): Promise<void> {
99
108
  }
100
109
  }
101
110
 
102
- const cleanup = () => {
111
+ const finish = () => {
103
112
  stdin.off("data", onData);
104
- if (typeof stdin.setRawMode === "function") {
105
- stdin.setRawMode(false);
106
- }
107
- stdin.pause();
108
- stdin.unref();
113
+ // Critical: leave stdin ready for Ink remount (old code paused+unref'd → hub died).
114
+ prepareStdinAfterInk();
115
+ resolve();
109
116
  };
110
117
 
111
118
  const onData = (chunk: string | Buffer) => {
112
119
  const s = typeof chunk === "string" ? chunk : chunk.toString("utf8");
113
120
  if (s.includes("\r") || s.includes("\n") || s.includes("\x03")) {
114
- cleanup();
115
- resolve();
121
+ finish();
116
122
  }
117
123
  };
118
124
  stdin.on("data", onData);