@ggui-ai/dev-stack 0.1.0-rc.1

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.
Files changed (37) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +63 -0
  3. package/dist/dev-server/auth.d.ts +44 -0
  4. package/dist/dev-server/auth.d.ts.map +1 -0
  5. package/dist/dev-server/auth.js +176 -0
  6. package/dist/dev-server/events.d.ts +43 -0
  7. package/dist/dev-server/events.d.ts.map +1 -0
  8. package/dist/dev-server/events.js +61 -0
  9. package/dist/dev-server/http.d.ts +79 -0
  10. package/dist/dev-server/http.d.ts.map +1 -0
  11. package/dist/dev-server/http.js +277 -0
  12. package/dist/dev-server/hub-preview.d.ts +56 -0
  13. package/dist/dev-server/hub-preview.d.ts.map +1 -0
  14. package/dist/dev-server/hub-preview.js +237 -0
  15. package/dist/dev-server/hub.d.ts +59 -0
  16. package/dist/dev-server/hub.d.ts.map +1 -0
  17. package/dist/dev-server/hub.js +723 -0
  18. package/dist/hub-preview/client.js +52346 -0
  19. package/dist/index.d.ts +34 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +49 -0
  22. package/dist/local-registry/compile-ui.d.ts +67 -0
  23. package/dist/local-registry/compile-ui.d.ts.map +1 -0
  24. package/dist/local-registry/compile-ui.js +148 -0
  25. package/dist/local-registry/local-registry.d.ts +93 -0
  26. package/dist/local-registry/local-registry.d.ts.map +1 -0
  27. package/dist/local-registry/local-registry.js +272 -0
  28. package/dist/local-registry/watcher.d.ts +42 -0
  29. package/dist/local-registry/watcher.d.ts.map +1 -0
  30. package/dist/local-registry/watcher.js +208 -0
  31. package/dist/run-dev.d.ts +147 -0
  32. package/dist/run-dev.d.ts.map +1 -0
  33. package/dist/run-dev.js +216 -0
  34. package/dist/runtime-supervisor.d.ts +140 -0
  35. package/dist/runtime-supervisor.d.ts.map +1 -0
  36. package/dist/runtime-supervisor.js +91 -0
  37. package/package.json +72 -0
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `@ggui-ai/dev-stack` — shared open dev engine for the ggui
3
+ * protocol. Every CLI / host that runs the local dev loop
4
+ * (`ggui dev` today, future `ggui dev --tunnel`, the local hub,
5
+ * any closed-CLI compat shim) consumes this barrel.
6
+ *
7
+ * Layering:
8
+ *
9
+ * @ggui-ai/ui-registry — contract (types-only)
10
+ * @ggui-ai/project-config — manifest schema / loaders
11
+ * @ggui-ai/agent-runtime — agent runtime adapter seam
12
+ * │
13
+ * ▼
14
+ * @ggui-ai/dev-stack — THIS PACKAGE: local engine
15
+ * │
16
+ * ▼
17
+ * @ggui-ai/cli — `ggui` binary; thin shell
18
+ *
19
+ * Design rule: the agent framework is never hardcoded — one engine
20
+ * composes under every host, with the runtime supplied via an
21
+ * adapter seam.
22
+ */
23
+ export { LocalUiRegistry, type LocalUiRegistryOptions, type LocalBundleResult, type BundleErrorLocation, } from './local-registry/local-registry.js';
24
+ export { discoverLocalUis, discoverFromGguiJsonPath, type DiscoveredUi, type DiscoveryIssue, type DiscoveryResult, type DiscoverOptions, } from '@ggui-ai/project-config/node';
25
+ export { compileUiOnDemand, resolveEntryFile, COMPILED_BUNDLE_CONTENT_TYPE, type CompileResult, } from './local-registry/compile-ui.js';
26
+ export { createLocalWatcher, statOrNull, type LocalWatcher, type LocalWatcherOptions, type WatchListener, } from './local-registry/watcher.js';
27
+ export { startDevServer, type DevServerOptions, type DevServerHandle, } from './dev-server/http.js';
28
+ export { openEventStream, type SseOptions } from './dev-server/events.js';
29
+ export { renderHubHtml, serveHubShell, type HubShellContext, } from './dev-server/hub.js';
30
+ export { renderHubPreviewHtml, serveHubPreviewShell, serveHubPreviewBundle, resolveHubPreviewBundlePath, extractSelectedId, type HubPreviewShellContext, } from './dev-server/hub-preview.js';
31
+ export { createSecurityPolicy, type CreatePolicyOptions, type DevServerSecurityPolicy, type PolicyOutcome, } from './dev-server/auth.js';
32
+ export { runDev, GguiDevError, DEFAULT_DEV_PORT, DEFAULT_DEV_HOST, type DevOptions, type DevBootstrap, } from './run-dev.js';
33
+ export { RuntimeSupervisor, DEFAULT_RUNTIME_BUFFER_SIZE, emptyRuntimeSnapshot, formatRuntimeEventLine, type RuntimeSupervisorOptions, type RuntimeStateSnapshot, type EmptyRuntimeStateSnapshot, type AnyRuntimeStateSnapshot, type RuntimeEventRecord, } from './runtime-supervisor.js';
34
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAGH,OAAO,EACL,eAAe,EACf,KAAK,sBAAsB,EAC3B,KAAK,iBAAiB,EACtB,KAAK,mBAAmB,GACzB,MAAM,oCAAoC,CAAC;AAM5C,OAAO,EACL,gBAAgB,EAChB,wBAAwB,EACxB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,8BAA8B,CAAC;AAGtC,OAAO,EACL,iBAAiB,EACjB,gBAAgB,EAChB,4BAA4B,EAC5B,KAAK,aAAa,GACnB,MAAM,gCAAgC,CAAC;AAGxC,OAAO,EACL,kBAAkB,EAClB,UAAU,EACV,KAAK,YAAY,EACjB,KAAK,mBAAmB,EACxB,KAAK,aAAa,GACnB,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EACL,cAAc,EACd,KAAK,gBAAgB,EACrB,KAAK,eAAe,GACrB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EAAE,eAAe,EAAE,KAAK,UAAU,EAAE,MAAM,wBAAwB,CAAC;AAG1E,OAAO,EACL,aAAa,EACb,aAAa,EACb,KAAK,eAAe,GACrB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACrB,2BAA2B,EAC3B,iBAAiB,EACjB,KAAK,sBAAsB,GAC5B,MAAM,6BAA6B,CAAC;AAGrC,OAAO,EACL,oBAAoB,EACpB,KAAK,mBAAmB,EACxB,KAAK,uBAAuB,EAC5B,KAAK,aAAa,GACnB,MAAM,sBAAsB,CAAC;AAG9B,OAAO,EACL,MAAM,EACN,YAAY,EACZ,gBAAgB,EAChB,gBAAgB,EAChB,KAAK,UAAU,EACf,KAAK,YAAY,GAClB,MAAM,cAAc,CAAC;AAKtB,OAAO,EACL,iBAAiB,EACjB,2BAA2B,EAC3B,oBAAoB,EACpB,sBAAsB,EACtB,KAAK,wBAAwB,EAC7B,KAAK,oBAAoB,EACzB,KAAK,yBAAyB,EAC9B,KAAK,uBAAuB,EAC5B,KAAK,kBAAkB,GACxB,MAAM,yBAAyB,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,49 @@
1
+ /**
2
+ * `@ggui-ai/dev-stack` — shared open dev engine for the ggui
3
+ * protocol. Every CLI / host that runs the local dev loop
4
+ * (`ggui dev` today, future `ggui dev --tunnel`, the local hub,
5
+ * any closed-CLI compat shim) consumes this barrel.
6
+ *
7
+ * Layering:
8
+ *
9
+ * @ggui-ai/ui-registry — contract (types-only)
10
+ * @ggui-ai/project-config — manifest schema / loaders
11
+ * @ggui-ai/agent-runtime — agent runtime adapter seam
12
+ * │
13
+ * ▼
14
+ * @ggui-ai/dev-stack — THIS PACKAGE: local engine
15
+ * │
16
+ * ▼
17
+ * @ggui-ai/cli — `ggui` binary; thin shell
18
+ *
19
+ * Design rule: the agent framework is never hardcoded — one engine
20
+ * composes under every host, with the runtime supplied via an
21
+ * adapter seam.
22
+ */
23
+ // Local UI registry
24
+ export { LocalUiRegistry, } from './local-registry/local-registry.js';
25
+ // Discovery — re-export from @ggui-ai/project-config/node for
26
+ // consumers that have historically imported these symbols from the
27
+ // dev-stack barrel. The canonical home is the schema-owner package;
28
+ // dev-stack's `LocalUiRegistry` consumes the same helper directly.
29
+ export { discoverLocalUis, discoverFromGguiJsonPath, } from '@ggui-ai/project-config/node';
30
+ // Compile-on-demand
31
+ export { compileUiOnDemand, resolveEntryFile, COMPILED_BUNDLE_CONTENT_TYPE, } from './local-registry/compile-ui.js';
32
+ // Watcher
33
+ export { createLocalWatcher, statOrNull, } from './local-registry/watcher.js';
34
+ // HTTP dev server
35
+ export { startDevServer, } from './dev-server/http.js';
36
+ // SSE event stream
37
+ export { openEventStream } from './dev-server/events.js';
38
+ // Local dev hub — HTML shell served from the same server at `/hub`.
39
+ export { renderHubHtml, serveHubShell, } from './dev-server/hub.js';
40
+ // Local dev hub — preview iframe shell served at `/hub/preview`.
41
+ export { renderHubPreviewHtml, serveHubPreviewShell, serveHubPreviewBundle, resolveHubPreviewBundlePath, extractSelectedId, } from './dev-server/hub-preview.js';
42
+ // Auth / CORS policy
43
+ export { createSecurityPolicy, } from './dev-server/auth.js';
44
+ // Orchestration — the seam every local-dev host composes against.
45
+ export { runDev, GguiDevError, DEFAULT_DEV_PORT, DEFAULT_DEV_HOST, } from './run-dev.js';
46
+ // Runtime supervision — ring-buffered view of the supervised agent
47
+ // runtime, consumed by the CLI banner, the future hub, and the
48
+ // HTTP `/runtime/...` snapshot surface.
49
+ export { RuntimeSupervisor, DEFAULT_RUNTIME_BUFFER_SIZE, emptyRuntimeSnapshot, formatRuntimeEventLine, } from './runtime-supervisor.js';
@@ -0,0 +1,67 @@
1
+ import { type Message } from 'esbuild';
2
+ import type { UiManifest } from '@ggui-ai/project-config';
3
+ /** Content type the compiled bundle is served with. Matches the
4
+ * precompiled path for consistency. */
5
+ export declare const COMPILED_BUNDLE_CONTENT_TYPE = "application/javascript+react";
6
+ export interface CompileInputs {
7
+ /** Absolute project root — directory containing `ggui.json`. */
8
+ projectRoot: string;
9
+ /** Absolute path to the `ggui.ui.json` this compile is for. */
10
+ manifestPath: string;
11
+ /** Parsed manifest — `entryPoint` drives the compile target when present. */
12
+ manifest: UiManifest;
13
+ }
14
+ export interface CompileOk {
15
+ kind: 'ok';
16
+ /** Raw ESM code. */
17
+ code: string;
18
+ /** Absolute path compiled (the entry point). Useful for logs. */
19
+ entry: string;
20
+ /** esbuild info/warning messages, if any. */
21
+ warnings: Message[];
22
+ }
23
+ export interface CompileMissingEntry {
24
+ kind: 'missing-entry';
25
+ /**
26
+ * Which places were tried — absolute paths. Either the explicit
27
+ * `manifest.entryPoint` (single element) or the fallback filename
28
+ * candidates (in search order).
29
+ */
30
+ tried: string[];
31
+ }
32
+ export interface CompileFailure {
33
+ kind: 'failure';
34
+ entry: string;
35
+ errors: Message[];
36
+ warnings: Message[];
37
+ }
38
+ export type CompileResult = CompileOk | CompileMissingEntry | CompileFailure;
39
+ /**
40
+ * Resolve the entry file for a UI manifest.
41
+ *
42
+ * Precedence:
43
+ * 1. `manifest.entryPoint` (relative to `projectRoot`, or
44
+ * absolute). Single-candidate result.
45
+ * 2. Fallback: the {@link ENTRY_CANDIDATES} filenames beside the
46
+ * manifest. First hit wins.
47
+ *
48
+ * Returns the resolved absolute path, or `null` with the list of
49
+ * candidates that were tried (for the 404/missing-entry response).
50
+ */
51
+ export declare function resolveEntryFile(inputs: CompileInputs): {
52
+ entry: string;
53
+ } | {
54
+ tried: string[];
55
+ };
56
+ /**
57
+ * Compile a UI's TSX source to an ESM bundle. Returns a discriminated
58
+ * result — consumers branch on `kind` to render the right HTTP
59
+ * response / log.
60
+ *
61
+ * esbuild's programmatic build can throw on catastrophic failures
62
+ * (e.g. native binary missing). Those bubble up to the caller — the
63
+ * HTTP layer surfaces them as 500s since they are environment /
64
+ * install problems, not user-source problems.
65
+ */
66
+ export declare function compileUiOnDemand(inputs: CompileInputs): Promise<CompileResult>;
67
+ //# sourceMappingURL=compile-ui.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compile-ui.d.ts","sourceRoot":"","sources":["../../src/local-registry/compile-ui.ts"],"names":[],"mappings":"AAoCA,OAAO,EAA4B,KAAK,OAAO,EAAE,MAAM,SAAS,CAAC;AACjE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AA6B1D;uCACuC;AACvC,eAAO,MAAM,4BAA4B,iCAAiC,CAAC;AAE3E,MAAM,WAAW,aAAa;IAC5B,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,+DAA+D;IAC/D,YAAY,EAAE,MAAM,CAAC;IACrB,6EAA6E;IAC7E,QAAQ,EAAE,UAAU,CAAC;CACtB;AAED,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,IAAI,CAAC;IACX,oBAAoB;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IACd,6CAA6C;IAC7C,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,eAAe,CAAC;IACtB;;;;OAIG;IACH,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,SAAS,CAAC;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,EAAE,CAAC;IAClB,QAAQ,EAAE,OAAO,EAAE,CAAC;CACrB;AAED,MAAM,MAAM,aAAa,GAAG,SAAS,GAAG,mBAAmB,GAAG,cAAc,CAAC;AAE7E;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,aAAa,GAAG;IAAE,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,CAkB/F;AAED;;;;;;;;;GASG;AACH,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,OAAO,CAAC,aAAa,CAAC,CAqCrF"}
@@ -0,0 +1,148 @@
1
+ /**
2
+ * Compile-on-demand for local `ggui dev` UIs.
3
+ *
4
+ * Resolves the authored TSX entry for a `ggui.ui.json` and runs
5
+ * esbuild against it, producing an ESM bundle Studio can hand to
6
+ * `DynamicComponent`. This replaces the "must pre-compile" state
7
+ * the foundation slice left behind — `ggui dev` now feels like a
8
+ * real dev server: start it, open Studio, see the render.
9
+ *
10
+ * Trust model:
11
+ *
12
+ * The closed `core/src/validation/ui-compiler.ts` layers in
13
+ * `validateComponentDetailed` — security patterns (`eval`,
14
+ * `fetch`), import allowlist enforcement, size caps. Those exist
15
+ * because the cloud pipeline accepts user-uploaded TSX and runs
16
+ * it downstream. **Localhost `ggui dev` is NOT that pipeline.**
17
+ * The developer is compiling their own project's source on their
18
+ * own machine; a token-gated origin-allowlisted local server has
19
+ * the same trust as the developer's shell. No security
20
+ * validation here.
21
+ *
22
+ * Shape chosen for v1:
23
+ *
24
+ * - esbuild `build` mode with bundle: true, so multi-file UIs
25
+ * work (import sibling helpers, theme tokens, wire hooks).
26
+ * - Sandbox externals match the runtime Studio renderer
27
+ * (`DynamicComponent` in `@ggui-ai/react`). React, the design
28
+ * system, `@ggui-ai/wire`, and `@ggui-ai/react` stay external
29
+ * and resolve against the import map Studio provides at
30
+ * render time.
31
+ * - No caching. Every request recompiles. esbuild TSX transform
32
+ * is ~50-200ms for a typical single-screen UI — fine for the
33
+ * dev loop. A watcher-driven cache lands with HMR, not here.
34
+ */
35
+ import { existsSync } from 'node:fs';
36
+ import { dirname, isAbsolute, resolve } from 'node:path';
37
+ import { build } from 'esbuild';
38
+ /**
39
+ * Packages the Studio runtime renderer already resolves from its
40
+ * own bundle (via `@ggui-ai/design/module-loader`'s import-map).
41
+ * Keeping them out of the compiled bundle keeps the artifact small
42
+ * and lets Studio's in-tree React instance be shared across every
43
+ * rendered UI.
44
+ */
45
+ const SANDBOX_EXTERNALS = [
46
+ 'react',
47
+ 'react/*',
48
+ 'react-dom',
49
+ 'react-dom/*',
50
+ '@ggui-ai/design',
51
+ '@ggui-ai/design/*',
52
+ '@ggui-ai/wire',
53
+ '@ggui-ai/wire/*',
54
+ '@ggui-ai/react',
55
+ '@ggui-ai/react/*',
56
+ ];
57
+ /**
58
+ * Filename candidates for the entry point when the manifest
59
+ * doesn't declare one. Kept narrow so the convention is predictable
60
+ * rather than "anything with a `.tsx` extension."
61
+ */
62
+ const ENTRY_CANDIDATES = ['ggui.ui.tsx', 'index.tsx', 'component.tsx'];
63
+ /** Content type the compiled bundle is served with. Matches the
64
+ * precompiled path for consistency. */
65
+ export const COMPILED_BUNDLE_CONTENT_TYPE = 'application/javascript+react';
66
+ /**
67
+ * Resolve the entry file for a UI manifest.
68
+ *
69
+ * Precedence:
70
+ * 1. `manifest.entryPoint` (relative to `projectRoot`, or
71
+ * absolute). Single-candidate result.
72
+ * 2. Fallback: the {@link ENTRY_CANDIDATES} filenames beside the
73
+ * manifest. First hit wins.
74
+ *
75
+ * Returns the resolved absolute path, or `null` with the list of
76
+ * candidates that were tried (for the 404/missing-entry response).
77
+ */
78
+ export function resolveEntryFile(inputs) {
79
+ const { projectRoot, manifestPath, manifest } = inputs;
80
+ if (manifest.entryPoint && manifest.entryPoint.length > 0) {
81
+ const declared = isAbsolute(manifest.entryPoint)
82
+ ? manifest.entryPoint
83
+ : resolve(projectRoot, manifest.entryPoint);
84
+ return existsSync(declared) ? { entry: declared } : { tried: [declared] };
85
+ }
86
+ const manifestDir = dirname(manifestPath);
87
+ const tried = [];
88
+ for (const filename of ENTRY_CANDIDATES) {
89
+ const candidate = resolve(manifestDir, filename);
90
+ tried.push(candidate);
91
+ if (existsSync(candidate))
92
+ return { entry: candidate };
93
+ }
94
+ return { tried };
95
+ }
96
+ /**
97
+ * Compile a UI's TSX source to an ESM bundle. Returns a discriminated
98
+ * result — consumers branch on `kind` to render the right HTTP
99
+ * response / log.
100
+ *
101
+ * esbuild's programmatic build can throw on catastrophic failures
102
+ * (e.g. native binary missing). Those bubble up to the caller — the
103
+ * HTTP layer surfaces them as 500s since they are environment /
104
+ * install problems, not user-source problems.
105
+ */
106
+ export async function compileUiOnDemand(inputs) {
107
+ const resolved = resolveEntryFile(inputs);
108
+ if ('tried' in resolved) {
109
+ return { kind: 'missing-entry', tried: resolved.tried };
110
+ }
111
+ const entry = resolved.entry;
112
+ try {
113
+ const result = await build({
114
+ entryPoints: [entry],
115
+ bundle: true,
116
+ format: 'esm',
117
+ target: 'es2020',
118
+ jsx: 'automatic',
119
+ jsxImportSource: 'react',
120
+ minify: false,
121
+ write: false,
122
+ external: SANDBOX_EXTERNALS,
123
+ platform: 'browser',
124
+ absWorkingDir: inputs.projectRoot,
125
+ treeShaking: true,
126
+ logLevel: 'silent',
127
+ });
128
+ const code = result.outputFiles[0]?.text ?? '';
129
+ return { kind: 'ok', code, entry, warnings: result.warnings };
130
+ }
131
+ catch (err) {
132
+ if (isBuildFailure(err)) {
133
+ return {
134
+ kind: 'failure',
135
+ entry,
136
+ errors: err.errors,
137
+ warnings: err.warnings,
138
+ };
139
+ }
140
+ throw err;
141
+ }
142
+ }
143
+ function isBuildFailure(err) {
144
+ return (err !== null &&
145
+ typeof err === 'object' &&
146
+ Array.isArray(err.errors) &&
147
+ Array.isArray(err.warnings));
148
+ }
@@ -0,0 +1,93 @@
1
+ import type { UiBundle, UiManifestEntry, UiRegistry, UiRegistryCapabilities } from '@ggui-ai/ui-registry';
2
+ import { type DiscoveryIssue, type GguiJsonV1 } from '@ggui-ai/project-config/node';
3
+ import { type WatchListener } from './watcher.js';
4
+ export interface LocalUiRegistryOptions {
5
+ /** Absolute project root — directory containing `ggui.json`. */
6
+ projectRoot: string;
7
+ /** The parsed `ggui.json`. Only `blueprints.include` is consumed today. */
8
+ manifest: GguiJsonV1;
9
+ }
10
+ /**
11
+ * Richer bundle result — the HTTP layer branches on this to return
12
+ * the right status code / JSON body. `LocalUiRegistry.getBundle`
13
+ * collapses everything into `UiBundle | undefined` per the
14
+ * `@ggui-ai/ui-registry` contract.
15
+ */
16
+ export type LocalBundleResult = {
17
+ kind: 'ok';
18
+ bundle: UiBundle;
19
+ source: 'precompiled' | 'compiled';
20
+ } | {
21
+ kind: 'not-found';
22
+ } | {
23
+ kind: 'missing-entry';
24
+ tried: string[];
25
+ } | {
26
+ kind: 'compile-failed';
27
+ entry: string;
28
+ errors: Array<{
29
+ text: string;
30
+ location: BundleErrorLocation | null;
31
+ }>;
32
+ warnings: Array<{
33
+ text: string;
34
+ location: BundleErrorLocation | null;
35
+ }>;
36
+ };
37
+ /** Flat, wire-safe subset of esbuild's Location shape. */
38
+ export interface BundleErrorLocation {
39
+ file: string;
40
+ line: number;
41
+ column: number;
42
+ length: number;
43
+ lineText: string;
44
+ }
45
+ export declare class LocalUiRegistry implements UiRegistry {
46
+ readonly capabilities: UiRegistryCapabilities;
47
+ private readonly projectRoot;
48
+ private readonly manifest;
49
+ private entriesById;
50
+ /** Absolute manifest dir → id. Rebuilt on every refresh so the
51
+ * watcher can attribute source-file changes to the right UI. */
52
+ private manifestDirIndex;
53
+ private lastIssues;
54
+ private loaded;
55
+ private watcher;
56
+ constructor(options: LocalUiRegistryOptions);
57
+ /**
58
+ * Force a re-scan of the filesystem against `blueprints.include`.
59
+ * Use after edits when `subscribe` isn't wired yet. The HTTP layer
60
+ * can call this on a PUT `/uis/refresh` or similar — not part of
61
+ * this slice.
62
+ */
63
+ refresh(): Promise<{
64
+ uiCount: number;
65
+ issues: DiscoveryIssue[];
66
+ }>;
67
+ /**
68
+ * Subscribe to registry change events. Flips
69
+ * `capabilities.observable` from spec to reality. Starts the
70
+ * underlying filesystem watcher on first call and keeps it
71
+ * alive until all listeners unsubscribe AND {@link close} is
72
+ * invoked (lifetime controlled by the enclosing dev server,
73
+ * not by subscriber churn — SSE clients come and go).
74
+ */
75
+ subscribe(listener: WatchListener): () => void;
76
+ /** Shut down the watcher (if any) and drop listeners. */
77
+ close(): Promise<void>;
78
+ private ensureWatcher;
79
+ /** Diagnostic snapshot — NOT part of the `UiRegistry` surface. */
80
+ getIssues(): readonly DiscoveryIssue[];
81
+ list(): Promise<UiManifestEntry[]>;
82
+ get(id: string): Promise<UiManifestEntry | undefined>;
83
+ getBundle(id: string): Promise<UiBundle | undefined>;
84
+ /**
85
+ * Richer bundle fetch — exposes the distinct "no colocated
86
+ * artifact AND no TSX entry" vs. "compile failed" states that
87
+ * `getBundle` collapses. The HTTP layer uses this directly so
88
+ * failures become actionable 4xx / 422 responses.
89
+ */
90
+ fetchBundle(id: string): Promise<LocalBundleResult>;
91
+ private ensureLoaded;
92
+ }
93
+ //# sourceMappingURL=local-registry.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"local-registry.d.ts","sourceRoot":"","sources":["../../src/local-registry/local-registry.ts"],"names":[],"mappings":"AAmCA,OAAO,KAAK,EACV,QAAQ,EACR,eAAe,EACf,UAAU,EACV,sBAAsB,EACvB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAGL,KAAK,cAAc,EACnB,KAAK,UAAU,EAChB,MAAM,8BAA8B,CAAC;AAOtC,OAAO,EAAyC,KAAK,aAAa,EAAE,MAAM,cAAc,CAAC;AAQzF,MAAM,WAAW,sBAAsB;IACrC,gEAAgE;IAChE,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,QAAQ,EAAE,UAAU,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,MAAM,iBAAiB,GACzB;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,MAAM,EAAE,QAAQ,CAAC;IAAC,MAAM,EAAE,aAAa,GAAG,UAAU,CAAA;CAAE,GACpE;IAAE,IAAI,EAAE,WAAW,CAAA;CAAE,GACrB;IAAE,IAAI,EAAE,eAAe,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,GAC1C;IACE,IAAI,EAAE,gBAAgB,CAAC;IACvB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;IACtE,QAAQ,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CAAA;KAAE,CAAC,CAAC;CACzE,CAAC;AAEN,0DAA0D;AAC1D,MAAM,WAAW,mBAAmB;IAClC,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,qBAAa,eAAgB,YAAW,UAAU;IAChD,QAAQ,CAAC,YAAY,EAAE,sBAAsB,CAG3C;IAEF,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAa;IACtC,OAAO,CAAC,WAAW,CAAmC;IACtD;oEACgE;IAChE,OAAO,CAAC,gBAAgB,CAA6B;IACrD,OAAO,CAAC,UAAU,CAAwB;IAC1C,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,OAAO,CAA6B;gBAEhC,OAAO,EAAE,sBAAsB;IAK3C;;;;;OAKG;IACG,OAAO,IAAI,OAAO,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,cAAc,EAAE,CAAA;KAAE,CAAC;IAkBvE;;;;;;;OAOG;IACH,SAAS,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,IAAI;IAU9C,yDAAyD;IACnD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAO5B,OAAO,CAAC,aAAa;IAYrB,kEAAkE;IAClE,SAAS,IAAI,SAAS,cAAc,EAAE;IAIhC,IAAI,IAAI,OAAO,CAAC,eAAe,EAAE,CAAC;IAKlC,GAAG,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,GAAG,SAAS,CAAC;IAMrD,SAAS,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,QAAQ,GAAG,SAAS,CAAC;IAK1D;;;;;OAKG;IACG,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;YA0D3C,YAAY;CAK3B"}
@@ -0,0 +1,272 @@
1
+ /**
2
+ * `LocalUiRegistry` — the first real implementation of
3
+ * `@ggui-ai/ui-registry`'s `UiRegistry` contract.
4
+ *
5
+ * Scope (compile-on-demand slice, 2026-04-18):
6
+ *
7
+ * - Read-only. `capabilities.writable === false`;
8
+ * `capabilities.observable === false`.
9
+ * - Manifest-backed. Discovery runs against `ggui.json` +
10
+ * `blueprints.include` globs via `./discovery.js` and is
11
+ * cached in memory. `refresh()` re-runs discovery on demand;
12
+ * file watching / `subscribe()` lands with a follow-up
13
+ * HMR slice.
14
+ * - `getBundle(id)` resolution order:
15
+ * 1. **Precompiled fast-path** — colocated `ggui.ui.js` or
16
+ * `ggui.ui.mjs` beside the manifest. Lets a project with
17
+ * its own build step opt out of compile-on-demand.
18
+ * 2. **Compile-on-demand** — resolve the manifest's
19
+ * `entryPoint` (or a conventional `.tsx` filename beside
20
+ * the manifest), run esbuild against it, return the
21
+ * bundled ESM output. See `./compile-ui.ts`.
22
+ * 3. **Undefined** — the id is known to the registry but we
23
+ * could find neither a precompiled artifact nor a TSX
24
+ * entry. Per the `UiRegistry` contract, this is a valid
25
+ * "no bundle" state, not an error — callers render the
26
+ * "no preview available" affordance.
27
+ *
28
+ * Consumers that want to distinguish "missing entry" from "compile
29
+ * failed" need the richer {@link LocalBundleSource.fetchBundle}
30
+ * result — `getBundle` collapses both to the contract's
31
+ * `undefined` so the `UiRegistry` seam stays clean. The HTTP layer
32
+ * calls `fetchBundle` directly for actionable 4xx / 422 responses.
33
+ */
34
+ import { existsSync, readFileSync, statSync } from 'node:fs';
35
+ import { dirname, join } from 'node:path';
36
+ import { discoverLocalUis, } from '@ggui-ai/project-config/node';
37
+ import { compileUiOnDemand, COMPILED_BUNDLE_CONTENT_TYPE, resolveEntryFile, } from './compile-ui.js';
38
+ import { createLocalWatcher } from './watcher.js';
39
+ /** Accepted bundle filenames, in preference order. */
40
+ const BUNDLE_FILENAMES = ['ggui.ui.js', 'ggui.ui.mjs'];
41
+ /** Content-Type we claim for the compiled bundle. */
42
+ const BUNDLE_CONTENT_TYPE = 'application/javascript+react';
43
+ export class LocalUiRegistry {
44
+ capabilities = {
45
+ writable: false,
46
+ observable: true,
47
+ };
48
+ projectRoot;
49
+ manifest;
50
+ entriesById = new Map();
51
+ /** Absolute manifest dir → id. Rebuilt on every refresh so the
52
+ * watcher can attribute source-file changes to the right UI. */
53
+ manifestDirIndex = new Map();
54
+ lastIssues = [];
55
+ loaded = false;
56
+ watcher = null;
57
+ constructor(options) {
58
+ this.projectRoot = options.projectRoot;
59
+ this.manifest = options.manifest;
60
+ }
61
+ /**
62
+ * Force a re-scan of the filesystem against `blueprints.include`.
63
+ * Use after edits when `subscribe` isn't wired yet. The HTTP layer
64
+ * can call this on a PUT `/uis/refresh` or similar — not part of
65
+ * this slice.
66
+ */
67
+ async refresh() {
68
+ const result = await discoverLocalUis({
69
+ projectRoot: this.projectRoot,
70
+ manifest: this.manifest,
71
+ });
72
+ const next = new Map();
73
+ const dirIndex = new Map();
74
+ for (const ui of result.uis) {
75
+ next.set(ui.id, ui);
76
+ dirIndex.set(dirname(ui.manifestPath), ui.id);
77
+ }
78
+ this.entriesById = next;
79
+ this.manifestDirIndex = dirIndex;
80
+ this.lastIssues = result.issues;
81
+ this.loaded = true;
82
+ return { uiCount: result.uis.length, issues: result.issues };
83
+ }
84
+ /**
85
+ * Subscribe to registry change events. Flips
86
+ * `capabilities.observable` from spec to reality. Starts the
87
+ * underlying filesystem watcher on first call and keeps it
88
+ * alive until all listeners unsubscribe AND {@link close} is
89
+ * invoked (lifetime controlled by the enclosing dev server,
90
+ * not by subscriber churn — SSE clients come and go).
91
+ */
92
+ subscribe(listener) {
93
+ // Ensure the manifest dir index is populated before wiring the
94
+ // watcher — otherwise the first fs events arrive before
95
+ // `refresh()` has ever run and attribute-by-dir lookups return
96
+ // null. `ensureLoaded` is idempotent + cheap.
97
+ void this.ensureLoaded();
98
+ const watcher = this.ensureWatcher();
99
+ return watcher.subscribe(listener);
100
+ }
101
+ /** Shut down the watcher (if any) and drop listeners. */
102
+ async close() {
103
+ if (this.watcher) {
104
+ await this.watcher.close();
105
+ this.watcher = null;
106
+ }
107
+ }
108
+ ensureWatcher() {
109
+ if (this.watcher)
110
+ return this.watcher;
111
+ this.watcher = createLocalWatcher({
112
+ projectRoot: this.projectRoot,
113
+ getManifestDirIndex: () => this.manifestDirIndex,
114
+ onManifestsChanged: async () => {
115
+ await this.refresh();
116
+ },
117
+ });
118
+ return this.watcher;
119
+ }
120
+ /** Diagnostic snapshot — NOT part of the `UiRegistry` surface. */
121
+ getIssues() {
122
+ return this.lastIssues;
123
+ }
124
+ async list() {
125
+ await this.ensureLoaded();
126
+ return Array.from(this.entriesById.values()).map(toEntry);
127
+ }
128
+ async get(id) {
129
+ await this.ensureLoaded();
130
+ const ui = this.entriesById.get(id);
131
+ return ui ? toEntry(ui) : undefined;
132
+ }
133
+ async getBundle(id) {
134
+ const result = await this.fetchBundle(id);
135
+ return result.kind === 'ok' ? result.bundle : undefined;
136
+ }
137
+ /**
138
+ * Richer bundle fetch — exposes the distinct "no colocated
139
+ * artifact AND no TSX entry" vs. "compile failed" states that
140
+ * `getBundle` collapses. The HTTP layer uses this directly so
141
+ * failures become actionable 4xx / 422 responses.
142
+ */
143
+ async fetchBundle(id) {
144
+ await this.ensureLoaded();
145
+ const ui = this.entriesById.get(id);
146
+ if (!ui)
147
+ return { kind: 'not-found' };
148
+ // Freshness rule (2026-04-18):
149
+ // precompiled `.js` serves only if it's AT LEAST AS NEW as
150
+ // the resolved TSX entry. If the TSX is newer — the user just
151
+ // edited it — we recompile and ignore the stale precompiled
152
+ // artifact. Stateless, watcher-independent, always correct.
153
+ //
154
+ // Without this rule a developer's save of ggui.ui.tsx would be
155
+ // silently masked by an older ggui.ui.js sitting beside it —
156
+ // exactly the "edit TSX, still serves stale JS" failure mode
157
+ // the HMR slice must not leave behind.
158
+ const precompiledPath = findColocatedBundlePath(ui.manifestPath);
159
+ const entryResolution = resolveEntryFile({
160
+ projectRoot: this.projectRoot,
161
+ manifestPath: ui.manifestPath,
162
+ manifest: ui.manifest,
163
+ });
164
+ const entryPath = 'entry' in entryResolution ? entryResolution.entry : null;
165
+ if (precompiledPath && shouldPreferPrecompiled(precompiledPath, entryPath)) {
166
+ const code = readFileSync(precompiledPath, 'utf-8');
167
+ return {
168
+ kind: 'ok',
169
+ bundle: { code, contentType: BUNDLE_CONTENT_TYPE },
170
+ source: 'precompiled',
171
+ };
172
+ }
173
+ // Compile-on-demand from the authored TSX entry (or surface
174
+ // missing-entry if neither precompiled nor entry exists).
175
+ const compiled = await compileUiOnDemand({
176
+ projectRoot: this.projectRoot,
177
+ manifestPath: ui.manifestPath,
178
+ manifest: ui.manifest,
179
+ });
180
+ if (compiled.kind === 'ok') {
181
+ return {
182
+ kind: 'ok',
183
+ bundle: { code: compiled.code, contentType: COMPILED_BUNDLE_CONTENT_TYPE },
184
+ source: 'compiled',
185
+ };
186
+ }
187
+ if (compiled.kind === 'missing-entry') {
188
+ return { kind: 'missing-entry', tried: compiled.tried };
189
+ }
190
+ return {
191
+ kind: 'compile-failed',
192
+ entry: compiled.entry,
193
+ errors: compiled.errors.map(toBundleMessage),
194
+ warnings: compiled.warnings.map(toBundleMessage),
195
+ };
196
+ }
197
+ async ensureLoaded() {
198
+ if (!this.loaded) {
199
+ await this.refresh();
200
+ }
201
+ }
202
+ }
203
+ function toBundleMessage(msg) {
204
+ const location = msg.location
205
+ ? {
206
+ file: msg.location.file,
207
+ line: msg.location.line,
208
+ column: msg.location.column,
209
+ length: msg.location.length,
210
+ lineText: msg.location.lineText,
211
+ }
212
+ : null;
213
+ return { text: msg.text, location };
214
+ }
215
+ function toEntry(ui) {
216
+ // `manifest.contentHash` is optional per the schema — authored
217
+ // manifests typically omit it until a compile step fills it in.
218
+ // The registry contract requires a non-undefined `contentHash`
219
+ // string; empty signals "no artifact-version pin yet", which is
220
+ // correct for a dev-only source registry.
221
+ return {
222
+ id: ui.id,
223
+ contentHash: ui.manifest.contentHash ?? '',
224
+ manifest: ui.manifest,
225
+ };
226
+ }
227
+ /**
228
+ * Locate the colocated precompiled bundle next to a manifest,
229
+ * if any. Returns the first matching filename per
230
+ * {@link BUNDLE_FILENAMES} preference or null. Does NOT read the
231
+ * bytes — the freshness comparison runs first, and we only
232
+ * actually read the file when we've decided to serve it.
233
+ */
234
+ function findColocatedBundlePath(manifestPath) {
235
+ const dir = dirname(manifestPath);
236
+ for (const filename of BUNDLE_FILENAMES) {
237
+ const candidate = join(dir, filename);
238
+ if (existsSync(candidate))
239
+ return candidate;
240
+ }
241
+ return null;
242
+ }
243
+ /**
244
+ * Mtime-based freshness gate between a precompiled artifact and
245
+ * an authored TSX entry.
246
+ *
247
+ * - No TSX entry at all → precompiled wins (nothing newer could
248
+ * replace it; this is the pre-compile-on-demand case).
249
+ * - TSX exists and is newer than precompiled → recompile. The
250
+ * developer just edited; serving stale JS is the UX bug this
251
+ * rule exists to prevent.
252
+ * - Precompiled newer or equal → precompiled wins (user's own
253
+ * build step ran; honour it).
254
+ *
255
+ * mtime-compare is stateless, watcher-independent, and correct
256
+ * even when subscribers are not connected.
257
+ */
258
+ function shouldPreferPrecompiled(precompiledPath, entryPath) {
259
+ if (!entryPath)
260
+ return true;
261
+ try {
262
+ const pre = statSync(precompiledPath).mtimeMs;
263
+ const src = statSync(entryPath).mtimeMs;
264
+ return pre >= src;
265
+ }
266
+ catch {
267
+ // If a stat fails mid-check the safer call is to fall through
268
+ // to recompile — a compile-on-demand with a live entry file
269
+ // always produces fresh output.
270
+ return false;
271
+ }
272
+ }