@aetherwing/fcp-terraform 0.1.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.
- package/LICENSE +21 -0
- package/README.md +252 -0
- package/dist/adapter.d.ts +14 -0
- package/dist/adapter.js +266 -0
- package/dist/hcl.d.ts +5 -0
- package/dist/hcl.js +176 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +14 -0
- package/dist/model.d.ts +38 -0
- package/dist/model.js +233 -0
- package/dist/ops.d.ts +4 -0
- package/dist/ops.js +346 -0
- package/dist/queries.d.ts +4 -0
- package/dist/queries.js +250 -0
- package/dist/types.d.ts +115 -0
- package/dist/types.js +1 -0
- package/dist/verbs.d.ts +3 -0
- package/dist/verbs.js +40 -0
- package/package.json +46 -0
package/dist/hcl.js
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Serialize a TerraformConfig to valid HCL.
|
|
3
|
+
*/
|
|
4
|
+
export function serializeToHcl(config) {
|
|
5
|
+
const sections = [];
|
|
6
|
+
// Group blocks by kind in Terraform-idiomatic order
|
|
7
|
+
const providers = getBlocksByKind(config, "provider");
|
|
8
|
+
const variables = getBlocksByKind(config, "variable");
|
|
9
|
+
const data = getBlocksByKind(config, "data");
|
|
10
|
+
const resources = getBlocksByKind(config, "resource");
|
|
11
|
+
const modules = getBlocksByKind(config, "module");
|
|
12
|
+
const outputs = getBlocksByKind(config, "output");
|
|
13
|
+
for (const block of providers)
|
|
14
|
+
sections.push(serializeProvider(block));
|
|
15
|
+
for (const block of variables)
|
|
16
|
+
sections.push(serializeVariable(block));
|
|
17
|
+
for (const block of data)
|
|
18
|
+
sections.push(serializeData(block));
|
|
19
|
+
for (const block of modules)
|
|
20
|
+
sections.push(serializeModule(block));
|
|
21
|
+
for (const block of resources)
|
|
22
|
+
sections.push(serializeResource(block, config));
|
|
23
|
+
for (const block of outputs)
|
|
24
|
+
sections.push(serializeOutput(block));
|
|
25
|
+
return sections.join("\n\n") + "\n";
|
|
26
|
+
}
|
|
27
|
+
function getBlocksByKind(config, kind) {
|
|
28
|
+
return config.blockOrder
|
|
29
|
+
.map((id) => config.blocks.get(id))
|
|
30
|
+
.filter((b) => b && b.kind === kind);
|
|
31
|
+
}
|
|
32
|
+
function serializeProvider(block) {
|
|
33
|
+
const lines = [];
|
|
34
|
+
lines.push(`provider "${block.fullType}" {`);
|
|
35
|
+
for (const attr of block.attributes.values()) {
|
|
36
|
+
lines.push(` ${formatAttribute(attr)}`);
|
|
37
|
+
}
|
|
38
|
+
lines.push("}");
|
|
39
|
+
return lines.join("\n");
|
|
40
|
+
}
|
|
41
|
+
function serializeVariable(block) {
|
|
42
|
+
const lines = [];
|
|
43
|
+
lines.push(`variable "${block.label}" {`);
|
|
44
|
+
for (const attr of block.attributes.values()) {
|
|
45
|
+
if (attr.key === "type") {
|
|
46
|
+
// type is unquoted in variables
|
|
47
|
+
lines.push(` type = ${attr.value}`);
|
|
48
|
+
}
|
|
49
|
+
else {
|
|
50
|
+
lines.push(` ${formatAttribute(attr)}`);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
lines.push("}");
|
|
54
|
+
return lines.join("\n");
|
|
55
|
+
}
|
|
56
|
+
function serializeData(block) {
|
|
57
|
+
const lines = [];
|
|
58
|
+
lines.push(`data "${block.fullType}" "${block.label}" {`);
|
|
59
|
+
for (const attr of block.attributes.values()) {
|
|
60
|
+
lines.push(` ${formatAttribute(attr)}`);
|
|
61
|
+
}
|
|
62
|
+
for (const nested of block.nestedBlocks) {
|
|
63
|
+
lines.push(...serializeNestedBlock(nested, 2));
|
|
64
|
+
}
|
|
65
|
+
lines.push("}");
|
|
66
|
+
return lines.join("\n");
|
|
67
|
+
}
|
|
68
|
+
function serializeModule(block) {
|
|
69
|
+
const lines = [];
|
|
70
|
+
lines.push(`module "${block.label}" {`);
|
|
71
|
+
for (const attr of block.attributes.values()) {
|
|
72
|
+
lines.push(` ${formatAttribute(attr)}`);
|
|
73
|
+
}
|
|
74
|
+
lines.push("}");
|
|
75
|
+
return lines.join("\n");
|
|
76
|
+
}
|
|
77
|
+
function serializeResource(block, config) {
|
|
78
|
+
const lines = [];
|
|
79
|
+
lines.push(`resource "${block.fullType}" "${block.label}" {`);
|
|
80
|
+
// Attributes
|
|
81
|
+
for (const attr of block.attributes.values()) {
|
|
82
|
+
lines.push(` ${formatAttribute(attr)}`);
|
|
83
|
+
}
|
|
84
|
+
// Meta: count
|
|
85
|
+
if (block.meta.count) {
|
|
86
|
+
lines.push(` count = ${block.meta.count}`);
|
|
87
|
+
}
|
|
88
|
+
if (block.meta.forEach) {
|
|
89
|
+
lines.push(` for_each = ${block.meta.forEach}`);
|
|
90
|
+
}
|
|
91
|
+
// Tags
|
|
92
|
+
if (block.tags.size > 0) {
|
|
93
|
+
lines.push("");
|
|
94
|
+
lines.push(" tags = {");
|
|
95
|
+
for (const [k, v] of block.tags) {
|
|
96
|
+
lines.push(` ${k} = "${v}"`);
|
|
97
|
+
}
|
|
98
|
+
lines.push(" }");
|
|
99
|
+
}
|
|
100
|
+
// Nested blocks
|
|
101
|
+
for (const nested of block.nestedBlocks) {
|
|
102
|
+
lines.push("");
|
|
103
|
+
lines.push(...serializeNestedBlock(nested, 2));
|
|
104
|
+
}
|
|
105
|
+
// depends_on from connections
|
|
106
|
+
const deps = getDependsOn(block, config);
|
|
107
|
+
if (deps.length > 0) {
|
|
108
|
+
lines.push("");
|
|
109
|
+
lines.push(` depends_on = [${deps.join(", ")}]`);
|
|
110
|
+
}
|
|
111
|
+
// Explicit depends_on from meta
|
|
112
|
+
if (block.meta.dependsOn.length > 0) {
|
|
113
|
+
const existing = deps.length > 0;
|
|
114
|
+
if (!existing) {
|
|
115
|
+
lines.push("");
|
|
116
|
+
lines.push(` depends_on = [${block.meta.dependsOn.join(", ")}]`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
lines.push("}");
|
|
120
|
+
return lines.join("\n");
|
|
121
|
+
}
|
|
122
|
+
function serializeOutput(block) {
|
|
123
|
+
const lines = [];
|
|
124
|
+
lines.push(`output "${block.label}" {`);
|
|
125
|
+
for (const attr of block.attributes.values()) {
|
|
126
|
+
if (attr.key === "value" && (attr.valueType === "reference" || attr.valueType === "expression")) {
|
|
127
|
+
lines.push(` value = ${attr.value}`);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
lines.push(` ${formatAttribute(attr)}`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
lines.push("}");
|
|
134
|
+
return lines.join("\n");
|
|
135
|
+
}
|
|
136
|
+
function serializeNestedBlock(nested, indent) {
|
|
137
|
+
const pad = " ".repeat(indent);
|
|
138
|
+
const lines = [];
|
|
139
|
+
lines.push(`${pad}${nested.type} {`);
|
|
140
|
+
for (const attr of nested.attributes.values()) {
|
|
141
|
+
lines.push(`${pad} ${formatAttribute(attr)}`);
|
|
142
|
+
}
|
|
143
|
+
lines.push(`${pad}}`);
|
|
144
|
+
return lines;
|
|
145
|
+
}
|
|
146
|
+
function formatAttribute(attr) {
|
|
147
|
+
const padded = attr.key.padEnd(Math.max(attr.key.length, 4));
|
|
148
|
+
switch (attr.valueType) {
|
|
149
|
+
case "string":
|
|
150
|
+
return `${padded} = "${attr.value}"`;
|
|
151
|
+
case "number":
|
|
152
|
+
case "bool":
|
|
153
|
+
case "reference":
|
|
154
|
+
return `${padded} = ${attr.value}`;
|
|
155
|
+
case "expression":
|
|
156
|
+
return `${padded} = ${attr.value}`;
|
|
157
|
+
case "list":
|
|
158
|
+
return `${padded} = ${attr.value}`;
|
|
159
|
+
case "map":
|
|
160
|
+
return `${padded} = jsonencode(${attr.value})`;
|
|
161
|
+
default:
|
|
162
|
+
return `${padded} = "${attr.value}"`;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
function getDependsOn(block, config) {
|
|
166
|
+
const deps = [];
|
|
167
|
+
for (const conn of config.connections.values()) {
|
|
168
|
+
if (conn.sourceId === block.id) {
|
|
169
|
+
const target = config.blocks.get(conn.targetId);
|
|
170
|
+
if (target && (target.kind === "resource" || target.kind === "data")) {
|
|
171
|
+
deps.push(`${target.fullType}.${target.label}`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return deps;
|
|
176
|
+
}
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { createFcpServer } from "@aetherwing/fcp-core";
|
|
2
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
3
|
+
import { TerraformAdapter } from "./adapter.js";
|
|
4
|
+
import { VERB_SPECS, REFERENCE_CARD_SECTIONS } from "./verbs.js";
|
|
5
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
6
|
+
export const server = createFcpServer({
|
|
7
|
+
domain: "terraform",
|
|
8
|
+
adapter: new TerraformAdapter(),
|
|
9
|
+
verbs: VERB_SPECS,
|
|
10
|
+
referenceCard: { sections: REFERENCE_CARD_SECTIONS },
|
|
11
|
+
});
|
|
12
|
+
const transport = new StdioServerTransport();
|
|
13
|
+
await server.connect(transport);
|
|
14
|
+
export { TerraformAdapter } from "./adapter.js";
|
package/dist/model.d.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import type { TerraformConfig, TfBlock, Connection, Attribute } from "./types.js";
|
|
2
|
+
export declare function generateId(): string;
|
|
3
|
+
/**
|
|
4
|
+
* Derive provider name from a Terraform resource type.
|
|
5
|
+
* "aws_s3_bucket" → "aws", "google_compute_instance" → "google", "azurerm_resource_group" → "azurerm"
|
|
6
|
+
*/
|
|
7
|
+
export declare function deriveProvider(fullType: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* Create a new empty TerraformConfig.
|
|
10
|
+
*/
|
|
11
|
+
export declare function createEmptyConfig(title: string): TerraformConfig;
|
|
12
|
+
export declare function rebuildLabelIndex(config: TerraformConfig): void;
|
|
13
|
+
/**
|
|
14
|
+
* Find a block by label. Returns the block if the label is unambiguous
|
|
15
|
+
* (only one block has that label). Returns undefined if no match or ambiguous.
|
|
16
|
+
*/
|
|
17
|
+
export declare function findByLabel(config: TerraformConfig, label: string): TfBlock | undefined;
|
|
18
|
+
/**
|
|
19
|
+
* Find a block by qualified label: "fullType.label" (e.g., "aws_vpc.main").
|
|
20
|
+
*/
|
|
21
|
+
export declare function findByQualifiedLabel(config: TerraformConfig, input: string): TfBlock | undefined;
|
|
22
|
+
export declare function findByType(config: TerraformConfig, fullType: string): TfBlock[];
|
|
23
|
+
export declare function findByKind(config: TerraformConfig, kind: string): TfBlock[];
|
|
24
|
+
export declare function findByProvider(config: TerraformConfig, provider: string): TfBlock[];
|
|
25
|
+
export declare function findConnections(config: TerraformConfig, blockId: string): Connection[];
|
|
26
|
+
export declare function addBlock(config: TerraformConfig, block: TfBlock): string | null;
|
|
27
|
+
export declare function removeBlock(config: TerraformConfig, id: string): TfBlock | null;
|
|
28
|
+
export declare function addConnection(config: TerraformConfig, conn: Connection): void;
|
|
29
|
+
export declare function removeConnection(config: TerraformConfig, id: string): Connection | null;
|
|
30
|
+
/**
|
|
31
|
+
* Create an Attribute from a string value, inferring the type.
|
|
32
|
+
* When forceString is true, skip number/bool detection (user explicitly quoted the value).
|
|
33
|
+
*/
|
|
34
|
+
export declare function makeAttribute(key: string, value: string, forceString?: boolean): Attribute;
|
|
35
|
+
/**
|
|
36
|
+
* Create a TfBlock from components.
|
|
37
|
+
*/
|
|
38
|
+
export declare function createBlock(kind: TfBlock["kind"], fullType: string, label: string, attrs: Record<string, string>, quotedParams?: Set<string>): TfBlock;
|
package/dist/model.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
let idCounter = 0;
|
|
2
|
+
export function generateId() {
|
|
3
|
+
return `tf_${(++idCounter).toString(36).padStart(4, "0")}_${Math.random().toString(36).slice(2, 6)}`;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Derive provider name from a Terraform resource type.
|
|
7
|
+
* "aws_s3_bucket" → "aws", "google_compute_instance" → "google", "azurerm_resource_group" → "azurerm"
|
|
8
|
+
*/
|
|
9
|
+
export function deriveProvider(fullType) {
|
|
10
|
+
const idx = fullType.indexOf("_");
|
|
11
|
+
return idx > 0 ? fullType.slice(0, idx) : fullType;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Create a new empty TerraformConfig.
|
|
15
|
+
*/
|
|
16
|
+
export function createEmptyConfig(title) {
|
|
17
|
+
return {
|
|
18
|
+
id: generateId(),
|
|
19
|
+
title,
|
|
20
|
+
filePath: null,
|
|
21
|
+
blocks: new Map(),
|
|
22
|
+
connections: new Map(),
|
|
23
|
+
blockOrder: [],
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
// ── Label index for O(1) lookup ─────────────────────────
|
|
27
|
+
const labelIndex = new Map(); // label → block IDs
|
|
28
|
+
export function rebuildLabelIndex(config) {
|
|
29
|
+
labelIndex.clear();
|
|
30
|
+
for (const [id, block] of config.blocks) {
|
|
31
|
+
const key = block.label.toLowerCase();
|
|
32
|
+
const ids = labelIndex.get(key);
|
|
33
|
+
if (ids) {
|
|
34
|
+
ids.push(id);
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
labelIndex.set(key, [id]);
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Find a block by label. Returns the block if the label is unambiguous
|
|
43
|
+
* (only one block has that label). Returns undefined if no match or ambiguous.
|
|
44
|
+
*/
|
|
45
|
+
export function findByLabel(config, label) {
|
|
46
|
+
// Try qualified label first (e.g., "aws_vpc.main")
|
|
47
|
+
const qualified = findByQualifiedLabel(config, label);
|
|
48
|
+
if (qualified)
|
|
49
|
+
return qualified;
|
|
50
|
+
const ids = labelIndex.get(label.toLowerCase());
|
|
51
|
+
if (!ids || ids.length === 0)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (ids.length === 1)
|
|
54
|
+
return config.blocks.get(ids[0]);
|
|
55
|
+
// Ambiguous — multiple blocks share this label
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Find a block by qualified label: "fullType.label" (e.g., "aws_vpc.main").
|
|
60
|
+
*/
|
|
61
|
+
export function findByQualifiedLabel(config, input) {
|
|
62
|
+
const dotIdx = input.indexOf(".");
|
|
63
|
+
if (dotIdx <= 0)
|
|
64
|
+
return undefined;
|
|
65
|
+
const typePart = input.slice(0, dotIdx);
|
|
66
|
+
const labelPart = input.slice(dotIdx + 1).toLowerCase();
|
|
67
|
+
const ids = labelIndex.get(labelPart);
|
|
68
|
+
if (!ids)
|
|
69
|
+
return undefined;
|
|
70
|
+
for (const id of ids) {
|
|
71
|
+
const block = config.blocks.get(id);
|
|
72
|
+
if (block && block.fullType === typePart)
|
|
73
|
+
return block;
|
|
74
|
+
}
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
export function findByType(config, fullType) {
|
|
78
|
+
const results = [];
|
|
79
|
+
for (const block of config.blocks.values()) {
|
|
80
|
+
if (block.fullType === fullType)
|
|
81
|
+
results.push(block);
|
|
82
|
+
}
|
|
83
|
+
return results;
|
|
84
|
+
}
|
|
85
|
+
export function findByKind(config, kind) {
|
|
86
|
+
const results = [];
|
|
87
|
+
for (const block of config.blocks.values()) {
|
|
88
|
+
if (block.kind === kind)
|
|
89
|
+
results.push(block);
|
|
90
|
+
}
|
|
91
|
+
return results;
|
|
92
|
+
}
|
|
93
|
+
export function findByProvider(config, provider) {
|
|
94
|
+
const results = [];
|
|
95
|
+
for (const block of config.blocks.values()) {
|
|
96
|
+
if (block.provider === provider)
|
|
97
|
+
results.push(block);
|
|
98
|
+
}
|
|
99
|
+
return results;
|
|
100
|
+
}
|
|
101
|
+
export function findConnections(config, blockId) {
|
|
102
|
+
const results = [];
|
|
103
|
+
for (const conn of config.connections.values()) {
|
|
104
|
+
if (conn.sourceId === blockId || conn.targetId === blockId)
|
|
105
|
+
results.push(conn);
|
|
106
|
+
}
|
|
107
|
+
return results;
|
|
108
|
+
}
|
|
109
|
+
export function addBlock(config, block) {
|
|
110
|
+
// Check uniqueness: reject only if same fullType AND same label
|
|
111
|
+
for (const existing of config.blocks.values()) {
|
|
112
|
+
if (existing.fullType === block.fullType && existing.label.toLowerCase() === block.label.toLowerCase()) {
|
|
113
|
+
return `${block.fullType} "${block.label}" already exists`;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
config.blocks.set(block.id, block);
|
|
117
|
+
config.blockOrder.push(block.id);
|
|
118
|
+
const key = block.label.toLowerCase();
|
|
119
|
+
const ids = labelIndex.get(key);
|
|
120
|
+
if (ids) {
|
|
121
|
+
ids.push(block.id);
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
labelIndex.set(key, [block.id]);
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
export function removeBlock(config, id) {
|
|
129
|
+
const block = config.blocks.get(id);
|
|
130
|
+
if (!block)
|
|
131
|
+
return null;
|
|
132
|
+
config.blocks.delete(id);
|
|
133
|
+
config.blockOrder = config.blockOrder.filter((bid) => bid !== id);
|
|
134
|
+
const key = block.label.toLowerCase();
|
|
135
|
+
const ids = labelIndex.get(key);
|
|
136
|
+
if (ids) {
|
|
137
|
+
const filtered = ids.filter((bid) => bid !== id);
|
|
138
|
+
if (filtered.length === 0) {
|
|
139
|
+
labelIndex.delete(key);
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
labelIndex.set(key, filtered);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
// Remove connections involving this block
|
|
146
|
+
for (const [connId, conn] of config.connections) {
|
|
147
|
+
if (conn.sourceId === id || conn.targetId === id) {
|
|
148
|
+
config.connections.delete(connId);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
return block;
|
|
152
|
+
}
|
|
153
|
+
export function addConnection(config, conn) {
|
|
154
|
+
config.connections.set(conn.id, conn);
|
|
155
|
+
}
|
|
156
|
+
export function removeConnection(config, id) {
|
|
157
|
+
const conn = config.connections.get(id);
|
|
158
|
+
if (!conn)
|
|
159
|
+
return null;
|
|
160
|
+
config.connections.delete(id);
|
|
161
|
+
return conn;
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Create an Attribute from a string value, inferring the type.
|
|
165
|
+
* When forceString is true, skip number/bool detection (user explicitly quoted the value).
|
|
166
|
+
*/
|
|
167
|
+
export function makeAttribute(key, value, forceString) {
|
|
168
|
+
if (!forceString) {
|
|
169
|
+
if (value === "true" || value === "false") {
|
|
170
|
+
return { key, value, valueType: "bool" };
|
|
171
|
+
}
|
|
172
|
+
if (/^\d+(\.\d+)?$/.test(value)) {
|
|
173
|
+
return { key, value, valueType: "number" };
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
if (value.startsWith("[") && value.endsWith("]")) {
|
|
177
|
+
// Parse list elements and quote non-reference/non-numeric/non-bool values
|
|
178
|
+
const inner = value.slice(1, -1).trim();
|
|
179
|
+
if (inner === "") {
|
|
180
|
+
return { key, value: "[]", valueType: "list" };
|
|
181
|
+
}
|
|
182
|
+
const elements = inner.split(",").map((e) => e.trim());
|
|
183
|
+
const quoted = elements.map((elem) => {
|
|
184
|
+
// References: aws_*, var.*, local.*, data.*, module.*, etc.
|
|
185
|
+
if (/^(aws_|google_|azurerm_|var\.|local\.|data\.|module\.)/.test(elem))
|
|
186
|
+
return elem;
|
|
187
|
+
// Numbers
|
|
188
|
+
if (/^\d+(\.\d+)?$/.test(elem))
|
|
189
|
+
return elem;
|
|
190
|
+
// Bools
|
|
191
|
+
if (elem === "true" || elem === "false")
|
|
192
|
+
return elem;
|
|
193
|
+
// Already quoted
|
|
194
|
+
if (elem.startsWith('"') && elem.endsWith('"'))
|
|
195
|
+
return elem;
|
|
196
|
+
// Everything else: quote it
|
|
197
|
+
return `"${elem}"`;
|
|
198
|
+
});
|
|
199
|
+
return { key, value: `[${quoted.join(", ")}]`, valueType: "list" };
|
|
200
|
+
}
|
|
201
|
+
if (value.startsWith("{")) {
|
|
202
|
+
// JSON object (contains ":") → use map type for jsonencode()
|
|
203
|
+
if (value.includes(":")) {
|
|
204
|
+
return { key, value, valueType: "map" };
|
|
205
|
+
}
|
|
206
|
+
return { key, value, valueType: "expression" };
|
|
207
|
+
}
|
|
208
|
+
// Check for Terraform references: aws_xxx.name.attr or var.xxx
|
|
209
|
+
if (/^(aws_|google_|azurerm_|var\.|local\.|data\.|module\.)/.test(value)) {
|
|
210
|
+
return { key, value, valueType: "reference" };
|
|
211
|
+
}
|
|
212
|
+
return { key, value, valueType: "string" };
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* Create a TfBlock from components.
|
|
216
|
+
*/
|
|
217
|
+
export function createBlock(kind, fullType, label, attrs, quotedParams) {
|
|
218
|
+
const attributes = new Map();
|
|
219
|
+
for (const [k, v] of Object.entries(attrs)) {
|
|
220
|
+
attributes.set(k, makeAttribute(k, v, quotedParams?.has(k)));
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
id: generateId(),
|
|
224
|
+
kind,
|
|
225
|
+
label,
|
|
226
|
+
fullType,
|
|
227
|
+
provider: deriveProvider(fullType),
|
|
228
|
+
attributes,
|
|
229
|
+
nestedBlocks: [],
|
|
230
|
+
tags: new Map(),
|
|
231
|
+
meta: { dependsOn: [] },
|
|
232
|
+
};
|
|
233
|
+
}
|
package/dist/ops.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { ParsedOp, OpResult } from "@aetherwing/fcp-core";
|
|
2
|
+
import type { EventLog } from "@aetherwing/fcp-core";
|
|
3
|
+
import type { TerraformConfig, TerraformEvent } from "./types.js";
|
|
4
|
+
export declare function dispatchOp(op: ParsedOp, config: TerraformConfig, log: EventLog<TerraformEvent>): OpResult;
|