import { HostProcessPlatform } from "@t3tools/shared/hostProcess";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, Flag, GlobalFlag, Prompt } from "effect/unstable/cli"; import packageJson from "../../package.json" with { type: "json" };import * as BootService from "../cloud/bootService.ts";import { compareExactServiceVersions } from "../cloud/serviceProtocol.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* (options?: { readonly allowDowngrade?: boolean;}) { const service = yield* BootService.BootService; const status = yield* service.status; if (status.installed && status.current) { return { changed: false, status } satisfies ServiceReconcileResult; } if ( status.installedVersion !== undefined && options?.allowDowngrade !== true && compareExactServiceVersions(packageJson.version, status.installedVersion) < 0 ) { return yield* new BootService.BootServiceDowngradeRefusedError({ installedVersion: status.installedVersion, targetVersion: packageJson.version, }); } const plan = yield* service.install(options); 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, macOS with launchd"; } if (!status.installed) { return "T3 Code service\n Status: not installed\n Next: Run `t3 service install`."; } const installedVersion = status.installedVersion ?? cliVersion; const problems = (status.problems ?? []).map( (problem) => ` [${problem}] ${BootService.formatBootServiceProblem(problem)}`, ); if ( !status.current && status.installedVersion !== undefined && compareExactServiceVersions(status.installedVersion, cliVersion) > 0 ) { return [ "T3 Code service", ` Status: installed · t3@${installedVersion} (newer than this t3@${cliVersion} CLI)`, ` Unit: ${status.unitPath}`, ` Logs: ${status.logPath}`, ...problems, ` Next: Use \`npx t3@${installedVersion} service update\` to repair it, or pass \`--allow-downgrade\` explicitly.`, ].join("\n"); } return [ "T3 Code service", ` Status: ${status.current ? `installed · t3@${installedVersion}` : "needs an update or repair"}`, ` Unit: ${status.unitPath}`, ` Logs: ${status.logPath}`, ...problems, ...(status.current ? [] : [` Next: Run \`npx t3@${cliVersion} 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 serviceReconcileFlags = { ...projectLocationFlags, allowDowngrade: Flag.boolean("allow-downgrade").pipe( Flag.withDescription("Allow replacing a newer installed service with this older CLI version."), Flag.withDefault(false), ),}; const serviceInstallCommand = Command.make("install", serviceReconcileFlags).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({ allowDowngrade: flags.allowDowngrade }); 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", serviceReconcileFlags).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({ allowDowngrade: flags.allowDowngrade }); 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 status = yield* service.status; const { supported, installed, current } = 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; } for (const problem of status.problems ?? []) { yield* Console.warn(`[${problem}] ${BootService.formatBootServiceProblem(problem)}`); } if ( installed && status.installedVersion !== undefined && compareExactServiceVersions(status.installedVersion, packageJson.version) > 0 ) { yield* Console.log( `A newer t3@${status.installedVersion} background service is installed. Leaving it unchanged.`, ); // This CLI cannot verify the newer service. Keep the manual fallback available. return false; } // A LaunchAgent starts at login and dies at logout; there is no // enable-linger equivalent on macOS. Do not promise more than that. const platform = yield* HostProcessPlatform; const wanted = yield* Prompt.run( Prompt.confirm({ message: installed ? "The installed T3 Code service needs an update or repair. Update it now?" : platform === "darwin" ? "Run T3 Code in the background whenever you log in to this Mac? " + "It stays reachable through T3 Connect while you are logged in." : "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)), BootServicePrerequisiteError: (error) => Console.warn(`Background setup did not finish: ${error.message}`).pipe(Effect.as(false)), BootServiceUpdatePendingError: (error) => Console.warn(`Background setup did not finish: ${error.message}`).pipe(Effect.as(false)), BootServiceDowngradeRefusedError: (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, ]),);
import { HostProcessPlatform } from "@t3tools/shared/hostProcess";
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, Flag, GlobalFlag, Prompt } from "effect/unstable/cli";
import packageJson from "../../package.json" with { type: "json" };
import * as BootService from "../cloud/bootService.ts";
import { compareExactServiceVersions } from "../cloud/serviceProtocol.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* (options?: {
readonly allowDowngrade?: boolean;
}) {
const service = yield* BootService.BootService;
const status = yield* service.status;
if (status.installed && status.current) {
return { changed: false, status } satisfies ServiceReconcileResult;
}
if (
status.installedVersion !== undefined &&
options?.allowDowngrade !== true &&
compareExactServiceVersions(packageJson.version, status.installedVersion) < 0
) {
return yield* new BootService.BootServiceDowngradeRefusedError({
installedVersion: status.installedVersion,
targetVersion: packageJson.version,
});
}
const plan = yield* service.install(options);
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, macOS with launchd";
}
if (!status.installed) {
return "T3 Code service\n Status: not installed\n Next: Run `t3 service install`.";
}
const installedVersion = status.installedVersion ?? cliVersion;
const problems = (status.problems ?? []).map(
(problem) => ` [${problem}] ${BootService.formatBootServiceProblem(problem)}`,
);
if (
!status.current &&
status.installedVersion !== undefined &&
compareExactServiceVersions(status.installedVersion, cliVersion) > 0
) {
return [
"T3 Code service",
` Status: installed · t3@${installedVersion} (newer than this t3@${cliVersion} CLI)`,
` Unit: ${status.unitPath}`,
` Logs: ${status.logPath}`,
...problems,
` Next: Use \`npx t3@${installedVersion} service update\` to repair it, or pass \`--allow-downgrade\` explicitly.`,
].join("\n");
}
return [
"T3 Code service",
` Status: ${status.current ? `installed · t3@${installedVersion}` : "needs an update or repair"}`,
` Unit: ${status.unitPath}`,
` Logs: ${status.logPath}`,
...problems,
...(status.current ? [] : [` Next: Run \`npx t3@${cliVersion} 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 serviceReconcileFlags = {
...projectLocationFlags,
allowDowngrade: Flag.boolean("allow-downgrade").pipe(
Flag.withDescription("Allow replacing a newer installed service with this older CLI version."),
Flag.withDefault(false),
),
};
const serviceInstallCommand = Command.make("install", serviceReconcileFlags).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({ allowDowngrade: flags.allowDowngrade });
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", serviceReconcileFlags).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({ allowDowngrade: flags.allowDowngrade });
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 status = yield* service.status;
const { supported, installed, current } = 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;
}
for (const problem of status.problems ?? []) {
yield* Console.warn(`[${problem}] ${BootService.formatBootServiceProblem(problem)}`);
}
if (
installed &&
status.installedVersion !== undefined &&
compareExactServiceVersions(status.installedVersion, packageJson.version) > 0
) {
yield* Console.log(
`A newer t3@${status.installedVersion} background service is installed. Leaving it unchanged.`,
);
// This CLI cannot verify the newer service. Keep the manual fallback available.
return false;
}
// A LaunchAgent starts at login and dies at logout; there is no
// enable-linger equivalent on macOS. Do not promise more than that.
const platform = yield* HostProcessPlatform;
const wanted = yield* Prompt.run(
Prompt.confirm({
message: installed
? "The installed T3 Code service needs an update or repair. Update it now?"
: platform === "darwin"
? "Run T3 Code in the background whenever you log in to this Mac? " +
"It stays reachable through T3 Connect while you are logged in."
: "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)),
BootServicePrerequisiteError: (error) =>
Console.warn(`Background setup did not finish: ${error.message}`).pipe(Effect.as(false)),
BootServiceUpdatePendingError: (error) =>
Console.warn(`Background setup did not finish: ${error.message}`).pipe(Effect.as(false)),
BootServiceDowngradeRefusedError: (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,
]),
);