@msn-control/liftoff 0.10.4 → 0.11.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.
Files changed (700) hide show
  1. package/DEVELOPER.md +238 -77
  2. package/README.md +38 -33
  3. package/assets/supported-stack.json +3 -136
  4. package/dist/adapters/azure/governance-assessment.d.ts +2 -0
  5. package/dist/adapters/azure/governance-assessment.js +21 -0
  6. package/dist/adapters/azure/governance-assessment.js.map +1 -0
  7. package/dist/adapters/filesystem/atomic-write.d.ts +2 -0
  8. package/dist/adapters/filesystem/atomic-write.js +74 -0
  9. package/dist/adapters/filesystem/atomic-write.js.map +1 -0
  10. package/dist/adapters/filesystem/errors.d.ts +2 -0
  11. package/dist/adapters/filesystem/errors.js +8 -0
  12. package/dist/adapters/filesystem/errors.js.map +1 -0
  13. package/dist/adapters/filesystem/manifest-file.d.ts +1 -0
  14. package/dist/adapters/filesystem/manifest-file.js +21 -0
  15. package/dist/adapters/filesystem/manifest-file.js.map +1 -0
  16. package/dist/adapters/filesystem/project-config.d.ts +4 -0
  17. package/dist/adapters/filesystem/project-config.js +157 -0
  18. package/dist/adapters/filesystem/project-config.js.map +1 -0
  19. package/dist/adapters/filesystem/project-discovery.d.ts +2 -0
  20. package/dist/adapters/filesystem/project-discovery.js +30 -0
  21. package/dist/adapters/filesystem/project-discovery.js.map +1 -0
  22. package/dist/adapters/filesystem/project-files.d.ts +6 -0
  23. package/dist/adapters/filesystem/project-files.js +68 -0
  24. package/dist/adapters/filesystem/project-files.js.map +1 -0
  25. package/dist/adapters/filesystem/project-lock.d.ts +8 -0
  26. package/dist/adapters/filesystem/project-lock.js +164 -0
  27. package/dist/adapters/filesystem/project-lock.js.map +1 -0
  28. package/dist/adapters/filesystem/project-paths.d.ts +2 -0
  29. package/dist/adapters/filesystem/project-paths.js +60 -0
  30. package/dist/adapters/filesystem/project-paths.js.map +1 -0
  31. package/dist/adapters/filesystem/project-transaction.d.ts +24 -0
  32. package/dist/adapters/filesystem/project-transaction.js +177 -0
  33. package/dist/adapters/filesystem/project-transaction.js.map +1 -0
  34. package/dist/adapters/filesystem/reviewed-update-transaction.d.ts +53 -0
  35. package/dist/adapters/filesystem/reviewed-update-transaction.js +864 -0
  36. package/dist/adapters/filesystem/reviewed-update-transaction.js.map +1 -0
  37. package/dist/adapters/filesystem/update-previews.d.ts +50 -0
  38. package/dist/adapters/filesystem/update-previews.js +613 -0
  39. package/dist/adapters/filesystem/update-previews.js.map +1 -0
  40. package/dist/adapters/git/governance-assessment.d.ts +22 -0
  41. package/dist/adapters/git/governance-assessment.js +161 -0
  42. package/dist/adapters/git/governance-assessment.js.map +1 -0
  43. package/dist/adapters/github/governance-assessment.d.ts +2 -0
  44. package/dist/adapters/github/governance-assessment.js +18 -0
  45. package/dist/adapters/github/governance-assessment.js.map +1 -0
  46. package/dist/adapters/packaged-assets/package-root.d.ts +3 -0
  47. package/dist/adapters/packaged-assets/package-root.js +21 -0
  48. package/dist/adapters/packaged-assets/package-root.js.map +1 -0
  49. package/dist/adapters/packaged-assets/supported-stack.d.ts +4 -0
  50. package/dist/adapters/packaged-assets/supported-stack.js +18 -0
  51. package/dist/adapters/packaged-assets/supported-stack.js.map +1 -0
  52. package/dist/adapters/packaged-assets/template-assets.d.ts +23 -0
  53. package/dist/adapters/packaged-assets/template-assets.js +36 -0
  54. package/dist/adapters/packaged-assets/template-assets.js.map +1 -0
  55. package/dist/adapters/process/shell-command.d.ts +6 -0
  56. package/dist/adapters/process/shell-command.js +27 -0
  57. package/dist/adapters/process/shell-command.js.map +1 -0
  58. package/dist/application/context.d.ts +25 -0
  59. package/dist/application/context.js +2 -0
  60. package/dist/application/context.js.map +1 -0
  61. package/dist/application/diagnose/doctor.d.ts +21 -0
  62. package/dist/application/diagnose/doctor.js +580 -0
  63. package/dist/application/diagnose/doctor.js.map +1 -0
  64. package/dist/application/diagnose/generated-project.d.ts +1 -0
  65. package/dist/application/diagnose/generated-project.js +105 -0
  66. package/dist/application/diagnose/generated-project.js.map +1 -0
  67. package/dist/application/diagnose/validate.d.ts +6 -0
  68. package/dist/application/diagnose/validate.js +27 -0
  69. package/dist/application/diagnose/validate.js.map +1 -0
  70. package/dist/application/initialize/fixture.d.ts +2 -0
  71. package/dist/application/initialize/fixture.js +39 -0
  72. package/dist/application/initialize/fixture.js.map +1 -0
  73. package/dist/application/initialize/inputs.d.ts +3 -0
  74. package/dist/application/initialize/inputs.js +35 -0
  75. package/dist/application/initialize/inputs.js.map +1 -0
  76. package/dist/application/initialize/use-case.d.ts +23 -0
  77. package/dist/application/initialize/use-case.js +360 -0
  78. package/dist/application/initialize/use-case.js.map +1 -0
  79. package/dist/application/migrate/use-case.d.ts +7 -0
  80. package/dist/application/migrate/use-case.js +378 -0
  81. package/dist/application/migrate/use-case.js.map +1 -0
  82. package/dist/application/project/catalog.d.ts +46 -0
  83. package/dist/application/project/catalog.js +12 -0
  84. package/dist/application/project/catalog.js.map +1 -0
  85. package/dist/application/project/manifest.d.ts +2 -0
  86. package/dist/application/project/manifest.js +29 -0
  87. package/dist/application/project/manifest.js.map +1 -0
  88. package/dist/application/project/planning.d.ts +6 -0
  89. package/dist/application/project/planning.js +12 -0
  90. package/dist/application/project/planning.js.map +1 -0
  91. package/dist/application/update/approval.d.ts +39 -0
  92. package/dist/application/update/approval.js +54 -0
  93. package/dist/application/update/approval.js.map +1 -0
  94. package/dist/application/update/command-guidance.d.ts +2 -0
  95. package/dist/application/update/command-guidance.js +14 -0
  96. package/dist/application/update/command-guidance.js.map +1 -0
  97. package/dist/application/update/inspection.d.ts +37 -0
  98. package/dist/application/update/inspection.js +235 -0
  99. package/dist/application/update/inspection.js.map +1 -0
  100. package/dist/application/update/migration-runtime.d.ts +14 -0
  101. package/dist/application/update/migration-runtime.js +178 -0
  102. package/dist/application/update/migration-runtime.js.map +1 -0
  103. package/dist/application/update/output.d.ts +134 -0
  104. package/dist/application/update/output.js +182 -0
  105. package/dist/application/update/output.js.map +1 -0
  106. package/dist/application/update/planning.d.ts +40 -0
  107. package/dist/application/update/planning.js +293 -0
  108. package/dist/application/update/planning.js.map +1 -0
  109. package/dist/application/update/preview.d.ts +49 -0
  110. package/dist/application/update/preview.js +220 -0
  111. package/dist/application/update/preview.js.map +1 -0
  112. package/dist/application/update/protected-source.d.ts +25 -0
  113. package/dist/application/update/protected-source.js +81 -0
  114. package/dist/application/update/protected-source.js.map +1 -0
  115. package/dist/application/update/reporting.d.ts +42 -0
  116. package/dist/application/update/reporting.js +326 -0
  117. package/dist/application/update/reporting.js.map +1 -0
  118. package/dist/application/update/revalidation-plan.d.ts +22 -0
  119. package/dist/application/update/revalidation-plan.js +108 -0
  120. package/dist/application/update/revalidation-plan.js.map +1 -0
  121. package/dist/application/update/revalidation.d.ts +70 -0
  122. package/dist/application/update/revalidation.js +337 -0
  123. package/dist/application/update/revalidation.js.map +1 -0
  124. package/dist/application/update/review-plan.d.ts +25 -0
  125. package/dist/application/update/review-plan.js +89 -0
  126. package/dist/application/update/review-plan.js.map +1 -0
  127. package/dist/application/update/transaction-approval.d.ts +29 -0
  128. package/dist/application/update/transaction-approval.js +82 -0
  129. package/dist/application/update/transaction-approval.js.map +1 -0
  130. package/dist/application/update/use-case.d.ts +9 -0
  131. package/dist/application/update/use-case.js +271 -0
  132. package/dist/application/update/use-case.js.map +1 -0
  133. package/dist/application/update/write-plan.d.ts +15 -0
  134. package/dist/application/update/write-plan.js +115 -0
  135. package/dist/application/update/write-plan.js.map +1 -0
  136. package/dist/application/upgrade/use-case.d.ts +6 -0
  137. package/dist/application/upgrade/use-case.js +82 -0
  138. package/dist/application/upgrade/use-case.js.map +1 -0
  139. package/dist/args.d.ts +5 -65
  140. package/dist/args.js +4 -472
  141. package/dist/args.js.map +1 -1
  142. package/dist/artifact-lifecycle.d.ts +1 -21
  143. package/dist/artifact-lifecycle.js +1 -75
  144. package/dist/artifact-lifecycle.js.map +1 -1
  145. package/dist/catalogs.d.ts +1 -50
  146. package/dist/catalogs.js +1 -472
  147. package/dist/catalogs.js.map +1 -1
  148. package/dist/cli/args/contracts.d.ts +51 -0
  149. package/dist/cli/args/contracts.js +2 -0
  150. package/dist/cli/args/contracts.js.map +1 -0
  151. package/dist/cli/args/definitions.d.ts +6 -0
  152. package/dist/cli/args/definitions.js +188 -0
  153. package/dist/cli/args/definitions.js.map +1 -0
  154. package/dist/cli/args/help.d.ts +5 -0
  155. package/dist/cli/args/help.js +142 -0
  156. package/dist/cli/args/help.js.map +1 -0
  157. package/dist/cli/args/parser.d.ts +5 -0
  158. package/dist/cli/args/parser.js +160 -0
  159. package/dist/cli/args/parser.js.map +1 -0
  160. package/dist/cli/args/readers.d.ts +4 -0
  161. package/dist/cli/args/readers.js +19 -0
  162. package/dist/cli/args/readers.js.map +1 -0
  163. package/dist/cli/commands/diagnose.d.ts +4 -0
  164. package/dist/cli/commands/diagnose.js +12 -0
  165. package/dist/cli/commands/diagnose.js.map +1 -0
  166. package/dist/cli/commands/dispatch.d.ts +3 -0
  167. package/dist/cli/commands/dispatch.js +94 -0
  168. package/dist/cli/commands/dispatch.js.map +1 -0
  169. package/dist/cli/commands/help.d.ts +3 -0
  170. package/dist/cli/commands/help.js +32 -0
  171. package/dist/cli/commands/help.js.map +1 -0
  172. package/dist/cli/commands/helpers.d.ts +8 -0
  173. package/dist/cli/commands/helpers.js +97 -0
  174. package/dist/cli/commands/helpers.js.map +1 -0
  175. package/dist/cli/commands/initialize.d.ts +3 -0
  176. package/dist/cli/commands/initialize.js +7 -0
  177. package/dist/cli/commands/initialize.js.map +1 -0
  178. package/dist/cli/commands/migrate.d.ts +3 -0
  179. package/dist/cli/commands/migrate.js +8 -0
  180. package/dist/cli/commands/migrate.js.map +1 -0
  181. package/dist/cli/commands/plan.d.ts +3 -0
  182. package/dist/cli/commands/plan.js +34 -0
  183. package/dist/cli/commands/plan.js.map +1 -0
  184. package/dist/cli/commands/reference.d.ts +5 -0
  185. package/dist/cli/commands/reference.js +43 -0
  186. package/dist/cli/commands/reference.js.map +1 -0
  187. package/dist/cli/commands/update.d.ts +3 -0
  188. package/dist/cli/commands/update.js +10 -0
  189. package/dist/cli/commands/update.js.map +1 -0
  190. package/dist/cli/commands/upgrade.d.ts +3 -0
  191. package/dist/cli/commands/upgrade.js +7 -0
  192. package/dist/cli/commands/upgrade.js.map +1 -0
  193. package/dist/cli/context.d.ts +1 -0
  194. package/dist/cli/context.js +2 -0
  195. package/dist/cli/context.js.map +1 -0
  196. package/dist/cli/project-options.d.ts +3 -0
  197. package/dist/cli/project-options.js +41 -0
  198. package/dist/cli/project-options.js.map +1 -0
  199. package/dist/cli.d.ts +3 -3
  200. package/dist/cli.js +2 -2
  201. package/dist/cli.js.map +1 -1
  202. package/dist/commands.d.ts +4 -35
  203. package/dist/commands.js +3 -2969
  204. package/dist/commands.js.map +1 -1
  205. package/dist/container-validation.d.ts +1 -1
  206. package/dist/domain/governance/activation/approvals.d.ts +351 -0
  207. package/dist/domain/governance/activation/approvals.js +528 -0
  208. package/dist/domain/governance/activation/approvals.js.map +1 -0
  209. package/dist/domain/governance/activation/canonical-json.d.ts +4 -0
  210. package/dist/domain/governance/activation/canonical-json.js +39 -0
  211. package/dist/domain/governance/activation/canonical-json.js.map +1 -0
  212. package/dist/domain/governance/activation/capabilities.d.ts +7 -0
  213. package/dist/domain/governance/activation/capabilities.js +33 -0
  214. package/dist/domain/governance/activation/capabilities.js.map +1 -0
  215. package/dist/domain/governance/activation/evidence.d.ts +63 -0
  216. package/dist/domain/governance/activation/evidence.js +444 -0
  217. package/dist/domain/governance/activation/evidence.js.map +1 -0
  218. package/dist/domain/governance/activation/graph.d.ts +397 -0
  219. package/dist/domain/governance/activation/graph.js +441 -0
  220. package/dist/domain/governance/activation/graph.js.map +1 -0
  221. package/dist/domain/governance/activation/inputs.d.ts +25 -0
  222. package/dist/domain/governance/activation/inputs.js +63 -0
  223. package/dist/domain/governance/activation/inputs.js.map +1 -0
  224. package/dist/domain/governance/activation/operations.d.ts +19 -0
  225. package/dist/domain/governance/activation/operations.js +181 -0
  226. package/dist/domain/governance/activation/operations.js.map +1 -0
  227. package/dist/domain/governance/activation/readiness.d.ts +27 -0
  228. package/dist/domain/governance/activation/readiness.js +269 -0
  229. package/dist/domain/governance/activation/readiness.js.map +1 -0
  230. package/dist/domain/governance/activation/types.d.ts +399 -0
  231. package/dist/domain/governance/activation/types.js +109 -0
  232. package/dist/domain/governance/activation/types.js.map +1 -0
  233. package/dist/domain/governance/activation/validators.d.ts +22 -0
  234. package/dist/domain/governance/activation/validators.js +1347 -0
  235. package/dist/domain/governance/activation/validators.js.map +1 -0
  236. package/dist/domain/governance/assessment/catalog.d.ts +14 -0
  237. package/dist/domain/governance/assessment/catalog.js +105 -0
  238. package/dist/domain/governance/assessment/catalog.js.map +1 -0
  239. package/dist/domain/governance/assessment/errors.d.ts +10 -0
  240. package/dist/domain/governance/assessment/errors.js +22 -0
  241. package/dist/domain/governance/assessment/errors.js.map +1 -0
  242. package/dist/domain/governance/assessment/index.d.ts +8 -0
  243. package/dist/domain/governance/assessment/index.js +9 -0
  244. package/dist/domain/governance/assessment/index.js.map +1 -0
  245. package/dist/domain/governance/assessment/live-normalize.d.ts +19 -0
  246. package/dist/domain/governance/assessment/live-normalize.js +403 -0
  247. package/dist/domain/governance/assessment/live-normalize.js.map +1 -0
  248. package/dist/domain/governance/assessment/predicates.d.ts +36 -0
  249. package/dist/domain/governance/assessment/predicates.js +545 -0
  250. package/dist/domain/governance/assessment/predicates.js.map +1 -0
  251. package/dist/domain/governance/assessment/report.d.ts +23 -0
  252. package/dist/domain/governance/assessment/report.js +266 -0
  253. package/dist/domain/governance/assessment/report.js.map +1 -0
  254. package/dist/domain/governance/assessment/sanitize.d.ts +8 -0
  255. package/dist/domain/governance/assessment/sanitize.js +60 -0
  256. package/dist/domain/governance/assessment/sanitize.js.map +1 -0
  257. package/dist/domain/governance/assessment/types.d.ts +174 -0
  258. package/dist/domain/governance/assessment/types.js +13 -0
  259. package/dist/domain/governance/assessment/types.js.map +1 -0
  260. package/dist/domain/governance/assessment/yaml.d.ts +6 -0
  261. package/dist/domain/governance/assessment/yaml.js +30 -0
  262. package/dist/domain/governance/assessment/yaml.js.map +1 -0
  263. package/dist/domain/governance/policy/identity.d.ts +64 -0
  264. package/dist/domain/governance/policy/identity.js +112 -0
  265. package/dist/domain/governance/policy/identity.js.map +1 -0
  266. package/dist/domain/migration/dependencies.d.ts +7 -0
  267. package/dist/domain/migration/dependencies.js +221 -0
  268. package/dist/domain/migration/dependencies.js.map +1 -0
  269. package/dist/domain/migration/inventory.d.ts +3 -0
  270. package/dist/domain/migration/inventory.js +15 -0
  271. package/dist/domain/migration/inventory.js.map +1 -0
  272. package/dist/domain/project/artifact-lifecycle.d.ts +69 -0
  273. package/dist/domain/project/artifact-lifecycle.js +137 -0
  274. package/dist/domain/project/artifact-lifecycle.js.map +1 -0
  275. package/dist/domain/project/catalog.d.ts +48 -0
  276. package/dist/domain/project/catalog.js +507 -0
  277. package/dist/domain/project/catalog.js.map +1 -0
  278. package/dist/domain/project/contracts.d.ts +247 -0
  279. package/dist/domain/project/contracts.js +2 -0
  280. package/dist/domain/project/contracts.js.map +1 -0
  281. package/dist/domain/project/errors.d.ts +3 -0
  282. package/dist/domain/project/errors.js +7 -0
  283. package/dist/domain/project/errors.js.map +1 -0
  284. package/dist/domain/project/infrastructure-layout.d.ts +26 -0
  285. package/dist/domain/project/infrastructure-layout.js +125 -0
  286. package/dist/domain/project/infrastructure-layout.js.map +1 -0
  287. package/dist/domain/project/inputs.d.ts +21 -0
  288. package/dist/domain/project/inputs.js +41 -0
  289. package/dist/domain/project/inputs.js.map +1 -0
  290. package/dist/domain/project/manifest/artifacts.d.ts +8 -0
  291. package/dist/domain/project/manifest/artifacts.js +150 -0
  292. package/dist/domain/project/manifest/artifacts.js.map +1 -0
  293. package/dist/domain/project/manifest/context.d.ts +13 -0
  294. package/dist/domain/project/manifest/context.js +2 -0
  295. package/dist/domain/project/manifest/context.js.map +1 -0
  296. package/dist/domain/project/manifest/fields.d.ts +6 -0
  297. package/dist/domain/project/manifest/fields.js +36 -0
  298. package/dist/domain/project/manifest/fields.js.map +1 -0
  299. package/dist/domain/project/manifest/governance.d.ts +8 -0
  300. package/dist/domain/project/manifest/governance.js +161 -0
  301. package/dist/domain/project/manifest/governance.js.map +1 -0
  302. package/dist/domain/project/manifest/project-identity.d.ts +6 -0
  303. package/dist/domain/project/manifest/project-identity.js +273 -0
  304. package/dist/domain/project/manifest/project-identity.js.map +1 -0
  305. package/dist/domain/project/manifest/reader.d.ts +9 -0
  306. package/dist/domain/project/manifest/reader.js +141 -0
  307. package/dist/domain/project/manifest/reader.js.map +1 -0
  308. package/dist/domain/project/paths.d.ts +2 -0
  309. package/dist/domain/project/paths.js +29 -0
  310. package/dist/domain/project/paths.js.map +1 -0
  311. package/dist/domain/project/planning.d.ts +19 -0
  312. package/dist/domain/project/planning.js +329 -0
  313. package/dist/domain/project/planning.js.map +1 -0
  314. package/dist/domain/project/retired-workload.d.ts +4 -0
  315. package/dist/domain/project/retired-workload.js +18 -0
  316. package/dist/domain/project/retired-workload.js.map +1 -0
  317. package/dist/domain/project/reviewed-update-artifacts.d.ts +2 -0
  318. package/dist/domain/project/reviewed-update-artifacts.js +3 -0
  319. package/dist/domain/project/reviewed-update-artifacts.js.map +1 -0
  320. package/dist/domain/project/supported-stack.d.ts +113 -0
  321. package/dist/domain/project/supported-stack.js +426 -0
  322. package/dist/domain/project/supported-stack.js.map +1 -0
  323. package/dist/domain/workstation/constraints.d.ts +6 -0
  324. package/dist/domain/workstation/constraints.js +8 -0
  325. package/dist/domain/workstation/constraints.js.map +1 -0
  326. package/dist/file-system.d.ts +10 -44
  327. package/dist/file-system.js +9 -1245
  328. package/dist/file-system.js.map +1 -1
  329. package/dist/framework-adapters.d.ts +1 -1
  330. package/dist/framework-validation.d.ts +1 -1
  331. package/dist/framework-validation.js +1 -1
  332. package/dist/framework-validation.js.map +1 -1
  333. package/dist/genai-templates.d.ts +2 -9
  334. package/dist/genai-templates.js +4 -5
  335. package/dist/genai-templates.js.map +1 -1
  336. package/dist/generators/common/artifacts.d.ts +6 -0
  337. package/dist/generators/common/artifacts.js +30 -0
  338. package/dist/generators/common/artifacts.js.map +1 -0
  339. package/dist/generators/common/base.d.ts +17 -0
  340. package/dist/generators/common/base.js +425 -0
  341. package/dist/generators/common/base.js.map +1 -0
  342. package/dist/generators/common/dotenv-validation.d.ts +2 -0
  343. package/dist/generators/common/dotenv-validation.js +80 -0
  344. package/dist/generators/common/dotenv-validation.js.map +1 -0
  345. package/dist/generators/common/environments.d.ts +3 -0
  346. package/dist/generators/common/environments.js +15 -0
  347. package/dist/generators/common/environments.js.map +1 -0
  348. package/dist/generators/common/frontend.d.ts +15 -0
  349. package/dist/generators/common/frontend.js +166 -0
  350. package/dist/generators/common/frontend.js.map +1 -0
  351. package/dist/generators/common/python-settings.d.ts +2 -0
  352. package/dist/generators/common/python-settings.js +70 -0
  353. package/dist/generators/common/python-settings.js.map +1 -0
  354. package/dist/generators/common/spec-workflow.d.ts +28 -0
  355. package/dist/generators/common/spec-workflow.js +567 -0
  356. package/dist/generators/common/spec-workflow.js.map +1 -0
  357. package/dist/generators/common/values.d.ts +13 -0
  358. package/dist/generators/common/values.js +16 -0
  359. package/dist/generators/common/values.js.map +1 -0
  360. package/dist/generators/containers/compose.d.ts +7 -0
  361. package/dist/generators/containers/compose.js +196 -0
  362. package/dist/generators/containers/compose.js.map +1 -0
  363. package/dist/generators/containers/context.d.ts +1 -0
  364. package/dist/generators/containers/context.js +43 -0
  365. package/dist/generators/containers/context.js.map +1 -0
  366. package/dist/generators/containers/images.d.ts +7 -0
  367. package/dist/generators/containers/images.js +119 -0
  368. package/dist/generators/containers/images.js.map +1 -0
  369. package/dist/generators/context.d.ts +42 -0
  370. package/dist/generators/context.js +38 -0
  371. package/dist/generators/context.js.map +1 -0
  372. package/dist/generators/genai/backend.d.ts +20 -0
  373. package/dist/generators/genai/backend.js +667 -0
  374. package/dist/generators/genai/backend.js.map +1 -0
  375. package/dist/generators/genai/configuration.d.ts +3 -0
  376. package/dist/generators/genai/configuration.js +43 -0
  377. package/dist/generators/genai/configuration.js.map +1 -0
  378. package/dist/generators/genai/database.d.ts +7 -0
  379. package/dist/generators/genai/database.js +71 -0
  380. package/dist/generators/genai/database.js.map +1 -0
  381. package/dist/generators/genai/functions.d.ts +15 -0
  382. package/dist/generators/genai/functions.js +156 -0
  383. package/dist/generators/genai/functions.js.map +1 -0
  384. package/dist/generators/genai/index.d.ts +8 -0
  385. package/dist/generators/genai/index.js +11 -0
  386. package/dist/generators/genai/index.js.map +1 -0
  387. package/dist/generators/genai/patterns.d.ts +9 -0
  388. package/dist/generators/genai/patterns.js +309 -0
  389. package/dist/generators/genai/patterns.js.map +1 -0
  390. package/dist/generators/infrastructure/azure.d.ts +18 -0
  391. package/dist/generators/infrastructure/azure.js +655 -0
  392. package/dist/generators/infrastructure/azure.js.map +1 -0
  393. package/dist/generators/infrastructure/names.d.ts +23 -0
  394. package/dist/generators/infrastructure/names.js +51 -0
  395. package/dist/generators/infrastructure/names.js.map +1 -0
  396. package/dist/generators/standard/configuration.d.ts +3 -0
  397. package/dist/generators/standard/configuration.js +26 -0
  398. package/dist/generators/standard/configuration.js.map +1 -0
  399. package/dist/generators/standard/go.d.ts +17 -0
  400. package/dist/generators/standard/go.js +338 -0
  401. package/dist/generators/standard/go.js.map +1 -0
  402. package/dist/generators/standard/index.d.ts +12 -0
  403. package/dist/generators/standard/index.js +12 -0
  404. package/dist/generators/standard/index.js.map +1 -0
  405. package/dist/generators/standard/node.d.ts +20 -0
  406. package/dist/generators/standard/node.js +314 -0
  407. package/dist/generators/standard/node.js.map +1 -0
  408. package/dist/generators/standard/python.d.ts +18 -0
  409. package/dist/generators/standard/python.js +191 -0
  410. package/dist/generators/standard/python.js.map +1 -0
  411. package/dist/go-template-assets.js +3 -3
  412. package/dist/go-template-assets.js.map +1 -1
  413. package/dist/governance-activation/activation-state.d.ts +1 -1
  414. package/dist/governance-activation/activation-state.js +49 -9
  415. package/dist/governance-activation/activation-state.js.map +1 -1
  416. package/dist/governance-activation/approvals.d.ts +1 -188
  417. package/dist/governance-activation/approvals.js +1 -507
  418. package/dist/governance-activation/approvals.js.map +1 -1
  419. package/dist/governance-activation/canonical-json.d.ts +1 -3
  420. package/dist/governance-activation/canonical-json.js +1 -35
  421. package/dist/governance-activation/canonical-json.js.map +1 -1
  422. package/dist/governance-activation/commands.d.ts +6 -1
  423. package/dist/governance-activation/commands.js +135 -107
  424. package/dist/governance-activation/commands.js.map +1 -1
  425. package/dist/governance-activation/compatibility.d.ts +30 -5
  426. package/dist/governance-activation/compatibility.js +74 -8
  427. package/dist/governance-activation/compatibility.js.map +1 -1
  428. package/dist/governance-activation/credentials.d.ts +3 -3
  429. package/dist/governance-activation/credentials.js +5 -5
  430. package/dist/governance-activation/credentials.js.map +1 -1
  431. package/dist/governance-activation/doctor.d.ts +1 -1
  432. package/dist/governance-activation/doctor.js +59 -100
  433. package/dist/governance-activation/doctor.js.map +1 -1
  434. package/dist/governance-activation/evidence.d.ts +1 -49
  435. package/dist/governance-activation/evidence.js +1 -245
  436. package/dist/governance-activation/evidence.js.map +1 -1
  437. package/dist/governance-activation/git-ignore.d.ts +9 -0
  438. package/dist/governance-activation/git-ignore.js +76 -0
  439. package/dist/governance-activation/git-ignore.js.map +1 -0
  440. package/dist/governance-activation/graph.d.ts +1 -397
  441. package/dist/governance-activation/graph.js +1 -440
  442. package/dist/governance-activation/graph.js.map +1 -1
  443. package/dist/governance-activation/historical-state.d.ts +81 -0
  444. package/dist/governance-activation/historical-state.js +817 -0
  445. package/dist/governance-activation/historical-state.js.map +1 -0
  446. package/dist/governance-activation/history-contracts.d.ts +109 -0
  447. package/dist/governance-activation/history-contracts.js +303 -0
  448. package/dist/governance-activation/history-contracts.js.map +1 -0
  449. package/dist/governance-activation/identity.d.ts +1 -35
  450. package/dist/governance-activation/identity.js +1 -88
  451. package/dist/governance-activation/identity.js.map +1 -1
  452. package/dist/governance-activation/index.d.ts +3 -0
  453. package/dist/governance-activation/index.js +3 -0
  454. package/dist/governance-activation/index.js.map +1 -1
  455. package/dist/governance-activation/inputs.d.ts +10 -0
  456. package/dist/governance-activation/inputs.js +158 -0
  457. package/dist/governance-activation/inputs.js.map +1 -0
  458. package/dist/governance-activation/migration-history.d.ts +97 -0
  459. package/dist/governance-activation/migration-history.js +388 -0
  460. package/dist/governance-activation/migration-history.js.map +1 -0
  461. package/dist/governance-activation/migration.d.ts +4 -3
  462. package/dist/governance-activation/migration.js +14 -6
  463. package/dist/governance-activation/migration.js.map +1 -1
  464. package/dist/governance-activation/phase-bootstrap-state.d.ts +3 -0
  465. package/dist/governance-activation/phase-bootstrap-state.js +128 -0
  466. package/dist/governance-activation/phase-bootstrap-state.js.map +1 -0
  467. package/dist/governance-activation/phase-discovery.d.ts +2 -0
  468. package/dist/governance-activation/phase-discovery.js +88 -0
  469. package/dist/governance-activation/phase-discovery.js.map +1 -0
  470. package/dist/governance-activation/phase-governance.d.ts +4 -0
  471. package/dist/governance-activation/phase-governance.js +141 -0
  472. package/dist/governance-activation/phase-governance.js.map +1 -0
  473. package/dist/governance-activation/phase-publication.d.ts +8 -0
  474. package/dist/governance-activation/phase-publication.js +318 -0
  475. package/dist/governance-activation/phase-publication.js.map +1 -0
  476. package/dist/governance-activation/proof-records.d.ts +3 -0
  477. package/dist/governance-activation/proof-records.js +78 -0
  478. package/dist/governance-activation/proof-records.js.map +1 -0
  479. package/dist/governance-activation/read-only.d.ts +30 -0
  480. package/dist/governance-activation/read-only.js +33 -0
  481. package/dist/governance-activation/read-only.js.map +1 -0
  482. package/dist/governance-activation/readiness.d.ts +1 -25
  483. package/dist/governance-activation/readiness.js +1 -222
  484. package/dist/governance-activation/readiness.js.map +1 -1
  485. package/dist/governance-activation/reconciliation.d.ts +1 -1
  486. package/dist/governance-activation/reconciliation.js +3 -3
  487. package/dist/governance-activation/reconciliation.js.map +1 -1
  488. package/dist/governance-activation/release-integrity.d.ts +1 -1
  489. package/dist/governance-activation/release-integrity.js +2 -2
  490. package/dist/governance-activation/release-integrity.js.map +1 -1
  491. package/dist/governance-activation/seed-lifecycle.d.ts +36 -6
  492. package/dist/governance-activation/seed-lifecycle.js +444 -77
  493. package/dist/governance-activation/seed-lifecycle.js.map +1 -1
  494. package/dist/governance-activation/source-of-truth.d.ts +5 -3
  495. package/dist/governance-activation/source-of-truth.js +32 -52
  496. package/dist/governance-activation/source-of-truth.js.map +1 -1
  497. package/dist/governance-activation/spec-kit-seed.d.ts +9 -0
  498. package/dist/governance-activation/spec-kit-seed.js +43 -0
  499. package/dist/governance-activation/spec-kit-seed.js.map +1 -0
  500. package/dist/governance-activation/task-projection.d.ts +1 -1
  501. package/dist/governance-activation/transition-planning.d.ts +17 -0
  502. package/dist/governance-activation/transition-planning.js +259 -0
  503. package/dist/governance-activation/transition-planning.js.map +1 -0
  504. package/dist/governance-activation/transition-ports.d.ts +150 -0
  505. package/dist/governance-activation/transition-ports.js +2 -0
  506. package/dist/governance-activation/transition-ports.js.map +1 -0
  507. package/dist/governance-activation/transition-process.d.ts +7 -0
  508. package/dist/governance-activation/transition-process.js +21 -0
  509. package/dist/governance-activation/transition-process.js.map +1 -0
  510. package/dist/governance-activation/transition-records.d.ts +56 -0
  511. package/dist/governance-activation/transition-records.js +208 -0
  512. package/dist/governance-activation/transition-records.js.map +1 -0
  513. package/dist/governance-activation/transitions.d.ts +14 -167
  514. package/dist/governance-activation/transitions.js +173 -2061
  515. package/dist/governance-activation/transitions.js.map +1 -1
  516. package/dist/governance-activation/types.d.ts +1 -390
  517. package/dist/governance-activation/types.js +1 -107
  518. package/dist/governance-activation/types.js.map +1 -1
  519. package/dist/governance-activation/validators.d.ts +1 -17
  520. package/dist/governance-activation/validators.js +1 -1304
  521. package/dist/governance-activation/validators.js.map +1 -1
  522. package/dist/governance-assessment/catalog.d.ts +1 -2
  523. package/dist/governance-assessment/catalog.js +18 -85
  524. package/dist/governance-assessment/catalog.js.map +1 -1
  525. package/dist/governance-assessment/command.d.ts +1 -1
  526. package/dist/governance-assessment/command.js +8 -12
  527. package/dist/governance-assessment/command.js.map +1 -1
  528. package/dist/governance-assessment/engine.d.ts +6 -11
  529. package/dist/governance-assessment/engine.js +490 -114
  530. package/dist/governance-assessment/engine.js.map +1 -1
  531. package/dist/governance-assessment/live-normalize.d.ts +1 -19
  532. package/dist/governance-assessment/live-normalize.js +1 -402
  533. package/dist/governance-assessment/live-normalize.js.map +1 -1
  534. package/dist/governance-assessment/live-transport.d.ts +2 -6
  535. package/dist/governance-assessment/live-transport.js +10 -22
  536. package/dist/governance-assessment/live-transport.js.map +1 -1
  537. package/dist/governance-assessment/live.js +55 -5
  538. package/dist/governance-assessment/live.js.map +1 -1
  539. package/dist/governance-assessment/predicates.d.ts +1 -27
  540. package/dist/governance-assessment/predicates.js +1 -329
  541. package/dist/governance-assessment/predicates.js.map +1 -1
  542. package/dist/governance-assessment/project.d.ts +20 -17
  543. package/dist/governance-assessment/project.js +101 -151
  544. package/dist/governance-assessment/project.js.map +1 -1
  545. package/dist/governance-assessment/readers.d.ts +1 -5
  546. package/dist/governance-assessment/readers.js +5 -13
  547. package/dist/governance-assessment/readers.js.map +1 -1
  548. package/dist/governance-assessment/report.d.ts +1 -23
  549. package/dist/governance-assessment/report.js +1 -265
  550. package/dist/governance-assessment/report.js.map +1 -1
  551. package/dist/governance-assessment/sanitize.d.ts +1 -8
  552. package/dist/governance-assessment/sanitize.js +1 -56
  553. package/dist/governance-assessment/sanitize.js.map +1 -1
  554. package/dist/governance-assessment/types.d.ts +1 -173
  555. package/dist/governance-assessment/types.js +1 -12
  556. package/dist/governance-assessment/types.js.map +1 -1
  557. package/dist/governance-assessment/yaml.d.ts +1 -6
  558. package/dist/governance-assessment/yaml.js +1 -29
  559. package/dist/governance-assessment/yaml.js.map +1 -1
  560. package/dist/init-filesystem.d.ts +1 -1
  561. package/dist/init-filesystem.js +126 -174
  562. package/dist/init-filesystem.js.map +1 -1
  563. package/dist/interactive.d.ts +2 -2
  564. package/dist/interactive.js +42 -41
  565. package/dist/interactive.js.map +1 -1
  566. package/dist/migrate-plan.d.ts +1 -1
  567. package/dist/migrate-plan.js +14 -8
  568. package/dist/migrate-plan.js.map +1 -1
  569. package/dist/npm-template-assets.js +5 -4
  570. package/dist/npm-template-assets.js.map +1 -1
  571. package/dist/openspec-profile.d.ts +1 -1
  572. package/dist/opentofu-template-assets.js +3 -3
  573. package/dist/opentofu-template-assets.js.map +1 -1
  574. package/dist/package-identity.d.ts +3 -0
  575. package/dist/package-identity.js +13 -2
  576. package/dist/package-identity.js.map +1 -1
  577. package/dist/planner.d.ts +1 -19
  578. package/dist/planner.js +1 -565
  579. package/dist/planner.js.map +1 -1
  580. package/dist/process-runner.d.ts +1 -1
  581. package/dist/project-dependencies.d.ts +5 -3
  582. package/dist/project-dependencies.js +74 -63
  583. package/dist/project-dependencies.js.map +1 -1
  584. package/dist/published-verifier.js +61 -30
  585. package/dist/published-verifier.js.map +1 -1
  586. package/dist/python-template-assets.js +6 -9
  587. package/dist/python-template-assets.js.map +1 -1
  588. package/dist/reconcile.d.ts +1 -1
  589. package/dist/reconcile.js +2 -2
  590. package/dist/reconcile.js.map +1 -1
  591. package/dist/repository-governance.d.ts +5 -2
  592. package/dist/repository-governance.js +140 -113
  593. package/dist/repository-governance.js.map +1 -1
  594. package/dist/runtime.js +1 -1
  595. package/dist/runtime.js.map +1 -1
  596. package/dist/scan.d.ts +6 -2
  597. package/dist/scan.js +78 -30
  598. package/dist/scan.js.map +1 -1
  599. package/dist/self-upgrade.d.ts +1 -1
  600. package/dist/self-upgrade.js +20 -7
  601. package/dist/self-upgrade.js.map +1 -1
  602. package/dist/stable-release.js +7 -1
  603. package/dist/stable-release.js.map +1 -1
  604. package/dist/standard-templates.d.ts +2 -2
  605. package/dist/standard-templates.js +6 -841
  606. package/dist/standard-templates.js.map +1 -1
  607. package/dist/supported-stack.d.ts +3 -115
  608. package/dist/supported-stack.js +4 -438
  609. package/dist/supported-stack.js.map +1 -1
  610. package/dist/telemetry/index.d.ts +1 -1
  611. package/dist/templates.d.ts +10 -20
  612. package/dist/templates.js +52 -3261
  613. package/dist/templates.js.map +1 -1
  614. package/dist/types.d.ts +1 -263
  615. package/dist/types.js +1 -1
  616. package/dist/types.js.map +1 -1
  617. package/dist/version.js +2 -6
  618. package/dist/version.js.map +1 -1
  619. package/dist/workstation-catalog.d.ts +5 -2
  620. package/dist/workstation-catalog.js +33 -2
  621. package/dist/workstation-catalog.js.map +1 -1
  622. package/dist/workstation.d.ts +4 -3
  623. package/dist/workstation.js +86 -32
  624. package/dist/workstation.js.map +1 -1
  625. package/docs/assets/liftoff-terminal.svg +3 -3
  626. package/docs/azure-deployment.md +107 -10
  627. package/docs/cli-reference.md +144 -35
  628. package/docs/configuration-and-manifests.md +152 -35
  629. package/docs/existing-repositories.md +48 -12
  630. package/docs/getting-started.md +44 -31
  631. package/docs/prerequisites.md +34 -35
  632. package/docs/project-structure.md +39 -46
  633. package/docs/repository-governance.md +96 -26
  634. package/docs/safety-and-consent.md +90 -28
  635. package/docs/spec-workflows-and-agents.md +31 -28
  636. package/docs/supported-stack.md +11 -9
  637. package/docs/troubleshooting.md +114 -54
  638. package/docs/workloads.md +52 -67
  639. package/package.json +4 -7
  640. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/UPSTREAM_LICENSE.txt +0 -21
  641. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/catalog.json +0 -520
  642. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/packaged/gitignore +0 -27
  643. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/README.md +0 -25
  644. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/components.json +0 -22
  645. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/eslint.config.js +0 -23
  646. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/index.html +0 -13
  647. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/package-lock.json +0 -6156
  648. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/package.json +0 -60
  649. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/public/power-apps.svg +0 -55
  650. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/App.tsx +0 -17
  651. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/assets/react.svg +0 -1
  652. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/mode-toggle.tsx +0 -37
  653. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/badge.tsx +0 -46
  654. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/button.tsx +0 -60
  655. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/calendar.tsx +0 -211
  656. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/card.tsx +0 -92
  657. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/chart.tsx +0 -355
  658. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/checkbox.tsx +0 -32
  659. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/command.tsx +0 -184
  660. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/dialog.tsx +0 -141
  661. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/dropdown-menu.tsx +0 -257
  662. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/input.tsx +0 -21
  663. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/label.tsx +0 -22
  664. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/popover.tsx +0 -46
  665. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/progress.tsx +0 -29
  666. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/select.tsx +0 -185
  667. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/separator.tsx +0 -26
  668. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/skeleton.tsx +0 -13
  669. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/table.tsx +0 -114
  670. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/tabs.tsx +0 -66
  671. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/textarea.tsx +0 -18
  672. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/components/ui/tooltip.tsx +0 -61
  673. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/hooks/use-theme.ts +0 -11
  674. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/index.css +0 -131
  675. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/lib/utils.ts +0 -6
  676. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/main.tsx +0 -10
  677. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/_layout.tsx +0 -31
  678. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/home.tsx +0 -59
  679. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/pages/not-found.tsx +0 -14
  680. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/query-provider.tsx +0 -26
  681. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/sonner-provider.tsx +0 -23
  682. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/providers/theme-provider.tsx +0 -64
  683. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/src/router.tsx +0 -24
  684. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.app.json +0 -34
  685. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.json +0 -13
  686. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/tsconfig.node.json +0 -26
  687. package/assets/power-apps-code-app/3438c352483e40982f6c5c0fc36fd71f8e7adbbb/starter/vite.config.ts +0 -19
  688. package/assets/power-apps-code-app/README.md +0 -27
  689. package/dist/code-apps-plugin.d.ts +0 -13
  690. package/dist/code-apps-plugin.js +0 -69
  691. package/dist/code-apps-plugin.js.map +0 -1
  692. package/dist/power-apps-assets.d.ts +0 -30
  693. package/dist/power-apps-assets.js +0 -131
  694. package/dist/power-apps-assets.js.map +0 -1
  695. package/dist/power-apps-templates.d.ts +0 -3
  696. package/dist/power-apps-templates.js +0 -177
  697. package/dist/power-apps-templates.js.map +0 -1
  698. package/dist/power-apps-validation.d.ts +0 -1
  699. package/dist/power-apps-validation.js +0 -79
  700. package/dist/power-apps-validation.js.map +0 -1
package/dist/commands.js CHANGED
@@ -1,2970 +1,4 @@
1
- import { createHash } from 'node:crypto';
2
- import { spawnSync } from 'node:child_process';
3
- import { existsSync, readFileSync } from 'node:fs';
4
- import { cp, lstat, mkdir, mkdtemp, readFile, readdir, readlink, rm, stat } from 'node:fs/promises';
5
- import os from 'node:os';
6
- import path from 'node:path';
7
- import { getCommandHelp, getGeneralHelp, readBooleanFlag, readListFlag, readStringFlag } from './args.js';
8
- import { getCodingAgent, getEnvironment, getFrameworkDefinition, getSpecWorkflow, listRegions, patterns, providers, searchRegions } from './catalogs.js';
9
- import { probeCodeAppsPlugin } from './code-apps-plugin.js';
10
- import { initializeFramework } from './framework-adapters.js';
11
- import { validateFrameworkInstallation } from './framework-validation.js';
12
- import { artifactPath, applyProjectFileTransaction, assertNewOrEmptyDirectory, captureProjectFileSnapshot, findProjectRoot, loadManifest, manifestHadFilteredLegacyNonDurableOwnership, manifestDisplayPath, readProjectFile, resolveProjectPath, validateGeneratedProject, writeArtifacts, writeProjectFile } from './file-system.js';
13
- import { InteractiveCancelledError, InteractivePrompter } from './interactive.js';
14
- import { applyMergePreflight, assertSafeInitTarget, authorizeMergePreflight, buildMergePreflight, captureTreeState, discoverGitRoot, resolveInitTargetFromDiscovery, validateStagedTree, withStagingArea, writeStagedArtifacts } from './init-filesystem.js';
15
- import { migrationCapabilityId, migrationChangeName, renderMigrationChecklist, renderMigrationDesign, renderMigrationProposal, renderMigrationSpec, renderMigrationTasks, seedMigrationGroups } from './migrate-plan.js';
16
- import { buildOpenSpecProfileWriteCommands, configureOpenSpecProfile, inspectOpenSpecProfile, OPEN_SPEC_DELIVERY, OPEN_SPEC_PROFILE, OPEN_SPEC_WORKFLOW_IDS } from './openspec-profile.js';
17
- import { buildProjectPlan, loadConfigOptions, mergeOptions, PlanValidationError, projectPlanEntries } from './planner.js';
18
- import { buildDependencySetupPlan, dependencyResumeCommand, runDependencySetup, verifyPowerAppsPackageMetadata } from './project-dependencies.js';
19
- import { formatCommand, NodeCommandRunner } from './process-runner.js';
20
- import { scanDefaults, scanLegacyProject } from './scan.js';
21
- import { hasDrift, reconcileProject } from './reconcile.js';
22
- import { compareSemver } from './semver.js';
23
- import { checkConfiguredRegistryTarget, runSelfUpgrade, selfUpgradeExitCode, selfUpgradeRemedy, selfUpgradeSummary } from './self-upgrade.js';
24
- import { canonicalManualInstallCommand, canonicalNpmRegistry, } from './package-identity.js';
25
- import { lookupStableRelease } from './stable-release.js';
26
- import { supportedStack } from './supported-stack.js';
27
- import { buildArtifacts, buildManifest, partitionGeneratedArtifacts } from './templates.js';
28
- import { PresentationSession } from './terminal.js';
29
- import { governanceCommand } from './governance-activation/commands.js';
30
- import { activationStateFilePathParts, loadActivationState as loadCurrentActivationState } from './governance-activation/activation-state.js';
31
- import { planHistoricalActivationStateMigration, updateFailureInjectionEnv } from './governance-activation/migration.js';
32
- import { currentActivationIdentity } from './governance-activation/graph.js';
33
- import { governanceChangeMetadataFileName, reconcileActiveGovernanceChange, validateGovernanceChangeMetadata } from './governance-activation/source-of-truth.js';
34
- import { governanceDoctorChecks } from './governance-activation/doctor.js';
35
- import { liftoffVersion } from './version.js';
36
- import { blockingReadinessFailures, detectHostEnvironment, installRequirement, probeWorkstation, selectLiftoffRuntimeRequirements, selectWorkstationRequirements } from './workstation.js';
37
- export async function runCommand(parsed, context) {
38
- const helpRequested = parsed.command !== undefined && readBooleanFlag(parsed.flags, 'help') === true;
39
- const jsonMode = !helpRequested && (parsed.command === 'doctor' ||
40
- parsed.command === 'update' ||
41
- parsed.command === 'governance' ||
42
- parsed.command === 'upgrade' ||
43
- parsed.command === 'validate') &&
44
- readBooleanFlag(parsed.flags, 'json') === true;
45
- const presentation = new PresentationSession({
46
- stdout: context.stdout,
47
- stderr: context.stderr,
48
- ...context.terminal,
49
- json: jsonMode
50
- });
51
- const executionContext = { ...context, presentation };
52
- try {
53
- if (parsed.command && readBooleanFlag(parsed.flags, 'help')) {
54
- renderCommandHelp(parsed.command, presentation, parsed.subcommand);
55
- return 0;
56
- }
57
- switch (parsed.command) {
58
- case undefined:
59
- case 'help':
60
- case '--help':
61
- if (parsed.positional[0]) {
62
- renderCommandHelp(parsed.positional[0], presentation);
63
- }
64
- else {
65
- renderGeneralHelp(presentation);
66
- }
67
- return 0;
68
- case 'version':
69
- presentation.rawStdout(`Liftoff ${liftoffVersion}\n`);
70
- return 0;
71
- case 'init':
72
- return await initCommand(parsed, executionContext);
73
- case 'plan':
74
- return await planCommand(parsed, executionContext);
75
- case 'patterns':
76
- return patternsCommand(executionContext);
77
- case 'providers':
78
- return providersCommand(executionContext);
79
- case 'regions':
80
- return regionsCommand(parsed, executionContext);
81
- case 'validate':
82
- return await validateCommand(parsed, executionContext);
83
- case 'update':
84
- return await updateCommand(parsed, executionContext);
85
- case 'upgrade':
86
- return await upgradeCommand(parsed, executionContext);
87
- case 'migrate':
88
- return await migrateCommand(parsed, executionContext);
89
- case 'doctor':
90
- return await doctorCommand(parsed, executionContext);
91
- case 'governance':
92
- return await governanceCommand(parsed, executionContext);
93
- case 'dev':
94
- return await helperCommand(parsed, executionContext, 'docker compose');
95
- case 'infra':
96
- return await helperCommand(parsed, executionContext, 'tofu');
97
- default:
98
- presentation.error(`Unknown command: ${parsed.command}`, 'Run `liftoff help` to list available commands.');
99
- return 1;
100
- }
101
- }
102
- catch (error) {
103
- if (error instanceof InteractiveCancelledError) {
104
- presentation.cancellation('Interactive operation stopped.');
105
- return 0;
106
- }
107
- if (error instanceof PlanValidationError) {
108
- presentation.error(error.issues.join('\n'), parsed.command ? `Run \`liftoff ${parsed.command} --help\` to review accepted values.` : undefined);
109
- return 1;
110
- }
111
- presentation.error(error instanceof Error ? error.message : String(error));
112
- return 1;
113
- }
114
- }
115
- async function initCommand(parsed, context) {
116
- const { presentation } = context;
117
- presentation.identity('Initialize the project and prepare its workstation');
118
- const runner = context.runner ?? new NodeCommandRunner();
119
- presentation.stage('Discover project context');
120
- const git = await discoverGitRoot(context.cwd, runner);
121
- let initial = await optionsFromParsedArgs(parsed, context.cwd, true);
122
- if (!initial.projectName && git.exact && git.root) {
123
- initial = { ...initial, projectName: path.basename(git.root) };
124
- }
125
- const interactive = initial.yes !== true;
126
- const prompter = interactive
127
- ? new InteractivePrompter({
128
- input: context.stdin,
129
- output: context.stdout,
130
- presentation,
131
- cwd: context.cwd,
132
- configuredRoot: git.exact ? git.root : undefined,
133
- runner
134
- })
135
- : undefined;
136
- try {
137
- const needsPrompts = interactive && hasMissingInitInputs(initial);
138
- if (needsPrompts) {
139
- presentation.stage('Configure project');
140
- }
141
- let options;
142
- let plan;
143
- let confirmed;
144
- try {
145
- options = needsPrompts ? await prompter.promptForInitOptions(initial) : initial;
146
- plan = buildProjectPlan(options, { requireProjectName: true });
147
- presentation.stage('Review resolved plan');
148
- confirmed = options.yes === true
149
- ? (presentation.definitions('Resolved project plan', projectPlanEntries(plan)), true)
150
- : await prompter.confirmPlan(plan);
151
- }
152
- catch (error) {
153
- if (error instanceof InteractiveCancelledError) {
154
- presentation.cancellation('Initialization stopped; no destination files were changed.');
155
- return 0;
156
- }
157
- throw error;
158
- }
159
- if (!confirmed) {
160
- presentation.cancellation('Initialization stopped; no destination files were changed.');
161
- return 0;
162
- }
163
- presentation.stage('Resolve destination');
164
- const target = resolveInitTargetFromDiscovery(git, plan.safeProjectName);
165
- await assertSafeInitTarget(target, target.mode === 'named-child' ? git.canonicalCwd : undefined);
166
- presentation.stage('Check workstation readiness');
167
- const readiness = await ensureWorkstationReady(plan, options, context, runner, presentation, prompter);
168
- if (!readiness.ready) {
169
- return 1;
170
- }
171
- const profileReadiness = await ensureOpenSpecProfileReady(plan, options, context, runner, presentation, prompter);
172
- if (!profileReadiness.ready) {
173
- return 1;
174
- }
175
- presentation.stage('Stage project files');
176
- const staged = await withStagingArea(async (area) => {
177
- const partition = partitionGeneratedArtifacts(buildArtifacts(plan));
178
- await writeStagedArtifacts(area, partition.liftoff, 'liftoff');
179
- presentation.stage('Initialize spec-driven framework', `${plan.specWorkflow.label} ${plan.framework.version}`);
180
- await initializeFramework(area, plan, runner, {
181
- env: context.env,
182
- ...presentation.childStreams(),
183
- onCommand: (command) => presentation.command(command)
184
- });
185
- await writeStagedArtifacts(area, partition.seed, 'seed');
186
- await writeStagedArtifacts(area, [partition.manifest], 'liftoff');
187
- presentation.stage('Validate staged project');
188
- await validateStagedTree(area);
189
- const stagedIssues = await validateGeneratedProject(area.root);
190
- if (stagedIssues.length > 0) {
191
- throw new Error(`Staged project validation failed:\n${stagedIssues.join('\n')}`);
192
- }
193
- const preflight = await buildMergePreflight(area, target.root);
194
- const authorized = await authorizeMergePreflight(preflight, options.force === true, interactive ? (paths) => prompter.confirmFileReplacements(paths) : undefined);
195
- if (!authorized) {
196
- return {
197
- status: interactive ? 'declined' : 'authorization-required'
198
- };
199
- }
200
- presentation.stage('Merge staged project', target.root);
201
- return { status: 'applied', merge: await applyMergePreflight(authorized) };
202
- });
203
- if (staged.status === 'declined') {
204
- presentation.cancellation('No destination files were changed.');
205
- return 0;
206
- }
207
- if (staged.status === 'authorization-required') {
208
- presentation.error('Existing regular-file conflicts require --force in non-interactive mode.', 'Review the listed conflicts, then rerun with `--force` only if every replacement is intended.');
209
- return 1;
210
- }
211
- const issues = await validateGeneratedProject(target.root);
212
- if (issues.length > 0) {
213
- presentation.error(`Initialized project validation failed:\n${issues.join('\n')}`);
214
- return 1;
215
- }
216
- const dependencyPhase = await handleProjectDependencies(plan, target.root, options, readiness.probes, context, runner, presentation, prompter);
217
- if (!dependencyPhase.success) {
218
- return 1;
219
- }
220
- presentation.bullets('Configured integrations', [
221
- `${plan.specWorkflow.label} ${plan.framework.version}`,
222
- ...plan.agents.map((agent) => `${agent.label}${plan.defaultAgent?.id === agent.id ? ' (default)' : ''}`),
223
- ...readiness.pluginProbes.map((probe) => `Code Apps plugin for ${probe.agent.label}: ${probe.state}`),
224
- ...(plan.specWorkflow.id === 'openspec'
225
- ? [
226
- `OpenSpec global profile: ${profileReadiness.changed ? 'configured' : 'verified'}; ` +
227
- `${OPEN_SPEC_WORKFLOW_IDS.length} workflows; skills and commands`,
228
- ...(plan.agents.some((agent) => agent.id === 'github-copilot')
229
- ? [`GitHub Copilot cloud agent: ${plan.copilotCloud ? 'enabled' : 'disabled'}`]
230
- : [])
231
- ]
232
- : []),
233
- plan.governanceProfile.id === 'none'
234
- ? 'Repository governance: disabled'
235
- : `Repository governance: ${plan.governanceProfile.label} policy ${plan.governanceProfile.policyVersion}; local handoff generated, live activation deferred`
236
- ]);
237
- if (readiness.deferred.length > 0) {
238
- presentation.bullets('Deferred advisory checks', readiness.deferred);
239
- }
240
- if (dependencyPhase.deferred.length > 0) {
241
- presentation.bullets('Deferred project dependencies', dependencyPhase.deferred);
242
- }
243
- presentation.completion(`Initialized ${plan.projectName}`, target.root, [
244
- { label: 'Target', value: target.root },
245
- { label: 'Spec workflow', value: plan.specWorkflow.label },
246
- { label: 'Coding agents', value: plan.agents.map((agent) => agent.label).join(', ') },
247
- {
248
- label: 'Repository governance',
249
- value: plan.governanceProfile.id === 'none'
250
- ? 'Disabled'
251
- : 'Deterministic setup generated; run /liftoff-setup next'
252
- }
253
- ], plan.governanceProfile.id === 'none'
254
- ? `liftoff validate ${JSON.stringify(target.root)}`
255
- : '/liftoff-setup');
256
- return 0;
257
- }
258
- finally {
259
- prompter?.close();
260
- }
261
- }
262
- async function planCommand(parsed, context) {
263
- const { presentation } = context;
264
- presentation.identity('Preview project decisions, artifacts, and workstation requirements');
265
- const options = await optionsFromParsedArgs(parsed, context.cwd, false);
266
- const plan = buildProjectPlan(options, { requireProjectName: false });
267
- const artifacts = buildArtifacts(plan);
268
- presentation.definitions('Project decisions', projectPlanEntries(plan));
269
- presentation.table(`Artifacts (${artifacts.length})`, ['Artifact', 'Lifecycle', 'Path'], artifacts.map((artifact) => [
270
- artifact.logicalName,
271
- artifact.lifecycle === 'project'
272
- ? `${artifact.lifecycle} (${artifact.provisioningGroup})`
273
- : artifact.lifecycle,
274
- artifact.pathParts.join('/')
275
- ]));
276
- const workstationRows = selectWorkstationRequirements(plan).map((requirement) => [
277
- requirement.definition.label,
278
- requirement.exactVersion
279
- ? `exactly ${requirement.exactVersion}`
280
- : requirement.minimumVersion
281
- ? `${requirement.minimumVersion}+`
282
- : 'available',
283
- requirement.severity
284
- ]);
285
- if (presentation.stdout.layout === 'plain') {
286
- presentation.section('Workstation requirements', workstationRows.map(([label, version, severity]) => `${label}: ${version} [${severity}]`));
287
- }
288
- else {
289
- presentation.table('Workstation requirements', ['Requirement', 'Version', 'Level'], workstationRows);
290
- }
291
- return 0;
292
- }
293
- async function ensureWorkstationReady(plan, options, context, runner, presentation, prompter, resumeInvocation = 'liftoff init', commandCwd) {
294
- const requirements = selectWorkstationRequirements(plan);
295
- const [initialProbes, pluginProbes] = await Promise.all([
296
- probeWorkstation(requirements, runner, { cwd: commandCwd }),
297
- plan.workload === 'power-apps-code-app' && plan.codeAppsPlugin
298
- ? probeCodeAppsPlugin(plan.agents, runner, commandCwd)
299
- : Promise.resolve([])
300
- ]);
301
- let probes = initialProbes;
302
- presentation.table('Workstation readiness', ['Requirement', 'Level', 'State', 'Detail'], probes.map((probe) => [
303
- probe.requirement.definition.label,
304
- probe.requirement.severity,
305
- probe.state,
306
- probe.detail
307
- ]));
308
- if (pluginProbes.length > 0) {
309
- presentation.table('Optional Code Apps plugin', ['Agent', 'State', 'Detail'], pluginProbes.map((probe) => [probe.agent.label, probe.state, probe.detail]));
310
- }
311
- const actionable = probes.filter((probe) => probe.state !== 'ready');
312
- const host = await detectHostEnvironment();
313
- const installInstruction = (probe) => {
314
- const recipe = probe.requirement.definition.install[host.platform];
315
- const automatic = recipe && (host.platform !== 'linux' || recipe.manager === 'npm' || recipe.manager === 'uv');
316
- if (automatic) {
317
- return formatCommand(recipe.command);
318
- }
319
- if (host.platform === 'linux') {
320
- return probe.requirement.definition.linuxRemedies[host.linuxFamily];
321
- }
322
- return `Install ${probe.requirement.definition.label} manually, then retry.`;
323
- };
324
- const authorizedInstallations = new Set();
325
- if (options.installTools === true) {
326
- for (const probe of actionable) {
327
- authorizedInstallations.add(probe.requirement.id);
328
- }
329
- }
330
- if (options.installTools === undefined &&
331
- options.yes !== true &&
332
- actionable.length > 0) {
333
- for (const probe of actionable) {
334
- const recipe = probe.requirement.definition.install[host.platform];
335
- const automatic = recipe && (host.platform !== 'linux' || recipe.manager === 'npm' || recipe.manager === 'uv');
336
- const constraint = probe.requirement.exactVersion
337
- ? `required exactly ${probe.requirement.exactVersion}`
338
- : probe.requirement.minimumVersion
339
- ? `required ${probe.requirement.minimumVersion} or newer`
340
- : 'required to be available';
341
- if (await prompter.confirmToolInstallation({
342
- label: probe.requirement.definition.label,
343
- severity: probe.requirement.severity,
344
- purpose: probe.requirement.reasons.join('; '),
345
- requirement: constraint,
346
- observed: `${probe.state} - ${probe.detail}`,
347
- ...(automatic
348
- ? { command: formatCommand(recipe.command) }
349
- : { remedy: installInstruction(probe) })
350
- })) {
351
- authorizedInstallations.add(probe.requirement.id);
352
- }
353
- }
354
- }
355
- if (authorizedInstallations.size > 0) {
356
- const updates = new Map();
357
- for (const probe of actionable) {
358
- if (!authorizedInstallations.has(probe.requirement.id)) {
359
- continue;
360
- }
361
- presentation.stage(`Install ${probe.requirement.definition.label}`, `${probe.state} - ${probe.detail}`);
362
- const recipe = probe.requirement.definition.install[host.platform];
363
- if (recipe && (host.platform !== 'linux' || recipe.manager === 'npm' || recipe.manager === 'uv')) {
364
- presentation.command(formatCommand(recipe.command));
365
- }
366
- const installation = await installRequirement(probe.requirement, probe, {
367
- authorized: true,
368
- host,
369
- runner,
370
- cwd: commandCwd,
371
- streamOptions: presentation.childStreams()
372
- });
373
- updates.set(probe.requirement.id, installation.probe);
374
- const kind = installation.state === 'installed'
375
- ? 'success'
376
- : probe.requirement.severity === 'blocking'
377
- ? 'error'
378
- : 'warning';
379
- presentation.status(kind, probe.requirement.definition.label, installation.detail);
380
- if (installation.remedy) {
381
- presentation.command(installation.remedy);
382
- }
383
- }
384
- probes = probes.map((probe) => updates.get(probe.requirement.id) ?? probe);
385
- }
386
- const blockers = blockingReadinessFailures(probes);
387
- if (blockers.length > 0) {
388
- for (const blocker of blockers) {
389
- presentation.error(`${blocker.requirement.definition.label}: ${blocker.detail}`, installInstruction(blocker));
390
- }
391
- presentation.error('Workstation readiness is incomplete.', options.installTools
392
- ? `Open a new terminal if PATH changed, then rerun \`${resumeInvocation}\` with the same project options.`
393
- : `Resume with \`${resumeInvocation} --install-tools\` plus the same project options after reviewing the commands.`);
394
- return { ready: false, deferred: [], probes, pluginProbes };
395
- }
396
- const deferred = [
397
- ...probes
398
- .filter((probe) => probe.requirement.severity === 'advisory' && probe.state !== 'ready')
399
- .map((probe) => `${probe.requirement.definition.label}: ${probe.detail} Remedy: ${installInstruction(probe)}`),
400
- ...probes.flatMap((probe) => probe.notices
401
- .filter((notice) => notice.state !== 'ready')
402
- .map((notice) => `${notice.label}: ${notice.detail}${notice.remedy ? ` Remedy: ${notice.remedy}` : ''}`)),
403
- ...pluginProbes
404
- .filter((probe) => probe.state !== 'ready')
405
- .map((probe) => `${probe.agent.label} Code Apps plugin: ${probe.detail}${probe.remedy ? ` Remedy: ${probe.remedy}` : ''}`)
406
- ];
407
- return { ready: true, deferred, probes, pluginProbes };
408
- }
409
- async function ensureOpenSpecProfileReady(plan, options, context, runner, presentation, prompter, resumeInvocation = 'liftoff init') {
410
- if (plan.specWorkflow.id !== 'openspec') {
411
- return { ready: true, changed: false };
412
- }
413
- presentation.stage('Check OpenSpec global profile');
414
- const inspection = await inspectOpenSpecProfile(plan.framework.executable, runner, {
415
- cwd: context.cwd,
416
- env: context.env
417
- });
418
- if (inspection.compatible) {
419
- presentation.status('success', 'OpenSpec global profile', `${OPEN_SPEC_PROFILE}; ${OPEN_SPEC_DELIVERY}; ${OPEN_SPEC_WORKFLOW_IDS.length} workflows`);
420
- return { ready: true, changed: false };
421
- }
422
- const commands = buildOpenSpecProfileWriteCommands(plan.framework.executable);
423
- const authorized = options.configureOpenSpecProfile === true ||
424
- (options.configureOpenSpecProfile === undefined &&
425
- prompter !== undefined &&
426
- await prompter.confirmOpenSpecProfileConfiguration({
427
- observed: [
428
- { label: 'Profile', value: inspection.state.profile },
429
- { label: 'Delivery', value: inspection.state.delivery },
430
- {
431
- label: 'Workflows',
432
- value: inspection.state.workflows.length > 0
433
- ? inspection.state.workflows.join(', ')
434
- : '(none)'
435
- }
436
- ],
437
- required: [
438
- { label: 'Profile', value: OPEN_SPEC_PROFILE },
439
- { label: 'Delivery', value: OPEN_SPEC_DELIVERY },
440
- { label: 'Workflows', value: OPEN_SPEC_WORKFLOW_IDS.join(', ') }
441
- ],
442
- differences: inspection.differences,
443
- commands: commands.map((command) => formatCommand(command))
444
- }));
445
- if (!authorized) {
446
- presentation.error('The global OpenSpec profile does not satisfy the Liftoff template contract.', `Run ${commands.map((command) => `\`${formatCommand(command)}\``).join(', then ')}, ` +
447
- `then rerun \`${resumeInvocation}\`; or authorize those commands with ` +
448
- '`--configure-openspec-profile`.');
449
- return { ready: false, changed: false };
450
- }
451
- presentation.stage('Configure OpenSpec global profile');
452
- await configureOpenSpecProfile(plan.framework.executable, runner, {
453
- cwd: context.cwd,
454
- env: context.env,
455
- ...presentation.childStreams(),
456
- onCommand: (command) => presentation.command(formatCommand(command))
457
- });
458
- presentation.status('success', 'OpenSpec global profile', `${OPEN_SPEC_PROFILE}; ${OPEN_SPEC_DELIVERY}; ${OPEN_SPEC_WORKFLOW_IDS.length} workflows (configured)`);
459
- return { ready: true, changed: true };
460
- }
461
- async function handleProjectDependencies(plan, projectRoot, options, probes, context, runner, presentation, prompter) {
462
- const dependencyPlan = buildDependencySetupPlan(plan, projectRoot, probes);
463
- let installDependencies = options.installDependencies === true;
464
- if (options.installDependencies === undefined &&
465
- options.yes !== true &&
466
- dependencyPlan.commands.length > 0) {
467
- installDependencies = await prompter.confirmDependencyInstallation(dependencyPlan.commands);
468
- }
469
- if (!installDependencies) {
470
- return {
471
- success: true,
472
- deferred: dependencyPlan.commands.map((command) => `${command.label}: ${dependencyResumeCommand(command)}`)
473
- };
474
- }
475
- if (plan.workload === 'power-apps-code-app') {
476
- const issues = await verifyPowerAppsPackageMetadata(projectRoot);
477
- if (issues.length > 0) {
478
- presentation.error('Power Apps dependency installation blocked', `${issues.join(' ')} Repair the project-owned package.json and package-lock.json before installing dependencies.`);
479
- return { success: false, deferred: [] };
480
- }
481
- }
482
- presentation.stage('Install project dependencies');
483
- const result = await runDependencySetup(dependencyPlan, projectRoot, runner, {
484
- ...presentation.childStreams(),
485
- onCommand: (command) => {
486
- presentation.status('pending', command.label, command.cwd);
487
- presentation.command(formatCommand(command.command));
488
- }
489
- });
490
- if (!result.success) {
491
- presentation.error('Project dependencies failed', `${result.failed?.label ?? 'dependency command'}: ${result.detail ?? 'unknown failure'}`);
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.');
494
- if (result.restoredMutations.length > 0) {
495
- presentation.warning(`Restored protected files: ${result.restoredMutations.join(', ')}`);
496
- }
497
- if (result.resumeCommand) {
498
- presentation.command(result.resumeCommand);
499
- }
500
- return { success: false, deferred: [] };
501
- }
502
- presentation.status('success', 'Project dependencies', `${result.completed.length} command${result.completed.length === 1 ? '' : 's'} completed`);
503
- return { success: true, deferred: [] };
504
- }
505
- function patternsCommand(context) {
506
- context.presentation.commandIdentity('patterns', 'Available GenAI application patterns');
507
- context.presentation.table('Patterns', ['Identifier', 'Pattern', 'Scaffold'], patterns.map((pattern) => [pattern.id, pattern.label, pattern.scaffoldStatus]));
508
- return 0;
509
- }
510
- function providersCommand(context) {
511
- context.presentation.commandIdentity('providers', 'Cloud provider availability');
512
- context.presentation.table('Providers', ['Identifier', 'Provider', 'Availability'], providers.map((provider) => [provider.id, provider.label, provider.status]));
513
- return 0;
514
- }
515
- function regionsCommand(parsed, context) {
516
- const cloud = readStringFlag(parsed.flags, 'cloud') ?? 'azure';
517
- context.presentation.commandIdentity('regions', 'Cloud deployment regions');
518
- if (cloud !== 'azure') {
519
- context.presentation.error(`${cloud} regions are not available until the provider adapter is implemented.`, 'Run `liftoff providers` to review currently available providers.');
520
- return 1;
521
- }
522
- const query = parsed.positional[0] ?? readStringFlag(parsed.flags, 'region');
523
- const regions = parsed.subcommand === 'search' && query ? searchRegions('azure', query) : listRegions('azure');
524
- if (regions.length === 0) {
525
- context.presentation.warning(`No Azure regions matched ${JSON.stringify(query ?? '')}.`);
526
- return 0;
527
- }
528
- context.presentation.table(query ? `Azure region matches for ${JSON.stringify(query)}` : 'Azure regions', ['Identifier', 'Region', 'Geography'], regions.map((region) => [region.slug, region.displayName, region.geography]));
529
- return 0;
530
- }
531
- async function validateCommand(parsed, context) {
532
- context.presentation.commandIdentity('validate', 'Validate a generated Liftoff project');
533
- const jsonMode = readBooleanFlag(parsed.flags, 'json') ?? false;
534
- const explicit = parsed.positional[0] ?? readStringFlag(parsed.flags, 'project');
535
- const projectRoot = explicit
536
- ? path.resolve(context.cwd, explicit)
537
- : (await findProjectRoot(context.cwd)) ?? context.cwd;
538
- const issues = await validateGeneratedProject(projectRoot);
539
- if (jsonMode) {
540
- context.presentation.rawStdout(`${JSON.stringify({
541
- schemaVersion: 1,
542
- projectRoot,
543
- valid: issues.length === 0,
544
- issues
545
- }, null, 2)}\n`);
546
- return issues.length === 0 ? 0 : 1;
547
- }
548
- if (issues.length > 0) {
549
- context.presentation.error(issues.join('\n'), 'Restore invalid generated files or the manifest from version control, then rerun validation.');
550
- return 1;
551
- }
552
- context.presentation.status('success', 'Generated project manifest is valid', projectRoot);
553
- return 0;
554
- }
555
- const STAGING_EXCLUDES = new Set(['.git', 'node_modules', 'vendor', '.venv', 'venv', '__pycache__', 'dist', 'build', '.next']);
556
- async function snapshotMigrationSource(sourceRoot) {
557
- const snapshot = new Map();
558
- const visit = async (pathParts) => {
559
- const current = path.join(sourceRoot, ...pathParts);
560
- const entries = await readdir(current, { withFileTypes: true });
561
- entries.sort((left, right) => left.name.localeCompare(right.name, 'en'));
562
- for (const entry of entries) {
563
- const childParts = [...pathParts, entry.name];
564
- const relativePath = childParts.join('/');
565
- const childPath = path.join(sourceRoot, ...childParts);
566
- const details = await lstat(childPath);
567
- if (details.isSymbolicLink()) {
568
- snapshot.set(relativePath, `symlink:${await readlink(childPath)}`);
569
- }
570
- else if (details.isDirectory()) {
571
- snapshot.set(relativePath, 'directory');
572
- await visit(childParts);
573
- }
574
- else if (details.isFile()) {
575
- const hash = createHash('sha256').update(await readFile(childPath)).digest('hex');
576
- snapshot.set(relativePath, `file:${hash}`);
577
- }
578
- else {
579
- snapshot.set(relativePath, 'other');
580
- }
581
- }
582
- };
583
- await visit([]);
584
- return snapshot;
585
- }
586
- function sourceSnapshotChanges(before, after) {
587
- return [...new Set([...before.keys(), ...after.keys()])]
588
- .filter((key) => before.get(key) !== after.get(key))
589
- .sort();
590
- }
591
- async function withUnchangedMigrationSource(sourceRoot, operation) {
592
- const before = await snapshotMigrationSource(sourceRoot);
593
- const assertUnchanged = async () => {
594
- const changes = sourceSnapshotChanges(before, await snapshotMigrationSource(sourceRoot));
595
- if (changes.length > 0) {
596
- throw new Error(`Migration source changed unexpectedly; refusing to continue:\n${changes.map((entry) => `- ${entry}`).join('\n')}`);
597
- }
598
- };
599
- let result;
600
- try {
601
- result = await operation();
602
- }
603
- catch (error) {
604
- await assertUnchanged();
605
- throw error;
606
- }
607
- await assertUnchanged();
608
- return result;
609
- }
610
- async function stageMigrationSource(area, sourceRoot) {
611
- const destination = path.join(area.root, 'migration', 'legacy');
612
- await cp(sourceRoot, destination, {
613
- recursive: true,
614
- filter: (source) => {
615
- const relative = path.relative(sourceRoot, source);
616
- if (!relative) {
617
- return true;
618
- }
619
- return !relative.split(path.sep).some((part) => STAGING_EXCLUDES.has(part));
620
- }
621
- });
622
- for (const entry of (await captureTreeState(area.root)).values()) {
623
- if (entry.pathParts[0] === 'migration' &&
624
- entry.pathParts[1] === 'legacy' &&
625
- entry.type !== 'directory') {
626
- area.origins.set(entry.pathParts.join('/'), 'seed');
627
- }
628
- }
629
- }
630
- function migrationPlanArtifacts(plan, inventory) {
631
- const groups = seedMigrationGroups(inventory, plan);
632
- if (plan.specWorkflow.id === 'openspec') {
633
- return {
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
- },
642
- {
643
- logicalName: 'migration-proposal',
644
- category: 'seed',
645
- lifecycle: 'seed',
646
- pathParts: ['openspec', 'changes', migrationChangeName, 'proposal.md'],
647
- content: renderMigrationProposal(plan, inventory)
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
- },
656
- {
657
- logicalName: 'migration-tasks',
658
- category: 'seed',
659
- lifecycle: 'seed',
660
- pathParts: ['openspec', 'changes', migrationChangeName, 'tasks.md'],
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()
676
- }
677
- ],
678
- location: `openspec/changes/${migrationChangeName}/ (run it with your agent workflow, e.g. /opsx:apply ${migrationChangeName})`
679
- };
680
- }
681
- return {
682
- artifacts: [{
683
- logicalName: 'migration-checklist',
684
- category: 'seed',
685
- lifecycle: 'seed',
686
- pathParts: ['MIGRATION.md'],
687
- content: renderMigrationChecklist(plan, inventory, groups)
688
- }],
689
- location: 'MIGRATION.md'
690
- };
691
- }
692
- async function executeMigration(parsed, context, sourceRoot) {
693
- const { presentation } = context;
694
- presentation.stage('Scan legacy project', sourceRoot);
695
- const inventory = await scanLegacyProject(sourceRoot);
696
- const { options: defaults, provenance } = scanDefaults(inventory);
697
- if (presentation.stdout.layout === 'plain') {
698
- presentation.section('Scan defaults (override in prompts or with flags)', provenance.map((item) => `${item.field}: ${String(item.value)} (detected: ${item.evidence})`));
699
- }
700
- else {
701
- presentation.table('Scan defaults (override in prompts or with flags)', ['Decision', 'Detected value', 'Evidence'], provenance.map((item) => [item.field, String(item.value), item.evidence]));
702
- }
703
- const flagOptions = await optionsFromParsedArgs(parsed, context.cwd, false);
704
- const initial = mergeOptions(defaults, flagOptions);
705
- if (flagOptions.pattern && flagOptions.projectType === undefined) {
706
- initial.projectType = 'genai';
707
- if (flagOptions.apiStack === undefined) {
708
- initial.apiStack = undefined;
709
- }
710
- }
711
- else if (flagOptions.apiStack && flagOptions.projectType === undefined) {
712
- initial.projectType = 'standard';
713
- initial.pattern = undefined;
714
- }
715
- if (flagOptions.projectType === 'standard' && flagOptions.pattern === undefined) {
716
- initial.pattern = undefined;
717
- }
718
- if (flagOptions.projectType === 'genai' && flagOptions.apiStack === undefined) {
719
- initial.apiStack = undefined;
720
- }
721
- const runner = context.runner ?? new NodeCommandRunner();
722
- const interactive = initial.yes !== true;
723
- const prompter = interactive
724
- ? new InteractivePrompter({
725
- input: context.stdin,
726
- output: context.stdout,
727
- presentation,
728
- cwd: context.cwd,
729
- runner
730
- })
731
- : undefined;
732
- try {
733
- const needsPrompts = interactive && hasMissingInitInputs(initial);
734
- if (needsPrompts) {
735
- presentation.stage('Configure migrated project');
736
- }
737
- const options = needsPrompts ? await prompter.promptForInitOptions(initial) : initial;
738
- const plan = buildProjectPlan(options, { requireProjectName: true });
739
- if (plan.workload === 'power-apps-code-app') {
740
- presentation.error('Power Apps code app migration is not supported.', 'Initialize a fresh Power Apps code app, then move application changes intentionally.');
741
- return 1;
742
- }
743
- presentation.stage('Review migration plan');
744
- const confirmed = options.yes === true
745
- ? (presentation.definitions('Resolved migration plan', projectPlanEntries(plan)), true)
746
- : await prompter.confirmPlan(plan, undefined, 'Migrate project?');
747
- if (!confirmed) {
748
- presentation.cancellation('Migration stopped; the source and destination were not modified.');
749
- return 0;
750
- }
751
- presentation.stage('Resolve fresh migration target');
752
- const parentDir = path.dirname(sourceRoot);
753
- let targetRoot = path.resolve(parentDir, plan.safeProjectName);
754
- if (targetRoot === sourceRoot) {
755
- targetRoot = path.resolve(parentDir, `${plan.safeProjectName}-liftoff`);
756
- }
757
- const target = { root: targetRoot, mode: 'named-child' };
758
- await assertSafeInitTarget(target, parentDir);
759
- await assertNewOrEmptyDirectory(targetRoot);
760
- presentation.stage('Check workstation readiness');
761
- const readinessParent = process.env.LIFTOFF_STAGING_ROOT
762
- ? path.resolve(process.env.LIFTOFF_STAGING_ROOT)
763
- : os.tmpdir();
764
- await mkdir(readinessParent, { recursive: true });
765
- const readinessRoot = await mkdtemp(path.join(readinessParent, 'liftoff-migrate-readiness-'));
766
- const readiness = await (async () => {
767
- try {
768
- return await ensureWorkstationReady(plan, options, context, runner, presentation, prompter, `liftoff migrate ${JSON.stringify(sourceRoot)}`, readinessRoot);
769
- }
770
- finally {
771
- await rm(readinessRoot, { recursive: true, force: true });
772
- }
773
- })();
774
- if (!readiness.ready) {
775
- return 1;
776
- }
777
- const profileReadiness = await ensureOpenSpecProfileReady(plan, options, context, runner, presentation, prompter, `liftoff migrate ${JSON.stringify(sourceRoot)}`);
778
- if (!profileReadiness.ready) {
779
- return 1;
780
- }
781
- const migrationPlan = migrationPlanArtifacts(plan, inventory);
782
- presentation.stage('Stage fresh migration project');
783
- await withStagingArea(async (area) => {
784
- const partition = partitionGeneratedArtifacts(buildArtifacts(plan));
785
- await writeStagedArtifacts(area, partition.liftoff, 'liftoff');
786
- presentation.stage('Initialize spec-driven framework', `${plan.specWorkflow.label} ${plan.framework.version}`);
787
- await initializeFramework(area, plan, runner, {
788
- env: context.env,
789
- ...presentation.childStreams(),
790
- onCommand: (command) => presentation.command(command)
791
- });
792
- await writeStagedArtifacts(area, partition.seed, 'seed');
793
- presentation.stage('Copy filtered legacy source', sourceRoot);
794
- await stageMigrationSource(area, sourceRoot);
795
- await writeStagedArtifacts(area, migrationPlan.artifacts, 'seed');
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
- }
817
- presentation.stage('Validate staged migration');
818
- await validateStagedTree(area);
819
- const stagedIssues = await validateGeneratedProject(area.root);
820
- if (stagedIssues.length > 0) {
821
- throw new Error(`Staged migration project validation failed:\n${stagedIssues.join('\n')}`);
822
- }
823
- const preflight = await buildMergePreflight(area, targetRoot);
824
- const existing = preflight.entries.filter((entry) => entry.destination.type !== 'missing');
825
- if (existing.length > 0) {
826
- throw new Error(`Migration target must remain new or empty; --force cannot replace existing content:\n` +
827
- existing.map((entry) => `- ${entry.relativePath}`).join('\n'));
828
- }
829
- const authorized = await authorizeMergePreflight(preflight, false);
830
- if (!authorized) {
831
- throw new Error('Migration target authorization failed.');
832
- }
833
- presentation.stage('Merge fresh migration target', targetRoot);
834
- await applyMergePreflight(authorized, { requireEmptyTarget: true });
835
- });
836
- const issues = await validateGeneratedProject(targetRoot);
837
- if (issues.length > 0) {
838
- presentation.error(`Migrated project validation failed:\n${issues.join('\n')}`);
839
- return 1;
840
- }
841
- const dependencyPhase = await handleProjectDependencies(plan, targetRoot, options, readiness.probes, context, runner, presentation, prompter);
842
- if (!dependencyPhase.success) {
843
- return 1;
844
- }
845
- presentation.bullets('Configured integrations', [
846
- `${plan.specWorkflow.label} ${plan.framework.version}`,
847
- ...plan.agents.map((agent) => `${agent.label}${plan.defaultAgent?.id === agent.id ? ' (default)' : ''}`),
848
- ...(plan.specWorkflow.id === 'openspec'
849
- ? [
850
- `OpenSpec global profile: ${profileReadiness.changed ? 'configured' : 'verified'}; ` +
851
- `${OPEN_SPEC_WORKFLOW_IDS.length} workflows; skills and commands`,
852
- ...(plan.agents.some((agent) => agent.id === 'github-copilot')
853
- ? [`GitHub Copilot cloud agent: ${plan.copilotCloud ? 'enabled' : 'disabled'}`]
854
- : [])
855
- ]
856
- : [])
857
- ]);
858
- if (readiness.deferred.length > 0) {
859
- presentation.bullets('Deferred advisory checks', readiness.deferred);
860
- }
861
- if (dependencyPhase.deferred.length > 0) {
862
- presentation.bullets('Deferred project dependencies', dependencyPhase.deferred);
863
- }
864
- presentation.bullets('Next steps', [
865
- `Optional - preserve history: copy the .git directory from ${sourceRoot} into ${targetRoot}, then commit the migration on top (git rename detection preserves file history).`,
866
- `Execute the migration plan: ${migrationPlan.location}`,
867
- 'Verify compliance: liftoff validate && liftoff doctor'
868
- ]);
869
- presentation.completion(`Migrated ${plan.projectName}`, targetRoot, [
870
- { label: 'Target', value: targetRoot },
871
- { label: 'Source', value: `${sourceRoot} (not modified)` },
872
- { label: 'Rollback', value: `Delete ${targetRoot}` }
873
- ], 'liftoff validate && liftoff doctor');
874
- return 0;
875
- }
876
- finally {
877
- prompter?.close();
878
- }
879
- }
880
- async function migrateCommand(parsed, context) {
881
- const sourceArg = parsed.positional[0];
882
- if (!sourceArg) {
883
- context.presentation.error('Usage: liftoff migrate <path-to-existing-project>', 'Run `liftoff migrate --help` for accepted migration options.');
884
- return 1;
885
- }
886
- const migrationOptions = await optionsFromParsedArgs(parsed, context.cwd, false);
887
- if (migrationOptions.projectType === 'power-apps-code-app') {
888
- context.presentation.error('Power Apps code app migration is not supported.', 'Initialize a fresh Power Apps code app with `liftoff init --type power-apps-code-app`, then move application changes intentionally.');
889
- return 1;
890
- }
891
- const sourceRoot = path.resolve(context.cwd, sourceArg);
892
- let sourceDetails;
893
- try {
894
- sourceDetails = await stat(sourceRoot);
895
- }
896
- catch {
897
- context.presentation.error(`Source project not found: ${sourceRoot}`);
898
- return 1;
899
- }
900
- if (!sourceDetails.isDirectory()) {
901
- context.presentation.error(`Source path is not a directory: ${sourceRoot}`);
902
- return 1;
903
- }
904
- if (existsSync(path.join(sourceRoot, 'liftoff.manifest.json'))) {
905
- context.presentation.error(`${sourceRoot} is already a Liftoff project.`, 'Use `liftoff update` for managed-core maintenance; in-place project template migration is not automated.');
906
- return 1;
907
- }
908
- context.presentation.identity('Migrate an existing application into a fresh Liftoff project');
909
- return withUnchangedMigrationSource(sourceRoot, () => executeMigration(parsed, context, sourceRoot));
910
- }
911
- function summarizeEntries(entries) {
912
- const summary = {
913
- new: 0,
914
- missing: 0,
915
- upgrade: 0,
916
- conflict: 0,
917
- moved: 0,
918
- orphan: 0,
919
- retired: 0,
920
- retiredRemoved: 0,
921
- retiredAbsent: 0,
922
- retiredConflict: 0,
923
- refresh: 0,
924
- unchanged: 0
925
- };
926
- for (const entry of entries) {
927
- if (entry.status === 'unchanged') {
928
- if (entry.refreshHash) {
929
- summary.refresh += 1;
930
- }
931
- else {
932
- summary.unchanged += 1;
933
- }
934
- continue;
935
- }
936
- if (entry.status === 'retired-conflict') {
937
- summary.retiredConflict += 1;
938
- continue;
939
- }
940
- if (entry.status === 'retired') {
941
- summary.retired += 1;
942
- if (entry.destinationOccupied === false) {
943
- summary.retiredAbsent += 1;
944
- }
945
- else {
946
- summary.retiredRemoved += 1;
947
- }
948
- continue;
949
- }
950
- if (entry.status === 'moved' && !entry.cleanMove) {
951
- summary.conflict += 1;
952
- continue;
953
- }
954
- summary[entry.status] += 1;
955
- }
956
- return summary;
957
- }
958
- function entryMarker(entry) {
959
- switch (entry.status) {
960
- case 'new':
961
- case 'missing':
962
- return '+';
963
- case 'upgrade':
964
- return '~';
965
- case 'conflict':
966
- return '!';
967
- case 'moved':
968
- return entry.cleanMove ? '>' : '!';
969
- case 'orphan':
970
- case 'retired':
971
- return '-';
972
- case 'retired-conflict':
973
- return '!';
974
- default:
975
- return '~';
976
- }
977
- }
978
- function entryDisplay(entry) {
979
- if (entry.status === 'moved' && entry.previousPathParts) {
980
- return `${manifestDisplayPath(entry.previousPathParts)} => ${manifestDisplayPath(entry.pathParts)}`;
981
- }
982
- return manifestDisplayPath(entry.pathParts);
983
- }
984
- function isDirtyGitWorktree(projectRoot) {
985
- if (!existsSync(path.join(projectRoot, '.git'))) {
986
- return false;
987
- }
988
- const result = spawnSync('git', ['status', '--porcelain'], { cwd: projectRoot, encoding: 'utf8' });
989
- return result.status === 0 && result.stdout.trim().length > 0;
990
- }
991
- function requestedProvisioningGroups(manifest, plan) {
992
- if (plan.workload === 'power-apps-code-app' ||
993
- manifest.project.workload.kind === 'power-apps-code-app') {
994
- return [];
995
- }
996
- const provisioned = new Set(manifest.projectArtifacts.map((artifact) => artifact.provisioningGroup));
997
- const groups = [];
998
- if (plan.includeFrontend &&
999
- !manifest.project.workload.frontend &&
1000
- !provisioned.has('frontend')) {
1001
- groups.push('frontend');
1002
- }
1003
- const recordedEnvironments = new Set(manifest.project.workload.environments);
1004
- for (const environment of plan.environments) {
1005
- const group = `environment:${environment.id}`;
1006
- if (!recordedEnvironments.has(environment.id) &&
1007
- !provisioned.has(group)) {
1008
- groups.push(group);
1009
- }
1010
- }
1011
- return groups;
1012
- }
1013
- async function inspectProvisioningGroups(projectRoot, render, groups) {
1014
- const projectArtifacts = render.filter((artifact) => artifact.lifecycle === 'project');
1015
- const plans = [];
1016
- for (const group of groups) {
1017
- const artifacts = projectArtifacts.filter((artifact) => artifact.provisioningGroup === group);
1018
- if (artifacts.length === 0) {
1019
- throw new Error(`Provisioning group ${group} did not render any project artifacts.`);
1020
- }
1021
- const entries = await Promise.all(artifacts.map(async (artifact) => {
1022
- const disk = await readProjectFile(projectRoot, artifact.pathParts);
1023
- if (disk === undefined) {
1024
- return {
1025
- group,
1026
- status: 'create',
1027
- rendered: artifact,
1028
- reason: 'destination is absent'
1029
- };
1030
- }
1031
- if (disk.toString('utf8') === artifact.content) {
1032
- return {
1033
- group,
1034
- status: 'adopt',
1035
- rendered: artifact,
1036
- reason: 'destination already matches the selected component'
1037
- };
1038
- }
1039
- return {
1040
- group,
1041
- status: 'conflict',
1042
- rendered: artifact,
1043
- reason: 'destination contains project-owned bytes'
1044
- };
1045
- }));
1046
- plans.push({
1047
- group,
1048
- entries,
1049
- blocked: entries.some((entry) => entry.status === 'conflict')
1050
- });
1051
- }
1052
- return plans;
1053
- }
1054
- function provisioningJson(plans) {
1055
- return plans.map((plan) => ({
1056
- group: plan.group,
1057
- status: plan.blocked ? 'blocked' : 'ready',
1058
- entries: plan.entries.map((entry) => ({
1059
- status: entry.status,
1060
- path: manifestDisplayPath(entry.rendered.pathParts),
1061
- reason: entry.reason
1062
- }))
1063
- }));
1064
- }
1065
- function projectProvenanceFor(artifact) {
1066
- return {
1067
- logicalName: artifact.logicalName,
1068
- category: artifact.category,
1069
- pathParts: artifact.pathParts,
1070
- generatedBy: liftoffVersion,
1071
- generationHash: `sha256:${createHash('sha256').update(artifact.content, 'utf8').digest('hex')}`,
1072
- provisioningGroup: artifact.provisioningGroup
1073
- };
1074
- }
1075
- function appendProvisionedProjectArtifacts(existing, plans) {
1076
- const byName = new Map(existing.map((artifact) => [artifact.logicalName, artifact]));
1077
- for (const plan of plans) {
1078
- if (plan.blocked) {
1079
- continue;
1080
- }
1081
- for (const entry of plan.entries) {
1082
- if (!byName.has(entry.rendered.logicalName)) {
1083
- byName.set(entry.rendered.logicalName, projectProvenanceFor(entry.rendered));
1084
- }
1085
- }
1086
- }
1087
- return [...byName.values()];
1088
- }
1089
- function sameWorkloadIntent(left, right) {
1090
- if (left.kind !== right.kind) {
1091
- return false;
1092
- }
1093
- if (left.kind === 'power-apps-code-app' && right.kind === 'power-apps-code-app') {
1094
- return left.codeAppsPlugin === right.codeAppsPlugin &&
1095
- left.starter.repository === right.starter.repository &&
1096
- left.starter.path === right.starter.path &&
1097
- left.starter.commit === right.starter.commit;
1098
- }
1099
- if (left.kind === 'power-apps-code-app' || right.kind === 'power-apps-code-app') {
1100
- return false;
1101
- }
1102
- return left.apiStack === right.apiStack &&
1103
- left.cloud === right.cloud &&
1104
- left.region === right.region &&
1105
- left.frontend === right.frontend &&
1106
- left.environments.length === right.environments.length &&
1107
- left.environments.every((environment, index) => environment === right.environments[index]) &&
1108
- (left.kind !== 'genai' || right.kind !== 'genai' || left.pattern === right.pattern);
1109
- }
1110
- function planWithBlockedProvisioning(plan, recordedWorkload, provisioningPlans) {
1111
- if (plan.workload === 'power-apps-code-app' ||
1112
- recordedWorkload.kind === 'power-apps-code-app') {
1113
- return plan;
1114
- }
1115
- const blockedGroups = new Set(provisioningPlans
1116
- .filter((group) => group.blocked)
1117
- .map((group) => group.group));
1118
- const hasBlockedEnvironment = [...blockedGroups].some((group) => group.startsWith('environment:'));
1119
- const environments = hasBlockedEnvironment
1120
- ? recordedWorkload.environments.map((id) => {
1121
- const environment = getEnvironment(id);
1122
- if (!environment) {
1123
- throw new Error(`Recorded environment ${id} is no longer supported.`);
1124
- }
1125
- return environment;
1126
- })
1127
- : plan.environments;
1128
- return {
1129
- ...plan,
1130
- includeFrontend: blockedGroups.has('frontend')
1131
- ? recordedWorkload.frontend
1132
- : plan.includeFrontend,
1133
- environments
1134
- };
1135
- }
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) {
1149
- for (const entry of entries) {
1150
- if (isUnownedUpdateConflict(entry, recordedByName)) {
1151
- continue;
1152
- }
1153
- const writesDestination = entry.status === 'new' ||
1154
- entry.status === 'missing' ||
1155
- entry.status === 'upgrade' ||
1156
- entry.status === 'moved' && (entry.cleanMove === true || force) ||
1157
- entry.status === 'conflict' && force;
1158
- const deletesDestination = entry.status === 'retired' ||
1159
- entry.status === 'retired-conflict' && force;
1160
- if (writesDestination) {
1161
- await resolveProjectPath(projectRoot, entry.pathParts);
1162
- }
1163
- if (deletesDestination) {
1164
- await resolveProjectPath(projectRoot, entry.pathParts);
1165
- }
1166
- if (entry.previousPathParts &&
1167
- (entry.status === 'moved' && (entry.cleanMove === true || force) || entry.status === 'conflict' && force)) {
1168
- await resolveProjectPath(projectRoot, entry.previousPathParts);
1169
- }
1170
- }
1171
- await resolveProjectPath(projectRoot, ['liftoff.manifest.json']);
1172
- }
1173
- function updateSnapshotKey(pathParts) {
1174
- return pathParts.join('\0');
1175
- }
1176
- async function captureUpdateSnapshots(projectRoot, manifest, render, initialSnapshots) {
1177
- const snapshots = new Map(initialSnapshots.map((snapshot) => [updateSnapshotKey(snapshot.pathParts), snapshot]));
1178
- const seenKeys = new Set(snapshots.keys());
1179
- const candidates = [
1180
- ...manifest.managedArtifacts.map((artifact) => artifact.pathParts),
1181
- ...render.map((artifact) => artifact.pathParts)
1182
- ];
1183
- const uncaptured = [];
1184
- for (const pathParts of candidates) {
1185
- const key = updateSnapshotKey(pathParts);
1186
- if (!seenKeys.has(key)) {
1187
- seenKeys.add(key);
1188
- uncaptured.push(pathParts);
1189
- }
1190
- }
1191
- for (const snapshot of await Promise.all(uncaptured.map((pathParts) => captureProjectFileSnapshot(projectRoot, pathParts)))) {
1192
- snapshots.set(updateSnapshotKey(snapshot.pathParts), snapshot);
1193
- }
1194
- return [...snapshots.values()];
1195
- }
1196
- function selectUpdatePreconditions(snapshots, entries, mutations, additionalPaths = []) {
1197
- const requiredKeys = new Set([
1198
- updateSnapshotKey(['liftoff.config.json']),
1199
- ...mutations.map((mutation) => updateSnapshotKey(mutation.pathParts)),
1200
- ...additionalPaths.map((pathParts) => updateSnapshotKey(pathParts))
1201
- ]);
1202
- for (const entry of entries) {
1203
- const adoptsExistingDestination = entry.status === 'moved' && entry.destinationMatches === true;
1204
- if (!(entry.status === 'unchanged' && entry.refreshHash) &&
1205
- !adoptsExistingDestination) {
1206
- continue;
1207
- }
1208
- requiredKeys.add(updateSnapshotKey(entry.pathParts));
1209
- if (entry.previousPathParts) {
1210
- requiredKeys.add(updateSnapshotKey(entry.previousPathParts));
1211
- }
1212
- }
1213
- return snapshots.filter((snapshot) => requiredKeys.has(updateSnapshotKey(snapshot.pathParts)));
1214
- }
1215
- function assertAuthorizedUpdateMutations(mutations, entries, provisioningPlans, additionalAuthorizedPaths = []) {
1216
- const authorized = new Set([
1217
- updateSnapshotKey(['liftoff.manifest.json']),
1218
- ...additionalAuthorizedPaths.map((pathParts) => updateSnapshotKey(pathParts))
1219
- ]);
1220
- for (const entry of entries) {
1221
- if (entry.rendered && entry.rendered.lifecycle !== 'managed-core') {
1222
- throw new Error(`Update reconciliation produced a non-core mutation candidate: ${entry.logicalName}`);
1223
- }
1224
- authorized.add(updateSnapshotKey(entry.pathParts));
1225
- if (entry.previousPathParts) {
1226
- authorized.add(updateSnapshotKey(entry.previousPathParts));
1227
- }
1228
- }
1229
- for (const plan of provisioningPlans) {
1230
- if (plan.blocked) {
1231
- continue;
1232
- }
1233
- for (const entry of plan.entries) {
1234
- if (entry.status === 'create') {
1235
- authorized.add(updateSnapshotKey(entry.rendered.pathParts));
1236
- }
1237
- }
1238
- }
1239
- for (const mutation of mutations) {
1240
- if (!authorized.has(updateSnapshotKey(mutation.pathParts))) {
1241
- throw new Error(`Update mutation is outside managed-core or authorized provisioning scope: ${manifestDisplayPath(mutation.pathParts)}`);
1242
- }
1243
- }
1244
- }
1245
- function identityFieldChanges(from, to = currentActivationIdentity) {
1246
- return Object.keys(to)
1247
- .filter((field) => from[field] !== undefined && from[field] !== to[field])
1248
- .map((field) => ({
1249
- field: `activationIdentity.${field}`,
1250
- from: from[field],
1251
- to: to[field]
1252
- }));
1253
- }
1254
- function manifestChanges(manifest, plannedManifest) {
1255
- const changes = [];
1256
- if (manifest.artifactVersion !== plannedManifest.artifactVersion) {
1257
- changes.push({
1258
- field: 'artifactVersion',
1259
- from: manifest.artifactVersion,
1260
- to: plannedManifest.artifactVersion
1261
- });
1262
- }
1263
- if (manifest.liftoffVersion !== plannedManifest.liftoffVersion) {
1264
- changes.push({
1265
- field: 'liftoffVersion',
1266
- from: manifest.liftoffVersion,
1267
- to: plannedManifest.liftoffVersion
1268
- });
1269
- }
1270
- if (manifest.governance.profile !== 'none' &&
1271
- manifest.governance.profile !== 'unspecified' &&
1272
- plannedManifest.governance.profile !== 'none' &&
1273
- plannedManifest.governance.profile !== 'unspecified') {
1274
- if (manifest.governance.policyVersion !== plannedManifest.governance.policyVersion) {
1275
- changes.push({
1276
- field: 'governance.policyVersion',
1277
- from: manifest.governance.policyVersion,
1278
- to: plannedManifest.governance.policyVersion
1279
- });
1280
- }
1281
- const fromIdentity = manifest.governance.activationIdentity;
1282
- const toIdentity = plannedManifest.governance.activationIdentity;
1283
- if (toIdentity) {
1284
- if (!fromIdentity) {
1285
- changes.push({
1286
- field: 'governance.activationIdentity',
1287
- from: null,
1288
- to: toIdentity
1289
- });
1290
- }
1291
- else {
1292
- changes.push(...identityFieldChanges(fromIdentity, toIdentity));
1293
- }
1294
- }
1295
- }
1296
- return changes;
1297
- }
1298
- function stateMigrationReconciliation(stateMigration) {
1299
- if (stateMigration.status === 'blocked') {
1300
- return {
1301
- status: 'blocked',
1302
- changedIdentityFields: [],
1303
- phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
1304
- issues: stateMigration.report.issues,
1305
- remedy: 'Preserve governance/activation-state.json byte-for-byte and provide an explicit versioned import mapping; checkboxes, filenames, and prose cannot become evidence.'
1306
- };
1307
- }
1308
- if (stateMigration.status === 'migrate') {
1309
- return {
1310
- status: 'reconciliation-required',
1311
- changedIdentityFields: identityFieldChanges(stateMigration.report.fromIdentity, stateMigration.report.toIdentity),
1312
- phaseImpact: {
1313
- preservedPhaseIds: stateMigration.report.preservedPhaseIds,
1314
- invalidPhaseIds: stateMigration.report.invalidPhaseIds
1315
- },
1316
- issues: [
1317
- `Historical activation state will be migrated with the managed update transaction; evidence bytes are preserved and ${stateMigration.report.reconciliationPath} records the explicit graph mapping.`
1318
- ],
1319
- remedy: 'Run governance status/verify after update and acknowledge the reconciliation record before executing affected phases.'
1320
- };
1321
- }
1322
- return {
1323
- status: 'not-required',
1324
- changedIdentityFields: [],
1325
- phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
1326
- issues: []
1327
- };
1328
- }
1329
- function activeChangePathParts(stateKind, changeId) {
1330
- return stateKind === 'openspec'
1331
- ? ['openspec', 'changes', changeId]
1332
- : ['specs', changeId];
1333
- }
1334
- async function activeChangeReconciliationReport(projectRoot) {
1335
- let loaded;
1336
- try {
1337
- loaded = await loadCurrentActivationState(projectRoot);
1338
- }
1339
- catch {
1340
- return {
1341
- status: 'not-required',
1342
- changedIdentityFields: [],
1343
- phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
1344
- issues: []
1345
- };
1346
- }
1347
- const activeChange = loaded?.state.activeChange;
1348
- if (!activeChange) {
1349
- return {
1350
- status: 'not-required',
1351
- changedIdentityFields: [],
1352
- phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
1353
- issues: []
1354
- };
1355
- }
1356
- const metadataPathParts = [
1357
- ...activeChangePathParts(activeChange.kind, activeChange.id),
1358
- governanceChangeMetadataFileName
1359
- ];
1360
- const bytes = await readProjectFile(projectRoot, metadataPathParts);
1361
- if (bytes === undefined) {
1362
- return {
1363
- status: 'blocked',
1364
- changedIdentityFields: [],
1365
- phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
1366
- issues: [
1367
- `${metadataPathParts.join('/')} is missing for active change ${activeChange.id}; no update mode can infer it from tasks or prose.`
1368
- ],
1369
- remedy: 'Restore schema-valid governance metadata or record an explicit supersession before continuing setup.'
1370
- };
1371
- }
1372
- let parsed;
1373
- try {
1374
- parsed = JSON.parse(bytes.toString('utf8'));
1375
- }
1376
- catch (error) {
1377
- return {
1378
- status: 'blocked',
1379
- changedIdentityFields: [],
1380
- phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
1381
- issues: [`Unable to parse ${metadataPathParts.join('/')}: ${error instanceof Error ? error.message : String(error)}`],
1382
- remedy: 'Restore schema-valid governance metadata; no update mode can infer active governance from checkboxes, filenames, or prose.'
1383
- };
1384
- }
1385
- let reconciliation;
1386
- try {
1387
- const metadata = validateGovernanceChangeMetadata(parsed);
1388
- reconciliation = reconcileActiveGovernanceChange({ metadata, evidence: [] });
1389
- }
1390
- catch (error) {
1391
- return {
1392
- status: 'blocked',
1393
- changedIdentityFields: [],
1394
- phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
1395
- issues: [`Invalid ${metadataPathParts.join('/')}: ${error instanceof Error ? error.message : String(error)}`],
1396
- remedy: 'Restore schema-valid governance metadata; no update mode can infer active governance from checkboxes, filenames, or prose.'
1397
- };
1398
- }
1399
- if (reconciliation.status === 'not-required') {
1400
- return {
1401
- status: 'not-required',
1402
- changedIdentityFields: [],
1403
- phaseImpact: {
1404
- preservedPhaseIds: reconciliation.preservedPhaseIds,
1405
- invalidPhaseIds: reconciliation.invalidPhaseIds
1406
- },
1407
- issues: []
1408
- };
1409
- }
1410
- if (reconciliation.status === 'blocked') {
1411
- return {
1412
- status: 'blocked',
1413
- changedIdentityFields: [],
1414
- phaseImpact: {
1415
- preservedPhaseIds: reconciliation.preservedPhaseIds,
1416
- invalidPhaseIds: reconciliation.invalidPhaseIds
1417
- },
1418
- issues: reconciliation.issues,
1419
- remedy: 'Upgrade Liftoff or add an explicit compatibility mapping before executing affected governance phases.'
1420
- };
1421
- }
1422
- return {
1423
- status: 'reconciliation-required',
1424
- changedIdentityFields: identityFieldChanges(reconciliation.fromIdentity, reconciliation.toIdentity),
1425
- phaseImpact: {
1426
- preservedPhaseIds: reconciliation.preservedPhaseIds,
1427
- invalidPhaseIds: reconciliation.invalidPhaseIds
1428
- },
1429
- issues: reconciliation.issues,
1430
- remedy: 'Managed definitions and manifest may be updated, but governance status/verify will block affected execution until the active change acknowledges the installed identity.'
1431
- };
1432
- }
1433
- function combineReconciliationReports(reports) {
1434
- if (reports.some((report) => report.status === 'blocked')) {
1435
- return {
1436
- status: 'blocked',
1437
- changedIdentityFields: reports.flatMap((report) => report.changedIdentityFields),
1438
- phaseImpact: {
1439
- preservedPhaseIds: [],
1440
- invalidPhaseIds: [...new Set(reports.flatMap((report) => report.phaseImpact.invalidPhaseIds))]
1441
- },
1442
- issues: reports.flatMap((report) => report.issues),
1443
- remedy: reports.find((report) => report.remedy)?.remedy
1444
- };
1445
- }
1446
- if (reports.some((report) => report.status === 'reconciliation-required')) {
1447
- return {
1448
- status: 'reconciliation-required',
1449
- changedIdentityFields: reports.flatMap((report) => report.changedIdentityFields),
1450
- phaseImpact: {
1451
- preservedPhaseIds: [...new Set(reports.flatMap((report) => report.phaseImpact.preservedPhaseIds))],
1452
- invalidPhaseIds: [...new Set(reports.flatMap((report) => report.phaseImpact.invalidPhaseIds))]
1453
- },
1454
- issues: reports.flatMap((report) => report.issues),
1455
- remedy: reports.find((report) => report.remedy)?.remedy
1456
- };
1457
- }
1458
- return {
1459
- status: 'not-required',
1460
- changedIdentityFields: [],
1461
- phaseImpact: { preservedPhaseIds: [], invalidPhaseIds: [] },
1462
- issues: []
1463
- };
1464
- }
1465
- function maybeInjectUpdateFailure(env, stage) {
1466
- const requested = env?.[updateFailureInjectionEnv] ?? process.env[updateFailureInjectionEnv];
1467
- if (requested === stage) {
1468
- throw new Error(`Injected managed-update failure at ${stage}.`);
1469
- }
1470
- }
1471
- async function updateCommand(parsed, context) {
1472
- const { presentation } = context;
1473
- const check = readBooleanFlag(parsed.flags, 'check') ?? false;
1474
- const force = readBooleanFlag(parsed.flags, 'force') ?? false;
1475
- const jsonMode = readBooleanFlag(parsed.flags, 'json') ?? false;
1476
- presentation.commandIdentity('update', 'Reconcile Liftoff-managed core files');
1477
- if (force && check) {
1478
- presentation.error('--force cannot be combined with --check.', 'Run `liftoff update --check` to inspect drift or `liftoff update --force` to overwrite conflicts.');
1479
- return 1;
1480
- }
1481
- const explicit = parsed.positional[0] ?? readStringFlag(parsed.flags, 'project');
1482
- const projectRoot = explicit ? path.resolve(context.cwd, explicit) : await findProjectRoot(context.cwd);
1483
- if (!projectRoot) {
1484
- 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.');
1485
- return 1;
1486
- }
1487
- const initialUpdateSnapshots = check
1488
- ? []
1489
- : await Promise.all([
1490
- captureProjectFileSnapshot(projectRoot, ['liftoff.manifest.json']),
1491
- captureProjectFileSnapshot(projectRoot, ['liftoff.config.json'])
1492
- ]);
1493
- const manifest = await loadManifest(projectRoot);
1494
- const oldByName = new Map(manifest.managedArtifacts.map((artifact) => [artifact.logicalName, artifact]));
1495
- if (compareSemver(manifest.liftoffVersion, liftoffVersion) > 0) {
1496
- presentation.error(`This project was written by Liftoff ${manifest.liftoffVersion}, which is newer than this CLI (${liftoffVersion}).`, 'Upgrade the CLI first.');
1497
- return 1;
1498
- }
1499
- const config = await loadConfigOptions('liftoff.config.json', projectRoot);
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
- }
1512
- if (plan.workload !== recordedWorkload.kind) {
1513
- const involvesPowerApps = plan.workload === 'power-apps-code-app' ||
1514
- recordedWorkload.kind === 'power-apps-code-app';
1515
- presentation.error(`Project type changes (${recordedWorkload.kind} -> ${plan.workload}) are not supported by update.`, involvesPowerApps
1516
- ? 'Initialize a fresh project for the new workload.'
1517
- : 'Initialize a fresh project and move production behavior through a reviewed project change.');
1518
- return 1;
1519
- }
1520
- if (plan.workload === 'power-apps-code-app' && recordedWorkload.kind === 'power-apps-code-app') {
1521
- const recordedStarter = recordedWorkload.starter;
1522
- const starterBaseline = supportedStack.upstreams['power-apps-code-app'];
1523
- if (plan.starter.repository !== starterBaseline.repository ||
1524
- plan.starter.path !== starterBaseline.path ||
1525
- recordedStarter.repository !== starterBaseline.repository ||
1526
- recordedStarter.path !== starterBaseline.path ||
1527
- !starterBaseline.compatibleSourceCommits.includes(recordedStarter.commit)) {
1528
- presentation.error('The Power Apps starter repository, path, or commit is not represented by this Liftoff release catalog.', 'Restore the recorded source identity or use a Liftoff version that catalogs the transition.');
1529
- return 1;
1530
- }
1531
- }
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
- }
1541
- if (plan.apiStack.id !== recordedWorkload.apiStack) {
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.');
1543
- return 1;
1544
- }
1545
- const recordedPattern = recordedWorkload.kind === 'genai' ? recordedWorkload.pattern : undefined;
1546
- const desiredPattern = plan.workload === 'genai' ? plan.pattern.id : undefined;
1547
- if (desiredPattern !== recordedPattern) {
1548
- 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.');
1549
- return 1;
1550
- }
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
- }
1559
- if (plan.specWorkflow.id !== manifest.project.specWorkflow) {
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.');
1561
- return 1;
1562
- }
1563
- const configuredAgents = plan.agents.map((agent) => agent.id);
1564
- if (manifest.framework.state === 'initialized' && (configuredAgents.length !== manifest.project.agents.length ||
1565
- configuredAgents.some((agent, index) => agent !== manifest.project.agents[index]) ||
1566
- plan.defaultAgent?.id !== manifest.project.defaultAgent)) {
1567
- 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.');
1568
- return 1;
1569
- }
1570
- let desiredRenderPlan = manifest.framework.state === 'legacy'
1571
- ? { ...plan, agents: [], defaultAgent: undefined }
1572
- : plan;
1573
- if (desiredRenderPlan.workload === 'power-apps-code-app' &&
1574
- recordedWorkload.kind === 'power-apps-code-app') {
1575
- desiredRenderPlan = {
1576
- ...desiredRenderPlan,
1577
- starter: recordedWorkload.starter
1578
- };
1579
- }
1580
- const desiredRender = buildArtifacts(desiredRenderPlan);
1581
- const provisioningPlans = await inspectProvisioningGroups(projectRoot, desiredRender, requestedProvisioningGroups(manifest, desiredRenderPlan));
1582
- const renderPlan = planWithBlockedProvisioning(desiredRenderPlan, recordedWorkload, provisioningPlans);
1583
- const render = buildArtifacts(renderPlan);
1584
- const scopedRender = [
1585
- ...render.filter((artifact) => artifact.lifecycle === 'managed-core'),
1586
- ...desiredRender.filter((artifact) => artifact.lifecycle === 'project' &&
1587
- provisioningPlans.some((group) => group.group === artifact.provisioningGroup))
1588
- ];
1589
- const updateSnapshots = check
1590
- ? []
1591
- : await captureUpdateSnapshots(projectRoot, manifest, scopedRender, initialUpdateSnapshots);
1592
- const entries = await reconcileProject(manifest, render, projectRoot);
1593
- const summary = summarizeEntries(entries);
1594
- const ownershipMigrationPending = manifest.artifactVersion !== 7 ||
1595
- manifestHadFilteredLegacyNonDurableOwnership(manifest);
1596
- const plannedManifest = buildManifest(renderPlan, render, {
1597
- frameworkState: manifest.framework.state,
1598
- projectArtifacts: manifest.projectArtifacts
1599
- });
1600
- const plannedManifestChanges = manifestChanges(manifest, plannedManifest);
1601
- const stateMigration = await planHistoricalActivationStateMigration(projectRoot);
1602
- const reconciliation = combineReconciliationReports([
1603
- stateMigrationReconciliation(stateMigration),
1604
- await activeChangeReconciliationReport(projectRoot)
1605
- ]);
1606
- if (reconciliation.status === 'blocked') {
1607
- const blockedReport = {
1608
- schemaVersion: 2,
1609
- mode: check ? 'check' : 'apply',
1610
- scope: 'managed-core',
1611
- status: 'blocked',
1612
- cliVersion: liftoffVersion,
1613
- projectVersion: manifest.liftoffVersion,
1614
- entries: [],
1615
- removed: [],
1616
- provisioning: [],
1617
- ownershipMigrationPending: false,
1618
- manifestChanges: plannedManifestChanges,
1619
- activationStateMigration: stateMigration.report,
1620
- reconciliation,
1621
- summary
1622
- };
1623
- if (jsonMode) {
1624
- presentation.rawStdout(`${JSON.stringify(blockedReport, null, 2)}\n`);
1625
- }
1626
- else {
1627
- presentation.status('error', 'Managed update blocked', reconciliation.issues.join('; '));
1628
- if (reconciliation.remedy) {
1629
- presentation.remedy(reconciliation.remedy);
1630
- }
1631
- }
1632
- return 1;
1633
- }
1634
- const workloadIntentChanged = !sameWorkloadIntent(plannedManifest.project.workload, manifest.project.workload);
1635
- const manifestRewritePending = ownershipMigrationPending ||
1636
- workloadIntentChanged;
1637
- const drift = hasDrift(entries) ||
1638
- manifestRewritePending ||
1639
- provisioningPlans.length > 0 ||
1640
- stateMigration.status === 'migrate';
1641
- const visible = entries.filter((entry) => entry.status !== 'unchanged' || entry.refreshHash);
1642
- if (!drift) {
1643
- if (jsonMode) {
1644
- presentation.rawStdout(`${JSON.stringify(check ? {
1645
- schemaVersion: 2,
1646
- mode: 'check',
1647
- scope: 'managed-core',
1648
- status: reconciliation.status === 'reconciliation-required' ? 'reconciliation-required' : 'current',
1649
- cliVersion: liftoffVersion,
1650
- projectVersion: manifest.liftoffVersion,
1651
- entries: [],
1652
- provisioning: [],
1653
- ownershipMigrationPending,
1654
- manifestChanges: plannedManifestChanges,
1655
- activationIdentity: manifest.governance.profile !== 'none' && manifest.governance.profile !== 'unspecified'
1656
- ? manifest.governance.activationIdentity ?? null
1657
- : null,
1658
- activationStateMigration: stateMigration.report,
1659
- reconciliation,
1660
- summary
1661
- } : {
1662
- schemaVersion: 2,
1663
- mode: 'apply',
1664
- scope: 'managed-core',
1665
- status: reconciliation.status === 'reconciliation-required' ? 'reconciliation-required' : 'current',
1666
- cliVersion: liftoffVersion,
1667
- projectVersion: manifest.liftoffVersion,
1668
- written: [],
1669
- removed: [],
1670
- skipped: [],
1671
- provisioning: [],
1672
- ownershipMigrationPending,
1673
- manifestChanges: plannedManifestChanges,
1674
- activationIdentity: manifest.governance.profile !== 'none' && manifest.governance.profile !== 'unspecified'
1675
- ? manifest.governance.activationIdentity ?? null
1676
- : null,
1677
- activationStateMigration: stateMigration.report,
1678
- reconciliation,
1679
- summary
1680
- }, null, 2)}\n`);
1681
- return 0;
1682
- }
1683
- presentation.definitions('Project versions', [
1684
- { label: 'Liftoff CLI', value: liftoffVersion },
1685
- { label: 'Project generated by', value: manifest.liftoffVersion }
1686
- ]);
1687
- presentation.status('success', 'Liftoff core is current', `${summary.unchanged} managed-core artifacts match; project files are not compared`);
1688
- return 0;
1689
- }
1690
- if (check) {
1691
- if (jsonMode) {
1692
- presentation.rawStdout(`${JSON.stringify({
1693
- schemaVersion: 2,
1694
- mode: 'check',
1695
- scope: 'managed-core',
1696
- cliVersion: liftoffVersion,
1697
- projectVersion: manifest.liftoffVersion,
1698
- entries: visible.map((entry) => ({
1699
- logicalName: entry.logicalName,
1700
- status: entry.status,
1701
- path: manifestDisplayPath(entry.pathParts),
1702
- previousPath: entry.previousPathParts ? manifestDisplayPath(entry.previousPathParts) : undefined,
1703
- fileDeleted: entry.status === 'retired' || entry.status === 'retired-conflict'
1704
- ? entry.destinationOccupied !== false
1705
- : undefined,
1706
- reason: entry.reason
1707
- })),
1708
- provisioning: provisioningJson(provisioningPlans),
1709
- ownershipMigrationPending,
1710
- manifestChanges: plannedManifestChanges,
1711
- activationIdentity: plannedManifest.governance.profile !== 'none' && plannedManifest.governance.profile !== 'unspecified'
1712
- ? plannedManifest.governance.activationIdentity ?? null
1713
- : null,
1714
- activationStateMigration: stateMigration.report,
1715
- reconciliation,
1716
- summary
1717
- }, null, 2)}\n`);
1718
- return 2;
1719
- }
1720
- presentation.definitions('Project versions', [
1721
- { label: 'Liftoff CLI', value: liftoffVersion },
1722
- { label: 'Project generated by', value: manifest.liftoffVersion }
1723
- ]);
1724
- if (visible.length > 0) {
1725
- if (presentation.stdout.layout === 'plain') {
1726
- presentation.section('Liftoff core drift', visible.map((entry) => `${entryMarker(entry)} ${entryDisplay(entry)} ${entry.reason}`));
1727
- }
1728
- else {
1729
- presentation.table('Liftoff core drift', ['Change', 'Artifact', 'Reason'], visible.map((entry) => [entryMarker(entry), entryDisplay(entry), entry.reason]));
1730
- }
1731
- }
1732
- for (const group of provisioningPlans) {
1733
- presentation.bullets(`Project component provisioning: ${group.group}`, group.entries.map((entry) => `${entry.status} ${manifestDisplayPath(entry.rendered.pathParts)} ${entry.reason}`));
1734
- }
1735
- const manifestMaintenance = [
1736
- ownershipMigrationPending
1737
- ? 'release legacy project artifacts from Liftoff update authority in manifest schema v7; no production file will be written'
1738
- : undefined,
1739
- workloadIntentChanged
1740
- ? 'record the requested project configuration intent after safe provisioning'
1741
- : undefined
1742
- ].filter((item) => item !== undefined);
1743
- if (manifestMaintenance.length > 0) {
1744
- presentation.bullets('Manifest maintenance', manifestMaintenance);
1745
- }
1746
- if (stateMigration.status === 'migrate') {
1747
- presentation.bullets('Activation-state migration', [
1748
- `${stateMigration.report.path}: explicit compatibility mapping preserves evidence bytes and stages ${stateMigration.report.reconciliationPath}`
1749
- ]);
1750
- }
1751
- if (reconciliation.status === 'reconciliation-required') {
1752
- presentation.bullets('Reconciliation required after update', [
1753
- ...reconciliation.changedIdentityFields.map((field) => `${field.field}: ${JSON.stringify(field.from)} -> ${JSON.stringify(field.to)}`),
1754
- `invalid phases: ${reconciliation.phaseImpact.invalidPhaseIds.join(', ') || 'none'}`,
1755
- reconciliation.remedy ?? 'Run governance status/verify before executing affected phases.'
1756
- ]);
1757
- }
1758
- const toWrite = summary.new +
1759
- summary.missing +
1760
- summary.upgrade +
1761
- summary.moved +
1762
- summary.refresh +
1763
- provisioningPlans.reduce((count, group) => count + (group.blocked
1764
- ? 0
1765
- : group.entries.filter((entry) => entry.status === 'create').length), 0) +
1766
- (manifestRewritePending ? 1 : 0) +
1767
- stateMigration.mutations.length;
1768
- const toRetire = summary.retired;
1769
- presentation.status('warning', 'Liftoff core maintenance available', `${toWrite} to write, ${toRetire} retired alias ownership record(s) to remove (${summary.retiredRemoved} file deletion(s)), ${summary.conflict + summary.retiredConflict} core conflict(s), ${summary.orphan} core orphan(s), ${summary.unchanged} core unchanged`);
1770
- if (toWrite > 0 || toRetire > 0) {
1771
- presentation.command('liftoff update');
1772
- }
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))) {
1780
- presentation.command('liftoff update --force');
1781
- }
1782
- return 2;
1783
- }
1784
- const blockedProvisioning = provisioningPlans.filter((group) => group.blocked);
1785
- if (isDirtyGitWorktree(projectRoot)) {
1786
- const warning = 'The project worktree has uncommitted changes; consider committing before applying.';
1787
- if (jsonMode) {
1788
- presentation.rawStderr(`Warning: ${warning}\n`);
1789
- }
1790
- else {
1791
- presentation.warning(warning);
1792
- }
1793
- }
1794
- presentation.stage('Apply safe Liftoff core changes', projectRoot);
1795
- await preflightUpdate(projectRoot, entries, force, oldByName);
1796
- maybeInjectUpdateFailure(context.env, 'after-preflight');
1797
- const written = [];
1798
- const retired = [];
1799
- const skipped = [];
1800
- const mutations = [];
1801
- for (const entry of entries) {
1802
- switch (entry.status) {
1803
- case 'new':
1804
- case 'missing':
1805
- case 'upgrade':
1806
- mutations.push({
1807
- type: 'write',
1808
- pathParts: entry.pathParts,
1809
- content: entry.rendered.content
1810
- });
1811
- written.push(entry);
1812
- break;
1813
- case 'moved':
1814
- if (!isUnownedUpdateConflict(entry, oldByName) && (entry.cleanMove || force)) {
1815
- if (!entry.destinationMatches) {
1816
- mutations.push({
1817
- type: 'write',
1818
- pathParts: entry.pathParts,
1819
- content: entry.rendered.content
1820
- });
1821
- }
1822
- mutations.push({ type: 'delete', pathParts: entry.previousPathParts });
1823
- written.push(entry);
1824
- }
1825
- else {
1826
- skipped.push(entry);
1827
- }
1828
- break;
1829
- case 'retired':
1830
- mutations.push({ type: 'delete', pathParts: entry.pathParts });
1831
- retired.push(entry);
1832
- break;
1833
- case 'retired-conflict':
1834
- if (force) {
1835
- mutations.push({ type: 'delete', pathParts: entry.pathParts });
1836
- retired.push(entry);
1837
- }
1838
- else {
1839
- skipped.push(entry);
1840
- }
1841
- break;
1842
- case 'conflict':
1843
- if (force && !isUnownedUpdateConflict(entry, oldByName)) {
1844
- mutations.push({
1845
- type: 'write',
1846
- pathParts: entry.pathParts,
1847
- content: entry.rendered.content
1848
- });
1849
- if (entry.previousPathParts) {
1850
- mutations.push({ type: 'delete', pathParts: entry.previousPathParts });
1851
- }
1852
- written.push(entry);
1853
- }
1854
- else {
1855
- skipped.push(entry);
1856
- }
1857
- break;
1858
- default:
1859
- break;
1860
- }
1861
- }
1862
- const provisioned = [];
1863
- for (const group of provisioningPlans) {
1864
- if (group.blocked) {
1865
- continue;
1866
- }
1867
- for (const entry of group.entries) {
1868
- if (entry.status === 'create') {
1869
- mutations.push({
1870
- type: 'write',
1871
- pathParts: entry.rendered.pathParts,
1872
- content: entry.rendered.content
1873
- });
1874
- }
1875
- provisioned.push(entry);
1876
- }
1877
- }
1878
- for (const mutation of stateMigration.mutations) {
1879
- mutations.push(mutation);
1880
- }
1881
- const skippedByName = new Map(skipped.map((entry) => [entry.logicalName, entry]));
1882
- const hasUnrecordedGovernanceConflict = skipped.some((entry) => entry.status === 'conflict' &&
1883
- entry.rendered?.category === 'governance' &&
1884
- !oldByName.has(entry.logicalName));
1885
- const hasProtectedRetiredAlias = skipped.some((entry) => entry.status === 'retired-conflict');
1886
- const nextProjectArtifacts = appendProvisionedProjectArtifacts(manifest.projectArtifacts, provisioningPlans);
1887
- const nextManifest = buildManifest(renderPlan, render.filter((artifact) => artifact.logicalName !== 'manifest'), {
1888
- frameworkState: manifest.framework.state,
1889
- projectArtifacts: nextProjectArtifacts
1890
- });
1891
- nextManifest.framework = manifest.framework;
1892
- nextManifest.project.specWorkflow = manifest.project.specWorkflow;
1893
- nextManifest.project.agents = manifest.project.agents;
1894
- if (manifest.project.defaultAgent) {
1895
- nextManifest.project.defaultAgent = manifest.project.defaultAgent;
1896
- }
1897
- else {
1898
- delete nextManifest.project.defaultAgent;
1899
- }
1900
- if ((hasUnrecordedGovernanceConflict || hasProtectedRetiredAlias) &&
1901
- nextManifest.governance.profile !== 'none') {
1902
- nextManifest.governance.state = 'handoff-partial';
1903
- }
1904
- nextManifest.managedArtifacts = nextManifest.managedArtifacts.flatMap((artifact) => {
1905
- if (!skippedByName.has(artifact.logicalName)) {
1906
- return [artifact];
1907
- }
1908
- const previous = oldByName.get(artifact.logicalName);
1909
- if (!previous) {
1910
- return [];
1911
- }
1912
- return [{ ...artifact, pathParts: previous.pathParts, contentHash: previous.contentHash }];
1913
- });
1914
- for (const entry of entries) {
1915
- if (entry.status === 'orphan') {
1916
- const previous = oldByName.get(entry.logicalName);
1917
- if (plan.governanceProfile.id === 'none' &&
1918
- previous.category === 'governance') {
1919
- continue;
1920
- }
1921
- nextManifest.managedArtifacts.push(previous);
1922
- }
1923
- if (entry.status === 'retired-conflict' && !force) {
1924
- const previous = oldByName.get(entry.logicalName);
1925
- if (plan.governanceProfile.id === 'none' &&
1926
- previous.category === 'governance') {
1927
- continue;
1928
- }
1929
- nextManifest.managedArtifacts.push(previous);
1930
- }
1931
- }
1932
- mutations.push({
1933
- type: 'write',
1934
- pathParts: ['liftoff.manifest.json'],
1935
- content: `${JSON.stringify(nextManifest, null, 2)}\n`
1936
- });
1937
- assertAuthorizedUpdateMutations(mutations, entries, provisioningPlans, stateMigration.mutations.map((mutation) => mutation.pathParts));
1938
- await applyProjectFileTransaction(projectRoot, mutations, {
1939
- onBeforeMutation: async (mutation, index) => {
1940
- maybeInjectUpdateFailure(context.env, `before-mutation:${index}`);
1941
- maybeInjectUpdateFailure(context.env, `before-path:${mutation.pathParts.join('/')}`);
1942
- },
1943
- preconditions: [
1944
- ...selectUpdatePreconditions(updateSnapshots, entries, mutations, provisioned.map((entry) => entry.rendered.pathParts)),
1945
- ...stateMigration.preconditions
1946
- ]
1947
- });
1948
- if (jsonMode) {
1949
- presentation.rawStdout(`${JSON.stringify({
1950
- schemaVersion: 2,
1951
- mode: 'apply',
1952
- scope: 'managed-core',
1953
- status: blockedProvisioning.length > 0 || skipped.length > 0
1954
- ? 'partial'
1955
- : reconciliation.status === 'reconciliation-required'
1956
- ? 'reconciliation-required'
1957
- : 'applied',
1958
- cliVersion: liftoffVersion,
1959
- projectVersion: manifest.liftoffVersion,
1960
- written: written.map((entry) => manifestDisplayPath(entry.pathParts)),
1961
- removed: retired.map((entry) => ({
1962
- logicalName: entry.logicalName,
1963
- status: entry.status === 'retired-conflict'
1964
- ? 'force-retired'
1965
- : entry.destinationOccupied === false
1966
- ? 'retired-absent'
1967
- : 'retired',
1968
- path: manifestDisplayPath(entry.pathParts),
1969
- fileDeleted: entry.destinationOccupied !== false,
1970
- reason: entry.reason
1971
- })),
1972
- stateWritten: stateMigration.mutations.map((mutation) => manifestDisplayPath(mutation.pathParts)),
1973
- skipped: skipped.map((entry) => ({
1974
- logicalName: entry.logicalName,
1975
- status: entry.status,
1976
- path: manifestDisplayPath(entry.pathParts),
1977
- reason: entry.reason
1978
- })),
1979
- provisioning: provisioningJson(provisioningPlans),
1980
- ownershipMigrationPending,
1981
- manifestChanges: manifestChanges(manifest, nextManifest),
1982
- activationIdentity: nextManifest.governance.profile !== 'none' && nextManifest.governance.profile !== 'unspecified'
1983
- ? nextManifest.governance.activationIdentity ?? null
1984
- : null,
1985
- activationStateMigration: stateMigration.report,
1986
- reconciliation,
1987
- summary
1988
- }, null, 2)}\n`);
1989
- return 0;
1990
- }
1991
- if (written.length > 0) {
1992
- presentation.bullets('Applied Liftoff core changes', written.map((entry) => `wrote ${entryDisplay(entry)}`));
1993
- }
1994
- const retiredDeleted = retired.filter((entry) => entry.destinationOccupied !== false);
1995
- const retiredAbsent = retired.filter((entry) => entry.destinationOccupied === false);
1996
- if (retiredDeleted.length > 0) {
1997
- presentation.bullets('Removed retired Liftoff aliases', retiredDeleted.map((entry) => `removed ${entryDisplay(entry)} ${entry.reason}`));
1998
- }
1999
- if (retiredAbsent.length > 0) {
2000
- presentation.bullets('Retired absent Liftoff alias ownership', retiredAbsent.map((entry) => `removed manifest ownership for ${entryDisplay(entry)} ${entry.reason}`));
2001
- }
2002
- if (stateMigration.status === 'migrate') {
2003
- presentation.bullets('Migrated activation state', stateMigration.mutations.map((mutation) => `wrote ${manifestDisplayPath(mutation.pathParts)}; evidence bytes preserved`));
2004
- }
2005
- if (provisioned.length > 0) {
2006
- presentation.bullets('Provisioned project components', provisioned.map((entry) => `${entry.status === 'create' ? 'wrote' : 'adopted'} ${manifestDisplayPath(entry.rendered.pathParts)}`));
2007
- }
2008
- if (blockedProvisioning.length > 0) {
2009
- presentation.bullets('Blocked project component provisioning', blockedProvisioning.flatMap((group) => group.entries
2010
- .filter((entry) => entry.status === 'conflict')
2011
- .map((entry) => `${group.group}: preserved ${manifestDisplayPath(entry.rendered.pathParts)}; --force cannot overwrite project-owned bytes`)));
2012
- }
2013
- if (skipped.length > 0) {
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)'}`));
2019
- }
2020
- const orphans = entries.filter((entry) => entry.status === 'orphan');
2021
- if (orphans.length > 0) {
2022
- presentation.bullets('Orphaned Liftoff core artifacts', orphans.map((entry) => `orphan ${entryDisplay(entry)} ${entry.reason}`));
2023
- }
2024
- if (reconciliation.status === 'reconciliation-required') {
2025
- presentation.bullets('Reconciliation required', [
2026
- ...reconciliation.changedIdentityFields.map((field) => `${field.field}: ${JSON.stringify(field.from)} -> ${JSON.stringify(field.to)}`),
2027
- `invalid phases: ${reconciliation.phaseImpact.invalidPhaseIds.join(', ') || 'none'}`,
2028
- reconciliation.remedy ?? 'Run governance status/verify before executing affected phases.'
2029
- ]);
2030
- }
2031
- presentation.completion('Updated project', [
2032
- `${written.length} core written`,
2033
- `${retired.length} retired ownership removed`,
2034
- `${retiredDeleted.length} retired file deleted`,
2035
- `${provisioned.length} project provisioned`,
2036
- ...(blockedProvisioning.length > 0
2037
- ? [`${blockedProvisioning.length} provisioning group(s) blocked`]
2038
- : []),
2039
- `${skipped.length} core skipped`
2040
- ].join(', '), [{ label: 'Manifest version', value: liftoffVersion }], 'liftoff validate && liftoff doctor');
2041
- return 0;
2042
- }
2043
- function renderSelfUpgradeResult(value, presentation) {
2044
- const details = [
2045
- { label: 'Current CLI', value: value.currentVersion },
2046
- ...('targetVersion' in value && value.targetVersion
2047
- ? [{ label: 'Canonical target', value: value.targetVersion }]
2048
- : []),
2049
- ...('registryKind' in value && value.registryKind
2050
- ? [{ label: 'Delivery registry', value: value.registryKind }]
2051
- : [])
2052
- ];
2053
- presentation.definitions('CLI upgrade result', details);
2054
- switch (value.status) {
2055
- case 'current':
2056
- presentation.status('success', 'CLI is current', selfUpgradeSummary(value));
2057
- return;
2058
- case 'update-available':
2059
- presentation.status('warning', 'CLI update available', selfUpgradeSummary(value));
2060
- presentation.command('liftoff upgrade');
2061
- return;
2062
- case 'upgraded':
2063
- presentation.completion('Liftoff CLI upgraded', selfUpgradeSummary(value), [{ label: 'Installed version', value: value.targetVersion }], 'liftoff update --check');
2064
- return;
2065
- case 'blocked':
2066
- case 'failed': {
2067
- presentation.status(value.status === 'blocked' ? 'warning' : 'error', value.status === 'blocked' ? 'CLI upgrade blocked' : 'CLI upgrade failed', selfUpgradeSummary(value));
2068
- const remedy = selfUpgradeRemedy(value);
2069
- if (remedy) {
2070
- presentation.remedy(remedy);
2071
- }
2072
- return;
2073
- }
2074
- default: {
2075
- const exhaustive = value;
2076
- throw new Error(`Unhandled self-upgrade result: ${String(exhaustive)}`);
2077
- }
2078
- }
2079
- }
2080
- async function upgradeCommand(parsed, context) {
2081
- const mode = readBooleanFlag(parsed.flags, 'check') === true ? 'check' : 'apply';
2082
- const json = readBooleanFlag(parsed.flags, 'json') === true;
2083
- if (!json) {
2084
- context.presentation.commandIdentity('upgrade', 'Replace the supported global Liftoff CLI installation');
2085
- }
2086
- const execute = context.selfUpgrade ?? ((request) => runSelfUpgrade(request, {
2087
- environment: context.env ?? process.env
2088
- }));
2089
- let value;
2090
- try {
2091
- value = await execute({
2092
- mode,
2093
- currentVersion: liftoffVersion,
2094
- stdout: context.stdout,
2095
- stderr: context.stderr,
2096
- json,
2097
- ...(!json
2098
- ? {
2099
- onStage: (stage, detail) => context.presentation.stage(stage, detail),
2100
- onInstallCommand: (command) => context.presentation.command(formatCommand(command))
2101
- }
2102
- : {})
2103
- });
2104
- }
2105
- catch {
2106
- value = {
2107
- schemaVersion: 1,
2108
- mode,
2109
- status: 'failed',
2110
- currentVersion: liftoffVersion,
2111
- reasonCode: 'verification_failed'
2112
- };
2113
- }
2114
- if (json) {
2115
- context.presentation.rawStdout(`${JSON.stringify(value, null, 2)}\n`);
2116
- }
2117
- else {
2118
- renderSelfUpgradeResult(value, context.presentation);
2119
- }
2120
- return selfUpgradeExitCode(value);
2121
- }
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
- }
2131
- if (result.status !== 0) {
2132
- return {
2133
- check: { label, severity: 'fail', detail: 'not found', remedy },
2134
- available: result.errorCode !== 'ENOENT'
2135
- };
2136
- }
2137
- const output = (result.stdout || result.stderr).split('\n')[0].trim();
2138
- const match = output.match(/(\d+)\.(\d+)/);
2139
- if (!match) {
2140
- return {
2141
- check: { label, severity: 'fail', detail: `unable to determine version from "${output}"`, remedy },
2142
- available: true
2143
- };
2144
- }
2145
- const found = [Number(match[1]), Number(match[2])];
2146
- if (found[0] < minimum[0] || found[0] === minimum[0] && found[1] < minimum[1]) {
2147
- return {
2148
- check: { label, severity: 'fail', detail: `${output} is below ${minimum.join('.')}`, remedy },
2149
- available: true
2150
- };
2151
- }
2152
- return {
2153
- check: { label, severity: 'ok', detail: output },
2154
- available: true
2155
- };
2156
- }
2157
- async function pythonRuntime(runner) {
2158
- const [major, minor] = (supportedStack.runtimes.python.minimumVersion ??
2159
- supportedStack.runtimes.python.version).split('.');
2160
- const minimum = [Number(major), Number(minor)];
2161
- const candidates = process.platform === 'win32'
2162
- ? [
2163
- { command: 'py', versionArgs: ['-3', '--version'], commandArgs: ['-3'] },
2164
- { command: 'python', versionArgs: ['--version'], commandArgs: [] },
2165
- { command: 'python3', versionArgs: ['--version'], commandArgs: [] }
2166
- ]
2167
- : [
2168
- { command: 'python3', versionArgs: ['--version'], commandArgs: [] },
2169
- { command: 'python', versionArgs: ['--version'], commandArgs: [] }
2170
- ];
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;
2182
- }
2183
- function doctorCheckFromProbe(probe) {
2184
- const severity = probe.state === 'ready'
2185
- ? 'ok'
2186
- : probe.requirement.severity === 'blocking'
2187
- ? 'fail'
2188
- : 'warn';
2189
- return {
2190
- id: probe.requirement.id,
2191
- label: probe.requirement.id,
2192
- severity,
2193
- state: probe.state,
2194
- requirementSeverity: probe.requirement.severity,
2195
- detail: probe.detail,
2196
- ...(probe.remedy ? { remedy: probe.remedy } : {})
2197
- };
2198
- }
2199
- function noticeId(requirementId, label) {
2200
- const suffix = label.toLowerCase().replace(/[^a-z0-9]+/g, '-').replace(/^-|-$/g, '');
2201
- return `${requirementId}:${suffix}`;
2202
- }
2203
- function workstationLayer(probes) {
2204
- const checks = probes.flatMap((probe) => [
2205
- doctorCheckFromProbe(probe),
2206
- ...probe.notices.map((notice) => ({
2207
- id: noticeId(probe.requirement.id, notice.label),
2208
- label: notice.label,
2209
- severity: notice.state === 'ready' ? 'ok' : 'warn',
2210
- state: notice.state,
2211
- requirementSeverity: 'advisory',
2212
- detail: notice.detail,
2213
- ...(notice.remedy ? { remedy: notice.remedy } : {})
2214
- }))
2215
- ]);
2216
- return { title: 'Environment', checks };
2217
- }
2218
- function workstationSelectionFromManifest(manifest) {
2219
- const framework = getFrameworkDefinition(manifest.project.specWorkflow);
2220
- const workload = manifest.project.workload;
2221
- return {
2222
- workload: workload.kind === 'power-apps-code-app'
2223
- ? { kind: 'power-apps-code-app' }
2224
- : {
2225
- kind: workload.kind,
2226
- apiStack: { id: workload.apiStack },
2227
- provider: { id: workload.cloud }
2228
- },
2229
- specWorkflow: { id: manifest.project.specWorkflow },
2230
- framework: { version: framework.version },
2231
- agents: manifest.framework.state === 'initialized'
2232
- ? manifest.project.agents.map((id) => {
2233
- const agent = getCodingAgent(id);
2234
- if (!agent) {
2235
- throw new Error(`Manifest references unknown coding agent ${id}.`);
2236
- }
2237
- return { id: agent.id, label: agent.label };
2238
- })
2239
- : []
2240
- };
2241
- }
2242
- async function frameworkDoctorChecks(projectRoot, manifest) {
2243
- if (manifest.framework.state === 'legacy') {
2244
- return [{
2245
- id: 'framework-legacy-state',
2246
- label: 'framework state',
2247
- severity: 'warn',
2248
- state: 'not-observable',
2249
- detail: `Legacy v${manifest.artifactVersion} manifest does not prove official ${manifest.framework.adapter} initialization or configured coding agents.`,
2250
- remedy: 'Reinitialize the framework explicitly before recording a v3 initialized framework contract.'
2251
- }];
2252
- }
2253
- const expected = getFrameworkDefinition(manifest.framework.adapter);
2254
- const frameworkLabel = getSpecWorkflow(manifest.framework.adapter)?.label ?? manifest.framework.adapter;
2255
- const contract = manifest.framework.contractVersion === expected.version
2256
- ? {
2257
- id: 'framework-contract',
2258
- label: 'framework contract',
2259
- severity: 'ok',
2260
- state: 'ready',
2261
- detail: `${frameworkLabel} ${expected.version}`
2262
- }
2263
- : {
2264
- id: 'framework-contract',
2265
- label: 'framework contract',
2266
- severity: 'fail',
2267
- state: 'outdated',
2268
- detail: `Manifest records ${manifest.framework.contractVersion}; this Liftoff version requires ${expected.version}.`,
2269
- remedy: `Install ${frameworkLabel} ${expected.version} and reinitialize its integrations.`
2270
- };
2271
- const markerIssues = await validateFrameworkInstallation(projectRoot, {
2272
- workflow: manifest.framework.adapter,
2273
- agents: manifest.project.agents,
2274
- ...(manifest.project.defaultAgent ? { defaultAgent: manifest.project.defaultAgent } : {})
2275
- });
2276
- const markers = markerIssues.length === 0
2277
- ? {
2278
- id: 'framework-markers',
2279
- label: 'framework markers',
2280
- severity: 'ok',
2281
- state: 'ready',
2282
- detail: `${manifest.project.agents.length} selected integration${manifest.project.agents.length === 1 ? '' : 's'} verified`
2283
- }
2284
- : {
2285
- id: 'framework-markers',
2286
- label: 'framework markers',
2287
- severity: 'fail',
2288
- state: 'unhealthy',
2289
- detail: `${markerIssues.length} issue(s): ${markerIssues[0]}`,
2290
- remedy: `Run the official ${frameworkLabel} initializer for the selected integrations.`
2291
- };
2292
- return [
2293
- contract,
2294
- {
2295
- id: 'selected-agents',
2296
- label: 'selected agents',
2297
- severity: 'ok',
2298
- state: 'ready',
2299
- detail: manifest.project.agents.join(', ')
2300
- },
2301
- markers
2302
- ];
2303
- }
2304
- async function stackProjectCheck(projectRoot, apiStack, runner) {
2305
- const requiredMetadata = {
2306
- 'python-fastapi': ['backend/pyproject.toml', 'backend/uv.lock'],
2307
- 'node-fastify': ['backend/package.json', 'backend/package-lock.json'],
2308
- 'go-huma': ['backend/go.mod', 'backend/go.sum']
2309
- };
2310
- const missingMetadata = requiredMetadata[apiStack].find((relativePath) => !existsSync(path.join(projectRoot, ...relativePath.split('/'))));
2311
- if (missingMetadata) {
2312
- return {
2313
- label: `${apiStack} project`,
2314
- severity: 'fail',
2315
- detail: `missing locked dependency metadata: ${missingMetadata}`,
2316
- remedy: `restore or repair the project-owned ${missingMetadata}`
2317
- };
2318
- }
2319
- if (apiStack === 'python-fastapi') {
2320
- const pyproject = readFileSync(path.join(projectRoot, 'backend', 'pyproject.toml'), 'utf8');
2321
- const lock = readFileSync(path.join(projectRoot, 'backend', 'uv.lock'), 'utf8');
2322
- const projectName = pyproject.match(/^\s*name\s*=\s*"([^"]+)"/m)?.[1];
2323
- const lockedProjectName = lock.match(/\[\[package\]\]\s+name\s*=\s*"([^"]+)"\s+version\s*=\s*"[^"]+"\s+source\s*=\s*\{\s*editable\s*=\s*"\."\s*\}/m)?.[1];
2324
- if (!projectName || lockedProjectName !== projectName) {
2325
- return {
2326
- label: `${apiStack} project`,
2327
- severity: 'fail',
2328
- detail: 'pyproject.toml and uv.lock do not identify the same project',
2329
- remedy: 'repair the project-owned backend/pyproject.toml and backend/uv.lock'
2330
- };
2331
- }
2332
- }
2333
- let result;
2334
- switch (apiStack) {
2335
- case 'python-fastapi':
2336
- {
2337
- const python = await pythonRuntime(runner);
2338
- if (!python) {
2339
- return { label: 'python project', severity: 'skipped', detail: 'python is unavailable' };
2340
- }
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);
2342
- }
2343
- break;
2344
- case 'node-fastify':
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);
2346
- break;
2347
- case 'go-huma':
2348
- result = await runReadOnly('go', ['mod', 'edit', '-json'], path.join(projectRoot, 'backend'), runner);
2349
- if (result.errorCode === 'ENOENT') {
2350
- return { label: 'go project', severity: 'skipped', detail: 'go is unavailable' };
2351
- }
2352
- break;
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
- }
2362
- if (result.status === 0) {
2363
- return { label: `${apiStack} project`, severity: 'ok', detail: 'stack configuration is valid' };
2364
- }
2365
- return {
2366
- label: `${apiStack} project`,
2367
- severity: 'fail',
2368
- detail: (result.stderr || result.stdout || 'stack validation failed').split('\n')[0],
2369
- remedy: `repair the generated ${apiStack} backend configuration`
2370
- };
2371
- }
2372
- async function powerAppsProjectCheck(projectRoot) {
2373
- const issues = await verifyPowerAppsPackageMetadata(projectRoot);
2374
- if (issues.length === 0) {
2375
- return {
2376
- id: 'power-apps-project',
2377
- label: 'Power Apps project',
2378
- severity: 'ok',
2379
- state: 'ready',
2380
- detail: 'SDK, Vite plugin, and deterministic lockfile metadata are valid'
2381
- };
2382
- }
2383
- return {
2384
- id: 'power-apps-project',
2385
- label: 'Power Apps project',
2386
- severity: 'fail',
2387
- state: 'unhealthy',
2388
- detail: issues[0],
2389
- remedy: 'repair the project-owned package.json and package-lock.json'
2390
- };
2391
- }
2392
- function runReadOnly(command, args, cwd, runner) {
2393
- return runner.run({ executable: command, args }, { cwd, timeoutMs: doctorProbeTimeoutMs });
2394
- }
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;
2398
- }
2399
- async function azureCloudChecks(runner) {
2400
- const auth = await runner.run({ executable: 'az', args: ['account', 'show', '-o', 'none', '--only-show-errors'] }, { timeoutMs: 15_000 });
2401
- if (auth.errorCode === 'ENOENT') {
2402
- return [{ label: 'az', severity: 'warn', detail: 'Azure CLI not found', remedy: 'install the Azure CLI' }];
2403
- }
2404
- if (auth.status === 0) {
2405
- return [{ label: 'azure auth', severity: 'ok', detail: 'authenticated' }];
2406
- }
2407
- return [{ label: 'azure auth', severity: 'warn', detail: 'not authenticated', remedy: 'run az login' }];
2408
- }
2409
- // ponytail: provider-keyed map so aws/gcp checks slot in when their adapters land
2410
- const CLOUD_CHECKS = {
2411
- azure: azureCloudChecks
2412
- };
2413
- async function cloudLayer(cloud, runner) {
2414
- const checks = CLOUD_CHECKS[cloud]
2415
- ? await CLOUD_CHECKS[cloud](runner)
2416
- : [{ label: cloud, severity: 'skipped', detail: `${cloud} provider checks are not available yet` }];
2417
- return { title: `Cloud - ${cloud}`, checks };
2418
- }
2419
- async function cliLayer(releaseLookup, configuredRegistryLookup) {
2420
- const checks = [
2421
- { label: 'version', severity: 'ok', detail: `Liftoff ${liftoffVersion}` }
2422
- ];
2423
- let latest;
2424
- try {
2425
- latest = (await releaseLookup()).version;
2426
- }
2427
- catch {
2428
- return { title: 'CLI', checks };
2429
- }
2430
- if (compareSemver(latest, liftoffVersion) > 0) {
2431
- let registryState = {
2432
- status: 'unavailable'
2433
- };
2434
- try {
2435
- registryState = await configuredRegistryLookup(latest);
2436
- }
2437
- catch {
2438
- // Canonical freshness remains useful when local npm registry inspection fails.
2439
- }
2440
- const manual = canonicalManualInstallCommand(latest);
2441
- checks.push({
2442
- label: 'cli freshness',
2443
- severity: 'warn',
2444
- detail: registryState.status === 'stale'
2445
- ? `Liftoff ${latest} is published, but the configured npm registry does not expose it`
2446
- : `Liftoff ${latest} is published, this CLI is ${liftoffVersion}`,
2447
- remedy: registryState.status === 'stale'
2448
- ? 'ask the managed registry owner to synchronize the canonical target, then run liftoff upgrade --check'
2449
- : `run liftoff upgrade --check, then liftoff upgrade; manual fallback where canonical npm is permitted: ${manual}`
2450
- });
2451
- }
2452
- else {
2453
- checks.push({
2454
- label: 'cli freshness',
2455
- severity: 'ok',
2456
- detail: `running ${liftoffVersion}, latest stable ${latest}`
2457
- });
2458
- }
2459
- return { title: 'CLI', checks };
2460
- }
2461
- async function projectLayer(projectRoot, manifest, runner) {
2462
- const checks = [];
2463
- const issues = await validateGeneratedProject(projectRoot);
2464
- if (issues.length > 0) {
2465
- checks.push({
2466
- label: 'manifest',
2467
- severity: 'fail',
2468
- detail: `${issues.length} issue(s): ${issues[0]}${issues.length > 1 ? ' ...' : ''}`,
2469
- remedy: 'restore the manifest or run liftoff update for managed-core repair'
2470
- });
2471
- }
2472
- else {
2473
- checks.push({
2474
- label: 'manifest',
2475
- severity: 'ok',
2476
- detail: `valid, ${manifest.managedArtifacts.length} managed core and ${manifest.projectArtifacts.length} project provenance entries`
2477
- });
2478
- }
2479
- if (compareSemver(manifest.liftoffVersion, liftoffVersion) > 0) {
2480
- checks.push({
2481
- label: 'version',
2482
- severity: 'warn',
2483
- detail: `project written by Liftoff ${manifest.liftoffVersion}, CLI is ${liftoffVersion}`,
2484
- remedy: `run liftoff upgrade --check, then liftoff upgrade; manual fallback: ${canonicalManualInstallCommand(manifest.liftoffVersion)}`
2485
- });
2486
- }
2487
- else {
2488
- checks.push({ label: 'version', severity: 'ok', detail: `generated by ${manifest.liftoffVersion}, CLI ${liftoffVersion}` });
2489
- }
2490
- if (manifest.governance.profile === 'unspecified') {
2491
- checks.push({
2492
- id: 'repository-governance',
2493
- label: 'repository governance',
2494
- severity: 'warn',
2495
- state: 'not-observable',
2496
- detail: 'manifest predates local governance handoff state',
2497
- remedy: 'run liftoff update --check to inspect default handoff adoption'
2498
- });
2499
- }
2500
- else if (manifest.governance.profile === 'none') {
2501
- checks.push({
2502
- id: 'repository-governance',
2503
- label: 'repository governance',
2504
- severity: 'ok',
2505
- state: 'disabled',
2506
- detail: 'disabled; no local handoff or live-enforcement claim'
2507
- });
2508
- }
2509
- else {
2510
- const governanceIssue = issues.find((issue) => /governance|repository-governance/i.test(issue));
2511
- const partialHandoff = manifest.governance.state === 'handoff-partial';
2512
- const severity = governanceIssue ? 'fail' : partialHandoff ? 'warn' : 'ok';
2513
- const detail = governanceIssue ??
2514
- (partialHandoff
2515
- ? `local ${manifest.governance.profile} policy ${manifest.governance.policyVersion} handoff is incomplete; one or more exact destinations remain outside Liftoff ownership`
2516
- : `local ${manifest.governance.profile} policy ${manifest.governance.policyVersion} handoff is intact; live enforcement is not inferred`);
2517
- const remedy = governanceIssue
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'
2519
- : partialHandoff
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'
2521
- : undefined;
2522
- checks.push({
2523
- id: 'repository-governance',
2524
- label: 'repository governance',
2525
- severity,
2526
- state: governanceIssue ? 'unhealthy' : manifest.governance.state,
2527
- detail,
2528
- ...(remedy ? { remedy } : {})
2529
- });
2530
- checks.push(...await governanceDoctorChecks(projectRoot, manifest));
2531
- }
2532
- checks.push(...await frameworkDoctorChecks(projectRoot, manifest));
2533
- if (manifest.project.workload.kind === 'power-apps-code-app') {
2534
- const starter = manifest.project.workload.starter;
2535
- checks.push({
2536
- id: 'power-apps-starter',
2537
- label: 'Power Apps starter',
2538
- severity: 'ok',
2539
- state: 'ready',
2540
- detail: `${starter.repository}/${starter.path} @ ${starter.commit}`
2541
- });
2542
- checks.push(await powerAppsProjectCheck(projectRoot));
2543
- }
2544
- else {
2545
- checks.push(await stackProjectCheck(projectRoot, manifest.project.workload.apiStack, runner));
2546
- }
2547
- try {
2548
- const config = await loadConfigOptions('liftoff.config.json', projectRoot);
2549
- const plan = buildProjectPlan(config, { requireProjectName: true });
2550
- const render = buildArtifacts(plan);
2551
- const entries = await reconcileProject(manifest, render, projectRoot);
2552
- const provisioningPlans = await inspectProvisioningGroups(projectRoot, render, requestedProvisioningGroups(manifest, plan));
2553
- const driftCount = entries.filter((entry) => entry.status !== 'unchanged' || entry.refreshHash).length +
2554
- provisioningPlans.length +
2555
- (manifest.artifactVersion === 7 ? 0 : 1);
2556
- if (driftCount > 0) {
2557
- checks.push({
2558
- label: 'managed core',
2559
- severity: 'warn',
2560
- detail: `${driftCount} core maintenance action(s) available`,
2561
- remedy: 'run liftoff update'
2562
- });
2563
- }
2564
- else {
2565
- checks.push({
2566
- label: 'managed core',
2567
- severity: 'ok',
2568
- detail: 'Liftoff core is current; project templates are not compared'
2569
- });
2570
- }
2571
- }
2572
- catch (error) {
2573
- checks.push({
2574
- label: 'managed core',
2575
- severity: 'fail',
2576
- detail: `liftoff.config.json could not be evaluated: ${error.message.split('\n')[0]}`,
2577
- remedy: 'repair liftoff.config.json'
2578
- });
2579
- }
2580
- return { title: 'Project', checks };
2581
- }
2582
- async function runtimeLayer(projectRoot, dockerAvailable, runner, manifest) {
2583
- const checks = [];
2584
- if (manifest.project.workload.kind === 'power-apps-code-app') {
2585
- if (!existsSync(path.join(projectRoot, 'node_modules'))) {
2586
- checks.push({
2587
- id: 'power-apps-cli',
2588
- label: 'Power Apps CLI',
2589
- severity: 'skipped',
2590
- state: 'not-observable',
2591
- detail: 'project dependencies are not installed',
2592
- remedy: 'run npm ci, then rerun liftoff doctor'
2593
- });
2594
- return { title: 'Runtime', checks };
2595
- }
2596
- const executable = process.platform === 'win32' ? 'npx.cmd' : 'npx';
2597
- const result = await runner.run({ executable, args: ['--no-install', 'power-apps', '--version'] }, { cwd: projectRoot, timeoutMs: 15_000 });
2598
- checks.push(result.status === 0
2599
- ? {
2600
- id: 'power-apps-cli',
2601
- label: 'Power Apps CLI',
2602
- severity: 'ok',
2603
- state: 'ready',
2604
- detail: result.stdout.trim() || 'local CLI is available'
2605
- }
2606
- : {
2607
- id: 'power-apps-cli',
2608
- label: 'Power Apps CLI',
2609
- severity: 'fail',
2610
- state: 'unhealthy',
2611
- detail: (result.stderr || 'local CLI probe failed').trim().split('\n')[0],
2612
- remedy: 'run npm ci and verify @microsoft/power-apps before binding the environment'
2613
- });
2614
- return { title: 'Runtime', checks };
2615
- }
2616
- if (existsSync(path.join(projectRoot, '.env.example'))) {
2617
- if (existsSync(path.join(projectRoot, '.env'))) {
2618
- checks.push({ label: '.env', severity: 'ok', detail: 'present' });
2619
- }
2620
- else {
2621
- checks.push({ label: '.env', severity: 'fail', detail: 'missing', remedy: 'copy .env.example to .env' });
2622
- }
2623
- }
2624
- else {
2625
- checks.push({ label: '.env', severity: 'skipped', detail: 'no .env.example in this project' });
2626
- }
2627
- if (!existsSync(path.join(projectRoot, 'docker-compose.yml'))) {
2628
- checks.push({ label: 'compose', severity: 'skipped', detail: 'no docker-compose.yml in this project' });
2629
- }
2630
- else if (!dockerAvailable) {
2631
- checks.push({ label: 'compose', severity: 'skipped', detail: 'docker is not installed, compose config not checked' });
2632
- }
2633
- else {
2634
- const result = await runner.run({ executable: 'docker', args: ['compose', 'config', '-q'] }, { cwd: projectRoot, timeoutMs: 15_000 });
2635
- if (result.status === 0) {
2636
- checks.push({ label: 'compose', severity: 'ok', detail: 'docker compose config is valid' });
2637
- }
2638
- else {
2639
- checks.push({
2640
- label: 'compose',
2641
- severity: 'fail',
2642
- detail: (result.stderr || 'docker compose config failed').split('\n')[0],
2643
- remedy: 'fix docker-compose.yml'
2644
- });
2645
- }
2646
- }
2647
- return { title: 'Runtime', checks };
2648
- }
2649
- async function codeAppsPluginLayer(projectRoot, manifest, runner) {
2650
- const agents = manifest.project.agents.map((id) => {
2651
- const agent = getCodingAgent(id);
2652
- if (!agent) {
2653
- throw new Error(`Manifest references unknown coding agent ${id}.`);
2654
- }
2655
- return agent;
2656
- });
2657
- const probes = await probeCodeAppsPlugin(agents, runner, projectRoot);
2658
- return {
2659
- title: 'Optional Code Apps plugin',
2660
- checks: probes.map((probe) => ({
2661
- id: probe.id,
2662
- label: probe.agent.label,
2663
- severity: probe.state === 'ready' ? 'ok' : 'warn',
2664
- state: probe.state,
2665
- requirementSeverity: 'advisory',
2666
- detail: probe.detail,
2667
- ...(probe.remedy ? { remedy: probe.remedy } : {})
2668
- }))
2669
- };
2670
- }
2671
- function renderDoctorLayers(layers, presentation) {
2672
- const statusKind = {
2673
- ok: 'success',
2674
- warn: 'warning',
2675
- fail: 'error',
2676
- skipped: 'pending'
2677
- };
2678
- for (const layer of layers) {
2679
- presentation.section(layer.title, layer.checks.flatMap((check) => {
2680
- const remedy = check.remedy ? ` - ${check.remedy}` : '';
2681
- return presentation.stdout
2682
- .status(statusKind[check.severity], check.label, `${check.detail}${remedy}`)
2683
- .trimEnd()
2684
- .split('\n');
2685
- }));
2686
- }
2687
- }
2688
- export function doctorExitCode(layers) {
2689
- return layers.some((layer) => layer.checks.some((check) => check.severity === 'fail')) ? 1 : 0;
2690
- }
2691
- async function doctorCommand(parsed, context) {
2692
- const jsonMode = readBooleanFlag(parsed.flags, 'json') ?? false;
2693
- const cloudOverride = readStringFlag(parsed.flags, 'cloud');
2694
- const layers = [];
2695
- context.presentation.commandIdentity('doctor', 'Inspect CLI, workstation, project, runtime, and cloud readiness');
2696
- const runner = context.runner ?? new NodeCommandRunner();
2697
- const projectRoot = await findProjectRoot(context.cwd);
2698
- let manifest;
2699
- let manifestError;
2700
- if (projectRoot) {
2701
- try {
2702
- manifest = await loadManifest(projectRoot);
2703
- }
2704
- catch (error) {
2705
- manifestError = error;
2706
- }
2707
- }
2708
- const releaseLookup = context.stableReleaseLookup ?? (() => lookupStableRelease({ registry: canonicalNpmRegistry }));
2709
- const configuredRegistryLookup = context.configuredRegistryTargetLookup ??
2710
- ((targetVersion) => checkConfiguredRegistryTarget(targetVersion, {
2711
- runner,
2712
- environment: context.env ?? process.env
2713
- }));
2714
- layers.push(await cliLayer(releaseLookup, configuredRegistryLookup));
2715
- const requirements = manifest
2716
- ? selectWorkstationRequirements(workstationSelectionFromManifest(manifest), { includeFramework: manifest.framework.state === 'initialized' })
2717
- : selectLiftoffRuntimeRequirements();
2718
- const probes = await probeWorkstation(requirements, runner);
2719
- const environment = workstationLayer(probes);
2720
- layers.push(environment);
2721
- const dockerAvailable = probes.some((probe) => probe.requirement.id === 'docker' && probe.state === 'ready');
2722
- if (projectRoot) {
2723
- if (manifestError) {
2724
- layers.push({
2725
- title: 'Project',
2726
- checks: [{ label: 'manifest', severity: 'fail', detail: manifestError.message, remedy: 'regenerate the project or use a matching CLI version' }]
2727
- });
2728
- }
2729
- if (manifest) {
2730
- layers.push(await projectLayer(projectRoot, manifest, runner));
2731
- layers.push(await runtimeLayer(projectRoot, dockerAvailable, runner, manifest));
2732
- if (manifest.project.workload.kind === 'power-apps-code-app') {
2733
- if (manifest.project.workload.codeAppsPlugin) {
2734
- layers.push(await codeAppsPluginLayer(projectRoot, manifest, runner));
2735
- }
2736
- }
2737
- else {
2738
- const workload = manifest.project.workload;
2739
- const cloud = cloudOverride ?? workload.cloud;
2740
- const cloudChecks = await cloudLayer(cloud, runner);
2741
- const pattern = workload.kind === 'genai'
2742
- ? patterns.find((candidate) => candidate.id === workload.pattern)
2743
- : undefined;
2744
- if (pattern?.worker && cloud === 'azure') {
2745
- const functionsAvailable = await binaryPresent('func', runner);
2746
- cloudChecks.checks.push(functionsAvailable
2747
- ? { label: 'functions tooling', severity: 'ok', detail: 'Azure Functions Core Tools installed' }
2748
- : { label: 'functions tooling', severity: 'warn', detail: 'Azure Functions Core Tools not found', remedy: 'npm install -g azure-functions-core-tools@4' });
2749
- }
2750
- layers.push(cloudChecks);
2751
- }
2752
- }
2753
- }
2754
- else if (cloudOverride) {
2755
- layers.push(await cloudLayer(cloudOverride, runner));
2756
- }
2757
- const failures = layers.reduce((count, layer) => count + layer.checks.filter((check) => check.severity === 'fail').length, 0);
2758
- const warnings = layers.reduce((count, layer) => count + layer.checks.filter((check) => check.severity === 'warn').length, 0);
2759
- if (jsonMode) {
2760
- context.presentation.rawStdout(`${JSON.stringify({ schemaVersion: 1, layers, summary: { failures, warnings } }, null, 2)}\n`);
2761
- }
2762
- else {
2763
- renderDoctorLayers(layers, context.presentation);
2764
- context.presentation.status(failures > 0 ? 'error' : warnings > 0 ? 'warning' : 'success', 'Doctor summary', `${failures} failure(s), ${warnings} warning(s)`);
2765
- }
2766
- return doctorExitCode(layers);
2767
- }
2768
- async function helperCommand(parsed, context, tool) {
2769
- const projectRoot = await findProjectRoot(context.cwd);
2770
- let infraProject;
2771
- if (projectRoot) {
2772
- const manifest = await loadManifest(projectRoot);
2773
- if (manifest.project.workload.kind === 'power-apps-code-app') {
2774
- if (parsed.command === 'dev') {
2775
- context.presentation.commandIdentity('dev', 'Power Apps local development');
2776
- context.presentation.section('Dependency prerequisite', ['Install the root lockfile before starting or after dependency changes.']);
2777
- context.presentation.command('npm ci');
2778
- context.presentation.section('Development server', []);
2779
- context.presentation.command('npm run dev');
2780
- return 0;
2781
- }
2782
- context.presentation.commandIdentity('infra', 'Power Apps hosting responsibility');
2783
- context.presentation.status('info', 'Not applicable', 'Power Apps code apps are hosted by Power Platform; Liftoff-managed OpenTofu infrastructure is not generated.');
2784
- return 0;
2785
- }
2786
- infraProject = {
2787
- root: projectRoot,
2788
- environments: manifest.project.workload.environments
2789
- };
2790
- }
2791
- const command = parsed.command === 'dev'
2792
- ? buildDevCommand(parsed)
2793
- : buildInfraCommand(parsed, infraProject);
2794
- context.presentation.commandIdentity(parsed.command ?? tool, `${tool} helper command`);
2795
- context.presentation.section(`${tool} helper command`, []);
2796
- context.presentation.command(command);
2797
- return 0;
2798
- }
2799
- function buildDevCommand(parsed) {
2800
- switch (parsed.subcommand) {
2801
- case 'down':
2802
- return 'docker compose down';
2803
- case 'logs':
2804
- return 'docker compose logs -f';
2805
- case 'reset':
2806
- return 'docker compose down --volumes';
2807
- case 'up':
2808
- default: {
2809
- const profile = readStringFlag(parsed.flags, 'profile');
2810
- return profile ? `docker compose --profile ${profile} up --build` : 'docker compose up --build';
2811
- }
2812
- }
2813
- }
2814
- function buildInfraCommand(parsed, project) {
2815
- const requestedEnvironment = readStringFlag(parsed.flags, 'env');
2816
- const env = requestedEnvironment ?? project?.environments[0] ?? 'dev';
2817
- const environment = getEnvironment(env);
2818
- if (!environment) {
2819
- throw new PlanValidationError([
2820
- `Unsupported environment: ${env}. Supported environments: dev, staging, prod.`
2821
- ]);
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}`] : [];
2837
- switch (parsed.subcommand) {
2838
- case 'apply':
2839
- args.push('apply', `-var-file=environments/${environment.id}.tfvars`);
2840
- break;
2841
- case 'output':
2842
- args.push('output');
2843
- break;
2844
- case 'init':
2845
- args.push('init');
2846
- break;
2847
- case 'plan':
2848
- default:
2849
- args.push('plan', `-var-file=environments/${environment.id}.tfvars`);
2850
- break;
2851
- }
2852
- return formatCommand({ executable: 'tofu', args });
2853
- }
2854
- async function optionsFromParsedArgs(parsed, cwd, includeProjectName) {
2855
- const configPath = readStringFlag(parsed.flags, 'config');
2856
- const configOptions = configPath ? await loadConfigOptions(configPath, cwd) : {};
2857
- const flagOptions = {
2858
- projectName: includeProjectName ? parsed.positional[0] ?? readStringFlag(parsed.flags, 'project') : readStringFlag(parsed.flags, 'project'),
2859
- projectType: readStringFlag(parsed.flags, 'type'),
2860
- genai: readBooleanFlag(parsed.flags, 'genai'),
2861
- apiStack: readStringFlag(parsed.flags, 'api'),
2862
- pattern: readStringFlag(parsed.flags, 'pattern'),
2863
- cloud: readStringFlag(parsed.flags, 'cloud'),
2864
- region: readStringFlag(parsed.flags, 'region'),
2865
- includeFrontend: readBooleanFlag(parsed.flags, 'frontend'),
2866
- environments: readListFlag(parsed.flags, 'environments'),
2867
- specWorkflow: readStringFlag(parsed.flags, 'spec'),
2868
- agents: readListFlag(parsed.flags, 'agents'),
2869
- defaultAgent: readStringFlag(parsed.flags, 'default-agent'),
2870
- codeAppsPlugin: readBooleanFlag(parsed.flags, 'code-apps-plugin'),
2871
- copilotCloud: readBooleanFlag(parsed.flags, 'copilot-cloud'),
2872
- configureOpenSpecProfile: readBooleanFlag(parsed.flags, 'configure-openspec-profile'),
2873
- governanceProfile: readStringFlag(parsed.flags, 'governance'),
2874
- configPath,
2875
- yes: readBooleanFlag(parsed.flags, 'yes') ?? false,
2876
- force: readBooleanFlag(parsed.flags, 'force'),
2877
- installTools: readBooleanFlag(parsed.flags, 'install-tools'),
2878
- installDependencies: readBooleanFlag(parsed.flags, 'install-dependencies')
2879
- };
2880
- return mergeOptions(configOptions, flagOptions);
2881
- }
2882
- function hasMissingInitInputs(options) {
2883
- const projectType = options.projectType ??
2884
- (options.genai === undefined ? undefined : options.genai ? 'genai' : 'standard') ??
2885
- (options.pattern ? 'genai' : options.apiStack ? 'standard' : undefined);
2886
- const missingTypeSpecific = projectType === 'genai'
2887
- ? !options.pattern
2888
- : projectType === 'standard'
2889
- ? !options.apiStack
2890
- : projectType === 'power-apps-code-app'
2891
- ? options.codeAppsPlugin === undefined
2892
- : true;
2893
- const missingDefaultAgent = options.specWorkflow === 'spec-kit' &&
2894
- (options.agents?.length ?? 0) > 1 &&
2895
- !options.defaultAgent;
2896
- const missingCopilotCloud = options.specWorkflow === 'openspec' &&
2897
- options.agents?.some((agent) => getCodingAgent(agent)?.id === 'github-copilot') === true &&
2898
- options.copilotCloud === undefined;
2899
- return !options.projectName ||
2900
- missingTypeSpecific ||
2901
- projectType !== 'power-apps-code-app' && !options.cloud ||
2902
- projectType !== 'power-apps-code-app' && options.includeFrontend === undefined ||
2903
- !options.specWorkflow ||
2904
- !options.agents ||
2905
- !options.governanceProfile ||
2906
- missingDefaultAgent ||
2907
- missingCopilotCloud ||
2908
- projectType !== 'power-apps-code-app' && !options.environments;
2909
- }
2910
- function renderGeneralHelp(presentation) {
2911
- const help = getGeneralHelp(liftoffVersion);
2912
- presentation.identity(`${help.title} - ${help.subtitle}`);
2913
- presentation.section('Usage', [help.usage]);
2914
- presentation.table('Global options', ['Option', 'Description'], help.globalOptions.map((option) => [option.syntax, option.description]));
2915
- for (const group of help.commandGroups) {
2916
- presentation.table(group.title, ['Command', 'Description'], group.entries.map((entry) => [entry.syntax, entry.description]));
2917
- }
2918
- presentation.status('info', 'Tip', help.hint);
2919
- }
2920
- function renderCommandHelp(command, presentation, subcommand) {
2921
- const help = getCommandHelp(command, subcommand);
2922
- presentation.commandIdentity(help.command, help.description);
2923
- presentation.section('Usage', [
2924
- presentation.stdout.layout === 'plain' ? `Usage: ${help.usage}` : help.usage
2925
- ]);
2926
- if (help.arguments.length > 0) {
2927
- presentation.table('Arguments', ['Argument', 'Description'], help.arguments.map((argument) => [argument.syntax, argument.description]));
2928
- }
2929
- if (help.subcommands.length > 0) {
2930
- presentation.bullets('Subcommands', help.subcommands);
2931
- }
2932
- for (const group of help.optionGroups) {
2933
- presentation.table(group.title, ['Option', 'Description'], group.entries.map((entry) => [
2934
- entry.syntax,
2935
- `${entry.description}${entry.defaultValue ? ` (default: ${entry.defaultValue})` : ''}`
2936
- ]));
2937
- }
2938
- }
2939
- export async function createFixtureProject(options) {
2940
- const tempRoot = await mkdtemp(path.join(os.tmpdir(), 'liftoff-'));
2941
- const plan = buildProjectPlan(options, { requireProjectName: true });
2942
- const target = artifactPath(tempRoot, [plan.safeProjectName]);
2943
- await mkdir(target, { recursive: true });
2944
- await assertNewOrEmptyDirectory(target);
2945
- await rm(target, { recursive: true, force: true });
2946
- await writeArtifacts(target, buildArtifacts(plan));
2947
- for (const marker of [
2948
- ...plan.framework.baseMarkers,
2949
- ...plan.agents.flatMap((agent) => plan.framework.agentMarkers[agent.id])
2950
- ]) {
2951
- let content = 'fixture marker\n';
2952
- if (marker.join('/') === '.specify/integration.json') {
2953
- const installed = plan.agents.map((agent) => agent.integrationIds['spec-kit']);
2954
- const defaultIntegration = plan.defaultAgent?.integrationIds['spec-kit'];
2955
- content = `${JSON.stringify({
2956
- integration_state_schema: 1,
2957
- integration: defaultIntegration,
2958
- default_integration: defaultIntegration,
2959
- installed_integrations: installed,
2960
- integration_settings: {}
2961
- }, null, 2)}\n`;
2962
- }
2963
- else if (marker.join('/') === '.specify/init-options.json') {
2964
- content = '{}\n';
2965
- }
2966
- await writeProjectFile(target, marker, content);
2967
- }
2968
- return target;
2969
- }
1
+ export { runCommand } from './cli/commands/dispatch.js';
2
+ export { createFixtureProject } from './application/initialize/fixture.js';
3
+ export { doctorExitCode } from './application/diagnose/doctor.js';
2970
4
  //# sourceMappingURL=commands.js.map