@endora-commerce/cli 0.100.0

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 (356) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +37 -0
  3. package/dist/bin/endora.d.ts +28 -0
  4. package/dist/bin/endora.d.ts.map +1 -0
  5. package/dist/bin/endora.js +926 -0
  6. package/dist/bin/endora.js.map +1 -0
  7. package/dist/check/estate.d.ts +189 -0
  8. package/dist/check/estate.d.ts.map +1 -0
  9. package/dist/check/estate.js +1037 -0
  10. package/dist/check/estate.js.map +1 -0
  11. package/dist/check/hosts.d.ts +25 -0
  12. package/dist/check/hosts.d.ts.map +1 -0
  13. package/dist/check/hosts.js +1347 -0
  14. package/dist/check/hosts.js.map +1 -0
  15. package/dist/check/index.d.ts +60 -0
  16. package/dist/check/index.d.ts.map +1 -0
  17. package/dist/check/index.js +111 -0
  18. package/dist/check/index.js.map +1 -0
  19. package/dist/check/layout.d.ts +136 -0
  20. package/dist/check/layout.d.ts.map +1 -0
  21. package/dist/check/layout.js +262 -0
  22. package/dist/check/layout.js.map +1 -0
  23. package/dist/check/ledger.d.ts +98 -0
  24. package/dist/check/ledger.d.ts.map +1 -0
  25. package/dist/check/ledger.js +173 -0
  26. package/dist/check/ledger.js.map +1 -0
  27. package/dist/check/peer-owners.d.ts +116 -0
  28. package/dist/check/peer-owners.d.ts.map +1 -0
  29. package/dist/check/peer-owners.js +225 -0
  30. package/dist/check/peer-owners.js.map +1 -0
  31. package/dist/check/report.d.ts +33 -0
  32. package/dist/check/report.d.ts.map +1 -0
  33. package/dist/check/report.js +107 -0
  34. package/dist/check/report.js.map +1 -0
  35. package/dist/check/run.d.ts +147 -0
  36. package/dist/check/run.d.ts.map +1 -0
  37. package/dist/check/run.js +111 -0
  38. package/dist/check/run.js.map +1 -0
  39. package/dist/checks.d.ts +17 -0
  40. package/dist/checks.d.ts.map +1 -0
  41. package/dist/checks.js +17 -0
  42. package/dist/checks.js.map +1 -0
  43. package/dist/dev/index.d.ts +83 -0
  44. package/dist/dev/index.d.ts.map +1 -0
  45. package/dist/dev/index.js +298 -0
  46. package/dist/dev/index.js.map +1 -0
  47. package/dist/generate/divergence.d.ts +38 -0
  48. package/dist/generate/divergence.d.ts.map +1 -0
  49. package/dist/generate/divergence.js +237 -0
  50. package/dist/generate/divergence.js.map +1 -0
  51. package/dist/generate/index.d.ts +90 -0
  52. package/dist/generate/index.d.ts.map +1 -0
  53. package/dist/generate/index.js +369 -0
  54. package/dist/generate/index.js.map +1 -0
  55. package/dist/index.d.ts +23 -0
  56. package/dist/index.d.ts.map +1 -0
  57. package/dist/index.js +35 -0
  58. package/dist/index.js.map +1 -0
  59. package/dist/inputs/declaration.d.ts +18 -0
  60. package/dist/inputs/declaration.d.ts.map +1 -0
  61. package/dist/inputs/declaration.js +64 -0
  62. package/dist/inputs/declaration.js.map +1 -0
  63. package/dist/inputs/env-file.d.ts +73 -0
  64. package/dist/inputs/env-file.d.ts.map +1 -0
  65. package/dist/inputs/env-file.js +134 -0
  66. package/dist/inputs/env-file.js.map +1 -0
  67. package/dist/inputs/prompt.d.ts +21 -0
  68. package/dist/inputs/prompt.d.ts.map +1 -0
  69. package/dist/inputs/prompt.js +59 -0
  70. package/dist/inputs/prompt.js.map +1 -0
  71. package/dist/inputs/resolve.d.ts +163 -0
  72. package/dist/inputs/resolve.d.ts.map +1 -0
  73. package/dist/inputs/resolve.js +290 -0
  74. package/dist/inputs/resolve.js.map +1 -0
  75. package/dist/install/host.d.ts +27 -0
  76. package/dist/install/host.d.ts.map +1 -0
  77. package/dist/install/host.js +90 -0
  78. package/dist/install/host.js.map +1 -0
  79. package/dist/install/index.d.ts +173 -0
  80. package/dist/install/index.d.ts.map +1 -0
  81. package/dist/install/index.js +793 -0
  82. package/dist/install/index.js.map +1 -0
  83. package/dist/install/wizard.d.ts +144 -0
  84. package/dist/install/wizard.d.ts.map +1 -0
  85. package/dist/install/wizard.js +362 -0
  86. package/dist/install/wizard.js.map +1 -0
  87. package/dist/lib/admin-artefacts.d.ts +70 -0
  88. package/dist/lib/admin-artefacts.d.ts.map +1 -0
  89. package/dist/lib/admin-artefacts.js +354 -0
  90. package/dist/lib/admin-artefacts.js.map +1 -0
  91. package/dist/lib/admin-surfaces.d.ts +298 -0
  92. package/dist/lib/admin-surfaces.d.ts.map +1 -0
  93. package/dist/lib/admin-surfaces.js +669 -0
  94. package/dist/lib/admin-surfaces.js.map +1 -0
  95. package/dist/lib/delegated-composer.d.ts +85 -0
  96. package/dist/lib/delegated-composer.d.ts.map +1 -0
  97. package/dist/lib/delegated-composer.js +241 -0
  98. package/dist/lib/delegated-composer.js.map +1 -0
  99. package/dist/lib/divergence-artefacts.d.ts +305 -0
  100. package/dist/lib/divergence-artefacts.d.ts.map +1 -0
  101. package/dist/lib/divergence-artefacts.js +828 -0
  102. package/dist/lib/divergence-artefacts.js.map +1 -0
  103. package/dist/lib/divergence.d.ts +337 -0
  104. package/dist/lib/divergence.d.ts.map +1 -0
  105. package/dist/lib/divergence.js +1005 -0
  106. package/dist/lib/divergence.js.map +1 -0
  107. package/dist/lib/docs-artefacts.d.ts +395 -0
  108. package/dist/lib/docs-artefacts.d.ts.map +1 -0
  109. package/dist/lib/docs-artefacts.js +781 -0
  110. package/dist/lib/docs-artefacts.js.map +1 -0
  111. package/dist/lib/emitted-exports.d.ts +21 -0
  112. package/dist/lib/emitted-exports.d.ts.map +1 -0
  113. package/dist/lib/emitted-exports.js +96 -0
  114. package/dist/lib/emitted-exports.js.map +1 -0
  115. package/dist/lib/emitted-freshness.d.ts +112 -0
  116. package/dist/lib/emitted-freshness.d.ts.map +1 -0
  117. package/dist/lib/emitted-freshness.js +288 -0
  118. package/dist/lib/emitted-freshness.js.map +1 -0
  119. package/dist/lib/entity-index-artefact.d.ts +95 -0
  120. package/dist/lib/entity-index-artefact.d.ts.map +1 -0
  121. package/dist/lib/entity-index-artefact.js +210 -0
  122. package/dist/lib/entity-index-artefact.js.map +1 -0
  123. package/dist/lib/instance-build-inputs.d.ts +108 -0
  124. package/dist/lib/instance-build-inputs.d.ts.map +1 -0
  125. package/dist/lib/instance-build-inputs.js +165 -0
  126. package/dist/lib/instance-build-inputs.js.map +1 -0
  127. package/dist/lib/module-docs.d.ts +473 -0
  128. package/dist/lib/module-docs.d.ts.map +1 -0
  129. package/dist/lib/module-docs.js +711 -0
  130. package/dist/lib/module-docs.js.map +1 -0
  131. package/dist/lib/module-package-subpaths.d.ts +56 -0
  132. package/dist/lib/module-package-subpaths.d.ts.map +1 -0
  133. package/dist/lib/module-package-subpaths.js +223 -0
  134. package/dist/lib/module-package-subpaths.js.map +1 -0
  135. package/dist/lib/module-packages.d.ts +200 -0
  136. package/dist/lib/module-packages.d.ts.map +1 -0
  137. package/dist/lib/module-packages.js +580 -0
  138. package/dist/lib/module-packages.js.map +1 -0
  139. package/dist/lib/module-population.d.ts +129 -0
  140. package/dist/lib/module-population.d.ts.map +1 -0
  141. package/dist/lib/module-population.js +172 -0
  142. package/dist/lib/module-population.js.map +1 -0
  143. package/dist/lib/module-roots.d.ts +337 -0
  144. package/dist/lib/module-roots.d.ts.map +1 -0
  145. package/dist/lib/module-roots.js +586 -0
  146. package/dist/lib/module-roots.js.map +1 -0
  147. package/dist/lib/nested-checkouts.d.ts +33 -0
  148. package/dist/lib/nested-checkouts.d.ts.map +1 -0
  149. package/dist/lib/nested-checkouts.js +160 -0
  150. package/dist/lib/nested-checkouts.js.map +1 -0
  151. package/dist/lib/platform-root.d.ts +43 -0
  152. package/dist/lib/platform-root.d.ts.map +1 -0
  153. package/dist/lib/platform-root.js +134 -0
  154. package/dist/lib/platform-root.js.map +1 -0
  155. package/dist/lib/platform-surface.d.ts +235 -0
  156. package/dist/lib/platform-surface.d.ts.map +1 -0
  157. package/dist/lib/platform-surface.js +393 -0
  158. package/dist/lib/platform-surface.js.map +1 -0
  159. package/dist/lib/port-registrations.d.ts +223 -0
  160. package/dist/lib/port-registrations.d.ts.map +1 -0
  161. package/dist/lib/port-registrations.js +532 -0
  162. package/dist/lib/port-registrations.js.map +1 -0
  163. package/dist/lib/read-size.d.ts +154 -0
  164. package/dist/lib/read-size.d.ts.map +1 -0
  165. package/dist/lib/read-size.js +182 -0
  166. package/dist/lib/read-size.js.map +1 -0
  167. package/dist/lib/registration-owners.d.ts +79 -0
  168. package/dist/lib/registration-owners.d.ts.map +1 -0
  169. package/dist/lib/registration-owners.js +77 -0
  170. package/dist/lib/registration-owners.js.map +1 -0
  171. package/dist/lib/release-index.d.ts +53 -0
  172. package/dist/lib/release-index.d.ts.map +1 -0
  173. package/dist/lib/release-index.js +162 -0
  174. package/dist/lib/release-index.js.map +1 -0
  175. package/dist/lib/repeating-timers.d.ts +79 -0
  176. package/dist/lib/repeating-timers.d.ts.map +1 -0
  177. package/dist/lib/repeating-timers.js +189 -0
  178. package/dist/lib/repeating-timers.js.map +1 -0
  179. package/dist/lib/source-text.d.ts +34 -0
  180. package/dist/lib/source-text.d.ts.map +1 -0
  181. package/dist/lib/source-text.js +80 -0
  182. package/dist/lib/source-text.js.map +1 -0
  183. package/dist/lib/specifiers.d.ts +20 -0
  184. package/dist/lib/specifiers.d.ts.map +1 -0
  185. package/dist/lib/specifiers.js +130 -0
  186. package/dist/lib/specifiers.js.map +1 -0
  187. package/dist/lib/sql-tables.d.ts +166 -0
  188. package/dist/lib/sql-tables.d.ts.map +1 -0
  189. package/dist/lib/sql-tables.js +464 -0
  190. package/dist/lib/sql-tables.js.map +1 -0
  191. package/dist/lib/switchable-modules.d.ts +54 -0
  192. package/dist/lib/switchable-modules.d.ts.map +1 -0
  193. package/dist/lib/switchable-modules.js +104 -0
  194. package/dist/lib/switchable-modules.js.map +1 -0
  195. package/dist/lib/tailwind-sources.d.ts +136 -0
  196. package/dist/lib/tailwind-sources.d.ts.map +1 -0
  197. package/dist/lib/tailwind-sources.js +307 -0
  198. package/dist/lib/tailwind-sources.js.map +1 -0
  199. package/dist/lib/ui-layer.d.ts +54 -0
  200. package/dist/lib/ui-layer.d.ts.map +1 -0
  201. package/dist/lib/ui-layer.js +57 -0
  202. package/dist/lib/ui-layer.js.map +1 -0
  203. package/dist/lib/workspace-packages.d.ts +186 -0
  204. package/dist/lib/workspace-packages.d.ts.map +1 -0
  205. package/dist/lib/workspace-packages.js +351 -0
  206. package/dist/lib/workspace-packages.js.map +1 -0
  207. package/dist/new-instance/deploy.d.ts +211 -0
  208. package/dist/new-instance/deploy.d.ts.map +1 -0
  209. package/dist/new-instance/deploy.js +1381 -0
  210. package/dist/new-instance/deploy.js.map +1 -0
  211. package/dist/new-instance/docs-toolchain.d.ts +66 -0
  212. package/dist/new-instance/docs-toolchain.d.ts.map +1 -0
  213. package/dist/new-instance/docs-toolchain.js +69 -0
  214. package/dist/new-instance/docs-toolchain.js.map +1 -0
  215. package/dist/new-instance/host.d.ts +124 -0
  216. package/dist/new-instance/host.d.ts.map +1 -0
  217. package/dist/new-instance/host.js +276 -0
  218. package/dist/new-instance/host.js.map +1 -0
  219. package/dist/new-instance/index.d.ts +118 -0
  220. package/dist/new-instance/index.d.ts.map +1 -0
  221. package/dist/new-instance/index.js +567 -0
  222. package/dist/new-instance/index.js.map +1 -0
  223. package/dist/new-instance/modules.d.ts +188 -0
  224. package/dist/new-instance/modules.d.ts.map +1 -0
  225. package/dist/new-instance/modules.js +392 -0
  226. package/dist/new-instance/modules.js.map +1 -0
  227. package/dist/new-instance/template.d.ts +505 -0
  228. package/dist/new-instance/template.d.ts.map +1 -0
  229. package/dist/new-instance/template.js +1886 -0
  230. package/dist/new-instance/template.js.map +1 -0
  231. package/dist/new-module/emit.d.ts +67 -0
  232. package/dist/new-module/emit.d.ts.map +1 -0
  233. package/dist/new-module/emit.js +1393 -0
  234. package/dist/new-module/emit.js.map +1 -0
  235. package/dist/new-module/host.d.ts +67 -0
  236. package/dist/new-module/host.d.ts.map +1 -0
  237. package/dist/new-module/host.js +224 -0
  238. package/dist/new-module/host.js.map +1 -0
  239. package/dist/new-module/index.d.ts +32 -0
  240. package/dist/new-module/index.d.ts.map +1 -0
  241. package/dist/new-module/index.js +193 -0
  242. package/dist/new-module/index.js.map +1 -0
  243. package/dist/new-module/spec.d.ts +188 -0
  244. package/dist/new-module/spec.d.ts.map +1 -0
  245. package/dist/new-module/spec.js +404 -0
  246. package/dist/new-module/spec.js.map +1 -0
  247. package/dist/new-module/text.d.ts +15 -0
  248. package/dist/new-module/text.d.ts.map +1 -0
  249. package/dist/new-module/text.js +22 -0
  250. package/dist/new-module/text.js.map +1 -0
  251. package/dist/new-storefront/dockerfile.d.ts +20 -0
  252. package/dist/new-storefront/dockerfile.d.ts.map +1 -0
  253. package/dist/new-storefront/dockerfile.js +131 -0
  254. package/dist/new-storefront/dockerfile.js.map +1 -0
  255. package/dist/new-storefront/gitignore.d.ts +23 -0
  256. package/dist/new-storefront/gitignore.d.ts.map +1 -0
  257. package/dist/new-storefront/gitignore.js +39 -0
  258. package/dist/new-storefront/gitignore.js.map +1 -0
  259. package/dist/new-storefront/index.d.ts +93 -0
  260. package/dist/new-storefront/index.d.ts.map +1 -0
  261. package/dist/new-storefront/index.js +329 -0
  262. package/dist/new-storefront/index.js.map +1 -0
  263. package/dist/new-storefront/npmrc.d.ts +98 -0
  264. package/dist/new-storefront/npmrc.d.ts.map +1 -0
  265. package/dist/new-storefront/npmrc.js +217 -0
  266. package/dist/new-storefront/npmrc.js.map +1 -0
  267. package/dist/new-storefront/reference.d.ts +189 -0
  268. package/dist/new-storefront/reference.d.ts.map +1 -0
  269. package/dist/new-storefront/reference.js +430 -0
  270. package/dist/new-storefront/reference.js.map +1 -0
  271. package/dist/new-storefront/rewrite.d.ts +171 -0
  272. package/dist/new-storefront/rewrite.d.ts.map +1 -0
  273. package/dist/new-storefront/rewrite.js +701 -0
  274. package/dist/new-storefront/rewrite.js.map +1 -0
  275. package/dist/release-index.json +284 -0
  276. package/dist/rules/action-route-permissions.d.ts +141 -0
  277. package/dist/rules/action-route-permissions.d.ts.map +1 -0
  278. package/dist/rules/action-route-permissions.js +556 -0
  279. package/dist/rules/action-route-permissions.js.map +1 -0
  280. package/dist/rules/bundle-pairing.d.ts +74 -0
  281. package/dist/rules/bundle-pairing.d.ts.map +1 -0
  282. package/dist/rules/bundle-pairing.js +281 -0
  283. package/dist/rules/bundle-pairing.js.map +1 -0
  284. package/dist/rules/channel-resolution.d.ts +17 -0
  285. package/dist/rules/channel-resolution.d.ts.map +1 -0
  286. package/dist/rules/channel-resolution.js +382 -0
  287. package/dist/rules/channel-resolution.js.map +1 -0
  288. package/dist/rules/command-coverage.d.ts +210 -0
  289. package/dist/rules/command-coverage.d.ts.map +1 -0
  290. package/dist/rules/command-coverage.js +714 -0
  291. package/dist/rules/command-coverage.js.map +1 -0
  292. package/dist/rules/container-imports.d.ts +60 -0
  293. package/dist/rules/container-imports.d.ts.map +1 -0
  294. package/dist/rules/container-imports.js +158 -0
  295. package/dist/rules/container-imports.js.map +1 -0
  296. package/dist/rules/default-language-prose.d.ts +212 -0
  297. package/dist/rules/default-language-prose.d.ts.map +1 -0
  298. package/dist/rules/default-language-prose.js +710 -0
  299. package/dist/rules/default-language-prose.js.map +1 -0
  300. package/dist/rules/diacritic-folds.d.ts +238 -0
  301. package/dist/rules/diacritic-folds.d.ts.map +1 -0
  302. package/dist/rules/diacritic-folds.js +681 -0
  303. package/dist/rules/diacritic-folds.js.map +1 -0
  304. package/dist/rules/entity-tenant-classification.d.ts +171 -0
  305. package/dist/rules/entity-tenant-classification.d.ts.map +1 -0
  306. package/dist/rules/entity-tenant-classification.js +323 -0
  307. package/dist/rules/entity-tenant-classification.js.map +1 -0
  308. package/dist/rules/entry-presence.d.ts +142 -0
  309. package/dist/rules/entry-presence.d.ts.map +1 -0
  310. package/dist/rules/entry-presence.js +339 -0
  311. package/dist/rules/entry-presence.js.map +1 -0
  312. package/dist/rules/entry-scope.d.ts +91 -0
  313. package/dist/rules/entry-scope.d.ts.map +1 -0
  314. package/dist/rules/entry-scope.js +404 -0
  315. package/dist/rules/entry-scope.js.map +1 -0
  316. package/dist/rules/env-inputs.d.ts +222 -0
  317. package/dist/rules/env-inputs.d.ts.map +1 -0
  318. package/dist/rules/env-inputs.js +951 -0
  319. package/dist/rules/env-inputs.js.map +1 -0
  320. package/dist/rules/kernel-boundary.d.ts +37 -0
  321. package/dist/rules/kernel-boundary.d.ts.map +1 -0
  322. package/dist/rules/kernel-boundary.js +195 -0
  323. package/dist/rules/kernel-boundary.js.map +1 -0
  324. package/dist/rules/nul-bytes.d.ts +233 -0
  325. package/dist/rules/nul-bytes.d.ts.map +1 -0
  326. package/dist/rules/nul-bytes.js +332 -0
  327. package/dist/rules/nul-bytes.js.map +1 -0
  328. package/dist/rules/platform-surface.d.ts +479 -0
  329. package/dist/rules/platform-surface.d.ts.map +1 -0
  330. package/dist/rules/platform-surface.js +749 -0
  331. package/dist/rules/platform-surface.js.map +1 -0
  332. package/dist/rules/port-catches.d.ts +225 -0
  333. package/dist/rules/port-catches.d.ts.map +1 -0
  334. package/dist/rules/port-catches.js +1374 -0
  335. package/dist/rules/port-catches.js.map +1 -0
  336. package/dist/rules/port-shape.d.ts +213 -0
  337. package/dist/rules/port-shape.d.ts.map +1 -0
  338. package/dist/rules/port-shape.js +670 -0
  339. package/dist/rules/port-shape.js.map +1 -0
  340. package/dist/rules/queue-names.d.ts +108 -0
  341. package/dist/rules/queue-names.d.ts.map +1 -0
  342. package/dist/rules/queue-names.js +395 -0
  343. package/dist/rules/queue-names.js.map +1 -0
  344. package/dist/rules/singleton-identity.d.ts +205 -0
  345. package/dist/rules/singleton-identity.d.ts.map +1 -0
  346. package/dist/rules/singleton-identity.js +830 -0
  347. package/dist/rules/singleton-identity.js.map +1 -0
  348. package/dist/rules/subscribe-seam.d.ts +121 -0
  349. package/dist/rules/subscribe-seam.d.ts.map +1 -0
  350. package/dist/rules/subscribe-seam.js +594 -0
  351. package/dist/rules/subscribe-seam.js.map +1 -0
  352. package/dist/rules/transaction-context.d.ts +40 -0
  353. package/dist/rules/transaction-context.d.ts.map +1 -0
  354. package/dist/rules/transaction-context.js +294 -0
  355. package/dist/rules/transaction-context.js.map +1 -0
  356. package/package.json +59 -0
@@ -0,0 +1,1886 @@
1
+ /**
2
+ * The tree `endora new instance` writes — one plan, three kinds of file, and a
3
+ * rule that refuses a fourth (`contracts/instance-tree.md` §1, §2).
4
+ *
5
+ * ## R1.1 and R1.2 are the whole design
6
+ *
7
+ * Every file here carries its {@link FileKind}, and the kind is what decides
8
+ * whether it may exist at all:
9
+ *
10
+ * * **the client's own** — a value only they can supply, or code they will
11
+ * edit. Written once, never regenerated, never read by us again.
12
+ * * **wiring** — the smallest expression that hands the platform something it
13
+ * cannot derive: a database handle, a root directory, a process's argv.
14
+ * Bounded by R1.4 and counted by {@link wiringLineCount}.
15
+ * * **derived** — rendered from a fact the platform or the module set already
16
+ * holds.
17
+ *
18
+ * **A file that is none of the three may not be written** (R1.2), which is the
19
+ * rule that refuses the fork one file at a time: a composition root is not the
20
+ * client's (it is ours), is not wiring (it is 2 628 lines), and is not derived
21
+ * (nothing generates it). The kind is a field on every entry rather than a
22
+ * comment, so T139's T4 can assert it over the plan the command actually
23
+ * builds.
24
+ *
25
+ * ## Nothing is copied, so nothing is rewritten
26
+ *
27
+ * R1.3 / R5.5 / NFR-002. `endora new storefront` copies a reference tree and
28
+ * rewrites every declaration that names something above it; this command copies
29
+ * nothing, so there is no `rewrite.ts` beside this file and there must never be
30
+ * one — its appearance would be evidence that something was copied that should
31
+ * not have been. Every string below is rendered from the resolved packages, the
32
+ * CLI's own manifest and the operator's own flags.
33
+ *
34
+ * ## No demo artefact, at any tier (D-216)
35
+ *
36
+ * *"A client scaffolding an instance for their own trading receives no demo
37
+ * artefact in a tree they own: no composition, no script, no example and no
38
+ * placeholder. Silence means no."* Nothing here writes one, and the capability
39
+ * is discoverable through the next-steps block rather than reported as an
40
+ * omission — a capability announced as a deficiency is not optional.
41
+ *
42
+ * ## What this build cannot yet write, said here rather than discovered
43
+ *
44
+ * The backend member's wiring names symbols on the platform's declared
45
+ * subpaths — `./composition`, `./db`, `./lifecycle`, `./overlay` and
46
+ * `./packages` today. **How many symbols that is is not written here** (D-100): the
47
+ * reconciliation test derives it from the barrels on every run and prints it,
48
+ * and the count in this sentence was already wrong when the three names below
49
+ * were wrong. `composeApp` is one of them since T118 — the
50
+ * position §2.3 stated (*"`composeApp` is imported, never written (R1.2)"*) is
51
+ * met, and the file below supplies the one argument that composition takes:
52
+ * `deploymentRoot`, the directory holding `apps/`, which no package can derive
53
+ * because in an instance the platform came out of `node_modules`
54
+ * (`contracts/application-root-supplier.md` R1.1). **No contribute callback is
55
+ * supplied and there is nowhere in this tree to write one** — R2.4 — so a
56
+ * client's instance contributes over no name a module defaults.
57
+ *
58
+ * The ORM configuration below is the one place an instance restates its own
59
+ * artefacts, and it has none, so the platform's `*From` factories answer over
60
+ * the packages it installed: `configuredEntitiesFrom`,
61
+ * `discoverConfiguredMigrations` and `mikroOrmConfigFrom` on `./db`, and
62
+ * `resolveManifestEntries` on `./lifecycle` with its three suppliers.
63
+ *
64
+ * **That sentence used to name three other symbols, and nothing held this file
65
+ * to it.** It read *"`configuredMigrations`, `configuredEntities` and
66
+ * `resolvedManifestEntries` are `./db`'s and `./lifecycle`'s **under other
67
+ * names**"* — a doc block describing the repair, beside rendered text that had
68
+ * never taken it, so a scaffolded backend did not compile and the knowledge was
69
+ * present the whole time. The guard is
70
+ * `test/new-instance/template-reconciliation.test.ts`' T1, at **symbol**
71
+ * granularity rather than subpath: `./composition` and `./lifecycle` are both
72
+ * declared subpaths, so a reconciliation of the specifier alone passes over all
73
+ * three errors.
74
+ *
75
+ * §2.3's sixth wiring file, `backend/src/cli.ts`, **is** written since
76
+ * `specs/123-oss-install-experience/` G2, and the omission that stood in its
77
+ * place is deleted rather than reworded. Its reason was *"the demo layer around
78
+ * it is exported under no subpath"*, and what discharged it was not a wider
79
+ * `exports` map: `backend/src/cli/demo-command.ts` moved into
80
+ * `<scope>platform/demo` — where `test/unit/kernel/host-residue-partition.test.ts`
81
+ * had it ledgered as platform-shaped residue all along — and the dispatch around
82
+ * both halves became `<scope>platform/cli`'s `runCli`. So the file this command
83
+ * renders names nothing it had to invent, holds no copy of the 479 lines it
84
+ * calls (D-207), and is five lines.
85
+ *
86
+ * The cost of the omission was measured rather than aesthetic: with no CLI, a
87
+ * scaffolded instance could run no `admin_users create`, so the admin bundle A5
88
+ * and A13 prove is built and styled had nobody to log in as.
89
+ */
90
+ import { isRequiredGiven, scopeToMembers, } from '@endora-commerce/contracts';
91
+ import { writeEnvFile } from '../inputs/env-file.js';
92
+ import { INSTANCE_BUILD_INPUTS } from '../lib/instance-build-inputs.js';
93
+ import { deployFiles, developmentComposeFile, DEV_COMPOSE_PATH, } from './deploy.js';
94
+ import { InstanceInputError } from './host.js';
95
+ /**
96
+ * The members this template writes, and therefore the whole `--without`
97
+ * vocabulary (`specs/118-instance-member-selection/contracts/instance-members.md`
98
+ * R3.5a, ruled by D-215).
99
+ *
100
+ * **This is the one statement of the member set**, and two consumers read it:
101
+ * the refusal below, and `endora install`'s checklist (125 R6.3a), which
102
+ * renders one row per entry and holds no list of its own. A member the template
103
+ * gains is added here, beside the decision that writes it, and reaches both —
104
+ * `test/new-instance-members.test.ts` fails when a planned member has no entry.
105
+ */
106
+ export const MEMBER_VOCABULARY = [
107
+ {
108
+ name: 'backend',
109
+ describes: 'the API and the workers — the part every other one talks to',
110
+ fixed: 'an instance is the tree that composes the platform, and the backend is what composes it',
111
+ },
112
+ {
113
+ name: 'admin',
114
+ describes: 'the operator interface, built as its own artefact',
115
+ fixed: null,
116
+ },
117
+ {
118
+ name: 'docs',
119
+ describes: "a documentation site rendering your modules' own pages",
120
+ fixed: null,
121
+ },
122
+ ];
123
+ /**
124
+ * F10 and F11 (`instance-members.md` §5.6), decided over the names alone — the
125
+ * refusal sentence, or `null` when every name is one this template can decline.
126
+ *
127
+ * Exported because two commands validate the same flag before either writes:
128
+ * `endora new instance`, and `endora install`, which reports it together with
129
+ * every other precondition (125 FR-157) rather than one refusal later.
130
+ */
131
+ export function memberRefusal(without) {
132
+ const names = without.map((name) => name.trim()).filter((name) => name.length > 0);
133
+ const vocabulary = MEMBER_VOCABULARY.map((entry) => entry.name);
134
+ if (names.includes('backend')) {
135
+ return ('`--without backend` is refused: an instance is the tree that composes the platform, and ' +
136
+ 'the backend member is what composes it. If the admin is meant to run on a second host, ' +
137
+ 'keep both members and deploy the built admin there — a machine layout is a fact about ' +
138
+ 'the deployment, not about the repository (`--topology three-host` writes the examples).');
139
+ }
140
+ const unknown = names.filter((name) => !vocabulary.includes(name));
141
+ if (unknown.length === 0)
142
+ return null;
143
+ const storefront = unknown.includes('storefront')
144
+ ? ' The storefront is not a member: it is its own repository, written by ' +
145
+ '`endora new storefront` — and `endora install --no-storefront` is how the one-shot ' +
146
+ 'leaves it out.'
147
+ : '';
148
+ return (`\`--without\` names ${unknown.join(', ')}, which ${unknown.length === 1 ? 'is' : 'are'} not ` +
149
+ `a member of an instance. The members are ${vocabulary.join(', ')}, and ` +
150
+ `\`--without\` names the ones not to write.${storefront}`);
151
+ }
152
+ /**
153
+ * A workspace name a client can actually install.
154
+ *
155
+ * npm's own rule, applied to the basename the operator chose, and **refused**
156
+ * rather than sanitised: a command that quietly renamed the directory the
157
+ * operator named would put a name nobody chose into the file that is the
158
+ * module list (R1.2 there).
159
+ */
160
+ export function assertWorkspaceName(name) {
161
+ if (/^[a-z0-9][a-z0-9._-]*$/.test(name))
162
+ return;
163
+ throw new InstanceInputError('F1', `"${name}" is not a usable npm package name, and it is the name the workspace root takes ` +
164
+ `from the directory you asked for. Use lower-case letters, digits, \`.\`, \`_\` and ` +
165
+ `\`-\`, starting with a letter or a digit — or scaffold into a directory whose basename ` +
166
+ `already is one. Nothing is written.`);
167
+ }
168
+ /**
169
+ * The deployment name, which is `DEPLOYMENT`'s value and nothing else reads it
170
+ * (§2.2).
171
+ *
172
+ * F4. It is a directory name under `apps/`, so the refusal is about what a
173
+ * directory name may be: no separator, no traversal, no leading dot.
174
+ */
175
+ export function assertDeploymentName(deployment) {
176
+ if (/^[a-z0-9][a-z0-9_-]*$/.test(deployment))
177
+ return;
178
+ throw new InstanceInputError('F4', `\`--deployment ${deployment}\` is not a deployment name. It names the directory under ` +
179
+ `\`apps/\` where this instance's overlay modules and its \`divergence.ts\` live, and it ` +
180
+ `is the value of \`DEPLOYMENT\` — so it is lower-case letters, digits, \`_\` and \`-\`, ` +
181
+ `starting with a letter or a digit. Nothing is written.`);
182
+ }
183
+ /**
184
+ * The derived artefacts an instance generates and commits none of (§2.6,
185
+ * `instance-repository.md` R3.2).
186
+ *
187
+ * **There are three and this list carried two** until T138 wrote the admin
188
+ * member. `admin/src/tailwind.generated.css` arrived with T124 on the same day
189
+ * this command landed, R3.2 already said three, and `new-instance.test.ts`'
190
+ * *"both generated artefacts are git-ignored"* asserted the stale count rather
191
+ * than catching it. A committed stylesheet enumeration is the tree and the
192
+ * install disagreeing about which packages were scanned — which is silent, and
193
+ * is the whole failure `admin-stylesheet-composition.md` exists for.
194
+ *
195
+ * **The fourth is the entity index** (`specs/109-backend-test-kit/` T065), and it
196
+ * is the first entry here that belongs to **no member**: its consumer is a test,
197
+ * and a test is not a member of a client's workspace. It is in this list for the
198
+ * one reason the list exists — `.gitignore` has to cover it — and it is
199
+ * `.gitignore`d for §2.6's own predicate: which modules a host installed is a
200
+ * fact about the install, so a committed index is the tree and the install
201
+ * disagreeing about which tables exist.
202
+ */
203
+ export const GENERATED_ARTEFACTS = [
204
+ 'admin/src/modules.generated.ts',
205
+ 'admin/src/tailwind.generated.css',
206
+ 'docs/sidebars.modules.generated.js',
207
+ 'backend/test/entities.generated.ts',
208
+ ];
209
+ /**
210
+ * The generated **trees** — a whole directory the generator owns, rather than a
211
+ * file it writes.
212
+ *
213
+ * They are apart from {@link GENERATED_ARTEFACTS} because the two answer
214
+ * different questions: that list is the files a reconciliation can name and
215
+ * compare, this one is what `.gitignore` has to cover. The documentation half
216
+ * of §2.6 is a *population* rather than a file — one copied page per page a
217
+ * module ships, one reference page per module, and the stamp that lets a run
218
+ * undo the previous one's copies — so a client's `.gitignore` names the
219
+ * directories and git's own "a tracked file is never ignored" keeps a page they
220
+ * write themselves visible with no exception list to maintain.
221
+ */
222
+ export const GENERATED_TREES = [
223
+ 'docs/docs/modules/**',
224
+ 'docs/docs/module-reference/**',
225
+ 'docs/.module-docs-copies.json',
226
+ ];
227
+ /**
228
+ * The named CLI aliases an instance's manifests earn
229
+ * (`specs/123-oss-install-experience/` T2-D).
230
+ *
231
+ * **Derived, never written.** `cli` is the pass-through and covers every
232
+ * command any installed module declares; what a named alias buys on top of it
233
+ * is that an operator reads it in `pnpm run`, and an alias addressing a module
234
+ * this instance did not install would fail with `unknown module` at the one
235
+ * moment a client is least able to tell a missing module from a broken CLI.
236
+ *
237
+ * **There is deliberately no `demo:seed` or `demo:reset` entry**, and
238
+ * `specs/123-oss-install-experience/` T2-D asked for both. D-216 is more
239
+ * specific than the task and is the owner's: *"a client scaffolding an instance
240
+ * for their own trading receives no demo artefact in a tree they own: no
241
+ * composition, **no script**, no example and no placeholder"* — and it names
242
+ * where the capability does belong, which is the next-steps block. `cli` reaches
243
+ * both verbs anyway (`pnpm run cli demo seed`), so nothing is unavailable; what
244
+ * is refused is a line in a client's manifest they did not ask for.
245
+ */
246
+ export function cliAliasesFor(modules) {
247
+ const installed = new Set(modules.map((module) => module.id));
248
+ const aliases = [];
249
+ for (const [alias, moduleId, command] of MODULE_CLI_ALIASES) {
250
+ if (installed.has(moduleId))
251
+ aliases.push([alias, `node dist/cli.js ${moduleId} ${command}`]);
252
+ }
253
+ return aliases.sort(([a], [b]) => a.localeCompare(b));
254
+ }
255
+ /**
256
+ * The module-declared commands an operator is given a name for.
257
+ *
258
+ * It is short on purpose and is not a mirror of every `cliCommands` entry in the
259
+ * estate: this command resolves package **manifests**, not their module
260
+ * manifests, so it cannot enumerate declarations — and a generated list of forty
261
+ * aliases would be forty scripts a client scrolls past to find the one that
262
+ * matters. `admin_users create` is the one an install cannot finish without.
263
+ */
264
+ const MODULE_CLI_ALIASES = [
265
+ ['admin:create', 'admin_users', 'create'],
266
+ ];
267
+ /**
268
+ * The backend member's scripts, every one of them reading the instance's own
269
+ * `.env` (`specs/123-oss-install-experience/` G3).
270
+ *
271
+ * **`--env-file-if-exists=../.env`, and the `..` is the whole of it.** Every
272
+ * root script is `pnpm -C backend run …`, so these run with `backend/` as the
273
+ * working directory while the `.env` a client is told to fill in sits at the
274
+ * root of the tree beside `.env.example` and beside the `.env` entry in
275
+ * `.gitignore`. Without the prefix the file is inert: a client fills it in, runs
276
+ * `pnpm run migrate`, and the process reads nothing at all — which is the second
277
+ * half of the defect the acceptance criterion papers over by supplying the
278
+ * values through `process.env` instead.
279
+ *
280
+ * `-if-exists` rather than `--env-file`, because a `.env` is not obligatory: a
281
+ * container-hosted instance is configured entirely from its process environment,
282
+ * and a flag that refused to start without the file would break exactly the
283
+ * deployment `deploy/compose.prod.yml` describes. A value already in the
284
+ * environment wins over the file, which is Node's own precedence and the one an
285
+ * operator expects.
286
+ *
287
+ * It is one function rather than a spelling repeated ten times, so a script
288
+ * added later cannot be the one that silently does not read the file.
289
+ */
290
+ export function backendScripts(input) {
291
+ const node = 'node --env-file-if-exists=../.env';
292
+ return {
293
+ // `tsc` and `node --watch` rather than `tsx`: no manifest this run can
294
+ // read declares a range for `tsx`, and a range this command chose would
295
+ // be a value nobody reviewed (R2.5a).
296
+ dev: `tsc -p tsconfig.json --watch & ${node} --watch dist/index.js`,
297
+ build: 'tsc -p tsconfig.json',
298
+ start: `${node} dist/index.js`,
299
+ worker: `${node} dist/worker.js`,
300
+ migrate: `${node} dist/migrate.js`,
301
+ 'module:install': `${node} dist/module-commands/install.js`,
302
+ 'module:uninstall': `${node} dist/module-commands/uninstall.js`,
303
+ 'module:enable': `${node} dist/module-commands/enable.js`,
304
+ 'module:disable': `${node} dist/module-commands/disable.js`,
305
+ 'module:status': `${node} dist/module-commands/status.js`,
306
+ // The operator CLI (`specs/123-oss-install-experience/` G2, T2-D).
307
+ // `cli` is the generic pass-through, so a module this instance installed
308
+ // which declares a `cliCommands` entry is addressable with no file in this
309
+ // tree edited; the named entries below are the aliases this repository's own
310
+ // `backend/package.json` carries, **derived** from what is installed rather
311
+ // than written.
312
+ cli: `${node} dist/cli.js`,
313
+ ...Object.fromEntries(cliAliasesFor(input.modules).map(([name, command]) => [
314
+ name,
315
+ command.replace(/^node /, `${node} `),
316
+ ])),
317
+ };
318
+ }
319
+ /** Lines of wiring in a plan — R1.4's bound, measured rather than intended. */
320
+ export function wiringLineCount(plan) {
321
+ return plan.files
322
+ .filter((file) => file.kind === 'wiring')
323
+ .reduce((total, file) => total + file.content.split('\n').length, 0);
324
+ }
325
+ function json(value) {
326
+ return `${JSON.stringify(value, null, 2)}\n`;
327
+ }
328
+ /**
329
+ * Which members this instance writes, as the environment declaration scopes on.
330
+ *
331
+ * `specs/118-instance-member-selection/`: an input read by no written member is
332
+ * **out of the population** — not declared, not asked for, not counted. The
333
+ * storefront is never here: under D-195 it is its own repository with its own
334
+ * declaration, and `endora new storefront` resolves that one.
335
+ */
336
+ function writtenMembers(admin) {
337
+ return admin ? ['backend', 'admin'] : ['backend'];
338
+ }
339
+ /**
340
+ * The environment inputs a client is asked to fill in, in declaration order.
341
+ *
342
+ * Two exclusions, and both are rules rather than names. A **generable secret**
343
+ * is written into the `.env` this command produces and appears in no
344
+ * `.env.example` (R2.5d), so asking for it would be asking for a value the tool
345
+ * has already supplied. And an input **no written member reads** is out of the
346
+ * population (118).
347
+ */
348
+ export function declaredEnvironmentInputs(declared, admin) {
349
+ return scopeToMembers(declared, writtenMembers(admin)).filter((input) => !(input.generable && input.secret));
350
+ }
351
+ /** The generable secrets this run owes a value for, in declaration order. */
352
+ export function generableEnvironmentInputs(declared, admin) {
353
+ return scopeToMembers(declared, writtenMembers(admin)).filter((input) => input.generable && input.secret);
354
+ }
355
+ /**
356
+ * One declaration, as an operator reads it: what it decides, then what it costs
357
+ * to leave unset.
358
+ *
359
+ * Both sentences are the **declaration's own**, carried across rather than
360
+ * rewritten. A rewrite here would be a second statement of a fact the author of
361
+ * the input already made, one tree away from where anybody would notice it had
362
+ * drifted — and an `optional` requirement carries *what is lost* precisely so
363
+ * that this line does not have to say the word "optional" and stop there.
364
+ */
365
+ function declarationComment(input) {
366
+ const lines = [...wrapEnvComment(input.describes.en)];
367
+ switch (input.requirement.kind) {
368
+ case 'required':
369
+ lines.push('# REQUIRED.');
370
+ break;
371
+ case 'requiredWhen':
372
+ lines.push(`# REQUIRED when ${input.requirement.input}=${input.requirement.equals}.`);
373
+ break;
374
+ case 'optional':
375
+ // The cost on a line of its own rather than spliced into a sentence of
376
+ // ours. Declarations disagree about whether `without` opens with a
377
+ // capital, and a sentence built as `without it, <text>` reads wrong for
378
+ // half of them — which would be this file rewriting the author's prose to
379
+ // fit its own grammar, one comma at a time.
380
+ lines.push('# optional — what you lose:', ...wrapEnvComment(input.requirement.without.en));
381
+ break;
382
+ }
383
+ if (input.secret)
384
+ lines.push('# SECRET: never commit this value and never print it.');
385
+ return lines;
386
+ }
387
+ /** One sentence, wrapped to a width a terminal and a diff both show whole. */
388
+ function wrapEnvComment(text) {
389
+ const out = [];
390
+ let line = '';
391
+ for (const word of text.split(' ')) {
392
+ if (line.length > 0 && `${line} ${word}`.length > 84) {
393
+ out.push(`# ${line}`);
394
+ line = word;
395
+ continue;
396
+ }
397
+ line = line.length === 0 ? word : `${line} ${word}`;
398
+ }
399
+ if (line.length > 0)
400
+ out.push(`# ${line}`);
401
+ return out;
402
+ }
403
+ /**
404
+ * `.env.example` — every input this instance actually reads (FR-010).
405
+ *
406
+ * **Two populations, and keeping them apart is the point.** The build inputs are
407
+ * inlined into an artefact by `docker build`; the declared inputs are read by a
408
+ * process on every start. An operator who conflates them rebuilds an image to
409
+ * change a password. They are two headed sections of one file rather than two
410
+ * files because a client fills in one `.env`, and a second example beside the
411
+ * first is a second thing to forget.
412
+ *
413
+ * Neither section is a list. The first is `INSTANCE_BUILD_INPUTS`; the second is
414
+ * the platform's declaration unioned with the manifests of the modules this run
415
+ * installed, scoped to the members it wrote. The defect it closes is the
416
+ * acceptance criterion's own note — *"its `.env.example` declares none of them,
417
+ * so a client who fills in the file the command wrote has nothing to put them
418
+ * in"* — and the note is deleted in the same merge request, because an
419
+ * instrument that reports a gap must not outlive it.
420
+ */
421
+ export function envExample(inputs, declared, admin) {
422
+ const lines = [
423
+ '# Everything this instance needs from its environment. Copy to `.env` and fill it in:',
424
+ '# `.env` is git-ignored, so nothing there is a value anybody but you chose.',
425
+ '#',
426
+ '# An entry with no value on the right of the `=` is one the platform has no honest',
427
+ '# default for — the declaration says so, and the refusal belongs to whatever reads it.',
428
+ '',
429
+ '# ---------------------------------------------------------------------------',
430
+ '# The BUILD inputs. Two of them are inlined into a bundle by `docker build`, so',
431
+ '# changing one of these means rebuilding an image rather than restarting a process.',
432
+ '# ---------------------------------------------------------------------------',
433
+ ];
434
+ for (const input of inputs) {
435
+ lines.push('', `# ${input.meaning}`, `# example: ${input.example}`);
436
+ lines.push(`${input.name}=${input.default ?? ''}`);
437
+ }
438
+ const runtime = declaredEnvironmentInputs(declared, admin);
439
+ if (runtime.length > 0) {
440
+ lines.push('', '# ---------------------------------------------------------------------------', '# The RUNTIME inputs, read on every start. Each sentence below is the platform\'s or', '# the declaring module\'s own: this file is derived from what you installed, so a', '# different module set is a different file and nothing here was written by hand.', '#', '# The secrets this instance signs and encrypts with are NOT here. `endora new', '# instance` generated them into `.env` beside this file and named them on its own', '# output; a generated secret belongs in no example and in no source file.', '# ---------------------------------------------------------------------------');
441
+ for (const input of runtime) {
442
+ lines.push('', ...declarationComment(input), `${input.name}=`);
443
+ }
444
+ }
445
+ return `${lines.join('\n')}\n`;
446
+ }
447
+ /**
448
+ * The `.env` this command writes — the generated secrets, and a blank for
449
+ * everything else the instance reads.
450
+ *
451
+ * R2.5d: a generated secret is written **where the operator can read it**,
452
+ * change it and copy it into a secret store. The placeholders are there so that
453
+ * the file a client edits is the file their instance reads: telling them to
454
+ * `cp .env.example .env` after this command has already written one would have
455
+ * them overwrite the generated secrets with empty strings, and the boot that
456
+ * then failed would name none of this.
457
+ *
458
+ * **Every placeholder is commented out, and that is not cosmetic.** A blank
459
+ * assignment is not the same state as no assignment: Node's `--env-file` reads
460
+ * `MEILISEARCH_URL=` as the empty string, and the platform's `??` fallbacks
461
+ * treat an empty string as a value. Measured on the instance acceptance
462
+ * criterion — a `.env` listing every optional input as a blank turned twenty
463
+ * "unset"s into twenty empty strings and the health route answered **503** over
464
+ * a search engine that was running. So the file lists what there is to fill in
465
+ * and changes nothing until a client removes a `#`, and {@link writeEnvFile}
466
+ * fills a placeholder in place rather than appending a second assignment.
467
+ *
468
+ * A `.env` the operator placed here first is **merged into, never rewritten**:
469
+ * their comments, their ordering and their own keys survive, and a value they
470
+ * already supplied is not generated over.
471
+ */
472
+ export function envFile(input, admin) {
473
+ const required = [];
474
+ const optional = [];
475
+ for (const declaration of declaredEnvironmentInputs(input.declared, admin)) {
476
+ (isRequiredGiven(declaration, {}) ? required : optional).push(declaration.name);
477
+ }
478
+ const seeded = [
479
+ '# This instance\'s own configuration. Git-ignored, and yours.',
480
+ '#',
481
+ '# `endora new instance` wrote it. The values it GENERATED are set, at the bottom; every',
482
+ '# other input this instance reads is listed below, commented out. Remove the `#` and',
483
+ '# fill one in to set it — a commented line and a line reading `NAME=` are NOT the same',
484
+ '# thing to the process that reads this file, and the second is an empty string.',
485
+ '#',
486
+ '# `.env.example` beside this file carries a sentence for each one saying what it',
487
+ '# decides and what leaving it unset costs. Do not copy it over this file.',
488
+ '',
489
+ '# Required — the instance does not start without these.',
490
+ ...required.map((name) => `#${name}=`),
491
+ '',
492
+ '# Optional — each has a cost stated in `.env.example`, and no default worth inventing.',
493
+ ...optional.map((name) => `#${name}=`),
494
+ '',
495
+ ].join('\n');
496
+ // The operator's file wins over the seed entirely: if they placed one, this
497
+ // run adds to it and re-orders nothing.
498
+ const base = input.existingEnv.length > 0 ? input.existingEnv : seeded;
499
+ return writeEnvFile(base, input.generated);
500
+ }
501
+ /**
502
+ * The plan. Nothing here touches the filesystem, so `--dry-run` reports exactly
503
+ * what a real run writes rather than a second derivation of it (R5.3).
504
+ */
505
+ export function planInstance(input) {
506
+ const files = [];
507
+ const omitted = [];
508
+ // §2.4 — decided first, because the workspace member list, the root scripts
509
+ // and the `.gitignore` all depend on whether this instance has an operator
510
+ // interface. Deciding it twice is how two files would come to disagree about
511
+ // a member one of them writes.
512
+ const admin = declinable('admin', adminMember(input), input);
513
+ // §2.4a — same reasoning, same three consequences (the member list, the root
514
+ // scripts, the `.gitignore`), decided in the same place.
515
+ const docs = declinable('docs', docsMember(input), input);
516
+ const dependencies = new Map();
517
+ // Each range is `^` over the version **that package** declares about itself,
518
+ // and never over another package's. A release is not uniform — 68 of this
519
+ // repository's packages moved to `0.8.0` on 2026-09-11 and 15 to `0.7.1` —
520
+ // so a module ranged at the platform's version is a range no registry can
521
+ // satisfy, which is a failure a client meets at their first install and
522
+ // nothing in a checkout can see (the tarball acceptance mode overrides every
523
+ // one of these ranges with a `file:` path).
524
+ dependencies.set(`${input.scope}platform`, `^${input.platformVersion}`);
525
+ for (const module of [...input.modules].sort((a, b) => a.id.localeCompare(b.id))) {
526
+ dependencies.set(module.packageName, `^${module.version}`);
527
+ }
528
+ // The packages the installed modules declare **optional** — and they are
529
+ // declared **here**, at the root, rather than in the admin member that needs
530
+ // them rendered (§2.4).
531
+ //
532
+ // pnpm resolves a package's peers from its **dependent's** context, and a
533
+ // module package's dependent in an instance is this manifest: the module set
534
+ // is the root's (R3.6) and the admin member declares none of it. So an
535
+ // optional peer declared in the admin member satisfies nothing — measured,
536
+ // `mod-invoices`' `@endora-commerce/page-builder-admin` stayed unresolved with
537
+ // the package installed and declared one member over, and Vite bound the
538
+ // import to an `__vite-optional-peer-dep:` stub whose every named export is
539
+ // missing. Declaring the module set a second time in the admin member would
540
+ // satisfy them and is exactly what R3.6 forbids; declaring their peers beside
541
+ // them is the same fact in the one place the set already lives.
542
+ if (admin.written) {
543
+ for (const [name, range] of [...input.adminPeers].sort(([a], [b]) => a.localeCompare(b))) {
544
+ if (BUILD_TOOL_PEERS.has(name))
545
+ continue;
546
+ if (!dependencies.has(name))
547
+ dependencies.set(name, range);
548
+ }
549
+ }
550
+ if (admin.omission !== null)
551
+ omitted.push({ path: 'admin/', reason: admin.omission });
552
+ if (docs.omission !== null)
553
+ omitted.push({ path: 'docs/', reason: docs.omission });
554
+ /**
555
+ * Has this instance anything for `endora generate` to render? (§2.5, §2.6.)
556
+ *
557
+ * One predicate, named once, read by the `generate` script, by `setup`'s term
558
+ * for it and by the `@endora-commerce/cli` devDependency the binary needs —
559
+ * three sites that used to spell `admin.written || docs.written` each, and a
560
+ * fourth artefact family is exactly how three copies of one condition come to
561
+ * disagree.
562
+ *
563
+ * The third term is T065's entity index, which belongs to no member: its
564
+ * population is the module packages this instance installed, so a headless
565
+ * instance with a module has an artefact and a `generate` script where before
566
+ * it had neither. An instance with **no** module installed still gets neither,
567
+ * which is the state `runGenerate` refuses under exit 1.
568
+ */
569
+ const generatesArtefacts = admin.written || docs.written || input.modules.length > 0;
570
+ // --- the workspace root (§2.1) -------------------------------------------
571
+ //
572
+ // The per-layer builds, and the composite that is their conjunction
573
+ // (`specs/122-layer-deployment-independence/contracts/layer-independence.md`
574
+ // §2 R2.1, under D-230). Three layers are deployed to three hosts on three
575
+ // schedules, so each is built by a command of its own — and a CI job on the
576
+ // admin host cannot cite a command it was never told.
577
+ //
578
+ // One list, so there is one predicate. The composite used to spell the same
579
+ // three terms inline; deriving it from the named entries is what keeps its
580
+ // value byte-identical to the named parts rather than merely similar to them.
581
+ const layerBuilds = [
582
+ ['build:backend', 'pnpm -C backend run build'],
583
+ ...(admin.written ? [['build:admin', 'pnpm -C admin run build']] : []),
584
+ ...(docs.written ? [['build:docs', 'pnpm -C docs run build']] : []),
585
+ ];
586
+ // `-C` and never `--filter <name>` — see the block below for what a filter
587
+ // cost the first end-to-end run.
588
+ // The four steps `setup` is the conjunction of (`specs/125-first-mile-install/`
589
+ // FR-109), named as **root scripts** rather than spelled out as commands.
590
+ // That is one level up from `build`'s derivation and buys the same property
591
+ // for a longer sequence: a change to what `migrate` or `module:install` runs
592
+ // reaches the composite with nothing here edited, where an inlined
593
+ // `pnpm -C backend run migrate` would be a second spelling of it.
594
+ //
595
+ // `generate` is conditional on the same predicate the script itself is: an
596
+ // instance with nothing to generate declares none, so the composite must not
597
+ // name one.
598
+ const setupSteps = [
599
+ ...(generatesArtefacts ? ['generate'] : []),
600
+ 'build',
601
+ 'migrate',
602
+ 'module:install --all',
603
+ ];
604
+ const rootScripts = {
605
+ migrate: 'pnpm -C backend run migrate',
606
+ dev: 'pnpm -C backend run dev',
607
+ ...Object.fromEntries(layerBuilds),
608
+ // §2.5's `build` is the whole instance's, and §2.5's `generate` is the
609
+ // three derived artefacts — two of which are the admin member's, so
610
+ // both entries name a member that may not be there. An instance with
611
+ // no operator interface gets neither rather than a script that fails
612
+ // on a directory nobody wrote.
613
+ build: layerBuilds.map(([, command]) => command).join(' && '),
614
+ // One `endora generate` renders every member's artefacts, so the root
615
+ // script is the command itself rather than a member's. An instance with
616
+ // nothing to generate gets no `generate` at all, rather than a script that
617
+ // fails on a directory nobody wrote.
618
+ ...(generatesArtefacts ? { generate: 'endora generate' } : {}),
619
+ // The development stack (`specs/125-first-mile-install/` FR-108). `--wait`
620
+ // is not decoration: it blocks until every health check in the rendered
621
+ // document passes, which is what stops `migrate` racing a Postgres that is
622
+ // still initialising — and it is what lets `setup` run straight after this.
623
+ // `down` keeps the volumes, because a development database is not a scratch
624
+ // file and `docker compose down -v` is not a step anybody should be one
625
+ // typo away from.
626
+ 'dev:services': `docker compose -f ${DEV_COMPOSE_PATH} up -d --wait`,
627
+ 'dev:services:down': `docker compose -f ${DEV_COMPOSE_PATH} down`,
628
+ // FR-109 — four typed lines collapsed into one, and every named step
629
+ // survives beside it for the operator who wants them (§3's User Story 3).
630
+ setup: setupSteps.map((step) => `pnpm run ${step}`).join(' && '),
631
+ start: 'pnpm -C backend run start',
632
+ // FR-110 — the admin bundle, served. Baseline step D1: `admin/package.json`
633
+ // has declared `preview` all along and no root script and no printed step
634
+ // named it, so a client who ran `build:admin` had a bundle and no way to
635
+ // look at it. With the member, like every other admin entry.
636
+ ...(admin.written ? { 'preview:admin': 'pnpm -C admin run preview' } : {}),
637
+ // One development command over the layers above (`specs/136-open-source-
638
+ // publication/` GAP-7, FR-060): `endora dev` supervises `start`,
639
+ // `preview:admin` and the sibling storefront's `dev` by **name**, so it
640
+ // changes none of them and none of the per-layer builds (FR-061, D-230).
641
+ // It is the CLI's, so it is declared exactly when the CLI is on the root's
642
+ // path — the predicate `generate` and the devDependency already share.
643
+ ...(generatesArtefacts ? { 'dev:all': 'endora dev' } : {}),
644
+ 'module:install': 'pnpm -C backend run module:install',
645
+ 'module:uninstall': 'pnpm -C backend run module:uninstall',
646
+ 'module:enable': 'pnpm -C backend run module:enable',
647
+ 'module:disable': 'pnpm -C backend run module:disable',
648
+ 'module:status': 'pnpm -C backend run module:status',
649
+ // The operator CLI (`specs/123-oss-install-experience/` G2, T2-D). `cli` is
650
+ // the generic pass-through, so a module this instance installed which
651
+ // declares a `cliCommands` entry is addressable with no file in this tree
652
+ // edited; the named entries beside it are **derived** from what is
653
+ // installed rather than written.
654
+ cli: 'pnpm -C backend run cli',
655
+ ...Object.fromEntries(cliAliasesFor(input.modules).map(([name]) => [name, `pnpm -C backend run ${name}`])),
656
+ };
657
+ files.push({
658
+ path: 'package.json',
659
+ kind: 'derived',
660
+ member: 'root',
661
+ content: json({
662
+ name: input.name,
663
+ private: true,
664
+ type: 'module',
665
+ ...(input.packageManager === undefined ? {} : { packageManager: input.packageManager }),
666
+ engines: { node: input.enginesNode },
667
+ // `pnpm -C backend`, never `pnpm --filter <name>`
668
+ // (`specs/110-instance-repository/` T141). These read
669
+ // `pnpm --filter backend run …` while the member below is named
670
+ // `<name>-backend`, so pnpm matched no project, printed `No projects
671
+ // matched the filters` and **exited 0** — every root script of a
672
+ // scaffolded instance was a silent no-op, and every step of the next-steps
673
+ // block the command prints was a successful nothing. Measured by the
674
+ // acceptance criterion's first end-to-end run (T140), which found an empty
675
+ // database behind a `migrate` that had exited 0.
676
+ //
677
+ // `-C` is the repair rather than a corrected filter for two reasons. It
678
+ // names the **directory** `pnpm-workspace.yaml` declares, so it cannot
679
+ // drift from a member's name again; and it fails loudly in both directions
680
+ // — a missing directory and a missing script are each exit 1 — where a
681
+ // name filter's whole failure mode is a green nothing. `--fail-if-no-match`
682
+ // would restore the refusal for a filter, and it is pnpm 9.5 and later
683
+ // only; `-C` needs no version this command cannot see.
684
+ scripts: rootScripts,
685
+ dependencies: Object.fromEntries([...dependencies].sort(([a], [b]) => a.localeCompare(b))),
686
+ devDependencies: Object.fromEntries(devDependenciesFor(input, generatesArtefacts)),
687
+ }),
688
+ });
689
+ files.push({
690
+ path: 'pnpm-workspace.yaml',
691
+ kind: 'wiring',
692
+ member: 'root',
693
+ content: [
694
+ '# The members of this workspace. One list, one place.',
695
+ '#',
696
+ '# The module list is NOT here: it is the root `package.json`\'s `dependencies`, and',
697
+ '# the platform discovers those from `node_modules` at runtime. Two spellings of one',
698
+ '# set is the one disagreement nothing in this tree could detect.',
699
+ 'packages:',
700
+ ' - backend',
701
+ ...(admin.written ? [' - admin'] : []),
702
+ ...(docs.written ? [' - docs'] : []),
703
+ '',
704
+ ].join('\n'),
705
+ });
706
+ files.push({
707
+ path: 'tsconfig.json',
708
+ kind: 'client',
709
+ member: 'root',
710
+ content: json({
711
+ compilerOptions: {
712
+ target: 'ES2023',
713
+ lib: ['ES2023'],
714
+ module: 'NodeNext',
715
+ moduleResolution: 'NodeNext',
716
+ strict: true,
717
+ skipLibCheck: true,
718
+ esModuleInterop: true,
719
+ forceConsistentCasingInFileNames: true,
720
+ },
721
+ }),
722
+ });
723
+ if (input.npmrc !== null) {
724
+ files.push({ path: '.npmrc', kind: 'wiring', member: 'root', content: input.npmrc });
725
+ }
726
+ files.push({
727
+ path: '.env.example',
728
+ kind: 'client',
729
+ member: 'root',
730
+ content: envExample(INSTANCE_BUILD_INPUTS, input.declared, admin.written),
731
+ });
732
+ // The instance's own configuration, holding whatever this run generated
733
+ // (R2.5d) and a blank for everything else it reads. It is `client` for the
734
+ // same reason `.env.example` is — it is the operator's from the moment it is
735
+ // written — and it is git-ignored, so it leaves this tree with nobody but
736
+ // them having seen it.
737
+ files.push({
738
+ path: '.env',
739
+ kind: 'client',
740
+ member: 'root',
741
+ content: envFile(input, admin.written),
742
+ });
743
+ files.push({
744
+ path: '.gitignore',
745
+ kind: 'derived',
746
+ member: 'root',
747
+ content: [
748
+ '# The generated artefacts THAT ARE FACTS ABOUT THE INSTALL. `pnpm run generate`',
749
+ '# writes them and this tree commits none of them: a different module set is a',
750
+ '# different bundle, and committing one makes this tree and the install disagree.',
751
+ '#',
752
+ '# `apps/<deployment>/divergence.generated.{md,json}` is deliberately NOT here. The',
753
+ '# same command writes it and it is a fact about THIS repository — what your own',
754
+ '# overlay modules decorate, intercept and consume — so it is committed and the diff',
755
+ '# is the point: it is where an upgrade that changes behaviour you depended on shows up.',
756
+ ...GENERATED_ARTEFACTS,
757
+ '',
758
+ '# The documentation site\'s are a population rather than a file: one copied page per',
759
+ '# page a module ships, one reference page per module, and the stamp that lets a run',
760
+ '# undo the previous one\'s copies. A **tracked** file is never ignored whatever the',
761
+ '# pattern says, so a page you write yourself stays visible and no exception list is',
762
+ '# written down here.',
763
+ ...GENERATED_TREES,
764
+ '',
765
+ 'node_modules',
766
+ 'dist',
767
+ '.next/',
768
+ '*.tsbuildinfo',
769
+ '',
770
+ '# This instance\'s own configuration, with the secrets this command generated.',
771
+ '# `.env.example` is the file to commit.',
772
+ '.env',
773
+ '.env*.local',
774
+ '',
775
+ ].join('\n'),
776
+ });
777
+ files.push({
778
+ path: 'README.md',
779
+ kind: 'client',
780
+ member: 'root',
781
+ content: readme(input, dependencies.size, rootScripts, {
782
+ admin: admin.written,
783
+ docs: docs.written,
784
+ }),
785
+ });
786
+ // --- the deployment (§2.2) -----------------------------------------------
787
+ files.push({
788
+ path: `apps/${input.deployment}/divergence.ts`,
789
+ kind: 'client',
790
+ member: 'deployment',
791
+ content: divergenceDeclaration(input.deployment),
792
+ });
793
+ files.push({
794
+ path: `apps/${input.deployment}/modules/.gitkeep`,
795
+ kind: 'client',
796
+ member: 'deployment',
797
+ content: '',
798
+ });
799
+ // --- the backend member (§2.3) -------------------------------------------
800
+ files.push({
801
+ path: 'backend/package.json',
802
+ kind: 'derived',
803
+ member: 'backend',
804
+ content: json({
805
+ name: `${input.name}-backend`,
806
+ private: true,
807
+ type: 'module',
808
+ // No dependency of its own: the module set is the root's (§2.3, R3.6).
809
+ scripts: backendScripts(input),
810
+ }),
811
+ });
812
+ files.push({
813
+ path: 'backend/tsconfig.json',
814
+ kind: 'client',
815
+ member: 'backend',
816
+ content: json({
817
+ compilerOptions: {
818
+ target: 'ES2023',
819
+ lib: ['ES2023'],
820
+ module: 'NodeNext',
821
+ moduleResolution: 'NodeNext',
822
+ outDir: 'dist',
823
+ rootDir: 'src',
824
+ strict: true,
825
+ skipLibCheck: true,
826
+ experimentalDecorators: true,
827
+ emitDecoratorMetadata: true,
828
+ esModuleInterop: true,
829
+ },
830
+ include: ['src'],
831
+ }),
832
+ });
833
+ for (const file of backendWiring(input))
834
+ files.push(file);
835
+ // --- the admin member (§2.4) ---------------------------------------------
836
+ for (const file of admin.files)
837
+ files.push(file);
838
+ // --- the documentation member (§2.4a) ------------------------------------
839
+ for (const file of docs.files)
840
+ files.push(file);
841
+ // --- the deployment examples (§2.7; layer-independence.md §3) ------------
842
+ //
843
+ // Last, because they are derived from the member decisions above and from
844
+ // nothing else but the topology. They belong to no member's directory: an
845
+ // example that deploys the admin is not the admin project's file, and a
846
+ // client editing one is editing the root of their own repository.
847
+ const deployInput = {
848
+ topology: input.topology,
849
+ admin: admin.written,
850
+ docs: docs.written,
851
+ npmrc: input.npmrc !== null,
852
+ enginesNode: input.enginesNode,
853
+ packageManager: input.packageManager,
854
+ // The same declaration the root `.env.example` is derived from. One
855
+ // derivation of what this instance needs, two readers of it.
856
+ declared: input.declared,
857
+ };
858
+ for (const file of deployFiles(deployInput))
859
+ files.push(file);
860
+ // --- the development environment (`specs/125-first-mile-install/` §4.1) ---
861
+ //
862
+ // At the **root** and not under `deploy/`, because everything in there is
863
+ // addressed to a person deploying to a host they own and this file is
864
+ // addressed to a person on a laptop (spec §5.3.1). Written unconditionally
865
+ // (FR-107): it is inert, it is derived from the same catalogue the examples
866
+ // are, and every audience the feature has wants it.
867
+ files.push(developmentComposeFile(deployInput));
868
+ return {
869
+ files,
870
+ omitted,
871
+ members: [
872
+ 'root',
873
+ 'deployment',
874
+ 'backend',
875
+ ...(admin.written ? ['admin'] : []),
876
+ ...(docs.written ? ['docs'] : []),
877
+ ],
878
+ registry: input.registry,
879
+ dependencies,
880
+ };
881
+ }
882
+ /**
883
+ * The deployment's own declaration, written **out in full with its doc block**
884
+ * (§2.2).
885
+ *
886
+ * `backend/src/apps/example/divergence.ts`'s own stated reason, which is why
887
+ * this is not an empty literal: *"the mechanism is easier to find than to
888
+ * remember … a field an author never sees is a field they never learn they
889
+ * have."*
890
+ */
891
+ function divergenceDeclaration(deployment) {
892
+ return `/**
893
+ * What this deployment does differently from core.
894
+ *
895
+ * Three fields, and each answers a question a walk of this tree cannot:
896
+ *
897
+ * * \`omittedModules\` — a module this deployment deliberately does not ship.
898
+ * The declaration is two-way: an entry for a module you do ship fails as
899
+ * loudly as an omission you did not declare.
900
+ * * \`decorationOrder\` — where two of your overlay modules decorate one
901
+ * registration, the order they wrap it in. \`beta(acme(core))\` and
902
+ * \`acme(beta(core))\` are different implementations, so the ambiguity is
903
+ * refused at boot rather than resolved by a directory read order.
904
+ * * \`reasons\` — one sentence per derived divergence, keyed as the generated
905
+ * report keys it. A divergence with no sentence is a finding; a sentence
906
+ * describing a divergence that is gone is the same finding walked the other
907
+ * way.
908
+ *
909
+ * It is written out empty on purpose. A field an author never sees is a field
910
+ * they never learn they have.
911
+ */
912
+ export const divergence = {
913
+ omittedModules: [],
914
+ decorationOrder: {},
915
+ reasons: {},
916
+ } as const;
917
+
918
+ /** The deployment this declaration belongs to. \`DEPLOYMENT=${deployment}\`. */
919
+ export const deployment = '${deployment}';
920
+ `;
921
+ }
922
+ /**
923
+ * The packages the instance builds with, each range read off a manifest this
924
+ * run resolved (§2.1, R2.5a).
925
+ *
926
+ * `instance-tree.md` §2.1 names the set — *"the platform's four peers plus
927
+ * `@mikro-orm/migrations`, `tsx`, `typescript`"* — and says nothing about where
928
+ * the **ranges** come from, which is the question R2.5a answers: a value with
929
+ * no source is one the command would have to invent. So each is derived:
930
+ *
931
+ * * the four peers, from `@endora-commerce/platform`'s own
932
+ * `peerDependencies`. A platform that widens `zod` to `^5` widens this in
933
+ * the same run, with nothing here to edit.
934
+ * * `@mikro-orm/migrations`, from the range the platform declares for
935
+ * `@mikro-orm/core`. The MikroORM packages version in lockstep and a
936
+ * migrator from another major does not load the driver from this one.
937
+ * * `ioredis`, from the platform's own `dependencies`. It is not in §2.1's
938
+ * list and it is not optional: the five operator commands construct the
939
+ * `OperatorResources` the platform asks them for, and one of its three
940
+ * members is a Redis connection.
941
+ * * `typescript`, from the CLI's own manifest — R2.3's first named source.
942
+ *
943
+ * **`tsx` is deliberately absent.** No manifest this run can read declares a
944
+ * range for it, so writing one would be the invention R2.5a forbids; the
945
+ * member's `dev` script uses `tsc` and `node --watch`, which need nothing that
946
+ * is not already here.
947
+ */
948
+ export function devDependenciesFor(input,
949
+ /**
950
+ * Does this instance have an artefact to generate at all?
951
+ *
952
+ * The root's `generate` script is `endora generate` — one run renders every
953
+ * member's artefacts — so the binary has to be on the **root's** path. An
954
+ * instance with no artefact family at all has no `generate` script, and
955
+ * declaring the tool that runs it would be a dependency with nothing to do.
956
+ *
957
+ * The caller's predicate is `planInstance`'s `generatesArtefacts`, which since
958
+ * T065 counts the entity index: a headless instance with a module installed
959
+ * does have something to render, so it does get the tool.
960
+ */
961
+ generates = false) {
962
+ const wanted = [
963
+ ['@mikro-orm/core', input.declaredRanges.get('@mikro-orm/core') ?? ''],
964
+ ['@mikro-orm/postgresql', input.declaredRanges.get('@mikro-orm/postgresql') ?? ''],
965
+ ['@mikro-orm/migrations', input.declaredRanges.get('@mikro-orm/core') ?? ''],
966
+ ['fastify', input.declaredRanges.get('fastify') ?? ''],
967
+ ['zod', input.declaredRanges.get('zod') ?? ''],
968
+ ['ioredis', input.declaredRanges.get('ioredis') ?? ''],
969
+ ['typescript', input.declaredRanges.get('typescript') ?? ''],
970
+ ...(generates
971
+ ? [[`${input.scope}cli`, `^${input.cliVersion}`]]
972
+ : []),
973
+ ];
974
+ // A package whose range no resolved manifest declares is **left out**, not
975
+ // guessed at. The client adds it and reviews the range they chose, which is
976
+ // one line of work; a range this command invented would be in their manifest
977
+ // forever with nobody's judgement behind it.
978
+ return wanted
979
+ .filter(([, range]) => range.length > 0)
980
+ .sort(([a], [b]) => a.localeCompare(b));
981
+ }
982
+ /**
983
+ * The backend member's wiring — R1.4's whole population.
984
+ *
985
+ * Each file is the smallest expression that hands the platform something it
986
+ * cannot derive: a database handle, a process's argv, a port, **a root
987
+ * directory**. Every symbol named below is on a subpath the platform's
988
+ * `exports` map declares. A symbol that exists nowhere is not written at all:
989
+ * `backend/src/cli.ts` is §2.3's sixth wiring file and is reported as an
990
+ * omission rather than rendered against a name somebody would have had to
991
+ * invent for it.
992
+ */
993
+ function backendWiring(input) {
994
+ const scope = input.scope;
995
+ const files = [];
996
+ files.push({
997
+ path: 'backend/src/index.ts',
998
+ kind: 'wiring',
999
+ member: 'backend',
1000
+ content: `// The API process. It reads the environment, composes, and listens.
1001
+ import { fileURLToPath } from 'node:url';
1002
+
1003
+ import { buildServer, composeApp } from '${scope}platform/composition';
1004
+
1005
+ const port = Number(process.env['PORT'] ?? 3001);
1006
+ const sessionCookieSecret = process.env['SESSION_COOKIE_SECRET'] ?? '';
1007
+ if (sessionCookieSecret === '') {
1008
+ console.error('SESSION_COOKIE_SECRET must be set.');
1009
+ process.exit(1);
1010
+ }
1011
+
1012
+ // The directory that holds \`apps/\` — this workspace's root, one level up from
1013
+ // the backend member. It is the one thing the platform cannot derive for
1014
+ // itself, and it is deliberately a required argument rather than a default that
1015
+ // would silently name a directory holding no \`apps/\` at all.
1016
+ const deploymentRoot = fileURLToPath(new URL('../..', import.meta.url));
1017
+
1018
+ const composition = await composeApp({ deploymentRoot });
1019
+ const app = await buildServer({
1020
+ sessionCookieSecret,
1021
+ openApi: { title: '${input.name}', version: '0.0.0', serverUrl: \`http://localhost:\${port}\` },
1022
+ modules: composition.modules,
1023
+ errorEnvelope: composition.errorEnvelope,
1024
+ apiInterceptors: composition.apiInterceptors,
1025
+ });
1026
+
1027
+ const shutdown = async (signal: string): Promise<void> => {
1028
+ app.log.info({ signal }, 'shutting down');
1029
+ await app.close();
1030
+ await composition.dispose();
1031
+ process.exit(0);
1032
+ };
1033
+ process.once('SIGINT', () => void shutdown('SIGINT'));
1034
+ process.once('SIGTERM', () => void shutdown('SIGTERM'));
1035
+
1036
+ await app.listen({ port, host: '0.0.0.0' });
1037
+ `,
1038
+ });
1039
+ files.push({
1040
+ path: 'backend/src/worker.ts',
1041
+ kind: 'wiring',
1042
+ member: 'backend',
1043
+ content: `// The queue-consumer process (Principle X). Same composition, no listen.
1044
+ import { fileURLToPath } from 'node:url';
1045
+
1046
+ import { buildServer, composeApp } from '${scope}platform/composition';
1047
+
1048
+ process.env['BACKEND_ROLE'] = 'worker';
1049
+ const sessionCookieSecret = process.env['SESSION_COOKIE_SECRET'] ?? '';
1050
+ if (sessionCookieSecret === '') {
1051
+ console.error('SESSION_COOKIE_SECRET must be set.');
1052
+ process.exit(1);
1053
+ }
1054
+
1055
+ const deploymentRoot = fileURLToPath(new URL('../..', import.meta.url));
1056
+
1057
+ const composition = await composeApp({ deploymentRoot });
1058
+ const app = await buildServer({
1059
+ sessionCookieSecret,
1060
+ openApi: { title: '${input.name} worker', version: '0.0.0', serverUrl: 'http://localhost' },
1061
+ modules: composition.modules,
1062
+ errorEnvelope: composition.errorEnvelope,
1063
+ });
1064
+
1065
+ const shutdown = async (signal: string): Promise<void> => {
1066
+ app.log.info({ signal }, 'worker shutting down');
1067
+ await app.close();
1068
+ await composition.dispose();
1069
+ process.exit(0);
1070
+ };
1071
+ process.once('SIGINT', () => void shutdown('SIGINT'));
1072
+ process.once('SIGTERM', () => void shutdown('SIGTERM'));
1073
+ app.log.info('worker started');
1074
+ `,
1075
+ });
1076
+ files.push({
1077
+ path: 'backend/src/mikro-orm.config.ts',
1078
+ kind: 'wiring',
1079
+ member: 'backend',
1080
+ content: `// The ORM configuration. It exists because a bin has none and cannot get one.
1081
+ //
1082
+ // The configuration itself is the platform's: the naming strategy Principle VI
1083
+ // is enforced by, the Migrator extension \`getMigrator()\` needs, and the
1084
+ // migration options an \`allOrNothing\` run takes. A hand-written
1085
+ // \`defineConfig\` here would compile and then create tables the installed
1086
+ // migrations do not name.
1087
+ import {
1088
+ configuredEntitiesFrom,
1089
+ discoverConfiguredMigrations,
1090
+ mikroOrmConfigFrom,
1091
+ } from '${scope}platform/db';
1092
+
1093
+ export default async function config() {
1094
+ const url = process.env['DATABASE_URL'];
1095
+ if (url === undefined || url === '') throw new Error('DATABASE_URL must be set.');
1096
+ // The committed half is empty, which is what an instance is: it ships no
1097
+ // generated manifest index and no committed registry, so its entities and
1098
+ // its migrations are the packages it installed and nothing else.
1099
+ const [entities, migrations] = await Promise.all([
1100
+ configuredEntitiesFrom({ coreEntities: [] }),
1101
+ discoverConfiguredMigrations({ coreEntries: [], manifests: [] }),
1102
+ ]);
1103
+ return mikroOrmConfigFrom({ entities, migrations });
1104
+ }
1105
+ `,
1106
+ });
1107
+ files.push({
1108
+ path: 'backend/src/migrate.ts',
1109
+ kind: 'wiring',
1110
+ member: 'backend',
1111
+ content: `// The schema, in the order the installed manifests compute. MikroORM's own
1112
+ // migrator over the configuration beside this file — no second ordering here.
1113
+ import { MikroORM } from '@mikro-orm/postgresql';
1114
+ import config from './mikro-orm.config.js';
1115
+
1116
+ const orm = await MikroORM.init(await config());
1117
+ try {
1118
+ await orm.getMigrator().up();
1119
+ } finally {
1120
+ await orm.close(true);
1121
+ }
1122
+ `,
1123
+ });
1124
+ // §2.3's sixth wiring file — the operator CLI
1125
+ // (`specs/123-oss-install-experience/` G2, T2-C). Five lines, because the
1126
+ // dispatch is `<scope>platform/cli`'s: argv, the demo verbs, the system scope
1127
+ // over the composed container and the exit code. What this file supplies is
1128
+ // the one thing no package can derive — the directory holding `apps/` — and
1129
+ // that is R1.4's definition of wiring.
1130
+ files.push({
1131
+ path: 'backend/src/cli.ts',
1132
+ kind: 'wiring',
1133
+ member: 'backend',
1134
+ content: `// Your modules' operator commands. \`pnpm run cli --list\` shows every one.
1135
+ import { fileURLToPath } from 'node:url';
1136
+ import { runCli } from '${scope}platform/cli';
1137
+
1138
+ await runCli({ deploymentRoot: fileURLToPath(new URL('../..', import.meta.url)) });
1139
+ `,
1140
+ });
1141
+ // The five `module:*` entry points' shared half, and it is fourteen lines
1142
+ // rather than ninety since `fix/instance-wiring-operator-runtime`. The
1143
+ // manifest resolution over three suppliers, the memoised `MikroORM` + `Redis`
1144
+ // open, the system scope and the exit code are
1145
+ // `<scope>platform/lifecycle`'s `runInstanceOperatorCommand`; what this file
1146
+ // supplies is the two values that genuinely name a path in the tree that
1147
+ // installs the platform — the directory holding `apps/`, and this instance's
1148
+ // own ORM configuration — and that is R1.4's definition of wiring. The
1149
+ // ninety-line version was more than a third of the whole 250-line budget and
1150
+ // is what A14 refused in `registry` mode.
1151
+ files.push({
1152
+ path: 'backend/src/module-commands/runtime.ts',
1153
+ kind: 'wiring',
1154
+ member: 'backend',
1155
+ content: `// The one OperatorRuntime the five commands beside this file share.
1156
+ import { fileURLToPath } from 'node:url';
1157
+
1158
+ import { runInstanceOperatorCommand } from '${scope}platform/lifecycle';
1159
+ import type { OperatorRuntime } from '${scope}platform/lifecycle';
1160
+ import config from '../mikro-orm.config.js';
1161
+
1162
+ // The directory that holds \`apps/\` — the same value \`index.ts\` hands
1163
+ // \`composeApp\`, two levels up from the compiled command rather than one.
1164
+ const deploymentRoot = fileURLToPath(new URL('../../..', import.meta.url));
1165
+
1166
+ /** Build the runtime, run one command inside a system scope, close, exit. */
1167
+ export function runOperatorCommand(
1168
+ run: (argv: readonly string[], rt: OperatorRuntime) => Promise<number>,
1169
+ ): Promise<never> {
1170
+ return runInstanceOperatorCommand({ deploymentRoot, ormConfig: config, run });
1171
+ }
1172
+ `,
1173
+ });
1174
+ for (const command of ['install', 'uninstall', 'enable', 'disable', 'status']) {
1175
+ const runner = `run${command[0].toUpperCase()}${command.slice(1)}Command`;
1176
+ files.push({
1177
+ path: `backend/src/module-commands/${command}.ts`,
1178
+ kind: 'wiring',
1179
+ member: 'backend',
1180
+ content: `import { ${runner} } from '${scope}platform/lifecycle';
1181
+ import { runOperatorCommand } from './runtime.js';
1182
+
1183
+ await runOperatorCommand(${runner});
1184
+ `,
1185
+ });
1186
+ }
1187
+ return files;
1188
+ }
1189
+ /**
1190
+ * The README's command block — every root script, in the order a client meets
1191
+ * them, each with what it is for (feature 122 T003).
1192
+ *
1193
+ * The **set** is the manifest's, not this list's: a name here that the run did
1194
+ * not declare contributes no line, and a script the run declared with no entry
1195
+ * here is a hole the reconciliation in `test/new-instance.test.ts` reports.
1196
+ * That is the only relationship a second enumeration may have with the first
1197
+ * (D-100) — this one carries the *order* and the *sentence*, and nothing else.
1198
+ *
1199
+ * The three per-layer builds carry one sentence between them and it is the
1200
+ * whole of D-230: the backend, the admin and the storefront are deployed to
1201
+ * hosts of their own, so each is built by a command of its own.
1202
+ */
1203
+ const README_COMMANDS = [
1204
+ // `specs/125-first-mile-install/` FR-108…FR-110, and the order is the order a
1205
+ // client meets them: the services first, because everything under them needs
1206
+ // one; the composite second, because it is what four of the lines below add
1207
+ // up to; and `preview:admin` beside `start`, because a built bundle nobody
1208
+ // can look at was baseline step D1.
1209
+ ['dev:services', '', 'PostgreSQL, Redis, Meilisearch and a mail catcher, from\n`compose.dev.yml` beside this file. It waits for each one to\nreport healthy'],
1210
+ ['dev:services:down', '', 'stops them, keeping their data'],
1211
+ ['setup', '', 'generate, build, migrate and install every module — the four\nsteps below, in one line. Each still exists on its own'],
1212
+ ['generate', '', 'the files the admin and the docs site are built from,\nand your deployment\'s divergence report'],
1213
+ ['build', '', 'the entry points, compiled, and every member built'],
1214
+ ['build:backend', '', 'one layer at a time. Each of the three is deployed on its own\nhost, on its own schedule, so each is built on its own too'],
1215
+ ['build:admin', '', ''],
1216
+ ['build:docs', '', ''],
1217
+ ['migrate', '', 'the schema, in the order the manifests compute'],
1218
+ ['module:install', ' --all', 'every module you declared, in dependency order'],
1219
+ ['start', '', 'the API'],
1220
+ ['preview:admin', '', 'the admin bundle you just built, served on its own port'],
1221
+ ['dev:all', '', 'the API, the admin preview and the storefront beside this\ndirectory, in one terminal. Ctrl-C stops all of them'],
1222
+ ['dev', '', 'the API, rebuilt and restarted as you edit your overlay'],
1223
+ ['module:status', '', 'what is installed, and what the operator has switched on'],
1224
+ ['module:enable', ' <id>', 'the operator\'s switch. A module that is off behaves as\nthough it were never installed'],
1225
+ ['module:disable', ' <id>', ''],
1226
+ ['module:uninstall', ' <id>', 'the reverse of `module:install`'],
1227
+ // `specs/123-oss-install-experience/` G2. `admin:create` is conditional on
1228
+ // `admin_users` being installed, which the filter below already handles: this
1229
+ // table is annotations, and which of them survive is the manifest's answer.
1230
+ ['admin:create', ' -- --email=…', 'the administrator you sign in as. Nothing else creates one'],
1231
+ ['cli', ' --list', 'every operator command your installed modules declare.\nRun one as `pnpm run cli <module> <command>`'],
1232
+ ];
1233
+ /** The block itself, aligned, over the scripts this run declared. */
1234
+ function commandBlock(scripts) {
1235
+ const rows = README_COMMANDS.filter(([script]) => scripts[script] !== undefined).map(([script, argument, note]) => [`pnpm run ${script}${argument}`, note]);
1236
+ const width = Math.max(...rows.map(([invocation]) => invocation.length)) + 3;
1237
+ return rows
1238
+ .map(([invocation, note]) => {
1239
+ if (note.length === 0)
1240
+ return invocation;
1241
+ const [first, ...rest] = note.split('\n');
1242
+ return [
1243
+ `${invocation.padEnd(width)}# ${first}`,
1244
+ ...rest.map((line) => `${''.padEnd(width)}# ${line}`),
1245
+ ].join('\n');
1246
+ })
1247
+ .join('\n');
1248
+ }
1249
+ /** The client's own README: what this tree is, and what maintains it. */
1250
+ /**
1251
+ * The tree's own README — the client's, written once and never read by us
1252
+ * again.
1253
+ *
1254
+ * The "what is here" table is built from the members this run actually wrote,
1255
+ * not from a list: which members exist depends on what resolved (§2.4, §2.4a),
1256
+ * and a README naming a directory the command omitted is the first thing a
1257
+ * client would find wrong with their new tree.
1258
+ *
1259
+ * **The command block is the manifest's `scripts`, ordered and annotated** —
1260
+ * feature 122 T003. It used to be six lines of prose naming five of the twelve
1261
+ * scripts the manifest declares, so a client reading it could not learn that
1262
+ * `dev`, `module:status` or the per-layer builds exist. Both sides are now one
1263
+ * run's, reconciled in `test/new-instance.test.ts`: a script with no annotation
1264
+ * below is red rather than a line a client never sees.
1265
+ */
1266
+ function readme(input, dependencyCount, scripts, members) {
1267
+ return `# ${input.name}
1268
+
1269
+ An Endora Commerce instance. It **composes** the platform; it is not a fork of it and holds a
1270
+ copy of no part of it.
1271
+
1272
+ ## What is here
1273
+
1274
+ | | |
1275
+ | --- | --- |
1276
+ | \`package.json\` | the module list. There is no other: the \`dependencies\` are what this instance composes, and the platform discovers them from \`node_modules\` at runtime |
1277
+ | \`apps/${input.deployment}/\` | your deployment — your overlay modules, \`divergence.ts\` (what you declare) and \`divergence.generated.md\` (what is derived from it; commit it and read the diff) |
1278
+ | \`backend/\` | the entry points: a process that listens, a process that consumes queues, an ORM configuration and the operator commands |
1279
+ ${members.admin ? '| `admin/` | the operator interface — the admin shell, mounted over the screens your modules ship |\n' : ''}${members.docs ? '| `docs/` | the documentation site — a page per module, written by the module that ships it |\n' : ''}
1280
+ ${String(dependencyCount)} packages are declared today. Every one of them is a dependency, so a
1281
+ fix in any of them reaches you through \`pnpm update\` with no file in this tree edited.
1282
+
1283
+ ## The commands this tree declares
1284
+
1285
+ \`\`\`
1286
+ pnpm install
1287
+ ${commandBlock(scripts)}
1288
+ \`\`\`
1289
+
1290
+ Your modules arrive as installed packages, and a package is installed by \`module:install\` and
1291
+ by **no boot**: that command applies its migrations, reconciles its settings and runs its
1292
+ install hook. Until it has run, \`start\` refuses and names the modules the platform requires.
1293
+ Adding a module later is \`pnpm add\`, then \`pnpm run migrate\` and \`module:install\` again —
1294
+ both are idempotent, so running them over a set that is already installed changes nothing.
1295
+
1296
+ ## Changing what the platform does
1297
+
1298
+ Four seams before a fork, in order of cost: the EventBus, an API interceptor, a strategy port,
1299
+ and \`ctx.di.decorate\` from your own overlay module in \`apps/${input.deployment}/modules/\`.
1300
+ Decoration is the only way an instance changes a platform behaviour — there is no file to
1301
+ shadow, because there is no file.
1302
+
1303
+ An overlay module is TypeScript that **nothing in this tree compiles** — Node loads it and
1304
+ strips the types as it goes. So it is written in the subset stripping accepts: an \`enum\`, a
1305
+ \`namespace\` or a constructor parameter property raises
1306
+ \`ERR_UNSUPPORTED_TYPESCRIPT_SYNTAX\` at boot, and no type-check you can run here reports it,
1307
+ because all three type-check cleanly. A union of string literals is the \`enum\` you want.
1308
+
1309
+ Whatever you reach for, \`pnpm run generate\` records it in
1310
+ \`apps/${input.deployment}/divergence.generated.md\`: every seam you used, which module owns
1311
+ the thing you changed, what that seam costs on the escalation ladder, and the sentence you
1312
+ wrote about it in \`divergence.ts\`. A divergence with no sentence is reported; so is a
1313
+ sentence describing a divergence that is gone.
1314
+
1315
+ A module you will never publish belongs in that directory. A module you intend to publish or
1316
+ install into a second instance is a package: \`pnpm pack\`, then install the tarball. A
1317
+ \`pnpm link\` is deliberately invisible to the platform's discovery, so it is neither.
1318
+
1319
+ ## Next
1320
+
1321
+ \`endora new storefront <dir>\` writes the customer-facing storefront. It is a separate
1322
+ repository on purpose: it shares two \`.env\` values with this tree and nothing else.
1323
+ `;
1324
+ }
1325
+ // ── the admin member (§2.4) ─────────────────────────────────────────────────
1326
+ //
1327
+ // `contracts/instance-tree.md` §2.4 in full: *"`admin/index.html`,
1328
+ // `admin/src/main.tsx`, `admin/vite.config.ts`, `admin/tailwind.config.ts`, the
1329
+ // brand assets, the theme **overrides**, and the **generated** contribution
1330
+ // registry and stylesheet enumeration (§2.6). Nothing else."*
1331
+ //
1332
+ // Two of those nouns no longer describe the tree and are written down here
1333
+ // rather than discovered by the next reader.
1334
+ //
1335
+ // * **`tailwind.config.ts` does not exist**, in this repository or anywhere
1336
+ // else: Tailwind v4 has no configuration file, and its `@theme` and
1337
+ // `@source` are CSS. What the member holds in its place is `src/index.css`
1338
+ // — the two imports and the override slot, which is §2.4's *"theme
1339
+ // overrides"* and `admin-stylesheet-composition.md` R3.1's.
1340
+ // * **`package.json` and `tsconfig.json` are not in §2.4's list** and a
1341
+ // workspace member is neither without them. §2.3 lists both for the backend;
1342
+ // the omission there is the list's rather than the design's.
1343
+ //
1344
+ // **The brand assets are not written**, and that is a decision rather than a
1345
+ // gap: a logo, a favicon and a PWA icon set are exactly the values R2.5a says a
1346
+ // command may not invent, and an instance that shipped ours would be wearing
1347
+ // our name. The reference deployment's `public/` also carries the admin service
1348
+ // worker, so `registerAdminServiceWorker` — which the shell exports and this
1349
+ // entry point does **not** call — would register an asset the tree does not
1350
+ // serve. A client drops their own files in `admin/public/` and links them from
1351
+ // `index.html`, both of which are theirs.
1352
+ /**
1353
+ * What the operator is told a declined member means — one sentence per
1354
+ * declinable member, keyed by the vocabulary's own names.
1355
+ */
1356
+ const DECLINED_CONSEQUENCE = {
1357
+ admin: 'This instance is a headless API by choice. Its backend still serves /api/v1/admin/*, so ' +
1358
+ 'an operator interface built elsewhere reaches it.',
1359
+ docs: 'The pages your module packages ship are still in their tarballs; this instance builds no ' +
1360
+ 'site to read them in.',
1361
+ };
1362
+ /**
1363
+ * A member decision, with the operator's `--without` applied on top of it
1364
+ * (118 R3.5c, R4.1).
1365
+ *
1366
+ * **Every reason that holds is printed, never the first.** A member declined
1367
+ * *and* unavailable in this build is two facts with two different remedies —
1368
+ * one stops being true when a package publishes and the other does not — so
1369
+ * the availability sentence the decision already carries is kept beside the
1370
+ * declined one rather than replaced by it.
1371
+ */
1372
+ function declinable(member, decision, input) {
1373
+ if (input.without?.has(member) !== true)
1374
+ return decision;
1375
+ const declined = `declined: you passed --without ${member}. ${DECLINED_CONSEQUENCE[member]} Scaffolding ` +
1376
+ `again without that flag writes it`;
1377
+ return {
1378
+ written: false,
1379
+ files: [],
1380
+ omission: decision.omission === null ? declined : `${declined}; and unavailable: ${decision.omission}`,
1381
+ };
1382
+ }
1383
+ /**
1384
+ * The packages the admin member declares, each range read off a manifest this
1385
+ * run resolved (R2.5a) — or the names that had none.
1386
+ *
1387
+ * **It declares no module**, and that is R3.6: *"the set appears exactly once in
1388
+ * the written tree — the root manifest's `dependencies`"*. An instance is a
1389
+ * workspace whose root holds the module packages, so pnpm links them into the
1390
+ * root's `node_modules`, which is where this member's own resolution reaches
1391
+ * them. A second spelling here is the one disagreement nothing in a client's
1392
+ * tree could detect.
1393
+ *
1394
+ * What it does declare is what its **own two source files name**: the shell
1395
+ * `main.tsx` mounts, the design system `index.css` imports, React, and the
1396
+ * build tools. The ranges for those come off the shell's own manifest — its
1397
+ * `peerDependencies` are what a host that mounts it must resolve, and its
1398
+ * optional peers are the shell's statement about what kind of application a
1399
+ * host is.
1400
+ */
1401
+ function adminMemberPackages(input) {
1402
+ const missing = [];
1403
+ const range = (name) => {
1404
+ const found = input.adminRanges.get(name);
1405
+ if (found === undefined || found.length === 0 || found.startsWith('workspace:')) {
1406
+ missing.push(name);
1407
+ return null;
1408
+ }
1409
+ return found;
1410
+ };
1411
+ const dependencies = new Map([
1412
+ [`${input.scope}admin-kit`, `^${input.adminKitVersion ?? ''}`],
1413
+ [`${input.scope}admin-shell`, `^${input.adminShellVersion ?? ''}`],
1414
+ ]);
1415
+ const devDependencies = [
1416
+ [`${input.scope}cli`, `^${input.cliVersion}`],
1417
+ ];
1418
+ for (const name of ['react', 'react-dom']) {
1419
+ const declared = range(name);
1420
+ if (declared !== null)
1421
+ dependencies.set(name, declared);
1422
+ }
1423
+ // Nothing else. What the **installed modules** declare optional is the root
1424
+ // manifest's, for the reason written beside it there: pnpm resolves a peer
1425
+ // from the dependent's context, and their dependent is the root.
1426
+ // `typescript` is the CLI's own (R2.3's first named source), exactly as the
1427
+ // backend member's is; the four build tools are the shell's optional peers.
1428
+ const typescript = input.declaredRanges.get('typescript');
1429
+ if (typescript === undefined || typescript.length === 0)
1430
+ missing.push('typescript');
1431
+ else
1432
+ devDependencies.push(['typescript', typescript]);
1433
+ for (const name of ['@tailwindcss/vite', '@vitejs/plugin-react', 'tailwindcss', 'vite']) {
1434
+ const declared = range(name);
1435
+ if (declared !== null)
1436
+ devDependencies.push([name, declared]);
1437
+ }
1438
+ return {
1439
+ dependencies: [...dependencies].sort(([a], [b]) => a.localeCompare(b)),
1440
+ devDependencies: devDependencies.sort(([a], [b]) => a.localeCompare(b)),
1441
+ missing,
1442
+ };
1443
+ }
1444
+ /**
1445
+ * The four the admin member declares as `devDependencies` rather than as
1446
+ * dependencies, because they build the bundle and are not in it.
1447
+ *
1448
+ * They reach this command as the admin shell's own optional peers — its
1449
+ * statement that a host which mounts it is a Vite application compiled with
1450
+ * Tailwind v4 — and that is one set, whichever block a consumer files each
1451
+ * member under.
1452
+ */
1453
+ const BUILD_TOOL_PEERS = new Set([
1454
+ '@tailwindcss/vite',
1455
+ '@vitejs/plugin-react',
1456
+ 'tailwindcss',
1457
+ 'vite',
1458
+ ]);
1459
+ /**
1460
+ * §2.4a, decided and rendered — or omitted, in the admin member's own grammar.
1461
+ *
1462
+ * **Why a member at all.** `instance-tree.md` §2.6 lists the documentation
1463
+ * registry among the three artefacts an instance generates and §2 listed no
1464
+ * member that would hold it, so the `.gitignore` this command writes has named
1465
+ * `docs/sidebars.modules.generated.js` since the command landed and nothing
1466
+ * wrote a site for it. A client installs thirty module packages, each shipping
1467
+ * its own `docs/` layer in its tarball, and until this member existed there was
1468
+ * nowhere for a human to read one.
1469
+ *
1470
+ * **Why an omission and not a refusal**, exactly as for the admin member: the
1471
+ * owner's subject is a backend instance, and a command that refused to write one
1472
+ * until Docusaurus had a range would be a command nobody could use. And **why
1473
+ * not silence**: a client who does not know they have no documentation site goes
1474
+ * looking for one.
1475
+ *
1476
+ * **Four files and no `tsconfig.json`.** The configuration and the sidebar are
1477
+ * `.js` rather than `.ts` — Docusaurus reads both — which is what keeps the
1478
+ * member off `@docusaurus/tsconfig`, `@docusaurus/types` and
1479
+ * `@docusaurus/module-type-aliases`, three more ranges this command would have
1480
+ * to find a source for in order to write a file whose whole content is two
1481
+ * objects. `sidebars.js` is also the name `resolveDocsLayout` looks for.
1482
+ */
1483
+ function docsMember(input) {
1484
+ const missing = ['@docusaurus/core', '@docusaurus/preset-classic'].filter((name) => {
1485
+ const range = input.docsRanges.get(name);
1486
+ return range === undefined || range.length === 0 || range.startsWith('workspace:');
1487
+ });
1488
+ if (missing.length > 0) {
1489
+ return {
1490
+ written: false,
1491
+ files: [],
1492
+ omission: `no manifest this run resolved declares a range for ${missing.join(', ')}, and the ` +
1493
+ `documentation site is built with ${missing.length === 1 ? 'it' : 'them'}. A range ` +
1494
+ `this command chose would be a value nobody reviewed, so none is written and the ` +
1495
+ `pages your module packages ship have no site to be read in`,
1496
+ };
1497
+ }
1498
+ return { written: true, files: docsFiles(input), omission: null };
1499
+ }
1500
+ /** The four files §2.4a's member is, in the order the plan writes them. */
1501
+ function docsFiles(input) {
1502
+ return [
1503
+ {
1504
+ path: 'docs/package.json',
1505
+ kind: 'derived',
1506
+ member: 'docs',
1507
+ content: json({
1508
+ name: `${input.name}-docs`,
1509
+ private: true,
1510
+ scripts: {
1511
+ // `endora generate` first, for the reason the admin member's `build`
1512
+ // runs it first: Docusaurus is a static build, so a navigation that
1513
+ // is stale or absent is a site with pages missing, and
1514
+ // `onBrokenLinks: 'throw'` turns the *absent* half into a crash
1515
+ // rather than a silence — which is the better of the two failures and
1516
+ // still not one a client should have to meet.
1517
+ generate: 'endora generate',
1518
+ dev: 'endora generate && docusaurus start',
1519
+ build: 'endora generate && docusaurus build',
1520
+ serve: 'docusaurus serve',
1521
+ },
1522
+ dependencies: {
1523
+ '@docusaurus/core': input.docsRanges.get('@docusaurus/core'),
1524
+ '@docusaurus/preset-classic': input.docsRanges.get('@docusaurus/preset-classic'),
1525
+ },
1526
+ devDependencies: { [`${input.scope}cli`]: `^${input.cliVersion}` },
1527
+ }),
1528
+ },
1529
+ {
1530
+ path: 'docs/docusaurus.config.js',
1531
+ kind: 'client',
1532
+ member: 'docs',
1533
+ content: docsConfig(input),
1534
+ },
1535
+ {
1536
+ path: 'docs/sidebars.js',
1537
+ kind: 'client',
1538
+ member: 'docs',
1539
+ content: docsSidebar(),
1540
+ },
1541
+ {
1542
+ path: 'docs/docs/intro.md',
1543
+ kind: 'client',
1544
+ member: 'docs',
1545
+ content: docsIntro(input),
1546
+ },
1547
+ ];
1548
+ }
1549
+ /**
1550
+ * The site's configuration — the client's, in the sense `admin/vite.config.ts`
1551
+ * is theirs: build-tool configuration they will edit.
1552
+ *
1553
+ * Three values are load-bearing rather than decorative. `onBrokenLinks: 'throw'`
1554
+ * is what makes a navigation entry naming a page that is not there fail the
1555
+ * build instead of serving a 404 nobody notices — it is the property feature
1556
+ * 100's own `build:docs` job exists for. The docs plugin declares **no**
1557
+ * `path`, so the content root is Docusaurus's own `docs/`, which is where
1558
+ * `endora generate` puts the pages your modules ship. And `routeBasePath: '/'`
1559
+ * makes the documentation the site rather than a section of one: an instance's
1560
+ * documentation site has nothing else in it.
1561
+ */
1562
+ function docsConfig(input) {
1563
+ return `// @ts-check
1564
+ // The documentation site of this instance. Yours to brand and to extend — the
1565
+ // title, the URL and the navbar below are values nobody but you can supply.
1566
+ //
1567
+ // What you should not remove:
1568
+ //
1569
+ // * \`onBrokenLinks: 'throw'\` — a navigation entry naming a page that is not
1570
+ // there fails the build instead of serving a 404 nobody notices;
1571
+ // * the docs plugin's absent \`path\` — the content root is Docusaurus's own
1572
+ // \`docs/\`, which is where \`endora generate\` copies the pages your module
1573
+ // packages ship.
1574
+
1575
+ /** @type {import('@docusaurus/types').Config} */
1576
+ const config = {
1577
+ title: '${input.name} documentation',
1578
+ tagline: 'Every module this instance installed, documented by the module that ships it.',
1579
+ url: 'https://example.com',
1580
+ baseUrl: '/',
1581
+ onBrokenLinks: 'throw',
1582
+ onBrokenMarkdownLinks: 'warn',
1583
+ favicon: undefined,
1584
+ presets: [
1585
+ [
1586
+ 'classic',
1587
+ {
1588
+ docs: {
1589
+ sidebarPath: './sidebars.js',
1590
+ routeBasePath: '/',
1591
+ },
1592
+ blog: false,
1593
+ },
1594
+ ],
1595
+ ],
1596
+ themeConfig: {
1597
+ navbar: {
1598
+ title: '${input.name}',
1599
+ items: [{ type: 'docSidebar', sidebarId: 'main', position: 'left', label: 'Documentation' }],
1600
+ },
1601
+ },
1602
+ };
1603
+
1604
+ module.exports = config;
1605
+ `;
1606
+ }
1607
+ /**
1608
+ * The navigation — the client's own, with one generated category in it.
1609
+ *
1610
+ * It is `admin/src/index.css`'s shape rather than `admin/src/main.tsx`'s: the
1611
+ * file is yours, and one line of it names a file a generator writes. Everything
1612
+ * you add goes beside \`'intro'\`; nothing you add has to know that the Modules
1613
+ * category is derived.
1614
+ */
1615
+ function docsSidebar() {
1616
+ return `// @ts-check
1617
+
1618
+ /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */
1619
+ const sidebars = {
1620
+ main: [
1621
+ 'intro',
1622
+ {
1623
+ type: 'category',
1624
+ label: 'Modules',
1625
+ link: { type: 'generated-index', title: 'Modules' },
1626
+ // Generated by \`endora generate\` over the module packages THIS instance
1627
+ // installed, and not committed: a different module set is a different
1628
+ // navigation. Run \`pnpm run generate\` before the first build.
1629
+ items: require('./sidebars.modules.generated.js'),
1630
+ },
1631
+ ],
1632
+ };
1633
+
1634
+ module.exports = sidebars;
1635
+ `;
1636
+ }
1637
+ /** The page the site opens on. Yours from the first word. */
1638
+ function docsIntro(input) {
1639
+ return `---
1640
+ title: ${input.name}
1641
+ sidebar_label: Start here
1642
+ slug: /
1643
+ ---
1644
+
1645
+ # ${input.name}
1646
+
1647
+ This is your instance's documentation site. Everything under **Modules** is
1648
+ written by the module that ships it and copied in by \`pnpm run generate\`, so a
1649
+ module you install brings its own pages and a module you remove takes them with
1650
+ it. Nothing under that category is yours to edit — the next \`generate\` undoes
1651
+ it.
1652
+
1653
+ Everything else is yours. Write your own operating notes beside this page and
1654
+ add them to \`sidebars.js\`.
1655
+ `;
1656
+ }
1657
+ /**
1658
+ * §2.4, decided and rendered — or omitted, in `new storefront`'s own grammar.
1659
+ *
1660
+ * **Why an omission and not a refusal**, unchanged from the build that wrote no
1661
+ * admin at all: *"the owner's subject is a backend instance, and a command that
1662
+ * refused to write one until an unrelated package existed would be a command
1663
+ * nobody could use to find out whether any of this works."* **Why not silence**:
1664
+ * `instance-repository.md` R8.2's reasoning one surface over — a client who does
1665
+ * not know they have no operator interface spends their first hour looking for
1666
+ * one.
1667
+ *
1668
+ * There are now two ways to reach that omission and they are reported apart,
1669
+ * because the remedies are different: a build in which the shell or the design
1670
+ * system does not resolve, and a build in which one of them does and a range its
1671
+ * own manifest should have declared is not there. The second is R2.5a — *"a
1672
+ * value the tool invented is a value nobody reviewed"* — and naming the range
1673
+ * is what makes it actionable rather than mysterious.
1674
+ */
1675
+ function adminMember(input) {
1676
+ const absent = [
1677
+ ...(input.adminShellVersion === null ? [`${input.scope}admin-shell`] : []),
1678
+ ...(input.adminKitVersion === null ? [`${input.scope}admin-kit`] : []),
1679
+ ];
1680
+ if (absent.length > 0) {
1681
+ return {
1682
+ written: false,
1683
+ files: [],
1684
+ omission: `${absent.join(' and ')} ${absent.length === 1 ? 'does' : 'do'} not resolve at the ` +
1685
+ `version being installed, and the admin member is mounted on ${absent.length === 1 ? 'it' : 'them'}; an instance scaffolded now is a headless API`,
1686
+ };
1687
+ }
1688
+ const packages = adminMemberPackages(input);
1689
+ if (packages.missing.length > 0) {
1690
+ return {
1691
+ written: false,
1692
+ files: [],
1693
+ omission: `no manifest this run resolved declares a range for ${packages.missing.join(', ')}, ` +
1694
+ `and the admin member cannot be built without ${packages.missing.length === 1 ? 'it' : 'them'}. A range this command chose would be a value nobody reviewed, so none is written ` +
1695
+ `and an instance scaffolded now is a headless API`,
1696
+ };
1697
+ }
1698
+ return { written: true, files: adminFiles(input, packages), omission: null };
1699
+ }
1700
+ /** The six files §2.4's member is, in the order the plan writes them. */
1701
+ function adminFiles(input, packages) {
1702
+ return [
1703
+ {
1704
+ path: 'admin/package.json',
1705
+ kind: 'derived',
1706
+ member: 'admin',
1707
+ content: json({
1708
+ name: `${input.name}-admin`,
1709
+ private: true,
1710
+ type: 'module',
1711
+ scripts: {
1712
+ // `endora generate` renders §2.6's two artefacts over the packages
1713
+ // this instance installed, and `build` runs it first for the reason
1714
+ // both artefacts exist: Vite and Tailwind are static, so a stale or
1715
+ // absent registry is a bundle with screens missing and a stylesheet
1716
+ // with classes missing, neither of which fails loudly.
1717
+ generate: 'endora generate',
1718
+ dev: 'endora generate && vite',
1719
+ build: 'endora generate && vite build',
1720
+ preview: 'vite preview',
1721
+ typecheck: 'tsc --noEmit',
1722
+ },
1723
+ dependencies: Object.fromEntries(packages.dependencies),
1724
+ devDependencies: Object.fromEntries(packages.devDependencies),
1725
+ }),
1726
+ },
1727
+ {
1728
+ path: 'admin/tsconfig.json',
1729
+ kind: 'client',
1730
+ member: 'admin',
1731
+ content: json({
1732
+ compilerOptions: {
1733
+ target: 'ES2023',
1734
+ lib: ['DOM', 'DOM.Iterable', 'ES2023'],
1735
+ module: 'ESNext',
1736
+ moduleResolution: 'Bundler',
1737
+ jsx: 'react-jsx',
1738
+ strict: true,
1739
+ skipLibCheck: true,
1740
+ noEmit: true,
1741
+ types: ['vite/client'],
1742
+ baseUrl: '.',
1743
+ // The `"@/*"` alias is how `endora generate` finds this project: both
1744
+ // artefacts land in the source root of the workspace member that
1745
+ // declares it, which is the derivation `check:admin-surface` and
1746
+ // `check:admin-zones` already share. Renaming it moves the artefacts;
1747
+ // deleting it leaves the generator with no project to write to.
1748
+ paths: { '@/*': ['./src/*'] },
1749
+ },
1750
+ include: ['src'],
1751
+ }),
1752
+ },
1753
+ {
1754
+ path: 'admin/index.html',
1755
+ kind: 'client',
1756
+ member: 'admin',
1757
+ content: adminIndexHtml(input),
1758
+ },
1759
+ {
1760
+ path: 'admin/vite.config.ts',
1761
+ kind: 'client',
1762
+ member: 'admin',
1763
+ content: adminViteConfig(),
1764
+ },
1765
+ {
1766
+ path: 'admin/src/main.tsx',
1767
+ kind: 'wiring',
1768
+ member: 'admin',
1769
+ content: `import { createRoot } from 'react-dom/client';
1770
+ import { AdminRoot } from '${input.scope}admin-shell';
1771
+ import { MODULE_ADMIN_CONTRIBUTIONS } from './modules.generated.js';
1772
+ import './index.css';
1773
+
1774
+ const root = document.getElementById('root');
1775
+ if (root === null) throw new Error('index.html has no #root to mount into.');
1776
+
1777
+ createRoot(root).render(<AdminRoot contributions={MODULE_ADMIN_CONTRIBUTIONS} />);
1778
+ `,
1779
+ },
1780
+ {
1781
+ path: 'admin/src/index.css',
1782
+ kind: 'client',
1783
+ member: 'admin',
1784
+ content: adminStylesheet(input),
1785
+ },
1786
+ ];
1787
+ }
1788
+ /** The document the bundle mounts into — the client's, and the client's to brand. */
1789
+ function adminIndexHtml(input) {
1790
+ return `<!doctype html>
1791
+ <html lang="en">
1792
+ <head>
1793
+ <meta charset="UTF-8" />
1794
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
1795
+ <!-- An operator interface is not a public page. -->
1796
+ <meta name="robots" content="noindex, nofollow" />
1797
+ <title>${input.name} — Admin</title>
1798
+ <!--
1799
+ This file is yours. A favicon, a web app manifest, your own fonts and
1800
+ your own <meta> all go here, and the files they name go in \`public/\`.
1801
+ The scaffold writes none of them: a logo is exactly the value a tool may
1802
+ not invent for you.
1803
+ -->
1804
+ </head>
1805
+ <body>
1806
+ <div id="root"></div>
1807
+ <script type="module" src="/src/main.tsx"></script>
1808
+ </body>
1809
+ </html>
1810
+ `;
1811
+ }
1812
+ /**
1813
+ * The build, which is Vite's business and therefore the client's file.
1814
+ *
1815
+ * Two plugins and a port. `@tailwindcss/vite` is what compiles `index.css`, and
1816
+ * without it every class in this admin is an unrecognised token — silently,
1817
+ * which is the failure `admin-stylesheet-composition.md` is about.
1818
+ */
1819
+ function adminViteConfig() {
1820
+ return `import { defineConfig, loadEnv } from 'vite';
1821
+ import react from '@vitejs/plugin-react';
1822
+ import tailwindcss from '@tailwindcss/vite';
1823
+
1824
+ export default defineConfig(({ mode }) => {
1825
+ // \`loadEnv(mode, cwd, '')\` — the empty prefix lets this file read an
1826
+ // unprefixed variable such as \`PORT\`. Only \`VITE_*\` reaches the bundle.
1827
+ const env = loadEnv(mode, process.cwd(), '');
1828
+ const port = Number(env['PORT']) || 3002;
1829
+ return {
1830
+ plugins: [react(), tailwindcss()],
1831
+ server: { port, strictPort: true },
1832
+ preview: { port, strictPort: true },
1833
+ // No source maps in a built admin: \`dist\` is served as static files, and a
1834
+ // \`.map\` beside a chunk is every module's screens, route guards and
1835
+ // permission codes, readable by anyone who can reach the app. A
1836
+ // reproduction runs \`pnpm run dev\`, which has them.
1837
+ build: { outDir: 'dist', sourcemap: false },
1838
+ };
1839
+ });
1840
+ `;
1841
+ }
1842
+ /**
1843
+ * The stylesheet — three imports and the override slot, in that order
1844
+ * (`admin-stylesheet-composition.md` R2.3, R3.1, R3.4).
1845
+ *
1846
+ * The order is the whole mechanism: Tailwind first, the design system's tokens
1847
+ * and classes second, the installed packages' `@source` declarations third, and
1848
+ * this deployment's redeclarations **last**, so a later rule wins over the
1849
+ * package's default. Each semantic token is an indirection, so a utility the
1850
+ * design system's own build never saw still resolves against the `:root` below.
1851
+ */
1852
+ function adminStylesheet(input) {
1853
+ return `@import "tailwindcss";
1854
+
1855
+ /*
1856
+ * The admin's design system — its tokens **and** its class vocabulary. It is a
1857
+ * package's, not this project's: a copy of it here would be a fork frozen on
1858
+ * the day you scaffolded, and a module release adding one class would render
1859
+ * unstyled in this instance with no diagnostic anywhere.
1860
+ *
1861
+ * Change a token by redeclaring it in the slot at the bottom of this file, and
1862
+ * a class by writing a later rule there. Never by editing the package.
1863
+ */
1864
+ @import "${input.scope}admin-kit/theme.css";
1865
+
1866
+ /*
1867
+ * The packages this admin composes, each declaring its own sources.
1868
+ *
1869
+ * Generated by \`pnpm run generate\` (\`endora generate\`) over the packages this
1870
+ * instance installed, and git-ignored: which packages those are is a fact about
1871
+ * the install rather than about this tree. Tailwind is a static scan and says
1872
+ * nothing about a source that matches nothing, so a package that is not scanned
1873
+ * loses every utility class only it declares — silently. Here the failures are
1874
+ * loud instead: a package that is not installed is \`Can't resolve\`, and one
1875
+ * whose tarball omits the file is \`ERR_PACKAGE_PATH_NOT_EXPORTED\`.
1876
+ */
1877
+ @import "./tailwind.generated.css";
1878
+
1879
+ /*
1880
+ * Your overrides go here, last. A \`:root\` line for a token, an ordinary rule
1881
+ * for a class. This slot is empty rather than absent: your first edit is a line
1882
+ * below this comment, and nothing above it is yours to change.
1883
+ */
1884
+ `;
1885
+ }
1886
+ //# sourceMappingURL=template.js.map