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,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
|
+
}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Profile management for tidyf
|
|
3
|
+
*
|
|
4
|
+
* Handles CRUD operations for profiles stored in ~/.tidy/profiles/
|
|
5
|
+
* Each profile is a directory containing settings.json and optionally rules.md
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
existsSync,
|
|
10
|
+
mkdirSync,
|
|
11
|
+
readFileSync,
|
|
12
|
+
readdirSync,
|
|
13
|
+
rmSync,
|
|
14
|
+
writeFileSync,
|
|
15
|
+
} from "fs";
|
|
16
|
+
import { join } from "path";
|
|
17
|
+
import { homedir } from "os";
|
|
18
|
+
import type { Profile, ProfileMetadata, ProfileExport } from "../types/profile.ts";
|
|
19
|
+
import type { TidyConfig } from "../types/config.ts";
|
|
20
|
+
import { getPreset } from "./presets.ts";
|
|
21
|
+
|
|
22
|
+
const CONFIG_DIR = ".tidy";
|
|
23
|
+
const PROFILES_DIR = "profiles";
|
|
24
|
+
const SETTINGS_FILE = "settings.json";
|
|
25
|
+
const RULES_FILE = "rules.md";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the profiles directory path
|
|
29
|
+
*/
|
|
30
|
+
export function getProfilesDir(): string {
|
|
31
|
+
return join(homedir(), CONFIG_DIR, PROFILES_DIR);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get a specific profile's directory path
|
|
36
|
+
*/
|
|
37
|
+
export function getProfileDir(name: string): string {
|
|
38
|
+
return join(getProfilesDir(), name);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get the path to a profile's settings.json
|
|
43
|
+
*/
|
|
44
|
+
export function getProfileConfigPath(name: string): string {
|
|
45
|
+
return join(getProfileDir(name), SETTINGS_FILE);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Get the path to a profile's rules.md
|
|
50
|
+
*/
|
|
51
|
+
export function getProfileRulesPath(name: string): string {
|
|
52
|
+
return join(getProfileDir(name), RULES_FILE);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Ensure profiles directory exists
|
|
57
|
+
*/
|
|
58
|
+
export function ensureProfilesDir(): void {
|
|
59
|
+
const profilesDir = getProfilesDir();
|
|
60
|
+
if (!existsSync(profilesDir)) {
|
|
61
|
+
mkdirSync(profilesDir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Validate a profile name
|
|
67
|
+
* @returns Object with valid boolean and optional error message
|
|
68
|
+
*/
|
|
69
|
+
export function validateProfileName(name: string): { valid: boolean; error?: string } {
|
|
70
|
+
// Must be non-empty and max 50 characters
|
|
71
|
+
if (!name || name.length === 0) {
|
|
72
|
+
return { valid: false, error: "Profile name cannot be empty" };
|
|
73
|
+
}
|
|
74
|
+
if (name.length > 50) {
|
|
75
|
+
return { valid: false, error: "Profile name must be 50 characters or less" };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Alphanumeric, hyphens, underscores only
|
|
79
|
+
if (!/^[a-zA-Z0-9_-]+$/.test(name)) {
|
|
80
|
+
return {
|
|
81
|
+
valid: false,
|
|
82
|
+
error: "Profile name can only contain letters, numbers, hyphens, and underscores",
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Reserved names
|
|
87
|
+
const reserved = ["default", "global", "local", "none", "profiles"];
|
|
88
|
+
if (reserved.includes(name.toLowerCase())) {
|
|
89
|
+
return { valid: false, error: `"${name}" is a reserved name` };
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return { valid: true };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Check if a profile exists
|
|
97
|
+
*/
|
|
98
|
+
export function profileExists(name: string): boolean {
|
|
99
|
+
return existsSync(getProfileDir(name));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* List all profiles with metadata
|
|
104
|
+
*/
|
|
105
|
+
export function listProfiles(): ProfileMetadata[] {
|
|
106
|
+
const profilesDir = getProfilesDir();
|
|
107
|
+
|
|
108
|
+
if (!existsSync(profilesDir)) {
|
|
109
|
+
return [];
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const profiles: ProfileMetadata[] = [];
|
|
113
|
+
|
|
114
|
+
try {
|
|
115
|
+
const entries = readdirSync(profilesDir, { withFileTypes: true });
|
|
116
|
+
|
|
117
|
+
for (const entry of entries) {
|
|
118
|
+
if (!entry.isDirectory()) continue;
|
|
119
|
+
if (entry.name.startsWith(".")) continue;
|
|
120
|
+
|
|
121
|
+
const profileConfigPath = getProfileConfigPath(entry.name);
|
|
122
|
+
const profileRulesPath = getProfileRulesPath(entry.name);
|
|
123
|
+
|
|
124
|
+
// Must have settings.json to be a valid profile
|
|
125
|
+
if (!existsSync(profileConfigPath)) continue;
|
|
126
|
+
|
|
127
|
+
try {
|
|
128
|
+
const content = readFileSync(profileConfigPath, "utf-8");
|
|
129
|
+
const profile: Profile = JSON.parse(content);
|
|
130
|
+
|
|
131
|
+
profiles.push({
|
|
132
|
+
name: entry.name,
|
|
133
|
+
description: profile.description,
|
|
134
|
+
createdAt: profile.createdAt,
|
|
135
|
+
modifiedAt: profile.modifiedAt,
|
|
136
|
+
hasCustomRules: existsSync(profileRulesPath),
|
|
137
|
+
});
|
|
138
|
+
} catch {
|
|
139
|
+
// Skip invalid profiles
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
} catch {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Sort alphabetically by name
|
|
147
|
+
profiles.sort((a, b) => a.name.localeCompare(b.name));
|
|
148
|
+
|
|
149
|
+
return profiles;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Read a profile's configuration
|
|
154
|
+
*/
|
|
155
|
+
export function readProfile(name: string): Profile | null {
|
|
156
|
+
const configPath = getProfileConfigPath(name);
|
|
157
|
+
|
|
158
|
+
if (!existsSync(configPath)) {
|
|
159
|
+
return null;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
try {
|
|
163
|
+
const content = readFileSync(configPath, "utf-8");
|
|
164
|
+
const profile: Profile = JSON.parse(content);
|
|
165
|
+
// Ensure name matches directory name
|
|
166
|
+
profile.name = name;
|
|
167
|
+
return profile;
|
|
168
|
+
} catch {
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Read a profile's custom rules (if any)
|
|
175
|
+
*/
|
|
176
|
+
export function readProfileRules(name: string): string | null {
|
|
177
|
+
const rulesPath = getProfileRulesPath(name);
|
|
178
|
+
|
|
179
|
+
if (!existsSync(rulesPath)) {
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
try {
|
|
184
|
+
return readFileSync(rulesPath, "utf-8");
|
|
185
|
+
} catch {
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Write a profile's configuration
|
|
192
|
+
*/
|
|
193
|
+
export function writeProfile(name: string, profile: Profile): void {
|
|
194
|
+
ensureProfilesDir();
|
|
195
|
+
|
|
196
|
+
const profileDir = getProfileDir(name);
|
|
197
|
+
if (!existsSync(profileDir)) {
|
|
198
|
+
mkdirSync(profileDir, { recursive: true });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Update metadata
|
|
202
|
+
const now = new Date().toISOString();
|
|
203
|
+
if (!profile.createdAt) {
|
|
204
|
+
profile.createdAt = now;
|
|
205
|
+
}
|
|
206
|
+
profile.modifiedAt = now;
|
|
207
|
+
profile.name = name;
|
|
208
|
+
|
|
209
|
+
const configPath = getProfileConfigPath(name);
|
|
210
|
+
writeFileSync(configPath, JSON.stringify(profile, null, 2), "utf-8");
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Write custom rules for a profile
|
|
215
|
+
*/
|
|
216
|
+
export function writeProfileRules(name: string, rules: string): void {
|
|
217
|
+
ensureProfilesDir();
|
|
218
|
+
|
|
219
|
+
const profileDir = getProfileDir(name);
|
|
220
|
+
if (!existsSync(profileDir)) {
|
|
221
|
+
mkdirSync(profileDir, { recursive: true });
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const rulesPath = getProfileRulesPath(name);
|
|
225
|
+
writeFileSync(rulesPath, rules, "utf-8");
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Delete a profile
|
|
230
|
+
*/
|
|
231
|
+
export function deleteProfile(name: string): void {
|
|
232
|
+
const profileDir = getProfileDir(name);
|
|
233
|
+
|
|
234
|
+
if (existsSync(profileDir)) {
|
|
235
|
+
rmSync(profileDir, { recursive: true, force: true });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/**
|
|
240
|
+
* Copy a profile to a new name
|
|
241
|
+
*/
|
|
242
|
+
export function copyProfile(source: string, destination: string): void {
|
|
243
|
+
const sourceProfile = readProfile(source);
|
|
244
|
+
if (!sourceProfile) {
|
|
245
|
+
throw new Error(`Source profile "${source}" not found`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (profileExists(destination)) {
|
|
249
|
+
throw new Error(`Destination profile "${destination}" already exists`);
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Copy config with new name and reset timestamps
|
|
253
|
+
const newProfile: Profile = {
|
|
254
|
+
...sourceProfile,
|
|
255
|
+
name: destination,
|
|
256
|
+
createdAt: new Date().toISOString(),
|
|
257
|
+
modifiedAt: new Date().toISOString(),
|
|
258
|
+
};
|
|
259
|
+
|
|
260
|
+
writeProfile(destination, newProfile);
|
|
261
|
+
|
|
262
|
+
// Copy rules if they exist
|
|
263
|
+
const sourceRules = readProfileRules(source);
|
|
264
|
+
if (sourceRules) {
|
|
265
|
+
writeProfileRules(destination, sourceRules);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Export a profile for sharing
|
|
271
|
+
*/
|
|
272
|
+
export function exportProfile(name: string): ProfileExport {
|
|
273
|
+
const profile = readProfile(name);
|
|
274
|
+
if (!profile) {
|
|
275
|
+
throw new Error(`Profile "${name}" not found`);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const rules = readProfileRules(name);
|
|
279
|
+
|
|
280
|
+
return {
|
|
281
|
+
version: "1.0",
|
|
282
|
+
exportedAt: new Date().toISOString(),
|
|
283
|
+
profile,
|
|
284
|
+
rules: rules || undefined,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/**
|
|
289
|
+
* Import a profile from an export
|
|
290
|
+
* @param data The exported profile data
|
|
291
|
+
* @param overrideName Optional name to use instead of the one in the export
|
|
292
|
+
* @returns The name of the imported profile
|
|
293
|
+
*/
|
|
294
|
+
export function importProfile(data: ProfileExport, overrideName?: string): string {
|
|
295
|
+
const name = overrideName || data.profile.name;
|
|
296
|
+
|
|
297
|
+
const validation = validateProfileName(name);
|
|
298
|
+
if (!validation.valid) {
|
|
299
|
+
throw new Error(validation.error);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (profileExists(name)) {
|
|
303
|
+
throw new Error(`Profile "${name}" already exists`);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Import with updated timestamps
|
|
307
|
+
const profile: Profile = {
|
|
308
|
+
...data.profile,
|
|
309
|
+
name,
|
|
310
|
+
createdAt: new Date().toISOString(),
|
|
311
|
+
modifiedAt: new Date().toISOString(),
|
|
312
|
+
};
|
|
313
|
+
|
|
314
|
+
writeProfile(name, profile);
|
|
315
|
+
|
|
316
|
+
// Import rules if present
|
|
317
|
+
if (data.rules) {
|
|
318
|
+
writeProfileRules(name, data.rules);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
return name;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Get the config fields that should be merged from a profile
|
|
326
|
+
* (excludes metadata fields)
|
|
327
|
+
*/
|
|
328
|
+
export function getProfileConfigFields(profile: Profile): Partial<TidyConfig> {
|
|
329
|
+
const { name, description, createdAt, modifiedAt, ...configFields } = profile;
|
|
330
|
+
return configFields;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Install a built-in preset as a profile
|
|
335
|
+
* @param presetName The name of the preset (developer, creative, student, downloads)
|
|
336
|
+
* @param profileName Optional custom name for the installed profile
|
|
337
|
+
* @returns The name of the installed profile
|
|
338
|
+
*/
|
|
339
|
+
export function installPreset(presetName: string, profileName?: string): string {
|
|
340
|
+
const preset = getPreset(presetName);
|
|
341
|
+
if (!preset) {
|
|
342
|
+
throw new Error(`Preset "${presetName}" not found. Available: developer, creative, student, downloads`);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
const name = profileName || presetName;
|
|
346
|
+
|
|
347
|
+
const validation = validateProfileName(name);
|
|
348
|
+
if (!validation.valid) {
|
|
349
|
+
throw new Error(validation.error);
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (profileExists(name)) {
|
|
353
|
+
throw new Error(`Profile "${name}" already exists. Use a different name or delete the existing profile.`);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const profile: Profile = {
|
|
357
|
+
...preset.profile,
|
|
358
|
+
name,
|
|
359
|
+
createdAt: new Date().toISOString(),
|
|
360
|
+
modifiedAt: new Date().toISOString(),
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
writeProfile(name, profile);
|
|
364
|
+
writeProfileRules(name, preset.rules);
|
|
365
|
+
|
|
366
|
+
return name;
|
|
367
|
+
}
|