289 lines
13 KiB
TypeScript
289 lines
13 KiB
TypeScript
import axios from "axios";
|
||
import { exec, spawn, ChildProcess } from "child_process";
|
||
import { promisify } from "util";
|
||
import fs from "fs";
|
||
import path from "path";
|
||
|
||
const execAsync = promisify(exec);
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// PATH CONFIGURATION
|
||
// ═══════════════════════════════════════════════════════════════
|
||
const ROOT = process.cwd();
|
||
const BLOCKCHAIN_DIR = path.join(ROOT, "blockchain");
|
||
const BACKEND_DIR = path.join(ROOT, "backend-agent");
|
||
const NODE_MODULES = path.join(BACKEND_DIR, "node_modules");
|
||
const TSX_CLI = path.join(BACKEND_DIR, "node_modules", "tsx", "dist", "cli.mjs");
|
||
const MOCK_COMFY_SCRIPT = path.join(ROOT, "scripts", "mock_comfy.ts");
|
||
const VALIDATE_SCRIPT = path.join(ROOT, "scripts", "validate_system.ts");
|
||
const MIDDLEWARE_CONFIG = path.join(ROOT, "middleware", "blockchain_config.json");
|
||
const BLOCKCHAIN_CONFIG = path.join(BLOCKCHAIN_DIR, "blockchain_config.json");
|
||
const DEPLOY_ADDRESS_FILE = path.join(BLOCKCHAIN_DIR, ".last_deploy_address");
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// HELPER FUNCTIONS
|
||
// ═══════════════════════════════════════════════════════════════
|
||
function shellQuote(arg: string): string {
|
||
return `"${arg.replace(/"/g, '\\"')}"`;
|
||
}
|
||
|
||
function buildCommand(command: string, args: string[]): string {
|
||
return [command, ...args].map(shellQuote).join(" ");
|
||
}
|
||
|
||
async function run(command: string, args: string[], cwd: string): Promise<string> {
|
||
const cmd = buildCommand(command, args);
|
||
const { stdout, stderr } = await execAsync(cmd, {
|
||
cwd,
|
||
windowsHide: true,
|
||
maxBuffer: 10 * 1024 * 1024,
|
||
env: { ...process.env, NODE_PATH: NODE_MODULES }
|
||
});
|
||
|
||
if (stdout) {
|
||
process.stdout.write(stdout);
|
||
}
|
||
if (stderr) {
|
||
process.stderr.write(stderr);
|
||
}
|
||
|
||
return stdout;
|
||
}
|
||
|
||
async function sleep(ms: number): Promise<void> {
|
||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||
}
|
||
|
||
async function ensureBackendDeps(): Promise<void> {
|
||
if (!fs.existsSync(NODE_MODULES)) {
|
||
console.log("📦 Installing backend dependencies...");
|
||
await run("npm", ["install"], BACKEND_DIR);
|
||
}
|
||
}
|
||
|
||
function spawnDetached(command: string, args: string[], cwd: string): ChildProcess {
|
||
const child = spawn(command, args, {
|
||
cwd,
|
||
detached: true,
|
||
stdio: "ignore",
|
||
shell: false,
|
||
windowsHide: true,
|
||
env: { ...process.env, NODE_PATH: NODE_MODULES }
|
||
});
|
||
child.unref();
|
||
return child;
|
||
}
|
||
|
||
function readLastDeployAddress(): string {
|
||
if (!fs.existsSync(DEPLOY_ADDRESS_FILE)) {
|
||
throw new Error(`Missing deploy address file: ${DEPLOY_ADDRESS_FILE}`);
|
||
}
|
||
|
||
const address = fs.readFileSync(DEPLOY_ADDRESS_FILE, "utf8").trim();
|
||
if (!address) {
|
||
throw new Error("Deploy address file is empty.");
|
||
}
|
||
|
||
return address;
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// PHASE 0: DOCKER CHECK (FAULT-TOLERANT LOOP)
|
||
// ═══════════════════════════════════════════════════════════════
|
||
async function waitForDocker(maxAttempts: number, delayMs: number): Promise<void> {
|
||
console.log("\n🔍 PHASE 0: Docker Daemon Check");
|
||
console.log("═══════════════════════════════════════════════════════════════\n");
|
||
|
||
for (let attempt = 1; attempt <= maxAttempts; attempt += 1) {
|
||
try {
|
||
await run("docker", ["info"], ROOT);
|
||
console.log("✅ Docker detected. Starting infrastructure....\n");
|
||
return;
|
||
} catch {
|
||
if (attempt === maxAttempts) {
|
||
throw new Error(
|
||
`Docker daemon not detected after ${maxAttempts} retries (${(maxAttempts * delayMs) / 1000}s total).`
|
||
);
|
||
}
|
||
console.log(`🟡 Waiting for Docker Daemon... (Please start Docker Desktop). Attempt ${attempt}/${maxAttempts}`);
|
||
await sleep(delayMs);
|
||
}
|
||
}
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// PHASE 1: BLOCKCHAIN IGNITION
|
||
// ═══════════════════════════════════════════════════════════════
|
||
async function startBlockchain(): Promise<void> {
|
||
console.log("\n⚡ PHASE 1: Blockchain Ignition");
|
||
console.log("═══════════════════════════════════════════════════════════════\n");
|
||
|
||
console.log("🚀 Starting Docker Compose for Private Geth Network...");
|
||
await run("docker", ["compose", "up", "-d"], BLOCKCHAIN_DIR);
|
||
|
||
console.log("⏳ Waiting for Geth RPC to become available...");
|
||
await waitForGeth(60000);
|
||
console.log("✅ Private Geth Network is Live.\n");
|
||
}
|
||
|
||
async function waitForGeth(timeoutMs: number): Promise<void> {
|
||
const started = Date.now();
|
||
let attempts = 0;
|
||
|
||
while (Date.now() - started < timeoutMs) {
|
||
attempts += 1;
|
||
try {
|
||
const response = await axios.post(
|
||
"http://127.0.0.1:8545",
|
||
{ jsonrpc: "2.0", id: 1, method: "eth_blockNumber", params: [] },
|
||
{ timeout: 4000 }
|
||
);
|
||
|
||
if (typeof response.data?.result === "string") {
|
||
const blockNumber = parseInt(response.data.result, 16);
|
||
console.log(` 📡 Geth responded. Current block: ${blockNumber}`);
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
const elapsed = ((Date.now() - started) / 1000).toFixed(1);
|
||
if (attempts % 5 === 0) {
|
||
console.log(` ⏳ Still waiting... (${elapsed}s elapsed)`);
|
||
}
|
||
}
|
||
|
||
await sleep(2000);
|
||
}
|
||
|
||
throw new Error("Timed out waiting for Geth RPC response.");
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// PHASE 2: THE LEGAL LAYER (AUTO-DEPLOY)
|
||
// ═══════════════════════════════════════════════════════════════
|
||
async function deployContracts(): Promise<void> {
|
||
console.log("\n📜 PHASE 2: The Legal Layer (Smart Contracts)");
|
||
console.log("═══════════════════════════════════════════════════════════════\n");
|
||
|
||
// console.log("🔨 Compiling contracts...");
|
||
// await run("npx", ["hardhat", "compile"], BLOCKCHAIN_DIR);
|
||
|
||
console.log("🔨 Deploying AstralAccess smart contract...");
|
||
await run("node", ["deploy.js"], BLOCKCHAIN_DIR);
|
||
|
||
const contractAddress = readLastDeployAddress();
|
||
console.log(` 📍 Contract deployed at: ${contractAddress}`);
|
||
|
||
console.log("📤 Exporting ABI and generating config...");
|
||
await run("node", ["scripts/export_abi.js", contractAddress], BLOCKCHAIN_DIR);
|
||
|
||
// Verify config was generated
|
||
if (!fs.existsSync(MIDDLEWARE_CONFIG)) {
|
||
throw new Error(`Expected generated config missing: ${MIDDLEWARE_CONFIG}`);
|
||
}
|
||
|
||
// Copy to blockchain directory for reference
|
||
fs.copyFileSync(MIDDLEWARE_CONFIG, BLOCKCHAIN_CONFIG);
|
||
|
||
console.log("✅ Smart Contracts Deployed & Config Exported.\n");
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// PHASE 3: SERVICE LAYER
|
||
// ═══════════════════════════════════════════════════════════════
|
||
async function startServices(): Promise<void> {
|
||
console.log("\n🛠️ PHASE 3: Service Layer");
|
||
console.log("═══════════════════════════════════════════════════════════════\n");
|
||
|
||
console.log("🎨 Starting Mock ComfyUI (AI Engine)...");
|
||
spawnDetached("node", [TSX_CLI, MOCK_COMFY_SCRIPT], ROOT);
|
||
await sleep(1000); // Brief pause for process spawn
|
||
|
||
console.log("🦞 Starting OpenClaw Middleware...");
|
||
// spawnDetached("npm", ["start"], BACKEND_DIR);
|
||
spawnDetached("node", [TSX_CLI, "server.ts"], BACKEND_DIR);
|
||
await sleep(2000); // Brief pause for process spawn
|
||
|
||
console.log("⏳ Polling services for availability...");
|
||
await waitForUrl("http://127.0.0.1:3001/health", 60000, "Middleware");
|
||
console.log(" ✅ OpenClaw Middleware is responding (Port 3001)");
|
||
|
||
await waitForUrl("http://127.0.0.1:8188/system_stats", 60000, "Mock AI");
|
||
console.log(" ✅ Mock ComfyUI is responding (Port 8188)");
|
||
|
||
console.log("\n✅ All Services Online.\n");
|
||
}
|
||
|
||
async function waitForUrl(url: string, timeoutMs: number, serviceName: string): Promise<void> {
|
||
const started = Date.now();
|
||
let attempts = 0;
|
||
|
||
while (Date.now() - started < timeoutMs) {
|
||
attempts += 1;
|
||
try {
|
||
const response = await axios.get(url, { timeout: 4000, validateStatus: () => true });
|
||
if (response.status >= 200 && response.status < 500) {
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
const elapsed = ((Date.now() - started) / 1000).toFixed(1);
|
||
if (attempts % 10 === 0) {
|
||
console.log(` ⏳ ${serviceName} not ready yet... (${elapsed}s elapsed)`);
|
||
}
|
||
}
|
||
|
||
await sleep(2000);
|
||
}
|
||
|
||
throw new Error(`Timed out waiting for ${serviceName}: ${url}`);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// PHASE 4: SELF-VALIDATION
|
||
// ═══════════════════════════════════════════════════════════════
|
||
async function runValidation(): Promise<void> {
|
||
console.log("\n🔬 PHASE 4: Self-Validation");
|
||
console.log("═══════════════════════════════════════════════════════════════\n");
|
||
|
||
await run("node", [TSX_CLI, VALIDATE_SCRIPT], ROOT);
|
||
}
|
||
|
||
// ═══════════════════════════════════════════════════════════════
|
||
// MAIN ORCHESTRATION
|
||
// ═══════════════════════════════════════════════════════════════
|
||
async function main(): Promise<void> {
|
||
console.log("\n");
|
||
console.log("╔═══════════════════════════════════════════════════════════════╗");
|
||
console.log("║ 🌌 PROJECT ASTRAL - ROBUST BOOTLOADER v2.0 ║");
|
||
console.log("╚═══════════════════════════════════════════════════════════════╝");
|
||
|
||
await ensureBackendDeps();
|
||
|
||
// Phase 0: Docker Check (Retry Loop - 20 attempts × 5s = 100s max)
|
||
await waitForDocker(20, 5000);
|
||
|
||
// Phase 1: Blockchain Ignition
|
||
await startBlockchain();
|
||
|
||
// Phase 2: Smart Contract Deployment
|
||
await deployContracts();
|
||
|
||
// Phase 3: Service Layer (Middleware + AI)
|
||
await startServices();
|
||
|
||
// Phase 4: Self-Validation
|
||
await runValidation();
|
||
|
||
console.log("\n");
|
||
console.log("╔═══════════════════════════════════════════════════════════════╗");
|
||
console.log("║ 🎉 ASTRAL BOOT SEQUENCE COMPLETE 🎉 ║");
|
||
console.log("╚═══════════════════════════════════════════════════════════════╝");
|
||
console.log("\n");
|
||
}
|
||
|
||
main().catch((error) => {
|
||
console.error("\n❌ FATAL ERROR during boot sequence:");
|
||
console.error(error instanceof Error ? error.message : String(error));
|
||
console.error("\nBoot sequence aborted.\n");
|
||
process.exitCode = 1;
|
||
});
|