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
|
@@ -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,9 +9,11 @@ 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";
|
|
14
|
-
import {
|
|
15
|
+
import { listProfiles, profileExists } from "../lib/profiles.ts";
|
|
16
|
+
import { getFileMetadata, scanFolderStructure } from "../lib/scanner.ts";
|
|
15
17
|
import { createWatcher, type FileWatcher } from "../lib/watcher.ts";
|
|
16
18
|
import type {
|
|
17
19
|
FileMetadata,
|
|
@@ -23,6 +25,41 @@ import type {
|
|
|
23
25
|
import { formatFileSize, moveFile } from "../utils/files.ts";
|
|
24
26
|
import { getCategoryIcon, getFileIcon } from "../utils/icons.ts";
|
|
25
27
|
|
|
28
|
+
// Folder cache for watch mode to avoid re-scanning on every file event
|
|
29
|
+
let folderCache: string[] = [];
|
|
30
|
+
let lastFolderScan = 0;
|
|
31
|
+
const FOLDER_CACHE_TTL = 5 * 60 * 1000; // 5 minutes
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Get existing folders with caching
|
|
35
|
+
*/
|
|
36
|
+
async function getExistingFolders(
|
|
37
|
+
targetPath: string,
|
|
38
|
+
ignore: string[] = [],
|
|
39
|
+
): Promise<string[]> {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
if (now - lastFolderScan > FOLDER_CACHE_TTL) {
|
|
42
|
+
try {
|
|
43
|
+
folderCache = await scanFolderStructure(targetPath, {
|
|
44
|
+
maxDepth: 3,
|
|
45
|
+
includeEmpty: false,
|
|
46
|
+
ignore,
|
|
47
|
+
});
|
|
48
|
+
lastFolderScan = now;
|
|
49
|
+
} catch {
|
|
50
|
+
// Target directory might not exist yet - use cached or empty
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return folderCache;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Invalidate folder cache (call after successful file moves)
|
|
58
|
+
*/
|
|
59
|
+
function invalidateFolderCache(): void {
|
|
60
|
+
lastFolderScan = 0;
|
|
61
|
+
}
|
|
62
|
+
|
|
26
63
|
/**
|
|
27
64
|
* Display a proposal briefly for watch mode
|
|
28
65
|
*/
|
|
@@ -57,6 +94,11 @@ async function executeProposalsSilent(
|
|
|
57
94
|
}
|
|
58
95
|
}
|
|
59
96
|
|
|
97
|
+
// Invalidate folder cache since new folders may have been created
|
|
98
|
+
if (success > 0) {
|
|
99
|
+
invalidateFolderCache();
|
|
100
|
+
}
|
|
101
|
+
|
|
60
102
|
return { success, failed };
|
|
61
103
|
}
|
|
62
104
|
|
|
@@ -112,8 +154,26 @@ export async function watchCommand(options: WatchOptions): Promise<void> {
|
|
|
112
154
|
// Initialize global config if needed
|
|
113
155
|
initGlobalConfig();
|
|
114
156
|
|
|
115
|
-
//
|
|
116
|
-
|
|
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();
|
|
117
177
|
|
|
118
178
|
// Determine paths to watch
|
|
119
179
|
let watchPaths: string[];
|
|
@@ -207,10 +267,13 @@ export async function watchCommand(options: WatchOptions): Promise<void> {
|
|
|
207
267
|
|
|
208
268
|
let proposal: OrganizationProposal;
|
|
209
269
|
try {
|
|
270
|
+
const existingFolders = await getExistingFolders(targetPath, config.ignore);
|
|
210
271
|
proposal = await analyzeFiles({
|
|
211
272
|
files,
|
|
212
273
|
targetDir: targetPath,
|
|
213
274
|
model: parseModelString(options.model),
|
|
275
|
+
existingFolders,
|
|
276
|
+
profileName: options.profile,
|
|
214
277
|
});
|
|
215
278
|
s.stop("Analysis complete");
|
|
216
279
|
} catch (error: any) {
|
|
@@ -276,10 +339,13 @@ export async function watchCommand(options: WatchOptions): Promise<void> {
|
|
|
276
339
|
s.start("Analyzing queued files...");
|
|
277
340
|
|
|
278
341
|
try {
|
|
342
|
+
const existingFolders = await getExistingFolders(targetPath, config.ignore);
|
|
279
343
|
const proposal = await analyzeFiles({
|
|
280
344
|
files: reviewQueue,
|
|
281
345
|
targetDir: targetPath,
|
|
282
346
|
model: parseModelString(options.model),
|
|
347
|
+
existingFolders,
|
|
348
|
+
profileName: options.profile,
|
|
283
349
|
});
|
|
284
350
|
s.stop("Analysis complete");
|
|
285
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";
|
|
@@ -94,6 +100,20 @@ You are an AI assistant that organizes files from a download folder. Analyze eac
|
|
|
94
100
|
5. Installer files go to Applications/Installers
|
|
95
101
|
6. Compressed files stay as Archives unless clearly part of another category
|
|
96
102
|
|
|
103
|
+
## Existing Folder Preferences
|
|
104
|
+
|
|
105
|
+
When existing folders are provided in the prompt:
|
|
106
|
+
|
|
107
|
+
1. **STRONGLY PREFER existing folders** - Use them when the file fits the category
|
|
108
|
+
2. **Match naming conventions** - If "Screenshots" exists, don't create "Screen Captures"
|
|
109
|
+
3. **Extend existing hierarchy** - OK to create subfolders under existing folders
|
|
110
|
+
4. **Only create new top-level folders** when no existing category applies
|
|
111
|
+
|
|
112
|
+
Examples:
|
|
113
|
+
- If "Images/Screenshots" exists, use it for screenshot files
|
|
114
|
+
- If "Documents/Work" exists but no "Reports" subfolder, create "Documents/Work/Reports"
|
|
115
|
+
- If only "Documents" exists, prefer "Documents/Receipts" over top-level "Receipts"
|
|
116
|
+
|
|
97
117
|
## Output Format
|
|
98
118
|
|
|
99
119
|
Return JSON with this exact structure:
|
|
@@ -333,3 +353,66 @@ export function getDefaultConfig(): TidyConfig {
|
|
|
333
353
|
export function getDefaultRules(): string {
|
|
334
354
|
return DEFAULT_RULES;
|
|
335
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
|
|
|
@@ -169,6 +173,10 @@ export interface AnalyzeFilesOptions {
|
|
|
169
173
|
instructions?: string;
|
|
170
174
|
/** Model override */
|
|
171
175
|
model?: ModelSelection;
|
|
176
|
+
/** Existing folders in target directory for consistency */
|
|
177
|
+
existingFolders?: string[];
|
|
178
|
+
/** Profile name to use for config and rules */
|
|
179
|
+
profileName?: string;
|
|
172
180
|
}
|
|
173
181
|
|
|
174
182
|
/**
|
|
@@ -311,7 +319,7 @@ export async function checkConflicts(
|
|
|
311
319
|
export async function analyzeFiles(
|
|
312
320
|
options: AnalyzeFilesOptions,
|
|
313
321
|
): Promise<OrganizationProposal> {
|
|
314
|
-
const { files, targetDir, instructions, model } = options;
|
|
322
|
+
const { files, targetDir, instructions, model, existingFolders, profileName } = options;
|
|
315
323
|
|
|
316
324
|
if (files.length === 0) {
|
|
317
325
|
return {
|
|
@@ -325,18 +333,28 @@ export async function analyzeFiles(
|
|
|
325
333
|
const opencodeClient = await getClient();
|
|
326
334
|
const sid = await getSessionId();
|
|
327
335
|
|
|
328
|
-
// Get configuration
|
|
329
|
-
const config =
|
|
330
|
-
const rulesPrompt =
|
|
336
|
+
// Get configuration with profile awareness
|
|
337
|
+
const config = resolveConfigWithProfile(profileName);
|
|
338
|
+
const rulesPrompt = getRulesPromptWithProfile(profileName);
|
|
331
339
|
|
|
332
340
|
// Build the prompt
|
|
333
341
|
const filesJson = formatFilesForPrompt(files);
|
|
334
342
|
|
|
343
|
+
// Build existing folders section if available
|
|
344
|
+
const existingFoldersSection = existingFolders?.length
|
|
345
|
+
? `
|
|
346
|
+
EXISTING FOLDERS in target directory:
|
|
347
|
+
${existingFolders.join("\n")}
|
|
348
|
+
|
|
349
|
+
IMPORTANT: Prefer using these existing folders when appropriate. Only create new folders when no suitable existing folder matches the file's category.
|
|
350
|
+
`
|
|
351
|
+
: "";
|
|
352
|
+
|
|
335
353
|
const userPrompt = `
|
|
336
354
|
Analyze the following files and organize them according to the rules.
|
|
337
355
|
|
|
338
356
|
Target directory: ${targetDir}
|
|
339
|
-
|
|
357
|
+
${existingFoldersSection}
|
|
340
358
|
${instructions ? `Additional instructions: ${instructions}\n` : ""}
|
|
341
359
|
Files to organize:
|
|
342
360
|
${filesJson}
|