1586 lines · 51.9 KB · TypeScript
RawDownload
import type {  DesktopSshEnvironmentBootstrap,  DesktopSshEnvironmentTarget,} from "@t3tools/contracts";import { type NetError, NetService } from "@t3tools/shared/Net";import { fromLenientJson } from "@t3tools/shared/schemaJson";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,  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;} 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  | SshPasswordPrompt; type SshEnvironmentEffectError =  | SshCommandError  | SshInvalidTargetError  | SshLaunchError  | SshPairingError  | SshReadinessError  | SshPasswordPromptError  | 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 RemoteHttpError = Schema.Struct({  error: Schema.optional(Schema.String),}); const decodeRemoteLaunchResult = Schema.decodeEffect(fromLenientJson(RemoteLaunchResult));const decodeRemotePairingResult = Schema.decodeEffect(fromLenientJson(RemotePairingResult));const decodeRemoteHttpError = Schema.decodeEffect(Schema.fromJsonString(RemoteHttpError)); 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_RUNNER_SCRIPT = `#!/bin/shset -euT3_NODE_SCRIPT_PATH=@@T3_NODE_SCRIPT_PATH@@if [ -n "$T3_NODE_SCRIPT_PATH" ]; then  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 npx and npm are unavailable on PATH.\\n' >&2exit 1`; export const REMOTE_LAUNCH_SCRIPT = `set -euSTATE_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"pick_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}wait_for_pid_exit() {  PID_TO_WAIT="$1"  WAIT_COUNT=0  while kill -0 "$PID_TO_WAIT" 2>/dev/null && [ "$WAIT_COUNT" -lt 20 ]; do    WAIT_COUNT=$((WAIT_COUNT + 1))    sleep 0.1  done}resolve_default_runtime_port() {  node - "$DEFAULT_RUNTIME_FILE" <<'NODE'const fs = require("node:fs");const runtimePath = process.argv[2] ?? "";try {  const runtime = JSON.parse(fs.readFileSync(runtimePath, "utf8"));  const pid = Number(runtime.pid);  const port = Number(runtime.port);  if (!Number.isInteger(pid) || !Number.isInteger(port)) {    process.exit(1);  }  const origin = new URL(String(runtime.origin ?? ""));  if (origin.protocol !== "http:" || !["127.0.0.1", "localhost"].includes(origin.hostname)) {    process.exit(1);  }  process.kill(pid, 0);  process.stdout.write(String(port));} catch {  process.exit(1);}NODE}REMOTE_PID="$(cat "$PID_FILE" 2>/dev/null || true)"REMOTE_PORT="$(cat "$PORT_FILE" 2>/dev/null || true)"REMOTE_MANAGED="$(cat "$MANAGED_FILE" 2>/dev/null || true)"DEFAULT_REMOTE_PORT="$(resolve_default_runtime_port 2>/dev/null || true)"if [ -n "$DEFAULT_REMOTE_PORT" ]; then  REMOTE_PORT="$DEFAULT_REMOTE_PORT"  if wait_ready "@@T3_REUSE_READY_TIMEOUT_MS@@"; then    if [ "$REMOTE_MANAGED" = "managed" ] && [ -n "$REMOTE_PID" ] && kill -0 "$REMOTE_PID" 2>/dev/null; then      kill "$REMOTE_PID" 2>/dev/null || true      wait_for_pid_exit "$REMOTE_PID"    fi    printf '%s\\n' "$REMOTE_PORT" >"$PORT_FILE"    printf 'external\\n' >"$MANAGED_FILE"    REMOTE_PID=""    REMOTE_MANAGED="external"  else    REMOTE_PID="$(cat "$PID_FILE" 2>/dev/null || true)"    REMOTE_PORT="$(cat "$PORT_FILE" 2>/dev/null || true)"    REMOTE_MANAGED="$(cat "$MANAGED_FILE" 2>/dev/null || true)"  fifiif [ "$REMOTE_MANAGED" = "external" ]; then  if [ -z "$REMOTE_PORT" ] || ! wait_ready "@@T3_REUSE_READY_TIMEOUT_MS@@"; then    REMOTE_PID=""    REMOTE_PORT=""    REMOTE_MANAGED=""  fielif [ -n "$REMOTE_PID" ] && [ -n "$REMOTE_PORT" ] && kill -0 "$REMOTE_PID" 2>/dev/null; then  if [ "$RUNNER_CHANGED" -eq 1 ]; then    kill "$REMOTE_PID" 2>/dev/null || true    wait_for_pid_exit "$REMOTE_PID"    REMOTE_PID=""    REMOTE_PORT=""    REMOTE_MANAGED=""  elif ! wait_ready "@@T3_REUSE_READY_TIMEOUT_MS@@"; then    kill "$REMOTE_PID" 2>/dev/null || true    wait_for_pid_exit "$REMOTE_PID"    REMOTE_PID=""    REMOTE_PORT=""    REMOTE_MANAGED=""  fielse  REMOTE_PID=""  REMOTE_PORT=""  REMOTE_MANAGED=""fiif [ -z "$REMOTE_PID" ] || [ -z "$REMOTE_PORT" ]; then  REMOTE_PORT="$(pick_port)" || true  if [ -z "$REMOTE_PORT" ]; then    printf 'Failed to find an available port on the remote host. Ensure node is available on PATH.\\n' >&2    exit 1  fi  nohup env T3CODE_NO_BROWSER=1 "$RUNNER_FILE" serve --host 127.0.0.1 --port "$REMOTE_PORT" --base-dir "$DEFAULT_SERVER_HOME" >>"$LOG_FILE" 2>&1 < /dev/null &  REMOTE_PID="$!"  printf '%s\\n' "$REMOTE_PID" >"$PID_FILE"  printf '%s\\n' "$REMOTE_PORT" >"$PORT_FILE"  printf 'managed\\n' >"$MANAGED_FILE"  if ! wait_ready "@@T3_READY_TIMEOUT_MS@@"; then    printf 'Remote T3 server did not become ready on 127.0.0.1:%s.\\n' "$REMOTE_PORT" >&2    tail -n 80 "$LOG_FILE" >&2 2>/dev/null || true    kill "$REMOTE_PID" 2>/dev/null || true    wait_for_pid_exit "$REMOTE_PID"    rm -f "$PID_FILE" "$PORT_FILE" "$MANAGED_FILE"    exit 1  fifiprintf '{"remotePort":%s,"serverKind":"%s"}\\n' "$REMOTE_PORT" "\${REMOTE_MANAGED:-managed}"`; export const REMOTE_PAIRING_SCRIPT = `set -euSTATE_DIR="$HOME/.t3/ssh-launch/@@T3_STATE_KEY@@"DEFAULT_SERVER_HOME="$HOME/.t3"RUNNER_FILE="$STATE_DIR/run-t3.sh"mkdir -p "$STATE_DIR"cat >"$RUNNER_FILE" <<'SH'@@T3_RUNNER_SCRIPT@@SHchmod 700 "$RUNNER_FILE"PAIRING_BASE_DIR="$DEFAULT_SERVER_HOME""$RUNNER_FILE" auth pairing create --base-dir "$PAIRING_BASE_DIR" --json`; export const REMOTE_STOP_SCRIPT = `set -euSTATE_DIR="$HOME/.t3/ssh-launch/@@T3_STATE_KEY@@"PID_FILE="$STATE_DIR/pid"PORT_FILE="$STATE_DIR/port"MANAGED_FILE="$STATE_DIR/managed"REMOTE_MANAGED="$(cat "$MANAGED_FILE" 2>/dev/null || true)"REMOTE_PID="$(cat "$PID_FILE" 2>/dev/null || true)"if [ "$REMOTE_MANAGED" != "external" ] && [ -n "$REMOTE_PID" ] && kill -0 "$REMOTE_PID" 2>/dev/null; then  kill "$REMOTE_PID" 2>/dev/null || true  WAIT_COUNT=0  while kill -0 "$REMOTE_PID" 2>/dev/null && [ "$WAIT_COUNT" -lt 20 ]; do    WAIT_COUNT=$((WAIT_COUNT + 1))    sleep 0.1  donefirm -f "$PID_FILE" "$PORT_FILE" "$MANAGED_FILE"printf '{"stopped":true}\\n'`; const REMOTE_LOG_TAIL_SCRIPT = `set -euSTATE_DIR="$HOME/.t3/ssh-launch/@@T3_STATE_KEY@@"LOG_FILE="$STATE_DIR/server.log"if [ -f "$LOG_FILE" ]; then  tail -n 80 "$LOG_FILE" 2>/dev/null || truefi`; export function buildRemoteT3RunnerScript(input?: RemoteT3RunnerOptions): string {  const packageSpec = shellSingleQuote(input?.packageSpec?.trim() || "t3@latest");  const nodeScriptPath = input?.nodeScriptPath?.trim() || "";  return stripTrailingNewlines(    applyScriptPlaceholders(REMOTE_RUNNER_SCRIPT, {      T3_PACKAGE_SPEC: packageSpec,

Lines 1–500 of 1586.