@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.
Files changed (82) hide show
  1. package/dist/cli/ui/components/App.d.ts.map +1 -1
  2. package/dist/cli/ui/components/App.js +72 -3
  3. package/dist/cli/ui/components/App.js.map +1 -1
  4. package/dist/cli/ui/components/screens/BranchListScreen.d.ts +3 -1
  5. package/dist/cli/ui/components/screens/BranchListScreen.d.ts.map +1 -1
  6. package/dist/cli/ui/components/screens/BranchListScreen.js +154 -32
  7. package/dist/cli/ui/components/screens/BranchListScreen.js.map +1 -1
  8. package/dist/cli/ui/hooks/useGitData.d.ts.map +1 -1
  9. package/dist/cli/ui/hooks/useGitData.js +17 -0
  10. package/dist/cli/ui/hooks/useGitData.js.map +1 -1
  11. package/dist/cli/ui/types.d.ts +2 -0
  12. package/dist/cli/ui/types.d.ts.map +1 -1
  13. package/dist/cli/ui/utils/branchFormatter.d.ts.map +1 -1
  14. package/dist/cli/ui/utils/branchFormatter.js +7 -2
  15. package/dist/cli/ui/utils/branchFormatter.js.map +1 -1
  16. package/dist/cli/ui/utils/modelOptions.d.ts.map +1 -1
  17. package/dist/cli/ui/utils/modelOptions.js +7 -0
  18. package/dist/cli/ui/utils/modelOptions.js.map +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +7 -40
  21. package/dist/index.js.map +1 -1
  22. package/dist/logging/logger.d.ts +24 -0
  23. package/dist/logging/logger.d.ts.map +1 -0
  24. package/dist/logging/logger.js +57 -0
  25. package/dist/logging/logger.js.map +1 -0
  26. package/dist/logging/rotation.d.ts +6 -0
  27. package/dist/logging/rotation.d.ts.map +1 -0
  28. package/dist/logging/rotation.js +26 -0
  29. package/dist/logging/rotation.js.map +1 -0
  30. package/dist/utils/prompt.d.ts +6 -0
  31. package/dist/utils/prompt.d.ts.map +1 -0
  32. package/dist/utils/prompt.js +57 -0
  33. package/dist/utils/prompt.js.map +1 -0
  34. package/dist/web/server/index.d.ts.map +1 -1
  35. package/dist/web/server/index.js +3 -3
  36. package/dist/web/server/index.js.map +1 -1
  37. package/dist/web/server/routes/branches.d.ts +2 -2
  38. package/dist/web/server/routes/branches.d.ts.map +1 -1
  39. package/dist/web/server/routes/branches.js.map +1 -1
  40. package/dist/web/server/routes/config.d.ts +2 -2
  41. package/dist/web/server/routes/config.d.ts.map +1 -1
  42. package/dist/web/server/routes/config.js.map +1 -1
  43. package/dist/web/server/routes/index.d.ts +2 -2
  44. package/dist/web/server/routes/index.d.ts.map +1 -1
  45. package/dist/web/server/routes/index.js.map +1 -1
  46. package/dist/web/server/routes/sessions.d.ts +2 -2
  47. package/dist/web/server/routes/sessions.d.ts.map +1 -1
  48. package/dist/web/server/routes/sessions.js.map +1 -1
  49. package/dist/web/server/routes/worktrees.d.ts +2 -2
  50. package/dist/web/server/routes/worktrees.d.ts.map +1 -1
  51. package/dist/web/server/routes/worktrees.js.map +1 -1
  52. package/dist/web/server/types.d.ts +4 -0
  53. package/dist/web/server/types.d.ts.map +1 -0
  54. package/dist/web/server/types.js +2 -0
  55. package/dist/web/server/types.js.map +1 -0
  56. package/dist/worktree.d.ts +1 -0
  57. package/dist/worktree.d.ts.map +1 -1
  58. package/dist/worktree.js.map +1 -1
  59. package/package.json +4 -3
  60. package/src/cli/ui/__tests__/components/ModelSelectorScreen.initial.test.tsx +13 -13
  61. package/src/cli/ui/__tests__/components/screens/BranchListScreen.test.tsx +81 -33
  62. package/src/cli/ui/__tests__/performance/branchList.performance.test.tsx +7 -3
  63. package/src/cli/ui/components/App.tsx +88 -2
  64. package/src/cli/ui/components/screens/BranchListScreen.tsx +198 -32
  65. package/src/cli/ui/hooks/useGitData.ts +20 -0
  66. package/src/cli/ui/types.ts +3 -0
  67. package/src/cli/ui/utils/branchFormatter.ts +7 -2
  68. package/src/cli/ui/utils/modelOptions.test.ts +14 -0
  69. package/src/cli/ui/utils/modelOptions.ts +7 -0
  70. package/src/index.ts +8 -45
  71. package/src/logging/logger.ts +79 -0
  72. package/src/logging/rotation.ts +25 -0
  73. package/src/utils/__tests__/prompt.test.ts +89 -0
  74. package/src/utils/prompt.ts +74 -0
  75. package/src/web/server/index.ts +6 -4
  76. package/src/web/server/routes/branches.ts +2 -2
  77. package/src/web/server/routes/config.ts +2 -2
  78. package/src/web/server/routes/index.ts +2 -2
  79. package/src/web/server/routes/sessions.ts +2 -2
  80. package/src/web/server/routes/worktrees.ts +2 -2
  81. package/src/web/server/types.ts +14 -0
  82. 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
  }
@@ -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
- "🟢": 1,
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
- "⚠️": 1,
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
+ }
@@ -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 fastify = Fastify({
26
- logger: {
27
- level: process.env.LOG_LEVEL || "info",
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: FastifyInstance,
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: FastifyInstance,
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: FastifyInstance,
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: FastifyInstance,
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: FastifyInstance,
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
+ >;
package/src/worktree.ts CHANGED
@@ -110,6 +110,7 @@ export interface WorktreeInfo {
110
110
  head: string;
111
111
  isAccessible?: boolean;
112
112
  invalidReason?: string;
113
+ hasUncommittedChanges?: boolean;
113
114
  }
114
115
 
115
116
  async function listWorktrees(): Promise<WorktreeInfo[]> {