@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.
- package/LICENSE +201 -0
- package/README.md +63 -0
- package/dist/dev-server/auth.d.ts +44 -0
- package/dist/dev-server/auth.d.ts.map +1 -0
- package/dist/dev-server/auth.js +176 -0
- package/dist/dev-server/events.d.ts +43 -0
- package/dist/dev-server/events.d.ts.map +1 -0
- package/dist/dev-server/events.js +61 -0
- package/dist/dev-server/http.d.ts +79 -0
- package/dist/dev-server/http.d.ts.map +1 -0
- package/dist/dev-server/http.js +277 -0
- package/dist/dev-server/hub-preview.d.ts +56 -0
- package/dist/dev-server/hub-preview.d.ts.map +1 -0
- package/dist/dev-server/hub-preview.js +237 -0
- package/dist/dev-server/hub.d.ts +59 -0
- package/dist/dev-server/hub.d.ts.map +1 -0
- package/dist/dev-server/hub.js +723 -0
- package/dist/hub-preview/client.js +52346 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/local-registry/compile-ui.d.ts +67 -0
- package/dist/local-registry/compile-ui.d.ts.map +1 -0
- package/dist/local-registry/compile-ui.js +148 -0
- package/dist/local-registry/local-registry.d.ts +93 -0
- package/dist/local-registry/local-registry.d.ts.map +1 -0
- package/dist/local-registry/local-registry.js +272 -0
- package/dist/local-registry/watcher.d.ts +42 -0
- package/dist/local-registry/watcher.d.ts.map +1 -0
- package/dist/local-registry/watcher.js +208 -0
- package/dist/run-dev.d.ts +147 -0
- package/dist/run-dev.d.ts.map +1 -0
- package/dist/run-dev.js +216 -0
- package/dist/runtime-supervisor.d.ts +140 -0
- package/dist/runtime-supervisor.d.ts.map +1 -0
- package/dist/runtime-supervisor.js +91 -0
- package/package.json +72 -0
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import type { UiRegistryEvent } from '@ggui-ai/ui-registry';
|
|
2
|
+
/**
|
|
3
|
+
* Listener for raw events the watcher emits. Delivery is
|
|
4
|
+
* at-least-once — consumers dedupe where needed (e.g. the SSE
|
|
5
|
+
* handler just forwards; Studio's subscription logic bumps a
|
|
6
|
+
* counter and doesn't care about dupes).
|
|
7
|
+
*/
|
|
8
|
+
export type WatchListener = (event: UiRegistryEvent) => void;
|
|
9
|
+
export interface LocalWatcherOptions {
|
|
10
|
+
/** Absolute project root — chokidar's root path. */
|
|
11
|
+
projectRoot: string;
|
|
12
|
+
/**
|
|
13
|
+
* Mapping from absolute manifest directory → current UI id.
|
|
14
|
+
* The watcher looks changed files up by walking up directories
|
|
15
|
+
* until it hits one of these paths.
|
|
16
|
+
*/
|
|
17
|
+
getManifestDirIndex: () => Map<string, string>;
|
|
18
|
+
/**
|
|
19
|
+
* Called when a `ggui.ui.json` is added or removed so the
|
|
20
|
+
* registry can re-run discovery. Any id/dir index
|
|
21
|
+
* changes it produces propagate on the next fs event.
|
|
22
|
+
*/
|
|
23
|
+
onManifestsChanged: () => Promise<void>;
|
|
24
|
+
}
|
|
25
|
+
export interface LocalWatcher {
|
|
26
|
+
/** Register a listener. Returns an unsubscribe function. */
|
|
27
|
+
subscribe(listener: WatchListener): () => void;
|
|
28
|
+
/** Start watching if not already. Safe to call repeatedly. */
|
|
29
|
+
start(): Promise<void>;
|
|
30
|
+
/** Stop the watcher and drop listeners. */
|
|
31
|
+
close(): Promise<void>;
|
|
32
|
+
}
|
|
33
|
+
export declare function createLocalWatcher(options: LocalWatcherOptions): LocalWatcher;
|
|
34
|
+
/**
|
|
35
|
+
* Utility used by tests and the registry — `fs.stat` with a
|
|
36
|
+
* `null` on missing-file so callers can compare mtimes without
|
|
37
|
+
* branching on ENOENT.
|
|
38
|
+
*/
|
|
39
|
+
export declare function statOrNull(path: string): Promise<{
|
|
40
|
+
mtimeMs: number;
|
|
41
|
+
} | null>;
|
|
42
|
+
//# sourceMappingURL=watcher.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"watcher.d.ts","sourceRoot":"","sources":["../../src/local-registry/watcher.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE5D;;;;;GAKG;AACH,MAAM,MAAM,aAAa,GAAG,CAAC,KAAK,EAAE,eAAe,KAAK,IAAI,CAAC;AAE7D,MAAM,WAAW,mBAAmB;IAClC,oDAAoD;IACpD,WAAW,EAAE,MAAM,CAAC;IACpB;;;;OAIG;IACH,mBAAmB,EAAE,MAAM,GAAG,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/C;;;;OAIG;IACH,kBAAkB,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CACzC;AAED,MAAM,WAAW,YAAY;IAC3B,4DAA4D;IAC5D,SAAS,CAAC,QAAQ,EAAE,aAAa,GAAG,MAAM,IAAI,CAAC;IAC/C,8DAA8D;IAC9D,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,2CAA2C;IAC3C,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACxB;AAUD,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,mBAAmB,GAAG,YAAY,CA0K7E;AAED;;;;GAIG;AACH,wBAAsB,UAAU,CAC9B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,GAAG,IAAI,CAAC,CAOrC"}
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Filesystem watcher for the local `ggui dev` registry.
|
|
3
|
+
*
|
|
4
|
+
* Maps raw chokidar events to the contract's `UiRegistryEvent`
|
|
5
|
+
* union (`added` / `changed` / `removed`). The watcher is scoped
|
|
6
|
+
* to what actually affects what Studio can see:
|
|
7
|
+
*
|
|
8
|
+
* - Any `ggui.ui.json` under the project tree → affects the
|
|
9
|
+
* manifest itself. A new one means `added`, a deletion
|
|
10
|
+
* means `removed`, an edit means `changed`.
|
|
11
|
+
* - Any `.tsx` / `.ts` / `.jsx` / `.js` file in the same
|
|
12
|
+
* directory as a known manifest (or a descendant of it)
|
|
13
|
+
* → affects the compiled bundle for that manifest's id.
|
|
14
|
+
* Emitted as `changed { id }`.
|
|
15
|
+
* - Any colocated `ggui.ui.js` / `ggui.ui.mjs` change →
|
|
16
|
+
* also `changed { id }` (the user's own build step ran).
|
|
17
|
+
*
|
|
18
|
+
* Scope deliberately narrow: we don't try to resolve cross-dir
|
|
19
|
+
* imports. A shared utility in `packages/shared/` won't trigger
|
|
20
|
+
* a refresh. That's a known limitation — re-importing the module
|
|
21
|
+
* graph from esbuild metafiles is a bigger slice. For the dev
|
|
22
|
+
* loop, edit the UI file, get the update. Shared-utility edits
|
|
23
|
+
* still work on the next manual reload.
|
|
24
|
+
*/
|
|
25
|
+
import { stat } from 'node:fs/promises';
|
|
26
|
+
import { dirname, relative, resolve, sep } from 'node:path';
|
|
27
|
+
import { watch as chokidarWatch } from 'chokidar';
|
|
28
|
+
/** Paths that should never trigger discovery / recompile work. */
|
|
29
|
+
const IGNORED_SEGMENTS = ['node_modules', '.git', 'dist', '.turbo', '.next', '.ggui-cache'];
|
|
30
|
+
/** Filename suffixes we care about for compile invalidation. */
|
|
31
|
+
const SOURCE_SUFFIXES = ['.tsx', '.ts', '.jsx', '.js', '.mjs', '.cjs', '.css'];
|
|
32
|
+
const MANIFEST_FILENAME = 'ggui.ui.json';
|
|
33
|
+
export function createLocalWatcher(options) {
|
|
34
|
+
const listeners = new Set();
|
|
35
|
+
let watcher = null;
|
|
36
|
+
let started = false;
|
|
37
|
+
function emit(event) {
|
|
38
|
+
for (const listener of listeners) {
|
|
39
|
+
try {
|
|
40
|
+
listener(event);
|
|
41
|
+
}
|
|
42
|
+
catch {
|
|
43
|
+
// A bad listener mustn't starve other listeners / the
|
|
44
|
+
// watcher itself. Errors surface in caller's own scope.
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Resolve the UI id a changed filesystem path belongs to by
|
|
50
|
+
* walking up directories until a known manifest dir matches.
|
|
51
|
+
* Returns `null` when the path lies outside all watched UIs.
|
|
52
|
+
*/
|
|
53
|
+
function findOwningId(absolutePath) {
|
|
54
|
+
const index = options.getManifestDirIndex();
|
|
55
|
+
let dir = dirname(absolutePath);
|
|
56
|
+
const root = options.projectRoot;
|
|
57
|
+
// Prevent walking past project root — files above it can
|
|
58
|
+
// never belong to a project UI.
|
|
59
|
+
const normalizedRoot = resolve(root);
|
|
60
|
+
while (dir.length >= normalizedRoot.length) {
|
|
61
|
+
const id = index.get(dir);
|
|
62
|
+
if (id !== undefined)
|
|
63
|
+
return id;
|
|
64
|
+
const parent = dirname(dir);
|
|
65
|
+
if (parent === dir)
|
|
66
|
+
break;
|
|
67
|
+
dir = parent;
|
|
68
|
+
}
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
function isInteresting(absolutePath) {
|
|
72
|
+
const rel = relative(options.projectRoot, absolutePath);
|
|
73
|
+
if (rel.startsWith('..'))
|
|
74
|
+
return false;
|
|
75
|
+
if (rel.split(sep).some((seg) => IGNORED_SEGMENTS.includes(seg)))
|
|
76
|
+
return false;
|
|
77
|
+
if (absolutePath.endsWith(sep + MANIFEST_FILENAME) || absolutePath.endsWith('/' + MANIFEST_FILENAME)) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
return SOURCE_SUFFIXES.some((suf) => absolutePath.endsWith(suf));
|
|
81
|
+
}
|
|
82
|
+
async function handleFsEvent(kind, absolutePath) {
|
|
83
|
+
if (!isInteresting(absolutePath))
|
|
84
|
+
return;
|
|
85
|
+
const isManifest = absolutePath.endsWith(sep + MANIFEST_FILENAME) ||
|
|
86
|
+
absolutePath.endsWith('/' + MANIFEST_FILENAME);
|
|
87
|
+
if (isManifest) {
|
|
88
|
+
// Let the registry re-scan, then decide what to emit from
|
|
89
|
+
// the updated index.
|
|
90
|
+
const before = new Set(options.getManifestDirIndex().values());
|
|
91
|
+
await options.onManifestsChanged();
|
|
92
|
+
const after = options.getManifestDirIndex();
|
|
93
|
+
const manifestDir = dirname(absolutePath);
|
|
94
|
+
const idForDir = after.get(manifestDir);
|
|
95
|
+
if (kind === 'unlink') {
|
|
96
|
+
// The manifest went away — find any id that was present
|
|
97
|
+
// before but isn't now and emit removed for each.
|
|
98
|
+
for (const id of before) {
|
|
99
|
+
if (!Array.from(after.values()).includes(id)) {
|
|
100
|
+
emit({ type: 'removed', id });
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (idForDir === undefined) {
|
|
106
|
+
// Manifest add/change that didn't produce a valid id
|
|
107
|
+
// (e.g., parse failure, duplicate-id conflict). Issues
|
|
108
|
+
// are collected by the registry; no event to emit.
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
if (!before.has(idForDir)) {
|
|
112
|
+
emit({ type: 'added', id: idForDir });
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
emit({ type: 'changed', id: idForDir, contentHash: '' });
|
|
116
|
+
}
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
// Non-manifest source file. Attribute it to the nearest
|
|
120
|
+
// manifest directory above.
|
|
121
|
+
const owningId = findOwningId(absolutePath);
|
|
122
|
+
if (owningId === null)
|
|
123
|
+
return;
|
|
124
|
+
emit({ type: 'changed', id: owningId, contentHash: '' });
|
|
125
|
+
}
|
|
126
|
+
async function start() {
|
|
127
|
+
if (started)
|
|
128
|
+
return;
|
|
129
|
+
started = true;
|
|
130
|
+
// chokidar v4 uses a path-match function for ignored. We
|
|
131
|
+
// filter in `handleFsEvent` too, but keeping the ignored
|
|
132
|
+
// list on the watcher avoids waking up for every stat on
|
|
133
|
+
// massive vendored trees.
|
|
134
|
+
watcher = chokidarWatch(options.projectRoot, {
|
|
135
|
+
ignored: (path) => {
|
|
136
|
+
const rel = relative(options.projectRoot, path);
|
|
137
|
+
if (rel === '' || rel === '.')
|
|
138
|
+
return false;
|
|
139
|
+
return rel.split(sep).some((seg) => IGNORED_SEGMENTS.includes(seg));
|
|
140
|
+
},
|
|
141
|
+
ignoreInitial: true,
|
|
142
|
+
persistent: true,
|
|
143
|
+
awaitWriteFinish: {
|
|
144
|
+
stabilityThreshold: 100,
|
|
145
|
+
pollInterval: 50,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
watcher.on('add', (p) => {
|
|
149
|
+
void handleFsEvent('add', p);
|
|
150
|
+
});
|
|
151
|
+
watcher.on('change', (p) => {
|
|
152
|
+
void handleFsEvent('change', p);
|
|
153
|
+
});
|
|
154
|
+
watcher.on('unlink', (p) => {
|
|
155
|
+
void handleFsEvent('unlink', p);
|
|
156
|
+
});
|
|
157
|
+
// Directory-level deletes bubble up through `unlink` for
|
|
158
|
+
// contained files; no special `unlinkDir` handling needed.
|
|
159
|
+
// Give chokidar a microtask to initialise before we return
|
|
160
|
+
// — many platforms emit a `ready` event, but awaiting that
|
|
161
|
+
// is implementation-specific. The watcher works synchronously
|
|
162
|
+
// enough that callers who depend on first-event ordering
|
|
163
|
+
// should use `awaitReady()` below.
|
|
164
|
+
await new Promise((ready) => {
|
|
165
|
+
const w = watcher;
|
|
166
|
+
if (!w)
|
|
167
|
+
return ready();
|
|
168
|
+
w.once('ready', () => ready());
|
|
169
|
+
// Safety timeout — extremely-slow fs init shouldn't block
|
|
170
|
+
// a CLI invocation forever.
|
|
171
|
+
setTimeout(ready, 500).unref?.();
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
return {
|
|
175
|
+
subscribe(listener) {
|
|
176
|
+
listeners.add(listener);
|
|
177
|
+
// Lazy-start on first subscribe keeps cost zero when
|
|
178
|
+
// nobody's watching.
|
|
179
|
+
void start();
|
|
180
|
+
return () => {
|
|
181
|
+
listeners.delete(listener);
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
start,
|
|
185
|
+
async close() {
|
|
186
|
+
listeners.clear();
|
|
187
|
+
if (watcher) {
|
|
188
|
+
await watcher.close();
|
|
189
|
+
watcher = null;
|
|
190
|
+
}
|
|
191
|
+
started = false;
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Utility used by tests and the registry — `fs.stat` with a
|
|
197
|
+
* `null` on missing-file so callers can compare mtimes without
|
|
198
|
+
* branching on ENOENT.
|
|
199
|
+
*/
|
|
200
|
+
export async function statOrNull(path) {
|
|
201
|
+
try {
|
|
202
|
+
const s = await stat(path);
|
|
203
|
+
return { mtimeMs: s.mtimeMs };
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return null;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { type DiscoveryResult } from '@ggui-ai/project-config/node';
|
|
2
|
+
import type { GguiJsonV1 } from '@ggui-ai/project-config';
|
|
3
|
+
import type { AgentRuntimeAdapter, AgentRuntimeHandle } from '@ggui-ai/agent-runtime';
|
|
4
|
+
import { LocalUiRegistry } from './local-registry/local-registry.js';
|
|
5
|
+
import { type DevServerHandle } from './dev-server/http.js';
|
|
6
|
+
import { RuntimeSupervisor } from './runtime-supervisor.js';
|
|
7
|
+
/** Default port the read-only dev registry binds to. */
|
|
8
|
+
export declare const DEFAULT_DEV_PORT = 6780;
|
|
9
|
+
/** Default bind address. Loopback-only so LAN peers can't reach the dev server by accident. */
|
|
10
|
+
export declare const DEFAULT_DEV_HOST = "127.0.0.1";
|
|
11
|
+
/**
|
|
12
|
+
* Options the command accepts. All optional — the zero-config call
|
|
13
|
+
* (`runDev({})`) uses `process.cwd()` and stdout.
|
|
14
|
+
*/
|
|
15
|
+
export interface DevOptions {
|
|
16
|
+
/**
|
|
17
|
+
* Where to start looking for `ggui.json`. Defaults to
|
|
18
|
+
* `process.cwd()`. Useful for tests and programmatic embeds.
|
|
19
|
+
*/
|
|
20
|
+
cwd?: string;
|
|
21
|
+
/**
|
|
22
|
+
* Writer for informational output. Defaults to `console.log`. The
|
|
23
|
+
* command never writes to stderr from success paths — errors
|
|
24
|
+
* throw so the caller decides how to render them.
|
|
25
|
+
*/
|
|
26
|
+
log?: (line: string) => void;
|
|
27
|
+
/**
|
|
28
|
+
* Don't start the HTTP dev server; just load + discover and
|
|
29
|
+
* return. Tests use this to keep the runner fast; the CLI leaves
|
|
30
|
+
* it off so `ggui dev` actually serves.
|
|
31
|
+
*/
|
|
32
|
+
serve?: boolean;
|
|
33
|
+
/** Bind port. Defaults to {@link DEFAULT_DEV_PORT}. `0` = OS-assigned. */
|
|
34
|
+
port?: number;
|
|
35
|
+
/** Bind host. Defaults to {@link DEFAULT_DEV_HOST} (loopback). */
|
|
36
|
+
host?: string;
|
|
37
|
+
/**
|
|
38
|
+
* Optional port the supervised agent runtime should bind. When
|
|
39
|
+
* provided AND `options.runtime` is supplied, the dev stack:
|
|
40
|
+
*
|
|
41
|
+
* 1. Sets `PORT=<runtimePort>` in the runtime's start env. This
|
|
42
|
+
* matches the convention every `ggui dev` agent template
|
|
43
|
+
* already respects (and Node's default `process.env.PORT`
|
|
44
|
+
* idiom).
|
|
45
|
+
* 2. Surfaces the same value as
|
|
46
|
+
* {@link AgentRuntimeStartInput.portHint} so adapters that
|
|
47
|
+
* don't read `PORT` directly can still honour it.
|
|
48
|
+
* 3. Echoes the chosen port on {@link DevBootstrap.runtimePort}
|
|
49
|
+
* so the CLI orchestration layer can forward it to a
|
|
50
|
+
* {@link TunnelProvider}.
|
|
51
|
+
*
|
|
52
|
+
* `undefined` preserves the current behaviour — the agent picks
|
|
53
|
+
* whatever port it wants, and the dev-stack doesn't care.
|
|
54
|
+
*
|
|
55
|
+
* This is narrow scope on purpose: the dev-stack only *forwards*
|
|
56
|
+
* the port; the CLI owns allocation + tunnel-side use.
|
|
57
|
+
*/
|
|
58
|
+
runtimePort?: number;
|
|
59
|
+
/**
|
|
60
|
+
* Optional agent runtime adapter. When present AND `serve !== false`,
|
|
61
|
+
* the dev stack calls `runtime.start(...)` after the HTTP registry
|
|
62
|
+
* is listening and tears it down on `server.close()`. The returned
|
|
63
|
+
* {@link DevBootstrap.runtime} exposes the live handle so callers
|
|
64
|
+
* (CLI banner, the hub, custom supervisors) can subscribe to
|
|
65
|
+
* status / log events.
|
|
66
|
+
*
|
|
67
|
+
* Leaving this out runs the UI loop only — the pre-supervision
|
|
68
|
+
* shape `ggui dev` has shipped. Framework-specific adapters
|
|
69
|
+
* (Claude Agent SDK, OpenAI Agents, etc.) live in their own
|
|
70
|
+
* packages; this seam is deliberately framework-neutral.
|
|
71
|
+
*/
|
|
72
|
+
runtime?: AgentRuntimeAdapter;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Result of a successful `ggui dev` bootstrap — the loaded manifest
|
|
76
|
+
* + absolute path to its source. Returned so tests (and future
|
|
77
|
+
* embedders) can drive the command without parsing stdout.
|
|
78
|
+
*/
|
|
79
|
+
export interface DevBootstrap {
|
|
80
|
+
manifestPath: string;
|
|
81
|
+
manifest: GguiJsonV1;
|
|
82
|
+
/**
|
|
83
|
+
* UIs discovered under `blueprints.include`. Malformed manifests
|
|
84
|
+
* show up in `discovery.issues`; the command still boots so the
|
|
85
|
+
* valid UIs are reachable.
|
|
86
|
+
*/
|
|
87
|
+
discovery: DiscoveryResult;
|
|
88
|
+
/**
|
|
89
|
+
* Local registry wrapping the discovered UIs. Callers can query
|
|
90
|
+
* `list()` / `get(id)` / `getBundle(id)` directly without going
|
|
91
|
+
* through HTTP — useful for tests and embedders.
|
|
92
|
+
*/
|
|
93
|
+
registry: LocalUiRegistry;
|
|
94
|
+
/**
|
|
95
|
+
* Running HTTP handle. Present iff `options.serve !== false`. Caller
|
|
96
|
+
* is responsible for shutting it down via `server.close()` —
|
|
97
|
+
* which also stops the supervised runtime when one is wired in.
|
|
98
|
+
*/
|
|
99
|
+
server: DevServerHandle | null;
|
|
100
|
+
/**
|
|
101
|
+
* Live agent runtime handle when `options.runtime` was supplied.
|
|
102
|
+
* `null` otherwise. Consumers MAY subscribe for status / log /
|
|
103
|
+
* error events. Ownership stays with `runDev`: calling
|
|
104
|
+
* `server.close()` stops the runtime before the HTTP socket
|
|
105
|
+
* closes so there are no half-state races.
|
|
106
|
+
*/
|
|
107
|
+
runtime: AgentRuntimeHandle | null;
|
|
108
|
+
/**
|
|
109
|
+
* Ring-buffered supervisor attached to {@link runtime}. Hosts
|
|
110
|
+
* (CLI banner refresh, the hub snapshot endpoint, HTTP status
|
|
111
|
+
* routes) call `runtimeSupervisor.snapshot()` for a consistent
|
|
112
|
+
* point-in-time view without having to subscribe themselves.
|
|
113
|
+
* `null` when no runtime was supplied OR the adapter's
|
|
114
|
+
* `start()` threw.
|
|
115
|
+
*/
|
|
116
|
+
runtimeSupervisor: RuntimeSupervisor | null;
|
|
117
|
+
/**
|
|
118
|
+
* The port the supervised agent runtime was asked to bind via
|
|
119
|
+
* {@link DevOptions.runtimePort}. Echoed here so CLI-layer
|
|
120
|
+
* orchestration (tunnel providers, banner renderers) can forward
|
|
121
|
+
* it to downstream consumers without re-reading DevOptions.
|
|
122
|
+
*
|
|
123
|
+
* `null` when {@link DevOptions.runtimePort} was not set, OR when
|
|
124
|
+
* `options.runtime` was absent (no agent to bind).
|
|
125
|
+
*
|
|
126
|
+
* Note: this is the *requested* port. The seam has no built-in
|
|
127
|
+
* way for an adapter to report back the port it actually bound
|
|
128
|
+
* on — adapters MUST honour `portHint` / `PORT` to keep this
|
|
129
|
+
* value honest. Framework-specific adapters that can't comply
|
|
130
|
+
* document that separately.
|
|
131
|
+
*/
|
|
132
|
+
runtimePort: number | null;
|
|
133
|
+
}
|
|
134
|
+
export declare class GguiDevError extends Error {
|
|
135
|
+
constructor(message: string);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Boot the local dev stack up to the "manifest loaded" milestone.
|
|
139
|
+
*
|
|
140
|
+
* Throws {@link GguiDevError} when no `ggui.json` is found in the
|
|
141
|
+
* walk. Re-throws the underlying `GguiJsonLoadError` from
|
|
142
|
+
* `@ggui-ai/project-config/node` when the file exists but fails to
|
|
143
|
+
* load — callers can inspect `.cause` (a `ZodError` for schema
|
|
144
|
+
* failures) for issue details.
|
|
145
|
+
*/
|
|
146
|
+
export declare function runDev(options?: DevOptions): Promise<DevBootstrap>;
|
|
147
|
+
//# sourceMappingURL=run-dev.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"run-dev.d.ts","sourceRoot":"","sources":["../src/run-dev.ts"],"names":[],"mappings":"AA6BA,OAAO,EAIL,KAAK,eAAe,EACrB,MAAM,8BAA8B,CAAC;AACtC,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAC1D,OAAO,KAAK,EACV,mBAAmB,EACnB,kBAAkB,EACnB,MAAM,wBAAwB,CAAC;AAChC,OAAO,EAAE,eAAe,EAAE,MAAM,oCAAoC,CAAC;AACrE,OAAO,EAAkB,KAAK,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAE5E,OAAO,EACL,iBAAiB,EAElB,MAAM,yBAAyB,CAAC;AAEjC,wDAAwD;AACxD,eAAO,MAAM,gBAAgB,OAAO,CAAC;AACrC,+FAA+F;AAC/F,eAAO,MAAM,gBAAgB,cAAc,CAAC;AAE5C;;;GAGG;AACH,MAAM,WAAW,UAAU;IACzB;;;OAGG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IAEb;;;;OAIG;IACH,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,IAAI,CAAC;IAE7B;;;;OAIG;IACH,KAAK,CAAC,EAAE,OAAO,CAAC;IAEhB,0EAA0E;IAC1E,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd,kEAAkE;IAClE,IAAI,CAAC,EAAE,MAAM,CAAC;IAEd;;;;;;;;;;;;;;;;;;;;OAoBG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IAErB;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,EAAE,mBAAmB,CAAC;CAC/B;AAED;;;;GAIG;AACH,MAAM,WAAW,YAAY;IAC3B,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,UAAU,CAAC;IACrB;;;;OAIG;IACH,SAAS,EAAE,eAAe,CAAC;IAC3B;;;;OAIG;IACH,QAAQ,EAAE,eAAe,CAAC;IAC1B;;;;OAIG;IACH,MAAM,EAAE,eAAe,GAAG,IAAI,CAAC;IAC/B;;;;;;OAMG;IACH,OAAO,EAAE,kBAAkB,GAAG,IAAI,CAAC;IACnC;;;;;;;OAOG;IACH,iBAAiB,EAAE,iBAAiB,GAAG,IAAI,CAAC;IAE5C;;;;;;;;;;;;;;OAcG;IACH,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,qBAAa,YAAa,SAAQ,KAAK;gBACzB,OAAO,EAAE,MAAM;CAI5B;AAED;;;;;;;;GAQG;AACH,wBAAsB,MAAM,CAAC,OAAO,GAAE,UAAe,GAAG,OAAO,CAAC,YAAY,CAAC,CAqK5E"}
|
package/dist/run-dev.js
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `runDev` — orchestration entry point for the shared open dev
|
|
3
|
+
* engine. The `@ggui-ai/cli` bin and any other host that wants to
|
|
4
|
+
* bring up the full local dev loop (registry + HTTP + SSE + auth)
|
|
5
|
+
* calls this function and keeps the returned handle alive.
|
|
6
|
+
*
|
|
7
|
+
* Responsibilities:
|
|
8
|
+
*
|
|
9
|
+
* 1. Find the project's `ggui.json` by walking up from `cwd`.
|
|
10
|
+
* 2. Load + validate it via `@ggui-ai/project-config/node`.
|
|
11
|
+
* 3. Discover the UIs under `blueprints.include`.
|
|
12
|
+
* 4. Build a `LocalUiRegistry` and (optionally) start the HTTP
|
|
13
|
+
* server with a bearer-gated policy.
|
|
14
|
+
* 5. Print a concrete summary so the developer can confirm what
|
|
15
|
+
* the engine actually sees.
|
|
16
|
+
*
|
|
17
|
+
* Optional: when the caller passes an
|
|
18
|
+
* {@link AgentRuntimeAdapter} via `options.runtime`, the dev stack
|
|
19
|
+
* calls `runtime.start(...)` after the HTTP server is listening
|
|
20
|
+
* and ties the runtime's lifetime to the server's close. No agent
|
|
21
|
+
* framework is hardcoded — the seam is framework-neutral.
|
|
22
|
+
*
|
|
23
|
+
* Kept deliberately narrow: the host (CLI, future `ggui dev
|
|
24
|
+
* --tunnel`, the local hub) owns arg parsing, log formatting beyond
|
|
25
|
+
* the baseline summary, and the decision of which runtime adapter
|
|
26
|
+
* to supply. `runDev` is the single seam every local-dev host
|
|
27
|
+
* composes against.
|
|
28
|
+
*/
|
|
29
|
+
import { dirname, resolve } from 'node:path';
|
|
30
|
+
import { discoverFromGguiJsonPath, findGguiJson, loadGguiJson, } from '@ggui-ai/project-config/node';
|
|
31
|
+
import { LocalUiRegistry } from './local-registry/local-registry.js';
|
|
32
|
+
import { startDevServer } from './dev-server/http.js';
|
|
33
|
+
import { createSecurityPolicy } from './dev-server/auth.js';
|
|
34
|
+
import { RuntimeSupervisor, formatRuntimeEventLine, } from './runtime-supervisor.js';
|
|
35
|
+
/** Default port the read-only dev registry binds to. */
|
|
36
|
+
export const DEFAULT_DEV_PORT = 6780;
|
|
37
|
+
/** Default bind address. Loopback-only so LAN peers can't reach the dev server by accident. */
|
|
38
|
+
export const DEFAULT_DEV_HOST = '127.0.0.1';
|
|
39
|
+
export class GguiDevError extends Error {
|
|
40
|
+
constructor(message) {
|
|
41
|
+
super(message);
|
|
42
|
+
this.name = 'GguiDevError';
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Boot the local dev stack up to the "manifest loaded" milestone.
|
|
47
|
+
*
|
|
48
|
+
* Throws {@link GguiDevError} when no `ggui.json` is found in the
|
|
49
|
+
* walk. Re-throws the underlying `GguiJsonLoadError` from
|
|
50
|
+
* `@ggui-ai/project-config/node` when the file exists but fails to
|
|
51
|
+
* load — callers can inspect `.cause` (a `ZodError` for schema
|
|
52
|
+
* failures) for issue details.
|
|
53
|
+
*/
|
|
54
|
+
export async function runDev(options = {}) {
|
|
55
|
+
const cwd = options.cwd ?? process.cwd();
|
|
56
|
+
const log = options.log ?? ((line) => process.stdout.write(`${line}\n`));
|
|
57
|
+
const manifestPath = findGguiJson(cwd);
|
|
58
|
+
if (!manifestPath) {
|
|
59
|
+
throw new GguiDevError(`No ggui.json found walking up from ${cwd}. ` +
|
|
60
|
+
`Run this command from inside a ggui project, or create a ggui.json at the project root.`);
|
|
61
|
+
}
|
|
62
|
+
const manifest = loadGguiJson(manifestPath);
|
|
63
|
+
log(`ggui dev — ${manifest.app.name} (${manifest.app.slug})`);
|
|
64
|
+
log(` manifest: ${manifestPath}`);
|
|
65
|
+
log(` protocol: ${manifest.protocol}`);
|
|
66
|
+
log(` blueprints.include: ${describeList(manifest.blueprints.include)}`);
|
|
67
|
+
log(` primitives.packages: ${describeList(manifest.primitives.packages)}`);
|
|
68
|
+
log(` primitives.local: ${describeList(manifest.primitives.local)}`);
|
|
69
|
+
log(` theme: ${manifest.theme ?? '(default tokens)'}`);
|
|
70
|
+
const discovery = await discoverFromGguiJsonPath(manifestPath, manifest);
|
|
71
|
+
if (discovery.uis.length === 0 && discovery.issues.length === 0) {
|
|
72
|
+
log(` uis: (none discovered)`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
log(` uis: ${discovery.uis.length} discovered`);
|
|
76
|
+
for (const ui of discovery.uis) {
|
|
77
|
+
log(` - ${ui.id} (${ui.manifest.name})`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (discovery.issues.length > 0) {
|
|
81
|
+
log(` issues: ${discovery.issues.length}`);
|
|
82
|
+
for (const issue of discovery.issues) {
|
|
83
|
+
log(` ! ${issue.path}: ${issue.message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// LocalUiRegistry re-runs discovery inside `refresh()`; reuse the
|
|
87
|
+
// current result by handing it a ready state rather than doing the
|
|
88
|
+
// same walk twice. Simplest: construct + refresh. Duplication cost
|
|
89
|
+
// is one extra walk on startup, which is cheap and keeps the
|
|
90
|
+
// registry's invariants (single source of truth) intact.
|
|
91
|
+
const projectRoot = dirname(resolve(manifestPath));
|
|
92
|
+
const registry = new LocalUiRegistry({ projectRoot, manifest });
|
|
93
|
+
await registry.refresh();
|
|
94
|
+
let server = null;
|
|
95
|
+
let runtime = null;
|
|
96
|
+
let runtimeSupervisor = null;
|
|
97
|
+
if (options.serve !== false) {
|
|
98
|
+
const security = createSecurityPolicy();
|
|
99
|
+
server = await startDevServer({
|
|
100
|
+
registry,
|
|
101
|
+
manifest,
|
|
102
|
+
port: options.port ?? DEFAULT_DEV_PORT,
|
|
103
|
+
host: options.host ?? DEFAULT_DEV_HOST,
|
|
104
|
+
security,
|
|
105
|
+
// Late-binding accessor so `/runtime/*` sees a supervisor
|
|
106
|
+
// that attaches AFTER the HTTP server is listening (current
|
|
107
|
+
// ordering, by design — server up first so the developer
|
|
108
|
+
// can reach `/health` even if the agent fails to boot).
|
|
109
|
+
getRuntimeSupervisor: () => runtimeSupervisor,
|
|
110
|
+
});
|
|
111
|
+
log(` registry: http://${server.host}:${server.port}`);
|
|
112
|
+
log(` GET /health (no auth)`);
|
|
113
|
+
log(` GET /hub (no auth — local dev dashboard)`);
|
|
114
|
+
log(` GET /hub/preview?ui=<id> (no auth — preview iframe)`);
|
|
115
|
+
log(` GET /uis (Authorization: Bearer ...)`);
|
|
116
|
+
log(` GET /uis/:id (Authorization: Bearer ...)`);
|
|
117
|
+
log(` GET /uis/:id/bundle (Authorization: Bearer ...)`);
|
|
118
|
+
log(` GET /events (SSE, Authorization: Bearer ...)`);
|
|
119
|
+
if (security.tokenGenerated) {
|
|
120
|
+
log(` token (generated): ${security.token}`);
|
|
121
|
+
log(` export GGUI_DEV_TOKEN=${security.token}`);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
log(` token: (from GGUI_DEV_TOKEN — not echoed)`);
|
|
125
|
+
}
|
|
126
|
+
if (options.runtime) {
|
|
127
|
+
// Supervision is additive — if `start()` throws we surface the
|
|
128
|
+
// error but leave the HTTP server up so the developer can still
|
|
129
|
+
// read the logged cause. Callers that want strict start-or-fail
|
|
130
|
+
// wrap `runDev` and close the server on rejection.
|
|
131
|
+
try {
|
|
132
|
+
runtime = await options.runtime.start({
|
|
133
|
+
projectRoot,
|
|
134
|
+
project: {
|
|
135
|
+
slug: manifest.app.slug,
|
|
136
|
+
name: manifest.app.name,
|
|
137
|
+
protocol: manifest.protocol,
|
|
138
|
+
},
|
|
139
|
+
// Forward the caller-requested runtime port two ways:
|
|
140
|
+
// - `portHint` — honoured by `@ggui-ai/agent-runtime`'s
|
|
141
|
+
// process adapter and any adapter that reads the seam's
|
|
142
|
+
// own port field.
|
|
143
|
+
// - `env.PORT` — honoured by the broad Node / framework
|
|
144
|
+
// convention. Matches what `ggui dev` and its closed-CLI
|
|
145
|
+
// predecessor have always passed to the supervised agent.
|
|
146
|
+
//
|
|
147
|
+
// An adapter that respects EITHER path will bind on the
|
|
148
|
+
// right port. An adapter that respects NEITHER is a
|
|
149
|
+
// framework-specific bug, not a dev-stack concern.
|
|
150
|
+
...(options.runtimePort !== undefined
|
|
151
|
+
? {
|
|
152
|
+
portHint: options.runtimePort,
|
|
153
|
+
env: { PORT: String(options.runtimePort) },
|
|
154
|
+
}
|
|
155
|
+
: {}),
|
|
156
|
+
});
|
|
157
|
+
log(` runtime: ${options.runtime.name} (${runtime.runId})`);
|
|
158
|
+
log(` GET /runtime/status (Authorization: Bearer ...)`);
|
|
159
|
+
log(` GET /runtime/events (Authorization: Bearer ...)`);
|
|
160
|
+
// Attach the supervisor AFTER we've logged the banner line
|
|
161
|
+
// so event-line forwarding doesn't race the header. The
|
|
162
|
+
// supervisor keeps a bounded ring of events for any host
|
|
163
|
+
// that wants a snapshot (CLI status refresh, the dev hub,
|
|
164
|
+
// the HTTP `/runtime/...` surface).
|
|
165
|
+
runtimeSupervisor = new RuntimeSupervisor({
|
|
166
|
+
adapter: options.runtime,
|
|
167
|
+
handle: runtime,
|
|
168
|
+
onEvent: (event) => log(` ${formatRuntimeEventLine(event)}`),
|
|
169
|
+
});
|
|
170
|
+
// Tie the runtime's lifetime to the server's close. The
|
|
171
|
+
// server's existing `close()` already sequences registry
|
|
172
|
+
// shutdown + socket teardown; we pre-sequence runtime stop
|
|
173
|
+
// in front of both so a runtime holding open sockets / child
|
|
174
|
+
// processes has a chance to exit cleanly before the dev
|
|
175
|
+
// server terminates its own connections.
|
|
176
|
+
const innerClose = server.close.bind(server);
|
|
177
|
+
server.close = async () => {
|
|
178
|
+
if (runtime) {
|
|
179
|
+
try {
|
|
180
|
+
await runtime.stop();
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// A runtime that fails to stop must not keep the HTTP
|
|
184
|
+
// server alive; swallow and continue. Callers that
|
|
185
|
+
// care can subscribe to the runtime's error events.
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
runtimeSupervisor?.close();
|
|
189
|
+
await innerClose();
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
194
|
+
log(` runtime: failed to start — ${message}`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return {
|
|
199
|
+
manifestPath,
|
|
200
|
+
manifest,
|
|
201
|
+
discovery,
|
|
202
|
+
registry,
|
|
203
|
+
server,
|
|
204
|
+
runtime,
|
|
205
|
+
runtimeSupervisor,
|
|
206
|
+
// Surface only when both a runtime was supplied AND the caller
|
|
207
|
+
// asked for a specific port. An absent runtime means nothing to
|
|
208
|
+
// bind; an unset `runtimePort` means the caller didn't care.
|
|
209
|
+
runtimePort: options.runtime && options.runtimePort !== undefined ? options.runtimePort : null,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
function describeList(items) {
|
|
213
|
+
if (items.length === 0)
|
|
214
|
+
return '(none)';
|
|
215
|
+
return items.join(', ');
|
|
216
|
+
}
|