From 6509a35804ee97dd8d711bdde334c46ca37c55b9 Mon Sep 17 00:00:00 2001 From: HWLAB Code Queue Date: Thu, 21 May 2026 14:44:09 +0000 Subject: [PATCH] feat: add hwlab cli and web skeleton --- fixtures/mvp/evidence/mvp-runtime-plan.md | 8 + fixtures/mvp/runtime.json | 85 ++++++++++ package.json | 7 +- tools/hwlab-cli/.gitkeep | 1 - tools/hwlab-cli/lib/cli.mjs | 192 ++++++++++++++++++++++ tools/hwlab-cli/package.json | 13 ++ web/hwlab-cloud-web/.gitignore | 1 + web/hwlab-cloud-web/.gitkeep | 1 - web/hwlab-cloud-web/app.mjs | 40 +++++ web/hwlab-cloud-web/index.html | 57 +++++++ web/hwlab-cloud-web/package.json | 10 ++ web/hwlab-cloud-web/runtime.mjs | 51 ++++++ web/hwlab-cloud-web/scripts/build.mjs | 12 ++ web/hwlab-cloud-web/scripts/check.mjs | 19 +++ web/hwlab-cloud-web/styles.css | 154 +++++++++++++++++ 15 files changed, 648 insertions(+), 3 deletions(-) create mode 100644 fixtures/mvp/evidence/mvp-runtime-plan.md create mode 100644 fixtures/mvp/runtime.json delete mode 100644 tools/hwlab-cli/.gitkeep create mode 100644 tools/hwlab-cli/lib/cli.mjs create mode 100644 tools/hwlab-cli/package.json create mode 100644 web/hwlab-cloud-web/.gitignore delete mode 100644 web/hwlab-cloud-web/.gitkeep create mode 100644 web/hwlab-cloud-web/app.mjs create mode 100644 web/hwlab-cloud-web/index.html create mode 100644 web/hwlab-cloud-web/package.json create mode 100644 web/hwlab-cloud-web/runtime.mjs create mode 100644 web/hwlab-cloud-web/scripts/build.mjs create mode 100644 web/hwlab-cloud-web/scripts/check.mjs create mode 100644 web/hwlab-cloud-web/styles.css diff --git a/fixtures/mvp/evidence/mvp-runtime-plan.md b/fixtures/mvp/evidence/mvp-runtime-plan.md new file mode 100644 index 00000000..140aa9bf --- /dev/null +++ b/fixtures/mvp/evidence/mvp-runtime-plan.md @@ -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 diff --git a/fixtures/mvp/runtime.json b/fixtures/mvp/runtime.json new file mode 100644 index 00000000..46992bd6 --- /dev/null +++ b/fixtures/mvp/runtime.json @@ -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" + ] +} diff --git a/package.json b/package.json index 5aa4d508..ad2dad1c 100644 --- a/package.json +++ b/package.json @@ -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" } } diff --git a/tools/hwlab-cli/.gitkeep b/tools/hwlab-cli/.gitkeep deleted file mode 100644 index 8b137891..00000000 --- a/tools/hwlab-cli/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/tools/hwlab-cli/lib/cli.mjs b/tools/hwlab-cli/lib/cli.mjs new file mode 100644 index 00000000..e65ec55a --- /dev/null +++ b/tools/hwlab-cli/lib/cli.mjs @@ -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); +} diff --git a/tools/hwlab-cli/package.json b/tools/hwlab-cli/package.json new file mode 100644 index 00000000..9fde11f8 --- /dev/null +++ b/tools/hwlab-cli/package.json @@ -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" + } +} diff --git a/web/hwlab-cloud-web/.gitignore b/web/hwlab-cloud-web/.gitignore new file mode 100644 index 00000000..849ddff3 --- /dev/null +++ b/web/hwlab-cloud-web/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/web/hwlab-cloud-web/.gitkeep b/web/hwlab-cloud-web/.gitkeep deleted file mode 100644 index 8b137891..00000000 --- a/web/hwlab-cloud-web/.gitkeep +++ /dev/null @@ -1 +0,0 @@ - diff --git a/web/hwlab-cloud-web/app.mjs b/web/hwlab-cloud-web/app.mjs new file mode 100644 index 00000000..01021424 --- /dev/null +++ b/web/hwlab-cloud-web/app.mjs @@ -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 = ` + ${project.projectId}
${project.name} + ${project.status} + ${project.environment} + ${project.updatedAt} + `; + projectsBody.append(row); +} diff --git a/web/hwlab-cloud-web/index.html b/web/hwlab-cloud-web/index.html new file mode 100644 index 00000000..6f04d800 --- /dev/null +++ b/web/hwlab-cloud-web/index.html @@ -0,0 +1,57 @@ + + + + + + HWLAB Cloud Web + + + + +
+
+
+

HWLAB

+

Cloud Web

+
+
dev skeleton
+
+ +
+
+

Runtime

+

Mock dashboard for the L6 CLI and cloud-web surface.

+
+
+
+ +
+
+

MVP route

+

Fixed dry-run path shared with the CLI skeleton.

+
+
    +
    + +
    +
    +

    Projects

    +

    Static project list seeded from the mock fixture.

    +
    +
    + + + + + + + + + + +
    ProjectStatusEnvironmentUpdated
    +
    +
    +
    + + diff --git a/web/hwlab-cloud-web/package.json b/web/hwlab-cloud-web/package.json new file mode 100644 index 00000000..2871c36c --- /dev/null +++ b/web/hwlab-cloud-web/package.json @@ -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" + } +} diff --git a/web/hwlab-cloud-web/runtime.mjs b/web/hwlab-cloud-web/runtime.mjs new file mode 100644 index 00000000..f56e840b --- /dev/null +++ b/web/hwlab-cloud-web/runtime.mjs @@ -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" + } + ] +}; diff --git a/web/hwlab-cloud-web/scripts/build.mjs b/web/hwlab-cloud-web/scripts/build.mjs new file mode 100644 index 00000000..8d8a057d --- /dev/null +++ b/web/hwlab-cloud-web/scripts/build.mjs @@ -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}`); diff --git a/web/hwlab-cloud-web/scripts/check.mjs b/web/hwlab-cloud-web/scripts/check.mjs new file mode 100644 index 00000000..be93ed4f --- /dev/null +++ b/web/hwlab-cloud-web/scripts/check.mjs @@ -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"); diff --git a/web/hwlab-cloud-web/styles.css b/web/hwlab-cloud-web/styles.css new file mode 100644 index 00000000..1f8a2437 --- /dev/null +++ b/web/hwlab-cloud-web/styles.css @@ -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; + } +}