tidyf 1.0.3 → 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/dist/cli.js +18000 -15880
- package/dist/index.js +18450 -16419
- package/package.json +1 -1
- package/src/cli.ts +42 -14
- package/src/commands/config.ts +70 -1
- package/src/commands/organize.ts +679 -44
- package/src/commands/profile.ts +943 -0
- package/src/commands/undo.ts +139 -0
- package/src/commands/watch.ts +24 -2
- package/src/lib/config.ts +69 -0
- package/src/lib/history.ts +139 -0
- package/src/lib/opencode.ts +11 -5
- package/src/lib/presets.ts +257 -0
- package/src/lib/profiles.ts +367 -0
- package/src/types/organizer.ts +24 -0
- package/src/types/profile.ts +70 -0
- package/src/utils/files.ts +15 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Undo command - revert file organization operations
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import { rename, mkdir } from "fs/promises";
|
|
8
|
+
import { dirname } from "path";
|
|
9
|
+
import color from "picocolors";
|
|
10
|
+
import { initGlobalConfig } from "../lib/config.ts";
|
|
11
|
+
import { cleanup } from "../lib/opencode.ts";
|
|
12
|
+
import {
|
|
13
|
+
deleteHistoryEntry,
|
|
14
|
+
getHistoryEntry,
|
|
15
|
+
getRecentHistory,
|
|
16
|
+
type HistoryEntry,
|
|
17
|
+
} from "../lib/history.ts";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Select a history entry to undo
|
|
21
|
+
*/
|
|
22
|
+
async function selectHistoryEntry(): Promise<HistoryEntry | null> {
|
|
23
|
+
const history = getRecentHistory();
|
|
24
|
+
|
|
25
|
+
if (history.length === 0) {
|
|
26
|
+
p.log.warn("No history to undo");
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const options = history.map((entry) => ({
|
|
31
|
+
value: entry.id,
|
|
32
|
+
label: `${new Date(entry.timestamp).toLocaleString()}`,
|
|
33
|
+
hint: `${entry.moves.length} files moved`,
|
|
34
|
+
}));
|
|
35
|
+
|
|
36
|
+
const selectedId = await p.select({
|
|
37
|
+
message: "Which operation to undo?",
|
|
38
|
+
options,
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
if (p.isCancel(selectedId)) {
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return getHistoryEntry(selectedId as string);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Undo a file move
|
|
50
|
+
*/
|
|
51
|
+
async function undoMove(source: string, destination: string): Promise<boolean> {
|
|
52
|
+
try {
|
|
53
|
+
// Ensure source directory exists (in case it was deleted)
|
|
54
|
+
await mkdir(dirname(source), { recursive: true });
|
|
55
|
+
|
|
56
|
+
await rename(destination, source);
|
|
57
|
+
return true;
|
|
58
|
+
} catch {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Main undo command
|
|
65
|
+
*/
|
|
66
|
+
export async function undoCommand(): Promise<void> {
|
|
67
|
+
p.intro(color.bgRed(color.black(" tidyf undo ")));
|
|
68
|
+
|
|
69
|
+
initGlobalConfig();
|
|
70
|
+
|
|
71
|
+
const entry = await selectHistoryEntry();
|
|
72
|
+
|
|
73
|
+
if (!entry) {
|
|
74
|
+
p.outro(color.yellow("Nothing to undo"));
|
|
75
|
+
cleanup();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
console.log();
|
|
80
|
+
p.log.info(color.bold("Operation details:"));
|
|
81
|
+
p.log.message(` Timestamp: ${new Date(entry.timestamp).toLocaleString()}`);
|
|
82
|
+
p.log.message(` Source: ${entry.source}`);
|
|
83
|
+
p.log.message(` Target: ${entry.target}`);
|
|
84
|
+
p.log.message(` Files moved: ${entry.moves.length}`);
|
|
85
|
+
|
|
86
|
+
const confirm = await p.confirm({
|
|
87
|
+
message: `Undo this operation? This will move ${entry.moves.length} files back to ${entry.source}`,
|
|
88
|
+
initialValue: false,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
92
|
+
p.outro(color.yellow("Cancelled"));
|
|
93
|
+
cleanup();
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const s = p.spinner();
|
|
98
|
+
s.start("Undoing file moves...");
|
|
99
|
+
|
|
100
|
+
let successCount = 0;
|
|
101
|
+
let failCount = 0;
|
|
102
|
+
|
|
103
|
+
for (const move of entry.moves) {
|
|
104
|
+
const destExists = existsSync(move.destination);
|
|
105
|
+
|
|
106
|
+
if (!destExists) {
|
|
107
|
+
p.log.warn(` Skipped: ${move.destination} (already moved/deleted)`);
|
|
108
|
+
failCount++;
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (await undoMove(move.source, move.destination)) {
|
|
113
|
+
successCount++;
|
|
114
|
+
} else {
|
|
115
|
+
failCount++;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
s.stop("Undo complete");
|
|
120
|
+
|
|
121
|
+
p.log.success(`\nMoved ${successCount} files back to ${color.cyan(entry.source)}`);
|
|
122
|
+
|
|
123
|
+
if (failCount > 0) {
|
|
124
|
+
p.log.warn(`${failCount} files could not be moved`);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const deleteFromHistory = await p.confirm({
|
|
128
|
+
message: "Remove this operation from history?",
|
|
129
|
+
initialValue: true,
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
if (p.isCancel(deleteFromHistory) || deleteFromHistory) {
|
|
133
|
+
deleteHistoryEntry(entry.id);
|
|
134
|
+
p.log.success("Removed from history");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
p.outro(color.green("Done!"));
|
|
138
|
+
cleanup();
|
|
139
|
+
}
|
package/src/commands/watch.ts
CHANGED
|
@@ -9,8 +9,10 @@ import {
|
|
|
9
9
|
initGlobalConfig,
|
|
10
10
|
parseModelString,
|
|
11
11
|
resolveConfig,
|
|
12
|
+
resolveConfigWithProfile,
|
|
12
13
|
} from "../lib/config.ts";
|
|
13
14
|
import { analyzeFiles, cleanup } from "../lib/opencode.ts";
|
|
15
|
+
import { listProfiles, profileExists } from "../lib/profiles.ts";
|
|
14
16
|
import { getFileMetadata, scanFolderStructure } from "../lib/scanner.ts";
|
|
15
17
|
import { createWatcher, type FileWatcher } from "../lib/watcher.ts";
|
|
16
18
|
import type {
|
|
@@ -152,8 +154,26 @@ export async function watchCommand(options: WatchOptions): Promise<void> {
|
|
|
152
154
|
// Initialize global config if needed
|
|
153
155
|
initGlobalConfig();
|
|
154
156
|
|
|
155
|
-
//
|
|
156
|
-
|
|
157
|
+
// Validate and resolve profile if specified
|
|
158
|
+
if (options.profile) {
|
|
159
|
+
if (!profileExists(options.profile)) {
|
|
160
|
+
p.log.error(`Profile "${options.profile}" not found`);
|
|
161
|
+
const profiles = listProfiles();
|
|
162
|
+
if (profiles.length > 0) {
|
|
163
|
+
p.log.info("Available profiles:");
|
|
164
|
+
profiles.forEach((pr) => p.log.message(` - ${pr.name}`));
|
|
165
|
+
}
|
|
166
|
+
p.outro("Canceled");
|
|
167
|
+
cleanup();
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
p.log.info(`Profile: ${color.cyan(options.profile)}`);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Resolve configuration (with profile if specified)
|
|
174
|
+
const config = options.profile
|
|
175
|
+
? resolveConfigWithProfile(options.profile)
|
|
176
|
+
: resolveConfig();
|
|
157
177
|
|
|
158
178
|
// Determine paths to watch
|
|
159
179
|
let watchPaths: string[];
|
|
@@ -253,6 +273,7 @@ export async function watchCommand(options: WatchOptions): Promise<void> {
|
|
|
253
273
|
targetDir: targetPath,
|
|
254
274
|
model: parseModelString(options.model),
|
|
255
275
|
existingFolders,
|
|
276
|
+
profileName: options.profile,
|
|
256
277
|
});
|
|
257
278
|
s.stop("Analysis complete");
|
|
258
279
|
} catch (error: any) {
|
|
@@ -324,6 +345,7 @@ export async function watchCommand(options: WatchOptions): Promise<void> {
|
|
|
324
345
|
targetDir: targetPath,
|
|
325
346
|
model: parseModelString(options.model),
|
|
326
347
|
existingFolders,
|
|
348
|
+
profileName: options.profile,
|
|
327
349
|
});
|
|
328
350
|
s.stop("Analysis complete");
|
|
329
351
|
|
package/src/lib/config.ts
CHANGED
|
@@ -8,6 +8,12 @@ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
|
8
8
|
import { join, dirname } from "path";
|
|
9
9
|
import { homedir } from "os";
|
|
10
10
|
import type { TidyConfig, ModelSelection } from "../types/config.ts";
|
|
11
|
+
import {
|
|
12
|
+
profileExists,
|
|
13
|
+
readProfile,
|
|
14
|
+
readProfileRules,
|
|
15
|
+
getProfileConfigFields,
|
|
16
|
+
} from "./profiles.ts";
|
|
11
17
|
|
|
12
18
|
const CONFIG_DIR = ".tidy";
|
|
13
19
|
const SETTINGS_FILE = "settings.json";
|
|
@@ -347,3 +353,66 @@ export function getDefaultConfig(): TidyConfig {
|
|
|
347
353
|
export function getDefaultRules(): string {
|
|
348
354
|
return DEFAULT_RULES;
|
|
349
355
|
}
|
|
356
|
+
|
|
357
|
+
/**
|
|
358
|
+
* Resolve the effective configuration with profile support
|
|
359
|
+
* Resolution order: defaults → global → profile → local
|
|
360
|
+
*/
|
|
361
|
+
export function resolveConfigWithProfile(
|
|
362
|
+
profileName?: string,
|
|
363
|
+
basePath: string = process.cwd(),
|
|
364
|
+
): TidyConfig {
|
|
365
|
+
// Start with defaults
|
|
366
|
+
const config: TidyConfig = { ...DEFAULT_CONFIG };
|
|
367
|
+
|
|
368
|
+
// Merge global config
|
|
369
|
+
const globalConfig = readConfig(getGlobalConfigPath());
|
|
370
|
+
Object.assign(config, globalConfig);
|
|
371
|
+
|
|
372
|
+
// Merge profile config (if specified and exists)
|
|
373
|
+
if (profileName && profileExists(profileName)) {
|
|
374
|
+
const profile = readProfile(profileName);
|
|
375
|
+
if (profile) {
|
|
376
|
+
const profileConfig = getProfileConfigFields(profile);
|
|
377
|
+
Object.assign(config, profileConfig);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// Merge local config (takes precedence)
|
|
382
|
+
const localConfig = readConfig(getLocalConfigPath(basePath));
|
|
383
|
+
Object.assign(config, localConfig);
|
|
384
|
+
|
|
385
|
+
return config;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Get the rules prompt with profile support
|
|
390
|
+
* Resolution order: local → profile → global → default
|
|
391
|
+
*/
|
|
392
|
+
export function getRulesPromptWithProfile(
|
|
393
|
+
profileName?: string,
|
|
394
|
+
basePath: string = process.cwd(),
|
|
395
|
+
): string {
|
|
396
|
+
// Try local rules first (always highest priority)
|
|
397
|
+
const localRules = readRules(getLocalRulesPath(basePath));
|
|
398
|
+
if (localRules) {
|
|
399
|
+
return localRules;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
// Try profile rules (if specified and exists)
|
|
403
|
+
if (profileName && profileExists(profileName)) {
|
|
404
|
+
const profileRules = readProfileRules(profileName);
|
|
405
|
+
if (profileRules) {
|
|
406
|
+
return profileRules;
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// Fall back to global rules
|
|
411
|
+
const globalRules = readRules(getGlobalRulesPath());
|
|
412
|
+
if (globalRules) {
|
|
413
|
+
return globalRules;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
// Return default rules
|
|
417
|
+
return DEFAULT_RULES;
|
|
418
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* History management for file operations
|
|
3
|
+
*
|
|
4
|
+
* Logs file moves to ~/.tidy/history.json for undo functionality
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
8
|
+
import { join, dirname } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
import { ensureDirectory } from "../utils/files.ts";
|
|
11
|
+
|
|
12
|
+
const HISTORY_DIR = ".tidy";
|
|
13
|
+
const HISTORY_FILE = "history.json";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get the history file path
|
|
17
|
+
*/
|
|
18
|
+
function getHistoryPath(): string {
|
|
19
|
+
return join(homedir(), HISTORY_DIR, HISTORY_FILE);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* History entry for a file operation
|
|
24
|
+
*/
|
|
25
|
+
export interface HistoryEntry {
|
|
26
|
+
/** Unique ID for this operation */
|
|
27
|
+
id: string;
|
|
28
|
+
/** Timestamp of operation */
|
|
29
|
+
timestamp: string;
|
|
30
|
+
/** Source directory */
|
|
31
|
+
source: string;
|
|
32
|
+
/** Target directory */
|
|
33
|
+
target: string;
|
|
34
|
+
/** Files that were moved */
|
|
35
|
+
moves: Array<{
|
|
36
|
+
source: string;
|
|
37
|
+
destination: string;
|
|
38
|
+
timestamp: string;
|
|
39
|
+
}>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Read history from file
|
|
44
|
+
*/
|
|
45
|
+
export function readHistory(): HistoryEntry[] {
|
|
46
|
+
const historyPath = getHistoryPath();
|
|
47
|
+
|
|
48
|
+
if (!existsSync(historyPath)) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
try {
|
|
53
|
+
const content = readFileSync(historyPath, "utf-8");
|
|
54
|
+
return JSON.parse(content);
|
|
55
|
+
} catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Write history to file
|
|
62
|
+
*/
|
|
63
|
+
function writeHistory(history: HistoryEntry[]): void {
|
|
64
|
+
const historyPath = getHistoryPath();
|
|
65
|
+
ensureDirectory(dirname(historyPath));
|
|
66
|
+
writeFileSync(historyPath, JSON.stringify(history, null, 2), "utf-8");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Create a new history entry
|
|
71
|
+
*/
|
|
72
|
+
export function createHistoryEntry(
|
|
73
|
+
source: string,
|
|
74
|
+
target: string,
|
|
75
|
+
): HistoryEntry {
|
|
76
|
+
return {
|
|
77
|
+
id: Date.now().toString(),
|
|
78
|
+
timestamp: new Date().toISOString(),
|
|
79
|
+
source,
|
|
80
|
+
target,
|
|
81
|
+
moves: [],
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Add a file move to history entry
|
|
87
|
+
*/
|
|
88
|
+
export function addMoveToHistory(
|
|
89
|
+
entry: HistoryEntry,
|
|
90
|
+
source: string,
|
|
91
|
+
destination: string,
|
|
92
|
+
): void {
|
|
93
|
+
entry.moves.push({
|
|
94
|
+
source,
|
|
95
|
+
destination,
|
|
96
|
+
timestamp: new Date().toISOString(),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Save a history entry
|
|
102
|
+
*/
|
|
103
|
+
export function saveHistoryEntry(entry: HistoryEntry): void {
|
|
104
|
+
const history = readHistory();
|
|
105
|
+
history.unshift(entry);
|
|
106
|
+
writeHistory(history);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Get history entry by ID
|
|
111
|
+
*/
|
|
112
|
+
export function getHistoryEntry(id: string): HistoryEntry | null {
|
|
113
|
+
const history = readHistory();
|
|
114
|
+
return history.find((entry) => entry.id === id) || null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Get the last N history entries
|
|
119
|
+
*/
|
|
120
|
+
export function getRecentHistory(limit: number = 10): HistoryEntry[] {
|
|
121
|
+
const history = readHistory();
|
|
122
|
+
return history.slice(0, limit);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Delete history entry by ID
|
|
127
|
+
*/
|
|
128
|
+
export function deleteHistoryEntry(id: string): void {
|
|
129
|
+
const history = readHistory();
|
|
130
|
+
const filtered = history.filter((entry) => entry.id !== id);
|
|
131
|
+
writeHistory(filtered);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Clear all history
|
|
136
|
+
*/
|
|
137
|
+
export function clearHistory(): void {
|
|
138
|
+
writeHistory([]);
|
|
139
|
+
}
|
package/src/lib/opencode.ts
CHANGED
|
@@ -20,7 +20,11 @@ import type {
|
|
|
20
20
|
OrganizationProposal,
|
|
21
21
|
} from "../types/organizer.ts";
|
|
22
22
|
import { fileExists } from "../utils/files.ts";
|
|
23
|
-
import {
|
|
23
|
+
import {
|
|
24
|
+
expandPath,
|
|
25
|
+
getRulesPromptWithProfile,
|
|
26
|
+
resolveConfigWithProfile,
|
|
27
|
+
} from "./config.ts";
|
|
24
28
|
|
|
25
29
|
const execAsync = promisify(exec);
|
|
26
30
|
|
|
@@ -171,6 +175,8 @@ export interface AnalyzeFilesOptions {
|
|
|
171
175
|
model?: ModelSelection;
|
|
172
176
|
/** Existing folders in target directory for consistency */
|
|
173
177
|
existingFolders?: string[];
|
|
178
|
+
/** Profile name to use for config and rules */
|
|
179
|
+
profileName?: string;
|
|
174
180
|
}
|
|
175
181
|
|
|
176
182
|
/**
|
|
@@ -313,7 +319,7 @@ export async function checkConflicts(
|
|
|
313
319
|
export async function analyzeFiles(
|
|
314
320
|
options: AnalyzeFilesOptions,
|
|
315
321
|
): Promise<OrganizationProposal> {
|
|
316
|
-
const { files, targetDir, instructions, model, existingFolders } = options;
|
|
322
|
+
const { files, targetDir, instructions, model, existingFolders, profileName } = options;
|
|
317
323
|
|
|
318
324
|
if (files.length === 0) {
|
|
319
325
|
return {
|
|
@@ -327,9 +333,9 @@ export async function analyzeFiles(
|
|
|
327
333
|
const opencodeClient = await getClient();
|
|
328
334
|
const sid = await getSessionId();
|
|
329
335
|
|
|
330
|
-
// Get configuration
|
|
331
|
-
const config =
|
|
332
|
-
const rulesPrompt =
|
|
336
|
+
// Get configuration with profile awareness
|
|
337
|
+
const config = resolveConfigWithProfile(profileName);
|
|
338
|
+
const rulesPrompt = getRulesPromptWithProfile(profileName);
|
|
333
339
|
|
|
334
340
|
// Build the prompt
|
|
335
341
|
const filesJson = formatFilesForPrompt(files);
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Built-in profile presets for common use cases
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Profile } from "../types/profile.ts";
|
|
6
|
+
|
|
7
|
+
export interface PresetDefinition {
|
|
8
|
+
name: string;
|
|
9
|
+
description: string;
|
|
10
|
+
profile: Omit<Profile, "name" | "createdAt" | "modifiedAt">;
|
|
11
|
+
rules: string;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const PRESET_DEFINITIONS: PresetDefinition[] = [
|
|
15
|
+
{
|
|
16
|
+
name: "developer",
|
|
17
|
+
description: "Organize source code, configs, and project files",
|
|
18
|
+
profile: {
|
|
19
|
+
description: "Developer-focused organization for code and configs",
|
|
20
|
+
defaultTarget: "~/Documents/Dev",
|
|
21
|
+
},
|
|
22
|
+
rules: `# Developer Profile Rules
|
|
23
|
+
|
|
24
|
+
You are organizing files for a software developer. Focus on project structure and code organization.
|
|
25
|
+
|
|
26
|
+
## Categories
|
|
27
|
+
|
|
28
|
+
### Code
|
|
29
|
+
- Source files: .ts, .tsx, .js, .jsx, .py, .go, .rs, .java, .c, .cpp, .swift, .kt
|
|
30
|
+
- Subcategorize by language family: JavaScript, Python, Go, Rust, Swift, etc.
|
|
31
|
+
|
|
32
|
+
### Config
|
|
33
|
+
- Configuration files: .json, .yaml, .yml, .toml, .env, .ini
|
|
34
|
+
- Subcategorize: Project Config, Editor Config, CI/CD
|
|
35
|
+
|
|
36
|
+
### Documentation
|
|
37
|
+
- README, CHANGELOG, LICENSE, .md files
|
|
38
|
+
- API docs, design docs
|
|
39
|
+
|
|
40
|
+
### Data
|
|
41
|
+
- Database files, SQL scripts, seed data
|
|
42
|
+
- JSON/CSV data files
|
|
43
|
+
|
|
44
|
+
### Scripts
|
|
45
|
+
- Shell scripts, batch files, automation scripts
|
|
46
|
+
- Build scripts, deployment scripts
|
|
47
|
+
|
|
48
|
+
## Strategy
|
|
49
|
+
|
|
50
|
+
1. **Group by project** - If filename contains project identifiers, keep files together
|
|
51
|
+
2. **Separate configs from code** - Configs go to their own folder
|
|
52
|
+
3. **Archive old files** - Anything with "old", "backup", or date suffixes
|
|
53
|
+
|
|
54
|
+
## Output Format
|
|
55
|
+
|
|
56
|
+
Return JSON:
|
|
57
|
+
\`\`\`json
|
|
58
|
+
{
|
|
59
|
+
"proposals": [{ "file": "name", "destination": "Code/JavaScript/project", "category": {...} }],
|
|
60
|
+
"strategy": "...",
|
|
61
|
+
"uncategorized": []
|
|
62
|
+
}
|
|
63
|
+
\`\`\`
|
|
64
|
+
`,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
name: "creative",
|
|
68
|
+
description: "Organize images, videos, design files, and media",
|
|
69
|
+
profile: {
|
|
70
|
+
description: "Creative workflow for designers and content creators",
|
|
71
|
+
defaultTarget: "~/Documents/Creative",
|
|
72
|
+
},
|
|
73
|
+
rules: `# Creative Profile Rules
|
|
74
|
+
|
|
75
|
+
You are organizing files for a designer or content creator. Focus on visual assets and media organization.
|
|
76
|
+
|
|
77
|
+
## Categories
|
|
78
|
+
|
|
79
|
+
### Images
|
|
80
|
+
- Photos: .jpg, .jpeg, .png, .heic, .raw, .cr2, .nef
|
|
81
|
+
- Graphics: .svg, .webp, .gif
|
|
82
|
+
- Subcategorize: Photos, Screenshots, Icons, Illustrations
|
|
83
|
+
|
|
84
|
+
### Design
|
|
85
|
+
- Figma exports, Sketch files, PSD, AI
|
|
86
|
+
- Subcategorize by project or client name if detectable
|
|
87
|
+
|
|
88
|
+
### Video
|
|
89
|
+
- .mp4, .mov, .avi, .mkv, .webm
|
|
90
|
+
- Subcategorize: Raw Footage, Exports, Clips
|
|
91
|
+
|
|
92
|
+
### Audio
|
|
93
|
+
- .mp3, .wav, .aiff, .flac, .m4a
|
|
94
|
+
- Subcategorize: Music, SFX, Voiceover
|
|
95
|
+
|
|
96
|
+
### Fonts
|
|
97
|
+
- .ttf, .otf, .woff, .woff2
|
|
98
|
+
- Keep in centralized Fonts folder
|
|
99
|
+
|
|
100
|
+
### 3D
|
|
101
|
+
- .obj, .fbx, .blend, .gltf
|
|
102
|
+
- 3D models and assets
|
|
103
|
+
|
|
104
|
+
## Strategy
|
|
105
|
+
|
|
106
|
+
1. **Date-based for photos** - Organize photos by YYYY/MM if dates detected in filename
|
|
107
|
+
2. **Project-based for design** - Group by client or project name
|
|
108
|
+
3. **Keep exports together** - Files with "export", "final", "v2" stay in same project folder
|
|
109
|
+
|
|
110
|
+
## Output Format
|
|
111
|
+
|
|
112
|
+
Return JSON:
|
|
113
|
+
\`\`\`json
|
|
114
|
+
{
|
|
115
|
+
"proposals": [{ "file": "name", "destination": "Images/Photos/2024/January", "category": {...} }],
|
|
116
|
+
"strategy": "...",
|
|
117
|
+
"uncategorized": []
|
|
118
|
+
}
|
|
119
|
+
\`\`\`
|
|
120
|
+
`,
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
name: "student",
|
|
124
|
+
description: "Organize documents, notes, and academic materials",
|
|
125
|
+
profile: {
|
|
126
|
+
description: "Academic organization for students and researchers",
|
|
127
|
+
defaultTarget: "~/Documents/School",
|
|
128
|
+
},
|
|
129
|
+
rules: `# Student Profile Rules
|
|
130
|
+
|
|
131
|
+
You are organizing files for a student. Focus on academic organization and study materials.
|
|
132
|
+
|
|
133
|
+
## Categories
|
|
134
|
+
|
|
135
|
+
### Notes
|
|
136
|
+
- Text files, markdown notes, OneNote exports
|
|
137
|
+
- Subcategorize by subject if detectable
|
|
138
|
+
|
|
139
|
+
### Documents
|
|
140
|
+
- PDFs, Word docs, essays, reports
|
|
141
|
+
- Subcategorize: Assignments, Readings, Submissions
|
|
142
|
+
|
|
143
|
+
### Slides
|
|
144
|
+
- PowerPoint, Keynote, Google Slides exports
|
|
145
|
+
- Lecture slides, presentations
|
|
146
|
+
|
|
147
|
+
### Spreadsheets
|
|
148
|
+
- Excel, CSV, data analysis files
|
|
149
|
+
- Lab data, calculations
|
|
150
|
+
|
|
151
|
+
### Textbooks
|
|
152
|
+
- E-books: .epub, .mobi, .pdf (large PDFs)
|
|
153
|
+
- Reference materials
|
|
154
|
+
|
|
155
|
+
### Research
|
|
156
|
+
- Papers, citations, bibliography files
|
|
157
|
+
- Research data and notes
|
|
158
|
+
|
|
159
|
+
## Strategy
|
|
160
|
+
|
|
161
|
+
1. **Subject detection** - Look for subject names in filenames (math, history, physics, etc.)
|
|
162
|
+
2. **Semester organization** - Group by term if dates/semesters detected
|
|
163
|
+
3. **Assignment priority** - Files with "assignment", "hw", "lab" get special handling
|
|
164
|
+
|
|
165
|
+
## Output Format
|
|
166
|
+
|
|
167
|
+
Return JSON:
|
|
168
|
+
\`\`\`json
|
|
169
|
+
{
|
|
170
|
+
"proposals": [{ "file": "name", "destination": "Notes/Physics/Chapter1", "category": {...} }],
|
|
171
|
+
"strategy": "...",
|
|
172
|
+
"uncategorized": []
|
|
173
|
+
}
|
|
174
|
+
\`\`\`
|
|
175
|
+
`,
|
|
176
|
+
},
|
|
177
|
+
{
|
|
178
|
+
name: "downloads",
|
|
179
|
+
description: "Aggressive cleanup for messy Downloads folders",
|
|
180
|
+
profile: {
|
|
181
|
+
description: "Fast cleanup for Downloads folder chaos",
|
|
182
|
+
defaultSource: "~/Downloads",
|
|
183
|
+
defaultTarget: "~/Documents/Organized",
|
|
184
|
+
},
|
|
185
|
+
rules: `# Downloads Cleanup Profile
|
|
186
|
+
|
|
187
|
+
You are aggressively cleaning a messy Downloads folder. Be decisive and organize everything.
|
|
188
|
+
|
|
189
|
+
## Categories
|
|
190
|
+
|
|
191
|
+
### Installers
|
|
192
|
+
- DMG, PKG, EXE, MSI, APP, DEB, RPM
|
|
193
|
+
- Move to Installers folder, suggest deletion after install
|
|
194
|
+
|
|
195
|
+
### Archives
|
|
196
|
+
- ZIP, RAR, 7Z, TAR.GZ
|
|
197
|
+
- Keep in Archives, note if should be extracted
|
|
198
|
+
|
|
199
|
+
### Documents
|
|
200
|
+
- PDF, DOCX, XLSX, PPTX, TXT
|
|
201
|
+
- Subcategorize: Receipts, Manuals, Forms, Other
|
|
202
|
+
|
|
203
|
+
### Images
|
|
204
|
+
- All image formats
|
|
205
|
+
- Screenshots go to Screenshots subfolder
|
|
206
|
+
|
|
207
|
+
### Videos
|
|
208
|
+
- All video formats
|
|
209
|
+
- Downloads, Clips, Tutorials
|
|
210
|
+
|
|
211
|
+
### Audio
|
|
212
|
+
- All audio formats
|
|
213
|
+
- Music, Podcasts, Recordings
|
|
214
|
+
|
|
215
|
+
### Code
|
|
216
|
+
- Source files, scripts, configs
|
|
217
|
+
- Move to Development folder
|
|
218
|
+
|
|
219
|
+
### Temporary
|
|
220
|
+
- .tmp, .part, .crdownload, incomplete downloads
|
|
221
|
+
- Flag for deletion
|
|
222
|
+
|
|
223
|
+
## Strategy
|
|
224
|
+
|
|
225
|
+
1. **Be aggressive** - Everything gets categorized, nothing stays in Downloads
|
|
226
|
+
2. **Detect duplicates** - Files like "file (1).pdf" are duplicates
|
|
227
|
+
3. **Date cleanup** - Old files (>30 days) can go to Archive
|
|
228
|
+
4. **Installers cleanup** - Suggest keeping only latest versions
|
|
229
|
+
|
|
230
|
+
## Output Format
|
|
231
|
+
|
|
232
|
+
Return JSON:
|
|
233
|
+
\`\`\`json
|
|
234
|
+
{
|
|
235
|
+
"proposals": [{ "file": "name", "destination": "Documents/Receipts", "category": {...} }],
|
|
236
|
+
"strategy": "...",
|
|
237
|
+
"uncategorized": []
|
|
238
|
+
}
|
|
239
|
+
\`\`\`
|
|
240
|
+
`,
|
|
241
|
+
},
|
|
242
|
+
];
|
|
243
|
+
|
|
244
|
+
export function getPresetNames(): string[] {
|
|
245
|
+
return PRESET_DEFINITIONS.map((p) => p.name);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function getPreset(name: string): PresetDefinition | undefined {
|
|
249
|
+
return PRESET_DEFINITIONS.find((p) => p.name === name);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
export function listPresets(): { name: string; description: string }[] {
|
|
253
|
+
return PRESET_DEFINITIONS.map((p) => ({
|
|
254
|
+
name: p.name,
|
|
255
|
+
description: p.description,
|
|
256
|
+
}));
|
|
257
|
+
}
|