"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.SCHEDULE_PRESETS = void 0;
exports.shQuoteForCron = shQuoteForCron;
exports.tokenizeShellArgs = tokenizeShellArgs;
exports.cmdQuoteForSchtasks = cmdQuoteForSchtasks;
exports.buildUnixCronLine = buildUnixCronLine;
exports.extractUnixCronArgs = extractUnixCronArgs;
exports.launchdScheduleToCron = launchdScheduleToCron;
exports.cronToLaunchdSchedule = cronToLaunchdSchedule;
exports.buildLaunchAgentPlist = buildLaunchAgentPlist;
exports.extractLaunchAgentArgs = extractLaunchAgentArgs;
exports.cronToSchtasksArgs = cronToSchtasksArgs;
exports.windowsTaskName = windowsTaskName;
exports.extractLaunchdSchedule = extractLaunchdSchedule;
exports.scheduleSetCommand = scheduleSetCommand;
exports.scheduleGetCommand = scheduleGetCommand;
exports.scheduleRemoveCommand = scheduleRemoveCommand;
exports.scheduleFixCommand = scheduleFixCommand;
exports.listSchedules = listSchedules;
exports.setScheduleForCwd = setScheduleForCwd;
exports.removeScheduleById = removeScheduleById;
exports.fixSchedules = fixSchedules;
const node_child_process_1 = require("node:child_process");
const fs = __importStar(require("node:fs"));
const os = __importStar(require("node:os"));
const path = __importStar(require("node:path"));
const readline = __importStar(require("node:readline/promises"));
const node_process_1 = require("node:process");
const logger_1 = require("../logger");
const platform_1 = require("../platform");
const chalk_1 = __importDefault(require("chalk"));
// ─── Preset schedules ────────────────────────────────────────────────────────
exports.SCHEDULE_PRESETS = [
    { label: 'Every 15 minutes', cron: '*/15 * * * *' },
    { label: 'Every 30 minutes', cron: '*/30 * * * *' },
    { label: 'Every hour', cron: '0 * * * *' },
    { label: 'Every 5 hours', cron: '0 */5 * * *' },
    { label: 'Every day at 8am', cron: '0 8 * * *' },
];
const PRESETS = exports.SCHEDULE_PRESETS;
async function pickCron() {
    console.log('\n  Select a schedule:');
    PRESETS.forEach((p, i) => console.log(`    ${chalk_1.default.cyan(String(i + 1))}. ${p.label}  ${chalk_1.default.dim(p.cron)}`));
    const rl = readline.createInterface({ input: node_process_1.stdin, output: node_process_1.stdout });
    try {
        while (true) {
            const raw = await rl.question(`\n  Choice ${chalk_1.default.dim('[1]')}: `);
            const val = raw.trim() || '1';
            const idx = parseInt(val, 10);
            if (idx >= 1 && idx <= PRESETS.length)
                return PRESETS[idx - 1].cron;
            console.log(chalk_1.default.yellow(`  Enter a number between 1 and ${PRESETS.length}.`));
        }
    }
    finally {
        rl.close();
    }
}
// ─── Shared helpers ───────────────────────────────────────────────────────────
function getAidevBin() {
    return (0, platform_1.findBin)('aidev') ?? 'aidev';
}
const SAFE_SHELL_ARG_RE = /^[a-zA-Z0-9_\-+=:.,\/@%]+$/;
/**
 * Quotes an arg for embedding inside a single-quoted shell command
 * (e.g. inside `zsh -c '…'`). Inside the outer single quotes the inner
 * string is parsed as shell, so we use double quotes and escape `\ $ ` "`.
 * Single quotes are not supported (would close the outer quote).
 */
function shQuoteForCron(s) {
    if (s.includes("'")) {
        throw new Error(`Single quotes are not supported in scheduled args: ${s}`);
    }
    if (SAFE_SHELL_ARG_RE.test(s))
        return s;
    return `"${s.replace(/([\\$`"])/g, '\\$1')}"`;
}
/** Tokenises a shell-style arg string we generated (unquoted or double-quoted). */
function tokenizeShellArgs(s) {
    const tokens = [];
    const re = /"((?:\\.|[^"\\])*)"|(\S+)/g;
    let m;
    while ((m = re.exec(s)) !== null) {
        if (m[1] !== undefined) {
            tokens.push(m[1].replace(/\\(.)/g, '$1'));
        }
        else {
            tokens.push(m[2]);
        }
    }
    return tokens;
}
/** Quotes an arg for embedding into a Windows schtasks /tr command string. */
function cmdQuoteForSchtasks(s) {
    if (/^[a-zA-Z0-9_\-+=:.,\/@%\\]+$/.test(s))
        return s;
    return `"${s.replace(/"/g, '""')}"`;
}
// ─── Unix (crontab) ───────────────────────────────────────────────────────────
const UNIX_MARKER_PREFIX = '# aidev-cwd:';
function getCrontab() {
    const result = (0, node_child_process_1.spawnSync)('crontab', ['-l'], { encoding: 'utf8' });
    return result.status === 0 ? result.stdout || '' : '';
}
function setCrontab(content) {
    const result = (0, node_child_process_1.spawnSync)('crontab', ['-'], { input: content, encoding: 'utf8' });
    return result.status === 0;
}
function buildUnixCronLine(cronExpr, cwd, nodeBin, aidevBin, extraArgs = []) {
    const marker = `${UNIX_MARKER_PREFIX}${cwd}`;
    const argsStr = extraArgs.length > 0 ? ' ' + extraArgs.map(shQuoteForCron).join(' ') : '';
    return `${cronExpr} zsh -i -l -c 'cd ${cwd} && ${nodeBin} ${aidevBin} run${argsStr}' ${marker}`;
}
/** Extracts the extra args (after `aidev run`) from an existing cron line. */
function extractUnixCronArgs(line) {
    // Capture the segment after "aidev run" up to the closing single quote of `zsh -c '…'`.
    const m = line.match(/\baidev\s+run\b([^']*)/);
    if (!m)
        return [];
    const seg = m[1].trim();
    return seg ? tokenizeShellArgs(seg) : [];
}
function scheduleSetUnix(cronExpr, extraArgs, cwd) {
    const aidevBin = getAidevBin();
    const nodeBin = (0, platform_1.findBin)('node') ?? 'node';
    const newLine = buildUnixCronLine(cronExpr, cwd, nodeBin, aidevBin, extraArgs);
    // Replace any existing aidev entry for this cwd, regardless of cron/args.
    const lines = getCrontab().split('\n').filter((l) => {
        if (!l.includes(UNIX_MARKER_PREFIX))
            return true;
        const m = l.match(/# aidev-cwd:(.+)$/);
        return !m || m[1].trim() !== cwd;
    });
    lines.push(newLine);
    const updated = lines.join('\n').replace(/\n+$/, '') + '\n';
    if (setCrontab(updated)) {
        logger_1.logger.success(`Cron schedule set: ${cronExpr}`);
        logger_1.logger.info(`Entry: ${newLine}`);
    }
    else {
        throw new Error('Failed to update crontab');
    }
}
function parseAidevEntries(crontab) {
    return crontab
        .split('\n')
        .filter((l) => l.includes(UNIX_MARKER_PREFIX))
        .map((line) => {
        const cwdMatch = line.match(/# aidev-cwd:(.+)$/);
        const cwd = cwdMatch ? cwdMatch[1].trim() : '(unknown)';
        const parts = line.trim().split(/\s+/);
        const cron = parts.slice(0, 5).join(' ');
        const extraArgs = extractUnixCronArgs(line);
        return { cron, cwd, line, extraArgs };
    });
}
function printEntriesTable(entries) {
    const cwdW = Math.max(9, ...entries.map((e) => e.cwd.length));
    const cronW = Math.max(8, ...entries.map((e) => e.cron.length));
    const header = `  ${chalk_1.default.bold('ID')}  ` +
        `${chalk_1.default.bold('Directory'.padEnd(cwdW))}  ` +
        `${chalk_1.default.bold('Schedule'.padEnd(cronW))}  ` +
        chalk_1.default.bold('Args');
    const sep = `  ──  ` + `${'─'.repeat(cwdW)}  ` + `${'─'.repeat(cronW)}  ` + '────';
    console.log(header);
    console.log(sep);
    entries.forEach((e, i) => {
        const id = chalk_1.default.cyan(String(i + 1).padStart(2));
        const isCurrentDir = e.cwd === process.cwd();
        const dir = isCurrentDir ? chalk_1.default.green(e.cwd.padEnd(cwdW)) : e.cwd.padEnd(cwdW);
        const args = e.extraArgs.length > 0 ? chalk_1.default.dim(e.extraArgs.join(' ')) : '';
        console.log(`  ${id}  ${dir}  ${e.cron.padEnd(cronW)}  ${args}`);
    });
}
function scheduleGetUnix() {
    const entries = parseAidevEntries(getCrontab());
    if (entries.length === 0) {
        logger_1.logger.warn('No aidev schedules found');
        logger_1.logger.info('Use "aidev schedule set" to configure one.');
        return;
    }
    logger_1.logger.info('Scheduled aidev jobs:');
    console.log();
    printEntriesTable(entries);
    console.log();
}
async function scheduleRemoveUnix(id) {
    const crontab = getCrontab();
    const entries = parseAidevEntries(crontab);
    if (entries.length === 0) {
        logger_1.logger.warn('No aidev schedules found');
        return;
    }
    let idx;
    if (id !== undefined) {
        idx = id - 1;
    }
    else {
        logger_1.logger.info('Scheduled aidev jobs:');
        console.log();
        printEntriesTable(entries);
        console.log();
        const rl = readline.createInterface({ input: node_process_1.stdin, output: node_process_1.stdout });
        try {
            while (true) {
                const raw = await rl.question(`  Remove ID ${chalk_1.default.dim('[1]')}: `);
                const val = raw.trim() || '1';
                const n = parseInt(val, 10);
                if (n >= 1 && n <= entries.length) {
                    idx = n - 1;
                    break;
                }
                console.log(chalk_1.default.yellow(`  Enter a number between 1 and ${entries.length}.`));
            }
        }
        finally {
            rl.close();
        }
    }
    if (idx < 0 || idx >= entries.length) {
        logger_1.logger.error(`Invalid ID: ${id}. Valid range: 1–${entries.length}`);
        process.exit(1);
    }
    const toRemove = entries[idx];
    const updated = crontab
        .split('\n')
        .filter((l) => l !== toRemove.line)
        .join('\n')
        .replace(/\n+$/, '') + '\n';
    if (setCrontab(updated)) {
        logger_1.logger.success(`Removed schedule for ${toRemove.cwd}`);
    }
    else {
        logger_1.logger.error('Failed to update crontab');
        process.exit(1);
    }
}
// ─── macOS (launchd) ─────────────────────────────────────────────────────────
//
// On macOS, LaunchAgents (unlike cron) run inside the user's GUI session and
// have access to the login Keychain. This lets claude / cursor / windsurf read
// their OAuth tokens, which they store in the Keychain via Electron safeStorage.
const DARWIN_LABEL_PREFIX = 'com.aidev.run.';
function launchdLabel(cwd) {
    // djb2 hash — stable, unique label per project directory
    let h = 5381;
    for (let i = 0; i < cwd.length; i++) {
        h = (((h << 5) + h) ^ cwd.charCodeAt(i)) >>> 0;
    }
    return `${DARWIN_LABEL_PREFIX}${h.toString(16)}`;
}
function getLaunchAgentsDir() {
    return path.join(os.homedir(), 'Library', 'LaunchAgents');
}
function xmlEscape(s) {
    return s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
}
function xmlUnescape(s) {
    return s.replace(/&lt;/g, '<').replace(/&gt;/g, '>').replace(/&amp;/g, '&');
}
/**
 * Maps a cron expression to a launchd schedule.
 * Supports the same subset as cronToSchtasksArgs.
 */
/** Inverse of {@link cronToLaunchdSchedule} for supported preset expressions. */
function launchdScheduleToCron(schedule) {
    if (schedule.key === 'StartInterval') {
        const { seconds } = schedule;
        if (seconds % 3600 === 0 && seconds >= 3600) {
            const hours = seconds / 3600;
            if (hours === 1)
                return '0 * * * *';
            return `0 */${hours} * * *`;
        }
        if (seconds % 60 === 0 && seconds >= 60) {
            const minutes = seconds / 60;
            return `*/${minutes} * * * *`;
        }
        return null;
    }
    return `0 ${schedule.hour} * * *`;
}
function cronToLaunchdSchedule(cron) {
    // */N * * * * → every N minutes
    const everyMin = cron.match(/^\*\/(\d+) \* \* \* \*$/);
    if (everyMin)
        return { key: 'StartInterval', seconds: parseInt(everyMin[1], 10) * 60 };
    // 0 * * * * → every hour
    if (cron === '0 * * * *')
        return { key: 'StartInterval', seconds: 3600 };
    // 0 */H * * * → every H hours
    const everyHr = cron.match(/^0 \*\/(\d+) \* \* \*$/);
    if (everyHr)
        return { key: 'StartInterval', seconds: parseInt(everyHr[1], 10) * 3600 };
    // 0 H * * * → daily at H:00
    const daily = cron.match(/^0 (\d+) \* \* \*$/);
    if (daily)
        return { key: 'StartCalendarInterval', hour: parseInt(daily[1], 10), minute: 0 };
    return null;
}
function buildLaunchAgentPlist(label, nodeBin, aidevBin, cwd, schedule, extraArgs = []) {
    // Capture PATH at scheduling time (from the developer's live terminal session).
    // This ensures the right node/claude/cursor/windsurf binaries are found when
    // the agent fires later.
    const envPath = process.env.PATH ?? '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin';
    const home = process.env.HOME ?? os.homedir();
    const scheduleXml = schedule.key === 'StartInterval'
        ? `\t<key>StartInterval</key>\n\t<integer>${schedule.seconds}</integer>`
        : [
            '\t<key>StartCalendarInterval</key>',
            '\t<dict>',
            '\t\t<key>Hour</key>',
            `\t\t<integer>${schedule.hour}</integer>`,
            '\t\t<key>Minute</key>',
            `\t\t<integer>${schedule.minute}</integer>`,
            '\t</dict>',
        ].join('\n');
    return [
        '<?xml version="1.0" encoding="UTF-8"?>',
        '<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">',
        '<plist version="1.0">',
        '<dict>',
        '\t<key>Label</key>',
        `\t<string>${xmlEscape(label)}</string>`,
        '\t<key>ProgramArguments</key>',
        '\t<array>',
        `\t\t<string>${xmlEscape(nodeBin)}</string>`,
        `\t\t<string>${xmlEscape(aidevBin)}</string>`,
        '\t\t<string>run</string>',
        ...extraArgs.map((a) => `\t\t<string>${xmlEscape(a)}</string>`),
        '\t</array>',
        '\t<key>WorkingDirectory</key>',
        `\t<string>${xmlEscape(cwd)}</string>`,
        scheduleXml,
        '\t<key>EnvironmentVariables</key>',
        '\t<dict>',
        '\t\t<key>PATH</key>',
        `\t\t<string>${xmlEscape(envPath)}</string>`,
        '\t\t<key>HOME</key>',
        `\t\t<string>${xmlEscape(home)}</string>`,
        '\t</dict>',
        '\t<key>RunAtLoad</key>',
        '\t<false/>',
        '</dict>',
        '</plist>',
        '',
    ].join('\n');
}
function scheduleSetDarwin(cronExpr, extraArgs, cwd) {
    const schedule = cronToLaunchdSchedule(cronExpr);
    if (!schedule) {
        throw new Error(`Cron expression "${cronExpr}" cannot be mapped to a launchd schedule. ` +
            'Choose a supported preset.');
    }
    const label = launchdLabel(cwd);
    const aidevBin = getAidevBin();
    const nodeBin = (0, platform_1.findBin)('node') ?? 'node';
    const launchAgentsDir = getLaunchAgentsDir();
    const plistPath = path.join(launchAgentsDir, `${label}.plist`);
    const plist = buildLaunchAgentPlist(label, nodeBin, aidevBin, cwd, schedule, extraArgs);
    try {
        fs.mkdirSync(launchAgentsDir, { recursive: true });
    }
    catch { /* already exists */ }
    // Unload any existing version first (ignore errors — it may not be loaded)
    if (fs.existsSync(plistPath)) {
        (0, node_child_process_1.spawnSync)('launchctl', ['unload', '-w', plistPath], { encoding: 'utf8' });
    }
    fs.writeFileSync(plistPath, plist, 'utf8');
    const loadResult = (0, node_child_process_1.spawnSync)('launchctl', ['load', '-w', plistPath], { encoding: 'utf8' });
    if (loadResult.status !== 0) {
        throw new Error(`Failed to load Launch Agent: ${loadResult.stderr?.trim() || 'unknown error'}`);
    }
    logger_1.logger.success(`Launch Agent scheduled: ${cronExpr}`);
    logger_1.logger.info(`Plist: ${plistPath}`);
}
/** Extracts the user-supplied extra args from an existing LaunchAgent plist. */
function extractLaunchAgentArgs(xml) {
    const block = xml.match(/<key>ProgramArguments<\/key>\s*<array>([\s\S]*?)<\/array>/);
    if (!block)
        return [];
    const strings = [...block[1].matchAll(/<string>([^<]*)<\/string>/g)].map((m) => xmlUnescape(m[1]));
    // ProgramArguments = [nodeBin, aidevBin, "run", ...extraArgs]
    if (strings.length <= 3)
        return [];
    return strings.slice(3);
}
function parseDarwinEntries() {
    const dir = getLaunchAgentsDir();
    let files;
    try {
        files = fs
            .readdirSync(dir)
            .filter((f) => f.startsWith(DARWIN_LABEL_PREFIX) && f.endsWith('.plist'))
            .map((f) => path.join(dir, f));
    }
    catch {
        return [];
    }
    return files.map((plistPath) => {
        const xml = fs.readFileSync(plistPath, 'utf8');
        const cwdMatch = xml.match(/<key>WorkingDirectory<\/key>\s*<string>([^<]+)<\/string>/);
        const cwd = cwdMatch ? cwdMatch[1] : '(unknown)';
        const labelMatch = xml.match(/<key>Label<\/key>\s*<string>([^<]+)<\/string>/);
        const label = labelMatch ? labelMatch[1] : path.basename(plistPath, '.plist');
        const intervalMatch = xml.match(/<key>StartInterval<\/key>\s*<integer>(\d+)<\/integer>/);
        let schedule = '(custom)';
        if (intervalMatch) {
            const secs = parseInt(intervalMatch[1], 10);
            schedule = secs % 3600 === 0 ? `every ${secs / 3600}h` : `every ${secs / 60}m`;
        }
        else {
            const hourMatch = xml.match(/<key>StartCalendarInterval<\/key>[\s\S]*?<key>Hour<\/key>\s*<integer>(\d+)<\/integer>/);
            if (hourMatch) {
                schedule = `daily at ${String(parseInt(hourMatch[1], 10)).padStart(2, '0')}:00`;
            }
        }
        const extraArgs = extractLaunchAgentArgs(xml);
        return { label, cwd, schedule, plistPath, extraArgs };
    });
}
function printDarwinEntriesTable(entries) {
    const cwdW = Math.max(9, ...entries.map((e) => e.cwd.length));
    const schedW = Math.max(8, ...entries.map((e) => e.schedule.length));
    const header = `  ${chalk_1.default.bold('ID')}  ` +
        `${chalk_1.default.bold('Directory'.padEnd(cwdW))}  ` +
        `${chalk_1.default.bold('Schedule'.padEnd(schedW))}  ` +
        chalk_1.default.bold('Args');
    const sep = `  ──  ${'─'.repeat(cwdW)}  ${'─'.repeat(schedW)}  ────`;
    console.log(header);
    console.log(sep);
    entries.forEach((e, i) => {
        const id = chalk_1.default.cyan(String(i + 1).padStart(2));
        const isCurrentDir = e.cwd === process.cwd();
        const dir = isCurrentDir ? chalk_1.default.green(e.cwd.padEnd(cwdW)) : e.cwd.padEnd(cwdW);
        const args = e.extraArgs.length > 0 ? chalk_1.default.dim(e.extraArgs.join(' ')) : '';
        console.log(`  ${id}  ${dir}  ${e.schedule.padEnd(schedW)}  ${args}`);
    });
}
function scheduleGetDarwin() {
    const entries = parseDarwinEntries();
    if (entries.length === 0) {
        logger_1.logger.warn('No aidev schedules found');
        logger_1.logger.info('Use "aidev schedule set" to configure one.');
        return;
    }
    logger_1.logger.info('Scheduled aidev jobs:');
    console.log();
    printDarwinEntriesTable(entries);
    console.log();
}
async function scheduleRemoveDarwin(id) {
    const entries = parseDarwinEntries();
    if (entries.length === 0) {
        logger_1.logger.warn('No aidev schedules found');
        return;
    }
    let idx;
    if (id !== undefined) {
        idx = id - 1;
    }
    else {
        logger_1.logger.info('Scheduled aidev jobs:');
        console.log();
        printDarwinEntriesTable(entries);
        console.log();
        const rl = readline.createInterface({ input: node_process_1.stdin, output: node_process_1.stdout });
        try {
            while (true) {
                const raw = await rl.question(`  Remove ID ${chalk_1.default.dim('[1]')}: `);
                const val = raw.trim() || '1';
                const n = parseInt(val, 10);
                if (n >= 1 && n <= entries.length) {
                    idx = n - 1;
                    break;
                }
                console.log(chalk_1.default.yellow(`  Enter a number between 1 and ${entries.length}.`));
            }
        }
        finally {
            rl.close();
        }
    }
    if (idx < 0 || idx >= entries.length) {
        logger_1.logger.error(`Invalid ID: ${id}. Valid range: 1–${entries.length}`);
        process.exit(1);
    }
    const toRemove = entries[idx];
    (0, node_child_process_1.spawnSync)('launchctl', ['unload', '-w', toRemove.plistPath], { encoding: 'utf8' });
    try {
        fs.unlinkSync(toRemove.plistPath);
    }
    catch { /* already removed */ }
    logger_1.logger.success(`Removed schedule for ${toRemove.cwd}`);
}
// ─── Windows (schtasks) ───────────────────────────────────────────────────────
/**
 * Converts a cron expression to schtasks /sc + /mo + /st arguments.
 * Supports the subset used by the preset list.
 */
function cronToSchtasksArgs(cron) {
    // */N * * * *  →  every N minutes
    const everyMin = cron.match(/^\*\/(\d+) \* \* \* \*$/);
    if (everyMin)
        return ['/sc', 'MINUTE', '/mo', everyMin[1]];
    // 0 * * * *  →  every hour
    if (cron === '0 * * * *')
        return ['/sc', 'HOURLY', '/mo', '1'];
    // 0 */N * * *  →  every N hours
    const everyHour = cron.match(/^0 \*\/(\d+) \* \* \*$/);
    if (everyHour)
        return ['/sc', 'HOURLY', '/mo', everyHour[1]];
    // 0 H * * *  →  daily at H:00
    const daily = cron.match(/^0 (\d+) \* \* \*$/);
    if (daily)
        return ['/sc', 'DAILY', '/st', `${daily[1].padStart(2, '0')}:00`];
    return null;
}
/** Stable task name derived from cwd + optional cron expr — safe for Task Scheduler. */
function windowsTaskName(cwd, cronExpr) {
    const sanitized = cwd.replace(/[:\\\/]+/g, '-').replace(/^-+|-+$/g, '');
    if (!cronExpr)
        return `aidev\\${sanitized}`;
    const cronTag = cronExpr.replace(/\*/g, 'x').replace(/\//g, 'e').replace(/\s+/g, '_');
    return `aidev\\${sanitized}--${cronTag}`;
}
function scheduleSetWindows(cronExpr, extraArgs, cwd) {
    const schtasksArgs = cronToSchtasksArgs(cronExpr);
    if (!schtasksArgs) {
        throw new Error(`Cron expression "${cronExpr}" cannot be mapped to Windows Task Scheduler. ` +
            'Choose a supported preset.');
    }
    const taskName = windowsTaskName(cwd, cronExpr);
    const aidevBin = getAidevBin();
    const argsStr = extraArgs.length > 0 ? ' ' + extraArgs.map(cmdQuoteForSchtasks).join(' ') : '';
    // cmd /c: run command and exit; /d: change drive+dir
    const command = `cmd /c cd /d "${cwd}" && "${aidevBin}" run${argsStr}`;
    const result = (0, node_child_process_1.spawnSync)('schtasks', ['/create', '/f', '/tn', taskName, '/tr', command, ...schtasksArgs], { encoding: 'utf8' });
    if (result.status === 0) {
        logger_1.logger.success(`Task Scheduler entry created: ${taskName}`);
        logger_1.logger.info(`Schedule: ${cronExpr}`);
    }
    else {
        throw new Error(`Failed to create Task Scheduler entry: ${result.stderr?.trim() || 'unknown error'}`);
    }
}
const WINDOWS_TASK_PREFIX = 'aidev\\';
function listWindowsAidevTasks() {
    const result = (0, node_child_process_1.spawnSync)('schtasks', ['/query', '/fo', 'CSV', '/nh'], { encoding: 'utf8' });
    if (result.status !== 0)
        return [];
    return result.stdout
        .split(/\r?\n/)
        .filter((line) => line.includes(`"\\${WINDOWS_TASK_PREFIX}`))
        .map((line) => {
        const cols = line.match(/"([^"]*)"/g)?.map((s) => s.replace(/"/g, ''));
        if (!cols || cols.length < 3)
            return null;
        return { taskName: cols[0].replace(/^\\/, ''), nextRun: cols[1], status: cols[2] };
    })
        .filter((x) => x !== null);
}
function printWindowsEntriesTable(entries) {
    const nameW = Math.max(4, ...entries.map((e) => e.taskName.length));
    const nextW = Math.max(8, ...entries.map((e) => e.nextRun.length));
    const header = `  ${chalk_1.default.bold('ID')}  ` +
        `${chalk_1.default.bold('Task'.padEnd(nameW))}  ` +
        chalk_1.default.bold('Next Run'.padEnd(nextW));
    const sep = `  ──  ` + `${'─'.repeat(nameW)}  ` + '─'.repeat(nextW);
    console.log(header);
    console.log(sep);
    entries.forEach((e, i) => {
        const id = chalk_1.default.cyan(String(i + 1).padStart(2));
        console.log(`  ${id}  ${e.taskName.padEnd(nameW)}  ${e.nextRun}`);
    });
}
function scheduleGetWindows() {
    const entries = listWindowsAidevTasks();
    if (entries.length === 0) {
        logger_1.logger.warn('No aidev schedules found');
        logger_1.logger.info('Use "aidev schedule set" to configure one.');
        return;
    }
    logger_1.logger.info('Scheduled aidev jobs:');
    console.log();
    printWindowsEntriesTable(entries);
    console.log();
}
async function scheduleRemoveWindows(id) {
    const entries = listWindowsAidevTasks();
    if (entries.length === 0) {
        logger_1.logger.warn('No aidev schedules found');
        return;
    }
    let idx;
    if (id !== undefined) {
        idx = id - 1;
    }
    else {
        logger_1.logger.info('Scheduled aidev jobs:');
        console.log();
        printWindowsEntriesTable(entries);
        console.log();
        const rl = readline.createInterface({ input: node_process_1.stdin, output: node_process_1.stdout });
        try {
            while (true) {
                const raw = await rl.question(`  Remove ID ${chalk_1.default.dim('[1]')}: `);
                const val = raw.trim() || '1';
                const n = parseInt(val, 10);
                if (n >= 1 && n <= entries.length) {
                    idx = n - 1;
                    break;
                }
                console.log(chalk_1.default.yellow(`  Enter a number between 1 and ${entries.length}.`));
            }
        }
        finally {
            rl.close();
        }
    }
    if (idx < 0 || idx >= entries.length) {
        logger_1.logger.error(`Invalid ID: ${id}. Valid range: 1–${entries.length}`);
        process.exit(1);
    }
    const toRemove = entries[idx];
    const result = (0, node_child_process_1.spawnSync)('schtasks', ['/delete', '/f', '/tn', toRemove.taskName], {
        encoding: 'utf8',
    });
    if (result.status === 0) {
        logger_1.logger.success(`Removed Task Scheduler entry: ${toRemove.taskName}`);
    }
    else {
        logger_1.logger.error(`Failed to remove task:\n${result.stderr}`);
        process.exit(1);
    }
}
// ─── schedule fix ─────────────────────────────────────────────────────────────
/** Extracts the launchd schedule object from a plist XML string. */
function extractLaunchdSchedule(xml) {
    const intervalMatch = xml.match(/<key>StartInterval<\/key>\s*<integer>(\d+)<\/integer>/);
    if (intervalMatch)
        return { key: 'StartInterval', seconds: parseInt(intervalMatch[1], 10) };
    const hourMatch = xml.match(/<key>Hour<\/key>\s*<integer>(\d+)<\/integer>/);
    const minMatch = xml.match(/<key>Minute<\/key>\s*<integer>(\d+)<\/integer>/);
    if (hourMatch && minMatch) {
        return {
            key: 'StartCalendarInterval',
            hour: parseInt(hourMatch[1], 10),
            minute: parseInt(minMatch[1], 10),
        };
    }
    return null;
}
function scheduleFixDarwin() {
    const entries = parseDarwinEntries();
    if (entries.length === 0) {
        logger_1.logger.warn('No aidev schedules found');
        return;
    }
    const aidevBin = getAidevBin();
    const nodeBin = (0, platform_1.findBin)('node') ?? 'node';
    let fixed = 0;
    for (const entry of entries) {
        const xml = fs.readFileSync(entry.plistPath, 'utf8');
        const schedule = extractLaunchdSchedule(xml);
        if (!schedule) {
            logger_1.logger.warn(`${entry.cwd}: cannot parse schedule — skipping`);
            continue;
        }
        const expected = buildLaunchAgentPlist(entry.label, nodeBin, aidevBin, entry.cwd, schedule, entry.extraArgs);
        if (expected === xml) {
            logger_1.logger.info(`${entry.cwd}: already up to date`);
            continue;
        }
        (0, node_child_process_1.spawnSync)('launchctl', ['unload', '-w', entry.plistPath], { encoding: 'utf8' });
        fs.writeFileSync(entry.plistPath, expected, 'utf8');
        const loadResult = (0, node_child_process_1.spawnSync)('launchctl', ['load', '-w', entry.plistPath], { encoding: 'utf8' });
        if (loadResult.status !== 0) {
            logger_1.logger.error(`Failed to reload ${entry.plistPath}:\n${loadResult.stderr}`);
            continue;
        }
        logger_1.logger.success(`Fixed: ${entry.cwd}`);
        fixed++;
    }
    logger_1.logger.info(`${fixed} fixed, ${entries.length - fixed} already up to date`);
}
function scheduleFixUnix() {
    const crontab = getCrontab();
    const entries = parseAidevEntries(crontab);
    if (entries.length === 0) {
        logger_1.logger.warn('No aidev schedules found');
        return;
    }
    const aidevBin = getAidevBin();
    const nodeBin = (0, platform_1.findBin)('node') ?? 'node';
    let lines = crontab.split('\n');
    let fixed = 0;
    for (const entry of entries) {
        const expected = buildUnixCronLine(entry.cron, entry.cwd, nodeBin, aidevBin, entry.extraArgs);
        if (entry.line === expected) {
            logger_1.logger.info(`${entry.cwd}: already up to date`);
            continue;
        }
        lines = lines.map((l) => (l === entry.line ? expected : l));
        logger_1.logger.success(`Fixed: ${entry.cwd}`);
        fixed++;
    }
    if (fixed > 0) {
        const updated = lines.join('\n').replace(/\n+$/, '') + '\n';
        if (!setCrontab(updated)) {
            logger_1.logger.error('Failed to update crontab');
            process.exit(1);
        }
    }
    logger_1.logger.info(`${fixed} fixed, ${entries.length - fixed} already up to date`);
}
function scheduleFixWindows() {
    logger_1.logger.warn('schedule fix is not supported on Windows — re-run "aidev schedule set" for each project.');
}
// ─── Public API ───────────────────────────────────────────────────────────────
async function scheduleSetCommand(cronExpr, extraArgs = []) {
    if (!cronExpr)
        cronExpr = await pickCron();
    const cwd = process.cwd();
    try {
        if (platform_1.isWindows)
            scheduleSetWindows(cronExpr, extraArgs, cwd);
        else if (process.platform === 'darwin')
            scheduleSetDarwin(cronExpr, extraArgs, cwd);
        else
            scheduleSetUnix(cronExpr, extraArgs, cwd);
    }
    catch (err) {
        logger_1.logger.error(err instanceof Error ? err.message : String(err));
        process.exit(1);
    }
}
async function scheduleGetCommand() {
    if (platform_1.isWindows)
        scheduleGetWindows();
    else if (process.platform === 'darwin')
        scheduleGetDarwin();
    else
        scheduleGetUnix();
}
async function scheduleRemoveCommand(id) {
    if (platform_1.isWindows)
        await scheduleRemoveWindows(id);
    else if (process.platform === 'darwin')
        await scheduleRemoveDarwin(id);
    else
        await scheduleRemoveUnix(id);
}
function scheduleFixCommand() {
    if (platform_1.isWindows)
        scheduleFixWindows();
    else if (process.platform === 'darwin')
        scheduleFixDarwin();
    else
        scheduleFixUnix();
}
function scheduleBackend() {
    if (platform_1.isWindows)
        return 'schtasks';
    if (process.platform === 'darwin')
        return 'launchd';
    return 'cron';
}
function presetLabelForCron(cron) {
    const preset = exports.SCHEDULE_PRESETS.find((p) => p.cron === cron);
    return preset?.label ?? cron;
}
function darwinScheduleLabel(xml) {
    const schedule = extractLaunchdSchedule(xml);
    if (!schedule)
        return { label: '(custom)', cron: null };
    const cron = launchdScheduleToCron(schedule);
    if (cron)
        return { label: presetLabelForCron(cron), cron };
    if (schedule.key === 'StartInterval') {
        const secs = schedule.seconds;
        const label = secs % 3600 === 0 ? `every ${secs / 3600}h` : `every ${secs / 60}m`;
        return { label, cron: null };
    }
    const label = `daily at ${String(schedule.hour).padStart(2, '0')}:${String(schedule.minute).padStart(2, '0')}`;
    return { label, cron: null };
}
function windowsSanitizedCwd(cwd) {
    return cwd.replace(/[:\\\/]+/g, '-').replace(/^-+|-+$/g, '');
}
function windowsTaskMatchesCwd(taskName, cwd) {
    const base = `aidev\\${windowsSanitizedCwd(cwd)}`;
    return taskName === base || taskName.startsWith(`${base}--`);
}
function cronFromWindowsTaskName(taskName, cwd) {
    const base = `aidev\\${windowsSanitizedCwd(cwd)}--`;
    if (!taskName.startsWith(base))
        return null;
    const tag = taskName.slice(base.length);
    for (const preset of exports.SCHEDULE_PRESETS) {
        const expected = preset.cron.replace(/\*/g, 'x').replace(/\//g, 'e').replace(/\s+/g, '_');
        if (tag === expected)
            return preset.cron;
    }
    return null;
}
function listUnixScheduleEntries(currentCwd) {
    return parseAidevEntries(getCrontab()).map((e, i) => ({
        id: i + 1,
        cwd: e.cwd,
        cron: e.cron,
        label: presetLabelForCron(e.cron),
        extraArgs: e.extraArgs,
        current: e.cwd === currentCwd,
    }));
}
function listDarwinScheduleEntries(currentCwd) {
    return parseDarwinEntries().map((e, i) => {
        const xml = fs.readFileSync(e.plistPath, 'utf8');
        const { label, cron } = darwinScheduleLabel(xml);
        return {
            id: i + 1,
            cwd: e.cwd,
            cron,
            label,
            extraArgs: e.extraArgs,
            current: e.cwd === currentCwd,
        };
    });
}
function listWindowsScheduleEntries(currentCwd) {
    return listWindowsAidevTasks().map((e, i) => {
        const cwd = e.taskName.replace(/^aidev\\/, '').split('--')[0]?.replace(/-/g, '/') ?? e.taskName;
        const cron = cronFromWindowsTaskName(e.taskName, currentCwd);
        return {
            id: i + 1,
            cwd,
            cron,
            label: cron ? presetLabelForCron(cron) : e.taskName,
            extraArgs: [],
            current: windowsTaskMatchesCwd(e.taskName, currentCwd),
        };
    });
}
function listSchedules(currentCwd) {
    const backend = scheduleBackend();
    const entries = backend === 'launchd'
        ? listDarwinScheduleEntries(currentCwd)
        : backend === 'schtasks'
            ? listWindowsScheduleEntries(currentCwd)
            : listUnixScheduleEntries(currentCwd);
    return {
        platform: process.platform,
        backend,
        presets: exports.SCHEDULE_PRESETS,
        entries,
        currentCwd,
        fixSupported: !platform_1.isWindows,
    };
}
function setScheduleForCwd(cwd, cronExpr, extraArgs = []) {
    try {
        if (platform_1.isWindows)
            scheduleSetWindows(cronExpr, extraArgs, cwd);
        else if (process.platform === 'darwin')
            scheduleSetDarwin(cronExpr, extraArgs, cwd);
        else
            scheduleSetUnix(cronExpr, extraArgs, cwd);
        return { ok: true, message: `Schedule set for ${cwd}: ${cronExpr}` };
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        return { ok: false, message };
    }
}
function removeScheduleById(id) {
    if (!Number.isInteger(id) || id < 1) {
        return { ok: false, message: `Invalid schedule id: ${id}` };
    }
    try {
        if (platform_1.isWindows) {
            const entries = listWindowsAidevTasks();
            if (id > entries.length) {
                return { ok: false, message: `Invalid schedule id: ${id}` };
            }
            const toRemove = entries[id - 1];
            const result = (0, node_child_process_1.spawnSync)('schtasks', ['/delete', '/f', '/tn', toRemove.taskName], {
                encoding: 'utf8',
            });
            if (result.status !== 0) {
                return { ok: false, message: result.stderr?.trim() || 'Failed to remove Task Scheduler entry' };
            }
            return { ok: true, message: `Removed ${toRemove.taskName}` };
        }
        if (process.platform === 'darwin') {
            const entries = parseDarwinEntries();
            if (id > entries.length) {
                return { ok: false, message: `Invalid schedule id: ${id}` };
            }
            const toRemove = entries[id - 1];
            (0, node_child_process_1.spawnSync)('launchctl', ['unload', '-w', toRemove.plistPath], { encoding: 'utf8' });
            try {
                fs.unlinkSync(toRemove.plistPath);
            }
            catch { /* already removed */ }
            return { ok: true, message: `Removed schedule for ${toRemove.cwd}` };
        }
        const crontab = getCrontab();
        const entries = parseAidevEntries(crontab);
        if (id > entries.length) {
            return { ok: false, message: `Invalid schedule id: ${id}` };
        }
        const toRemove = entries[id - 1];
        const updated = crontab
            .split('\n')
            .filter((l) => l !== toRemove.line)
            .join('\n')
            .replace(/\n+$/, '') + '\n';
        if (!setCrontab(updated)) {
            return { ok: false, message: 'Failed to update crontab' };
        }
        return { ok: true, message: `Removed schedule for ${toRemove.cwd}` };
    }
    catch (err) {
        const message = err instanceof Error ? err.message : String(err);
        return { ok: false, message };
    }
}
function fixSchedules() {
    if (platform_1.isWindows) {
        return {
            ok: false,
            message: 'schedule fix is not supported on Windows — re-run schedule set for each project.',
            fixed: 0,
            unchanged: 0,
        };
    }
    if (process.platform === 'darwin') {
        const entries = parseDarwinEntries();
        if (entries.length === 0) {
            return { ok: true, message: 'No aidev schedules found', fixed: 0, unchanged: 0 };
        }
        const aidevBin = getAidevBin();
        const nodeBin = (0, platform_1.findBin)('node') ?? 'node';
        let fixed = 0;
        let unchanged = 0;
        for (const entry of entries) {
            const xml = fs.readFileSync(entry.plistPath, 'utf8');
            const schedule = extractLaunchdSchedule(xml);
            if (!schedule)
                continue;
            const expected = buildLaunchAgentPlist(entry.label, nodeBin, aidevBin, entry.cwd, schedule, entry.extraArgs);
            if (expected === xml) {
                unchanged++;
                continue;
            }
            (0, node_child_process_1.spawnSync)('launchctl', ['unload', '-w', entry.plistPath], { encoding: 'utf8' });
            fs.writeFileSync(entry.plistPath, expected, 'utf8');
            const loadResult = (0, node_child_process_1.spawnSync)('launchctl', ['load', '-w', entry.plistPath], { encoding: 'utf8' });
            if (loadResult.status !== 0) {
                return {
                    ok: false,
                    message: loadResult.stderr?.trim() || `Failed to reload ${entry.plistPath}`,
                    fixed,
                    unchanged,
                };
            }
            fixed++;
        }
        return {
            ok: true,
            message: `${fixed} fixed, ${unchanged} already up to date`,
            fixed,
            unchanged,
        };
    }
    const crontab = getCrontab();
    const entries = parseAidevEntries(crontab);
    if (entries.length === 0) {
        return { ok: true, message: 'No aidev schedules found', fixed: 0, unchanged: 0 };
    }
    const aidevBin = getAidevBin();
    const nodeBin = (0, platform_1.findBin)('node') ?? 'node';
    let lines = crontab.split('\n');
    let fixed = 0;
    let unchanged = 0;
    for (const entry of entries) {
        const expected = buildUnixCronLine(entry.cron, entry.cwd, nodeBin, aidevBin, entry.extraArgs);
        if (entry.line === expected) {
            unchanged++;
            continue;
        }
        lines = lines.map((l) => (l === entry.line ? expected : l));
        fixed++;
    }
    if (fixed > 0) {
        const updated = lines.join('\n').replace(/\n+$/, '') + '\n';
        if (!setCrontab(updated)) {
            return { ok: false, message: 'Failed to update crontab', fixed: 0, unchanged };
        }
    }
    return {
        ok: true,
        message: `${fixed} fixed, ${unchanged} already up to date`,
        fixed,
        unchanged,
    };
}
//# sourceMappingURL=schedule.js.map