mcp-souschef 2.1.2__py3-none-any.whl → 2.2.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.
@@ -205,6 +205,32 @@ def _extract_plan_exports(content: str, var_name: str) -> list[dict[str, str]]:
205
205
  return exports
206
206
 
207
207
 
208
+ def _is_quote_blocked(
209
+ ch: str, in_single_quote: bool, in_double_quote: bool, in_backtick: bool
210
+ ) -> bool:
211
+ """Check if a quote character is blocked by other active quotes."""
212
+ if ch == "'":
213
+ return in_double_quote or in_backtick
214
+ if ch == '"':
215
+ return in_single_quote or in_backtick
216
+ if ch == "`":
217
+ return in_single_quote or in_double_quote
218
+ return False
219
+
220
+
221
+ def _toggle_quote(
222
+ ch: str, in_single_quote: bool, in_double_quote: bool, in_backtick: bool
223
+ ) -> tuple[bool, bool, bool]:
224
+ """Toggle the appropriate quote state based on character."""
225
+ if ch == "'":
226
+ return not in_single_quote, in_double_quote, in_backtick
227
+ if ch == '"':
228
+ return in_single_quote, not in_double_quote, in_backtick
229
+ if ch == "`":
230
+ return in_single_quote, in_double_quote, not in_backtick
231
+ return in_single_quote, in_double_quote, in_backtick
232
+
233
+
208
234
  def _update_quote_state(
209
235
  ch: str,
210
236
  in_single_quote: bool,
@@ -213,18 +239,21 @@ def _update_quote_state(
213
239
  escape_next: bool,
214
240
  ) -> tuple[bool, bool, bool, bool]:
215
241
  """Update quote tracking state for shell script parsing."""
242
+ # Handle escape sequences
216
243
  if escape_next:
217
244
  return in_single_quote, in_double_quote, in_backtick, False
218
245
 
219
246
  if ch == "\\":
220
247
  return in_single_quote, in_double_quote, in_backtick, True
221
248
 
222
- if ch == "'" and not in_double_quote and not in_backtick:
223
- return not in_single_quote, in_double_quote, in_backtick, False
224
- if ch == '"' and not in_single_quote and not in_backtick:
225
- return in_single_quote, not in_double_quote, in_backtick, False
226
- if ch == "`" and not in_single_quote and not in_double_quote:
227
- return in_single_quote, in_double_quote, not in_backtick, False
249
+ # Handle quote characters
250
+ if ch in ("'", '"', "`") and not _is_quote_blocked(
251
+ ch, in_single_quote, in_double_quote, in_backtick
252
+ ):
253
+ single, double, backtick = _toggle_quote(
254
+ ch, in_single_quote, in_double_quote, in_backtick
255
+ )
256
+ return single, double, backtick, False
228
257
 
229
258
  return in_single_quote, in_double_quote, in_backtick, False
230
259
 
@@ -616,6 +616,50 @@ def _generate_inspec_package_checks(
616
616
  return lines
617
617
 
618
618
 
619
+ def _generate_inspec_resource_checks(
620
+ resource_type: str,
621
+ resource_name: str,
622
+ properties: dict[str, Any] | None = None,
623
+ custom_checks: list[str] | None = None,
624
+ ) -> list[str]:
625
+ """
626
+ Generate InSpec checks for a resource using a generic pattern.
627
+
628
+ Args:
629
+ resource_type: InSpec resource type (e.g., 'file', 'user', 'service').
630
+ resource_name: Name/path of the resource.
631
+ properties: Optional resource properties to check.
632
+ custom_checks: Optional list of custom check lines.
633
+
634
+ Returns:
635
+ List of InSpec check lines.
636
+
637
+ """
638
+ lines = [f" describe {resource_type}('{resource_name}') do"]
639
+
640
+ # Add custom checks if provided
641
+ if custom_checks:
642
+ lines.extend(custom_checks)
643
+ else:
644
+ # Default: should exist
645
+ lines.append(INSPEC_SHOULD_EXIST)
646
+
647
+ # Add property checks
648
+ if properties:
649
+ property_map = {
650
+ "mode": lambda v: f" its('mode') {{ should cmp '{v}' }}",
651
+ "owner": lambda v: f" its('owner') {{ should eq '{v}' }}",
652
+ "group": lambda v: f" its('group') {{ should eq '{v}' }}",
653
+ "shell": lambda v: f" its('shell') {{ should eq '{v}' }}",
654
+ }
655
+ for prop, value in properties.items():
656
+ if prop in property_map:
657
+ lines.append(property_map[prop](value))
658
+
659
+ lines.append(INSPEC_END_INDENT)
660
+ return lines
661
+
662
+
619
663
  def _generate_inspec_service_checks(resource_name: str) -> list[str]:
620
664
  """
621
665
  Generate InSpec checks for service resource.
@@ -627,12 +671,14 @@ def _generate_inspec_service_checks(resource_name: str) -> list[str]:
627
671
  List of InSpec check lines.
628
672
 
629
673
  """
630
- return [
631
- f" describe service('{resource_name}') do",
632
- " it { should be_running }",
633
- " it { should be_enabled }",
634
- INSPEC_END_INDENT,
635
- ]
674
+ return _generate_inspec_resource_checks(
675
+ "service",
676
+ resource_name,
677
+ custom_checks=[
678
+ " it { should be_running }",
679
+ " it { should be_enabled }",
680
+ ],
681
+ )
636
682
 
637
683
 
638
684
  def _generate_inspec_file_checks(
@@ -649,15 +695,11 @@ def _generate_inspec_file_checks(
649
695
  List of InSpec check lines.
650
696
 
651
697
  """
652
- lines = [f" describe file('{resource_name}') do", INSPEC_SHOULD_EXIST]
653
- if "mode" in properties:
654
- lines.append(f" its('mode') {{ should cmp '{properties['mode']}' }}")
655
- if "owner" in properties:
656
- lines.append(f" its('owner') {{ should eq '{properties['owner']}' }}")
657
- if "group" in properties:
658
- lines.append(f" its('group') {{ should eq '{properties['group']}' }}")
659
- lines.append(INSPEC_END_INDENT)
660
- return lines
698
+ return _generate_inspec_resource_checks(
699
+ "file",
700
+ resource_name,
701
+ properties=properties,
702
+ )
661
703
 
662
704
 
663
705
  def _generate_inspec_directory_checks(
@@ -674,15 +716,15 @@ def _generate_inspec_directory_checks(
674
716
  List of InSpec check lines.
675
717
 
676
718
  """
677
- lines = [
678
- f" describe file('{resource_name}') do",
679
- INSPEC_SHOULD_EXIST,
680
- " it { should be_directory }",
681
- ]
682
- if "mode" in properties:
683
- lines.append(f" its('mode') {{ should cmp '{properties['mode']}' }}")
684
- lines.append(INSPEC_END_INDENT)
685
- return lines
719
+ return _generate_inspec_resource_checks(
720
+ "file",
721
+ resource_name,
722
+ properties=properties,
723
+ custom_checks=[
724
+ INSPEC_SHOULD_EXIST,
725
+ " it { should be_directory }",
726
+ ],
727
+ )
686
728
 
687
729
 
688
730
  def _generate_inspec_user_checks(
@@ -699,11 +741,11 @@ def _generate_inspec_user_checks(
699
741
  List of InSpec check lines.
700
742
 
701
743
  """
702
- lines = [f" describe user('{resource_name}') do", INSPEC_SHOULD_EXIST]
703
- if "shell" in properties:
704
- lines.append(f" its('shell') {{ should eq '{properties['shell']}' }}")
705
- lines.append(INSPEC_END_INDENT)
706
- return lines
744
+ return _generate_inspec_resource_checks(
745
+ "user",
746
+ resource_name,
747
+ properties=properties,
748
+ )
707
749
 
708
750
 
709
751
  def _generate_inspec_group_checks(resource_name: str) -> list[str]:
@@ -717,11 +759,7 @@ def _generate_inspec_group_checks(resource_name: str) -> list[str]:
717
759
  List of InSpec check lines.
718
760
 
719
761
  """
720
- return [
721
- f" describe group('{resource_name}') do",
722
- INSPEC_SHOULD_EXIST,
723
- INSPEC_END_INDENT,
724
- ]
762
+ return _generate_inspec_resource_checks("group", resource_name)
725
763
 
726
764
 
727
765
  def _generate_inspec_from_resource(
@@ -45,6 +45,64 @@ def read_cookbook_metadata(path: str) -> str:
45
45
  return f"An error occurred: {e}"
46
46
 
47
47
 
48
+ def _scan_cookbook_directory(
49
+ cookbook_path, dir_name: str
50
+ ) -> tuple[str, list[str]] | None:
51
+ """
52
+ Scan a single cookbook directory for files.
53
+
54
+ Args:
55
+ cookbook_path: Path to the cookbook root.
56
+ dir_name: Name of the subdirectory to scan.
57
+
58
+ Returns:
59
+ Tuple of (dir_name, files) if directory exists and has files, None otherwise.
60
+
61
+ """
62
+ dir_path = _safe_join(cookbook_path, dir_name)
63
+ if not dir_path.exists() or not dir_path.is_dir():
64
+ return None
65
+
66
+ files = [f.name for f in dir_path.iterdir() if f.is_file()]
67
+ return (dir_name, files) if files else None
68
+
69
+
70
+ def _collect_cookbook_structure(cookbook_path) -> dict[str, list[str]]:
71
+ """
72
+ Collect all standard cookbook directories and their files.
73
+
74
+ Args:
75
+ cookbook_path: Path to the cookbook root.
76
+
77
+ Returns:
78
+ Dictionary mapping directory names to file lists.
79
+
80
+ """
81
+ structure = {}
82
+ common_dirs = [
83
+ "recipes",
84
+ "attributes",
85
+ "templates",
86
+ "files",
87
+ "resources",
88
+ "providers",
89
+ "libraries",
90
+ "definitions",
91
+ ]
92
+
93
+ for dir_name in common_dirs:
94
+ result = _scan_cookbook_directory(cookbook_path, dir_name)
95
+ if result:
96
+ structure[result[0]] = result[1]
97
+
98
+ # Check for metadata.rb
99
+ metadata_path = _safe_join(cookbook_path, METADATA_FILENAME)
100
+ if metadata_path.exists():
101
+ structure["metadata"] = [METADATA_FILENAME]
102
+
103
+ return structure
104
+
105
+
48
106
  def list_cookbook_structure(path: str) -> str:
49
107
  """
50
108
  List the structure of a Chef cookbook directory.
@@ -62,29 +120,7 @@ def list_cookbook_structure(path: str) -> str:
62
120
  if not cookbook_path.is_dir():
63
121
  return f"Error: {path} is not a directory"
64
122
 
65
- structure = {}
66
- common_dirs = [
67
- "recipes",
68
- "attributes",
69
- "templates",
70
- "files",
71
- "resources",
72
- "providers",
73
- "libraries",
74
- "definitions",
75
- ]
76
-
77
- for dir_name in common_dirs:
78
- dir_path = _safe_join(cookbook_path, dir_name)
79
- if dir_path.exists() and dir_path.is_dir():
80
- files = [f.name for f in dir_path.iterdir() if f.is_file()]
81
- if files:
82
- structure[dir_name] = files
83
-
84
- # Check for metadata.rb
85
- metadata_path = _safe_join(cookbook_path, METADATA_FILENAME)
86
- if metadata_path.exists():
87
- structure["metadata"] = [METADATA_FILENAME]
123
+ structure = _collect_cookbook_structure(cookbook_path)
88
124
 
89
125
  if not structure:
90
126
  return f"Warning: No standard cookbook structure found in {path}"