1import * as HostProcess from "@t3tools/shared/HostProcess";
2import * as Effect from "effect/Effect";
3import * as FileSystem from "effect/FileSystem";
4import * as Option from "effect/Option";
5import * as Schema from "effect/Schema";
6import { Command, GlobalFlag } from "effect/cli";
7import * as CliError from "effect/cli/CliError";
8
9import { awaitServiceLauncherStop } from "../cloud/serviceLauncherClient.ts";
10import * as ServerConfig from "../config.ts";
11import { awaitLauncherLoss } from "../launcherWatch.ts";
12import * as DatabaseMaintenance from "../persistence/DatabaseMaintenance.ts";
13import * as ProcessRunner from "../processRunner.ts";
14import * as AutomaticPort from "./automaticPort.ts";
15import { type CliServerFlags, resolveServerConfig, sharedServerCommandFlags } from "./config.ts";
16
17const encodeCommand = Schema.encodeEffect(Schema.fromJsonString(Schema.String));
18
19const runServerCommand = Effect.fn("server.runServerCommand")(function* (
20 flags: CliServerFlags,
21 options?: {
22 readonly startupPresentation?: ServerConfig.StartupPresentation;
23 readonly forceAutoBootstrapProjectFromCwd?: boolean;
24 readonly rejectRunningServer?: boolean;
25 },
26) {
27 // Before config resolution, which claims a port and creates directories.
28 const { runServer } = yield* Effect.promise(() => import("../server.ts"));
29 const logLevel = yield* GlobalFlag.LogLevel;
30 const selectAutomaticPort = yield* AutomaticPort.make.pipe(Effect.provide(ProcessRunner.layer));
31 const config = yield* resolveServerConfig(flags, logLevel, { ...options, selectAutomaticPort });
32 const run = Effect.raceFirst(
33 runServer.pipe(Effect.provideService(ServerConfig.ServerConfig, config)),
34 awaitServiceLauncherStop().pipe(Effect.andThen(Effect.interrupt)),
35 ).pipe(
36 // Settings' Reclaim stops the server; repair once the database is closed, before the restart.
37 Effect.ensuring(
38 DatabaseMaintenance.runRequestedReclaim(config.dbPath).pipe(
39 Effect.provide(DatabaseMaintenance.layer),
40 ),
41 ),
42 );
43 if (config.mode !== "desktop" || config.desktopBootstrapToken === undefined) return yield* run;
44 return yield* Effect.raceFirst(
45 run,
46 awaitLauncherLoss().pipe(
47 // End like SIGTERM does (interrupt-only exit, so the runtime forces process exit).
48 Effect.andThen(Effect.interrupt),
49 ),
50 );
51});
52
53export const runDefaultServerCommand = Effect.fn("server.runDefaultServerCommand")(function* (
54 flags: CliServerFlags,
55) {
56 if (Option.isSome(flags.cwd)) {
57 const cwd = flags.cwd.value.trim();
58 const fs = yield* FileSystem.FileSystem;
59 const platform = yield* HostProcess.Platform;
60 const explicitPath =
61 cwd === "." ||
62 cwd === ".." ||
63 cwd === "~" ||
64 /[/\\]/.test(cwd) ||
65 (platform === "win32" && /^[a-z]:/i.test(cwd));
66 if (!explicitPath && (!(yield* fs.exists(cwd)) || (yield* fs.stat(cwd)).type !== "Directory")) {
67 return yield* new CliError.UserError({
68 cause: cwd,
69 userMessage: `Unknown command ${yield* encodeCommand(cwd)}. Use "t3 --help" for commands or an explicit path such as "t3 ./my-project" for a new directory.`,
70 });
71 }
72 }
73 return yield* runServerCommand(flags, { rejectRunningServer: true });
74});
75
76export const startCommand = Command.make("start", { ...sharedServerCommandFlags }).pipe(
77 Command.withDescription("Same as `t3`."),
78 Command.unlisted,
79 Command.withHandler((flags) => runServerCommand(flags, { rejectRunningServer: true })),
80);
81
82// Serve never bootstraps a project, so it does not offer the flag that asks for one.
83const { autoBootstrapProjectFromCwd: _, ...serveCommandFlags } = sharedServerCommandFlags;
84
85export const serveCommand = Command.make("serve", serveCommandFlags).pipe(
86 Command.withShortDescription("Run T3 Code in this terminal, with no browser."),
87 Command.withDescription(
88 "Run T3 Code in this terminal without opening a browser, and print pairing details. The background service runs this. To set up access from your other devices, run `t3 up`.",
89 ),
90 Command.withHandler((flags) =>
91 runServerCommand(
92 { ...flags, autoBootstrapProjectFromCwd: Option.none() },
93 {
94 startupPresentation: "headless",
95 forceAutoBootstrapProjectFromCwd: false,
96 },
97 ),
98 ),
99);