Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
114 changes: 114 additions & 0 deletions apps/server/src/cli/service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,17 @@ import {
afterEach(() => vi.restoreAllMocks());

const status = {
schemaVersion: BootService.BOOT_SERVICE_STATUS_SCHEMA_VERSION,
supported: true,
manager: "systemd",
installed: true,
enabled: "enabled",
running: "running",
current: true,
configuredVersion: "0.0.29",
unitPath: "/home/me/.config/systemd/user/t3code.service",
logPath: "/home/me/.t3/userdata/logs/boot-service.log",
observedAt: "2026-09-26T00:00:00.000Z",
} as const;

it("reports the installed service version and host paths", () => {
Expand Down Expand Up @@ -95,6 +101,114 @@ it("reports a newer installed service and tells the CLI to catch up to it", () =
assert.notInclude(output, "npx");
});

const observation = {
manager: "systemd",
source: "systemctl --user show t3code.service",
observedAt: "2026-09-26T00:00:00.000Z",
reachable: true,
enabled: "enabled",
running: "running",
state: "active",
subState: "running",
processId: 4321,
configuredProgramPath: "/home/me/.t3/runtime/versions/0.0.29/t3",
configuredVersion: "0.0.29",
restartCount: 0,
lastResult: "success",
} satisfies BootService.BootServiceManagerObservation;

it("emits the versioned machine-readable status contract with --json", () => {
const parsed = JSON.parse(
formatServiceStatus({ ...status, observation }, "0.0.29", { json: true }),
) as Record<string, unknown>;

expect(parsed.schemaVersion).toBe(BootService.BOOT_SERVICE_STATUS_SCHEMA_VERSION);
expect(parsed.schemaVersion).toBe(2);
expect(parsed.manager).toBe("systemd");
expect(parsed.running).toBe("running");
expect(parsed.cliVersion).toBe("0.0.29");
expect(parsed.unitPath).toBe(status.unitPath);
// Configured launch metadata is separate and there is no observed running
// server version in this contract.
expect(parsed.configuredVersion).toBe("0.0.29");
expect("runningVersion" in parsed).toBe(false);
const observationJson = parsed.observation as Record<string, unknown>;
expect(observationJson.configuredVersion).toBe("0.0.29");
expect(observationJson.configuredProgramPath).toBe("/home/me/.t3/runtime/versions/0.0.29/t3");
expect("runningVersion" in observationJson).toBe(false);
});

it("keeps human status output and adds manager observation lines", () => {
const output = formatServiceStatus({ ...status, observation }, "0.0.29");

expect(output).toContain("Status: installed · t3@0.0.29");
expect(output).toContain("Manager: systemd · running running");
expect(output).toContain("Configured launcher: t3@0.0.29");
expect(output).toContain("Manager state: active/running");
expect(output).toContain("Enabled: enabled");
expect(output).toContain(
"Observed: 2026-09-26T00:00:00.000Z (systemctl --user show t3code.service)",
);
expect(output).not.toContain("Note:");
expect(output).not.toContain("t3@0.0.29 (running)");
});

it("does not report a non-running manager observation as healthy", () => {
const output = formatServiceStatus(
{
...status,
running: "not-loaded",
observation: { ...observation, running: "not-loaded", detail: "launch-agent-not-loaded" },
},
"0.0.29",
);

expect(output).toContain("Manager: systemd · running not loaded");
expect(output).toContain("Manager detail: launch-agent-not-loaded");
expect(output).toContain("that is a manager observation, not application health");
});

it("reports a transitioning manager state without calling it stopped or healthy", () => {
const output = formatServiceStatus(
{
...status,
running: "transitioning",
observation: {
...observation,
running: "transitioning",
state: "deactivating",
subState: "stop-sigterm",
},
},
"0.0.29",
);

expect(output).toContain("Manager: systemd · running changing state");
expect(output).toContain("Manager state: deactivating/stop-sigterm");
expect(output).toContain("that is a manager observation, not application health");
expect(output).not.toContain("running stopped");
});

it("shows the configured program path when it carries no exact version", () => {
const {
configuredVersion: _configuredVersion,
configuredProgramPath: _configuredProgramPath,
...observationWithoutBinding
} = observation;
const output = formatServiceStatus(
{
...status,
observation: {
...observationWithoutBinding,
configuredProgramPath: "/home/me/.t3/runtime/versions/nightly/t3",
},
},
"0.0.29",
);

expect(output).toContain("Configured launcher: /home/me/.t3/runtime/versions/nightly/t3");
});

const newerServiceStatus = { ...status, current: false, installedVersion: "999.0.0" };

function makeTestService(serviceStatus: BootService.BootServiceStatus) {
Expand Down
88 changes: 85 additions & 3 deletions apps/server/src/cli/service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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 References from "effect/References";
import * as Terminal from "effect/Terminal";
import { Command, Flag, GlobalFlag, Prompt } from "effect/unstable/cli";
import { FetchHttpClient } from "effect/unstable/http";
Expand Down Expand Up @@ -66,7 +67,11 @@ export const reconcileService = Effect.fn("cli.service.reconcile")(function* (op
export function formatServiceStatus(
status: BootService.BootServiceStatus,
cliVersion: string,
options?: { readonly json?: boolean },
): string {
if (options?.json) {
return JSON.stringify({ ...status, cliVersion }, null, 2);
}
if (!status.supported) {
return "T3 Code service\n Status: unavailable on this machine\n Supported on: Linux with systemd, macOS with launchd";
}
Expand All @@ -77,6 +82,8 @@ export function formatServiceStatus(
const problems = (status.problems ?? []).map(
(problem) => ` [${problem}] ${BootService.formatBootServiceProblem(problem)}`,
);
const observationLines = formatServiceObservation(status);
const warning = formatServiceObservationWarning(status);
if (
!status.current &&
status.installedVersion !== undefined &&
Expand All @@ -87,7 +94,9 @@ export function formatServiceStatus(
` Status: installed · t3@${installedVersion} (newer than this t3@${cliVersion} CLI)`,
` Unit: ${status.unitPath}`,
` Logs: ${status.logPath}`,
...observationLines,
...problems,
...warning,
` Next: Run \`t3 update ${installedVersion}\` to match it, or pass \`--allow-downgrade\` to \`t3 service install\` explicitly.`,
].join("\n");
}
Expand All @@ -96,20 +105,88 @@ export function formatServiceStatus(
` Status: ${status.current ? `installed · t3@${installedVersion}` : "needs an update or repair"}`,
` Unit: ${status.unitPath}`,
` Logs: ${status.logPath}`,
...observationLines,
...problems,
...warning,
...(status.current ? [] : [" Next: Run `t3 service install` to repair it."]),
].join("\n");
}

/** Manager observation lines. Additive: absent when no live observation exists. */
function formatServiceObservation(status: BootService.BootServiceStatus): ReadonlyArray<string> {
const observation = status.observation;
if (observation === undefined) return [];
const running =
observation.running === "running"
? "running"
: observation.running === "stopped"
? "stopped"
: observation.running === "transitioning"
? "changing state"
: observation.running === "not-loaded"
? "not loaded"
: "unknown";
const configured =
observation.configuredVersion !== undefined
? `t3@${observation.configuredVersion}`
: observation.configuredProgramPath;
const rawState =
observation.state === undefined
? undefined
: observation.subState === undefined
? observation.state
: `${observation.state}/${observation.subState}`;
return [
` Manager: ${observation.manager} · running ${running}`,
` Enabled: ${observation.enabled}`,
...(configured === undefined ? [] : [` Configured launcher: ${configured}`]),
...(rawState === undefined ? [] : [` Manager state: ${rawState}`]),
...(observation.restartCount === undefined
? []
: [` Restarts (systemd, monotonic since start): ${observation.restartCount}`]),
...(observation.lastResult === undefined ? [] : [` Last result: ${observation.lastResult}`]),
...(observation.detail === undefined ? [] : [` Manager detail: ${observation.detail}`]),
` Observed: ${observation.observedAt} (${observation.source})`,
];
}

/**
* A non-running manager observation must not read as healthy, but a state file
* and `current` cannot prove the server answers either. Keep both claims
* separate and say which one the note is based on.
*/
function formatServiceObservationWarning(
status: BootService.BootServiceStatus,
): ReadonlyArray<string> {
const observation = status.observation;
if (observation === undefined || observation.running === "running") return [];
return [
` Note: the service manager reports the job as '${observation.running}'; that is a manager observation, not application health.`,
];
}

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>,
options?: { readonly quietLogs?: boolean },
) {
const logLevel = yield* GlobalFlag.LogLevel;
const config = yield* resolveCliAuthConfig(flags, logLevel);
return yield* run.pipe(Effect.provide(bootServiceLayer(config)));
const minimumLogLevel = options?.quietLogs ? "Error" : config.logLevel;
return yield* run.pipe(
Effect.provide(
bootServiceLayer(config).pipe(
Layer.provide(Layer.succeed(References.MinimumLogLevel, minimumLogLevel)),
),
),
);
});

const jsonFlag = Flag.Boolean("json").pipe(
Flag.withDescription("Emit JSON instead of human-readable output."),
Flag.withDefault(false),
);

const serviceReconcileFlags = {
...projectLocationFlags,
allowDowngrade: Flag.Boolean("allow-downgrade").pipe(
Expand Down Expand Up @@ -201,15 +278,20 @@ const serviceUninstallCommand = Command.make("uninstall", projectLocationFlags).
),
);

const serviceStatusCommand = Command.make("status", projectLocationFlags).pipe(
const serviceStatusCommand = Command.make("status", {
...projectLocationFlags,
json: jsonFlag,
}).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));
const status = yield* service.status;
yield* Console.log(formatServiceStatus(status, packageJson.version, { json: flags.json }));
}),
{ quietLogs: flags.json },
),
),
);
Expand Down
Loading
Loading