@intentius/chant-lexicon-terraform 0.57.0 → 0.58.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (381) hide show
  1. package/dist/composites/terraform-adopt-op.d.ts +126 -0
  2. package/dist/composites/terraform-adopt-op.d.ts.map +1 -0
  3. package/dist/composites/terraform-apply-op.d.ts +63 -16
  4. package/dist/composites/terraform-apply-op.d.ts.map +1 -1
  5. package/dist/composites/terraform-watch-op.d.ts +80 -36
  6. package/dist/composites/terraform-watch-op.d.ts.map +1 -1
  7. package/dist/config.d.ts +49 -2
  8. package/dist/config.d.ts.map +1 -1
  9. package/dist/describe-resources.d.ts +256 -16
  10. package/dist/describe-resources.d.ts.map +1 -1
  11. package/dist/hcl/descend.d.ts +98 -0
  12. package/dist/hcl/descend.d.ts.map +1 -0
  13. package/dist/hcl/marker.d.ts +130 -0
  14. package/dist/hcl/marker.d.ts.map +1 -0
  15. package/dist/hcl/parse.d.ts +142 -9
  16. package/dist/hcl/parse.d.ts.map +1 -1
  17. package/dist/hcl/references.d.ts +76 -0
  18. package/dist/hcl/references.d.ts.map +1 -0
  19. package/dist/hcl/roots.d.ts +22 -3
  20. package/dist/hcl/roots.d.ts.map +1 -1
  21. package/dist/hcl/suppressions.d.ts +54 -0
  22. package/dist/hcl/suppressions.d.ts.map +1 -0
  23. package/dist/hcl/value.d.ts +77 -0
  24. package/dist/hcl/value.d.ts.map +1 -0
  25. package/dist/index.d.ts +5 -2
  26. package/dist/index.d.ts.map +1 -1
  27. package/dist/integrity.json +32 -5
  28. package/dist/lint/audit-catalog.d.ts +3 -2
  29. package/dist/lint/audit-catalog.d.ts.map +1 -1
  30. package/dist/lint/audit-lineage.d.ts +68 -0
  31. package/dist/lint/audit-lineage.d.ts.map +1 -1
  32. package/dist/lint/post-synth/blocks.d.ts +53 -0
  33. package/dist/lint/post-synth/blocks.d.ts.map +1 -0
  34. package/dist/lint/post-synth/fixtures/load.d.ts +37 -0
  35. package/dist/lint/post-synth/fixtures/load.d.ts.map +1 -0
  36. package/dist/lint/post-synth/index.d.ts.map +1 -1
  37. package/dist/lint/post-synth/module-source.d.ts +38 -0
  38. package/dist/lint/post-synth/module-source.d.ts.map +1 -0
  39. package/dist/lint/post-synth/scope.d.ts +65 -0
  40. package/dist/lint/post-synth/scope.d.ts.map +1 -0
  41. package/dist/lint/post-synth/tf001.d.ts +18 -0
  42. package/dist/lint/post-synth/tf001.d.ts.map +1 -1
  43. package/dist/lint/post-synth/tf002.d.ts +32 -0
  44. package/dist/lint/post-synth/tf002.d.ts.map +1 -0
  45. package/dist/lint/post-synth/tf003.d.ts +22 -0
  46. package/dist/lint/post-synth/tf003.d.ts.map +1 -0
  47. package/dist/lint/post-synth/tf004.d.ts +22 -0
  48. package/dist/lint/post-synth/tf004.d.ts.map +1 -0
  49. package/dist/lint/post-synth/tf005.d.ts +28 -0
  50. package/dist/lint/post-synth/tf005.d.ts.map +1 -0
  51. package/dist/lint/post-synth/tf006.d.ts +21 -0
  52. package/dist/lint/post-synth/tf006.d.ts.map +1 -0
  53. package/dist/lint/post-synth/tf007.d.ts +21 -0
  54. package/dist/lint/post-synth/tf007.d.ts.map +1 -0
  55. package/dist/lint/post-synth/tf008.d.ts +22 -0
  56. package/dist/lint/post-synth/tf008.d.ts.map +1 -0
  57. package/dist/lint/post-synth/tf009.d.ts +24 -0
  58. package/dist/lint/post-synth/tf009.d.ts.map +1 -0
  59. package/dist/lint/post-synth/tf010.d.ts +19 -0
  60. package/dist/lint/post-synth/tf010.d.ts.map +1 -0
  61. package/dist/lint/post-synth/tf011.d.ts +18 -0
  62. package/dist/lint/post-synth/tf011.d.ts.map +1 -0
  63. package/dist/lint/post-synth/tf012.d.ts +17 -0
  64. package/dist/lint/post-synth/tf012.d.ts.map +1 -0
  65. package/dist/lint/post-synth/tf013.d.ts +21 -0
  66. package/dist/lint/post-synth/tf013.d.ts.map +1 -0
  67. package/dist/lint/post-synth/tf014.d.ts +25 -0
  68. package/dist/lint/post-synth/tf014.d.ts.map +1 -0
  69. package/dist/lint/post-synth/tf015.d.ts +21 -0
  70. package/dist/lint/post-synth/tf015.d.ts.map +1 -0
  71. package/dist/lint/post-synth/tf016.d.ts +30 -0
  72. package/dist/lint/post-synth/tf016.d.ts.map +1 -0
  73. package/dist/lint/post-synth/tf017.d.ts +19 -0
  74. package/dist/lint/post-synth/tf017.d.ts.map +1 -0
  75. package/dist/lint/post-synth/tf018.d.ts +20 -0
  76. package/dist/lint/post-synth/tf018.d.ts.map +1 -0
  77. package/dist/lint/post-synth/tf019.d.ts +25 -0
  78. package/dist/lint/post-synth/tf019.d.ts.map +1 -0
  79. package/dist/lint/post-synth/tf020.d.ts +38 -0
  80. package/dist/lint/post-synth/tf020.d.ts.map +1 -0
  81. package/dist/lint/post-synth/tf021.d.ts +41 -0
  82. package/dist/lint/post-synth/tf021.d.ts.map +1 -0
  83. package/dist/lint/post-synth/tf022.d.ts +24 -0
  84. package/dist/lint/post-synth/tf022.d.ts.map +1 -0
  85. package/dist/lint/post-synth/tf024.d.ts +24 -0
  86. package/dist/lint/post-synth/tf024.d.ts.map +1 -0
  87. package/dist/lint/post-synth/tf025.d.ts +29 -0
  88. package/dist/lint/post-synth/tf025.d.ts.map +1 -0
  89. package/dist/lint/post-synth/tf026.d.ts +21 -0
  90. package/dist/lint/post-synth/tf026.d.ts.map +1 -0
  91. package/dist/lint/rules/fixtures/load.d.ts +13 -0
  92. package/dist/lint/rules/fixtures/load.d.ts.map +1 -0
  93. package/dist/lint/rules/plan-before-apply.d.ts +11 -0
  94. package/dist/lint/rules/plan-before-apply.d.ts.map +1 -1
  95. package/dist/lint/secret-shape.d.ts +68 -0
  96. package/dist/lint/secret-shape.d.ts.map +1 -0
  97. package/dist/live-ownership.d.ts +55 -0
  98. package/dist/live-ownership.d.ts.map +1 -0
  99. package/dist/lsp/option-keys.d.ts.map +1 -1
  100. package/dist/manifest.json +1 -1
  101. package/dist/okf/index.md +24 -0
  102. package/dist/okf/rules/TF002.md +11 -0
  103. package/dist/okf/rules/TF003.md +11 -0
  104. package/dist/okf/rules/TF004.md +11 -0
  105. package/dist/okf/rules/TF005.md +11 -0
  106. package/dist/okf/rules/TF006.md +11 -0
  107. package/dist/okf/rules/TF007.md +11 -0
  108. package/dist/okf/rules/TF008.md +11 -0
  109. package/dist/okf/rules/TF009.md +11 -0
  110. package/dist/okf/rules/TF010.md +11 -0
  111. package/dist/okf/rules/TF011.md +11 -0
  112. package/dist/okf/rules/TF012.md +11 -0
  113. package/dist/okf/rules/TF013.md +11 -0
  114. package/dist/okf/rules/TF014.md +11 -0
  115. package/dist/okf/rules/TF015.md +11 -0
  116. package/dist/okf/rules/TF016.md +11 -0
  117. package/dist/okf/rules/TF017.md +11 -0
  118. package/dist/okf/rules/TF018.md +11 -0
  119. package/dist/okf/rules/TF019.md +11 -0
  120. package/dist/okf/rules/TF020.md +11 -0
  121. package/dist/okf/rules/TF021.md +11 -0
  122. package/dist/okf/rules/TF022.md +11 -0
  123. package/dist/okf/rules/TF024.md +11 -0
  124. package/dist/okf/rules/TF025.md +11 -0
  125. package/dist/okf/rules/TF026.md +11 -0
  126. package/dist/op/activities/index.d.ts +13 -6
  127. package/dist/op/activities/index.d.ts.map +1 -1
  128. package/dist/op/activities/live-detect.d.ts +51 -0
  129. package/dist/op/activities/live-detect.d.ts.map +1 -0
  130. package/dist/op/activities/terraform.d.ts +445 -17
  131. package/dist/op/activities/terraform.d.ts.map +1 -1
  132. package/dist/op/activity-contracts.d.ts +148 -0
  133. package/dist/op/activity-contracts.d.ts.map +1 -0
  134. package/dist/op/adoption.d.ts +118 -0
  135. package/dist/op/adoption.d.ts.map +1 -0
  136. package/dist/op/builders.d.ts +46 -6
  137. package/dist/op/builders.d.ts.map +1 -1
  138. package/dist/op/resolve-root-mode.d.ts +45 -0
  139. package/dist/op/resolve-root-mode.d.ts.map +1 -0
  140. package/dist/plugin.d.ts.map +1 -1
  141. package/dist/rules/blocks.ts +109 -0
  142. package/dist/rules/module-source.ts +121 -0
  143. package/dist/rules/plan-before-apply.ts +11 -0
  144. package/dist/rules/scope.ts +107 -0
  145. package/dist/rules/tf001.ts +32 -8
  146. package/dist/rules/tf002.ts +151 -0
  147. package/dist/rules/tf003.ts +71 -0
  148. package/dist/rules/tf004.ts +63 -0
  149. package/dist/rules/tf005.ts +76 -0
  150. package/dist/rules/tf006.ts +53 -0
  151. package/dist/rules/tf007.ts +78 -0
  152. package/dist/rules/tf008.ts +75 -0
  153. package/dist/rules/tf009.ts +59 -0
  154. package/dist/rules/tf010.ts +51 -0
  155. package/dist/rules/tf011.ts +49 -0
  156. package/dist/rules/tf012.ts +47 -0
  157. package/dist/rules/tf013.ts +63 -0
  158. package/dist/rules/tf014.ts +72 -0
  159. package/dist/rules/tf015.ts +63 -0
  160. package/dist/rules/tf016.ts +75 -0
  161. package/dist/rules/tf017.ts +50 -0
  162. package/dist/rules/tf018.ts +72 -0
  163. package/dist/rules/tf019.ts +72 -0
  164. package/dist/rules/tf020.ts +123 -0
  165. package/dist/rules/tf021.ts +126 -0
  166. package/dist/rules/tf022.ts +61 -0
  167. package/dist/rules/tf024.ts +70 -0
  168. package/dist/rules/tf025.ts +101 -0
  169. package/dist/rules/tf026.ts +80 -0
  170. package/dist/skills/chant-terraform.md +1 -1
  171. package/package.json +7 -2
  172. package/src/__fixtures__/live/main.tf +28 -0
  173. package/src/__fixtures__/live-adopt/estate.chdf.hcl +6 -0
  174. package/src/__fixtures__/live-adopt/main.tf +21 -0
  175. package/src/__fixtures__/live-estate/README.md +58 -0
  176. package/src/__fixtures__/live-estate/compute.tf +10 -0
  177. package/src/__fixtures__/live-estate/estate.chdf.hcl +1 -0
  178. package/src/__fixtures__/live-estate/logs.tf +8 -0
  179. package/src/__fixtures__/live-estate/network.tf +24 -0
  180. package/src/__fixtures__/live-estate/rule.tf +11 -0
  181. package/src/__fixtures__/live-estate/unowned.tf +21 -0
  182. package/src/__fixtures__/live-estate/versions.tf +31 -0
  183. package/src/__fixtures__/live-ls.json +98 -0
  184. package/src/__fixtures__/live-plan.json +285 -0
  185. package/src/__fixtures__/live-sidecar/estate.chdf.hcl +3 -0
  186. package/src/__fixtures__/live-sidecar/main.tf +24 -0
  187. package/src/__fixtures__/module-tree/README.md +16 -0
  188. package/src/__fixtures__/module-tree/cycle/main.tf +3 -0
  189. package/src/__fixtures__/module-tree/cycle/modules/a/main.tf +9 -0
  190. package/src/__fixtures__/module-tree/cycle/modules/b/main.tf +9 -0
  191. package/src/__fixtures__/module-tree/missing/main.tf +9 -0
  192. package/src/__fixtures__/module-tree/outside/root/main.tf +9 -0
  193. package/src/__fixtures__/module-tree/root/main.tf +28 -0
  194. package/src/__fixtures__/module-tree/root/modules/cdn/main.tf +15 -0
  195. package/src/__fixtures__/module-tree/root/modules/cdn/modules/bucket/main.tf +11 -0
  196. package/src/__fixtures__/module-tree/shared/main.tf +5 -0
  197. package/src/__fixtures__/references/main.tf +56 -0
  198. package/src/codegen/lint-rules-page.test.ts +28 -0
  199. package/src/composites/terraform-adopt-op.acceptance.test.ts +166 -0
  200. package/src/composites/terraform-adopt-op.test.ts +170 -0
  201. package/src/composites/terraform-adopt-op.ts +197 -0
  202. package/src/composites/terraform-apply-op.acceptance.test.ts +183 -5
  203. package/src/composites/terraform-apply-op.test.ts +167 -15
  204. package/src/composites/terraform-apply-op.ts +105 -28
  205. package/src/composites/terraform-watch-op.test.ts +85 -26
  206. package/src/composites/terraform-watch-op.ts +130 -73
  207. package/src/config.ts +32 -3
  208. package/src/describe-resources.live.test.ts +598 -0
  209. package/src/describe-resources.test.ts +53 -0
  210. package/src/describe-resources.ts +811 -28
  211. package/src/hcl/descend.test.ts +162 -0
  212. package/src/hcl/descend.ts +273 -0
  213. package/src/hcl/marker.test.ts +147 -0
  214. package/src/hcl/marker.ts +235 -0
  215. package/src/hcl/parse.test.ts +69 -0
  216. package/src/hcl/parse.ts +249 -27
  217. package/src/hcl/references.test.ts +116 -0
  218. package/src/hcl/references.ts +172 -0
  219. package/src/hcl/roots.ts +59 -5
  220. package/src/hcl/suppressions.test.ts +147 -0
  221. package/src/hcl/suppressions.ts +204 -0
  222. package/src/hcl/value.test.ts +73 -0
  223. package/src/hcl/value.ts +106 -0
  224. package/src/index.ts +31 -1
  225. package/src/lint/audit-catalog.ts +264 -3
  226. package/src/lint/audit-lineage.ts +319 -1
  227. package/src/lint/audit.test.ts +40 -1
  228. package/src/lint/post-synth/blocks.ts +109 -0
  229. package/src/lint/post-synth/fixtures/TF001/negative.tf +28 -0
  230. package/src/lint/post-synth/fixtures/TF001/positive.tf +24 -0
  231. package/src/lint/post-synth/fixtures/TF002/negative.tf +17 -0
  232. package/src/lint/post-synth/fixtures/TF002/positive-legacy.tf +10 -0
  233. package/src/lint/post-synth/fixtures/TF002/positive-no-version.tf +12 -0
  234. package/src/lint/post-synth/fixtures/TF002/positive.tf +7 -0
  235. package/src/lint/post-synth/fixtures/TF003/negative.tf +10 -0
  236. package/src/lint/post-synth/fixtures/TF003/positive.tf +8 -0
  237. package/src/lint/post-synth/fixtures/TF004/negative-git.tf +3 -0
  238. package/src/lint/post-synth/fixtures/TF004/negative-local.tf +3 -0
  239. package/src/lint/post-synth/fixtures/TF004/negative.tf +4 -0
  240. package/src/lint/post-synth/fixtures/TF004/positive.tf +3 -0
  241. package/src/lint/post-synth/fixtures/TF005/negative-local.tf +3 -0
  242. package/src/lint/post-synth/fixtures/TF005/negative-registry.tf +3 -0
  243. package/src/lint/post-synth/fixtures/TF005/negative-sha.tf +3 -0
  244. package/src/lint/post-synth/fixtures/TF005/negative.tf +3 -0
  245. package/src/lint/post-synth/fixtures/TF005/positive-branch.tf +3 -0
  246. package/src/lint/post-synth/fixtures/TF005/positive-non-semver.tf +3 -0
  247. package/src/lint/post-synth/fixtures/TF005/positive.tf +3 -0
  248. package/src/lint/post-synth/fixtures/TF006/negative.tf +5 -0
  249. package/src/lint/post-synth/fixtures/TF006/positive-empty-default.tf +11 -0
  250. package/src/lint/post-synth/fixtures/TF006/positive.tf +6 -0
  251. package/src/lint/post-synth/fixtures/TF007/negative.tf +10 -0
  252. package/src/lint/post-synth/fixtures/TF007/positive.tf +9 -0
  253. package/src/lint/post-synth/fixtures/TF008/negative-variable.tf +10 -0
  254. package/src/lint/post-synth/fixtures/TF008/negative.tf +8 -0
  255. package/src/lint/post-synth/fixtures/TF008/positive-other-provider.tf +10 -0
  256. package/src/lint/post-synth/fixtures/TF008/positive.tf +5 -0
  257. package/src/lint/post-synth/fixtures/TF009/negative.tf +10 -0
  258. package/src/lint/post-synth/fixtures/TF009/positive.tf +4 -0
  259. package/src/lint/post-synth/fixtures/TF010/negative.tf +5 -0
  260. package/src/lint/post-synth/fixtures/TF010/positive.tf +4 -0
  261. package/src/lint/post-synth/fixtures/TF011/negative.tf +5 -0
  262. package/src/lint/post-synth/fixtures/TF011/positive-blank.tf +5 -0
  263. package/src/lint/post-synth/fixtures/TF011/positive.tf +4 -0
  264. package/src/lint/post-synth/fixtures/TF012/negative.tf +4 -0
  265. package/src/lint/post-synth/fixtures/TF012/positive.tf +3 -0
  266. package/src/lint/post-synth/fixtures/TF013/negative.tf +8 -0
  267. package/src/lint/post-synth/fixtures/TF013/positive-data.tf +8 -0
  268. package/src/lint/post-synth/fixtures/TF013/positive.tf +8 -0
  269. package/src/lint/post-synth/fixtures/TF014/negative/main.tf +26 -0
  270. package/src/lint/post-synth/fixtures/TF014/negative/modules/cdn/main.tf +19 -0
  271. package/src/lint/post-synth/fixtures/TF014/positive/main.tf +16 -0
  272. package/src/lint/post-synth/fixtures/TF014/positive/modules/cdn/main.tf +15 -0
  273. package/src/lint/post-synth/fixtures/TF015/negative/main.tf +13 -0
  274. package/src/lint/post-synth/fixtures/TF015/negative/modules/cdn/main.tf +16 -0
  275. package/src/lint/post-synth/fixtures/TF015/positive/main.tf +13 -0
  276. package/src/lint/post-synth/fixtures/TF015/positive/modules/cdn/main.tf +14 -0
  277. package/src/lint/post-synth/fixtures/TF016/negative-template.tf +7 -0
  278. package/src/lint/post-synth/fixtures/TF016/negative.tf +16 -0
  279. package/src/lint/post-synth/fixtures/TF016/positive-shared-file.tf +12 -0
  280. package/src/lint/post-synth/fixtures/TF016/positive.tf +13 -0
  281. package/src/lint/post-synth/fixtures/TF017/negative.tf +4 -0
  282. package/src/lint/post-synth/fixtures/TF017/positive.tf +5 -0
  283. package/src/lint/post-synth/fixtures/TF018/negative.tf +9 -0
  284. package/src/lint/post-synth/fixtures/TF018/positive-data.tf +9 -0
  285. package/src/lint/post-synth/fixtures/TF018/positive.tf +4 -0
  286. package/src/lint/post-synth/fixtures/TF019/negative.tf +14 -0
  287. package/src/lint/post-synth/fixtures/TF019/positive-output.tf +5 -0
  288. package/src/lint/post-synth/fixtures/TF019/positive.tf +16 -0
  289. package/src/lint/post-synth/fixtures/TF020/negative.tf +43 -0
  290. package/src/lint/post-synth/fixtures/TF020/positive.tf +32 -0
  291. package/src/lint/post-synth/fixtures/TF020/scoped/main.tf +21 -0
  292. package/src/lint/post-synth/fixtures/TF020/scoped/modules/cdn/main.tf +13 -0
  293. package/src/lint/post-synth/fixtures/TF021/negative.tf +31 -0
  294. package/src/lint/post-synth/fixtures/TF021/positive-length.tf +8 -0
  295. package/src/lint/post-synth/fixtures/TF021/positive.tf +8 -0
  296. package/src/lint/post-synth/fixtures/TF022/negative-locator.tf +7 -0
  297. package/src/lint/post-synth/fixtures/TF022/negative.tf +10 -0
  298. package/src/lint/post-synth/fixtures/TF022/positive-nested.tf +10 -0
  299. package/src/lint/post-synth/fixtures/TF022/positive.tf +6 -0
  300. package/src/lint/post-synth/fixtures/TF023/negative/main.tf +15 -0
  301. package/src/lint/post-synth/fixtures/TF023/positive/main.tf +9 -0
  302. package/src/lint/post-synth/fixtures/TF023/positive/state.json +24 -0
  303. package/src/lint/post-synth/fixtures/TF024/live-clean/main.tf +9 -0
  304. package/src/lint/post-synth/fixtures/TF024/live-with-backend/main.tf +14 -0
  305. package/src/lint/post-synth/fixtures/TF024/live-with-cloud/main.tf +16 -0
  306. package/src/lint/post-synth/fixtures/TF025/live-clean/main.tf +13 -0
  307. package/src/lint/post-synth/fixtures/TF025/live-with-workspace-ref/main.tf +13 -0
  308. package/src/lint/post-synth/fixtures/TF026/live-default/main.tf +9 -0
  309. package/src/lint/post-synth/fixtures/TF026/live-explicit-delete/main.tf +13 -0
  310. package/src/lint/post-synth/fixtures/TF026/live-keep/main.tf +13 -0
  311. package/src/lint/post-synth/fixtures/load.ts +108 -0
  312. package/src/lint/post-synth/index.ts +48 -0
  313. package/src/lint/post-synth/module-rules.test.ts +153 -0
  314. package/src/lint/post-synth/module-source.test.ts +75 -0
  315. package/src/lint/post-synth/module-source.ts +121 -0
  316. package/src/lint/post-synth/post-synth.test.ts +14 -15
  317. package/src/lint/post-synth/scope.ts +107 -0
  318. package/src/lint/post-synth/single-block.test.ts +263 -0
  319. package/src/lint/post-synth/suppression.test.ts +110 -0
  320. package/src/lint/post-synth/tf001.ts +32 -8
  321. package/src/lint/post-synth/tf002.test.ts +93 -0
  322. package/src/lint/post-synth/tf002.ts +151 -0
  323. package/src/lint/post-synth/tf003.test.ts +75 -0
  324. package/src/lint/post-synth/tf003.ts +71 -0
  325. package/src/lint/post-synth/tf004.test.ts +47 -0
  326. package/src/lint/post-synth/tf004.ts +63 -0
  327. package/src/lint/post-synth/tf005.test.ts +57 -0
  328. package/src/lint/post-synth/tf005.ts +76 -0
  329. package/src/lint/post-synth/tf006.ts +53 -0
  330. package/src/lint/post-synth/tf007.ts +78 -0
  331. package/src/lint/post-synth/tf008.ts +75 -0
  332. package/src/lint/post-synth/tf009.ts +59 -0
  333. package/src/lint/post-synth/tf010.ts +51 -0
  334. package/src/lint/post-synth/tf011.ts +49 -0
  335. package/src/lint/post-synth/tf012.ts +47 -0
  336. package/src/lint/post-synth/tf013.ts +63 -0
  337. package/src/lint/post-synth/tf014.ts +72 -0
  338. package/src/lint/post-synth/tf015.ts +63 -0
  339. package/src/lint/post-synth/tf016.ts +75 -0
  340. package/src/lint/post-synth/tf017.ts +50 -0
  341. package/src/lint/post-synth/tf018.ts +72 -0
  342. package/src/lint/post-synth/tf019.ts +72 -0
  343. package/src/lint/post-synth/tf020.ts +123 -0
  344. package/src/lint/post-synth/tf021.ts +126 -0
  345. package/src/lint/post-synth/tf022.ts +61 -0
  346. package/src/lint/post-synth/tf024.test.ts +42 -0
  347. package/src/lint/post-synth/tf024.ts +70 -0
  348. package/src/lint/post-synth/tf025.test.ts +46 -0
  349. package/src/lint/post-synth/tf025.ts +101 -0
  350. package/src/lint/post-synth/tf026.test.ts +56 -0
  351. package/src/lint/post-synth/tf026.ts +80 -0
  352. package/src/lint/presets.test.ts +72 -0
  353. package/src/lint/rules/fixtures/TF101/negative-op.ts +2 -0
  354. package/src/lint/rules/fixtures/TF101/positive-op.ts +2 -0
  355. package/src/lint/rules/fixtures/load.ts +53 -0
  356. package/src/lint/rules/plan-before-apply.test.ts +112 -7
  357. package/src/lint/rules/plan-before-apply.ts +11 -0
  358. package/src/lint/secret-shape.test.ts +150 -0
  359. package/src/lint/secret-shape.ts +159 -0
  360. package/src/live-ownership.ts +60 -0
  361. package/src/lsp/completions.test.ts +3 -2
  362. package/src/lsp/option-keys.ts +10 -5
  363. package/src/op/activities/choudoufu.acceptance.test.ts +143 -0
  364. package/src/op/activities/choudoufu.test.ts +693 -0
  365. package/src/op/activities/index.ts +40 -4
  366. package/src/op/activities/live-detect.test.ts +143 -0
  367. package/src/op/activities/live-detect.ts +162 -0
  368. package/src/op/activities/registry.test.ts +11 -3
  369. package/src/op/activities/terraform.test.ts +7 -3
  370. package/src/op/activities/terraform.ts +823 -69
  371. package/src/op/activity-contracts.test.ts +147 -0
  372. package/src/op/activity-contracts.ts +178 -0
  373. package/src/op/adoption.test.ts +170 -0
  374. package/src/op/adoption.ts +241 -0
  375. package/src/op/builders.test.ts +60 -5
  376. package/src/op/builders.ts +93 -5
  377. package/src/op/resolve-root-mode.test.ts +85 -0
  378. package/src/op/resolve-root-mode.ts +72 -0
  379. package/src/plugin.test.ts +68 -0
  380. package/src/plugin.ts +75 -9
  381. package/src/skills/chant-terraform.md +1 -1
@@ -0,0 +1,241 @@
1
+ /**
2
+ * The adoption ledger: which live resources this estate's markers can claim,
3
+ * read off `live-plan -json`'s document and rendered as text (#2105).
4
+ *
5
+ * Pure. No filesystem, no child process, no HCL parse — everything here is a
6
+ * projection of GitHub issue #788's `unowned` section, which is the one place
7
+ * choudoufu states both halves of the answer: the live resource sitting at a
8
+ * declared instance's identity, and the `tofu-estate`/`tofu-address` pair that
9
+ * would adopt it. Two tags is the whole ownership contract (choudoufu's
10
+ * `live/MARKERS.md`), so those two values are the whole of what an adoption
11
+ * needs to know.
12
+ *
13
+ * ## Why chant renders a ledger at all
14
+ *
15
+ * choudoufu prints its own, under `live-plan -adoption-only`, and it is the
16
+ * better report for a human at a terminal: it counts the marker-carrying and
17
+ * the identity-by-declaration halves of the declared population separately and
18
+ * says why each unadoptable instance is unadoptable. But `-adoption-only` and
19
+ * `-json` are refused together (`internal/command/live_plan.go`: "this run
20
+ * cannot produce both reports at once"), and an Op that reports adoptables
21
+ * needs the machine-readable document anyway — for the counts it publishes as
22
+ * outcome attributes, and for the addresses an adoption step acts on. So the
23
+ * ledger below is rendered from the document that run already has, in the row
24
+ * form `-adoption-only` prints, rather than paying for a third live read.
25
+ *
26
+ * ## Ambiguity
27
+ *
28
+ * `unowned[]` is one entry per live resource, keyed by the declared instance
29
+ * whose identity found it, so two live resources at one declared identity are
30
+ * two entries carrying the same `addr`. That is the ambiguous case: no single
31
+ * tag write claims the address, and picking one of the two is a decision about
32
+ * the estate rather than something a tool infers. {@link readAdoptionLedger}
33
+ * separates those into `contested` and never lets them into `adoptions`, and
34
+ * `TerraformAdoptOp` passes the contested list to its Adopt step so the Op's
35
+ * result names what it refused as well as what it wrote.
36
+ */
37
+
38
+ /** One live resource a marker write would bind to a declared instance. */
39
+ export interface AdoptionCandidate {
40
+ /** The declared instance address whose identity found the live resource. */
41
+ addr: string;
42
+ /** The live resource's type, as choudoufu's `unowned[].type`. */
43
+ type: string;
44
+ /** The identity the live resource was read with — the handle a human, or a tagging call, needs. */
45
+ identity: string;
46
+ /** The `tofu-estate` value that adopts it. */
47
+ markerEstate: string;
48
+ /** The `tofu-address` value that adopts it, escaped as choudoufu stores it. */
49
+ markerAddress: string;
50
+ /**
51
+ * The paste-ready tagging command choudoufu printed for this address under
52
+ * `live-plan -adoption-only`, when it printed one. Absent for a type whose
53
+ * service has its own tagging call this fork does not spell out (IAM,
54
+ * Route53, S3 and friends): the two marker values above are still the whole
55
+ * contract, but the caller has to write them itself.
56
+ */
57
+ command?: string;
58
+ }
59
+
60
+ /** What {@link readAdoptionLedger} found in one `live-plan -json` document. */
61
+ export interface AdoptionLedger {
62
+ /** Candidates at an address exactly one live resource sits at. The adoptable set. */
63
+ adoptions: AdoptionCandidate[];
64
+ /**
65
+ * Every candidate at an address more than one live resource sits at. Never
66
+ * adopted: reported so a human can delete one or write the marker by hand.
67
+ * Both (or all) candidates for a contested address appear, so the report
68
+ * shows what the choice is between.
69
+ */
70
+ contested: AdoptionCandidate[];
71
+ /** How many distinct declared addresses are contested. */
72
+ ambiguous: number;
73
+ }
74
+
75
+ /** `unowned[]`'s entry shape, per choudoufu's `views.StatelessUnowned` json tags. */
76
+ interface UnownedEntry {
77
+ addr?: unknown;
78
+ type?: unknown;
79
+ identity?: unknown;
80
+ tofu_estate?: unknown;
81
+ adopt_tofu_estate?: unknown;
82
+ adopt_tofu_address?: unknown;
83
+ }
84
+
85
+ const str = (v: unknown): string => (typeof v === "string" ? v : "");
86
+
87
+ /**
88
+ * Project a `live-plan -json` document's `unowned` section into the adoptable
89
+ * and the contested sets.
90
+ *
91
+ * An entry counts as a candidate only when choudoufu offered both marker
92
+ * values. Both empty means adoption was not this run's to offer — the resource
93
+ * belongs to another estate (`tofu_estate` names it), or the run had no estate
94
+ * name of its own — and such an entry is neither adoptable nor contested here,
95
+ * because there is no tag write to refuse.
96
+ *
97
+ * `commands` maps a declared address to the paste-ready tagging command
98
+ * choudoufu printed for it, from {@link parseAdoptionCommands}; omit it when
99
+ * the run had no `-adoption-only` render to read one out of.
100
+ */
101
+ export function readAdoptionLedger(document: unknown, commands?: ReadonlyMap<string, string>): AdoptionLedger {
102
+ const unowned = (document as { unowned?: unknown } | null | undefined)?.unowned;
103
+ if (!Array.isArray(unowned)) return { adoptions: [], contested: [], ambiguous: 0 };
104
+
105
+ const byAddr = new Map<string, AdoptionCandidate[]>();
106
+ for (const raw of unowned) {
107
+ const e = (raw ?? {}) as UnownedEntry;
108
+ const markerEstate = str(e.adopt_tofu_estate);
109
+ const markerAddress = str(e.adopt_tofu_address);
110
+ if (!markerEstate && !markerAddress) continue;
111
+ const addr = str(e.addr);
112
+ const command = commands?.get(addr);
113
+ const candidate: AdoptionCandidate = {
114
+ addr,
115
+ type: str(e.type),
116
+ identity: str(e.identity),
117
+ markerEstate,
118
+ markerAddress,
119
+ ...(command ? { command } : {}),
120
+ };
121
+ const at = byAddr.get(addr);
122
+ if (at) at.push(candidate);
123
+ else byAddr.set(addr, [candidate]);
124
+ }
125
+
126
+ const adoptions: AdoptionCandidate[] = [];
127
+ const contested: AdoptionCandidate[] = [];
128
+ let ambiguous = 0;
129
+ for (const candidates of byAddr.values()) {
130
+ if (candidates.length === 1) {
131
+ adoptions.push(candidates[0]);
132
+ } else {
133
+ ambiguous++;
134
+ contested.push(...candidates);
135
+ }
136
+ }
137
+ return { adoptions, contested, ambiguous };
138
+ }
139
+
140
+ /**
141
+ * Pair each declared address in a `live-plan -adoption-only` render with the
142
+ * tagging command choudoufu printed under it.
143
+ *
144
+ * The render's "Adoptable now" rows are ` <addr> <- <type> <identity>`
145
+ * followed by indented detail lines, one of which is ` adopt with: <cmd>`
146
+ * (`internal/command/views/live_adoption.go`). choudoufu builds that command
147
+ * to be pasted verbatim: every interpolated value is shell-quoted, the tagging
148
+ * verb comes from its own botocore-derived table rather than a guess, and the
149
+ * provider configuration's region and endpoint ride along as `--region` and
150
+ * `--endpoint-url`, so the write lands where the resource is rather than
151
+ * wherever an operator's AWS CLI profile points. Re-deriving any of that from
152
+ * the two marker values alone would be a worse command, so this reads the one
153
+ * choudoufu already wrote.
154
+ *
155
+ * An address the render offered no command for is simply absent from the map.
156
+ */
157
+ export function parseAdoptionCommands(ledgerText: string): Map<string, string> {
158
+ const commands = new Map<string, string>();
159
+ let current: string | undefined;
160
+ for (const line of ledgerText.split("\n")) {
161
+ const row = /^ {2}(\S+) <- \S+ /.exec(line);
162
+ if (row) {
163
+ current = row[1];
164
+ continue;
165
+ }
166
+ const cmd = /^ {6}(?:or )?adopt with: (.+)$/.exec(line);
167
+ if (cmd && current) {
168
+ commands.set(current, cmd[1].trim());
169
+ continue;
170
+ }
171
+ // A blank line ends a row's detail block; a non-indented line ends the
172
+ // section. Either way the next "adopt with:" belongs to nobody until the
173
+ // next row line names an address.
174
+ if (line.trim() === "" || !line.startsWith(" ")) current = undefined;
175
+ }
176
+ return commands;
177
+ }
178
+
179
+ const plural = (n: number, one: string, many: string): string => (n === 1 ? one : many);
180
+
181
+ /**
182
+ * Render {@link readAdoptionLedger}'s answer as the text an issue or PR body
183
+ * carries under the plan.
184
+ *
185
+ * One line per adoptable match, naming the declared address, the live resource
186
+ * it binds, and the two tag values that adopt it, in the token forms
187
+ * `-adoption-only` prints them in (`<addr> <- <type> <identity>`,
188
+ * `tofu-estate=`, `tofu-address=`). Contested addresses follow, listed and not
189
+ * offered. An empty ledger still renders a line: "nothing adoptable" is a
190
+ * result a reader wants, and a section that vanishes reads as one that was
191
+ * never computed.
192
+ */
193
+ export function renderAdoptionLedger(ledger: AdoptionLedger, estate?: string): string {
194
+ const where = estate ? `, estate ${JSON.stringify(estate)}` : "";
195
+ const lines: string[] = [];
196
+
197
+ if (ledger.adoptions.length === 0) {
198
+ lines.push(`Adoptable now: nothing${where}`, "");
199
+ lines.push(
200
+ "No live resource sits at a declared identity carrying a marker this estate could write. " +
201
+ "Anything the plan proposes creating, it proposes creating for real.",
202
+ );
203
+ } else {
204
+ lines.push(
205
+ `Adoptable now: ${ledger.adoptions.length} live ${plural(ledger.adoptions.length, "resource", "resources")}${where}`,
206
+ "",
207
+ );
208
+ lines.push(
209
+ "Each line is a live resource found at a declared resource's identity, carrying no marker for " +
210
+ "this estate. Writing the two tags shown adopts it; nothing is bound until they are written, " +
211
+ "because ownership is the tofu-estate and tofu-address pair and nothing else.",
212
+ "",
213
+ );
214
+ for (const c of ledger.adoptions) {
215
+ lines.push(
216
+ ` ${c.addr} <- ${c.type} ${c.identity} write: ` +
217
+ `tofu-estate=${c.markerEstate} tofu-address=${c.markerAddress}`,
218
+ );
219
+ }
220
+ }
221
+
222
+ if (ledger.contested.length > 0) {
223
+ lines.push("");
224
+ lines.push(
225
+ `Ambiguous: ${ledger.ambiguous} declared ${plural(ledger.ambiguous, "address", "addresses")} ` +
226
+ "with more than one candidate",
227
+ "",
228
+ );
229
+ lines.push(
230
+ "More than one live resource sits at each of these declared identities, so no single tag write " +
231
+ "claims the address. None of them is adopted. Resolve it by deleting the duplicate or by " +
232
+ "writing the marker onto the one you mean.",
233
+ "",
234
+ );
235
+ for (const c of ledger.contested) {
236
+ lines.push(` ${c.addr} <- ${c.type} ${c.identity}`);
237
+ }
238
+ }
239
+
240
+ return lines.join("\n");
241
+ }
@@ -7,7 +7,15 @@
7
7
 
8
8
  import { describe, test, expect } from "vitest";
9
9
  import { isStepOutputRef, type StepOutputRef } from "@intentius/chant/op";
10
- import { terraformInit, terraformPlan, terraformApply, terraformShow } from "./builders";
10
+ import {
11
+ terraformInit,
12
+ terraformPlan,
13
+ terraformApply,
14
+ terraformShow,
15
+ choudoufuLivePlan,
16
+ choudoufuLiveLs,
17
+ choudoufuLiveCheck,
18
+ } from "./builders";
11
19
 
12
20
  describe("terraform typed step builders (#2086)", () => {
13
21
  test("terraformInit: root is positional, longInfra by default", () => {
@@ -73,18 +81,65 @@ describe("terraform typed step builders (#2086)", () => {
73
81
  test(".out throws when the producing step has no id", () => {
74
82
  expect(() => terraformPlan("app").out.planFile).toThrow(/has no id/);
75
83
  });
84
+
85
+ test("terraformApply: no opts at all is valid (#2103, a live root's bare apply)", () => {
86
+ expect(terraformApply("estate")).toMatchObject({
87
+ fn: "terraformApply",
88
+ args: { root: "estate" },
89
+ profile: "longInfra",
90
+ });
91
+ expect(terraformApply("estate").args?.planFile).toBeUndefined();
92
+ });
93
+ });
94
+
95
+ describe("choudoufu typed step builders (#2103)", () => {
96
+ test("choudoufuLivePlan: root is positional, longInfra by default, estate is optional", () => {
97
+ expect(choudoufuLivePlan("estate")).toMatchObject({
98
+ kind: "activity",
99
+ fn: "choudoufuLivePlan",
100
+ args: { root: "estate" },
101
+ profile: "longInfra",
102
+ });
103
+ expect(choudoufuLivePlan("estate", { estate: "prod-networking" })).toMatchObject({
104
+ args: { root: "estate", estate: "prod-networking" },
105
+ });
106
+ });
107
+
108
+ test("choudoufuLivePlan: .out refs for drift, unowned, adoptable and documentPath", () => {
109
+ const plan = choudoufuLivePlan("estate", { id: "live-plan" });
110
+ expect(isStepOutputRef(plan.out.drift as StepOutputRef)).toBe(true);
111
+ expect((plan.out.unowned as StepOutputRef).path).toBe("unowned");
112
+ expect((plan.out.adoptable as StepOutputRef).path).toBe("adoptable");
113
+ expect((plan.out.documentPath as StepOutputRef).path).toBe("documentPath");
114
+ });
115
+
116
+ test("choudoufuLiveLs: fastIdempotent by default", () => {
117
+ expect(choudoufuLiveLs("estate", { consistent: true })).toMatchObject({
118
+ fn: "choudoufuLiveLs",
119
+ args: { root: "estate", consistent: true },
120
+ profile: "fastIdempotent",
121
+ });
122
+ });
123
+
124
+ test("choudoufuLiveCheck: fastIdempotent by default, no opts needed at all", () => {
125
+ expect(choudoufuLiveCheck("estate")).toMatchObject({
126
+ kind: "activity",
127
+ fn: "choudoufuLiveCheck",
128
+ args: { root: "estate" },
129
+ profile: "fastIdempotent",
130
+ });
131
+ });
76
132
  });
77
133
 
78
134
  // ── Compile-time-only: authoring-time type errors (never executed) ──────────
79
135
  function _typeChecksOnly(): void {
80
- // @ts-expect-error — planFile is required on an apply: this activity has no
81
- // bare-apply mode, so omitting it is a compile error, not a runtime one.
82
- terraformApply("app");
83
-
84
136
  // @ts-expect-error — "planfile" (wrong case) is not a key of TerraformApplyArgs.
85
137
  terraformApply("app", { planfile: "chant.tfplan" });
86
138
 
87
139
  // @ts-expect-error — `root` is positional; it is not a member of opts.
88
140
  terraformInit("app", { root: "other" });
141
+
142
+ // @ts-expect-error: "estatee" (typo) is not a key of ChoudoufuLivePlanArgs.
143
+ choudoufuLivePlan("estate", { estatee: "prod" });
89
144
  }
90
145
  void _typeChecksOnly;
@@ -27,6 +27,10 @@ import type {
27
27
  TerraformPlanArgs,
28
28
  TerraformApplyArgs,
29
29
  TerraformShowArgs,
30
+ ChoudoufuLivePlanArgs,
31
+ ChoudoufuLiveLsArgs,
32
+ ChoudoufuLiveCheckArgs,
33
+ ChoudoufuAdoptArgs,
30
34
  } from "./activities/terraform";
31
35
 
32
36
  /** Extra opts every wrapper below accepts alongside its activity's own fields. */
@@ -63,20 +67,104 @@ export const terraformPlan = (
63
67
  };
64
68
 
65
69
  /**
66
- * `terraform apply <planFile>` in the named root — the fully typed twin of the
70
+ * `terraform apply <planFile>` in the named root, the fully typed twin of the
67
71
  * `terraformApply` activity. `opts` is {@link TerraformApplyArgs} itself,
68
- * minus the positional `root`, so `planFile` stays required at the call site
69
- * and the activity's bare-apply refusal is a compile error rather than a
70
- * runtime one. Defaults to the `longInfra` profile.
72
+ * minus the positional `root`. `planFile` is optional in the type and
73
+ * required in practice, on a live root as much as a stock one: the activity
74
+ * refuses a missing one, and on a live root the file is the approval artifact
75
+ * choudoufu v0.13.0 re-plans against and refuses on a mismatch (choudoufu
76
+ * #878). Defaults to the `longInfra` profile.
71
77
  */
72
78
  export const terraformApply = (
73
79
  root: string,
74
- opts: WithStepRefs<Omit<TerraformApplyArgs, "root">> & StepOpts,
80
+ opts?: WithStepRefs<Omit<TerraformApplyArgs, "root">> & StepOpts,
75
81
  ): NamedActivityStep => {
76
82
  const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
77
83
  return activity("terraformApply", { root, ...args }, { profile: profile ?? "longInfra", ...(id ? { id } : {}) });
78
84
  };
79
85
 
86
+ /**
87
+ * `choudoufu live-plan -detailed-exitcode -json -estate=<estate>` in the
88
+ * named root, plus the human render. The fully typed twin of the
89
+ * `choudoufuLivePlan` activity (#2103). `opts` is {@link ChoudoufuLivePlanArgs}
90
+ * itself, minus the positional `root`; `estate` is optional, auto-detected
91
+ * from the root's `live` block or `estate.chdf.hcl` sidecar when omitted.
92
+ * Defaults to the `longInfra` profile: like `terraformPlan`, this reads the
93
+ * live system in full (the estate-wide sweep).
94
+ *
95
+ * Give this step an `id` to read `.out.drift`, `.out.unowned`,
96
+ * `.out.adoptable` or `.out.documentPath` from a later step.
97
+ */
98
+ export const choudoufuLivePlan = (
99
+ root: string,
100
+ opts?: WithStepRefs<Omit<ChoudoufuLivePlanArgs, "root">> & StepOpts,
101
+ ): NamedActivityStep => {
102
+ const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
103
+ return activity(
104
+ "choudoufuLivePlan",
105
+ { root, ...args },
106
+ { profile: profile ?? "longInfra", ...(id ? { id } : {}) },
107
+ );
108
+ };
109
+
110
+ /**
111
+ * `choudoufu live-ls -estate=<estate> -json [-consistent]` in the named root,
112
+ * the fully typed twin of the `choudoufuLiveLs` activity (#2103). `opts` is
113
+ * {@link ChoudoufuLiveLsArgs} itself, minus the positional `root`. Defaults to
114
+ * the `fastIdempotent` profile: `live-ls` reads a tagging-API listing and
115
+ * calls no other provider.
116
+ */
117
+ export const choudoufuLiveLs = (
118
+ root: string,
119
+ opts?: WithStepRefs<Omit<ChoudoufuLiveLsArgs, "root">> & StepOpts,
120
+ ): NamedActivityStep => {
121
+ const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
122
+ return activity(
123
+ "choudoufuLiveLs",
124
+ { root, ...args },
125
+ { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) },
126
+ );
127
+ };
128
+
129
+ /**
130
+ * `choudoufu live-check -json` in the named root, the fully typed twin of the
131
+ * `choudoufuLiveCheck` activity (#2103). `opts` is {@link ChoudoufuLiveCheckArgs}
132
+ * itself, minus the positional `root`. Defaults to the `fastIdempotent`
133
+ * profile: no cloud calls, no state.
134
+ */
135
+ export const choudoufuLiveCheck = (
136
+ root: string,
137
+ opts?: WithStepRefs<Omit<ChoudoufuLiveCheckArgs, "root">> & StepOpts,
138
+ ): NamedActivityStep => {
139
+ const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
140
+ return activity(
141
+ "choudoufuLiveCheck",
142
+ { root, ...args },
143
+ { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) },
144
+ );
145
+ };
146
+
147
+ /**
148
+ * Write the ownership markers that claim an adoption ledger's matches, in the
149
+ * named root. The fully typed twin of the `choudoufuAdopt` activity (#2105).
150
+ * `opts` is {@link ChoudoufuAdoptArgs} itself, minus the positional `root`, so
151
+ * `adoptions` is required at the call site — an adopt step with nothing to
152
+ * adopt is an authoring mistake, not a no-op worth defaulting to. Defaults to
153
+ * the `longInfra` profile: one tagging round trip per resource, against the
154
+ * cloud.
155
+ */
156
+ export const choudoufuAdopt = (
157
+ root: string,
158
+ opts: WithStepRefs<Omit<ChoudoufuAdoptArgs, "root">> & StepOpts,
159
+ ): NamedActivityStep => {
160
+ const { args, profile, id } = takeProfileAndId(opts as Record<string, unknown> | undefined);
161
+ return activity(
162
+ "choudoufuAdopt",
163
+ { root, ...args },
164
+ { profile: profile ?? "longInfra", ...(id ? { id } : {}) },
165
+ );
166
+ };
167
+
80
168
  /**
81
169
  * `terraform show` in the named root, over state or over a saved plan. `opts`
82
170
  * is {@link TerraformShowArgs} itself, minus the positional `root`. Defaults
@@ -0,0 +1,85 @@
1
+ import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, describe, expect, test } from "vitest";
5
+ import { resolveRootModeSync } from "./resolve-root-mode";
6
+
7
+ const dirs: string[] = [];
8
+
9
+ function tempProject(): string {
10
+ const dir = mkdtempSync(join(tmpdir(), "chant-tf-resolve-root-mode-"));
11
+ dirs.push(dir);
12
+ return dir;
13
+ }
14
+
15
+ afterEach(() => {
16
+ for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true });
17
+ });
18
+
19
+ describe("resolveRootModeSync (#2106)", () => {
20
+ test("live: choudoufu binary and a declared estate", () => {
21
+ const project = tempProject();
22
+ mkdirSync(join(project, "root"), { recursive: true });
23
+ writeFileSync(
24
+ join(project, "root", "main.tf"),
25
+ ["terraform {", " live {", ' estate = "fixture-estate"', " }", "}", ""].join("\n"),
26
+ );
27
+ writeFileSync(
28
+ join(project, "chant.config.json"),
29
+ JSON.stringify({ terraform: { binary: "choudoufu", roots: { app: { dir: "./root" } } } }),
30
+ );
31
+ expect(resolveRootModeSync("app", project)).toEqual({ mode: "live", dir: join(project, "root") });
32
+ });
33
+
34
+ test("state: choudoufu binary but no declared estate", () => {
35
+ const project = tempProject();
36
+ mkdirSync(join(project, "root"), { recursive: true });
37
+ writeFileSync(join(project, "root", "main.tf"), 'resource "null_resource" "x" {}\n');
38
+ writeFileSync(
39
+ join(project, "chant.config.json"),
40
+ JSON.stringify({ terraform: { binary: "choudoufu", roots: { app: { dir: "./root" } } } }),
41
+ );
42
+ expect(resolveRootModeSync("app", project)).toEqual({ mode: "state", dir: join(project, "root") });
43
+ });
44
+
45
+ test("state: no binary declared at all, root dir need not even exist", () => {
46
+ const project = tempProject();
47
+ writeFileSync(join(project, "chant.config.json"), JSON.stringify({ terraform: { roots: { app: { dir: "./root" } } } }));
48
+ expect(resolveRootModeSync("app", project)).toEqual({ mode: "state", dir: join(project, "root") });
49
+ });
50
+
51
+ test("undefined: no chant.config.json findable from cwd", () => {
52
+ const project = tempProject();
53
+ expect(resolveRootModeSync("app", project)).toBeUndefined();
54
+ });
55
+
56
+ test("undefined: config found is chant.config.ts, not .json", () => {
57
+ const project = tempProject();
58
+ writeFileSync(join(project, "chant.config.ts"), "export default {} as const;\n");
59
+ expect(resolveRootModeSync("app", project)).toBeUndefined();
60
+ });
61
+
62
+ test("undefined: config exists but names no such root", () => {
63
+ const project = tempProject();
64
+ writeFileSync(
65
+ join(project, "chant.config.json"),
66
+ JSON.stringify({ terraform: { binary: "choudoufu", roots: { other: { dir: "./root" } } } }),
67
+ );
68
+ expect(resolveRootModeSync("app", project)).toBeUndefined();
69
+ });
70
+
71
+ test("undefined: choudoufu binary but the root directory does not exist", () => {
72
+ const project = tempProject();
73
+ writeFileSync(
74
+ join(project, "chant.config.json"),
75
+ JSON.stringify({ terraform: { binary: "choudoufu", roots: { app: { dir: "./missing" } } } }),
76
+ );
77
+ expect(resolveRootModeSync("app", project)).toBeUndefined();
78
+ });
79
+
80
+ test("defaults cwd to process.cwd() when omitted", () => {
81
+ // No assertion on the result (process.cwd() during a test run is
82
+ // unpredictable), only that it does not throw with no argument.
83
+ expect(() => resolveRootModeSync("app")).not.toThrow();
84
+ });
85
+ });
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Best-effort, synchronous read of whether a named `terraform.roots` entry is
3
+ * live: `terraform.binary` is `"choudoufu"` and the root's directory declares
4
+ * an estate — a `live { }` block or an `estate.chdf.hcl` sidecar (#2103).
5
+ *
6
+ * Two callers decide something at build time, before any activity runs, and
7
+ * neither can afford the async work `../op/activities/terraform.ts`'s own
8
+ * `resolveRoot` does (`loadChantConfigUpward`, a full `chant.config.ts`/`.json`
9
+ * walk):
10
+ *
11
+ * - `TerraformApplyOp` (`../composites/terraform-apply-op.ts`) decides which
12
+ * phase shape to build — a stock plan-file pairing or a live re-plan-at-
13
+ * apply shape — before any step is emitted.
14
+ * - TF101 (`../lint/rules/plan-before-apply.ts`) decides whether a
15
+ * `terraformApply` call it is statically inspecting is exempt from the
16
+ * plan-file pairing it otherwise enforces.
17
+ *
18
+ * Both run in the build process (`chant build`/`chant lint`) rather than in
19
+ * an activity at run time, so a synchronous read is available, but only the
20
+ * cheap half of it: `findProjectConfig` (`@intentius/chant/project-root`) is a plain, already-
21
+ * synchronous upward filesystem walk, but a `chant.config.ts` is project-
22
+ * authored code, and evaluating it synchronously outside chant's own
23
+ * config-sandbox machinery is more than either caller's best-effort question
24
+ * is worth. So this reads `chant.config.json` only; a project on a `.ts`
25
+ * config, or with no config discoverable at all, or naming no matching root,
26
+ * resolves to `undefined` — "unknown" reads as "stock" to both callers, which
27
+ * is the conservative direction: TF101 keeps firing, and the composite keeps
28
+ * building the plan-file-carrying shape.
29
+ */
30
+
31
+ import { existsSync, readFileSync } from "node:fs";
32
+ import { resolve } from "node:path";
33
+ import { findProjectConfig } from "@intentius/chant/project-root";
34
+ import type { TerraformConfig } from "../config";
35
+ import { detectLiveEstate } from "./activities/live-detect";
36
+
37
+ export interface ResolvedRootMode {
38
+ /** `"live"` when `terraform.binary` is `"choudoufu"` and the root declares an estate; `"state"` otherwise. */
39
+ mode: "live" | "state";
40
+ /** The root module directory, resolved to an absolute path. */
41
+ dir: string;
42
+ }
43
+
44
+ /**
45
+ * Resolve `rootName`'s mode against the nearest `chant.config.json` found by
46
+ * walking up from `cwd` (default `process.cwd()`, matching every other
47
+ * `cwd`-defaulting option in this lexicon). `undefined` when that cannot be
48
+ * done with a plain, synchronous read: no config found, the config found is
49
+ * `chant.config.ts`, the config declares no such root, or the root's
50
+ * directory does not exist.
51
+ */
52
+ export function resolveRootModeSync(rootName: string, cwd?: string): ResolvedRootMode | undefined {
53
+ const { dir: projectRoot, configPath } = findProjectConfig(resolve(cwd ?? process.cwd()));
54
+ if (!configPath || !configPath.endsWith(".json")) return undefined;
55
+
56
+ let raw: unknown;
57
+ try {
58
+ raw = JSON.parse(readFileSync(configPath, "utf-8"));
59
+ } catch {
60
+ return undefined;
61
+ }
62
+
63
+ const terraform = (raw as { terraform?: TerraformConfig } | null)?.terraform;
64
+ const root = terraform?.roots?.[rootName];
65
+ if (!terraform || !root || typeof root.dir !== "string") return undefined;
66
+
67
+ const dir = resolve(projectRoot, root.dir);
68
+ if (terraform.binary !== "choudoufu") return { mode: "state", dir };
69
+ if (!existsSync(dir)) return undefined;
70
+
71
+ return { mode: detectLiveEstate(dir) !== undefined ? "live" : "state", dir };
72
+ }