@akiojin/gwt 2.12.0 → 2.13.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/dist/cli/ui/components/App.d.ts.map +1 -1
- package/dist/cli/ui/components/App.js +72 -3
- package/dist/cli/ui/components/App.js.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts +3 -1
- package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
- package/dist/cli/ui/components/screens/BranchListScreen.js +154 -32
- package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
- package/dist/cli/ui/hooks/useGitData.d.ts.map +1 -1
- package/dist/cli/ui/hooks/useGitData.js +17 -0
- package/dist/cli/ui/hooks/useGitData.js.map +1 -1
- package/dist/cli/ui/types.d.ts +2 -0
- package/dist/cli/ui/types.d.ts.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
- package/dist/cli/ui/utils/branchFormatter.js +7 -2
- package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
- package/dist/cli/ui/utils/modelOptions.d.ts.map +1 -1
- package/dist/cli/ui/utils/modelOptions.js +7 -0
- package/dist/cli/ui/utils/modelOptions.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -40
- package/dist/index.js.map +1 -1
- package/dist/logging/logger.d.ts +24 -0
- package/dist/logging/logger.d.ts.map +1 -0
- package/dist/logging/logger.js +57 -0
- package/dist/logging/logger.js.map +1 -0
- package/dist/logging/rotation.d.ts +6 -0
- package/dist/logging/rotation.d.ts.map +1 -0
- package/dist/logging/rotation.js +26 -0
- package/dist/logging/rotation.js.map +1 -0
- package/dist/utils/prompt.d.ts +6 -0
- package/dist/utils/prompt.d.ts.map +1 -0
- package/dist/utils/prompt.js +57 -0
- package/dist/utils/prompt.js.map +1 -0
- package/dist/web/server/index.d.ts.map +1 -1
- package/dist/web/server/index.js +3 -3
- package/dist/web/server/index.js.map +1 -1
- package/dist/web/server/routes/branches.d.ts +2 -2
- package/dist/web/server/routes/branches.d.ts.map +1 -1
- package/dist/web/server/routes/branches.js.map +1 -1
- package/dist/web/server/routes/config.d.ts +2 -2
- package/dist/web/server/routes/config.d.ts.map +1 -1
- package/dist/web/server/routes/config.js.map +1 -1
- package/dist/web/server/routes/index.d.ts +2 -2
- package/dist/web/server/routes/index.d.ts.map +1 -1
- package/dist/web/server/routes/index.js.map +1 -1
- package/dist/web/server/routes/sessions.d.ts +2 -2
- package/dist/web/server/routes/sessions.d.ts.map +1 -1
- package/dist/web/server/routes/sessions.js.map +1 -1
- package/dist/web/server/routes/worktrees.d.ts +2 -2
- package/dist/web/server/routes/worktrees.d.ts.map +1 -1
- package/dist/web/server/routes/worktrees.js.map +1 -1
- package/dist/web/server/types.d.ts +4 -0
- package/dist/web/server/types.d.ts.map +1 -0
- package/dist/web/server/types.js +2 -0
- package/dist/web/server/types.js.map +1 -0
- package/dist/worktree.d.ts +1 -0
- package/dist/worktree.d.ts.map +1 -1
- package/dist/worktree.js.map +1 -1
- package/package.json +4 -3
- package/src/cli/ui/__tests__/components/ModelSelectorScreen.initial.test.tsx +13 -13
- package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +81 -33
- package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +7 -3
- package/src/cli/ui/components/App.tsx +88 -2
- package/src/cli/ui/components/screens/BranchListScreen.tsx +198 -32
- package/src/cli/ui/hooks/useGitData.ts +20 -0
- package/src/cli/ui/types.ts +3 -0
- package/src/cli/ui/utils/branchFormatter.ts +7 -2
- package/src/cli/ui/utils/modelOptions.test.ts +14 -0
- package/src/cli/ui/utils/modelOptions.ts +7 -0
- package/src/index.ts +8 -45
- package/src/logging/logger.ts +79 -0
- package/src/logging/rotation.ts +25 -0
- package/src/utils/__tests__/prompt.test.ts +89 -0
- package/src/utils/prompt.ts +74 -0
- package/src/web/server/index.ts +6 -4
- package/src/web/server/routes/branches.ts +2 -2
- package/src/web/server/routes/config.ts +2 -2
- package/src/web/server/routes/index.ts +2 -2
- package/src/web/server/routes/sessions.ts +2 -2
- package/src/web/server/routes/worktrees.ts +2 -2
- package/src/web/server/types.ts +14 -0
- package/src/worktree.ts +1 -0
|
@@ -12,6 +12,7 @@ import { getPullRequestByBranch } from "../../../github.js";
|
|
|
12
12
|
import type { BranchInfo, WorktreeInfo } from "../types.js";
|
|
13
13
|
import type { WorktreeInfo as GitWorktreeInfo } from "../../../worktree.js";
|
|
14
14
|
import { getLastToolUsageMap } from "../../../config/index.js";
|
|
15
|
+
import { hasUncommittedChanges } from "../../../git.js";
|
|
15
16
|
|
|
16
17
|
export interface UseGitDataOptions {
|
|
17
18
|
enableAutoRefresh?: boolean;
|
|
@@ -65,6 +66,22 @@ export function useGitData(options?: UseGitDataOptions): UseGitDataResult {
|
|
|
65
66
|
}
|
|
66
67
|
worktreesData = [];
|
|
67
68
|
}
|
|
69
|
+
|
|
70
|
+
// enrich worktrees with uncommitted status (only for accessible paths)
|
|
71
|
+
worktreesData = await Promise.all(
|
|
72
|
+
worktreesData.map(async (wt) => {
|
|
73
|
+
if (wt.isAccessible === false) {
|
|
74
|
+
return wt;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const hasUncommitted = await hasUncommittedChanges(wt.path);
|
|
78
|
+
return { ...wt, hasUncommittedChanges: hasUncommitted };
|
|
79
|
+
} catch {
|
|
80
|
+
return wt;
|
|
81
|
+
}
|
|
82
|
+
}),
|
|
83
|
+
);
|
|
84
|
+
|
|
68
85
|
const lastToolUsageMap = await getLastToolUsageMap(repoRoot);
|
|
69
86
|
|
|
70
87
|
// upstream情報とdivergence情報を取得
|
|
@@ -109,6 +126,9 @@ export function useGitData(options?: UseGitDataOptions): UseGitDataResult {
|
|
|
109
126
|
locked: false, // worktree.ts doesn't expose locked status
|
|
110
127
|
prunable: worktree.isAccessible === false,
|
|
111
128
|
isAccessible: worktree.isAccessible ?? true, // Default to true if undefined
|
|
129
|
+
...(worktree.hasUncommittedChanges !== undefined
|
|
130
|
+
? { hasUncommittedChanges: worktree.hasUncommittedChanges }
|
|
131
|
+
: {}),
|
|
112
132
|
};
|
|
113
133
|
worktreeMap.set(worktree.branch, uiWorktreeInfo);
|
|
114
134
|
}
|
package/src/cli/ui/types.ts
CHANGED
|
@@ -6,6 +6,7 @@ export interface WorktreeInfo {
|
|
|
6
6
|
locked: boolean;
|
|
7
7
|
prunable: boolean;
|
|
8
8
|
isAccessible?: boolean;
|
|
9
|
+
hasUncommittedChanges?: boolean;
|
|
9
10
|
}
|
|
10
11
|
|
|
11
12
|
export type AITool = string;
|
|
@@ -229,6 +230,8 @@ export interface BranchItem extends BranchInfo {
|
|
|
229
230
|
syncStatus?: SyncStatus;
|
|
230
231
|
syncInfo?: string | undefined;
|
|
231
232
|
remoteName?: string | undefined;
|
|
233
|
+
// クリーンアップ判定で「未コミット/未プッシュなし」と評価された場合に true
|
|
234
|
+
safeToCleanup?: boolean;
|
|
232
235
|
}
|
|
233
236
|
|
|
234
237
|
/**
|
|
@@ -65,7 +65,8 @@ const iconWidthOverrides: Record<string, number> = {
|
|
|
65
65
|
"🚀": 1,
|
|
66
66
|
"📌": 1,
|
|
67
67
|
// Worktree status icons
|
|
68
|
-
"🟢":
|
|
68
|
+
"🟢": 2,
|
|
69
|
+
"⚪": 2,
|
|
69
70
|
"🟠": 1,
|
|
70
71
|
// Change status icons
|
|
71
72
|
"👉": 1,
|
|
@@ -73,7 +74,11 @@ const iconWidthOverrides: Record<string, number> = {
|
|
|
73
74
|
"📤": 1,
|
|
74
75
|
"🔃": 1,
|
|
75
76
|
"✅": 1,
|
|
76
|
-
"⚠️":
|
|
77
|
+
"⚠️": 2,
|
|
78
|
+
"⚠": 1,
|
|
79
|
+
"🛡": 2,
|
|
80
|
+
"☑": 2,
|
|
81
|
+
"☐": 2,
|
|
77
82
|
// Remote markers
|
|
78
83
|
"🔗": 1,
|
|
79
84
|
"💻": 1,
|
|
@@ -23,6 +23,7 @@ describe("modelOptions", () => {
|
|
|
23
23
|
expect(unique.size).toBe(ids.length);
|
|
24
24
|
expect(ids).toEqual([
|
|
25
25
|
"gpt-5.1-codex",
|
|
26
|
+
"gpt-5.2",
|
|
26
27
|
"gpt-5.1-codex-max",
|
|
27
28
|
"gpt-5.1-codex-mini",
|
|
28
29
|
"gpt-5.1",
|
|
@@ -36,6 +37,19 @@ describe("modelOptions", () => {
|
|
|
36
37
|
expect(getDefaultInferenceForModel(codexMax)).toBe("medium");
|
|
37
38
|
});
|
|
38
39
|
|
|
40
|
+
it("exposes gpt-5.2 with xhigh reasoning and medium default", () => {
|
|
41
|
+
const codex52 = getModelOptions("codex-cli").find(
|
|
42
|
+
(m) => m.id === "gpt-5.2",
|
|
43
|
+
);
|
|
44
|
+
expect(codex52?.inferenceLevels).toEqual([
|
|
45
|
+
"xhigh",
|
|
46
|
+
"high",
|
|
47
|
+
"medium",
|
|
48
|
+
"low",
|
|
49
|
+
]);
|
|
50
|
+
expect(getDefaultInferenceForModel(codex52)).toBe("medium");
|
|
51
|
+
});
|
|
52
|
+
|
|
39
53
|
it("lists expected Gemini models", () => {
|
|
40
54
|
expect(byId("gemini-cli")).toEqual([
|
|
41
55
|
"gemini-3-pro-preview",
|
|
@@ -33,6 +33,13 @@ const MODEL_OPTIONS: Record<string, ModelOption[]> = {
|
|
|
33
33
|
defaultInference: "high",
|
|
34
34
|
isDefault: true,
|
|
35
35
|
},
|
|
36
|
+
{
|
|
37
|
+
id: "gpt-5.2",
|
|
38
|
+
label: "gpt-5.2",
|
|
39
|
+
description: "Latest frontier model with extra high reasoning",
|
|
40
|
+
inferenceLevels: CODEX_MAX_LEVELS,
|
|
41
|
+
defaultInference: "medium",
|
|
42
|
+
},
|
|
36
43
|
{
|
|
37
44
|
id: "gpt-5.1-codex-max",
|
|
38
45
|
label: "gpt-5.1-codex-max",
|
package/src/index.ts
CHANGED
|
@@ -33,6 +33,7 @@ import {
|
|
|
33
33
|
getTerminalStreams,
|
|
34
34
|
waitForUserAcknowledgement,
|
|
35
35
|
} from "./utils/terminal.js";
|
|
36
|
+
import { createLogger } from "./logging/logger.js";
|
|
36
37
|
import { getToolById, getSharedEnvironment } from "./config/tools.js";
|
|
37
38
|
import { launchCustomAITool } from "./launcher.js";
|
|
38
39
|
import { saveSession, loadSession } from "./config/index.js";
|
|
@@ -43,18 +44,21 @@ import {
|
|
|
43
44
|
} from "./utils/session.js";
|
|
44
45
|
import { getPackageVersion } from "./utils.js";
|
|
45
46
|
import { findLatestClaudeSessionId } from "./utils/session.js";
|
|
46
|
-
import readline from "node:readline";
|
|
47
47
|
import { resolveContinueSessionId } from "./cli/ui/utils/continueSession.js";
|
|
48
48
|
import {
|
|
49
49
|
installDependenciesForWorktree,
|
|
50
50
|
DependencyInstallError,
|
|
51
51
|
type DependencyInstallResult,
|
|
52
52
|
} from "./services/dependency-installer.js";
|
|
53
|
+
import { waitForEnter } from "./utils/prompt.js";
|
|
53
54
|
|
|
54
55
|
const ERROR_PROMPT = chalk.yellow(
|
|
55
56
|
"Review the error details, then press Enter to continue.",
|
|
56
57
|
);
|
|
57
58
|
|
|
59
|
+
// Category: cli
|
|
60
|
+
const appLogger = createLogger({ category: "cli" });
|
|
61
|
+
|
|
58
62
|
async function waitForErrorAcknowledgement(): Promise<void> {
|
|
59
63
|
await waitForUserAcknowledgement(ERROR_PROMPT);
|
|
60
64
|
}
|
|
@@ -64,14 +68,17 @@ async function waitForErrorAcknowledgement(): Promise<void> {
|
|
|
64
68
|
*/
|
|
65
69
|
function printError(message: string): void {
|
|
66
70
|
console.error(chalk.red(`❌ ${message}`));
|
|
71
|
+
appLogger.error({ message });
|
|
67
72
|
}
|
|
68
73
|
|
|
69
74
|
function printInfo(message: string): void {
|
|
70
75
|
console.log(chalk.blue(`ℹ️ ${message}`));
|
|
76
|
+
appLogger.info({ message });
|
|
71
77
|
}
|
|
72
78
|
|
|
73
79
|
function printWarning(message: string): void {
|
|
74
80
|
console.warn(chalk.yellow(`⚠️ ${message}`));
|
|
81
|
+
appLogger.warn({ message });
|
|
75
82
|
}
|
|
76
83
|
|
|
77
84
|
type GitStepResult<T> = { ok: true; value: T } | { ok: false };
|
|
@@ -192,50 +199,6 @@ async function runDependencyInstallStep<T extends DependencyInstallResult>(
|
|
|
192
199
|
}
|
|
193
200
|
}
|
|
194
201
|
|
|
195
|
-
async function waitForEnter(promptMessage: string): Promise<void> {
|
|
196
|
-
if (!process.stdin.isTTY) {
|
|
197
|
-
// For non-interactive environments, resolve immediately.
|
|
198
|
-
return;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
// Ensure stdin is resumed and not in raw mode before using readline.
|
|
202
|
-
// This is crucial for environments where stdin might be paused or in raw mode
|
|
203
|
-
// by other libraries (like Ink.js).
|
|
204
|
-
if (typeof process.stdin.resume === "function") {
|
|
205
|
-
process.stdin.resume();
|
|
206
|
-
}
|
|
207
|
-
if (process.stdin.isRaw) {
|
|
208
|
-
process.stdin.setRawMode(false);
|
|
209
|
-
}
|
|
210
|
-
|
|
211
|
-
await new Promise<void>((resolve) => {
|
|
212
|
-
const rl = readline.createInterface({
|
|
213
|
-
input: process.stdin,
|
|
214
|
-
output: process.stdout,
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// Handle Ctrl+C to gracefully exit.
|
|
218
|
-
rl.on("SIGINT", () => {
|
|
219
|
-
rl.close();
|
|
220
|
-
// Restore stdin to a paused state before exiting.
|
|
221
|
-
if (typeof process.stdin.pause === "function") {
|
|
222
|
-
process.stdin.pause();
|
|
223
|
-
}
|
|
224
|
-
process.exit(0);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
rl.question(`${promptMessage}\n`, () => {
|
|
228
|
-
rl.close();
|
|
229
|
-
// Pause stdin again to allow other parts of the application
|
|
230
|
-
// to take control if needed.
|
|
231
|
-
if (typeof process.stdin.pause === "function") {
|
|
232
|
-
process.stdin.pause();
|
|
233
|
-
}
|
|
234
|
-
resolve();
|
|
235
|
-
});
|
|
236
|
-
});
|
|
237
|
-
}
|
|
238
|
-
|
|
239
202
|
function showHelp(): void {
|
|
240
203
|
console.log(`
|
|
241
204
|
Worktree Manager
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import os from "node:os";
|
|
4
|
+
import pino, { type LoggerOptions, type Logger } from "pino";
|
|
5
|
+
import { pruneOldLogs } from "./rotation.js";
|
|
6
|
+
|
|
7
|
+
type Category = "cli" | "server" | "worker" | string;
|
|
8
|
+
|
|
9
|
+
export interface LoggerConfig {
|
|
10
|
+
level?: string;
|
|
11
|
+
logDir?: string;
|
|
12
|
+
filename?: string;
|
|
13
|
+
category?: Category;
|
|
14
|
+
base?: Record<string, unknown>;
|
|
15
|
+
keepDays?: number;
|
|
16
|
+
/** For tests or sync writes use pino.destination sync */
|
|
17
|
+
sync?: boolean;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create a pino logger with unified structure and category field.
|
|
22
|
+
* - Writes to a single file stream (no per-component files)
|
|
23
|
+
* - Adds `category` to each log record
|
|
24
|
+
* - Prunes files older than keepDays at startup
|
|
25
|
+
*/
|
|
26
|
+
export function createLogger(config: LoggerConfig = {}): Logger {
|
|
27
|
+
const level = config.level ?? process.env.LOG_LEVEL ?? "info";
|
|
28
|
+
const cwdBase = path.basename(process.cwd()) || "workspace";
|
|
29
|
+
const defaultLogDir = path.join(os.homedir(), ".gwt", "logs", cwdBase);
|
|
30
|
+
const logDir = config.logDir ?? defaultLogDir;
|
|
31
|
+
const filename = config.filename ?? `${formatDate(new Date())}.jsonl`;
|
|
32
|
+
const category = config.category ?? "default";
|
|
33
|
+
const keepDays = config.keepDays ?? 7;
|
|
34
|
+
|
|
35
|
+
if (!fs.existsSync(logDir)) {
|
|
36
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Startup rotation
|
|
40
|
+
pruneOldLogs(logDir, keepDays);
|
|
41
|
+
|
|
42
|
+
const destination = path.join(logDir, filename);
|
|
43
|
+
|
|
44
|
+
const options: LoggerOptions = {
|
|
45
|
+
level,
|
|
46
|
+
base: {
|
|
47
|
+
category,
|
|
48
|
+
...(config.base ?? {}),
|
|
49
|
+
},
|
|
50
|
+
timestamp: pino.stdTimeFunctions.isoTime,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
if (config.sync) {
|
|
54
|
+
const destinationStream = pino.destination({ dest: destination, sync: true });
|
|
55
|
+
return pino(options, destinationStream);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const transport = pino.transport({
|
|
59
|
+
targets: [
|
|
60
|
+
{
|
|
61
|
+
target: "pino/file",
|
|
62
|
+
options: { destination, mkdir: true, append: true },
|
|
63
|
+
level,
|
|
64
|
+
},
|
|
65
|
+
],
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
return pino(options, transport);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Convenience logger for quick use (category defaults to "default"). */
|
|
72
|
+
export const logger = createLogger();
|
|
73
|
+
|
|
74
|
+
export function formatDate(date: Date): string {
|
|
75
|
+
const year = date.getFullYear();
|
|
76
|
+
const month = String(date.getMonth() + 1).padStart(2, "0");
|
|
77
|
+
const day = String(date.getDate()).padStart(2, "0");
|
|
78
|
+
return `${year}-${month}-${day}`;
|
|
79
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Delete log files older than `keepDays` from the given directory.
|
|
6
|
+
* This is called at startup to enforce 7-day retention without size limits.
|
|
7
|
+
*/
|
|
8
|
+
export function pruneOldLogs(logDir: string, keepDays = 7): void {
|
|
9
|
+
if (!fs.existsSync(logDir)) return;
|
|
10
|
+
|
|
11
|
+
const cutoff = Date.now() - keepDays * 24 * 60 * 60 * 1000;
|
|
12
|
+
|
|
13
|
+
for (const entry of fs.readdirSync(logDir)) {
|
|
14
|
+
const full = path.join(logDir, entry);
|
|
15
|
+
try {
|
|
16
|
+
const stat = fs.statSync(full);
|
|
17
|
+
if (!stat.isFile()) continue;
|
|
18
|
+
if (stat.mtime.getTime() < cutoff) {
|
|
19
|
+
fs.unlinkSync(full);
|
|
20
|
+
}
|
|
21
|
+
} catch {
|
|
22
|
+
// Ignore individual file errors to avoid breaking startup
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { PassThrough } from "node:stream";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
|
|
4
|
+
// Shared mock target to avoid hoisting issues
|
|
5
|
+
const terminalStreams: Record<string, unknown> = {};
|
|
6
|
+
|
|
7
|
+
vi.mock("../terminal.js", () => ({
|
|
8
|
+
getTerminalStreams: () => terminalStreams,
|
|
9
|
+
}));
|
|
10
|
+
|
|
11
|
+
const withTimeout = <T>(promise: Promise<T>, ms = 500): Promise<T> =>
|
|
12
|
+
Promise.race([
|
|
13
|
+
promise,
|
|
14
|
+
new Promise<T>((_, reject) =>
|
|
15
|
+
setTimeout(() => reject(new Error("timeout")), ms),
|
|
16
|
+
),
|
|
17
|
+
]);
|
|
18
|
+
|
|
19
|
+
describe("waitForEnter", () => {
|
|
20
|
+
it("uses terminal stdin/stdout and resolves after newline on TTY", async () => {
|
|
21
|
+
vi.resetModules();
|
|
22
|
+
for (const key of Object.keys(terminalStreams)) {
|
|
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 });
|
|
29
|
+
|
|
30
|
+
let resumed = false;
|
|
31
|
+
let paused = false;
|
|
32
|
+
const originalResume = stdin.resume.bind(stdin);
|
|
33
|
+
const originalPause = stdin.pause.bind(stdin);
|
|
34
|
+
// Track resume/pause calls
|
|
35
|
+
stdin.resume = (() => {
|
|
36
|
+
resumed = true;
|
|
37
|
+
return originalResume();
|
|
38
|
+
}) as typeof stdin.resume;
|
|
39
|
+
stdin.pause = (() => {
|
|
40
|
+
paused = true;
|
|
41
|
+
return originalPause();
|
|
42
|
+
}) as typeof stdin.pause;
|
|
43
|
+
|
|
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
|
+
const { waitForEnter } = await import("../prompt.js");
|
|
55
|
+
|
|
56
|
+
const waiting = withTimeout(waitForEnter("prompt"), 200);
|
|
57
|
+
stdin.write("hello\n");
|
|
58
|
+
|
|
59
|
+
await expect(waiting).resolves.toBeUndefined();
|
|
60
|
+
expect(resumed).toBe(true);
|
|
61
|
+
expect(paused).toBe(true);
|
|
62
|
+
expect(exitRawMode).toHaveBeenCalled();
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("returns immediately on non-TTY stdin", async () => {
|
|
66
|
+
vi.resetModules();
|
|
67
|
+
for (const key of Object.keys(terminalStreams)) {
|
|
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
|
+
});
|
|
82
|
+
|
|
83
|
+
const { waitForEnter } = await import("../prompt.js");
|
|
84
|
+
|
|
85
|
+
const start = Date.now();
|
|
86
|
+
await waitForEnter("prompt");
|
|
87
|
+
expect(Date.now() - start).toBeLessThan(50);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import readline from "node:readline";
|
|
2
|
+
import { getTerminalStreams } from "./terminal.js";
|
|
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> {
|
|
9
|
+
const terminal = getTerminalStreams();
|
|
10
|
+
const stdin = terminal.stdin as NodeJS.ReadStream | undefined;
|
|
11
|
+
const stdout = terminal.stdout as NodeJS.WriteStream | undefined;
|
|
12
|
+
|
|
13
|
+
if (!stdin || typeof stdin.on !== "function" || !stdin.isTTY) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
terminal.exitRawMode?.();
|
|
18
|
+
|
|
19
|
+
if (typeof stdin.resume === "function") {
|
|
20
|
+
stdin.resume();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if ((stdin as NodeJS.ReadStream & { isRaw?: boolean }).isRaw) {
|
|
24
|
+
try {
|
|
25
|
+
(stdin as NodeJS.ReadStream & { setRawMode?: (flag: boolean) => void }).setRawMode?.(false);
|
|
26
|
+
} catch {
|
|
27
|
+
// Ignore raw mode errors
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
await new Promise<void>((resolve) => {
|
|
32
|
+
const rl = readline.createInterface({ input: stdin, output: stdout });
|
|
33
|
+
|
|
34
|
+
const cleanup = () => {
|
|
35
|
+
rl.removeAllListeners();
|
|
36
|
+
rl.close();
|
|
37
|
+
const remover = (method: "off" | "removeListener") =>
|
|
38
|
+
(stdin as unknown as Record<string, (event: string, fn: () => void) => void>)[method]?.(
|
|
39
|
+
"end",
|
|
40
|
+
onEnd,
|
|
41
|
+
);
|
|
42
|
+
remover("off");
|
|
43
|
+
remover("removeListener");
|
|
44
|
+
const removerErr = (method: "off" | "removeListener") =>
|
|
45
|
+
(stdin as unknown as Record<string, (event: string, fn: () => void) => void>)[method]?.(
|
|
46
|
+
"error",
|
|
47
|
+
onEnd,
|
|
48
|
+
);
|
|
49
|
+
removerErr("off");
|
|
50
|
+
removerErr("removeListener");
|
|
51
|
+
if (typeof stdin.pause === "function") {
|
|
52
|
+
stdin.pause();
|
|
53
|
+
}
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const onEnd = () => {
|
|
57
|
+
cleanup();
|
|
58
|
+
resolve();
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
rl.on("SIGINT", () => {
|
|
62
|
+
cleanup();
|
|
63
|
+
process.exit(0);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
rl.question(`${promptMessage}\n`, () => {
|
|
67
|
+
cleanup();
|
|
68
|
+
resolve();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
stdin.once("end", onEnd);
|
|
72
|
+
stdin.once("error", onEnd);
|
|
73
|
+
});
|
|
74
|
+
}
|
package/src/web/server/index.ts
CHANGED
|
@@ -14,6 +14,8 @@ import { PTYManager } from "./pty/manager.js";
|
|
|
14
14
|
import { WebSocketHandler } from "./websocket/handler.js";
|
|
15
15
|
import { registerRoutes } from "./routes/index.js";
|
|
16
16
|
import { importOsEnvIntoSharedConfig } from "./env/importer.js";
|
|
17
|
+
import { createLogger } from "../../logging/logger.js";
|
|
18
|
+
import type { WebFastifyInstance } from "./types.js";
|
|
17
19
|
|
|
18
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
19
21
|
const __dirname = dirname(__filename);
|
|
@@ -22,10 +24,10 @@ const __dirname = dirname(__filename);
|
|
|
22
24
|
* Webサーバーを起動
|
|
23
25
|
*/
|
|
24
26
|
export async function startWebServer(): Promise<void> {
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
const serverLogger = createLogger({ category: "server" });
|
|
28
|
+
|
|
29
|
+
const fastify: WebFastifyInstance = Fastify({
|
|
30
|
+
loggerInstance: serverLogger,
|
|
29
31
|
});
|
|
30
32
|
|
|
31
33
|
// PTYマネージャーとWebSocketハンドラーを初期化
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* ブランチ関連のREST APIエンドポイント。
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { FastifyInstance } from "fastify";
|
|
8
7
|
import {
|
|
9
8
|
listBranches,
|
|
10
9
|
getBranchByName,
|
|
@@ -16,12 +15,13 @@ import type {
|
|
|
16
15
|
BranchSyncRequest,
|
|
17
16
|
BranchSyncResult,
|
|
18
17
|
} from "../../../types/api.js";
|
|
18
|
+
import type { WebFastifyInstance } from "../types.js";
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* ブランチ関連のルートを登録
|
|
22
22
|
*/
|
|
23
23
|
export async function registerBranchRoutes(
|
|
24
|
-
fastify:
|
|
24
|
+
fastify: WebFastifyInstance,
|
|
25
25
|
): Promise<void> {
|
|
26
26
|
// GET /api/branches - すべてのブランチ一覧を取得
|
|
27
27
|
fastify.get<{ Reply: ApiResponse<Branch[]> }>(
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
* Config Routes
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import type { FastifyInstance } from "fastify";
|
|
6
5
|
import { loadToolsConfig, saveToolsConfig } from "../../../config/tools.js";
|
|
7
6
|
import {
|
|
8
7
|
loadEnvHistory,
|
|
@@ -17,6 +16,7 @@ import type {
|
|
|
17
16
|
} from "../../../types/api.js";
|
|
18
17
|
import type { CustomAITool as FileCustomAITool } from "../../../types/tools.js";
|
|
19
18
|
import { getImportedEnvKeys } from "../env/importer.js";
|
|
19
|
+
import type { WebFastifyInstance } from "../types.js";
|
|
20
20
|
|
|
21
21
|
function normalizeEnv(
|
|
22
22
|
env: Record<string, string> | undefined,
|
|
@@ -135,7 +135,7 @@ function diffEnvHistory(
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
export async function registerConfigRoutes(
|
|
138
|
-
fastify:
|
|
138
|
+
fastify: WebFastifyInstance,
|
|
139
139
|
): Promise<void> {
|
|
140
140
|
fastify.get<{ Reply: ApiResponse<ConfigPayload> }>(
|
|
141
141
|
"/api/config",
|
|
@@ -5,19 +5,19 @@
|
|
|
5
5
|
* 仕様: specs/SPEC-d5e56259/contracts/rest-api.yaml
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { FastifyInstance } from "fastify";
|
|
9
8
|
import type { PTYManager } from "../pty/manager.js";
|
|
10
9
|
import { registerBranchRoutes } from "./branches.js";
|
|
11
10
|
import { registerWorktreeRoutes } from "./worktrees.js";
|
|
12
11
|
import { registerSessionRoutes } from "./sessions.js";
|
|
13
12
|
import { registerConfigRoutes } from "./config.js";
|
|
14
13
|
import type { HealthResponse } from "../../../types/api.js";
|
|
14
|
+
import type { WebFastifyInstance } from "../types.js";
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* すべてのルートを登録
|
|
18
18
|
*/
|
|
19
19
|
export async function registerRoutes(
|
|
20
|
-
fastify:
|
|
20
|
+
fastify: WebFastifyInstance,
|
|
21
21
|
ptyManager: PTYManager,
|
|
22
22
|
): Promise<void> {
|
|
23
23
|
// ヘルスチェック
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* AI Toolセッション関連のREST APIエンドポイント。
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { FastifyInstance } from "fastify";
|
|
8
7
|
import type { PTYManager } from "../pty/manager.js";
|
|
9
8
|
import type {
|
|
10
9
|
ApiResponse,
|
|
@@ -13,12 +12,13 @@ import type {
|
|
|
13
12
|
} from "../../../types/api.js";
|
|
14
13
|
import { saveSession } from "../../../config/index.js";
|
|
15
14
|
import { execa } from "execa";
|
|
15
|
+
import type { WebFastifyInstance } from "../types.js";
|
|
16
16
|
|
|
17
17
|
/**
|
|
18
18
|
* セッション関連のルートを登録
|
|
19
19
|
*/
|
|
20
20
|
export async function registerSessionRoutes(
|
|
21
|
-
fastify:
|
|
21
|
+
fastify: WebFastifyInstance,
|
|
22
22
|
ptyManager: PTYManager,
|
|
23
23
|
): Promise<void> {
|
|
24
24
|
// GET /api/sessions - すべてのセッション一覧を取得
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
* Worktree関連のREST APIエンドポイント。
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { FastifyInstance } from "fastify";
|
|
8
7
|
import {
|
|
9
8
|
listWorktrees,
|
|
10
9
|
getWorktreeByPath,
|
|
@@ -16,12 +15,13 @@ import type {
|
|
|
16
15
|
Worktree,
|
|
17
16
|
CreateWorktreeRequest,
|
|
18
17
|
} from "../../../types/api.js";
|
|
18
|
+
import type { WebFastifyInstance } from "../types.js";
|
|
19
19
|
|
|
20
20
|
/**
|
|
21
21
|
* Worktree関連のルートを登録
|
|
22
22
|
*/
|
|
23
23
|
export async function registerWorktreeRoutes(
|
|
24
|
-
fastify:
|
|
24
|
+
fastify: WebFastifyInstance,
|
|
25
25
|
): Promise<void> {
|
|
26
26
|
// GET /api/worktrees - すべてのWorktree一覧を取得
|
|
27
27
|
fastify.get<{ Reply: ApiResponse<Worktree[]> }>(
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FastifyInstance,
|
|
3
|
+
RawReplyDefaultExpression,
|
|
4
|
+
RawRequestDefaultExpression,
|
|
5
|
+
RawServerDefault,
|
|
6
|
+
} from "fastify";
|
|
7
|
+
import type { Logger } from "pino";
|
|
8
|
+
|
|
9
|
+
export type WebFastifyInstance = FastifyInstance<
|
|
10
|
+
RawServerDefault,
|
|
11
|
+
RawRequestDefaultExpression<RawServerDefault>,
|
|
12
|
+
RawReplyDefaultExpression<RawServerDefault>,
|
|
13
|
+
Logger
|
|
14
|
+
>;
|