ftreeview 0.1.1
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/LICENSE +21 -0
- package/README.md +235 -0
- package/dist/cli.js +1619 -0
- package/package.json +58 -0
- package/src/App.jsx +243 -0
- package/src/cli.js +228 -0
- package/src/components/StatusBar.jsx +270 -0
- package/src/components/TreeLine.jsx +190 -0
- package/src/components/TreeView.jsx +129 -0
- package/src/hooks/useChangedFiles.js +82 -0
- package/src/hooks/useGitStatus.js +347 -0
- package/src/hooks/useIgnore.js +182 -0
- package/src/hooks/useNavigation.js +247 -0
- package/src/hooks/useTree.js +508 -0
- package/src/hooks/useWatcher.js +129 -0
- package/src/index.js +22 -0
- package/src/lib/changeStatus.js +79 -0
- package/src/lib/connectors.js +233 -0
- package/src/lib/constants.js +64 -0
- package/src/lib/icons.js +658 -0
- package/src/lib/theme.js +102 -0
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "ftreeview",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Terminal file explorer with git awareness and live updates",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "src/index.js",
|
|
7
|
+
"files": [
|
|
8
|
+
"dist",
|
|
9
|
+
"src",
|
|
10
|
+
"README.md",
|
|
11
|
+
"LICENSE"
|
|
12
|
+
],
|
|
13
|
+
"bin": {
|
|
14
|
+
"ftree": "dist/cli.js"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18.0.0"
|
|
18
|
+
},
|
|
19
|
+
"scripts": {
|
|
20
|
+
"build": "node build.js",
|
|
21
|
+
"dev": "node build.js --run",
|
|
22
|
+
"prepublishOnly": "npm run build",
|
|
23
|
+
"test": "echo \"Error: no test specified\" && exit 1"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"cli",
|
|
27
|
+
"file",
|
|
28
|
+
"explorer",
|
|
29
|
+
"tree",
|
|
30
|
+
"terminal",
|
|
31
|
+
"tui",
|
|
32
|
+
"watcher",
|
|
33
|
+
"git"
|
|
34
|
+
],
|
|
35
|
+
"author": "ftree contributors",
|
|
36
|
+
"license": "MIT",
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "git+https://github.com/chaitanyame/watchout.git"
|
|
40
|
+
},
|
|
41
|
+
"bugs": {
|
|
42
|
+
"url": "https://github.com/chaitanyame/watchout/issues"
|
|
43
|
+
},
|
|
44
|
+
"homepage": "https://github.com/chaitanyame/watchout#readme",
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"chalk": "^5.4.1",
|
|
47
|
+
"chokidar": "^4.0.3",
|
|
48
|
+
"ignore": "^6.0.2",
|
|
49
|
+
"ink": "^5.0.1",
|
|
50
|
+
"react": "^18.3.1"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"esbuild": "^0.27.3"
|
|
54
|
+
},
|
|
55
|
+
"exports": {
|
|
56
|
+
".": "./src/index.js"
|
|
57
|
+
}
|
|
58
|
+
}
|
package/src/App.jsx
ADDED
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App Component - Root Component for ftree
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates between:
|
|
5
|
+
* - useTree: Manages the file tree data structure
|
|
6
|
+
* - useNavigation: Handles keyboard navigation and cursor state
|
|
7
|
+
* - TreeView: Renders the visible portion of the tree
|
|
8
|
+
* - StatusBar: Shows helpful information (T9)
|
|
9
|
+
* - useGitStatus: Provides git status integration
|
|
10
|
+
* - useWatcher: Monitors file system changes
|
|
11
|
+
*
|
|
12
|
+
* This component uses React hooks to manage state and coordinates
|
|
13
|
+
* between the data flow, navigation state, and rendering.
|
|
14
|
+
*/
|
|
15
|
+
import React, { useEffect, useCallback, useMemo, useState, useRef } from 'react';
|
|
16
|
+
import { isAbsolute, relative, resolve, sep } from 'node:path';
|
|
17
|
+
import { Box } from 'ink';
|
|
18
|
+
import TreeView from './components/TreeView.jsx';
|
|
19
|
+
import StatusBar, { calculateCounts, calculateGitStatus } from './components/StatusBar.jsx';
|
|
20
|
+
import { useTree, applyGitStatus, applyChangeIndicators } from './hooks/useTree.js';
|
|
21
|
+
import { useNavigation } from './hooks/useNavigation.js';
|
|
22
|
+
import { useGitStatus } from './hooks/useGitStatus.js';
|
|
23
|
+
import { useWatcher } from './hooks/useWatcher.js';
|
|
24
|
+
import { useChangedFiles } from './hooks/useChangedFiles.js';
|
|
25
|
+
import { mapWatcherEventToChangeStatus } from './lib/changeStatus.js';
|
|
26
|
+
import { getTheme, resolveIconSet, resolveThemeName } from './lib/theme.js';
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Clean exit handler - ensures proper terminal state on exit
|
|
30
|
+
* Prints a newline to avoid leaving cursor on status line
|
|
31
|
+
*/
|
|
32
|
+
function setupCleanExit() {
|
|
33
|
+
const onExit = () => {
|
|
34
|
+
// Print newline for clean exit
|
|
35
|
+
process.stdout.write('\n');
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
// Handle normal exit
|
|
39
|
+
process.on('exit', onExit);
|
|
40
|
+
|
|
41
|
+
// Handle SIGINT (Ctrl+C)
|
|
42
|
+
process.on('SIGINT', () => {
|
|
43
|
+
onExit();
|
|
44
|
+
process.exit(0);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// Return cleanup function
|
|
48
|
+
return () => {
|
|
49
|
+
process.off('exit', onExit);
|
|
50
|
+
process.off('SIGINT');
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* App Component Props
|
|
56
|
+
* @property {string} rootPath - Root directory path to explore
|
|
57
|
+
* @property {object} options - Configuration options from CLI
|
|
58
|
+
*/
|
|
59
|
+
export default function App({ rootPath, options = {}, version }) {
|
|
60
|
+
const rootAbsPath = resolve(rootPath);
|
|
61
|
+
const iconSet = resolveIconSet({ cliIconSet: options.iconSet, noIcons: options.noIcons });
|
|
62
|
+
const theme = getTheme(resolveThemeName({ cliTheme: options.theme }));
|
|
63
|
+
|
|
64
|
+
// Track selected path to preserve cursor position across rebuilds
|
|
65
|
+
const [selectedPath, setSelectedPath] = useState(null);
|
|
66
|
+
|
|
67
|
+
// Setup clean exit handler - ensures proper terminal state on exit
|
|
68
|
+
useEffect(() => setupCleanExit(), []);
|
|
69
|
+
|
|
70
|
+
// Get tree data and rebuild function
|
|
71
|
+
const { tree, flatList, refreshFlatList, rebuildTree } = useTree(rootPath, options);
|
|
72
|
+
|
|
73
|
+
// Get git status (enabled unless --no-git flag is set)
|
|
74
|
+
const { statusMap, isGitRepo, refresh: refreshGit } = useGitStatus(
|
|
75
|
+
rootPath,
|
|
76
|
+
!options.noGit
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
// Get navigation state (cursor, viewport)
|
|
80
|
+
const { cursor, viewportStart, setCursor } = useNavigation(
|
|
81
|
+
flatList,
|
|
82
|
+
rebuildTree,
|
|
83
|
+
refreshFlatList
|
|
84
|
+
);
|
|
85
|
+
const cursorRef = useRef(cursor);
|
|
86
|
+
cursorRef.current = cursor;
|
|
87
|
+
|
|
88
|
+
// Track recently changed files for visual highlighting
|
|
89
|
+
const { changedFiles, markChanged } = useChangedFiles();
|
|
90
|
+
|
|
91
|
+
// Apply git/change indicators during render (memoized) to avoid effect-driven update loops.
|
|
92
|
+
useMemo(() => {
|
|
93
|
+
if (!tree) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const gitStatuses = isGitRepo ? statusMap : new Map();
|
|
98
|
+
applyGitStatus(tree, gitStatuses);
|
|
99
|
+
applyChangeIndicators(tree, gitStatuses, changedFiles);
|
|
100
|
+
}, [statusMap, tree, isGitRepo, changedFiles]);
|
|
101
|
+
|
|
102
|
+
// Terminal size state (for resize handling)
|
|
103
|
+
const [termSize, setTermSize] = useState({
|
|
104
|
+
width: process.stdout.columns,
|
|
105
|
+
height: process.stdout.rows,
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
// Listen for terminal resize events
|
|
109
|
+
useEffect(() => {
|
|
110
|
+
const handleResize = () => {
|
|
111
|
+
setTermSize({
|
|
112
|
+
width: process.stdout.columns,
|
|
113
|
+
height: process.stdout.rows,
|
|
114
|
+
});
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
// Listen for resize events
|
|
118
|
+
process.stdout.on('resize', handleResize);
|
|
119
|
+
|
|
120
|
+
// Cleanup listener on unmount
|
|
121
|
+
return () => {
|
|
122
|
+
process.stdout.off('resize', handleResize);
|
|
123
|
+
};
|
|
124
|
+
}, []);
|
|
125
|
+
|
|
126
|
+
// Update selected path when cursor changes
|
|
127
|
+
useEffect(() => {
|
|
128
|
+
if (flatList.length > 0 && cursor >= 0 && cursor < flatList.length) {
|
|
129
|
+
const currentNode = flatList[cursor];
|
|
130
|
+
if (currentNode) {
|
|
131
|
+
setSelectedPath((prev) => (prev === currentNode.path ? prev : currentNode.path));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}, [cursor, flatList]);
|
|
135
|
+
|
|
136
|
+
// Restore cursor position after rebuild
|
|
137
|
+
useEffect(() => {
|
|
138
|
+
if (!selectedPath || flatList.length === 0) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const newIndex = flatList.findIndex((node) => node.path === selectedPath);
|
|
143
|
+
if (newIndex === -1) {
|
|
144
|
+
setCursor((prev) => Math.min(prev, flatList.length - 1));
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (newIndex !== cursorRef.current) {
|
|
149
|
+
setCursor(newIndex);
|
|
150
|
+
}
|
|
151
|
+
}, [flatList, selectedPath, setCursor]);
|
|
152
|
+
|
|
153
|
+
// Convert watcher path to tree-relative path format
|
|
154
|
+
const toTreeRelativePath = useCallback((watchPath) => {
|
|
155
|
+
if (!watchPath) {
|
|
156
|
+
return '';
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
const absolutePath = isAbsolute(watchPath)
|
|
160
|
+
? watchPath
|
|
161
|
+
: resolve(rootAbsPath, watchPath);
|
|
162
|
+
const relativePath = relative(rootAbsPath, absolutePath);
|
|
163
|
+
|
|
164
|
+
if (!relativePath || relativePath.startsWith('..')) {
|
|
165
|
+
return '';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return relativePath.split(sep).join('/');
|
|
169
|
+
}, [rootAbsPath]);
|
|
170
|
+
|
|
171
|
+
// File watcher - rebuilds tree and refreshes git status on file changes
|
|
172
|
+
// Enabled unless --no-watch flag is set
|
|
173
|
+
const handleTreeChange = useCallback((eventsOrEvent, watchPath) => {
|
|
174
|
+
const batch = Array.isArray(eventsOrEvent)
|
|
175
|
+
? eventsOrEvent
|
|
176
|
+
: [{ event: eventsOrEvent, path: watchPath }];
|
|
177
|
+
|
|
178
|
+
for (const item of batch) {
|
|
179
|
+
if (!item) continue;
|
|
180
|
+
const changeStatus = mapWatcherEventToChangeStatus(item.event);
|
|
181
|
+
const relativePath = toTreeRelativePath(item.path);
|
|
182
|
+
|
|
183
|
+
if (changeStatus && relativePath) {
|
|
184
|
+
markChanged(relativePath, changeStatus);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
rebuildTree(true);
|
|
189
|
+
if (isGitRepo) {
|
|
190
|
+
refreshGit();
|
|
191
|
+
}
|
|
192
|
+
}, [rebuildTree, isGitRepo, refreshGit, markChanged, toTreeRelativePath]);
|
|
193
|
+
|
|
194
|
+
const { isWatching } = useWatcher(rootPath, handleTreeChange, {
|
|
195
|
+
noWatch: options.noWatch,
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
// Calculate viewport dimensions
|
|
199
|
+
// Reserve space: 1 line for status bar (2 if help enabled)
|
|
200
|
+
const helpEnabled = options.help !== false; // Show help by default
|
|
201
|
+
const statusBarHeight = helpEnabled ? 2 : 1;
|
|
202
|
+
const viewportHeight = termSize.height - statusBarHeight;
|
|
203
|
+
const termWidth = termSize.width;
|
|
204
|
+
|
|
205
|
+
// Calculate file and directory counts
|
|
206
|
+
const { fileCount, dirCount } = calculateCounts(flatList);
|
|
207
|
+
|
|
208
|
+
// Calculate git status summary
|
|
209
|
+
const { summary: gitSummary } = calculateGitStatus(statusMap);
|
|
210
|
+
|
|
211
|
+
return (
|
|
212
|
+
<Box flexDirection="column" height="100%">
|
|
213
|
+
{/* Main tree view area */}
|
|
214
|
+
<Box flexGrow={1}>
|
|
215
|
+
<TreeView
|
|
216
|
+
flatList={flatList}
|
|
217
|
+
cursor={cursor}
|
|
218
|
+
viewportStart={viewportStart}
|
|
219
|
+
viewportHeight={viewportHeight}
|
|
220
|
+
termWidth={termWidth}
|
|
221
|
+
iconSet={iconSet}
|
|
222
|
+
theme={theme}
|
|
223
|
+
/>
|
|
224
|
+
</Box>
|
|
225
|
+
|
|
226
|
+
{/* StatusBar */}
|
|
227
|
+
<StatusBar
|
|
228
|
+
version={version}
|
|
229
|
+
rootPath={rootPath}
|
|
230
|
+
fileCount={fileCount}
|
|
231
|
+
dirCount={dirCount}
|
|
232
|
+
isWatching={isWatching}
|
|
233
|
+
gitEnabled={!options.noGit}
|
|
234
|
+
isGitRepo={isGitRepo}
|
|
235
|
+
gitSummary={gitSummary}
|
|
236
|
+
showHelp={helpEnabled}
|
|
237
|
+
termWidth={termWidth}
|
|
238
|
+
iconSet={iconSet}
|
|
239
|
+
theme={theme}
|
|
240
|
+
/>
|
|
241
|
+
</Box>
|
|
242
|
+
);
|
|
243
|
+
}
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ftree CLI entry point
|
|
5
|
+
* Terminal file explorer with git awareness and live updates
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
9
|
+
import { resolve, dirname } from 'node:path';
|
|
10
|
+
import { fileURLToPath } from 'node:url';
|
|
11
|
+
import { statSync, readdirSync } from 'node:fs';
|
|
12
|
+
import { render } from 'ink';
|
|
13
|
+
import React from 'react';
|
|
14
|
+
|
|
15
|
+
import App from './App.jsx';
|
|
16
|
+
|
|
17
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
|
|
19
|
+
// Get version from package.json
|
|
20
|
+
const pkgPath = resolve(__dirname, '../package.json');
|
|
21
|
+
const { version } = JSON.parse(readFileSync(pkgPath, 'utf-8'));
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Print help message
|
|
25
|
+
*/
|
|
26
|
+
function printHelp() {
|
|
27
|
+
console.log(`
|
|
28
|
+
ftree - Terminal file explorer with git awareness and live updates
|
|
29
|
+
|
|
30
|
+
USAGE:
|
|
31
|
+
ftree [path]
|
|
32
|
+
|
|
33
|
+
ARGUMENTS:
|
|
34
|
+
[path] Path to directory to explore (default: current directory)
|
|
35
|
+
|
|
36
|
+
OPTIONS:
|
|
37
|
+
-h, --help Print this help message
|
|
38
|
+
-v, --version Print version information
|
|
39
|
+
--no-git Disable git integration
|
|
40
|
+
--no-icons Use ASCII icons (disable emoji/nerd icons)
|
|
41
|
+
--no-watch Disable file watching
|
|
42
|
+
--icon-set <emoji|nerd|ascii> Icon set (default: emoji). You can also set FTREE_ICON_SET.
|
|
43
|
+
--theme <vscode> UI theme (default: vscode). You can also set FTREE_THEME.
|
|
44
|
+
|
|
45
|
+
EXAMPLES:
|
|
46
|
+
ftree # Explore current directory
|
|
47
|
+
ftree ~/projects # Explore specific directory
|
|
48
|
+
ftree /path/to/project # Explore project path
|
|
49
|
+
|
|
50
|
+
EXIT CODES:
|
|
51
|
+
0 - Success
|
|
52
|
+
1 - Invalid path or error
|
|
53
|
+
`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Print version information
|
|
58
|
+
*/
|
|
59
|
+
function printVersion() {
|
|
60
|
+
console.log(`ftree v${version}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Validate a directory path
|
|
65
|
+
* @param {string} path - Path to validate
|
|
66
|
+
* @returns {{valid: boolean, error?: string}}
|
|
67
|
+
*/
|
|
68
|
+
function validatePath(path) {
|
|
69
|
+
const resolvedPath = resolve(path);
|
|
70
|
+
|
|
71
|
+
// Check if path exists
|
|
72
|
+
if (!existsSync(resolvedPath)) {
|
|
73
|
+
return {
|
|
74
|
+
valid: false,
|
|
75
|
+
error: `Error: Path does not exist: ${resolvedPath}`,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check if it's a directory
|
|
80
|
+
try {
|
|
81
|
+
const stats = statSync(resolvedPath);
|
|
82
|
+
if (!stats.isDirectory()) {
|
|
83
|
+
return {
|
|
84
|
+
valid: false,
|
|
85
|
+
error: `Error: Path is not a directory: ${resolvedPath}`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
} catch (err) {
|
|
89
|
+
return {
|
|
90
|
+
valid: false,
|
|
91
|
+
error: `Error: Cannot access path: ${resolvedPath} (${err.message})`,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check if directory is readable
|
|
96
|
+
try {
|
|
97
|
+
readdirSync(resolvedPath);
|
|
98
|
+
} catch (err) {
|
|
99
|
+
return {
|
|
100
|
+
valid: false,
|
|
101
|
+
error: `Error: Cannot read directory: ${resolvedPath} (${err.message})`,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return { valid: true };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Parse command line arguments
|
|
110
|
+
* @returns {{path?: string, help: boolean, version: boolean, options: object}}
|
|
111
|
+
*/
|
|
112
|
+
function parseArgs() {
|
|
113
|
+
const args = process.argv.slice(2);
|
|
114
|
+
const result = {
|
|
115
|
+
help: false,
|
|
116
|
+
version: false,
|
|
117
|
+
options: {
|
|
118
|
+
noIcons: false,
|
|
119
|
+
noGit: false,
|
|
120
|
+
noWatch: false,
|
|
121
|
+
help: true, // show help line by default
|
|
122
|
+
iconSet: undefined,
|
|
123
|
+
theme: undefined,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
for (let i = 0; i < args.length; i++) {
|
|
128
|
+
const arg = args[i];
|
|
129
|
+
|
|
130
|
+
if (arg === '-h' || arg === '--help') {
|
|
131
|
+
result.help = true;
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
if (arg === '-v' || arg === '--version') {
|
|
135
|
+
result.version = true;
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (arg === '--no-git') {
|
|
140
|
+
result.options.noGit = true;
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
if (arg === '--no-icons') {
|
|
144
|
+
result.options.noIcons = true;
|
|
145
|
+
continue;
|
|
146
|
+
}
|
|
147
|
+
if (arg === '--no-watch') {
|
|
148
|
+
result.options.noWatch = true;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (arg === '--no-help') {
|
|
152
|
+
result.options.help = false;
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (arg === '--icon-set' || arg.startsWith('--icon-set=')) {
|
|
157
|
+
const value = arg.includes('=')
|
|
158
|
+
? arg.split('=')[1]
|
|
159
|
+
: args[++i];
|
|
160
|
+
if (!value) {
|
|
161
|
+
console.error('Error: --icon-set requires a value (emoji|nerd|ascii)');
|
|
162
|
+
process.exit(1);
|
|
163
|
+
}
|
|
164
|
+
result.options.iconSet = value;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
if (arg === '--theme' || arg.startsWith('--theme=')) {
|
|
169
|
+
const value = arg.includes('=')
|
|
170
|
+
? arg.split('=')[1]
|
|
171
|
+
: args[++i];
|
|
172
|
+
if (!value) {
|
|
173
|
+
console.error('Error: --theme requires a value');
|
|
174
|
+
process.exit(1);
|
|
175
|
+
}
|
|
176
|
+
result.options.theme = value;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (!arg.startsWith('-')) {
|
|
181
|
+
// Positional argument (path)
|
|
182
|
+
result.path = arg;
|
|
183
|
+
continue;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
console.error(`Error: Unknown option: ${arg}`);
|
|
187
|
+
console.error('Use --help for usage information');
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Main CLI entry point
|
|
196
|
+
*/
|
|
197
|
+
function main() {
|
|
198
|
+
const { path, help, version, options } = parseArgs();
|
|
199
|
+
|
|
200
|
+
// Handle help flag
|
|
201
|
+
if (help) {
|
|
202
|
+
printHelp();
|
|
203
|
+
process.exit(0);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Handle version flag
|
|
207
|
+
if (version) {
|
|
208
|
+
printVersion();
|
|
209
|
+
process.exit(0);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Determine target path (default to current directory)
|
|
213
|
+
const targetPath = path || '.';
|
|
214
|
+
|
|
215
|
+
// Validate path
|
|
216
|
+
const validation = validatePath(targetPath);
|
|
217
|
+
if (!validation.valid) {
|
|
218
|
+
console.error(validation.error);
|
|
219
|
+
process.exit(1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Render the Ink app
|
|
223
|
+
const resolvedPath = resolve(targetPath);
|
|
224
|
+
render(React.createElement(App, { rootPath: resolvedPath, options, version: `v${version}` }));
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Run CLI
|
|
228
|
+
main();
|