First Release of Claw3D (#11)
Co-authored-by: iamlukethedev <iamlukethedev@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import { constants as fsConstants, promises as fs } from "node:fs";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(scriptDir, "..");
|
||||
const uxAuditDir = path.join(repoRoot, "output", "playwright", "ux-audit");
|
||||
const transientFiles = [
|
||||
path.join(repoRoot, ".agent", "ux-audit.md"),
|
||||
path.join(repoRoot, ".agent", "execplan-pending.md"),
|
||||
];
|
||||
|
||||
async function ensureDir(dir) {
|
||||
await fs.mkdir(dir, { recursive: true });
|
||||
}
|
||||
|
||||
async function clearDirContents(dir) {
|
||||
await ensureDir(dir);
|
||||
const entries = await fs.readdir(dir);
|
||||
await Promise.all(
|
||||
entries.map((entry) =>
|
||||
fs.rm(path.join(dir, entry), { recursive: true, force: true }),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
async function removeIfPresent(filePath) {
|
||||
try {
|
||||
await fs.unlink(filePath);
|
||||
} catch (error) {
|
||||
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") {
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function run(command, args) {
|
||||
return spawnSync(command, args, { encoding: "utf8" });
|
||||
}
|
||||
|
||||
async function stopPlaywrightSessions() {
|
||||
const codeHome = process.env.CODEX_HOME ?? path.join(os.homedir(), ".codex");
|
||||
const pwcli = path.join(codeHome, "skills", "playwright", "scripts", "playwright_cli.sh");
|
||||
try {
|
||||
await fs.access(pwcli, fsConstants.X_OK);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
const result = run(pwcli, ["session-stop-all"]);
|
||||
if (result.status === 0) return;
|
||||
if (result.error) {
|
||||
throw result.error;
|
||||
}
|
||||
}
|
||||
|
||||
function killPattern(pattern) {
|
||||
const result = run("pkill", ["-f", pattern]);
|
||||
if (result.status === 0 || result.status === 1) return;
|
||||
if (result.error && result.error.code === "ENOENT") return;
|
||||
if (result.error) throw result.error;
|
||||
}
|
||||
|
||||
function cleanupPlaywrightProcesses() {
|
||||
killPattern("ms-playwright/daemon");
|
||||
killPattern("playwright/cli.js run-mcp-server");
|
||||
killPattern("chrome-headless-shell");
|
||||
killPattern("Google Chrome --headless");
|
||||
killPattern("Chromium --headless");
|
||||
}
|
||||
|
||||
async function main() {
|
||||
await stopPlaywrightSessions();
|
||||
cleanupPlaywrightProcesses();
|
||||
await clearDirContents(uxAuditDir);
|
||||
for (const transientFile of transientFiles) {
|
||||
await removeIfPresent(transientFile);
|
||||
}
|
||||
console.log("cleanup:ux-artifacts complete");
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error("cleanup:ux-artifacts failed");
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -0,0 +1,82 @@
|
||||
import { spawn } from "node:child_process";
|
||||
import net from "node:net";
|
||||
|
||||
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
||||
|
||||
const getFreePort = async () => {
|
||||
for (let i = 0; i < 30; i++) {
|
||||
const port = 20000 + Math.floor(Math.random() * 20000);
|
||||
const ok = await new Promise((resolve) => {
|
||||
const server = net.createServer();
|
||||
server.once("error", () => resolve(false));
|
||||
server.listen(port, "127.0.0.1", () => {
|
||||
server.close(() => resolve(true));
|
||||
});
|
||||
});
|
||||
if (ok) return port;
|
||||
}
|
||||
throw new Error("Failed to find a free port for smoke test.");
|
||||
};
|
||||
|
||||
const main = async () => {
|
||||
const port = await getFreePort();
|
||||
const url = `http://127.0.0.1:${port}/`;
|
||||
|
||||
const child = spawn(process.execPath, ["server/index.js", "--dev"], {
|
||||
env: {
|
||||
...process.env,
|
||||
HOST: "127.0.0.1",
|
||||
PORT: String(port),
|
||||
},
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
const lines = [];
|
||||
const pushLines = (chunk) => {
|
||||
const text = String(chunk ?? "");
|
||||
for (const line of text.split(/\r?\n/)) {
|
||||
if (!line) continue;
|
||||
lines.push(line);
|
||||
if (lines.length > 80) lines.shift();
|
||||
}
|
||||
};
|
||||
child.stdout.on("data", pushLines);
|
||||
child.stderr.on("data", pushLines);
|
||||
|
||||
const deadline = Date.now() + 60_000;
|
||||
let lastErr = null;
|
||||
|
||||
try {
|
||||
while (Date.now() < deadline) {
|
||||
if (child.exitCode !== null) {
|
||||
throw new Error(`Dev server exited early with code ${child.exitCode}.`);
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(url, { redirect: "manual" });
|
||||
if (res.status >= 200 && res.status < 500) {
|
||||
process.stdout.write(`OK ${res.status} ${url}\n`);
|
||||
return;
|
||||
}
|
||||
lastErr = new Error(`Unexpected status ${res.status} for ${url}`);
|
||||
} catch (err) {
|
||||
lastErr = err;
|
||||
}
|
||||
|
||||
await sleep(500);
|
||||
}
|
||||
|
||||
throw new Error(
|
||||
`Timed out waiting for dev server to respond at ${url}. Last error: ${lastErr?.message || "unknown"}`
|
||||
);
|
||||
} finally {
|
||||
child.kill("SIGTERM");
|
||||
await Promise.race([new Promise((r) => child.once("exit", r)), sleep(2000)]);
|
||||
}
|
||||
};
|
||||
|
||||
main().catch((err) => {
|
||||
process.stderr.write(String(err?.stack || err) + "\n");
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
const fs = require("node:fs");
|
||||
const path = require("node:path");
|
||||
const { execFileSync } = require("node:child_process");
|
||||
const readline = require("node:readline/promises");
|
||||
|
||||
const { resolveStudioSettingsPath } = require("../server/studio-settings");
|
||||
|
||||
const DEFAULT_GATEWAY_URL = "ws://127.0.0.1:18789";
|
||||
|
||||
const parseArgs = (argv) => {
|
||||
return {
|
||||
force: argv.includes("--force"),
|
||||
};
|
||||
};
|
||||
|
||||
const tryReadGatewayTokenFromOpenclawCli = () => {
|
||||
try {
|
||||
const raw = execFileSync("openclaw", ["config", "get", "gateway.auth.token"], {
|
||||
encoding: "utf8",
|
||||
stdio: ["ignore", "pipe", "ignore"],
|
||||
});
|
||||
const token = String(raw ?? "").trim();
|
||||
return token || null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
async function main() {
|
||||
const args = parseArgs(process.argv.slice(2));
|
||||
|
||||
const settingsPath = resolveStudioSettingsPath(process.env);
|
||||
const settingsDir = path.dirname(settingsPath);
|
||||
|
||||
if (fs.existsSync(settingsPath) && !args.force) {
|
||||
console.error(
|
||||
`Studio settings already exist at ${settingsPath}. Re-run with --force to overwrite.`
|
||||
);
|
||||
process.exitCode = 1;
|
||||
return;
|
||||
}
|
||||
|
||||
const rl = readline.createInterface({
|
||||
input: process.stdin,
|
||||
output: process.stdout,
|
||||
});
|
||||
|
||||
try {
|
||||
const urlAnswer = await rl.question(
|
||||
`Upstream Gateway URL [${DEFAULT_GATEWAY_URL}]: `
|
||||
);
|
||||
const gatewayUrl = (urlAnswer || DEFAULT_GATEWAY_URL).trim();
|
||||
if (!gatewayUrl) {
|
||||
throw new Error("Gateway URL is required.");
|
||||
}
|
||||
|
||||
const tokenDefault = tryReadGatewayTokenFromOpenclawCli();
|
||||
const tokenPrompt = tokenDefault
|
||||
? "Upstream Gateway Token [detected from openclaw]: "
|
||||
: "Upstream Gateway Token: ";
|
||||
const tokenAnswer = await rl.question(tokenPrompt);
|
||||
const token = (tokenAnswer || tokenDefault || "").trim();
|
||||
if (!token) {
|
||||
throw new Error(
|
||||
"Gateway token is required. Provide it, or install/openclaw so it can be auto-detected."
|
||||
);
|
||||
}
|
||||
|
||||
fs.mkdirSync(settingsDir, { recursive: true });
|
||||
const next = {
|
||||
version: 1,
|
||||
gateway: {
|
||||
url: gatewayUrl,
|
||||
token,
|
||||
},
|
||||
};
|
||||
fs.writeFileSync(settingsPath, JSON.stringify(next, null, 2), "utf8");
|
||||
|
||||
console.info(`Wrote Studio settings to ${settingsPath}.`);
|
||||
} finally {
|
||||
rl.close();
|
||||
}
|
||||
}
|
||||
|
||||
main().catch((err) => {
|
||||
const msg = err instanceof Error ? err.message : String(err);
|
||||
console.error(msg);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const repoRoot = process.cwd();
|
||||
const requestedSourcePath =
|
||||
process.argv[2]?.trim() ||
|
||||
process.env.OPENCLAW_GATEWAY_CLIENT_SOURCE?.trim() ||
|
||||
process.env.OPENCLAW_UI_PATH?.trim() ||
|
||||
"";
|
||||
const sourcePath = requestedSourcePath
|
||||
? path.resolve(requestedSourcePath)
|
||||
: "";
|
||||
const destPath = path.join(
|
||||
repoRoot,
|
||||
"src",
|
||||
"lib",
|
||||
"gateway",
|
||||
"openclaw",
|
||||
"GatewayBrowserClient.ts"
|
||||
);
|
||||
|
||||
if (!sourcePath) {
|
||||
console.error(
|
||||
"Missing upstream gateway client source path. Provide it as `npm run sync:gateway-client -- /path/to/gateway.ts` or set OPENCLAW_GATEWAY_CLIENT_SOURCE."
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
console.error(`Missing upstream gateway client at ${sourcePath}.`);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
let contents = fs.readFileSync(sourcePath, "utf8");
|
||||
contents = contents
|
||||
.replace(
|
||||
/from "\.\.\/\.\.\/\.\.\/src\/gateway\/protocol\/client-info\.js";/g,
|
||||
'from "./client-info";'
|
||||
)
|
||||
.replace(
|
||||
/from "\.\.\/\.\.\/\.\.\/src\/gateway\/device-auth\.js";/g,
|
||||
'from "./device-auth-payload";'
|
||||
);
|
||||
|
||||
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
||||
fs.writeFileSync(destPath, contents, "utf8");
|
||||
console.log(`Synced gateway client to ${destPath}.`);
|
||||
Reference in New Issue
Block a user