gitforest 1.1.2 → 1.2.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/package.json +1 -1
- package/src/app.tsx +57 -1
- package/src/cli/commands/auth.ts +75 -0
- package/src/cli/commands/batch.ts +141 -0
- package/src/cli/commands/dir.ts +285 -0
- package/src/cli/commands/github.ts +238 -0
- package/src/cli/commands/helpers.ts +15 -0
- package/src/cli/commands/list.ts +76 -0
- package/src/cli/commands/setup.ts +86 -0
- package/src/cli/index.ts +7 -582
- package/src/components/AddDirectoryDialog.tsx +151 -0
- package/src/components/HelpOverlay.tsx +1 -0
- package/src/components/Layout.tsx +44 -99
- package/src/components/UnifiedProjectItem.tsx +1 -1
- package/src/components/onboarding/DirectoriesStep.unit.test.ts +34 -34
- package/src/constants.ts +2 -0
- package/src/git/index.ts +1 -2
- package/src/github/unified.ts +3 -4
- package/src/hooks/useBackgroundFetch.ts +1 -1
- package/src/hooks/useConfirmDialogActions.ts +1 -1
- package/src/hooks/useKeyBindings.ts +181 -131
- package/src/hooks/useUnifiedRepos.ts +54 -44
- package/src/index.tsx +48 -7
- package/src/scanner/index.ts +34 -20
- package/src/services/editor.ts +121 -0
- package/src/state/actions.ts +23 -2
- package/src/state/reducer.ts +23 -0
- package/src/types/index.ts +14 -2
- package/src/git/service.ts +0 -539
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { Box, Text, useInput } from "ink";
|
|
2
|
+
import { homedir } from "os";
|
|
3
|
+
import {
|
|
4
|
+
HybridDirectoryBrowser,
|
|
5
|
+
formatDisplayPath,
|
|
6
|
+
} from "./onboarding/DirectoriesStep.tsx";
|
|
7
|
+
import type { AddDirectoryDialogState } from "../types/index.ts";
|
|
8
|
+
|
|
9
|
+
export interface AddDirectoryDialogProps {
|
|
10
|
+
state: AddDirectoryDialogState;
|
|
11
|
+
onUpdate: (updates: Partial<AddDirectoryDialogState>) => void;
|
|
12
|
+
onConfirm: (path: string, maxDepth: number, label: string) => void;
|
|
13
|
+
onCancel: () => void;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function AddDirectoryDialog({
|
|
17
|
+
state,
|
|
18
|
+
onUpdate,
|
|
19
|
+
onConfirm,
|
|
20
|
+
onCancel,
|
|
21
|
+
}: AddDirectoryDialogProps) {
|
|
22
|
+
const { step, selectedPath, maxDepthInput, labelInput, error } = state;
|
|
23
|
+
|
|
24
|
+
// Handle maxDepth and label input
|
|
25
|
+
useInput((input, key) => {
|
|
26
|
+
// Browse mode is handled entirely by HybridDirectoryBrowser
|
|
27
|
+
if (step === "browse") return;
|
|
28
|
+
|
|
29
|
+
if (key.escape) {
|
|
30
|
+
if (step === "maxDepth") {
|
|
31
|
+
// Go back to browser
|
|
32
|
+
onUpdate({ step: "browse", selectedPath: null, error: null });
|
|
33
|
+
} else if (step === "label") {
|
|
34
|
+
// Go back to maxDepth
|
|
35
|
+
onUpdate({ step: "maxDepth", error: null });
|
|
36
|
+
}
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (key.return) {
|
|
41
|
+
if (step === "maxDepth") {
|
|
42
|
+
const depth = parseInt(maxDepthInput, 10);
|
|
43
|
+
if (isNaN(depth) || depth < 0 || depth > 10) {
|
|
44
|
+
onUpdate({ error: "Max depth must be between 0 and 10" });
|
|
45
|
+
return;
|
|
46
|
+
}
|
|
47
|
+
onUpdate({ step: "label", error: null });
|
|
48
|
+
} else if (step === "label") {
|
|
49
|
+
if (!selectedPath) return;
|
|
50
|
+
const depth = parseInt(maxDepthInput, 10);
|
|
51
|
+
onConfirm(selectedPath, isNaN(depth) ? 2 : depth, labelInput.trim());
|
|
52
|
+
}
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (step === "maxDepth") {
|
|
57
|
+
if ((key as any).backspace || (key as any).delete) {
|
|
58
|
+
onUpdate({ maxDepthInput: maxDepthInput.slice(0, -1) });
|
|
59
|
+
} else if (/^[0-9]$/.test(input)) {
|
|
60
|
+
onUpdate({ maxDepthInput: maxDepthInput + input });
|
|
61
|
+
}
|
|
62
|
+
} else if (step === "label") {
|
|
63
|
+
if ((key as any).backspace || (key as any).delete) {
|
|
64
|
+
onUpdate({ labelInput: labelInput.slice(0, -1) });
|
|
65
|
+
} else if (input.length === 1) {
|
|
66
|
+
onUpdate({ labelInput: labelInput + input });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const handleBrowserSelect = (path: string) => {
|
|
72
|
+
onUpdate({
|
|
73
|
+
selectedPath: path,
|
|
74
|
+
step: "maxDepth",
|
|
75
|
+
maxDepthInput: "2",
|
|
76
|
+
labelInput: "",
|
|
77
|
+
error: null,
|
|
78
|
+
});
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const handleBrowserCancel = () => {
|
|
82
|
+
onCancel();
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
return (
|
|
86
|
+
<Box
|
|
87
|
+
flexDirection="column"
|
|
88
|
+
borderStyle="round"
|
|
89
|
+
borderColor="cyan"
|
|
90
|
+
paddingX={2}
|
|
91
|
+
paddingY={1}
|
|
92
|
+
width={80}
|
|
93
|
+
>
|
|
94
|
+
<Box marginBottom={1}>
|
|
95
|
+
<Text bold color="cyan">
|
|
96
|
+
Add Directory
|
|
97
|
+
</Text>
|
|
98
|
+
</Box>
|
|
99
|
+
|
|
100
|
+
{/* Step 1: Browse for directory */}
|
|
101
|
+
{step === "browse" && (
|
|
102
|
+
<HybridDirectoryBrowser
|
|
103
|
+
startingPath={homedir()}
|
|
104
|
+
onSelect={handleBrowserSelect}
|
|
105
|
+
onCancel={handleBrowserCancel}
|
|
106
|
+
/>
|
|
107
|
+
)}
|
|
108
|
+
|
|
109
|
+
{/* Step 2: Max depth input */}
|
|
110
|
+
{step === "maxDepth" && (
|
|
111
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
112
|
+
<Text>Max scan depth (0-10, default 2):</Text>
|
|
113
|
+
<Box gap={1}>
|
|
114
|
+
<Text color="yellow">{"> "}</Text>
|
|
115
|
+
<Text color="yellow">{maxDepthInput}</Text>
|
|
116
|
+
<Text color="yellow" inverse>_</Text>
|
|
117
|
+
</Box>
|
|
118
|
+
<Text dimColor>Path: {formatDisplayPath(selectedPath || "")}</Text>
|
|
119
|
+
{error && <Text color="red">{error}</Text>}
|
|
120
|
+
<Box marginTop={1} gap={2}>
|
|
121
|
+
<Text dimColor>Enter:</Text>
|
|
122
|
+
<Text dimColor>continue</Text>
|
|
123
|
+
<Text dimColor>Esc:</Text>
|
|
124
|
+
<Text dimColor>back to browser</Text>
|
|
125
|
+
</Box>
|
|
126
|
+
</Box>
|
|
127
|
+
)}
|
|
128
|
+
|
|
129
|
+
{/* Step 3: Label input */}
|
|
130
|
+
{step === "label" && (
|
|
131
|
+
<Box flexDirection="column" marginBottom={1}>
|
|
132
|
+
<Text>Label (optional, press Enter to skip):</Text>
|
|
133
|
+
<Box gap={1}>
|
|
134
|
+
<Text color="yellow">{"> "}</Text>
|
|
135
|
+
<Text color="yellow">{labelInput}</Text>
|
|
136
|
+
<Text color="yellow" inverse>_</Text>
|
|
137
|
+
</Box>
|
|
138
|
+
<Text dimColor>Path: {formatDisplayPath(selectedPath || "")}</Text>
|
|
139
|
+
<Text dimColor>Depth: {maxDepthInput}</Text>
|
|
140
|
+
{error && <Text color="red">{error}</Text>}
|
|
141
|
+
<Box marginTop={1} gap={2}>
|
|
142
|
+
<Text dimColor>Enter:</Text>
|
|
143
|
+
<Text dimColor>confirm</Text>
|
|
144
|
+
<Text dimColor>Esc:</Text>
|
|
145
|
+
<Text dimColor>back to depth</Text>
|
|
146
|
+
</Box>
|
|
147
|
+
</Box>
|
|
148
|
+
)}
|
|
149
|
+
</Box>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -58,6 +58,7 @@ const HELP_SECTIONS = [
|
|
|
58
58
|
{ key: "/", desc: "Filter projects" },
|
|
59
59
|
{ key: "s", desc: "Cycle sort field" },
|
|
60
60
|
{ key: "x", desc: "Command palette" },
|
|
61
|
+
{ key: "+", desc: "Add directory" },
|
|
61
62
|
{ key: "r", desc: "Refresh" },
|
|
62
63
|
{ key: "?", desc: "Toggle help" },
|
|
63
64
|
{ key: "Esc", desc: "Cancel / exit mode" },
|
|
@@ -7,21 +7,24 @@ import { ConfirmDialog } from "./ConfirmDialog.tsx";
|
|
|
7
7
|
import { CloneDialog } from "./CloneDialog.tsx";
|
|
8
8
|
import { RepoDetailModal } from "./RepoDetailModal.tsx";
|
|
9
9
|
import { CommandPalette } from "./CommandPalette.tsx";
|
|
10
|
+
import { AddDirectoryDialog } from "./AddDirectoryDialog.tsx";
|
|
10
11
|
import { useStore, useFilteredUnifiedRepos, useSelectedUnifiedRepos } from "../state/store.tsx";
|
|
11
12
|
import { useConfirmDialogActions } from "../hooks/useConfirmDialogActions.ts";
|
|
12
|
-
import { startAction, endAction, setMessage } from "../state/actions.ts";
|
|
13
|
+
import { startAction, endAction, setMessage, updateAddDirectoryDialog, hideAddDirectoryDialog } from "../state/actions.ts";
|
|
13
14
|
import { executeCommand } from "../operations/commands.ts";
|
|
14
15
|
import { errorToString } from "../utils/errors.ts";
|
|
16
|
+
import { openInEditor, openInBrowser } from "../services/editor.ts";
|
|
15
17
|
import { UI } from "../constants.ts";
|
|
16
|
-
import type { GitforestConfig, CommandConfig } from "../types/index.ts";
|
|
18
|
+
import type { GitforestConfig, CommandConfig, UnifiedRepo } from "../types/index.ts";
|
|
17
19
|
|
|
18
20
|
interface LayoutProps {
|
|
19
21
|
config: GitforestConfig;
|
|
20
|
-
onRefresh: () => Promise<void>;
|
|
21
|
-
onClone?: (repos:
|
|
22
|
+
onRefresh: (options?: { forceRefresh?: boolean }) => Promise<void>;
|
|
23
|
+
onClone?: (repos: UnifiedRepo[], targetDir: string, useSSH: boolean) => Promise<void>;
|
|
24
|
+
onAddDirectory?: (path: string, maxDepth: number, label: string) => Promise<{ success: boolean; error?: string }>;
|
|
22
25
|
}
|
|
23
26
|
|
|
24
|
-
export function Layout({ config, onRefresh, onClone }: LayoutProps) {
|
|
27
|
+
export function Layout({ config, onRefresh, onClone, onAddDirectory }: LayoutProps) {
|
|
25
28
|
const { state, dispatch } = useStore();
|
|
26
29
|
const { mode, confirmDialog, cloneDialog } = state;
|
|
27
30
|
const filteredRepos = useFilteredUnifiedRepos();
|
|
@@ -178,13 +181,9 @@ export function Layout({ config, onRefresh, onClone }: LayoutProps) {
|
|
|
178
181
|
break;
|
|
179
182
|
case "browser":
|
|
180
183
|
if (repo.github?.htmlUrl) {
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
stderr: "ignore",
|
|
185
|
-
});
|
|
186
|
-
} catch (error) {
|
|
187
|
-
dispatch(setMessage(`Failed to open browser: ${errorToString(error)}`));
|
|
184
|
+
const browserResult = await openInBrowser(repo.github.htmlUrl);
|
|
185
|
+
if (!browserResult.success) {
|
|
186
|
+
dispatch(setMessage(`Failed to open browser: ${browserResult.error}`));
|
|
188
187
|
}
|
|
189
188
|
} else {
|
|
190
189
|
dispatch(setMessage("No GitHub URL available for this repository"));
|
|
@@ -192,90 +191,9 @@ export function Layout({ config, onRefresh, onClone }: LayoutProps) {
|
|
|
192
191
|
break;
|
|
193
192
|
case "editor":
|
|
194
193
|
if (repo.localPath) {
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
// Check for project-specific editor
|
|
199
|
-
if (repo.localPath) {
|
|
200
|
-
const matchedDir = config.directories.find(d =>
|
|
201
|
-
repo.localPath!.startsWith(d.path.replace(/^~/, process.env.HOME || ""))
|
|
202
|
-
);
|
|
203
|
-
if (matchedDir?.editor) {
|
|
204
|
-
editor = matchedDir.editor;
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
|
|
208
|
-
if (!editor) {
|
|
209
|
-
editor = process.env.EDITOR || "code";
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Split command and args once
|
|
213
|
-
const parts = editor.split(" ");
|
|
214
|
-
const cmd = parts[0]!;
|
|
215
|
-
const args = parts.slice(1);
|
|
216
|
-
|
|
217
|
-
// Check if it's a known terminal editor
|
|
218
|
-
const terminalEditors = ["vim", "nvim", "nano", "vi", "emacs", "hx", "helix"];
|
|
219
|
-
const isTerminal = terminalEditors.includes(cmd);
|
|
220
|
-
|
|
221
|
-
try {
|
|
222
|
-
if (isTerminal) {
|
|
223
|
-
// Suspends Ink's raw mode to allow the editor to take over
|
|
224
|
-
if (process.stdin.setRawMode) {
|
|
225
|
-
process.stdin.setRawMode(false);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// Spawn with inherit to take over terminal
|
|
229
|
-
// IMPORTANT: Use split cmd and args
|
|
230
|
-
const proc = Bun.spawn([cmd, ...args, repo.localPath], {
|
|
231
|
-
stdin: "inherit",
|
|
232
|
-
stdout: "inherit",
|
|
233
|
-
stderr: "inherit",
|
|
234
|
-
});
|
|
235
|
-
|
|
236
|
-
await proc.exited;
|
|
237
|
-
|
|
238
|
-
// Resume raw mode
|
|
239
|
-
if (process.stdin.setRawMode) {
|
|
240
|
-
process.stdin.setRawMode(true);
|
|
241
|
-
}
|
|
242
|
-
} else {
|
|
243
|
-
// GUI Editor
|
|
244
|
-
|
|
245
|
-
// Use 'open' command for macOS GUI editors (VS Code, Cursor) specific optimization
|
|
246
|
-
if (process.platform === "darwin" && (cmd === "code" || cmd === "cursor")) {
|
|
247
|
-
const appName = cmd === "code" ? "Visual Studio Code" : "Cursor";
|
|
248
|
-
|
|
249
|
-
const openArgs = ["open", "-a", appName, repo.localPath];
|
|
250
|
-
openArgs.push("--args", "-n"); // Force new window
|
|
251
|
-
|
|
252
|
-
const subprocess = Bun.spawn(openArgs, {
|
|
253
|
-
stdin: "ignore",
|
|
254
|
-
stdout: "ignore",
|
|
255
|
-
stderr: "ignore",
|
|
256
|
-
});
|
|
257
|
-
subprocess.unref();
|
|
258
|
-
|
|
259
|
-
} else {
|
|
260
|
-
// Fallback for other GUI editors
|
|
261
|
-
|
|
262
|
-
// Auto-inject -n for code/cursor if not using 'open' strategy or on other platforms
|
|
263
|
-
if ((cmd === "code" || cmd === "cursor") && !args.includes("-n") && !args.includes("--new-window")) {
|
|
264
|
-
args.push("-n");
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const subprocess = Bun.spawn([cmd, ...args, repo.localPath], {
|
|
268
|
-
stdin: "ignore",
|
|
269
|
-
stdout: "ignore",
|
|
270
|
-
stderr: "ignore",
|
|
271
|
-
});
|
|
272
|
-
subprocess.unref();
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
} catch (error) {
|
|
276
|
-
dispatch(setMessage(`Failed to open editor: ${errorToString(error)}`));
|
|
277
|
-
// Ensure raw mode is back if we failed mid-flight
|
|
278
|
-
if (process.stdin.setRawMode) process.stdin.setRawMode(true);
|
|
194
|
+
const editorResult = await openInEditor(repo.localPath, config);
|
|
195
|
+
if (!editorResult.success) {
|
|
196
|
+
dispatch(setMessage(`Failed to open editor: ${editorResult.error}`));
|
|
279
197
|
}
|
|
280
198
|
} else {
|
|
281
199
|
dispatch(setMessage("No local path available for this repository"));
|
|
@@ -303,8 +221,8 @@ export function Layout({ config, onRefresh, onClone }: LayoutProps) {
|
|
|
303
221
|
const result = await executeCommand(command, projectPath);
|
|
304
222
|
dispatch(endAction());
|
|
305
223
|
if (result.success) {
|
|
306
|
-
const shortOutput = result.output && result.output.length >
|
|
307
|
-
? result.output.slice(0,
|
|
224
|
+
const shortOutput = result.output && result.output.length > UI.OUTPUT_TRUNCATION_LENGTH
|
|
225
|
+
? result.output.slice(0, UI.OUTPUT_TRUNCATION_LENGTH) + "..."
|
|
308
226
|
: result.output || "Done";
|
|
309
227
|
dispatch(setMessage(`${command.name}: ${shortOutput}`));
|
|
310
228
|
} else {
|
|
@@ -312,7 +230,7 @@ export function Layout({ config, onRefresh, onClone }: LayoutProps) {
|
|
|
312
230
|
}
|
|
313
231
|
} catch (error) {
|
|
314
232
|
dispatch(endAction());
|
|
315
|
-
dispatch(setMessage(`${command.name} failed: ${
|
|
233
|
+
dispatch(setMessage(`${command.name} failed: ${errorToString(error)}`));
|
|
316
234
|
}
|
|
317
235
|
};
|
|
318
236
|
|
|
@@ -334,6 +252,33 @@ export function Layout({ config, onRefresh, onClone }: LayoutProps) {
|
|
|
334
252
|
);
|
|
335
253
|
}
|
|
336
254
|
|
|
255
|
+
// Show add directory dialog
|
|
256
|
+
if (mode === "add-directory" && state.addDirectoryDialog) {
|
|
257
|
+
const handleAddDirConfirm = async (path: string, maxDepth: number, label: string) => {
|
|
258
|
+
if (onAddDirectory) {
|
|
259
|
+
const result = await onAddDirectory(path, maxDepth, label);
|
|
260
|
+
if (result.success) {
|
|
261
|
+
dispatch(hideAddDirectoryDialog());
|
|
262
|
+
dispatch(setMessage(`Added directory: ${path}`));
|
|
263
|
+
await onRefresh({ forceRefresh: true });
|
|
264
|
+
} else {
|
|
265
|
+
dispatch(updateAddDirectoryDialog({ error: result.error ?? "Failed to add directory" }));
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
};
|
|
269
|
+
|
|
270
|
+
return (
|
|
271
|
+
<Box flexDirection="column" height={terminalHeight} justifyContent="center" alignItems="center">
|
|
272
|
+
<AddDirectoryDialog
|
|
273
|
+
state={state.addDirectoryDialog}
|
|
274
|
+
onUpdate={(updates) => dispatch(updateAddDirectoryDialog(updates))}
|
|
275
|
+
onConfirm={handleAddDirConfirm}
|
|
276
|
+
onCancel={() => dispatch(hideAddDirectoryDialog())}
|
|
277
|
+
/>
|
|
278
|
+
</Box>
|
|
279
|
+
);
|
|
280
|
+
}
|
|
281
|
+
|
|
337
282
|
// Show filter options overlay
|
|
338
283
|
if (mode === "filter-options") {
|
|
339
284
|
return (
|
|
@@ -388,7 +388,7 @@ export function UnifiedProjectItem({ repo, isSelected, isCursor }: UnifiedProjec
|
|
|
388
388
|
<Text backgroundColor={bg}>{' '}</Text>
|
|
389
389
|
|
|
390
390
|
{/* Sync status */}
|
|
391
|
-
<Text color={isCursor ? fg : sync.color} backgroundColor={
|
|
391
|
+
<Text color={isCursor ? fg : sync.color} backgroundColor={bg}>
|
|
392
392
|
{sync.text}
|
|
393
393
|
</Text>
|
|
394
394
|
|
|
@@ -255,66 +255,66 @@ describe("Directory Browser Utility Functions", () => {
|
|
|
255
255
|
|
|
256
256
|
// These tests require specific platform or permission setups
|
|
257
257
|
// Mark them as todo since they're environment-dependent
|
|
258
|
-
test.todo("should handle very long paths gracefully");
|
|
259
|
-
test.todo("should handle paths with special characters");
|
|
260
|
-
test.todo("should handle permission denied errors gracefully");
|
|
258
|
+
test.todo("should handle very long paths gracefully", () => {});
|
|
259
|
+
test.todo("should handle paths with special characters", () => {});
|
|
260
|
+
test.todo("should handle permission denied errors gracefully", () => {});
|
|
261
261
|
});
|
|
262
262
|
});
|
|
263
263
|
|
|
264
264
|
describe("Browser State Management", () => {
|
|
265
265
|
describe("input buffer changes", () => {
|
|
266
|
-
test.todo("should reset selection when input changes");
|
|
267
|
-
test.todo("should reset scroll offset when input changes");
|
|
268
|
-
test.todo("should not reset when typing same character");
|
|
266
|
+
test.todo("should reset selection when input changes", () => {});
|
|
267
|
+
test.todo("should reset scroll offset when input changes", () => {});
|
|
268
|
+
test.todo("should not reset when typing same character", () => {});
|
|
269
269
|
});
|
|
270
270
|
|
|
271
271
|
describe("currentPath changes", () => {
|
|
272
|
-
test.todo("should reset scroll offset when navigating to new directory");
|
|
273
|
-
test.todo("should reset selection when navigating to new directory");
|
|
274
|
-
test.todo("should reload directory entries for new path");
|
|
272
|
+
test.todo("should reset scroll offset when navigating to new directory", () => {});
|
|
273
|
+
test.todo("should reset selection when navigating to new directory", () => {});
|
|
274
|
+
test.todo("should reload directory entries for new path", () => {});
|
|
275
275
|
});
|
|
276
276
|
|
|
277
277
|
describe("tab completion", () => {
|
|
278
|
-
test.todo("should cycle through completions when pressing Tab repeatedly");
|
|
279
|
-
test.todo("should add trailing slash after completing directory");
|
|
280
|
-
test.todo("should update currentPath to completed directory");
|
|
281
|
-
test.todo("should update folder list to show completed directory's contents");
|
|
278
|
+
test.todo("should cycle through completions when pressing Tab repeatedly", () => {});
|
|
279
|
+
test.todo("should add trailing slash after completing directory", () => {});
|
|
280
|
+
test.todo("should update currentPath to completed directory", () => {});
|
|
281
|
+
test.todo("should update folder list to show completed directory's contents", () => {});
|
|
282
282
|
});
|
|
283
283
|
|
|
284
284
|
describe("backspace handling", () => {
|
|
285
|
-
test.todo("should allow backspacing initial tilde");
|
|
286
|
-
test.todo("should allow backspacing to empty input");
|
|
287
|
-
test.todo("should reset selection when backspacing");
|
|
288
|
-
test.todo("should reset scroll offset when backspacing");
|
|
285
|
+
test.todo("should allow backspacing initial tilde", () => {});
|
|
286
|
+
test.todo("should allow backspacing to empty input", () => {});
|
|
287
|
+
test.todo("should reset selection when backspacing", () => {});
|
|
288
|
+
test.todo("should reset scroll offset when backspacing", () => {});
|
|
289
289
|
});
|
|
290
290
|
|
|
291
291
|
describe("escape key handling", () => {
|
|
292
|
-
test.todo("should go to parent directory when input ends with /");
|
|
293
|
-
test.todo("should clear input when input has text beyond ~");
|
|
294
|
-
test.todo("should cancel when input is just ~");
|
|
295
|
-
test.todo("should reset all state when going back");
|
|
292
|
+
test.todo("should go to parent directory when input ends with /", () => {});
|
|
293
|
+
test.todo("should clear input when input has text beyond ~", () => {});
|
|
294
|
+
test.todo("should cancel when input is just ~", () => {});
|
|
295
|
+
test.todo("should reset all state when going back", () => {});
|
|
296
296
|
});
|
|
297
297
|
|
|
298
298
|
describe("Ctrl+U handling", () => {
|
|
299
|
-
test.todo("should reset input to ~");
|
|
300
|
-
test.todo("should reset selection");
|
|
301
|
-
test.todo("should reset scroll offset");
|
|
302
|
-
test.todo("should clear errors");
|
|
299
|
+
test.todo("should reset input to ~", () => {});
|
|
300
|
+
test.todo("should reset selection", () => {});
|
|
301
|
+
test.todo("should reset scroll offset", () => {});
|
|
302
|
+
test.todo("should clear errors", () => {});
|
|
303
303
|
});
|
|
304
304
|
|
|
305
305
|
describe("folder filtering logic", () => {
|
|
306
|
-
test.todo("should filter by name when input has no slashes");
|
|
307
|
-
test.todo("should NOT filter when input starts with /");
|
|
308
|
-
test.todo("should NOT filter when input contains /");
|
|
309
|
-
test.todo("should show all folders when typing /Volumes");
|
|
310
|
-
test.todo("should show all folders when typing ~/code/proj");
|
|
306
|
+
test.todo("should filter by name when input has no slashes", () => {});
|
|
307
|
+
test.todo("should NOT filter when input starts with /", () => {});
|
|
308
|
+
test.todo("should NOT filter when input contains /", () => {});
|
|
309
|
+
test.todo("should show all folders when typing /Volumes", () => {});
|
|
310
|
+
test.todo("should show all folders when typing ~/code/proj", () => {});
|
|
311
311
|
});
|
|
312
312
|
|
|
313
313
|
describe("dynamic currentPath updates", () => {
|
|
314
|
-
test.todo("should update currentPath when input is valid directory");
|
|
315
|
-
test.todo("should expand ~ before validating");
|
|
316
|
-
test.todo("should reload entries when currentPath changes");
|
|
317
|
-
test.todo("should default to startingPath when input is empty");
|
|
314
|
+
test.todo("should update currentPath when input is valid directory", () => {});
|
|
315
|
+
test.todo("should expand ~ before validating", () => {});
|
|
316
|
+
test.todo("should reload entries when currentPath changes", () => {});
|
|
317
|
+
test.todo("should default to startingPath when input is empty", () => {});
|
|
318
318
|
});
|
|
319
319
|
});
|
|
320
320
|
|
package/src/constants.ts
CHANGED
|
@@ -14,6 +14,8 @@ export const UI = {
|
|
|
14
14
|
MIN_TERMINAL_HEIGHT: 24,
|
|
15
15
|
/** Default terminal height if detection fails */
|
|
16
16
|
DEFAULT_TERMINAL_HEIGHT: 24,
|
|
17
|
+
/** Maximum length for command output before truncation */
|
|
18
|
+
OUTPUT_TRUNCATION_LENGTH: 50,
|
|
17
19
|
} as const;
|
|
18
20
|
|
|
19
21
|
// ============================================================================
|
package/src/git/index.ts
CHANGED
|
@@ -1,2 +1 @@
|
|
|
1
|
-
export * from "./types.js";
|
|
2
|
-
export * from "./service.js";
|
|
1
|
+
export * from "./types.js";
|
package/src/github/unified.ts
CHANGED
|
@@ -369,10 +369,9 @@ export async function cloneGitHubRepo(
|
|
|
369
369
|
}
|
|
370
370
|
|
|
371
371
|
// Check if target directory exists
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
// }
|
|
372
|
+
if (!existsSync(targetDir)) {
|
|
373
|
+
return { success: false, error: `Target directory does not exist: ${targetDir}` };
|
|
374
|
+
}
|
|
376
375
|
|
|
377
376
|
const repoPath = `${targetDir}/${repo.name}`;
|
|
378
377
|
const url = useSSH ? repo.github.sshUrl : repo.github.cloneUrl;
|
|
@@ -15,7 +15,7 @@ export interface BackgroundFetchDeps {
|
|
|
15
15
|
|
|
16
16
|
export function useBackgroundFetch(
|
|
17
17
|
config: GitforestConfig,
|
|
18
|
-
onRefresh: () => Promise<void>,
|
|
18
|
+
onRefresh: (options?: { forceRefresh?: boolean }) => Promise<void>,
|
|
19
19
|
deps?: Partial<BackgroundFetchDeps>,
|
|
20
20
|
) {
|
|
21
21
|
const { state, dispatch } = useStore();
|
|
@@ -7,7 +7,7 @@ import type { GitforestConfig, Project } from "../types/index.ts";
|
|
|
7
7
|
|
|
8
8
|
interface UseConfirmDialogActionsOptions {
|
|
9
9
|
config: GitforestConfig;
|
|
10
|
-
onRefresh: () => Promise<void>;
|
|
10
|
+
onRefresh: (options?: { forceRefresh?: boolean }) => Promise<void>;
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
export function useConfirmDialogActions({
|