From d4a995ae5cb456405d5184ed9a67a363dceada0e Mon Sep 17 00:00:00 2001 From: zeroknots Date: Sat, 7 Nov 2026 04:17:45 +0000 Subject: [PATCH] feat(server): allow explicit environment display labels --- .../src/environment/ServerEnvironment.test.ts | 12 ++- .../ServerEnvironmentLabel.test.ts | 94 +++++++++++++++++++ .../src/environment/ServerEnvironmentLabel.ts | 7 ++ docs/user/background-service.md | 16 ++++ 4 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 apps/server/src/environment/ServerEnvironmentLabel.test.ts diff --git a/apps/server/src/environment/ServerEnvironment.test.ts b/apps/server/src/environment/ServerEnvironment.test.ts index dc719ed93c41b..2e98366898c14 100644 --- a/apps/server/src/environment/ServerEnvironment.test.ts +++ b/apps/server/src/environment/ServerEnvironment.test.ts @@ -228,12 +228,20 @@ it.layer(Layer.mergeAll(NodeServices.layer, NodeCrypto.layer))("ServerEnvironmen const first = yield* Effect.gen(function* () { const serverEnvironment = yield* ServerEnvironment.ServerEnvironment; return yield* serverEnvironment.getDescriptor; - }).pipe(Effect.provide(layerServerEnvironment(baseDir))); + }).pipe( + Effect.provide(layerServerEnvironment(baseDir)), + Effect.provideService(HostProcess.Environment, { T3CODE_ENVIRONMENT_LABEL: "Personal" }), + ); const second = yield* Effect.gen(function* () { const serverEnvironment = yield* ServerEnvironment.ServerEnvironment; return yield* serverEnvironment.getDescriptor; - }).pipe(Effect.provide(layerServerEnvironment(baseDir))); + }).pipe( + Effect.provide(layerServerEnvironment(baseDir)), + Effect.provideService(HostProcess.Environment, { T3CODE_ENVIRONMENT_LABEL: "Work" }), + ); + expect(first.label).not.toBe(second.label); + expect({ ...first, label: second.label }).toEqual(second); expect(first.environmentId).toBe(second.environmentId); expect(first.orchestrationProtocolVersion).toBe(ORCHESTRATION_PROTOCOL_VERSION); expect(second.capabilities.repositoryIdentity).toBe(true); diff --git a/apps/server/src/environment/ServerEnvironmentLabel.test.ts b/apps/server/src/environment/ServerEnvironmentLabel.test.ts new file mode 100644 index 0000000000000..3a591c5239971 --- /dev/null +++ b/apps/server/src/environment/ServerEnvironmentLabel.test.ts @@ -0,0 +1,94 @@ +import { expect, it } from "@effect/vitest"; +import * as HostProcess from "@t3tools/shared/HostProcess"; +import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner"; + +import * as ProcessRunner from "../processRunner.ts"; +import { resolveServerEnvironmentLabel } from "./ServerEnvironmentLabel.ts"; + +it.effect.each( + [undefined, "", " ", "\t\r\n", "  "].flatMap((override) => + [ + { + machineInfo: 'PRETTY_HOSTNAME=" Friendly host "\n', + command: "Other host", + hostname: "host", + cwd: "project", + expected: "Friendly host", + }, + { + machineInfo: "", + command: " Command host\n", + hostname: "host", + cwd: "project", + expected: "Command host", + }, + { machineInfo: "", command: "", hostname: " host ", cwd: "project", expected: "host" }, + { machineInfo: "", command: "", hostname: " \t", cwd: " project ", expected: "project" }, + { machineInfo: "", command: "", hostname: "", cwd: "\t", expected: "T3 environment" }, + ].map((host) => ({ ...host, override })), + ), +)("keeps fallback $expected for unset or blank override '$override'", (host) => + Effect.gen(function* () { + const label = yield* resolveServerEnvironmentLabel({ cwdBaseName: host.cwd }).pipe( + Effect.provideService(HostProcess.Environment, { T3CODE_ENVIRONMENT_LABEL: host.override }), + Effect.provideService(HostProcess.Platform, "linux"), + Effect.provideService(HostProcess.Hostname, host.hostname), + Effect.provideService( + FileSystem.FileSystem, + FileSystem.makeNoop({ + exists: () => Effect.succeed(host.machineInfo !== ""), + readFileString: () => Effect.succeed(host.machineInfo), + }), + ), + Effect.provideService(ProcessRunner.ProcessRunner, { + run: () => + Effect.succeed({ + stdout: host.command, + stderr: "", + code: ChildProcessSpawner.ExitCode(0), + timedOut: false, + stdoutTruncated: false, + stderrTruncated: false, + stdoutInvalidUtf8: false, + stderrInvalidUtf8: false, + }), + }), + ); + + expect(label).toBe(host.expected); + }), +); + +it.effect.each( + (["linux", "darwin", "win32"] as const).flatMap((platform) => + [ + { username: "personal", override: " Personal workspace \t", expected: "Personal workspace" }, + { username: "work", override: "\n仕事用 🛠️ ", expected: "仕事用 🛠️" }, + ].map((account) => ({ ...account, platform })), + ), +)("uses the explicit label for $username on $platform without host probes", (account) => + Effect.gen(function* () { + const label = yield* resolveServerEnvironmentLabel({ cwdBaseName: "same-project" }).pipe( + Effect.provideService(HostProcess.Environment, { + T3CODE_ENVIRONMENT_LABEL: account.override, + }), + Effect.provideService(HostProcess.Platform, account.platform), + Effect.provideService(HostProcess.Hostname, "same-host"), + Effect.provideService(HostProcess.Username, account.username), + Effect.provideService( + FileSystem.FileSystem, + FileSystem.makeNoop({ + exists: () => Effect.die("Explicit labels must not inspect host files"), + readFileString: () => Effect.die("Explicit labels must not read host files"), + }), + ), + Effect.provideService(ProcessRunner.ProcessRunner, { + run: () => Effect.die("Explicit labels must not spawn host probes"), + }), + ); + + expect(label).toBe(account.expected); + }), +); diff --git a/apps/server/src/environment/ServerEnvironmentLabel.ts b/apps/server/src/environment/ServerEnvironmentLabel.ts index 73ff4a7639103..86d48be570835 100644 --- a/apps/server/src/environment/ServerEnvironmentLabel.ts +++ b/apps/server/src/environment/ServerEnvironmentLabel.ts @@ -179,9 +179,16 @@ const resolveFriendlyHostLabel = Effect.fn("resolveFriendlyHostLabel")(function* return null; }); +/** A nonblank display-only override skips host probes; otherwise retain the host/directory fallback. */ export const resolveServerEnvironmentLabel = Effect.fn("resolveServerEnvironmentLabel")(function* ( input: ResolveServerEnvironmentLabelInput, ) { + const environment = yield* HostProcess.Environment; + const environmentLabel = normalizeLabel(environment.T3CODE_ENVIRONMENT_LABEL); + if (environmentLabel) { + return environmentLabel; + } + const friendlyHostLabel = yield* resolveFriendlyHostLabel(); if (friendlyHostLabel) { return friendlyHostLabel; diff --git a/docs/user/background-service.md b/docs/user/background-service.md index 83974146c2cdf..160fa0e340072 100644 --- a/docs/user/background-service.md +++ b/docs/user/background-service.md @@ -66,6 +66,7 @@ and updates. For example: ```dotenv T3CODE_PORT=3773 T3CODE_HOST=127.0.0.1 +T3CODE_ENVIRONMENT_LABEL="Build server" ``` Use `KEY=VALUE` entries, quoting values that contain spaces or `#`. Values are @@ -81,6 +82,21 @@ A missing file is ignored; other read failures stop service startup. If the file contains credentials, restrict access to that file with `chmod 600` (for the default T3 home, `chmod 600 ~/.t3/service.env`). +### Name an environment + +Set `T3CODE_ENVIRONMENT_LABEL` in the server's environment to distinguish multiple +T3 servers on one machine. The server trims surrounding whitespace. An unset or +blank value keeps the automatically detected label. + +This changes the server's display label, not its environment ID, OS hostname, or +network address. Existing client-side aliases are unchanged. For a background +service, put the variable in `service.env` and restart the service after running +agent work has finished. For a server in a terminal: + +```sh +T3CODE_ENVIRONMENT_LABEL="Build server" t3 serve +``` + ## Platform support Linux needs systemd user services. Setup enables lingering so T3 Code starts at