@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/commands.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from 'node:crypto';
|
|
2
2
|
import { spawnSync } from 'node:child_process';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
3
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
4
4
|
import { cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, rm, stat } from 'node:fs/promises';
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
@@ -13,12 +13,17 @@ import { artifactPath, applyProjectFileTransaction, assertNewOrEmptyDirectory, c
|
|
|
13
13
|
import { InteractiveCancelledError, InteractivePrompter } from './interactive.js';
|
|
14
14
|
import { applyMergePreflight, assertSafeInitTarget, authorizeMergePreflight, buildMergePreflight, captureTreeState, discoverGitRoot, resolveInitTargetFromDiscovery, validateStagedTree, withStagingArea, writeStagedArtifacts } from './init-filesystem.js';
|
|
15
15
|
import { renderMigrationChecklist, renderMigrationProposal, renderMigrationTasks, seedMigrationGroups } from './migrate-plan.js';
|
|
16
|
+
import { buildOpenSpecProfileWriteCommands, configureOpenSpecProfile, inspectOpenSpecProfile, OPEN_SPEC_DELIVERY, OPEN_SPEC_PROFILE, OPEN_SPEC_WORKFLOW_IDS } from './openspec-profile.js';
|
|
16
17
|
import { buildProjectPlan, loadConfigOptions, mergeOptions, PlanValidationError, projectPlanEntries } from './planner.js';
|
|
17
18
|
import { buildDependencySetupPlan, dependencyResumeCommand, runDependencySetup, verifyPowerAppsPackageMetadata } from './project-dependencies.js';
|
|
18
19
|
import { formatCommand, NodeCommandRunner } from './process-runner.js';
|
|
19
20
|
import { scanDefaults, scanLegacyProject } from './scan.js';
|
|
20
21
|
import { hasDrift, reconcileProject } from './reconcile.js';
|
|
21
22
|
import { compareSemver } from './semver.js';
|
|
23
|
+
import { checkConfiguredRegistryTarget, runSelfUpgrade, selfUpgradeExitCode, selfUpgradeRemedy, selfUpgradeSummary } from './self-upgrade.js';
|
|
24
|
+
import { canonicalManualInstallCommand, canonicalNpmRegistry, } from './package-identity.js';
|
|
25
|
+
import { lookupStableRelease } from './stable-release.js';
|
|
26
|
+
import { supportedStack } from './supported-stack.js';
|
|
22
27
|
import { buildArtifacts, buildManifest, partitionGeneratedArtifacts } from './templates.js';
|
|
23
28
|
import { PresentationSession } from './terminal.js';
|
|
24
29
|
import { liftoffVersion } from './version.js';
|
|
@@ -27,6 +32,7 @@ export async function runCommand(parsed, context) {
|
|
|
27
32
|
const helpRequested = parsed.command !== undefined && readBooleanFlag(parsed.flags, 'help') === true;
|
|
28
33
|
const jsonMode = !helpRequested && (parsed.command === 'doctor' ||
|
|
29
34
|
parsed.command === 'update' ||
|
|
35
|
+
parsed.command === 'upgrade' ||
|
|
30
36
|
parsed.command === 'validate') &&
|
|
31
37
|
readBooleanFlag(parsed.flags, 'json') === true;
|
|
32
38
|
const presentation = new PresentationSession({
|
|
@@ -69,6 +75,8 @@ export async function runCommand(parsed, context) {
|
|
|
69
75
|
return await validateCommand(parsed, executionContext);
|
|
70
76
|
case 'update':
|
|
71
77
|
return await updateCommand(parsed, executionContext);
|
|
78
|
+
case 'upgrade':
|
|
79
|
+
return await upgradeCommand(parsed, executionContext);
|
|
72
80
|
case 'migrate':
|
|
73
81
|
return await migrateCommand(parsed, executionContext);
|
|
74
82
|
case 'doctor':
|
|
@@ -151,12 +159,17 @@ async function initCommand(parsed, context) {
|
|
|
151
159
|
if (!readiness.ready) {
|
|
152
160
|
return 1;
|
|
153
161
|
}
|
|
162
|
+
const profileReadiness = await ensureOpenSpecProfileReady(plan, options, context, runner, presentation, prompter);
|
|
163
|
+
if (!profileReadiness.ready) {
|
|
164
|
+
return 1;
|
|
165
|
+
}
|
|
154
166
|
presentation.stage('Stage project files');
|
|
155
167
|
const staged = await withStagingArea(async (area) => {
|
|
156
168
|
const partition = partitionGeneratedArtifacts(buildArtifacts(plan));
|
|
157
169
|
await writeStagedArtifacts(area, partition.durable, 'liftoff');
|
|
158
170
|
presentation.stage('Initialize spec-driven framework', `${plan.specWorkflow.label} ${plan.framework.version}`);
|
|
159
171
|
await initializeFramework(area, plan, runner, {
|
|
172
|
+
env: context.env,
|
|
160
173
|
...presentation.childStreams(),
|
|
161
174
|
onCommand: (command) => presentation.command(command)
|
|
162
175
|
});
|
|
@@ -198,7 +211,19 @@ async function initCommand(parsed, context) {
|
|
|
198
211
|
presentation.bullets('Configured integrations', [
|
|
199
212
|
`${plan.specWorkflow.label} ${plan.framework.version}`,
|
|
200
213
|
...plan.agents.map((agent) => `${agent.label}${plan.defaultAgent?.id === agent.id ? ' (default)' : ''}`),
|
|
201
|
-
...readiness.pluginProbes.map((probe) => `Code Apps plugin for ${probe.agent.label}: ${probe.state}`)
|
|
214
|
+
...readiness.pluginProbes.map((probe) => `Code Apps plugin for ${probe.agent.label}: ${probe.state}`),
|
|
215
|
+
...(plan.specWorkflow.id === 'openspec'
|
|
216
|
+
? [
|
|
217
|
+
`OpenSpec global profile: ${profileReadiness.changed ? 'configured' : 'verified'}; ` +
|
|
218
|
+
`${OPEN_SPEC_WORKFLOW_IDS.length} workflows; skills and commands`,
|
|
219
|
+
...(plan.agents.some((agent) => agent.id === 'github-copilot')
|
|
220
|
+
? [`GitHub Copilot cloud agent: ${plan.copilotCloud ? 'enabled' : 'disabled'}`]
|
|
221
|
+
: [])
|
|
222
|
+
]
|
|
223
|
+
: []),
|
|
224
|
+
plan.governanceProfile.id === 'none'
|
|
225
|
+
? 'Repository governance: disabled'
|
|
226
|
+
: `Repository governance: ${plan.governanceProfile.label} policy ${plan.governanceProfile.policyVersion}; local handoff generated, live activation deferred`
|
|
202
227
|
]);
|
|
203
228
|
if (readiness.deferred.length > 0) {
|
|
204
229
|
presentation.bullets('Deferred advisory checks', readiness.deferred);
|
|
@@ -209,7 +234,13 @@ async function initCommand(parsed, context) {
|
|
|
209
234
|
presentation.completion(`Initialized ${plan.projectName}`, target.root, [
|
|
210
235
|
{ label: 'Target', value: target.root },
|
|
211
236
|
{ label: 'Spec workflow', value: plan.specWorkflow.label },
|
|
212
|
-
{ label: 'Coding agents', value: plan.agents.map((agent) => agent.label).join(', ') }
|
|
237
|
+
{ label: 'Coding agents', value: plan.agents.map((agent) => agent.label).join(', ') },
|
|
238
|
+
{
|
|
239
|
+
label: 'Repository governance',
|
|
240
|
+
value: plan.governanceProfile.id === 'none'
|
|
241
|
+
? 'Disabled'
|
|
242
|
+
: 'Handoff generated; commit and push before read-only Phase 0'
|
|
243
|
+
}
|
|
213
244
|
], `liftoff validate ${JSON.stringify(target.root)}`);
|
|
214
245
|
return 0;
|
|
215
246
|
}
|
|
@@ -358,6 +389,58 @@ async function ensureWorkstationReady(plan, options, context, runner, presentati
|
|
|
358
389
|
];
|
|
359
390
|
return { ready: true, deferred, probes, pluginProbes };
|
|
360
391
|
}
|
|
392
|
+
async function ensureOpenSpecProfileReady(plan, options, context, runner, presentation, prompter, resumeInvocation = 'liftoff init') {
|
|
393
|
+
if (plan.specWorkflow.id !== 'openspec') {
|
|
394
|
+
return { ready: true, changed: false };
|
|
395
|
+
}
|
|
396
|
+
presentation.stage('Check OpenSpec global profile');
|
|
397
|
+
const inspection = await inspectOpenSpecProfile(plan.framework.executable, runner, {
|
|
398
|
+
cwd: context.cwd,
|
|
399
|
+
env: context.env
|
|
400
|
+
});
|
|
401
|
+
if (inspection.compatible) {
|
|
402
|
+
presentation.status('success', 'OpenSpec global profile', `${OPEN_SPEC_PROFILE}; ${OPEN_SPEC_DELIVERY}; ${OPEN_SPEC_WORKFLOW_IDS.length} workflows`);
|
|
403
|
+
return { ready: true, changed: false };
|
|
404
|
+
}
|
|
405
|
+
const commands = buildOpenSpecProfileWriteCommands(plan.framework.executable);
|
|
406
|
+
const authorized = options.configureOpenSpecProfile === true ||
|
|
407
|
+
(options.configureOpenSpecProfile === undefined &&
|
|
408
|
+
prompter !== undefined &&
|
|
409
|
+
await prompter.confirmOpenSpecProfileConfiguration({
|
|
410
|
+
observed: [
|
|
411
|
+
{ label: 'Profile', value: inspection.state.profile },
|
|
412
|
+
{ label: 'Delivery', value: inspection.state.delivery },
|
|
413
|
+
{
|
|
414
|
+
label: 'Workflows',
|
|
415
|
+
value: inspection.state.workflows.length > 0
|
|
416
|
+
? inspection.state.workflows.join(', ')
|
|
417
|
+
: '(none)'
|
|
418
|
+
}
|
|
419
|
+
],
|
|
420
|
+
required: [
|
|
421
|
+
{ label: 'Profile', value: OPEN_SPEC_PROFILE },
|
|
422
|
+
{ label: 'Delivery', value: OPEN_SPEC_DELIVERY },
|
|
423
|
+
{ label: 'Workflows', value: OPEN_SPEC_WORKFLOW_IDS.join(', ') }
|
|
424
|
+
],
|
|
425
|
+
differences: inspection.differences,
|
|
426
|
+
commands: commands.map((command) => formatCommand(command))
|
|
427
|
+
}));
|
|
428
|
+
if (!authorized) {
|
|
429
|
+
presentation.error('The global OpenSpec profile does not satisfy the Liftoff template contract.', `Run ${commands.map((command) => `\`${formatCommand(command)}\``).join(', then ')}, ` +
|
|
430
|
+
`then rerun \`${resumeInvocation}\`; or authorize those commands with ` +
|
|
431
|
+
'`--configure-openspec-profile`.');
|
|
432
|
+
return { ready: false, changed: false };
|
|
433
|
+
}
|
|
434
|
+
presentation.stage('Configure OpenSpec global profile');
|
|
435
|
+
await configureOpenSpecProfile(plan.framework.executable, runner, {
|
|
436
|
+
cwd: context.cwd,
|
|
437
|
+
env: context.env,
|
|
438
|
+
...presentation.childStreams(),
|
|
439
|
+
onCommand: (command) => presentation.command(formatCommand(command))
|
|
440
|
+
});
|
|
441
|
+
presentation.status('success', 'OpenSpec global profile', `${OPEN_SPEC_PROFILE}; ${OPEN_SPEC_DELIVERY}; ${OPEN_SPEC_WORKFLOW_IDS.length} workflows (configured)`);
|
|
442
|
+
return { ready: true, changed: true };
|
|
443
|
+
}
|
|
361
444
|
async function handleProjectDependencies(plan, projectRoot, options, probes, context, runner, presentation, prompter) {
|
|
362
445
|
const dependencyPlan = buildDependencySetupPlan(plan, projectRoot, probes);
|
|
363
446
|
let installDependencies = options.installDependencies === true;
|
|
@@ -638,6 +721,10 @@ async function executeMigration(parsed, context, sourceRoot) {
|
|
|
638
721
|
if (!readiness.ready) {
|
|
639
722
|
return 1;
|
|
640
723
|
}
|
|
724
|
+
const profileReadiness = await ensureOpenSpecProfileReady(plan, options, context, runner, presentation, prompter, `liftoff migrate ${JSON.stringify(sourceRoot)}`);
|
|
725
|
+
if (!profileReadiness.ready) {
|
|
726
|
+
return 1;
|
|
727
|
+
}
|
|
641
728
|
const migrationPlan = migrationPlanArtifacts(plan, inventory);
|
|
642
729
|
presentation.stage('Stage fresh migration project');
|
|
643
730
|
await withStagingArea(async (area) => {
|
|
@@ -645,6 +732,7 @@ async function executeMigration(parsed, context, sourceRoot) {
|
|
|
645
732
|
await writeStagedArtifacts(area, partition.durable, 'liftoff');
|
|
646
733
|
presentation.stage('Initialize spec-driven framework', `${plan.specWorkflow.label} ${plan.framework.version}`);
|
|
647
734
|
await initializeFramework(area, plan, runner, {
|
|
735
|
+
env: context.env,
|
|
648
736
|
...presentation.childStreams(),
|
|
649
737
|
onCommand: (command) => presentation.command(command)
|
|
650
738
|
});
|
|
@@ -683,7 +771,16 @@ async function executeMigration(parsed, context, sourceRoot) {
|
|
|
683
771
|
}
|
|
684
772
|
presentation.bullets('Configured integrations', [
|
|
685
773
|
`${plan.specWorkflow.label} ${plan.framework.version}`,
|
|
686
|
-
...plan.agents.map((agent) => `${agent.label}${plan.defaultAgent?.id === agent.id ? ' (default)' : ''}`)
|
|
774
|
+
...plan.agents.map((agent) => `${agent.label}${plan.defaultAgent?.id === agent.id ? ' (default)' : ''}`),
|
|
775
|
+
...(plan.specWorkflow.id === 'openspec'
|
|
776
|
+
? [
|
|
777
|
+
`OpenSpec global profile: ${profileReadiness.changed ? 'configured' : 'verified'}; ` +
|
|
778
|
+
`${OPEN_SPEC_WORKFLOW_IDS.length} workflows; skills and commands`,
|
|
779
|
+
...(plan.agents.some((agent) => agent.id === 'github-copilot')
|
|
780
|
+
? [`GitHub Copilot cloud agent: ${plan.copilotCloud ? 'enabled' : 'disabled'}`]
|
|
781
|
+
: [])
|
|
782
|
+
]
|
|
783
|
+
: [])
|
|
687
784
|
]);
|
|
688
785
|
if (readiness.deferred.length > 0) {
|
|
689
786
|
presentation.bullets('Deferred advisory checks', readiness.deferred);
|
|
@@ -888,10 +985,13 @@ async function updateCommand(parsed, context) {
|
|
|
888
985
|
}
|
|
889
986
|
if (plan.workload === 'power-apps-code-app' && recordedWorkload.kind === 'power-apps-code-app') {
|
|
890
987
|
const recordedStarter = recordedWorkload.starter;
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
plan.starter.
|
|
894
|
-
|
|
988
|
+
const starterBaseline = supportedStack.upstreams['power-apps-code-app'];
|
|
989
|
+
if (plan.starter.repository !== starterBaseline.repository ||
|
|
990
|
+
plan.starter.path !== starterBaseline.path ||
|
|
991
|
+
recordedStarter.repository !== starterBaseline.repository ||
|
|
992
|
+
recordedStarter.path !== starterBaseline.path ||
|
|
993
|
+
!starterBaseline.compatibleSourceCommits.includes(recordedStarter.commit)) {
|
|
994
|
+
presentation.error('The Power Apps starter repository, path, or commit is not represented by this Liftoff release catalog.', 'Restore the recorded source identity or use a Liftoff version that catalogs the transition.');
|
|
895
995
|
return 1;
|
|
896
996
|
}
|
|
897
997
|
}
|
|
@@ -918,18 +1018,26 @@ async function updateCommand(parsed, context) {
|
|
|
918
1018
|
presentation.error('AI agent or default-agent changes require official framework initialization and are not supported by liftoff update.', 'Restore liftoff.config.json to the integrations recorded in liftoff.manifest.json.');
|
|
919
1019
|
return 1;
|
|
920
1020
|
}
|
|
921
|
-
const
|
|
1021
|
+
const renderPlan = manifest.framework.state === 'legacy'
|
|
1022
|
+
? { ...plan, agents: [], defaultAgent: undefined }
|
|
1023
|
+
: plan;
|
|
1024
|
+
const render = buildArtifacts(renderPlan);
|
|
922
1025
|
const updateSnapshots = check
|
|
923
1026
|
? []
|
|
924
1027
|
: await captureUpdateSnapshots(projectRoot, manifest, render, initialUpdateSnapshots);
|
|
925
1028
|
const entries = await reconcileProject(manifest, render, projectRoot);
|
|
926
1029
|
const summary = summarizeEntries(entries);
|
|
927
|
-
const manifestUpgradePending = manifest.artifactVersion !==
|
|
1030
|
+
const manifestUpgradePending = manifest.artifactVersion !== 5;
|
|
1031
|
+
const starterUpgradePending = plan.workload === 'power-apps-code-app' &&
|
|
1032
|
+
recordedWorkload.kind === 'power-apps-code-app' &&
|
|
1033
|
+
recordedWorkload.starter.commit !== plan.starter.commit;
|
|
928
1034
|
const nonDurableNames = new Set(render
|
|
929
1035
|
.filter((artifact) => artifact.category === 'seed' || artifact.category === 'framework')
|
|
930
1036
|
.map((artifact) => artifact.logicalName));
|
|
931
1037
|
const manifestOwnershipCleanupPending = manifest.artifacts.some((artifact) => nonDurableNames.has(artifact.logicalName)) || manifestHadFilteredLegacySeedOwnership(manifest);
|
|
932
|
-
const manifestRewritePending = manifestUpgradePending ||
|
|
1038
|
+
const manifestRewritePending = manifestUpgradePending ||
|
|
1039
|
+
manifestOwnershipCleanupPending ||
|
|
1040
|
+
starterUpgradePending;
|
|
933
1041
|
const drift = hasDrift(entries) || manifestRewritePending;
|
|
934
1042
|
const visible = entries.filter((entry) => entry.status !== 'unchanged' || entry.refreshHash);
|
|
935
1043
|
if (!drift) {
|
|
@@ -996,9 +1104,12 @@ async function updateCommand(parsed, context) {
|
|
|
996
1104
|
}
|
|
997
1105
|
}
|
|
998
1106
|
const manifestMaintenance = [
|
|
999
|
-
manifestUpgradePending ? 'upgrade liftoff.manifest.json to schema
|
|
1107
|
+
manifestUpgradePending ? 'upgrade liftoff.manifest.json to schema v5' : undefined,
|
|
1000
1108
|
manifestOwnershipCleanupPending
|
|
1001
1109
|
? 'remove obsolete seed or framework entries from durable manifest ownership'
|
|
1110
|
+
: undefined,
|
|
1111
|
+
starterUpgradePending
|
|
1112
|
+
? `record the current packaged Power Apps starter ${plan.starter.commit}`
|
|
1002
1113
|
: undefined
|
|
1003
1114
|
].filter((item) => item !== undefined);
|
|
1004
1115
|
if (manifestMaintenance.length > 0) {
|
|
@@ -1083,6 +1194,9 @@ async function updateCommand(parsed, context) {
|
|
|
1083
1194
|
}
|
|
1084
1195
|
const oldByName = new Map(manifest.artifacts.map((artifact) => [artifact.logicalName, artifact]));
|
|
1085
1196
|
const skippedByName = new Map(skipped.map((entry) => [entry.logicalName, entry]));
|
|
1197
|
+
const hasUnrecordedGovernanceConflict = skipped.some((entry) => entry.status === 'conflict' &&
|
|
1198
|
+
entry.rendered?.category === 'governance' &&
|
|
1199
|
+
!oldByName.has(entry.logicalName));
|
|
1086
1200
|
const nextManifest = buildManifest(plan, render.filter((artifact) => artifact.logicalName !== 'manifest'), { frameworkState: manifest.framework.state });
|
|
1087
1201
|
nextManifest.framework = manifest.framework;
|
|
1088
1202
|
nextManifest.project.specWorkflow = manifest.project.specWorkflow;
|
|
@@ -1093,6 +1207,10 @@ async function updateCommand(parsed, context) {
|
|
|
1093
1207
|
else {
|
|
1094
1208
|
delete nextManifest.project.defaultAgent;
|
|
1095
1209
|
}
|
|
1210
|
+
if (hasUnrecordedGovernanceConflict &&
|
|
1211
|
+
nextManifest.governance.profile !== 'none') {
|
|
1212
|
+
nextManifest.governance.state = 'handoff-partial';
|
|
1213
|
+
}
|
|
1096
1214
|
nextManifest.artifacts = nextManifest.artifacts.flatMap((artifact) => {
|
|
1097
1215
|
// config is user-owned after create: carry the recorded entry forward untouched
|
|
1098
1216
|
if (artifact.logicalName === 'liftoff-config') {
|
|
@@ -1109,7 +1227,12 @@ async function updateCommand(parsed, context) {
|
|
|
1109
1227
|
});
|
|
1110
1228
|
for (const entry of entries) {
|
|
1111
1229
|
if (entry.status === 'orphan') {
|
|
1112
|
-
|
|
1230
|
+
const previous = oldByName.get(entry.logicalName);
|
|
1231
|
+
if (plan.governanceProfile.id === 'none' &&
|
|
1232
|
+
previous.category === 'governance') {
|
|
1233
|
+
continue;
|
|
1234
|
+
}
|
|
1235
|
+
nextManifest.artifacts.push(previous);
|
|
1113
1236
|
}
|
|
1114
1237
|
}
|
|
1115
1238
|
mutations.push({
|
|
@@ -1147,6 +1270,85 @@ async function updateCommand(parsed, context) {
|
|
|
1147
1270
|
presentation.completion('Updated project', `${written.length} written, ${skipped.length} skipped, ${summary.orphan} orphan(s)`, [{ label: 'Manifest version', value: liftoffVersion }], 'liftoff validate && liftoff doctor');
|
|
1148
1271
|
return 0;
|
|
1149
1272
|
}
|
|
1273
|
+
function renderSelfUpgradeResult(value, presentation) {
|
|
1274
|
+
const details = [
|
|
1275
|
+
{ label: 'Current CLI', value: value.currentVersion },
|
|
1276
|
+
...('targetVersion' in value && value.targetVersion
|
|
1277
|
+
? [{ label: 'Canonical target', value: value.targetVersion }]
|
|
1278
|
+
: []),
|
|
1279
|
+
...('registryKind' in value && value.registryKind
|
|
1280
|
+
? [{ label: 'Delivery registry', value: value.registryKind }]
|
|
1281
|
+
: [])
|
|
1282
|
+
];
|
|
1283
|
+
presentation.definitions('CLI upgrade result', details);
|
|
1284
|
+
switch (value.status) {
|
|
1285
|
+
case 'current':
|
|
1286
|
+
presentation.status('success', 'CLI is current', selfUpgradeSummary(value));
|
|
1287
|
+
return;
|
|
1288
|
+
case 'update-available':
|
|
1289
|
+
presentation.status('warning', 'CLI update available', selfUpgradeSummary(value));
|
|
1290
|
+
presentation.command('liftoff upgrade');
|
|
1291
|
+
return;
|
|
1292
|
+
case 'upgraded':
|
|
1293
|
+
presentation.completion('Liftoff CLI upgraded', selfUpgradeSummary(value), [{ label: 'Installed version', value: value.targetVersion }], 'liftoff update --check');
|
|
1294
|
+
return;
|
|
1295
|
+
case 'blocked':
|
|
1296
|
+
case 'failed': {
|
|
1297
|
+
presentation.status(value.status === 'blocked' ? 'warning' : 'error', value.status === 'blocked' ? 'CLI upgrade blocked' : 'CLI upgrade failed', selfUpgradeSummary(value));
|
|
1298
|
+
const remedy = selfUpgradeRemedy(value);
|
|
1299
|
+
if (remedy) {
|
|
1300
|
+
presentation.remedy(remedy);
|
|
1301
|
+
}
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
default: {
|
|
1305
|
+
const exhaustive = value;
|
|
1306
|
+
throw new Error(`Unhandled self-upgrade result: ${String(exhaustive)}`);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
async function upgradeCommand(parsed, context) {
|
|
1311
|
+
const mode = readBooleanFlag(parsed.flags, 'check') === true ? 'check' : 'apply';
|
|
1312
|
+
const json = readBooleanFlag(parsed.flags, 'json') === true;
|
|
1313
|
+
if (!json) {
|
|
1314
|
+
context.presentation.commandIdentity('upgrade', 'Replace the supported global Liftoff CLI installation');
|
|
1315
|
+
}
|
|
1316
|
+
const execute = context.selfUpgrade ?? ((request) => runSelfUpgrade(request, {
|
|
1317
|
+
environment: context.env ?? process.env
|
|
1318
|
+
}));
|
|
1319
|
+
let value;
|
|
1320
|
+
try {
|
|
1321
|
+
value = await execute({
|
|
1322
|
+
mode,
|
|
1323
|
+
currentVersion: liftoffVersion,
|
|
1324
|
+
stdout: context.stdout,
|
|
1325
|
+
stderr: context.stderr,
|
|
1326
|
+
json,
|
|
1327
|
+
...(!json
|
|
1328
|
+
? {
|
|
1329
|
+
onStage: (stage, detail) => context.presentation.stage(stage, detail),
|
|
1330
|
+
onInstallCommand: (command) => context.presentation.command(formatCommand(command))
|
|
1331
|
+
}
|
|
1332
|
+
: {})
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
catch {
|
|
1336
|
+
value = {
|
|
1337
|
+
schemaVersion: 1,
|
|
1338
|
+
mode,
|
|
1339
|
+
status: 'failed',
|
|
1340
|
+
currentVersion: liftoffVersion,
|
|
1341
|
+
reasonCode: 'verification_failed'
|
|
1342
|
+
};
|
|
1343
|
+
}
|
|
1344
|
+
if (json) {
|
|
1345
|
+
context.presentation.rawStdout(`${JSON.stringify(value, null, 2)}\n`);
|
|
1346
|
+
}
|
|
1347
|
+
else {
|
|
1348
|
+
renderSelfUpgradeResult(value, context.presentation);
|
|
1349
|
+
}
|
|
1350
|
+
return selfUpgradeExitCode(value);
|
|
1351
|
+
}
|
|
1150
1352
|
function versionedBinaryCheck(label, command, args, minimum, remedy) {
|
|
1151
1353
|
const result = spawnSync(command, args, { encoding: 'utf8' });
|
|
1152
1354
|
if (result.status !== 0) {
|
|
@@ -1164,6 +1366,9 @@ function versionedBinaryCheck(label, command, args, minimum, remedy) {
|
|
|
1164
1366
|
return { label, severity: 'ok', detail: output };
|
|
1165
1367
|
}
|
|
1166
1368
|
function pythonRuntime() {
|
|
1369
|
+
const [major, minor] = (supportedStack.runtimes.python.minimumVersion ??
|
|
1370
|
+
supportedStack.runtimes.python.version).split('.');
|
|
1371
|
+
const minimum = [Number(major), Number(minor)];
|
|
1167
1372
|
const candidates = process.platform === 'win32'
|
|
1168
1373
|
? [
|
|
1169
1374
|
{ command: 'py', versionArgs: ['-3', '--version'], commandArgs: ['-3'] },
|
|
@@ -1174,7 +1379,7 @@ function pythonRuntime() {
|
|
|
1174
1379
|
{ command: 'python3', versionArgs: ['--version'], commandArgs: [] },
|
|
1175
1380
|
{ command: 'python', versionArgs: ['--version'], commandArgs: [] }
|
|
1176
1381
|
];
|
|
1177
|
-
return candidates.find((candidate) => versionedBinaryCheck('python', candidate.command, candidate.versionArgs,
|
|
1382
|
+
return candidates.find((candidate) => versionedBinaryCheck('python', candidate.command, candidate.versionArgs, minimum, `install Python ${major}.${minor} or newer`).severity === 'ok') ?? candidates.find((candidate) => binaryPresent(candidate.command));
|
|
1178
1383
|
}
|
|
1179
1384
|
function doctorCheckFromProbe(probe) {
|
|
1180
1385
|
const severity = probe.state === 'ready'
|
|
@@ -1298,6 +1503,34 @@ async function frameworkDoctorChecks(projectRoot, manifest) {
|
|
|
1298
1503
|
];
|
|
1299
1504
|
}
|
|
1300
1505
|
function stackProjectCheck(projectRoot, apiStack) {
|
|
1506
|
+
const requiredMetadata = {
|
|
1507
|
+
'python-fastapi': ['backend/pyproject.toml', 'backend/uv.lock'],
|
|
1508
|
+
'node-fastify': ['backend/package.json', 'backend/package-lock.json'],
|
|
1509
|
+
'go-huma': ['backend/go.mod', 'backend/go.sum']
|
|
1510
|
+
};
|
|
1511
|
+
const missingMetadata = requiredMetadata[apiStack].find((relativePath) => !existsSync(path.join(projectRoot, ...relativePath.split('/'))));
|
|
1512
|
+
if (missingMetadata) {
|
|
1513
|
+
return {
|
|
1514
|
+
label: `${apiStack} project`,
|
|
1515
|
+
severity: 'fail',
|
|
1516
|
+
detail: `missing locked dependency metadata: ${missingMetadata}`,
|
|
1517
|
+
remedy: `restore ${missingMetadata} or run liftoff update`
|
|
1518
|
+
};
|
|
1519
|
+
}
|
|
1520
|
+
if (apiStack === 'python-fastapi') {
|
|
1521
|
+
const pyproject = readFileSync(path.join(projectRoot, 'backend', 'pyproject.toml'), 'utf8');
|
|
1522
|
+
const lock = readFileSync(path.join(projectRoot, 'backend', 'uv.lock'), 'utf8');
|
|
1523
|
+
const projectName = pyproject.match(/^\s*name\s*=\s*"([^"]+)"/m)?.[1];
|
|
1524
|
+
const lockedProjectName = lock.match(/\[\[package\]\]\s+name\s*=\s*"([^"]+)"\s+version\s*=\s*"[^"]+"\s+source\s*=\s*\{\s*editable\s*=\s*"\."\s*\}/m)?.[1];
|
|
1525
|
+
if (!projectName || lockedProjectName !== projectName) {
|
|
1526
|
+
return {
|
|
1527
|
+
label: `${apiStack} project`,
|
|
1528
|
+
severity: 'fail',
|
|
1529
|
+
detail: 'pyproject.toml and uv.lock do not identify the same project',
|
|
1530
|
+
remedy: 'restore backend/pyproject.toml and backend/uv.lock or run liftoff update'
|
|
1531
|
+
};
|
|
1532
|
+
}
|
|
1533
|
+
}
|
|
1301
1534
|
let result;
|
|
1302
1535
|
switch (apiStack) {
|
|
1303
1536
|
case 'python-fastapi':
|
|
@@ -1377,39 +1610,37 @@ async function cloudLayer(cloud, runner) {
|
|
|
1377
1610
|
: [{ label: cloud, severity: 'skipped', detail: `${cloud} provider checks are not available yet` }];
|
|
1378
1611
|
return { title: `Cloud - ${cloud}`, checks };
|
|
1379
1612
|
}
|
|
1380
|
-
async function
|
|
1381
|
-
const registry = process.env.LIFTOFF_REGISTRY ?? 'https://registry.npmjs.org';
|
|
1382
|
-
const controller = new AbortController();
|
|
1383
|
-
const timer = setTimeout(() => controller.abort(), 2000);
|
|
1384
|
-
try {
|
|
1385
|
-
const response = await fetch(`${registry}/@msn-control%2fliftoff/latest`, { signal: controller.signal });
|
|
1386
|
-
if (!response.ok) {
|
|
1387
|
-
return undefined;
|
|
1388
|
-
}
|
|
1389
|
-
const data = (await response.json());
|
|
1390
|
-
return data.version;
|
|
1391
|
-
}
|
|
1392
|
-
catch {
|
|
1393
|
-
return undefined; // offline or unreachable: doctor stays quiet about freshness
|
|
1394
|
-
}
|
|
1395
|
-
finally {
|
|
1396
|
-
clearTimeout(timer);
|
|
1397
|
-
}
|
|
1398
|
-
}
|
|
1399
|
-
async function cliLayer() {
|
|
1613
|
+
async function cliLayer(releaseLookup, configuredRegistryLookup) {
|
|
1400
1614
|
const checks = [
|
|
1401
1615
|
{ label: 'version', severity: 'ok', detail: `Liftoff ${liftoffVersion}` }
|
|
1402
1616
|
];
|
|
1403
|
-
|
|
1404
|
-
|
|
1617
|
+
let latest;
|
|
1618
|
+
try {
|
|
1619
|
+
latest = (await releaseLookup()).version;
|
|
1620
|
+
}
|
|
1621
|
+
catch {
|
|
1405
1622
|
return { title: 'CLI', checks };
|
|
1406
1623
|
}
|
|
1407
1624
|
if (compareSemver(latest, liftoffVersion) > 0) {
|
|
1625
|
+
let registryState = {
|
|
1626
|
+
status: 'unavailable'
|
|
1627
|
+
};
|
|
1628
|
+
try {
|
|
1629
|
+
registryState = await configuredRegistryLookup(latest);
|
|
1630
|
+
}
|
|
1631
|
+
catch {
|
|
1632
|
+
// Canonical freshness remains useful when local npm registry inspection fails.
|
|
1633
|
+
}
|
|
1634
|
+
const manual = canonicalManualInstallCommand(latest);
|
|
1408
1635
|
checks.push({
|
|
1409
1636
|
label: 'cli freshness',
|
|
1410
1637
|
severity: 'warn',
|
|
1411
|
-
detail:
|
|
1412
|
-
|
|
1638
|
+
detail: registryState.status === 'stale'
|
|
1639
|
+
? `Liftoff ${latest} is published, but the configured npm registry does not expose it`
|
|
1640
|
+
: `Liftoff ${latest} is published, this CLI is ${liftoffVersion}`,
|
|
1641
|
+
remedy: registryState.status === 'stale'
|
|
1642
|
+
? 'ask the managed registry owner to synchronize the canonical target, then run liftoff upgrade --check'
|
|
1643
|
+
: `run liftoff upgrade --check, then liftoff upgrade; manual fallback where canonical npm is permitted: ${manual}`
|
|
1413
1644
|
});
|
|
1414
1645
|
}
|
|
1415
1646
|
else {
|
|
@@ -1440,12 +1671,53 @@ async function projectLayer(projectRoot, manifest) {
|
|
|
1440
1671
|
label: 'version',
|
|
1441
1672
|
severity: 'warn',
|
|
1442
1673
|
detail: `project written by Liftoff ${manifest.liftoffVersion}, CLI is ${liftoffVersion}`,
|
|
1443
|
-
remedy:
|
|
1674
|
+
remedy: `run liftoff upgrade --check, then liftoff upgrade; manual fallback: ${canonicalManualInstallCommand(manifest.liftoffVersion)}`
|
|
1444
1675
|
});
|
|
1445
1676
|
}
|
|
1446
1677
|
else {
|
|
1447
1678
|
checks.push({ label: 'version', severity: 'ok', detail: `generated by ${manifest.liftoffVersion}, CLI ${liftoffVersion}` });
|
|
1448
1679
|
}
|
|
1680
|
+
if (manifest.governance.profile === 'unspecified') {
|
|
1681
|
+
checks.push({
|
|
1682
|
+
id: 'repository-governance',
|
|
1683
|
+
label: 'repository governance',
|
|
1684
|
+
severity: 'warn',
|
|
1685
|
+
state: 'not-observable',
|
|
1686
|
+
detail: 'manifest predates local governance handoff state',
|
|
1687
|
+
remedy: 'run liftoff update --check to inspect default handoff adoption'
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
else if (manifest.governance.profile === 'none') {
|
|
1691
|
+
checks.push({
|
|
1692
|
+
id: 'repository-governance',
|
|
1693
|
+
label: 'repository governance',
|
|
1694
|
+
severity: 'ok',
|
|
1695
|
+
state: 'disabled',
|
|
1696
|
+
detail: 'disabled; no local handoff or live-enforcement claim'
|
|
1697
|
+
});
|
|
1698
|
+
}
|
|
1699
|
+
else {
|
|
1700
|
+
const governanceIssue = issues.find((issue) => /governance|repository-governance/i.test(issue));
|
|
1701
|
+
const partialHandoff = manifest.governance.state === 'handoff-partial';
|
|
1702
|
+
const severity = governanceIssue ? 'fail' : partialHandoff ? 'warn' : 'ok';
|
|
1703
|
+
const detail = governanceIssue ??
|
|
1704
|
+
(partialHandoff
|
|
1705
|
+
? `local ${manifest.governance.profile} policy ${manifest.governance.policyVersion} handoff is incomplete; one or more exact destinations remain outside Liftoff ownership`
|
|
1706
|
+
: `local ${manifest.governance.profile} policy ${manifest.governance.policyVersion} handoff is intact; live enforcement is not inferred`);
|
|
1707
|
+
const remedy = governanceIssue
|
|
1708
|
+
? 'inspect with liftoff update --check; run liftoff update for safe repairs or use --force only after reviewing exact conflicts; neither activates GitHub governance'
|
|
1709
|
+
: partialHandoff
|
|
1710
|
+
? 'inspect liftoff update --check; use liftoff update --force only after reviewing each governance conflict'
|
|
1711
|
+
: undefined;
|
|
1712
|
+
checks.push({
|
|
1713
|
+
id: 'repository-governance',
|
|
1714
|
+
label: 'repository governance',
|
|
1715
|
+
severity,
|
|
1716
|
+
state: governanceIssue ? 'unhealthy' : manifest.governance.state,
|
|
1717
|
+
detail,
|
|
1718
|
+
...(remedy ? { remedy } : {})
|
|
1719
|
+
});
|
|
1720
|
+
}
|
|
1449
1721
|
checks.push(...await frameworkDoctorChecks(projectRoot, manifest));
|
|
1450
1722
|
if (manifest.project.workload.kind === 'power-apps-code-app') {
|
|
1451
1723
|
const starter = manifest.project.workload.starter;
|
|
@@ -1603,6 +1875,7 @@ async function doctorCommand(parsed, context) {
|
|
|
1603
1875
|
const cloudOverride = readStringFlag(parsed.flags, 'cloud');
|
|
1604
1876
|
const layers = [];
|
|
1605
1877
|
context.presentation.commandIdentity('doctor', 'Inspect CLI, workstation, project, runtime, and cloud readiness');
|
|
1878
|
+
const runner = context.runner ?? new NodeCommandRunner();
|
|
1606
1879
|
const projectRoot = await findProjectRoot(context.cwd);
|
|
1607
1880
|
let manifest;
|
|
1608
1881
|
let manifestError;
|
|
@@ -1614,8 +1887,17 @@ async function doctorCommand(parsed, context) {
|
|
|
1614
1887
|
manifestError = error;
|
|
1615
1888
|
}
|
|
1616
1889
|
}
|
|
1617
|
-
|
|
1618
|
-
|
|
1890
|
+
const releaseLookup = context.stableReleaseLookup ?? (() => lookupStableRelease({
|
|
1891
|
+
registry: context.env?.LIFTOFF_REGISTRY ??
|
|
1892
|
+
process.env.LIFTOFF_REGISTRY ??
|
|
1893
|
+
canonicalNpmRegistry
|
|
1894
|
+
}));
|
|
1895
|
+
const configuredRegistryLookup = context.configuredRegistryTargetLookup ??
|
|
1896
|
+
((targetVersion) => checkConfiguredRegistryTarget(targetVersion, {
|
|
1897
|
+
runner,
|
|
1898
|
+
environment: context.env ?? process.env
|
|
1899
|
+
}));
|
|
1900
|
+
layers.push(await cliLayer(releaseLookup, configuredRegistryLookup));
|
|
1619
1901
|
const requirements = manifest
|
|
1620
1902
|
? selectWorkstationRequirements(workstationSelectionFromManifest(manifest), { includeFramework: manifest.framework.state === 'initialized' })
|
|
1621
1903
|
: selectLiftoffRuntimeRequirements();
|
|
@@ -1738,6 +2020,9 @@ async function optionsFromParsedArgs(parsed, cwd, includeProjectName) {
|
|
|
1738
2020
|
agents: readListFlag(parsed.flags, 'agents'),
|
|
1739
2021
|
defaultAgent: readStringFlag(parsed.flags, 'default-agent'),
|
|
1740
2022
|
codeAppsPlugin: readBooleanFlag(parsed.flags, 'code-apps-plugin'),
|
|
2023
|
+
copilotCloud: readBooleanFlag(parsed.flags, 'copilot-cloud'),
|
|
2024
|
+
configureOpenSpecProfile: readBooleanFlag(parsed.flags, 'configure-openspec-profile'),
|
|
2025
|
+
governanceProfile: readStringFlag(parsed.flags, 'governance'),
|
|
1741
2026
|
configPath,
|
|
1742
2027
|
yes: readBooleanFlag(parsed.flags, 'yes') ?? false,
|
|
1743
2028
|
force: readBooleanFlag(parsed.flags, 'force'),
|
|
@@ -1760,13 +2045,18 @@ function hasMissingInitInputs(options) {
|
|
|
1760
2045
|
const missingDefaultAgent = options.specWorkflow === 'spec-kit' &&
|
|
1761
2046
|
(options.agents?.length ?? 0) > 1 &&
|
|
1762
2047
|
!options.defaultAgent;
|
|
2048
|
+
const missingCopilotCloud = options.specWorkflow === 'openspec' &&
|
|
2049
|
+
options.agents?.some((agent) => getCodingAgent(agent)?.id === 'github-copilot') === true &&
|
|
2050
|
+
options.copilotCloud === undefined;
|
|
1763
2051
|
return !options.projectName ||
|
|
1764
2052
|
missingTypeSpecific ||
|
|
1765
2053
|
projectType !== 'power-apps-code-app' && !options.cloud ||
|
|
1766
2054
|
projectType !== 'power-apps-code-app' && options.includeFrontend === undefined ||
|
|
1767
2055
|
!options.specWorkflow ||
|
|
1768
2056
|
!options.agents ||
|
|
2057
|
+
!options.governanceProfile ||
|
|
1769
2058
|
missingDefaultAgent ||
|
|
2059
|
+
missingCopilotCloud ||
|
|
1770
2060
|
projectType !== 'power-apps-code-app' && !options.environments;
|
|
1771
2061
|
}
|
|
1772
2062
|
function renderGeneralHelp(presentation) {
|