Files
agent-radar 2ed3415fb7
Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
源码快照 @ 2026-09-01 (source-only, 剔除非源码素材)
2026-09-01 20:17:27 +08:00

327 lines
8.1 KiB
JavaScript

import { spawn, spawnSync } from "node:child_process";
import { randomBytes } from "node:crypto";
import { readFile, rm } from "node:fs/promises";
import { homedir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const healthPollIntervalMs = 250;
const healthTimeoutMs = 15_000;
const shutdownTimeoutMs = 5_000;
const scriptDir = dirname(fileURLToPath(import.meta.url));
const workspaceRoot = join(scriptDir, "..", "..");
const tuttidDir = join(workspaceRoot, "services", "tuttid");
const defaultsPath = join(workspaceRoot, "config", "tutti.defaults.json");
const generatedDefaults = JSON.parse(await readFile(defaultsPath, "utf8"));
const installDevCliScriptPath = join(
workspaceRoot,
"tools",
"scripts",
"install-dev-cli.mjs"
);
const stateDirOverride = process.env.TUTTI_STATE_DIR?.trim();
const stateDir = stateDirOverride || resolveDevelopmentStateRoot();
const listenerInfoPath = join(
stateDir,
generatedDefaults.state.runDirName,
generatedDefaults.state.listenerInfoFileName
);
const accessToken = randomBytes(32).toString("base64url");
let shutdownStarted = false;
let exitCode = 0;
installDevCli();
generateBuiltinApps();
const daemon = spawn(resolveCommand("go"), ["run", "."], {
cwd: tuttidDir,
env: {
...process.env,
TUTTID_ACCESS_TOKEN: accessToken,
TUTTID_ADDR: "127.0.0.1:0",
TUTTID_LISTENER_INFO_PATH: listenerInfoPath,
TUTTID_LOG_OUTPUT: process.env.TUTTID_LOG_OUTPUT?.trim() || "tee",
TUTTI_ANALYTICS_DEBUG:
process.env.TUTTI_ANALYTICS_DEBUG?.trim() ||
process.env.VITE_TUTTI_ANALYTICS_DEBUG?.trim() ||
"",
TUTTI_ENV: "development",
TUTTI_STATE_DIR: stateDir
},
stdio: ["ignore", "pipe", "pipe"]
});
daemon.stdout?.on("data", (chunk) => {
process.stdout.write(`[tuttid] ${chunk.toString()}`);
});
daemon.stderr?.on("data", (chunk) => {
process.stderr.write(`[tuttid] ${chunk.toString()}`);
});
let vite = null;
try {
const baseUrl = await waitForHealthyBaseUrl(
listenerInfoPath,
accessToken,
() => isAlive(daemon)
);
const startupWorkspaceID = await resolveStartupWorkspaceID(
baseUrl,
accessToken
);
console.log(`[dev-web] tuttid backend ready at ${baseUrl}`);
console.log(`[dev-web] State root: ${stateDir}`);
console.log(`[dev-web] Do not open that backend URL in the browser.`);
console.log(`[dev-web] Open the Vite Local URL printed below instead.`);
console.log(`[dev-web] CLI endpoint ready: tutti-dev status`);
if (startupWorkspaceID) {
console.log(
`[dev-web] Startup workspace is injected automatically: ${startupWorkspaceID}`
);
}
vite = spawn(
resolveCommand("pnpm"),
["--filter", "@tutti-os/desktop", "dev:web"],
{
cwd: workspaceRoot,
env: {
...process.env,
VITE_TUTTID_ACCESS_TOKEN: accessToken,
VITE_TUTTID_BASE_URL: baseUrl,
VITE_TUTTI_WEB_DEV: "1",
...(startupWorkspaceID
? { VITE_TUTTI_WEB_WORKSPACE_ID: startupWorkspaceID }
: {})
},
stdio: "inherit"
}
);
const handleSignal = async (signal) => {
if (shutdownStarted) {
return;
}
exitCode = signal === "SIGINT" ? 130 : 143;
await shutdown(vite, daemon);
process.exit(exitCode);
};
process.on("SIGINT", handleSignal);
process.on("SIGTERM", handleSignal);
const result = await Promise.race([
onceExit(daemon).then(({ code, signal }) => ({
code: code ?? (signal ? 1 : 0),
signal,
source: "daemon"
})),
onceExit(vite).then(({ code, signal }) => ({
code: code ?? (signal ? 1 : 0),
signal,
source: "vite"
}))
]);
if (result.source === "daemon" && isAlive(vite) && result.code !== 0) {
console.error("[dev-web] tuttid exited before Vite stopped.");
exitCode = result.code || 1;
} else {
exitCode = result.code;
}
} catch (error) {
exitCode = 1;
const message = error instanceof Error ? error.message : String(error);
console.error(`[dev-web] failed to start: ${message}`);
} finally {
await shutdown(vite, daemon);
await clearListenerInfoIfOwned(listenerInfoPath, accessToken);
}
process.exit(exitCode);
function resolveDevelopmentStateRoot() {
return join(homedir(), generatedDefaults.state.developmentDirName);
}
function installDevCli() {
const result = spawnSync(process.execPath, [installDevCliScriptPath], {
cwd: workspaceRoot,
env: {
...process.env,
TUTTI_ENV: "development"
},
stdio: "inherit"
});
if (result.status !== 0) {
throw new Error(
`install tutti-dev failed with exit code ${result.status ?? "unknown"}`
);
}
}
function generateBuiltinApps() {
const result = spawnSync(resolveCommand("pnpm"), ["generate:builtin-apps"], {
cwd: workspaceRoot,
env: {
...process.env,
TUTTI_ENV: "development"
},
stdio: "inherit"
});
if (result.status !== 0) {
throw new Error(
`generate builtin apps failed with exit code ${result.status ?? "unknown"}`
);
}
}
async function waitForHealthyBaseUrl(
listenerInfoPathToCheck,
token,
isProcessAlive
) {
const deadline = Date.now() + healthTimeoutMs;
let lastError;
while (Date.now() < deadline) {
if (!isProcessAlive()) {
throw new Error(
`tuttid exited before health check succeeded: ${String(lastError ?? "unknown error")}`
);
}
try {
const baseUrl = await readListenerInfoBaseUrl(listenerInfoPathToCheck);
const health = await requestJSON(new URL("/v1/health", baseUrl), token);
if (health?.status === "ok") {
return baseUrl;
}
lastError = new Error(
`unexpected health response: ${JSON.stringify(health)}`
);
} catch (error) {
lastError = error;
}
await sleep(healthPollIntervalMs);
}
throw new Error(
`timed out waiting for tuttid health: ${String(lastError ?? "unknown error")}`
);
}
async function resolveStartupWorkspaceID(baseUrl, token) {
try {
const response = await requestJSON(
new URL("/v1/workspaces/startup", baseUrl),
token
);
return response?.workspace?.id ?? null;
} catch {
return null;
}
}
async function requestJSON(url, token) {
const response = await fetch(url, {
headers: {
accept: "application/json",
authorization: `Bearer ${token}`
},
signal: AbortSignal.timeout(5_000)
});
if (!response.ok) {
throw new Error(
`request failed with status ${response.status} ${response.statusText}`.trim()
);
}
return response.json();
}
async function readListenerInfoBaseUrl(listenerInfoPathToRead) {
const content = await readFile(listenerInfoPathToRead, "utf8");
const parsed = JSON.parse(content);
if (!parsed?.addr || typeof parsed.addr !== "string") {
throw new Error(`invalid listener info: ${content}`);
}
return `http://${parsed.addr}`;
}
async function clearListenerInfoIfOwned(listenerInfoPathToClear, token) {
try {
const content = await readFile(listenerInfoPathToClear, "utf8");
const parsed = JSON.parse(content);
if (parsed?.auth?.token !== token) {
return;
}
} catch {
return;
}
await rm(listenerInfoPathToClear, { force: true });
await rm(`${listenerInfoPathToClear}.tmp`, { force: true });
}
function resolveCommand(command) {
if (process.platform === "win32") {
return `${command}.cmd`;
}
return command;
}
function isAlive(child) {
if (!child) {
return false;
}
return child.exitCode === null && child.signalCode === null;
}
function onceExit(child) {
return new Promise((resolve) => {
child.once("exit", (code, signal) => {
resolve({ code, signal });
});
});
}
async function shutdown(viteChild, daemonChild) {
if (shutdownStarted) {
return;
}
shutdownStarted = true;
await stopChild(viteChild, "SIGINT");
await stopChild(daemonChild, "SIGTERM");
}
async function stopChild(child, signal) {
if (!child || !isAlive(child)) {
return;
}
child.kill(signal);
await Promise.race([
onceExit(child),
sleep(shutdownTimeoutMs).then(() => {
if (isAlive(child)) {
child.kill("SIGKILL");
}
})
]);
}
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(resolve, ms);
});
}