Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
7500d94
chore(native): import accepted Windows SCM host helper from #8 7455a7b6
Sep 28, 2026
305360a
feat(service): import tested Stage A status candidate from #9 22734455e
Sep 28, 2026
d4e8a15
feat(service): join whole-service stop to a child drain over launcher…
Sep 28, 2026
ed282a6
fix(service): deliver host stop control to the launcher and require a…
Sep 28, 2026
69a9c8d
feat(service): bind t3 service install/status/restart/uninstall to th…
Sep 28, 2026
36c3ca6
test(service): run the launcher transitions with a portable scripted …
Sep 28, 2026
79da5f9
Merge remote-tracking branch 'origin/main' into env1/r10b-win-lifecyc…
Sep 28, 2026
0be8291
fix(service): bind SCM ownership to the real account and parse image …
Sep 29, 2026
faef7ee
fix(service): observe bounded SCM transitions and separate registrati…
Sep 29, 2026
279fab8
fix(service): make private stop delivery atomic and prove the real ma…
Sep 29, 2026
f970def
docs(service): align Windows SCM status, transitions and control with…
Sep 29, 2026
9c37284
test(service): share SCM host argument vectors with the native parser
nullStack65 Sep 29, 2026
a12d42a
fix(service): match native effective args and preserve stopped-servic…
nullStack65 Sep 29, 2026
6281c77
Merge remote-tracking branch 'origin/main' into delivery/sol-t3-pr10-…
nullStack65 Oct 7, 2026
e97a35a
Merge remote-tracking branch 'origin/main' into delivery/pr10
kameron-smith-closura Oct 7, 2026
3cac8ed
fix(service): validate Windows host in CI and clarify candidate guidance
kameron-smith-closura Oct 7, 2026
f3aefbe
fix(service): keep lifecycle implementation helpers private
kameron-smith-closura Oct 7, 2026
e95bd91
Merge remote-tracking branch 'origin/main' into delivery/pr10
kameron-smith-closura Oct 7, 2026
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
10 changes: 9 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ jobs:

- name: Check Rust formatting
run: |
for crate in resource-monitor kde-snap-shot hyprland-snap-shot; do
for crate in resource-monitor kde-snap-shot hyprland-snap-shot windows-service-host; do
cargo fmt --manifest-path "native/$crate/Cargo.toml" -- --check
done

Expand All @@ -216,6 +216,14 @@ jobs:
cargo test --locked --manifest-path "native/$crate/Cargo.toml"
done

- name: Check portable Windows service host
run: cargo test --locked --features test-child --manifest-path native/windows-service-host/Cargo.toml

- name: Check Windows service host target
run: |
rustup target add x86_64-pc-windows-gnu
cargo check --locked --target x86_64-pc-windows-gnu --manifest-path native/windows-service-host/Cargo.toml

# The static analysis below needs a macOS runner, which bills ~6.7x a Linux
# minute, so gate it on the native sources it actually lints instead of paying
# for it on every push. Detection is API-only (no checkout) and fails open: if
Expand Down
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
90 changes: 86 additions & 4 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,9 +67,13 @@ 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";
return "T3 Code service\n Status: unavailable on this machine\n Supported on: Linux with systemd, macOS with launchd, and Windows with the T3 service host and an explicit service account";
}
if (!status.installed) {
return "T3 Code service\n Status: not installed\n Next: Run `t3 service install`.";
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