mirror of
https://github.com/blackboxprogramming/BlackRoad-Operating-System.git
synced 2026-03-18 03:33:59 -05:00
Merge commit 'bb6b5676a63f13fdfbfa9bd6db997217e441c126'
This commit is contained in:
@@ -87,6 +87,15 @@ npm run check:health
|
|||||||
|
|
||||||
The script reads `os-spec/os-spec.json` and prints a status table. It is intended for operator use; production monitoring should still live in deployed observability stacks.
|
The script reads `os-spec/os-spec.json` and prints a status table. It is intended for operator use; production monitoring should still live in deployed observability stacks.
|
||||||
|
|
||||||
|
## OS Control Commands
|
||||||
|
|
||||||
|
- `npm run deploy:all`
|
||||||
|
- `npm run deploy:service -- <id>`
|
||||||
|
- `npm run health:all`
|
||||||
|
- `npm run health:matrix`
|
||||||
|
- `npm run env:check`
|
||||||
|
- `npm run repair`
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
### 🤖 AI & Communication
|
### 🤖 AI & Communication
|
||||||
|
|||||||
38
infra/cloudflare.json
Normal file
38
infra/cloudflare.json
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"zones": {
|
||||||
|
"blackroad.systems": {
|
||||||
|
"records": [
|
||||||
|
{
|
||||||
|
"type": "CNAME",
|
||||||
|
"name": "core",
|
||||||
|
"target": "core.up.railway.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CNAME",
|
||||||
|
"name": "api",
|
||||||
|
"target": "YOUR_RAILWAY_API_URL_HERE"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CNAME",
|
||||||
|
"name": "operator",
|
||||||
|
"target": "operator.<railway-app-hash>.railway.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CNAME",
|
||||||
|
"name": "console",
|
||||||
|
"target": "console-production.up.railway.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CNAME",
|
||||||
|
"name": "docs",
|
||||||
|
"target": "docs-service.railway.app"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "CNAME",
|
||||||
|
"name": "app",
|
||||||
|
"target": "blackroad.systems"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
infra/env-spec.json
Normal file
25
infra/env-spec.json
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
{
|
||||||
|
"core": {
|
||||||
|
"PORT": "8080"
|
||||||
|
},
|
||||||
|
"api": {
|
||||||
|
"PORT": "8080",
|
||||||
|
"CORE_URL": "https://core.blackroad.systems"
|
||||||
|
},
|
||||||
|
"agents": {
|
||||||
|
"PORT": "8080"
|
||||||
|
},
|
||||||
|
"operator": {
|
||||||
|
"PORT": "8080"
|
||||||
|
},
|
||||||
|
"console": {
|
||||||
|
"PORT": "8080",
|
||||||
|
"API_URL": "https://api.blackroad.systems"
|
||||||
|
},
|
||||||
|
"web": {
|
||||||
|
"PORT": "8080"
|
||||||
|
},
|
||||||
|
"docs": {
|
||||||
|
"PORT": "8080"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -12,7 +12,10 @@
|
|||||||
"check:health": "ts-node tools/health-check.ts",
|
"check:health": "ts-node tools/health-check.ts",
|
||||||
"deploy:service": "ts-node scripts/deployService.ts",
|
"deploy:service": "ts-node scripts/deployService.ts",
|
||||||
"deploy:all": "ts-node scripts/deployAll.ts",
|
"deploy:all": "ts-node scripts/deployAll.ts",
|
||||||
"health:all": "ts-node scripts/checkHealth.ts"
|
"health:all": "ts-node scripts/checkHealth.ts",
|
||||||
|
"health:matrix": "ts-node scripts/healthMatrix.ts",
|
||||||
|
"env:check": "ts-node scripts/checkEnv.ts",
|
||||||
|
"repair": "ts-node scripts/repair.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"express": "^4.21.0",
|
"express": "^4.21.0",
|
||||||
|
|||||||
40
scripts/checkEnv.ts
Normal file
40
scripts/checkEnv.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
|
||||||
|
type EnvSpec = Record<string, Record<string, string>>;
|
||||||
|
|
||||||
|
async function loadEnvSpec(): Promise<EnvSpec> {
|
||||||
|
const specPath = path.join(__dirname, "..", "infra", "env-spec.json");
|
||||||
|
const raw = await fs.readFile(specPath, "utf-8");
|
||||||
|
return JSON.parse(raw) as EnvSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printServiceRequirements(service: string, variables: string[], missing: string[]): void {
|
||||||
|
console.log(`\nService: ${service}`);
|
||||||
|
console.log(` Required variables: ${variables.join(", ") || "(none)"}`);
|
||||||
|
|
||||||
|
if (missing.length > 0) {
|
||||||
|
console.warn(` ⚠️ Missing in environment (Railway likely missing): ${missing.join(", ")}`);
|
||||||
|
} else {
|
||||||
|
console.log(" ✅ All variables present in current environment");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const spec = await loadEnvSpec();
|
||||||
|
console.log("🔍 Checking environment requirements by service...");
|
||||||
|
|
||||||
|
for (const [service, vars] of Object.entries(spec)) {
|
||||||
|
const requiredVars = Object.keys(vars);
|
||||||
|
const missing = requiredVars.filter((name) => process.env[name] === undefined);
|
||||||
|
printServiceRequirements(service, requiredVars, missing);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\nDone.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to check environment variables:", (error as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
73
scripts/healthMatrix.ts
Normal file
73
scripts/healthMatrix.ts
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
import { ServiceConfig } from "./types";
|
||||||
|
|
||||||
|
type HealthResult = {
|
||||||
|
id: string;
|
||||||
|
status: "OK" | "FAIL";
|
||||||
|
url: string;
|
||||||
|
httpStatus: number | string;
|
||||||
|
latencyMs: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function loadServices(): Promise<ServiceConfig[]> {
|
||||||
|
const servicesPath = path.join(__dirname, "..", "infra", "services.json");
|
||||||
|
const raw = await fs.readFile(servicesPath, "utf-8");
|
||||||
|
return JSON.parse(raw) as ServiceConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkService(service: ServiceConfig): Promise<HealthResult> {
|
||||||
|
const url = `https://${service.domain}${service.healthPath}`;
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, { method: "GET" });
|
||||||
|
const latencyMs = Date.now() - start;
|
||||||
|
const status: "OK" | "FAIL" = response.ok ? "OK" : "FAIL";
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: service.id,
|
||||||
|
status,
|
||||||
|
url,
|
||||||
|
httpStatus: response.status,
|
||||||
|
latencyMs,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
id: service.id,
|
||||||
|
status: "FAIL",
|
||||||
|
url,
|
||||||
|
httpStatus: (error as Error).message,
|
||||||
|
latencyMs: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function printMatrix(results: HealthResult[]): void {
|
||||||
|
console.log(
|
||||||
|
`${"service".padEnd(10)} ${"status".padEnd(8)} ${"url".padEnd(30)} latency`
|
||||||
|
);
|
||||||
|
console.log(
|
||||||
|
`${"-".repeat(10)} ${"-".repeat(8)} ${"-".repeat(30)} ${"-".repeat(7)}`
|
||||||
|
);
|
||||||
|
|
||||||
|
for (const result of results) {
|
||||||
|
const latency = result.latencyMs !== null ? `${result.latencyMs}ms` : "--";
|
||||||
|
const statusLabel = `${result.status} ${result.httpStatus}`.trim();
|
||||||
|
|
||||||
|
console.log(
|
||||||
|
`${result.id.padEnd(10)} ${statusLabel.padEnd(8)} ${result.url.padEnd(30)} ${latency}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
const services = await loadServices();
|
||||||
|
const results = await Promise.all(services.map((service) => checkService(service)));
|
||||||
|
printMatrix(results);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch((error) => {
|
||||||
|
console.error("Failed to build health matrix:", (error as Error).message);
|
||||||
|
});
|
||||||
129
scripts/repair.ts
Normal file
129
scripts/repair.ts
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
import fs from "fs/promises";
|
||||||
|
import path from "path";
|
||||||
|
import { spawn } from "child_process";
|
||||||
|
import fetch from "node-fetch";
|
||||||
|
import { ServiceConfig } from "./types";
|
||||||
|
|
||||||
|
type HealthStatus = {
|
||||||
|
id: string;
|
||||||
|
ok: boolean;
|
||||||
|
status: number | string;
|
||||||
|
latencyMs: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
async function loadServices(): Promise<ServiceConfig[]> {
|
||||||
|
const servicesPath = path.join(__dirname, "..", "infra", "services.json");
|
||||||
|
const raw = await fs.readFile(servicesPath, "utf-8");
|
||||||
|
return JSON.parse(raw) as ServiceConfig[];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function runCommand(command: string, args: string[], label: string): Promise<void> {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
const proc = spawn(command, args, { stdio: "pipe" });
|
||||||
|
let stdout = "";
|
||||||
|
let stderr = "";
|
||||||
|
|
||||||
|
proc.stdout.on("data", (data) => {
|
||||||
|
stdout += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.stderr.on("data", (data) => {
|
||||||
|
stderr += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
proc.on("close", (code) => {
|
||||||
|
if (stdout.trim()) {
|
||||||
|
console.log(stdout.trim());
|
||||||
|
}
|
||||||
|
if (stderr.trim()) {
|
||||||
|
console.error(stderr.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (code && code !== 0) {
|
||||||
|
console.warn(`${label} exited with code ${code}`);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkHealth(service: ServiceConfig): Promise<HealthStatus> {
|
||||||
|
const url = `https://${service.domain}${service.healthPath}`;
|
||||||
|
const start = Date.now();
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, { method: "GET" });
|
||||||
|
return {
|
||||||
|
id: service.id,
|
||||||
|
ok: response.ok,
|
||||||
|
status: response.status,
|
||||||
|
latencyMs: Date.now() - start,
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
id: service.id,
|
||||||
|
ok: false,
|
||||||
|
status: (error as Error).message,
|
||||||
|
latencyMs: null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function checkAllServices(services: ServiceConfig[]): Promise<HealthStatus[]> {
|
||||||
|
const results: HealthStatus[] = [];
|
||||||
|
for (const service of services) {
|
||||||
|
const status = await checkHealth(service);
|
||||||
|
results.push(status);
|
||||||
|
}
|
||||||
|
return results;
|
||||||
|
}
|
||||||
|
|
||||||
|
function printSummary(results: HealthStatus[], label: string): void {
|
||||||
|
console.log(`\n${label}`);
|
||||||
|
for (const result of results) {
|
||||||
|
const latency = result.latencyMs !== null ? `${result.latencyMs}ms` : "--";
|
||||||
|
const statusText = result.ok ? "OK" : "FAIL";
|
||||||
|
console.log(`- ${result.id}: ${statusText} (${result.status}) latency=${latency}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function attemptRestarts(failing: HealthStatus[]): Promise<void> {
|
||||||
|
if (failing.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("\nAttempting restart…");
|
||||||
|
for (const service of failing) {
|
||||||
|
console.log(`Restarting ${service.id}...`);
|
||||||
|
await runCommand("npm", ["run", "deploy:service", "--", service.id], `deploy:service ${service.id}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function main(): Promise<void> {
|
||||||
|
try {
|
||||||
|
const services = await loadServices();
|
||||||
|
|
||||||
|
console.log("Running env:check...");
|
||||||
|
await runCommand("npm", ["run", "env:check"], "env:check");
|
||||||
|
|
||||||
|
console.log("\nRunning health:all...");
|
||||||
|
await runCommand("npm", ["run", "health:all"], "health:all");
|
||||||
|
|
||||||
|
const initialHealth = await checkAllServices(services);
|
||||||
|
const failing = initialHealth.filter((result) => !result.ok);
|
||||||
|
|
||||||
|
if (failing.length > 0) {
|
||||||
|
await attemptRestarts(failing);
|
||||||
|
}
|
||||||
|
|
||||||
|
const finalHealth = await checkAllServices(services);
|
||||||
|
printSummary(initialHealth, "Initial health");
|
||||||
|
printSummary(finalHealth, "Final health");
|
||||||
|
|
||||||
|
console.log("\nRepair routine complete.");
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Repair routine encountered an error:", (error as Error).message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
main();
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2019",
|
"target": "ES2019",
|
||||||
|
"target": "ES2021",
|
||||||
"module": "commonjs",
|
"module": "commonjs",
|
||||||
|
"moduleResolution": "node",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"esModuleInterop": true,
|
"esModuleInterop": true,
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
@@ -10,4 +12,8 @@
|
|||||||
"rootDir": "."
|
"rootDir": "."
|
||||||
},
|
},
|
||||||
"include": ["scripts/**/*.ts", "tools/**/*.ts", "infra/**/*.json"]
|
"include": ["scripts/**/*.ts", "tools/**/*.ts", "infra/**/*.json"]
|
||||||
|
"include": ["scripts/**/*.ts", "tools/**/*.ts", "infra/**/*.json"],
|
||||||
|
"ts-node": {
|
||||||
|
"esm": false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user