@msn-control/liftoff 0.10.2 → 0.10.4
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 +209 -1
- package/README.md +12 -1
- package/assets/governance/single-maintainer-gitflow/assessment-controls.json +39 -0
- package/assets/supported-stack.json +4 -2
- package/dist/args.d.ts +2 -2
- package/dist/args.js +41 -10
- package/dist/args.js.map +1 -1
- package/dist/artifact-lifecycle.d.ts +1 -1
- package/dist/artifact-lifecycle.js +3 -1
- package/dist/artifact-lifecycle.js.map +1 -1
- package/dist/cli.js +2 -1
- package/dist/cli.js.map +1 -1
- package/dist/commands.js +214 -57
- package/dist/commands.js.map +1 -1
- package/dist/file-system.d.ts +3 -0
- package/dist/file-system.js +51 -10
- package/dist/file-system.js.map +1 -1
- package/dist/governance-activation/commands.js +165 -7
- package/dist/governance-activation/commands.js.map +1 -1
- package/dist/governance-activation/readiness.d.ts +2 -0
- package/dist/governance-activation/readiness.js +77 -12
- package/dist/governance-activation/readiness.js.map +1 -1
- package/dist/governance-activation/seed-lifecycle.d.ts +19 -1
- package/dist/governance-activation/seed-lifecycle.js +158 -7
- package/dist/governance-activation/seed-lifecycle.js.map +1 -1
- package/dist/governance-activation/transitions.d.ts +9 -0
- package/dist/governance-activation/transitions.js +37 -4
- package/dist/governance-activation/transitions.js.map +1 -1
- package/dist/governance-assessment/catalog.d.ts +8 -0
- package/dist/governance-assessment/catalog.js +98 -0
- package/dist/governance-assessment/catalog.js.map +1 -0
- package/dist/governance-assessment/command.d.ts +10 -0
- package/dist/governance-assessment/command.js +81 -0
- package/dist/governance-assessment/command.js.map +1 -0
- package/dist/governance-assessment/engine.d.ts +19 -0
- package/dist/governance-assessment/engine.js +586 -0
- package/dist/governance-assessment/engine.js.map +1 -0
- package/dist/governance-assessment/index.d.ts +5 -0
- package/dist/governance-assessment/index.js +6 -0
- package/dist/governance-assessment/index.js.map +1 -0
- package/dist/governance-assessment/live-normalize.d.ts +19 -0
- package/dist/governance-assessment/live-normalize.js +403 -0
- package/dist/governance-assessment/live-normalize.js.map +1 -0
- package/dist/governance-assessment/live-transport.d.ts +38 -0
- package/dist/governance-assessment/live-transport.js +467 -0
- package/dist/governance-assessment/live-transport.js.map +1 -0
- package/dist/governance-assessment/live.d.ts +2 -0
- package/dist/governance-assessment/live.js +639 -0
- package/dist/governance-assessment/live.js.map +1 -0
- package/dist/governance-assessment/predicates.d.ts +27 -0
- package/dist/governance-assessment/predicates.js +330 -0
- package/dist/governance-assessment/predicates.js.map +1 -0
- package/dist/governance-assessment/project.d.ts +37 -0
- package/dist/governance-assessment/project.js +268 -0
- package/dist/governance-assessment/project.js.map +1 -0
- package/dist/governance-assessment/readers.d.ts +18 -0
- package/dist/governance-assessment/readers.js +130 -0
- package/dist/governance-assessment/readers.js.map +1 -0
- package/dist/governance-assessment/report.d.ts +23 -0
- package/dist/governance-assessment/report.js +266 -0
- package/dist/governance-assessment/report.js.map +1 -0
- package/dist/governance-assessment/sanitize.d.ts +8 -0
- package/dist/governance-assessment/sanitize.js +57 -0
- package/dist/governance-assessment/sanitize.js.map +1 -0
- package/dist/governance-assessment/types.d.ts +178 -0
- package/dist/governance-assessment/types.js +13 -0
- package/dist/governance-assessment/types.js.map +1 -0
- package/dist/governance-assessment/yaml.d.ts +6 -0
- package/dist/governance-assessment/yaml.js +30 -0
- package/dist/governance-assessment/yaml.js.map +1 -0
- package/dist/migrate-plan.d.ts +4 -0
- package/dist/migrate-plan.js +73 -2
- package/dist/migrate-plan.js.map +1 -1
- package/dist/power-apps-templates.js +4 -0
- package/dist/power-apps-templates.js.map +1 -1
- package/dist/process-runner.d.ts +4 -0
- package/dist/process-runner.js +164 -8
- package/dist/process-runner.js.map +1 -1
- package/dist/published-verifier.js +6 -0
- package/dist/published-verifier.js.map +1 -1
- package/dist/release-identity.js +19 -0
- package/dist/release-identity.js.map +1 -1
- package/dist/repository-governance.d.ts +5 -0
- package/dist/repository-governance.js +133 -3
- package/dist/repository-governance.js.map +1 -1
- package/dist/telemetry/contract.d.ts +2 -0
- package/dist/telemetry/contract.js +7 -0
- package/dist/telemetry/contract.js.map +1 -1
- package/dist/templates.js +74 -29
- package/dist/templates.js.map +1 -1
- package/docs/cli-reference.md +83 -1
- package/docs/configuration-and-manifests.md +11 -1
- package/docs/project-structure.md +8 -3
- package/docs/repository-governance.md +125 -3
- package/docs/telemetry.md +9 -3
- package/docs/workloads.md +7 -0
- package/package.json +3 -2
package/dist/cli.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,IAAI,MAAM,WAAW,CAAC;AAE7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,UAAU,EAAuB,MAAM,eAAe,CAAC;AAChE,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EACL,wBAAwB,EACxB,mBAAmB,EACnB,YAAY,EACb,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEpD,OAAO,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"}
|
package/dist/commands.js
CHANGED
|
@@ -12,7 +12,7 @@ import { validateFrameworkInstallation } from './framework-validation.js';
|
|
|
12
12
|
import { artifactPath, applyProjectFileTransaction, assertNewOrEmptyDirectory, captureProjectFileSnapshot, findProjectRoot, loadManifest, manifestHadFilteredLegacyNonDurableOwnership, manifestDisplayPath, readProjectFile, resolveProjectPath, validateGeneratedProject, writeArtifacts, writeProjectFile } from './file-system.js';
|
|
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
|
-
import { renderMigrationChecklist, renderMigrationProposal, renderMigrationTasks, seedMigrationGroups } from './migrate-plan.js';
|
|
15
|
+
import { migrationCapabilityId, migrationChangeName, renderMigrationChecklist, renderMigrationDesign, renderMigrationProposal, renderMigrationSpec, renderMigrationTasks, seedMigrationGroups } from './migrate-plan.js';
|
|
16
16
|
import { buildOpenSpecProfileWriteCommands, configureOpenSpecProfile, inspectOpenSpecProfile, OPEN_SPEC_DELIVERY, OPEN_SPEC_PROFILE, OPEN_SPEC_WORKFLOW_IDS } from './openspec-profile.js';
|
|
17
17
|
import { buildProjectPlan, loadConfigOptions, mergeOptions, PlanValidationError, projectPlanEntries } from './planner.js';
|
|
18
18
|
import { buildDependencySetupPlan, dependencyResumeCommand, runDependencySetup, verifyPowerAppsPackageMetadata } from './project-dependencies.js';
|
|
@@ -27,7 +27,7 @@ import { supportedStack } from './supported-stack.js';
|
|
|
27
27
|
import { buildArtifacts, buildManifest, partitionGeneratedArtifacts } from './templates.js';
|
|
28
28
|
import { PresentationSession } from './terminal.js';
|
|
29
29
|
import { governanceCommand } from './governance-activation/commands.js';
|
|
30
|
-
import { loadActivationState as loadCurrentActivationState } from './governance-activation/activation-state.js';
|
|
30
|
+
import { activationStateFilePathParts, loadActivationState as loadCurrentActivationState } from './governance-activation/activation-state.js';
|
|
31
31
|
import { planHistoricalActivationStateMigration, updateFailureInjectionEnv } from './governance-activation/migration.js';
|
|
32
32
|
import { currentActivationIdentity } from './governance-activation/graph.js';
|
|
33
33
|
import { governanceChangeMetadataFileName, reconcileActiveGovernanceChange, validateGovernanceChangeMetadata } from './governance-activation/source-of-truth.js';
|
|
@@ -51,7 +51,7 @@ export async function runCommand(parsed, context) {
|
|
|
51
51
|
const executionContext = { ...context, presentation };
|
|
52
52
|
try {
|
|
53
53
|
if (parsed.command && readBooleanFlag(parsed.flags, 'help')) {
|
|
54
|
-
renderCommandHelp(parsed.command, presentation);
|
|
54
|
+
renderCommandHelp(parsed.command, presentation, parsed.subcommand);
|
|
55
55
|
return 0;
|
|
56
56
|
}
|
|
57
57
|
switch (parsed.command) {
|
|
@@ -489,7 +489,8 @@ async function handleProjectDependencies(plan, projectRoot, options, probes, con
|
|
|
489
489
|
});
|
|
490
490
|
if (!result.success) {
|
|
491
491
|
presentation.error('Project dependencies failed', `${result.failed?.label ?? 'dependency command'}: ${result.detail ?? 'unknown failure'}`);
|
|
492
|
-
presentation.status('info', '
|
|
492
|
+
presentation.status('info', 'Protected dependency metadata preserved', 'Package manifests and lock metadata were checked and restored when changed.');
|
|
493
|
+
presentation.warning('Dependency scripts may have changed other project files; review the working tree before retrying.');
|
|
493
494
|
if (result.restoredMutations.length > 0) {
|
|
494
495
|
presentation.warning(`Restored protected files: ${result.restoredMutations.join(', ')}`);
|
|
495
496
|
}
|
|
@@ -631,22 +632,50 @@ function migrationPlanArtifacts(plan, inventory) {
|
|
|
631
632
|
if (plan.specWorkflow.id === 'openspec') {
|
|
632
633
|
return {
|
|
633
634
|
artifacts: [
|
|
635
|
+
{
|
|
636
|
+
logicalName: 'migration-change-metadata',
|
|
637
|
+
category: 'seed',
|
|
638
|
+
lifecycle: 'seed',
|
|
639
|
+
pathParts: ['openspec', 'changes', migrationChangeName, '.openspec.yaml'],
|
|
640
|
+
content: 'schema: spec-driven\n'
|
|
641
|
+
},
|
|
634
642
|
{
|
|
635
643
|
logicalName: 'migration-proposal',
|
|
636
644
|
category: 'seed',
|
|
637
645
|
lifecycle: 'seed',
|
|
638
|
-
pathParts: ['openspec', 'changes',
|
|
646
|
+
pathParts: ['openspec', 'changes', migrationChangeName, 'proposal.md'],
|
|
639
647
|
content: renderMigrationProposal(plan, inventory)
|
|
640
648
|
},
|
|
649
|
+
{
|
|
650
|
+
logicalName: 'migration-design',
|
|
651
|
+
category: 'seed',
|
|
652
|
+
lifecycle: 'seed',
|
|
653
|
+
pathParts: ['openspec', 'changes', migrationChangeName, 'design.md'],
|
|
654
|
+
content: renderMigrationDesign(plan, inventory)
|
|
655
|
+
},
|
|
641
656
|
{
|
|
642
657
|
logicalName: 'migration-tasks',
|
|
643
658
|
category: 'seed',
|
|
644
659
|
lifecycle: 'seed',
|
|
645
|
-
pathParts: ['openspec', 'changes',
|
|
660
|
+
pathParts: ['openspec', 'changes', migrationChangeName, 'tasks.md'],
|
|
646
661
|
content: renderMigrationTasks(groups)
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
logicalName: 'migration-spec',
|
|
665
|
+
category: 'seed',
|
|
666
|
+
lifecycle: 'seed',
|
|
667
|
+
pathParts: [
|
|
668
|
+
'openspec',
|
|
669
|
+
'changes',
|
|
670
|
+
migrationChangeName,
|
|
671
|
+
'specs',
|
|
672
|
+
migrationCapabilityId,
|
|
673
|
+
'spec.md'
|
|
674
|
+
],
|
|
675
|
+
content: renderMigrationSpec()
|
|
647
676
|
}
|
|
648
677
|
],
|
|
649
|
-
location:
|
|
678
|
+
location: `openspec/changes/${migrationChangeName}/ (run it with your agent workflow, e.g. /opsx:apply ${migrationChangeName})`
|
|
650
679
|
};
|
|
651
680
|
}
|
|
652
681
|
return {
|
|
@@ -765,6 +794,26 @@ async function executeMigration(parsed, context, sourceRoot) {
|
|
|
765
794
|
await stageMigrationSource(area, sourceRoot);
|
|
766
795
|
await writeStagedArtifacts(area, migrationPlan.artifacts, 'seed');
|
|
767
796
|
await writeStagedArtifacts(area, [partition.manifest], 'liftoff');
|
|
797
|
+
if (plan.specWorkflow.id === 'openspec') {
|
|
798
|
+
const command = {
|
|
799
|
+
executable: plan.framework.executable,
|
|
800
|
+
args: ['validate', migrationChangeName, '--strict']
|
|
801
|
+
};
|
|
802
|
+
presentation.stage('Validate generated migration plan', migrationChangeName);
|
|
803
|
+
presentation.command(formatCommand(command));
|
|
804
|
+
const result = await runner.run(command, {
|
|
805
|
+
cwd: area.root,
|
|
806
|
+
env: context.env,
|
|
807
|
+
timeoutMs: 30_000
|
|
808
|
+
});
|
|
809
|
+
if (result.status !== 0 || result.timedOut || result.errorCode) {
|
|
810
|
+
const detail = result.timedOut
|
|
811
|
+
? 'command timed out'
|
|
812
|
+
: result.errorMessage || result.stderr.trim().split(/\r?\n/)[0] ||
|
|
813
|
+
`exit status ${result.status}`;
|
|
814
|
+
throw new Error(`Generated migration plan failed strict validation: ${result.displayCommand}: ${detail}`);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
768
817
|
presentation.stage('Validate staged migration');
|
|
769
818
|
await validateStagedTree(area);
|
|
770
819
|
const stagedIssues = await validateGeneratedProject(area.root);
|
|
@@ -1084,8 +1133,23 @@ function planWithBlockedProvisioning(plan, recordedWorkload, provisioningPlans)
|
|
|
1084
1133
|
environments
|
|
1085
1134
|
};
|
|
1086
1135
|
}
|
|
1087
|
-
|
|
1136
|
+
function isUnownedUpdateConflict(entry, recordedByName) {
|
|
1137
|
+
if (entry.status === 'moved') {
|
|
1138
|
+
return entry.destinationOccupied === true && entry.destinationMatches !== true;
|
|
1139
|
+
}
|
|
1140
|
+
if (entry.status !== 'conflict') {
|
|
1141
|
+
return false;
|
|
1142
|
+
}
|
|
1143
|
+
const recorded = recordedByName.get(entry.logicalName);
|
|
1144
|
+
return !recorded ||
|
|
1145
|
+
recorded.category !== entry.rendered?.category ||
|
|
1146
|
+
recorded.pathParts.join('\0') !== entry.pathParts.join('\0');
|
|
1147
|
+
}
|
|
1148
|
+
async function preflightUpdate(projectRoot, entries, force, recordedByName) {
|
|
1088
1149
|
for (const entry of entries) {
|
|
1150
|
+
if (isUnownedUpdateConflict(entry, recordedByName)) {
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1089
1153
|
const writesDestination = entry.status === 'new' ||
|
|
1090
1154
|
entry.status === 'missing' ||
|
|
1091
1155
|
entry.status === 'upgrade' ||
|
|
@@ -1427,13 +1491,24 @@ async function updateCommand(parsed, context) {
|
|
|
1427
1491
|
captureProjectFileSnapshot(projectRoot, ['liftoff.config.json'])
|
|
1428
1492
|
]);
|
|
1429
1493
|
const manifest = await loadManifest(projectRoot);
|
|
1494
|
+
const oldByName = new Map(manifest.managedArtifacts.map((artifact) => [artifact.logicalName, artifact]));
|
|
1430
1495
|
if (compareSemver(manifest.liftoffVersion, liftoffVersion) > 0) {
|
|
1431
1496
|
presentation.error(`This project was written by Liftoff ${manifest.liftoffVersion}, which is newer than this CLI (${liftoffVersion}).`, 'Upgrade the CLI first.');
|
|
1432
1497
|
return 1;
|
|
1433
1498
|
}
|
|
1434
1499
|
const config = await loadConfigOptions('liftoff.config.json', projectRoot);
|
|
1435
|
-
const plan = buildProjectPlan(config, { requireProjectName: true });
|
|
1436
1500
|
const recordedWorkload = manifest.project.workload;
|
|
1501
|
+
if (recordedWorkload.kind !== 'power-apps-code-app' &&
|
|
1502
|
+
config.cloud !== undefined &&
|
|
1503
|
+
config.cloud !== recordedWorkload.cloud) {
|
|
1504
|
+
presentation.error(`Cloud changes (${recordedWorkload.cloud} -> ${config.cloud}) are a migration, not an update.`, 'Restore liftoff.config.json or perform the cloud change through a separately reviewed project migration.');
|
|
1505
|
+
return 1;
|
|
1506
|
+
}
|
|
1507
|
+
const plan = buildProjectPlan(config, { requireProjectName: true });
|
|
1508
|
+
if (plan.projectName !== manifest.project.name) {
|
|
1509
|
+
presentation.error(`Project name changes (${manifest.project.name} -> ${plan.projectName}) are a migration, not an update.`, 'Restore liftoff.config.json or perform the rename through a separately reviewed project migration.');
|
|
1510
|
+
return 1;
|
|
1511
|
+
}
|
|
1437
1512
|
if (plan.workload !== recordedWorkload.kind) {
|
|
1438
1513
|
const involvesPowerApps = plan.workload === 'power-apps-code-app' ||
|
|
1439
1514
|
recordedWorkload.kind === 'power-apps-code-app';
|
|
@@ -1455,6 +1530,14 @@ async function updateCommand(parsed, context) {
|
|
|
1455
1530
|
}
|
|
1456
1531
|
}
|
|
1457
1532
|
else if (plan.workload !== 'power-apps-code-app' && recordedWorkload.kind !== 'power-apps-code-app') {
|
|
1533
|
+
if (plan.provider.id !== recordedWorkload.cloud) {
|
|
1534
|
+
presentation.error(`Cloud changes (${recordedWorkload.cloud} -> ${plan.provider.id}) are a migration, not an update.`, 'Restore liftoff.config.json or perform the cloud change through a separately reviewed project migration.');
|
|
1535
|
+
return 1;
|
|
1536
|
+
}
|
|
1537
|
+
if (plan.region.slug !== recordedWorkload.region) {
|
|
1538
|
+
presentation.error(`Region changes (${recordedWorkload.region} -> ${plan.region.slug}) are a migration, not an update.`, 'Restore liftoff.config.json or perform the region change through a separately reviewed project migration.');
|
|
1539
|
+
return 1;
|
|
1540
|
+
}
|
|
1458
1541
|
if (plan.apiStack.id !== recordedWorkload.apiStack) {
|
|
1459
1542
|
presentation.error(`API stack changes (${recordedWorkload.apiStack} -> ${plan.apiStack.id}) are a migration, not an update.`, 'Initialize a fresh project and move production behavior through a reviewed project change.');
|
|
1460
1543
|
return 1;
|
|
@@ -1466,6 +1549,13 @@ async function updateCommand(parsed, context) {
|
|
|
1466
1549
|
return 1;
|
|
1467
1550
|
}
|
|
1468
1551
|
}
|
|
1552
|
+
if (manifest.governance.profile !== 'none' &&
|
|
1553
|
+
manifest.governance.profile !== 'unspecified' &&
|
|
1554
|
+
plan.governanceProfile.id === 'none' &&
|
|
1555
|
+
await readProjectFile(projectRoot, [...activationStateFilePathParts]) !== undefined) {
|
|
1556
|
+
presentation.error('Repository governance cannot be disabled by update while governance/activation-state.json exists.', 'Restore liftoff.config.json or complete a separately supported deactivation and reconciliation workflow before disabling governance. This Liftoff release does not infer deactivation or the absence of live enforcement.');
|
|
1557
|
+
return 1;
|
|
1558
|
+
}
|
|
1469
1559
|
if (plan.specWorkflow.id !== manifest.project.specWorkflow) {
|
|
1470
1560
|
presentation.error(`Spec workflow changes (${manifest.project.specWorkflow} -> ${plan.specWorkflow.id}) require official framework initialization and are not supported by liftoff update.`, 'Restore the workflow recorded in liftoff.manifest.json or migrate into a fresh project.');
|
|
1471
1561
|
return 1;
|
|
@@ -1680,7 +1770,13 @@ async function updateCommand(parsed, context) {
|
|
|
1680
1770
|
if (toWrite > 0 || toRetire > 0) {
|
|
1681
1771
|
presentation.command('liftoff update');
|
|
1682
1772
|
}
|
|
1683
|
-
|
|
1773
|
+
const unownedConflicts = entries.filter((entry) => isUnownedUpdateConflict(entry, oldByName));
|
|
1774
|
+
if (unownedConflicts.length > 0) {
|
|
1775
|
+
presentation.bullets('Unowned destinations remain protected', unownedConflicts.map((entry) => `${entryDisplay(entry)}: review and resolve this unowned destination manually; --force cannot overwrite it`));
|
|
1776
|
+
}
|
|
1777
|
+
if (entries.some((entry) => !isUnownedUpdateConflict(entry, oldByName) &&
|
|
1778
|
+
(entry.status === 'conflict' || entry.status === 'retired-conflict' ||
|
|
1779
|
+
entry.status === 'moved' && !entry.cleanMove))) {
|
|
1684
1780
|
presentation.command('liftoff update --force');
|
|
1685
1781
|
}
|
|
1686
1782
|
return 2;
|
|
@@ -1696,7 +1792,7 @@ async function updateCommand(parsed, context) {
|
|
|
1696
1792
|
}
|
|
1697
1793
|
}
|
|
1698
1794
|
presentation.stage('Apply safe Liftoff core changes', projectRoot);
|
|
1699
|
-
await preflightUpdate(projectRoot, entries, force);
|
|
1795
|
+
await preflightUpdate(projectRoot, entries, force, oldByName);
|
|
1700
1796
|
maybeInjectUpdateFailure(context.env, 'after-preflight');
|
|
1701
1797
|
const written = [];
|
|
1702
1798
|
const retired = [];
|
|
@@ -1715,7 +1811,7 @@ async function updateCommand(parsed, context) {
|
|
|
1715
1811
|
written.push(entry);
|
|
1716
1812
|
break;
|
|
1717
1813
|
case 'moved':
|
|
1718
|
-
if (entry.cleanMove || force) {
|
|
1814
|
+
if (!isUnownedUpdateConflict(entry, oldByName) && (entry.cleanMove || force)) {
|
|
1719
1815
|
if (!entry.destinationMatches) {
|
|
1720
1816
|
mutations.push({
|
|
1721
1817
|
type: 'write',
|
|
@@ -1744,7 +1840,7 @@ async function updateCommand(parsed, context) {
|
|
|
1744
1840
|
}
|
|
1745
1841
|
break;
|
|
1746
1842
|
case 'conflict':
|
|
1747
|
-
if (force) {
|
|
1843
|
+
if (force && !isUnownedUpdateConflict(entry, oldByName)) {
|
|
1748
1844
|
mutations.push({
|
|
1749
1845
|
type: 'write',
|
|
1750
1846
|
pathParts: entry.pathParts,
|
|
@@ -1782,7 +1878,6 @@ async function updateCommand(parsed, context) {
|
|
|
1782
1878
|
for (const mutation of stateMigration.mutations) {
|
|
1783
1879
|
mutations.push(mutation);
|
|
1784
1880
|
}
|
|
1785
|
-
const oldByName = new Map(manifest.managedArtifacts.map((artifact) => [artifact.logicalName, artifact]));
|
|
1786
1881
|
const skippedByName = new Map(skipped.map((entry) => [entry.logicalName, entry]));
|
|
1787
1882
|
const hasUnrecordedGovernanceConflict = skipped.some((entry) => entry.status === 'conflict' &&
|
|
1788
1883
|
entry.rendered?.category === 'governance' &&
|
|
@@ -1916,9 +2011,11 @@ async function updateCommand(parsed, context) {
|
|
|
1916
2011
|
.map((entry) => `${group.group}: preserved ${manifestDisplayPath(entry.rendered.pathParts)}; --force cannot overwrite project-owned bytes`)));
|
|
1917
2012
|
}
|
|
1918
2013
|
if (skipped.length > 0) {
|
|
1919
|
-
presentation.bullets('Skipped Liftoff core conflicts', skipped.map((entry) => entry
|
|
1920
|
-
? `protected
|
|
1921
|
-
:
|
|
2014
|
+
presentation.bullets('Skipped Liftoff core conflicts', skipped.map((entry) => isUnownedUpdateConflict(entry, oldByName)
|
|
2015
|
+
? `protected unowned destination ${entryDisplay(entry)} ${entry.reason} (resolve manually; --force cannot overwrite it)`
|
|
2016
|
+
: entry.status === 'retired-conflict'
|
|
2017
|
+
? `protected retired alias ${entryDisplay(entry)} ${entry.reason}${force ? '' : ' (delete it manually or use --force to remove)'}`
|
|
2018
|
+
: `skipped ${entryDisplay(entry)} ${entry.reason}${force ? '' : ' (use --force to overwrite)'}`));
|
|
1922
2019
|
}
|
|
1923
2020
|
const orphans = entries.filter((entry) => entry.status === 'orphan');
|
|
1924
2021
|
if (orphans.length > 0) {
|
|
@@ -2022,23 +2119,42 @@ async function upgradeCommand(parsed, context) {
|
|
|
2022
2119
|
}
|
|
2023
2120
|
return selfUpgradeExitCode(value);
|
|
2024
2121
|
}
|
|
2025
|
-
|
|
2026
|
-
|
|
2122
|
+
const doctorProbeTimeoutMs = 15_000;
|
|
2123
|
+
async function versionedBinaryCheck(label, command, args, minimum, remedy, runner) {
|
|
2124
|
+
const result = await runner.run({ executable: command, args }, { timeoutMs: doctorProbeTimeoutMs });
|
|
2125
|
+
if (result.timedOut) {
|
|
2126
|
+
return {
|
|
2127
|
+
check: { label, severity: 'fail', detail: 'version probe timed out', remedy },
|
|
2128
|
+
available: false
|
|
2129
|
+
};
|
|
2130
|
+
}
|
|
2027
2131
|
if (result.status !== 0) {
|
|
2028
|
-
return {
|
|
2132
|
+
return {
|
|
2133
|
+
check: { label, severity: 'fail', detail: 'not found', remedy },
|
|
2134
|
+
available: result.errorCode !== 'ENOENT'
|
|
2135
|
+
};
|
|
2029
2136
|
}
|
|
2030
2137
|
const output = (result.stdout || result.stderr).split('\n')[0].trim();
|
|
2031
2138
|
const match = output.match(/(\d+)\.(\d+)/);
|
|
2032
2139
|
if (!match) {
|
|
2033
|
-
return {
|
|
2140
|
+
return {
|
|
2141
|
+
check: { label, severity: 'fail', detail: `unable to determine version from "${output}"`, remedy },
|
|
2142
|
+
available: true
|
|
2143
|
+
};
|
|
2034
2144
|
}
|
|
2035
2145
|
const found = [Number(match[1]), Number(match[2])];
|
|
2036
2146
|
if (found[0] < minimum[0] || found[0] === minimum[0] && found[1] < minimum[1]) {
|
|
2037
|
-
return {
|
|
2147
|
+
return {
|
|
2148
|
+
check: { label, severity: 'fail', detail: `${output} is below ${minimum.join('.')}`, remedy },
|
|
2149
|
+
available: true
|
|
2150
|
+
};
|
|
2038
2151
|
}
|
|
2039
|
-
return {
|
|
2152
|
+
return {
|
|
2153
|
+
check: { label, severity: 'ok', detail: output },
|
|
2154
|
+
available: true
|
|
2155
|
+
};
|
|
2040
2156
|
}
|
|
2041
|
-
function pythonRuntime() {
|
|
2157
|
+
async function pythonRuntime(runner) {
|
|
2042
2158
|
const [major, minor] = (supportedStack.runtimes.python.minimumVersion ??
|
|
2043
2159
|
supportedStack.runtimes.python.version).split('.');
|
|
2044
2160
|
const minimum = [Number(major), Number(minor)];
|
|
@@ -2052,7 +2168,17 @@ function pythonRuntime() {
|
|
|
2052
2168
|
{ command: 'python3', versionArgs: ['--version'], commandArgs: [] },
|
|
2053
2169
|
{ command: 'python', versionArgs: ['--version'], commandArgs: [] }
|
|
2054
2170
|
];
|
|
2055
|
-
|
|
2171
|
+
let available;
|
|
2172
|
+
for (const candidate of candidates) {
|
|
2173
|
+
const probe = await versionedBinaryCheck('python', candidate.command, candidate.versionArgs, minimum, `install Python ${major}.${minor} or newer`, runner);
|
|
2174
|
+
if (probe.check.severity === 'ok') {
|
|
2175
|
+
return candidate;
|
|
2176
|
+
}
|
|
2177
|
+
if (probe.available && !available) {
|
|
2178
|
+
available = candidate;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
return available;
|
|
2056
2182
|
}
|
|
2057
2183
|
function doctorCheckFromProbe(probe) {
|
|
2058
2184
|
const severity = probe.state === 'ready'
|
|
@@ -2175,7 +2301,7 @@ async function frameworkDoctorChecks(projectRoot, manifest) {
|
|
|
2175
2301
|
markers
|
|
2176
2302
|
];
|
|
2177
2303
|
}
|
|
2178
|
-
function stackProjectCheck(projectRoot, apiStack) {
|
|
2304
|
+
async function stackProjectCheck(projectRoot, apiStack, runner) {
|
|
2179
2305
|
const requiredMetadata = {
|
|
2180
2306
|
'python-fastapi': ['backend/pyproject.toml', 'backend/uv.lock'],
|
|
2181
2307
|
'node-fastify': ['backend/package.json', 'backend/package-lock.json'],
|
|
@@ -2208,23 +2334,31 @@ function stackProjectCheck(projectRoot, apiStack) {
|
|
|
2208
2334
|
switch (apiStack) {
|
|
2209
2335
|
case 'python-fastapi':
|
|
2210
2336
|
{
|
|
2211
|
-
const python = pythonRuntime();
|
|
2337
|
+
const python = await pythonRuntime(runner);
|
|
2212
2338
|
if (!python) {
|
|
2213
2339
|
return { label: 'python project', severity: 'skipped', detail: 'python is unavailable' };
|
|
2214
2340
|
}
|
|
2215
|
-
result = runReadOnly(python.command, [...python.commandArgs, '-c', 'from pathlib import Path; p=Path("backend/apis/main.py"); compile(p.read_text(), str(p), "exec")'], projectRoot);
|
|
2341
|
+
result = await runReadOnly(python.command, [...python.commandArgs, '-c', 'from pathlib import Path; p=Path("backend/apis/main.py"); compile(p.read_text(), str(p), "exec")'], projectRoot, runner);
|
|
2216
2342
|
}
|
|
2217
2343
|
break;
|
|
2218
2344
|
case 'node-fastify':
|
|
2219
|
-
result = runReadOnly('node', ['-e', 'const f=require("fs"); JSON.parse(f.readFileSync("backend/package.json")); JSON.parse(f.readFileSync("backend/tsconfig.json"));'], projectRoot);
|
|
2345
|
+
result = await runReadOnly('node', ['-e', 'const f=require("fs"); JSON.parse(f.readFileSync("backend/package.json")); JSON.parse(f.readFileSync("backend/tsconfig.json"));'], projectRoot, runner);
|
|
2220
2346
|
break;
|
|
2221
2347
|
case 'go-huma':
|
|
2222
|
-
|
|
2348
|
+
result = await runReadOnly('go', ['mod', 'edit', '-json'], path.join(projectRoot, 'backend'), runner);
|
|
2349
|
+
if (result.errorCode === 'ENOENT') {
|
|
2223
2350
|
return { label: 'go project', severity: 'skipped', detail: 'go is unavailable' };
|
|
2224
2351
|
}
|
|
2225
|
-
result = runReadOnly('go', ['mod', 'edit', '-json'], path.join(projectRoot, 'backend'));
|
|
2226
2352
|
break;
|
|
2227
2353
|
}
|
|
2354
|
+
if (result.timedOut) {
|
|
2355
|
+
return {
|
|
2356
|
+
label: `${apiStack} project`,
|
|
2357
|
+
severity: 'fail',
|
|
2358
|
+
detail: 'stack validation timed out',
|
|
2359
|
+
remedy: `repair the generated ${apiStack} backend configuration`
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2228
2362
|
if (result.status === 0) {
|
|
2229
2363
|
return { label: `${apiStack} project`, severity: 'ok', detail: 'stack configuration is valid' };
|
|
2230
2364
|
}
|
|
@@ -2255,13 +2389,12 @@ async function powerAppsProjectCheck(projectRoot) {
|
|
|
2255
2389
|
remedy: 'repair the project-owned package.json and package-lock.json'
|
|
2256
2390
|
};
|
|
2257
2391
|
}
|
|
2258
|
-
function runReadOnly(command, args, cwd) {
|
|
2259
|
-
|
|
2260
|
-
return { status: result.status, stdout: result.stdout, stderr: result.stderr };
|
|
2392
|
+
function runReadOnly(command, args, cwd, runner) {
|
|
2393
|
+
return runner.run({ executable: command, args }, { cwd, timeoutMs: doctorProbeTimeoutMs });
|
|
2261
2394
|
}
|
|
2262
|
-
function binaryPresent(command) {
|
|
2263
|
-
const
|
|
2264
|
-
return
|
|
2395
|
+
async function binaryPresent(command, runner) {
|
|
2396
|
+
const result = await runner.run({ executable: command, args: ['--version'] }, { timeoutMs: doctorProbeTimeoutMs });
|
|
2397
|
+
return result.status === 0 && !result.timedOut;
|
|
2265
2398
|
}
|
|
2266
2399
|
async function azureCloudChecks(runner) {
|
|
2267
2400
|
const auth = await runner.run({ executable: 'az', args: ['account', 'show', '-o', 'none', '--only-show-errors'] }, { timeoutMs: 15_000 });
|
|
@@ -2325,7 +2458,7 @@ async function cliLayer(releaseLookup, configuredRegistryLookup) {
|
|
|
2325
2458
|
}
|
|
2326
2459
|
return { title: 'CLI', checks };
|
|
2327
2460
|
}
|
|
2328
|
-
async function projectLayer(projectRoot, manifest) {
|
|
2461
|
+
async function projectLayer(projectRoot, manifest, runner) {
|
|
2329
2462
|
const checks = [];
|
|
2330
2463
|
const issues = await validateGeneratedProject(projectRoot);
|
|
2331
2464
|
if (issues.length > 0) {
|
|
@@ -2382,9 +2515,9 @@ async function projectLayer(projectRoot, manifest) {
|
|
|
2382
2515
|
? `local ${manifest.governance.profile} policy ${manifest.governance.policyVersion} handoff is incomplete; one or more exact destinations remain outside Liftoff ownership`
|
|
2383
2516
|
: `local ${manifest.governance.profile} policy ${manifest.governance.policyVersion} handoff is intact; live enforcement is not inferred`);
|
|
2384
2517
|
const remedy = governanceIssue
|
|
2385
|
-
? 'inspect with liftoff update --check; run liftoff update for safe repairs or use --force only after reviewing exact conflicts; neither activates GitHub governance'
|
|
2518
|
+
? 'inspect with liftoff update --check; run liftoff update for safe repairs or use --force only after reviewing exact already-managed conflicts; unowned destinations require manual resolution; neither activates GitHub governance'
|
|
2386
2519
|
: partialHandoff
|
|
2387
|
-
? 'inspect liftoff update --check;
|
|
2520
|
+
? 'inspect liftoff update --check; resolve unowned destinations manually; liftoff update --force may replace only already-managed conflicts or remove exact retired aliases after review'
|
|
2388
2521
|
: undefined;
|
|
2389
2522
|
checks.push({
|
|
2390
2523
|
id: 'repository-governance',
|
|
@@ -2409,7 +2542,7 @@ async function projectLayer(projectRoot, manifest) {
|
|
|
2409
2542
|
checks.push(await powerAppsProjectCheck(projectRoot));
|
|
2410
2543
|
}
|
|
2411
2544
|
else {
|
|
2412
|
-
checks.push(stackProjectCheck(projectRoot, manifest.project.workload.apiStack));
|
|
2545
|
+
checks.push(await stackProjectCheck(projectRoot, manifest.project.workload.apiStack, runner));
|
|
2413
2546
|
}
|
|
2414
2547
|
try {
|
|
2415
2548
|
const config = await loadConfigOptions('liftoff.config.json', projectRoot);
|
|
@@ -2572,11 +2705,7 @@ async function doctorCommand(parsed, context) {
|
|
|
2572
2705
|
manifestError = error;
|
|
2573
2706
|
}
|
|
2574
2707
|
}
|
|
2575
|
-
const releaseLookup = context.stableReleaseLookup ?? (() => lookupStableRelease({
|
|
2576
|
-
registry: context.env?.LIFTOFF_REGISTRY ??
|
|
2577
|
-
process.env.LIFTOFF_REGISTRY ??
|
|
2578
|
-
canonicalNpmRegistry
|
|
2579
|
-
}));
|
|
2708
|
+
const releaseLookup = context.stableReleaseLookup ?? (() => lookupStableRelease({ registry: canonicalNpmRegistry }));
|
|
2580
2709
|
const configuredRegistryLookup = context.configuredRegistryTargetLookup ??
|
|
2581
2710
|
((targetVersion) => checkConfiguredRegistryTarget(targetVersion, {
|
|
2582
2711
|
runner,
|
|
@@ -2598,7 +2727,7 @@ async function doctorCommand(parsed, context) {
|
|
|
2598
2727
|
});
|
|
2599
2728
|
}
|
|
2600
2729
|
if (manifest) {
|
|
2601
|
-
layers.push(await projectLayer(projectRoot, manifest));
|
|
2730
|
+
layers.push(await projectLayer(projectRoot, manifest, runner));
|
|
2602
2731
|
layers.push(await runtimeLayer(projectRoot, dockerAvailable, runner, manifest));
|
|
2603
2732
|
if (manifest.project.workload.kind === 'power-apps-code-app') {
|
|
2604
2733
|
if (manifest.project.workload.codeAppsPlugin) {
|
|
@@ -2613,7 +2742,8 @@ async function doctorCommand(parsed, context) {
|
|
|
2613
2742
|
? patterns.find((candidate) => candidate.id === workload.pattern)
|
|
2614
2743
|
: undefined;
|
|
2615
2744
|
if (pattern?.worker && cloud === 'azure') {
|
|
2616
|
-
|
|
2745
|
+
const functionsAvailable = await binaryPresent('func', runner);
|
|
2746
|
+
cloudChecks.checks.push(functionsAvailable
|
|
2617
2747
|
? { label: 'functions tooling', severity: 'ok', detail: 'Azure Functions Core Tools installed' }
|
|
2618
2748
|
: { label: 'functions tooling', severity: 'warn', detail: 'Azure Functions Core Tools not found', remedy: 'npm install -g azure-functions-core-tools@4' });
|
|
2619
2749
|
}
|
|
@@ -2637,6 +2767,7 @@ async function doctorCommand(parsed, context) {
|
|
|
2637
2767
|
}
|
|
2638
2768
|
async function helperCommand(parsed, context, tool) {
|
|
2639
2769
|
const projectRoot = await findProjectRoot(context.cwd);
|
|
2770
|
+
let infraProject;
|
|
2640
2771
|
if (projectRoot) {
|
|
2641
2772
|
const manifest = await loadManifest(projectRoot);
|
|
2642
2773
|
if (manifest.project.workload.kind === 'power-apps-code-app') {
|
|
@@ -2652,8 +2783,14 @@ async function helperCommand(parsed, context, tool) {
|
|
|
2652
2783
|
context.presentation.status('info', 'Not applicable', 'Power Apps code apps are hosted by Power Platform; Liftoff-managed OpenTofu infrastructure is not generated.');
|
|
2653
2784
|
return 0;
|
|
2654
2785
|
}
|
|
2786
|
+
infraProject = {
|
|
2787
|
+
root: projectRoot,
|
|
2788
|
+
environments: manifest.project.workload.environments
|
|
2789
|
+
};
|
|
2655
2790
|
}
|
|
2656
|
-
const command = parsed.command === 'dev'
|
|
2791
|
+
const command = parsed.command === 'dev'
|
|
2792
|
+
? buildDevCommand(parsed)
|
|
2793
|
+
: buildInfraCommand(parsed, infraProject);
|
|
2657
2794
|
context.presentation.commandIdentity(parsed.command ?? tool, `${tool} helper command`);
|
|
2658
2795
|
context.presentation.section(`${tool} helper command`, []);
|
|
2659
2796
|
context.presentation.command(command);
|
|
@@ -2674,25 +2811,45 @@ function buildDevCommand(parsed) {
|
|
|
2674
2811
|
}
|
|
2675
2812
|
}
|
|
2676
2813
|
}
|
|
2677
|
-
function buildInfraCommand(parsed) {
|
|
2678
|
-
const
|
|
2814
|
+
function buildInfraCommand(parsed, project) {
|
|
2815
|
+
const requestedEnvironment = readStringFlag(parsed.flags, 'env');
|
|
2816
|
+
const env = requestedEnvironment ?? project?.environments[0] ?? 'dev';
|
|
2679
2817
|
const environment = getEnvironment(env);
|
|
2680
2818
|
if (!environment) {
|
|
2681
2819
|
throw new PlanValidationError([
|
|
2682
2820
|
`Unsupported environment: ${env}. Supported environments: dev, staging, prod.`
|
|
2683
2821
|
]);
|
|
2684
2822
|
}
|
|
2823
|
+
if (project &&
|
|
2824
|
+
requestedEnvironment !== undefined &&
|
|
2825
|
+
!project.environments.includes(environment.id)) {
|
|
2826
|
+
throw new PlanValidationError([
|
|
2827
|
+
`Environment ${environment.id} is not selected for this project. ` +
|
|
2828
|
+
`Selected environments: ${project.environments.join(', ')}.`
|
|
2829
|
+
]);
|
|
2830
|
+
}
|
|
2831
|
+
const infrastructureRoot = project
|
|
2832
|
+
? path.join(project.root, 'infrastructure', 'opentofu', 'azure')
|
|
2833
|
+
.split(path.sep)
|
|
2834
|
+
.join('/')
|
|
2835
|
+
: undefined;
|
|
2836
|
+
const args = infrastructureRoot ? [`-chdir=${infrastructureRoot}`] : [];
|
|
2685
2837
|
switch (parsed.subcommand) {
|
|
2686
2838
|
case 'apply':
|
|
2687
|
-
|
|
2839
|
+
args.push('apply', `-var-file=environments/${environment.id}.tfvars`);
|
|
2840
|
+
break;
|
|
2688
2841
|
case 'output':
|
|
2689
|
-
|
|
2842
|
+
args.push('output');
|
|
2843
|
+
break;
|
|
2690
2844
|
case 'init':
|
|
2691
|
-
|
|
2845
|
+
args.push('init');
|
|
2846
|
+
break;
|
|
2692
2847
|
case 'plan':
|
|
2693
2848
|
default:
|
|
2694
|
-
|
|
2849
|
+
args.push('plan', `-var-file=environments/${environment.id}.tfvars`);
|
|
2850
|
+
break;
|
|
2695
2851
|
}
|
|
2852
|
+
return formatCommand({ executable: 'tofu', args });
|
|
2696
2853
|
}
|
|
2697
2854
|
async function optionsFromParsedArgs(parsed, cwd, includeProjectName) {
|
|
2698
2855
|
const configPath = readStringFlag(parsed.flags, 'config');
|
|
@@ -2760,8 +2917,8 @@ function renderGeneralHelp(presentation) {
|
|
|
2760
2917
|
}
|
|
2761
2918
|
presentation.status('info', 'Tip', help.hint);
|
|
2762
2919
|
}
|
|
2763
|
-
function renderCommandHelp(command, presentation) {
|
|
2764
|
-
const help = getCommandHelp(command);
|
|
2920
|
+
function renderCommandHelp(command, presentation, subcommand) {
|
|
2921
|
+
const help = getCommandHelp(command, subcommand);
|
|
2765
2922
|
presentation.commandIdentity(help.command, help.description);
|
|
2766
2923
|
presentation.section('Usage', [
|
|
2767
2924
|
presentation.stdout.layout === 'plain' ? `Usage: ${help.usage}` : help.usage
|