cfengine 0.18.0__tar.gz → 0.19.0__tar.gz

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.
Files changed (170) hide show
  1. {cfengine-0.18.0 → cfengine-0.19.0}/PKG-INFO +1 -1
  2. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine.egg-info/PKG-INFO +1 -1
  3. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine.egg-info/SOURCES.txt +8 -0
  4. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine.egg-info/scm_file_list.json +8 -0
  5. cfengine-0.19.0/src/cfengine.egg-info/scm_version.json +8 -0
  6. cfengine-0.19.0/src/cfengine_cli/cfengine_wrapper/cfengine_commands.py +44 -0
  7. cfengine-0.19.0/src/cfengine_cli/cfengine_wrapper/cfengine_objects.py +120 -0
  8. cfengine-0.19.0/src/cfengine_cli/cfengine_wrapper/cfengine_utils.py +191 -0
  9. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/commands.py +0 -39
  10. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/deptool.py +2 -0
  11. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/docs.py +6 -2
  12. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/lint.py +33 -10
  13. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/main.py +166 -7
  14. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/syntax-description.json +121 -0
  15. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/004_block_type.expected.txt +1 -1
  16. cfengine-0.19.0/tests/lint/020_missing_braces.expected.txt +7 -0
  17. cfengine-0.19.0/tests/lint/020_missing_braces.x.cf +15 -0
  18. cfengine-0.19.0/tests/lint/020_missing_semicolon.expected.txt +7 -0
  19. cfengine-0.19.0/tests/lint/020_missing_semicolon.x.cf +18 -0
  20. cfengine-0.19.0/tests/unit/__init__.py +0 -0
  21. cfengine-0.18.0/src/cfengine.egg-info/scm_version.json +0 -8
  22. {cfengine-0.18.0 → cfengine-0.19.0}/.github/dependabot.yml +0 -0
  23. {cfengine-0.18.0 → cfengine-0.19.0}/.github/workflows/lint-policy-in-other-repos.yml +0 -0
  24. {cfengine-0.18.0 → cfengine-0.19.0}/.github/workflows/make-check.yml +0 -0
  25. {cfengine-0.18.0 → cfengine-0.19.0}/.github/workflows/pypi-publish.yml +0 -0
  26. {cfengine-0.18.0 → cfengine-0.19.0}/.github/workflows/stale.yml +0 -0
  27. {cfengine-0.18.0 → cfengine-0.19.0}/.github/workflows/update-syntax-description.yml +0 -0
  28. {cfengine-0.18.0 → cfengine-0.19.0}/.gitignore +0 -0
  29. {cfengine-0.18.0 → cfengine-0.19.0}/.python-version +0 -0
  30. {cfengine-0.18.0 → cfengine-0.19.0}/CLAUDE.md +0 -0
  31. {cfengine-0.18.0 → cfengine-0.19.0}/HACKING.md +0 -0
  32. {cfengine-0.18.0 → cfengine-0.19.0}/LICENSE +0 -0
  33. {cfengine-0.18.0 → cfengine-0.19.0}/Makefile +0 -0
  34. {cfengine-0.18.0 → cfengine-0.19.0}/README.md +0 -0
  35. {cfengine-0.18.0 → cfengine-0.19.0}/ci/01-install.sh +0 -0
  36. {cfengine-0.18.0 → cfengine-0.19.0}/ci/02-safe-tests.sh +0 -0
  37. {cfengine-0.18.0 → cfengine-0.19.0}/ci/03-unsafe-tests.sh +0 -0
  38. {cfengine-0.18.0 → cfengine-0.19.0}/pyproject.toml +0 -0
  39. {cfengine-0.18.0 → cfengine-0.19.0}/setup.cfg +0 -0
  40. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine.egg-info/dependency_links.txt +0 -0
  41. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine.egg-info/entry_points.txt +0 -0
  42. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine.egg-info/requires.txt +0 -0
  43. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine.egg-info/top_level.txt +0 -0
  44. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/__init__.py +0 -0
  45. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/__main__.py +0 -0
  46. {cfengine-0.18.0/src/cfengine_cli/masterfiles → cfengine-0.19.0/src/cfengine_cli/cfengine_wrapper}/__init__.py +0 -0
  47. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/changelog.py +0 -0
  48. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/deptool-README.md +0 -0
  49. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/dev.py +0 -0
  50. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/format.py +0 -0
  51. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/lint_csv.py +0 -0
  52. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/lint_yml.py +0 -0
  53. {cfengine-0.18.0/tests/unit → cfengine-0.19.0/src/cfengine_cli/masterfiles}/__init__.py +0 -0
  54. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/masterfiles/analyze.py +0 -0
  55. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/masterfiles/check_download_matches_git.py +0 -0
  56. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/masterfiles/download.py +0 -0
  57. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/masterfiles/generate_git_tags.py +0 -0
  58. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/masterfiles/generate_release_information.py +0 -0
  59. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/masterfiles/generate_vcf_download.py +0 -0
  60. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/masterfiles/generate_vcf_git_checkout.py +0 -0
  61. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/paths.py +0 -0
  62. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/profile.py +0 -0
  63. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/shell.py +0 -0
  64. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/syntax_tree.py +0 -0
  65. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/up.py +0 -0
  66. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/utils.py +0 -0
  67. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/validate.py +0 -0
  68. {cfengine-0.18.0 → cfengine-0.19.0}/src/cfengine_cli/version.py +0 -0
  69. {cfengine-0.18.0 → cfengine-0.19.0}/tests/README.md +0 -0
  70. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/001_hello_world.expected.cf +0 -0
  71. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/001_hello_world.input.cf +0 -0
  72. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/002_basics.expected.cf +0 -0
  73. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/002_basics.input.cf +0 -0
  74. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/003_wrapping.expected.cf +0 -0
  75. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/003_wrapping.input.cf +0 -0
  76. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/004_comments.expected.cf +0 -0
  77. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/004_comments.input.cf +0 -0
  78. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/005_bundle_comments.expected.cf +0 -0
  79. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/005_bundle_comments.input.cf +0 -0
  80. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/006_remove_empty_comments.expected.cf +0 -0
  81. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/006_remove_empty_comments.input.cf +0 -0
  82. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/007_class_guarded_empty_lines.expected.cf +0 -0
  83. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/007_class_guarded_empty_lines.input.cf +0 -0
  84. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/008_long_string.expected.cf +0 -0
  85. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/008_long_string.input.cf +0 -0
  86. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/009_single_line.expected.cf +0 -0
  87. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/009_single_line.input.cf +0 -0
  88. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/010_stakeholder.expected.cf +0 -0
  89. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/010_stakeholder.input.cf +0 -0
  90. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/011_macros.expected.cf +0 -0
  91. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/011_macros.input.cf +0 -0
  92. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/011_promises.expected.cf +0 -0
  93. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/011_promises.input.cf +0 -0
  94. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/012_sameline_comments.expected.cf +0 -0
  95. {cfengine-0.18.0 → cfengine-0.19.0}/tests/format/012_sameline_comments.input.cf +0 -0
  96. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/001_hello_world.cf +0 -0
  97. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/002_deprecations.expected.txt +0 -0
  98. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/002_deprecations.x.cf +0 -0
  99. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/003_name_lowercase.cf +0 -0
  100. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/003_name_lowercase.expected.txt +0 -0
  101. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/003_name_lowercase.x.cf +0 -0
  102. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/004_block_type.cf +0 -0
  103. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/004_block_type.x.cf +0 -0
  104. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/005_syntax_error.cf +0 -0
  105. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/005_syntax_error.expected.txt +0 -0
  106. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/005_syntax_error.x.cf +0 -0
  107. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/006_empty_file.expected.txt +0 -0
  108. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/006_empty_file.x.cf +0 -0
  109. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/007_namespace.cf +0 -0
  110. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/007_namespace.expected.txt +0 -0
  111. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/007_namespace.x.cf +0 -0
  112. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/008_bundle_shadows_function.cf +0 -0
  113. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/008_bundle_shadows_function.expected.txt +0 -0
  114. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/008_bundle_shadows_function.x.cf +0 -0
  115. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/009_unknown_function_inside_vars.cf +0 -0
  116. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/009_unknown_function_inside_vars.expected.txt +0 -0
  117. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/009_unknown_function_inside_vars.x.cf +0 -0
  118. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/010_mutually_exclusive_types_vars.cf +0 -0
  119. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/010_mutually_exclusive_types_vars.expected.txt +0 -0
  120. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/010_mutually_exclusive_types_vars.x.cf +0 -0
  121. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/011_invalid_attributes.expected.txt +0 -0
  122. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/011_invalid_attributes.x.cf +0 -0
  123. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/012_function_call_arg_count.cf +0 -0
  124. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/012_function_call_arg_count.expected.txt +0 -0
  125. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/012_function_call_arg_count.x.cf +0 -0
  126. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/013_num_args_body.expected.txt +0 -0
  127. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/013_num_args_body.x.cf +0 -0
  128. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/013_num_args_bundle.expected.txt +0 -0
  129. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/013_num_args_bundle.x.cf +0 -0
  130. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/013_num_args_bundle_body.cf +0 -0
  131. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/014_variadic_func_arg_count.cf +0 -0
  132. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/014_variadic_func_arg_count.expected.txt +0 -0
  133. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/014_variadic_func_arg_count.x.cf +0 -0
  134. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/015_macro_multi_def_bundle.cf +0 -0
  135. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/015_macro_multi_def_bundle.expected.txt +0 -0
  136. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/015_macro_multi_def_bundle.x.cf +0 -0
  137. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/016_half_promises.cf +0 -0
  138. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/016_half_promises.expected.txt +0 -0
  139. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/016_half_promises.x.cf +0 -0
  140. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/017_implies_body.cf +0 -0
  141. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/017_implies_body.expected.txt +0 -0
  142. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/017_implies_body.x.cf +0 -0
  143. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/018_nested_calls.cf +0 -0
  144. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/018_nested_calls.expected.txt +0 -0
  145. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/018_nested_calls.x.cf +0 -0
  146. {cfengine-0.18.0 → cfengine-0.19.0}/tests/lint/019_bundle_name_expansion.cf +0 -0
  147. {cfengine-0.18.0 → cfengine-0.19.0}/tests/run-format-tests.sh +0 -0
  148. {cfengine-0.18.0 → cfengine-0.19.0}/tests/run-lint-tests.sh +0 -0
  149. {cfengine-0.18.0 → cfengine-0.19.0}/tests/run-shell-tests.sh +0 -0
  150. {cfengine-0.18.0 → cfengine-0.19.0}/tests/run-up-validate-tests.sh +0 -0
  151. {cfengine-0.18.0 → cfengine-0.19.0}/tests/shell/001-help.sh +0 -0
  152. {cfengine-0.18.0 → cfengine-0.19.0}/tests/shell/002-version.sh +0 -0
  153. {cfengine-0.18.0 → cfengine-0.19.0}/tests/shell/003-format.sh +0 -0
  154. {cfengine-0.18.0 → cfengine-0.19.0}/tests/shell/004-format-check.sh +0 -0
  155. {cfengine-0.18.0 → cfengine-0.19.0}/tests/shell/005-lint.sh +0 -0
  156. {cfengine-0.18.0 → cfengine-0.19.0}/tests/unit/test_changelog.py +0 -0
  157. {cfengine-0.18.0 → cfengine-0.19.0}/tests/unit/test_deps.py +0 -0
  158. {cfengine-0.18.0 → cfengine-0.19.0}/tests/unit/test_format.py +0 -0
  159. {cfengine-0.18.0 → cfengine-0.19.0}/tests/unit/test_lint_csv.py +0 -0
  160. {cfengine-0.18.0 → cfengine-0.19.0}/tests/unit/test_lint_yml.py +0 -0
  161. {cfengine-0.18.0 → cfengine-0.19.0}/tests/unit/test_paths.py +0 -0
  162. {cfengine-0.18.0 → cfengine-0.19.0}/tests/unit/test_utils.py +0 -0
  163. {cfengine-0.18.0 → cfengine-0.19.0}/tests/unit/test_version.py +0 -0
  164. {cfengine-0.18.0 → cfengine-0.19.0}/tests/up-validate/001_complex_config.yaml +0 -0
  165. {cfengine-0.18.0 → cfengine-0.19.0}/tests/up-validate/002_version.x.yaml +0 -0
  166. {cfengine-0.18.0 → cfengine-0.19.0}/tests/up-validate/003_box.x.yaml +0 -0
  167. {cfengine-0.18.0 → cfengine-0.19.0}/tests/up-validate/004_count.x.yaml +0 -0
  168. {cfengine-0.18.0 → cfengine-0.19.0}/tests/up-validate/mocks/cloud_config.json +0 -0
  169. {cfengine-0.18.0 → cfengine-0.19.0}/tests/up-validate/mocks/vagrant +0 -0
  170. {cfengine-0.18.0 → cfengine-0.19.0}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cfengine
3
- Version: 0.18.0
3
+ Version: 0.19.0
4
4
  Summary: Human-oriented CLI for interacting with CFEngine policy language, projects, and other CFEngine tools
5
5
  License: GNU GENERAL PUBLIC LICENSE
6
6
  Version 3, 29 June 2007
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: cfengine
3
- Version: 0.18.0
3
+ Version: 0.19.0
4
4
  Summary: Human-oriented CLI for interacting with CFEngine policy language, projects, and other CFEngine tools
5
5
  License: GNU GENERAL PUBLIC LICENSE
6
6
  Version 3, 29 June 2007
@@ -46,6 +46,10 @@ src/cfengine_cli/up.py
46
46
  src/cfengine_cli/utils.py
47
47
  src/cfengine_cli/validate.py
48
48
  src/cfengine_cli/version.py
49
+ src/cfengine_cli/cfengine_wrapper/__init__.py
50
+ src/cfengine_cli/cfengine_wrapper/cfengine_commands.py
51
+ src/cfengine_cli/cfengine_wrapper/cfengine_objects.py
52
+ src/cfengine_cli/cfengine_wrapper/cfengine_utils.py
49
53
  src/cfengine_cli/masterfiles/__init__.py
50
54
  src/cfengine_cli/masterfiles/analyze.py
51
55
  src/cfengine_cli/masterfiles/check_download_matches_git.py
@@ -137,6 +141,10 @@ tests/lint/018_nested_calls.cf
137
141
  tests/lint/018_nested_calls.expected.txt
138
142
  tests/lint/018_nested_calls.x.cf
139
143
  tests/lint/019_bundle_name_expansion.cf
144
+ tests/lint/020_missing_braces.expected.txt
145
+ tests/lint/020_missing_braces.x.cf
146
+ tests/lint/020_missing_semicolon.expected.txt
147
+ tests/lint/020_missing_semicolon.x.cf
140
148
  tests/shell/001-help.sh
141
149
  tests/shell/002-version.sh
142
150
  tests/shell/003-format.sh
@@ -31,6 +31,10 @@
31
31
  "src/cfengine_cli/__main__.py",
32
32
  "src/cfengine_cli/paths.py",
33
33
  "src/cfengine_cli/dev.py",
34
+ "src/cfengine_cli/cfengine_wrapper/cfengine_commands.py",
35
+ "src/cfengine_cli/cfengine_wrapper/cfengine_objects.py",
36
+ "src/cfengine_cli/cfengine_wrapper/cfengine_utils.py",
37
+ "src/cfengine_cli/cfengine_wrapper/__init__.py",
34
38
  "src/cfengine_cli/masterfiles/check_download_matches_git.py",
35
39
  "src/cfengine_cli/masterfiles/generate_vcf_git_checkout.py",
36
40
  "src/cfengine_cli/masterfiles/__init__.py",
@@ -49,6 +53,7 @@
49
53
  "tests/run-format-tests.sh",
50
54
  "tests/lint/015_macro_multi_def_bundle.cf",
51
55
  "tests/lint/010_mutually_exclusive_types_vars.cf",
56
+ "tests/lint/020_missing_semicolon.x.cf",
52
57
  "tests/lint/010_mutually_exclusive_types_vars.x.cf",
53
58
  "tests/lint/013_num_args_body.expected.txt",
54
59
  "tests/lint/007_namespace.x.cf",
@@ -59,7 +64,9 @@
59
64
  "tests/lint/009_unknown_function_inside_vars.cf",
60
65
  "tests/lint/008_bundle_shadows_function.expected.txt",
61
66
  "tests/lint/013_num_args_bundle.x.cf",
67
+ "tests/lint/020_missing_semicolon.expected.txt",
62
68
  "tests/lint/017_implies_body.cf",
69
+ "tests/lint/020_missing_braces.expected.txt",
63
70
  "tests/lint/017_implies_body.expected.txt",
64
71
  "tests/lint/008_bundle_shadows_function.cf",
65
72
  "tests/lint/003_name_lowercase.x.cf",
@@ -83,6 +90,7 @@
83
90
  "tests/lint/011_invalid_attributes.x.cf",
84
91
  "tests/lint/018_nested_calls.cf",
85
92
  "tests/lint/014_variadic_func_arg_count.cf",
93
+ "tests/lint/020_missing_braces.x.cf",
86
94
  "tests/lint/017_implies_body.x.cf",
87
95
  "tests/lint/012_function_call_arg_count.x.cf",
88
96
  "tests/lint/007_namespace.expected.txt",
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "0.19.0",
3
+ "distance": 0,
4
+ "node": "gef5f6e5f16cb63fc1fab7cec47207a9bb839a9d5",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-07-15"
8
+ }
@@ -0,0 +1,44 @@
1
+ from cfbs.commands import build_command
2
+ from cf_remote.commands import deploy as deploy_command
3
+ from cf_remote.commands import install as install_command
4
+ from cf_remote.commands import destroy as destroy_command
5
+
6
+ from cfengine_cli.cfengine_wrapper.cfengine_utils import (
7
+ require_executable,
8
+ require_installation,
9
+ )
10
+
11
+
12
+ def report(target: str | None = None) -> int: # TODO? ENT-14122
13
+ installation = require_installation(target)
14
+ rc = installation.agent.run("-KIf update.cf", "-KI")
15
+ if rc != 0:
16
+ return rc
17
+ return installation.hub.run(
18
+ "--query rebase -H 127.0.0.1", "--query delta -H 127.0.0.1"
19
+ )
20
+
21
+
22
+ def run(*args, target: str | None = None) -> int:
23
+ agent = require_executable("cf-agent", target)
24
+ if args:
25
+ return agent.run(*args)
26
+ return agent.run("-KIf update.cf", "-KI")
27
+
28
+
29
+ def install() -> int: # TODO ENT-14117
30
+ return install_command(None, None)
31
+
32
+
33
+ def destroy(groupname, del_all=False) -> int:
34
+ if del_all:
35
+ return destroy_command(None)
36
+ return destroy_command(groupname)
37
+
38
+
39
+ def build() -> int: # TODO ENT-14119
40
+ return build_command()
41
+
42
+
43
+ def deploy() -> int: # TODO ENT-14119
44
+ return deploy_command(None, None)
@@ -0,0 +1,120 @@
1
+ from dataclasses import dataclass
2
+ from cf_remote.remote import run_command
3
+ import subprocess
4
+ import logging
5
+ import os
6
+
7
+
8
+ def _ensure_default_agent_flags(command: str) -> str:
9
+ """
10
+ cf-agent needs -K (no-lock), -I (inform), and -f (specify
11
+ file) to actually run against a policy file the way people expect.
12
+ If `command` has no flags at all -- e.g. someone just ran
13
+ `cfengine run mypolicy.cf` -- prepend "-KIf" so a bare filename works.
14
+
15
+ Deliberately does NOT try to detect and fill in individual missing
16
+ letters (e.g. leaving -f out of "-KI somefile.cf")
17
+ If any flag is present at all, we assume the invocation was deliberate and
18
+ leave it exactly as given.
19
+ """
20
+ tokens = command.split()
21
+ has_flags = any(t.startswith("-") for t in tokens)
22
+ if has_flags:
23
+ return command
24
+ return f"-KIf {command}".strip()
25
+
26
+
27
+ class Executable:
28
+ """
29
+ A single binary (cf-agent or cf-hub) at a known location -- either
30
+ "local" or a remote host identifier ("user@ip"). Knows its own path
31
+ and how to run a command against itself, whether that means a local
32
+ subprocess or an SSH call via cf-remote.
33
+ """
34
+
35
+ def __init__(self, name: str, location: str, path: str, aliases=None) -> None:
36
+ self.name = name # "cf-agent" / "cf-hub"
37
+ self.location = location # "local" or "user@ip"
38
+ self.path = path # absolute path to the binary at that location
39
+ self.aliases = (
40
+ aliases or []
41
+ ) # friendly names from cf-remote's saved state, e.g. "hub", "local", "remote"
42
+
43
+ @property
44
+ def is_local(self) -> bool:
45
+ return self.location == "local"
46
+
47
+ @property
48
+ def label(self) -> str:
49
+ """Best human-facing identifier: a friendly alias if we have one, else the location."""
50
+ if self.aliases:
51
+ return f"{self.aliases[0]} ({self.location})"
52
+ return self.location
53
+
54
+ def run(self, *commands) -> int:
55
+ rc = 0
56
+ for command in commands:
57
+ rc = self._run_one(command)
58
+ if rc != 0:
59
+ return rc
60
+ return rc
61
+
62
+ def _run_one(self, command: str) -> int:
63
+ if self.name == "cf-agent":
64
+ command = _ensure_default_agent_flags(command)
65
+
66
+ if self.is_local:
67
+ args = [self.path] + command.split()
68
+ # cf-agent picks its workdir based on privilege: as root it uses
69
+ # /var/cfengine/inputs (where policy actually lives); as a
70
+ # regular user it falls back to ~/.cfagent/inputs instead,
71
+ # which won't have update.cf. Elevate to match the remote path,
72
+ # which already runs everything via sudo.
73
+ if os.geteuid() != 0:
74
+ args = ["sudo"] + args
75
+ result = subprocess.run(args)
76
+ return result.returncode
77
+
78
+ full_command = f"{self.path} {command}"
79
+ logging.warning(f"Executing command {full_command} on {self.location}")
80
+ output = run_command(self.location, full_command, sudo=True)
81
+ if (
82
+ output is None
83
+ ): # TODO: Test this, I've had some policy failing but returning output instead or error-code (I think)
84
+ # Error already logged in run_command
85
+ return 1
86
+ if output:
87
+ print(output)
88
+ return 0
89
+
90
+ def __repr__(self) -> str:
91
+ return f"<Executable {self.name} @ {self.location}>"
92
+
93
+
94
+ @dataclass
95
+ class Installation:
96
+ """
97
+ One coherent system that has BOTH cf-agent and cf-hub, so callers
98
+ that need both (e.g. report()) can pick one location and get a
99
+ matched pair, rather than resolving each binary independently.
100
+ """
101
+
102
+ location: str
103
+ agent: Executable
104
+ hub: Executable
105
+
106
+ @property
107
+ def is_local(self) -> bool:
108
+ return self.location == "local"
109
+
110
+ @property
111
+ def aliases(self) -> list:
112
+ # agent and hub are always built from the same alias list for a
113
+ # given host (see _find_all_paired), so either would do here.
114
+ return self.agent.aliases
115
+
116
+ @property
117
+ def label(self) -> str:
118
+ if self.aliases:
119
+ return f"{self.aliases[0]} ({self.location})"
120
+ return self.location
@@ -0,0 +1,191 @@
1
+ import os
2
+ import shutil
3
+ import logging
4
+ import sys
5
+
6
+ from collections.abc import Iterator
7
+ from cfengine_cli.paths import bin
8
+ from cfengine_cli.utils import UserError
9
+ from cfengine_cli.cfengine_wrapper.cfengine_objects import Executable, Installation
10
+ from cf_remote.remote import get_info
11
+ from cf_remote.paths import CLOUD_STATE_FPATH
12
+ from cf_remote.utils import read_json
13
+
14
+
15
+ def _find_local_path(binary_name: str) -> str | None:
16
+ candidate = bin(binary_name)
17
+ if os.path.isfile(candidate) and os.access(candidate, os.X_OK):
18
+ return candidate
19
+ return shutil.which(binary_name)
20
+
21
+
22
+ def _known_hosts(role_filter=None) -> Iterator[tuple[str, list[str]]]:
23
+ """
24
+ Yields (host_id, aliases) from cf-remote's saved/spawned VM state --
25
+ host_id is "user@ip" (same source _get_hubs() in cf_remote.commands
26
+ reads from); aliases are the friendly names cf-remote knows this host
27
+ by: the group name it was saved/spawned under (e.g. "hub", "hub2")
28
+ and its per-host key within that group. Optionally filter by
29
+ vm["role"] (e.g. "hub" or "client").
30
+ """
31
+ if not os.path.exists(CLOUD_STATE_FPATH):
32
+ return
33
+ vms_info = read_json(CLOUD_STATE_FPATH)
34
+ if not vms_info:
35
+ return
36
+ for group_name, group in vms_info.items():
37
+ alias_group = group_name.lstrip("@")
38
+ for host_key, vm in group.items():
39
+ if host_key == "meta":
40
+ continue
41
+ if role_filter and vm.get("role") != role_filter:
42
+ continue
43
+ host_id = "{}@{}".format(vm["user"], vm["public_ips"][0])
44
+ aliases = [alias_group]
45
+ if host_key != alias_group:
46
+ aliases.append(host_key)
47
+ yield host_id, aliases
48
+
49
+
50
+ def _find_all(binary_name: str) -> list[Executable]:
51
+ """Every location -- local, plus every matching remote host -- with `binary_name` installed."""
52
+ executables = []
53
+
54
+ local_path = _find_local_path(binary_name)
55
+ if local_path:
56
+ executables.append(Executable(binary_name, "local", local_path))
57
+
58
+ role_filter = "hub" if binary_name == "cf-hub" else None
59
+ key = "agent" if binary_name == "cf-agent" else "hub"
60
+ for host, aliases in _known_hosts(role_filter=role_filter):
61
+ try:
62
+ data = get_info(host)
63
+ except (Exception, SystemExit) as e:
64
+ """Need to catch SystemExit as cf-remote's get_info() will SystemExit if
65
+ any ssh-connections does not work, for our case we still want to fetch
66
+ the ones that are up in case the user wants to use a different host"""
67
+ logging.warning(f"Skipping {host}: {e}")
68
+ continue
69
+ if not data:
70
+ continue
71
+ binary_path = data.get(key)
72
+ if binary_path:
73
+ executables.append(
74
+ Executable(binary_name, host, binary_path, aliases=aliases)
75
+ )
76
+
77
+ return executables
78
+
79
+
80
+ def _find_all_paired() -> list[Installation]:
81
+ """Every location -- local or remote -- that has BOTH cf-agent and cf-hub."""
82
+ installations = []
83
+
84
+ local_agent_path = _find_local_path("cf-agent")
85
+ local_hub_path = _find_local_path("cf-hub")
86
+ if local_agent_path and local_hub_path:
87
+ installations.append(
88
+ Installation(
89
+ location="local",
90
+ agent=Executable("cf-agent", "local", local_agent_path),
91
+ hub=Executable("cf-hub", "local", local_hub_path),
92
+ )
93
+ )
94
+
95
+ for host, aliases in _known_hosts(role_filter="hub"):
96
+ try:
97
+ data = get_info(host)
98
+ except Exception:
99
+ continue
100
+ if not data:
101
+ continue
102
+ agent_path = data.get("agent")
103
+ hub_path = data.get("hub")
104
+ if agent_path and hub_path:
105
+ installations.append(
106
+ Installation(
107
+ location=host,
108
+ agent=Executable("cf-agent", host, agent_path, aliases=aliases),
109
+ hub=Executable("cf-hub", host, hub_path, aliases=aliases),
110
+ )
111
+ )
112
+
113
+ return installations
114
+
115
+
116
+ def _prompt_choice(candidates, description):
117
+ if not sys.stdin.isatty():
118
+ labels = ", ".join(c.label for c in candidates)
119
+ raise UserError(
120
+ f"Multiple installations of {description} found ({labels}) "
121
+ f"and no terminal to prompt on. Specify one with --host."
122
+ )
123
+ print(f"Multiple installations of {description} found:")
124
+ for i, c in enumerate(candidates, 1):
125
+ print(f" {i}) {c.label}")
126
+ while True:
127
+ choice = input(f"Select which to use [1-{len(candidates)}]: ").strip()
128
+ if choice.isdigit() and 1 <= int(choice) <= len(candidates):
129
+ return candidates[int(choice) - 1]
130
+ print("Invalid selection, try again.")
131
+
132
+
133
+ def _exact_match(candidate, target: str) -> bool:
134
+ return target == candidate.location or target in candidate.aliases
135
+
136
+
137
+ def _loose_match(candidate, target: str) -> bool:
138
+ """Substring match, for when nothing matched exactly."""
139
+ if target in candidate.location:
140
+ return True
141
+ return any(target in alias for alias in candidate.aliases)
142
+
143
+
144
+ def _select(candidates, description, target: str | None = None):
145
+ """
146
+ Picks one candidate (an Executable or Installation) out of a list:
147
+ - none found -> UserError
148
+ - `target` given -> match against location OR any alias (exact, or
149
+ substring -- an IP, username, or friendly name like "hub"),
150
+ UserError on no match
151
+ - exactly one -> use it, no prompt
152
+ - multiple, no target -> interactive prompt
153
+ """
154
+ if not candidates:
155
+ raise UserError(
156
+ f"Could not find {description} locally or on any configured remote host."
157
+ )
158
+
159
+ if target:
160
+ matches = [c for c in candidates if _exact_match(c, target)]
161
+ if not matches:
162
+ matches = [c for c in candidates if _loose_match(c, target)]
163
+ if not matches:
164
+ available = ", ".join(c.label for c in candidates)
165
+ raise UserError(
166
+ f"No installation of {description} matches '{target}'. Available: {available}"
167
+ )
168
+ if len(matches) == 1:
169
+ return matches[0]
170
+ return _prompt_choice(matches, description)
171
+
172
+ if len(candidates) == 1:
173
+ return candidates[0]
174
+
175
+ return _prompt_choice(candidates, description)
176
+
177
+
178
+ def require_executable(name: str, target: str | None = None) -> Executable:
179
+ chosen = _select(_find_all(name), name, target)
180
+ logging.warning(
181
+ f"Using {'local' if chosen.is_local else 'remote'} installation of {name} ({chosen.label})"
182
+ )
183
+ return chosen
184
+
185
+
186
+ def require_installation(target: str | None = None) -> Installation:
187
+ chosen = _select(_find_all_paired(), "cf-agent + cf-hub", target)
188
+ logging.warning(
189
+ f"Using {'local' if chosen.is_local else 'remote'} installation of cf-agent and cf-hub ({chosen.label})"
190
+ )
191
+ return chosen
@@ -5,28 +5,14 @@ import yaml
5
5
  from cfengine_cli.profile import profile_cfengine, generate_callstack
6
6
  from cfengine_cli.dev import dispatch_dev_subcommand
7
7
  from cfengine_cli.lint import lint_args
8
- from cfengine_cli.shell import user_command
9
- from cfengine_cli.paths import bin
10
8
  from cfengine_cli.version import cfengine_cli_version_string
11
9
  from cfengine_cli.format import format_paths
12
10
  from cfengine_cli.utils import UserError
13
11
  from cfengine_cli.up import validate_config, up_do, resolve_templates
14
- from cfbs.commands import build_command
15
- from cf_remote.commands import deploy as deploy_command
16
12
  from cf_remote.paths import cf_remote_dir
17
13
  from pydantic import ValidationError
18
14
 
19
15
 
20
- def _require_cfagent():
21
- if not os.path.exists(bin("cf-agent")):
22
- raise UserError(f"cf-agent not found at {bin('cf-agent')}")
23
-
24
-
25
- def _require_cfhub():
26
- if not os.path.exists(bin("cf-hub")):
27
- raise UserError(f"cf-hub not found at {bin('cf-hub')}")
28
-
29
-
30
16
  def help() -> int:
31
17
  print("Example usage:")
32
18
  print("cfengine run")
@@ -38,16 +24,6 @@ def version() -> int:
38
24
  return 0
39
25
 
40
26
 
41
- def build() -> int:
42
- r = build_command()
43
- return r
44
-
45
-
46
- def deploy() -> int:
47
- r = deploy_command(None, None)
48
- return r
49
-
50
-
51
27
  def format(names, line_length, check) -> int:
52
28
  return format_paths(names, line_length, check)
53
29
 
@@ -69,21 +45,6 @@ def lint(files, strict, syntax_path) -> int:
69
45
  return errors
70
46
 
71
47
 
72
- def report() -> int:
73
- _require_cfhub()
74
- _require_cfagent()
75
- user_command(f"{bin('cf-agent')} -KIf update.cf && {bin('cf-agent')} -KI")
76
- user_command(f"{bin('cf-hub')} --query rebase -H 127.0.0.1")
77
- user_command(f"{bin('cf-hub')} --query delta -H 127.0.0.1")
78
- return 0
79
-
80
-
81
- def run() -> int:
82
- _require_cfagent()
83
- user_command(f"{bin('cf-agent')} -KIf update.cf && {bin('cf-agent')} -KI")
84
- return 0
85
-
86
-
87
48
  def dev(subcommand, args) -> int:
88
49
  return dispatch_dev_subcommand(subcommand, args)
89
50
 
@@ -29,6 +29,7 @@ HUMAN_NAME = {
29
29
  "zlib": "zlib",
30
30
  "librsync": "librsync",
31
31
  "leech": "leech",
32
+ "leech2": "leech2",
32
33
  "apache": "Apache",
33
34
  "apr": "APR",
34
35
  "apr-util": "apr-util",
@@ -60,6 +61,7 @@ HOME_URL = {
60
61
  "zlib": "https://www.zlib.net/",
61
62
  "librsync": "https://github.com/librsync/librsync/releases",
62
63
  "leech": "https://github.com/larsewi/leech/releases",
64
+ "leech2": "https://github.com/larsewi/leech2/releases",
63
65
  "apache": "https://httpd.apache.org/",
64
66
  "apr": "https://apr.apache.org/",
65
67
  "apr-util": "https://apr.apache.org/",
@@ -11,6 +11,7 @@ TODO: This code needs several adjustments to better fit into
11
11
  import os
12
12
  import json
13
13
  import subprocess
14
+ import textwrap
14
15
 
15
16
  import markdown_it
16
17
  from cfbs.pretty import pretty_file
@@ -375,8 +376,11 @@ def _run_formatter(tool, args, cwd, install_hint):
375
376
  check=True,
376
377
  cwd=cwd,
377
378
  )
378
- except:
379
- raise UserError(f"Encountered an error running {tool}\nInstall: {install_hint}")
379
+ except FileNotFoundError:
380
+ raise UserError(f"'{tool}' not found. Install it with: {install_hint}")
381
+ except subprocess.CalledProcessError as e:
382
+ error_msg = textwrap.indent(str(e.stderr).strip(), "> ")
383
+ raise UserError(f"Encountered an error running {tool}:\n{error_msg}")
380
384
  return result
381
385
 
382
386
 
@@ -493,14 +493,26 @@ class State:
493
493
  return
494
494
 
495
495
 
496
+ def _last_real_sibling_before(node: Node) -> Node | None:
497
+ """Walk backwards from node through its preceding siblings, skipping
498
+ comments and macros, to find the last real (content) sibling."""
499
+ sib = node.prev_sibling
500
+ while sib is not None and sib.type in ("comment", "macro"):
501
+ sib = sib.prev_sibling
502
+ return sib
503
+
504
+
496
505
  def _check_syntax(policy_file: PolicyFile, state: State) -> int:
497
- """Iterate over a syntax tree and print errors if it is empty or has syntax
498
- errors.
506
+ """Iterate over a syntax tree and print errors if it is empty, has syntax
507
+ errors, or is missing expected tokens (e.g. a dropped ';').
499
508
 
500
509
  Notably, printing syntax errors _does not happen during parsing_.
501
510
 
502
511
  Tree sitter has already fully parsed the policy, we iterate over the result
503
- and see if it has inserted any "ERROR" nodes.
512
+ and see if it has inserted any "ERROR" nodes, or any "MISSING" nodes
513
+ (tree-sitter's error recovery can silently insert a synthetic token,
514
+ e.g. a missing ';' right before a closing '}', without ever producing
515
+ an ERROR node.
504
516
 
505
517
  Stops at first error. Returns number of errors, always 0 or 1 in this case.
506
518
  """
@@ -522,15 +534,26 @@ def _check_syntax(policy_file: PolicyFile, state: State) -> int:
522
534
  state.start_file(policy_file)
523
535
  for node in policy_file.nodes:
524
536
  state.navigate(node)
525
- if node.type != "ERROR":
526
- continue
527
537
  line = node.range.start_point[0] + 1
528
538
  column = node.range.start_point[1] + 1
529
- _highlight_range(node, lines)
530
- location = state.get_location_extended(line, column)
531
- print(f"Error: Syntax error {location}")
532
- state.end_file()
533
- return 1
539
+ if node.type == "ERROR":
540
+ _highlight_range(node, lines)
541
+ location = state.get_location_extended(line, column)
542
+ print(f"Error: Syntax error {location}")
543
+ state.end_file()
544
+ return 1
545
+ if node.is_missing:
546
+ # To avoid pointing at #comment/@macro -lines
547
+ prev = _last_real_sibling_before(node)
548
+ report_node = prev if prev is not None else node
549
+ _highlight_range(report_node, lines)
550
+ line = report_node.range.end_point[0] + 1
551
+ column = report_node.range.end_point[1] + 1
552
+ missing = node.type if not node.is_named else f"token: {node.type}"
553
+ location = state.get_location_extended(line, column)
554
+ print(f"Error: Missing '{missing}' {location}")
555
+ state.end_file()
556
+ return 1
534
557
  state.end_file()
535
558
  return 0
536
559