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 +6 -0
- package/package.json +1 -1
- package/src/soft-exit.ts +36 -0
- package/src/tui/hub.ts +20 -14
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
package/src/soft-exit.ts
ADDED
|
@@ -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
|
-
|
|
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 (
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
|
111
|
+
const finish = () => {
|
|
103
112
|
stdin.off("data", onData);
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
115
|
-
resolve();
|
|
121
|
+
finish();
|
|
116
122
|
}
|
|
117
123
|
};
|
|
118
124
|
stdin.on("data", onData);
|