426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
643 lines
17 KiB
JavaScript
643 lines
17 KiB
JavaScript
#!/usr/bin/env node
|
|
// Drives repo automation dev all with explicit CLI and CI behavior.
|
|
import { execFileSync, spawn } from "node:child_process";
|
|
import { existsSync } from "node:fs";
|
|
import net from "node:net";
|
|
import process from "node:process";
|
|
import { resolveDevAllSkipPlugins } from "./lib/script-metadata.mjs";
|
|
|
|
const args = new Set(process.argv.slice(2));
|
|
const dryRun = args.has("--dry-run");
|
|
const fullPrepare =
|
|
args.has("--full-prepare") || process.env.DEV_ALL_FULL_PREPARE === "1";
|
|
const forcePrepare =
|
|
args.has("--force-prepare") || process.env.DEV_ALL_FORCE_PREPARE === "1";
|
|
const skipPrepare =
|
|
args.has("--no-prepare") || process.env.DEV_ALL_SKIP_PREPARE === "1";
|
|
const skipCloudDb =
|
|
args.has("--no-cloud-db") || process.env.DEV_ALL_SKIP_CLOUD_DB === "1";
|
|
const buildNativePlugins =
|
|
args.has("--build-native-plugins") ||
|
|
process.env.DEV_ALL_BUILD_NATIVE_PLUGINS === "1";
|
|
const enableTestAuth =
|
|
args.has("--test-auth") || process.env.DEV_ALL_ENABLE_TEST_AUTH !== "0";
|
|
|
|
const repoRoot = process.cwd();
|
|
const bunBin =
|
|
process.env.BUN_BIN ||
|
|
(process.env.npm_execpath?.includes("bun")
|
|
? process.env.npm_execpath
|
|
: undefined) ||
|
|
(process.env.BUN_INSTALL
|
|
? `${process.env.BUN_INSTALL}/bin/bun`
|
|
: undefined) ||
|
|
"bun";
|
|
const bunBinDir = bunBin.includes("/")
|
|
? bunBin.slice(0, bunBin.lastIndexOf("/"))
|
|
: "";
|
|
const childPath = [bunBinDir, process.env.PATH].filter(Boolean).join(":");
|
|
|
|
function envDefault(key, value) {
|
|
return process.env[key]?.trim() || value;
|
|
}
|
|
|
|
const ports = {
|
|
agentApi: envDefault("DEV_ALL_AGENT_API_PORT", "31337"),
|
|
frontend: envDefault("DEV_ALL_FRONTEND_PORT", "2138"),
|
|
homepage: envDefault("DEV_ALL_HOMEPAGE_PORT", "4444"),
|
|
osHomepage: envDefault("DEV_ALL_OS_HOMEPAGE_PORT", "4455"),
|
|
cloudWeb: envDefault("DEV_ALL_CLOUD_WEB_PORT", "3000"),
|
|
cloudApi: envDefault("DEV_ALL_CLOUD_API_PORT", "8787"),
|
|
cloudDb: envDefault("DEV_ALL_CLOUD_DB_PORT", "55432"),
|
|
};
|
|
|
|
const urls = {
|
|
agentApi: `http://127.0.0.1:${ports.agentApi}`,
|
|
frontend: `http://localhost:${ports.frontend}`,
|
|
homepage: `http://localhost:${ports.homepage}`,
|
|
osHomepage: `http://localhost:${ports.osHomepage}`,
|
|
cloudWeb: `http://localhost:${ports.cloudWeb}`,
|
|
cloudApi: `http://localhost:${ports.cloudApi}`,
|
|
cloudDb: `postgresql://postgres@127.0.0.1:${ports.cloudDb}/postgres`,
|
|
};
|
|
const localTestAuthSecret = envDefault(
|
|
"PLAYWRIGHT_TEST_AUTH_SECRET",
|
|
"playwright-local-auth-secret",
|
|
);
|
|
const serviceStartupTimeoutMs = Number.parseInt(
|
|
envDefault("DEV_ALL_SERVICE_STARTUP_TIMEOUT_MS", "120000"),
|
|
10,
|
|
);
|
|
|
|
const packagedCloudAvailable = existsSync(
|
|
`${repoRoot}/packages/cloud/api/package.json`,
|
|
);
|
|
const cloudMode = packagedCloudAvailable ? "packages" : "legacy";
|
|
const commonEnv = {
|
|
...process.env,
|
|
NODE_ENV: "development",
|
|
PATH: childPath,
|
|
ELIZA_DEV_SOURCE: envDefault("ELIZA_DEV_SOURCE", "1"),
|
|
ELIZA_DEV_NO_WATCH: envDefault("ELIZA_DEV_NO_WATCH", "0"),
|
|
};
|
|
const cloudSharedEnv = {
|
|
...commonEnv,
|
|
API_DEV_PORT: ports.cloudApi,
|
|
DATABASE_URL: envDefault("DATABASE_URL", urls.cloudDb),
|
|
ELIZA_CLOUD_LOCAL_APP_URL: urls.cloudWeb,
|
|
ELIZA_CLOUD_LOCAL_API_URL: urls.cloudApi,
|
|
NEXT_PUBLIC_APP_URL: urls.cloudWeb,
|
|
NEXT_PUBLIC_API_URL: urls.cloudApi,
|
|
NEXT_PUBLIC_ELIZA_APP_URL: urls.homepage,
|
|
NEXT_PUBLIC_ELIZA_API_URL: urls.cloudApi,
|
|
NEXT_PUBLIC_ELIZA_PROXY_URL: urls.cloudWeb,
|
|
NEXT_PUBLIC_STEWARD_API_URL: `${urls.cloudApi}/steward`,
|
|
VITE_ELIZA_APP_URL: urls.homepage,
|
|
VITE_ELIZA_CLOUD_URL: urls.cloudWeb,
|
|
VITE_ELIZA_OS_URL: urls.osHomepage,
|
|
...(enableTestAuth
|
|
? {
|
|
AGENT_TEST_BOOTSTRAP_ADMIN: "true",
|
|
PLAYWRIGHT_TEST_AUTH: "true",
|
|
PLAYWRIGHT_TEST_AUTH_SECRET: localTestAuthSecret,
|
|
RATE_LIMIT_DISABLED: "true",
|
|
}
|
|
: {}),
|
|
VITE_API_PROXY_TARGET: urls.cloudApi,
|
|
VITE_ALLOWED_HOSTS: [
|
|
"localhost",
|
|
"127.0.0.1",
|
|
"::1",
|
|
process.env.VITE_ALLOWED_HOSTS,
|
|
]
|
|
.filter(Boolean)
|
|
.join(","),
|
|
};
|
|
const agentEnv = {
|
|
...commonEnv,
|
|
API_PORT: ports.agentApi,
|
|
SERVER_PORT: ports.agentApi,
|
|
ELIZA_PORT: ports.agentApi,
|
|
ELIZA_API_PORT: ports.agentApi,
|
|
ELIZA_UI_ENABLE: "true",
|
|
ELIZA_API_BIND: envDefault("ELIZA_API_BIND", "127.0.0.1"),
|
|
ELIZAOS_CLOUD_BASE_URL: envDefault(
|
|
"ELIZAOS_CLOUD_BASE_URL",
|
|
`${urls.cloudApi}/api/v1`,
|
|
),
|
|
ELIZA_CLOUD_URL: urls.cloudWeb,
|
|
ELIZA_WALLET_OS_STORE: envDefault("ELIZA_WALLET_OS_STORE", "0"),
|
|
ELIZA_ALLOW_NO_PROVIDER: envDefault("ELIZA_ALLOW_NO_PROVIDER", "1"),
|
|
ELIZA_DISABLE_LOCAL_EMBEDDINGS: envDefault(
|
|
"ELIZA_DISABLE_LOCAL_EMBEDDINGS",
|
|
"true",
|
|
),
|
|
// Plugins that opt out of the dev-all agent stack declare
|
|
// `elizaos.scripts.devStack.skipInDevAll` in their own package.json; resolved
|
|
// through the discovery seam so no plugin names live in this file.
|
|
ELIZA_SKIP_PLUGINS: [
|
|
process.env.ELIZA_SKIP_PLUGINS,
|
|
...resolveDevAllSkipPlugins({ repoRoot }),
|
|
]
|
|
.filter(Boolean)
|
|
.join(","),
|
|
PGLITE_DATA_DIR: envDefault(
|
|
"DEV_ALL_AGENT_PGLITE_DATA_DIR",
|
|
`${repoRoot}/.eliza/agent-pglite`,
|
|
),
|
|
EVM_PRIVATE_KEY: "",
|
|
SOLANA_PRIVATE_KEY: "",
|
|
};
|
|
const frontendEnv = {
|
|
...commonEnv,
|
|
PORT: ports.frontend,
|
|
ELIZA_UI_PORT: ports.frontend,
|
|
ELIZA_API_PORT: ports.agentApi,
|
|
ELIZA_PORT: ports.agentApi,
|
|
VITE_ELIZA_CLOUD_BASE: urls.cloudWeb,
|
|
VITE_ELIZA_IOS_API_BASE: urls.cloudApi,
|
|
VITE_ELIZACLOUD_API_URL: urls.cloudApi,
|
|
VITE_ASSET_BASE_URL: envDefault(
|
|
"VITE_ASSET_BASE_URL",
|
|
"https://blob.elizacloud.ai",
|
|
),
|
|
VITE_ELIZA_APP_URL: urls.homepage,
|
|
VITE_ELIZA_CLOUD_URL: urls.cloudWeb,
|
|
VITE_ELIZA_OS_URL: urls.osHomepage,
|
|
ELIZA_APP_VITE_NO_DISCOVERY: envDefault("ELIZA_APP_VITE_NO_DISCOVERY", "1"),
|
|
...(enableTestAuth ? { VITE_PLAYWRIGHT_TEST_AUTH: "true" } : {}),
|
|
};
|
|
const homepageEnv = {
|
|
...commonEnv,
|
|
PORT: ports.homepage,
|
|
VITE_ELIZACLOUD_API_URL: urls.cloudApi,
|
|
VITE_ELIZA_APP_URL: urls.homepage,
|
|
VITE_ELIZA_CLOUD_URL: urls.cloudWeb,
|
|
VITE_ELIZA_OS_URL: urls.osHomepage,
|
|
...(enableTestAuth ? { VITE_PLAYWRIGHT_TEST_AUTH: "true" } : {}),
|
|
};
|
|
const osHomepageEnv = {
|
|
...commonEnv,
|
|
PORT: ports.osHomepage,
|
|
VITE_ELIZACLOUD_API_URL: urls.cloudApi,
|
|
VITE_ELIZA_APP_URL: urls.homepage,
|
|
VITE_ELIZA_CLOUD_URL: urls.cloudWeb,
|
|
VITE_ELIZA_OS_URL: urls.osHomepage,
|
|
...(enableTestAuth ? { VITE_PLAYWRIGHT_TEST_AUTH: "true" } : {}),
|
|
};
|
|
const cloudDbEnv = {
|
|
...cloudSharedEnv,
|
|
PGLITE_PORT: ports.cloudDb,
|
|
PGLITE_HOST: "127.0.0.1",
|
|
};
|
|
const cloudApiService = packagedCloudAvailable
|
|
? { cwd: "packages/cloud/api", command: [bunBin, "run", "dev"] }
|
|
: { cwd: "cloud", command: [bunBin, "run", "dev:api"] };
|
|
const cloudWebService = packagedCloudAvailable
|
|
? {
|
|
cwd: "packages/app",
|
|
command: [
|
|
bunBin,
|
|
"run",
|
|
"dev",
|
|
"--",
|
|
"--host",
|
|
"0.0.0.0",
|
|
"--port",
|
|
ports.cloudWeb,
|
|
"--strictPort",
|
|
],
|
|
}
|
|
: {
|
|
cwd: "cloud",
|
|
command: [
|
|
bunBin,
|
|
"run",
|
|
"dev:web",
|
|
"--",
|
|
"--host",
|
|
"0.0.0.0",
|
|
"--port",
|
|
ports.cloudWeb,
|
|
"--strictPort",
|
|
],
|
|
};
|
|
|
|
const services = [
|
|
!skipCloudDb && {
|
|
name: "cloud-db",
|
|
cwd: ".",
|
|
command: [bunBin, "run", "db:cloud:pglite"],
|
|
env: cloudDbEnv,
|
|
},
|
|
{ name: "cloud-api", ...cloudApiService, env: cloudSharedEnv },
|
|
{
|
|
name: "cloud-web",
|
|
...cloudWebService,
|
|
env: { ...cloudSharedEnv, PORT: ports.cloudWeb },
|
|
},
|
|
{
|
|
name: "agent",
|
|
cwd: ".",
|
|
command: ["node", "packages/scripts/dev-agent-watch.mjs"],
|
|
env: agentEnv,
|
|
},
|
|
{
|
|
name: "frontend",
|
|
cwd: "packages/app",
|
|
command: [
|
|
bunBin,
|
|
"run",
|
|
"dev",
|
|
"--",
|
|
"--host",
|
|
"0.0.0.0",
|
|
"--port",
|
|
ports.frontend,
|
|
"--strictPort",
|
|
],
|
|
env: frontendEnv,
|
|
},
|
|
{
|
|
name: "homepage",
|
|
cwd: "packages/homepage",
|
|
command: [
|
|
bunBin,
|
|
"run",
|
|
"dev",
|
|
"--",
|
|
"--host",
|
|
"0.0.0.0",
|
|
"--port",
|
|
ports.homepage,
|
|
"--strictPort",
|
|
],
|
|
env: homepageEnv,
|
|
},
|
|
{
|
|
name: "os-homepage",
|
|
cwd: "packages/os/homepage",
|
|
command: [
|
|
bunBin,
|
|
"run",
|
|
"dev",
|
|
"--",
|
|
"--host",
|
|
"0.0.0.0",
|
|
"--port",
|
|
ports.osHomepage,
|
|
"--strictPort",
|
|
],
|
|
env: osHomepageEnv,
|
|
},
|
|
].filter(Boolean);
|
|
|
|
const cloudDevVarsCommand = packagedCloudAvailable
|
|
? [bunBin, "run", "packages/scripts/cloud/admin/sync-api-dev-vars.ts"]
|
|
: [bunBin, "run", "--cwd", "cloud", "packages/scripts/sync-api-dev-vars.ts"];
|
|
|
|
function packageDistReady(packageDir, requiredFiles) {
|
|
if (forcePrepare) return false;
|
|
return requiredFiles.every((file) =>
|
|
existsSync(`${repoRoot}/${packageDir}/${file}`),
|
|
);
|
|
}
|
|
|
|
function buildDefaultPrepareCommands() {
|
|
const generateCommands = [
|
|
{
|
|
label: "shared i18n keywords",
|
|
cwd: "packages/shared",
|
|
command: [bunBin, "run", "build:i18n"],
|
|
env: commonEnv,
|
|
},
|
|
{
|
|
label: "cloud dev vars",
|
|
cwd: ".",
|
|
command: cloudDevVarsCommand,
|
|
env: cloudSharedEnv,
|
|
},
|
|
];
|
|
|
|
const buildCommands = [];
|
|
if (!packageDistReady("packages/shared", ["dist/index.js"])) {
|
|
buildCommands.push({
|
|
label: "shared package build",
|
|
cwd: "packages/shared",
|
|
command: [bunBin, "run", "build:dist"],
|
|
env: commonEnv,
|
|
});
|
|
}
|
|
if (
|
|
!packageDistReady("packages/ui", [
|
|
"dist/index.js",
|
|
"dist/cloud-ui/index.css",
|
|
])
|
|
) {
|
|
buildCommands.push({
|
|
label: "ui package build",
|
|
cwd: "packages/ui",
|
|
command: [bunBin, "run", "build:dist"],
|
|
env: commonEnv,
|
|
});
|
|
}
|
|
|
|
const stages = [generateCommands, buildCommands];
|
|
if (buildNativePlugins) {
|
|
stages.push([
|
|
{
|
|
label: "app native plugin build",
|
|
cwd: "packages/app",
|
|
command: [bunBin, "run", "plugin:build"],
|
|
env: frontendEnv,
|
|
},
|
|
]);
|
|
}
|
|
return stages.filter((stage) => stage.length > 0);
|
|
}
|
|
|
|
const prepareCommands = fullPrepare
|
|
? [
|
|
[
|
|
{
|
|
label: "dev:prepare",
|
|
cwd: ".",
|
|
command: [bunBin, "run", "dev:prepare"],
|
|
env: commonEnv,
|
|
},
|
|
],
|
|
]
|
|
: buildDefaultPrepareCommands();
|
|
|
|
function printPlan() {
|
|
console.log("[dev:all] local stack");
|
|
console.log(` agent API: ${urls.agentApi}`);
|
|
console.log(` frontend: ${urls.frontend}`);
|
|
console.log(` app home: ${urls.homepage}`);
|
|
console.log(` OS home: ${urls.osHomepage}`);
|
|
console.log(` cloud web: ${urls.cloudWeb}`);
|
|
console.log(` cloud API: ${urls.cloudApi}`);
|
|
console.log(` cloud src: ${cloudMode}`);
|
|
if (!skipCloudDb) console.log(` cloud DB: ${urls.cloudDb}`);
|
|
console.log(
|
|
` test auth: ${enableTestAuth ? "enabled (local only)" : "disabled"}`,
|
|
);
|
|
console.log("");
|
|
}
|
|
|
|
function canConnect(host, port) {
|
|
return new Promise((resolve) => {
|
|
const socket = net.createConnection({ host, port: Number(port) });
|
|
socket.once("connect", () => {
|
|
socket.destroy();
|
|
resolve(true);
|
|
});
|
|
socket.once("error", () => {
|
|
socket.destroy();
|
|
resolve(false);
|
|
});
|
|
socket.setTimeout(750, () => {
|
|
socket.destroy();
|
|
resolve(false);
|
|
});
|
|
});
|
|
}
|
|
|
|
async function waitForPort(label, host, port) {
|
|
const startedAt = Date.now();
|
|
while (Date.now() - startedAt < serviceStartupTimeoutMs) {
|
|
if (await canConnect(host, port)) return;
|
|
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
}
|
|
throw new Error(`[dev:all] ${label} did not start on ${host}:${port}`);
|
|
}
|
|
|
|
function describePortOwner(port) {
|
|
try {
|
|
return execFileSync("lsof", ["-nP", `-iTCP:${port}`, "-sTCP:LISTEN"], {
|
|
encoding: "utf8",
|
|
}).trim();
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
async function assertPortsAvailable() {
|
|
const checks = [
|
|
!skipCloudDb && ["cloud DB", "127.0.0.1", ports.cloudDb],
|
|
["cloud API", "127.0.0.1", ports.cloudApi],
|
|
["cloud web", "127.0.0.1", ports.cloudWeb],
|
|
["agent API", "127.0.0.1", ports.agentApi],
|
|
["frontend", "127.0.0.1", ports.frontend],
|
|
["app home", "127.0.0.1", ports.homepage],
|
|
["OS home", "127.0.0.1", ports.osHomepage],
|
|
].filter(Boolean);
|
|
|
|
const occupied = [];
|
|
for (const [label, host, port] of checks) {
|
|
if (await canConnect(host, port)) {
|
|
occupied.push({ label, port, owner: describePortOwner(port) });
|
|
}
|
|
}
|
|
|
|
if (occupied.length > 0) {
|
|
const details = occupied
|
|
.map(
|
|
({ label, port, owner }) =>
|
|
` ${label} port ${port} is already in use${owner ? `\n${owner}` : ""}`,
|
|
)
|
|
.join("\n\n");
|
|
throw new Error(`[dev:all] cannot start; ports are occupied:\n${details}`);
|
|
}
|
|
}
|
|
|
|
function runOnce(label, cwd, command, env) {
|
|
return new Promise((resolve, reject) => {
|
|
console.log(`[dev:all] ${label}: ${command.join(" ")}`);
|
|
if (dryRun) return resolve();
|
|
const child = spawn(command[0], command.slice(1), {
|
|
cwd: cwd === "." ? repoRoot : `${repoRoot}/${cwd}`,
|
|
env,
|
|
stdio: "inherit",
|
|
});
|
|
child.on("error", reject);
|
|
child.on("exit", (code, signal) => {
|
|
if (code === 0) resolve();
|
|
else reject(new Error(`${label} exited with ${signal ?? code}`));
|
|
});
|
|
});
|
|
}
|
|
|
|
async function runPrepareStage(stage) {
|
|
await Promise.all(
|
|
stage.map(({ label, cwd, command, env }) =>
|
|
runOnce(label, cwd, command, env),
|
|
),
|
|
);
|
|
}
|
|
|
|
function prefixStream(stream, label, target) {
|
|
let pending = "";
|
|
stream.setEncoding("utf8");
|
|
stream.on("data", (chunk) => {
|
|
pending += chunk;
|
|
const lines = pending.split(/\r?\n/);
|
|
pending = lines.pop() ?? "";
|
|
for (const line of lines) target.write(`[${label}] ${line}\n`);
|
|
});
|
|
stream.on("end", () => {
|
|
if (pending) target.write(`[${label}] ${pending}\n`);
|
|
});
|
|
}
|
|
|
|
function startService(service) {
|
|
console.log(
|
|
`[dev:all] starting ${service.name}: ${service.command.join(" ")}`,
|
|
);
|
|
if (dryRun) return null;
|
|
const child = spawn(service.command[0], service.command.slice(1), {
|
|
cwd: service.cwd === "." ? repoRoot : `${repoRoot}/${service.cwd}`,
|
|
env: service.env,
|
|
detached: true,
|
|
stdio: ["inherit", "pipe", "pipe"],
|
|
});
|
|
child.serviceName = service.name;
|
|
prefixStream(child.stdout, service.name, process.stdout);
|
|
prefixStream(child.stderr, service.name, process.stderr);
|
|
child.on("error", (error) => {
|
|
console.error(
|
|
`[dev:all] ${service.name} failed to start: ${error.message}`,
|
|
);
|
|
});
|
|
return child;
|
|
}
|
|
|
|
function stopChildren(children) {
|
|
for (const child of children) {
|
|
if (!child) continue;
|
|
signalProcessGroup(child.pid, "SIGTERM");
|
|
}
|
|
}
|
|
|
|
function signalProcessGroup(pid, signal) {
|
|
try {
|
|
process.kill(-pid, signal);
|
|
return;
|
|
} catch {
|
|
// Fall back to walking descendants when the process group is gone.
|
|
}
|
|
signalProcessTree(pid, signal);
|
|
}
|
|
|
|
function childPids(pid) {
|
|
try {
|
|
return execFileSync("pgrep", ["-P", String(pid)], {
|
|
encoding: "utf8",
|
|
})
|
|
.split(/\s+/)
|
|
.filter(Boolean)
|
|
.map((value) => Number.parseInt(value, 10))
|
|
.filter(Number.isFinite);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
function signalProcessTree(pid, signal) {
|
|
for (const childPid of childPids(pid)) {
|
|
signalProcessTree(childPid, signal);
|
|
}
|
|
try {
|
|
process.kill(pid, signal);
|
|
} catch {
|
|
// Process already exited.
|
|
}
|
|
}
|
|
|
|
function stopChildrenAndExit(children, code) {
|
|
stopChildren(children);
|
|
setTimeout(() => {
|
|
for (const child of children) {
|
|
if (child) signalProcessGroup(child.pid, "SIGKILL");
|
|
}
|
|
process.exit(code);
|
|
}, 5000).unref();
|
|
}
|
|
|
|
async function main() {
|
|
printPlan();
|
|
if (!dryRun) await assertPortsAvailable();
|
|
|
|
if (!skipPrepare) {
|
|
if (!fullPrepare) {
|
|
console.log(
|
|
"[dev:all] using fast source prepare (pass --force-prepare to refresh dist, --full-prepare for Turbo)",
|
|
);
|
|
if (!buildNativePlugins) {
|
|
console.log(
|
|
"[dev:all] skipping native plugin prebuilds (pass --build-native-plugins if needed)",
|
|
);
|
|
}
|
|
}
|
|
for (const stage of prepareCommands) {
|
|
await runPrepareStage(stage);
|
|
}
|
|
} else {
|
|
console.log("[dev:all] skipping prepare steps");
|
|
}
|
|
|
|
if (dryRun) return;
|
|
await assertPortsAvailable();
|
|
const children = [];
|
|
|
|
const cloudDbService = services.find(
|
|
(service) => service.name === "cloud-db",
|
|
);
|
|
if (cloudDbService) {
|
|
const cloudDb = startService(cloudDbService);
|
|
if (cloudDb) children.push(cloudDb);
|
|
console.log(`[dev:all] waiting for cloud DB on 127.0.0.1:${ports.cloudDb}`);
|
|
await waitForPort("cloud DB", "127.0.0.1", ports.cloudDb);
|
|
}
|
|
|
|
for (const service of services) {
|
|
if (service.name === "cloud-db") continue;
|
|
const child = startService(service);
|
|
if (child) children.push(child);
|
|
}
|
|
|
|
let shuttingDown = false;
|
|
const shutdown = (signal) => {
|
|
if (shuttingDown) return;
|
|
shuttingDown = true;
|
|
console.log(`[dev:all] ${signal} received; stopping services`);
|
|
stopChildren(children);
|
|
setTimeout(() => {
|
|
for (const child of children) {
|
|
if (child) signalProcessGroup(child.pid, "SIGKILL");
|
|
}
|
|
process.exit(0);
|
|
}, 5000).unref();
|
|
};
|
|
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
|
|
for (const child of children) {
|
|
child.on("exit", (code, signal) => {
|
|
if (shuttingDown) return;
|
|
console.error(
|
|
`[dev:all] service ${child.serviceName ?? "unknown"} exited; stopping stack (${signal ?? code})`,
|
|
);
|
|
shuttingDown = true;
|
|
stopChildrenAndExit(children, typeof code === "number" ? code : 1);
|
|
});
|
|
}
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(
|
|
`[dev:all] ${error instanceof Error ? error.message : String(error)}`,
|
|
);
|
|
process.exit(1);
|
|
});
|