@ggui-ai/project-config 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 +66 -0
- package/dist/discovery.d.ts +74 -0
- package/dist/discovery.d.ts.map +1 -0
- package/dist/discovery.js +167 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +15 -0
- package/dist/mcp-mount-discovery.d.ts +82 -0
- package/dist/mcp-mount-discovery.d.ts.map +1 -0
- package/dist/mcp-mount-discovery.js +226 -0
- package/dist/node.d.ts +69 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +157 -0
- package/dist/primitive-discovery.d.ts +95 -0
- package/dist/primitive-discovery.d.ts.map +1 -0
- package/dist/primitive-discovery.js +281 -0
- package/dist/primitives-manifest.d.ts +106 -0
- package/dist/primitives-manifest.d.ts.map +1 -0
- package/dist/primitives-manifest.js +131 -0
- package/dist/schema.d.ts +520 -0
- package/dist/schema.d.ts.map +1 -0
- package/dist/schema.js +556 -0
- package/dist/theme-loader.d.ts +104 -0
- package/dist/theme-loader.d.ts.map +1 -0
- package/dist/theme-loader.js +263 -0
- package/dist/theme.d.ts +311 -0
- package/dist/theme.d.ts.map +1 -0
- package/dist/theme.js +366 -0
- package/dist/ui-manifest.d.ts +95 -0
- package/dist/ui-manifest.d.ts.map +1 -0
- package/dist/ui-manifest.js +114 -0
- package/package.json +67 -0
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP mount discovery — turns a loaded `ggui.json#mcpMounts` list into
|
|
3
|
+
* a resolved, imported, factory-invoked set of mount payloads the
|
|
4
|
+
* OSS `ggui serve` path threads into
|
|
5
|
+
* `createGguiServer({ mcpMounts: ... })`.
|
|
6
|
+
*
|
|
7
|
+
* The flow per spec:
|
|
8
|
+
*
|
|
9
|
+
* string (spec)
|
|
10
|
+
* → resolve to absolute file path (relative → project root;
|
|
11
|
+
* bare → Node module resolver)
|
|
12
|
+
* → dynamic-import
|
|
13
|
+
* → pick `createGguiMcpMount` or default export
|
|
14
|
+
* → invoke (zero-arg; awaits Promise)
|
|
15
|
+
* → structural validate `{ name: string; handlers: array }`
|
|
16
|
+
*
|
|
17
|
+
* This module is **type-thin**: it does NOT import `McpServerMount`
|
|
18
|
+
* from `@ggui-ai/mcp-server`, keeping `@ggui-ai/project-config`'s dep
|
|
19
|
+
* graph honest (no reverse dep from config → server). The CLI
|
|
20
|
+
* (`@ggui-ai/cli`), which already pulls `@ggui-ai/mcp-server`,
|
|
21
|
+
* narrows the returned `handlers: unknown[]` to the concrete
|
|
22
|
+
* `SharedHandler[]` at the adapter boundary. Down-layer collision
|
|
23
|
+
* checks + per-handler validation happen inside
|
|
24
|
+
* `composeHandlersWithMounts` when `createGguiServer` runs.
|
|
25
|
+
*
|
|
26
|
+
* ## Error model
|
|
27
|
+
*
|
|
28
|
+
* Non-throwing per source — one malformed mount does not black-hole
|
|
29
|
+
* the rest of the list. Every failure mode surfaces as an entry in
|
|
30
|
+
* `issues`, with the declared spec on `.path` so renderers can group
|
|
31
|
+
* issues by source. Callers that need fail-fast (`ggui serve` boot)
|
|
32
|
+
* inspect `result.issues` themselves and escalate to a fatal exit.
|
|
33
|
+
* Mirrors the blueprint / primitive discovery split.
|
|
34
|
+
*
|
|
35
|
+
* ## Factory contract lock
|
|
36
|
+
*
|
|
37
|
+
* The resolved module must export either:
|
|
38
|
+
*
|
|
39
|
+
* - a named `createGguiMcpMount` function, OR
|
|
40
|
+
* - a default export that is a function.
|
|
41
|
+
*
|
|
42
|
+
* Named export wins if both are present. The function receives zero
|
|
43
|
+
* arguments today; passing options is a future additive (widening the
|
|
44
|
+
* `ggui.json#mcpMounts` entry from `string` to `string | { module,
|
|
45
|
+
* options? }` and threading `options` through here).
|
|
46
|
+
*
|
|
47
|
+
* Mount factories are responsible for their own backing state —
|
|
48
|
+
* instantiating stores, seeding fixtures, wiring adapters — so the
|
|
49
|
+
* operator's config stays data-only. The factory may be `async`.
|
|
50
|
+
*/
|
|
51
|
+
import { existsSync } from 'node:fs';
|
|
52
|
+
import { createRequire } from 'node:module';
|
|
53
|
+
import { isAbsolute, join, resolve } from 'node:path';
|
|
54
|
+
import { pathToFileURL } from 'node:url';
|
|
55
|
+
/**
|
|
56
|
+
* The export name callers should use on their mount module. A default
|
|
57
|
+
* export is also accepted when this named export is absent; the named
|
|
58
|
+
* form is preferred because it gives readers a visible symbol at the
|
|
59
|
+
* declaration site (`export function createGguiMcpMount() { ... }`).
|
|
60
|
+
*/
|
|
61
|
+
export const GGUI_MCP_MOUNT_FACTORY_EXPORT = 'createGguiMcpMount';
|
|
62
|
+
/**
|
|
63
|
+
* Walk `ggui.json#mcpMounts` and produce resolved + invoked mount
|
|
64
|
+
* payloads. Non-throwing per source; each failure lands on `issues`.
|
|
65
|
+
*
|
|
66
|
+
* Ordering: `mounts` preserves declaration order (so collision errors
|
|
67
|
+
* inside `composeHandlersWithMounts` deterministically blame the
|
|
68
|
+
* later-declared duplicate). `issues` likewise preserves declaration
|
|
69
|
+
* order so CLI renderers can print "issue #1 first" if they want.
|
|
70
|
+
*/
|
|
71
|
+
export async function discoverMcpMounts(options) {
|
|
72
|
+
const { projectRoot, manifest } = options;
|
|
73
|
+
if (!isAbsolute(projectRoot)) {
|
|
74
|
+
throw new Error(`discoverMcpMounts: projectRoot must be absolute, got ${projectRoot}`);
|
|
75
|
+
}
|
|
76
|
+
const specs = manifest.mcpMounts;
|
|
77
|
+
const mounts = [];
|
|
78
|
+
const issues = [];
|
|
79
|
+
if (specs.length === 0)
|
|
80
|
+
return { mounts, issues };
|
|
81
|
+
const resolveModule = options.resolveModule ?? makeDefaultResolver(projectRoot);
|
|
82
|
+
const importModule = options.importModule ?? ((url) => import(url));
|
|
83
|
+
for (const spec of specs) {
|
|
84
|
+
let modulePath;
|
|
85
|
+
try {
|
|
86
|
+
modulePath = resolveModule(spec, projectRoot);
|
|
87
|
+
}
|
|
88
|
+
catch (cause) {
|
|
89
|
+
issues.push({
|
|
90
|
+
path: spec,
|
|
91
|
+
message: `Could not resolve mcpMounts entry "${spec}" — is the file present or the package installed? ` +
|
|
92
|
+
`(${errorMessage(cause)})`,
|
|
93
|
+
cause,
|
|
94
|
+
});
|
|
95
|
+
continue;
|
|
96
|
+
}
|
|
97
|
+
let mod;
|
|
98
|
+
try {
|
|
99
|
+
mod = await importModule(pathToFileURL(modulePath).href);
|
|
100
|
+
}
|
|
101
|
+
catch (cause) {
|
|
102
|
+
issues.push({
|
|
103
|
+
path: spec,
|
|
104
|
+
message: `Failed to import mcpMounts entry "${spec}" (${modulePath}): ${errorMessage(cause)}`,
|
|
105
|
+
cause,
|
|
106
|
+
});
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
const factory = pickFactory(mod);
|
|
110
|
+
if (factory === null) {
|
|
111
|
+
issues.push({
|
|
112
|
+
path: spec,
|
|
113
|
+
message: `mcpMounts entry "${spec}" must export a \`${GGUI_MCP_MOUNT_FACTORY_EXPORT}\` function ` +
|
|
114
|
+
`(or a default export that is a function) returning { name, handlers }. Got: ${describeModule(mod)}.`,
|
|
115
|
+
});
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
let payload;
|
|
119
|
+
try {
|
|
120
|
+
payload = await factory();
|
|
121
|
+
}
|
|
122
|
+
catch (cause) {
|
|
123
|
+
issues.push({
|
|
124
|
+
path: spec,
|
|
125
|
+
message: `mcpMounts entry "${spec}" factory threw during invocation: ${errorMessage(cause)}`,
|
|
126
|
+
cause,
|
|
127
|
+
});
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
const validated = validateMountShape(payload);
|
|
131
|
+
if (!validated.ok) {
|
|
132
|
+
issues.push({
|
|
133
|
+
path: spec,
|
|
134
|
+
message: `mcpMounts entry "${spec}" factory returned an unexpected shape: ${validated.message}`,
|
|
135
|
+
});
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
mounts.push({
|
|
139
|
+
spec,
|
|
140
|
+
modulePath,
|
|
141
|
+
mount: validated.mount,
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
return { mounts, issues };
|
|
145
|
+
}
|
|
146
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
147
|
+
// Internals
|
|
148
|
+
// ─────────────────────────────────────────────────────────────────────
|
|
149
|
+
/**
|
|
150
|
+
* Default resolver: relative paths resolve filesystem-wise against
|
|
151
|
+
* the project root (with an `existsSync` gate so a missing file
|
|
152
|
+
* produces a sharp error message, not a noisy `createRequire` trace);
|
|
153
|
+
* bare specifiers resolve through `createRequire` anchored at the
|
|
154
|
+
* project root.
|
|
155
|
+
*/
|
|
156
|
+
function makeDefaultResolver(projectRoot) {
|
|
157
|
+
// Anchor at `<projectRoot>/package.json` — same convention the
|
|
158
|
+
// primitive resolver uses. `createRequire` doesn't require the
|
|
159
|
+
// anchor file to exist; Node uses the path only to establish the
|
|
160
|
+
// ancestor `node_modules/` resolution chain.
|
|
161
|
+
const originUrl = pathToFileURL(join(projectRoot, 'package.json')).href;
|
|
162
|
+
const req = createRequire(originUrl);
|
|
163
|
+
return (spec) => {
|
|
164
|
+
if (spec.startsWith('./') || spec.startsWith('../') || isAbsolute(spec)) {
|
|
165
|
+
const resolved = resolve(projectRoot, spec);
|
|
166
|
+
if (!existsSync(resolved)) {
|
|
167
|
+
throw new Error(`relative mcpMounts path "${spec}" resolved to ${resolved}, which does not exist`);
|
|
168
|
+
}
|
|
169
|
+
return resolved;
|
|
170
|
+
}
|
|
171
|
+
return req.resolve(spec);
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
function pickFactory(mod) {
|
|
175
|
+
if (mod === null || typeof mod !== 'object')
|
|
176
|
+
return null;
|
|
177
|
+
const record = mod;
|
|
178
|
+
const named = record[GGUI_MCP_MOUNT_FACTORY_EXPORT];
|
|
179
|
+
if (typeof named === 'function') {
|
|
180
|
+
return named;
|
|
181
|
+
}
|
|
182
|
+
const dflt = record['default'];
|
|
183
|
+
if (typeof dflt === 'function') {
|
|
184
|
+
return dflt;
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
}
|
|
188
|
+
function validateMountShape(payload) {
|
|
189
|
+
if (payload === null || typeof payload !== 'object') {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
message: `expected an object shaped { name: string; handlers: [] }, got ${typeof payload}`,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
const obj = payload;
|
|
196
|
+
const name = obj['name'];
|
|
197
|
+
if (typeof name !== 'string' || name.length === 0) {
|
|
198
|
+
return {
|
|
199
|
+
ok: false,
|
|
200
|
+
message: `\`name\` must be a non-empty string (used in collision-error messages + composition telemetry)`,
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
const handlers = obj['handlers'];
|
|
204
|
+
if (!Array.isArray(handlers)) {
|
|
205
|
+
return {
|
|
206
|
+
ok: false,
|
|
207
|
+
message: `\`handlers\` must be an array (received: ${typeof handlers})`,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
ok: true,
|
|
212
|
+
mount: {
|
|
213
|
+
name,
|
|
214
|
+
handlers: handlers,
|
|
215
|
+
},
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
function describeModule(mod) {
|
|
219
|
+
if (mod === null || typeof mod !== 'object')
|
|
220
|
+
return typeof mod;
|
|
221
|
+
const keys = Object.keys(mod);
|
|
222
|
+
return keys.length === 0 ? '(no exports)' : `exports: ${keys.join(', ')}`;
|
|
223
|
+
}
|
|
224
|
+
function errorMessage(cause) {
|
|
225
|
+
return cause instanceof Error ? cause.message : String(cause);
|
|
226
|
+
}
|
package/dist/node.d.ts
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { GguiJsonV1 } from './schema.js';
|
|
2
|
+
/** How many parent directories `findGguiJson` will walk by default. */
|
|
3
|
+
export declare const DEFAULT_FIND_MAX_DEPTH = 8;
|
|
4
|
+
/**
|
|
5
|
+
* Walk up from `startDir` (default: `process.cwd()`) looking for a
|
|
6
|
+
* `ggui.json`. Returns the absolute path to the first match, or
|
|
7
|
+
* `null` if no file is found within `maxDepth` levels.
|
|
8
|
+
*
|
|
9
|
+
* Stops when the filesystem root is reached, regardless of `maxDepth`.
|
|
10
|
+
* Never throws — a missing file is a valid result ("not in a ggui
|
|
11
|
+
* project"), not an error.
|
|
12
|
+
*/
|
|
13
|
+
export declare function findGguiJson(startDir?: string, maxDepth?: number): string | null;
|
|
14
|
+
/**
|
|
15
|
+
* Error thrown when a `ggui.json` fails to load — missing file,
|
|
16
|
+
* malformed JSON, or schema validation failure. Wraps the underlying
|
|
17
|
+
* cause (`SyntaxError` / `ZodError`) on `.cause` so callers can
|
|
18
|
+
* inspect issue lists when they need to.
|
|
19
|
+
*/
|
|
20
|
+
export declare class GguiJsonLoadError extends Error {
|
|
21
|
+
readonly path: string;
|
|
22
|
+
constructor(message: string, path: string, options?: {
|
|
23
|
+
cause?: unknown;
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Read `ggui.json` at `path`, parse JSON, validate against the v1
|
|
28
|
+
* schema. Returns the fully-defaulted document.
|
|
29
|
+
*
|
|
30
|
+
* Throws {@link GguiJsonLoadError} if:
|
|
31
|
+
* - the file does not exist,
|
|
32
|
+
* - the file is not valid JSON,
|
|
33
|
+
* - the document fails schema validation (cause set to `ZodError`).
|
|
34
|
+
*/
|
|
35
|
+
export declare function loadGguiJson(path: string): GguiJsonV1;
|
|
36
|
+
/**
|
|
37
|
+
* Result of {@link safeLoadGguiJson} — mirrors `z.safeParse` so
|
|
38
|
+
* consumers can branch without try/catch.
|
|
39
|
+
*/
|
|
40
|
+
export type SafeLoadResult = {
|
|
41
|
+
success: true;
|
|
42
|
+
data: GguiJsonV1;
|
|
43
|
+
} | {
|
|
44
|
+
success: false;
|
|
45
|
+
error: GguiJsonLoadError;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Non-throwing variant of {@link loadGguiJson}. Returns a
|
|
49
|
+
* discriminated result. Use this in CLI surfaces that render issue
|
|
50
|
+
* lists directly.
|
|
51
|
+
*/
|
|
52
|
+
export declare function safeLoadGguiJson(path: string): SafeLoadResult;
|
|
53
|
+
/**
|
|
54
|
+
* Serialize and write a `ggui.json` document to `path`. The input is
|
|
55
|
+
* re-validated against the v1 schema before writing so a caller
|
|
56
|
+
* cannot accidentally persist a document that wouldn't round-trip
|
|
57
|
+
* through {@link loadGguiJson}.
|
|
58
|
+
*
|
|
59
|
+
* Output format: 2-space indent, trailing newline.
|
|
60
|
+
*/
|
|
61
|
+
export declare function saveGguiJson(path: string, doc: GguiJsonV1): void;
|
|
62
|
+
export { GGUI_JSON_FILENAME, GguiJsonV1, parseGguiJson, safeParseGguiJson, } from './schema.js';
|
|
63
|
+
export { discoverLocalUis, discoverFromGguiJsonPath, type DiscoveredUi, type DiscoveryIssue, type DiscoveryResult, type DiscoverOptions, } from './discovery.js';
|
|
64
|
+
export { discoverPrimitives, discoverPrimitivesFromGguiJsonPath, type DiscoveredPrimitiveCatalog, type PrimitiveDiscoveryIssue, type PrimitiveDiscoveryResult, type DiscoverPrimitivesOptions, } from './primitive-discovery.js';
|
|
65
|
+
export { discoverMcpMounts, GGUI_MCP_MOUNT_FACTORY_EXPORT, type DiscoveredMcpMount, type McpMountDiscoveryIssue, type McpMountDiscoveryResult, type DiscoverMcpMountsOptions, } from './mcp-mount-discovery.js';
|
|
66
|
+
export { GGUI_PRIMITIVES_JSON_FILENAME, PrimitivesManifestV1, parsePrimitivesManifest, safeParsePrimitivesManifest, type PrimitivesManifest, } from './primitives-manifest.js';
|
|
67
|
+
export { ThemeDocumentV1, parseThemeDocument, safeParseThemeDocument, type ThemeDocument, } from './theme.js';
|
|
68
|
+
export { loadTheme, loadThemeFromGguiJsonPath, type LoadedTheme, type LoadThemeOptions, type LoadThemeResult, type ThemeLoadIssue, } from './theme-loader.js';
|
|
69
|
+
//# sourceMappingURL=node.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"node.d.ts","sourceRoot":"","sources":["../src/node.ts"],"names":[],"mappings":"AAqBA,OAAO,EAEL,UAAU,EAEX,MAAM,aAAa,CAAC;AAErB,uEAAuE;AACvE,eAAO,MAAM,sBAAsB,IAAI,CAAC;AAExC;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAC1B,QAAQ,GAAE,MAAsB,EAChC,QAAQ,GAAE,MAA+B,GACxC,MAAM,GAAG,IAAI,CAUf;AAED;;;;;GAKG;AACH,qBAAa,iBAAkB,SAAQ,KAAK;IAC1C,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;gBAEV,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAE;CAKzE;AAED;;;;;;;;GAQG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAoCrD;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,GACtB;IAAE,OAAO,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,UAAU,CAAA;CAAE,GACnC;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,iBAAiB,CAAA;CAAE,CAAC;AAEjD;;;;GAIG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAS7D;AAED;;;;;;;GAOG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,UAAU,GAAG,IAAI,CAGhE;AAMD,OAAO,EACL,kBAAkB,EAClB,UAAU,EACV,aAAa,EACb,iBAAiB,GAClB,MAAM,aAAa,CAAC;AAMrB,OAAO,EACL,gBAAgB,EAChB,wBAAwB,EACxB,KAAK,YAAY,EACjB,KAAK,cAAc,EACnB,KAAK,eAAe,EACpB,KAAK,eAAe,GACrB,MAAM,gBAAgB,CAAC;AAOxB,OAAO,EACL,kBAAkB,EAClB,kCAAkC,EAClC,KAAK,0BAA0B,EAC/B,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,EAC7B,KAAK,yBAAyB,GAC/B,MAAM,0BAA0B,CAAC;AAOlC,OAAO,EACL,iBAAiB,EACjB,6BAA6B,EAC7B,KAAK,kBAAkB,EACvB,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,wBAAwB,GAC9B,MAAM,0BAA0B,CAAC;AAIlC,OAAO,EACL,6BAA6B,EAC7B,oBAAoB,EACpB,uBAAuB,EACvB,2BAA2B,EAC3B,KAAK,kBAAkB,GACxB,MAAM,0BAA0B,CAAC;AAMlC,OAAO,EACL,eAAe,EACf,kBAAkB,EAClB,sBAAsB,EACtB,KAAK,aAAa,GACnB,MAAM,YAAY,CAAC;AAMpB,OAAO,EACL,SAAS,EACT,yBAAyB,EACzB,KAAK,WAAW,EAChB,KAAK,gBAAgB,EACrB,KAAK,eAAe,EACpB,KAAK,cAAc,GACpB,MAAM,mBAAmB,CAAC"}
|
package/dist/node.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node-only filesystem helpers for `ggui.json`.
|
|
3
|
+
*
|
|
4
|
+
* Lives on the `@ggui-ai/project-config/node` subpath (not the root
|
|
5
|
+
* barrel) because it imports `node:fs` / `node:path`. Pulling those
|
|
6
|
+
* builtins into the root barrel would break browser bundlers that
|
|
7
|
+
* resolve `@ggui-ai/project-config` directly (webpack refuses the
|
|
8
|
+
* `node:` scheme). The schema + parser in `./schema.ts` stay
|
|
9
|
+
* browser-safe and are re-exported both from here and from the root
|
|
10
|
+
* barrel.
|
|
11
|
+
*
|
|
12
|
+
* Intended callers:
|
|
13
|
+
*
|
|
14
|
+
* - Open `ggui` CLI (local dev / run / pair / serve / bench). Walks
|
|
15
|
+
* upward from `process.cwd()` to find the project-root file.
|
|
16
|
+
* - Any hosting integration that reads the open manifest to
|
|
17
|
+
* reconstitute an agent. Hosting layers treat `ggui.json` as
|
|
18
|
+
* input-only; per-tenant overlay config lives outside this package.
|
|
19
|
+
*/
|
|
20
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
21
|
+
import { dirname, join, resolve } from 'node:path';
|
|
22
|
+
import { GGUI_JSON_FILENAME, parseGguiJson, } from './schema.js';
|
|
23
|
+
/** How many parent directories `findGguiJson` will walk by default. */
|
|
24
|
+
export const DEFAULT_FIND_MAX_DEPTH = 8;
|
|
25
|
+
/**
|
|
26
|
+
* Walk up from `startDir` (default: `process.cwd()`) looking for a
|
|
27
|
+
* `ggui.json`. Returns the absolute path to the first match, or
|
|
28
|
+
* `null` if no file is found within `maxDepth` levels.
|
|
29
|
+
*
|
|
30
|
+
* Stops when the filesystem root is reached, regardless of `maxDepth`.
|
|
31
|
+
* Never throws — a missing file is a valid result ("not in a ggui
|
|
32
|
+
* project"), not an error.
|
|
33
|
+
*/
|
|
34
|
+
export function findGguiJson(startDir = process.cwd(), maxDepth = DEFAULT_FIND_MAX_DEPTH) {
|
|
35
|
+
let dir = resolve(startDir);
|
|
36
|
+
for (let i = 0; i <= maxDepth; i++) {
|
|
37
|
+
const candidate = join(dir, GGUI_JSON_FILENAME);
|
|
38
|
+
if (existsSync(candidate))
|
|
39
|
+
return candidate;
|
|
40
|
+
const parent = dirname(dir);
|
|
41
|
+
if (parent === dir)
|
|
42
|
+
return null;
|
|
43
|
+
dir = parent;
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Error thrown when a `ggui.json` fails to load — missing file,
|
|
49
|
+
* malformed JSON, or schema validation failure. Wraps the underlying
|
|
50
|
+
* cause (`SyntaxError` / `ZodError`) on `.cause` so callers can
|
|
51
|
+
* inspect issue lists when they need to.
|
|
52
|
+
*/
|
|
53
|
+
export class GguiJsonLoadError extends Error {
|
|
54
|
+
path;
|
|
55
|
+
constructor(message, path, options) {
|
|
56
|
+
super(message, options);
|
|
57
|
+
this.name = 'GguiJsonLoadError';
|
|
58
|
+
this.path = path;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Read `ggui.json` at `path`, parse JSON, validate against the v1
|
|
63
|
+
* schema. Returns the fully-defaulted document.
|
|
64
|
+
*
|
|
65
|
+
* Throws {@link GguiJsonLoadError} if:
|
|
66
|
+
* - the file does not exist,
|
|
67
|
+
* - the file is not valid JSON,
|
|
68
|
+
* - the document fails schema validation (cause set to `ZodError`).
|
|
69
|
+
*/
|
|
70
|
+
export function loadGguiJson(path) {
|
|
71
|
+
if (!existsSync(path)) {
|
|
72
|
+
throw new GguiJsonLoadError(`ggui.json not found at ${path}`, path);
|
|
73
|
+
}
|
|
74
|
+
let raw;
|
|
75
|
+
try {
|
|
76
|
+
raw = readFileSync(path, 'utf-8');
|
|
77
|
+
}
|
|
78
|
+
catch (cause) {
|
|
79
|
+
throw new GguiJsonLoadError(`Failed to read ggui.json at ${path}`, path, { cause });
|
|
80
|
+
}
|
|
81
|
+
let decoded;
|
|
82
|
+
try {
|
|
83
|
+
decoded = JSON.parse(raw);
|
|
84
|
+
}
|
|
85
|
+
catch (cause) {
|
|
86
|
+
throw new GguiJsonLoadError(`ggui.json at ${path} is not valid JSON`, path, { cause });
|
|
87
|
+
}
|
|
88
|
+
try {
|
|
89
|
+
return parseGguiJson(decoded);
|
|
90
|
+
}
|
|
91
|
+
catch (cause) {
|
|
92
|
+
throw new GguiJsonLoadError(`ggui.json at ${path} failed schema validation`, path, { cause });
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Non-throwing variant of {@link loadGguiJson}. Returns a
|
|
97
|
+
* discriminated result. Use this in CLI surfaces that render issue
|
|
98
|
+
* lists directly.
|
|
99
|
+
*/
|
|
100
|
+
export function safeLoadGguiJson(path) {
|
|
101
|
+
try {
|
|
102
|
+
return { success: true, data: loadGguiJson(path) };
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
if (error instanceof GguiJsonLoadError) {
|
|
106
|
+
return { success: false, error };
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Serialize and write a `ggui.json` document to `path`. The input is
|
|
113
|
+
* re-validated against the v1 schema before writing so a caller
|
|
114
|
+
* cannot accidentally persist a document that wouldn't round-trip
|
|
115
|
+
* through {@link loadGguiJson}.
|
|
116
|
+
*
|
|
117
|
+
* Output format: 2-space indent, trailing newline.
|
|
118
|
+
*/
|
|
119
|
+
export function saveGguiJson(path, doc) {
|
|
120
|
+
const validated = parseGguiJson(doc);
|
|
121
|
+
writeFileSync(path, JSON.stringify(validated, null, 2) + '\n', 'utf-8');
|
|
122
|
+
}
|
|
123
|
+
// Re-export schema + parser from the same subpath so Node callers
|
|
124
|
+
// can do `import { parseGguiJson, loadGguiJson } from
|
|
125
|
+
// '@ggui-ai/project-config/node'` without two imports. Browser callers
|
|
126
|
+
// use the root barrel, which exports the browser-safe subset.
|
|
127
|
+
export { GGUI_JSON_FILENAME, GguiJsonV1, parseGguiJson, safeParseGguiJson, } from './schema.js';
|
|
128
|
+
// Filesystem-backed blueprint manifest discovery. Lives here (not the
|
|
129
|
+
// browser-safe root barrel) because it imports `node:fs/promises` +
|
|
130
|
+
// `tinyglobby`. Consumed by `@ggui-ai/dev-stack` (for the `ggui dev`
|
|
131
|
+
// hub) and `@ggui-ai/mcp-server` (for `ggui serve` boot wiring).
|
|
132
|
+
export { discoverLocalUis, discoverFromGguiJsonPath, } from './discovery.js';
|
|
133
|
+
// Filesystem + Node-module-resolution backed primitive manifest
|
|
134
|
+
// discovery. Symmetric with blueprint discovery above but resolves
|
|
135
|
+
// npm specifiers through `createRequire` rather than glob-only.
|
|
136
|
+
// Consumed by `@ggui-ai/mcp-server` at `ggui serve` boot to index
|
|
137
|
+
// every primitive source declared in `ggui.json#primitives`.
|
|
138
|
+
export { discoverPrimitives, discoverPrimitivesFromGguiJsonPath, } from './primitive-discovery.js';
|
|
139
|
+
// Node-only resolution of `ggui.json#mcpMounts`. Dynamic-imports each
|
|
140
|
+
// declared module, invokes its `createGguiMcpMount` factory, and
|
|
141
|
+
// returns structurally-validated mount payloads. Consumed by
|
|
142
|
+
// `@ggui-ai/cli` at `ggui serve` boot to thread operator-declared
|
|
143
|
+
// tool bundles through `createGguiServer({ mcpMounts })`.
|
|
144
|
+
export { discoverMcpMounts, GGUI_MCP_MOUNT_FACTORY_EXPORT, } from './mcp-mount-discovery.js';
|
|
145
|
+
// Browser-safe primitive manifest schema + parsers re-exported here
|
|
146
|
+
// too so Node callers get the full surface from one import path.
|
|
147
|
+
export { GGUI_PRIMITIVES_JSON_FILENAME, PrimitivesManifestV1, parsePrimitivesManifest, safeParsePrimitivesManifest, } from './primitives-manifest.js';
|
|
148
|
+
// Browser-safe theme document schema + parsers — re-exported on the
|
|
149
|
+
// Node subpath so `ggui serve` / `ggui dev` get the full theme
|
|
150
|
+
// surface from one import path. See `./theme.js` for the v1
|
|
151
|
+
// plain-DTCG contract.
|
|
152
|
+
export { ThemeDocumentV1, parseThemeDocument, safeParseThemeDocument, } from './theme.js';
|
|
153
|
+
// Filesystem-backed theme loader — consumes `ggui.json#theme`,
|
|
154
|
+
// resolves it against the project root, parses + validates, and
|
|
155
|
+
// returns a `LoadedTheme` ready for the OSS `ggui serve` server.
|
|
156
|
+
// Non-throwing per-file; the CLI escalates issues to fatal at boot.
|
|
157
|
+
export { loadTheme, loadThemeFromGguiJsonPath, } from './theme-loader.js';
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { GguiJsonV1 } from './schema.js';
|
|
2
|
+
import { type PrimitivesManifest } from './primitives-manifest.js';
|
|
3
|
+
/**
|
|
4
|
+
* One discovered primitive catalog — the import specifier + absolute
|
|
5
|
+
* manifest path + the parsed manifest. Callers address by `import`;
|
|
6
|
+
* paths drive doc-file lookups and informational output.
|
|
7
|
+
*/
|
|
8
|
+
export interface DiscoveredPrimitiveCatalog {
|
|
9
|
+
/**
|
|
10
|
+
* Where the source came from. `'package'` = resolved through an
|
|
11
|
+
* npm specifier declared in `ggui.json#primitives.packages`;
|
|
12
|
+
* `'local'` = matched by a glob in `ggui.json#primitives.local`.
|
|
13
|
+
* Renderers may use this to annotate the catalog differently.
|
|
14
|
+
*/
|
|
15
|
+
source: 'package' | 'local';
|
|
16
|
+
/**
|
|
17
|
+
* The specifier consumers import from (equal to `manifest.import`).
|
|
18
|
+
* Keyed separately from the manifest so callers don't have to
|
|
19
|
+
* reach into the nested shape for the common case.
|
|
20
|
+
*/
|
|
21
|
+
import: string;
|
|
22
|
+
/** Absolute path to the discovered `ggui.primitives.json`. */
|
|
23
|
+
manifestPath: string;
|
|
24
|
+
/** Parsed manifest. */
|
|
25
|
+
manifest: PrimitivesManifest;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* An error surfaced during discovery. Shape matches
|
|
29
|
+
* {@link DiscoveryIssue} from blueprint discovery so CLI renderers
|
|
30
|
+
* can treat both issue streams identically.
|
|
31
|
+
*/
|
|
32
|
+
export interface PrimitiveDiscoveryIssue {
|
|
33
|
+
/**
|
|
34
|
+
* Path or specifier of the offending source. For `packages`
|
|
35
|
+
* entries this is the declared npm specifier (not a filesystem
|
|
36
|
+
* path) since resolution may have failed before we could locate
|
|
37
|
+
* a file. For `local` entries this is a relative path under the
|
|
38
|
+
* project root.
|
|
39
|
+
*/
|
|
40
|
+
path: string;
|
|
41
|
+
message: string;
|
|
42
|
+
/** Underlying cause (typically `ZodError`, `SyntaxError`, or the resolver error). */
|
|
43
|
+
cause?: unknown;
|
|
44
|
+
}
|
|
45
|
+
/** Result of a full primitive-discovery pass. */
|
|
46
|
+
export interface PrimitiveDiscoveryResult {
|
|
47
|
+
/** Successfully parsed catalogs. Ordering: packages first (in declaration order),
|
|
48
|
+
* then local (filesystem-walk order from tinyglobby). */
|
|
49
|
+
catalogs: DiscoveredPrimitiveCatalog[];
|
|
50
|
+
/** Issues that should be rendered to the user; discovery still returns. */
|
|
51
|
+
issues: PrimitiveDiscoveryIssue[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Options for {@link discoverPrimitives}. `resolveModule` is exposed
|
|
55
|
+
* so tests can inject a fake resolver; real callers omit it and get
|
|
56
|
+
* the Node `createRequire` default anchored at `projectRoot`.
|
|
57
|
+
*/
|
|
58
|
+
export interface DiscoverPrimitivesOptions {
|
|
59
|
+
/**
|
|
60
|
+
* Absolute project root — the directory containing `ggui.json`.
|
|
61
|
+
* Globs in `primitives.local` resolve relative to this directory;
|
|
62
|
+
* npm specifiers in `primitives.packages` resolve through a
|
|
63
|
+
* `require` anchored here.
|
|
64
|
+
*/
|
|
65
|
+
projectRoot: string;
|
|
66
|
+
/** The parsed `ggui.json`. Only `primitives.{packages,local}` is consumed. */
|
|
67
|
+
manifest: GguiJsonV1;
|
|
68
|
+
/**
|
|
69
|
+
* Hook for tests — given the declared specifier and the project
|
|
70
|
+
* root, return the absolute path to a representative file inside
|
|
71
|
+
* the package (any file that lets us walk UP to the enclosing
|
|
72
|
+
* `package.json`). Throw to signal "not resolvable". Production
|
|
73
|
+
* callers omit this; the default resolver uses `createRequire`
|
|
74
|
+
* against `<projectRoot>/package.json`.
|
|
75
|
+
*/
|
|
76
|
+
resolveModule?: (specifier: string, projectRoot: string) => string;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Walk both `primitives.packages` and `primitives.local`, build the
|
|
80
|
+
* catalog, accumulate issues.
|
|
81
|
+
*
|
|
82
|
+
* Non-throwing per source. Duplicate-`import`-specifier across
|
|
83
|
+
* sources is an issue (first-seen wins, later ones accumulate as
|
|
84
|
+
* issues). A glob pattern that matches nothing is allowed — the
|
|
85
|
+
* manifest may exist ahead of primitives being authored; operators
|
|
86
|
+
* can still run `ggui dev` without the catalog being complete.
|
|
87
|
+
*/
|
|
88
|
+
export declare function discoverPrimitives(options: DiscoverPrimitivesOptions): Promise<PrimitiveDiscoveryResult>;
|
|
89
|
+
/**
|
|
90
|
+
* Convenience wrapper for the common "I have a `ggui.json` path,
|
|
91
|
+
* give me the primitives" flow. Resolves the project root from the
|
|
92
|
+
* manifest path.
|
|
93
|
+
*/
|
|
94
|
+
export declare function discoverPrimitivesFromGguiJsonPath(manifestPath: string, manifest: GguiJsonV1, extra?: Omit<DiscoverPrimitivesOptions, 'projectRoot' | 'manifest'>): Promise<PrimitiveDiscoveryResult>;
|
|
95
|
+
//# sourceMappingURL=primitive-discovery.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"primitive-discovery.d.ts","sourceRoot":"","sources":["../src/primitive-discovery.ts"],"names":[],"mappings":"AAyDA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAC9C,OAAO,EAGL,KAAK,kBAAkB,EACxB,MAAM,0BAA0B,CAAC;AAElC;;;;GAIG;AACH,MAAM,WAAW,0BAA0B;IACzC;;;;;OAKG;IACH,MAAM,EAAE,SAAS,GAAG,OAAO,CAAC;IAC5B;;;;OAIG;IACH,MAAM,EAAE,MAAM,CAAC;IACf,8DAA8D;IAC9D,YAAY,EAAE,MAAM,CAAC;IACrB,uBAAuB;IACvB,QAAQ,EAAE,kBAAkB,CAAC;CAC9B;AAED;;;;GAIG;AACH,MAAM,WAAW,uBAAuB;IACtC;;;;;;OAMG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,qFAAqF;IACrF,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,iDAAiD;AACjD,MAAM,WAAW,wBAAwB;IACvC;8DAC0D;IAC1D,QAAQ,EAAE,0BAA0B,EAAE,CAAC;IACvC,2EAA2E;IAC3E,MAAM,EAAE,uBAAuB,EAAE,CAAC;CACnC;AAED;;;;GAIG;AACH,MAAM,WAAW,yBAAyB;IACxC;;;;;OAKG;IACH,WAAW,EAAE,MAAM,CAAC;IACpB,8EAA8E;IAC9E,QAAQ,EAAE,UAAU,CAAC;IACrB;;;;;;;OAOG;IACH,aAAa,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,KAAK,MAAM,CAAC;CACpE;AAED;;;;;;;;;GASG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,yBAAyB,GACjC,OAAO,CAAC,wBAAwB,CAAC,CA0HnC;AAED;;;;GAIG;AACH,wBAAsB,kCAAkC,CACtD,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,UAAU,EACpB,KAAK,GAAE,IAAI,CAAC,yBAAyB,EAAE,aAAa,GAAG,UAAU,CAAM,GACtE,OAAO,CAAC,wBAAwB,CAAC,CAGnC"}
|