mcp-souschef 2.5.3__py3-none-any.whl → 3.0.0__py3-none-any.whl
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.
- {mcp_souschef-2.5.3.dist-info → mcp_souschef-3.0.0.dist-info}/METADATA +135 -28
- mcp_souschef-3.0.0.dist-info/RECORD +46 -0
- {mcp_souschef-2.5.3.dist-info → mcp_souschef-3.0.0.dist-info}/WHEEL +1 -1
- souschef/__init__.py +43 -3
- souschef/assessment.py +1260 -69
- souschef/ci/common.py +126 -0
- souschef/ci/github_actions.py +4 -93
- souschef/ci/gitlab_ci.py +3 -53
- souschef/ci/jenkins_pipeline.py +3 -60
- souschef/cli.py +129 -20
- souschef/converters/__init__.py +2 -2
- souschef/converters/cookbook_specific.py +125 -0
- souschef/converters/cookbook_specific.py.backup +109 -0
- souschef/converters/playbook.py +1022 -15
- souschef/converters/resource.py +113 -10
- souschef/converters/template.py +177 -0
- souschef/core/constants.py +13 -0
- souschef/core/metrics.py +313 -0
- souschef/core/path_utils.py +12 -9
- souschef/core/validation.py +53 -0
- souschef/deployment.py +85 -33
- souschef/parsers/attributes.py +397 -32
- souschef/parsers/recipe.py +48 -10
- souschef/server.py +715 -37
- souschef/ui/app.py +1658 -379
- souschef/ui/health_check.py +36 -0
- souschef/ui/pages/ai_settings.py +563 -0
- souschef/ui/pages/cookbook_analysis.py +3270 -166
- souschef/ui/pages/validation_reports.py +274 -0
- mcp_souschef-2.5.3.dist-info/RECORD +0 -38
- {mcp_souschef-2.5.3.dist-info → mcp_souschef-3.0.0.dist-info}/entry_points.txt +0 -0
- {mcp_souschef-2.5.3.dist-info → mcp_souschef-3.0.0.dist-info}/licenses/LICENSE +0 -0
souschef/parsers/attributes.py
CHANGED
|
@@ -4,9 +4,11 @@ import re
|
|
|
4
4
|
from typing import Any
|
|
5
5
|
|
|
6
6
|
from souschef.core.constants import (
|
|
7
|
+
ATTRIBUTE_PREFIX,
|
|
7
8
|
ERROR_FILE_NOT_FOUND,
|
|
8
9
|
ERROR_IS_DIRECTORY,
|
|
9
10
|
ERROR_PERMISSION_DENIED,
|
|
11
|
+
VALUE_PREFIX,
|
|
10
12
|
)
|
|
11
13
|
from souschef.core.path_utils import _normalize_path
|
|
12
14
|
from souschef.parsers.template import _strip_ruby_comments
|
|
@@ -64,7 +66,351 @@ def parse_attributes(path: str, resolve_precedence: bool = True) -> str:
|
|
|
64
66
|
return f"An error occurred: {e}"
|
|
65
67
|
|
|
66
68
|
|
|
67
|
-
def
|
|
69
|
+
def _extract_precedence_and_path(line: str) -> tuple[str, str, str] | None:
|
|
70
|
+
"""Extract precedence and attribute path from a line."""
|
|
71
|
+
precedence_types = (
|
|
72
|
+
"default",
|
|
73
|
+
"force_default",
|
|
74
|
+
"normal",
|
|
75
|
+
"override",
|
|
76
|
+
"force_override",
|
|
77
|
+
"automatic",
|
|
78
|
+
)
|
|
79
|
+
if not (line.startswith(precedence_types) and "[" in line):
|
|
80
|
+
return None
|
|
81
|
+
|
|
82
|
+
# Extract precedence
|
|
83
|
+
if line.startswith("default"):
|
|
84
|
+
precedence = "default"
|
|
85
|
+
attr_part = line[7:].strip()
|
|
86
|
+
elif line.startswith("force_default"):
|
|
87
|
+
precedence = "force_default"
|
|
88
|
+
attr_part = line[13:].strip()
|
|
89
|
+
elif line.startswith("normal"):
|
|
90
|
+
precedence = "normal"
|
|
91
|
+
attr_part = line[6:].strip()
|
|
92
|
+
elif line.startswith("override"):
|
|
93
|
+
precedence = "override"
|
|
94
|
+
attr_part = line[8:].strip()
|
|
95
|
+
elif line.startswith("force_override"):
|
|
96
|
+
precedence = "force_override"
|
|
97
|
+
attr_part = line[14:].strip()
|
|
98
|
+
elif line.startswith("automatic"):
|
|
99
|
+
precedence = "automatic"
|
|
100
|
+
attr_part = line[9:].strip()
|
|
101
|
+
else:
|
|
102
|
+
return None
|
|
103
|
+
|
|
104
|
+
# Find the attribute path and value
|
|
105
|
+
equals_pos = attr_part.find("=")
|
|
106
|
+
if equals_pos == -1:
|
|
107
|
+
return None
|
|
108
|
+
|
|
109
|
+
attr_path_part = attr_part[:equals_pos].strip()
|
|
110
|
+
value_start = attr_part[equals_pos + 1 :].strip()
|
|
111
|
+
|
|
112
|
+
# Clean up the path
|
|
113
|
+
attr_path = (
|
|
114
|
+
attr_path_part.replace("']['", ".")
|
|
115
|
+
.replace('"]["', ".")
|
|
116
|
+
.replace("['", "")
|
|
117
|
+
.replace("']", "")
|
|
118
|
+
.replace('["', "")
|
|
119
|
+
.replace('"]', "")
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
return precedence, attr_path, value_start
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _is_ruby_array_syntax(value: str) -> bool:
|
|
126
|
+
"""Check if value uses Ruby array syntax."""
|
|
127
|
+
stripped = value.strip()
|
|
128
|
+
return stripped.startswith("%w") or (
|
|
129
|
+
stripped.startswith("[") and stripped.endswith("]")
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _should_stop_collecting(stripped: str, precedence_types: tuple[str, ...]) -> bool:
|
|
134
|
+
"""Check if we should stop collecting multiline value based on line content."""
|
|
135
|
+
# Stop if we hit another attribute declaration
|
|
136
|
+
if stripped.startswith(precedence_types) and "[" in stripped:
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
# Stop if we hit Ruby control structures that indicate end of attribute
|
|
140
|
+
return stripped.startswith(
|
|
141
|
+
(
|
|
142
|
+
"case ",
|
|
143
|
+
"if ",
|
|
144
|
+
"unless ",
|
|
145
|
+
"when ",
|
|
146
|
+
"else",
|
|
147
|
+
"end",
|
|
148
|
+
"def ",
|
|
149
|
+
"class ",
|
|
150
|
+
"module ",
|
|
151
|
+
)
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _update_string_state(
|
|
156
|
+
char: str,
|
|
157
|
+
in_string: bool,
|
|
158
|
+
string_char: str | None,
|
|
159
|
+
line: str,
|
|
160
|
+
value_lines: list[str],
|
|
161
|
+
) -> tuple[bool, str | None]:
|
|
162
|
+
"""Update string parsing state for a single character."""
|
|
163
|
+
if not in_string and char in ('"', "'"):
|
|
164
|
+
return True, char
|
|
165
|
+
elif (
|
|
166
|
+
in_string
|
|
167
|
+
and string_char is not None
|
|
168
|
+
and char == string_char
|
|
169
|
+
and (
|
|
170
|
+
not value_lines
|
|
171
|
+
or line[value_lines[-1].rfind(string_char) + 1 :].count("\\") % 2 == 0
|
|
172
|
+
)
|
|
173
|
+
):
|
|
174
|
+
return False, None
|
|
175
|
+
return in_string, string_char
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def _update_bracket_depths(
|
|
179
|
+
char: str,
|
|
180
|
+
brace_depth: int,
|
|
181
|
+
bracket_depth: int,
|
|
182
|
+
paren_depth: int,
|
|
183
|
+
) -> tuple[int, int, int]:
|
|
184
|
+
"""Update bracket/braces/parentheses depth counters for a single character."""
|
|
185
|
+
if char == "{":
|
|
186
|
+
return brace_depth + 1, bracket_depth, paren_depth
|
|
187
|
+
elif char == "}":
|
|
188
|
+
return brace_depth - 1, bracket_depth, paren_depth
|
|
189
|
+
elif char == "[":
|
|
190
|
+
return brace_depth, bracket_depth + 1, paren_depth
|
|
191
|
+
elif char == "]":
|
|
192
|
+
return brace_depth, bracket_depth - 1, paren_depth
|
|
193
|
+
elif char == "(":
|
|
194
|
+
return brace_depth, bracket_depth, paren_depth + 1
|
|
195
|
+
elif char == ")":
|
|
196
|
+
return brace_depth, bracket_depth, paren_depth - 1
|
|
197
|
+
return brace_depth, bracket_depth, paren_depth
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _update_parsing_state(
|
|
201
|
+
line: str,
|
|
202
|
+
in_string: bool,
|
|
203
|
+
string_char: str | None,
|
|
204
|
+
brace_depth: int,
|
|
205
|
+
bracket_depth: int,
|
|
206
|
+
paren_depth: int,
|
|
207
|
+
value_lines: list[str],
|
|
208
|
+
) -> tuple[bool, str | None, int, int, int]:
|
|
209
|
+
"""Update parsing state for string literals and bracket/braces depth."""
|
|
210
|
+
for char in line:
|
|
211
|
+
if in_string:
|
|
212
|
+
in_string, string_char = _update_string_state(
|
|
213
|
+
char,
|
|
214
|
+
in_string,
|
|
215
|
+
string_char,
|
|
216
|
+
line,
|
|
217
|
+
value_lines,
|
|
218
|
+
)
|
|
219
|
+
else:
|
|
220
|
+
# Check for entering string
|
|
221
|
+
if char in ('"', "'"):
|
|
222
|
+
in_string, string_char = True, char
|
|
223
|
+
else:
|
|
224
|
+
# Update bracket depths
|
|
225
|
+
brace_depth, bracket_depth, paren_depth = _update_bracket_depths(
|
|
226
|
+
char, brace_depth, bracket_depth, paren_depth
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
return in_string, string_char, brace_depth, bracket_depth, paren_depth
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
def _is_value_complete(
|
|
233
|
+
in_string: bool,
|
|
234
|
+
brace_depth: int,
|
|
235
|
+
bracket_depth: int,
|
|
236
|
+
paren_depth: int,
|
|
237
|
+
value_lines: list[str],
|
|
238
|
+
line: str,
|
|
239
|
+
next_line: str,
|
|
240
|
+
precedence_types: tuple[str, ...],
|
|
241
|
+
) -> bool:
|
|
242
|
+
"""Check if the multiline value collection is complete."""
|
|
243
|
+
# Must be outside strings and have balanced brackets/braces
|
|
244
|
+
if in_string or brace_depth > 0 or bracket_depth > 0 or paren_depth > 0:
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
# For arrays like %w(...), check if we have the closing paren
|
|
248
|
+
if value_lines and value_lines[0].strip().startswith("%w"):
|
|
249
|
+
return ")" in line
|
|
250
|
+
|
|
251
|
+
# For regular arrays/hashes, break when brackets are balanced
|
|
252
|
+
return (
|
|
253
|
+
brace_depth == 0
|
|
254
|
+
and bracket_depth == 0
|
|
255
|
+
and paren_depth == 0
|
|
256
|
+
and (
|
|
257
|
+
not next_line
|
|
258
|
+
or next_line.startswith(precedence_types)
|
|
259
|
+
or next_line.startswith(("case ", "if ", "unless "))
|
|
260
|
+
)
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
def _collect_multiline_value(lines: list[str], start_idx: int) -> tuple[str, int]:
|
|
265
|
+
"""Collect multiline attribute value."""
|
|
266
|
+
value_lines: list[str] = []
|
|
267
|
+
i = start_idx
|
|
268
|
+
brace_depth = 0
|
|
269
|
+
bracket_depth = 0
|
|
270
|
+
paren_depth = 0
|
|
271
|
+
in_string = False
|
|
272
|
+
string_char = None
|
|
273
|
+
|
|
274
|
+
precedence_types = (
|
|
275
|
+
"default",
|
|
276
|
+
"force_default",
|
|
277
|
+
"normal",
|
|
278
|
+
"override",
|
|
279
|
+
"force_override",
|
|
280
|
+
"automatic",
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
while i < len(lines):
|
|
284
|
+
line = lines[i]
|
|
285
|
+
stripped = line.strip()
|
|
286
|
+
|
|
287
|
+
# Skip empty lines at the beginning
|
|
288
|
+
if not value_lines and not stripped:
|
|
289
|
+
i += 1
|
|
290
|
+
continue
|
|
291
|
+
|
|
292
|
+
# Check if we should stop collecting
|
|
293
|
+
if _should_stop_collecting(stripped, precedence_types):
|
|
294
|
+
break
|
|
295
|
+
|
|
296
|
+
# Update parsing state for strings and brackets
|
|
297
|
+
(
|
|
298
|
+
in_string,
|
|
299
|
+
string_char,
|
|
300
|
+
brace_depth,
|
|
301
|
+
bracket_depth,
|
|
302
|
+
paren_depth,
|
|
303
|
+
) = _update_parsing_state(
|
|
304
|
+
line,
|
|
305
|
+
in_string,
|
|
306
|
+
string_char,
|
|
307
|
+
brace_depth,
|
|
308
|
+
bracket_depth,
|
|
309
|
+
paren_depth,
|
|
310
|
+
value_lines,
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
value_lines.append(line)
|
|
314
|
+
|
|
315
|
+
# Check if the value is complete
|
|
316
|
+
next_line = lines[i + 1].strip() if i + 1 < len(lines) else ""
|
|
317
|
+
if _is_value_complete(
|
|
318
|
+
in_string,
|
|
319
|
+
brace_depth,
|
|
320
|
+
bracket_depth,
|
|
321
|
+
paren_depth,
|
|
322
|
+
value_lines,
|
|
323
|
+
line,
|
|
324
|
+
next_line,
|
|
325
|
+
precedence_types,
|
|
326
|
+
):
|
|
327
|
+
break
|
|
328
|
+
|
|
329
|
+
i += 1
|
|
330
|
+
|
|
331
|
+
return "\n".join(value_lines).strip(), i
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def _convert_ruby_word_array(content: str) -> str:
|
|
335
|
+
"""Convert Ruby %w(...) array syntax to YAML list."""
|
|
336
|
+
# Extract the array content
|
|
337
|
+
match = re.match(r"%w\s*\((.*?)\)", content, re.DOTALL)
|
|
338
|
+
if not match:
|
|
339
|
+
return content
|
|
340
|
+
|
|
341
|
+
array_content = match.group(1)
|
|
342
|
+
# Split on whitespace and newlines, clean up
|
|
343
|
+
items = []
|
|
344
|
+
for item in re.split(r"\s+", array_content):
|
|
345
|
+
item = item.strip()
|
|
346
|
+
if item and not item.startswith("#"): # Skip comments
|
|
347
|
+
items.append(f" - {item}")
|
|
348
|
+
return "\n".join(items) if items else "[]"
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _convert_ruby_array(content: str) -> str:
|
|
352
|
+
"""Convert Ruby [item1, item2] array syntax to YAML list."""
|
|
353
|
+
# Remove brackets and strip
|
|
354
|
+
array_content = content.strip()[1:-1]
|
|
355
|
+
if not array_content.strip():
|
|
356
|
+
return "[]"
|
|
357
|
+
|
|
358
|
+
items = []
|
|
359
|
+
# Split on commas, but be careful with nested structures
|
|
360
|
+
for item in array_content.split(","):
|
|
361
|
+
item = item.strip()
|
|
362
|
+
if item:
|
|
363
|
+
items.append(f" - {item}")
|
|
364
|
+
return "\n".join(items) if items else "[]"
|
|
365
|
+
|
|
366
|
+
|
|
367
|
+
def _convert_ruby_hash(content: str) -> str:
|
|
368
|
+
"""Convert Ruby {key: value} hash syntax to YAML mapping."""
|
|
369
|
+
# Remove braces and strip
|
|
370
|
+
hash_content = content.strip()[1:-1]
|
|
371
|
+
if not hash_content.strip():
|
|
372
|
+
return "{}"
|
|
373
|
+
|
|
374
|
+
lines = []
|
|
375
|
+
# Split on commas
|
|
376
|
+
for pair in hash_content.split(","):
|
|
377
|
+
pair = pair.strip()
|
|
378
|
+
if ":" in pair:
|
|
379
|
+
key, val = pair.split(":", 1)
|
|
380
|
+
lines.append(f" {key.strip()}: {val.strip()}")
|
|
381
|
+
return "\n".join(lines) if lines else "{}"
|
|
382
|
+
|
|
383
|
+
|
|
384
|
+
def _convert_ruby_value_to_yaml(value: str) -> str:
|
|
385
|
+
"""
|
|
386
|
+
Convert Ruby value syntax to YAML-compatible format.
|
|
387
|
+
|
|
388
|
+
Args:
|
|
389
|
+
value: Raw Ruby value string.
|
|
390
|
+
|
|
391
|
+
Returns:
|
|
392
|
+
YAML-compatible value string.
|
|
393
|
+
|
|
394
|
+
"""
|
|
395
|
+
stripped = value.strip()
|
|
396
|
+
|
|
397
|
+
# Handle %w(...) array syntax
|
|
398
|
+
if stripped.startswith("%w"):
|
|
399
|
+
return _convert_ruby_word_array(stripped)
|
|
400
|
+
|
|
401
|
+
# Handle regular arrays [item1, item2]
|
|
402
|
+
if stripped.startswith("[") and stripped.endswith("]"):
|
|
403
|
+
return _convert_ruby_array(stripped)
|
|
404
|
+
|
|
405
|
+
# Handle hashes {key: value, key2: value2}
|
|
406
|
+
if stripped.startswith("{") and stripped.endswith("}"):
|
|
407
|
+
return _convert_ruby_hash(stripped)
|
|
408
|
+
|
|
409
|
+
# Return as-is for other values
|
|
410
|
+
return value
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def _extract_attributes(content: str) -> list[dict[str, str]]: # noqa: C901
|
|
68
414
|
"""
|
|
69
415
|
Extract Chef attributes from attributes file content.
|
|
70
416
|
|
|
@@ -79,36 +425,55 @@ def _extract_attributes(content: str) -> list[dict[str, str]]:
|
|
|
79
425
|
# Strip comments first
|
|
80
426
|
clean_content = _strip_ruby_comments(content)
|
|
81
427
|
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
pattern = (
|
|
86
|
-
r"(default|force_default|normal|override|force_override|automatic)"
|
|
87
|
-
r"((?:\[[^\]]+\])+)\s*=\s*([^\n]+)"
|
|
88
|
-
)
|
|
428
|
+
# Split content into lines for easier processing
|
|
429
|
+
lines = clean_content.split("\n")
|
|
430
|
+
i = 0
|
|
89
431
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
432
|
+
while i < len(lines):
|
|
433
|
+
line = lines[i].strip()
|
|
434
|
+
|
|
435
|
+
# Try to extract precedence and path
|
|
436
|
+
result = _extract_precedence_and_path(line)
|
|
437
|
+
if result is not None:
|
|
438
|
+
precedence, attr_path, value_start = result
|
|
439
|
+
|
|
440
|
+
# Check if this is Ruby array syntax
|
|
441
|
+
is_ruby_array = _is_ruby_array_syntax(value_start)
|
|
442
|
+
|
|
443
|
+
# Collect the value (may span multiple lines)
|
|
444
|
+
value_lines = [value_start]
|
|
445
|
+
i += 1
|
|
446
|
+
|
|
447
|
+
# Continue collecting lines
|
|
448
|
+
full_value, i = _collect_multiline_value(lines, i)
|
|
449
|
+
|
|
450
|
+
if full_value:
|
|
451
|
+
value_lines[0] = full_value
|
|
452
|
+
|
|
453
|
+
# Join value lines and clean up
|
|
454
|
+
value = "\n".join(value_lines).strip()
|
|
455
|
+
|
|
456
|
+
# For Ruby arrays, reconstruct the full syntax for conversion
|
|
457
|
+
if (
|
|
458
|
+
is_ruby_array
|
|
459
|
+
and not value.startswith("%w")
|
|
460
|
+
and (not value.startswith("["))
|
|
461
|
+
):
|
|
462
|
+
# This was a multiline Ruby array, reconstruct %w syntax
|
|
463
|
+
value = f"%w(\n{value}\n)"
|
|
464
|
+
|
|
465
|
+
# Convert Ruby syntax to YAML-compatible format
|
|
466
|
+
value = _convert_ruby_value_to_yaml(value)
|
|
467
|
+
|
|
468
|
+
attributes.append(
|
|
469
|
+
{
|
|
470
|
+
"precedence": precedence,
|
|
471
|
+
"path": attr_path,
|
|
472
|
+
"value": value,
|
|
473
|
+
}
|
|
474
|
+
)
|
|
475
|
+
else:
|
|
476
|
+
i += 1
|
|
112
477
|
|
|
113
478
|
return attributes
|
|
114
479
|
|
|
@@ -236,8 +601,8 @@ def _format_resolved_attributes(
|
|
|
236
601
|
# Sort by attribute path for consistent output
|
|
237
602
|
for path in sorted(resolved.keys()):
|
|
238
603
|
info = resolved[path]
|
|
239
|
-
result.append(f"
|
|
240
|
-
result.append(f"
|
|
604
|
+
result.append(f"{ATTRIBUTE_PREFIX}{path}")
|
|
605
|
+
result.append(f" {VALUE_PREFIX}{info['value']}")
|
|
241
606
|
result.append(
|
|
242
607
|
f" Precedence: {info['precedence']} (level {info['precedence_level']})"
|
|
243
608
|
)
|
souschef/parsers/recipe.py
CHANGED
|
@@ -38,11 +38,15 @@ def parse_recipe(path: str) -> str:
|
|
|
38
38
|
content = file_path.read_text(encoding="utf-8")
|
|
39
39
|
|
|
40
40
|
resources = _extract_resources(content)
|
|
41
|
+
include_recipes = _extract_include_recipes(content)
|
|
41
42
|
|
|
42
|
-
|
|
43
|
-
|
|
43
|
+
# Combine resources and include_recipes
|
|
44
|
+
all_items = resources + include_recipes
|
|
44
45
|
|
|
45
|
-
|
|
46
|
+
if not all_items:
|
|
47
|
+
return f"Warning: No Chef resources or include_recipe calls found in {path}"
|
|
48
|
+
|
|
49
|
+
return _format_resources(all_items)
|
|
46
50
|
|
|
47
51
|
except ValueError as e:
|
|
48
52
|
return f"Error: {e}"
|
|
@@ -114,6 +118,36 @@ def _extract_resources(content: str) -> list[dict[str, str]]:
|
|
|
114
118
|
return resources
|
|
115
119
|
|
|
116
120
|
|
|
121
|
+
def _extract_include_recipes(content: str) -> list[dict[str, str]]:
|
|
122
|
+
"""
|
|
123
|
+
Extract include_recipe calls from recipe content.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
content: Raw content of recipe file.
|
|
127
|
+
|
|
128
|
+
Returns:
|
|
129
|
+
List of dictionaries containing include_recipe information.
|
|
130
|
+
|
|
131
|
+
"""
|
|
132
|
+
include_recipes = []
|
|
133
|
+
# Strip comments first
|
|
134
|
+
clean_content = _strip_ruby_comments(content)
|
|
135
|
+
|
|
136
|
+
# Match include_recipe calls: include_recipe 'recipe_name'
|
|
137
|
+
pattern = r"include_recipe\s+['\"]([^'\"]+)['\"]"
|
|
138
|
+
|
|
139
|
+
for match in re.finditer(pattern, clean_content):
|
|
140
|
+
recipe_name = match.group(1)
|
|
141
|
+
include_recipes.append(
|
|
142
|
+
{
|
|
143
|
+
"type": "include_recipe",
|
|
144
|
+
"name": recipe_name,
|
|
145
|
+
}
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
return include_recipes
|
|
149
|
+
|
|
150
|
+
|
|
117
151
|
def _extract_conditionals(content: str) -> list[dict[str, Any]]:
|
|
118
152
|
"""
|
|
119
153
|
Extract Ruby conditionals from recipe code.
|
|
@@ -189,12 +223,16 @@ def _format_resources(resources: list[dict[str, Any]]) -> str:
|
|
|
189
223
|
for i, resource in enumerate(resources, 1):
|
|
190
224
|
if i > 1:
|
|
191
225
|
result.append("")
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
result.append(f"
|
|
197
|
-
|
|
198
|
-
result.append(f"
|
|
226
|
+
if resource["type"] == "include_recipe":
|
|
227
|
+
result.append(f"Include Recipe {i}:")
|
|
228
|
+
result.append(f" Recipe: {resource['name']}")
|
|
229
|
+
else:
|
|
230
|
+
result.append(f"Resource {i}:")
|
|
231
|
+
result.append(f" Type: {resource['type']}")
|
|
232
|
+
result.append(f" Name: {resource['name']}")
|
|
233
|
+
if "action" in resource:
|
|
234
|
+
result.append(f" Action: {resource['action']}")
|
|
235
|
+
if "properties" in resource:
|
|
236
|
+
result.append(f" Properties: {resource['properties']}")
|
|
199
237
|
|
|
200
238
|
return "\n".join(result)
|