@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.
Files changed (97) hide show
  1. package/DEVELOPER.md +209 -1
  2. package/README.md +12 -1
  3. package/assets/governance/single-maintainer-gitflow/assessment-controls.json +39 -0
  4. package/assets/supported-stack.json +4 -2
  5. package/dist/args.d.ts +2 -2
  6. package/dist/args.js +41 -10
  7. package/dist/args.js.map +1 -1
  8. package/dist/artifact-lifecycle.d.ts +1 -1
  9. package/dist/artifact-lifecycle.js +3 -1
  10. package/dist/artifact-lifecycle.js.map +1 -1
  11. package/dist/cli.js +2 -1
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands.js +214 -57
  14. package/dist/commands.js.map +1 -1
  15. package/dist/file-system.d.ts +3 -0
  16. package/dist/file-system.js +51 -10
  17. package/dist/file-system.js.map +1 -1
  18. package/dist/governance-activation/commands.js +165 -7
  19. package/dist/governance-activation/commands.js.map +1 -1
  20. package/dist/governance-activation/readiness.d.ts +2 -0
  21. package/dist/governance-activation/readiness.js +77 -12
  22. package/dist/governance-activation/readiness.js.map +1 -1
  23. package/dist/governance-activation/seed-lifecycle.d.ts +19 -1
  24. package/dist/governance-activation/seed-lifecycle.js +158 -7
  25. package/dist/governance-activation/seed-lifecycle.js.map +1 -1
  26. package/dist/governance-activation/transitions.d.ts +9 -0
  27. package/dist/governance-activation/transitions.js +37 -4
  28. package/dist/governance-activation/transitions.js.map +1 -1
  29. package/dist/governance-assessment/catalog.d.ts +8 -0
  30. package/dist/governance-assessment/catalog.js +98 -0
  31. package/dist/governance-assessment/catalog.js.map +1 -0
  32. package/dist/governance-assessment/command.d.ts +10 -0
  33. package/dist/governance-assessment/command.js +81 -0
  34. package/dist/governance-assessment/command.js.map +1 -0
  35. package/dist/governance-assessment/engine.d.ts +19 -0
  36. package/dist/governance-assessment/engine.js +586 -0
  37. package/dist/governance-assessment/engine.js.map +1 -0
  38. package/dist/governance-assessment/index.d.ts +5 -0
  39. package/dist/governance-assessment/index.js +6 -0
  40. package/dist/governance-assessment/index.js.map +1 -0
  41. package/dist/governance-assessment/live-normalize.d.ts +19 -0
  42. package/dist/governance-assessment/live-normalize.js +403 -0
  43. package/dist/governance-assessment/live-normalize.js.map +1 -0
  44. package/dist/governance-assessment/live-transport.d.ts +38 -0
  45. package/dist/governance-assessment/live-transport.js +467 -0
  46. package/dist/governance-assessment/live-transport.js.map +1 -0
  47. package/dist/governance-assessment/live.d.ts +2 -0
  48. package/dist/governance-assessment/live.js +639 -0
  49. package/dist/governance-assessment/live.js.map +1 -0
  50. package/dist/governance-assessment/predicates.d.ts +27 -0
  51. package/dist/governance-assessment/predicates.js +330 -0
  52. package/dist/governance-assessment/predicates.js.map +1 -0
  53. package/dist/governance-assessment/project.d.ts +37 -0
  54. package/dist/governance-assessment/project.js +268 -0
  55. package/dist/governance-assessment/project.js.map +1 -0
  56. package/dist/governance-assessment/readers.d.ts +18 -0
  57. package/dist/governance-assessment/readers.js +130 -0
  58. package/dist/governance-assessment/readers.js.map +1 -0
  59. package/dist/governance-assessment/report.d.ts +23 -0
  60. package/dist/governance-assessment/report.js +266 -0
  61. package/dist/governance-assessment/report.js.map +1 -0
  62. package/dist/governance-assessment/sanitize.d.ts +8 -0
  63. package/dist/governance-assessment/sanitize.js +57 -0
  64. package/dist/governance-assessment/sanitize.js.map +1 -0
  65. package/dist/governance-assessment/types.d.ts +178 -0
  66. package/dist/governance-assessment/types.js +13 -0
  67. package/dist/governance-assessment/types.js.map +1 -0
  68. package/dist/governance-assessment/yaml.d.ts +6 -0
  69. package/dist/governance-assessment/yaml.js +30 -0
  70. package/dist/governance-assessment/yaml.js.map +1 -0
  71. package/dist/migrate-plan.d.ts +4 -0
  72. package/dist/migrate-plan.js +73 -2
  73. package/dist/migrate-plan.js.map +1 -1
  74. package/dist/power-apps-templates.js +4 -0
  75. package/dist/power-apps-templates.js.map +1 -1
  76. package/dist/process-runner.d.ts +4 -0
  77. package/dist/process-runner.js +164 -8
  78. package/dist/process-runner.js.map +1 -1
  79. package/dist/published-verifier.js +6 -0
  80. package/dist/published-verifier.js.map +1 -1
  81. package/dist/release-identity.js +19 -0
  82. package/dist/release-identity.js.map +1 -1
  83. package/dist/repository-governance.d.ts +5 -0
  84. package/dist/repository-governance.js +133 -3
  85. package/dist/repository-governance.js.map +1 -1
  86. package/dist/telemetry/contract.d.ts +2 -0
  87. package/dist/telemetry/contract.js +7 -0
  88. package/dist/telemetry/contract.js.map +1 -1
  89. package/dist/templates.js +74 -29
  90. package/dist/templates.js.map +1 -1
  91. package/docs/cli-reference.md +83 -1
  92. package/docs/configuration-and-manifests.md +11 -1
  93. package/docs/project-structure.md +8 -3
  94. package/docs/repository-governance.md +125 -3
  95. package/docs/telemetry.md +9 -3
  96. package/docs/workloads.md +7 -0
  97. 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":"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"}
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', 'Scaffold preserved', projectRoot);
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', 'migrate-to-liftoff', 'proposal.md'],
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', 'migrate-to-liftoff', 'tasks.md'],
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: 'openspec/changes/migrate-to-liftoff/ (run it with your agent workflow, e.g. /opsx:apply migrate-to-liftoff)'
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
- async function preflightUpdate(projectRoot, entries, force) {
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
- if (summary.conflict > 0 || summary.retiredConflict > 0) {
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.status === 'retired-conflict'
1920
- ? `protected retired alias ${entryDisplay(entry)} ${entry.reason}${force ? '' : ' (delete it manually or use --force to remove)'}`
1921
- : `skipped ${entryDisplay(entry)} ${entry.reason}${force ? '' : ' (use --force to overwrite)'}`));
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
- function versionedBinaryCheck(label, command, args, minimum, remedy) {
2026
- const result = spawnSync(command, args, { encoding: 'utf8' });
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 { label, severity: 'fail', detail: 'not found', remedy };
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 { label, severity: 'fail', detail: `unable to determine version from "${output}"`, remedy };
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 { label, severity: 'fail', detail: `${output} is below ${minimum.join('.')}`, remedy };
2147
+ return {
2148
+ check: { label, severity: 'fail', detail: `${output} is below ${minimum.join('.')}`, remedy },
2149
+ available: true
2150
+ };
2038
2151
  }
2039
- return { label, severity: 'ok', detail: output };
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
- 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));
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
- if (!binaryPresent('go')) {
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
- const result = spawnSync(command, args, { cwd, encoding: 'utf8' });
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 probe = process.platform === 'win32' ? 'where' : 'which';
2264
- return spawnSync(probe, [command], { encoding: 'utf8' }).status === 0;
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; use liftoff update --force only after reviewing each governance conflict'
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
- cloudChecks.checks.push(binaryPresent('func')
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' ? buildDevCommand(parsed) : buildInfraCommand(parsed);
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 env = readStringFlag(parsed.flags, 'env') ?? 'dev';
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
- return `tofu apply -var-file=environments/${environment.id}.tfvars`;
2839
+ args.push('apply', `-var-file=environments/${environment.id}.tfvars`);
2840
+ break;
2688
2841
  case 'output':
2689
- return 'tofu output';
2842
+ args.push('output');
2843
+ break;
2690
2844
  case 'init':
2691
- return 'tofu init';
2845
+ args.push('init');
2846
+ break;
2692
2847
  case 'plan':
2693
2848
  default:
2694
- return `tofu plan -var-file=environments/${environment.id}.tfvars`;
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