polish-cli
Version:
AI-powered file organization for Obsidian with automatic markdown conversion
557 lines ⢠21.9 kB
JavaScript
import chalk from 'chalk';
import inquirer from 'inquirer';
import * as path from 'path';
import { ProfileManager } from '../../services/ProfileManager.js';
import { ConfigService } from '../../services/ConfigService.js';
export async function profileCommand(action, name, options = {}) {
const profileManager = new ProfileManager();
await profileManager.initialize();
switch (action) {
case 'create':
await createProfile(profileManager, name, options);
break;
case 'list':
await listProfiles(profileManager);
break;
case 'switch':
await switchProfile(profileManager, name);
break;
case 'delete':
await deleteProfile(profileManager, name, options.force);
break;
case 'current':
await showCurrentProfile(profileManager);
break;
case 'clone':
await cloneProfile(profileManager, name, options);
break;
case 'rename':
await renameProfile(profileManager, name);
break;
case 'export':
await exportProfiles(profileManager, name, options);
break;
case 'import':
await importProfiles(profileManager, name, options);
break;
case 'init':
await initializeProfile(profileManager, name, options);
break;
case 'add-source':
await addSourceDirectory(profileManager, name);
break;
case 'remove-source':
await removeSourceDirectory(profileManager, name);
break;
case 'list-sources':
await listSourceDirectories(profileManager);
break;
default:
console.log(chalk.yellow(`Unknown profile action: ${action}`));
console.log(chalk.gray('Available actions:'));
console.log(chalk.gray(' Profile management: create, list, switch, delete, current, clone, rename'));
console.log(chalk.gray(' Import/Export: export, import, init'));
console.log(chalk.gray(' Source directories: add-source, remove-source, list-sources'));
}
}
async function createProfile(profileManager, name, options) {
if (!name) {
const answer = await inquirer.prompt([
{
type: 'input',
name: 'name',
message: 'Profile name:',
validate: (input) => input.length > 0 || 'Profile name is required',
},
]);
name = answer.name;
}
console.log(chalk.bold(`\nšÆ Creating profile: ${name}\n`));
// Get configuration for the new profile
const configService = new ConfigService();
let config = configService.getDefaultConfig();
if (options.vault) {
config.vault.path = options.vault;
}
if (options.originals) {
config.originals.path = options.originals;
}
// Interactive configuration
const answers = await inquirer.prompt([
{
type: 'input',
name: 'description',
message: 'Description (optional):',
default: options.description || '',
},
{
type: 'input',
name: 'vaultPath',
message: 'Obsidian vault path:',
default: config.vault.path,
validate: (input) => input.length > 0 || 'Vault path is required',
},
{
type: 'input',
name: 'originalsPath',
message: 'Original files organization path:',
default: config.originals.path,
},
{
type: 'checkbox',
name: 'sources',
message: 'Select source folders to monitor:',
choices: [
{ name: 'Desktop', value: path.join(process.env.HOME || '', 'Desktop') },
{ name: 'Downloads', value: path.join(process.env.HOME || '', 'Downloads') },
{ name: 'Documents', value: path.join(process.env.HOME || '', 'Documents') },
],
default: config.sources.map(s => s.path),
},
{
type: 'list',
name: 'organizationStyle',
message: 'How should original files be organized?',
choices: [
{ name: 'By file type', value: 'type-based' },
{ name: 'By project/context', value: 'project-based' },
{ name: 'By date', value: 'date-based' },
],
default: config.originals.organizationStyle,
},
{
type: 'list',
name: 'mode',
message: 'Default processing mode:',
choices: [
{ name: 'Claude Code (no API key needed)', value: 'claude-code' },
{ name: 'Claude API (requires API key)', value: 'api' },
{ name: 'Hybrid (API with local fallback)', value: 'hybrid' },
{ name: 'Local only (no AI)', value: 'local' },
],
default: config.api.mode,
},
]);
// Update config with answers
config.vault.path = answers.vaultPath;
config.originals.path = answers.originalsPath;
config.originals.organizationStyle = answers.organizationStyle;
config.sources = answers.sources.map((src) => ({
path: src,
includeSubfolders: false,
}));
config.api.mode = answers.mode;
try {
const profile = await profileManager.createProfile(name, config, answers.description || undefined);
console.log(chalk.green(`\nā Profile '${profile.name}' created successfully!`));
// Ask if they want to switch to this profile
const switchAnswer = await inquirer.prompt([
{
type: 'confirm',
name: 'switch',
message: 'Switch to this profile now?',
default: true,
},
]);
if (switchAnswer.switch) {
await profileManager.setActiveProfile(profile.name);
console.log(chalk.cyan(`ā Switched to profile '${profile.name}'`));
}
}
catch (error) {
console.error(chalk.red('Failed to create profile:'), error instanceof Error ? error.message : error);
}
}
async function listProfiles(profileManager) {
const profiles = await profileManager.listProfiles();
if (profiles.length === 0) {
console.log(chalk.yellow('\nNo profiles found. Run'), chalk.cyan('polish profile create'), chalk.yellow('to create one.'));
return;
}
console.log(chalk.bold('\nš Profiles:\n'));
profiles.forEach(profile => {
const activeIndicator = profile.isActive ? chalk.green('ā') : chalk.gray('ā');
const lastUsed = new Date(profile.lastUsed);
const timeAgo = formatTimeAgo(lastUsed);
console.log(`${activeIndicator} ${chalk.bold(profile.name)}`);
if (profile.description) {
console.log(` ${chalk.gray(profile.description)}`);
}
console.log(` ${chalk.cyan('Vault:')} ${profile.vaultPath}`);
console.log(` ${chalk.cyan('Originals:')} ${profile.originalsPath}`);
console.log(` ${chalk.cyan('Sources:')} ${profile.sourceCount} folder${profile.sourceCount !== 1 ? 's' : ''}`);
console.log(` ${chalk.gray('Last used:')} ${timeAgo}`);
console.log();
});
}
async function switchProfile(profileManager, name) {
if (!name) {
const profiles = await profileManager.listProfiles();
if (profiles.length === 0) {
console.log(chalk.yellow('No profiles available.'));
return;
}
const activeProfile = profiles.find(p => p.isActive)?.name;
const choices = profiles
.filter(p => !p.isActive)
.map(p => ({ name: `${p.name} - ${p.description || 'No description'}`, value: p.name }));
if (choices.length === 0) {
console.log(chalk.yellow('No other profiles to switch to.'));
return;
}
const answer = await inquirer.prompt([
{
type: 'list',
name: 'profile',
message: `Switch from '${activeProfile}' to:`,
choices,
},
]);
name = answer.profile;
}
try {
await profileManager.setActiveProfile(name);
console.log(chalk.green(`ā Switched to profile '${name}'`));
}
catch (error) {
console.error(chalk.red('Failed to switch profile:'), error instanceof Error ? error.message : error);
}
}
async function deleteProfile(profileManager, name, force = false) {
if (!name) {
const profiles = await profileManager.listProfiles();
const choices = profiles
.filter(p => p.name !== 'default')
.map(p => ({ name: `${p.name} - ${p.description || 'No description'}`, value: p.name }));
if (choices.length === 0) {
console.log(chalk.yellow('No profiles available to delete (cannot delete default profile).'));
return;
}
const answer = await inquirer.prompt([
{
type: 'list',
name: 'profile',
message: 'Select profile to delete:',
choices,
},
]);
name = answer.profile;
}
if (!force) {
const confirmation = await inquirer.prompt([
{
type: 'confirm',
name: 'confirm',
message: chalk.red(`Are you sure you want to delete profile '${name}'?`),
default: false,
},
]);
if (!confirmation.confirm) {
console.log(chalk.gray('Delete cancelled.'));
return;
}
}
try {
await profileManager.deleteProfile(name);
console.log(chalk.green(`ā Profile '${name}' deleted successfully`));
}
catch (error) {
console.error(chalk.red('Failed to delete profile:'), error instanceof Error ? error.message : error);
}
}
async function showCurrentProfile(profileManager) {
try {
const activeProfileName = await profileManager.getActiveProfile();
if (!activeProfileName) {
console.log(chalk.yellow('No active profile set.'));
return;
}
const profile = await profileManager.getProfile(activeProfileName);
if (!profile) {
console.log(chalk.red('Active profile not found.'));
return;
}
console.log(chalk.bold(`\nš Current Profile: ${profile.name}\n`));
if (profile.description) {
console.log(chalk.gray(`Description: ${profile.description}`));
}
console.log(chalk.cyan('Vault path:'), profile.config.vault.path);
console.log(chalk.cyan('Originals path:'), profile.config.originals.path);
console.log(chalk.cyan('Organization style:'), profile.config.originals.organizationStyle);
console.log(chalk.cyan('Processing mode:'), profile.config.api.mode);
console.log(chalk.cyan('Source folders:'));
profile.config.sources.forEach(source => {
console.log(` - ${source.path}${source.includeSubfolders ? ' (including subfolders)' : ''}`);
});
const lastUsed = new Date(profile.lastUsed);
console.log(chalk.gray(`\nLast used: ${formatTimeAgo(lastUsed)}`));
}
catch (error) {
console.error(chalk.red('Failed to show current profile:'), error instanceof Error ? error.message : error);
}
}
async function cloneProfile(profileManager, sourceName, options) {
if (!sourceName) {
const profiles = await profileManager.listProfiles();
const choices = profiles.map(p => ({ name: `${p.name} - ${p.description || 'No description'}`, value: p.name }));
const answer = await inquirer.prompt([
{
type: 'list',
name: 'source',
message: 'Select profile to clone:',
choices,
},
]);
sourceName = answer.source;
}
let targetName = options.targetName;
if (!targetName) {
const targetAnswer = await inquirer.prompt([
{
type: 'input',
name: 'target',
message: 'New profile name:',
validate: (input) => input.length > 0 || 'Profile name is required',
},
]);
targetName = targetAnswer.target;
}
const descriptionAnswer = await inquirer.prompt([
{
type: 'input',
name: 'description',
message: 'Description (optional):',
default: options.description || '',
},
]);
try {
const profile = await profileManager.cloneProfile(sourceName, targetName, descriptionAnswer.description || undefined);
console.log(chalk.green(`ā Profile '${profile.name}' cloned from '${sourceName}'`));
}
catch (error) {
console.error(chalk.red('Failed to clone profile:'), error instanceof Error ? error.message : error);
}
}
async function renameProfile(profileManager, oldName) {
if (!oldName) {
const profiles = await profileManager.listProfiles();
const choices = profiles
.filter(p => p.name !== 'default')
.map(p => ({ name: `${p.name} - ${p.description || 'No description'}`, value: p.name }));
const answer = await inquirer.prompt([
{
type: 'list',
name: 'profile',
message: 'Select profile to rename:',
choices,
},
]);
oldName = answer.profile;
}
const newNameAnswer = await inquirer.prompt([
{
type: 'input',
name: 'newName',
message: `New name for '${oldName}':`,
validate: (input) => input.length > 0 || 'Profile name is required',
},
]);
try {
await profileManager.renameProfile(oldName, newNameAnswer.newName);
console.log(chalk.green(`ā Profile renamed from '${oldName}' to '${newNameAnswer.newName}'`));
}
catch (error) {
console.error(chalk.red('Failed to rename profile:'), error instanceof Error ? error.message : error);
}
}
async function exportProfiles(profileManager, filePath, _options) {
if (!filePath) {
const answer = await inquirer.prompt([
{
type: 'input',
name: 'filePath',
message: 'Export file path:',
default: './polish-profiles.json',
},
]);
filePath = answer.filePath;
}
try {
await profileManager.exportProfiles(filePath);
console.log(chalk.green(`ā Profiles exported to ${filePath}`));
}
catch (error) {
console.error(chalk.red('Failed to export profiles:'), error instanceof Error ? error.message : error);
}
}
async function importProfiles(profileManager, filePath, options) {
if (!filePath) {
const answer = await inquirer.prompt([
{
type: 'input',
name: 'filePath',
message: 'Import file path:',
validate: (input) => input.length > 0 || 'File path is required',
},
]);
filePath = answer.filePath;
}
try {
const imported = await profileManager.importProfiles(filePath, options.force);
if (imported.length > 0) {
console.log(chalk.green(`ā Imported ${imported.length} profile(s):`));
imported.forEach(name => console.log(` - ${name}`));
}
else {
console.log(chalk.yellow('No new profiles imported (use --force to overwrite existing profiles)'));
}
}
catch (error) {
console.error(chalk.red('Failed to import profiles:'), error instanceof Error ? error.message : error);
}
}
async function initializeProfile(profileManager, name, options) {
console.log(chalk.bold('\nšÆ Polish Profile Initialization\n'));
const profiles = await profileManager.listProfiles();
if (profiles.length > 0) {
console.log(chalk.yellow('Profiles already exist. Use'), chalk.cyan('polish profile create'), chalk.yellow('to create a new profile.'));
return;
}
// Initialize with default profile
await createProfile(profileManager, name || 'default', options);
}
async function addSourceDirectory(profileManager, sourcePath) {
const activeProfileName = await profileManager.getActiveProfile();
if (!activeProfileName) {
console.log(chalk.red('No active profile found. Please create a profile first.'));
return;
}
const activeProfile = await profileManager.getProfile(activeProfileName);
if (!activeProfile) {
console.log(chalk.red('Active profile not found.'));
return;
}
let newSourcePath = sourcePath;
if (!newSourcePath) {
const answer = await inquirer.prompt([
{
type: 'input',
name: 'path',
message: 'Enter source directory path:',
validate: (input) => {
if (!input.trim())
return 'Path is required';
if (!path.isAbsolute(input))
return 'Path must be absolute';
return true;
},
},
]);
newSourcePath = answer.path;
}
// Check if source already exists
const exists = activeProfile.config.sources.some(source => source.path === newSourcePath);
if (exists) {
console.log(chalk.yellow(`Source directory '${newSourcePath}' is already tracked.`));
return;
}
// Add new source
activeProfile.config.sources.push({
path: newSourcePath,
includeSubfolders: false,
});
try {
await profileManager.updateProfile(activeProfileName, { config: activeProfile.config });
console.log(chalk.green(`ā Added source directory: ${newSourcePath}`));
}
catch (error) {
console.error(chalk.red('Failed to add source directory:'), error instanceof Error ? error.message : error);
}
}
async function removeSourceDirectory(profileManager, sourcePath) {
const activeProfileName = await profileManager.getActiveProfile();
if (!activeProfileName) {
console.log(chalk.red('No active profile found. Please create a profile first.'));
return;
}
const activeProfile = await profileManager.getProfile(activeProfileName);
if (!activeProfile) {
console.log(chalk.red('Active profile not found.'));
return;
}
let targetPath = sourcePath;
if (!targetPath) {
if (activeProfile.config.sources.length === 0) {
console.log(chalk.yellow('No source directories configured.'));
return;
}
const answer = await inquirer.prompt([
{
type: 'list',
name: 'path',
message: 'Select source directory to remove:',
choices: activeProfile.config.sources.map(source => ({
name: source.path,
value: source.path,
})),
},
]);
targetPath = answer.path;
}
// Remove source
const originalLength = activeProfile.config.sources.length;
activeProfile.config.sources = activeProfile.config.sources.filter(source => source.path !== targetPath);
if (activeProfile.config.sources.length === originalLength) {
console.log(chalk.yellow(`Source directory '${targetPath}' not found.`));
return;
}
try {
await profileManager.updateProfile(activeProfileName, { config: activeProfile.config });
console.log(chalk.green(`ā Removed source directory: ${targetPath}`));
}
catch (error) {
console.error(chalk.red('Failed to remove source directory:'), error instanceof Error ? error.message : error);
}
}
async function listSourceDirectories(profileManager) {
const activeProfileName = await profileManager.getActiveProfile();
if (!activeProfileName) {
console.log(chalk.red('No active profile found. Please create a profile first.'));
return;
}
const activeProfile = await profileManager.getProfile(activeProfileName);
if (!activeProfile) {
console.log(chalk.red('Active profile not found.'));
return;
}
console.log(chalk.bold(`\nSource Directories for '${activeProfile.name}':\n`));
if (activeProfile.config.sources.length === 0) {
console.log(chalk.gray('No source directories configured.'));
console.log(chalk.gray('Use'), chalk.cyan('polish profile add-source'), chalk.gray('to add directories.'));
return;
}
activeProfile.config.sources.forEach((source, index) => {
console.log(chalk.cyan(`${index + 1}.`), source.path);
if (source.includeSubfolders) {
console.log(chalk.gray(' āā Includes subfolders'));
}
});
console.log(chalk.gray(`\nTotal: ${activeProfile.config.sources.length} source director${activeProfile.config.sources.length === 1 ? 'y' : 'ies'}`));
}
function formatTimeAgo(date) {
const now = new Date();
const diffMs = now.getTime() - date.getTime();
const diffMinutes = Math.floor(diffMs / (1000 * 60));
const diffHours = Math.floor(diffMs / (1000 * 60 * 60));
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
if (diffMinutes < 1)
return 'just now';
if (diffMinutes < 60)
return `${diffMinutes}m ago`;
if (diffHours < 24)
return `${diffHours}h ago`;
if (diffDays < 7)
return `${diffDays}d ago`;
return date.toLocaleDateString();
}
//# sourceMappingURL=profile.js.map