@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/ops.js
ADDED
|
@@ -0,0 +1,346 @@
|
|
|
1
|
+
import { findByLabel, addBlock, removeBlock, addConnection, removeConnection, createBlock, generateId, makeAttribute, rebuildLabelIndex, } from "./model.js";
|
|
2
|
+
export function dispatchOp(op, config, log) {
|
|
3
|
+
const handler = HANDLERS[op.verb];
|
|
4
|
+
if (!handler) {
|
|
5
|
+
return { success: false, message: `unhandled verb "${op.verb}"` };
|
|
6
|
+
}
|
|
7
|
+
return handler(op, config, log);
|
|
8
|
+
}
|
|
9
|
+
// ── Verb handlers ──────────────────────────────────────────
|
|
10
|
+
function handleAdd(op, config, log) {
|
|
11
|
+
const subKind = op.positionals[0]?.toLowerCase();
|
|
12
|
+
switch (subKind) {
|
|
13
|
+
case "resource": {
|
|
14
|
+
const fullType = op.positionals[1];
|
|
15
|
+
const label = op.positionals[2];
|
|
16
|
+
if (!fullType || !label) {
|
|
17
|
+
return { success: false, message: "add resource requires TYPE and LABEL" };
|
|
18
|
+
}
|
|
19
|
+
const block = createBlock("resource", fullType, label, op.params, op.quotedParams);
|
|
20
|
+
const err = addBlock(config, block);
|
|
21
|
+
if (err)
|
|
22
|
+
return { success: false, message: err };
|
|
23
|
+
log.append({ type: "block_added", block: structuredClone(block) });
|
|
24
|
+
return { success: true, message: `resource ${fullType}.${label}`, prefix: "+" };
|
|
25
|
+
}
|
|
26
|
+
case "provider": {
|
|
27
|
+
const name = op.positionals[1];
|
|
28
|
+
if (!name)
|
|
29
|
+
return { success: false, message: "add provider requires PROVIDER name" };
|
|
30
|
+
const block = createBlock("provider", name, name, op.params, op.quotedParams);
|
|
31
|
+
block.provider = name;
|
|
32
|
+
const err = addBlock(config, block);
|
|
33
|
+
if (err)
|
|
34
|
+
return { success: false, message: err };
|
|
35
|
+
log.append({ type: "block_added", block: structuredClone(block) });
|
|
36
|
+
return { success: true, message: `provider "${name}"`, prefix: "+" };
|
|
37
|
+
}
|
|
38
|
+
case "variable": {
|
|
39
|
+
const label = op.positionals[1];
|
|
40
|
+
if (!label)
|
|
41
|
+
return { success: false, message: "add variable requires NAME" };
|
|
42
|
+
const block = createBlock("variable", "variable", label, op.params, op.quotedParams);
|
|
43
|
+
block.provider = "";
|
|
44
|
+
const err = addBlock(config, block);
|
|
45
|
+
if (err)
|
|
46
|
+
return { success: false, message: err };
|
|
47
|
+
log.append({ type: "block_added", block: structuredClone(block) });
|
|
48
|
+
return { success: true, message: `variable "${label}"`, prefix: "+" };
|
|
49
|
+
}
|
|
50
|
+
case "output": {
|
|
51
|
+
const label = op.positionals[1];
|
|
52
|
+
if (!label)
|
|
53
|
+
return { success: false, message: "add output requires NAME" };
|
|
54
|
+
const block = createBlock("output", "output", label, op.params, op.quotedParams);
|
|
55
|
+
block.provider = "";
|
|
56
|
+
const err = addBlock(config, block);
|
|
57
|
+
if (err)
|
|
58
|
+
return { success: false, message: err };
|
|
59
|
+
log.append({ type: "block_added", block: structuredClone(block) });
|
|
60
|
+
return { success: true, message: `output "${label}"`, prefix: "+" };
|
|
61
|
+
}
|
|
62
|
+
case "data": {
|
|
63
|
+
const fullType = op.positionals[1];
|
|
64
|
+
const label = op.positionals[2];
|
|
65
|
+
if (!fullType || !label) {
|
|
66
|
+
return { success: false, message: "add data requires TYPE and LABEL" };
|
|
67
|
+
}
|
|
68
|
+
const block = createBlock("data", fullType, label, op.params, op.quotedParams);
|
|
69
|
+
block.kind = "data";
|
|
70
|
+
const err = addBlock(config, block);
|
|
71
|
+
if (err)
|
|
72
|
+
return { success: false, message: err };
|
|
73
|
+
log.append({ type: "block_added", block: structuredClone(block) });
|
|
74
|
+
return { success: true, message: `data ${fullType}.${label}`, prefix: "+" };
|
|
75
|
+
}
|
|
76
|
+
case "module": {
|
|
77
|
+
const label = op.positionals[1];
|
|
78
|
+
if (!label)
|
|
79
|
+
return { success: false, message: "add module requires LABEL" };
|
|
80
|
+
const block = createBlock("module", "module", label, op.params, op.quotedParams);
|
|
81
|
+
block.provider = "";
|
|
82
|
+
block.kind = "module";
|
|
83
|
+
const err = addBlock(config, block);
|
|
84
|
+
if (err)
|
|
85
|
+
return { success: false, message: err };
|
|
86
|
+
log.append({ type: "block_added", block: structuredClone(block) });
|
|
87
|
+
return { success: true, message: `module "${label}"`, prefix: "+" };
|
|
88
|
+
}
|
|
89
|
+
default:
|
|
90
|
+
return { success: false, message: `unknown add type "${subKind}". Use: resource, provider, variable, output, data, module` };
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
function handleSet(op, config, log) {
|
|
94
|
+
const label = op.positionals[0];
|
|
95
|
+
if (!label)
|
|
96
|
+
return { success: false, message: "set requires a block LABEL" };
|
|
97
|
+
const block = findByLabel(config, label);
|
|
98
|
+
if (!block)
|
|
99
|
+
return { success: false, message: `block "${label}" not found` };
|
|
100
|
+
const keys = Object.keys(op.params);
|
|
101
|
+
if (keys.length === 0)
|
|
102
|
+
return { success: false, message: "set requires at least one key:value" };
|
|
103
|
+
for (const [k, v] of Object.entries(op.params)) {
|
|
104
|
+
const before = block.attributes.get(k) ?? null;
|
|
105
|
+
const after = makeAttribute(k, v, op.quotedParams?.has(k));
|
|
106
|
+
block.attributes.set(k, after);
|
|
107
|
+
log.append({ type: "attribute_set", blockId: block.id, key: k, before: before ? structuredClone(before) : null, after: structuredClone(after) });
|
|
108
|
+
}
|
|
109
|
+
return { success: true, message: `${label}: set ${keys.join(", ")}`, prefix: "*" };
|
|
110
|
+
}
|
|
111
|
+
function handleRemove(op, config, log) {
|
|
112
|
+
// Selector-based removal
|
|
113
|
+
if (op.selectors.length > 0) {
|
|
114
|
+
const resolved = resolveSelectors(op.selectors, config);
|
|
115
|
+
if (resolved.length === 0)
|
|
116
|
+
return { success: false, message: "no blocks match selector" };
|
|
117
|
+
for (const block of resolved) {
|
|
118
|
+
removeBlock(config, block.id);
|
|
119
|
+
log.append({ type: "block_removed", block: structuredClone(block) });
|
|
120
|
+
}
|
|
121
|
+
return { success: true, message: `removed ${resolved.length} block(s)`, prefix: "@" };
|
|
122
|
+
}
|
|
123
|
+
// Label-based removal
|
|
124
|
+
const label = op.positionals[0];
|
|
125
|
+
if (!label)
|
|
126
|
+
return { success: false, message: "remove requires LABEL or @selector" };
|
|
127
|
+
const block = findByLabel(config, label);
|
|
128
|
+
if (!block)
|
|
129
|
+
return { success: false, message: `block "${label}" not found` };
|
|
130
|
+
removeBlock(config, block.id);
|
|
131
|
+
log.append({ type: "block_removed", block: structuredClone(block) });
|
|
132
|
+
return { success: true, message: `${block.kind} "${label}"`, prefix: "-" };
|
|
133
|
+
}
|
|
134
|
+
function handleConnect(op, config, log) {
|
|
135
|
+
// Expect: positionals = ["source", "->", "target"]
|
|
136
|
+
const arrowIdx = op.positionals.indexOf("->");
|
|
137
|
+
if (arrowIdx < 0)
|
|
138
|
+
return { success: false, message: "connect requires SRC -> TGT" };
|
|
139
|
+
const srcLabel = op.positionals.slice(0, arrowIdx).join(" ");
|
|
140
|
+
const tgtLabel = op.positionals.slice(arrowIdx + 1).join(" ");
|
|
141
|
+
if (!srcLabel || !tgtLabel)
|
|
142
|
+
return { success: false, message: "connect requires SRC -> TGT" };
|
|
143
|
+
const src = findByLabel(config, srcLabel);
|
|
144
|
+
const tgt = findByLabel(config, tgtLabel);
|
|
145
|
+
if (!src)
|
|
146
|
+
return { success: false, message: `source "${srcLabel}" not found` };
|
|
147
|
+
if (!tgt)
|
|
148
|
+
return { success: false, message: `target "${tgtLabel}" not found` };
|
|
149
|
+
const conn = {
|
|
150
|
+
id: generateId(),
|
|
151
|
+
sourceId: src.id,
|
|
152
|
+
targetId: tgt.id,
|
|
153
|
+
sourceLabel: src.label,
|
|
154
|
+
targetLabel: tgt.label,
|
|
155
|
+
label: op.params["label"],
|
|
156
|
+
};
|
|
157
|
+
addConnection(config, conn);
|
|
158
|
+
log.append({ type: "connection_added", connection: structuredClone(conn) });
|
|
159
|
+
return { success: true, message: `${srcLabel} -> ${tgtLabel}`, prefix: "~" };
|
|
160
|
+
}
|
|
161
|
+
function handleDisconnect(op, config, log) {
|
|
162
|
+
const arrowIdx = op.positionals.indexOf("->");
|
|
163
|
+
if (arrowIdx < 0)
|
|
164
|
+
return { success: false, message: "disconnect requires SRC -> TGT" };
|
|
165
|
+
const srcLabel = op.positionals.slice(0, arrowIdx).join(" ");
|
|
166
|
+
const tgtLabel = op.positionals.slice(arrowIdx + 1).join(" ");
|
|
167
|
+
const src = findByLabel(config, srcLabel);
|
|
168
|
+
const tgt = findByLabel(config, tgtLabel);
|
|
169
|
+
if (!src || !tgt)
|
|
170
|
+
return { success: false, message: "source or target not found" };
|
|
171
|
+
for (const [id, conn] of config.connections) {
|
|
172
|
+
if (conn.sourceId === src.id && conn.targetId === tgt.id) {
|
|
173
|
+
removeConnection(config, id);
|
|
174
|
+
log.append({ type: "connection_removed", connection: structuredClone(conn) });
|
|
175
|
+
return { success: true, message: `${srcLabel} -> ${tgtLabel}`, prefix: "-" };
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
return { success: false, message: `no connection from "${srcLabel}" to "${tgtLabel}"` };
|
|
179
|
+
}
|
|
180
|
+
function handleLabel(op, config, log) {
|
|
181
|
+
const oldLabel = op.positionals[0];
|
|
182
|
+
const newLabel = op.positionals[1];
|
|
183
|
+
if (!oldLabel || !newLabel)
|
|
184
|
+
return { success: false, message: "label requires OLD_LABEL NEW_LABEL" };
|
|
185
|
+
const block = findByLabel(config, oldLabel);
|
|
186
|
+
if (!block)
|
|
187
|
+
return { success: false, message: `block "${oldLabel}" not found` };
|
|
188
|
+
// Check for type+label conflict
|
|
189
|
+
for (const existing of config.blocks.values()) {
|
|
190
|
+
if (existing.id !== block.id && existing.fullType === block.fullType && existing.label.toLowerCase() === newLabel.toLowerCase()) {
|
|
191
|
+
return { success: false, message: `${block.fullType} "${newLabel}" already exists` };
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
const before = block.label;
|
|
195
|
+
block.label = newLabel;
|
|
196
|
+
rebuildLabelIndex(config);
|
|
197
|
+
log.append({ type: "block_renamed", blockId: block.id, before, after: newLabel });
|
|
198
|
+
return { success: true, message: `"${before}" → "${newLabel}"`, prefix: "*" };
|
|
199
|
+
}
|
|
200
|
+
function applyTags(block, tagsStr, log) {
|
|
201
|
+
const pairs = tagsStr.split(",").map((p) => p.trim());
|
|
202
|
+
for (const pair of pairs) {
|
|
203
|
+
const eqIdx = pair.indexOf("=");
|
|
204
|
+
if (eqIdx < 0)
|
|
205
|
+
continue;
|
|
206
|
+
const key = pair.slice(0, eqIdx).trim();
|
|
207
|
+
const val = pair.slice(eqIdx + 1).trim();
|
|
208
|
+
const before = block.tags.get(key) ?? null;
|
|
209
|
+
block.tags.set(key, val);
|
|
210
|
+
log.append({ type: "tag_set", blockId: block.id, key, before, after: val });
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function handleStyle(op, config, log) {
|
|
214
|
+
const tagsStr = op.params["tags"];
|
|
215
|
+
if (!tagsStr)
|
|
216
|
+
return { success: false, message: "style requires tags:\"Key=Val,Key2=Val2\"" };
|
|
217
|
+
// Selector-based styling
|
|
218
|
+
if (op.selectors.length > 0) {
|
|
219
|
+
const resolved = resolveSelectors(op.selectors, config);
|
|
220
|
+
if (resolved.length === 0)
|
|
221
|
+
return { success: false, message: "no blocks match selector" };
|
|
222
|
+
for (const block of resolved) {
|
|
223
|
+
applyTags(block, tagsStr, log);
|
|
224
|
+
}
|
|
225
|
+
return { success: true, message: `styled ${resolved.length} block(s)`, prefix: "@" };
|
|
226
|
+
}
|
|
227
|
+
// Label-based styling
|
|
228
|
+
const label = op.positionals[0];
|
|
229
|
+
if (!label)
|
|
230
|
+
return { success: false, message: "style requires LABEL or @selector" };
|
|
231
|
+
const block = findByLabel(config, label);
|
|
232
|
+
if (!block)
|
|
233
|
+
return { success: false, message: `block "${label}" not found` };
|
|
234
|
+
applyTags(block, tagsStr, log);
|
|
235
|
+
return { success: true, message: `${label}: tags set`, prefix: "*" };
|
|
236
|
+
}
|
|
237
|
+
function handleNest(op, config, log) {
|
|
238
|
+
const label = op.positionals[0];
|
|
239
|
+
const blockType = op.positionals[1];
|
|
240
|
+
if (!label || !blockType)
|
|
241
|
+
return { success: false, message: "nest requires LABEL BLOCK_TYPE" };
|
|
242
|
+
const block = findByLabel(config, label);
|
|
243
|
+
if (!block)
|
|
244
|
+
return { success: false, message: `block "${label}" not found` };
|
|
245
|
+
const attrs = new Map();
|
|
246
|
+
for (const [k, v] of Object.entries(op.params)) {
|
|
247
|
+
attrs.set(k, makeAttribute(k, v, op.quotedParams?.has(k)));
|
|
248
|
+
}
|
|
249
|
+
const nested = { id: generateId(), type: blockType, attributes: attrs };
|
|
250
|
+
block.nestedBlocks.push(nested);
|
|
251
|
+
log.append({ type: "nested_block_added", blockId: block.id, nestedBlock: structuredClone(nested) });
|
|
252
|
+
return { success: true, message: `${label}: ${blockType} block added`, prefix: "+" };
|
|
253
|
+
}
|
|
254
|
+
function handleUnset(op, config, log) {
|
|
255
|
+
const label = op.positionals[0];
|
|
256
|
+
if (!label)
|
|
257
|
+
return { success: false, message: "unset requires LABEL" };
|
|
258
|
+
const block = findByLabel(config, label);
|
|
259
|
+
if (!block)
|
|
260
|
+
return { success: false, message: `block "${label}" not found` };
|
|
261
|
+
const keys = op.positionals.slice(1);
|
|
262
|
+
if (keys.length === 0)
|
|
263
|
+
return { success: false, message: "unset requires at least one KEY" };
|
|
264
|
+
for (const key of keys) {
|
|
265
|
+
const before = block.attributes.get(key);
|
|
266
|
+
if (before) {
|
|
267
|
+
block.attributes.delete(key);
|
|
268
|
+
log.append({ type: "attribute_removed", blockId: block.id, key, before: structuredClone(before) });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
return { success: true, message: `${label}: unset ${keys.join(", ")}`, prefix: "*" };
|
|
272
|
+
}
|
|
273
|
+
function handleUnnest(op, config, log) {
|
|
274
|
+
const label = op.positionals[0];
|
|
275
|
+
const blockType = op.positionals[1];
|
|
276
|
+
if (!label || !blockType)
|
|
277
|
+
return { success: false, message: "unnest requires LABEL BLOCK_TYPE [INDEX]" };
|
|
278
|
+
const block = findByLabel(config, label);
|
|
279
|
+
if (!block)
|
|
280
|
+
return { success: false, message: `block "${label}" not found` };
|
|
281
|
+
const matching = block.nestedBlocks.filter((nb) => nb.type === blockType);
|
|
282
|
+
if (matching.length === 0)
|
|
283
|
+
return { success: false, message: `no "${blockType}" nested block on "${label}"` };
|
|
284
|
+
const indexStr = op.positionals[2];
|
|
285
|
+
let target;
|
|
286
|
+
if (indexStr !== undefined) {
|
|
287
|
+
const idx = parseInt(indexStr, 10);
|
|
288
|
+
if (isNaN(idx) || idx < 0 || idx >= matching.length) {
|
|
289
|
+
return { success: false, message: `index ${indexStr} out of range (0-${matching.length - 1})` };
|
|
290
|
+
}
|
|
291
|
+
target = matching[idx];
|
|
292
|
+
}
|
|
293
|
+
else {
|
|
294
|
+
// Remove last block of that type
|
|
295
|
+
target = matching[matching.length - 1];
|
|
296
|
+
}
|
|
297
|
+
block.nestedBlocks = block.nestedBlocks.filter((nb) => nb.id !== target.id);
|
|
298
|
+
log.append({ type: "nested_block_removed", blockId: block.id, nestedBlock: structuredClone(target) });
|
|
299
|
+
return { success: true, message: `${label}: ${blockType} block removed`, prefix: "-" };
|
|
300
|
+
}
|
|
301
|
+
// ── Selector resolution ─────────────────────────────────
|
|
302
|
+
function resolveSelectors(selectors, config) {
|
|
303
|
+
let results = [...config.blocks.values()];
|
|
304
|
+
for (const sel of selectors) {
|
|
305
|
+
if (sel === "@all")
|
|
306
|
+
continue;
|
|
307
|
+
if (sel.startsWith("@type:")) {
|
|
308
|
+
const type = sel.slice(6);
|
|
309
|
+
results = results.filter((b) => b.fullType === type);
|
|
310
|
+
}
|
|
311
|
+
else if (sel.startsWith("@kind:")) {
|
|
312
|
+
const kind = sel.slice(6);
|
|
313
|
+
results = results.filter((b) => b.kind === kind);
|
|
314
|
+
}
|
|
315
|
+
else if (sel.startsWith("@provider:")) {
|
|
316
|
+
const provider = sel.slice(10);
|
|
317
|
+
results = results.filter((b) => b.provider === provider);
|
|
318
|
+
}
|
|
319
|
+
else if (sel.startsWith("@tag:")) {
|
|
320
|
+
const tagExpr = sel.slice(5);
|
|
321
|
+
const eqIdx = tagExpr.indexOf("=");
|
|
322
|
+
if (eqIdx >= 0) {
|
|
323
|
+
const key = tagExpr.slice(0, eqIdx);
|
|
324
|
+
const val = tagExpr.slice(eqIdx + 1);
|
|
325
|
+
results = results.filter((b) => b.tags.get(key) === val);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
results = results.filter((b) => b.tags.has(tagExpr));
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return results;
|
|
333
|
+
}
|
|
334
|
+
// ── Handler registry ────────────────────────────────────
|
|
335
|
+
const HANDLERS = {
|
|
336
|
+
add: handleAdd,
|
|
337
|
+
set: handleSet,
|
|
338
|
+
remove: handleRemove,
|
|
339
|
+
connect: handleConnect,
|
|
340
|
+
disconnect: handleDisconnect,
|
|
341
|
+
label: handleLabel,
|
|
342
|
+
style: handleStyle,
|
|
343
|
+
nest: handleNest,
|
|
344
|
+
unnest: handleUnnest,
|
|
345
|
+
unset: handleUnset,
|
|
346
|
+
};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { TerraformConfig } from "./types.js";
|
|
2
|
+
import type { EventLog } from "@aetherwing/fcp-core";
|
|
3
|
+
import type { TerraformEvent } from "./types.js";
|
|
4
|
+
export declare function dispatchQuery(query: string, config: TerraformConfig, eventLog?: EventLog<TerraformEvent>): string | Promise<string>;
|
package/dist/queries.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { serializeToHcl } from "./hcl.js";
|
|
2
|
+
import { findByLabel, findByKind } from "./model.js";
|
|
3
|
+
export function dispatchQuery(query, config, eventLog) {
|
|
4
|
+
const parts = query.trim().split(/\s+/);
|
|
5
|
+
const cmd = parts[0]?.toLowerCase() ?? "";
|
|
6
|
+
const args = parts.slice(1);
|
|
7
|
+
switch (cmd) {
|
|
8
|
+
case "map":
|
|
9
|
+
return queryMap(config);
|
|
10
|
+
case "list":
|
|
11
|
+
return queryList(config, args);
|
|
12
|
+
case "describe":
|
|
13
|
+
return queryDescribe(config, args[0]);
|
|
14
|
+
case "plan":
|
|
15
|
+
return queryPlan(config);
|
|
16
|
+
case "graph":
|
|
17
|
+
return queryGraph(config);
|
|
18
|
+
case "validate":
|
|
19
|
+
return queryValidate(config);
|
|
20
|
+
case "stats":
|
|
21
|
+
return queryStats(config);
|
|
22
|
+
case "status":
|
|
23
|
+
return queryStatus(config);
|
|
24
|
+
case "find":
|
|
25
|
+
return queryFind(config, args.join(" "));
|
|
26
|
+
case "history":
|
|
27
|
+
return queryHistory(eventLog, parseInt(args[0]) || 10);
|
|
28
|
+
default:
|
|
29
|
+
return `Unknown query: "${cmd}". Available: map, list, describe, plan, graph, validate, stats, status, find, history`;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function queryMap(config) {
|
|
33
|
+
const lines = [];
|
|
34
|
+
lines.push(`Terraform Config: ${config.title}`);
|
|
35
|
+
lines.push("");
|
|
36
|
+
const resources = findByKind(config, "resource");
|
|
37
|
+
const variables = findByKind(config, "variable");
|
|
38
|
+
const outputs = findByKind(config, "output");
|
|
39
|
+
const providers = findByKind(config, "provider");
|
|
40
|
+
const data = findByKind(config, "data");
|
|
41
|
+
if (providers.length > 0) {
|
|
42
|
+
lines.push(`Providers: ${providers.map((p) => p.label).join(", ")}`);
|
|
43
|
+
}
|
|
44
|
+
if (resources.length > 0) {
|
|
45
|
+
const typeCounts = new Map();
|
|
46
|
+
for (const r of resources) {
|
|
47
|
+
typeCounts.set(r.fullType, (typeCounts.get(r.fullType) ?? 0) + 1);
|
|
48
|
+
}
|
|
49
|
+
const types = [...typeCounts.entries()].map(([t, c]) => `${t} x${c}`).join(", ");
|
|
50
|
+
lines.push(`Resources (${resources.length}): ${types}`);
|
|
51
|
+
}
|
|
52
|
+
if (data.length > 0)
|
|
53
|
+
lines.push(`Data Sources: ${data.length}`);
|
|
54
|
+
if (variables.length > 0)
|
|
55
|
+
lines.push(`Variables: ${variables.length}`);
|
|
56
|
+
if (outputs.length > 0)
|
|
57
|
+
lines.push(`Outputs: ${outputs.length}`);
|
|
58
|
+
if (config.connections.size > 0)
|
|
59
|
+
lines.push(`Connections: ${config.connections.size}`);
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
|
62
|
+
function queryList(config, args) {
|
|
63
|
+
let blocks = [...config.blocks.values()];
|
|
64
|
+
// Filter by selector if provided
|
|
65
|
+
if (args.length > 0 && args[0].startsWith("@")) {
|
|
66
|
+
const sel = args[0];
|
|
67
|
+
if (sel.startsWith("@type:")) {
|
|
68
|
+
const type = sel.slice(6);
|
|
69
|
+
blocks = blocks.filter((b) => b.fullType === type);
|
|
70
|
+
}
|
|
71
|
+
else if (sel.startsWith("@kind:")) {
|
|
72
|
+
const kind = sel.slice(6);
|
|
73
|
+
blocks = blocks.filter((b) => b.kind === kind);
|
|
74
|
+
}
|
|
75
|
+
else if (sel.startsWith("@provider:")) {
|
|
76
|
+
const provider = sel.slice(10);
|
|
77
|
+
blocks = blocks.filter((b) => b.provider === provider);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (blocks.length === 0)
|
|
81
|
+
return "No blocks found.";
|
|
82
|
+
const lines = [];
|
|
83
|
+
for (const block of blocks) {
|
|
84
|
+
const type = block.kind === "resource" || block.kind === "data"
|
|
85
|
+
? `${block.fullType}.${block.label}`
|
|
86
|
+
: `${block.kind}.${block.label}`;
|
|
87
|
+
const attrCount = block.attributes.size;
|
|
88
|
+
lines.push(` ${block.kind.padEnd(10)} ${type.padEnd(35)} (${attrCount} attrs)`);
|
|
89
|
+
}
|
|
90
|
+
return lines.join("\n");
|
|
91
|
+
}
|
|
92
|
+
function queryDescribe(config, label) {
|
|
93
|
+
if (!label)
|
|
94
|
+
return "describe requires a LABEL";
|
|
95
|
+
const block = findByLabel(config, label);
|
|
96
|
+
if (!block)
|
|
97
|
+
return `block "${label}" not found`;
|
|
98
|
+
const lines = [];
|
|
99
|
+
const ref = block.kind === "resource" || block.kind === "data"
|
|
100
|
+
? `${block.fullType}.${block.label}`
|
|
101
|
+
: `${block.kind} "${block.label}"`;
|
|
102
|
+
lines.push(`${block.kind}: ${ref}`);
|
|
103
|
+
if (block.provider)
|
|
104
|
+
lines.push(` provider: ${block.provider}`);
|
|
105
|
+
if (block.attributes.size > 0) {
|
|
106
|
+
lines.push(" attributes:");
|
|
107
|
+
for (const attr of block.attributes.values()) {
|
|
108
|
+
lines.push(` ${attr.key} = ${attr.valueType === "string" ? `"${attr.value}"` : attr.value} (${attr.valueType})`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
if (block.tags.size > 0) {
|
|
112
|
+
lines.push(" tags:");
|
|
113
|
+
for (const [k, v] of block.tags) {
|
|
114
|
+
lines.push(` ${k} = "${v}"`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (block.nestedBlocks.length > 0) {
|
|
118
|
+
lines.push(" nested blocks:");
|
|
119
|
+
for (const nested of block.nestedBlocks) {
|
|
120
|
+
lines.push(` ${nested.type} {}`);
|
|
121
|
+
for (const attr of nested.attributes.values()) {
|
|
122
|
+
lines.push(` ${attr.key} = ${attr.value}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Show connections
|
|
127
|
+
const conns = [...config.connections.values()].filter((c) => c.sourceId === block.id || c.targetId === block.id);
|
|
128
|
+
if (conns.length > 0) {
|
|
129
|
+
lines.push(" connections:");
|
|
130
|
+
for (const c of conns) {
|
|
131
|
+
if (c.sourceId === block.id) {
|
|
132
|
+
lines.push(` → ${c.targetLabel}`);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
lines.push(` ← ${c.sourceLabel}`);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
if (block.meta.count)
|
|
140
|
+
lines.push(` count: ${block.meta.count}`);
|
|
141
|
+
if (block.meta.forEach)
|
|
142
|
+
lines.push(` for_each: ${block.meta.forEach}`);
|
|
143
|
+
if (block.meta.dependsOn.length > 0)
|
|
144
|
+
lines.push(` depends_on: ${block.meta.dependsOn.join(", ")}`);
|
|
145
|
+
return lines.join("\n");
|
|
146
|
+
}
|
|
147
|
+
async function queryPlan(config) {
|
|
148
|
+
if (config.blocks.size === 0)
|
|
149
|
+
return "Empty configuration. Add some resources first.";
|
|
150
|
+
const hcl = serializeToHcl(config);
|
|
151
|
+
try {
|
|
152
|
+
const { parse } = await import("@cdktf/hcl2json");
|
|
153
|
+
await parse("plan.tf", hcl);
|
|
154
|
+
}
|
|
155
|
+
catch (err) {
|
|
156
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
157
|
+
return hcl + `\n\n⚠ HCL validation failed: ${msg}`;
|
|
158
|
+
}
|
|
159
|
+
return hcl;
|
|
160
|
+
}
|
|
161
|
+
function queryGraph(config) {
|
|
162
|
+
if (config.connections.size === 0)
|
|
163
|
+
return "No connections. Use 'connect SRC -> TGT' to add dependencies.";
|
|
164
|
+
const lines = ["Dependency Graph:"];
|
|
165
|
+
for (const conn of config.connections.values()) {
|
|
166
|
+
const src = config.blocks.get(conn.sourceId);
|
|
167
|
+
const tgt = config.blocks.get(conn.targetId);
|
|
168
|
+
if (src && tgt) {
|
|
169
|
+
const label = conn.label ? ` (${conn.label})` : "";
|
|
170
|
+
lines.push(` ${src.label} -> ${tgt.label}${label}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return lines.join("\n");
|
|
174
|
+
}
|
|
175
|
+
function queryValidate(config) {
|
|
176
|
+
const issues = [];
|
|
177
|
+
for (const block of config.blocks.values()) {
|
|
178
|
+
if (block.kind === "resource" && block.attributes.size === 0 && block.nestedBlocks.length === 0) {
|
|
179
|
+
issues.push(` WARNING: resource ${block.fullType}.${block.label} has no attributes`);
|
|
180
|
+
}
|
|
181
|
+
if (block.kind === "output" && !block.attributes.has("value")) {
|
|
182
|
+
issues.push(` ERROR: output "${block.label}" missing required "value" attribute`);
|
|
183
|
+
}
|
|
184
|
+
if (block.kind === "module" && !block.attributes.has("source")) {
|
|
185
|
+
issues.push(` ERROR: module "${block.label}" missing required "source" attribute`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
// Check for dangling connections
|
|
189
|
+
for (const conn of config.connections.values()) {
|
|
190
|
+
if (!config.blocks.has(conn.sourceId)) {
|
|
191
|
+
issues.push(` ERROR: connection references missing source block`);
|
|
192
|
+
}
|
|
193
|
+
if (!config.blocks.has(conn.targetId)) {
|
|
194
|
+
issues.push(` ERROR: connection references missing target block`);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (issues.length === 0)
|
|
198
|
+
return "Configuration is valid.";
|
|
199
|
+
return `Found ${issues.length} issue(s):\n${issues.join("\n")}`;
|
|
200
|
+
}
|
|
201
|
+
function queryStats(config) {
|
|
202
|
+
const counts = new Map();
|
|
203
|
+
for (const block of config.blocks.values()) {
|
|
204
|
+
counts.set(block.kind, (counts.get(block.kind) ?? 0) + 1);
|
|
205
|
+
}
|
|
206
|
+
const lines = [`Total blocks: ${config.blocks.size}`];
|
|
207
|
+
for (const [kind, count] of counts) {
|
|
208
|
+
lines.push(` ${kind}: ${count}`);
|
|
209
|
+
}
|
|
210
|
+
lines.push(`Connections: ${config.connections.size}`);
|
|
211
|
+
return lines.join("\n");
|
|
212
|
+
}
|
|
213
|
+
function queryStatus(config) {
|
|
214
|
+
const lines = [];
|
|
215
|
+
lines.push(`Title: ${config.title}`);
|
|
216
|
+
lines.push(`File: ${config.filePath ?? "(unsaved)"}`);
|
|
217
|
+
lines.push(`Blocks: ${config.blocks.size}`);
|
|
218
|
+
lines.push(`Connections: ${config.connections.size}`);
|
|
219
|
+
return lines.join("\n");
|
|
220
|
+
}
|
|
221
|
+
function queryFind(config, text) {
|
|
222
|
+
if (!text)
|
|
223
|
+
return "find requires search text";
|
|
224
|
+
const lower = text.toLowerCase();
|
|
225
|
+
const matches = [...config.blocks.values()].filter((b) => b.label.toLowerCase().includes(lower) || b.fullType.toLowerCase().includes(lower));
|
|
226
|
+
if (matches.length === 0)
|
|
227
|
+
return `No blocks matching "${text}"`;
|
|
228
|
+
return matches.map((b) => ` ${b.kind} ${b.fullType}.${b.label}`).join("\n");
|
|
229
|
+
}
|
|
230
|
+
function queryHistory(eventLog, count) {
|
|
231
|
+
if (!eventLog)
|
|
232
|
+
return "No event log available.";
|
|
233
|
+
const events = eventLog.recent(count);
|
|
234
|
+
if (events.length === 0)
|
|
235
|
+
return "No events.";
|
|
236
|
+
return events.map((e, i) => {
|
|
237
|
+
switch (e.type) {
|
|
238
|
+
case "block_added": return ` ${i + 1}. + ${e.block.kind} ${e.block.label}`;
|
|
239
|
+
case "block_removed": return ` ${i + 1}. - ${e.block.kind} ${e.block.label}`;
|
|
240
|
+
case "attribute_set": return ` ${i + 1}. * set ${e.key} on block`;
|
|
241
|
+
case "attribute_removed": return ` ${i + 1}. * unset ${e.key}`;
|
|
242
|
+
case "connection_added": return ` ${i + 1}. ~ ${e.connection.sourceLabel} -> ${e.connection.targetLabel}`;
|
|
243
|
+
case "connection_removed": return ` ${i + 1}. - disconnect`;
|
|
244
|
+
case "tag_set": return ` ${i + 1}. * tag ${e.key}`;
|
|
245
|
+
case "tag_removed": return ` ${i + 1}. - tag ${e.key}`;
|
|
246
|
+
case "block_renamed": return ` ${i + 1}. * rename ${e.before} → ${e.after}`;
|
|
247
|
+
default: return ` ${i + 1}. ${e.type}`;
|
|
248
|
+
}
|
|
249
|
+
}).join("\n");
|
|
250
|
+
}
|