@topogram/cli 0.3.34

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 (257) hide show
  1. package/ARCHITECTURE.md +67 -0
  2. package/CHANGELOG.md +240 -0
  3. package/README.md +223 -0
  4. package/package.json +51 -0
  5. package/src/adoption/index.js +3 -0
  6. package/src/adoption/plan.js +702 -0
  7. package/src/adoption/reporting.js +464 -0
  8. package/src/adoption/review-groups.js +313 -0
  9. package/src/agent-ops/query-builders.js +5012 -0
  10. package/src/archive/archive.js +141 -0
  11. package/src/archive/compact.js +26 -0
  12. package/src/archive/jsonl.js +70 -0
  13. package/src/archive/resolver-bridge.js +82 -0
  14. package/src/archive/schema.js +87 -0
  15. package/src/archive/unarchive.js +108 -0
  16. package/src/catalog.js +752 -0
  17. package/src/cli/catalog-alias.js +166 -0
  18. package/src/cli.js +9738 -0
  19. package/src/component-behavior.js +173 -0
  20. package/src/example-implementation.js +91 -0
  21. package/src/format.js +19 -0
  22. package/src/generator/adapters.d.ts +4 -0
  23. package/src/generator/adapters.js +325 -0
  24. package/src/generator/api.d.ts +1 -0
  25. package/src/generator/api.js +1196 -0
  26. package/src/generator/check.js +355 -0
  27. package/src/generator/component-conformance.js +767 -0
  28. package/src/generator/components.js +39 -0
  29. package/src/generator/context/bundle.js +291 -0
  30. package/src/generator/context/diff.js +256 -0
  31. package/src/generator/context/digest.js +182 -0
  32. package/src/generator/context/domain-coverage.js +94 -0
  33. package/src/generator/context/domain-page.js +137 -0
  34. package/src/generator/context/index.js +42 -0
  35. package/src/generator/context/report.js +121 -0
  36. package/src/generator/context/shared.js +1397 -0
  37. package/src/generator/context/slice.js +703 -0
  38. package/src/generator/context/task-mode.js +466 -0
  39. package/src/generator/docs.js +327 -0
  40. package/src/generator/index.js +161 -0
  41. package/src/generator/native/parity-bundle.js +311 -0
  42. package/src/generator/output.js +300 -0
  43. package/src/generator/registry.js +482 -0
  44. package/src/generator/runtime/app-bundle.js +456 -0
  45. package/src/generator/runtime/bundle-shared.js +166 -0
  46. package/src/generator/runtime/compile-check.js +163 -0
  47. package/src/generator/runtime/deployment.js +287 -0
  48. package/src/generator/runtime/environment.js +635 -0
  49. package/src/generator/runtime/index.js +32 -0
  50. package/src/generator/runtime/runtime-check.js +554 -0
  51. package/src/generator/runtime/shared.js +515 -0
  52. package/src/generator/runtime/smoke.js +219 -0
  53. package/src/generator/schema.js +204 -0
  54. package/src/generator/sdlc/board.js +66 -0
  55. package/src/generator/sdlc/doc-page.js +53 -0
  56. package/src/generator/sdlc/index.js +23 -0
  57. package/src/generator/sdlc/release-notes.js +62 -0
  58. package/src/generator/sdlc/traceability-matrix.js +65 -0
  59. package/src/generator/shared.js +29 -0
  60. package/src/generator/surfaces/contracts.js +146 -0
  61. package/src/generator/surfaces/databases/contract.js +40 -0
  62. package/src/generator/surfaces/databases/index.js +84 -0
  63. package/src/generator/surfaces/databases/lifecycle-shared.d.ts +1 -0
  64. package/src/generator/surfaces/databases/lifecycle-shared.js +612 -0
  65. package/src/generator/surfaces/databases/migration-plan.js +281 -0
  66. package/src/generator/surfaces/databases/postgres/capabilities.js +14 -0
  67. package/src/generator/surfaces/databases/postgres/drizzle.js +99 -0
  68. package/src/generator/surfaces/databases/postgres/index.js +9 -0
  69. package/src/generator/surfaces/databases/postgres/lifecycle.js +16 -0
  70. package/src/generator/surfaces/databases/postgres/prisma.js +159 -0
  71. package/src/generator/surfaces/databases/postgres/sql-migration.js +102 -0
  72. package/src/generator/surfaces/databases/postgres/sql-schema.js +34 -0
  73. package/src/generator/surfaces/databases/shared.d.ts +1 -0
  74. package/src/generator/surfaces/databases/shared.js +350 -0
  75. package/src/generator/surfaces/databases/snapshot.js +96 -0
  76. package/src/generator/surfaces/databases/sqlite/capabilities.js +14 -0
  77. package/src/generator/surfaces/databases/sqlite/index.js +8 -0
  78. package/src/generator/surfaces/databases/sqlite/lifecycle.js +16 -0
  79. package/src/generator/surfaces/databases/sqlite/prisma.js +143 -0
  80. package/src/generator/surfaces/databases/sqlite/sql-migration.js +65 -0
  81. package/src/generator/surfaces/databases/sqlite/sql-schema.js +27 -0
  82. package/src/generator/surfaces/index.js +25 -0
  83. package/src/generator/surfaces/native/swiftui-app.js +38 -0
  84. package/src/generator/surfaces/native/swiftui-templates/Package.swift.txt +20 -0
  85. package/src/generator/surfaces/native/swiftui-templates/README.generated.md +26 -0
  86. package/src/generator/surfaces/native/swiftui-templates/runtime/DynamicScreens.swift +682 -0
  87. package/src/generator/surfaces/native/swiftui-templates/runtime/TodoAPIClient.swift +156 -0
  88. package/src/generator/surfaces/native/swiftui-templates/runtime/TodoSwiftUIApp.swift +44 -0
  89. package/src/generator/surfaces/native/swiftui-templates/runtime/Visibility.swift +183 -0
  90. package/src/generator/surfaces/services/express.d.ts +1 -0
  91. package/src/generator/surfaces/services/express.js +766 -0
  92. package/src/generator/surfaces/services/hono.d.ts +1 -0
  93. package/src/generator/surfaces/services/hono.js +204 -0
  94. package/src/generator/surfaces/services/index.js +42 -0
  95. package/src/generator/surfaces/services/persistence-wiring.js +240 -0
  96. package/src/generator/surfaces/services/runtime-helpers.js +631 -0
  97. package/src/generator/surfaces/services/server-contract.js +80 -0
  98. package/src/generator/surfaces/services/stateless.d.ts +1 -0
  99. package/src/generator/surfaces/services/stateless.js +97 -0
  100. package/src/generator/surfaces/shared.js +64 -0
  101. package/src/generator/surfaces/web/api-client.js +1 -0
  102. package/src/generator/surfaces/web/forms.js +1 -0
  103. package/src/generator/surfaces/web/index.d.ts +2 -0
  104. package/src/generator/surfaces/web/index.js +53 -0
  105. package/src/generator/surfaces/web/react-components.js +248 -0
  106. package/src/generator/surfaces/web/react.js +538 -0
  107. package/src/generator/surfaces/web/routes.js +1 -0
  108. package/src/generator/surfaces/web/screens.js +1 -0
  109. package/src/generator/surfaces/web/shared.js +369 -0
  110. package/src/generator/surfaces/web/sveltekit-actions.js +28 -0
  111. package/src/generator/surfaces/web/sveltekit-components.js +234 -0
  112. package/src/generator/surfaces/web/sveltekit.js +426 -0
  113. package/src/generator/surfaces/web/ui-web-contract.js +65 -0
  114. package/src/generator/surfaces/web/vanilla.js +239 -0
  115. package/src/generator/verification.js +84 -0
  116. package/src/generator.js +1 -0
  117. package/src/import/core/context.js +52 -0
  118. package/src/import/core/contracts.js +23 -0
  119. package/src/import/core/registry.js +81 -0
  120. package/src/import/core/runner.js +646 -0
  121. package/src/import/core/shared.js +910 -0
  122. package/src/import/enrichers/auth-session.js +18 -0
  123. package/src/import/enrichers/django-rest.js +226 -0
  124. package/src/import/enrichers/doc-linking.js +20 -0
  125. package/src/import/enrichers/rails-controllers.js +246 -0
  126. package/src/import/enrichers/rails-models.js +130 -0
  127. package/src/import/enrichers/workflow-target-state.js +10 -0
  128. package/src/import/extractors/api/aspnet-core.js +304 -0
  129. package/src/import/extractors/api/django-routes.js +318 -0
  130. package/src/import/extractors/api/express.js +154 -0
  131. package/src/import/extractors/api/fastify.js +371 -0
  132. package/src/import/extractors/api/flutter-dio.js +135 -0
  133. package/src/import/extractors/api/generic-route-fallback.js +90 -0
  134. package/src/import/extractors/api/graphql-code-first.js +565 -0
  135. package/src/import/extractors/api/graphql-sdl.js +309 -0
  136. package/src/import/extractors/api/jaxrs.js +303 -0
  137. package/src/import/extractors/api/micronaut.js +213 -0
  138. package/src/import/extractors/api/next-route.js +50 -0
  139. package/src/import/extractors/api/next-server-action.js +51 -0
  140. package/src/import/extractors/api/nextauth.js +52 -0
  141. package/src/import/extractors/api/openapi-code.js +242 -0
  142. package/src/import/extractors/api/openapi.js +232 -0
  143. package/src/import/extractors/api/rails-routes.js +230 -0
  144. package/src/import/extractors/api/react-native-repository.js +128 -0
  145. package/src/import/extractors/api/retrofit.js +103 -0
  146. package/src/import/extractors/api/spring-web.js +372 -0
  147. package/src/import/extractors/api/swift-webapi.js +116 -0
  148. package/src/import/extractors/api/trpc.js +212 -0
  149. package/src/import/extractors/db/django-models.js +232 -0
  150. package/src/import/extractors/db/dotnet-models.js +93 -0
  151. package/src/import/extractors/db/drizzle.js +242 -0
  152. package/src/import/extractors/db/ef-core.js +221 -0
  153. package/src/import/extractors/db/flutter-entities.js +120 -0
  154. package/src/import/extractors/db/jpa.js +120 -0
  155. package/src/import/extractors/db/liquibase.js +180 -0
  156. package/src/import/extractors/db/mybatis-xml.js +145 -0
  157. package/src/import/extractors/db/prisma.js +185 -0
  158. package/src/import/extractors/db/rails-schema.js +175 -0
  159. package/src/import/extractors/db/react-native-entities.js +95 -0
  160. package/src/import/extractors/db/room.js +193 -0
  161. package/src/import/extractors/db/snapshot.js +112 -0
  162. package/src/import/extractors/db/sql.js +180 -0
  163. package/src/import/extractors/db/swiftdata.js +137 -0
  164. package/src/import/extractors/ui/android-compose.js +230 -0
  165. package/src/import/extractors/ui/backend-only.js +70 -0
  166. package/src/import/extractors/ui/blazor.js +227 -0
  167. package/src/import/extractors/ui/flutter-screens.js +152 -0
  168. package/src/import/extractors/ui/maui-xaml.js +135 -0
  169. package/src/import/extractors/ui/next-app-router.js +83 -0
  170. package/src/import/extractors/ui/next-pages-router.js +141 -0
  171. package/src/import/extractors/ui/razor-pages.js +181 -0
  172. package/src/import/extractors/ui/react-native-screens.js +166 -0
  173. package/src/import/extractors/ui/react-router.js +139 -0
  174. package/src/import/extractors/ui/sveltekit.js +123 -0
  175. package/src/import/extractors/ui/swiftui.js +193 -0
  176. package/src/import/extractors/ui/uikit.js +175 -0
  177. package/src/import/extractors/verification/generic.js +290 -0
  178. package/src/import/extractors/workflows/generic.js +137 -0
  179. package/src/import/index.js +7 -0
  180. package/src/import/provenance.js +158 -0
  181. package/src/new-project.js +2107 -0
  182. package/src/parser.js +439 -0
  183. package/src/policy/review-boundaries.js +165 -0
  184. package/src/project-config.js +535 -0
  185. package/src/proofs/backend-parity.js +19 -0
  186. package/src/proofs/contract-audit.js +220 -0
  187. package/src/proofs/ios-parity.js +7 -0
  188. package/src/proofs/issues-parity.js +10 -0
  189. package/src/proofs/web-parity.js +50 -0
  190. package/src/realization/api/build-api-realization.js +5 -0
  191. package/src/realization/api/index.js +1 -0
  192. package/src/realization/backend/build-backend-runtime-realization.js +82 -0
  193. package/src/realization/backend/index.d.ts +1 -0
  194. package/src/realization/backend/index.js +4 -0
  195. package/src/realization/db/build-db-realization.js +17 -0
  196. package/src/realization/db/index.js +3 -0
  197. package/src/realization/db/migration-plan.js +5 -0
  198. package/src/realization/db/snapshot.js +5 -0
  199. package/src/realization/ui/build-ui-shared-realization.js +305 -0
  200. package/src/realization/ui/build-web-realization.js +189 -0
  201. package/src/realization/ui/index.js +2 -0
  202. package/src/reconcile/docs.js +280 -0
  203. package/src/reconcile/index.js +3 -0
  204. package/src/reconcile/journeys.js +441 -0
  205. package/src/resolver/docs.js +1 -0
  206. package/src/resolver/enrich/acceptance-criterion.js +14 -0
  207. package/src/resolver/enrich/bug.js +12 -0
  208. package/src/resolver/enrich/component.js +2 -0
  209. package/src/resolver/enrich/index.js +1 -0
  210. package/src/resolver/enrich/pitch.js +18 -0
  211. package/src/resolver/enrich/requirement.js +20 -0
  212. package/src/resolver/enrich/task.js +16 -0
  213. package/src/resolver/expressions.js +1 -0
  214. package/src/resolver/index.js +2422 -0
  215. package/src/resolver/normalize.js +1 -0
  216. package/src/resolver.js +1 -0
  217. package/src/sdlc/adopt.js +65 -0
  218. package/src/sdlc/check.js +86 -0
  219. package/src/sdlc/dod/acceptance-criterion.js +22 -0
  220. package/src/sdlc/dod/bug.js +26 -0
  221. package/src/sdlc/dod/document.js +23 -0
  222. package/src/sdlc/dod/index.js +25 -0
  223. package/src/sdlc/dod/pitch.js +23 -0
  224. package/src/sdlc/dod/requirement.js +34 -0
  225. package/src/sdlc/dod/task.js +39 -0
  226. package/src/sdlc/explain.js +116 -0
  227. package/src/sdlc/history.js +80 -0
  228. package/src/sdlc/paths.js +11 -0
  229. package/src/sdlc/release.js +106 -0
  230. package/src/sdlc/scaffold.js +89 -0
  231. package/src/sdlc/status-filter.js +54 -0
  232. package/src/sdlc/transition.js +112 -0
  233. package/src/sdlc/transitions/acceptance-criterion.js +28 -0
  234. package/src/sdlc/transitions/bug.js +31 -0
  235. package/src/sdlc/transitions/document.js +29 -0
  236. package/src/sdlc/transitions/index.js +56 -0
  237. package/src/sdlc/transitions/pitch.js +34 -0
  238. package/src/sdlc/transitions/requirement.js +31 -0
  239. package/src/sdlc/transitions/task.js +34 -0
  240. package/src/template-trust.js +597 -0
  241. package/src/validator/expressions.js +1 -0
  242. package/src/validator/index.js +3424 -0
  243. package/src/validator/kinds.js +346 -0
  244. package/src/validator/per-kind/acceptance-criterion.js +91 -0
  245. package/src/validator/per-kind/bug.js +77 -0
  246. package/src/validator/per-kind/component.js +274 -0
  247. package/src/validator/per-kind/domain.js +205 -0
  248. package/src/validator/per-kind/pitch.js +101 -0
  249. package/src/validator/per-kind/requirement.js +75 -0
  250. package/src/validator/per-kind/task.js +96 -0
  251. package/src/validator/registry.js +1 -0
  252. package/src/validator/utils.js +12 -0
  253. package/src/validator.js +1 -0
  254. package/src/workflows.js +7597 -0
  255. package/src/workspace-docs.js +265 -0
  256. package/template-helpers/react.js +5 -0
  257. package/template-helpers/sveltekit.js +5 -0
@@ -0,0 +1,456 @@
1
+ import {
2
+ generateCompileCheckBundle
3
+ } from "./compile-check.js";
4
+ import {
5
+ generateDeploymentBundle
6
+ } from "./deployment.js";
7
+ import {
8
+ generateEnvironmentBundle
9
+ } from "./environment.js";
10
+ import {
11
+ generateRuntimeCheckBundle
12
+ } from "./runtime-check.js";
13
+ import {
14
+ generateRuntimeSmokeBundle
15
+ } from "./smoke.js";
16
+ import {
17
+ buildVerificationSummary,
18
+ getDefaultEnvironmentProjections,
19
+ resolveRuntimeTopology,
20
+ runtimePorts,
21
+ runtimeUrls
22
+ } from "./shared.js";
23
+ import { getExampleImplementation } from "../../example-implementation.js";
24
+ import { mergeNamedBundles, renderLoadEnvScript, renderNestedBundleShellScript } from "./bundle-shared.js";
25
+
26
+ function runtimeReferenceFor(graph, options = {}) {
27
+ try {
28
+ return structuredClone(getExampleImplementation(graph, options).runtime.reference);
29
+ } catch {
30
+ return {
31
+ appBundle: {
32
+ name: "Topogram App Bundle",
33
+ demoContainerName: "Hello workflow",
34
+ demoPrimaryTitle: "Hello page"
35
+ },
36
+ environment: { databaseName: "topogram_app", envExample: "" },
37
+ ports: { server: 3000, web: 5173 },
38
+ demoEnv: { userId: "11111111-1111-4111-8111-111111111111" },
39
+ smoke: { webPath: "/", expectText: "Topogram" },
40
+ runtimeCheck: {}
41
+ };
42
+ }
43
+ }
44
+
45
+ function buildAppBundlePlan(graph, options = {}) {
46
+ const runtimeReference = runtimeReferenceFor(graph, options);
47
+ const topology = resolveRuntimeTopology(graph, options);
48
+ const { apiProjection, uiProjection, dbProjection } = getDefaultEnvironmentProjections(graph, options);
49
+ const environmentProfile = options.profileId || "local_process";
50
+ const deployProfile = options.deployProfileId || "fly_io";
51
+ const smokeVerification = buildVerificationSummary(graph, ["smoke", "journey"]);
52
+ const runtimeVerification = buildVerificationSummary(graph, ["runtime", "contract", "journey"]);
53
+ if (smokeVerification) {
54
+ runtimeReference.smoke.verification = smokeVerification;
55
+ }
56
+ if (runtimeVerification) {
57
+ runtimeReference.runtimeCheck.verification = runtimeVerification;
58
+ }
59
+ return {
60
+ type: "app_bundle_plan",
61
+ name: runtimeReference.appBundle.name,
62
+ projections: {
63
+ api: apiProjection?.id || null,
64
+ ui: uiProjection?.id || null,
65
+ db: dbProjection?.id || null
66
+ },
67
+ topology: {
68
+ components: topology.components.map((component) => ({
69
+ id: component.id,
70
+ type: component.type,
71
+ projection: component.projection.id,
72
+ generator: component.generator,
73
+ port: component.port ?? null,
74
+ api: component.api || null,
75
+ database: component.database || null
76
+ }))
77
+ },
78
+ runtimeReference,
79
+ profiles: {
80
+ environment: environmentProfile,
81
+ deployment: deployProfile
82
+ },
83
+ commands: {
84
+ bootstrap: "./scripts/bootstrap.sh",
85
+ dev: "./scripts/dev.sh",
86
+ compile: "./scripts/compile-check.sh",
87
+ runtime: "./scripts/runtime.sh",
88
+ smoke: "./scripts/smoke.sh",
89
+ runtimeCheck: "./scripts/runtime-check.sh",
90
+ deployCheck: "./scripts/deploy-check.sh"
91
+ },
92
+ layout: {
93
+ apps: "apps",
94
+ deploy: "deploy",
95
+ smoke: "smoke",
96
+ runtimeCheck: "runtime-check",
97
+ compile: "compile",
98
+ services: "apps/services",
99
+ web: "apps/web",
100
+ db: "apps/db",
101
+ native: "apps/native"
102
+ }
103
+ };
104
+ }
105
+
106
+ function renderAppBundleEnvExample(plan) {
107
+ const demo = plan.runtimeReference.demoEnv;
108
+ const databaseName = plan.runtimeReference.environment.databaseName || "topogram_app";
109
+ const topology = {
110
+ primaryApi: { port: plan.topology.components.find((component) => component.type === "api")?.port },
111
+ primaryWeb: { port: plan.topology.components.find((component) => component.type === "web")?.port }
112
+ };
113
+ const ports = runtimePorts(plan.runtimeReference, topology);
114
+ const urls = runtimeUrls(plan.runtimeReference, topology);
115
+ if (!plan.projections.dbPlatform) {
116
+ return `# App bundle defaults
117
+ TOPOGRAM_ENVIRONMENT_PROFILE=${plan.profiles.environment}
118
+ TOPOGRAM_DEPLOY_PROFILE=${plan.profiles.deployment}
119
+
120
+ # Local runtime defaults
121
+ ${plan.projections.api ? `SERVER_PORT=${ports.server}\n` : ""}${plan.projections.ui ? `WEB_PORT=${ports.web}\n` : ""}${plan.projections.api && plan.projections.ui ? `PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}\n` : ""}PUBLIC_TOPOGRAM_DEMO_USER_ID=${demo.userId}
122
+ TOPOGRAM_DEMO_USER_ID=${demo.userId}
123
+ ${plan.runtimeReference.environment.envExample || ""}
124
+
125
+ # Smoke-test defaults
126
+ ${plan.projections.api ? `TOPOGRAM_API_BASE_URL=${urls.api}\n` : ""}${plan.projections.ui ? `TOPOGRAM_WEB_BASE_URL=${urls.web}\n` : ""}`;
127
+ }
128
+ if (plan.projections.dbPlatform === "db_sqlite") {
129
+ return `# App bundle defaults
130
+ TOPOGRAM_ENVIRONMENT_PROFILE=${plan.profiles.environment}
131
+ TOPOGRAM_DEPLOY_PROFILE=${plan.profiles.deployment}
132
+
133
+ # Local runtime defaults
134
+ SERVER_PORT=${ports.server}
135
+ WEB_PORT=${ports.web}
136
+ DATABASE_URL=file:./var/${databaseName}.sqlite
137
+ PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}
138
+ PUBLIC_TOPOGRAM_DEMO_USER_ID=${demo.userId}
139
+ TOPOGRAM_DEMO_USER_ID=${demo.userId}
140
+ ${plan.runtimeReference.environment.envExample || ""}
141
+ TOPOGRAM_SEED_DEMO=true
142
+
143
+ # Smoke-test defaults
144
+ TOPOGRAM_API_BASE_URL=${urls.api}
145
+ TOPOGRAM_WEB_BASE_URL=${urls.web}
146
+ `;
147
+ }
148
+ return `# App bundle defaults
149
+ TOPOGRAM_ENVIRONMENT_PROFILE=${plan.profiles.environment}
150
+ TOPOGRAM_DEPLOY_PROFILE=${plan.profiles.deployment}
151
+
152
+ # Local runtime defaults
153
+ DB_PORT=5432
154
+ SERVER_PORT=${ports.server}
155
+ WEB_PORT=${ports.web}
156
+ POSTGRES_DB=${databaseName}
157
+ POSTGRES_USER=\${USER:-postgres}
158
+ POSTGRES_PASSWORD=postgres
159
+ DATABASE_URL=postgresql://\${POSTGRES_USER}@localhost:5432/${databaseName}
160
+ DATABASE_ADMIN_URL=postgresql://\${POSTGRES_USER}@localhost:5432/postgres
161
+ PUBLIC_TOPOGRAM_API_BASE_URL=${urls.api}
162
+ PUBLIC_TOPOGRAM_DEMO_USER_ID=${demo.userId}
163
+ TOPOGRAM_DEMO_USER_ID=${demo.userId}
164
+ ${plan.runtimeReference.environment.envExample || ""}
165
+ TOPOGRAM_SEED_DEMO=true
166
+
167
+ # Smoke-test defaults
168
+ TOPOGRAM_API_BASE_URL=${urls.api}
169
+ TOPOGRAM_WEB_BASE_URL=${urls.web}
170
+ `;
171
+ }
172
+
173
+ function renderAppBundleReadme(plan) {
174
+ const urls = runtimeUrls(plan.runtimeReference, {
175
+ primaryApi: { port: plan.topology.components.find((component) => component.type === "api")?.port },
176
+ primaryWeb: { port: plan.topology.components.find((component) => component.type === "web")?.port }
177
+ });
178
+ return `# ${plan.name}
179
+
180
+ This is the polished generated app bundle for Topogram v0.1.
181
+
182
+ It includes:
183
+ - \`apps/services/<api-id>/\`: generated API service scaffolds
184
+ - \`apps/web/<web-id>/\`: generated web scaffolds
185
+ - \`apps/db/<db-id>/\`: generated DB lifecycle bundles
186
+ - \`apps/native/<native-id>/\`: generated native app scaffolds
187
+ - \`deploy/\`: deployment packaging
188
+ - \`compile/\`: generated compile verification
189
+ - \`smoke/\`: minimal runtime confidence check
190
+ - \`runtime-check/\`: richer staged runtime verification with JSON reporting
191
+
192
+ ## Start Here
193
+
194
+ 1. Copy \`.env.example\` to \`.env\` if you want to customize defaults
195
+ 2. Bootstrap the app:
196
+ - \`bash ${plan.commands.bootstrap.replace("./", "")}\`
197
+ - this provisions or migrates the database and seeds demo data by default
198
+ 3. Run the app:
199
+ - \`bash ${plan.commands.dev.replace("./", "")}\`
200
+ 4. Compile-check it:
201
+ - \`bash ${plan.commands.compile.replace("./", "")}\`
202
+ 5. Run self-contained local runtime verification:
203
+ - \`bash ${plan.commands.runtime.replace("./", "")}\`
204
+
205
+ Or, with the app still running, run richer staged runtime checks in another terminal:
206
+ - \`bash ${plan.commands.runtimeCheck.replace("./", "")}\`
207
+ Then run the lightweight smoke check:
208
+ - \`bash ${plan.commands.smoke.replace("./", "")}\`
209
+
210
+ ## Golden Path
211
+
212
+ For the default generated bundle:
213
+
214
+ 1. Use the \`${plan.profiles.environment}\` environment profile
215
+ 2. Run \`bash ${plan.commands.bootstrap.replace("./", "")}\`
216
+ 3. Run \`bash ${plan.commands.dev.replace("./", "")}\`
217
+ 4. Open the web app at \`${urls.web}${plan.runtimeReference.smoke.webPath}\`
218
+ 5. Confirm the seeded "${plan.runtimeReference.appBundle.demoContainerName}" and "${plan.runtimeReference.appBundle.demoPrimaryTitle}" flow through the stack
219
+ 6. Run \`bash ${plan.commands.compile.replace("./", "")}\`
220
+ 7. Run \`bash ${plan.commands.runtime.replace("./", "")}\`
221
+
222
+ ## Deployment
223
+
224
+ - Validate deploy configuration:
225
+ - \`bash ${plan.commands.deployCheck.replace("./", "")}\`
226
+ - Then use the generated deployment bundle under \`deploy/\`
227
+
228
+ ## Notes
229
+
230
+ - The default generated app profile is \`${plan.profiles.environment}\`
231
+ - The default generated deployment profile is \`${plan.profiles.deployment}\`
232
+ - Demo data is seeded during bootstrap unless \`TOPOGRAM_SEED_DEMO=false\`
233
+ - If \`.env\` is missing, generated scripts fall back to \`.env.example\`
234
+ - You can regenerate other environment or deployment profiles from the Topogram source project
235
+ - The generated server exposes \`GET /health\` for liveness and \`GET /ready\` for DB-backed readiness
236
+ - \`compile/\` is self-contained and does not require the app to be running
237
+ - \`smoke/\` and \`runtime-check/\` are probes against a running local stack
238
+ - \`scripts/runtime.sh\` starts the local stack, waits for readiness, runs the probes, and stops the stack
239
+ `;
240
+ }
241
+
242
+ function renderAppBundlePackageJson() {
243
+ return `${JSON.stringify({
244
+ name: "topogram-app-bundle",
245
+ private: true,
246
+ scripts: {
247
+ check: "npm run compile",
248
+ bootstrap: "bash ./scripts/bootstrap.sh",
249
+ dev: "bash ./scripts/dev.sh",
250
+ compile: "bash ./scripts/compile-check.sh",
251
+ runtime: "bash ./scripts/runtime.sh",
252
+ "runtime-check": "bash ./scripts/runtime-check.sh",
253
+ smoke: "bash ./scripts/smoke.sh",
254
+ probe: "npm run smoke && npm run runtime-check",
255
+ "deploy:check": "bash ./scripts/deploy-check.sh"
256
+ }
257
+ }, null, 2)}\n`;
258
+ }
259
+
260
+ function renderAppBundleLoadEnvScript() {
261
+ return renderLoadEnvScript();
262
+ }
263
+
264
+ function renderAppBundleBootstrapScript() {
265
+ return renderNestedBundleShellScript("apps", "scripts/bootstrap-db.sh");
266
+ }
267
+
268
+ function renderAppBundleDevScript() {
269
+ return renderNestedBundleShellScript("apps", "scripts/stack-dev.sh");
270
+ }
271
+
272
+ function renderAppBundleRuntimeScript() {
273
+ return `#!/usr/bin/env bash
274
+ set -euo pipefail
275
+
276
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
277
+ ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
278
+ STACK_PID=""
279
+
280
+ . "$SCRIPT_DIR/load-env.sh"
281
+
282
+ kill_tree() {
283
+ local pid="$1"
284
+ local child
285
+ while IFS= read -r child; do
286
+ [[ -n "$child" ]] && kill_tree "$child"
287
+ done < <(pgrep -P "$pid" 2>/dev/null || true)
288
+ kill "$pid" >/dev/null 2>&1 || true
289
+ }
290
+
291
+ cleanup() {
292
+ if [[ -n "$STACK_PID" ]]; then
293
+ kill_tree "$STACK_PID"
294
+ wait "$STACK_PID" >/dev/null 2>&1 || true
295
+ fi
296
+ }
297
+
298
+ trap cleanup EXIT INT TERM
299
+
300
+ bash "$SCRIPT_DIR/bootstrap.sh"
301
+
302
+ TOPOGRAM_SKIP_STACK_BOOTSTRAP=true bash "$SCRIPT_DIR/dev.sh" &
303
+ STACK_PID=$!
304
+
305
+ node "$SCRIPT_DIR/wait-for-stack.mjs"
306
+ bash "$SCRIPT_DIR/smoke.sh"
307
+ bash "$SCRIPT_DIR/runtime-check.sh"
308
+ `;
309
+ }
310
+
311
+ function renderAppBundleWaitForStackScript(plan) {
312
+ const topology = {
313
+ primaryApi: { port: plan.topology.components.find((component) => component.type === "api")?.port },
314
+ primaryWeb: { port: plan.topology.components.find((component) => component.type === "web")?.port }
315
+ };
316
+ const ports = runtimePorts(plan.runtimeReference, topology);
317
+ const urls = runtimeUrls(plan.runtimeReference, topology);
318
+ const endpoints = [
319
+ ...(plan.projections.api ? [
320
+ { label: "api health", url: `${urls.api}/health`, expectJson: "ok" },
321
+ { label: "api readiness", url: `${urls.api}/ready`, expectJson: "ready" }
322
+ ] : []),
323
+ ...(plan.projections.ui ? [
324
+ { label: "web app", url: `${urls.web}${plan.runtimeReference.smoke.webPath || "/"}` }
325
+ ] : [])
326
+ ];
327
+ return `const endpoints = ${JSON.stringify(endpoints, null, 2)};
328
+ const timeoutMs = Number(process.env.TOPOGRAM_RUNTIME_WAIT_MS || "60000");
329
+ const intervalMs = Number(process.env.TOPOGRAM_RUNTIME_WAIT_INTERVAL_MS || "500");
330
+ const startedAt = Date.now();
331
+
332
+ function envUrl(url) {
333
+ return String(url)
334
+ .replace("http://localhost:${ports.server}", process.env.TOPOGRAM_API_BASE_URL || process.env.PUBLIC_TOPOGRAM_API_BASE_URL || "http://localhost:${ports.server}")
335
+ .replace("http://localhost:${ports.web}", process.env.TOPOGRAM_WEB_BASE_URL || process.env.PUBLIC_TOPOGRAM_WEB_BASE_URL || \`http://localhost:\${process.env.WEB_PORT || "${ports.web}"}\`);
336
+ }
337
+
338
+ function sleep(ms) {
339
+ return new Promise((resolve) => setTimeout(resolve, ms));
340
+ }
341
+
342
+ async function isReady(endpoint) {
343
+ const url = envUrl(endpoint.url);
344
+ try {
345
+ const response = await fetch(url);
346
+ if (response.status !== 200) {
347
+ return { ok: false, message: \`\${endpoint.label} returned \${response.status}\` };
348
+ }
349
+ if (endpoint.expectJson) {
350
+ const body = await response.json().catch(() => null);
351
+ if (body?.[endpoint.expectJson] !== true) {
352
+ return { ok: false, message: \`\${endpoint.label} did not report \${endpoint.expectJson}=true\` };
353
+ }
354
+ }
355
+ return { ok: true };
356
+ } catch (error) {
357
+ const code = error?.cause?.code || error?.code || (error instanceof Error ? error.message : String(error));
358
+ return { ok: false, message: \`\${endpoint.label} not reachable: \${code}\` };
359
+ }
360
+ }
361
+
362
+ if (endpoints.length === 0) {
363
+ console.log("No runtime endpoints are configured; skipping readiness wait.");
364
+ process.exit(0);
365
+ }
366
+
367
+ let lastMessage = "";
368
+ while (Date.now() - startedAt < timeoutMs) {
369
+ const results = await Promise.all(endpoints.map((endpoint) => isReady(endpoint)));
370
+ if (results.every((result) => result.ok)) {
371
+ console.log("Generated stack is ready.");
372
+ process.exit(0);
373
+ }
374
+ lastMessage = results.filter((result) => !result.ok).map((result) => result.message).join("; ");
375
+ await sleep(intervalMs);
376
+ }
377
+
378
+ console.error(\`Generated stack did not become ready within \${timeoutMs}ms. \${lastMessage}\`);
379
+ process.exit(1);
380
+ `;
381
+ }
382
+
383
+ function renderAppBundleSmokeScript() {
384
+ return renderNestedBundleShellScript("smoke", "scripts/smoke.sh");
385
+ }
386
+
387
+ function renderAppBundleRuntimeCheckScript() {
388
+ return renderNestedBundleShellScript("runtime-check", "scripts/check.sh");
389
+ }
390
+
391
+ function renderAppBundleCompileScript() {
392
+ return renderNestedBundleShellScript("compile", "scripts/check.sh");
393
+ }
394
+
395
+ function renderAppBundleDeployCheckScript() {
396
+ return renderNestedBundleShellScript("deploy", "scripts/deploy-check.sh");
397
+ }
398
+
399
+ function noopBundle(name, message) {
400
+ return {
401
+ "README.md": `# ${name}\n\n${message}\n`,
402
+ "scripts/check.sh": `#!/usr/bin/env bash\nset -euo pipefail\necho ${JSON.stringify(message)}\n`,
403
+ "scripts/smoke.sh": `#!/usr/bin/env bash\nset -euo pipefail\necho ${JSON.stringify(message)}\n`,
404
+ "scripts/deploy-check.sh": `#!/usr/bin/env bash\nset -euo pipefail\necho ${JSON.stringify(message)}\n`
405
+ };
406
+ }
407
+
408
+ export function generateAppBundle(graph, options = {}) {
409
+ const plan = buildAppBundlePlan(graph, options);
410
+ const topology = resolveRuntimeTopology(graph, options);
411
+ const projections = getDefaultEnvironmentProjections(graph, options);
412
+ plan.projections.dbPlatform = projections.dbProjection?.platform || null;
413
+ const fullStack = topology.apiComponents.length > 0 && topology.webComponents.length > 0 && topology.dbComponents.length > 0;
414
+ const envBundle = generateEnvironmentBundle(graph, { ...options, profileId: plan.profiles.environment });
415
+ const deployBundle = fullStack
416
+ ? generateDeploymentBundle(graph, { ...options, profileId: plan.profiles.deployment })
417
+ : noopBundle("Deployment Check", "No deployment bundle is generated for this partial topology.");
418
+ const smokeBundle = fullStack
419
+ ? generateRuntimeSmokeBundle(graph, options)
420
+ : noopBundle("Runtime Smoke", "No runtime smoke bundle is generated for this partial topology.");
421
+ const runtimeCheckBundle = fullStack
422
+ ? generateRuntimeCheckBundle(graph, options)
423
+ : noopBundle("Runtime Check", "No runtime check bundle is generated for this partial topology.");
424
+ const compileBundle = generateCompileCheckBundle(graph, options);
425
+
426
+ const files = {
427
+ ".env.example": renderAppBundleEnvExample(plan),
428
+ ".gitignore": "node_modules/\n.env\n**/node_modules/\n**/package-lock.json\n",
429
+ "README.md": renderAppBundleReadme(plan),
430
+ "package.json": renderAppBundlePackageJson(),
431
+ "app-bundle-plan.json": `${JSON.stringify(plan, null, 2)}\n`,
432
+ "scripts/load-env.sh": renderAppBundleLoadEnvScript(),
433
+ "scripts/bootstrap.sh": renderAppBundleBootstrapScript(),
434
+ "scripts/dev.sh": renderAppBundleDevScript(),
435
+ "scripts/runtime.sh": renderAppBundleRuntimeScript(),
436
+ "scripts/wait-for-stack.mjs": renderAppBundleWaitForStackScript(plan),
437
+ "scripts/compile-check.sh": renderAppBundleCompileScript(),
438
+ "scripts/runtime-check.sh": renderAppBundleRuntimeCheckScript(),
439
+ "scripts/smoke.sh": renderAppBundleSmokeScript(),
440
+ "scripts/deploy-check.sh": renderAppBundleDeployCheckScript()
441
+ };
442
+
443
+ mergeNamedBundles(files, {
444
+ apps: envBundle,
445
+ deploy: deployBundle,
446
+ smoke: smokeBundle,
447
+ "runtime-check": runtimeCheckBundle,
448
+ compile: compileBundle
449
+ });
450
+
451
+ return files;
452
+ }
453
+
454
+ export function generateAppBundlePlan(graph, options = {}) {
455
+ return buildAppBundlePlan(graph, options);
456
+ }
@@ -0,0 +1,166 @@
1
+ export function mergeBundleFiles(files, prefix, bundle) {
2
+ for (const [filePath, contents] of Object.entries(bundle || {})) {
3
+ files[`${prefix}/${filePath}`] = contents;
4
+ }
5
+ }
6
+
7
+ export function mergeNamedBundles(files, bundles) {
8
+ for (const [prefix, bundle] of Object.entries(bundles || {})) {
9
+ mergeBundleFiles(files, prefix, bundle);
10
+ }
11
+ }
12
+
13
+ export function renderLoadEnvScript(options = {}) {
14
+ const searchParentEnv = Boolean(options.searchParentEnv);
15
+ const rootDir = options.rootDirExpression || 'ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"';
16
+ const base = [
17
+ "#!/usr/bin/env bash",
18
+ "set -euo pipefail",
19
+ "",
20
+ rootDir,
21
+ 'DEFAULT_ENV_FILE="$ROOT_DIR/.env"',
22
+ 'DEFAULT_ENV_EXAMPLE_FILE="$ROOT_DIR/.env.example"'
23
+ ];
24
+ if (searchParentEnv) {
25
+ base.push(
26
+ 'FALLBACK_ENV_FILE="$(cd "$ROOT_DIR/.." && pwd)/.env"',
27
+ 'FALLBACK_ENV_EXAMPLE_FILE="$(cd "$ROOT_DIR/.." && pwd)/.env.example"'
28
+ );
29
+ }
30
+ base.push('ENV_FILE="${TOPOGRAM_ENV_FILE:-$DEFAULT_ENV_FILE}"', "");
31
+ if (searchParentEnv) {
32
+ base.push(
33
+ 'if [[ ! -f "$ENV_FILE" && -z "${TOPOGRAM_ENV_FILE:-}" && -f "$FALLBACK_ENV_FILE" ]]; then',
34
+ ' ENV_FILE="$FALLBACK_ENV_FILE"',
35
+ "fi",
36
+ ""
37
+ );
38
+ }
39
+ base.push(
40
+ 'if [[ ! -f "$ENV_FILE" && -z "${TOPOGRAM_ENV_FILE:-}" && -f "$DEFAULT_ENV_EXAMPLE_FILE" ]]; then',
41
+ ' ENV_FILE="$DEFAULT_ENV_EXAMPLE_FILE"',
42
+ "fi",
43
+ ""
44
+ );
45
+ if (searchParentEnv) {
46
+ base.push(
47
+ 'if [[ ! -f "$ENV_FILE" && -z "${TOPOGRAM_ENV_FILE:-}" && -f "$FALLBACK_ENV_EXAMPLE_FILE" ]]; then',
48
+ ' ENV_FILE="$FALLBACK_ENV_EXAMPLE_FILE"',
49
+ "fi",
50
+ ""
51
+ );
52
+ }
53
+ base.push(
54
+ 'if [[ -f "$ENV_FILE" ]]; then',
55
+ " set -a",
56
+ ' . "$ENV_FILE"',
57
+ " set +a",
58
+ "fi",
59
+ "",
60
+ 'if [[ -n "${DATABASE_URL:-}" ]]; then',
61
+ ' if [[ "$DATABASE_URL" == file:* ]]; then',
62
+ ' database_path="${DATABASE_URL#file:}"',
63
+ ' if [[ "$database_path" != /* ]]; then',
64
+ ` DATABASE_URL="file:$ROOT_DIR/$(printf '%s' "$database_path" | sed 's#^./##')"`,
65
+ " export DATABASE_URL",
66
+ " fi",
67
+ ' elif [[ "$DATABASE_URL" != /* && "$DATABASE_URL" != postgresql:* ]]; then',
68
+ ` DATABASE_URL="$ROOT_DIR/$(printf '%s' "$DATABASE_URL" | sed 's#^./##')"`,
69
+ " export DATABASE_URL",
70
+ " fi",
71
+ "fi",
72
+ ""
73
+ );
74
+ return base.join("\n");
75
+ }
76
+
77
+ export function renderEnvAwareShellScript(bodyLines, options = {}) {
78
+ const rootDir = options.rootDirExpression || 'ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"';
79
+ const loadEnvScript = options.loadEnvScript || '"$SCRIPT_DIR/load-env.sh"';
80
+ const lines = [
81
+ "#!/usr/bin/env bash",
82
+ "set -euo pipefail",
83
+ "",
84
+ 'SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"',
85
+ rootDir,
86
+ `. ${loadEnvScript}`,
87
+ "",
88
+ ...(bodyLines || [])
89
+ ];
90
+ return `${lines.join("\n")}\n`;
91
+ }
92
+
93
+ export function renderRootShellScript(bodyLines, options = {}) {
94
+ const includeScriptDir = options.includeScriptDir !== false;
95
+ const blankLineAfterRoot = options.blankLineAfterRoot !== false;
96
+ const rootDir = options.rootDirExpression || (includeScriptDir
97
+ ? 'ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"'
98
+ : 'ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"');
99
+ const lines = [
100
+ "#!/usr/bin/env bash",
101
+ "set -euo pipefail",
102
+ ""
103
+ ];
104
+ if (includeScriptDir) {
105
+ lines.push('SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"');
106
+ }
107
+ lines.push(rootDir);
108
+ if (blankLineAfterRoot) {
109
+ lines.push("");
110
+ }
111
+ lines.push(...(bodyLines || []));
112
+ return `${lines.join("\n")}\n`;
113
+ }
114
+
115
+ export function renderRootEnvFileShellScript(bodyLines, options = {}) {
116
+ return renderRootShellScript([
117
+ 'ENV_FILE="${TOPOGRAM_ENV_FILE:-$ROOT_DIR/.env}"',
118
+ "",
119
+ 'if [[ -f "$ENV_FILE" ]]; then',
120
+ " set -a",
121
+ ' . "$ENV_FILE"',
122
+ " set +a",
123
+ "fi",
124
+ "",
125
+ ...(bodyLines || [])
126
+ ], options);
127
+ }
128
+
129
+ export function renderNestedBundleShellScript(bundleDir, scriptPath, options = {}) {
130
+ return renderEnvAwareShellScript([`(cd "$ROOT_DIR/${bundleDir}" && bash ./${scriptPath})`], options);
131
+ }
132
+
133
+ export function renderNodeScriptRunner(scriptFileName, options = {}) {
134
+ const searchParentEnv = Boolean(options.searchParentEnv);
135
+ const lines = [
136
+ "#!/usr/bin/env bash",
137
+ "set -euo pipefail",
138
+ "",
139
+ 'SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"',
140
+ 'ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"',
141
+ 'DEFAULT_ENV_FILE="$ROOT_DIR/.env"'
142
+ ];
143
+ if (searchParentEnv) {
144
+ lines.push('FALLBACK_ENV_FILE="$(cd "$ROOT_DIR/.." && pwd)/.env"');
145
+ }
146
+ lines.push('ENV_FILE="${TOPOGRAM_ENV_FILE:-$DEFAULT_ENV_FILE}"', "");
147
+ if (searchParentEnv) {
148
+ lines.push(
149
+ 'if [[ ! -f "$ENV_FILE" && -z "${TOPOGRAM_ENV_FILE:-}" && -f "$FALLBACK_ENV_FILE" ]]; then',
150
+ ' ENV_FILE="$FALLBACK_ENV_FILE"',
151
+ "fi",
152
+ ""
153
+ );
154
+ }
155
+ lines.push(
156
+ 'if [[ -f "$ENV_FILE" ]]; then',
157
+ " set -a",
158
+ ' . "$ENV_FILE"',
159
+ " set +a",
160
+ "fi",
161
+ "",
162
+ `node "$SCRIPT_DIR/${scriptFileName}"`,
163
+ ""
164
+ );
165
+ return lines.join("\n");
166
+ }