@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,612 @@
1
+ import {
2
+ buildDbProjectionContract,
3
+ dbProfileForProjection,
4
+ dbProjectionCandidates,
5
+ generatorDefaultsMap,
6
+ getProjection
7
+ } from "./shared.js";
8
+ import { normalizeDbSchemaSnapshot } from "./snapshot.js";
9
+ import { generatePostgresDrizzleSchema } from "./postgres/drizzle.js";
10
+ import { generatePostgresPrismaSchema } from "./postgres/prisma.js";
11
+ import { generateSqlitePrismaSchema } from "./sqlite/prisma.js";
12
+
13
+ function defaultInputPathForGraph(graph, options = {}) {
14
+ if (options.topogramInputPath) {
15
+ return options.topogramInputPath;
16
+ }
17
+ return ".";
18
+ }
19
+
20
+ function dbLifecyclePlan(graph, projection, options = {}) {
21
+ const contract = buildDbProjectionContract(graph, projection);
22
+ const snapshot = normalizeDbSchemaSnapshot(contract);
23
+ const engine = snapshot.engine;
24
+ const ormProfiles = engine === "postgres" ? ["prisma", "drizzle"] : ["prisma"];
25
+
26
+ return {
27
+ type: "db_lifecycle_plan",
28
+ projection: snapshot.projection,
29
+ engine,
30
+ dbProfile: contract.profile,
31
+ ormProfiles,
32
+ runtimeProfile: ormProfiles.includes("prisma") ? "prisma" : null,
33
+ inputPath: defaultInputPathForGraph(graph, options),
34
+ state: {
35
+ currentSnapshot: "state/current.snapshot.json",
36
+ desiredSnapshot: "state/desired.snapshot.json",
37
+ migrationPlan: "state/migration.plan.json",
38
+ migrationSql: "state/migration.sql"
39
+ },
40
+ bundle: {
41
+ emptySnapshot: "snapshots/empty.snapshot.json",
42
+ prismaSchema: ormProfiles.includes("prisma") ? "prisma/schema.prisma" : null,
43
+ drizzleSchema: engine === "postgres" ? "drizzle/schema.ts" : null
44
+ },
45
+ environment: {
46
+ required: ["DATABASE_URL"],
47
+ optional:
48
+ engine === "postgres"
49
+ ? ["DATABASE_ADMIN_URL", "TOPOGRAM_BIN", "TOPOGRAM_INPUT_PATH", "TOPOGRAM_DB_STATE_DIR"]
50
+ : ["TOPOGRAM_BIN", "TOPOGRAM_INPUT_PATH", "TOPOGRAM_DB_STATE_DIR"]
51
+ },
52
+ greenfield: {
53
+ detection: "missing current snapshot",
54
+ provisioning:
55
+ engine === "postgres"
56
+ ? "create database when DATABASE_ADMIN_URL is available, otherwise assume DATABASE_URL already points at a provisioned database"
57
+ : "create sqlite file if missing",
58
+ setup: [
59
+ "generate desired schema snapshot",
60
+ "generate initial SQL migration from the bundled empty snapshot",
61
+ "apply SQL to the target database",
62
+ ...(ormProfiles.includes("prisma") ? ["run prisma generate against the bundled schema"] : []),
63
+ "persist desired snapshot as current"
64
+ ]
65
+ },
66
+ brownfield: {
67
+ detection: "existing current snapshot",
68
+ setup: [
69
+ "generate desired schema snapshot",
70
+ "diff current snapshot against desired snapshot",
71
+ "stop if manual intervention is required",
72
+ "generate additive SQL migration",
73
+ "apply SQL to the target database",
74
+ ...(ormProfiles.includes("prisma") ? ["run prisma generate against the bundled schema"] : []),
75
+ "persist desired snapshot as current"
76
+ ],
77
+ safety: "manual migration required plans are never auto-applied"
78
+ }
79
+ };
80
+ }
81
+
82
+ function renderEmptySnapshotForProjection(projection) {
83
+ const engine = projection.platform === "db_sqlite" ? "sqlite" : "postgres";
84
+ return {
85
+ type: "db_schema_snapshot",
86
+ projection: {
87
+ id: projection.id,
88
+ name: projection.name || projection.id,
89
+ platform: projection.platform
90
+ },
91
+ profile: dbProfileForProjection(projection),
92
+ generatorDefaults: generatorDefaultsMap(projection),
93
+ engine,
94
+ enums: [],
95
+ tables: []
96
+ };
97
+ }
98
+
99
+ function renderDbLifecycleEnvExample(projection, plan) {
100
+ const engine = projection.platform === "db_sqlite" ? "sqlite" : "postgres";
101
+ const inputPath = "../../../../topogram";
102
+ if (engine === "sqlite") {
103
+ return `DATABASE_URL=file:./var/${projection.id}.sqlite\nTOPOGRAM_INPUT_PATH=${inputPath}\n`;
104
+ }
105
+
106
+ return `POSTGRES_USER=\${USER:-postgres}\nDATABASE_URL=postgresql://\${POSTGRES_USER}@localhost:5432/${projection.id}\nDATABASE_ADMIN_URL=postgresql://\${POSTGRES_USER}@localhost:5432/postgres\nTOPOGRAM_INPUT_PATH=${inputPath}\n`;
107
+ }
108
+
109
+ function renderDbLifecycleReadme(plan) {
110
+ return `# ${plan.projection.name} Lifecycle
111
+
112
+ This bundle gives agents a repeatable database workflow for projection \`${plan.projection.id}\`.
113
+
114
+ ## Modes
115
+
116
+ - Greenfield: run \`./scripts/db-bootstrap-or-migrate.sh\` with no current snapshot
117
+ - Brownfield: run \`./scripts/db-bootstrap-or-migrate.sh\` with \`state/current.snapshot.json\` already present
118
+
119
+ ## Required Environment
120
+
121
+ ${plan.environment.required.map((name) => `- \`${name}\``).join("\n")}
122
+
123
+ ## Optional Environment
124
+
125
+ ${plan.environment.optional.map((name) => `- \`${name}\``).join("\n")}
126
+
127
+ ## Files
128
+
129
+ - Desired snapshot: \`${plan.state.desiredSnapshot}\`
130
+ - Current snapshot: \`${plan.state.currentSnapshot}\`
131
+ - Migration plan: \`${plan.state.migrationPlan}\`
132
+ - Migration SQL: \`${plan.state.migrationSql}\`
133
+
134
+ ## Commands
135
+
136
+ - \`./scripts/db-status.sh\`
137
+ - \`./scripts/db-bootstrap.sh\`
138
+ - \`./scripts/db-migrate.sh\`
139
+ - \`./scripts/db-bootstrap-or-migrate.sh\`
140
+ `;
141
+ }
142
+
143
+ function renderDbLifecycleCommonScript(plan) {
144
+ const engine = plan.engine;
145
+ return `#!/usr/bin/env bash
146
+ set -euo pipefail
147
+
148
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
149
+ BUNDLE_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
150
+ resolve_path_candidate() {
151
+ local candidate="$1"
152
+ local base_dir="$2"
153
+ local resolved=""
154
+ if [[ -z "$candidate" ]]; then
155
+ return 1
156
+ fi
157
+ if [[ "$candidate" = /* ]]; then
158
+ resolved="$candidate"
159
+ else
160
+ resolved="$(cd "$base_dir" && cd "$candidate" 2>/dev/null && pwd)" || return 1
161
+ fi
162
+ printf '%s\\n' "$resolved"
163
+ }
164
+ find_topogram_bin() {
165
+ if [[ -n "\${TOPOGRAM_BIN:-}" ]]; then
166
+ printf '%s\\n' "$TOPOGRAM_BIN"
167
+ return
168
+ fi
169
+ local candidates=(
170
+ "$BUNDLE_DIR/../../../../node_modules/.bin/topogram"
171
+ "$BUNDLE_DIR/../../../node_modules/.bin/topogram"
172
+ "$PWD/node_modules/.bin/topogram"
173
+ )
174
+ local candidate
175
+ for candidate in "\${candidates[@]}"; do
176
+ if [[ -x "$candidate" ]]; then
177
+ printf '%s\\n' "$candidate"
178
+ return
179
+ fi
180
+ done
181
+ if command -v topogram >/dev/null 2>&1; then
182
+ command -v topogram
183
+ return
184
+ fi
185
+ echo "Unable to locate the Topogram CLI. Install @topogram/cli or set TOPOGRAM_BIN." >&2
186
+ exit 1
187
+ }
188
+ discover_input_path() {
189
+ if [[ -n "\${TOPOGRAM_INPUT_PATH:-}" ]]; then
190
+ local resolved=""
191
+ if resolved="$(resolve_path_candidate "$TOPOGRAM_INPUT_PATH" "$PWD")"; then
192
+ printf '%s\\n' "$resolved"
193
+ return
194
+ fi
195
+ if resolved="$(resolve_path_candidate "$TOPOGRAM_INPUT_PATH" "$BUNDLE_DIR")"; then
196
+ printf '%s\\n' "$resolved"
197
+ return
198
+ fi
199
+ echo "TOPOGRAM_INPUT_PATH is set but cannot be resolved: $TOPOGRAM_INPUT_PATH" >&2
200
+ exit 1
201
+ fi
202
+ local resolved=""
203
+ local candidates=(
204
+ "$BUNDLE_DIR/../../../.."
205
+ "$BUNDLE_DIR/../../.."
206
+ "$BUNDLE_DIR/../../../../.."
207
+ "$PWD"
208
+ )
209
+ local candidate
210
+ for candidate in "\${candidates[@]}"; do
211
+ if resolved="$(resolve_path_candidate "$candidate" "$PWD")" && [[ -d "$resolved/topogram" ]]; then
212
+ printf '%s\\n' "$resolved/topogram"
213
+ return
214
+ fi
215
+ done
216
+ echo "Unable to locate a Topogram workspace. Set TOPOGRAM_INPUT_PATH." >&2
217
+ exit 1
218
+ }
219
+ TOPOGRAM_BIN="$(find_topogram_bin)"
220
+ INPUT_PATH="$(discover_input_path)"
221
+ PROJECTION_ID="${plan.projection.id}"
222
+ STATE_DIR="\${TOPOGRAM_DB_STATE_DIR:-$BUNDLE_DIR/state}"
223
+ CURRENT_SNAPSHOT="$STATE_DIR/current.snapshot.json"
224
+ DESIRED_SNAPSHOT="$STATE_DIR/desired.snapshot.json"
225
+ PLAN_JSON="$STATE_DIR/migration.plan.json"
226
+ MIGRATION_SQL="$STATE_DIR/migration.sql"
227
+ EMPTY_SNAPSHOT="$BUNDLE_DIR/${plan.bundle.emptySnapshot}"
228
+ PRISMA_SCHEMA="${plan.bundle.prismaSchema ? `$BUNDLE_DIR/${plan.bundle.prismaSchema}` : ""}"
229
+
230
+ mkdir -p "$STATE_DIR"
231
+
232
+ require_env() {
233
+ local name="$1"
234
+ if [[ -z "\${!name:-}" ]]; then
235
+ echo "Missing required environment variable: $name" >&2
236
+ exit 1
237
+ fi
238
+ }
239
+
240
+ ${engine === "postgres" ? `postgres_cli_url() {
241
+ require_env DATABASE_URL
242
+ node --input-type=module -e 'const url = new URL(process.argv[1]); url.searchParams.delete("schema"); console.log(url.toString());' "$DATABASE_URL"
243
+ }
244
+
245
+ postgres_admin_cli_url() {
246
+ require_env DATABASE_ADMIN_URL
247
+ node --input-type=module -e 'const url = new URL(process.argv[1]); url.searchParams.delete("schema"); console.log(url.toString());' "$DATABASE_ADMIN_URL"
248
+ }
249
+ ` : `normalize_sqlite_database_url() {
250
+ require_env DATABASE_URL
251
+ local base_dir
252
+ base_dir="$(cd "$BUNDLE_DIR/../.." && pwd)"
253
+ local database_path="$DATABASE_URL"
254
+ if [[ "$database_path" == file:* ]]; then
255
+ database_path="\${database_path#file:}"
256
+ fi
257
+ if [[ "$database_path" != /* ]]; then
258
+ database_path="$base_dir/$(printf '%s' "$database_path" | sed 's#^\./##')"
259
+ fi
260
+ mkdir -p "$(dirname "$database_path")"
261
+ export DATABASE_URL="file:$database_path"
262
+ }
263
+
264
+ sqlite_cli_database_path() {
265
+ normalize_sqlite_database_url
266
+ printf '%s\n' "\${DATABASE_URL#file:}"
267
+ }
268
+ `}
269
+
270
+ ensure_db_cli_on_path() {
271
+ ${engine === "postgres" ? ` if command -v psql >/dev/null 2>&1; then
272
+ return
273
+ fi
274
+ local candidates=(
275
+ "/opt/homebrew/opt/postgresql@18/bin"
276
+ "/opt/homebrew/opt/postgresql@17/bin"
277
+ "/opt/homebrew/opt/postgresql@16/bin"
278
+ "/usr/local/opt/postgresql@18/bin"
279
+ "/usr/local/opt/postgresql@17/bin"
280
+ "/usr/local/opt/postgresql@16/bin"
281
+ )
282
+ local candidate
283
+ for candidate in "\${candidates[@]}"; do
284
+ if [[ -x "$candidate/psql" ]]; then
285
+ export PATH="$candidate:$PATH"
286
+ return
287
+ fi
288
+ done
289
+ if command -v brew >/dev/null 2>&1; then
290
+ local prefix=""
291
+ prefix="$(brew --prefix postgresql@18 2>/dev/null || true)"
292
+ if [[ -n "$prefix" && -x "$prefix/bin/psql" ]]; then
293
+ export PATH="$prefix/bin:$PATH"
294
+ return
295
+ fi
296
+ prefix="$(brew --prefix postgresql@17 2>/dev/null || true)"
297
+ if [[ -n "$prefix" && -x "$prefix/bin/psql" ]]; then
298
+ export PATH="$prefix/bin:$PATH"
299
+ return
300
+ fi
301
+ prefix="$(brew --prefix postgresql@16 2>/dev/null || true)"
302
+ if [[ -n "$prefix" && -x "$prefix/bin/psql" ]]; then
303
+ export PATH="$prefix/bin:$PATH"
304
+ return
305
+ fi
306
+ fi
307
+ echo "Unable to locate psql. Install PostgreSQL or add it to PATH." >&2
308
+ exit 1` : ` if command -v sqlite3 >/dev/null 2>&1; then
309
+ return
310
+ fi
311
+ echo "Unable to locate sqlite3. Install SQLite or add it to PATH." >&2
312
+ exit 1`}
313
+ }
314
+
315
+ list_live_tables_json() {
316
+ require_env DATABASE_URL
317
+ ensure_db_cli_on_path
318
+ ${engine === "postgres" ? ` local cli_url
319
+ cli_url="$(postgres_cli_url)"
320
+ psql "$cli_url" -Atqc "select coalesce(json_agg(tablename order by tablename)::text, '[]') from pg_tables where schemaname = 'public'"` : ` normalize_sqlite_database_url
321
+ local database_path
322
+ database_path="$(sqlite_cli_database_path)"
323
+ if [[ ! -f "$database_path" ]]; then
324
+ printf '[]\\n'
325
+ return
326
+ fi
327
+ sqlite3 "$database_path" "select json_group_array(name) from (select name from sqlite_master where type = 'table' and name not like 'sqlite_%' order by name)" | sed 's/^$/[]/'`}
328
+ }
329
+
330
+ reconcile_existing_database_snapshot() {
331
+ generate_desired_snapshot
332
+ local live_tables
333
+ live_tables="$(list_live_tables_json)"
334
+ node --input-type=module -e 'import fs from "node:fs"; const live = JSON.parse(process.argv[1] || "[]"); const desired = JSON.parse(fs.readFileSync(process.argv[2], "utf8")).tables.map((table) => table.table).sort(); const actual = [...live].sort(); if (actual.length === 0) process.exit(1); if (JSON.stringify(actual) === JSON.stringify(desired)) process.exit(0); console.error("Existing database tables do not match the desired Topogram schema."); console.error(JSON.stringify({ actual, desired }, null, 2)); process.exit(2);' "$live_tables" "$DESIRED_SNAPSHOT"
335
+ }
336
+
337
+ current_snapshot_matches_live_database() {
338
+ if [[ ! -f "$CURRENT_SNAPSHOT" ]]; then
339
+ return 1
340
+ fi
341
+ local live_tables
342
+ live_tables="$(list_live_tables_json)"
343
+ node --input-type=module -e 'import fs from "node:fs"; const live = [...new Set(JSON.parse(process.argv[1] || "[]"))].sort(); const current = JSON.parse(fs.readFileSync(process.argv[2], "utf8")).tables.map((table) => table.table).sort(); process.exit(JSON.stringify(live) === JSON.stringify(current) ? 0 : 1);' "$live_tables" "$CURRENT_SNAPSHOT"
344
+ }
345
+
346
+ infer_current_snapshot_from_live_tables() {
347
+ generate_desired_snapshot
348
+ local live_tables
349
+ live_tables="$(list_live_tables_json)"
350
+ node --input-type=module -e 'import fs from "node:fs"; const live = [...new Set(JSON.parse(process.argv[1] || "[]"))].sort(); const desiredSnapshotPath = process.argv[2]; const outputPath = process.argv[3]; const desiredSnapshot = JSON.parse(fs.readFileSync(desiredSnapshotPath, "utf8")); const desiredTables = new Map((desiredSnapshot.tables || []).map((table) => [table.table, table])); if (live.length === 0) process.exit(1); const unknown = live.filter((name) => !desiredTables.has(name)); if (unknown.length > 0) process.exit(2); const inferredTables = live.map((name) => desiredTables.get(name)).filter(Boolean); const inferredSnapshot = { ...desiredSnapshot, tables: inferredTables }; fs.writeFileSync(outputPath, JSON.stringify(inferredSnapshot, null, 2) + "\\n", "utf8"); process.exit(inferredTables.length === desiredSnapshot.tables.length ? 0 : 3);' "$live_tables" "$DESIRED_SNAPSHOT" "$CURRENT_SNAPSHOT"
351
+ }
352
+
353
+ generate_desired_snapshot() {
354
+ "$TOPOGRAM_BIN" "$INPUT_PATH" --generate db-schema-snapshot --projection "$PROJECTION_ID" > "$DESIRED_SNAPSHOT"
355
+ }
356
+
357
+ generate_migration_plan() {
358
+ local from_snapshot="$1"
359
+ "$TOPOGRAM_BIN" "$INPUT_PATH" --generate db-migration-plan --projection "$PROJECTION_ID" --from-snapshot "$from_snapshot" > "$PLAN_JSON"
360
+ }
361
+
362
+ generate_sql_migration() {
363
+ local from_snapshot="$1"
364
+ "$TOPOGRAM_BIN" "$INPUT_PATH" --generate sql-migration --projection "$PROJECTION_ID" --from-snapshot "$from_snapshot" > "$MIGRATION_SQL"
365
+ }
366
+
367
+ ensure_supported_plan() {
368
+ node --input-type=module -e 'import fs from "node:fs"; const plan = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); if (!plan.supported) { console.error("Manual migration required."); console.error(JSON.stringify(plan.manual, null, 2)); process.exit(2); }' "$PLAN_JSON"
369
+ }
370
+
371
+ provision_database_if_needed() {
372
+ require_env DATABASE_URL
373
+ ensure_db_cli_on_path
374
+ ${engine === "postgres" ? ` local cli_url
375
+ cli_url="$(postgres_cli_url)"
376
+ if psql "$cli_url" -c 'select 1' >/dev/null 2>&1; then
377
+ return
378
+ fi
379
+ if [[ -z "\${DATABASE_ADMIN_URL:-}" ]]; then
380
+ echo "DATABASE_URL is not reachable and DATABASE_ADMIN_URL was not provided." >&2
381
+ exit 1
382
+ fi
383
+ local admin_cli_url
384
+ admin_cli_url="$(postgres_admin_cli_url)"
385
+ local db_name
386
+ db_name="$(node --input-type=module -e 'const url = new URL(process.argv[1]); console.log(url.pathname.replace(/^\\//, ""));' "$DATABASE_URL")"
387
+ if ! psql "$admin_cli_url" -Atqc "select 1 from pg_database where datname = '$db_name'" | grep -q 1; then
388
+ psql "$admin_cli_url" -c "create database \\"$db_name\\""
389
+ fi` : ` local database_path
390
+ database_path="$(sqlite_cli_database_path)"
391
+ mkdir -p "$(dirname "$database_path")"`}
392
+ }
393
+
394
+ apply_sql() {
395
+ require_env DATABASE_URL
396
+ ensure_db_cli_on_path
397
+ ${engine === "postgres" ? ` local cli_url
398
+ cli_url="$(postgres_cli_url)"
399
+ psql "$cli_url" -v ON_ERROR_STOP=1 -f "$MIGRATION_SQL"` : ` local database_path
400
+ database_path="$(sqlite_cli_database_path)"
401
+ sqlite3 "$database_path" < "$MIGRATION_SQL"`}
402
+ }
403
+
404
+ refresh_runtime_clients() {
405
+ ${plan.bundle.prismaSchema ? ` if [[ "\${TOPOGRAM_SKIP_RUNTIME_CLIENT_REFRESH:-0}" == "1" ]]; then
406
+ return
407
+ fi
408
+ local prisma_version="5.22.0"
409
+ ${engine === "sqlite" ? ` normalize_sqlite_database_url
410
+ ` : ""} local runtime_server_dir="\${TOPOGRAM_RUNTIME_SERVER_DIR:-}"
411
+ if [[ -z "$runtime_server_dir" && -f "$BUNDLE_DIR/../server/package.json" ]]; then
412
+ runtime_server_dir="$BUNDLE_DIR/../server"
413
+ fi
414
+ if [[ -z "$runtime_server_dir" && -d "$BUNDLE_DIR/../../services" ]]; then
415
+ for candidate in "$BUNDLE_DIR"/../../services/*; do
416
+ if [[ -f "$candidate/package.json" ]]; then
417
+ runtime_server_dir="$candidate"
418
+ break
419
+ fi
420
+ done
421
+ fi
422
+ if [[ -n "$runtime_server_dir" && -f "$runtime_server_dir/package.json" ]]; then
423
+ (cd "$runtime_server_dir" && npm install && npm exec -- prisma db push --schema "$PRISMA_SCHEMA" --skip-generate)
424
+ return
425
+ fi
426
+ npx -p "prisma@$prisma_version" prisma db push --schema "$PRISMA_SCHEMA" --skip-generate` : ` :`}
427
+ }
428
+ `;
429
+ }
430
+
431
+ function renderDbStatusScript() {
432
+ return `#!/usr/bin/env bash
433
+ set -euo pipefail
434
+ . "$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)/db-common.sh"
435
+
436
+ generate_desired_snapshot
437
+
438
+ if [[ -f "$CURRENT_SNAPSHOT" ]]; then
439
+ generate_migration_plan "$CURRENT_SNAPSHOT"
440
+ cat "$PLAN_JSON"
441
+ else
442
+ echo '{"mode":"greenfield","currentSnapshot":null}'
443
+ fi
444
+ `;
445
+ }
446
+
447
+ function renderDbBootstrapScript() {
448
+ return `#!/usr/bin/env bash
449
+ set -euo pipefail
450
+ . "$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)/db-common.sh"
451
+
452
+ if [[ -f "$CURRENT_SNAPSHOT" ]]; then
453
+ echo "Current snapshot already exists at $CURRENT_SNAPSHOT. Run ./scripts/db-migrate.sh instead." >&2
454
+ exit 1
455
+ fi
456
+
457
+ if reconcile_existing_database_snapshot; then
458
+ refresh_runtime_clients
459
+ cp "$DESIRED_SNAPSHOT" "$CURRENT_SNAPSHOT"
460
+ echo "Existing database already matches desired schema. Recorded current snapshot."
461
+ exit 0
462
+ else
463
+ status=$?
464
+ if [[ "$status" -eq 2 ]]; then
465
+ if infer_current_snapshot_from_live_tables; then
466
+ echo "Existing database tables matched a subset of the desired Topogram schema. Recorded an inferred current snapshot."
467
+ exec bash "$SCRIPT_DIR/db-migrate.sh"
468
+ else
469
+ infer_status=$?
470
+ if [[ "$infer_status" -eq 3 ]]; then
471
+ echo "Existing database tables matched a subset of the desired Topogram schema. Recorded an inferred current snapshot."
472
+ exec bash "$SCRIPT_DIR/db-migrate.sh"
473
+ fi
474
+ echo "Existing database is not empty and does not match the desired Topogram schema." >&2
475
+ echo "Create or provide a matching current snapshot before running bootstrap." >&2
476
+ exit 1
477
+ fi
478
+ fi
479
+ fi
480
+
481
+ provision_database_if_needed
482
+ generate_desired_snapshot
483
+ generate_sql_migration "$EMPTY_SNAPSHOT"
484
+ apply_sql
485
+ refresh_runtime_clients
486
+ cp "$DESIRED_SNAPSHOT" "$CURRENT_SNAPSHOT"
487
+ echo "Greenfield bootstrap complete."
488
+ `;
489
+ }
490
+
491
+ function renderDbMigrateScript() {
492
+ return `#!/usr/bin/env bash
493
+ set -euo pipefail
494
+ . "$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)/db-common.sh"
495
+
496
+ if [[ ! -f "$CURRENT_SNAPSHOT" ]]; then
497
+ echo "No current snapshot found at $CURRENT_SNAPSHOT. Run ./scripts/db-bootstrap.sh instead." >&2
498
+ exit 1
499
+ fi
500
+
501
+ if ! current_snapshot_matches_live_database; then
502
+ if infer_current_snapshot_from_live_tables; then
503
+ echo "Current snapshot did not match the live database. Replaced it with an inferred snapshot from live tables."
504
+ else
505
+ infer_status=$?
506
+ if [[ "$infer_status" -eq 3 ]]; then
507
+ echo "Current snapshot did not match the live database. Replaced it with an inferred snapshot from live tables."
508
+ else
509
+ echo "Current snapshot does not match the live database and could not be inferred safely." >&2
510
+ exit 1
511
+ fi
512
+ fi
513
+ fi
514
+
515
+ generate_desired_snapshot
516
+ generate_migration_plan "$CURRENT_SNAPSHOT"
517
+ ensure_supported_plan
518
+ generate_sql_migration "$CURRENT_SNAPSHOT"
519
+ apply_sql
520
+ refresh_runtime_clients
521
+ cp "$DESIRED_SNAPSHOT" "$CURRENT_SNAPSHOT"
522
+ echo "Brownfield migration complete."
523
+ `;
524
+ }
525
+
526
+ function renderDbBootstrapOrMigrateScript() {
527
+ return `#!/usr/bin/env bash
528
+ set -euo pipefail
529
+
530
+ SCRIPT_DIR="$(cd "$(dirname "\${BASH_SOURCE[0]}")" && pwd)"
531
+ STATE_DIR="\${TOPOGRAM_DB_STATE_DIR:-$(cd "$SCRIPT_DIR/../state" && pwd)}"
532
+ CURRENT_SNAPSHOT="$STATE_DIR/current.snapshot.json"
533
+
534
+ if [[ -f "$CURRENT_SNAPSHOT" ]]; then
535
+ exec bash "$SCRIPT_DIR/db-migrate.sh"
536
+ fi
537
+
538
+ . "$SCRIPT_DIR/db-common.sh"
539
+ if reconcile_existing_database_snapshot; then
540
+ refresh_runtime_clients
541
+ cp "$DESIRED_SNAPSHOT" "$CURRENT_SNAPSHOT"
542
+ echo "Existing database already matches desired schema. Recorded current snapshot."
543
+ exit 0
544
+ else
545
+ status=$?
546
+ if [[ "$status" -eq 2 ]]; then
547
+ if infer_current_snapshot_from_live_tables; then
548
+ echo "Existing database tables matched a subset of the desired Topogram schema. Recorded an inferred current snapshot."
549
+ exec bash "$SCRIPT_DIR/db-migrate.sh"
550
+ else
551
+ infer_status=$?
552
+ if [[ "$infer_status" -eq 3 ]]; then
553
+ echo "Existing database tables matched a subset of the desired Topogram schema. Recorded an inferred current snapshot."
554
+ exec bash "$SCRIPT_DIR/db-migrate.sh"
555
+ fi
556
+ echo "Existing database is not empty and does not match the desired Topogram schema." >&2
557
+ echo "Create or provide a matching current snapshot before running migrations." >&2
558
+ exit 1
559
+ fi
560
+ fi
561
+ fi
562
+
563
+ exec bash "$SCRIPT_DIR/db-bootstrap.sh"
564
+ `;
565
+ }
566
+
567
+ function generateDbLifecycleBundle(graph, projection, options = {}) {
568
+ const plan = dbLifecyclePlan(graph, projection, options);
569
+ const files = {
570
+ "README.md": renderDbLifecycleReadme(plan),
571
+ ".env.example": renderDbLifecycleEnvExample(projection, plan),
572
+ "scripts/db-common.sh": renderDbLifecycleCommonScript(plan),
573
+ "scripts/db-status.sh": renderDbStatusScript(),
574
+ "scripts/db-bootstrap.sh": renderDbBootstrapScript(),
575
+ "scripts/db-migrate.sh": renderDbMigrateScript(),
576
+ "scripts/db-bootstrap-or-migrate.sh": renderDbBootstrapOrMigrateScript(),
577
+ "snapshots/empty.snapshot.json": `${JSON.stringify(renderEmptySnapshotForProjection(projection), null, 2)}\n`,
578
+ "state/.gitkeep": ""
579
+ };
580
+
581
+ if (plan.bundle.prismaSchema) {
582
+ files[plan.bundle.prismaSchema] =
583
+ projection.platform === "db_sqlite"
584
+ ? generateSqlitePrismaSchema(graph, { projectionId: projection.id })
585
+ : generatePostgresPrismaSchema(graph, { projectionId: projection.id });
586
+ }
587
+ if (plan.bundle.drizzleSchema) {
588
+ files[plan.bundle.drizzleSchema] = generatePostgresDrizzleSchema(graph, { projectionId: projection.id });
589
+ }
590
+
591
+ return files;
592
+ }
593
+
594
+ export function generateDbLifecyclePlanForProjection(graph, projection, options = {}) {
595
+ return dbLifecyclePlan(graph, projection, options);
596
+ }
597
+
598
+ export function generateDbLifecyclePlan(graph, options = {}) {
599
+ if (options.projectionId) {
600
+ return dbLifecyclePlan(graph, getProjection(graph, options.projectionId), options);
601
+ }
602
+
603
+ const output = {};
604
+ for (const projection of dbProjectionCandidates(graph)) {
605
+ output[projection.id] = dbLifecyclePlan(graph, projection, options);
606
+ }
607
+ return output;
608
+ }
609
+
610
+ export function generateDbLifecycleBundleForProjection(graph, projection, options = {}) {
611
+ return generateDbLifecycleBundle(graph, projection, options);
612
+ }