@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/dist/schema.js ADDED
@@ -0,0 +1,556 @@
1
+ /**
2
+ * `ggui.json` v1 — the OSS `ggui` server's runtime source of truth.
3
+ *
4
+ * When a developer runs `ggui serve`, the OSS server reads this file
5
+ * to discover everything it needs: app identity, protocol version,
6
+ * blueprint manifests, primitive packages, theme tokens, declared
7
+ * adapter intent. The schema is framework-neutral — it describes
8
+ * *the ggui app*, never the internal orchestration of any specific
9
+ * agent runtime. LLM choice, system prompt, framework-specific tool
10
+ * inventory belong in each framework's own config, not here.
11
+ *
12
+ * The manifest surface model:
13
+ *
14
+ * - `ggui.json` — this file. Open. Root manifest.
15
+ * - `ggui.ui.json` — one per authored UI, colocated with source.
16
+ * Indexed via `blueprints.include` globs.
17
+ *
18
+ * **Rules for extending:**
19
+ *
20
+ * 1. **Additive only within `schema: '1'`.** Optional fields with
21
+ * defaults are safe. New top-level fields MUST default to
22
+ * behavioural no-ops so older tooling can ignore them.
23
+ * 2. **Framework-neutral.** If a field is meaningful only to one
24
+ * agent framework (Claude SDK, Vercel AI SDK, LangGraph, ADK,
25
+ * CrewAI, Mastra, etc.), it does NOT belong here. Same manifest
26
+ * describes the same ggui app across all frameworks.
27
+ * 3. **Host-neutral.** Build / runtime / deploy / sizing fields live
28
+ * in a hosting-vendor overlay, never here.
29
+ * 4. **No vendor names in enum values.** `residency: 'acme-cloud'`
30
+ * is the exact mistake a previous draft made. If an enum needs
31
+ * to name a hosting provider, it belongs in that provider's
32
+ * overlay.
33
+ *
34
+ * **Strictness (locked 2026-04-18):**
35
+ *
36
+ * Root object is strict — unknown top-level keys cause parse to
37
+ * fail. This catches typos (`"blueprint"` singular, `"component"`
38
+ * for `"primitives"`, etc.) and prevents silent drift toward the
39
+ * agent-runtime-centric shape we explicitly rejected. Nested
40
+ * objects are also strict for v1; additive fields are a coordinated
41
+ * schema change, not a drive-by.
42
+ */
43
+ import { z } from 'zod';
44
+ import { appPublicEnvSchema, strictGadgetDescriptorSchema, } from '@ggui-ai/protocol';
45
+ /**
46
+ * App slug — URL-safe identifier. 2–64 chars, lowercase
47
+ * alphanumeric + hyphens, no leading/trailing hyphen. Used as a
48
+ * namespace primitive by the ggui protocol for addressing.
49
+ */
50
+ const SlugSchema = z
51
+ .string()
52
+ .regex(/^[a-z0-9][a-z0-9-]{0,62}[a-z0-9]$/, {
53
+ message: 'Slug must be lowercase alphanumeric with hyphens, 2-64 chars, not starting or ending with a hyphen.',
54
+ });
55
+ /**
56
+ * ggui protocol version. Two accepted shapes:
57
+ *
58
+ * - **Prelaunch draft-date** — `draft-YYYY-MM-DD` (e.g.,
59
+ * `draft-2026-04-19`). This is the canonical prelaunch form;
60
+ * `@ggui-ai/protocol#PROTOCOL_VERSION` currently ships this shape.
61
+ * - **Semver** — `major.minor`, `major.minor.patch`, with optional
62
+ * pre-release suffix (e.g., `1.0.0`, `2.0.0-alpha.1`). This becomes
63
+ * canonical once the protocol is frozen for launch (first release:
64
+ * `1.0.0`).
65
+ *
66
+ * The wire contract version this app speaks — distinct from the
67
+ * `schema` field which versions the file format itself.
68
+ *
69
+ * Future tightening: could import a known-versions list from
70
+ * `@ggui-ai/protocol` and validate membership. Deliberately left as
71
+ * pattern-only for v1 to avoid the cross-package dep until a real
72
+ * consumer benefits from the tighter check.
73
+ */
74
+ const ProtocolVersionSchema = z
75
+ .string()
76
+ .regex(/^(draft-\d{4}-\d{2}-\d{2}|\d+\.\d+(?:\.\d+)?(?:-[0-9A-Za-z.-]+)?)$/, {
77
+ message: 'Protocol version must be `draft-YYYY-MM-DD` (prelaunch) or semver `major.minor[.patch][-pre-release]`.',
78
+ });
79
+ /**
80
+ * App identity — the minimal set of fields every ggui consumer
81
+ * needs to address this app. **Identity only.** No mode, no
82
+ * framework, no LLM config, no runtime policy.
83
+ */
84
+ const AppSchema = z.strictObject({
85
+ slug: SlugSchema,
86
+ name: z.string().min(1).max(120),
87
+ /**
88
+ * Per-app gadget catalog. The CLI
89
+ * passes these through to `InMemoryAppMetadataStore` so the
90
+ * handshake / push / ops handlers can validate every
91
+ * `clientCapabilities.gadgets[*]` ref against the registered
92
+ * set's package identity + export names. Each entry is a gadget
93
+ * PACKAGE and MUST satisfy the strict
94
+ * `strictGadgetDescriptorSchema` from `@ggui-ai/protocol`
95
+ * (a non-empty `exports[]` array, each export carrying required
96
+ * description / usage / example).
97
+ *
98
+ * Omitting this field falls back to the protocol's
99
+ * `STDLIB_GADGETS` defaults (the 7 first-party hooks).
100
+ * Declared 3rd-party wrappers (Leaflet, Mapbox, Stripe, …) land
101
+ * here.
102
+ */
103
+ gadgets: z
104
+ .array(strictGadgetDescriptorSchema)
105
+ .optional(),
106
+ /**
107
+ * Public env channel. Operator-stamped key→value map that
108
+ * the server projects (filtered to declared wrappers' `requires`)
109
+ * onto `globalThis.__ggui__.publicEnv` so wrapper hooks can read
110
+ * values via `getPublicEnv(key)`.
111
+ *
112
+ * Keys MUST match `GGUI_PUBLIC_APP_[A-Z0-9_]+` — the `GGUI_PUBLIC_USER_`
113
+ * namespace is reserved for a future per-user channel. Values are
114
+ * arbitrary strings; the server projects only keys whose values are
115
+ * declared in some registered wrapper's `requires`. Operators who
116
+ * declare a key without a value can use the empty string (the
117
+ * Mapbox SDK will surface "Invalid token" on first request rather
118
+ * than a missing-key error from the push gate).
119
+ *
120
+ * Omitting this field is equivalent to `{}` — wrappers without
121
+ * `requires` work unchanged; wrappers that declare `requires` will
122
+ * fail at push-gate validation if their required keys are missing.
123
+ */
124
+ publicEnv: appPublicEnvSchema.optional(),
125
+ /**
126
+ * MCP-Apps presentation mode (canvas slice). `'canvas'` makes
127
+ * `ggui_new_session` mint a session-scoped iframe
128
+ * (`ui://ggui/session/<sessionId>`) and route subsequent pushes
129
+ * through the session WebSocket channel instead of returning a
130
+ * per-push `ui://ggui/render/<shortCode>` resource. `'inline'`
131
+ * (default when omitted) preserves the per-push iframe behavior
132
+ * that flagship MCP-Apps hosts already expect.
133
+ */
134
+ defaultMcpAppsMode: z.enum(['inline', 'canvas']).optional(),
135
+ });
136
+ /**
137
+ * Operator identity — who's running this server. Surfaced on the
138
+ * public welcome page at `/` so visitors see "operated by X" instead
139
+ * of unbranded server chrome.
140
+ *
141
+ * **Privacy-first by default.** Every field is optional. Nothing is
142
+ * shown when nothing is set — no "anonymous" placeholder, no
143
+ * hostname-derived fallback. The visitor sees just server identity
144
+ * and public-surface affordances (login link, share-link hints).
145
+ *
146
+ * Distinct from `app` (which identifies the *server*) — `operator`
147
+ * identifies the *human/entity* running the server. A single
148
+ * operator can run multiple `app`s, but the welcome page always
149
+ * shows ONE operator block (one server = one ggui.json).
150
+ *
151
+ * **What this is NOT:**
152
+ * - Not auth (the admin token is its own credential)
153
+ * - Not telemetry (server doesn't phone home with operator info)
154
+ * - Not user/builder identity (those are MCP-pairing concerns)
155
+ */
156
+ const OperatorSchema = z.strictObject({
157
+ /** Display name shown on the welcome page. e.g. "Wanseob Lim" or
158
+ * "Loqu Inc.". Free-form — no slug constraint. */
159
+ name: z.string().min(1).max(200).optional(),
160
+ /** Operator's homepage / brand URL. Linkified on the welcome page
161
+ * if set. Must be a valid http(s) URL. */
162
+ url: z
163
+ .url({ message: 'operator.url must be a valid http(s) URL' })
164
+ .optional(),
165
+ /** One-line description shown under the operator name. e.g.
166
+ * "personal ggui server — tinkering only" or "Loqu's hosted ggui
167
+ * for friends + family". Plain text, no markup. */
168
+ tagline: z.string().min(1).max(280).optional(),
169
+ /** mailto-eligible contact address. Linkified as `mailto:` on the
170
+ * welcome page if set. Operators uncomfortable putting this on a
171
+ * public page should leave it unset — `name` and `url` carry the
172
+ * "how to reach me" load just as well. */
173
+ contact: z
174
+ .email({ message: 'operator.contact must be a valid email' })
175
+ .optional(),
176
+ });
177
+ /**
178
+ * Blueprint discovery block. Points at per-UI `ggui.ui.json`
179
+ * manifests colocated with their TSX sources. The OSS server
180
+ * resolves the globs at boot and indexes the matched manifests.
181
+ *
182
+ * Glob-based by design — adding a new UI is a filesystem
183
+ * operation, not a `ggui.json` edit. If a project needs explicit
184
+ * per-manifest declarations (mixed source roots, selective
185
+ * registration), a `list` field can land later as an additive
186
+ * union; for v1 the blessed path is globs.
187
+ */
188
+ const BlueprintsSchema = z.strictObject({
189
+ /** Glob patterns relative to the `ggui.json` directory. Each match
190
+ * should resolve to a `ggui.ui.json` file. */
191
+ include: z.array(z.string().min(1)).default([]),
192
+ });
193
+ /**
194
+ * Primitive discovery block. Tells the OSS server where to find
195
+ * the component primitives the UI generator can compose from.
196
+ *
197
+ * - `packages` — npm package specifiers (`@ggui-ai/design/primitives`,
198
+ * `@mycompany/ui`). The server resolves the package and
199
+ * enumerates its declared primitives. The exact package-level
200
+ * discovery convention (`package.json#ggui.primitives`,
201
+ * per-primitive manifests, etc.) is an open design question
202
+ * pending its own slice — the schema accepts the declaration
203
+ * shape regardless of which convention wins.
204
+ * - `local` — glob patterns resolving to per-primitive manifest
205
+ * files (`ggui.primitive.json`) for primitives authored inside
206
+ * the project. Symmetric with `blueprints.include`.
207
+ */
208
+ const PrimitivesSchema = z.strictObject({
209
+ packages: z
210
+ .array(z.string().min(1))
211
+ .default(['@ggui-ai/design/primitives']),
212
+ local: z.array(z.string().min(1)).default([]),
213
+ });
214
+ /**
215
+ * Agent block — tells `ggui serve` where the user's agent lives.
216
+ *
217
+ * Only one field in v1: `entry`, a path (relative to the directory
218
+ * containing `ggui.json`) to the agent's entrypoint. The `ggui serve`
219
+ * command resolves this path, maps the extension to a spawn command
220
+ * (`.js/.mjs/.cjs` → `node <entry>`; `.ts/.tsx/.mts` →
221
+ * `node --import=tsx <entry>`), and supervises the subprocess
222
+ * alongside the MCP server.
223
+ *
224
+ * Namespace justification: the thing we're pointing at IS the agent
225
+ * (user code), not the "runtime" (the supervisor). `agent.*` has room
226
+ * to grow (`agent.env`, `agent.port` if/when real needs surface); none
227
+ * of that is in this slice. `app.agent` was rejected — `app.*` is
228
+ * identity, not execution config. `runtime.*` was rejected — muddles
229
+ * user-code vs supervisor.
230
+ *
231
+ * Deliberately not in v1: env vars, restart policies, port hints,
232
+ * arg arrays. Every one of those fields has a reasonable default
233
+ * (empty env / crash-and-exit / OS-assigned / no args) and can be
234
+ * added additively later under `schema: '1'`.
235
+ *
236
+ * **Block-level optional**: The entire `agent` block is optional.
237
+ * Absent means "run MCP-only" — `ggui serve` boots the MCP server
238
+ * without attaching an agent loop.
239
+ */
240
+ const AgentSchema = z.strictObject({
241
+ /** Path to the agent's entry file, relative to the ggui.json
242
+ * directory. Supported extensions: .js, .mjs, .cjs, .ts, .tsx,
243
+ * .mts. Validation of extension happens at `ggui serve` time —
244
+ * the schema stays permissive so malformed entries surface with
245
+ * the CLI's error message (which can list supported extensions +
246
+ * remediation), not a zod issue trace. */
247
+ entry: z.string().min(1, 'agent.entry must not be empty'),
248
+ });
249
+ /**
250
+ * Theme color mode. The same identifier the design registry uses
251
+ * (`@ggui-ai/design`'s `ThemeMode`). Held here as a literal union
252
+ * so project-config can validate without a runtime dep on design —
253
+ * the two stay structurally identical (single source of truth is
254
+ * the JSON wire shape, not either type alias).
255
+ */
256
+ const ThemeModeSchema = z.enum(['light', 'dark']);
257
+ /**
258
+ * Flat dot-path token overrides applied on top of a registered
259
+ * preset's tokens.
260
+ *
261
+ * Keys are dot-paths into the DTCG token tree (`color.primary.500`,
262
+ * `shape.radius.lg`, `font.family.sans`). Values replace the
263
+ * resolved leaf's `$value`. Unknown paths are silently ignored at
264
+ * load time — the console token editor mints valid keys, and an
265
+ * accidentally-stale override should not fail manifest parse.
266
+ *
267
+ * Why flat (not deep tree): the editor mutates leaves one at a
268
+ * time and serializes a diff; flat keys make the diff trivially
269
+ * computable and `JSON.stringify` round-trip stable. Authoring by
270
+ * hand is a side path.
271
+ */
272
+ const ThemeOverridesSchema = z.record(z.string(), z.string());
273
+ /**
274
+ * `theme: { preset, mode?, overrides? }` — pick a registered preset
275
+ * from `@ggui-ai/design`'s registry. `preset` is the registry key
276
+ * (e.g. `'claudic'`, `'ggui'`, `'premium-zen'`). The loader
277
+ * returns an issue when the id is unregistered.
278
+ */
279
+ const ThemePresetConfigSchema = z.strictObject({
280
+ preset: z.string().min(1),
281
+ mode: ThemeModeSchema.optional(),
282
+ overrides: ThemeOverridesSchema.optional(),
283
+ });
284
+ /**
285
+ * `theme: { file, mode? }` — load tokens from a project-relative
286
+ * DTCG JSON file. The `mode` is metadata only (the file's tokens
287
+ * ARE the resolved theme, not a switch); it propagates to consumers
288
+ * that need to vary based on color scheme (e.g. injecting
289
+ * `color-scheme: dark` on the document).
290
+ */
291
+ const ThemeFileConfigSchema = z.strictObject({
292
+ file: z.string().min(1),
293
+ mode: ThemeModeSchema.optional(),
294
+ });
295
+ /**
296
+ * Discriminated theme selection. Three accepted shapes:
297
+ *
298
+ * 1. `string` — preset id shorthand (`theme: 'claudic'` ≡
299
+ * `theme: { preset: 'claudic' }`)
300
+ * 2. `{ preset, mode?, overrides? }` — registered preset with
301
+ * optional mode + override layer
302
+ * 3. `{ file, mode? }` — DTCG JSON file
303
+ *
304
+ * Strict objects on shapes 2 and 3 reject unknown keys to catch
305
+ * typos at parse (`{ presset: 'claudic' }` fails, not silently
306
+ * loads default).
307
+ */
308
+ export const ThemeConfigSchema = z.union([
309
+ z.string().min(1),
310
+ ThemePresetConfigSchema,
311
+ ThemeFileConfigSchema,
312
+ ]);
313
+ /**
314
+ * Storage block — opt-in persistence wiring for the OSS `ggui serve`
315
+ * runtime. Absent means **every storage surface stays in-memory** —
316
+ * the zero-config OSS default (state resets on process restart).
317
+ * Present means the operator has explicitly asked for the named
318
+ * adapters; no surface ever silently creates a file on disk without
319
+ * the operator declaring it here.
320
+ *
321
+ * ## Why `{sessions, vectors}` together
322
+ *
323
+ * Sessions + vectors are the two substantial persistence surfaces
324
+ * the OSS server honors today. Shipping them in the same block from
325
+ * v1 avoids a second schema churn when vectors or a future store
326
+ * (e.g. threads) lands — there's one `storage` shape and each known
327
+ * surface is an optional child. A future `storage.threads` or
328
+ * `storage.blueprints` slice adds a sibling field without reshaping
329
+ * the block.
330
+ *
331
+ * ## Why per-surface `driver`
332
+ *
333
+ * Explicit driver makes the choice visible at a glance — both to
334
+ * the operator reading the file and to future adapters that land
335
+ * against the same interfaces (`postgres`, `redis`, `libsql`, …).
336
+ * A flat `sessions: "./path.sqlite"` shape would bake sqlite into
337
+ * the schema and force a breaking redesign the first time another
338
+ * driver landed.
339
+ *
340
+ * ## Why per-surface `path` instead of a shared root
341
+ *
342
+ * Sessions + vectors can (and typically should) live in separate
343
+ * files: session data is small + hot-path; vector data is large +
344
+ * append-heavy. Coupling them into one file conflates two different
345
+ * IO profiles. Operators who want co-located storage can point both
346
+ * paths at the same file — but the schema doesn't force that.
347
+ *
348
+ * ## Deliberately out of scope (v1)
349
+ *
350
+ * - `driver: 'postgres' | 'redis' | …` — future additive unions
351
+ * once a reference adapter lands.
352
+ * - Connection pools, WAL pragma toggles, backup schedules — all
353
+ * real, all belong in an adapter-specific options block (not
354
+ * here) if they ever need to be surfaced.
355
+ * - `encryption` / `at-rest` settings — cross-cutting concern;
356
+ * not an OSS-v1 responsibility.
357
+ * - Secret expansion (`${VAR}` in paths) — operators can emit
358
+ * the manifest from templated input today; a schema-level
359
+ * token is a future additive.
360
+ *
361
+ * ## Strictness
362
+ *
363
+ * Root `storage` is strict — unknown surface keys (`sesions: …`,
364
+ * `vectros: …`, `blueprints: …`) fail parse. Per-surface shapes
365
+ * are discriminated on `driver`; each variant is strict so typos
366
+ * like `{driver: 'sqlite', paht: './x'}` fail immediately.
367
+ */
368
+ const StorageMemorySchema = z.strictObject({
369
+ driver: z.literal('memory'),
370
+ });
371
+ const StorageSqliteSchema = z.strictObject({
372
+ driver: z.literal('sqlite'),
373
+ /** Filesystem path to the SQLite database file. Relative paths
374
+ * resolve from the ggui.json directory at adapter-instantiation
375
+ * time. Empty strings are rejected at parse. */
376
+ path: z.string().min(1, 'storage.<surface>.path must not be empty'),
377
+ });
378
+ /**
379
+ * Discriminated union over storage drivers. Add new drivers as
380
+ * additive siblings under the same discriminator — never widen an
381
+ * existing variant, since that would silently change what older
382
+ * manifests mean.
383
+ */
384
+ const StorageSurfaceSchema = z.discriminatedUnion('driver', [
385
+ StorageMemorySchema,
386
+ StorageSqliteSchema,
387
+ ]);
388
+ const StorageSchema = z.strictObject({
389
+ /** Session persistence (events + stack + identity). Absent =
390
+ * in-memory (sessions reset on restart). */
391
+ sessions: StorageSurfaceSchema.optional(),
392
+ /** Vector index persistence (RAG, blueprint embeddings). Absent =
393
+ * in-memory (index rebuilds on restart). */
394
+ vectors: StorageSurfaceSchema.optional(),
395
+ /** Persistent-chat thread + message persistence. Absent = in-memory
396
+ * (threads vanish on restart — correct for dev, unsafe for any
397
+ * deployment Portal's self-hosted flow talks to). Pair with
398
+ * `driver: 'sqlite'` + a `path` for durable local storage; the
399
+ * `@ggui-ai/mcp-server-core/sqlite#SqliteThreadStore` reference
400
+ * impl consumes the same file-per-surface layout the sessions/
401
+ * vectors drivers already use. */
402
+ threads: StorageSurfaceSchema.optional(),
403
+ });
404
+ /**
405
+ * The authoritative v1 schema. `z.infer<typeof GguiJsonV1>` yields
406
+ * the static TypeScript type. A valid document round-trips cleanly:
407
+ * parse → `JSON.stringify` → re-parse produces an equivalent value.
408
+ *
409
+ * **Required fields:** `schema`, `protocol`, `app`.
410
+ * **Optional fields with defaults:** `blueprints`, `primitives`,
411
+ * `adapters`. A zero-config `ggui.json` is just the three required
412
+ * fields — the OSS server boots with defaults for everything else.
413
+ * **Optional without default:** `theme` (absent = shipped default
414
+ * tokens), `agent` (absent = `ggui serve` runs MCP-only).
415
+ *
416
+ * **Strict root:** unknown top-level keys are rejected.
417
+ */
418
+ export const GguiJsonV1 = z.strictObject({
419
+ /** File-format version — always `"1"` for v1. Distinct from
420
+ * `protocol` (which versions the wire contract the app speaks). */
421
+ schema: z
422
+ .literal('1')
423
+ .describe('File-format version. Always "1" for v1. Distinct from `protocol` (which versions the wire contract the app speaks).'),
424
+ /** ggui wire protocol version the app speaks. */
425
+ protocol: ProtocolVersionSchema.describe('ggui wire protocol version the app speaks. Pinned per file-format version.'),
426
+ /** App identity. */
427
+ app: AppSchema.describe('App identity — slug + display name. Slug is the stable machine identifier; name is human-facing.'),
428
+ /** Operator identity — who's running this server. Optional;
429
+ * surfaced on the public welcome page at `/` when set. Privacy-
430
+ * first: nothing renders when nothing is set, no "anonymous"
431
+ * placeholder. See {@link OperatorConfig}. */
432
+ operator: OperatorSchema.optional().describe('Operator identity surfaced on the public welcome page. All subfields optional; absent = no operator block rendered (privacy-first default).'),
433
+ /** Blueprint discovery. Defaults to no custom UIs declared. */
434
+ blueprints: BlueprintsSchema.default({ include: [] }).describe('Glob patterns the OSS server walks to discover authored UIs. Each match must point to a `ggui.ui.json` colocated with its TSX source. Empty default = no custom blueprints registered.'),
435
+ /** Primitive discovery. Defaults to the shipped
436
+ * `@ggui-ai/design/primitives` package + no local primitives. */
437
+ primitives: PrimitivesSchema.default({
438
+ packages: ['@ggui-ai/design/primitives'],
439
+ local: [],
440
+ }).describe('Where the server discovers UI primitives. `packages[]` lists npm packages shipping a `ggui.primitives.json` manifest; `local[]` lists project-relative manifest paths. Defaults to the shipped `@ggui-ai/design/primitives` package.'),
441
+ /**
442
+ * Theme selection. Three accepted shapes:
443
+ *
444
+ * - **Shorthand string** — `theme: "claudic"`. Picks a preset from
445
+ * the `@ggui-ai/design` registry. Default mode is `'light'`.
446
+ * - **Preset object** — `theme: { preset: "claudic", mode: "dark",
447
+ * overrides: { "color.primary.500": "#cc785c" } }`. Picks a
448
+ * registered preset, optional explicit mode, optional flat
449
+ * dot-path token overrides applied on top of the preset's
450
+ * tokens before CSS emission.
451
+ * - **File object** — `theme: { file: "./theme.json", mode: "dark" }`.
452
+ * Loads tokens from a project-relative DTCG JSON file. The
453
+ * `mode` is metadata only — the file's tokens are the resolved
454
+ * theme; mode propagates to consumers that vary based on it
455
+ * (e.g. `color-scheme: dark` injection).
456
+ *
457
+ * Absent (`theme: undefined`) means use the shipped default tokens
458
+ * from `@ggui-ai/design` (currently `lightTheme`).
459
+ */
460
+ theme: ThemeConfigSchema.optional().describe('Theme selection. Accepts a preset id (string), a `{ preset, mode, overrides }` object, or a `{ file, mode }` object pointing at a DTCG JSON tokens file. Absent = shipped default tokens.'),
461
+ /** Agent-runtime config for `ggui serve`. Absent means the
462
+ * command boots MCP-only (no agent loop attached). */
463
+ agent: AgentSchema.optional().describe('Agent-runtime config for `ggui serve`. Absent = command boots MCP-only (no agent loop attached).'),
464
+ /** Explicit storage adapter declarations. Absent means every
465
+ * persistence surface stays in-memory (the zero-config OSS
466
+ * default). Present means the operator has opted in per surface
467
+ * — no surface silently creates a file on disk. See
468
+ * {@link StorageConfig}. */
469
+ storage: StorageSchema.optional().describe('Per-surface storage adapter declarations. Absent = every persistence surface (sessions, threads, vectors, kv) stays in-memory (zero-config OSS default). Present = opt-in per surface; no surface silently creates a file on disk.'),
470
+ /**
471
+ * Default plugin-registry URL for this app.
472
+ *
473
+ * When the user runs `ggui install <scope/name@version>` or `ggui
474
+ * publish` from this project, the CLI resolves the target registry
475
+ * via a three-layer chain (highest priority wins):
476
+ *
477
+ * 1. Explicit `--registry=<url>` flag passed to the CLI.
478
+ * 2. `GGUI_REGISTRY` environment variable.
479
+ * 3. This `registry` field in `ggui.json`.
480
+ * 4. (none → error) The CLI ships with NO hard-coded default;
481
+ * operators MUST pick a registry per app.
482
+ *
483
+ * Mirrors the npm `--registry` / `npm_config_registry` / `.npmrc`
484
+ * resolution pattern but anchored on `ggui.json` for the project-
485
+ * local layer (instead of a dotfile) — the same file that holds
486
+ * `app.gadgets` is the natural place to declare which
487
+ * marketplace those libraries come from.
488
+ *
489
+ * Validated as a URL at schema-parse time; the CLI does not
490
+ * second-guess scheme / host beyond URL-shape (HTTPS recommendation
491
+ * is enforced at install-time, not here, so dev fixtures pointing
492
+ * at `http://localhost:<port>` parse cleanly).
493
+ *
494
+ * Absent = no project-local default; the CLI falls through to env
495
+ * var or flag, and errors if neither is set.
496
+ */
497
+ registry: z
498
+ .url({ message: 'registry must be a valid URL' })
499
+ .optional()
500
+ .describe('Default plugin-registry URL for `ggui install` / `ggui publish`. Three-layer resolution: `--registry` flag > `GGUI_REGISTRY` env > this field > error if unset. Absent = no project-local default.'),
501
+ /**
502
+ * Local MCP tool mounts — the operator-facing seam for declaring
503
+ * additional tool surfaces that aggregate onto `/mcp` alongside
504
+ * ggui-native tools (`ggui_push`, etc.). Each entry is a module
505
+ * specifier:
506
+ *
507
+ * - Relative paths (`./path/to/mount.js`, `../local/mcp.mjs`)
508
+ * resolve against the directory containing `ggui.json`. Must
509
+ * include the file extension — we do not guess `.js` vs
510
+ * `.mjs`. File must exist.
511
+ * - Bare npm specifiers (`@my-org/mcp-mount`, `some-pkg/entry`)
512
+ * resolve through Node's module resolver anchored at the
513
+ * project root.
514
+ *
515
+ * Each resolved module must export a `createGguiMcpMount` function
516
+ * (or a default export) returning `{ name: string; handlers:
517
+ * SharedHandler[] }` — i.e., an `McpServerMount` from
518
+ * `@ggui-ai/mcp-server`. The factory owns its own state (backing
519
+ * store, seed, etc.). Zero-argument today; arguments are a future
520
+ * additive (widen the entry to `string | { module, options? }`).
521
+ *
522
+ * Default: `[]` (no mounts). Collisions between mount tool names
523
+ * and ggui-native tool names (or between two mounts) are rejected
524
+ * at `createGguiServer` composition time, not silently — see
525
+ * `packages/mcp-server/src/mcp-mounts.ts::composeHandlersWithMounts`. */
526
+ mcpMounts: z
527
+ .array(z.string().min(1, 'mcpMounts entries must be non-empty module specifiers'))
528
+ .default([])
529
+ .describe('Local MCP tool mounts — each entry is a module specifier (relative path or bare npm specifier) exporting `createGguiMcpMount`. The factory returns `{name, handlers}` aggregated onto `/mcp` alongside ggui-native tools. Tool-name collisions are rejected at composition time.'),
530
+ });
531
+ /**
532
+ * Canonical filename — always at the project root, always this
533
+ * name. Exported so tooling uses the constant instead of
534
+ * hard-coding the string.
535
+ */
536
+ export const GGUI_JSON_FILENAME = 'ggui.json';
537
+ /**
538
+ * Parse a raw JSON value into a validated {@link GguiJsonV1}.
539
+ * Throws a `ZodError` with human-readable issues on invalid input.
540
+ *
541
+ * Accepts any `unknown` — callers are expected to have already
542
+ * decoded the JSON (`JSON.parse(source)`). Applies defaults for
543
+ * absent optional fields.
544
+ */
545
+ export function parseGguiJson(raw) {
546
+ return GguiJsonV1.parse(raw);
547
+ }
548
+ /**
549
+ * Safe-parse variant — returns a discriminated `z.safeParse` result
550
+ * (`{ success: true, data }` vs `{ success: false, error }`). Prefer
551
+ * this inside CLI tooling where you want to render the issue list
552
+ * without try/catch.
553
+ */
554
+ export function safeParseGguiJson(raw) {
555
+ return GguiJsonV1.safeParse(raw);
556
+ }
@@ -0,0 +1,104 @@
1
+ import type { DtcgTheme, ThemeMode } from '@ggui-ai/design/themes';
2
+ import type { GguiJsonV1, ThemeConfig } from './schema.js';
3
+ import { type ThemeDocument } from './theme.js';
4
+ /**
5
+ * One loaded theme — discriminated on `source`. Every variant carries
6
+ * the resolved `mode`, the parsed token document, and the pre-rendered
7
+ * `--ggui-*` CSS variable block.
8
+ *
9
+ * **`document` is shape-discriminated by `source`** (no casts, two
10
+ * structurally-similar but separately-validated underlying types):
11
+ *
12
+ * - `default` / `preset` → `DtcgTheme` (the canonical theme shape
13
+ * consumed by every registry-side theme — produces full
14
+ * `--ggui-*` CSS through `parseTheme`; every group required).
15
+ * - `file` → `ThemeDocument` (the strict-Zod schema for the open
16
+ * `ggui.json#theme` file format — same `color`/`font`/`spacing`/
17
+ * `shape`/`motion`/`canvas`/`accessibility`/`zIndex` vocabulary
18
+ * as `DtcgTheme`, but `motion`/`canvas`/`accessibility`/`zIndex`
19
+ * are optional so external tools that emit only a subset still
20
+ * parse).
21
+ *
22
+ * Consumers narrow on `source` if they ever need the shape. CSS-variable
23
+ * emission happens upstream into `cssVariables`, so most downstream code
24
+ * only reads that string.
25
+ */
26
+ export type LoadedTheme = {
27
+ readonly source: 'default';
28
+ readonly mode: ThemeMode;
29
+ readonly document: DtcgTheme;
30
+ readonly cssVariables: string;
31
+ } | {
32
+ readonly source: 'preset';
33
+ /** Registry id (`'claudic'`, `'ggui'`, …). */
34
+ readonly preset: string;
35
+ readonly mode: ThemeMode;
36
+ /** Flat dot-path token overrides applied on top of the preset. */
37
+ readonly overrides?: Record<string, string>;
38
+ readonly document: DtcgTheme;
39
+ readonly cssVariables: string;
40
+ } | {
41
+ readonly source: 'file';
42
+ /** Absolute filesystem path of the loaded theme file. */
43
+ readonly path: string;
44
+ readonly mode: ThemeMode;
45
+ readonly document: ThemeDocument;
46
+ readonly cssVariables: string;
47
+ };
48
+ /**
49
+ * An error surfaced during theme load. Shape matches the issue
50
+ * streams blueprint + primitive discovery produce so CLI renderers
51
+ * can treat all three identically.
52
+ */
53
+ export interface ThemeLoadIssue {
54
+ /**
55
+ * Path of the offending theme source. For file sources this is
56
+ * the resolved absolute path; for preset sources it's the preset
57
+ * id (so the CLI line reads `theme: claudic — not registered`
58
+ * cleanly).
59
+ */
60
+ path: string;
61
+ message: string;
62
+ cause?: unknown;
63
+ }
64
+ /** Result of {@link loadTheme}. One of two exhaustive branches. */
65
+ export type LoadThemeResult = {
66
+ readonly ok: true;
67
+ readonly theme: LoadedTheme;
68
+ } | {
69
+ readonly ok: false;
70
+ readonly issue: ThemeLoadIssue;
71
+ };
72
+ /**
73
+ * Options accepted by {@link loadTheme}. `projectRoot` is required —
74
+ * relative `manifest.theme.file` paths are resolved from the
75
+ * directory containing `ggui.json` (same rule `storage.*.path` uses).
76
+ */
77
+ export interface LoadThemeOptions {
78
+ /** Absolute project root — the directory containing `ggui.json`. */
79
+ projectRoot: string;
80
+ /** Parsed `ggui.json`. Only the optional `theme` field is consumed. */
81
+ manifest: GguiJsonV1;
82
+ }
83
+ /**
84
+ * Load the theme declared in `ggui.json#theme`, or fall back to the
85
+ * shipped default when the field is absent.
86
+ *
87
+ * Non-throwing — single-issue failure mode returned as a tagged
88
+ * result. Throws only for programmer errors (non-absolute
89
+ * `projectRoot`), matching the other discovery helpers.
90
+ */
91
+ export declare function loadTheme(options: LoadThemeOptions): LoadThemeResult;
92
+ /**
93
+ * Convenience wrapper for the "I have a `ggui.json` path" flow.
94
+ * Resolves the project root from the manifest path.
95
+ */
96
+ export declare function loadThemeFromGguiJsonPath(manifestPath: string, manifest: GguiJsonV1): LoadThemeResult;
97
+ /**
98
+ * Re-exported for callers that need to accept any of the parse-time
99
+ * theme shapes (string shorthand, preset object, file object) and
100
+ * dispatch on the discriminator. `ThemeConfig` is the static
101
+ * counterpart of `ggui.json#theme`'s zod union.
102
+ */
103
+ export type { ThemeConfig };
104
+ //# sourceMappingURL=theme-loader.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"theme-loader.d.ts","sourceRoot":"","sources":["../src/theme-loader.ts"],"names":[],"mappings":"AA4CA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC;AACnE,OAAO,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC3D,OAAO,EAAsB,KAAK,aAAa,EAAE,MAAM,YAAY,CAAC;AAEpE;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,MAAM,WAAW,GACnB;IACE,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B,GACD;IACE,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC;IAC1B,8CAA8C;IAC9C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,kEAAkE;IAClE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC5C,QAAQ,CAAC,QAAQ,EAAE,SAAS,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B,GACD;IACE,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,yDAAyD;IACzD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,IAAI,EAAE,SAAS,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,aAAa,CAAC;IACjC,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B,CAAC;AAEN;;;;GAIG;AACH,MAAM,WAAW,cAAc;IAC7B;;;;;OAKG;IACH,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;IAChB,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,mEAAmE;AACnE,MAAM,MAAM,eAAe,GACvB;IACE,QAAQ,CAAC,EAAE,EAAE,IAAI,CAAC;IAClB,QAAQ,CAAC,KAAK,EAAE,WAAW,CAAC;CAC7B,GACD;IACE,QAAQ,CAAC,EAAE,EAAE,KAAK,CAAC;IACnB,QAAQ,CAAC,KAAK,EAAE,cAAc,CAAC;CAChC,CAAC;AAEN;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,oEAAoE;IACpE,WAAW,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,QAAQ,EAAE,UAAU,CAAC;CACtB;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,eAAe,CAwCpE;AAyMD;;;GAGG;AACH,wBAAgB,yBAAyB,CACvC,YAAY,EAAE,MAAM,EACpB,QAAQ,EAAE,UAAU,GACnB,eAAe,CAGjB;AAED;;;;;GAKG;AACH,YAAY,EAAE,WAAW,EAAE,CAAC"}