← Files RivetARCHIVED FILE
node_modules/rivet-design/dist/services/DevServerRuntimeService.js
21.8 KB · Nov 7, 2026 · 13:16 UTC
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.createDevServerRuntimeService = exports.DevServerRuntimeService = exports.parsePortFromDevServerOutput = void 0;
const child_process_1 = require("child_process");
const fs_1 = __importDefault(require("fs"));
const net_1 = __importDefault(require("net"));
const path_1 = __importDefault(require("path"));
const util_1 = require("util");
const logger_1 = require("../utils/logger");
const devServerCommand_1 = require("../utils/devServerCommand");
const OperatingSystemServices_1 = require("./OperatingSystemServices");
const errorMessage_1 = require("../utils/errorMessage");
const log = (0, logger_1.createLogger)('DevServerRuntimeService');
const execAsync = (0, util_1.promisify)(child_process_1.exec);
const COMMON_DEV_SERVER_PORTS = [
3000, 3001, 3002, 5173, 5174, 5175, 8080, 4200, 6006, 6007,
];
const DEFAULT_START_TIMEOUT_MS = 30_000;
const DEFAULT_PORT_CHECK_INTERVAL_MS = 500;
const DEFAULT_STABILITY_ATTEMPTS = 4;
const DEFAULT_STABILITY_INTERVAL_MS = 500;
const STARTUP_ERROR_OUTPUT_TAIL_CHARS = 300;
const MAX_EXCLUSIVE_VALID_TCP_PORT = 65_536;
const MIN_VALID_TCP_PORT = 1;
const SAFE_SCRIPT_NAME_PATTERN = '[A-Za-z0-9:_-]+';
const SAFE_START_COMMAND_PATTERN = new RegExp(`^(?:npm run ${SAFE_SCRIPT_NAME_PATTERN}(?: -- --port \\d+)?|yarn ${SAFE_SCRIPT_NAME_PATTERN}(?: --port \\d+)?|pnpm (?:run )?${SAFE_SCRIPT_NAME_PATTERN}(?: --port \\d+)?)$`);
const SAFE_INSTALL_COMMAND_PATTERN = /^(?:npm install|yarn install|pnpm install)$/;
const PORT_PLACEHOLDER_PATTERN = /\$\{?RIVET_PORT\}?|__RIVET_PORT__|\{\{RIVET_PORT\}\}/;
const PORT_PLACEHOLDER_REPLACE_PATTERN = /\$\{?RIVET_PORT\}?|__RIVET_PORT__|\{\{RIVET_PORT\}\}/g;
const DEV_SERVER_ENV_ALLOWLIST = [
'APPDATA',
'CI',
'HOME',
'LANG',
'LC_ALL',
'LOCALAPPDATA',
'LOGNAME',
'PATH',
'SHELL',
'SystemRoot',
'TEMP',
'TMP',
'TMPDIR',
'USER',
'USERPROFILE',
];
const parsePortFromDevServerOutput = (text) => {
const matches = [];
const patterns = [
/Local:\s*https?:\/\/[^\s]*:(\d+)/gi,
/(?:localhost|127\.0\.0\.1):(\d+)/gi,
/(?:on port|listening on port)\s+(\d+)/gi,
];
for (const pattern of patterns) {
for (const match of text.matchAll(pattern)) {
const port = Number(match[1]);
if (Number.isInteger(port) &&
port >= MIN_VALID_TCP_PORT &&
port < MAX_EXCLUSIVE_VALID_TCP_PORT) {
matches.push({ index: match.index ?? 0, port });
}
}
}
matches.sort((a, b) => a.index - b.index);
return matches[matches.length - 1]?.port ?? null;
};
exports.parsePortFromDevServerOutput = parsePortFromDevServerOutput;
const inferMissingToolsFromError = (text) => {
const lower = text.toLowerCase();
const missingTools = [];
if (lower.includes('yarn: command not found') ||
lower.includes('missing yarn')) {
missingTools.push('yarn');
}
if (lower.includes('node: command not found') ||
lower.includes('missing node')) {
missingTools.push('node');
}
if (lower.includes('git: command not found') ||
lower.includes('missing git')) {
missingTools.push('git');
}
return missingTools;
};
const buildStartFailureGuidance = (options) => {
const installCommand = options.installCommand ?? 'npm install';
return [
options.summary ??
`Start command \`${options.command}\` (run in ${options.projectPath}) did not produce a running dev server within ${DEFAULT_START_TIMEOUT_MS / 1000}s.`,
options.outputTail
? `Last output: ${options.outputTail}`
: 'No output was captured.',
`Rivet never installs dependencies or modifies the project. Diagnose the output yourself: if dependencies are missing, run \`${installCommand}\` in the project and retry; otherwise start the dev server manually and re-run \`rivet open\` with \`--user-port <port>\` to attach to it.`,
].join(' ');
};
const defaultIsPortResponding = (port) => new Promise((resolve) => {
const socket = net_1.default.connect(port, 'localhost');
socket.on('connect', () => {
socket.destroy();
resolve(true);
});
socket.on('error', () => {
socket.destroy();
resolve(false);
});
socket.setTimeout(DEFAULT_PORT_CHECK_INTERVAL_MS);
socket.on('timeout', () => {
socket.destroy();
resolve(false);
});
});
const defaultFindAvailablePort = async (startPort) => {
for (let candidate = startPort; candidate < MAX_EXCLUSIVE_VALID_TCP_PORT; candidate++) {
if (!(await defaultIsPortResponding(candidate))) {
return candidate;
}
}
throw new Error(`No available port found from ${startPort}`);
};
const discoverStartupCommandFromProjectScripts = async (options) => {
const packageJsonPath = path_1.default.join(options.projectPath, 'package.json');
try {
const parsed = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf8'));
const scriptName = devServerCommand_1.DEV_SCRIPT_CANDIDATES.find((candidate) => typeof parsed.scripts?.[candidate] === 'string');
if (!scriptName) {
return null;
}
const startCommand = options.packageManager === 'npm'
? `npm run ${scriptName}`
: `${options.packageManager} ${scriptName}`;
return {
startCommand,
installCommand: buildInstallCommand(options.packageManager),
};
}
catch {
return null;
}
};
const normalizeProjectPath = (projectPath) => {
const resolved = path_1.default.resolve(projectPath);
try {
return fs_1.default.realpathSync(resolved);
}
catch {
return resolved;
}
};
const isSafeCachedStartupCommand = (entry) => {
if (entry.lastFailureAt) {
return false;
}
if (!SAFE_START_COMMAND_PATTERN.test(entry.startCommand.trim())) {
return false;
}
if (!entry.installCommand) {
return true;
}
return SAFE_INSTALL_COMMAND_PATTERN.test(entry.installCommand.trim());
};
const buildDevServerProcessEnv = (extraEnv = {}) => {
const env = {};
for (const key of DEV_SERVER_ENV_ALLOWLIST) {
const value = process.env[key];
if (value !== undefined) {
env[key] = value;
}
}
return { ...env, ...extraEnv };
};
class ProjectStartupCommandCache {
async read(projectPath) {
const cachePath = this.resolveCachePath(projectPath);
try {
if (!fs_1.default.existsSync(cachePath)) {
return null;
}
const parsed = JSON.parse(fs_1.default.readFileSync(cachePath, 'utf8'));
if (typeof parsed.startCommand !== 'string') {
return null;
}
const entry = {
startCommand: parsed.startCommand,
installCommand: typeof parsed.installCommand === 'string'
? parsed.installCommand
: undefined,
lastFailureAt: typeof parsed.lastFailureAt === 'string'
? parsed.lastFailureAt
: undefined,
};
if (!isSafeCachedStartupCommand(entry)) {
return null;
}
return entry;
}
catch (error) {
log.debug(`Failed to read startup command cache: ${(0, errorMessage_1.errorMessage)(error)}`);
return null;
}
}
async write(projectPath, entry) {
const cachePath = this.resolveCachePath(projectPath);
try {
fs_1.default.mkdirSync(path_1.default.dirname(cachePath), { recursive: true, mode: 0o700 });
fs_1.default.writeFileSync(cachePath, JSON.stringify({
...entry,
lastUsedAt: new Date().toISOString(),
}, null, 2), { mode: 0o600 });
}
catch (error) {
log.debug(`Failed to write startup command cache: ${(0, errorMessage_1.errorMessage)(error)}`);
}
}
async markFailed(projectPath, reason) {
const cachePath = this.resolveCachePath(projectPath);
try {
const existing = (await this.read(projectPath)) ?? { startCommand: '' };
fs_1.default.mkdirSync(path_1.default.dirname(cachePath), { recursive: true, mode: 0o700 });
fs_1.default.writeFileSync(cachePath, JSON.stringify({
...existing,
lastFailureAt: new Date().toISOString(),
lastFailureReason: reason,
}, null, 2), { mode: 0o600 });
}
catch {
}
}
resolveCachePath(projectPath) {
return path_1.default.join(projectPath, '.rivet', 'agent-dev-server-cache.json');
}
}
const defaultRuntimeDeps = () => ({
exec: execAsync,
isPortResponding: defaultIsPortResponding,
findAvailablePort: defaultFindAvailablePort,
sleep: (ms) => new Promise((resolve) => setTimeout(resolve, ms)),
operatingSystemServices: OperatingSystemServices_1.selectedOperatingSystemServices,
cache: new ProjectStartupCommandCache(),
discoverStartupCommand: discoverStartupCommandFromProjectScripts,
});
const buildInstallCommand = (packageManager) => {
if (packageManager === 'yarn') {
return 'yarn install';
}
if (packageManager === 'pnpm') {
return 'pnpm install';
}
return 'npm install';
};
const bindPortToStartCommand = (options) => {
const { framework, selectedPort } = options;
const trimmedCommand = options.startCommand.trim();
if (!trimmedCommand || framework === 'static') {
return { command: trimmedCommand, mode: 'none' };
}
if (PORT_PLACEHOLDER_PATTERN.test(trimmedCommand)) {
return {
command: trimmedCommand.replace(PORT_PLACEHOLDER_REPLACE_PATTERN, String(selectedPort)),
env: { PORT: String(selectedPort) },
mode: 'placeholder',
};
}
const withoutExplicitPort = trimmedCommand
.replace(/\s--\s--port\s+\d+\b/g, '')
.replace(/\s--\s-p\s+\d+\b/g, '')
.replace(/\s--port\s+\d+\b/g, '')
.replace(/\s--port=\d+\b/g, '')
.replace(/\s-p\s+\d+\b/g, '')
.replace(/\s-p=\d+\b/g, '')
.replace(/\s-p\d+\b/g, '')
.trim();
if (framework === 'cra') {
return {
command: withoutExplicitPort.replace(/^PORT=\d+\s+/, ''),
env: { PORT: String(selectedPort) },
mode: 'cra_env',
};
}
if (/^npm\s+run\s+\S+/.test(withoutExplicitPort)) {
return {
command: `${withoutExplicitPort} -- --port ${selectedPort}`,
env: { PORT: String(selectedPort) },
mode: 'npm_flag',
};
}
if (/^yarn\s+\S+/.test(withoutExplicitPort)) {
return {
command: `${withoutExplicitPort} --port ${selectedPort}`,
env: { PORT: String(selectedPort) },
mode: 'yarn_flag',
};
}
if (/^pnpm\s+(run\s+)?\S+/.test(withoutExplicitPort)) {
return {
command: `${withoutExplicitPort} --port ${selectedPort}`,
env: { PORT: String(selectedPort) },
mode: 'pnpm_flag',
};
}
return {
command: `${withoutExplicitPort} --port ${selectedPort}`,
env: { PORT: String(selectedPort) },
mode: 'fallback_flag',
};
};
class DevServerRuntimeService {
deps;
process = null;
port = null;
projectPath = null;
constructor(deps = defaultRuntimeDeps()) {
this.deps = deps;
}
async resolveStartupCommand(options) {
const cached = await this.deps.cache.read(options.projectPath);
if (cached?.startCommand.trim() && isSafeCachedStartupCommand(cached)) {
return { ...cached, source: 'cache' };
}
const discovered = await this.deps.discoverStartupCommand?.({
projectPath: options.projectPath,
framework: options.framework,
packageManager: options.packageManager,
});
if (discovered?.startCommand.trim()) {
return { ...discovered, source: 'discovered' };
}
const devConfig = devServerCommand_1.FRAMEWORK_DEV_CONFIG[options.framework] ?? {
devCommand: 'dev',
defaultPort: 3000,
};
const command = (0, devServerCommand_1.buildDevServerCommand)(options.framework, options.packageManager, devConfig.devCommand, options.preferredPort);
return {
startCommand: [command.cmd, ...command.args].join(' '),
installCommand: buildInstallCommand(options.packageManager),
source: 'deterministic',
};
}
async start(options) {
await this.stop();
const selectedPort = await this.deps.findAvailablePort(options.preferredPort);
const commandResolution = options.startCommand
? {
startCommand: options.startCommand,
installCommand: options.installCommand,
source: 'deterministic',
}
: await this.resolveStartupCommand({
projectPath: options.projectPath,
framework: options.framework,
packageManager: options.packageManager,
preferredPort: selectedPort,
});
const boundCommand = bindPortToStartCommand({
startCommand: commandResolution.startCommand,
framework: options.framework,
selectedPort,
});
const preExistingPorts = await this.collectPreExistingPorts(selectedPort);
let output = '';
let portFromOutput = null;
this.projectPath = options.projectPath;
this.process = this.deps.operatingSystemServices.projectProcesses.start({ shellCommand: boundCommand.command }, {
cwd: options.projectPath,
env: buildDevServerProcessEnv(boundCommand.env),
stdio: ['ignore', 'pipe', 'pipe'],
detached: false,
});
const child = this.process;
const pid = child.pid;
const recordOutput = (text) => {
output += text;
portFromOutput = (0, exports.parsePortFromDevServerOutput)(output);
};
child.stdout?.on('data', (data) => recordOutput(data.toString()));
child.stderr?.on('data', (data) => recordOutput(data.toString()));
child.on('exit', () => {
if (this.process === child) {
this.process = null;
}
options.onExit?.();
});
child.on('error', (error) => {
log.warn(`Dev server process error: ${(0, errorMessage_1.errorMessage)(error)}`);
});
const detectedPort = await this.waitForServerAndDetectPort({
expectedPort: selectedPort,
preExistingPorts,
getPortFromOutput: () => portFromOutput,
});
if (!detectedPort) {
const outputTail = output.trim().slice(-STARTUP_ERROR_OUTPUT_TAIL_CHARS);
await this.stop();
const missingTools = inferMissingToolsFromError(output);
const error = buildStartFailureGuidance({
command: boundCommand.command,
projectPath: options.projectPath,
outputTail,
installCommand: commandResolution.installCommand,
});
if (commandResolution.source === 'cache') {
await this.deps.cache.markFailed(options.projectPath, error);
}
return {
success: false,
error,
...(missingTools.length > 0 && { missingTools }),
};
}
this.port = detectedPort;
const stability = await this.waitForStableServer(detectedPort);
if (!stability.success) {
const outputTail = output.trim().slice(-STARTUP_ERROR_OUTPUT_TAIL_CHARS);
await this.stop();
const error = buildStartFailureGuidance({
command: boundCommand.command,
projectPath: options.projectPath,
outputTail,
installCommand: commandResolution.installCommand,
summary: `Start command \`${boundCommand.command}\` (run in ${options.projectPath}) started but failed: ${stability.error}`,
});
if (commandResolution.source === 'cache') {
await this.deps.cache.markFailed(options.projectPath, error);
}
return {
success: false,
error,
};
}
await this.deps.cache.write(options.projectPath, {
startCommand: commandResolution.startCommand,
installCommand: commandResolution.installCommand,
});
return {
success: true,
host: 'localhost',
port: detectedPort,
pid,
ownership: 'rivet_owned',
startedByRivet: true,
stop: () => this.stop(),
process: child,
commandSource: commandResolution.source,
portBindingMode: boundCommand.mode,
};
}
async stop() {
const ownedProcess = this.process;
const ownedPort = this.port;
const ownedProjectPath = this.projectPath;
if (ownedProcess && ownedProcess.exitCode === null) {
let hasExited = false;
const recordExit = () => {
hasExited = true;
};
ownedProcess.once('exit', recordExit);
try {
await this.deps.operatingSystemServices.projectProcesses.terminate(ownedProcess, { isForceful: false });
}
catch {
}
await this.deps.sleep(1_000);
if (!hasExited) {
try {
await this.deps.operatingSystemServices.projectProcesses.terminate(ownedProcess, { isForceful: true });
}
catch {
}
}
ownedProcess.off('exit', recordExit);
}
if (ownedPort && ownedProjectPath) {
await this.killProjectListenerOnPort(ownedPort, ownedProjectPath);
}
this.process = null;
this.port = null;
this.projectPath = null;
}
async collectPreExistingPorts(preferredPort) {
const ports = [
preferredPort,
...COMMON_DEV_SERVER_PORTS.filter((port) => port !== preferredPort),
];
const preExistingPorts = new Set();
for (const port of ports) {
if (await this.deps.isPortResponding(port)) {
preExistingPorts.add(port);
}
}
return preExistingPorts;
}
async waitForServerAndDetectPort(options) {
const maxAttempts = Math.ceil(DEFAULT_START_TIMEOUT_MS / DEFAULT_PORT_CHECK_INTERVAL_MS) + 1;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const outputPort = options.getPortFromOutput();
if (outputPort &&
!options.preExistingPorts.has(outputPort) &&
(await this.deps.isPortResponding(outputPort))) {
return outputPort;
}
if (!this.process || this.process.killed) {
return null;
}
if (!options.preExistingPorts.has(options.expectedPort) &&
(await this.deps.isPortResponding(options.expectedPort))) {
return options.expectedPort;
}
await this.deps.sleep(DEFAULT_PORT_CHECK_INTERVAL_MS);
}
return null;
}
async waitForStableServer(port) {
for (let attempt = 0; attempt < DEFAULT_STABILITY_ATTEMPTS; attempt++) {
if (!this.process || this.process.killed) {
return {
success: false,
error: 'Dev server process exited shortly after startup.',
};
}
if (!(await this.deps.isPortResponding(port))) {
return {
success: false,
error: `Dev server on localhost:${port} stopped responding during startup.`,
};
}
if (attempt < DEFAULT_STABILITY_ATTEMPTS - 1) {
await this.deps.sleep(DEFAULT_STABILITY_INTERVAL_MS);
}
}
return { success: true };
}
async killProjectListenerOnPort(port, projectPath) {
try {
const { stdout } = await this.deps.exec(`lsof -nP -iTCP:${port} -sTCP:LISTEN -Fp`);
const pids = stdout
.split('\n')
.map((line) => line.trim())
.filter((line) => line.startsWith('p'))
.map((line) => Number(line.slice(1)))
.filter((pid) => Number.isInteger(pid) && pid > 0);
const expectedPath = normalizeProjectPath(projectPath);
for (const pid of [...new Set(pids)]) {
const cwd = await this.getProcessCwd(pid);
if (!cwd || normalizeProjectPath(cwd) !== expectedPath) {
continue;
}
await this.deps.exec(`kill -9 ${pid}`);
}
}
catch {
}
}
async getProcessCwd(pid) {
try {
const { stdout } = await this.deps.exec(`lsof -a -p ${pid} -d cwd -Fn`);
const cwdLine = stdout
.split('\n')
.map((line) => line.trim())
.find((line) => line.startsWith('n'));
return cwdLine ? cwdLine.slice(1) : null;
}
catch {
return null;
}
}
}
exports.DevServerRuntimeService = DevServerRuntimeService;
const createDevServerRuntimeService = () => new DevServerRuntimeService();
exports.createDevServerRuntimeService = createDevServerRuntimeService;
//# sourceMappingURL=DevServerRuntimeService.js.mapSHA-256: c43103b6288ac567d8236455e9f209507e41ba6f1c719ecbc07c2362e43abc08