xsoar-cli 2.0.0__tar.gz → 2.0.1__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 (134) hide show
  1. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/CHANGELOG.md +6 -0
  2. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/PKG-INFO +1 -1
  3. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/_version.py +2 -2
  4. xsoar_cli-2.0.1/fixes-todo.md +591 -0
  5. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/cli.py +4 -3
  6. xsoar_cli-2.0.1/src/xsoar_cli/commands/content/README.md +70 -0
  7. xsoar_cli-2.0.1/src/xsoar_cli/commands/content/commands.py +172 -0
  8. xsoar_cli-2.0.1/src/xsoar_cli/utilities/download_content_handlers.py +135 -0
  9. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/xsoar_client/content.py +74 -6
  10. xsoar_cli-2.0.1/tests/test_content.py +699 -0
  11. xsoar_cli-2.0.0/src/xsoar_cli/commands/content/README.md +0 -39
  12. xsoar_cli-2.0.0/src/xsoar_cli/commands/content/commands.py +0 -80
  13. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/.github/workflows/main.yml +0 -0
  14. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/.github/workflows/pull-request-open.yml +0 -0
  15. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/.github/workflows/release.yml +0 -0
  16. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/.github/workflows/update-changelog.yml +0 -0
  17. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/.gitignore +0 -0
  18. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/CLAUDE.md +0 -0
  19. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/CONTRIBUTING.md +0 -0
  20. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/LICENSE.txt +0 -0
  21. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/README.md +0 -0
  22. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/pyproject.toml +0 -0
  23. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/__init__.py +0 -0
  24. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/__init__.py +0 -0
  25. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/case/README.md +0 -0
  26. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/case/__init__.py +0 -0
  27. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/case/commands.py +0 -0
  28. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/config/README.md +0 -0
  29. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/config/__init__.py +0 -0
  30. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/config/commands.py +0 -0
  31. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/content/__init__.py +0 -0
  32. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/graph/README.md +0 -0
  33. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/graph/__init__.py +0 -0
  34. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/graph/commands.py +0 -0
  35. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/integration/README.md +0 -0
  36. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/integration/__init__.py +0 -0
  37. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/integration/commands.py +0 -0
  38. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/manifest/README.md +0 -0
  39. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/manifest/__init__.py +0 -0
  40. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/manifest/commands.py +0 -0
  41. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/pack/README.md +0 -0
  42. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/pack/__init__.py +0 -0
  43. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/pack/commands.py +0 -0
  44. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/playbook/README.md +0 -0
  45. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/playbook/__init__.py +0 -0
  46. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/playbook/commands.py +0 -0
  47. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/plugins/README.md +0 -0
  48. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/plugins/__init__.py +0 -0
  49. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/plugins/commands.py +0 -0
  50. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/rbac/README.md +0 -0
  51. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/rbac/__init__.py +0 -0
  52. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/commands/rbac/commands.py +0 -0
  53. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/configuration.py +0 -0
  54. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/error_handling/__init__.py +0 -0
  55. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/error_handling/connection.py +0 -0
  56. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/error_handling/http.py +0 -0
  57. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/log.py +0 -0
  58. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/plugins/README.md +0 -0
  59. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/plugins/__init__.py +0 -0
  60. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/plugins/manager.py +0 -0
  61. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/utilities/__init__.py +0 -0
  62. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/utilities/config_file.py +0 -0
  63. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/utilities/content.py +0 -0
  64. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/utilities/manifest.py +0 -0
  65. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/utilities/validators.py +0 -0
  66. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/utilities/version_check.py +0 -0
  67. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/xsoar_client/__init__.py +0 -0
  68. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/xsoar_client/artifact_providers/__init__.py +0 -0
  69. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/xsoar_client/artifact_providers/azure.py +0 -0
  70. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/xsoar_client/artifact_providers/base.py +0 -0
  71. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/xsoar_client/artifact_providers/s3.py +0 -0
  72. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/xsoar_client/cases.py +0 -0
  73. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/xsoar_client/client.py +0 -0
  74. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/xsoar_client/constants.py +0 -0
  75. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/xsoar_client/integrations.py +0 -0
  76. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/xsoar_client/packs.py +0 -0
  77. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/src/xsoar_cli/xsoar_client/rbac.py +0 -0
  78. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/__init__.py +0 -0
  79. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/conftest.py +0 -0
  80. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_base.py +0 -0
  81. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_case.py +0 -0
  82. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_config.py +0 -0
  83. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Download/playbook-empty.yml +0 -0
  84. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_CommonPlaybooks/.pack-ignore +0 -0
  85. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_CommonPlaybooks/.secrets-ignore +0 -0
  86. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_CommonPlaybooks/Author_image.png +0 -0
  87. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_CommonPlaybooks/Playbooks/GenericPlaybook.yml +0 -0
  88. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_CommonPlaybooks/README.md +0 -0
  89. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_CommonPlaybooks/pack_metadata.json +0 -0
  90. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_CommonScripts/.pack-ignore +0 -0
  91. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_CommonScripts/.secrets-ignore +0 -0
  92. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_CommonScripts/Author_image.png +0 -0
  93. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_CommonScripts/README.md +0 -0
  94. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_CommonScripts/Scripts/GenericScript/GenericScript.py +0 -0
  95. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_CommonScripts/Scripts/GenericScript/GenericScript.yml +0 -0
  96. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_CommonScripts/Scripts/GenericScript/README.md +0 -0
  97. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_CommonScripts/pack_metadata.json +0 -0
  98. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_EDR/.pack-ignore +0 -0
  99. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_EDR/.secrets-ignore +0 -0
  100. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_EDR/Author_image.png +0 -0
  101. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_EDR/Playbooks/EDR_InitialTriage.yml +0 -0
  102. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_EDR/README.md +0 -0
  103. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_EDR/Scripts/EDR_FetchFile/EDR_FetchFile.py +0 -0
  104. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_EDR/Scripts/EDR_FetchFile/EDR_FetchFile.yml +0 -0
  105. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_EDR/Scripts/EDR_FetchFile/README.md +0 -0
  106. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_EDR/Scripts/EDR_Triage/EDR_Triage.py +0 -0
  107. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_EDR/Scripts/EDR_Triage/EDR_Triage.yml +0 -0
  108. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_EDR/Scripts/EDR_Triage/README.md +0 -0
  109. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_EDR/Scripts/LegacyItem/LegacyItem.py +0 -0
  110. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_EDR/Scripts/LegacyItem/LegacyItem.yml +0 -0
  111. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_EDR/Scripts/LegacyItem/README.md +0 -0
  112. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_EDR/Scripts/LegacyItem/test_data/basescript-dummy.json +0 -0
  113. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_EDR/pack_metadata.json +0 -0
  114. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_Layouts/.pack-ignore +0 -0
  115. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_Layouts/.secrets-ignore +0 -0
  116. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_Layouts/Author_image.png +0 -0
  117. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_Layouts/Layouts/layoutscontainer-GenericLayout.json +0 -0
  118. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_Layouts/README.md +0 -0
  119. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/MyOrg_Layouts/pack_metadata.json +0 -0
  120. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/Packs/Not_applicable/Playbooks/Empty.yml +0 -0
  121. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/manifest_base.json +0 -0
  122. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/manifest_invalid.json +0 -0
  123. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/manifest_with_pack_not_on_server.json +0 -0
  124. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/server_base_response.json +0 -0
  125. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/server_base_response_missing_one_pack.json +0 -0
  126. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/server_base_response_with_updates.json +0 -0
  127. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_data/server_base_response_with_updates_and_one_extra.json +0 -0
  128. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_error_handling.py +0 -0
  129. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_graph.py +0 -0
  130. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_manifest.py +0 -0
  131. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_pack.py +0 -0
  132. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_playbook.py +0 -0
  133. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/tests/test_plugins.py +0 -0
  134. {xsoar_cli-2.0.0 → xsoar_cli-2.0.1}/uv.lock +0 -0
@@ -6,6 +6,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ### Added
10
+
11
+ - `content download` subcommand for downloading individual content items by name. Supports playbooks and layouts (`xsoar-cli content download --type playbook|layout <name>`).
12
+
13
+ ## [2.0.0] - 2026-04-07
14
+
9
15
  ### Fixed
10
16
 
11
17
  - Fixed typo "Uknown" in error messages for `Content.download_item`, `Content.attach_item`, and `Content.detach_item`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: xsoar-cli
3
- Version: 2.0.0
3
+ Version: 2.0.1
4
4
  Project-URL: Documentation, https://github.com/tlium/xsoar-cli#readme
5
5
  Project-URL: Issues, https://github.com/tlium/xsoar-cli/issues
6
6
  Project-URL: Source, https://github.com/tlium/xsoar-cli
@@ -18,7 +18,7 @@ version_tuple: tuple[int | str, ...]
18
18
  commit_id: str | None
19
19
  __commit_id__: str | None
20
20
 
21
- __version__ = version = '2.0.0'
22
- __version_tuple__ = version_tuple = (2, 0, 0)
21
+ __version__ = version = '2.0.1'
22
+ __version_tuple__ = version_tuple = (2, 0, 1)
23
23
 
24
24
  __commit_id__ = commit_id = None
@@ -0,0 +1,591 @@
1
+ # Refactoring Analysis
2
+
3
+ Codebase analysis identifying areas that could or should be refactored to improve
4
+ maintainability, consistency, and readiness for future expansion.
5
+
6
+ Items are grouped into tiers by effort and risk. Each item includes the
7
+ rationale, affected files, and a concrete description of what the fix looks like.
8
+
9
+ ## Summary
10
+
11
+ | # | Item | Type | Effort | Risk | Impact | Files | Reason | Status |
12
+ |-----|------|------|--------|------|--------|-------|--------|--------|
13
+ | 1.1 | `print()` in `Packs.get_outdated` | Consistency | Trivial | None | Low | `xsoar_client/packs.py` L108-109 | Breaks logging convention; domain layer should not produce user output | Done |
14
+ | 1.1a | Surface "pack not in artifacts repo" warning to user | Improvement | Low | Low | Medium | `commands/pack/commands.py` | Item 1.1 removed user-facing output; the CLI layer should re-surface this warning | Done |
15
+ | 1.2 | Missing return types in `Content` | Consistency | Trivial | None | Low | `xsoar_client/content.py` L83, L90, L97, L102 | Inconsistent with the rest of the codebase; hinders static analysis | Done |
16
+ | 1.3 | `list` builtin shadowed in content command | Bugfix | Trivial | None | Low | `commands/content/commands.py` L59 | Prevents use of `isinstance(..., list)` and invites subtle bugs | Done |
17
+ | 1.4 | "Uknown" typo in `Content` errors | Bugfix | Trivial | None | Low | `xsoar_client/content.py` L63, L73, L82 | Typo in user-facing error messages (three occurrences) | Done |
18
+ | 2.1 | Repeated `item_type` dispatch in `Content` | Refactor | Low | Low | Medium | `xsoar_client/content.py` L57-84 | Three methods share copy-pasted `if/else`; adding a content type means editing all three | |
19
+ | 2.2 | Broken temp file handling in `Packs.deploy` | Bugfix | Low | Low | Medium | `xsoar_client/packs.py` L92-93 | File never cleaned up; fails on Windows; two noqa comments suppressing real issues | Done |
20
+ | 2.3 | Redundant `skip_validation` logic in `Packs.deploy` | Cleanup | Low | Low | Low | `xsoar_client/packs.py` L86-91 | Both branches set `skip_validation` to the same value; `deploy` duplicates `deploy_zip` | Done |
21
+ | 2.4 | Eager init in S3 vs lazy init in Azure provider | Consistency | Low | Low | Medium | `artifact_providers/s3.py` L12-16, `artifact_providers/azure.py` L23-33 | Inconsistent patterns; S3 fails at construction even when provider is unused | Done |
22
+ | 3.1 | Domain classes lack a common base class | Refactor | Medium | Low | High | `xsoar_client/cases.py` L10, `integrations.py` L10, `rbac.py` L10, `content.py` L14, `packs.py` L18 | Five classes duplicate the same `__init__`; no shared hook for cross-cutting concerns | |
23
+ | 3.2 | Inconsistent `raise_for_status()` in domain classes | Robustness | Medium | Low | High | `xsoar_client/content.py` L20, `client.py` L56-78 | Some methods silently swallow HTTP errors, causing confusing downstream failures | |
24
+ | 3.3 | Repeated `--environment` / client setup boilerplate | Refactor | Medium | Medium | High | All `commands/*/commands.py` (see 3.3 detail below) | ~5 identical lines in every command; adding a new command requires copying boilerplate | |
25
+ | 3.4 | Manifest `commands.py` mixes CLI and business logic | Refactor | Medium | Low | Medium | `commands/manifest/commands.py` L25-113 (helpers), 447 lines total | 447-line file; business logic not independently testable without Click | |
26
+ | 3.5 | Error handling layer underutilized | Improvement | Medium | Medium | Medium | `error_handling/http.py` L8-22, `commands/case/commands.py` L55-57 | `HTTPErrorHandler` used in one command; all others handle errors ad-hoc | |
27
+ | 4.1 | `Packs.deploy` mixes download and upload | Refactor | Low | Low | Low | `xsoar_client/packs.py` L82-101 | Overlaps with `deploy_zip`; neither path is independently testable | |
28
+ | 4.2 | `validate_xsoar_connectivity` decorator complexity | Cleanup | Low | Low | Low | `utilities/validators.py` L19-68 | Accepts four input shapes; hard to predict behavior at a glance | Done |
29
+ | 4.3 | Test coverage gaps in command groups | Testing | High | Low | High | `tests/test_graph.py`, `test_manifest.py`, `test_pack.py`, `test_playbook.py` | Most command groups have only "exits 0" tests; prerequisite for safe refactoring | |
30
+
31
+ All file paths in the table are relative to `src/xsoar_cli/`.
32
+
33
+ ---
34
+
35
+ ## Tier 1: Trivial fixes (low effort, no risk)
36
+
37
+ These are isolated corrections that can each be done in a single commit with no
38
+ behavioral change.
39
+
40
+ ### 1.1 `Packs.get_outdated` uses `print()` instead of `logging`
41
+
42
+ **File:** `src/xsoar_cli/xsoar_client/packs.py` lines 108-109
43
+
44
+ ```python
45
+ except ValueError:
46
+ msg = f"WARNING: custom pack {pack['id']} installed on XSOAR server, ..."
47
+ print(msg, file=sys.stderr)
48
+ ```
49
+
50
+ This is the only place in the entire codebase that uses `print()` for warnings.
51
+ Domain classes (the `xsoar_client` layer) should not produce user-facing output;
52
+ that responsibility belongs to the CLI layer. Using `print()` here also bypasses
53
+ the logging infrastructure entirely, so the warning never appears in the log file.
54
+
55
+ **Fix:** Replace with `logger.warning(...)`. If the CLI layer needs to surface
56
+ this to the user, it should catch the condition itself or inspect the return
57
+ value.
58
+
59
+ **Follow-up (1.1a):** The `print()` call was the only way this warning reached
60
+ the user. Now that it is logged instead, the CLI layer (`pack/commands.py` or
61
+ any other caller of `get_outdated()`) should detect skipped packs and display a
62
+ warning via `click.echo()`. This could be done by having `get_outdated()` return
63
+ the skipped pack IDs alongside the outdated list, or by accepting a callback.
64
+
65
+ ---
66
+
67
+ ### 1.2 Missing return type annotations in `Content`
68
+
69
+ **File:** `src/xsoar_cli/xsoar_client/content.py`
70
+
71
+ Four methods lack return type annotations:
72
+
73
+ | Method | Line | Suggested return type |
74
+ |--------|------|----------------------|
75
+ | `_list_playbooks` | L83 | `list[dict]` |
76
+ | `_list_scripts` | L90 | `list[dict]` |
77
+ | `_list_commands` | L97 | `list[dict]` |
78
+ | `list` | L102 | `list[dict] \| dict[str, list[dict]]` |
79
+
80
+ The rest of the codebase consistently uses type hints. These should follow suit.
81
+
82
+ **Fix:** Add explicit return types to all four methods.
83
+
84
+ ---
85
+
86
+ ### 1.3 Content command function shadows Python builtin `list`
87
+
88
+ **File:** `src/xsoar_cli/commands/content/commands.py` line 59
89
+
90
+ ```python
91
+ def list(ctx, environment, content_type, detail, verbose):
92
+ ```
93
+
94
+ The function name shadows the Python builtin `list`. The file itself
95
+ acknowledges the problem with a workaround comment at lines 71-72:
96
+
97
+ ```python
98
+ # Note: isinstance(json_blob, list) cannot be used here because the function
99
+ # name "list" shadows the builtin.
100
+ ```
101
+
102
+ **Fix:** Rename the function to `list_content` (or similar) and use
103
+ `@click.command("list")` to preserve the CLI-facing name. This removes the
104
+ need for the workaround comment and prevents future bugs from the shadowed
105
+ builtin.
106
+
107
+ ---
108
+
109
+ ### 1.4 Typo: "Uknown" in `Content` error messages
110
+
111
+ **File:** `src/xsoar_cli/xsoar_client/content.py`
112
+
113
+ The string `"Uknown"` appears in three `ValueError` messages:
114
+
115
+ | Line | Method |
116
+ |------|--------|
117
+ | L63 | `download_item` |
118
+ | L73 | `attach_item` |
119
+ | L82 | `detach_item` |
120
+
121
+ Should be `"Unknown"`.
122
+
123
+ **Fix:** Fix the typo in all three places (related to item 2.1 below, which
124
+ refactors the dispatch pattern these messages live in).
125
+
126
+ ---
127
+
128
+ ## Tier 2: Small refactors (low effort, low risk)
129
+
130
+ Each of these touches one or two files and can be done independently.
131
+
132
+ ### 2.1 Repeated `item_type` dispatch in `Content`
133
+
134
+ **File:** `src/xsoar_cli/xsoar_client/content.py` lines 57-84
135
+
136
+ Three methods (`download_item` at L57, `attach_item` at L67, `detach_item` at
137
+ L77) share the exact same structure:
138
+
139
+ ```python
140
+ def download_item(self, item_type: str, item_id: str) -> bytes:
141
+ if item_type == "playbook":
142
+ endpoint = f"/{item_type}/{item_id}/yaml"
143
+ response = self.client.make_request(endpoint=endpoint, method="GET")
144
+ else:
145
+ msg = 'Uknown item_type selected for download. Must be one of ["playbook"]'
146
+ raise ValueError(msg)
147
+ response.raise_for_status()
148
+ return response.content
149
+ ```
150
+
151
+ The `if item_type == "playbook" ... else raise ValueError` block is copy-pasted
152
+ three times. When a new content type is added (e.g. `"script"`), all three
153
+ methods need to be updated in lockstep, which is error-prone.
154
+
155
+ **Fix options (pick one):**
156
+
157
+ - **Validation helper:** Extract a `_validate_item_type(item_type)` method that
158
+ raises `ValueError` for unsupported types. Each method calls it once and then
159
+ proceeds with endpoint construction without the `if/else`.
160
+
161
+ - **Endpoint map:** A class-level dict mapping `(action, item_type)` to endpoint
162
+ templates. Each method does a lookup and raises if the key is missing. Adding
163
+ a new content type becomes a single dict entry.
164
+
165
+ Either approach also fixes the typo from item 1.4.
166
+
167
+ ---
168
+
169
+ ### 2.2 Temporary file handling in `Packs.deploy`
170
+
171
+ **File:** `src/xsoar_cli/xsoar_client/packs.py` lines 92-93
172
+
173
+ ```python
174
+ tmp = tempfile.NamedTemporaryFile() # noqa: SIM115
175
+ with open(tmp.name, "wb") as f: # noqa: PTH123
176
+ f.write(filedata)
177
+ ```
178
+
179
+ Problems:
180
+
181
+ 1. `NamedTemporaryFile` is created but never used as a context manager. The file
182
+ object is not explicitly closed or cleaned up.
183
+ 2. The file is opened a second time via `open(tmp.name, "wb")`, which on Windows
184
+ would fail because `NamedTemporaryFile` keeps the file open by default.
185
+ 3. Two `noqa` comments suppress linter warnings that are flagging real issues.
186
+
187
+ **Fix:**
188
+
189
+ ```python
190
+ with tempfile.NamedTemporaryFile(suffix=".zip", delete=False) as tmp:
191
+ tmp.write(filedata)
192
+ tmp.flush()
193
+ tmp_path = tmp.name
194
+
195
+ try:
196
+ self.client.demisto_py_instance.upload_content_packs(tmp_path, **params)
197
+ except ApiException as ex:
198
+ ...
199
+ finally:
200
+ Path(tmp_path).unlink(missing_ok=True)
201
+ ```
202
+
203
+ This also eliminates the need for the two `noqa` comments.
204
+
205
+ ---
206
+
207
+ ### 2.3 `Packs.deploy` duplicates `skip_validation`/`skip_verify` logic
208
+
209
+ **File:** `src/xsoar_cli/xsoar_client/packs.py` lines 86-91
210
+
211
+ ```python
212
+ if custom:
213
+ params["skip_validation"] = "false"
214
+ params["skip_verify"] = "true"
215
+ else:
216
+ params["skip_validation"] = "false"
217
+ params["skip_verify"] = "false"
218
+ ```
219
+
220
+ `skip_validation` is `"false"` in both branches. Only `skip_verify` differs.
221
+ This can be simplified to:
222
+
223
+ ```python
224
+ params = {
225
+ "skip_validation": "false",
226
+ "skip_verify": "true" if custom else "false",
227
+ }
228
+ ```
229
+
230
+ The existing `deploy_zip` method (L79-85) also accepts `skip_validation`/
231
+ `skip_verify` as booleans and converts them to strings internally. The two
232
+ methods overlap in responsibility. Consider having `deploy` call `deploy_zip`
233
+ after downloading, rather than reimplementing the upload step.
234
+
235
+ ---
236
+
237
+ ### 2.4 S3 provider eager init vs Azure lazy init
238
+
239
+ **Files:**
240
+ - `src/xsoar_cli/xsoar_client/artifact_providers/s3.py` lines 12-16
241
+ - `src/xsoar_cli/xsoar_client/artifact_providers/azure.py` lines 23-33
242
+
243
+ `S3ArtifactProvider.__init__` eagerly creates the boto3 session and S3 resource:
244
+
245
+ ```python
246
+ # s3.py L12-16
247
+ def __init__(self, *, bucket_name, verify_ssl=True):
248
+ self.bucket_name = bucket_name
249
+ self.verify_ssl = verify_ssl
250
+ self.session = boto3.session.Session()
251
+ self.s3 = self.session.resource("s3")
252
+ ```
253
+
254
+ `AzureArtifactProvider` uses lazy `@property` accessors that defer creation
255
+ until first use (L23-33).
256
+
257
+ This inconsistency means:
258
+
259
+ - S3 provider creation fails immediately if AWS credentials are missing, even if
260
+ the current command does not need the artifact provider.
261
+ - Azure defers the failure to the point of actual use, which is the better
262
+ behavior.
263
+
264
+ **Fix:** Make S3 follow the same lazy pattern as Azure. Store `bucket_name` and
265
+ `verify_ssl` in `__init__`, create the session/resource in a `@property`.
266
+
267
+ ---
268
+
269
+ ## Tier 3: Medium refactors (medium effort, low-to-medium risk)
270
+
271
+ These touch multiple files or introduce new abstractions. Each should be done as
272
+ an isolated step.
273
+
274
+ ### 3.1 Domain classes lack a common base class
275
+
276
+ **Files** (all under `src/xsoar_cli/xsoar_client/`):
277
+
278
+ | Class | File | `__init__` line |
279
+ |-------|------|-----------------|
280
+ | `Cases` | `cases.py` | L10 |
281
+ | `Content` | `content.py` | L14 |
282
+ | `Integrations` | `integrations.py` | L10 |
283
+ | `Packs` | `packs.py` | L18 |
284
+ | `Rbac` | `rbac.py` | L10 |
285
+
286
+ All five domain classes follow the same pattern:
287
+
288
+ ```python
289
+ class Cases:
290
+ def __init__(self, client: Client) -> None:
291
+ self.client = client
292
+ ```
293
+
294
+ A `BaseDomain` class would:
295
+
296
+ - Eliminate the duplicated `__init__` in each class.
297
+ - Provide a natural place to add shared behavior later (automatic
298
+ `raise_for_status()`, response logging, retry logic, request tracing).
299
+ - Make it explicit that these classes share a contract.
300
+
301
+ **Sketch:**
302
+
303
+ ```python
304
+ class BaseDomain:
305
+ def __init__(self, client: Client) -> None:
306
+ self.client = client
307
+
308
+ class Cases(BaseDomain):
309
+ def get(self, case_id: int) -> dict:
310
+ ...
311
+ ```
312
+
313
+ `Packs` has a slightly different `__init__` (extra `custom_pack_authors`
314
+ parameter). It would call `super().__init__(client)` and then set its own
315
+ attributes.
316
+
317
+ The base class could live in a new file `src/xsoar_cli/xsoar_client/base.py`
318
+ or in the existing `__init__.py`.
319
+
320
+ ---
321
+
322
+ ### 3.2 Inconsistent HTTP response error handling in domain classes
323
+
324
+ **Files:** All domain classes under `src/xsoar_cli/xsoar_client/`,
325
+ `client.py` L56-78 (`make_request`)
326
+
327
+ Some methods call `response.raise_for_status()`, some don't:
328
+
329
+ | Method | File | Line | Calls `raise_for_status()`? |
330
+ |--------|------|------|-----------------------------|
331
+ | `Cases.get()` | `cases.py` | L13 | Yes |
332
+ | `Cases.create()` | `cases.py` | L20 | Yes |
333
+ | `Content.get_bundle()` | `content.py` | L17 | **No** |
334
+ | `Content.get_detached()` | `content.py` | L38 | Yes |
335
+ | `Packs.get_installed()` | `packs.py` | L24 | Yes |
336
+ | `Rbac.get_users()` | `rbac.py` | L14 | Yes |
337
+
338
+ If `Content.get_bundle()` receives a non-2xx response, it silently tries to
339
+ untar the error body and fails with a confusing `tarfile` error rather than a
340
+ clear HTTP error.
341
+
342
+ **Fix options:**
343
+
344
+ 1. **Audit and add:** Go through every method, add `raise_for_status()` where
345
+ missing. Simple but requires ongoing discipline.
346
+
347
+ 2. **Default-safe `make_request`:** Add a `raise_for_status=True` parameter to
348
+ `Client.make_request()` (L56) so the default is safe. Methods that need the
349
+ raw response can opt out. This ties in well with a `BaseDomain` class
350
+ (item 3.1) that could wrap requests uniformly.
351
+
352
+ Option 2 is preferable because it makes the safe path the default and shifts
353
+ the burden to the rare cases that need raw responses.
354
+
355
+ ---
356
+
357
+ ### 3.3 Repeated `--environment` option and client setup boilerplate
358
+
359
+ **Files:** Every `commands.py` under `src/xsoar_cli/commands/`
360
+
361
+ Almost every command repeats this pattern:
362
+
363
+ ```python
364
+ @click.option("--environment", default=None, help="Default environment set in config file.")
365
+ @click.pass_context
366
+ @load_config
367
+ @validate_xsoar_connectivity()
368
+ def some_command(ctx, environment, ...):
369
+ config = get_xsoar_config(ctx)
370
+ xsoar_client: Client = config.get_client(environment)
371
+ active_env = environment or config.default_environment
372
+ ...
373
+ ```
374
+
375
+ The `--environment` option appears at the following locations:
376
+
377
+ | File | Lines | Commands |
378
+ |------|-------|----------|
379
+ | `commands/case/commands.py` | L36, L132 | `get`, `create` |
380
+ | `commands/pack/commands.py` | L23, L47 | `delete`, `get_outdated` |
381
+ | `commands/rbac/commands.py` | L24, L34, L44 | `getroles`, `getusers`, `getusergroups` |
382
+ | `commands/manifest/commands.py` | L122, L153, L229, L300, L356 | `generate`, `update`, `validate`, `diff`, `deploy` |
383
+ | `commands/content/commands.py` | L23, L52 | `get_detached`, `list` |
384
+ | `commands/integration/commands.py` | L24 | `dump` |
385
+ | `commands/playbook/commands.py` | L26 | `download` |
386
+ | `commands/graph/commands.py` | L30 | All graph subcommands (via `_common_graph_options`) |
387
+
388
+ That is 16 occurrences of the same option definition, plus 15+ repetitions of
389
+ the `get_xsoar_config` / `get_client` / `active_env` setup block.
390
+
391
+ **Fix:** Create a shared decorator (e.g., `@xsoar_command`) that:
392
+
393
+ 1. Adds `--environment` as a Click option.
394
+ 2. Applies `@load_config`.
395
+ 3. Resolves `config`, `xsoar_client`, and `active_env` and passes them to the
396
+ decorated function (either via context or as injected parameters).
397
+
398
+ Optionally, this decorator could accept flags to also apply
399
+ `@validate_xsoar_connectivity()` and/or `@validate_artifacts_provider`.
400
+
401
+ This would reduce each command to its unique logic and make adding new commands
402
+ less error-prone. The decorator should live in `utilities/` alongside the
403
+ existing `load_config`.
404
+
405
+ Note that `graph/commands.py` already partially solves this with
406
+ `_common_graph_options` (L22-34). A project-wide decorator would subsume that
407
+ pattern.
408
+
409
+ **Risk note:** This touches many files but each change is mechanical. It should
410
+ be done in one pass to keep the pattern consistent, with the test suite
411
+ validating each command still works.
412
+
413
+ ---
414
+
415
+ ### 3.4 Extract manifest business logic from `commands.py`
416
+
417
+ **File:** `src/xsoar_cli/commands/manifest/commands.py` (447 lines)
418
+
419
+ This is the largest command file by a wide margin. It contains both CLI wiring
420
+ (Click decorators, `click.echo()` calls, `ctx.exit()`) and business logic
421
+ (manifest file I/O, version comparison loops, pack availability checking).
422
+
423
+ The following helpers could move to `src/xsoar_cli/utilities/manifest.py`:
424
+
425
+ | Function | Current location | Lines |
426
+ |----------|-----------------|-------|
427
+ | `load_manifest` | `manifest/commands.py` | L25-34 |
428
+ | `write_manifest` | `manifest/commands.py` | L37-43 |
429
+ | `_pack_found_locally` | `manifest/commands.py` | L46-59 |
430
+ | `_validate_manifest_keys` | `manifest/commands.py` | L62-73 |
431
+ | `_check_pack_availability` | `manifest/commands.py` | L76-113 |
432
+
433
+ `utilities/manifest.py` already holds some manifest-related logic
434
+ (`find_installed_packs_not_in_manifest`, `find_packs_in_manifest_not_installed`,
435
+ `find_version_mismatch`). The split is partially done. Completing it would:
436
+
437
+ - Keep `commands.py` focused on Click wiring and user-facing output.
438
+ - Make the business logic independently testable without invoking Click.
439
+ - Reduce the file to a more reviewable size.
440
+
441
+ `_check_pack_availability` currently calls `click.echo()` for progress dots and
442
+ raises `click.ClickException`. To move it cleanly, it should either accept a
443
+ progress callback or return results that the command layer formats. Similarly,
444
+ `load_manifest` currently raises `click.ClickException`; it should raise a plain
445
+ exception that the command layer catches and converts.
446
+
447
+ ---
448
+
449
+ ### 3.5 Error handling layer is underutilized
450
+
451
+ **Files:**
452
+ - `src/xsoar_cli/error_handling/http.py` L8-22 (`HTTPErrorHandler` class and `_messages` dict)
453
+ - `src/xsoar_cli/error_handling/connection.py` L9-56 (`ConnectionErrorHandler`)
454
+ - All command modules
455
+
456
+ The `error_handling/` package defines two well-structured handler classes:
457
+
458
+ - `HTTPErrorHandler` (L8): maps HTTP status codes to user-friendly messages,
459
+ with per-context customization via `_messages` (L17-22).
460
+ - `ConnectionErrorHandler`: extracts actionable messages from exception chains.
461
+
462
+ However, `HTTPErrorHandler` is only used in `commands/case/commands.py` at
463
+ L55-57:
464
+
465
+ ```python
466
+ except HTTPError as ex:
467
+ handler = HTTPErrorHandler()
468
+ click.echo(f"Error: {handler.get_message(ex, context='case')}")
469
+ ```
470
+
471
+ All other commands that handle HTTP errors do so ad-hoc:
472
+
473
+ - `commands/manifest/commands.py` `deploy` (L397-415): catches `RuntimeError`
474
+ wrapping `ApiException`, manually parses the JSON error body.
475
+ - `commands/pack/commands.py`, `commands/rbac/commands.py`,
476
+ `commands/integration/commands.py`: let exceptions from `raise_for_status()`
477
+ propagate uncaught (Click prints the traceback).
478
+
479
+ `ConnectionErrorHandler` is only used in `utilities/validators.py` L59.
480
+
481
+ **Fix:** Expand the `HTTPErrorHandler._messages` dict with entries for contexts
482
+ used by other commands (`"pack"`, `"manifest"`, `"integration"`, etc.) and
483
+ apply the handler consistently. This could also be tied into the common
484
+ decorator from item 3.3, where a top-level exception handler wraps every
485
+ command invocation.
486
+
487
+ This is a gradual process. No need to do it all at once, but each new command
488
+ should use the error handling layer from the start.
489
+
490
+ ---
491
+
492
+ ## Tier 4: Larger structural improvements (higher effort)
493
+
494
+ These are not urgent but would pay off as the codebase grows.
495
+
496
+ ### 4.1 `Packs.deploy` mixes download and upload responsibilities
497
+
498
+ **File:** `src/xsoar_cli/xsoar_client/packs.py` lines 82-101
499
+
500
+ The `deploy` method:
501
+
502
+ 1. Downloads the pack (via `self.download()`, called at L84).
503
+ 2. Determines `skip_validation`/`skip_verify` params (L86-91).
504
+ 3. Writes data to a temp file (L92-93).
505
+ 4. Uploads it (via `demisto_py_instance.upload_content_packs()`, L96).
506
+
507
+ Meanwhile, `deploy_zip` (L79-85) exists as a separate method that handles step 4
508
+ from a user-provided file path. The two methods overlap but don't share code.
509
+
510
+ **Fix:** Have `deploy` download the pack, write it to a temp file, and then
511
+ call `deploy_zip` for the upload step. This avoids duplicating the
512
+ `upload_content_packs` call and its error handling, and makes both paths
513
+ independently testable.
514
+
515
+ ---
516
+
517
+ ### 4.2 `validate_xsoar_connectivity` decorator complexity
518
+
519
+ **File:** `src/xsoar_cli/utilities/validators.py` lines 19-68
520
+
521
+ The `validate_xsoar_connectivity` decorator accepts environments as `None`,
522
+ `str`, `list[str]`, or `Callable` (union type at L20-21):
523
+
524
+ ```python
525
+ def validate_xsoar_connectivity(
526
+ environments: str | list[str] | Callable[[click.Context], str | list[str]] | None = None,
527
+ ) -> Callable:
528
+ ```
529
+
530
+ This flexibility is powerful but makes the decorator's behavior hard to predict
531
+ at a glance. The `Callable` variant is used only once, in
532
+ `commands/case/commands.py` for the `clone` command:
533
+
534
+ ```python
535
+ @validate_xsoar_connectivity(lambda ctx: [ctx.params["source"], ctx.params["dest"]])
536
+ ```
537
+
538
+ If the common decorator from item 3.3 is introduced, the multi-environment
539
+ validation could be handled more explicitly (e.g., the decorator accepts a list
540
+ of parameter names to resolve environments from). This would simplify the type
541
+ signature and make the intent clearer.
542
+
543
+ Not urgent, but worth revisiting if item 3.3 is implemented.
544
+
545
+ ---
546
+
547
+ ### 4.3 Test coverage gaps
548
+
549
+ **Files:**
550
+
551
+ | Test file | Commands covered | Depth |
552
+ |-----------|-----------------|-------|
553
+ | `tests/test_config.py` | `config validate` | Thorough (7 test classes, edge cases) |
554
+ | `tests/test_case.py` | `case get`, `case create`, `case clone` | Good (success + error paths) |
555
+ | `tests/test_error_handling.py` | Error handler classes | Good (parametrized) |
556
+ | `tests/test_plugins.py` | Plugin system | Good (unit + integration) |
557
+ | `tests/test_base.py` | Root CLI group | Minimal (3 parametrized cases) |
558
+ | `tests/test_graph.py` | `graph` | Minimal (single "exits 0" test) |
559
+ | `tests/test_manifest.py` | `manifest` | Minimal (single "exits 0" test) |
560
+ | `tests/test_pack.py` | `pack` | Minimal (single "exits 0" test) |
561
+ | `tests/test_playbook.py` | `playbook` | Minimal (single "exits 0" test) |
562
+
563
+ The `test_config.py` and `test_case.py` files demonstrate how to properly test
564
+ commands with mocked clients and config. The same patterns should be applied to
565
+ the other command groups.
566
+
567
+ This is not a refactoring item per se, but test coverage is a prerequisite for
568
+ safely performing the refactors listed above. Expanding tests for `manifest`,
569
+ `pack`, and `playbook` commands before refactoring those modules reduces the
570
+ risk of regressions.
571
+
572
+ ---
573
+
574
+ ## Suggested execution order
575
+
576
+ The items below are ordered to minimize risk and maximize incremental value.
577
+ Each step should be a separate commit or PR.
578
+
579
+ 1. **Tier 1 quick wins** (items 1.1 through 1.4). These are independent and can
580
+ all be done in a single batch.
581
+ 2. **Tier 2 small refactors** (items 2.1 through 2.4). Each is independent and
582
+ can be done in any order.
583
+ 3. **Item 3.1** (domain base class). Introduces the abstraction that item 3.2
584
+ depends on.
585
+ 4. **Item 3.2** (consistent `raise_for_status()`). Builds on the base class.
586
+ 5. **Item 3.4** (extract manifest business logic). Independent of items 3.1-3.2.
587
+ 6. **Item 3.3** (common `--environment` decorator). Largest single change. Should
588
+ come after the simpler refactors are settled and test coverage is solid.
589
+ 7. **Item 3.5** (standardize error handling). Gradual, can be done command by
590
+ command alongside other work.
591
+ 8. **Tier 4 items** as opportunities arise.
@@ -64,14 +64,15 @@ def cli(ctx: click.Context, debug: bool) -> None:
64
64
 
65
65
 
66
66
  def _register_commands() -> None:
67
- cli.add_command(config_commands.config)
68
67
  cli.add_command(case_commands.case)
68
+ cli.add_command(config_commands.config)
69
69
  cli.add_command(content_commands.content)
70
- cli.add_command(pack_commands.pack)
70
+
71
+ cli.add_command(graph_commands.graph)
71
72
  cli.add_command(integration_commands.integration)
72
73
  cli.add_command(manifest_commands.manifest)
74
+ cli.add_command(pack_commands.pack)
73
75
  cli.add_command(playbook_commands.playbook)
74
- cli.add_command(graph_commands.graph)
75
76
  cli.add_command(plugin_commands.plugins)
76
77
  cli.add_command(rbac_commands.rbac)
77
78