tidyf 1.0.2 → 1.1.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/README.md +11 -0
- package/dist/cli.js +20300 -16019
- package/dist/index.js +18473 -16331
- package/package.json +57 -56
- package/src/cli.ts +51 -15
- package/src/commands/config.ts +70 -1
- package/src/commands/organize.ts +696 -43
- package/src/commands/profile.ts +943 -0
- package/src/commands/undo.ts +139 -0
- package/src/commands/watch.ts +69 -3
- package/src/lib/config.ts +83 -0
- package/src/lib/history.ts +139 -0
- package/src/lib/opencode.ts +24 -6
- package/src/lib/presets.ts +257 -0
- package/src/lib/profiles.ts +367 -0
- package/src/lib/scanner.ts +103 -1
- package/src/types/organizer.ts +24 -0
- package/src/types/profile.ts +70 -0
- package/src/utils/files.ts +15 -1
package/src/lib/scanner.ts
CHANGED
|
@@ -3,12 +3,114 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { readdir, stat, readFile } from "fs/promises";
|
|
6
|
-
import { join, extname, basename } from "path";
|
|
6
|
+
import { join, extname, basename, relative } from "path";
|
|
7
7
|
import { lookup as lookupMimeType } from "mime-types";
|
|
8
8
|
import type { FileMetadata } from "../types/organizer.ts";
|
|
9
9
|
import { shouldIgnore } from "./config.ts";
|
|
10
10
|
import { isDirectory, isFile } from "../utils/files.ts";
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Options for scanning folder structure
|
|
14
|
+
*/
|
|
15
|
+
export interface ScanFolderOptions {
|
|
16
|
+
/** Maximum depth to scan (default: 3) */
|
|
17
|
+
maxDepth?: number;
|
|
18
|
+
/** Include empty folders (default: false) */
|
|
19
|
+
includeEmpty?: boolean;
|
|
20
|
+
/** Patterns to ignore */
|
|
21
|
+
ignore?: string[];
|
|
22
|
+
/** Maximum number of folders to return (default: 100) */
|
|
23
|
+
maxFolders?: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Scan a directory and return existing folder structure as relative paths
|
|
28
|
+
* Used to inform AI about existing organization
|
|
29
|
+
*/
|
|
30
|
+
export async function scanFolderStructure(
|
|
31
|
+
dirPath: string,
|
|
32
|
+
options: ScanFolderOptions = {}
|
|
33
|
+
): Promise<string[]> {
|
|
34
|
+
const {
|
|
35
|
+
maxDepth = 3,
|
|
36
|
+
includeEmpty = false,
|
|
37
|
+
ignore = [],
|
|
38
|
+
maxFolders = 100,
|
|
39
|
+
} = options;
|
|
40
|
+
|
|
41
|
+
const folders: string[] = [];
|
|
42
|
+
|
|
43
|
+
await scanFoldersInternal(dirPath, dirPath, 0, maxDepth, includeEmpty, ignore, folders, maxFolders);
|
|
44
|
+
|
|
45
|
+
// Sort by depth (shallower first) then alphabetically
|
|
46
|
+
folders.sort((a, b) => {
|
|
47
|
+
const depthA = a.split("/").length;
|
|
48
|
+
const depthB = b.split("/").length;
|
|
49
|
+
if (depthA !== depthB) return depthA - depthB;
|
|
50
|
+
return a.localeCompare(b);
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
return folders.slice(0, maxFolders);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function scanFoldersInternal(
|
|
57
|
+
basePath: string,
|
|
58
|
+
currentPath: string,
|
|
59
|
+
currentDepth: number,
|
|
60
|
+
maxDepth: number,
|
|
61
|
+
includeEmpty: boolean,
|
|
62
|
+
ignore: string[],
|
|
63
|
+
folders: string[],
|
|
64
|
+
maxFolders: number
|
|
65
|
+
): Promise<void> {
|
|
66
|
+
if (currentDepth >= maxDepth || folders.length >= maxFolders) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
try {
|
|
71
|
+
const entries = await readdir(currentPath, { withFileTypes: true });
|
|
72
|
+
|
|
73
|
+
for (const entry of entries) {
|
|
74
|
+
if (folders.length >= maxFolders) break;
|
|
75
|
+
|
|
76
|
+
if (!entry.isDirectory()) continue;
|
|
77
|
+
|
|
78
|
+
// Skip hidden folders and ignored patterns
|
|
79
|
+
if (entry.name.startsWith(".")) continue;
|
|
80
|
+
if (shouldIgnore(entry.name, ignore)) continue;
|
|
81
|
+
|
|
82
|
+
const fullPath = join(currentPath, entry.name);
|
|
83
|
+
const relativePath = relative(basePath, fullPath);
|
|
84
|
+
|
|
85
|
+
// Check if folder has contents (if we care about empty folders)
|
|
86
|
+
if (!includeEmpty) {
|
|
87
|
+
try {
|
|
88
|
+
const contents = await readdir(fullPath);
|
|
89
|
+
if (contents.length === 0) continue;
|
|
90
|
+
} catch {
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
folders.push(relativePath);
|
|
96
|
+
|
|
97
|
+
// Recursively scan subdirectories
|
|
98
|
+
await scanFoldersInternal(
|
|
99
|
+
basePath,
|
|
100
|
+
fullPath,
|
|
101
|
+
currentDepth + 1,
|
|
102
|
+
maxDepth,
|
|
103
|
+
includeEmpty,
|
|
104
|
+
ignore,
|
|
105
|
+
folders,
|
|
106
|
+
maxFolders
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
} catch {
|
|
110
|
+
// Silently skip directories we can't read
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
12
114
|
/**
|
|
13
115
|
* Options for scanning a directory
|
|
14
116
|
*/
|
package/src/types/organizer.ts
CHANGED
|
@@ -22,6 +22,8 @@ export interface FileMetadata {
|
|
|
22
22
|
mimeType?: string;
|
|
23
23
|
/** Optional content preview (first N bytes/lines) */
|
|
24
24
|
contentPreview?: string;
|
|
25
|
+
/** Content hash for duplicate detection */
|
|
26
|
+
hash?: string;
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -56,6 +58,18 @@ export interface FileMoveProposal {
|
|
|
56
58
|
conflictExists: boolean;
|
|
57
59
|
}
|
|
58
60
|
|
|
61
|
+
/**
|
|
62
|
+
* A group of duplicate files (same content hash)
|
|
63
|
+
*/
|
|
64
|
+
export interface DuplicateGroup {
|
|
65
|
+
/** Content hash shared by all files */
|
|
66
|
+
hash: string;
|
|
67
|
+
/** Files with identical content */
|
|
68
|
+
files: FileMetadata[];
|
|
69
|
+
/** Total wasted space (all but one copy) */
|
|
70
|
+
wastedBytes: number;
|
|
71
|
+
}
|
|
72
|
+
|
|
59
73
|
/**
|
|
60
74
|
* Result of AI analysis
|
|
61
75
|
*/
|
|
@@ -68,6 +82,8 @@ export interface OrganizationProposal {
|
|
|
68
82
|
uncategorized: FileMetadata[];
|
|
69
83
|
/** Timestamp of analysis */
|
|
70
84
|
analyzedAt: Date;
|
|
85
|
+
/** Detected duplicate file groups */
|
|
86
|
+
duplicates?: DuplicateGroup[];
|
|
71
87
|
}
|
|
72
88
|
|
|
73
89
|
/**
|
|
@@ -88,6 +104,12 @@ export interface OrganizeOptions {
|
|
|
88
104
|
target?: string;
|
|
89
105
|
/** Model override */
|
|
90
106
|
model?: string;
|
|
107
|
+
/** Profile name to use */
|
|
108
|
+
profile?: string;
|
|
109
|
+
/** Output JSON instead of interactive UI */
|
|
110
|
+
json?: boolean;
|
|
111
|
+
/** Detect duplicate files by content hash */
|
|
112
|
+
detectDuplicates?: boolean;
|
|
91
113
|
}
|
|
92
114
|
|
|
93
115
|
/**
|
|
@@ -104,6 +126,8 @@ export interface WatchOptions {
|
|
|
104
126
|
queue?: boolean;
|
|
105
127
|
/** Model override */
|
|
106
128
|
model?: string;
|
|
129
|
+
/** Profile name to use */
|
|
130
|
+
profile?: string;
|
|
107
131
|
}
|
|
108
132
|
|
|
109
133
|
/**
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile types for tidyf
|
|
3
|
+
*
|
|
4
|
+
* Profiles allow users to create named configuration presets
|
|
5
|
+
* that bundle source/target paths, AI model preferences, ignore patterns,
|
|
6
|
+
* and optionally custom organization rules.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { TidyConfig } from "./config.ts";
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Profile configuration - extends TidyConfig with metadata
|
|
13
|
+
* Profiles inherit from global config, only overriding specified fields.
|
|
14
|
+
*/
|
|
15
|
+
export interface Profile extends Partial<TidyConfig> {
|
|
16
|
+
/** Profile name (directory name) */
|
|
17
|
+
name: string;
|
|
18
|
+
/** Human-readable description */
|
|
19
|
+
description?: string;
|
|
20
|
+
/** When the profile was created */
|
|
21
|
+
createdAt?: string;
|
|
22
|
+
/** When the profile was last modified */
|
|
23
|
+
modifiedAt?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Profile metadata for listing (without full config)
|
|
28
|
+
*/
|
|
29
|
+
export interface ProfileMetadata {
|
|
30
|
+
/** Profile name */
|
|
31
|
+
name: string;
|
|
32
|
+
/** Human-readable description */
|
|
33
|
+
description?: string;
|
|
34
|
+
/** When the profile was created */
|
|
35
|
+
createdAt?: string;
|
|
36
|
+
/** When the profile was last modified */
|
|
37
|
+
modifiedAt?: string;
|
|
38
|
+
/** Whether profile has custom rules.md */
|
|
39
|
+
hasCustomRules: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Options for profile command
|
|
44
|
+
*/
|
|
45
|
+
export interface ProfileCommandOptions {
|
|
46
|
+
/** Subcommand: list, create, edit, delete, show, copy, export, import */
|
|
47
|
+
action?: string;
|
|
48
|
+
/** Profile name for operations */
|
|
49
|
+
name?: string;
|
|
50
|
+
/** Additional arguments (e.g., destination for copy) */
|
|
51
|
+
args?: string[];
|
|
52
|
+
/** Create from current effective config */
|
|
53
|
+
fromCurrent?: boolean;
|
|
54
|
+
/** Force operation without confirmation */
|
|
55
|
+
force?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Export format for profiles (for sharing)
|
|
60
|
+
*/
|
|
61
|
+
export interface ProfileExport {
|
|
62
|
+
/** Export format version */
|
|
63
|
+
version: string;
|
|
64
|
+
/** When the export was created */
|
|
65
|
+
exportedAt: string;
|
|
66
|
+
/** The profile configuration */
|
|
67
|
+
profile: Profile;
|
|
68
|
+
/** Optional custom rules content */
|
|
69
|
+
rules?: string;
|
|
70
|
+
}
|
package/src/utils/files.ts
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
* File system utilities for tidy
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { rename, mkdir, access, copyFile, unlink, stat } from "fs/promises";
|
|
5
|
+
import { rename, mkdir, access, copyFile, unlink, stat, readFile } from "fs/promises";
|
|
6
|
+
import { createHash } from "crypto";
|
|
6
7
|
import { dirname, join, basename, extname } from "path";
|
|
7
8
|
import { existsSync } from "fs";
|
|
8
9
|
import type { MoveResult, MoveStatus } from "../types/organizer.ts";
|
|
@@ -196,3 +197,16 @@ export async function isFile(path: string): Promise<boolean> {
|
|
|
196
197
|
return false;
|
|
197
198
|
}
|
|
198
199
|
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Compute a hash of file contents for duplicate detection
|
|
203
|
+
* Uses MD5 for speed (not cryptographic security)
|
|
204
|
+
*/
|
|
205
|
+
export async function computeFileHash(filePath: string): Promise<string | null> {
|
|
206
|
+
try {
|
|
207
|
+
const content = await readFile(filePath);
|
|
208
|
+
return createHash("md5").update(content).digest("hex");
|
|
209
|
+
} catch {
|
|
210
|
+
return null;
|
|
211
|
+
}
|
|
212
|
+
}
|