@msn-control/liftoff 0.7.0 → 0.9.0
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/README.md +13 -1
- package/assets/governance/single-maintainer-gitflow/policy.md +705 -0
- package/assets/locks/frontend/package-lock.json +1012 -1694
- package/assets/locks/frontend/package.json +5 -6
- package/assets/locks/go-backend/go.mod +17 -0
- package/assets/locks/go-backend/go.sum +159 -0
- package/assets/locks/node-backend/package-lock.json +1473 -2320
- package/assets/locks/node-backend/package.json +8 -7
- package/assets/locks/opentofu-azure/.terraform.lock.hcl +39 -0
- package/assets/locks/opentofu-azure/versions.tf +10 -0
- package/assets/locks/python-genai/function-requirements.txt +1734 -0
- package/assets/locks/python-genai/liftoff_template_python_genai.egg-info/PKG-INFO +25 -0
- package/assets/locks/python-genai/liftoff_template_python_genai.egg-info/SOURCES.txt +6 -0
- package/assets/locks/python-genai/liftoff_template_python_genai.egg-info/dependency_links.txt +1 -0
- package/assets/locks/python-genai/liftoff_template_python_genai.egg-info/requires.txt +23 -0
- package/assets/locks/python-genai/liftoff_template_python_genai.egg-info/top_level.txt +1 -0
- package/assets/locks/python-genai/pyproject.toml +42 -0
- package/assets/locks/python-genai/uv.lock +2536 -0
- package/assets/locks/python-standard/pyproject.toml +36 -0
- package/assets/locks/python-standard/uv.lock +1780 -0
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/catalog.json +4 -4
- package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/package-lock.json +667 -1108
- package/assets/supported-stack.json +711 -0
- package/dist/args.js +14 -0
- package/dist/args.js.map +1 -1
- package/dist/catalogs.d.ts +3 -1
- package/dist/catalogs.js +29 -7
- package/dist/catalogs.js.map +1 -1
- package/dist/cli.js +2 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands.d.ts +6 -0
- package/dist/commands.js +331 -41
- package/dist/commands.js.map +1 -1
- package/dist/container-validation.d.ts +2 -0
- package/dist/container-validation.js +26 -0
- package/dist/container-validation.js.map +1 -0
- package/dist/file-system.js +130 -5
- package/dist/file-system.js.map +1 -1
- package/dist/framework-adapters.d.ts +1 -1
- package/dist/framework-adapters.js +10 -3
- package/dist/framework-adapters.js.map +1 -1
- package/dist/framework-validation.d.ts +1 -0
- package/dist/framework-validation.js +26 -0
- package/dist/framework-validation.js.map +1 -1
- package/dist/go-template-assets.d.ts +2 -0
- package/dist/go-template-assets.js +14 -0
- package/dist/go-template-assets.js.map +1 -0
- package/dist/init-filesystem.js +5 -4
- package/dist/init-filesystem.js.map +1 -1
- package/dist/interactive.d.ts +13 -0
- package/dist/interactive.js +25 -7
- package/dist/interactive.js.map +1 -1
- package/dist/openspec-profile.d.ts +28 -0
- package/dist/openspec-profile.js +172 -0
- package/dist/openspec-profile.js.map +1 -0
- package/dist/opentofu-template-assets.d.ts +2 -0
- package/dist/opentofu-template-assets.js +10 -0
- package/dist/opentofu-template-assets.js.map +1 -0
- package/dist/package-identity.d.ts +8 -0
- package/dist/package-identity.js +16 -0
- package/dist/package-identity.js.map +1 -0
- package/dist/planner.js +61 -4
- package/dist/planner.js.map +1 -1
- package/dist/power-apps-templates.js +18 -0
- package/dist/power-apps-templates.js.map +1 -1
- package/dist/project-dependencies.d.ts +1 -1
- package/dist/project-dependencies.js +15 -34
- package/dist/project-dependencies.js.map +1 -1
- package/dist/published-verifier.js +11 -1
- package/dist/published-verifier.js.map +1 -1
- package/dist/python-template-assets.d.ts +4 -0
- package/dist/python-template-assets.js +34 -0
- package/dist/python-template-assets.js.map +1 -0
- package/dist/repository-governance.d.ts +17 -0
- package/dist/repository-governance.js +507 -0
- package/dist/repository-governance.js.map +1 -0
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +3 -1
- package/dist/runtime.js.map +1 -1
- package/dist/self-upgrade.d.ts +77 -0
- package/dist/self-upgrade.js +499 -0
- package/dist/self-upgrade.js.map +1 -0
- package/dist/stable-release.d.ts +18 -0
- package/dist/stable-release.js +66 -0
- package/dist/stable-release.js.map +1 -0
- package/dist/standard-templates.js +28 -97
- package/dist/standard-templates.js.map +1 -1
- package/dist/supported-stack.d.ts +115 -0
- package/dist/supported-stack.js +439 -0
- package/dist/supported-stack.js.map +1 -0
- package/dist/telemetry/contract.d.ts +1 -1
- package/dist/telemetry/contract.js +1 -0
- package/dist/telemetry/contract.js.map +1 -1
- package/dist/templates.js +238 -127
- package/dist/templates.js.map +1 -1
- package/dist/types.d.ts +21 -1
- package/dist/workstation-catalog.js +15 -12
- package/dist/workstation-catalog.js.map +1 -1
- package/dist/workstation.js +16 -6
- package/dist/workstation.js.map +1 -1
- package/docs/cli-reference.md +72 -2
- package/docs/configuration-and-manifests.md +13 -5
- package/docs/existing-repositories.md +44 -8
- package/docs/getting-started.md +34 -9
- package/docs/prerequisites.md +47 -4
- package/docs/project-structure.md +27 -1
- package/docs/repository-governance.md +91 -0
- package/docs/safety-and-consent.md +50 -13
- package/docs/spec-workflows-and-agents.md +61 -9
- package/docs/supported-stack.md +89 -0
- package/docs/telemetry.md +6 -0
- package/docs/troubleshooting.md +115 -0
- package/docs/workloads.md +14 -5
- package/package.json +14 -6
package/dist/templates.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { addGenAiExtensionArtifacts } from './genai-templates.js';
|
|
3
|
+
import { assertImmutableGeneratedContainerReferences } from './container-validation.js';
|
|
3
4
|
import { addPowerAppsCodeAppArtifacts } from './power-apps-templates.js';
|
|
4
5
|
import { addStandardStackArtifacts, renderStandardDockerfile, renderStandardEnv } from './standard-templates.js';
|
|
5
6
|
import { liftoffVersion } from './version.js';
|
|
6
7
|
import { renderNpmLock, renderNpmPackage } from './npm-template-assets.js';
|
|
8
|
+
import { renderOpenTofuProviderLock, renderOpenTofuVersions } from './opentofu-template-assets.js';
|
|
7
9
|
import { formatCommand } from './process-runner.js';
|
|
10
|
+
import { formatContainerImage, supportedStack } from './supported-stack.js';
|
|
11
|
+
import { buildRepositoryGovernanceArtifacts, governancePolicyVersion } from './repository-governance.js';
|
|
12
|
+
import { OPEN_SPEC_DELIVERY, OPEN_SPEC_PROFILE, OPEN_SPEC_WORKFLOW_IDS } from './openspec-profile.js';
|
|
13
|
+
import { renderFunctionRequirementsAsset, renderPythonLock, renderPythonPyprojectAsset } from './python-template-assets.js';
|
|
8
14
|
import { workstationRequirementCatalog } from './workstation-catalog.js';
|
|
9
15
|
const contentHash = (content) => `sha256:${createHash('sha256').update(content, 'utf8').digest('hex')}`;
|
|
10
16
|
const DEFAULT_FUNCTION_WORKER_QUEUE_NAME = 'events';
|
|
@@ -77,10 +83,14 @@ export function buildArtifacts(plan) {
|
|
|
77
83
|
addPowerAppsWorkloadArtifacts(add, plan);
|
|
78
84
|
break;
|
|
79
85
|
}
|
|
80
|
-
|
|
86
|
+
addSpecWorkflowArtifacts(add, plan);
|
|
87
|
+
for (const artifact of buildRepositoryGovernanceArtifacts(plan)) {
|
|
88
|
+
add(artifact.logicalName, artifact.category, artifact.pathParts, artifact.content);
|
|
89
|
+
}
|
|
81
90
|
if (plan.workload !== 'power-apps-code-app' && plan.includeFrontend) {
|
|
82
91
|
addFrontendArtifacts(add, plan);
|
|
83
92
|
}
|
|
93
|
+
assertImmutableGeneratedContainerReferences(artifacts);
|
|
84
94
|
const manifest = buildManifest(plan, artifacts);
|
|
85
95
|
artifacts.push({
|
|
86
96
|
logicalName: 'manifest',
|
|
@@ -154,7 +164,7 @@ export function buildManifest(plan, artifacts, options = {}) {
|
|
|
154
164
|
environments: plan.environments.map((environment) => environment.id)
|
|
155
165
|
};
|
|
156
166
|
return {
|
|
157
|
-
artifactVersion:
|
|
167
|
+
artifactVersion: 5,
|
|
158
168
|
generatedBy: 'Mission Control Liftoff',
|
|
159
169
|
liftoffVersion,
|
|
160
170
|
project: {
|
|
@@ -169,6 +179,16 @@ export function buildManifest(plan, artifacts, options = {}) {
|
|
|
169
179
|
adapter: plan.framework.id,
|
|
170
180
|
...(frameworkState === 'initialized' ? { contractVersion: plan.framework.version } : {})
|
|
171
181
|
},
|
|
182
|
+
governance: plan.governanceProfile.id === 'none'
|
|
183
|
+
? {
|
|
184
|
+
profile: 'none',
|
|
185
|
+
state: 'disabled'
|
|
186
|
+
}
|
|
187
|
+
: {
|
|
188
|
+
profile: plan.governanceProfile.id,
|
|
189
|
+
policyVersion: governancePolicyVersion,
|
|
190
|
+
state: 'handoff-generated'
|
|
191
|
+
},
|
|
172
192
|
artifacts: artifacts
|
|
173
193
|
.filter((artifact) => artifact.category !== 'seed' && artifact.category !== 'framework')
|
|
174
194
|
.map((artifact) => ({
|
|
@@ -193,7 +213,8 @@ function addBaseArtifacts(add, plan) {
|
|
|
193
213
|
environments: plan.environments.map((environment) => environment.id),
|
|
194
214
|
specWorkflow: plan.specWorkflow.id,
|
|
195
215
|
agents: plan.agents.map((agent) => agent.id),
|
|
196
|
-
...(plan.defaultAgent ? { defaultAgent: plan.defaultAgent.id } : {})
|
|
216
|
+
...(plan.defaultAgent ? { defaultAgent: plan.defaultAgent.id } : {}),
|
|
217
|
+
governanceProfile: plan.governanceProfile.id
|
|
197
218
|
}, null, 2));
|
|
198
219
|
add('env-example', 'configuration', ['.env.example'], renderEnvExample(plan));
|
|
199
220
|
add('backend-dockerfile', 'runtime', ['Dockerfile'], plan.workload === 'genai' ? renderBackendDockerfile() : renderStandardDockerfile(plan));
|
|
@@ -205,12 +226,14 @@ function addPowerAppsProjectConfig(add, plan) {
|
|
|
205
226
|
specWorkflow: plan.specWorkflow.id,
|
|
206
227
|
agents: plan.agents.map((agent) => agent.id),
|
|
207
228
|
...(plan.defaultAgent ? { defaultAgent: plan.defaultAgent.id } : {}),
|
|
208
|
-
codeAppsPlugin: plan.codeAppsPlugin
|
|
229
|
+
codeAppsPlugin: plan.codeAppsPlugin,
|
|
230
|
+
governanceProfile: plan.governanceProfile.id
|
|
209
231
|
}, null, 2));
|
|
210
232
|
}
|
|
211
233
|
function addBackendArtifacts(add, plan) {
|
|
212
234
|
const routeModule = pyModule(genAiPattern(plan).id);
|
|
213
235
|
add('backend-pyproject', 'backend', ['backend', 'pyproject.toml'], renderBackendPyproject(plan));
|
|
236
|
+
add('backend-uv-lock', 'backend', ['backend', 'uv.lock'], renderPythonLock('genai', `${plan.safeProjectName}-backend`));
|
|
214
237
|
add('backend-package', 'backend', ['backend', '__init__.py'], '');
|
|
215
238
|
add('backend-api-package', 'backend', ['backend', 'apis', '__init__.py'], '');
|
|
216
239
|
add('backend-main', 'backend', ['backend', 'apis', 'main.py'], renderFastApiMain(plan, routeModule));
|
|
@@ -285,7 +308,8 @@ function addDockerArtifacts(add, plan) {
|
|
|
285
308
|
}
|
|
286
309
|
function addInfrastructureArtifacts(add, plan) {
|
|
287
310
|
const base = ['infrastructure', 'opentofu', 'azure'];
|
|
288
|
-
add('opentofu-versions', 'infrastructure', [...base, 'versions.tf'],
|
|
311
|
+
add('opentofu-versions', 'infrastructure', [...base, 'versions.tf'], renderOpenTofuVersions());
|
|
312
|
+
add('opentofu-provider-lock', 'infrastructure', [...base, '.terraform.lock.hcl'], renderOpenTofuProviderLock());
|
|
289
313
|
add('opentofu-providers', 'infrastructure', [...base, 'providers.tf'], renderTofuProviders());
|
|
290
314
|
add('opentofu-variables', 'infrastructure', [...base, 'variables.tf'], renderTofuVariables(plan));
|
|
291
315
|
add('opentofu-main', 'infrastructure', [...base, 'main.tf'], renderTofuMain(plan));
|
|
@@ -297,7 +321,7 @@ function addInfrastructureArtifacts(add, plan) {
|
|
|
297
321
|
add(`opentofu-${environment.id}-tfvars`, 'infrastructure', [...base, 'environments', `${environment.id}.tfvars`], renderTofuTfvars(plan, environment.id));
|
|
298
322
|
}
|
|
299
323
|
}
|
|
300
|
-
function
|
|
324
|
+
function addSpecWorkflowArtifacts(add, plan) {
|
|
301
325
|
if (plan.workload === 'power-apps-code-app') {
|
|
302
326
|
if (plan.specWorkflow.id === 'openspec') {
|
|
303
327
|
const changeName = `bootstrap-${plan.safeProjectName}`;
|
|
@@ -333,7 +357,7 @@ function addGovernanceArtifacts(add, plan) {
|
|
|
333
357
|
}
|
|
334
358
|
}
|
|
335
359
|
function renderPowerAppsOpenSpecConfig(plan) {
|
|
336
|
-
return `schema: spec-driven
|
|
360
|
+
return `schema: spec-driven${renderOpenSpecCopilotConfig(plan)}
|
|
337
361
|
context: |
|
|
338
362
|
Project: ${plan.projectName}
|
|
339
363
|
Workload: Microsoft Power Apps code app
|
|
@@ -351,6 +375,14 @@ rules:
|
|
|
351
375
|
- Include npm lint and build verification.
|
|
352
376
|
`;
|
|
353
377
|
}
|
|
378
|
+
function renderOpenSpecCopilotConfig(plan) {
|
|
379
|
+
return plan.agents.some((agent) => agent.id === 'github-copilot')
|
|
380
|
+
? `
|
|
381
|
+
githubCopilot:
|
|
382
|
+
cloudAgent: ${plan.copilotCloud}
|
|
383
|
+
`
|
|
384
|
+
: '\n';
|
|
385
|
+
}
|
|
354
386
|
function renderPowerAppsSeedProposal(plan) {
|
|
355
387
|
return `# Proposal: bootstrap-${plan.safeProjectName}
|
|
356
388
|
|
|
@@ -425,10 +457,11 @@ function addFrontendArtifacts(add, plan) {
|
|
|
425
457
|
function renderDirectBuildAndTestGuide(plan) {
|
|
426
458
|
let backendCommands;
|
|
427
459
|
if (plan.workload === 'genai' || plan.apiStack.id === 'python-fastapi') {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
460
|
+
const extras = plan.workload === 'genai' && hasFunctionWorker(plan)
|
|
461
|
+
? ' --extra functions'
|
|
462
|
+
: '';
|
|
463
|
+
backendCommands = `uv sync --frozen --project backend --extra test${extras}
|
|
464
|
+
uv run --project backend python -m pytest -q backend/tests`;
|
|
432
465
|
}
|
|
433
466
|
else if (plan.apiStack.id === 'node-fastify') {
|
|
434
467
|
backendCommands = `cd backend
|
|
@@ -452,24 +485,12 @@ npm ci
|
|
|
452
485
|
npm run build
|
|
453
486
|
\`\`\`
|
|
454
487
|
` : '';
|
|
455
|
-
const windowsPythonCommands = plan.workload === 'genai' || plan.apiStack.id === 'python-fastapi'
|
|
456
|
-
? `
|
|
457
|
-
|
|
458
|
-
Equivalent Windows dependency commands:
|
|
459
|
-
|
|
460
|
-
\`\`\`powershell
|
|
461
|
-
py -3 -m venv .venv
|
|
462
|
-
.venv\\Scripts\\python.exe -m pip install -e "./backend[test]"
|
|
463
|
-
\`\`\`
|
|
464
|
-
`
|
|
465
|
-
: '';
|
|
466
488
|
const functionCommands = plan.workload === 'genai' && hasFunctionWorker(plan) ? `
|
|
467
489
|
|
|
468
|
-
Run the Function worker unit tests from the same Python
|
|
490
|
+
Run the Function worker unit tests from the same locked Python environment:
|
|
469
491
|
|
|
470
492
|
\`\`\`bash
|
|
471
|
-
|
|
472
|
-
(cd functions/${functionWorkerName(plan)} && ../../.venv/bin/python -m pytest -q)
|
|
493
|
+
(cd functions/${functionWorkerName(plan)} && uv run --project ../../backend --directory . python -m pytest -q)
|
|
473
494
|
\`\`\`
|
|
474
495
|
` : '';
|
|
475
496
|
return `## Project Dependencies, Build, And Test
|
|
@@ -480,7 +501,7 @@ Run the Function worker unit tests from the same Python virtual environment:
|
|
|
480
501
|
${backendCommands}
|
|
481
502
|
\`\`\`
|
|
482
503
|
|
|
483
|
-
${
|
|
504
|
+
${frontendCommands}${functionCommands}`;
|
|
484
505
|
}
|
|
485
506
|
function renderAdvisoryReadinessGuide(plan) {
|
|
486
507
|
const selected = [
|
|
@@ -508,12 +529,21 @@ function renderAdvisoryReadinessGuide(plan) {
|
|
|
508
529
|
function renderSpecWorkflowGuide(plan) {
|
|
509
530
|
const agents = plan.agents.map((agent) => `${agent.label}${plan.defaultAgent?.id === agent.id ? ' (default integration)' : ''}`).join(', ');
|
|
510
531
|
const ownership = plan.specWorkflow.id === 'openspec'
|
|
511
|
-
? 'OpenSpec
|
|
532
|
+
? 'OpenSpec workflow skills, commands, configuration, and optional cloud-agent files'
|
|
512
533
|
: 'Spec Kit core files, integration state, and the selected Copilot or Claude skill integrations';
|
|
534
|
+
const openSpecDetails = plan.specWorkflow.id === 'openspec'
|
|
535
|
+
? [
|
|
536
|
+
`- OpenSpec profile: ${OPEN_SPEC_PROFILE}; delivery: ${OPEN_SPEC_DELIVERY}; workflows: ${OPEN_SPEC_WORKFLOW_IDS.join(', ')}`,
|
|
537
|
+
...(plan.agents.some((agent) => agent.id === 'github-copilot')
|
|
538
|
+
? ['- GitHub Copilot cloud agent: controlled by `githubCopilot.cloudAgent` in `openspec/config.yaml`']
|
|
539
|
+
: [])
|
|
540
|
+
].join('\n')
|
|
541
|
+
: '';
|
|
513
542
|
return `## Spec-Driven Workflow And Validation
|
|
514
543
|
|
|
515
544
|
- Workflow: ${plan.specWorkflow.label} ${plan.framework.version}
|
|
516
545
|
- AI coding agents: ${agents}
|
|
546
|
+
${openSpecDetails ? `${openSpecDetails}\n` : ''}
|
|
517
547
|
- Framework ownership: the official initializer owns ${ownership}. Liftoff validates these files but excludes framework-owned output and one-time seed content from durable manifest hashes.
|
|
518
548
|
- Deferred tools: advisory workstation checks may be deferred. Liftoff never claims they are installed and never installs them without \`--install-tools\`.
|
|
519
549
|
|
|
@@ -527,6 +557,8 @@ Validate the scaffold and workstation after setup:
|
|
|
527
557
|
liftoff validate
|
|
528
558
|
liftoff doctor
|
|
529
559
|
\`\`\`
|
|
560
|
+
|
|
561
|
+
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.
|
|
530
562
|
`;
|
|
531
563
|
}
|
|
532
564
|
function renderGeneratedConfigurationGuide(plan) {
|
|
@@ -553,6 +585,10 @@ Copy \`.env.example\` to \`.env\`, then configure only the integrations you use:
|
|
|
553
585
|
function renderGeneratedUpdateGuide() {
|
|
554
586
|
return `## Safe Liftoff Updates
|
|
555
587
|
|
|
588
|
+
\`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\`.
|
|
589
|
+
|
|
590
|
+
\`single-maintainer-gitflow\` repository governance generates a local handoff only. Review \`.liftoff/governance/README.md\`, commit and push the project, then run a selected-agent launcher for read-only Phase 0. Live enforcement requires a separately approved governance change.
|
|
591
|
+
|
|
556
592
|
\`liftoff update\` immediately applies safe managed changes without prompting in interactive, redirected, and automated environments. Use \`liftoff update --check\` for a read-only human drift report, or \`liftoff update --check --json\` as an automation gate that exits 0 when clean and 2 when drift exists.
|
|
557
593
|
|
|
558
594
|
Local or user-owned conflicts are skipped by default. Review the reported paths, commit or copy local work, and use \`liftoff update --force\` only when every listed overwrite is intended. An occupied destination with different user bytes remains preserved without force, while an identical destination is adopted without rewriting it. Update never deletes orphans or installs dependencies. A failed transaction is rolled back, but Liftoff retains no backup after a successful overwrite.
|
|
@@ -694,16 +730,26 @@ LANGFUSE_SECRET_KEY=
|
|
|
694
730
|
`;
|
|
695
731
|
}
|
|
696
732
|
function renderBackendDockerfile() {
|
|
697
|
-
return `FROM
|
|
733
|
+
return `FROM ${formatContainerImage(supportedStack.containers['uv-tool'])} AS uv
|
|
734
|
+
FROM ${formatContainerImage(supportedStack.containers['python-runtime'])}
|
|
698
735
|
|
|
699
736
|
WORKDIR /app
|
|
700
737
|
|
|
701
738
|
ENV PYTHONDONTWRITEBYTECODE=1
|
|
702
739
|
ENV PYTHONUNBUFFERED=1
|
|
703
740
|
ENV PYTHONPATH=/app
|
|
741
|
+
ENV PATH=/app/backend/.venv/bin:$PATH
|
|
742
|
+
COPY --from=uv /uv /uvx /bin/
|
|
704
743
|
|
|
744
|
+
ARG UV_DEFAULT_INDEX=https://pypi.org/simple
|
|
705
745
|
COPY backend/pyproject.toml /app/backend/pyproject.toml
|
|
706
|
-
|
|
746
|
+
COPY backend/uv.lock /app/backend/uv.lock
|
|
747
|
+
RUN --mount=type=cache,target=/root/.cache/uv \\
|
|
748
|
+
uv export --frozen --no-dev --no-emit-project --project /app/backend --output-file /tmp/requirements.txt \\
|
|
749
|
+
&& uv venv /app/backend/.venv \\
|
|
750
|
+
&& uv pip install --python /app/backend/.venv/bin/python --require-hashes \\
|
|
751
|
+
--default-index "$UV_DEFAULT_INDEX" --requirements /tmp/requirements.txt \\
|
|
752
|
+
&& rm /tmp/requirements.txt
|
|
707
753
|
|
|
708
754
|
COPY backend /app/backend
|
|
709
755
|
COPY database /app/database
|
|
@@ -713,43 +759,7 @@ CMD ["uvicorn", "backend.apis.main:app", "--host", "0.0.0.0", "--port", "8000"]
|
|
|
713
759
|
`;
|
|
714
760
|
}
|
|
715
761
|
function renderBackendPyproject(plan) {
|
|
716
|
-
return `
|
|
717
|
-
name = "${plan.safeProjectName}-backend"
|
|
718
|
-
version = "0.1.0"
|
|
719
|
-
requires-python = ">=3.12"
|
|
720
|
-
dependencies = [
|
|
721
|
-
"fastapi>=0.111",
|
|
722
|
-
"uvicorn[standard]>=0.30",
|
|
723
|
-
"pydantic>=2.7",
|
|
724
|
-
"pydantic-settings>=2.3",
|
|
725
|
-
"pydantic-ai-slim[openai]==1.107.1",
|
|
726
|
-
"scalar-fastapi>=1.0",
|
|
727
|
-
"sqlalchemy[asyncio]>=2.0",
|
|
728
|
-
"asyncpg>=0.29",
|
|
729
|
-
"psycopg[binary]>=3.2",
|
|
730
|
-
"alembic>=1.13",
|
|
731
|
-
"redis>=5.0",
|
|
732
|
-
"langfuse==2.60.10",
|
|
733
|
-
"azure-servicebus>=7.12",
|
|
734
|
-
"azure-identity>=1.17",
|
|
735
|
-
"azure-storage-blob>=12.20",
|
|
736
|
-
"azure-communication-email>=1.0"
|
|
737
|
-
]
|
|
738
|
-
|
|
739
|
-
[project.optional-dependencies]
|
|
740
|
-
test = ["pytest>=8.2", "httpx>=0.27"]
|
|
741
|
-
|
|
742
|
-
[build-system]
|
|
743
|
-
requires = ["setuptools>=70"]
|
|
744
|
-
build-backend = "setuptools.build_meta"
|
|
745
|
-
|
|
746
|
-
[tool.setuptools]
|
|
747
|
-
packages = []
|
|
748
|
-
|
|
749
|
-
[tool.pytest.ini_options]
|
|
750
|
-
pythonpath = [".."]
|
|
751
|
-
testpaths = ["tests"]
|
|
752
|
-
`;
|
|
762
|
+
return renderPythonPyprojectAsset('genai', `${plan.safeProjectName}-backend`);
|
|
753
763
|
}
|
|
754
764
|
function renderFastApiMain(plan, routeModule) {
|
|
755
765
|
return `from fastapi import FastAPI
|
|
@@ -1136,8 +1146,12 @@ class LangfuseTracer:
|
|
|
1136
1146
|
|
|
1137
1147
|
@asynccontextmanager
|
|
1138
1148
|
async def trace(self, name: str, input_data: Any = None):
|
|
1139
|
-
remote_trace = self._client.
|
|
1140
|
-
|
|
1149
|
+
remote_trace = self._client.start_observation(
|
|
1150
|
+
name=name,
|
|
1151
|
+
as_type="span",
|
|
1152
|
+
input=input_data,
|
|
1153
|
+
)
|
|
1154
|
+
remote_id = getattr(remote_trace, "trace_id", None)
|
|
1141
1155
|
handle = TraceHandle(
|
|
1142
1156
|
enabled=True,
|
|
1143
1157
|
trace_id=str(remote_id) if remote_id is not None else None,
|
|
@@ -1149,6 +1163,8 @@ class LangfuseTracer:
|
|
|
1149
1163
|
raise
|
|
1150
1164
|
else:
|
|
1151
1165
|
remote_trace.update(output=handle.output)
|
|
1166
|
+
finally:
|
|
1167
|
+
remote_trace.end()
|
|
1152
1168
|
|
|
1153
1169
|
|
|
1154
1170
|
def build_tracer(
|
|
@@ -1293,21 +1309,25 @@ from backend.observability.tracing import (
|
|
|
1293
1309
|
|
|
1294
1310
|
|
|
1295
1311
|
class FakeRemoteTrace:
|
|
1296
|
-
|
|
1312
|
+
trace_id = "trace-123"
|
|
1297
1313
|
|
|
1298
1314
|
def __init__(self):
|
|
1299
1315
|
self.updates = []
|
|
1316
|
+
self.ended = False
|
|
1300
1317
|
|
|
1301
1318
|
def update(self, **values):
|
|
1302
1319
|
self.updates.append(values)
|
|
1303
1320
|
|
|
1321
|
+
def end(self):
|
|
1322
|
+
self.ended = True
|
|
1323
|
+
|
|
1304
1324
|
|
|
1305
1325
|
class FakeLangfuse:
|
|
1306
1326
|
def __init__(self):
|
|
1307
1327
|
self.calls = []
|
|
1308
1328
|
self.remote_trace = FakeRemoteTrace()
|
|
1309
1329
|
|
|
1310
|
-
def
|
|
1330
|
+
def start_observation(self, **values):
|
|
1311
1331
|
self.calls.append(values)
|
|
1312
1332
|
return self.remote_trace
|
|
1313
1333
|
|
|
@@ -1337,8 +1357,11 @@ def test_configured_tracing_updates_langfuse_operation():
|
|
|
1337
1357
|
trace.set_output({"answer": "world"})
|
|
1338
1358
|
|
|
1339
1359
|
asyncio.run(scenario())
|
|
1340
|
-
assert client.calls == [
|
|
1360
|
+
assert client.calls == [
|
|
1361
|
+
{"name": "agent.run", "as_type": "span", "input": {"prompt": "hello"}}
|
|
1362
|
+
]
|
|
1341
1363
|
assert client.remote_trace.updates == [{"output": {"answer": "world"}}]
|
|
1364
|
+
assert client.remote_trace.ended is True
|
|
1342
1365
|
|
|
1343
1366
|
|
|
1344
1367
|
def test_partial_langfuse_configuration_fails(monkeypatch):
|
|
@@ -1712,15 +1735,11 @@ Deployed triggers use \`ServiceBusConnection__fullyQualifiedNamespace\` and \`Se
|
|
|
1712
1735
|
## Local Development
|
|
1713
1736
|
|
|
1714
1737
|
\`\`\`bash
|
|
1715
|
-
|
|
1716
|
-
source .venv/bin/activate
|
|
1717
|
-
pip install -r requirements.txt
|
|
1738
|
+
uv sync --frozen --project ../../backend --extra test --extra functions
|
|
1718
1739
|
cp local.settings.example.json local.settings.json
|
|
1719
|
-
python -m pytest -q
|
|
1740
|
+
uv run --project ../../backend --directory . python -m pytest -q
|
|
1720
1741
|
func start
|
|
1721
1742
|
\`\`\`
|
|
1722
|
-
|
|
1723
|
-
On Windows, activate the virtual environment with \`.venv\\Scripts\\activate\`.
|
|
1724
1743
|
`;
|
|
1725
1744
|
}
|
|
1726
1745
|
function renderFunctionHostJson() {
|
|
@@ -1747,9 +1766,7 @@ function renderFunctionLocalSettings(plan) {
|
|
|
1747
1766
|
}, null, 2);
|
|
1748
1767
|
}
|
|
1749
1768
|
function renderFunctionRequirements() {
|
|
1750
|
-
return
|
|
1751
|
-
pytest>=8.2
|
|
1752
|
-
`;
|
|
1769
|
+
return renderFunctionRequirementsAsset();
|
|
1753
1770
|
}
|
|
1754
1771
|
function renderFunctionApp(plan) {
|
|
1755
1772
|
const pattern = genAiPattern(plan);
|
|
@@ -1861,7 +1878,9 @@ function renderDockerCompose(plan) {
|
|
|
1861
1878
|
depends_on:
|
|
1862
1879
|
- backend
|
|
1863
1880
|
` : '';
|
|
1864
|
-
const postgresImage = plan.workload === 'genai'
|
|
1881
|
+
const postgresImage = plan.workload === 'genai'
|
|
1882
|
+
? formatContainerImage(supportedStack.containers.pgvector)
|
|
1883
|
+
: formatContainerImage(supportedStack.containers.postgres);
|
|
1865
1884
|
return `services:
|
|
1866
1885
|
backend:
|
|
1867
1886
|
build:
|
|
@@ -1888,46 +1907,136 @@ ${frontendService}
|
|
|
1888
1907
|
POSTGRES_DB: ${plan.safeProjectName.replace(/-/g, '_')}
|
|
1889
1908
|
ports:
|
|
1890
1909
|
- "5432:5432"
|
|
1910
|
+
healthcheck:
|
|
1911
|
+
test: ["CMD-SHELL", "pg_isready -U postgres"]
|
|
1912
|
+
interval: 3s
|
|
1913
|
+
timeout: 3s
|
|
1914
|
+
retries: 10
|
|
1891
1915
|
|
|
1892
1916
|
redis:
|
|
1893
|
-
image: redis
|
|
1917
|
+
image: ${formatContainerImage(supportedStack.containers.redis)}
|
|
1894
1918
|
ports:
|
|
1895
1919
|
- "6379:6379"
|
|
1896
1920
|
|
|
1897
1921
|
azurite:
|
|
1898
|
-
image:
|
|
1922
|
+
image: ${formatContainerImage(supportedStack.containers.azurite)}
|
|
1899
1923
|
command: azurite --blobHost 0.0.0.0
|
|
1900
1924
|
ports:
|
|
1901
1925
|
- "10000:10000"
|
|
1902
1926
|
|
|
1903
1927
|
mailpit:
|
|
1904
|
-
image:
|
|
1928
|
+
image: ${formatContainerImage(supportedStack.containers.mailpit)}
|
|
1905
1929
|
ports:
|
|
1906
1930
|
- "8025:8025"
|
|
1907
1931
|
|
|
1908
|
-
${plan.workload === 'genai' ? ` langfuse:
|
|
1909
|
-
image: langfuse
|
|
1932
|
+
${plan.workload === 'genai' ? ` langfuse-worker:
|
|
1933
|
+
image: ${formatContainerImage(supportedStack.containers['langfuse-worker'])}
|
|
1910
1934
|
profiles:
|
|
1911
1935
|
- observability
|
|
1912
|
-
|
|
1936
|
+
depends_on: &langfuse-dependencies
|
|
1937
|
+
postgres:
|
|
1938
|
+
condition: service_healthy
|
|
1939
|
+
langfuse-redis:
|
|
1940
|
+
condition: service_healthy
|
|
1941
|
+
clickhouse:
|
|
1942
|
+
condition: service_healthy
|
|
1943
|
+
minio:
|
|
1944
|
+
condition: service_healthy
|
|
1945
|
+
environment: &langfuse-environment
|
|
1913
1946
|
DATABASE_URL: postgresql://postgres:postgres@postgres:5432/${plan.safeProjectName.replace(/-/g, '_')}
|
|
1914
|
-
|
|
1915
|
-
SALT: local-development-
|
|
1947
|
+
NEXTAUTH_URL: http://localhost:3000
|
|
1948
|
+
SALT: local-development-salt
|
|
1949
|
+
ENCRYPTION_KEY: 0000000000000000000000000000000000000000000000000000000000000000
|
|
1950
|
+
TELEMETRY_ENABLED: "false"
|
|
1951
|
+
CLICKHOUSE_MIGRATION_URL: clickhouse://clickhouse:9000
|
|
1952
|
+
CLICKHOUSE_URL: http://clickhouse:8123
|
|
1953
|
+
CLICKHOUSE_USER: clickhouse
|
|
1954
|
+
CLICKHOUSE_PASSWORD: clickhouse
|
|
1955
|
+
REDIS_HOST: langfuse-redis
|
|
1956
|
+
REDIS_PORT: 6379
|
|
1957
|
+
REDIS_AUTH: langfuse-redis
|
|
1958
|
+
LANGFUSE_S3_EVENT_UPLOAD_BUCKET: langfuse
|
|
1959
|
+
LANGFUSE_S3_EVENT_UPLOAD_REGION: auto
|
|
1960
|
+
LANGFUSE_S3_EVENT_UPLOAD_ACCESS_KEY_ID: minio
|
|
1961
|
+
LANGFUSE_S3_EVENT_UPLOAD_SECRET_ACCESS_KEY: miniosecret
|
|
1962
|
+
LANGFUSE_S3_EVENT_UPLOAD_ENDPOINT: http://minio:9000
|
|
1963
|
+
LANGFUSE_S3_EVENT_UPLOAD_FORCE_PATH_STYLE: "true"
|
|
1964
|
+
LANGFUSE_S3_MEDIA_UPLOAD_BUCKET: langfuse
|
|
1965
|
+
LANGFUSE_S3_MEDIA_UPLOAD_REGION: auto
|
|
1966
|
+
LANGFUSE_S3_MEDIA_UPLOAD_ACCESS_KEY_ID: minio
|
|
1967
|
+
LANGFUSE_S3_MEDIA_UPLOAD_SECRET_ACCESS_KEY: miniosecret
|
|
1968
|
+
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: http://minio:9000
|
|
1969
|
+
LANGFUSE_S3_MEDIA_UPLOAD_FORCE_PATH_STYLE: "true"
|
|
1970
|
+
|
|
1971
|
+
langfuse:
|
|
1972
|
+
image: ${formatContainerImage(supportedStack.containers['langfuse-web'])}
|
|
1973
|
+
profiles:
|
|
1974
|
+
- observability
|
|
1975
|
+
depends_on: *langfuse-dependencies
|
|
1976
|
+
environment:
|
|
1977
|
+
<<: *langfuse-environment
|
|
1978
|
+
NEXTAUTH_SECRET: local-development-secret
|
|
1979
|
+
LANGFUSE_S3_MEDIA_UPLOAD_ENDPOINT: http://localhost:9090
|
|
1916
1980
|
ports:
|
|
1917
1981
|
- "3000:3000"
|
|
1982
|
+
|
|
1983
|
+
clickhouse:
|
|
1984
|
+
image: ${formatContainerImage(supportedStack.containers.clickhouse)}
|
|
1985
|
+
profiles:
|
|
1986
|
+
- observability
|
|
1987
|
+
user: "101:101"
|
|
1988
|
+
environment:
|
|
1989
|
+
CLICKHOUSE_DB: default
|
|
1990
|
+
CLICKHOUSE_USER: clickhouse
|
|
1991
|
+
CLICKHOUSE_PASSWORD: clickhouse
|
|
1992
|
+
volumes:
|
|
1993
|
+
- langfuse-clickhouse-data:/var/lib/clickhouse
|
|
1994
|
+
- langfuse-clickhouse-logs:/var/log/clickhouse-server
|
|
1995
|
+
healthcheck:
|
|
1996
|
+
test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:8123/ping || exit 1"]
|
|
1997
|
+
interval: 5s
|
|
1998
|
+
timeout: 5s
|
|
1999
|
+
retries: 10
|
|
2000
|
+
|
|
2001
|
+
minio:
|
|
2002
|
+
image: ${formatContainerImage(supportedStack.containers.minio)}
|
|
2003
|
+
profiles:
|
|
2004
|
+
- observability
|
|
2005
|
+
entrypoint: sh
|
|
2006
|
+
command: -c 'mkdir -p /data/langfuse && minio server --address ":9000" --console-address ":9001" /data'
|
|
2007
|
+
environment:
|
|
2008
|
+
MINIO_ROOT_USER: minio
|
|
2009
|
+
MINIO_ROOT_PASSWORD: miniosecret
|
|
2010
|
+
ports:
|
|
2011
|
+
- "9090:9000"
|
|
2012
|
+
- "127.0.0.1:9091:9001"
|
|
2013
|
+
volumes:
|
|
2014
|
+
- langfuse-minio-data:/data
|
|
2015
|
+
healthcheck:
|
|
2016
|
+
test: ["CMD", "mc", "ready", "local"]
|
|
2017
|
+
interval: 1s
|
|
2018
|
+
timeout: 5s
|
|
2019
|
+
retries: 5
|
|
2020
|
+
|
|
2021
|
+
langfuse-redis:
|
|
2022
|
+
image: ${formatContainerImage(supportedStack.containers.redis)}
|
|
2023
|
+
profiles:
|
|
2024
|
+
- observability
|
|
2025
|
+
command: ["redis-server", "--requirepass", "langfuse-redis", "--maxmemory-policy", "noeviction"]
|
|
2026
|
+
volumes:
|
|
2027
|
+
- langfuse-redis-data:/data
|
|
2028
|
+
healthcheck:
|
|
2029
|
+
test: ["CMD", "redis-cli", "-a", "langfuse-redis", "ping"]
|
|
2030
|
+
interval: 3s
|
|
2031
|
+
timeout: 10s
|
|
2032
|
+
retries: 10
|
|
2033
|
+
` : ''}
|
|
2034
|
+
${plan.workload === 'genai' ? `volumes:
|
|
2035
|
+
langfuse-clickhouse-data:
|
|
2036
|
+
langfuse-clickhouse-logs:
|
|
2037
|
+
langfuse-minio-data:
|
|
2038
|
+
langfuse-redis-data:
|
|
1918
2039
|
` : ''}
|
|
1919
|
-
`;
|
|
1920
|
-
}
|
|
1921
|
-
function renderTofuVersions() {
|
|
1922
|
-
return `terraform {
|
|
1923
|
-
required_version = ">= 1.6.0"
|
|
1924
|
-
required_providers {
|
|
1925
|
-
azurerm = {
|
|
1926
|
-
source = "hashicorp/azurerm"
|
|
1927
|
-
version = "~> 3.110"
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
}
|
|
1931
2040
|
`;
|
|
1932
2041
|
}
|
|
1933
2042
|
function renderTofuProviders() {
|
|
@@ -1940,7 +2049,7 @@ function renderTofuVariables(plan) {
|
|
|
1940
2049
|
const frontendVariables = plan.includeFrontend ? `
|
|
1941
2050
|
variable "frontend_image" {
|
|
1942
2051
|
type = string
|
|
1943
|
-
default = "
|
|
2052
|
+
default = "${formatContainerImage(supportedStack.containers['container-apps-bootstrap'])}"
|
|
1944
2053
|
description = "Frontend image. Replace the bootstrap image with the generated frontend image after pushing it to ACR."
|
|
1945
2054
|
}
|
|
1946
2055
|
` : '';
|
|
@@ -1953,7 +2062,7 @@ variable "function_worker_queue_name" {
|
|
|
1953
2062
|
|
|
1954
2063
|
variable "functions_python_version" {
|
|
1955
2064
|
type = string
|
|
1956
|
-
default = "
|
|
2065
|
+
default = "${supportedStack.runtimes.python.releaseLine}"
|
|
1957
2066
|
description = "Python runtime version for the generated Azure Functions worker."
|
|
1958
2067
|
}
|
|
1959
2068
|
` : '';
|
|
@@ -1980,7 +2089,7 @@ variable "resource_suffix" {
|
|
|
1980
2089
|
|
|
1981
2090
|
variable "backend_image" {
|
|
1982
2091
|
type = string
|
|
1983
|
-
default = "
|
|
2092
|
+
default = "${formatContainerImage(supportedStack.containers['container-apps-bootstrap'])}"
|
|
1984
2093
|
description = "Backend image. Replace the bootstrap image with the generated backend image after pushing it to ACR."
|
|
1985
2094
|
}
|
|
1986
2095
|
|
|
@@ -2281,7 +2390,7 @@ resource "azurerm_storage_account" "main" {
|
|
|
2281
2390
|
|
|
2282
2391
|
resource "azurerm_storage_container" "documents" {
|
|
2283
2392
|
name = "documents"
|
|
2284
|
-
|
|
2393
|
+
storage_account_id = azurerm_storage_account.main.id
|
|
2285
2394
|
container_access_type = "private"
|
|
2286
2395
|
}
|
|
2287
2396
|
|
|
@@ -2305,11 +2414,12 @@ resource "azurerm_communication_service" "main" {
|
|
|
2305
2414
|
}
|
|
2306
2415
|
|
|
2307
2416
|
resource "azurerm_key_vault" "main" {
|
|
2308
|
-
name
|
|
2309
|
-
location
|
|
2310
|
-
resource_group_name
|
|
2311
|
-
tenant_id
|
|
2312
|
-
sku_name
|
|
2417
|
+
name = "${names.keyVault}"
|
|
2418
|
+
location = azurerm_resource_group.main.location
|
|
2419
|
+
resource_group_name = azurerm_resource_group.main.name
|
|
2420
|
+
tenant_id = data.azurerm_client_config.current.tenant_id
|
|
2421
|
+
sku_name = "standard"
|
|
2422
|
+
rbac_authorization_enabled = true
|
|
2313
2423
|
}
|
|
2314
2424
|
|
|
2315
2425
|
data "azurerm_client_config" "current" {}
|
|
@@ -2383,17 +2493,18 @@ Build the generated backend in ACR, then replace the bootstrap image:
|
|
|
2383
2493
|
|
|
2384
2494
|
\`\`\`bash
|
|
2385
2495
|
ACR_NAME="$(tofu output -raw container_registry_name)"
|
|
2386
|
-
|
|
2496
|
+
SOURCE_SHA="$(git rev-parse HEAD)"
|
|
2497
|
+
az acr build --registry "$ACR_NAME" --image ${plan.safeProjectName}-backend:"$SOURCE_SHA" ../../..
|
|
2387
2498
|
${plan.includeFrontend ? `BACKEND_URL="https://$(tofu output -raw backend_url)"
|
|
2388
|
-
az acr build --registry "$ACR_NAME" --image ${plan.safeProjectName}-frontend:
|
|
2499
|
+
az acr build --registry "$ACR_NAME" --image ${plan.safeProjectName}-frontend:"$SOURCE_SHA" --build-arg VITE_API_BASE_URL="$BACKEND_URL" ../../../frontend
|
|
2389
2500
|
` : ''}\`\`\`
|
|
2390
2501
|
|
|
2391
2502
|
Persist the deployed images in \`environments/${env}.tfvars\` so future applies do not restore the bootstrap image:
|
|
2392
2503
|
|
|
2393
2504
|
\`\`\`hcl
|
|
2394
|
-
backend_image = "<login-server>/${plan.safeProjectName}-backend
|
|
2505
|
+
backend_image = "<login-server>/${plan.safeProjectName}-backend@sha256:<manifest-digest>"
|
|
2395
2506
|
backend_target_port = 8000
|
|
2396
|
-
${plan.includeFrontend ? `frontend_image = "<login-server>/${plan.safeProjectName}-frontend
|
|
2507
|
+
${plan.includeFrontend ? `frontend_image = "<login-server>/${plan.safeProjectName}-frontend@sha256:<manifest-digest>"
|
|
2397
2508
|
` : ''}\`\`\`
|
|
2398
2509
|
|
|
2399
2510
|
\`\`\`bash
|
|
@@ -2414,15 +2525,15 @@ function renderTofuTfvars(plan, environment) {
|
|
|
2414
2525
|
['environment', JSON.stringify(environment)],
|
|
2415
2526
|
['location', JSON.stringify(plan.region.slug)],
|
|
2416
2527
|
['resource_suffix', JSON.stringify(stableResourceSuffix(plan, environment))],
|
|
2417
|
-
['backend_image', JSON.stringify('
|
|
2528
|
+
['backend_image', JSON.stringify(formatContainerImage(supportedStack.containers['container-apps-bootstrap']))],
|
|
2418
2529
|
['backend_target_port', '80'],
|
|
2419
2530
|
['enable_private_networking', 'false']
|
|
2420
2531
|
];
|
|
2421
2532
|
if (plan.includeFrontend) {
|
|
2422
|
-
values.push(['frontend_image', JSON.stringify('
|
|
2533
|
+
values.push(['frontend_image', JSON.stringify(formatContainerImage(supportedStack.containers['container-apps-bootstrap']))]);
|
|
2423
2534
|
}
|
|
2424
2535
|
if (hasFunctionWorker(plan)) {
|
|
2425
|
-
values.push(['function_worker_queue_name', JSON.stringify(DEFAULT_FUNCTION_WORKER_QUEUE_NAME)], ['functions_python_version', JSON.stringify(
|
|
2536
|
+
values.push(['function_worker_queue_name', JSON.stringify(DEFAULT_FUNCTION_WORKER_QUEUE_NAME)], ['functions_python_version', JSON.stringify(supportedStack.runtimes.python.releaseLine)]);
|
|
2426
2537
|
}
|
|
2427
2538
|
const width = Math.max(...values.map(([key]) => key.length));
|
|
2428
2539
|
return values.map(([key, value]) => `${key.padEnd(width)} = ${value}`).join('\n');
|
|
@@ -2435,7 +2546,7 @@ function renderOpenSpecConfig(plan) {
|
|
|
2435
2546
|
: plan.apiStack.id === 'node-fastify'
|
|
2436
2547
|
? 'Keep backend API code under backend/src.'
|
|
2437
2548
|
: 'Keep the Go entrypoint under backend/cmd/api and reusable code under backend/internal.';
|
|
2438
|
-
return `schema: spec-driven
|
|
2549
|
+
return `schema: spec-driven${renderOpenSpecCopilotConfig(plan)}
|
|
2439
2550
|
|
|
2440
2551
|
context: |
|
|
2441
2552
|
Project generated by Mission Control Liftoff.
|
|
@@ -2471,7 +2582,7 @@ rules:
|
|
|
2471
2582
|
const functionsRule = hasFunctionWorker(plan) ? `
|
|
2472
2583
|
- Keep Azure Functions trigger adapters under functions/${functionWorkerName(plan)}.
|
|
2473
2584
|
- Keep reusable GenAI orchestration under backend/orchestration.` : '';
|
|
2474
|
-
return `schema: spec-driven
|
|
2585
|
+
return `schema: spec-driven${renderOpenSpecCopilotConfig(plan)}
|
|
2475
2586
|
|
|
2476
2587
|
context: |
|
|
2477
2588
|
Project generated by Mission Control Liftoff.
|
|
@@ -2804,16 +2915,15 @@ async function submit(): Promise<void> {
|
|
|
2804
2915
|
`;
|
|
2805
2916
|
}
|
|
2806
2917
|
function renderFrontendStyles() {
|
|
2807
|
-
return `@
|
|
2808
|
-
@tailwind components;
|
|
2809
|
-
@tailwind utilities;
|
|
2918
|
+
return `@import "tailwindcss";
|
|
2810
2919
|
`;
|
|
2811
2920
|
}
|
|
2812
2921
|
function renderFrontendViteConfig() {
|
|
2813
2922
|
return `import { defineConfig } from 'vite';
|
|
2814
|
-
import
|
|
2923
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
2924
|
+
import vue from '@vitejs/plugin-vue';
|
|
2815
2925
|
|
|
2816
|
-
export default defineConfig({ plugins: [vue()] });
|
|
2926
|
+
export default defineConfig({ plugins: [vue(), tailwindcss()] });
|
|
2817
2927
|
`;
|
|
2818
2928
|
}
|
|
2819
2929
|
function renderFrontendTailwindConfig() {
|
|
@@ -2827,16 +2937,17 @@ export default {
|
|
|
2827
2937
|
`;
|
|
2828
2938
|
}
|
|
2829
2939
|
function renderFrontendDockerfile() {
|
|
2830
|
-
return `FROM node
|
|
2940
|
+
return `FROM ${formatContainerImage(supportedStack.containers['node-runtime'])} AS build
|
|
2831
2941
|
WORKDIR /app
|
|
2942
|
+
ARG NPM_CONFIG_REGISTRY=https://registry.npmjs.org
|
|
2832
2943
|
COPY package.json package-lock.json* ./
|
|
2833
|
-
RUN npm
|
|
2944
|
+
RUN npm ci --ignore-scripts --no-audit --no-fund --registry="$NPM_CONFIG_REGISTRY"
|
|
2834
2945
|
COPY . .
|
|
2835
2946
|
ARG VITE_API_BASE_URL=http://localhost:8000
|
|
2836
2947
|
ENV VITE_API_BASE_URL=$VITE_API_BASE_URL
|
|
2837
2948
|
RUN npm run build
|
|
2838
2949
|
|
|
2839
|
-
FROM nginx
|
|
2950
|
+
FROM ${formatContainerImage(supportedStack.containers['nginx-runtime'])}
|
|
2840
2951
|
COPY --from=build /app/dist /usr/share/nginx/html
|
|
2841
2952
|
`;
|
|
2842
2953
|
}
|