@litmers/cursorflow-orchestrator 0.1.18 → 0.1.26

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 (234) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/README.md +25 -7
  3. package/commands/cursorflow-clean.md +19 -0
  4. package/commands/cursorflow-runs.md +59 -0
  5. package/commands/cursorflow-stop.md +55 -0
  6. package/dist/cli/clean.js +178 -6
  7. package/dist/cli/clean.js.map +1 -1
  8. package/dist/cli/index.js +12 -1
  9. package/dist/cli/index.js.map +1 -1
  10. package/dist/cli/init.js +8 -7
  11. package/dist/cli/init.js.map +1 -1
  12. package/dist/cli/logs.js +126 -77
  13. package/dist/cli/logs.js.map +1 -1
  14. package/dist/cli/monitor.d.ts +7 -0
  15. package/dist/cli/monitor.js +1021 -202
  16. package/dist/cli/monitor.js.map +1 -1
  17. package/dist/cli/prepare.js +39 -21
  18. package/dist/cli/prepare.js.map +1 -1
  19. package/dist/cli/resume.js +268 -163
  20. package/dist/cli/resume.js.map +1 -1
  21. package/dist/cli/run.js +11 -5
  22. package/dist/cli/run.js.map +1 -1
  23. package/dist/cli/runs.d.ts +5 -0
  24. package/dist/cli/runs.js +214 -0
  25. package/dist/cli/runs.js.map +1 -0
  26. package/dist/cli/setup-commands.js +0 -0
  27. package/dist/cli/signal.js +8 -8
  28. package/dist/cli/signal.js.map +1 -1
  29. package/dist/cli/stop.d.ts +5 -0
  30. package/dist/cli/stop.js +215 -0
  31. package/dist/cli/stop.js.map +1 -0
  32. package/dist/cli/tasks.d.ts +10 -0
  33. package/dist/cli/tasks.js +165 -0
  34. package/dist/cli/tasks.js.map +1 -0
  35. package/dist/core/auto-recovery.d.ts +212 -0
  36. package/dist/core/auto-recovery.js +737 -0
  37. package/dist/core/auto-recovery.js.map +1 -0
  38. package/dist/core/failure-policy.d.ts +156 -0
  39. package/dist/core/failure-policy.js +488 -0
  40. package/dist/core/failure-policy.js.map +1 -0
  41. package/dist/core/orchestrator.d.ts +16 -2
  42. package/dist/core/orchestrator.js +439 -105
  43. package/dist/core/orchestrator.js.map +1 -1
  44. package/dist/core/reviewer.d.ts +2 -0
  45. package/dist/core/reviewer.js +2 -0
  46. package/dist/core/reviewer.js.map +1 -1
  47. package/dist/core/runner.d.ts +33 -10
  48. package/dist/core/runner.js +374 -164
  49. package/dist/core/runner.js.map +1 -1
  50. package/dist/services/logging/buffer.d.ts +67 -0
  51. package/dist/services/logging/buffer.js +309 -0
  52. package/dist/services/logging/buffer.js.map +1 -0
  53. package/dist/services/logging/console.d.ts +89 -0
  54. package/dist/services/logging/console.js +169 -0
  55. package/dist/services/logging/console.js.map +1 -0
  56. package/dist/services/logging/file-writer.d.ts +71 -0
  57. package/dist/services/logging/file-writer.js +516 -0
  58. package/dist/services/logging/file-writer.js.map +1 -0
  59. package/dist/services/logging/formatter.d.ts +39 -0
  60. package/dist/services/logging/formatter.js +227 -0
  61. package/dist/services/logging/formatter.js.map +1 -0
  62. package/dist/services/logging/index.d.ts +11 -0
  63. package/dist/services/logging/index.js +30 -0
  64. package/dist/services/logging/index.js.map +1 -0
  65. package/dist/services/logging/parser.d.ts +31 -0
  66. package/dist/services/logging/parser.js +222 -0
  67. package/dist/services/logging/parser.js.map +1 -0
  68. package/dist/services/process/index.d.ts +59 -0
  69. package/dist/services/process/index.js +257 -0
  70. package/dist/services/process/index.js.map +1 -0
  71. package/dist/types/agent.d.ts +20 -0
  72. package/dist/types/agent.js +6 -0
  73. package/dist/types/agent.js.map +1 -0
  74. package/dist/types/config.d.ts +65 -0
  75. package/dist/types/config.js +6 -0
  76. package/dist/types/config.js.map +1 -0
  77. package/dist/types/events.d.ts +125 -0
  78. package/dist/types/events.js +6 -0
  79. package/dist/types/events.js.map +1 -0
  80. package/dist/types/index.d.ts +12 -0
  81. package/dist/types/index.js +37 -0
  82. package/dist/types/index.js.map +1 -0
  83. package/dist/types/lane.d.ts +43 -0
  84. package/dist/types/lane.js +6 -0
  85. package/dist/types/lane.js.map +1 -0
  86. package/dist/types/logging.d.ts +71 -0
  87. package/dist/types/logging.js +16 -0
  88. package/dist/types/logging.js.map +1 -0
  89. package/dist/types/review.d.ts +17 -0
  90. package/dist/types/review.js +6 -0
  91. package/dist/types/review.js.map +1 -0
  92. package/dist/types/run.d.ts +32 -0
  93. package/dist/types/run.js +6 -0
  94. package/dist/types/run.js.map +1 -0
  95. package/dist/types/task.d.ts +71 -0
  96. package/dist/types/task.js +6 -0
  97. package/dist/types/task.js.map +1 -0
  98. package/dist/ui/components.d.ts +134 -0
  99. package/dist/ui/components.js +389 -0
  100. package/dist/ui/components.js.map +1 -0
  101. package/dist/ui/log-viewer.d.ts +49 -0
  102. package/dist/ui/log-viewer.js +449 -0
  103. package/dist/ui/log-viewer.js.map +1 -0
  104. package/dist/utils/checkpoint.d.ts +87 -0
  105. package/dist/utils/checkpoint.js +317 -0
  106. package/dist/utils/checkpoint.js.map +1 -0
  107. package/dist/utils/config.d.ts +4 -0
  108. package/dist/utils/config.js +18 -8
  109. package/dist/utils/config.js.map +1 -1
  110. package/dist/utils/cursor-agent.js.map +1 -1
  111. package/dist/utils/dependency.d.ts +74 -0
  112. package/dist/utils/dependency.js +420 -0
  113. package/dist/utils/dependency.js.map +1 -0
  114. package/dist/utils/doctor.js +17 -11
  115. package/dist/utils/doctor.js.map +1 -1
  116. package/dist/utils/enhanced-logger.d.ts +10 -33
  117. package/dist/utils/enhanced-logger.js +108 -20
  118. package/dist/utils/enhanced-logger.js.map +1 -1
  119. package/dist/utils/git.d.ts +121 -0
  120. package/dist/utils/git.js +484 -11
  121. package/dist/utils/git.js.map +1 -1
  122. package/dist/utils/health.d.ts +91 -0
  123. package/dist/utils/health.js +556 -0
  124. package/dist/utils/health.js.map +1 -0
  125. package/dist/utils/lock.d.ts +95 -0
  126. package/dist/utils/lock.js +332 -0
  127. package/dist/utils/lock.js.map +1 -0
  128. package/dist/utils/log-buffer.d.ts +17 -0
  129. package/dist/utils/log-buffer.js +14 -0
  130. package/dist/utils/log-buffer.js.map +1 -0
  131. package/dist/utils/log-constants.d.ts +23 -0
  132. package/dist/utils/log-constants.js +28 -0
  133. package/dist/utils/log-constants.js.map +1 -0
  134. package/dist/utils/log-formatter.d.ts +25 -0
  135. package/dist/utils/log-formatter.js +237 -0
  136. package/dist/utils/log-formatter.js.map +1 -0
  137. package/dist/utils/log-service.d.ts +19 -0
  138. package/dist/utils/log-service.js +47 -0
  139. package/dist/utils/log-service.js.map +1 -0
  140. package/dist/utils/logger.d.ts +46 -27
  141. package/dist/utils/logger.js +82 -60
  142. package/dist/utils/logger.js.map +1 -1
  143. package/dist/utils/path.d.ts +19 -0
  144. package/dist/utils/path.js +77 -0
  145. package/dist/utils/path.js.map +1 -0
  146. package/dist/utils/process-manager.d.ts +21 -0
  147. package/dist/utils/process-manager.js +138 -0
  148. package/dist/utils/process-manager.js.map +1 -0
  149. package/dist/utils/retry.d.ts +121 -0
  150. package/dist/utils/retry.js +374 -0
  151. package/dist/utils/retry.js.map +1 -0
  152. package/dist/utils/run-service.d.ts +88 -0
  153. package/dist/utils/run-service.js +412 -0
  154. package/dist/utils/run-service.js.map +1 -0
  155. package/dist/utils/state.d.ts +62 -3
  156. package/dist/utils/state.js +317 -11
  157. package/dist/utils/state.js.map +1 -1
  158. package/dist/utils/task-service.d.ts +82 -0
  159. package/dist/utils/task-service.js +348 -0
  160. package/dist/utils/task-service.js.map +1 -0
  161. package/dist/utils/template.d.ts +14 -0
  162. package/dist/utils/template.js +122 -0
  163. package/dist/utils/template.js.map +1 -0
  164. package/dist/utils/types.d.ts +2 -271
  165. package/dist/utils/types.js +16 -0
  166. package/dist/utils/types.js.map +1 -1
  167. package/package.json +38 -23
  168. package/scripts/ai-security-check.js +0 -1
  169. package/scripts/local-security-gate.sh +0 -0
  170. package/scripts/monitor-lanes.sh +94 -0
  171. package/scripts/patches/test-cursor-agent.js +0 -1
  172. package/scripts/release.sh +0 -0
  173. package/scripts/setup-security.sh +0 -0
  174. package/scripts/stream-logs.sh +72 -0
  175. package/scripts/verify-and-fix.sh +0 -0
  176. package/src/cli/clean.ts +187 -6
  177. package/src/cli/index.ts +12 -1
  178. package/src/cli/init.ts +8 -7
  179. package/src/cli/logs.ts +124 -77
  180. package/src/cli/monitor.ts +1815 -898
  181. package/src/cli/prepare.ts +41 -21
  182. package/src/cli/resume.ts +753 -626
  183. package/src/cli/run.ts +12 -5
  184. package/src/cli/runs.ts +212 -0
  185. package/src/cli/setup-commands.ts +0 -0
  186. package/src/cli/signal.ts +8 -7
  187. package/src/cli/stop.ts +209 -0
  188. package/src/cli/tasks.ts +154 -0
  189. package/src/core/auto-recovery.ts +909 -0
  190. package/src/core/failure-policy.ts +592 -0
  191. package/src/core/orchestrator.ts +1131 -704
  192. package/src/core/reviewer.ts +4 -0
  193. package/src/core/runner.ts +444 -180
  194. package/src/services/logging/buffer.ts +326 -0
  195. package/src/services/logging/console.ts +193 -0
  196. package/src/services/logging/file-writer.ts +526 -0
  197. package/src/services/logging/formatter.ts +268 -0
  198. package/src/services/logging/index.ts +16 -0
  199. package/src/services/logging/parser.ts +232 -0
  200. package/src/services/process/index.ts +261 -0
  201. package/src/types/agent.ts +24 -0
  202. package/src/types/config.ts +79 -0
  203. package/src/types/events.ts +156 -0
  204. package/src/types/index.ts +29 -0
  205. package/src/types/lane.ts +56 -0
  206. package/src/types/logging.ts +96 -0
  207. package/src/types/review.ts +20 -0
  208. package/src/types/run.ts +37 -0
  209. package/src/types/task.ts +79 -0
  210. package/src/ui/components.ts +430 -0
  211. package/src/ui/log-viewer.ts +485 -0
  212. package/src/utils/checkpoint.ts +374 -0
  213. package/src/utils/config.ts +18 -8
  214. package/src/utils/cursor-agent.ts +1 -1
  215. package/src/utils/dependency.ts +482 -0
  216. package/src/utils/doctor.ts +18 -11
  217. package/src/utils/enhanced-logger.ts +122 -60
  218. package/src/utils/git.ts +517 -11
  219. package/src/utils/health.ts +596 -0
  220. package/src/utils/lock.ts +346 -0
  221. package/src/utils/log-buffer.ts +28 -0
  222. package/src/utils/log-constants.ts +26 -0
  223. package/src/utils/log-formatter.ts +245 -0
  224. package/src/utils/log-service.ts +49 -0
  225. package/src/utils/logger.ts +100 -51
  226. package/src/utils/path.ts +45 -0
  227. package/src/utils/process-manager.ts +100 -0
  228. package/src/utils/retry.ts +413 -0
  229. package/src/utils/run-service.ts +433 -0
  230. package/src/utils/state.ts +385 -11
  231. package/src/utils/task-service.ts +370 -0
  232. package/src/utils/template.ts +92 -0
  233. package/src/utils/types.ts +2 -314
  234. package/templates/basic.json +21 -0
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Review-related type definitions
3
+ */
4
+
5
+ export interface ReviewIssue {
6
+ severity: 'critical' | 'major' | 'minor';
7
+ description: string;
8
+ file?: string;
9
+ suggestion?: string;
10
+ }
11
+
12
+ export interface ReviewResult {
13
+ status: 'approved' | 'needs_changes';
14
+ buildSuccess: boolean;
15
+ issues: ReviewIssue[];
16
+ suggestions: string[];
17
+ summary: string;
18
+ raw: string;
19
+ }
20
+
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Run-related type definitions
3
+ */
4
+
5
+ import type { LaneInfo } from './lane';
6
+
7
+ export type RunStatus = 'running' | 'completed' | 'failed' | 'partial' | 'pending';
8
+
9
+ export interface RunInfo {
10
+ id: string;
11
+ path: string;
12
+ taskName: string;
13
+ status: RunStatus;
14
+ startTime: number;
15
+ endTime?: number;
16
+ duration: number;
17
+ lanes: LaneInfo[];
18
+ branches: string[];
19
+ worktrees: string[];
20
+ }
21
+
22
+ /**
23
+ * Flow info for multiple concurrent flows
24
+ */
25
+ export interface FlowInfo {
26
+ id: string;
27
+ path: string;
28
+ status: RunStatus;
29
+ startTime: number;
30
+ endTime?: number;
31
+ laneCount: number;
32
+ completedCount: number;
33
+ failedCount: number;
34
+ isAlive: boolean;
35
+ pid?: number;
36
+ }
37
+
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Task-related type definitions
3
+ */
4
+
5
+ import type { DependencyPolicy, DependencyRequestPlan } from './agent';
6
+ import type { LaneFileInfo } from './lane';
7
+
8
+ export interface Task {
9
+ name: string;
10
+ prompt: string;
11
+ model?: string;
12
+ /** Acceptance criteria for the AI reviewer to validate */
13
+ acceptanceCriteria?: string[];
14
+ /** Task-level dependencies (format: "lane:task") */
15
+ dependsOn?: string[];
16
+ /** Task execution timeout in milliseconds. Overrides lane-level timeout. */
17
+ timeout?: number;
18
+ }
19
+
20
+ export interface RunnerConfig {
21
+ tasks: Task[];
22
+ dependsOn?: string[];
23
+ pipelineBranch?: string;
24
+ worktreeDir?: string;
25
+ branchPrefix?: string;
26
+ worktreeRoot?: string;
27
+ baseBranch?: string;
28
+ model?: string;
29
+ dependencyPolicy: DependencyPolicy;
30
+ enableReview?: boolean;
31
+ /** Output format for cursor-agent (default: 'stream-json') */
32
+ agentOutputFormat?: 'stream-json' | 'json' | 'plain';
33
+ reviewModel?: string;
34
+ reviewAllTasks?: boolean;
35
+ maxReviewIterations?: number;
36
+ acceptanceCriteria?: string[];
37
+ /** Task execution timeout in milliseconds. Default: 600000 (10 minutes) */
38
+ timeout?: number;
39
+ /**
40
+ * Enable intervention feature (stdin piping for message injection).
41
+ * Warning: May cause stdout buffering issues on some systems.
42
+ * Default: false
43
+ */
44
+ enableIntervention?: boolean;
45
+ /**
46
+ * Disable Git operations (worktree, branch, push, commit).
47
+ * Useful for testing or environments without Git remote.
48
+ * Default: false
49
+ */
50
+ noGit?: boolean;
51
+ }
52
+
53
+ export interface TaskDirInfo {
54
+ name: string;
55
+ path: string;
56
+ timestamp: Date;
57
+ featureName: string;
58
+ lanes: LaneFileInfo[];
59
+ validationStatus: ValidationStatus;
60
+ lastValidated?: number;
61
+ }
62
+
63
+ export interface TaskExecutionResult {
64
+ taskName: string;
65
+ taskBranch: string;
66
+ status: 'FINISHED' | 'ERROR' | 'BLOCKED_DEPENDENCY';
67
+ error?: string;
68
+ dependencyRequest?: DependencyRequestPlan | null;
69
+ }
70
+
71
+ export interface TaskResult {
72
+ taskName: string;
73
+ taskBranch: string;
74
+ acceptanceCriteria?: string[];
75
+ [key: string]: any;
76
+ }
77
+
78
+ export type ValidationStatus = 'valid' | 'warnings' | 'errors' | 'unknown';
79
+
@@ -0,0 +1,430 @@
1
+ /**
2
+ * CursorFlow UI Components
3
+ *
4
+ * Reusable terminal UI components for interactive screens.
5
+ */
6
+
7
+ import * as readline from 'readline';
8
+ import * as logger from '../utils/logger';
9
+
10
+ /**
11
+ * 상태 아이콘 유틸리티
12
+ */
13
+ export const StatusIcons = {
14
+ running: '🔄',
15
+ completed: '✅',
16
+ done: '✅',
17
+ failed: '❌',
18
+ partial: '⚠️',
19
+ pending: '⏳',
20
+ valid: '✅',
21
+ errors: '❌',
22
+ warnings: '⚠️',
23
+ } as const;
24
+
25
+ export function getStatusIcon(status: string): string {
26
+ return StatusIcons[status as keyof typeof StatusIcons] || '❓';
27
+ }
28
+
29
+ /**
30
+ * ANSI 코드 제거
31
+ */
32
+ export function stripAnsi(str: string): string {
33
+ return str.replace(/\x1b\[[0-9;]*m/g, '');
34
+ }
35
+
36
+ /**
37
+ * 문자열 패딩 (ANSI 코드 고려)
38
+ */
39
+ export function pad(str: string, width: number, align: 'left' | 'right' | 'center' = 'left'): string {
40
+ const visibleLength = stripAnsi(str).length;
41
+ const padding = Math.max(0, width - visibleLength);
42
+
43
+ switch (align) {
44
+ case 'right':
45
+ return ' '.repeat(padding) + str;
46
+ case 'center':
47
+ const left = Math.floor(padding / 2);
48
+ const right = padding - left;
49
+ return ' '.repeat(left) + str + ' '.repeat(right);
50
+ default:
51
+ return str + ' '.repeat(padding);
52
+ }
53
+ }
54
+
55
+ /**
56
+ * 프로그레스 바 렌더링
57
+ */
58
+ export function renderProgressBar(current: number, total: number, width: number = 20): string {
59
+ const ratio = total > 0 ? current / total : 0;
60
+ const filled = Math.min(width, Math.round(ratio * width));
61
+ const empty = width - filled;
62
+
63
+ const bar = `${'█'.repeat(filled)}${'░'.repeat(empty)}`;
64
+ const percent = `${Math.round(ratio * 100)}%`.padStart(4);
65
+
66
+ return `[${bar}] ${percent}`;
67
+ }
68
+
69
+ /**
70
+ * 확인 다이얼로그
71
+ */
72
+ export async function confirm(message: string): Promise<boolean> {
73
+ return new Promise((resolve) => {
74
+ const rl = readline.createInterface({
75
+ input: process.stdin,
76
+ output: process.stdout,
77
+ });
78
+
79
+ rl.question(`${message} (y/N): `, (answer) => {
80
+ rl.close();
81
+ resolve(answer.toLowerCase() === 'y' || answer.toLowerCase() === 'yes');
82
+ });
83
+ });
84
+ }
85
+
86
+ /**
87
+ * 알림 표시
88
+ */
89
+ export interface Notification {
90
+ message: string;
91
+ type: 'info' | 'success' | 'error' | 'warning';
92
+ time: number;
93
+ }
94
+
95
+ export function renderNotification(notification: Notification | null): string {
96
+ if (!notification) return '';
97
+
98
+ const colors: Record<Notification['type'], string> = {
99
+ info: logger.COLORS.cyan,
100
+ success: logger.COLORS.green,
101
+ error: logger.COLORS.red,
102
+ warning: logger.COLORS.yellow,
103
+ };
104
+
105
+ const icons: Record<Notification['type'], string> = {
106
+ info: 'ℹ️',
107
+ success: '✅',
108
+ error: '❌',
109
+ warning: '⚠️',
110
+ };
111
+
112
+ return `${colors[notification.type]}${icons[notification.type]} ${notification.message}${logger.COLORS.reset}`;
113
+ }
114
+
115
+ /**
116
+ * 키 도움말 바
117
+ */
118
+ export function renderKeyHelp(keys: { key: string; action: string }[]): string {
119
+ const parts = keys.map(k => `[${k.key}] ${k.action}`);
120
+ return `${logger.COLORS.gray}${parts.join(' ')}${logger.COLORS.reset}`;
121
+ }
122
+
123
+ /**
124
+ * 헤더 렌더링
125
+ */
126
+ export function renderHeader(title: string, subtitle?: string): string[] {
127
+ const lines: string[] = [
128
+ `${logger.COLORS.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${logger.COLORS.reset}`,
129
+ `${logger.COLORS.cyan} ${title}${logger.COLORS.reset}`,
130
+ ];
131
+
132
+ if (subtitle) {
133
+ lines.push(`${logger.COLORS.gray} ${subtitle}${logger.COLORS.reset}`);
134
+ }
135
+
136
+ lines.push(`${logger.COLORS.cyan}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${logger.COLORS.reset}`);
137
+
138
+ return lines;
139
+ }
140
+
141
+ /**
142
+ * 기본 인터랙티브 화면 클래스
143
+ */
144
+ export abstract class InteractiveScreen {
145
+ protected notification: Notification | null = null;
146
+ protected running: boolean = false;
147
+
148
+ start(): void {
149
+ this.running = true;
150
+ this.setupTerminal();
151
+ this.render();
152
+ }
153
+
154
+ stop(): void {
155
+ this.running = false;
156
+ this.restoreTerminal();
157
+ }
158
+
159
+ abstract render(): void;
160
+ abstract handleKey(key: string, keyInfo?: any): void;
161
+
162
+ protected setupTerminal(): void {
163
+ if (process.stdin.isTTY) {
164
+ process.stdin.setRawMode(true);
165
+ }
166
+ readline.emitKeypressEvents(process.stdin);
167
+ process.stdin.on('keypress', this.keypressHandler);
168
+ this.hideCursor();
169
+ }
170
+
171
+ protected restoreTerminal(): void {
172
+ process.stdin.removeListener('keypress', this.keypressHandler);
173
+ if (process.stdin.isTTY) {
174
+ process.stdin.setRawMode(false);
175
+ }
176
+ this.showCursor();
177
+ }
178
+
179
+ private keypressHandler = (str: string, key: any) => {
180
+ if (key && key.ctrl && key.name === 'c') {
181
+ this.stop();
182
+ process.exit(0);
183
+ }
184
+ this.handleKey(key ? key.name : str, key);
185
+ };
186
+
187
+ protected showNotification(message: string, type: Notification['type']): void {
188
+ this.notification = { message, type, time: Date.now() };
189
+ }
190
+
191
+ protected clearOldNotification(): void {
192
+ if (this.notification && Date.now() - this.notification.time > 3000) {
193
+ this.notification = null;
194
+ }
195
+ }
196
+
197
+ protected clearScreen(): void {
198
+ process.stdout.write('\x1Bc');
199
+ }
200
+
201
+ protected hideCursor(): void {
202
+ process.stdout.write('\x1B[?25l');
203
+ }
204
+
205
+ protected showCursor(): void {
206
+ process.stdout.write('\x1B[?25h');
207
+ }
208
+ }
209
+
210
+ /**
211
+ * 선택 가능한 리스트 컴포넌트
212
+ */
213
+ export class SelectableList<T> {
214
+ items: T[];
215
+ selectedIndex: number = 0;
216
+ private formatter: (item: T, selected: boolean) => string;
217
+ private maxVisible: number;
218
+ private scrollOffset: number = 0;
219
+
220
+ constructor(
221
+ items: T[],
222
+ formatter: (item: T, selected: boolean) => string,
223
+ maxVisible: number = 15
224
+ ) {
225
+ this.items = items;
226
+ this.formatter = formatter;
227
+ this.maxVisible = maxVisible;
228
+ }
229
+
230
+ moveUp(): void {
231
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
232
+ this.adjustScroll();
233
+ }
234
+
235
+ moveDown(): void {
236
+ this.selectedIndex = Math.min(this.items.length - 1, this.selectedIndex + 1);
237
+ this.adjustScroll();
238
+ }
239
+
240
+ private adjustScroll(): void {
241
+ if (this.selectedIndex < this.scrollOffset) {
242
+ this.scrollOffset = this.selectedIndex;
243
+ } else if (this.selectedIndex >= this.scrollOffset + this.maxVisible) {
244
+ this.scrollOffset = this.selectedIndex - this.maxVisible + 1;
245
+ }
246
+ }
247
+
248
+ getSelected(): T | undefined {
249
+ return this.items[this.selectedIndex];
250
+ }
251
+
252
+ getSelectedIndex(): number {
253
+ return this.selectedIndex;
254
+ }
255
+
256
+ setItems(items: T[]): void {
257
+ this.items = items;
258
+ if (this.selectedIndex >= items.length) {
259
+ this.selectedIndex = Math.max(0, items.length - 1);
260
+ }
261
+ this.adjustScroll();
262
+ }
263
+
264
+ render(): string[] {
265
+ const visibleItems = this.items.slice(
266
+ this.scrollOffset,
267
+ this.scrollOffset + this.maxVisible
268
+ );
269
+
270
+ const lines = visibleItems.map((item, i) => {
271
+ const actualIndex = i + this.scrollOffset;
272
+ const isSelected = actualIndex === this.selectedIndex;
273
+ return this.formatter(item, isSelected);
274
+ });
275
+
276
+ // 스크롤 표시
277
+ if (this.scrollOffset > 0) {
278
+ lines.unshift(` ${logger.COLORS.gray}↑ ${this.scrollOffset} more...${logger.COLORS.reset}`);
279
+ }
280
+
281
+ const remaining = this.items.length - this.scrollOffset - this.maxVisible;
282
+ if (remaining > 0) {
283
+ lines.push(` ${logger.COLORS.gray}↓ ${remaining} more...${logger.COLORS.reset}`);
284
+ }
285
+
286
+ return lines;
287
+ }
288
+ }
289
+
290
+ /**
291
+ * 체크박스 리스트 컴포넌트
292
+ */
293
+ export class CheckboxList<T> {
294
+ items: T[];
295
+ checked: Set<number> = new Set();
296
+ selectedIndex: number = 0;
297
+ private formatter: (item: T, isSelected: boolean, isChecked: boolean) => string;
298
+ private maxVisible: number;
299
+ private scrollOffset: number = 0;
300
+
301
+ constructor(
302
+ items: T[],
303
+ formatter: (item: T, isSelected: boolean, isChecked: boolean) => string,
304
+ maxVisible: number = 15
305
+ ) {
306
+ this.items = items;
307
+ this.formatter = formatter;
308
+ this.maxVisible = maxVisible;
309
+ }
310
+
311
+ moveUp(): void {
312
+ this.selectedIndex = Math.max(0, this.selectedIndex - 1);
313
+ this.adjustScroll();
314
+ }
315
+
316
+ moveDown(): void {
317
+ this.selectedIndex = Math.min(this.items.length - 1, this.selectedIndex + 1);
318
+ this.adjustScroll();
319
+ }
320
+
321
+ private adjustScroll(): void {
322
+ if (this.selectedIndex < this.scrollOffset) {
323
+ this.scrollOffset = this.selectedIndex;
324
+ } else if (this.selectedIndex >= this.scrollOffset + this.maxVisible) {
325
+ this.scrollOffset = this.selectedIndex - this.maxVisible + 1;
326
+ }
327
+ }
328
+
329
+ toggle(): void {
330
+ if (this.checked.has(this.selectedIndex)) {
331
+ this.checked.delete(this.selectedIndex);
332
+ } else {
333
+ this.checked.add(this.selectedIndex);
334
+ }
335
+ }
336
+
337
+ selectAll(): void {
338
+ for (let i = 0; i < this.items.length; i++) {
339
+ this.checked.add(i);
340
+ }
341
+ }
342
+
343
+ deselectAll(): void {
344
+ this.checked.clear();
345
+ }
346
+
347
+ getChecked(): T[] {
348
+ return this.items.filter((_, i) => this.checked.has(i));
349
+ }
350
+
351
+ render(): string[] {
352
+ const visibleItems = this.items.slice(
353
+ this.scrollOffset,
354
+ this.scrollOffset + this.maxVisible
355
+ );
356
+
357
+ const lines = visibleItems.map((item, i) => {
358
+ const actualIndex = i + this.scrollOffset;
359
+ const isSelected = actualIndex === this.selectedIndex;
360
+ const isChecked = this.checked.has(actualIndex);
361
+ return this.formatter(item, isSelected, isChecked);
362
+ });
363
+
364
+ if (this.scrollOffset > 0) {
365
+ lines.unshift(` ${logger.COLORS.gray}↑ ${this.scrollOffset} more...${logger.COLORS.reset}`);
366
+ }
367
+
368
+ const remaining = this.items.length - this.scrollOffset - this.maxVisible;
369
+ if (remaining > 0) {
370
+ lines.push(` ${logger.COLORS.gray}↓ ${remaining} more...${logger.COLORS.reset}`);
371
+ }
372
+
373
+ return lines;
374
+ }
375
+ }
376
+
377
+ /**
378
+ * 스크롤 가능 버퍼 컴포넌트
379
+ */
380
+ export class ScrollableBuffer<T> {
381
+ private items: T[] = [];
382
+ private scrollOffset: number = 0;
383
+ private pageSize: number;
384
+
385
+ constructor(pageSize: number = 20) {
386
+ this.pageSize = pageSize;
387
+ }
388
+
389
+ setItems(items: T[]): void {
390
+ this.items = items;
391
+ // Adjust scroll offset if it goes beyond new items
392
+ const maxOffset = Math.max(0, this.items.length - this.pageSize);
393
+ if (this.scrollOffset > maxOffset) {
394
+ this.scrollOffset = maxOffset;
395
+ }
396
+ }
397
+
398
+ scrollUp(lines: number = 1): void {
399
+ this.scrollOffset = Math.max(0, this.scrollOffset - lines);
400
+ }
401
+
402
+ scrollDown(lines: number = 1): void {
403
+ const maxOffset = Math.max(0, this.items.length - this.pageSize);
404
+ this.scrollOffset = Math.min(maxOffset, this.scrollOffset + lines);
405
+ }
406
+
407
+ scrollToTop(): void {
408
+ this.scrollOffset = 0;
409
+ }
410
+
411
+ scrollToBottom(): void {
412
+ this.scrollOffset = Math.max(0, this.items.length - this.pageSize);
413
+ }
414
+
415
+ getVisibleItems(): T[] {
416
+ return this.items.slice(this.scrollOffset, this.scrollOffset + this.pageSize);
417
+ }
418
+
419
+ getTotalCount(): number {
420
+ return this.items.length;
421
+ }
422
+
423
+ getScrollInfo(): { offset: number; total: number; pageSize: number } {
424
+ return {
425
+ offset: this.scrollOffset,
426
+ total: this.items.length,
427
+ pageSize: this.pageSize
428
+ };
429
+ }
430
+ }