dispatch

tcmts.cli.main.dispatch · #fguasc2v · term · runs on the server

uses node:path

import { buildsDir, runOnBuild } from "#codebase/tcmts/cli/builds";
import { Codebase, sessionDirFor } from "#codebase/tcmts/cli/codebase";
import { routeMcp, routes, routeTcm } from "#codebase/tcmts/cli/commands";
import { helpText, readsBranches } from "#codebase/tcmts/cli/help";
import {
  checkInto, execute, followMirror, importProject, Printed, unknownCommand, watch,
} from "#codebase/tcmts/cli/main";
import {
  endSession, isSessionBranch, liveMcpServers, registerMcpServer, serveMcp, sessionClient, startSession, superviseMcp,
  sweepSessions,
} from "#codebase/tcmts/cli/mcp";
import { inPendingMerge } from "#codebase/tcmts/cli/merge";
import { handlers } from "#codebase/tcmts/cli/run";
import type { Started } from "#codebase/tcmts/cli/run";
import { showInstallProgress, UsageError } from "#codebase/tcmts/cli/ui";
import { vitestChild } from "#codebase/tcmts/cli/vitest";
import { TS_VERSION } from "#codebase/tcmts/core/program";
import { cleanMessage } from "#codebase/tcmts/store";
import { parse } from "@bomb.sh/router";
import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path_1 from "node:path";

export async function dispatch(argv: string[]): Promise<number> {
  // -C <dir>: as if tcm were started there, as git's -C: the codebase there, not one TCM_CODEBASE or a session's
  // TCM_BRANCH names (the caller's): to run a tcm (a session's own, say) on a codebase made to try something in.
  // A tcm of its own, started there: this one read TCM_CODEBASE as it started
  if (argv[0] === "-C") {
    const dir = argv[1] && path_1.resolve(argv[1]);
    if (!dir || !fs.statSync(dir, { throwIfNoEntry: false })?.isDirectory())
      throw new UsageError(
        `-C <dir>: ${argv[1] ? `${argv[1]} isn't a directory` : "give the directory to run in"}`,
      );
    const { TCM_CODEBASE: _, TCM_BRANCH: __, ...env } = process.env;
    const r = spawnSync(
      process.execPath,
      [...process.execArgv, process.argv[1]!, ...argv.slice(2)],
      { cwd: dir, env, stdio: "inherit" },
    );
    return r.status ?? 1;
  }
  // the builds kept (tcm's repository's dist/), for what this tcm starts too: a `tcm run` of the codebase's own tcm
  const kept = buildsDir();
  if (kept) process.env.TCM_BUILDS = kept;
  // --build <build>: the command run by another tcm, a kept build (`tcm builds`) or a tcm.js
  if (argv[0] === "--build" || argv[0]?.startsWith("--build=")) {
    const spec = argv[0] === "--build" ? argv[1] : argv[0].slice("--build=".length);
    if (!spec)
      throw new UsageError(
        "--build <build>: known-good, previous, an id `tcm builds` lists, or a tcm.js",
      );
    return runOnBuild(spec, argv.slice(argv[0] === "--build" ? 2 : 1));
  }
  // a hash as tcm shows it (`#abc`) with another `#` before it (`##abc`) is that hash (not after `--`: a program's)
  const literals = argv.indexOf("--");
  argv = argv.map((a, i) =>
    (literals < 0 || i < literals) && /^##+[a-z2-7]+$/.test(a) ? a.replace(/^#+/, "#") : a,
  );
  const cwd = process.cwd();
  // a session's scratch files are its own, in its directory (`tcm mcp --branch`, TCM_BRANCH): a file named by
  // itself is there, and one with a path is where the path says
  // a file given is where the caller says; the default one is the codebase's (or its session's), wherever tcm runs
  const scratch = (f?: string, fallback = "scratch.ts") => {
    if (process.env.TCM_BRANCH && !/[\\/]/.test(f ?? ""))
      return path_1.resolve(
        sessionDirFor(Codebase.findRoot(cwd), process.env.TCM_BRANCH),
        f ?? fallback,
      );
    if (f !== undefined) return path_1.resolve(cwd, f);
    try {
      return path_1.join(Codebase.findRoot(cwd), fallback);
    } catch {
      return path_1.resolve(cwd, fallback);
    }
  };
  // the test runner's child process (see vitest.ts), not a command anyone types
  if (argv[0] === "internal-vitest") return (await vitestChild(argv[1]!), 0);

  const word = argv[0] && !argv[0].startsWith("-") ? argv[0] : undefined;
  if (word === "help") {
    const path = argv.slice(1).filter((a) => !a.startsWith("-"));
    if (path[0] && !Object.hasOwn(routes(), path[0])) throw new UsageError(unknownCommand(path[0]));
    throw new Printed(helpText(path));
  }
  if (word && !Object.hasOwn(routes(), word)) throw new UsageError(unknownCommand(word));
  const cmd = word as keyof ReturnType<typeof routes> | undefined;
  const rest = argv.slice(cmd ? 1 : 0);
  showInstallProgress();

  if (!cmd) {
    const { model } = execute(cmd, parse(routeTcm(), { argv: rest }));
    watch(Codebase.find(cwd), checkInto(model.into), model.fresh);
    return -1; // keep running
  }
  // the editor says where the workspace is (initialize), so the codebase is found then
  // agents connect before there may be a codebase; each tool runs tcm itself, so this stays out of the way
  if (cmd === "mcp") {
    const { model } = execute(cmd, parse(routeMcp(), { argv: rest }));
    // a session: every command it runs works on its own branch, made from the codebase's own if it's new (with
    // --session, a new one for each server: every agent sharing one .mcp.json gets its own)
    const root = Codebase.findRoot(cwd);
    // (its tools run tcm in the codebase's directory: paths they're given are the codebase's)
    if (process.env.TCM_MCP_SERVER) {
      await serveMcp(
        root,
        [process.execPath, ...process.execArgv, process.argv[1]!],
        `TypeScript ${TS_VERSION}`,
        process.env.TCM_BRANCH || undefined,
        // (which tcm, to say when a call has an option it doesn't: this code's own hash, as `tcm status` says)
        (dispatch as unknown as Record<symbol, string | undefined>)[Symbol.for("tcm.hash")],
      );
      return 0;
    }
    // an agent works on a branch of its own, as work on a git branch is, until the user lands it: the one --branch
    // names, or the one its last session (from the same directory) left holding work, or a new one; --direct, on the
    // codebase's own. Claimed, and said in its record, in one write transaction: no server starting meanwhile takes it
    const client = sessionClient(cwd);
    let resumed = false;
    if (model.direct) registerMcpServer(root, process.env.TCM_BRANCH || undefined, client);
    else {
      const claimed = await startSession(root, client, model.branch);
      resumed = claimed.resumed;
      registerMcpServer(root, claimed.branch, client);
      process.env.TCM_BRANCH = claimed.branch;
      // what sessions whose server was killed left (it's tidying: a server starts all the same)
      try {
        sweepSessions(root);
      } catch {}
    }
    // the session's branch as its record says now (the server can move onto another: tcm_cli ["switch", <b>])
    const branchNow = () =>
      model.direct
        ? undefined
        : (liveMcpServers(root).find((s) => s.pid === process.pid)?.branch ??
          process.env.TCM_BRANCH);
    // the server proper, under a supervisor that starts it again (on the same branch) when this tcm is replaced or it
    // crashes
    const env = () => {
      const branch = branchNow();
      return {
        ...process.env,
        ...(branch ? { TCM_BRANCH: branch } : {}),
        TCM_MCP_SERVER: "1",
        TCM_MCP_PID: String(process.pid),
        ...(resumed ? { TCM_MCP_RESUMED: "1" } : {}),
        ...(model.direct ? { TCM_MCP_DIRECT: "1" } : {}),
      };
    };
    const code = await superviseMcp(
      [process.execPath, ...process.execArgv, process.argv[1]!, "mcp"],
      env,
      root,
      process.argv[1]!,
    );
    // a branch a server made for itself that holds nothing main doesn't goes with it
    const last = branchNow();
    if (last && isSessionBranch(last)) endSession(root, last, process.pid);
    return code;
  }
  // what every command is given; those that don't open the codebase (`tcm init`, `tcm migrate`, …) run now, finding it
  // themselves if they need one
  const started: Started = {
    cmd,
    rest,
    cwd,
    scratch,
    kept,
    // (which tcm: this code's own hash, which covers every command)
    tcmHash: (dispatch as unknown as Record<symbol, string | undefined>)[Symbol.for("tcm.hash")],
  };
  const handler = handlers()[cmd];
  if ("alone" in handler) return handler.alone(started);
  // help and usage errors don't need a codebase: check the line before looking for one (parsed again, typed,
  // by its handler)
  const line = execute(
    cmd,
    (parse as (definition: unknown, input: { argv: string[] }) => { ok: boolean })(
      routes()[cmd](),
      {
        argv: rest,
      },
    ),
  ) as { model?: { branch?: string; message?: string } };
  const cb = Codebase.find(cwd);
  // --branch, of a command that only reads: that branch, read as it is, and nothing changed (this process's own, a
  // session's, is untouched, its `#codebase` files too)
  const reading = readsBranches([cmd]) ? line.model?.branch : undefined;
  if (reading) cb.store.readOnly(reading);
  cb.adoptMirror = (m) =>
    importProject(cb, m.dir, "update", m.into, false, m.exclude, m.tests, false);
  // a mirror's files changed outside tcm come in first (importing them is the command's job, and checking them
  // against the codebase `mirror --check`'s)
  if (cmd !== "import" && cmd !== "mirror" && !reading) followMirror(cb);
  // -m: what the entry the command makes is for (each command says it afresh: a worker runs many on one store)
  cb.store.message = cleanMessage(line.model?.message);
  const run = () => handler.run({ ...started, cb });
  // a merge into this branch not finished: what reads or changes code works on it as merged, settling it
  return !reading &&
    [
      "add",
      "update",
      "edit",
      "patch",
      "resolve",
      "rm",
      "mv",
      "alias",
      "todo",
      "status",
      "view",
      "ls",
      "find",
      "search",
      "check",
      "diff",
      "deps",
      "dependents",
      "docs",
      "at",
      "test",
      "history",
      "lint",
      "fmt",
      "run",
    ].includes(cmd)
    ? inPendingMerge(cb, run)
    : run();
}

uses

used by