@akiojin/gwt 4.1.1 → 4.3.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.
- package/README.md +28 -3
- package/dist/claude.d.ts +4 -0
- package/dist/claude.d.ts.map +1 -1
- package/dist/claude.js +13 -1
- package/dist/claude.js.map +1 -1
- package/dist/cli/ui/components/App.d.ts.map +1 -1
- package/dist/cli/ui/components/App.js +68 -68
- package/dist/cli/ui/components/App.js.map +1 -1
- package/dist/cli/ui/components/common/Select.d.ts +3 -1
- package/dist/cli/ui/components/common/Select.d.ts.map +1 -1
- package/dist/cli/ui/components/common/Select.js +13 -2
- package/dist/cli/ui/components/common/Select.js.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.js +6 -1
- package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
- package/dist/client/assets/index-ChHC-Puh.css +1 -0
- package/dist/client/assets/index-PqK9jkug.js +78 -0
- package/dist/client/index.html +2 -2
- package/dist/config/builtin-tools.d.ts.map +1 -1
- package/dist/config/builtin-tools.js +3 -0
- package/dist/config/builtin-tools.js.map +1 -1
- package/dist/config/tools.d.ts.map +1 -1
- package/dist/config/tools.js +10 -1
- package/dist/config/tools.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +37 -4
- package/dist/index.js.map +1 -1
- package/dist/launcher.d.ts.map +1 -1
- package/dist/launcher.js +15 -0
- package/dist/launcher.js.map +1 -1
- package/dist/services/aiToolResolver.d.ts.map +1 -1
- package/dist/services/aiToolResolver.js +55 -8
- package/dist/services/aiToolResolver.js.map +1 -1
- package/dist/services/customToolResolver.d.ts.map +1 -1
- package/dist/services/customToolResolver.js +22 -17
- package/dist/services/customToolResolver.js.map +1 -1
- package/dist/utils/prompt.d.ts +12 -0
- package/dist/utils/prompt.d.ts.map +1 -1
- package/dist/utils/prompt.js +60 -10
- package/dist/utils/prompt.js.map +1 -1
- package/dist/utils/webui.js +1 -1
- package/dist/web/client/src/components/BranchGraph.d.ts +5 -0
- package/dist/web/client/src/components/BranchGraph.d.ts.map +1 -1
- package/dist/web/client/src/components/BranchGraph.js +35 -108
- package/dist/web/client/src/components/BranchGraph.js.map +1 -1
- package/dist/web/client/src/components/graph/BranchDetailPanel.d.ts +15 -0
- package/dist/web/client/src/components/graph/BranchDetailPanel.d.ts.map +1 -0
- package/dist/web/client/src/components/graph/BranchDetailPanel.js +57 -0
- package/dist/web/client/src/components/graph/BranchDetailPanel.js.map +1 -0
- package/dist/web/client/src/components/graph/BranchNode.d.ts +13 -0
- package/dist/web/client/src/components/graph/BranchNode.d.ts.map +1 -0
- package/dist/web/client/src/components/graph/BranchNode.js +103 -0
- package/dist/web/client/src/components/graph/BranchNode.js.map +1 -0
- package/dist/web/client/src/components/graph/ClusterNode.d.ts +13 -0
- package/dist/web/client/src/components/graph/ClusterNode.d.ts.map +1 -0
- package/dist/web/client/src/components/graph/ClusterNode.js +109 -0
- package/dist/web/client/src/components/graph/ClusterNode.js.map +1 -0
- package/dist/web/client/src/components/graph/SynapticCanvas.d.ts +17 -0
- package/dist/web/client/src/components/graph/SynapticCanvas.d.ts.map +1 -0
- package/dist/web/client/src/components/graph/SynapticCanvas.js +94 -0
- package/dist/web/client/src/components/graph/SynapticCanvas.js.map +1 -0
- package/dist/web/client/src/components/graph/SynapticEdge.d.ts +13 -0
- package/dist/web/client/src/components/graph/SynapticEdge.d.ts.map +1 -0
- package/dist/web/client/src/components/graph/SynapticEdge.js +113 -0
- package/dist/web/client/src/components/graph/SynapticEdge.js.map +1 -0
- package/dist/web/client/src/components/graph/graphUtils.d.ts +67 -0
- package/dist/web/client/src/components/graph/graphUtils.d.ts.map +1 -0
- package/dist/web/client/src/components/graph/graphUtils.js +175 -0
- package/dist/web/client/src/components/graph/graphUtils.js.map +1 -0
- package/dist/web/client/src/components/graph/index.d.ts +10 -0
- package/dist/web/client/src/components/graph/index.d.ts.map +1 -0
- package/dist/web/client/src/components/graph/index.js +10 -0
- package/dist/web/client/src/components/graph/index.js.map +1 -0
- package/dist/web/client/src/lib/websocket.d.ts.map +1 -1
- package/dist/web/client/src/lib/websocket.js +2 -1
- package/dist/web/client/src/lib/websocket.js.map +1 -1
- package/dist/web/client/vite.config.js +1 -1
- package/dist/web/server/env/importer.d.ts.map +1 -1
- package/dist/web/server/env/importer.js +4 -0
- package/dist/web/server/env/importer.js.map +1 -1
- package/dist/web/server/index.d.ts.map +1 -1
- package/dist/web/server/index.js +9 -0
- package/dist/web/server/index.js.map +1 -1
- package/dist/web/server/pty/manager.d.ts.map +1 -1
- package/dist/web/server/pty/manager.js +24 -1
- package/dist/web/server/pty/manager.js.map +1 -1
- package/dist/web/server/routes/sessions.d.ts.map +1 -1
- package/dist/web/server/routes/sessions.js +7 -0
- package/dist/web/server/routes/sessions.js.map +1 -1
- package/dist/web/server/tray.d.ts +1 -1
- package/dist/web/server/tray.d.ts.map +1 -1
- package/dist/web/server/tray.js +52 -34
- package/dist/web/server/tray.js.map +1 -1
- package/dist/web/server/websocket/handler.d.ts.map +1 -1
- package/dist/web/server/websocket/handler.js +4 -0
- package/dist/web/server/websocket/handler.js.map +1 -1
- package/package.json +6 -3
- package/src/claude.ts +15 -1
- package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +2 -1
- package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +142 -8
- package/src/cli/ui/__tests__/components/App.test.tsx +4 -3
- package/src/cli/ui/__tests__/components/ModelSelectorScreen.initial.test.tsx +1 -0
- package/src/cli/ui/__tests__/components/common/Select.test.tsx +45 -0
- package/src/cli/ui/components/App.tsx +91 -81
- package/src/cli/ui/components/common/Select.tsx +14 -1
- package/src/cli/ui/components/screens/BranchListScreen.tsx +6 -1
- package/src/cli/ui/types.ts +1 -1
- package/src/config/builtin-tools.ts +3 -0
- package/src/config/tools.ts +24 -1
- package/src/index.ts +50 -3
- package/src/launcher.ts +26 -0
- package/src/services/aiToolResolver.ts +75 -9
- package/src/services/customToolResolver.ts +32 -17
- package/src/utils/__tests__/prompt.test.ts +72 -35
- package/src/utils/prompt.ts +79 -10
- package/src/utils/webui.ts +1 -1
- package/src/web/client/src/components/BranchGraph.tsx +51 -208
- package/src/web/client/src/components/graph/BranchDetailPanel.tsx +152 -0
- package/src/web/client/src/components/graph/BranchNode.tsx +200 -0
- package/src/web/client/src/components/graph/ClusterNode.tsx +211 -0
- package/src/web/client/src/components/graph/SynapticCanvas.tsx +171 -0
- package/src/web/client/src/components/graph/SynapticEdge.tsx +311 -0
- package/src/web/client/src/components/graph/graphUtils.ts +265 -0
- package/src/web/client/src/components/graph/index.ts +10 -0
- package/src/web/client/src/index.css +314 -29
- package/src/web/client/src/lib/websocket.ts +2 -1
- package/src/web/client/vite.config.ts +1 -1
- package/src/web/server/env/importer.ts +5 -0
- package/src/web/server/index.ts +10 -0
- package/src/web/server/pty/manager.ts +43 -1
- package/src/web/server/routes/sessions.ts +15 -0
- package/src/web/server/tray.ts +62 -46
- package/src/web/server/websocket/handler.ts +13 -0
- package/dist/client/assets/index-DsDNCy5f.css +0 -1
- package/dist/client/assets/index-v8smkNOL.js +0 -72
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import { execa } from "execa";
|
|
2
2
|
import { platform } from "os";
|
|
3
3
|
import { getToolById } from "../config/tools.js";
|
|
4
|
+
import { CLAUDE_CODE_TOOL } from "../config/builtin-tools.js";
|
|
4
5
|
import {
|
|
5
6
|
CODEX_DEFAULT_ARGS,
|
|
6
7
|
CLAUDE_PERMISSION_SKIP_ARGS,
|
|
7
8
|
} from "../shared/aiToolConstants.js";
|
|
8
9
|
import { prepareCustomToolExecution } from "./customToolResolver.js";
|
|
9
10
|
import type { LaunchOptions } from "../types/tools.js";
|
|
11
|
+
import { createLogger } from "../logging/logger.js";
|
|
12
|
+
|
|
13
|
+
const logger = createLogger({ category: "resolver" });
|
|
10
14
|
|
|
11
15
|
const DETECTION_COMMAND = platform() === "win32" ? "where" : "which";
|
|
12
16
|
const MIN_BUN_MAJOR = 1;
|
|
@@ -41,12 +45,32 @@ export class AIToolResolutionError extends Error {
|
|
|
41
45
|
async function commandExists(command: string): Promise<boolean> {
|
|
42
46
|
try {
|
|
43
47
|
await execa(DETECTION_COMMAND, [command], { shell: true });
|
|
48
|
+
logger.debug({ command, exists: true }, "Command check");
|
|
44
49
|
return true;
|
|
45
50
|
} catch {
|
|
51
|
+
logger.debug({ command, exists: false }, "Command check");
|
|
46
52
|
return false;
|
|
47
53
|
}
|
|
48
54
|
}
|
|
49
55
|
|
|
56
|
+
/**
|
|
57
|
+
* コマンドのフルパスを取得
|
|
58
|
+
* node-ptyはシェルを経由しないため、フルパスが必要
|
|
59
|
+
*/
|
|
60
|
+
async function resolveCommandPath(command: string): Promise<string | null> {
|
|
61
|
+
try {
|
|
62
|
+
const { stdout } = await execa(DETECTION_COMMAND, [command], {
|
|
63
|
+
shell: true,
|
|
64
|
+
});
|
|
65
|
+
const fullPath = stdout.trim().split("\n")[0];
|
|
66
|
+
logger.debug({ command, fullPath }, "Command path resolved");
|
|
67
|
+
return fullPath || null;
|
|
68
|
+
} catch {
|
|
69
|
+
logger.debug({ command, fullPath: null }, "Command path resolution failed");
|
|
70
|
+
return null;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
50
74
|
let bunxCheckPromise: Promise<void> | null = null;
|
|
51
75
|
|
|
52
76
|
async function ensureBunxAvailable(): Promise<void> {
|
|
@@ -67,6 +91,7 @@ async function ensureBunxAvailable(): Promise<void> {
|
|
|
67
91
|
try {
|
|
68
92
|
const { stdout } = await execa("bun", ["--version"]);
|
|
69
93
|
const version = stdout.trim();
|
|
94
|
+
logger.debug({ bunVersion: version }, "Bun version detected");
|
|
70
95
|
const major = parseInt(version.split(".")[0] ?? "0", 10);
|
|
71
96
|
if (!Number.isFinite(major) || major < MIN_BUN_MAJOR) {
|
|
72
97
|
throw new AIToolResolutionError(
|
|
@@ -144,20 +169,40 @@ export async function resolveClaudeCommand(
|
|
|
144
169
|
options: ClaudeCommandOptions = {},
|
|
145
170
|
): Promise<ResolvedCommand> {
|
|
146
171
|
const args = buildClaudeArgs(options);
|
|
147
|
-
|
|
148
|
-
|
|
172
|
+
const envOverrides = CLAUDE_CODE_TOOL.env
|
|
173
|
+
? { env: { ...CLAUDE_CODE_TOOL.env } as NodeJS.ProcessEnv }
|
|
174
|
+
: {};
|
|
175
|
+
|
|
176
|
+
// フルパスを取得(node-ptyはシェルを経由しないため必要)
|
|
177
|
+
const claudePath = await resolveCommandPath("claude");
|
|
178
|
+
if (claudePath) {
|
|
179
|
+
logger.info(
|
|
180
|
+
{ command: claudePath, usesFallback: false },
|
|
181
|
+
"Claude command resolved",
|
|
182
|
+
);
|
|
149
183
|
return {
|
|
150
|
-
command:
|
|
184
|
+
command: claudePath,
|
|
151
185
|
args,
|
|
152
186
|
usesFallback: false,
|
|
187
|
+
...envOverrides,
|
|
153
188
|
};
|
|
154
189
|
}
|
|
155
190
|
|
|
156
|
-
|
|
191
|
+
// bunxへフォールバック
|
|
192
|
+
const bunxPath = await resolveCommandPath("bunx");
|
|
193
|
+
if (!bunxPath) {
|
|
194
|
+
await ensureBunxAvailable(); // エラーをスローする
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
logger.info(
|
|
198
|
+
{ command: bunxPath ?? "bunx", usesFallback: true },
|
|
199
|
+
"Claude command resolved (fallback)",
|
|
200
|
+
);
|
|
157
201
|
return {
|
|
158
|
-
command: "bunx",
|
|
202
|
+
command: bunxPath ?? "bunx",
|
|
159
203
|
args: [CLAUDE_CLI_PACKAGE, ...args],
|
|
160
204
|
usesFallback: true,
|
|
205
|
+
...envOverrides,
|
|
161
206
|
};
|
|
162
207
|
}
|
|
163
208
|
|
|
@@ -198,17 +243,32 @@ export async function resolveCodexCommand(
|
|
|
198
243
|
): Promise<ResolvedCommand> {
|
|
199
244
|
const args = buildCodexArgs(options);
|
|
200
245
|
|
|
201
|
-
|
|
246
|
+
// フルパスを取得(node-ptyはシェルを経由しないため必要)
|
|
247
|
+
const codexPath = await resolveCommandPath("codex");
|
|
248
|
+
if (codexPath) {
|
|
249
|
+
logger.info(
|
|
250
|
+
{ command: codexPath, usesFallback: false },
|
|
251
|
+
"Codex command resolved",
|
|
252
|
+
);
|
|
202
253
|
return {
|
|
203
|
-
command:
|
|
254
|
+
command: codexPath,
|
|
204
255
|
args,
|
|
205
256
|
usesFallback: false,
|
|
206
257
|
};
|
|
207
258
|
}
|
|
208
259
|
|
|
209
|
-
|
|
260
|
+
// bunxへフォールバック
|
|
261
|
+
const bunxPath = await resolveCommandPath("bunx");
|
|
262
|
+
if (!bunxPath) {
|
|
263
|
+
await ensureBunxAvailable(); // エラーをスローする
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
logger.info(
|
|
267
|
+
{ command: bunxPath ?? "bunx", usesFallback: true },
|
|
268
|
+
"Codex command resolved (fallback)",
|
|
269
|
+
);
|
|
210
270
|
return {
|
|
211
|
-
command: "bunx",
|
|
271
|
+
command: bunxPath ?? "bunx",
|
|
212
272
|
args: [CODEX_CLI_PACKAGE, ...args],
|
|
213
273
|
usesFallback: true,
|
|
214
274
|
};
|
|
@@ -223,6 +283,7 @@ export async function resolveCustomToolCommand(
|
|
|
223
283
|
): Promise<ResolvedCommand> {
|
|
224
284
|
const tool = await getToolById(options.toolId);
|
|
225
285
|
if (!tool) {
|
|
286
|
+
logger.error({ toolId: options.toolId }, "Custom tool not found");
|
|
226
287
|
throw new AIToolResolutionError(
|
|
227
288
|
"CUSTOM_TOOL_NOT_FOUND",
|
|
228
289
|
`Custom tool not found: ${options.toolId}`,
|
|
@@ -235,6 +296,11 @@ export async function resolveCustomToolCommand(
|
|
|
235
296
|
|
|
236
297
|
const execution = await prepareCustomToolExecution(tool, options);
|
|
237
298
|
|
|
299
|
+
logger.info(
|
|
300
|
+
{ toolId: options.toolId, command: execution.command },
|
|
301
|
+
"Custom tool command resolved",
|
|
302
|
+
);
|
|
303
|
+
|
|
238
304
|
return {
|
|
239
305
|
command: execution.command,
|
|
240
306
|
args: execution.args,
|
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { execa } from "execa";
|
|
2
2
|
import type { CustomAITool, LaunchOptions } from "../types/tools.js";
|
|
3
|
+
import { createLogger } from "../logging/logger.js";
|
|
4
|
+
|
|
5
|
+
const logger = createLogger({ category: "custom-resolver" });
|
|
3
6
|
|
|
4
7
|
export interface CustomToolExecutionPlan {
|
|
5
8
|
command: string;
|
|
@@ -21,6 +24,7 @@ export async function resolveCommandPath(commandName: string): Promise<string> {
|
|
|
21
24
|
);
|
|
22
25
|
}
|
|
23
26
|
|
|
27
|
+
logger.debug({ commandName, resolvedPath }, "Command path resolved");
|
|
24
28
|
return resolvedPath;
|
|
25
29
|
} catch (error) {
|
|
26
30
|
const reason = error instanceof Error ? error.message : String(error);
|
|
@@ -55,6 +59,10 @@ export function buildCustomToolArgs(
|
|
|
55
59
|
args.push(...options.extraArgs);
|
|
56
60
|
}
|
|
57
61
|
|
|
62
|
+
logger.debug(
|
|
63
|
+
{ toolId: tool.id, argsCount: args.length },
|
|
64
|
+
"Custom tool args built",
|
|
65
|
+
);
|
|
58
66
|
return args;
|
|
59
67
|
}
|
|
60
68
|
|
|
@@ -62,37 +70,44 @@ export async function prepareCustomToolExecution(
|
|
|
62
70
|
tool: CustomAITool,
|
|
63
71
|
options: LaunchOptions = {},
|
|
64
72
|
): Promise<CustomToolExecutionPlan> {
|
|
65
|
-
const
|
|
73
|
+
const baseArgs = buildCustomToolArgs(tool, options);
|
|
66
74
|
const envOverrides: NodeJS.ProcessEnv | undefined = tool.env
|
|
67
75
|
? ({ ...tool.env } as NodeJS.ProcessEnv)
|
|
68
76
|
: undefined;
|
|
69
77
|
|
|
78
|
+
let command: string;
|
|
79
|
+
let args: string[];
|
|
80
|
+
|
|
70
81
|
switch (tool.type) {
|
|
71
82
|
case "path": {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
...(envOverrides ? { env: envOverrides } : {}),
|
|
76
|
-
};
|
|
83
|
+
command = tool.command;
|
|
84
|
+
args = baseArgs;
|
|
85
|
+
break;
|
|
77
86
|
}
|
|
78
87
|
case "bunx": {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
...(envOverrides ? { env: envOverrides } : {}),
|
|
83
|
-
};
|
|
88
|
+
command = "bunx";
|
|
89
|
+
args = [tool.command, ...baseArgs];
|
|
90
|
+
break;
|
|
84
91
|
}
|
|
85
92
|
case "command": {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
args,
|
|
90
|
-
...(envOverrides ? { env: envOverrides } : {}),
|
|
91
|
-
};
|
|
93
|
+
command = await resolveCommandPath(tool.command);
|
|
94
|
+
args = baseArgs;
|
|
95
|
+
break;
|
|
92
96
|
}
|
|
93
97
|
default: {
|
|
94
98
|
const exhaustive: never = tool.type;
|
|
95
99
|
throw new Error(`Unknown custom tool type: ${exhaustive as string}`);
|
|
96
100
|
}
|
|
97
101
|
}
|
|
102
|
+
|
|
103
|
+
logger.debug(
|
|
104
|
+
{ toolId: tool.id, toolType: tool.type, command, hasEnv: !!envOverrides },
|
|
105
|
+
"Custom tool execution prepared",
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
command,
|
|
110
|
+
args,
|
|
111
|
+
...(envOverrides ? { env: envOverrides } : {}),
|
|
112
|
+
};
|
|
98
113
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { PassThrough } from "node:stream";
|
|
2
|
-
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { describe, expect, it, vi, beforeEach } from "vitest";
|
|
3
3
|
|
|
4
4
|
// Shared mock target to avoid hoisting issues
|
|
5
5
|
const terminalStreams: Record<string, unknown> = {};
|
|
@@ -16,16 +16,32 @@ const withTimeout = <T>(promise: Promise<T>, ms = 500): Promise<T> =>
|
|
|
16
16
|
),
|
|
17
17
|
]);
|
|
18
18
|
|
|
19
|
+
const resetTerminalStreams = () => {
|
|
20
|
+
vi.resetModules();
|
|
21
|
+
for (const key of Object.keys(terminalStreams)) {
|
|
22
|
+
delete terminalStreams[key];
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const setupTerminalStreams = (isTTY: boolean) => {
|
|
27
|
+
const stdin = new PassThrough() as unknown as NodeJS.ReadStream;
|
|
28
|
+
const stdout = new PassThrough() as unknown as NodeJS.WriteStream;
|
|
29
|
+
Object.defineProperty(stdin, "isTTY", { value: isTTY, configurable: true });
|
|
30
|
+
const exitRawMode = vi.fn();
|
|
31
|
+
Object.assign(terminalStreams, {
|
|
32
|
+
stdin,
|
|
33
|
+
stdout,
|
|
34
|
+
stderr: stdout,
|
|
35
|
+
usingFallback: false,
|
|
36
|
+
exitRawMode,
|
|
37
|
+
});
|
|
38
|
+
return { stdin, stdout, exitRawMode };
|
|
39
|
+
};
|
|
40
|
+
|
|
19
41
|
describe("waitForEnter", () => {
|
|
20
42
|
it("uses terminal stdin/stdout and resolves after newline on TTY", async () => {
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
delete terminalStreams[key];
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
const stdin = new PassThrough() as unknown as NodeJS.ReadStream;
|
|
27
|
-
const stdout = new PassThrough() as unknown as NodeJS.WriteStream;
|
|
28
|
-
Object.defineProperty(stdin, "isTTY", { value: true });
|
|
43
|
+
resetTerminalStreams();
|
|
44
|
+
const { stdin, exitRawMode } = setupTerminalStreams(true);
|
|
29
45
|
|
|
30
46
|
let resumed = false;
|
|
31
47
|
let paused = false;
|
|
@@ -41,16 +57,6 @@ describe("waitForEnter", () => {
|
|
|
41
57
|
return originalPause();
|
|
42
58
|
}) as typeof stdin.pause;
|
|
43
59
|
|
|
44
|
-
const exitRawMode = vi.fn();
|
|
45
|
-
|
|
46
|
-
Object.assign(terminalStreams, {
|
|
47
|
-
stdin,
|
|
48
|
-
stdout,
|
|
49
|
-
stderr: stdout,
|
|
50
|
-
usingFallback: false,
|
|
51
|
-
exitRawMode,
|
|
52
|
-
});
|
|
53
|
-
|
|
54
60
|
const { waitForEnter } = await import("../prompt.js");
|
|
55
61
|
|
|
56
62
|
const waiting = withTimeout(waitForEnter("prompt"), 200);
|
|
@@ -63,22 +69,8 @@ describe("waitForEnter", () => {
|
|
|
63
69
|
});
|
|
64
70
|
|
|
65
71
|
it("returns immediately on non-TTY stdin", async () => {
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
delete terminalStreams[key];
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
const stdin = new PassThrough() as unknown as NodeJS.ReadStream;
|
|
72
|
-
const stdout = new PassThrough() as unknown as NodeJS.WriteStream;
|
|
73
|
-
Object.defineProperty(stdin, "isTTY", { value: false });
|
|
74
|
-
|
|
75
|
-
Object.assign(terminalStreams, {
|
|
76
|
-
stdin,
|
|
77
|
-
stdout,
|
|
78
|
-
stderr: stdout,
|
|
79
|
-
usingFallback: false,
|
|
80
|
-
exitRawMode: vi.fn(),
|
|
81
|
-
});
|
|
72
|
+
resetTerminalStreams();
|
|
73
|
+
setupTerminalStreams(false);
|
|
82
74
|
|
|
83
75
|
const { waitForEnter } = await import("../prompt.js");
|
|
84
76
|
|
|
@@ -87,3 +79,48 @@ describe("waitForEnter", () => {
|
|
|
87
79
|
expect(Date.now() - start).toBeLessThan(50);
|
|
88
80
|
});
|
|
89
81
|
});
|
|
82
|
+
|
|
83
|
+
describe("confirmYesNo", () => {
|
|
84
|
+
let stdin: NodeJS.ReadStream;
|
|
85
|
+
let exitRawMode: ReturnType<typeof vi.fn>;
|
|
86
|
+
|
|
87
|
+
beforeEach(() => {
|
|
88
|
+
resetTerminalStreams();
|
|
89
|
+
const setup = setupTerminalStreams(true);
|
|
90
|
+
stdin = setup.stdin;
|
|
91
|
+
exitRawMode = setup.exitRawMode;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("resolves true when user inputs y on TTY", async () => {
|
|
95
|
+
const { confirmYesNo } = await import("../prompt.js");
|
|
96
|
+
|
|
97
|
+
const waiting = withTimeout(confirmYesNo("push?"), 200);
|
|
98
|
+
stdin.write("y\n");
|
|
99
|
+
|
|
100
|
+
await expect(waiting).resolves.toBe(true);
|
|
101
|
+
expect(exitRawMode).toHaveBeenCalled();
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("uses default value when input is empty on TTY", async () => {
|
|
105
|
+
const { confirmYesNo } = await import("../prompt.js");
|
|
106
|
+
|
|
107
|
+
const waiting = withTimeout(
|
|
108
|
+
confirmYesNo("push?", { defaultValue: true }),
|
|
109
|
+
200,
|
|
110
|
+
);
|
|
111
|
+
stdin.write("\n");
|
|
112
|
+
|
|
113
|
+
await expect(waiting).resolves.toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("returns default immediately on non-TTY stdin", async () => {
|
|
117
|
+
Object.defineProperty(stdin, "isTTY", { value: false });
|
|
118
|
+
|
|
119
|
+
const { confirmYesNo } = await import("../prompt.js");
|
|
120
|
+
|
|
121
|
+
const start = Date.now();
|
|
122
|
+
const result = await confirmYesNo("push?", { defaultValue: false });
|
|
123
|
+
expect(result).toBe(false);
|
|
124
|
+
expect(Date.now() - start).toBeLessThan(50);
|
|
125
|
+
});
|
|
126
|
+
});
|
package/src/utils/prompt.ts
CHANGED
|
@@ -1,17 +1,26 @@
|
|
|
1
1
|
import readline from "node:readline";
|
|
2
2
|
import { getTerminalStreams } from "./terminal.js";
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
type ReadlinePromptOptions<T> = {
|
|
5
|
+
fallback: T;
|
|
6
|
+
onAnswer: (answer: string) => T | undefined;
|
|
7
|
+
shouldSkip?: (terminal: ReturnType<typeof getTerminalStreams>) => boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
async function runReadlinePrompt<T>(
|
|
11
|
+
promptText: string,
|
|
12
|
+
{ fallback, onAnswer, shouldSkip }: ReadlinePromptOptions<T>,
|
|
13
|
+
): Promise<T> {
|
|
9
14
|
const terminal = getTerminalStreams();
|
|
10
15
|
const stdin = terminal.stdin as NodeJS.ReadStream | undefined;
|
|
11
16
|
const stdout = terminal.stdout as NodeJS.WriteStream | undefined;
|
|
12
17
|
|
|
13
18
|
if (!stdin || typeof stdin.on !== "function" || !stdin.isTTY) {
|
|
14
|
-
return;
|
|
19
|
+
return fallback;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (shouldSkip?.(terminal)) {
|
|
23
|
+
return fallback;
|
|
15
24
|
}
|
|
16
25
|
|
|
17
26
|
terminal.exitRawMode?.();
|
|
@@ -30,10 +39,15 @@ export async function waitForEnter(promptMessage: string): Promise<void> {
|
|
|
30
39
|
}
|
|
31
40
|
}
|
|
32
41
|
|
|
33
|
-
|
|
42
|
+
return new Promise<T>((resolve) => {
|
|
34
43
|
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
44
|
+
let finished = false;
|
|
35
45
|
|
|
36
46
|
const cleanup = () => {
|
|
47
|
+
if (finished) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
finished = true;
|
|
37
51
|
rl.removeAllListeners();
|
|
38
52
|
rl.close();
|
|
39
53
|
const remover = (method: "off" | "removeListener") =>
|
|
@@ -61,7 +75,7 @@ export async function waitForEnter(promptMessage: string): Promise<void> {
|
|
|
61
75
|
|
|
62
76
|
const onEnd = () => {
|
|
63
77
|
cleanup();
|
|
64
|
-
resolve();
|
|
78
|
+
resolve(fallback);
|
|
65
79
|
};
|
|
66
80
|
|
|
67
81
|
rl.on("SIGINT", () => {
|
|
@@ -69,12 +83,67 @@ export async function waitForEnter(promptMessage: string): Promise<void> {
|
|
|
69
83
|
process.exit(0);
|
|
70
84
|
});
|
|
71
85
|
|
|
72
|
-
rl.question(`${
|
|
86
|
+
rl.question(`${promptText}\n`, (answer) => {
|
|
87
|
+
const result = onAnswer(answer);
|
|
73
88
|
cleanup();
|
|
74
|
-
resolve();
|
|
89
|
+
resolve(result !== undefined ? result : fallback);
|
|
75
90
|
});
|
|
76
91
|
|
|
77
92
|
stdin.once("end", onEnd);
|
|
78
93
|
stdin.once("error", onEnd);
|
|
79
94
|
});
|
|
80
95
|
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Wait for Enter using the same terminal streams as Ink.
|
|
99
|
+
* Falls back to no-op on non-interactive stdin to avoid blocking pipelines.
|
|
100
|
+
*/
|
|
101
|
+
export async function waitForEnter(promptMessage: string): Promise<void> {
|
|
102
|
+
await runReadlinePrompt(promptMessage, {
|
|
103
|
+
fallback: undefined,
|
|
104
|
+
onAnswer: () => undefined,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Prompts the user for a yes/no confirmation in the terminal.
|
|
110
|
+
* Falls back to the default value on non-interactive stdin or fallback terminals.
|
|
111
|
+
*
|
|
112
|
+
* @param promptMessage - The message to display to the user
|
|
113
|
+
* @param options - Configuration options
|
|
114
|
+
* @param options.defaultValue - The default value when input is empty or stdin is non-interactive
|
|
115
|
+
* @returns A promise that resolves to true for yes, false for no
|
|
116
|
+
*/
|
|
117
|
+
export async function confirmYesNo(
|
|
118
|
+
promptMessage: string,
|
|
119
|
+
options: { defaultValue?: boolean } = {},
|
|
120
|
+
): Promise<boolean> {
|
|
121
|
+
const fallback = options.defaultValue ?? false;
|
|
122
|
+
|
|
123
|
+
const suffix =
|
|
124
|
+
options.defaultValue === undefined
|
|
125
|
+
? "[y/n]"
|
|
126
|
+
: options.defaultValue
|
|
127
|
+
? "[Y/n]"
|
|
128
|
+
: "[y/N]";
|
|
129
|
+
|
|
130
|
+
const promptText = `${promptMessage} ${suffix}`.trim();
|
|
131
|
+
|
|
132
|
+
return runReadlinePrompt(promptText, {
|
|
133
|
+
fallback,
|
|
134
|
+
shouldSkip: (terminal) => terminal.usingFallback,
|
|
135
|
+
onAnswer: (answer) => {
|
|
136
|
+
const normalized = answer.trim().toLowerCase();
|
|
137
|
+
if (normalized === "y" || normalized === "yes") {
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
if (normalized === "n" || normalized === "no") {
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
if (normalized.length === 0 && options.defaultValue !== undefined) {
|
|
144
|
+
return options.defaultValue;
|
|
145
|
+
}
|
|
146
|
+
return undefined;
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
}
|