@msn-control/liftoff 0.10.3 → 0.11.1

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 (656) hide show
  1. package/DEVELOPER.md +364 -20
  2. package/README.md +29 -29
  3. package/assets/governance/single-maintainer-gitflow/assessment-controls.json +39 -0
  4. package/assets/supported-stack.json +7 -138
  5. package/dist/adapters/azure/governance-assessment.d.ts +2 -0
  6. package/dist/adapters/azure/governance-assessment.js +21 -0
  7. package/dist/adapters/azure/governance-assessment.js.map +1 -0
  8. package/dist/adapters/filesystem/atomic-write.d.ts +2 -0
  9. package/dist/adapters/filesystem/atomic-write.js +74 -0
  10. package/dist/adapters/filesystem/atomic-write.js.map +1 -0
  11. package/dist/adapters/filesystem/errors.d.ts +2 -0
  12. package/dist/adapters/filesystem/errors.js +8 -0
  13. package/dist/adapters/filesystem/errors.js.map +1 -0
  14. package/dist/adapters/filesystem/manifest-file.d.ts +1 -0
  15. package/dist/adapters/filesystem/manifest-file.js +21 -0
  16. package/dist/adapters/filesystem/manifest-file.js.map +1 -0
  17. package/dist/adapters/filesystem/project-config.d.ts +4 -0
  18. package/dist/adapters/filesystem/project-config.js +157 -0
  19. package/dist/adapters/filesystem/project-config.js.map +1 -0
  20. package/dist/adapters/filesystem/project-discovery.d.ts +2 -0
  21. package/dist/adapters/filesystem/project-discovery.js +30 -0
  22. package/dist/adapters/filesystem/project-discovery.js.map +1 -0
  23. package/dist/adapters/filesystem/project-files.d.ts +6 -0
  24. package/dist/adapters/filesystem/project-files.js +68 -0
  25. package/dist/adapters/filesystem/project-files.js.map +1 -0
  26. package/dist/adapters/filesystem/project-lock.d.ts +8 -0
  27. package/dist/adapters/filesystem/project-lock.js +164 -0
  28. package/dist/adapters/filesystem/project-lock.js.map +1 -0
  29. package/dist/adapters/filesystem/project-paths.d.ts +2 -0
  30. package/dist/adapters/filesystem/project-paths.js +60 -0
  31. package/dist/adapters/filesystem/project-paths.js.map +1 -0
  32. package/dist/adapters/filesystem/project-transaction.d.ts +23 -0
  33. package/dist/adapters/filesystem/project-transaction.js +158 -0
  34. package/dist/adapters/filesystem/project-transaction.js.map +1 -0
  35. package/dist/adapters/git/governance-assessment.d.ts +22 -0
  36. package/dist/adapters/git/governance-assessment.js +161 -0
  37. package/dist/adapters/git/governance-assessment.js.map +1 -0
  38. package/dist/adapters/github/governance-assessment.d.ts +2 -0
  39. package/dist/adapters/github/governance-assessment.js +18 -0
  40. package/dist/adapters/github/governance-assessment.js.map +1 -0
  41. package/dist/adapters/packaged-assets/package-root.d.ts +3 -0
  42. package/dist/adapters/packaged-assets/package-root.js +21 -0
  43. package/dist/adapters/packaged-assets/package-root.js.map +1 -0
  44. package/dist/adapters/packaged-assets/supported-stack.d.ts +4 -0
  45. package/dist/adapters/packaged-assets/supported-stack.js +18 -0
  46. package/dist/adapters/packaged-assets/supported-stack.js.map +1 -0
  47. package/dist/adapters/packaged-assets/template-assets.d.ts +23 -0
  48. package/dist/adapters/packaged-assets/template-assets.js +36 -0
  49. package/dist/adapters/packaged-assets/template-assets.js.map +1 -0
  50. package/dist/adapters/process/shell-command.d.ts +5 -0
  51. package/dist/adapters/process/shell-command.js +21 -0
  52. package/dist/adapters/process/shell-command.js.map +1 -0
  53. package/dist/application/context.d.ts +20 -0
  54. package/dist/application/context.js +2 -0
  55. package/dist/application/context.js.map +1 -0
  56. package/dist/application/diagnose/doctor.d.ts +21 -0
  57. package/dist/application/diagnose/doctor.js +586 -0
  58. package/dist/application/diagnose/doctor.js.map +1 -0
  59. package/dist/application/diagnose/generated-project.d.ts +1 -0
  60. package/dist/application/diagnose/generated-project.js +105 -0
  61. package/dist/application/diagnose/generated-project.js.map +1 -0
  62. package/dist/application/diagnose/validate.d.ts +6 -0
  63. package/dist/application/diagnose/validate.js +27 -0
  64. package/dist/application/diagnose/validate.js.map +1 -0
  65. package/dist/application/initialize/fixture.d.ts +2 -0
  66. package/dist/application/initialize/fixture.js +39 -0
  67. package/dist/application/initialize/fixture.js.map +1 -0
  68. package/dist/application/initialize/inputs.d.ts +3 -0
  69. package/dist/application/initialize/inputs.js +35 -0
  70. package/dist/application/initialize/inputs.js.map +1 -0
  71. package/dist/application/initialize/use-case.d.ts +23 -0
  72. package/dist/application/initialize/use-case.js +360 -0
  73. package/dist/application/initialize/use-case.js.map +1 -0
  74. package/dist/application/migrate/use-case.d.ts +7 -0
  75. package/dist/application/migrate/use-case.js +378 -0
  76. package/dist/application/migrate/use-case.js.map +1 -0
  77. package/dist/application/project/catalog.d.ts +46 -0
  78. package/dist/application/project/catalog.js +12 -0
  79. package/dist/application/project/catalog.js.map +1 -0
  80. package/dist/application/project/manifest.d.ts +2 -0
  81. package/dist/application/project/manifest.js +29 -0
  82. package/dist/application/project/manifest.js.map +1 -0
  83. package/dist/application/project/planning.d.ts +6 -0
  84. package/dist/application/project/planning.js +12 -0
  85. package/dist/application/project/planning.js.map +1 -0
  86. package/dist/application/update/planning.d.ts +40 -0
  87. package/dist/application/update/planning.js +293 -0
  88. package/dist/application/update/planning.js.map +1 -0
  89. package/dist/application/update/reporting.d.ts +42 -0
  90. package/dist/application/update/reporting.js +325 -0
  91. package/dist/application/update/reporting.js.map +1 -0
  92. package/dist/application/update/use-case.d.ts +8 -0
  93. package/dist/application/update/use-case.js +600 -0
  94. package/dist/application/update/use-case.js.map +1 -0
  95. package/dist/application/upgrade/use-case.d.ts +6 -0
  96. package/dist/application/upgrade/use-case.js +82 -0
  97. package/dist/application/upgrade/use-case.js.map +1 -0
  98. package/dist/args.d.ts +5 -65
  99. package/dist/args.js +4 -441
  100. package/dist/args.js.map +1 -1
  101. package/dist/artifact-lifecycle.d.ts +1 -21
  102. package/dist/artifact-lifecycle.js +1 -73
  103. package/dist/artifact-lifecycle.js.map +1 -1
  104. package/dist/catalogs.d.ts +1 -50
  105. package/dist/catalogs.js +1 -472
  106. package/dist/catalogs.js.map +1 -1
  107. package/dist/cli/args/contracts.d.ts +51 -0
  108. package/dist/cli/args/contracts.js +2 -0
  109. package/dist/cli/args/contracts.js.map +1 -0
  110. package/dist/cli/args/definitions.d.ts +6 -0
  111. package/dist/cli/args/definitions.js +187 -0
  112. package/dist/cli/args/definitions.js.map +1 -0
  113. package/dist/cli/args/help.d.ts +5 -0
  114. package/dist/cli/args/help.js +130 -0
  115. package/dist/cli/args/help.js.map +1 -0
  116. package/dist/cli/args/parser.d.ts +5 -0
  117. package/dist/cli/args/parser.js +145 -0
  118. package/dist/cli/args/parser.js.map +1 -0
  119. package/dist/cli/args/readers.d.ts +4 -0
  120. package/dist/cli/args/readers.js +19 -0
  121. package/dist/cli/args/readers.js.map +1 -0
  122. package/dist/cli/commands/diagnose.d.ts +4 -0
  123. package/dist/cli/commands/diagnose.js +12 -0
  124. package/dist/cli/commands/diagnose.js.map +1 -0
  125. package/dist/cli/commands/dispatch.d.ts +3 -0
  126. package/dist/cli/commands/dispatch.js +94 -0
  127. package/dist/cli/commands/dispatch.js.map +1 -0
  128. package/dist/cli/commands/help.d.ts +3 -0
  129. package/dist/cli/commands/help.js +32 -0
  130. package/dist/cli/commands/help.js.map +1 -0
  131. package/dist/cli/commands/helpers.d.ts +8 -0
  132. package/dist/cli/commands/helpers.js +97 -0
  133. package/dist/cli/commands/helpers.js.map +1 -0
  134. package/dist/cli/commands/initialize.d.ts +3 -0
  135. package/dist/cli/commands/initialize.js +7 -0
  136. package/dist/cli/commands/initialize.js.map +1 -0
  137. package/dist/cli/commands/migrate.d.ts +3 -0
  138. package/dist/cli/commands/migrate.js +8 -0
  139. package/dist/cli/commands/migrate.js.map +1 -0
  140. package/dist/cli/commands/plan.d.ts +3 -0
  141. package/dist/cli/commands/plan.js +34 -0
  142. package/dist/cli/commands/plan.js.map +1 -0
  143. package/dist/cli/commands/reference.d.ts +5 -0
  144. package/dist/cli/commands/reference.js +43 -0
  145. package/dist/cli/commands/reference.js.map +1 -0
  146. package/dist/cli/commands/update.d.ts +3 -0
  147. package/dist/cli/commands/update.js +9 -0
  148. package/dist/cli/commands/update.js.map +1 -0
  149. package/dist/cli/commands/upgrade.d.ts +3 -0
  150. package/dist/cli/commands/upgrade.js +7 -0
  151. package/dist/cli/commands/upgrade.js.map +1 -0
  152. package/dist/cli/context.d.ts +1 -0
  153. package/dist/cli/context.js +2 -0
  154. package/dist/cli/context.js.map +1 -0
  155. package/dist/cli/project-options.d.ts +3 -0
  156. package/dist/cli/project-options.js +41 -0
  157. package/dist/cli/project-options.js.map +1 -0
  158. package/dist/cli.d.ts +3 -3
  159. package/dist/cli.js +4 -3
  160. package/dist/cli.js.map +1 -1
  161. package/dist/commands.d.ts +4 -35
  162. package/dist/commands.js +3 -2812
  163. package/dist/commands.js.map +1 -1
  164. package/dist/container-validation.d.ts +1 -1
  165. package/dist/domain/governance/activation/approvals.d.ts +351 -0
  166. package/dist/domain/governance/activation/approvals.js +528 -0
  167. package/dist/domain/governance/activation/approvals.js.map +1 -0
  168. package/dist/domain/governance/activation/canonical-json.d.ts +4 -0
  169. package/dist/domain/governance/activation/canonical-json.js +39 -0
  170. package/dist/domain/governance/activation/canonical-json.js.map +1 -0
  171. package/dist/domain/governance/activation/capabilities.d.ts +7 -0
  172. package/dist/domain/governance/activation/capabilities.js +33 -0
  173. package/dist/domain/governance/activation/capabilities.js.map +1 -0
  174. package/dist/domain/governance/activation/evidence.d.ts +63 -0
  175. package/dist/domain/governance/activation/evidence.js +444 -0
  176. package/dist/domain/governance/activation/evidence.js.map +1 -0
  177. package/dist/domain/governance/activation/graph.d.ts +397 -0
  178. package/dist/domain/governance/activation/graph.js +441 -0
  179. package/dist/domain/governance/activation/graph.js.map +1 -0
  180. package/dist/domain/governance/activation/inputs.d.ts +25 -0
  181. package/dist/domain/governance/activation/inputs.js +63 -0
  182. package/dist/domain/governance/activation/inputs.js.map +1 -0
  183. package/dist/domain/governance/activation/operations.d.ts +19 -0
  184. package/dist/domain/governance/activation/operations.js +181 -0
  185. package/dist/domain/governance/activation/operations.js.map +1 -0
  186. package/dist/domain/governance/activation/readiness.d.ts +27 -0
  187. package/dist/domain/governance/activation/readiness.js +269 -0
  188. package/dist/domain/governance/activation/readiness.js.map +1 -0
  189. package/dist/domain/governance/activation/types.d.ts +399 -0
  190. package/dist/domain/governance/activation/types.js +109 -0
  191. package/dist/domain/governance/activation/types.js.map +1 -0
  192. package/dist/domain/governance/activation/validators.d.ts +22 -0
  193. package/dist/domain/governance/activation/validators.js +1347 -0
  194. package/dist/domain/governance/activation/validators.js.map +1 -0
  195. package/dist/domain/governance/assessment/catalog.d.ts +14 -0
  196. package/dist/domain/governance/assessment/catalog.js +105 -0
  197. package/dist/domain/governance/assessment/catalog.js.map +1 -0
  198. package/dist/domain/governance/assessment/errors.d.ts +10 -0
  199. package/dist/domain/governance/assessment/errors.js +22 -0
  200. package/dist/domain/governance/assessment/errors.js.map +1 -0
  201. package/dist/domain/governance/assessment/index.d.ts +8 -0
  202. package/dist/domain/governance/assessment/index.js +9 -0
  203. package/dist/domain/governance/assessment/index.js.map +1 -0
  204. package/dist/domain/governance/assessment/live-normalize.d.ts +19 -0
  205. package/dist/domain/governance/assessment/live-normalize.js +403 -0
  206. package/dist/domain/governance/assessment/live-normalize.js.map +1 -0
  207. package/dist/domain/governance/assessment/predicates.d.ts +36 -0
  208. package/dist/domain/governance/assessment/predicates.js +545 -0
  209. package/dist/domain/governance/assessment/predicates.js.map +1 -0
  210. package/dist/domain/governance/assessment/report.d.ts +23 -0
  211. package/dist/domain/governance/assessment/report.js +266 -0
  212. package/dist/domain/governance/assessment/report.js.map +1 -0
  213. package/dist/domain/governance/assessment/sanitize.d.ts +8 -0
  214. package/dist/domain/governance/assessment/sanitize.js +60 -0
  215. package/dist/domain/governance/assessment/sanitize.js.map +1 -0
  216. package/dist/domain/governance/assessment/types.d.ts +174 -0
  217. package/dist/domain/governance/assessment/types.js +13 -0
  218. package/dist/domain/governance/assessment/types.js.map +1 -0
  219. package/dist/domain/governance/assessment/yaml.d.ts +6 -0
  220. package/dist/domain/governance/assessment/yaml.js +30 -0
  221. package/dist/domain/governance/assessment/yaml.js.map +1 -0
  222. package/dist/domain/governance/policy/identity.d.ts +64 -0
  223. package/dist/domain/governance/policy/identity.js +112 -0
  224. package/dist/domain/governance/policy/identity.js.map +1 -0
  225. package/dist/domain/migration/dependencies.d.ts +7 -0
  226. package/dist/domain/migration/dependencies.js +221 -0
  227. package/dist/domain/migration/dependencies.js.map +1 -0
  228. package/dist/domain/migration/inventory.d.ts +3 -0
  229. package/dist/domain/migration/inventory.js +15 -0
  230. package/dist/domain/migration/inventory.js.map +1 -0
  231. package/dist/domain/project/artifact-lifecycle.d.ts +69 -0
  232. package/dist/domain/project/artifact-lifecycle.js +137 -0
  233. package/dist/domain/project/artifact-lifecycle.js.map +1 -0
  234. package/dist/domain/project/catalog.d.ts +48 -0
  235. package/dist/domain/project/catalog.js +507 -0
  236. package/dist/domain/project/catalog.js.map +1 -0
  237. package/dist/domain/project/contracts.d.ts +247 -0
  238. package/dist/domain/project/contracts.js +2 -0
  239. package/dist/domain/project/contracts.js.map +1 -0
  240. package/dist/domain/project/errors.d.ts +3 -0
  241. package/dist/domain/project/errors.js +7 -0
  242. package/dist/domain/project/errors.js.map +1 -0
  243. package/dist/domain/project/infrastructure-layout.d.ts +26 -0
  244. package/dist/domain/project/infrastructure-layout.js +124 -0
  245. package/dist/domain/project/infrastructure-layout.js.map +1 -0
  246. package/dist/domain/project/inputs.d.ts +21 -0
  247. package/dist/domain/project/inputs.js +41 -0
  248. package/dist/domain/project/inputs.js.map +1 -0
  249. package/dist/domain/project/manifest/artifacts.d.ts +8 -0
  250. package/dist/domain/project/manifest/artifacts.js +150 -0
  251. package/dist/domain/project/manifest/artifacts.js.map +1 -0
  252. package/dist/domain/project/manifest/context.d.ts +13 -0
  253. package/dist/domain/project/manifest/context.js +2 -0
  254. package/dist/domain/project/manifest/context.js.map +1 -0
  255. package/dist/domain/project/manifest/fields.d.ts +6 -0
  256. package/dist/domain/project/manifest/fields.js +36 -0
  257. package/dist/domain/project/manifest/fields.js.map +1 -0
  258. package/dist/domain/project/manifest/governance.d.ts +8 -0
  259. package/dist/domain/project/manifest/governance.js +161 -0
  260. package/dist/domain/project/manifest/governance.js.map +1 -0
  261. package/dist/domain/project/manifest/project-identity.d.ts +6 -0
  262. package/dist/domain/project/manifest/project-identity.js +273 -0
  263. package/dist/domain/project/manifest/project-identity.js.map +1 -0
  264. package/dist/domain/project/manifest/reader.d.ts +9 -0
  265. package/dist/domain/project/manifest/reader.js +141 -0
  266. package/dist/domain/project/manifest/reader.js.map +1 -0
  267. package/dist/domain/project/paths.d.ts +2 -0
  268. package/dist/domain/project/paths.js +29 -0
  269. package/dist/domain/project/paths.js.map +1 -0
  270. package/dist/domain/project/planning.d.ts +19 -0
  271. package/dist/domain/project/planning.js +329 -0
  272. package/dist/domain/project/planning.js.map +1 -0
  273. package/dist/domain/project/retired-workload.d.ts +4 -0
  274. package/dist/domain/project/retired-workload.js +18 -0
  275. package/dist/domain/project/retired-workload.js.map +1 -0
  276. package/dist/domain/project/supported-stack.d.ts +113 -0
  277. package/dist/domain/project/supported-stack.js +426 -0
  278. package/dist/domain/project/supported-stack.js.map +1 -0
  279. package/dist/domain/workstation/constraints.d.ts +6 -0
  280. package/dist/domain/workstation/constraints.js +8 -0
  281. package/dist/domain/workstation/constraints.js.map +1 -0
  282. package/dist/file-system.d.ts +10 -41
  283. package/dist/file-system.js +9 -1204
  284. package/dist/file-system.js.map +1 -1
  285. package/dist/framework-adapters.d.ts +1 -1
  286. package/dist/framework-validation.d.ts +1 -1
  287. package/dist/framework-validation.js +1 -1
  288. package/dist/framework-validation.js.map +1 -1
  289. package/dist/genai-templates.d.ts +2 -9
  290. package/dist/genai-templates.js +4 -5
  291. package/dist/genai-templates.js.map +1 -1
  292. package/dist/generators/common/artifacts.d.ts +6 -0
  293. package/dist/generators/common/artifacts.js +30 -0
  294. package/dist/generators/common/artifacts.js.map +1 -0
  295. package/dist/generators/common/base.d.ts +17 -0
  296. package/dist/generators/common/base.js +421 -0
  297. package/dist/generators/common/base.js.map +1 -0
  298. package/dist/generators/common/dotenv-validation.d.ts +2 -0
  299. package/dist/generators/common/dotenv-validation.js +80 -0
  300. package/dist/generators/common/dotenv-validation.js.map +1 -0
  301. package/dist/generators/common/environments.d.ts +3 -0
  302. package/dist/generators/common/environments.js +15 -0
  303. package/dist/generators/common/environments.js.map +1 -0
  304. package/dist/generators/common/frontend.d.ts +15 -0
  305. package/dist/generators/common/frontend.js +166 -0
  306. package/dist/generators/common/frontend.js.map +1 -0
  307. package/dist/generators/common/python-settings.d.ts +2 -0
  308. package/dist/generators/common/python-settings.js +70 -0
  309. package/dist/generators/common/python-settings.js.map +1 -0
  310. package/dist/generators/common/spec-workflow.d.ts +28 -0
  311. package/dist/generators/common/spec-workflow.js +567 -0
  312. package/dist/generators/common/spec-workflow.js.map +1 -0
  313. package/dist/generators/common/values.d.ts +13 -0
  314. package/dist/generators/common/values.js +16 -0
  315. package/dist/generators/common/values.js.map +1 -0
  316. package/dist/generators/containers/compose.d.ts +7 -0
  317. package/dist/generators/containers/compose.js +196 -0
  318. package/dist/generators/containers/compose.js.map +1 -0
  319. package/dist/generators/containers/context.d.ts +1 -0
  320. package/dist/generators/containers/context.js +43 -0
  321. package/dist/generators/containers/context.js.map +1 -0
  322. package/dist/generators/containers/images.d.ts +7 -0
  323. package/dist/generators/containers/images.js +119 -0
  324. package/dist/generators/containers/images.js.map +1 -0
  325. package/dist/generators/context.d.ts +42 -0
  326. package/dist/generators/context.js +38 -0
  327. package/dist/generators/context.js.map +1 -0
  328. package/dist/generators/genai/backend.d.ts +20 -0
  329. package/dist/generators/genai/backend.js +667 -0
  330. package/dist/generators/genai/backend.js.map +1 -0
  331. package/dist/generators/genai/configuration.d.ts +3 -0
  332. package/dist/generators/genai/configuration.js +43 -0
  333. package/dist/generators/genai/configuration.js.map +1 -0
  334. package/dist/generators/genai/database.d.ts +7 -0
  335. package/dist/generators/genai/database.js +71 -0
  336. package/dist/generators/genai/database.js.map +1 -0
  337. package/dist/generators/genai/functions.d.ts +15 -0
  338. package/dist/generators/genai/functions.js +156 -0
  339. package/dist/generators/genai/functions.js.map +1 -0
  340. package/dist/generators/genai/index.d.ts +8 -0
  341. package/dist/generators/genai/index.js +11 -0
  342. package/dist/generators/genai/index.js.map +1 -0
  343. package/dist/generators/genai/patterns.d.ts +9 -0
  344. package/dist/generators/genai/patterns.js +309 -0
  345. package/dist/generators/genai/patterns.js.map +1 -0
  346. package/dist/generators/infrastructure/azure.d.ts +18 -0
  347. package/dist/generators/infrastructure/azure.js +655 -0
  348. package/dist/generators/infrastructure/azure.js.map +1 -0
  349. package/dist/generators/infrastructure/names.d.ts +23 -0
  350. package/dist/generators/infrastructure/names.js +51 -0
  351. package/dist/generators/infrastructure/names.js.map +1 -0
  352. package/dist/generators/standard/configuration.d.ts +3 -0
  353. package/dist/generators/standard/configuration.js +26 -0
  354. package/dist/generators/standard/configuration.js.map +1 -0
  355. package/dist/generators/standard/go.d.ts +17 -0
  356. package/dist/generators/standard/go.js +338 -0
  357. package/dist/generators/standard/go.js.map +1 -0
  358. package/dist/generators/standard/index.d.ts +12 -0
  359. package/dist/generators/standard/index.js +12 -0
  360. package/dist/generators/standard/index.js.map +1 -0
  361. package/dist/generators/standard/node.d.ts +20 -0
  362. package/dist/generators/standard/node.js +314 -0
  363. package/dist/generators/standard/node.js.map +1 -0
  364. package/dist/generators/standard/python.d.ts +18 -0
  365. package/dist/generators/standard/python.js +191 -0
  366. package/dist/generators/standard/python.js.map +1 -0
  367. package/dist/go-template-assets.js +3 -3
  368. package/dist/go-template-assets.js.map +1 -1
  369. package/dist/governance-activation/activation-state.d.ts +1 -1
  370. package/dist/governance-activation/activation-state.js +46 -9
  371. package/dist/governance-activation/activation-state.js.map +1 -1
  372. package/dist/governance-activation/approvals.d.ts +1 -188
  373. package/dist/governance-activation/approvals.js +1 -507
  374. package/dist/governance-activation/approvals.js.map +1 -1
  375. package/dist/governance-activation/canonical-json.d.ts +1 -3
  376. package/dist/governance-activation/canonical-json.js +1 -35
  377. package/dist/governance-activation/canonical-json.js.map +1 -1
  378. package/dist/governance-activation/commands.d.ts +1 -1
  379. package/dist/governance-activation/commands.js +107 -103
  380. package/dist/governance-activation/commands.js.map +1 -1
  381. package/dist/governance-activation/compatibility.d.ts +11 -3
  382. package/dist/governance-activation/compatibility.js +31 -4
  383. package/dist/governance-activation/compatibility.js.map +1 -1
  384. package/dist/governance-activation/credentials.d.ts +3 -3
  385. package/dist/governance-activation/credentials.js +5 -5
  386. package/dist/governance-activation/credentials.js.map +1 -1
  387. package/dist/governance-activation/doctor.d.ts +1 -1
  388. package/dist/governance-activation/doctor.js +28 -96
  389. package/dist/governance-activation/doctor.js.map +1 -1
  390. package/dist/governance-activation/evidence.d.ts +1 -49
  391. package/dist/governance-activation/evidence.js +1 -245
  392. package/dist/governance-activation/evidence.js.map +1 -1
  393. package/dist/governance-activation/git-ignore.d.ts +9 -0
  394. package/dist/governance-activation/git-ignore.js +76 -0
  395. package/dist/governance-activation/git-ignore.js.map +1 -0
  396. package/dist/governance-activation/graph.d.ts +1 -397
  397. package/dist/governance-activation/graph.js +1 -440
  398. package/dist/governance-activation/graph.js.map +1 -1
  399. package/dist/governance-activation/identity.d.ts +1 -35
  400. package/dist/governance-activation/identity.js +1 -88
  401. package/dist/governance-activation/identity.js.map +1 -1
  402. package/dist/governance-activation/index.d.ts +3 -0
  403. package/dist/governance-activation/index.js +3 -0
  404. package/dist/governance-activation/index.js.map +1 -1
  405. package/dist/governance-activation/inputs.d.ts +8 -0
  406. package/dist/governance-activation/inputs.js +146 -0
  407. package/dist/governance-activation/inputs.js.map +1 -0
  408. package/dist/governance-activation/migration.d.ts +4 -3
  409. package/dist/governance-activation/migration.js +14 -6
  410. package/dist/governance-activation/migration.js.map +1 -1
  411. package/dist/governance-activation/phase-bootstrap-state.d.ts +3 -0
  412. package/dist/governance-activation/phase-bootstrap-state.js +128 -0
  413. package/dist/governance-activation/phase-bootstrap-state.js.map +1 -0
  414. package/dist/governance-activation/phase-discovery.d.ts +2 -0
  415. package/dist/governance-activation/phase-discovery.js +88 -0
  416. package/dist/governance-activation/phase-discovery.js.map +1 -0
  417. package/dist/governance-activation/phase-governance.d.ts +4 -0
  418. package/dist/governance-activation/phase-governance.js +141 -0
  419. package/dist/governance-activation/phase-governance.js.map +1 -0
  420. package/dist/governance-activation/phase-publication.d.ts +8 -0
  421. package/dist/governance-activation/phase-publication.js +318 -0
  422. package/dist/governance-activation/phase-publication.js.map +1 -0
  423. package/dist/governance-activation/read-only.d.ts +30 -0
  424. package/dist/governance-activation/read-only.js +93 -0
  425. package/dist/governance-activation/read-only.js.map +1 -0
  426. package/dist/governance-activation/readiness.d.ts +1 -24
  427. package/dist/governance-activation/readiness.js +1 -220
  428. package/dist/governance-activation/readiness.js.map +1 -1
  429. package/dist/governance-activation/reconciliation.d.ts +1 -1
  430. package/dist/governance-activation/reconciliation.js +3 -3
  431. package/dist/governance-activation/reconciliation.js.map +1 -1
  432. package/dist/governance-activation/release-integrity.d.ts +1 -1
  433. package/dist/governance-activation/release-integrity.js +2 -2
  434. package/dist/governance-activation/release-integrity.js.map +1 -1
  435. package/dist/governance-activation/seed-lifecycle.d.ts +10 -2
  436. package/dist/governance-activation/seed-lifecycle.js +244 -66
  437. package/dist/governance-activation/seed-lifecycle.js.map +1 -1
  438. package/dist/governance-activation/source-of-truth.d.ts +5 -3
  439. package/dist/governance-activation/source-of-truth.js +32 -52
  440. package/dist/governance-activation/source-of-truth.js.map +1 -1
  441. package/dist/governance-activation/spec-kit-seed.d.ts +9 -0
  442. package/dist/governance-activation/spec-kit-seed.js +43 -0
  443. package/dist/governance-activation/spec-kit-seed.js.map +1 -0
  444. package/dist/governance-activation/task-projection.d.ts +1 -1
  445. package/dist/governance-activation/transition-planning.d.ts +15 -0
  446. package/dist/governance-activation/transition-planning.js +250 -0
  447. package/dist/governance-activation/transition-planning.js.map +1 -0
  448. package/dist/governance-activation/transition-ports.d.ts +150 -0
  449. package/dist/governance-activation/transition-ports.js +2 -0
  450. package/dist/governance-activation/transition-ports.js.map +1 -0
  451. package/dist/governance-activation/transition-process.d.ts +7 -0
  452. package/dist/governance-activation/transition-process.js +21 -0
  453. package/dist/governance-activation/transition-process.js.map +1 -0
  454. package/dist/governance-activation/transition-records.d.ts +56 -0
  455. package/dist/governance-activation/transition-records.js +208 -0
  456. package/dist/governance-activation/transition-records.js.map +1 -0
  457. package/dist/governance-activation/transitions.d.ts +6 -157
  458. package/dist/governance-activation/transitions.js +143 -2045
  459. package/dist/governance-activation/transitions.js.map +1 -1
  460. package/dist/governance-activation/types.d.ts +1 -390
  461. package/dist/governance-activation/types.js +1 -107
  462. package/dist/governance-activation/types.js.map +1 -1
  463. package/dist/governance-activation/validators.d.ts +1 -17
  464. package/dist/governance-activation/validators.js +1 -1304
  465. package/dist/governance-activation/validators.js.map +1 -1
  466. package/dist/governance-assessment/catalog.d.ts +7 -0
  467. package/dist/governance-assessment/catalog.js +31 -0
  468. package/dist/governance-assessment/catalog.js.map +1 -0
  469. package/dist/governance-assessment/command.d.ts +10 -0
  470. package/dist/governance-assessment/command.js +77 -0
  471. package/dist/governance-assessment/command.js.map +1 -0
  472. package/dist/governance-assessment/engine.d.ts +14 -0
  473. package/dist/governance-assessment/engine.js +935 -0
  474. package/dist/governance-assessment/engine.js.map +1 -0
  475. package/dist/governance-assessment/index.d.ts +5 -0
  476. package/dist/governance-assessment/index.js +6 -0
  477. package/dist/governance-assessment/index.js.map +1 -0
  478. package/dist/governance-assessment/live-normalize.d.ts +1 -0
  479. package/dist/governance-assessment/live-normalize.js +2 -0
  480. package/dist/governance-assessment/live-normalize.js.map +1 -0
  481. package/dist/governance-assessment/live-transport.d.ts +34 -0
  482. package/dist/governance-assessment/live-transport.js +455 -0
  483. package/dist/governance-assessment/live-transport.js.map +1 -0
  484. package/dist/governance-assessment/live.d.ts +2 -0
  485. package/dist/governance-assessment/live.js +689 -0
  486. package/dist/governance-assessment/live.js.map +1 -0
  487. package/dist/governance-assessment/predicates.d.ts +1 -0
  488. package/dist/governance-assessment/predicates.js +2 -0
  489. package/dist/governance-assessment/predicates.js.map +1 -0
  490. package/dist/governance-assessment/project.d.ts +33 -0
  491. package/dist/governance-assessment/project.js +178 -0
  492. package/dist/governance-assessment/project.js.map +1 -0
  493. package/dist/governance-assessment/readers.d.ts +14 -0
  494. package/dist/governance-assessment/readers.js +122 -0
  495. package/dist/governance-assessment/readers.js.map +1 -0
  496. package/dist/governance-assessment/report.d.ts +1 -0
  497. package/dist/governance-assessment/report.js +2 -0
  498. package/dist/governance-assessment/report.js.map +1 -0
  499. package/dist/governance-assessment/sanitize.d.ts +1 -0
  500. package/dist/governance-assessment/sanitize.js +2 -0
  501. package/dist/governance-assessment/sanitize.js.map +1 -0
  502. package/dist/governance-assessment/types.d.ts +6 -0
  503. package/dist/governance-assessment/types.js +2 -0
  504. package/dist/governance-assessment/types.js.map +1 -0
  505. package/dist/governance-assessment/yaml.d.ts +1 -0
  506. package/dist/governance-assessment/yaml.js +2 -0
  507. package/dist/governance-assessment/yaml.js.map +1 -0
  508. package/dist/init-filesystem.d.ts +1 -1
  509. package/dist/init-filesystem.js +126 -174
  510. package/dist/init-filesystem.js.map +1 -1
  511. package/dist/interactive.d.ts +2 -2
  512. package/dist/interactive.js +42 -41
  513. package/dist/interactive.js.map +1 -1
  514. package/dist/migrate-plan.d.ts +5 -1
  515. package/dist/migrate-plan.js +87 -10
  516. package/dist/migrate-plan.js.map +1 -1
  517. package/dist/npm-template-assets.js +5 -4
  518. package/dist/npm-template-assets.js.map +1 -1
  519. package/dist/openspec-profile.d.ts +1 -1
  520. package/dist/opentofu-template-assets.js +3 -3
  521. package/dist/opentofu-template-assets.js.map +1 -1
  522. package/dist/package-identity.d.ts +3 -0
  523. package/dist/package-identity.js +13 -2
  524. package/dist/package-identity.js.map +1 -1
  525. package/dist/planner.d.ts +1 -19
  526. package/dist/planner.js +1 -565
  527. package/dist/planner.js.map +1 -1
  528. package/dist/process-runner.d.ts +5 -1
  529. package/dist/process-runner.js +164 -8
  530. package/dist/process-runner.js.map +1 -1
  531. package/dist/project-dependencies.d.ts +5 -3
  532. package/dist/project-dependencies.js +74 -63
  533. package/dist/project-dependencies.js.map +1 -1
  534. package/dist/published-verifier.js +67 -30
  535. package/dist/published-verifier.js.map +1 -1
  536. package/dist/python-template-assets.js +6 -9
  537. package/dist/python-template-assets.js.map +1 -1
  538. package/dist/reconcile.d.ts +1 -1
  539. package/dist/reconcile.js +2 -2
  540. package/dist/reconcile.js.map +1 -1
  541. package/dist/release-identity.js +19 -0
  542. package/dist/release-identity.js.map +1 -1
  543. package/dist/repository-governance.d.ts +10 -2
  544. package/dist/repository-governance.js +240 -99
  545. package/dist/repository-governance.js.map +1 -1
  546. package/dist/runtime.js +1 -1
  547. package/dist/runtime.js.map +1 -1
  548. package/dist/scan.d.ts +6 -2
  549. package/dist/scan.js +78 -30
  550. package/dist/scan.js.map +1 -1
  551. package/dist/self-upgrade.d.ts +1 -1
  552. package/dist/self-upgrade.js +20 -7
  553. package/dist/self-upgrade.js.map +1 -1
  554. package/dist/stable-release.js +7 -1
  555. package/dist/stable-release.js.map +1 -1
  556. package/dist/standard-templates.d.ts +2 -2
  557. package/dist/standard-templates.js +6 -841
  558. package/dist/standard-templates.js.map +1 -1
  559. package/dist/supported-stack.d.ts +3 -115
  560. package/dist/supported-stack.js +4 -438
  561. package/dist/supported-stack.js.map +1 -1
  562. package/dist/telemetry/contract.d.ts +2 -0
  563. package/dist/telemetry/contract.js +7 -0
  564. package/dist/telemetry/contract.js.map +1 -1
  565. package/dist/telemetry/index.d.ts +1 -1
  566. package/dist/templates.d.ts +10 -20
  567. package/dist/templates.js +52 -3238
  568. package/dist/templates.js.map +1 -1
  569. package/dist/types.d.ts +1 -263
  570. package/dist/types.js +1 -1
  571. package/dist/types.js.map +1 -1
  572. package/dist/version.js +2 -6
  573. package/dist/version.js.map +1 -1
  574. package/dist/workstation-catalog.d.ts +5 -2
  575. package/dist/workstation-catalog.js +33 -2
  576. package/dist/workstation-catalog.js.map +1 -1
  577. package/dist/workstation.d.ts +4 -3
  578. package/dist/workstation.js +86 -32
  579. package/dist/workstation.js.map +1 -1
  580. package/docs/assets/liftoff-terminal.svg +3 -3
  581. package/docs/azure-deployment.md +107 -10
  582. package/docs/cli-reference.md +127 -11
  583. package/docs/configuration-and-manifests.md +150 -35
  584. package/docs/existing-repositories.md +33 -6
  585. package/docs/getting-started.md +44 -31
  586. package/docs/prerequisites.md +34 -35
  587. package/docs/project-structure.md +45 -47
  588. package/docs/repository-governance.md +207 -24
  589. package/docs/safety-and-consent.md +65 -12
  590. package/docs/spec-workflows-and-agents.md +31 -28
  591. package/docs/supported-stack.md +11 -9
  592. package/docs/telemetry.md +9 -3
  593. package/docs/troubleshooting.md +107 -49
  594. package/docs/workloads.md +53 -62
  595. package/package.json +6 -8
  596. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/UPSTREAM_LICENSE.txt +0 -21
  597. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/catalog.json +0 -520
  598. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/packaged/gitignore +0 -27
  599. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/README.md +0 -25
  600. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/components.json +0 -22
  601. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/eslint.config.js +0 -23
  602. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/index.html +0 -13
  603. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/package-lock.json +0 -6156
  604. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/package.json +0 -60
  605. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/public/power-apps.svg +0 -55
  606. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/App.tsx +0 -17
  607. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/assets/react.svg +0 -1
  608. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/mode-toggle.tsx +0 -37
  609. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/badge.tsx +0 -46
  610. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/button.tsx +0 -60
  611. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/calendar.tsx +0 -211
  612. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/card.tsx +0 -92
  613. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/chart.tsx +0 -355
  614. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/checkbox.tsx +0 -32
  615. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/command.tsx +0 -184
  616. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/dialog.tsx +0 -141
  617. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/dropdown-menu.tsx +0 -257
  618. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/input.tsx +0 -21
  619. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/label.tsx +0 -22
  620. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/popover.tsx +0 -46
  621. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/progress.tsx +0 -29
  622. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/select.tsx +0 -185
  623. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/separator.tsx +0 -26
  624. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/skeleton.tsx +0 -13
  625. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/table.tsx +0 -114
  626. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/tabs.tsx +0 -66
  627. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/textarea.tsx +0 -18
  628. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/tooltip.tsx +0 -61
  629. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/hooks/use-theme.ts +0 -11
  630. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/index.css +0 -131
  631. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/lib/utils.ts +0 -6
  632. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/main.tsx +0 -10
  633. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/_layout.tsx +0 -31
  634. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/home.tsx +0 -59
  635. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/not-found.tsx +0 -14
  636. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/query-provider.tsx +0 -26
  637. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/sonner-provider.tsx +0 -23
  638. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/theme-provider.tsx +0 -64
  639. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/router.tsx +0 -24
  640. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.app.json +0 -34
  641. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.json +0 -13
  642. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.node.json +0 -26
  643. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/vite.config.ts +0 -19
  644. package/assets/power-apps-code-app/README.md +0 -27
  645. package/dist/code-apps-plugin.d.ts +0 -13
  646. package/dist/code-apps-plugin.js +0 -69
  647. package/dist/code-apps-plugin.js.map +0 -1
  648. package/dist/power-apps-assets.d.ts +0 -30
  649. package/dist/power-apps-assets.js +0 -131
  650. package/dist/power-apps-assets.js.map +0 -1
  651. package/dist/power-apps-templates.d.ts +0 -3
  652. package/dist/power-apps-templates.js +0 -173
  653. package/dist/power-apps-templates.js.map +0 -1
  654. package/dist/power-apps-validation.d.ts +0 -1
  655. package/dist/power-apps-validation.js +0 -79
  656. package/dist/power-apps-validation.js.map +0 -1
package/dist/templates.js CHANGED
@@ -1,74 +1,28 @@
1
- import { createHash } from 'node:crypto';
2
- import { addGenAiExtensionArtifacts } from './genai-templates.js';
1
+ import { createGeneratorContext } from './generators/context.js';
2
+ import { packagedTemplateAssets } from './adapters/packaged-assets/template-assets.js';
3
+ import { packagedSupportedStack } from './adapters/packaged-assets/supported-stack.js';
4
+ import { addBaseArtifacts } from './generators/common/base.js';
5
+ import { addDockerArtifacts } from './generators/containers/compose.js';
6
+ import { addEnvironmentArtifacts } from './generators/common/environments.js';
7
+ import { addFrontendArtifacts } from './generators/common/frontend.js';
8
+ import { addGenAiExtensionArtifacts } from './generators/genai/index.js';
9
+ import { addInfrastructureArtifacts } from './generators/infrastructure/azure.js';
10
+ import { addSpecWorkflowArtifacts } from './generators/common/spec-workflow.js';
11
+ import { addStandardStackArtifacts } from './generators/standard/index.js';
3
12
  import { assertImmutableGeneratedContainerReferences } from './container-validation.js';
4
- import { addPowerAppsCodeAppArtifacts } from './power-apps-templates.js';
5
- import { addStandardStackArtifacts, renderStandardDockerfile, renderStandardEnv } from './standard-templates.js';
6
- import { liftoffVersion } from './version.js';
7
- import { renderNpmLock, renderNpmPackage } from './npm-template-assets.js';
8
- import { renderOpenTofuProviderLock, renderOpenTofuVersions } from './opentofu-template-assets.js';
9
- import { formatCommand } from './process-runner.js';
10
- import { formatContainerImage, supportedStack } from './supported-stack.js';
11
- import { buildRepositoryGovernanceArtifacts, governancePolicyVersion } from './repository-governance.js';
13
+ import { buildRepositoryGovernanceArtifacts } from './repository-governance.js';
14
+ import { createArtifactAdder } from './generators/common/artifacts.js';
15
+ import { createHash } from 'node:crypto';
12
16
  import { currentActivationIdentity } from './governance-activation/graph.js';
13
- import { OPEN_SPEC_DELIVERY, OPEN_SPEC_PROFILE, OPEN_SPEC_WORKFLOW_IDS } from './openspec-profile.js';
14
- import { renderFunctionRequirementsAsset, renderPythonLock, renderPythonPyprojectAsset } from './python-template-assets.js';
15
- import { workstationRequirementCatalog } from './workstation-catalog.js';
17
+ import { ensureTrailingNewline } from './generators/common/artifacts.js';
18
+ import { governancePolicyVersion } from './repository-governance.js';
19
+ import { liftoffVersion } from './version.js';
16
20
  const contentHash = (content) => `sha256:${createHash('sha256').update(content, 'utf8').digest('hex')}`;
17
- const DEFAULT_FUNCTION_WORKER_QUEUE_NAME = 'events';
18
- export const AZURE_NAME_LIMITS = {
19
- resourceGroup: 90,
20
- containerRegistry: 50,
21
- identity: 128,
22
- containerAppEnvironment: 60,
23
- backendContainerApp: 32,
24
- frontendContainerApp: 32,
25
- functionServicePlan: 40,
26
- functionApp: 60,
27
- postgres: 63,
28
- redis: 63,
29
- storage: 24,
30
- serviceBus: 50,
31
- communication: 63,
32
- keyVault: 24
33
- };
34
- const boundedToken = (value, length) => value.slice(0, length).replace(/-+$/g, '') || 'app';
35
- export function buildAzureResourceNames(plan, environment, resourceSuffix) {
36
- const workload = boundedToken(plan.safeProjectName, 12);
37
- const compactWorkload = boundedToken(plan.safeProjectName.replace(/-/g, ''), 8);
38
- return {
39
- resourceGroup: `rg-${workload}-${environment}`,
40
- containerRegistry: `acr${compactWorkload}${resourceSuffix}`,
41
- identity: `id-${workload}-${environment}`,
42
- containerAppEnvironment: `cae-${workload}-${environment}`,
43
- backendContainerApp: `ca-${workload}-be-${environment}`,
44
- frontendContainerApp: `ca-${workload}-fe-${environment}`,
45
- functionServicePlan: `asp-${workload}-fn-${environment}`,
46
- functionApp: `func-${workload}-${environment}-${resourceSuffix}`,
47
- postgres: `psql-${workload}-${environment}-${resourceSuffix}`,
48
- redis: `redis-${workload}-${environment}-${resourceSuffix}`,
49
- storage: `st${compactWorkload}${resourceSuffix}`,
50
- serviceBus: `sb-${workload}-${environment}-${resourceSuffix}`,
51
- communication: `acs-${workload}-${environment}-${resourceSuffix}`,
52
- keyVault: `kv-${compactWorkload}-${resourceSuffix}`
53
- };
54
- }
55
- function stableResourceSuffix(plan, environment) {
56
- return createHash('sha256')
57
- .update(`${plan.safeProjectName}:${environment}`, 'utf8')
58
- .digest('hex')
59
- .slice(0, 12);
21
+ export { AZURE_NAME_LIMITS, buildAzureResourceNames } from './generators/infrastructure/names.js';
22
+ export function resolveGeneratorContext(plan, assets = packagedTemplateAssets) {
23
+ return createGeneratorContext(plan, assets, packagedSupportedStack);
60
24
  }
61
- const pyModule = (value) => value.replace(/-/g, '_');
62
- const titleCase = (value) => value.replace(/(^|[-_\s])([a-z])/g, (_match, prefix, letter) => `${prefix ? ' ' : ''}${letter.toUpperCase()}`).trim();
63
- const sourceString = (value) => JSON.stringify(value);
64
- const scriptSourceString = (value) => sourceString(value).replaceAll('<', '\\u003c');
65
- const escapeHtml = (value) => value.replaceAll('&', '&amp;').replaceAll('<', '&lt;').replaceAll('>', '&gt;').replaceAll('"', '&quot;');
66
- const genAiPattern = (plan) => {
67
- return plan.pattern;
68
- };
69
- const hasFunctionWorker = (plan) => plan.workload === 'genai' && plan.provider.id === 'azure' && plan.pattern.worker;
70
- const functionWorkerName = (plan) => `${plan.pattern.id}-worker`;
71
- export function buildArtifacts(plan) {
25
+ export function buildArtifacts(plan, context = resolveGeneratorContext(plan)) {
72
26
  const artifacts = [];
73
27
  const addProject = createArtifactAdder(artifacts, 'project', 'base');
74
28
  const addDesiredState = createArtifactAdder(artifacts, 'desired-state');
@@ -76,21 +30,18 @@ export function buildArtifacts(plan) {
76
30
  const addSeed = createArtifactAdder(artifacts, 'seed');
77
31
  switch (plan.workload) {
78
32
  case 'genai':
79
- addGenAiWorkloadArtifacts(addProject, addDesiredState, artifacts, plan);
33
+ addGenAiWorkloadArtifacts(addProject, addDesiredState, artifacts, plan, context);
80
34
  break;
81
35
  case 'standard':
82
- addStandardWorkloadArtifacts(addProject, addDesiredState, artifacts, plan);
83
- break;
84
- case 'power-apps-code-app':
85
- addPowerAppsWorkloadArtifacts(addDesiredState, artifacts, plan);
36
+ addStandardWorkloadArtifacts(addProject, addDesiredState, artifacts, plan, context);
86
37
  break;
87
38
  }
88
39
  addSpecWorkflowArtifacts(addSeed, addFramework, plan);
89
40
  for (const artifact of buildRepositoryGovernanceArtifacts(plan)) {
90
41
  artifacts.push({ ...artifact, content: ensureTrailingNewline(artifact.content) });
91
42
  }
92
- if (plan.workload !== 'power-apps-code-app' && plan.includeFrontend) {
93
- addFrontendArtifacts(createArtifactAdder(artifacts, 'project', 'frontend'), plan);
43
+ if (plan.includeFrontend) {
44
+ addFrontendArtifacts(createArtifactAdder(artifacts, 'project', 'frontend'), plan, context);
94
45
  }
95
46
  assertImmutableGeneratedContainerReferences(artifacts);
96
47
  const manifest = buildManifest(plan, artifacts);
@@ -103,55 +54,20 @@ export function buildArtifacts(plan) {
103
54
  });
104
55
  return artifacts;
105
56
  }
106
- function createArtifactAdder(artifacts, lifecycle, provisioningGroup) {
107
- return (logicalName, category, pathParts, content) => {
108
- const normalizedContent = ensureTrailingNewline(content);
109
- if (lifecycle === 'project') {
110
- if (!provisioningGroup) {
111
- throw new Error(`Project artifact ${logicalName} is missing a provisioning group.`);
112
- }
113
- artifacts.push({
114
- logicalName,
115
- category,
116
- lifecycle,
117
- provisioningGroup,
118
- pathParts,
119
- content: normalizedContent
120
- });
121
- return;
122
- }
123
- artifacts.push({
124
- logicalName,
125
- category,
126
- lifecycle,
127
- pathParts,
128
- content: normalizedContent
129
- });
130
- };
131
- }
132
- function addGenAiWorkloadArtifacts(add, addDesiredState, artifacts, plan) {
133
- addBaseArtifacts(add, addDesiredState, plan);
134
- addGenAiExtensionArtifacts(add, plan, {
135
- backend: addBackendArtifacts,
136
- database: addDatabaseArtifacts,
137
- pattern: addPatternArtifacts,
138
- functions: addFunctionArtifacts
139
- });
140
- addApiWorkloadOperations(add, artifacts, plan);
141
- }
142
- function addStandardWorkloadArtifacts(add, addDesiredState, artifacts, plan) {
143
- addBaseArtifacts(add, addDesiredState, plan);
144
- addStandardStackArtifacts(add, plan);
145
- addApiWorkloadOperations(add, artifacts, plan);
57
+ function addGenAiWorkloadArtifacts(add, addDesiredState, artifacts, plan, context) {
58
+ addBaseArtifacts(add, addDesiredState, plan, context);
59
+ addGenAiExtensionArtifacts(add, plan, context);
60
+ addApiWorkloadOperations(add, artifacts, plan, context);
146
61
  }
147
- function addPowerAppsWorkloadArtifacts(addDesiredState, artifacts, plan) {
148
- addPowerAppsCodeAppArtifacts(createArtifactAdder(artifacts, 'project', 'power-apps-starter'), plan);
149
- addPowerAppsProjectConfig(addDesiredState, plan);
62
+ function addStandardWorkloadArtifacts(add, addDesiredState, artifacts, plan, context) {
63
+ addBaseArtifacts(add, addDesiredState, plan, context);
64
+ addStandardStackArtifacts(add, plan, context);
65
+ addApiWorkloadOperations(add, artifacts, plan, context);
150
66
  }
151
- function addApiWorkloadOperations(add, artifacts, plan) {
67
+ function addApiWorkloadOperations(add, artifacts, plan, context) {
152
68
  addEnvironmentArtifacts(artifacts, plan);
153
- addDockerArtifacts(add, plan);
154
- addInfrastructureArtifacts(add, artifacts, plan);
69
+ addDockerArtifacts(add, plan, context);
70
+ addInfrastructureArtifacts(add, artifacts, plan, context);
155
71
  }
156
72
  export function partitionGeneratedArtifacts(artifacts) {
157
73
  const manifest = artifacts.find((artifact) => artifact.logicalName === 'manifest');
@@ -173,30 +89,24 @@ export function partitionGeneratedArtifacts(artifacts) {
173
89
  export function buildManifest(plan, artifacts, options = {}) {
174
90
  const frameworkState = options.frameworkState ?? 'initialized';
175
91
  const agents = frameworkState === 'initialized' ? plan.agents.map((agent) => agent.id) : [];
176
- const workload = plan.workload === 'power-apps-code-app'
92
+ const workload = plan.workload === 'genai'
177
93
  ? {
178
- kind: 'power-apps-code-app',
179
- starter: plan.starter,
180
- codeAppsPlugin: plan.codeAppsPlugin
94
+ kind: 'genai',
95
+ apiStack: plan.apiStack.id,
96
+ pattern: plan.pattern.id,
97
+ cloud: plan.provider.id,
98
+ region: plan.region.slug,
99
+ frontend: plan.includeFrontend,
100
+ environments: plan.environments.map((environment) => environment.id)
181
101
  }
182
- : plan.workload === 'genai'
183
- ? {
184
- kind: 'genai',
185
- apiStack: plan.apiStack.id,
186
- pattern: plan.pattern.id,
187
- cloud: plan.provider.id,
188
- region: plan.region.slug,
189
- frontend: plan.includeFrontend,
190
- environments: plan.environments.map((environment) => environment.id)
191
- }
192
- : {
193
- kind: 'standard',
194
- apiStack: plan.apiStack.id,
195
- cloud: plan.provider.id,
196
- region: plan.region.slug,
197
- frontend: plan.includeFrontend,
198
- environments: plan.environments.map((environment) => environment.id)
199
- };
102
+ : {
103
+ kind: 'standard',
104
+ apiStack: plan.apiStack.id,
105
+ cloud: plan.provider.id,
106
+ region: plan.region.slug,
107
+ frontend: plan.includeFrontend,
108
+ environments: plan.environments.map((environment) => environment.id)
109
+ };
200
110
  return {
201
111
  artifactVersion: 7,
202
112
  generatedBy: 'Mission Control Liftoff',
@@ -244,3100 +154,4 @@ export function buildManifest(plan, artifacts, options = {}) {
244
154
  }))
245
155
  };
246
156
  }
247
- function addBaseArtifacts(add, addDesiredState, plan) {
248
- add('root-readme', 'documentation', ['README.md'], renderRootReadme(plan));
249
- add('root-gitignore', 'project', ['.gitignore'], renderGeneratedGitignore());
250
- addDesiredState('liftoff-config', 'project', ['liftoff.config.json'], JSON.stringify({
251
- projectName: plan.projectName,
252
- projectType: plan.projectType.id,
253
- apiStack: plan.apiStack.id,
254
- ...(plan.workload === 'genai' ? { pattern: plan.pattern.id } : {}),
255
- cloud: plan.provider.id,
256
- region: plan.region.slug,
257
- includeFrontend: plan.includeFrontend,
258
- environments: plan.environments.map((environment) => environment.id),
259
- specWorkflow: plan.specWorkflow.id,
260
- agents: plan.agents.map((agent) => agent.id),
261
- ...(plan.defaultAgent ? { defaultAgent: plan.defaultAgent.id } : {}),
262
- governanceProfile: plan.governanceProfile.id
263
- }, null, 2));
264
- add('env-example', 'configuration', ['.env.example'], renderEnvExample(plan));
265
- add('backend-dockerfile', 'runtime', ['Dockerfile'], plan.workload === 'genai' ? renderBackendDockerfile() : renderStandardDockerfile(plan));
266
- }
267
- function addPowerAppsProjectConfig(add, plan) {
268
- add('liftoff-config', 'project', ['liftoff.config.json'], JSON.stringify({
269
- projectName: plan.projectName,
270
- projectType: plan.projectType.id,
271
- specWorkflow: plan.specWorkflow.id,
272
- agents: plan.agents.map((agent) => agent.id),
273
- ...(plan.defaultAgent ? { defaultAgent: plan.defaultAgent.id } : {}),
274
- codeAppsPlugin: plan.codeAppsPlugin,
275
- governanceProfile: plan.governanceProfile.id
276
- }, null, 2));
277
- }
278
- function addBackendArtifacts(add, plan) {
279
- const routeModule = pyModule(genAiPattern(plan).id);
280
- add('backend-pyproject', 'backend', ['backend', 'pyproject.toml'], renderBackendPyproject(plan));
281
- add('backend-uv-lock', 'backend', ['backend', 'uv.lock'], renderPythonLock('genai', `${plan.safeProjectName}-backend`));
282
- add('backend-package', 'backend', ['backend', '__init__.py'], '');
283
- add('backend-api-package', 'backend', ['backend', 'apis', '__init__.py'], '');
284
- add('backend-main', 'backend', ['backend', 'apis', 'main.py'], renderFastApiMain(plan, routeModule));
285
- add('backend-health-routes', 'backend', ['backend', 'apis', 'routes', 'health.py'], renderHealthRoutes());
286
- add('backend-pattern-routes', 'backend', ['backend', 'apis', 'routes', `${routeModule}.py`], renderPatternRoutes(plan));
287
- add('backend-routes-package', 'backend', ['backend', 'apis', 'routes', '__init__.py'], '');
288
- add('backend-auth-dependency', 'backend', ['backend', 'apis', 'dependencies', 'auth.py'], renderAuthDependency());
289
- add('backend-config-package', 'backend', ['backend', 'config', '__init__.py'], '');
290
- add('backend-settings', 'backend', ['backend', 'config', 'settings.py'], renderSettings(plan));
291
- add('backend-orchestration-package', 'backend', ['backend', 'orchestration', '__init__.py'], '');
292
- add('backend-model-config', 'backend', ['backend', 'orchestration', 'model_config.py'], renderModelConfig(plan));
293
- add('backend-messaging-tool', 'backend', ['backend', 'orchestration', 'tools', 'messaging.py'], renderMessagingBoundary());
294
- add('backend-tools-package', 'backend', ['backend', 'orchestration', 'tools', '__init__.py'], '');
295
- add('backend-observability', 'backend', ['backend', 'observability', 'tracing.py'], renderTracing());
296
- add('backend-observability-package', 'backend', ['backend', 'observability', '__init__.py'], '');
297
- add('backend-test-health', 'backend-test', ['backend', 'tests', 'test_health.py'], renderBackendHealthTest());
298
- add('backend-test-messaging', 'backend-test', ['backend', 'tests', 'test_messaging.py'], renderMessagingTest());
299
- add('backend-test-tracing', 'backend-test', ['backend', 'tests', 'test_tracing.py'], renderTracingTest());
300
- }
301
- function addDatabaseArtifacts(add, plan) {
302
- add('database-alembic-ini', 'database', ['database', 'alembic.ini'], renderAlembicIni());
303
- add('database-alembic-env', 'database', ['database', 'migrations', 'env.py'], renderAlembicEnv());
304
- add('database-initial-migration', 'database', ['database', 'migrations', 'versions', '0001_initial.py'], renderInitialMigration(plan));
305
- add('database-schema', 'database', ['database', 'models', 'schema.sql'], renderDatabaseSchema(plan));
306
- }
307
- function addPatternArtifacts(add, plan) {
308
- const pattern = genAiPattern(plan);
309
- const routeModule = pyModule(pattern.id);
310
- add('pattern-agent', 'pattern', ['backend', 'orchestration', 'agents', `${routeModule}_agent.py`], renderPatternAgent(plan));
311
- add('pattern-agent-test', 'backend-test', ['backend', 'tests', `test_${routeModule}_orchestration.py`], renderPatternAgentTest(plan));
312
- add('pattern-prompt', 'pattern', ['backend', 'orchestration', 'prompts', `${pattern.id}.md`], renderPromptTemplate(plan));
313
- add('pattern-agent-package', 'pattern', ['backend', 'orchestration', 'agents', '__init__.py'], '');
314
- add('pattern-prompt-readme', 'pattern', ['backend', 'orchestration', 'prompts', 'README.md'], renderPromptReadme());
315
- if (pattern.id === 'rag') {
316
- add('rag-vector-store', 'pattern', ['backend', 'orchestration', 'retrieval', 'vector_store.py'], renderVectorStore());
317
- add('rag-retrieval-package', 'pattern', ['backend', 'orchestration', 'retrieval', '__init__.py'], '');
318
- }
319
- if (pattern.worker) {
320
- add('pattern-worker', 'pattern', ['backend', 'workers', `${routeModule}_worker.py`], renderPatternWorker(plan));
321
- add('backend-workers-package', 'pattern', ['backend', 'workers', '__init__.py'], '');
322
- }
323
- if (pattern.id === 'fine-tuned') {
324
- add('fine-tuned-eval-dataset', 'pattern', ['backend', 'evaluation', 'datasets', 'sample.jsonl'], '{"input":"Example request","expected":"Expected response placeholder"}');
325
- }
326
- }
327
- function addFunctionArtifacts(add, plan) {
328
- if (!hasFunctionWorker(plan)) {
329
- return;
330
- }
331
- const workerName = functionWorkerName(plan);
332
- const workerBase = ['functions', workerName];
333
- add('functions-readme', 'functions', ['functions', 'README.md'], renderFunctionsReadme());
334
- add('function-worker-readme', 'functions', [...workerBase, 'README.md'], renderFunctionWorkerReadme(plan));
335
- add('function-worker-host', 'functions', [...workerBase, 'host.json'], renderFunctionHostJson());
336
- add('function-worker-local-settings', 'functions', [...workerBase, 'local.settings.example.json'], renderFunctionLocalSettings(plan));
337
- add('function-worker-requirements', 'functions', [...workerBase, 'requirements.txt'], renderFunctionRequirements());
338
- add('function-worker-app', 'functions', [...workerBase, 'function_app.py'], renderFunctionApp(plan));
339
- add('function-worker-test', 'functions-test', [...workerBase, 'tests', 'test_function_app.py'], renderFunctionTest());
340
- add('function-worker-funcignore', 'functions', [...workerBase, '.funcignore'], renderFunctionFuncIgnore());
341
- add('function-worker-gitignore', 'functions', [...workerBase, '.gitignore'], renderFunctionGitIgnore());
342
- }
343
- function addEnvironmentArtifacts(artifacts, plan) {
344
- for (const environment of plan.environments) {
345
- const add = createArtifactAdder(artifacts, 'project', `environment:${environment.id}`);
346
- add(`environment-${environment.id}-backend`, 'environment', ['environments', environment.id, 'backend.env'], plan.workload === 'genai' ? renderBackendEnv(plan, environment.id) : renderStandardEnv(plan, environment.id));
347
- if (plan.workload === 'genai' && hasFunctionWorker(plan)) {
348
- add(`environment-${environment.id}-functions`, 'environment', ['environments', environment.id, 'functions.env'], renderFunctionsEnv(plan, environment.id));
349
- }
350
- }
351
- }
352
- function addDockerArtifacts(add, plan) {
353
- add('docker-compose', 'local-development', ['docker-compose.yml'], renderDockerCompose(plan));
354
- }
355
- function addInfrastructureArtifacts(add, artifacts, plan) {
356
- const base = ['infrastructure', 'opentofu', 'azure'];
357
- add('opentofu-versions', 'infrastructure', [...base, 'versions.tf'], renderOpenTofuVersions());
358
- add('opentofu-provider-lock', 'infrastructure', [...base, '.terraform.lock.hcl'], renderOpenTofuProviderLock());
359
- add('opentofu-providers', 'infrastructure', [...base, 'providers.tf'], renderTofuProviders());
360
- add('opentofu-variables', 'infrastructure', [...base, 'variables.tf'], renderTofuVariables(plan));
361
- add('opentofu-main', 'infrastructure', [...base, 'main.tf'], renderTofuMain(plan));
362
- add('opentofu-outputs', 'infrastructure', [...base, 'outputs.tf'], renderTofuOutputs(plan));
363
- add('opentofu-local-state', 'infrastructure', [...base, 'backend.local.tf'], renderTofuLocalState());
364
- add('opentofu-remote-state-example', 'infrastructure', [...base, 'backend.remote.example.tf'], renderTofuRemoteStateExample());
365
- add('opentofu-readme', 'infrastructure', [...base, 'README.md'], renderTofuReadme(plan));
366
- for (const environment of plan.environments) {
367
- createArtifactAdder(artifacts, 'project', `environment:${environment.id}`)(`opentofu-${environment.id}-tfvars`, 'infrastructure', [...base, 'environments', `${environment.id}.tfvars`], renderTofuTfvars(plan, environment.id));
368
- }
369
- }
370
- function addSpecWorkflowArtifacts(addSeed, addFramework, plan) {
371
- if (plan.workload === 'power-apps-code-app') {
372
- if (plan.specWorkflow.id === 'openspec') {
373
- const changeName = `bootstrap-${plan.safeProjectName}`;
374
- addSeed('openspec-config', 'seed', ['openspec', 'config.yaml'], renderPowerAppsOpenSpecConfig(plan));
375
- addSeed('openspec-seed-change-metadata', 'seed', ['openspec', 'changes', changeName, '.openspec.yaml'], 'schema: spec-driven');
376
- addSeed('openspec-seed-proposal', 'seed', ['openspec', 'changes', changeName, 'proposal.md'], renderPowerAppsSeedProposal(plan));
377
- addSeed('openspec-seed-design', 'seed', ['openspec', 'changes', changeName, 'design.md'], renderPowerAppsSeedDesign(plan));
378
- addSeed('openspec-seed-tasks', 'seed', ['openspec', 'changes', changeName, 'tasks.md'], renderSeedTasks(plan));
379
- addSeed('openspec-seed-spec', 'seed', ['openspec', 'changes', changeName, 'specs', seedCapabilityId(plan), 'spec.md'], renderSeedSpec(plan));
380
- addSeed('openspec-spec-placeholder', 'seed', ['openspec', 'specs', '.gitkeep'], '');
381
- }
382
- else {
383
- addSeed('spec-kit-constitution', 'seed', ['.specify', 'memory', 'constitution.md'], renderPowerAppsSpecKitConstitution(plan));
384
- addFramework('spec-kit-spec-template', 'framework', ['.specify', 'templates', 'spec-template.md'], renderSpecKitSpecTemplate());
385
- addFramework('spec-kit-plan-template', 'framework', ['.specify', 'templates', 'plan-template.md'], renderSpecKitPlanTemplate());
386
- addSeed('specs-placeholder', 'seed', ['specs', '.gitkeep'], '');
387
- }
388
- return;
389
- }
390
- if (plan.specWorkflow.id === 'openspec') {
391
- const changeName = `bootstrap-${plan.safeProjectName}`;
392
- addSeed('openspec-config', 'seed', ['openspec', 'config.yaml'], renderOpenSpecConfig(plan));
393
- addSeed('openspec-seed-change-metadata', 'seed', ['openspec', 'changes', changeName, '.openspec.yaml'], 'schema: spec-driven');
394
- addSeed('openspec-seed-proposal', 'seed', ['openspec', 'changes', changeName, 'proposal.md'], renderSeedProposal(plan));
395
- addSeed('openspec-seed-design', 'seed', ['openspec', 'changes', changeName, 'design.md'], renderSeedDesign(plan));
396
- addSeed('openspec-seed-tasks', 'seed', ['openspec', 'changes', changeName, 'tasks.md'], renderSeedTasks(plan));
397
- addSeed('openspec-seed-spec', 'seed', ['openspec', 'changes', changeName, 'specs', seedCapabilityId(plan), 'spec.md'], renderSeedSpec(plan));
398
- addSeed('openspec-spec-placeholder', 'seed', ['openspec', 'specs', '.gitkeep'], '');
399
- }
400
- else {
401
- addSeed('spec-kit-constitution', 'seed', ['.specify', 'memory', 'constitution.md'], renderSpecKitConstitution(plan));
402
- addFramework('spec-kit-spec-template', 'framework', ['.specify', 'templates', 'spec-template.md'], renderSpecKitSpecTemplate());
403
- addFramework('spec-kit-plan-template', 'framework', ['.specify', 'templates', 'plan-template.md'], renderSpecKitPlanTemplate());
404
- addSeed('specs-placeholder', 'seed', ['specs', '.gitkeep'], '');
405
- }
406
- }
407
- function renderPowerAppsOpenSpecConfig(plan) {
408
- return `schema: spec-driven${renderOpenSpecCopilotConfig(plan)}
409
- context: |
410
- Project: ${plan.projectName}
411
- Workload: Microsoft Power Apps code app
412
- Stack: React, Vite, TypeScript, Tailwind CSS, Power Apps SDK
413
- Starter: ${plan.starter.repository}/${plan.starter.path}@${plan.starter.commit}
414
- Runtime boundary: browser-hosted code app with connector-first data access
415
- Environment binding: explicit post-scaffold Power Apps CLI action
416
- rules:
417
- proposal:
418
- - Keep tenant, environment, connection, solution, and deployment choices explicit.
419
- design:
420
- - Prefer Power Platform connectors and generated service modules over custom APIs.
421
- - Do not add Liftoff-owned Azure or API infrastructure unless a later change explicitly introduces it.
422
- tasks:
423
- - Include npm lint and build verification.
424
- `;
425
- }
426
- function renderOpenSpecCopilotConfig(plan) {
427
- return plan.agents.some((agent) => agent.id === 'github-copilot')
428
- ? `
429
- githubCopilot:
430
- cloudAgent: ${plan.copilotCloud}
431
- `
432
- : '\n';
433
- }
434
- function renderPowerAppsSeedProposal(plan) {
435
- const capability = seedCapabilityId(plan);
436
- return `# Proposal: bootstrap-${plan.safeProjectName}
437
-
438
- ## Why
439
-
440
- Bootstrap the generated ${plan.projectName} Power Apps code app baseline created by Mission Control Liftoff.
441
-
442
- ## What Changes
443
-
444
- - Establish the generated React, Vite, TypeScript, Tailwind CSS, Power Apps SDK, workflow, and governance baseline.
445
- - Confirm that tenant binding, authentication, connector choices, solution packaging, deployment, and domain-specific product behavior are deferred to follow-up changes.
446
-
447
- ## Capabilities
448
-
449
- ### New Capabilities
450
-
451
- - \`${capability}\`: Generated Power Apps code app baseline for this Liftoff project.
452
-
453
- ### Modified Capabilities
454
-
455
- - None.
456
-
457
- ## Impact
458
-
459
- - Generated Power Apps starter files, package metadata, workflow files, and governance files.
460
- - No Liftoff-owned backend API, Docker stack, Azure resources, \`power.config.json\`, tenant binding, credentials, or deployment.
461
- `;
462
- }
463
- function renderPowerAppsSeedDesign(plan) {
464
- return `# Design: bootstrap-${plan.safeProjectName}
465
-
466
- ## Context
467
-
468
- The project uses React, Vite, TypeScript, Tailwind CSS, and Microsoft's Power Apps SDK.
469
- It was initialized from \`${plan.starter.path}\` at \`${plan.starter.commit}\`.
470
-
471
- ## Goals / Non-Goals
472
-
473
- **Goals:**
474
-
475
- - Keep the generated baseline aligned to the approved Mission Control Power Apps code app stack.
476
- - Verify only deterministic local scaffold checks before archiving this bootstrap change.
477
-
478
- **Non-Goals:**
479
-
480
- - Define domain-specific product behavior, including screens, connector behavior, tenant binding, solution packaging, deployment, or credentials, in the bootstrap change.
481
-
482
- ## Decisions
483
-
484
- - Keep pages under \`src/pages\` and shared UI under \`src/components\`.
485
- - Keep connector and generated service boundaries explicit under \`src\`.
486
- - Use TanStack Query for server state and Zustand only for local client state.
487
- - Bind to a tenant and environment only through Microsoft's Power Apps CLI.
488
-
489
- ## Risks / Trade-offs
490
-
491
- - Follow-up product changes own real business behavior; this seed only proves the generated local baseline is coherent.
492
- `;
493
- }
494
- function renderPowerAppsSpecKitConstitution(plan) {
495
- return `# ${plan.projectName} Constitution
496
-
497
- ## I. Power Apps workload boundary
498
-
499
- The application MUST remain a Power Apps code app using React, Vite, TypeScript, Tailwind CSS,
500
- the Power Apps SDK, and the Power Apps Vite plugin. Tenant and environment binding MUST remain
501
- an explicit developer action.
502
-
503
- ## II. Connector-first integration
504
-
505
- Features MUST prefer Power Platform connectors and generated service modules. A custom API or
506
- Azure resource requires an explicit specification and MUST NOT be inferred from this scaffold.
507
-
508
- ## III. Project structure
509
-
510
- Pages belong under \`src/pages\`, shared UI under \`src/components\`, and reusable providers or
511
- connector boundaries under named \`src\` modules. \`power.config.json\` is never fabricated.
512
-
513
- ## IV. Quality gates
514
-
515
- Every change MUST pass the applicable npm lint and build checks plus \`liftoff validate\`.
516
- `;
517
- }
518
- function addFrontendArtifacts(add, plan) {
519
- add('frontend-package', 'frontend', ['frontend', 'package.json'], renderFrontendPackage(plan));
520
- add('frontend-lock', 'frontend', ['frontend', 'package-lock.json'], renderNpmLock('frontend', `${plan.safeProjectName}-frontend`));
521
- add('frontend-index', 'frontend', ['frontend', 'index.html'], renderFrontendIndex(plan));
522
- add('frontend-main', 'frontend', ['frontend', 'src', 'main.ts'], renderFrontendMain());
523
- add('frontend-app', 'frontend', ['frontend', 'src', 'App.vue'], renderFrontendApp(plan));
524
- add('frontend-env-example', 'frontend', ['frontend', '.env.example'], 'VITE_API_BASE_URL=http://localhost:8000');
525
- add('frontend-styles', 'frontend', ['frontend', 'src', 'styles.css'], renderFrontendStyles());
526
- add('frontend-vite-config', 'frontend', ['frontend', 'vite.config.ts'], renderFrontendViteConfig());
527
- add('frontend-tailwind-config', 'frontend', ['frontend', 'tailwind.config.ts'], renderFrontendTailwindConfig());
528
- add('frontend-dockerfile', 'frontend', ['frontend', 'Dockerfile'], renderFrontendDockerfile());
529
- }
530
- function renderDirectBuildAndTestGuide(plan) {
531
- let backendCommands;
532
- if (plan.workload === 'genai' || plan.apiStack.id === 'python-fastapi') {
533
- const extras = plan.workload === 'genai' && hasFunctionWorker(plan)
534
- ? ' --extra functions'
535
- : '';
536
- backendCommands = `uv sync --frozen --project backend --extra test${extras}
537
- uv run --project backend python -m pytest -q backend/tests`;
538
- }
539
- else if (plan.apiStack.id === 'node-fastify') {
540
- backendCommands = `cd backend
541
- npm ci
542
- npm run build
543
- npm test`;
544
- }
545
- else {
546
- backendCommands = `cd backend
547
- go mod download
548
- go test ./...`;
549
- }
550
- const frontendCommands = plan.includeFrontend ? `
551
-
552
- Build the frontend without a running backend:
553
-
554
- \`\`\`bash
555
- cp frontend/.env.example frontend/.env
556
- cd frontend
557
- npm ci
558
- npm run build
559
- \`\`\`
560
- ` : '';
561
- const functionCommands = plan.workload === 'genai' && hasFunctionWorker(plan) ? `
562
-
563
- Run the Function worker unit tests from the same locked Python environment:
564
-
565
- \`\`\`bash
566
- (cd functions/${functionWorkerName(plan)} && uv run --project ../../backend --directory . python -m pytest -q)
567
- \`\`\`
568
- ` : '';
569
- return `## Project Dependencies, Build, And Test
570
-
571
- \`liftoff init --install-dependencies\` runs the selected stack's locked project-local dependency commands. To run or resume them manually:
572
-
573
- \`\`\`bash
574
- ${backendCommands}
575
- \`\`\`
576
-
577
- ${frontendCommands}${functionCommands}`;
578
- }
579
- function renderDeterministicSetupGuide(plan) {
580
- const backend = plan.workload === 'genai' || plan.apiStack.id === 'python-fastapi'
581
- ? '`uv run --project backend python -m pytest -q backend/tests`'
582
- : plan.apiStack.id === 'node-fastify'
583
- ? '`npm test` from `backend/` after `npm ci` and `npm run build`'
584
- : '`go test ./...` from `backend/`';
585
- const frontend = plan.includeFrontend
586
- ? '- Frontend build: `npm run build` from `frontend/` after `npm ci`.'
587
- : '- Frontend build: inapplicable because no frontend was generated.';
588
- const governance = plan.governanceProfile.id === 'none'
589
- ? 'Repository governance is disabled, so there is no `/liftoff-setup` integration, managed phase graph, or post-init governance activation path.'
590
- : 'Run `/liftoff-setup` from a selected agent. It completes, syncs, and archives the generated bootstrap seed, then stops at explicit authority gates. Commit and push are separate approvals, and rerunning `/liftoff-setup` resumes idempotently without repeating verified phases.';
591
- return `## Deterministic Setup
592
-
593
- ${governance}
594
-
595
- The local baseline contains only applicable checks:
596
-
597
- - \`liftoff validate\`
598
- - Backend tests: ${backend}
599
- ${frontend}
600
- - \`docker compose config -q\`
601
- - \`tofu fmt -check -recursive\`
602
- - \`tofu init -backend=false\`
603
- - \`tofu validate\`
604
- - strict ${plan.specWorkflow.label} validation
605
-
606
- No baseline step runs a live OpenTofu plan or apply, starts containers, deploys,
607
- mutates GitHub, or asks for cloud credentials. Absent components are recorded as
608
- inapplicable rather than simulated. After setup archives the seed, use normal
609
- OpenSpec or Spec Kit changes for features and the governed GitFlow release path
610
- after activation evidence is green.
611
- `;
612
- }
613
- function renderAdvisoryReadinessGuide(plan) {
614
- const selected = [
615
- 'docker',
616
- 'opentofu',
617
- ...(plan.provider.id === 'azure' ? ['azure-cli'] : [])
618
- ];
619
- return selected.map((id) => {
620
- const requirement = workstationRequirementCatalog[id];
621
- const mac = requirement.install.darwin;
622
- const windows = requirement.install.win32;
623
- const commands = [
624
- ...(mac ? [`macOS: \`${formatCommand(mac.command)}\``] : []),
625
- ...(windows ? [`Windows: \`${formatCommand(windows.command)}\``] : []),
626
- `Linux: ${requirement.linuxRemedies.unknown}`
627
- ].join('; ');
628
- const health = id === 'docker'
629
- ? ' After installation, start Docker Desktop or the Docker daemon.'
630
- : id === 'azure-cli'
631
- ? ' After installation, run `az login` if authentication is not ready.'
632
- : '';
633
- return `- ${requirement.label}: ${commands}.${health}`;
634
- }).join('\n');
635
- }
636
- function renderSpecWorkflowGuide(plan) {
637
- const agents = plan.agents.map((agent) => `${agent.label}${plan.defaultAgent?.id === agent.id ? ' (default integration)' : ''}`).join(', ');
638
- const ownership = plan.specWorkflow.id === 'openspec'
639
- ? 'OpenSpec workflow skills, commands, configuration, and optional cloud-agent files'
640
- : 'Spec Kit core files, integration state, and the selected Copilot or Claude skill integrations';
641
- const openSpecDetails = plan.specWorkflow.id === 'openspec'
642
- ? [
643
- `- OpenSpec profile: ${OPEN_SPEC_PROFILE}; delivery: ${OPEN_SPEC_DELIVERY}; workflows: ${OPEN_SPEC_WORKFLOW_IDS.join(', ')}`,
644
- ...(plan.agents.some((agent) => agent.id === 'github-copilot')
645
- ? ['- GitHub Copilot cloud agent: controlled by `githubCopilot.cloudAgent` in `openspec/config.yaml`']
646
- : [])
647
- ].join('\n')
648
- : '';
649
- return `## Spec-Driven Workflow And Validation
650
-
651
- - Workflow: ${plan.specWorkflow.label} ${plan.framework.version}
652
- - AI coding agents: ${agents}
653
- ${openSpecDetails ? `${openSpecDetails}\n` : ''}
654
- - Framework ownership: the official initializer owns ${ownership}. Liftoff validates these files but excludes framework-owned output and one-time seed content from managed-core hashes.
655
- - Deferred tools: advisory workstation checks may be deferred. Liftoff never claims they are installed and never installs them without \`--install-tools\`.
656
-
657
- If \`liftoff doctor\` reports a selected advisory tool as missing, use its registered readiness remedy:
658
-
659
- ${renderAdvisoryReadinessGuide(plan)}
660
-
661
- Validate the scaffold and workstation after setup:
662
-
663
- \`\`\`bash
664
- liftoff validate
665
- liftoff doctor
666
- \`\`\`
667
-
668
- For an existing OpenSpec project, change workflow delivery with \`openspec config profile\` and refresh framework-owned files with \`openspec update\`. Plain \`liftoff update\` does not regenerate OpenSpec integrations.
669
- `;
670
- }
671
- function renderGeneratedConfigurationGuide(plan) {
672
- const frontendConfiguration = plan.includeFrontend
673
- ? '\n- `frontend/.env` configures `VITE_API_BASE_URL`; the production build does not contact the backend.'
674
- : '';
675
- if (plan.workload === 'standard') {
676
- return `## Runtime Configuration
677
-
678
- Copy \`.env.example\` to \`.env\` before running outside Docker Compose. The backend requires \`DATABASE_URL\` and \`REDIS_URL\`. \`CORS_ALLOWED_ORIGINS\` is a comma-separated allowlist and defaults to the local frontend at \`http://localhost:5173\`.${frontendConfiguration}
679
- `;
680
- }
681
- return `## Starter Integration Configuration
682
-
683
- Copy \`.env.example\` to \`.env\`, then configure only the integrations you use:
684
-
685
- - \`PYDANTIC_AI_MODEL\` is required when production orchestration is invoked. If it is absent, the agent raises an explicit configuration error rather than returning a placeholder answer.
686
- - Redis Streams uses \`REDIS_URL\` and \`REDIS_STREAM_NAME\`.
687
- - Azure Service Bus uses \`SERVICE_BUS_QUEUE_NAME\` plus either \`SERVICE_BUS_CONNECTION_STRING\` or \`SERVICE_BUS_FULLY_QUALIFIED_NAMESPACE\`; set \`AZURE_CLIENT_ID\` when selecting a user-assigned managed identity.
688
- - Langfuse requires both \`LANGFUSE_PUBLIC_KEY\` and \`LANGFUSE_SECRET_KEY\`, with optional \`LANGFUSE_HOST\`. Without both keys, tracing is explicitly disabled and no remote trace ID is reported.${frontendConfiguration}
689
- - \`CORS_ALLOWED_ORIGINS\` is a comma-separated frontend-origin allowlist and defaults to \`http://localhost:5173\`.
690
- `;
691
- }
692
- function renderGeneratedUpdateGuide(plan) {
693
- const governance = plan.governanceProfile.id === 'none'
694
- ? 'Repository governance is disabled for this project, so Liftoff does not generate setup integrations, a managed phase graph, credential-policy schema, or post-init setup command.'
695
- : '`single-maintainer-gitflow` repository governance generates deterministic setup artifacts only. Review `.liftoff/governance/README.md`, then run `/liftoff-setup` from a selected agent. Live enforcement requires evidence and explicit approval; it is never inferred from generated files.';
696
- return `## Safe Liftoff Updates
697
-
698
- \`liftoff upgrade\` replaces a supported global Liftoff CLI installation; it does not inspect or modify this project. Check and apply CLI replacement separately with \`liftoff upgrade --check\` and \`liftoff upgrade\`.
699
-
700
- ${governance}
701
-
702
- \`liftoff update\` maintains only explicit Liftoff core files, currently the repository-governance policy, context, guide, phase graph, credential-policy schema, and selected-agent \`/liftoff-setup\` integrations. Use \`liftoff update --check\` for a read-only core report, or \`liftoff update --check --json\` as an automation gate that exits 0 when core state is clean and 2 when maintenance is available.
703
-
704
- Application source, tests, dependencies and locks, schemas, containers, environment files, documentation, and infrastructure become project-owned after generation. No update mode, including \`--force\`, can restore or replace them. Enabling a previously absent frontend or environment in \`liftoff.config.json\` may provision that component once at absent destinations; a collision blocks the whole component and cannot be forced.
705
-
706
- Project template modernization is a separately reviewed production change and is not performed by ordinary update or by the existing non-Liftoff \`migrate\` command. Managed-core conflicts are skipped by default; after reviewing every listed core path, \`liftoff update --force\` may replace only those core conflicts. Managed-core orphans remain on disk, and update never installs dependencies. A failed transaction is rolled back, but Liftoff retains no backup after a successful core overwrite.
707
-
708
- Liftoff rejects malformed, traversal, absolute, drive-qualified, UNC, separator-containing, or symlink-escaping manifest paths before artifact access. If the manifest is unsafe or malformed, restore \`liftoff.manifest.json\` from version control or regenerate the project with a matching Liftoff version; do not hand-edit unsafe paths. Run \`liftoff <command> --help\` for command-specific syntax because unknown flags, subcommands, values, and extra arguments fail before any write.
709
- `;
710
- }
711
- function renderRootReadme(plan) {
712
- if (plan.workload === 'standard') {
713
- return `# ${plan.projectName}
714
-
715
- Generated by Mission Control Liftoff.
716
-
717
- ## Stack
718
-
719
- - Project type: Standard application
720
- - API: ${plan.apiStack.label}
721
- - Database tooling: ${plan.apiStack.databaseTooling}
722
- - API reference: Scalar with OpenAPI
723
- - Cloud: ${plan.provider.label} (${plan.region.slug})
724
- - Infrastructure: OpenTofu
725
- - Database: PostgreSQL
726
- - Cache and local messaging: Redis
727
- - Local development: Docker Compose
728
- ${plan.includeFrontend ? '- Frontend: Vue 3 with Tailwind\n' : ''}
729
- ${renderDeterministicSetupGuide(plan)}
730
- ## Local Development
731
-
732
- \`\`\`bash
733
- docker compose up --build
734
- \`\`\`
735
-
736
- The backend API is available on port 8000. Health and readiness endpoints are available at \`/health\` and \`/ready\`; Scalar is exposed at \`/scalar\`.
737
-
738
- ${renderGeneratedConfigurationGuide(plan)}
739
- ${renderDirectBuildAndTestGuide(plan)}
740
- ${renderGeneratedUpdateGuide(plan)}
741
- ## Infrastructure
742
-
743
- \`\`\`bash
744
- cd infrastructure/opentofu/azure
745
- tofu init
746
- tofu plan -var-file=environments/dev.tfvars
747
- tofu apply -var-file=environments/dev.tfvars
748
- \`\`\`
749
-
750
- The first apply uses a public bootstrap image. Follow \`infrastructure/opentofu/azure/README.md\` to build the generated backend in ACR and apply its image.
751
-
752
- ${renderSpecWorkflowGuide(plan)}
753
- `;
754
- }
755
- const pattern = genAiPattern(plan);
756
- const functionsStackLine = hasFunctionWorker(plan) ? `- Azure Functions worker: Python v2 Service Bus trigger under \`functions/${functionWorkerName(plan)}\`
757
- ` : '';
758
- const functionsSection = hasFunctionWorker(plan) ? `
759
- ## Azure Functions Worker
760
-
761
- Azure Functions trigger adapters live under \`functions/${functionWorkerName(plan)}\`. Keep reusable GenAI orchestration under \`backend/orchestration\`; \`backend/workers\` remains the place for backend-adjacent or containerized worker code.
762
- ` : '';
763
- const genericSection = pattern.id === 'generic' ? `
764
- ## Choosing A Specialization Later
765
-
766
- This project intentionally starts with a neutral text-in/result-out GenAI boundary. It does not assume RAG, conversation history, agent tools, streaming, fine-tuning, multi-agent coordination, or workflow orchestration.
767
-
768
- Generated application files are project-owned. When requirements become clear, specialize through a reviewed project change; \`liftoff update\` and \`--force\` do not convert this project to another GenAI pattern.
769
- ` : '';
770
- return `# ${plan.projectName}
771
-
772
- Generated by Mission Control Liftoff.
773
-
774
- ## Stack
775
-
776
- - Backend: FastAPI, PydanticAI, Pydantic settings, Scalar
777
- - Pattern: ${pattern.label}
778
- - Cloud: ${plan.provider.label} (${plan.region.slug})
779
- - Infrastructure: OpenTofu
780
- - Database: PostgreSQL with Alembic migrations${pattern.id === 'rag' ? ' and pgvector retrieval' : ''}
781
- - Cache and local messaging: Redis
782
- - Observability: Langfuse
783
- - Local development: Docker Compose
784
- ${functionsStackLine}
785
- ${plan.includeFrontend ? '- Frontend: Vue 3 with Tailwind\n' : ''}
786
- ${renderDeterministicSetupGuide(plan)}
787
- ## Local Development
788
-
789
- \`\`\`bash
790
- docker compose up --build
791
- docker compose --profile observability up --build
792
- \`\`\`
793
-
794
- The backend API is available on port 8000. Scalar is exposed at \`/scalar\`.
795
-
796
- ${renderGeneratedConfigurationGuide(plan)}
797
- ${renderDirectBuildAndTestGuide(plan)}
798
- ${renderGeneratedUpdateGuide(plan)}
799
- ## Infrastructure
800
-
801
- \`\`\`bash
802
- cd infrastructure/opentofu/azure
803
- tofu init
804
- tofu plan -var-file=environments/dev.tfvars
805
- tofu apply -var-file=environments/dev.tfvars
806
- \`\`\`
807
-
808
- The first apply uses a public bootstrap image. Follow \`infrastructure/opentofu/azure/README.md\` to build the generated backend in ACR and apply its image.
809
-
810
- ${renderSpecWorkflowGuide(plan)}
811
- ${functionsSection}
812
- ${genericSection}
813
- `;
814
- }
815
- function renderGeneratedGitignore() {
816
- return `.venv/
817
- __pycache__/
818
- .pytest_cache/
819
- node_modules/
820
- dist/
821
- .env
822
- migration/legacy/
823
- *.tfstate
824
- *.tfstate.*
825
- .terraform/
826
- `;
827
- }
828
- function renderEnvExample(plan) {
829
- if (plan.workload === 'standard') {
830
- return renderStandardEnv(plan);
831
- }
832
- const pattern = genAiPattern(plan);
833
- return `APP_ENV=dev
834
- APP_NAME=${plan.safeProjectName}
835
- GENAI_PATTERN=${pattern.id}
836
- CLOUD_PROVIDER=${plan.provider.id}
837
- AZURE_REGION=${plan.region.slug}
838
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@postgres:5432/${plan.safeProjectName.replace(/-/g, '_')}
839
- REDIS_URL=redis://redis:6379/0
840
- REDIS_STREAM_NAME=liftoff-events
841
- MESSAGING_TRANSPORT=redis-streams
842
- SERVICE_BUS_QUEUE_NAME=${DEFAULT_FUNCTION_WORKER_QUEUE_NAME}
843
- SERVICE_BUS_CONNECTION_STRING=
844
- SERVICE_BUS_FULLY_QUALIFIED_NAMESPACE=
845
- AZURE_CLIENT_ID=
846
- BLOB_ENDPOINT=http://azurite:10000/devstoreaccount1
847
- CORS_ALLOWED_ORIGINS=http://localhost:5173
848
- PYDANTIC_AI_MODEL=
849
- LANGFUSE_HOST=http://langfuse:3000
850
- LANGFUSE_PUBLIC_KEY=
851
- LANGFUSE_SECRET_KEY=
852
- `;
853
- }
854
- function renderBackendDockerfile() {
855
- return `FROM ${formatContainerImage(supportedStack.containers['uv-tool'])} AS uv
856
- FROM ${formatContainerImage(supportedStack.containers['python-runtime'])}
857
-
858
- WORKDIR /app
859
-
860
- ENV PYTHONDONTWRITEBYTECODE=1
861
- ENV PYTHONUNBUFFERED=1
862
- ENV PYTHONPATH=/app
863
- ENV PATH=/app/backend/.venv/bin:$PATH
864
- COPY --from=uv /uv /uvx /bin/
865
-
866
- ARG UV_DEFAULT_INDEX=https://pypi.org/simple
867
- COPY backend/pyproject.toml /app/backend/pyproject.toml
868
- COPY backend/uv.lock /app/backend/uv.lock
869
- RUN --mount=type=cache,target=/root/.cache/uv \\
870
- uv export --frozen --no-dev --no-emit-project --project /app/backend --output-file /tmp/requirements.txt \\
871
- && uv venv /app/backend/.venv \\
872
- && uv pip install --python /app/backend/.venv/bin/python --require-hashes \\
873
- --default-index "$UV_DEFAULT_INDEX" --requirements /tmp/requirements.txt \\
874
- && rm /tmp/requirements.txt
875
-
876
- COPY backend /app/backend
877
- COPY database /app/database
878
-
879
- EXPOSE 8000
880
- CMD ["uvicorn", "backend.apis.main:app", "--host", "0.0.0.0", "--port", "8000"]
881
- `;
882
- }
883
- function renderBackendPyproject(plan) {
884
- return renderPythonPyprojectAsset('genai', `${plan.safeProjectName}-backend`);
885
- }
886
- function renderFastApiMain(plan, routeModule) {
887
- return `from fastapi import FastAPI
888
- from fastapi.middleware.cors import CORSMiddleware
889
-
890
- try:
891
- from scalar_fastapi import get_scalar_api_reference
892
- except ImportError: # pragma: no cover - dependency is present in generated runtime
893
- get_scalar_api_reference = None
894
-
895
- from backend.apis.routes import health, ${routeModule}
896
- from backend.config.settings import get_settings
897
-
898
-
899
- settings = get_settings()
900
- app = FastAPI(title=settings.app_name, version="0.1.0")
901
- app.add_middleware(
902
- CORSMiddleware,
903
- allow_origins=[
904
- origin.strip()
905
- for origin in settings.cors_allowed_origins.split(",")
906
- if origin.strip()
907
- ],
908
- allow_methods=["*"],
909
- allow_headers=["*"],
910
- )
911
-
912
- app.include_router(health.router)
913
- app.include_router(${routeModule}.router)
914
-
915
-
916
- @app.get("/scalar", include_in_schema=False)
917
- def scalar_reference():
918
- if get_scalar_api_reference is None:
919
- return {"message": "Install scalar-fastapi to enable the Scalar developer portal."}
920
- return get_scalar_api_reference(openapi_url=app.openapi_url, title=f"{app.title} API")
921
- `;
922
- }
923
- function renderHealthRoutes() {
924
- return `from fastapi import APIRouter
925
-
926
- router = APIRouter(tags=["operations"])
927
-
928
-
929
- @router.get("/health")
930
- def health():
931
- return {"status": "ok"}
932
-
933
-
934
- @router.get("/ready")
935
- def ready():
936
- return {"status": "ready"}
937
- `;
938
- }
939
- function renderPatternRoutes(plan) {
940
- const pattern = genAiPattern(plan);
941
- const moduleName = pyModule(pattern.id);
942
- const agentName = `${moduleName}_agent`;
943
- const prefix = pattern.routePrefix;
944
- if (pattern.id === 'streaming') {
945
- return `from fastapi import APIRouter
946
- from fastapi.responses import StreamingResponse
947
-
948
- from backend.orchestration.agents.${agentName} import stream_response
949
-
950
- router = APIRouter(prefix="${prefix}", tags=["${pattern.id}"])
951
-
952
-
953
- @router.get("")
954
- def stream(prompt: str):
955
- return StreamingResponse(stream_response(prompt), media_type="text/event-stream")
956
- `;
957
- }
958
- if (pattern.id === 'rag') {
959
- return `from fastapi import APIRouter
960
- from pydantic import BaseModel
961
-
962
- from backend.orchestration.agents.${agentName} import answer_question, enqueue_ingestion
963
-
964
- router = APIRouter(prefix="${prefix}", tags=["rag"])
965
-
966
-
967
- class QueryRequest(BaseModel):
968
- question: str
969
-
970
-
971
- class IngestionRequest(BaseModel):
972
- source_uri: str
973
-
974
-
975
- @router.post("/query")
976
- async def query(request: QueryRequest):
977
- return await answer_question(request.question)
978
-
979
-
980
- @router.post("/ingest")
981
- async def ingest(request: IngestionRequest):
982
- return await enqueue_ingestion(request.source_uri)
983
- `;
984
- }
985
- const bodyClass = `${titleCase(pattern.id).replace(/\s/g, '')}Request`;
986
- return `from fastapi import APIRouter
987
- from pydantic import BaseModel
988
-
989
- from backend.orchestration.agents.${agentName} import run_${moduleName}
990
-
991
- router = APIRouter(prefix="${prefix}", tags=["${pattern.id}"])
992
-
993
-
994
- class ${bodyClass}(BaseModel):
995
- input: str
996
-
997
-
998
- @router.post("/run")
999
- async def run(request: ${bodyClass}):
1000
- return await run_${moduleName}(request.input)
1001
- `;
1002
- }
1003
- function renderAuthDependency() {
1004
- return `from dataclasses import dataclass
1005
-
1006
-
1007
- @dataclass(frozen=True)
1008
- class CurrentUser:
1009
- subject: str = "local-developer"
1010
-
1011
-
1012
- async def get_current_user() -> CurrentUser:
1013
- return CurrentUser()
1014
- `;
1015
- }
1016
- function renderSettings(plan) {
1017
- const pattern = genAiPattern(plan);
1018
- return `from functools import lru_cache
1019
- from pydantic_settings import BaseSettings, SettingsConfigDict
1020
-
1021
-
1022
- class Settings(BaseSettings):
1023
- model_config = SettingsConfigDict(env_file=".env", extra="ignore")
1024
-
1025
- app_name: str = ${sourceString(plan.projectName)}
1026
- app_env: str = "dev"
1027
- genai_pattern: str = "${pattern.id}"
1028
- cloud_provider: str = "${plan.provider.id}"
1029
- azure_region: str = "${plan.region.slug}"
1030
- database_url: str
1031
- redis_url: str
1032
- redis_stream_name: str = "liftoff-events"
1033
- messaging_transport: str = "redis-streams"
1034
- service_bus_queue_name: str = "${DEFAULT_FUNCTION_WORKER_QUEUE_NAME}"
1035
- service_bus_connection_string: str | None = None
1036
- service_bus_fully_qualified_namespace: str | None = None
1037
- azure_client_id: str | None = None
1038
- blob_endpoint: str | None = None
1039
- cors_allowed_origins: str = "http://localhost:5173"
1040
- pydantic_ai_model: str | None = None
1041
- langfuse_host: str | None = None
1042
- langfuse_public_key: str | None = None
1043
- langfuse_secret_key: str | None = None
1044
-
1045
-
1046
- @lru_cache
1047
- def get_settings() -> Settings:
1048
- return Settings()
1049
- `;
1050
- }
1051
- function renderModelConfig(plan) {
1052
- const pattern = genAiPattern(plan);
1053
- return `import os
1054
- from dataclasses import dataclass
1055
- from typing import Protocol
1056
-
1057
-
1058
- class ModelConfigurationError(RuntimeError):
1059
- pass
1060
-
1061
-
1062
- class AgentRunner(Protocol):
1063
- async def run(self, prompt: str) -> str:
1064
- ...
1065
-
1066
-
1067
- @dataclass(frozen=True)
1068
- class ModelConfig:
1069
- model_name: str
1070
- pattern: str = "${pattern.id}"
1071
-
1072
- @classmethod
1073
- def from_environment(cls) -> "ModelConfig":
1074
- model_name = os.getenv("PYDANTIC_AI_MODEL", "").strip()
1075
- if not model_name:
1076
- raise ModelConfigurationError(
1077
- "PYDANTIC_AI_MODEL is required before invoking production GenAI orchestration. "
1078
- "Use a PydanticAI model name such as 'openai:gpt-4.1-mini'."
1079
- )
1080
- return cls(model_name=model_name)
1081
-
1082
-
1083
- class PydanticAgentRunner:
1084
- def __init__(self, config: ModelConfig):
1085
- from pydantic_ai import Agent
1086
-
1087
- self._agent = Agent(config.model_name)
1088
-
1089
- async def run(self, prompt: str) -> str:
1090
- result = await self._agent.run(prompt)
1091
- output = getattr(result, "output", None)
1092
- if output is None:
1093
- output = getattr(result, "data", None)
1094
- if output is None:
1095
- raise RuntimeError("PydanticAI returned a result without output data.")
1096
- return str(output)
1097
-
1098
-
1099
- def build_agent_runner(config: ModelConfig | None = None) -> AgentRunner:
1100
- return PydanticAgentRunner(config or ModelConfig.from_environment())
1101
- `;
1102
- }
1103
- function renderMessagingBoundary() {
1104
- return `import json
1105
- import os
1106
- from collections.abc import Callable
1107
- from typing import Any, Protocol
1108
-
1109
-
1110
- class MessagingConfigurationError(RuntimeError):
1111
- pass
1112
-
1113
-
1114
- class MessagePublisher(Protocol):
1115
- async def publish(self, topic: str, payload: dict) -> None:
1116
- ...
1117
-
1118
-
1119
- class RedisStreamClient(Protocol):
1120
- async def xadd(self, name: str, fields: dict[str, str]) -> Any:
1121
- ...
1122
-
1123
-
1124
- class ServiceBusSender(Protocol):
1125
- async def __aenter__(self) -> "ServiceBusSender":
1126
- ...
1127
-
1128
- async def __aexit__(self, exc_type, exc, traceback) -> None:
1129
- ...
1130
-
1131
- async def send_messages(self, message: Any) -> None:
1132
- ...
1133
-
1134
-
1135
- class ServiceBusClient(Protocol):
1136
- def get_queue_sender(self, *, queue_name: str) -> ServiceBusSender:
1137
- ...
1138
-
1139
-
1140
- def _serialize(topic: str, payload: dict) -> str:
1141
- return json.dumps({"topic": topic, "payload": payload}, separators=(",", ":"), sort_keys=True)
1142
-
1143
-
1144
- class RedisStreamPublisher:
1145
- def __init__(self, client: RedisStreamClient, stream_name: str):
1146
- self._client = client
1147
- self._stream_name = stream_name
1148
-
1149
- async def publish(self, topic: str, payload: dict) -> None:
1150
- await self._client.xadd(
1151
- self._stream_name,
1152
- {"topic": topic, "payload": _serialize(topic, payload)},
1153
- )
1154
-
1155
-
1156
- class AzureServiceBusPublisher:
1157
- def __init__(
1158
- self,
1159
- client: ServiceBusClient,
1160
- queue_name: str,
1161
- message_factory: Callable[[str], Any] | None = None,
1162
- ):
1163
- self._client = client
1164
- self._queue_name = queue_name
1165
- self._message_factory = message_factory or self._default_message_factory
1166
-
1167
- @staticmethod
1168
- def _default_message_factory(body: str) -> Any:
1169
- from azure.servicebus import ServiceBusMessage
1170
-
1171
- return ServiceBusMessage(body)
1172
-
1173
- async def publish(self, topic: str, payload: dict) -> None:
1174
- message = self._message_factory(_serialize(topic, payload))
1175
- async with self._client.get_queue_sender(queue_name=self._queue_name) as sender:
1176
- await sender.send_messages(message)
1177
-
1178
-
1179
- def build_message_publisher(
1180
- transport: str,
1181
- *,
1182
- redis_client: RedisStreamClient | None = None,
1183
- service_bus_client: ServiceBusClient | None = None,
1184
- message_factory: Callable[[str], Any] | None = None,
1185
- ) -> MessagePublisher:
1186
- if transport == "redis-streams":
1187
- stream_name = os.getenv("REDIS_STREAM_NAME", "liftoff-events").strip()
1188
- if not stream_name:
1189
- raise MessagingConfigurationError("REDIS_STREAM_NAME must not be empty.")
1190
- if redis_client is None:
1191
- redis_url = os.getenv("REDIS_URL", "").strip()
1192
- if not redis_url:
1193
- raise MessagingConfigurationError("REDIS_URL is required for redis-streams messaging.")
1194
- from redis.asyncio import Redis
1195
-
1196
- redis_client = Redis.from_url(redis_url, decode_responses=True)
1197
- return RedisStreamPublisher(redis_client, stream_name)
1198
-
1199
- if transport == "azure-service-bus":
1200
- queue_name = os.getenv("SERVICE_BUS_QUEUE_NAME", "").strip()
1201
- if not queue_name:
1202
- raise MessagingConfigurationError(
1203
- "SERVICE_BUS_QUEUE_NAME is required for azure-service-bus messaging."
1204
- )
1205
- if service_bus_client is None:
1206
- connection_string = os.getenv("SERVICE_BUS_CONNECTION_STRING", "").strip()
1207
- namespace = os.getenv("SERVICE_BUS_FULLY_QUALIFIED_NAMESPACE", "").strip()
1208
- from azure.servicebus.aio import ServiceBusClient as AzureServiceBusClient
1209
-
1210
- if connection_string:
1211
- service_bus_client = AzureServiceBusClient.from_connection_string(connection_string)
1212
- elif namespace:
1213
- from azure.identity.aio import DefaultAzureCredential
1214
-
1215
- client_id = os.getenv("AZURE_CLIENT_ID", "").strip() or None
1216
- credential = DefaultAzureCredential(managed_identity_client_id=client_id)
1217
- service_bus_client = AzureServiceBusClient(namespace, credential)
1218
- else:
1219
- raise MessagingConfigurationError(
1220
- "Set SERVICE_BUS_CONNECTION_STRING or SERVICE_BUS_FULLY_QUALIFIED_NAMESPACE "
1221
- "for azure-service-bus messaging."
1222
- )
1223
- return AzureServiceBusPublisher(service_bus_client, queue_name, message_factory)
1224
-
1225
- raise MessagingConfigurationError(
1226
- f"Unsupported MESSAGING_TRANSPORT '{transport}'. "
1227
- "Expected 'redis-streams' or 'azure-service-bus'."
1228
- )
1229
- `;
1230
- }
1231
- function renderTracing() {
1232
- return `import os
1233
- from contextlib import asynccontextmanager
1234
- from dataclasses import dataclass
1235
- from typing import Any, AsyncContextManager, Protocol
1236
-
1237
-
1238
- class TracingConfigurationError(RuntimeError):
1239
- pass
1240
-
1241
-
1242
- @dataclass
1243
- class TraceHandle:
1244
- enabled: bool
1245
- trace_id: str | None
1246
- output: Any = None
1247
-
1248
- def set_output(self, output: Any) -> None:
1249
- self.output = output
1250
-
1251
-
1252
- class Tracer(Protocol):
1253
- def trace(self, name: str, input_data: Any = None) -> AsyncContextManager[TraceHandle]:
1254
- ...
1255
-
1256
-
1257
- class DisabledTracer:
1258
- @asynccontextmanager
1259
- async def trace(self, name: str, input_data: Any = None):
1260
- del name, input_data
1261
- yield TraceHandle(enabled=False, trace_id=None)
1262
-
1263
-
1264
- class LangfuseTracer:
1265
- def __init__(self, client: Any):
1266
- self._client = client
1267
-
1268
-
1269
- @asynccontextmanager
1270
- async def trace(self, name: str, input_data: Any = None):
1271
- remote_trace = self._client.start_observation(
1272
- name=name,
1273
- as_type="span",
1274
- input=input_data,
1275
- )
1276
- remote_id = getattr(remote_trace, "trace_id", None)
1277
- handle = TraceHandle(
1278
- enabled=True,
1279
- trace_id=str(remote_id) if remote_id is not None else None,
1280
- )
1281
- try:
1282
- yield handle
1283
- except Exception as error:
1284
- remote_trace.update(level="ERROR", status_message=str(error))
1285
- raise
1286
- else:
1287
- remote_trace.update(output=handle.output)
1288
- finally:
1289
- remote_trace.end()
1290
-
1291
-
1292
- def build_tracer(
1293
- *,
1294
- client: Any = None,
1295
- public_key: str | None = None,
1296
- secret_key: str | None = None,
1297
- host: str | None = None,
1298
- ) -> Tracer:
1299
- if client is not None:
1300
- return LangfuseTracer(client)
1301
-
1302
- resolved_public_key = public_key or os.getenv("LANGFUSE_PUBLIC_KEY", "").strip()
1303
- resolved_secret_key = secret_key or os.getenv("LANGFUSE_SECRET_KEY", "").strip()
1304
- if not resolved_public_key and not resolved_secret_key:
1305
- return DisabledTracer()
1306
- if not resolved_public_key or not resolved_secret_key:
1307
- raise TracingConfigurationError(
1308
- "LANGFUSE_PUBLIC_KEY and LANGFUSE_SECRET_KEY must be configured together."
1309
- )
1310
-
1311
- from langfuse import Langfuse
1312
-
1313
- resolved_host = host or os.getenv("LANGFUSE_HOST", "").strip() or None
1314
- kwargs = {
1315
- "public_key": resolved_public_key,
1316
- "secret_key": resolved_secret_key,
1317
- }
1318
- if resolved_host:
1319
- kwargs["host"] = resolved_host
1320
- return LangfuseTracer(Langfuse(**kwargs))
1321
- `;
1322
- }
1323
- function renderBackendHealthTest() {
1324
- return `from fastapi.testclient import TestClient
1325
-
1326
- from backend.apis.main import app
1327
-
1328
-
1329
- def test_health():
1330
- client = TestClient(app)
1331
- response = client.get("/health")
1332
- assert response.status_code == 200
1333
- assert response.json()["status"] == "ok"
1334
-
1335
-
1336
- def test_cors_preflight_for_local_frontend():
1337
- response = TestClient(app).options(
1338
- "/health",
1339
- headers={
1340
- "Origin": "http://localhost:5173",
1341
- "Access-Control-Request-Method": "GET",
1342
- },
1343
- )
1344
- assert response.status_code == 200
1345
- assert response.headers["access-control-allow-origin"] == "http://localhost:5173"
1346
- `;
1347
- }
1348
- function renderMessagingTest() {
1349
- return `import asyncio
1350
- import json
1351
-
1352
- from backend.orchestration.tools.messaging import build_message_publisher
1353
-
1354
-
1355
- class FakeRedisClient:
1356
- def __init__(self):
1357
- self.calls = []
1358
-
1359
- async def xadd(self, name, fields):
1360
- self.calls.append((name, fields))
1361
-
1362
-
1363
- class FakeSender:
1364
- def __init__(self):
1365
- self.messages = []
1366
-
1367
- async def __aenter__(self):
1368
- return self
1369
-
1370
- async def __aexit__(self, exc_type, exc, traceback):
1371
- return None
1372
-
1373
- async def send_messages(self, message):
1374
- self.messages.append(message)
1375
-
1376
-
1377
- class FakeServiceBusClient:
1378
- def __init__(self, sender):
1379
- self.sender = sender
1380
- self.queue_names = []
1381
-
1382
- def get_queue_sender(self, *, queue_name):
1383
- self.queue_names.append(queue_name)
1384
- return self.sender
1385
-
1386
-
1387
- def test_redis_stream_publisher_uses_xadd(monkeypatch):
1388
- monkeypatch.setenv("REDIS_STREAM_NAME", "orchestration-events")
1389
- client = FakeRedisClient()
1390
- publisher = build_message_publisher("redis-streams", redis_client=client)
1391
-
1392
- asyncio.run(publisher.publish("rag.ingest", {"source_uri": "az://document"}))
1393
-
1394
- stream_name, fields = client.calls[0]
1395
- assert stream_name == "orchestration-events"
1396
- assert fields["topic"] == "rag.ingest"
1397
- assert json.loads(fields["payload"]) == {
1398
- "payload": {"source_uri": "az://document"},
1399
- "topic": "rag.ingest",
1400
- }
1401
-
1402
-
1403
- def test_service_bus_publisher_uses_async_sender(monkeypatch):
1404
- monkeypatch.setenv("SERVICE_BUS_QUEUE_NAME", "orchestration-jobs")
1405
- sender = FakeSender()
1406
- client = FakeServiceBusClient(sender)
1407
- publisher = build_message_publisher(
1408
- "azure-service-bus",
1409
- service_bus_client=client,
1410
- message_factory=lambda body: body,
1411
- )
1412
-
1413
- asyncio.run(publisher.publish("workflow.run", {"job_id": "job-1"}))
1414
-
1415
- assert client.queue_names == ["orchestration-jobs"]
1416
- assert json.loads(sender.messages[0]) == {
1417
- "payload": {"job_id": "job-1"},
1418
- "topic": "workflow.run",
1419
- }
1420
- `;
1421
- }
1422
- function renderTracingTest() {
1423
- return `import asyncio
1424
-
1425
- import pytest
1426
-
1427
- from backend.observability.tracing import (
1428
- TracingConfigurationError,
1429
- build_tracer,
1430
- )
1431
-
1432
-
1433
- class FakeRemoteTrace:
1434
- trace_id = "trace-123"
1435
-
1436
- def __init__(self):
1437
- self.updates = []
1438
- self.ended = False
1439
-
1440
- def update(self, **values):
1441
- self.updates.append(values)
1442
-
1443
- def end(self):
1444
- self.ended = True
1445
-
1446
-
1447
- class FakeLangfuse:
1448
- def __init__(self):
1449
- self.calls = []
1450
- self.remote_trace = FakeRemoteTrace()
1451
-
1452
- def start_observation(self, **values):
1453
- self.calls.append(values)
1454
- return self.remote_trace
1455
-
1456
-
1457
- def test_unconfigured_tracing_is_explicitly_disabled(monkeypatch):
1458
- monkeypatch.delenv("LANGFUSE_PUBLIC_KEY", raising=False)
1459
- monkeypatch.delenv("LANGFUSE_SECRET_KEY", raising=False)
1460
-
1461
- async def scenario():
1462
- async with build_tracer().trace("offline") as trace:
1463
- assert trace.enabled is False
1464
- assert trace.trace_id is None
1465
-
1466
- asyncio.run(scenario())
1467
-
1468
-
1469
- def test_configured_tracing_updates_langfuse_operation():
1470
- client = FakeLangfuse()
1471
-
1472
- async def scenario():
1473
- async with build_tracer(client=client).trace(
1474
- "agent.run",
1475
- {"prompt": "hello"},
1476
- ) as trace:
1477
- assert trace.enabled is True
1478
- assert trace.trace_id == "trace-123"
1479
- trace.set_output({"answer": "world"})
1480
-
1481
- asyncio.run(scenario())
1482
- assert client.calls == [
1483
- {"name": "agent.run", "as_type": "span", "input": {"prompt": "hello"}}
1484
- ]
1485
- assert client.remote_trace.updates == [{"output": {"answer": "world"}}]
1486
- assert client.remote_trace.ended is True
1487
-
1488
-
1489
- def test_partial_langfuse_configuration_fails(monkeypatch):
1490
- monkeypatch.setenv("LANGFUSE_PUBLIC_KEY", "public")
1491
- monkeypatch.delenv("LANGFUSE_SECRET_KEY", raising=False)
1492
- with pytest.raises(TracingConfigurationError, match="configured together"):
1493
- build_tracer()
1494
- `;
1495
- }
1496
- function renderAlembicIni() {
1497
- return `[alembic]
1498
- script_location = %(here)s/migrations
1499
- sqlalchemy.url = driver://user:pass@localhost/dbname
1500
- `;
1501
- }
1502
- function renderAlembicEnv() {
1503
- return `import os
1504
-
1505
- from alembic import context
1506
- from sqlalchemy import create_engine
1507
-
1508
-
1509
- def run_migrations_online():
1510
- database_url = os.environ.get("DATABASE_URL")
1511
- if not database_url:
1512
- raise RuntimeError("DATABASE_URL is required to run migrations")
1513
- database_url = database_url.replace("postgresql+asyncpg://", "postgresql+psycopg://", 1)
1514
- database_url = database_url.replace("postgresql://", "postgresql+psycopg://", 1)
1515
- connectable = create_engine(database_url)
1516
- with connectable.connect() as connection:
1517
- context.configure(connection=connection, target_metadata=None)
1518
- with context.begin_transaction():
1519
- context.run_migrations()
1520
-
1521
-
1522
- run_migrations_online()
1523
- `;
1524
- }
1525
- function renderInitialMigration(plan) {
1526
- const vectorExtension = genAiPattern(plan).id === 'rag' ? ' op.execute("CREATE EXTENSION IF NOT EXISTS vector")\n' : '';
1527
- return `from alembic import op
1528
- import sqlalchemy as sa
1529
-
1530
- revision = "0001_initial"
1531
- down_revision = None
1532
- branch_labels = None
1533
- depends_on = None
1534
-
1535
-
1536
- def upgrade():
1537
- ${vectorExtension} op.create_table(
1538
- "events",
1539
- sa.Column("id", sa.Integer(), primary_key=True),
1540
- sa.Column("event_type", sa.String(length=120), nullable=False),
1541
- sa.Column("payload", sa.JSON(), nullable=False),
1542
- sa.Column("created_at", sa.DateTime(), server_default=sa.func.now(), nullable=False),
1543
- )
1544
-
1545
-
1546
- def downgrade():
1547
- op.drop_table("events")
1548
- `;
1549
- }
1550
- function renderDatabaseSchema(plan) {
1551
- return `CREATE TABLE IF NOT EXISTS events (
1552
- id SERIAL PRIMARY KEY,
1553
- event_type VARCHAR(120) NOT NULL,
1554
- payload JSONB NOT NULL,
1555
- created_at TIMESTAMPTZ NOT NULL DEFAULT now()
1556
- );
1557
- ${genAiPattern(plan).id === 'rag' ? '\nCREATE EXTENSION IF NOT EXISTS vector;\n' : ''}`;
1558
- }
1559
- function renderPatternAgent(plan) {
1560
- const pattern = genAiPattern(plan);
1561
- const moduleName = pyModule(pattern.id);
1562
- if (pattern.id === 'rag') {
1563
- return `import os
1564
-
1565
- from backend.observability.tracing import Tracer, build_tracer
1566
- from backend.orchestration.model_config import AgentRunner, build_agent_runner
1567
- from backend.orchestration.tools.messaging import MessagePublisher, build_message_publisher
1568
-
1569
-
1570
- async def _run_agent(
1571
- operation: str,
1572
- prompt: str,
1573
- runner: AgentRunner | None,
1574
- tracer: Tracer | None,
1575
- ) -> str:
1576
- selected_runner = runner or build_agent_runner()
1577
- selected_tracer = tracer or build_tracer()
1578
- async with selected_tracer.trace(operation, {"prompt": prompt}) as trace:
1579
- output = await selected_runner.run(prompt)
1580
- trace.set_output({"text": output})
1581
- return output
1582
-
1583
-
1584
- async def answer_question(
1585
- question: str,
1586
- *,
1587
- runner: AgentRunner | None = None,
1588
- tracer: Tracer | None = None,
1589
- ) -> dict:
1590
- answer = await _run_agent(
1591
- "rag.query",
1592
- f"Answer the question using retrieved evidence when available.\\nQuestion: {question}",
1593
- runner,
1594
- tracer,
1595
- )
1596
- return {
1597
- "answer": answer,
1598
- "question": question,
1599
- "citations": [],
1600
- }
1601
-
1602
-
1603
- async def enqueue_ingestion(
1604
- source_uri: str,
1605
- *,
1606
- publisher: MessagePublisher | None = None,
1607
- ) -> dict:
1608
- selected_publisher = publisher or build_message_publisher(
1609
- os.getenv("MESSAGING_TRANSPORT", "redis-streams")
1610
- )
1611
- await selected_publisher.publish("rag.ingest", {"source_uri": source_uri})
1612
- return {"status": "queued", "source_uri": source_uri}
1613
- `;
1614
- }
1615
- if (pattern.id === 'streaming') {
1616
- return `import json
1617
-
1618
- from backend.observability.tracing import Tracer, build_tracer
1619
- from backend.orchestration.model_config import AgentRunner, build_agent_runner
1620
-
1621
-
1622
- async def stream_response(
1623
- prompt: str,
1624
- *,
1625
- runner: AgentRunner | None = None,
1626
- tracer: Tracer | None = None,
1627
- ):
1628
- selected_runner = runner or build_agent_runner()
1629
- selected_tracer = tracer or build_tracer()
1630
- async with selected_tracer.trace("streaming.run", {"prompt": prompt}) as trace:
1631
- output = await selected_runner.run(
1632
- f"Respond concisely and safely to this streaming request:\\n{prompt}"
1633
- )
1634
- trace.set_output({"text": output})
1635
- yield f"data: {json.dumps({'text': output})}\\n\\n"
1636
- `;
1637
- }
1638
- const instruction = pattern.id === 'generic'
1639
- ? 'Respond safely and usefully to this general-purpose AI request:'
1640
- : `Run the ${pattern.label} orchestration contract for this input:`;
1641
- return `from backend.observability.tracing import Tracer, build_tracer
1642
- from backend.orchestration.model_config import AgentRunner, build_agent_runner
1643
-
1644
-
1645
- async def run_${moduleName}(
1646
- input_text: str,
1647
- *,
1648
- runner: AgentRunner | None = None,
1649
- tracer: Tracer | None = None,
1650
- ) -> dict:
1651
- selected_runner = runner or build_agent_runner()
1652
- selected_tracer = tracer or build_tracer()
1653
- prompt = (
1654
- "${instruction}\\n"
1655
- f"{input_text}"
1656
- )
1657
- async with selected_tracer.trace("${pattern.id}.run", {"input": input_text}) as trace:
1658
- output = await selected_runner.run(prompt)
1659
- trace.set_output({"result": output})
1660
- return {
1661
- "result": output,
1662
- "input": input_text,
1663
- }
1664
- `;
1665
- }
1666
- function renderPatternAgentTest(plan) {
1667
- const pattern = genAiPattern(plan);
1668
- const moduleName = pyModule(pattern.id);
1669
- const agentModule = `backend.orchestration.agents.${moduleName}_agent`;
1670
- if (pattern.id === 'rag') {
1671
- return `import asyncio
1672
-
1673
- import pytest
1674
-
1675
- from ${agentModule} import answer_question, enqueue_ingestion
1676
- from backend.observability.tracing import DisabledTracer
1677
- from backend.orchestration.model_config import ModelConfigurationError
1678
-
1679
-
1680
- class FakeRunner:
1681
- async def run(self, prompt):
1682
- assert "Question: What is Liftoff?" in prompt
1683
- return "Liftoff is the generated orchestration starter."
1684
-
1685
-
1686
- class FakePublisher:
1687
- def __init__(self):
1688
- self.messages = []
1689
-
1690
- async def publish(self, topic, payload):
1691
- self.messages.append((topic, payload))
1692
-
1693
-
1694
- def test_rag_query_uses_injected_runner_without_network():
1695
- result = asyncio.run(
1696
- answer_question(
1697
- "What is Liftoff?",
1698
- runner=FakeRunner(),
1699
- tracer=DisabledTracer(),
1700
- )
1701
- )
1702
- assert result == {
1703
- "answer": "Liftoff is the generated orchestration starter.",
1704
- "question": "What is Liftoff?",
1705
- "citations": [],
1706
- }
1707
-
1708
-
1709
- def test_rag_ingestion_uses_injected_publisher():
1710
- publisher = FakePublisher()
1711
- result = asyncio.run(
1712
- enqueue_ingestion("az://documents/one.pdf", publisher=publisher)
1713
- )
1714
- assert result == {
1715
- "status": "queued",
1716
- "source_uri": "az://documents/one.pdf",
1717
- }
1718
- assert publisher.messages == [
1719
- ("rag.ingest", {"source_uri": "az://documents/one.pdf"})
1720
- ]
1721
-
1722
-
1723
- def test_missing_model_configuration_is_explicit(monkeypatch):
1724
- monkeypatch.delenv("PYDANTIC_AI_MODEL", raising=False)
1725
- with pytest.raises(ModelConfigurationError, match="PYDANTIC_AI_MODEL is required"):
1726
- asyncio.run(answer_question("unconfigured"))
1727
- `;
1728
- }
1729
- if (pattern.id === 'streaming') {
1730
- return `import asyncio
1731
-
1732
- import pytest
1733
-
1734
- from ${agentModule} import stream_response
1735
- from backend.observability.tracing import DisabledTracer
1736
- from backend.orchestration.model_config import ModelConfigurationError
1737
-
1738
-
1739
- class FakeRunner:
1740
- async def run(self, prompt):
1741
- assert "stream this" in prompt
1742
- return "offline streamed answer"
1743
-
1744
-
1745
- def test_streaming_uses_injected_runner_without_network():
1746
- async def collect():
1747
- return [
1748
- chunk
1749
- async for chunk in stream_response(
1750
- "stream this",
1751
- runner=FakeRunner(),
1752
- tracer=DisabledTracer(),
1753
- )
1754
- ]
1755
-
1756
- chunks = asyncio.run(collect())
1757
- assert chunks == ['data: {"text": "offline streamed answer"}\\n\\n']
1758
-
1759
-
1760
- def test_missing_model_configuration_is_explicit(monkeypatch):
1761
- monkeypatch.delenv("PYDANTIC_AI_MODEL", raising=False)
1762
-
1763
- async def collect():
1764
- return [chunk async for chunk in stream_response("unconfigured")]
1765
-
1766
- with pytest.raises(ModelConfigurationError, match="PYDANTIC_AI_MODEL is required"):
1767
- asyncio.run(collect())
1768
- `;
1769
- }
1770
- return `import asyncio
1771
-
1772
- import pytest
1773
-
1774
- from ${agentModule} import run_${moduleName}
1775
- from backend.observability.tracing import DisabledTracer
1776
- from backend.orchestration.model_config import ModelConfigurationError
1777
-
1778
-
1779
- class FakeRunner:
1780
- async def run(self, prompt):
1781
- assert "offline input" in prompt
1782
- return "offline ${pattern.id} result"
1783
-
1784
-
1785
- def test_${moduleName}_uses_injected_runner_without_network():
1786
- result = asyncio.run(
1787
- run_${moduleName}(
1788
- "offline input",
1789
- runner=FakeRunner(),
1790
- tracer=DisabledTracer(),
1791
- )
1792
- )
1793
- assert result == {
1794
- "result": "offline ${pattern.id} result",
1795
- "input": "offline input",
1796
- }
1797
-
1798
-
1799
- def test_missing_model_configuration_is_explicit(monkeypatch):
1800
- monkeypatch.delenv("PYDANTIC_AI_MODEL", raising=False)
1801
- with pytest.raises(ModelConfigurationError, match="PYDANTIC_AI_MODEL is required"):
1802
- asyncio.run(run_${moduleName}("unconfigured"))
1803
- `;
1804
- }
1805
- function renderPromptTemplate(plan) {
1806
- const pattern = genAiPattern(plan);
1807
- if (pattern.id === 'generic') {
1808
- return `# Generic GenAI System Prompt
1809
-
1810
- Respond safely and usefully to the general-purpose request.
1811
-
1812
- Do not assume retrieval, conversation memory, tools, streaming, fine-tuning, multi-agent coordination, or workflow behavior unless the project explicitly adds it.
1813
- `;
1814
- }
1815
- return `# ${pattern.label} Prompt
1816
-
1817
- You are implementing a ${pattern.label} generated by Mission Control Liftoff.
1818
-
1819
- Use PydanticAI orchestration and return outputs that match the API contract.
1820
- `;
1821
- }
1822
- function renderPromptReadme() {
1823
- return `# Prompt Templates
1824
-
1825
- Store prompt templates here and reference them from the PydanticAI orchestration layer.
1826
- `;
1827
- }
1828
- function renderVectorStore() {
1829
- return `from typing import Protocol
1830
-
1831
-
1832
- class VectorStore(Protocol):
1833
- async def search(self, query: str, limit: int = 5) -> list[dict]:
1834
- ...
1835
-
1836
-
1837
- class PgVectorStore:
1838
- async def search(self, query: str, limit: int = 5) -> list[dict]:
1839
- return []
1840
- `;
1841
- }
1842
- function renderPatternWorker(plan) {
1843
- const pattern = genAiPattern(plan);
1844
- return `async def run_worker() -> None:
1845
- # Consume ${pattern.label} jobs from the configured messaging boundary.
1846
- return None
1847
- `;
1848
- }
1849
- function renderFunctionsReadme() {
1850
- return `# Azure Functions Workers
1851
-
1852
- Azure Functions trigger adapters live under \`functions/<worker-name>\`.
1853
-
1854
- Keep reusable GenAI orchestration, model configuration, prompt handling, and domain logic under \`backend/orchestration\`. Use \`backend/workers\` for backend-adjacent or containerized workers; use this folder for Azure Functions runtime files such as \`host.json\`, trigger bindings, local settings, and Function app tests.
1855
- `;
1856
- }
1857
- function renderFunctionWorkerReadme(plan) {
1858
- const workerName = functionWorkerName(plan);
1859
- const pattern = genAiPattern(plan);
1860
- return `# ${workerName}
1861
-
1862
- Azure Functions worker scaffold for ${pattern.label}.
1863
-
1864
- This Function app uses the Python v2 decorator programming model and a Service Bus queue trigger. The trigger adapter should stay thin: decode the message, validate the envelope, and call shared code from \`backend/orchestration\` after that shared code is packaged with the Function app.
1865
-
1866
- Deployed triggers use \`ServiceBusConnection__fullyQualifiedNamespace\` and \`ServiceBusConnection__clientId\` to select the same user-assigned identity that OpenTofu grants the Service Bus Data Receiver role. \`SERVICEBUS_QUEUE_NAME\` is populated from \`function_worker_queue_name\`. Function host storage uses the complete \`AzureWebJobsStorage\` connection setting.
1867
-
1868
- ## Local Development
1869
-
1870
- \`\`\`bash
1871
- uv sync --frozen --project ../../backend --extra test --extra functions
1872
- cp local.settings.example.json local.settings.json
1873
- uv run --project ../../backend --directory . python -m pytest -q
1874
- func start
1875
- \`\`\`
1876
- `;
1877
- }
1878
- function renderFunctionHostJson() {
1879
- return JSON.stringify({
1880
- version: '2.0',
1881
- extensionBundle: {
1882
- id: 'Microsoft.Azure.Functions.ExtensionBundle',
1883
- version: '[4.*, 5.0.0)'
1884
- }
1885
- }, null, 2);
1886
- }
1887
- function renderFunctionLocalSettings(plan) {
1888
- const pattern = genAiPattern(plan);
1889
- return JSON.stringify({
1890
- IsEncrypted: false,
1891
- Values: {
1892
- AzureWebJobsStorage: 'UseDevelopmentStorage=true',
1893
- FUNCTIONS_WORKER_RUNTIME: 'python',
1894
- SERVICEBUS_QUEUE_NAME: DEFAULT_FUNCTION_WORKER_QUEUE_NAME,
1895
- ServiceBusConnection__fullyQualifiedNamespace: '<service-bus-namespace>.servicebus.windows.net',
1896
- GENAI_PATTERN: pattern.id,
1897
- SHARED_ORCHESTRATION_ROOT: '../../backend'
1898
- }
1899
- }, null, 2);
1900
- }
1901
- function renderFunctionRequirements() {
1902
- return renderFunctionRequirementsAsset();
1903
- }
1904
- function renderFunctionApp(plan) {
1905
- const pattern = genAiPattern(plan);
1906
- const moduleName = pyModule(pattern.id);
1907
- return `import json
1908
- import logging
1909
-
1910
- import azure.functions as func
1911
-
1912
-
1913
- app = func.FunctionApp()
1914
-
1915
-
1916
- def decode_message_payload(body: str) -> dict:
1917
- try:
1918
- value = json.loads(body)
1919
- except json.JSONDecodeError:
1920
- return {"raw": body}
1921
- if isinstance(value, dict):
1922
- return value
1923
- return {"value": value}
1924
-
1925
-
1926
- @app.service_bus_queue_trigger(
1927
- arg_name="message",
1928
- queue_name="%SERVICEBUS_QUEUE_NAME%",
1929
- connection="ServiceBusConnection",
1930
- )
1931
- def process_${moduleName}_work(message: func.ServiceBusMessage) -> None:
1932
- payload = decode_message_payload(message.get_body().decode("utf-8"))
1933
- logging.info("Received ${pattern.id} worker message with keys: %s", sorted(payload.keys()))
1934
- # Keep this adapter thin; call backend.orchestration code from packaged shared modules.
1935
- `;
1936
- }
1937
- function renderFunctionTest() {
1938
- return `from function_app import decode_message_payload
1939
-
1940
-
1941
- def test_decode_message_payload_for_json_object():
1942
- assert decode_message_payload('{"source_uri":"az://documents/example.pdf"}') == {
1943
- "source_uri": "az://documents/example.pdf"
1944
- }
1945
-
1946
-
1947
- def test_decode_message_payload_for_plain_text():
1948
- assert decode_message_payload("plain text") == {"raw": "plain text"}
1949
- `;
1950
- }
1951
- function renderFunctionFuncIgnore() {
1952
- return `.venv/
1953
- __pycache__/
1954
- .pytest_cache/
1955
- local.settings.json
1956
- tests/
1957
- `;
1958
- }
1959
- function renderFunctionGitIgnore() {
1960
- return `.venv/
1961
- __pycache__/
1962
- .pytest_cache/
1963
- local.settings.json
1964
- `;
1965
- }
1966
- function renderBackendEnv(plan, environment) {
1967
- const pattern = genAiPattern(plan);
1968
- const transport = environment === 'dev' ? 'redis-streams' : 'azure-service-bus';
1969
- return `APP_ENV=${environment}
1970
- APP_NAME=${plan.safeProjectName}
1971
- GENAI_PATTERN=${pattern.id}
1972
- CLOUD_PROVIDER=${plan.provider.id}
1973
- AZURE_REGION=${plan.region.slug}
1974
- DATABASE_URL=postgresql+asyncpg://postgres:postgres@postgres:5432/${plan.safeProjectName.replace(/-/g, '_')}
1975
- REDIS_URL=redis://redis:6379/0
1976
- REDIS_STREAM_NAME=liftoff-events
1977
- MESSAGING_TRANSPORT=${transport}
1978
- SERVICE_BUS_QUEUE_NAME=${DEFAULT_FUNCTION_WORKER_QUEUE_NAME}
1979
- SERVICE_BUS_CONNECTION_STRING=
1980
- SERVICE_BUS_FULLY_QUALIFIED_NAMESPACE=${environment === 'dev' ? '' : '<service-bus-namespace>.servicebus.windows.net'}
1981
- AZURE_CLIENT_ID=
1982
- BLOB_ENDPOINT=
1983
- CORS_ALLOWED_ORIGINS=http://localhost:5173
1984
- PYDANTIC_AI_MODEL=
1985
- LANGFUSE_HOST=${environment === 'dev' ? 'http://langfuse:3000' : ''}
1986
- LANGFUSE_PUBLIC_KEY=
1987
- LANGFUSE_SECRET_KEY=
1988
- `;
1989
- }
1990
- function renderFunctionsEnv(plan, environment) {
1991
- const pattern = genAiPattern(plan);
1992
- return `APP_ENV=${environment}
1993
- APP_NAME=${plan.safeProjectName}
1994
- GENAI_PATTERN=${pattern.id}
1995
- FUNCTIONS_WORKER_RUNTIME=python
1996
- SERVICEBUS_QUEUE_NAME=${DEFAULT_FUNCTION_WORKER_QUEUE_NAME}
1997
- ServiceBusConnection__fullyQualifiedNamespace=<service-bus-namespace>.servicebus.windows.net
1998
- ServiceBusConnection__clientId=<managed-identity-client-id>
1999
- AzureWebJobsStorage=<storage-connection-string>
2000
- SHARED_ORCHESTRATION_ROOT=../../backend
2001
- `;
2002
- }
2003
- function renderDockerCompose(plan) {
2004
- const localEnvironment = plan.environments.find((environment) => environment.id === 'dev') ?? plan.environments[0];
2005
- const frontendService = plan.includeFrontend ? `
2006
- frontend:
2007
- build:
2008
- context: ./frontend
2009
- ports:
2010
- - "5173:80"
2011
- depends_on:
2012
- - backend
2013
- ` : '';
2014
- const postgresImage = plan.workload === 'genai'
2015
- ? formatContainerImage(supportedStack.containers.pgvector)
2016
- : formatContainerImage(supportedStack.containers.postgres);
2017
- return `services:
2018
- backend:
2019
- build:
2020
- context: .
2021
- dockerfile: Dockerfile
2022
- env_file:
2023
- - ./environments/${localEnvironment.id}/backend.env
2024
- environment:
2025
- MESSAGING_TRANSPORT: redis-streams
2026
- BLOB_ENDPOINT: http://azurite:10000/devstoreaccount1
2027
- ports:
2028
- - "8000:8000"
2029
- depends_on:
2030
- - postgres
2031
- - redis
2032
- - azurite
2033
- - mailpit
2034
- ${frontendService}
2035
- postgres:
2036
- image: ${postgresImage}
2037
- environment:
2038
- POSTGRES_USER: postgres
2039
- POSTGRES_PASSWORD: postgres
2040
- POSTGRES_DB: ${plan.safeProjectName.replace(/-/g, '_')}
2041
- ports:
2042
- - "5432:5432"
2043
- healthcheck:
2044
- test: ["CMD-SHELL", "pg_isready -U postgres"]
2045
- interval: 3s
2046
- timeout: 3s
2047
- retries: 10
2048
-
2049
- redis:
2050
- image: ${formatContainerImage(supportedStack.containers.redis)}
2051
- ports:
2052
- - "6379:6379"
2053
-
2054
- azurite:
2055
- image: ${formatContainerImage(supportedStack.containers.azurite)}
2056
- command: azurite --blobHost 0.0.0.0
2057
- ports:
2058
- - "10000:10000"
2059
-
2060
- mailpit:
2061
- image: ${formatContainerImage(supportedStack.containers.mailpit)}
2062
- ports:
2063
- - "8025:8025"
2064
-
2065
- ${plan.workload === 'genai' ? ` langfuse-worker:
2066
- image: ${formatContainerImage(supportedStack.containers['langfuse-worker'])}
2067
- profiles:
2068
- - observability
2069
- depends_on: &langfuse-dependencies
2070
- postgres:
2071
- condition: service_healthy
2072
- langfuse-redis:
2073
- condition: service_healthy
2074
- clickhouse:
2075
- condition: service_healthy
2076
- minio:
2077
- condition: service_healthy
2078
- environment: &langfuse-environment
2079
- DATABASE_URL: postgresql://postgres:postgres@postgres:5432/${plan.safeProjectName.replace(/-/g, '_')}
2080
- NEXTAUTH_URL: http://localhost:3000
2081
- SALT: local-development-salt
2082
- ENCRYPTION_KEY: 0000000000000000000000000000000000000000000000000000000000000000
2083
- TELEMETRY_ENABLED: "false"
2084
- CLICKHOUSE_MIGRATION_URL: clickhouse://clickhouse:9000
2085
- CLICKHOUSE_URL: http://clickhouse:8123
2086
- CLICKHOUSE_USER: clickhouse
2087
- CLICKHOUSE_PASSWORD: clickhouse
2088
- REDIS_HOST: langfuse-redis
2089
- REDIS_PORT: 6379
2090
- REDIS_AUTH: langfuse-redis
2091
- LANGFUSE_S3_EVENT_UPLOAD_BUCKET: langfuse
2092
- LANGFUSE_S3_EVENT_UPLOAD_REGION: auto
2093
- LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: minio
2094
- LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: miniosecret
2095
- LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: http://minio:9000
2096
- LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: "true"
2097
- LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: langfuse
2098
- LANGFUSE_S3_MEDIA_UPLOAD_REGION: auto
2099
- LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: minio
2100
- LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: miniosecret
2101
- LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: http://minio:9000
2102
- LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: "true"
2103
-
2104
- langfuse:
2105
- image: ${formatContainerImage(supportedStack.containers['langfuse-web'])}
2106
- profiles:
2107
- - observability
2108
- depends_on: *langfuse-dependencies
2109
- environment:
2110
- <<: *langfuse-environment
2111
- NEXTAUTH_SECRET: local-development-secret
2112
- LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: http://localhost:9090
2113
- ports:
2114
- - "3000:3000"
2115
-
2116
- clickhouse:
2117
- image: ${formatContainerImage(supportedStack.containers.clickhouse)}
2118
- profiles:
2119
- - observability
2120
- user: "101:101"
2121
- environment:
2122
- CLICKHOUSE_DB: default
2123
- CLICKHOUSE_USER: clickhouse
2124
- CLICKHOUSE_PASSWORD: clickhouse
2125
- volumes:
2126
- - langfuse-clickhouse-data:/var/lib/clickhouse
2127
- - langfuse-clickhouse-logs:/var/log/clickhouse-server
2128
- healthcheck:
2129
- test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1"]
2130
- interval: 5s
2131
- timeout: 5s
2132
- retries: 10
2133
-
2134
- minio:
2135
- image: ${formatContainerImage(supportedStack.containers.minio)}
2136
- profiles:
2137
- - observability
2138
- entrypoint: sh
2139
- command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
2140
- environment:
2141
- MINIO_ROOT_USER: minio
2142
- MINIO_ROOT_PASSWORD: miniosecret
2143
- ports:
2144
- - "9090:9000"
2145
- - "127.0.0.1:9091:9001"
2146
- volumes:
2147
- - langfuse-minio-data:/data
2148
- healthcheck:
2149
- test: ["CMD", "mc", "ready", "local"]
2150
- interval: 1s
2151
- timeout: 5s
2152
- retries: 5
2153
-
2154
- langfuse-redis:
2155
- image: ${formatContainerImage(supportedStack.containers.redis)}
2156
- profiles:
2157
- - observability
2158
- command: ["redis-server", "--requirepass", "langfuse-redis", "--maxmemory-policy", "noeviction"]
2159
- volumes:
2160
- - langfuse-redis-data:/data
2161
- healthcheck:
2162
- test: ["CMD", "redis-cli", "-a", "langfuse-redis", "ping"]
2163
- interval: 3s
2164
- timeout: 10s
2165
- retries: 10
2166
- ` : ''}
2167
- ${plan.workload === 'genai' ? `volumes:
2168
- langfuse-clickhouse-data:
2169
- langfuse-clickhouse-logs:
2170
- langfuse-minio-data:
2171
- langfuse-redis-data:
2172
- ` : ''}
2173
- `;
2174
- }
2175
- function renderTofuProviders() {
2176
- return `provider "azurerm" {
2177
- features {}
2178
- }
2179
- `;
2180
- }
2181
- function renderTofuVariables(plan) {
2182
- const frontendVariables = plan.includeFrontend ? `
2183
- variable "frontend_image" {
2184
- type = string
2185
- default = "${formatContainerImage(supportedStack.containers['container-apps-bootstrap'])}"
2186
- description = "Frontend image. Replace the bootstrap image with the generated frontend image after pushing it to ACR."
2187
- }
2188
- ` : '';
2189
- const functionVariables = hasFunctionWorker(plan) ? `
2190
- variable "function_worker_queue_name" {
2191
- type = string
2192
- default = "events"
2193
- description = "Service Bus queue consumed by the generated Azure Functions worker."
2194
- }
2195
-
2196
- variable "functions_python_version" {
2197
- type = string
2198
- default = "${supportedStack.runtimes.python.releaseLine}"
2199
- description = "Python runtime version for the generated Azure Functions worker."
2200
- }
2201
- ` : '';
2202
- return `variable "environment" {
2203
- type = string
2204
- description = "Deployment environment name."
2205
- }
2206
-
2207
- variable "location" {
2208
- type = string
2209
- description = "Azure region slug."
2210
- default = "${plan.region.slug}"
2211
- }
2212
-
2213
- variable "resource_suffix" {
2214
- type = string
2215
- description = "Twelve-character lowercase alphanumeric suffix for globally scoped Azure resource names."
2216
-
2217
- validation {
2218
- condition = can(regex("^[a-z0-9]{12}$", var.resource_suffix))
2219
- error_message = "resource_suffix must contain exactly 12 lowercase letters or numbers."
2220
- }
2221
- }
2222
-
2223
- variable "backend_image" {
2224
- type = string
2225
- default = "${formatContainerImage(supportedStack.containers['container-apps-bootstrap'])}"
2226
- description = "Backend image. Replace the bootstrap image with the generated backend image after pushing it to ACR."
2227
- }
2228
-
2229
- variable "backend_target_port" {
2230
- type = number
2231
- default = 80
2232
- description = "Backend ingress port. Set to 8000 when switching from the bootstrap image to the generated backend."
2233
- }
2234
- ${frontendVariables}
2235
- variable "postgres_admin_password" {
2236
- type = string
2237
- sensitive = true
2238
- description = "PostgreSQL administrator password supplied at apply time."
2239
- }
2240
-
2241
- variable "enable_private_networking" {
2242
- type = bool
2243
- default = false
2244
- description = "Enable production-oriented private-networking-ready settings."
2245
- }
2246
- ${functionVariables}
2247
- `;
2248
- }
2249
- function renderTofuMain(plan) {
2250
- const functionPattern = plan.workload === 'genai' && hasFunctionWorker(plan)
2251
- ? genAiPattern(plan)
2252
- : undefined;
2253
- const names = buildAzureResourceNames(plan, '${var.environment}', '${var.resource_suffix}');
2254
- const queueName = hasFunctionWorker(plan)
2255
- ? 'var.function_worker_queue_name'
2256
- : JSON.stringify(DEFAULT_FUNCTION_WORKER_QUEUE_NAME);
2257
- const projectIdentityEnv = plan.workload === 'genai' ? `
2258
- env {
2259
- name = "GENAI_PATTERN"
2260
- value = "${genAiPattern(plan).id}"
2261
- }
2262
- ` : `
2263
- env {
2264
- name = "API_STACK"
2265
- value = "${plan.apiStack.id}"
2266
- }
2267
- `;
2268
- const frontendCorsEnvironment = plan.includeFrontend ? `
2269
- env {
2270
- name = "CORS_ALLOWED_ORIGINS"
2271
- value = "https://\${azurerm_container_app.frontend.ingress[0].fqdn}"
2272
- }
2273
- ` : '';
2274
- const frontendContainer = plan.includeFrontend ? `
2275
- resource "azurerm_container_app" "frontend" {
2276
- name = "${names.frontendContainerApp}"
2277
- container_app_environment_id = azurerm_container_app_environment.main.id
2278
- resource_group_name = azurerm_resource_group.main.name
2279
- revision_mode = "Single"
2280
-
2281
- identity {
2282
- type = "UserAssigned"
2283
- identity_ids = [azurerm_user_assigned_identity.app.id]
2284
- }
2285
-
2286
- registry {
2287
- server = azurerm_container_registry.main.login_server
2288
- identity = azurerm_user_assigned_identity.app.id
2289
- }
2290
-
2291
- template {
2292
- container {
2293
- name = "frontend"
2294
- image = var.frontend_image
2295
- cpu = 0.25
2296
- memory = "0.5Gi"
2297
- }
2298
- }
2299
-
2300
- ingress {
2301
- external_enabled = true
2302
- target_port = 80
2303
- traffic_weight {
2304
- percentage = 100
2305
- latest_revision = true
2306
- }
2307
- }
2308
-
2309
- depends_on = [azurerm_role_assignment.acr_pull]
2310
- }
2311
- ` : '';
2312
- const functionWorker = hasFunctionWorker(plan) ? `
2313
- resource "azurerm_service_plan" "functions" {
2314
- name = "${names.functionServicePlan}"
2315
- resource_group_name = azurerm_resource_group.main.name
2316
- location = azurerm_resource_group.main.location
2317
- os_type = "Linux"
2318
- sku_name = "Y1"
2319
- }
2320
-
2321
- resource "azurerm_linux_function_app" "worker" {
2322
- name = "${names.functionApp}"
2323
- resource_group_name = azurerm_resource_group.main.name
2324
- location = azurerm_resource_group.main.location
2325
- service_plan_id = azurerm_service_plan.functions.id
2326
- storage_account_name = azurerm_storage_account.main.name
2327
- storage_account_access_key = azurerm_storage_account.main.primary_access_key
2328
-
2329
- identity {
2330
- type = "UserAssigned"
2331
- identity_ids = [azurerm_user_assigned_identity.app.id]
2332
- }
2333
-
2334
- site_config {
2335
- application_stack {
2336
- python_version = var.functions_python_version
2337
- }
2338
- }
2339
-
2340
- app_settings = {
2341
- APP_ENV = var.environment
2342
- APP_NAME = "${plan.safeProjectName}"
2343
- GENAI_PATTERN = "${functionPattern?.id}"
2344
- FUNCTIONS_WORKER_RUNTIME = "python"
2345
- SERVICEBUS_QUEUE_NAME = var.function_worker_queue_name
2346
- ServiceBusConnection__clientId = azurerm_user_assigned_identity.app.client_id
2347
- ServiceBusConnection__fullyQualifiedNamespace = "\${azurerm_servicebus_namespace.main.name}.servicebus.windows.net"
2348
- SHARED_ORCHESTRATION_ROOT = "../../backend"
2349
- }
2350
- }
2351
-
2352
- resource "azurerm_role_assignment" "function_servicebus_receiver" {
2353
- scope = azurerm_servicebus_namespace.main.id
2354
- role_definition_name = "Azure Service Bus Data Receiver"
2355
- principal_id = azurerm_user_assigned_identity.app.principal_id
2356
- }
2357
-
2358
- resource "azurerm_role_assignment" "function_storage_blob_contributor" {
2359
- scope = azurerm_storage_account.main.id
2360
- role_definition_name = "Storage Blob Data Contributor"
2361
- principal_id = azurerm_user_assigned_identity.app.principal_id
2362
- }
2363
- ` : '';
2364
- return `resource "azurerm_resource_group" "main" {
2365
- name = "${names.resourceGroup}"
2366
- location = var.location
2367
- }
2368
-
2369
- resource "azurerm_container_registry" "main" {
2370
- name = "${names.containerRegistry}"
2371
- resource_group_name = azurerm_resource_group.main.name
2372
- location = azurerm_resource_group.main.location
2373
- sku = "Basic"
2374
- admin_enabled = false
2375
- }
2376
-
2377
- resource "azurerm_user_assigned_identity" "app" {
2378
- name = "${names.identity}"
2379
- resource_group_name = azurerm_resource_group.main.name
2380
- location = azurerm_resource_group.main.location
2381
- }
2382
-
2383
- resource "azurerm_role_assignment" "acr_pull" {
2384
- scope = azurerm_container_registry.main.id
2385
- role_definition_name = "AcrPull"
2386
- principal_id = azurerm_user_assigned_identity.app.principal_id
2387
- }
2388
-
2389
- resource "azurerm_container_app_environment" "main" {
2390
- name = "${names.containerAppEnvironment}"
2391
- resource_group_name = azurerm_resource_group.main.name
2392
- location = azurerm_resource_group.main.location
2393
- }
2394
-
2395
- resource "azurerm_container_app" "backend" {
2396
- name = "${names.backendContainerApp}"
2397
- container_app_environment_id = azurerm_container_app_environment.main.id
2398
- resource_group_name = azurerm_resource_group.main.name
2399
- revision_mode = "Single"
2400
-
2401
- identity {
2402
- type = "UserAssigned"
2403
- identity_ids = [azurerm_user_assigned_identity.app.id]
2404
- }
2405
-
2406
- registry {
2407
- server = azurerm_container_registry.main.login_server
2408
- identity = azurerm_user_assigned_identity.app.id
2409
- }
2410
-
2411
- secret {
2412
- name = "database-url"
2413
- value = "postgresql://liftoffadmin:\${urlencode(var.postgres_admin_password)}@\${azurerm_postgresql_flexible_server.main.fqdn}:5432/postgres?sslmode=require"
2414
- }
2415
-
2416
- secret {
2417
- name = "redis-url"
2418
- value = "rediss://:\${urlencode(azurerm_redis_cache.main.primary_access_key)}@\${azurerm_redis_cache.main.hostname}:\${azurerm_redis_cache.main.ssl_port}/0"
2419
- }
2420
-
2421
- template {
2422
- container {
2423
- name = "backend"
2424
- image = var.backend_image
2425
- cpu = 0.5
2426
- memory = "1Gi"
2427
-
2428
- env {
2429
- name = "APP_ENV"
2430
- value = var.environment
2431
- }
2432
-
2433
- env {
2434
- name = "APP_NAME"
2435
- value = "${plan.safeProjectName}"
2436
- }
2437
-
2438
- env {
2439
- name = "PROJECT_TYPE"
2440
- value = "${plan.projectType.id}"
2441
- }
2442
- ${projectIdentityEnv}
2443
- env {
2444
- name = "CLOUD_PROVIDER"
2445
- value = "azure"
2446
- }
2447
-
2448
- env {
2449
- name = "AZURE_REGION"
2450
- value = var.location
2451
- }
2452
- ${frontendCorsEnvironment}
2453
-
2454
- env {
2455
- name = "DATABASE_URL"
2456
- secret_name = "database-url"
2457
- }
2458
-
2459
- env {
2460
- name = "REDIS_URL"
2461
- secret_name = "redis-url"
2462
- }
2463
-
2464
- env {
2465
- name = "MESSAGING_TRANSPORT"
2466
- value = "azure-service-bus"
2467
- }
2468
-
2469
- env {
2470
- name = "BLOB_ENDPOINT"
2471
- value = azurerm_storage_account.main.primary_blob_endpoint
2472
- }
2473
- }
2474
- }
2475
-
2476
- ingress {
2477
- external_enabled = true
2478
- target_port = var.backend_target_port
2479
- traffic_weight {
2480
- percentage = 100
2481
- latest_revision = true
2482
- }
2483
- }
2484
-
2485
- depends_on = [azurerm_role_assignment.acr_pull]
2486
- }
2487
- ${frontendContainer}
2488
- resource "azurerm_postgresql_flexible_server" "main" {
2489
- name = "${names.postgres}"
2490
- resource_group_name = azurerm_resource_group.main.name
2491
- location = azurerm_resource_group.main.location
2492
- version = "16"
2493
- administrator_login = "liftoffadmin"
2494
- administrator_password = var.postgres_admin_password
2495
- storage_mb = 32768
2496
- sku_name = "B_Standard_B1ms"
2497
- }
2498
-
2499
- resource "azurerm_postgresql_flexible_server_firewall_rule" "azure_services" {
2500
- count = var.enable_private_networking ? 0 : 1
2501
- name = "AllowAzureServices"
2502
- server_id = azurerm_postgresql_flexible_server.main.id
2503
- start_ip_address = "0.0.0.0"
2504
- end_ip_address = "0.0.0.0"
2505
- }
2506
-
2507
- resource "azurerm_redis_cache" "main" {
2508
- name = "${names.redis}"
2509
- location = azurerm_resource_group.main.location
2510
- resource_group_name = azurerm_resource_group.main.name
2511
- capacity = 0
2512
- family = "C"
2513
- sku_name = "Basic"
2514
- }
2515
-
2516
- resource "azurerm_storage_account" "main" {
2517
- name = "${names.storage}"
2518
- resource_group_name = azurerm_resource_group.main.name
2519
- location = azurerm_resource_group.main.location
2520
- account_tier = "Standard"
2521
- account_replication_type = "LRS"
2522
- }
2523
-
2524
- resource "azurerm_storage_container" "documents" {
2525
- name = "documents"
2526
- storage_account_id = azurerm_storage_account.main.id
2527
- container_access_type = "private"
2528
- }
2529
-
2530
- resource "azurerm_servicebus_namespace" "main" {
2531
- name = "${names.serviceBus}"
2532
- location = azurerm_resource_group.main.location
2533
- resource_group_name = azurerm_resource_group.main.name
2534
- sku = "Standard"
2535
- }
2536
-
2537
- resource "azurerm_servicebus_queue" "events" {
2538
- name = ${queueName}
2539
- namespace_id = azurerm_servicebus_namespace.main.id
2540
- }
2541
- ${functionWorker}
2542
-
2543
- resource "azurerm_communication_service" "main" {
2544
- name = "${names.communication}"
2545
- resource_group_name = azurerm_resource_group.main.name
2546
- data_location = "United States"
2547
- }
2548
-
2549
- resource "azurerm_key_vault" "main" {
2550
- name = "${names.keyVault}"
2551
- location = azurerm_resource_group.main.location
2552
- resource_group_name = azurerm_resource_group.main.name
2553
- tenant_id = data.azurerm_client_config.current.tenant_id
2554
- sku_name = "standard"
2555
- rbac_authorization_enabled = true
2556
- }
2557
-
2558
- data "azurerm_client_config" "current" {}
2559
- `;
2560
- }
2561
- function renderTofuOutputs(plan) {
2562
- const functionOutputs = hasFunctionWorker(plan) ? `
2563
- output "function_app_name" {
2564
- value = azurerm_linux_function_app.worker.name
2565
- }
2566
-
2567
- output "function_worker_queue_name" {
2568
- value = azurerm_servicebus_queue.events.name
2569
- }
2570
- ` : '';
2571
- return `output "backend_url" {
2572
- value = azurerm_container_app.backend.ingress[0].fqdn
2573
- }
2574
-
2575
- ${plan.includeFrontend ? `output "frontend_url" {
2576
- value = azurerm_container_app.frontend.ingress[0].fqdn
2577
- }
2578
- ` : ''}${functionOutputs}output "container_registry" {
2579
- value = azurerm_container_registry.main.login_server
2580
- }
2581
-
2582
- output "container_registry_name" {
2583
- value = azurerm_container_registry.main.name
2584
- }
2585
- `;
2586
- }
2587
- function renderTofuLocalState() {
2588
- return `# Local state is the V1 default for first-use simplicity.
2589
- # Teams can replace this file with backend.remote.example.tf when adopting shared state.
2590
- `;
2591
- }
2592
- function renderTofuRemoteStateExample() {
2593
- return `# Rename to backend.tf and configure values for shared state.
2594
- # terraform {
2595
- # backend "azurerm" {
2596
- # resource_group_name = "rg-opentofu-state"
2597
- # storage_account_name = "stliftoffstate"
2598
- # container_name = "tfstate"
2599
- # key = "mission-control/liftoff.tfstate"
2600
- # }
2601
- # }
2602
- `;
2603
- }
2604
- function renderTofuReadme(plan) {
2605
- const env = plan.environments[0]?.id ?? 'dev';
2606
- const functionSection = plan.workload === 'genai' && hasFunctionWorker(plan) ? `
2607
- ## Azure Functions Worker
2608
-
2609
- This project includes an Azure Functions worker under \`functions/${functionWorkerName(plan)}\`. The OpenTofu configuration attaches one user-assigned identity, grants its principal the Service Bus Data Receiver role, and selects it through \`ServiceBusConnection__clientId\` plus \`ServiceBusConnection__fullyQualifiedNamespace\`. \`function_worker_queue_name\` provisions the queue, configures \`SERVICEBUS_QUEUE_NAME\`, and drives the worker queue output. Function host storage uses the complete key-backed \`AzureWebJobsStorage\` connection setting.
2610
- ` : '';
2611
- return `# Azure OpenTofu
2612
-
2613
- Azure is the complete V1 provider for this Liftoff project.
2614
-
2615
- ## Bootstrap Infrastructure
2616
-
2617
- The first apply uses a public bootstrap image so Azure Container Apps can start before the new ACR contains application images.
2618
-
2619
- \`\`\`bash
2620
- tofu init
2621
- tofu plan -var-file=environments/${env}.tfvars
2622
- tofu apply -var-file=environments/${env}.tfvars
2623
- \`\`\`
2624
-
2625
- Build the generated backend in ACR, then replace the bootstrap image:
2626
-
2627
- \`\`\`bash
2628
- ACR_NAME="$(tofu output -raw container_registry_name)"
2629
- SOURCE_SHA="$(git rev-parse HEAD)"
2630
- az acr build --registry "$ACR_NAME" --image ${plan.safeProjectName}-backend:"$SOURCE_SHA" ../../..
2631
- ${plan.includeFrontend ? `BACKEND_URL="https://$(tofu output -raw backend_url)"
2632
- az acr build --registry "$ACR_NAME" --image ${plan.safeProjectName}-frontend:"$SOURCE_SHA" --build-arg VITE_API_BASE_URL="$BACKEND_URL" ../../../frontend
2633
- ` : ''}\`\`\`
2634
-
2635
- Persist the deployed images in \`environments/${env}.tfvars\` so future applies do not restore the bootstrap image:
2636
-
2637
- \`\`\`hcl
2638
- backend_image = "<login-server>/${plan.safeProjectName}-backend@sha256:<manifest-digest>"
2639
- backend_target_port = 8000
2640
- ${plan.includeFrontend ? `frontend_image = "<login-server>/${plan.safeProjectName}-frontend@sha256:<manifest-digest>"
2641
- ` : ''}\`\`\`
2642
-
2643
- \`\`\`bash
2644
- tofu apply -var-file=environments/${env}.tfvars
2645
- \`\`\`
2646
-
2647
- Local OpenTofu state is generated by default. Use \`backend.remote.example.tf\` as the starting point for team remote state.
2648
- The default PostgreSQL firewall permits Azure-hosted services. Replace it with private networking before production; set \`enable_private_networking=true\` only when the required VNet, delegated subnet, and private DNS resources are added.
2649
-
2650
- ## Azure Name Suffixes
2651
-
2652
- Each environment tfvars file contains a deterministic 12-character lowercase alphanumeric \`resource_suffix\` used by globally scoped Azure names. If Azure reports that a name is already taken, replace that environment's suffix with another unique value matching \`^[a-z0-9]{12}$\`; \`tofu validate\` rejects invalid overrides before deployment.
2653
- ${functionSection}
2654
- `;
2655
- }
2656
- function renderTofuTfvars(plan, environment) {
2657
- const values = [
2658
- ['environment', JSON.stringify(environment)],
2659
- ['location', JSON.stringify(plan.region.slug)],
2660
- ['resource_suffix', JSON.stringify(stableResourceSuffix(plan, environment))],
2661
- ['backend_image', JSON.stringify(formatContainerImage(supportedStack.containers['container-apps-bootstrap']))],
2662
- ['backend_target_port', '80'],
2663
- ['enable_private_networking', 'false']
2664
- ];
2665
- if (plan.includeFrontend) {
2666
- values.push(['frontend_image', JSON.stringify(formatContainerImage(supportedStack.containers['container-apps-bootstrap']))]);
2667
- }
2668
- if (hasFunctionWorker(plan)) {
2669
- values.push(['function_worker_queue_name', JSON.stringify(DEFAULT_FUNCTION_WORKER_QUEUE_NAME)], ['functions_python_version', JSON.stringify(supportedStack.runtimes.python.releaseLine)]);
2670
- }
2671
- const width = Math.max(...values.map(([key]) => key.length));
2672
- return values.map(([key, value]) => `${key.padEnd(width)} = ${value}`).join('\n');
2673
- }
2674
- function renderOpenSpecConfig(plan) {
2675
- const frontendRule = plan.includeFrontend ? '\n - Keep frontend code under frontend.' : '';
2676
- if (plan.workload === 'standard') {
2677
- const backendRule = plan.apiStack.id === 'python-fastapi'
2678
- ? 'Keep backend API code under backend/apis.'
2679
- : plan.apiStack.id === 'node-fastify'
2680
- ? 'Keep backend API code under backend/src.'
2681
- : 'Keep the Go entrypoint under backend/cmd/api and reusable code under backend/internal.';
2682
- return `schema: spec-driven${renderOpenSpecCopilotConfig(plan)}
2683
-
2684
- context: |
2685
- Project generated by Mission Control Liftoff.
2686
- Project type: Standard application.
2687
- API stack: ${plan.apiStack.label}.
2688
- Database tooling: ${plan.apiStack.databaseTooling}.
2689
- API developer portal: Scalar.
2690
- Infrastructure: OpenTofu.
2691
- Primary cloud: Azure (${plan.region.slug}).
2692
- Local development: Docker Compose.
2693
- Database: PostgreSQL.
2694
- Cache and local messaging: Redis.
2695
- Environments: ${plan.environments.map((environment) => environment.id).join(', ')}.
2696
-
2697
- rules:
2698
- specs:
2699
- - Requirements must describe observable product behavior.
2700
- - Cloud behavior must identify environment differences for generated environments.
2701
- design:
2702
- - Use ${plan.apiStack.framework} for backend APIs.
2703
- - Use ${plan.apiStack.databaseTooling} for database access and migrations.
2704
- - Use OpenTofu for infrastructure changes.${frontendRule}
2705
- - ${backendRule}
2706
- - Keep database artifacts under database.
2707
- tasks:
2708
- - Include local Docker Compose verification.
2709
- - Include OpenTofu validation for generated infrastructure.
2710
- `;
2711
- }
2712
- const pattern = genAiPattern(plan);
2713
- const functionsContext = hasFunctionWorker(plan) ? `
2714
- Azure Functions worker: functions/${functionWorkerName(plan)}.` : '';
2715
- const functionsRule = hasFunctionWorker(plan) ? `
2716
- - Keep Azure Functions trigger adapters under functions/${functionWorkerName(plan)}.
2717
- - Keep reusable GenAI orchestration under backend/orchestration.` : '';
2718
- return `schema: spec-driven${renderOpenSpecCopilotConfig(plan)}
2719
-
2720
- context: |
2721
- Project generated by Mission Control Liftoff.
2722
- GenAI pattern: ${pattern.label}
2723
- Application framework: FastAPI + PydanticAI.
2724
- API developer portal: Scalar.
2725
- Infrastructure: OpenTofu.
2726
- Primary cloud: Azure (${plan.region.slug}).
2727
- Local development: Docker Compose.
2728
- Database: PostgreSQL with Alembic migrations.
2729
- Cache and local messaging: Redis.
2730
- Observability: Langfuse.
2731
- Environments: ${plan.environments.map((environment) => environment.id).join(', ')}.
2732
- ${functionsContext}
2733
-
2734
- rules:
2735
- specs:
2736
- - Requirements must describe observable product behavior.
2737
- - Cloud behavior must identify environment differences for generated environments.
2738
- design:
2739
- - Use PydanticAI for orchestration logic.
2740
- - Use Pydantic settings models for runtime configuration.
2741
- - Use OpenTofu for infrastructure changes.${frontendRule}
2742
- - Keep backend API code under backend/apis.
2743
- ${functionsRule}
2744
- - Keep database artifacts under database.
2745
- tasks:
2746
- - Include local Docker Compose verification.
2747
- - Include OpenTofu validation for generated infrastructure.
2748
- `;
2749
- }
2750
- function renderSeedProposal(plan) {
2751
- const capability = seedCapabilityId(plan);
2752
- if (plan.workload === 'standard') {
2753
- return `## Why
2754
-
2755
- Bootstrap the generated ${plan.apiStack.label} standard application baseline created by Mission Control Liftoff.
2756
-
2757
- ## What Changes
2758
-
2759
- - Establish the approved backend, infrastructure, local development, and governance baseline.
2760
- - Confirm that domain-specific product behavior is deferred to follow-up spec-driven changes.
2761
-
2762
- ## Capabilities
2763
-
2764
- ### New Capabilities
2765
-
2766
- - \`${capability}\`: Generated standard application baseline for this Liftoff project.
2767
-
2768
- ### Modified Capabilities
2769
-
2770
- - None.
2771
-
2772
- ## Impact
2773
-
2774
- - Generated ${plan.apiStack.label} backend, OpenTofu infrastructure, Docker Compose local development, and governance files.
2775
- `;
2776
- }
2777
- const pattern = genAiPattern(plan);
2778
- const functionsChange = hasFunctionWorker(plan) ? '\n- Establish Azure Functions worker trigger adapters for event-driven processing.' : '';
2779
- return `## Why
2780
-
2781
- Bootstrap the generated ${pattern.label} application baseline created by Mission Control Liftoff.
2782
-
2783
- ## What Changes
2784
-
2785
- - Establish the approved backend, infrastructure, local development, and governance baseline.
2786
- - Confirm that domain-specific product behavior is deferred to follow-up spec-driven changes.
2787
- ${functionsChange}
2788
-
2789
- ## Capabilities
2790
-
2791
- ### New Capabilities
2792
-
2793
- - \`${capability}\`: Generated application baseline for this Liftoff project.
2794
-
2795
- ### Modified Capabilities
2796
-
2797
- - None.
2798
-
2799
- ## Impact
2800
-
2801
- - Generated FastAPI/PydanticAI backend, OpenTofu infrastructure, Docker Compose local development, and governance files.
2802
- `;
2803
- }
2804
- function renderSeedDesign(plan) {
2805
- if (plan.workload === 'standard') {
2806
- return `## Context
2807
-
2808
- This standard project was generated with Liftoff using ${plan.apiStack.label}, Azure, OpenTofu, and ${plan.specWorkflow.label}.
2809
-
2810
- ## Goals / Non-Goals
2811
-
2812
- **Goals:**
2813
-
2814
- - Keep the generated baseline aligned to the approved Mission Control stack.
2815
- - Verify only deterministic local scaffold checks before archiving this bootstrap change.
2816
-
2817
- **Non-Goals:**
2818
-
2819
- - Define domain-specific product behavior in the bootstrap change.
2820
- - Deploy infrastructure, start containers, mutate GitHub, or perform cloud plan/apply operations.
2821
-
2822
- ## Decisions
2823
-
2824
- - Use ${plan.apiStack.framework} for backend APIs.
2825
- - Use ${plan.apiStack.databaseTooling} for PostgreSQL integration.
2826
- - Use OpenTofu for Azure infrastructure.
2827
- - Use Docker Compose for local development.
2828
-
2829
- ## Risks / Trade-offs
2830
-
2831
- - Follow-up product changes own real business behavior; this seed only proves the generated local baseline is coherent.
2832
- `;
2833
- }
2834
- const pattern = genAiPattern(plan);
2835
- const functionsDecision = hasFunctionWorker(plan) ? '\n- Keep Azure Functions trigger adapters under functions/' + functionWorkerName(plan) + ' and shared GenAI logic under backend/orchestration.' : '';
2836
- return `## Context
2837
-
2838
- This project was generated with Liftoff using ${pattern.label}, Azure, OpenTofu, and ${plan.specWorkflow.label}.
2839
-
2840
- ## Goals / Non-Goals
2841
-
2842
- **Goals:**
2843
-
2844
- - Keep the generated baseline aligned to the approved Mission Control stack.
2845
- - Verify only deterministic local scaffold checks before archiving this bootstrap change.
2846
-
2847
- **Non-Goals:**
2848
-
2849
- - Define domain-specific product behavior in the bootstrap change.
2850
- - Deploy infrastructure, start containers, mutate GitHub, or perform cloud plan/apply operations.
2851
-
2852
- ## Decisions
2853
-
2854
- - Use FastAPI and PydanticAI for backend APIs and orchestration.
2855
- - Use OpenTofu for Azure infrastructure.
2856
- - Use Docker Compose for local development.
2857
- ${functionsDecision}
2858
-
2859
- ## Risks / Trade-offs
2860
-
2861
- - Follow-up product changes own real business behavior; this seed only proves the generated local baseline is coherent.
2862
- `;
2863
- }
2864
- function seedCapabilityId(plan) {
2865
- if (plan.workload === 'power-apps-code-app') {
2866
- return 'power-apps-code-app-baseline';
2867
- }
2868
- if (plan.workload === 'standard') {
2869
- return `${plan.apiStack.id}-application-baseline`;
2870
- }
2871
- return `${genAiPattern(plan).id}-application-baseline`;
2872
- }
2873
- function seedCapabilityPurpose(plan) {
2874
- if (plan.workload === 'power-apps-code-app') {
2875
- return `Define the generated ${plan.projectName} Power Apps code app baseline that setup verifies and archives before domain-specific implementation or deployment begins.`;
2876
- }
2877
- if (plan.workload === 'standard') {
2878
- return `Define the generated ${plan.apiStack.label} application baseline that setup verifies and archives before domain-specific implementation or deployment begins.`;
2879
- }
2880
- return `Define the generated ${genAiPattern(plan).label} application baseline that setup verifies and archives before domain-specific implementation or deployment begins.`;
2881
- }
2882
- function renderSeedSpec(plan) {
2883
- const capability = seedCapabilityId(plan);
2884
- const purpose = seedCapabilityPurpose(plan);
2885
- if (plan.workload === 'power-apps-code-app') {
2886
- return `## Purpose
2887
-
2888
- ${purpose}
2889
-
2890
- ## ADDED Requirements
2891
-
2892
- ### Requirement: Generated Power Apps code app baseline is locally verifiable
2893
- The bootstrap seed SHALL describe only the generated Power Apps code app scaffold, workflow configuration, and local validation boundary. Domain-specific screens, connector behavior, tenant binding, solution packaging, deployment, credentials, and other product behavior SHALL be deferred to later changes.
2894
-
2895
- #### Scenario: Generated baseline is present
2896
- - **WHEN** the \`bootstrap-${plan.safeProjectName}\` change is generated
2897
- - **THEN** it declares the \`${capability}\` capability
2898
- - **AND** the generated React, Vite, TypeScript, Tailwind CSS, Power Apps SDK, workflow, and governance files are in scope
2899
-
2900
- #### Scenario: Product behavior is deferred
2901
- - **WHEN** the bootstrap change is completed
2902
- - **THEN** no domain-specific connector, tenant, environment, solution, credential, deployment, backend API, Docker, or OpenTofu behavior is introduced
2903
-
2904
- #### Scenario: Local baseline checks pass
2905
- - **WHEN** deterministic setup verifies the seed
2906
- - **THEN** \`liftoff validate\`, the root frontend build, and strict OpenSpec validation have passed
2907
- - **AND** backend, Docker, worker, and OpenTofu checks are recorded as inapplicable without placeholder commands
2908
- `;
2909
- }
2910
- if (plan.workload === 'standard') {
2911
- return `## Purpose
2912
-
2913
- ${purpose}
2914
-
2915
- ## ADDED Requirements
2916
-
2917
- ### Requirement: Generated ${plan.apiStack.label} baseline is locally verifiable
2918
- The bootstrap seed SHALL describe only the generated ${plan.apiStack.label} backend, optional frontend, Docker Compose, OpenTofu, workflow configuration, and governance baseline. Domain-specific product behavior SHALL be deferred to later changes.
2919
-
2920
- #### Scenario: Generated baseline is present
2921
- - **WHEN** the \`bootstrap-${plan.safeProjectName}\` change is generated
2922
- - **THEN** it declares the \`${capability}\` capability
2923
- - **AND** the generated backend, database, Docker Compose, OpenTofu, workflow, and governance files are in scope
2924
-
2925
- #### Scenario: Product behavior is deferred
2926
- - **WHEN** the bootstrap change is completed
2927
- - **THEN** no domain-specific API, data model, UI, deployment, GitHub, or cloud mutation is introduced
2928
-
2929
- #### Scenario: Local baseline checks pass
2930
- - **WHEN** deterministic setup verifies the seed
2931
- - **THEN** \`liftoff validate\`, backend tests, Docker Compose configuration validation, OpenTofu formatting, backend-disabled initialization, OpenTofu validation, and strict OpenSpec validation have passed
2932
- - **AND** frontend checks are run only when a frontend was generated
2933
- `;
2934
- }
2935
- const pattern = genAiPattern(plan);
2936
- const worker = hasFunctionWorker(plan)
2937
- ? 'Azure Functions worker, '
2938
- : '';
2939
- return `## Purpose
2940
-
2941
- ${purpose}
2942
-
2943
- ## ADDED Requirements
2944
-
2945
- ### Requirement: Generated ${pattern.label} baseline is locally verifiable
2946
- The bootstrap seed SHALL describe only the generated FastAPI/PydanticAI backend, ${worker}optional frontend, Docker Compose, OpenTofu, workflow configuration, and governance baseline. Domain-specific product behavior SHALL be deferred to later changes.
2947
-
2948
- #### Scenario: Generated baseline is present
2949
- - **WHEN** the \`bootstrap-${plan.safeProjectName}\` change is generated
2950
- - **THEN** it declares the \`${capability}\` capability
2951
- - **AND** the generated GenAI backend, database, Docker Compose, OpenTofu, workflow, governance${hasFunctionWorker(plan) ? ', and worker' : ''} files are in scope
2952
-
2953
- #### Scenario: Product behavior is deferred
2954
- - **WHEN** the bootstrap change is completed
2955
- - **THEN** no domain-specific prompts, tools, retrieval corpus, agent policy, UI, deployment, GitHub, or cloud mutation is introduced
2956
-
2957
- #### Scenario: Local baseline checks pass
2958
- - **WHEN** deterministic setup verifies the seed
2959
- - **THEN** \`liftoff validate\`, backend tests,${hasFunctionWorker(plan) ? ' worker tests,' : ''} Docker Compose configuration validation, OpenTofu formatting, backend-disabled initialization, OpenTofu validation, and strict OpenSpec validation have passed
2960
- - **AND** frontend checks are run only when a frontend was generated
2961
- `;
2962
- }
2963
- function seedTaskChecks(plan) {
2964
- const checks = [
2965
- {
2966
- id: '2.1',
2967
- label: 'Run Liftoff manifest validation',
2968
- command: { executable: 'liftoff', args: ['validate'] }
2969
- }
2970
- ];
2971
- if (plan.workload === 'power-apps-code-app') {
2972
- checks.push({
2973
- id: '2.2',
2974
- label: 'Record backend tests as inapplicable',
2975
- inapplicable: 'no Liftoff backend is generated for Power Apps code apps'
2976
- });
2977
- }
2978
- else if (plan.workload === 'genai' || plan.apiStack.id === 'python-fastapi') {
2979
- checks.push({
2980
- id: '2.2',
2981
- label: 'Run backend tests',
2982
- command: { executable: 'uv', args: ['run', '--project', 'backend', 'python', '-m', 'pytest', '-q', 'backend/tests'] }
2983
- });
2984
- }
2985
- else if (plan.apiStack.id === 'node-fastify') {
2986
- checks.push({
2987
- id: '2.2',
2988
- label: 'Run backend tests',
2989
- command: { executable: 'npm', args: ['test'] },
2990
- cwd: ['backend']
2991
- });
2992
- }
2993
- else {
2994
- checks.push({
2995
- id: '2.2',
2996
- label: 'Run backend tests',
2997
- command: { executable: 'go', args: ['test', './...'] },
2998
- cwd: ['backend']
2999
- });
3000
- }
3001
- if (plan.workload === 'genai' && hasFunctionWorker(plan)) {
3002
- checks.push({
3003
- id: '2.3',
3004
- label: 'Run generated worker tests',
3005
- command: { executable: 'uv', args: ['run', '--project', '../../backend', '--directory', '.', 'python', '-m', 'pytest', '-q'] },
3006
- cwd: ['functions', functionWorkerName(plan)]
3007
- });
3008
- }
3009
- else {
3010
- checks.push({
3011
- id: '2.3',
3012
- label: 'Record worker tests as inapplicable',
3013
- inapplicable: 'no generated worker boundary is present'
3014
- });
3015
- }
3016
- if (plan.workload === 'power-apps-code-app') {
3017
- checks.push({
3018
- id: '2.4',
3019
- label: 'Run frontend build',
3020
- command: { executable: 'npm', args: ['run', 'build'] }
3021
- });
3022
- }
3023
- else if (plan.includeFrontend) {
3024
- checks.push({
3025
- id: '2.4',
3026
- label: 'Run frontend build',
3027
- command: { executable: 'npm', args: ['run', 'build'] },
3028
- cwd: ['frontend']
3029
- });
3030
- }
3031
- else {
3032
- checks.push({
3033
- id: '2.4',
3034
- label: 'Record frontend build as inapplicable',
3035
- inapplicable: 'no generated frontend is present'
3036
- });
3037
- }
3038
- if (plan.workload === 'power-apps-code-app') {
3039
- checks.push({
3040
- id: '2.5',
3041
- label: 'Record Docker Compose validation as inapplicable',
3042
- inapplicable: 'no generated Docker Compose boundary is present'
3043
- });
3044
- checks.push({
3045
- id: '2.6',
3046
- label: 'Record OpenTofu formatting as inapplicable',
3047
- inapplicable: 'no generated OpenTofu boundary is present'
3048
- });
3049
- checks.push({
3050
- id: '2.7',
3051
- label: 'Record OpenTofu initialization as inapplicable',
3052
- inapplicable: 'no generated OpenTofu boundary is present'
3053
- });
3054
- checks.push({
3055
- id: '2.8',
3056
- label: 'Record OpenTofu validation as inapplicable',
3057
- inapplicable: 'no generated OpenTofu boundary is present'
3058
- });
3059
- }
3060
- else {
3061
- checks.push({
3062
- id: '2.5',
3063
- label: 'Validate Docker Compose configuration without startup',
3064
- command: { executable: 'docker', args: ['compose', 'config', '-q'] }
3065
- });
3066
- checks.push({
3067
- id: '2.6',
3068
- label: 'Check OpenTofu formatting',
3069
- command: { executable: 'tofu', args: ['fmt', '-check', '-recursive'] },
3070
- cwd: ['infrastructure', 'opentofu', 'azure']
3071
- });
3072
- checks.push({
3073
- id: '2.7',
3074
- label: 'Initialize OpenTofu without a remote backend',
3075
- command: { executable: 'tofu', args: ['init', '-backend=false'] },
3076
- cwd: ['infrastructure', 'opentofu', 'azure']
3077
- });
3078
- checks.push({
3079
- id: '2.8',
3080
- label: 'Validate OpenTofu configuration without plan or apply',
3081
- command: { executable: 'tofu', args: ['validate'] },
3082
- cwd: ['infrastructure', 'opentofu', 'azure']
3083
- });
3084
- }
3085
- checks.push({
3086
- id: '2.9',
3087
- label: 'Run strict OpenSpec validation',
3088
- command: { executable: 'openspec', args: ['validate', `bootstrap-${plan.safeProjectName}`, '--strict'] }
3089
- });
3090
- return checks;
3091
- }
3092
- function renderSeedTaskCheck(check) {
3093
- if (!check.command) {
3094
- return `- [ ] ${check.id} ${check.label}: inapplicable because ${check.inapplicable}.`;
3095
- }
3096
- const cwd = check.cwd && check.cwd.length > 0 ? ` from \`${check.cwd.join('/')}\`` : '';
3097
- return `- [ ] ${check.id} ${check.label}${cwd}: \`${formatCommand(check.command)}\`.`;
3098
- }
3099
- function renderSeedTasks(plan) {
3100
- return `## 1. Bootstrap Scope
3101
-
3102
- - [ ] 1.1 Confirm generated baseline files, \`liftoff.manifest.json\`, OpenSpec configuration, and the \`${seedCapabilityId(plan)}\` delta spec are present.
3103
- - [ ] 1.2 Confirm domain-specific product behavior is deferred to follow-up OpenSpec changes; do not replace generated placeholders in this bootstrap change.
3104
-
3105
- ## 2. Local Baseline Checks
3106
-
3107
- ${seedTaskChecks(plan).map(renderSeedTaskCheck).join('\n')}
3108
-
3109
- ## 3. Completion
3110
-
3111
- - [ ] 3.1 After every applicable local check succeeds and every absent component is recorded inapplicable, archive with \`openspec archive bootstrap-${plan.safeProjectName} --yes\`, then require \`openspec validate --all --strict\` to pass. OpenSpec archive updates the main specs as part of archive; do not pass \`--skip-specs\`.
3112
-
3113
- No task in this bootstrap seed starts containers, runs cloud plan/apply, mutates GitHub, or implements domain-specific product behavior.
3114
- `;
3115
- }
3116
- function renderSpecKitConstitution(plan) {
3117
- if (plan.workload === 'standard') {
3118
- const backendLayout = plan.apiStack.id === 'python-fastapi'
3119
- ? 'backend/apis'
3120
- : plan.apiStack.id === 'node-fastify' ? 'backend/src' : 'backend/cmd/api and backend/internal';
3121
- return `# Mission Control Liftoff Constitution
3122
-
3123
- ## Principle 1: Approved Application Stack
3124
- Generated backend services MUST use ${plan.apiStack.framework}, ${plan.apiStack.databaseTooling}, and Scalar for API documentation.
3125
-
3126
- ## Principle 2: Standard Project Layout
3127
- Backend APIs live under ${backendLayout}. Database artifacts live under database.${plan.includeFrontend ? ' Frontend code lives under frontend.' : ''}
3128
-
3129
- ## Principle 3: Infrastructure As Code
3130
- Cloud infrastructure MUST be defined with OpenTofu. Azure is the supported V1 provider.
3131
-
3132
- ## Principle 4: Local Development Parity
3133
- Projects MUST include Docker Compose for local development with PostgreSQL, Redis, local blob storage, and local messaging behavior.
3134
-
3135
- ## Principle 5: Observability And Operations
3136
- Services MUST use structured logging and environment-specific configuration for ${plan.environments.map((environment) => environment.id).join(', ')}.
3137
- `;
3138
- }
3139
- const functionsLayout = plan.workload === 'genai' && hasFunctionWorker(plan)
3140
- ? ` Azure Functions trigger adapters live under functions/${functionWorkerName(plan)} and call shared orchestration from backend/orchestration.`
3141
- : '';
3142
- return `# Mission Control Liftoff Constitution
3143
-
3144
- ## Principle 1: Approved Application Stack
3145
- Generated backend services MUST use FastAPI, PydanticAI, Pydantic configuration models, and Scalar for API documentation.
3146
-
3147
- ## Principle 2: Standard Project Layout
3148
- Backend APIs live under backend/apis. Database artifacts live under database.${plan.includeFrontend ? ' Frontend code lives under frontend.' : ''}${functionsLayout}
3149
-
3150
- ## Principle 3: Infrastructure As Code
3151
- Cloud infrastructure MUST be defined with OpenTofu. Azure is the supported V1 provider.
3152
-
3153
- ## Principle 4: Local Development Parity
3154
- Projects MUST include Docker Compose for local development with PostgreSQL, Redis, local blob storage, and local messaging behavior.
3155
-
3156
- ## Principle 5: Observability And Operations
3157
- LLM workflows MUST include Langfuse tracing hooks and environment-specific configuration for ${plan.environments.map((environment) => environment.id).join(', ')}.
3158
- `;
3159
- }
3160
- function renderSpecKitSpecTemplate() {
3161
- return `# Feature Specification
3162
-
3163
- ## User Scenarios
3164
-
3165
- ## Requirements
3166
-
3167
- ## Success Criteria
3168
- `;
3169
- }
3170
- function renderSpecKitPlanTemplate() {
3171
- return `# Implementation Plan
3172
-
3173
- ## Technical Context
3174
-
3175
- ## Constitution Check
3176
-
3177
- ## Tasks
3178
- `;
3179
- }
3180
- function renderFrontendPackage(plan) {
3181
- return renderNpmPackage('frontend', `${plan.safeProjectName}-frontend`);
3182
- }
3183
- function renderFrontendIndex(plan) {
3184
- return `<div id="app"></div><script type="module" src="/src/main.ts"></script><title>${escapeHtml(plan.projectName)}</title>`;
3185
- }
3186
- function renderFrontendMain() {
3187
- return `import { createApp } from 'vue';
3188
- import App from './App.vue';
3189
- import './styles.css';
3190
-
3191
- createApp(App).mount('#app');
3192
- `;
3193
- }
3194
- function renderFrontendApp(plan) {
3195
- const descriptor = plan.workload === 'genai'
3196
- ? genAiPattern(plan).id === 'generic'
3197
- ? genAiPattern(plan).label
3198
- : `${genAiPattern(plan).label} starter`
3199
- : `${plan.apiStack.label} starter`;
3200
- const apiContract = plan.workload === 'standard'
3201
- ? { route: '/api', method: 'GET', bodyField: '', queryParameter: '', requiresInput: false }
3202
- : genAiPattern(plan).id === 'rag'
3203
- ? { route: `${genAiPattern(plan).routePrefix}/query`, method: 'POST', bodyField: 'question', queryParameter: '', requiresInput: true }
3204
- : genAiPattern(plan).id === 'streaming'
3205
- ? { route: genAiPattern(plan).routePrefix, method: 'GET', bodyField: '', queryParameter: 'prompt', requiresInput: true }
3206
- : { route: `${genAiPattern(plan).routePrefix}/run`, method: 'POST', bodyField: 'input', queryParameter: '', requiresInput: true };
3207
- return `<script setup lang="ts">
3208
- import { ref } from 'vue';
3209
-
3210
- const title = ${scriptSourceString(plan.projectName)};
3211
- const starter = ${scriptSourceString(plan.frontendStarter)};
3212
- const descriptor = ${scriptSourceString(descriptor)};
3213
- const apiBaseUrl = (import.meta.env.VITE_API_BASE_URL || 'http://localhost:8000').replace(/\\/+$/, '');
3214
- const route = ${scriptSourceString(apiContract.route)};
3215
- const method = ${scriptSourceString(apiContract.method)};
3216
- const bodyField = ${scriptSourceString(apiContract.bodyField)};
3217
- const queryParameter = ${scriptSourceString(apiContract.queryParameter)};
3218
- const requiresInput = ${apiContract.requiresInput};
3219
-
3220
- const input = ref('');
3221
- const loading = ref(false);
3222
- const result = ref('');
3223
- const errorMessage = ref('');
3224
-
3225
- async function submit(): Promise<void> {
3226
- const value = input.value.trim();
3227
- if (requiresInput && !value) {
3228
- errorMessage.value = 'Enter a value before running the starter.';
3229
- return;
3230
- }
3231
-
3232
- loading.value = true;
3233
- result.value = '';
3234
- errorMessage.value = '';
3235
- try {
3236
- const query = queryParameter
3237
- ? '?' + queryParameter + '=' + encodeURIComponent(value)
3238
- : '';
3239
- const request: RequestInit = { method };
3240
- if (method === 'POST') {
3241
- request.headers = { 'Content-Type': 'application/json' };
3242
- request.body = JSON.stringify({ [bodyField]: value });
3243
- }
3244
- const response = await fetch(apiBaseUrl + route + query, request);
3245
- const responseText = await response.text();
3246
- if (!response.ok) {
3247
- throw new Error(
3248
- 'Backend request failed (' + response.status + '): ' +
3249
- (responseText || response.statusText)
3250
- );
3251
- }
3252
- if ((response.headers.get('content-type') || '').includes('application/json')) {
3253
- result.value = JSON.stringify(JSON.parse(responseText), null, 2);
3254
- } else {
3255
- result.value = responseText;
3256
- }
3257
- } catch (error) {
3258
- errorMessage.value = error instanceof Error
3259
- ? error.message
3260
- : 'The backend request failed unexpectedly.';
3261
- } finally {
3262
- loading.value = false;
3263
- }
3264
- }
3265
- </script>
3266
-
3267
- <template>
3268
- <main class="min-h-screen bg-slate-50 text-slate-950">
3269
- <section class="mx-auto flex min-h-screen w-full max-w-5xl flex-col gap-6 px-6 py-10">
3270
- <header>
3271
- <p class="text-sm font-semibold uppercase tracking-wide text-emerald-700">Mission Control Liftoff</p>
3272
- <h1 class="mt-2 text-3xl font-bold">{{ title }}</h1>
3273
- <p class="mt-2 text-slate-600">{{ descriptor }}</p>
3274
- </header>
3275
- <section class="rounded-lg border border-slate-200 bg-white p-6 shadow-sm">
3276
- <h2 class="text-xl font-semibold">{{ starter }}</h2>
3277
- <p class="mt-2 text-sm text-slate-500">API: {{ apiBaseUrl }}{{ route }}</p>
3278
- <textarea
3279
- v-if="requiresInput"
3280
- v-model="input"
3281
- class="mt-4 min-h-40 w-full rounded-md border border-slate-300 p-3"
3282
- :disabled="loading"
3283
- placeholder="Enter input for the generated backend."
3284
- />
3285
- <button
3286
- class="mt-4 rounded-md bg-emerald-700 px-4 py-2 font-semibold text-white disabled:cursor-not-allowed disabled:opacity-60"
3287
- :disabled="loading"
3288
- type="button"
3289
- @click="submit"
3290
- >
3291
- {{ loading ? 'Running...' : 'Run' }}
3292
- </button>
3293
- <p v-if="errorMessage" class="mt-4 rounded-md bg-red-50 p-3 text-red-800" role="alert">
3294
- {{ errorMessage }}
3295
- </p>
3296
- <pre v-if="result" class="mt-4 overflow-auto rounded-md bg-slate-950 p-4 text-sm text-white" aria-live="polite">{{ result }}</pre>
3297
- </section>
3298
- </section>
3299
- </main>
3300
- </template>
3301
- `;
3302
- }
3303
- function renderFrontendStyles() {
3304
- return `@import "tailwindcss";
3305
- `;
3306
- }
3307
- function renderFrontendViteConfig() {
3308
- return `import { defineConfig } from 'vite';
3309
- import tailwindcss from '@tailwindcss/vite';
3310
- import vue from '@vitejs/plugin-vue';
3311
-
3312
- export default defineConfig({ plugins: [vue(), tailwindcss()] });
3313
- `;
3314
- }
3315
- function renderFrontendTailwindConfig() {
3316
- return `import type { Config } from 'tailwindcss';
3317
-
3318
- export default {
3319
- content: ['./index.html', './src/**/*.{vue,ts}'],
3320
- theme: { extend: {} },
3321
- plugins: []
3322
- } satisfies Config;
3323
- `;
3324
- }
3325
- function renderFrontendDockerfile() {
3326
- return `FROM ${formatContainerImage(supportedStack.containers['node-runtime'])} AS build
3327
- WORKDIR /app
3328
- ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org
3329
- COPY package.json package-lock.json* ./
3330
- RUN npm ci --ignore-scripts --no-audit --no-fund --registry="$NPM_CONFIG_REGISTRY"
3331
- COPY . .
3332
- ARG VITE_API_BASE_URL=http://localhost:8000
3333
- ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
3334
- RUN npm run build
3335
-
3336
- FROM ${formatContainerImage(supportedStack.containers['nginx-runtime'])}
3337
- COPY --from=build /app/dist /usr/share/nginx/html
3338
- `;
3339
- }
3340
- function ensureTrailingNewline(content) {
3341
- return content.endsWith('\n') ? content : `${content}\n`;
3342
- }
3343
157
  //# sourceMappingURL=templates.js.map