@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,793 @@
1
+ /**
2
+ * `endora install <dir>` — one command from nothing to an installed Endora
3
+ * Commerce (`specs/125-first-mile-install/spec.md` §4.3, FR-140…FR-161).
4
+ *
5
+ * ## What it is, in one sentence
6
+ *
7
+ * The two `new` commands write trees and run nothing; between the second of
8
+ * them and a shop there were **nine** further typed steps (§2.3). This verb
9
+ * runs them, and it is deliberately the only place in the product where a
10
+ * scaffold and a pipeline meet.
11
+ *
12
+ * ## It composes, and reimplements nothing (FR-143)
13
+ *
14
+ * `runNewInstance` and `runNewStorefront` are the writers. Nothing here renders
15
+ * a template, resolves a module or decides a member: a second implementation of
16
+ * any of those is D-100's failure mode with a new name, and the case in
17
+ * `test/install.test.ts` that reads this file's own source is what keeps it
18
+ * true. The four flags that belong to `new instance` — `--module`,
19
+ * `--deployment`, `--registry`, `--topology` — are passed through unread.
20
+ *
21
+ * ## When it asks, and when it never does
22
+ *
23
+ * `cli-product.md` R2.5c is a **ceiling**: a prompt may be issued only when
24
+ * both descriptors are TTYs and no `--non-interactive`, `--dry-run` or CI
25
+ * marker is present. Below it, this command asks nothing at all and every
26
+ * missing answer is part of the one refusal — the posture Phase 3 shipped, and
27
+ * unchanged. At a terminal it runs the wizard (`wizard.ts`, spec §6), under
28
+ * R2.5f — 125's PR-1(b), accepted by the owner on 2026-09-25 and placed as
29
+ * D-269. The wizard is a thing that fills flags in: its question set is the
30
+ * flag set, so what it hands the rest of this function is the same options a
31
+ * fully-flagged `--non-interactive` run supplies (SC-107).
32
+ *
33
+ * ## Validate completely, then write — and then, only then, run
34
+ *
35
+ * `runNewInstance`'s discipline (R5.2), one layer up and with a third phase.
36
+ * Every precondition is decided before the first byte is written: the target,
37
+ * the Node version, a package-manager runner, a reachable Docker daemon unless
38
+ * `--no-services`, a reference storefront unless `--no-storefront`, and every
39
+ * answer the pipeline will need. They are reported in **one** refusal naming
40
+ * all of them (FR-157), because a client who has to run a command five times to
41
+ * learn five things has been handed a puzzle.
42
+ *
43
+ * ## Where nothing of ours is installed, it provisions a host first (D-271)
44
+ *
45
+ * `new instance` reads the platform's and every module's manifest off the
46
+ * packages installed beside the target or the working directory, and under
47
+ * `npx` there are none there. So when the platform does not resolve, the first
48
+ * step installs this CLI's release index into a temporary directory
49
+ * (`host.ts`) and `new instance` is called with that directory as its working
50
+ * directory — before the target is written, removed once it has been, kept and
51
+ * named when anything fails. It is not a pipeline step: the instance is not
52
+ * left with it, and a checkout or a pre-installed host never plans it.
53
+ *
54
+ * ## Every step prints the command it is about to run (FR-156)
55
+ *
56
+ * So an operator watching can reproduce any step by hand, and a failure names a
57
+ * command rather than a phase. A failure exits with **that step's own** exit
58
+ * code and prints what is left as a resumable list — the steps are the
59
+ * instance's own root scripts, so the list is a sequence a client can finish by
60
+ * typing.
61
+ */
62
+ import { spawn, spawnSync } from 'node:child_process';
63
+ import { existsSync, mkdtempSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'node:fs';
64
+ import { tmpdir } from 'node:os';
65
+ import { isAbsolute, join, resolve, sep } from 'node:path';
66
+ import { parseEnvFile, writeEnvFile } from '../inputs/env-file.js';
67
+ import { generateSecret, interactivityOf, mayPrompt, } from '../inputs/resolve.js';
68
+ import { ownReleaseIndexPath } from '../lib/release-index.js';
69
+ import { developmentAddresses, developmentMailUrl, DEV_COMPOSE_PATH, } from '../new-instance/deploy.js';
70
+ import { MEMBER_VOCABULARY, memberRefusal, runNewInstance, } from '../new-instance/index.js';
71
+ import { probePlatform } from '../new-instance/host.js';
72
+ import { runNewStorefront, } from '../new-storefront/index.js';
73
+ import { HOST_DIRECTORY_PREFIX, hostNpmrc, readReleaseIndex, writeHost } from './host.js';
74
+ import { answeredByFlags, answersLine, askWizard, WizardClosedError, } from './wizard.js';
75
+ /** A refusal the operator can act on — exit 1. */
76
+ export class InstallInputError extends Error {
77
+ name = 'InstallInputError';
78
+ }
79
+ /**
80
+ * An input this run could not read — exit 2 — or a failed host install, which
81
+ * exits with **that step's own** code like every other step (FR-156).
82
+ */
83
+ export class InstallHostError extends Error {
84
+ name = 'InstallHostError';
85
+ exitCode;
86
+ constructor(message, exitCode = 2) {
87
+ super(message);
88
+ this.exitCode = exitCode;
89
+ }
90
+ }
91
+ /**
92
+ * The lowest Node this command will run an instance on.
93
+ *
94
+ * The **CLI's own** `engines.node`, read from its own manifest, for R2.3's
95
+ * reason: an instance re-declares that value rather than choosing one, so a
96
+ * one-shot that checked a different number would be enforcing a requirement
97
+ * nobody wrote down.
98
+ */
99
+ function ownEnginesNode() {
100
+ try {
101
+ const manifest = JSON.parse(readFileSync(new URL('../../package.json', import.meta.url), 'utf8'));
102
+ return typeof manifest.engines?.node === 'string' ? manifest.engines.node : undefined;
103
+ }
104
+ catch {
105
+ return undefined;
106
+ }
107
+ }
108
+ /** `>=22.17.0` against `process.versions.node`, and nothing cleverer. */
109
+ function satisfiesNode(range, version) {
110
+ const wanted = /(\d+)\.(\d+)\.(\d+)/.exec(range);
111
+ if (wanted === null)
112
+ return true;
113
+ const have = /(\d+)\.(\d+)\.(\d+)/.exec(version);
114
+ if (have === null)
115
+ return true;
116
+ for (let index = 1; index <= 3; index += 1) {
117
+ const left = Number(have[index]);
118
+ const right = Number(wanted[index]);
119
+ if (left !== right)
120
+ return left > right;
121
+ }
122
+ return true;
123
+ }
124
+ /** Does this command answer at all? A probe, never a version comparison. */
125
+ function probe(command, args) {
126
+ const result = spawnSync(command, [...args], { stdio: 'ignore', timeout: 20_000 });
127
+ return result.error === undefined && result.status === 0;
128
+ }
129
+ /**
130
+ * How this machine runs `pnpm`, in the declared order (FR-158).
131
+ *
132
+ * `pnpm` on `PATH` first, then `corepack pnpm@<range>` — and **never**
133
+ * `corepack enable`, which is baseline step A1 and a command that writes shims
134
+ * into a directory this program does not own. `corepack pnpm@…` runs the
135
+ * package manager without changing anything about the machine, which is the
136
+ * property that lets this step be a fallback rather than an installation.
137
+ */
138
+ function resolvePackageManagers() {
139
+ const found = [];
140
+ if (probe('pnpm', ['--version'])) {
141
+ found.push({ command: 'pnpm', prefix: [], label: 'pnpm' });
142
+ }
143
+ if (probe('corepack', ['--version'])) {
144
+ found.push({ command: 'corepack', prefix: ['pnpm@latest'], label: 'corepack pnpm@latest' });
145
+ }
146
+ return found;
147
+ }
148
+ /**
149
+ * Is a Docker daemon there? `docker info` answers, `docker --version` does not.
150
+ *
151
+ * **The server version it prints is the answer, not its exit status.** Docker
152
+ * CLI 28 exits 0 with an empty line when no daemon is listening (29 exits 1),
153
+ * so a status-only probe told a client on 28 with Docker stopped that the
154
+ * services step could run. Only a daemon can name its own version.
155
+ */
156
+ function dockerIsReachable() {
157
+ const result = spawnSync('docker', ['info', '--format', '{{.ServerVersion}}'], {
158
+ stdio: ['ignore', 'pipe', 'ignore'],
159
+ encoding: 'utf8',
160
+ timeout: 20_000,
161
+ });
162
+ return result.error === undefined && result.status === 0 && result.stdout.trim() !== '';
163
+ }
164
+ /**
165
+ * The refusals, collected.
166
+ *
167
+ * One object, filled completely and then thrown once: FR-157's *"in one
168
+ * refusal naming all of them"*. A `throw` at the first finding is the shape
169
+ * this deliberately is not — it makes the number of runs a client needs equal
170
+ * to the number of things they got wrong.
171
+ */
172
+ class Refusals {
173
+ found = [];
174
+ add(sentence) {
175
+ this.found.push(sentence);
176
+ }
177
+ get length() {
178
+ return this.found.length;
179
+ }
180
+ throwIfAny() {
181
+ if (this.found.length === 0)
182
+ return;
183
+ throw new InstallInputError(`\`endora install\` cannot run yet — ${String(this.found.length)} thing` +
184
+ `${this.found.length === 1 ? '' : 's'} to settle first:\n` +
185
+ this.found.map((sentence) => ` - ${sentence}`).join('\n') +
186
+ `\n\nNothing was written and nothing was started.`);
187
+ }
188
+ }
189
+ /** The target must be empty, or hold nothing but a `.env` — `new instance`'s rule. */
190
+ function occupied(dir) {
191
+ if (!existsSync(dir))
192
+ return null;
193
+ const entries = readdirSync(dir);
194
+ if (entries.length === 0)
195
+ return null;
196
+ if (entries.length === 1 && entries[0] === '.env')
197
+ return null;
198
+ return entries.slice(0, 5).join(', ');
199
+ }
200
+ /**
201
+ * Is there a reference storefront to copy, from where this command is standing?
202
+ *
203
+ * **The finding this function exists for**: `endora new storefront` copies the
204
+ * reference storefront out of a **checkout of the platform repository**
205
+ * (`new-storefront/reference.ts`: *"no pnpm-workspace.yaml above …"*), so an
206
+ * installed CLI standing in an empty directory cannot write one however it is
207
+ * asked. The spec's §7.6 assumes otherwise. The honest answer here is to refuse
208
+ * **before** writing an instance, naming the flag that skips the storefront —
209
+ * fail-closed, with a remedy the client can act on — rather than to write half
210
+ * the trio and fail in the middle of the pipeline.
211
+ */
212
+ function referenceStorefrontRoot(cwd) {
213
+ let dir = resolve(cwd);
214
+ for (;;) {
215
+ if (existsSync(join(dir, 'pnpm-workspace.yaml')))
216
+ return dir;
217
+ const parent = resolve(dir, '..');
218
+ if (parent === dir)
219
+ return null;
220
+ dir = parent;
221
+ }
222
+ }
223
+ /**
224
+ * FR-105's derivation, performed on the tree this run just wrote.
225
+ *
226
+ * The values are read off **the rendered `compose.dev.yml`** and are written
227
+ * only over names the instance's own `.env` already lists as unanswered
228
+ * placeholders. That is the intersection stated structurally rather than as a
229
+ * list: the placeholders are this instance's declaration, rendered, so an
230
+ * instance that installs neither `search` nor `email` has no line to fill in
231
+ * for either and no name of theirs appears in this file.
232
+ *
233
+ * A value the operator already supplied is never written over — `writeEnvFile`
234
+ * fills a **commented** placeholder in place, and a filled line is not one.
235
+ */
236
+ function deriveEnvironment(targetDir) {
237
+ const envPath = join(targetDir, '.env');
238
+ const composePath = join(targetDir, DEV_COMPOSE_PATH);
239
+ if (!existsSync(envPath) || !existsSync(composePath))
240
+ return [];
241
+ const text = readFileSync(envPath, 'utf8');
242
+ const answered = parseEnvFile(text);
243
+ const placeholders = new Set([...text.matchAll(/^\s*#\s*([A-Z][A-Z0-9_]*)\s*=/gm)].map((match) => match[1]));
244
+ const addresses = [...developmentAddresses(readFileSync(composePath, 'utf8'))].filter(([name]) => placeholders.has(name) && !answered.has(name));
245
+ if (addresses.length === 0)
246
+ return [];
247
+ // The sentence is the whole of the mitigation, so it names **which** values
248
+ // it is about: they are filled in above, each over its own placeholder and in
249
+ // the declaration's own order, and a comment at the bottom saying "these" and
250
+ // nothing more would send a reader looking for a block that is not there.
251
+ const header = [
252
+ '',
253
+ `# ${addresses.map(([name]) => name).join(', ')} above were DERIVED by \`endora install\``,
254
+ `# from ${DEV_COMPOSE_PATH} beside this file: they are the addresses of the containers`,
255
+ '# `pnpm run dev:services` starts, on THIS machine. Nothing invented them and nothing',
256
+ '# asked you — change a published port in that file and they are what it publishes.',
257
+ '#',
258
+ '# On a machine that is not a development one they are not the values you want: replace',
259
+ '# each with the address of the service you actually run. They fail closed — a database',
260
+ '# that is not there refuses a connection — rather than quietly writing somewhere else.',
261
+ ].join('\n');
262
+ const written = writeEnvFile(`${text.replace(/\n+$/, '')}\n${header}\n`, new Map(addresses));
263
+ writeFileSync(envPath, written, 'utf8');
264
+ return addresses.map(([name]) => name);
265
+ }
266
+ /** The storefront's own `.env`, with the values this run owes it (FR-153/154). */
267
+ function storefrontInputs(instance, secret) {
268
+ const port = '3001';
269
+ return {
270
+ NEXT_PUBLIC_API_BASE_URL: `http://localhost:${port}`,
271
+ BACKEND_BASE_URL: `http://localhost:${port}`,
272
+ NEXT_PUBLIC_SITE_URL: 'http://localhost:3000',
273
+ // The platform's own fallback, which is what the instance's default Sales
274
+ // Channel is created with when `DEFAULT_SALES_CHANNEL_CODE` is unset
275
+ // (`kernel/sales-channels/default-channel-reconciler.ts`). A different
276
+ // string here would point the storefront at a channel nothing creates.
277
+ NEXT_PUBLIC_SALES_CHANNEL_CODE: 'default',
278
+ REVALIDATE_SECRET: secret,
279
+ ...Object.fromEntries(instance.resolved
280
+ .filter((entry) => entry.name === 'DEFAULT_SALES_CHANNEL_CODE')
281
+ .map((entry) => ['NEXT_PUBLIC_SALES_CHANNEL_CODE', entry.value])),
282
+ };
283
+ }
284
+ /**
285
+ * The whole command.
286
+ *
287
+ * Three phases, in this order and never interleaved: **decide** (every
288
+ * precondition, one refusal), **write** (the two scaffolds and the derived
289
+ * `.env`), **run** (the pipeline, each step echoed). A `--dry-run` stops after
290
+ * the first and reports the other two.
291
+ */
292
+ export async function runInstall(given) {
293
+ const cwd = given.cwd ?? process.cwd();
294
+ const output = [];
295
+ const say = (line) => {
296
+ output.push(line);
297
+ given.echo?.(line);
298
+ };
299
+ // ── ask, only where R2.5c's conjunction holds (R2.5f i) ───────────────────
300
+ const interactive = mayPrompt(given.interactivity ??
301
+ interactivityOf({
302
+ nonInteractive: given.nonInteractive === true,
303
+ dryRun: given.dryRun === true,
304
+ }));
305
+ let options = given;
306
+ let provenance;
307
+ if (interactive) {
308
+ try {
309
+ const outcome = await askWizard(given, given.io ?? { input: process.stdin, output: process.stdout, terminal: true }, {
310
+ vocabulary: MEMBER_VOCABULARY,
311
+ storefront: {
312
+ available: referenceStorefrontRoot(cwd) !== null,
313
+ reason: 'not from here: it is copied out of a checkout of the platform repository, ' +
314
+ 'and there is none above this directory — `endora new storefront` adds it later',
315
+ },
316
+ });
317
+ options = { ...given, ...outcome.answers };
318
+ provenance = outcome;
319
+ }
320
+ catch (error) {
321
+ if (error instanceof WizardClosedError)
322
+ throw new InstallInputError(error.message);
323
+ throw error;
324
+ }
325
+ }
326
+ else {
327
+ // Nothing asked: every answer is a flag, and the two offered questions a
328
+ // flag did not answer take §6.2's recommendation — *everything*, *yes* —
329
+ // which is what Phase 3 already did and is now said out loud.
330
+ const fromFlags = answeredByFlags(given);
331
+ provenance = {
332
+ fromFlags,
333
+ prompted: [],
334
+ recommended: ['parts', 'services'].filter((id) => !fromFlags.has(id)),
335
+ };
336
+ }
337
+ // ── decide ────────────────────────────────────────────────────────────────
338
+ const refusals = new Refusals();
339
+ // A missing directory is one refusal among the others rather than the first
340
+ // and only one: a run with no answers at all is told everything it owes in
341
+ // one message (FR-157, R3.2), and the checks that need a directory are the
342
+ // only ones it skips.
343
+ const namedDir = options.dir === undefined || options.dir.trim().length === 0
344
+ ? null
345
+ : isAbsolute(options.dir)
346
+ ? options.dir
347
+ : resolve(cwd, options.dir);
348
+ if (namedDir === null) {
349
+ refusals.add('`endora install` takes the directory to write — `<dir>` — and outside the wizard has no ' +
350
+ "default: the directory's basename becomes the workspace name, so a default would " +
351
+ 'invent a name nobody chose.');
352
+ }
353
+ const wantsStorefront = options.storefront !== false;
354
+ const wantsServices = options.services !== false;
355
+ const namedStorefrontDir = !wantsStorefront || namedDir === null
356
+ ? null
357
+ : options.storefrontDir === undefined
358
+ ? `${namedDir}-storefront`
359
+ : isAbsolute(options.storefrontDir)
360
+ ? options.storefrontDir
361
+ : resolve(cwd, options.storefrontDir);
362
+ const declinedRefusal = memberRefusal(options.without ?? []);
363
+ if (declinedRefusal !== null)
364
+ refusals.add(declinedRefusal);
365
+ const inTheWay = namedDir === null ? null : occupied(namedDir);
366
+ if (inTheWay !== null) {
367
+ refusals.add(`${namedDir} exists and is not empty (${inTheWay}). An instance's files are yours; ` +
368
+ 'this command never merges into a directory. Scaffold into an empty one — a ' +
369
+ 'directory holding nothing but a `.env` is the exception, and that file is read as ' +
370
+ 'your own answers.');
371
+ }
372
+ // FR-160 — a sibling, never a child. A storefront inside the instance would
373
+ // be swept into its `pnpm-workspace.yaml` globs and become a member of a
374
+ // workspace it is not part of, which is D-230's topology broken by a
375
+ // directory choice.
376
+ if (namedStorefrontDir !== null && namedStorefrontDir.startsWith(namedDir + sep)) {
377
+ refusals.add(`--storefront-dir ${namedStorefrontDir} is inside ${namedDir}. The storefront is its own ` +
378
+ "repository (D-195): written there it would be swept into the instance's workspace " +
379
+ 'globs and become a member of a workspace it is not part of. Put it beside the ' +
380
+ 'instance — the default is `<dir>-storefront`.');
381
+ }
382
+ if (namedStorefrontDir !== null) {
383
+ const storefrontInTheWay = occupied(namedStorefrontDir);
384
+ if (storefrontInTheWay !== null) {
385
+ refusals.add(`${namedStorefrontDir} exists and is not empty (${storefrontInTheWay}). Pass ` +
386
+ '`--storefront-dir <path>` for another directory, or `--no-storefront` to write no ' +
387
+ 'storefront at all.');
388
+ }
389
+ }
390
+ const engines = ownEnginesNode();
391
+ if (engines !== undefined && !satisfiesNode(engines, process.versions.node)) {
392
+ refusals.add(`this build needs Node ${engines} and is running on ${process.versions.node}. The ` +
393
+ 'instance it writes declares the same range, so a run on this one would install a ' +
394
+ 'tree its own manifest refuses.');
395
+ }
396
+ const runners = options.packageManagers ?? resolvePackageManagers();
397
+ if (runners.length === 0) {
398
+ refusals.add('no package manager to run: `pnpm` is not on PATH and `corepack` is not either. ' +
399
+ 'Install pnpm (`npm i -g pnpm`), or use a Node that ships corepack — this command ' +
400
+ 'runs `corepack pnpm@latest` when it has to, and never `corepack enable`, which ' +
401
+ 'writes shims into a directory it does not own.');
402
+ }
403
+ if (wantsServices && (options.dockerReachable ?? dockerIsReachable()) === false) {
404
+ refusals.add('no Docker daemon answered. The development stack — PostgreSQL, Redis, Meilisearch ' +
405
+ 'and a mail catcher — is started with `docker compose`, so this run has nothing to ' +
406
+ 'migrate against. Start Docker, or pass `--no-services` and point the instance at ' +
407
+ 'services you run yourself.');
408
+ }
409
+ if (wantsStorefront && referenceStorefrontRoot(cwd) === null) {
410
+ refusals.add('`endora new storefront` copies the reference storefront out of a checkout of the ' +
411
+ `platform repository, and there is none above ${cwd}. Run this from inside a ` +
412
+ 'checkout to get both trees, or pass `--no-storefront` to write the instance alone ' +
413
+ '— the storefront can be added later with `endora new storefront <dir>`.');
414
+ }
415
+ const admin = {
416
+ email: options.adminEmail,
417
+ password: options.adminPassword,
418
+ firstName: options.adminFirstName,
419
+ lastName: options.adminLastName,
420
+ };
421
+ const missingAdmin = [
422
+ ['--admin-email', admin.email],
423
+ ['--admin-password', admin.password],
424
+ ['--admin-first-name', admin.firstName],
425
+ ['--admin-last-name', admin.lastName],
426
+ ].filter(([, value]) => value === undefined || value.trim().length === 0);
427
+ if (missingAdmin.length > 0) {
428
+ // FR-159 — a generated password is not permitted: `generable` requires that
429
+ // two correct values are interchangeable, and a password the operator has
430
+ // to remember is not one of those.
431
+ refusals.add(`the administrator is missing ${missingAdmin.map(([flag]) => flag).join(', ')}. ` +
432
+ 'Nothing else creates one — a freshly migrated instance has no account at all — and ' +
433
+ 'the password is never generated: it is the one value you have to remember.');
434
+ }
435
+ if (options.demo === undefined) {
436
+ // FR-124 — required, by 125 PR-2 option (c) as ruled (D-269).
437
+ refusals.add('say whether to install demo data: `--demo` seeds every installed module\'s example ' +
438
+ 'rows, `--no-demo` seeds none. There is deliberately no default — an instance you ' +
439
+ 'are going to sell from wants none of it and one you are evaluating wants it before ' +
440
+ 'the first screen, and picking for you would be picking wrong for one of them.');
441
+ }
442
+ refusals.throwIfAny();
443
+ // Both are decided: a missing directory was a refusal above.
444
+ const targetDir = namedDir;
445
+ const storefrontDir = namedStorefrontDir;
446
+ const runner = runners[0];
447
+ const dryRun = options.dryRun === true;
448
+ const run = options.run ?? spawnStep;
449
+ // ── provision, only where nothing of ours resolves (D-271) ───────────────
450
+ // Decided before anything is written, like every other input: an index this
451
+ // build cannot read, or a `--registry` that is not one, refuses here.
452
+ const hostStep = planHost({
453
+ cwd,
454
+ targetDir,
455
+ runner,
456
+ registry: options.registry,
457
+ releaseIndexFile: options.releaseIndexFile,
458
+ });
459
+ // ── write ─────────────────────────────────────────────────────────────────
460
+ say(`endora install ${targetDir}${dryRun ? ' — dry run, nothing written' : ''}`);
461
+ const answers = answersLine(provenance);
462
+ say(` ${answers}`);
463
+ if (hostStep !== null) {
464
+ const keptLine = ` the temporary host is kept at ${hostStep.cwd} so you can read what its install ` +
465
+ 'left; delete it when you are done.';
466
+ say(`\n[host] ${hostStep.command} # in ${hostStep.cwd}`);
467
+ say(` ${hostStep.purpose}`);
468
+ const code = await run(hostStep);
469
+ if (code !== 0) {
470
+ say(keptLine);
471
+ throw new InstallHostError(`${hostStep.command} failed (exit ${String(code)}) in the temporary host ` +
472
+ `${hostStep.cwd}, so no package of this release could be read and nothing was ` +
473
+ `written to ${targetDir}. The host is kept there so you can read what its install ` +
474
+ 'left. If the registry needs configuration, pass `--registry <url>`; then run this ' +
475
+ 'command again.', code);
476
+ }
477
+ }
478
+ const without = (options.without ?? []).filter((name) => name.trim().length > 0);
479
+ let instance;
480
+ try {
481
+ instance = await runNewInstance({
482
+ dir: targetDir,
483
+ ...(options.modules === undefined ? {} : { modules: options.modules }),
484
+ // R6.3b — nothing at all when every member is wanted, so the common case
485
+ // is the argv a bare `endora new instance <dir>` has.
486
+ ...(without.length === 0 ? {} : { without }),
487
+ ...(options.deployment === undefined ? {} : { deployment: options.deployment }),
488
+ ...(options.registry === undefined ? {} : { registry: options.registry }),
489
+ ...(options.topology === undefined ? {} : { topology: options.topology }),
490
+ // D-270 — with no `--module`, every module of the open-source set this
491
+ // run resolved rather than the smallest set. A policy, not a list: which
492
+ // modules that is stays `new instance`'s to derive (FR-143).
493
+ moduleSeed: 'available',
494
+ dryRun,
495
+ // The host, when there is one, is only a place to look: every resolution
496
+ // is `new instance`'s, unchanged (D-271 clause 1.1).
497
+ cwd: hostStep?.cwd ?? cwd,
498
+ });
499
+ }
500
+ catch (error) {
501
+ if (hostStep !== null) {
502
+ say(` the temporary host is kept at ${hostStep.cwd} so you can read what it holds; ` +
503
+ 'delete it when you are done.');
504
+ }
505
+ throw error;
506
+ }
507
+ if (hostStep !== null) {
508
+ rmSync(hostStep.cwd, { recursive: true, force: true });
509
+ say(` removed the temporary host at ${hostStep.cwd}` +
510
+ (dryRun ? ` — it was provisioned to read the module set; nothing was written to ${targetDir}` : ''));
511
+ }
512
+ say(` ${dryRun ? 'would write' : 'wrote'} ${String(instance.plan.files.length)} files across ` +
513
+ `${instance.plan.members.join(', ')} — ${instance.modules.ids.length} module(s)`);
514
+ say(` ${instance.provenance}`);
515
+ // FR-153 — generated **once**, for two trees. See `revalidateSecret` above.
516
+ const secret = options.revalidateSecret ?? (dryRun ? '<generated on a real run>' : generateSecret());
517
+ let storefront = null;
518
+ if (storefrontDir !== null) {
519
+ storefront = await runNewStorefront({
520
+ dir: storefrontDir,
521
+ cwd,
522
+ dryRun,
523
+ nonInteractive: true,
524
+ inputs: storefrontInputs(instance, secret),
525
+ });
526
+ say(` ${dryRun ? 'would write' : 'wrote'} the storefront at ${storefrontDir} — ` +
527
+ `${String(storefront.plan.files.length)} files`);
528
+ say(` ${storefront.provenance}`);
529
+ }
530
+ const derived = dryRun || !wantsServices ? [] : deriveEnvironment(targetDir);
531
+ if (derived.length > 0) {
532
+ say(` derived from ${DEV_COMPOSE_PATH} into .env: ${derived.join(', ')} — the addresses of ` +
533
+ 'the containers the next step starts, on this machine');
534
+ }
535
+ if (!dryRun && storefrontDir !== null && options.revalidateSecret === undefined) {
536
+ writeSharedSecret(targetDir, secret);
537
+ say(' generated REVALIDATE_SECRET once and wrote it into both trees — the one value no ' +
538
+ 'sequence of `endora new instance` and `endora new storefront` can agree on');
539
+ }
540
+ // ── run ───────────────────────────────────────────────────────────────────
541
+ const steps = plan({
542
+ targetDir,
543
+ storefrontDir,
544
+ runner,
545
+ services: wantsServices,
546
+ demo: options.demo === true,
547
+ admin,
548
+ });
549
+ let exitCode = 0;
550
+ const done = [];
551
+ if (!dryRun) {
552
+ for (const step of steps) {
553
+ say(`\n[${String(done.length + 1)}/${String(steps.length)}] ${step.command}`);
554
+ say(` ${step.purpose}`);
555
+ const code = await run(step);
556
+ done.push(step);
557
+ if (code === 0)
558
+ continue;
559
+ if (step.optional === true) {
560
+ say(` that failed (exit ${String(code)}) and the install is complete without it. ` +
561
+ `Retry with \`${step.command}\`, and \`pnpm run cli demo reset\` withdraws it again.`);
562
+ continue;
563
+ }
564
+ exitCode = code;
565
+ say(`\n${step.command} failed (exit ${String(code)}). Remaining steps, in order:`);
566
+ for (const left of steps.slice(done.length))
567
+ say(` ${left.command} # ${left.purpose}`);
568
+ break;
569
+ }
570
+ }
571
+ else {
572
+ say('\nThe pipeline it would run, in order:');
573
+ for (const step of steps)
574
+ say(` ${step.command} # ${step.purpose}`);
575
+ }
576
+ if (exitCode === 0) {
577
+ for (const line of closing({
578
+ targetDir,
579
+ storefrontDir,
580
+ admin,
581
+ instance,
582
+ dryRun,
583
+ demo: options.demo === true,
584
+ recommended: provenance.recommended,
585
+ services: wantsServices,
586
+ passwordFromFlag: provenance.fromFlags.has('admin-password'),
587
+ })) {
588
+ say(line);
589
+ }
590
+ }
591
+ return {
592
+ targetDir,
593
+ storefrontDir,
594
+ instance,
595
+ storefront,
596
+ steps,
597
+ hostStep,
598
+ output,
599
+ exitCode,
600
+ derived,
601
+ answers,
602
+ dryRun,
603
+ };
604
+ }
605
+ /**
606
+ * The temporary host step, or `null` when the platform already resolves.
607
+ *
608
+ * D-271: provisioned **exactly** when `@endora-commerce/platform` does not
609
+ * resolve beside the target or the working directory — a checkout, the
610
+ * acceptance harness and a client who installed the packages beside all plan
611
+ * none, and run exactly as before. Its files are written here, before the
612
+ * header is printed, because a release index this build cannot read is an input
613
+ * the run could not read and is refused before anything is started.
614
+ */
615
+ function planHost(input) {
616
+ const { scope, resolves } = probePlatform({ cwd: input.cwd, targetDir: input.targetDir });
617
+ if (resolves)
618
+ return null;
619
+ const file = input.releaseIndexFile ?? ownReleaseIndexPath();
620
+ const read = readReleaseIndex(file);
621
+ if ('problem' in read) {
622
+ throw new InstallHostError(`${scope}platform does not resolve from ${input.targetDir} or ${input.cwd}, and this ` +
623
+ `build's release index — the packages it would install to read the module set from — ` +
624
+ `could not be read: ${read.problem}. Nothing was written. Reinstall ` +
625
+ `\`${scope}cli\`, or install the platform beside the target directory and run this ` +
626
+ 'command again.');
627
+ }
628
+ const npmrc = hostNpmrc(input.registry, scope);
629
+ const dir = mkdtempSync(join(tmpdir(), HOST_DIRECTORY_PREFIX));
630
+ writeHost(dir, read.index, npmrc);
631
+ const argv = ['install'];
632
+ return {
633
+ id: 'host',
634
+ command: `${input.runner.label} ${argv.join(' ')}`,
635
+ purpose: `nothing of ours is installed beside ${input.targetDir}, so every package of this ` +
636
+ `release (${String(read.index.packages.length)}) is installed into a temporary host to ` +
637
+ 'read the module manifests from. It is removed when the instance is written.',
638
+ bin: input.runner.command,
639
+ argv: [...input.runner.prefix, ...argv],
640
+ cwd: dir,
641
+ };
642
+ }
643
+ /**
644
+ * The shared secret, into the instance's own `.env`.
645
+ *
646
+ * The storefront gets it as a resolved input — it declares the variable, so its
647
+ * own resolution writes it. The instance's `.env` lists it as a placeholder
648
+ * (the platform declares it `generable: false`, so `new instance` writes no
649
+ * value), and this is where the second half of the pair is filled in.
650
+ */
651
+ function writeSharedSecret(targetDir, secret) {
652
+ const envPath = join(targetDir, '.env');
653
+ if (!existsSync(envPath))
654
+ return;
655
+ const text = readFileSync(envPath, 'utf8');
656
+ if (!/^\s*#?\s*REVALIDATE_SECRET\s*=/m.test(text))
657
+ return;
658
+ if (parseEnvFile(text).has('REVALIDATE_SECRET'))
659
+ return;
660
+ writeFileSync(envPath, writeEnvFile(text, new Map([['REVALIDATE_SECRET', secret]])), 'utf8');
661
+ }
662
+ /**
663
+ * The pipeline (FR-155).
664
+ *
665
+ * It is the printed next-steps sequence and nothing else, and it runs the
666
+ * instance's **own root scripts** rather than their contents: `setup` is the
667
+ * composite the block names, so a client reproducing this by hand types the
668
+ * same four words. A step this list has that the block does not would make the
669
+ * two paths two products.
670
+ */
671
+ function plan(input) {
672
+ const { runner } = input;
673
+ const step = (id, cwd, argv, purpose, optional = false) => ({
674
+ id,
675
+ command: `${runner.label} ${argv.join(' ')}`.trim(),
676
+ purpose,
677
+ bin: runner.command,
678
+ argv: [...runner.prefix, ...argv],
679
+ cwd,
680
+ ...(optional ? { optional: true } : {}),
681
+ });
682
+ const steps = [
683
+ step('install', input.targetDir, ['install'], 'the platform, the admin shell and every module you declared, from the registry'),
684
+ ];
685
+ if (input.services) {
686
+ steps.push(step('services', input.targetDir, ['run', 'dev:services'], 'PostgreSQL, Redis, Meilisearch and a mail catcher, waited for until each is healthy'));
687
+ }
688
+ steps.push(step('setup', input.targetDir, ['run', 'setup'], 'generate, build, migrate and install every module — the four steps, in order'), step('admin', input.targetDir, [
689
+ 'run',
690
+ 'admin:create',
691
+ '--',
692
+ `--email=${input.admin.email ?? ''}`,
693
+ `--password=${input.admin.password ?? ''}`,
694
+ `--first-name=${input.admin.firstName ?? ''}`,
695
+ `--last-name=${input.admin.lastName ?? ''}`,
696
+ ], 'the administrator you sign in as. Nothing else creates one'));
697
+ if (input.demo) {
698
+ // FR-126 — after `module:install --all` and after the administrator, and
699
+ // non-fatal. The reverse ordering would make an optional extra able to red
700
+ // a mandatory outcome.
701
+ steps.push(step('demo', input.targetDir, ['run', 'cli', 'demo', 'seed'], "every installed module's example rows, withdrawable with `cli demo reset`", true));
702
+ }
703
+ if (input.storefrontDir !== null) {
704
+ steps.push(step('storefront-install', input.storefrontDir, ['install'], 'the storefront\'s own dependencies, in its own repository'));
705
+ }
706
+ return steps;
707
+ }
708
+ /** What a client reads when it worked: where things are, and what is next. */
709
+ function closing(input) {
710
+ const lines = ['', input.dryRun ? 'It would then be yours to start:' : 'Done. To start it:'];
711
+ // One command first (`specs/136-open-source-publication/` GAP-7, FR-060):
712
+ // the supervisor over the per-layer commands below, which stay printed for
713
+ // the operator who wants a layer on its own. A storefront somewhere other
714
+ // than the default sibling is named, since `dev:all` looks for the default.
715
+ const defaultStorefront = `${input.targetDir}-storefront`;
716
+ const devAllArgs = input.storefrontDir !== null && input.storefrontDir !== defaultStorefront
717
+ ? ` -- --storefront-dir ${input.storefrontDir}`
718
+ : '';
719
+ const rootManifest = input.instance.plan.files.find((file) => file.path === 'package.json');
720
+ const rootScripts = rootManifest === undefined
721
+ ? {}
722
+ : (JSON.parse(rootManifest.content).scripts ?? {});
723
+ if (typeof rootScripts['dev:all'] === 'string') {
724
+ lines.push(` cd ${input.targetDir} && pnpm run dev:all${devAllArgs} # every layer, one terminal; Ctrl-C stops them`, '', 'Or one layer at a time:');
725
+ }
726
+ lines.push(` cd ${input.targetDir} && pnpm run start # the API, on http://localhost:3001`);
727
+ if (input.instance.plan.members.includes('admin')) {
728
+ lines.push(' pnpm run preview:admin # the admin bundle, in a second terminal');
729
+ }
730
+ if (input.storefrontDir !== null) {
731
+ lines.push(` cd ${input.storefrontDir} && pnpm run build && pnpm run start # the shop`);
732
+ }
733
+ const mail = developmentMailUrl(existsSync(join(input.targetDir, DEV_COMPOSE_PATH))
734
+ ? readFileSync(join(input.targetDir, DEV_COMPOSE_PATH), 'utf8')
735
+ : '');
736
+ if (mail !== undefined) {
737
+ lines.push(` mail this instance sends is caught at ${mail} and leaves your machine never`);
738
+ }
739
+ lines.push('', `Sign in as ${input.admin.email ?? ''} with the password you ` +
740
+ `${input.passwordFromFlag ? 'passed on the command line' : 'entered above'}.`, input.demo
741
+ ? `Demo data ${input.dryRun ? 'would be' : 'was'} seeded. \`pnpm run cli demo reset\` withdraws it ` +
742
+ 'and leaves your own rows alone.'
743
+ : `No demo data ${input.dryRun ? 'would be' : 'was'} seeded. \`pnpm run cli demo seed\` adds a ` +
744
+ "shop's worth of it, and `demo reset` withdraws it again.");
745
+ // D-270 — the module set is not a question and not an input, so it is not in
746
+ // the `[answers]` or `[inputs]` line; it is a derived value, and the run says
747
+ // what it derived and how to take any of it back. Off is non-destructive
748
+ // (Constitution XVII), which is what makes "everything" the safe default.
749
+ if (input.instance.modules.defaulted) {
750
+ const count = input.instance.modules.ids.length;
751
+ lines.push('', `${String(count)} module${count === 1 ? '' : 's'} ${input.dryRun ? 'would be' : 'were'} ` +
752
+ 'installed, because no `--module` was given: every module of the open-source set ' +
753
+ 'this run found, each switched on.', 'Switch any of them off in the admin under Modules (/platform/modules). Off keeps its ' +
754
+ 'data, and switching it back on brings it back.');
755
+ }
756
+ // R2.5f (iv) — every answer taken as a recommendation, said to be one, with
757
+ // what reverses it. A recommendation nobody is told they accepted is a
758
+ // default with better manners.
759
+ const reversals = {
760
+ directory: `the directory ${input.targetDir} — pass another as \`endora install <dir>\``,
761
+ parts: 'every part this build can write — `--without <member>` and `--no-storefront` leave one ' +
762
+ 'out of the next install',
763
+ services: input.services
764
+ ? 'the development services, started — `pnpm run dev:services:down` stops them'
765
+ : null,
766
+ demo: null,
767
+ 'admin-email': null,
768
+ 'admin-password': null,
769
+ 'admin-name': null,
770
+ };
771
+ const taken = input.recommended
772
+ .map((id) => reversals[id])
773
+ .filter((line) => line !== null);
774
+ if (taken.length > 0) {
775
+ lines.push('', 'Recommended, and taken because nothing said otherwise:');
776
+ for (const line of taken)
777
+ lines.push(` ${line}`);
778
+ }
779
+ return lines;
780
+ }
781
+ /** The default runner: spawn it, inherit the descriptors, answer its code. */
782
+ function spawnStep(step) {
783
+ return new Promise((resolveCode) => {
784
+ const child = spawn(step.bin, [...step.argv], {
785
+ cwd: step.cwd,
786
+ stdio: 'inherit',
787
+ env: process.env,
788
+ });
789
+ child.on('error', () => resolveCode(127));
790
+ child.on('close', (code) => resolveCode(code ?? 1));
791
+ });
792
+ }
793
+ //# sourceMappingURL=index.js.map