@ngocsangairvds/vsaf 3.1.27 → 3.2.1

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 (303) hide show
  1. package/package.json +2 -2
  2. package/src/global.js +70 -10
  3. package/tools/skills/vds-scripts-skill/.openskills.json +6 -0
  4. package/tools/skills/vds-scripts-skill/QUALITY.md +44 -0
  5. package/tools/skills/vds-scripts-skill/SKILL.md +135 -0
  6. package/tools/skills/vds-scripts-skill/references/audit-commands.md +171 -0
  7. package/tools/skills/vds-scripts-skill/references/capability-index.md +34 -0
  8. package/tools/skills/vds-scripts-skill/references/development-commands.md +12 -0
  9. package/tools/skills/vds-scripts-skill/references/google-sheets.md +73 -0
  10. package/tools/skills/vds-scripts-skill/references/integration-commands.md +17 -0
  11. package/tools/skills/vds-scripts-skill/references/platform-bootstrap.md +31 -0
  12. package/tools/skills/vds-scripts-skill/references/specialist-routing.md +14 -0
  13. package/tools/skills/vds-scripts-skill/references/validation-commands.md +15 -0
  14. package/tools/skills/vsaf-build/SKILL.md +32 -2
  15. package/tools/skills/vsaf-ship/SKILL.md +41 -10
  16. package/tools/skills/vsaf-test/SKILL.md +8 -0
  17. package/tools/vds-scripts/.mcp.json +11 -0
  18. package/tools/vds-scripts/.secrets.baseline +133 -0
  19. package/tools/vds-scripts/AGENTS.md +152 -0
  20. package/tools/vds-scripts/CLAUDE.md +101 -0
  21. package/tools/vds-scripts/CLI_COMMAND_OPTIMIZATION.md +156 -0
  22. package/tools/vds-scripts/PACKAGE_P125B_IMPLEMENTATION_SUMMARY.md +131 -0
  23. package/tools/vds-scripts/PROJECT_COMPLETION_SUMMARY.md +45 -0
  24. package/tools/vds-scripts/README.md +97 -0
  25. package/tools/vds-scripts/bitbucket_manifest_mapping.toml +34 -0
  26. package/tools/vds-scripts/bitbucket_orchestrator/ARCHITECTURE_ANALYSIS.md +258 -0
  27. package/tools/vds-scripts/bitbucket_orchestrator/BITBUCKET_API_PRACTICES.md +393 -0
  28. package/tools/vds-scripts/bitbucket_orchestrator/EVALUATION_REPORT.md +61 -0
  29. package/tools/vds-scripts/bitbucket_orchestrator/FEATURES.md +908 -0
  30. package/tools/vds-scripts/bitbucket_orchestrator/README.md +687 -0
  31. package/tools/vds-scripts/bitbucket_orchestrator/pyproject.toml +40 -0
  32. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/__init__.py +20 -0
  33. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/async_client.py +657 -0
  34. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/cli.py +2108 -0
  35. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/client.py +2534 -0
  36. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/config.py +171 -0
  37. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/errors.py +67 -0
  38. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/factory.py +185 -0
  39. package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/protocols.py +244 -0
  40. package/tools/vds-scripts/bitbucket_orchestrator/tests/__init__.py +8 -0
  41. package/tools/vds-scripts/bitbucket_orchestrator/tests/conftest.py +65 -0
  42. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_advanced_search.py +151 -0
  43. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_async_client.py +546 -0
  44. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_branch_permissions.py +145 -0
  45. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_cli.py +115 -0
  46. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client.py +157 -0
  47. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_branch_conditions.py +79 -0
  48. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_code_advanced.py +163 -0
  49. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_code_file.py +32 -0
  50. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_deployment_environments.py +194 -0
  51. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_issues.py +164 -0
  52. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_pipelines_advanced.py +179 -0
  53. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_pr_blockers.py +119 -0
  54. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_repository_variables.py +156 -0
  55. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code.py +98 -0
  56. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code_advanced.py +282 -0
  57. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code_insights.py +335 -0
  58. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_conditions.py +147 -0
  59. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_config.py +131 -0
  60. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_deployment_env.py +352 -0
  61. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_factory.py +371 -0
  62. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_fork_operations.py +204 -0
  63. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_issue_cli.py +261 -0
  64. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_pipeline_advanced.py +270 -0
  65. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_pr_blocker.py +204 -0
  66. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_protocols.py +334 -0
  67. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_repo_settings.py +343 -0
  68. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_repo_variables.py +270 -0
  69. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_webhooks.py +189 -0
  70. package/tools/vds-scripts/bitbucket_orchestrator/tests/test_workspace.py +233 -0
  71. package/tools/vds-scripts/bitbucket_orchestrator/uv.lock +742 -0
  72. package/tools/vds-scripts/confluence_orchestrator/Dockerfile +19 -0
  73. package/tools/vds-scripts/confluence_orchestrator/README.md +412 -0
  74. package/tools/vds-scripts/confluence_orchestrator/SYNC_SCRIPTS.md +127 -0
  75. package/tools/vds-scripts/confluence_orchestrator/SYNC_STANDARDIZATION.md +108 -0
  76. package/tools/vds-scripts/confluence_orchestrator/pyproject.toml +48 -0
  77. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/__init__.py +20 -0
  78. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/cli.py +2532 -0
  79. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/config.py +175 -0
  80. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/content.py +290 -0
  81. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/content_v2.py +94 -0
  82. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/crawl_tree.py +1835 -0
  83. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/errors.py +80 -0
  84. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/eventing.py +109 -0
  85. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/http.py +1114 -0
  86. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/orchestration.py +165 -0
  87. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/reporting.py +78 -0
  88. package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/tree.py +121 -0
  89. package/tools/vds-scripts/confluence_orchestrator/sync_pdfs_from_markdown.py +213 -0
  90. package/tools/vds-scripts/confluence_orchestrator/sync_pdfs_to_confluence.py +305 -0
  91. package/tools/vds-scripts/confluence_orchestrator/sync_png_attachments.py +305 -0
  92. package/tools/vds-scripts/confluence_orchestrator/tests/__init__.py +0 -0
  93. package/tools/vds-scripts/confluence_orchestrator/tests/conftest.py +8 -0
  94. package/tools/vds-scripts/confluence_orchestrator/tests/test_advanced_content.py +224 -0
  95. package/tools/vds-scripts/confluence_orchestrator/tests/test_advanced_search.py +188 -0
  96. package/tools/vds-scripts/confluence_orchestrator/tests/test_cache_management.py +247 -0
  97. package/tools/vds-scripts/confluence_orchestrator/tests/test_cli.py +499 -0
  98. package/tools/vds-scripts/confluence_orchestrator/tests/test_config.py +83 -0
  99. package/tools/vds-scripts/confluence_orchestrator/tests/test_content.py +186 -0
  100. package/tools/vds-scripts/confluence_orchestrator/tests/test_content_flags.py +27 -0
  101. package/tools/vds-scripts/confluence_orchestrator/tests/test_crawl_tree.py +2250 -0
  102. package/tools/vds-scripts/confluence_orchestrator/tests/test_draft_management.py +223 -0
  103. package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing.py +71 -0
  104. package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_chaos.py +37 -0
  105. package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_rate_limit.py +44 -0
  106. package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_timeout.py +49 -0
  107. package/tools/vds-scripts/confluence_orchestrator/tests/test_export.py +230 -0
  108. package/tools/vds-scripts/confluence_orchestrator/tests/test_history.py +204 -0
  109. package/tools/vds-scripts/confluence_orchestrator/tests/test_http.py +117 -0
  110. package/tools/vds-scripts/confluence_orchestrator/tests/test_orchestration.py +91 -0
  111. package/tools/vds-scripts/confluence_orchestrator/tests/test_reporting.py +24 -0
  112. package/tools/vds-scripts/confluence_orchestrator/tests/test_search_cql.py +34 -0
  113. package/tools/vds-scripts/confluence_orchestrator/tests/test_space_management.py +237 -0
  114. package/tools/vds-scripts/confluence_orchestrator/tests/test_space_permissions.py +332 -0
  115. package/tools/vds-scripts/confluence_orchestrator/tests/test_user_group_management.py +388 -0
  116. package/tools/vds-scripts/confluence_orchestrator/uv.lock +1023 -0
  117. package/tools/vds-scripts/git_orchestrator/ENHANCEMENT_SUMMARY.md +119 -0
  118. package/tools/vds-scripts/git_orchestrator/README.md +280 -0
  119. package/tools/vds-scripts/git_orchestrator/VERIFICATION_REPORT.md +152 -0
  120. package/tools/vds-scripts/git_orchestrator/pyproject.toml +35 -0
  121. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/__init__.py +7 -0
  122. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/__main__.py +4 -0
  123. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/cli.py +847 -0
  124. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/logging_config.py +63 -0
  125. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/manifest.py +129 -0
  126. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/orchestrator.py +819 -0
  127. package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/reporting.py +53 -0
  128. package/tools/vds-scripts/git_orchestrator/tests/__init__.py +0 -0
  129. package/tools/vds-scripts/git_orchestrator/tests/test_cli_settings.py +21 -0
  130. package/tools/vds-scripts/git_orchestrator/tests/test_integration.py +74 -0
  131. package/tools/vds-scripts/git_orchestrator/tests/test_manifest.py +79 -0
  132. package/tools/vds-scripts/git_orchestrator/tests/test_orchestrator.py +204 -0
  133. package/tools/vds-scripts/git_orchestrator/tests/test_public_api.py +236 -0
  134. package/tools/vds-scripts/git_orchestrator/tests/test_resilience.py +345 -0
  135. package/tools/vds-scripts/git_orchestrator/uv.lock +271 -0
  136. package/tools/vds-scripts/jira_orchestrator/README.md +770 -0
  137. package/tools/vds-scripts/jira_orchestrator/pyproject.toml +39 -0
  138. package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/__init__.py +1 -0
  139. package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/adapter.py +1320 -0
  140. package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/cli.py +2271 -0
  141. package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/config.py +138 -0
  142. package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/errors.py +67 -0
  143. package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/reporting.py +65 -0
  144. package/tools/vds-scripts/jira_orchestrator/tests/__init__.py +1 -0
  145. package/tools/vds-scripts/jira_orchestrator/tests/conftest.py +86 -0
  146. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_agile_list_payloads.py +54 -0
  147. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_bulk_operations.py +69 -0
  148. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_components.py +57 -0
  149. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_createmeta.py +45 -0
  150. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_dashboard.py +117 -0
  151. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_issue_properties.py +54 -0
  152. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_permissions_compat.py +42 -0
  153. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_reindex.py +42 -0
  154. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_remote_links.py +76 -0
  155. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_transitions.py +91 -0
  156. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_user_management.py +110 -0
  157. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_version_management.py +133 -0
  158. package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_watchers.py +41 -0
  159. package/tools/vds-scripts/jira_orchestrator/tests/test_advanced_search.py +164 -0
  160. package/tools/vds-scripts/jira_orchestrator/tests/test_agile.py +256 -0
  161. package/tools/vds-scripts/jira_orchestrator/tests/test_application_properties.py +193 -0
  162. package/tools/vds-scripts/jira_orchestrator/tests/test_backlog.py +91 -0
  163. package/tools/vds-scripts/jira_orchestrator/tests/test_bulk_operations.py +277 -0
  164. package/tools/vds-scripts/jira_orchestrator/tests/test_cli.py +106 -0
  165. package/tools/vds-scripts/jira_orchestrator/tests/test_components.py +106 -0
  166. package/tools/vds-scripts/jira_orchestrator/tests/test_config.py +164 -0
  167. package/tools/vds-scripts/jira_orchestrator/tests/test_dashboard.py +122 -0
  168. package/tools/vds-scripts/jira_orchestrator/tests/test_discover_fields.py +207 -0
  169. package/tools/vds-scripts/jira_orchestrator/tests/test_filter_management.py +333 -0
  170. package/tools/vds-scripts/jira_orchestrator/tests/test_issue_archiving.py +164 -0
  171. package/tools/vds-scripts/jira_orchestrator/tests/test_issue_links.py +257 -0
  172. package/tools/vds-scripts/jira_orchestrator/tests/test_issue_properties.py +171 -0
  173. package/tools/vds-scripts/jira_orchestrator/tests/test_link_types.py +314 -0
  174. package/tools/vds-scripts/jira_orchestrator/tests/test_parse_set.py +37 -0
  175. package/tools/vds-scripts/jira_orchestrator/tests/test_permissions.py +273 -0
  176. package/tools/vds-scripts/jira_orchestrator/tests/test_reindex.py +81 -0
  177. package/tools/vds-scripts/jira_orchestrator/tests/test_remote_links.py +254 -0
  178. package/tools/vds-scripts/jira_orchestrator/tests/test_security_schemes.py +170 -0
  179. package/tools/vds-scripts/jira_orchestrator/tests/test_transitions_changelog.py +114 -0
  180. package/tools/vds-scripts/jira_orchestrator/tests/test_user_management.py +226 -0
  181. package/tools/vds-scripts/jira_orchestrator/tests/test_version_management.py +339 -0
  182. package/tools/vds-scripts/jira_orchestrator/tests/test_watchers.py +101 -0
  183. package/tools/vds-scripts/jira_orchestrator/tests/test_worklog.py +223 -0
  184. package/tools/vds-scripts/jira_orchestrator/uv.lock +738 -0
  185. package/tools/vds-scripts/mcp_server/Dockerfile +34 -0
  186. package/tools/vds-scripts/mcp_server/README.md +140 -0
  187. package/tools/vds-scripts/mcp_server/pyproject.toml +42 -0
  188. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/__init__.py +4 -0
  189. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/config.py +36 -0
  190. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/server.py +66 -0
  191. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/__init__.py +14 -0
  192. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/bitbucket_tools.py +47 -0
  193. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/confluence_tools.py +59 -0
  194. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/git_tools.py +71 -0
  195. package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/jira_tools.py +63 -0
  196. package/tools/vds-scripts/mcp_server/tests/__init__.py +2 -0
  197. package/tools/vds-scripts/mcp_server/tests/conftest.py +29 -0
  198. package/tools/vds-scripts/mcp_server/tests/unit/__init__.py +2 -0
  199. package/tools/vds-scripts/mcp_server/tests/unit/test_bitbucket_tools.py +25 -0
  200. package/tools/vds-scripts/mcp_server/tests/unit/test_confluence_tools.py +25 -0
  201. package/tools/vds-scripts/mcp_server/tests/unit/test_git_tools.py +32 -0
  202. package/tools/vds-scripts/mcp_server/tests/unit/test_jira_tools.py +32 -0
  203. package/tools/vds-scripts/mcp_server/tests/verification/__init__.py +2 -0
  204. package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_confluence_tools.py +40 -0
  205. package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_jira_tools.py +37 -0
  206. package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_tool_registration.py +47 -0
  207. package/tools/vds-scripts/mcp_server/uv.lock +1032 -0
  208. package/tools/vds-scripts/mypy.ini +5 -0
  209. package/tools/vds-scripts/pyproject.toml +29 -0
  210. package/tools/vds-scripts/repo-manifest.yaml +273 -0
  211. package/tools/vds-scripts/repo-manifest.yaml.example +25 -0
  212. package/tools/vds-scripts/scripts/BRD-Validation-API.postman_collection.json +706 -0
  213. package/tools/vds-scripts/scripts/BRD-Validation-README.md +308 -0
  214. package/tools/vds-scripts/scripts/README.md +162 -0
  215. package/tools/vds-scripts/scripts/bootstrap_uv.sh +30 -0
  216. package/tools/vds-scripts/scripts/brd-validation-environment.json +51 -0
  217. package/tools/vds-scripts/scripts/brd-validation-test-results.json +13023 -0
  218. package/tools/vds-scripts/scripts/brd_coverage_report.json +276 -0
  219. package/tools/vds-scripts/scripts/create_memory_session.py +35 -0
  220. package/tools/vds-scripts/scripts/deployment/load_docker_images_offline.sh +90 -0
  221. package/tools/vds-scripts/scripts/final_completion_report.md +139 -0
  222. package/tools/vds-scripts/scripts/folder_structure_report.json +321 -0
  223. package/tools/vds-scripts/scripts/generate_completion_report.py +125 -0
  224. package/tools/vds-scripts/scripts/generate_intellij_modules.py +150 -0
  225. package/tools/vds-scripts/scripts/link_integrity_report.json +807 -0
  226. package/tools/vds-scripts/scripts/move_audit_artifact_pages.py +255 -0
  227. package/tools/vds-scripts/scripts/move_audit_artifact_pages_rest.py +165 -0
  228. package/tools/vds-scripts/scripts/move_wrong_dept_pages.py +216 -0
  229. package/tools/vds-scripts/scripts/save_intellij_memories.py +120 -0
  230. package/tools/vds-scripts/scripts/save_memories_to_vds_ai.py +83 -0
  231. package/tools/vds-scripts/scripts/save_memories_vds_style.py +129 -0
  232. package/tools/vds-scripts/scripts/search_intellij_memories.py +50 -0
  233. package/tools/vds-scripts/scripts/setup_intellij_workspace.py +65 -0
  234. package/tools/vds-scripts/scripts/target-state-automation/README.md +89 -0
  235. package/tools/vds-scripts/scripts/target-state-automation/confluence_sync_coordinator.sh +27 -0
  236. package/tools/vds-scripts/scripts/target-state-automation/coordination.sh +114 -0
  237. package/tools/vds-scripts/scripts/target-state-automation/diagram_coordinator.sh +25 -0
  238. package/tools/vds-scripts/scripts/target-state-automation/docs_root.sh +22 -0
  239. package/tools/vds-scripts/scripts/target-state-automation/generate_diagrams.sh +22 -0
  240. package/tools/vds-scripts/scripts/target-state-automation/markdown_coordinator.sh +25 -0
  241. package/tools/vds-scripts/scripts/target-state-automation/progress_dashboard.sh +17 -0
  242. package/tools/vds-scripts/scripts/target-state-automation/schema_coordinator.sh +25 -0
  243. package/tools/vds-scripts/scripts/target-state-automation/sync_confluence.sh +30 -0
  244. package/tools/vds-scripts/scripts/target-state-automation/update_dependencies.sh +19 -0
  245. package/tools/vds-scripts/scripts/target-state-automation/validate_links.sh +86 -0
  246. package/tools/vds-scripts/scripts/target-state-automation/validate_markdown.sh +52 -0
  247. package/tools/vds-scripts/scripts/target-state-automation/validate_schemas.sh +26 -0
  248. package/tools/vds-scripts/scripts/target-state-automation/validate_structure.sh +98 -0
  249. package/tools/vds-scripts/scripts/update_modules_xml.py +190 -0
  250. package/tools/vds-scripts/scripts/uv-workspace-alignment-verification-2026-03-25.md +128 -0
  251. package/tools/vds-scripts/scripts/validate_brd_coverage.py +179 -0
  252. package/tools/vds-scripts/scripts/validate_folder_structure.py +240 -0
  253. package/tools/vds-scripts/scripts/validate_link_integrity.py +272 -0
  254. package/tools/vds-scripts/scripts/vds_sh_helpers.sh +180 -0
  255. package/tools/vds-scripts/scripts/verification/phase2_portable_paths_ubuntu_docker.sh +26 -0
  256. package/tools/vds-scripts/scripts/worktree_uv.sh +48 -0
  257. package/tools/vds-scripts/uv.lock +8 -0
  258. package/tools/vds-scripts/vds_cli/README.md +126 -0
  259. package/tools/vds-scripts/vds_cli/VERIFICATION_REPORT.md +41 -0
  260. package/tools/vds-scripts/vds_cli/pyproject.toml +38 -0
  261. package/tools/vds-scripts/vds_cli/src/vds_cli/__init__.py +3 -0
  262. package/tools/vds-scripts/vds_cli/src/vds_cli/cli.py +173 -0
  263. package/tools/vds-scripts/vds_cli/src/vds_cli/docs_sync.py +1203 -0
  264. package/tools/vds-scripts/vds_cli/src/vds_cli/env.py +41 -0
  265. package/tools/vds-scripts/vds_cli/src/vds_cli/google_sheets_orchestrator/__init__.py +3 -0
  266. package/tools/vds-scripts/vds_cli/src/vds_cli/google_sheets_orchestrator/google_sheets_orchestrator.py +198 -0
  267. package/tools/vds-scripts/vds_cli/src/vds_cli/router.py +93 -0
  268. package/tools/vds-scripts/vds_cli/src/vds_cli/sync_api.py +647 -0
  269. package/tools/vds-scripts/vds_cli/src/vds_cli/sync_service.py +266 -0
  270. package/tools/vds-scripts/vds_cli/tests/__init__.py +2 -0
  271. package/tools/vds-scripts/vds_cli/tests/conftest.py +49 -0
  272. package/tools/vds-scripts/vds_cli/tests/unit/__init__.py +2 -0
  273. package/tools/vds-scripts/vds_cli/tests/unit/test_cli.py +143 -0
  274. package/tools/vds-scripts/vds_cli/tests/unit/test_docs_sync.py +422 -0
  275. package/tools/vds-scripts/vds_cli/tests/unit/test_env.py +51 -0
  276. package/tools/vds-scripts/vds_cli/tests/unit/test_router.py +72 -0
  277. package/tools/vds-scripts/vds_cli/tests/unit/test_sync_api.py +357 -0
  278. package/tools/vds-scripts/vds_cli/tests/unit/test_sync_service.py +160 -0
  279. package/tools/vds-scripts/vds_cli/tests/verification/__init__.py +2 -0
  280. package/tools/vds-scripts/vds_cli/tests/verification/test_bitbucket_real.py +33 -0
  281. package/tools/vds-scripts/vds_cli/tests/verification/test_confluence_real.py +35 -0
  282. package/tools/vds-scripts/vds_cli/tests/verification/test_jira_real.py +41 -0
  283. package/tools/vds-scripts/vds_cli/uv.lock +524 -0
  284. package/tools/vds-scripts/vds_cli_common/README.md +190 -0
  285. package/tools/vds-scripts/vds_cli_common/pyproject.toml +92 -0
  286. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/__init__.py +34 -0
  287. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/completers.py +139 -0
  288. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/context.py +201 -0
  289. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/env.py +119 -0
  290. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/errors.py +318 -0
  291. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/output.py +284 -0
  292. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/paths.py +78 -0
  293. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/testing.py +213 -0
  294. package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/version.py +85 -0
  295. package/tools/vds-scripts/vds_cli_common/tests/__init__.py +1 -0
  296. package/tools/vds-scripts/vds_cli_common/tests/test_completers.py +148 -0
  297. package/tools/vds-scripts/vds_cli_common/tests/test_context.py +192 -0
  298. package/tools/vds-scripts/vds_cli_common/tests/test_env.py +102 -0
  299. package/tools/vds-scripts/vds_cli_common/tests/test_errors.py +186 -0
  300. package/tools/vds-scripts/vds_cli_common/tests/test_output.py +229 -0
  301. package/tools/vds-scripts/vds_cli_common/tests/test_paths.py +61 -0
  302. package/tools/vds-scripts/vds_cli_common/tests/test_testing.py +138 -0
  303. package/tools/vds-scripts/vds_cli_common/tests/test_version.py +64 -0
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Sync local PDF files with Confluence page attachments.
4
+
5
+ This script:
6
+ 1. Lists local PDF files from a directory
7
+ 2. Lists current attachments on a Confluence page
8
+ 3. Matches PDF attachments by filename
9
+ 4. Uploads/replaces PDF files on Confluence page
10
+ 5. Optionally compares modification times with markdown sources
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ import typer
19
+ from confluence_orchestrator.config import load_settings
20
+ from confluence_orchestrator.content import ContentClient
21
+ from confluence_orchestrator.http import ConfluenceClient
22
+ from rich.console import Console
23
+ from rich.progress import Progress, SpinnerColumn, TextColumn
24
+ from rich.table import Table
25
+
26
+ app = typer.Typer(help="Sync local PDF files with Confluence page attachments")
27
+ console = Console()
28
+
29
+
30
+ def _build_http_client(settings, server: str) -> ConfluenceClient:
31
+ """Build HTTP client for Confluence."""
32
+ return ConfluenceClient(settings, server=server)
33
+
34
+
35
+ def _build_content_client(settings, server: str) -> ContentClient:
36
+ """Build content client for Confluence."""
37
+ http_client = _build_http_client(settings, server)
38
+ return ContentClient(http_client)
39
+
40
+
41
+ def find_pdf_files(directory: Path, pattern: str | None = None, recursive: bool = True) -> list[Path]:
42
+ """Find all PDF files in directory, optionally matching pattern."""
43
+ if recursive:
44
+ pdf_files = sorted(directory.rglob("*.pdf"))
45
+ else:
46
+ pdf_files = sorted(directory.glob("*.pdf"))
47
+ if pattern:
48
+ pdf_files = [f for f in pdf_files if pattern.lower() in f.name.lower()]
49
+ return pdf_files
50
+
51
+
52
+ def match_attachments(
53
+ local_pdfs: list[Path], confluence_attachments: list[dict[str, Any]]
54
+ ) -> dict[str, dict[str, Any]]:
55
+ """Match local PDF files with Confluence attachments by filename."""
56
+ matches: dict[str, dict[str, Any]] = {}
57
+
58
+ # Create lookup by filename (case-insensitive)
59
+ attachment_lookup: dict[str, dict[str, Any]] = {}
60
+ for att in confluence_attachments:
61
+ filename = att.get("title", "").lower()
62
+ attachment_lookup[filename] = att
63
+
64
+ # Match local files to attachments
65
+ for local_file in local_pdfs:
66
+ filename_lower = local_file.name.lower()
67
+ if filename_lower in attachment_lookup:
68
+ matches[local_file.name] = {
69
+ "local_file": local_file,
70
+ "attachment": attachment_lookup[filename_lower],
71
+ "action": "replace",
72
+ }
73
+ else:
74
+ matches[local_file.name] = {
75
+ "local_file": local_file,
76
+ "attachment": None,
77
+ "action": "upload",
78
+ }
79
+
80
+ return matches
81
+
82
+
83
+ @app.command("sync")
84
+ def sync_pdfs(
85
+ page_id: str = typer.Argument(..., help="Confluence page ID"),
86
+ pdf_directory: Path = typer.Option(..., "--dir", "-d", help="Directory containing PDF files"),
87
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
88
+ pattern: str | None = typer.Option(None, "--pattern", "-p", help="Filter PDF files by pattern"),
89
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be done without making changes"),
90
+ comment: str | None = typer.Option(None, "--comment", "-c", help="Comment for attachment updates"),
91
+ yes: bool = typer.Option(False, "--yes", "-y", help="Auto-confirm without prompting"),
92
+ ) -> None:
93
+ """Sync local PDF files with Confluence page attachments."""
94
+
95
+ if not pdf_directory.exists() or not pdf_directory.is_dir():
96
+ console.print(f"[red]Error: Directory not found: {pdf_directory}[/red]")
97
+ raise typer.Exit(code=1) from None
98
+
99
+ try:
100
+ settings = load_settings(strict=True)
101
+ server_key = server or settings.default_server
102
+ client = _build_content_client(settings, server_key)
103
+
104
+ # Get page info
105
+ with console.status("[bold green]Fetching Confluence page info..."):
106
+ page_info = client.get_page(page_id)
107
+ page_title = page_info.get("title", "Unknown")
108
+
109
+ console.print(f"\n[bold]Page:[/bold] {page_title} (ID: {page_id})")
110
+ console.print(f"[bold]Server:[/bold] {server_key}\n")
111
+
112
+ # List local PDF files
113
+ console.print(f"[bold]Scanning PDF files in:[/bold] {pdf_directory}")
114
+ local_pdfs = find_pdf_files(pdf_directory, pattern)
115
+
116
+ if not local_pdfs:
117
+ console.print("[yellow]No PDF files found in directory[/yellow]")
118
+ raise typer.Exit(code=0) from None
119
+
120
+ console.print(f"Found [green]{len(local_pdfs)}[/green] PDF file(s):")
121
+ for pdf in local_pdfs:
122
+ size = pdf.stat().st_size
123
+ console.print(f" • {pdf.name} ({size:,} bytes)")
124
+
125
+ # List Confluence attachments
126
+ console.print("\n[bold]Fetching Confluence attachments...[/bold]")
127
+ attachments_result = client.list_attachments(page_id)
128
+ # Handle both list and dict responses
129
+ if isinstance(attachments_result, list):
130
+ attachments = attachments_result
131
+ elif isinstance(attachments_result, dict):
132
+ attachments = attachments_result.get("results", [])
133
+ else:
134
+ attachments = []
135
+
136
+ # Filter PDF attachments
137
+ pdf_attachments = [
138
+ att for att in attachments
139
+ if att.get("title", "").lower().endswith(".pdf")
140
+ ]
141
+
142
+ console.print(f"Found [green]{len(pdf_attachments)}[/green] PDF attachment(s) on page:")
143
+ for att in pdf_attachments:
144
+ name = att.get("title", "Unknown")
145
+ size = att.get("size", 0)
146
+ att_id = att.get("id", "Unknown")
147
+ console.print(f" • {name} (ID: {att_id}, {size:,} bytes)")
148
+
149
+ # Match local files with attachments
150
+ console.print("\n[bold]Matching files...[/bold]")
151
+ matches = match_attachments(local_pdfs, pdf_attachments)
152
+
153
+ # Display sync plan
154
+ table = Table(title="Sync Plan", show_header=True, header_style="bold magenta")
155
+ table.add_column("Local File", style="cyan")
156
+ table.add_column("Action", style="yellow")
157
+ table.add_column("Confluence Attachment", style="green")
158
+ table.add_column("Size", justify="right")
159
+
160
+ replace_count = 0
161
+ upload_count = 0
162
+
163
+ for filename, match_info in matches.items():
164
+ local_file = match_info["local_file"]
165
+ attachment = match_info["attachment"]
166
+ action = match_info["action"]
167
+ size = local_file.stat().st_size
168
+
169
+ if action == "replace":
170
+ replace_count += 1
171
+ att_name = attachment.get("title", "Unknown") if attachment else "N/A"
172
+ table.add_row(filename, "[yellow]REPLACE[/yellow]", att_name, f"{size:,} bytes")
173
+ else:
174
+ upload_count += 1
175
+ table.add_row(filename, "[green]UPLOAD NEW[/green]", "—", f"{size:,} bytes")
176
+
177
+ console.print(table)
178
+ console.print(f"\n[bold]Summary:[/bold] {replace_count} to replace, {upload_count} to upload")
179
+
180
+ if dry_run:
181
+ console.print("\n[yellow]DRY RUN: No changes made[/yellow]")
182
+ raise typer.Exit(code=0) from None
183
+
184
+ # Confirm before proceeding
185
+ if not yes:
186
+ if not typer.confirm("\n[bold]Proceed with sync?[/bold]"):
187
+ console.print("[yellow]Cancelled[/yellow]")
188
+ raise typer.Exit(code=0) from None
189
+
190
+ # Perform sync
191
+ console.print("\n[bold]Syncing attachments...[/bold]")
192
+
193
+ with Progress(
194
+ SpinnerColumn(),
195
+ TextColumn("[progress.description]{task.description}"),
196
+ console=console,
197
+ ) as progress:
198
+ # Replace existing attachments
199
+ for filename, match_info in matches.items():
200
+ if match_info["action"] == "replace":
201
+ local_file = match_info["local_file"]
202
+ attachment = match_info["attachment"]
203
+ att_id = attachment.get("id")
204
+
205
+ task = progress.add_task(f"Replacing {filename}...", total=None)
206
+
207
+ # Update existing attachment
208
+ try:
209
+ client.update_attachment(
210
+ page_id,
211
+ att_id,
212
+ str(local_file),
213
+ filename=local_file.name,
214
+ content_type="application/pdf",
215
+ comment=comment or f"Updated from local file: {local_file.name}",
216
+ )
217
+ progress.update(task, completed=True)
218
+ console.print(f" [green]✓[/green] Replaced {filename}")
219
+ except Exception as e:
220
+ progress.update(task, completed=True)
221
+ console.print(f" [red]✗[/red] Failed to replace {filename}: {e}")
222
+
223
+ # Upload new attachments
224
+ for filename, match_info in matches.items():
225
+ if match_info["action"] == "upload":
226
+ local_file = match_info["local_file"]
227
+
228
+ task = progress.add_task(f"Uploading {filename}...", total=None)
229
+
230
+ try:
231
+ client.upload_attachment(
232
+ page_id,
233
+ str(local_file),
234
+ filename=local_file.name,
235
+ content_type="application/pdf",
236
+ comment=comment or f"Uploaded: {local_file.name}",
237
+ )
238
+ progress.update(task, completed=True)
239
+ console.print(f" [green]✓[/green] Uploaded {filename}")
240
+ except Exception as e:
241
+ progress.update(task, completed=True)
242
+ console.print(f" [red]✗[/red] Failed to upload {filename}: {e}")
243
+
244
+ console.print("\n[bold green]Sync completed![/bold green]")
245
+
246
+ except Exception as exc:
247
+ console.print(f"[red]Error: {exc}[/red]")
248
+ raise typer.Exit(code=1) from None
249
+
250
+
251
+ @app.command("list")
252
+ def list_attachments(
253
+ page_id: str = typer.Argument(..., help="Confluence page ID"),
254
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
255
+ pdf_only: bool = typer.Option(False, "--pdf-only", help="Show only PDF attachments"),
256
+ ) -> None:
257
+ """List attachments on a Confluence page."""
258
+
259
+ try:
260
+ settings = load_settings(strict=True)
261
+ server_key = server or settings.default_server
262
+ client = _build_content_client(settings, server_key)
263
+
264
+ # Get page info
265
+ page_info = client.get_page(page_id)
266
+ page_title = page_info.get("title", "Unknown")
267
+
268
+ console.print(f"\n[bold]Page:[/bold] {page_title} (ID: {page_id})")
269
+ console.print(f"[bold]Server:[/bold] {server_key}\n")
270
+
271
+ # List attachments
272
+ attachments_result = client.list_attachments(page_id)
273
+ attachments = attachments_result if isinstance(attachments_result, list) else attachments_result.get("results", [])
274
+
275
+ if pdf_only:
276
+ attachments = [att for att in attachments if att.get("title", "").lower().endswith(".pdf")]
277
+
278
+ if not attachments:
279
+ console.print("[yellow]No attachments found[/yellow]")
280
+ raise typer.Exit(code=0) from None
281
+
282
+ table = Table(title="Attachments", show_header=True, header_style="bold magenta")
283
+ table.add_column("Filename", style="cyan")
284
+ table.add_column("ID", style="green")
285
+ table.add_column("Size", justify="right")
286
+ table.add_column("Type", style="yellow")
287
+
288
+ for att in attachments:
289
+ name = att.get("title", "Unknown")
290
+ att_id = att.get("id", "Unknown")
291
+ size = att.get("size", 0)
292
+ mime_type = att.get("metadata", {}).get("mediaType", "unknown")
293
+
294
+ table.add_row(name, att_id, f"{size:,} bytes", mime_type)
295
+
296
+ console.print(table)
297
+
298
+ except Exception as exc:
299
+ console.print(f"[red]Error: {exc}[/red]")
300
+ raise typer.Exit(code=1) from None
301
+
302
+
303
+ if __name__ == "__main__":
304
+ app()
305
+
@@ -0,0 +1,305 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Sync local PNG files with Confluence page attachments.
4
+
5
+ This script:
6
+ 1. Lists local PNG files from a directory
7
+ 2. Lists current attachments on a Confluence page
8
+ 3. Matches PNG attachments by filename
9
+ 4. Uploads/replaces PNG files on Confluence page
10
+ 5. Optionally rearranges attachments to match local order
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+ import typer
19
+ from confluence_orchestrator.config import load_settings
20
+ from confluence_orchestrator.content import ContentClient
21
+ from confluence_orchestrator.http import ConfluenceClient
22
+ from rich.console import Console
23
+ from rich.progress import Progress, SpinnerColumn, TextColumn
24
+ from rich.table import Table
25
+
26
+ app = typer.Typer(help="Sync local PNG files with Confluence page attachments")
27
+ console = Console()
28
+
29
+
30
+ def _build_http_client(settings, server: str) -> ConfluenceClient:
31
+ """Build HTTP client for Confluence."""
32
+ return ConfluenceClient(settings, server=server)
33
+
34
+
35
+ def _build_content_client(settings, server: str) -> ContentClient:
36
+ """Build content client for Confluence."""
37
+ http_client = _build_http_client(settings, server)
38
+ return ContentClient(http_client)
39
+
40
+
41
+ def find_png_files(directory: Path, pattern: str | None = None, recursive: bool = True) -> list[Path]:
42
+ """Find all PNG files in directory, optionally matching pattern."""
43
+ if recursive:
44
+ png_files = sorted(directory.rglob("*.png"))
45
+ else:
46
+ png_files = sorted(directory.glob("*.png"))
47
+ if pattern:
48
+ png_files = [f for f in png_files if pattern.lower() in f.name.lower()]
49
+ return png_files
50
+
51
+
52
+ def match_attachments(
53
+ local_pngs: list[Path], confluence_attachments: list[dict[str, Any]]
54
+ ) -> dict[str, dict[str, Any]]:
55
+ """Match local PNG files with Confluence attachments by filename."""
56
+ matches: dict[str, dict[str, Any]] = {}
57
+
58
+ # Create lookup by filename (case-insensitive)
59
+ attachment_lookup: dict[str, dict[str, Any]] = {}
60
+ for att in confluence_attachments:
61
+ filename = att.get("title", "").lower()
62
+ attachment_lookup[filename] = att
63
+
64
+ # Match local files to attachments
65
+ for local_file in local_pngs:
66
+ filename_lower = local_file.name.lower()
67
+ if filename_lower in attachment_lookup:
68
+ matches[local_file.name] = {
69
+ "local_file": local_file,
70
+ "attachment": attachment_lookup[filename_lower],
71
+ "action": "replace",
72
+ }
73
+ else:
74
+ matches[local_file.name] = {
75
+ "local_file": local_file,
76
+ "attachment": None,
77
+ "action": "upload",
78
+ }
79
+
80
+ return matches
81
+
82
+
83
+ @app.command("sync")
84
+ def sync_pngs(
85
+ page_id: str = typer.Argument(..., help="Confluence page ID"),
86
+ png_directory: Path = typer.Option(..., "--dir", "-d", help="Directory containing PNG files"),
87
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
88
+ pattern: str | None = typer.Option(None, "--pattern", "-p", help="Filter PNG files by pattern"),
89
+ dry_run: bool = typer.Option(False, "--dry-run", help="Show what would be done without making changes"),
90
+ comment: str | None = typer.Option(None, "--comment", "-c", help="Comment for attachment updates"),
91
+ yes: bool = typer.Option(False, "--yes", "-y", help="Auto-confirm without prompting"),
92
+ ) -> None:
93
+ """Sync local PNG files with Confluence page attachments."""
94
+
95
+ if not png_directory.exists() or not png_directory.is_dir():
96
+ console.print(f"[red]Error: Directory not found: {png_directory}[/red]")
97
+ raise typer.Exit(code=1) from None
98
+
99
+ try:
100
+ settings = load_settings(strict=True)
101
+ server_key = server or settings.default_server
102
+ client = _build_content_client(settings, server_key)
103
+
104
+ # Get page info
105
+ with console.status("[bold green]Fetching Confluence page info..."):
106
+ page_info = client.get_page(page_id)
107
+ page_title = page_info.get("title", "Unknown")
108
+
109
+ console.print(f"\n[bold]Page:[/bold] {page_title} (ID: {page_id})")
110
+ console.print(f"[bold]Server:[/bold] {server_key}\n")
111
+
112
+ # List local PNG files
113
+ console.print(f"[bold]Scanning PNG files in:[/bold] {png_directory}")
114
+ local_pngs = find_png_files(png_directory, pattern)
115
+
116
+ if not local_pngs:
117
+ console.print("[yellow]No PNG files found in directory[/yellow]")
118
+ raise typer.Exit(code=0) from None
119
+
120
+ console.print(f"Found [green]{len(local_pngs)}[/green] PNG file(s):")
121
+ for png in local_pngs:
122
+ size = png.stat().st_size
123
+ console.print(f" • {png.name} ({size:,} bytes)")
124
+
125
+ # List Confluence attachments
126
+ console.print("\n[bold]Fetching Confluence attachments...[/bold]")
127
+ attachments_result = client.list_attachments(page_id)
128
+ # Handle both list and dict responses
129
+ if isinstance(attachments_result, list):
130
+ attachments = attachments_result
131
+ elif isinstance(attachments_result, dict):
132
+ attachments = attachments_result.get("results", [])
133
+ else:
134
+ attachments = []
135
+
136
+ # Filter PNG attachments
137
+ png_attachments = [
138
+ att for att in attachments
139
+ if att.get("title", "").lower().endswith(".png")
140
+ ]
141
+
142
+ console.print(f"Found [green]{len(png_attachments)}[/green] PNG attachment(s) on page:")
143
+ for att in png_attachments:
144
+ name = att.get("title", "Unknown")
145
+ size = att.get("size", 0)
146
+ att_id = att.get("id", "Unknown")
147
+ console.print(f" • {name} (ID: {att_id}, {size:,} bytes)")
148
+
149
+ # Match local files with attachments
150
+ console.print("\n[bold]Matching files...[/bold]")
151
+ matches = match_attachments(local_pngs, png_attachments)
152
+
153
+ # Display sync plan
154
+ table = Table(title="Sync Plan", show_header=True, header_style="bold magenta")
155
+ table.add_column("Local File", style="cyan")
156
+ table.add_column("Action", style="yellow")
157
+ table.add_column("Confluence Attachment", style="green")
158
+ table.add_column("Size", justify="right")
159
+
160
+ replace_count = 0
161
+ upload_count = 0
162
+
163
+ for filename, match_info in matches.items():
164
+ local_file = match_info["local_file"]
165
+ attachment = match_info["attachment"]
166
+ action = match_info["action"]
167
+ size = local_file.stat().st_size
168
+
169
+ if action == "replace":
170
+ replace_count += 1
171
+ att_name = attachment.get("title", "Unknown") if attachment else "N/A"
172
+ table.add_row(filename, "[yellow]REPLACE[/yellow]", att_name, f"{size:,} bytes")
173
+ else:
174
+ upload_count += 1
175
+ table.add_row(filename, "[green]UPLOAD NEW[/green]", "—", f"{size:,} bytes")
176
+
177
+ console.print(table)
178
+ console.print(f"\n[bold]Summary:[/bold] {replace_count} to replace, {upload_count} to upload")
179
+
180
+ if dry_run:
181
+ console.print("\n[yellow]DRY RUN: No changes made[/yellow]")
182
+ raise typer.Exit(code=0) from None
183
+
184
+ # Confirm before proceeding
185
+ if not yes:
186
+ if not typer.confirm("\n[bold]Proceed with sync?[/bold]"):
187
+ console.print("[yellow]Cancelled[/yellow]")
188
+ raise typer.Exit(code=0) from None
189
+
190
+ # Perform sync
191
+ console.print("\n[bold]Syncing attachments...[/bold]")
192
+
193
+ with Progress(
194
+ SpinnerColumn(),
195
+ TextColumn("[progress.description]{task.description}"),
196
+ console=console,
197
+ ) as progress:
198
+ # Replace existing attachments
199
+ for filename, match_info in matches.items():
200
+ if match_info["action"] == "replace":
201
+ local_file = match_info["local_file"]
202
+ attachment = match_info["attachment"]
203
+ att_id = attachment.get("id")
204
+
205
+ task = progress.add_task(f"Replacing {filename}...", total=None)
206
+
207
+ # Update existing attachment
208
+ try:
209
+ client.update_attachment(
210
+ page_id,
211
+ att_id,
212
+ str(local_file),
213
+ filename=local_file.name,
214
+ content_type="image/png",
215
+ comment=comment or f"Updated from local file: {local_file.name}",
216
+ )
217
+ progress.update(task, completed=True)
218
+ console.print(f" [green]✓[/green] Replaced {filename}")
219
+ except Exception as e:
220
+ progress.update(task, completed=True)
221
+ console.print(f" [red]✗[/red] Failed to replace {filename}: {e}")
222
+
223
+ # Upload new attachments
224
+ for filename, match_info in matches.items():
225
+ if match_info["action"] == "upload":
226
+ local_file = match_info["local_file"]
227
+
228
+ task = progress.add_task(f"Uploading {filename}...", total=None)
229
+
230
+ try:
231
+ client.upload_attachment(
232
+ page_id,
233
+ str(local_file),
234
+ filename=local_file.name,
235
+ content_type="image/png",
236
+ comment=comment or f"Uploaded: {local_file.name}",
237
+ )
238
+ progress.update(task, completed=True)
239
+ console.print(f" [green]✓[/green] Uploaded {filename}")
240
+ except Exception as e:
241
+ progress.update(task, completed=True)
242
+ console.print(f" [red]✗[/red] Failed to upload {filename}: {e}")
243
+
244
+ console.print("\n[bold green]Sync completed![/bold green]")
245
+
246
+ except Exception as exc:
247
+ console.print(f"[red]Error: {exc}[/red]")
248
+ raise typer.Exit(code=1) from None
249
+
250
+
251
+ @app.command("list")
252
+ def list_attachments(
253
+ page_id: str = typer.Argument(..., help="Confluence page ID"),
254
+ server: str | None = typer.Option(None, "--server", "-s", help="internal|external"),
255
+ png_only: bool = typer.Option(False, "--png-only", help="Show only PNG attachments"),
256
+ ) -> None:
257
+ """List attachments on a Confluence page."""
258
+
259
+ try:
260
+ settings = load_settings(strict=True)
261
+ server_key = server or settings.default_server
262
+ client = _build_content_client(settings, server_key)
263
+
264
+ # Get page info
265
+ page_info = client.get_page(page_id)
266
+ page_title = page_info.get("title", "Unknown")
267
+
268
+ console.print(f"\n[bold]Page:[/bold] {page_title} (ID: {page_id})")
269
+ console.print(f"[bold]Server:[/bold] {server_key}\n")
270
+
271
+ # List attachments
272
+ attachments_result = client.list_attachments(page_id)
273
+ attachments = attachments_result if isinstance(attachments_result, list) else attachments_result.get("results", [])
274
+
275
+ if png_only:
276
+ attachments = [att for att in attachments if att.get("title", "").lower().endswith(".png")]
277
+
278
+ if not attachments:
279
+ console.print("[yellow]No attachments found[/yellow]")
280
+ raise typer.Exit(code=0) from None
281
+
282
+ table = Table(title="Attachments", show_header=True, header_style="bold magenta")
283
+ table.add_column("Filename", style="cyan")
284
+ table.add_column("ID", style="green")
285
+ table.add_column("Size", justify="right")
286
+ table.add_column("Type", style="yellow")
287
+
288
+ for att in attachments:
289
+ name = att.get("title", "Unknown")
290
+ att_id = att.get("id", "Unknown")
291
+ size = att.get("size", 0)
292
+ mime_type = att.get("metadata", {}).get("mediaType", "unknown")
293
+
294
+ table.add_row(name, att_id, f"{size:,} bytes", mime_type)
295
+
296
+ console.print(table)
297
+
298
+ except Exception as exc:
299
+ console.print(f"[red]Error: {exc}[/red]")
300
+ raise typer.Exit(code=1) from None
301
+
302
+
303
+ if __name__ == "__main__":
304
+ app()
305
+
@@ -0,0 +1,8 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from pathlib import Path
5
+
6
+ SRC_PATH = Path(__file__).resolve().parents[1] / "src"
7
+ if str(SRC_PATH) not in sys.path:
8
+ sys.path.insert(0, str(SRC_PATH))