From f6d6c714de643f17a4cd268ea641db78eea6d723 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 4 Oct 2026 22:29:59 +0000 Subject: [PATCH 1/6] fix(cli): reject accidental server launches --- apps/server/src/binCli.ts | 10 ++++- apps/server/src/cli/app.test.ts | 68 ++++++++++++++++++++++++++++++ apps/server/src/cli/config.test.ts | 55 ++++++++++++++++++++++++ apps/server/src/cli/config.ts | 21 ++++++++- apps/server/src/cli/server.ts | 26 +++++++++++- docs/user/install.md | 8 +++- 6 files changed, 183 insertions(+), 5 deletions(-) diff --git a/apps/server/src/binCli.ts b/apps/server/src/binCli.ts index a4148e707bf6..c39fa3e3b620 100644 --- a/apps/server/src/binCli.ts +++ b/apps/server/src/binCli.ts @@ -15,7 +15,7 @@ import { pairCommand } from "./cli/pair.ts"; 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 { runDefaultServerCommand, serveCommand, startCommand } from "./cli/server.ts"; import { updateCommand } from "./cli/update.ts"; import { uninstallCommand } from "./cli/uninstall.ts"; import { serviceLauncherCommand } from "./cli/serviceLauncher.ts"; @@ -56,8 +56,14 @@ const connectUnavailableCommand = Command.make("connect", { export const makeCli = ({ cloudEnabled = hasCloudPublicConfig } = {}) => Command.make("t3", { ...sharedServerCommandFlags }).pipe( Command.withDescription("Run the T3 Code server."), - Command.withHandler((flags) => runServerCommand(flags)), + Command.withHandler(runDefaultServerCommand), Command.withSubcommands([ + Command.make("help").pipe( + Command.withDescription("Show command help."), + Command.withHandler(() => + Effect.fail(new CliError.ShowHelp({ commandPath: ["t3"], errors: [] })), + ), + ), acpMcpBridgeCommand, acpMcpCallCommand, startCommand, diff --git a/apps/server/src/cli/app.test.ts b/apps/server/src/cli/app.test.ts index e478f66f0731..b8ac61c1cca7 100644 --- a/apps/server/src/cli/app.test.ts +++ b/apps/server/src/cli/app.test.ts @@ -17,10 +17,12 @@ import * as NetService from "@t3tools/shared/Net"; import * as ConfigProvider from "effect/ConfigProvider"; import * as Effect from "effect/Effect"; import * as Layer from "effect/Layer"; +import * as Schema from "effect/Schema"; import { Command } from "effect/unstable/cli"; import { afterEach, describe, expect, vi } from "vite-plus/test"; import { makeCli } from "../binCli.ts"; +import { PersistedServerRuntimeState } from "../serverRuntimeState.ts"; vi.mock("node:os", async (importOriginal) => { const os = await importOriginal(); @@ -29,6 +31,8 @@ vi.mock("node:os", async (importOriginal) => { afterEach(() => vi.mocked(NodeOS.homedir).mockReset()); +const encodeRuntimeState = Schema.encodeEffect(Schema.fromJsonString(PersistedServerRuntimeState)); + const runCli = (args: ReadonlyArray, env: Record = {}) => Command.runWith(makeCli(), { version: "0.0.0" })(args).pipe( Effect.provide( @@ -130,6 +134,70 @@ const withTempDirectory = ( (root) => Effect.promise(() => NodeFSP.rm(root, { recursive: true, force: true })), ); +describe("t3 server command safety", () => { + it.effect("rejects unknown command words without creating a home or project", () => + withTempDirectory("t3-cli-unknown-", (root) => + Effect.gen(function* () { + const baseDir = NodePath.join(root, "home"); + for (const word of ["account", "login", "clients", "conenct"]) { + const error = yield* runCli([word, "--base-dir", baseDir]).pipe(Effect.flip); + expect(String(error)).toContain(`Unknown command "${word}"`); + expect(yield* pathExists(word)).toBe(false); + expect(yield* pathExists(baseDir)).toBe(false); + } + }), + ), + ); + + it.effect("shows help without creating state", () => + withTempDirectory("t3-cli-help-", (root) => + Effect.gen(function* () { + const baseDir = NodePath.join(root, "home"); + const help = yield* runCli(["help"], { T3CODE_HOME: baseDir }).pipe(Effect.flip); + expect(help).toMatchObject({ _tag: "ShowHelp", commandPath: ["t3"], errors: [] }); + expect(yield* pathExists(baseDir)).toBe(false); + }), + ), + ); + + it.effect("refuses manual startup over a live server before creating directories", () => + withTempDirectory("t3-cli-running-", (root) => + Effect.gen(function* () { + const baseDir = NodePath.join(root, "home"); + const stateDir = NodePath.join(baseDir, "userdata"); + const statePath = NodePath.join(stateDir, "server-runtime.json"); + const record = yield* encodeRuntimeState({ + version: 1, + pid: process.pid, + port: 3773, + origin: "http://127.0.0.1:3773", + startedAt: "2026-10-01T00:00:00.000Z", + serviceManaged: true, + }); + yield* Effect.promise(() => NodeFSP.mkdir(stateDir, { recursive: true })); + yield* Effect.promise(() => NodeFSP.writeFile(statePath, record)); + const newDirectory = NodePath.join(root, "new-project"); + for (const args of [ + [], + ["start"], + ["."], + ["node_modules"], + [newDirectory], + ["start", newDirectory], + ]) { + const error = yield* runCli(args, { T3CODE_HOME: baseDir }).pipe(Effect.flip); + expect(String(error)).toContain("A T3 Code server is already running"); + expect(yield* Effect.promise(() => NodeFSP.readFile(statePath, "utf8"))).toBe(record); + expect(yield* pathExists(newDirectory)).toBe(false); + expect(yield* Effect.promise(() => NodeFSP.readdir(stateDir))).toEqual([ + "server-runtime.json", + ]); + } + }), + ), + ); +}); + describe("t3 app", () => { it.effect("rejects SSH before it tries to reach a desktop app", () => withTempDirectory("t3-app-ssh-test-", (root) => diff --git a/apps/server/src/cli/config.test.ts b/apps/server/src/cli/config.test.ts index a133c6d4b23c..f64e01952483 100644 --- a/apps/server/src/cli/config.test.ts +++ b/apps/server/src/cli/config.test.ts @@ -80,6 +80,61 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => { ); }); + it.effect("keeps stale records and supervised startup out of the manual launch preflight", () => + Effect.gen(function* () { + const fs = yield* FileSystem.FileSystem; + const path = yield* Path.Path; + const root = yield* fs.makeTempDirectoryScoped({ prefix: "t3-cli-preflight-" }); + for (const [name, pid, mode, rejectRunningServer] of [ + ["stale", 2_147_483_647, "web", true], + ["desktop", process.pid, "desktop", true], + ["serve", process.pid, "web", false], + ] as const) { + const baseDir = path.join(root, name); + const stateDir = path.join(baseDir, "userdata"); + yield* fs.makeDirectory(stateDir, { recursive: true }); + yield* fs.writeFileString( + path.join(stateDir, "server-runtime.json"), + yield* encodeUnknownJson({ + version: 1, + pid, + port: 3773, + origin: "http://127.0.0.1:3773", + startedAt: "2026-10-01T00:00:00.000Z", + }), + ); + const cwd = path.join(root, `${name}-project`); + const config = yield* resolveServerConfig( + { + mode: Option.some(mode), + port: Option.some(8788), + host: Option.none(), + baseDir: Option.some(baseDir), + cwd: Option.some(cwd), + devUrl: Option.none(), + noBrowser: Option.none(), + bootstrapFd: Option.none(), + autoBootstrapProjectFromCwd: Option.none(), + logWebSocketEvents: Option.none(), + tailscaleServeEnabled: Option.none(), + tailscaleServePort: Option.none(), + }, + Option.none(), + { rejectRunningServer }, + ).pipe( + Effect.provide( + Layer.merge( + NetService.layer, + ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })), + ), + ), + ); + expect(config.cwd).toBe(cwd); + expect(yield* fs.exists(cwd)).toBe(true); + } + }), + ); + it.effect("enables a trimmed reusable auth token only for web dev mode", () => Effect.gen(function* () { const baseDir = yield* FileSystem.FileSystem.pipe( diff --git a/apps/server/src/cli/config.ts b/apps/server/src/cli/config.ts index 62461e286748..5e540847598a 100644 --- a/apps/server/src/cli/config.ts +++ b/apps/server/src/cli/config.ts @@ -19,10 +19,12 @@ import * as Schema from "effect/Schema"; import * as SchemaIssue from "effect/SchemaIssue"; import * as SchemaTransformation from "effect/SchemaTransformation"; import { Argument, Flag } from "effect/unstable/cli"; +import * as CliError from "effect/unstable/cli/CliError"; import { readBootstrapEnvelope } from "../bootstrap.ts"; import * as ServerConfig from "../config.ts"; import { expandHomePath, resolveBaseDir } from "../os-jank.ts"; +import { isProcessAlive, readPersistedServerRuntimeState } from "../serverRuntimeState.ts"; const modeFlag = Flag.Literals("mode", ServerConfig.RuntimeMode.literals).pipe( Flag.withDescription("Runtime mode. `desktop` keeps loopback defaults unless overridden."), @@ -248,12 +250,19 @@ const loadPersistedObservabilitySettings = Effect.fn(function* (settingsPath: st return parsePersistedServerObservabilitySettings(raw); }); +class ServerAlreadyRunningError extends CliError.UserError { + override get message() { + return String(this.cause); + } +} + export const resolveServerConfig = ( flags: CliServerFlags, cliLogLevel: Option.Option, options?: { readonly startupPresentation?: ServerConfig.StartupPresentation; readonly forceAutoBootstrapProjectFromCwd?: boolean; + readonly rejectRunningServer?: boolean; }, ) => Effect.gen(function* () { @@ -324,10 +333,20 @@ export const resolveServerConfig = ( ); const rawCwd = Option.getOrElse(normalizedFlags.cwd, () => process.cwd()); const cwd = path.resolve(yield* expandHomePath(rawCwd.trim())); - yield* fs.makeDirectory(cwd, { recursive: true }); const derivedPaths = yield* ServerConfig.deriveServerPaths(baseDir, devUrl, { baseDirIsExplicit: Option.isSome(explicitBaseDir), }); + // An interactive CLI must not start over a discovered server. Lifetime locking + // and supervisor handoff are separate; this preflight cannot arbitrate two starts. + if (options?.rejectRunningServer && mode === "web") { + const runtime = yield* readPersistedServerRuntimeState(derivedPaths.serverRuntimeStatePath); + if (Option.isSome(runtime) && runtime.value.pid > 0 && isProcessAlive(runtime.value.pid)) { + return yield* new ServerAlreadyRunningError({ + cause: `A T3 Code server is already running for ${baseDir} (pid ${runtime.value.pid}, ${runtime.value.origin}). Connect to that server, stop it before starting another, or use a different --base-dir.`, + }); + } + } + yield* fs.makeDirectory(cwd, { recursive: true }); yield* ServerConfig.ensureServerDirectories(derivedPaths); const persistedObservabilitySettings = yield* loadPersistedObservabilitySettings( derivedPaths.settingsPath, diff --git a/apps/server/src/cli/server.ts b/apps/server/src/cli/server.ts index 298109abdb5f..2101b5f3da01 100644 --- a/apps/server/src/cli/server.ts +++ b/apps/server/src/cli/server.ts @@ -1,5 +1,8 @@ import * as Effect from "effect/Effect"; +import * as FileSystem from "effect/FileSystem"; +import * as Option from "effect/Option"; import { Command, GlobalFlag } from "effect/unstable/cli"; +import * as CliError from "effect/unstable/cli/CliError"; import * as ServerConfig from "../config.ts"; import { runServer } from "../server.ts"; @@ -10,6 +13,7 @@ export const runServerCommand = ( options?: { readonly startupPresentation?: ServerConfig.StartupPresentation; readonly forceAutoBootstrapProjectFromCwd?: boolean; + readonly rejectRunningServer?: boolean; }, ) => Effect.gen(function* () { @@ -18,9 +22,29 @@ export const runServerCommand = ( return yield* runServer.pipe(Effect.provideService(ServerConfig.ServerConfig, config)); }); +class UnknownServerCommandError extends CliError.UserError { + override get message() { + return `Unknown command ${JSON.stringify(this.cause)}. Use "t3 --help" for commands or an explicit path such as "t3 ./my-project" for a new directory.`; + } +} + +/** Bare words can name existing directories, but must not create typo projects. */ +export const runDefaultServerCommand = (flags: CliServerFlags) => + Effect.gen(function* () { + if (Option.isSome(flags.cwd)) { + const cwd = flags.cwd.value.trim(); + const fs = yield* FileSystem.FileSystem; + const explicitPath = cwd === "." || cwd === ".." || cwd === "~" || /[/\\]/.test(cwd); + if (!explicitPath && !(yield* fs.exists(cwd))) { + return yield* new UnknownServerCommandError({ cause: cwd }); + } + } + return yield* runServerCommand(flags, { rejectRunningServer: true }); + }); + export const startCommand = Command.make("start", { ...sharedServerCommandFlags }).pipe( Command.withDescription("Run the T3 Code server."), - Command.withHandler((flags) => runServerCommand(flags)), + Command.withHandler((flags) => runServerCommand(flags, { rejectRunningServer: true })), ); export const serveCommand = Command.make("serve", { ...sharedServerCommandFlags }).pipe( diff --git a/docs/user/install.md b/docs/user/install.md index 372d8e6e5607..980837199e9b 100644 --- a/docs/user/install.md +++ b/docs/user/install.md @@ -33,7 +33,13 @@ line to add. Set `T3CODE_CHANNEL=nightly` to install the nightly train, or | Move to the newest release | `t3 update` | | Remove it again | `t3 uninstall` | -Run `t3 --help` for the full reference. +Run `t3 help` or `t3 --help` for the full reference. To start in a new working +directory, use an explicit path such as `t3 ./my-project`. A bare directory name +is accepted only if it already exists. + +If `t3` or `t3 start` reports an already running server, connect to that server +instead. Stop it before starting a replacement, or use a different `--base-dir` +for an independent server. To try T3 Code once without installing it, run `npx t3@latest` instead (needs Node.js for `npx`). From 06e4c8241c760bce28e2e4630c1f8925e6aedd6a Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 4 Oct 2026 22:49:41 +0000 Subject: [PATCH 2/6] fix(cli): keep server command runner private --- apps/server/src/cli/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/server/src/cli/server.ts b/apps/server/src/cli/server.ts index 2101b5f3da01..5b68ce05256e 100644 --- a/apps/server/src/cli/server.ts +++ b/apps/server/src/cli/server.ts @@ -8,7 +8,7 @@ import * as ServerConfig from "../config.ts"; import { runServer } from "../server.ts"; import { type CliServerFlags, resolveServerConfig, sharedServerCommandFlags } from "./config.ts"; -export const runServerCommand = ( +const runServerCommand = ( flags: CliServerFlags, options?: { readonly startupPresentation?: ServerConfig.StartupPresentation; From bf5147fbbba7aaf209ee61d761c16fcb4b027915 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 4 Oct 2026 23:12:14 +0000 Subject: [PATCH 3/6] fix(cli): require bare project paths to be directories --- apps/server/src/cli/app.test.ts | 4 ++-- apps/server/src/cli/server.ts | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/apps/server/src/cli/app.test.ts b/apps/server/src/cli/app.test.ts index b8ac61c1cca7..1f5a02d15c2f 100644 --- a/apps/server/src/cli/app.test.ts +++ b/apps/server/src/cli/app.test.ts @@ -139,10 +139,10 @@ describe("t3 server command safety", () => { withTempDirectory("t3-cli-unknown-", (root) => Effect.gen(function* () { const baseDir = NodePath.join(root, "home"); - for (const word of ["account", "login", "clients", "conenct"]) { + for (const word of ["account", "login", "clients", "conenct", "package.json"]) { const error = yield* runCli([word, "--base-dir", baseDir]).pipe(Effect.flip); expect(String(error)).toContain(`Unknown command "${word}"`); - expect(yield* pathExists(word)).toBe(false); + expect(yield* pathExists(word)).toBe(word === "package.json"); expect(yield* pathExists(baseDir)).toBe(false); } }), diff --git a/apps/server/src/cli/server.ts b/apps/server/src/cli/server.ts index 5b68ce05256e..8a30d68cf250 100644 --- a/apps/server/src/cli/server.ts +++ b/apps/server/src/cli/server.ts @@ -35,7 +35,10 @@ export const runDefaultServerCommand = (flags: CliServerFlags) => const cwd = flags.cwd.value.trim(); const fs = yield* FileSystem.FileSystem; const explicitPath = cwd === "." || cwd === ".." || cwd === "~" || /[/\\]/.test(cwd); - if (!explicitPath && !(yield* fs.exists(cwd))) { + if ( + !explicitPath && + (!(yield* fs.exists(cwd)) || (yield* fs.stat(cwd)).type !== "Directory") + ) { return yield* new UnknownServerCommandError({ cause: cwd }); } } From fb9c33494aac064f02af1846267843ebd92bfb87 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 4 Oct 2026 23:25:29 +0000 Subject: [PATCH 4/6] fix(cli): recognize drive-relative project paths --- apps/server/src/cli/app.test.ts | 1 + apps/server/src/cli/server.ts | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/apps/server/src/cli/app.test.ts b/apps/server/src/cli/app.test.ts index 1f5a02d15c2f..a8d1262c0733 100644 --- a/apps/server/src/cli/app.test.ts +++ b/apps/server/src/cli/app.test.ts @@ -182,6 +182,7 @@ describe("t3 server command safety", () => { ["start"], ["."], ["node_modules"], + ["C:new-project"], [newDirectory], ["start", newDirectory], ]) { diff --git a/apps/server/src/cli/server.ts b/apps/server/src/cli/server.ts index 8a30d68cf250..c0c6aefb8576 100644 --- a/apps/server/src/cli/server.ts +++ b/apps/server/src/cli/server.ts @@ -34,7 +34,7 @@ export const runDefaultServerCommand = (flags: CliServerFlags) => if (Option.isSome(flags.cwd)) { const cwd = flags.cwd.value.trim(); const fs = yield* FileSystem.FileSystem; - const explicitPath = cwd === "." || cwd === ".." || cwd === "~" || /[/\\]/.test(cwd); + const explicitPath = cwd === "." || cwd === ".." || cwd === "~" || /[/\\]|^[a-z]:/i.test(cwd); if ( !explicitPath && (!(yield* fs.exists(cwd)) || (yield* fs.stat(cwd)).type !== "Directory") From a69b27005fdf7f7ba4cc40de234db9d741e979c7 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Sun, 4 Oct 2026 23:46:10 +0000 Subject: [PATCH 5/6] fix(cli): limit drive prefixes to windows hosts --- apps/server/src/cli/app.test.ts | 23 ++++++++++++++++++++--- apps/server/src/cli/server.ts | 9 ++++++++- 2 files changed, 28 insertions(+), 4 deletions(-) diff --git a/apps/server/src/cli/app.test.ts b/apps/server/src/cli/app.test.ts index a8d1262c0733..a91ea416ffca 100644 --- a/apps/server/src/cli/app.test.ts +++ b/apps/server/src/cli/app.test.ts @@ -139,8 +139,18 @@ describe("t3 server command safety", () => { withTempDirectory("t3-cli-unknown-", (root) => Effect.gen(function* () { const baseDir = NodePath.join(root, "home"); - for (const word of ["account", "login", "clients", "conenct", "package.json"]) { - const error = yield* runCli([word, "--base-dir", baseDir]).pipe(Effect.flip); + for (const word of [ + "account", + "login", + "clients", + "conenct", + "package.json", + "C:new-project", + ]) { + const error = yield* runCli([word, "--base-dir", baseDir]).pipe( + Effect.provideService(HostProcessPlatform, "linux"), + Effect.flip, + ); expect(String(error)).toContain(`Unknown command "${word}"`); expect(yield* pathExists(word)).toBe(word === "package.json"); expect(yield* pathExists(baseDir)).toBe(false); @@ -177,6 +187,7 @@ describe("t3 server command safety", () => { yield* Effect.promise(() => NodeFSP.mkdir(stateDir, { recursive: true })); yield* Effect.promise(() => NodeFSP.writeFile(statePath, record)); const newDirectory = NodePath.join(root, "new-project"); + const platform = yield* HostProcessPlatform; for (const args of [ [], ["start"], @@ -186,7 +197,13 @@ describe("t3 server command safety", () => { [newDirectory], ["start", newDirectory], ]) { - const error = yield* runCli(args, { T3CODE_HOME: baseDir }).pipe(Effect.flip); + const error = yield* runCli(args, { T3CODE_HOME: baseDir }).pipe( + Effect.provideService( + HostProcessPlatform, + args[0] === "C:new-project" ? "win32" : platform, + ), + Effect.flip, + ); expect(String(error)).toContain("A T3 Code server is already running"); expect(yield* Effect.promise(() => NodeFSP.readFile(statePath, "utf8"))).toBe(record); expect(yield* pathExists(newDirectory)).toBe(false); diff --git a/apps/server/src/cli/server.ts b/apps/server/src/cli/server.ts index c0c6aefb8576..08a6a89fc80e 100644 --- a/apps/server/src/cli/server.ts +++ b/apps/server/src/cli/server.ts @@ -1,3 +1,4 @@ +import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Option from "effect/Option"; @@ -34,7 +35,13 @@ export const runDefaultServerCommand = (flags: CliServerFlags) => if (Option.isSome(flags.cwd)) { const cwd = flags.cwd.value.trim(); const fs = yield* FileSystem.FileSystem; - const explicitPath = cwd === "." || cwd === ".." || cwd === "~" || /[/\\]|^[a-z]:/i.test(cwd); + const platform = yield* HostProcessPlatform; + const explicitPath = + cwd === "." || + cwd === ".." || + cwd === "~" || + /[/\\]/.test(cwd) || + (platform === "win32" && /^[a-z]:/i.test(cwd)); if ( !explicitPath && (!(yield* fs.exists(cwd)) || (yield* fs.stat(cwd)).type !== "Directory") From b2e9e837258ea0ea64a0cd34366e4539d5f91d31 Mon Sep 17 00:00:00 2001 From: maria-rcks Date: Mon, 5 Oct 2026 00:28:35 +0000 Subject: [PATCH 6/6] refactor(cli): reuse errors and regression setup --- apps/server/src/cli/config.test.ts | 10 +--------- apps/server/src/cli/config.ts | 8 +------- apps/server/src/cli/server.ts | 14 +++++++------- 3 files changed, 9 insertions(+), 23 deletions(-) diff --git a/apps/server/src/cli/config.test.ts b/apps/server/src/cli/config.test.ts index f64e01952483..efb3ec5d73cb 100644 --- a/apps/server/src/cli/config.test.ts +++ b/apps/server/src/cli/config.test.ts @@ -106,18 +106,10 @@ it.layer(NodeServices.layer)("cli config resolution", (it) => { const cwd = path.join(root, `${name}-project`); const config = yield* resolveServerConfig( { + ...minimalWebFlags(baseDir), mode: Option.some(mode), port: Option.some(8788), - host: Option.none(), - baseDir: Option.some(baseDir), cwd: Option.some(cwd), - devUrl: Option.none(), - noBrowser: Option.none(), - bootstrapFd: Option.none(), - autoBootstrapProjectFromCwd: Option.none(), - logWebSocketEvents: Option.none(), - tailscaleServeEnabled: Option.none(), - tailscaleServePort: Option.none(), }, Option.none(), { rejectRunningServer }, diff --git a/apps/server/src/cli/config.ts b/apps/server/src/cli/config.ts index 5e540847598a..e78b8b54d73b 100644 --- a/apps/server/src/cli/config.ts +++ b/apps/server/src/cli/config.ts @@ -250,12 +250,6 @@ const loadPersistedObservabilitySettings = Effect.fn(function* (settingsPath: st return parsePersistedServerObservabilitySettings(raw); }); -class ServerAlreadyRunningError extends CliError.UserError { - override get message() { - return String(this.cause); - } -} - export const resolveServerConfig = ( flags: CliServerFlags, cliLogLevel: Option.Option, @@ -341,7 +335,7 @@ export const resolveServerConfig = ( if (options?.rejectRunningServer && mode === "web") { const runtime = yield* readPersistedServerRuntimeState(derivedPaths.serverRuntimeStatePath); if (Option.isSome(runtime) && runtime.value.pid > 0 && isProcessAlive(runtime.value.pid)) { - return yield* new ServerAlreadyRunningError({ + return yield* new CliError.UserError({ cause: `A T3 Code server is already running for ${baseDir} (pid ${runtime.value.pid}, ${runtime.value.origin}). Connect to that server, stop it before starting another, or use a different --base-dir.`, }); } diff --git a/apps/server/src/cli/server.ts b/apps/server/src/cli/server.ts index 08a6a89fc80e..44662c36b540 100644 --- a/apps/server/src/cli/server.ts +++ b/apps/server/src/cli/server.ts @@ -2,6 +2,7 @@ import { HostProcessPlatform } from "@t3tools/shared/hostProcess"; import * as Effect from "effect/Effect"; import * as FileSystem from "effect/FileSystem"; import * as Option from "effect/Option"; +import * as Schema from "effect/Schema"; import { Command, GlobalFlag } from "effect/unstable/cli"; import * as CliError from "effect/unstable/cli/CliError"; @@ -9,6 +10,8 @@ import * as ServerConfig from "../config.ts"; import { runServer } from "../server.ts"; import { type CliServerFlags, resolveServerConfig, sharedServerCommandFlags } from "./config.ts"; +const encodeCommand = Schema.encodeEffect(Schema.fromJsonString(Schema.String)); + const runServerCommand = ( flags: CliServerFlags, options?: { @@ -23,12 +26,6 @@ const runServerCommand = ( return yield* runServer.pipe(Effect.provideService(ServerConfig.ServerConfig, config)); }); -class UnknownServerCommandError extends CliError.UserError { - override get message() { - return `Unknown command ${JSON.stringify(this.cause)}. Use "t3 --help" for commands or an explicit path such as "t3 ./my-project" for a new directory.`; - } -} - /** Bare words can name existing directories, but must not create typo projects. */ export const runDefaultServerCommand = (flags: CliServerFlags) => Effect.gen(function* () { @@ -46,7 +43,10 @@ export const runDefaultServerCommand = (flags: CliServerFlags) => !explicitPath && (!(yield* fs.exists(cwd)) || (yield* fs.stat(cwd)).type !== "Directory") ) { - return yield* new UnknownServerCommandError({ cause: cwd }); + return yield* new CliError.UserError({ + cause: cwd, + userMessage: `Unknown command ${yield* encodeCommand(cwd)}. Use "t3 --help" for commands or an explicit path such as "t3 ./my-project" for a new directory.`, + }); } } return yield* runServerCommand(flags, { rejectRunningServer: true });