@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
136 lines • 4.67 kB
JavaScript
import { existsSync } from 'node:fs';
import { join } from 'node:path';
import { Cron } from 'croner';
import { parseCommandFile } from '../custom-commands/parser.js';
import { addScheduleRun, generateScheduleId, loadSchedules, saveSchedules, updateScheduleRun, } from './storage.js';
export class ScheduleRunner {
cronJobs = new Map();
queue = [];
isRunning = false;
isProcessing = false;
callbacks;
constructor(callbacks) {
this.callbacks = callbacks;
}
/**
* Start all scheduled cron jobs
*/
async start() {
if (this.isRunning)
return;
this.isRunning = true;
const schedules = await loadSchedules();
const enabledSchedules = schedules.filter(s => s.enabled);
for (const schedule of enabledSchedules) {
this.registerCronJob(schedule);
}
}
/**
* Stop all cron jobs and clear the queue
*/
stop() {
this.isRunning = false;
for (const [, job] of this.cronJobs) {
job.stop();
}
this.cronJobs.clear();
this.queue = [];
}
/**
* Get the number of active cron jobs
*/
getActiveJobCount() {
return this.cronJobs.size;
}
/**
* Get the current queue length
*/
getQueueLength() {
return this.queue.length;
}
/**
* Check if currently processing a job
*/
getIsProcessing() {
return this.isProcessing;
}
registerCronJob(schedule) {
const job = new Cron(schedule.cron, () => {
this.enqueueJob(schedule);
});
this.cronJobs.set(schedule.id, job);
}
enqueueJob(schedule) {
// Don't duplicate — if this schedule is already queued, skip
if (this.queue.some(s => s.id === schedule.id))
return;
this.queue.push(schedule);
void this.processQueue();
}
async processQueue() {
if (this.isProcessing || this.queue.length === 0)
return;
this.isProcessing = true;
while (this.queue.length > 0 && this.isRunning) {
const schedule = this.queue.shift();
if (schedule) {
await this.executeJob(schedule);
}
}
this.isProcessing = false;
}
async executeJob(schedule) {
const run = {
id: `run-${generateScheduleId()}`,
scheduleId: schedule.id,
command: schedule.command,
startedAt: new Date().toISOString(),
completedAt: null,
status: 'running',
};
await addScheduleRun(run);
this.callbacks.onJobStart(schedule);
try {
// Clear messages for fresh context
await this.callbacks.clearMessages();
// Load the schedule file from .nanocoder/schedules/
const filePath = join(process.cwd(), '.nanocoder', 'schedules', schedule.command);
if (!existsSync(filePath)) {
throw new Error(`Schedule file not found: ${schedule.command}. Ensure it exists in .nanocoder/schedules/`);
}
const parsed = parseCommandFile(filePath);
const prompt = `[Executing scheduled command: ${schedule.command}]\n\n${parsed.content}`;
await this.callbacks.handleMessageSubmit(prompt);
// Wait for the conversation to complete
await this.callbacks.waitForConversationComplete();
// Update run status
run.completedAt = new Date().toISOString();
run.status = 'success';
await updateScheduleRun(run.id, {
completedAt: run.completedAt,
status: 'success',
});
// Update lastRunAt on the schedule
const schedules = await loadSchedules();
const idx = schedules.findIndex(s => s.id === schedule.id);
if (idx !== -1 && schedules[idx]) {
schedules[idx].lastRunAt = run.completedAt;
await saveSchedules(schedules);
}
this.callbacks.onJobComplete(schedule, run);
}
catch (error) {
const errorMsg = error instanceof Error ? error.message : String(error);
run.completedAt = new Date().toISOString();
run.status = 'error';
run.error = errorMsg;
await updateScheduleRun(run.id, {
completedAt: run.completedAt,
status: 'error',
error: errorMsg,
});
this.callbacks.onJobError(schedule, errorMsg);
}
}
}
//# sourceMappingURL=runner.js.map