import * as Context from "effect/Context";import * as Config from "effect/Config";import * as DateTime from "effect/DateTime";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as Schema from "effect/Schema"; import { HostProcessArguments, HostProcessExecutablePath, HostProcessPlatform,} from "@t3tools/shared/hostProcess"; import * as ProcessRunner from "../processRunner.ts";import { ensurePinnedRuntimeInstalled, pinnedRuntimePaths } from "./pinnedRuntime.ts"; /** * Installs T3 Code as a per-user boot service. Linux-only for now: systemd * user unit + loginctl enable-linger. The service runs a stable or pinned * runtime — never an ephemeral `npx t3` cache whose eviction could break * startup. */ const BOOT_SERVICE_NAME = "t3code"; export const BOOT_SERVICE_UNIT_FILE = `${BOOT_SERVICE_NAME}.service`;export const BOOT_SERVICE_UNIT_ENV = "T3_BOOT_SERVICE_UNIT"; const EPHEMERAL_CACHE_SEGMENTS = [ "/_npx/", // npx "\\_npx\\", "/pnpm/dlx/", // pnpm dlx (~/.cache/pnpm/dlx and $PNPM_HOME/.pnpm/dlx) "/.pnpm/dlx/", "/.bun/install/cache/", // bunx]; /** * `npx t3` (and pnpm dlx / bunx) run out of ephemeral package-manager * caches that can be evicted at any time — a boot service must never point * there. Global installs, repo checkouts, and the pinned runtime below are * all stable. */export function isEphemeralCacheEntry(entryPath: string): boolean { return EPHEMERAL_CACHE_SEGMENTS.some((segment) => entryPath.includes(segment));} /** * systemd expands `%` specifiers in most directive values, including the * `append:` file paths, which take the rest of the line literally and must * NOT be quoted. */export function escapeSystemdSpecifiers(value: string): string { return value.replaceAll("%", "%%");} /** * systemd word-splits ExecStart and Environment values and expands `%` * specifiers, so paths with spaces or percents must be quoted and escaped. */export function quoteSystemdValue(value: string): string { const escaped = escapeSystemdSpecifiers(value); return /[\s"'\\]/.test(escaped) ? `"${escaped.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"` : escaped;} export interface BootServicePlan { /** Absolute path of the node binary running this CLI. */ readonly nodePath: string; /** Absolute path of the pinned t3 entry point the unit will run. */ readonly t3EntryPath: string; readonly baseDir: string; readonly logPath: string; readonly unitPath: string;} /** * Pure so it is testable byte-for-byte. systemd user units run with a * minimal environment: every path must be absolute, and the service must * not rely on PATH, nvm shims, or shell profiles. Failures land in * `logPath` because `systemctl --user` failures are otherwise invisible. */export function renderBootServiceUnit(plan: BootServicePlan): string { // No After=network-online.target: it does not exist in the systemd *user* // manager, so ordering on it is silently ignored. The server retries its // relay connection, and Restart=always covers early-boot failures. return [ "[Unit]", "Description=T3 Code server", // Give up after 5 crashes in 5 minutes so a persistently broken install // (deleted runtime, broken workspace) stops instead of restarting every // 5s forever and growing the unrotated append log without bound. "StartLimitIntervalSec=300", "StartLimitBurst=5", "", "[Service]", "Type=simple", "WorkingDirectory=%h", `Environment=T3CODE_HOME=${quoteSystemdValue(plan.baseDir)}`, `Environment=${BOOT_SERVICE_UNIT_ENV}=${BOOT_SERVICE_UNIT_FILE}`, `ExecStart=${quoteSystemdValue(plan.nodePath)} ${quoteSystemdValue(plan.t3EntryPath)} serve`, "Restart=always", "RestartSec=5", `StandardOutput=append:${escapeSystemdSpecifiers(plan.logPath)}`, `StandardError=append:${escapeSystemdSpecifiers(plan.logPath)}`, "", "[Install]", "WantedBy=default.target", "", ].join("\n");} export class BootServiceUnsupportedError extends Schema.TaggedErrorClass<BootServiceUnsupportedError>()( "BootServiceUnsupportedError", { platform: Schema.String },) { override get message(): string { return `Background setup currently supports Linux with systemd; this machine reports '${this.platform}'.`; }} export class BootServiceCommandError extends Schema.TaggedErrorClass<BootServiceCommandError>()( "BootServiceCommandError", { step: Schema.String, exitCode: Schema.optional(Schema.Number), stdoutLength: Schema.optional(Schema.Number), stderrLength: Schema.optional(Schema.Number), cause: Schema.optional(Schema.Defect()), },) { override get message(): string { return this.exitCode === undefined ? `Background setup failed while ${this.step}.` : `Background setup failed while ${this.step} (exit code ${this.exitCode}).`; }} export class BootServiceInstallError extends Schema.TaggedErrorClass<BootServiceInstallError>()( "BootServiceInstallError", { cause: Schema.Defect() },) { override get message(): string { return "Could not set up the T3 Code background service."; }} export type BootServiceError = | BootServiceUnsupportedError | BootServiceCommandError | BootServiceInstallError; export interface BootServiceStatus { readonly supported: boolean; readonly installed: boolean; /** False when the installed unit no longer matches what install would write. */ readonly current: boolean; readonly unitPath: string; readonly logPath: string;} export class BootService extends Context.Service< BootService, { /** Installs the pinned runtime + unit, enables linger, starts the service. */ readonly install: Effect.Effect<BootServicePlan, BootServiceError>; /** * Stops and removes the unit; leaves the pinned runtime for reuse. * Returns whether a unit was actually removed. */ readonly uninstall: Effect.Effect<boolean, BootServiceError>; readonly status: Effect.Effect<BootServiceStatus, BootServiceError>; }>()("t3/cloud/bootService") {} export interface BootServiceHost { readonly execPath: string; readonly cliEntryPath: string;} export const make = Effect.fn("cloud.boot_service.make")(function* (input: { readonly baseDir: string; readonly logsDir: string; readonly cliVersion: string; readonly host?: BootServiceHost;}) { const hostExecPath = yield* HostProcessExecutablePath; const hostArguments = yield* HostProcessArguments; const host = input.host ?? { execPath: hostExecPath, // When running the packed CLI this is dist/bin.mjs; when stable (global // install, repo checkout) the boot service runs this same artifact. cliEntryPath: hostArguments[1] ?? "", }; const platform = yield* HostProcessPlatform; const homeDir = yield* Config.string("HOME").pipe(Config.withDefault("")); const fs = yield* FileSystem.FileSystem; const path = yield* Path.Path; const runner = yield* ProcessRunner.ProcessRunner; const unitDir = path.join(homeDir, ".config", "systemd", "user"); const unitPath = path.join(unitDir, BOOT_SERVICE_UNIT_FILE); const logPath = path.join(input.logsDir, "boot-service.log"); const runtimePaths = pinnedRuntimePaths(path, input.baseDir, input.cliVersion); const requireSystemdLinux = Effect.gen(function* () { if (platform !== "linux" || homeDir === "") { return yield* new BootServiceUnsupportedError({ platform }); } }); const runStep = Effect.fn("cloud.boot_service.run_step")(function* ( step: string, command: string, args: ReadonlyArray<string>, options?: { readonly timeout?: Duration.Input }, ) { return yield* runner.run({ command, args, timeout: options?.timeout }).pipe( Effect.mapError((cause) => new BootServiceCommandError({ step, cause })), Effect.filterOrFail( (result) => result.code === 0, (result) => new BootServiceCommandError({ step, exitCode: Number(result.code), stdoutLength: result.stdout.length, stderrLength: result.stderr.length, }), ), Effect.tapError((error) => DateTime.now.pipe( Effect.flatMap((now) => fs.writeFileString(logPath, `${DateTime.formatIso(now)} ${error.message}\n`, { flag: "a", }), ), Effect.ignore, ), ), ); }); /** * Ensures plannedEntryPath exists before the unit points at it. A stable * install (global bin, repo checkout) is used as-is; an ephemeral cache * entry is replaced by `npm install --prefix`-ing the exact running * version into <baseDir>/runtime/versions/<v>. A real install (not a copy * of bin.mjs) because t3 ships native deps like node-pty. */ const ensurePinnedRuntime = Effect.gen(function* () { if (!isEphemeralCacheEntry(host.cliEntryPath)) { return; } yield* ensurePinnedRuntimeInstalled({ baseDir: input.baseDir, version: input.cliVersion, fs, path, runner, }).pipe( Effect.mapError((error) => error.step.startsWith("installing") ? new BootServiceCommandError({ step: error.step, exitCode: error.exitCode, stdoutLength: error.stdoutLength, stderrLength: error.stderrLength, cause: error.cause, }) : new BootServiceInstallError({ cause: error }), ), Effect.tapError((error) => DateTime.now.pipe( Effect.flatMap((now) => fs.writeFileString(logPath, `${DateTime.formatIso(now)} ${error.message}\n`, { flag: "a", }), ), Effect.ignore, ), ), ); }); // Where the unit will point: derivable without touching the network, so // status can compare units purely; install materializes it first. const plannedEntryPath = isEphemeralCacheEntry(host.cliEntryPath) ? runtimePaths.entryPath : host.cliEntryPath; const plan: BootServicePlan = { nodePath: host.execPath, t3EntryPath: plannedEntryPath, baseDir: input.baseDir, logPath, unitPath, }; const install: BootService["Service"]["install"] = Effect.gen(function* () { yield* requireSystemdLinux; yield* fs .makeDirectory(input.logsDir, { recursive: true }) .pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause }))); yield* ensurePinnedRuntime; const previousUnit = yield* fs.exists(unitPath).pipe( Effect.flatMap((exists) => exists ? fs.readFileString(unitPath).pipe(Effect.map(Option.some)) : Effect.succeed(Option.none<string>()), ), Effect.mapError((cause) => new BootServiceInstallError({ cause })), ); yield* fs.makeDirectory(unitDir, { recursive: true }).pipe( Effect.andThen(fs.writeFileString(unitPath, renderBootServiceUnit(plan))), Effect.mapError((cause) => new BootServiceInstallError({ cause })), ); // If any activation step fails, remove the unit again: a leftover file // would make service status report it as installed even though it was // never enabled or lingered. yield* Effect.gen(function* () { yield* runStep("reloading systemd user units", "systemctl", ["--user", "daemon-reload"]); yield* runStep("enabling the service", "systemctl", [ "--user", "enable", BOOT_SERVICE_UNIT_FILE, ]); // restart rather than enable --now: --now does not replace an already // running process, so repairing a stale unit would leave the old // server running until reboot. restart also starts a stopped service. yield* runStep("starting the service", "systemctl", [ "--user", "restart", BOOT_SERVICE_UNIT_FILE, ]); // Linger keeps the user manager (and this service) running without an // open session — the whole point on a box reached over SSH. No // username argument: loginctl defaults to the calling user, which is // always right, while $USER can be stale (su without -l) or unset. yield* runStep("enabling lingering for this user", "loginctl", ["enable-linger"]); }).pipe(Effect.tapError(() => rollbackFailedInstall(previousUnit))); return plan; }).pipe(Effect.withSpan("cloud.boot_service.install")); // If activation fails partway (e.g. enable succeeds but restart/linger // fails), leave nothing behind: disable removes the enable symlink, remove // deletes the file, daemon-reload clears the stale definition — otherwise a // dangling wants/ symlink logs "Failed to load unit" at every boot and the // next lifecycle command misreports the state. const rollbackFailedInstall = Effect.fn("cloud.boot_service.rollback_failed_install")(function* ( previousUnit: Option.Option<string>, ) { if (Option.isSome(previousUnit)) { yield* fs.writeFileString(unitPath, previousUnit.value).pipe(Effect.ignore); } else { yield* runStep("cleaning up the service", "systemctl", [ "--user", "disable", "--now", BOOT_SERVICE_UNIT_FILE, ]).pipe(Effect.ignore); yield* fs.remove(unitPath).pipe(Effect.ignore); } yield* runStep("reloading systemd user units", "systemctl", ["--user", "daemon-reload"]).pipe( Effect.ignore, ); if (Option.isSome(previousUnit)) { yield* runStep("restoring the previous service", "systemctl", [ "--user", "restart", BOOT_SERVICE_UNIT_FILE, ]).pipe(Effect.ignore); } }); const uninstall: BootService["Service"]["uninstall"] = Effect.gen(function* () { yield* requireSystemdLinux; const exists = yield* fs .exists(unitPath) .pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause }))); if (!exists) { return false; } yield* runStep("stopping the service", "systemctl", [ "--user", "disable", "--now", BOOT_SERVICE_UNIT_FILE, ]); yield* fs .remove(unitPath) .pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause }))); yield* runStep("reloading systemd user units", "systemctl", ["--user", "daemon-reload"]); return true; }).pipe(Effect.withSpan("cloud.boot_service.uninstall")); const status: BootService["Service"]["status"] = Effect.gen(function* () { if (platform !== "linux" || homeDir === "") { return { supported: false, installed: false, current: false, unitPath, logPath }; } const unitExists = yield* fs.exists(unitPath); if (!unitExists) { return { supported: true, installed: false, current: false, unitPath, logPath }; } const unit = yield* fs.readFileString(unitPath); // A unit is current only if it matches what install would write now (an // older CLI wrote a different runtime/node path) AND the entry point it // references still exists (a pinned runtime under ~/.t3 can be deleted to // reclaim space). Either mismatch makes connect offer a repair. const entryExists = yield* fs.exists(plannedEntryPath); const current = unit === renderBootServiceUnit(plan) && entryExists; return { supported: true, installed: true, current, unitPath, logPath }; }).pipe( Effect.mapError((cause) => new BootServiceInstallError({ cause })), Effect.withSpan("cloud.boot_service.status"), ); return BootService.of({ install, uninstall, status });}); export const layer = (input: { readonly baseDir: string; readonly logsDir: string; readonly cliVersion: string; readonly host?: BootServiceHost;}) => Layer.effect(BootService, make(input));
import * as Context from "effect/Context";
import * as Config from "effect/Config";
import * as DateTime from "effect/DateTime";
import * as Duration from "effect/Duration";
import * as Effect from "effect/Effect";
import * as FileSystem from "effect/FileSystem";
import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as Path from "effect/Path";
import * as Schema from "effect/Schema";
import {
HostProcessArguments,
HostProcessExecutablePath,
HostProcessPlatform,
} from "@t3tools/shared/hostProcess";
import * as ProcessRunner from "../processRunner.ts";
import { ensurePinnedRuntimeInstalled, pinnedRuntimePaths } from "./pinnedRuntime.ts";
/**
* Installs T3 Code as a per-user boot service. Linux-only for now: systemd
* user unit + loginctl enable-linger. The service runs a stable or pinned
* runtime — never an ephemeral `npx t3` cache whose eviction could break
* startup.
*/
const BOOT_SERVICE_NAME = "t3code";
export const BOOT_SERVICE_UNIT_FILE = `${BOOT_SERVICE_NAME}.service`;
export const BOOT_SERVICE_UNIT_ENV = "T3_BOOT_SERVICE_UNIT";
const EPHEMERAL_CACHE_SEGMENTS = [
"/_npx/", // npx
"\\_npx\\",
"/pnpm/dlx/", // pnpm dlx (~/.cache/pnpm/dlx and $PNPM_HOME/.pnpm/dlx)
"/.pnpm/dlx/",
"/.bun/install/cache/", // bunx
];
/**
* `npx t3` (and pnpm dlx / bunx) run out of ephemeral package-manager
* caches that can be evicted at any time — a boot service must never point
* there. Global installs, repo checkouts, and the pinned runtime below are
* all stable.
*/
export function isEphemeralCacheEntry(entryPath: string): boolean {
return EPHEMERAL_CACHE_SEGMENTS.some((segment) => entryPath.includes(segment));
}
/**
* systemd expands `%` specifiers in most directive values, including the
* `append:` file paths, which take the rest of the line literally and must
* NOT be quoted.
*/
export function escapeSystemdSpecifiers(value: string): string {
return value.replaceAll("%", "%%");
}
/**
* systemd word-splits ExecStart and Environment values and expands `%`
* specifiers, so paths with spaces or percents must be quoted and escaped.
*/
export function quoteSystemdValue(value: string): string {
const escaped = escapeSystemdSpecifiers(value);
return /[\s"'\\]/.test(escaped)
? `"${escaped.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`
: escaped;
}
export interface BootServicePlan {
/** Absolute path of the node binary running this CLI. */
readonly nodePath: string;
/** Absolute path of the pinned t3 entry point the unit will run. */
readonly t3EntryPath: string;
readonly baseDir: string;
readonly logPath: string;
readonly unitPath: string;
}
/**
* Pure so it is testable byte-for-byte. systemd user units run with a
* minimal environment: every path must be absolute, and the service must
* not rely on PATH, nvm shims, or shell profiles. Failures land in
* `logPath` because `systemctl --user` failures are otherwise invisible.
*/
export function renderBootServiceUnit(plan: BootServicePlan): string {
// No After=network-online.target: it does not exist in the systemd *user*
// manager, so ordering on it is silently ignored. The server retries its
// relay connection, and Restart=always covers early-boot failures.
return [
"[Unit]",
"Description=T3 Code server",
// Give up after 5 crashes in 5 minutes so a persistently broken install
// (deleted runtime, broken workspace) stops instead of restarting every
// 5s forever and growing the unrotated append log without bound.
"StartLimitIntervalSec=300",
"StartLimitBurst=5",
"",
"[Service]",
"Type=simple",
"WorkingDirectory=%h",
`Environment=T3CODE_HOME=${quoteSystemdValue(plan.baseDir)}`,
`Environment=${BOOT_SERVICE_UNIT_ENV}=${BOOT_SERVICE_UNIT_FILE}`,
`ExecStart=${quoteSystemdValue(plan.nodePath)} ${quoteSystemdValue(plan.t3EntryPath)} serve`,
"Restart=always",
"RestartSec=5",
`StandardOutput=append:${escapeSystemdSpecifiers(plan.logPath)}`,
`StandardError=append:${escapeSystemdSpecifiers(plan.logPath)}`,
"",
"[Install]",
"WantedBy=default.target",
"",
].join("\n");
}
export class BootServiceUnsupportedError extends Schema.TaggedErrorClass<BootServiceUnsupportedError>()(
"BootServiceUnsupportedError",
{ platform: Schema.String },
) {
override get message(): string {
return `Background setup currently supports Linux with systemd; this machine reports '${this.platform}'.`;
}
}
export class BootServiceCommandError extends Schema.TaggedErrorClass<BootServiceCommandError>()(
"BootServiceCommandError",
{
step: Schema.String,
exitCode: Schema.optional(Schema.Number),
stdoutLength: Schema.optional(Schema.Number),
stderrLength: Schema.optional(Schema.Number),
cause: Schema.optional(Schema.Defect()),
},
) {
override get message(): string {
return this.exitCode === undefined
? `Background setup failed while ${this.step}.`
: `Background setup failed while ${this.step} (exit code ${this.exitCode}).`;
}
}
export class BootServiceInstallError extends Schema.TaggedErrorClass<BootServiceInstallError>()(
"BootServiceInstallError",
{ cause: Schema.Defect() },
) {
override get message(): string {
return "Could not set up the T3 Code background service.";
}
}
export type BootServiceError =
| BootServiceUnsupportedError
| BootServiceCommandError
| BootServiceInstallError;
export interface BootServiceStatus {
readonly supported: boolean;
readonly installed: boolean;
/** False when the installed unit no longer matches what install would write. */
readonly current: boolean;
readonly unitPath: string;
readonly logPath: string;
}
export class BootService extends Context.Service<
BootService,
{
/** Installs the pinned runtime + unit, enables linger, starts the service. */
readonly install: Effect.Effect<BootServicePlan, BootServiceError>;
/**
* Stops and removes the unit; leaves the pinned runtime for reuse.
* Returns whether a unit was actually removed.
*/
readonly uninstall: Effect.Effect<boolean, BootServiceError>;
readonly status: Effect.Effect<BootServiceStatus, BootServiceError>;
}
>()("t3/cloud/bootService") {}
export interface BootServiceHost {
readonly execPath: string;
readonly cliEntryPath: string;
}
export const make = Effect.fn("cloud.boot_service.make")(function* (input: {
readonly baseDir: string;
readonly logsDir: string;
readonly cliVersion: string;
readonly host?: BootServiceHost;
}) {
const hostExecPath = yield* HostProcessExecutablePath;
const hostArguments = yield* HostProcessArguments;
const host = input.host ?? {
execPath: hostExecPath,
// When running the packed CLI this is dist/bin.mjs; when stable (global
// install, repo checkout) the boot service runs this same artifact.
cliEntryPath: hostArguments[1] ?? "",
};
const platform = yield* HostProcessPlatform;
const homeDir = yield* Config.string("HOME").pipe(Config.withDefault(""));
const fs = yield* FileSystem.FileSystem;
const path = yield* Path.Path;
const runner = yield* ProcessRunner.ProcessRunner;
const unitDir = path.join(homeDir, ".config", "systemd", "user");
const unitPath = path.join(unitDir, BOOT_SERVICE_UNIT_FILE);
const logPath = path.join(input.logsDir, "boot-service.log");
const runtimePaths = pinnedRuntimePaths(path, input.baseDir, input.cliVersion);
const requireSystemdLinux = Effect.gen(function* () {
if (platform !== "linux" || homeDir === "") {
return yield* new BootServiceUnsupportedError({ platform });
}
});
const runStep = Effect.fn("cloud.boot_service.run_step")(function* (
step: string,
command: string,
args: ReadonlyArray<string>,
options?: { readonly timeout?: Duration.Input },
) {
return yield* runner.run({ command, args, timeout: options?.timeout }).pipe(
Effect.mapError((cause) => new BootServiceCommandError({ step, cause })),
Effect.filterOrFail(
(result) => result.code === 0,
(result) =>
new BootServiceCommandError({
step,
exitCode: Number(result.code),
stdoutLength: result.stdout.length,
stderrLength: result.stderr.length,
}),
),
Effect.tapError((error) =>
DateTime.now.pipe(
Effect.flatMap((now) =>
fs.writeFileString(logPath, `${DateTime.formatIso(now)} ${error.message}\n`, {
flag: "a",
}),
),
Effect.ignore,
),
),
);
});
/**
* Ensures plannedEntryPath exists before the unit points at it. A stable
* install (global bin, repo checkout) is used as-is; an ephemeral cache
* entry is replaced by `npm install --prefix`-ing the exact running
* version into <baseDir>/runtime/versions/<v>. A real install (not a copy
* of bin.mjs) because t3 ships native deps like node-pty.
*/
const ensurePinnedRuntime = Effect.gen(function* () {
if (!isEphemeralCacheEntry(host.cliEntryPath)) {
return;
}
yield* ensurePinnedRuntimeInstalled({
baseDir: input.baseDir,
version: input.cliVersion,
fs,
path,
runner,
}).pipe(
Effect.mapError((error) =>
error.step.startsWith("installing")
? new BootServiceCommandError({
step: error.step,
exitCode: error.exitCode,
stdoutLength: error.stdoutLength,
stderrLength: error.stderrLength,
cause: error.cause,
})
: new BootServiceInstallError({ cause: error }),
),
Effect.tapError((error) =>
DateTime.now.pipe(
Effect.flatMap((now) =>
fs.writeFileString(logPath, `${DateTime.formatIso(now)} ${error.message}\n`, {
flag: "a",
}),
),
Effect.ignore,
),
),
);
});
// Where the unit will point: derivable without touching the network, so
// status can compare units purely; install materializes it first.
const plannedEntryPath = isEphemeralCacheEntry(host.cliEntryPath)
? runtimePaths.entryPath
: host.cliEntryPath;
const plan: BootServicePlan = {
nodePath: host.execPath,
t3EntryPath: plannedEntryPath,
baseDir: input.baseDir,
logPath,
unitPath,
};
const install: BootService["Service"]["install"] = Effect.gen(function* () {
yield* requireSystemdLinux;
yield* fs
.makeDirectory(input.logsDir, { recursive: true })
.pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause })));
yield* ensurePinnedRuntime;
const previousUnit = yield* fs.exists(unitPath).pipe(
Effect.flatMap((exists) =>
exists
? fs.readFileString(unitPath).pipe(Effect.map(Option.some))
: Effect.succeed(Option.none<string>()),
),
Effect.mapError((cause) => new BootServiceInstallError({ cause })),
);
yield* fs.makeDirectory(unitDir, { recursive: true }).pipe(
Effect.andThen(fs.writeFileString(unitPath, renderBootServiceUnit(plan))),
Effect.mapError((cause) => new BootServiceInstallError({ cause })),
);
// If any activation step fails, remove the unit again: a leftover file
// would make service status report it as installed even though it was
// never enabled or lingered.
yield* Effect.gen(function* () {
yield* runStep("reloading systemd user units", "systemctl", ["--user", "daemon-reload"]);
yield* runStep("enabling the service", "systemctl", [
"--user",
"enable",
BOOT_SERVICE_UNIT_FILE,
]);
// restart rather than enable --now: --now does not replace an already
// running process, so repairing a stale unit would leave the old
// server running until reboot. restart also starts a stopped service.
yield* runStep("starting the service", "systemctl", [
"--user",
"restart",
BOOT_SERVICE_UNIT_FILE,
]);
// Linger keeps the user manager (and this service) running without an
// open session — the whole point on a box reached over SSH. No
// username argument: loginctl defaults to the calling user, which is
// always right, while $USER can be stale (su without -l) or unset.
yield* runStep("enabling lingering for this user", "loginctl", ["enable-linger"]);
}).pipe(Effect.tapError(() => rollbackFailedInstall(previousUnit)));
return plan;
}).pipe(Effect.withSpan("cloud.boot_service.install"));
// If activation fails partway (e.g. enable succeeds but restart/linger
// fails), leave nothing behind: disable removes the enable symlink, remove
// deletes the file, daemon-reload clears the stale definition — otherwise a
// dangling wants/ symlink logs "Failed to load unit" at every boot and the
// next lifecycle command misreports the state.
const rollbackFailedInstall = Effect.fn("cloud.boot_service.rollback_failed_install")(function* (
previousUnit: Option.Option<string>,
) {
if (Option.isSome(previousUnit)) {
yield* fs.writeFileString(unitPath, previousUnit.value).pipe(Effect.ignore);
} else {
yield* runStep("cleaning up the service", "systemctl", [
"--user",
"disable",
"--now",
BOOT_SERVICE_UNIT_FILE,
]).pipe(Effect.ignore);
yield* fs.remove(unitPath).pipe(Effect.ignore);
}
yield* runStep("reloading systemd user units", "systemctl", ["--user", "daemon-reload"]).pipe(
Effect.ignore,
);
if (Option.isSome(previousUnit)) {
yield* runStep("restoring the previous service", "systemctl", [
"--user",
"restart",
BOOT_SERVICE_UNIT_FILE,
]).pipe(Effect.ignore);
}
});
const uninstall: BootService["Service"]["uninstall"] = Effect.gen(function* () {
yield* requireSystemdLinux;
const exists = yield* fs
.exists(unitPath)
.pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause })));
if (!exists) {
return false;
}
yield* runStep("stopping the service", "systemctl", [
"--user",
"disable",
"--now",
BOOT_SERVICE_UNIT_FILE,
]);
yield* fs
.remove(unitPath)
.pipe(Effect.mapError((cause) => new BootServiceInstallError({ cause })));
yield* runStep("reloading systemd user units", "systemctl", ["--user", "daemon-reload"]);
return true;
}).pipe(Effect.withSpan("cloud.boot_service.uninstall"));
const status: BootService["Service"]["status"] = Effect.gen(function* () {
if (platform !== "linux" || homeDir === "") {
return { supported: false, installed: false, current: false, unitPath, logPath };
}
const unitExists = yield* fs.exists(unitPath);
if (!unitExists) {
return { supported: true, installed: false, current: false, unitPath, logPath };
}
const unit = yield* fs.readFileString(unitPath);
// A unit is current only if it matches what install would write now (an
// older CLI wrote a different runtime/node path) AND the entry point it
// references still exists (a pinned runtime under ~/.t3 can be deleted to
// reclaim space). Either mismatch makes connect offer a repair.
const entryExists = yield* fs.exists(plannedEntryPath);
const current = unit === renderBootServiceUnit(plan) && entryExists;
return { supported: true, installed: true, current, unitPath, logPath };
}).pipe(
Effect.mapError((cause) => new BootServiceInstallError({ cause })),
Effect.withSpan("cloud.boot_service.status"),
);
return BootService.of({ install, uninstall, status });
});
export const layer = (input: {
readonly baseDir: string;
readonly logsDir: string;
readonly cliVersion: string;
readonly host?: BootServiceHost;
}) => Layer.effect(BootService, make(input));