import path from 'node:path'; import { access, mkdir, rm } from 'node:fs/promises'; import { pathToFileURL } from 'node:url'; import { getEnv } from '../config/env.js'; import { sendManagedStockAlertWebPush } from './stock-alert-service.js'; export type ManagedScheduleServiceResult = { ok: boolean; skipped: boolean; title: string; body: string; itemCount: number; lines: string[]; ios: { ok: boolean; skipped: boolean; reason?: string; sentCount: number; failedCount: number; }; web: { ok: boolean; skipped: boolean; reason?: string; sentCount: number; failedCount: number; }; }; export type ManagedScheduleServiceMetadata = { scheduleId: number; serviceKey: string; packageName: string; relativeDirectory: string; manifestPath: string; readmePath: string; sourcePath: string; runtimePath: string; }; function sanitizeManagedServiceToken(value: string) { return value .trim() .toLowerCase() .replace(/[^a-z0-9]+/g, '-') .replace(/^-+|-+$/g, '') .slice(0, 60); } function appendScheduleSuffixOnce(value: string, suffix: string) { const normalizedValue = value.trim(); const normalizedSuffix = suffix.trim().toLowerCase(); if (!normalizedValue || !normalizedSuffix) { return normalizedValue; } return normalizedValue.toLowerCase().endsWith(`-${normalizedSuffix}`) ? normalizedValue : `${normalizedValue}-${suffix.trim()}`; } function getScheduleRepoRoot() { const env = getEnv(); return env.SERVER_COMMAND_MAIN_PROJECT_ROOT || env.PLAN_MAIN_PROJECT_REPO_PATH || process.cwd(); } export function buildManagedScheduleServiceMetadata(scheduleId: number, workId: string): ManagedScheduleServiceMetadata { const workToken = sanitizeManagedServiceToken(appendScheduleSuffixOnce(workId, 'service')) || 'task-service'; const serviceKey = `schedule-${scheduleId}-${workToken}`; const relativeDirectory = `.auto_codex/schedule/${scheduleId}`; const packageName = workToken || `schedule-${scheduleId}-service`; return { scheduleId, serviceKey, packageName, relativeDirectory, manifestPath: `${relativeDirectory}/service-manifest.json`, readmePath: `${relativeDirectory}/README.md`, sourcePath: `${relativeDirectory}/service.ts`, runtimePath: `${relativeDirectory}/service.mjs`, }; } export async function prepareManagedScheduleServiceDirectory(scheduleId: number) { const repoRoot = getScheduleRepoRoot(); const absoluteDirectory = path.join(repoRoot, '.auto_codex', 'schedule', String(scheduleId)); await mkdir(absoluteDirectory, { recursive: true }); await rm(path.join(absoluteDirectory, 'managed-service'), { recursive: true, force: true }); } export async function removeManagedScheduleServiceArtifacts(scheduleId: number) { const repoRoot = getScheduleRepoRoot(); const scheduleDirectory = path.join(repoRoot, '.auto_codex', 'schedule', String(scheduleId)); await rm(path.join(scheduleDirectory, 'managed-service'), { recursive: true, force: true }); await rm(path.join(scheduleDirectory, 'README.md'), { force: true }); await rm(path.join(scheduleDirectory, 'service-manifest.json'), { force: true }); await rm(path.join(scheduleDirectory, 'service.ts'), { force: true }); await rm(path.join(scheduleDirectory, 'service.mjs'), { force: true }); } export async function hasManagedScheduleServicePackage(relativeDirectory: string | null | undefined) { const trimmedDirectory = String(relativeDirectory ?? '').trim(); if (!trimmedDirectory) { return false; } const repoRoot = getScheduleRepoRoot(); const runtimePath = path.join(repoRoot, trimmedDirectory, 'service.mjs'); const manifestPath = path.join(repoRoot, trimmedDirectory, 'service-manifest.json'); try { await Promise.all([access(runtimePath), access(manifestPath)]); return true; } catch { return false; } } export async function runManagedScheduleService(relativeDirectory: string) { const repoRoot = getScheduleRepoRoot(); const runtimePath = path.join(repoRoot, relativeDirectory, 'service.mjs'); const importedModule = await import(`${pathToFileURL(runtimePath).href}?t=${Date.now()}`); const run = typeof importedModule.run === 'function' ? importedModule.run : typeof importedModule.default?.run === 'function' ? importedModule.default.run : null; if (!run) { throw new Error(`스케줄 서비스 실행 함수를 찾을 수 없습니다: ${relativeDirectory}/service.mjs`); } return run({ scheduleRoot: path.join(repoRoot, trimmedRelativeDirectory(relativeDirectory)), scheduleDirectory: trimmedRelativeDirectory(relativeDirectory), repoRoot, now: new Date().toISOString(), runCurrentPriceStockAlertService(definition: { scheduleId: number; serviceKey: string; title: string }) { return sendManagedStockAlertWebPush({ scheduleId: definition.scheduleId, serviceKey: definition.serviceKey, title: definition.title, mode: 'price', }); }, runChangeRateThresholdStockAlertService( definition: { scheduleId: number; serviceKey: string; title: string; thresholdPercent: number }, ) { return sendManagedStockAlertWebPush({ scheduleId: definition.scheduleId, serviceKey: definition.serviceKey, title: definition.title, mode: 'change-threshold', thresholdPercent: definition.thresholdPercent, }); }, runChangeRateAndVolumeSpikeStockAlertService( definition: { scheduleId: number; serviceKey: string; title: string; thresholdPercent: number; minVolumeIncreasePercent: number; }, ) { return sendManagedStockAlertWebPush({ scheduleId: definition.scheduleId, serviceKey: definition.serviceKey, title: definition.title, mode: 'change-threshold-volume-spike', thresholdPercent: definition.thresholdPercent, minVolumeIncreasePercent: definition.minVolumeIncreasePercent, }); }, }) as Promise; } function trimmedRelativeDirectory(relativeDirectory: string) { return String(relativeDirectory ?? '').trim().replace(/\\/g, '/').replace(/^\/+|\/+$/g, ''); }