@msn-control/liftoff 0.10.3 → 0.11.1

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