UNPKG

@nanocollective/nanocoder

Version:

A local-first CLI coding agent that brings the power of agentic coding tools like Claude Code and Gemini CLI to local models or controlled APIs like OpenRouter

177 lines 8.91 kB
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime"; import { existsSync } from 'node:fs'; import { join } from 'node:path'; import { Box, Text } from 'ink'; import React from 'react'; import { useTheme } from '../hooks/useTheme.js'; import { formatCronHuman, generateScheduleId, getNextRunTime, loadScheduleRuns, loadSchedules, saveSchedules, validateCron, } from '../schedule/index.js'; function ScheduleMessage({ message, isError, }) { const { colors } = useTheme(); return (_jsx(Box, { marginY: 1, children: _jsx(Text, { color: isError ? colors.error : colors.success, children: message }) })); } function ScheduleListDisplay({ schedules }) { const { colors } = useTheme(); if (schedules.length === 0) { return (_jsx(Box, { marginY: 1, children: _jsx(Text, { color: colors.secondary, children: "No schedules configured. Use /schedule create name && /schedule add \"cron\" name" }) })); } return (_jsxs(Box, { flexDirection: "column", marginY: 1, children: [_jsx(Text, { bold: true, color: colors.primary, children: "Schedules" }), schedules.map(schedule => { const nextRun = getNextRunTime(schedule.cron); const humanCron = formatCronHuman(schedule.cron); return (_jsxs(Box, { flexDirection: "column", marginTop: 1, children: [_jsxs(Text, { children: [_jsx(Text, { color: colors.info, bold: true, children: schedule.id }), _jsx(Text, { color: colors.secondary, children: " \u2014 " }), _jsx(Text, { color: colors.primary, children: schedule.command })] }), _jsxs(Text, { color: colors.secondary, children: [' ', "cron: ", schedule.cron, " (", humanCron, ")"] }), _jsxs(Text, { color: colors.secondary, children: [' ', "enabled: ", schedule.enabled ? 'yes' : 'no'] }), nextRun && (_jsxs(Text, { color: colors.secondary, children: [' ', "next run: ", nextRun.toLocaleString()] })), schedule.lastRunAt && (_jsxs(Text, { color: colors.secondary, children: [' ', "last run: ", new Date(schedule.lastRunAt).toLocaleString()] }))] }, schedule.id)); })] })); } export const scheduleCommand = { name: 'schedule', description: 'Manage scheduled jobs', handler: async (args) => { const subcommand = args[0]?.toLowerCase(); // No subcommand or "list" — show all schedules if (!subcommand || subcommand === 'list') { const schedules = await loadSchedules(); return React.createElement(ScheduleListDisplay, { key: `schedule-list-${Date.now()}`, schedules, }); } // Add a new schedule if (subcommand === 'add') { const rest = args.slice(1).join(' '); // Parse: "cron expression" command.md (or just command) const cronMatch = rest.match(/^"([^"]+)"\s+(.+)$/); if (!cronMatch) { return React.createElement(ScheduleMessage, { key: `schedule-error-${Date.now()}`, message: 'Usage: /schedule add "cron expression" command\nExample: /schedule add "0 9 * * MON" deps-update', isError: true, }); } const cronExpr = cronMatch[1] ?? ''; let commandFile = cronMatch[2]?.trim() ?? ''; // Infer .md extension if not provided if (!commandFile.endsWith('.md')) { commandFile = `${commandFile}.md`; } // Validate cron const cronError = validateCron(cronExpr); if (cronError) { return React.createElement(ScheduleMessage, { key: `schedule-error-${Date.now()}`, message: `Invalid cron expression: ${cronError}`, isError: true, }); } // Check schedule file exists const commandPath = join(process.cwd(), '.nanocoder', 'schedules', commandFile); if (!existsSync(commandPath)) { return React.createElement(ScheduleMessage, { key: `schedule-error-${Date.now()}`, message: `Schedule file not found: .nanocoder/schedules/${commandFile}\nCreate one with: /schedule create ${commandFile.replace(/\.md$/, '')}`, isError: true, }); } const schedule = { id: generateScheduleId(), cron: cronExpr, command: commandFile, enabled: true, createdAt: new Date().toISOString(), lastRunAt: null, }; const schedules = await loadSchedules(); schedules.push(schedule); await saveSchedules(schedules); const humanCron = formatCronHuman(cronExpr); return React.createElement(ScheduleMessage, { key: `schedule-added-${Date.now()}`, message: `Schedule added: ${schedule.id}${commandFile} (${humanCron})`, }); } // Create — intercepted in app-util.ts to trigger AI assistance if (subcommand === 'create') { return React.createElement(ScheduleMessage, { key: `schedule-error-${Date.now()}`, message: 'Usage: /schedule create <name>\nExample: /schedule create deps-update', isError: true, }); } // Remove a schedule if (subcommand === 'remove' || subcommand === 'rm') { const scheduleId = args[1]; if (!scheduleId) { return React.createElement(ScheduleMessage, { key: `schedule-error-${Date.now()}`, message: 'Usage: /schedule remove <id>', isError: true, }); } const schedules = await loadSchedules(); const index = schedules.findIndex(s => s.id === scheduleId); if (index === -1) { return React.createElement(ScheduleMessage, { key: `schedule-error-${Date.now()}`, message: `Schedule not found: ${scheduleId}`, isError: true, }); } const removed = schedules.splice(index, 1)[0]; await saveSchedules(schedules); return React.createElement(ScheduleMessage, { key: `schedule-removed-${Date.now()}`, message: `Schedule removed: ${removed?.id}${removed?.command}`, }); } // Start scheduler mode — this is handled specially in app-util.ts if (subcommand === 'start') { // This case is intercepted before reaching the command handler // If we get here, something went wrong return React.createElement(ScheduleMessage, { key: `schedule-error-${Date.now()}`, message: 'Scheduler mode could not be started.', isError: true, }); } // Show logs if (subcommand === 'logs') { const scheduleId = args[1]; const runs = await loadScheduleRuns(); const filtered = scheduleId ? runs.filter(r => r.scheduleId === scheduleId) : runs; if (filtered.length === 0) { return React.createElement(ScheduleMessage, { key: `schedule-logs-${Date.now()}`, message: scheduleId ? `No runs found for schedule ${scheduleId}` : 'No schedule runs recorded yet.', }); } const logLines = filtered .slice(-20) .reverse() .map(r => { const start = new Date(r.startedAt).toLocaleString(); const statusIcon = r.status === 'success' ? '[ok]' : r.status === 'error' ? '[err]' : '[...]'; const duration = r.completedAt ? `${Math.round((new Date(r.completedAt).getTime() - new Date(r.startedAt).getTime()) / 1000)}s` : 'running'; return `${statusIcon} ${r.command}${start} (${duration})${r.error ? ` — ${r.error}` : ''}`; }) .join('\n'); return React.createElement(ScheduleMessage, { key: `schedule-logs-${Date.now()}`, message: logLines, }); } // Unknown subcommand return React.createElement(ScheduleMessage, { key: `schedule-error-${Date.now()}`, message: 'Unknown subcommand. Available: create, add, list, remove, start, logs', isError: true, }); }, }; //# sourceMappingURL=schedule.js.map