feat: add hwlab cli and web skeleton

This commit is contained in:
HWLAB Code Queue
2026-05-21 14:44:09 +00:00
parent 77f70ff4ad
commit 6509a35804
15 changed files with 648 additions and 3 deletions
@@ -0,0 +1,8 @@
# HWLAB L6 MVP Runtime Plan
This evidence fixture is a mock artifact used by the CLI and cloud web skeleton.
- Environment: dev
- Endpoint: http://74.48.78.17:6667
- Scope: browser/CLI/gateway through cloud-web/cloud-api
- Status: dry-run only
+85
View File
@@ -0,0 +1,85 @@
{
"project": {
"projectId": "proj_mvp-l6",
"name": "HWLAB L6 MVP",
"description": "Mock project for the HWLAB CLI and cloud web skeleton.",
"status": "active",
"environment": "dev",
"labels": {
"track": "mvp",
"surface": "cli-web"
},
"createdBy": "service_hwlab-cli",
"createdAt": "2026-05-21T00:00:00.000Z",
"updatedAt": "2026-05-21T00:00:00.000Z"
},
"projects": [
{
"projectId": "proj_mvp-l6",
"name": "HWLAB L6 MVP",
"status": "active",
"environment": "dev",
"updatedAt": "2026-05-21T00:00:00.000Z"
},
{
"projectId": "proj_cloud-web",
"name": "Cloud Web Skeleton",
"status": "active",
"environment": "dev",
"updatedAt": "2026-05-20T12:00:00.000Z"
}
],
"serviceRoute": [
"browser/CLI/gateway",
"master hwlab-edge-proxy",
"frp",
"D601 hwlab-dev/hwlab-router",
"cloud-web/cloud-api"
],
"mvpSteps": [
"browser/CLI/gateway",
"master hwlab-edge-proxy",
"frp",
"D601 hwlab-dev/hwlab-router",
"cloud-web/cloud-api",
"hardware trusted closed loop",
"agent automation closed loop",
"evidence record",
"worker cleanup"
],
"endpoints": {
"dev": "http://74.48.78.17:6667"
},
"loops": [
{
"name": "hardware trusted closed loop",
"description": "Route a validated wiring config to the gateway and observe the corresponding hardware operation lifecycle."
},
{
"name": "agent automation closed loop",
"description": "Schedule agent and worker sessions, execute a scoped operation, and persist the resulting audit and evidence trail."
}
],
"evidence": [
{
"evidenceId": "evidence_mvp-runtime-plan",
"projectId": "proj_mvp-l6",
"operationId": "op_mvp-0001",
"kind": "report",
"uri": "fixtures/mvp/evidence/mvp-runtime-plan.md",
"sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"serviceId": "hwlab-cli",
"environment": "dev",
"createdAt": "2026-05-21T00:00:00.000Z",
"metadata": {
"source": "mock"
}
}
],
"cleanup": [
"release worker sessions",
"clear transient gateway state",
"archive evidence references",
"close the CLI-runner task"
]
}
+6 -1
View File
@@ -5,6 +5,11 @@
"type": "module",
"scripts": {
"validate": "node scripts/validate-contract.mjs",
"check": "node --check internal/protocol/index.mjs && node --check scripts/validate-contract.mjs && node scripts/validate-contract.mjs"
"check": "node --check internal/protocol/index.mjs && node --check scripts/validate-contract.mjs && node scripts/validate-contract.mjs",
"cli:health": "node tools/hwlab-cli/bin/hwlab-cli.mjs health",
"cli:dry-run": "node tools/hwlab-cli/bin/hwlab-cli.mjs test e2e --env dev --mvp --dry-run",
"cli:projects": "node tools/hwlab-cli/bin/hwlab-cli.mjs project list",
"web:check": "node web/hwlab-cloud-web/scripts/check.mjs",
"web:build": "node web/hwlab-cloud-web/scripts/build.mjs"
}
}
-1
View File
@@ -1 +0,0 @@
+192
View File
@@ -0,0 +1,192 @@
import fs from "node:fs";
import path from "node:path";
import { DEV_ENDPOINT } from "../../../internal/protocol/index.mjs";
const DEFAULT_PROJECTS = [
{
projectId: "proj_mvp-l6",
name: "HWLAB L6 MVP",
status: "active",
environment: "dev",
updatedAt: "2026-05-21T00:00:00.000Z"
},
{
projectId: "proj_cloud-web",
name: "Cloud Web Skeleton",
status: "active",
environment: "dev",
updatedAt: "2026-05-20T12:00:00.000Z"
}
];
function loadRuntimeFixture(cwd) {
const fixturePath = path.resolve(cwd, "fixtures/mvp/runtime.json");
if (!fs.existsSync(fixturePath)) {
return null;
}
const raw = fs.readFileSync(fixturePath, "utf8");
return JSON.parse(raw);
}
function resolveRuntime(cwd) {
const fixture = loadRuntimeFixture(cwd);
if (fixture) {
return fixture;
}
return {
endpoints: { dev: DEV_ENDPOINT },
projects: DEFAULT_PROJECTS,
mvpSteps: [
"browser/CLI/gateway",
"master hwlab-edge-proxy",
"frp",
"D601 hwlab-dev/hwlab-router",
"cloud-web/cloud-api",
"hardware trusted closed loop",
"agent automation closed loop",
"evidence record",
"worker cleanup"
],
loops: [
{ name: "hardware trusted closed loop" },
{ name: "agent automation closed loop" }
],
evidence: [],
cleanup: []
};
}
function formatJson(value) {
return JSON.stringify(value, null, 2);
}
function writeLine(stream, line = "") {
stream.write(`${line}\n`);
}
function writeProjectList(runtime, stdout) {
writeLine(stdout, "hwlab-cli project list");
writeLine(stdout, `endpoint: ${runtime.endpoints?.dev ?? DEV_ENDPOINT}`);
for (const project of runtime.projects ?? []) {
writeLine(
stdout,
`${project.projectId} | ${project.name} | ${project.status} | ${project.environment} | ${project.updatedAt}`
);
}
}
function writeHealth(runtime, stdout) {
writeLine(stdout, "hwlab-cli health");
writeLine(stdout, "status: ok");
writeLine(stdout, `environment: dev`);
writeLine(stdout, `endpoint: ${runtime.endpoints?.dev ?? DEV_ENDPOINT}`);
writeLine(stdout, `projects: ${(runtime.projects ?? []).length}`);
}
function writeDryRun(runtime, stdout) {
writeLine(stdout, "hwlab-cli test e2e --env dev --mvp --dry-run");
writeLine(stdout, `endpoint: ${runtime.endpoints?.dev ?? DEV_ENDPOINT}`);
writeLine(stdout, "mvp route:");
writeLine(
stdout,
` ${runtime.serviceRoute?.join(" -> ") ?? "browser/CLI/gateway -> master hwlab-edge-proxy -> frp -> D601 hwlab-dev/hwlab-router -> cloud-web/cloud-api"}`
);
for (const step of runtime.mvpSteps ?? []) {
writeLine(stdout, `- ${step}`);
}
writeLine(stdout, "closed loops:");
for (const loop of runtime.loops ?? []) {
writeLine(stdout, `- ${loop.name}${loop.description ? `: ${loop.description}` : ""}`);
}
writeLine(stdout, "evidence record:");
for (const evidence of runtime.evidence ?? []) {
writeLine(
stdout,
`- ${evidence.evidenceId} | ${evidence.kind} | ${evidence.serviceId} | ${evidence.uri}`
);
}
writeLine(stdout, "worker cleanup:");
for (const step of runtime.cleanup ?? []) {
writeLine(stdout, `- ${step}`);
}
writeLine(stdout, "dry-run only: no DEV/PROD changes were made");
}
function writeHelp(stdout) {
writeLine(stdout, "hwlab-cli");
writeLine(stdout, "commands:");
writeLine(stdout, " health");
writeLine(stdout, " project list");
writeLine(stdout, " test e2e --env dev --mvp --dry-run");
}
function parseArgs(args) {
const flags = new Set();
const options = new Map();
const rest = [];
for (let index = 0; index < args.length; index += 1) {
const arg = args[index];
if (arg.startsWith("--")) {
flags.add(arg);
const next = args[index + 1];
if (next && !next.startsWith("--")) {
options.set(arg, next);
index += 1;
}
} else {
rest.push(arg);
}
}
return { flags, options, rest };
}
export async function runCli(argv, io) {
const stdout = io.stdout ?? process.stdout;
const stderr = io.stderr ?? process.stderr;
const runtime = resolveRuntime(io.cwd ?? process.cwd());
const [command = "help", subcommand, ...rest] = argv;
const { flags, options } = parseArgs(rest);
if (command === "health") {
writeHealth(runtime, stdout);
return 0;
}
if (command === "project" && subcommand === "list") {
writeProjectList(runtime, stdout);
return 0;
}
if (command === "test" && subcommand === "e2e") {
const env = options.get("--env") ?? null;
const isDryRun = flags.has("--dry-run");
const isMvp = flags.has("--mvp");
if (env !== "dev") {
writeLine(stderr, "only --env dev is supported in the skeleton");
return 1;
}
if (!isMvp || !isDryRun) {
writeLine(stderr, "use --mvp --dry-run for the skeleton");
return 1;
}
writeDryRun(runtime, stdout);
return 0;
}
if (command === "project" && !subcommand) {
writeLine(stderr, "usage: hwlab-cli project list");
return 1;
}
if (command === "test" && !subcommand) {
writeLine(stderr, "usage: hwlab-cli test e2e --env dev --mvp --dry-run");
return 1;
}
writeHelp(stdout);
return 0;
}
export function formatRuntime(runtime) {
return formatJson(runtime);
}
+13
View File
@@ -0,0 +1,13 @@
{
"name": "hwlab-cli",
"version": "0.0.0",
"private": true,
"type": "module",
"bin": {
"hwlab-cli": "./bin/hwlab-cli.mjs"
},
"scripts": {
"check": "node --check bin/hwlab-cli.mjs && node --check lib/cli.mjs",
"health": "node bin/hwlab-cli.mjs health"
}
}
+1
View File
@@ -0,0 +1 @@
dist/
-1
View File
@@ -1 +0,0 @@
+40
View File
@@ -0,0 +1,40 @@
import { runtime } from "./runtime.mjs";
const runtimeGrid = document.querySelector("#runtime-grid");
const steps = document.querySelector("#mvp-steps");
const projectsBody = document.querySelector("#projects-body");
const status = document.querySelector("#status");
status.textContent = "dev skeleton";
const runtimePairs = [
["Endpoint", runtime.endpoints.dev],
["Route", runtime.serviceRoute.join(" -> ")],
["Closed loops", runtime.closedLoops.join(" / ")],
["Evidence", runtime.evidence[0].uri]
];
for (const [label, value] of runtimePairs) {
const dt = document.createElement("dt");
dt.textContent = label;
const dd = document.createElement("dd");
dd.textContent = value;
runtimeGrid.append(dt, dd);
}
for (const step of runtime.mvpSteps) {
const li = document.createElement("li");
li.textContent = step;
steps.append(li);
}
for (const project of runtime.projects) {
const row = document.createElement("tr");
row.innerHTML = `
<td>${project.projectId}<br /><strong>${project.name}</strong></td>
<td>${project.status}</td>
<td>${project.environment}</td>
<td>${project.updatedAt}</td>
`;
projectsBody.append(row);
}
+57
View File
@@ -0,0 +1,57 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>HWLAB Cloud Web</title>
<link rel="stylesheet" href="./styles.css" />
<script type="module" src="./app.mjs"></script>
</head>
<body>
<main class="shell">
<header class="topbar">
<div>
<p class="eyebrow">HWLAB</p>
<h1>Cloud Web</h1>
</div>
<div class="status" id="status">dev skeleton</div>
</header>
<section class="panel">
<div class="panel-head">
<h2>Runtime</h2>
<p>Mock dashboard for the L6 CLI and cloud-web surface.</p>
</div>
<dl class="grid" id="runtime-grid"></dl>
</section>
<section class="panel">
<div class="panel-head">
<h2>MVP route</h2>
<p>Fixed dry-run path shared with the CLI skeleton.</p>
</div>
<ol class="steps" id="mvp-steps"></ol>
</section>
<section class="panel">
<div class="panel-head">
<h2>Projects</h2>
<p>Static project list seeded from the mock fixture.</p>
</div>
<div class="table-wrap">
<table>
<thead>
<tr>
<th>Project</th>
<th>Status</th>
<th>Environment</th>
<th>Updated</th>
</tr>
</thead>
<tbody id="projects-body"></tbody>
</table>
</div>
</section>
</main>
</body>
</html>
+10
View File
@@ -0,0 +1,10 @@
{
"name": "hwlab-cloud-web",
"version": "0.0.0",
"private": true,
"type": "module",
"scripts": {
"check": "node scripts/check.mjs",
"build": "node scripts/build.mjs"
}
}
+51
View File
@@ -0,0 +1,51 @@
export const runtime = {
endpoints: {
dev: "http://74.48.78.17:6667"
},
serviceRoute: [
"browser/CLI/gateway",
"master hwlab-edge-proxy",
"frp",
"D601 hwlab-dev/hwlab-router",
"cloud-web/cloud-api"
],
mvpSteps: [
"browser/CLI/gateway",
"master hwlab-edge-proxy",
"frp",
"D601 hwlab-dev/hwlab-router",
"cloud-web/cloud-api",
"hardware trusted closed loop",
"agent automation closed loop",
"evidence record",
"worker cleanup"
],
closedLoops: [
"hardware trusted closed loop",
"agent automation closed loop"
],
projects: [
{
projectId: "proj_mvp-l6",
name: "HWLAB L6 MVP",
status: "active",
environment: "dev",
updatedAt: "2026-05-21T00:00:00.000Z"
},
{
projectId: "proj_cloud-web",
name: "Cloud Web Skeleton",
status: "active",
environment: "dev",
updatedAt: "2026-05-20T12:00:00.000Z"
}
],
evidence: [
{
evidenceId: "evidence_mvp-runtime-plan",
kind: "report",
uri: "fixtures/mvp/evidence/mvp-runtime-plan.md",
serviceId: "hwlab-cli"
}
]
};
+12
View File
@@ -0,0 +1,12 @@
import fs from "node:fs";
import path from "node:path";
const rootDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
const distDir = path.resolve(rootDir, "dist");
fs.mkdirSync(distDir, { recursive: true });
for (const file of ["index.html", "styles.css", "app.mjs", "runtime.mjs"]) {
fs.copyFileSync(path.resolve(rootDir, file), path.resolve(distDir, file));
}
console.log(`hwlab-cloud-web build ok: ${distDir}`);
+19
View File
@@ -0,0 +1,19 @@
import fs from "node:fs";
import path from "node:path";
const rootDir = path.resolve(path.dirname(new URL(import.meta.url).pathname), "..");
const requiredFiles = ["index.html", "styles.css", "app.mjs", "runtime.mjs"];
for (const file of requiredFiles) {
const filePath = path.resolve(rootDir, file);
if (!fs.existsSync(filePath)) {
throw new Error(`missing web asset: ${file}`);
}
}
const html = fs.readFileSync(path.resolve(rootDir, "index.html"), "utf8");
if (!html.includes("HWLAB Cloud Web")) {
throw new Error("index.html missing title");
}
console.log("hwlab-cloud-web check ok");
+154
View File
@@ -0,0 +1,154 @@
:root {
color-scheme: dark;
--bg: #0d1117;
--surface: #161b22;
--surface-2: #1f2630;
--text: #e6edf3;
--muted: #9aa4b2;
--accent: #58a6ff;
--border: #30363d;
--good: #3fb950;
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
min-height: 100%;
background: var(--bg);
color: var(--text);
font: 14px/1.5 system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
}
body {
padding: 24px;
}
.shell {
max-width: 1160px;
margin: 0 auto;
display: grid;
gap: 16px;
}
.topbar,
.panel {
background: var(--surface);
border: 1px solid var(--border);
}
.topbar {
display: flex;
justify-content: space-between;
gap: 16px;
align-items: center;
padding: 16px 18px;
}
.eyebrow {
margin: 0 0 4px;
color: var(--muted);
text-transform: uppercase;
letter-spacing: 0;
font-size: 12px;
}
h1,
h2,
p,
dl,
ol,
table {
margin: 0;
}
h1 {
font-size: 22px;
}
.status {
padding: 8px 12px;
border: 1px solid var(--border);
background: var(--surface-2);
color: var(--good);
}
.panel {
padding: 16px 18px;
}
.panel-head {
display: grid;
gap: 4px;
margin-bottom: 16px;
}
.panel-head p {
color: var(--muted);
}
.grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px 18px;
}
.grid dt {
color: var(--muted);
font-size: 12px;
}
.grid dd {
margin: 2px 0 0;
font-weight: 600;
word-break: break-word;
}
.steps {
display: grid;
gap: 8px;
padding-left: 20px;
}
.steps li {
padding-left: 8px;
}
.table-wrap {
overflow-x: auto;
}
table {
width: 100%;
border-collapse: collapse;
}
th,
td {
text-align: left;
padding: 10px 8px;
border-bottom: 1px solid var(--border);
}
th {
color: var(--muted);
font-weight: 500;
}
@media (max-width: 760px) {
body {
padding: 14px;
}
.topbar {
flex-direction: column;
align-items: flex-start;
}
.grid {
grid-template-columns: 1fr;
}
}