1695 lines · 55.4 KB · TypeScript
RawDownload
import type {  DesktopSshEnvironmentBootstrap,  DesktopSshEnvironmentTarget,} from "@t3tools/contracts";import * as NetService from "@t3tools/shared/Net";import { extractJsonObject, fromLenientJson } from "@t3tools/shared/schemaJson";import { satisfiesSemverRange } from "@t3tools/shared/semver";import * as Context from "effect/Context";import * as Deferred from "effect/Deferred";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Exit from "effect/Exit";import * as FileSystem from "effect/FileSystem";import * as Layer from "effect/Layer";import * as Option from "effect/Option";import * as Path from "effect/Path";import * as Ref from "effect/Ref";import * as Schedule from "effect/Schedule";import * as Schema from "effect/Schema";import * as Scope from "effect/Scope";import * as Stream from "effect/Stream";import { HttpClient, HttpClientRequest } from "effect/unstable/http";import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"; import {  buildSshChildEnvironment,  type SshAuthOptions,  SshPasswordPrompt,  isSshAuthFailure,} from "./auth.ts";import {  baseSshArgs,  buildSshHostSpecEffect,  collectProcessOutput,  getLastNonEmptyOutputLine,  remoteStateKey,  resolveSshTarget,  runSshCommand,  SSH_COMMAND,  targetConnectionKey,} from "./command.ts";import {  SshCommandError,  SshHttpBridgeError,  SshInvalidTargetError,  SshLaunchError,  SshPairingError,  SshPasswordPromptError,  SshReadinessError,} from "./errors.ts"; export const DEFAULT_REMOTE_PORT = 3773;const REMOTE_PORT_SCAN_WINDOW = 200;const SSH_READY_TIMEOUT_MS = 20_000;const SSH_READY_PROBE_TIMEOUT_MS = 1_000;const TUNNEL_SHUTDOWN_TIMEOUT_MS = 2_000;const REMOTE_READY_TIMEOUT_MS = 15_000;const REMOTE_REUSE_READY_TIMEOUT_MS = 2_000; export interface RemoteT3RunnerOptions {  readonly packageSpec?: string;  readonly nodeScriptPath?: string | null;  readonly nodeEngineRange?: string | null;} export interface SshEnvironmentManagerOptions {  readonly resolveCliPackageSpec?: () => string;  readonly resolveCliRunner?: Effect.Effect<RemoteT3RunnerOptions>;} interface SshTunnelEntry {  readonly key: string;  readonly target: DesktopSshEnvironmentTarget;  readonly remotePort: number;  readonly remoteServerKind: "external" | "managed" | null;  readonly localPort: number;  readonly httpBaseUrl: string;  readonly wsBaseUrl: string;  readonly process: ChildProcessSpawner.ChildProcessHandle;  readonly scope: Scope.Scope;} type SshEnvironmentEffectContext =  | ChildProcessSpawner.ChildProcessSpawner  | FileSystem.FileSystem  | Path.Path  | HttpClient.HttpClient  | NetService.NetService  | SshPasswordPrompt; type SshEnvironmentEffectError =  | SshCommandError  | SshInvalidTargetError  | SshLaunchError  | SshPairingError  | SshReadinessError  | SshPasswordPromptError  | NetService.NetError; function makeSshTunnelCancelledError(target: DesktopSshEnvironmentTarget): SshCommandError {  return new SshCommandError({    command: ["ssh"],    exitCode: null,    stderr: "",    message: `SSH environment connection was cancelled for ${target.alias || target.hostname}.`,  });} function sshTargetLogFields(target: DesktopSshEnvironmentTarget) {  return {    alias: target.alias,    hostname: target.hostname,    username: target.username,    port: target.port,  };} function sshRunnerLogFields(runner: RemoteT3RunnerOptions | undefined) {  if (runner?.nodeScriptPath?.trim()) {    return { runner: "node-script", nodeScriptPath: runner.nodeScriptPath.trim() };  }  if (runner?.packageSpec?.trim()) {    return { runner: "package", packageSpec: runner.packageSpec.trim() };  }  return { runner: "default" };} interface SshAuthOperationInput<T> {  readonly key: string;  readonly target: DesktopSshEnvironmentTarget;  readonly operation: (    authOptions: SshAuthOptions,  ) => Effect.Effect<T, SshEnvironmentEffectError, SshEnvironmentEffectContext>;} interface SshAuthAttemptInput<T> extends SshAuthOperationInput<T> {  readonly promptCount: number;  readonly authSecret: string | null;} export interface SshEnvironmentManagerShape {  readonly ensureEnvironment: (    target: DesktopSshEnvironmentTarget,    options?: { readonly issuePairingToken?: boolean },  ) => Effect.Effect<    DesktopSshEnvironmentBootstrap,    SshEnvironmentEffectError,    SshEnvironmentEffectContext  >;  readonly disconnectEnvironment: (    target: DesktopSshEnvironmentTarget,  ) => Effect.Effect<void, SshEnvironmentEffectError, SshEnvironmentEffectContext>;} const RemoteLaunchResult = Schema.Struct({  remotePort: Schema.Number,  serverKind: Schema.optional(Schema.Literals(["external", "managed"])),}); const RemotePairingResult = Schema.Struct({  credential: Schema.String,}); const decodeRemoteLaunchResult = Schema.decodeEffect(fromLenientJson(RemoteLaunchResult));const decodeRemotePairingResult = Schema.decodeEffect(fromLenientJson(RemotePairingResult)); const decodeRemoteJsonOutput = <A, E>(  stdout: string,  decode: (input: string) => Effect.Effect<A, E>,): Effect.Effect<A, E> =>  decode(stdout).pipe(    Effect.catch((error) =>      Effect.gen(function* () {        const jsonObject = extractJsonObject(stdout);        if (jsonObject === stdout.trim()) {          return yield* Effect.fail(error);        }        const exit = yield* Effect.exit(decode(jsonObject));        if (Exit.isSuccess(exit)) {          return exit.value;        }        return yield* Effect.fail(error);      }),    ),  ); const decodeRemoteLaunchOutput = (stdout: string) =>  decodeRemoteJsonOutput(stdout, decodeRemoteLaunchResult); const decodeRemotePairingOutput = (stdout: string) =>  decodeRemoteJsonOutput(stdout, decodeRemotePairingResult); const remoteNodeEngineCheckMain = function remoteNodeEngineCheckMain() {  const range = process.argv[2] || "";  const rawVersion =    process.versions && process.versions.node ? process.versions.node : process.version;   if (!satisfiesSemverRange(rawVersion, range)) {    process.stderr.write(      "Remote node " + rawVersion + " does not satisfy required range " + range + ".\n",    );    process.exit(1);  }}; function buildRemoteNodeEngineCheckScript(): string {  return `${satisfiesSemverRange.toString()}(${remoteNodeEngineCheckMain.toString()})();`;} export function normalizeSshErrorMessage(stderr: string, fallbackMessage: string): string {  const cleaned = stderr.trim();  return cleaned.length > 0 ? cleaned : fallbackMessage;} function stripTrailingNewlines(value: string): string {  return value.replace(/\n+$/u, "");} function shellSingleQuote(value: string): string {  return `'${value.replaceAll("'", "'\\''")}'`;} function applyScriptPlaceholders(  template: string,  replacements: Readonly<Record<string, string>>,): string {  let result = template;  for (const [token, value] of Object.entries(replacements)) {    result = result.replaceAll(`@@${token}@@`, value);  }  return result;} export function describeReadinessCause(cause: unknown): unknown {  if (cause instanceof SshReadinessError) {    return {      _tag: cause._tag,      message: cause.message,      ...(cause.cause === undefined ? {} : { cause: describeReadinessCause(cause.cause) }),    };  }  if (cause instanceof Error) {    return {      name: cause.name,      message: cause.message,      ...(cause.cause === undefined ? {} : { cause: describeReadinessCause(cause.cause) }),    };  }  if (typeof cause !== "object" || cause === null) {    return cause;  }   const record = cause as Readonly<Record<string, unknown>>;  return {    ...(typeof record._tag === "string" ? { _tag: record._tag } : {}),    ...(typeof record.message === "string" ? { message: record.message } : {}),    ...(record.reason === undefined ? {} : { reason: describeReadinessCause(record.reason) }),    ...(record.cause === undefined ? {} : { cause: describeReadinessCause(record.cause) }),  };} export const REMOTE_PICK_PORT_SCRIPT = `const fs = require("node:fs");const net = require("node:net");const filePath = process.argv[2] ?? "";const defaultPort = Number.parseInt(process.argv[3] ?? "", 10);const scanWindow = Number.parseInt(process.argv[4] ?? "", 10);const raw = fs.existsSync(filePath) ? fs.readFileSync(filePath, "utf8").trim() : "";const preferred = Number.parseInt(raw, 10);const start = Number.isInteger(preferred) ? preferred : defaultPort;const end = start + scanWindow; function tryPort(port) {  return new Promise((resolve) => {    const server = net.createServer();    server.unref();    server.once("error", () => resolve(false));    server.listen(port, "127.0.0.1", () => {      server.close((error) => resolve(error ? false : port));    });  });} (async () => {  for (let port = start; port < end; port += 1) {    const available = await tryPort(port);    if (available) {      process.stdout.write(String(port));      return;    }  }  process.exit(1);})().catch(() => process.exit(1));`; export const REMOTE_WAIT_READY_SCRIPT = `const http = require("node:http");const port = Number.parseInt(process.argv[2] ?? "", 10);const timeoutMs = Number.parseInt(process.argv[3] ?? "", 10);const probeTimeoutMs = Number.parseInt(process.argv[4] ?? "", 10);if (!Number.isInteger(port) || !Number.isInteger(timeoutMs) || !Number.isInteger(probeTimeoutMs)) {  process.exit(1);}const deadline = Date.now() + timeoutMs; function sleep(ms) {  return new Promise((resolve) => setTimeout(resolve, ms));} function probe() {  return new Promise((resolve) => {    const request = http.get(      {        hostname: "127.0.0.1",        port,        path: "/",        timeout: probeTimeoutMs,      },      (response) => {        response.resume();        response.once("end", () => {          resolve(response.statusCode >= 200 && response.statusCode < 300);        });      },    );    request.once("timeout", () => {      request.destroy();      resolve(false);    });    request.once("error", () => resolve(false));  });} (async () => {  while (Date.now() < deadline) {    if (await probe()) {      process.exit(0);    }    await sleep(100);  }  process.exit(1);})().catch(() => process.exit(1));`; export const REMOTE_NODE_ENV_SCRIPT = `prepend_path_if_dir() {  if [ -d "$1" ]; then    case ":$PATH:" in      *":$1:"*) ;;      *) PATH="$1:$PATH" ;;    esac  fi} remote_node_satisfies_engine() {  T3_NODE_ENGINE_RANGE=@@T3_NODE_ENGINE_RANGE@@  if [ -z "$T3_NODE_ENGINE_RANGE" ]; then    return 0  fi  node - "$T3_NODE_ENGINE_RANGE" <<'NODE'@@T3_NODE_ENGINE_CHECK_SCRIPT@@NODE} ensure_remote_node_path() {  if command -v node >/dev/null 2>&1 && remote_node_satisfies_engine >/dev/null 2>&1; then    return 0  fi   prepend_path_if_dir "$HOME/.local/bin"  prepend_path_if_dir "$HOME/bin"  prepend_path_if_dir "/opt/homebrew/bin"  prepend_path_if_dir "/usr/local/bin"  prepend_path_if_dir "/usr/bin"  prepend_path_if_dir "/bin"   if [ -z "\${VOLTA_HOME:-}" ]; then    VOLTA_HOME="$HOME/.volta"  fi  export VOLTA_HOME  prepend_path_if_dir "$VOLTA_HOME/bin"   prepend_path_if_dir "$HOME/.asdf/shims"  prepend_path_if_dir "$HOME/.asdf/bin"  if [ ! -x "$HOME/.asdf/shims/node" ] && [ -s "$HOME/.asdf/asdf.sh" ]; then    # shellcheck disable=SC1090    . "$HOME/.asdf/asdf.sh"  fi   prepend_path_if_dir "$HOME/.local/share/mise/shims"  prepend_path_if_dir "$HOME/.mise/shims"  if ! command -v node >/dev/null 2>&1 && command -v mise >/dev/null 2>&1; then    eval "$(mise activate sh)" >/dev/null 2>&1 || true  fi   if [ -z "\${FNM_DIR:-}" ]; then    FNM_DIR="$HOME/.local/share/fnm"  fi  export FNM_DIR  prepend_path_if_dir "$FNM_DIR"  prepend_path_if_dir "$HOME/.fnm"  if ! command -v node >/dev/null 2>&1 && command -v fnm >/dev/null 2>&1; then    eval "$(fnm env --use-on-cd --shell sh)" >/dev/null 2>&1 || eval "$(fnm env --shell sh)" >/dev/null 2>&1 || true  fi   prepend_path_if_dir "$HOME/.nodenv/bin"  prepend_path_if_dir "$HOME/.nodenv/shims"  if ! command -v node >/dev/null 2>&1 && command -v nodenv >/dev/null 2>&1; then    eval "$(nodenv init -)" >/dev/null 2>&1 || true  fi   if [ -z "\${NVM_DIR:-}" ]; then    NVM_DIR="$HOME/.nvm"  fi  export NVM_DIR   if [ -s "$NVM_DIR/nvm.sh" ]; then    # shellcheck disable=SC1090    . "$NVM_DIR/nvm.sh"    if ! command -v node >/dev/null 2>&1 && command -v nvm >/dev/null 2>&1; then      nvm use --silent default >/dev/null 2>&1 || nvm use --silent node >/dev/null 2>&1 || nvm use --silent --lts >/dev/null 2>&1 || true    fi  fi   if ! command -v node >/dev/null 2>&1 && [ -d "$NVM_DIR/versions/node" ]; then    for T3_NODE_BIN in "$NVM_DIR"/versions/node/*/bin; do      if [ -x "$T3_NODE_BIN/node" ]; then        PATH="$T3_NODE_BIN:$PATH"        export PATH      fi    done  fi   command -v node >/dev/null 2>&1 && remote_node_satisfies_engine}`; export const REMOTE_RUNNER_SCRIPT = `#!/bin/shset -eu@@T3_NODE_ENV_SCRIPT@@ensure_remote_node_path || trueT3_NODE_SCRIPT_PATH=@@T3_NODE_SCRIPT_PATH@@if [ -n "$T3_NODE_SCRIPT_PATH" ]; then  if ! command -v node >/dev/null 2>&1; then    printf 'Remote host is missing node on PATH. Install Node or configure a supported version manager for non-interactive shells.\\n' >&2    exit 1  fi  exec node "$T3_NODE_SCRIPT_PATH" "$@"fiif command -v t3 >/dev/null 2>&1; then  exec t3 "$@"fiif command -v npx >/dev/null 2>&1; then  exec npx --yes @@T3_PACKAGE_SPEC@@ "$@"fiif command -v npm >/dev/null 2>&1; then  exec npm exec --yes @@T3_PACKAGE_SPEC@@ -- "$@"fiprintf 'Remote host is missing the t3 CLI and could not install @@T3_PACKAGE_SPEC@@ because node/npm/npx are unavailable on PATH. Install Node or configure a supported version manager for non-interactive shells.\\n' >&2exit 1`; export const REMOTE_LAUNCH_SCRIPT = `set -eu@@T3_NODE_ENV_SCRIPT@@STATE_KEY="$1"STATE_DIR="$HOME/.t3/ssh-launch/$STATE_KEY"DEFAULT_SERVER_HOME="$HOME/.t3"DEFAULT_RUNTIME_FILE="$DEFAULT_SERVER_HOME/userdata/server-runtime.json"PORT_FILE="$STATE_DIR/port"PID_FILE="$STATE_DIR/pid"MANAGED_FILE="$STATE_DIR/managed"LOG_FILE="$STATE_DIR/server.log"RUNNER_FILE="$STATE_DIR/run-t3.sh"RUNNER_NEXT="$STATE_DIR/run-t3.next.$$"mkdir -p "$STATE_DIR"cleanup_runner_next() {  rm -f "$RUNNER_NEXT"}trap cleanup_runner_next EXITcat >"$RUNNER_NEXT" <<'SH'@@T3_RUNNER_SCRIPT@@SHRUNNER_CHANGED=0if [ ! -f "$RUNNER_FILE" ] || ! cmp -s "$RUNNER_NEXT" "$RUNNER_FILE"; then  RUNNER_CHANGED=1fimv "$RUNNER_NEXT" "$RUNNER_FILE"chmod 700 "$RUNNER_FILE"if ! ensure_remote_node_path; then  printf 'Remote host is missing node on PATH. Install Node or configure a supported version manager for non-interactive shells.\\n' >&2  exit 1fipick_port() {  node - "$PORT_FILE" "@@T3_DEFAULT_REMOTE_PORT@@" "@@T3_REMOTE_PORT_SCAN_WINDOW@@" <<'NODE'@@T3_PICK_PORT_SCRIPT@@NODE}wait_ready() {  node - "$REMOTE_PORT" "$1" "@@T3_READY_PROBE_TIMEOUT_MS@@" <<'NODE'@@T3_WAIT_READY_SCRIPT@@NODE}

Lines 1–500 of 1695.