@msn-control/liftoff 0.9.0 → 0.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -5
- package/assets/locks/frontend/package-lock.json +51 -51
- package/assets/locks/frontend/package.json +1 -1
- package/assets/locks/node-backend/package-lock.json +353 -12
- package/assets/locks/node-backend/package.json +3 -3
- package/assets/supported-stack.json +9 -9
- package/dist/args.js +5 -5
- package/dist/args.js.map +1 -1
- package/dist/artifact-lifecycle.d.ts +4 -0
- package/dist/artifact-lifecycle.js +38 -0
- package/dist/artifact-lifecycle.js.map +1 -0
- package/dist/commands.js +327 -76
- package/dist/commands.js.map +1 -1
- package/dist/file-system.d.ts +1 -1
- package/dist/file-system.js +171 -29
- package/dist/file-system.js.map +1 -1
- package/dist/init-filesystem.js +2 -2
- package/dist/init-filesystem.js.map +1 -1
- package/dist/power-apps-templates.js +3 -1
- package/dist/power-apps-templates.js.map +1 -1
- package/dist/reconcile.js +4 -10
- package/dist/reconcile.js.map +1 -1
- package/dist/repository-governance.js +4 -0
- package/dist/repository-governance.js.map +1 -1
- package/dist/stable-release.js +1 -1
- package/dist/stable-release.js.map +1 -1
- package/dist/templates.d.ts +5 -1
- package/dist/templates.js +102 -56
- package/dist/templates.js.map +1 -1
- package/dist/types.d.ts +24 -4
- package/docs/cli-reference.md +39 -25
- package/docs/configuration-and-manifests.md +39 -24
- package/docs/existing-repositories.md +15 -15
- package/docs/getting-started.md +3 -2
- package/docs/project-structure.md +6 -1
- package/docs/repository-governance.md +2 -2
- package/docs/safety-and-consent.md +22 -17
- package/docs/spec-workflows-and-agents.md +5 -4
- package/docs/supported-stack.md +14 -17
- package/docs/troubleshooting.md +16 -8
- package/docs/workloads.md +9 -8
- package/package.json +1 -1
package/dist/commands.js
CHANGED
|
@@ -5,11 +5,11 @@ import { cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, rm, stat } from
|
|
|
5
5
|
import os from 'node:os';
|
|
6
6
|
import path from 'node:path';
|
|
7
7
|
import { getCommandHelp, getGeneralHelp, readBooleanFlag, readListFlag, readStringFlag } from './args.js';
|
|
8
|
-
import { getCodingAgent, getFrameworkDefinition, getSpecWorkflow, listRegions, patterns, providers, searchRegions } from './catalogs.js';
|
|
8
|
+
import { getCodingAgent, getEnvironment, getFrameworkDefinition, getSpecWorkflow, listRegions, patterns, providers, searchRegions } from './catalogs.js';
|
|
9
9
|
import { probeCodeAppsPlugin } from './code-apps-plugin.js';
|
|
10
10
|
import { initializeFramework } from './framework-adapters.js';
|
|
11
11
|
import { validateFrameworkInstallation } from './framework-validation.js';
|
|
12
|
-
import { artifactPath, applyProjectFileTransaction, assertNewOrEmptyDirectory, captureProjectFileSnapshot, findProjectRoot, loadManifest,
|
|
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
15
|
import { renderMigrationChecklist, renderMigrationProposal, renderMigrationTasks, seedMigrationGroups } from './migrate-plan.js';
|
|
@@ -166,7 +166,7 @@ async function initCommand(parsed, context) {
|
|
|
166
166
|
presentation.stage('Stage project files');
|
|
167
167
|
const staged = await withStagingArea(async (area) => {
|
|
168
168
|
const partition = partitionGeneratedArtifacts(buildArtifacts(plan));
|
|
169
|
-
await writeStagedArtifacts(area, partition.
|
|
169
|
+
await writeStagedArtifacts(area, partition.liftoff, 'liftoff');
|
|
170
170
|
presentation.stage('Initialize spec-driven framework', `${plan.specWorkflow.label} ${plan.framework.version}`);
|
|
171
171
|
await initializeFramework(area, plan, runner, {
|
|
172
172
|
env: context.env,
|
|
@@ -255,7 +255,13 @@ async function planCommand(parsed, context) {
|
|
|
255
255
|
const plan = buildProjectPlan(options, { requireProjectName: false });
|
|
256
256
|
const artifacts = buildArtifacts(plan);
|
|
257
257
|
presentation.definitions('Project decisions', projectPlanEntries(plan));
|
|
258
|
-
presentation.table(`Artifacts (${artifacts.length})`, ['Artifact', 'Path'], artifacts.map((artifact) => [
|
|
258
|
+
presentation.table(`Artifacts (${artifacts.length})`, ['Artifact', 'Lifecycle', 'Path'], artifacts.map((artifact) => [
|
|
259
|
+
artifact.logicalName,
|
|
260
|
+
artifact.lifecycle === 'project'
|
|
261
|
+
? `${artifact.lifecycle} (${artifact.provisioningGroup})`
|
|
262
|
+
: artifact.lifecycle,
|
|
263
|
+
artifact.pathParts.join('/')
|
|
264
|
+
]));
|
|
259
265
|
const workstationRows = selectWorkstationRequirements(plan).map((requirement) => [
|
|
260
266
|
requirement.definition.label,
|
|
261
267
|
requirement.exactVersion
|
|
@@ -458,7 +464,7 @@ async function handleProjectDependencies(plan, projectRoot, options, probes, con
|
|
|
458
464
|
if (plan.workload === 'power-apps-code-app') {
|
|
459
465
|
const issues = await verifyPowerAppsPackageMetadata(projectRoot);
|
|
460
466
|
if (issues.length > 0) {
|
|
461
|
-
presentation.error('Power Apps dependency installation blocked', `${issues.join(' ')}
|
|
467
|
+
presentation.error('Power Apps dependency installation blocked', `${issues.join(' ')} Repair the project-owned package.json and package-lock.json before installing dependencies.`);
|
|
462
468
|
return { success: false, deferred: [] };
|
|
463
469
|
}
|
|
464
470
|
}
|
|
@@ -617,12 +623,14 @@ function migrationPlanArtifacts(plan, inventory) {
|
|
|
617
623
|
{
|
|
618
624
|
logicalName: 'migration-proposal',
|
|
619
625
|
category: 'seed',
|
|
626
|
+
lifecycle: 'seed',
|
|
620
627
|
pathParts: ['openspec', 'changes', 'migrate-to-liftoff', 'proposal.md'],
|
|
621
628
|
content: renderMigrationProposal(plan, inventory)
|
|
622
629
|
},
|
|
623
630
|
{
|
|
624
631
|
logicalName: 'migration-tasks',
|
|
625
632
|
category: 'seed',
|
|
633
|
+
lifecycle: 'seed',
|
|
626
634
|
pathParts: ['openspec', 'changes', 'migrate-to-liftoff', 'tasks.md'],
|
|
627
635
|
content: renderMigrationTasks(groups)
|
|
628
636
|
}
|
|
@@ -634,6 +642,7 @@ function migrationPlanArtifacts(plan, inventory) {
|
|
|
634
642
|
artifacts: [{
|
|
635
643
|
logicalName: 'migration-checklist',
|
|
636
644
|
category: 'seed',
|
|
645
|
+
lifecycle: 'seed',
|
|
637
646
|
pathParts: ['MIGRATION.md'],
|
|
638
647
|
content: renderMigrationChecklist(plan, inventory, groups)
|
|
639
648
|
}],
|
|
@@ -729,7 +738,7 @@ async function executeMigration(parsed, context, sourceRoot) {
|
|
|
729
738
|
presentation.stage('Stage fresh migration project');
|
|
730
739
|
await withStagingArea(async (area) => {
|
|
731
740
|
const partition = partitionGeneratedArtifacts(buildArtifacts(plan));
|
|
732
|
-
await writeStagedArtifacts(area, partition.
|
|
741
|
+
await writeStagedArtifacts(area, partition.liftoff, 'liftoff');
|
|
733
742
|
presentation.stage('Initialize spec-driven framework', `${plan.specWorkflow.label} ${plan.framework.version}`);
|
|
734
743
|
await initializeFramework(area, plan, runner, {
|
|
735
744
|
env: context.env,
|
|
@@ -829,7 +838,7 @@ async function migrateCommand(parsed, context) {
|
|
|
829
838
|
return 1;
|
|
830
839
|
}
|
|
831
840
|
if (existsSync(path.join(sourceRoot, 'liftoff.manifest.json'))) {
|
|
832
|
-
context.presentation.error(`${sourceRoot} is already a Liftoff project.`, 'Use `liftoff update`
|
|
841
|
+
context.presentation.error(`${sourceRoot} is already a Liftoff project.`, 'Use `liftoff update` for managed-core maintenance; in-place project template migration is not automated.');
|
|
833
842
|
return 1;
|
|
834
843
|
}
|
|
835
844
|
context.presentation.identity('Migrate an existing application into a fresh Liftoff project');
|
|
@@ -885,6 +894,151 @@ function isDirtyGitWorktree(projectRoot) {
|
|
|
885
894
|
const result = spawnSync('git', ['status', '--porcelain'], { cwd: projectRoot, encoding: 'utf8' });
|
|
886
895
|
return result.status === 0 && result.stdout.trim().length > 0;
|
|
887
896
|
}
|
|
897
|
+
function requestedProvisioningGroups(manifest, plan) {
|
|
898
|
+
if (plan.workload === 'power-apps-code-app' ||
|
|
899
|
+
manifest.project.workload.kind === 'power-apps-code-app') {
|
|
900
|
+
return [];
|
|
901
|
+
}
|
|
902
|
+
const provisioned = new Set(manifest.projectArtifacts.map((artifact) => artifact.provisioningGroup));
|
|
903
|
+
const groups = [];
|
|
904
|
+
if (plan.includeFrontend &&
|
|
905
|
+
!manifest.project.workload.frontend &&
|
|
906
|
+
!provisioned.has('frontend')) {
|
|
907
|
+
groups.push('frontend');
|
|
908
|
+
}
|
|
909
|
+
const recordedEnvironments = new Set(manifest.project.workload.environments);
|
|
910
|
+
for (const environment of plan.environments) {
|
|
911
|
+
const group = `environment:${environment.id}`;
|
|
912
|
+
if (!recordedEnvironments.has(environment.id) &&
|
|
913
|
+
!provisioned.has(group)) {
|
|
914
|
+
groups.push(group);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
return groups;
|
|
918
|
+
}
|
|
919
|
+
async function inspectProvisioningGroups(projectRoot, render, groups) {
|
|
920
|
+
const projectArtifacts = render.filter((artifact) => artifact.lifecycle === 'project');
|
|
921
|
+
const plans = [];
|
|
922
|
+
for (const group of groups) {
|
|
923
|
+
const artifacts = projectArtifacts.filter((artifact) => artifact.provisioningGroup === group);
|
|
924
|
+
if (artifacts.length === 0) {
|
|
925
|
+
throw new Error(`Provisioning group ${group} did not render any project artifacts.`);
|
|
926
|
+
}
|
|
927
|
+
const entries = await Promise.all(artifacts.map(async (artifact) => {
|
|
928
|
+
const disk = await readProjectFile(projectRoot, artifact.pathParts);
|
|
929
|
+
if (disk === undefined) {
|
|
930
|
+
return {
|
|
931
|
+
group,
|
|
932
|
+
status: 'create',
|
|
933
|
+
rendered: artifact,
|
|
934
|
+
reason: 'destination is absent'
|
|
935
|
+
};
|
|
936
|
+
}
|
|
937
|
+
if (disk.toString('utf8') === artifact.content) {
|
|
938
|
+
return {
|
|
939
|
+
group,
|
|
940
|
+
status: 'adopt',
|
|
941
|
+
rendered: artifact,
|
|
942
|
+
reason: 'destination already matches the selected component'
|
|
943
|
+
};
|
|
944
|
+
}
|
|
945
|
+
return {
|
|
946
|
+
group,
|
|
947
|
+
status: 'conflict',
|
|
948
|
+
rendered: artifact,
|
|
949
|
+
reason: 'destination contains project-owned bytes'
|
|
950
|
+
};
|
|
951
|
+
}));
|
|
952
|
+
plans.push({
|
|
953
|
+
group,
|
|
954
|
+
entries,
|
|
955
|
+
blocked: entries.some((entry) => entry.status === 'conflict')
|
|
956
|
+
});
|
|
957
|
+
}
|
|
958
|
+
return plans;
|
|
959
|
+
}
|
|
960
|
+
function provisioningJson(plans) {
|
|
961
|
+
return plans.map((plan) => ({
|
|
962
|
+
group: plan.group,
|
|
963
|
+
status: plan.blocked ? 'blocked' : 'ready',
|
|
964
|
+
entries: plan.entries.map((entry) => ({
|
|
965
|
+
status: entry.status,
|
|
966
|
+
path: manifestDisplayPath(entry.rendered.pathParts),
|
|
967
|
+
reason: entry.reason
|
|
968
|
+
}))
|
|
969
|
+
}));
|
|
970
|
+
}
|
|
971
|
+
function projectProvenanceFor(artifact) {
|
|
972
|
+
return {
|
|
973
|
+
logicalName: artifact.logicalName,
|
|
974
|
+
category: artifact.category,
|
|
975
|
+
pathParts: artifact.pathParts,
|
|
976
|
+
generatedBy: liftoffVersion,
|
|
977
|
+
generationHash: `sha256:${createHash('sha256').update(artifact.content, 'utf8').digest('hex')}`,
|
|
978
|
+
provisioningGroup: artifact.provisioningGroup
|
|
979
|
+
};
|
|
980
|
+
}
|
|
981
|
+
function appendProvisionedProjectArtifacts(existing, plans) {
|
|
982
|
+
const byName = new Map(existing.map((artifact) => [artifact.logicalName, artifact]));
|
|
983
|
+
for (const plan of plans) {
|
|
984
|
+
if (plan.blocked) {
|
|
985
|
+
continue;
|
|
986
|
+
}
|
|
987
|
+
for (const entry of plan.entries) {
|
|
988
|
+
if (!byName.has(entry.rendered.logicalName)) {
|
|
989
|
+
byName.set(entry.rendered.logicalName, projectProvenanceFor(entry.rendered));
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
return [...byName.values()];
|
|
994
|
+
}
|
|
995
|
+
function sameWorkloadIntent(left, right) {
|
|
996
|
+
if (left.kind !== right.kind) {
|
|
997
|
+
return false;
|
|
998
|
+
}
|
|
999
|
+
if (left.kind === 'power-apps-code-app' && right.kind === 'power-apps-code-app') {
|
|
1000
|
+
return left.codeAppsPlugin === right.codeAppsPlugin &&
|
|
1001
|
+
left.starter.repository === right.starter.repository &&
|
|
1002
|
+
left.starter.path === right.starter.path &&
|
|
1003
|
+
left.starter.commit === right.starter.commit;
|
|
1004
|
+
}
|
|
1005
|
+
if (left.kind === 'power-apps-code-app' || right.kind === 'power-apps-code-app') {
|
|
1006
|
+
return false;
|
|
1007
|
+
}
|
|
1008
|
+
return left.apiStack === right.apiStack &&
|
|
1009
|
+
left.cloud === right.cloud &&
|
|
1010
|
+
left.region === right.region &&
|
|
1011
|
+
left.frontend === right.frontend &&
|
|
1012
|
+
left.environments.length === right.environments.length &&
|
|
1013
|
+
left.environments.every((environment, index) => environment === right.environments[index]) &&
|
|
1014
|
+
(left.kind !== 'genai' || right.kind !== 'genai' || left.pattern === right.pattern);
|
|
1015
|
+
}
|
|
1016
|
+
function planWithBlockedProvisioning(plan, recordedWorkload, provisioningPlans) {
|
|
1017
|
+
if (plan.workload === 'power-apps-code-app' ||
|
|
1018
|
+
recordedWorkload.kind === 'power-apps-code-app') {
|
|
1019
|
+
return plan;
|
|
1020
|
+
}
|
|
1021
|
+
const blockedGroups = new Set(provisioningPlans
|
|
1022
|
+
.filter((group) => group.blocked)
|
|
1023
|
+
.map((group) => group.group));
|
|
1024
|
+
const hasBlockedEnvironment = [...blockedGroups].some((group) => group.startsWith('environment:'));
|
|
1025
|
+
const environments = hasBlockedEnvironment
|
|
1026
|
+
? recordedWorkload.environments.map((id) => {
|
|
1027
|
+
const environment = getEnvironment(id);
|
|
1028
|
+
if (!environment) {
|
|
1029
|
+
throw new Error(`Recorded environment ${id} is no longer supported.`);
|
|
1030
|
+
}
|
|
1031
|
+
return environment;
|
|
1032
|
+
})
|
|
1033
|
+
: plan.environments;
|
|
1034
|
+
return {
|
|
1035
|
+
...plan,
|
|
1036
|
+
includeFrontend: blockedGroups.has('frontend')
|
|
1037
|
+
? recordedWorkload.frontend
|
|
1038
|
+
: plan.includeFrontend,
|
|
1039
|
+
environments
|
|
1040
|
+
};
|
|
1041
|
+
}
|
|
888
1042
|
async function preflightUpdate(projectRoot, entries, force) {
|
|
889
1043
|
for (const entry of entries) {
|
|
890
1044
|
const writesDestination = entry.status === 'new' ||
|
|
@@ -909,10 +1063,8 @@ async function captureUpdateSnapshots(projectRoot, manifest, render, initialSnap
|
|
|
909
1063
|
const snapshots = new Map(initialSnapshots.map((snapshot) => [updateSnapshotKey(snapshot.pathParts), snapshot]));
|
|
910
1064
|
const seenKeys = new Set(snapshots.keys());
|
|
911
1065
|
const candidates = [
|
|
912
|
-
...manifest.
|
|
913
|
-
...render
|
|
914
|
-
.filter((artifact) => artifact.category !== 'seed' && artifact.category !== 'framework')
|
|
915
|
-
.map((artifact) => artifact.pathParts)
|
|
1066
|
+
...manifest.managedArtifacts.map((artifact) => artifact.pathParts),
|
|
1067
|
+
...render.map((artifact) => artifact.pathParts)
|
|
916
1068
|
];
|
|
917
1069
|
const uncaptured = [];
|
|
918
1070
|
for (const pathParts of candidates) {
|
|
@@ -927,10 +1079,11 @@ async function captureUpdateSnapshots(projectRoot, manifest, render, initialSnap
|
|
|
927
1079
|
}
|
|
928
1080
|
return [...snapshots.values()];
|
|
929
1081
|
}
|
|
930
|
-
function selectUpdatePreconditions(snapshots, entries, mutations) {
|
|
1082
|
+
function selectUpdatePreconditions(snapshots, entries, mutations, additionalPaths = []) {
|
|
931
1083
|
const requiredKeys = new Set([
|
|
932
1084
|
updateSnapshotKey(['liftoff.config.json']),
|
|
933
|
-
...mutations.map((mutation) => updateSnapshotKey(mutation.pathParts))
|
|
1085
|
+
...mutations.map((mutation) => updateSnapshotKey(mutation.pathParts)),
|
|
1086
|
+
...additionalPaths.map((pathParts) => updateSnapshotKey(pathParts))
|
|
934
1087
|
]);
|
|
935
1088
|
for (const entry of entries) {
|
|
936
1089
|
const adoptsExistingDestination = entry.status === 'moved' && entry.destinationMatches === true;
|
|
@@ -945,12 +1098,41 @@ function selectUpdatePreconditions(snapshots, entries, mutations) {
|
|
|
945
1098
|
}
|
|
946
1099
|
return snapshots.filter((snapshot) => requiredKeys.has(updateSnapshotKey(snapshot.pathParts)));
|
|
947
1100
|
}
|
|
1101
|
+
function assertAuthorizedUpdateMutations(mutations, entries, provisioningPlans) {
|
|
1102
|
+
const authorized = new Set([
|
|
1103
|
+
updateSnapshotKey(['liftoff.manifest.json'])
|
|
1104
|
+
]);
|
|
1105
|
+
for (const entry of entries) {
|
|
1106
|
+
if (entry.rendered && entry.rendered.lifecycle !== 'managed-core') {
|
|
1107
|
+
throw new Error(`Update reconciliation produced a non-core mutation candidate: ${entry.logicalName}`);
|
|
1108
|
+
}
|
|
1109
|
+
authorized.add(updateSnapshotKey(entry.pathParts));
|
|
1110
|
+
if (entry.previousPathParts) {
|
|
1111
|
+
authorized.add(updateSnapshotKey(entry.previousPathParts));
|
|
1112
|
+
}
|
|
1113
|
+
}
|
|
1114
|
+
for (const plan of provisioningPlans) {
|
|
1115
|
+
if (plan.blocked) {
|
|
1116
|
+
continue;
|
|
1117
|
+
}
|
|
1118
|
+
for (const entry of plan.entries) {
|
|
1119
|
+
if (entry.status === 'create') {
|
|
1120
|
+
authorized.add(updateSnapshotKey(entry.rendered.pathParts));
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
for (const mutation of mutations) {
|
|
1125
|
+
if (!authorized.has(updateSnapshotKey(mutation.pathParts))) {
|
|
1126
|
+
throw new Error(`Update mutation is outside managed-core or authorized provisioning scope: ${manifestDisplayPath(mutation.pathParts)}`);
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
948
1130
|
async function updateCommand(parsed, context) {
|
|
949
1131
|
const { presentation } = context;
|
|
950
1132
|
const check = readBooleanFlag(parsed.flags, 'check') ?? false;
|
|
951
1133
|
const force = readBooleanFlag(parsed.flags, 'force') ?? false;
|
|
952
1134
|
const jsonMode = readBooleanFlag(parsed.flags, 'json') ?? false;
|
|
953
|
-
presentation.commandIdentity('update', 'Reconcile
|
|
1135
|
+
presentation.commandIdentity('update', 'Reconcile Liftoff-managed core files');
|
|
954
1136
|
if (force && check) {
|
|
955
1137
|
presentation.error('--force cannot be combined with --check.', 'Run `liftoff update --check` to inspect drift or `liftoff update --force` to overwrite conflicts.');
|
|
956
1138
|
return 1;
|
|
@@ -980,7 +1162,7 @@ async function updateCommand(parsed, context) {
|
|
|
980
1162
|
recordedWorkload.kind === 'power-apps-code-app';
|
|
981
1163
|
presentation.error(`Project type changes (${recordedWorkload.kind} -> ${plan.workload}) are not supported by update.`, involvesPowerApps
|
|
982
1164
|
? 'Initialize a fresh project for the new workload.'
|
|
983
|
-
: '
|
|
1165
|
+
: 'Initialize a fresh project and move production behavior through a reviewed project change.');
|
|
984
1166
|
return 1;
|
|
985
1167
|
}
|
|
986
1168
|
if (plan.workload === 'power-apps-code-app' && recordedWorkload.kind === 'power-apps-code-app') {
|
|
@@ -997,13 +1179,13 @@ async function updateCommand(parsed, context) {
|
|
|
997
1179
|
}
|
|
998
1180
|
else if (plan.workload !== 'power-apps-code-app' && recordedWorkload.kind !== 'power-apps-code-app') {
|
|
999
1181
|
if (plan.apiStack.id !== recordedWorkload.apiStack) {
|
|
1000
|
-
presentation.error(`API stack changes (${recordedWorkload.apiStack} -> ${plan.apiStack.id}) are a migration, not an update.`, '
|
|
1182
|
+
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.');
|
|
1001
1183
|
return 1;
|
|
1002
1184
|
}
|
|
1003
1185
|
const recordedPattern = recordedWorkload.kind === 'genai' ? recordedWorkload.pattern : undefined;
|
|
1004
1186
|
const desiredPattern = plan.workload === 'genai' ? plan.pattern.id : undefined;
|
|
1005
1187
|
if (desiredPattern !== recordedPattern) {
|
|
1006
|
-
presentation.error(`Pattern changes (${recordedPattern ?? 'none'} -> ${desiredPattern ?? 'none'}) are a migration, not an update.`, '
|
|
1188
|
+
presentation.error(`Pattern changes (${recordedPattern ?? 'none'} -> ${desiredPattern ?? 'none'}) are a migration, not an update.`, 'Use a separately reviewed project migration; the existing migrate command only adopts non-Liftoff sources into a fresh target.');
|
|
1007
1189
|
return 1;
|
|
1008
1190
|
}
|
|
1009
1191
|
}
|
|
@@ -1018,48 +1200,65 @@ async function updateCommand(parsed, context) {
|
|
|
1018
1200
|
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.');
|
|
1019
1201
|
return 1;
|
|
1020
1202
|
}
|
|
1021
|
-
|
|
1203
|
+
let desiredRenderPlan = manifest.framework.state === 'legacy'
|
|
1022
1204
|
? { ...plan, agents: [], defaultAgent: undefined }
|
|
1023
1205
|
: plan;
|
|
1206
|
+
if (desiredRenderPlan.workload === 'power-apps-code-app' &&
|
|
1207
|
+
recordedWorkload.kind === 'power-apps-code-app') {
|
|
1208
|
+
desiredRenderPlan = {
|
|
1209
|
+
...desiredRenderPlan,
|
|
1210
|
+
starter: recordedWorkload.starter
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
const desiredRender = buildArtifacts(desiredRenderPlan);
|
|
1214
|
+
const provisioningPlans = await inspectProvisioningGroups(projectRoot, desiredRender, requestedProvisioningGroups(manifest, desiredRenderPlan));
|
|
1215
|
+
const renderPlan = planWithBlockedProvisioning(desiredRenderPlan, recordedWorkload, provisioningPlans);
|
|
1024
1216
|
const render = buildArtifacts(renderPlan);
|
|
1217
|
+
const scopedRender = [
|
|
1218
|
+
...render.filter((artifact) => artifact.lifecycle === 'managed-core'),
|
|
1219
|
+
...desiredRender.filter((artifact) => artifact.lifecycle === 'project' &&
|
|
1220
|
+
provisioningPlans.some((group) => group.group === artifact.provisioningGroup))
|
|
1221
|
+
];
|
|
1025
1222
|
const updateSnapshots = check
|
|
1026
1223
|
? []
|
|
1027
|
-
: await captureUpdateSnapshots(projectRoot, manifest,
|
|
1224
|
+
: await captureUpdateSnapshots(projectRoot, manifest, scopedRender, initialUpdateSnapshots);
|
|
1028
1225
|
const entries = await reconcileProject(manifest, render, projectRoot);
|
|
1029
1226
|
const summary = summarizeEntries(entries);
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
const
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1227
|
+
const ownershipMigrationPending = manifest.artifactVersion !== 6 ||
|
|
1228
|
+
manifestHadFilteredLegacyNonDurableOwnership(manifest);
|
|
1229
|
+
const plannedManifest = buildManifest(renderPlan, render, {
|
|
1230
|
+
frameworkState: manifest.framework.state,
|
|
1231
|
+
projectArtifacts: manifest.projectArtifacts
|
|
1232
|
+
});
|
|
1233
|
+
const workloadIntentChanged = !sameWorkloadIntent(plannedManifest.project.workload, manifest.project.workload);
|
|
1234
|
+
const manifestRewritePending = ownershipMigrationPending ||
|
|
1235
|
+
workloadIntentChanged;
|
|
1236
|
+
const drift = hasDrift(entries) ||
|
|
1237
|
+
manifestRewritePending ||
|
|
1238
|
+
provisioningPlans.length > 0;
|
|
1042
1239
|
const visible = entries.filter((entry) => entry.status !== 'unchanged' || entry.refreshHash);
|
|
1043
1240
|
if (!drift) {
|
|
1044
1241
|
if (jsonMode) {
|
|
1045
1242
|
presentation.rawStdout(`${JSON.stringify(check ? {
|
|
1046
|
-
schemaVersion:
|
|
1243
|
+
schemaVersion: 2,
|
|
1047
1244
|
mode: 'check',
|
|
1245
|
+
scope: 'managed-core',
|
|
1048
1246
|
cliVersion: liftoffVersion,
|
|
1049
1247
|
projectVersion: manifest.liftoffVersion,
|
|
1050
1248
|
entries: [],
|
|
1051
|
-
|
|
1052
|
-
|
|
1249
|
+
provisioning: [],
|
|
1250
|
+
ownershipMigrationPending,
|
|
1053
1251
|
summary
|
|
1054
1252
|
} : {
|
|
1055
|
-
schemaVersion:
|
|
1253
|
+
schemaVersion: 2,
|
|
1056
1254
|
mode: 'apply',
|
|
1255
|
+
scope: 'managed-core',
|
|
1057
1256
|
cliVersion: liftoffVersion,
|
|
1058
1257
|
projectVersion: manifest.liftoffVersion,
|
|
1059
1258
|
written: [],
|
|
1060
1259
|
skipped: [],
|
|
1061
|
-
|
|
1062
|
-
|
|
1260
|
+
provisioning: [],
|
|
1261
|
+
ownershipMigrationPending,
|
|
1063
1262
|
summary
|
|
1064
1263
|
}, null, 2)}\n`);
|
|
1065
1264
|
return 0;
|
|
@@ -1068,14 +1267,15 @@ async function updateCommand(parsed, context) {
|
|
|
1068
1267
|
{ label: 'Liftoff CLI', value: liftoffVersion },
|
|
1069
1268
|
{ label: 'Project generated by', value: manifest.liftoffVersion }
|
|
1070
1269
|
]);
|
|
1071
|
-
presentation.status('success', '
|
|
1270
|
+
presentation.status('success', 'Liftoff core is current', `${summary.unchanged} managed-core artifacts match; project files are not compared`);
|
|
1072
1271
|
return 0;
|
|
1073
1272
|
}
|
|
1074
1273
|
if (check) {
|
|
1075
1274
|
if (jsonMode) {
|
|
1076
1275
|
presentation.rawStdout(`${JSON.stringify({
|
|
1077
|
-
schemaVersion:
|
|
1276
|
+
schemaVersion: 2,
|
|
1078
1277
|
mode: 'check',
|
|
1278
|
+
scope: 'managed-core',
|
|
1079
1279
|
cliVersion: liftoffVersion,
|
|
1080
1280
|
projectVersion: manifest.liftoffVersion,
|
|
1081
1281
|
entries: visible.map((entry) => ({
|
|
@@ -1085,8 +1285,8 @@ async function updateCommand(parsed, context) {
|
|
|
1085
1285
|
previousPath: entry.previousPathParts ? manifestDisplayPath(entry.previousPathParts) : undefined,
|
|
1086
1286
|
reason: entry.reason
|
|
1087
1287
|
})),
|
|
1088
|
-
|
|
1089
|
-
|
|
1288
|
+
provisioning: provisioningJson(provisioningPlans),
|
|
1289
|
+
ownershipMigrationPending,
|
|
1090
1290
|
summary
|
|
1091
1291
|
}, null, 2)}\n`);
|
|
1092
1292
|
return 2;
|
|
@@ -1097,19 +1297,21 @@ async function updateCommand(parsed, context) {
|
|
|
1097
1297
|
]);
|
|
1098
1298
|
if (visible.length > 0) {
|
|
1099
1299
|
if (presentation.stdout.layout === 'plain') {
|
|
1100
|
-
presentation.section('
|
|
1300
|
+
presentation.section('Liftoff core drift', visible.map((entry) => `${entryMarker(entry)} ${entryDisplay(entry)} ${entry.reason}`));
|
|
1101
1301
|
}
|
|
1102
1302
|
else {
|
|
1103
|
-
presentation.table('
|
|
1303
|
+
presentation.table('Liftoff core drift', ['Change', 'Artifact', 'Reason'], visible.map((entry) => [entryMarker(entry), entryDisplay(entry), entry.reason]));
|
|
1104
1304
|
}
|
|
1105
1305
|
}
|
|
1306
|
+
for (const group of provisioningPlans) {
|
|
1307
|
+
presentation.bullets(`Project component provisioning: ${group.group}`, group.entries.map((entry) => `${entry.status} ${manifestDisplayPath(entry.rendered.pathParts)} ${entry.reason}`));
|
|
1308
|
+
}
|
|
1106
1309
|
const manifestMaintenance = [
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
? 'remove obsolete seed or framework entries from durable manifest ownership'
|
|
1310
|
+
ownershipMigrationPending
|
|
1311
|
+
? 'release legacy project artifacts from Liftoff update authority in manifest schema v6; no production file will be written'
|
|
1110
1312
|
: undefined,
|
|
1111
|
-
|
|
1112
|
-
?
|
|
1313
|
+
workloadIntentChanged
|
|
1314
|
+
? 'record the requested project configuration intent after safe provisioning'
|
|
1113
1315
|
: undefined
|
|
1114
1316
|
].filter((item) => item !== undefined);
|
|
1115
1317
|
if (manifestMaintenance.length > 0) {
|
|
@@ -1120,8 +1322,11 @@ async function updateCommand(parsed, context) {
|
|
|
1120
1322
|
summary.upgrade +
|
|
1121
1323
|
summary.moved +
|
|
1122
1324
|
summary.refresh +
|
|
1325
|
+
provisioningPlans.reduce((count, group) => count + (group.blocked
|
|
1326
|
+
? 0
|
|
1327
|
+
: group.entries.filter((entry) => entry.status === 'create').length), 0) +
|
|
1123
1328
|
(manifestRewritePending ? 1 : 0);
|
|
1124
|
-
presentation.status('warning', '
|
|
1329
|
+
presentation.status('warning', 'Liftoff core maintenance available', `${toWrite} to write, ${summary.conflict} core conflict(s), ${summary.orphan} core orphan(s), ${summary.unchanged} core unchanged`);
|
|
1125
1330
|
if (toWrite > 0) {
|
|
1126
1331
|
presentation.command('liftoff update');
|
|
1127
1332
|
}
|
|
@@ -1130,6 +1335,7 @@ async function updateCommand(parsed, context) {
|
|
|
1130
1335
|
}
|
|
1131
1336
|
return 2;
|
|
1132
1337
|
}
|
|
1338
|
+
const blockedProvisioning = provisioningPlans.filter((group) => group.blocked);
|
|
1133
1339
|
if (isDirtyGitWorktree(projectRoot)) {
|
|
1134
1340
|
const warning = 'The project worktree has uncommitted changes; consider committing before applying.';
|
|
1135
1341
|
if (jsonMode) {
|
|
@@ -1139,7 +1345,7 @@ async function updateCommand(parsed, context) {
|
|
|
1139
1345
|
presentation.warning(warning);
|
|
1140
1346
|
}
|
|
1141
1347
|
}
|
|
1142
|
-
presentation.stage('Apply safe
|
|
1348
|
+
presentation.stage('Apply safe Liftoff core changes', projectRoot);
|
|
1143
1349
|
await preflightUpdate(projectRoot, entries, force);
|
|
1144
1350
|
const written = [];
|
|
1145
1351
|
const skipped = [];
|
|
@@ -1192,12 +1398,32 @@ async function updateCommand(parsed, context) {
|
|
|
1192
1398
|
break;
|
|
1193
1399
|
}
|
|
1194
1400
|
}
|
|
1195
|
-
const
|
|
1401
|
+
const provisioned = [];
|
|
1402
|
+
for (const group of provisioningPlans) {
|
|
1403
|
+
if (group.blocked) {
|
|
1404
|
+
continue;
|
|
1405
|
+
}
|
|
1406
|
+
for (const entry of group.entries) {
|
|
1407
|
+
if (entry.status === 'create') {
|
|
1408
|
+
mutations.push({
|
|
1409
|
+
type: 'write',
|
|
1410
|
+
pathParts: entry.rendered.pathParts,
|
|
1411
|
+
content: entry.rendered.content
|
|
1412
|
+
});
|
|
1413
|
+
}
|
|
1414
|
+
provisioned.push(entry);
|
|
1415
|
+
}
|
|
1416
|
+
}
|
|
1417
|
+
const oldByName = new Map(manifest.managedArtifacts.map((artifact) => [artifact.logicalName, artifact]));
|
|
1196
1418
|
const skippedByName = new Map(skipped.map((entry) => [entry.logicalName, entry]));
|
|
1197
1419
|
const hasUnrecordedGovernanceConflict = skipped.some((entry) => entry.status === 'conflict' &&
|
|
1198
1420
|
entry.rendered?.category === 'governance' &&
|
|
1199
1421
|
!oldByName.has(entry.logicalName));
|
|
1200
|
-
const
|
|
1422
|
+
const nextProjectArtifacts = appendProvisionedProjectArtifacts(manifest.projectArtifacts, provisioningPlans);
|
|
1423
|
+
const nextManifest = buildManifest(renderPlan, render.filter((artifact) => artifact.logicalName !== 'manifest'), {
|
|
1424
|
+
frameworkState: manifest.framework.state,
|
|
1425
|
+
projectArtifacts: nextProjectArtifacts
|
|
1426
|
+
});
|
|
1201
1427
|
nextManifest.framework = manifest.framework;
|
|
1202
1428
|
nextManifest.project.specWorkflow = manifest.project.specWorkflow;
|
|
1203
1429
|
nextManifest.project.agents = manifest.project.agents;
|
|
@@ -1211,11 +1437,7 @@ async function updateCommand(parsed, context) {
|
|
|
1211
1437
|
nextManifest.governance.profile !== 'none') {
|
|
1212
1438
|
nextManifest.governance.state = 'handoff-partial';
|
|
1213
1439
|
}
|
|
1214
|
-
nextManifest.
|
|
1215
|
-
// config is user-owned after create: carry the recorded entry forward untouched
|
|
1216
|
-
if (artifact.logicalName === 'liftoff-config') {
|
|
1217
|
-
return [oldByName.get('liftoff-config') ?? artifact];
|
|
1218
|
-
}
|
|
1440
|
+
nextManifest.managedArtifacts = nextManifest.managedArtifacts.flatMap((artifact) => {
|
|
1219
1441
|
if (!skippedByName.has(artifact.logicalName)) {
|
|
1220
1442
|
return [artifact];
|
|
1221
1443
|
}
|
|
@@ -1232,7 +1454,7 @@ async function updateCommand(parsed, context) {
|
|
|
1232
1454
|
previous.category === 'governance') {
|
|
1233
1455
|
continue;
|
|
1234
1456
|
}
|
|
1235
|
-
nextManifest.
|
|
1457
|
+
nextManifest.managedArtifacts.push(previous);
|
|
1236
1458
|
}
|
|
1237
1459
|
}
|
|
1238
1460
|
mutations.push({
|
|
@@ -1240,34 +1462,52 @@ async function updateCommand(parsed, context) {
|
|
|
1240
1462
|
pathParts: ['liftoff.manifest.json'],
|
|
1241
1463
|
content: `${JSON.stringify(nextManifest, null, 2)}\n`
|
|
1242
1464
|
});
|
|
1465
|
+
assertAuthorizedUpdateMutations(mutations, entries, provisioningPlans);
|
|
1243
1466
|
await applyProjectFileTransaction(projectRoot, mutations, {
|
|
1244
|
-
preconditions: selectUpdatePreconditions(updateSnapshots, entries, mutations)
|
|
1467
|
+
preconditions: selectUpdatePreconditions(updateSnapshots, entries, mutations, provisioned.map((entry) => entry.rendered.pathParts))
|
|
1245
1468
|
});
|
|
1246
1469
|
if (jsonMode) {
|
|
1247
1470
|
presentation.rawStdout(`${JSON.stringify({
|
|
1248
|
-
schemaVersion:
|
|
1471
|
+
schemaVersion: 2,
|
|
1249
1472
|
mode: 'apply',
|
|
1473
|
+
scope: 'managed-core',
|
|
1474
|
+
status: blockedProvisioning.length > 0 ? 'partial' : 'applied',
|
|
1250
1475
|
cliVersion: liftoffVersion,
|
|
1251
1476
|
projectVersion: manifest.liftoffVersion,
|
|
1252
1477
|
written: written.map((entry) => manifestDisplayPath(entry.pathParts)),
|
|
1253
1478
|
skipped: skipped.map((entry) => ({ path: manifestDisplayPath(entry.pathParts), reason: entry.reason })),
|
|
1254
|
-
|
|
1255
|
-
|
|
1479
|
+
provisioning: provisioningJson(provisioningPlans),
|
|
1480
|
+
ownershipMigrationPending,
|
|
1256
1481
|
summary
|
|
1257
1482
|
}, null, 2)}\n`);
|
|
1258
1483
|
return 0;
|
|
1259
1484
|
}
|
|
1260
1485
|
if (written.length > 0) {
|
|
1261
|
-
presentation.bullets('Applied changes', written.map((entry) => `wrote ${entryDisplay(entry)}`));
|
|
1486
|
+
presentation.bullets('Applied Liftoff core changes', written.map((entry) => `wrote ${entryDisplay(entry)}`));
|
|
1487
|
+
}
|
|
1488
|
+
if (provisioned.length > 0) {
|
|
1489
|
+
presentation.bullets('Provisioned project components', provisioned.map((entry) => `${entry.status === 'create' ? 'wrote' : 'adopted'} ${manifestDisplayPath(entry.rendered.pathParts)}`));
|
|
1490
|
+
}
|
|
1491
|
+
if (blockedProvisioning.length > 0) {
|
|
1492
|
+
presentation.bullets('Blocked project component provisioning', blockedProvisioning.flatMap((group) => group.entries
|
|
1493
|
+
.filter((entry) => entry.status === 'conflict')
|
|
1494
|
+
.map((entry) => `${group.group}: preserved ${manifestDisplayPath(entry.rendered.pathParts)}; --force cannot overwrite project-owned bytes`)));
|
|
1262
1495
|
}
|
|
1263
1496
|
if (skipped.length > 0) {
|
|
1264
|
-
presentation.bullets('Skipped conflicts', skipped.map((entry) => `skipped ${entryDisplay(entry)} ${entry.reason}${force ? '' : ' (use --force to overwrite)'}`));
|
|
1497
|
+
presentation.bullets('Skipped Liftoff core conflicts', skipped.map((entry) => `skipped ${entryDisplay(entry)} ${entry.reason}${force ? '' : ' (use --force to overwrite)'}`));
|
|
1265
1498
|
}
|
|
1266
1499
|
const orphans = entries.filter((entry) => entry.status === 'orphan');
|
|
1267
1500
|
if (orphans.length > 0) {
|
|
1268
|
-
presentation.bullets('Orphaned artifacts', orphans.map((entry) => `orphan ${entryDisplay(entry)} ${entry.reason}`));
|
|
1501
|
+
presentation.bullets('Orphaned Liftoff core artifacts', orphans.map((entry) => `orphan ${entryDisplay(entry)} ${entry.reason}`));
|
|
1269
1502
|
}
|
|
1270
|
-
presentation.completion('Updated project',
|
|
1503
|
+
presentation.completion('Updated project', [
|
|
1504
|
+
`${written.length} core written`,
|
|
1505
|
+
`${provisioned.length} project provisioned`,
|
|
1506
|
+
...(blockedProvisioning.length > 0
|
|
1507
|
+
? [`${blockedProvisioning.length} provisioning group(s) blocked`]
|
|
1508
|
+
: []),
|
|
1509
|
+
`${skipped.length} core skipped`
|
|
1510
|
+
].join(', '), [{ label: 'Manifest version', value: liftoffVersion }], 'liftoff validate && liftoff doctor');
|
|
1271
1511
|
return 0;
|
|
1272
1512
|
}
|
|
1273
1513
|
function renderSelfUpgradeResult(value, presentation) {
|
|
@@ -1514,7 +1754,7 @@ function stackProjectCheck(projectRoot, apiStack) {
|
|
|
1514
1754
|
label: `${apiStack} project`,
|
|
1515
1755
|
severity: 'fail',
|
|
1516
1756
|
detail: `missing locked dependency metadata: ${missingMetadata}`,
|
|
1517
|
-
remedy: `restore ${missingMetadata}
|
|
1757
|
+
remedy: `restore or repair the project-owned ${missingMetadata}`
|
|
1518
1758
|
};
|
|
1519
1759
|
}
|
|
1520
1760
|
if (apiStack === 'python-fastapi') {
|
|
@@ -1527,7 +1767,7 @@ function stackProjectCheck(projectRoot, apiStack) {
|
|
|
1527
1767
|
label: `${apiStack} project`,
|
|
1528
1768
|
severity: 'fail',
|
|
1529
1769
|
detail: 'pyproject.toml and uv.lock do not identify the same project',
|
|
1530
|
-
remedy: '
|
|
1770
|
+
remedy: 'repair the project-owned backend/pyproject.toml and backend/uv.lock'
|
|
1531
1771
|
};
|
|
1532
1772
|
}
|
|
1533
1773
|
}
|
|
@@ -1579,7 +1819,7 @@ async function powerAppsProjectCheck(projectRoot) {
|
|
|
1579
1819
|
severity: 'fail',
|
|
1580
1820
|
state: 'unhealthy',
|
|
1581
1821
|
detail: issues[0],
|
|
1582
|
-
remedy: '
|
|
1822
|
+
remedy: 'repair the project-owned package.json and package-lock.json'
|
|
1583
1823
|
};
|
|
1584
1824
|
}
|
|
1585
1825
|
function runReadOnly(command, args, cwd) {
|
|
@@ -1660,11 +1900,15 @@ async function projectLayer(projectRoot, manifest) {
|
|
|
1660
1900
|
label: 'manifest',
|
|
1661
1901
|
severity: 'fail',
|
|
1662
1902
|
detail: `${issues.length} issue(s): ${issues[0]}${issues.length > 1 ? ' ...' : ''}`,
|
|
1663
|
-
remedy: 'restore
|
|
1903
|
+
remedy: 'restore the manifest or run liftoff update for managed-core repair'
|
|
1664
1904
|
});
|
|
1665
1905
|
}
|
|
1666
1906
|
else {
|
|
1667
|
-
checks.push({
|
|
1907
|
+
checks.push({
|
|
1908
|
+
label: 'manifest',
|
|
1909
|
+
severity: 'ok',
|
|
1910
|
+
detail: `valid, ${manifest.managedArtifacts.length} managed core and ${manifest.projectArtifacts.length} project provenance entries`
|
|
1911
|
+
});
|
|
1668
1912
|
}
|
|
1669
1913
|
if (compareSemver(manifest.liftoffVersion, liftoffVersion) > 0) {
|
|
1670
1914
|
checks.push({
|
|
@@ -1738,22 +1982,29 @@ async function projectLayer(projectRoot, manifest) {
|
|
|
1738
1982
|
const plan = buildProjectPlan(config, { requireProjectName: true });
|
|
1739
1983
|
const render = buildArtifacts(plan);
|
|
1740
1984
|
const entries = await reconcileProject(manifest, render, projectRoot);
|
|
1741
|
-
const
|
|
1985
|
+
const provisioningPlans = await inspectProvisioningGroups(projectRoot, render, requestedProvisioningGroups(manifest, plan));
|
|
1986
|
+
const driftCount = entries.filter((entry) => entry.status !== 'unchanged' || entry.refreshHash).length +
|
|
1987
|
+
provisioningPlans.length +
|
|
1988
|
+
(manifest.artifactVersion === 6 ? 0 : 1);
|
|
1742
1989
|
if (driftCount > 0) {
|
|
1743
1990
|
checks.push({
|
|
1744
|
-
label: '
|
|
1991
|
+
label: 'managed core',
|
|
1745
1992
|
severity: 'warn',
|
|
1746
|
-
detail: `${driftCount}
|
|
1993
|
+
detail: `${driftCount} core maintenance action(s) available`,
|
|
1747
1994
|
remedy: 'run liftoff update'
|
|
1748
1995
|
});
|
|
1749
1996
|
}
|
|
1750
1997
|
else {
|
|
1751
|
-
checks.push({
|
|
1998
|
+
checks.push({
|
|
1999
|
+
label: 'managed core',
|
|
2000
|
+
severity: 'ok',
|
|
2001
|
+
detail: 'Liftoff core is current; project templates are not compared'
|
|
2002
|
+
});
|
|
1752
2003
|
}
|
|
1753
2004
|
}
|
|
1754
2005
|
catch (error) {
|
|
1755
2006
|
checks.push({
|
|
1756
|
-
label: '
|
|
2007
|
+
label: 'managed core',
|
|
1757
2008
|
severity: 'fail',
|
|
1758
2009
|
detail: `liftoff.config.json could not be evaluated: ${error.message.split('\n')[0]}`,
|
|
1759
2010
|
remedy: 'repair liftoff.config.json'
|