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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tidyf",
3
- "version": "1.0.3",
3
+ "version": "1.1.0",
4
4
  "description": "AI-powered file organizer using opencode.ai",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
package/src/cli.ts CHANGED
@@ -9,6 +9,8 @@ import updateNotifier from "simple-update-notifier";
9
9
  import { createRequire } from "module";
10
10
  import { configCommand } from "./commands/config.ts";
11
11
  import { organizeCommand } from "./commands/organize.ts";
12
+ import { profileCommand } from "./commands/profile.ts";
13
+ import { undoCommand } from "./commands/undo.ts";
12
14
  import { watchCommand } from "./commands/watch.ts";
13
15
 
14
16
  const require = createRequire(import.meta.url);
@@ -24,20 +26,6 @@ program
24
26
  .description("AI-powered file organizer using opencode.ai")
25
27
  .version(pkg.version);
26
28
 
27
- // Default command - organize files
28
- program
29
- .argument("[path]", "Directory to organize (default: ~/Downloads)")
30
- .option("-d, --dry-run", "Preview changes without moving files")
31
- .option("-y, --yes", "Skip confirmation prompts and apply all")
32
- .option("-r, --recursive", "Scan subdirectories")
33
- .option("--depth <n>", "Max subdirectory depth to scan", "1")
34
- .option("-s, --source <path>", "Source directory to organize")
35
- .option("-t, --target <path>", "Target directory for organized files")
36
- .option("-m, --model <id>", "Override model (provider/model)")
37
- .action(async (path, options) => {
38
- await organizeCommand({ path: path || options.source, ...options });
39
- });
40
-
41
29
  // Watch command - monitor folders for new files
42
30
  program
43
31
  .command("watch")
@@ -48,10 +36,24 @@ program
48
36
  .option("-a, --auto", "Auto-apply without confirmation")
49
37
  .option("-q, --queue", "Queue files for review instead of auto-apply")
50
38
  .option("-m, --model <id>", "Override model (provider/model)")
39
+ .option("-p, --profile <name>", "Use named profile")
51
40
  .action(async (paths, options) => {
52
41
  await watchCommand({ paths, ...options });
53
42
  });
54
43
 
44
+ // Profile command - manage organization profiles
45
+ program
46
+ .command("profile [action]")
47
+ .alias("pr")
48
+ .description("Manage organization profiles")
49
+ .argument("[name]", "Profile name for action")
50
+ .argument("[extra]", "Extra argument (e.g., destination for copy)")
51
+ .option("-c, --from-current", "Create from current effective config")
52
+ .option("-f, --force", "Skip confirmation prompts")
53
+ .action(async (action, name, extra, options) => {
54
+ await profileCommand({ action, name, args: extra ? [extra] : [], ...options });
55
+ });
56
+
55
57
  // Config command - configure settings
56
58
  program
57
59
  .command("config")
@@ -62,6 +64,32 @@ program
62
64
  await configCommand(options);
63
65
  });
64
66
 
67
+ // Undo command - revert file organization
68
+ program
69
+ .command("undo")
70
+ .alias("u")
71
+ .description("Undo the last file organization operation")
72
+ .action(async () => {
73
+ await undoCommand();
74
+ });
75
+
76
+ // Default command - organize files (must be defined last to not intercept subcommands)
77
+ program
78
+ .argument("[path]", "Directory to organize (default: ~/Downloads)")
79
+ .option("-d, --dry-run", "Preview changes without moving files")
80
+ .option("-y, --yes", "Skip confirmation prompts and apply all")
81
+ .option("-r, --recursive", "Scan subdirectories")
82
+ .option("--depth <n>", "Max subdirectory depth to scan", "1")
83
+ .option("-s, --source <path>", "Source directory to organize")
84
+ .option("-t, --target <path>", "Target directory for organized files")
85
+ .option("-m, --model <id>", "Override model (provider/model)")
86
+ .option("-p, --profile <name>", "Use named profile")
87
+ .option("--json", "Output JSON instead of interactive UI")
88
+ .option("--detect-duplicates", "Detect duplicate files by content hash")
89
+ .action(async (path, options) => {
90
+ await organizeCommand({ path: path || options.source, ...options });
91
+ });
92
+
65
93
  // Handle errors gracefully
66
94
  process.on("unhandledRejection", (error: Error) => {
67
95
  console.error("Error:", error.message);
@@ -21,17 +21,36 @@ import {
21
21
  writeRules,
22
22
  } from "../lib/config.ts";
23
23
  import { cleanup, getAvailableModels } from "../lib/opencode.ts";
24
+ import {
25
+ listProfiles,
26
+ profileExists,
27
+ validateProfileName,
28
+ writeProfile,
29
+ } from "../lib/profiles.ts";
24
30
  import type {
25
31
  ConfigOptions,
26
32
  ModelSelection,
27
33
  TidyConfig,
28
34
  } from "../types/config.ts";
35
+ import type { Profile } from "../types/profile.ts";
29
36
 
30
37
  /**
31
38
  * Main config command
32
39
  */
33
40
  export async function configCommand(options: ConfigOptions): Promise<void> {
34
- p.intro(color.bgCyan(color.black(" tidyf config ")));
41
+ try {
42
+ p.intro(color.bgCyan(color.black(" tidyf config ")));
43
+ } catch (error: any) {
44
+ console.log(color.cyan(" tidyf config "));
45
+ console.log();
46
+ console.log("Unable to display interactive interface.");
47
+ console.log("Your terminal may not support interactive prompts.");
48
+ console.log();
49
+ console.log(`Edit your config directly at: ${getGlobalConfigPath()}`);
50
+ console.log(`Edit your rules at: ${getGlobalRulesPath()}`);
51
+ console.log();
52
+ process.exit(0);
53
+ }
35
54
 
36
55
  // Initialize global config if needed
37
56
  initGlobalConfig();
@@ -95,6 +114,11 @@ export async function configCommand(options: ConfigOptions): Promise<void> {
95
114
  value: "view",
96
115
  label: "View Current Configuration",
97
116
  },
117
+ {
118
+ value: "save_as_profile",
119
+ label: "Save as Profile",
120
+ hint: "Create a new profile from current settings",
121
+ },
98
122
  {
99
123
  value: "reset",
100
124
  label: "Reset to Defaults",
@@ -137,6 +161,9 @@ export async function configCommand(options: ConfigOptions): Promise<void> {
137
161
  case "view":
138
162
  viewConfig(effectiveConfig, scope);
139
163
  break;
164
+ case "save_as_profile":
165
+ await saveAsProfile(effectiveConfig);
166
+ break;
140
167
  case "reset":
141
168
  await resetConfig(configPath, rulesPath, scope);
142
169
  break;
@@ -628,3 +655,45 @@ async function resetConfig(
628
655
 
629
656
  p.log.success("Configuration reset to defaults");
630
657
  }
658
+
659
+ /**
660
+ * Save current configuration as a new profile
661
+ */
662
+ async function saveAsProfile(config: TidyConfig): Promise<void> {
663
+ // Get profile name
664
+ const name = await p.text({
665
+ message: "Profile name:",
666
+ placeholder: "work",
667
+ validate: (value) => {
668
+ const validation = validateProfileName(value);
669
+ if (!validation.valid) return validation.error;
670
+ if (profileExists(value)) return `Profile "${value}" already exists`;
671
+ },
672
+ });
673
+
674
+ if (p.isCancel(name)) return;
675
+
676
+ // Get description
677
+ const description = await p.text({
678
+ message: "Description (optional):",
679
+ placeholder: "e.g., Work documents and projects",
680
+ });
681
+
682
+ // Create profile from current config
683
+ const profile: Profile = {
684
+ name,
685
+ description: p.isCancel(description) ? undefined : description || undefined,
686
+ ...config,
687
+ };
688
+
689
+ writeProfile(name, profile);
690
+
691
+ p.log.success(`Profile "${name}" created!`);
692
+ p.log.message(color.dim(`Use with: tidyf -p ${name}`));
693
+
694
+ // Show existing profiles
695
+ const profiles = listProfiles();
696
+ if (profiles.length > 1) {
697
+ p.log.info(`You now have ${profiles.length} profiles: ${profiles.map((pr) => pr.name).join(", ")}`);
698
+ }
699
+ }