@msn-control/liftoff 0.5.0 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -10,7 +10,7 @@
10
10
  },
11
11
  "dependencies": {
12
12
  "@vitejs/plugin-vue": "^5.0.5",
13
- "vite": "^5.3.1",
13
+ "vite": "^6.4.3",
14
14
  "vue": "^3.4.29",
15
15
  "tailwindcss": "^3.4.4",
16
16
  "autoprefixer": "^10.4.19",
@@ -10,7 +10,7 @@
10
10
  "dependencies": {
11
11
  "@fastify/cors": "^10.0.1",
12
12
  "@fastify/swagger": "^9.4.0",
13
- "drizzle-orm": "^0.44.0",
13
+ "drizzle-orm": "^0.45.2",
14
14
  "fastify": "^5.4.0",
15
15
  "pg": "^8.16.0"
16
16
  },
@@ -1758,9 +1758,9 @@
1758
1758
  }
1759
1759
  },
1760
1760
  "node_modules/drizzle-orm": {
1761
- "version": "0.44.7",
1762
- "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.44.7.tgz",
1763
- "integrity": "sha512-quIpnYznjU9lHshEOAYLoZ9s3jweleHlZIAWR/jX9gAWNg/JhQ1wj0KGRf7/Zm+obRrYd9GjPVJg790QY9N5AQ==",
1761
+ "version": "0.45.2",
1762
+ "resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.2.tgz",
1763
+ "integrity": "sha512-kY0BSaTNYWnoDMVoyY8uxmyHjpJW1geOmBMdSSicKo9CIIWkSxMIj2rkeSR51b8KAPB7m+qysjuHme5nKP+E5Q==",
1764
1764
  "license": "Apache-2.0",
1765
1765
  "peerDependencies": {
1766
1766
  "@aws-sdk/client-rds-data": ">=3",
@@ -3751,11 +3751,13 @@
3751
3751
  "cpu": [
3752
3752
  "ppc64"
3753
3753
  ],
3754
- "extraneous": true,
3754
+ "dev": true,
3755
3755
  "license": "MIT",
3756
+ "optional": true,
3756
3757
  "os": [
3757
3758
  "aix"
3758
3759
  ],
3760
+ "peer": true,
3759
3761
  "engines": {
3760
3762
  "node": ">=18"
3761
3763
  }
@@ -3767,11 +3769,13 @@
3767
3769
  "cpu": [
3768
3770
  "arm"
3769
3771
  ],
3770
- "extraneous": true,
3772
+ "dev": true,
3771
3773
  "license": "MIT",
3774
+ "optional": true,
3772
3775
  "os": [
3773
3776
  "android"
3774
3777
  ],
3778
+ "peer": true,
3775
3779
  "engines": {
3776
3780
  "node": ">=18"
3777
3781
  }
@@ -3783,11 +3787,13 @@
3783
3787
  "cpu": [
3784
3788
  "arm64"
3785
3789
  ],
3786
- "extraneous": true,
3790
+ "dev": true,
3787
3791
  "license": "MIT",
3792
+ "optional": true,
3788
3793
  "os": [
3789
3794
  "android"
3790
3795
  ],
3796
+ "peer": true,
3791
3797
  "engines": {
3792
3798
  "node": ">=18"
3793
3799
  }
@@ -3799,11 +3805,13 @@
3799
3805
  "cpu": [
3800
3806
  "x64"
3801
3807
  ],
3802
- "extraneous": true,
3808
+ "dev": true,
3803
3809
  "license": "MIT",
3810
+ "optional": true,
3804
3811
  "os": [
3805
3812
  "android"
3806
3813
  ],
3814
+ "peer": true,
3807
3815
  "engines": {
3808
3816
  "node": ">=18"
3809
3817
  }
@@ -3815,11 +3823,13 @@
3815
3823
  "cpu": [
3816
3824
  "arm64"
3817
3825
  ],
3818
- "extraneous": true,
3826
+ "dev": true,
3819
3827
  "license": "MIT",
3828
+ "optional": true,
3820
3829
  "os": [
3821
3830
  "darwin"
3822
3831
  ],
3832
+ "peer": true,
3823
3833
  "engines": {
3824
3834
  "node": ">=18"
3825
3835
  }
@@ -3831,11 +3841,13 @@
3831
3841
  "cpu": [
3832
3842
  "x64"
3833
3843
  ],
3834
- "extraneous": true,
3844
+ "dev": true,
3835
3845
  "license": "MIT",
3846
+ "optional": true,
3836
3847
  "os": [
3837
3848
  "darwin"
3838
3849
  ],
3850
+ "peer": true,
3839
3851
  "engines": {
3840
3852
  "node": ">=18"
3841
3853
  }
@@ -3847,11 +3859,13 @@
3847
3859
  "cpu": [
3848
3860
  "arm64"
3849
3861
  ],
3850
- "extraneous": true,
3862
+ "dev": true,
3851
3863
  "license": "MIT",
3864
+ "optional": true,
3852
3865
  "os": [
3853
3866
  "freebsd"
3854
3867
  ],
3868
+ "peer": true,
3855
3869
  "engines": {
3856
3870
  "node": ">=18"
3857
3871
  }
@@ -3863,11 +3877,13 @@
3863
3877
  "cpu": [
3864
3878
  "x64"
3865
3879
  ],
3866
- "extraneous": true,
3880
+ "dev": true,
3867
3881
  "license": "MIT",
3882
+ "optional": true,
3868
3883
  "os": [
3869
3884
  "freebsd"
3870
3885
  ],
3886
+ "peer": true,
3871
3887
  "engines": {
3872
3888
  "node": ">=18"
3873
3889
  }
@@ -3879,11 +3895,13 @@
3879
3895
  "cpu": [
3880
3896
  "arm"
3881
3897
  ],
3882
- "extraneous": true,
3898
+ "dev": true,
3883
3899
  "license": "MIT",
3900
+ "optional": true,
3884
3901
  "os": [
3885
3902
  "linux"
3886
3903
  ],
3904
+ "peer": true,
3887
3905
  "engines": {
3888
3906
  "node": ">=18"
3889
3907
  }
@@ -3895,11 +3913,13 @@
3895
3913
  "cpu": [
3896
3914
  "arm64"
3897
3915
  ],
3898
- "extraneous": true,
3916
+ "dev": true,
3899
3917
  "license": "MIT",
3918
+ "optional": true,
3900
3919
  "os": [
3901
3920
  "linux"
3902
3921
  ],
3922
+ "peer": true,
3903
3923
  "engines": {
3904
3924
  "node": ">=18"
3905
3925
  }
@@ -3911,11 +3931,13 @@
3911
3931
  "cpu": [
3912
3932
  "ia32"
3913
3933
  ],
3914
- "extraneous": true,
3934
+ "dev": true,
3915
3935
  "license": "MIT",
3936
+ "optional": true,
3916
3937
  "os": [
3917
3938
  "linux"
3918
3939
  ],
3940
+ "peer": true,
3919
3941
  "engines": {
3920
3942
  "node": ">=18"
3921
3943
  }
@@ -3927,11 +3949,13 @@
3927
3949
  "cpu": [
3928
3950
  "loong64"
3929
3951
  ],
3930
- "extraneous": true,
3952
+ "dev": true,
3931
3953
  "license": "MIT",
3954
+ "optional": true,
3932
3955
  "os": [
3933
3956
  "linux"
3934
3957
  ],
3958
+ "peer": true,
3935
3959
  "engines": {
3936
3960
  "node": ">=18"
3937
3961
  }
@@ -3943,11 +3967,13 @@
3943
3967
  "cpu": [
3944
3968
  "mips64el"
3945
3969
  ],
3946
- "extraneous": true,
3970
+ "dev": true,
3947
3971
  "license": "MIT",
3972
+ "optional": true,
3948
3973
  "os": [
3949
3974
  "linux"
3950
3975
  ],
3976
+ "peer": true,
3951
3977
  "engines": {
3952
3978
  "node": ">=18"
3953
3979
  }
@@ -3959,11 +3985,13 @@
3959
3985
  "cpu": [
3960
3986
  "ppc64"
3961
3987
  ],
3962
- "extraneous": true,
3988
+ "dev": true,
3963
3989
  "license": "MIT",
3990
+ "optional": true,
3964
3991
  "os": [
3965
3992
  "linux"
3966
3993
  ],
3994
+ "peer": true,
3967
3995
  "engines": {
3968
3996
  "node": ">=18"
3969
3997
  }
@@ -3975,11 +4003,13 @@
3975
4003
  "cpu": [
3976
4004
  "riscv64"
3977
4005
  ],
3978
- "extraneous": true,
4006
+ "dev": true,
3979
4007
  "license": "MIT",
4008
+ "optional": true,
3980
4009
  "os": [
3981
4010
  "linux"
3982
4011
  ],
4012
+ "peer": true,
3983
4013
  "engines": {
3984
4014
  "node": ">=18"
3985
4015
  }
@@ -3991,11 +4021,13 @@
3991
4021
  "cpu": [
3992
4022
  "s390x"
3993
4023
  ],
3994
- "extraneous": true,
4024
+ "dev": true,
3995
4025
  "license": "MIT",
4026
+ "optional": true,
3996
4027
  "os": [
3997
4028
  "linux"
3998
4029
  ],
4030
+ "peer": true,
3999
4031
  "engines": {
4000
4032
  "node": ">=18"
4001
4033
  }
@@ -4007,11 +4039,13 @@
4007
4039
  "cpu": [
4008
4040
  "x64"
4009
4041
  ],
4010
- "extraneous": true,
4042
+ "dev": true,
4011
4043
  "license": "MIT",
4044
+ "optional": true,
4012
4045
  "os": [
4013
4046
  "linux"
4014
4047
  ],
4048
+ "peer": true,
4015
4049
  "engines": {
4016
4050
  "node": ">=18"
4017
4051
  }
@@ -4023,11 +4057,13 @@
4023
4057
  "cpu": [
4024
4058
  "arm64"
4025
4059
  ],
4026
- "extraneous": true,
4060
+ "dev": true,
4027
4061
  "license": "MIT",
4062
+ "optional": true,
4028
4063
  "os": [
4029
4064
  "netbsd"
4030
4065
  ],
4066
+ "peer": true,
4031
4067
  "engines": {
4032
4068
  "node": ">=18"
4033
4069
  }
@@ -4039,11 +4075,13 @@
4039
4075
  "cpu": [
4040
4076
  "x64"
4041
4077
  ],
4042
- "extraneous": true,
4078
+ "dev": true,
4043
4079
  "license": "MIT",
4080
+ "optional": true,
4044
4081
  "os": [
4045
4082
  "netbsd"
4046
4083
  ],
4084
+ "peer": true,
4047
4085
  "engines": {
4048
4086
  "node": ">=18"
4049
4087
  }
@@ -4055,11 +4093,13 @@
4055
4093
  "cpu": [
4056
4094
  "arm64"
4057
4095
  ],
4058
- "extraneous": true,
4096
+ "dev": true,
4059
4097
  "license": "MIT",
4098
+ "optional": true,
4060
4099
  "os": [
4061
4100
  "openbsd"
4062
4101
  ],
4102
+ "peer": true,
4063
4103
  "engines": {
4064
4104
  "node": ">=18"
4065
4105
  }
@@ -4071,11 +4111,13 @@
4071
4111
  "cpu": [
4072
4112
  "x64"
4073
4113
  ],
4074
- "extraneous": true,
4114
+ "dev": true,
4075
4115
  "license": "MIT",
4116
+ "optional": true,
4076
4117
  "os": [
4077
4118
  "openbsd"
4078
4119
  ],
4120
+ "peer": true,
4079
4121
  "engines": {
4080
4122
  "node": ">=18"
4081
4123
  }
@@ -4087,11 +4129,13 @@
4087
4129
  "cpu": [
4088
4130
  "arm64"
4089
4131
  ],
4090
- "extraneous": true,
4132
+ "dev": true,
4091
4133
  "license": "MIT",
4134
+ "optional": true,
4092
4135
  "os": [
4093
4136
  "openharmony"
4094
4137
  ],
4138
+ "peer": true,
4095
4139
  "engines": {
4096
4140
  "node": ">=18"
4097
4141
  }
@@ -4103,11 +4147,13 @@
4103
4147
  "cpu": [
4104
4148
  "x64"
4105
4149
  ],
4106
- "extraneous": true,
4150
+ "dev": true,
4107
4151
  "license": "MIT",
4152
+ "optional": true,
4108
4153
  "os": [
4109
4154
  "sunos"
4110
4155
  ],
4156
+ "peer": true,
4111
4157
  "engines": {
4112
4158
  "node": ">=18"
4113
4159
  }
@@ -4119,11 +4165,13 @@
4119
4165
  "cpu": [
4120
4166
  "arm64"
4121
4167
  ],
4122
- "extraneous": true,
4168
+ "dev": true,
4123
4169
  "license": "MIT",
4170
+ "optional": true,
4124
4171
  "os": [
4125
4172
  "win32"
4126
4173
  ],
4174
+ "peer": true,
4127
4175
  "engines": {
4128
4176
  "node": ">=18"
4129
4177
  }
@@ -4135,11 +4183,13 @@
4135
4183
  "cpu": [
4136
4184
  "ia32"
4137
4185
  ],
4138
- "extraneous": true,
4186
+ "dev": true,
4139
4187
  "license": "MIT",
4188
+ "optional": true,
4140
4189
  "os": [
4141
4190
  "win32"
4142
4191
  ],
4192
+ "peer": true,
4143
4193
  "engines": {
4144
4194
  "node": ">=18"
4145
4195
  }
@@ -4151,11 +4201,13 @@
4151
4201
  "cpu": [
4152
4202
  "x64"
4153
4203
  ],
4154
- "extraneous": true,
4204
+ "dev": true,
4155
4205
  "license": "MIT",
4206
+ "optional": true,
4156
4207
  "os": [
4157
4208
  "win32"
4158
4209
  ],
4210
+ "peer": true,
4159
4211
  "engines": {
4160
4212
  "node": ">=18"
4161
4213
  }
@@ -4191,9 +4243,11 @@
4191
4243
  "version": "0.28.1",
4192
4244
  "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz",
4193
4245
  "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==",
4194
- "extraneous": true,
4246
+ "dev": true,
4195
4247
  "hasInstallScript": true,
4196
4248
  "license": "MIT",
4249
+ "optional": true,
4250
+ "peer": true,
4197
4251
  "bin": {
4198
4252
  "esbuild": "bin/esbuild"
4199
4253
  },
@@ -17,7 +17,7 @@
17
17
  "dependencies": {
18
18
  "@fastify/cors": "^10.0.1",
19
19
  "@fastify/swagger": "^9.4.0",
20
- "drizzle-orm": "^0.44.0",
20
+ "drizzle-orm": "^0.45.2",
21
21
  "fastify": "^5.4.0",
22
22
  "pg": "^8.16.0"
23
23
  },
package/dist/cli.js CHANGED
@@ -11,6 +11,7 @@ try {
11
11
  const parsed = parseArgs(process.argv.slice(2));
12
12
  process.exitCode = await runCommand(parsed, {
13
13
  cwd: process.cwd(),
14
+ stdin: process.stdin,
14
15
  stdout: process.stdout,
15
16
  stderr: process.stderr
16
17
  });
package/dist/cli.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEpD,IAAI,CAAC;IACH,MAAM,YAAY,GAAG,gBAAgB,EAAE,CAAC;IACxC,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC;IAChC,CAAC;IACD,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,OAAO,CAAC,QAAQ,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE;QAC1C,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE;QAClB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM;KACvB,CAAC,CAAC;AACL,CAAC;AAAC,OAAO,KAAK,EAAE,CAAC;IACf,MAAM,YAAY,GAAG,IAAI,mBAAmB,CAAC;QAC3C,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM;KACvB,CAAC,CAAC;IACH,YAAY,CAAC,KAAK,CAChB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EACtD,6DAA6D,CAC9D,CAAC;IACF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC"}
1
+ {"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAE,MAAM,cAAc,CAAC;AAChD,OAAO,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AAEpD,IAAI,CAAC;IACH,MAAM,YAAY,GAAG,gBAAgB,EAAE,CAAC;IACxC,IAAI,YAAY,EAAE,CAAC;QACjB,MAAM,IAAI,KAAK,CAAC,YAAY,CAAC,CAAC;IAChC,CAAC;IACD,MAAM,MAAM,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAChD,OAAO,CAAC,QAAQ,GAAG,MAAM,UAAU,CAAC,MAAM,EAAE;QAC1C,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE;QAClB,KAAK,EAAE,OAAO,CAAC,KAAK;QACpB,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM;KACvB,CAAC,CAAC;AACL,CAAC;AAAC,OAAO,KAAK,EAAE,CAAC;IACf,MAAM,YAAY,GAAG,IAAI,mBAAmB,CAAC;QAC3C,MAAM,EAAE,OAAO,CAAC,MAAM;QACtB,MAAM,EAAE,OAAO,CAAC,MAAM;KACvB,CAAC,CAAC;IACH,YAAY,CAAC,KAAK,CAChB,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EACtD,6DAA6D,CAC9D,CAAC;IACF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC"}
package/dist/commands.js CHANGED
@@ -9,8 +9,8 @@ import { getCodingAgent, getFrameworkDefinition, getSpecWorkflow, listRegions, p
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, findProjectRoot, loadManifest, manifestDisplayPath, resolveProjectPath, validateGeneratedProject, writeArtifacts, writeProjectFile } from './file-system.js';
13
- import { InteractiveCancelledError, InteractivePrompter } from './interactive.js';
12
+ import { artifactPath, applyProjectFileTransaction, assertNewOrEmptyDirectory, captureProjectFileSnapshot, findProjectRoot, loadManifest, manifestDisplayPath, resolveProjectPath, validateGeneratedProject, writeArtifacts, writeProjectFile } from './file-system.js';
13
+ import { InteractiveCancelledError, InteractivePrompter, isInteractiveTerminal } from './interactive.js';
14
14
  import { applyMergePreflight, assertSafeInitTarget, authorizeMergePreflight, buildMergePreflight, captureTreeState, discoverGitRoot, resolveInitTargetFromDiscovery, validateStagedTree, withStagingArea, writeStagedArtifacts } from './init-filesystem.js';
15
15
  import { renderMigrationChecklist, renderMigrationProposal, renderMigrationTasks, seedMigrationGroups } from './migrate-plan.js';
16
16
  import { buildProjectPlan, loadConfigOptions, mergeOptions, PlanValidationError, projectPlanEntries } from './planner.js';
@@ -21,6 +21,7 @@ import { hasDrift, reconcileProject } from './reconcile.js';
21
21
  import { compareSemver } from './semver.js';
22
22
  import { buildArtifacts, buildManifest, partitionGeneratedArtifacts } from './templates.js';
23
23
  import { PresentationSession } from './terminal.js';
24
+ import { buildUpdateImpact } from './update-impact.js';
24
25
  import { liftoffVersion } from './version.js';
25
26
  import { blockingReadinessFailures, detectHostEnvironment, installRequirement, probeWorkstation, selectLiftoffRuntimeRequirements, selectWorkstationRequirements } from './workstation.js';
26
27
  export async function runCommand(parsed, context) {
@@ -805,11 +806,71 @@ async function preflightUpdate(projectRoot, entries, force) {
805
806
  }
806
807
  await resolveProjectPath(projectRoot, ['liftoff.manifest.json']);
807
808
  }
809
+ function updateSnapshotKey(pathParts) {
810
+ return pathParts.join('\0');
811
+ }
812
+ function reconciliationSignature(entries) {
813
+ return JSON.stringify(entries.map((entry) => ({
814
+ logicalName: entry.logicalName,
815
+ status: entry.status,
816
+ pathParts: entry.pathParts,
817
+ previousPathParts: entry.previousPathParts,
818
+ reason: entry.reason,
819
+ cleanMove: entry.cleanMove,
820
+ sourceModified: entry.sourceModified,
821
+ destinationOccupied: entry.destinationOccupied,
822
+ refreshHash: entry.refreshHash,
823
+ destinationMatches: entry.destinationMatches
824
+ })));
825
+ }
826
+ async function captureInteractiveUpdateSnapshots(projectRoot, entries, inputSnapshots) {
827
+ const snapshots = new Map(inputSnapshots.map((snapshot) => [updateSnapshotKey(snapshot.pathParts), snapshot]));
828
+ for (const entry of entries) {
829
+ const actionable = entry.status !== 'orphan' &&
830
+ (entry.status !== 'unchanged' || entry.refreshHash === true);
831
+ if (!actionable) {
832
+ continue;
833
+ }
834
+ for (const pathParts of [entry.pathParts, entry.previousPathParts]) {
835
+ if (!pathParts) {
836
+ continue;
837
+ }
838
+ const key = updateSnapshotKey(pathParts);
839
+ if (!snapshots.has(key)) {
840
+ snapshots.set(key, await captureProjectFileSnapshot(projectRoot, pathParts));
841
+ }
842
+ }
843
+ }
844
+ return snapshots;
845
+ }
846
+ function authorizedInteractiveSnapshotKeys(entries, force) {
847
+ const keys = new Set([
848
+ updateSnapshotKey(['liftoff.config.json']),
849
+ updateSnapshotKey(['liftoff.manifest.json'])
850
+ ]);
851
+ for (const entry of entries) {
852
+ const authorized = entry.status === 'new' ||
853
+ entry.status === 'missing' ||
854
+ entry.status === 'upgrade' ||
855
+ entry.status === 'unchanged' && entry.refreshHash === true ||
856
+ entry.status === 'moved' && (entry.cleanMove === true || force) ||
857
+ entry.status === 'conflict' && force;
858
+ if (!authorized) {
859
+ continue;
860
+ }
861
+ keys.add(updateSnapshotKey(entry.pathParts));
862
+ if (entry.previousPathParts) {
863
+ keys.add(updateSnapshotKey(entry.previousPathParts));
864
+ }
865
+ }
866
+ return keys;
867
+ }
808
868
  async function updateCommand(parsed, context) {
809
869
  const { presentation } = context;
810
- const apply = readBooleanFlag(parsed.flags, 'apply') ?? false;
811
- const force = readBooleanFlag(parsed.flags, 'force') ?? false;
870
+ let apply = readBooleanFlag(parsed.flags, 'apply') ?? false;
871
+ let force = readBooleanFlag(parsed.flags, 'force') ?? false;
812
872
  const jsonMode = readBooleanFlag(parsed.flags, 'json') ?? false;
873
+ let dirtyWorktreeWarningShown = false;
813
874
  presentation.commandIdentity('update', 'Reconcile the project with current Liftoff templates');
814
875
  if (force && !apply) {
815
876
  presentation.error('--force requires --apply.', 'Run `liftoff update --apply --force` only after reviewing the reported conflicts.');
@@ -821,6 +882,15 @@ async function updateCommand(parsed, context) {
821
882
  presentation.error(`No liftoff.manifest.json found in ${context.cwd} or any parent directory.`, 'Run this command inside a Liftoff project or provide its path explicitly.');
822
883
  return 1;
823
884
  }
885
+ const interactiveReview = !apply &&
886
+ !jsonMode &&
887
+ isInteractiveTerminal(context.stdin, context.stdout);
888
+ const interactiveInputSnapshots = interactiveReview
889
+ ? await Promise.all([
890
+ captureProjectFileSnapshot(projectRoot, ['liftoff.manifest.json']),
891
+ captureProjectFileSnapshot(projectRoot, ['liftoff.config.json'])
892
+ ])
893
+ : [];
824
894
  const manifest = await loadManifest(projectRoot);
825
895
  if (compareSemver(manifest.liftoffVersion, liftoffVersion) > 0) {
826
896
  presentation.error(`This project was written by Liftoff ${manifest.liftoffVersion}, which is newer than this CLI (${liftoffVersion}).`, 'Upgrade the CLI first.');
@@ -874,6 +944,8 @@ async function updateCommand(parsed, context) {
874
944
  const summary = summarizeEntries(entries);
875
945
  const drift = hasDrift(entries);
876
946
  const visible = entries.filter((entry) => entry.status !== 'unchanged' || entry.refreshHash);
947
+ const impact = buildUpdateImpact(entries);
948
+ let interactiveSnapshots;
877
949
  if (!apply) {
878
950
  if (jsonMode) {
879
951
  presentation.rawStdout(`${JSON.stringify({
@@ -908,10 +980,59 @@ async function updateCommand(parsed, context) {
908
980
  }
909
981
  const toWrite = summary.new + summary.missing + summary.upgrade + summary.moved + summary.refresh;
910
982
  presentation.status('warning', 'Drift detected', `${toWrite} to write, ${summary.conflict} conflict(s), ${summary.orphan} orphan(s), ${summary.unchanged} unchanged`);
911
- presentation.command('liftoff update --apply');
912
- return 2;
983
+ if (!impact.hasActionableChanges ||
984
+ !interactiveReview) {
985
+ if (impact.hasActionableChanges) {
986
+ presentation.command('liftoff update --apply');
987
+ }
988
+ return 2;
989
+ }
990
+ if (isDirtyGitWorktree(projectRoot)) {
991
+ presentation.warning('The project worktree has uncommitted changes; commit or back up local work before accepting an update.');
992
+ dirtyWorktreeWarningShown = true;
993
+ }
994
+ interactiveSnapshots = await captureInteractiveUpdateSnapshots(projectRoot, entries, interactiveInputSnapshots);
995
+ const reviewedEntries = await reconcileProject(manifest, render, projectRoot);
996
+ if (reconciliationSignature(reviewedEntries) !== reconciliationSignature(entries)) {
997
+ presentation.error('The project changed while Liftoff prepared the update.', 'Run `liftoff update` again to review the current state.');
998
+ return 1;
999
+ }
1000
+ const prompter = new InteractivePrompter({
1001
+ input: context.stdin,
1002
+ output: context.stdout,
1003
+ presentation,
1004
+ cwd: projectRoot
1005
+ });
1006
+ try {
1007
+ prompter.presentUpdateImpact(impact);
1008
+ if (impact.hasSafeActions) {
1009
+ const safeAccepted = await prompter.confirmSafeUpdate(impact.safeActionCount);
1010
+ if (!safeAccepted) {
1011
+ presentation.cancellation('Update declined; no project files were changed.');
1012
+ return 2;
1013
+ }
1014
+ }
1015
+ if (impact.conflicts.length > 0) {
1016
+ force = await prompter.confirmConflictOverwrite(impact.conflicts, impact.managedPathsRemovedOnOverwrite);
1017
+ if (!impact.hasSafeActions && !force) {
1018
+ presentation.cancellation('Update declined; no project files were changed.');
1019
+ return 2;
1020
+ }
1021
+ }
1022
+ apply = true;
1023
+ }
1024
+ catch (error) {
1025
+ if (error instanceof InteractiveCancelledError) {
1026
+ presentation.cancellation('Update stopped before authorization; no project files were changed.');
1027
+ return 0;
1028
+ }
1029
+ throw error;
1030
+ }
1031
+ finally {
1032
+ prompter.close();
1033
+ }
913
1034
  }
914
- if (isDirtyGitWorktree(projectRoot)) {
1035
+ if (!dirtyWorktreeWarningShown && isDirtyGitWorktree(projectRoot)) {
915
1036
  presentation.warning('The project worktree has uncommitted changes; consider committing before applying.');
916
1037
  }
917
1038
  presentation.stage('Apply safe template changes', projectRoot);
@@ -1003,7 +1124,15 @@ async function updateCommand(parsed, context) {
1003
1124
  pathParts: ['liftoff.manifest.json'],
1004
1125
  content: `${JSON.stringify(nextManifest, null, 2)}\n`
1005
1126
  });
1006
- await applyProjectFileTransaction(projectRoot, mutations);
1127
+ const authorizedSnapshotKeys = interactiveSnapshots
1128
+ ? authorizedInteractiveSnapshotKeys(entries, force)
1129
+ : undefined;
1130
+ const transactionPreconditions = interactiveSnapshots && authorizedSnapshotKeys
1131
+ ? [...interactiveSnapshots.values()].filter((snapshot) => authorizedSnapshotKeys.has(updateSnapshotKey(snapshot.pathParts)))
1132
+ : undefined;
1133
+ await applyProjectFileTransaction(projectRoot, mutations, {
1134
+ ...(transactionPreconditions ? { preconditions: transactionPreconditions } : {})
1135
+ });
1007
1136
  if (jsonMode) {
1008
1137
  presentation.rawStdout(`${JSON.stringify({
1009
1138
  schemaVersion: 1,