tidyf 1.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.
- package/LICENSE +21 -0
- package/README.md +299 -0
- package/dist/cli.js +19340 -0
- package/dist/fsevents-hj42pnne.node +0 -0
- package/dist/index.js +17617 -0
- package/package.json +58 -0
- package/src/cli.ts +63 -0
- package/src/commands/config.ts +630 -0
- package/src/commands/organize.ts +396 -0
- package/src/commands/watch.ts +302 -0
- package/src/index.ts +93 -0
- package/src/lib/config.ts +335 -0
- package/src/lib/opencode.ts +380 -0
- package/src/lib/scanner.ts +296 -0
- package/src/lib/watcher.ts +151 -0
- package/src/types/config.ts +69 -0
- package/src/types/organizer.ts +144 -0
- package/src/utils/files.ts +198 -0
- package/src/utils/icons.ts +195 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File watcher for tidy
|
|
3
|
+
*
|
|
4
|
+
* Uses chokidar to watch directories for new files and emits batched events
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import chokidar from "chokidar";
|
|
8
|
+
import { EventEmitter } from "events";
|
|
9
|
+
import type { WatchEvent } from "../types/organizer.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Options for the file watcher
|
|
13
|
+
*/
|
|
14
|
+
export interface WatcherOptions {
|
|
15
|
+
/** Debounce delay in milliseconds */
|
|
16
|
+
delay?: number;
|
|
17
|
+
/** Patterns to ignore */
|
|
18
|
+
ignore?: string[];
|
|
19
|
+
/** Whether to watch subdirectories */
|
|
20
|
+
recursive?: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* File watcher that batches events and emits after debounce
|
|
25
|
+
*/
|
|
26
|
+
export class FileWatcher extends EventEmitter {
|
|
27
|
+
private watcher: chokidar.FSWatcher | null = null;
|
|
28
|
+
private debounceTimer: NodeJS.Timeout | null = null;
|
|
29
|
+
private pendingFiles: Map<string, WatchEvent> = new Map();
|
|
30
|
+
private isRunning = false;
|
|
31
|
+
|
|
32
|
+
constructor(private options: WatcherOptions = {}) {
|
|
33
|
+
super();
|
|
34
|
+
this.options.delay = this.options.delay ?? 3000;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Start watching the specified paths
|
|
39
|
+
*/
|
|
40
|
+
start(paths: string[]): void {
|
|
41
|
+
if (this.isRunning) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
this.isRunning = true;
|
|
46
|
+
|
|
47
|
+
// Build ignore patterns for chokidar
|
|
48
|
+
const ignored = [
|
|
49
|
+
/(^|[\/\\])\../, // Hidden files
|
|
50
|
+
"**/node_modules/**",
|
|
51
|
+
"**/.git/**",
|
|
52
|
+
...(this.options.ignore || []),
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
this.watcher = chokidar.watch(paths, {
|
|
56
|
+
persistent: true,
|
|
57
|
+
ignoreInitial: true,
|
|
58
|
+
ignored,
|
|
59
|
+
depth: this.options.recursive ? undefined : 0,
|
|
60
|
+
awaitWriteFinish: {
|
|
61
|
+
stabilityThreshold: 2000,
|
|
62
|
+
pollInterval: 100,
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
this.watcher.on("add", (path) => this.handleEvent("add", path));
|
|
67
|
+
this.watcher.on("change", (path) => this.handleEvent("change", path));
|
|
68
|
+
this.watcher.on("error", (error) => this.emit("error", error));
|
|
69
|
+
|
|
70
|
+
this.watcher.on("ready", () => {
|
|
71
|
+
this.emit("ready", paths);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Handle a file event
|
|
77
|
+
*/
|
|
78
|
+
private handleEvent(type: "add" | "change", path: string): void {
|
|
79
|
+
// Add to pending files
|
|
80
|
+
this.pendingFiles.set(path, {
|
|
81
|
+
type,
|
|
82
|
+
path,
|
|
83
|
+
timestamp: new Date(),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Reset debounce timer
|
|
87
|
+
if (this.debounceTimer) {
|
|
88
|
+
clearTimeout(this.debounceTimer);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
this.debounceTimer = setTimeout(() => {
|
|
92
|
+
this.flushPendingFiles();
|
|
93
|
+
}, this.options.delay);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Flush pending files and emit batch event
|
|
98
|
+
*/
|
|
99
|
+
private flushPendingFiles(): void {
|
|
100
|
+
if (this.pendingFiles.size === 0) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const files = Array.from(this.pendingFiles.values());
|
|
105
|
+
this.pendingFiles.clear();
|
|
106
|
+
|
|
107
|
+
this.emit("files", files);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Stop watching
|
|
112
|
+
*/
|
|
113
|
+
stop(): void {
|
|
114
|
+
if (this.debounceTimer) {
|
|
115
|
+
clearTimeout(this.debounceTimer);
|
|
116
|
+
this.debounceTimer = null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Flush any remaining files
|
|
120
|
+
this.flushPendingFiles();
|
|
121
|
+
|
|
122
|
+
if (this.watcher) {
|
|
123
|
+
this.watcher.close();
|
|
124
|
+
this.watcher = null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
this.isRunning = false;
|
|
128
|
+
this.emit("stop");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Check if watcher is running
|
|
133
|
+
*/
|
|
134
|
+
get running(): boolean {
|
|
135
|
+
return this.isRunning;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Get number of pending files
|
|
140
|
+
*/
|
|
141
|
+
get pendingCount(): number {
|
|
142
|
+
return this.pendingFiles.size;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Create a new file watcher
|
|
148
|
+
*/
|
|
149
|
+
export function createWatcher(options?: WatcherOptions): FileWatcher {
|
|
150
|
+
return new FileWatcher(options);
|
|
151
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration types for tidy
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Model selection for AI operations
|
|
7
|
+
*/
|
|
8
|
+
export interface ModelSelection {
|
|
9
|
+
provider: string;
|
|
10
|
+
model: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Folder rule configuration
|
|
15
|
+
*/
|
|
16
|
+
export interface FolderRule {
|
|
17
|
+
/** Source folder patterns (glob or paths) */
|
|
18
|
+
sources: string[];
|
|
19
|
+
/** Target base directory */
|
|
20
|
+
target: string;
|
|
21
|
+
/** Whether to watch this folder */
|
|
22
|
+
watch?: boolean;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Category rule for organizing files
|
|
27
|
+
*/
|
|
28
|
+
export interface CategoryRule {
|
|
29
|
+
/** Category name */
|
|
30
|
+
name: string;
|
|
31
|
+
/** File extensions that belong to this category */
|
|
32
|
+
extensions?: string[];
|
|
33
|
+
/** MIME type patterns */
|
|
34
|
+
mimeTypes?: string[];
|
|
35
|
+
/** Subfolder name */
|
|
36
|
+
subfolder: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Main configuration interface
|
|
41
|
+
*/
|
|
42
|
+
export interface TidyConfig {
|
|
43
|
+
/** Model for file analysis */
|
|
44
|
+
organizer?: ModelSelection;
|
|
45
|
+
/** Default source directory */
|
|
46
|
+
defaultSource?: string;
|
|
47
|
+
/** Default target directory */
|
|
48
|
+
defaultTarget?: string;
|
|
49
|
+
/** Whether watch mode is enabled by default */
|
|
50
|
+
watchEnabled?: boolean;
|
|
51
|
+
/** Folder rules */
|
|
52
|
+
folders?: FolderRule[];
|
|
53
|
+
/** Category rules (hints for AI) */
|
|
54
|
+
categories?: CategoryRule[];
|
|
55
|
+
/** Files/patterns to ignore */
|
|
56
|
+
ignore?: string[];
|
|
57
|
+
/** Whether to read file content for better categorization */
|
|
58
|
+
readContent?: boolean;
|
|
59
|
+
/** Max file size to read content (in bytes) */
|
|
60
|
+
maxContentSize?: number;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Options for config command
|
|
65
|
+
*/
|
|
66
|
+
export interface ConfigOptions {
|
|
67
|
+
/** Whether to configure locally (current directory) */
|
|
68
|
+
local?: boolean;
|
|
69
|
+
}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Core types for the file organizer
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Metadata about a file to be organized
|
|
7
|
+
*/
|
|
8
|
+
export interface FileMetadata {
|
|
9
|
+
/** Full path to the file */
|
|
10
|
+
path: string;
|
|
11
|
+
/** File name without path */
|
|
12
|
+
name: string;
|
|
13
|
+
/** File extension (without dot) */
|
|
14
|
+
extension: string;
|
|
15
|
+
/** File size in bytes */
|
|
16
|
+
size: number;
|
|
17
|
+
/** Last modified timestamp */
|
|
18
|
+
modifiedAt: Date;
|
|
19
|
+
/** Created timestamp */
|
|
20
|
+
createdAt: Date;
|
|
21
|
+
/** MIME type if detectable */
|
|
22
|
+
mimeType?: string;
|
|
23
|
+
/** Optional content preview (first N bytes/lines) */
|
|
24
|
+
contentPreview?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* AI-suggested category for a file
|
|
29
|
+
*/
|
|
30
|
+
export interface FileCategory {
|
|
31
|
+
/** Category name (e.g., "Documents", "Images", "Projects") */
|
|
32
|
+
name: string;
|
|
33
|
+
/** Subcategory if applicable (e.g., "Work", "Personal") */
|
|
34
|
+
subcategory?: string;
|
|
35
|
+
/** Suggested subfolder path relative to target */
|
|
36
|
+
suggestedPath: string;
|
|
37
|
+
/** Confidence score 0-1 */
|
|
38
|
+
confidence: number;
|
|
39
|
+
/** AI's reasoning for this categorization */
|
|
40
|
+
reasoning: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A proposed file move operation
|
|
45
|
+
*/
|
|
46
|
+
export interface FileMoveProposal {
|
|
47
|
+
/** Original file path */
|
|
48
|
+
sourcePath: string;
|
|
49
|
+
/** Source file metadata */
|
|
50
|
+
file: FileMetadata;
|
|
51
|
+
/** Proposed destination path (full path) */
|
|
52
|
+
destination: string;
|
|
53
|
+
/** Category information */
|
|
54
|
+
category: FileCategory;
|
|
55
|
+
/** Whether file already exists at destination */
|
|
56
|
+
conflictExists: boolean;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Result of AI analysis
|
|
61
|
+
*/
|
|
62
|
+
export interface OrganizationProposal {
|
|
63
|
+
/** List of proposed moves */
|
|
64
|
+
proposals: FileMoveProposal[];
|
|
65
|
+
/** Overall strategy explanation */
|
|
66
|
+
strategy: string;
|
|
67
|
+
/** Files that couldn't be categorized */
|
|
68
|
+
uncategorized: FileMetadata[];
|
|
69
|
+
/** Timestamp of analysis */
|
|
70
|
+
analyzedAt: Date;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Options for organize command
|
|
75
|
+
*/
|
|
76
|
+
export interface OrganizeOptions {
|
|
77
|
+
/** Directory to scan */
|
|
78
|
+
path?: string;
|
|
79
|
+
/** Preview without executing */
|
|
80
|
+
dryRun?: boolean;
|
|
81
|
+
/** Skip confirmations */
|
|
82
|
+
yes?: boolean;
|
|
83
|
+
/** Scan subdirectories */
|
|
84
|
+
recursive?: boolean;
|
|
85
|
+
/** Max depth for recursive scan */
|
|
86
|
+
depth?: string;
|
|
87
|
+
/** Target directory for organized files */
|
|
88
|
+
target?: string;
|
|
89
|
+
/** Model override */
|
|
90
|
+
model?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Options for watch command
|
|
95
|
+
*/
|
|
96
|
+
export interface WatchOptions {
|
|
97
|
+
/** Directories to watch */
|
|
98
|
+
paths?: string[];
|
|
99
|
+
/** Debounce delay in ms */
|
|
100
|
+
delay?: string;
|
|
101
|
+
/** Auto-apply without confirmation */
|
|
102
|
+
auto?: boolean;
|
|
103
|
+
/** Queue for review instead of auto-apply */
|
|
104
|
+
queue?: boolean;
|
|
105
|
+
/** Model override */
|
|
106
|
+
model?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Watch event for a new file
|
|
111
|
+
*/
|
|
112
|
+
export interface WatchEvent {
|
|
113
|
+
/** Event type */
|
|
114
|
+
type: "add" | "change";
|
|
115
|
+
/** File path */
|
|
116
|
+
path: string;
|
|
117
|
+
/** Timestamp */
|
|
118
|
+
timestamp: Date;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Status of a file move operation
|
|
123
|
+
*/
|
|
124
|
+
export type MoveStatus =
|
|
125
|
+
| "pending"
|
|
126
|
+
| "moving"
|
|
127
|
+
| "completed"
|
|
128
|
+
| "failed"
|
|
129
|
+
| "skipped"
|
|
130
|
+
| "conflict";
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Result of a file move operation
|
|
134
|
+
*/
|
|
135
|
+
export interface MoveResult {
|
|
136
|
+
/** Source path */
|
|
137
|
+
source: string;
|
|
138
|
+
/** Destination path */
|
|
139
|
+
destination: string;
|
|
140
|
+
/** Move status */
|
|
141
|
+
status: MoveStatus;
|
|
142
|
+
/** Error message if failed */
|
|
143
|
+
error?: string;
|
|
144
|
+
}
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* File system utilities for tidy
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { rename, mkdir, access, copyFile, unlink, stat } from "fs/promises";
|
|
6
|
+
import { dirname, join, basename, extname } from "path";
|
|
7
|
+
import { existsSync } from "fs";
|
|
8
|
+
import type { MoveResult, MoveStatus } from "../types/organizer.ts";
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Check if a file exists
|
|
12
|
+
*/
|
|
13
|
+
export async function fileExists(filePath: string): Promise<boolean> {
|
|
14
|
+
try {
|
|
15
|
+
await access(filePath);
|
|
16
|
+
return true;
|
|
17
|
+
} catch {
|
|
18
|
+
return false;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Ensure a directory exists, creating it if necessary
|
|
24
|
+
*/
|
|
25
|
+
export async function ensureDirectory(dirPath: string): Promise<void> {
|
|
26
|
+
if (!existsSync(dirPath)) {
|
|
27
|
+
await mkdir(dirPath, { recursive: true });
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate a unique filename by appending a number if the file already exists
|
|
33
|
+
*/
|
|
34
|
+
export async function generateUniqueName(
|
|
35
|
+
destination: string
|
|
36
|
+
): Promise<string> {
|
|
37
|
+
if (!(await fileExists(destination))) {
|
|
38
|
+
return destination;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const dir = dirname(destination);
|
|
42
|
+
const ext = extname(destination);
|
|
43
|
+
const base = basename(destination, ext);
|
|
44
|
+
|
|
45
|
+
let counter = 1;
|
|
46
|
+
let newPath: string;
|
|
47
|
+
|
|
48
|
+
do {
|
|
49
|
+
newPath = join(dir, `${base} (${counter})${ext}`);
|
|
50
|
+
counter++;
|
|
51
|
+
} while (await fileExists(newPath));
|
|
52
|
+
|
|
53
|
+
return newPath;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Conflict resolution strategy
|
|
58
|
+
*/
|
|
59
|
+
export type ConflictStrategy = "rename" | "overwrite" | "skip";
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a destination path conflict
|
|
63
|
+
*/
|
|
64
|
+
export async function resolveConflict(
|
|
65
|
+
destination: string,
|
|
66
|
+
strategy: ConflictStrategy
|
|
67
|
+
): Promise<{ path: string; status: MoveStatus }> {
|
|
68
|
+
const exists = await fileExists(destination);
|
|
69
|
+
|
|
70
|
+
if (!exists) {
|
|
71
|
+
return { path: destination, status: "pending" };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
switch (strategy) {
|
|
75
|
+
case "rename":
|
|
76
|
+
return { path: await generateUniqueName(destination), status: "pending" };
|
|
77
|
+
case "overwrite":
|
|
78
|
+
return { path: destination, status: "pending" };
|
|
79
|
+
case "skip":
|
|
80
|
+
return { path: destination, status: "skipped" };
|
|
81
|
+
default:
|
|
82
|
+
return { path: destination, status: "conflict" };
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Move a file from source to destination
|
|
88
|
+
*/
|
|
89
|
+
export async function moveFile(
|
|
90
|
+
source: string,
|
|
91
|
+
destination: string,
|
|
92
|
+
options: {
|
|
93
|
+
overwrite?: boolean;
|
|
94
|
+
backup?: boolean;
|
|
95
|
+
} = {}
|
|
96
|
+
): Promise<MoveResult> {
|
|
97
|
+
try {
|
|
98
|
+
// Ensure the destination directory exists
|
|
99
|
+
await ensureDirectory(dirname(destination));
|
|
100
|
+
|
|
101
|
+
// Check if destination exists
|
|
102
|
+
const destExists = await fileExists(destination);
|
|
103
|
+
|
|
104
|
+
if (destExists) {
|
|
105
|
+
if (options.backup) {
|
|
106
|
+
// Create a backup of the existing file
|
|
107
|
+
const backupPath = await generateUniqueName(destination + ".backup");
|
|
108
|
+
await copyFile(destination, backupPath);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (!options.overwrite) {
|
|
112
|
+
// Generate a unique name
|
|
113
|
+
destination = await generateUniqueName(destination);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Try to rename (move) the file
|
|
118
|
+
try {
|
|
119
|
+
await rename(source, destination);
|
|
120
|
+
} catch (error: any) {
|
|
121
|
+
// If rename fails (cross-device), fall back to copy + delete
|
|
122
|
+
if (error.code === "EXDEV") {
|
|
123
|
+
await copyFile(source, destination);
|
|
124
|
+
await unlink(source);
|
|
125
|
+
} else {
|
|
126
|
+
throw error;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return {
|
|
131
|
+
source,
|
|
132
|
+
destination,
|
|
133
|
+
status: "completed",
|
|
134
|
+
};
|
|
135
|
+
} catch (error: any) {
|
|
136
|
+
return {
|
|
137
|
+
source,
|
|
138
|
+
destination,
|
|
139
|
+
status: "failed",
|
|
140
|
+
error: error.message,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Get file stats safely
|
|
147
|
+
*/
|
|
148
|
+
export async function getFileStats(
|
|
149
|
+
filePath: string
|
|
150
|
+
): Promise<{ size: number; mtime: Date; birthtime: Date } | null> {
|
|
151
|
+
try {
|
|
152
|
+
const stats = await stat(filePath);
|
|
153
|
+
return {
|
|
154
|
+
size: stats.size,
|
|
155
|
+
mtime: stats.mtime,
|
|
156
|
+
birthtime: stats.birthtime,
|
|
157
|
+
};
|
|
158
|
+
} catch {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Format file size for display
|
|
165
|
+
*/
|
|
166
|
+
export function formatFileSize(bytes: number): string {
|
|
167
|
+
if (bytes === 0) return "0 B";
|
|
168
|
+
|
|
169
|
+
const units = ["B", "KB", "MB", "GB", "TB"];
|
|
170
|
+
const k = 1024;
|
|
171
|
+
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
|
172
|
+
|
|
173
|
+
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${units[i]}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Check if a path is a directory
|
|
178
|
+
*/
|
|
179
|
+
export async function isDirectory(path: string): Promise<boolean> {
|
|
180
|
+
try {
|
|
181
|
+
const stats = await stat(path);
|
|
182
|
+
return stats.isDirectory();
|
|
183
|
+
} catch {
|
|
184
|
+
return false;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Check if a path is a file
|
|
190
|
+
*/
|
|
191
|
+
export async function isFile(path: string): Promise<boolean> {
|
|
192
|
+
try {
|
|
193
|
+
const stats = await stat(path);
|
|
194
|
+
return stats.isFile();
|
|
195
|
+
} catch {
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
}
|