@topogram/cli 0.3.63 → 0.3.65

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 (344) hide show
  1. package/package.json +1 -1
  2. package/src/adoption/plan/index.js +703 -0
  3. package/src/adoption/plan.d.ts +6 -0
  4. package/src/adoption/plan.js +12 -703
  5. package/src/adoption/reporting.d.ts +10 -0
  6. package/src/adoption/review-groups.d.ts +6 -0
  7. package/src/agent-brief.d.ts +3 -0
  8. package/src/agent-brief.js +495 -0
  9. package/src/agent-ops/query-builders/auth.js +375 -0
  10. package/src/agent-ops/query-builders/change-risk/change-plan.js +123 -0
  11. package/src/agent-ops/query-builders/change-risk/import-plan.js +49 -0
  12. package/src/agent-ops/query-builders/change-risk/maintained.js +286 -0
  13. package/src/agent-ops/query-builders/change-risk/review-packets.js +123 -0
  14. package/src/agent-ops/query-builders/change-risk/risk.js +189 -0
  15. package/src/agent-ops/query-builders/change-risk.js +25 -0
  16. package/src/agent-ops/query-builders/common.js +149 -0
  17. package/src/agent-ops/query-builders/maintained-risk.js +539 -0
  18. package/src/agent-ops/query-builders/maintained-shared.js +120 -0
  19. package/src/agent-ops/query-builders/multi-agent.js +547 -0
  20. package/src/agent-ops/query-builders/projection-impacts.js +514 -0
  21. package/src/agent-ops/query-builders/work-packets.js +417 -0
  22. package/src/agent-ops/query-builders/workflow-context-shared.js +300 -0
  23. package/src/agent-ops/query-builders/workflow-context.js +398 -0
  24. package/src/agent-ops/query-builders/workflow-presets-core.js +676 -0
  25. package/src/agent-ops/query-builders/workflow-presets.js +341 -0
  26. package/src/agent-ops/query-builders.d.ts +26 -0
  27. package/src/agent-ops/query-builders.js +42 -5021
  28. package/src/archive/archive.d.ts +2 -0
  29. package/src/archive/compact.d.ts +1 -0
  30. package/src/archive/unarchive.d.ts +1 -0
  31. package/src/catalog/constants.js +10 -0
  32. package/src/catalog/copy.js +60 -0
  33. package/src/catalog/diagnostics.js +15 -0
  34. package/src/catalog/entries.js +42 -0
  35. package/src/catalog/files.js +67 -0
  36. package/src/catalog/provenance.js +122 -0
  37. package/src/catalog/source.js +150 -0
  38. package/src/catalog/validation.js +252 -0
  39. package/src/catalog.d.ts +12 -0
  40. package/src/catalog.js +18 -750
  41. package/src/cli/catalog-alias.d.ts +1 -0
  42. package/src/cli/command-parser.js +38 -0
  43. package/src/cli/command-parsers/core.js +102 -0
  44. package/src/cli/command-parsers/generator.js +39 -0
  45. package/src/cli/command-parsers/import.js +44 -0
  46. package/src/cli/command-parsers/legacy-workflow.js +21 -0
  47. package/src/cli/command-parsers/project.js +47 -0
  48. package/src/cli/command-parsers/sdlc.js +47 -0
  49. package/src/cli/command-parsers/shared.js +51 -0
  50. package/src/cli/command-parsers/template.js +48 -0
  51. package/src/cli/commands/agent.js +47 -0
  52. package/src/cli/commands/catalog/check.js +31 -0
  53. package/src/cli/commands/catalog/copy.js +59 -0
  54. package/src/cli/commands/catalog/doctor.js +248 -0
  55. package/src/cli/commands/catalog/help.js +21 -0
  56. package/src/cli/commands/catalog/list.js +52 -0
  57. package/src/cli/commands/catalog/runner.js +92 -0
  58. package/src/cli/commands/catalog/shared.js +17 -0
  59. package/src/cli/commands/catalog/show.js +134 -0
  60. package/src/cli/commands/catalog.js +32 -0
  61. package/src/cli/commands/check.js +268 -0
  62. package/src/cli/commands/doctor.js +268 -0
  63. package/src/cli/commands/emit.js +149 -0
  64. package/src/cli/commands/generate.js +96 -0
  65. package/src/cli/commands/generator-policy/package-info.js +162 -0
  66. package/src/cli/commands/generator-policy/payloads.js +372 -0
  67. package/src/cli/commands/generator-policy/printers.js +159 -0
  68. package/src/cli/commands/generator-policy/runner.js +81 -0
  69. package/src/cli/commands/generator-policy/shared.js +39 -0
  70. package/src/cli/commands/generator-policy.js +17 -0
  71. package/src/cli/commands/generator.js +443 -0
  72. package/src/cli/commands/import/adopt.js +170 -0
  73. package/src/cli/commands/import/check.js +91 -0
  74. package/src/cli/commands/import/diff.js +84 -0
  75. package/src/cli/commands/import/help.js +47 -0
  76. package/src/cli/commands/import/paths.js +277 -0
  77. package/src/cli/commands/import/plan.js +284 -0
  78. package/src/cli/commands/import/refresh.js +470 -0
  79. package/src/cli/commands/import/status-history.js +196 -0
  80. package/src/cli/commands/import/workspace.js +230 -0
  81. package/src/cli/commands/import-runner.js +157 -0
  82. package/src/cli/commands/import.js +35 -0
  83. package/src/cli/commands/inspect.js +55 -0
  84. package/src/cli/commands/new.js +94 -0
  85. package/src/cli/commands/package/constants.js +17 -0
  86. package/src/cli/commands/package/doctor.js +240 -0
  87. package/src/cli/commands/package/help.js +27 -0
  88. package/src/cli/commands/package/lockfile.js +135 -0
  89. package/src/cli/commands/package/npm.js +97 -0
  90. package/src/cli/commands/package/reporting.js +35 -0
  91. package/src/cli/commands/package/runner.js +33 -0
  92. package/src/cli/commands/package/shared.js +9 -0
  93. package/src/cli/commands/package/update-cli.js +252 -0
  94. package/src/cli/commands/package/versions.js +35 -0
  95. package/src/cli/commands/package.js +31 -0
  96. package/src/cli/commands/query/change-plan.js +68 -0
  97. package/src/cli/commands/query/definitions.js +202 -0
  98. package/src/cli/commands/query/import-adopt.js +121 -0
  99. package/src/cli/commands/query/runner/artifacts.js +102 -0
  100. package/src/cli/commands/query/runner/boundaries.js +211 -0
  101. package/src/cli/commands/query/runner/change.js +182 -0
  102. package/src/cli/commands/query/runner/import-adopt.js +111 -0
  103. package/src/cli/commands/query/runner/index.js +31 -0
  104. package/src/cli/commands/query/runner/output.js +12 -0
  105. package/src/cli/commands/query/runner/workflow.js +241 -0
  106. package/src/cli/commands/query/runner.js +3 -0
  107. package/src/cli/commands/query/workflow-context.js +5 -0
  108. package/src/cli/commands/query/workspace.js +274 -0
  109. package/src/cli/commands/query.js +11 -0
  110. package/src/cli/commands/release-rollout.js +257 -0
  111. package/src/cli/commands/release-shared.js +528 -0
  112. package/src/cli/commands/release-status.js +429 -0
  113. package/src/cli/commands/release.js +107 -0
  114. package/src/cli/commands/sdlc.js +168 -0
  115. package/src/cli/commands/setup.js +76 -0
  116. package/src/cli/commands/source.js +291 -0
  117. package/src/cli/commands/template/baseline.js +100 -0
  118. package/src/cli/commands/template/check.js +466 -0
  119. package/src/cli/commands/template/constants.js +8 -0
  120. package/src/cli/commands/template/diagnostics.js +26 -0
  121. package/src/cli/commands/template/help.js +28 -0
  122. package/src/cli/commands/template/lifecycle.js +404 -0
  123. package/src/cli/commands/template/list-show.js +287 -0
  124. package/src/cli/commands/template/policy.js +422 -0
  125. package/src/cli/commands/template/shared.js +127 -0
  126. package/src/cli/commands/template/updates.js +352 -0
  127. package/src/cli/commands/template-runner.js +198 -0
  128. package/src/cli/commands/template.js +43 -0
  129. package/src/cli/commands/trust.js +219 -0
  130. package/src/cli/commands/version.js +40 -0
  131. package/src/cli/commands/widget.js +168 -0
  132. package/src/cli/commands/workflow.js +63 -0
  133. package/src/cli/dispatcher.js +392 -0
  134. package/src/cli/help-dispatch.js +188 -0
  135. package/src/cli/help.js +296 -0
  136. package/src/cli/migration-guidance.js +59 -0
  137. package/src/cli/options.js +96 -0
  138. package/src/cli/output-safety.js +107 -0
  139. package/src/cli/path-normalization.js +29 -0
  140. package/src/cli.js +47 -11711
  141. package/src/example-implementation.d.ts +2 -0
  142. package/src/format.d.ts +1 -0
  143. package/src/generator/api/contracts.js +497 -0
  144. package/src/generator/api/metadata.js +221 -0
  145. package/src/generator/api/openapi.js +559 -0
  146. package/src/generator/api/schema.js +124 -0
  147. package/src/generator/api/types.d.ts +98 -0
  148. package/src/generator/api.js +3 -1195
  149. package/src/generator/check.d.ts +1 -0
  150. package/src/generator/context/bundle.d.ts +1 -0
  151. package/src/generator/context/shared/domain-sdlc.js +282 -0
  152. package/src/generator/context/shared/maintained-boundary.js +665 -0
  153. package/src/generator/context/shared/metrics.js +85 -0
  154. package/src/generator/context/shared/primitives.js +64 -0
  155. package/src/generator/context/shared/relationships.js +453 -0
  156. package/src/generator/context/shared/summaries.js +263 -0
  157. package/src/generator/context/shared/types.d.ts +207 -0
  158. package/src/generator/context/shared.d.ts +44 -0
  159. package/src/generator/context/shared.js +80 -1390
  160. package/src/generator/context/slice/core.js +397 -0
  161. package/src/generator/context/slice/sdlc.js +417 -0
  162. package/src/generator/context/slice/ui-packets.js +183 -0
  163. package/src/generator/context/slice.js +2 -859
  164. package/src/generator/native/parity-bundle.js +2 -1
  165. package/src/generator/registry/index.js +507 -0
  166. package/src/generator/registry.js +18 -504
  167. package/src/generator/runtime/environment/index.js +666 -0
  168. package/src/generator/runtime/environment.js +4 -666
  169. package/src/generator/runtime/runtime-check/index.js +554 -0
  170. package/src/generator/runtime/runtime-check.js +4 -554
  171. package/src/generator/runtime/shared/index.js +572 -0
  172. package/src/generator/runtime/shared.js +19 -570
  173. package/src/generator/shared.d.ts +2 -0
  174. package/src/generator/surfaces/shared.d.ts +3 -0
  175. package/src/generator/surfaces/web/html-escape.js +22 -0
  176. package/src/generator/surfaces/web/react.js +10 -8
  177. package/src/generator/surfaces/web/sveltekit.js +7 -5
  178. package/src/generator/surfaces/web/vanilla.js +8 -4
  179. package/src/generator/widget-conformance/behavior-report.js +258 -0
  180. package/src/generator/widget-conformance/checks.js +371 -0
  181. package/src/generator/widget-conformance/projection-context.js +200 -0
  182. package/src/generator/widget-conformance/report.js +166 -0
  183. package/src/generator/widget-conformance/types.d.ts +121 -0
  184. package/src/generator/widget-conformance.js +3 -824
  185. package/src/generator.d.ts +2 -0
  186. package/src/github-client.js +520 -0
  187. package/src/import/core/context.d.ts +3 -0
  188. package/src/import/core/contracts.d.ts +1 -0
  189. package/src/import/core/registry.d.ts +4 -0
  190. package/src/import/core/runner/candidates.js +217 -0
  191. package/src/import/core/runner/options.js +22 -0
  192. package/src/import/core/runner/reports.js +50 -0
  193. package/src/import/core/runner/run.js +79 -0
  194. package/src/import/core/runner/tracks.js +150 -0
  195. package/src/import/core/runner/ui-drafts.js +337 -0
  196. package/src/import/core/runner.js +3 -698
  197. package/src/import/core/shared/api-routes.js +221 -0
  198. package/src/import/core/shared/candidates.js +97 -0
  199. package/src/import/core/shared/files.js +177 -0
  200. package/src/import/core/shared/next-app.js +389 -0
  201. package/src/import/core/shared/types.d.ts +51 -0
  202. package/src/import/core/shared/ui-routes.js +230 -0
  203. package/src/import/core/shared.js +67 -910
  204. package/src/import/extractors/api/flutter-dio.js +4 -8
  205. package/src/import/extractors/api/react-native-repository.js +4 -8
  206. package/src/import/index.d.ts +4 -0
  207. package/src/import/provenance.d.ts +4 -0
  208. package/src/new-project/constants.js +128 -0
  209. package/src/new-project/create.js +83 -0
  210. package/src/new-project/json.js +28 -0
  211. package/src/new-project/metadata.js +96 -0
  212. package/src/new-project/package-spec.js +161 -0
  213. package/src/new-project/project-files.js +348 -0
  214. package/src/new-project/template-policy.js +269 -0
  215. package/src/new-project/template-resolution.js +368 -0
  216. package/src/new-project/template-snapshots.js +430 -0
  217. package/src/new-project/template-updates.js +512 -0
  218. package/src/new-project/types.d.ts +83 -0
  219. package/src/new-project.js +6 -2188
  220. package/src/npm-safety.js +79 -0
  221. package/src/parser.d.ts +87 -0
  222. package/src/parser.js +118 -0
  223. package/src/path-helpers.d.ts +1 -0
  224. package/src/path-helpers.js +20 -0
  225. package/src/policy/review-boundaries.d.ts +15 -0
  226. package/src/project-config/index.js +564 -0
  227. package/src/project-config.js +19 -560
  228. package/src/reconcile/docs.d.ts +8 -0
  229. package/src/reconcile/journeys.d.ts +1 -0
  230. package/src/resolver/enrich/acceptance-criterion.js +2 -0
  231. package/src/resolver/enrich/bug.js +2 -0
  232. package/src/resolver/enrich/pitch.js +2 -0
  233. package/src/resolver/enrich/requirement.js +2 -0
  234. package/src/resolver/enrich/task.js +2 -0
  235. package/src/resolver/index.js +19 -2089
  236. package/src/resolver/normalize.js +384 -1
  237. package/src/resolver/plans.js +168 -0
  238. package/src/resolver/projections-api.js +494 -0
  239. package/src/resolver/projections-db.js +133 -0
  240. package/src/resolver/projections-ui.js +317 -0
  241. package/src/resolver/shapes.js +251 -0
  242. package/src/resolver/shared.js +278 -0
  243. package/src/resolver/widgets.js +132 -0
  244. package/src/resolver.d.ts +1 -0
  245. package/src/runtime-support.js +29 -0
  246. package/src/sdlc/adopt.d.ts +1 -0
  247. package/src/sdlc/check.d.ts +1 -0
  248. package/src/sdlc/explain.d.ts +1 -0
  249. package/src/sdlc/release.d.ts +1 -0
  250. package/src/sdlc/scaffold.d.ts +1 -0
  251. package/src/sdlc/transition.d.ts +1 -0
  252. package/src/template-trust/constants.js +62 -0
  253. package/src/template-trust/content.js +258 -0
  254. package/src/template-trust/diff.js +92 -0
  255. package/src/template-trust/policy.js +61 -0
  256. package/src/template-trust/record.js +90 -0
  257. package/src/template-trust/status.js +182 -0
  258. package/src/template-trust.js +24 -687
  259. package/src/text-helpers.d.ts +7 -0
  260. package/src/text-helpers.js +245 -0
  261. package/src/topogram-config.js +306 -0
  262. package/src/topogram-types.d.ts +69 -0
  263. package/src/validator/common.js +488 -0
  264. package/src/validator/data-model.js +237 -0
  265. package/src/validator/docs.js +167 -0
  266. package/src/validator/expressions.js +146 -1
  267. package/src/validator/index.d.ts +23 -0
  268. package/src/validator/index.js +32 -3585
  269. package/src/validator/kinds.d.ts +41 -0
  270. package/src/validator/kinds.js +2 -0
  271. package/src/validator/model-helpers.js +46 -0
  272. package/src/validator/per-kind/acceptance-criterion.js +5 -0
  273. package/src/validator/per-kind/bug.js +6 -0
  274. package/src/validator/per-kind/domain.js +15 -2
  275. package/src/validator/per-kind/pitch.js +7 -0
  276. package/src/validator/per-kind/requirement.js +5 -0
  277. package/src/validator/per-kind/task.js +7 -0
  278. package/src/validator/per-kind/widget.js +14 -0
  279. package/src/validator/projections/api-http-async.js +410 -0
  280. package/src/validator/projections/api-http-authz.js +88 -0
  281. package/src/validator/projections/api-http-core.js +205 -0
  282. package/src/validator/projections/api-http-policies.js +339 -0
  283. package/src/validator/projections/api-http-responses.js +233 -0
  284. package/src/validator/projections/api-http.js +44 -0
  285. package/src/validator/projections/db.js +353 -0
  286. package/src/validator/projections/generator-defaults.js +45 -0
  287. package/src/validator/projections/helpers.js +87 -0
  288. package/src/validator/projections/ui-helpers.js +214 -0
  289. package/src/validator/projections/ui-navigation.js +344 -0
  290. package/src/validator/projections/ui-structure.js +364 -0
  291. package/src/validator/projections/ui-widgets.js +493 -0
  292. package/src/validator/projections/ui.js +46 -0
  293. package/src/validator/registry.js +48 -1
  294. package/src/validator/utils.d.ts +20 -0
  295. package/src/validator/utils.js +115 -12
  296. package/src/validator.d.ts +2 -0
  297. package/src/widget-behavior.d.ts +1 -0
  298. package/src/workflows/adoption/index.js +26 -0
  299. package/src/workflows/docs-generate.js +262 -0
  300. package/src/workflows/docs-scan.js +703 -0
  301. package/src/workflows/docs.js +15 -0
  302. package/src/workflows/import-app/api/collect.js +221 -0
  303. package/src/workflows/import-app/api/openapi.js +257 -0
  304. package/src/workflows/import-app/api/routes.js +327 -0
  305. package/src/workflows/import-app/api/sources.js +22 -0
  306. package/src/workflows/import-app/api.js +4 -0
  307. package/src/workflows/import-app/db.js +538 -0
  308. package/src/workflows/import-app/index.js +30 -0
  309. package/src/workflows/import-app/shared.js +218 -0
  310. package/src/workflows/import-app/ui.js +443 -0
  311. package/src/workflows/import-app/workflow.js +159 -0
  312. package/src/workflows/reconcile/adoption-plan/build.js +208 -0
  313. package/src/workflows/reconcile/adoption-plan/dependencies.js +75 -0
  314. package/src/workflows/reconcile/adoption-plan/outputs.js +143 -0
  315. package/src/workflows/reconcile/adoption-plan/paths.js +58 -0
  316. package/src/workflows/reconcile/adoption-plan/projection-patches.js +177 -0
  317. package/src/workflows/reconcile/adoption-plan/reasons.js +107 -0
  318. package/src/workflows/reconcile/adoption-plan.js +32 -0
  319. package/src/workflows/reconcile/auth/closures.js +115 -0
  320. package/src/workflows/reconcile/auth/formatters.js +142 -0
  321. package/src/workflows/reconcile/auth/inference.js +330 -0
  322. package/src/workflows/reconcile/auth/roles.js +122 -0
  323. package/src/workflows/reconcile/auth.js +37 -0
  324. package/src/workflows/reconcile/bundle-core/index.js +600 -0
  325. package/src/workflows/reconcile/bundle-core.js +14 -0
  326. package/src/workflows/reconcile/bundle-shared.js +75 -0
  327. package/src/workflows/reconcile/candidate-model.js +477 -0
  328. package/src/workflows/reconcile/canonical-surface.js +264 -0
  329. package/src/workflows/reconcile/gap-report.js +333 -0
  330. package/src/workflows/reconcile/ids.js +6 -0
  331. package/src/workflows/reconcile/impacts/adoption-plan.js +192 -0
  332. package/src/workflows/reconcile/impacts/indexes.js +101 -0
  333. package/src/workflows/reconcile/impacts/patches.js +252 -0
  334. package/src/workflows/reconcile/impacts/reports.js +80 -0
  335. package/src/workflows/reconcile/impacts.js +16 -0
  336. package/src/workflows/reconcile/index.js +7 -0
  337. package/src/workflows/reconcile/renderers.js +461 -0
  338. package/src/workflows/reconcile/summary.js +90 -0
  339. package/src/workflows/reconcile/workflow.js +309 -0
  340. package/src/workflows/shared.js +189 -0
  341. package/src/workflows/types.d.ts +93 -0
  342. package/src/workflows.d.ts +1 -0
  343. package/src/workflows.js +10 -7652
  344. package/src/workspace-docs.d.ts +29 -0
@@ -0,0 +1,666 @@
1
+ import { generateDbLifecyclePlan } from "../../surfaces/databases/lifecycle-shared.js";
2
+ import { getExampleImplementation } from "../../../example-implementation.js";
3
+ import {
4
+ generateDbBundle,
5
+ generateNativeBundle,
6
+ generateServerBundle,
7
+ generateWebBundle,
8
+ dbEnvVarsForComponent,
9
+ getDefaultEnvironmentProjections,
10
+ resolveRuntimeTopology,
11
+ runtimeDemoUserId,
12
+ runtimePorts,
13
+ runtimeUrls
14
+ } from "../shared.js";
15
+ import { mergeNamedBundles, renderEnvAwareShellScript, renderLoadEnvScript, renderRootShellScript } from "../bundle-shared.js";
16
+ import { generatorProfile as manifestGeneratorProfile } from "../../registry.js";
17
+
18
+ function projectionHintProfile(projection, fallback) {
19
+ if (!projection) {
20
+ return fallback;
21
+ }
22
+ for (const entry of projection.generatorDefaults || []) {
23
+ if (entry.key === "profile" && entry.value != null) {
24
+ return entry.value;
25
+ }
26
+ }
27
+ return fallback;
28
+ }
29
+
30
+ function runtimeReferenceFor(graph, options = {}) {
31
+ try {
32
+ return getExampleImplementation(graph, options).runtime.reference;
33
+ } catch {
34
+ return {
35
+ environment: { name: "Topogram Runtime", databaseName: "topogram_app", envExample: "" },
36
+ ports: { server: 3000, web: 5173 },
37
+ demoEnv: { userId: "11111111-1111-4111-8111-111111111111" }
38
+ };
39
+ }
40
+ }
41
+
42
+ function buildEnvironmentPlan(graph, options = {}) {
43
+ const runtimeReference = runtimeReferenceFor(graph, options);
44
+ const topology = resolveRuntimeTopology(graph, options);
45
+ const { apiProjection, uiProjection, dbProjection } = getDefaultEnvironmentProjections(graph, options);
46
+ const dbLifecycle = dbProjection ? generateDbLifecyclePlan(graph, { ...options, projectionId: dbProjection.id }) : null;
47
+ const dbEngine = dbLifecycle?.engine || null;
48
+ const profile = options.profileId || (dbEngine === "sqlite" || !dbProjection ? "local_process" : "local_docker");
49
+ const usesDocker = profile === "local_docker";
50
+ const webProfile = manifestGeneratorProfile(topology.primaryWeb?.generator?.id, null) || projectionHintProfile(uiProjection, "sveltekit");
51
+ const isSqlite = dbEngine === "sqlite";
52
+ const ports = runtimePorts(runtimeReference, topology);
53
+
54
+ return {
55
+ type: "environment_plan",
56
+ environment: {
57
+ id: [apiProjection?.id, uiProjection?.id, dbProjection?.id].filter(Boolean).join("__") || "topogram_runtime",
58
+ name: runtimeReference.environment.name,
59
+ mode: "local_dev",
60
+ profile
61
+ },
62
+ topology: {
63
+ runtimes: topology.runtimes.map((runtime) => ({
64
+ id: runtime.id,
65
+ kind: runtime.kind,
66
+ projection: runtime.projection.id,
67
+ generator: runtime.generator,
68
+ port: runtime.port ?? null,
69
+ uses_api: runtime.api || null,
70
+ uses_database: runtime.database || null
71
+ }))
72
+ },
73
+ projections: {
74
+ api: {
75
+ id: apiProjection?.id || null,
76
+ type: apiProjection?.type || null
77
+ },
78
+ ui: {
79
+ id: uiProjection?.id || null,
80
+ type: uiProjection?.type || null
81
+ },
82
+ db: {
83
+ id: dbProjection?.id || null,
84
+ type: dbProjection?.type || null,
85
+ engine: dbEngine,
86
+ profile: dbProjection?.generatorDefaults?.profile || dbProjection?.profile || null
87
+ }
88
+ },
89
+ generators: {
90
+ server: "hono-server",
91
+ web: "sveltekit-app",
92
+ dbLifecycle: "db-lifecycle-bundle"
93
+ },
94
+ runtimeProfiles: {
95
+ server: "hono",
96
+ web: webProfile,
97
+ orm: dbLifecycle?.runtimeProfile || null,
98
+ database: dbLifecycle?.dbProfile || null
99
+ },
100
+ orchestration: {
101
+ usesDocker,
102
+ database: !dbProjection
103
+ ? "none"
104
+ : isSqlite
105
+ ? "local_process_sqlite"
106
+ : usesDocker
107
+ ? "docker_postgres"
108
+ : "local_process_postgres"
109
+ },
110
+ directories: {
111
+ server: topology.primaryApi ? topology.serviceDir(topology.primaryApi) : null,
112
+ web: topology.primaryWeb ? topology.webDir(topology.primaryWeb) : null,
113
+ db: topology.primaryDb ? topology.dbDir(topology.primaryDb) : null,
114
+ native: topology.nativeRuntimes[0] ? topology.nativeDir(topology.nativeRuntimes[0]) : null,
115
+ scripts: "scripts"
116
+ },
117
+ runtimes: {
118
+ apis: topology.apiRuntimes.map((component) => ({
119
+ id: component.id,
120
+ projection: component.projection.id,
121
+ port: component.port || ports.server,
122
+ dir: topology.serviceDir(component),
123
+ uses_database: component.database,
124
+ databaseEnv: component.databaseRuntime
125
+ ? dbEnvVarsForComponent(component.databaseRuntime, { primary: component.databaseRuntime?.id === topology.primaryDb?.id })
126
+ : null
127
+ })),
128
+ webs: topology.webRuntimes.map((component) => ({
129
+ id: component.id,
130
+ projection: component.projection.id,
131
+ port: component.port || ports.web,
132
+ dir: topology.webDir(component),
133
+ uses_api: component.api
134
+ })),
135
+ natives: topology.nativeRuntimes.map((component) => ({
136
+ id: component.id,
137
+ projection: component.projection.id,
138
+ dir: topology.nativeDir(component),
139
+ uses_api: component.api
140
+ })),
141
+ databases: topology.dbRuntimes.map((component) => ({
142
+ id: component.id,
143
+ projection: component.projection.id,
144
+ type: component.projection.type,
145
+ engine: component.id === topology.primaryDb?.id ? dbEngine : null,
146
+ port: component.port,
147
+ dir: topology.dbDir(component),
148
+ env: dbEnvVarsForComponent(component, { primary: component.id === topology.primaryDb?.id })
149
+ }))
150
+ },
151
+ ports: {
152
+ database: !dbProjection || isSqlite ? null : topology.primaryDb?.port || 5432,
153
+ server: ports.server,
154
+ web: ports.web
155
+ },
156
+ files: {
157
+ rootEnv: ".env.example",
158
+ dockerCompose: usesDocker ? "docker-compose.yml" : null,
159
+ readme: "README.md",
160
+ packageJson: "package.json"
161
+ },
162
+ commands: {
163
+ bootstrapDb: "./scripts/bootstrap-db.sh",
164
+ dev: "./scripts/stack-dev.sh",
165
+ dockerDb: usesDocker ? "./scripts/docker-db.sh" : null,
166
+ dockerStack: usesDocker ? "./scripts/docker-stack.sh" : null
167
+ },
168
+ runtimeReference
169
+ };
170
+ }
171
+
172
+ function renderEnvironmentEnvExample(plan) {
173
+ const demoUserId = runtimeDemoUserId(plan.runtimeReference);
174
+ const databaseName = plan.runtimeReference.environment.databaseName || "topogram_app";
175
+ const urls = runtimeUrls(plan.runtimeReference, {
176
+ primaryApi: { port: plan.ports.server },
177
+ primaryWeb: { port: plan.ports.web }
178
+ });
179
+ const extraDatabaseLines = plan.runtimes.databases
180
+ .filter((component) => component.id !== plan.runtimes.databases[0]?.id)
181
+ .map((component) => {
182
+ const fallbackName = `${databaseName}_${component.id}`;
183
+ if (component.engine === "sqlite") {
184
+ return `${component.env.databaseUrl}=file:./var/${component.id}.sqlite`;
185
+ }
186
+ return [
187
+ `${component.env.dbPort}=${component.port || 5432}`,
188
+ `${component.env.postgresDb}=${fallbackName}`,
189
+ `${component.env.databaseUrl}=postgresql://\${POSTGRES_USER}@localhost:${component.port || 5432}/${fallbackName}?schema=public`,
190
+ `${component.env.databaseAdminUrl}=postgresql://\${POSTGRES_USER}@localhost:${component.port || 5432}/postgres`
191
+ ].join("\n");
192
+ })
193
+ .filter(Boolean)
194
+ .join("\n");
195
+ const commonLines = `# Environment profile
196
+ TOPOGRAM_ENVIRONMENT_PROFILE=${plan.environment.profile}
197
+
198
+ # Local stack ports
199
+ ${plan.runtimes.apis.length ? `SERVER_PORT=${plan.ports.server}\n` : ""}${plan.runtimes.webs.length ? `WEB_PORT=${plan.ports.web}\n` : ""}${plan.runtimes.webs.length && plan.runtimes.apis.length ? `PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}\n` : ""}${plan.runtimes.webs.length ? `TOPOGRAM_CORS_ORIGINS=${urls.web},http://127.0.0.1:${plan.ports.web}\n` : ""}PUBLIC_TOPOGRAM_DEMO_USER_ID=${demoUserId}
200
+ TOPOGRAM_DEMO_USER_ID=${demoUserId}
201
+ ${plan.runtimeReference.environment.envExample || ""}
202
+ TOPOGRAM_SEED_DEMO=true
203
+ `;
204
+ if (!plan.projections.db.type) {
205
+ return commonLines;
206
+ }
207
+ if (plan.projections.db.engine === "sqlite") {
208
+ return `# Environment profile
209
+ TOPOGRAM_ENVIRONMENT_PROFILE=${plan.environment.profile}
210
+
211
+ # Local stack ports
212
+ SERVER_PORT=${plan.ports.server}
213
+ WEB_PORT=${plan.ports.web}
214
+
215
+ # Local SQLite defaults
216
+ DATABASE_URL=file:./var/${databaseName}.sqlite
217
+ ${extraDatabaseLines ? `${extraDatabaseLines}\n` : ""}PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}
218
+ TOPOGRAM_CORS_ORIGINS=${urls.web},http://127.0.0.1:${plan.ports.web}
219
+ PUBLIC_TOPOGRAM_DEMO_USER_ID=${demoUserId}
220
+ TOPOGRAM_DEMO_USER_ID=${demoUserId}
221
+ ${plan.runtimeReference.environment.envExample || ""}
222
+ TOPOGRAM_SEED_DEMO=true
223
+ `;
224
+ }
225
+
226
+ return `# Environment profile
227
+ TOPOGRAM_ENVIRONMENT_PROFILE=${plan.environment.profile}
228
+
229
+ # Local stack ports
230
+ DB_PORT=${plan.ports.database || 5432}
231
+ SERVER_PORT=${plan.ports.server}
232
+ WEB_PORT=${plan.ports.web}
233
+
234
+ # Local Postgres defaults
235
+ POSTGRES_DB=${databaseName}
236
+ POSTGRES_USER=\${USER:-postgres}
237
+ POSTGRES_PASSWORD=postgres
238
+
239
+ # Local shell/runtime defaults
240
+ DATABASE_URL=postgresql://\${POSTGRES_USER}@localhost:${plan.ports.database || 5432}/${databaseName}?schema=public
241
+ DATABASE_ADMIN_URL=postgresql://\${POSTGRES_USER}@localhost:${plan.ports.database || 5432}/postgres
242
+ ${extraDatabaseLines ? `${extraDatabaseLines}\n` : ""}PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}
243
+ TOPOGRAM_CORS_ORIGINS=${urls.web},http://127.0.0.1:${plan.ports.web}
244
+ PUBLIC_TOPOGRAM_DEMO_USER_ID=${demoUserId}
245
+ TOPOGRAM_DEMO_USER_ID=${demoUserId}
246
+ ${plan.runtimeReference.environment.envExample || ""}
247
+ TOPOGRAM_SEED_DEMO=true
248
+ `;
249
+ }
250
+
251
+ function renderEnvironmentPackageJson(plan) {
252
+ const scripts = {
253
+ "db:bootstrap": "bash ./scripts/bootstrap-db.sh",
254
+ "dev:server": "bash ./scripts/server-dev.sh",
255
+ "dev:web": "bash ./scripts/web-dev.sh",
256
+ dev: "bash ./scripts/stack-dev.sh"
257
+ };
258
+ if (plan.orchestration.usesDocker) {
259
+ scripts["docker:db"] = "bash ./scripts/docker-db.sh";
260
+ scripts["docker:stack"] = "bash ./scripts/docker-stack.sh";
261
+ }
262
+ return `${JSON.stringify({
263
+ name: "topogram-runtime-stack",
264
+ private: true,
265
+ scripts
266
+ }, null, 2)}\n`;
267
+ }
268
+
269
+ function apiDatabaseExportLines(component) {
270
+ if (!component?.databaseEnv) {
271
+ return [];
272
+ }
273
+ const env = component.databaseEnv;
274
+ return [
275
+ `if [[ -n "\${${env.databaseUrl}:-}" ]]; then export DATABASE_URL="\${${env.databaseUrl}}"; fi`,
276
+ `if [[ -n "\${${env.databaseAdminUrl}:-}" ]]; then export DATABASE_ADMIN_URL="\${${env.databaseAdminUrl}}"; fi`
277
+ ];
278
+ }
279
+
280
+ function renderEnvironmentReadme(plan) {
281
+ const hasDb = plan.runtimes.databases.length > 0;
282
+ const hasApi = plan.runtimes.apis.length > 0;
283
+ const hasWeb = plan.runtimes.webs.length > 0;
284
+ const hasNative = plan.runtimes.natives.length > 0;
285
+ const localProcessNotes = !hasDb
286
+ ? "- This bundle has no generated database surface."
287
+ : plan.projections.db.engine === "sqlite"
288
+ ? "- SQLite is file-backed for this bundle; no separate DB server is required."
289
+ : `- Make sure the Postgres server is already running before \`${plan.commands.bootstrapDb}\`.\n- \`DATABASE_URL\` and \`DATABASE_ADMIN_URL\` should point at your local or managed Postgres instance.`;
290
+ const dockerSection = plan.orchestration.usesDocker
291
+ ? `## Alternative Docker Workflow
292
+
293
+ - Start only the database: \`${plan.commands.dockerDb}\`
294
+ - Start the database, server, and web app in containers: \`${plan.commands.dockerStack}\`
295
+ `
296
+ : `## Local Process Notes
297
+
298
+ - Install Node.js and npm locally before using this bundle.
299
+ ${localProcessNotes}
300
+ `;
301
+
302
+ return `# ${plan.environment.name}
303
+
304
+ This bundle packages the generated runtime into one local environment:
305
+
306
+ ${hasApi ? "- `services/<api-id>/`: generated API service scaffolds\n" : ""}${hasWeb ? `- \`web/<web-id>/\`: generated ${plan.runtimeProfiles.web === "react" ? "Vite + React Router" : plan.runtimeProfiles.web === "vanilla" ? "vanilla HTML/CSS/JS" : "SvelteKit"} web scaffolds\n` : ""}${hasNative ? "- `native/<native-id>/`: generated native app scaffolds\n" : ""}${hasDb ? "- `db/<db-id>/`: generated DB lifecycle bundles\n" : ""}${plan.files.dockerCompose ? `- \`${plan.files.dockerCompose}\`: local Postgres container` : hasDb ? (plan.projections.db.engine === "sqlite" ? "- local SQLite file orchestration (no Docker files generated)" : "- local-process Postgres orchestration (no Docker files generated)") : "- no DB orchestration is generated"}
307
+
308
+ ## Quick Start
309
+
310
+ 1. Copy \`.env.example\` to \`.env\` if you want to customize defaults
311
+ 2. Start the database:
312
+ - ${!hasDb ? "not applicable" : plan.projections.db.engine === "sqlite" ? "no separate DB service is required" : plan.orchestration.usesDocker ? `\`${plan.commands.dockerDb}\`` : "use your local Postgres service"}
313
+ 3. Bootstrap or migrate the database:
314
+ - \`${plan.commands.bootstrapDb}\`
315
+ 4. Start the stack:
316
+ - \`${plan.commands.dev}\`
317
+
318
+ ## Demo Seed Data
319
+
320
+ - Bootstrap seeds demo data by default
321
+ - Set \`TOPOGRAM_SEED_DEMO=false\` to skip demo seeding
322
+ - Default seeded IDs come from \`.env.example\`
323
+
324
+ ${dockerSection}
325
+
326
+ ## Notes
327
+
328
+ - ${hasApi && hasDb ? `The generated server expects ${plan.projections.db.engine === "sqlite" ? "SQLite plus Prisma." : "Postgres plus Prisma."}` : hasApi ? "The generated server is stateless." : "No server surface is generated."}
329
+ - ${hasWeb && hasApi ? "The generated web app talks to `PUBLIC_TOPOGRAM_API_BASE_URL`." : hasWeb ? "The generated web app is standalone." : "No web surface is generated."}
330
+ - ${hasNative ? "Native app scaffolds use the same UI surface contracts as web surfaces." : "No native surface is generated."}
331
+ - If \`.env\` is missing, generated scripts fall back to \`.env.example\`.
332
+ - The DB lifecycle scripts remain the source of truth for greenfield bootstrap and brownfield migration.
333
+ `;
334
+ }
335
+
336
+ function renderEnvironmentLoadEnvScript() {
337
+ return renderLoadEnvScript({ searchParentEnv: true });
338
+ }
339
+
340
+ function renderEnvironmentBootstrapDbScript(plan) {
341
+ const dbBootstrapLines = plan.runtimes.databases.map((component) => {
342
+ const env = component.env;
343
+ const runtimeApi = plan.runtimes.apis.find((apiRuntime) => apiRuntime.uses_database === component.id);
344
+ const assignments = [
345
+ `DATABASE_URL="\${${env.databaseUrl}:-}"`,
346
+ `DATABASE_ADMIN_URL="\${${env.databaseAdminUrl}:-}"`,
347
+ runtimeApi ? `TOPOGRAM_RUNTIME_SERVER_DIR="$ROOT_DIR/${runtimeApi.dir}"` : null
348
+ ].filter(Boolean).join(" ");
349
+ return `(cd "$ROOT_DIR/${component.dir}" && TOPOGRAM_ENV_FILE=/dev/null ${assignments} bash ./scripts/db-bootstrap-or-migrate.sh)`;
350
+ });
351
+ const primaryApi = plan.runtimes.apis[0];
352
+ if (plan.runtimes.databases.length === 0) {
353
+ return renderEnvAwareShellScript([
354
+ 'echo "No database runtimes are configured; skipping DB bootstrap."'
355
+ ]);
356
+ }
357
+ return renderEnvAwareShellScript([
358
+ `if [[ "\${TOPOGRAM_ENVIRONMENT_PROFILE:-${plan.environment.profile}}" == "local_docker" ]]; then`,
359
+ " if ! command -v docker >/dev/null 2>&1; then",
360
+ ' echo "Docker is required for the local_docker profile, but it is not installed." >&2',
361
+ ' echo "Set TOPOGRAM_ENVIRONMENT_PROFILE=local_process and point DATABASE_URL at a working local database, or install Docker." >&2',
362
+ " exit 1",
363
+ " fi",
364
+ ' docker compose -f "$ROOT_DIR/docker-compose.yml" up -d db',
365
+ "fi",
366
+ ...dbBootstrapLines,
367
+ 'if [[ "${TOPOGRAM_SEED_DEMO:-true}" != "false" ]]; then',
368
+ ...apiDatabaseExportLines(primaryApi),
369
+ ...(primaryApi?.uses_database
370
+ ? [`(cd "$ROOT_DIR/${primaryApi.dir}" && npm install && npm exec -- prisma generate --schema prisma/schema.prisma && npm exec -- prisma db push --schema prisma/schema.prisma --skip-generate && npm run seed:demo)`]
371
+ : ['echo "No DB-backed API component is configured; skipping demo seed."']),
372
+ "fi"
373
+ ]);
374
+ }
375
+
376
+ function componentScriptOptions() {
377
+ return {
378
+ rootDirExpression: 'ROOT_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"',
379
+ loadEnvScript: '"$ROOT_DIR/scripts/load-env.sh"'
380
+ };
381
+ }
382
+
383
+ function runtimePortExpression(plan, runtimes, component, sharedEnvName) {
384
+ const runtimeEnvName = `${component.id.toUpperCase()}_PORT`;
385
+ const primaryRuntime = runtimes[0];
386
+ const fallback = primaryRuntime?.id === component.id
387
+ ? `\${${sharedEnvName}:-${component.port}}`
388
+ : `${component.port}`;
389
+ return `\${${runtimeEnvName}:-${fallback}}`;
390
+ }
391
+
392
+ function renderEnvironmentServerDevScript(plan, component = plan.runtimes.apis[0], options = {}) {
393
+ if (!component) {
394
+ return renderEnvAwareShellScript(['echo "No API runtimes are configured."']);
395
+ }
396
+ const guardPortsScript = options.componentScript ? '"$ROOT_DIR/scripts/guard-ports.mjs"' : '"$SCRIPT_DIR/guard-ports.mjs"';
397
+ const serverPortExpression = runtimePortExpression(plan, plan.runtimes.apis, component, "SERVER_PORT");
398
+ return renderEnvAwareShellScript([
399
+ `node ${guardPortsScript} api`,
400
+ "",
401
+ ...apiDatabaseExportLines(component),
402
+ `export PORT="${serverPortExpression}"`,
403
+ `export TOPOGRAM_CORS_ORIGINS="\${TOPOGRAM_CORS_ORIGINS:-http://localhost:\${WEB_PORT:-${plan.ports.web}},http://127.0.0.1:\${WEB_PORT:-${plan.ports.web}}}"`,
404
+ "",
405
+ `cd "$ROOT_DIR/${component.dir}"`,
406
+ "npm install",
407
+ ...(component.uses_database ? ["npm exec -- prisma generate --schema prisma/schema.prisma"] : []),
408
+ "npm run dev"
409
+ ], options.componentScript ? componentScriptOptions() : {});
410
+ }
411
+
412
+ function renderEnvironmentWebDevScript(plan, component = plan.runtimes.webs[0], options = {}) {
413
+ if (!component) {
414
+ return renderEnvAwareShellScript(['echo "No web runtimes are configured."']);
415
+ }
416
+ const apiRuntime = plan.runtimes.apis.find((entry) => entry.id === component.uses_api) || plan.runtimes.apis[0];
417
+ const guardPortsScript = options.componentScript ? '"$ROOT_DIR/scripts/guard-ports.mjs"' : '"$SCRIPT_DIR/guard-ports.mjs"';
418
+ const webPortExpression = runtimePortExpression(plan, plan.runtimes.webs, component, "WEB_PORT");
419
+ return renderEnvAwareShellScript([
420
+ `node ${guardPortsScript} web`,
421
+ "",
422
+ ...(apiRuntime ? [`export PUBLIC_TOPOGRAM_API_BASE_URL="\${PUBLIC_TOPOGRAM_API_BASE_URL:-http://localhost:\${${apiRuntime.id.toUpperCase()}_PORT:-\${SERVER_PORT:-${apiRuntime.port}}}}"`] : []),
423
+ `export TOPOGRAM_CORS_ORIGINS="\${TOPOGRAM_CORS_ORIGINS:-http://localhost:${webPortExpression},http://127.0.0.1:${webPortExpression}}"`,
424
+ "",
425
+ `cd "$ROOT_DIR/${component.dir}"`,
426
+ "npm install",
427
+ `npm run dev -- --host "\${WEB_HOST:-127.0.0.1}" --port "${webPortExpression}"`,
428
+ ], options.componentScript ? componentScriptOptions() : {});
429
+ }
430
+
431
+ function renderEnvironmentStackDevScript(plan) {
432
+ const startLines = [
433
+ ...plan.runtimes.apis.map((component) => `bash "$SCRIPT_DIR/services/${component.id}-dev.sh" &\nPIDS+=($!)`),
434
+ ...plan.runtimes.webs.map((component) => `bash "$SCRIPT_DIR/web/${component.id}-dev.sh" &\nPIDS+=($!)`)
435
+ ];
436
+ return `#!/usr/bin/env bash
437
+ set -euo pipefail
438
+
439
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
440
+ PIDS=()
441
+
442
+ node "$SCRIPT_DIR/guard-ports.mjs" stack
443
+
444
+ if [[ "\${TOPOGRAM_SKIP_STACK_BOOTSTRAP:-false}" != "true" ]]; then
445
+ bash "$SCRIPT_DIR/bootstrap-db.sh"
446
+ fi
447
+
448
+ ${startLines.length ? startLines.join("\n") : 'echo "No long-running dev services are configured."'}
449
+
450
+ kill_tree() {
451
+ local pid="$1"
452
+ local child
453
+ while IFS= read -r child; do
454
+ [[ -n "$child" ]] && kill_tree "$child"
455
+ done < <(pgrep -P "$pid" 2>/dev/null || true)
456
+ kill "$pid" >/dev/null 2>&1 || true
457
+ }
458
+
459
+ cleanup() {
460
+ if [[ "\${#PIDS[@]}" -gt 0 ]]; then
461
+ for pid in "\${PIDS[@]}"; do
462
+ kill_tree "$pid"
463
+ done
464
+ fi
465
+ }
466
+
467
+ trap cleanup EXIT INT TERM
468
+ ${startLines.length ? "wait" : ""}
469
+ `;
470
+ }
471
+
472
+ function renderEnvironmentGuardPortsScript(plan) {
473
+ const ports = [
474
+ ...plan.runtimes.apis.map((component, index) => ({ id: component.id, type: "api", env: `${component.id.toUpperCase()}_PORT`, fallbackEnv: index === 0 ? "SERVER_PORT" : null, port: component.port })),
475
+ ...plan.runtimes.webs.map((component, index) => ({ id: component.id, type: "web", env: `${component.id.toUpperCase()}_PORT`, fallbackEnv: index === 0 ? "WEB_PORT" : null, port: component.port }))
476
+ ];
477
+ return `#!/usr/bin/env node
478
+ import net from "node:net";
479
+
480
+ const role = process.argv[2] || "stack";
481
+ const ports = ${JSON.stringify(ports, null, 2)};
482
+ const expectedService = ${JSON.stringify(plan.runtimeReference.serviceName || "")};
483
+
484
+ function effectivePort(entry) {
485
+ return Number(process.env[entry.env] || (entry.fallbackEnv ? process.env[entry.fallbackEnv] : "") || entry.port);
486
+ }
487
+
488
+ function portInUse(port) {
489
+ return new Promise((resolve) => {
490
+ const server = net.createServer();
491
+ server.once("error", (error) => {
492
+ resolve(Boolean(error && error.code === "EADDRINUSE"));
493
+ });
494
+ server.once("listening", () => {
495
+ server.close(() => resolve(false));
496
+ });
497
+ server.listen(port, "127.0.0.1");
498
+ });
499
+ }
500
+
501
+ async function readHealth(port) {
502
+ try {
503
+ const response = await fetch(\`http://127.0.0.1:\${port}/health\`);
504
+ const body = await response.json().catch(() => null);
505
+ return { status: response.status, body };
506
+ } catch {
507
+ return null;
508
+ }
509
+ }
510
+
511
+ async function failForServerPort(port) {
512
+ const health = await readHealth(port);
513
+ if (health?.body?.service && expectedService && health.body.service !== expectedService) {
514
+ console.error(\`Port \${port} is already serving \${health.body.service}, not \${expectedService}.\`);
515
+ console.error("Stop the other stack or override SERVER_PORT/PUBLIC_TOPOGRAM_API_BASE_URL before retrying.");
516
+ process.exit(1);
517
+ }
518
+ if (health?.body?.service) {
519
+ console.error(\`Port \${port} is already in use by \${health.body.service}.\`);
520
+ } else {
521
+ console.error(\`Port \${port} is already in use.\`);
522
+ }
523
+ process.exit(1);
524
+ }
525
+
526
+ async function failForWebPort(port) {
527
+ console.error(\`Port \${port} is already in use.\`);
528
+ console.error("Stop the other web dev server or override WEB_PORT before retrying.");
529
+ process.exit(1);
530
+ }
531
+
532
+ for (const entry of ports) {
533
+ if (role !== "stack" && role !== entry.type) {
534
+ continue;
535
+ }
536
+ const port = effectivePort(entry);
537
+ if (await portInUse(port)) {
538
+ if (entry.type === "api") {
539
+ await failForServerPort(port);
540
+ }
541
+ await failForWebPort(port);
542
+ }
543
+ }
544
+ `;
545
+ }
546
+
547
+ function renderEnvironmentDockerDbScript() {
548
+ return renderRootShellScript(['docker compose -f "$ROOT_DIR/docker-compose.yml" up -d db']);
549
+ }
550
+
551
+ function renderEnvironmentDockerStackScript() {
552
+ return renderRootShellScript(['docker compose -f "$ROOT_DIR/docker-compose.yml" up --build']);
553
+ }
554
+
555
+ function renderEnvironmentDockerCompose(plan) {
556
+ return `services:
557
+ db:
558
+ image: postgres:16-alpine
559
+ restart: unless-stopped
560
+ environment:
561
+ POSTGRES_DB: \${POSTGRES_DB:-${plan.runtimeReference.environment.databaseName || "topogram_app"}}
562
+ POSTGRES_USER: \${POSTGRES_USER:-postgres}
563
+ POSTGRES_PASSWORD: \${POSTGRES_PASSWORD:-postgres}
564
+ ports:
565
+ - "\${DB_PORT:-5432}:5432"
566
+ volumes:
567
+ - postgres-data:/var/lib/postgresql/data
568
+
569
+ server:
570
+ image: node:22-alpine
571
+ working_dir: /app
572
+ depends_on:
573
+ - db
574
+ environment:
575
+ DATABASE_URL: postgresql://\${POSTGRES_USER:-postgres}:\${POSTGRES_PASSWORD:-postgres}@db:5432/\${POSTGRES_DB:-${plan.runtimeReference.environment.databaseName || "topogram_app"}}?schema=public
576
+ PORT: \${SERVER_PORT:-${plan.ports.server}}
577
+ TOPOGRAM_CORS_ORIGINS: http://localhost:\${WEB_PORT:-${plan.ports.web}},http://127.0.0.1:\${WEB_PORT:-${plan.ports.web}}
578
+ ports:
579
+ - "127.0.0.1:\${SERVER_PORT:-${plan.ports.server}}:\${SERVER_PORT:-${plan.ports.server}}"
580
+ volumes:
581
+ - ./${plan.directories.server}:/app
582
+ command: >
583
+ sh -lc "npm install &&
584
+ npm exec -- prisma generate --schema prisma/schema.prisma &&
585
+ npm run dev"
586
+
587
+ web:
588
+ image: node:22-alpine
589
+ working_dir: /app
590
+ depends_on:
591
+ - server
592
+ environment:
593
+ PUBLIC_TOPOGRAM_API_BASE_URL: http://localhost:\${SERVER_PORT:-${plan.ports.server}}
594
+ ports:
595
+ - "127.0.0.1:\${WEB_PORT:-${plan.ports.web}}:\${WEB_PORT:-${plan.ports.web}}"
596
+ volumes:
597
+ - ./${plan.directories.web}:/app
598
+ command: >
599
+ sh -lc "npm install &&
600
+ npm run dev -- --host 0.0.0.0 --port \${WEB_PORT:-${plan.ports.web}}"
601
+
602
+ volumes:
603
+ postgres-data:
604
+ `;
605
+ }
606
+
607
+ export function generateEnvironmentBundle(graph, options = {}) {
608
+ const plan = buildEnvironmentPlan(graph, options);
609
+ const topology = resolveRuntimeTopology(graph, options);
610
+ const files = {
611
+ ".env.example": renderEnvironmentEnvExample(plan),
612
+ ".gitignore": "node_modules/\n.env\npostgres-data/\n",
613
+ "README.md": renderEnvironmentReadme(plan),
614
+ "package.json": renderEnvironmentPackageJson(plan),
615
+ "scripts/load-env.sh": renderEnvironmentLoadEnvScript(),
616
+ "scripts/bootstrap-db.sh": renderEnvironmentBootstrapDbScript(plan),
617
+ "scripts/server-dev.sh": renderEnvironmentServerDevScript(plan),
618
+ "scripts/web-dev.sh": renderEnvironmentWebDevScript(plan),
619
+ "scripts/stack-dev.sh": renderEnvironmentStackDevScript(plan),
620
+ "scripts/guard-ports.mjs": renderEnvironmentGuardPortsScript(plan)
621
+ };
622
+
623
+ for (const component of plan.runtimes.apis) {
624
+ files[`scripts/services/${component.id}-dev.sh`] = renderEnvironmentServerDevScript(plan, component, { componentScript: true });
625
+ }
626
+ for (const component of plan.runtimes.webs) {
627
+ files[`scripts/web/${component.id}-dev.sh`] = renderEnvironmentWebDevScript(plan, component, { componentScript: true });
628
+ }
629
+
630
+ if (plan.orchestration.usesDocker) {
631
+ files["docker-compose.yml"] = renderEnvironmentDockerCompose(plan);
632
+ files["scripts/docker-db.sh"] = renderEnvironmentDockerDbScript();
633
+ files["scripts/docker-stack.sh"] = renderEnvironmentDockerStackScript();
634
+ }
635
+
636
+ for (const component of topology.apiRuntimes) {
637
+ const serverBundle = generateServerBundle(graph, component.projection.id, { ...options, component });
638
+ mergeNamedBundles(files, {
639
+ [topology.serviceDir(component)]: serverBundle
640
+ });
641
+ }
642
+ for (const component of topology.webRuntimes) {
643
+ const webBundle = generateWebBundle(graph, component.projection.id, { ...options, component });
644
+ mergeNamedBundles(files, {
645
+ [topology.webDir(component)]: webBundle
646
+ });
647
+ }
648
+ for (const component of topology.nativeRuntimes) {
649
+ const nativeBundle = generateNativeBundle(graph, component.projection.id, { ...options, component });
650
+ mergeNamedBundles(files, {
651
+ [topology.nativeDir(component)]: nativeBundle
652
+ });
653
+ }
654
+ for (const component of topology.dbRuntimes) {
655
+ const dbBundle = generateDbBundle(graph, component.projection.id, { ...options, component });
656
+ mergeNamedBundles(files, {
657
+ [topology.dbDir(component)]: dbBundle
658
+ });
659
+ }
660
+
661
+ return files;
662
+ }
663
+
664
+ export function generateEnvironmentPlan(graph, options = {}) {
665
+ return buildEnvironmentPlan(graph, options);
666
+ }