Part II · Boot and connectCLI bootstrap
Chapter 5source checked

The `npx t3` bootstrap path

The published package resolves commands and configuration, locates its copied web client, and hands one readonly ServerConfig value to the layered runtime.

What this chapter resolves
  • Trace npm's executable entry through command selection and server launch.
  • Apply the real CLI, environment, desktop-bootstrap, and default precedence rules.
  • Distinguish bundled JavaScript, copied web assets, and native external dependencies.

npx t3 looks like one command, but it crosses three distinct systems: npm selects a published executable, the Effect CLI selects a command and resolves configuration, and the server runtime acquires the services described in Chapter 6. Keeping those stages separate makes several otherwise surprising behaviors obvious.

What npm actually installs

The package is named t3, exposes dist/bin.mjs as the t3 executable, and publishes only dist. That directory is deliberately more than one JavaScript file.

Artifacts inside or beside the published T3 CLI bundle
ArtifactHow it gets thereWhy it remains distinct
dist/bin.mjsbundled from src/bin.tsthe npm executable and command graph
dist/service-launcher.mjssecond bundle entrystandalone managed-update trial and handoff process
dist/client/*copied from the completed web buildstatic web application served by the CLI
selected node_moduleskept external by policynative binaries/loaders must exist on the real filesystem
Figure 5.1 · Published package exploderbuild edges are not runtime calls
T3 CLI package artifact graphDiagram loading

Zoom with the controls, +/, or Ctrl/ + trackpad scroll. Enable Pan to drag, use two-finger scrolling, or use the arrow keys. 0 fits the diagram; Esc leaves Pan or expanded view.

T3 CLI package artifact graph
Text equivalent

The source CLI and service launcher become separate files in dist. The web build is copied to dist/client. Native packages remain in installed node_modules. package.json maps the t3 executable to dist/bin.mjs and publishes only dist.

Figure 5.1. The server build waits for the web build, bundles the CLI and service launcher, copies the web output, and leaves the native dependency closure external. Publish then asserts that the required executable, launcher, and client entry exist.

One entry, several command meanings

apps/server/src/bin.ts:23–71 ↗verbatim · typescript · 4cbb1e25
const connectPublicConfigMissingMessage =
  "T3 Connect commands are unavailable: this build is missing T3 Connect public configuration.";
 
class ConnectPublicConfigMissingError extends CliError.UserError {
  override get message() {
    return connectPublicConfigMissingMessage;
  }
}
 
const connectUnavailableCommand = Command.make("connect", {
  command: Argument.string("command").pipe(Argument.variadic),
}).pipe(
  Command.withDescription("T3 Connect is unavailable in builds without public configuration."),
  Command.withHidden,
  Command.withHandler(() =>
    Effect.fail(
      new CliError.ShowHelp({
        commandPath: ["t3", "connect"],
        errors: [new ConnectPublicConfigMissingError({ cause: connectPublicConfigMissingMessage })],
      }),
    ),
  ),
);
 
export const makeCli = ({ cloudEnabled = hasCloudPublicConfig } = {}) =>
  Command.make("t3", { ...sharedServerCommandFlags }).pipe(
    Command.withDescription("Run the T3 Code server."),
    Command.withHandler((flags) => runServerCommand(flags)),
    Command.withSubcommands([
      startCommand,
      serveCommand,
      pairCommand,
      authCommand,
      projectCommand,
      serviceCommand,
      servicePreflightCommand,
      triageCommand,
      cloudEnabled ? connectCommand : connectUnavailableCommand,
    ]),
  );
 
export const cli = makeCli();
 
if (import.meta.main) {
  Command.run(cli, { version: packageJson.version }).pipe(
    Effect.scoped,
    Effect.provide(CliRuntimeLayer),
    NodeRuntime.runMain,
  );
Read this as: Bare t3 uses the root handler; start and serve are explicit subcommands. Connect is always registered: builds without public cloud configuration install a hidden handler that returns an explanatory failure.

Bare t3 and t3 start both call runServerCommand with normal browser presentation. t3 serve is not merely an alias for --no-browser: it requests headless presentation and forces cwd auto-bootstrap off so a service can start without inventing project state.

apps/server/src/cli/server.ts:21–35 ↗verbatim · typescript · 11441311
export const startCommand = Command.make("start", { ...sharedServerCommandFlags }).pipe(
  Command.withDescription("Run the T3 Code server."),
  Command.withHandler((flags) => runServerCommand(flags)),
);
 
export const serveCommand = Command.make("serve", { ...sharedServerCommandFlags }).pipe(
  Command.withDescription(
    "Run the T3 Code server without opening a browser and print headless pairing details.",
  ),
  Command.withHandler((flags) =>
    runServerCommand(flags, {
      startupPresentation: "headless",
      forceAutoBootstrapProjectFromCwd: false,
    }),
  ),
Read this as: The command choice can override later configuration. Headless startup wins over a no-browser input, and serve explicitly disables cwd project bootstrap.
Interactive flow

Follow `npx t3` into the runtime

Use Previous/Next or select a stage; the moving token stops where ownership changes.

1 / 7

Step 1 of 7: Resolve

npmTransport boundaryResolve

npm selects the published executable

The package bin map points the t3 command at dist/bin.mjs. The copied client and launcher are sibling runtime artifacts, not source-tree assumptions.

  1. 1. Resolve · npm selects the published executable (Transport boundary)

    The package bin map points the t3 command at dist/bin.mjs. The copied client and launcher are sibling runtime artifacts, not source-tree assumptions.

    apps/server/package.json:1–52apps/server/scripts/cli.ts:225–304
  2. 2. Dispatch · The Effect CLI selects a handler (Runtime work)

    Bare, start, serve, auth, pair, project, service, hidden __service-preflight, triage, and connect share one typed command graph. Connect is functional or hidden-unavailable according to the compiled cloud configuration.

    apps/server/src/bin.ts:23–71apps/server/src/cli/servicePreflight.ts:7–16apps/server/src/cli/server.ts:8–35
  3. 3. Resolve config · Inputs collapse into one ServerConfig (Durable state)

    Each field selects from the channels that actually apply to it; the resolver also creates required directories and constructs one readonly service value.

    apps/server/src/cli/config.ts:20–191apps/server/src/cli/config.ts:210–392
  4. 4. Locate UI · Development and packaged clients diverge (Client state)

    A dev URL disables eager staticDir resolution. Loopback requests redirect to the development server; other eligible requests may still lazily locate the built client. Without a dev URL, lookup is eager.

    apps/server/src/config.ts:206–225apps/server/src/http.ts:233–322
  5. 5. Launch · Configuration becomes an Effect service (Runtime work)

    runServerCommand provides the resolved ServerConfig to runServer. Layer acquisition now owns the process lifetime and registers activation-aware roots.

    apps/server/src/cli/server.ts:8–35apps/server/src/server.ts:474–689
  6. 6. Prepare · The listener and parked roots reach the activation boundary (Runtime work)

    Presentation is registered behind the same gate. Startup waits for the listener and auxiliary roots, prepares the launcher trial, publishes welcome, and then activates.

    apps/server/src/serverRuntimeStartup.ts:385–583apps/server/src/serverActivation.ts:11–25
  7. 7. Present · Presentation mode chooses how access is exposed (Transport boundary)

    Web browser presentation resolves a pairing target and honors noBrowser; desktop resolves its base URL. Headless output alone reads the bound port, combines it with configured host policy, and prints a credential, fragment URL, and QR code.

    apps/server/src/serverRuntimeStartup.ts:257–288apps/server/src/startupAccess.ts:71–147

Configuration precedence is field-specific

The common helper selects the first present Option, but callers supply different subsets of channels and defaults. Mode and port consult CLI, environment, desktop bootstrap, and a default; dev URL has CLI/environment inputs; cwd uses CLI/process cwd; presentation comes from the selected command. “CLI always wins” is therefore directionally useful, not a complete specification.

apps/server/src/cli/config.ts:244–280 ↗verbatim · typescript · f772af33
    const mode: ServerConfig.RuntimeMode = Option.getOrElse(
      resolveOptionPrecedence(
        normalizedFlags.mode,
        Option.fromUndefinedOr(env.mode),
        Option.fromUndefinedOr(bootstrap?.mode),
      ),
      () => "web",
    );
 
    const port = yield* Option.match(
      resolveOptionPrecedence(
        normalizedFlags.port,
        Option.fromUndefinedOr(env.port),
        Option.fromUndefinedOr(bootstrap?.port),
      ),
      {
        onSome: (value) => Effect.succeed(value),
        onNone: () => {
          if (mode === "desktop") {
            return Effect.succeed(ServerConfig.DEFAULT_PORT);
          }
          return findAvailablePort(ServerConfig.DEFAULT_PORT);
        },
      },
    );
    const devUrl = Option.getOrElse(
      resolveOptionPrecedence(normalizedFlags.devUrl, Option.fromUndefinedOr(env.devUrl)),
      () => undefined,
    );
    const explicitBaseDir = resolveOptionPrecedence(
      normalizedFlags.baseDir,
      Option.fromUndefinedOr(env.t3Home),
    ).pipe(Option.filter((value) => value.trim().length > 0));
    const baseDir = yield* resolveBaseDir(
      Option.getOrUndefined(
        resolveOptionPrecedence(explicitBaseDir, Option.fromUndefinedOr(bootstrap?.t3Home)),
      ),
Read this as: Mode and port use CLI → environment → desktop bootstrap → default. Base directory treats CLI or T3CODE_HOME as explicit before consulting the desktop bootstrap envelope.
Interactive lab

Resolve four startup configurations

Choose an input set and inspect the winning channel and downstream consequence.

Scenario 1 of 4: Web defaults

Web defaultsExpected path

Prefer 3773, then ask the OS for an ephemeral port

With no explicit mode or port, the CLI uses web mode and tries the default once before reserving an OS-assigned loopback port.

  • cwd defaults to the invoking process and is normalized to an absolute path.
  • Browser presentation is enabled by default.
  • cwd project auto-bootstrap defaults on in web mode.
  • An undefined host becomes the concrete loopback bind 127.0.0.1 in both platform implementations.
  1. Web defaults · Prefer 3773, then ask the OS for an ephemeral port (Expected path)

    With no explicit mode or port, the CLI uses web mode and tries the default once before reserving an OS-assigned loopback port.

    • cwd defaults to the invoking process and is normalized to an absolute path.
    • Browser presentation is enabled by default.
    • cwd project auto-bootstrap defaults on in web mode.
    • An undefined host becomes the concrete loopback bind 127.0.0.1 in both platform implementations.
    apps/server/src/cli/config.ts:210–392packages/shared/src/Net.ts:194–200apps/server/src/config.ts:99–155apps/server/src/server.ts:123–240
  2. CLI beats env · Explicit flags win their fields (Alternative)

    A CLI mode, port, host, or base directory is selected before the corresponding environment and bootstrap values.

    • Precedence is evaluated independently per field.
    • An explicit base directory changes how state paths are derived.
    • Supplying a dev URL disables eager static lookup; non-loopback fallback may still resolve built assets lazily.
    apps/server/src/cli/config.ts:20–191apps/server/src/cli/config.ts:210–392apps/server/src/config.ts:99–155
  3. Desktop bootstrap · The parent can inject one-time process configuration (Caveat)

    When no higher-precedence value exists, a bootstrap envelope supplies mode, port, host, T3 home, telemetry descriptors, and presentation-related settings.

    • The bootstrap envelope is read only when a bootstrap fd is configured.
    • The Electron parent normally chooses a port before placing it in that envelope.
    • Standalone CLI desktop-mode fallback is exactly port 3773; desktop defaults to loopback host and no browser.
    apps/server/src/cli/config.ts:210–392apps/server/src/cli/config.ts:20–191apps/desktop/src/app/DesktopApp.ts:69–103apps/desktop/src/backend/DesktopBackendConfiguration.ts:366–414
  4. Headless serve · The command overrides presentation intent (Expected path)

    Serve forces headless output and disables automatic cwd project creation even if ordinary web defaults would enable it.

    • No browser is opened.
    • The output uses the listener's actual port, not only the configured fallback.
    • The pairing credential is placed in the URL fragment.
    apps/server/src/cli/server.ts:8–35apps/server/src/startupAccess.ts:71–147

State paths and client selection

The resolver derives paths only after the base directory and dev URL are known, creates the cwd and server directories, loads persisted observability endpoints, and then returns the effective readonly ServerConfig value. A dev URL enables a loopback redirect and disables eager static lookup; a non-redirected eligible request can still resolve built assets lazily. Without a dev URL, lookup eagerly checks the compiled-adjacent client directory before the monorepo fallback. If neither has an index, the catch-all route returns HTTP 503 instead of an application shell.

T3
Source-locked editionRead against fa219001d · 23 Aug 2026
Book search

Find a concept, module, or source path

Type two or more characters.