// src/gadget-tasks.ts // Copyright (C) 2026 Rob Colbert // Licensed under the Apache License, Version 2.0 import env from "./config/env.ts"; import PlatformService from "./services/platform.ts"; import SchedulerService from "./services/scheduler.ts"; import TaskLockService from "./services/lock.ts"; import { GadgetLog, detectWorkspace, type IDroneRegistration, type IProject } from "@gadget/api"; import { GadgetProcess } from "./lib/process.ts"; const log = new GadgetLog({ name: "GadgetTasks", slug: "gadget-tasks" }); interface UserCredentials { email: string; password: string; } class GadgetTasks extends GadgetProcess { private selectedDrone: IDroneRegistration | null = null; private startupDir: string | undefined = undefined; get name(): string { return "GadgetTasks"; } get slug(): string { return "gadget-tasks"; } constructor() { super(); } async start(): Promise { this.hookProcessSignals(); /* * Detect workspace directory by walking up the directory tree. * This allows starting from anywhere within a workspace's hierarchy. */ const detection = await detectWorkspace(); this.startupDir = detection.startupDir; if (!detection.found) { this.log.error("no workspace found", { startupDir: detection.startupDir, message: "Start gadget-tasks from within a managed workspace or its subdirectories. No .gadget/workspace.json found in any parent directory.", }); throw new Error( "No workspace found. Cannot start gadget-tasks in an unmanaged directory.", ); } // Change to the workspace directory for all operations const workspaceDir = detection.workspaceDir!; process.chdir(workspaceDir); this.log.info("workspace detected", { workspaceDir, startupDir: detection.startupDir, navigated: workspaceDir !== detection.startupDir, }); // 1. Acquire singleton lock via Redis const lockAcquired = await TaskLockService.acquire(); if (!lockAcquired) { this.log.error("another instance of gadget-tasks is already running"); process.exit(1); } // 2. Get user credentials (CLI args or interactive prompt) const credentials = await this.getUserCredentials(); // 3. Authenticate with platform await PlatformService.start(); await PlatformService.authenticate(credentials.email, credentials.password); this.log.info("authenticated with platform", { email: credentials.email }); // 4. Select a drone this.selectedDrone = await this.selectDrone(); PlatformService.setSelectedDrone(this.selectedDrone); this.log.info("drone selected", { droneId: this.selectedDrone._id, hostname: this.selectedDrone.hostname, }); // 5. Connect Socket.IO await PlatformService.connectSocket(); // 6. Start scheduler await SchedulerService.start(); // 7. Fetch and schedule active tasks await this.scheduleAllTasks(); this.log.info(`gadget-tasks v${env.pkg.version} started, ${SchedulerService.scheduledCount} tasks scheduled`); } async stop(): Promise { this.log.info(`gadget-tasks v${env.pkg.version} shutting down`); await SchedulerService.stop(); await PlatformService.stop(); await TaskLockService.release(); /* * Restore the original startup directory before exiting. */ if (this.startupDir) { process.chdir(this.startupDir); this.log.info("restored startup directory", { startupDir: this.startupDir }); } this.log.info("shutdown complete"); return 0; } /** * Query all active projects and schedule their enabled tasks. */ async scheduleAllTasks(): Promise { const projects = await PlatformService.getProjects(); const activeProjects = projects.filter((p) => p.status === "active"); let taskCount = 0; for (const project of activeProjects) { for (const task of project.tasks) { if (!task.enabled) continue; SchedulerService.scheduleTask(task, project as unknown as IProject); taskCount++; } } this.log.info("tasks scheduled", { projectCount: activeProjects.length, taskCount, }); } /** * Get user credentials from CLI args or interactive prompt. * Same pattern as gadget-drone startup. */ async getUserCredentials(): Promise { const args = process.argv.slice(2); const userArg = args.find((a) => a.startsWith("--user=")); const passArg = args.find((a) => a.startsWith("--password=")); if (userArg && passArg) { return { email: userArg.split("=")[1], password: passArg.split("=")[1], }; } const { input: inqInput, password: inqPassword } = await import("@inquirer/prompts"); return { email: await inqInput({ message: "📧 Enter Email: " }), password: await inqPassword({ message: "🔑 Enter Password: " }), }; } /** * Select a drone for task execution. If only one is available, * auto-select it. Otherwise, present an interactive selection. */ async selectDrone(): Promise { const drones = await PlatformService.getAvailableDrones(); if (drones.length === 0) { throw new Error("No available drones. Start a gadget-drone instance first."); } if (drones.length === 1) { this.log.info("auto-selecting only available drone", { droneId: drones[0]._id }); return drones[0]; } console.log("\nAvailable Drones:"); drones.forEach((d, i) => { console.log(` ${i + 1}. ${d.hostname} (${d.workspaceId}) - ${d.status}`); }); const { select: inqSelect } = await import("@inquirer/prompts"); const selection = await inqSelect({ message: "Select a drone for task execution:", choices: drones.map((d) => ({ name: `${d.hostname} (${d.workspaceId})`, value: d, })), }); return selection; } hookProcessSignals(): void { process.title = this.name; process.on("unhandledRejection", async (error: Error) => { this.log.error("Unhandled rejection", { error, stack: error.stack }); const exitCode = await this.stop(); process.exit(exitCode); }); process.on("warning", (error) => { if (error.name === "DeprecationWarning") return; this.log.alert("warning", { error }); }); process.on("SIGINT", async () => { this.log.info("SIGINT received"); const exitCode = await this.stop(); process.exit(exitCode); }); } } (async () => { try { const tasks = new GadgetTasks(); await tasks.start(); } catch (error) { console.error("failed to start gadget-tasks", error); process.exit(-1); } })();