199 lines · 6.9 KB · TypeScript
RawDownload
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* <A, E>(  flags: { readonly baseDir: Parameters<typeof resolveCliAuthConfig>[0]["baseDir"] },  run: Effect.Effect<A, E, BootService.BootService>,) {  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 = <R>(  offer: Effect.Effect<boolean, BootService.BootServiceError | Terminal.QuitError, R>,) =>  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,  ]),);