import * as p from '@clack/prompts'; import chalk from 'chalk'; import ora from 'ora'; import { supabase, getCachedValue } from '../lib/supabase.js'; import { getActiveWorkspace, viewHeatmap } from './tasks.js'; interface DisplayTask { task: any; displayId: string; isSubtask: boolean; } function parseCommaSeparatedList(input: string): string[] { const result: string[] = []; let current = ''; let inQuotes = false; let quoteChar = ''; for (let i = 0; i < input.length; i++) { const char = input[i]; if (inQuotes) { if (char === quoteChar) { inQuotes = false; } else { current += char; } } else { if (char === '"' || char === "'") { inQuotes = true; quoteChar = char; } else if (char === ',') { result.push(current.trim()); current = ''; } else { current += char; } } } if (current.trim() || result.length > 0 || inQuotes) { result.push(current.trim()); } return result.map(s => s.trim()).filter(s => s.length > 0); } export async function launchInteractiveMode() { console.clear(); console.log(chalk.bold.cyan(`╭───────────────────────────────────────────╮`)); console.log(chalk.bold.cyan(`│ VeloWork Terminal │`)); console.log(chalk.bold.cyan(`╰───────────────────────────────────────────╯`)); console.log(chalk.gray(`Type "help" to see available commands, "exit" to quit.\n`)); // Check login const { data: { user }, error: userError } = await supabase.auth.getUser(); if (userError || !user) { console.log(chalk.red('Error: You must be logged in to launch the terminal.')); console.log(`Run ${chalk.cyan('velowork login')} first to authenticate.`); return; } const workspace = await getActiveWorkspace(user); if (!workspace) { console.log(chalk.red('Error: No active workspace found.')); return; } console.log(chalk.green(`Connected to Workspace: ${chalk.bold(workspace.name)}\n`)); let activeChecklist: any = null; let sortedChecklists: any[] = []; let displayTasks: DisplayTask[] = []; // Initial load of checklists const loadChecklists = async () => { const { data, error } = await supabase .from('checklists') .select('*, tasks(id)') .eq('workspace_id', workspace.id) .eq('user_id', user.id); if (error) { console.log(chalk.red(`Error loading checklists: ${error.message}`)); return []; } // Sort by created_at ascending return (data || []).sort( (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime() ); }; const loadTasks = async (checklistId: string) => { const { data, error } = await supabase .from('tasks') .select('*') .eq('checklist_id', checklistId); if (error) { console.log(chalk.red(`Error loading tasks: ${error.message}`)); return []; } // Sort by created_at ascending const sorted = (data || []).sort( (a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime() ); // Build hierarchical display mapping const parents = sorted.filter(t => !t.parent_id); const mapped: DisplayTask[] = []; parents.forEach((pt, pIndex) => { const parentIdStr = `${pIndex + 1}`; mapped.push({ task: pt, displayId: parentIdStr, isSubtask: false }); const children = sorted.filter(t => t.parent_id === pt.id); children.forEach((ct, cIndex) => { mapped.push({ task: ct, displayId: `${parentIdStr}.${cIndex + 1}`, isSubtask: true }); }); }); return mapped; }; // Populate initially sortedChecklists = await loadChecklists(); let exit = false; while (!exit) { // Construct Prompt String const pathName = activeChecklist ? `/${activeChecklist.title.replace(/\s+/g, '_')}` : '/'; const promptLabel = `${chalk.bold.green('user@velowork')} ${chalk.bold.cyan(pathName)} $`; const cmdInput = await p.text({ message: promptLabel, placeholder: 'Enter command...', }); if (!cmdInput || p.isCancel(cmdInput)) { exit = true; break; } const inputStr = (cmdInput as string).trim(); if (!inputStr) continue; // Parse commands and arguments const args = inputStr.split(/\s+/); const command = args[0].toLowerCase(); // Extractor for quotes e.g. create "New Checklist" or add "Do task" const getJoinedArgs = (startIndex: number) => { return args.slice(startIndex).join(' ').replace(/^["']|["']$/g, ''); }; const getCommandRemainder = (cmd: string) => { return inputStr.substring(cmd.length).trim(); }; try { if (command === 'exit' || command === 'quit') { exit = true; break; } if (command === 'clear') { console.clear(); continue; } if (command === 'help') { showHelp(!!activeChecklist); continue; } if (command === 'neofetch' || command === 'fetch') { const { showFetch } = await import('./info.js'); showFetch(); continue; } // ROOT DIRECTORY CONTEXT COMMANDS if (!activeChecklist) { switch (command) { case 'ls': { sortedChecklists = await loadChecklists(); if (sortedChecklists.length === 0) { console.log(chalk.yellow('No checklists found. Use `create "Name"` to make one.')); } else { const daily = sortedChecklists.map((c, i) => ({ c, id: i + 1 })).filter(x => !x.c.is_reusable); const reusable = sortedChecklists.map((c, i) => ({ c, id: i + 1 })).filter(x => x.c.is_reusable); if (daily.length > 0) { console.log(chalk.bold.cyan('\nDaily Checklists:')); daily.forEach(({ c, id }) => { const taskCount = c.tasks ? c.tasks.length : 0; const dateStr = c.date ? ` (${c.date})` : ''; console.log(` [${id}] ${chalk.green(c.title)}${dateStr} - ${taskCount} tasks`); }); } if (reusable.length > 0) { console.log(chalk.bold.cyan('\nRecurring Checklists:')); reusable.forEach(({ c, id }) => { const taskCount = c.tasks ? c.tasks.length : 0; console.log(` [${id}] ${chalk.green(c.title)} - ${taskCount} tasks`); }); } console.log(''); } break; } case 'create': { const title = getJoinedArgs(1); if (!title) { console.log(chalk.red('Error: Please specify a checklist title. Usage: create "Checklist Name"')); break; } const todayStr = new Date().toISOString().split('T')[0]; const spinner = ora('Creating checklist...').start(); const { data: newC, error } = await supabase .from('checklists') .insert({ title, workspace_id: workspace.id, user_id: user.id, date: todayStr, is_reusable: false, }) .select() .single(); if (error || !newC) { spinner.fail(chalk.red(`Failed to create checklist: ${error?.message}`)); } else { // Reload sortedChecklists = await loadChecklists(); const newId = sortedChecklists.findIndex(c => c.id === newC.id) + 1; spinner.succeed(chalk.green(`Created checklist: "${newC.title}" (ID: ${newId})`)); console.log(chalk.cyan(`Type 'cd ${newId}' to enter this checklist.`)); } break; } case 'cd': { const target = args[1]; if (!target) { console.log(chalk.red('Error: Please specify a checklist ID/index. Usage: cd ')); break; } if (target === '..' || target === '/') { // Already at root break; } const checkIndex = parseInt(target) - 1; if (!isNaN(checkIndex) && checkIndex >= 0 && checkIndex < sortedChecklists.length) { activeChecklist = sortedChecklists[checkIndex]; displayTasks = await loadTasks(activeChecklist.id); console.log(chalk.green(`Entered: ${chalk.bold(activeChecklist.title)}`)); } else { // Try finding by UUID prefix match const match = sortedChecklists.find(c => c.id.startsWith(target) || c.title.toLowerCase() === target.toLowerCase()); if (match) { activeChecklist = match; displayTasks = await loadTasks(activeChecklist.id); console.log(chalk.green(`Entered: ${chalk.bold(activeChecklist.title)}`)); } else { console.log(chalk.red(`Error: Invalid checklist index or ID "${target}". Type "ls" to list them.`)); } } break; } case 'heatmap': { const yearVal = args[1]; await viewHeatmap(yearVal); break; } default: { console.log(chalk.red(`Command not found: "${command}". Type "help" to see available commands.`)); break; } } } // CHECKLIST DIRECTORY CONTEXT COMMANDS else { switch (command) { case 'ls': { displayTasks = await loadTasks(activeChecklist.id); if (displayTasks.length === 0) { console.log(chalk.yellow(`No tasks in "${activeChecklist.title}". Use \`add "Task Name"\` to create one.`)); } else { console.log(chalk.bold.cyan(`\nTasks in ${activeChecklist.title}:`)); displayTasks.forEach((dt) => { const status = dt.task.completed ? chalk.green('[x]') : '[ ]'; const indent = dt.isSubtask ? ' ' : ' '; const displayNum = chalk.cyan(`${dt.displayId}.`); const text = dt.task.completed ? chalk.gray.strikethrough(dt.task.text) : dt.task.text; console.log(`${indent}${displayNum} ${status} ${text}`); }); console.log(''); } break; } case 'add': { const remainder = getCommandRemainder('add'); const tasksList = parseCommaSeparatedList(remainder); if (tasksList.length === 0) { console.log(chalk.red('Error: Please specify task text. Usage: add "Task Name" or add "Task 1", "Task 2"')); break; } const spinner = ora(tasksList.length > 1 ? `Adding ${tasksList.length} tasks...` : 'Adding task...').start(); const inserts = tasksList.map(text => ({ checklist_id: activeChecklist.id, user_id: user.id, text, completed: false, })); const { error } = await supabase .from('tasks') .insert(inserts); if (error) { spinner.fail(chalk.red(`Failed to add task(s): ${error.message}`)); } else { spinner.succeed(chalk.green(tasksList.length > 1 ? `${tasksList.length} tasks added.` : 'Task added.')); displayTasks = await loadTasks(activeChecklist.id); } break; } case 'sub': { const remainder = getCommandRemainder('sub'); const match = remainder.match(/^([^\s,]+)(?:\s*,\s*|\s+)(.+)$/); if (!match) { console.log(chalk.red('Error: Invalid usage. Usage: sub "Subtask Name" (e.g. sub 1 "Buy Milk") or sub "Sub 1", "Sub 2"')); break; } const parentDisplayId = match[1]; const subtaskTextRemainder = match[2]; const subtasksList = parseCommaSeparatedList(subtaskTextRemainder); if (subtasksList.length === 0) { console.log(chalk.red('Error: Please specify subtask text. Usage: sub "Subtask Name" or sub "Sub 1", "Sub 2"')); break; } const parentTarget = displayTasks.find(dt => dt.displayId === parentDisplayId); if (!parentTarget) { console.log(chalk.red(`Error: Task ID "${parentDisplayId}" not found. Type "ls" to view tasks.`)); break; } if (parentTarget.isSubtask) { console.log(chalk.red('Error: Cannot create a subtask under another subtask. Use a main task ID (e.g. 1, 2).')); break; } const spinner = ora(subtasksList.length > 1 ? `Adding ${subtasksList.length} subtasks...` : 'Adding subtask...').start(); const inserts = subtasksList.map(text => ({ checklist_id: activeChecklist.id, user_id: user.id, text, completed: false, parent_id: parentTarget.task.id, })); const { error } = await supabase .from('tasks') .insert(inserts); if (error) { spinner.fail(chalk.red(`Failed to add subtask(s): ${error.message}`)); } else { spinner.succeed(chalk.green(subtasksList.length > 1 ? `${subtasksList.length} subtasks added.` : 'Subtask added.')); displayTasks = await loadTasks(activeChecklist.id); } break; } case 'check': case 'uncheck': { const displayId = args[1]; if (!displayId) { console.log(chalk.red(`Error: Please specify task ID. Usage: ${command} `)); break; } const target = displayTasks.find(dt => dt.displayId === displayId); if (!target) { console.log(chalk.red(`Error: Task ID "${displayId}" not found.`)); break; } const isCompleted = command === 'check'; const spinner = ora(`${isCompleted ? 'Checking' : 'Unchecking'} task...`).start(); const { error } = await supabase .from('tasks') .update({ completed: isCompleted }) .eq('id', target.task.id); if (error) { spinner.fail(chalk.red(`Failed to update task: ${error.message}`)); } else { spinner.succeed(chalk.green(`Task updated.`)); displayTasks = await loadTasks(activeChecklist.id); } break; } case 'rm': { const displayId = args[1]; if (!displayId) { console.log(chalk.red('Error: Please specify task ID. Usage: rm ')); break; } const target = displayTasks.find(dt => dt.displayId === displayId); if (!target) { console.log(chalk.red(`Error: Task ID "${displayId}" not found.`)); break; } const spinner = ora('Deleting task...').start(); const { error } = await supabase .from('tasks') .delete() .eq('id', target.task.id); if (error) { spinner.fail(chalk.red(`Failed to delete task: ${error.message}`)); } else { spinner.succeed(chalk.green('Task deleted.')); displayTasks = await loadTasks(activeChecklist.id); } break; } case 'cd': { const target = args[1]; if (!target) { console.log(chalk.red('Error: Please specify destination. Usage: cd ..')); break; } if (target === '..') { activeChecklist = null; displayTasks = []; sortedChecklists = await loadChecklists(); console.log(chalk.green('Returned to root directory.')); } else { console.log(chalk.red('Error: You are already inside a checklist. Type "cd .." to go back to root first.')); } break; } default: { console.log(chalk.red(`Command not found: "${command}". Type "help" to see available commands.`)); break; } } } } catch (err: any) { console.log(chalk.red(`Unexpected error: ${err.message}`)); } } p.outro(chalk.bold.cyan('Exit terminal. Goodbye!')); } function showHelp(inChecklist: boolean) { console.log('\n' + chalk.bold.cyan('=== Terminal Shell Help ===')); if (!inChecklist) { console.log(`\n${chalk.bold('Navigation & Folders (Root Context)')}`); console.log(` ${chalk.green('ls')} - List all available checklists`); console.log(` ${chalk.green('create ""')} - Create a new checklist`); console.log(` ${chalk.green('cd ')} - Enter a checklist by its folder number`); console.log(` ${chalk.green('heatmap [year]')} - View task completion heatmap (e.g. heatmap 2026)`); } else { console.log(`\n${chalk.bold('Managing Tasks (Checklist Context)')}`); console.log(` ${chalk.green('ls')} - List all tasks and subtasks in this checklist`); console.log(` ${chalk.green('add "",""')} - Add one or more main tasks (comma-separated)`); console.log(` ${chalk.green('sub "",""')} - Add one or more subtasks under a main task ID (comma-separated)`); console.log(` ${chalk.green('check ')} - Mark task or subtask as completed`); console.log(` ${chalk.green('uncheck ')} - Mark task or subtask as incomplete`); console.log(` ${chalk.green('rm ')} - Permanently delete a task or subtask`); console.log(` ${chalk.green('cd ..')} - Exit checklist folder and go back to root`); } console.log(`\n${chalk.bold('General')}`); console.log(` ${chalk.green('clear')} - Clear the terminal screen`); console.log(` ${chalk.green('neofetch')} - Display ASCII art system info`); console.log(` ${chalk.green('logout')} - Sign out and clear stored session`); console.log(` ${chalk.green('exit')} / ${chalk.green('quit')} - Exit interactive terminal`); console.log(`\n${chalk.bold('Uninstall')}`); console.log(` To uninstall the CLI globally: run ${chalk.green('npm uninstall -g velowork-cli')} in your system shell`); console.log(chalk.bold.cyan('===========================\n')); }