← back to dortort__claude-code-scheduler

Function bodies 90 total

All specs Real LLM only Function bodies
add function · typescript · L33-L89 (57 LOC)
src/cli/commands/add.ts
export async function add(args: AddArgs): Promise<AddResult> {
  if (!args.name || !args.cron || !args.command || !args.workingDirectory) {
    return {
      success: false,
      configSaved: false,
      osRegistered: false,
      error: 'Missing required arguments: --name, --cron, --command, --working-directory',
    };
  }

  // Ensure executor is installed
  const initResult = await ensureExecutorInstalled();
  if (!initResult.success) {
    return {
      success: false,
      configSaved: false,
      osRegistered: false,
      error: `Failed to install executor: ${initResult.error}`,
    };
  }

  const task = createTask({
    name: args.name,
    description: args.description,
    trigger: { type: 'cron', expression: args.cron, timezone: 'local' },
    execution: {
      command: args.command,
      workingDirectory: args.workingDirectory,
      timeout: args.timeout ?? 300,
      skipPermissions: args.skipPermissions ?? false,
    },
  });

  const configPath = getGlobalSche
getBinDir function · typescript · L18-L20 (3 LOC)
src/cli/commands/init.ts
function getBinDir(): string {
  return path.join(os.homedir(), '.claude', 'bin');
}
getExecutorPath function · typescript · L22-L24 (3 LOC)
src/cli/commands/init.ts
export function getExecutorPath(): string {
  return path.join(getBinDir(), 'claude-scheduler-executor.js');
}
getShimPath function · typescript · L26-L28 (3 LOC)
src/cli/commands/init.ts
export function getShimPath(): string {
  return path.join(getBinDir(), 'claude-scheduler-run');
}
getExecutorSourcePath function · typescript · L34-L38 (5 LOC)
src/cli/commands/init.ts
function getExecutorSourcePath(): string {
  // This file is at src/cli/commands/init.ts or dist/cli/commands/init.js
  // Executor is at src/cli/executor.ts or dist/cli/executor.js
  return path.resolve(path.dirname(new URL(import.meta.url).pathname), '..', 'executor.js');
}
init function · typescript · L53-L78 (26 LOC)
src/cli/commands/init.ts
export async function init(): Promise<InitResult> {
  const binDir = getBinDir();
  const executorDest = getExecutorPath();
  const shimDest = getShimPath();

  try {
    await fs.mkdir(binDir, { recursive: true });

    // Copy executor source to stable path
    const executorSource = getExecutorSourcePath();
    await fs.copyFile(executorSource, executorDest);

    // Write bash shim
    const shimContent = SHIM_TEMPLATE.replace('{{EXECUTOR_PATH}}', executorDest);
    await fs.writeFile(shimDest, shimContent, { mode: 0o755 });

    return { success: true, executorPath: executorDest, shimPath: shimDest };
  } catch (err) {
    return {
      success: false,
      executorPath: executorDest,
      shimPath: shimDest,
      error: (err as Error).message,
    };
  }
}
isExecutorInstalled function · typescript · L83-L91 (9 LOC)
src/cli/commands/init.ts
export async function isExecutorInstalled(): Promise<boolean> {
  try {
    await fs.access(getExecutorPath());
    await fs.access(getShimPath());
    return true;
  } catch {
    return false;
  }
}
Same scanner, your repo: https://repobility.com — Repobility
ensureExecutorInstalled function · typescript · L96-L101 (6 LOC)
src/cli/commands/init.ts
export async function ensureExecutorInstalled(): Promise<InitResult> {
  if (await isExecutorInstalled()) {
    return { success: true, executorPath: getExecutorPath(), shimPath: getShimPath() };
  }
  return init();
}
migrate function · typescript · L20-L73 (54 LOC)
src/cli/commands/migrate.ts
export async function migrate(): Promise<MigrateResult> {
  const initResult = await ensureExecutorInstalled();
  if (!initResult.success) {
    return {
      success: false,
      migrated: [],
      skipped: [],
      removedScripts: [],
      errors: [{ taskId: '*', error: `Failed to install executor: ${initResult.error}` }],
    };
  }

  const configPath = getGlobalSchedulesPath();
  const config = await loadConfig(configPath);
  const shimPath = getShimPath();
  const logsDir = getLogsDir();

  const migrated: string[] = [];
  const skipped: string[] = [];
  const removedScripts: string[] = [];
  const errors: Array<{ taskId: string; error: string }> = [];

  for (const task of config.tasks) {
    if (!task.enabled) {
      skipped.push(task.id);
      continue;
    }

    // Re-register with OS scheduler pointing to shared executor
    try {
      await registerTask(task, shimPath);
      migrated.push(task.id);
    } catch (err) {
      errors.push({ taskId: task.id, error: (e
remove function · typescript · L27-L75 (49 LOC)
src/cli/commands/remove.ts
export async function remove(args: RemoveArgs): Promise<RemoveResult> {
  if (!args.id) {
    return {
      success: false,
      configSaved: false,
      osUnregistered: false,
      error: 'Missing required argument: --id',
    };
  }

  const configPath = getGlobalSchedulesPath();
  const config = await loadConfig(configPath);
  const task = findTask(config, args.id);

  if (!task) {
    return {
      success: false,
      configSaved: false,
      osUnregistered: false,
      error: `Task not found: ${args.id}`,
    };
  }

  // Remove from config
  const updated = removeTask(config, task.id);
  await saveConfig(configPath, updated);

  // Unregister from OS
  try {
    await unregisterTask(task.id);
  } catch (err) {
    return {
      success: false,
      taskId: task.id,
      taskName: task.name,
      configSaved: true,
      osUnregistered: false,
      error: (err as Error).message,
    };
  }

  return {
    success: true,
    taskId: task.id,
    taskName: task.name,
 
sync function · typescript · L26-L28 (3 LOC)
src/cli/commands/sync.ts
export async function sync(
  register: (task: ScheduledTask, shimPath: string) => Promise<void>,
  options?: { taskId?: string; configPath?: string },
update function · typescript · L34-L105 (72 LOC)
src/cli/commands/update.ts
export async function update(args: UpdateArgs): Promise<UpdateResult> {
  if (!args.id) {
    return {
      success: false,
      configSaved: false,
      osReregistered: false,
      error: 'Missing required argument: --id',
    };
  }

  const configPath = getGlobalSchedulesPath();
  const config = await loadConfig(configPath);
  const existing = findTask(config, args.id);

  if (!existing) {
    return {
      success: false,
      configSaved: false,
      osReregistered: false,
      error: `Task not found: ${args.id}`,
    };
  }

  // Build updates
  const updates: Partial<Pick<ScheduledTask, 'name' | 'description' | 'enabled' | 'trigger' | 'execution'>> = {};

  if (args.name !== undefined) updates.name = args.name;
  if (args.description !== undefined) updates.description = args.description;
  if (args.enabled !== undefined) updates.enabled = args.enabled;

  if (args.cron !== undefined) {
    updates.trigger = { type: 'cron', expression: args.cron, timezone: existing.trigge
fileMtime function · typescript · L31-L38 (8 LOC)
src/cli/executor.ts
async function fileMtime(filePath: string): Promise<number> {
  try {
    const s = await stat(filePath);
    return Math.floor(s.mtimeMs / 1000);
  } catch {
    return 0;
  }
}
acquireLock function · typescript · L40-L86 (47 LOC)
src/cli/executor.ts
async function acquireLock(taskId: string, timeout: number): Promise<string> {
  const lockDir = `/tmp/claude-scheduler-${taskId}.lock`;

  try {
    await mkdir(lockDir);
  } catch {
    // Lock exists — check if stale
    const now = Math.floor(Date.now() / 1000);
    const mtime = await fileMtime(lockDir);
    const age = now - mtime;

    if (age > timeout + 60) {
      // Check if holding process is alive
      try {
        const pidStr = await readFile(path.join(lockDir, 'pid'), 'utf-8');
        const pid = parseInt(pidStr.trim(), 10);
        if (!isNaN(pid)) {
          try {
            process.kill(pid, 0);
            // Process is alive — skip
            throw new Error(`Task ${taskId} still running (PID ${pid}), skipping.`);
          } catch (e) {
            if ((e as NodeJS.ErrnoException).code !== 'ESRCH') throw e;
            // Process is dead — stale lock
          }
        }
      } catch (e) {
        if ((e as Error).message.includes('skipping')) throw e;
   
releaseLock function · typescript · L88-L90 (3 LOC)
src/cli/executor.ts
async function releaseLock(lockDir: string): Promise<void> {
  await rm(lockDir, { recursive: true, force: true });
}
Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
spawnClaude function · typescript · L99-L154 (56 LOC)
src/cli/executor.ts
function spawnClaude(
  command: string,
  options: {
    cwd: string;
    skipPermissions: boolean;
    env?: Record<string, string>;
    stdoutPath: string;
    stderrPath: string;
    timeout: number;
  },
): Promise<SpawnResult> {
  return new Promise((resolve) => {
    const { createWriteStream } = require('node:fs') as typeof import('node:fs');
    const stdoutStream = createWriteStream(options.stdoutPath);
    const stderrStream = createWriteStream(options.stderrPath);

    const args = ['-p'];
    if (options.skipPermissions) {
      args.push('--dangerously-skip-permissions');
    }
    args.push(command);

    const childEnv = { ...process.env, ...(options.env ?? {}) };
    const child = spawn('claude', args, {
      cwd: options.cwd,
      env: childEnv,
      stdio: ['ignore', 'pipe', 'pipe'],
    });

    child.stdout?.pipe(stdoutStream);
    child.stderr?.pipe(stderrStream);

    let timedOut = false;
    const timer = setTimeout(() => {
      timedOut = true;
      child
writeStatus function · typescript · L158-L160 (3 LOC)
src/cli/executor.ts
async function writeStatus(logsDir: string, taskId: string, status: string): Promise<void> {
  await writeFile(path.join(logsDir, `${taskId}.status`), status, 'utf-8');
}
runDirect function · typescript · L164-L180 (17 LOC)
src/cli/executor.ts
async function runDirect(
  task: ScheduledTask,
  logsDir: string,
): Promise<SpawnResult> {
  const logPaths = getLogPaths(logsDir, task.id, process.platform);
  const stdoutPath = logPaths.stdout ?? logPaths.combined ?? path.join(logsDir, `${task.id}.out.log`);
  const stderrPath = logPaths.stderr ?? path.join(logsDir, `${task.id}.err.log`);

  return spawnClaude(task.execution.command, {
    cwd: task.execution.workingDirectory,
    skipPermissions: task.execution.skipPermissions,
    env: task.execution.env,
    stdoutPath,
    stderrPath,
    timeout: task.execution.timeout,
  });
}
runWorktree function · typescript · L184-L187 (4 LOC)
src/cli/executor.ts
async function runWorktree(
  task: ScheduledTask,
  logsDir: string,
): Promise<SpawnResult & { worktreePath?: string; worktreeBranch?: string; pushed?: boolean }> {
run function · typescript · L232-L324 (93 LOC)
src/cli/executor.ts
export async function run(taskId: string): Promise<void> {
  const configPath = process.env.CLAUDE_SCHEDULER_CONFIG
    ?? path.join(os.homedir(), '.claude', 'schedules.json');

  const config = await loadConfig(configPath);
  const task = findTask(config, taskId);

  if (!task) {
    const logsDir = getLogsDir();
    await ensureLogsDir(logsDir);
    await writeStatus(logsDir, taskId, 'failure:config-error');
    process.exitCode = 1;
    console.error(`Task not found: ${taskId}`);
    return;
  }

  if (!task.enabled) {
    console.error(`Task disabled: ${taskId}`);
    return;
  }

  // Restore PATH if stored
  if ((task as Record<string, unknown>).userPath) {
    process.env.PATH = (task as Record<string, unknown>).userPath as string;
  }

  // Set custom env vars
  if (task.execution.env) {
    for (const [key, value] of Object.entries(task.execution.env)) {
      process.env[key] = value;
    }
  }

  const logsDir = getLogsDir();
  await ensureLogsDir(logsDir);

  let lockDir: s
main function · typescript · L11-L140 (130 LOC)
src/cli/index.ts
async function main() {
  const args = process.argv.slice(2);
  const subcommand = args[0];

  if (!subcommand) {
    console.error('Usage: claude-scheduler <subcommand> [options]');
    console.error('Subcommands: init, sync, add, remove, update, migrate');
    process.exit(1);
  }

  try {
    switch (subcommand) {
      case 'init': {
        const result = await init();
        console.log(JSON.stringify(result));
        process.exitCode = result.success ? 0 : 1;
        break;
      }

      case 'sync': {
        const { values } = parseArgs({
          args: args.slice(1),
          options: {
            id: { type: 'string' },
            config: { type: 'string' },
          },
          strict: false,
        });

        // Dynamic import to avoid loading platform code eagerly
        const { registerTask } = await import('./platform.js');
        const result = await sync(registerTask, {
          taskId: values.id as string | undefined,
          configPath: values.confi
registerTask function · typescript · L23-L32 (10 LOC)
src/cli/platform.ts
export async function registerTask(task: ScheduledTask, shimPath: string): Promise<void> {
  const platform = process.platform;
  if (platform === 'darwin') {
    await registerDarwin(task, shimPath);
  } else if (platform === 'linux') {
    await registerLinux(task, shimPath);
  } else {
    throw new Error(`Unsupported platform: ${platform}`);
  }
}
unregisterTask function · typescript · L37-L44 (8 LOC)
src/cli/platform.ts
export async function unregisterTask(taskId: string): Promise<void> {
  const platform = process.platform;
  if (platform === 'darwin') {
    await unregisterDarwin(taskId);
  } else if (platform === 'linux') {
    await unregisterLinux(taskId);
  }
}
Want fix-PRs on findings? Install Repobility's GitHub App · github.com/apps/repobility-bot
registerDarwin function · typescript · L48-L76 (29 LOC)
src/cli/platform.ts
async function registerDarwin(task: ScheduledTask, shimPath: string): Promise<void> {
  const logsDir = getLogsDir();
  const cronExpr = task.trigger.type === 'cron' ? task.trigger.expression : undefined;

  const darwinTask: DarwinSchedulerTask = {
    id: task.id,
    name: task.name,
    command: task.execution.command,
    workingDirectory: task.execution.workingDirectory,
    timeout: task.execution.timeout,
    skipPermissions: task.execution.skipPermissions,
    logsDir,
    userPath: process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin',
    wrapperScriptPath: shimPath,
    cronExpression: cronExpr,
    runAtLoad: task.trigger.type === 'once',
  };

  const plistContent = generatePlist(darwinTask);
  const plistPath = getPlistPath(task.id);

  // Unload existing if present (ignore errors)
  try {
    await defaultExec('launchctl', ['unload', plistPath]);
  } catch { /* not loaded */ }

  await fs.writeFile(plistPath, plistContent, 'utf-8');
  await defaultExec('launchctl', ['load'
unregisterDarwin function · typescript · L78-L86 (9 LOC)
src/cli/platform.ts
async function unregisterDarwin(taskId: string): Promise<void> {
  const plistPath = getPlistPath(taskId);
  try {
    await defaultExec('launchctl', ['unload', plistPath]);
  } catch { /* not loaded */ }
  try {
    await fs.unlink(plistPath);
  } catch { /* doesn't exist */ }
}
registerLinux function · typescript · L90-L119 (30 LOC)
src/cli/platform.ts
async function registerLinux(task: ScheduledTask, shimPath: string): Promise<void> {
  const logsDir = getLogsDir();
  const cronExpr = task.trigger.type === 'cron' ? task.trigger.expression : undefined;

  const linuxTask: LinuxSchedulerTask = {
    id: task.id,
    name: task.name,
    command: task.execution.command,
    workingDirectory: task.execution.workingDirectory,
    timeout: task.execution.timeout,
    skipPermissions: task.execution.skipPermissions,
    logsDir,
    userPath: process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin',
    wrapperScriptPath: `${shimPath} ${task.id}`,
    cronExpression: cronExpr,
    timezone: task.trigger.timezone !== 'local' ? task.trigger.timezone : undefined,
  };

  let existingCrontab = '';
  try {
    const result = await defaultExec('crontab', ['-l']);
    existingCrontab = result.stdout;
  } catch { /* no crontab */ }

  const newCrontab = buildCrontabContent(existingCrontab, linuxTask);
  const tmpFile = `/tmp/crontab-scheduler-${process.
unregisterLinux function · typescript · L121-L133 (13 LOC)
src/cli/platform.ts
async function unregisterLinux(taskId: string): Promise<void> {
  let existingCrontab = '';
  try {
    const result = await defaultExec('crontab', ['-l']);
    existingCrontab = result.stdout;
  } catch { return; /* no crontab */ }

  const newCrontab = buildCrontabContent(existingCrontab, null, taskId);
  const tmpFile = `/tmp/crontab-scheduler-${process.pid}`;
  await fs.writeFile(tmpFile, newCrontab, 'utf-8');
  await defaultExec('crontab', [tmpFile]);
  await fs.unlink(tmpFile);
}
getGlobalSchedulesPath function · typescript · L19-L21 (3 LOC)
src/config.ts
export function getGlobalSchedulesPath(): string {
  return path.join(os.homedir(), '.claude', 'schedules.json');
}
getProjectSchedulesPath function · typescript · L23-L25 (3 LOC)
src/config.ts
export function getProjectSchedulesPath(projectPath: string): string {
  return path.join(projectPath, '.claude', 'schedules.json');
}
getLogsDir function · typescript · L27-L29 (3 LOC)
src/config.ts
export function getLogsDir(): string {
  return path.join(os.homedir(), '.claude', 'logs');
}
getHistoryPath function · typescript · L31-L33 (3 LOC)
src/config.ts
export function getHistoryPath(): string {
  return path.join(os.homedir(), '.claude', 'execution-history.jsonl');
}
Methodology: Repobility · https://repobility.com/research/state-of-ai-code-2026/
getSessionIdPath function · typescript · L39-L44 (6 LOC)
src/config.ts
export function getSessionIdPath(taskId: string): string {
  if (!isSafeIdentifier(taskId)) {
    throw new Error(`Invalid task ID: ${taskId}`);
  }
  return path.join(getLogsDir(), `${taskId}.session`);
}
loadConfig function · typescript · L51-L59 (9 LOC)
src/config.ts
export async function loadConfig(filePath: string): Promise<SchedulesConfig> {
  try {
    const raw = await fs.readFile(filePath, 'utf-8');
    const data = JSON.parse(raw);
    return SchedulesConfigSchema.parse(data);
  } catch {
    return createEmptyConfig();
  }
}
saveConfig function · typescript · L65-L73 (9 LOC)
src/config.ts
export async function saveConfig(filePath: string, config: SchedulesConfig): Promise<void> {
  // Validate before writing
  SchedulesConfigSchema.parse(config);
  const dir = path.dirname(filePath);
  await fs.mkdir(dir, { recursive: true });
  const tmpPath = path.join(dir, `.schedules.${process.pid}.tmp`);
  await fs.writeFile(tmpPath, JSON.stringify(config, null, 2) + '\n', 'utf-8');
  await fs.rename(tmpPath, filePath);
}
loadMergedConfig function · typescript · L89-L130 (42 LOC)
src/config.ts
export async function loadMergedConfig(
  projectPath: string,
  globalPath?: string,
): Promise<MergedConfigResult> {
  const resolvedGlobalPath = globalPath ?? getGlobalSchedulesPath();
  const projectConfigPath = getProjectSchedulesPath(projectPath);

  const global = await loadConfig(resolvedGlobalPath);
  const project = await loadConfig(projectConfigPath);

  // Collect global task IDs
  const globalTaskIds = new Set(global.tasks.map(t => t.id));

  // Sanitize project tasks: strip skipPermissions, drop colliding IDs
  const sanitizedProjectTasks: ScheduledTask[] = [];
  for (const task of project.tasks) {
    if (globalTaskIds.has(task.id)) {
      // Global wins on ID collision - silently drop project task
      console.warn(`[claude-scheduler] Project task "${task.name}" (${task.id}) dropped: collides with global task ID`);
      continue;
    }
    // Strip skipPermissions from project tasks (trust boundary)
    if (task.execution.skipPermissions) {
      console.warn(`[claud
addTask function · typescript · L138-L146 (9 LOC)
src/config.ts
export function addTask(config: SchedulesConfig, task: ScheduledTask): SchedulesConfig {
  if (config.tasks.some(t => t.id === task.id)) {
    throw new Error(`Duplicate task ID: ${task.id}`);
  }
  return {
    ...config,
    tasks: [...config.tasks, task],
  };
}
updateTask function · typescript · L152-L169 (18 LOC)
src/config.ts
export function updateTask(
  config: SchedulesConfig,
  taskId: string,
  updates: Partial<Pick<ScheduledTask, 'name' | 'description' | 'enabled' | 'trigger' | 'execution' | 'tags'>>,
): SchedulesConfig {
  const index = config.tasks.findIndex(t => t.id === taskId);
  if (index === -1) {
    throw new Error(`Task not found: ${taskId}`);
  }
  const updated: ScheduledTask = {
    ...config.tasks[index],
    ...updates,
    updatedAt: new Date().toISOString(),
  };
  const tasks = [...config.tasks];
  tasks[index] = updated;
  return { ...config, tasks };
}
removeTask function · typescript · L175-L184 (10 LOC)
src/config.ts
export function removeTask(config: SchedulesConfig, taskId: string): SchedulesConfig {
  const index = config.tasks.findIndex(t => t.id === taskId);
  if (index === -1) {
    throw new Error(`Task not found: ${taskId}`);
  }
  return {
    ...config,
    tasks: config.tasks.filter(t => t.id !== taskId),
  };
}
findTask function · typescript · L189-L197 (9 LOC)
src/config.ts
export function findTask(config: SchedulesConfig, idOrName: string): ScheduledTask | undefined {
  // Try ID match first
  const byId = config.tasks.find(t => t.id === idOrName);
  if (byId) return byId;

  // Fall back to case-insensitive name match
  const lowerSearch = idOrName.toLowerCase();
  return config.tasks.find(t => t.name.toLowerCase() === lowerSearch);
}
Same scanner, your repo: https://repobility.com — Repobility
cronToHuman function · typescript · L11-L17 (7 LOC)
src/cron/humanizer.ts
export function cronToHuman(expression: string): string {
  try {
    return cronstrue.toString(expression, { use24HourTimeFormat: false });
  } catch {
    return expression;
  }
}
formatDate function · typescript · L22-L32 (11 LOC)
src/cron/humanizer.ts
export function formatDate(date: Date | string): string {
  const d = typeof date === 'string' ? new Date(date) : date;
  return d.toLocaleString('en-US', {
    year: 'numeric',
    month: 'short',
    day: 'numeric',
    hour: 'numeric',
    minute: '2-digit',
    hour12: true,
  });
}
formatDuration function · typescript · L37-L51 (15 LOC)
src/cron/humanizer.ts
export function formatDuration(ms: number): string {
  if (ms < 1000) return '0s';

  const seconds = Math.floor(ms / 1000);
  const hours = Math.floor(seconds / 3600);
  const minutes = Math.floor((seconds % 3600) / 60);
  const secs = seconds % 60;

  const parts: string[] = [];
  if (hours > 0) parts.push(`${hours}h`);
  if (minutes > 0) parts.push(`${minutes}m`);
  if (secs > 0 || parts.length === 0) parts.push(`${secs}s`);

  return parts.join(' ');
}
formatRelativeTime function · typescript · L56-L75 (20 LOC)
src/cron/humanizer.ts
export function formatRelativeTime(date: Date | string): string {
  const d = typeof date === 'string' ? new Date(date) : date;
  const diffMs = Date.now() - d.getTime();
  const diffSeconds = Math.floor(diffMs / 1000);

  if (diffSeconds < 60) return 'just now';

  const diffMinutes = Math.floor(diffSeconds / 60);
  if (diffMinutes < 60) {
    return `${diffMinutes} minute${diffMinutes === 1 ? '' : 's'} ago`;
  }

  const diffHours = Math.floor(diffMinutes / 60);
  if (diffHours < 24) {
    return `${diffHours} hour${diffHours === 1 ? '' : 's'} ago`;
  }

  const diffDays = Math.floor(diffHours / 24);
  return `${diffDays} day${diffDays === 1 ? '' : 's'} ago`;
}
validateCron function · typescript · L16-L37 (22 LOC)
src/cron/parser.ts
export function validateCron(expression: string): ValidationResult {
  if (!expression || expression.trim().length === 0) {
    return { valid: false, error: 'Empty cron expression' };
  }

  // Reject 6-field (with seconds) or invalid field counts
  const fields = expression.trim().split(/\s+/);
  if (fields.length !== 5) {
    return { valid: false, error: `Expected 5 fields, got ${fields.length}` };
  }

  try {
    // Croner validates on construction
    new Cron(expression);
    return { valid: true };
  } catch (err) {
    return {
      valid: false,
      error: err instanceof Error ? err.message : 'Invalid cron expression',
    };
  }
}
getNextRuns function · typescript · L42-L58 (17 LOC)
src/cron/parser.ts
export function getNextRuns(expression: string, count: number, timezone?: string): Date[] {
  const options: Record<string, unknown> = {};
  if (timezone && timezone !== 'local') {
    options.timezone = timezone;
  }

  const cron = new Cron(expression, options);
  const runs: Date[] = [];
  let next = cron.nextRun();

  while (next && runs.length < count) {
    runs.push(next);
    next = cron.nextRun(new Date(next.getTime() + 1000));
  }

  return runs;
}
getNextRun function · typescript · L63-L66 (4 LOC)
src/cron/parser.ts
export function getNextRun(expression: string, timezone?: string): Date {
  const runs = getNextRuns(expression, 1, timezone);
  return runs[0];
}
parseHour function · typescript · L80-L92 (13 LOC)
src/cron/parser.ts
function parseHour(timeStr: string): number | undefined {
  const match = timeStr.match(/(\d{1,2})\s*(am|pm)?/i);
  if (!match) return undefined;

  let hour = parseInt(match[1], 10);
  const meridiem = match[2]?.toLowerCase();

  if (meridiem === 'pm' && hour < 12) hour += 12;
  if (meridiem === 'am' && hour === 12) hour = 0;

  if (hour < 0 || hour > 23) return undefined;
  return hour;
}
Provenance: Repobility (https://repobility.com) — every score reproducible from /scan/
naturalLanguageToCron function · typescript · L98-L143 (46 LOC)
src/cron/parser.ts
export function naturalLanguageToCron(input: string): string | undefined {
  const lower = input.toLowerCase().trim();

  // Check presets first
  for (const [name, expr] of Object.entries(CRON_PRESETS)) {
    if (lower === name) return expr;
  }

  // "every minute"
  if (/^every\s+minute$/.test(lower)) {
    return '* * * * *';
  }

  // "every N minutes"
  const everyNMin = lower.match(/^every\s+(\d+)\s+minutes?$/);
  if (everyNMin) {
    const n = parseInt(everyNMin[1], 10);
    if (n >= 1 && n <= 59) return `*/${n} * * * *`;
  }

  // "daily at Xam/pm"
  const dailyAt = lower.match(/^daily\s+at\s+(.+)$/);
  if (dailyAt) {
    const hour = parseHour(dailyAt[1]);
    if (hour !== undefined) return `0 ${hour} * * *`;
  }

  // "every weekday at Xam/pm"
  const weekdayAt = lower.match(/^every\s+weekday\s+at\s+(.+)$/);
  if (weekdayAt) {
    const hour = parseHour(weekdayAt[1]);
    if (hour !== undefined) return `0 ${hour} * * 1-5`;
  }

  // "every <day> at Xam/pm"
  const dayAt = lo
recordExecution function · typescript · L12-L19 (8 LOC)
src/history/index.ts
export async function recordExecution(
  historyPath: string,
  record: ExecutionHistoryRecord,
): Promise<void> {
  await fs.mkdir(path.dirname(historyPath), { recursive: true });
  const line = JSON.stringify(record) + '\n';
  await fs.appendFile(historyPath, line, 'utf-8');
}
getRecentExecutions function · typescript · L34-L84 (51 LOC)
src/history/index.ts
export async function getRecentExecutions(
  historyPath: string,
  options?: QueryOptions,
): Promise<ExecutionHistoryRecord[]> {
  let content: string;
  try {
    content = await fs.readFile(historyPath, 'utf-8');
  } catch {
    return [];
  }

  const lines = content.trim().split('\n').filter(l => l.length > 0);
  const records: ExecutionHistoryRecord[] = [];

  for (const line of lines) {
    try {
      const record = JSON.parse(line) as ExecutionHistoryRecord;
      records.push(record);
    } catch {
      // Skip corrupted lines
    }
  }

  // Apply filters
  let filtered = records;

  if (options?.taskId) {
    filtered = filtered.filter(r => r.taskId === options.taskId);
  }
  if (options?.taskName) {
    filtered = filtered.filter(r => r.taskName === options.taskName);
  }
  if (options?.project) {
    filtered = filtered.filter(r => r.project === options.project);
  }
  if (options?.status) {
    filtered = filtered.filter(r => r.status === options.status);
  }

  // Sor
page 1 / 2next ›