code-squad-cli 1.3.0 → 2.0.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.
@@ -1,47 +0,0 @@
1
- export interface Thread {
2
- type: 'worktree' | 'local';
3
- name: string;
4
- path: string;
5
- branch?: string;
6
- id?: string;
7
- }
8
- export interface ThreadChoice {
9
- type: 'existing' | 'new' | 'exit' | 'delete-selected';
10
- thread?: Thread;
11
- }
12
- export type ThreadAction = 'open' | 'delete' | 'back';
13
- export type NewThreadType = 'worktree' | 'local' | 'back';
14
- /**
15
- * 메인 스레드 선택 UI (d키로 삭제 지원)
16
- */
17
- export declare function selectThread(threads: Thread[], repoName: string): Promise<ThreadChoice>;
18
- /**
19
- * 스레드 액션 선택 (열기/삭제)
20
- */
21
- export declare function selectThreadAction(threadName: string): Promise<ThreadAction>;
22
- /**
23
- * 새 작업 타입 선택 (워크트리/로컬)
24
- */
25
- export declare function selectNewThreadType(): Promise<NewThreadType>;
26
- /**
27
- * 새 워크트리 생성 폼
28
- */
29
- export declare function newWorktreeForm(defaultBasePath: string): Promise<{
30
- name: string;
31
- path: string;
32
- } | null>;
33
- /**
34
- * 새 로컬 스레드 이름 입력
35
- */
36
- export declare function newLocalForm(): Promise<string | null>;
37
- /**
38
- * 워크트리 삭제 확인
39
- */
40
- export declare function confirmDeleteWorktree(threadName: string): Promise<{
41
- confirmed: boolean;
42
- removeGitWorktree: boolean;
43
- }>;
44
- /**
45
- * 로컬 스레드 삭제 확인
46
- */
47
- export declare function confirmDeleteLocal(threadName: string): Promise<boolean>;
@@ -1,328 +0,0 @@
1
- import { createPrompt, useState, useKeypress, usePrefix, isEnterKey, isBackspaceKey, } from '@inquirer/core';
2
- import { confirm } from '@inquirer/prompts';
3
- import chalk from 'chalk';
4
- import * as path from 'path';
5
- /**
6
- * ESC로 취소 가능한 커스텀 Input
7
- */
8
- const cancelableInput = createPrompt((config, done) => {
9
- const [value, setValue] = useState(config.default || '');
10
- const [error, setError] = useState(null);
11
- const prefix = usePrefix({ status: 'idle' });
12
- useKeypress((key, rl) => {
13
- if (key.name === 'escape') {
14
- done(null);
15
- }
16
- else if (isEnterKey(key)) {
17
- if (config.validate) {
18
- const result = config.validate(value);
19
- if (result !== true) {
20
- setError(typeof result === 'string' ? result : 'Invalid input');
21
- return;
22
- }
23
- }
24
- done(value);
25
- }
26
- else if (isBackspaceKey(key)) {
27
- setValue(value.slice(0, -1));
28
- setError(null);
29
- }
30
- else if (key.ctrl || key.name === 'tab' || key.name === 'up' || key.name === 'down') {
31
- // ignore control keys
32
- }
33
- else {
34
- // printable character (use sequence for accurate input including -, _, etc.)
35
- const seq = key.sequence;
36
- if (seq && seq.length === 1 && seq >= ' ') {
37
- setValue(value + seq);
38
- setError(null);
39
- }
40
- }
41
- });
42
- const errorMsg = error ? chalk.red(`\n${error}`) : '';
43
- return `${prefix} ${config.message} ${chalk.cyan(value)}${errorMsg}\n${chalk.dim('ESC:cancel Enter:confirm')}`;
44
- });
45
- /**
46
- * 경로를 터미널 폭에 맞게 축약
47
- */
48
- function truncatePath(fullPath, maxLen) {
49
- if (fullPath.length <= maxLen)
50
- return fullPath;
51
- const home = process.env.HOME || '';
52
- let display = fullPath.startsWith(home)
53
- ? '~' + fullPath.slice(home.length)
54
- : fullPath;
55
- if (display.length <= maxLen)
56
- return display;
57
- const parts = display.split(path.sep);
58
- if (parts.length > 2) {
59
- return '…/' + parts.slice(-2).join('/');
60
- }
61
- return '…' + display.slice(-maxLen + 1);
62
- }
63
- /**
64
- * 커스텀 Select (vim 키바인딩)
65
- */
66
- const vimSelect = createPrompt((config, done) => {
67
- const { choices, pageSize = 15, shortcuts = [] } = config;
68
- const enabledChoices = choices.filter((c) => !c.disabled);
69
- const [selectedIndex, setSelectedIndex] = useState(0);
70
- const prefix = usePrefix({ status: 'idle' });
71
- useKeypress((key) => {
72
- // vim navigation
73
- if (key.name === 'j' || key.name === 'down') {
74
- const nextIndex = (selectedIndex + 1) % enabledChoices.length;
75
- setSelectedIndex(nextIndex);
76
- }
77
- else if (key.name === 'k' || key.name === 'up') {
78
- const prevIndex = (selectedIndex - 1 + enabledChoices.length) %
79
- enabledChoices.length;
80
- setSelectedIndex(prevIndex);
81
- }
82
- else if (isEnterKey(key)) {
83
- done(enabledChoices[selectedIndex].value);
84
- }
85
- else if (key.name === 'escape' || key.name === 'b') {
86
- // 뒤로가기 (shortcuts에 back이 있으면)
87
- const backShortcut = shortcuts.find((s) => s.key === 'back');
88
- if (backShortcut) {
89
- done(backShortcut.value);
90
- }
91
- }
92
- else {
93
- // 커스텀 단축키 체크
94
- const shortcut = shortcuts.find((s) => s.key === key.name);
95
- if (shortcut) {
96
- done(shortcut.value);
97
- }
98
- }
99
- });
100
- // 페이지네이션 계산
101
- const totalItems = enabledChoices.length;
102
- const halfPage = Math.floor(pageSize / 2);
103
- let startIndex = 0;
104
- if (totalItems > pageSize) {
105
- if (selectedIndex <= halfPage) {
106
- startIndex = 0;
107
- }
108
- else if (selectedIndex >= totalItems - halfPage) {
109
- startIndex = totalItems - pageSize;
110
- }
111
- else {
112
- startIndex = selectedIndex - halfPage;
113
- }
114
- }
115
- const visibleChoices = enabledChoices.slice(startIndex, startIndex + pageSize);
116
- // 렌더링
117
- const lines = visibleChoices.map((choice, i) => {
118
- const actualIndex = startIndex + i;
119
- const isSelected = actualIndex === selectedIndex;
120
- const cursor = isSelected ? chalk.cyan('❯') : ' ';
121
- const name = isSelected ? chalk.cyan(choice.name) : choice.name;
122
- return `${cursor} ${name}`;
123
- });
124
- // 스크롤 인디케이터
125
- if (startIndex > 0) {
126
- lines.unshift(chalk.dim(' ↑ more'));
127
- }
128
- if (startIndex + pageSize < totalItems) {
129
- lines.push(chalk.dim(' ↓ more'));
130
- }
131
- // 단축키 힌트
132
- const shortcutHints = shortcuts
133
- .filter((s) => s.description)
134
- .map((s) => chalk.dim(`${s.key}:${s.description}`))
135
- .join(' ');
136
- const hint = chalk.dim('j/k:navigate enter:select') + (shortcutHints ? ' ' + shortcutHints : '');
137
- return `${prefix} ${chalk.bold(config.message)}\n${lines.join('\n')}\n${hint}`;
138
- });
139
- /**
140
- * 메인 스레드 선택 UI (d키로 삭제 지원)
141
- */
142
- export async function selectThread(threads, repoName) {
143
- const cols = process.stdout.columns || 80;
144
- const nameWidth = 18;
145
- const prefixWidth = 6;
146
- const pathMaxLen = Math.max(20, cols - nameWidth - prefixWidth - 5);
147
- // 스레드 + 새 작업 옵션
148
- const threadChoices = threads.map((t) => {
149
- const typeIcon = t.type === 'worktree' ? chalk.cyan('[W]') : chalk.yellow('[L]');
150
- const displayPath = truncatePath(t.path, pathMaxLen);
151
- return {
152
- name: `${typeIcon} ${t.name.padEnd(nameWidth)} ${chalk.dim(displayPath)}`,
153
- value: { type: 'existing', thread: t },
154
- thread: t,
155
- };
156
- });
157
- const newChoice = {
158
- name: chalk.green('+ 새 작업'),
159
- value: { type: 'new' },
160
- thread: null,
161
- };
162
- const allChoices = [...threadChoices, newChoice];
163
- // 커스텀 프롬프트로 'd' 키 지원
164
- const threadSelect = createPrompt((config, done) => {
165
- const [selectedIndex, setSelectedIndex] = useState(0);
166
- const prefix = usePrefix({ status: 'idle' });
167
- const pageSize = 15;
168
- useKeypress((key) => {
169
- if (key.name === 'j' || key.name === 'down') {
170
- setSelectedIndex((selectedIndex + 1) % allChoices.length);
171
- }
172
- else if (key.name === 'k' || key.name === 'up') {
173
- setSelectedIndex((selectedIndex - 1 + allChoices.length) % allChoices.length);
174
- }
175
- else if (isEnterKey(key)) {
176
- done(allChoices[selectedIndex].value);
177
- }
178
- else if (key.name === 'n') {
179
- done({ type: 'new' });
180
- }
181
- else if (key.name === 'q') {
182
- done({ type: 'exit' });
183
- }
184
- else if (key.name === 'd') {
185
- // d키: 현재 선택된 것이 스레드면 삭제 흐름
186
- const current = allChoices[selectedIndex];
187
- if (current.thread) {
188
- done({ type: 'delete-selected', thread: current.thread });
189
- }
190
- }
191
- });
192
- // 페이지네이션
193
- const totalItems = allChoices.length;
194
- const halfPage = Math.floor(pageSize / 2);
195
- let startIndex = 0;
196
- if (totalItems > pageSize) {
197
- if (selectedIndex <= halfPage) {
198
- startIndex = 0;
199
- }
200
- else if (selectedIndex >= totalItems - halfPage) {
201
- startIndex = totalItems - pageSize;
202
- }
203
- else {
204
- startIndex = selectedIndex - halfPage;
205
- }
206
- }
207
- const visibleChoices = allChoices.slice(startIndex, startIndex + pageSize);
208
- const lines = visibleChoices.map((choice, i) => {
209
- const actualIndex = startIndex + i;
210
- const isSelected = actualIndex === selectedIndex;
211
- const cursor = isSelected ? chalk.cyan('❯') : ' ';
212
- const name = isSelected ? chalk.cyan(choice.name) : choice.name;
213
- return `${cursor} ${name}`;
214
- });
215
- if (startIndex > 0) {
216
- lines.unshift(chalk.dim(' ↑ more'));
217
- }
218
- if (startIndex + pageSize < totalItems) {
219
- lines.push(chalk.dim(' ↓ more'));
220
- }
221
- const hint = chalk.dim('j/k:nav n:new d:delete q:quit');
222
- return `${prefix} ${config.message}\n${lines.join('\n')}\n${hint}`;
223
- });
224
- return threadSelect({ message: chalk.bold(repoName) });
225
- }
226
- /**
227
- * 스레드 액션 선택 (열기/삭제)
228
- */
229
- export async function selectThreadAction(threadName) {
230
- const choices = [
231
- { name: '터미널 열기', value: 'open' },
232
- { name: chalk.red('삭제하기'), value: 'delete' },
233
- { name: chalk.dim('← 뒤로'), value: 'back' },
234
- ];
235
- return vimSelect({
236
- message: `'${threadName}'`,
237
- choices,
238
- shortcuts: [{ key: 'back', value: 'back' }],
239
- });
240
- }
241
- /**
242
- * 새 작업 타입 선택 (워크트리/로컬)
243
- */
244
- export async function selectNewThreadType() {
245
- const choices = [
246
- {
247
- name: chalk.cyan('워크트리') +
248
- chalk.dim(' - 새 브랜치와 디렉토리 생성'),
249
- value: 'worktree',
250
- },
251
- {
252
- name: chalk.yellow('로컬') + chalk.dim(' - 현재 디렉토리에서 작업'),
253
- value: 'local',
254
- },
255
- { name: chalk.dim('← 뒤로'), value: 'back' },
256
- ];
257
- return vimSelect({
258
- message: '새 작업 타입',
259
- choices,
260
- shortcuts: [{ key: 'back', value: 'back' }],
261
- });
262
- }
263
- /**
264
- * 새 워크트리 생성 폼
265
- */
266
- export async function newWorktreeForm(defaultBasePath) {
267
- const name = await cancelableInput({
268
- message: '워크트리 이름:',
269
- validate: (value) => {
270
- if (!value.trim())
271
- return '이름을 입력해주세요';
272
- if (!/^[a-zA-Z0-9_-]+$/.test(value)) {
273
- return '영문, 숫자, -, _ 만 사용 가능';
274
- }
275
- return true;
276
- },
277
- });
278
- if (!name)
279
- return null;
280
- const defaultPath = `${defaultBasePath}/${name}`;
281
- const pathInput = await cancelableInput({
282
- message: '경로:',
283
- default: defaultPath,
284
- });
285
- if (!pathInput)
286
- return null;
287
- return { name, path: pathInput };
288
- }
289
- /**
290
- * 새 로컬 스레드 이름 입력
291
- */
292
- export async function newLocalForm() {
293
- const name = await cancelableInput({
294
- message: '로컬 스레드 이름:',
295
- validate: (value) => {
296
- if (!value.trim())
297
- return '이름을 입력해주세요';
298
- return true;
299
- },
300
- });
301
- return name || null;
302
- }
303
- /**
304
- * 워크트리 삭제 확인
305
- */
306
- export async function confirmDeleteWorktree(threadName) {
307
- const confirmed = await confirm({
308
- message: `'${threadName}' 워크트리를 삭제할까요?`,
309
- default: true,
310
- });
311
- if (!confirmed) {
312
- return { confirmed: false, removeGitWorktree: false };
313
- }
314
- const removeGitWorktree = await confirm({
315
- message: 'Git worktree와 브랜치도 함께 삭제할까요?',
316
- default: true,
317
- });
318
- return { confirmed, removeGitWorktree };
319
- }
320
- /**
321
- * 로컬 스레드 삭제 확인
322
- */
323
- export async function confirmDeleteLocal(threadName) {
324
- return await confirm({
325
- message: `'${threadName}' 로컬 스레드를 삭제할까요?`,
326
- default: true,
327
- });
328
- }