@yourrealm/sdk 0.0.0-alpha.1378.ce03c8dc

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 (230) hide show
  1. package/LICENSE-EXCEPTION +26 -0
  2. package/README.md +34 -0
  3. package/dist/chunk-3R6KEFOI.js +22 -0
  4. package/dist/chunk-67BDAMYX.js +376 -0
  5. package/dist/chunk-AITGS27R.js +139 -0
  6. package/dist/chunk-CMAEHQ4F.js +6 -0
  7. package/dist/chunk-MLKGABMK.js +9 -0
  8. package/dist/chunk-O3UIK34B.js +1430 -0
  9. package/dist/chunk-VHTUSZUX.js +176 -0
  10. package/dist/cli.js +9006 -0
  11. package/dist/docker.js +54 -0
  12. package/dist/index.js +1053 -0
  13. package/dist/ingress.js +436 -0
  14. package/dist/jsx-runtime.js +2 -0
  15. package/dist/preact.js +22 -0
  16. package/dist/service-url.js +11 -0
  17. package/dist/services.js +213 -0
  18. package/dist/test.js +5069 -0
  19. package/dist/ui.js +254 -0
  20. package/dist/worker-protocol.js +89 -0
  21. package/package.json +112 -0
  22. package/src/app-icon.ts +74 -0
  23. package/src/apply.ts +52 -0
  24. package/src/bundle-ui.ts +211 -0
  25. package/src/bundle.ts +546 -0
  26. package/src/cli/build-dev-landing.ts +84 -0
  27. package/src/cli/build-install-panel.ts +90 -0
  28. package/src/cli/build-runtime.ts +133 -0
  29. package/src/cli/build.ts +63 -0
  30. package/src/cli/dev.ts +107 -0
  31. package/src/cli/down.ts +60 -0
  32. package/src/cli/main.ts +56 -0
  33. package/src/cli/test.ts +61 -0
  34. package/src/cli/validate.ts +37 -0
  35. package/src/compile.ts +100 -0
  36. package/src/create-app.ts +982 -0
  37. package/src/deep-merge.ts +33 -0
  38. package/src/dev/fixture.ts +48 -0
  39. package/src/dev/install-handler.ts +383 -0
  40. package/src/dev/server.ts +968 -0
  41. package/src/dev/stack.ts +108 -0
  42. package/src/dev/state.ts +159 -0
  43. package/src/docker.ts +96 -0
  44. package/src/errors.ts +61 -0
  45. package/src/factory.ts +481 -0
  46. package/src/garage.ts +56 -0
  47. package/src/generator/hash.ts +6 -0
  48. package/src/generator/resolve-refs.ts +207 -0
  49. package/src/grants-builder.ts +100 -0
  50. package/src/grants.ts +260 -0
  51. package/src/home-api.ts +172 -0
  52. package/src/home-cookie.ts +10 -0
  53. package/src/home-registry.ts +120 -0
  54. package/src/home.ts +187 -0
  55. package/src/host-registry.ts +143 -0
  56. package/src/http-header.ts +20 -0
  57. package/src/identifiers.ts +49 -0
  58. package/src/index.ts +66 -0
  59. package/src/info.ts +64 -0
  60. package/src/ini.ts +50 -0
  61. package/src/json.ts +14 -0
  62. package/src/jsx-runtime/index.ts +5 -0
  63. package/src/keys.ts +29 -0
  64. package/src/mysql.ts +56 -0
  65. package/src/pack-build.ts +108 -0
  66. package/src/pack.ts +115 -0
  67. package/src/peer-registry.ts +121 -0
  68. package/src/peer-world.ts +117 -0
  69. package/src/permission-meta.ts +116 -0
  70. package/src/permissions.ts +108 -0
  71. package/src/pkg-root.ts +34 -0
  72. package/src/postgres.ts +79 -0
  73. package/src/preact/index.ts +27 -0
  74. package/src/redis.ts +43 -0
  75. package/src/ref.ts +271 -0
  76. package/src/remote-registry.ts +33 -0
  77. package/src/required-host-kinds.ts +110 -0
  78. package/src/resolve-context.ts +111 -0
  79. package/src/resolve.ts +471 -0
  80. package/src/runtime/docker/compose/compose-schema.ts +118 -0
  81. package/src/runtime/docker/compose/convert-to-docker-compose.ts +278 -0
  82. package/src/runtime/docker/compose/default-hardening.ts +21 -0
  83. package/src/runtime/docker/compose/effects.ts +107 -0
  84. package/src/runtime/docker/compose/index.ts +17 -0
  85. package/src/runtime/docker/compose/labels.ts +39 -0
  86. package/src/runtime/docker/compose/types.ts +78 -0
  87. package/src/runtime/docker/home-proxy-server/index.ts +177 -0
  88. package/src/runtime/docker/home-proxy-server/protocol.ts +53 -0
  89. package/src/runtime/docker/ingress/build-config.ts +819 -0
  90. package/src/runtime/docker/ingress/index.ts +16 -0
  91. package/src/runtime/docker/ingress/types.ts +182 -0
  92. package/src/runtime/docker/log-frames.ts +121 -0
  93. package/src/runtime/docker/logs-server/index.ts +366 -0
  94. package/src/runtime/docker/spawn-server/context-key.ts +82 -0
  95. package/src/runtime/docker/spawn-server/index.ts +492 -0
  96. package/src/runtime/docker/spawn-server/protocol.ts +70 -0
  97. package/src/runtime/docker/spawn-server/spec.ts +122 -0
  98. package/src/runtime/docker/stats-server/disks.ts +161 -0
  99. package/src/runtime/docker/stats-server/index.ts +322 -0
  100. package/src/runtime/home-client/index.ts +71 -0
  101. package/src/runtime/log-line.ts +24 -0
  102. package/src/runtime/logs-client/index.ts +175 -0
  103. package/src/runtime/spawn-client/index.ts +93 -0
  104. package/src/runtime/spawn-client/kaniko.ts +79 -0
  105. package/src/runtime/stats-client/index.ts +65 -0
  106. package/src/runtime/stats-sample.ts +95 -0
  107. package/src/safe-json.ts +37 -0
  108. package/src/sandbox.ts +1209 -0
  109. package/src/schema/build-apps.ts +38 -0
  110. package/src/schema/home.ts +217 -0
  111. package/src/schema/host.ts +792 -0
  112. package/src/schema/inputs.ts +26 -0
  113. package/src/schema/nodes.ts +75 -0
  114. package/src/schema/peer.ts +400 -0
  115. package/src/schema/public-domain.ts +116 -0
  116. package/src/schema/remote.ts +74 -0
  117. package/src/security.ts +594 -0
  118. package/src/serialize.ts +21 -0
  119. package/src/service-definition.ts +284 -0
  120. package/src/service-url.ts +64 -0
  121. package/src/service.ts +39 -0
  122. package/src/services/index.ts +5 -0
  123. package/src/shared.ts +65 -0
  124. package/src/speculative-silo.ts +96 -0
  125. package/src/stable-secret.ts +14 -0
  126. package/src/surfaces.ts +264 -0
  127. package/src/tailwind-bin.ts +56 -0
  128. package/src/test/boot.ts +192 -0
  129. package/src/test/compile.ts +501 -0
  130. package/src/test/describe.ts +117 -0
  131. package/src/test/index.ts +11 -0
  132. package/src/test/ingress-fixture.ts +37 -0
  133. package/src/test/types.ts +318 -0
  134. package/src/trusted-headers.ts +79 -0
  135. package/src/types.ts +126 -0
  136. package/src/ui/components/aside.tsx +96 -0
  137. package/src/ui/components/badge.tsx +42 -0
  138. package/src/ui/components/button.tsx +59 -0
  139. package/src/ui/components/card.tsx +106 -0
  140. package/src/ui/components/checkbox.tsx +37 -0
  141. package/src/ui/components/code-block.tsx +29 -0
  142. package/src/ui/components/combobox.tsx +119 -0
  143. package/src/ui/components/config-field.tsx +284 -0
  144. package/src/ui/components/dialog.tsx +137 -0
  145. package/src/ui/components/domain-editor.tsx +317 -0
  146. package/src/ui/components/domain-list.ts +205 -0
  147. package/src/ui/components/dropdown-menu.tsx +73 -0
  148. package/src/ui/components/empty-state.tsx +51 -0
  149. package/src/ui/components/highlight.ts +59 -0
  150. package/src/ui/components/icons.tsx +101 -0
  151. package/src/ui/components/image-posture.tsx +85 -0
  152. package/src/ui/components/input.tsx +28 -0
  153. package/src/ui/components/label.tsx +16 -0
  154. package/src/ui/components/line-chart.tsx +113 -0
  155. package/src/ui/components/page.tsx +39 -0
  156. package/src/ui/components/permission-row.tsx +262 -0
  157. package/src/ui/components/permission-section.tsx +51 -0
  158. package/src/ui/components/pie-chart.tsx +245 -0
  159. package/src/ui/components/schema-block.tsx +60 -0
  160. package/src/ui/components/separator.tsx +42 -0
  161. package/src/ui/components/sheet.tsx +130 -0
  162. package/src/ui/components/sidebar.tsx +650 -0
  163. package/src/ui/components/silo-picker.tsx +510 -0
  164. package/src/ui/components/skeleton.tsx +13 -0
  165. package/src/ui/components/sonner.tsx +33 -0
  166. package/src/ui/components/switch.tsx +58 -0
  167. package/src/ui/components/table-view.ts +219 -0
  168. package/src/ui/components/table.tsx +506 -0
  169. package/src/ui/components/tabs.tsx +59 -0
  170. package/src/ui/components/timeago.tsx +84 -0
  171. package/src/ui/components/tooltip.tsx +56 -0
  172. package/src/ui/components.json +21 -0
  173. package/src/ui/dev/dev-shell.tsx +145 -0
  174. package/src/ui/home/home-grid.tsx +177 -0
  175. package/src/ui/hooks/use-mobile.ts +19 -0
  176. package/src/ui/index.ts +189 -0
  177. package/src/ui/install/adapter.ts +193 -0
  178. package/src/ui/install/deploy-targets-picker.tsx +185 -0
  179. package/src/ui/install/install-app-panel.tsx +912 -0
  180. package/src/ui/install/install-error.tsx +62 -0
  181. package/src/ui/install/perms-inactive.ts +50 -0
  182. package/src/ui/install/required-fields.ts +37 -0
  183. package/src/ui/install/tabs-form.tsx +557 -0
  184. package/src/ui/tokens.css +279 -0
  185. package/src/ui/utils.ts +78 -0
  186. package/src/ui-error.ts +139 -0
  187. package/src/ui-runtime/dev-landing-entry.tsx +258 -0
  188. package/src/ui-runtime/index.ts +83 -0
  189. package/src/ui-runtime/install-entry.tsx +119 -0
  190. package/src/ui-runtime/mount.ts +367 -0
  191. package/src/ui-runtime/runtime.css +56 -0
  192. package/src/ui-surface.ts +451 -0
  193. package/src/valkey.ts +43 -0
  194. package/src/worker/extract-tar.ts +125 -0
  195. package/src/worker/identity.ts +183 -0
  196. package/src/worker/index.ts +16 -0
  197. package/src/worker/public-ip.ts +92 -0
  198. package/src/worker/queue.ts +54 -0
  199. package/src/worker/reconcile.ts +127 -0
  200. package/src/worker/start-worker.ts +227 -0
  201. package/src/worker/ui-proxy.ts +335 -0
  202. package/src/worker/uplink.ts +523 -0
  203. package/src/worker/validate.ts +34 -0
  204. package/src/worker/verify-relay.ts +102 -0
  205. package/src/worker-protocol/app-dirs.ts +94 -0
  206. package/src/worker-protocol/home-proxy-paths.ts +76 -0
  207. package/src/worker-protocol/index.ts +55 -0
  208. package/src/worker-protocol/ingress.ts +166 -0
  209. package/src/worker-protocol/logs-socket-path.ts +50 -0
  210. package/src/worker-protocol/networks.ts +39 -0
  211. package/src/worker-protocol/node-version.ts +70 -0
  212. package/src/worker-protocol/silo-paths.ts +114 -0
  213. package/src/worker-protocol/spawn-paths.ts +165 -0
  214. package/src/worker-protocol/stats-socket-path.ts +40 -0
  215. package/src/worker-protocol/update-files.ts +77 -0
  216. package/src/worker-protocol/uplink/constants.ts +86 -0
  217. package/src/worker-protocol/uplink/errors.ts +61 -0
  218. package/src/worker-protocol/uplink/frame.ts +114 -0
  219. package/src/worker-protocol/uplink/identity.ts +40 -0
  220. package/src/worker-protocol/uplink/index.ts +63 -0
  221. package/src/worker-protocol/uplink/methods.ts +462 -0
  222. package/src/worker-protocol/uplink/mux.ts +646 -0
  223. package/src/worker-protocol/uplink/protocol.ts +11 -0
  224. package/src/worker-protocol/uplink/rpc.ts +309 -0
  225. package/src/worker-protocol/uplink/session.ts +540 -0
  226. package/src/worker-protocol/uplink/test-support.ts +188 -0
  227. package/src/worker-protocol/worker-mount.ts +27 -0
  228. package/src/worker-protocol/worker-state.ts +190 -0
  229. package/src/xml.ts +49 -0
  230. package/src/yaml.ts +15 -0
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Deep-merge `over` on top of `base`. Plain objects recurse; everything
3
+ * else (scalars, arrays, `{__realmRef}` secret markers) replaces wholesale.
4
+ *
5
+ * Used to fold a `configSchema`-synthesized default shape together with
6
+ * saved config values, and (in the Home eval pipeline) to fold resolved
7
+ * permission values into the same tree so `getDesiredState` receives one
8
+ * unified `config` argument.
9
+ */
10
+ export function deepMergeConfig(
11
+ base: Record<string, unknown>,
12
+ over: Record<string, unknown>,
13
+ ): Record<string, unknown> {
14
+ const out: Record<string, unknown> = { ...base };
15
+ for (const [k, v] of Object.entries(over)) {
16
+ const current = out[k];
17
+ if (isPlainMergeableObject(v) && isPlainMergeableObject(current)) {
18
+ out[k] = deepMergeConfig(current as Record<string, unknown>, v as Record<string, unknown>);
19
+ } else {
20
+ out[k] = v;
21
+ }
22
+ }
23
+ return out;
24
+ }
25
+
26
+ function isPlainMergeableObject(v: unknown): boolean {
27
+ return (
28
+ v != null &&
29
+ typeof v === "object" &&
30
+ !Array.isArray(v) &&
31
+ !("__realmRef" in (v as Record<string, unknown>))
32
+ );
33
+ }
@@ -0,0 +1,48 @@
1
+ // `realm-sdk dev` fixture loader. The dev server compiles a realm.ts against a
2
+ // CompileFixture (config + grants + peers) the same way the test harness does,
3
+ // standing in for the operator's deploy form and the peer-world. The values
4
+ // come from a `realm-sdk.dev.ts` next to the realm.ts — plain TS whose default
5
+ // export matches `CompileFixture<typeof app>` — or from an empty fixture, so
6
+ // apps needing neither config nor grants work with no file at all.
7
+
8
+ import { resolve } from "@std/path";
9
+ import type { CompileFixture } from "../test/types.ts";
10
+
11
+ const DEV_FIXTURE_FILENAME = "realm-sdk.dev.ts";
12
+
13
+ interface LoadResult {
14
+ fixture: CompileFixture<unknown>;
15
+ /** Absolute path to the loaded fixture, or null if defaults were used. */
16
+ source: string | null;
17
+ }
18
+
19
+ export async function loadDevFixture(cwd: string): Promise<LoadResult> {
20
+ const path = resolve(cwd, DEV_FIXTURE_FILENAME);
21
+ try {
22
+ await Deno.stat(path);
23
+ } catch {
24
+ // No dev fixture — fall back to empty. An app with required config or
25
+ // grants then fails to compile with an error pointing at writing one.
26
+ return {
27
+ fixture: { config: {} as never },
28
+ source: null,
29
+ };
30
+ }
31
+
32
+ // Deno resolves a dynamic import against its own URL, so the specifier is a
33
+ // file:// URL; the query busts the module cache so reloads see fixture edits.
34
+ const url = new URL(`file://${path}?t=${Date.now()}`);
35
+ const mod = await import(url.href);
36
+ const fixture = (mod.default ?? mod) as CompileFixture<unknown>;
37
+ return { fixture, source: path };
38
+ }
39
+
40
+ /**
41
+ * Stable HMAC seed string for the dev process. `compile()` runs HMAC
42
+ * against this; using `realm-dev:<cwd>` keeps two restarts in the same
43
+ * directory deterministic so passwords + `r.stableSecret(...)` values
44
+ * don't reshuffle on every reload.
45
+ */
46
+ export function stableDevSeed(cwd: string): string {
47
+ return `realm-dev:${cwd}`;
48
+ }
@@ -0,0 +1,383 @@
1
+ // Server-side handlers for the `/install` route in `realm-sdk dev`. The
2
+ // install panel (shared with Home) reaches them through the InstallAdapter in
3
+ // install-entry.tsx, which POSTs to `/install/api/preview` and
4
+ // `/install/api/install`. Install returns the new DevState and stack handle for
5
+ // server.ts to swap in; the `realm-sdk.dev.ts` write here is only persistence,
6
+ // which the file watcher ignores. Failures throw `InstallHandlerError` carrying
7
+ // a shape the browser adapter rehydrates as an `InstallErrorShape`.
8
+
9
+ import { basename, join } from "@std/path";
10
+ import type { Grants } from "../home.ts";
11
+ import type { InstallErrorShape, PreviewImage, PreviewResult } from "../ui/install/adapter.ts";
12
+ import { bundle } from "../bundle.ts";
13
+ import {
14
+ extractSchema,
15
+ extractAppKind,
16
+ type ConfigField,
17
+ type ConfigFieldTree,
18
+ type SerializablePermission,
19
+ } from "../sandbox.ts";
20
+ import { compile } from "../test/compile.ts";
21
+ import type { SiloFixture } from "../test/types.ts";
22
+ import type { SiloGrants } from "../schema/host.ts";
23
+ import { SPECULATIVE_SILO } from "../speculative-silo.ts";
24
+ import { descriptorsToGrants } from "../grants-builder.ts";
25
+ import { formatIssue, RealmCompileError } from "../errors.ts";
26
+ import { loadDevFixture, stableDevSeed } from "./fixture.ts";
27
+ import { bringUp, compileAndBundle, DevCompileError, type DevState } from "./state.ts";
28
+
29
+ /**
30
+ * Server-side counterpart to InstallAdapterError. Thrown by handlers;
31
+ * `installErrorToResponse` turns it into the wire JSON shape the
32
+ * browser install adapter parses back via `parseInstallError`.
33
+ */
34
+ export class InstallHandlerError extends Error {
35
+ constructor(public readonly shape: InstallErrorShape) {
36
+ super(shape.kind === "violations" ? "grant violations" : shape.message);
37
+ this.name = "InstallHandlerError";
38
+ }
39
+ }
40
+
41
+ export function installErrorToResponse(err: InstallHandlerError): Response {
42
+ if (err.shape.kind === "violations") {
43
+ return Response.json(
44
+ { error: "grant violations", violations: err.shape.violations },
45
+ { status: 400 },
46
+ );
47
+ }
48
+ return Response.json({ error: err.shape.message }, { status: 400 });
49
+ }
50
+
51
+ function toErrorShape(err: unknown): InstallErrorShape {
52
+ // DevCompileError carries both the flat issue list (for non-grant
53
+ // failures) and the raw violations (for grant failures). Prefer the
54
+ // structured violations shape when present so the panel renders the
55
+ // per-permission list — same UX as Home's POST /api/apps.
56
+ if (err instanceof DevCompileError) {
57
+ if (err.violations.length > 0) {
58
+ return { kind: "violations", violations: [...err.violations] };
59
+ }
60
+ const message =
61
+ err.issues.length === 1
62
+ ? formatIssue(err.issues[0]!)
63
+ : err.issues.map((i) => `- ${formatIssue(i)}`).join("\n");
64
+ return { kind: "message", message };
65
+ }
66
+ if (err instanceof RealmCompileError) {
67
+ const message =
68
+ err.issues.length === 1
69
+ ? formatIssue(err.issues[0]!)
70
+ : err.issues.map((i) => `- ${formatIssue(i)}`).join("\n");
71
+ return { kind: "message", message };
72
+ }
73
+ return { kind: "message", message: (err as Error).message };
74
+ }
75
+
76
+ /**
77
+ * Walk a config tree and synthesize defaults the same way Home's preview
78
+ * does: every leaf with a `default` lands in the object, optional
79
+ * tabs/sections stay undefined, required ones recurse in.
80
+ */
81
+ function defaultConfigValues(schema: ConfigFieldTree): Record<string, unknown> {
82
+ const out: Record<string, unknown> = {};
83
+ for (const [name, field] of Object.entries(schema) as [string, ConfigField][]) {
84
+ if (field.type === "permission") continue;
85
+ if (field.type === "tab" || field.type === "section") {
86
+ if (field.optional) continue;
87
+ out[name] = defaultConfigValues(field.fields);
88
+ } else if ("default" in field && field.default !== undefined) {
89
+ out[name] = field.default;
90
+ }
91
+ }
92
+ return out;
93
+ }
94
+
95
+ /**
96
+ * Whether an on-disk Silo record can answer what this declaration asks for.
97
+ *
98
+ * Presence alone is not enough. `compile()` checks the same fields and throws
99
+ * a message about adding `hostPath` / `gid` to a `silo:` fixture, which is
100
+ * good advice for someone writing `realm.test.ts` and useless to an operator
101
+ * clicking through the install panel - they would see it as "preview images
102
+ * skipped", pointing at a file the panel says it owns. A half-written record
103
+ * falls back to the placeholder instead, which is what a missing one does.
104
+ */
105
+ function satisfiesAsk(record: SiloFixture | undefined, ask: SiloGrants): boolean {
106
+ if (record === undefined) return false;
107
+ if (ask.volume !== undefined && (record.hostPath === undefined || record.gid === undefined)) {
108
+ return false;
109
+ }
110
+ // A require-map the record doesn't publish would fail resolution outright,
111
+ // and an unmet require is never the right answer at preview time - see
112
+ // `speculative-silo.ts`.
113
+ if (typeof ask.network === "object") {
114
+ for (const [name, port] of Object.entries(ask.network)) {
115
+ if (record.services?.[name] !== port) return false;
116
+ }
117
+ }
118
+ return true;
119
+ }
120
+
121
+ /**
122
+ * Point every `host.silo` declaration at a Silo the speculative compile can
123
+ * resolve, and return the grants and Silo records to compile against.
124
+ *
125
+ * The preview grants every permission straight off the descriptors, and a Silo
126
+ * is the one grant a descriptor cannot supply: the operator picks it. So the
127
+ * map comes back empty at that path and `sanitizeGrants` drops the grant for
128
+ * carrying no id, leaving the app to crash on `config.silo.volume`. Two ways
129
+ * out, in order of fidelity:
130
+ *
131
+ * 1. `realm-sdk.dev.ts` already describes a Silo that can satisfy the ask.
132
+ * Use it - previewing against a real directory beats any placeholder.
133
+ * Note the panel cannot produce this state on its own: an install writes
134
+ * back only `{config, grants}` (`renderDevFixture`), never `silo:`
135
+ * records, so this branch belongs to an author who hand-wrote the fixture
136
+ * the way they would for `realm.test.ts`.
137
+ * 2. Anything else - nothing on disk, or a record that cannot answer what
138
+ * this declaration asks for. Stand in a synthetic Silo built from the
139
+ * same constants Home's speculative resolver uses, so both surfaces show
140
+ * the operator the same placeholder.
141
+ *
142
+ * Done here rather than by teaching `compile()` a speculative mode, which
143
+ * would be a second place that fabricates a Silo. This way the resolver runs
144
+ * unmodified against a Silo record, exactly as it does for an app's own test.
145
+ */
146
+ export function speculativeSiloFixture(
147
+ permsFlat: Record<string, SerializablePermission>,
148
+ grants: Grants,
149
+ disk: { grants?: Grants; silo?: Record<string, SiloFixture> },
150
+ ): { grants: Grants; silo: Record<string, SiloFixture> } {
151
+ const silo: Record<string, SiloFixture> = { ...disk.silo };
152
+ const out: Grants = { ...grants };
153
+ // Publications the stand-in must offer for a require-map to resolve. See
154
+ // `speculative-silo.ts` for why an unmet require is never the right answer
155
+ // at preview time.
156
+ const services: Record<string, number> = {};
157
+ let synthetic = false;
158
+
159
+ for (const [path, perm] of Object.entries(permsFlat)) {
160
+ if (perm.kind !== "host.silo") continue;
161
+ const bound = disk.grants?.[path];
162
+ const boundId =
163
+ bound?.kind === "host.silo" && typeof bound.data === "string" ? bound.data : undefined;
164
+ const ask = perm.data as SiloGrants;
165
+ if (boundId !== undefined && satisfiesAsk(silo[boundId], ask)) {
166
+ out[path] = { kind: "host.silo", data: boundId };
167
+ continue;
168
+ }
169
+ out[path] = { kind: "host.silo", data: SPECULATIVE_SILO.id };
170
+ synthetic = true;
171
+ if (typeof ask.network === "object") Object.assign(services, ask.network);
172
+ }
173
+
174
+ if (synthetic) {
175
+ silo[SPECULATIVE_SILO.id] = {
176
+ hostPath: SPECULATIVE_SILO.hostPath,
177
+ containerPath: SPECULATIVE_SILO.containerPath,
178
+ gid: SPECULATIVE_SILO.gid,
179
+ apiKey: SPECULATIVE_SILO.apiKey,
180
+ services,
181
+ };
182
+ }
183
+ return { grants: out, silo };
184
+ }
185
+
186
+ export async function handlePreview(
187
+ source: string,
188
+ ctx: { cwd: string; appName: string },
189
+ ): Promise<PreviewResult> {
190
+ let bundledCode: string;
191
+ try {
192
+ bundledCode = await bundle(source);
193
+ } catch (err) {
194
+ throw new InstallHandlerError(toErrorShape(err));
195
+ }
196
+ let extracted;
197
+ let kind: "single" | "system";
198
+ try {
199
+ extracted = await extractSchema(bundledCode);
200
+ kind = await extractAppKind(bundledCode);
201
+ } catch (err) {
202
+ throw new InstallHandlerError(toErrorShape(err));
203
+ }
204
+
205
+ // Read the existing fixture once. Used to pre-fill the panel (so
206
+ // /install on a previously-installed app turns into an edit flow)
207
+ // and to seed the speculative compile with realistic values.
208
+ const { fixture: rawFixture } = await loadDevFixture(ctx.cwd);
209
+ const currentConfig = rawFixture.config as Record<string, unknown> | undefined;
210
+ const currentGrants = rawFixture.grants as Grants | undefined;
211
+
212
+ // Speculative compile so the summary can show image cards. We grant
213
+ // every declared permission (mirrors Home's preview) and fill config
214
+ // with declared defaults. Failures are silent: the panel still shows
215
+ // permission + config rows; image cards just won't appear.
216
+ let images: PreviewImage[] | null = null;
217
+ let hasPersistentVolumes = false;
218
+ try {
219
+ const granted = descriptorsToGrants(extracted.permissionsFlat, {
220
+ enabled: new Set(Object.keys(extracted.permissionsFlat)),
221
+ grantValues: {},
222
+ });
223
+ const silos = speculativeSiloFixture(extracted.permissionsFlat, granted, {
224
+ grants: currentGrants,
225
+ silo: rawFixture.silo,
226
+ });
227
+ const fixture = {
228
+ ...rawFixture,
229
+ seed: rawFixture.seed ?? stableDevSeed(ctx.cwd),
230
+ config: defaultConfigValues(extracted.configFields) as never,
231
+ grants: silos.grants as never,
232
+ silo: silos.silo,
233
+ };
234
+ const result = await compile({ bundledCode, appName: ctx.appName, fixture });
235
+ if (!result.error) {
236
+ const refs = new Set<string>();
237
+ for (const svc of Object.values(result.app.services)) {
238
+ // `build:` services have no image ref yet — skip them.
239
+ if (svc.image) refs.add(svc.image);
240
+ }
241
+ hasPersistentVolumes = Object.values(result.app.services).some(
242
+ (s) => (s.volumes?.length ?? 0) > 0,
243
+ );
244
+ // Dev has no Home image-pin cache, so every field is null/unknown.
245
+ // The panel already renders that as "not yet pinned" cards.
246
+ images = [...refs].map(
247
+ (ref) =>
248
+ ({
249
+ ref,
250
+ digest: null,
251
+ runsAs: null,
252
+ runsAsRoot: false,
253
+ exposes: [],
254
+ volumes: [],
255
+ imageCreatedAt: null,
256
+ }) satisfies PreviewImage,
257
+ );
258
+ }
259
+ } catch (err) {
260
+ // Speculative compile failures are expected — required config blank,
261
+ // sandbox eval throws, etc. Leave images null and let the rest of the
262
+ // preview render, logging why the image cards aren't appearing.
263
+ console.warn(`[realm-dev] preview images skipped: ${(err as Error).message}`);
264
+ }
265
+
266
+ return {
267
+ permissionsFlat: extracted.permissionsFlat,
268
+ configFields: extracted.configFields,
269
+ images,
270
+ kind,
271
+ hasPersistentVolumes,
272
+ ...(currentConfig !== undefined && Object.keys(currentConfig).length > 0
273
+ ? { currentConfig }
274
+ : {}),
275
+ ...(currentGrants !== undefined && Object.keys(currentGrants).length > 0
276
+ ? { currentGrants }
277
+ : {}),
278
+ };
279
+ }
280
+
281
+ interface InstallHandlerCtx {
282
+ cwd: string;
283
+ realmFile: string;
284
+ tmpDir: string;
285
+ }
286
+
287
+ interface InstallPayloadRequest {
288
+ source: string;
289
+ config: Record<string, unknown>;
290
+ grants: Grants;
291
+ /** Mirror of the panel's DeployTargets. Ignored in dev (single local target). */
292
+ deployed?: unknown;
293
+ }
294
+
295
+ /**
296
+ * Render a `realm-sdk.dev.ts` source string from the install panel's
297
+ * `{config, grants}`. The output is the panel-managed counterpart to a
298
+ * hand-written dev fixture — same shape (`CompileFixture<typeof app>`),
299
+ * importing the local `realm.ts(x)` for type inference.
300
+ *
301
+ * `JSON.stringify(..., 2)` is sufficient as TS source: every value the
302
+ * panel collects (config inputs, grant data) is JSON-safe by
303
+ * construction (the wire shape between panel and server is JSON), and
304
+ * JSON object/array literals are valid TS expressions.
305
+ *
306
+ * Secrets land in plaintext here. Authors should gitignore this file
307
+ * if their realm.ts declares `r.input.secret(...)` fields.
308
+ */
309
+ function renderDevFixture(opts: {
310
+ realmFileBasename: string;
311
+ config: Record<string, unknown>;
312
+ grants: Grants;
313
+ }): string {
314
+ const header = [
315
+ "// Auto-generated by `realm-sdk dev`'s install panel. Overwritten on",
316
+ "// every install — if you hand-edit, run /install again with the new",
317
+ "// values or your edits will be clobbered. Plaintext secrets land",
318
+ "// here; gitignore as needed.",
319
+ ].join("\n");
320
+ return `${header}
321
+
322
+ import type { CompileFixture } from "@yourrealm/sdk/test";
323
+ import type app from "./${opts.realmFileBasename}";
324
+
325
+ export default {
326
+ config: ${JSON.stringify(opts.config, null, 2)},
327
+ grants: ${JSON.stringify(opts.grants, null, 2)},
328
+ } satisfies CompileFixture<typeof app>;
329
+ `;
330
+ }
331
+
332
+ export async function handleInstall(
333
+ payload: InstallPayloadRequest,
334
+ ctx: InstallHandlerCtx,
335
+ ): Promise<{ state: DevState; stack: Awaited<ReturnType<typeof bringUp>> }> {
336
+ let next: DevState;
337
+ try {
338
+ next = await compileAndBundle({
339
+ cwd: ctx.cwd,
340
+ realmFile: ctx.realmFile,
341
+ override: {
342
+ source: payload.source,
343
+ config: payload.config,
344
+ grants: payload.grants,
345
+ },
346
+ });
347
+ } catch (err) {
348
+ throw new InstallHandlerError(toErrorShape(err));
349
+ }
350
+
351
+ // Order: bringUp first, then persist. If `bringUp` fails (image pull
352
+ // problem, compose conflict, port collision), `realm-sdk.dev.ts`
353
+ // stays in its pre-install state — the file watcher won't loop on a
354
+ // bad fixture and the operator can retry without manual rollback.
355
+ let stack: Awaited<ReturnType<typeof bringUp>>;
356
+ try {
357
+ stack = await bringUp(next, ctx.tmpDir);
358
+ } catch (err) {
359
+ throw new InstallHandlerError(toErrorShape(err));
360
+ }
361
+
362
+ // Containers are running on the new config + grants, so persist the panel's
363
+ // choices to realm-sdk.dev.ts for the next dev server start. Identical bytes
364
+ // skip the write, keeping the file watcher quiet on a no-op install.
365
+ const fixtureSource = renderDevFixture({
366
+ realmFileBasename: basename(ctx.realmFile),
367
+ config: payload.config,
368
+ grants: payload.grants,
369
+ });
370
+ const fixturePath = join(ctx.cwd, "realm-sdk.dev.ts");
371
+ try {
372
+ const existing = await Deno.readTextFile(fixturePath).catch(() => null);
373
+ if (existing !== fixtureSource) {
374
+ await Deno.writeTextFile(fixturePath, fixtureSource);
375
+ }
376
+ } catch (err) {
377
+ throw new InstallHandlerError({
378
+ kind: "message",
379
+ message: `failed to write realm-sdk.dev.ts: ${(err as Error).message}`,
380
+ });
381
+ }
382
+ return { state: next, stack };
383
+ }