mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 15:48:00 +08:00
* Add recommended automations marketplace flow Co-authored-by: openhands <openhands@all-hands.dev> * Update GitHub MCP QA findings Co-authored-by: openhands <openhands@all-hands.dev> * Polish MCP and automation marketplace UI Add a shared MCP logo badge, make marketplace cards more compact, and show required MCP logos prominently on recommended automation cards. Update the extensions package lock to the per-entry catalog split and save QA screenshots/results in .pr/. Co-authored-by: openhands <openhands@all-hands.dev> * Align recommended automations styling Remove the custom gradient treatment and match the recommended automation cards and setup modal to the existing Automations and MCP page surfaces, spacing, borders, and typography. Co-authored-by: openhands <openhands@all-hands.dev> * Show existing automations before recommendations Move the recommended automations section below the current automation list and creation guidance so the page prioritizes the user existing automation state. Co-authored-by: openhands <openhands@all-hands.dev> * Add recommendations to onboarding Show recommended automations below the Say Hello input so new users can launch a curated automation from the final onboarding step. Co-authored-by: openhands <openhands@all-hands.dev> * Use extensions MCP marketplace exports Remove the local MCP marketplace wrapper and consume MCP catalog data plus logo mappings directly from @openhands/extensions/mcps. Co-authored-by: openhands <openhands@all-hands.dev> * Archive previous PR QA and add refreshed artifacts Co-authored-by: openhands <openhands@all-hands.dev> * Add scheduled automation QA evidence Co-authored-by: openhands <openhands@all-hands.dev> * Streamline recommended automation launch Co-authored-by: openhands <openhands@all-hands.dev> * Refresh QA evidence and remove old artifacts Co-authored-by: openhands <openhands@all-hands.dev> * Ensure canvas tools are on Python path * Require MCP installs before launching recommendations * Remove archived PR artifacts * Drop redundant PYTHONPATH launcher changes * Inline recommended automation catalog * Point extensions dependency at main * Augment recommended automation prompts with explicit API instructions When a recommended automation is selected, the pre-filled prompt now includes backend-specific API instructions so the agent calls the correct endpoint: - Local backends: directs the agent to use the local automation API from <RUNTIME_SERVICES> with $OPENHANDS_AUTOMATION_API_KEY auth, and explicitly tells it NOT to call the cloud API at app.all-hands.dev. - Cloud backends: directs the agent to use the OpenHands Cloud Automations API at app.all-hands.dev with Bearer $OPENHANDS_API_KEY. The buildAutomationPrompt() helper is exported for testability. Three new unit tests cover both backend kinds and prompt preservation. Co-authored-by: openhands <openhands@all-hands.dev> * Fix recommended automation launch regressions Co-authored-by: openhands <openhands@all-hands.dev> * Expose local automation API key to agent terminals Co-authored-by: openhands <openhands@all-hands.dev> * Stabilize conversation panel stop menu test Co-authored-by: openhands <openhands@all-hands.dev> * chore: Remove PR-only artifacts * fix: pin @openhands/extensions to specific commit for reproducibility Updates the dependency from #main to the exact commit SHA (3bba8e3b) that contains the MCP and automation catalogs added in extensions#237. This ensures reproducible builds since npm ci will use the locked SHA instead of potentially picking up a newer main. * Address final automation marketplace review comments Co-authored-by: openhands <openhands@all-hands.dev> --------- Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: allhands-bot <allhands-bot@users.noreply.github.com>
632 lines
23 KiB
JavaScript
632 lines
23 KiB
JavaScript
/**
|
|
* Static-frontend Development Stack
|
|
*
|
|
* Mirrors the dockerless automation stack but serves a production build of the
|
|
* frontend via `scripts/static-server.mjs` instead of the Vite dev server.
|
|
* Designed for slow / flaky network situations (e.g. plane wifi)
|
|
* where Vite's ~1000 individual module requests per page load are the
|
|
* bottleneck. The static build collapses the frontend into ~50 hashed
|
|
* chunks that all 304 cleanly on reload.
|
|
*
|
|
* Architecture (identical to dev-with-automation, only the frontend differs):
|
|
* ┌──────────────────────────────────────────────────────────────────────────┐
|
|
* │ http://localhost:8000 (Ingress Proxy) │
|
|
* │ /api/automation/* → Automation Backend │
|
|
* │ /api/*, /sockets → Agent Server │
|
|
* │ /* → Static Frontend │
|
|
* └──────────────────────────────────────────────────────────────────────────┘
|
|
* │ │ │
|
|
* ▼ ▼ ▼
|
|
* ┌─────────────┐ ┌───────────────┐ ┌──────────────────┐
|
|
* │ sirv-cli │ │ Agent Server │ │ Automation │
|
|
* │ build/ │ │ (uvx) :18000 │ │ Backend (uvx) │
|
|
* │ :3001 │ │ │ │ :18001 │
|
|
* └─────────────┘ └───────────────┘ └──────────────────┘
|
|
*
|
|
* Usage:
|
|
* npm run dev:dangerously-dockerless
|
|
* npm run dev:dangerously-dockerless -- --port 12000
|
|
* npm run dev:dangerously-dockerless -- --skip-build # reuse an existing build/
|
|
* npm run dev:dangerously-dockerless -- --automation-ref feat/my-branch
|
|
*
|
|
* Environment variables (all optional, same as dev:automation):
|
|
* - PORT: Ingress port (default: 8000)
|
|
* - OH_AUTOMATION_GIT_REF: Git ref for automation (default: main)
|
|
* - OH_AGENT_SERVER_GIT_REF: Git ref for agent-server
|
|
* - OH_SECRET_KEY: Session secret key
|
|
*/
|
|
|
|
import { spawn, spawnSync } from "node:child_process";
|
|
import { join, resolve, dirname } from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import { setTimeout as delay } from "node:timers/promises";
|
|
import process from "node:process";
|
|
|
|
import { buildFrontend } from "./static-build.mjs";
|
|
import {
|
|
buildAgentServerCommand,
|
|
buildSafeDevConfig,
|
|
buildAgentServerEnv,
|
|
formatMissingUvxGuidance,
|
|
isPortBusy,
|
|
releaseStaleConversationLeases,
|
|
} from "./dev-safe.mjs";
|
|
import {
|
|
getProcessTreeSpawnOptions,
|
|
isProcessRunning,
|
|
signalProcessTree,
|
|
} from "./dev-process-utils.mjs";
|
|
import {
|
|
buildAgentServerAutomationEnv,
|
|
buildAutomationCommand,
|
|
buildConfig,
|
|
} from "./dev-with-automation.mjs";
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const projectRoot = resolve(__dirname, "..");
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Terminal Styling
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
const c = {
|
|
reset: "\x1b[0m",
|
|
bold: "\x1b[1m",
|
|
dim: "\x1b[2m",
|
|
red: "\x1b[31m",
|
|
green: "\x1b[32m",
|
|
yellow: "\x1b[33m",
|
|
blue: "\x1b[34m",
|
|
magenta: "\x1b[35m",
|
|
cyan: "\x1b[36m",
|
|
};
|
|
|
|
function logService(name, message, color = c.reset) {
|
|
const ts = new Date().toISOString().split("T")[1].split(".")[0];
|
|
console.log(`${c.dim}${ts}${c.reset} ${color}[${name}]${c.reset} ${message}`);
|
|
}
|
|
|
|
function logStep(step, message) {
|
|
console.log(`${c.cyan}[${step}]${c.reset} ${message}`);
|
|
}
|
|
|
|
function logSuccess(message) {
|
|
console.log(`${c.green}✓${c.reset} ${message}`);
|
|
}
|
|
|
|
function logError(message) {
|
|
console.error(`${c.red}✗${c.reset} ${message}`);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// CLI parsing
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
export function parseArgs(argv = process.argv.slice(2)) {
|
|
const config = {
|
|
port: null,
|
|
automationGitRef: null,
|
|
automationRepo: null,
|
|
skipBuild: false,
|
|
verbose: false,
|
|
};
|
|
|
|
for (let i = 0; i < argv.length; i++) {
|
|
switch (argv[i]) {
|
|
case "-p":
|
|
case "--port":
|
|
config.port = parseInt(argv[++i], 10);
|
|
break;
|
|
case "--automation-ref":
|
|
config.automationGitRef = argv[++i];
|
|
break;
|
|
case "--automation-repo":
|
|
config.automationRepo = argv[++i];
|
|
break;
|
|
case "--skip-build":
|
|
config.skipBuild = true;
|
|
break;
|
|
case "-v":
|
|
case "--verbose":
|
|
config.verbose = true;
|
|
break;
|
|
case "-h":
|
|
case "--help":
|
|
showHelp();
|
|
process.exit(0);
|
|
}
|
|
}
|
|
|
|
return config;
|
|
}
|
|
|
|
function showHelp() {
|
|
console.log(`
|
|
Agent Canvas Static-frontend Development Stack
|
|
|
|
Runs the dockerless automation stack, but serves a production build of the
|
|
frontend via scripts/static-server.mjs. Use this when a remote or flaky network
|
|
makes Vite's per-module requests painful (e.g. ngrok or plane wifi).
|
|
|
|
USAGE:
|
|
npm run dev:dangerously-dockerless [-- options]
|
|
|
|
OPTIONS:
|
|
-p, --port <port> Ingress port (default: 8000)
|
|
--automation-ref <ref> Git ref for automation backend (default: main)
|
|
--automation-repo <url> Git repo URL for automation
|
|
--skip-build Reuse existing build/ directory (faster restart)
|
|
-v, --verbose Show detailed output
|
|
-h, --help Show this help
|
|
|
|
ENVIRONMENT VARIABLES:
|
|
PORT Alternative to --port
|
|
OH_AUTOMATION_GIT_REF Alternative to --automation-ref
|
|
OH_AGENT_SERVER_GIT_REF Git ref for agent-server SDK
|
|
OH_SECRET_KEY Secret key for sessions
|
|
|
|
ACCESS POINTS:
|
|
Main UI: http://localhost:PORT/
|
|
API Docs: http://localhost:PORT/api/automation/docs
|
|
|
|
NOTES:
|
|
• The build is produced once at startup. Edit the source and rerun this
|
|
command (or rebuild with \`npm run build:app\`) to pick up changes.
|
|
• The static server sends ETag headers, so reloads return 304s instead of
|
|
refetching content — much friendlier on slow links.
|
|
`);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Prerequisites & Setup
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
function commandExists(cmd) {
|
|
const result =
|
|
process.platform === "win32"
|
|
? spawnSync("where.exe", [cmd], { stdio: "pipe" })
|
|
: spawnSync("sh", ["-c", `command -v ${cmd}`], { stdio: "pipe" });
|
|
|
|
return result.status === 0;
|
|
}
|
|
|
|
function checkPrerequisites() {
|
|
logStep("1/3", "Checking prerequisites...");
|
|
|
|
if (!commandExists("uvx")) {
|
|
console.error(formatMissingUvxGuidance(projectRoot));
|
|
process.exit(1);
|
|
}
|
|
logSuccess("uvx found");
|
|
|
|
if (!commandExists("npm")) {
|
|
logError("npm is required but not found");
|
|
process.exit(1);
|
|
}
|
|
logSuccess("npm found");
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Process Management
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
const processes = new Map();
|
|
let shuttingDown = false;
|
|
|
|
function spawnService(name, command, args, options = {}) {
|
|
const proc = spawn(command, args, getProcessTreeSpawnOptions({
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|
env: { ...process.env, ...options.env },
|
|
cwd: options.cwd,
|
|
shell: process.platform === "win32",
|
|
}));
|
|
|
|
const color = options.color || c.reset;
|
|
|
|
proc.stdout.on("data", (data) => {
|
|
data
|
|
.toString()
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.forEach((line) => logService(name, line.trim(), color));
|
|
});
|
|
|
|
proc.stderr.on("data", (data) => {
|
|
data
|
|
.toString()
|
|
.split("\n")
|
|
.filter(Boolean)
|
|
.forEach((line) => logService(name, line.trim(), c.yellow));
|
|
});
|
|
|
|
proc.on("error", (error) => {
|
|
logError(`${name} failed to start: ${error.message}`);
|
|
});
|
|
|
|
proc.on("exit", (code) => {
|
|
if (code !== 0 && code !== null && !shuttingDown) {
|
|
logService(name, `Exited with code ${code}`, c.red);
|
|
}
|
|
processes.delete(name);
|
|
});
|
|
|
|
processes.set(name, proc);
|
|
return proc;
|
|
}
|
|
|
|
async function waitForService(name, url, timeoutMs = 30000) {
|
|
const start = Date.now();
|
|
|
|
while (Date.now() - start < timeoutMs) {
|
|
try {
|
|
const res = await fetch(url);
|
|
if (res.ok) {
|
|
logService(name, `Ready at ${url}`, c.green);
|
|
return true;
|
|
}
|
|
} catch {
|
|
// Keep trying
|
|
}
|
|
await delay(500);
|
|
}
|
|
|
|
logService(name, `Timeout waiting for ${url}`, c.red);
|
|
return false;
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Service Starters (agent-server + automation are byte-for-byte the same as
|
|
// dev-with-automation; the only difference is the frontend service.)
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
function startAgentServer(config) {
|
|
logService(
|
|
"agent-server",
|
|
`Starting on port ${config.agentServerPort}...`,
|
|
c.blue,
|
|
);
|
|
|
|
const agentServerCmd = buildAgentServerCommand(process.env);
|
|
logService("agent-server", `Using ${agentServerCmd.source}`, c.dim);
|
|
|
|
const safeConfig = buildSafeDevConfig(config.canvasPath, {
|
|
...process.env,
|
|
OH_CANVAS_SAFE_STATE_DIR: config.stateDir,
|
|
OH_CANVAS_SAFE_BACKEND_PORT: config.agentServerPort.toString(),
|
|
OH_CANVAS_SAFE_VSCODE_PORT: config.vscodePort.toString(),
|
|
});
|
|
|
|
const agentServerEnv = {
|
|
...buildAgentServerEnv(safeConfig),
|
|
...buildAgentServerAutomationEnv(config),
|
|
};
|
|
|
|
spawnService(
|
|
"agent-server",
|
|
agentServerCmd.command,
|
|
[
|
|
...agentServerCmd.args,
|
|
"--host",
|
|
"0.0.0.0",
|
|
"--port",
|
|
String(config.agentServerPort),
|
|
],
|
|
{
|
|
cwd: safeConfig.workspacesPath,
|
|
env: agentServerEnv,
|
|
color: c.blue,
|
|
},
|
|
);
|
|
}
|
|
|
|
function buildAutomationBackendEnv(config) {
|
|
return {
|
|
AUTOMATION_AGENT_SERVER_URL: `http://localhost:${config.agentServerPort}`,
|
|
AUTOMATION_AGENT_SERVER_API_KEY: config.sessionApiKey,
|
|
AUTOMATION_DB_URL: `sqlite+aiosqlite:///${join(config.stateDir, "automations.db")}`,
|
|
AUTOMATION_BASE_URL: `http://localhost:${config.ingressPort}`,
|
|
AUTOMATION_WORKSPACE_BASE: join(config.stateDir, "workspaces"),
|
|
AUTOMATION_LOCAL_API_KEY: config.localApiKey,
|
|
AUTOMATION_CORS_ORIGINS: `http://localhost:${config.ingressPort},http://127.0.0.1:${config.ingressPort},http://localhost:3001,http://127.0.0.1:3001`,
|
|
FILE_STORE: "local",
|
|
LOCAL_STORAGE_PATH: join(config.stateDir, "storage"),
|
|
OPENHANDS_SUPPRESS_BANNER: "1",
|
|
};
|
|
}
|
|
|
|
function startAutomationBackend(config) {
|
|
logService(
|
|
"automation",
|
|
`Starting on port ${config.autoBackendPort}...`,
|
|
c.green,
|
|
);
|
|
|
|
const automationCmd = buildAutomationCommand(process.env);
|
|
logService("automation", `Using ${automationCmd.source}`, c.dim);
|
|
|
|
spawnService(
|
|
"automation",
|
|
automationCmd.command,
|
|
[
|
|
...automationCmd.args,
|
|
"--host",
|
|
"0.0.0.0",
|
|
"--port",
|
|
config.autoBackendPort.toString(),
|
|
],
|
|
{
|
|
cwd: config.stateDir,
|
|
env: buildAutomationBackendEnv(config),
|
|
color: c.green,
|
|
},
|
|
);
|
|
}
|
|
|
|
function startStaticServer(config) {
|
|
// Reuse `vitePort` as the upstream port name so the ingress route table
|
|
// below stays identical to dev-with-automation.mjs.
|
|
logService("static", `Starting on port ${config.vitePort}...`, c.magenta);
|
|
|
|
// Mirror the proxy targets that vite.config.ts exposes in dev mode so that
|
|
// hitting :3001 directly behaves like Vite's dev server (e.g. /server_info
|
|
// is forwarded to the agent-server instead of falling back to the SPA
|
|
// shell). Without this, /server_info on :3001 returns index.html.
|
|
const staticServerScript = join(projectRoot, "scripts", "static-server.mjs");
|
|
spawnService(
|
|
"static",
|
|
"node",
|
|
[
|
|
staticServerScript,
|
|
"--dir",
|
|
join(config.canvasPath, "build"),
|
|
"--host",
|
|
"0.0.0.0",
|
|
"--port",
|
|
String(config.vitePort),
|
|
"--route",
|
|
`/api/automation=http://localhost:${config.autoBackendPort}`,
|
|
"--route",
|
|
`/api=http://localhost:${config.agentServerPort}`,
|
|
"--route",
|
|
`/sockets=http://localhost:${config.agentServerPort}`,
|
|
"--route",
|
|
`/server_info=http://localhost:${config.agentServerPort}`,
|
|
"--route",
|
|
`/health=http://localhost:${config.agentServerPort}`,
|
|
"--route",
|
|
`/ready=http://localhost:${config.agentServerPort}`,
|
|
"--route",
|
|
`/alive=http://localhost:${config.agentServerPort}`,
|
|
"--route",
|
|
`/docs=http://localhost:${config.agentServerPort}`,
|
|
"--route",
|
|
`/redoc=http://localhost:${config.agentServerPort}`,
|
|
"--route",
|
|
`/openapi.json=http://localhost:${config.agentServerPort}`,
|
|
],
|
|
{
|
|
cwd: config.canvasPath,
|
|
color: c.magenta,
|
|
},
|
|
);
|
|
}
|
|
|
|
function startIngress(config) {
|
|
logService("ingress", `Starting on port ${config.ingressPort}...`, c.yellow);
|
|
|
|
const ingressScript = join(projectRoot, "scripts", "ingress.mjs");
|
|
|
|
spawnService(
|
|
"ingress",
|
|
"node",
|
|
[
|
|
ingressScript,
|
|
"--port",
|
|
config.ingressPort.toString(),
|
|
"--route",
|
|
`/api/automation=http://localhost:${config.autoBackendPort}`,
|
|
"--route",
|
|
`/api=http://localhost:${config.agentServerPort}`,
|
|
"--route",
|
|
`/sockets=http://localhost:${config.agentServerPort}`,
|
|
"--route",
|
|
`/server_info=http://localhost:${config.agentServerPort}`,
|
|
"--route",
|
|
`/health=http://localhost:${config.agentServerPort}`,
|
|
"--route",
|
|
`/ready=http://localhost:${config.agentServerPort}`,
|
|
"--route",
|
|
`/alive=http://localhost:${config.agentServerPort}`,
|
|
"--route",
|
|
`/docs=http://localhost:${config.agentServerPort}`,
|
|
"--route",
|
|
`/redoc=http://localhost:${config.agentServerPort}`,
|
|
"--route",
|
|
`/openapi.json=http://localhost:${config.agentServerPort}`,
|
|
"--default",
|
|
`http://localhost:${config.vitePort}`,
|
|
],
|
|
{
|
|
cwd: projectRoot,
|
|
color: c.yellow,
|
|
},
|
|
);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Shutdown / Banner
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
function shutdown() {
|
|
if (shuttingDown) return;
|
|
shuttingDown = true;
|
|
|
|
console.log("");
|
|
console.log(`${c.yellow}Shutting down...${c.reset}`);
|
|
|
|
for (const [name, proc] of processes) {
|
|
logService(name, "Stopping...", c.dim);
|
|
signalProcessTree(proc, "SIGTERM");
|
|
}
|
|
|
|
setTimeout(() => {
|
|
for (const [name, proc] of processes) {
|
|
if (isProcessRunning(proc)) {
|
|
logService(name, "Force stopping...", c.dim);
|
|
signalProcessTree(proc, "SIGKILL");
|
|
}
|
|
}
|
|
process.exit(0);
|
|
}, 3000);
|
|
}
|
|
|
|
process.on("SIGINT", shutdown);
|
|
process.on("SIGTERM", shutdown);
|
|
|
|
function printBanner(config) {
|
|
console.log("");
|
|
console.log(
|
|
`${c.green}${c.bold}╔══════════════════════════════════════════════════════════════╗${c.reset}`,
|
|
);
|
|
console.log(
|
|
`${c.green}${c.bold}║${c.reset} ${c.bold}Agent Canvas Static-frontend Stack${c.reset} ${c.green}${c.bold}║${c.reset}`,
|
|
);
|
|
console.log(
|
|
`${c.green}${c.bold}╠══════════════════════════════════════════════════════════════╣${c.reset}`,
|
|
);
|
|
console.log(
|
|
`${c.green}${c.bold}║${c.reset} ${c.green}${c.bold}║${c.reset}`,
|
|
);
|
|
console.log(
|
|
`${c.green}${c.bold}║${c.reset} Main UI: ${c.cyan}http://localhost:${config.ingressPort}/${c.reset}`.padEnd(
|
|
75,
|
|
) + `${c.green}${c.bold}║${c.reset}`,
|
|
);
|
|
console.log(
|
|
`${c.green}${c.bold}║${c.reset} API Docs: ${c.cyan}http://localhost:${config.ingressPort}/api/automation/docs${c.reset}`.padEnd(
|
|
75,
|
|
) + `${c.green}${c.bold}║${c.reset}`,
|
|
);
|
|
console.log(
|
|
`${c.green}${c.bold}║${c.reset} ${c.green}${c.bold}║${c.reset}`,
|
|
);
|
|
console.log(
|
|
`${c.green}${c.bold}╚══════════════════════════════════════════════════════════════╝${c.reset}`,
|
|
);
|
|
console.log("");
|
|
console.log(`${c.dim}State directory: ${config.stateDir}${c.reset}`);
|
|
console.log(
|
|
`${c.dim}Frontend served from: ${join(config.canvasPath, "build")}${c.reset}`,
|
|
);
|
|
console.log(
|
|
`${c.dim}Edit sources, then re-run \`npm run dev:dangerously-dockerless\` to rebuild.${c.reset}`,
|
|
);
|
|
console.log(`${c.dim}Press Ctrl+C to stop${c.reset}`);
|
|
console.log("");
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Main
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
async function main() {
|
|
const args = parseArgs();
|
|
const config = await buildConfig(args);
|
|
|
|
console.log("");
|
|
console.log(
|
|
`${c.cyan}${c.bold}Agent Canvas Static-frontend Development Stack${c.reset}`,
|
|
);
|
|
console.log("");
|
|
|
|
// Setup phase (1/3)
|
|
checkPrerequisites();
|
|
|
|
// Ensure isolated state dirs (same as dev-with-automation).
|
|
const { mkdirSync } = await import("node:fs");
|
|
for (const dir of [
|
|
config.stateDir,
|
|
join(config.stateDir, "conversations"),
|
|
join(config.stateDir, "workspaces"),
|
|
join(config.stateDir, "bash_events"),
|
|
join(config.stateDir, "storage"),
|
|
]) {
|
|
mkdirSync(dir, { recursive: true });
|
|
}
|
|
|
|
// Build phase (2/3): block until the SPA is ready to serve.
|
|
buildFrontend(config, args);
|
|
|
|
// Service phase (3/3)
|
|
logStep("3/3", "Starting services...");
|
|
|
|
// The agent-server skip-loads any conversation whose `owner_lease.json`
|
|
// is held by a different `owner_instance_id` and not yet expired (45 s
|
|
// TTL). If a previous agent-server (e.g. from `npm run dev`) was killed
|
|
// ungracefully — or we restart faster than the lease TTL — every
|
|
// conversation gets hidden until those stale leases age out, which
|
|
// looks like "the new agent-server doesn't inherit my conversations".
|
|
// Bail out if a live agent-server is already bound to our port (we'd
|
|
// collide anyway), otherwise unlink the stale leases so the new server
|
|
// can claim ownership immediately.
|
|
if (await isPortBusy(config.agentServerPort)) {
|
|
logError(
|
|
`Port ${config.agentServerPort} is already in use — another ` +
|
|
`agent-server is running. Stop it (e.g. quit \`npm run dev\`) ` +
|
|
`before running dev:dangerously-dockerless.`,
|
|
);
|
|
process.exit(1);
|
|
}
|
|
const conversationsPath = join(config.stateDir, "conversations");
|
|
const cleared = releaseStaleConversationLeases(conversationsPath);
|
|
if (cleared > 0) {
|
|
logService(
|
|
"agent-server",
|
|
`Released ${cleared} stale conversation lease(s) so the new ` +
|
|
`agent-server can resume ownership.`,
|
|
c.dim,
|
|
);
|
|
}
|
|
|
|
startAgentServer(config);
|
|
await waitForService(
|
|
"agent-server",
|
|
`http://localhost:${config.agentServerPort}/server_info`,
|
|
);
|
|
|
|
startAutomationBackend(config);
|
|
|
|
startStaticServer(config);
|
|
|
|
await delay(2000);
|
|
|
|
startIngress(config);
|
|
|
|
await delay(1000);
|
|
|
|
printBanner(config);
|
|
}
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Exports for testing
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
export { buildAutomationBackendEnv, buildFrontend, startStaticServer };
|
|
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
// Main entry point (only when run directly, not when imported)
|
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
|
|
const isMainModule =
|
|
process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
|
|
if (isMainModule) {
|
|
main().catch((err) => {
|
|
logError(`Fatal error: ${err.message}`);
|
|
if (err.stack) {
|
|
console.error(c.dim + err.stack + c.reset);
|
|
}
|
|
process.exit(1);
|
|
});
|
|
}
|