Skip to content
Source
Sign inSign up

binCli.ts

145 lines5.3 KB

1import { doctorCommand } from "./cli/doctor.ts";
2import { policyCommand } from "./cli/policy.ts";
3import * as NodeRuntime from "@effect/platform-node/NodeRuntime";
4import * as NodeCrypto from "@effect/platform-node/NodeCrypto";
5import * as NodeServices from "@effect/platform-node/NodeServices";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import { Argument, Command } from "effect/cli";
9import * as CliError from "effect/cli/CliError";
10​
11import * as NetService from "@t3tools/shared/Net";
12import packageJson from "../package.json" with { type: "json" };
13import { acpMcpBridgeCommand, acpMcpCallCommand } from "./cli/acpMcpBridge.ts";
14import { authCommand } from "./cli/auth.ts";
15import { environmentCommand } from "./cli/environment.ts";
16import { appCommand } from "./cli/app.ts";
17import { browserCommand } from "./cli/browser.ts";
18import { pairCommand } from "./cli/pair.ts";
19import { hasCloudPublicConfig } from "./cloud/publicConfig.ts";
20import { extendConnectionAttemptTimeout, keepOutboundFetchOnHttp1 } from "./outboundHttp.ts";
21import { sharedServerCommandFlags } from "./cli/config.ts";
22import { projectCommand } from "./cli/project.ts";
23import { threadCommand } from "./cli/thread.ts";
24import { runDefaultServerCommand, serveCommand, startCommand } from "./cli/server.ts";
25import { updateCommand } from "./cli/update.ts";
26import {
27 confirmCliInstall,
28 inTerminalForeground,
29 installCommand,
30 offerCliInstall,
31} from "./cli/install.ts";
32import { uninstallCommand } from "./cli/uninstall.ts";
33import { runServiceLauncherCli } from "./cli/serviceLauncher.ts";
34import { speechWorkerCommand } from "./cli/speechWorker.ts";
35import { claudeHistoryCommand } from "./cli/claudeHistory.ts";
36import { sshHelperCommand } from "./cli/sshHelper.ts";
37import { serviceCommand } from "./cli/service.ts";
38import { servicePreflightCommand } from "./cli/servicePreflight.ts";
39import { themeCommand } from "./cli/theme.ts";
40import { traceCommand } from "./cli/trace.ts";
41import { triageCommand } from "./cli/triage.ts";
42import { connectCommand, statusCommand, upCommand } from "./cli/up.ts";
43​
44const layerCliRuntime = Layer.mergeAll(NodeServices.layer, NodeCrypto.layer, NetService.layer);
45​
46const connectPublicConfigMissingMessage =
47 "T3 Connect commands are unavailable: this build is missing T3 Connect public configuration.";
48​
49class ConnectPublicConfigMissingError extends CliError.UserError {
50 override get message() {
51 return connectPublicConfigMissingMessage;
52 }
54​
55const connectUnavailableCommand = Command.make("connect", {
56 command: Argument.String("command").pipe(Argument.variadic),
57}).pipe(
58 Command.withDescription("T3 Connect is unavailable in builds without public configuration."),
59 Command.unlisted,
60 Command.withHandler(() =>
61 Effect.fail(
62 new CliError.ShowHelp({
63 commandPath: ["t3", "connect"],
64 errors: [new ConnectPublicConfigMissingError({ cause: connectPublicConfigMissingMessage })],
65 }),
66 ),
67 ),
68);
69​
70export const makeCli = ({ cloudEnabled = hasCloudPublicConfig } = {}) =>
71 Command.make("t3", { ...sharedServerCommandFlags }).pipe(
72 Command.withDescription(
73 "Run T3 Code in this terminal and open it in your browser. To use it from your other devices, run `t3 up`.",
74 ),
75 Command.withHandler(runDefaultServerCommand),
76 Command.withSubcommands([
77 Command.make("help").pipe(
78 Command.withDescription("Show command help."),
79 Command.withHandler(() =>
80 Effect.fail(new CliError.ShowHelp({ commandPath: ["t3"], errors: [] })),
81 ),
82 ),
83 acpMcpBridgeCommand,
84 acpMcpCallCommand,
85 startCommand,
86 serveCommand,
87 appCommand,
88 pairCommand,
89 authCommand,
90 environmentCommand,
91 browserCommand,
92 projectCommand,
93 threadCommand,
94 upCommand,
95 statusCommand,
96 serviceCommand,
97 updateCommand,
98 installCommand,
99 uninstallCommand,
100 // bin.ts runs the bare service invocation; this keeps flags and help working like other commands.
101 Command.make("__service-launcher").pipe(
102 Command.unlisted,
103 // The launcher owns shutdown until its child exits; `runMain` would interrupt that shutdown.
104 Command.withHandler(() => Effect.sync(() => void runServiceLauncherCli())),
105 ),
106 claudeHistoryCommand,
107 speechWorkerCommand,
108 sshHelperCommand,
109​
110 servicePreflightCommand,
111 themeCommand,
112 traceCommand,
113 doctorCommand,
114 policyCommand,
115 triageCommand,
116 cloudEnabled ? connectCommand : connectUnavailableCommand,
117 ]),
118 );
119​
120export const cli = makeCli();
121​
122export function runCli() {
123 extendConnectionAttemptTimeout();
124 keepOutboundFetchOnHttp1();
125 offerCliInstall({
126 args: process.argv.slice(2),
127 // A child started over IPC (a service launcher) has no one to answer.
128 interactive: Boolean(process.stdin.isTTY && process.stdout.isTTY) && process.send === undefined,
129 foreground: inTerminalForeground,
130 confirm: confirmCliInstall,
131 }).pipe(
132 // The offer handles its own errors; even an unexpected defect must not stop the command.
133 Effect.catchDefect(() => Effect.succeed(true)),
134 Effect.flatMap((proceed) =>
135 proceed
136 ? Command.run(cli, { version: packageJson.version })
137 : Effect.sync(() => {
138 process.exitCode = 130;
139 }),
140 ),
141 Effect.scoped,
142 Effect.provide(layerCliRuntime),
143 NodeRuntime.runMain,
144 );