import * as Console from "effect/Console"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; import * as Terminal from "effect/Terminal"; import { Command, GlobalFlag, Prompt } from "effect/unstable/cli"; import packageJson from "../../package.json" with { type: "json" }; import * as BootService from "../cloud/bootService.ts"; import type * as ServerConfig from "../config.ts"; import * as ProcessRunner from "../processRunner.ts"; import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts"; export const bootServiceLayer = (config: ServerConfig.ServerConfig["Service"]) => BootService.layer({ baseDir: config.baseDir, logsDir: config.logsDir, cliVersion: packageJson.version, }).pipe(Layer.provide(ProcessRunner.layer)); export type ServiceReconcileResult = | { readonly changed: false; readonly status: BootService.BootServiceStatus; } | { readonly changed: true; readonly previouslyInstalled: boolean; readonly plan: BootService.BootServicePlan; }; /** Install, update, or repair the service using the CLI version running this command. */ export const reconcileService = Effect.fn("cli.service.reconcile")(function* () { const service = yield* BootService.BootService; const status = yield* service.status; if (status.installed && status.current) { return { changed: false, status } satisfies ServiceReconcileResult; } const plan = yield* service.install; return { changed: true, previouslyInstalled: status.installed, plan, } satisfies ServiceReconcileResult; }); export function formatServiceStatus( status: BootService.BootServiceStatus, cliVersion: string, ): string { if (!status.supported) { return "T3 Code service\n Status: unavailable on this machine\n Supported on: Linux with systemd"; } if (!status.installed) { return "T3 Code service\n Status: not installed\n Next: Run `t3 service install`."; } return [ "T3 Code service", ` Status: ${status.current ? `installed ยท t3@${cliVersion}` : "needs an update or repair"}`, ` Unit: ${status.unitPath}`, ` Logs: ${status.logPath}`, ...(status.current ? [] : [" Next: Run `npx t3@latest service update`."]), ].join("\n"); } const runServiceCommand = Effect.fn("cli.service.run")(function* ( flags: { readonly baseDir: Parameters[0]["baseDir"] }, run: Effect.Effect, ) { const logLevel = yield* GlobalFlag.LogLevel; const config = yield* resolveCliAuthConfig(flags, logLevel); return yield* run.pipe(Effect.provide(bootServiceLayer(config))); }); const serviceInstallCommand = Command.make("install", projectLocationFlags).pipe( Command.withDescription("Install T3 Code as a background service for this user."), Command.withHandler((flags) => runServiceCommand( flags, Effect.gen(function* () { const result = yield* reconcileService(); if (!result.changed) { yield* Console.log( `T3 Code service is already installed with t3@${packageJson.version}.`, ); return; } yield* Console.log( `${result.previouslyInstalled ? "Updated" : "Installed"} T3 Code service with t3@${packageJson.version}.\nLogs: ${result.plan.logPath}`, ); }), ), ), ); const serviceUpdateCommand = Command.make("update", projectLocationFlags).pipe( Command.withDescription( "Update or repair the background service using this CLI version. Use `npx t3@latest service update` for the latest release.", ), Command.withHandler((flags) => runServiceCommand( flags, Effect.gen(function* () { const result = yield* reconcileService(); if (!result.changed) { yield* Console.log(`T3 Code service is already using t3@${packageJson.version}.`); return; } yield* Console.log( `${result.previouslyInstalled ? "Updated" : "Installed"} T3 Code service with t3@${packageJson.version}.\nLogs: ${result.plan.logPath}`, ); }), ), ), ); const serviceUninstallCommand = Command.make("uninstall", projectLocationFlags).pipe( Command.withDescription("Stop and remove the T3 Code background service."), Command.withHandler((flags) => runServiceCommand( flags, Effect.gen(function* () { const service = yield* BootService.BootService; const removed = yield* service.uninstall; yield* Console.log( removed ? "Removed the T3 Code service." : "T3 Code service is not installed.", ); }), ), ), ); const serviceStatusCommand = Command.make("status", projectLocationFlags).pipe( Command.withDescription("Show whether the T3 Code background service is installed."), Command.withHandler((flags) => runServiceCommand( flags, Effect.gen(function* () { const service = yield* BootService.BootService; yield* Console.log(formatServiceStatus(yield* service.status, packageJson.version)); }), ), ), ); export const offerServiceDuringOnboarding = Effect.gen(function* () { const service = yield* BootService.BootService; const { supported, installed, current } = yield* service.status; if (!supported) { return false; } if (installed && current) { yield* Console.log("T3 Code is already set up to run in the background on this machine."); return true; } const wanted = yield* Prompt.run( Prompt.confirm({ message: installed ? "The installed T3 Code service needs an update or repair. Update it now?" : "Run T3 Code in the background whenever this machine boots? " + "It stays reachable through T3 Connect even after you log out.", initial: true, }), ); if (!wanted) { return false; } const result = yield* reconcileService(); if (result.changed) { yield* Console.log( `Background service ${result.previouslyInstalled ? "updated" : "installed"}. Logs: ${result.plan.logPath}`, ); } return true; }); export const recoverServiceOnboardingOffer = ( offer: Effect.Effect, ) => offer.pipe( Effect.catchTags({ QuitError: () => Effect.succeed(false), BootServiceUnsupportedError: (error) => Console.log(`Skipping background setup: ${error.message}`).pipe(Effect.as(false)), BootServiceCommandError: (error) => Console.warn(`Background setup did not finish: ${error.message}`).pipe(Effect.as(false)), BootServiceInstallError: (error) => Console.warn(`Background setup did not finish: ${error.message}`).pipe(Effect.as(false)), }), ); export const serviceCommand = Command.make("service").pipe( Command.withDescription("Manage the T3 Code background service."), Command.withSubcommands([ serviceInstallCommand, serviceUninstallCommand, serviceUpdateCommand, serviceStatusCommand, ]), );