mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-25 03:04:29 -04:00
feat(config-builder): scaffold Vite Lit schema spike
This commit is contained in:
@@ -10,6 +10,27 @@ Use the same front-end stack as the existing OpenClaw web UI (`ui/`):
|
||||
- Lit
|
||||
- Plain CSS (no Next.js/Tailwind)
|
||||
|
||||
## Planning
|
||||
## Current status
|
||||
|
||||
Phase 0 spike is in place:
|
||||
|
||||
- app boots with Vite + Lit
|
||||
- `OpenClawSchema.toJSONSchema()` runs in browser bundle
|
||||
- `buildConfigSchema()` UI hints load in browser bundle
|
||||
|
||||
To run locally:
|
||||
|
||||
```bash
|
||||
pnpm --filter @openclaw/config-builder dev
|
||||
```
|
||||
|
||||
## Notes
|
||||
|
||||
Implementation details are tracked in `.local/config-builder-spec.md`.
|
||||
|
||||
For the spike, Vite aliases lightweight browser shims for:
|
||||
|
||||
- `src/version.ts`
|
||||
- `src/channels/registry.ts`
|
||||
|
||||
This keeps schema imports browser-safe while preserving the existing Node runtime modules.
|
||||
|
||||
12
apps/config-builder/index.html
Normal file
12
apps/config-builder/index.html
Normal file
@@ -0,0 +1,12 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>OpenClaw Config Builder</title>
|
||||
</head>
|
||||
<body>
|
||||
<config-builder-app></config-builder-app>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
20
apps/config-builder/package.json
Normal file
20
apps/config-builder/package.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "@openclaw/config-builder",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"build": "vite build",
|
||||
"dev": "vite",
|
||||
"preview": "vite preview",
|
||||
"test": "vitest run --config vitest.config.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"json5": "^2.2.3",
|
||||
"lit": "^3.3.2",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"vite": "7.3.1",
|
||||
"vitest": "4.0.18"
|
||||
}
|
||||
}
|
||||
32
apps/config-builder/src/lib/schema-spike.ts
Normal file
32
apps/config-builder/src/lib/schema-spike.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { buildConfigSchema } from "@openclaw/config/schema.ts";
|
||||
import { OpenClawSchema } from "@openclaw/config/zod-schema.ts";
|
||||
|
||||
type JsonSchemaRoot = {
|
||||
properties?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type SchemaSpikeSummary = {
|
||||
schemaRootProperties: number;
|
||||
schemaTopSections: string[];
|
||||
uiHintCount: number;
|
||||
generatedAt: string;
|
||||
version: string;
|
||||
};
|
||||
|
||||
export function runSchemaSpike(): SchemaSpikeSummary {
|
||||
const schema = OpenClawSchema.toJSONSchema({
|
||||
target: "draft-07",
|
||||
unrepresentable: "any",
|
||||
}) as JsonSchemaRoot;
|
||||
const topLevelProps = Object.keys(schema.properties ?? {});
|
||||
|
||||
const configSchema = buildConfigSchema();
|
||||
|
||||
return {
|
||||
schemaRootProperties: topLevelProps.length,
|
||||
schemaTopSections: topLevelProps.slice(0, 10),
|
||||
uiHintCount: Object.keys(configSchema.uiHints).length,
|
||||
generatedAt: configSchema.generatedAt,
|
||||
version: configSchema.version,
|
||||
};
|
||||
}
|
||||
2
apps/config-builder/src/main.ts
Normal file
2
apps/config-builder/src/main.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
import "./styles.css";
|
||||
import "./ui/app.ts";
|
||||
10
apps/config-builder/src/shims/channel-registry.ts
Normal file
10
apps/config-builder/src/shims/channel-registry.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
// Browser-safe channel ID shim for config-builder schema imports.
|
||||
export const CHANNEL_IDS = [
|
||||
"telegram",
|
||||
"whatsapp",
|
||||
"discord",
|
||||
"googlechat",
|
||||
"slack",
|
||||
"signal",
|
||||
"imessage",
|
||||
] as const;
|
||||
3
apps/config-builder/src/shims/version.ts
Normal file
3
apps/config-builder/src/shims/version.ts
Normal file
@@ -0,0 +1,3 @@
|
||||
// Browser-safe version shim for config-builder schema imports.
|
||||
// Real gateway/runtime paths can still use src/version.ts.
|
||||
export const VERSION = "dev";
|
||||
21
apps/config-builder/src/styles.css
Normal file
21
apps/config-builder/src/styles.css
Normal file
@@ -0,0 +1,21 @@
|
||||
:root {
|
||||
color-scheme: dark;
|
||||
font-family: Inter, "Segoe UI", Roboto, sans-serif;
|
||||
background: #09090b;
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
margin: 0;
|
||||
min-height: 100%;
|
||||
background: #09090b;
|
||||
}
|
||||
|
||||
body {
|
||||
min-height: 100vh;
|
||||
}
|
||||
164
apps/config-builder/src/ui/app.ts
Normal file
164
apps/config-builder/src/ui/app.ts
Normal file
@@ -0,0 +1,164 @@
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { runSchemaSpike, type SchemaSpikeSummary } from "../lib/schema-spike.ts";
|
||||
|
||||
type AppState =
|
||||
| { status: "loading" }
|
||||
| { status: "ready"; summary: SchemaSpikeSummary }
|
||||
| { status: "error"; message: string };
|
||||
|
||||
class ConfigBuilderApp extends LitElement {
|
||||
static override styles = css`
|
||||
:host {
|
||||
display: block;
|
||||
min-height: 100vh;
|
||||
background: #09090b;
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.shell {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
padding: 40px 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.4rem;
|
||||
font-weight: 700;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
margin-top: 10px;
|
||||
color: #a1a1aa;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.card {
|
||||
margin-top: 20px;
|
||||
border: 1px solid #27272a;
|
||||
border-radius: 12px;
|
||||
background: #18181b;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.row {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #27272a;
|
||||
font-size: 0.92rem;
|
||||
}
|
||||
|
||||
.row:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #a1a1aa;
|
||||
}
|
||||
|
||||
.value {
|
||||
font-family: "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
|
||||
monospace;
|
||||
text-align: right;
|
||||
color: #fafafa;
|
||||
}
|
||||
|
||||
.ok {
|
||||
color: #4ade80;
|
||||
}
|
||||
|
||||
.error {
|
||||
color: #f87171;
|
||||
white-space: pre-wrap;
|
||||
font-family: "SFMono-Regular", Menlo, Monaco, Consolas, "Liberation Mono", "Courier New",
|
||||
monospace;
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.hint {
|
||||
margin-top: 16px;
|
||||
color: #71717a;
|
||||
font-size: 0.85rem;
|
||||
}
|
||||
`;
|
||||
|
||||
private state: AppState = { status: "loading" };
|
||||
|
||||
override connectedCallback(): void {
|
||||
super.connectedCallback();
|
||||
this.bootstrap();
|
||||
}
|
||||
|
||||
private bootstrap(): void {
|
||||
try {
|
||||
const summary = runSchemaSpike();
|
||||
this.state = { status: "ready", summary };
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.stack ?? error.message : String(error);
|
||||
this.state = { status: "error", message };
|
||||
}
|
||||
this.requestUpdate();
|
||||
}
|
||||
|
||||
override render() {
|
||||
return html`<main class="shell">
|
||||
<h1 class="title">OpenClaw Config Builder</h1>
|
||||
<div class="subtitle">Phase 0 spike: schema imports + browser runtime checks.</div>
|
||||
|
||||
${this.renderBody()}
|
||||
|
||||
<div class="hint">
|
||||
Next: replace this spike page with Explorer read-only group rendering.
|
||||
</div>
|
||||
</main>`;
|
||||
}
|
||||
|
||||
private renderBody() {
|
||||
if (this.state.status === "loading") {
|
||||
return html`<section class="card">Loading schema spike…</section>`;
|
||||
}
|
||||
|
||||
if (this.state.status === "error") {
|
||||
return html`<section class="card">
|
||||
<div class="row">
|
||||
<span class="label">Schema import status</span>
|
||||
<span class="value error">failed</span>
|
||||
</div>
|
||||
<pre class="error">${this.state.message}</pre>
|
||||
</section>`;
|
||||
}
|
||||
|
||||
const { summary } = this.state;
|
||||
return html`<section class="card">
|
||||
<div class="row">
|
||||
<span class="label">Schema import status</span>
|
||||
<span class="value ok">ok</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">Top-level schema sections</span>
|
||||
<span class="value">${summary.schemaRootProperties}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">UI hint entries</span>
|
||||
<span class="value">${summary.uiHintCount}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">Schema version</span>
|
||||
<span class="value">${summary.version}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">Generated at</span>
|
||||
<span class="value">${summary.generatedAt}</span>
|
||||
</div>
|
||||
<div class="row">
|
||||
<span class="label">Sample sections</span>
|
||||
<span class="value">${summary.schemaTopSections.join(", ") || "(none)"}</span>
|
||||
</div>
|
||||
</section>`;
|
||||
}
|
||||
}
|
||||
|
||||
customElements.define("config-builder-app", ConfigBuilderApp);
|
||||
11
apps/config-builder/tsconfig.json
Normal file
11
apps/config-builder/tsconfig.json
Normal file
@@ -0,0 +1,11 @@
|
||||
{
|
||||
"extends": "../../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"noEmit": true,
|
||||
"types": ["vite/client"],
|
||||
"paths": {
|
||||
"@openclaw/config/*": ["../../src/config/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src/**/*", "vite.config.ts"]
|
||||
}
|
||||
60
apps/config-builder/vite.config.ts
Normal file
60
apps/config-builder/vite.config.ts
Normal file
@@ -0,0 +1,60 @@
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { defineConfig } from "vite";
|
||||
|
||||
const here = path.dirname(fileURLToPath(import.meta.url));
|
||||
const repoRoot = path.resolve(here, "../..");
|
||||
|
||||
function normalizeBase(input: string): string {
|
||||
const trimmed = input.trim();
|
||||
if (!trimmed) {
|
||||
return "/";
|
||||
}
|
||||
if (trimmed === "./") {
|
||||
return "./";
|
||||
}
|
||||
if (trimmed.endsWith("/")) {
|
||||
return trimmed;
|
||||
}
|
||||
return `${trimmed}/`;
|
||||
}
|
||||
|
||||
export default defineConfig(() => {
|
||||
const envBase = process.env.OPENCLAW_CONFIG_BUILDER_BASE_PATH?.trim();
|
||||
const base = envBase ? normalizeBase(envBase) : "./";
|
||||
return {
|
||||
base,
|
||||
publicDir: path.resolve(here, "public"),
|
||||
resolve: {
|
||||
alias: [
|
||||
{
|
||||
find: "@openclaw/config",
|
||||
replacement: path.resolve(repoRoot, "src/config"),
|
||||
},
|
||||
{
|
||||
// src/config/schema.ts imports ../version.js; redirect to a browser-safe shim.
|
||||
find: "../version.js",
|
||||
replacement: path.resolve(here, "src/shims/version.ts"),
|
||||
},
|
||||
{
|
||||
// src/config/schema.ts imports ../channels/registry.js; redirect to a light shim.
|
||||
find: "../channels/registry.js",
|
||||
replacement: path.resolve(here, "src/shims/channel-registry.ts"),
|
||||
},
|
||||
],
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ["lit"],
|
||||
},
|
||||
build: {
|
||||
outDir: path.resolve(here, "../../dist/config-builder"),
|
||||
emptyOutDir: true,
|
||||
sourcemap: true,
|
||||
},
|
||||
server: {
|
||||
host: true,
|
||||
port: 5174,
|
||||
strictPort: true,
|
||||
},
|
||||
};
|
||||
});
|
||||
8
apps/config-builder/vitest.config.ts
Normal file
8
apps/config-builder/vitest.config.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from "vitest/config";
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
environment: "node",
|
||||
include: ["src/**/*.test.ts"],
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user