golem-kit 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,41 @@
1
+ # Golem
2
+
3
+ ![Golem mascot](assets/golem-mascot.png)
4
+
5
+ ## What is Golem?
6
+
7
+ Golem is an early local application shell for building applications with AI. **Your app is its own IDE:** enter build mode, ask a locally authenticated Codex to edit the app, and keep using it in the same browser shell.
8
+
9
+ ## Getting Started
10
+
11
+ Install Node.js >=22.18.0 and pnpm 10.28.2. Then run:
12
+
13
+ ```sh
14
+ mkdir my-app
15
+ cd my-app
16
+ npx golem-kit init
17
+ ```
18
+
19
+ Start the application:
20
+
21
+ ```sh
22
+ ./golem dev
23
+ ```
24
+
25
+ Open the browser address printed in the terminal, enter build mode, and start a conversation. Build chat requires the Codex CLI installed and authenticated on the same machine. After a successful build-mode change, the browser shell rebuilds and refreshes; conversations are saved in `.golem/` and restored when the server restarts.
26
+
27
+ `./golem build` writes the browser shell to `dist/`. `./golem dev` binds only to `127.0.0.1:3000`.
28
+
29
+ ## Local source development
30
+
31
+ Use an installed package by default, or opt into a durable checkout while developing Golem:
32
+
33
+ ```sh
34
+ GOLEM_SOURCE=/path/to/golem ./golem dev
35
+ ```
36
+
37
+ See [the local CLI guide](docs/local-cli.md) for the complete command contract.
38
+
39
+ ## Current scope
40
+
41
+ Claude integration, domain storage, accounts, and permissions are planned, not part of this release.
Binary file
@@ -0,0 +1,64 @@
1
+ # Local command contract
2
+
3
+ Use Node.js >=22.18.0 (native TypeScript execution) and pnpm 10.28.2.
4
+ Run `pnpm install`, then `./golem help`. The executable wrapper resolves the
5
+ project-local CLI relative to itself, including when invoked from another directory.
6
+ The browser shell uses the local Vite build and the published `golem-ui` package.
7
+
8
+ ## Source resolution seam
9
+
10
+ The root wrapper explicitly selects `src/cli.ts` relative to the wrapper's own
11
+ directory. This is the sole CLI entrypoint resolution point today; there is no
12
+ package-name lookup or resolution configuration. Generated app wrappers add an
13
+ explicit `GOLEM_SOURCE=/path/to/golem` opt-in for running a framework checkout
14
+ while keeping the app cwd. Unset it to use the installed pinned package.
15
+
16
+ The CLI imports `./dev-server.ts` relative to the wrapper. The server serves built `dist/`
17
+ files and does not resolve packages or depend on
18
+ the caller's working directory. Keep source/package selection outside this server
19
+ boundary so a hot-source workflow can select the CLI without changing HTTP startup.
20
+
21
+ ## Commands
22
+
23
+ | Command | Behavior | Exit status |
24
+ | --- | --- | --- |
25
+ | `./golem help` (or `./golem`) | List all commands | 0 |
26
+ | `./golem dev` | Refresh the browser build, then serve it at `http://127.0.0.1:3000/` until SIGINT/SIGTERM | 0 on clean shutdown; 1 on startup failure |
27
+ | `./golem build` | Build the browser shell into `dist/` | 0 |
28
+ | `./golem doctor` | Report local shell and backend readiness | 0 |
29
+
30
+ Unknown commands and extra arguments exit 2. Built assets are served directly; extensionless browser
31
+ routes fall back to `index.html`, while missing assets return 404. Malformed URLs return 400. The
32
+ server binds to loopback only. Port 3000 must be free.
33
+ `doctor` succeeding means its report ran, not that the full product is ready.
34
+
35
+ `src/dev-server.ts` exports `startDevServer(port = 3000)`, resolving to a listening
36
+ Node HTTP server. It refreshes the browser build before listening; the CLI owns signal handling and output. `src/browser/app.tsx` is the
37
+ composition boundary: anonymous identity and browser navigation are explicit host adapters, while
38
+ the chat adapter connects explicit browser build-mode sessions to the local Codex runtime.
39
+
40
+ Check types with `pnpm exec tsc --noEmit`; run the CLI/HTTP smoke check with
41
+ `node --test --test-concurrency=1 test/*.mjs` (serial because the CLI test intentionally removes
42
+ and rebuilds the shared `dist/` directory; requires port 3000).
43
+
44
+ ## Generated projects
45
+
46
+ `golem-kit init` creates `package.json`, `golem.config.ts`, `src/app.tsx`,
47
+ `docs/domain.md`, and an executable `./golem`. Normal initialization writes the
48
+ pinned npm dependency `golem-kit@<framework version>` and installs it with pnpm.
49
+ For local packed-tarball acceptance only, set `GOLEM_KIT_TARBALL=/path/to/golem-kit.tgz`.
50
+ An existing package is supported only when it already declares `golem-kit`; its
51
+ metadata is preserved. Other nonempty destinations are refused.
52
+
53
+ Two-checkout development:
54
+
55
+ ```sh
56
+ GOLEM_SOURCE=/path/to/golem /path/to/app/golem build
57
+ GOLEM_SOURCE=/path/to/golem /path/to/app/golem dev
58
+ ```
59
+
60
+ The wrapper changes into the app first, so `src/` and `dist/` remain app-owned.
61
+ The framework checkout uses its own installed dependencies. Source-mode builds
62
+ print framework path and short git revision, plus the same fields for
63
+ `GOLEM_UI_SOURCE` when that override is set. Omit both overrides to return to the
64
+ installed pinned package.
@@ -0,0 +1,65 @@
1
+ # Local UI source development
2
+
3
+ The normal Golem checkout uses the pinned published `golem-ui@0.1.1` dependency. For a local
4
+ change to `golem-ui`, set `GOLEM_UI_SOURCE` to an isolated UI checkout for that command. Golem
5
+ aliases the package entry point and stylesheet to `src/index.ts` and `src/styles.css`, while
6
+ deduplicating React with the Golem checkout. It also loads the UI checkout's installed
7
+ `@tailwindcss/vite` plugin and adds the checkout as a Tailwind `@source`, so utility classes in
8
+ edited UI components are compiled and scanned.
9
+
10
+ ## Clean checkout
11
+
12
+ ```sh
13
+ git clone https://github.com/tonylampada/golem.git
14
+ git clone https://github.com/tonylampada/golem-ui.git
15
+ cd golem
16
+ pnpm install
17
+ cd ../golem-ui
18
+ pnpm install
19
+ cd ../golem
20
+ ```
21
+
22
+ Run the Golem checkout against the checked-out UI source:
23
+
24
+ ```sh
25
+ GOLEM_UI_SOURCE=../golem-ui ./golem dev
26
+ ```
27
+
28
+ The build prints both checkout paths and their short git revisions in source mode. `./golem
29
+ build` accepts the same override. The path must contain the golem-ui `package.json`, `src/index.ts`,
30
+ and `src/styles.css`; invalid paths fail with an explanatory error.
31
+
32
+ To return to the published dependency, omit the variable:
33
+
34
+ ```sh
35
+ ./golem dev
36
+ ```
37
+
38
+ With the server running, these direct browser assertions check the expected placeholder, computed
39
+ font/display, and desktop/mobile chat/canvas geometry. Install Playwright and its managed Chromium
40
+ in a temporary directory. Set GOLEM_BROWSER_EXECUTABLE only when using another browser binary:
41
+
42
+ ```sh
43
+ browser_tools=$(mktemp -d)
44
+ npm --prefix "$browser_tools" install --no-save playwright
45
+ "$browser_tools/node_modules/.bin/playwright" install chromium
46
+ export PLAYWRIGHT_MODULE="$browser_tools/node_modules/playwright/index.mjs"
47
+ GOLEM_UI_SOURCE=../golem-ui GOLEM_EXPECTED_PLACEHOLDER='Message the agent…' GOLEM_BROWSER_SCREENSHOT=.artifacts/source \
48
+ node scripts/browser-assertions.mjs
49
+ unset GOLEM_UI_SOURCE GOLEM_EXPECTED_PLACEHOLDER
50
+ GOLEM_BROWSER_SCREENSHOT=.artifacts/published node scripts/browser-assertions.mjs
51
+ rm -rf "$browser_tools"
52
+ ```
53
+
54
+ For example, an explicit browser binary can be selected with `GOLEM_BROWSER_EXECUTABLE=/path/to/chrome`.
55
+
56
+ For a temporary source marker experiment, set GOLEM_EXPECTED_PLACEHOLDER to the marker after
57
+ editing the isolated UI checkout. Ordinary source mode expects the normal Message the agent… placeholder.
58
+ No package scripts or lockfiles need to change when switching modes. `golem-ui` itself uses Vite
59
+ and its package build is `pnpm build`; source mode consumes its TypeScript entry point directly.
60
+
61
+ For generated apps, `GOLEM_SOURCE=/path/to/golem /path/to/app/golem dev` opts into
62
+ the framework checkout while preserving the app cwd and lockfile. Omit
63
+ `GOLEM_SOURCE` to use the installed pinned `golem-kit`; combine it with
64
+ `GOLEM_UI_SOURCE=/path/to/golem-ui` when developing both checkouts. Source-mode
65
+ builds print each source path and short git revision.
@@ -0,0 +1 @@
1
+ export default { title: 'Golem' }
package/index.html ADDED
@@ -0,0 +1,12 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <title>Golem</title>
7
+ </head>
8
+ <body>
9
+ <div id="root"></div>
10
+ <script type="module" src="/src/browser/main.tsx"></script>
11
+ </body>
12
+ </html>
package/package.json ADDED
@@ -0,0 +1,48 @@
1
+ {
2
+ "name": "golem-kit",
3
+ "version": "0.1.0",
4
+ "description": "Starter kit and local CLI for Golem applications.",
5
+ "type": "module",
6
+ "files": [
7
+ "README.md",
8
+ "assets/golem-mascot.png",
9
+ "docs/local-cli.md",
10
+ "docs/source-development.md",
11
+ "golem.config.ts",
12
+ "index.html",
13
+ "src",
14
+ "tsconfig.json",
15
+ "vite.config.ts"
16
+ ],
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/tonylampada/golem.git"
20
+ },
21
+ "bugs": {
22
+ "url": "https://github.com/tonylampada/golem/issues"
23
+ },
24
+ "homepage": "https://github.com/tonylampada/golem#readme",
25
+ "packageManager": "pnpm@10.28.2",
26
+ "engines": {
27
+ "node": ">=22.18.0",
28
+ "pnpm": "10.28.2"
29
+ },
30
+ "bin": {
31
+ "golem-kit": "src/cli.ts"
32
+ },
33
+ "scripts": {
34
+ "test": "node --test --test-concurrency=1 test/*.test.mjs"
35
+ },
36
+ "dependencies": {
37
+ "@types/node": "22.20.3",
38
+ "@types/react": "19.2.2",
39
+ "@types/react-dom": "19.2.1",
40
+ "@vitejs/plugin-react": "5.0.4",
41
+ "typescript": "7.0.2",
42
+ "vite": "7.1.9",
43
+ "tsx": "4.23.13",
44
+ "golem-ui": "0.1.1",
45
+ "react": "19.2.0",
46
+ "react-dom": "19.2.0"
47
+ }
48
+ }
package/src/app.tsx ADDED
@@ -0,0 +1,10 @@
1
+ export default function App() {
2
+ return (
3
+ <section className="flex h-full min-h-64 items-center justify-center bg-neutral-50 p-6 text-center">
4
+ <div>
5
+ <h1 className="text-lg font-semibold">Welcome to Golem</h1>
6
+ <p className="mt-2 text-sm text-neutral-500">Edit src/app.tsx to build your app.</p>
7
+ </div>
8
+ </section>
9
+ )
10
+ }
@@ -0,0 +1,164 @@
1
+ import type { ChatAdapter, ChatAttachment, ChatMessage, IdentityAdapter, NavigationAdapter, Route, User } from 'golem-ui'
2
+
3
+ const unavailable = () => Promise.reject(new Error('No agent or identity service is connected.'))
4
+
5
+ /** Anonymous means only that this host has no sign-in service; it grants no authorization. */
6
+ export const anonymousIdentity: IdentityAdapter = {
7
+ currentUser: async () => null,
8
+ subscribe: () => () => {},
9
+ signIn: unavailable,
10
+ signUp: unavailable,
11
+ requestCode: unavailable,
12
+ verifyCode: unavailable,
13
+ signOut: async () => {},
14
+ listMembers: async () => [],
15
+ invite: unavailable,
16
+ removeMember: unavailable,
17
+ setRole: unavailable,
18
+ }
19
+
20
+ function route(): Route {
21
+ return { path: window.location.pathname, params: Object.fromEntries(new URLSearchParams(window.location.search)) }
22
+ }
23
+
24
+ export const navigation: NavigationAdapter = {
25
+ current: route,
26
+ go(path) { window.history.pushState({}, '', path); window.dispatchEvent(new PopStateEvent('popstate')) },
27
+ subscribe(listener) {
28
+ const update = () => listener(route())
29
+ window.addEventListener('popstate', update)
30
+ return () => window.removeEventListener('popstate', update)
31
+ },
32
+ }
33
+
34
+ const storageKey = 'golem.browser.session'
35
+ let sessionId: string | undefined = (() => {
36
+ try { return window.sessionStorage.getItem(storageKey) ?? undefined } catch { return undefined }
37
+ })()
38
+ let messages: ChatMessage[] = []
39
+ let cursor = -1
40
+ let eventLog = new Map<number, { sequence: number; type: string; text?: string; status?: string; reason?: string }>()
41
+ let status = 'starting'
42
+ let source: EventSource | undefined
43
+ const listeners = new Set<(messages: ChatMessage[]) => void>()
44
+ const statusListeners = new Set<(status: string) => void>()
45
+ const emit = () => listeners.forEach((listener) => listener([...messages]))
46
+ const setStatus = (next: string) => { status = next; statusListeners.forEach((listener) => listener(status)) }
47
+
48
+ function mergeEvents(events: Array<{ sequence: number; type: string; text?: string; status?: string; reason?: string }>): string | undefined {
49
+ for (const event of events) if (!eventLog.has(event.sequence)) eventLog.set(event.sequence, event)
50
+ const ordered = [...eventLog.values()].sort((left, right) => left.sequence - right.sequence)
51
+ cursor = Math.max(cursor, ...ordered.map((event) => event.sequence))
52
+ messages = fromEvents(ordered)
53
+ return ordered.findLast((event) => event.type === 'status')?.status
54
+ }
55
+
56
+ function fromEvents(events: Array<{ type: string; sequence: number; text?: string; status?: string; reason?: string }>): ChatMessage[] {
57
+ return events.flatMap((event) => {
58
+ if ((event.type === 'user' || event.type === 'message') && event.text) {
59
+ return [{ id: `${event.sequence}`, role: event.type === 'user' ? 'user' : 'agent', text: event.text, at: new Date().toISOString() }]
60
+ }
61
+ if (event.type === 'error') return [{ id: `${event.sequence}`, role: 'agent', text: `Error: ${event.text ?? 'Agent failed.'}`, at: new Date().toISOString() }]
62
+ if (event.type === 'interrupted') return [{ id: `${event.sequence}`, role: 'agent', text: `Interrupted${event.reason ? `: ${event.reason}` : '.'}`, at: new Date().toISOString() }]
63
+ return []
64
+ })
65
+ }
66
+
67
+ /** The only session-starting call in the UI — declares build intent explicitly; the server decides permission from it. */
68
+ export async function startBrowserSession(): Promise<{ id: string; backend: string }> {
69
+ const response = await fetch('/api/sessions', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ backend: 'codex', intent: 'build' }) })
70
+ const result = await response.json() as { id?: string; backend?: string; error?: string }
71
+ if (!response.ok || !result.id) throw new Error(result.error ?? 'Unable to start session')
72
+ source?.close()
73
+ source = undefined
74
+ sessionId = result.id
75
+ window.sessionStorage.setItem(storageKey, sessionId)
76
+ cursor = -1
77
+ eventLog = new Map()
78
+ setStatus('ready')
79
+ messages = []
80
+ emit()
81
+ return { id: result.id, backend: result.backend ?? 'codex' }
82
+ }
83
+
84
+ export function currentBrowserSession(): string | undefined { return sessionId }
85
+
86
+ export async function restoreBrowserSession(): Promise<boolean> {
87
+ if (!sessionId) return false
88
+ const response = await fetch(`/api/sessions/${sessionId}/history`)
89
+ if (response.status === 404) {
90
+ sessionId = undefined
91
+ window.sessionStorage.removeItem(storageKey)
92
+ return false
93
+ }
94
+ if (!response.ok) throw new Error((await response.json()).error ?? 'Unable to restore session')
95
+ const result = await response.json() as { events: Array<{ sequence: number; type: string; text?: string; reason?: string }>; status: string }
96
+ eventLog = new Map()
97
+ setStatus(mergeEvents(result.events) ?? result.status)
98
+ emit()
99
+ return true
100
+ }
101
+
102
+ export function subscribeBrowserStatus(listener: (status: string) => void): () => void {
103
+ statusListeners.add(listener)
104
+ listener(status)
105
+ return () => statusListeners.delete(listener)
106
+ }
107
+
108
+ export async function interruptBrowserSession(): Promise<void> {
109
+ if (!sessionId) return
110
+ const response = await fetch(`/api/sessions/${sessionId}/interrupt`, { method: 'POST', headers: { 'Content-Type': 'application/json' } })
111
+ if (!response.ok) throw new Error((await response.json()).error ?? 'Unable to interrupt session')
112
+ }
113
+
114
+ function applyEvent(event: { sessionId?: string; sequence: number; type: string; text?: string; status?: string; reason?: string }): void {
115
+ if (event.sessionId && event.sessionId !== sessionId) return
116
+ if (event.sequence <= cursor) return
117
+ cursor = event.sequence
118
+ if (event.status) setStatus(event.status)
119
+ // Live-only: a replayed 'rebuilt' from history/reload restoration must never re-trigger this.
120
+ if (event.type === 'rebuilt') { window.location.reload(); return }
121
+ mergeEvents([event])
122
+ emit()
123
+ }
124
+
125
+ export const chat: ChatAdapter = {
126
+ history: async () => {
127
+ if (!sessionId) return []
128
+ const response = await fetch(`/api/sessions/${sessionId}/history`)
129
+ if (!response.ok) throw new Error((await response.json()).error ?? 'Unable to load session history')
130
+ const result = await response.json() as { events: Array<{ sequence: number; type: string; text?: string; reason?: string }>; status: string }
131
+ setStatus(mergeEvents(result.events) ?? result.status)
132
+ emit()
133
+ return [...messages]
134
+ },
135
+ subscribe(listener) {
136
+ listeners.add(listener)
137
+ if (sessionId) {
138
+ source?.close()
139
+ const subscribedSession = sessionId
140
+ let subscribedSource: EventSource
141
+ const connect = () => {
142
+ const nextSource = new EventSource(`/api/sessions/${subscribedSession}/events?after=${cursor}`)
143
+ subscribedSource = nextSource
144
+ source = nextSource
145
+ nextSource.onmessage = (event) => applyEvent(JSON.parse(event.data))
146
+ nextSource.onerror = () => {
147
+ if (source === nextSource && nextSource.readyState === EventSource.CLOSED && sessionId === subscribedSession) {
148
+ connect()
149
+ }
150
+ }
151
+ }
152
+ connect()
153
+ return () => { subscribedSource.close(); if (source === subscribedSource) source = undefined; listeners.delete(listener) }
154
+ }
155
+ return () => listeners.delete(listener)
156
+ },
157
+ async send(text, attachments) {
158
+ if (!sessionId) throw new Error('Enter build mode before sending a message')
159
+ const response = await fetch(`/api/sessions/${sessionId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ text, attachments }) })
160
+ if (!response.ok) throw new Error((await response.json()).error ?? 'Agent request failed')
161
+ },
162
+ }
163
+
164
+ export type { ChatMessage, User }
@@ -0,0 +1,9 @@
1
+ declare module '@golem/app' {
2
+ import type { ComponentType } from 'react'
3
+ const App: ComponentType
4
+ export default App
5
+ }
6
+ declare module '@golem/config' {
7
+ const config: { title: string }
8
+ export default config
9
+ }
@@ -0,0 +1,63 @@
1
+ import { useEffect, useRef, useState } from 'react'
2
+ import { Chat, Shell } from 'golem-ui'
3
+ import UserApp from '@golem/app'
4
+ import projectConfig from '@golem/config'
5
+ import { anonymousIdentity, chat, interruptBrowserSession, navigation, restoreBrowserSession, startBrowserSession, subscribeBrowserStatus } from './adapters'
6
+
7
+ const shellAdapters = { identity: anonymousIdentity, navigation }
8
+ const chatAdapters = { chat }
9
+
10
+ export function App() {
11
+ const [mode, setMode] = useState(false)
12
+ const [session, setSession] = useState<string>()
13
+ const [sessionStatus, setSessionStatus] = useState('starting')
14
+ const [enteringBuildMode, setEnteringBuildMode] = useState(false)
15
+ const enteringBuildModeRef = useRef(false)
16
+ const [runtime, setRuntime] = useState<{ codex: boolean; claude: boolean }>({ codex: false, claude: false })
17
+ const [error, setError] = useState<string>()
18
+ useEffect(() => {
19
+ fetch('/api/runtime').then((response) => response.json()).then((result) => {
20
+ const discoveries = result.discoveries as Array<{ agent: string; status: string; runnable: boolean }>
21
+ setRuntime({ codex: discoveries.some((item) => item.agent === 'codex' && item.status === 'available' && item.runnable), claude: false })
22
+ }).catch(() => setError('Runtime discovery unavailable.'))
23
+ }, [])
24
+ useEffect(() => {
25
+ const unsubscribe = subscribeBrowserStatus(setSessionStatus)
26
+ restoreBrowserSession().then((restored) => {
27
+ if (restored) { setSession(window.sessionStorage.getItem('golem.browser.session') ?? undefined); setMode(true) }
28
+ }).catch((cause) => setError(cause instanceof Error ? cause.message : String(cause)))
29
+ return unsubscribe
30
+ }, [])
31
+ const enterBuildMode = async () => {
32
+ if (enteringBuildModeRef.current) return
33
+ enteringBuildModeRef.current = true
34
+ setEnteringBuildMode(true)
35
+ setError(undefined)
36
+ try { const started = await startBrowserSession(); setSession(started.id); setMode(true) }
37
+ catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)) }
38
+ finally { enteringBuildModeRef.current = false; setEnteringBuildMode(false) }
39
+ }
40
+ const interrupt = async () => {
41
+ try { await interruptBrowserSession() } catch (cause) { setError(cause instanceof Error ? cause.message : String(cause)) }
42
+ }
43
+ return (
44
+ <Shell
45
+ config={{ title: projectConfig.title, chatSide: 'left', breakpoint: 768 }}
46
+ adapters={shellAdapters}
47
+ chat={
48
+ <div className="flex h-full min-h-0 flex-col">
49
+ <div className="flex items-center justify-between border-b border-neutral-200 bg-white px-4 py-3 text-sm">
50
+ <span className="font-medium">{mode ? 'Build mode' : 'Conversation mode'}</span>
51
+ <span className={mode ? 'text-green-700' : 'text-neutral-500'}>{mode ? `${sessionStatus} · ${session}` : 'Not connected'}</span>
52
+ {mode && (sessionStatus === 'ready' || sessionStatus === 'starting') && <button className="rounded border border-red-300 px-2 py-1 text-red-700" onClick={interrupt}>Interrupt</button>}
53
+ </div>
54
+ {!mode ? <div className="p-4 text-sm"><p className="text-neutral-600">Codex: {runtime.codex ? 'available' : 'unavailable'} · Claude: not yet connected</p><button className="mt-4 rounded bg-neutral-900 px-3 py-2 text-white disabled:opacity-40" disabled={!runtime.codex || enteringBuildMode} onClick={enterBuildMode}>Enter build mode</button>{error && <p className="mt-3 text-red-700">{error}</p>}</div> : <Chat key={session} config={{ agentName: 'Golem Codex', emptyState: 'Ask Codex to inspect or explain this workspace.' }} adapters={chatAdapters} />}
55
+ </div>
56
+ }
57
+ canvas={
58
+ <UserApp />
59
+ }
60
+ account={<span className="shrink-0 text-sm text-neutral-500">Guest</span>}
61
+ />
62
+ )
63
+ }
@@ -0,0 +1,9 @@
1
+ import { createRoot } from 'react-dom/client'
2
+ import 'golem-ui/styles.css'
3
+ import { App } from './app'
4
+
5
+ document.documentElement.style.height = '100%'
6
+ document.body.style.height = '100%'
7
+ document.body.style.margin = '0'
8
+ document.getElementById('root')!.style.height = '100%'
9
+ createRoot(document.getElementById('root')!).render(<App />)
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
@@ -0,0 +1,70 @@
1
+ import { execFileSync } from 'node:child_process';
2
+ import { existsSync } from 'node:fs';
3
+ import { fileURLToPath } from 'node:url';
4
+ import { resolve } from 'node:path';
5
+ import { build } from 'vite';
6
+ import { resolveUiSource } from '../vite.config.ts';
7
+
8
+ const frameworkRoot = resolve(fileURLToPath(new URL('..', import.meta.url)));
9
+
10
+ /**
11
+ * The one build boundary used by both `./golem build` and `./golem dev`.
12
+ * Typecheck runs before the Vite build on purpose: Vite's own `dist/` write only happens once
13
+ * its build succeeds, so checking types first means a type-only failure never touches `dist/`
14
+ * either, matching the same previous-build-preserved-on-failure guarantee a parse failure gets.
15
+ */
16
+ export async function buildBrowser(): Promise<void> {
17
+ const ui = resolveUiSource();
18
+ if (process.env.GOLEM_SOURCE) console.log(`Golem source: ${frameworkRoot} (${gitRevision(frameworkRoot)})`);
19
+ if (ui) {
20
+ console.log(`Golem source: ${frameworkRoot} (${gitRevision(frameworkRoot)})`);
21
+ console.log(`golem-ui source: ${ui.root} (${ui.revision})`);
22
+ }
23
+ const tsc = resolve(frameworkRoot, 'node_modules/.bin/tsc');
24
+ const typeRoots = existsSync(resolve(frameworkRoot, 'node_modules/@types'))
25
+ ? resolve(frameworkRoot, 'node_modules/@types')
26
+ : resolve(frameworkRoot, '../@types');
27
+ execFileSync(tsc, ['--noEmit', '-p', resolve(frameworkRoot, 'tsconfig.json')], {
28
+ cwd: process.cwd(),
29
+ stdio: 'inherit',
30
+ });
31
+ execFileSync(tsc, [
32
+ '--ignoreConfig', '--noEmit', '--jsx', 'react-jsx', '--module', 'ESNext',
33
+ '--moduleResolution', 'Bundler', '--skipLibCheck', '--types', 'node,react,react-dom',
34
+ '--typeRoots', typeRoots,
35
+ resolve(process.cwd(), 'src/app.tsx'), resolve(process.cwd(), 'golem.config.ts'),
36
+ ], { cwd: process.cwd(), stdio: 'inherit' });
37
+ await build({ configFile: resolve(frameworkRoot, 'vite.config.ts') });
38
+ }
39
+
40
+ let buildInFlight: Promise<void> | undefined
41
+ let queued = false
42
+
43
+ /**
44
+ * Server-owned rebuild trigger for after a build-mode turn. Coalesces overlapping calls into
45
+ * one rerun instead of racing concurrent `buildBrowser()` invocations.
46
+ */
47
+ export function rebuild(): Promise<void> {
48
+ if (buildInFlight) { queued = true; return buildInFlight }
49
+ buildInFlight = runQueuedBuilds()
50
+ return buildInFlight
51
+ }
52
+
53
+ async function runQueuedBuilds(): Promise<void> {
54
+ try {
55
+ do {
56
+ queued = false
57
+ await buildBrowser()
58
+ } while (queued)
59
+ } finally {
60
+ buildInFlight = undefined
61
+ }
62
+ }
63
+
64
+ function gitRevision(root: string): string {
65
+ try {
66
+ return execFileSync('git', ['-C', root, 'rev-parse', '--short', 'HEAD'], { encoding: 'utf8' }).trim();
67
+ } catch {
68
+ return 'unavailable';
69
+ }
70
+ }