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