@akiojin/gwt 4.1.0 → 4.2.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 (42) hide show
  1. package/README.md +28 -3
  2. package/dist/claude.d.ts +2 -0
  3. package/dist/claude.d.ts.map +1 -1
  4. package/dist/claude.js +2 -0
  5. package/dist/claude.js.map +1 -1
  6. package/dist/cli/ui/components/App.d.ts.map +1 -1
  7. package/dist/cli/ui/components/App.js +8 -5
  8. package/dist/cli/ui/components/App.js.map +1 -1
  9. package/dist/cli/ui/components/common/Select.d.ts +3 -1
  10. package/dist/cli/ui/components/common/Select.d.ts.map +1 -1
  11. package/dist/cli/ui/components/common/Select.js +13 -2
  12. package/dist/cli/ui/components/common/Select.js.map +1 -1
  13. package/dist/cli/ui/utils/modelOptions.d.ts +4 -0
  14. package/dist/cli/ui/utils/modelOptions.d.ts.map +1 -1
  15. package/dist/cli/ui/utils/modelOptions.js +19 -0
  16. package/dist/cli/ui/utils/modelOptions.js.map +1 -1
  17. package/dist/index.d.ts.map +1 -1
  18. package/dist/index.js +54 -15
  19. package/dist/index.js.map +1 -1
  20. package/dist/utils/prompt.d.ts +12 -0
  21. package/dist/utils/prompt.d.ts.map +1 -1
  22. package/dist/utils/prompt.js +60 -10
  23. package/dist/utils/prompt.js.map +1 -1
  24. package/dist/worktree.d.ts +14 -0
  25. package/dist/worktree.d.ts.map +1 -1
  26. package/dist/worktree.js +33 -2
  27. package/dist/worktree.js.map +1 -1
  28. package/package.json +2 -2
  29. package/src/claude.ts +2 -0
  30. package/src/cli/ui/__tests__/components/App.protected-branch.test.tsx +2 -1
  31. package/src/cli/ui/__tests__/components/App.shortcuts.test.tsx +38 -8
  32. package/src/cli/ui/__tests__/components/App.test.tsx +4 -3
  33. package/src/cli/ui/__tests__/components/ModelSelectorScreen.initial.test.tsx +1 -0
  34. package/src/cli/ui/__tests__/components/common/Select.test.tsx +45 -0
  35. package/src/cli/ui/components/App.tsx +15 -4
  36. package/src/cli/ui/components/common/Select.tsx +14 -1
  37. package/src/cli/ui/utils/modelOptions.test.ts +12 -0
  38. package/src/cli/ui/utils/modelOptions.ts +19 -0
  39. package/src/index.ts +70 -14
  40. package/src/utils/__tests__/prompt.test.ts +72 -35
  41. package/src/utils/prompt.ts +79 -10
  42. package/src/worktree.ts +48 -1
@@ -1,17 +1,26 @@
1
1
  import readline from "node:readline";
2
2
  import { getTerminalStreams } from "./terminal.js";
3
3
 
4
- /**
5
- * Wait for Enter using the same terminal streams as Ink.
6
- * Falls back to no-op on non-interactive stdin to avoid blocking pipelines.
7
- */
8
- export async function waitForEnter(promptMessage: string): Promise<void> {
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
- await new Promise<void>((resolve) => {
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(`${promptMessage}\n`, () => {
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
+ }
package/src/worktree.ts CHANGED
@@ -21,6 +21,7 @@ import {
21
21
  ensureGitignoreEntry,
22
22
  branchExists,
23
23
  getCurrentBranch,
24
+ getCurrentBranchName,
24
25
  } from "./git.js";
25
26
  import { getConfig } from "./config/index.js";
26
27
  import { GIT_CONFIG } from "./config/constants.js";
@@ -195,9 +196,55 @@ export async function listAdditionalWorktrees(): Promise<WorktreeInfo[]> {
195
196
  export async function worktreeExists(
196
197
  branchName: string,
197
198
  ): Promise<string | null> {
199
+ const resolution = await resolveWorktreePathForBranch(branchName);
200
+ return resolution.path;
201
+ }
202
+
203
+ /**
204
+ * Resolution result for a branch-associated worktree path.
205
+ */
206
+ export interface WorktreePathResolution {
207
+ path: string | null;
208
+ mismatch?: {
209
+ path: string;
210
+ actualBranch: string | null;
211
+ };
212
+ }
213
+
214
+ /**
215
+ * Resolve a worktree path for the selected branch and verify the actual checkout.
216
+ */
217
+ export async function resolveWorktreePathForBranch(
218
+ branchName: string,
219
+ ): Promise<WorktreePathResolution> {
198
220
  const worktrees = await listWorktrees();
199
221
  const worktree = worktrees.find((w) => w.branch === branchName);
200
- return worktree ? worktree.path : null;
222
+ if (!worktree) {
223
+ return { path: null };
224
+ }
225
+
226
+ try {
227
+ const actualBranch = (await getCurrentBranchName(worktree.path)).trim();
228
+ if (!actualBranch || actualBranch !== branchName) {
229
+ return {
230
+ path: null,
231
+ mismatch: {
232
+ path: worktree.path,
233
+ actualBranch: actualBranch || null,
234
+ },
235
+ };
236
+ }
237
+ } catch {
238
+ return {
239
+ path: null,
240
+ mismatch: {
241
+ path: worktree.path,
242
+ actualBranch: null,
243
+ },
244
+ };
245
+ }
246
+
247
+ return { path: worktree.path };
201
248
  }
202
249
 
203
250
  export async function generateWorktreePath(