@ohzw/worktree-command-tui 0.1.1 → 0.1.3
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/README.md +91 -26
- package/dist/app.d.ts +1 -1
- package/dist/app.js +242 -114
- package/dist/components/ActionPanel.d.ts +4 -2
- package/dist/components/ActionPanel.js +87 -135
- package/dist/components/ContextBar.d.ts +4 -1
- package/dist/components/ContextBar.js +29 -3
- package/dist/components/Header.js +5 -1
- package/dist/components/HelpWindow.d.ts +7 -0
- package/dist/components/HelpWindow.js +29 -0
- package/dist/components/LogPanel.d.ts +10 -3
- package/dist/components/LogPanel.js +240 -33
- package/dist/components/WorktreeList.js +20 -40
- package/dist/core/command-runner.d.ts +11 -0
- package/dist/core/command-runner.js +59 -7
- package/dist/core/config-lifecycle.d.ts +25 -0
- package/dist/core/config-lifecycle.js +160 -0
- package/dist/core/config.d.ts +2 -3
- package/dist/core/config.js +0 -48
- package/dist/core/git-metadata.d.ts +25 -0
- package/dist/core/git-metadata.js +84 -0
- package/dist/core/git-worktrees.d.ts +2 -1
- package/dist/core/git-worktrees.js +30 -11
- package/dist/core/github-metadata.d.ts +21 -0
- package/dist/core/github-metadata.js +153 -0
- package/dist/core/init.d.ts +3 -2
- package/dist/core/init.js +9 -57
- package/dist/core/log-reader.d.ts +7 -0
- package/dist/core/log-reader.js +59 -0
- package/dist/core/posix-process.d.ts +2 -2
- package/dist/core/posix-process.js +19 -4
- package/dist/core/process-control.d.ts +2 -2
- package/dist/core/process-control.js +5 -2
- package/dist/core/runtime-state.d.ts +42 -0
- package/dist/core/runtime-state.js +125 -0
- package/dist/core/runtime.d.ts +19 -39
- package/dist/core/runtime.js +112 -216
- package/dist/core/session-store.js +22 -7
- package/dist/core/tui-interaction.d.ts +31 -0
- package/dist/core/tui-interaction.js +59 -0
- package/dist/core/worktree-projection.d.ts +76 -0
- package/dist/core/worktree-projection.js +132 -0
- package/dist/main.js +6 -5
- package/dist/terminal/viewport.d.ts +15 -0
- package/dist/terminal/viewport.js +49 -0
- package/package.json +1 -1
|
@@ -1,54 +1,261 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
import { sanitizeInlineText } from '../core/worktree-projection.js';
|
|
4
|
+
import { getScrollbarThumbRows, sliceTailViewport } from '../terminal/viewport.js';
|
|
5
|
+
const ESCAPE = '\u001B';
|
|
6
|
+
const CSI = '\u009B';
|
|
7
|
+
const BEL = '\u0007';
|
|
8
|
+
const STRING_TERMINATOR_PREFIX = '\u001B';
|
|
9
|
+
const STRING_TERMINATOR_SUFFIX = '\\';
|
|
10
|
+
const STRING_TERMINATOR = '\u009C';
|
|
11
|
+
const DEVICE_CONTROL_STRING = '\u0090';
|
|
12
|
+
const START_OF_STRING = '\u0098';
|
|
13
|
+
const OPERATING_SYSTEM_COMMAND = '\u009D';
|
|
14
|
+
const PRIVACY_MESSAGE = '\u009E';
|
|
15
|
+
const APPLICATION_PROGRAM_COMMAND = '\u009F';
|
|
16
|
+
const ANSI_FOREGROUND_COLORS = new Map([
|
|
17
|
+
[30, 'black'],
|
|
18
|
+
[31, 'red'],
|
|
19
|
+
[32, 'green'],
|
|
20
|
+
[33, 'yellow'],
|
|
21
|
+
[34, 'blue'],
|
|
22
|
+
[35, 'magenta'],
|
|
23
|
+
[36, 'cyan'],
|
|
24
|
+
[37, 'white'],
|
|
25
|
+
[90, 'gray'],
|
|
26
|
+
[91, 'red'],
|
|
27
|
+
[92, 'green'],
|
|
28
|
+
[93, 'yellow'],
|
|
29
|
+
[94, 'blue'],
|
|
30
|
+
[95, 'magenta'],
|
|
31
|
+
[96, 'cyan'],
|
|
32
|
+
[97, 'white'],
|
|
33
|
+
]);
|
|
34
|
+
function sanitizeTextChunk(value) {
|
|
5
35
|
return value
|
|
6
|
-
.replace(ANSI_ESCAPE_PATTERN, '')
|
|
7
36
|
.replace(/[\r\t\u2028\u2029]+/g, ' ')
|
|
8
37
|
.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f]/g, '')
|
|
9
|
-
.replace(/\p{Cf}/gu, '')
|
|
10
|
-
|
|
38
|
+
.replace(/\p{Cf}/gu, '');
|
|
39
|
+
}
|
|
40
|
+
function sameStyle(a, b) {
|
|
41
|
+
return a.color === b.color && a.dimColor === b.dimColor && a.bold === b.bold;
|
|
42
|
+
}
|
|
43
|
+
function appendSegment(segments, text, style) {
|
|
44
|
+
if (text.length === 0) {
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
const last = segments.at(-1);
|
|
48
|
+
if (last && sameStyle(last, style)) {
|
|
49
|
+
last.text += text;
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
segments.push({ ...style, text });
|
|
53
|
+
}
|
|
54
|
+
function skipExtendedColor(codes, index) {
|
|
55
|
+
const mode = Number(codes[index + 1] ?? Number.NaN);
|
|
56
|
+
if (mode === 5) {
|
|
57
|
+
return index + 2;
|
|
58
|
+
}
|
|
59
|
+
if (mode === 2) {
|
|
60
|
+
return index + 4;
|
|
61
|
+
}
|
|
62
|
+
return index;
|
|
63
|
+
}
|
|
64
|
+
function applyAnsiSgr(style, parameters) {
|
|
65
|
+
const codes = parameters.length === 0 ? ['0'] : parameters.split(';');
|
|
66
|
+
for (let index = 0; index < codes.length; index += 1) {
|
|
67
|
+
const rawCode = codes[index] ?? '0';
|
|
68
|
+
if (rawCode.includes(':')) {
|
|
69
|
+
if (Number(rawCode.split(':', 1)[0]) === 38) {
|
|
70
|
+
delete style.color;
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const code = Number(rawCode || 0);
|
|
75
|
+
if (code === 0) {
|
|
76
|
+
delete style.color;
|
|
77
|
+
delete style.dimColor;
|
|
78
|
+
delete style.bold;
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
if (code === 1) {
|
|
82
|
+
style.bold = true;
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
if (code === 2) {
|
|
86
|
+
style.dimColor = true;
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
if (code === 22) {
|
|
90
|
+
delete style.bold;
|
|
91
|
+
delete style.dimColor;
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (code === 38) {
|
|
95
|
+
delete style.color;
|
|
96
|
+
index = skipExtendedColor(codes, index);
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (code === 48 || code === 58) {
|
|
100
|
+
index = skipExtendedColor(codes, index);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (code === 39) {
|
|
104
|
+
delete style.color;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
const color = ANSI_FOREGROUND_COLORS.get(code);
|
|
108
|
+
if (color) {
|
|
109
|
+
style.color = color;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
function trimLineEnd(segments) {
|
|
114
|
+
const result = [...segments];
|
|
115
|
+
while (result.length > 0) {
|
|
116
|
+
const last = result.at(-1);
|
|
117
|
+
const trimmed = last.text.trimEnd();
|
|
118
|
+
if (trimmed.length > 0) {
|
|
119
|
+
if (trimmed.length !== last.text.length) {
|
|
120
|
+
result[result.length - 1] = { ...last, text: trimmed };
|
|
121
|
+
}
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
result.pop();
|
|
125
|
+
}
|
|
126
|
+
return result;
|
|
127
|
+
}
|
|
128
|
+
function pushLine(lines, segments) {
|
|
129
|
+
const trimmedSegments = trimLineEnd(segments);
|
|
130
|
+
lines.push({ segments: trimmedSegments.length > 0 ? trimmedSegments : [{ text: ' ' }] });
|
|
131
|
+
}
|
|
132
|
+
function isAnsiFinalByte(value) {
|
|
133
|
+
const code = value.charCodeAt(0);
|
|
134
|
+
return code >= 0x40 && code <= 0x7E;
|
|
135
|
+
}
|
|
136
|
+
function isAnsiIntermediateByte(value) {
|
|
137
|
+
const code = value.charCodeAt(0);
|
|
138
|
+
return code >= 0x20 && code <= 0x2F;
|
|
139
|
+
}
|
|
140
|
+
function consumeCsi(value, index, style) {
|
|
141
|
+
let cursor = index;
|
|
142
|
+
while (cursor < value.length && !isAnsiFinalByte(value[cursor])) {
|
|
143
|
+
cursor += 1;
|
|
144
|
+
}
|
|
145
|
+
if (cursor >= value.length) {
|
|
146
|
+
return value.length;
|
|
147
|
+
}
|
|
148
|
+
const finalByte = value[cursor];
|
|
149
|
+
const sequence = value.slice(index, cursor);
|
|
150
|
+
const hasIntermediate = Array.from(sequence).some(isAnsiIntermediateByte);
|
|
151
|
+
if (!hasIntermediate && finalByte === 'm') {
|
|
152
|
+
applyAnsiSgr(style, sequence);
|
|
153
|
+
}
|
|
154
|
+
return cursor + 1;
|
|
155
|
+
}
|
|
156
|
+
function consumeStringControl(value, index, allowBelTerminator) {
|
|
157
|
+
let cursor = index;
|
|
158
|
+
while (cursor < value.length) {
|
|
159
|
+
if ((allowBelTerminator && value[cursor] === BEL) || value[cursor] === STRING_TERMINATOR) {
|
|
160
|
+
return cursor + 1;
|
|
161
|
+
}
|
|
162
|
+
if (value[cursor] === STRING_TERMINATOR_PREFIX && value[cursor + 1] === STRING_TERMINATOR_SUFFIX) {
|
|
163
|
+
return cursor + 2;
|
|
164
|
+
}
|
|
165
|
+
cursor += 1;
|
|
166
|
+
}
|
|
167
|
+
return value.length;
|
|
168
|
+
}
|
|
169
|
+
function isStringControl(value) {
|
|
170
|
+
return value === DEVICE_CONTROL_STRING
|
|
171
|
+
|| value === START_OF_STRING
|
|
172
|
+
|| value === OPERATING_SYSTEM_COMMAND
|
|
173
|
+
|| value === PRIVACY_MESSAGE
|
|
174
|
+
|| value === APPLICATION_PROGRAM_COMMAND;
|
|
175
|
+
}
|
|
176
|
+
function consumeEscape(value, index, style) {
|
|
177
|
+
if (value[index] === CSI) {
|
|
178
|
+
return consumeCsi(value, index + 1, style);
|
|
179
|
+
}
|
|
180
|
+
if (isStringControl(value[index])) {
|
|
181
|
+
return consumeStringControl(value, index + 1, value[index] === OPERATING_SYSTEM_COMMAND);
|
|
182
|
+
}
|
|
183
|
+
const next = value[index + 1];
|
|
184
|
+
if (next === undefined) {
|
|
185
|
+
return index + 1;
|
|
186
|
+
}
|
|
187
|
+
if (next === '[') {
|
|
188
|
+
return consumeCsi(value, index + 2, style);
|
|
189
|
+
}
|
|
190
|
+
if (next === ']') {
|
|
191
|
+
return consumeStringControl(value, index + 2, true);
|
|
192
|
+
}
|
|
193
|
+
if (next === 'P' || next === '^' || next === '_' || next === 'X') {
|
|
194
|
+
return consumeStringControl(value, index + 2, false);
|
|
195
|
+
}
|
|
196
|
+
let cursor = index + 1;
|
|
197
|
+
while (cursor < value.length && isAnsiIntermediateByte(value[cursor])) {
|
|
198
|
+
cursor += 1;
|
|
199
|
+
}
|
|
200
|
+
return Math.min(cursor + 1, value.length);
|
|
201
|
+
}
|
|
202
|
+
function parseLogContent(value) {
|
|
203
|
+
const lines = [];
|
|
204
|
+
let segments = [];
|
|
205
|
+
const style = {};
|
|
206
|
+
let textStart = 0;
|
|
207
|
+
let index = 0;
|
|
208
|
+
while (index < value.length) {
|
|
209
|
+
const character = value[index];
|
|
210
|
+
if (character === '\n') {
|
|
211
|
+
appendSegment(segments, sanitizeTextChunk(value.slice(textStart, index)), style);
|
|
212
|
+
pushLine(lines, segments);
|
|
213
|
+
segments = [];
|
|
214
|
+
index += 1;
|
|
215
|
+
textStart = index;
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
if (character === ESCAPE || character === CSI || isStringControl(character)) {
|
|
219
|
+
appendSegment(segments, sanitizeTextChunk(value.slice(textStart, index)), style);
|
|
220
|
+
index = consumeEscape(value, index, style);
|
|
221
|
+
textStart = index;
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
index += 1;
|
|
225
|
+
}
|
|
226
|
+
appendSegment(segments, sanitizeTextChunk(value.slice(textStart)), style);
|
|
227
|
+
pushLine(lines, segments);
|
|
228
|
+
return lines;
|
|
229
|
+
}
|
|
230
|
+
function plainLine(text, style = {}) {
|
|
231
|
+
return { segments: [{ ...style, text }] };
|
|
232
|
+
}
|
|
233
|
+
function getLineText(line) {
|
|
234
|
+
return line.segments.map(segment => segment.text).join('');
|
|
11
235
|
}
|
|
12
236
|
export function buildLogLines(logs) {
|
|
13
237
|
if (logs.length === 0) {
|
|
14
|
-
return [
|
|
238
|
+
return [plainLine('No *.log files yet.', { dimColor: true })];
|
|
15
239
|
}
|
|
16
240
|
const lines = [];
|
|
17
241
|
for (const [index, log] of logs.entries()) {
|
|
18
242
|
if (index > 0) {
|
|
19
|
-
lines.push(
|
|
20
|
-
}
|
|
21
|
-
lines.push({ text: `[${log.name}]`, color: 'cyan' });
|
|
22
|
-
const contentLines = log.content.length > 0 ? log.content.split('\n') : ['(empty)'];
|
|
23
|
-
for (const line of contentLines) {
|
|
24
|
-
lines.push({ text: sanitizeLogLine(line) || ' ' });
|
|
243
|
+
lines.push(plainLine(' ', { dimColor: true }));
|
|
25
244
|
}
|
|
245
|
+
lines.push(plainLine(`[${sanitizeInlineText(log.name)}]`, { color: 'cyan' }));
|
|
246
|
+
lines.push(...parseLogContent(log.content.length > 0 ? log.content : '(empty)'));
|
|
26
247
|
}
|
|
27
248
|
return lines;
|
|
28
249
|
}
|
|
29
|
-
function getScrollbarThumbRows(totalLines, viewportHeight, scrollOffset) {
|
|
30
|
-
if (totalLines <= viewportHeight) {
|
|
31
|
-
return new Set();
|
|
32
|
-
}
|
|
33
|
-
const thumbSize = Math.max(1, Math.floor((viewportHeight / totalLines) * viewportHeight));
|
|
34
|
-
const maxScrollOffset = Math.max(1, totalLines - viewportHeight);
|
|
35
|
-
const thumbStart = Math.round((scrollOffset / maxScrollOffset) * (viewportHeight - thumbSize));
|
|
36
|
-
return new Set(Array.from({ length: thumbSize }, (_, index) => thumbStart + index));
|
|
37
|
-
}
|
|
38
250
|
export function LogPanel({ logs, width, height, scrollOffset = 0, title = 'Logs (*.log · tail 120)', }) {
|
|
39
251
|
const lines = buildLogLines(logs);
|
|
40
|
-
const
|
|
41
|
-
const
|
|
42
|
-
const
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
: Math.max(0, lines.length - contentViewportHeight - effectiveScrollOffset);
|
|
46
|
-
const visibleLines = contentViewportHeight === undefined
|
|
47
|
-
? lines
|
|
48
|
-
: lines.slice(startIndex, startIndex + contentViewportHeight);
|
|
49
|
-
const showScrollbar = contentViewportHeight !== undefined && lines.length > contentViewportHeight;
|
|
252
|
+
const viewport = sliceTailViewport(lines, height === undefined ? lines.length : height - 3, scrollOffset);
|
|
253
|
+
const contentViewportHeight = viewport.viewportHeight;
|
|
254
|
+
const startIndex = viewport.startIndex;
|
|
255
|
+
const visibleLines = viewport.visibleItems;
|
|
256
|
+
const showScrollbar = height !== undefined && lines.length > contentViewportHeight;
|
|
50
257
|
const scrollbarThumbRows = showScrollbar
|
|
51
|
-
? getScrollbarThumbRows(lines.length, contentViewportHeight,
|
|
258
|
+
? getScrollbarThumbRows(lines.length, contentViewportHeight, viewport.topScrollOffset)
|
|
52
259
|
: new Set();
|
|
53
|
-
return (_jsxs(Box, { width: width, height: height, borderStyle: "round", borderColor: "yellow", flexDirection: "column", paddingX: 1, overflow: "hidden", children: [_jsx(Text, { bold: true, color: "yellow", wrap: "truncate-end", children: title }), _jsx(Box, { height: contentViewportHeight, flexDirection: "column", overflow: "hidden", children: visibleLines.map((line, index) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, children: _jsx(Text, {
|
|
260
|
+
return (_jsxs(Box, { width: width, height: height, borderStyle: "round", borderColor: "yellow", flexDirection: "column", paddingX: 1, overflow: "hidden", children: [_jsx(Text, { bold: true, color: "yellow", wrap: "truncate-end", children: title }), _jsx(Box, { height: contentViewportHeight, flexDirection: "column", overflow: "hidden", children: visibleLines.map((line, index) => (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, children: _jsx(Text, { wrap: "truncate-end", children: line.segments.map((segment, segmentIndex) => (_jsx(Text, { color: segment.color, dimColor: segment.dimColor, bold: segment.bold, children: segment.text }, `${segmentIndex}-${segment.text}`))) }) }), showScrollbar ? (_jsx(Text, { color: scrollbarThumbRows.has(index) ? 'yellow' : 'gray', dimColor: !scrollbarThumbRows.has(index), children: scrollbarThumbRows.has(index) ? '█' : '│' })) : null] }, `${startIndex + index}-${getLineText(line)}`))) })] }));
|
|
54
261
|
}
|
|
@@ -1,45 +1,30 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, Text } from 'ink';
|
|
3
|
+
import { projectWorktreeListRow, sanitizeInlineText } from '../core/worktree-projection.js';
|
|
4
|
+
import { getScrollbarThumbRows, sliceListViewport } from '../terminal/viewport.js';
|
|
3
5
|
const MIN_BRANCH_WIDTH = 24;
|
|
4
|
-
function
|
|
5
|
-
|
|
6
|
-
.replace(/[\r\n\t\u2028\u2029]+/g, ' ')
|
|
7
|
-
.replace(/[\u0000-\u001f\u007f-\u009f]/g, '')
|
|
8
|
-
.replace(/\p{Cf}/gu, '')
|
|
9
|
-
.replace(/\s+/g, ' ')
|
|
10
|
-
.trim();
|
|
11
|
-
}
|
|
12
|
-
function getIndicator(row) {
|
|
13
|
-
if (row.tags.includes('active')) {
|
|
6
|
+
function getIndicator(state) {
|
|
7
|
+
if (state === 'active') {
|
|
14
8
|
return '*';
|
|
15
9
|
}
|
|
16
|
-
if (
|
|
10
|
+
if (state === 'invalid') {
|
|
17
11
|
return '!';
|
|
18
12
|
}
|
|
19
|
-
if (
|
|
13
|
+
if (state === 'external') {
|
|
20
14
|
return '^';
|
|
21
15
|
}
|
|
22
|
-
if (row.tags.includes('main')) {
|
|
23
|
-
return '#';
|
|
24
|
-
}
|
|
25
16
|
return '-';
|
|
26
17
|
}
|
|
27
|
-
function getRowColor(
|
|
28
|
-
if (
|
|
18
|
+
function getRowColor(projection) {
|
|
19
|
+
if (projection.state === 'active') {
|
|
29
20
|
return 'green';
|
|
30
21
|
}
|
|
31
|
-
if (isSelected) {
|
|
22
|
+
if (projection.isSelected) {
|
|
32
23
|
return 'cyan';
|
|
33
24
|
}
|
|
34
|
-
if (
|
|
25
|
+
if (projection.state === 'invalid') {
|
|
35
26
|
return 'red';
|
|
36
27
|
}
|
|
37
|
-
if (row.tags.includes('external')) {
|
|
38
|
-
return 'yellow';
|
|
39
|
-
}
|
|
40
|
-
if (row.tags.includes('main')) {
|
|
41
|
-
return 'blue';
|
|
42
|
-
}
|
|
43
28
|
return undefined;
|
|
44
29
|
}
|
|
45
30
|
function truncateLabel(value, width) {
|
|
@@ -48,26 +33,21 @@ function truncateLabel(value, width) {
|
|
|
48
33
|
}
|
|
49
34
|
return `${value.slice(0, Math.max(width - 1, 0))}…`;
|
|
50
35
|
}
|
|
51
|
-
function getScrollbarThumbRows(totalLines, viewportHeight, scrollOffset) {
|
|
52
|
-
if (totalLines <= viewportHeight) {
|
|
53
|
-
return new Set();
|
|
54
|
-
}
|
|
55
|
-
const thumbSize = Math.max(1, Math.floor((viewportHeight / totalLines) * viewportHeight));
|
|
56
|
-
const maxScrollOffset = Math.max(1, totalLines - viewportHeight);
|
|
57
|
-
const thumbStart = Math.round((scrollOffset / maxScrollOffset) * (viewportHeight - thumbSize));
|
|
58
|
-
return new Set(Array.from({ length: thumbSize }, (_, index) => thumbStart + index));
|
|
59
|
-
}
|
|
60
36
|
export function WorktreeList({ rows, selectedIndex, width, height, stacked, scrollOffset = 0, }) {
|
|
61
37
|
const branchWidth = Math.max(MIN_BRANCH_WIDTH, (width ?? 34) - 7);
|
|
62
|
-
const
|
|
63
|
-
const
|
|
64
|
-
const effectiveScrollOffset =
|
|
65
|
-
const visibleRows =
|
|
38
|
+
const viewport = sliceListViewport(rows, height === undefined ? rows.length : height - 3, scrollOffset);
|
|
39
|
+
const contentViewportHeight = viewport.viewportHeight;
|
|
40
|
+
const effectiveScrollOffset = viewport.scrollOffset;
|
|
41
|
+
const visibleRows = viewport.visibleItems;
|
|
66
42
|
const showScrollbar = height !== undefined && rows.length > contentViewportHeight;
|
|
67
43
|
const scrollbarThumbRows = showScrollbar ? getScrollbarThumbRows(rows.length, contentViewportHeight, effectiveScrollOffset) : new Set();
|
|
68
44
|
return (_jsxs(Box, { width: width, height: height, flexGrow: stacked ? 0 : 1, marginRight: stacked ? 0 : 1, borderStyle: "round", borderColor: "cyan", flexDirection: "column", paddingX: 1, overflowY: "hidden", children: [_jsx(Text, { bold: true, color: "cyan", children: "Worktrees" }), visibleRows.map((row, index) => {
|
|
69
45
|
const isSelected = index + effectiveScrollOffset === selectedIndex;
|
|
70
|
-
const
|
|
71
|
-
|
|
46
|
+
const projection = projectWorktreeListRow(row, isSelected);
|
|
47
|
+
const tagSuffix = projection.isMain ? ' [root]' : '';
|
|
48
|
+
const color = getRowColor(projection);
|
|
49
|
+
const branchText = sanitizeInlineText(row.branch);
|
|
50
|
+
const line = `${isSelected ? '>' : ' '} ${getIndicator(projection.state)} ${truncateLabel(branchText, Math.max(1, branchWidth - tagSuffix.length))}${tagSuffix}`;
|
|
51
|
+
return (_jsxs(Box, { flexDirection: "row", children: [_jsx(Box, { flexGrow: 1, flexShrink: 1, children: _jsx(Text, { color: color, dimColor: !isSelected && color === undefined, bold: projection.state === 'active', wrap: "truncate-end", children: line }, row.path) }), showScrollbar ? (_jsx(Text, { color: scrollbarThumbRows.has(index) ? 'cyan' : 'gray', dimColor: !scrollbarThumbRows.has(index), children: scrollbarThumbRows.has(index) ? '█' : '│' })) : null] }, row.path));
|
|
72
52
|
})] }));
|
|
73
53
|
}
|
|
@@ -1,3 +1,13 @@
|
|
|
1
|
+
interface CommandLogOptions {
|
|
2
|
+
command: string[];
|
|
3
|
+
cwd: string;
|
|
4
|
+
logsDir: string;
|
|
5
|
+
logFileBase: string;
|
|
6
|
+
errorLabel?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function runCommandToLog({ command, cwd, logsDir, logFileBase, errorLabel, }: CommandLogOptions): Promise<{
|
|
9
|
+
logPath: string;
|
|
10
|
+
}>;
|
|
1
11
|
export declare function startDetachedCommand({ command, cwd, logsDir, logFileBase, }: {
|
|
2
12
|
command: string[];
|
|
3
13
|
cwd: string;
|
|
@@ -8,3 +18,4 @@ export declare function startDetachedCommand({ command, cwd, logsDir, logFileBas
|
|
|
8
18
|
pgid: number;
|
|
9
19
|
logPath: string;
|
|
10
20
|
}>;
|
|
21
|
+
export {};
|
|
@@ -1,6 +1,53 @@
|
|
|
1
1
|
import { closeSync, mkdirSync, openSync } from 'node:fs';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
+
function getLogEnvironment() {
|
|
5
|
+
return {
|
|
6
|
+
...process.env,
|
|
7
|
+
FORCE_COLOR: process.env.FORCE_COLOR ?? '1',
|
|
8
|
+
CLICOLOR_FORCE: process.env.CLICOLOR_FORCE ?? '1',
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function runCommandToLog({ command, cwd, logsDir, logFileBase, errorLabel = 'setup command', }) {
|
|
12
|
+
mkdirSync(logsDir, { recursive: true });
|
|
13
|
+
const logPath = path.join(logsDir, `${logFileBase}.log`);
|
|
14
|
+
const fd = openSync(logPath, 'a');
|
|
15
|
+
const { promise, resolve, reject } = Promise.withResolvers();
|
|
16
|
+
let settled = false;
|
|
17
|
+
const finalize = () => {
|
|
18
|
+
if (settled) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
settled = true;
|
|
22
|
+
closeSync(fd);
|
|
23
|
+
return true;
|
|
24
|
+
};
|
|
25
|
+
const child = spawn(command[0], command.slice(1), {
|
|
26
|
+
cwd,
|
|
27
|
+
env: getLogEnvironment(),
|
|
28
|
+
stdio: ['ignore', fd, fd],
|
|
29
|
+
});
|
|
30
|
+
child.once('error', error => {
|
|
31
|
+
if (finalize()) {
|
|
32
|
+
reject(error);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
child.once('exit', (code, signal) => {
|
|
36
|
+
if (!finalize()) {
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
if (code === 0) {
|
|
40
|
+
resolve({ logPath });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (code !== null) {
|
|
44
|
+
reject(new Error(`${errorLabel} exited with code ${code}; see ${logPath}`));
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
reject(new Error(`${errorLabel} exited due to signal ${signal ?? 'unknown'}; see ${logPath}`));
|
|
48
|
+
});
|
|
49
|
+
return promise;
|
|
50
|
+
}
|
|
4
51
|
export function startDetachedCommand({ command, cwd, logsDir, logFileBase, }) {
|
|
5
52
|
mkdirSync(logsDir, { recursive: true });
|
|
6
53
|
const logPath = path.join(logsDir, `${logFileBase}.log`);
|
|
@@ -10,29 +57,34 @@ export function startDetachedCommand({ command, cwd, logsDir, logFileBase, }) {
|
|
|
10
57
|
const child = spawn(command[0], command.slice(1), {
|
|
11
58
|
cwd,
|
|
12
59
|
detached: true,
|
|
60
|
+
env: getLogEnvironment(),
|
|
13
61
|
stdio: ['ignore', fd, fd],
|
|
14
62
|
});
|
|
15
63
|
const finalize = () => {
|
|
16
64
|
if (settled) {
|
|
17
|
-
return;
|
|
65
|
+
return false;
|
|
18
66
|
}
|
|
19
67
|
settled = true;
|
|
20
68
|
closeSync(fd);
|
|
69
|
+
return true;
|
|
21
70
|
};
|
|
22
71
|
child.once('error', error => {
|
|
23
|
-
finalize()
|
|
24
|
-
|
|
72
|
+
if (finalize()) {
|
|
73
|
+
reject(error);
|
|
74
|
+
}
|
|
25
75
|
});
|
|
26
76
|
child.once('spawn', () => {
|
|
27
77
|
const pid = child.pid;
|
|
28
78
|
if (pid === undefined) {
|
|
29
|
-
finalize()
|
|
30
|
-
|
|
79
|
+
if (finalize()) {
|
|
80
|
+
reject(new Error('spawn succeeded without pid'));
|
|
81
|
+
}
|
|
31
82
|
return;
|
|
32
83
|
}
|
|
33
84
|
child.unref();
|
|
34
|
-
finalize()
|
|
35
|
-
|
|
85
|
+
if (finalize()) {
|
|
86
|
+
resolve({ pid, pgid: pid, logPath });
|
|
87
|
+
}
|
|
36
88
|
});
|
|
37
89
|
return promise;
|
|
38
90
|
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { type ToolConfig } from './config.js';
|
|
2
|
+
export interface DefaultToolConfigOptions {
|
|
3
|
+
namespaceSeed: string;
|
|
4
|
+
packageManager: 'bun' | 'pnpm' | 'yarn' | 'npm';
|
|
5
|
+
script: string;
|
|
6
|
+
}
|
|
7
|
+
export interface ConfigInitResult {
|
|
8
|
+
path: string;
|
|
9
|
+
config: ToolConfig;
|
|
10
|
+
}
|
|
11
|
+
export interface ConfigInitOptions {
|
|
12
|
+
workspaceRoot: string;
|
|
13
|
+
force: boolean;
|
|
14
|
+
config: ToolConfig;
|
|
15
|
+
}
|
|
16
|
+
export declare function isSafeNamespace(value: unknown): value is string;
|
|
17
|
+
export declare function toSafeNamespace(value: string): string;
|
|
18
|
+
export declare function validateToolConfig(raw: unknown): ToolConfig;
|
|
19
|
+
export declare function createDefaultToolConfig(options: DefaultToolConfigOptions): ToolConfig;
|
|
20
|
+
export declare function loadToolConfig({ repoRoot }: {
|
|
21
|
+
repoRoot: string;
|
|
22
|
+
}): Promise<ToolConfig>;
|
|
23
|
+
export declare function renderConfigJsonc(config: ToolConfig): string;
|
|
24
|
+
export declare function findExistingConfigPath(workspaceRoot: string, fileExists: (filePath: string) => Promise<boolean>): Promise<string | null>;
|
|
25
|
+
export declare function writeToolConfigForRepo({ workspaceRoot, force, config }: ConfigInitOptions, fileExists: (filePath: string) => Promise<boolean>): Promise<ConfigInitResult>;
|