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,630 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config command - configure AI models, folders, and rules
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import * as p from "@clack/prompts";
|
|
6
|
+
import { existsSync } from "fs";
|
|
7
|
+
import color from "picocolors";
|
|
8
|
+
import {
|
|
9
|
+
expandPath,
|
|
10
|
+
getDefaultConfig,
|
|
11
|
+
getDefaultRules,
|
|
12
|
+
getGlobalConfigPath,
|
|
13
|
+
getGlobalRulesPath,
|
|
14
|
+
getLocalConfigPath,
|
|
15
|
+
getLocalRulesPath,
|
|
16
|
+
initGlobalConfig,
|
|
17
|
+
readConfig,
|
|
18
|
+
readRules,
|
|
19
|
+
resolveConfig,
|
|
20
|
+
writeConfig,
|
|
21
|
+
writeRules,
|
|
22
|
+
} from "../lib/config.ts";
|
|
23
|
+
import { cleanup, getAvailableModels } from "../lib/opencode.ts";
|
|
24
|
+
import type {
|
|
25
|
+
ConfigOptions,
|
|
26
|
+
ModelSelection,
|
|
27
|
+
TidyConfig,
|
|
28
|
+
} from "../types/config.ts";
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Main config command
|
|
32
|
+
*/
|
|
33
|
+
export async function configCommand(options: ConfigOptions): Promise<void> {
|
|
34
|
+
p.intro(color.bgCyan(color.black(" tidyf config ")));
|
|
35
|
+
|
|
36
|
+
// Initialize global config if needed
|
|
37
|
+
initGlobalConfig();
|
|
38
|
+
|
|
39
|
+
// Determine scope
|
|
40
|
+
const scope = options.local ? "local" : "global";
|
|
41
|
+
const configPath =
|
|
42
|
+
scope === "global" ? getGlobalConfigPath() : getLocalConfigPath();
|
|
43
|
+
const rulesPath =
|
|
44
|
+
scope === "global" ? getGlobalRulesPath() : getLocalRulesPath();
|
|
45
|
+
|
|
46
|
+
p.log.info(`Configuring ${color.bold(scope)} settings`);
|
|
47
|
+
p.log.message(color.dim(`Config: ${configPath}`));
|
|
48
|
+
|
|
49
|
+
// Main menu loop
|
|
50
|
+
let done = false;
|
|
51
|
+
while (!done) {
|
|
52
|
+
// Re-read config each iteration to show updated values
|
|
53
|
+
const currentConfig = readConfig(configPath);
|
|
54
|
+
const effectiveConfig = resolveConfig();
|
|
55
|
+
|
|
56
|
+
const action = await p.select({
|
|
57
|
+
message: "What would you like to configure?",
|
|
58
|
+
options: [
|
|
59
|
+
{
|
|
60
|
+
value: "model",
|
|
61
|
+
label: "AI Model",
|
|
62
|
+
hint: `Current: ${effectiveConfig.organizer?.provider}/${effectiveConfig.organizer?.model}`,
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
value: "source",
|
|
66
|
+
label: "Default Source Directory",
|
|
67
|
+
hint: effectiveConfig.defaultSource || "Not set",
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
value: "target",
|
|
71
|
+
label: "Default Target Directory",
|
|
72
|
+
hint: effectiveConfig.defaultTarget || "Not set",
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
value: "ignore",
|
|
76
|
+
label: "Ignore Patterns",
|
|
77
|
+
hint: `${effectiveConfig.ignore?.length || 0} patterns`,
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
value: "content",
|
|
81
|
+
label: "Content Reading",
|
|
82
|
+
hint: effectiveConfig.readContent ? "Enabled" : "Disabled",
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
value: "watch",
|
|
86
|
+
label: "Watch Mode",
|
|
87
|
+
hint: effectiveConfig.watchEnabled ? "Enabled" : "Disabled",
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
value: "rules",
|
|
91
|
+
label: "Edit Organization Rules",
|
|
92
|
+
hint: "Customize AI prompts",
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
value: "view",
|
|
96
|
+
label: "View Current Configuration",
|
|
97
|
+
},
|
|
98
|
+
{
|
|
99
|
+
value: "reset",
|
|
100
|
+
label: "Reset to Defaults",
|
|
101
|
+
hint: color.red("Destructive"),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
value: "done",
|
|
105
|
+
label: "Done",
|
|
106
|
+
},
|
|
107
|
+
],
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
if (p.isCancel(action) || action === "done") {
|
|
111
|
+
done = true;
|
|
112
|
+
break;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
switch (action) {
|
|
116
|
+
case "model":
|
|
117
|
+
await configureModel(configPath, currentConfig);
|
|
118
|
+
break;
|
|
119
|
+
case "source":
|
|
120
|
+
await configureSource(configPath, currentConfig);
|
|
121
|
+
break;
|
|
122
|
+
case "target":
|
|
123
|
+
await configureTarget(configPath, currentConfig);
|
|
124
|
+
break;
|
|
125
|
+
case "ignore":
|
|
126
|
+
await configureIgnore(configPath, currentConfig);
|
|
127
|
+
break;
|
|
128
|
+
case "content":
|
|
129
|
+
await configureContent(configPath, currentConfig);
|
|
130
|
+
break;
|
|
131
|
+
case "watch":
|
|
132
|
+
await configureWatch(configPath, currentConfig);
|
|
133
|
+
break;
|
|
134
|
+
case "rules":
|
|
135
|
+
await configureRules(rulesPath);
|
|
136
|
+
break;
|
|
137
|
+
case "view":
|
|
138
|
+
viewConfig(effectiveConfig, scope);
|
|
139
|
+
break;
|
|
140
|
+
case "reset":
|
|
141
|
+
await resetConfig(configPath, rulesPath, scope);
|
|
142
|
+
break;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
p.outro(color.green("Configuration saved!"));
|
|
147
|
+
cleanup();
|
|
148
|
+
process.exit(0);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* Configure AI model
|
|
153
|
+
*/
|
|
154
|
+
/**
|
|
155
|
+
* Configure AI model
|
|
156
|
+
*/
|
|
157
|
+
async function configureModel(
|
|
158
|
+
configPath: string,
|
|
159
|
+
config: TidyConfig,
|
|
160
|
+
): Promise<void> {
|
|
161
|
+
// Try to get available models
|
|
162
|
+
const s = p.spinner();
|
|
163
|
+
s.start("Fetching available models from OpenCode...");
|
|
164
|
+
|
|
165
|
+
let providers: any[] = [];
|
|
166
|
+
try {
|
|
167
|
+
const response = await getAvailableModels();
|
|
168
|
+
if (response.error) {
|
|
169
|
+
throw new Error("Failed to fetch models");
|
|
170
|
+
}
|
|
171
|
+
providers = response.data?.providers || [];
|
|
172
|
+
s.stop(`Fetched ${providers.length} providers`);
|
|
173
|
+
} catch (error: any) {
|
|
174
|
+
s.stop("Failed to fetch models");
|
|
175
|
+
p.log.error(error.message);
|
|
176
|
+
p.log.warn("Using manual entry fallback.");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// Helper to format model display
|
|
180
|
+
const formatModel = (sel?: ModelSelection) => {
|
|
181
|
+
if (!sel) return color.dim("default");
|
|
182
|
+
return `${color.cyan(sel.provider)}/${color.green(sel.model)}`;
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
p.log.info(`Current Model: ${formatModel(config.organizer)}`);
|
|
186
|
+
|
|
187
|
+
let providerId: string;
|
|
188
|
+
let modelName: string;
|
|
189
|
+
|
|
190
|
+
if (providers.length > 0) {
|
|
191
|
+
// Select Provider
|
|
192
|
+
const providerOptions = providers.map((prov) => ({
|
|
193
|
+
value: prov.id,
|
|
194
|
+
label: prov.name || prov.id,
|
|
195
|
+
}));
|
|
196
|
+
|
|
197
|
+
// Add custom option
|
|
198
|
+
providerOptions.push({
|
|
199
|
+
value: "custom",
|
|
200
|
+
label: "Enter custom provider...",
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
const selectedProvider = await p.select({
|
|
204
|
+
message: "Select AI provider:",
|
|
205
|
+
options: providerOptions,
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
if (p.isCancel(selectedProvider)) return;
|
|
209
|
+
|
|
210
|
+
if (selectedProvider === "custom") {
|
|
211
|
+
const customProv = await p.text({
|
|
212
|
+
message: "Enter provider ID:",
|
|
213
|
+
placeholder: "opencode",
|
|
214
|
+
validate: (value) => {
|
|
215
|
+
if (!value) return "Provider ID is required";
|
|
216
|
+
},
|
|
217
|
+
});
|
|
218
|
+
if (p.isCancel(customProv)) return;
|
|
219
|
+
providerId = customProv;
|
|
220
|
+
|
|
221
|
+
const customModel = await p.text({
|
|
222
|
+
message: "Enter model ID:",
|
|
223
|
+
placeholder: "gpt-4o",
|
|
224
|
+
validate: (value) => {
|
|
225
|
+
if (!value) return "Model ID is required";
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
if (p.isCancel(customModel)) return;
|
|
229
|
+
modelName = customModel;
|
|
230
|
+
} else {
|
|
231
|
+
providerId = selectedProvider as string;
|
|
232
|
+
const providerData = providers.find((p) => p.id === providerId);
|
|
233
|
+
|
|
234
|
+
if (!providerData || !providerData.models) {
|
|
235
|
+
p.log.warn(
|
|
236
|
+
`No models found for provider ${providerId}, please enter manually.`,
|
|
237
|
+
);
|
|
238
|
+
const customModel = await p.text({
|
|
239
|
+
message: "Enter model ID:",
|
|
240
|
+
placeholder: "gpt-4o",
|
|
241
|
+
validate: (value) => {
|
|
242
|
+
if (!value) return "Model ID is required";
|
|
243
|
+
},
|
|
244
|
+
});
|
|
245
|
+
if (p.isCancel(customModel)) return;
|
|
246
|
+
modelName = customModel;
|
|
247
|
+
} else {
|
|
248
|
+
// Handle models (can be array or object map)
|
|
249
|
+
let modelIds: string[] = [];
|
|
250
|
+
if (Array.isArray(providerData.models)) {
|
|
251
|
+
modelIds = providerData.models;
|
|
252
|
+
} else if (typeof providerData.models === "object") {
|
|
253
|
+
modelIds = Object.keys(providerData.models);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
if (modelIds.length === 0) {
|
|
257
|
+
p.log.warn(`No models found for provider ${providerId}`);
|
|
258
|
+
const customModel = await p.text({
|
|
259
|
+
message: "Enter model ID:",
|
|
260
|
+
});
|
|
261
|
+
if (p.isCancel(customModel)) return;
|
|
262
|
+
modelName = customModel;
|
|
263
|
+
} else {
|
|
264
|
+
const modelOptions = modelIds.map((model: string) => ({
|
|
265
|
+
value: model,
|
|
266
|
+
label: model,
|
|
267
|
+
}));
|
|
268
|
+
// Add custom option
|
|
269
|
+
modelOptions.push({
|
|
270
|
+
value: "custom",
|
|
271
|
+
label: "Enter custom model...",
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
const selectedModel = await p.select({
|
|
275
|
+
message: "Select model:",
|
|
276
|
+
options: modelOptions,
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
if (p.isCancel(selectedModel)) return;
|
|
280
|
+
|
|
281
|
+
if (selectedModel === "custom") {
|
|
282
|
+
const customModel = await p.text({
|
|
283
|
+
message: "Enter model ID:",
|
|
284
|
+
});
|
|
285
|
+
if (p.isCancel(customModel)) return;
|
|
286
|
+
modelName = customModel;
|
|
287
|
+
} else {
|
|
288
|
+
modelName = selectedModel as string;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
} else {
|
|
294
|
+
// Fallback to manual entry if no providers found
|
|
295
|
+
const manualEntry = await p.text({
|
|
296
|
+
message: "Enter model (format: provider/model):",
|
|
297
|
+
placeholder: "opencode/gpt-4o",
|
|
298
|
+
validate: (value) => {
|
|
299
|
+
if (!value.includes("/")) {
|
|
300
|
+
return "Model must be in format: provider/model";
|
|
301
|
+
}
|
|
302
|
+
},
|
|
303
|
+
});
|
|
304
|
+
|
|
305
|
+
if (p.isCancel(manualEntry)) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const parts = manualEntry.split("/");
|
|
310
|
+
providerId = parts[0];
|
|
311
|
+
modelName = parts.slice(1).join("/");
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
config.organizer = { provider: providerId, model: modelName };
|
|
315
|
+
writeConfig(configPath, config);
|
|
316
|
+
|
|
317
|
+
p.log.success(`Model set to ${color.cyan(providerId + "/" + modelName)}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Configure source directory
|
|
322
|
+
*/
|
|
323
|
+
async function configureSource(
|
|
324
|
+
configPath: string,
|
|
325
|
+
config: TidyConfig,
|
|
326
|
+
): Promise<void> {
|
|
327
|
+
const source = await p.text({
|
|
328
|
+
message: "Enter default source directory to organize files from:",
|
|
329
|
+
initialValue: config.defaultSource || "~/Downloads",
|
|
330
|
+
placeholder: "~/Downloads",
|
|
331
|
+
validate: (value) => {
|
|
332
|
+
if (!value) {
|
|
333
|
+
return "Source directory is required";
|
|
334
|
+
}
|
|
335
|
+
},
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
if (p.isCancel(source)) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
config.defaultSource = source;
|
|
343
|
+
writeConfig(configPath, config);
|
|
344
|
+
|
|
345
|
+
p.log.success(`Source directory set to ${color.cyan(source)}`);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Configure target directory
|
|
350
|
+
*/
|
|
351
|
+
async function configureTarget(
|
|
352
|
+
configPath: string,
|
|
353
|
+
config: TidyConfig,
|
|
354
|
+
): Promise<void> {
|
|
355
|
+
const target = await p.text({
|
|
356
|
+
message: "Enter default target directory for organized files:",
|
|
357
|
+
initialValue: config.defaultTarget || "~/Documents/Organized",
|
|
358
|
+
placeholder: "~/Documents/Organized",
|
|
359
|
+
validate: (value) => {
|
|
360
|
+
if (!value) {
|
|
361
|
+
return "Target directory is required";
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
});
|
|
365
|
+
|
|
366
|
+
if (p.isCancel(target)) {
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
config.defaultTarget = target;
|
|
371
|
+
writeConfig(configPath, config);
|
|
372
|
+
|
|
373
|
+
p.log.success(`Target directory set to ${color.cyan(target)}`);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/**
|
|
377
|
+
* Configure ignore patterns
|
|
378
|
+
*/
|
|
379
|
+
async function configureIgnore(
|
|
380
|
+
configPath: string,
|
|
381
|
+
config: TidyConfig,
|
|
382
|
+
): Promise<void> {
|
|
383
|
+
const currentPatterns = config.ignore || [];
|
|
384
|
+
|
|
385
|
+
p.log.info("Current ignore patterns:");
|
|
386
|
+
if (currentPatterns.length === 0) {
|
|
387
|
+
p.log.message(color.dim(" (none)"));
|
|
388
|
+
} else {
|
|
389
|
+
for (const pattern of currentPatterns) {
|
|
390
|
+
p.log.message(` ${pattern}`);
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
const action = await p.select({
|
|
395
|
+
message: "What would you like to do?",
|
|
396
|
+
options: [
|
|
397
|
+
{ value: "add", label: "Add pattern" },
|
|
398
|
+
{ value: "remove", label: "Remove pattern" },
|
|
399
|
+
{ value: "reset", label: "Reset to defaults" },
|
|
400
|
+
{ value: "back", label: "Back" },
|
|
401
|
+
],
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
if (p.isCancel(action) || action === "back") {
|
|
405
|
+
return;
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
switch (action) {
|
|
409
|
+
case "add": {
|
|
410
|
+
const pattern = await p.text({
|
|
411
|
+
message: "Enter pattern to ignore (e.g., *.tmp, .DS_Store):",
|
|
412
|
+
placeholder: "*.tmp",
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
if (!p.isCancel(pattern) && pattern) {
|
|
416
|
+
config.ignore = [...currentPatterns, pattern];
|
|
417
|
+
writeConfig(configPath, config);
|
|
418
|
+
p.log.success(`Added pattern: ${color.cyan(pattern)}`);
|
|
419
|
+
}
|
|
420
|
+
break;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
case "remove": {
|
|
424
|
+
if (currentPatterns.length === 0) {
|
|
425
|
+
p.log.warn("No patterns to remove");
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
const toRemove = await p.select({
|
|
430
|
+
message: "Select pattern to remove:",
|
|
431
|
+
options: currentPatterns.map((p) => ({ value: p, label: p })),
|
|
432
|
+
});
|
|
433
|
+
|
|
434
|
+
if (!p.isCancel(toRemove)) {
|
|
435
|
+
config.ignore = currentPatterns.filter((p) => p !== toRemove);
|
|
436
|
+
writeConfig(configPath, config);
|
|
437
|
+
p.log.success(`Removed pattern: ${color.cyan(toRemove as string)}`);
|
|
438
|
+
}
|
|
439
|
+
break;
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
case "reset": {
|
|
443
|
+
const defaults = getDefaultConfig();
|
|
444
|
+
config.ignore = defaults.ignore;
|
|
445
|
+
writeConfig(configPath, config);
|
|
446
|
+
p.log.success("Reset ignore patterns to defaults");
|
|
447
|
+
break;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Configure content reading
|
|
454
|
+
*/
|
|
455
|
+
async function configureContent(
|
|
456
|
+
configPath: string,
|
|
457
|
+
config: TidyConfig,
|
|
458
|
+
): Promise<void> {
|
|
459
|
+
const enabled = await p.confirm({
|
|
460
|
+
message:
|
|
461
|
+
"Enable content reading? (Reads text files to help AI categorize better)",
|
|
462
|
+
initialValue: config.readContent ?? false,
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
if (p.isCancel(enabled)) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
config.readContent = enabled;
|
|
470
|
+
|
|
471
|
+
if (enabled) {
|
|
472
|
+
const maxSize = await p.text({
|
|
473
|
+
message: "Maximum file size to read (bytes):",
|
|
474
|
+
initialValue: String(config.maxContentSize || 10240),
|
|
475
|
+
placeholder: "10240",
|
|
476
|
+
validate: (value) => {
|
|
477
|
+
const num = parseInt(value);
|
|
478
|
+
if (isNaN(num) || num <= 0) {
|
|
479
|
+
return "Must be a positive number";
|
|
480
|
+
}
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
|
|
484
|
+
if (!p.isCancel(maxSize)) {
|
|
485
|
+
config.maxContentSize = parseInt(maxSize);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
writeConfig(configPath, config);
|
|
490
|
+
p.log.success(
|
|
491
|
+
`Content reading ${enabled ? color.green("enabled") : color.yellow("disabled")}`,
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
/**
|
|
496
|
+
* Configure watch mode
|
|
497
|
+
*/
|
|
498
|
+
async function configureWatch(
|
|
499
|
+
configPath: string,
|
|
500
|
+
config: TidyConfig,
|
|
501
|
+
): Promise<void> {
|
|
502
|
+
const enabled = await p.confirm({
|
|
503
|
+
message: "Enable watch mode by default? (Auto-organize new files in source directory)",
|
|
504
|
+
initialValue: config.watchEnabled ?? false,
|
|
505
|
+
});
|
|
506
|
+
|
|
507
|
+
if (p.isCancel(enabled)) {
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
config.watchEnabled = enabled;
|
|
512
|
+
writeConfig(configPath, config);
|
|
513
|
+
|
|
514
|
+
p.log.success(
|
|
515
|
+
`Watch mode ${enabled ? color.green("enabled") : color.yellow("disabled")}`,
|
|
516
|
+
);
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Configure rules (open in editor hint)
|
|
521
|
+
*/
|
|
522
|
+
async function configureRules(rulesPath: string): Promise<void> {
|
|
523
|
+
const rules = readRules(rulesPath);
|
|
524
|
+
|
|
525
|
+
if (!rules) {
|
|
526
|
+
const create = await p.confirm({
|
|
527
|
+
message: "No rules file found. Create one with defaults?",
|
|
528
|
+
initialValue: true,
|
|
529
|
+
});
|
|
530
|
+
|
|
531
|
+
if (p.isCancel(create) || !create) {
|
|
532
|
+
return;
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
writeRules(rulesPath, getDefaultRules());
|
|
536
|
+
p.log.success("Created rules file with defaults");
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
p.log.info(`Rules file: ${color.cyan(rulesPath)}`);
|
|
540
|
+
p.log.message(
|
|
541
|
+
color.dim(
|
|
542
|
+
"Edit this markdown file to customize how AI categorizes your files.",
|
|
543
|
+
),
|
|
544
|
+
);
|
|
545
|
+
p.log.message(
|
|
546
|
+
color.dim("You can define categories, special rules, and output format."),
|
|
547
|
+
);
|
|
548
|
+
|
|
549
|
+
// Show first few lines as preview
|
|
550
|
+
const preview = (readRules(rulesPath) || "")
|
|
551
|
+
.split("\n")
|
|
552
|
+
.slice(0, 10)
|
|
553
|
+
.join("\n");
|
|
554
|
+
console.log();
|
|
555
|
+
console.log(color.dim(preview));
|
|
556
|
+
console.log(color.dim("..."));
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* View current configuration
|
|
561
|
+
*/
|
|
562
|
+
function viewConfig(config: TidyConfig, scope: string): void {
|
|
563
|
+
console.log();
|
|
564
|
+
p.log.info(color.bold(`Current ${scope} configuration:`));
|
|
565
|
+
console.log();
|
|
566
|
+
|
|
567
|
+
p.log.message(
|
|
568
|
+
`${color.bold("AI Model:")} ${config.organizer?.provider}/${config.organizer?.model}`,
|
|
569
|
+
);
|
|
570
|
+
p.log.message(
|
|
571
|
+
`${color.bold("Default Source:")} ${config.defaultSource || "(not set)"}`,
|
|
572
|
+
);
|
|
573
|
+
p.log.message(
|
|
574
|
+
`${color.bold("Default Target:")} ${config.defaultTarget || "(not set)"}`,
|
|
575
|
+
);
|
|
576
|
+
p.log.message(
|
|
577
|
+
`${color.bold("Content Reading:")} ${config.readContent ? "Enabled" : "Disabled"}`,
|
|
578
|
+
);
|
|
579
|
+
p.log.message(
|
|
580
|
+
`${color.bold("Watch Mode:")} ${config.watchEnabled ? "Enabled" : "Disabled"}`,
|
|
581
|
+
);
|
|
582
|
+
|
|
583
|
+
if (config.readContent) {
|
|
584
|
+
p.log.message(
|
|
585
|
+
`${color.bold("Max Content Size:")} ${config.maxContentSize} bytes`,
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
console.log();
|
|
590
|
+
p.log.message(color.bold("Ignore Patterns:"));
|
|
591
|
+
for (const pattern of config.ignore || []) {
|
|
592
|
+
p.log.message(` ${pattern}`);
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
if (config.folders && config.folders.length > 0) {
|
|
596
|
+
console.log();
|
|
597
|
+
p.log.message(color.bold("Configured Folders:"));
|
|
598
|
+
for (const folder of config.folders) {
|
|
599
|
+
p.log.message(` Sources: ${folder.sources.join(", ")}`);
|
|
600
|
+
p.log.message(` Target: ${folder.target}`);
|
|
601
|
+
p.log.message(` Watch: ${folder.watch ? "Yes" : "No"}`);
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
console.log();
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* Reset configuration to defaults
|
|
610
|
+
*/
|
|
611
|
+
async function resetConfig(
|
|
612
|
+
configPath: string,
|
|
613
|
+
rulesPath: string,
|
|
614
|
+
scope: string,
|
|
615
|
+
): Promise<void> {
|
|
616
|
+
const confirm = await p.confirm({
|
|
617
|
+
message: `Reset all ${scope} settings to defaults? This cannot be undone.`,
|
|
618
|
+
initialValue: false,
|
|
619
|
+
});
|
|
620
|
+
|
|
621
|
+
if (p.isCancel(confirm) || !confirm) {
|
|
622
|
+
return;
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
const defaults = getDefaultConfig();
|
|
626
|
+
writeConfig(configPath, defaults);
|
|
627
|
+
writeRules(rulesPath, getDefaultRules());
|
|
628
|
+
|
|
629
|
+
p.log.success("Configuration reset to defaults");
|
|
630
|
+
}
|