runStudio
uses node:path
Runs tcm studio: the studio tcm ships (studio), copied into the codebase's .tcm/studio/ (again when tcm's
changes) and run there by tcm start on this codebase, its own branch, listening on 127.0.0.1 alone, until it's
stopped. It runs the codebase's code and shows how it runs, so it takes a token, kept in .tcm/studio/token (only
the user can read it): the link it prints carries it, and signs the browser in. One already running on the codebase
is said, its link opened, rather than another started.
import { openBrowser } from "#codebase/tcmts/atproto"; import { routeStudio } from "#codebase/tcmts/cli/commands"; import { execute } from "#codebase/tcmts/cli/main"; import { migrateCopy } from "#codebase/tcmts/cli/migrate"; import { SHIPPED_APP } from "#codebase/tcmts/cli/pack"; import type { Context } from "#codebase/tcmts/cli/run"; import { liveRunning, sayRunning } from "#codebase/tcmts/cli/running"; import { bold, dim, yellow } from "#codebase/tcmts/cli/style"; import { freePort, shippedApp, uiOutput } from "#codebase/tcmts/cli/sync"; import { UsageError } from "#codebase/tcmts/cli/ui"; import { parse } from "@bomb.sh/router"; import { spawn } from "node:child_process"; import { createHash, randomBytes } from "node:crypto"; import fs from "node:fs"; import http from "node:http"; import path_1 from "node:path"; export async function runStudio({ cb, cmd, rest }: Context): Promise<number> { const m = execute(cmd, parse(routeStudio(), { argv: rest })).model; const root = cb.lay.root; const dir = path_1.join(cb.lay.tcmDir, "studio"); fs.mkdirSync(path_1.join(dir, ".tcm"), { recursive: true }); // (what's here is the studio's and this machine's: its token above all, never committed) fs.writeFileSync(path_1.join(dir, ".gitignore"), "*\n"); const tokenFile = path_1.join(dir, "token"); let token = ""; try { const st = fs.lstatSync(tokenFile); if (st.isFile() && (st.mode & 0o077) === 0) token = fs.readFileSync(tokenFile, "utf8").trim(); } catch {} if (!/^[0-9a-f]{48}$/.test(token)) { token = randomBytes(24).toString("hex"); fs.rmSync(tokenFile, { force: true }); fs.writeFileSync(tokenFile, `${token}\n`, { mode: 0o600 }); } const say = (url: string) => { const link = `${url.replace(/\/?$/, "/")}#token=${token}`; console.log(` open ${link} ${dim("(signs this browser in)")}`); if (!m["no-open"]) openBrowser(link); }; const already = liveRunning(root).find((r) => r.command === "studio" && r.url); if (already) { console.log( `${bold("tcm studio")} is already running on ${root} ${dim(`(process ${already.pid})`)}`, ); say(already.url!); return 0; } const shipped = shippedApp("studio"); if (!shipped) throw new UsageError("this tcm doesn't ship the studio: a tcm installed from its package does"); const from = path_1.join(shipped, "codebase.sqlite"); const to = path_1.join(dir, ".tcm", "codebase.sqlite"); const digest = createHash("sha256").update(fs.readFileSync(from)).digest("hex"); const mark = path_1.join(dir, ".tcm", "shipped"); if (!fs.existsSync(to) || !fs.existsSync(mark) || fs.readFileSync(mark, "utf8") !== digest) { for (const f of [to, `${to}-wal`, `${to}-shm`]) fs.rmSync(f, { force: true }); fs.copyFileSync(from, to); fs.writeFileSync(mark, digest); } // (an older tcm may have packed it, or copied it here) migrateCopy(to); const free = (p: number) => new Promise<boolean>((resolve) => { const s = http.createServer(); s.once("error", () => resolve(false)); s.listen(p, "127.0.0.1", () => s.close(() => resolve(true))); }); const port = m.port && (await free(m.port)) ? m.port : await freePort(); if (m.port && port !== m.port) console.log(dim(`port ${m.port} is taken: ${port} instead`)); const env: NodeJS.ProcessEnv = { ...process.env, TCM_SHARE_CODEBASE: root, TCM_SHARE_TOKEN: token, TCM_SHARE_PUBLIC: "0", // (it stops when this does, however this ends) TCM_STOP_WITH_PARENT: "1", }; // (the codebase's own branch, whichever this was run on) delete env.TCM_CODEBASE; delete env.TCM_BRANCH; console.log(`${bold("tcm studio")} ${root}`); console.log( dim(" starting (its first start, and the first after tcm changes, builds it: a few seconds)"), ); const child = spawn( process.execPath, [process.argv[1]!, "start", SHIPPED_APP, "--port", String(port), "--host", "127.0.0.1"], { cwd: dir, env, stdio: ["ignore", "pipe", "pipe"] }, ); let started = false; const shown = uiOutput(); const out = (d: Buffer) => { const text = d.toString("utf8"); if (!started && /url\s+http:\/\/\S+/.test(text)) { started = true; const url = `http://127.0.0.1:${port}/`; sayRunning(root, { command: "studio", url, branch: cb.store.branch().name }); say(url); return; } if (started) for (const line of shown(text)) console.log(dim(` studio ${line}`)); }; child.stdout!.on("data", out); child.stderr!.on("data", (d: Buffer) => { for (const line of shown(d.toString("utf8"))) console.log(yellow(` studio ${line}`)); }); const stop = () => child.kill(); process.once("SIGINT", stop); process.once("SIGTERM", stop); return new Promise((resolve) => child.on("exit", (code) => resolve(code ?? 0))); }
uses
- tcmts.atproto.openBrowser
- tcmts.cli.commands.routeStudio
- tcmts.cli.main.execute
- tcmts.cli.migrate.migrateCopy
- tcmts.cli.pack.SHIPPED_APP
- tcmts.cli.run.Context
- tcmts.cli.running.liveRunning
- tcmts.cli.running.sayRunning
- tcmts.cli.style.bold
- tcmts.cli.style.dim
- tcmts.cli.style.yellow
- tcmts.cli.sync.freePort
- tcmts.cli.sync.shippedApp
- tcmts.cli.sync.uiOutput
- tcmts.cli.ui.UsageError