@intentius/chant-lexicon-terraform 0.124.31 → 0.124.32
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.
- package/dist/__fixtures__/terragrunt/affected-cases.js +111 -0
- package/dist/__fixtures__/terragrunt/record.js +57 -0
- package/dist/behaviour/index.js +6 -0
- package/dist/behaviour/kinds.js +194 -0
- package/dist/behaviour/predict.js +297 -0
- package/dist/behaviour/reference-catalog.js +178 -0
- package/dist/behaviour/request.js +482 -0
- package/dist/change-set.js +234 -0
- package/dist/changed-roots.js +97 -0
- package/dist/codegen/docs-cli.js +3 -0
- package/dist/codegen/docs.js +44 -0
- package/dist/codegen/generate-cli.js +9 -0
- package/dist/codegen/generate.js +59 -0
- package/dist/codegen/package.js +43 -0
- package/dist/commands.js +106 -0
- package/dist/components/capability-plugin.d.ts +38 -2
- package/dist/components/capability-plugin.d.ts.map +1 -1
- package/dist/components/capability-plugin.js +260 -0
- package/dist/composites/terraform-adopt-op.js +152 -0
- package/dist/composites/terraform-apply-op.js +183 -0
- package/dist/composites/terraform-pin-rollout-op.js +45 -0
- package/dist/composites/terraform-watch-op.js +219 -0
- package/dist/config.js +118 -0
- package/dist/describe-resources.js +984 -0
- package/dist/generated/index.js +1 -0
- package/dist/hcl/descend.js +208 -0
- package/dist/hcl/edges.js +287 -0
- package/dist/hcl/jsonencode.js +442 -0
- package/dist/hcl/marker.js +212 -0
- package/dist/hcl/parse.js +372 -0
- package/dist/hcl/references.js +156 -0
- package/dist/hcl/roots.js +98 -0
- package/dist/hcl/suppressions.js +189 -0
- package/dist/hcl/value.js +72 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +46 -0
- package/dist/integrity.json +2 -2
- package/dist/lint/audit-catalog.js +501 -0
- package/dist/lint/audit-lineage.js +442 -0
- package/dist/lint/post-synth/blocks.js +87 -0
- package/dist/lint/post-synth/fixtures/load.js +94 -0
- package/dist/lint/post-synth/flat-attributes.js +135 -0
- package/dist/lint/post-synth/index.js +90 -0
- package/dist/lint/post-synth/module-source.js +153 -0
- package/dist/lint/post-synth/scope.js +104 -0
- package/dist/lint/post-synth/terragrunt.js +63 -0
- package/dist/lint/post-synth/tf001.js +138 -0
- package/dist/lint/post-synth/tf002.js +143 -0
- package/dist/lint/post-synth/tf003.js +60 -0
- package/dist/lint/post-synth/tf004.js +54 -0
- package/dist/lint/post-synth/tf005.js +68 -0
- package/dist/lint/post-synth/tf006.js +43 -0
- package/dist/lint/post-synth/tf007.js +66 -0
- package/dist/lint/post-synth/tf008.js +64 -0
- package/dist/lint/post-synth/tf009.js +49 -0
- package/dist/lint/post-synth/tf010.js +40 -0
- package/dist/lint/post-synth/tf011.js +38 -0
- package/dist/lint/post-synth/tf012.js +36 -0
- package/dist/lint/post-synth/tf013.js +53 -0
- package/dist/lint/post-synth/tf014.js +61 -0
- package/dist/lint/post-synth/tf015.js +62 -0
- package/dist/lint/post-synth/tf016.js +66 -0
- package/dist/lint/post-synth/tf017.js +39 -0
- package/dist/lint/post-synth/tf018.js +63 -0
- package/dist/lint/post-synth/tf019.js +60 -0
- package/dist/lint/post-synth/tf020.js +103 -0
- package/dist/lint/post-synth/tf021.js +117 -0
- package/dist/lint/post-synth/tf022.js +51 -0
- package/dist/lint/post-synth/tf024.js +62 -0
- package/dist/lint/post-synth/tf025.js +83 -0
- package/dist/lint/post-synth/tf026.js +74 -0
- package/dist/lint/post-synth/tf027.js +66 -0
- package/dist/lint/post-synth/tf028.js +83 -0
- package/dist/lint/post-synth/tf029.js +73 -0
- package/dist/lint/post-synth/tf030.js +253 -0
- package/dist/lint/post-synth/tf031.js +332 -0
- package/dist/lint/post-synth/tf032.js +201 -0
- package/dist/lint/post-synth/tf033.js +126 -0
- package/dist/lint/post-synth/tf034.js +45 -0
- package/dist/lint/post-synth/tf035.js +63 -0
- package/dist/lint/post-synth/tf036.js +97 -0
- package/dist/lint/post-synth/tf037.js +80 -0
- package/dist/lint/post-synth/tf038.js +63 -0
- package/dist/lint/post-synth/tf039.js +55 -0
- package/dist/lint/post-synth/tf040.js +67 -0
- package/dist/lint/post-synth/tf041.js +53 -0
- package/dist/lint/post-synth/tf042.js +38 -0
- package/dist/lint/post-synth/tf043.js +45 -0
- package/dist/lint/post-synth/tf044.js +45 -0
- package/dist/lint/post-synth/tf045.js +44 -0
- package/dist/lint/rules/fixtures/load.js +50 -0
- package/dist/lint/rules/index.js +4 -0
- package/dist/lint/rules/plan-before-apply.js +136 -0
- package/dist/lint/secret-shape.js +149 -0
- package/dist/live-ownership.js +57 -0
- package/dist/lsp/completions.js +95 -0
- package/dist/lsp/context.js +358 -0
- package/dist/lsp/hover.js +45 -0
- package/dist/lsp/option-keys.js +93 -0
- package/dist/manifest.json +1 -1
- package/dist/op/activities/index.js +30 -0
- package/dist/op/activities/live-detect.js +143 -0
- package/dist/op/activities/pin-rollout.js +45 -0
- package/dist/op/activities/terraform.js +1270 -0
- package/dist/op/activity-contracts.js +162 -0
- package/dist/op/adoption.js +272 -0
- package/dist/op/builders.js +125 -0
- package/dist/op/resolve-root-mode.js +77 -0
- package/dist/package-cli.js +16 -0
- package/dist/pin/edit-ts.js +76 -0
- package/dist/pin/edit.js +118 -0
- package/dist/pin/forge.js +65 -0
- package/dist/pin/index.js +16 -0
- package/dist/pin/rollout.d.ts.map +1 -1
- package/dist/pin/rollout.js +382 -0
- package/dist/pin/scan.js +287 -0
- package/dist/pin/source.js +171 -0
- package/dist/pin/waves.js +109 -0
- package/dist/plan-digest.js +67 -0
- package/dist/plugin.js +323 -0
- package/dist/serializer.js +23 -0
- package/dist/state-ownership.js +29 -0
- package/dist/terragrunt/affected.d.ts.map +1 -1
- package/dist/terragrunt/affected.js +290 -0
- package/dist/terragrunt/index.js +10 -0
- package/dist/terragrunt/mocks.js +270 -0
- package/dist/terragrunt/run.js +264 -0
- package/dist/terragrunt/units.js +195 -0
- package/dist/terragrunt/wave.js +125 -0
- package/dist/validate-cli.js +4 -0
- package/dist/validate.js +31 -0
- package/package.json +10 -10
- package/src/codegen/package.ts +2 -2
- package/src/components/capability-plugin.test.ts +97 -2
- package/src/components/capability-plugin.ts +117 -12
- package/src/components/pr-loop.tofu.test.ts +51 -11
- package/src/index.ts +1 -0
- package/src/pin/rollout.ts +2 -1
- package/src/terragrunt/affected.test.ts +11 -0
- package/src/terragrunt/affected.ts +2 -1
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Activity contracts for this lexicon's own `op/activities` (chant #2101).
|
|
3
|
+
*
|
|
4
|
+
* The schema-shaped sibling of `./activities/index.ts`: that module is
|
|
5
|
+
* resolved by convention at `@intentius/chant-lexicon-terraform/op/activities`
|
|
6
|
+
* for the run-time registry, and this one at
|
|
7
|
+
* `@intentius/chant-lexicon-terraform/op/activity-contracts` for build-time
|
|
8
|
+
* validation. `loadActivityContracts`
|
|
9
|
+
* (`packages/core/src/op/activity-contract-registry.ts`) merges what it finds
|
|
10
|
+
* here into the map core's own OPS012 and OPS013 validate every `Chant::Op`
|
|
11
|
+
* step against. Since #2122 moved those checks into core they fire on every
|
|
12
|
+
* project that declares an Op, so an Op built out of terraform steps is
|
|
13
|
+
* checked wherever it is built. Before this existed, both examples that carry
|
|
14
|
+
* an Op had to keep it outside `src/` where `chant dev check-lexicon`'s
|
|
15
|
+
* example build could not see it.
|
|
16
|
+
*
|
|
17
|
+
* Each contract's `args` schema mirrors the corresponding `*Args` interface in
|
|
18
|
+
* `./activities/terraform.ts`, and each `returns` schema the corresponding
|
|
19
|
+
* `*Result`. Authored with `z.strictObject(...)`, never `z.object(...)`: the
|
|
20
|
+
* default form silently drops an unrecognized key instead of rejecting it, so
|
|
21
|
+
* a misspelled `planfile` would vanish rather than fail the build. The
|
|
22
|
+
* `returns` schemas are the half OPS013 needs — `plan.out.planFile` feeding an
|
|
23
|
+
* Apply step, `plan.out.text` feeding a `reconcilePr` body — so a field a step
|
|
24
|
+
* references has to be spelled here for the reference to validate.
|
|
25
|
+
*
|
|
26
|
+
* `json` is `z.unknown()` throughout: it is terraform's own `-json` document,
|
|
27
|
+
* whose shape is the provider's, not chant's. A dot-path reference into it
|
|
28
|
+
* therefore does not resolve, which is deliberate. Reference `text`, or one of
|
|
29
|
+
* the counts projected out of the document, instead of reaching into
|
|
30
|
+
* terraform's JSON from an Op.
|
|
31
|
+
*/
|
|
32
|
+
import { z } from "zod";
|
|
33
|
+
import { activityContract } from "@intentius/chant/op";
|
|
34
|
+
/** `root` and `cwd`, the two fields every activity in this lexicon takes ({@link TerraformRootArgs}). */
|
|
35
|
+
const rootArgs = { root: z.string(), cwd: z.string().optional() };
|
|
36
|
+
/**
|
|
37
|
+
* `adds`/`changes`/`destroys`, the counts projected out of a plan's
|
|
38
|
+
* `resource_changes` ({@link PlanChangeCounts}).
|
|
39
|
+
*/
|
|
40
|
+
const planChangeCounts = { adds: z.number(), changes: z.number(), destroys: z.number() };
|
|
41
|
+
/**
|
|
42
|
+
* One adoption candidate as `../adoption.ts` models it. Loose on purpose: an
|
|
43
|
+
* Op never reaches into a candidate's fields, it hands the whole array from a
|
|
44
|
+
* live-plan step to `choudoufuAdopt`, and a whole-value reference is validated
|
|
45
|
+
* without walking the shape.
|
|
46
|
+
*/
|
|
47
|
+
const adoptionCandidate = z.object({}).loose();
|
|
48
|
+
/**
|
|
49
|
+
* The `root` arg names the terraform root module the step acts on — the
|
|
50
|
+
* estate-side thing every activity here touches — so op.json's IR resolves it
|
|
51
|
+
* into the step's `entities` (#2022) and a renderer can join the step to the
|
|
52
|
+
* root instead of guessing off `cwd`, which is only where the config search
|
|
53
|
+
* starts.
|
|
54
|
+
*/
|
|
55
|
+
const rootEntity = { entities: ["root"] };
|
|
56
|
+
export const terraformInitContract = activityContract("terraformInit", z.strictObject({
|
|
57
|
+
...rootArgs,
|
|
58
|
+
upgrade: z.boolean().optional(),
|
|
59
|
+
reconfigure: z.boolean().optional(),
|
|
60
|
+
}), z.object({ dir: z.string(), workspace: z.string().optional() }), rootEntity);
|
|
61
|
+
export const terraformPlanContract = activityContract("terraformPlan", z.strictObject({
|
|
62
|
+
...rootArgs,
|
|
63
|
+
planFile: z.string().optional(),
|
|
64
|
+
destroy: z.boolean().optional(),
|
|
65
|
+
vars: z.record(z.string(), z.unknown()).optional(),
|
|
66
|
+
}), z.object({
|
|
67
|
+
...planChangeCounts,
|
|
68
|
+
changed: z.boolean(),
|
|
69
|
+
planFile: z.string(),
|
|
70
|
+
dir: z.string(),
|
|
71
|
+
json: z.unknown(),
|
|
72
|
+
text: z.string(),
|
|
73
|
+
planDigest: z.string(),
|
|
74
|
+
/** The root's change-set part (#3181); its shape is `@intentius/chant/change-set`'s `ChangeSetPart`. */
|
|
75
|
+
changeSet: z.object({ member: z.object({ member: z.string(), planDigest: z.string().nullable() }).passthrough(), entries: z.array(z.unknown()) }),
|
|
76
|
+
}), rootEntity);
|
|
77
|
+
export const terraformApplyContract = activityContract("terraformApply", z.strictObject({ ...rootArgs, planFile: z.string().optional() }), z.object({
|
|
78
|
+
planFile: z.string().optional(),
|
|
79
|
+
dir: z.string(),
|
|
80
|
+
applied: z.boolean(),
|
|
81
|
+
refused: z.enum(["approval-mismatch", "wrong-estate"]).optional(),
|
|
82
|
+
refusal: z.string().optional(),
|
|
83
|
+
}), rootEntity);
|
|
84
|
+
export const terraformShowContract = activityContract("terraformShow", z.strictObject({ ...rootArgs, planFile: z.string().optional() }), z.object({
|
|
85
|
+
...planChangeCounts,
|
|
86
|
+
source: z.enum(["plan", "state"]),
|
|
87
|
+
json: z.unknown(),
|
|
88
|
+
text: z.string(),
|
|
89
|
+
dir: z.string(),
|
|
90
|
+
planFile: z.string().optional(),
|
|
91
|
+
}), rootEntity);
|
|
92
|
+
export const choudoufuLivePlanContract = activityContract("choudoufuLivePlan", z.strictObject({
|
|
93
|
+
...rootArgs,
|
|
94
|
+
estate: z.string().optional(),
|
|
95
|
+
adoptionOnly: z.boolean().optional(),
|
|
96
|
+
}), z.object({
|
|
97
|
+
...planChangeCounts,
|
|
98
|
+
unowned: z.number(),
|
|
99
|
+
adoptable: z.number(),
|
|
100
|
+
drift: z.boolean(),
|
|
101
|
+
json: z.unknown(),
|
|
102
|
+
text: z.string(),
|
|
103
|
+
ledger: z.string(),
|
|
104
|
+
finding: z.string(),
|
|
105
|
+
adoptions: z.array(adoptionCandidate),
|
|
106
|
+
contested: z.array(adoptionCandidate),
|
|
107
|
+
ambiguous: z.number(),
|
|
108
|
+
dir: z.string(),
|
|
109
|
+
documentPath: z.string(),
|
|
110
|
+
estate: z.string(),
|
|
111
|
+
}), rootEntity);
|
|
112
|
+
export const choudoufuLiveLsContract = activityContract("choudoufuLiveLs", z.strictObject({
|
|
113
|
+
...rootArgs,
|
|
114
|
+
estate: z.string().optional(),
|
|
115
|
+
consistent: z.boolean().optional(),
|
|
116
|
+
}), z.object({ json: z.unknown(), dir: z.string(), estate: z.string() }), rootEntity);
|
|
117
|
+
export const choudoufuLiveCheckContract = activityContract("choudoufuLiveCheck", z.strictObject({ ...rootArgs }), z.object({ refused: z.boolean(), json: z.unknown(), text: z.string(), dir: z.string() }), rootEntity);
|
|
118
|
+
export const choudoufuAdoptContract = activityContract("choudoufuAdopt", z.strictObject({
|
|
119
|
+
...rootArgs,
|
|
120
|
+
adoptions: z.array(adoptionCandidate),
|
|
121
|
+
contested: z.array(adoptionCandidate).optional(),
|
|
122
|
+
}), z.object({
|
|
123
|
+
mechanism: z.literal("tag-write"),
|
|
124
|
+
adopted: z.array(z.string()),
|
|
125
|
+
adoptedCount: z.number(),
|
|
126
|
+
refused: z.array(z.object({ addr: z.string(), identity: z.string(), reason: z.string() })),
|
|
127
|
+
ambiguous: z.number(),
|
|
128
|
+
dir: z.string(),
|
|
129
|
+
}), rootEntity);
|
|
130
|
+
/** One root of a pin-bump rollout ({@link PinRoot} in `../pin/waves.ts`). */
|
|
131
|
+
const pinRoot = z.strictObject({ root: z.string(), dependsOn: z.array(z.string()).optional(), tsSource: z.string().optional() });
|
|
132
|
+
/**
|
|
133
|
+
* `terraformPinRollout` (#3189). It names no `terraform.roots` key: a rollout
|
|
134
|
+
* moves a module's pin across many roots, found by directory, so it carries no
|
|
135
|
+
* `rootEntity`. `status` and `summary` are the fields an Op reads: the run's
|
|
136
|
+
* outcome and a body for a report step.
|
|
137
|
+
*/
|
|
138
|
+
export const terraformPinRolloutContract = activityContract("terraformPinRollout", z.strictObject({
|
|
139
|
+
module: z.string(),
|
|
140
|
+
from: z.string(),
|
|
141
|
+
to: z.string(),
|
|
142
|
+
roots: z.array(pinRoot).optional(),
|
|
143
|
+
canaries: z.array(z.string()).optional(),
|
|
144
|
+
wavesFrom: z.string().optional(),
|
|
145
|
+
wavesPrefix: z.string().optional(),
|
|
146
|
+
mode: z.enum(["report", "pull-request"]).optional(),
|
|
147
|
+
base: z.string().optional(),
|
|
148
|
+
remote: z.string().optional(),
|
|
149
|
+
appliedCheck: z.string().optional(),
|
|
150
|
+
cwd: z.string().optional(),
|
|
151
|
+
}), z.object({
|
|
152
|
+
module: z.string(),
|
|
153
|
+
from: z.string(),
|
|
154
|
+
to: z.string(),
|
|
155
|
+
mode: z.enum(["report", "pull-request"]),
|
|
156
|
+
base: z.string(),
|
|
157
|
+
status: z.enum(["complete", "opened", "would-open", "waiting", "stopped"]),
|
|
158
|
+
roots: z.array(z.object({}).loose()),
|
|
159
|
+
waves: z.array(z.object({}).loose()),
|
|
160
|
+
stop: z.string().optional(),
|
|
161
|
+
summary: z.string(),
|
|
162
|
+
}));
|
|
@@ -0,0 +1,272 @@
|
|
|
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, #2241).
|
|
4
|
+
*
|
|
5
|
+
* Pure. No filesystem, no child process, no HCL parse — everything here is a
|
|
6
|
+
* projection of two sections of choudoufu's document, which between them state
|
|
7
|
+
* both halves of the answer: the live resource an adoption would claim, and
|
|
8
|
+
* the `tofu-estate`/`tofu-address` pair that claims it. Two tags is the whole
|
|
9
|
+
* ownership contract (choudoufu's `live/MARKERS.md`), so those two values are
|
|
10
|
+
* the whole of what an adoption needs to know.
|
|
11
|
+
*
|
|
12
|
+
* ## The two sections, and why there are two
|
|
13
|
+
*
|
|
14
|
+
* `unowned[]` (choudoufu issue #788) is a live resource read at an identity
|
|
15
|
+
* the configuration itself declares: a log group's name is in the block, so an
|
|
16
|
+
* unmarked live one with that name is found by reading it.
|
|
17
|
+
*
|
|
18
|
+
* `adoptable[]` (choudoufu issue #962, shipped in v0.15.0) is a live resource
|
|
19
|
+
* the estate-wide sweep matched to a declared instance by content, for a
|
|
20
|
+
* declaration that carries no identity at all: EC2 assigns a VPC's id, so a
|
|
21
|
+
* declared `aws_vpc` lands in `omissions[]` as `NEEDS_DISCOVERY` and the live
|
|
22
|
+
* VPC standing at its `cidr_block` is found by comparing arguments. Each row
|
|
23
|
+
* carries `matched[]`, the arguments that agreed, and `adopt_command`, the
|
|
24
|
+
* paste-ready tagging command, so nothing on this path parses the human
|
|
25
|
+
* render any more. Before v0.15.0 the document had no row for that match at
|
|
26
|
+
* all and chant read the two regexes {@link parseAdoptionCommands} still
|
|
27
|
+
* holds, which is what chant #2168 measured and filed.
|
|
28
|
+
*
|
|
29
|
+
* The two sections are disjoint by construction, so this reads both and keys
|
|
30
|
+
* the union by declared address.
|
|
31
|
+
*
|
|
32
|
+
* ## What the empty ledger means
|
|
33
|
+
*
|
|
34
|
+
* `adoptable[]` and `swept[]` are populated only on a run that asked the
|
|
35
|
+
* estate-wide sweep the account-bounded question (`-adoption-only`, or
|
|
36
|
+
* `TOFU_LIVE_COLLECT_UNCLAIMED=1` on a `-json` run). {@link AdoptionLedger}
|
|
37
|
+
* carries `swept` for exactly that reason: an empty `adoptions` under an empty
|
|
38
|
+
* `swept` is "this run did not look", not "there is nothing to adopt", and
|
|
39
|
+
* {@link renderAdoptionLedger} says which.
|
|
40
|
+
*
|
|
41
|
+
* ## Why chant renders a ledger at all
|
|
42
|
+
*
|
|
43
|
+
* choudoufu prints its own, under `live-plan -adoption-only`, and it is the
|
|
44
|
+
* better report for a human at a terminal: it counts the marker-carrying and
|
|
45
|
+
* the identity-by-declaration halves of the declared population separately and
|
|
46
|
+
* says why each unadoptable instance is unadoptable. But `-adoption-only` and
|
|
47
|
+
* `-json` are refused together (`internal/command/live_plan.go`: "this run
|
|
48
|
+
* cannot produce both reports at once"), and an Op that reports adoptables
|
|
49
|
+
* needs the machine-readable document anyway, for the counts it publishes as
|
|
50
|
+
* outcome attributes and for the addresses an adoption step acts on. So the
|
|
51
|
+
* ledger below is rendered from the document that run already has, in the row
|
|
52
|
+
* form `-adoption-only` prints, rather than paying for a third live read.
|
|
53
|
+
*
|
|
54
|
+
* ## Ambiguity
|
|
55
|
+
*
|
|
56
|
+
* Both sections are one entry per live resource, keyed by the declared
|
|
57
|
+
* instance the resource was matched to, so two live resources at one
|
|
58
|
+
* declaration are two entries carrying the same `addr`. That is the ambiguous
|
|
59
|
+
* case: no single tag write claims the address, and picking one of the two is
|
|
60
|
+
* a decision about the estate rather than something a tool infers.
|
|
61
|
+
* {@link readAdoptionLedger} separates those into `contested` and never lets
|
|
62
|
+
* them into `adoptions`, and `TerraformAdoptOp` passes the contested list to
|
|
63
|
+
* its Adopt step so the Op's result names what it refused as well as what it
|
|
64
|
+
* wrote.
|
|
65
|
+
*/
|
|
66
|
+
const str = (v) => (typeof v === "string" ? v : "");
|
|
67
|
+
/** `matched[]` as choudoufu writes it, dropping anything that is not a pair of strings. */
|
|
68
|
+
function readMatched(raw) {
|
|
69
|
+
if (!Array.isArray(raw))
|
|
70
|
+
return [];
|
|
71
|
+
const out = [];
|
|
72
|
+
for (const item of raw) {
|
|
73
|
+
const m = (item ?? {});
|
|
74
|
+
const attribute = str(m.attribute);
|
|
75
|
+
if (attribute)
|
|
76
|
+
out.push({ attribute, value: str(m.value) });
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
/** `swept[]` as choudoufu writes it: resource type names, and nothing else. */
|
|
81
|
+
function readSwept(document) {
|
|
82
|
+
const swept = document?.swept;
|
|
83
|
+
return Array.isArray(swept) ? swept.filter((t) => typeof t === "string") : [];
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Project a `live-plan -json` document's `unowned` and `adoptable` sections
|
|
87
|
+
* into the adoptable and the contested sets.
|
|
88
|
+
*
|
|
89
|
+
* An entry counts as a candidate only when choudoufu offered both marker
|
|
90
|
+
* values. Both empty means adoption was not this run's to offer: the resource
|
|
91
|
+
* belongs to another estate (`tofu_estate` names it), or the run had no estate
|
|
92
|
+
* name of its own, and such an entry is neither adoptable nor contested here,
|
|
93
|
+
* because there is no tag write to refuse.
|
|
94
|
+
*
|
|
95
|
+
* `commands` maps a declared address to the paste-ready tagging command
|
|
96
|
+
* choudoufu printed for it, from {@link parseAdoptionCommands}. An
|
|
97
|
+
* `adoptable[]` row carries its own command in the document and never needs
|
|
98
|
+
* the map; omit the map entirely when the run had no `-adoption-only` render
|
|
99
|
+
* to read one out of.
|
|
100
|
+
*/
|
|
101
|
+
export function readAdoptionLedger(document, commands) {
|
|
102
|
+
const doc = (document ?? {});
|
|
103
|
+
const swept = readSwept(document);
|
|
104
|
+
const byAddr = new Map();
|
|
105
|
+
const add = (candidate) => {
|
|
106
|
+
const at = byAddr.get(candidate.addr);
|
|
107
|
+
if (at)
|
|
108
|
+
at.push(candidate);
|
|
109
|
+
else
|
|
110
|
+
byAddr.set(candidate.addr, [candidate]);
|
|
111
|
+
};
|
|
112
|
+
for (const raw of Array.isArray(doc.unowned) ? doc.unowned : []) {
|
|
113
|
+
const e = (raw ?? {});
|
|
114
|
+
const markerEstate = str(e.adopt_tofu_estate);
|
|
115
|
+
const markerAddress = str(e.adopt_tofu_address);
|
|
116
|
+
if (!markerEstate && !markerAddress)
|
|
117
|
+
continue;
|
|
118
|
+
const addr = str(e.addr);
|
|
119
|
+
const command = commands?.get(addr);
|
|
120
|
+
add({
|
|
121
|
+
addr,
|
|
122
|
+
type: str(e.type),
|
|
123
|
+
identity: str(e.identity),
|
|
124
|
+
markerEstate,
|
|
125
|
+
markerAddress,
|
|
126
|
+
...(command ? { command } : {}),
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
for (const raw of Array.isArray(doc.adoptable) ? doc.adoptable : []) {
|
|
130
|
+
const e = (raw ?? {});
|
|
131
|
+
const markerEstate = str(e.adopt_tofu_estate);
|
|
132
|
+
const markerAddress = str(e.adopt_tofu_address);
|
|
133
|
+
if (!markerEstate && !markerAddress)
|
|
134
|
+
continue;
|
|
135
|
+
const addr = str(e.addr);
|
|
136
|
+
const command = str(e.adopt_command) || commands?.get(addr);
|
|
137
|
+
const matched = readMatched(e.matched);
|
|
138
|
+
add({
|
|
139
|
+
addr,
|
|
140
|
+
type: str(e.type),
|
|
141
|
+
identity: str(e.identity),
|
|
142
|
+
markerEstate,
|
|
143
|
+
markerAddress,
|
|
144
|
+
...(command ? { command } : {}),
|
|
145
|
+
...(matched.length > 0 ? { matched } : {}),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const adoptions = [];
|
|
149
|
+
const contested = [];
|
|
150
|
+
let ambiguous = 0;
|
|
151
|
+
for (const candidates of byAddr.values()) {
|
|
152
|
+
if (candidates.length === 1) {
|
|
153
|
+
adoptions.push(candidates[0]);
|
|
154
|
+
}
|
|
155
|
+
else {
|
|
156
|
+
ambiguous++;
|
|
157
|
+
contested.push(...candidates);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return { adoptions, contested, ambiguous, swept };
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Pair each declared address in a `live-plan -adoption-only` render with the
|
|
164
|
+
* tagging command choudoufu printed under it.
|
|
165
|
+
*
|
|
166
|
+
* The render's "Adoptable now" rows are ` <addr> <- <type> <identity>`
|
|
167
|
+
* followed by indented detail lines, one of which is ` adopt with: <cmd>`
|
|
168
|
+
* (`internal/command/views/live_adoption.go`). choudoufu builds that command
|
|
169
|
+
* to be pasted verbatim: every interpolated value is shell-quoted, the tagging
|
|
170
|
+
* verb comes from its own botocore-derived table rather than a guess, and the
|
|
171
|
+
* provider configuration's region and endpoint ride along as `--region` and
|
|
172
|
+
* `--endpoint-url`, so the write lands where the resource is rather than
|
|
173
|
+
* wherever an operator's AWS CLI profile points. Re-deriving any of that from
|
|
174
|
+
* the two marker values alone would be a worse command, so this reads the one
|
|
175
|
+
* choudoufu already wrote.
|
|
176
|
+
*
|
|
177
|
+
* An address the render offered no command for is simply absent from the map.
|
|
178
|
+
*
|
|
179
|
+
* ## Which caller still needs this (#2241)
|
|
180
|
+
*
|
|
181
|
+
* One: the `unowned[]` half of {@link readAdoptionLedger}. choudoufu's
|
|
182
|
+
* `views.StatelessUnowned` has no command field, so a live resource found at
|
|
183
|
+
* an identity the configuration declares still gets its paste-ready command
|
|
184
|
+
* from the human render and from nowhere else. The `adoptable[]` half no
|
|
185
|
+
* longer reads a line of text: those rows carry `adopt_command` in the
|
|
186
|
+
* document itself since v0.15.0 (choudoufu #962), which is what made the
|
|
187
|
+
* adopt path stop resting on two regexes over a render nobody promised to
|
|
188
|
+
* keep stable.
|
|
189
|
+
*/
|
|
190
|
+
export function parseAdoptionCommands(ledgerText) {
|
|
191
|
+
const commands = new Map();
|
|
192
|
+
let current;
|
|
193
|
+
for (const line of ledgerText.split("\n")) {
|
|
194
|
+
const row = /^ {2}(\S+) <- \S+ /.exec(line);
|
|
195
|
+
if (row) {
|
|
196
|
+
current = row[1];
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
const cmd = /^ {6}(?:or )?adopt with: (.+)$/.exec(line);
|
|
200
|
+
if (cmd && current) {
|
|
201
|
+
commands.set(current, cmd[1].trim());
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
// A blank line ends a row's detail block; a non-indented line ends the
|
|
205
|
+
// section. Either way the next "adopt with:" belongs to nobody until the
|
|
206
|
+
// next row line names an address.
|
|
207
|
+
if (line.trim() === "" || !line.startsWith(" "))
|
|
208
|
+
current = undefined;
|
|
209
|
+
}
|
|
210
|
+
return commands;
|
|
211
|
+
}
|
|
212
|
+
const plural = (n, one, many) => (n === 1 ? one : many);
|
|
213
|
+
/**
|
|
214
|
+
* Render {@link readAdoptionLedger}'s answer as the text an issue or PR body
|
|
215
|
+
* carries under the plan.
|
|
216
|
+
*
|
|
217
|
+
* One line per adoptable match, naming the declared address, the live resource
|
|
218
|
+
* it binds, and the two tag values that adopt it, in the token forms
|
|
219
|
+
* `-adoption-only` prints them in (`<addr> <- <type> <identity>`,
|
|
220
|
+
* `tofu-estate=`, `tofu-address=`). A content match adds the arguments it
|
|
221
|
+
* rested on under its row, in the same `matched on:` form the human render
|
|
222
|
+
* uses, because "these two agreed on this cidr" is the whole evidence for a
|
|
223
|
+
* match nobody read an identity for. Contested addresses follow, listed and
|
|
224
|
+
* not offered.
|
|
225
|
+
*
|
|
226
|
+
* An empty ledger still renders a line: "nothing adoptable" is a result a
|
|
227
|
+
* reader wants, and a section that vanishes reads as one that was never
|
|
228
|
+
* computed. Which of the two empties it is comes from `swept`: a run that
|
|
229
|
+
* asked no estate-wide sweep found nothing because it did not look, and
|
|
230
|
+
* saying so is the difference between a report and a silence.
|
|
231
|
+
*/
|
|
232
|
+
export function renderAdoptionLedger(ledger, estate) {
|
|
233
|
+
const where = estate ? `, estate ${JSON.stringify(estate)}` : "";
|
|
234
|
+
const lines = [];
|
|
235
|
+
if (ledger.adoptions.length === 0) {
|
|
236
|
+
lines.push(`Adoptable now: nothing${where}`, "");
|
|
237
|
+
lines.push(ledger.swept.length === 0
|
|
238
|
+
? "No estate-wide sweep ran on this plan, so nothing here says whether an unmarked live " +
|
|
239
|
+
"resource exists for any declared instance. This is silence, not a finding: ask the " +
|
|
240
|
+
"account-bounded question with an adoption run."
|
|
241
|
+
: "No live resource this run could claim was found, across " +
|
|
242
|
+
`${ledger.swept.length} swept resource ${plural(ledger.swept.length, "type", "types")} ` +
|
|
243
|
+
`(${ledger.swept.join(", ")}). Anything the plan proposes creating, it proposes ` +
|
|
244
|
+
"creating for real.");
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
lines.push(`Adoptable now: ${ledger.adoptions.length} live ${plural(ledger.adoptions.length, "resource", "resources")}${where}`, "");
|
|
248
|
+
lines.push("Each line is a live resource this estate could claim, found either at a declared resource's " +
|
|
249
|
+
"own identity or by matching a declaration's arguments, and carrying no marker for this " +
|
|
250
|
+
"estate. Writing the two tags shown adopts it; nothing is bound until they are written, " +
|
|
251
|
+
"because ownership is the tofu-estate and tofu-address pair and nothing else.", "");
|
|
252
|
+
for (const c of ledger.adoptions) {
|
|
253
|
+
lines.push(` ${c.addr} <- ${c.type} ${c.identity} write: ` +
|
|
254
|
+
`tofu-estate=${c.markerEstate} tofu-address=${c.markerAddress}`);
|
|
255
|
+
if (c.matched?.length) {
|
|
256
|
+
lines.push(` matched on: ${c.matched.map((m) => `${m.attribute}=${m.value}`).join(", ")}`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (ledger.contested.length > 0) {
|
|
261
|
+
lines.push("");
|
|
262
|
+
lines.push(`Ambiguous: ${ledger.ambiguous} declared ${plural(ledger.ambiguous, "address", "addresses")} ` +
|
|
263
|
+
"with more than one candidate", "");
|
|
264
|
+
lines.push("More than one live resource sits at each of these declared identities, so no single tag write " +
|
|
265
|
+
"claims the address. None of them is adopted. Resolve it by deleting the duplicate or by " +
|
|
266
|
+
"writing the marker onto the one you mean.", "");
|
|
267
|
+
for (const c of ledger.contested) {
|
|
268
|
+
lines.push(` ${c.addr} <- ${c.type} ${c.identity}`);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return lines.join("\n");
|
|
272
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed step-builder wrappers for this lexicon's activities, copied from
|
|
3
|
+
* `lexicons/k3s/src/op/builders.ts` (chant #1288 Stage 2). `opts`'s type in
|
|
4
|
+
* each wrapper below IS the activity's own `*Args` interface, via
|
|
5
|
+
* `Omit`/`WithStepRefs`, never restated: rename a field on the activity and
|
|
6
|
+
* the builder's callers stop compiling.
|
|
7
|
+
*
|
|
8
|
+
* The `root` key is positional in every wrapper, because naming the root is
|
|
9
|
+
* the one thing a terraform step cannot be authored without.
|
|
10
|
+
*
|
|
11
|
+
* `id` routes to the step's `id` field rather than into `args`. That matters
|
|
12
|
+
* here more than anywhere else in the lexicon: `.out` throws without an id
|
|
13
|
+
* (`packages/core/src/op/builders.ts`), and `.out` is how an Apply step names
|
|
14
|
+
* the Plan step's `planFile` as a `StepOutputRef` instead of guessing the
|
|
15
|
+
* path a second time.
|
|
16
|
+
*/
|
|
17
|
+
import { activity, takeProfileAndId, } from "@intentius/chant/op";
|
|
18
|
+
/**
|
|
19
|
+
* `terraform init` in the named root — the fully typed twin of the
|
|
20
|
+
* `terraformInit` activity. `opts` is {@link TerraformInitArgs} itself, minus
|
|
21
|
+
* the positional `root`. Defaults to the `longInfra` profile: init downloads
|
|
22
|
+
* providers.
|
|
23
|
+
*/
|
|
24
|
+
export const terraformInit = (root, opts) => {
|
|
25
|
+
const { args, profile, id } = takeProfileAndId(opts);
|
|
26
|
+
return activity("terraformInit", { root, ...args }, { profile: profile ?? "longInfra", ...(id ? { id } : {}) });
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* `terraform plan` in the named root, writing a saved plan. `opts` is
|
|
30
|
+
* {@link TerraformPlanArgs} itself, minus the positional `root`. Defaults to
|
|
31
|
+
* the `longInfra` profile: plan refreshes every resource against its provider.
|
|
32
|
+
*
|
|
33
|
+
* Give this step an `id` when a later step applies its plan — `plan.out.planFile`
|
|
34
|
+
* is the reference an Apply step takes.
|
|
35
|
+
*/
|
|
36
|
+
export const terraformPlan = (root, opts) => {
|
|
37
|
+
const { args, profile, id } = takeProfileAndId(opts);
|
|
38
|
+
return activity("terraformPlan", { root, ...args }, { profile: profile ?? "longInfra", ...(id ? { id } : {}) });
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* `terraform apply <planFile>` in the named root, the fully typed twin of the
|
|
42
|
+
* `terraformApply` activity. `opts` is {@link TerraformApplyArgs} itself,
|
|
43
|
+
* minus the positional `root`. `planFile` is optional in the type and
|
|
44
|
+
* required in practice, on a live root as much as a stock one: the activity
|
|
45
|
+
* refuses a missing one, and on a live root the file is the approval artifact
|
|
46
|
+
* choudoufu v0.13.0 re-plans against and refuses on a mismatch (choudoufu
|
|
47
|
+
* #878). Defaults to the `longInfra` profile.
|
|
48
|
+
*/
|
|
49
|
+
export const terraformApply = (root, opts) => {
|
|
50
|
+
const { args, profile, id } = takeProfileAndId(opts);
|
|
51
|
+
return activity("terraformApply", { root, ...args }, { profile: profile ?? "longInfra", ...(id ? { id } : {}) });
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* `choudoufu live-plan -detailed-exitcode -json` in the named root, plus the
|
|
55
|
+
* human render. The fully typed twin of the `choudoufuLivePlan` activity
|
|
56
|
+
* (#2103). `opts` is {@link ChoudoufuLivePlanArgs} itself, minus the
|
|
57
|
+
* positional `root`; `estate` is optional, auto-detected from the root's
|
|
58
|
+
* `live` block or `estate.chdf.hcl` sidecar when omitted, and a root that
|
|
59
|
+
* declares one is run with no `-estate` at all, which is the only form
|
|
60
|
+
* choudoufu admits there (choudoufu #894, fixed in v0.14.0).
|
|
61
|
+
* Defaults to the `longInfra` profile: like `terraformPlan`, this reads the
|
|
62
|
+
* live system in full (the estate-wide sweep).
|
|
63
|
+
*
|
|
64
|
+
* Give this step an `id` to read `.out.drift`, `.out.unowned`,
|
|
65
|
+
* `.out.adoptable` or `.out.documentPath` from a later step.
|
|
66
|
+
*/
|
|
67
|
+
export const choudoufuLivePlan = (root, opts) => {
|
|
68
|
+
const { args, profile, id } = takeProfileAndId(opts);
|
|
69
|
+
return activity("choudoufuLivePlan", { root, ...args }, { profile: profile ?? "longInfra", ...(id ? { id } : {}) });
|
|
70
|
+
};
|
|
71
|
+
/**
|
|
72
|
+
* `choudoufu live-ls -estate=<estate> -json [-consistent]` in the named root,
|
|
73
|
+
* the fully typed twin of the `choudoufuLiveLs` activity (#2103). `opts` is
|
|
74
|
+
* {@link ChoudoufuLiveLsArgs} itself, minus the positional `root`. Defaults to
|
|
75
|
+
* the `fastIdempotent` profile: `live-ls` reads a tagging-API listing and
|
|
76
|
+
* calls no other provider.
|
|
77
|
+
*/
|
|
78
|
+
export const choudoufuLiveLs = (root, opts) => {
|
|
79
|
+
const { args, profile, id } = takeProfileAndId(opts);
|
|
80
|
+
return activity("choudoufuLiveLs", { root, ...args }, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* `choudoufu live-check -json` in the named root, the fully typed twin of the
|
|
84
|
+
* `choudoufuLiveCheck` activity (#2103). `opts` is {@link ChoudoufuLiveCheckArgs}
|
|
85
|
+
* itself, minus the positional `root`. Defaults to the `fastIdempotent`
|
|
86
|
+
* profile: no cloud calls, no state.
|
|
87
|
+
*/
|
|
88
|
+
export const choudoufuLiveCheck = (root, opts) => {
|
|
89
|
+
const { args, profile, id } = takeProfileAndId(opts);
|
|
90
|
+
return activity("choudoufuLiveCheck", { root, ...args }, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
|
|
91
|
+
};
|
|
92
|
+
/**
|
|
93
|
+
* Write the ownership markers that claim an adoption ledger's matches, in the
|
|
94
|
+
* named root. The fully typed twin of the `choudoufuAdopt` activity (#2105).
|
|
95
|
+
* `opts` is {@link ChoudoufuAdoptArgs} itself, minus the positional `root`, so
|
|
96
|
+
* `adoptions` is required at the call site — an adopt step with nothing to
|
|
97
|
+
* adopt is an authoring mistake, not a no-op worth defaulting to. Defaults to
|
|
98
|
+
* the `longInfra` profile: one tagging round trip per resource, against the
|
|
99
|
+
* cloud.
|
|
100
|
+
*/
|
|
101
|
+
export const choudoufuAdopt = (root, opts) => {
|
|
102
|
+
const { args, profile, id } = takeProfileAndId(opts);
|
|
103
|
+
return activity("choudoufuAdopt", { root, ...args }, { profile: profile ?? "longInfra", ...(id ? { id } : {}) });
|
|
104
|
+
};
|
|
105
|
+
/**
|
|
106
|
+
* `terraform show` in the named root, over state or over a saved plan. `opts`
|
|
107
|
+
* is {@link TerraformShowArgs} itself, minus the positional `root`. Defaults
|
|
108
|
+
* to the `fastIdempotent` profile: show reads an artifact and calls no
|
|
109
|
+
* provider.
|
|
110
|
+
*/
|
|
111
|
+
export const terraformShow = (root, opts) => {
|
|
112
|
+
const { args, profile, id } = takeProfileAndId(opts);
|
|
113
|
+
return activity("terraformShow", { root, ...args }, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
|
|
114
|
+
};
|
|
115
|
+
/**
|
|
116
|
+
* One run of a pin-bump rollout (#3189). `opts` is
|
|
117
|
+
* {@link TerraformPinRolloutArgs} itself. It names no root: the rollout finds
|
|
118
|
+
* the roots that call the module. Defaults to the `fastIdempotent` profile: a
|
|
119
|
+
* repeated run reads the forge again and finds the PR an earlier attempt
|
|
120
|
+
* opened.
|
|
121
|
+
*/
|
|
122
|
+
export const terraformPinRollout = (opts) => {
|
|
123
|
+
const { args, profile, id } = takeProfileAndId(opts);
|
|
124
|
+
return activity("terraformPinRollout", args, { profile: profile ?? "fastIdempotent", ...(id ? { id } : {}) });
|
|
125
|
+
};
|
|
@@ -0,0 +1,77 @@
|
|
|
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, either a `live { }` block or an `estate.chdf.hcl` sidecar (#2103).
|
|
5
|
+
*
|
|
6
|
+
* Three callers decide something at build time, before any activity runs, and
|
|
7
|
+
* none 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`) reads the
|
|
12
|
+
* mode to word the Gate phase's approval description, and to refuse a
|
|
13
|
+
* root whose `policy` block sets `undeclared_untagged = "delete"`. The
|
|
14
|
+
* mode no longer picks a phase shape: #2157 made the live and stock Apply
|
|
15
|
+
* steps one step, so both roots build the same phases.
|
|
16
|
+
* - `TerraformWatchOp` (`../composites/terraform-watch-op.ts`) cross-checks
|
|
17
|
+
* its hand-set `live` flag against the root's real mode and refuses a
|
|
18
|
+
* mismatch (#2216).
|
|
19
|
+
* - TF101 (`../lint/rules/plan-before-apply.ts`) decides whether a
|
|
20
|
+
* `terraformApply` call it is statically inspecting is exempt from the
|
|
21
|
+
* plan-file pairing it otherwise enforces.
|
|
22
|
+
*
|
|
23
|
+
* All three run in the build process (`chant build`/`chant lint`) rather than
|
|
24
|
+
* in an activity at run time, so a synchronous read is available, but only the
|
|
25
|
+
* cheap half of it: `findProjectConfig` (`@intentius/chant/project-root`) is a
|
|
26
|
+
* plain, already-synchronous upward filesystem walk, but a `chant.config.ts`
|
|
27
|
+
* is project-authored code, and evaluating it synchronously outside chant's
|
|
28
|
+
* own config-sandbox machinery is more than any caller's best-effort question
|
|
29
|
+
* is worth. So this reads `chant.config.json` only; a project on a `.ts`
|
|
30
|
+
* config, or with no config discoverable at all, or naming no matching root,
|
|
31
|
+
* resolves to `undefined`, and "unknown" reads as "stock" to every caller.
|
|
32
|
+
*
|
|
33
|
+
* That default is conservative for the two callers that only ever add a
|
|
34
|
+
* requirement by knowing more: TF101 keeps enforcing the plan-file pairing,
|
|
35
|
+
* and the watch Op keeps accepting the flag it was given. It is the
|
|
36
|
+
* permissive direction for the apply Op's policy refusal and for the watch
|
|
37
|
+
* Op's mismatch, which is why neither refusal is left to this function alone:
|
|
38
|
+
* TF027 and TF028 (`../lint/post-synth/`) ask the same questions of the
|
|
39
|
+
* parsed HCL the build already stamped `mode` onto, whatever the config file
|
|
40
|
+
* is written in, and those checks are the guarantee (#2216). Every project in
|
|
41
|
+
* this repository is on a `chant.config.ts`, so a refusal that resolves the
|
|
42
|
+
* mode through this function alone never fires for anyone.
|
|
43
|
+
*/
|
|
44
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
45
|
+
import { resolve } from "node:path";
|
|
46
|
+
import { findProjectConfig } from "@intentius/chant/project-root";
|
|
47
|
+
import { detectLiveEstate } from "./activities/live-detect.js";
|
|
48
|
+
/**
|
|
49
|
+
* Resolve `rootName`'s mode against the nearest `chant.config.json` found by
|
|
50
|
+
* walking up from `cwd` (default `process.cwd()`, matching every other
|
|
51
|
+
* `cwd`-defaulting option in this lexicon). `undefined` when that cannot be
|
|
52
|
+
* done with a plain, synchronous read: no config found, the config found is
|
|
53
|
+
* `chant.config.ts`, the config declares no such root, or the root's
|
|
54
|
+
* directory does not exist.
|
|
55
|
+
*/
|
|
56
|
+
export function resolveRootModeSync(rootName, cwd) {
|
|
57
|
+
const { dir: projectRoot, configPath } = findProjectConfig(resolve(cwd ?? process.cwd()));
|
|
58
|
+
if (!configPath || !configPath.endsWith(".json"))
|
|
59
|
+
return undefined;
|
|
60
|
+
let raw;
|
|
61
|
+
try {
|
|
62
|
+
raw = JSON.parse(readFileSync(configPath, "utf-8"));
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
const terraform = raw?.terraform;
|
|
68
|
+
const root = terraform?.roots?.[rootName];
|
|
69
|
+
if (!terraform || !root || typeof root.dir !== "string")
|
|
70
|
+
return undefined;
|
|
71
|
+
const dir = resolve(projectRoot, root.dir);
|
|
72
|
+
if (terraform.binary !== "choudoufu")
|
|
73
|
+
return { mode: "state", dir };
|
|
74
|
+
if (!existsSync(dir))
|
|
75
|
+
return undefined;
|
|
76
|
+
return { mode: detectLiveEstate(dir) !== undefined ? "live" : "state", dir };
|
|
77
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
#!/usr/bin/env tsx
|
|
2
|
+
import { generate, writeGeneratedFiles } from "./codegen/generate.js";
|
|
3
|
+
import { packageLexicon } from "./codegen/package.js";
|
|
4
|
+
import { writeBundleSpec } from "@intentius/chant/codegen/package";
|
|
5
|
+
import { join, dirname } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
const srcDir = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
// 1. Generate src/generated/ files (writeGeneratedFiles resolves its own target)
|
|
9
|
+
const genResult = await generate({ verbose: true });
|
|
10
|
+
writeGeneratedFiles(genResult);
|
|
11
|
+
// 2. Run package pipeline and write dist/
|
|
12
|
+
const { spec, stats } = await packageLexicon({ verbose: true });
|
|
13
|
+
const distDir = join(dirname(srcDir), "dist");
|
|
14
|
+
writeBundleSpec(spec, distDir);
|
|
15
|
+
console.error(`Packaged ${stats.resources} resources, ${stats.ruleCount} rules, ${stats.skillCount} skills`);
|
|
16
|
+
console.error(`dist/ written to ${distDir}`);
|