261 lines · 8.8 KB · TypeScript
RawDownload
// @effect-diagnostics globalTimers:off -- The Node socket client owns its response deadline and clears it on every completion path.import * as NodeCrypto from "node:crypto";import * as NodeNet from "node:net";import * as NodeOS from "node:os"; import {  DESKTOP_APP_ACTIVATION_PROTOCOL_VERSION,  DesktopAppActivationErrorCode,  DesktopAppActivationResponse,  type DesktopAppActivationPlatform,  type DesktopAppActivationRequest,} from "@t3tools/contracts";import { resolveDesktopAppControlAddress } from "@t3tools/shared/desktopAppControl";import {  HostProcessPlatform,  HostProcessUserId,  HostProcessWorkingDirectory,} from "@t3tools/shared/hostProcess";import * as Config from "effect/Config";import * as Console from "effect/Console";import * as Effect from "effect/Effect";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as Schema from "effect/Schema";import { Argument, Command } from "effect/unstable/cli"; import { expandHomePath, resolveBaseDir } from "../os-jank.ts";import { baseDirFlag } from "./config.ts"; const CLI_RESPONSE_TIMEOUT_MS = 17_000;const MAX_RESPONSE_BYTES = 64 * 1024;const isDesktopAppActivationResponse = Schema.is(DesktopAppActivationResponse); export class DesktopAppSshUnsupportedError extends Schema.TaggedErrorClass<DesktopAppSshUnsupportedError>()(  "DesktopAppSshUnsupportedError",  {},) {  override get message(): string {    return "`t3 app` only controls a desktop app on the same machine. It cannot run over SSH.";  }} export class DesktopAppPlatformUnsupportedError extends Schema.TaggedErrorClass<DesktopAppPlatformUnsupportedError>()(  "DesktopAppPlatformUnsupportedError",  { platform: Schema.String },) {  override get message(): string {    return `\`t3 app\` is not supported on ${this.platform}.`;  }} export class DesktopAppUnreachableError extends Schema.TaggedErrorClass<DesktopAppUnreachableError>()(  "DesktopAppUnreachableError",  {    candidateAddresses: Schema.Array(Schema.String),    requestId: Schema.String,    workspaceRoot: Schema.String,    cause: Schema.Defect(),  },) {  override get message(): string {    return "Could not reach the T3 Code desktop app. Start or update the desktop app on this machine, then run `t3 app` again. A running T3 Code server is not enough.";  }} export class DesktopAppRequestFailedError extends Schema.TaggedErrorClass<DesktopAppRequestFailedError>()(  "DesktopAppRequestFailedError",  {    code: DesktopAppActivationErrorCode,    requestId: Schema.String,    workspaceRoot: Schema.String,    cause: Schema.Defect(),  },) {  override get message(): string {    return `T3 Code could not open ${this.workspaceRoot} (${this.code}).`;  }} function isDesktopPlatform(platform: NodeJS.Platform): platform is DesktopAppActivationPlatform {  return platform === "darwin" || platform === "linux" || platform === "win32";} export function sendDesktopAppActivationRequest(input: {  readonly address: string;  readonly fallbackAddress?: string;  readonly request: DesktopAppActivationRequest;  readonly timeoutMs?: number;}): Promise<DesktopAppActivationResponse> {  return new Promise((resolve, reject) => {    const socket = NodeNet.createConnection(input.address);    socket.setEncoding("utf8");    let buffer = "";    let settled = false;    let connected = false;     const finish = (      result:        | { readonly type: "success"; readonly response: DesktopAppActivationResponse }        | { readonly type: "failure"; readonly error: Error },    ) => {      if (settled) return;      settled = true;      clearTimeout(timeout);      socket.destroy();      if (result.type === "success") resolve(result.response);      else reject(result.error);    };     const timeout = setTimeout(() => {      finish({        type: "failure",        error: new Error("The desktop app did not respond in time."),      });    }, input.timeoutMs ?? CLI_RESPONSE_TIMEOUT_MS);     socket.once("connect", () => {      connected = true;      socket.write(`${JSON.stringify(input.request)}\n`);    });    socket.on("data", (chunk) => {      buffer += chunk;      if (Buffer.byteLength(buffer, "utf8") > MAX_RESPONSE_BYTES) {        finish({ type: "failure", error: new Error("The desktop app response is too large.") });        return;      }      const newline = buffer.indexOf("\n");      if (newline === -1) return;       let parsed: unknown;      try {        parsed = JSON.parse(buffer.slice(0, newline));      } catch {        finish({          type: "failure",          error: new Error("The desktop app response is not valid JSON."),        });        return;      }      if (!isDesktopAppActivationResponse(parsed)) {        finish({ type: "failure", error: new Error("The desktop app response is invalid.") });        return;      }      if (parsed.requestId !== input.request.requestId) {        finish({          type: "failure",          error: new Error("The desktop app response did not match this request."),        });        return;      }      finish({ type: "success", response: parsed });    });    socket.once("error", (error: NodeJS.ErrnoException) => {      if (        !settled &&        !connected &&        input.fallbackAddress !== undefined &&        (error.code === "ENOENT" || error.code === "ECONNREFUSED")      ) {        settled = true;        clearTimeout(timeout);        socket.destroy();        resolve(          sendDesktopAppActivationRequest({            address: input.fallbackAddress,            request: input.request,            ...(input.timeoutMs === undefined ? {} : { timeoutMs: input.timeoutMs }),          }),        );        return;      }      finish({ type: "failure", error });    });    socket.once("end", () => {      finish({ type: "failure", error: new Error("The desktop app closed the connection.") });    });  });} const appEnvironment = Config.all({  t3Home: Config.string("T3CODE_HOME").pipe(Config.option, Config.map(Option.getOrUndefined)),  sshConnection: Config.string("SSH_CONNECTION").pipe(Config.option),  sshTty: Config.string("SSH_TTY").pipe(Config.option),}); const runAppCommand = Effect.fn("cli.app")(function* (flags: {  readonly baseDir: Option.Option<string>;  readonly workspaceRoot: Option.Option<string>;}) {  const environment = yield* appEnvironment;  const hostPlatform = yield* HostProcessPlatform;  if (Option.isSome(environment.sshConnection) || Option.isSome(environment.sshTty)) {    return yield* new DesktopAppSshUnsupportedError({});  }  if (!isDesktopPlatform(hostPlatform)) {    return yield* new DesktopAppPlatformUnsupportedError({ platform: hostPlatform });  }   const path = yield* Path.Path;  const configuredBaseDir = Option.getOrUndefined(flags.baseDir) ?? environment.t3Home;  const baseDir = yield* resolveBaseDir(configuredBaseDir);  const allowDevFallback = Option.isNone(flags.baseDir) && !environment.t3Home?.trim();  const rawWorkspaceRoot =    Option.getOrUndefined(flags.workspaceRoot) ?? (yield* HostProcessWorkingDirectory);  const workspaceRoot = path.resolve(yield* expandHomePath(rawWorkspaceRoot));  const userId = yield* HostProcessUserId;  const resolveAddress = (stateSubdirectory: "userdata" | "dev") =>    resolveDesktopAppControlAddress({      stateDir: path.join(baseDir, stateSubdirectory),      platform: hostPlatform,      tempDir: NodeOS.tmpdir(),      userId,      joinPath: path.join,    }).address;  const request: DesktopAppActivationRequest = {    version: DESKTOP_APP_ACTIVATION_PROTOCOL_VERSION,    requestId: NodeCrypto.randomUUID(),    type: "open-workspace",    workspaceRoot,    platform: hostPlatform,  };  const address = resolveAddress("userdata");  const fallbackAddress = allowDevFallback ? resolveAddress("dev") : undefined;   const response = yield* Effect.tryPromise({    try: () =>      sendDesktopAppActivationRequest({        address,        ...(fallbackAddress === undefined ? {} : { fallbackAddress }),        request,      }),    catch: (cause) =>      new DesktopAppUnreachableError({        candidateAddresses: fallbackAddress === undefined ? [address] : [address, fallbackAddress],        requestId: request.requestId,        workspaceRoot,        cause,      }),  });  if (!response.ok) {    return yield* new DesktopAppRequestFailedError({      code: response.code,      requestId: response.requestId,      workspaceRoot,      cause: response,    });  }   yield* Console.log(`Opened ${workspaceRoot} in T3 Code.`);}); export const appCommand = Command.make("app", {  baseDir: baseDirFlag,  workspaceRoot: Argument.string("path").pipe(    Argument.withDescription("Project directory. Default: current directory."),    Argument.optional,  ),}).pipe(  Command.withDescription("Open a project in the running T3 Code desktop app."),  Command.withHandler(runAppCommand),);