@dharta/cli 0.6.77 → 0.6.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +215 -11
  3. package/dist/agent.js +3 -2
  4. package/dist/agent.js.map +1 -1
  5. package/dist/api-error.js +6 -6
  6. package/dist/api-error.js.map +1 -1
  7. package/dist/cli.d.ts +3 -0
  8. package/dist/cli.js +860 -149
  9. package/dist/cli.js.map +1 -1
  10. package/dist/command-tree.d.ts +3 -0
  11. package/dist/command-tree.js +10 -0
  12. package/dist/command-tree.js.map +1 -0
  13. package/dist/commands/agents.d.ts +54 -17
  14. package/dist/commands/agents.js +246 -136
  15. package/dist/commands/agents.js.map +1 -1
  16. package/dist/commands/analytics.js +6 -5
  17. package/dist/commands/analytics.js.map +1 -1
  18. package/dist/commands/api.js +71 -86
  19. package/dist/commands/api.js.map +1 -1
  20. package/dist/commands/appearance.js +5 -6
  21. package/dist/commands/appearance.js.map +1 -1
  22. package/dist/commands/artifact.js +7 -6
  23. package/dist/commands/artifact.js.map +1 -1
  24. package/dist/commands/audit-log.js +2 -1
  25. package/dist/commands/audit-log.js.map +1 -1
  26. package/dist/commands/auth.js +26 -6
  27. package/dist/commands/auth.js.map +1 -1
  28. package/dist/commands/caps.js +11 -15
  29. package/dist/commands/caps.js.map +1 -1
  30. package/dist/commands/deploy.js +25 -10
  31. package/dist/commands/deploy.js.map +1 -1
  32. package/dist/commands/dev.d.ts +10 -1
  33. package/dist/commands/dev.js +56 -25
  34. package/dist/commands/dev.js.map +1 -1
  35. package/dist/commands/doctor.d.ts +2 -0
  36. package/dist/commands/doctor.js +60 -0
  37. package/dist/commands/doctor.js.map +1 -1
  38. package/dist/commands/embed-keys.js +4 -13
  39. package/dist/commands/embed-keys.js.map +1 -1
  40. package/dist/commands/env.js +4 -7
  41. package/dist/commands/env.js.map +1 -1
  42. package/dist/commands/files.js +3 -4
  43. package/dist/commands/files.js.map +1 -1
  44. package/dist/commands/fleet.d.ts +129 -0
  45. package/dist/commands/fleet.js +408 -0
  46. package/dist/commands/fleet.js.map +1 -0
  47. package/dist/commands/genui.d.ts +2 -0
  48. package/dist/commands/genui.js +53 -0
  49. package/dist/commands/genui.js.map +1 -0
  50. package/dist/commands/git-credential.js +2 -3
  51. package/dist/commands/git-credential.js.map +1 -1
  52. package/dist/commands/kartas.d.ts +9 -0
  53. package/dist/commands/kartas.js +33 -11
  54. package/dist/commands/kartas.js.map +1 -1
  55. package/dist/commands/keys.d.ts +17 -18
  56. package/dist/commands/keys.js +108 -19
  57. package/dist/commands/keys.js.map +1 -1
  58. package/dist/commands/login.js +2 -1
  59. package/dist/commands/login.js.map +1 -1
  60. package/dist/commands/members.d.ts +10 -0
  61. package/dist/commands/members.js +20 -6
  62. package/dist/commands/members.js.map +1 -1
  63. package/dist/commands/model-connections.d.ts +44 -0
  64. package/dist/commands/model-connections.js +435 -0
  65. package/dist/commands/model-connections.js.map +1 -0
  66. package/dist/commands/open.d.ts +2 -2
  67. package/dist/commands/open.js +4 -3
  68. package/dist/commands/open.js.map +1 -1
  69. package/dist/commands/org.d.ts +9 -0
  70. package/dist/commands/org.js +42 -0
  71. package/dist/commands/org.js.map +1 -0
  72. package/dist/commands/organization.d.ts +16 -0
  73. package/dist/commands/organization.js +118 -0
  74. package/dist/commands/organization.js.map +1 -0
  75. package/dist/commands/parity.js +1 -3
  76. package/dist/commands/parity.js.map +1 -1
  77. package/dist/commands/repos.d.ts +4 -0
  78. package/dist/commands/repos.js +17 -0
  79. package/dist/commands/repos.js.map +1 -0
  80. package/dist/commands/rollback.js +2 -3
  81. package/dist/commands/rollback.js.map +1 -1
  82. package/dist/commands/schedules.js +26 -21
  83. package/dist/commands/schedules.js.map +1 -1
  84. package/dist/commands/scopes.js +8 -1
  85. package/dist/commands/scopes.js.map +1 -1
  86. package/dist/commands/sessions.js +9 -3
  87. package/dist/commands/sessions.js.map +1 -1
  88. package/dist/commands/setup.js +5 -4
  89. package/dist/commands/setup.js.map +1 -1
  90. package/dist/commands/update.d.ts +1 -0
  91. package/dist/commands/update.js +24 -11
  92. package/dist/commands/update.js.map +1 -1
  93. package/dist/commands/webhooks.d.ts +4 -1
  94. package/dist/commands/webhooks.js +21 -16
  95. package/dist/commands/webhooks.js.map +1 -1
  96. package/dist/completions.js +24 -13
  97. package/dist/completions.js.map +1 -1
  98. package/dist/config.d.ts +12 -0
  99. package/dist/config.js +121 -7
  100. package/dist/config.js.map +1 -1
  101. package/dist/context.d.ts +2 -0
  102. package/dist/context.js +23 -7
  103. package/dist/context.js.map +1 -1
  104. package/dist/docs/cli-reference.d.ts +2 -0
  105. package/dist/docs/cli-reference.js +191 -0
  106. package/dist/docs/cli-reference.js.map +1 -0
  107. package/dist/exit-codes.d.ts +25 -0
  108. package/dist/exit-codes.js +107 -0
  109. package/dist/exit-codes.js.map +1 -0
  110. package/dist/genui.d.ts +178 -0
  111. package/dist/genui.js +1130 -0
  112. package/dist/genui.js.map +1 -0
  113. package/dist/harnesses.js +2 -1
  114. package/dist/harnesses.js.map +1 -1
  115. package/dist/jq.d.ts +4 -0
  116. package/dist/jq.js +91 -0
  117. package/dist/jq.js.map +1 -0
  118. package/dist/login-offer.js +6 -2
  119. package/dist/login-offer.js.map +1 -1
  120. package/dist/manifest.d.ts +12 -0
  121. package/dist/manifest.js +126 -0
  122. package/dist/manifest.js.map +1 -1
  123. package/dist/model-auth.d.ts +29 -0
  124. package/dist/model-auth.js +117 -0
  125. package/dist/model-auth.js.map +1 -0
  126. package/dist/model-settings.d.ts +23 -0
  127. package/dist/model-settings.js +186 -0
  128. package/dist/model-settings.js.map +1 -0
  129. package/dist/money.js +2 -1
  130. package/dist/money.js.map +1 -1
  131. package/dist/output.d.ts +6 -0
  132. package/dist/output.js +54 -0
  133. package/dist/output.js.map +1 -1
  134. package/dist/runtime.d.ts +1 -1
  135. package/dist/runtime.js +1 -1
  136. package/dist/scaffold.js +2 -1
  137. package/dist/scaffold.js.map +1 -1
  138. package/dist/validators.js +17 -11
  139. package/dist/validators.js.map +1 -1
  140. package/dist/version-check.d.ts +1 -1
  141. package/dist/version-check.js +1 -1
  142. package/dist/version-check.js.map +1 -1
  143. package/node_modules/@dharta/contracts/LICENSE +21 -0
  144. package/node_modules/@dharta/contracts/README.md +45 -0
  145. package/node_modules/@dharta/contracts/artifact-runtime.json +750 -0
  146. package/node_modules/@dharta/contracts/dist/activity.d.ts +62 -0
  147. package/node_modules/@dharta/contracts/dist/activity.d.ts.map +1 -0
  148. package/node_modules/@dharta/contracts/dist/activity.js +178 -0
  149. package/node_modules/@dharta/contracts/dist/activity.js.map +1 -0
  150. package/node_modules/@dharta/contracts/dist/artifact-runtime-formats.d.ts +20 -0
  151. package/node_modules/@dharta/contracts/dist/artifact-runtime-formats.d.ts.map +1 -0
  152. package/node_modules/@dharta/contracts/dist/artifact-runtime-formats.js +53 -0
  153. package/node_modules/@dharta/contracts/dist/artifact-runtime-formats.js.map +1 -0
  154. package/node_modules/@dharta/contracts/dist/artifact-runtime-validators.generated.d.ts +37 -0
  155. package/node_modules/@dharta/contracts/dist/artifact-runtime-validators.generated.d.ts.map +1 -0
  156. package/node_modules/@dharta/contracts/dist/artifact-runtime-validators.generated.js +5107 -0
  157. package/node_modules/@dharta/contracts/dist/artifact-runtime-validators.generated.js.map +1 -0
  158. package/node_modules/@dharta/contracts/dist/artifact-runtime.d.ts +154 -0
  159. package/node_modules/@dharta/contracts/dist/artifact-runtime.d.ts.map +1 -0
  160. package/node_modules/@dharta/contracts/dist/artifact-runtime.generated.d.ts +1670 -0
  161. package/node_modules/@dharta/contracts/dist/artifact-runtime.generated.d.ts.map +1 -0
  162. package/node_modules/@dharta/contracts/dist/artifact-runtime.generated.js +1903 -0
  163. package/node_modules/@dharta/contracts/dist/artifact-runtime.generated.js.map +1 -0
  164. package/node_modules/@dharta/contracts/dist/artifact-runtime.js +377 -0
  165. package/node_modules/@dharta/contracts/dist/artifact-runtime.js.map +1 -0
  166. package/node_modules/@dharta/contracts/dist/index.d.ts +6 -0
  167. package/node_modules/@dharta/contracts/dist/index.d.ts.map +1 -0
  168. package/node_modules/@dharta/contracts/dist/index.js +6 -0
  169. package/node_modules/@dharta/contracts/dist/index.js.map +1 -0
  170. package/node_modules/@dharta/contracts/dist/model-auth.d.ts +31 -0
  171. package/node_modules/@dharta/contracts/dist/model-auth.d.ts.map +1 -0
  172. package/node_modules/@dharta/contracts/dist/model-auth.js +45 -0
  173. package/node_modules/@dharta/contracts/dist/model-auth.js.map +1 -0
  174. package/node_modules/@dharta/contracts/dist/tasks.d.ts +44 -0
  175. package/node_modules/@dharta/contracts/dist/tasks.d.ts.map +1 -0
  176. package/node_modules/@dharta/contracts/dist/tasks.js +127 -0
  177. package/node_modules/@dharta/contracts/dist/tasks.js.map +1 -0
  178. package/node_modules/@dharta/contracts/dist/wire-events.d.ts +71 -0
  179. package/node_modules/@dharta/contracts/dist/wire-events.d.ts.map +1 -0
  180. package/node_modules/@dharta/contracts/dist/wire-events.js +48 -0
  181. package/node_modules/@dharta/contracts/dist/wire-events.js.map +1 -0
  182. package/node_modules/@dharta/contracts/model-auth-capabilities-v1.json +168 -0
  183. package/node_modules/@dharta/contracts/model-auth-reasons-v1.json +38 -0
  184. package/node_modules/@dharta/contracts/package.json +44 -0
  185. package/node_modules/@dharta/contracts/src/activity.ts +215 -0
  186. package/node_modules/@dharta/contracts/src/artifact-runtime-formats.ts +54 -0
  187. package/node_modules/@dharta/contracts/src/artifact-runtime-validators.generated.ts +29 -0
  188. package/node_modules/@dharta/contracts/src/artifact-runtime.generated.ts +2093 -0
  189. package/node_modules/@dharta/contracts/src/artifact-runtime.ts +520 -0
  190. package/node_modules/@dharta/contracts/src/index.ts +5 -0
  191. package/node_modules/@dharta/contracts/src/model-auth.ts +82 -0
  192. package/node_modules/@dharta/contracts/src/tasks.ts +142 -0
  193. package/node_modules/@dharta/contracts/src/wire-events.ts +150 -0
  194. package/node_modules/@dharta/contracts/wire-events.json +29 -0
  195. package/node_modules/@dharta/sdk/LICENSE +1 -1
  196. package/node_modules/@dharta/sdk/README.md +22 -8
  197. package/node_modules/@dharta/sdk/dist/errors.d.ts +9 -7
  198. package/node_modules/@dharta/sdk/dist/errors.d.ts.map +1 -1
  199. package/node_modules/@dharta/sdk/dist/errors.js +36 -18
  200. package/node_modules/@dharta/sdk/dist/errors.js.map +1 -1
  201. package/node_modules/@dharta/sdk/dist/fleet.d.ts +69 -0
  202. package/node_modules/@dharta/sdk/dist/fleet.d.ts.map +1 -0
  203. package/node_modules/@dharta/sdk/dist/fleet.js +240 -0
  204. package/node_modules/@dharta/sdk/dist/fleet.js.map +1 -0
  205. package/node_modules/@dharta/sdk/dist/http.d.ts +6 -2
  206. package/node_modules/@dharta/sdk/dist/http.d.ts.map +1 -1
  207. package/node_modules/@dharta/sdk/dist/http.js +71 -12
  208. package/node_modules/@dharta/sdk/dist/http.js.map +1 -1
  209. package/node_modules/@dharta/sdk/dist/idempotency.d.ts +12 -0
  210. package/node_modules/@dharta/sdk/dist/idempotency.d.ts.map +1 -0
  211. package/node_modules/@dharta/sdk/dist/idempotency.js +72 -0
  212. package/node_modules/@dharta/sdk/dist/idempotency.js.map +1 -0
  213. package/node_modules/@dharta/sdk/dist/index.d.ts +20 -6
  214. package/node_modules/@dharta/sdk/dist/index.d.ts.map +1 -1
  215. package/node_modules/@dharta/sdk/dist/index.js +27 -4
  216. package/node_modules/@dharta/sdk/dist/index.js.map +1 -1
  217. package/node_modules/@dharta/sdk/dist/resources.d.ts +108 -13
  218. package/node_modules/@dharta/sdk/dist/resources.d.ts.map +1 -1
  219. package/node_modules/@dharta/sdk/dist/resources.js +403 -57
  220. package/node_modules/@dharta/sdk/dist/resources.js.map +1 -1
  221. package/node_modules/@dharta/sdk/dist/types.d.ts +436 -16
  222. package/node_modules/@dharta/sdk/dist/types.d.ts.map +1 -1
  223. package/node_modules/@dharta/sdk/dist/types.js +31 -2
  224. package/node_modules/@dharta/sdk/dist/types.js.map +1 -1
  225. package/node_modules/@dharta/sdk/package.json +9 -3
  226. package/node_modules/@dharta/sdk/src/errors.ts +41 -18
  227. package/node_modules/@dharta/sdk/src/fleet.ts +339 -0
  228. package/node_modules/@dharta/sdk/src/http.ts +110 -17
  229. package/node_modules/@dharta/sdk/src/idempotency.ts +101 -0
  230. package/node_modules/@dharta/sdk/src/index.ts +50 -11
  231. package/node_modules/@dharta/sdk/src/resources.ts +549 -68
  232. package/node_modules/@dharta/sdk/src/types.ts +629 -20
  233. package/package.json +16 -6
  234. package/src/agent.ts +3 -2
  235. package/src/api-error.ts +6 -5
  236. package/src/cli.ts +978 -163
  237. package/src/command-tree.ts +13 -0
  238. package/src/commands/agents.ts +365 -167
  239. package/src/commands/analytics.ts +6 -5
  240. package/src/commands/api.ts +73 -92
  241. package/src/commands/appearance.ts +5 -6
  242. package/src/commands/artifact.ts +7 -6
  243. package/src/commands/audit-log.ts +2 -1
  244. package/src/commands/auth.ts +25 -5
  245. package/src/commands/caps.ts +11 -15
  246. package/src/commands/deploy.ts +18 -4
  247. package/src/commands/dev.ts +86 -25
  248. package/src/commands/doctor.ts +69 -0
  249. package/src/commands/embed-keys.ts +4 -14
  250. package/src/commands/env.ts +4 -7
  251. package/src/commands/files.ts +3 -4
  252. package/src/commands/fleet.ts +672 -0
  253. package/src/commands/genui.ts +59 -0
  254. package/src/commands/git-credential.ts +2 -3
  255. package/src/commands/kartas.ts +45 -11
  256. package/src/commands/keys.ts +152 -35
  257. package/src/commands/login.ts +2 -1
  258. package/src/commands/members.ts +31 -6
  259. package/src/commands/model-connections.ts +547 -0
  260. package/src/commands/open.ts +5 -4
  261. package/src/commands/org.ts +57 -0
  262. package/src/commands/organization.ts +153 -0
  263. package/src/commands/parity.ts +1 -3
  264. package/src/commands/repos.ts +23 -0
  265. package/src/commands/rollback.ts +2 -3
  266. package/src/commands/schedules.ts +33 -22
  267. package/src/commands/scopes.ts +8 -1
  268. package/src/commands/sessions.ts +9 -3
  269. package/src/commands/setup.ts +5 -4
  270. package/src/commands/update.ts +28 -12
  271. package/src/commands/webhooks.ts +30 -19
  272. package/src/completions.ts +25 -13
  273. package/src/config.ts +141 -9
  274. package/src/context.ts +34 -7
  275. package/src/docs/cli-reference.ts +206 -0
  276. package/src/exit-codes.ts +122 -0
  277. package/src/genui.ts +1398 -0
  278. package/src/harnesses.ts +2 -1
  279. package/src/jq.ts +109 -0
  280. package/src/login-offer.ts +6 -2
  281. package/src/manifest.ts +134 -0
  282. package/src/model-auth.ts +173 -0
  283. package/src/model-settings.ts +222 -0
  284. package/src/money.ts +5 -1
  285. package/src/output.ts +60 -0
  286. package/src/runtime.ts +1 -1
  287. package/src/scaffold.ts +2 -1
  288. package/src/validators.ts +18 -11
  289. package/src/version-check.ts +1 -1
  290. package/dist/commands/model-keys.d.ts +0 -22
  291. package/dist/commands/model-keys.js +0 -222
  292. package/dist/commands/model-keys.js.map +0 -1
  293. package/src/commands/model-keys.ts +0 -278
package/dist/genui.js ADDED
@@ -0,0 +1,1130 @@
1
+ // RFC 0056 generative UI: the CLI-side mirror of the data plane's `[genui]`
2
+ // contract, plus the compiler that turns a source folder into the bundle the
3
+ // data plane serves.
4
+ //
5
+ // SOURCE OF TRUTH: apps/dharta-python/src/dharta/genui/manifest.py (the
6
+ // `[genui]` table) and apps/dharta-python/src/dharta/genui/bundle.py (the
7
+ // `dharta.genui-bundle.v1` file). Those two files decide what a release is
8
+ // allowed to carry; everything here is a local mirror whose only job is to fail
9
+ // on the developer's machine instead of at deploy. Change either Python file and
10
+ // this one has to move with it. test/genui-crossplane.test.ts reads both files
11
+ // out of the monorepo and pins every constant below against them. The widget
12
+ // re-validates the same shape a third time in
13
+ // sdks/widget/src/transport/managedAgents.ts (`coerceGenuiCatalog`).
14
+ //
15
+ // The mirror is hand-written rather than generated from a @dharta/contracts
16
+ // schema because that package's established pattern is a language-neutral JSON
17
+ // file pinned by conformance tests on BOTH sides (see its README). The Python
18
+ // side of that pin lives in apps/dharta-python, so a half-pinned schema would
19
+ // claim a guarantee it does not have - and the CLI would gain a runtime
20
+ // dependency it does not otherwise need.
21
+ import { createHash } from "node:crypto";
22
+ import { promises as fs } from "node:fs";
23
+ import * as path from "node:path";
24
+ import TOML from "@iarna/toml";
25
+ import { setGenuiBundleSha256, tableScalarLiteral } from "./manifest.js";
26
+ // --- The contract, mirrored -------------------------------------------------
27
+ export const BUNDLE_FORMAT = "dharta.genui-bundle.v1";
28
+ export const MAX_BUNDLE_BYTES = 5 * 1024 * 1024;
29
+ export const MAX_COMPONENTS = 128;
30
+ export const MAX_WORKER_BYTES = 500 * 1024;
31
+ export const MAX_HTML_BYTES = 1024 * 1024;
32
+ export const MAX_EVENTS = 64;
33
+ export const MAX_EVENT_NAME_LENGTH = 128;
34
+ export const EVENT_CADENCES = ["agent_action", "reference", "side_effect"];
35
+ // Must equal bundle.py's TIER1_BUILTINS exactly: a Tier-1 component is only an
36
+ // alias for one of Dharta's own host-rendered primitives.
37
+ export const TIER1_BUILTINS = [
38
+ "Badge",
39
+ "Button",
40
+ "Card",
41
+ "Checkbox",
42
+ "Column",
43
+ "Divider",
44
+ "Heading",
45
+ "Row",
46
+ "Select",
47
+ "Text",
48
+ "TextField",
49
+ ];
50
+ // manifest.py's _IDENTIFIER, for both catalog_id and component names.
51
+ export const IDENTIFIER = /^[A-Za-z][A-Za-z0-9_.:/@-]{0,127}$/;
52
+ // Where `dharta genui build` looks for component sources when
53
+ // `[genui.build] source` is unset. One directory per component, named exactly
54
+ // as the component.
55
+ export const DEFAULT_SOURCE_DIR = "genui/components";
56
+ export const COMPONENT_SPEC_FILE = "component.json";
57
+ export const TIER2_SOURCE_FILE = "worker.js";
58
+ export const TIER3_SOURCE_FILE = "index.html";
59
+ // The data plane's own wording for the two path rejections in
60
+ // bundle.py::_read_regular_file. `checkGenui` reports them verbatim, because a
61
+ // pre-flight refusal should read exactly like the release rejection it
62
+ // predicts; the build appends the offending path, because there the developer
63
+ // is being told which link to remove.
64
+ const CROSSES_SYMLINK = "[genui] bundle path crosses a non-directory or symlink";
65
+ const NOT_A_REGULAR_FILE = "[genui] bundle must be a regular file";
66
+ /** A `[genui]` section, or a bundle, that the data plane would reject. */
67
+ export class GenuiError extends Error {
68
+ }
69
+ function plainObject(value) {
70
+ return typeof value === "object" && value !== null && !Array.isArray(value);
71
+ }
72
+ /**
73
+ * Parse the `[genui]` table out of a loaded `dharta.toml`. Returns null when
74
+ * there is no section (generative UI is opt-in); throws `GenuiError` on a
75
+ * section the data plane would reject.
76
+ */
77
+ export function parseGenuiSection(manifest, options = {}) {
78
+ const requireDigest = options.requireDigest !== false;
79
+ const raw = manifest.genui;
80
+ if (raw === undefined)
81
+ return null;
82
+ if (!plainObject(raw))
83
+ throw new GenuiError("[genui] must be a table");
84
+ const catalogId = raw.catalog_id;
85
+ if (typeof catalogId !== "string" || !IDENTIFIER.test(catalogId.trim())) {
86
+ throw new GenuiError("[genui] requires a non-empty string catalog_id");
87
+ }
88
+ const rawBundle = raw.bundle;
89
+ if (rawBundle !== undefined && (typeof rawBundle !== "string" || !rawBundle.trim())) {
90
+ throw new GenuiError("[genui] bundle must be a non-empty string path when set");
91
+ }
92
+ const bundle = typeof rawBundle === "string" ? rawBundle.trim() : null;
93
+ if (options.text !== undefined)
94
+ rejectFloatVersion(options.text);
95
+ const version = raw.version === undefined ? 1 : raw.version;
96
+ if (typeof version !== "number" || !Number.isInteger(version) || version < 1) {
97
+ throw new GenuiError("[genui] version must be a positive integer");
98
+ }
99
+ const digest = raw.bundle_sha256;
100
+ const digestValid = typeof digest === "string" && /^[0-9a-f]{64}$/.test(digest);
101
+ if (bundle !== null) {
102
+ if (requireDigest && !digestValid) {
103
+ throw new GenuiError("[genui] bundle_sha256 must be a lowercase 64-character SHA-256 digest");
104
+ }
105
+ }
106
+ else if (digest !== undefined) {
107
+ throw new GenuiError("[genui] bundle_sha256 requires bundle");
108
+ }
109
+ const enabled = raw.enabled === undefined ? true : raw.enabled;
110
+ if (typeof enabled !== "boolean")
111
+ throw new GenuiError("[genui] enabled must be a boolean");
112
+ return {
113
+ catalogId: catalogId.trim(),
114
+ version,
115
+ bundle,
116
+ bundleSha256: digestValid ? digest : null,
117
+ components: parseComponents(raw.components === undefined ? [] : raw.components, bundle !== null),
118
+ enabled,
119
+ source: parseBuildTable(raw.build),
120
+ };
121
+ }
122
+ // `[genui.build]` is a CLI-only sub-table: `dharta genui build` reads it, the
123
+ // release contract never does. Safe because manifest.py's parse_genui reads
124
+ // only its own known keys off the section mapping (catalog_id, bundle, version,
125
+ // bundle_sha256, enabled, components) and nothing in releases.py rejects
126
+ // unrecognized keys, so an extra sub-table is simply ignored at deploy. Keeping
127
+ // it out of `[genui]` itself means a CLI-only key can never collide with a key
128
+ // the data plane later adds there.
129
+ function parseBuildTable(raw) {
130
+ if (raw === undefined)
131
+ return DEFAULT_SOURCE_DIR;
132
+ if (!plainObject(raw))
133
+ throw new GenuiError("[genui.build] must be a table");
134
+ const source = raw.source;
135
+ if (source === undefined)
136
+ return DEFAULT_SOURCE_DIR;
137
+ if (typeof source !== "string" || !source.trim()) {
138
+ throw new GenuiError("[genui.build] source must be a non-empty string path when set");
139
+ }
140
+ return source.trim();
141
+ }
142
+ // `@iarna/toml` returns a JavaScript number for both `version = 1` and
143
+ // `version = 1.0`, so by the time the value reaches us the two are identical.
144
+ // Python's tomllib keeps them apart: `1.0` is a float, and manifest.py's
145
+ // `isinstance(version, int)` rejects it. Read the literal back out of the
146
+ // manifest text so the deploy-time failure surfaces here instead.
147
+ //
148
+ // Best effort by construction: it only sees a plain `[genui]` table with a
149
+ // `version = ...` line. An inline (`genui = { version = 1.0 }`) or dotted
150
+ // (`genui.version = 1.0`) spelling is invisible to it and still fails at
151
+ // deploy, with this same message.
152
+ function rejectFloatVersion(text) {
153
+ const literal = tableScalarLiteral(text, "genui", "version");
154
+ if (literal === null || !isTomlFloatLiteral(literal))
155
+ return;
156
+ throw new GenuiError("[genui] version must be a positive integer");
157
+ }
158
+ function isTomlFloatLiteral(literal) {
159
+ if (/^[+-]?(inf|nan)$/.test(literal))
160
+ return true;
161
+ if (/^[+-]?0[xob]/i.test(literal))
162
+ return false; // hex/octal/binary are integers
163
+ return /[.eE]/.test(literal);
164
+ }
165
+ function parseComponents(raw, hasBundle) {
166
+ if (!Array.isArray(raw))
167
+ throw new GenuiError("[genui] components must be an array of tables");
168
+ const out = [];
169
+ const seen = new Set();
170
+ for (const entry of raw) {
171
+ if (!plainObject(entry))
172
+ throw new GenuiError("each [genui] component must be a table");
173
+ const rawName = entry.name;
174
+ if (typeof rawName !== "string" || !IDENTIFIER.test(rawName.trim())) {
175
+ throw new GenuiError("each [genui] component needs a non-empty name");
176
+ }
177
+ const name = rawName.trim();
178
+ if (seen.has(name))
179
+ throw new GenuiError(`duplicate genui component: '${name}'`);
180
+ seen.add(name);
181
+ const tier = entry.tier === undefined ? 1 : entry.tier;
182
+ if (tier !== 1 && tier !== 2 && tier !== 3) {
183
+ throw new GenuiError(`component '${name}' tier must be one of 1, 2, 3`);
184
+ }
185
+ if (tier >= 2 && !hasBundle) {
186
+ throw new GenuiError(`component '${name}' is tier ${tier} (builder code) but [genui] declares no bundle`);
187
+ }
188
+ out.push({ name, tier });
189
+ }
190
+ return out;
191
+ }
192
+ // --- bundle validation (mirror of bundle.py _validate_payload) --------------
193
+ /**
194
+ * The catalog the data plane synthesizes when `[genui]` declares no bundle
195
+ * (bundle.py load_genui_bundle_with_bytes). It runs the SAME payload validation
196
+ * over it, so a Tier-1-only catalog is not unvalidated: a component whose name
197
+ * is not a Dharta builtin, or a 129th component, is rejected at deploy.
198
+ */
199
+ export function synthesizeBundlePayload(section) {
200
+ return {
201
+ format: BUNDLE_FORMAT,
202
+ catalog_id: section.catalogId,
203
+ version: section.version,
204
+ components: section.components.map((component) => ({
205
+ name: component.name,
206
+ tier: component.tier,
207
+ implementation: { builtin: component.name },
208
+ })),
209
+ };
210
+ }
211
+ /**
212
+ * Throw `GenuiError` unless `payload` is a bundle this manifest can ship.
213
+ *
214
+ * `rawText` is the bundle file's bytes as read off disk, when there was one.
215
+ * The whole integer rule lives here because JavaScript cannot see it in the
216
+ * parsed value: `JSON.parse` collapses `1.0` to `1`, while Python's
217
+ * `json.loads` yields a float that fails `isinstance(..., int)`.
218
+ */
219
+ export function validateBundlePayload(payload, section, rawText) {
220
+ if (!plainObject(payload))
221
+ throw new GenuiError("[genui] bundle root must be an object");
222
+ if (payload.format !== BUNDLE_FORMAT) {
223
+ throw new GenuiError(`[genui] bundle format must be '${BUNDLE_FORMAT}'`);
224
+ }
225
+ if (rawText !== undefined)
226
+ rejectNonIntegerLiterals(rawText);
227
+ if (payload.catalog_id !== section.catalogId ||
228
+ typeof payload.version !== "number" ||
229
+ !Number.isInteger(payload.version) ||
230
+ payload.version !== section.version) {
231
+ throw new GenuiError("[genui] bundle identity does not match dharta.toml");
232
+ }
233
+ const rawComponents = payload.components;
234
+ if (!Array.isArray(rawComponents) || rawComponents.length > MAX_COMPONENTS) {
235
+ throw new GenuiError(`[genui] bundle components must be an array of at most ${MAX_COMPONENTS} entries`);
236
+ }
237
+ const actual = new Map();
238
+ for (const component of rawComponents) {
239
+ if (!plainObject(component)) {
240
+ throw new GenuiError("[genui] every bundled component must be an object");
241
+ }
242
+ const name = component.name;
243
+ const tier = component.tier;
244
+ if (typeof name !== "string" ||
245
+ !name ||
246
+ actual.has(name) ||
247
+ (tier !== 1 && tier !== 2 && tier !== 3)) {
248
+ throw new GenuiError("[genui] bundled component name/tier is invalid or duplicated");
249
+ }
250
+ actual.set(name, tier);
251
+ if (component.props_schema !== undefined && !plainObject(component.props_schema)) {
252
+ throw new GenuiError(`[genui] component '${name}' props_schema must be an object`);
253
+ }
254
+ const events = component.events === undefined ? {} : component.events;
255
+ if (!plainObject(events)) {
256
+ throw new GenuiError(`[genui] component '${name}' events must be an object`);
257
+ }
258
+ validateEvents(name, events);
259
+ if (!plainObject(component.implementation)) {
260
+ throw new GenuiError(`[genui] component '${name}' needs an implementation object`);
261
+ }
262
+ validateImplementation(name, tier, component.implementation);
263
+ }
264
+ const expected = new Map(section.components.map((c) => [c.name, c.tier]));
265
+ const matches = actual.size === expected.size &&
266
+ [...expected].every(([name, tier]) => actual.get(name) === tier);
267
+ if (!matches) {
268
+ throw new GenuiError("[genui] bundled components do not exactly match dharta.toml " +
269
+ `(dharta.toml: ${describeComponents(expected)}; bundle: ${describeComponents(actual)})`);
270
+ }
271
+ }
272
+ function describeComponents(components) {
273
+ const entries = [...components].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
274
+ return entries.length === 0
275
+ ? "(none)"
276
+ : entries.map(([name, tier]) => `${name}@tier${tier}`).join(", ");
277
+ }
278
+ function validateImplementation(name, tier, impl) {
279
+ if (tier === 1) {
280
+ const keys = Object.keys(impl);
281
+ const builtin = impl.builtin;
282
+ if (keys.length !== 1 ||
283
+ keys[0] !== "builtin" ||
284
+ typeof builtin !== "string" ||
285
+ !TIER1_BUILTINS.includes(builtin)) {
286
+ throw new GenuiError(`[genui] Tier-1 component '${name}' must alias a Dharta builtin ` +
287
+ `(one of ${TIER1_BUILTINS.join(", ")})`);
288
+ }
289
+ // A built-in name may only alias itself. The widget merges a registered
290
+ // catalog OVER the built-in one, so `{ name: "Card", builtin: "Badge" }`
291
+ // reads as an alias but takes the built-in's name, and every Card in the
292
+ // transcript - including surfaces that never asked for this catalog - would
293
+ // draw as a Badge.
294
+ if (TIER1_BUILTINS.includes(name) && builtin !== name) {
295
+ throw new GenuiError(`[genui] Tier-1 component '${name}' takes a built-in component name and aliases ` +
296
+ `'${builtin}'; a built-in name may only alias itself. Rename the component, or ` +
297
+ `drop the alias so '${name}' renders as the built-in '${name}'.`);
298
+ }
299
+ return;
300
+ }
301
+ // A builder component may not take a built-in's name: the renderer resolves
302
+ // by name, so a Tier-2/3 component called `Card` would shadow the host
303
+ // primitive every other surface expects. Tier-1 entries are aliases FOR those
304
+ // primitives, so they keep the name.
305
+ if (TIER1_BUILTINS.includes(name)) {
306
+ throw new GenuiError(`[genui] Tier-${tier} component '${name}' may not reuse the name of a Dharta builtin; ` +
307
+ "rename it, or declare it as a Tier-1 alias");
308
+ }
309
+ const field = tier === 2 ? "worker" : "html";
310
+ const limit = tier === 2 ? MAX_WORKER_BYTES : MAX_HTML_BYTES;
311
+ const source = impl[field];
312
+ const keys = Object.keys(impl);
313
+ if (keys.length !== 1 ||
314
+ keys[0] !== field ||
315
+ typeof source !== "string" ||
316
+ Buffer.byteLength(source, "utf8") > limit) {
317
+ throw new GenuiError(`[genui] Tier-${tier} component '${name}' needs bounded ${field} source ` +
318
+ `(a string of at most ${limit} bytes)`);
319
+ }
320
+ }
321
+ function validateEvents(name, events) {
322
+ const entries = Object.entries(events);
323
+ if (entries.length > MAX_EVENTS) {
324
+ throw new GenuiError(`[genui] component '${name}' declares too many events`);
325
+ }
326
+ for (const [eventName, raw] of entries) {
327
+ if (!eventName ||
328
+ // Python's len() counts code points, so an emoji costs 1 there and 2 in
329
+ // UTF-16. Count the same units it does.
330
+ Array.from(eventName).length > MAX_EVENT_NAME_LENGTH ||
331
+ !plainObject(raw) ||
332
+ !EVENT_CADENCES.includes(raw.cadence)) {
333
+ throw new GenuiError(`[genui] component '${name}' has an invalid event declaration '${eventName}' ` +
334
+ `(each event needs cadence = ${EVENT_CADENCES.join(" | ")})`);
335
+ }
336
+ // `context_schema: null` is accepted: Python tests `is not None` first.
337
+ if (raw.context_schema !== undefined &&
338
+ raw.context_schema !== null &&
339
+ !plainObject(raw.context_schema)) {
340
+ throw new GenuiError(`[genui] component '${name}' event schema must be an object`);
341
+ }
342
+ }
343
+ }
344
+ /**
345
+ * Reject a numeric literal the contract requires to be an integer but which was
346
+ * written as a float: the root object's `version`, and `tier` inside the root's
347
+ * `components` array. Python's `json.loads` returns a float for `1.0` / `1e0`
348
+ * and the data plane's `isinstance(..., int)` refuses it, while JavaScript has
349
+ * a single number type and cannot tell after parsing.
350
+ *
351
+ * Depth-tracked so it judges only those two positions: a component's
352
+ * `props_schema` may legitimately describe a `version` or `tier` of its own,
353
+ * and that is builder data, not the bundle's contract.
354
+ */
355
+ function rejectNonIntegerLiterals(text) {
356
+ // The key that opened each container: null for the root object and for the
357
+ // objects inside an array, "components" for the components array itself.
358
+ const containers = [];
359
+ let lastKey = null;
360
+ for (let index = 0; index < text.length; index++) {
361
+ const char = text[index];
362
+ if (char === '"') {
363
+ let end = index + 1;
364
+ while (end < text.length && text[end] !== '"')
365
+ end += text[end] === "\\" ? 2 : 1;
366
+ lastKey = text.slice(index + 1, end);
367
+ index = end;
368
+ continue;
369
+ }
370
+ if (char === "{" || char === "[") {
371
+ containers.push(lastKey);
372
+ lastKey = null;
373
+ continue;
374
+ }
375
+ if (char === "}" || char === "]") {
376
+ containers.pop();
377
+ lastKey = null;
378
+ continue;
379
+ }
380
+ if (char !== ":")
381
+ continue;
382
+ const atRoot = containers.length === 1;
383
+ const inComponent = containers.length === 3 && containers[1] === "components" && containers[2] === null;
384
+ if (!(atRoot && lastKey === "version") && !(inComponent && lastKey === "tier"))
385
+ continue;
386
+ const literal = text.slice(index + 1).match(/^\s*([^\s,}\]]+)/)?.[1];
387
+ if (literal === undefined || !isJsonFloatLiteral(literal))
388
+ continue;
389
+ throw new GenuiError(lastKey === "version"
390
+ ? "[genui] bundle identity does not match dharta.toml"
391
+ : "[genui] bundled component name/tier is invalid or duplicated");
392
+ }
393
+ }
394
+ function isJsonFloatLiteral(literal) {
395
+ return /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/.test(literal) && /[.eE]/.test(literal);
396
+ }
397
+ /**
398
+ * The first number literal in `text` that a JavaScript double cannot carry, or
399
+ * null when there is none. Two shapes qualify, and both are silent:
400
+ *
401
+ * An integer beyond `Number.MAX_SAFE_INTEGER`. `JSON.parse` rounds
402
+ * `12345678901234567890` to `12345678901234567000` and nothing downstream can
403
+ * tell, so a build would write digits into the bundle that the source never
404
+ * had - and the data plane, whose `int` is arbitrary precision, would read the
405
+ * builder's own digits back out of its own copy.
406
+ *
407
+ * A float that overflows a double. `1e400` parses to `Infinity`, which
408
+ * `JSON.stringify` writes as `null`: the bundle would carry no number at all
409
+ * where the source had one, while Python's `float` reads the same literal as
410
+ * `inf` and keeps it.
411
+ *
412
+ * Every other float is left alone. `0.1` and `1.5e300` are IEEE doubles on both
413
+ * planes, so the two lose exactly the same precision and still agree.
414
+ *
415
+ * The only place the real number still exists is the raw text, so read it
416
+ * there, the same way `rejectNonIntegerLiterals` reads `version` and `tier`.
417
+ */
418
+ function findUnsafeNumberLiteral(text) {
419
+ // One frame per open container. `base` is the path of the container itself;
420
+ // `key` and `index` say where the container's NEXT value goes.
421
+ const frames = [];
422
+ // The path of the value about to be read.
423
+ const at = () => {
424
+ const frame = frames[frames.length - 1];
425
+ if (frame === undefined)
426
+ return "the document root";
427
+ if (frame.array)
428
+ return `${frame.base}[${frame.index}]`;
429
+ return frame.base === "" ? frame.key : `${frame.base}.${frame.key}`;
430
+ };
431
+ for (let index = 0; index < text.length; index++) {
432
+ const char = text[index];
433
+ if (char === '"') {
434
+ let end = index + 1;
435
+ while (end < text.length && text[end] !== '"')
436
+ end += text[end] === "\\" ? 2 : 1;
437
+ const value = text.slice(index + 1, end);
438
+ index = end;
439
+ // A string followed by `:` is its object's next key. Every other string
440
+ // is a value, and a value never moves the path.
441
+ const frame = frames[frames.length - 1];
442
+ if (frame !== undefined && !frame.array && /^\s*:/.test(text.slice(index + 1))) {
443
+ frame.key = value;
444
+ }
445
+ continue;
446
+ }
447
+ if (char === "{" || char === "[") {
448
+ frames.push({
449
+ array: char === "[",
450
+ base: frames.length === 0 ? "" : at(),
451
+ key: "",
452
+ index: 0,
453
+ });
454
+ continue;
455
+ }
456
+ if (char === "}" || char === "]") {
457
+ frames.pop();
458
+ continue;
459
+ }
460
+ if (char === ",") {
461
+ const frame = frames[frames.length - 1];
462
+ if (frame?.array)
463
+ frame.index += 1;
464
+ continue;
465
+ }
466
+ if (char !== "-" && (char < "0" || char > "9"))
467
+ continue;
468
+ const literal = text.slice(index).match(/^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?/)?.[0];
469
+ if (literal === undefined)
470
+ continue;
471
+ index += literal.length - 1;
472
+ if (/^-?\d+$/.test(literal)) {
473
+ // BigInt, not Number: the comparison has to be exact at the boundary,
474
+ // and Number() has already lost the digits that decide it.
475
+ const digits = literal.startsWith("-") ? literal.slice(1) : literal;
476
+ if (BigInt(digits) > BigInt(Number.MAX_SAFE_INTEGER)) {
477
+ return { path: at(), literal, kind: "integer" };
478
+ }
479
+ }
480
+ else if (!Number.isFinite(Number(literal))) {
481
+ return { path: at(), literal, kind: "overflow" };
482
+ }
483
+ }
484
+ return null;
485
+ }
486
+ // --- canonical serialization ------------------------------------------------
487
+ /**
488
+ * Byte-for-byte reproducible JSON, matching the data plane's own canonical form
489
+ * (`json.dumps(..., ensure_ascii=True, separators=(",", ":"), sort_keys=True)`):
490
+ * sorted keys, compact separators, ASCII only. The digest recorded in
491
+ * dharta.toml is over these exact bytes, so a rebuild of unchanged sources must
492
+ * produce an unchanged file.
493
+ *
494
+ * Written out key by key rather than sorted into an object and handed to
495
+ * `JSON.stringify`, because a JavaScript object does not preserve the order you
496
+ * insert integer-like keys in: it emits those first, in ascending numeric
497
+ * order. `{"1","10","2","alpha"}` is Python's order and `{"1","2","10","alpha"}`
498
+ * is what an object round-trip produces, which is a different digest for the
499
+ * same catalog.
500
+ *
501
+ * Numbers are JavaScript doubles, and this writes back whatever `JSON.parse`
502
+ * produced. `1.0` therefore serializes as `1` and `-0.0` as `0` - the source
503
+ * spelling is already gone by the time the value gets here, and re-deriving it
504
+ * is not possible. Both forms are the same double on the data plane too, so
505
+ * this only ever narrows the spelling, never the value.
506
+ *
507
+ * Two shapes a double cannot carry at all: an integer beyond
508
+ * `Number.MAX_SAFE_INTEGER`, which arrives already rounded, and a float that
509
+ * overflows a double, which arrives as `Infinity` and would be written out as
510
+ * `null`. `findUnsafeNumberLiteral` refuses both in a component.json rather
511
+ * than letting the build ship a number the source never wrote.
512
+ */
513
+ export function canonicalJson(value) {
514
+ return escapeNonAscii(encodeCanonical(value));
515
+ }
516
+ function encodeCanonical(value) {
517
+ if (Array.isArray(value))
518
+ return `[${value.map(encodeCanonical).join(",")}]`;
519
+ if (plainObject(value)) {
520
+ const members = Object.keys(value)
521
+ .sort(byCodePoint)
522
+ .map((key) => `${JSON.stringify(key)}:${encodeCanonical(value[key])}`);
523
+ return `{${members.join(",")}}`;
524
+ }
525
+ return JSON.stringify(value) ?? "null";
526
+ }
527
+ // Python sorts by Unicode code point; JavaScript's default sort compares UTF-16
528
+ // code units, which orders astral-plane keys differently. Compare code points so
529
+ // a builder's exotic props_schema key cannot make the two planes disagree.
530
+ function byCodePoint(a, b) {
531
+ const ca = Array.from(a);
532
+ const cb = Array.from(b);
533
+ for (let i = 0; i < Math.min(ca.length, cb.length); i++) {
534
+ const delta = (ca[i].codePointAt(0) ?? 0) - (cb[i].codePointAt(0) ?? 0);
535
+ if (delta !== 0)
536
+ return delta;
537
+ }
538
+ return ca.length - cb.length;
539
+ }
540
+ function escapeNonAscii(json) {
541
+ // U+007F..U+FFFF, matching Python's ensure_ascii, which escapes everything
542
+ // outside the printable ASCII range - DEL included.
543
+ return json.replace(/[\u007f-\uffff]/g, (char) => `\\u${char.charCodeAt(0).toString(16).padStart(4, "0")}`);
544
+ }
545
+ export function sha256Hex(bytes) {
546
+ return createHash("sha256").update(bytes).digest("hex");
547
+ }
548
+ /** Read `dir/dharta.toml`. Throws when it is missing or unparseable. */
549
+ export async function readManifest(dir) {
550
+ const manifestPath = path.join(dir, "dharta.toml");
551
+ let text;
552
+ try {
553
+ text = await fs.readFile(manifestPath, "utf8");
554
+ }
555
+ catch {
556
+ throw new GenuiError(`No dharta.toml at ${manifestPath}. Run this from an agent folder, or pass its path.`);
557
+ }
558
+ try {
559
+ return { path: manifestPath, text, parsed: TOML.parse(text) };
560
+ }
561
+ catch (error) {
562
+ throw new GenuiError(`Could not parse ${manifestPath}: ${error.message}`);
563
+ }
564
+ }
565
+ /**
566
+ * Read and parse an agent's `[genui]` section once. Every command path threads
567
+ * the result rather than re-reading the manifest: `dharta dev`'s doctor already
568
+ * holds a parsed one and hands it straight in.
569
+ */
570
+ export async function loadGenui(source, options = {}) {
571
+ const manifest = typeof source === "string" ? await readManifest(source) : source;
572
+ return {
573
+ ...manifest,
574
+ section: parseGenuiSection(manifest.parsed, { ...options, text: manifest.text }),
575
+ };
576
+ }
577
+ // Release-relative paths are split on "/" only, matching pathlib's Path.parts
578
+ // on the POSIX data plane: there, a backslash is an ordinary filename
579
+ // character, and treating it as a separator here would reject paths the server
580
+ // accepts.
581
+ function relativeParts(relative) {
582
+ return relative.split("/").filter((part) => part !== "" && part !== ".");
583
+ }
584
+ // A release-relative path may not escape the agent folder - the data plane
585
+ // rejects the same thing in load_genui_bundle.
586
+ function requireInsideRelease(relative, label) {
587
+ if (path.isAbsolute(relative) ||
588
+ relative.startsWith("/") ||
589
+ relativeParts(relative).includes("..")) {
590
+ throw new GenuiError(`[genui] ${label} must stay inside the release: ${relative}`);
591
+ }
592
+ return relative;
593
+ }
594
+ /**
595
+ * Walk a release-relative path's parent components, refusing a symlink or a
596
+ * non-directory exactly the way bundle.py's `_read_regular_file` does. Returns
597
+ * the absolute leaf path.
598
+ *
599
+ * `withPath` names the offending component in the message. The read path leaves
600
+ * it off so a pre-flight refusal reads exactly like the release rejection it
601
+ * predicts; the build turns it on, because there the developer needs to know
602
+ * which link to remove.
603
+ */
604
+ async function walkBundleParents(root, relative, options) {
605
+ const parts = relativeParts(relative);
606
+ let cursor = root;
607
+ for (const part of parts.slice(0, -1)) {
608
+ cursor = path.join(cursor, part);
609
+ let stats;
610
+ try {
611
+ stats = await fs.lstat(cursor);
612
+ }
613
+ catch {
614
+ // Python reaches a missing parent as "crosses a non-directory". The
615
+ // rejection is the same either way; which sentence is more useful
616
+ // depends on the caller, so it decides.
617
+ if (options.onMissingParent === "throw") {
618
+ throw new GenuiError(missingBundleMessage(relative));
619
+ }
620
+ return null;
621
+ }
622
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
623
+ throw new GenuiError(options.withPath
624
+ ? `${CROSSES_SYMLINK}: ${path.relative(root, cursor)}`
625
+ : CROSSES_SYMLINK);
626
+ }
627
+ }
628
+ return path.join(root, ...parts);
629
+ }
630
+ function missingBundleMessage(relative) {
631
+ return `[genui] bundle does not exist: ${relative} - run \`dharta genui build\` to build it.`;
632
+ }
633
+ /**
634
+ * Read the bundle exactly the way bundle.py's `_read_regular_file` does: no
635
+ * symlink anywhere on the path, a regular file at the leaf, and nothing larger
636
+ * than the cap. `lstat` throughout, because the point is the link itself, not
637
+ * what it points at - a symlinked bundle resolves fine locally and is refused
638
+ * by the release validator's O_NOFOLLOW open.
639
+ */
640
+ async function readBundleBytes(root, relative) {
641
+ const file = await walkBundleParents(root, relative, {
642
+ withPath: false,
643
+ onMissingParent: "throw",
644
+ });
645
+ let stats;
646
+ try {
647
+ stats = await fs.lstat(file);
648
+ }
649
+ catch {
650
+ throw new GenuiError(missingBundleMessage(relative));
651
+ }
652
+ if (stats.isSymbolicLink() || !stats.isFile())
653
+ throw new GenuiError(NOT_A_REGULAR_FILE);
654
+ if (stats.size > MAX_BUNDLE_BYTES) {
655
+ throw new GenuiError(`${NOT_A_REGULAR_FILE} no larger than 5 MiB`);
656
+ }
657
+ return fs.readFile(file);
658
+ }
659
+ /**
660
+ * The write-side of the same policy. Without it the build happily writes
661
+ * THROUGH a symlink that every later read then refuses, so `build` would report
662
+ * success and `check` would immediately contradict it.
663
+ */
664
+ async function assertBundleWritable(root, relative) {
665
+ const file = await walkBundleParents(root, relative, {
666
+ withPath: true,
667
+ onMissingParent: "stop", // mkdir will create it; nothing to inspect yet
668
+ });
669
+ if (file === null)
670
+ return;
671
+ const stats = await fs.lstat(file).catch(() => null);
672
+ if (stats === null)
673
+ return; // does not exist yet: the normal first build
674
+ if (stats.isSymbolicLink() || !stats.isFile()) {
675
+ throw new GenuiError(`${NOT_A_REGULAR_FILE}: ${relative}`);
676
+ }
677
+ }
678
+ /**
679
+ * Report every reason the data plane would reject this agent's `[genui]`
680
+ * declaration. Shared by `dharta genui check`, `dharta dev`'s doctor
681
+ * (warnings), and the deploy pre-flight (refusal) so all three say the same
682
+ * thing. Pass an already-read manifest to avoid a second read.
683
+ */
684
+ export async function checkGenui(source) {
685
+ let manifest;
686
+ try {
687
+ manifest = typeof source === "string" ? await readManifest(source) : source;
688
+ }
689
+ catch {
690
+ // A missing or unparseable dharta.toml is not a genui problem: the doctor
691
+ // and the deploy both report it in their own words, on their own path.
692
+ return { manifest: path.join(source, "dharta.toml"), section: null, problems: [] };
693
+ }
694
+ const dir = path.dirname(manifest.path);
695
+ let section;
696
+ try {
697
+ section = parseGenuiSection(manifest.parsed, { text: manifest.text });
698
+ }
699
+ catch (error) {
700
+ return { manifest: manifest.path, section: null, problems: [error.message] };
701
+ }
702
+ // No section: generative UI is off. Disabled: the data plane skips the
703
+ // catalog entirely (releases.py _validate_genui_tree), so nothing below can
704
+ // fail a deploy.
705
+ if (section === null || !section.enabled) {
706
+ return { manifest: manifest.path, section, problems: [] };
707
+ }
708
+ const problems = [];
709
+ try {
710
+ // A Tier-1-only catalog carries no file, but the data plane still
711
+ // synthesizes and validates a payload for it, so we do too.
712
+ if (section.bundle === null) {
713
+ validateBundlePayload(synthesizeBundlePayload(section), section);
714
+ }
715
+ else {
716
+ requireInsideRelease(section.bundle, "bundle");
717
+ const raw = await readBundleBytes(dir, section.bundle);
718
+ const digest = sha256Hex(raw);
719
+ if (digest !== section.bundleSha256) {
720
+ problems.push("[genui] bundle bytes do not match bundle_sha256 " +
721
+ `(${section.bundle} is ${digest}, dharta.toml records ${section.bundleSha256}) - ` +
722
+ "run `dharta genui build` to rebuild it and re-record the digest.");
723
+ }
724
+ else {
725
+ const text = decodeUtf8(raw, `[genui] bundle must be valid UTF-8 JSON`);
726
+ let payload;
727
+ try {
728
+ payload = JSON.parse(text);
729
+ }
730
+ catch {
731
+ throw new GenuiError("[genui] bundle must be valid UTF-8 JSON");
732
+ }
733
+ validateBundlePayload(payload, section, text);
734
+ }
735
+ }
736
+ }
737
+ catch (error) {
738
+ problems.push(error.message);
739
+ }
740
+ return { manifest: manifest.path, section, problems };
741
+ }
742
+ // Decode UTF-8 strictly. Node's default replaces an invalid byte with U+FFFD,
743
+ // which would silently ship a mangled worker or document.
744
+ function decodeUtf8(bytes, message) {
745
+ try {
746
+ return new TextDecoder("utf-8", { fatal: true }).decode(bytes);
747
+ }
748
+ catch {
749
+ throw new GenuiError(message);
750
+ }
751
+ }
752
+ /**
753
+ * Deploy pre-flight: refuse locally, before anything is pushed or uploaded,
754
+ * with the message the release validator would return.
755
+ */
756
+ export async function genuiPreflight(dir, options = {}) {
757
+ const { section, problems } = await checkGenui(dir);
758
+ if (options.deployFiles && section?.enabled && section.bundle) {
759
+ problems.push(...(await missingFromDeploy(dir, section.bundle, options.deployFiles)));
760
+ }
761
+ if (problems.length === 0)
762
+ return;
763
+ throw new Error("Generative-UI pre-flight failed - the release would be rejected:\n" +
764
+ problems.map((problem) => ` ${problem}`).join("\n") +
765
+ "\n\n Fix the [genui] section in dharta.toml (or run `dharta genui build`), then re-deploy.");
766
+ }
767
+ async function missingFromDeploy(dir, bundle, deployFiles) {
768
+ // A bundle that is not on disk is already reported by checkGenui, with the
769
+ // fix that actually applies: build it. "git is not shipping it" is true of
770
+ // every file that does not exist, so saying it here would name a second
771
+ // cause that is only a consequence of the first.
772
+ const onDisk = await fs
773
+ .stat(path.join(dir, ...relativeParts(bundle)))
774
+ .then(() => true)
775
+ .catch(() => false);
776
+ if (!onDisk)
777
+ return [];
778
+ // Listing can fail for reasons that are the deploy's own to report (a nested
779
+ // repository, a git that will not run). An advisory pre-flight never blocks
780
+ // on its inability to answer.
781
+ const files = await deployFiles().catch(() => null);
782
+ if (files === null)
783
+ return [];
784
+ // Compare normalized paths, not the two spellings of one path. A bundle
785
+ // declared with a leading "./" or a doubled slash names the file git lists
786
+ // plainly, on both planes: pathlib's Path.parts drops the same "." and empty
787
+ // segments relativeParts does. Comparing the raw strings would refuse a
788
+ // bundle the deploy is carrying, with a git-ignore diagnosis that is wrong.
789
+ const wanted = relativeParts(bundle).join("/");
790
+ if (files.some((file) => relativeParts(file).join("/") === wanted))
791
+ return [];
792
+ return [
793
+ `${wanted} is not among the files this deploy ships, so the release would fail on a ` +
794
+ "missing [genui] bundle. Git ignore rules exclude it: unignore it, or force-add it " +
795
+ `with \`git add -f ${wanted}\`, then commit it and re-run \`dharta deploy\`.`,
796
+ ];
797
+ }
798
+ /** `dharta genui check`: the structured result. Throws when something is wrong. */
799
+ export async function genuiCheckData(dir) {
800
+ const manifest = await readManifest(dir); // a missing manifest is an error here
801
+ const { section, problems } = await checkGenui(manifest);
802
+ if (problems.length > 0) {
803
+ throw new Error("[genui] check failed - a deploy would be rejected:\n" +
804
+ problems.map((problem) => ` ${problem}`).join("\n"));
805
+ }
806
+ if (section === null) {
807
+ return { manifest: manifest.path, configured: false, components: [] };
808
+ }
809
+ return {
810
+ manifest: manifest.path,
811
+ configured: true,
812
+ catalogId: section.catalogId,
813
+ version: section.version,
814
+ enabled: section.enabled,
815
+ bundle: section.bundle,
816
+ bundleSha256: section.bundleSha256,
817
+ components: section.components,
818
+ };
819
+ }
820
+ export function formatGenuiStatus(status) {
821
+ if (!status.configured) {
822
+ return `No [genui] section in ${status.manifest} - generative UI is off for this agent.`;
823
+ }
824
+ const count = status.components.length;
825
+ const lines = [
826
+ `✓ [genui] catalog '${status.catalogId}' v${status.version}: ` +
827
+ `${count} component${count === 1 ? "" : "s"} (${describeTiers(status.components)})`,
828
+ ];
829
+ if (status.enabled === false) {
830
+ // The catalog is registered but not served, and neither the data plane nor
831
+ // this check read its bundle. Saying the digest matches would be a claim
832
+ // nothing verified.
833
+ lines.push(" enabled = false: the catalog is not served, so its bundle was not checked.");
834
+ }
835
+ else if (status.bundle === null) {
836
+ lines.push(" No bundle: every component is Tier 1, so Dharta renders them with its own primitives.");
837
+ }
838
+ else {
839
+ lines.push(` Bundle ${status.bundle} matches bundle_sha256 ${status.bundleSha256}.`);
840
+ }
841
+ return lines.join("\n");
842
+ }
843
+ function describeTiers(components) {
844
+ const parts = [1, 2, 3]
845
+ .map((tier) => ({ tier, count: components.filter((c) => c.tier === tier).length }))
846
+ .filter((entry) => entry.count > 0)
847
+ .map((entry) => `${entry.count} Tier ${entry.tier}`);
848
+ return parts.length > 0 ? parts.join(", ") : "none";
849
+ }
850
+ const SPEC_KEYS = ["tier", "builtin", "props_schema", "events"];
851
+ /**
852
+ * Compile `[genui.build] source` into a validated bundle payload. Nothing is
853
+ * written: this is the seam at the data plane's contract, so a caller can
854
+ * assert on the exact object a release would carry.
855
+ */
856
+ export async function compileBundlePayload(section, dir) {
857
+ requireInsideRelease(section.source, "source");
858
+ const sourceDir = path.join(dir, section.source);
859
+ await requireExactMatch(section, sourceDir, await discoverComponents(sourceDir, section.source));
860
+ const components = await Promise.all(section.components.map((component) => compileComponent(sourceDir, section.source, component)));
861
+ const payload = { format: BUNDLE_FORMAT, catalog_id: section.catalogId, version: section.version, components };
862
+ validateBundlePayload(payload, section);
863
+ return payload;
864
+ }
865
+ /**
866
+ * Compile `[genui.build] source` into the bundle at `[genui] bundle` and record
867
+ * its digest in dharta.toml. Validates everything the data plane validates
868
+ * before writing, so a bad catalog fails here rather than at deploy.
869
+ */
870
+ export async function buildGenui(dir, log) {
871
+ const { path: manifestPath, text, section } = await loadGenui(dir, { requireDigest: false });
872
+ if (section === null) {
873
+ throw new GenuiError(`No [genui] section in ${manifestPath}. Add one (see the commented block a ` +
874
+ "scaffolded dharta.toml ships with) before building a component bundle.");
875
+ }
876
+ if (section.bundle === null) {
877
+ // parseGenuiSection already rejected a Tier-2/3 component without a bundle,
878
+ // so reaching here means Tier-1 only. Nothing is written, but the catalog
879
+ // the data plane will synthesize still has to be valid.
880
+ validateBundlePayload(synthesizeBundlePayload(section), section);
881
+ log(`Nothing to build: [genui] declares no bundle and all ${section.components.length} ` +
882
+ "component(s) are Tier 1, which Dharta renders with its own primitives.");
883
+ return { status: "nothing-to-build", components: section.components.length };
884
+ }
885
+ requireInsideRelease(section.bundle, "bundle");
886
+ const payload = await compileBundlePayload(section, dir);
887
+ const digest = await writeBundle(dir, { manifestPath, text, section }, payload);
888
+ log(`✓ Built ${section.components.length} component${section.components.length === 1 ? "" : "s"} ` +
889
+ `(${describeTiers(section.components)}) -> ${section.bundle}`);
890
+ log(` bundle_sha256 = "${digest}" recorded in dharta.toml`);
891
+ if (!section.enabled) {
892
+ log(" Note: enabled = false, so this catalog is registered but not served.");
893
+ }
894
+ return {
895
+ status: "built",
896
+ bundle: section.bundle,
897
+ digest,
898
+ components: section.components.length,
899
+ };
900
+ }
901
+ async function writeBundle(dir, manifest, payload) {
902
+ const { manifestPath, text, section } = manifest;
903
+ const relative = section.bundle;
904
+ const bytes = Buffer.from(`${canonicalJson(payload)}\n`, "utf8");
905
+ if (bytes.byteLength > MAX_BUNDLE_BYTES) {
906
+ throw new GenuiError(`Built bundle is ${bytes.byteLength} bytes; the limit is ${MAX_BUNDLE_BYTES}. ` +
907
+ "Move the largest component to a leaner implementation or split the catalog.");
908
+ }
909
+ const digest = sha256Hex(bytes);
910
+ // Refuse to write through a symlink the read side would then reject: `build`
911
+ // must not report success that `check` immediately contradicts.
912
+ await assertBundleWritable(dir, relative);
913
+ // Record the digest, in memory, before anything lands. A manifest whose
914
+ // [genui] table cannot be patched (an inline table, or dotted keys) must fail
915
+ // with setGenuiBundleSha256's hand-edit guidance and leave no half-built tree:
916
+ // a bundle on disk with no matching digest is exactly the state `check`
917
+ // exists to catch.
918
+ const patched = setGenuiBundleSha256(text, digest);
919
+ const bundlePath = path.join(dir, ...relativeParts(relative));
920
+ await fs.mkdir(path.dirname(bundlePath), { recursive: true });
921
+ await fs.writeFile(bundlePath, bytes);
922
+ // Read back what actually landed, BEFORE dharta.toml records the digest. An
923
+ // editor, a filter driver, or a CRLF checkout can change the bytes between
924
+ // write and hash, and a manifest patched first would be left naming a digest
925
+ // the file on disk does not have - the state this check exists to prevent.
926
+ const written = sha256Hex(await fs.readFile(bundlePath));
927
+ if (written !== digest) {
928
+ throw new GenuiError(`Wrote ${relative} but it hashes to ${written}, not the expected ${digest}. ` +
929
+ "Something is rewriting the file on disk (a Git filter or editor hook is the " +
930
+ "usual cause); the release would be rejected. dharta.toml was left untouched, so " +
931
+ "it still records whatever digest it had before this build.");
932
+ }
933
+ try {
934
+ await fs.writeFile(manifestPath, patched, "utf8");
935
+ }
936
+ catch (error) {
937
+ // The bundle is on disk and dharta.toml still names the previous digest,
938
+ // so `check` reports a mismatch until the two agree. Nothing is lost: the
939
+ // build is a pure function of the sources, so a rerun produces the same
940
+ // bytes and records the same digest.
941
+ throw new GenuiError(`Wrote ${relative}, but could not record its digest in ${manifestPath}: ` +
942
+ `${error.message}. The bundle is built and correct; only the manifest ` +
943
+ "is behind, so `dharta genui check` reports a digest mismatch until it is written. " +
944
+ "Fix the cause (a read-only file, a full disk), then run `dharta genui build` " +
945
+ "again - it rebuilds the same bytes, so repeating it is safe.");
946
+ }
947
+ return digest;
948
+ }
949
+ /**
950
+ * Every component directory under `sourceDir`, named by its path relative to
951
+ * it. A directory holding a `component.json` IS a component and is not
952
+ * descended into, so a namespaced name like `charts/Bar` (legal in the
953
+ * identifier both planes share) lives at `charts/Bar/component.json`.
954
+ *
955
+ * Unlike the bundle path these follow symlinks - the source layout is a CLI
956
+ * concern the data plane never sees, and `readdir`'s Dirent would report a
957
+ * symlinked component folder as a link rather than a directory and silently
958
+ * hide it.
959
+ */
960
+ async function discoverComponents(sourceDir, label) {
961
+ const found = [];
962
+ const walk = async (absolute, relative) => {
963
+ let names;
964
+ try {
965
+ names = await fs.readdir(absolute);
966
+ }
967
+ catch {
968
+ if (relative !== "")
969
+ return;
970
+ throw new GenuiError(`[genui.build] source folder not found: ${label}. Create one directory per component ` +
971
+ `there (for example ${label}/Card/${COMPONENT_SPEC_FILE}), or set ` +
972
+ '`source = "<path>"` in [genui.build].');
973
+ }
974
+ for (const name of names.sort()) {
975
+ // Dot-directories are tooling, never components.
976
+ if (name.startsWith("."))
977
+ continue;
978
+ const child = path.join(absolute, name);
979
+ const stats = await fs.stat(child).catch(() => null);
980
+ if (!stats?.isDirectory())
981
+ continue;
982
+ const childRelative = relative === "" ? name : `${relative}/${name}`;
983
+ const isComponent = await fs
984
+ .stat(path.join(child, COMPONENT_SPEC_FILE))
985
+ .then((spec) => spec.isFile())
986
+ .catch(() => false);
987
+ if (isComponent)
988
+ found.push(childRelative);
989
+ else
990
+ await walk(child, childRelative);
991
+ }
992
+ };
993
+ await walk(sourceDir, "");
994
+ return found.sort();
995
+ }
996
+ // The manifest's `components` array is the builder's declaration of what this
997
+ // release ships and at what trust tier. A build never edits it: a mismatch is a
998
+ // question only the builder can answer, so name both sides and stop.
999
+ async function requireExactMatch(section, sourceDir, found) {
1000
+ const declared = new Set(section.components.map((c) => c.name));
1001
+ const onDisk = new Set(found);
1002
+ const missing = [...declared].filter((name) => !onDisk.has(name)).sort();
1003
+ const extra = found.filter((name) => !declared.has(name));
1004
+ if (missing.length === 0 && extra.length === 0)
1005
+ return;
1006
+ // A component is DISCOVERED by its component.json, so a folder that exists
1007
+ // but has no spec reads as "missing" here. Say the more useful thing.
1008
+ for (const name of missing) {
1009
+ const folder = componentDir(sourceDir, section.source, name);
1010
+ const exists = await fs.stat(folder).then((st) => st.isDirectory()).catch(() => false);
1011
+ if (exists) {
1012
+ throw new GenuiError(`[genui] component '${name}': ${section.source}/${name}/${COMPONENT_SPEC_FILE} is missing.`);
1013
+ }
1014
+ }
1015
+ const lines = ["[genui] source folders do not match the components declared in dharta.toml."];
1016
+ if (missing.length > 0) {
1017
+ lines.push(` Declared but missing from ${section.source}/: ${missing.join(", ")}`);
1018
+ }
1019
+ if (extra.length > 0) {
1020
+ lines.push(` Present in ${section.source}/ but not declared: ${extra.join(", ")}`);
1021
+ }
1022
+ lines.push(" Add the missing folders, or add/remove entries in the [genui] components array.", " `dharta genui build` never edits that array - the trust tier is your declaration.");
1023
+ throw new GenuiError(lines.join("\n"));
1024
+ }
1025
+ // A component name is only ever a JSON string on the data plane, but the CLI
1026
+ // turns it into a path - and the identifier both planes share permits "." and
1027
+ // "/", so "a/../b" is a legal name. Resolve here, once, and refuse to climb out
1028
+ // of the source folder.
1029
+ function componentDir(sourceDir, label, name) {
1030
+ const parts = relativeParts(name);
1031
+ if (name.startsWith("/") || parts.includes("..")) {
1032
+ throw new GenuiError(`[genui] component '${name}' cannot be a source path: it escapes ${label}/.`);
1033
+ }
1034
+ return path.join(sourceDir, ...parts);
1035
+ }
1036
+ async function compileComponent(sourceDir, label, component) {
1037
+ const dir = componentDir(sourceDir, label, component.name);
1038
+ const where = `${label}/${component.name}/${COMPONENT_SPEC_FILE}`;
1039
+ let raw;
1040
+ try {
1041
+ raw = await fs.readFile(path.join(dir, COMPONENT_SPEC_FILE), "utf8");
1042
+ }
1043
+ catch {
1044
+ throw new GenuiError(`[genui] component '${component.name}': ${where} is missing.`);
1045
+ }
1046
+ let parsed;
1047
+ try {
1048
+ parsed = JSON.parse(raw);
1049
+ }
1050
+ catch (error) {
1051
+ throw new GenuiError(`[genui] component '${component.name}': ${where} is not valid JSON (${error.message}).`);
1052
+ }
1053
+ if (!plainObject(parsed)) {
1054
+ throw new GenuiError(`[genui] component '${component.name}': ${where} must be a JSON object.`);
1055
+ }
1056
+ const unsafe = findUnsafeNumberLiteral(raw);
1057
+ if (unsafe !== null) {
1058
+ throw new GenuiError(`[genui] component '${component.name}': ${where} has the number ${unsafe.literal} at ` +
1059
+ `${unsafe.path}, which ` +
1060
+ (unsafe.kind === "integer"
1061
+ ? "is outside the range JavaScript represents exactly (up to " +
1062
+ `${Number.MAX_SAFE_INTEGER}). The build would round it and ship a number you ` +
1063
+ "never wrote, while the data plane keeps yours exactly."
1064
+ : "overflows a JavaScript double to Infinity. The build would ship `null` where " +
1065
+ "you wrote a number, while the data plane reads the same literal as a float " +
1066
+ "and keeps it.") +
1067
+ " Write it as a string, or use a number a double can hold.");
1068
+ }
1069
+ const unknownKeys = Object.keys(parsed).filter((key) => !SPEC_KEYS.includes(key));
1070
+ if (unknownKeys.length > 0) {
1071
+ throw new GenuiError(`[genui] component '${component.name}': ${where} has unknown key(s) ` +
1072
+ `${unknownKeys.join(", ")}. Supported: ${SPEC_KEYS.join(", ")}.`);
1073
+ }
1074
+ const tier = parsed.tier;
1075
+ if (tier !== 1 && tier !== 2 && tier !== 3) {
1076
+ throw new GenuiError(`[genui] component '${component.name}': ${where} needs tier 1, 2, or 3.`);
1077
+ }
1078
+ if (tier !== component.tier) {
1079
+ throw new GenuiError(`[genui] component '${component.name}': ${where} declares tier ${tier} but ` +
1080
+ `dharta.toml declares tier ${component.tier}. The manifest is the trust declaration; ` +
1081
+ "make them agree.");
1082
+ }
1083
+ if (parsed.builtin !== undefined && typeof parsed.builtin !== "string") {
1084
+ throw new GenuiError(`[genui] component '${component.name}': ${where} builtin must be a string.`);
1085
+ }
1086
+ for (const key of ["props_schema", "events"]) {
1087
+ if (parsed[key] !== undefined && !plainObject(parsed[key])) {
1088
+ throw new GenuiError(`[genui] component '${component.name}': ${where} ${key} must be an object.`);
1089
+ }
1090
+ }
1091
+ const spec = {
1092
+ builtin: parsed.builtin,
1093
+ props_schema: parsed.props_schema,
1094
+ events: parsed.events,
1095
+ };
1096
+ const out = { name: component.name, tier: component.tier };
1097
+ if (spec.props_schema !== undefined)
1098
+ out.props_schema = spec.props_schema;
1099
+ if (spec.events !== undefined)
1100
+ out.events = spec.events;
1101
+ out.implementation = await readImplementation(dir, label, component, spec);
1102
+ return out;
1103
+ }
1104
+ async function readImplementation(dir, label, component, spec) {
1105
+ if (component.tier === 1) {
1106
+ // A Tier-1 component is an alias for a Dharta builtin; the common case is
1107
+ // that the folder is named after the builtin it aliases.
1108
+ return { builtin: spec.builtin ?? component.name };
1109
+ }
1110
+ if (spec.builtin !== undefined) {
1111
+ throw new GenuiError(`[genui] component '${component.name}': builtin is Tier-1 only; ` +
1112
+ `a Tier-${component.tier} component ships its own source.`);
1113
+ }
1114
+ const file = component.tier === 2 ? TIER2_SOURCE_FILE : TIER3_SOURCE_FILE;
1115
+ const field = component.tier === 2 ? "worker" : "html";
1116
+ const where = `${label}/${component.name}/${file}`;
1117
+ let bytes;
1118
+ try {
1119
+ bytes = await fs.readFile(path.join(dir, file));
1120
+ }
1121
+ catch {
1122
+ throw new GenuiError(`[genui] component '${component.name}' is tier ${component.tier}, so it needs ${where}.`);
1123
+ }
1124
+ // The bundle is UTF-8 JSON. Decoding leniently would replace an invalid byte
1125
+ // with U+FFFD and ship a silently corrupted worker or document.
1126
+ return {
1127
+ [field]: decodeUtf8(bytes, `[genui] component '${component.name}': ${where} is not valid UTF-8.`),
1128
+ };
1129
+ }
1130
+ //# sourceMappingURL=genui.js.map