@intentius/chant-lexicon-terraform 0.124.52 → 0.124.54

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 (336) hide show
  1. package/README.md +7 -6
  2. package/dist/__fixtures__/pr-loop/estate.component.d.ts +7 -0
  3. package/dist/__fixtures__/pr-loop/estate.component.d.ts.map +1 -0
  4. package/dist/__fixtures__/pr-loop/estate.component.js +7 -0
  5. package/dist/__fixtures__/report/generate.d.ts +26 -0
  6. package/dist/__fixtures__/report/generate.d.ts.map +1 -0
  7. package/dist/__fixtures__/report/generate.js +58 -0
  8. package/dist/applied-state.d.ts +27 -0
  9. package/dist/applied-state.d.ts.map +1 -0
  10. package/dist/applied-state.js +44 -0
  11. package/dist/backend-config.d.ts +53 -0
  12. package/dist/backend-config.d.ts.map +1 -0
  13. package/dist/backend-config.js +136 -0
  14. package/dist/behaviour/kinds.d.ts.map +1 -1
  15. package/dist/behaviour/kinds.js +4 -0
  16. package/dist/behaviour/predict.d.ts.map +1 -1
  17. package/dist/behaviour/predict.js +6 -2
  18. package/dist/behaviour/reference-catalog.d.ts +16 -5
  19. package/dist/behaviour/reference-catalog.d.ts.map +1 -1
  20. package/dist/behaviour/reference-catalog.js +16 -5
  21. package/dist/behaviour/request.d.ts.map +1 -1
  22. package/dist/behaviour/request.js +3 -2
  23. package/dist/changed-roots.d.ts +6 -4
  24. package/dist/changed-roots.d.ts.map +1 -1
  25. package/dist/changed-roots.js +14 -18
  26. package/dist/commands.d.ts.map +1 -1
  27. package/dist/commands.js +4 -3
  28. package/dist/components/capability-plugin.d.ts +65 -3
  29. package/dist/components/capability-plugin.d.ts.map +1 -1
  30. package/dist/components/capability-plugin.js +197 -13
  31. package/dist/components/compose-group.d.ts +145 -0
  32. package/dist/components/compose-group.d.ts.map +1 -0
  33. package/dist/components/compose-group.js +407 -0
  34. package/dist/components/placeholder-preview.d.ts +78 -0
  35. package/dist/components/placeholder-preview.d.ts.map +1 -0
  36. package/dist/components/placeholder-preview.js +254 -0
  37. package/dist/compose/apply-events.d.ts +46 -0
  38. package/dist/compose/apply-events.d.ts.map +1 -0
  39. package/dist/compose/apply-events.js +77 -0
  40. package/dist/compose/estate.d.ts +194 -0
  41. package/dist/compose/estate.d.ts.map +1 -0
  42. package/dist/compose/estate.js +363 -0
  43. package/dist/compose/hcl.d.ts +99 -0
  44. package/dist/compose/hcl.d.ts.map +1 -0
  45. package/dist/compose/hcl.js +302 -0
  46. package/dist/compose/module-address.d.ts +24 -0
  47. package/dist/compose/module-address.d.ts.map +1 -0
  48. package/dist/compose/module-address.js +43 -0
  49. package/dist/compose/root.d.ts +127 -0
  50. package/dist/compose/root.d.ts.map +1 -0
  51. package/dist/compose/root.js +586 -0
  52. package/dist/compose/run.d.ts +183 -0
  53. package/dist/compose/run.d.ts.map +1 -0
  54. package/dist/compose/run.js +447 -0
  55. package/dist/compose/slice.d.ts +102 -0
  56. package/dist/compose/slice.d.ts.map +1 -0
  57. package/dist/compose/slice.js +249 -0
  58. package/dist/compose/state-address.d.ts +28 -0
  59. package/dist/compose/state-address.d.ts.map +1 -0
  60. package/dist/compose/state-address.js +154 -0
  61. package/dist/compose/state-merge.d.ts +52 -0
  62. package/dist/compose/state-merge.d.ts.map +1 -0
  63. package/dist/compose/state-merge.js +135 -0
  64. package/dist/compose.d.ts +179 -0
  65. package/dist/compose.d.ts.map +1 -0
  66. package/dist/compose.js +482 -0
  67. package/dist/composites/terraform-apply-op.d.ts.map +1 -1
  68. package/dist/composites/terraform-apply-op.js +7 -3
  69. package/dist/config.d.ts +59 -13
  70. package/dist/config.d.ts.map +1 -1
  71. package/dist/config.js +43 -9
  72. package/dist/describe-resources.d.ts +74 -12
  73. package/dist/describe-resources.d.ts.map +1 -1
  74. package/dist/describe-resources.js +374 -66
  75. package/dist/drift-read.d.ts +33 -0
  76. package/dist/drift-read.d.ts.map +1 -0
  77. package/dist/drift-read.js +243 -0
  78. package/dist/drift-record.d.ts +147 -0
  79. package/dist/drift-record.d.ts.map +1 -0
  80. package/dist/drift-record.js +383 -0
  81. package/dist/hcl/edges.d.ts.map +1 -1
  82. package/dist/hcl/edges.js +24 -1
  83. package/dist/hcl/files.d.ts +61 -0
  84. package/dist/hcl/files.d.ts.map +1 -0
  85. package/dist/hcl/files.js +221 -0
  86. package/dist/hcl/instances.d.ts +82 -0
  87. package/dist/hcl/instances.d.ts.map +1 -0
  88. package/dist/hcl/instances.js +390 -0
  89. package/dist/hcl/parse.d.ts +81 -9
  90. package/dist/hcl/parse.d.ts.map +1 -1
  91. package/dist/hcl/parse.js +114 -16
  92. package/dist/hcl/roots.d.ts.map +1 -1
  93. package/dist/hcl/roots.js +5 -0
  94. package/dist/hcl/suppressions.d.ts.map +1 -1
  95. package/dist/hcl/suppressions.js +8 -2
  96. package/dist/index.d.ts +11 -2
  97. package/dist/index.d.ts.map +1 -1
  98. package/dist/index.js +21 -2
  99. package/dist/integrity.json +3 -3
  100. package/dist/live-plan-reasons.d.ts +21 -0
  101. package/dist/live-plan-reasons.d.ts.map +1 -0
  102. package/dist/live-plan-reasons.js +47 -0
  103. package/dist/lsp/option-keys.d.ts.map +1 -1
  104. package/dist/lsp/option-keys.js +2 -0
  105. package/dist/manifest.json +1 -1
  106. package/dist/op/activities/index.d.ts +6 -2
  107. package/dist/op/activities/index.d.ts.map +1 -1
  108. package/dist/op/activities/index.js +5 -1
  109. package/dist/op/activities/live-detect.d.ts +4 -3
  110. package/dist/op/activities/live-detect.d.ts.map +1 -1
  111. package/dist/op/activities/live-detect.js +57 -32
  112. package/dist/op/activities/pin-rollout.d.ts +2 -0
  113. package/dist/op/activities/pin-rollout.d.ts.map +1 -1
  114. package/dist/op/activities/pin-rollout.js +1 -0
  115. package/dist/op/activities/terraform.d.ts +153 -5
  116. package/dist/op/activities/terraform.d.ts.map +1 -1
  117. package/dist/op/activities/terraform.js +205 -20
  118. package/dist/op/activity-contracts.d.ts +27 -0
  119. package/dist/op/activity-contracts.d.ts.map +1 -1
  120. package/dist/op/activity-contracts.js +19 -1
  121. package/dist/op/builders.d.ts +7 -1
  122. package/dist/op/builders.d.ts.map +1 -1
  123. package/dist/op/builders.js +9 -0
  124. package/dist/pin/edit.d.ts.map +1 -1
  125. package/dist/pin/edit.js +35 -0
  126. package/dist/pin/rollout.d.ts +2 -0
  127. package/dist/pin/rollout.d.ts.map +1 -1
  128. package/dist/pin/rollout.js +14 -11
  129. package/dist/plan-shape.d.ts +30 -0
  130. package/dist/plan-shape.d.ts.map +1 -0
  131. package/dist/plan-shape.js +125 -0
  132. package/dist/plugin.d.ts.map +1 -1
  133. package/dist/plugin.js +32 -18
  134. package/dist/report.d.ts +15 -6
  135. package/dist/report.d.ts.map +1 -1
  136. package/dist/report.js +28 -55
  137. package/dist/skills/chant-terraform.md +1 -1
  138. package/dist/state/azure-blob.d.ts +68 -0
  139. package/dist/state/azure-blob.d.ts.map +1 -0
  140. package/dist/state/azure-blob.js +201 -0
  141. package/dist/state/backend.d.ts +158 -0
  142. package/dist/state/backend.d.ts.map +1 -0
  143. package/dist/state/backend.js +406 -0
  144. package/dist/state/exec.d.ts +39 -0
  145. package/dist/state/exec.d.ts.map +1 -0
  146. package/dist/state/exec.js +80 -0
  147. package/dist/state/gcs.d.ts +62 -0
  148. package/dist/state/gcs.d.ts.map +1 -0
  149. package/dist/state/gcs.js +224 -0
  150. package/dist/state/index.d.ts +15 -0
  151. package/dist/state/index.d.ts.map +1 -0
  152. package/dist/state/index.js +14 -0
  153. package/dist/state/locks.d.ts +105 -0
  154. package/dist/state/locks.d.ts.map +1 -0
  155. package/dist/state/locks.js +130 -0
  156. package/dist/state/object-store.d.ts +37 -0
  157. package/dist/state/object-store.d.ts.map +1 -0
  158. package/dist/state/object-store.js +20 -0
  159. package/dist/state/root-states.d.ts +131 -0
  160. package/dist/state/root-states.d.ts.map +1 -0
  161. package/dist/state/root-states.js +241 -0
  162. package/dist/state/s3.d.ts +68 -0
  163. package/dist/state/s3.d.ts.map +1 -0
  164. package/dist/state/s3.js +170 -0
  165. package/dist/state/store.d.ts +51 -0
  166. package/dist/state/store.d.ts.map +1 -0
  167. package/dist/state/store.js +185 -0
  168. package/dist/terragrunt/affected.d.ts.map +1 -1
  169. package/dist/terragrunt/affected.js +2 -1
  170. package/package.json +7 -3
  171. package/src/__fixtures__/ACCEPTANCE.md +69 -7
  172. package/src/__fixtures__/compose/apply-failed-in-app.jsonl +12 -0
  173. package/src/__fixtures__/compose/state-app.json +66 -0
  174. package/src/__fixtures__/compose/state-edge.json +40 -0
  175. package/src/__fixtures__/compose/state-net.json +58 -0
  176. package/src/__fixtures__/compose/three-app.plan.json +184 -0
  177. package/src/__fixtures__/compose/three-composed.plan.json +509 -0
  178. package/src/__fixtures__/compose/three-edge.plan.json +144 -0
  179. package/src/__fixtures__/compose/three-net.plan.json +174 -0
  180. package/src/__fixtures__/compose/unknown-composed.plan.json +228 -0
  181. package/src/__fixtures__/compose/working-after-apply.json +145 -0
  182. package/src/__fixtures__/config-blocks/app/main.tf +61 -0
  183. package/src/__fixtures__/config-blocks/app/modules/network/main.tf +4 -0
  184. package/src/__fixtures__/config-blocks/app-json/main.tf.json +39 -0
  185. package/src/__fixtures__/config-files/shadow/main.tf +3 -0
  186. package/src/__fixtures__/config-files/shadow/main.tofu +3 -0
  187. package/src/__fixtures__/config-files/shadow/other.tf +3 -0
  188. package/src/__fixtures__/config-files/tf/main.tf +11 -0
  189. package/src/__fixtures__/config-files/tf-json/main.tf.json +18 -0
  190. package/src/__fixtures__/config-files/tofu/main.tofu +11 -0
  191. package/src/__fixtures__/config-files/tofu-json/main.tofu.json +18 -0
  192. package/src/__fixtures__/config-files/tofu-only/main.tofu +3 -0
  193. package/src/__fixtures__/drift-record/README.md +11 -0
  194. package/src/__fixtures__/drift-record/live-plan-sections.json +27 -0
  195. package/src/__fixtures__/instance-forms/app/main.tf +110 -0
  196. package/src/__fixtures__/instance-forms/app/modules/svc/main.tf +3 -0
  197. package/src/__fixtures__/instance-vars/app/main.tf +52 -0
  198. package/src/__fixtures__/instance-vars/app/modules/app/main.tf +12 -0
  199. package/src/__fixtures__/instance-vars/app/modules/edge/main.tf +3 -0
  200. package/src/__fixtures__/instance-vars/app/terraform.tfvars +1 -0
  201. package/src/__fixtures__/live-module-keyed/live-graph.json +259 -0
  202. package/src/__fixtures__/live-module-keyed/live-plan.json +87 -0
  203. package/src/__fixtures__/live-module-keyed/root/estate.chdf.hcl +1 -0
  204. package/src/__fixtures__/live-module-keyed/root/main.tf +21 -0
  205. package/src/__fixtures__/live-module-keyed/root/modules/app/main.tf +17 -0
  206. package/src/__fixtures__/live-module-keyed/root/modules/edge/main.tf +7 -0
  207. package/src/__fixtures__/live-module-keyed/root/versions.tf +30 -0
  208. package/src/__fixtures__/live-module-keyed/show-state.json +146 -0
  209. package/src/__fixtures__/pr-loop/README.md +50 -0
  210. package/src/__fixtures__/pr-loop/chant.config.json +13 -0
  211. package/src/__fixtures__/pr-loop/estate.component.ts +10 -0
  212. package/src/__fixtures__/pr-loop/roots/a/main.tf +11 -0
  213. package/src/__fixtures__/pr-loop/roots/app/main.tf +19 -0
  214. package/src/__fixtures__/pr-loop/roots/b/main.tf +7 -0
  215. package/src/__fixtures__/pr-loop/roots/dns/main.tf +3 -0
  216. package/src/__fixtures__/pr-loop/roots/net/main.tf +7 -0
  217. package/src/__fixtures__/report/applied.show.json +51 -0
  218. package/src/__fixtures__/report/generate.ts +63 -0
  219. package/src/__fixtures__/report/plan.show.json +200 -0
  220. package/src/__fixtures__/report/tf-apply/change-set.json +170 -0
  221. package/src/__fixtures__/report/tf-apply/report.json +338 -0
  222. package/src/__fixtures__/report/tf-plan/change-set.json +170 -0
  223. package/src/__fixtures__/report/tf-plan/report.json +320 -0
  224. package/src/__fixtures__/three-cloud-azure.graph.json +290 -0
  225. package/src/applied-state.ts +52 -0
  226. package/src/backend-config.test.ts +105 -0
  227. package/src/backend-config.ts +133 -0
  228. package/src/behaviour/kinds.ts +4 -0
  229. package/src/behaviour/predict.ts +5 -2
  230. package/src/behaviour/reference-catalog.plugin.test.ts +65 -0
  231. package/src/behaviour/reference-catalog.ts +16 -5
  232. package/src/behaviour/request.ts +3 -2
  233. package/src/change-set-instances.test.ts +98 -0
  234. package/src/changed-roots.ts +14 -16
  235. package/src/commands.ts +4 -3
  236. package/src/components/capability-plugin.test.ts +172 -5
  237. package/src/components/capability-plugin.ts +212 -11
  238. package/src/components/change-set-graph.e2e.test.ts +11 -5
  239. package/src/components/compose-group.test.ts +486 -0
  240. package/src/components/compose-group.ts +489 -0
  241. package/src/components/demo-composed-roots.e2e.test.ts +440 -0
  242. package/src/components/member-outputs.test.ts +76 -0
  243. package/src/components/placeholder-preview.test.ts +147 -0
  244. package/src/components/placeholder-preview.ts +257 -0
  245. package/src/components/pr-loop-compose.tofu.e2e.test.ts +363 -0
  246. package/src/components/pr-loop.tofu.test.ts +112 -56
  247. package/src/compose/apply-events.test.ts +62 -0
  248. package/src/compose/apply-events.ts +100 -0
  249. package/src/compose/estate.choudoufu.e2e.test.ts +112 -0
  250. package/src/compose/estate.test.ts +421 -0
  251. package/src/compose/estate.ts +443 -0
  252. package/src/compose/hcl.ts +311 -0
  253. package/src/compose/module-address.ts +49 -0
  254. package/src/compose/root.test.ts +416 -0
  255. package/src/compose/root.ts +592 -0
  256. package/src/compose/run.test.ts +440 -0
  257. package/src/compose/run.tofu.test.ts +176 -0
  258. package/src/compose/run.ts +527 -0
  259. package/src/compose/slice.test.ts +184 -0
  260. package/src/compose/slice.ts +290 -0
  261. package/src/compose/state-address.ts +157 -0
  262. package/src/compose/state-merge.test.ts +131 -0
  263. package/src/compose/state-merge.ts +148 -0
  264. package/src/compose.test.ts +248 -0
  265. package/src/compose.ts +627 -0
  266. package/src/composites/terraform-apply-op.test.ts +9 -0
  267. package/src/composites/terraform-apply-op.ts +7 -3
  268. package/src/config-file-listers.test.ts +265 -0
  269. package/src/config.ts +44 -9
  270. package/src/describe-resources.engine-facts.test.ts +412 -0
  271. package/src/describe-resources.live-module-keyed.test.ts +290 -0
  272. package/src/describe-resources.ts +416 -70
  273. package/src/drift-read.ts +284 -0
  274. package/src/drift-record.test.ts +223 -0
  275. package/src/drift-record.ts +455 -0
  276. package/src/hcl/config-blocks.test.ts +174 -0
  277. package/src/hcl/edges.ts +21 -1
  278. package/src/hcl/files.ts +234 -0
  279. package/src/hcl/instance-forms.test.ts +94 -0
  280. package/src/hcl/instances.test.ts +137 -0
  281. package/src/hcl/instances.ts +422 -0
  282. package/src/hcl/parse.test.ts +139 -1
  283. package/src/hcl/parse.ts +149 -16
  284. package/src/hcl/roots.ts +6 -0
  285. package/src/hcl/suppressions.ts +8 -2
  286. package/src/index.ts +76 -0
  287. package/src/live-plan-reasons.ts +54 -0
  288. package/src/lsp/completions.test.ts +1 -1
  289. package/src/lsp/option-keys.ts +2 -0
  290. package/src/op/activities/choudoufu-plan-set-op.test.ts +111 -0
  291. package/src/op/activities/index.ts +9 -0
  292. package/src/op/activities/live-detect.ts +55 -26
  293. package/src/op/activities/pin-rollout.ts +3 -0
  294. package/src/op/activities/registry.test.ts +2 -1
  295. package/src/op/activities/terraform.test.ts +55 -0
  296. package/src/op/activities/terraform.ts +266 -22
  297. package/src/op/activity-contracts.test.ts +1 -0
  298. package/src/op/activity-contracts.ts +24 -1
  299. package/src/op/builders.ts +14 -0
  300. package/src/pin/bundle.test.ts +2 -0
  301. package/src/pin/edit.ts +30 -0
  302. package/src/pin/rollout.ts +16 -11
  303. package/src/plan-shape.test.ts +148 -0
  304. package/src/plan-shape.ts +128 -0
  305. package/src/plugin.ts +33 -19
  306. package/src/report.test.ts +82 -1
  307. package/src/report.ts +31 -54
  308. package/src/signal-only-root.test.ts +72 -0
  309. package/src/skills/chant-terraform.md +1 -1
  310. package/src/state/azure-blob.ts +223 -0
  311. package/src/state/backend-verdict.test.ts +187 -0
  312. package/src/state/backend.test.ts +127 -0
  313. package/src/state/backend.ts +488 -0
  314. package/src/state/exec.ts +111 -0
  315. package/src/state/gcs.ts +254 -0
  316. package/src/state/index.ts +15 -0
  317. package/src/state/locks.test.ts +84 -0
  318. package/src/state/locks.ts +201 -0
  319. package/src/state/object-store.ts +35 -0
  320. package/src/state/root-states-floci.e2e.test.ts +66 -0
  321. package/src/state/root-states.test.ts +462 -0
  322. package/src/state/root-states.ts +309 -0
  323. package/src/state/s3.ts +193 -0
  324. package/src/state/store.ts +225 -0
  325. package/src/terragrunt/affected.ts +2 -1
  326. package/src/workspace-kinds.test.ts +4 -17
  327. package/workspace-kinds.json +1 -24
  328. package/dist/workspace-member.d.ts +0 -30
  329. package/dist/workspace-member.d.ts.map +0 -1
  330. package/dist/workspace-member.js +0 -91
  331. package/schemas/pulumi.schema.json +0 -34
  332. package/src/testdata/pulumi-member/Pulumi.yaml +0 -2
  333. package/src/testdata/pulumi-member/stack-export.json +0 -53
  334. package/src/testdata/pulumi-unexported/Pulumi.yaml +0 -2
  335. package/src/workspace-member.test.ts +0 -89
  336. package/src/workspace-member.ts +0 -126
@@ -0,0 +1,311 @@
1
+ /**
2
+ * The HCL a composition reads from a root's files (chant#4438): its backend
3
+ * and the state that backend names, its `terraform_remote_state` and
4
+ * `terraform_estate_outputs` reads, its `live` estate, the providers its lock
5
+ * file pins, and the local modules it calls.
6
+ *
7
+ * Lifted from terragucci at c493e0e (`compose.ts` `blankComments`,
8
+ * `lockedProviders`, `localModules`, `importLiteral`, `jsonBlocks`;
9
+ * `detect.ts` `blockBody`, `hclAttr`, `stateOf`, `estateOf`,
10
+ * `estateOutputReads`, `liveRoots`; `linked.ts` `configFiles`), reading a
11
+ * root's `-backend-config` from `terraform.roots.<name>.backendConfig` where
12
+ * terragucci read `TF_CLI_ARGS_init`. Regex over text with comments blanked,
13
+ * as there: no HCL parse runs.
14
+ */
15
+
16
+ import { existsSync, readFileSync } from "node:fs";
17
+ import { join, relative, resolve } from "node:path";
18
+ import { isJsonConfigFile, listConfigFilesSafe } from "../hcl/files";
19
+ import { literal, stateAddress, type Attr, type StateKey } from "./state-address";
20
+ import { readBackend, type ReadBackendInput } from "../state/backend";
21
+
22
+ export type { StateKey } from "./state-address";
23
+
24
+ /**
25
+ * HCL text with its comments blanked to spaces, newlines kept, so every
26
+ * position in it is the same position in the text as written. Strings and
27
+ * their escapes are read through, so a `#` or `//` in one stays.
28
+ */
29
+ export function blankComments(text: string): string {
30
+ const out = text.split("");
31
+ let inString = false;
32
+ for (let i = 0; i < text.length; i++) {
33
+ const c = text[i]!;
34
+ if (inString) {
35
+ if (c === "\\") i++;
36
+ else if (c === '"') inString = false;
37
+ continue;
38
+ }
39
+ if (c === '"') inString = true;
40
+ else if (c === "#" || (c === "/" && text[i + 1] === "/")) {
41
+ for (; i < text.length && text[i] !== "\n"; i++) out[i] = " ";
42
+ } else if (c === "/" && text[i + 1] === "*") {
43
+ const end = text.indexOf("*/", i + 2);
44
+ const stop = end < 0 ? text.length : end + 2;
45
+ for (; i < stop; i++) if (text[i] !== "\n") out[i] = " ";
46
+ i--;
47
+ }
48
+ }
49
+ return out.join("");
50
+ }
51
+
52
+ /** The text of the block that starts at `start`: up to the brace that closes its first `{`. */
53
+ export function blockBody(text: string, start: number): string {
54
+ let depth = 0;
55
+ for (let i = text.indexOf("{", start); i >= 0 && i < text.length; i++) {
56
+ if (text[i] === "{") depth++;
57
+ else if (text[i] === "}" && --depth === 0) return text.slice(start, i + 1);
58
+ }
59
+ return text.slice(start);
60
+ }
61
+
62
+ /**
63
+ * An HCL block's attribute: its string, `null` when it is written as anything
64
+ * but a plain string, undefined when it is not written.
65
+ */
66
+ export const hclAttr =
67
+ (body: string): Attr =>
68
+ (name) => {
69
+ const m = body.match(new RegExp(`(?<![\\w.-])${name}\\s*=(?!=)\\s*(.)`));
70
+ if (!m) return undefined;
71
+ if (m[1] !== '"') return null;
72
+ const start = m.index! + m[0].length;
73
+ let out = "";
74
+ for (let i = start; i < body.length; i++) {
75
+ if (body[i] === "\\") {
76
+ out += body[++i] ?? "";
77
+ continue;
78
+ }
79
+ if (body[i] === '"') return literal(out) ?? null;
80
+ out += body[i];
81
+ }
82
+ return null;
83
+ };
84
+
85
+ /**
86
+ * A root's own configuration files, not its modules', as paths: the set the
87
+ * root's `binary` reads (`.tf` and `.tf.json`, plus `.tofu` and `.tofu.json`
88
+ * for tofu or choudoufu), from the lexicon's one lister (chant#4457).
89
+ */
90
+ export function configFiles(dir: string, binary?: string): string[] {
91
+ return listConfigFilesSafe(dir, binary).map((n) => join(dir, n));
92
+ }
93
+
94
+ /** One configuration file's text, comments blanked when it is HCL. */
95
+ export interface RootText {
96
+ file: string;
97
+ text: string;
98
+ json: boolean;
99
+ }
100
+
101
+ /** The text of a root's own configuration files, comments blanked so positions still match. */
102
+ export function rootTexts(dir: string, binary?: string): RootText[] {
103
+ return configFiles(dir, binary).map((f) => {
104
+ const raw = readFileSync(f, "utf-8");
105
+ const json = isJsonConfigFile(f);
106
+ return { file: f, text: json ? raw : blankComments(raw), json };
107
+ });
108
+ }
109
+
110
+ const objectOf = (v: unknown): Record<string, unknown> | undefined => (v && typeof v === "object" && !Array.isArray(v) ? (v as Record<string, unknown>) : undefined);
111
+ /** A block in Terraform's JSON syntax: an object, or a list of objects whose first is taken. */
112
+ const firstOf = (v: unknown): Record<string, unknown> | undefined => objectOf(Array.isArray(v) ? v[0] : v);
113
+ const parseJson = (text: string): unknown => {
114
+ try {
115
+ return JSON.parse(text);
116
+ } catch {
117
+ return undefined;
118
+ }
119
+ };
120
+
121
+ /** The blocks of a kind in a JSON-syntax file: `{"import": [...]}` or `{"import": {...}}`. */
122
+ export function jsonBlocks(doc: unknown, kind: string): Record<string, unknown>[] {
123
+ const v = objectOf(doc)?.[kind];
124
+ if (Array.isArray(v)) return v.filter((x): x is Record<string, unknown> => !!x && typeof x === "object");
125
+ return v && typeof v === "object" ? [v as Record<string, unknown>] : [];
126
+ }
127
+
128
+ /** A `terraform_remote_state` block: its label, the state it reads, and whether it repeats (count or for_each). */
129
+ export interface RemoteStateRead extends StateKey {
130
+ name: string;
131
+ repeated: boolean;
132
+ }
133
+
134
+ /** What a root's code says about state: the state its backend keeps and the states its `terraform_remote_state` blocks read. */
135
+ export interface RootState {
136
+ own?: StateKey;
137
+ reads: RemoteStateRead[];
138
+ }
139
+
140
+ const asKey = (a: ReturnType<typeof stateAddress>): StateKey | undefined => ("unresolved" in a ? undefined : a);
141
+
142
+ /** A backend configuration value as an address part: a plain string, `null` for anything else written, undefined for none. */
143
+ const configValue = (v: unknown): string | null | undefined => {
144
+ if (v === undefined || v === null) return undefined;
145
+ if (typeof v === "number" || typeof v === "boolean") return String(v);
146
+ return literal(v) ?? null;
147
+ };
148
+
149
+ /**
150
+ * The state the root at `dir` keeps, and the states its
151
+ * `terraform_remote_state` blocks read. `rel` is the root's directory
152
+ * relative to the project, which a `local` path resolves against.
153
+ *
154
+ * Which backend and which configuration name the root's own state is
155
+ * state/backend.ts's {@link readBackend} verdict (chant#4478): the code with
156
+ * the `-backend-config` flags of `TF_CLI_ARGS`, `TF_CLI_ARGS_init`,
157
+ * `backendConfigFiles` and `backendConfig` over it, and what `init` recorded
158
+ * once it has run. A key neither gives is read from the code's own block,
159
+ * which still names nested ones (a `cloud` or `remote` backend's
160
+ * `workspaces`). An address the code does not spell in plain strings is left
161
+ * out, so nothing lines up with it.
162
+ */
163
+ export function rootState(dir: string, rel: string, input: ReadBackendInput = {}): RootState {
164
+ const out: RootState = { reads: [] };
165
+ // The code's own backend block, the first one as declaredBackend reads it: its type and attributes.
166
+ let code: { type: string; get: Attr } | undefined;
167
+ for (const { text, json } of rootTexts(dir, input.binary)) {
168
+ if (json) {
169
+ const doc = parseJson(text);
170
+ for (const t of jsonBlocks(doc, "terraform")) {
171
+ if (code) break;
172
+ if (t.cloud !== undefined) {
173
+ const b = firstOf(t.cloud);
174
+ code = { type: "cloud", get: (n) => literal(b?.[n] ?? firstOf(b?.workspaces)?.[n]) };
175
+ break;
176
+ }
177
+ const [type, v] = Object.entries(objectOf(t.backend) ?? {})[0] ?? [];
178
+ if (!type) continue;
179
+ const b = firstOf(v);
180
+ code = { type, get: (n) => literal(b?.[n] ?? firstOf(b?.workspaces)?.[n]) };
181
+ }
182
+ for (const d of jsonBlocks(doc, "data")) {
183
+ for (const [name, v] of Object.entries(objectOf(d.terraform_remote_state) ?? {})) {
184
+ const b = firstOf(v);
185
+ const config = objectOf(b?.config);
186
+ const type = literal(b?.backend);
187
+ if (!config || typeof type !== "string") continue;
188
+ const key = asKey(stateAddress(type, (n) => literal(config[n] ?? firstOf(config.workspaces)?.[n]), rel));
189
+ if (key) out.reads.push({ name, ...key, repeated: b?.count !== undefined || b?.for_each !== undefined });
190
+ }
191
+ }
192
+ continue;
193
+ }
194
+ if (!code) {
195
+ const m = text.match(/\bbackend\s+"([^"]+)"\s*\{/);
196
+ if (m) code = { type: m[1]!, get: hclAttr(blockBody(text, m.index!)) };
197
+ else {
198
+ const c = /^\s*cloud\s*\{/m.exec(text);
199
+ if (c) code = { type: "cloud", get: hclAttr(blockBody(text, c.index!)) };
200
+ }
201
+ }
202
+ for (const m of text.matchAll(/\bdata\s+"terraform_remote_state"\s+"([^"]+)"\s*\{/g)) {
203
+ const body = blockBody(text, m.index!);
204
+ const type = hclAttr(body)("backend");
205
+ if (typeof type !== "string") continue;
206
+ const config = /\bconfig\s*=\s*\{/.exec(body);
207
+ const get = config ? hclAttr(blockBody(body, config.index!)) : () => undefined;
208
+ const key = asKey(stateAddress(type, get, rel));
209
+ if (key) out.reads.push({ name: m[1]!, ...key, repeated: /^\s*(count|for_each)\s*=/m.test(body) });
210
+ }
211
+ }
212
+ const { backend } = readBackend(dir, input);
213
+ const fromCode = code?.type === backend.type ? code.get : (): undefined => undefined;
214
+ const get: Attr = (n) => configValue(backend.config[n] ?? objectOf(backend.config.workspaces)?.[n]) ?? fromCode(n);
215
+ out.own = asKey(stateAddress(backend.type, get, rel));
216
+ return out;
217
+ }
218
+
219
+ /** Two addresses name one state: the same key, and the same bucket where both name one. */
220
+ export const sameState = (a: StateKey, b: StateKey): boolean => a.key === b.key && (!a.bucket || !b.bucket || a.bucket === b.bucket);
221
+
222
+ const LIVE_SIDECAR = "estate.chdf.hcl";
223
+
224
+ /**
225
+ * A choudoufu root's estate, from its `estate.chdf.hcl` or the `live` block in
226
+ * its `terraform` block; undefined when it declares none. A `live` block takes
227
+ * effect only under choudoufu, so the files read are the ones choudoufu reads
228
+ * unless `binary` names another.
229
+ */
230
+ export function liveEstate(dir: string, binary = "choudoufu"): { live: boolean; estate?: string } {
231
+ const sidecar = join(dir, LIVE_SIDECAR);
232
+ if (existsSync(sidecar)) {
233
+ const estate = hclAttr(blankComments(readFileSync(sidecar, "utf-8")))("estate");
234
+ return { live: true, ...(typeof estate === "string" ? { estate } : {}) };
235
+ }
236
+ for (const { text, json } of rootTexts(dir, binary)) {
237
+ if (json) continue;
238
+ for (const t of text.matchAll(/^\s*terraform\s*\{/gm)) {
239
+ const body = blockBody(text, t.index!);
240
+ const live = body.match(/\blive\s*\{/);
241
+ if (!live) continue;
242
+ const estate = hclAttr(blockBody(body, live.index!))("estate");
243
+ return { live: true, ...(typeof estate === "string" ? { estate } : {}) };
244
+ }
245
+ }
246
+ return { live: false };
247
+ }
248
+
249
+ /** Each `data "terraform_estate_outputs"` block of a root: its label and the estate whose outputs it reads. */
250
+ export function estateOutputReads(dir: string, binary?: string): { name: string; estate: string }[] {
251
+ const out: { name: string; estate: string }[] = [];
252
+ for (const { text, json } of rootTexts(dir, binary)) {
253
+ if (json) continue;
254
+ for (const m of text.matchAll(/\bdata\s+"terraform_estate_outputs"\s+"([^"]+)"\s*\{/g)) {
255
+ const e = hclAttr(blockBody(text, m.index!))("estate");
256
+ if (typeof e === "string") out.push({ name: m[1]!, estate: e });
257
+ }
258
+ }
259
+ return out;
260
+ }
261
+
262
+ /** The provider versions a root's lock file pins, by provider address. */
263
+ export function lockedProviders(dir: string): Map<string, string> {
264
+ const out = new Map<string, string>();
265
+ let text = "";
266
+ try {
267
+ text = readFileSync(join(dir, ".terraform.lock.hcl"), "utf-8");
268
+ } catch {
269
+ return out;
270
+ }
271
+ for (const m of text.matchAll(/^provider\s+"([^"]+)"\s*\{/gm)) {
272
+ const v = blockBody(text, m.index!).match(/\bversion\s*=\s*"([^"]+)"/)?.[1];
273
+ if (v) out.set(m[1]!, v);
274
+ }
275
+ return out;
276
+ }
277
+
278
+ /** Local module directories the root at `dir` calls (`source = "./..."` or `"../..."`), followed through, absolute. */
279
+ export function localModules(dir: string, binary?: string): string[] {
280
+ const seen = new Set<string>();
281
+ const queue = [resolve(dir)];
282
+ while (queue.length > 0) {
283
+ const at = queue.pop()!;
284
+ for (const { text, json } of rootTexts(at, binary)) {
285
+ if (json) continue;
286
+ for (const m of text.matchAll(/\bmodule\s+"[^"]+"\s*\{/g)) {
287
+ const src = blockBody(text, m.index!).match(/\bsource\s*=\s*"(\.{1,2}\/[^"]*)"/)?.[1];
288
+ if (!src) continue;
289
+ const abs = resolve(at, src);
290
+ if (seen.has(abs) || abs === resolve(dir)) continue;
291
+ seen.add(abs);
292
+ queue.push(abs);
293
+ }
294
+ }
295
+ }
296
+ return [...seen].sort();
297
+ }
298
+
299
+ /** `abs` relative to `from`, with `/` between parts, `.` for `from` itself. */
300
+ export const relPath = (from: string, abs: string): string => relative(from, abs).split("\\").join("/") || ".";
301
+
302
+ /** An `import` block the composed root can take as it is: a literal string id, no for_each, no provider of the root's own. */
303
+ export function importLiteral(body: string): boolean {
304
+ if (/^\s*(for_each|provider)\s*=/m.test(body)) return false;
305
+ return /^\s*id\s*=\s*"(?:[^"\\$%]|\\.)*"\s*$/m.test(body);
306
+ }
307
+
308
+ /** Whether a JSON-syntax `import` block takes no literal string id (or has for_each or a provider). */
309
+ export function jsonImportRefused(text: string): boolean {
310
+ return jsonBlocks(parseJson(text), "import").some((b) => typeof b.id !== "string" || /\$\{|%\{/.test(b.id) || b.for_each !== undefined || b.provider !== undefined);
311
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Addresses in a composed root (chant#4442): each member a top-level
3
+ * `module.<m>` call, so a member's addresses are the composed root's with
4
+ * `module.<m>.` taken off. `chant terraform report` (../report.ts) and the
5
+ * composed run's slicer (./slice.ts) and state split (./state-merge.ts) all
6
+ * strip with these functions, so there is one implementation.
7
+ */
8
+
9
+ type Json = Record<string, unknown>;
10
+
11
+ const isObject = (v: unknown): v is Json => v !== null && typeof v === "object" && !Array.isArray(v);
12
+
13
+ const TOP_MODULE = /^module\.([^.[\]]+)(?:\[(?:"(?:[^"\\]|\\.)*"|\d+)\])?(?:\.|$)/;
14
+
15
+ /** The top-level module an address or module address is in, or undefined for the composed root's own. */
16
+ export function topModule(address: string): string | undefined {
17
+ return TOP_MODULE.exec(address)?.[1];
18
+ }
19
+
20
+ /** `address` with its leading `module.<m>` (and index) taken off. An address in no module is returned as it is. */
21
+ export function stripModule(address: string): string {
22
+ const m = TOP_MODULE.exec(address);
23
+ return m ? address.slice(m[0].length) : address;
24
+ }
25
+
26
+ /** Whether `address` is `module.<m>` or inside it. */
27
+ export const inModule = (address: string | undefined, m: string): boolean => address !== undefined && topModule(address) === m;
28
+
29
+ /**
30
+ * A `resource_changes` (or `resource_drift`) entry with its top module taken
31
+ * off `address`, `previous_address` and `module_address`. A module address
32
+ * that was the top module itself is dropped, as a root planned alone has none.
33
+ */
34
+ export function stripChange(rc: Json): Json {
35
+ const out: Json = { ...rc, address: stripModule(String(rc.address)) };
36
+ if (typeof rc.previous_address === "string") out.previous_address = stripModule(rc.previous_address);
37
+ if (typeof rc.module_address === "string") {
38
+ const rest = stripModule(rc.module_address);
39
+ if (rest === "" || rest === rc.module_address) delete out.module_address;
40
+ else out.module_address = rest;
41
+ }
42
+ return out;
43
+ }
44
+
45
+ /** The entries of a `resource_changes` list in module `m` (undefined: the composed root's own), each stripped. */
46
+ export function moduleChanges(list: unknown, m: string | undefined): Json[] {
47
+ const changes = Array.isArray(list) ? list : [];
48
+ return changes.filter((r): r is Json => isObject(r) && topModule(String(r.module_address ?? r.address ?? "")) === m).map(stripChange);
49
+ }