@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
JavaScript
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