/** * Windows service controller — runs the bot at logon. Preferred mechanism is a * hidden ONLOGON Scheduled Task, but registering a logon-triggered task needs * admin, so from a normal (non-elevated) terminal we fall back to a launcher in * the per-user Startup folder — both run a small .vbs that starts node with no * console window; the app logs to a file. Stop precisely targets our node * process by command line, so it works regardless of how it was launched. */ import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"; import { join } from "node:path"; import { launchDetached, runSafe } from "./platform.js"; import type { LaunchSpec, ServiceController, ServiceResult } from "./types.js"; const TASK = "GrokTelegramBot"; /** Launcher dropped in the per-user Startup folder when no admin is available. */ const STARTUP_VBS = "GrokTelegramBot.vbs"; function taskName(spec?: { windowsTaskName?: string }): string { return spec?.windowsTaskName || TASK; } function startupVbsFile(spec?: { slug?: string }): string { return spec?.slug ? `GrokTelegramBot-${spec.slug}.vbs` : STARTUP_VBS; } /** The per-user Startup folder (runs at logon for the current user, no admin). * Undefined only if APPDATA is unset (e.g. running with no roaming profile). */ function startupDir(): string | undefined { const appData = process.env.APPDATA; return appData ? join(appData, "Microsoft", "Windows", "Start Menu", "Programs", "Startup") : undefined; } function startupVbsPath(spec?: { slug?: string }): string | undefined { const dir = startupDir(); return dir ? join(dir, startupVbsFile(spec)) : undefined; } /** Remove a leftover Startup-folder launcher (e.g. from an earlier non-elevated * install) so a task-based install never double-launches the bot at logon. */ function removeStartupLauncher(): void { const p = startupVbsPath(); if (p) rmSync(p, { force: true }); } /** Canonical launcher in the bot folder (the Scheduled Task points at it). */ function vbsPath(spec: LaunchSpec): string { return join(spec.cwd, "run-service.vbs"); } /** True when our hidden Scheduled Task is registered. */ function taskInstalled(spec?: { windowsTaskName?: string }): boolean { return runSafe("schtasks", ["/Query", "/TN", taskName(spec)]).ok; } /** True when a bot process matching this spec is currently running. Launch * paths use this to avoid starting a second instance — two pollers on one * bot token make Telegram return 409 Conflict. */ function isRunning(spec: LaunchSpec): boolean { const proc = runSafe("powershell", ["-NoProfile", "-Command", countScript(entryOf(spec))]); return proc.ok && /[1-9]\d*/.test(proc.out.trim()); } export const windowsController: ServiceController = { platform: "windows", async install(spec) { mkdirSync(spec.logsDir, { recursive: true }); const vbs = vbsPath(spec); writeFileSync(vbs, vbsLauncher(spec), "utf-8"); // Preferred: a hidden ONLOGON Scheduled Task. Registering a *logon-triggered* // task is a privileged operation, so /Create succeeds only from an elevated // (admin) terminal. From a normal terminal it returns "Access is denied". const tn = taskName(spec); runSafe("schtasks", ["/Delete", "/F", "/TN", tn]); // replace if present const res = runSafe("schtasks", [ "/Create", "/F", "/SC", "ONLOGON", "/TN", tn, "/TR", `wscript.exe "${vbs}"`, ]); if (res.ok) { removeStartupLauncher(); // avoid a leftover launcher double-starting the bot if (!isRunning(spec)) runSafe("schtasks", ["/Run", "/TN", tn]); return ok(`Installed scheduled task "${tn}" (starts at logon) and launched it.`); } // A task may still exist that we just couldn't overwrite (e.g. created by an // earlier elevated install). Reuse it rather than ALSO adding a Startup // launcher, which would double-launch the bot at logon (409 Conflict). if (taskInstalled(spec)) { removeStartupLauncher(); if (!isRunning(spec)) runSafe("schtasks", ["/Run", "/TN", tn]); return ok(`Scheduled task "${tn}" already exists; launched it. (Re-run elevated to recreate it.)`); } // Fallback (no admin — the common case): drop the launcher in the per-user // Startup folder. It runs hidden at every logon with no elevation. const startupVbs = startupVbsPath(spec); const dir = startupDir(); if (!startupVbs || !dir) { return fail( `Could not create the logon task (${res.out.trim()}) and no per-user Startup folder is available. ` + `Re-run "grok-tg install" from an elevated terminal (Run as administrator).`, ); } try { mkdirSync(dir, { recursive: true }); writeFileSync(startupVbs, vbsLauncher(spec), "utf-8"); } catch (e) { return fail(`Startup-folder install failed: ${(e as Error).message}`); } // Detached: the VBS is a forever-restart loop — never wait on it (hangs install/start/restart). if (!isRunning(spec)) { const launched = launchDetached("wscript.exe", [startupVbs]); if (!launched.ok) return fail(`Installed launcher but failed to start: ${launched.out}`); } return ok( `Installed via the Startup folder — starts hidden at logon, no admin needed — and launched it.\n` + `(Tip: run "grok-tg install" from an elevated terminal to use a hidden Scheduled Task instead.)`, ); }, async uninstall(spec) { await this.stop(spec); runSafe("schtasks", ["/Delete", "/F", "/TN", taskName(spec)]); // best-effort (may not exist) rmSync(vbsPath(spec), { force: true }); const startupVbs = startupVbsPath(spec); if (startupVbs) rmSync(startupVbs, { force: true }); return ok(`Removed "${taskName(spec)}" (scheduled task and/or Startup launcher).`); }, async start(spec) { if (isRunning(spec)) return ok("Already running."); if (taskInstalled(spec)) { // schtasks /Run returns once the task is queued (does not wait for the bot). const res = runSafe("schtasks", ["/Run", "/TN", taskName(spec)]); return res.ok ? ok("Started.") : fail(res.out); } const startupVbs = startupVbsPath(spec); if (startupVbs && existsSync(startupVbs)) { // Forever-restart VBS — must be detached or this CLI never returns. const launched = launchDetached("wscript.exe", [startupVbs]); return launched.ok ? ok("Started.") : fail(launched.out); } // Local run-service.vbs in the package dir (task points here when elevated). const local = vbsPath(spec); if (existsSync(local)) { const launched = launchDetached("wscript.exe", [local]); return launched.ok ? ok("Started.") : fail(launched.out); } return fail(`Not installed. Run "grok-tg install" first.`); }, async stop(spec) { runSafe("schtasks", ["/End", "/TN", taskName(spec)]); // best-effort if task-based const res = runSafe("powershell", ["-NoProfile", "-Command", killScript(entryOf(spec))]); return ok(`Stopped. ${res.out.trim()}`); }, async status(spec) { const installedTask = taskInstalled(spec); const startupVbs = startupVbsPath(spec); const installedStartup = !!startupVbs && existsSync(startupVbs); const installed = installedTask || installedStartup; const running = isRunning(spec); const how = installedTask ? "scheduled task" : installedStartup ? "Startup folder" : "—"; const detail = installedTask ? `\n${runSafe("schtasks", ["/Query", "/TN", taskName(spec), "/FO", "LIST"]).out.trim()}` : installedStartup ? `\nLauncher: ${startupVbs}` : ""; return ok( `Installed: ${installed ? `yes (${how})` : "no"} | Running: ${running ? "yes" : "no"}${detail}`, ); }, }; /** The bot entry file — unique enough to identify the bot process. It may be * followed by trailing args (e.g. `--instance