diff --git a/docs/app/(home)/Openclaw-OS/page.tsx b/docs/app/(home)/openclaw-os/page.tsx similarity index 98% rename from docs/app/(home)/Openclaw-OS/page.tsx rename to docs/app/(home)/openclaw-os/page.tsx index 6bb3f117b..9819b9e5e 100644 --- a/docs/app/(home)/Openclaw-OS/page.tsx +++ b/docs/app/(home)/openclaw-os/page.tsx @@ -32,7 +32,7 @@ export default function OpenClawOSPage() { compact showBanner={false} showPlaygroundButton={false} - desktopPreviewImage="/OpenclawOS-hero.png" + desktopPreviewImage="/OpenclawOS-hero1.png" desktopPreviewImageAlt="OpenClaw OS desktop preview" desktopPreviewImageWidth={3200} desktopPreviewImageHeight={1036} diff --git a/docs/components/site-primary-nav.tsx b/docs/components/site-primary-nav.tsx index fb8621cdb..bda8515a5 100644 --- a/docs/components/site-primary-nav.tsx +++ b/docs/components/site-primary-nav.tsx @@ -9,7 +9,7 @@ export const PRIMARY_SITE_NAV_ITEMS = [ { title: "Playground", href: "/playground", newTab: false }, { title: "Demo", href: "/demo/github", newTab: true }, { title: "Blogs", href: "/blog", newTab: false }, - { title: "OpenClaw OS", href: "/Openclaw-OS", newTab: false, badge: "New" }, + { title: "OpenClaw OS", href: "/openclaw-os", newTab: false, badge: "New" }, ] as const; export function SitePrimaryNav() { diff --git a/docs/generated/chat-system-prompt.txt b/docs/generated/chat-system-prompt.txt index af8c67770..e98309971 100644 --- a/docs/generated/chat-system-prompt.txt +++ b/docs/generated/chat-system-prompt.txt @@ -31,7 +31,7 @@ Separator(orientation?: "horizontal" | "vertical", decorative?: boolean) — Vis ### Tables Table(columns: Col[]) — Data table — column-oriented. Each Col holds its own data array. -Col(label: string, data, type?: "string" | "number" | "action") — Column definition — holds label + data array +Col(label: string, data: any, type?: "string" | "number" | "action") — Column definition — holds label + data array ### Charts (2D) BarChart(labels: string[], series: Series[], variant?: "grouped" | "stacked", xLabel?: string, yLabel?: string) — Vertical bars; use for comparing values across categories with one or more series @@ -53,20 +53,20 @@ ScatterSeries(name: string, points: Point[]) — Named dataset Point(x: number, y: number, z?: number) — Data point with numeric coordinates ### Forms -Form(name: string, buttons: Buttons, fields) — Form container with fields and explicit action buttons +Form(name: string, buttons: Buttons, fields?: FormControl[]) — Form container with fields and explicit action buttons FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup, hint?: string) — Field with label, input component, and optional hint text Label(text: string) — Text label Input(name: string, placeholder?: string, type?: "text" | "email" | "password" | "number" | "url", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) TextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) SelectItem(value: string, label: string) — Option for Select -DatePicker(name: string, mode?: "single" | "range", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?) +DatePicker(name: string, mode?: "single" | "range", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) Slider(name: string, variant: "continuous" | "discrete", min: number, max: number, step?: number, defaultValue?: number[], label?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) — Numeric slider input; supports continuous and discrete (stepped) variants -CheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?) +CheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding>) CheckBoxItem(label: string, description: string, name: string, defaultChecked?: boolean) RadioGroup(name: string, items: RadioItem[], defaultValue?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) RadioItem(label: string, description: string, value: string) -SwitchGroup(name: string, items: SwitchItem[], variant?: "clear" | "card" | "sunk", value?) — Group of switch toggles +SwitchGroup(name: string, items: SwitchItem[], variant?: "clear" | "card" | "sunk", value?: $binding>) — Group of switch toggles SwitchItem(label?: string, description?: string, name: string, defaultChecked?: boolean) — Individual switch toggle - Define EACH FormControl as its own reference — do NOT inline all controls in one array. - NEVER nest Form inside Form. diff --git a/docs/generated/playground-component-spec.json b/docs/generated/playground-component-spec.json index d36586a0b..a1001e93c 100644 --- a/docs/generated/playground-component-spec.json +++ b/docs/generated/playground-component-spec.json @@ -46,7 +46,7 @@ "description": "Data table — column-oriented. Each Col holds its own data array." }, "Col": { - "signature": "Col(label: string, data, type?: \"string\" | \"number\" | \"action\")", + "signature": "Col(label: string, data: any, type?: \"string\" | \"number\" | \"action\")", "description": "Column definition — holds label + data array" }, "BarChart": { @@ -102,7 +102,7 @@ "description": "Data point with numeric coordinates" }, "Form": { - "signature": "Form(name: string, buttons: Buttons, fields)", + "signature": "Form(name: string, buttons: Buttons, fields?: FormControl[])", "description": "Form container with fields and explicit action buttons" }, "FormControl": { @@ -130,7 +130,7 @@ "description": "Option for Select" }, "DatePicker": { - "signature": "DatePicker(name: string, mode?: \"single\" | \"range\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?)", + "signature": "DatePicker(name: string, mode?: \"single\" | \"range\", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding)", "description": "" }, "Slider": { @@ -138,7 +138,7 @@ "description": "Numeric slider input; supports continuous and discrete (stepped) variants" }, "CheckBoxGroup": { - "signature": "CheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?)", + "signature": "CheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding>)", "description": "" }, "CheckBoxItem": { @@ -154,7 +154,7 @@ "description": "" }, "SwitchGroup": { - "signature": "SwitchGroup(name: string, items: SwitchItem[], variant?: \"clear\" | \"card\" | \"sunk\", value?)", + "signature": "SwitchGroup(name: string, items: SwitchItem[], variant?: \"clear\" | \"card\" | \"sunk\", value?: $binding>)", "description": "Group of switch toggles" }, "SwitchItem": { @@ -170,7 +170,7 @@ "description": "Group of Button components. direction: \"row\" (default) | \"column\"." }, "Stack": { - "signature": "Stack([children], direction?: \"row\" | \"column\", gap?: \"none\" | \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"2xl\", align?: \"start\" | \"center\" | \"end\" | \"stretch\" | \"baseline\", justify?: \"start\" | \"center\" | \"end\" | \"between\" | \"around\" | \"evenly\", wrap?: boolean)", + "signature": "Stack(children: any[], direction?: \"row\" | \"column\", gap?: \"none\" | \"xs\" | \"s\" | \"m\" | \"l\" | \"xl\" | \"2xl\", align?: \"start\" | \"center\" | \"end\" | \"stretch\" | \"baseline\", justify?: \"start\" | \"center\" | \"end\" | \"between\" | \"around\" | \"evenly\", wrap?: boolean)", "description": "Flex container. direction: \"row\"|\"column\" (default \"column\"). gap: \"none\"|\"xs\"|\"s\"|\"m\"|\"l\"|\"xl\"|\"2xl\" (default \"m\"). align: \"start\"|\"center\"|\"end\"|\"stretch\"|\"baseline\". justify: \"start\"|\"center\"|\"end\"|\"between\"|\"around\"|\"evenly\"." }, "Tabs": { @@ -265,7 +265,10 @@ }, { "name": "Tables", - "components": ["Table", "Col"], + "components": [ + "Table", + "Col" + ], "notes": [ "- Table is COLUMN-oriented: Table([Col(\"Label\", dataArray), Col(\"Count\", countArray, \"number\")]). Use array pluck for data: data.rows.fieldName", "- Col data can be component arrays for styled cells: Col(\"Status\", @Each(data.rows, \"item\", Tag(item.status, null, \"sm\", item.status == \"open\" ? \"success\" : \"danger\")))", @@ -295,7 +298,12 @@ }, { "name": "Charts (1D)", - "components": ["PieChart", "RadialChart", "SingleStackedBarChart", "Slice"], + "components": [ + "PieChart", + "RadialChart", + "SingleStackedBarChart", + "Slice" + ], "notes": [ "- PieChart and BarChart need NUMBERS, not objects. For list data, use @Count(@Filter(...)) to aggregate:", "- PieChart from list: `PieChart([\"Low\", \"Med\", \"High\"], [@Count(@Filter(data.rows, \"priority\", \"==\", \"low\")), @Count(@Filter(data.rows, \"priority\", \"==\", \"medium\")), @Count(@Filter(data.rows, \"priority\", \"==\", \"high\"))], \"donut\")`", @@ -304,7 +312,11 @@ }, { "name": "Charts (Scatter)", - "components": ["ScatterChart", "ScatterSeries", "Point"] + "components": [ + "ScatterChart", + "ScatterSeries", + "Point" + ] }, { "name": "Forms", @@ -338,14 +350,20 @@ }, { "name": "Buttons", - "components": ["Button", "Buttons"], + "components": [ + "Button", + "Buttons" + ], "notes": [ "- Toggle in @Each: @Each(rows, \"t\", Button(t.status == \"open\" ? \"Close\" : \"Reopen\", Action([...])))" ] }, { "name": "Data Display", - "components": ["TagBlock", "Tag"], + "components": [ + "TagBlock", + "Tag" + ], "notes": [ "- Color-mapped Tag: Tag(value, null, \"sm\", value == \"high\" ? \"danger\" : value == \"medium\" ? \"warning\" : \"neutral\")" ] diff --git a/docs/generated/playground-system-prompt.txt b/docs/generated/playground-system-prompt.txt index ba91045c0..9124f44a6 100644 --- a/docs/generated/playground-system-prompt.txt +++ b/docs/generated/playground-system-prompt.txt @@ -18,7 +18,7 @@ Props typed `ActionExpression` accept an Action([@steps...]) expression. See the Props marked `$binding` accept a `$variable` reference for two-way binding. ### Layout -Stack([children], direction?: "row" | "column", gap?: "none" | "xs" | "s" | "m" | "l" | "xl" | "2xl", align?: "start" | "center" | "end" | "stretch" | "baseline", justify?: "start" | "center" | "end" | "between" | "around" | "evenly", wrap?: boolean) — Flex container. direction: "row"|"column" (default "column"). gap: "none"|"xs"|"s"|"m"|"l"|"xl"|"2xl" (default "m"). align: "start"|"center"|"end"|"stretch"|"baseline". justify: "start"|"center"|"end"|"between"|"around"|"evenly". +Stack(children: any[], direction?: "row" | "column", gap?: "none" | "xs" | "s" | "m" | "l" | "xl" | "2xl", align?: "start" | "center" | "end" | "stretch" | "baseline", justify?: "start" | "center" | "end" | "between" | "around" | "evenly", wrap?: boolean) — Flex container. direction: "row"|"column" (default "column"). gap: "none"|"xs"|"s"|"m"|"l"|"xl"|"2xl" (default "m"). align: "start"|"center"|"end"|"stretch"|"baseline". justify: "start"|"center"|"end"|"between"|"around"|"evenly". Tabs(items: TabItem[]) — Tabbed container TabItem(value: string, trigger: string, content: (TextContent | MarkDownRenderer | CardHeader | Callout | TextCallout | CodeBlock | Image | ImageBlock | ImageGallery | Separator | HorizontalBarChart | RadarChart | PieChart | RadialChart | SingleStackedBarChart | ScatterChart | AreaChart | BarChart | LineChart | Table | TagBlock | Form | Buttons | Steps)[]) — value is unique id, trigger is tab label, content is array of components Accordion(items: AccordionItem[]) — Collapsible sections @@ -53,7 +53,7 @@ CodeBlock(language: string, codeString: string) — Syntax-highlighted code bloc ### Tables Table(columns: Col[]) — Data table — column-oriented. Each Col holds its own data array. -Col(label: string, data, type?: "string" | "number" | "action") — Column definition — holds label + data array +Col(label: string, data: any, type?: "string" | "number" | "action") — Column definition — holds label + data array - Table is COLUMN-oriented: Table([Col("Label", dataArray), Col("Count", countArray, "number")]). Use array pluck for data: data.rows.fieldName - Col data can be component arrays for styled cells: Col("Status", @Each(data.rows, "item", Tag(item.status, null, "sm", item.status == "open" ? "success" : "danger"))) - Row actions: Col("Actions", @Each(data.rows, "t", Button("Edit", Action([@Set($showEdit, true), @Set($editId, t.id)])))) @@ -89,20 +89,20 @@ ScatterSeries(name: string, points: Point[]) — Named dataset Point(x: number, y: number, z?: number) — Data point with numeric coordinates ### Forms -Form(name: string, buttons: Buttons, fields) — Form container with fields and explicit action buttons +Form(name: string, buttons: Buttons, fields?: FormControl[]) — Form container with fields and explicit action buttons FormControl(label: string, input: Input | TextArea | Select | DatePicker | Slider | CheckBoxGroup | RadioGroup, hint?: string) — Field with label, input component, and optional hint text Label(text: string) — Text label Input(name: string, placeholder?: string, type?: "text" | "email" | "password" | "number" | "url", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) TextArea(name: string, placeholder?: string, rows?: number, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) Select(name: string, items: SelectItem[], placeholder?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) SelectItem(value: string, label: string) — Option for Select -DatePicker(name: string, mode?: "single" | "range", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?) +DatePicker(name: string, mode?: "single" | "range", rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) Slider(name: string, variant: "continuous" | "discrete", min: number, max: number, step?: number, defaultValue?: number[], label?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) — Numeric slider input; supports continuous and discrete (stepped) variants -CheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?) +CheckBoxGroup(name: string, items: CheckBoxItem[], rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding>) CheckBoxItem(label: string, description: string, name: string, defaultChecked?: boolean) RadioGroup(name: string, items: RadioItem[], defaultValue?: string, rules?: {required?: boolean, email?: boolean, url?: boolean, numeric?: boolean, min?: number, max?: number, minLength?: number, maxLength?: number, pattern?: string}, value?: $binding) RadioItem(label: string, description: string, value: string) -SwitchGroup(name: string, items: SwitchItem[], variant?: "clear" | "card" | "sunk", value?) — Group of switch toggles +SwitchGroup(name: string, items: SwitchItem[], variant?: "clear" | "card" | "sunk", value?: $binding>) — Group of switch toggles SwitchItem(label?: string, description?: string, name: string, defaultChecked?: boolean) — Individual switch toggle - For Form fields, define EACH FormControl as its own reference — do NOT inline all controls in one array. This allows progressive field-by-field streaming. - NEVER nest Form inside Form — each Form should be a standalone container. diff --git a/docs/public/OpenclawOS-hero.png b/docs/public/OpenclawOS-hero1.png similarity index 100% rename from docs/public/OpenclawOS-hero.png rename to docs/public/OpenclawOS-hero1.png diff --git a/docs/public/openclaw-os/install.ps1 b/docs/public/openclaw-os/install.ps1 new file mode 100644 index 000000000..5b72f923f --- /dev/null +++ b/docs/public/openclaw-os/install.ps1 @@ -0,0 +1,264 @@ +# OpenClaw OS Installer (Windows / PowerShell 5.1+) +# +# Install: +# iwr -useb https://openui.com/openclaw-os/install.ps1 | iex +# +# Uninstall (one-liner): +# & ([scriptblock]::Create((iwr -useb https://openui.com/openclaw-os/install.ps1).Content)) -Uninstall +# +# Or download then run: +# iwr https://openui.com/openclaw-os/install.ps1 -OutFile install.ps1 +# ./install.ps1 # install +# ./install.ps1 -Uninstall # uninstall + +param( + [switch]$Uninstall +) + +$ErrorActionPreference = 'Stop' + +# Force TLS 1.2 — Windows PowerShell 5.1 defaults to TLS 1.0/1.1, which fails +# against modern HTTPS origins (openui.com, GitHub via npx degit). +[Net.ServicePointManager]::SecurityProtocol = + [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12 + +# Suppress corepack's interactive download prompt — there is no TTY in `iwr | iex`. +$env:COREPACK_ENABLE_DOWNLOAD_PROMPT = '0' + +$Prefix = '[openclaw-os]' +$Repo = 'thesysdev/openclaw-os' +$SrcDir = Join-Path $env:USERPROFILE '.openclaw\openui\openclaw-os' +$PluginDir = Join-Path $SrcDir 'packages\claw-plugin' +$PluginId = 'openclaw-os-plugin' +$OpenclawConfig = Join-Path $env:USERPROFILE '.openclaw\openclaw.json' +$LegacyDir = Join-Path $env:USERPROFILE ".openclaw\extensions\$PluginId" + +function Write-Log ($msg) { Write-Host "$Prefix $msg" -ForegroundColor DarkGray } +function Write-Ok ($msg) { Write-Host "$Prefix $msg" -ForegroundColor Cyan } +function Write-Warn2 ($msg) { Write-Host "$Prefix WARNING: $msg" -ForegroundColor Yellow } +function Write-Fatal ($msg) { Write-Host "$Prefix ERROR: $msg" -ForegroundColor Red; exit 1 } +function Write-Step ($msg) { Write-Host ""; Write-Host "==> $msg" -ForegroundColor Magenta } + +function Test-Cmd($name) { + $null -ne (Get-Command $name -ErrorAction SilentlyContinue) +} + +function Require-Cmd($name, $hint) { + if (-not (Test-Cmd $name)) { Write-Fatal $hint } +} + +function Banner { + Write-Host "" + Write-Host "OpenClaw OS" -ForegroundColor Magenta -NoNewline + Write-Host " — Generative UI for OpenClaw" -ForegroundColor DarkGray + Write-Host "" +} + +function Check-Prereqs { + Write-Step 'Checking prerequisites' + Require-Cmd 'openclaw' 'OpenClaw CLI not found. Install it first: https://openclaw.ai/install.ps1' + Require-Cmd 'node' 'Node.js not found. Install Node 22+ from https://nodejs.org' + Require-Cmd 'npx' 'npx not found. Reinstall Node.js to get npx.' + + if (-not (Test-Cmd 'pnpm')) { + Write-Log 'pnpm not found, installing via corepack…' + # Native commands do not throw on non-zero exit; check $LASTEXITCODE explicitly. + & corepack enable 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Log 'corepack enable failed, falling back to npm install -g pnpm…' + & npm install -g pnpm 2>&1 | Out-Null + if ($LASTEXITCODE -ne 0) { + Write-Fatal 'Could not install pnpm. Install manually: npm i -g pnpm' + } + } + } + + $nodeMajor = [int](node -p 'process.versions.node.split(".")[0]') + if ($nodeMajor -lt 22) { Write-Fatal "Node $nodeMajor detected. OpenClaw OS plugin requires Node 22+." } + + $oc = (& openclaw --version 2>$null | Select-Object -First 1) + Write-Ok "openclaw $oc · node $(node --version) · pnpm $(pnpm --version)" +} + +function Download-Source { + Write-Step "Downloading OpenClaw OS source ($Repo)" + New-Item -ItemType Directory -Force -Path (Split-Path $SrcDir) | Out-Null + + if (Test-Path $SrcDir) { + Write-Log "Removing previous source at $SrcDir" + Remove-Item -Recurse -Force $SrcDir + } + + & npx -y degit $Repo $SrcDir + if ($LASTEXITCODE -ne 0) { Write-Fatal "degit failed for $Repo. Check network and that the repo is public." } + + if (-not (Test-Path (Join-Path $PluginDir 'openclaw.plugin.json'))) { + Write-Fatal "Downloaded source missing $PluginDir\openclaw.plugin.json. Repo layout may have changed." + } + Write-Ok "Downloaded to $SrcDir" +} + +function Build-Plugin { + Write-Step 'Building plugin (pnpm install + bundle-ui + build)' + Write-Log 'Compiles claw-client UI and bundles the plugin. Expect 1–3 minutes on first run.' + + Push-Location $SrcDir + try { + & pnpm install --no-frozen-lockfile + if ($LASTEXITCODE -ne 0) { Write-Fatal 'Workspace pnpm install failed.' } + } finally { Pop-Location } + + # Run README's canonical sequence explicitly, not the `prepack` lifecycle — + # protects against silent no-op if the package.json script gets renamed. + Push-Location $PluginDir + try { + & pnpm bundle-ui + if ($LASTEXITCODE -ne 0) { Write-Fatal 'pnpm bundle-ui failed.' } + & pnpm build + if ($LASTEXITCODE -ne 0) { Write-Fatal 'pnpm build failed.' } + } finally { Pop-Location } + + if (-not (Test-Path (Join-Path $PluginDir 'dist\index.js'))) { Write-Fatal 'Build did not produce dist\index.js.' } + if (-not (Test-Path (Join-Path $PluginDir 'static'))) { Write-Fatal 'Build did not produce static\ (claw-client UI).' } + + Write-Ok 'Built plugin: dist\ + static\ ready' +} + +function Shrink-Source { + Write-Step "Removing node_modules (pnpm symlinks trip openclaw's install scanner)" + Get-ChildItem -Path $SrcDir -Recurse -Force -Directory -Filter 'node_modules' -ErrorAction SilentlyContinue | + Sort-Object FullName -Descending | + ForEach-Object { Remove-Item -Recurse -Force $_.FullName -ErrorAction SilentlyContinue } + Write-Ok 'node_modules removed (built artifacts kept)' +} + +function Install-Plugin { + Write-Step 'Registering plugin with OpenClaw' + & openclaw plugins install $PluginDir --force + if ($LASTEXITCODE -ne 0) { Write-Fatal 'openclaw plugins install failed.' } + Write-Ok 'Plugin installed' +} + +function Ensure-ToolsAllowed { + Write-Step 'Ensuring plugin tools are accessible' + if (-not (Test-Path $OpenclawConfig)) { Write-Warn2 "OpenClaw config not found at $OpenclawConfig, skipping"; return } + + $cfg = Get-Content -Raw $OpenclawConfig | ConvertFrom-Json + + # Avoid shadowing PowerShell's built-in $profile automatic variable. + $toolProfile = if ($cfg.tools -and $cfg.tools.profile) { $cfg.tools.profile } else { '' } + $alsoAllow = @() + if ($cfg.tools -and $cfg.tools.alsoAllow) { $alsoAllow = @($cfg.tools.alsoAllow) } + + if (-not $toolProfile -or $toolProfile -eq 'full') { Write-Log 'No restrictive tool profile, no patch needed'; return } + if ($alsoAllow -contains 'group:plugins') { Write-Log 'tools.alsoAllow already includes group:plugins'; return } + + # Use Add-Member defensively so this works under Set-StrictMode and on configs + # where `tools` or `tools.alsoAllow` is missing. + if (-not $cfg.PSObject.Properties['tools']) { + $cfg | Add-Member -MemberType NoteProperty -Name tools -Value ([pscustomobject]@{}) + } + if (-not $cfg.tools.PSObject.Properties['alsoAllow']) { + $cfg.tools | Add-Member -MemberType NoteProperty -Name alsoAllow -Value @() + } + $cfg.tools.alsoAllow = @($alsoAllow + 'group:plugins') + + # Force UTF-8 *without* BOM. PS 5.1's `Set-Content -Encoding utf8` writes a BOM, + # which the gateway's JSON parser may reject. + $json = $cfg | ConvertTo-Json -Depth 50 + $utf8NoBom = New-Object System.Text.UTF8Encoding $false + [System.IO.File]::WriteAllText($OpenclawConfig, $json, $utf8NoBom) + Write-Ok "Added group:plugins to tools.alsoAllow (profile=$toolProfile)" +} + +function Restart-Gateway { + Write-Step 'Restarting OpenClaw gateway' + # Native commands do not throw on non-zero exit, so try/catch wouldn't fire. + & openclaw gateway restart + if ($LASTEXITCODE -eq 0) { + Write-Ok 'Gateway restarted' + } else { + Write-Warn2 'Could not restart gateway automatically. Run: openclaw gateway restart' + } +} + +function Verify { + Write-Step 'Verifying installation' + $json = & openclaw plugins list --json 2>$null | Out-String + if (-not $json) { Write-Fatal 'openclaw plugins list --json returned nothing.' } + $data = $json | ConvertFrom-Json + + $found = $null + $stack = New-Object System.Collections.Stack + $stack.Push($data) + while ($stack.Count -gt 0) { + $node = $stack.Pop() + if ($null -eq $node) { continue } + if ($node -is [System.Collections.IEnumerable] -and -not ($node -is [string])) { + foreach ($child in $node) { $stack.Push($child) } + } elseif ($node.PSObject -and $node.PSObject.Properties) { + if ($node.PSObject.Properties.Name -contains 'id' -and $node.id -eq $PluginId) { $found = $node; break } + foreach ($p in $node.PSObject.Properties) { $stack.Push($p.Value) } + } + } + + if ($found) { + Write-Ok "$PluginId registered (status=$($found.status) enabled=$($found.enabled))" + } else { + Write-Fatal "$PluginId not visible in 'openclaw plugins list --json'." + } +} + +function Uninstall-Plugin { + Write-Step "Disabling $PluginId" + & openclaw plugins disable $PluginId + if ($LASTEXITCODE -ne 0) { Write-Warn2 'Could not disable plugin (may not be installed). Continuing.' } else { Write-Ok 'Plugin disabled' } + + Write-Step "Uninstalling $PluginId" + & openclaw plugins uninstall $PluginId + if ($LASTEXITCODE -ne 0) { Write-Warn2 'Could not uninstall plugin (may not be registered). Continuing.' } else { Write-Ok 'Plugin uninstalled' } +} + +function Remove-Source { + Write-Step "Removing source at $SrcDir" + if (Test-Path $SrcDir) { Remove-Item -Recurse -Force $SrcDir; Write-Ok "Removed $SrcDir" } + else { Write-Log 'Source dir not present, skipping' } +} + +function Remove-LegacyGlobalInstall { + if (Test-Path $LegacyDir) { + Write-Step "Removing legacy global install at $LegacyDir" + Remove-Item -Recurse -Force $LegacyDir + Write-Ok "Removed $LegacyDir" + } +} + +function Do-Install { + Banner + Check-Prereqs + Download-Source + Build-Plugin + Shrink-Source + Install-Plugin + Ensure-ToolsAllowed + Restart-Gateway + Verify + Write-Host "" + Write-Host "✓ OpenClaw OS installed." -ForegroundColor Cyan + Write-Host " Open the Claw UI from your OpenClaw client to start generating apps." -ForegroundColor DarkGray + Write-Host "" +} + +function Do-Uninstall { + Banner + Require-Cmd 'openclaw' 'OpenClaw CLI not found — nothing to uninstall.' + Uninstall-Plugin + Remove-Source + Remove-LegacyGlobalInstall + Restart-Gateway + Write-Host "" + Write-Host "✓ OpenClaw OS uninstalled." -ForegroundColor Cyan + Write-Host "" +} + +if ($Uninstall) { Do-Uninstall } else { Do-Install } diff --git a/docs/public/openclaw-os/install.sh b/docs/public/openclaw-os/install.sh new file mode 100755 index 000000000..151dfccea --- /dev/null +++ b/docs/public/openclaw-os/install.sh @@ -0,0 +1,242 @@ +#!/bin/bash +set -euo pipefail + +# OpenClaw OS Installer (macOS / Linux) +# Install: curl -fsSL https://openui.com/openclaw-os/install.sh | bash +# Uninstall: curl -fsSL https://openui.com/openclaw-os/install.sh | bash -s -- uninstall + +# Suppress corepack's interactive download prompt — there is no TTY in `curl | bash`. +export COREPACK_ENABLE_DOWNLOAD_PROMPT=0 + +PREFIX="[openclaw-os]" +REPO="thesysdev/openclaw-os" +SRC_DIR="$HOME/.openclaw/openui/openclaw-os" +PLUGIN_DIR="$SRC_DIR/packages/claw-plugin" +PLUGIN_ID="openclaw-os-plugin" +OPENCLAW_CONFIG="$HOME/.openclaw/openclaw.json" + +BOLD='\033[1m' +ACCENT='\033[38;2;255;77;77m' +INFO='\033[38;2;136;146;176m' +SUCCESS='\033[38;2;0;229;204m' +WARN='\033[38;2;255;176;32m' +ERROR='\033[38;2;230;57;70m' +NC='\033[0m' + +log() { printf "${INFO}%s${NC} %s\n" "$PREFIX" "$1"; } +ok() { printf "${SUCCESS}%s${NC} %s\n" "$PREFIX" "$1"; } +warn() { printf "${WARN}%s WARNING:${NC} %s\n" "$PREFIX" "$1"; } +fatal(){ printf "${ERROR}%s ERROR:${NC} %s\n" "$PREFIX" "$1" >&2; exit 1; } +step() { printf "\n${BOLD}${ACCENT}==>${NC} ${BOLD}%s${NC}\n" "$1"; } + +require_cmd() { + command -v "$1" >/dev/null 2>&1 || fatal "$2" +} + +banner() { + printf "\n${BOLD}${ACCENT}OpenClaw OS${NC} ${INFO}— Generative UI for OpenClaw${NC}\n\n" +} + +check_prereqs() { + step "Checking prerequisites" + require_cmd openclaw "OpenClaw CLI not found. Install it first: https://openclaw.ai/install.sh" + require_cmd node "Node.js not found. Install Node 22+ from https://nodejs.org" + require_cmd npx "npx not found. Reinstall Node.js to get npx." + + if ! command -v pnpm >/dev/null 2>&1; then + log "pnpm not found, installing via corepack…" + corepack enable >/dev/null 2>&1 || npm install -g pnpm >/dev/null 2>&1 \ + || fatal "Could not install pnpm. Install manually: npm i -g pnpm" + fi + + local node_major + node_major="$(node -p 'process.versions.node.split(".")[0]')" + if [[ "$node_major" -lt 22 ]]; then + fatal "Node $node_major detected. OpenClaw OS plugin requires Node 22+." + fi + + ok "openclaw $(openclaw --version 2>/dev/null | head -1) · node $(node --version) · pnpm $(pnpm --version)" +} + +download_source() { + step "Downloading OpenClaw OS source ($REPO)" + + mkdir -p "$HOME/.openclaw/openui" + if [[ -d "$SRC_DIR" ]]; then + log "Removing previous source at $SRC_DIR" + rm -rf "$SRC_DIR" + fi + + npx -y degit "$REPO" "$SRC_DIR" || fatal "degit failed for $REPO. Check network and that the repo is public." + + [[ -f "$PLUGIN_DIR/openclaw.plugin.json" ]] \ + || fatal "Downloaded source is missing $PLUGIN_DIR/openclaw.plugin.json. Repo layout may have changed." + + ok "Downloaded to $SRC_DIR" +} + +build_plugin() { + step "Building plugin (pnpm install + bundle-ui + build)" + log "This compiles the claw-client UI and bundles the plugin. Expect 1–3 minutes on first run." + + ( cd "$SRC_DIR" && pnpm install --no-frozen-lockfile ) \ + || fatal "Workspace pnpm install failed. See output above." + + # Run the README's canonical sequence explicitly rather than relying on the `prepack` + # lifecycle, so a future package.json rename can't silently no-op the build. + ( cd "$PLUGIN_DIR" && pnpm bundle-ui && pnpm build ) \ + || fatal "Plugin build failed (bundle-ui or build). See output above." + + [[ -f "$PLUGIN_DIR/dist/index.js" ]] || fatal "Build did not produce dist/index.js." + [[ -d "$PLUGIN_DIR/static" ]] || fatal "Build did not produce static/ (claw-client UI)." + + ok "Built plugin: dist/ + static/ ready" +} + +shrink_source() { + step "Removing node_modules (pnpm symlinks trip openclaw's install scanner)" + # Recursive — catches nested workspaces too, not just one level under packages/. + find "$SRC_DIR" -type d -name node_modules -prune -exec rm -rf {} + 2>/dev/null || true + ok "node_modules removed (built artifacts kept)" +} + +install_plugin() { + step "Registering plugin with OpenClaw" + + openclaw plugins install "$PLUGIN_DIR" --force \ + || fatal "openclaw plugins install failed. Run with --verbose for detail." + + ok "Plugin installed" +} + +ensure_tools_allowed() { + step "Ensuring plugin tools are accessible" + + if [[ ! -f "$OPENCLAW_CONFIG" ]]; then + warn "OpenClaw config not found at $OPENCLAW_CONFIG, skipping" + return + fi + + if ! command -v jq >/dev/null 2>&1; then + warn "jq not found, skipping tool-policy patch. If plugin tools are blocked, add 'group:plugins' to tools.alsoAllow in $OPENCLAW_CONFIG manually." + return + fi + + local profile alsoAllow + profile=$(jq -r '.tools.profile // empty' "$OPENCLAW_CONFIG") + alsoAllow=$(jq -r '(.tools.alsoAllow // []) | join(",")' "$OPENCLAW_CONFIG") + + if [[ -z "$profile" || "$profile" == "full" ]]; then + log "No restrictive tool profile, no patch needed" + return + fi + + if [[ ",$alsoAllow," == *",group:plugins,"* ]]; then + log "tools.alsoAllow already includes group:plugins" + return + fi + + local tmp + tmp=$(mktemp) + jq '.tools.alsoAllow = ((.tools.alsoAllow // []) + ["group:plugins"])' "$OPENCLAW_CONFIG" > "$tmp" \ + && mv "$tmp" "$OPENCLAW_CONFIG" + ok "Added group:plugins to tools.alsoAllow (profile=$profile)" +} + +restart_gateway() { + step "Restarting OpenClaw gateway" + if openclaw gateway restart 2>&1; then + ok "Gateway restarted" + else + warn "Could not restart gateway automatically. Run: openclaw gateway restart" + fi +} + +verify() { + step "Verifying installation" + local found="" + if command -v jq >/dev/null 2>&1; then + found=$(openclaw plugins list --json 2>/dev/null \ + | jq -r --arg id "$PLUGIN_ID" '.. | select(.id? == $id) | "\(.id) status=\(.status) enabled=\(.enabled)"' \ + | head -1) + else + found=$(openclaw plugins list --json 2>/dev/null | tr -d '\n ' | grep -o "\"id\":\"$PLUGIN_ID\"" | head -1) + fi + + if [[ -n "$found" ]]; then + ok "$PLUGIN_ID registered ($found)" + else + fatal "$PLUGIN_ID not visible in 'openclaw plugins list --json'. Run: openclaw plugins list --json | grep $PLUGIN_ID" + fi +} + +uninstall_plugin() { + step "Disabling $PLUGIN_ID" + if openclaw plugins disable "$PLUGIN_ID" 2>&1; then + ok "Plugin disabled" + else + warn "Could not disable plugin (may not be installed). Continuing." + fi + + step "Uninstalling $PLUGIN_ID" + if openclaw plugins uninstall "$PLUGIN_ID" 2>&1; then + ok "Plugin uninstalled" + else + warn "Could not uninstall plugin (may not be registered). Continuing." + fi +} + +remove_source() { + step "Removing source at $SRC_DIR" + if [[ -d "$SRC_DIR" ]]; then + rm -rf "$SRC_DIR" + ok "Removed $SRC_DIR" + else + log "Source dir not present, skipping" + fi +} + +remove_legacy_global_install() { + local legacy="$HOME/.openclaw/extensions/$PLUGIN_ID" + if [[ -d "$legacy" ]]; then + step "Removing legacy global install at $legacy" + rm -rf "$legacy" + ok "Removed $legacy" + fi +} + +do_install() { + banner + check_prereqs + download_source + build_plugin + shrink_source + install_plugin + ensure_tools_allowed + restart_gateway + verify + + printf "\n${SUCCESS}${BOLD}✓ OpenClaw OS installed.${NC}\n" + printf "${INFO} Open the Claw UI from your OpenClaw client to start generating apps.${NC}\n\n" +} + +do_uninstall() { + banner + require_cmd openclaw "OpenClaw CLI not found — nothing to uninstall." + uninstall_plugin + remove_source + remove_legacy_global_install + restart_gateway + + printf "\n${SUCCESS}${BOLD}✓ OpenClaw OS uninstalled.${NC}\n\n" +} + +main() { + case "${1:-install}" in + install) do_install ;; + uninstall) do_uninstall ;; + *) fatal "Unknown command: $1. Use 'install' (default) or 'uninstall'." ;; + esac +} + +main "$@"