Skip to content
Merged
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ There's no public docs site yet, checkout the miscellaneous markdown files in [d
## Documentation

- [Getting started](./docs/getting-started/quick-start.md)
- [Remote access](./docs/user/remote-access.md)
- [Keeping T3 Code in sync](./docs/user/server-updates.md)
- [Architecture overview](./docs/architecture/overview.md)
- [Provider guides](./docs/providers/codex.md)
- [Operations](./docs/operations/ci.md)
Expand Down
17 changes: 16 additions & 1 deletion apps/server/src/bin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,18 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {
}).pipe(Effect.provide(Layer.mergeAll(CliRuntimeLayer, TestConsole.layer))),
);

it.effect("exposes service lifecycle commands without T3 Connect configuration", () =>
Effect.gen(function* () {
const { output } = yield* captureStdout(runCli(["service", "--help"], noConnectCli));

assert.include(output, "Manage the T3 Code background service.");
assert.include(output, "install");
assert.include(output, "uninstall");
assert.include(output, "update");
assert.include(output, "status");
}),
);

it.effect("reports fresh headless connect state without requiring local configuration", () =>
Effect.gen(function* () {
const baseDir = NodeFS.mkdtempSync(
Expand Down Expand Up @@ -305,7 +317,10 @@ it.layer(NodeServices.layer)("bin cli parsing", (it) => {
runConnectCli(["connect", "logout", "--base-dir", baseDir]),
);

assert.equal(output, "Signed out of T3 Connect locally.");
assert.equal(
output,
"Signed out of T3 Connect locally.\nThe background service is managed separately with `t3 service`.",
);
assert.isFalse(NodeFS.existsSync(tokenPath));
}),
);
Expand Down
2 changes: 2 additions & 0 deletions apps/server/src/bin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { hasCloudPublicConfig } from "./cloud/publicConfig.ts";
import { sharedServerCommandFlags } from "./cli/config.ts";
import { projectCommand } from "./cli/project.ts";
import { runServerCommand, serveCommand, startCommand } from "./cli/server.ts";
import { serviceCommand } from "./cli/service.ts";

const CliRuntimeLayer = Layer.mergeAll(NodeServices.layer, NetService.layer);

Expand Down Expand Up @@ -47,6 +48,7 @@ export const makeCli = ({ cloudEnabled = hasCloudPublicConfig } = {}) =>
serveCommand,
authCommand,
projectCommand,
serviceCommand,
cloudEnabled ? connectCommand : connectUnavailableCommand,
]),
);
Expand Down
4 changes: 2 additions & 2 deletions apps/server/src/cli/connect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ import {
formatRelayClientReady,
headlessSessionConfig,
isPublishAgentActivityEnabledValue,
recoverBootServiceOffer,
reportCloudDisconnectResults,
} from "./connect.ts";
import { recoverServiceOnboardingOffer } from "./service.ts";

it("explains how to complete headless authorization", () => {
assert.equal(
Expand Down Expand Up @@ -58,7 +58,7 @@ it.effect("detects headless operation from individual SSH config values", () =>

it.effect("treats cancelling optional background setup as a successful skip", () =>
Effect.gen(function* () {
const result = yield* recoverBootServiceOffer(Effect.fail(new Terminal.QuitError({})));
const result = yield* recoverServiceOnboardingOffer(Effect.fail(new Terminal.QuitError({})));
assert.isFalse(result);
}),
);
Expand Down
80 changes: 11 additions & 69 deletions apps/server/src/cli/connect.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import {
} from "@t3tools/contracts";
import { RelayOkResponse } from "@t3tools/contracts/relay";
import * as RelayClient from "@t3tools/shared/relayClient";
import * as Terminal from "effect/Terminal";
import { withRelayClientTracing } from "@t3tools/shared/relayTracing";
import * as Cause from "effect/Cause";
import * as Config from "effect/Config";
Expand All @@ -20,6 +19,7 @@ import * as Layer from "effect/Layer";
import * as Option from "effect/Option";
import * as References from "effect/References";
import * as Schema from "effect/Schema";
import * as Terminal from "effect/Terminal";
import { Command, Flag, GlobalFlag, Prompt } from "effect/unstable/cli";
import {
FetchHttpClient,
Expand All @@ -29,7 +29,6 @@ import {
} from "effect/unstable/http";
import * as HttpApiClient from "effect/unstable/httpapi/HttpApiClient";

import packageJson from "../../package.json" with { type: "json" };
import * as EnvironmentAuth from "../auth/EnvironmentAuth.ts";
import * as ServerSecretStore from "../auth/ServerSecretStore.ts";
import * as BootService from "../cloud/bootService.ts";
Expand All @@ -45,9 +44,13 @@ import { headlessRelayClientTracingLayer } from "../cloud/relayTracing.ts";
import * as ServerConfig from "../config.ts";
import * as ServerEnvironment from "../environment/ServerEnvironment.ts";
import * as ExternalLauncher from "../process/externalLauncher.ts";
import * as ProcessRunner from "../processRunner.ts";
import { readPersistedServerRuntimeState } from "../serverRuntimeState.ts";
import { projectLocationFlags, resolveCliAuthConfig } from "./config.ts";
import {
bootServiceLayer,
offerServiceDuringOnboarding,
recoverServiceOnboardingOffer,
} from "./service.ts";

const jsonFlag = Flag.boolean("json").pipe(
Flag.withDescription("Emit JSON instead of human-readable output."),
Expand Down Expand Up @@ -396,21 +399,6 @@ const disconnectCloud = Effect.fn("cloud.cli.disconnect")(function* (options: {
if (options.clearAuthorization) {
const tokens = yield* CliTokenManager.CloudCliTokenManager;
yield* tokens.clear;

// uninstall itself no-ops when nothing is installed (and on non-Linux),
// so no status pre-check that could mask a real removal failure.
const bootService = yield* BootService.BootService;
yield* bootService.uninstall.pipe(
Effect.tap((removed) =>
removed ? Console.log("Removed the T3 Code background service.") : Effect.void,
),
Effect.catchTag("BootServiceUnsupportedError", () => Effect.succeed(false)),
Effect.catch((error) =>
Console.warn(`Could not remove the background service: ${error.message}`).pipe(
Effect.as(false),
),
),
);
}

yield* reportCloudDisconnectResults({
Expand All @@ -420,7 +408,9 @@ const disconnectCloud = Effect.fn("cloud.cli.disconnect")(function* (options: {
});

if (options.clearAuthorization) {
yield* Console.log("Signed out of T3 Connect locally.");
yield* Console.log(
"Signed out of T3 Connect locally.\nThe background service is managed separately with `t3 service`.",
);
}
});

Expand Down Expand Up @@ -457,11 +447,7 @@ const runCloudCommand = Effect.fn("cloud.cli.run_cloud_command")(function* <A, E
RelayClient.layerCloudflared({ baseDir: config.baseDir }),
EnvironmentAuth.runtimeLayer,
ServerEnvironment.layer,
BootService.layer({
baseDir: config.baseDir,
logsDir: config.logsDir,
cliVersion: packageJson.version,
}).pipe(Layer.provide(ProcessRunner.layer)),
bootServiceLayer(config),
headlessRelayClientTracingLayer,
).pipe(
Layer.provideMerge(FetchHttpClient.layer),
Expand Down Expand Up @@ -683,50 +669,6 @@ const connectLogoutCommand = Command.make("logout", {
),
);

const offerBootService = Effect.gen(function* () {
const bootService = yield* BootService.BootService;
const { supported, installed, current } = yield* bootService.status;
if (!supported) {
// Don't prompt for something that can only fail; background setup is
// Linux/systemd-only for now.
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 background service is from an older setup. 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 plan = yield* bootService.install;
yield* Console.log(`Background service installed. Logs: ${plan.logPath}`);
return true;
});

export const recoverBootServiceOffer = <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 connectCommand = Command.make("connect", {
...projectLocationFlags,
headless: headlessFlag,
Expand All @@ -748,7 +690,7 @@ export const connectCommand = Command.make("connect", {

// Connect itself already succeeded; a boot-service failure must not
// fail the command, just tell the user what happened and move on.
const background = yield* recoverBootServiceOffer(offerBootService);
const background = yield* recoverServiceOnboardingOffer(offerServiceDuringOnboarding);
yield* Console.log(
background
? "\n✓ Background service ready\n\nT3 Code will stay reachable after you log out."
Expand Down
37 changes: 37 additions & 0 deletions apps/server/src/cli/service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { assert, it } from "@effect/vitest";

import { formatServiceStatus } from "./service.ts";

const status = {
supported: true,
installed: true,
current: true,
unitPath: "/home/me/.config/systemd/user/t3code.service",
logPath: "/home/me/.t3/userdata/logs/boot-service.log",
} as const;

it("reports the installed service version and host paths", () => {
assert.equal(
formatServiceStatus(status, "0.0.29"),
[
"T3 Code service",
" Status: installed · t3@0.0.29",
" Unit: /home/me/.config/systemd/user/t3code.service",
" Logs: /home/me/.t3/userdata/logs/boot-service.log",
].join("\n"),
);
});

it("gives a direct repair command for a stale service", () => {
assert.include(
formatServiceStatus({ ...status, current: false }, "0.0.29"),
"Next: Run `npx t3@latest service update`.",
);
});

it("explains service availability without systemd", () => {
assert.include(
formatServiceStatus({ ...status, supported: false, installed: false }, "0.0.29"),
"Supported on: Linux with systemd",
);
});
Loading
Loading